UNPKG

n2words

Version:

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

406 lines (340 loc) 13.3 kB
/** * Georgian (Georgia) language converter * * CLDR: ka-GE | Georgian as used in Georgia * * Georgian-specific rules: * - Vigesimal (base-20) system for 20-99 * - 40 = ორმოცი (2×20), 60 = სამოცი (3×20), 80 = ოთხმოცი (4×20) * - 30/50/70/90 use "და" + "ათი": ოცდაათი (20+10), ორმოცდაათი (40+10) * - Compound numbers use "და" (da = "and") connector * - Hundreds: unit prefix + ას (ორასი = 200) * - Short scale for large numbers (მილიონი, მილიარდი, ტრილიონი) */ 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 (module-level constants) // ============================================================================ // Numbers 0-9 (primitives) const ONES = ['ნული', 'ერთი', 'ორი', 'სამი', 'ოთხი', 'ხუთი', 'ექვსი', 'შვიდი', 'რვა', 'ცხრა'] // Numbers 10-19 const TEENS = ['ათი', 'თერთმეტი', 'თორმეტი', 'ცამეტი', 'თოთხმეტი', 'თხუთმეტი', 'თექვსმეტი', 'ჩვიდმეტი', 'თვრამეტი', 'ცხრამეტი'] // Vigesimal bases: 20, 40, 60, 80 (with და connector forms) const VIGESIMAL = ['', 'ოცი', 'ორმოცი', 'სამოცი', 'ოთხმოცი'] const VIGESIMAL_DA = ['', 'ოცდა', 'ორმოცდა', 'სამოცდა', 'ოთხმოცდა'] // Prefixes for hundreds (unit forms without final vowel) const HUNDRED_PREFIXES = ['', '', 'ორ', 'სამ', 'ოთხ', 'ხუთ', 'ექვს', 'შვიდ', 'რვა', 'ცხრა'] const HUNDRED = 'ასი' const HUNDRED_STEM = 'ას' // Without final vowel const THOUSAND = 'ათასი' const THOUSAND_STEM = 'ათას' // Without final vowel // Scale words (short scale) - indexed by segment position const SCALES = ['', '', 'მილიონი', 'მილიარდი', 'ტრილიონი', 'კვადრილიონი', 'კვინტილიონი', 'სექსტილიონი'] // 3-digit grouping; the table tops out at sextillion. Past it the builder clamps // to the last scale word (`SCALES[scaleIndex] || SCALES[SCALES.length - 1]`), // silently collapsing the magnitude — so cap there. export const cardinalMax = western(SCALES.length - 1) export const ordinalMax = western(SCALES.length - 1) export const currencyMax = western(SCALES.length - 1) const ZERO = 'ნული' const NEGATIVE = 'მინუს' const DECIMAL_SEP = 'მთელი' // Ordinal suffix (Georgian adds -ე to form ordinals) const ORDINAL_SUFFIX = 'ე' // Ordinal forms for 1-9 (these are special) const ORDINAL_ONES = ['', 'პირველი', 'მეორე', 'მესამე', 'მეოთხე', 'მეხუთე', 'მეექვსე', 'მეშვიდე', 'მერვე', 'მეცხრე'] // Currency (Georgian Lari) const LARI = 'ლარი' const TETRI = 'თეთრი' // ============================================================================ // Segment Building // ============================================================================ /** * Builds segment word for 0-99 using vigesimal system. * @param {number} n - Number 0-99 * @returns {string} Georgian word */ function buildTens(n) { if (n < 10) return ONES[n] if (n < 20) return TEENS[n - 10] const vigesimalGroup = Math.trunc(n / 20) const remainder = n % 20 if (remainder === 0) { return VIGESIMAL[vigesimalGroup] } // Use და connector: ოცდა + remainder const base = VIGESIMAL_DA[vigesimalGroup] if (remainder < 10) { return base + ONES[remainder] } return base + TEENS[remainder - 10] } /** * Builds segment word for 0-999. * Returns object with full form and stem form (without final vowel). * @param {number} n - Number 0-999 * @returns {{full: string, stem: string}} Georgian words */ function buildSegment(n) { if (n === 0) return { full: '', stem: '' } if (n < 100) { const word = buildTens(n) // Remove final vowel for stem const lastChar = word.slice(-1) const stem = (lastChar === 'ი' || lastChar === 'ა') ? word.slice(0, -1) : word return { full: word, stem } } const hundreds = Math.trunc(n / 100) const remainder = n % 100 // Build hundreds: ასი (100), ორასი (200), etc. let hundredWord if (hundreds === 1) { hundredWord = remainder > 0 ? HUNDRED_STEM : HUNDRED } else { hundredWord = HUNDRED_PREFIXES[hundreds] + (remainder > 0 ? HUNDRED_STEM : HUNDRED) } if (remainder > 0) { const remainderWord = buildTens(remainder) const full = hundredWord + ' ' + remainderWord // Stem removes final vowel from remainder const lastChar = remainderWord.slice(-1) const remainderStem = (lastChar === 'ი' || lastChar === 'ა') ? remainderWord.slice(0, -1) : remainderWord return { full, stem: hundredWord + ' ' + remainderStem } } // Hundreds only - stem removes final ი return { full: hundredWord, stem: hundredWord.slice(0, -1) } } // ============================================================================ // Conversion Functions // ============================================================================ /** * Converts a non-negative integer to Georgian words. * @param {bigint} n - Non-negative integer to convert * @returns {string} Georgian words */ function integerToWords(n) { if (n === 0n) return ZERO // Fast path: numbers < 1000 if (n < 1000n) { const { full } = buildSegment(Number(n)) return full } // Fast path: numbers < 1,000,000 (thousands) if (n < 1_000_000n) { const thousands = Number(n / 1000n) const remainder = Number(n % 1000n) let result if (thousands === 1) { // "ათასი" not "ერთი ათასი" result = remainder > 0 ? THOUSAND_STEM : THOUSAND } else { // Use stem form before ათასი const { stem: thousandsPart } = buildSegment(thousands) result = thousandsPart + ' ' + (remainder > 0 ? THOUSAND_STEM : THOUSAND) } if (remainder > 0) { const { full: remainderWord } = buildSegment(remainder) result += ' ' + remainderWord } return result } // For numbers >= 1,000,000, use scale decomposition return buildLargeNumberWords(n) } /** * Builds words for numbers >= 1,000,000. * @param {bigint} n - Number >= 1,000,000 * @returns {string} Georgian words */ function buildLargeNumberWords(n) { const numStr = n.toString() const len = numStr.length // Build segments of 3 digits from left to right 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 } // Convert segments to words const parts = [] let scaleIndex = segments.length - 1 for (let i = 0; i < segments.length; i++) { const segment = segments[i] if (segment !== 0) { if (scaleIndex === 0) { // Units (no scale) const { full } = buildSegment(segment) parts.push(full) } else if (scaleIndex === 1) { // Thousands - check if there's a remainder const hasRemainder = segments.slice(i + 1).some(s => s !== 0) const thousandWord = hasRemainder ? THOUSAND_STEM : THOUSAND if (segment === 1) { parts.push(thousandWord) } else { const { stem } = buildSegment(segment) parts.push(stem + ' ' + thousandWord) } } else { // Million and above const scaleWord = SCALES[scaleIndex] || SCALES[SCALES.length - 1] if (segment === 1) { parts.push('ერთი ' + scaleWord) } else { const { full } = buildSegment(segment) parts.push(full + ' ' + scaleWord) } } } scaleIndex-- } return parts.join(' ') } /** * Converts decimal digits to Georgian words. * @param {string} decimalPart - Decimal digits (without the point) * @returns {string} Georgian words for decimal part */ function decimalPartToWords(decimalPart) { let result = '' // Handle leading zeros let i = 0 while (i < decimalPart.length && decimalPart[i] === '0') { if (result) result += ' ' result += ZERO i++ } // Convert remainder as a single number const remainder = decimalPart.slice(i) if (remainder) { if (result) result += ' ' result += integerToWords(BigInt(remainder)) } return result } /** * Converts a numeric value to Georgian words. * * This is the main public API. It accepts any valid numeric input * (number, string, or bigint) and handles parsing internally. * @param {number | string | bigint} value - The numeric value to convert * @returns {string} The number in Georgian words * @throws {TypeError} If value is not a valid numeric type * @throws {Error} If value is not a valid number format * @example * toCardinal(21) // 'ოცდაერთი' * toCardinal(100) // 'ასი' * toCardinal(1000) // 'ათასი' */ 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 Functions // ============================================================================ /** * Converts a non-negative integer to Georgian ordinal words. * Georgian ordinals are formed by: * - 1-9: special forms (პირველი, მეორე, მესამე, etc.) * - 10+: მე- prefix + cardinal + -ე suffix * @param {bigint} n - Non-negative integer to convert * @returns {string} Georgian ordinal words */ function integerToOrdinal(n) { if (n === 0n) return '' if (n <= 9n) return ORDINAL_ONES[Number(n)] // For 10+, use მე- + cardinal stem + -ე const cardinal = integerToWords(n) // Remove final vowel and add -ე suffix const lastChar = cardinal.slice(-1) let stem if (lastChar === 'ი' || lastChar === 'ა') { stem = cardinal.slice(0, -1) } else { stem = cardinal } return 'მე' + stem + ORDINAL_SUFFIX } /** * Converts a numeric value to Georgian ordinal words. * @param {number | string | bigint} value - The numeric value to convert * @returns {string} The ordinal in Georgian words * @throws {TypeError} If value is not a valid numeric type * @throws {Error} If value is not a positive integer * @example * toOrdinal(1) // 'პირველი' * toOrdinal(10) // 'მეათე' * toOrdinal(21) // 'მეოცდაერთე' */ function toOrdinal(value) { const n = parseOrdinalValue(value) // Ordinals build on the cardinal speller, so they share its ceiling. checkMax(n, ordinalMax) return integerToOrdinal(n) } // ============================================================================ // Currency Functions // ============================================================================ /** * Converts a numeric value to Georgian Lari currency words. * @param {number | string | bigint} value - The numeric value to convert * @returns {string} The currency in Georgian 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) } // Lari if (dollars > 0n || cents === 0n) { const lariWord = integerToWords(dollars) parts.push(lariWord + ' ' + LARI) } // Tetri if (cents > 0n) { const tetriWord = integerToWords(cents) parts.push(tetriWord + ' ' + TETRI) } return parts.join(' ') } // ============================================================================ // Public API // ============================================================================ export { toCardinal, toOrdinal, toCurrency }