Skip to content

Numbers

PinyinJS converts numbers to Chinese numerals and pinyin without loading a dictionary.

import { numeralHanzi, readNumeral } from "@kensio/pinyinjs";
numeralHanzi(12345); // "一万两千三百四十五"
numeralHanzi(2026, { style: "digits" }); // "二〇二六"

A number can be read as a quantity or as individual digits:

numeralHanzi(2026); // "两千零二十六", 2026个, a quantity
numeralHanzi(2026, { style: "digits" }); // "二〇二六", 2026年, a year

Choose the style for standalone numbers. For example, a year is usually read digit by digit, while a count uses a quantity. The default style is cardinal.

The following rules determine where 零 and 十 appear:

numeralHanzi(10); // "十", not 一十
numeralHanzi(115); // "一百一十五", but here the 一 stays
numeralHanzi(1005); // "一千零五", a skipped place is spoken
numeralHanzi(1500); // "一千五百", a trailing one is not
numeralHanzi(25_000); // "两万五千"
numeralHanzi(20_050); // "两万零五十", the lower group leaves a gap
numeralHanzi(100_000_005); // "一亿零五"

Chinese groups large numbers by 万 (ten thousand). For example, 12,345 is one 万 plus 2,345.

With the default setting, a lone 2 before 千, 万 or 亿 is written 两. Examples include 两千, 两万 and 一万两千. Other positions use 二, as in 十二, 二十, 二百 and 十二万.

The liang option controls this choice. leading follows the dictionary prescription that excludes 两 in forms such as 三万二千. always also permits 两 there:

numeralHanzi(12_000); // "一万两千", always, the default
numeralHanzi(12_000, { liang: "leading" }); // "一万二千", the 词典's own rule
numeralHanzi(12_000, { liang: "never" }); // "一万二千", and 二千 for 2,000 too

Set counts when a standalone 2 counts a following noun or measure word, as in 两个西瓜 or 两个人:

numeralHanzi(2); // "二", a number counting nothing
numeralHanzi(2, { counts: true }); // "两", 两个西瓜
numeralHanzi(12, { counts: true }); // "十二", the 二 of 十二个 is inside the number
numeralHanzi(200, { counts: true }); // "二百", and so is the 二 of 二百个

This changes only a standalone 2. liang: "never" keeps 二 even when counts is set.

numeralHanzi("007", { style: "digits" }); // "〇〇七", the zeros survive
numeralHanzi("007"); // "七", counted, they do not
numeralHanzi(2019, { style: "digits", zero: "" }); // "二零一九"

Pass a string to preserve leading zeros, such as those in a room number. A JavaScript number cannot retain them.

import { writeSyllable } from "@kensio/pinyinjs";
const said = (value, options) =>
readNumeral(value, options)
?.map((s) => writeSyllable(s))
.join(" ");
said(2026); // "liǎng qiān líng èr shí liù"
said(2026, { style: "digits" }); // "èr líng èr liù"
said(110, { style: "digits", yao: true }); // "yāo yāo líng"
said("3.14"); // "sān diǎn yī sì"
said(-40); // "fù sì shí"

The optional yao setting reads 一 as yāo in digit sequences, as in telephone numbers. It is off by default. Ordinary year readings use , as in 2019 (èr líng yī jiǔ).

The returned syllables have underlying tones. For example, 一 is returned as . Apply tone sandhi when you need its contextual spoken tone:

import { applySandhi } from "@kensio/pinyinjs";
applySandhi(readNumeral(100) ?? []); // yì bǎi

Apply sandhi to the integer part of a cardinal number. Individually spoken digits retain their underlying tones, including digits after a decimal point. For example, 一百 becomes yìbǎi, while 3.14 is sān diǎn yī sì. The pinyinjs number command applies this distinction.

Chinese reads the denominator before the numerator. Use these functions for the correct order:

percentHanzi(95); // "百分之九十五", "of a hundred parts, ninety-five"
fractionHanzi(3, 4); // "四分之三", the denominator is named first
Terminal window
$ pinyinjs number 2026 --digits
2026 二〇二六 èr líng èr liù
$ pinyinjs number 95 --percent
95 百分之九十五 bǎi fēn zhī jiǔ shí wǔ

The command runs without a dictionary. See the command line for its options.

convert reads numbers within Chinese text and infers their style from the surrounding characters:

convert(dictionary, "我有3个苹果。"); // "Wǒ yǒu sān gè píngguǒ."
convert(dictionary, "1988年之后"); // "yī jiǔ bā bā nián zhīhòu"
convert(dictionary, "95%的人"); // "bǎifēnzhījiǔshíwǔ de rén"
convert(dictionary, "3D打印"); // "sān D dǎyìn"
convert(dictionary, "我有3个", { numbers: "keep" }); // "wǒ yǒu3gè"

Style selection follows these rules:

The text What happens Why
four digits before 年 spelled out 1998年 is a year; 30年 is thirty years
digits before % or % 百分之, reversed the sign is read, and read first
anything else counted what almost every digit in running text is

A standalone 2 immediately before a Han character normally reads 两. This sets the counts behaviour described above:

convert(dictionary, "我们买了2个西瓜"); // "wǒmen mǎile liǎng gè xīguā"
convert(dictionary, "他2岁"); // "tā liǎng suì"
convert(dictionary, "2点"); // "liǎng diǎn", the same 两 as 2:00
convert(dictionary, "2万人"); // "liǎng wàn rén", as 20,000 is 两万

Exceptions use 二 when the digit labels a month, day, number, floor, route, class or period (月, 日, 号, 楼, 路, 班 and 期). Ordinals preceded by 第 also use 二. 十 and 百 keep the forms 二十 and 二百.

convert(dictionary, "2月"); // "èr yuè", February
convert(dictionary, "2号"); // "èr hào", the second of the month
convert(dictionary, "第2次"); // "dì èr cì", an ordinal names a position
convert(dictionary, "12个"); // "shí'èr gè", a 2 inside a larger number

A cardinal number is written as one pinyin word. For example, 25个 is èrshíwǔ gè. Digit sequences remain separated, as in 1998年 (yī jiǔ jiǔ bā nián). Sandhi crosses the word boundary, so 1个 becomes yí gè.

A decimal joins the cardinal integer part into one word, then writes 点 and the fractional digits separately:

convert(dictionary, "一共75.5元"); // "yígòng qīshíwǔ diǎn wǔ yuán"
convert(dictionary, "3.14"); // "sān diǎn yī sì"

Digits touching -, / or their full-width equivalents are preserved. This includes labels such as 3202-5625 and COVID-19.

Colon-separated times are read aloud:

convert(dictionary, "6:30起床"); // "liù diǎn sānshí fēn qǐchuáng"
convert(dictionary, "2:30"); // "liǎng diǎn sānshí fēn", two o'clock is 两
convert(dictionary, "12:00"); // "shí'èr diǎn", no 零零分 on the hour
convert(dictionary, "16:9的"); // "16:9de", a ratio, and left alone

Time recognition requires exactly two digits after the colon. In the measured corpus, all 104 matching examples were times. Ratios and scores such as 16:9 and 2:1 remained unchanged.

Time output includes 分 after the minutes to distinguish a time from a decimal. The hour, 点, minutes and 分 are separate words.

A four-digit number without 年 is read as a quantity. For example, 他生于1990。 does not automatically select year-style digits. Use the standalone number functions with an explicit style when the application knows that the number is a year or label.

Currency expressions such as $5 and ¥100 are preserved. The converter does not infer a currency name from the symbol.

Dashed and slashed dates are preserved, and phone numbers do not automatically select yāo. Dates written with 年月日 use the normal number rules. For example, 三月 is sān yuè and 三十一日 is sānshíyī rì.

pnpm numerals compares numeral readings with CC-CEDICT. All 17 digit sequences with recorded readings match when the appropriate settings are supplied. Three emergency numbers require yāo. The source examples all spell out digits, so cardinal readings are tested separately with worked examples.

End-to-end convert matches 7 of those 17 examples. The remaining ten require label-style digits, while automatic conversion selects a quantity. These include 110, 88, 996 and 95后. Context does not always distinguish a quantity from a label.