Sandhi
Tone sandhi changes a syllable’s tone according to its context. PinyinJS applies it to parsed syllables, including changes across word boundaries.
import { applySandhi, readWord } from "@kensio/pinyinjs";
const buShi = readWord("bùshì") ?? [];applySandhi(buShi); // bú shì, 不 flattens before a fourth toneapplySandhi(buShi, { yiBu: false }); // unchanged
const niHao = readWord("nǐhǎo") ?? [];applySandhi(niHao); // unchanged by defaultapplySandhi(niHao, { thirdTone: true }); // ní hǎoThe dictionary stores underlying tones. This lets you choose whether to apply sandhi when formatting a reading.
一 and 不
Section titled “一 and 不”On by default.
不 changes from bù to bú before a fourth tone:
convert(dictionary, "不是"); // "bú shì"convert(dictionary, "不对"); // "bú duì"convert(dictionary, "不行"); // "bùxíng", 行 is second tone here, so no change一 changes from yī to yì before tones 1, 2 and 3, and to yí before tone 4. It keeps yī in ordinals and final position:
convert(dictionary, "一天"); // "yì tiān", before first toneconvert(dictionary, "一起"); // "yìqǐ", before third toneconvert(dictionary, "一个"); // "yí gè", before fourth toneconvert(dictionary, "一样"); // "yíyàng"convert(dictionary, "第一"); // "dìyī", ordinal, unchanged一 in numbers and ordinals
Section titled “一 in numbers and ordinals”Counting uses can change the tone of 一. Digit sequences and ordinals retain its underlying tone:
convert(dictionary, "十一月"); // "shíyīyuè", a last digit, not a quantityconvert(dictionary, "十一点"); // "shíyīdiǎn"convert(dictionary, "第一次"); // "dìyīcì", ordinalconvert(dictionary, "万一你来"); // "wànyī nǐ lái"convert(dictionary, "31日"); // "sānshíyī rì", the same through the digitsconvert(dictionary, "一个"); // "yí gè", still counting, so still assimilatesconvert(dictionary, "当时一个人"); // "dāngshí yí gè rén", 时 is not 十convert(dictionary, "那是一条狗"); // "nà shì yìtiáo gǒu", nor is 是The rule identifies a number-final 一 by a preceding numeral and the absence of a following numeral. The middle 一 in 一百一十 still counts 十 and remains eligible for sandhi.
convert uses the source Chinese characters to distinguish numeral uses. applySandhi can also work from pinyin alone, but identical spellings can be ambiguous. For example, shí may represent 十 or 时, and dì may represent 第 or 地.
In 88,866 corpus lines, providing the characters corrected 1,575 一 tone decisions compared with using spellings alone:
| 一 after 是 (是一个, 那是一样) | 1,338 |
| after 前, 试, 晚, 释, 始, 时, 弟 | 231 |
| a real 十 shedding a wrong assimilation | 6 |
| broken | 0 |
The spelling-based numeral set excludes 亿 because its reading overlaps with non-numeral characters such as 意 and 议. Character-aware conversion can distinguish these cases. Its numeral set includes 十, 百, 千 and 万, with traditional and financial forms.
Turn both off with sandhi: { yiBu: false }, or --no-sandhi at the command
line.
Third tone
Section titled “Third tone”Off by default.
A third tone can become a second tone before another third tone. For example, 你好 is spoken ní hǎo. Standard pinyin spelling retains nǐ hǎo:
convert(dictionary, "好好"); // "hǎohǎo"convert(dictionary, "好好", { sandhi: { thirdTone: true } }); // "háohǎo"Enable third-tone sandhi for pronunciation guides or speech exercises. Leave it disabled when you want ordinary written pinyin.
const henHao = readWord("hěnhǎo") ?? [];applySandhi(henHao); // hěn hǎoapplySandhi(henHao, { thirdTone: true }); // hén hǎoWord and phrase grouping
Section titled “Word and phrase grouping”Third-tone sandhi depends on prosodic feet, groups of syllables pronounced together. It is strongest within a foot and more optional across larger boundaries. The reference used here is Shih 1986.
PinyinJS approximates these groups with three rules:
const said = { sandhi: { thirdTone: true } };convert(dictionary, "展览馆", said); // "zhánlánguǎn", 展览 + 馆convert(dictionary, "纸老虎", said); // "zhǐláohǔ", 纸 + 老虎convert(dictionary, "老板很好", said); // "láobǎn hén hǎo"- Within a word, dictionary boundaries determine the order of changes. 展覽館 splits as 展覽 + 館, while 紙老虎 splits as 紙 + 老虎. A split requires both parts to be dictionary words, and the most even split is preferred.
- A one-syllable word joins the following word’s group. This changes 很 in 很喜歡 and 我 and 也 in 我也很好 (
wó yé hén hǎo). - Two multi-syllable words remain separate groups. For example, 行長 and 很喜歡 keep the boundary in
hángzhǎng hén xǐhuan.
The approximation misses one-syllable words that attach to the preceding word. For example, it writes 保管好 as báoguǎn hǎo, while the intended pronunciation is báoguán hǎo. Resolving this requires grammatical information beyond word boundaries.
Across word boundaries
Section titled “Across word boundaries”The pass processes the complete syllable array. 一 and 不 can change tone in response to a syllable in the next word.
Third-tone sandhi also needs word grouping. convert supplies the decoder’s groups. A direct applySandhi call treats the reading as one word unless you supply SandhiGrouping:
const reading = readWord("hángzhǎnghěnxǐhuan") ?? [];applySandhi(reading, { thirdTone: true }); // háng zháng hén xǐ huanapplySandhi(reading, { thirdTone: true }, [2, 1, 2]); // háng zhǎng hén xǐ huanEach grouping entry describes one word, using either its syllable count or the counts of its parts. For example, [[1, 2]] describes 紙老虎. Groupings with the wrong total syllable count are ignored. The sandhi CLI command derives groups from spaces in its input.
Options
Section titled “Options”applySandhi(syllables, options?, grouping?, characters?) accepts the same options as ConvertOptions.sandhi. The optional characters array associates each syllable with its source Han character. Use undefined when one character cannot represent the syllable:
| Field | Default | Does |
|---|---|---|
yiBu |
true |
一 and 不 tone changes |
thirdTone |
false |
third tone before third tone |
Options are merged with the defaults. { thirdTone: true } keeps yiBu enabled.
Limitations
Section titled “Limitations”Erhua is stored in the dictionary and handled by orthography. The sandhi pass does not change it. The half-third tone before a non-third tone has no separate pinyin spelling and is not marked.
From the command line
Section titled “From the command line”$ pinyinjs sandhi bùshìbùshì bú shì
$ pinyinjs sandhi --third-tone nǐhǎonǐhǎo ní hǎosandhi accepts written pinyin and runs without a dictionary. The --no-sandhi and --third-tone flags also work with convert, html and explain.
