# Checking typed pinyin
Source: https://pinyinjs.dev/checking/
Index of every page: https://pinyinjs.dev/llms.txt
`check` compares typed pinyin with Chinese text and reports a verdict for each
syllable.
```ts
import { check } from "@kensio/pinyinjs";
const marked = check(dictionary, "银行", "yínxíng");
marked.syllables.map((one) => one.verdict); // ["correct", "wrong"]
marked.syllables[1]?.source; // "行"
marked.score; // 0.5
```
Use it to mark pronunciation exercises and highlight missing syllables, wrong
readings or tone mistakes.
## Accepted variations
The checker accepts several ways to write the same reading.
Tone marks and tone numbers can be mixed within a word. The
[syllable parser](https://pinyinjs.dev/syllables/) treats `bei3` and `běi` as the same syllable:
```ts
check(dictionary, "北京", "běijīng").isCorrect; // true
check(dictionary, "北京", "bei3jing1").isCorrect; // true
check(dictionary, "北京", "bei3jīng").isCorrect; // true
```
When the decoder is uncertain, the checker accepts its plausible alternatives.
For 行 alone, this includes `xíng`, `háng` and `héng`:
```ts
check(dictionary, "行", "xíng").isCorrect; // true
check(dictionary, "行", "háng").isCorrect; // true
```
A reading that breaks a recognised word is rejected. For example, the 行 in
银行 must be `háng`:
```ts
check(dictionary, "银行", "yínxíng").syllables[1]?.verdict; // "wrong"
```
This uses the same threshold as [`isUncertain`](https://pinyinjs.dev/confidence/). An alternative
is accepted if choosing it would cost less than one word boundary.
Both underlying tones and [sandhi](https://pinyinjs.dev/sandhi/) forms are accepted. 你好 can be
`nǐ hǎo` or `ní hǎo`, and 不是 can be `bù shì` or `bú shì`:
```ts
check(dictionary, "你好", "nǐ hǎo").isCorrect; // true
check(dictionary, "你好", "ní hǎo").isCorrect; // true
check(dictionary, "不是", "bú shì").isCorrect; // true
check(dictionary, "不是", "bù shì").isCorrect; // true
```
An omitted tone is reported separately from a wrong tone:
```ts
check(dictionary, "北京", "bei jing").syllables.map((one) => one.verdict);
// ["toneless", "toneless"]
check(dictionary, "北京", "bei3jing3").syllables.map((one) => one.verdict);
// ["correct", "tone"]
```
Apostrophes mark syllable boundaries. The checker accepts their omission when
the syllables are still unambiguous:
```ts
check(dictionary, "西安", "Xī'ān").isCorrect; // true
check(dictionary, "西安", "xi1an1").isCorrect; // true
check(dictionary, "海鸥", "hǎiōu").isCorrect; // true, the mark is optional there
```
For 西安, `xiān` is wrong because it is one syllable. `Xīān` passes because
the two tone marks identify two syllables.
Word spacing is reported separately. See [word spacing](#word-spacing).
## Syllable verdicts
The result contains one entry per expected or typed syllable, in order.
| Verdict | Means |
| ---------- | ---------------------------------------------- |
| `correct` | right syllable, right tone |
| `toneless` | right syllable, no tone written |
| `tone` | right syllable, wrong tone |
| `wrong` | wrong syllable |
| `missing` | a syllable of the reading that was not typed |
| `extra` | a syllable typed that the reading does not use |
The `spacing` field reports word spacing separately from the syllable verdict.
An unmarked neutral-tone syllable is `correct`. Pinyin normally writes the
neutral tone without a mark:
```ts
check(dictionary, "我的书", "wǒ de shū").isCorrect; // true
```
### Requiring tones
By default, `toneless` counts as correct. Set `tones: "required"` to count it
as a mistake. The verdict remains `toneless` in either mode.
```ts
check(dictionary, "北京", "bei jing").isCorrect; // true
check(dictionary, "北京", "bei jing", { tones: "required" }).isCorrect; // false
```
## Word spacing
The `spacing` field reports whether word boundaries match. For example,
`yín háng` has the right syllables for 银行 but splits one word into two:
```ts
const split = check(dictionary, "银行", "yín háng");
split.syllables.map((one) => one.verdict); // ["correct", "correct"]
split.syllables.map((one) => one.spacing); // ["correct", "split"]
```
| Spacing | Means |
| --------- | --------------------------------------------- |
| `correct` | a word begins here, or does not, as it should |
| `split` | a word was written as two |
| `joined` | two words were written as one |
`spacing` is undefined for a missing or extra syllable because it has no
counterpart to compare.
Spacing is always reported. Set `spacing: "required"` to include it in the score:
```ts
check(dictionary, "银行", "yín háng").isCorrect; // true
check(dictionary, "银行", "yín háng", { spacing: "required" }).isCorrect; // false
```
Spacing is excluded from the score by default. The
[orthography implementation](https://pinyinjs.dev/orthography/) covers only part of the standard,
and some valid spellings may differ from its output. Enable spacing checks
when the exercise is specifically testing word boundaries.
### Accepted boundaries
With `spacing: "required"`, the checker accepts these variations:
```ts
const graded = { spacing: "required" } as const;
```
Both orthographic grouping and dictionary word boundaries are accepted.
Orthographic grouping joins aspect particles to verbs and separates the generic
part of a place name. `grouping: false` uses dictionary words directly:
```ts
check(dictionary, "他看了", "tā kànle", graded).isCorrect; // true, 分词连写
check(dictionary, "他看了", "tā kàn le", graded).isCorrect; // true, the words
check(dictionary, "南京市", "Nánjīng Shì", graded).isCorrect; // true
check(dictionary, "南京市", "Nánjīngshì", graded).isCorrect; // true
```
A hyphenated word can be written with a space or joined without the hyphen.
For example, 干干净净 is normally `gāngān-jìngjìng`:
```ts
check(dictionary, "干干净净", "gāngān-jìngjìng", graded).isCorrect; // true
check(dictionary, "干干净净", "gāngān jìngjìng", graded).isCorrect; // true
check(dictionary, "干干净净", "gāngānjìngjìng", graded).isCorrect; // true
```
Other missing or extra boundaries are reported as spacing mistakes:
```ts
check(dictionary, "我要去北京。", "wǒyàoqùběijīng", graded).syllables.map(
(one) => one.spacing,
); // ["correct", "joined", "joined", "joined", "correct"]
```
## Syllable details
| Field | Is |
| ----------- | ------------------------------------------------------- |
| `verdict` | one of the six above |
| `spacing` | `correct`, `split` or `joined` |
| `isCorrect` | whether it counts as right in the score |
| `expected` | the `Syllable` expected here, or undefined for an extra |
| `actual` | the `Syllable` typed here, or undefined for a missing |
| `text` | what was typed, exactly as written |
| `source` | the characters the expected syllable reads |
| `at` | where those characters start, in code points |
Use `source` and `at` to highlight the characters associated with a mistake.
```ts
const marked = check(dictionary, "我要去银行", "wǒ yào qù yínxíng");
marked.syllables
.filter((one) => !one.isCorrect)
.map((one) => [one.source, one.at, one.text]);
// [["行", 4, "xíng"]]
```
`text` preserves what the learner typed. If it cannot be parsed as a syllable,
`actual` is undefined and the verdict is `wrong`.
## Score
`score` is the proportion of reported syllables counted as correct, from 0 to 1.
`isCorrect` is true when every reported syllable counts as correct.
The denominator includes extra typed syllables. Adding a syllable and omitting
a syllable both reduce the score.
```ts
check(dictionary, "北京", "běi běi jīng").score; // 2/3
check(dictionary, "北京市", "běi shì").score; // 2/3
```
## Alignment
The checker aligns expected and typed syllables by their toneless spellings
before grading them. A missing syllable does not shift every subsequent
syllable into the wrong position:
```ts
check(dictionary, "北京市", "běi shì").syllables.map((one) => one.verdict);
// ["correct", "missing", "correct"]
```
Unmatched syllables within each gap are paired as substitutions. For 银行 typed
as `yínxíng`, `yín` aligns and `xíng` is one wrong syllable in place of `háng`.
## Options
`check` accepts every [conversion option](https://pinyinjs.dev/options/), plus `tones` and
`spacing` to control scoring.
| Option | Default | Values |
| --------- | ------------ | -------------------------- |
| `tones` | `"optional"` | `"optional"`, `"required"` |
| `spacing` | `"optional"` | `"optional"`, `"required"` |
Use `readings` when the exercise requires a particular pronunciation:
```ts
check(dictionary, "这篇文章不太长。", "zhè piān wénzhāng bú tài cháng", {
readings: { 太长: "tài cháng" },
}).isCorrect; // true
```
`locale: "zh-TW"` grades against the 國語 reading, so 垃圾 reads `lèsè` where
`zh-CN` reads `lājī`.
## At the command line
```console
$ pinyinjs check 银行 yínxíng
银行 yínháng 50%
银 yín yín correct
行 háng xíng wrong
$ pinyinjs check 银行 "yín háng" --require-spacing
银行 yínháng 50%
银 yín yín correct
行 háng háng correct split
```
The CLI prints the text, expected reading and score, followed by each syllable
with its source characters, expected reading, typed text and verdict. Spacing
mistakes appear beside the affected syllables.
The first argument is Chinese text. Remaining arguments are joined as the typed
pinyin, so `pinyinjs check 北京市 běijīng shì` works without quoting the pinyin.
For standard input, provide one tab-separated pair per line:
```console
$ printf '银行\tyínxíng\n北京\tbei3jing3\n' | pinyinjs check
```
Use `--require-tones` and `--require-spacing` to include them in the score.
Conversion flags also apply. For example, `--locale zh-TW` checks 國語 readings.
`--json` includes every result field, including `at` and `source`.
## Conversion cost
Each check performs three conversions. The first supplies the expected reading
and its [confidence report](https://pinyinjs.dev/confidence/). The second reverses both sandhi
settings to obtain the alternative tone forms. These two conversions cover all
four sandhi combinations because 一/不 sandhi and third-tone sandhi affect
different syllables. The third conversion supplies the alternative word
spacing.