# Sandhi Source: https://pinyinjs.dev/sandhi/ Index of every page: https://pinyinjs.dev/llms.txt Tone sandhi changes a syllable's tone according to its context. PinyinJS applies it to parsed syllables, including changes across word boundaries. ```ts import { applySandhi, readWord } from "@kensio/pinyinjs"; const buShi = readWord("bùshì") ?? []; applySandhi(buShi); // bú shì, 不 flattens before a fourth tone applySandhi(buShi, { yiBu: false }); // unchanged const niHao = readWord("nǐhǎo") ?? []; applySandhi(niHao); // unchanged by default applySandhi(niHao, { thirdTone: true }); // ní hǎo ``` The dictionary stores underlying tones. This lets you choose whether to apply sandhi when formatting a reading. ## 一 and 不 On by default. 不 changes from `bù` to `bú` before a fourth tone: ```ts 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: ```ts convert(dictionary, "一天"); // "yì tiān", before first tone convert(dictionary, "一起"); // "yìqǐ", before third tone convert(dictionary, "一个"); // "yí gè", before fourth tone convert(dictionary, "一样"); // "yíyàng" convert(dictionary, "第一"); // "dìyī", ordinal, unchanged ``` ### 一 in numbers and ordinals Counting uses can change the tone of 一. Digit sequences and ordinals retain its underlying tone: ```ts convert(dictionary, "十一月"); // "shíyīyuè", a last digit, not a quantity convert(dictionary, "十一点"); // "shíyīdiǎn" convert(dictionary, "第一次"); // "dìyīcì", ordinal convert(dictionary, "万一你来"); // "wànyī nǐ lái" convert(dictionary, "31日"); // "sānshíyī rì", the same through the digits convert(dictionary, "一个"); // "yí gè", still counting, so still assimilates convert(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 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`: ```ts 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. ```ts const henHao = readWord("hěnhǎo") ?? []; applySandhi(henHao); // hěn hǎo applySandhi(henHao, { thirdTone: true }); // hén hǎo ``` ### 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](https://www.researchgate.net/publication/36071823_The_Prosodic_Domain_of_Tone_Sandhi_in_Chinese). PinyinJS approximates these groups with three rules: ```ts 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" ``` 1. 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. 2. A one-syllable word joins the following word's group. This changes 很 in 很喜歡 and 我 and 也 in 我也很好 (`wó yé hén hǎo`). 3. 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 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`: ```ts const reading = readWord("hángzhǎnghěnxǐhuan") ?? []; applySandhi(reading, { thirdTone: true }); // háng zháng hén xǐ huan applySandhi(reading, { thirdTone: true }, [2, 1, 2]); // háng zhǎng hén xǐ huan ``` Each 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 `applySandhi(syllables, options?, grouping?, characters?)` accepts the same options as [`ConvertOptions.sandhi`](https://pinyinjs.dev/options/#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 Erhua is stored in the dictionary and handled by [orthography](https://pinyinjs.dev/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 ```console $ pinyinjs sandhi bùshì bùshì bú shì $ pinyinjs sandhi --third-tone nǐhǎo nǐhǎo ní hǎo ``` `sandhi` accepts written pinyin and runs without a dictionary. The `--no-sandhi` and `--third-tone` flags also work with `convert`, `html` and `explain`.