UNPKG

n2words

Version:

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

516 lines (440 loc) 16.5 kB
/** * Biblical Hebrew (Israel) language converter * * CLDR: hbo-IL | Biblical/Ancient Hebrew * * Key features: * - Gender agreement (masculine default, feminine via option) * - Dual forms for 2, 200, 2000 * - Special 1-9 thousands construct state * - "ו" (ve) conjunction rules vary by position * - Per-digit decimal reading */ import { parseCardinalValue } from './utils/parse-cardinal.js' import { parseCurrencyValue } from './utils/parse-currency.js' import { parseOrdinalValue } from './utils/parse-ordinal.js' import { checkMax } from './utils/check-max.js' import { bounded, western } from './utils/scale.js' import { resolveOptions } from './utils/resolve-options.js' // ============================================================================ // Vocabulary (arrays for indexed access - faster than object property lookup) // ============================================================================ // Masculine forms (default in Biblical Hebrew) - index 0 unused const ONES_MASC = ['', 'אחד', 'שניים', 'שלשה', 'ארבעה', 'חמשה', 'ששה', 'שבעה', 'שמונה', 'תשעה'] const TEENS_MASC = ['עשרה', 'אחד עשר', 'שנים עשר', 'שלשה עשר', 'ארבעה עשר', 'חמשה עשר', 'ששה עשר', 'שבעה עשר', 'שמונה עשר', 'תשעה עשר'] const THOUSANDS_MASC = ['', 'אלף', 'אלפיים', 'שלשה אלפים', 'ארבעה אלפים', 'חמשה אלפים', 'ששה אלפים', 'שבעה אלפים', 'שמונה אלפים', 'תשעה אלפים'] // Feminine forms - index 0 unused const ONES_FEM = ['', 'אחת', 'שתים', 'שלש', 'ארבע', 'חמש', 'שש', 'שבע', 'שמונה', 'תשע'] const TEENS_FEM = ['עשר', 'אחת עשרה', 'שתים עשרה', 'שלש עשרה', 'ארבע עשרה', 'חמש עשרה', 'שש עשרה', 'שבע עשרה', 'שמונה עשרה', 'תשע עשרה'] const THOUSANDS_FEM = ['', 'אלף', 'אלפיים', 'שלשת אלפים', 'ארבעת אלפים', 'חמשת אלפים', 'ששת אלפים', 'שבעת אלפים', 'שמונת אלפים', 'תשעת אלפים'] // Shared vocabulary const TENS = ['', '', 'עשרים', 'שלשים', 'ארבעים', 'חמישים', 'ששים', 'שבעים', 'שמונים', 'תשעים'] const HUNDREDS = ['', 'מאה', 'מאתיים', 'שלשה מאות', 'ארבעה מאות', 'חמשה מאות', 'ששה מאות', 'שבעה מאות', 'שמונה מאות', 'תשעה מאות'] const HUNDREDS_FEM = ['', 'מאה', 'מאתיים', 'שלש מאות', 'ארבע מאות', 'חמש מאות', 'שש מאות', 'שבע מאות', 'שמונה מאות', 'תשע מאות'] // Scale words (index 1 = thousands, 2 = millions, etc.) const SCALE = ['', 'אלף', 'מיליון', 'מיליארד', 'טריליון', 'קוודרליון', 'קווינטיליון'] const SCALE_PLURAL = ['', 'אלפים', 'מיליונים', 'מיליארדים', 'טריליונים', 'קוודרליונים', 'קווינטיליונים'] // Supported magnitude ceilings (checked at the public entry points). Cardinal // SCALE reaches index SCALE.length-1 (10^18), so cardinals/currency must stay // below 10^(SCALE.length * 3) = 10^21. Ordinals are bounded lower: past 10^6 // the millions multiplier is built with buildScaleSegment (0-999), so n must // stay below 10^9. Decimals are read digit-by-digit (no ceiling). export const cardinalMax = western(SCALE.length - 1) export const ordinalMax = bounded(9) export const currencyMax = western(SCALE.length - 1) const ZERO = 'אפס' const NEGATIVE = 'מינוס' const DECIMAL_SEP = 'נקודה' // Ordinal forms (masculine) - used in Biblical Hebrew const ORDINAL_ONES = ['', 'ראשון', 'שני', 'שלישי', 'רביעי', 'חמישי', 'שישי', 'שביעי', 'שמיני', 'תשיעי'] const ORDINAL_TEENS = ['עשירי', 'אחד עשר', 'שנים עשר', 'שלשה עשר', 'ארבעה עשר', 'חמשה עשר', 'ששה עשר', 'שבעה עשר', 'שמונה עשר', 'תשעה עשר'] const ORDINAL_TENS = ['', '', 'עשרים', 'שלשים', 'ארבעים', 'חמישים', 'ששים', 'שבעים', 'שמונים', 'תשעים'] const ORDINAL_HUNDRED = 'מאה' // Currency (Biblical Shekel - historical usage) const SHEKEL = 'שקל' const SHEKEL_PLURAL = 'שקלים' const GERAH = 'גרה' const GERAH_PLURAL = 'גרות' // ============================================================================ // Segment Building // ============================================================================ /** * Builds segment word for scale segments (thousands, millions, etc.). * "ו" is added before tens and ones when following hundreds. * @param {number} n - Segment value (0-999) * @param {string} andWord - Conjunction word * @param {string[]} ONES - Ones vocabulary * @param {string[]} TEENS - Teens vocabulary * @param {string[]} HUNDREDS_ARR - Hundreds vocabulary * @returns {string} Segment word */ function buildScaleSegment(n, andWord, ONES, TEENS, HUNDREDS_ARR) { if (n === 0) return '' const ones = n % 10 const tens = Math.trunc(n / 10) % 10 const hundreds = Math.trunc(n / 100) let result = '' // Hundreds if (hundreds > 0) { result = HUNDREDS_ARR[hundreds] } // Tens and ones if (tens === 1) { // Teens (10-19) const teenWord = TEENS[ones] if (result) { result += ' ' + andWord + teenWord } else { result = teenWord } } else { // Tens (20-90) if (tens >= 2) { if (result) { result += ' ' + andWord + TENS[tens] } else { result = TENS[tens] } } // Ones if (ones > 0) { if (result) { result += ' ' + andWord + ONES[ones] } else { result = ONES[ones] } } } return result } /** * Builds segment word for units segment (no scale word). * "ו" is only added before the final ones digit. * @param {number} n - Segment value (0-999) * @param {string} andWord - Conjunction word * @param {string[]} ONES - Ones vocabulary * @param {string[]} TEENS - Teens vocabulary * @param {string[]} HUNDREDS_ARR - Hundreds vocabulary * @returns {string} Segment word */ function buildUnitsSegment(n, andWord, ONES, TEENS, HUNDREDS_ARR) { if (n === 0) return '' const ones = n % 10 const tens = Math.trunc(n / 10) % 10 const hundreds = Math.trunc(n / 100) let result = '' // Hundreds if (hundreds > 0) { result = HUNDREDS_ARR[hundreds] } // Tens (no conjunction) if (tens === 1) { // Teens (10-19) if (result) { result += ' ' + TEENS[ones] } else { result = TEENS[ones] } } else { if (tens >= 2) { if (result) { result += ' ' + TENS[tens] } else { result = TENS[tens] } } // Ones - conjunction only here if (ones > 0) { if (result) { result += ' ' + andWord + ONES[ones] } else { result = ONES[ones] } } } return result } // ============================================================================ // Conversion Functions // ============================================================================ /** * Converts a non-negative integer to Biblical Hebrew words. * @param {bigint} n - Non-negative integer to convert * @param {('masculine'|'feminine')} gender - Grammatical gender * @param {string} andWord - Conjunction word * @returns {string} Biblical Hebrew words */ function integerToWords(n, gender, andWord) { if (n === 0n) return ZERO const isFeminine = gender === 'feminine' // Select vocabulary based on gender const ONES = isFeminine ? ONES_FEM : ONES_MASC const TEENS = isFeminine ? TEENS_FEM : TEENS_MASC const THOUSANDS_SPECIAL = isFeminine ? THOUSANDS_FEM : THOUSANDS_MASC const HUNDREDS_ARR = isFeminine ? HUNDREDS_FEM : HUNDREDS // Fast path: numbers < 1000 if (n < 1000n) { return buildUnitsSegment(Number(n), andWord, ONES, TEENS, HUNDREDS_ARR) } // Extract segments using BigInt modulo const segments = [] let temp = n while (temp > 0n) { segments.push(Number(temp % 1000n)) temp = temp / 1000n } // Build result string directly let result = '' for (let i = segments.length - 1; i >= 0; i--) { const segment = segments[i] if (segment === 0) continue if (i === 0) { // Units segment (no scale word) const segmentWord = buildUnitsSegment(segment, andWord, ONES, TEENS, HUNDREDS_ARR) if (result) { // Add "ו" before single-digit units when following scale words if (segment <= 9) { result += ' ' + andWord + segmentWord } else { result += ' ' + segmentWord } } else { result = segmentWord } } else if (i === 1) { // Thousands - special handling for 1-9 if (segment <= 9) { if (result) result += ' ' result += THOUSANDS_SPECIAL[segment] } else { const segmentWord = buildScaleSegment(segment, andWord, ONES, TEENS, HUNDREDS_ARR) if (result) result += ' ' result += segmentWord + ' ' + SCALE[1] } } else { // Millions and above if (segment === 1) { if (result) result += ' ' result += SCALE[i] } else { const segmentWord = buildScaleSegment(segment, andWord, ONES, TEENS, HUNDREDS_ARR) if (result) result += ' ' result += segmentWord + ' ' + SCALE_PLURAL[i] } } } return result } /** * Converts decimal digits to Biblical Hebrew words (digit by digit). * @param {string} decimalPart - Decimal digits (without the point) * @param {('masculine'|'feminine')} gender - Grammatical gender * @returns {string} Biblical Hebrew words for decimal part */ function decimalPartToWords(decimalPart, gender) { const ONES = gender === 'feminine' ? ONES_FEM : ONES_MASC let result = '' for (let i = 0; i < decimalPart.length; i++) { const d = parseInt(decimalPart[i], 10) if (result) result += ' ' result += d === 0 ? ZERO : ONES[d] } return result } /** * @typedef {object} CardinalOptions * @property {('masculine'|'feminine')} [gender] - Grammatical gender * @property {string} [andWord] - Custom conjunction word */ /** @type {Required<CardinalOptions>} */ export const cardinalDefaults = { gender: 'masculine', andWord: 'ו' } /** @type {{ gender: ReadonlyArray<Required<CardinalOptions>['gender']> }} */ export const cardinalValues = { gender: ['masculine', 'feminine'] } /** * Converts a numeric value to Biblical Hebrew words. * @param {number | string | bigint} value - The numeric value to convert * @param {CardinalOptions} [options] - Optional configuration * @returns {string} The number in Biblical Hebrew words */ function toCardinal(value, options) { const { isNegative, integerPart, decimalPart } = parseCardinalValue(value) checkMax(integerPart, cardinalMax) const { gender, andWord } = resolveOptions(options, cardinalDefaults, cardinalValues) let result = '' if (isNegative) { result = NEGATIVE + ' ' } result += integerToWords(integerPart, gender, andWord) if (decimalPart) { result += ' ' + DECIMAL_SEP + ' ' + decimalPartToWords(decimalPart, gender) } return result } // ============================================================================ // Ordinal Functions // ============================================================================ /** * Builds ordinal for tens and ones (0-99). * @param {number} n - Number 0-99 * @returns {string} Ordinal word */ function buildOrdinalTensOnes(n) { if (n === 0) return '' if (n < 10) return ORDINAL_ONES[n] if (n < 20) return ORDINAL_TEENS[n - 10] const ones = n % 10 const tens = Math.trunc(n / 10) if (ones === 0) { // Round tens: add "וראשון" return ORDINAL_TENS[tens] + ' ו' + ORDINAL_ONES[1] } // Compound: cardinal tens + ordinal ones return ORDINAL_TENS[tens] + ' ו' + ORDINAL_ONES[ones] } /** * Converts a non-negative integer to Biblical Hebrew ordinal words. * @param {bigint} n - Non-negative integer to convert * @returns {string} Biblical Hebrew ordinal words */ function integerToOrdinal(n) { if (n === 0n) return '' if (n === 1n) return ORDINAL_ONES[1] // Numbers < 100 if (n < 100n) { return buildOrdinalTensOnes(Number(n)) } // Numbers < 1000 if (n < 1000n) { const hundreds = Number(n / 100n) const remainder = Number(n % 100n) if (remainder === 0) { return ORDINAL_HUNDRED } return HUNDREDS[hundreds] + ' ' + buildOrdinalTensOnes(remainder) } // Numbers < 1,000,000 if (n < 1_000_000n) { const thousands = Number(n / 1000n) const remainder = Number(n % 1000n) if (remainder === 0) { if (thousands <= 9) { return THOUSANDS_MASC[thousands] } return buildScaleSegment(thousands, 'ו', ONES_MASC, TEENS_MASC, HUNDREDS) + ' ' + SCALE[1] } // Cardinal thousands + ordinal remainder let result if (thousands <= 9) { result = THOUSANDS_MASC[thousands] } else { result = buildScaleSegment(thousands, 'ו', ONES_MASC, TEENS_MASC, HUNDREDS) + ' ' + SCALE[1] } if (remainder < 100) { return result + ' ' + buildOrdinalTensOnes(remainder) } const remHundreds = Math.trunc(remainder / 100) const remTensOnes = remainder % 100 if (remTensOnes === 0) { return result + ' ' + HUNDREDS[remHundreds] } return result + ' ' + HUNDREDS[remHundreds] + ' ' + buildOrdinalTensOnes(remTensOnes) } // Numbers >= 1,000,000 const millions = Number(n / 1_000_000n) const remainder = n % 1_000_000n if (remainder === 0n) { if (millions === 1) { return SCALE[2] } return buildScaleSegment(millions, 'ו', ONES_MASC, TEENS_MASC, HUNDREDS) + ' ' + SCALE_PLURAL[2] } // Cardinal millions + ordinal remainder let result if (millions === 1) { result = SCALE[2] } else { result = buildScaleSegment(millions, 'ו', ONES_MASC, TEENS_MASC, HUNDREDS) + ' ' + SCALE_PLURAL[2] } return result + ' ' + integerToOrdinal(remainder) } /** * Converts a numeric value to Biblical Hebrew ordinal words. * @param {number | string | bigint} value - The numeric value to convert * @returns {string} The ordinal in Biblical Hebrew words * @throws {TypeError} If value is not a valid numeric type * @throws {Error} If value is not a positive integer * @example * toOrdinal(1) // 'ראשון' * toOrdinal(21) // 'עשרים וראשון' */ function toOrdinal(value) { const n = parseOrdinalValue(value) checkMax(n, ordinalMax) return integerToOrdinal(n) } // ============================================================================ // Currency Functions // ============================================================================ /** * Converts a numeric value to Biblical Hebrew Shekel currency words. * @param {number | string | bigint} value - The numeric value to convert * @returns {string} The currency in Biblical Hebrew words * @throws {TypeError} If value is not a valid numeric type * @throws {Error} If value is not a valid number format * @example * toCurrency(1) // 'שקל אחד' * toCurrency(2.50) // 'שניים שקלים חמישים גרות' */ function toCurrency(value) { const { isNegative, dollars, cents } = parseCurrencyValue(value) checkMax(dollars, currencyMax) const parts = [] if (isNegative) { parts.push(NEGATIVE) } // Shekels (masculine) if (dollars > 0n || cents === 0n) { if (dollars === 1n) { parts.push(SHEKEL + ' ' + ONES_MASC[1]) } else if (dollars === 2n) { parts.push(ONES_MASC[2] + ' ' + SHEKEL_PLURAL) } else { const shekelWord = integerToWords(dollars, 'masculine', 'ו') parts.push(shekelWord + ' ' + SHEKEL_PLURAL) } } // Gerah (feminine subunit) if (cents > 0n) { const centNum = Number(cents) if (centNum === 1) { parts.push(GERAH + ' ' + ONES_FEM[1]) } else { const gerahWord = integerToWords(cents, 'feminine', 'ו') parts.push(gerahWord + ' ' + GERAH_PLURAL) } } return parts.join(' ') } // ============================================================================ // Exports // ============================================================================ export { toCardinal, toOrdinal, toCurrency }