UNPKG

n2words

Version:

Convert numbers to words in 70+ languages with zero dependencies. Supports BigInt, decimals, and browser/Node.js environments.

420 lines (359 loc) 11.5 kB
/** * Yoruba (Nigeria) language converter * * CLDR: yo-NG | Yoruba as used in Nigeria * * Yoruba uses a complex vigesimal (base-20) system with: * - Additive patterns: 1-4 added to decade (lé = "plus") * - Subtractive patterns: 5-9 subtracted from next decade (dín = "minus") * - Odd decades (30,50,70,90) formed by subtracting 10 from next even decade * - Even decades (20,40,60,80,100) are multiples of 20 * * Examples: * - 21 = ọ̀kan lé lógún (1 + 20) * - 15 = àrùndínlógún (20 - 5) * - 50 = àádọ́ta (60 - 10) * - 45 = àrùndínláàádọ́ta (50 - 5) */ 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 vigesimal speller composes every magnitude. export const cardinalMax = UNBOUNDED export const ordinalMax = UNBOUNDED export const currencyMax = UNBOUNDED // ============================================================================ // Vocabulary (module-level constants) // ============================================================================ // Basic numbers 1-10 const ONES = [ '', 'ọ̀kan', // 1 'èjì', // 2 'ẹ̀ta', // 3 'ẹ̀rin', // 4 'àrùn', // 5 'ẹ̀fà', // 6 'èje', // 7 'ẹ̀jọ', // 8 'ẹ̀sán', // 9 'ẹ̀wá', // 10 ] // Numbers 11-14 (additive: X + 10, using "lá") const TEENS_ADD = [ '', 'ọ̀kànlá', // 11 = 1 + 10 'èjìlá', // 12 = 2 + 10 'ẹ̀talá', // 13 = 3 + 10 'ẹ̀rinlá', // 14 = 4 + 10 ] // Numbers 15-19 (subtractive: 20 - X, using "dín") const TEENS_SUB = [ 'àrùndínlógún', // 15 = 20 - 5 'ẹ̀rìndínlógún', // 16 = 20 - 4 'ẹ̀tadínlógún', // 17 = 20 - 3 'èjìdínlógún', // 18 = 20 - 2 'ọ̀kàndínlógún', // 19 = 20 - 1 ] // Decades (base-20 structure) // Even decades are multiples of 20 // Odd decades subtract 10 from next even decade /** @type {Record<number, string>} */ const DECADES = { 20: 'ogún', // 20 = 20 × 1 30: 'ọgbọ̀n', // 30 = 20 + 10 (special word) 40: 'ogójì', // 40 = 20 × 2 50: 'àádọ́ta', // 50 = 60 - 10 60: 'ogóta', // 60 = 20 × 3 70: 'àádọ́rin', // 70 = 80 - 10 80: 'ogórin', // 80 = 20 × 4 90: 'àádọ́rùn', // 90 = 100 - 10 100: 'ọgọ́rùn', // 100 = 20 × 5 } // Prefixes for adding to decades (lé lógún, lé lọgbọ̀n, etc.) /** @type {Record<number, string>} */ const DECADE_ADD_SUFFIX = { 20: 'lógún', 30: 'lọgbọ̀n', 40: 'lógójì', 50: 'láàádọ́ta', 60: 'lógóta', 70: 'láàádọ́rin', 80: 'lógórin', 90: 'láàádọ́rùn', 100: 'lọ́gọ́rùn', } // Prefixes for subtracting from decades (dín lógójì, etc.) /** @type {Record<number, string>} */ const DECADE_SUB_SUFFIX = { 20: 'dínlógún', 30: 'dínlọgbọ̀n', 40: 'dínlógójì', 50: 'dínláàádọ́ta', 60: 'dínlógóta', 70: 'dínláàádọ́rin', 80: 'dínlógórin', 90: 'dínláàádọ́rùn', 100: 'dínlọ́gọ́rùn', } // Scale words const HUNDRED = 'ọgọ́rùn' const TWO_HUNDRED = 'igba' // 200 (special word, historically 200 cowries) const FOUR_HUNDRED = 'irinwó' // 400 (20 × 20) const THOUSAND = 'ẹgbẹ̀rún' // 1000 const TEN_THOUSAND = 'ẹgbàárùn' // 10,000 (special) const TWENTY_THOUSAND = 'ọ̀kẹ́' // 20,000 (bag of cowries) const MILLION = 'mílíọ̀nù' // million (loanword) const ZERO = 'òdo' const NEGATIVE = 'àìní' const DECIMAL_SEP = 'àmì' const AND = 'ó lé' // "and" / "plus" connector // ============================================================================ // Ordinal Vocabulary // ============================================================================ // Yoruba ordinals use "ìkẹ-" prefix: ìkẹta (3rd), ìkẹrin (4th) // First and second have special forms const ORDINAL_FIRST = 'àkọ́kọ́' // first const ORDINAL_SECOND = 'ìkejì' // second const ORDINAL_PREFIX = 'ìkẹ' // ============================================================================ // Currency Vocabulary (Nigerian Naira) // ============================================================================ const NAIRA = 'náírà' const KOBO = 'kọ́bọ̀' // ============================================================================ // Segment Building // ============================================================================ /** * Builds word for numbers 0-99 * @param {number} n - Integer in the range 0-99 * @returns {string} Yoruba words for the number */ function buildUnder100(n) { if (n === 0) return '' if (n <= 10) return ONES[n] // 11-14: additive from 10 if (n <= 14) return TEENS_ADD[n - 10] // 15-19: subtractive from 20 if (n <= 19) return TEENS_SUB[n - 15] // Exact decades if (n % 10 === 0) return DECADES[n] const decade = Math.trunc(n / 10) * 10 const unit = n % 10 // 1-4 are added to current decade (21 = 1 + 20, not 1 + 30) if (unit <= 4) { return ONES[unit] + ' lé ' + DECADE_ADD_SUFFIX[decade] } // 5-9 are subtracted from next decade const nextDecade = decade + 10 const subtractAmount = 10 - unit return ONES[subtractAmount] + DECADE_SUB_SUFFIX[nextDecade] } // ============================================================================ // Conversion Functions // ============================================================================ /** * Converts hundreds (100-999) * @param {number} n - Integer in the range 0-999 * @returns {string} Yoruba words for the number */ function convertHundreds(n) { if (n < 100) return buildUnder100(n) const hundreds = Math.trunc(n / 100) const remainder = n % 100 let result // Special cases for 200 and 400 if (hundreds === 2 && remainder === 0) { return TWO_HUNDRED } if (hundreds === 4 && remainder === 0) { return FOUR_HUNDRED } // Build hundreds if (hundreds === 1) { result = HUNDRED } else if (hundreds === 2) { result = TWO_HUNDRED } else if (hundreds === 4) { result = FOUR_HUNDRED } else { // Other hundreds: X ọgọ́rùn result = ONES[hundreds] + ' ' + HUNDRED } if (remainder > 0) { result += ' ' + AND + ' ' + buildUnder100(remainder) } return result } /** * Converts a non-negative integer to Yoruba words. * @param {bigint} n - Non-negative integer to convert * @returns {string} Yoruba words */ function integerToWords(n) { if (n === 0n) return ZERO // Fast path: numbers < 100 if (n < 100n) { return buildUnder100(Number(n)) } // Numbers < 1000 if (n < 1000n) { return convertHundreds(Number(n)) } // Build from segments const parts = [] let remaining = n // Millions if (remaining >= 1_000_000n) { const millions = remaining / 1_000_000n remaining = remaining % 1_000_000n if (millions === 1n) { parts.push(MILLION + ' kan') } else { parts.push(MILLION + ' ' + integerToWords(millions)) } } // Thousands if (remaining >= 1000n) { const thousands = remaining / 1000n remaining = remaining % 1000n if (thousands === 1n) { parts.push(THOUSAND + ' kan') } else if (thousands === 10n) { parts.push(TEN_THOUSAND) } else if (thousands === 20n) { parts.push(TWENTY_THOUSAND) } else if (thousands < 100n) { parts.push(THOUSAND + ' ' + buildUnder100(Number(thousands))) } else { parts.push(THOUSAND + ' ' + convertHundreds(Number(thousands))) } } // Hundreds and below if (remaining > 0n) { if (remaining < 100n) { if (parts.length > 0) { parts.push(AND + ' ' + buildUnder100(Number(remaining))) } else { parts.push(buildUnder100(Number(remaining))) } } else { if (parts.length > 0) { parts.push(AND + ' ' + convertHundreds(Number(remaining))) } else { parts.push(convertHundreds(Number(remaining))) } } } return parts.join(', ') } /** * Converts decimal digits to Yoruba words. * @param {string} decimalPart - Decimal digits * @returns {string} Yoruba words for decimal */ function decimalPartToWords(decimalPart) { const parts = [] for (const digit of decimalPart) { const d = parseInt(digit, 10) parts.push(d === 0 ? ZERO : ONES[d]) } return parts.join(' ') } /** * Converts a numeric value to Yoruba words. * @param {number | string | bigint} value - The numeric value to convert * @returns {string} The number in Yoruba 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 Yoruba ordinal words. * * Yoruba ordinals: àkọ́kọ́ (1st), ìkejì (2nd), ìkẹta (3rd), ìkẹrin (4th), etc. * @param {bigint} n - Positive integer to convert * @returns {string} Yoruba ordinal words */ function integerToOrdinal(n) { // Special forms if (n === 1n) return ORDINAL_FIRST if (n === 2n) return ORDINAL_SECOND // For 3+, use ìkẹ- prefix + cardinal return ORDINAL_PREFIX + integerToWords(n) } /** * Converts a numeric value to Yoruba 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) // 'àkọ́kọ́' * toOrdinal(2) // 'ìkejì' * toOrdinal(3) // 'ìkẹẹ̀ta' */ function toOrdinal(value) { const integerPart = parseOrdinalValue(value) return integerToOrdinal(integerPart) } // ============================================================================ // CURRENCY: toCurrency(value) // ============================================================================ /** * Converts a numeric value to Yoruba currency words (Nigerian Naira). * * Uses náírà (naira) and kọ́bọ̀ (kobo). * @param {number | string | bigint} value - The currency amount to convert * @returns {string} The amount in Yoruba 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) // 'èjì lé lógójì náírà' * toCurrency(1.50) // 'ọ̀kan náírà àti àádọ́ta kọ́bọ̀' * toCurrency(-5) // 'àìní àrùn náírà' */ function toCurrency(value) { const { isNegative, dollars: naira, cents: kobo } = parseCurrencyValue(value) let result = '' if (isNegative) { result = NEGATIVE + ' ' } // Naira part if (naira > 0n || kobo === 0n) { result += integerToWords(naira) + ' ' + NAIRA } // Kobo part if (kobo > 0n) { if (naira > 0n) { result += ' àti ' } result += integerToWords(kobo) + ' ' + KOBO } return result } export { toCardinal, toOrdinal, toCurrency }