n2words
Version:
Convert numbers to words in 70+ languages with zero dependencies. Supports BigInt, decimals, and browser/Node.js environments.
262 lines (221 loc) • 8.29 kB
JavaScript
/**
* Persian (Iran) language converter
*
* CLDR: fa-IR | Persian as used in Iran
*
* Key features:
* - "و" (and) conjunction for compound numbers
* - Omit "یک" (one) before thousand
* - Pre-composed hundreds (دویست, سيصد, etc.)
*/
import { parseCardinalValue } from './utils/parse-cardinal.js'
import { parseCurrencyValue } from './utils/parse-currency.js'
import { parseOrdinalValue } from './utils/parse-ordinal.js'
import { UNBOUNDED } from './utils/scale.js'
// No fixed scale ceiling — the speller composes every magnitude.
export const cardinalMax = UNBOUNDED
export const ordinalMax = UNBOUNDED
export const currencyMax = UNBOUNDED
// ============================================================================
// Vocabulary
// ============================================================================
/** @type {Record<number, string>} */
const ONES = { 1: 'یک', 2: 'دو', 3: 'سه', 4: 'چهار', 5: 'پنج', 6: 'شش', 7: 'هفت', 8: 'هشت', 9: 'نه' }
/** @type {Record<number, string>} */
const TEENS = { 10: 'ده', 11: 'یازده', 12: 'دوازده', 13: 'سیزده', 14: 'چهارده', 15: 'پانزده', 16: 'شانزده', 17: 'هفده', 18: 'هجده', 19: 'نوزده' }
/** @type {Record<number, string>} */
const TENS = { 20: 'بیست', 30: 'سی', 40: 'چهل', 50: 'پنجاه', 60: 'شصت', 70: 'هفتاد', 80: 'هشتاد', 90: 'نود' }
/** @type {Record<number, string>} */
const HUNDREDS = { 100: 'صد', 200: 'دویست', 300: 'سيصد', 400: 'چهار صد', 500: 'پانصد', 600: 'ششصد', 700: 'هفتصد', 800: 'هشتصد', 900: 'نهصد' }
const THOUSAND = 'هزار'
const MILLION = 'میلیون'
const MILLIARD = 'میلیارد'
const ZERO = 'صفر'
const NEGATIVE = 'منفى'
const DECIMAL_SEP = 'ممیّز'
// ============================================================================
// Ordinal Vocabulary
// ============================================================================
// Persian ordinals: add -ُم (-om) suffix to cardinal
// Special forms for 1st, 2nd, 3rd
const ORDINAL_SUFFIX = 'م' // ـُم (-om)
/** @type {Record<number, string>} */
const ORDINAL_ONES = {
1: 'اول', // avval (first)
2: 'دوم', // dovvom (second)
3: 'سوم', // sevvom (third)
4: 'چهارم',
5: 'پنجم',
6: 'ششم',
7: 'هفتم',
8: 'هشتم',
9: 'نهم',
}
// ============================================================================
// Currency Vocabulary (Iranian Rial)
// ============================================================================
const RIAL = 'ریال'
// ============================================================================
// Conversion Functions
// ============================================================================
/**
* Converts a non-negative integer to Persian cardinal words.
* @param {bigint} n - Non-negative integer to convert
* @returns {string} Persian cardinal words
*/
function integerToWords(n) {
if (n === 0n) return ZERO
// 1-9
if (n <= 9n) {
return ONES[Number(n)]
}
// 10-19
if (n <= 19n) {
return TEENS[Number(n)]
}
// 20-99
if (n < 100n) {
const ones = n % 10n
const tens = n - ones
if (ones === 0n) {
return TENS[Number(tens)]
}
return `${TENS[Number(tens)]} و ${ONES[Number(ones)]}`
}
// 100-999
if (n < 1000n) {
const hundreds = 100n * (n / 100n)
const remainder = n - hundreds
if (remainder === 0n) {
return HUNDREDS[Number(hundreds)]
}
return `${HUNDREDS[Number(hundreds)]} و ${integerToWords(remainder)}`
}
// 1000-999999
if (n < 1_000_000n) {
const thousandMultiplier = n / 1000n
// Persian omits "one" before thousand: 1000 is just "هزار", not "یک هزار"
const thousandPrefix = thousandMultiplier === 1n
? ''
: integerToWords(thousandMultiplier) + ' '
const remainder = n % 1000n
const suffix = remainder === 0n ? '' : ' ' + integerToWords(remainder)
return `${thousandPrefix}${THOUSAND}${suffix}`
}
// 1000000-999999999 (millions)
if (n < 1_000_000_000n) {
const millionMultiplier = n / 1_000_000n
const millionPrefix = integerToWords(millionMultiplier) + ' ' + MILLION
const remainder = n % 1_000_000n
const suffix = remainder === 0n ? '' : ' و ' + integerToWords(remainder)
return `${millionPrefix}${suffix}`
}
// 1000000000+ (milliards - 10^9)
const milliardMultiplier = n / 1_000_000_000n
const milliardPrefix = integerToWords(milliardMultiplier) + ' ' + MILLIARD
const remainder = n % 1_000_000_000n
const suffix = remainder === 0n ? '' : ' و ' + integerToWords(remainder)
return `${milliardPrefix}${suffix}`
}
/**
* Converts the fractional digits of a number to Persian words.
* @param {string} decimalPart - The fractional digits as a string
* @returns {string} Persian words for the decimal portion
*/
function decimalPartToWords(decimalPart) {
let result = ''
let i = 0
while (i < decimalPart.length && decimalPart[i] === '0') {
if (result) result += ' '
result += ZERO
i++
}
const remainder = decimalPart.slice(i)
if (remainder) {
if (result) result += ' '
result += integerToWords(BigInt(remainder))
}
return result
}
/**
* Converts a numeric value to Persian words.
* @param {number | string | bigint} value - The numeric value to convert
* @returns {string} The number in Persian words
*/
function toCardinal(value) {
const { isNegative, integerPart, decimalPart } = parseCardinalValue(value)
let result = ''
if (isNegative) {
result = NEGATIVE + ' '
}
result += integerToWords(integerPart)
if (decimalPart) {
result += ' ' + DECIMAL_SEP + ' ' + decimalPartToWords(decimalPart)
}
return result
}
// ============================================================================
// ORDINAL: toOrdinal(value)
// ============================================================================
/**
* Converts a non-negative integer to Persian ordinal words.
*
* Persian ordinals: اول (1st), دوم (2nd), سوم (3rd), then cardinal + م suffix.
* @param {bigint} n - Positive integer to convert
* @returns {string} Persian ordinal words
*/
function integerToOrdinal(n) {
// Special forms for 1-9
if (n >= 1n && n <= 9n) {
return ORDINAL_ONES[Number(n)]
}
// For 10+, add -م suffix to cardinal
const cardinal = integerToWords(n)
return cardinal + ORDINAL_SUFFIX
}
/**
* Converts a numeric value to Persian ordinal words.
* @param {number | string | bigint} value - The numeric value to convert (positive integer)
* @returns {string} The number as ordinal words
* @throws {TypeError} If value is not a valid numeric type
* @throws {RangeError} If value is negative, zero, or has a decimal part
* @example
* toOrdinal(1) // 'اول'
* toOrdinal(2) // 'دوم'
* toOrdinal(10) // 'دهم'
*/
function toOrdinal(value) {
const integerPart = parseOrdinalValue(value)
return integerToOrdinal(integerPart)
}
// ============================================================================
// CURRENCY: toCurrency(value)
// ============================================================================
/**
* Converts a numeric value to Persian currency words (Rial).
*
* Iranian Rial has no subunit in modern usage.
* (Historically dinar was 1/100 rial, but not used today)
* @param {number | string | bigint} value - The currency amount to convert
* @returns {string} The amount in Persian currency words
* @throws {TypeError} If value is not a valid numeric type
* @throws {Error} If value is not a valid number format
* @example
* toCurrency(42) // 'چهل و دو ریال'
* toCurrency(1000) // 'هزار ریال'
* toCurrency(-5) // 'منفى پنج ریال'
*/
function toCurrency(value) {
const { isNegative, dollars: rial } = parseCurrencyValue(value)
let result = ''
if (isNegative) {
result = NEGATIVE + ' '
}
result += integerToWords(rial)
result += ' ' + RIAL
return result
}
// ============================================================================
// Exports
// ============================================================================
export { toCardinal, toOrdinal, toCurrency }