UNPKG

n2words

Version:

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

351 lines (295 loc) 10.4 kB
/** * Azerbaijani (Azerbaijan) language converter * * CLDR: az-AZ | Azerbaijani as used in Azerbaijan * * Key features: * - Turkic language patterns * - Implicit "bir" (one) omission before hundreds and thousands * - Short scale naming */ 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 { western } from './utils/scale.js' // ============================================================================ // Vocabulary // ============================================================================ const ONES = ['', 'bir', 'iki', 'üç', 'dörd', 'beş', 'altı', 'yeddi', 'səkkiz', 'doqquz'] const TEENS = ['on', 'on bir', 'on iki', 'on üç', 'on dörd', 'on beş', 'on altı', 'on yeddi', 'on səkkiz', 'on doqquz'] const TENS = ['', '', 'iyirmi', 'otuz', 'qırx', 'əlli', 'altmış', 'yetmiş', 'səksən', 'doxsan'] const HUNDRED = 'yüz' const THOUSAND = 'min' const ZERO = 'sıfır' const NEGATIVE = 'mənfi' const DECIMAL_SEP = 'nöqtə' // Short scale const SCALE_WORDS = ['', THOUSAND, 'milyon', 'milyar', 'trilyon', 'katrilyon', 'kentilyon'] // Supported magnitude ceiling (checked at the public entry points). SCALE_WORDS // reaches index SCALE_WORDS.length-1 (kentilyon, 10^18), so values must stay // below 10^(SCALE_WORDS.length * 3) = 10^21. Ordinal builds on the cardinal // (integerToWords + suffix) and currency too, so all three share the ceiling — // and the decimal is also spelled via integerToWords, so it's guarded as well. export const cardinalMax = western(SCALE_WORDS.length - 1) export const ordinalMax = western(SCALE_WORDS.length - 1) export const currencyMax = western(SCALE_WORDS.length - 1) // ============================================================================ // Ordinal Vocabulary // ============================================================================ // Azerbaijani ordinals use -(i/ı/u/ü)nci/ncı/ncu/ncü suffix with vowel harmony // Special forms for 1-10 /** @type {Record<number, string>} */ const ORDINAL_SPECIAL = { 1: 'birinci', 2: 'ikinci', 3: 'üçüncü', 4: 'dördüncü', 5: 'beşinci', 6: 'altıncı', 7: 'yeddinci', 8: 'səkkizinci', 9: 'doqquzuncu', 10: 'onuncu', } // ============================================================================ // Currency Vocabulary (Azerbaijani Manat) // ============================================================================ const MANAT = 'manat' const QEPIK = 'qəpik' // subunit (100 qəpik = 1 manat) // ============================================================================ // Precomputed Lookup Table // ============================================================================ /** * Builds the Azerbaijani words for a 3-digit segment (0-999). * @param {number} n - The segment value * @returns {string} The segment in words */ function buildSegment(n) { if (n === 0) return '' const ones = n % 10 const tensDigit = Math.trunc(n / 10) % 10 const hundredsDigit = Math.trunc(n / 100) const parts = [] if (hundredsDigit > 0) { if (hundredsDigit === 1) { parts.push(HUNDRED) } else { parts.push(ONES[hundredsDigit] + ' ' + HUNDRED) } } if (tensDigit === 1) { parts.push(TEENS[ones]) } else { if (tensDigit > 1) { parts.push(TENS[tensDigit]) } if (ones > 0) { parts.push(ONES[ones]) } } return parts.join(' ') } // ============================================================================ // Conversion Functions // ============================================================================ /** * Converts a non-negative integer to Azerbaijani words. * @param {bigint} n - The integer value * @returns {string} The integer in words */ function integerToWords(n) { if (n === 0n) return ZERO if (n < 1000n) { return buildSegment(Number(n)) } return buildLargeNumberWords(n) } /** * Builds Azerbaijani words for numbers >= 1000 using scale words. * @param {bigint} n - The integer value * @returns {string} The number in words */ function buildLargeNumberWords(n) { const numStr = n.toString() const len = numStr.length const segments = [] const segmentSize = 3 const remainderLen = len % segmentSize let pos = 0 if (remainderLen > 0) { segments.push(Number(numStr.slice(0, remainderLen))) pos = remainderLen } while (pos < len) { segments.push(Number(numStr.slice(pos, pos + segmentSize))) pos += segmentSize } const parts = [] let scaleIndex = segments.length - 1 for (let i = 0; i < segments.length; i++) { const segment = segments[i] if (segment !== 0) { const scaleWord = SCALE_WORDS[scaleIndex] || '' if (scaleIndex === 0) { parts.push(buildSegment(segment)) } else if (scaleIndex === 1 && segment === 1) { // Omit "bir" before thousand parts.push(scaleWord) } else { parts.push(buildSegment(segment) + ' ' + scaleWord) } } scaleIndex-- } return parts.join(' ') } /** * Converts the decimal-part digit string to Azerbaijani words. * @param {string} decimalPart - The decimal digits as a string * @returns {string} The decimal part in 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 Azerbaijani words. * @param {number | string | bigint} value - The numeric value to convert * @returns {string} The number in Azerbaijani words */ function toCardinal(value) { const { isNegative, integerPart, decimalPart } = parseCardinalValue(value) // Both the integer part and the decimal's significant digits are spelled via // the scale builder, so both must clear the ceiling. checkMax(integerPart, cardinalMax, decimalPart) let result = '' if (isNegative) { result = NEGATIVE + ' ' } result += integerToWords(integerPart) if (decimalPart) { result += ' ' + DECIMAL_SEP + ' ' + decimalPartToWords(decimalPart) } return result } // ============================================================================ // ORDINAL: toOrdinal(value) // ============================================================================ /** * Determines the ordinal suffix based on Azerbaijani vowel harmony. * @param {string} word - The cardinal word * @returns {string} The appropriate suffix */ function getOrdinalSuffix(word) { // Azerbaijani vowel harmony: back vowels (a,ı,o,u) vs front vowels (ə,e,i,ö,ü) const backVowels = 'aıou' const frontVowels = 'əeiöü' // Scan from end for last vowel for (let i = word.length - 1; i >= 0; i--) { const char = word[i] if (backVowels.includes(char)) { // Back vowels: -ıncı (after a,ı) or -uncu (after o,u) if ('ou'.includes(char)) return 'uncu' return 'ıncı' } if (frontVowels.includes(char)) { // Front vowels: -inci (after ə,e,i) or -üncü (after ö,ü) if ('öü'.includes(char)) return 'üncü' return 'inci' } } return 'inci' // default } /** * Converts a non-negative integer to Azerbaijani ordinal words. * * Azerbaijani ordinals: birinci (1st), ikinci (2nd), üçüncü (3rd), etc. * Uses vowel harmony for suffix selection. * @param {bigint} n - Positive integer to convert * @returns {string} Azerbaijani ordinal words */ function integerToOrdinal(n) { // Special forms for 1-10 if (n >= 1n && n <= 10n) { return ORDINAL_SPECIAL[Number(n)] } // For numbers > 10, get cardinal without spaces and add appropriate suffix const cardinal = integerToWords(n).replace(/ /g, '') const suffix = getOrdinalSuffix(cardinal) return cardinal + suffix } /** * Converts a numeric value to Azerbaijani 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) // 'birinci' * toOrdinal(2) // 'ikinci' * toOrdinal(21) // 'iyirmibirinci' */ function toOrdinal(value) { const integerPart = parseOrdinalValue(value) // Ordinal builds on the cardinal (integerToWords + suffix), so it shares the // ceiling — past it the cardinal's trailing-space artifact would be hidden by // the space-stripping and emit a silently-wrong ordinal. checkMax(integerPart, ordinalMax) return integerToOrdinal(integerPart) } // ============================================================================ // CURRENCY: toCurrency(value) // ============================================================================ /** * Converts a numeric value to Azerbaijani currency words (Manat). * * Uses manat and qəpik (100 qəpik = 1 manat). * @param {number | string | bigint} value - The currency amount to convert * @returns {string} The amount in Azerbaijani 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) // 'qırx iki manat' * toCurrency(1.50) // 'bir manat əlli qəpik' * toCurrency(-5) // 'mənfi beş manat' */ function toCurrency(value) { const { isNegative, dollars: manat, cents: qepik } = parseCurrencyValue(value) checkMax(manat, currencyMax) let result = '' if (isNegative) { result = NEGATIVE + ' ' } // Manat part if (manat > 0n || qepik === 0n) { result += integerToWords(manat) + ' ' + MANAT } // Qəpik part if (qepik > 0n) { if (manat > 0n) { result += ' ' } result += integerToWords(qepik) + ' ' + QEPIK } return result } // ============================================================================ // Exports // ============================================================================ export { toCardinal, toOrdinal, toCurrency }