UNPKG

n2words

Version:

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

360 lines (304 loc) 10.2 kB
/** * Japanese (Japan) language converter * * CLDR: ja-JP | Japanese as used in Japan * * Japanese-specific rules: * - Myriad (万-based) grouping: 4 digits per segment instead of 3 * - 一 omission: Omit "一" before 十, 百, 千 but NOT before 万 and higher scales * - Kanji numerals: 零一二三四五六七八九 * - No spaces between characters */ 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) // ============================================================================ // Ones words (1-9), index 0 unused const ONES = ['', '一', '二', '三', '四', '五', '六', '七', '八', '九'] // Scale words for powers of 10,000 (万-based system) // Index 0 = 万 (10^4), 1 = 億 (10^8), 2 = 兆 (10^12), etc. const SCALES = [ '万', // 10^4 (man) '億', // 10^8 (oku) '兆', // 10^12 (chō) '京', // 10^16 (kei) '垓', // 10^20 (gai) '秭', // 10^24 (jo/shi) '穣', // 10^28 (jō) '溝', // 10^32 (kō) '澗', // 10^36 (kan) '正', // 10^40 (sei) '載', // 10^44 (sai) '極', // 10^48 (goku) '恒河沙', // 10^52 (gōgasha) '阿僧祇', // 10^56 (asōgi) '那由他', // 10^60 (nayuta) '不可思議', // 10^64 (fukashigi) '無量大数', // 10^68 (muryōtaisū) ] // 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 (prefix) 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) const ZERO = '零' const NEGATIVE = 'マイナス' const DECIMAL_SEP = '点' // ============================================================================ // Ordinal Vocabulary // ============================================================================ // Ordinal prefix const ORDINAL_PREFIX = '第' // ============================================================================ // Currency Vocabulary (Japanese Yen) // ============================================================================ // Yen (main unit) const YEN = '円' // Sen (1/100 yen) - historically used, now rare const SEN = '銭' // Internal scale words (within 4-digit segments) const TEN = '十' const HUNDRED = '百' const THOUSAND = '千' // ============================================================================ // Segment Building // ============================================================================ /** * Builds segment word for 0-9999 with 一 omission rules. * - Omit 一 before 十, 百, 千 * @param {number} n - Segment value (0-9999) * @returns {string} Japanese kanji 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 (千) - omit 一 when 1 if (thousands > 0) { if (thousands === 1) { result += THOUSAND } else { result += ONES[thousands] + THOUSAND } } // Hundreds (百) - omit 一 when 1 if (hundreds > 0) { if (hundreds === 1) { result += HUNDRED } else { result += ONES[hundreds] + HUNDRED } } // Tens (十) - omit 一 when 1 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 Japanese words. * @param {bigint} n - Non-negative integer to convert * @returns {string} Japanese kanji words */ function integerToWords(n) { if (n === 0n) return ZERO // Fast path: numbers < 10000 if (n < 10000n) { return buildSegment(Number(n)) } // Fast path: numbers < 100,000,000 (万 range) if (n < 100_000_000n) { const man = Number(n / 10000n) const remainder = Number(n % 10000n) // For 万 and above, we need 一 before the scale word when segment is 1 let result if (man === 1) { result = '一' + SCALES[0] // 一万 } else { result = buildSegment(man) + SCALES[0] } if (remainder > 0) { result += buildSegment(remainder) } return result } // For numbers >= 100,000,000, use scale decomposition return buildLargeNumberWords(n) } /** * Builds words for numbers >= 100,000,000. * Uses BigInt modulo for 4-digit (myriad) segment extraction. * @param {bigint} n - Number >= 100,000,000 * @returns {string} Japanese kanji words */ function buildLargeNumberWords(n) { // Extract segments using BigInt modulo (faster than string slicing) // Segments stored least-significant first (index 0 = units, 1 = 万, etc.) const segments = [] let temp = n while (temp > 0n) { segments.push(Number(temp % 10000n)) temp = temp / 10000n } // Build result string directly (process from most-significant to least) let result = '' for (let i = segments.length - 1; i >= 0; i--) { const segment = segments[i] if (segment === 0) continue if (i > 0) { // For scales >= 万, we need 一 before scale word when segment is 1 if (segment === 1) { result += '一' + SCALES[i - 1] } else { result += buildSegment(segment) + SCALES[i - 1] } } else { // Units segment (no scale word) result += buildSegment(segment) } } return result || ZERO } /** * Converts decimal digits to Japanese words (digit by digit). * @param {string} decimalPart - Decimal digits (without the point) * @returns {string} Japanese kanji words for decimal part */ function decimalPartToWords(decimalPart) { let result = '' for (let i = 0; i < decimalPart.length; i++) { const digit = parseInt(decimalPart[i], 10) if (digit === 0) { result += ZERO } else { result += ONES[digit] } } return result } /** * Converts a numeric value to Japanese 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 Japanese kanji words * @throws {TypeError} If value is not a valid numeric type * @throws {Error} If value is not a valid number format * @example * toCardinal(42) // '四十二' * toCardinal(10000) // '一万' * toCardinal(100000000) // '一億' */ function toCardinal(value) { const { isNegative, integerPart, decimalPart } = parseCardinalValue(value) // The fraction is spelled digit by digit, so only the integer part has a ceiling. checkMax(integerPart, cardinalMax) let result = '' if (isNegative) { result = NEGATIVE } result += integerToWords(integerPart) if (decimalPart) { result += DECIMAL_SEP + decimalPartToWords(decimalPart) } return result } // ============================================================================ // ORDINAL: toOrdinal(value) // ============================================================================ /** * Converts a positive integer to Japanese ordinal words. * * Japanese ordinals: 第 prefix + cardinal number. * @param {bigint} n - Positive integer to convert * @returns {string} Japanese ordinal words */ function integerToOrdinal(n) { return ORDINAL_PREFIX + integerToWords(n) } /** * Converts a numeric value to Japanese 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(10) // '第十' * toOrdinal(100) // '第百' */ function toOrdinal(value) { const integerPart = parseOrdinalValue(value) // Ordinals prefix the cardinal speller, so they share its ceiling. checkMax(integerPart, ordinalMax) return integerToOrdinal(integerPart) } // ============================================================================ // CURRENCY: toCurrency(value, options?) // ============================================================================ /** * Converts a numeric value to Japanese currency words (Yen). * * Note: Sen (銭, 1/100 yen) is included for completeness but is rarely used * in modern Japan. Most transactions are in whole yen. * @param {number | string | bigint} value - The currency amount to convert * @returns {string} The amount in Japanese 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(1) // '一円' * toCurrency(0.50) // '五十銭' * toCurrency(42.50) // '四十二円五十銭' */ function toCurrency(value) { const { isNegative, dollars: yen, cents: sen } = parseCurrencyValue(value) checkMax(yen, currencyMax) // Build result let result = '' if (isNegative) result = NEGATIVE // Yen part if (yen > 0n) { result += integerToWords(yen) + YEN } // Sen part (1/100 yen) if (sen > 0n) { result += integerToWords(sen) + SEN } // Handle zero case if (yen === 0n && sen === 0n) { result += ZERO + YEN } return result } // ============================================================================ // Public API // ============================================================================ export { toCardinal, toOrdinal, toCurrency }