chaffjs 0.32.0 → 0.33.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 +7 -0
- package/dist/cli-text.js +2 -2
- package/dist/cross-run.d.ts.map +1 -1
- package/dist/cross-run.js +1 -0
- package/dist/cross-run.js.map +1 -1
- package/dist/detectors/agreement-slip.d.ts.map +1 -1
- package/dist/detectors/agreement-slip.js +4 -0
- package/dist/detectors/agreement-slip.js.map +1 -1
- package/dist/detectors/formula-amount.d.ts +3 -0
- package/dist/detectors/formula-amount.d.ts.map +1 -0
- package/dist/detectors/formula-amount.js +41 -0
- package/dist/detectors/formula-amount.js.map +1 -0
- package/dist/detectors/installment-total-mismatch.d.ts.map +1 -1
- package/dist/detectors/installment-total-mismatch.js +27 -8
- package/dist/detectors/installment-total-mismatch.js.map +1 -1
- package/dist/detectors/orthographic-variant.d.ts.map +1 -1
- package/dist/detectors/orthographic-variant.js +4 -3
- package/dist/detectors/orthographic-variant.js.map +1 -1
- package/dist/detectors/registry/formula-amount.d.ts +3 -0
- package/dist/detectors/registry/formula-amount.d.ts.map +1 -0
- package/dist/detectors/registry/formula-amount.js +3 -0
- package/dist/detectors/registry/formula-amount.js.map +1 -0
- package/dist/detectors/structure-tree.d.ts +1 -1
- package/dist/detectors/structure-tree.d.ts.map +1 -1
- package/dist/detectors/structure-tree.js +4 -2
- package/dist/detectors/structure-tree.js.map +1 -1
- package/dist/eval.js +3 -3
- package/dist/eval.js.map +1 -1
- package/dist/fix-plan/text.js +2 -2
- package/dist/fix-plan/text.js.map +1 -1
- package/dist/lexicon-genres.d.ts +7 -0
- package/dist/lexicon-genres.d.ts.map +1 -0
- package/dist/lexicon-genres.js +8 -0
- package/dist/lexicon-genres.js.map +1 -0
- package/dist/plugin.d.ts +6 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/profile/parse.d.ts.map +1 -1
- package/dist/profile/parse.js +5 -0
- package/dist/profile/parse.js.map +1 -1
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +1 -0
- package/dist/run.js.map +1 -1
- package/dist/structure/date-range.d.ts +3 -2
- package/dist/structure/date-range.d.ts.map +1 -1
- package/dist/structure/date-range.js +4 -4
- package/dist/structure/date-range.js.map +1 -1
- package/dist/structure/formula-amount.d.ts +38 -0
- package/dist/structure/formula-amount.d.ts.map +1 -0
- package/dist/structure/formula-amount.js +187 -0
- package/dist/structure/formula-amount.js.map +1 -0
- package/dist/structure/installment-fee.d.ts +52 -0
- package/dist/structure/installment-fee.d.ts.map +1 -0
- package/dist/structure/installment-fee.js +110 -0
- package/dist/structure/installment-fee.js.map +1 -0
- package/dist/structure/installment-total.d.ts +7 -0
- package/dist/structure/installment-total.d.ts.map +1 -1
- package/dist/structure/installment-total.js +6 -6
- package/dist/structure/installment-total.js.map +1 -1
- package/dist/structure/start-end-range.d.ts +30 -0
- package/dist/structure/start-end-range.d.ts.map +1 -0
- package/dist/structure/start-end-range.js +79 -0
- package/dist/structure/start-end-range.js.map +1 -0
- package/package.json +4 -4
- package/profiles/property-sale.yaml +21 -0
- package/rules/date-range-reversed.yaml +4 -4
- package/rules/formula-amount-mismatch.yaml +67 -0
- package/rules/installment-total-mismatch.yaml +9 -4
- package/rules/orthographic-variant.yaml +3 -3
- package/src/cli-text.ts +2 -2
- package/src/cross-run.ts +1 -0
- package/src/detectors/agreement-slip.ts +3 -0
- package/src/detectors/formula-amount.ts +59 -0
- package/src/detectors/installment-total-mismatch.ts +34 -8
- package/src/detectors/orthographic-variant.ts +5 -3
- package/src/detectors/registry/formula-amount.ts +4 -0
- package/src/detectors/structure-tree.ts +4 -2
- package/src/eval.ts +3 -3
- package/src/fix-plan/text.ts +2 -2
- package/src/lexicon-genres.ts +11 -0
- package/src/plugin.ts +6 -0
- package/src/profile/parse.ts +8 -0
- package/src/run.ts +1 -0
- package/src/structure/date-range.ts +4 -3
- package/src/structure/formula-amount.ts +236 -0
- package/src/structure/installment-fee.ts +201 -0
- package/src/structure/installment-total.ts +6 -6
- package/src/structure/start-end-range.ts +120 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { escapeRegExp } from "../orthography.ts";
|
|
2
|
+
import type { Lexicon } from "../plugin.ts";
|
|
3
|
+
import { bare, withoutTrailingWeekday, type DatedSpan } from "./date-range.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 始まりの語と終わりの語で書いた期間(The term starts on 1 April 2026 and ends on 31 March 2027、2026年4月1日から開始し、2027年3月31日に終了する)。
|
|
7
|
+
* 一つ目の日付に始まりの語が、二つ目の日付に終わりの語が付き、二つの日付の間は付いた語とつなぎの語だけ。
|
|
8
|
+
* 文に始まりの語か終わりの語が二つあれば、どの日付がどの期間のものか決まらないので読まない。語は語彙表(range-start-end)から受け取る。
|
|
9
|
+
*/
|
|
10
|
+
export type StartEndWord = { readonly word: string; readonly position: "before" | "after" };
|
|
11
|
+
|
|
12
|
+
export type StartEndWords = {
|
|
13
|
+
readonly starts: readonly StartEndWord[];
|
|
14
|
+
readonly ends: readonly StartEndWord[];
|
|
15
|
+
/** 始まりの語と終わりの語の間に立つ語(and、and shall、し、)。 */
|
|
16
|
+
readonly joints: readonly string[];
|
|
17
|
+
/**
|
|
18
|
+
* 終わりの日付の後ろに付く終わりの語(に終了)の、さらに後ろに来てよい語(する、し、)。「、」で終わる語は文が続くので頭だけを照らし、
|
|
19
|
+
* ほかは文の終わりまでがその語だけ。「に終了した旧契約」は名詞にかかる語で、期間の終わりではない。
|
|
20
|
+
*/
|
|
21
|
+
readonly tails: readonly string[];
|
|
22
|
+
readonly weekdays: readonly string[];
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** 文の終わり。英語は後ろが大文字のときだけ("in Jan. 2026" は切らない)。段落の切れ目も文の終わり。 */
|
|
26
|
+
const SENTENCE_END = /[。!?]|[.!?]\s+(?=[A-Z])|\n\s*\n/gu;
|
|
27
|
+
|
|
28
|
+
const flat = (text: string): string => text.replace(/\s+/gu, " ").toLowerCase();
|
|
29
|
+
|
|
30
|
+
const LATIN = /[a-z]/u;
|
|
31
|
+
|
|
32
|
+
/** 英字の語は語の切れ目で照らす(ends が depends に当たらない)。英字で始まらない・終わらない語は、その側を照らさない。 */
|
|
33
|
+
const wordPattern = (word: string, flags: string): RegExp => {
|
|
34
|
+
const head = LATIN.test(word.charAt(0)) ? "(?<![a-z])" : "";
|
|
35
|
+
const tail = LATIN.test(word.at(-1) ?? "") ? "(?![a-z])" : "";
|
|
36
|
+
return new RegExp(`${head}${escapeRegExp(word)}${tail}`, flags);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const endsWithWord = (text: string, word: string): boolean => new RegExp(`${wordPattern(word, "u").source}$`, "u").test(text);
|
|
40
|
+
|
|
41
|
+
const startsWithWord = (text: string, word: string): boolean => new RegExp(`^${wordPattern(word, "u").source}`, "u").test(text);
|
|
42
|
+
|
|
43
|
+
/** 文の中で、語のどれかに当たる所の数。重なる当たり(starts と starts on)は一つ。 */
|
|
44
|
+
export const occurrences = (text: string, words: readonly StartEndWord[]): number => {
|
|
45
|
+
const spans = words
|
|
46
|
+
.flatMap(({ word }) => [...text.matchAll(wordPattern(word, "gu"))].map((match) => ({ start: match.index, end: match.index + match[0].length })))
|
|
47
|
+
.toSorted((left, right) => left.start - right.start || right.end - left.end);
|
|
48
|
+
return spans.reduce(
|
|
49
|
+
(acc, span) => (span.start >= acc.end ? { count: acc.count + 1, end: span.end } : { count: acc.count, end: Math.max(acc.end, span.end) }),
|
|
50
|
+
{
|
|
51
|
+
count: 0,
|
|
52
|
+
end: -1,
|
|
53
|
+
},
|
|
54
|
+
).count;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
type Sentence = { readonly before: string; readonly between: string; readonly after: string; readonly whole: string };
|
|
58
|
+
|
|
59
|
+
/** 二つの日付を含む文を、始まりの日付の前、二つの日付の間、終わりの日付の後ろに分ける。間に文の終わりがあれば undefined。 */
|
|
60
|
+
const sentenceOf = (source: string, start: DatedSpan, end: DatedSpan): Sentence | undefined => {
|
|
61
|
+
const between = source.slice(start.end, end.offset);
|
|
62
|
+
if (new RegExp(SENTENCE_END.source, "u").test(between)) return undefined;
|
|
63
|
+
const from = [...source.slice(0, start.offset).matchAll(SENTENCE_END)].map((match) => match.index + match[0].length).at(-1) ?? 0;
|
|
64
|
+
const tail = source.slice(end.end);
|
|
65
|
+
const to = end.end + ([...tail.matchAll(SENTENCE_END)][0]?.index ?? tail.length);
|
|
66
|
+
const before = flat(source.slice(from, start.offset)).trimEnd();
|
|
67
|
+
const after = bare(source.slice(end.end, to));
|
|
68
|
+
return { before, between: bare(between), after, whole: flat(source.slice(from, to)) };
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const beforeWord = (text: string, words: readonly StartEndWord[]): string | undefined =>
|
|
72
|
+
words.find((entry) => entry.position === "before" && endsWithWord(text, entry.word))?.word;
|
|
73
|
+
|
|
74
|
+
const afterWord = (text: string, words: readonly StartEndWord[]): string | undefined =>
|
|
75
|
+
words.find((entry) => entry.position === "after" && startsWithWord(text, entry.word))?.word;
|
|
76
|
+
|
|
77
|
+
const endsPredicate = (rest: string, tails: readonly string[]): boolean => tails.some((tail) => (tail.endsWith("、") ? rest.startsWith(tail) : rest === tail));
|
|
78
|
+
|
|
79
|
+
/** 終わりの日付の後ろの終わりの語で、その後ろが述語の終わり(に終了する。に終了し、)。 */
|
|
80
|
+
const endAfterWord = (after: string, words: StartEndWords): string | undefined => {
|
|
81
|
+
const word = afterWord(after, words.ends);
|
|
82
|
+
return word !== undefined && endsPredicate(after.slice(word.length).trim(), words.tails) ? word : undefined;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/** 間の語から、始まりの日付の後ろの語と終わりの日付の前の語を除いた残りが、つなぎの語か。 */
|
|
86
|
+
const joinedBetween = (between: string, words: StartEndWords, startAfter: string | undefined, endBefore: string | undefined): boolean => {
|
|
87
|
+
const head = startAfter === undefined ? between : between.slice(startAfter.length).trimStart();
|
|
88
|
+
const rest = endBefore === undefined ? head : head.slice(0, head.length - endBefore.length).trimEnd();
|
|
89
|
+
return words.joints.some((joint) => joint.toLowerCase() === rest);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/** 二つの日付が、始まりの語と終わりの語で書いた一つの期間か。 */
|
|
93
|
+
export const isStartEndRange = (source: string, start: DatedSpan, end: DatedSpan, words: StartEndWords): boolean => {
|
|
94
|
+
const sentence = sentenceOf(source, start, end);
|
|
95
|
+
if (sentence === undefined) return false;
|
|
96
|
+
if (occurrences(sentence.whole, words.starts) !== 1 || occurrences(sentence.whole, words.ends) !== 1) return false;
|
|
97
|
+
const between = withoutTrailingWeekday(sentence.between, words.weekdays);
|
|
98
|
+
const startBefore = beforeWord(withoutTrailingWeekday(sentence.before, words.weekdays), words.starts);
|
|
99
|
+
const startAfter = afterWord(between, words.starts);
|
|
100
|
+
const endBefore = beforeWord(between, words.ends);
|
|
101
|
+
const endAfter = endAfterWord(sentence.after, words);
|
|
102
|
+
if ((startBefore ?? startAfter) === undefined || (endBefore ?? endAfter) === undefined) return false;
|
|
103
|
+
return joinedBetween(between, words, startAfter, endBefore);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const wordsIn = (entries: Lexicon, group: "start" | "end"): StartEndWord[] =>
|
|
107
|
+
entries
|
|
108
|
+
.filter((entry) => entry.group === group)
|
|
109
|
+
.map((entry) => ({ word: entry.pattern.toLowerCase(), position: entry.position === "after" ? "after" : "before" }));
|
|
110
|
+
|
|
111
|
+
const patternsIn = (entries: Lexicon, group: string): string[] => entries.filter((entry) => entry.group === group).map((entry) => entry.pattern.toLowerCase());
|
|
112
|
+
|
|
113
|
+
/** 語彙表 range-start-end の行を、group(start、end、joint、tail)で分ける。 */
|
|
114
|
+
export const startEndWordsOf = (entries: Lexicon, weekdays: readonly string[]): StartEndWords => ({
|
|
115
|
+
starts: wordsIn(entries, "start"),
|
|
116
|
+
ends: wordsIn(entries, "end"),
|
|
117
|
+
joints: patternsIn(entries, "joint"),
|
|
118
|
+
tails: patternsIn(entries, "tail"),
|
|
119
|
+
weekdays,
|
|
120
|
+
});
|