n2words
Version:
Convert numbers to words in 70+ languages with zero dependencies. Supports BigInt, decimals, and browser/Node.js environments.
385 lines (323 loc) • 12 kB
JavaScript
/**
* Traditional Chinese (Taiwan) language converter
*
* CLDR: zh-Hant-TW | Traditional Chinese as used in Taiwan
*
* Key features:
* - Myriad-based (萬, 億) grouping - 4 digits
* - Formal (financial) vs common numerals
* - Zero insertion for skipped positions
* - No word separators (concatenated format)
*
* Differences from Simplified:
* - Different character forms (e.g., 負/负, 點/点, 億/亿, 萬/万)
* - Some formal numerals differ (參/叁, 貳/贰, 陸/陆)
*/
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 { bounded } from './utils/scale.js'
import { resolveOptions } from './utils/resolve-options.js'
// ============================================================================
// Vocabulary
// ============================================================================
// Common (everyday) numerals - Traditional forms
const ONES_COMMON = ['零', '一', '二', '三', '四', '五', '六', '七', '八', '九']
const TEN_COMMON = '十'
const HUNDRED_COMMON = '百'
const THOUSAND_COMMON = '千'
// Formal (financial) numerals - Traditional forms
const ONES_FORMAL = ['零', '壹', '貳', '參', '肆', '伍', '陸', '柒', '捌', '玖']
const TEN_FORMAL = '拾'
const HUNDRED_FORMAL = '佰'
const THOUSAND_FORMAL = '仟'
// Scale words - Traditional forms
const WAN_WORD = '萬' // 10,000
const YI_WORD = '億' // 100,000,000
// Supported magnitude ceiling (checked at the public entry points). Numbers
// >= 億 (10^8) split into n / 10^8, which is then spelled by convertBelowYi —
// itself only valid below 億. So the ceiling is 億² = 10^16. Ordinal (第 +
// cardinal) and currency build on the cardinal, so they share it. Decimals are
// spelled digit-by-digit, so they have no ceiling.
export const cardinalMax = bounded(16)
export const ordinalMax = bounded(16)
export const currencyMax = bounded(16)
const ZERO = '零'
const NEGATIVE = '負'
const DECIMAL_SEP = '點'
// ============================================================================
// Ordinal Vocabulary
// ============================================================================
const ORDINAL_PREFIX = '第'
// ============================================================================
// Currency Vocabulary (New Taiwan Dollar)
// ============================================================================
// Formal currency (default)
const YUAN_FORMAL = '圓'
const JIAO_FORMAL = '角'
const FEN_FORMAL = '分'
const ZHENG_FORMAL = '整' // "exactly" suffix when whole amount
// Common currency
const YUAN_COMMON = '元'
// ============================================================================
// Conversion Functions
// ============================================================================
/**
* Converts a non-negative integer to Traditional Chinese words.
* @param {bigint} n - The integer value to convert
* @param {boolean} [formal] - Use formal/financial numerals
* @returns {string} The integer in Traditional Chinese words
*/
function integerToWords(n, formal = true) {
if (n === 0n) return ZERO
const ones = formal ? ONES_FORMAL : ONES_COMMON
const ten = formal ? TEN_FORMAL : TEN_COMMON
const hundred = formal ? HUNDRED_FORMAL : HUNDRED_COMMON
const thousand = formal ? THOUSAND_FORMAL : THOUSAND_COMMON
// Convert number below 萬 (10,000)
/**
* @param {bigint} value The number below 10,000 to convert
* @returns {string} The number in Traditional Chinese words
*/
function convertBelowWan(value) {
if (value === 0n) return ''
const parts = []
let needsZero = false
// Thousands (千)
const thousandsVal = value / 1000n
const thousandsRemainder = value % 1000n
if (thousandsVal > 0n) {
parts.push(ones[Number(thousandsVal)] + thousand)
needsZero = thousandsRemainder > 0n && thousandsRemainder < 100n
}
// Hundreds (百)
const hundredsVal = thousandsRemainder / 100n
const hundredsRemainder = thousandsRemainder % 100n
if (hundredsVal > 0n) {
if (needsZero) {
parts.push(ZERO)
}
parts.push(ones[Number(hundredsVal)] + hundred)
needsZero = hundredsRemainder > 0n && hundredsRemainder < 10n
}
else if (thousandsVal > 0n && hundredsRemainder > 0n) {
needsZero = true
}
// Tens (十)
const tensVal = hundredsRemainder / 10n
const onesVal = hundredsRemainder % 10n
if (tensVal > 0n) {
if (needsZero) {
parts.push(ZERO)
needsZero = false
}
parts.push(ones[Number(tensVal)] + ten)
}
else if ((hundredsVal > 0n || thousandsVal > 0n) && onesVal > 0n) {
needsZero = true
}
// Ones
if (onesVal > 0n) {
if (needsZero) {
parts.push(ZERO)
}
parts.push(ones[Number(onesVal)])
}
return parts.join('')
}
// Convert number below 億 (100 million)
/**
* @param {bigint} value The number below 100,000,000 to convert
* @returns {string} The number in Traditional Chinese words
*/
function convertBelowYi(value) {
if (value === 0n) return ''
const parts = []
if (value >= 10_000n) {
const wanValue = value / 10_000n
const wanRemainder = value % 10_000n
parts.push(convertBelowWan(wanValue) + WAN_WORD)
if (wanRemainder > 0n) {
const wanEndsWithZero = wanValue % 10n === 0n
const remainderMissesThousands = wanRemainder < 1000n
const needsZero = wanEndsWithZero || remainderMissesThousands
if (needsZero) {
parts.push(ZERO)
}
parts.push(convertBelowWan(wanRemainder))
}
}
else {
parts.push(convertBelowWan(value))
}
return parts.join('')
}
// Main conversion
const parts = []
if (n >= 100_000_000n) {
const yiValue = n / 100_000_000n
const yiRemainder = n % 100_000_000n
const yiWords = convertBelowYi(yiValue)
parts.push(yiWords + YI_WORD)
if (yiRemainder > 0n) {
const needsZero = yiRemainder < 10_000_000n
if (needsZero) {
parts.push(ZERO)
}
parts.push(convertBelowYi(yiRemainder))
}
}
else {
parts.push(convertBelowYi(n))
}
return parts.join('')
}
/**
* Converts each digit of a decimal string to Traditional Chinese words.
* @param {string} decimalString - The decimal digits to convert
* @param {boolean} [formal] - Use formal/financial numerals
* @returns {string[]} The decimal digits as Traditional Chinese words
*/
function decimalDigitsToWords(decimalString, formal = true) {
const ones = formal ? ONES_FORMAL : ONES_COMMON
const words = []
for (const char of decimalString) {
words.push(ones[Number(char)])
}
return words
}
/**
* @typedef {object} CardinalOptions
* @property {boolean} [formal] - Use formal/financial numerals
*/
/** @type {Required<CardinalOptions>} */
export const cardinalDefaults = { formal: true }
/**
* Converts a numeric value to Traditional Chinese words.
* @param {number | string | bigint} value - The numeric value to convert
* @param {CardinalOptions} [options] - Optional configuration
* @returns {string} The number in Traditional Chinese words
*/
function toCardinal(value, options) {
const { isNegative, integerPart, decimalPart } = parseCardinalValue(value)
checkMax(integerPart, cardinalMax)
// Apply option defaults
const { formal } = resolveOptions(options, cardinalDefaults)
let result = ''
if (isNegative) {
result = NEGATIVE
}
result += integerToWords(integerPart, formal)
if (decimalPart) {
result += DECIMAL_SEP + decimalDigitsToWords(decimalPart, formal).join('')
}
return result
}
// ============================================================================
// ORDINAL: toOrdinal(value, options?)
// ============================================================================
/**
* Converts a non-negative integer to Traditional Chinese ordinal words.
*
* Traditional Chinese ordinals use "第" prefix + cardinal number.
* @param {bigint} n - Positive integer to convert
* @param {boolean} formal - Use formal/financial numerals
* @returns {string} Traditional Chinese ordinal words
*/
function integerToOrdinal(n, formal = true) {
return ORDINAL_PREFIX + integerToWords(n, formal)
}
/**
* @typedef {object} OrdinalOptions
* @property {boolean} [formal] - Use formal/financial numerals
*/
/** @type {Required<OrdinalOptions>} */
export const ordinalDefaults = { formal: true }
/**
* Converts a numeric value to Traditional Chinese ordinal words.
* @param {number | string | bigint} value - The numeric value to convert (positive integer)
* @param {OrdinalOptions} [options] - Optional configuration
* @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(1, { formal: false }) // '第一'
*/
function toOrdinal(value, options) {
const integerPart = parseOrdinalValue(value)
checkMax(integerPart, ordinalMax)
const { formal } = resolveOptions(options, ordinalDefaults)
return integerToOrdinal(integerPart, formal)
}
// ============================================================================
// CURRENCY: toCurrency(value, options?)
// ============================================================================
/**
* @typedef {object} CurrencyOptions
* @property {boolean} [formal] - Use formal/financial numerals
*/
/** @type {Required<CurrencyOptions>} */
export const currencyDefaults = { formal: true }
/**
* Converts a numeric value to Traditional Chinese currency words (New Taiwan Dollar).
*
* Uses 圓 (yuan), 角 (jiao, 1/10), 分 (fen, 1/100).
* Formal writing adds 整 (zheng) for whole amounts.
* @param {number | string | bigint} value - The currency amount to convert
* @param {CurrencyOptions} [options] - Optional configuration
* @returns {string} The amount in Traditional Chinese 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.50) // '壹圓伍角整'
* toCurrency(42, { formal: false }) // '四十二元整'
*/
function toCurrency(value, options) {
const { isNegative, dollars: yuan, cents } = parseCurrencyValue(value)
checkMax(yuan, currencyMax)
const { formal } = resolveOptions(options, currencyDefaults)
const yuanWord = formal ? YUAN_FORMAL : YUAN_COMMON
// Split cents into jiao (tens) and fen (ones)
const jiao = cents / 10n
const fen = cents % 10n
let result = ''
if (isNegative) {
result = NEGATIVE
}
// Yuan part (always show for zero with no cents)
if (yuan > 0n || cents === 0n) {
result += integerToWords(yuan, formal)
result += yuanWord
}
// Jiao part (tens of cents)
if (jiao > 0n) {
const ones = formal ? ONES_FORMAL : ONES_COMMON
result += ones[Number(jiao)] + JIAO_FORMAL
}
// Fen part (ones of cents)
if (fen > 0n) {
const ones = formal ? ONES_FORMAL : ONES_COMMON
// Need zero if we have yuan but no jiao
if (yuan > 0n && jiao === 0n) {
result += ZERO
}
result += ones[Number(fen)] + FEN_FORMAL
}
else if (jiao > 0n) {
// Has jiao but no fen - add 整
result += ZHENG_FORMAL
}
else {
// Whole yuan only - add 整
result += ZHENG_FORMAL
}
return result
}
// ============================================================================
// Exports
// ============================================================================
export { toCardinal, toOrdinal, toCurrency }