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.
Files changed (198) hide show
  1. package/dist/derived/session-hours.d.ts +26 -0
  2. package/dist/derived/session-hours.d.ts.map +1 -0
  3. package/dist/derived/session-hours.js +29 -0
  4. package/dist/derived/session-hours.js.map +1 -0
  5. package/dist/derived/time-lengths.d.ts +37 -0
  6. package/dist/derived/time-lengths.d.ts.map +1 -0
  7. package/dist/derived/time-lengths.js +168 -0
  8. package/dist/derived/time-lengths.js.map +1 -0
  9. package/dist/derived/unit-counts.d.ts +7 -0
  10. package/dist/derived/unit-counts.d.ts.map +1 -0
  11. package/dist/derived/unit-counts.js +13 -0
  12. package/dist/derived/unit-counts.js.map +1 -0
  13. package/dist/derived/working-hours.d.ts +36 -0
  14. package/dist/derived/working-hours.d.ts.map +1 -0
  15. package/dist/derived/working-hours.js +55 -0
  16. package/dist/derived/working-hours.js.map +1 -0
  17. package/dist/detectors/acronym-expansion.d.ts +1 -1
  18. package/dist/detectors/acronym-expansion.d.ts.map +1 -1
  19. package/dist/detectors/acronym-expansion.js +46 -2
  20. package/dist/detectors/acronym-expansion.js.map +1 -1
  21. package/dist/detectors/capitalised-term.d.ts +44 -0
  22. package/dist/detectors/capitalised-term.d.ts.map +1 -0
  23. package/dist/detectors/capitalised-term.js +101 -0
  24. package/dist/detectors/capitalised-term.js.map +1 -0
  25. package/dist/detectors/date-outside-period.d.ts.map +1 -1
  26. package/dist/detectors/date-outside-period.js +4 -1
  27. package/dist/detectors/date-outside-period.js.map +1 -1
  28. package/dist/detectors/derived-numbers.d.ts +2 -0
  29. package/dist/detectors/derived-numbers.d.ts.map +1 -1
  30. package/dist/detectors/derived-numbers.js +72 -18
  31. package/dist/detectors/derived-numbers.js.map +1 -1
  32. package/dist/detectors/name-variant.d.ts.map +1 -1
  33. package/dist/detectors/name-variant.js +57 -10
  34. package/dist/detectors/name-variant.js.map +1 -1
  35. package/dist/detectors/party-order.d.ts +3 -0
  36. package/dist/detectors/party-order.d.ts.map +1 -0
  37. package/dist/detectors/party-order.js +46 -0
  38. package/dist/detectors/party-order.js.map +1 -0
  39. package/dist/detectors/registry/connection-time-short.d.ts +3 -0
  40. package/dist/detectors/registry/connection-time-short.d.ts.map +1 -0
  41. package/dist/detectors/registry/connection-time-short.js +3 -0
  42. package/dist/detectors/registry/connection-time-short.js.map +1 -0
  43. package/dist/detectors/registry/duration-product-mismatch.d.ts +3 -0
  44. package/dist/detectors/registry/duration-product-mismatch.d.ts.map +1 -0
  45. package/dist/detectors/registry/duration-product-mismatch.js +3 -0
  46. package/dist/detectors/registry/duration-product-mismatch.js.map +1 -0
  47. package/dist/detectors/registry/party-order.d.ts +3 -0
  48. package/dist/detectors/registry/party-order.d.ts.map +1 -0
  49. package/dist/detectors/registry/party-order.js +3 -0
  50. package/dist/detectors/registry/party-order.js.map +1 -0
  51. package/dist/detectors/registry/requirement-lists.d.ts +3 -0
  52. package/dist/detectors/registry/requirement-lists.d.ts.map +1 -0
  53. package/dist/detectors/registry/requirement-lists.js +3 -0
  54. package/dist/detectors/registry/requirement-lists.js.map +1 -0
  55. package/dist/detectors/registry/return-type.d.ts +3 -0
  56. package/dist/detectors/registry/return-type.d.ts.map +1 -0
  57. package/dist/detectors/registry/return-type.js +3 -0
  58. package/dist/detectors/registry/return-type.js.map +1 -0
  59. package/dist/detectors/requirement-lists.d.ts +3 -0
  60. package/dist/detectors/requirement-lists.d.ts.map +1 -0
  61. package/dist/detectors/requirement-lists.js +46 -0
  62. package/dist/detectors/requirement-lists.js.map +1 -0
  63. package/dist/detectors/return-type.d.ts +3 -0
  64. package/dist/detectors/return-type.d.ts.map +1 -0
  65. package/dist/detectors/return-type.js +24 -0
  66. package/dist/detectors/return-type.js.map +1 -0
  67. package/dist/detectors/structure-tree.d.ts.map +1 -1
  68. package/dist/detectors/structure-tree.js +9 -1
  69. package/dist/detectors/structure-tree.js.map +1 -1
  70. package/dist/detectors/time-order.d.ts +1 -0
  71. package/dist/detectors/time-order.d.ts.map +1 -1
  72. package/dist/detectors/time-order.js +30 -4
  73. package/dist/detectors/time-order.js.map +1 -1
  74. package/dist/detectors/undefined-term.d.ts.map +1 -1
  75. package/dist/detectors/undefined-term.js +19 -3
  76. package/dist/detectors/undefined-term.js.map +1 -1
  77. package/dist/document.d.ts.map +1 -1
  78. package/dist/document.js +4 -1
  79. package/dist/document.js.map +1 -1
  80. package/dist/facts/table-facts.d.ts +2 -0
  81. package/dist/facts/table-facts.d.ts.map +1 -1
  82. package/dist/facts/table-facts.js +2 -0
  83. package/dist/facts/table-facts.js.map +1 -1
  84. package/dist/name-variants.d.ts +6 -1
  85. package/dist/name-variants.d.ts.map +1 -1
  86. package/dist/name-variants.js +68 -0
  87. package/dist/name-variants.js.map +1 -1
  88. package/dist/name-word-order.d.ts +49 -0
  89. package/dist/name-word-order.d.ts.map +1 -0
  90. package/dist/name-word-order.js +114 -0
  91. package/dist/name-word-order.js.map +1 -0
  92. package/dist/plugin.d.ts +7 -0
  93. package/dist/plugin.d.ts.map +1 -1
  94. package/dist/return-type.d.ts +33 -0
  95. package/dist/return-type.d.ts.map +1 -0
  96. package/dist/return-type.js +302 -0
  97. package/dist/return-type.js.map +1 -0
  98. package/dist/structure/connection-times.d.ts +56 -0
  99. package/dist/structure/connection-times.d.ts.map +1 -0
  100. package/dist/structure/connection-times.js +165 -0
  101. package/dist/structure/connection-times.js.map +1 -0
  102. package/dist/structure/date-order-tie.d.ts +13 -0
  103. package/dist/structure/date-order-tie.d.ts.map +1 -0
  104. package/dist/structure/date-order-tie.js +61 -0
  105. package/dist/structure/date-order-tie.js.map +1 -0
  106. package/dist/structure/date-order.d.ts +2 -2
  107. package/dist/structure/date-order.d.ts.map +1 -1
  108. package/dist/structure/date-order.js +26 -9
  109. package/dist/structure/date-order.js.map +1 -1
  110. package/dist/structure/date-outside-period.d.ts +6 -0
  111. package/dist/structure/date-outside-period.d.ts.map +1 -1
  112. package/dist/structure/date-outside-period.js +49 -3
  113. package/dist/structure/date-outside-period.js.map +1 -1
  114. package/dist/structure/date-range.d.ts +5 -2
  115. package/dist/structure/date-range.d.ts.map +1 -1
  116. package/dist/structure/date-range.js +37 -9
  117. package/dist/structure/date-range.js.map +1 -1
  118. package/dist/structure/leg-times.d.ts +2 -0
  119. package/dist/structure/leg-times.d.ts.map +1 -1
  120. package/dist/structure/leg-times.js +13 -0
  121. package/dist/structure/leg-times.js.map +1 -1
  122. package/dist/structure/party-order.d.ts +33 -0
  123. package/dist/structure/party-order.d.ts.map +1 -0
  124. package/dist/structure/party-order.js +79 -0
  125. package/dist/structure/party-order.js.map +1 -0
  126. package/dist/structure/requirement-lists.d.ts +37 -0
  127. package/dist/structure/requirement-lists.d.ts.map +1 -0
  128. package/dist/structure/requirement-lists.js +89 -0
  129. package/dist/structure/requirement-lists.js.map +1 -0
  130. package/dist/structure/time-order.d.ts +21 -0
  131. package/dist/structure/time-order.d.ts.map +1 -1
  132. package/dist/structure/time-order.js +5 -6
  133. package/dist/structure/time-order.js.map +1 -1
  134. package/dist/structure/version-order.d.ts +2 -0
  135. package/dist/structure/version-order.d.ts.map +1 -1
  136. package/dist/structure/version-order.js +10 -0
  137. package/dist/structure/version-order.js.map +1 -1
  138. package/dist/surname-near.d.ts +6 -0
  139. package/dist/surname-near.d.ts.map +1 -0
  140. package/dist/surname-near.js +31 -0
  141. package/dist/surname-near.js.map +1 -0
  142. package/dist/table-cells.d.ts +13 -0
  143. package/dist/table-cells.d.ts.map +1 -0
  144. package/dist/table-cells.js +36 -0
  145. package/dist/table-cells.js.map +1 -0
  146. package/dist/table-names.d.ts +42 -0
  147. package/dist/table-names.d.ts.map +1 -0
  148. package/dist/table-names.js +94 -0
  149. package/dist/table-names.js.map +1 -0
  150. package/package.json +3 -3
  151. package/rules/connection-time-short.yaml +52 -0
  152. package/rules/date-outside-period.yaml +4 -4
  153. package/rules/duration-mismatch.yaml +7 -4
  154. package/rules/duration-product-mismatch.yaml +73 -0
  155. package/rules/name-variant.yaml +8 -2
  156. package/rules/party-order.yaml +84 -0
  157. package/rules/required-listed-as-preferred.yaml +102 -0
  158. package/rules/return-type-mismatch.yaml +79 -0
  159. package/rules/undefined-term.yaml +21 -7
  160. package/src/derived/session-hours.ts +62 -0
  161. package/src/derived/time-lengths.ts +226 -0
  162. package/src/derived/unit-counts.ts +18 -0
  163. package/src/derived/working-hours.ts +97 -0
  164. package/src/detectors/acronym-expansion.ts +57 -2
  165. package/src/detectors/capitalised-term.ts +147 -0
  166. package/src/detectors/date-outside-period.ts +4 -1
  167. package/src/detectors/derived-numbers.ts +94 -21
  168. package/src/detectors/name-variant.ts +71 -13
  169. package/src/detectors/party-order.ts +50 -0
  170. package/src/detectors/registry/connection-time-short.ts +4 -0
  171. package/src/detectors/registry/duration-product-mismatch.ts +4 -0
  172. package/src/detectors/registry/party-order.ts +4 -0
  173. package/src/detectors/registry/requirement-lists.ts +4 -0
  174. package/src/detectors/registry/return-type.ts +4 -0
  175. package/src/detectors/requirement-lists.ts +65 -0
  176. package/src/detectors/return-type.ts +30 -0
  177. package/src/detectors/structure-tree.ts +12 -1
  178. package/src/detectors/time-order.ts +40 -5
  179. package/src/detectors/undefined-term.ts +20 -3
  180. package/src/document.ts +4 -1
  181. package/src/facts/table-facts.ts +3 -0
  182. package/src/name-variants.ts +76 -1
  183. package/src/name-word-order.ts +149 -0
  184. package/src/plugin.ts +5 -0
  185. package/src/return-type.ts +361 -0
  186. package/src/structure/connection-times.ts +226 -0
  187. package/src/structure/date-order-tie.ts +86 -0
  188. package/src/structure/date-order.ts +41 -14
  189. package/src/structure/date-outside-period.ts +72 -7
  190. package/src/structure/date-range.ts +45 -9
  191. package/src/structure/leg-times.ts +15 -0
  192. package/src/structure/party-order.ts +115 -0
  193. package/src/structure/requirement-lists.ts +145 -0
  194. package/src/structure/time-order.ts +8 -7
  195. package/src/structure/version-order.ts +10 -0
  196. package/src/surname-near.ts +31 -0
  197. package/src/table-cells.ts +41 -0
  198. package/src/table-names.ts +113 -0
@@ -0,0 +1,97 @@
1
+ import type { Span } from "../plugin.ts";
2
+ import { dayShiftOf, SECONDS_PER_DAY, withoutDayShiftBefore, type Mark, type TimeSpan, type TimeWords } from "../structure/time-marks.ts";
3
+ import { labelOf, mentionsIn, readScopes, within, type TimeLength } from "./time-lengths.ts";
4
+
5
+ /**
6
+ * Working hours: the stated total (実働8時間, 8 hours a day) against the end minus the start minus the break
7
+ * (9:00〜17:30、休憩1時間 is 7.5 hours). Read in one line, or in a list whose items say it together, holding exactly one
8
+ * range of two times, one break with its length and one total, and no other length. A range across midnight is read
9
+ * only when the end carries a next-day mark (22:00〜翌7:00). Pure.
10
+ */
11
+ export type WorkingHoursWords = {
12
+ /** What stands alone between the two times of a range (〜, to, から). */
13
+ readonly joiners: readonly string[];
14
+ readonly dayShifts: readonly Mark[];
15
+ readonly breaks: readonly Mark[];
16
+ readonly totals: readonly Mark[];
17
+ readonly approximate: readonly Mark[];
18
+ };
19
+
20
+ export type WorkingHoursMismatch = {
21
+ readonly start: Span;
22
+ readonly end: Span;
23
+ readonly break: Span;
24
+ readonly total: Span;
25
+ /** The total the times and the break give, in minutes. */
26
+ readonly expected: number;
27
+ };
28
+
29
+ type ClockRange = Span & { readonly from: Span; readonly to: Span; readonly minutes: number | undefined };
30
+
31
+ const SECONDS_PER_MINUTE = 60;
32
+
33
+ const keyOf = (text: string): string => text.normalize("NFKC").trim().toLowerCase().replace(/\s+/gu, " ");
34
+
35
+ const timeWordsOf = (words: WorkingHoursWords): TimeWords => ({ legs: [], columns: [], dayShifts: words.dayShifts, zones: [] });
36
+
37
+ /** The minutes from one time to the other, the end a day later when it carries a next-day mark. */
38
+ const minutesOf = (text: string, from: TimeSpan, to: TimeSpan, words: TimeWords): number | undefined => {
39
+ const shift = dayShiftOf(text.slice(from.end, to.start), text, to.end, words);
40
+ const seconds = to.seconds + Math.max(0, shift) * SECONDS_PER_DAY - from.seconds;
41
+ return seconds > 0 ? seconds / SECONDS_PER_MINUTE : undefined;
42
+ };
43
+
44
+ /** Two times written one after the other with only a joiner between them (9:00〜17:30, 9 a.m. to 5:30 p.m.). */
45
+ export const clockRanges = (text: string, times: readonly TimeSpan[], words: WorkingHoursWords): ClockRange[] => {
46
+ const joiners = new Set(words.joiners.map(keyOf));
47
+ const timeWords = timeWordsOf(words);
48
+ return times.flatMap((from, index): ClockRange[] => {
49
+ const to = times[index + 1];
50
+ if (to === undefined || !joiners.has(keyOf(withoutDayShiftBefore(text.slice(from.end, to.start), timeWords)))) return [];
51
+ return [{ start: from.start, end: to.end, from, to, minutes: minutesOf(text, from, to, timeWords) }];
52
+ });
53
+ };
54
+
55
+ const isApproximate = (text: string, length: Span, words: WorkingHoursWords): boolean => labelOf(text, length, words.approximate) !== undefined;
56
+
57
+ type Labeled = { readonly length: TimeLength; readonly label: Span };
58
+
59
+ const labeled = (text: string, lengths: readonly TimeLength[], marks: readonly Mark[]): Labeled[] =>
60
+ lengths.flatMap((length) => {
61
+ const label = labelOf(text, length, marks);
62
+ return label === undefined ? [] : [{ length, label }];
63
+ });
64
+
65
+ /** The one statement in scope, or undefined when the scope does not hold exactly one. */
66
+ const statementIn = (text: string, scope: Span, ranges: readonly ClockRange[], lengths: readonly TimeLength[], words: WorkingHoursWords) => {
67
+ const inRanges = ranges.filter((range) => within(range, scope));
68
+ const inLengths = lengths.filter((length) => within(length, scope));
69
+ const breaks = labeled(text, inLengths, words.breaks);
70
+ const totals = labeled(text, inLengths, words.totals);
71
+ const [range, breakLength, total] = [inRanges[0], breaks[0], totals[0]];
72
+ if (inRanges.length !== 1 || breaks.length !== 1 || totals.length !== 1 || inLengths.length !== 2) return undefined;
73
+ if (range?.minutes === undefined || breakLength === undefined || total === undefined || breakLength.length === total.length) return undefined;
74
+ const label = breakLength.label;
75
+ const unexplained = mentionsIn(text, scope, words.breaks).some((mention) => mention.end <= label.start || mention.start >= label.end);
76
+ if (unexplained || isApproximate(text, breakLength.length, words) || isApproximate(text, total.length, words)) return undefined;
77
+ return { range: { ...range, minutes: range.minutes }, breakLength: breakLength.length, total: total.length };
78
+ };
79
+
80
+ /** Working hours whose stated total is not the end minus the start minus the break. */
81
+ export const workingHoursMismatches = (
82
+ text: string,
83
+ source: string,
84
+ sentences: readonly Span[],
85
+ times: readonly TimeSpan[],
86
+ lengths: readonly TimeLength[],
87
+ words: WorkingHoursWords,
88
+ ): WorkingHoursMismatch[] => {
89
+ const ranges = clockRanges(text, times, words);
90
+ return readScopes(text, source, { sentences, times }, (scope) => statementIn(text, scope, ranges, lengths, words)).flatMap(
91
+ ({ range, breakLength, total }) => {
92
+ const expected = range.minutes - breakLength.minutes;
93
+ if (expected <= 0 || expected === total.minutes) return [];
94
+ return [{ start: range.from, end: range.to, break: breakLength, total, expected }];
95
+ },
96
+ );
97
+ };
@@ -292,10 +292,65 @@ const proseLines = (lines: readonly string[]): string[] => {
292
292
  return withoutFences(lines.map((line, index) => (index < skipped || INDENTED.test(line) ? "" : line)));
293
293
  };
294
294
 
295
- /** 用語集の見出し語として、すぐ後ろに名前を書いた略語。 */
295
+ /**
296
+ * 同じ書き方の見出し語が、名前で確かめられてこれだけある文書は、その書き方の用語集。用語集では、見出し語の後ろの段落はどれも定義なので、
297
+ * 名前の文字が揃わない項目(ADVIS: In hydrologic terms, a program…、AMVER: Automated Mutual Assistance Vessel Rescue System)も
298
+ * 説明済みと読む。書き方ごとに数えるので、名前を添えた箇条書きが並ぶ文書でも、略語だけの行(PDF、### MATLAB)は定義の見出しと読まない。
299
+ */
300
+ const GLOSSARY_ENTRIES = 5;
301
+
302
+ /**
303
+ * 見出し語の行。略語 1 つか、/ で並べた略語(AMVER/SEAS)。前に 1 文字の語(A AMS)を置いてもよい。空白で並べた大文字の語
304
+ * (SEE ALSO、XYZ QRS)は見出しや略語の並びで、見出し語ではない。
305
+ */
306
+ const HEADWORD_WORD = String.raw`[A-Z][A-Z&-]*[A-Z]`;
307
+ const HEADWORD_LINE = new RegExp(String.raw`^(?:#{1,6}\s+)?(?:[A-Z]\s)?(?<words>${HEADWORD_WORD}(?:/${HEADWORD_WORD})*)\s*(?<colon>[::])?(?:\s+#+)?$`, "u");
308
+ const HEADING_LINE = /^#{1,6}\s/u;
309
+
310
+ /** 定義の段落。小文字の語を含み(次の見出し語ではなく)、見出しでもない行。 */
311
+ const isDefinitionLine = (line: string): boolean => /\p{Ll}/u.test(line) && !HEADING_LINE.test(line);
312
+
313
+ /** 用語集の、見出し語だけの行と、その次の空でない行が定義の段落のもの。 */
314
+ const glossaryLineTerms = (lines: readonly string[], index: number): string[] => {
315
+ const line = (lines[index] ?? "").trim();
316
+ const groups = HEADWORD_LINE.exec(line.replaceAll(STRONG, ""))?.groups;
317
+ if (groups?.["words"] === undefined || (groups["colon"] !== undefined && !MARKED_LINE.test(line))) return [];
318
+ const next = lines.slice(index + 1, index + 1 + NEXT_LINE_REACH).find((candidate) => candidate.trim() !== "");
319
+ return next !== undefined && isDefinitionLine(next.trim()) ? groups["words"].split("/") : [];
320
+ };
321
+
322
+ /** 用語集の、印を付けた見出し語と区切りの後ろの定義(**ADVIS**: In hydrologic terms…、- ADVIS — a program…)。 */
323
+ const glossaryLeadTerm = (line: string): string[] => {
324
+ const lead = TERM_LEAD.exec(line.trim());
325
+ const term = lead?.groups?.["strongTerm"] ?? lead?.groups?.["term"];
326
+ return term !== undefined && /\p{Ll}/u.test(lead?.groups?.["rest"] ?? "") ? [term] : [];
327
+ };
328
+
329
+ type EntryForm = {
330
+ readonly named: (lines: readonly string[], index: number) => string[];
331
+ readonly defined: (lines: readonly string[], index: number) => string[];
332
+ };
333
+
334
+ /** 見出し語の書き方。行だけの見出し語と、印を付けた見出し語の後ろの名前。表は隣のセルの名前だけで読む。 */
335
+ const ENTRY_FORMS: readonly EntryForm[] = [
336
+ { named: (lines, index) => (TABLE_ROW.test(lines[index] ?? "") ? [] : lineTerm(lines, index)), defined: glossaryLineTerms },
337
+ {
338
+ named: (lines, index) => (TABLE_ROW.test(lines[index] ?? "") ? [] : leadTerm(lines[index] ?? "")),
339
+ defined: (lines, index) => glossaryLeadTerm(lines[index] ?? ""),
340
+ },
341
+ { named: (lines, index) => (TABLE_ROW.test(lines[index] ?? "") ? tableTerms(lines[index] ?? "") : []), defined: () => [] },
342
+ ];
343
+
344
+ /** 一つの書き方で見出し語と読む略語。その書き方の用語集なら、名前でない定義を書いたものも。 */
345
+ const formTerms = (lines: readonly string[], form: EntryForm): string[] => {
346
+ const named = lines.flatMap((_, index) => form.named(lines, index));
347
+ return new Set(named).size < GLOSSARY_ENTRIES ? named : [...named, ...lines.flatMap((_, index) => form.defined(lines, index))];
348
+ };
349
+
350
+ /** 用語集の見出し語として、すぐ後ろに名前を書いた略語。用語集と読める書き方では、名前でなくても定義を書いた略語。 */
296
351
  export const termEntryAcronyms = (source: string): ReadonlySet<string> => {
297
352
  const lines = proseLines(source.split(/\r?\n/u));
298
- return new Set(lines.flatMap((line, index) => (TABLE_ROW.test(line) ? tableTerms(line) : [...lineTerm(lines, index), ...leadTerm(line)])));
353
+ return new Set(ENTRY_FORMS.flatMap((form) => formTerms(lines, form)));
299
354
  };
300
355
 
301
356
  export type ExpandedAt = (body: string, acronym: string, at: number) => boolean;
@@ -0,0 +1,147 @@
1
+ // A term written like the document's defined terms, capitalised inside a sentence (its Affiliates, disclose Proprietary
2
+ // Data), that the document defines nowhere. Read only where the document defines its terms with a defining word
3
+ // (MIN_DEFINED of them: "X" means, (the "X")). A capitalised run is also the name of a law, a body, a place, a party or a
4
+ // part of the document, so every name the language package lists is left, and so is every run set apart by markup (a
5
+ // heading, a link, bold, an HTML element, a table row, a list item's label). Pure.
6
+ import { escapeRegExp } from "../orthography.ts";
7
+ import type { Span, Token } from "../plugin.ts";
8
+
9
+ export type CapitalisedTerm = { readonly offset: number; readonly term: string };
10
+
11
+ /** The word lists the decision reads, matched as written. */
12
+ export type CapitalisedWords = {
13
+ /** A run holding one of these is a name (Act, Policy, Inc, California, Monday, Supplier), not a term. */
14
+ readonly names: readonly string[];
15
+ /** A run opening with one of these points at a part of the document (Section 4, Schedule A). */
16
+ readonly divisions: readonly string[];
17
+ /** Small words inside one name (Statement of Work, Trials and Betas). */
18
+ readonly joiners: readonly string[];
19
+ };
20
+
21
+ /** A term the document defines; bare is a definition with no defining word (("GPC")), which names more than it defines. */
22
+ export type Defined = { readonly term: string; readonly bare?: boolean };
23
+
24
+ export type CapitalisedInput = {
25
+ readonly source: string;
26
+ /** The text a reader sees (the source without link destinations): where a lower-case use counts. */
27
+ readonly readable: string;
28
+ readonly sentences: readonly (readonly Token[])[];
29
+ readonly defined: readonly Defined[];
30
+ /** Words set in quotes or bold anywhere ("Account" represents …, **Security Measures**): named on purpose. */
31
+ readonly named: readonly string[];
32
+ /** Spans set apart by markup: headings, links, bold, HTML elements, table rows, list items' labels. */
33
+ readonly marked: readonly Span[];
34
+ readonly words: CapitalisedWords;
35
+ };
36
+
37
+ /** Fewer definitions than this, and capitals are the document's style, not its way of naming terms. */
38
+ const MIN_DEFINED = 2;
39
+ const CAPITALISED = /^\p{Lu}\p{Ll}+(?:['’]\p{Ll}+)?$/u;
40
+ const ACRONYM = /^\p{Lu}{2,}$/u;
41
+ const DEFINED_FORM = /^\p{Lu}/u;
42
+ const LOWER_WORD = /^\p{Ll}+$/u;
43
+ const HYPHEN = "-";
44
+
45
+ const isCapitalised = (token: Token | undefined): boolean => token !== undefined && CAPITALISED.test(token.surface);
46
+ /** A word that carries a run on once it has started: a capitalised word or an acronym (Commercial AI System). */
47
+ const continues = (token: Token | undefined): boolean => isCapitalised(token) || (token !== undefined && ACRONYM.test(token.surface));
48
+ const adjacent = (left: Token, right: Token): boolean => right.span.start - left.span.end <= 1;
49
+ const touching = (left: Token, right: Token): boolean => right.span.start === left.span.end;
50
+
51
+ /** The forms one term is written in: lower case, its last word as written and singular (Policies, Policy; Viruses, Virus). */
52
+ const formsOf = (term: string): string[] => {
53
+ const words = term.toLowerCase().split(/[\s-]+/u);
54
+ const head = words.slice(0, -1).join(" ");
55
+ const last = words.at(-1) ?? "";
56
+ const lasts = [last, last.replace(/ies$/u, "y"), last.replace(/es$/u, ""), last.replace(/(?<!s)s$/u, "")];
57
+ return lasts.map((form) => (head === "" ? form : `${head} ${form}`));
58
+ };
59
+
60
+ /** How many tokens the capitalised run starting at index takes: words that continue it, and a joiner or hyphen between two. */
61
+ const runLength = (tokens: readonly Token[], index: number, joiners: ReadonlySet<string>): number => {
62
+ const word = tokens[index];
63
+ const next = tokens[index + 1];
64
+ const after = tokens[index + 2];
65
+ if (next === undefined || word === undefined || !adjacent(word, next)) return 1;
66
+ if (continues(next)) return 1 + runLength(tokens, index + 1, joiners);
67
+ const hyphened = next.surface === HYPHEN && touching(word, next) && after !== undefined && touching(next, after);
68
+ const joined = hyphened || (joiners.has(next.surface) && after !== undefined && adjacent(next, after));
69
+ return joined && continues(after) ? 2 + runLength(tokens, index + 2, joiners) : 1;
70
+ };
71
+
72
+ type Run = { readonly start: number; readonly end: number };
73
+
74
+ /** The capitalised runs of one sentence, as [start, end) token indexes; a run never starts inside the one before. */
75
+ const runsOf = (tokens: readonly Token[], joiners: ReadonlySet<string>): Run[] =>
76
+ tokens.reduce<Run[]>((runs, token, index) => {
77
+ if ((runs.at(-1)?.end ?? 0) > index || !isCapitalised(token)) return runs;
78
+ return [...runs, { start: index, end: index + runLength(tokens, index, joiners) }];
79
+ }, []);
80
+
81
+ const inside = (offset: number, spans: readonly Span[]): boolean => spans.some((span) => span.start <= offset && offset < span.end);
82
+
83
+ /** Used like a defined term: after a lower-case word, and not a modifier of a lower-case noun after it (the Premium plan). */
84
+ const placedLikeATerm = (before: Token | undefined, after: Token | undefined): boolean =>
85
+ before !== undefined && LOWER_WORD.test(before.surface) && !(after !== undefined && after.pos === "NOUN" && LOWER_WORD.test(after.surface));
86
+
87
+ /** Every run of whole words in a term (GitHub Service: GitHub, Service, GitHub Service). */
88
+ const partsOf = (term: string): string[] => {
89
+ const words = term.split(/\s+/u);
90
+ return words.flatMap((_, start) => words.slice(start).map((__, length) => words.slice(start, start + length + 1).join(" ")));
91
+ };
92
+
93
+ /** The text also writes the term in lower case (your account, account settings): an ordinary word, or a screen's label. */
94
+ const writtenInLowerCase = (text: string, term: string): boolean => {
95
+ const words = term
96
+ .toLowerCase()
97
+ .split(/[\s-]+/u)
98
+ .map(escapeRegExp)
99
+ .join(String.raw`[\s-]+`);
100
+ return new RegExp(String.raw`(?<!\p{L})${words}(?:e?s)?(?!\p{L})`, "u").test(text);
101
+ };
102
+
103
+ /**
104
+ * The first use of each capitalised run used inside a sentence like a defined term that names nothing the language
105
+ * package lists, is not set apart by markup, is never written in lower case, and neither is nor holds a defined or named
106
+ * term in any case.
107
+ */
108
+ export const capitalisedTerms = (input: CapitalisedInput): CapitalisedTerm[] => {
109
+ const defining = input.defined.filter((definition) => definition.bare !== true && DEFINED_FORM.test(definition.term));
110
+ if (defining.length < MIN_DEFINED) return [];
111
+ const joiners = new Set(input.words.joiners);
112
+ const divisions = new Set(input.words.divisions.flatMap(formsOf));
113
+ const known = new Set([...input.defined.map((definition) => definition.term), ...input.named, ...input.words.names].flatMap(formsOf));
114
+ const isKnown = (term: string): boolean => partsOf(term).some((part) => formsOf(part).some((form) => known.has(form)));
115
+ const uses = input.sentences.flatMap((tokens) =>
116
+ runsOf(tokens, joiners).flatMap(({ start, end }): CapitalisedTerm[] => {
117
+ const first = tokens[start];
118
+ const last = tokens[end - 1];
119
+ if (first === undefined || last === undefined || !placedLikeATerm(tokens[start - 1], tokens[end])) return [];
120
+ if (inside(first.span.start, input.marked) || formsOf(first.surface).some((form) => divisions.has(form))) return [];
121
+ const term = input.source.slice(first.span.start, last.span.end);
122
+ return isKnown(term) || writtenInLowerCase(input.readable, term) ? [] : [{ offset: first.span.start, term }];
123
+ }),
124
+ );
125
+ return uses.filter((use, index) => uses.findIndex((other) => formsOf(other.term).some((form) => formsOf(use.term).includes(form))) === index);
126
+ };
127
+
128
+ const BOLD = /\*\*([^*\n]+)\*\*|__([^_\n]+)__/gu;
129
+ const QUOTED = /["“]([^"“”\n]{1,80}?)[,.]?["”]/gu;
130
+ const HTML_ELEMENT = /<([a-z][a-z0-9]*)\b[^>]*>[^<]*<\/\1>/giu;
131
+ const TABLE_ROW = /^[ \t]*\|.*$/gmu;
132
+ const ITEM_LABEL = /^[ \t]*(?:[*+-]|\d+[.)])[ \t]+[^:\n]{1,80}:/gmu;
133
+
134
+ const spansOf = (source: string, pattern: RegExp): Span[] =>
135
+ [...source.matchAll(pattern)].map((match) => ({ start: match.index, end: match.index + match[0].length }));
136
+
137
+ /** Bold, HTML elements, table rows and list items' labels in a Markdown source: labels and cells, not running text. */
138
+ export const setApartSpans = (source: string): Span[] => [BOLD, HTML_ELEMENT, TABLE_ROW, ITEM_LABEL].flatMap((pattern) => spansOf(source, pattern));
139
+
140
+ const LINK_DESTINATION = /\]\([^)\n]*\)|<?https?:\/\/[^\s)>]+>?/gu;
141
+
142
+ /** The source with link destinations and URLs blanked: the words a reader sees. */
143
+ export const readableText = (source: string): string => source.replaceAll(LINK_DESTINATION, (destination) => " ".repeat(destination.length));
144
+
145
+ /** The words set in quotes or bold anywhere in a source, without the emphasis marks. */
146
+ export const namedWords = (source: string): string[] =>
147
+ [BOLD, QUOTED].flatMap((pattern) => [...source.matchAll(pattern)].map((match) => (match[1] ?? match[2] ?? "").replaceAll(/[*_]/gu, "").trim()));
@@ -6,10 +6,13 @@ import { quoteAt } from "./structure-tree.ts";
6
6
 
7
7
  const patternsOf = (doc: ProseDocument, lexicon: string): string[] => (doc.lexicons[lexicon] ?? []).map((entry) => entry.pattern);
8
8
 
9
- /** 期間の語、期間の外に置く語、範囲の記号(range-connector、range-opener、range-frame の間の語)、月と曜日の名は語彙表から取る。 */
9
+ /** 期間の語、期間の外に置く語、文書の仕事の期間の語、締め切りの語、概要の節の見出しの語、範囲の記号(range-connector、range-opener、range-frame の間の語)、月と曜日の名は語彙表から取る。 */
10
10
  const wordsOf = (doc: ProseDocument): OutsideWords => ({
11
11
  labels: patternsOf(doc, "period-label"),
12
+ terms: patternsOf(doc, "period-term-label"),
12
13
  asides: patternsOf(doc, "period-aside"),
14
+ deadlines: patternsOf(doc, "period-deadline"),
15
+ overviews: patternsOf(doc, "period-overview-heading"),
13
16
  connectors: [
14
17
  ...patternsOf(doc, "range-connector"),
15
18
  ...patternsOf(doc, "range-opener"),
@@ -15,7 +15,12 @@ import {
15
15
  type NightsMismatch,
16
16
  type StayColumnWords,
17
17
  } from "../derived/stays.ts";
18
- import { escapeRegExp } from "../orthography.ts";
18
+ import { numeralCounts } from "../derived/unit-counts.ts";
19
+ import { timeLengths, type LengthWords, type TimeLength } from "../derived/time-lengths.ts";
20
+ import { workingHoursMismatches, type WorkingHoursMismatch } from "../derived/working-hours.ts";
21
+ import { sessionHoursMismatches } from "../derived/session-hours.ts";
22
+ import { clockTimes } from "../compare/clock-time.ts";
23
+ import { secondsOf, type Mark } from "../structure/time-marks.ts";
19
24
  import { proseAndTablesOf } from "../table-text.ts";
20
25
  import { quoteAt } from "./structure-tree.ts";
21
26
 
@@ -93,29 +98,18 @@ const durationsOf = (doc: ProseDocument, quantities: readonly Quantity[]): Durat
93
98
 
94
99
  const sentencesOf = (doc: ProseDocument): Span[] => doc.sentences.map((sentence) => sentence.span);
95
100
 
96
- const LATIN_UNIT = /^[a-z]+$/iu;
97
-
98
- /** 数字と泊数の単位(3泊、3 nights)。英字の単位は語の切れ目まで。 */
99
- const digitNights = (text: string, units: readonly string[]): Count[] =>
100
- units.flatMap((unit) => {
101
- const tail = LATIN_UNIT.test(unit) ? "(?![\\p{L}\\p{N}])" : "";
102
- const pattern = new RegExp(`(?<![\\p{N}.,])(\\p{Nd}+)\\s?${escapeRegExp(unit)}${tail}`, "giu");
103
- return [...text.matchAll(pattern)].map((match) => ({
104
- start: match.index,
105
- end: match.index + match[0].length,
106
- amount: Number((match[1] ?? "").normalize("NFKC")),
107
- }));
108
- });
109
-
110
101
  /** 泊数(3泊、3 nights、three nights)。text は地の文と表(リンク先や code は読まない)。目安の泊数(最大3泊)は除く。 */
111
- const nightsOf = (doc: ProseDocument, text: string): Count[] => {
112
- const units = patternsOf(doc, "stay-night");
113
- const digits = digitNights(text, units);
114
- const taken = spanIndex(digits);
102
+ /** 数と単位(3泊、15 sessions、three nights)。 */
103
+ const unitCountsOf = (doc: ProseDocument, text: string, units: readonly string[]): Count[] => {
104
+ const numerals = numeralCounts(text, units);
105
+ const taken = spanIndex(numerals);
115
106
  const worded = numberWordCounts(text, patternsOf(doc, "count-number"), units).filter((count) => !overlapsAny(taken, count));
116
- return [...digits, ...worded].filter((count) => !isApproximate(doc, count)).toSorted((left, right) => left.start - right.start);
107
+ return [...numerals, ...worded].toSorted((left, right) => left.start - right.start);
117
108
  };
118
109
 
110
+ const nightsOf = (doc: ProseDocument, text: string): Count[] =>
111
+ unitCountsOf(doc, text, patternsOf(doc, "stay-night")).filter((count) => !isApproximate(doc, count));
112
+
119
113
  const finding = (doc: ProseDocument, offset: number, values: Record<string, string | number>, variant?: string): Finding => ({
120
114
  rule: "duration-mismatch",
121
115
  severity: "warning",
@@ -143,6 +137,57 @@ const stayColumnWords = (doc: ProseDocument): StayColumnWords => ({
143
137
  units: patternsOf(doc, "stay-night"),
144
138
  });
145
139
 
140
+ const marksOf = (doc: ProseDocument, id: string): Mark[] =>
141
+ (doc.lexicons[id] ?? []).map((entry) => ({ pattern: entry.pattern, position: entry.position, group: entry.group }));
142
+
143
+ const SECONDS_PER_HOUR = 3600;
144
+ const SECONDS_PER_MINUTE = 60;
145
+
146
+ /** The hour and minute units of unit-time, told apart by their weight in seconds. */
147
+ const unitsWeighing = (doc: ProseDocument, seconds: number): string[] =>
148
+ (doc.lexicons["unit-time"] ?? []).filter((entry) => entry.weight === seconds).map((entry) => entry.pattern);
149
+
150
+ const lengthWords = (doc: ProseDocument): LengthWords => ({
151
+ hourUnits: unitsWeighing(doc, SECONDS_PER_HOUR),
152
+ minuteUnits: unitsWeighing(doc, SECONDS_PER_MINUTE),
153
+ halves: patternsOf(doc, "length-half"),
154
+ numberWords: patternsOf(doc, "count-number"),
155
+ });
156
+
157
+ const lengthsOf = (doc: ProseDocument, text: string): TimeLength[] => timeLengths(text, lengthWords(doc));
158
+
159
+ const MINUTES_PER_HOUR = 60;
160
+ const HOUR_DECIMALS = 100;
161
+
162
+ const hoursOf = (minutes: number): number => Math.round((minutes / MINUTES_PER_HOUR) * HOUR_DECIMALS) / HOUR_DECIMALS;
163
+
164
+ const workingHoursFinding = (doc: ProseDocument, mismatch: WorkingHoursMismatch): Finding =>
165
+ finding(
166
+ doc,
167
+ mismatch.total.start,
168
+ {
169
+ start: written(doc, mismatch.start),
170
+ end: written(doc, mismatch.end),
171
+ break: written(doc, mismatch.break),
172
+ total: written(doc, mismatch.total),
173
+ expected: hoursOf(mismatch.expected),
174
+ },
175
+ "working-hours",
176
+ );
177
+
178
+ /** 始業から終業まで、休憩を除いた時間が、書いた実働と合わない。 */
179
+ const workingHoursOf = (doc: ProseDocument, text: string): Finding[] => {
180
+ const times = clockTimes(text).map((time) => ({ start: time.start, end: time.end, seconds: secondsOf(time.key) }));
181
+ const words = {
182
+ joiners: patternsOf(doc, "clock-range-joiner"),
183
+ dayShifts: marksOf(doc, "day-shift-mark"),
184
+ breaks: marksOf(doc, "working-hours-break"),
185
+ totals: marksOf(doc, "working-hours-total"),
186
+ approximate: marksOf(doc, "approximate-marker"),
187
+ };
188
+ return workingHoursMismatches(text, doc.source, sentencesOf(doc), times, lengthsOf(doc, text), words).map((mismatch) => workingHoursFinding(doc, mismatch));
189
+ };
190
+
146
191
  /** 始まり + 期間 ≠ 終わり。泊数が二つの日付の差と合わない。「2泊3日」の日数が泊数より一つ多くない。 */
147
192
  export const durationMismatch: Detector = (doc): Finding[] => {
148
193
  if (doc.structure === undefined) return [];
@@ -173,7 +218,35 @@ export const durationMismatch: Detector = (doc): Finding[] => {
173
218
  const nightsDays = nightsDaysMismatches(pairs).map((mismatch) =>
174
219
  finding(doc, mismatch.days.start, { nights: written(doc, mismatch.nights), days: written(doc, mismatch.days), expected: mismatch.expected }, "nights-days"),
175
220
  );
176
- return [...lengths, ...stays.map((mismatch) => nightsFinding(doc, mismatch)), ...nightsDays];
221
+ return [...lengths, ...stays.map((mismatch) => nightsFinding(doc, mismatch)), ...nightsDays, ...workingHoursOf(doc, proseAndTables)];
222
+ };
223
+
224
+ /** 番号の書き方(ordinal-frame)の頭の語のすぐ後ろの数。「第15回」は15番目の回で、15回ではない。 */
225
+ const isOrdinal = (doc: ProseDocument, text: string, count: Span): boolean =>
226
+ patternsOf(doc, "ordinal-frame").some((frame) => {
227
+ const head = frame.split("{n}")[0] ?? "";
228
+ return head !== "" && text.slice(Math.max(0, count.start - head.length), count.start).toLowerCase() === head.toLowerCase();
229
+ });
230
+
231
+ /** 回数 × 1回の長さが、書いた合計と合わない(90分×15回(計24時間))。 */
232
+ export const durationProductMismatch: Detector = (doc): Finding[] => {
233
+ const text = proseAndTablesOf(doc);
234
+ const counts = unitCountsOf(doc, text, patternsOf(doc, "session-count-unit")).filter((count) => !isOrdinal(doc, text, count));
235
+ const words = { totals: marksOf(doc, "session-total"), approximate: marksOf(doc, "approximate-marker") };
236
+ return sessionHoursMismatches(text, doc.source, sentencesOf(doc), counts, lengthsOf(doc, text), words).map((mismatch) => ({
237
+ rule: "duration-product-mismatch",
238
+ severity: "warning",
239
+ line: 0,
240
+ column: 0,
241
+ quote: quoteAt(doc.source, mismatch.total.start),
242
+ values: {
243
+ count: written(doc, mismatch.count),
244
+ length: written(doc, mismatch.length),
245
+ total: written(doc, mismatch.total),
246
+ expected: hoursOf(mismatch.expected),
247
+ offset: mismatch.total.start,
248
+ },
249
+ }));
177
250
  };
178
251
 
179
252
  /** 四桁の年。木が日付と読まなかった英語の年(founded in 2015、born 1980)もここで読む。 */
@@ -1,10 +1,15 @@
1
1
  import type { Detector, Finding, ProseDocument } from "../plugin.ts";
2
- import { cuedNamesIn, mentionsIn, nameVariants, suffixedNamesIn, type NameMention, type VariantChars } from "../name-variants.ts";
2
+ import { cuedNamesIn, mentionsIn, nameVariants, suffixedNamesIn, withKnownNeighbours, type NameMention, type VariantChars } from "../name-variants.ts";
3
3
  import { nameCueAt, type NameCues } from "../name-cue.ts";
4
4
  import { quoteAt } from "./structure-tree.ts";
5
5
  import { companyMentionsIn, companyVariants, type CompanyForm, type IsProper } from "../company-names.ts";
6
+ import { proseAndTablesOf } from "../table-text.ts";
7
+ import { tableBodyCells } from "../facts/table-facts.ts";
8
+ import { cellNamesIn, cellNameVariants, proseNamesOf } from "../table-names.ts";
6
9
  import { placeMentionsIn, placeVariants, type PlaceReader, type PlaceWord } from "../place-names.ts";
7
- import type { Token } from "../plugin.ts";
10
+ import type { TableCell, Token } from "../plugin.ts";
11
+ import { proseWithCells } from "../table-cells.ts";
12
+ import { labelledSpans, orderNamesOf, quotedSpans, stemOf, titleCaseSpans, wordOrderVariants, type OrderWord } from "../name-word-order.ts";
8
13
 
9
14
  // 人の名前と読ませる敬称(様、さん)は語彙表 person-suffix、人を指す前置き(担当の)は person-lead、名前のすぐ後ろに来る語
10
15
  // (です、まで)は name-particle、字体の違う同じ字(斎・斉・齋)は name-variant-char が組(group)ごとに言う。
@@ -31,7 +36,14 @@ const placeWordsOf = (doc: ProseDocument): PlaceWord[] =>
31
36
  /** 名前の頭に立たない語の品詞(The、at、and)。 */
32
37
  const FUNCTION_POS: ReadonlySet<string> = new Set(["DET", "ADP", "PRON", "CCONJ", "SCONJ", "AUX", "PART"]);
33
38
 
34
- const tokensOf = (doc: ProseDocument): Token[] => doc.sentences.flatMap((sentence) => sentence.tokens ?? []);
39
+ /** 表の本体の升。本文が表を覆わない文書(Markdown でない)には無い。 */
40
+ const cellsOf = (doc: ProseDocument): readonly TableCell[] => doc.tableCells?.() ?? tableBodyCells(proseAndTablesOf(doc));
41
+
42
+ /** 文の語と表の升の語を、文書の順に。 */
43
+ const tokensOf = (doc: ProseDocument): Token[] =>
44
+ [...doc.sentences.flatMap((sentence) => sentence.tokens ?? []), ...cellsOf(doc).flatMap((cell) => cell.tokens ?? [])].toSorted(
45
+ (left, right) => left.span.start - right.span.start,
46
+ );
35
47
 
36
48
  /** 場所の名前の語の数の上限。名前の読みを探す範囲。 */
37
49
  const MAX_PLACE_TOKENS = 20;
@@ -74,37 +86,83 @@ const companyFindings = (doc: ProseDocument, prose: string, reported: readonly R
74
86
  .filter(({ mention }) => !overlapsAny(reported, mention.offset, mention.surface.length))
75
87
  .map(({ mention, usual, kind }) => ({ offset: mention.offset, name: mention.surface, usual, kind: kind === "spelling" ? kind : `company-${kind}` }));
76
88
 
77
- /** 場所の名前の書き分け。人や会社の名前の見方がすでに指した所と重なるものは除く。 */
89
+ /** 場所の名前の書き分け。表の本体の升の中の名前も読む(見出しの行は読まない)。人や会社の名前の見方がすでに指した所と重なるものは除く。 */
78
90
  const placeFindings = (doc: ProseDocument, prose: string, reported: readonly Reported[]): Reported[] =>
79
- placeVariants(placeMentionsIn(prose, placeWordsOf(doc), placeReaderOf(doc)))
91
+ placeVariants(placeMentionsIn(proseWithCells(prose, cellsOf(doc)), placeWordsOf(doc), placeReaderOf(doc)))
80
92
  .filter(({ mention }) => !overlapsAny(reported, mention.offset, mention.surface.length))
81
93
  .map(({ mention, usual, kind }) => ({ offset: mention.offset, name: mention.surface, usual, kind }));
82
94
 
95
+ /** 名前の中で比べる語の品詞。助詞や冠詞(の、of)は語の順に数えない。 */
96
+ const CONTENT_POS: ReadonlySet<string> = new Set(["NOUN", "PROPN", "ADJ", "NUM", "VERB"]);
97
+
98
+ /** 欄の後ろの名前を切る所: 内容語でも名前の中の小さな語(of)でもない語の頭。 */
99
+ const orderStopsOf = (doc: ProseDocument): number[] => {
100
+ const joiners = new Set(patternsOf(doc, "name-title-joiner"));
101
+ return tokensOf(doc)
102
+ .filter((token) => !CONTENT_POS.has(token.pos) && !joiners.has(token.surface.toLowerCase()))
103
+ .map((token) => token.span.start);
104
+ };
105
+
106
+ const sentenceStartsOf = (doc: ProseDocument): ReadonlySet<number> => new Set(doc.sentences.flatMap((sentence) => sentence.tokens?.[0]?.span.start ?? []));
107
+
108
+ const orderWordsOf = (doc: ProseDocument): OrderWord[] => {
109
+ const suffixes = patternsOf(doc, "name-stem-suffix");
110
+ return tokensOf(doc)
111
+ .filter((token) => CONTENT_POS.has(token.pos))
112
+ .map((token) => ({ start: token.span.start, end: token.span.end, key: stemOf(token.surface, suffixes) }));
113
+ };
114
+
115
+ /**
116
+ * 語の順を入れ替えて書いた同じ名前(統計学基礎 と 基礎統計学)。名前と読める所は、括弧の中(name-quote)、大文字で始まる語の並び
117
+ * (name-title-joiner を挟んでよい)、名前を書く欄の後ろ(name-label)。大文字の語の並びどうしは、片方が表の升まるごとに
118
+ * 書いた名前のときだけ比べる。ほかの見方がすでに指した所と重なるものは除く。
119
+ */
120
+ const orderFindings = (doc: ProseDocument, prose: string, reported: readonly Reported[]): Reported[] => {
121
+ const starts = sentenceStartsOf(doc);
122
+ const spans = [
123
+ ...quotedSpans(prose, patternsOf(doc, "name-quote")),
124
+ ...titleCaseSpans(prose, patternsOf(doc, "name-title-joiner"), (offset) => starts.has(offset)),
125
+ ...labelledSpans(prose, patternsOf(doc, "name-label")),
126
+ ];
127
+ const cells = new Set(tableBodyCells(proseAndTablesOf(doc)).map((cell) => cell.text.trim()));
128
+ return wordOrderVariants(orderNamesOf(doc.source, spans, orderWordsOf(doc), orderStopsOf(doc)), cells)
129
+ .filter(({ name }) => !overlapsAny(reported, name.offset, name.surface.length))
130
+ .map(({ name, usual }) => ({ offset: name.offset, name: name.surface, usual, kind: "order" }));
131
+ };
132
+
83
133
  /** 人・製品・会社の名前の現れ。解析器が固有名詞と読む語、敬称の付く名前、前後の語で名前と読める漢字。 */
84
134
  const nameMentionsOf = (doc: ProseDocument, prose: string, chars: VariantChars): NameMention[] => {
85
135
  const cues: NameCues = { leads: patternsOf(doc, "person-lead"), suffixes: patternsOf(doc, "person-suffix"), particles: patternsOf(doc, "name-particle") };
86
136
  const tagged = doc.sentences.flatMap((sentence) => mentionsIn(sentence.tokens ?? [], doc.source, cues.suffixes));
87
137
  const taggedOrSuffixed = [...tagged, ...suffixedNamesIn(prose, cues.suffixes, chars, tagged)];
88
- return [...taggedOrSuffixed, ...cuedNamesIn(prose, cues, chars, taggedOrSuffixed)]
89
- .map((mention): NameMention => {
90
- const cue = mention.cue ?? nameCueAt(prose, mention.offset, mention.surface, cues);
91
- return cue === undefined ? mention : { ...mention, cue };
92
- })
93
- .toSorted((left, right) => left.offset - right.offset);
138
+ const cued = [...taggedOrSuffixed, ...cuedNamesIn(prose, cues, chars, taggedOrSuffixed)].map((mention): NameMention => {
139
+ const cue = mention.cue ?? nameCueAt(prose, mention.offset, mention.surface, cues);
140
+ return cue === undefined ? mention : { ...mention, cue };
141
+ });
142
+ return withKnownNeighbours(cued, prose).toSorted((left, right) => left.offset - right.offset);
94
143
  };
95
144
 
145
+ /** 表の升に書いた名前の書き分け。名前の形をした升を、本文の名前と比べる。ほかの見方がすでに指した所と重なるものは除く。 */
146
+ const tableFindings = (doc: ProseDocument, prose: string, mentions: readonly NameMention[], reported: readonly Reported[]): Reported[] =>
147
+ cellNameVariants(cellNamesIn(cellsOf(doc)), proseNamesOf(mentions, prose))
148
+ .filter(({ name }) => !reported.some((other) => other.offset < name.offset + name.surface.length && name.offset < other.offset + other.name.length))
149
+ .map(({ name, usual, kind }) => ({ offset: name.offset, name: name.surface, usual, kind }));
150
+
96
151
  /** 同じ名前を、文書の中で少しだけ違う形に書いた所(GitHub と Github、山田太郎 と 山田太朗)。少ないほうを指す。 */
97
152
  export const nameVariant: Detector = (doc): Finding[] => {
98
153
  const prose = doc.prose ?? doc.source;
99
154
  const chars = variantCharsOf(doc);
100
- const names = nameVariants(nameMentionsOf(doc, prose, chars), chars).map(({ mention, usual, kind }): Reported => ({
155
+ const mentions = nameMentionsOf(doc, prose, chars);
156
+ const names = nameVariants(mentions, chars).map(({ mention, usual, kind }): Reported => ({
101
157
  offset: mention.offset,
102
158
  name: mention.surface,
103
159
  usual,
104
160
  kind,
105
161
  }));
106
162
  const namesAndCompanies = [...names, ...companyFindings(doc, prose, names)];
107
- return [...namesAndCompanies, ...placeFindings(doc, prose, namesAndCompanies)]
163
+ const withTables = [...namesAndCompanies, ...tableFindings(doc, prose, mentions, namesAndCompanies)];
164
+ const withPlaces = [...withTables, ...placeFindings(doc, prose, withTables)];
165
+ return [...withPlaces, ...orderFindings(doc, prose, withPlaces)]
108
166
  .toSorted((left, right) => left.offset - right.offset)
109
167
  .map(({ offset, name, usual, kind }) => ({
110
168
  rule: "",