n2words
Version:
Convert numbers to words in 70+ languages with zero dependencies. Supports BigInt, decimals, and browser/Node.js environments.
368 lines (304 loc) • 11.9 kB
JavaScript
/**
* Simplified Chinese (China) language converter
*
* CLDR: zh-Hans-CN | Simplified Chinese as used in China
*
* Key features:
* - Myriad-based (万, 亿) grouping - 4 digits
* - Formal (financial) vs common numerals
* - Zero insertion for skipped positions
* - No word separators (concatenated format)
*/
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
const ONES_COMMON = ['零', '一', '二', '三', '四', '五', '六', '七', '八', '九']
const TEN_COMMON = '十'
const HUNDRED_COMMON = '百'
const THOUSAND_COMMON = '千'
// Formal (financial) numerals - harder to alter/forge
const ONES_FORMAL = ['零', '壹', '贰', '叁', '肆', '伍', '陆', '柒', '捌', '玖']
const TEN_FORMAL = '拾'
const HUNDRED_FORMAL = '佰'
const THOUSAND_FORMAL = '仟'
// Scale words
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.
// No scale table — bounded by the convertBelowYi recursion (亿² = 10^16),
// declared via the escape hatch and verified by the gate. The three forms share it.
export const cardinalMax = bounded(16)
export const ordinalMax = bounded(16)
export const currencyMax = bounded(16)
const ZERO = '零'
const NEGATIVE = '负'
const DECIMAL_SEP = '点'
// ============================================================================
// Ordinal Vocabulary
// ============================================================================
// Ordinal prefix
const ORDINAL_PREFIX = '第'
// ============================================================================
// Currency Vocabulary (Chinese Yuan / Renminbi)
// ============================================================================
// Yuan (main unit) - formal uses 圆, common uses 元
const YUAN_FORMAL = '圆'
const YUAN_COMMON = '元'
// Jiao (1/10 yuan) - both use 角
const JIAO = '角'
// Fen (1/100 yuan) - both use 分
const FEN = '分'
// "Whole" when no jiao/fen
const ZHENG = '整'
// ============================================================================
// Conversion Functions
// ============================================================================
/**
* Convert number below 万 (10,000) to words using direct string concatenation.
* @param {bigint} value The integer (0-9999) to convert
* @param {string[]} ones Digit words for 0-9
* @param {string} ten Word for the tens place
* @param {string} hundred Word for the hundreds place
* @param {string} thousand Word for the thousands place
* @returns {string} The number in words
*/
function convertBelowWan(value, ones, ten, hundred, thousand) {
if (value === 0n) return ''
let result = ''
let needsZero = false
// Thousands (千)
const thousandsVal = value / 1000n
const thousandsRemainder = value % 1000n
if (thousandsVal > 0n) {
result = ones[Number(thousandsVal)] + thousand
needsZero = thousandsRemainder > 0n && thousandsRemainder < 100n
}
// Hundreds (百)
const hundredsVal = thousandsRemainder / 100n
const hundredsRemainder = thousandsRemainder % 100n
if (hundredsVal > 0n) {
if (needsZero) result += ZERO
result += 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) result += ZERO
result += ones[Number(tensVal)] + ten
needsZero = false
}
else if ((hundredsVal > 0n || thousandsVal > 0n) && onesVal > 0n) {
needsZero = true
}
// Ones
if (onesVal > 0n) {
if (needsZero) result += ZERO
result += ones[Number(onesVal)]
}
return result
}
/**
* Convert number below 亿 (100 million) to words.
* @param {bigint} value The integer to convert
* @param {string[]} ones Digit words for 0-9
* @param {string} ten Word for the tens place
* @param {string} hundred Word for the hundreds place
* @param {string} thousand Word for the thousands place
* @returns {string} The number in words
*/
function convertBelowYi(value, ones, ten, hundred, thousand) {
if (value === 0n) return ''
if (value >= 10_000n) {
const wanValue = value / 10_000n
const wanRemainder = value % 10_000n
let result = convertBelowWan(wanValue, ones, ten, hundred, thousand) + WAN_WORD
if (wanRemainder > 0n) {
const needsZero = (wanValue % 10n === 0n) || (wanRemainder < 1000n)
if (needsZero) result += ZERO
result += convertBelowWan(wanRemainder, ones, ten, hundred, thousand)
}
return result
}
return convertBelowWan(value, ones, ten, hundred, thousand)
}
/**
* Convert an integer to Simplified Chinese words.
* @param {bigint} n The integer to convert
* @param {boolean} [formal] Use formal/financial numerals
* @returns {string} The integer in Simplified 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
// Handle numbers >= 亿 (100 million)
if (n >= 100_000_000n) {
const yiValue = n / 100_000_000n
const yiRemainder = n % 100_000_000n
let result = convertBelowYi(yiValue, ones, ten, hundred, thousand) + YI_WORD
if (yiRemainder > 0n) {
if (yiRemainder < 10_000_000n) result += ZERO
result += convertBelowYi(yiRemainder, ones, ten, hundred, thousand)
}
return result
}
return convertBelowYi(n, ones, ten, hundred, thousand)
}
/**
* Convert decimal digits to words using direct concatenation.
* @param {string} decimalString The decimal digits to convert
* @param {string[]} ones Digit words for 0-9
* @returns {string} The decimal digits in words
*/
function decimalDigitsToWords(decimalString, ones) {
let result = ''
for (let i = 0; i < decimalString.length; i++) {
result += ones[Number(decimalString[i])]
}
return result
}
/**
* @typedef {object} CardinalOptions
* @property {boolean} [formal] - Use formal/financial numerals
*/
/** @type {Required<CardinalOptions>} */
export const cardinalDefaults = { formal: true }
/**
* Converts a numeric value to Simplified Chinese words.
* @param {number | string | bigint} value - The numeric value to convert
* @param {CardinalOptions} [options] - Optional configuration
* @returns {string} The number in Simplified 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 = isNegative ? NEGATIVE : ''
result += integerToWords(integerPart, formal)
if (decimalPart) {
const ones = formal ? ONES_FORMAL : ONES_COMMON
result += DECIMAL_SEP + decimalDigitsToWords(decimalPart, ones)
}
return result
}
// ============================================================================
// ORDINAL: toOrdinal(value)
// ============================================================================
/**
* Converts a positive integer to Simplified Chinese ordinal words.
*
* Chinese ordinals: 第 prefix + cardinal number.
* @param {bigint} n - Positive integer to convert
* @param {boolean} formal - Use formal numerals
* @returns {string} Simplified Chinese ordinal words
*/
function integerToOrdinal(n, formal) {
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 Simplified 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(1, { formal: false }) // '第一'
* toOrdinal(10) // '第壹拾'
*/
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 Simplified Chinese currency words (Yuan/Renminbi).
* @param {number | string | bigint} value - The currency amount to convert
* @param {CurrencyOptions} [options] - Optional configuration
* @returns {string} The amount in Simplified 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.50) // '肆拾贰圆伍角整'
* toCurrency(1) // '壹圆整'
* toCurrency(0.05) // '伍分'
* toCurrency(42.50, { formal: false }) // '四十二元五角整'
*/
function toCurrency(value, options) {
const { isNegative, dollars: yuan, cents } = parseCurrencyValue(value)
checkMax(yuan, currencyMax)
const { formal } = resolveOptions(options, currencyDefaults)
const ones = formal ? ONES_FORMAL : ONES_COMMON
const yuanWord = formal ? YUAN_FORMAL : YUAN_COMMON
// Split cents into jiao (tens) and fen (ones)
const jiao = cents / 10n
const fen = cents % 10n
// Build result
let result = ''
if (isNegative) result = NEGATIVE
// Yuan part
if (yuan > 0n) {
result += integerToWords(yuan, formal) + yuanWord
}
// Jiao part (1/10)
if (jiao > 0n) {
result += ones[Number(jiao)] + JIAO
}
else if (yuan > 0n && fen > 0n) {
// Need zero placeholder between yuan and fen
result += ZERO
}
// Fen part (1/100)
if (fen > 0n) {
result += ones[Number(fen)] + FEN
}
else if (yuan > 0n || jiao > 0n) {
// Add 整 (zheng) to indicate whole amount
result += ZHENG
}
// Handle zero case
if (yuan === 0n && jiao === 0n && fen === 0n) {
result += ZERO + yuanWord + ZHENG
}
return result
}
// ============================================================================
// Exports
// ============================================================================
export { toCardinal, toOrdinal, toCurrency }