UNPKG

n2words

Version:

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

409 lines (359 loc) 11.3 kB
/** * Hungarian (Hungary) language converter * * CLDR: hu-HU | Hungarian as used in Hungary * * Key features: * - Agglutinative structure (no spaces between compound parts) * - Special handling for "egy" (one) omission * - Pre-composed twenties (huszonegy through huszonkilenc) * - "két" instead of "kettő" in compound forms */ 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 // ============================================================================ // Word map using BigInt keys for scale values const WORDS = new Map([ [1_000_000_000_000_000_000_000_000_000n, 'quadrilliárd'], [1_000_000_000_000_000_000_000_000n, 'quadrillió'], [1_000_000_000_000_000_000_000n, 'trilliárd'], [1_000_000_000_000_000_000n, 'trillió'], [1_000_000_000_000_000n, 'billiárd'], [1_000_000_000_000n, 'billió'], [1_000_000_000n, 'milliárd'], [1_000_000n, 'millió'], [1000n, 'ezer'], [100n, 'száz'], [90n, 'kilencven'], [80n, 'nyolcvan'], [70n, 'hetven'], [60n, 'hatvan'], [50n, 'ötven'], [40n, 'negyven'], [30n, 'harminc'], [29n, 'huszonkilenc'], [28n, 'huszonnyolc'], [27n, 'huszonhét'], [26n, 'huszonhat'], [25n, 'huszonöt'], [24n, 'huszonnégy'], [23n, 'huszonhárom'], [22n, 'huszonkettő'], [21n, 'huszonegy'], [20n, 'húsz'], [19n, 'tizenkilenc'], [18n, 'tizennyolc'], [17n, 'tizenhét'], [16n, 'tizenhat'], [15n, 'tizenöt'], [14n, 'tizennégy'], [13n, 'tizenhárom'], [12n, 'tizenkettő'], [11n, 'tizenegy'], [10n, 'tíz'], [9n, 'kilenc'], [8n, 'nyolc'], [7n, 'hét'], [6n, 'hat'], [5n, 'öt'], [4n, 'négy'], [3n, 'három'], [2n, 'kettő'], [1n, 'egy'], [0n, 'nulla'], ]) const ZERO = 'nulla' const NEGATIVE = 'mínusz' const DECIMAL_SEP = 'egész' // ============================================================================ // Ordinal Vocabulary // ============================================================================ // Hungarian ordinals: special forms 1-10, then cardinal + -dik/-ik suffix // Vowel harmony determines -dik vs -ik /** @type {Record<number, string>} */ const ORDINAL_SPECIAL = { 1: 'első', 2: 'második', 3: 'harmadik', 4: 'negyedik', 5: 'ötödik', 6: 'hatodik', 7: 'hetedik', 8: 'nyolcadik', 9: 'kilencedik', 10: 'tizedik', 20: 'huszadik', 30: 'harmincadik', 40: 'negyvenedik', 50: 'ötvenedik', 60: 'hatvanadik', 70: 'hetvenedik', 80: 'nyolcvanadik', 90: 'kilencvenedik', 100: 'századik', 1000: 'ezredik', } // ============================================================================ // Currency Vocabulary (Hungarian Forint) // ============================================================================ const FORINT = 'forint' // same singular and plural const FILLER = 'fillér' // subunit (rarely used, same singular and plural) // ============================================================================ // Conversion Functions // ============================================================================ /** * @param {bigint} value The scale or vocabulary key to look up * @returns {string | undefined} The Hungarian word for the key, or undefined if absent */ function wordForScale(value) { return WORDS.get(value) } /** * @param {bigint} n The integer (under 100) to convert * @returns {string} The number in Hungarian words */ function tensToCardinal(n) { if (WORDS.has(n)) { return /** @type {string} */ (WORDS.get(n)) } const tens = n / 10n const units = n % 10n return wordForScale(tens * 10n) + integerToWordsInner(units) } /** * @param {bigint} n The integer (under 1000) to convert * @returns {string} The number in Hungarian words */ function hundredsToCardinal(n) { const hundreds = n / 100n let prefix = 'száz' if (hundreds !== 1n) { prefix = integerToWordsInner(hundreds, '') + prefix } const postfix = integerToWordsInner(n % 100n, '') return prefix + postfix } /** * @param {bigint} n The integer (under one million) to convert * @returns {string} The number in Hungarian words */ function thousandsToCardinal(n) { const thousands = n / 1000n let prefix = 'ezer' if (thousands !== 1n) { prefix = integerToWordsInner(thousands, '') + prefix } const postfix = integerToWordsInner(n % 1000n, '') const middle = (n <= 2000n || postfix === '') ? '' : '-' return prefix + middle + postfix } // Scale values for finding appropriate scale (avoids toString) const SCALES = [ 1_000_000_000_000_000_000_000_000_000n, 1_000_000_000_000_000_000_000_000n, 1_000_000_000_000_000_000_000n, 1_000_000_000_000_000_000n, 1_000_000_000_000_000n, 1_000_000_000_000n, 1_000_000_000n, 1_000_000n, ] /** * @param {bigint} n The integer (one million or greater) to convert * @returns {string} The number in Hungarian words */ function bigNumberToCardinal(n) { // Find appropriate scale using BigInt comparison (avoids toString) let exp = 1_000_000n for (const scale of SCALES) { if (n >= scale) { exp = scale break } } const prefix = integerToWordsInner(n / exp, '') const rest = integerToWordsInner(n % exp, '') const postfix = (rest === '') ? '' : ('-' + rest) return prefix + wordForScale(exp) + postfix } /** * @param {bigint} n The integer to convert * @param {string} [zeroWord] The word to use for zero (empty string in compound contexts) * @returns {string} The number in Hungarian words */ function integerToWordsInner(n, zeroWord = ZERO) { // Normalize to BigInt for consistent comparisons if (typeof n !== 'bigint') n = BigInt(n) if (n === 0n) { return zeroWord } if (zeroWord === '' && n === 2n) { return 'két' } if (n < 30n) { return /** @type {string} */ (wordForScale(n)) } if (n < 100n) { return tensToCardinal(n) } if (n < 1000n) { return hundredsToCardinal(n) } if (n < 1_000_000n) { return thousandsToCardinal(n) } return bigNumberToCardinal(n) } /** * @param {bigint} n The integer to convert * @returns {string} The number in Hungarian words */ function integerToWords(n) { return integerToWordsInner(n, ZERO) } /** * @param {string} decimalPart The decimal digits (after the separator) to convert * @returns {string} The decimal digits in Hungarian words */ 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 Hungarian words. * @param {number | string | bigint} value - The numeric value to convert * @returns {string} The number in Hungarian 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 Hungarian ordinal words. * * Hungarian ordinals: első (1st), második (2nd), harmadik (3rd), etc. * 1-10 have special forms, others use cardinal + -dik/-edik/-adik/-ödik suffix. * @param {bigint} n - Positive integer to convert * @returns {string} Hungarian ordinal words */ function integerToOrdinal(n) { const num = Number(n) // Exact special forms if (ORDINAL_SPECIAL[num]) { return ORDINAL_SPECIAL[num] } // For compound numbers, get cardinal and add suffix const cardinal = integerToWords(n) // Determine suffix based on last vowel (vowel harmony) // Back vowels (a,á,o,ó,u,ú) -> -adik // Front unrounded (e,é,i,í) -> -edik // Front rounded (ö,ő,ü,ű) -> -ödik // If ends in consonant, add linking vowel + dik const lastChar = cardinal[cardinal.length - 1] // If ends in vowel, just add -dik if ('aáoóuúeéiíöőüű'.includes(lastChar)) { return cardinal + 'dik' } // If ends in consonant, find last vowel for harmony const backVowels = 'aáoóuú' const frontRoundedVowels = 'öőüű' for (let i = cardinal.length - 1; i >= 0; i--) { const char = cardinal[i] if (backVowels.includes(char)) { return cardinal + 'adik' } if (frontRoundedVowels.includes(char)) { return cardinal + 'ödik' } if ('eéií'.includes(char)) { return cardinal + 'edik' } } // Default to -edik return cardinal + 'edik' } /** * Converts a numeric value to Hungarian 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) // 'első' * toOrdinal(2) // 'második' * toOrdinal(21) // 'huszonegyedik' */ function toOrdinal(value) { const integerPart = parseOrdinalValue(value) return integerToOrdinal(integerPart) } // ============================================================================ // CURRENCY: toCurrency(value) // ============================================================================ /** * Converts a numeric value to Hungarian currency words (Hungarian Forint). * * Uses forint (no plural form needed in Hungarian). * Fillér (1/100) is rarely used but included for completeness. * @param {number | string | bigint} value - The currency amount to convert * @returns {string} The amount in Hungarian 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) // 'negyvenkettő forint' * toCurrency(1.50) // 'egy forint ötven fillér' * toCurrency(-5) // 'mínusz öt forint' */ function toCurrency(value) { const { isNegative, dollars: forint, cents: filler } = parseCurrencyValue(value) let result = '' if (isNegative) { result = NEGATIVE + ' ' } // Forint part if (forint > 0n || filler === 0n) { result += integerToWords(forint) + ' ' + FORINT } // Fillér part if (filler > 0n) { if (forint > 0n) { result += ' ' } result += integerToWords(filler) + ' ' + FILLER } return result } // ============================================================================ // Exports // ============================================================================ export { toCardinal, toOrdinal, toCurrency }