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.
Files changed (87) hide show
  1. package/README.md +7 -0
  2. package/dist/cli-text.js +2 -2
  3. package/dist/cross-run.d.ts.map +1 -1
  4. package/dist/cross-run.js +1 -0
  5. package/dist/cross-run.js.map +1 -1
  6. package/dist/detectors/agreement-slip.d.ts.map +1 -1
  7. package/dist/detectors/agreement-slip.js +4 -0
  8. package/dist/detectors/agreement-slip.js.map +1 -1
  9. package/dist/detectors/formula-amount.d.ts +3 -0
  10. package/dist/detectors/formula-amount.d.ts.map +1 -0
  11. package/dist/detectors/formula-amount.js +41 -0
  12. package/dist/detectors/formula-amount.js.map +1 -0
  13. package/dist/detectors/installment-total-mismatch.d.ts.map +1 -1
  14. package/dist/detectors/installment-total-mismatch.js +27 -8
  15. package/dist/detectors/installment-total-mismatch.js.map +1 -1
  16. package/dist/detectors/orthographic-variant.d.ts.map +1 -1
  17. package/dist/detectors/orthographic-variant.js +4 -3
  18. package/dist/detectors/orthographic-variant.js.map +1 -1
  19. package/dist/detectors/registry/formula-amount.d.ts +3 -0
  20. package/dist/detectors/registry/formula-amount.d.ts.map +1 -0
  21. package/dist/detectors/registry/formula-amount.js +3 -0
  22. package/dist/detectors/registry/formula-amount.js.map +1 -0
  23. package/dist/detectors/structure-tree.d.ts +1 -1
  24. package/dist/detectors/structure-tree.d.ts.map +1 -1
  25. package/dist/detectors/structure-tree.js +4 -2
  26. package/dist/detectors/structure-tree.js.map +1 -1
  27. package/dist/eval.js +3 -3
  28. package/dist/eval.js.map +1 -1
  29. package/dist/fix-plan/text.js +2 -2
  30. package/dist/fix-plan/text.js.map +1 -1
  31. package/dist/lexicon-genres.d.ts +7 -0
  32. package/dist/lexicon-genres.d.ts.map +1 -0
  33. package/dist/lexicon-genres.js +8 -0
  34. package/dist/lexicon-genres.js.map +1 -0
  35. package/dist/plugin.d.ts +6 -0
  36. package/dist/plugin.d.ts.map +1 -1
  37. package/dist/profile/parse.d.ts.map +1 -1
  38. package/dist/profile/parse.js +5 -0
  39. package/dist/profile/parse.js.map +1 -1
  40. package/dist/run.d.ts.map +1 -1
  41. package/dist/run.js +1 -0
  42. package/dist/run.js.map +1 -1
  43. package/dist/structure/date-range.d.ts +3 -2
  44. package/dist/structure/date-range.d.ts.map +1 -1
  45. package/dist/structure/date-range.js +4 -4
  46. package/dist/structure/date-range.js.map +1 -1
  47. package/dist/structure/formula-amount.d.ts +38 -0
  48. package/dist/structure/formula-amount.d.ts.map +1 -0
  49. package/dist/structure/formula-amount.js +187 -0
  50. package/dist/structure/formula-amount.js.map +1 -0
  51. package/dist/structure/installment-fee.d.ts +52 -0
  52. package/dist/structure/installment-fee.d.ts.map +1 -0
  53. package/dist/structure/installment-fee.js +110 -0
  54. package/dist/structure/installment-fee.js.map +1 -0
  55. package/dist/structure/installment-total.d.ts +7 -0
  56. package/dist/structure/installment-total.d.ts.map +1 -1
  57. package/dist/structure/installment-total.js +6 -6
  58. package/dist/structure/installment-total.js.map +1 -1
  59. package/dist/structure/start-end-range.d.ts +30 -0
  60. package/dist/structure/start-end-range.d.ts.map +1 -0
  61. package/dist/structure/start-end-range.js +79 -0
  62. package/dist/structure/start-end-range.js.map +1 -0
  63. package/package.json +4 -4
  64. package/profiles/property-sale.yaml +21 -0
  65. package/rules/date-range-reversed.yaml +4 -4
  66. package/rules/formula-amount-mismatch.yaml +67 -0
  67. package/rules/installment-total-mismatch.yaml +9 -4
  68. package/rules/orthographic-variant.yaml +3 -3
  69. package/src/cli-text.ts +2 -2
  70. package/src/cross-run.ts +1 -0
  71. package/src/detectors/agreement-slip.ts +3 -0
  72. package/src/detectors/formula-amount.ts +59 -0
  73. package/src/detectors/installment-total-mismatch.ts +34 -8
  74. package/src/detectors/orthographic-variant.ts +5 -3
  75. package/src/detectors/registry/formula-amount.ts +4 -0
  76. package/src/detectors/structure-tree.ts +4 -2
  77. package/src/eval.ts +3 -3
  78. package/src/fix-plan/text.ts +2 -2
  79. package/src/lexicon-genres.ts +11 -0
  80. package/src/plugin.ts +6 -0
  81. package/src/profile/parse.ts +8 -0
  82. package/src/run.ts +1 -0
  83. package/src/structure/date-range.ts +4 -3
  84. package/src/structure/formula-amount.ts +236 -0
  85. package/src/structure/installment-fee.ts +201 -0
  86. package/src/structure/installment-total.ts +6 -6
  87. package/src/structure/start-end-range.ts +120 -0
@@ -0,0 +1,30 @@
1
+ import type { Lexicon } from "../plugin.ts";
2
+ import { type DatedSpan } from "./date-range.ts";
3
+ /**
4
+ * 始まりの語と終わりの語で書いた期間(The term starts on 1 April 2026 and ends on 31 March 2027、2026年4月1日から開始し、2027年3月31日に終了する)。
5
+ * 一つ目の日付に始まりの語が、二つ目の日付に終わりの語が付き、二つの日付の間は付いた語とつなぎの語だけ。
6
+ * 文に始まりの語か終わりの語が二つあれば、どの日付がどの期間のものか決まらないので読まない。語は語彙表(range-start-end)から受け取る。
7
+ */
8
+ export type StartEndWord = {
9
+ readonly word: string;
10
+ 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
+ /** 文の中で、語のどれかに当たる所の数。重なる当たり(starts と starts on)は一つ。 */
25
+ export declare const occurrences: (text: string, words: readonly StartEndWord[]) => number;
26
+ /** 二つの日付が、始まりの語と終わりの語で書いた一つの期間か。 */
27
+ export declare const isStartEndRange: (source: string, start: DatedSpan, end: DatedSpan, words: StartEndWords) => boolean;
28
+ /** 語彙表 range-start-end の行を、group(start、end、joint、tail)で分ける。 */
29
+ export declare const startEndWordsOf: (entries: Lexicon, weekdays: readonly string[]) => StartEndWords;
30
+ //# sourceMappingURL=start-end-range.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"start-end-range.d.ts","sourceRoot":"","sources":["../../src/structure/start-end-range.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAgC,KAAK,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAE/E;;;;GAIG;AACH,MAAM,MAAM,YAAY,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAA;CAAE,CAAC;AAE5F,MAAM,MAAM,aAAa,GAAG;IAC1B,QAAQ,CAAC,MAAM,EAAE,SAAS,YAAY,EAAE,CAAC;IACzC,QAAQ,CAAC,IAAI,EAAE,SAAS,YAAY,EAAE,CAAC;IACvC,2CAA2C;IAC3C,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC;;;OAGG;IACH,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;CACtC,CAAC;AAoBF,uDAAuD;AACvD,eAAO,MAAM,WAAW,GAAI,MAAM,MAAM,EAAE,OAAO,SAAS,YAAY,EAAE,KAAG,MAW1E,CAAC;AAqCF,oCAAoC;AACpC,eAAO,MAAM,eAAe,GAAI,QAAQ,MAAM,EAAE,OAAO,SAAS,EAAE,KAAK,SAAS,EAAE,OAAO,aAAa,KAAG,OAWxG,CAAC;AASF,+DAA+D;AAC/D,eAAO,MAAM,eAAe,GAAI,SAAS,OAAO,EAAE,UAAU,SAAS,MAAM,EAAE,KAAG,aAM9E,CAAC"}
@@ -0,0 +1,79 @@
1
+ import { escapeRegExp } from "../orthography.js";
2
+ import { bare, withoutTrailingWeekday } from "./date-range.js";
3
+ /** 文の終わり。英語は後ろが大文字のときだけ("in Jan. 2026" は切らない)。段落の切れ目も文の終わり。 */
4
+ const SENTENCE_END = /[。!?]|[.!?]\s+(?=[A-Z])|\n\s*\n/gu;
5
+ const flat = (text) => text.replace(/\s+/gu, " ").toLowerCase();
6
+ const LATIN = /[a-z]/u;
7
+ /** 英字の語は語の切れ目で照らす(ends が depends に当たらない)。英字で始まらない・終わらない語は、その側を照らさない。 */
8
+ const wordPattern = (word, flags) => {
9
+ const head = LATIN.test(word.charAt(0)) ? "(?<![a-z])" : "";
10
+ const tail = LATIN.test(word.at(-1) ?? "") ? "(?![a-z])" : "";
11
+ return new RegExp(`${head}${escapeRegExp(word)}${tail}`, flags);
12
+ };
13
+ const endsWithWord = (text, word) => new RegExp(`${wordPattern(word, "u").source}$`, "u").test(text);
14
+ const startsWithWord = (text, word) => new RegExp(`^${wordPattern(word, "u").source}`, "u").test(text);
15
+ /** 文の中で、語のどれかに当たる所の数。重なる当たり(starts と starts on)は一つ。 */
16
+ export const occurrences = (text, words) => {
17
+ const spans = words
18
+ .flatMap(({ word }) => [...text.matchAll(wordPattern(word, "gu"))].map((match) => ({ start: match.index, end: match.index + match[0].length })))
19
+ .toSorted((left, right) => left.start - right.start || right.end - left.end);
20
+ return spans.reduce((acc, span) => (span.start >= acc.end ? { count: acc.count + 1, end: span.end } : { count: acc.count, end: Math.max(acc.end, span.end) }), {
21
+ count: 0,
22
+ end: -1,
23
+ }).count;
24
+ };
25
+ /** 二つの日付を含む文を、始まりの日付の前、二つの日付の間、終わりの日付の後ろに分ける。間に文の終わりがあれば undefined。 */
26
+ const sentenceOf = (source, start, end) => {
27
+ const between = source.slice(start.end, end.offset);
28
+ if (new RegExp(SENTENCE_END.source, "u").test(between))
29
+ return undefined;
30
+ const from = [...source.slice(0, start.offset).matchAll(SENTENCE_END)].map((match) => match.index + match[0].length).at(-1) ?? 0;
31
+ const tail = source.slice(end.end);
32
+ const to = end.end + ([...tail.matchAll(SENTENCE_END)][0]?.index ?? tail.length);
33
+ const before = flat(source.slice(from, start.offset)).trimEnd();
34
+ const after = bare(source.slice(end.end, to));
35
+ return { before, between: bare(between), after, whole: flat(source.slice(from, to)) };
36
+ };
37
+ const beforeWord = (text, words) => words.find((entry) => entry.position === "before" && endsWithWord(text, entry.word))?.word;
38
+ const afterWord = (text, words) => words.find((entry) => entry.position === "after" && startsWithWord(text, entry.word))?.word;
39
+ const endsPredicate = (rest, tails) => tails.some((tail) => (tail.endsWith("、") ? rest.startsWith(tail) : rest === tail));
40
+ /** 終わりの日付の後ろの終わりの語で、その後ろが述語の終わり(に終了する。に終了し、)。 */
41
+ const endAfterWord = (after, words) => {
42
+ const word = afterWord(after, words.ends);
43
+ return word !== undefined && endsPredicate(after.slice(word.length).trim(), words.tails) ? word : undefined;
44
+ };
45
+ /** 間の語から、始まりの日付の後ろの語と終わりの日付の前の語を除いた残りが、つなぎの語か。 */
46
+ const joinedBetween = (between, words, startAfter, endBefore) => {
47
+ const head = startAfter === undefined ? between : between.slice(startAfter.length).trimStart();
48
+ const rest = endBefore === undefined ? head : head.slice(0, head.length - endBefore.length).trimEnd();
49
+ return words.joints.some((joint) => joint.toLowerCase() === rest);
50
+ };
51
+ /** 二つの日付が、始まりの語と終わりの語で書いた一つの期間か。 */
52
+ export const isStartEndRange = (source, start, end, words) => {
53
+ const sentence = sentenceOf(source, start, end);
54
+ if (sentence === undefined)
55
+ return false;
56
+ if (occurrences(sentence.whole, words.starts) !== 1 || occurrences(sentence.whole, words.ends) !== 1)
57
+ return false;
58
+ const between = withoutTrailingWeekday(sentence.between, words.weekdays);
59
+ const startBefore = beforeWord(withoutTrailingWeekday(sentence.before, words.weekdays), words.starts);
60
+ const startAfter = afterWord(between, words.starts);
61
+ const endBefore = beforeWord(between, words.ends);
62
+ const endAfter = endAfterWord(sentence.after, words);
63
+ if ((startBefore ?? startAfter) === undefined || (endBefore ?? endAfter) === undefined)
64
+ return false;
65
+ return joinedBetween(between, words, startAfter, endBefore);
66
+ };
67
+ const wordsIn = (entries, group) => entries
68
+ .filter((entry) => entry.group === group)
69
+ .map((entry) => ({ word: entry.pattern.toLowerCase(), position: entry.position === "after" ? "after" : "before" }));
70
+ const patternsIn = (entries, group) => entries.filter((entry) => entry.group === group).map((entry) => entry.pattern.toLowerCase());
71
+ /** 語彙表 range-start-end の行を、group(start、end、joint、tail)で分ける。 */
72
+ export const startEndWordsOf = (entries, weekdays) => ({
73
+ starts: wordsIn(entries, "start"),
74
+ ends: wordsIn(entries, "end"),
75
+ joints: patternsIn(entries, "joint"),
76
+ tails: patternsIn(entries, "tail"),
77
+ weekdays,
78
+ });
79
+ //# sourceMappingURL=start-end-range.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"start-end-range.js","sourceRoot":"","sources":["../../src/structure/start-end-range.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chaffjs",
3
- "version": "0.32.0",
3
+ "version": "0.33.0",
4
4
  "description": "Prose validation harness. Finds what is hard to read, and never rewrites it for you.",
5
5
  "keywords": [
6
6
  "chaff",
@@ -12,7 +12,7 @@
12
12
  "japanese",
13
13
  "ci"
14
14
  ],
15
- "homepage": "https://github.com/isamu/lab/tree/main/text",
15
+ "homepage": "https://isamu.github.io/lab/en/",
16
16
  "repository": {
17
17
  "type": "git",
18
18
  "url": "git+https://github.com/isamu/lab.git",
@@ -88,8 +88,8 @@
88
88
  },
89
89
  "dependencies": {
90
90
  "@anthropic-ai/sdk": "^0.125.0",
91
- "@chaffjs/lang-en": "0.29.0",
92
- "@chaffjs/lang-ja": "0.31.0",
91
+ "@chaffjs/lang-en": "0.30.0",
92
+ "@chaffjs/lang-ja": "0.32.0",
93
93
  "character-entities": "^2.0.2",
94
94
  "mdast-util-from-markdown": "^2.0.3",
95
95
  "mdast-util-frontmatter": "^2.0.1",
@@ -0,0 +1,21 @@
1
+ id: property-sale
2
+
3
+ name:
4
+ ja: 不動産の売買の精算書
5
+ en: Property sale settlement statement
6
+
7
+ why:
8
+ ja: 日本の不動産の売買の精算書は、仲介手数料を「売買代金の3%+6万円(消費税込み)」のように税込みと書き、税率を書かないことが多くあります。宅地建物取引業者の仲介手数料には消費税10%がかかるため、精算書では、税率を書いていなくても10%で手数料の計算を確かめます。
9
+ en: A Japanese property sale settlement statement often writes a brokerage fee as "3% of the price plus 60,000 yen, consumption tax included" without the rate. A broker's fee bears the 10% consumption tax, so in such a statement the fee is checked at 10% even when the rate is not written. English statements have no such convention, so this profile has no English part.
10
+
11
+ # 設定(chaff.yaml の profile / by_path)が無いときに、内容から選ぶ。売買の精算の項目(売買代金、手付金、残代金、仲介手数料、
12
+ # 売買契約日、決済日)の行が min_lines 行以上あれば精算書として読む。代金の表の三行(売買代金・手付金・残代金)だけでは選ばない。
13
+ # 表の行(| 売買代金 | …)も「決済日:…」の行も、見出しの括弧(手付金(契約時にお支払い済み))も数える。
14
+ detect:
15
+ ja:
16
+ line: "^[ \\t]*\\|?[ \\t]*(?:売買代金|手付金|残代金|仲介手数料|売買契約日|決済日)(?:[((][^))\\n]*[))])?[ \\t]*[||::]"
17
+ min_lines: 4
18
+
19
+ ja:
20
+ # 見出しが「消費税込み」とだけ書き、見出しにも文書にも税率が無いときの税率(%)(formula-amount-mismatch)。
21
+ tax_rate: 10
@@ -10,8 +10,8 @@ name:
10
10
  en: A period that ends before it starts
11
11
 
12
12
  why:
13
- ja: 「2026年4月1日〜2026年3月31日」のように、期間の終わりの日付が始まりより前だと、読み手はどちらの日付が正しいのかを問い合わせることになります。年を書き写し間違えたときや、日付を片方だけ直したときに起きます。二つの日付の間が範囲の記号(〜、–)だけのときと、「から」でつなぎ終わりの日付のあとに「まで」が続くときを期間として比べます。年の無い期間(12月28日〜1月4日)は年をまたぐかもしれないので比べません。年を片方にだけ書いた期間(2026年7月9日〜7月8日)は同じ年と読み、逆になるときは、年の無い側の曜日がその年に合うか、翌年(または前年)にまたぐと読むと一年近くになるときだけ言います(2026年12月28日〜1月4日は年をまたぐ期間です)。
14
- en: 'When a period such as "5 April 2026 – 2 April 2026" ends before it starts, the reader has to ask which date is right. It happens when a year is copied wrong, or when only one of the two dates is changed. Two dates are read as a period when only a range mark (–, —) or a word such as "through" or "until" stands between them, or when they are written "from … to …". A period without a year ("28 December – 4 January") may run into the next year and is not compared. With the year on one end only ("July 9 – July 8, 2026"), both ends are read in that year; a reversed one is reported only when the weekday on the end without a year matches that reading, or when reading it across the year end would make the period nearly a year long ("December 28 – January 4, 2027" crosses the year end).'
13
+ ja: 「2026年4月1日〜2026年3月31日」のように、期間の終わりの日付が始まりより前だと、読み手はどちらの日付が正しいのかを問い合わせることになります。年を書き写し間違えたときや、日付を片方だけ直したときに起きます。二つの日付の間が範囲の記号(〜、–)だけのときと、「から」でつなぎ終わりの日付のあとに「まで」が続くときと、一つの文に始まりの語と終わりの語が一つずつあり二つの日付に付くとき(2026年4月1日から開始し、2027年3月31日に終了する)を期間として比べます。年の無い期間(12月28日〜1月4日)は年をまたぐかもしれないので比べません。年を片方にだけ書いた期間(2026年7月9日〜7月8日)は同じ年と読み、逆になるときは、年の無い側の曜日がその年に合うか、翌年(または前年)にまたぐと読むと一年近くになるときだけ言います(2026年12月28日〜1月4日は年をまたぐ期間です)。
14
+ en: 'When a period such as "5 April 2026 – 2 April 2026" ends before it starts, the reader has to ask which date is right. It happens when a year is copied wrong, or when only one of the two dates is changed. Two dates are read as a period when only a range mark (–, —) or a word such as "through" or "until" stands between them, or when they are written "from … to …", or when one sentence gives the start and the end with one start word and one end word on the two dates ("starts on 1 April 2026 and ends on 31 March 2027"). A period without a year ("28 December – 4 January") may run into the next year and is not compared. With the year on one end only ("July 9 – July 8, 2026"), both ends are read in that year; a reversed one is reported only when the weekday on the end without a year matches that reading, or when reading it across the year end would make the period nearly a year long ("December 28 – January 4, 2027" crosses the year end).'
15
15
 
16
16
  message:
17
17
  ja: 期間「{period}」の終わりが始まりより前です
@@ -44,5 +44,5 @@ example:
44
44
  Applications are open 1 April 2026 – 31 March 2027.
45
45
 
46
46
  not_flagged:
47
- ja: 年の無い期間(12月28日〜1月4日)、「から」だけで「まで」の無いもの(10日から3日に変更)、日付の間に言葉のあるものは比べません。
48
- en: 'A period without a year, "from … to …" in a sentence with a word that changes a date ("moved from March 10 to March 3", "… was postponed"), and two dates with words between them are not compared.'
47
+ ja: 年の無い期間(12月28日〜1月4日)、「から」だけで「まで」の無いもの(10日から3日に変更)、日付の間に言葉のあるもの、始まりの語か終わりの語が二つある文は比べません。
48
+ en: 'A period without a year, "from … to …" in a sentence with a word that changes a date ("moved from March 10 to March 3", "… was postponed"), two dates with words between them, and a sentence with two start words or two end words ("starts on … and phase 2 starts on …") are not compared.'
@@ -0,0 +1,67 @@
1
+ id: formula-amount-mismatch
2
+ layer: L3
3
+ status: stable
4
+ how_to_find: formula-amount
5
+ # The words of a formula come from formula-amount-word and percent-unit; a tax-included word and a stated tax rate are read
6
+ # with tax-price-column, as tax-mismatch reads them, and a tax's name with a word of inclusion (消費税8%込み) with tax-label
7
+ # and tax-included-word. The amounts are currency-notation's. A document profile may give the
8
+ # tax rate a label means when the document states none (tax_rate).
9
+ extra_word_lists: [formula-amount-word, percent-unit, tax-price-column, tax-label, tax-included-word]
10
+ severity: warning
11
+
12
+ name:
13
+ ja: 見出しに書いた計算と合わない金額
14
+ en: An amount that is not what its label's formula gives
15
+
16
+ why:
17
+ ja: 精算書や請求書で「仲介手数料(売買代金の3%+60,000円、消費税込み)」のように見出しに計算のしかたを書くと、読み手は金額を確かめずに払います。売買代金を直して手数料を直し忘れたとき、決まった額や税を足し忘れたとき、桁を打ち間違えたときに食い違います。見出しに「名前の率%」(売買代金の3%)か「率% of 名前」(5% of the sale price)があり、行の終わりに金額が一つあるとき、文書がその名前の見出しで書いた金額(売買代金 | 48,000,000円)に率を掛け、「+」の後の額を足して比べます。見出しが税込みと言えば、見出しの税率、無ければ文書が「消費税10%」のように書いた税率、それも無ければ文書の種類の税率を足します。
18
+ en: 'When a statement writes how a fee is worked out in its label ("Commission (5% of the sale price)"), readers pay it without checking. It goes wrong when the price is changed and the fee is not, when a fixed part or the tax is left out, or when a digit is mistyped. When a label holds "rate% of name" (or "name × rate%") and its line ends in one amount, the rate is applied to the amount the document labels with that name (Sale price | $324,000.00), a fixed amount after "plus" is added, and the result is compared with the line''s amount. A label saying tax is included adds the rate it states, else the one rate the document states beside a tax word.'
19
+
20
+ message:
21
+ ja: 「{written}」が、{name}「{base}」で「{formula}」を計算した額({expected})と合いません
22
+ en: '{written} is not "{formula}" worked out on the {name} {base} ({expected})'
23
+
24
+ how_to_fix:
25
+ ja: 元の金額と率、足す額を確かめて、金額か見出しの計算を直してください。
26
+ en: Check the amount it is worked out from, the rate and any fixed part, and correct the amount or the label.
27
+
28
+ levels: { strict: error, normal: warning, relaxed: info }
29
+
30
+ use_for: [business, technical, blog, legal, docs, academic, literature, speech]
31
+ off_for:
32
+ literature: "Length, rhythm, repetition and register are the author's choices in literature, and dialogue mixes styles. Only what one work can hold itself to runs: consistent spacing and commas, and numbering without gaps."
33
+
34
+ group: facts
35
+ summary:
36
+ ja: 「売買代金の3%」のような見出しの計算と合わない金額
37
+ en: An amount that is not what its label's formula ("5% of the sale price") gives
38
+ example:
39
+ ja:
40
+ before: |
41
+ | 項目 | 金額 |
42
+ | --- | --- |
43
+ | 売買代金 | 30,000,000円 |
44
+ | 仲介手数料(売買代金の3%+60,000円、消費税10%込み) | 990,000円 |
45
+ after: |
46
+ | 項目 | 金額 |
47
+ | --- | --- |
48
+ | 売買代金 | 30,000,000円 |
49
+ | 仲介手数料(売買代金の3%+60,000円、消費税10%込み) | 1,056,000円 |
50
+ en:
51
+ before: |
52
+ | Item | Amount |
53
+ | --- | --- |
54
+ | Sale price | $250,000.00 |
55
+ | Commission (5% of the sale price) | $12,000.00 |
56
+ after: |
57
+ | Item | Amount |
58
+ | --- | --- |
59
+ | Sale price | $250,000.00 |
60
+ | Commission (5% of the sale price) | $12,500.00 |
61
+
62
+ not_flagged:
63
+ ja: 端数は切り捨て・四捨五入・切り上げのどれでも、税の前でも後でも、円やドルの位で合っていると読みます。その名前の見出しの金額が無いもの、名前の見出しが二つあって額が違うもの、見出しに率と名前の組が二つあるもの、見出しに足す額のほかの金額があるもの、通貨が違うもの、行の金額が「165万円」のように桁の語で書いたものは比べません。税込みと書いて、見出しにも文書にも文書の種類にも税率が無いもの、文書が違う税率を二つ書いたものも比べません。文の中に書いた計算は読みません。
64
+ en: "Rounding down, to the nearest or up, before or after the tax, to the cent or to the unit, is read as right. Nothing is compared when no line labels the named amount, when two such lines give different amounts, when the label holds two rates with names or an amount other than the fixed part, when the currencies differ, or when the line's amount is written with a word of scale. A label saying tax is included is not compared when no rate is stated in it, in the document or in the document's profile, or when the document states two rates. A formula written in a sentence is not read."
65
+ level_meaning:
66
+ ja: 金額が、見出しに書いた計算で出した額と合わないとき指摘
67
+ en: reported when an amount is not what its label's formula gives
@@ -12,13 +12,18 @@ name:
12
12
  en: A total of payments that is not the payments added up
13
13
 
14
14
  why:
15
- ja: ローンや分割払いの案内で「お支払回数 60回、毎月のお支払額 32,800円、お支払総額 1,968,000円」と並べて書くと、読み手は掛け算を確かめずに総額を信じます。金利や回数を変えて作り直したときに、毎回の額か総額のどちらかが古いまま残って食い違います。支払回数・毎回のお支払額・お支払総額の見出しにそれぞれ一つの額があるとき、回数×毎回の額をお支払総額と比べます。初回や最終回の額が別に書いてあれば「初回 + 毎回の額 ×(残りの回数)+ 最終回」で比べます。見出しが無くても、一つの文に「32,800円×59回」のような回数と額の組があれば、それを足して比べます。毎回の額は、書いた桁の半分までの丸めが回数分たまることを認めます。
16
- en: 'When a loan or installment plan says "48 monthly payments of $537.50; total of payments $25,800", readers trust the total without multiplying. It goes wrong when the plan is redone with another rate or term and the payment or the total is left as it was. When the number of payments, the regular payment and the total of payments are each stated once, the number times the payment is compared with the total; a first or final payment stated apart is added as written (first + payment × the rest + final). Without labels, the payments of one sentence ("47 payments of $550.53 and 1 payment of $550.71") are added up. Each regular payment may be rounded by half its last written digit.'
15
+ ja: ローンや分割払いの案内で「お支払回数 60回、毎月のお支払額 32,800円、お支払総額 1,968,000円」と並べて書くと、読み手は掛け算を確かめずに総額を信じます。金利や回数を変えて作り直したときに、毎回の額か総額のどちらかが古いまま残って食い違います。支払回数・毎回のお支払額・お支払総額の見出しにそれぞれ一つの額があるとき、回数×毎回の額をお支払総額と比べます。初回や最終回の額が別に書いてあれば「初回 + 毎回の額 ×(残りの回数)+ 最終回」で比べます。見出しが無くても、一つの文に「32,800円×59回」のような回数と額の組があれば、それを足して比べます。毎回の額は、書いた桁の半分までの丸めが回数分たまることを認めます。修学旅行や林間学校の案内のように「第1回:32,000円」「第2回:30,000円」と分納を表の行や箇条書きで並べ、同じ節に「旅行代金は、1人あたり62,000円」と費用を書いたときは、分納の額を足して費用と比べます。
16
+ en: 'When a loan or installment plan says "48 monthly payments of $537.50; total of payments $25,800", readers trust the total without multiplying. It goes wrong when the plan is redone with another rate or term and the payment or the total is left as it was. When the number of payments, the regular payment and the total of payments are each stated once, the number times the payment is compared with the total; a first or final payment stated apart is added as written (first + payment × the rest + final). Without labels, the payments of one sentence ("47 payments of $550.53 and 1 payment of $550.71") are added up. Each regular payment may be rounded by half its last written digit. When a school trip or camp notice lists numbered instalments as table rows or list items ("1st payment: £150", "Final payment: $60") and states the cost in the same section ("The trip costs £300 per student"), the instalments are added up and compared with the cost.'
17
17
 
18
18
  message:
19
19
  ja: 支払額を足すと {payments} = {sum} で、お支払総額 {total} と合いません
20
20
  en: "The payments add up to {sum} ({payments}), not the total of payments {total}"
21
21
 
22
+ messages:
23
+ fee:
24
+ ja: 分納の額を足すと {payments} = {sum} で、同じ節に書いた費用 {total} と合いません
25
+ en: "The instalments add up to {sum} ({payments}), not the cost {total} stated with them"
26
+
22
27
  how_to_fix:
23
28
  ja: 回数・毎回の額・総額のどれが正しいかを確かめて、ほかを直してください。端数を初回や最終回で調整するなら、その回の額を書いてください。
24
29
  en: Check which of the number of payments, the payment and the total is right, and correct the others. If a first or final payment absorbs the rounding, state its amount.
@@ -54,5 +59,5 @@ example:
54
59
  - Total of payments: $2,496.00
55
60
 
56
61
  not_flagged:
57
- ja: 総額の書いていない文書、「約」「程度」の付いた額、「32,800円〜33,000円」のような幅、同じ見出しに違う値が二度ある(いくつかの支払い方を並べた)文書、回数と額の組が文ごとに違う文書、一つの文に「12回×10,000円または24回×6,000円」のように繰り返す支払いが二つある(選べる支払い方を並べた)記載は比べません。ボーナス払い・頭金・残価・据置のある文書と、「端数は初回で調整」のように端数の扱いを書いて初回・最終回の額を書いていない文書も比べません。通貨の違う額は比べません。表の列見出しだけに「回数」「総額」とある表は読みません。
58
- en: 'A document with no total of payments, an amount marked "about" or "up to", a range ("$540 to $560"), a label stated twice with different values (plans side by side), sentences of payments that differ from one another, and a sentence with two repeated payments ("12 payments of $100 or 24 payments of $60", a choice) are not compared. Neither is a document with a bonus, balloon, down payment, deposit or deferred payment, nor one that says a payment absorbs the rounding ("the final payment may vary") without stating that payment. Amounts in different currencies are not compared. A table that names the count and the total only in its column headings is not read.'
62
+ ja: 総額の書いていない文書、「約」「程度」の付いた額、「32,800円〜33,000円」のような幅、同じ見出しに違う値が二度ある(いくつかの支払い方を並べた)文書、回数と額の組が文ごとに違う文書、一つの文に「12回×10,000円または24回×6,000円」のように繰り返す支払いが二つある(選べる支払い方を並べた)記載は比べません。ボーナス払い・頭金・残価・据置のある文書と、「端数は初回で調整」のように端数の扱いを書いて初回・最終回の額を書いていない文書も比べません。通貨の違う額は比べません。表の列見出しだけに「回数」「総額」とある表は読みません。分納の並びは、費用が節に一つだけ書いてあり、第1回から番号が抜けも重なりもなく続き、どの回も額が一つだけ読め、節に積立金・分納などの語があるときだけ比べます。申込金は分納の一つとして並べたときだけ足し、ほかの所に書いてあれば比べません。
63
+ en: 'A document with no total of payments, an amount marked "about" or "up to", a range ("$540 to $560"), a label stated twice with different values (plans side by side), sentences of payments that differ from one another, and a sentence with two repeated payments ("12 payments of $100 or 24 payments of $60", a choice) are not compared. Neither is a document with a bonus, balloon, down payment, deposit or deferred payment, nor one that says a payment absorbs the rounding ("the final payment may vary") without stating that payment. Amounts in different currencies are not compared. A table that names the count and the total only in its column headings is not read. Numbered instalments are compared only when their section states one cost, they run from the first with no gap or repeat, each has one amount, and the section speaks of payments or instalments. A deposit is added only when listed as one of the instalments; named anywhere else, nothing is compared.'
@@ -4,7 +4,7 @@ status: stable
4
4
  how_to_find: orthographic-variant
5
5
  # 語の一覧は持たない。文書の中の語を、日本語は辞書の形の読み(出来る・できる はどちらもデキル)で、カタカナ語は小さい字と「ー」を
6
6
  # ならした形で、英字は大文字小文字とハイフンを外した形で束ね、同じ語の書き方が二つあれば少ないほうを指す。
7
- # 読みが同じで意味の違う語(成る・なる)は lang-ja の orthographic-variant-distinct に並べて外す。
7
+ # 読みが同じで意味の違う語(成る・なる)は lang-ja の orthographic-variant-distinct に並べて外す。genres を付けた語はそのジャンルだけで外す。
8
8
  uses: [pos]
9
9
  severity: info
10
10
 
@@ -56,8 +56,8 @@ example:
56
56
  Send the form by email. We answer every email within a day. Attach the receipt to the email.
57
57
 
58
58
  not_flagged:
59
- ja: 読みが同じでも漢字の違う語(書く・描く)、補助動詞・形式名詞と本来の使い方の書き分け(見て下さい・資料を下さい)、書き分けが意味の違いになる語(成る・なる、言う・いう)、語末の「ー」だけの違い(katakana-long-vowel が見ます)、鉤括弧や引用符の中、語の直後のふりがなの括弧の中(HTTP(えいちてぃーてぃーぴー))、chaff.yaml の names に書いた名前は見ません。名詞に続けて書いたかなの名詞(全員ぶん)と慣用句の動詞(気をつける)は、同じ読みの語(文、名札を付ける)と別に比べます。英字は、頭の字の大文字小文字(Email と email)と、大文字だけの語(強調や略語)、大文字の名前に語尾が付いた語(関数名の SENDs)、同じ文の略語の字を示した語(CLIPER と並ぶ CLImatology)を書き方の違いにしません(HBase のような名前の大文字は比べます)。少ないほうが三分の一を超えるときも言いません。文学のジャンルでは既定で止めています(その時代の書き方のため)。
60
- en: The case of a word's first letter (Email, email; Long-Term, long-term) is not a spelling, nor is a word in capitals only (emphasis or an acronym), nor a name in capitals with an ending (SENDs, OPENed), nor the letters of an acronym picked out beside it (CLImatology next to CLIPER). A name spelled with capitals (HBase) is still compared. Hyphenated words that are another word (re-sign, resign), part of an address (github.com), anything in quotation marks, a Japanese reading in brackets right after a word (HTTP(えいちてぃーてぃーぴー)) and the team's names are not compared. Nothing is reported when the minority is more than a third. The literature preset turns it off, since old spelling belongs to its time.
59
+ ja: 読みが同じでも漢字の違う語(書く・描く)、補助動詞・形式名詞と本来の使い方の書き分け(見て下さい・資料を下さい)、書き分けが意味の違いになる語(成る・なる、言う・いう。法務のジャンルでは、法令の書き方で書き分ける 時・とき と 物・もの も)、語末の「ー」だけの違い(katakana-long-vowel が見ます)、鉤括弧や引用符の中、語の直後のふりがなの括弧の中(HTTP(えいちてぃーてぃーぴー))、chaff.yaml の names に書いた名前は見ません。名詞に続けて書いたかなの名詞(全員ぶん)と慣用句の動詞(気をつける)は、同じ読みの語(文、名札を付ける)と別に比べます。英字は、頭の字の大文字小文字(Email と email)と、大文字だけの語(強調や略語)、大文字の名前に語尾が付いた語(関数名の SENDs)、同じ文の略語の字を示した語(CLIPER と並ぶ CLImatology)を書き方の違いにしません(HBase のような名前の大文字は比べます)。少ないほうが三分の一を超えるときも言いません。文学のジャンルでは既定で止めています(その時代の書き方のため)。
60
+ en: The case of a word's first letter (Email, email; Long-Term, long-term) is not a spelling, nor is a word in capitals only (emphasis or an acronym), nor a name in capitals with an ending (SENDs, OPENed), nor the letters of an acronym picked out beside it (CLImatology next to CLIPER). A name spelled with capitals (HBase) is still compared. In Japanese, words whose kanji and kana forms mean different things (成る and なる) are not compared, and in the legal genres neither are 時 and とき or 物 and もの, which legal drafting writes apart. Hyphenated words that are another word (re-sign, resign), part of an address (github.com), anything in quotation marks, a Japanese reading in brackets right after a word (HTTP(えいちてぃーてぃーぴー)) and the team's names are not compared. Nothing is reported when the minority is more than a third. The literature preset turns it off, since old spelling belongs to its time.
61
61
  level_meaning:
62
62
  ja: 少ないほうが語の {limit}% 以下のとき指摘
63
63
  en: reported when the minority spelling is {limit}% of the word's uses or less
package/src/cli-text.ts CHANGED
@@ -67,7 +67,7 @@ const USAGE_JA = `chaff — 文章の読みにくいところを見つけます
67
67
  chaff feedback <file> --rule <rule> [--line N] | --missed --line N
68
68
  誤った指摘・見逃しの報告の下書きを作る(何も送らない。--with-config で chaff.yaml 全体も載せる)
69
69
 
70
- --compact エンジニア向けの 1 行形式
70
+ --compact エンジニア向けの短い形式。指摘 1 件を 2 行で出す(位置・重さ・説明、その下に rule の id)
71
71
  --no-guide ジャンルを決めたときに最初に出す、そのジャンルの指針を出さない
72
72
  --experimental 試験中の rule も動かす
73
73
  --genre <ジャンル> この実行だけジャンルを決める(chaff.yaml より優先)
@@ -126,7 +126,7 @@ const USAGE_EN = `chaff — finds what makes writing hard to read. It never rewr
126
126
  chaff feedback <file> --rule <rule> [--line N] | --missed --line N
127
127
  draft a report of a wrong or missed finding (sends nothing; --with-config adds all of chaff.yaml)
128
128
 
129
- --compact one line per finding, for engineers
129
+ --compact a short form for engineers: two lines per finding (position, severity, message; the rule id below)
130
130
  --no-guide leave out the genre's guide, which comes first when the genre is set
131
131
  --experimental run the experimental rules too
132
132
  --genre <genre> the genre for this run only (wins over chaff.yaml)
package/src/cross-run.ts CHANGED
@@ -40,6 +40,7 @@ const optionsFor = (input: CrossInput, rule: RuleDefinition): DetectorOptions =>
40
40
  lexicon: rule.word_list === undefined ? undefined : input.doc.lexicons[rule.word_list],
41
41
  where: rule.where,
42
42
  ...(rule.options === undefined ? {} : { settings: optionValues(settleOptions(rule.id, rule.options, input.context.optionLayers ?? [])) }),
43
+ genre: input.context.genre,
43
44
  });
44
45
 
45
46
  /** What one rule did to the documents taking part (by path): its findings, or why it did not run after all. */
@@ -1,4 +1,5 @@
1
1
  import type { Detector, Finding, Lexicon, Token } from "../plugin.ts";
2
+ import { isPartOfAddress } from "./address-word.ts";
2
3
  import { isPartOfLongerWord } from "./doubled-word.ts";
3
4
 
4
5
  /**
@@ -130,6 +131,8 @@ const numberSlipAt = (source: string, tokens: readonly Token[], at: number, list
130
131
  const nounAt = at + determiner.length + modifiers.length;
131
132
  const noun = tokens[nounAt];
132
133
  if (!isCommonNoun(noun) || !hasOneNumber(noun, lists.invariant) || isPlural(noun) === determiner.plural) return undefined;
134
+ // A file name or a path (a quotes.json, a docs/ folder) is spelled as it must be, not counted.
135
+ if (isPartOfAddress(source, noun.span.start, noun.span.end)) return undefined;
133
136
  if (mayBeVerb(determiner, modifiers, noun, tokens[nounAt + 1], lists.finite)) return undefined;
134
137
  const span = tokens.slice(at, nounAt + 1);
135
138
  if (!endsNounPhrase(tokens[nounAt + 1], lists.finite) || !joinedBySpace(source, span) || isPartOfLongerWord(source, determiner.head, noun)) return undefined;
@@ -0,0 +1,59 @@
1
+ // formula-amount-mismatch: the reading half. The words of a formula come from the lexicon formula-amount-word (name-rate,
2
+ // rate-name, plus, not-included) and percent-unit; the tax words are tax-price-column's, as tax-mismatch reads them, with tax-label and
3
+ // tax-included-word for a tax named as included (消費税8%込み). The amounts are
4
+ // currency-notation's, read with amount-multiplier's words of scale. The profile may give the tax rate a document leaves out.
5
+ import type { Detector, Finding, ProseDocument } from "../plugin.ts";
6
+ import { formulaAmountMismatches, type FormulaWords } from "../structure/formula-amount.ts";
7
+ import type { StatedAmount } from "../structure/stated-multiple.ts";
8
+ import type { TaxPriceRole, TaxPriceWord } from "../structure/tax-price.ts";
9
+ import { scaledAmountOf, scaleWordsOf } from "./amount-scale.ts";
10
+ import { amountsIn, formsOf } from "./currency-notation.ts";
11
+ import { proseAndTablesOf } from "../table-text.ts";
12
+ import { quoteAround } from "./quote-around.ts";
13
+
14
+ const TAX_ROLES: readonly string[] = ["excluded", "included", "rate"] satisfies readonly TaxPriceRole[];
15
+ const isTaxRole = (group: string | undefined): group is TaxPriceRole => group !== undefined && TAX_ROLES.includes(group);
16
+
17
+ const patternsOf = (doc: ProseDocument, lexicon: string, group?: string): string[] =>
18
+ (doc.lexicons[lexicon] ?? []).filter((entry) => group === undefined || entry.group === group).map((entry) => entry.pattern);
19
+
20
+ const taxWordsOf = (doc: ProseDocument): TaxPriceWord[] =>
21
+ (doc.lexicons["tax-price-column"] ?? []).flatMap((entry) => (isTaxRole(entry.group) ? [{ pattern: entry.pattern, role: entry.group }] : []));
22
+
23
+ const wordsOf = (doc: ProseDocument): FormulaWords => ({
24
+ nameRate: patternsOf(doc, "formula-amount-word", "name-rate"),
25
+ rateName: patternsOf(doc, "formula-amount-word", "rate-name"),
26
+ plus: patternsOf(doc, "formula-amount-word", "plus"),
27
+ percent: patternsOf(doc, "percent-unit"),
28
+ tax: taxWordsOf(doc),
29
+ taxNames: patternsOf(doc, "tax-label"),
30
+ includedWords: patternsOf(doc, "tax-included-word"),
31
+ notIncluded: patternsOf(doc, "formula-amount-word", "not-included"),
32
+ });
33
+
34
+ const amountsOf = (text: string, doc: ProseDocument): StatedAmount[] => {
35
+ const scales = scaleWordsOf(doc);
36
+ return amountsIn(
37
+ text,
38
+ formsOf(doc),
39
+ scales.map((scale) => scale.word),
40
+ ).flatMap((amount) => {
41
+ const read = scaledAmountOf(text, amount, scales);
42
+ return read === undefined
43
+ ? []
44
+ : [{ offset: read.offset, end: amount.offset + amount.written.length, currency: read.currency, value: read.value, scale: read.unit }];
45
+ });
46
+ };
47
+
48
+ export const formulaAmount: Detector = (doc): Finding[] => {
49
+ const text = proseAndTablesOf(doc);
50
+ const input = { text, amounts: amountsOf(text, doc), defaultTaxRate: doc.profile?.taxRate };
51
+ return formulaAmountMismatches(input, wordsOf(doc)).map((issue) => ({
52
+ rule: "formula-amount-mismatch",
53
+ severity: "warning",
54
+ line: 0,
55
+ column: 0,
56
+ quote: quoteAround(text, issue.offset),
57
+ values: { ...issue.values, offset: issue.offset },
58
+ }));
59
+ };
@@ -3,9 +3,11 @@
3
3
  // labels are installment-label, the other words installment-word, approximate-marker, number-word (digit), range-connector
4
4
  // and amount-range-word.
5
5
  import type { Detector, Finding, Lexicon, ProseDocument } from "../plugin.ts";
6
+ import { installmentFeeMismatches, type InstallmentSeriesWords } from "../structure/installment-fee.ts";
6
7
  import { installmentTotalMismatches, type InstallmentAmount, type InstallmentWords, type PositionedWord } from "../structure/installment-total.ts";
7
8
  import { scaledAmountOf, scaleWordsOf } from "./amount-scale.ts";
8
9
  import { amountsIn, formsOf } from "./currency-notation.ts";
10
+ import type { StructureIssue } from "../structure/issues.ts";
9
11
  import { proseAndTablesOf } from "../table-text.ts";
10
12
  import { quoteAround } from "./quote-around.ts";
11
13
 
@@ -41,6 +43,20 @@ const wordsOf = (doc: ProseDocument): InstallmentWords => ({
41
43
  numberWords: numberWordsOf(doc),
42
44
  });
43
45
 
46
+ const weightedOf = (doc: ProseDocument, lexicon: string, group: string): InstallmentSeriesWords["ordinals"] =>
47
+ entriesOf(doc, lexicon, group).flatMap((entry) => (entry.weight === undefined ? [] : [{ word: entry.pattern, value: entry.weight }]));
48
+
49
+ const seriesWordsOf = (doc: ProseDocument): InstallmentSeriesWords => ({
50
+ numbered: patternsOf(doc, "installment-label", "numbered"),
51
+ finals: patternsOf(doc, "installment-label", "final"),
52
+ deposits: patternsOf(doc, "installment-label", "deposit"),
53
+ ordinals: [...weightedOf(doc, "installment-word", "ordinal"), ...numberWordsOf(doc)],
54
+ ordinalSuffixes: [...new Set(patternsOf(doc, "ordinal-suffix"))],
55
+ payments: patternsOf(doc, "installment-word", "payment"),
56
+ fees: patternsOf(doc, "installment-word", "fee"),
57
+ feeLinks: patternsOf(doc, "installment-word", "fee-link"),
58
+ });
59
+
44
60
  const amountsOf = (text: string, doc: ProseDocument): InstallmentAmount[] => {
45
61
  const scales = scaleWordsOf(doc);
46
62
  return amountsIn(
@@ -55,14 +71,24 @@ const amountsOf = (text: string, doc: ProseDocument): InstallmentAmount[] => {
55
71
  });
56
72
  };
57
73
 
74
+ const findingOf = (text: string, issue: StructureIssue, variant?: string): Finding => ({
75
+ rule: "installment-total-mismatch",
76
+ severity: "warning",
77
+ line: 0,
78
+ column: 0,
79
+ quote: quoteAround(text, issue.offset),
80
+ values: { ...issue.values, offset: issue.offset },
81
+ ...(variant === undefined ? {} : { variant }),
82
+ });
83
+
58
84
  export const installmentTotalMismatch: Detector = (doc): Finding[] => {
59
85
  const text = proseAndTablesOf(doc);
60
- return installmentTotalMismatches(text, amountsOf(text, doc), wordsOf(doc)).map((issue) => ({
61
- rule: "installment-total-mismatch",
62
- severity: "warning",
63
- line: 0,
64
- column: 0,
65
- quote: quoteAround(text, issue.offset),
66
- values: { ...issue.values, offset: issue.offset },
67
- }));
86
+ const amounts = amountsOf(text, doc);
87
+ const words = wordsOf(doc);
88
+ const loans = installmentTotalMismatches(text, amounts, words);
89
+ const headings = (doc.markup?.headings ?? []).map((heading) => heading.start);
90
+ const fees = installmentFeeMismatches(text, amounts, headings, words, seriesWordsOf(doc)).filter(
91
+ (issue) => !loans.some((loan) => loan.offset === issue.offset),
92
+ );
93
+ return [...loans.map((issue) => findingOf(text, issue)), ...fees.map((issue) => findingOf(text, issue, "fee"))];
68
94
  };
@@ -8,6 +8,7 @@ import { furiganaSpans } from "../furigana.ts";
8
8
  import { acronymsIn, isCapitalsNotSpelling } from "../capitals-with-small.ts";
9
9
  import { isPartOfAddress } from "./address-word.ts";
10
10
  import { wholeWordRuns, type WholeWordRun } from "../whole-word-runs.ts";
11
+ import { entriesForGenre } from "../lexicon-genres.ts";
11
12
 
12
13
  /**
13
14
  * A word found in the document: where it is, which sentence it is in, its key and spelling, how it is written when that differs, and
@@ -41,7 +42,8 @@ const IDIOM = "orthographic-variant-idiom";
41
42
  /** Lexicon: words the tokenizer cuts in one spelling and not the other (お客様, お客 and さま). Each is read whole, keyed by its group. */
42
43
  const WHOLE_WORD = "orthographic-variant-whole-word";
43
44
 
44
- const patternsOf = (doc: ProseDocument, id: string): string[] => (doc.lexicons[id] ?? []).map((entry) => entry.pattern);
45
+ const patternsOf = (doc: ProseDocument, id: string, genre: string | undefined): string[] =>
46
+ entriesForGenre(doc.lexicons[id] ?? [], genre).map((entry) => entry.pattern);
45
47
 
46
48
  /**
47
49
  * Whether a stretch of the sentence is outside a quotation, the team's names and a reading in brackets (HTTP(えいちてぃーてぃーぴー)):
@@ -300,8 +302,8 @@ const cellPlace = (cell: TableCell, names: readonly string[]): Place => {
300
302
  */
301
303
  export const orthographicVariant: Detector = (doc, options): Finding[] => {
302
304
  const skips: Skips = {
303
- skip: new Set([...patternsOf(doc, DISTINCT), ...patternsOf(doc, FUKUSHI)]),
304
- idioms: new Set(patternsOf(doc, IDIOM)),
305
+ skip: new Set([...patternsOf(doc, DISTINCT, options.genre), ...patternsOf(doc, FUKUSHI, options.genre)]),
306
+ idioms: new Set(patternsOf(doc, IDIOM, options.genre)),
305
307
  wholeWords: new Map((doc.lexicons[WHOLE_WORD] ?? []).map((entry) => [entry.pattern, entry.group ?? entry.pattern])),
306
308
  };
307
309
  const names = doc.names ?? [];
@@ -0,0 +1,4 @@
1
+ import type { Detector } from "../../plugin.ts";
2
+ import { formulaAmount } from "../formula-amount.ts";
3
+
4
+ export const detector: Detector = formulaAmount;
@@ -14,6 +14,7 @@ import { withCompoundHours } from "../structure/compound-hours.ts";
14
14
  import { timeLengths } from "../derived/time-lengths.ts";
15
15
  import { measureUnitsOf } from "../facts/measure-units.ts";
16
16
  import { rangeFrameOf, reversedRanges, type DatedSpan, type RangeWords } from "../structure/date-range.ts";
17
+ import { isStartEndRange, startEndWordsOf } from "../structure/start-end-range.ts";
17
18
  import { afterLabel } from "../structure/stated-period.ts";
18
19
  import { afterSpanLabel } from "../structure/span-label.ts";
19
20
  import { percentSumMismatches, type ShareWords } from "../structure/percent-sum.ts";
@@ -310,11 +311,12 @@ const labelledAt =
310
311
  return after !== undefined && offset >= lineStart + after;
311
312
  };
312
313
 
313
- /** 期間の終わりが始まりより前。範囲の記号と語は言語パッケージの語彙表(range-connector、range-opener、range-closer)から取る。 */
314
+ /** 期間の終わりが始まりより前。範囲の記号と語は言語パッケージの語彙表(range-connector、range-opener、range-closer、range-start-end)から取る。 */
314
315
  export const dateRangeReversed: Detector = (doc): Finding[] => {
315
316
  if (doc.structure === undefined) return [];
316
317
  const dates = datedSpans(doc.structure);
317
- return reversedRanges(doc.source, dates, rangeWordsOf(doc), labelledAt(doc))
318
+ const startEnd = startEndWordsOf(doc.lexicons["range-start-end"] ?? [], patternsOf(doc, "weekday"));
319
+ return reversedRanges(doc.source, dates, rangeWordsOf(doc), labelledAt(doc), (start, end) => isStartEndRange(doc.source, start, end, startEnd))
318
320
  .filter((issue) => {
319
321
  const start = dateAt(dates, issue.offset);
320
322
  return !quotedDates(doc, dates[start], dates[start + 1]);
package/src/eval.ts CHANGED
@@ -46,12 +46,12 @@ const candidates = (rule: RuleDefinition, current: number): number[] => {
46
46
  return [...new Set([...fromLevels, ...scaled, current])].toSorted((left, right) => left - right);
47
47
  };
48
48
 
49
- const countAt = (docs: readonly ProseDocument[], rule: RuleDefinition, limit: number, settings: DetectorOptions["settings"]): Point => {
49
+ const countAt = (docs: readonly ProseDocument[], rule: RuleDefinition, limit: number, settings: DetectorOptions["settings"], genre: string): Point => {
50
50
  const detector = DETECTORS[rule.how_to_find];
51
51
  if (detector === undefined) return { limit, findings: 0, documents: 0, per10k: 0 };
52
52
  const perDoc = docs.map((doc) => {
53
53
  const lexicon = rule.word_list === undefined ? undefined : doc.lexicons[rule.word_list];
54
- return detector(doc, { limit, lexicon, where: rule.where, settings }).length;
54
+ return detector(doc, { limit, lexicon, where: rule.where, settings, genre }).length;
55
55
  });
56
56
  const findings = perDoc.reduce((sum, count) => sum + count, 0);
57
57
  const chars = docs.reduce((sum, doc) => sum + doc.sentences.reduce((inner, sentence) => inner + charLength(sentence), 0), 0);
@@ -111,7 +111,7 @@ export const evaluate = (
111
111
  .map((rule) => {
112
112
  const current = limits[rule.id] ?? rule.levels.normal ?? 1;
113
113
  const settings = rule.options === undefined ? undefined : optionValues(settleOptions(rule.id, rule.options, optionLayers));
114
- const sweep = candidates(rule, current).map((limit) => countAt(docs, rule, limit, settings));
114
+ const sweep = candidates(rule, current).map((limit) => countAt(docs, rule, limit, settings, genre));
115
115
  return {
116
116
  rule: rule.id,
117
117
  name: rule.name[language] ?? rule.id,