chaffjs 0.29.0 → 0.30.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/dist/derived/session-hours.d.ts +26 -0
- package/dist/derived/session-hours.d.ts.map +1 -0
- package/dist/derived/session-hours.js +29 -0
- package/dist/derived/session-hours.js.map +1 -0
- package/dist/derived/time-lengths.d.ts +37 -0
- package/dist/derived/time-lengths.d.ts.map +1 -0
- package/dist/derived/time-lengths.js +168 -0
- package/dist/derived/time-lengths.js.map +1 -0
- package/dist/derived/unit-counts.d.ts +7 -0
- package/dist/derived/unit-counts.d.ts.map +1 -0
- package/dist/derived/unit-counts.js +13 -0
- package/dist/derived/unit-counts.js.map +1 -0
- package/dist/derived/working-hours.d.ts +36 -0
- package/dist/derived/working-hours.d.ts.map +1 -0
- package/dist/derived/working-hours.js +55 -0
- package/dist/derived/working-hours.js.map +1 -0
- package/dist/detectors/acronym-expansion.d.ts +1 -1
- package/dist/detectors/acronym-expansion.d.ts.map +1 -1
- package/dist/detectors/acronym-expansion.js +46 -2
- package/dist/detectors/acronym-expansion.js.map +1 -1
- package/dist/detectors/capitalised-term.d.ts +44 -0
- package/dist/detectors/capitalised-term.d.ts.map +1 -0
- package/dist/detectors/capitalised-term.js +101 -0
- package/dist/detectors/capitalised-term.js.map +1 -0
- package/dist/detectors/date-outside-period.d.ts.map +1 -1
- package/dist/detectors/date-outside-period.js +4 -1
- package/dist/detectors/date-outside-period.js.map +1 -1
- package/dist/detectors/derived-numbers.d.ts +2 -0
- package/dist/detectors/derived-numbers.d.ts.map +1 -1
- package/dist/detectors/derived-numbers.js +72 -18
- package/dist/detectors/derived-numbers.js.map +1 -1
- package/dist/detectors/name-variant.d.ts.map +1 -1
- package/dist/detectors/name-variant.js +57 -10
- package/dist/detectors/name-variant.js.map +1 -1
- package/dist/detectors/party-order.d.ts +3 -0
- package/dist/detectors/party-order.d.ts.map +1 -0
- package/dist/detectors/party-order.js +46 -0
- package/dist/detectors/party-order.js.map +1 -0
- package/dist/detectors/registry/connection-time-short.d.ts +3 -0
- package/dist/detectors/registry/connection-time-short.d.ts.map +1 -0
- package/dist/detectors/registry/connection-time-short.js +3 -0
- package/dist/detectors/registry/connection-time-short.js.map +1 -0
- package/dist/detectors/registry/duration-product-mismatch.d.ts +3 -0
- package/dist/detectors/registry/duration-product-mismatch.d.ts.map +1 -0
- package/dist/detectors/registry/duration-product-mismatch.js +3 -0
- package/dist/detectors/registry/duration-product-mismatch.js.map +1 -0
- package/dist/detectors/registry/party-order.d.ts +3 -0
- package/dist/detectors/registry/party-order.d.ts.map +1 -0
- package/dist/detectors/registry/party-order.js +3 -0
- package/dist/detectors/registry/party-order.js.map +1 -0
- package/dist/detectors/registry/requirement-lists.d.ts +3 -0
- package/dist/detectors/registry/requirement-lists.d.ts.map +1 -0
- package/dist/detectors/registry/requirement-lists.js +3 -0
- package/dist/detectors/registry/requirement-lists.js.map +1 -0
- package/dist/detectors/registry/return-type.d.ts +3 -0
- package/dist/detectors/registry/return-type.d.ts.map +1 -0
- package/dist/detectors/registry/return-type.js +3 -0
- package/dist/detectors/registry/return-type.js.map +1 -0
- package/dist/detectors/requirement-lists.d.ts +3 -0
- package/dist/detectors/requirement-lists.d.ts.map +1 -0
- package/dist/detectors/requirement-lists.js +46 -0
- package/dist/detectors/requirement-lists.js.map +1 -0
- package/dist/detectors/return-type.d.ts +3 -0
- package/dist/detectors/return-type.d.ts.map +1 -0
- package/dist/detectors/return-type.js +24 -0
- package/dist/detectors/return-type.js.map +1 -0
- package/dist/detectors/structure-tree.d.ts.map +1 -1
- package/dist/detectors/structure-tree.js +9 -1
- package/dist/detectors/structure-tree.js.map +1 -1
- package/dist/detectors/time-order.d.ts +1 -0
- package/dist/detectors/time-order.d.ts.map +1 -1
- package/dist/detectors/time-order.js +30 -4
- package/dist/detectors/time-order.js.map +1 -1
- package/dist/detectors/undefined-term.d.ts.map +1 -1
- package/dist/detectors/undefined-term.js +19 -3
- package/dist/detectors/undefined-term.js.map +1 -1
- package/dist/document.d.ts.map +1 -1
- package/dist/document.js +4 -1
- package/dist/document.js.map +1 -1
- package/dist/facts/table-facts.d.ts +2 -0
- package/dist/facts/table-facts.d.ts.map +1 -1
- package/dist/facts/table-facts.js +2 -0
- package/dist/facts/table-facts.js.map +1 -1
- package/dist/name-variants.d.ts +6 -1
- package/dist/name-variants.d.ts.map +1 -1
- package/dist/name-variants.js +68 -0
- package/dist/name-variants.js.map +1 -1
- package/dist/name-word-order.d.ts +49 -0
- package/dist/name-word-order.d.ts.map +1 -0
- package/dist/name-word-order.js +114 -0
- package/dist/name-word-order.js.map +1 -0
- package/dist/plugin.d.ts +7 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/return-type.d.ts +33 -0
- package/dist/return-type.d.ts.map +1 -0
- package/dist/return-type.js +302 -0
- package/dist/return-type.js.map +1 -0
- package/dist/structure/connection-times.d.ts +56 -0
- package/dist/structure/connection-times.d.ts.map +1 -0
- package/dist/structure/connection-times.js +165 -0
- package/dist/structure/connection-times.js.map +1 -0
- package/dist/structure/date-order-tie.d.ts +13 -0
- package/dist/structure/date-order-tie.d.ts.map +1 -0
- package/dist/structure/date-order-tie.js +61 -0
- package/dist/structure/date-order-tie.js.map +1 -0
- package/dist/structure/date-order.d.ts +2 -2
- package/dist/structure/date-order.d.ts.map +1 -1
- package/dist/structure/date-order.js +26 -9
- package/dist/structure/date-order.js.map +1 -1
- package/dist/structure/date-outside-period.d.ts +6 -0
- package/dist/structure/date-outside-period.d.ts.map +1 -1
- package/dist/structure/date-outside-period.js +49 -3
- package/dist/structure/date-outside-period.js.map +1 -1
- package/dist/structure/date-range.d.ts +5 -2
- package/dist/structure/date-range.d.ts.map +1 -1
- package/dist/structure/date-range.js +37 -9
- package/dist/structure/date-range.js.map +1 -1
- package/dist/structure/leg-times.d.ts +2 -0
- package/dist/structure/leg-times.d.ts.map +1 -1
- package/dist/structure/leg-times.js +13 -0
- package/dist/structure/leg-times.js.map +1 -1
- package/dist/structure/party-order.d.ts +33 -0
- package/dist/structure/party-order.d.ts.map +1 -0
- package/dist/structure/party-order.js +79 -0
- package/dist/structure/party-order.js.map +1 -0
- package/dist/structure/requirement-lists.d.ts +37 -0
- package/dist/structure/requirement-lists.d.ts.map +1 -0
- package/dist/structure/requirement-lists.js +89 -0
- package/dist/structure/requirement-lists.js.map +1 -0
- package/dist/structure/time-order.d.ts +21 -0
- package/dist/structure/time-order.d.ts.map +1 -1
- package/dist/structure/time-order.js +5 -6
- package/dist/structure/time-order.js.map +1 -1
- package/dist/structure/version-order.d.ts +2 -0
- package/dist/structure/version-order.d.ts.map +1 -1
- package/dist/structure/version-order.js +10 -0
- package/dist/structure/version-order.js.map +1 -1
- package/dist/surname-near.d.ts +6 -0
- package/dist/surname-near.d.ts.map +1 -0
- package/dist/surname-near.js +31 -0
- package/dist/surname-near.js.map +1 -0
- package/dist/table-cells.d.ts +13 -0
- package/dist/table-cells.d.ts.map +1 -0
- package/dist/table-cells.js +36 -0
- package/dist/table-cells.js.map +1 -0
- package/dist/table-names.d.ts +42 -0
- package/dist/table-names.d.ts.map +1 -0
- package/dist/table-names.js +94 -0
- package/dist/table-names.js.map +1 -0
- package/package.json +3 -3
- package/rules/connection-time-short.yaml +52 -0
- package/rules/date-outside-period.yaml +4 -4
- package/rules/duration-mismatch.yaml +7 -4
- package/rules/duration-product-mismatch.yaml +73 -0
- package/rules/name-variant.yaml +8 -2
- package/rules/party-order.yaml +84 -0
- package/rules/required-listed-as-preferred.yaml +102 -0
- package/rules/return-type-mismatch.yaml +79 -0
- package/rules/undefined-term.yaml +21 -7
- package/src/derived/session-hours.ts +62 -0
- package/src/derived/time-lengths.ts +226 -0
- package/src/derived/unit-counts.ts +18 -0
- package/src/derived/working-hours.ts +97 -0
- package/src/detectors/acronym-expansion.ts +57 -2
- package/src/detectors/capitalised-term.ts +147 -0
- package/src/detectors/date-outside-period.ts +4 -1
- package/src/detectors/derived-numbers.ts +94 -21
- package/src/detectors/name-variant.ts +71 -13
- package/src/detectors/party-order.ts +50 -0
- package/src/detectors/registry/connection-time-short.ts +4 -0
- package/src/detectors/registry/duration-product-mismatch.ts +4 -0
- package/src/detectors/registry/party-order.ts +4 -0
- package/src/detectors/registry/requirement-lists.ts +4 -0
- package/src/detectors/registry/return-type.ts +4 -0
- package/src/detectors/requirement-lists.ts +65 -0
- package/src/detectors/return-type.ts +30 -0
- package/src/detectors/structure-tree.ts +12 -1
- package/src/detectors/time-order.ts +40 -5
- package/src/detectors/undefined-term.ts +20 -3
- package/src/document.ts +4 -1
- package/src/facts/table-facts.ts +3 -0
- package/src/name-variants.ts +76 -1
- package/src/name-word-order.ts +149 -0
- package/src/plugin.ts +5 -0
- package/src/return-type.ts +361 -0
- package/src/structure/connection-times.ts +226 -0
- package/src/structure/date-order-tie.ts +86 -0
- package/src/structure/date-order.ts +41 -14
- package/src/structure/date-outside-period.ts +72 -7
- package/src/structure/date-range.ts +45 -9
- package/src/structure/leg-times.ts +15 -0
- package/src/structure/party-order.ts +115 -0
- package/src/structure/requirement-lists.ts +145 -0
- package/src/structure/time-order.ts +8 -7
- package/src/structure/version-order.ts +10 -0
- package/src/surname-near.ts +31 -0
- package/src/table-cells.ts +41 -0
- package/src/table-names.ts +113 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// required-listed-as-preferred: one skill or qualification listed under a required heading or label and again under a
|
|
2
|
+
// preferred one. The headings and labels, the units and words of a length of experience, and the words that only end an
|
|
3
|
+
// item come from the lexicons. Pure.
|
|
4
|
+
import { escapeRegExp } from "../orthography.ts";
|
|
5
|
+
import type { StructureIssue } from "./issues.ts";
|
|
6
|
+
|
|
7
|
+
/** A heading, a line standing alone (a label if it is one of the words), or one list item, in document order. */
|
|
8
|
+
export type RequirementBlock =
|
|
9
|
+
| { readonly kind: "heading"; readonly depth: number; readonly text: string; readonly offset: number }
|
|
10
|
+
| { readonly kind: "line"; readonly text: string; readonly offset: number }
|
|
11
|
+
| { readonly kind: "item"; readonly text: string; readonly offset: number };
|
|
12
|
+
|
|
13
|
+
export type ValuedWord = { readonly word: string; readonly value: number };
|
|
14
|
+
|
|
15
|
+
export type RequirementWords = {
|
|
16
|
+
/** Headings and labels of what an applicant must have (必須, Required) and of what is welcome (歓迎, Preferred). */
|
|
17
|
+
readonly required: readonly string[];
|
|
18
|
+
readonly preferred: readonly string[];
|
|
19
|
+
/** Units of a length of experience, valued in months (年 12, month 1). */
|
|
20
|
+
readonly units: readonly ValuedWord[];
|
|
21
|
+
/** Words that stand before or after a length and belong to it (at least, 以上, of). */
|
|
22
|
+
readonly before: readonly string[];
|
|
23
|
+
readonly after: readonly string[];
|
|
24
|
+
/** Numbers written as words (three, 三). */
|
|
25
|
+
readonly numbers: readonly ValuedWord[];
|
|
26
|
+
/** Words that only end an item and are not the skill (の方, こと). */
|
|
27
|
+
readonly endings: readonly string[];
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
type Kind = "required" | "preferred";
|
|
31
|
+
|
|
32
|
+
/** container is the offset of the heading holding the lists (a posting, 応募の条件), -1 for none; only one container's items are compared. */
|
|
33
|
+
type Requirement = { readonly kind: Kind; readonly text: string; readonly offset: number; readonly container: number };
|
|
34
|
+
|
|
35
|
+
/** The skill an item names with its length of experience taken out, and that length in months (0 when none). */
|
|
36
|
+
type Reading = { readonly core: string; readonly months: number };
|
|
37
|
+
|
|
38
|
+
const MARKS = /[*_`~]/gu;
|
|
39
|
+
|
|
40
|
+
const LATIN = /^[a-z]/u;
|
|
41
|
+
|
|
42
|
+
const plain = (text: string): string => text.normalize("NFKC").toLowerCase().replace(MARKS, "").replace(/\s+/gu, " ").trim();
|
|
43
|
+
|
|
44
|
+
/** A Latin word is matched at word edges; other scripts as written. */
|
|
45
|
+
const wordPattern = (word: string): string => {
|
|
46
|
+
const escaped = escapeRegExp(plain(word));
|
|
47
|
+
return LATIN.test(plain(word)) ? `(?<![a-z])${escaped}(?![a-z])` : escaped;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const alternation = (words: readonly string[]): string =>
|
|
51
|
+
words
|
|
52
|
+
.toSorted((left, right) => right.length - left.length)
|
|
53
|
+
.map(wordPattern)
|
|
54
|
+
.join("|");
|
|
55
|
+
|
|
56
|
+
/** The text without the characters at either end that the test accepts (no backtracking regex on long lines). */
|
|
57
|
+
const trimWhere = (text: string, isEdge: (char: string) => boolean): string => {
|
|
58
|
+
const chars = [...text];
|
|
59
|
+
const start = chars.findIndex((char) => !isEdge(char));
|
|
60
|
+
return start === -1 ? "" : chars.slice(start, chars.findLastIndex((char) => !isEdge(char)) + 1).join("");
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const LABEL_EDGE = /[\s【[[「((】]\]」))::]/u;
|
|
64
|
+
|
|
65
|
+
const asLabel = (text: string): string => trimWhere(plain(text).replace(/[-‐–]/gu, " "), (char) => LABEL_EDGE.test(char)).replace(/\s+/gu, " ");
|
|
66
|
+
|
|
67
|
+
const kindOfLabel = (label: string, words: RequirementWords): Kind | undefined => {
|
|
68
|
+
if (label === "") return undefined;
|
|
69
|
+
if (words.required.some((word) => asLabel(word) === label)) return "required";
|
|
70
|
+
return words.preferred.some((word) => asLabel(word) === label) ? "preferred" : undefined;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const HEADING_PARTS = /[・//、,,((::]/u;
|
|
74
|
+
|
|
75
|
+
/** A heading names its kind by its first part (必須スキル・経験, 歓迎(あれば尚可)); a line standing alone, by all of it. */
|
|
76
|
+
const kindOfHeading = (text: string, words: RequirementWords): Kind | undefined => kindOfLabel(asLabel(text.split(HEADING_PARTS)[0] ?? ""), words);
|
|
77
|
+
|
|
78
|
+
type Scope = { readonly depth: number; readonly kind: Kind | undefined; readonly offset: number };
|
|
79
|
+
|
|
80
|
+
type Walk = { readonly scopes: readonly Scope[]; readonly label: Kind | undefined; readonly items: readonly Requirement[] };
|
|
81
|
+
|
|
82
|
+
/** A heading opens a scope that its subheadings inherit unless they name their own kind; a label holds until the next heading or label. */
|
|
83
|
+
const step = (walk: Walk, block: RequirementBlock, words: RequirementWords): Walk => {
|
|
84
|
+
if (block.kind === "heading") {
|
|
85
|
+
const outer = walk.scopes.filter((scope) => scope.depth < block.depth);
|
|
86
|
+
const kind = kindOfHeading(block.text, words) ?? outer.at(-1)?.kind;
|
|
87
|
+
return { ...walk, scopes: [...outer, { depth: block.depth, kind, offset: block.offset }], label: undefined };
|
|
88
|
+
}
|
|
89
|
+
if (block.kind === "line") {
|
|
90
|
+
const kind = kindOfLabel(asLabel(block.text), words);
|
|
91
|
+
return kind === undefined ? walk : { ...walk, label: kind };
|
|
92
|
+
}
|
|
93
|
+
const kind = walk.label ?? walk.scopes.at(-1)?.kind;
|
|
94
|
+
const container = walk.scopes.findLast((scope) => scope.kind === undefined)?.offset ?? -1;
|
|
95
|
+
return kind === undefined ? walk : { ...walk, items: [...walk.items, { kind, text: block.text.trim(), offset: block.offset, container }] };
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const numberPattern = (words: RequirementWords): string =>
|
|
99
|
+
[String.raw`\d+(?:\.\d+)?`, alternation(words.numbers.map((entry) => entry.word))].filter((part) => part !== "").join("|");
|
|
100
|
+
|
|
101
|
+
const lengthPattern = (words: RequirementWords): RegExp | undefined => {
|
|
102
|
+
if (words.units.length === 0) return undefined;
|
|
103
|
+
const before = words.before.length === 0 ? "" : `(?:(?:${alternation(words.before)})\\s*)?`;
|
|
104
|
+
const after = words.after.length === 0 ? "" : `(?:\\s*(?:${alternation(words.after)}))*`;
|
|
105
|
+
return new RegExp(`${before}(${numberPattern(words)})\\s*\\+?\\s*(${alternation(words.units.map((unit) => unit.word))})['’]?${after}`, "gu");
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const valueOf = (written: string, words: readonly ValuedWord[]): number => words.find((entry) => plain(entry.word) === written)?.value ?? Number(written);
|
|
109
|
+
|
|
110
|
+
/** Punctuation, symbols and spaces, but not the + and # that end a name (C++, C#). */
|
|
111
|
+
const PUNCTUATION = /(?<![\p{L}\p{N}+#])[+#]|[^\p{L}\p{N}+#]/gu;
|
|
112
|
+
|
|
113
|
+
const EDGE_PUNCTUATION = /[\p{P}\s]/u;
|
|
114
|
+
|
|
115
|
+
const withoutEnding = (written: string, endings: readonly string[]): string => {
|
|
116
|
+
const text = trimWhere(written, (char) => char !== "#" && EDGE_PUNCTUATION.test(char));
|
|
117
|
+
const ending = endings.map(plain).find((word) => word !== "" && text.endsWith(word) && text.length > word.length);
|
|
118
|
+
return ending === undefined ? text : withoutEnding(text.slice(0, -ending.length), endings);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const readingOf = (text: string, words: RequirementWords): Reading => {
|
|
122
|
+
const written = plain(text);
|
|
123
|
+
const pattern = lengthPattern(words);
|
|
124
|
+
const lengths = pattern === undefined ? [] : [...written.matchAll(pattern)];
|
|
125
|
+
const months = lengths.map((match) => valueOf(match[1] ?? "", words.numbers) * (words.units.find((unit) => plain(unit.word) === match[2])?.value ?? 0));
|
|
126
|
+
const rest = pattern === undefined ? written : written.replace(pattern, " ");
|
|
127
|
+
return { core: withoutEnding(rest.replace(/\s+/gu, " ").trim(), words.endings).replace(PUNCTUATION, ""), months: Math.max(0, ...months) };
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/** A preferred item repeats a required one when it names the same skill and asks for no longer experience. */
|
|
131
|
+
const repeats = (preferred: Reading, required: Reading): boolean =>
|
|
132
|
+
preferred.core !== "" && preferred.core === required.core && preferred.months <= required.months;
|
|
133
|
+
|
|
134
|
+
/** The preferred items that repeat a required item of the same document, once each, in document order. */
|
|
135
|
+
export const requiredListedAsPreferred = (blocks: readonly RequirementBlock[], words: RequirementWords): StructureIssue[] => {
|
|
136
|
+
const { items } = blocks.reduce<Walk>((walk, block) => step(walk, block, words), { scopes: [], label: undefined, items: [] });
|
|
137
|
+
const required = items.filter((item) => item.kind === "required").map((item) => ({ item, reading: readingOf(item.text, words) }));
|
|
138
|
+
return items
|
|
139
|
+
.filter((item) => item.kind === "preferred")
|
|
140
|
+
.flatMap((item) => {
|
|
141
|
+
const reading = readingOf(item.text, words);
|
|
142
|
+
const match = required.find((candidate) => candidate.item.container === item.container && repeats(reading, candidate.reading));
|
|
143
|
+
return match === undefined ? [] : [{ offset: item.offset, values: { item: item.text, required: match.item.text } }];
|
|
144
|
+
});
|
|
145
|
+
};
|
|
@@ -13,7 +13,7 @@ import { crossesZones, dayShiftOf, withoutDayShiftBefore, zonesIn, SECONDS_PER_D
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
/** 頭に時刻を書いた項目。seconds は 0 時からの秒、shift は翌日の印(1)か無印(0)。 */
|
|
16
|
-
type Stop = {
|
|
16
|
+
export type Stop = {
|
|
17
17
|
readonly offset: number;
|
|
18
18
|
readonly written: string;
|
|
19
19
|
readonly seconds: number;
|
|
@@ -130,11 +130,12 @@ const breaksIn = (day: readonly Stop[]): StructureIssue[] => {
|
|
|
130
130
|
return values.slice(1).flatMap((value, index) => (value < (values[index] ?? "") ? issueAt(day, outOfPlace(values, index + 1, 1)) : []));
|
|
131
131
|
};
|
|
132
132
|
|
|
133
|
-
/**
|
|
134
|
-
export const
|
|
133
|
+
/** 一日の予定として並べた時刻を、日ごとに。並びの中で日付か時間帯が変われば別の日。 */
|
|
134
|
+
export const scheduleDays = (source: string, times: readonly TimeSpan[], dates: readonly DateSpan[], words: TimeWords): Stop[][] => {
|
|
135
135
|
const inputs = { source, times, dates, words };
|
|
136
|
-
return runsOf(source).flatMap((run) =>
|
|
137
|
-
const stops = run.flatMap((line) => stopOf(line, inputs) ?? []);
|
|
138
|
-
return daysOf(stops).flatMap(breaksIn);
|
|
139
|
-
});
|
|
136
|
+
return runsOf(source).flatMap((run) => daysOf(run.flatMap((line) => stopOf(line, inputs) ?? [])));
|
|
140
137
|
};
|
|
138
|
+
|
|
139
|
+
/** 一日の予定の時刻のうち、早い順から外れたもの。 */
|
|
140
|
+
export const timeOrderBreaks = (source: string, times: readonly TimeSpan[], dates: readonly DateSpan[], words: TimeWords): StructureIssue[] =>
|
|
141
|
+
scheduleDays(source, times, dates, words).flatMap(breaksIn);
|
|
@@ -32,6 +32,16 @@ const compareVersions = (left: Version, right: Version): number => {
|
|
|
32
32
|
return differing === -1 ? comparePrerelease(left.prerelease, right.prerelease) : Math.sign((left.parts[differing] ?? 0) - (right.parts[differing] ?? 0));
|
|
33
33
|
};
|
|
34
34
|
|
|
35
|
+
/** Whether every label is a version and no step goes against direction (equal versions do not break it). */
|
|
36
|
+
export const versionsAlong = (labels: readonly (string | undefined)[], direction: number): boolean => {
|
|
37
|
+
const versions = labels.flatMap((label) => {
|
|
38
|
+
const version = label === undefined ? undefined : parse(label);
|
|
39
|
+
return version === undefined ? [] : [version];
|
|
40
|
+
});
|
|
41
|
+
if (versions.length !== labels.length) return false;
|
|
42
|
+
return versions.slice(1).every((version, index) => compareVersions(version, versions[index] ?? version) !== -direction);
|
|
43
|
+
};
|
|
44
|
+
|
|
35
45
|
/** The longest run along direction ending at each version (equal versions do not break it). */
|
|
36
46
|
const longestEndingAt = (versions: readonly Version[], direction: number): number[] =>
|
|
37
47
|
versions.reduce<number[]>((lengths, version, index) => {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// 名が同じ人の名前の、姓だけの書き損じ(Whitford と Whitfield)。一字違い(isNearWord)より遠い違いを、英字の長い姓に限って見る。
|
|
2
|
+
|
|
3
|
+
const LATIN_WORD = /^[a-z]+$/u;
|
|
4
|
+
|
|
5
|
+
/** 二字違いまで見る姓の長さ。短い姓(Martin と Marten、Hansen と Hanson)は別の姓のことが多い。 */
|
|
6
|
+
const MIN_SURNAME_LENGTH = 7;
|
|
7
|
+
/** 頭からそろう字の数。頭が違えば別の姓(Robertson と Robinson)。 */
|
|
8
|
+
const MIN_SURNAME_PREFIX = 4;
|
|
9
|
+
/** 頭と終わりのそろった字のあいだに残る字の数。短いほうで二字まで、長いほうで三字まで(Whitford の or と Whitfield の iel)。 */
|
|
10
|
+
const MAX_SURNAME_GAP = 2;
|
|
11
|
+
|
|
12
|
+
const sharedPrefix = (left: string, right: string): number => {
|
|
13
|
+
const differ = [...left].findIndex((char, index) => char !== right.charAt(index));
|
|
14
|
+
return differ < 0 ? left.length : differ;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 二つの英字の姓が、頭が四字以上そろい、終わりの字もそろい、あいだの二字(長いほうは三字)までだけ違うか。どちらも七字以上で。
|
|
19
|
+
* 終わりだけの違い(Whitford と Whitfords)は語の形の違い。
|
|
20
|
+
*/
|
|
21
|
+
export const isNearSurname = (left: string, right: string): boolean => {
|
|
22
|
+
if (left === right || !LATIN_WORD.test(left) || !LATIN_WORD.test(right) || Math.min(left.length, right.length) < MIN_SURNAME_LENGTH) return false;
|
|
23
|
+
const prefix = sharedPrefix(left, right);
|
|
24
|
+
if (prefix < MIN_SURNAME_PREFIX) return false;
|
|
25
|
+
const [leftRest, rightRest] = [left.slice(prefix), right.slice(prefix)];
|
|
26
|
+
const suffix = sharedPrefix([...leftRest].toReversed().join(""), [...rightRest].toReversed().join(""));
|
|
27
|
+
if (suffix === 0) return false;
|
|
28
|
+
const gaps = [leftRest.length - suffix, rightRest.length - suffix].toSorted((first, second) => first - second);
|
|
29
|
+
const [shorter = 0, longer = 0] = gaps;
|
|
30
|
+
return shorter <= MAX_SURNAME_GAP && longer <= MAX_SURNAME_GAP + 1;
|
|
31
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { ProseDocument, Segmentation, Span, TableCell, Token } from "./plugin.ts";
|
|
2
|
+
import { proseAndTablesOf } from "./table-text.ts";
|
|
3
|
+
import { tableBodyCells, type Cell } from "./facts/table-facts.ts";
|
|
4
|
+
|
|
5
|
+
// 表の升の語。本文は表を覆うので、升の字は文の分割も品詞解析も通らない。升ごとに adapter に渡し、語の位置を source の上に戻す。
|
|
6
|
+
|
|
7
|
+
const LETTER = /\p{L}/u;
|
|
8
|
+
|
|
9
|
+
const shiftToken = (token: Token, by: number): Token => ({ ...token, span: { start: token.span.start + by, end: token.span.end + by } });
|
|
10
|
+
|
|
11
|
+
/** 升一つの語。字の無い升(数だけ、空)は解析しない。adapter が語を返さなければ tokens は無い。 */
|
|
12
|
+
export const cellWithTokens = (cell: Cell, segment: (text: string) => Segmentation): TableCell => {
|
|
13
|
+
if (!LETTER.test(cell.text)) return cell;
|
|
14
|
+
const sentences = segment(cell.text).sentences;
|
|
15
|
+
if (!sentences.some((sentence) => sentence.tokens !== undefined)) return cell;
|
|
16
|
+
return { ...cell, tokens: sentences.flatMap((sentence) => (sentence.tokens ?? []).map((token) => shiftToken(token, cell.start))) };
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/** 文書の表(引用とコードの外)の本体の升と、その語。 */
|
|
20
|
+
export const tableCellsOf = (doc: ProseDocument, segment: (text: string) => Segmentation): TableCell[] =>
|
|
21
|
+
tableBodyCells(proseAndTablesOf(doc)).map((cell) => cellWithTokens(cell, segment));
|
|
22
|
+
|
|
23
|
+
/** prose に、升の字だけを書き戻したもの(表の見出しの行と区切りの | は覆ったまま)。位置は変えない。 */
|
|
24
|
+
export const proseWithCells = (prose: string, cells: readonly (Span & { readonly text: string })[]): string => {
|
|
25
|
+
const pieces: string[] = [];
|
|
26
|
+
const end = cells
|
|
27
|
+
.toSorted((left, right) => left.start - right.start)
|
|
28
|
+
.reduce((from, cell) => {
|
|
29
|
+
if (cell.start < from || cell.end - cell.start !== cell.text.length) return from;
|
|
30
|
+
pieces.push(prose.slice(from, cell.start), cell.text);
|
|
31
|
+
return cell.end;
|
|
32
|
+
}, 0);
|
|
33
|
+
pieces.push(prose.slice(end));
|
|
34
|
+
return pieces.join("");
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** 表の升の語は、読む rule があるときに一度だけ作る。升ごとの解析を、表を読まない rule に払わせない。 */
|
|
38
|
+
export const lazyCells = (make: () => readonly TableCell[]): (() => readonly TableCell[]) => {
|
|
39
|
+
const cells: { value?: readonly TableCell[] } = {};
|
|
40
|
+
return () => (cells.value ??= make());
|
|
41
|
+
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { isNearWord, nameKey, type NameMention } from "./name-variants.ts";
|
|
2
|
+
import type { TableCell, Token } from "./plugin.ts";
|
|
3
|
+
import { withoutEdgeMarks } from "./facts/trim-marks.ts";
|
|
4
|
+
|
|
5
|
+
// 表の升に書いた名前と、本文の名前の書き分け(本文は Sofia Mendes、担当者の表は Sofia Mendez)。表の升は品詞解析を通らないので、
|
|
6
|
+
// 名前の形をした升だけを読み、本文で読めた名前と比べる。比べるのは姓と名のように二語以上の名前で、一語だけが違うときに限る。
|
|
7
|
+
// 同じ語がほかにあることが、同じ人を指す手がかりになる。漢字・かなの名前は、本文と同じく読みが同じときに限る(佐々木 美穂 と
|
|
8
|
+
// 佐々木 美保)。読みの違う一字違い(佐藤 太郎 と 佐藤 次郎)は別の人のことが多い。
|
|
9
|
+
|
|
10
|
+
/** 表の升の名前。words は空白で分けた語。reading は升の語の読み(読めなければ無い)。 */
|
|
11
|
+
export type CellName = { readonly surface: string; readonly offset: number; readonly words: readonly string[]; readonly reading?: string | undefined };
|
|
12
|
+
|
|
13
|
+
/** 本文の名前の書き方と、その数。reading は最初の現れの読み。 */
|
|
14
|
+
export type ProseName = { readonly surface: string; readonly words: readonly string[]; readonly count: number; readonly reading?: string | undefined };
|
|
15
|
+
|
|
16
|
+
export type CellNameVariant = { readonly name: CellName; readonly usual: string; readonly kind: "spelling" | "reading" | "near" };
|
|
17
|
+
|
|
18
|
+
/** 英字の名前の升: 大文字で始まる語が二つから四つ。小文字の語(Write the procedure)を含む升は名前でない。 */
|
|
19
|
+
const LATIN_NAME = /^\p{Lu}[\p{L}'’.-]*(?: \p{Lu}[\p{L}'’.-]*){1,3}$/u;
|
|
20
|
+
/** 漢字・カタカナの名前の升: 名前の字の語が、空白一つを挟んで二つか三つ(佐々木 美穂)。 */
|
|
21
|
+
const CJK_NAME = /^[\p{Script=Han}\p{Script=Katakana}ー々]+(?:[ \u3000][\p{Script=Han}\p{Script=Katakana}ー々]+){1,2}$/u;
|
|
22
|
+
const SPACE = /[ \u3000]/u;
|
|
23
|
+
const LOWER = /\p{Ll}/u;
|
|
24
|
+
|
|
25
|
+
/** 範囲の中の語の読みをつないだもの。記号と空白の語は読まない。読めない語があれば無い。 */
|
|
26
|
+
export const readingWithin = (tokens: readonly Token[], start: number, end: number): string | undefined => {
|
|
27
|
+
const words = tokens.filter((token) => token.span.start >= start && token.span.end <= end && token.pos !== "PUNCT" && token.surface.trim() !== "");
|
|
28
|
+
const readings = words.map((token) => token.reading);
|
|
29
|
+
return words.length > 0 && readings.every((reading) => reading !== undefined && reading !== "") ? readings.join("") : undefined;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/** 名前の形をした升。升の字の前後の空白と強調の印は名前の外。 */
|
|
33
|
+
export const cellNamesIn = (cells: readonly TableCell[]): CellName[] =>
|
|
34
|
+
cells.flatMap((cell) => {
|
|
35
|
+
const surface = withoutEdgeMarks(cell.text.trim());
|
|
36
|
+
if (!(LATIN_NAME.test(surface) && LOWER.test(surface)) && !CJK_NAME.test(surface)) return [];
|
|
37
|
+
const offset = cell.start + cell.text.indexOf(surface);
|
|
38
|
+
return [{ surface, offset, words: surface.split(SPACE), reading: readingWithin(cell.tokens ?? [], offset, offset + surface.length) }];
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 本文の名前の書き方ごとの数。空白一つで並んだ名前の現れ(佐々木 と 美穂)は、一つの名前(佐々木 美穂)としても数える。
|
|
43
|
+
* 日本語の解析器は姓と名のあいだの空白で名前を切る。
|
|
44
|
+
*/
|
|
45
|
+
export const proseNamesOf = (mentions: readonly NameMention[], source: string): ProseName[] => {
|
|
46
|
+
const sorted = mentions.toSorted((left, right) => left.offset - right.offset);
|
|
47
|
+
const joined = sorted.flatMap((mention, index): Written[] => {
|
|
48
|
+
const next = sorted[index + 1];
|
|
49
|
+
const end = mention.offset + mention.surface.length;
|
|
50
|
+
if (next === undefined || next.offset !== end + 1 || !SPACE.test(source.charAt(end))) return [];
|
|
51
|
+
const reading = mention.reading === undefined || next.reading === undefined ? undefined : `${mention.reading}${next.reading}`;
|
|
52
|
+
return [{ surface: `${mention.surface}${source.charAt(end)}${next.surface}`, reading }];
|
|
53
|
+
});
|
|
54
|
+
const forms = new Map<string, ProseName>();
|
|
55
|
+
[...sorted, ...joined].forEach(({ surface, reading }) => {
|
|
56
|
+
const form = forms.get(surface);
|
|
57
|
+
forms.set(surface, form === undefined ? { surface, words: surface.split(SPACE), count: 1, reading } : { ...form, count: form.count + 1 });
|
|
58
|
+
});
|
|
59
|
+
return [...forms.values()];
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
type Written = { readonly surface: string; readonly reading: string | undefined };
|
|
63
|
+
|
|
64
|
+
const LATIN = /\p{Script=Latin}/u;
|
|
65
|
+
/** 英字の語の中の記号(O'Connor、Anne-Marie)。一字違いは字だけで見る。 */
|
|
66
|
+
const WORD_MARKS = /['’.-]/gu;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 二つの英字の語が一字違いか(名前の見方と同じ isNearWord)。漢字・かなの語は見ない。升は読みが分からず、一字違いの名前
|
|
70
|
+
* (佐藤 太郎 と 佐藤 次郎)は別の人のことが多い。
|
|
71
|
+
*/
|
|
72
|
+
const isNearNameWord = (left: string, right: string): boolean =>
|
|
73
|
+
LATIN.test(left) && LATIN.test(right) && isNearWord(left.toLowerCase().replaceAll(WORD_MARKS, ""), right.toLowerCase().replaceAll(WORD_MARKS, ""));
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 升の名前が、本文の名前の書き分けか。記号・幅・大小だけの違いか、語の数が同じで一語だけが違い、残りの語が同じとき。違う一語は、
|
|
77
|
+
* 英字なら一字違い、漢字・かなら名前全体の読みが同じ(本文の名前の見方と同じ)。この二つは、本文の形が二度以上、升の形が文書の
|
|
78
|
+
* 中で一度だけのとき(別の人のこともある)。
|
|
79
|
+
*/
|
|
80
|
+
export const cellNameRelation = (name: CellName, uses: number, prose: ProseName): CellNameVariant["kind"] | undefined => {
|
|
81
|
+
if (name.surface === prose.surface || name.words.length !== prose.words.length) return undefined;
|
|
82
|
+
if (nameKey(name.surface) === nameKey(prose.surface)) return "spelling";
|
|
83
|
+
const differ = name.words.flatMap((word, index) => (word === prose.words[index] ? [] : [index]));
|
|
84
|
+
const [at] = differ;
|
|
85
|
+
if (differ.length !== 1 || at === undefined || uses !== 1 || prose.count < 2) return undefined;
|
|
86
|
+
if (isNearNameWord(name.words[at] ?? "", prose.words[at] ?? "")) return "near";
|
|
87
|
+
return name.reading !== undefined && name.reading === prose.reading ? "reading" : undefined;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/** 記号・幅・大小だけの違いを、読みの同じものと一字違いより先に取る。 */
|
|
91
|
+
const KIND_ORDER: readonly CellNameVariant["kind"][] = ["spelling", "reading", "near"];
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 表の升の名前のうち、本文の名前の書き分けのもの。記号だけの違いを先に、その中で本文の形のうち一番多いものと比べる。
|
|
95
|
+
* 書き方ごとに最初の升を一つ。
|
|
96
|
+
*/
|
|
97
|
+
export const cellNameVariants = (names: readonly CellName[], prose: readonly ProseName[]): CellNameVariant[] => {
|
|
98
|
+
const uses = new Map(prose.map((form) => [form.surface, form.count]));
|
|
99
|
+
const firsts = new Map<string, CellName>();
|
|
100
|
+
names.forEach((name) => {
|
|
101
|
+
uses.set(name.surface, (uses.get(name.surface) ?? 0) + 1);
|
|
102
|
+
if (!firsts.has(name.surface)) firsts.set(name.surface, name);
|
|
103
|
+
});
|
|
104
|
+
return [...firsts.values()].flatMap((name) => {
|
|
105
|
+
const [usual] = prose
|
|
106
|
+
.flatMap((form) => {
|
|
107
|
+
const kind = cellNameRelation(name, uses.get(name.surface) ?? 0, form);
|
|
108
|
+
return kind === undefined ? [] : [{ form, kind }];
|
|
109
|
+
})
|
|
110
|
+
.toSorted((left, right) => KIND_ORDER.indexOf(left.kind) - KIND_ORDER.indexOf(right.kind) || right.form.count - left.form.count);
|
|
111
|
+
return usual === undefined ? [] : [{ name, usual: usual.form.surface, kind: usual.kind }];
|
|
112
|
+
});
|
|
113
|
+
};
|