UNPKG

n2words

Version:

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

347 lines (287 loc) 9.58 kB
/** * Korean (South Korea) language converter * * CLDR: ko-KR | Korean as used in South Korea * * Key features: * - Myriad-based (만) grouping - 4 digits * - Implicit '일' (one) omission before scale words * - Space separation after 만+ scales * - Hangul numerals */ 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 { myriad } from './utils/scale.js' // ============================================================================ // Vocabulary (module-level constants) // ============================================================================ const ONES = ['', '일', '이', '삼', '사', '오', '육', '칠', '팔', '구'] const TEN = '십' const HUNDRED = '백' const THOUSAND = '천' const ZERO = '영' const NEGATIVE = '마이너스' const DECIMAL_SEP = '점' // Myriad scale words (powers of 10,000) // 만 (10^4), 억 (10^8), 조 (10^12), 경 (10^16), etc. const SCALES = ['만', '억', '조', '경', '해', '자', '양'] // Myriad (4-digit) grouping: each scale word covers a power of 10,000, so the // first unsupported value is 10^((SCALES.length + 1) * 4). Ordinals and currency // build on the cardinal speller, so they share its ceiling. export const cardinalMax = myriad(SCALES.length) export const ordinalMax = myriad(SCALES.length) export const currencyMax = myriad(SCALES.length) // ============================================================================ // Ordinal Vocabulary // ============================================================================ const ORDINAL_PREFIX = '제' // ============================================================================ // Currency Vocabulary (Korean Won) // ============================================================================ const WON = '원' // ============================================================================ // Segment Building // ============================================================================ /** * Builds segment word for 0-9999 (4-digit myriad segment). * Korean omits "일" before 십, 백, 천. * @param {number} n - Segment value (0-9999) * @returns {string} Korean words for the segment */ function buildSegment(n) { if (n === 0) return '' const ones = n % 10 const tens = Math.trunc(n / 10) % 10 const hundreds = Math.trunc(n / 100) % 10 const thousands = Math.trunc(n / 1000) let result = '' // Thousands if (thousands > 0) { if (thousands === 1) { result += THOUSAND } else { result += ONES[thousands] + THOUSAND } } // Hundreds if (hundreds > 0) { if (hundreds === 1) { result += HUNDRED } else { result += ONES[hundreds] + HUNDRED } } // Tens if (tens > 0) { if (tens === 1) { result += TEN } else { result += ONES[tens] + TEN } } // Ones if (ones > 0) { result += ONES[ones] } return result } // ============================================================================ // Conversion Functions // ============================================================================ /** * Converts a non-negative integer to Korean words. * @param {bigint} n - Non-negative integer to convert * @returns {string} Korean words */ function integerToWords(n) { if (n === 0n) return ZERO // Fast path: numbers < 10000 if (n < 10000n) { return buildSegment(Number(n)) } // For numbers >= 10000, use myriad decomposition return buildLargeNumberWords(n) } /** * Builds words for numbers >= 10000. * Uses myriad (만) grouping - 4 digits per segment. * @param {bigint} n - Number >= 10000 * @returns {string} Korean words */ function buildLargeNumberWords(n) { const numStr = n.toString() const len = numStr.length // Build segments of 4 digits from right to left const segments = [] const segmentSize = 4 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 segment (no scale word) parts.push({ word: buildSegment(segment), isScale: false }) } else { // Segment with scale word const scaleWord = SCALES[scaleIndex - 1] // Korean omits segment when it's 1 before scale words if (segment === 1) { parts.push({ word: scaleWord, isScale: true }) } else { parts.push({ word: buildSegment(segment), isScale: false }) parts.push({ word: scaleWord, isScale: true }) } } } scaleIndex-- } // Join with Korean spacing rules return joinKoreanParts(parts) } /** * Joins parts with Korean spacing rules. * - Concatenate without spaces within segments * - Space after scale words before next number * @param {Array<{word: string, isScale: boolean}>} parts - Parts with isScale metadata * @returns {string} Joined string */ function joinKoreanParts(parts) { if (parts.length === 0) return ZERO if (parts.length === 1) return parts[0].word const result = [] for (let i = 0; i < parts.length; i++) { const part = parts[i] const prevPart = i > 0 ? parts[i - 1] : null // Add space after scale words before next number if (prevPart && prevPart.isScale && !part.isScale) { result.push(' ') } result.push(part.word) } return result.join('') } /** * Converts decimal digits to Korean words. * @param {string} decimalPart - Decimal digits (without the point) * @returns {string} Korean words for decimal part (space-separated) */ function decimalPartToWords(decimalPart) { const parts = [] // Handle leading zeros let i = 0 while (i < decimalPart.length && decimalPart[i] === '0') { parts.push(ZERO) i++ } // Convert remainder as a single number const remainder = decimalPart.slice(i) if (remainder) { parts.push(integerToWords(BigInt(remainder))) } return parts.join(' ') } /** * Converts a numeric value to Korean words. * @param {number | string | bigint} value - The numeric value to convert * @returns {string} The number in Korean 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(10000) // '만' * toCardinal(1000000) // '백만' */ function toCardinal(value) { const { isNegative, integerPart, decimalPart } = parseCardinalValue(value) checkMax(integerPart, cardinalMax, decimalPart) const parts = [] if (isNegative) { parts.push(NEGATIVE) } parts.push(integerToWords(integerPart)) if (decimalPart) { parts.push(DECIMAL_SEP) parts.push(decimalPartToWords(decimalPart)) } return parts.join(' ') } // ============================================================================ // ORDINAL: toOrdinal(value) // ============================================================================ /** * Converts a non-negative integer to Korean ordinal words. * * Korean ordinals use "제" prefix + Sino-Korean numeral. * @param {bigint} n - Positive integer to convert * @returns {string} Korean ordinal words */ function integerToOrdinal(n) { return ORDINAL_PREFIX + integerToWords(n) } /** * Converts a numeric value to Korean 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) // '제일' * toOrdinal(2) // '제이' * toOrdinal(10) // '제십' */ function toOrdinal(value) { const integerPart = parseOrdinalValue(value) checkMax(integerPart, ordinalMax) return integerToOrdinal(integerPart) } // ============================================================================ // CURRENCY: toCurrency(value) // ============================================================================ /** * Converts a numeric value to Korean currency words (Won). * * Korean Won has no subunit (jeon are historical). * Amounts are rounded to whole won. * @param {number | string | bigint} value - The currency amount to convert * @returns {string} The amount in Korean 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) // '사십이원' * toCurrency(1000) // '천원' * toCurrency(-5) // '마이너스 오원' */ function toCurrency(value) { const { isNegative, dollars: won } = parseCurrencyValue(value) checkMax(won, currencyMax) let result = '' if (isNegative) { result = NEGATIVE + ' ' } result += integerToWords(won) result += WON return result } // ============================================================================ // Public API // ============================================================================ export { toCardinal, toOrdinal, toCurrency }