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
JavaScript
/**
* 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 }