@kensio/pinyinjs 1.6.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +211 -5
- package/data/core.entries +397 -396
- package/data/core.freq +0 -0
- package/data/core.keys +1 -0
- package/data/full.entries +9717 -9717
- package/data/manifest.json +12 -12
- package/data/script.map +7 -7
- package/data/standard.entries +967 -966
- package/data/standard.freq +0 -0
- package/data/standard.keys +1 -0
- package/dist/cli/arguments.d.ts +38 -0
- package/dist/cli/arguments.d.ts.map +1 -1
- package/dist/cli/arguments.js +46 -0
- package/dist/cli/arguments.js.map +1 -1
- package/dist/cli/commands.d.ts.map +1 -1
- package/dist/cli/commands.js +207 -1
- package/dist/cli/commands.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +5 -0
- package/dist/cli/help.js.map +1 -1
- package/dist/decode/convert.d.ts +16 -0
- package/dist/decode/convert.d.ts.map +1 -1
- package/dist/decode/convert.js +3 -28
- package/dist/decode/convert.js.map +1 -1
- package/dist/decode/runs.d.ts +11 -0
- package/dist/decode/runs.d.ts.map +1 -1
- package/dist/decode/runs.js +19 -0
- package/dist/decode/runs.js.map +1 -1
- package/dist/decode/segment.d.ts +66 -0
- package/dist/decode/segment.d.ts.map +1 -0
- package/dist/decode/segment.js +79 -0
- package/dist/decode/segment.js.map +1 -0
- package/dist/dictionary/artifact.d.ts +10 -0
- package/dist/dictionary/artifact.d.ts.map +1 -1
- package/dist/dictionary/artifact.js +17 -0
- package/dist/dictionary/artifact.js.map +1 -1
- package/dist/dictionary/corpus-mass.d.ts +68 -0
- package/dist/dictionary/corpus-mass.d.ts.map +1 -0
- package/dist/dictionary/corpus-mass.js +123 -0
- package/dist/dictionary/corpus-mass.js.map +1 -0
- package/dist/dictionary/dictionary.d.ts +56 -0
- package/dist/dictionary/dictionary.d.ts.map +1 -1
- package/dist/dictionary/dictionary.js +116 -1
- package/dist/dictionary/dictionary.js.map +1 -1
- package/dist/dictionary/erhua.d.ts +4 -0
- package/dist/dictionary/erhua.d.ts.map +1 -1
- package/dist/dictionary/erhua.js +6 -0
- package/dist/dictionary/erhua.js.map +1 -1
- package/dist/dictionary/merge.d.ts.map +1 -1
- package/dist/dictionary/merge.js +16 -8
- package/dist/dictionary/merge.js.map +1 -1
- package/dist/dictionary/tiers.d.ts +2 -1
- package/dist/dictionary/tiers.d.ts.map +1 -1
- package/dist/dictionary/tiers.js +77 -11
- package/dist/dictionary/tiers.js.map +1 -1
- package/dist/grade/check.d.ts +156 -0
- package/dist/grade/check.d.ts.map +1 -0
- package/dist/grade/check.js +356 -0
- package/dist/grade/check.js.map +1 -0
- package/dist/index.d.ts +11 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/search/candidates.d.ts +84 -0
- package/dist/search/candidates.d.ts.map +1 -0
- package/dist/search/candidates.js +290 -0
- package/dist/search/candidates.js.map +1 -0
- package/dist/search/match.d.ts +72 -0
- package/dist/search/match.d.ts.map +1 -0
- package/dist/search/match.js +419 -0
- package/dist/search/match.js.map +1 -0
- package/dist/search/query.d.ts +49 -0
- package/dist/search/query.d.ts.map +1 -0
- package/dist/search/query.js +115 -0
- package/dist/search/query.js.map +1 -0
- package/dist/search/reverse-index.d.ts +141 -0
- package/dist/search/reverse-index.d.ts.map +1 -0
- package/dist/search/reverse-index.js +356 -0
- package/dist/search/reverse-index.js.map +1 -0
- package/dist/sources/unihan.d.ts +9 -0
- package/dist/sources/unihan.d.ts.map +1 -1
- package/dist/sources/unihan.js +12 -1
- package/dist/sources/unihan.js.map +1 -1
- package/package.json +2 -3
- package/CHANGELOG.md +0 -634
package/dist/dictionary/tiers.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isSingleCharacter, toCharacters } from "../script/characters.js";
|
|
2
|
+
import { traditionalForms } from "./artifact.js";
|
|
2
3
|
/**
|
|
3
4
|
* The dictionary tiers, smallest first and each contained in the next.
|
|
4
5
|
*
|
|
@@ -65,6 +66,71 @@ function charactersInUse(entries) {
|
|
|
65
66
|
}
|
|
66
67
|
return inUse;
|
|
67
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* The characters a tier holds: the ones in use, and the ones it has to answer.
|
|
71
|
+
*
|
|
72
|
+
* "In use" alone is not enough, because a tier is keyed by more than the
|
|
73
|
+
* headwords it holds — an entry claims every 繁體 spelling attested for it, and
|
|
74
|
+
* some of those spellings are rare characters with entries of their own. A tier
|
|
75
|
+
* holding 卒 is keyed for 䘚 whether or not it holds 䘚, so without the
|
|
76
|
+
* character's own entry it answers that key from the 卒 that reached it
|
|
77
|
+
* sideways and reads `zú`, where `full` lets 䘚's own entry win the key and
|
|
78
|
+
* reads `zhú`. Neither reading is wrong — 䘚 written for 卒 really is `zú` —
|
|
79
|
+
* but which one you get must not depend on how much of the dictionary has
|
|
80
|
+
* loaded, or the documented upgrade path contradicts itself mid-paragraph.
|
|
81
|
+
*
|
|
82
|
+
* Admitting the entry is all it takes: the artifact's `claimKeys` puts
|
|
83
|
+
* headwords ahead of 繁體 aliases in every tier, so once 䘚 is present it wins
|
|
84
|
+
* its own key exactly as it does in `full`.
|
|
85
|
+
*
|
|
86
|
+
* It repeats to a fixpoint because an admitted character claims spellings of
|
|
87
|
+
* its own, and one of those can be another character with an entry behind it.
|
|
88
|
+
* On the current sources it settles at once — 242 characters admitted, then a
|
|
89
|
+
* confirming pass that admits none — but nothing in the merge rules a chain
|
|
90
|
+
* out, and the pass is a set lookup per entry against a build that reads four
|
|
91
|
+
* source files.
|
|
92
|
+
*/
|
|
93
|
+
function charactersHeld(entries, words) {
|
|
94
|
+
const headed = new Set();
|
|
95
|
+
for (const entry of entries) {
|
|
96
|
+
if (isSingleCharacter(entry.hans)) {
|
|
97
|
+
headed.add(entry.hans);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const inUse = charactersInUse(entries);
|
|
101
|
+
const held = new Set();
|
|
102
|
+
for (const character of headed) {
|
|
103
|
+
if (inUse.has(character)) {
|
|
104
|
+
held.add(character);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Admit the characters this entry claims the key of, reporting any news.
|
|
109
|
+
*/
|
|
110
|
+
const admitClaims = (entry) => {
|
|
111
|
+
let admitted = false;
|
|
112
|
+
for (const form of traditionalForms(entry)) {
|
|
113
|
+
if (headed.has(form) && !held.has(form)) {
|
|
114
|
+
held.add(form);
|
|
115
|
+
admitted = true;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return admitted;
|
|
119
|
+
};
|
|
120
|
+
for (const entry of words) {
|
|
121
|
+
admitClaims(entry);
|
|
122
|
+
}
|
|
123
|
+
let growing = true;
|
|
124
|
+
while (growing) {
|
|
125
|
+
growing = false;
|
|
126
|
+
for (const entry of entries) {
|
|
127
|
+
if (isSingleCharacter(entry.hans) && held.has(entry.hans)) {
|
|
128
|
+
growing = admitClaims(entry) || growing;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return held;
|
|
133
|
+
}
|
|
68
134
|
/**
|
|
69
135
|
* The entries a tier contains.
|
|
70
136
|
*
|
|
@@ -72,7 +138,8 @@ function charactersInUse(entries) {
|
|
|
72
138
|
* makes a dictionary usable at all: they cover roughly half of running text on
|
|
73
139
|
* their own, and they are the fallback for every position no word matches.
|
|
74
140
|
* Tiering cuts the phrase tail and the unused characters, never a character
|
|
75
|
-
* that some word is written with
|
|
141
|
+
* that some word is written with — and never one the tier is keyed for anyway,
|
|
142
|
+
* which is what {@link charactersHeld} adds on top.
|
|
76
143
|
*
|
|
77
144
|
* `full` holds everything, including the characters no source uses in a word.
|
|
78
145
|
* They cost little there and are the difference between reading a rare
|
|
@@ -82,15 +149,14 @@ export function selectTier(entries, tier) {
|
|
|
82
149
|
if (tier === "full") {
|
|
83
150
|
return entries;
|
|
84
151
|
}
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
return [...characters, ...words];
|
|
152
|
+
const words = tier === "core"
|
|
153
|
+
? []
|
|
154
|
+
: entries
|
|
155
|
+
.filter((entry) => !isSingleCharacter(entry.hans))
|
|
156
|
+
.toSorted(byFrequency)
|
|
157
|
+
.slice(0, STANDARD_TIER_WORDS);
|
|
158
|
+
const held = charactersHeld(entries, words);
|
|
159
|
+
const characters = entries.filter((entry) => isSingleCharacter(entry.hans) && held.has(entry.hans));
|
|
160
|
+
return tier === "core" ? characters : [...characters, ...words];
|
|
95
161
|
}
|
|
96
162
|
//# sourceMappingURL=tiers.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tiers.js","sourceRoot":"","sources":["../../src/dictionary/tiers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"tiers.js","sourceRoot":"","sources":["../../src/dictionary/tiers.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { type ConvertOptions } from "../decode/convert.js";
|
|
2
|
+
import type { Dictionary } from "../dictionary/dictionary.js";
|
|
3
|
+
import { type Syllable } from "../syllable/syllable.js";
|
|
4
|
+
/**
|
|
5
|
+
* What one syllable of a typed transcription turned out to be.
|
|
6
|
+
*
|
|
7
|
+
* `toneless` is deliberately separate from `tone`: {@link Syllable.tone} says
|
|
8
|
+
* whether a tone was written at all, so "you left the tones off" is a different
|
|
9
|
+
* thing to report than "you wrote the wrong one", and only the second is a
|
|
10
|
+
* mistake about the language. {@link CheckOptions.tones} decides which of them
|
|
11
|
+
* counts against the score.
|
|
12
|
+
*/
|
|
13
|
+
export type PinyinVerdict = "correct" | "toneless" | "tone" | "wrong" | "missing" | "extra";
|
|
14
|
+
/**
|
|
15
|
+
* Where a typed syllable falls against the word boundaries the reading has.
|
|
16
|
+
*
|
|
17
|
+
* `split` is a word written as two — `yín háng` for 银行 — and `joined` is two
|
|
18
|
+
* words run into one. Both are reported whatever {@link CheckOptions.spacing}
|
|
19
|
+
* says, so an application can show them without having to grade them.
|
|
20
|
+
*/
|
|
21
|
+
export type SpacingVerdict = "correct" | "split" | "joined";
|
|
22
|
+
/**
|
|
23
|
+
* Whether one dimension of the writing has to be right for a perfect score.
|
|
24
|
+
*
|
|
25
|
+
* `optional` still reports the verdict and counts the syllable correct anyway;
|
|
26
|
+
* `required` counts it wrong. Both dimensions this applies to — tones and word
|
|
27
|
+
* spacing — are things an exercise may or may not be teaching yet.
|
|
28
|
+
*/
|
|
29
|
+
export type CheckRequirement = "optional" | "required";
|
|
30
|
+
/**
|
|
31
|
+
* How a transcription should be checked.
|
|
32
|
+
*
|
|
33
|
+
* Every {@link ConvertOptions} field is accepted and passed to the conversion
|
|
34
|
+
* the answer is read from, which is what `readings` is worth having here for:
|
|
35
|
+
* an exercise knows which sense of 长 its own sentence uses, where the decoder
|
|
36
|
+
* can only weigh the evidence.
|
|
37
|
+
*/
|
|
38
|
+
export interface CheckOptions extends ConvertOptions {
|
|
39
|
+
/** Whether a tone has to be written. Defaults to `optional`. */
|
|
40
|
+
readonly tones?: CheckRequirement;
|
|
41
|
+
/**
|
|
42
|
+
* Whether the word spacing has to be right. Defaults to `optional`.
|
|
43
|
+
*
|
|
44
|
+
* Off by default because the 分词连写 this grades against is not a complete
|
|
45
|
+
* 正词法 implementation — see [orthography](../../docs/orthography/) for
|
|
46
|
+
* where it stops — so a learner can write a word the standard writes
|
|
47
|
+
* differently from the way this does. On for an exercise teaching the
|
|
48
|
+
* spacing, where getting it right is the point.
|
|
49
|
+
*/
|
|
50
|
+
readonly spacing?: CheckRequirement;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* One syllable of the check: what was expected there, and what was typed.
|
|
54
|
+
*/
|
|
55
|
+
export interface CheckedSyllable {
|
|
56
|
+
readonly verdict: PinyinVerdict;
|
|
57
|
+
/**
|
|
58
|
+
* Where this syllable falls against the word boundaries, where both a reading
|
|
59
|
+
* and a typed syllable met here.
|
|
60
|
+
*
|
|
61
|
+
* A separate axis from {@link CheckedSyllable.verdict}, because it is a
|
|
62
|
+
* separate mistake: `yín háng` reads 银行 perfectly and writes it as two
|
|
63
|
+
* words. Undefined for a syllable that was only expected or only typed.
|
|
64
|
+
*/
|
|
65
|
+
readonly spacing: SpacingVerdict | undefined;
|
|
66
|
+
/** Whether this counts as right in {@link PinyinCheck.score}. */
|
|
67
|
+
readonly isCorrect: boolean;
|
|
68
|
+
/** The reading expected here, or undefined where nothing was expected. */
|
|
69
|
+
readonly expected: Syllable | undefined;
|
|
70
|
+
/**
|
|
71
|
+
* What was typed here, parsed, or undefined where nothing was typed.
|
|
72
|
+
*
|
|
73
|
+
* Also undefined for something typed that is not a syllable at all, which
|
|
74
|
+
* {@link CheckedSyllable.text} still reports as written.
|
|
75
|
+
*/
|
|
76
|
+
readonly actual: Syllable | undefined;
|
|
77
|
+
/** What was typed here, exactly as written, or empty where nothing was. */
|
|
78
|
+
readonly text: string;
|
|
79
|
+
/**
|
|
80
|
+
* The characters the expected syllable reads.
|
|
81
|
+
*
|
|
82
|
+
* Undefined where a syllable reads on into the characters named before it,
|
|
83
|
+
* exactly as {@link ConvertedPiece.source} is, and where nothing was
|
|
84
|
+
* expected at all.
|
|
85
|
+
*/
|
|
86
|
+
readonly source: string | undefined;
|
|
87
|
+
/**
|
|
88
|
+
* Where those characters start, in code points from the start of the text.
|
|
89
|
+
*
|
|
90
|
+
* What highlighting the mistake in the text itself needs, and counted the way
|
|
91
|
+
* {@link import("../decode/segment.js").Segment.at} counts.
|
|
92
|
+
*/
|
|
93
|
+
readonly at: number | undefined;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* A typed transcription marked against the text it was written for.
|
|
97
|
+
*/
|
|
98
|
+
export interface PinyinCheck {
|
|
99
|
+
/** One entry per syllable expected or typed, in order. */
|
|
100
|
+
readonly syllables: readonly CheckedSyllable[];
|
|
101
|
+
/** Whether every syllable counted as correct. */
|
|
102
|
+
readonly isCorrect: boolean;
|
|
103
|
+
/**
|
|
104
|
+
* The share of the syllables that counted as correct, from 0 to 1.
|
|
105
|
+
*
|
|
106
|
+
* Over the entries reported rather than over the expected reading, so that
|
|
107
|
+
* inventing a syllable costs as much as dropping one: an answer with an extra
|
|
108
|
+
* syllable in it has more entries than the reading has syllables.
|
|
109
|
+
*/
|
|
110
|
+
readonly score: number;
|
|
111
|
+
/** The reading that was expected, as the conversion writes it. */
|
|
112
|
+
readonly reading: readonly Syllable[];
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Mark a typed pinyin transcription against the text it was written for.
|
|
116
|
+
*
|
|
117
|
+
* ```ts
|
|
118
|
+
* check(dictionary, "北京", "bei3jing3").syllables.map((one) => one.verdict);
|
|
119
|
+
* // ["correct", "tone"]
|
|
120
|
+
* ```
|
|
121
|
+
*
|
|
122
|
+
* **The point of doing this with a dictionary is being fair in the ways a
|
|
123
|
+
* string comparison cannot be**, and every one of these is a learner being
|
|
124
|
+
* marked wrong for being right:
|
|
125
|
+
*
|
|
126
|
+
* - **Either notation, mixed freely.** `běi` and `bei3` are the same syllable
|
|
127
|
+
* and both parse, so `bei3jīng` is not a spelling mistake.
|
|
128
|
+
* - **A reading the decoder itself was unsure of.** Where another reading of
|
|
129
|
+
* the same character was there for the taking, it is accepted — see
|
|
130
|
+
* {@link rejectedReadings}. 银行 is not one of those: the word settles both
|
|
131
|
+
* syllables, so `yínxíng` really is wrong.
|
|
132
|
+
* - **Sandhi either way.** 你好 is written `nǐ hǎo` and said `ní hǎo`, and 不是
|
|
133
|
+
* is `bú shì` written and `bù shì` underneath. Both pass.
|
|
134
|
+
* - **Tones written or not.** A syllable typed with no tone is `toneless`
|
|
135
|
+
* rather than `tone`, and {@link CheckOptions.tones} says whether that counts
|
|
136
|
+
* against the score. A neutral tone typed without a mark is simply correct,
|
|
137
|
+
* because that is how pinyin writes it.
|
|
138
|
+
* - **Apostrophes.** Not a word boundary and not a sound: `nǐhǎo`, `nǐ hǎo` and
|
|
139
|
+
* `nǐ'hǎo` all read the same, and check the same.
|
|
140
|
+
*
|
|
141
|
+
* Word spacing is graded on its own axis, reported as
|
|
142
|
+
* {@link CheckedSyllable.spacing} and counted only where
|
|
143
|
+
* {@link CheckOptions.spacing} asks for it — and tolerant in the same spirit,
|
|
144
|
+
* since 分词连写 and the words the dictionary knows are two conventions this
|
|
145
|
+
* package writes and a learner may have been taught either.
|
|
146
|
+
*
|
|
147
|
+
* What comes back is one {@link CheckedSyllable} per syllable expected or
|
|
148
|
+
* typed, in order, each naming the characters it reads and where they are, so
|
|
149
|
+
* that a mistake can be shown against the text rather than against the answer.
|
|
150
|
+
*
|
|
151
|
+
* This is {@link import("../accuracy/score.js").scoreCase} — the scorer that
|
|
152
|
+
* marks the decoder against the gold corpus — pointed at a person instead, and
|
|
153
|
+
* it costs three conversions of the text.
|
|
154
|
+
*/
|
|
155
|
+
export declare function check(dictionary: Dictionary, text: string, typed: string, options?: CheckOptions): PinyinCheck;
|
|
156
|
+
//# sourceMappingURL=check.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"check.d.ts","sourceRoot":"","sources":["../../src/grade/check.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,cAAc,EAGpB,MAAM,sBAAsB,CAAC;AAG9B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAE9D,OAAO,EAEL,KAAK,QAAQ,EAEd,MAAM,yBAAyB,CAAC;AAGjC;;;;;;;;GAQG;AACH,MAAM,MAAM,aAAa,GACrB,SAAS,GACT,UAAU,GACV,MAAM,GACN,OAAO,GACP,SAAS,GACT,OAAO,CAAC;AAEZ;;;;;;GAMG;AACH,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG,OAAO,GAAG,QAAQ,CAAC;AAE5D;;;;;;GAMG;AACH,MAAM,MAAM,gBAAgB,GAAG,UAAU,GAAG,UAAU,CAAC;AAEvD;;;;;;;GAOG;AACH,MAAM,WAAW,YAAa,SAAQ,cAAc;IAClD,gEAAgE;IAChE,QAAQ,CAAC,KAAK,CAAC,EAAE,gBAAgB,CAAC;IAClC;;;;;;;;OAQG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,gBAAgB,CAAC;CACrC;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC;IAChC;;;;;;;OAOG;IACH,QAAQ,CAAC,OAAO,EAAE,cAAc,GAAG,SAAS,CAAC;IAC7C,iEAAiE;IACjE,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,0EAA0E;IAC1E,QAAQ,CAAC,QAAQ,EAAE,QAAQ,GAAG,SAAS,CAAC;IACxC;;;;;OAKG;IACH,QAAQ,CAAC,MAAM,EAAE,QAAQ,GAAG,SAAS,CAAC;IACtC,2EAA2E;IAC3E,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;;;;;;OAMG;IACH,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,SAAS,CAAC;CACjC;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,0DAA0D;IAC1D,QAAQ,CAAC,SAAS,EAAE,SAAS,eAAe,EAAE,CAAC;IAC/C,iDAAiD;IACjD,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B;;;;;;OAMG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,kEAAkE;IAClE,QAAQ,CAAC,OAAO,EAAE,SAAS,QAAQ,EAAE,CAAC;CACvC;AAqaD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,wBAAgB,KAAK,CACnB,UAAU,EAAE,UAAU,EACtB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,YAAiB,GACzB,WAAW,CAuBb"}
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import { alignSequences } from "../accuracy/align.js";
|
|
2
|
+
import { tokenisePinyin } from "../accuracy/tokenise.js";
|
|
3
|
+
import { convertPieces, convertPiecesUnscored, } from "../decode/convert.js";
|
|
4
|
+
import { READING_CHARGE } from "../decode/lattice.js";
|
|
5
|
+
import { toCharacters } from "../script/characters.js";
|
|
6
|
+
import { readSyllable, writeSyllableSpelling, } from "../syllable/syllable.js";
|
|
7
|
+
import { NEUTRAL_TONE } from "../tone/tone.js";
|
|
8
|
+
/**
|
|
9
|
+
* The other corner of the sandhi square.
|
|
10
|
+
*
|
|
11
|
+
* A learner may write the underlying tones or the tones actually said, and both
|
|
12
|
+
* are right — 你好 is written `nǐ hǎo` and said `ní hǎo`. Rather than convert
|
|
13
|
+
* four times for the four combinations of {@link SandhiOptions}, this converts
|
|
14
|
+
* twice and inverts both switches at once, which reaches every tone: 一 and 不
|
|
15
|
+
* sandhi applies to syllables in the first and fourth tones, third-tone sandhi
|
|
16
|
+
* only to syllables in the third, so the two passes never touch the same
|
|
17
|
+
* syllable and each one's two forms both appear across the two corners.
|
|
18
|
+
*/
|
|
19
|
+
function oppositeSandhi(sandhi) {
|
|
20
|
+
return {
|
|
21
|
+
yiBu: !(sandhi?.yiBu ?? true),
|
|
22
|
+
thirdTone: !(sandhi?.thirdTone ?? false),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Where each piece's source characters start, in code points.
|
|
27
|
+
*
|
|
28
|
+
* Read off the sources rather than tracked through the conversion, which works
|
|
29
|
+
* because every character of the text is named by exactly one piece — a piece
|
|
30
|
+
* naming none is either pinyin orthography the source has no trace of, such as
|
|
31
|
+
* the space between two words, or a syllable reading on into the characters the
|
|
32
|
+
* piece before it named.
|
|
33
|
+
*/
|
|
34
|
+
function sourcePositions(pieces) {
|
|
35
|
+
const positions = [];
|
|
36
|
+
let at = 0;
|
|
37
|
+
for (const piece of pieces) {
|
|
38
|
+
positions.push(at);
|
|
39
|
+
at += toCharacters(piece.source ?? "").length;
|
|
40
|
+
}
|
|
41
|
+
return positions;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* What a piece written between two syllables makes of the join.
|
|
45
|
+
*
|
|
46
|
+
* Only a hyphen written where the syllables would otherwise have run together
|
|
47
|
+
* is 分词连写's internal boundary; anything else — a space, punctuation, a
|
|
48
|
+
* stretch that was never Han — separates two words.
|
|
49
|
+
*/
|
|
50
|
+
function junctionOver(text, held) {
|
|
51
|
+
return text === "-" && held === "join" ? "hyphen" : "break";
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The syllables a conversion writes, with their positions and what joins them.
|
|
55
|
+
*
|
|
56
|
+
* A conversion writes its syllables as adjacent pieces within a word — the
|
|
57
|
+
* 隔音符号 is part of a syllable's own spelling rather than a piece of its own —
|
|
58
|
+
* so anything standing between two of them is a boundary, and the hyphen of
|
|
59
|
+
* 分词连写 is the one that is not a word boundary.
|
|
60
|
+
*/
|
|
61
|
+
function readingSyllables(pieces) {
|
|
62
|
+
const positions = sourcePositions(pieces);
|
|
63
|
+
const syllables = [];
|
|
64
|
+
// The first syllable of a text begins a word, whatever comes before it.
|
|
65
|
+
let junction = "break";
|
|
66
|
+
for (const [at, piece] of pieces.entries()) {
|
|
67
|
+
if (piece.syllable === undefined) {
|
|
68
|
+
junction = junctionOver(piece.text, junction);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
syllables.push({
|
|
72
|
+
piece: { ...piece, syllable: piece.syllable },
|
|
73
|
+
/* c8 ignore next -- one position is recorded for each piece there is */
|
|
74
|
+
at: positions[at] ?? 0,
|
|
75
|
+
junction,
|
|
76
|
+
});
|
|
77
|
+
junction = "join";
|
|
78
|
+
}
|
|
79
|
+
return syllables;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* The readings the decode turned down without much reason to.
|
|
83
|
+
*
|
|
84
|
+
* The decode charges {@link READING_CHARGE} for a word boundary, so a rejected
|
|
85
|
+
* reading costing less than that was available without breaking any dictionary
|
|
86
|
+
* word apart — see {@link import("../decode/confidence.js").isUncertain}, which
|
|
87
|
+
* is the same threshold read as a question rather than as a filter. The library
|
|
88
|
+
* knows it was guessing there, so a learner who guessed the other way is not
|
|
89
|
+
* wrong.
|
|
90
|
+
*
|
|
91
|
+
* Only a rejected reading of exactly the one character this syllable reads is
|
|
92
|
+
* taken. An alternative can cover a different stretch — 玩儿 read as `wánr`
|
|
93
|
+
* competes with 玩 `wán` and 儿 `ér` — and a claim about other characters says
|
|
94
|
+
* nothing about what belongs in this position.
|
|
95
|
+
*/
|
|
96
|
+
function rejectedReadings(piece) {
|
|
97
|
+
const { confidence, source } = piece;
|
|
98
|
+
if (source === undefined || toCharacters(source).length !== 1) {
|
|
99
|
+
return [];
|
|
100
|
+
}
|
|
101
|
+
return (confidence?.alternatives ?? []).flatMap((alternative) => {
|
|
102
|
+
const [syllable] = alternative.reading;
|
|
103
|
+
return alternative.cost < READING_CHARGE &&
|
|
104
|
+
alternative.to - alternative.from === 1 &&
|
|
105
|
+
alternative.reading.length === 1 &&
|
|
106
|
+
syllable !== undefined
|
|
107
|
+
? [syllable]
|
|
108
|
+
: [];
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Gather readings into the spellings and tones they allow.
|
|
113
|
+
*
|
|
114
|
+
* The tone is allowed to be undefined only because {@link Syllable.tone} is: a
|
|
115
|
+
* reading that came out of the dictionary always carries one.
|
|
116
|
+
*/
|
|
117
|
+
function acceptedReadings(readings) {
|
|
118
|
+
const accepted = new Map();
|
|
119
|
+
for (const syllable of readings) {
|
|
120
|
+
const base = writeSyllableSpelling(syllable);
|
|
121
|
+
const tones = accepted.get(base) ?? new Set();
|
|
122
|
+
tones.add(syllable.tone);
|
|
123
|
+
accepted.set(base, tones);
|
|
124
|
+
}
|
|
125
|
+
return accepted;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Read the text, and work out what a typed syllable is allowed to be.
|
|
129
|
+
*
|
|
130
|
+
* Three conversions of the same text, and each one answers a different question.
|
|
131
|
+
* The first is the answer itself, with the confidence report beside it. The
|
|
132
|
+
* second is the other corner of the sandhi square, which is where the second
|
|
133
|
+
* form of every tone comes from. The third is the same text written under the
|
|
134
|
+
* other spacing convention, which is where a boundary's tolerance comes from:
|
|
135
|
+
* 分词连写 is what puts 了 on its verb and separates 市 from 南京, and a learner
|
|
136
|
+
* who wrote the words the dictionary knows instead — `tā kàn le`, `Nánjīngshì`
|
|
137
|
+
* — has written one of the two conventions this package implements rather than
|
|
138
|
+
* made a mistake.
|
|
139
|
+
*
|
|
140
|
+
* All three decode identically. Sandhi rewrites tones and 分词连写 moves
|
|
141
|
+
* boundaries; neither touches the syllables underneath, so all three line up one
|
|
142
|
+
* for one.
|
|
143
|
+
*/
|
|
144
|
+
function expectedReading(dictionary, text, options) {
|
|
145
|
+
const written = readingSyllables(convertPieces(dictionary, text, options));
|
|
146
|
+
const said = readingSyllables(convertPiecesUnscored(dictionary, text, {
|
|
147
|
+
...options,
|
|
148
|
+
sandhi: oppositeSandhi(options.sandhi),
|
|
149
|
+
}));
|
|
150
|
+
const spaced = readingSyllables(convertPiecesUnscored(dictionary, text, {
|
|
151
|
+
...options,
|
|
152
|
+
grouping: !(options.grouping ?? true),
|
|
153
|
+
}));
|
|
154
|
+
return written.map(({ piece, at, junction }, index) => {
|
|
155
|
+
// Sliced rather than indexed, so that a conversion somehow reporting fewer
|
|
156
|
+
// syllables than the others contributes nothing rather than needing a
|
|
157
|
+
// fallback: the caller's own conversion decides alone.
|
|
158
|
+
const alternate = said
|
|
159
|
+
.slice(index, index + 1)
|
|
160
|
+
.map((one) => one.piece.syllable);
|
|
161
|
+
const other = spaced.slice(index, index + 1).map((one) => one.junction);
|
|
162
|
+
return {
|
|
163
|
+
syllable: piece.syllable,
|
|
164
|
+
accepted: acceptedReadings([
|
|
165
|
+
piece.syllable,
|
|
166
|
+
...alternate,
|
|
167
|
+
...rejectedReadings(piece),
|
|
168
|
+
]),
|
|
169
|
+
// A hyphen is neither a break nor a join, and so allows both: it is a
|
|
170
|
+
// boundary the standard writes inside a word, and a learner rendering it
|
|
171
|
+
// as a space has not invented one.
|
|
172
|
+
acceptsBreak: junction !== "join" || other.some((one) => one !== "join"),
|
|
173
|
+
acceptsJoin: junction !== "break" || other.some((one) => one !== "break"),
|
|
174
|
+
source: piece.source,
|
|
175
|
+
at,
|
|
176
|
+
};
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Read what was typed, one syllable at a time.
|
|
181
|
+
*
|
|
182
|
+
* {@link tokenisePinyin} is what the decoder is scored with, and it is tolerant
|
|
183
|
+
* in the two ways that matter here: word spacing is discarded, and the
|
|
184
|
+
* apostrophes and hyphens inside a word are read as the syllable boundaries
|
|
185
|
+
* they are and dropped. Both are orthography rather than pronunciation.
|
|
186
|
+
*
|
|
187
|
+
* A stretch that is not pinyin at all comes back whole, and is reported as
|
|
188
|
+
* written rather than thrown away.
|
|
189
|
+
*/
|
|
190
|
+
function typedReading(typed) {
|
|
191
|
+
const { syllables, wordStarts } = tokenisePinyin(typed);
|
|
192
|
+
return syllables.map((text, at) => {
|
|
193
|
+
const syllable = readSyllable(text);
|
|
194
|
+
return {
|
|
195
|
+
text,
|
|
196
|
+
syllable,
|
|
197
|
+
base: syllable === undefined
|
|
198
|
+
? text.toLowerCase().normalize("NFC")
|
|
199
|
+
: writeSyllableSpelling(syllable),
|
|
200
|
+
tone: syllable?.tone,
|
|
201
|
+
startsWord: wordStarts.has(at),
|
|
202
|
+
};
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Match the expected syllables up with the typed ones.
|
|
207
|
+
*
|
|
208
|
+
* Aligned on the toneless spellings, for the reason
|
|
209
|
+
* {@link import("../accuracy/score.js").scoreCase} aligns on them: a syllable
|
|
210
|
+
* dropped or invented halfway through would otherwise knock everything after it
|
|
211
|
+
* out of step, and be reported as a mistake on every syllable rather than on
|
|
212
|
+
* the one.
|
|
213
|
+
*
|
|
214
|
+
* What the alignment leaves over is then paired off within each gap, which is
|
|
215
|
+
* what turns two unrelated stretches into substitutions: 银行 typed `yínxíng`
|
|
216
|
+
* anchors on `yín`, leaving one expected `háng` against one typed `xíng` — one
|
|
217
|
+
* wrong syllable rather than one missing and one invented.
|
|
218
|
+
*/
|
|
219
|
+
function pairSyllables(expected, actual) {
|
|
220
|
+
const pairs = [];
|
|
221
|
+
let held = 0;
|
|
222
|
+
let typed = 0;
|
|
223
|
+
const gap = (untilExpected, untilActual) => {
|
|
224
|
+
while (held < untilExpected || typed < untilActual) {
|
|
225
|
+
const takesExpected = held < untilExpected;
|
|
226
|
+
const takesActual = typed < untilActual;
|
|
227
|
+
pairs.push({
|
|
228
|
+
expected: takesExpected ? held : undefined,
|
|
229
|
+
actual: takesActual ? typed : undefined,
|
|
230
|
+
});
|
|
231
|
+
held += takesExpected ? 1 : 0;
|
|
232
|
+
typed += takesActual ? 1 : 0;
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
for (const anchor of alignSequences(expected, actual)) {
|
|
236
|
+
gap(anchor.expected, anchor.actual);
|
|
237
|
+
pairs.push({ expected: held, actual: typed });
|
|
238
|
+
held++;
|
|
239
|
+
typed++;
|
|
240
|
+
}
|
|
241
|
+
gap(expected.length, actual.length);
|
|
242
|
+
return pairs;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* What a typed syllable is, against what was expected in its place.
|
|
246
|
+
*
|
|
247
|
+
* The neutral tone is the one tone pinyin writes with no mark at all, so a
|
|
248
|
+
* syllable expected in it and typed without one has had its tone written
|
|
249
|
+
* correctly rather than left off.
|
|
250
|
+
*/
|
|
251
|
+
function verdictFor(expected, actual) {
|
|
252
|
+
const tones = expected.accepted.get(actual.base);
|
|
253
|
+
if (tones === undefined) {
|
|
254
|
+
return "wrong";
|
|
255
|
+
}
|
|
256
|
+
if (actual.tone === undefined) {
|
|
257
|
+
return tones.has(NEUTRAL_TONE) ? "correct" : "toneless";
|
|
258
|
+
}
|
|
259
|
+
return tones.has(actual.tone) ? "correct" : "tone";
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Where a typed syllable falls against the word boundaries.
|
|
263
|
+
*/
|
|
264
|
+
function spacingFor(expected, actual) {
|
|
265
|
+
if (actual.startsWord) {
|
|
266
|
+
return expected.acceptsBreak ? "correct" : "split";
|
|
267
|
+
}
|
|
268
|
+
return expected.acceptsJoin ? "correct" : "joined";
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Mark one pairing.
|
|
272
|
+
*
|
|
273
|
+
* The two axes are graded separately and reported separately, and a syllable
|
|
274
|
+
* only counts as right where both of them are: `yín háng` reads 银行 perfectly
|
|
275
|
+
* and writes it as two words, which is one mistake rather than none.
|
|
276
|
+
*/
|
|
277
|
+
function checkPair(expected, actual, required) {
|
|
278
|
+
const verdict = expected === undefined
|
|
279
|
+
? "extra"
|
|
280
|
+
: actual === undefined
|
|
281
|
+
? "missing"
|
|
282
|
+
: verdictFor(expected, actual);
|
|
283
|
+
const spacing = expected === undefined || actual === undefined
|
|
284
|
+
? undefined
|
|
285
|
+
: spacingFor(expected, actual);
|
|
286
|
+
const readRight = verdict === "correct" ||
|
|
287
|
+
(verdict === "toneless" && required.tones === "optional");
|
|
288
|
+
const spacedRight = spacing !== "split" && spacing !== "joined"
|
|
289
|
+
? true
|
|
290
|
+
: required.spacing === "optional";
|
|
291
|
+
return {
|
|
292
|
+
verdict,
|
|
293
|
+
spacing,
|
|
294
|
+
isCorrect: readRight && spacedRight,
|
|
295
|
+
expected: expected?.syllable,
|
|
296
|
+
actual: actual?.syllable,
|
|
297
|
+
text: actual?.text ?? "",
|
|
298
|
+
source: expected?.source,
|
|
299
|
+
at: expected?.at,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Mark a typed pinyin transcription against the text it was written for.
|
|
304
|
+
*
|
|
305
|
+
* ```ts
|
|
306
|
+
* check(dictionary, "北京", "bei3jing3").syllables.map((one) => one.verdict);
|
|
307
|
+
* // ["correct", "tone"]
|
|
308
|
+
* ```
|
|
309
|
+
*
|
|
310
|
+
* **The point of doing this with a dictionary is being fair in the ways a
|
|
311
|
+
* string comparison cannot be**, and every one of these is a learner being
|
|
312
|
+
* marked wrong for being right:
|
|
313
|
+
*
|
|
314
|
+
* - **Either notation, mixed freely.** `běi` and `bei3` are the same syllable
|
|
315
|
+
* and both parse, so `bei3jīng` is not a spelling mistake.
|
|
316
|
+
* - **A reading the decoder itself was unsure of.** Where another reading of
|
|
317
|
+
* the same character was there for the taking, it is accepted — see
|
|
318
|
+
* {@link rejectedReadings}. 银行 is not one of those: the word settles both
|
|
319
|
+
* syllables, so `yínxíng` really is wrong.
|
|
320
|
+
* - **Sandhi either way.** 你好 is written `nǐ hǎo` and said `ní hǎo`, and 不是
|
|
321
|
+
* is `bú shì` written and `bù shì` underneath. Both pass.
|
|
322
|
+
* - **Tones written or not.** A syllable typed with no tone is `toneless`
|
|
323
|
+
* rather than `tone`, and {@link CheckOptions.tones} says whether that counts
|
|
324
|
+
* against the score. A neutral tone typed without a mark is simply correct,
|
|
325
|
+
* because that is how pinyin writes it.
|
|
326
|
+
* - **Apostrophes.** Not a word boundary and not a sound: `nǐhǎo`, `nǐ hǎo` and
|
|
327
|
+
* `nǐ'hǎo` all read the same, and check the same.
|
|
328
|
+
*
|
|
329
|
+
* Word spacing is graded on its own axis, reported as
|
|
330
|
+
* {@link CheckedSyllable.spacing} and counted only where
|
|
331
|
+
* {@link CheckOptions.spacing} asks for it — and tolerant in the same spirit,
|
|
332
|
+
* since 分词连写 and the words the dictionary knows are two conventions this
|
|
333
|
+
* package writes and a learner may have been taught either.
|
|
334
|
+
*
|
|
335
|
+
* What comes back is one {@link CheckedSyllable} per syllable expected or
|
|
336
|
+
* typed, in order, each naming the characters it reads and where they are, so
|
|
337
|
+
* that a mistake can be shown against the text rather than against the answer.
|
|
338
|
+
*
|
|
339
|
+
* This is {@link import("../accuracy/score.js").scoreCase} — the scorer that
|
|
340
|
+
* marks the decoder against the gold corpus — pointed at a person instead, and
|
|
341
|
+
* it costs three conversions of the text.
|
|
342
|
+
*/
|
|
343
|
+
export function check(dictionary, text, typed, options = {}) {
|
|
344
|
+
const { tones = "optional", spacing = "optional", ...converting } = options;
|
|
345
|
+
const expected = expectedReading(dictionary, text, converting);
|
|
346
|
+
const actual = typedReading(typed);
|
|
347
|
+
const syllables = pairSyllables(expected.map((one) => writeSyllableSpelling(one.syllable)), actual.map((one) => one.base)).map((pair) => checkPair(pair.expected === undefined ? undefined : expected[pair.expected], pair.actual === undefined ? undefined : actual[pair.actual], { tones, spacing }));
|
|
348
|
+
const correct = syllables.filter((one) => one.isCorrect).length;
|
|
349
|
+
return {
|
|
350
|
+
syllables,
|
|
351
|
+
isCorrect: correct === syllables.length,
|
|
352
|
+
score: syllables.length === 0 ? 1 : correct / syllables.length,
|
|
353
|
+
reading: expected.map((one) => one.syllable),
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
//# sourceMappingURL=check.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"check.js","sourceRoot":"","sources":["../../src/grade/check.ts"],"names":[],"mappings":"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"}
|