UNPKG

n2words

Version:

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

424 lines (357 loc) 12.4 kB
/** * Turkish (Turkey) language converter * * CLDR: tr-TR | Turkish as used in Turkey * * Key features: * - Omits 'bir' (one) before hundreds and thousands * - Optional dropSpaces for compound form * - 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' import { resolveOptions } from './utils/resolve-options.js' // ============================================================================ // Vocabulary (module-level constants) // ============================================================================ const ONES = ['', 'bir', 'iki', 'üç', 'dört', 'beş', 'altı', 'yedi', 'sekiz', 'dokuz'] const TEENS = ['on', 'on bir', 'on iki', 'on üç', 'on dört', 'on beş', 'on altı', 'on yedi', 'on sekiz', 'on dokuz'] const TENS = ['', '', 'yirmi', 'otuz', 'kırk', 'elli', 'altmış', 'yetmiş', 'seksen', 'doksan'] const HUNDRED = 'yüz' const THOUSAND = 'bin' const ZERO = 'sıfır' const NEGATIVE = 'eksi' const DECIMAL_SEP = 'virgül' // Short scale const SCALES = ['milyon', 'milyar', 'trilyon', 'katrilyon', 'kentilyon'] // Supported magnitude ceiling (checked at the public entry points). SCALES is // indexed [scaleIndex - 2] (units and thousands are separate), so segments are // [units, thousands, then one per SCALES entry] -> the ceiling is // 10^((SCALES.length + 2) * 3) = 10^21. Ordinal (integerToWords + suffix), // currency, and the decimal all route through the cardinal builder, so all // share the ceiling. export const cardinalMax = western(SCALES.length + 1) export const ordinalMax = western(SCALES.length + 1) export const currencyMax = western(SCALES.length + 1) // ============================================================================ // Ordinal Vocabulary // ============================================================================ // Turkish 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: 'yedinci', 8: 'sekizinci', 9: 'dokuzuncu', 10: 'onuncu', } // ============================================================================ // Currency Vocabulary (Turkish Lira) // ============================================================================ const LIRA = 'lira' // same singular and plural const KURUS = 'kuruş' // subunit (100 kuruş = 1 lira) // ============================================================================ // Segment Building // ============================================================================ /** * Builds segment word for 0-999. * Omits "bir" before "yüz" (hundred). * @param {number} n - Segment value (0-999) * @param {string} [separator] - Separator between words * @returns {string} Segment words */ function buildSegment(n, separator = ' ') { if (n === 0) return '' const ones = n % 10 const tens = Math.trunc(n / 10) % 10 const hundreds = Math.trunc(n / 100) const parts = [] // Hundreds - omit "bir" before yüz if (hundreds > 0) { if (hundreds === 1) { parts.push(HUNDRED) } else { parts.push(ONES[hundreds] + separator + HUNDRED) } } // Tens and ones const tensOnes = n % 100 if (tensOnes === 0) { // Just hundreds } else if (tensOnes < 10) { parts.push(ONES[ones]) } else if (tensOnes < 20) { parts.push(TEENS[ones].replace(' ', separator)) } else if (ones === 0) { parts.push(TENS[tens]) } else { parts.push(TENS[tens] + separator + ONES[ones]) } return parts.join(separator) } // ============================================================================ // Conversion Functions // ============================================================================ /** * Converts a non-negative integer to Turkish words. * @param {bigint} n - Non-negative integer to convert * @param {boolean} dropSpaces - Remove spaces for compound form * @returns {string} Turkish words */ function integerToWords(n, dropSpaces) { if (n === 0n) return ZERO const sep = dropSpaces ? '' : ' ' // Fast path: numbers < 1000 if (n < 1000n) { return buildSegment(Number(n), sep) } // Fast path: numbers < 1,000,000 (thousands) if (n < 1_000_000n) { const thousands = Number(n / 1000n) const remainder = Number(n % 1000n) // Omit "bir" before bin (thousand) let result if (thousands === 1) { result = THOUSAND } else { result = buildSegment(thousands, sep) + sep + THOUSAND } if (remainder > 0) { result += sep + buildSegment(remainder, sep) } return result } // For numbers >= 1,000,000, use scale decomposition return buildLargeNumberWords(n, dropSpaces) } /** * Builds words for numbers >= 1,000,000. * @param {bigint} n - Number >= 1,000,000 * @param {boolean} dropSpaces - Remove spaces for compound form * @returns {string} Turkish words */ function buildLargeNumberWords(n, dropSpaces) { const sep = dropSpaces ? '' : ' ' const numStr = n.toString() const len = numStr.length // Build segments of 3 digits from right to left 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) { const segmentWord = buildSegment(segment, sep) if (scaleIndex === 0) { // Units segment parts.push(segmentWord) } else if (scaleIndex === 1) { // Thousands - omit "bir" before bin if (segment === 1) { parts.push(THOUSAND) } else { parts.push(segmentWord + sep + THOUSAND) } } else { // Millions+ - "bir" is kept before scale words const scaleWord = SCALES[scaleIndex - 2] parts.push(segmentWord + sep + scaleWord) } } scaleIndex-- } return parts.join(sep) } /** * Converts decimal digits to Turkish words. * @param {string} decimalPart - Decimal digits (without the point) * @param {boolean} dropSpaces - Remove spaces for compound form * @returns {string} Turkish words for decimal part */ function decimalPartToWords(decimalPart, dropSpaces) { const sep = dropSpaces ? '' : ' ' let result = '' // Handle leading zeros let i = 0 while (i < decimalPart.length && decimalPart[i] === '0') { if (result) result += sep result += ZERO i++ } // Convert remainder as a single number const remainder = decimalPart.slice(i) if (remainder) { if (result) result += sep result += integerToWords(BigInt(remainder), dropSpaces) } return result } /** * @typedef {object} CardinalOptions * @property {boolean} [dropSpaces] - Remove spaces for compound form */ /** @type {Required<CardinalOptions>} */ export const cardinalDefaults = { dropSpaces: false } /** * Converts a numeric value to Turkish words. * @param {number | string | bigint} value - The numeric value to convert * @param {CardinalOptions} [options] - Conversion options * @returns {string} The number in Turkish words * @throws {TypeError} If value is not a valid numeric type * @throws {Error} If value is not a valid number format * @example * toCardinal(21) // 'yirmi bir' * toCardinal(21, { dropSpaces: true }) // 'yirmibir' * toCardinal(1000) // 'bin' */ function toCardinal(value, options) { 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) // Apply option defaults const { dropSpaces } = resolveOptions(options, cardinalDefaults) const sep = dropSpaces ? '' : ' ' let result = '' if (isNegative) { result = NEGATIVE + sep } result += integerToWords(integerPart, dropSpaces) if (decimalPart) { result += sep + DECIMAL_SEP + sep + decimalPartToWords(decimalPart, dropSpaces) } return result } // ============================================================================ // ORDINAL: toOrdinal(value) // ============================================================================ /** * Determines the ordinal suffix based on Turkish vowel harmony. * @param {string} word - The cardinal word * @returns {string} The appropriate suffix */ function getOrdinalSuffix(word) { // Turkish vowel harmony: back vowels (a,ı,o,u) vs front vowels (e,i,ö,ü) // Find last vowel to determine suffix 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 Turkish ordinal words. * * Turkish ordinals: birinci (1st), ikinci (2nd), üçüncü (3rd), etc. * Uses vowel harmony for suffix selection. * @param {bigint} n - Positive integer to convert * @returns {string} Turkish ordinal words */ function integerToOrdinal(n) { // Special forms for 1-10 if (n >= 1n && n <= 10n) { return ORDINAL_SPECIAL[Number(n)] } // For numbers > 10, add appropriate suffix to cardinal (dropSpaces=true) const cardinal = integerToWords(n, true) const suffix = getOrdinalSuffix(cardinal) return cardinal + suffix } /** * Converts a numeric value to Turkish 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) // 'yirmibirinci' */ function toOrdinal(value) { const integerPart = parseOrdinalValue(value) checkMax(integerPart, ordinalMax) return integerToOrdinal(integerPart) } // ============================================================================ // CURRENCY: toCurrency(value) // ============================================================================ /** * Converts a numeric value to Turkish currency words (Turkish Lira). * * Uses lira and kuruş (100 kuruş = 1 lira). * @param {number | string | bigint} value - The currency amount to convert * @returns {string} The amount in Turkish 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) // 'kırk iki lira' * toCurrency(1.50) // 'bir lira elli kuruş' * toCurrency(-5) // 'eksi beş lira' */ function toCurrency(value) { const { isNegative, dollars: lira, cents: kurus } = parseCurrencyValue(value) checkMax(lira, currencyMax) let result = '' if (isNegative) { result = NEGATIVE + ' ' } // Lira part if (lira > 0n || kurus === 0n) { result += integerToWords(lira, false) + ' ' + LIRA } // Kuruş part if (kurus > 0n) { if (lira > 0n) { result += ' ' } result += integerToWords(kurus, false) + ' ' + KURUS } return result } // ============================================================================ // Public API // ============================================================================ export { toCardinal, toOrdinal, toCurrency }