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, unchangedThe characters 一 and 不
Section titled “The characters 一 and 不”Both rules apply to a character rather than to a sound. 医, 依, 衣 and 伊 are all yī, and 部, 布, 步 and 埠 are all bù. None of them changes tone for what follows it:
convert(dictionary, "医生"); // "yīshēng", not yìshēngconvert(dictionary, "依然"); // "yīrán"convert(dictionary, "部队"); // "bùduì", not búduìconvert(dictionary, "全部是"); // "quánbù shì", across a word boundary tooconvert knows which Han characters it read and applies both rules only to 一 and 不. applySandhi given pinyin alone has only the spelling, so pinyinjs sandhi yīshēng still changes a 医 it cannot see.
A 一 that ends a word
Section titled “A 一 that ends a word”一 assimilates to the syllable it counts, and that syllable is inside its own word. A 一 at the end of a longer word counts nothing, so it keeps yī whatever starts the next word:
convert(dictionary, "唯一办法"); // "wéiyī bànfǎ", not wéiyíconvert(dictionary, "统一中国"); // "tǒngyī Zhōngguó"convert(dictionary, "星期一去"); // "xīngqīyī qù"convert(dictionary, "一个"); // "yí gè", here 一 is a word on its ownThe last line is the distinction. A 一 counting a 个 is a one-syllable word that joins the following word’s group, while the 一 of 唯一 is already inside a word. 不 behaves differently and still changes at the end of a word, since what it negates is often in the next one.
This comes from the word boundaries the decoder found. Supply a SandhiGrouping to applySandhi where you know them.
一 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 时, dì may represent 第 or 地, and yī may represent 一 or 医.
In 88,866 corpus lines, providing the characters corrected 1,575 一 tone decisions compared with using spellings alone. The count was measured for this rule alone, before the two rules above were added:
| 一 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 and 一 also need 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, and is what identifies 一 and 不. 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.
