chaffjs 0.30.0 → 0.31.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 (530) hide show
  1. package/dist/announced-count.d.ts +2 -0
  2. package/dist/announced-count.d.ts.map +1 -1
  3. package/dist/announced-count.js +4 -3
  4. package/dist/announced-count.js.map +1 -1
  5. package/dist/contacts/addresses.d.ts +21 -0
  6. package/dist/contacts/addresses.d.ts.map +1 -0
  7. package/dist/contacts/addresses.js +144 -0
  8. package/dist/contacts/addresses.js.map +1 -0
  9. package/dist/contacts/emails.d.ts +11 -0
  10. package/dist/contacts/emails.d.ts.map +1 -0
  11. package/dist/contacts/emails.js +51 -0
  12. package/dist/contacts/emails.js.map +1 -0
  13. package/dist/contacts/variants.d.ts +14 -0
  14. package/dist/contacts/variants.d.ts.map +1 -0
  15. package/dist/contacts/variants.js +25 -0
  16. package/dist/contacts/variants.js.map +1 -0
  17. package/dist/derived/battery-runtime.d.ts +85 -0
  18. package/dist/derived/battery-runtime.d.ts.map +1 -0
  19. package/dist/derived/battery-runtime.js +217 -0
  20. package/dist/derived/battery-runtime.js.map +1 -0
  21. package/dist/derived/period-counts.d.ts +48 -0
  22. package/dist/derived/period-counts.d.ts.map +1 -0
  23. package/dist/derived/period-counts.js +128 -0
  24. package/dist/derived/period-counts.js.map +1 -0
  25. package/dist/derived/time-lengths.d.ts.map +1 -1
  26. package/dist/derived/time-lengths.js +12 -9
  27. package/dist/derived/time-lengths.js.map +1 -1
  28. package/dist/derived/use-interval.d.ts +33 -0
  29. package/dist/derived/use-interval.d.ts.map +1 -0
  30. package/dist/derived/use-interval.js +93 -0
  31. package/dist/derived/use-interval.js.map +1 -0
  32. package/dist/detectors/announced-count.d.ts.map +1 -1
  33. package/dist/detectors/announced-count.js +1 -0
  34. package/dist/detectors/announced-count.js.map +1 -1
  35. package/dist/detectors/capacity-runtime.d.ts +7 -0
  36. package/dist/detectors/capacity-runtime.d.ts.map +1 -0
  37. package/dist/detectors/capacity-runtime.js +98 -0
  38. package/dist/detectors/capacity-runtime.js.map +1 -0
  39. package/dist/detectors/contact-variant.d.ts +3 -0
  40. package/dist/detectors/contact-variant.d.ts.map +1 -0
  41. package/dist/detectors/contact-variant.js +21 -0
  42. package/dist/detectors/contact-variant.js.map +1 -0
  43. package/dist/detectors/count-over-capacity.d.ts +4 -0
  44. package/dist/detectors/count-over-capacity.d.ts.map +1 -0
  45. package/dist/detectors/count-over-capacity.js +26 -0
  46. package/dist/detectors/count-over-capacity.js.map +1 -0
  47. package/dist/detectors/date-outside-period.d.ts.map +1 -1
  48. package/dist/detectors/date-outside-period.js +2 -1
  49. package/dist/detectors/date-outside-period.js.map +1 -1
  50. package/dist/detectors/date-stamp-order.d.ts +3 -0
  51. package/dist/detectors/date-stamp-order.d.ts.map +1 -0
  52. package/dist/detectors/date-stamp-order.js +24 -0
  53. package/dist/detectors/date-stamp-order.js.map +1 -0
  54. package/dist/detectors/defined-term-form.d.ts.map +1 -1
  55. package/dist/detectors/defined-term-form.js +57 -7
  56. package/dist/detectors/defined-term-form.js.map +1 -1
  57. package/dist/detectors/definition-use.d.ts +1 -1
  58. package/dist/detectors/definition-use.d.ts.map +1 -1
  59. package/dist/detectors/definition-use.js +4 -2
  60. package/dist/detectors/definition-use.js.map +1 -1
  61. package/dist/detectors/derived-numbers.d.ts +9 -1
  62. package/dist/detectors/derived-numbers.d.ts.map +1 -1
  63. package/dist/detectors/derived-numbers.js +2 -2
  64. package/dist/detectors/derived-numbers.js.map +1 -1
  65. package/dist/detectors/due-date.d.ts.map +1 -1
  66. package/dist/detectors/due-date.js +6 -0
  67. package/dist/detectors/due-date.js.map +1 -1
  68. package/dist/detectors/en-shape.d.ts.map +1 -1
  69. package/dist/detectors/en-shape.js +7 -4
  70. package/dist/detectors/en-shape.js.map +1 -1
  71. package/dist/detectors/event-deadline.d.ts +3 -0
  72. package/dist/detectors/event-deadline.d.ts.map +1 -0
  73. package/dist/detectors/event-deadline.js +24 -0
  74. package/dist/detectors/event-deadline.js.map +1 -0
  75. package/dist/detectors/fact-consistency.d.ts +1 -1
  76. package/dist/detectors/fact-consistency.d.ts.map +1 -1
  77. package/dist/detectors/fact-consistency.js +52 -3
  78. package/dist/detectors/fact-consistency.js.map +1 -1
  79. package/dist/detectors/heading-case.d.ts +5 -2
  80. package/dist/detectors/heading-case.d.ts.map +1 -1
  81. package/dist/detectors/heading-case.js +12 -4
  82. package/dist/detectors/heading-case.js.map +1 -1
  83. package/dist/detectors/heading-echo.d.ts +5 -0
  84. package/dist/detectors/heading-echo.d.ts.map +1 -1
  85. package/dist/detectors/heading-echo.js +14 -1
  86. package/dist/detectors/heading-echo.js.map +1 -1
  87. package/dist/detectors/layout-code.d.ts +5 -0
  88. package/dist/detectors/layout-code.d.ts.map +1 -0
  89. package/dist/detectors/layout-code.js +124 -0
  90. package/dist/detectors/layout-code.js.map +1 -0
  91. package/dist/detectors/measured-facts.d.ts +8 -0
  92. package/dist/detectors/measured-facts.d.ts.map +1 -0
  93. package/dist/detectors/measured-facts.js +18 -0
  94. package/dist/detectors/measured-facts.js.map +1 -0
  95. package/dist/detectors/name-variant-model-codes.d.ts +11 -0
  96. package/dist/detectors/name-variant-model-codes.d.ts.map +1 -0
  97. package/dist/detectors/name-variant-model-codes.js +21 -0
  98. package/dist/detectors/name-variant-model-codes.js.map +1 -0
  99. package/dist/detectors/name-variant.d.ts.map +1 -1
  100. package/dist/detectors/name-variant.js +25 -4
  101. package/dist/detectors/name-variant.js.map +1 -1
  102. package/dist/detectors/orthographic-variant.d.ts.map +1 -1
  103. package/dist/detectors/orthographic-variant.js +62 -7
  104. package/dist/detectors/orthographic-variant.js.map +1 -1
  105. package/dist/detectors/orthography.d.ts.map +1 -1
  106. package/dist/detectors/orthography.js +12 -6
  107. package/dist/detectors/orthography.js.map +1 -1
  108. package/dist/detectors/quantity-range.d.ts +3 -0
  109. package/dist/detectors/quantity-range.d.ts.map +1 -0
  110. package/dist/detectors/quantity-range.js +35 -0
  111. package/dist/detectors/quantity-range.js.map +1 -0
  112. package/dist/detectors/range-band.d.ts +5 -0
  113. package/dist/detectors/range-band.d.ts.map +1 -0
  114. package/dist/detectors/range-band.js +80 -0
  115. package/dist/detectors/range-band.js.map +1 -0
  116. package/dist/detectors/registry/capacity-runtime-mismatch.d.ts +3 -0
  117. package/dist/detectors/registry/capacity-runtime-mismatch.d.ts.map +1 -0
  118. package/dist/detectors/registry/capacity-runtime-mismatch.js +3 -0
  119. package/dist/detectors/registry/capacity-runtime-mismatch.js.map +1 -0
  120. package/dist/detectors/registry/contact-variant.d.ts +3 -0
  121. package/dist/detectors/registry/contact-variant.d.ts.map +1 -0
  122. package/dist/detectors/registry/contact-variant.js +3 -0
  123. package/dist/detectors/registry/contact-variant.js.map +1 -0
  124. package/dist/detectors/registry/count-over-capacity.d.ts +3 -0
  125. package/dist/detectors/registry/count-over-capacity.d.ts.map +1 -0
  126. package/dist/detectors/registry/count-over-capacity.js +3 -0
  127. package/dist/detectors/registry/count-over-capacity.js.map +1 -0
  128. package/dist/detectors/registry/date-stamp-order.d.ts +3 -0
  129. package/dist/detectors/registry/date-stamp-order.d.ts.map +1 -0
  130. package/dist/detectors/registry/date-stamp-order.js +3 -0
  131. package/dist/detectors/registry/date-stamp-order.js.map +1 -0
  132. package/dist/detectors/registry/event-deadline.d.ts +3 -0
  133. package/dist/detectors/registry/event-deadline.d.ts.map +1 -0
  134. package/dist/detectors/registry/event-deadline.js +3 -0
  135. package/dist/detectors/registry/event-deadline.js.map +1 -0
  136. package/dist/detectors/registry/interval-count-mismatch.d.ts +3 -0
  137. package/dist/detectors/registry/interval-count-mismatch.d.ts.map +1 -0
  138. package/dist/detectors/registry/interval-count-mismatch.js +3 -0
  139. package/dist/detectors/registry/interval-count-mismatch.js.map +1 -0
  140. package/dist/detectors/registry/layout-code-mismatch.d.ts +3 -0
  141. package/dist/detectors/registry/layout-code-mismatch.d.ts.map +1 -0
  142. package/dist/detectors/registry/layout-code-mismatch.js +3 -0
  143. package/dist/detectors/registry/layout-code-mismatch.js.map +1 -0
  144. package/dist/detectors/registry/quantity-range.d.ts +3 -0
  145. package/dist/detectors/registry/quantity-range.d.ts.map +1 -0
  146. package/dist/detectors/registry/quantity-range.js +3 -0
  147. package/dist/detectors/registry/quantity-range.js.map +1 -0
  148. package/dist/detectors/registry/range-band-mismatch.d.ts +3 -0
  149. package/dist/detectors/registry/range-band-mismatch.d.ts.map +1 -0
  150. package/dist/detectors/registry/range-band-mismatch.js +3 -0
  151. package/dist/detectors/registry/range-band-mismatch.js.map +1 -0
  152. package/dist/detectors/registry/rent-multiple-mismatch.d.ts +3 -0
  153. package/dist/detectors/registry/rent-multiple-mismatch.d.ts.map +1 -0
  154. package/dist/detectors/registry/rent-multiple-mismatch.js +3 -0
  155. package/dist/detectors/registry/rent-multiple-mismatch.js.map +1 -0
  156. package/dist/detectors/registry/scaled-quantity-mismatch.d.ts +3 -0
  157. package/dist/detectors/registry/scaled-quantity-mismatch.d.ts.map +1 -0
  158. package/dist/detectors/registry/scaled-quantity-mismatch.js +3 -0
  159. package/dist/detectors/registry/scaled-quantity-mismatch.js.map +1 -0
  160. package/dist/detectors/registry/step-time-sum-mismatch.d.ts +3 -0
  161. package/dist/detectors/registry/step-time-sum-mismatch.d.ts.map +1 -0
  162. package/dist/detectors/registry/step-time-sum-mismatch.js +3 -0
  163. package/dist/detectors/registry/step-time-sum-mismatch.js.map +1 -0
  164. package/dist/detectors/registry/time-range-reversed.d.ts +3 -0
  165. package/dist/detectors/registry/time-range-reversed.d.ts.map +1 -0
  166. package/dist/detectors/registry/time-range-reversed.js +3 -0
  167. package/dist/detectors/registry/time-range-reversed.js.map +1 -0
  168. package/dist/detectors/registry/unlisted-item-used.d.ts +3 -0
  169. package/dist/detectors/registry/unlisted-item-used.d.ts.map +1 -0
  170. package/dist/detectors/registry/unlisted-item-used.js +3 -0
  171. package/dist/detectors/registry/unlisted-item-used.js.map +1 -0
  172. package/dist/detectors/registry/walk-time-distance-mismatch.d.ts +3 -0
  173. package/dist/detectors/registry/walk-time-distance-mismatch.d.ts.map +1 -0
  174. package/dist/detectors/registry/walk-time-distance-mismatch.js +3 -0
  175. package/dist/detectors/registry/walk-time-distance-mismatch.js.map +1 -0
  176. package/dist/detectors/rent-multiple-mismatch.d.ts +3 -0
  177. package/dist/detectors/rent-multiple-mismatch.d.ts.map +1 -0
  178. package/dist/detectors/rent-multiple-mismatch.js +39 -0
  179. package/dist/detectors/rent-multiple-mismatch.js.map +1 -0
  180. package/dist/detectors/scaled-quantity.d.ts +3 -0
  181. package/dist/detectors/scaled-quantity.d.ts.map +1 -0
  182. package/dist/detectors/scaled-quantity.js +37 -0
  183. package/dist/detectors/scaled-quantity.js.map +1 -0
  184. package/dist/detectors/signals.d.ts.map +1 -1
  185. package/dist/detectors/signals.js +3 -1
  186. package/dist/detectors/signals.js.map +1 -1
  187. package/dist/detectors/step-time-sum.d.ts +3 -0
  188. package/dist/detectors/step-time-sum.d.ts.map +1 -0
  189. package/dist/detectors/step-time-sum.js +46 -0
  190. package/dist/detectors/step-time-sum.js.map +1 -0
  191. package/dist/detectors/structure-tree.d.ts +2 -0
  192. package/dist/detectors/structure-tree.d.ts.map +1 -1
  193. package/dist/detectors/structure-tree.js +22 -5
  194. package/dist/detectors/structure-tree.js.map +1 -1
  195. package/dist/detectors/structure.js +1 -1
  196. package/dist/detectors/structure.js.map +1 -1
  197. package/dist/detectors/technical-docs.d.ts +5 -0
  198. package/dist/detectors/technical-docs.d.ts.map +1 -1
  199. package/dist/detectors/technical-docs.js +12 -0
  200. package/dist/detectors/technical-docs.js.map +1 -1
  201. package/dist/detectors/time-order.d.ts +1 -0
  202. package/dist/detectors/time-order.d.ts.map +1 -1
  203. package/dist/detectors/time-order.js +3 -1
  204. package/dist/detectors/time-order.js.map +1 -1
  205. package/dist/detectors/unit-consistency.d.ts +1 -1
  206. package/dist/detectors/unit-consistency.d.ts.map +1 -1
  207. package/dist/detectors/unit-consistency.js +46 -38
  208. package/dist/detectors/unit-consistency.js.map +1 -1
  209. package/dist/detectors/unlisted-items.d.ts +3 -0
  210. package/dist/detectors/unlisted-items.d.ts.map +1 -0
  211. package/dist/detectors/unlisted-items.js +57 -0
  212. package/dist/detectors/unlisted-items.js.map +1 -0
  213. package/dist/detectors/use-count-words.d.ts +8 -0
  214. package/dist/detectors/use-count-words.d.ts.map +1 -0
  215. package/dist/detectors/use-count-words.js +20 -0
  216. package/dist/detectors/use-count-words.js.map +1 -0
  217. package/dist/detectors/use-interval.d.ts +3 -0
  218. package/dist/detectors/use-interval.d.ts.map +1 -0
  219. package/dist/detectors/use-interval.js +39 -0
  220. package/dist/detectors/use-interval.js.map +1 -0
  221. package/dist/detectors/walk-time-distance-mismatch.d.ts +4 -0
  222. package/dist/detectors/walk-time-distance-mismatch.d.ts.map +1 -0
  223. package/dist/detectors/walk-time-distance-mismatch.js +38 -0
  224. package/dist/detectors/walk-time-distance-mismatch.js.map +1 -0
  225. package/dist/facts/approximate-tolerance.d.ts +7 -0
  226. package/dist/facts/approximate-tolerance.d.ts.map +1 -0
  227. package/dist/facts/approximate-tolerance.js +16 -0
  228. package/dist/facts/approximate-tolerance.js.map +1 -0
  229. package/dist/facts/item-amounts.d.ts +18 -0
  230. package/dist/facts/item-amounts.d.ts.map +1 -0
  231. package/dist/facts/item-amounts.js +67 -0
  232. package/dist/facts/item-amounts.js.map +1 -0
  233. package/dist/facts/labelled-facts.d.ts +4 -0
  234. package/dist/facts/labelled-facts.d.ts.map +1 -1
  235. package/dist/facts/labelled-facts.js +21 -6
  236. package/dist/facts/labelled-facts.js.map +1 -1
  237. package/dist/facts/measure-units.d.ts +5 -0
  238. package/dist/facts/measure-units.d.ts.map +1 -0
  239. package/dist/facts/measure-units.js +28 -0
  240. package/dist/facts/measure-units.js.map +1 -0
  241. package/dist/facts/measures.d.ts +23 -4
  242. package/dist/facts/measures.d.ts.map +1 -1
  243. package/dist/facts/measures.js +68 -19
  244. package/dist/facts/measures.js.map +1 -1
  245. package/dist/facts/retention-periods.d.ts +51 -0
  246. package/dist/facts/retention-periods.d.ts.map +1 -0
  247. package/dist/facts/retention-periods.js +148 -0
  248. package/dist/facts/retention-periods.js.map +1 -0
  249. package/dist/facts/unit-conflicts.d.ts +3 -2
  250. package/dist/facts/unit-conflicts.d.ts.map +1 -1
  251. package/dist/facts/unit-conflicts.js +15 -7
  252. package/dist/facts/unit-conflicts.js.map +1 -1
  253. package/dist/facts/unit-equivalents.d.ts +13 -0
  254. package/dist/facts/unit-equivalents.d.ts.map +1 -0
  255. package/dist/facts/unit-equivalents.js +56 -0
  256. package/dist/facts/unit-equivalents.js.map +1 -0
  257. package/dist/facts/unit-tolerance.d.ts +4 -0
  258. package/dist/facts/unit-tolerance.d.ts.map +1 -0
  259. package/dist/facts/unit-tolerance.js +13 -0
  260. package/dist/facts/unit-tolerance.js.map +1 -0
  261. package/dist/facts/unit-word.d.ts +8 -0
  262. package/dist/facts/unit-word.d.ts.map +1 -0
  263. package/dist/facts/unit-word.js +34 -0
  264. package/dist/facts/unit-word.js.map +1 -0
  265. package/dist/facts/written-precision.d.ts +10 -0
  266. package/dist/facts/written-precision.d.ts.map +1 -0
  267. package/dist/facts/written-precision.js +20 -0
  268. package/dist/facts/written-precision.js.map +1 -0
  269. package/dist/kana-spelling.d.ts +12 -0
  270. package/dist/kana-spelling.d.ts.map +1 -1
  271. package/dist/kana-spelling.js +23 -0
  272. package/dist/kana-spelling.js.map +1 -1
  273. package/dist/model-codes.d.ts +23 -0
  274. package/dist/model-codes.d.ts.map +1 -0
  275. package/dist/model-codes.js +81 -0
  276. package/dist/model-codes.js.map +1 -0
  277. package/dist/name-spelling-chars.d.ts +16 -0
  278. package/dist/name-spelling-chars.d.ts.map +1 -0
  279. package/dist/name-spelling-chars.js +14 -0
  280. package/dist/name-spelling-chars.js.map +1 -0
  281. package/dist/name-variants.d.ts +3 -2
  282. package/dist/name-variants.d.ts.map +1 -1
  283. package/dist/name-variants.js +4 -7
  284. package/dist/name-variants.js.map +1 -1
  285. package/dist/phone-numbers.d.ts.map +1 -1
  286. package/dist/phone-numbers.js +22 -5
  287. package/dist/phone-numbers.js.map +1 -1
  288. package/dist/place-names.d.ts +12 -5
  289. package/dist/place-names.d.ts.map +1 -1
  290. package/dist/place-names.js +34 -14
  291. package/dist/place-names.js.map +1 -1
  292. package/dist/plugin.d.ts +5 -0
  293. package/dist/plugin.d.ts.map +1 -1
  294. package/dist/preamble-paragraphs.d.ts +2 -1
  295. package/dist/preamble-paragraphs.d.ts.map +1 -1
  296. package/dist/preamble-paragraphs.js +16 -1
  297. package/dist/preamble-paragraphs.js.map +1 -1
  298. package/dist/profile/parse.d.ts.map +1 -1
  299. package/dist/profile/parse.js +11 -0
  300. package/dist/profile/parse.js.map +1 -1
  301. package/dist/spacing-code.d.ts +6 -0
  302. package/dist/spacing-code.d.ts.map +1 -0
  303. package/dist/spacing-code.js +16 -0
  304. package/dist/spacing-code.js.map +1 -0
  305. package/dist/structure/amount-range.d.ts +2 -2
  306. package/dist/structure/amount-range.d.ts.map +1 -1
  307. package/dist/structure/amount-range.js +12 -5
  308. package/dist/structure/amount-range.js.map +1 -1
  309. package/dist/structure/capacity-count.d.ts +59 -0
  310. package/dist/structure/capacity-count.d.ts.map +1 -0
  311. package/dist/structure/capacity-count.js +118 -0
  312. package/dist/structure/capacity-count.js.map +1 -0
  313. package/dist/structure/counted-amounts.d.ts +6 -0
  314. package/dist/structure/counted-amounts.d.ts.map +1 -1
  315. package/dist/structure/counted-amounts.js +4 -4
  316. package/dist/structure/counted-amounts.js.map +1 -1
  317. package/dist/structure/date-outside-period.d.ts +3 -1
  318. package/dist/structure/date-outside-period.d.ts.map +1 -1
  319. package/dist/structure/date-outside-period.js +8 -5
  320. package/dist/structure/date-outside-period.js.map +1 -1
  321. package/dist/structure/date-range.d.ts +1 -0
  322. package/dist/structure/date-range.d.ts.map +1 -1
  323. package/dist/structure/date-range.js +1 -1
  324. package/dist/structure/date-range.js.map +1 -1
  325. package/dist/structure/deadline-after-event.d.ts +23 -0
  326. package/dist/structure/deadline-after-event.d.ts.map +1 -0
  327. package/dist/structure/deadline-after-event.js +42 -0
  328. package/dist/structure/deadline-after-event.js.map +1 -0
  329. package/dist/structure/definition-use.d.ts +1 -1
  330. package/dist/structure/definition-use.d.ts.map +1 -1
  331. package/dist/structure/definition-use.js +55 -2
  332. package/dist/structure/definition-use.js.map +1 -1
  333. package/dist/structure/due-date.d.ts.map +1 -1
  334. package/dist/structure/due-date.js +5 -27
  335. package/dist/structure/due-date.js.map +1 -1
  336. package/dist/structure/labelled-date-order.d.ts +38 -0
  337. package/dist/structure/labelled-date-order.d.ts.map +1 -0
  338. package/dist/structure/labelled-date-order.js +41 -0
  339. package/dist/structure/labelled-date-order.js.map +1 -0
  340. package/dist/structure/layout-codes.d.ts +68 -0
  341. package/dist/structure/layout-codes.d.ts.map +1 -0
  342. package/dist/structure/layout-codes.js +161 -0
  343. package/dist/structure/layout-codes.js.map +1 -0
  344. package/dist/structure/measured-amounts.d.ts +17 -0
  345. package/dist/structure/measured-amounts.d.ts.map +1 -0
  346. package/dist/structure/measured-amounts.js +110 -0
  347. package/dist/structure/measured-amounts.js.map +1 -0
  348. package/dist/structure/quantity-range.d.ts +8 -0
  349. package/dist/structure/quantity-range.d.ts.map +1 -0
  350. package/dist/structure/quantity-range.js +36 -0
  351. package/dist/structure/quantity-range.js.map +1 -0
  352. package/dist/structure/range-bands.d.ts +75 -0
  353. package/dist/structure/range-bands.d.ts.map +1 -0
  354. package/dist/structure/range-bands.js +244 -0
  355. package/dist/structure/range-bands.js.map +1 -0
  356. package/dist/structure/rent-multiple.d.ts +76 -0
  357. package/dist/structure/rent-multiple.d.ts.map +1 -0
  358. package/dist/structure/rent-multiple.js +186 -0
  359. package/dist/structure/rent-multiple.js.map +1 -0
  360. package/dist/structure/scaled-quantities.d.ts +50 -0
  361. package/dist/structure/scaled-quantities.d.ts.map +1 -0
  362. package/dist/structure/scaled-quantities.js +153 -0
  363. package/dist/structure/scaled-quantities.js.map +1 -0
  364. package/dist/structure/stamp-order.d.ts +16 -0
  365. package/dist/structure/stamp-order.d.ts.map +1 -0
  366. package/dist/structure/stamp-order.js +78 -0
  367. package/dist/structure/stamp-order.js.map +1 -0
  368. package/dist/structure/step-times.d.ts +49 -0
  369. package/dist/structure/step-times.d.ts.map +1 -0
  370. package/dist/structure/step-times.js +128 -0
  371. package/dist/structure/step-times.js.map +1 -0
  372. package/dist/structure/term-deadline.d.ts +19 -0
  373. package/dist/structure/term-deadline.d.ts.map +1 -0
  374. package/dist/structure/term-deadline.js +28 -0
  375. package/dist/structure/term-deadline.js.map +1 -0
  376. package/dist/structure/time-range.d.ts +16 -0
  377. package/dist/structure/time-range.d.ts.map +1 -0
  378. package/dist/structure/time-range.js +57 -0
  379. package/dist/structure/time-range.js.map +1 -0
  380. package/dist/structure/total-line.d.ts +16 -0
  381. package/dist/structure/total-line.d.ts.map +1 -0
  382. package/dist/structure/total-line.js +44 -0
  383. package/dist/structure/total-line.js.map +1 -0
  384. package/dist/structure/total.d.ts +7 -1
  385. package/dist/structure/total.d.ts.map +1 -1
  386. package/dist/structure/total.js +28 -5
  387. package/dist/structure/total.js.map +1 -1
  388. package/dist/structure/unlisted-items.d.ts +70 -0
  389. package/dist/structure/unlisted-items.d.ts.map +1 -0
  390. package/dist/structure/unlisted-items.js +152 -0
  391. package/dist/structure/unlisted-items.js.map +1 -0
  392. package/dist/structure/walk-time.d.ts +87 -0
  393. package/dist/structure/walk-time.d.ts.map +1 -0
  394. package/dist/structure/walk-time.js +195 -0
  395. package/dist/structure/walk-time.js.map +1 -0
  396. package/package.json +3 -3
  397. package/profiles/listing.yaml +20 -0
  398. package/rules/announced-count-mismatch.yaml +2 -2
  399. package/rules/capacity-runtime-mismatch.yaml +66 -0
  400. package/rules/contact-variant.yaml +76 -0
  401. package/rules/count-over-capacity.yaml +55 -0
  402. package/rules/date-outside-period.yaml +4 -4
  403. package/rules/date-stamp-order.yaml +82 -0
  404. package/rules/deadline-after-event.yaml +67 -0
  405. package/rules/defined-name-repeated.yaml +2 -2
  406. package/rules/due-before-issue.yaml +11 -7
  407. package/rules/fact-conflict.yaml +9 -4
  408. package/rules/heading-echo.yaml +2 -2
  409. package/rules/interval-count-mismatch.yaml +54 -0
  410. package/rules/latin-spacing.yaml +2 -2
  411. package/rules/layout-code-mismatch.yaml +68 -0
  412. package/rules/name-variant.yaml +8 -2
  413. package/rules/no-em-dash.yaml +2 -2
  414. package/rules/preamble-length.yaml +2 -2
  415. package/rules/quantity-range-reversed.yaml +53 -0
  416. package/rules/range-band-mismatch.yaml +70 -0
  417. package/rules/rent-multiple-mismatch.yaml +61 -0
  418. package/rules/scaled-quantity-mismatch.yaml +69 -0
  419. package/rules/step-time-sum-mismatch.yaml +65 -0
  420. package/rules/time-range-reversed.yaml +48 -0
  421. package/rules/title-case-consistency.yaml +2 -2
  422. package/rules/total-mismatch.yaml +2 -2
  423. package/rules/unit-mismatch.yaml +12 -4
  424. package/rules/unlisted-item-used.yaml +90 -0
  425. package/rules/unused-definition.yaml +2 -2
  426. package/rules/use-before-definition.yaml +2 -2
  427. package/rules/walk-time-distance-mismatch.yaml +55 -0
  428. package/src/announced-count.ts +6 -3
  429. package/src/contacts/addresses.ts +179 -0
  430. package/src/contacts/emails.ts +63 -0
  431. package/src/contacts/variants.ts +33 -0
  432. package/src/derived/battery-runtime.ts +284 -0
  433. package/src/derived/period-counts.ts +178 -0
  434. package/src/derived/time-lengths.ts +12 -9
  435. package/src/derived/use-interval.ts +151 -0
  436. package/src/detectors/announced-count.ts +1 -0
  437. package/src/detectors/capacity-runtime.ts +128 -0
  438. package/src/detectors/contact-variant.ts +24 -0
  439. package/src/detectors/count-over-capacity.ts +37 -0
  440. package/src/detectors/date-outside-period.ts +2 -1
  441. package/src/detectors/date-stamp-order.ts +28 -0
  442. package/src/detectors/defined-term-form.ts +63 -7
  443. package/src/detectors/definition-use.ts +6 -2
  444. package/src/detectors/derived-numbers.ts +3 -3
  445. package/src/detectors/due-date.ts +10 -2
  446. package/src/detectors/en-shape.ts +11 -4
  447. package/src/detectors/event-deadline.ts +29 -0
  448. package/src/detectors/fact-consistency.ts +64 -3
  449. package/src/detectors/heading-case.ts +13 -4
  450. package/src/detectors/heading-echo.ts +17 -1
  451. package/src/detectors/layout-code.ts +174 -0
  452. package/src/detectors/measured-facts.ts +20 -0
  453. package/src/detectors/name-variant-model-codes.ts +27 -0
  454. package/src/detectors/name-variant.ts +32 -6
  455. package/src/detectors/orthographic-variant.ts +78 -9
  456. package/src/detectors/orthography.ts +16 -6
  457. package/src/detectors/quantity-range.ts +47 -0
  458. package/src/detectors/range-band.ts +125 -0
  459. package/src/detectors/registry/capacity-runtime-mismatch.ts +4 -0
  460. package/src/detectors/registry/contact-variant.ts +4 -0
  461. package/src/detectors/registry/count-over-capacity.ts +4 -0
  462. package/src/detectors/registry/date-stamp-order.ts +4 -0
  463. package/src/detectors/registry/event-deadline.ts +4 -0
  464. package/src/detectors/registry/interval-count-mismatch.ts +4 -0
  465. package/src/detectors/registry/layout-code-mismatch.ts +4 -0
  466. package/src/detectors/registry/quantity-range.ts +4 -0
  467. package/src/detectors/registry/range-band-mismatch.ts +4 -0
  468. package/src/detectors/registry/rent-multiple-mismatch.ts +4 -0
  469. package/src/detectors/registry/scaled-quantity-mismatch.ts +4 -0
  470. package/src/detectors/registry/step-time-sum-mismatch.ts +4 -0
  471. package/src/detectors/registry/time-range-reversed.ts +4 -0
  472. package/src/detectors/registry/unlisted-item-used.ts +4 -0
  473. package/src/detectors/registry/walk-time-distance-mismatch.ts +4 -0
  474. package/src/detectors/rent-multiple-mismatch.ts +56 -0
  475. package/src/detectors/scaled-quantity.ts +49 -0
  476. package/src/detectors/signals.ts +3 -1
  477. package/src/detectors/step-time-sum.ts +55 -0
  478. package/src/detectors/structure-tree.ts +30 -5
  479. package/src/detectors/structure.ts +1 -1
  480. package/src/detectors/technical-docs.ts +12 -0
  481. package/src/detectors/time-order.ts +5 -1
  482. package/src/detectors/unit-consistency.ts +50 -38
  483. package/src/detectors/unlisted-items.ts +72 -0
  484. package/src/detectors/use-count-words.ts +32 -0
  485. package/src/detectors/use-interval.ts +51 -0
  486. package/src/detectors/walk-time-distance-mismatch.ts +45 -0
  487. package/src/facts/approximate-tolerance.ts +19 -0
  488. package/src/facts/item-amounts.ts +94 -0
  489. package/src/facts/labelled-facts.ts +29 -7
  490. package/src/facts/measure-units.ts +31 -0
  491. package/src/facts/measures.ts +113 -25
  492. package/src/facts/retention-periods.ts +216 -0
  493. package/src/facts/unit-conflicts.ts +22 -9
  494. package/src/facts/unit-equivalents.ts +70 -0
  495. package/src/facts/unit-tolerance.ts +17 -0
  496. package/src/facts/unit-word.ts +35 -0
  497. package/src/facts/written-precision.ts +23 -0
  498. package/src/kana-spelling.ts +25 -0
  499. package/src/model-codes.ts +104 -0
  500. package/src/name-spelling-chars.ts +34 -0
  501. package/src/name-variants.ts +8 -7
  502. package/src/phone-numbers.ts +26 -6
  503. package/src/place-names.ts +53 -14
  504. package/src/plugin.ts +2 -0
  505. package/src/preamble-paragraphs.ts +21 -2
  506. package/src/profile/parse.ts +10 -0
  507. package/src/spacing-code.ts +26 -0
  508. package/src/structure/amount-range.ts +16 -7
  509. package/src/structure/capacity-count.ts +196 -0
  510. package/src/structure/counted-amounts.ts +5 -4
  511. package/src/structure/date-outside-period.ts +11 -6
  512. package/src/structure/date-range.ts +1 -1
  513. package/src/structure/deadline-after-event.ts +67 -0
  514. package/src/structure/definition-use.ts +59 -2
  515. package/src/structure/due-date.ts +8 -32
  516. package/src/structure/labelled-date-order.ts +84 -0
  517. package/src/structure/layout-codes.ts +210 -0
  518. package/src/structure/measured-amounts.ts +138 -0
  519. package/src/structure/quantity-range.ts +54 -0
  520. package/src/structure/range-bands.ts +316 -0
  521. package/src/structure/rent-multiple.ts +277 -0
  522. package/src/structure/scaled-quantities.ts +225 -0
  523. package/src/structure/stamp-order.ts +100 -0
  524. package/src/structure/step-times.ts +169 -0
  525. package/src/structure/term-deadline.ts +35 -0
  526. package/src/structure/time-range.ts +64 -0
  527. package/src/structure/total-line.ts +63 -0
  528. package/src/structure/total.ts +41 -6
  529. package/src/structure/unlisted-items.ts +207 -0
  530. package/src/structure/walk-time.ts +285 -0
@@ -0,0 +1,225 @@
1
+ import type { Span } from "../plugin.ts";
2
+ import { escapeRegExp } from "../orthography.ts";
3
+ import { cellsOf, tablesOf, type Cell } from "../facts/table-facts.ts";
4
+ import { measuredValues, type MeasureUnit } from "../facts/measures.ts";
5
+ import { withoutEdgeMarks } from "../facts/trim-marks.ts";
6
+ import { linesOf, type Line } from "./lines.ts";
7
+ import type { Mark } from "./time-marks.ts";
8
+
9
+ /**
10
+ * A table with one column per number of servings or batch size (4人分 / 8人分, Serves 4 / Serves 8, ×1 / ×2), and the one
11
+ * row that does not grow by the columns' ratio while the others do. The words come from the language packages. Pure.
12
+ */
13
+ export type ScaleWords = {
14
+ /** What marks a column as a number of servings (人分 after the number, Serves before it). */
15
+ readonly columns: readonly Mark[];
16
+ /** An amount that is left to the cook (少々, to taste): its row may stay as it is. */
17
+ readonly unscaled: readonly string[];
18
+ /** Spellings of one unit (teaspoon, teaspoons, tsp), each named by its group. */
19
+ readonly unitForms: readonly { readonly pattern: string; readonly group: string }[];
20
+ /** Units with a factor to a base unit (g and kg), so 1 kg and 1000 g read as one amount. */
21
+ readonly measures: readonly MeasureUnit[];
22
+ };
23
+
24
+ /** amount is in the base unit when the unit has a factor (1 kg is 1000); written is the number as written (1). */
25
+ export type Amount = { readonly amount: number; readonly written: number; readonly unit: string };
26
+
27
+ export type ScaleSlip = {
28
+ /** The row's first cell: the item. */
29
+ readonly item: Cell;
30
+ /** The cell that does not scale, and the column it is in. */
31
+ readonly cell: Cell;
32
+ readonly heading: string;
33
+ /** The cell it is compared with, and that column. */
34
+ readonly other: Cell;
35
+ readonly otherHeading: string;
36
+ /** What the cell would hold if it scaled like the other rows, in the unit it is written in. */
37
+ readonly expected: number;
38
+ readonly scaledRows: number;
39
+ };
40
+
41
+ /** Fewer rows than this that scale exactly are no evidence that the table scales at all. */
42
+ export const MIN_SCALED_ROWS = 3;
43
+ const SAME = 1e-9;
44
+ const MIN_COLUMNS = 2;
45
+
46
+ const NUMBER_TEXT = "[0-9]+(?:\\.[0-9]+)?";
47
+
48
+ /** The number of servings a column heading names (4人分, Serves 8, ×2), or undefined. */
49
+ export const servingsOf = (heading: string, marks: readonly Mark[]): number | undefined => {
50
+ const text = withoutEdgeMarks(heading.normalize("NFKC").trim()).toLowerCase();
51
+ const read = marks
52
+ .map((mark) => {
53
+ const word = escapeRegExp(mark.pattern.normalize("NFKC").toLowerCase());
54
+ const pattern = mark.position === "before" ? `^${word}\\s*(${NUMBER_TEXT})$` : `^(${NUMBER_TEXT})\\s*${word}$`;
55
+ return new RegExp(pattern, "u").exec(text)?.[1];
56
+ })
57
+ .find((number) => number !== undefined);
58
+ const servings = read === undefined ? undefined : Number(read);
59
+ return servings === undefined || !Number.isFinite(servings) || servings <= 0 ? undefined : servings;
60
+ };
61
+
62
+ /** A whole or decimal number (1,000 and 1.5), with a fraction after it (1 1/2) or as a fraction (1/2, ½). */
63
+ const WHOLE = String.raw`[0-9]+(?:,[0-9]{3})*(?:\.[0-9]+)?`;
64
+ const FRACTION = String.raw`(?:\s([0-9]+)[/⁄]([0-9]+)|[/⁄]([0-9]+))?`;
65
+ const AMOUNT = new RegExp(String.raw`(?<![\p{N}.,/⁄])(${WHOLE})${FRACTION}(?![\p{N}/⁄])`, "gu");
66
+
67
+ const valueOf = (match: RegExpMatchArray): number => {
68
+ const whole = Number((match[1] ?? "").replace(/,/gu, ""));
69
+ if (match[4] !== undefined) return whole / Number(match[4]);
70
+ return match[2] === undefined ? whole : whole + Number(match[2]) / Number(match[3]);
71
+ };
72
+
73
+ const LETTER_BEFORE = "(?<!\\p{L})";
74
+ const LETTER_AFTER = "(?!\\p{L})";
75
+
76
+ /** The cell's text with its number taken out and each spelling of a unit written as its group: "2 teaspoons" is "#teaspoon". */
77
+ const unitKey = (text: string, numberAt: Span, forms: ScaleWords["unitForms"]): string => {
78
+ const rest = `${text.slice(0, numberAt.start)}#${text.slice(numberAt.end)}`.toLowerCase();
79
+ const groups = new Map(forms.map((form) => [form.pattern.toLowerCase(), form.group]));
80
+ const spellings = [...groups.keys()].filter((pattern) => pattern !== "").toSorted((left, right) => right.length - left.length);
81
+ const grouped =
82
+ spellings.length === 0
83
+ ? rest
84
+ : rest.replace(
85
+ new RegExp(`${LETTER_BEFORE}(?:${spellings.map(escapeRegExp).join("|")})${LETTER_AFTER}`, "gu"),
86
+ (spelling) => `<${groups.get(spelling) ?? spelling}>`,
87
+ );
88
+ return grouped.replace(/\s+/gu, "");
89
+ };
90
+
91
+ /** NFKC writes ½ as 1⁄2, so 1½ would read as 11⁄2: a space keeps the whole number apart (1 1⁄2). */
92
+ const VULGAR_AFTER_DIGIT = /([0-90-9])([¼½¾⅐-⅞])/gu;
93
+
94
+ const normalized = (cellText: string): string => withoutEdgeMarks(cellText.replace(VULGAR_AFTER_DIGIT, "$1 $2").normalize("NFKC").trim());
95
+
96
+ /** One cell's amount and the unit it is in. undefined when the cell holds no number, or more than one (1と1/2, 2 (300 g)). */
97
+ export const amountOf = (cellText: string, words: ScaleWords): Amount | undefined => {
98
+ const text = normalized(cellText);
99
+ const numbers = [...text.matchAll(AMOUNT)];
100
+ const [only] = numbers;
101
+ if (numbers.length !== 1 || only === undefined) return undefined;
102
+ const written = valueOf(only);
103
+ if (!Number.isFinite(written)) return undefined;
104
+ const [measured] = measuredValues(text, words.measures);
105
+ const factor = measured?.factors[0];
106
+ if (measured !== undefined && factor !== undefined && measured.start === only.index && measured.end === text.length) {
107
+ return { amount: measured.amount * factor, written: measured.amount, unit: `=${measured.dimension}` };
108
+ }
109
+ return { amount: written, written, unit: unitKey(text, { start: only.index, end: only.index + only[0].length }, words.unitForms) };
110
+ };
111
+
112
+ const isUnscaled = (text: string, unscaled: readonly string[]): boolean => {
113
+ const lower = text.normalize("NFKC").toLowerCase();
114
+ return unscaled.some((word) => word !== "" && lower.includes(word.normalize("NFKC").toLowerCase()));
115
+ };
116
+
117
+ type Column = { readonly index: number; readonly servings: number; readonly heading: string };
118
+
119
+ type Row = { readonly item: Cell; readonly cells: readonly Cell[] };
120
+
121
+ /** The rows of one table whose item is named, as cells. */
122
+ const rowsOf = (rows: readonly Line[]): Row[] =>
123
+ rows.flatMap((line) => {
124
+ const cells = cellsOf(line);
125
+ const [item] = cells;
126
+ return item === undefined || withoutEdgeMarks(item.text) === "" ? [] : [{ item, cells }];
127
+ });
128
+
129
+ type Pair = {
130
+ readonly row: Row;
131
+ readonly baseCell: Cell;
132
+ readonly cell: Cell;
133
+ readonly from: Amount;
134
+ readonly to: Amount;
135
+ readonly scales: boolean;
136
+ };
137
+
138
+ /** Each row's two cells, when both are amounts in one unit and the row is not left to the cook. */
139
+ const readablePairs = (rows: readonly Row[], base: Column, other: Column, words: ScaleWords): Pair[] =>
140
+ rows.flatMap((row): Pair[] => {
141
+ const [baseCell, cell] = [row.cells[base.index], row.cells[other.index]];
142
+ if (baseCell === undefined || cell === undefined) return [];
143
+ if (isUnscaled(baseCell.text, words.unscaled) || isUnscaled(cell.text, words.unscaled)) return [];
144
+ const [from, to] = [amountOf(baseCell.text, words), amountOf(cell.text, words)];
145
+ if (from === undefined || to === undefined || from.unit !== to.unit) return [];
146
+ const scaled = (from.amount * other.servings) / base.servings;
147
+ return [{ row, baseCell, cell, from, to, scales: Math.abs(to.amount - scaled) <= SAME * Math.max(Math.abs(scaled), 1) }];
148
+ });
149
+
150
+ type Departure = { readonly pair: Pair; readonly scaled: number };
151
+
152
+ /** The one row of a column that departs from the base column while at least MIN_SCALED_ROWS others scale exactly. */
153
+ const departureOf = (rows: readonly Row[], base: Column, other: Column, words: ScaleWords): Departure | undefined => {
154
+ const pairs = readablePairs(rows, base, other, words);
155
+ const departing = pairs.filter((pair) => !pair.scales);
156
+ const [only] = departing;
157
+ const scaled = pairs.length - departing.length;
158
+ return departing.length === 1 && only !== undefined && scaled >= MIN_SCALED_ROWS ? { pair: only, scaled } : undefined;
159
+ };
160
+
161
+ /** What `amount` would be if it were `ratio` times `from`, in the unit `amount` is written in. */
162
+ export const expectedOf = (from: Amount, amount: Amount, ratio: number): number => {
163
+ const perWritten = amount.written === 0 || amount.amount === 0 ? 1 : amount.amount / amount.written;
164
+ return (from.amount * ratio) / perWritten;
165
+ };
166
+
167
+ const columnSlip = (base: Column, other: Column, { pair, scaled }: Departure): ScaleSlip => ({
168
+ item: pair.row.item,
169
+ cell: pair.cell,
170
+ heading: other.heading,
171
+ other: pair.baseCell,
172
+ otherHeading: base.heading,
173
+ expected: expectedOf(pair.from, pair.to, other.servings / base.servings),
174
+ scaledRows: scaled,
175
+ });
176
+
177
+ const baseSlip = (base: Column, other: Column, { pair, scaled }: Departure): ScaleSlip => ({
178
+ item: pair.row.item,
179
+ cell: pair.baseCell,
180
+ heading: base.heading,
181
+ other: pair.cell,
182
+ otherHeading: other.heading,
183
+ expected: expectedOf(pair.to, pair.from, base.servings / other.servings),
184
+ scaledRows: scaled,
185
+ });
186
+
187
+ type Found = { readonly other: Column; readonly departure: Departure | undefined };
188
+
189
+ /** Whether the row's cells outside the first column scale with each other (a row fixed in every column does not). */
190
+ const othersAgree = (found: readonly Found[]): boolean => {
191
+ const perServing = found.map(({ other, departure }) => (departure === undefined ? Number.NaN : departure.pair.to.amount / other.servings));
192
+ const [first] = perServing;
193
+ return first !== undefined && perServing.every((value) => Math.abs(value - first) <= SAME * Math.max(Math.abs(first), 1));
194
+ };
195
+
196
+ /**
197
+ * One table's slips. Each column is compared with the first servings column. When the same row departs in every other column
198
+ * (three or more columns) and those columns scale with each other, it is the first column's cell that is off, and that cell is
199
+ * reported once; a row that departs everywhere without that (the same amount in every column) may be fixed on purpose.
200
+ */
201
+ const tableSlips = (columns: readonly Column[], rows: readonly Row[], words: ScaleWords): ScaleSlip[] => {
202
+ const [base, ...others] = columns;
203
+ if (base === undefined) return [];
204
+ const found: Found[] = others.map((other) => ({ other, departure: departureOf(rows, base, other, words) }));
205
+ const [first] = found;
206
+ const offRows = new Set(found.map(({ departure }) => departure?.pair.row));
207
+ if (others.length > 1 && offRows.size === 1 && first?.departure !== undefined) {
208
+ return othersAgree(found) ? [baseSlip(base, first.other, first.departure)] : [];
209
+ }
210
+ return found.flatMap(({ other, departure }) => (departure === undefined ? [] : [columnSlip(base, other, departure)]));
211
+ };
212
+
213
+ const columnsOf = (header: Line, marks: readonly Mark[]): Column[] =>
214
+ cellsOf(header).flatMap((cell, index): Column[] => {
215
+ const servings = index === 0 ? undefined : servingsOf(cell.text, marks);
216
+ return servings === undefined ? [] : [{ index, servings, heading: withoutEdgeMarks(cell.text.trim()) }];
217
+ });
218
+
219
+ /** Every table of the source with two or more servings columns, and the cells in them that do not scale. */
220
+ export const scaleSlips = (source: string, words: ScaleWords): ScaleSlip[] =>
221
+ tablesOf(linesOf(source)).flatMap((table) => {
222
+ const columns = columnsOf(table.header, words.columns);
223
+ const distinct = new Set(columns.map((column) => column.servings));
224
+ return distinct.size === columns.length && columns.length >= MIN_COLUMNS ? tableSlips(columns, rowsOf(table.rows), words) : [];
225
+ });
@@ -0,0 +1,100 @@
1
+ import type { DatedSpan } from "./date-range.ts";
2
+ import type { StructureIssue } from "./issues.ts";
3
+ import { linesOf } from "./lines.ts";
4
+
5
+ /**
6
+ * A document's stamp dates out of order: an established date (2024年10月1日 制定, Established: April 1, 2023) later than an
7
+ * update (2024年9月1日 最終改定, Last updated: September 1, 2024). A stamp line (or one part of a line that holds several dates)
8
+ * holds one full date and, once the date is taken out, nothing but one stamp word (with colons, brackets, emphasis or a list
9
+ * marker around it). Only stamps written together are compared. Both stamps are reported, each naming the other: nothing tells
10
+ * which of the two dates is the wrong one. Pure; the stamp words come from the caller.
11
+ */
12
+ export type StampWords = { readonly established: readonly string[]; readonly updated: readonly string[] };
13
+
14
+ type Stamp = { readonly kind: "established" | "updated"; readonly date: DatedSpan; readonly line: number };
15
+
16
+ const FULL_DATE = /^\d{4}-\d{2}-\d{2}$/u;
17
+ /** How many lines apart two stamps may be and still be one document's stamp block; further apart, they date two documents. */
18
+ const MAX_LINE_GAP = 6;
19
+ /** What may stand around a stamp word: spaces, colons, brackets, table bars, Markdown emphasis and list markers. */
20
+ const AROUND_WORD = /[\s::*_()()[\]「」【】||\-‐−・]/u;
21
+ /** A part of a line between a wide space, a tab or a bar. */
22
+ const FIELD = /[^\t\u3000||]+/gu;
23
+
24
+ const bareWord = (text: string): string => {
25
+ const chars = [...text];
26
+ const first = chars.findIndex((char) => !AROUND_WORD.test(char));
27
+ const last = chars.findLastIndex((char) => !AROUND_WORD.test(char));
28
+ return first === -1
29
+ ? ""
30
+ : chars
31
+ .slice(first, last + 1)
32
+ .join("")
33
+ .toLowerCase();
34
+ };
35
+
36
+ const kindOf = (word: string, words: StampWords): Stamp["kind"] | undefined => {
37
+ const isOne = (list: readonly string[]): boolean => list.some((entry) => entry.toLowerCase() === word);
38
+ if (isOne(words.established)) return "established";
39
+ return isOne(words.updated) ? "updated" : undefined;
40
+ };
41
+
42
+ type Field = { readonly start: number; readonly end: number; readonly line: number };
43
+
44
+ const datesIn = (dates: readonly DatedSpan[], field: Field): DatedSpan[] => dates.filter((date) => date.offset >= field.start && date.end <= field.end);
45
+
46
+ /** A line, or, when it holds more than one date, its parts between wide spaces, tabs and bars (作成:平成16年2月4日, a wide space, 最終改定:令和3年11月19日). */
47
+ const fieldsOf = (source: string, dates: readonly DatedSpan[]): Field[] =>
48
+ linesOf(source).flatMap((line) => {
49
+ const whole = { start: line.start, end: line.start + line.text.length, line: line.number };
50
+ if (datesIn(dates, whole).length < 2) return [whole];
51
+ return [...line.text.matchAll(FIELD)].map((match) => ({
52
+ start: line.start + match.index,
53
+ end: line.start + match.index + match[0].length,
54
+ line: line.number,
55
+ }));
56
+ });
57
+
58
+ /** The stamps of a text: a line or field holding one full date and one stamp word, nothing else. */
59
+ const stampsOf = (source: string, dates: readonly DatedSpan[], words: StampWords): Stamp[] =>
60
+ fieldsOf(source, dates).flatMap((field) => {
61
+ const [date, ...more] = datesIn(dates, field);
62
+ if (date === undefined || more.length > 0 || !FULL_DATE.test(date.value)) return [];
63
+ const rest = `${source.slice(field.start, date.offset)} ${source.slice(date.end, field.end)}`;
64
+ const kind = kindOf(bareWord(rest), words);
65
+ return kind === undefined ? [] : [{ kind, date, line: field.line }];
66
+ });
67
+
68
+ /** Stamps written together, each within MAX_LINE_GAP lines of the one before: one document's stamp block. */
69
+ const blocksOf = (stamps: readonly Stamp[]): Stamp[][] =>
70
+ stamps.reduce<Stamp[][]>((blocks, stamp, index) => {
71
+ const previous = stamps[index - 1];
72
+ const last = blocks.at(-1);
73
+ if (previous === undefined || last === undefined || stamp.line - previous.line > MAX_LINE_GAP) return [...blocks, [stamp]];
74
+ return [...blocks.slice(0, -1), [...last, stamp]];
75
+ }, []);
76
+
77
+ const valuesOf = (established: Stamp, updated: Stamp): Record<string, string> => ({ established: established.date.value, updated: updated.date.value });
78
+
79
+ /** In one block: each established stamp later than an update, against the latest such update, and each such update against the first such established stamp. */
80
+ const blockIssues = (block: readonly Stamp[]): StructureIssue[] => {
81
+ const established = block.filter((stamp) => stamp.kind === "established");
82
+ const updates = block.filter((stamp) => stamp.kind === "updated").toSorted((a, b) => b.date.value.localeCompare(a.date.value));
83
+ const ofEstablished = established.flatMap((stamp) => {
84
+ const latest = updates.find((updated) => updated.date.value < stamp.date.value);
85
+ return latest === undefined ? [] : [{ offset: stamp.date.offset, values: valuesOf(stamp, latest) }];
86
+ });
87
+ const ofUpdates = updates.flatMap((updated) => {
88
+ const later = established.find((stamp) => stamp.date.value > updated.date.value);
89
+ return later === undefined ? [] : [{ offset: updated.date.offset, values: { ...valuesOf(later, updated), side: "updated" } }];
90
+ });
91
+ return [...ofEstablished, ...ofUpdates];
92
+ };
93
+
94
+ /** Each established stamp later than an update of its block, and each such update, the one naming the other. */
95
+ export const stampOrderIssues = (source: string, dates: readonly DatedSpan[], words: StampWords): StructureIssue[] => {
96
+ if (words.established.length === 0 || words.updated.length === 0) return [];
97
+ return blocksOf(stampsOf(source, dates, words))
98
+ .flatMap(blockIssues)
99
+ .toSorted((a, b) => a.offset - b.offset);
100
+ };
@@ -0,0 +1,169 @@
1
+ import type { Span } from "../plugin.ts";
2
+ import { labelOf, timeLengths, type LengthWords, type TimeLength } from "../derived/time-lengths.ts";
3
+ import { isWordAt, type Mark } from "./time-marks.ts";
4
+ import { linesOf } from "./lines.ts";
5
+
6
+ /**
7
+ * A stated total time (所要時間:80分, Total time: 1 hr 20 min) against the sum of the times written in the numbered steps
8
+ * ((10分), bake for 40 minutes). The words come from the language packages; nothing here knows a language. Pure.
9
+ */
10
+ export type StepTimeWords = {
11
+ readonly lengths: LengthWords;
12
+ /** Labels of a total time (所要時間, Total time), written before the length. */
13
+ readonly totals: readonly Mark[];
14
+ /** Marks of a rough figure (約, about, 程度): the total or a step that carries one is compared with a margin. */
15
+ readonly approximate: readonly Mark[];
16
+ /** Marks of a time that is not how long the step takes (10分後, every 5 minutes, after 10 minutes). */
17
+ readonly notOwn: readonly Mark[];
18
+ /** What joins the two ends of a range (10〜15分, 10 to 15 minutes): a step or total with a range is not read. */
19
+ readonly rangeJoiners: readonly string[];
20
+ };
21
+
22
+ /** A length of time with whether it was written as a rough figure. */
23
+ export type ReadTime = Span & { readonly minutes: number; readonly approximate: boolean };
24
+
25
+ export type StepTimeSlip = {
26
+ readonly total: ReadTime;
27
+ readonly sum: number;
28
+ readonly steps: number;
29
+ /** How far the sum may be from the total and still agree, in minutes: 0 unless a rough figure is involved. */
30
+ readonly margin: number;
31
+ };
32
+
33
+ /** A rough total agrees with a sum within this share of the total, and never less than ROUGH_MINUTES. */
34
+ export const ROUGH_SHARE = 0.1;
35
+ export const ROUGH_MINUTES = 5;
36
+ const MIN_STEPS = 2;
37
+
38
+ /** 1. or 1) with a space after it (so 1.5 hours is no step); the full-width 1.or 1)may run straight on. */
39
+ const STEP_ITEM = /^\s*\p{Nd}{1,3}(?:[.)]\s|[.)])/u;
40
+ const INDENTED = /^\s+\S/u;
41
+
42
+ type ListRead = { readonly lists: Span[][]; readonly open: boolean; readonly lastLine: number };
43
+
44
+ /** One line's place in the numbered lists: a new item, a line under the last item, a blank, or the end of the list. */
45
+ const readLine = (read: ListRead, line: { readonly text: string; readonly start: number; readonly number: number }): ListRead => {
46
+ const end = line.start + line.text.length;
47
+ const list = read.lists[read.lists.length - 1];
48
+ const last = list?.[list.length - 1];
49
+ if (STEP_ITEM.test(line.text)) {
50
+ if (read.open && list !== undefined) list.push({ start: line.start, end });
51
+ else read.lists.push([{ start: line.start, end }]);
52
+ return { lists: read.lists, open: true, lastLine: line.number };
53
+ }
54
+ if (line.text.trim() === "") return read;
55
+ if (read.open && list !== undefined && last !== undefined && INDENTED.test(line.text)) {
56
+ if (read.lastLine === line.number - 1) list[list.length - 1] = { start: last.start, end };
57
+ return { ...read, lastLine: line.number };
58
+ }
59
+ return { ...read, open: false };
60
+ };
61
+
62
+ /** The numbered lists of the source, each a run of items with only blank or indented lines between them. An item holds its line and the indented lines right under it. */
63
+ export const stepLists = (source: string): Span[][] => linesOf(source).reduce<ListRead>(readLine, { lists: [], open: false, lastLine: 0 }).lists;
64
+
65
+ /** Every numbered step of the source. */
66
+ export const stepItems = (source: string): Span[] => stepLists(source).flat();
67
+
68
+ const lower = (text: string): string => text.toLowerCase();
69
+
70
+ const sideOf = (marks: readonly Mark[], side: "before" | "after"): string[] =>
71
+ marks
72
+ .filter((mark) => (mark.position ?? "before") === side)
73
+ .map((mark) => lower(mark.pattern))
74
+ .filter((pattern) => pattern !== "")
75
+ .toSorted((left, right) => right.length - left.length);
76
+
77
+ /** The mark written right before at (white space between only), and where it starts. */
78
+ const markEndingAt = (text: string, at: number, patterns: readonly string[]): number | undefined => {
79
+ const head = lower(text.slice(0, at)).trimEnd();
80
+ const found = patterns.find((pattern) => isWordAt(head, head.length - pattern.length, pattern));
81
+ return found === undefined ? undefined : head.length - found.length;
82
+ };
83
+
84
+ /** Whether a mark is written right after at (white space between only). */
85
+ const markStartingAt = (text: string, at: number, patterns: readonly string[]): boolean => {
86
+ const tail = lower(text.slice(at));
87
+ const gap = tail.length - tail.trimStart().length;
88
+ return patterns.some((pattern) => isWordAt(tail, gap, pattern));
89
+ };
90
+
91
+ const DIGIT_BEFORE = /\p{Nd}\s*$/u;
92
+
93
+ /** Whether the length is the second end of a range: a joiner right before it, after a number, a number word or a unit (10〜15分, one to two hours, 10 minutes to 15 minutes). */
94
+ const endsRange = (text: string, at: number, words: StepTimeWords): boolean => {
95
+ const start = markEndingAt(text, at, words.rangeJoiners.map(lower));
96
+ if (start === undefined) return false;
97
+ const { numberWords, hourUnits, minuteUnits } = words.lengths;
98
+ const ends = [...numberWords, ...hourUnits, ...minuteUnits].map(lower).filter((word) => word !== "");
99
+ return DIGIT_BEFORE.test(text.slice(0, start)) || markEndingAt(text, start, ends) !== undefined;
100
+ };
101
+
102
+ type Reading = { readonly time: ReadTime; readonly from: number; readonly own: boolean; readonly range: boolean };
103
+
104
+ /** Each length in text, with where its rough-figure mark starts, whether it is the step's own time, and whether it ends a range. */
105
+ const readingsOf = (text: string, words: StepTimeWords): Reading[] =>
106
+ timeLengths(text, words.lengths).map((length: TimeLength): Reading => {
107
+ const roughBefore = markEndingAt(text, length.start, sideOf(words.approximate, "before"));
108
+ const roughAfter = markStartingAt(text, length.end, sideOf(words.approximate, "after"));
109
+ const from = roughBefore ?? length.start;
110
+ const notOwn =
111
+ markEndingAt(text, from, sideOf(words.notOwn, "before")) !== undefined ||
112
+ (!roughAfter && markStartingAt(text, length.end, sideOf(words.notOwn, "after")));
113
+ return {
114
+ time: { start: length.start, end: length.end, minutes: length.minutes, approximate: roughBefore !== undefined || roughAfter },
115
+ from,
116
+ own: !notOwn,
117
+ range: endsRange(text, length.start, words),
118
+ };
119
+ });
120
+
121
+ /** How long one step takes: its one own length of time. undefined when it has none, more than one, or a range. */
122
+ export const stepTime = (text: string, words: StepTimeWords): ReadTime | undefined => {
123
+ const readings = readingsOf(text, words);
124
+ if (readings.some((reading) => reading.range)) return undefined;
125
+ const own = readings.filter((reading) => reading.own);
126
+ return own.length === 1 ? own[0]?.time : undefined;
127
+ };
128
+
129
+ const outside = (offset: number, items: readonly Span[]): boolean => !items.some((item) => offset >= item.start && offset < item.end);
130
+
131
+ /** The totals written on one line. A line with a range (Total time: 10 to 15 minutes) states no exact total. */
132
+ const totalsOn = (line: { readonly text: string; readonly start: number }, words: StepTimeWords): ReadTime[] => {
133
+ const readings = readingsOf(line.text, words);
134
+ if (readings.some((reading) => reading.range)) return [];
135
+ return readings.flatMap((reading): ReadTime[] => {
136
+ const label = labelOf(line.text, { start: reading.from, end: reading.time.end }, words.totals);
137
+ if (label === undefined || !reading.own) return [];
138
+ return [{ ...reading.time, start: line.start + reading.time.start, end: line.start + reading.time.end }];
139
+ });
140
+ };
141
+
142
+ /** The total time the source states outside its steps: a length with a total label right before it. Only one value is read. */
143
+ export const statedTotal = (source: string, items: readonly Span[], words: StepTimeWords): ReadTime | undefined => {
144
+ const totals = linesOf(source)
145
+ .filter((line) => outside(line.start, items))
146
+ .flatMap((line) => totalsOn(line, words));
147
+ const values = new Set(totals.map((total) => total.minutes));
148
+ return values.size === 1 ? totals[0] : undefined;
149
+ };
150
+
151
+ /** The margin a rough figure allows: ROUGH_SHARE of the total, at least ROUGH_MINUTES. */
152
+ export const marginOf = (total: number, rough: boolean): number => (rough ? Math.max(ROUGH_MINUTES, total * ROUGH_SHARE) : 0);
153
+
154
+ /**
155
+ * The stated total against the sum of the steps' times, when the document has one numbered list, every step's time is readable,
156
+ * and they do not agree. Two lists may be two procedures, or one of them no procedure, so a document with two is not read.
157
+ */
158
+ export const stepTimeSlip = (source: string, words: StepTimeWords): StepTimeSlip | undefined => {
159
+ const lists = stepLists(source);
160
+ const [items] = lists;
161
+ if (lists.length !== 1 || items === undefined || items.length < MIN_STEPS) return undefined;
162
+ const total = statedTotal(source, items, words);
163
+ if (total === undefined) return undefined;
164
+ const times = items.map((item) => stepTime(source.slice(item.start, item.end), words));
165
+ if (times.some((time) => time === undefined)) return undefined;
166
+ const sum = times.reduce((minutes, time) => minutes + (time?.minutes ?? 0), 0);
167
+ const margin = marginOf(total.minutes, total.approximate || times.some((time) => time?.approximate === true));
168
+ return Math.abs(sum - total.minutes) <= margin ? undefined : { total, sum, steps: items.length, margin };
169
+ };
@@ -0,0 +1,35 @@
1
+ /**
2
+ * 文書の仕事の期間(開講期間、Term)と、後ろの節の締め切りの日付を比べた結果。語の有無は呼ぶ側が読んで渡す。
3
+ * 年の無い締め切りは、期間の始まりと終わりが同じ年のときだけその年と読む。年をまたぐ期間では、期間の外の月日は始まりの前とも終わりの後とも読める。
4
+ */
5
+ export type TermDates = { readonly start: string; readonly end: string };
6
+
7
+ /** 締め切りの日付を囲む項目か文の語: 締め切りの語、期間の外に置く語、期間の後に来てよい仕事の語(成績発表、再試験)。 */
8
+ export type DeadlineMarks = { readonly deadline: boolean; readonly aside: boolean; readonly afterTerm: boolean };
9
+
10
+ export type DeadlineVerdict = "late" | "within" | "before" | "no-year" | "not-deadline" | "aside" | "after-term";
11
+
12
+ const YEAR_LENGTH = "2026".length;
13
+ const DATED = /^\d{4}-\d{2}-\d{2}$/u;
14
+ const MONTH_DAY = /^\d{2}-\d{2}$/u;
15
+
16
+ /** 締め切りの日付を年月日で。年が無ければ、期間の始まりと終わりの年が同じときだけその年を足す。 */
17
+ export const deadlineDate = (value: string, term: TermDates): string | undefined => {
18
+ if (DATED.test(value)) return value;
19
+ if (!MONTH_DAY.test(value) || !DATED.test(term.start) || !DATED.test(term.end)) return undefined;
20
+ const year = term.start.slice(0, YEAR_LENGTH);
21
+ return year === term.end.slice(0, YEAR_LENGTH) ? `${year}-${value}` : undefined;
22
+ };
23
+
24
+ const placeOf = (date: string, term: TermDates): DeadlineVerdict => {
25
+ if (date > term.end) return "late";
26
+ return date < term.start ? "before" : "within";
27
+ };
28
+
29
+ export const deadlineVerdict = (value: string, term: TermDates, marks: DeadlineMarks): DeadlineVerdict => {
30
+ if (!marks.deadline) return "not-deadline";
31
+ if (marks.afterTerm) return "after-term";
32
+ if (marks.aside) return "aside";
33
+ const date = deadlineDate(value, term);
34
+ return date === undefined || !DATED.test(term.end) ? "no-year" : placeOf(date, term);
35
+ };
@@ -0,0 +1,64 @@
1
+ import type { StructureIssue } from "./issues.ts";
2
+ import { isRange, type RangeWords } from "./date-range.ts";
3
+ import { dayShiftOf, type TimeSpan, type TimeWords } from "./time-marks.ts";
4
+
5
+ /**
6
+ * 時刻の範囲の終わりが始まりより前(14:00〜13:30、2:00–1:30 pm、午後2時から午後1時半まで)。隣り合う二つの時刻の間が
7
+ * 範囲の記号だけか、「から … まで」「from … to」のときを範囲と読む(date-range と同じ読み方)。変更の語のある文は読まない。
8
+ * 終わりに翌日の印(翌、+1、next day)があれば日をまたぐ範囲で、言わない。印が無くても、三時間以上戻る終わりは
9
+ * 夜をまたぐ範囲(22:00〜6:00 の夜勤、18:00〜9:00)か十二時間制の読み違い(9:00–5:00)と見て言わない。打ち間違いは一、二時間の戻り。
10
+ * 片方にだけ午前・午後(a.m.、p.m.)を書いた範囲は、書いていない側も同じと読む(2:00–1:30 pm は 14:00 から 13:30)。
11
+ * ただし正午をまたぐ書き方(8:00–6:00 pm)があるので、そのときは一時間未満の戻りだけを言う。
12
+ */
13
+
14
+ const SECONDS_PER_HOUR = 3600;
15
+ /** 書いたまま読んだ範囲で、打ち間違いと見る戻りの上限。 */
16
+ const SLIP_SECONDS = 3 * SECONDS_PER_HOUR;
17
+ /** 午前・午後を片方から借りた範囲で、打ち間違いと見る戻りの上限。 */
18
+ const BORROWED_SLIP_SECONDS = SECONDS_PER_HOUR;
19
+ const NOON_SECONDS = 12 * SECONDS_PER_HOUR;
20
+ const ONE_OCLOCK_SECONDS = SECONDS_PER_HOUR;
21
+ const LAST_TWELVE_HOUR_SECONDS = 13 * SECONDS_PER_HOUR;
22
+
23
+ const MERIDIEM = /[ap]\.?m\b|午前|午後/iu;
24
+
25
+ const hasMeridiem = (source: string, time: TimeSpan): boolean => MERIDIEM.test(source.slice(time.start, time.end));
26
+
27
+ /** 午前・午後を書かず、十二時間制でも読める時刻(1:00〜12:59)。 */
28
+ const isTwelveHour = (time: TimeSpan): boolean => time.seconds >= ONE_OCLOCK_SECONDS && time.seconds < LAST_TWELVE_HOUR_SECONDS;
29
+
30
+ /** 午前・午後を書かない時刻を、もう片方の午前・午後で読む。 */
31
+ const borrowed = (bare: number, written: number): number => {
32
+ const afternoon = written >= NOON_SECONDS;
33
+ if (afternoon && bare < NOON_SECONDS) return bare + NOON_SECONDS;
34
+ if (!afternoon && bare >= NOON_SECONDS) return bare - NOON_SECONDS;
35
+ return bare;
36
+ };
37
+
38
+ type Reading = { readonly from: number; readonly to: number; readonly borrowed: boolean };
39
+
40
+ /** 範囲の二つの時刻を秒で。片方にだけ午前・午後があれば、もう片方もそれで読む。 */
41
+ export const rangeReading = (source: string, start: TimeSpan, end: TimeSpan): Reading => {
42
+ const [startMarked, endMarked] = [hasMeridiem(source, start), hasMeridiem(source, end)];
43
+ if (endMarked && !startMarked && isTwelveHour(start)) return { from: borrowed(start.seconds, end.seconds), to: end.seconds, borrowed: true };
44
+ if (startMarked && !endMarked && isTwelveHour(end)) return { from: start.seconds, to: borrowed(end.seconds, start.seconds), borrowed: true };
45
+ return { from: start.seconds, to: end.seconds, borrowed: false };
46
+ };
47
+
48
+ /** 終わりが始まりより前で、その戻りが打ち間違いの幅に収まるか。 */
49
+ export const isReversed = (reading: Reading): boolean => {
50
+ const back = reading.from - reading.to;
51
+ return back > 0 && back < (reading.borrowed ? BORROWED_SLIP_SECONDS : SLIP_SECONDS);
52
+ };
53
+
54
+ const asDated = (time: TimeSpan): { offset: number; end: number; value: string } => ({ offset: time.start, end: time.end, value: "" });
55
+
56
+ /** 隣り合う二つの時刻のうち、範囲をなし、終わりが始まりより前のもの。範囲の書き出しを指し、書いたままの範囲を見せる。 */
57
+ export const reversedTimeRanges = (source: string, times: readonly TimeSpan[], range: RangeWords, words: TimeWords): StructureIssue[] =>
58
+ times.slice(1).flatMap((end, index) => {
59
+ const start = times[index];
60
+ if (start === undefined || !isRange(source, asDated(start), asDated(end), range, () => false)) return [];
61
+ if (dayShiftOf(source.slice(start.end, end.start), source, end.end, words) !== 0 || !isReversed(rangeReading(source, start, end))) return [];
62
+ const written = source.slice(start.start, end.end).replace(/\s+/gu, " ");
63
+ return [{ offset: start.start, values: { range: written, start: source.slice(start.start, start.end), end: source.slice(end.start, end.end) } }];
64
+ });
@@ -0,0 +1,63 @@
1
+ import { isTotalLabel, leadingText } from "./total.ts";
2
+ import { outsideBrackets } from "./connection-times.ts";
3
+
4
+ /** 合計の語に添える語。position が before なら合計の語の前だけ、after なら後ろだけ、無ければどちらにも置ける。 */
5
+ export type TotalQualifier = { readonly pattern: string; readonly position?: "before" | "after" | undefined };
6
+
7
+ /** labels: 合計の語(total-label)。qualifiers: 合計の語の前後に置いても合計の行のままの語(total-label-qualifier)。 */
8
+ export type TotalLineWords = { readonly labels: readonly string[]; readonly qualifiers: readonly TotalQualifier[] };
9
+
10
+ /** 見出しの終わり。区切り(: |)があればそこまで、無ければ金額の始まりまで。 */
11
+ const SEPARATOR = /[::|]/u;
12
+ const AMOUNT_START = /[$€£¥¥\p{N}]/u;
13
+ const EMPHASIS = /[*_]/gu;
14
+ const SPACES = /\s+/u;
15
+
16
+ const wordsOf = (text: string): string[] =>
17
+ text
18
+ .toLowerCase()
19
+ .split(SPACES)
20
+ .filter((word) => word.length > 0);
21
+
22
+ const labelText = (lead: string): string => {
23
+ const separator = lead.search(SEPARATOR);
24
+ if (separator !== -1) return lead.slice(0, separator);
25
+ const amount = lead.search(AMOUNT_START);
26
+ return amount === -1 ? lead : lead.slice(0, amount);
27
+ };
28
+
29
+ /** 行頭の見出しを語に分ける。括弧の注記((incl. tax))と強調の印は除く。 */
30
+ const labelWords = (text: string): string[] => wordsOf(outsideBrackets(labelText(leadingText(text))).replace(EMPHASIS, ""));
31
+
32
+ const startsAt = (words: readonly string[], label: readonly string[], index: number): boolean => label.every((word, offset) => words[index + offset] === word);
33
+
34
+ const allowedOn = (side: "before" | "after", qualifiers: readonly TotalQualifier[]): ReadonlySet<string> =>
35
+ new Set(
36
+ qualifiers.filter((qualifier) => qualifier.position !== (side === "before" ? "after" : "before")).map((qualifier) => qualifier.pattern.toLowerCase()),
37
+ );
38
+
39
+ /** 見出しの語のどこかに合計の語が一つ続けてあり、その前と後ろの語がどれも、そちら側に置ける添える語か。 */
40
+ const isQualifiedLabel = (words: readonly string[], label: readonly string[], qualifiers: readonly TotalQualifier[]): boolean => {
41
+ const before = allowedOn("before", qualifiers);
42
+ const after = allowedOn("after", qualifiers);
43
+ return (
44
+ label.length > 0 &&
45
+ words.some(
46
+ (_, index) =>
47
+ startsAt(words, label, index) &&
48
+ words.slice(0, index).every((word) => before.has(word)) &&
49
+ words.slice(index + label.length).every((word) => after.has(word)),
50
+ )
51
+ );
52
+ };
53
+
54
+ /**
55
+ * 合計の行か。合計の語で始まる行(Total、合計(月額))に加えて、合計の語に添える語だけを前後に置いた見出し
56
+ * (Total per month、Monthly total、Total due)も合計の行と読む。添える語でない語(Total area)が入れば読まない。
57
+ */
58
+ export const isTotalLine = (text: string, words: TotalLineWords): boolean => {
59
+ if (isTotalLabel(text, words.labels)) return true;
60
+ if (words.qualifiers.length === 0) return false;
61
+ const label = labelWords(text);
62
+ return words.labels.some((total) => isQualifiedLabel(label, wordsOf(total), words.qualifiers));
63
+ };