chaffjs 0.32.0 → 0.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/dist/api.d.ts +7 -1
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js.map +1 -1
- package/dist/cli-text.d.ts.map +1 -1
- package/dist/cli-text.js +6 -4
- package/dist/cli-text.js.map +1 -1
- package/dist/commands/grade.d.ts.map +1 -1
- package/dist/commands/grade.js +66 -21
- package/dist/commands/grade.js.map +1 -1
- package/dist/company-kana-spelling.d.ts +6 -1
- package/dist/company-kana-spelling.d.ts.map +1 -1
- package/dist/company-kana-spelling.js +19 -2
- package/dist/company-kana-spelling.js.map +1 -1
- package/dist/company-name-ending.d.ts +7 -0
- package/dist/company-name-ending.d.ts.map +1 -0
- package/dist/company-name-ending.js +44 -0
- package/dist/company-name-ending.js.map +1 -0
- package/dist/company-names.d.ts +11 -3
- package/dist/company-names.d.ts.map +1 -1
- package/dist/company-names.js +27 -14
- package/dist/company-names.js.map +1 -1
- package/dist/cross-run.d.ts.map +1 -1
- package/dist/cross-run.js +1 -0
- package/dist/cross-run.js.map +1 -1
- package/dist/custom/no-rewrite.d.ts +3 -0
- package/dist/custom/no-rewrite.d.ts.map +1 -0
- package/dist/custom/no-rewrite.js +3 -0
- package/dist/custom/no-rewrite.js.map +1 -0
- package/dist/custom/parse.d.ts.map +1 -1
- package/dist/custom/parse.js +14 -2
- package/dist/custom/parse.js.map +1 -1
- package/dist/detectors/age-words.d.ts +5 -0
- package/dist/detectors/age-words.d.ts.map +1 -0
- package/dist/detectors/age-words.js +8 -0
- package/dist/detectors/age-words.js.map +1 -0
- package/dist/detectors/agreement-slip.d.ts.map +1 -1
- package/dist/detectors/agreement-slip.js +4 -0
- package/dist/detectors/agreement-slip.js.map +1 -1
- package/dist/detectors/annual-pay-mismatch.d.ts.map +1 -1
- package/dist/detectors/annual-pay-mismatch.js +2 -12
- package/dist/detectors/annual-pay-mismatch.js.map +1 -1
- package/dist/detectors/blank-structure.d.ts.map +1 -1
- package/dist/detectors/blank-structure.js +4 -3
- package/dist/detectors/blank-structure.js.map +1 -1
- package/dist/detectors/closing-balance-mismatch.d.ts.map +1 -1
- package/dist/detectors/closing-balance-mismatch.js +1 -0
- package/dist/detectors/closing-balance-mismatch.js.map +1 -1
- package/dist/detectors/code-fences.d.ts +5 -0
- package/dist/detectors/code-fences.d.ts.map +1 -1
- package/dist/detectors/code-fences.js +9 -0
- package/dist/detectors/code-fences.js.map +1 -1
- package/dist/detectors/condition-toki.d.ts +16 -0
- package/dist/detectors/condition-toki.d.ts.map +1 -0
- package/dist/detectors/condition-toki.js +42 -0
- package/dist/detectors/condition-toki.js.map +1 -0
- package/dist/detectors/conjugated-entry.d.ts +17 -0
- package/dist/detectors/conjugated-entry.d.ts.map +1 -0
- package/dist/detectors/conjugated-entry.js +50 -0
- package/dist/detectors/conjugated-entry.js.map +1 -0
- package/dist/detectors/content-morphemes.d.ts +2 -0
- package/dist/detectors/content-morphemes.d.ts.map +1 -1
- package/dist/detectors/content-morphemes.js +1 -1
- package/dist/detectors/content-morphemes.js.map +1 -1
- package/dist/detectors/cross-facts.d.ts.map +1 -1
- package/dist/detectors/cross-facts.js +2 -1
- package/dist/detectors/cross-facts.js.map +1 -1
- package/dist/detectors/custom.d.ts.map +1 -1
- package/dist/detectors/custom.js +8 -2
- package/dist/detectors/custom.js.map +1 -1
- package/dist/detectors/document-shape.d.ts.map +1 -1
- package/dist/detectors/document-shape.js +6 -0
- package/dist/detectors/document-shape.js.map +1 -1
- package/dist/detectors/due-date.d.ts.map +1 -1
- package/dist/detectors/due-date.js +12 -2
- package/dist/detectors/due-date.js.map +1 -1
- package/dist/detectors/fact-consistency.d.ts.map +1 -1
- package/dist/detectors/fact-consistency.js +12 -13
- package/dist/detectors/fact-consistency.js.map +1 -1
- package/dist/detectors/form-line.d.ts +9 -0
- package/dist/detectors/form-line.d.ts.map +1 -0
- package/dist/detectors/form-line.js +36 -0
- package/dist/detectors/form-line.js.map +1 -0
- package/dist/detectors/formula-amount.d.ts +3 -0
- package/dist/detectors/formula-amount.d.ts.map +1 -0
- package/dist/detectors/formula-amount.js +31 -0
- package/dist/detectors/formula-amount.js.map +1 -0
- package/dist/detectors/heading-claim.d.ts +14 -0
- package/dist/detectors/heading-claim.d.ts.map +1 -0
- package/dist/detectors/heading-claim.js +49 -0
- package/dist/detectors/heading-claim.js.map +1 -0
- package/dist/detectors/heading-echo.d.ts.map +1 -1
- package/dist/detectors/heading-echo.js +29 -1
- package/dist/detectors/heading-echo.js.map +1 -1
- package/dist/detectors/installment-total-mismatch.d.ts.map +1 -1
- package/dist/detectors/installment-total-mismatch.js +28 -19
- package/dist/detectors/installment-total-mismatch.js.map +1 -1
- package/dist/detectors/list-parallel.d.ts +5 -2
- package/dist/detectors/list-parallel.d.ts.map +1 -1
- package/dist/detectors/list-parallel.js +45 -7
- package/dist/detectors/list-parallel.js.map +1 -1
- package/dist/detectors/name-variant.d.ts.map +1 -1
- package/dist/detectors/name-variant.js +34 -7
- package/dist/detectors/name-variant.js.map +1 -1
- package/dist/detectors/number-unit.d.ts +10 -0
- package/dist/detectors/number-unit.d.ts.map +1 -1
- package/dist/detectors/number-unit.js +28 -8
- package/dist/detectors/number-unit.js.map +1 -1
- package/dist/detectors/orthographic-variant.d.ts.map +1 -1
- package/dist/detectors/orthographic-variant.js +4 -3
- package/dist/detectors/orthographic-variant.js.map +1 -1
- package/dist/detectors/orthography.d.ts.map +1 -1
- package/dist/detectors/orthography.js +16 -7
- package/dist/detectors/orthography.js.map +1 -1
- package/dist/detectors/per-unit-words.d.ts +7 -0
- package/dist/detectors/per-unit-words.d.ts.map +1 -0
- package/dist/detectors/per-unit-words.js +17 -0
- package/dist/detectors/per-unit-words.js.map +1 -0
- package/dist/detectors/period-multiple-mismatch.d.ts.map +1 -1
- package/dist/detectors/period-multiple-mismatch.js +2 -12
- package/dist/detectors/period-multiple-mismatch.js.map +1 -1
- package/dist/detectors/reference-number-variant.d.ts.map +1 -1
- package/dist/detectors/reference-number-variant.js +21 -3
- package/dist/detectors/reference-number-variant.js.map +1 -1
- package/dist/detectors/registry/condition-toki.d.ts +3 -0
- package/dist/detectors/registry/condition-toki.d.ts.map +1 -0
- package/dist/detectors/registry/condition-toki.js +3 -0
- package/dist/detectors/registry/condition-toki.js.map +1 -0
- package/dist/detectors/registry/formula-amount.d.ts +3 -0
- package/dist/detectors/registry/formula-amount.d.ts.map +1 -0
- package/dist/detectors/registry/formula-amount.js +3 -0
- package/dist/detectors/registry/formula-amount.js.map +1 -0
- package/dist/detectors/rent-multiple-mismatch.d.ts.map +1 -1
- package/dist/detectors/rent-multiple-mismatch.js +2 -12
- package/dist/detectors/rent-multiple-mismatch.js.map +1 -1
- package/dist/detectors/row-sentence-amounts.d.ts.map +1 -1
- package/dist/detectors/row-sentence-amounts.js +5 -10
- package/dist/detectors/row-sentence-amounts.js.map +1 -1
- package/dist/detectors/stated-amounts.d.ts +4 -0
- package/dist/detectors/stated-amounts.d.ts.map +1 -0
- package/dist/detectors/stated-amounts.js +12 -0
- package/dist/detectors/stated-amounts.js.map +1 -0
- package/dist/detectors/step-time-sum.d.ts.map +1 -1
- package/dist/detectors/step-time-sum.js +2 -1
- package/dist/detectors/step-time-sum.js.map +1 -1
- package/dist/detectors/structure-tree.d.ts +1 -1
- package/dist/detectors/structure-tree.d.ts.map +1 -1
- package/dist/detectors/structure-tree.js +7 -2
- package/dist/detectors/structure-tree.js.map +1 -1
- package/dist/detectors/team.d.ts.map +1 -1
- package/dist/detectors/team.js +10 -6
- package/dist/detectors/team.js.map +1 -1
- package/dist/detectors/time-order.d.ts.map +1 -1
- package/dist/detectors/time-order.js +17 -13
- package/dist/detectors/time-order.js.map +1 -1
- package/dist/detectors/unknown-word.d.ts +4 -1
- package/dist/detectors/unknown-word.d.ts.map +1 -1
- package/dist/detectors/unknown-word.js +18 -2
- package/dist/detectors/unknown-word.js.map +1 -1
- package/dist/detectors/use-interval.d.ts +1 -0
- package/dist/detectors/use-interval.d.ts.map +1 -1
- package/dist/detectors/use-interval.js +15 -10
- package/dist/detectors/use-interval.js.map +1 -1
- package/dist/detectors/use-maximum.d.ts.map +1 -1
- package/dist/detectors/use-maximum.js +3 -1
- package/dist/detectors/use-maximum.js.map +1 -1
- package/dist/eval.js +3 -3
- package/dist/eval.js.map +1 -1
- package/dist/extension/document-view.d.ts +2 -2
- package/dist/extension/document-view.d.ts.map +1 -1
- package/dist/extension/document-view.js +17 -8
- package/dist/extension/document-view.js.map +1 -1
- package/dist/extension/entry-genres.d.ts +5 -0
- package/dist/extension/entry-genres.d.ts.map +1 -0
- package/dist/extension/entry-genres.js +13 -0
- package/dist/extension/entry-genres.js.map +1 -0
- package/dist/extension/module-detector.d.ts.map +1 -1
- package/dist/extension/module-detector.js +4 -4
- package/dist/extension/module-detector.js.map +1 -1
- package/dist/extension/plugin-parse.d.ts +1 -1
- package/dist/extension/plugin-parse.d.ts.map +1 -1
- package/dist/extension/plugin-parse.js +24 -7
- package/dist/extension/plugin-parse.js.map +1 -1
- package/dist/extension/problem-text.d.ts.map +1 -1
- package/dist/extension/problem-text.js +2 -0
- package/dist/extension/problem-text.js.map +1 -1
- package/dist/facts/age-values.d.ts.map +1 -1
- package/dist/facts/age-values.js +20 -2
- package/dist/facts/age-values.js.map +1 -1
- package/dist/facts/column-units.d.ts +31 -0
- package/dist/facts/column-units.d.ts.map +1 -0
- package/dist/facts/column-units.js +37 -0
- package/dist/facts/column-units.js.map +1 -0
- package/dist/facts/fact-values.d.ts +4 -0
- package/dist/facts/fact-values.d.ts.map +1 -1
- package/dist/facts/fact-values.js +3 -1
- package/dist/facts/fact-values.js.map +1 -1
- package/dist/facts/labelled-facts.d.ts +10 -0
- package/dist/facts/labelled-facts.d.ts.map +1 -1
- package/dist/facts/labelled-facts.js +6 -1
- package/dist/facts/labelled-facts.js.map +1 -1
- package/dist/facts/per-unit.d.ts +36 -0
- package/dist/facts/per-unit.d.ts.map +1 -0
- package/dist/facts/per-unit.js +56 -0
- package/dist/facts/per-unit.js.map +1 -0
- package/dist/facts/row-sentence-amounts.d.ts +3 -12
- package/dist/facts/row-sentence-amounts.d.ts.map +1 -1
- package/dist/facts/row-sentence-amounts.js +1 -15
- package/dist/facts/row-sentence-amounts.js.map +1 -1
- package/dist/facts/table-facts.d.ts +1 -1
- package/dist/facts/table-facts.d.ts.map +1 -1
- package/dist/facts/table-facts.js +13 -9
- package/dist/facts/table-facts.js.map +1 -1
- package/dist/facts/term-values.d.ts +13 -1
- package/dist/facts/term-values.d.ts.map +1 -1
- package/dist/facts/term-values.js +71 -12
- package/dist/facts/term-values.js.map +1 -1
- package/dist/fix-plan/text.js +2 -2
- package/dist/fix-plan/text.js.map +1 -1
- package/dist/grade/baseline.d.ts +4 -2
- package/dist/grade/baseline.d.ts.map +1 -1
- package/dist/grade/baseline.js +13 -11
- package/dist/grade/baseline.js.map +1 -1
- package/dist/grade/expect-read.d.ts +16 -0
- package/dist/grade/expect-read.d.ts.map +1 -0
- package/dist/grade/expect-read.js +52 -0
- package/dist/grade/expect-read.js.map +1 -0
- package/dist/grade/expect.d.ts +50 -0
- package/dist/grade/expect.d.ts.map +1 -0
- package/dist/grade/expect.js +53 -0
- package/dist/grade/expect.js.map +1 -0
- package/dist/grade/grade-item.d.ts +1 -1
- package/dist/grade/grade-item.d.ts.map +1 -1
- package/dist/grade/grade-item.js +13 -3
- package/dist/grade/grade-item.js.map +1 -1
- package/dist/grade/item.d.ts +9 -1
- package/dist/grade/item.d.ts.map +1 -1
- package/dist/grade/item.js +32 -7
- package/dist/grade/item.js.map +1 -1
- package/dist/grade/result.d.ts +5 -0
- package/dist/grade/result.d.ts.map +1 -1
- package/dist/grade/results-read.d.ts.map +1 -1
- package/dist/grade/results-read.js +4 -2
- package/dist/grade/results-read.js.map +1 -1
- package/dist/grade/rubric-file.d.ts +23 -0
- package/dist/grade/rubric-file.d.ts.map +1 -0
- package/dist/grade/rubric-file.js +35 -0
- package/dist/grade/rubric-file.js.map +1 -0
- package/dist/grade/rubric.d.ts +2 -0
- package/dist/grade/rubric.d.ts.map +1 -1
- package/dist/grade/rubric.js +75 -47
- package/dist/grade/rubric.js.map +1 -1
- package/dist/grade/setup.d.ts +2 -1
- package/dist/grade/setup.d.ts.map +1 -1
- package/dist/grade/setup.js +2 -2
- package/dist/grade/setup.js.map +1 -1
- package/dist/grade/stamp.d.ts +2 -0
- package/dist/grade/stamp.d.ts.map +1 -1
- package/dist/grade/stamp.js +8 -2
- package/dist/grade/stamp.js.map +1 -1
- package/dist/grade/task-rubric.d.ts +16 -0
- package/dist/grade/task-rubric.d.ts.map +1 -0
- package/dist/grade/task-rubric.js +34 -0
- package/dist/grade/task-rubric.js.map +1 -0
- package/dist/grade/text.d.ts +5 -1
- package/dist/grade/text.d.ts.map +1 -1
- package/dist/grade/text.js +16 -4
- package/dist/grade/text.js.map +1 -1
- package/dist/lexicon-genres.d.ts +7 -0
- package/dist/lexicon-genres.d.ts.map +1 -0
- package/dist/lexicon-genres.js +8 -0
- package/dist/lexicon-genres.js.map +1 -0
- package/dist/name-char-reading.d.ts +5 -0
- package/dist/name-char-reading.d.ts.map +1 -1
- package/dist/name-char-reading.js +15 -0
- package/dist/name-char-reading.js.map +1 -1
- package/dist/name-end-letter.d.ts +19 -0
- package/dist/name-end-letter.d.ts.map +1 -0
- package/dist/name-end-letter.js +37 -0
- package/dist/name-end-letter.js.map +1 -0
- package/dist/name-neighbours.d.ts.map +1 -1
- package/dist/name-neighbours.js +2 -1
- package/dist/name-neighbours.js.map +1 -1
- package/dist/name-separator.d.ts +6 -0
- package/dist/name-separator.d.ts.map +1 -1
- package/dist/name-separator.js +29 -0
- package/dist/name-separator.js.map +1 -1
- package/dist/name-variants.d.ts +6 -3
- package/dist/name-variants.d.ts.map +1 -1
- package/dist/name-variants.js +37 -10
- package/dist/name-variants.js.map +1 -1
- package/dist/number-name.d.ts.map +1 -1
- package/dist/number-name.js +11 -2
- package/dist/number-name.js.map +1 -1
- package/dist/plugin.d.ts +6 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/profile/cue-lines.d.ts +6 -0
- package/dist/profile/cue-lines.d.ts.map +1 -0
- package/dist/profile/cue-lines.js +13 -0
- package/dist/profile/cue-lines.js.map +1 -0
- package/dist/profile/parse.d.ts +1 -1
- package/dist/profile/parse.d.ts.map +1 -1
- package/dist/profile/parse.js +8 -2
- package/dist/profile/parse.js.map +1 -1
- package/dist/reference-numbers.d.ts +2 -0
- package/dist/reference-numbers.d.ts.map +1 -1
- package/dist/reference-numbers.js +1 -1
- package/dist/reference-numbers.js.map +1 -1
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +1 -0
- package/dist/run.js.map +1 -1
- package/dist/spaced-person-name.d.ts.map +1 -1
- package/dist/spaced-person-name.js +26 -1
- package/dist/spaced-person-name.js.map +1 -1
- package/dist/spacing-code.d.ts.map +1 -1
- package/dist/spacing-code.js +10 -1
- package/dist/spacing-code.js.map +1 -1
- package/dist/spacing-minority.d.ts +7 -0
- package/dist/spacing-minority.d.ts.map +1 -1
- package/dist/spacing-minority.js +14 -0
- package/dist/spacing-minority.js.map +1 -1
- package/dist/structure/closing-balance.d.ts +9 -0
- package/dist/structure/closing-balance.d.ts.map +1 -1
- package/dist/structure/closing-balance.js +34 -7
- package/dist/structure/closing-balance.js.map +1 -1
- package/dist/structure/date-range.d.ts +3 -2
- package/dist/structure/date-range.d.ts.map +1 -1
- package/dist/structure/date-range.js +4 -4
- package/dist/structure/date-range.js.map +1 -1
- package/dist/structure/definition-use.d.ts.map +1 -1
- package/dist/structure/definition-use.js +20 -1
- package/dist/structure/definition-use.js.map +1 -1
- package/dist/structure/document-context.d.ts +2 -0
- package/dist/structure/document-context.d.ts.map +1 -0
- package/dist/structure/document-context.js +42 -0
- package/dist/structure/document-context.js.map +1 -0
- package/dist/structure/fenced-lines.d.ts +7 -0
- package/dist/structure/fenced-lines.d.ts.map +1 -0
- package/dist/structure/fenced-lines.js +27 -0
- package/dist/structure/fenced-lines.js.map +1 -0
- package/dist/structure/formula-amount.d.ts +38 -0
- package/dist/structure/formula-amount.d.ts.map +1 -0
- package/dist/structure/formula-amount.js +187 -0
- package/dist/structure/formula-amount.js.map +1 -0
- package/dist/structure/installment-fee.d.ts +52 -0
- package/dist/structure/installment-fee.d.ts.map +1 -0
- package/dist/structure/installment-fee.js +110 -0
- package/dist/structure/installment-fee.js.map +1 -0
- package/dist/structure/installment-total.d.ts +7 -0
- package/dist/structure/installment-total.d.ts.map +1 -1
- package/dist/structure/installment-total.js +6 -6
- package/dist/structure/installment-total.js.map +1 -1
- package/dist/structure/labelled-date-order.d.ts +31 -9
- package/dist/structure/labelled-date-order.d.ts.map +1 -1
- package/dist/structure/labelled-date-order.js +60 -15
- package/dist/structure/labelled-date-order.js.map +1 -1
- package/dist/structure/labelled-order.d.ts +7 -1
- package/dist/structure/labelled-order.d.ts.map +1 -1
- package/dist/structure/labelled-order.js +10 -3
- package/dist/structure/labelled-order.js.map +1 -1
- package/dist/structure/line-amount-label.d.ts.map +1 -1
- package/dist/structure/line-amount-label.js +2 -1
- package/dist/structure/line-amount-label.js.map +1 -1
- package/dist/structure/line-amount.d.ts +0 -5
- package/dist/structure/line-amount.d.ts.map +1 -1
- package/dist/structure/line-amount.js +1 -10
- package/dist/structure/line-amount.js.map +1 -1
- package/dist/structure/prose-total.d.ts +8 -0
- package/dist/structure/prose-total.d.ts.map +1 -1
- package/dist/structure/prose-total.js +70 -2
- package/dist/structure/prose-total.js.map +1 -1
- package/dist/structure/runs.d.ts.map +1 -1
- package/dist/structure/runs.js +9 -1
- package/dist/structure/runs.js.map +1 -1
- package/dist/structure/signed-date-line.d.ts +15 -0
- package/dist/structure/signed-date-line.d.ts.map +1 -0
- package/dist/structure/signed-date-line.js +68 -0
- package/dist/structure/signed-date-line.js.map +1 -0
- package/dist/structure/start-end-range.d.ts +30 -0
- package/dist/structure/start-end-range.d.ts.map +1 -0
- package/dist/structure/start-end-range.js +79 -0
- package/dist/structure/start-end-range.js.map +1 -0
- package/dist/structure/tax.d.ts.map +1 -1
- package/dist/structure/tax.js +5 -3
- package/dist/structure/tax.js.map +1 -1
- package/dist/structure/total-line.d.ts.map +1 -1
- package/dist/structure/total-line.js +10 -6
- package/dist/structure/total-line.js.map +1 -1
- package/dist/structure/total.d.ts +2 -0
- package/dist/structure/total.d.ts.map +1 -1
- package/dist/structure/total.js +34 -3
- package/dist/structure/total.js.map +1 -1
- package/dist/subject-codes.d.ts +21 -0
- package/dist/subject-codes.d.ts.map +1 -0
- package/dist/subject-codes.js +79 -0
- package/dist/subject-codes.js.map +1 -0
- package/dist/unlabelled-references.d.ts +6 -2
- package/dist/unlabelled-references.d.ts.map +1 -1
- package/dist/unlabelled-references.js +11 -10
- package/dist/unlabelled-references.js.map +1 -1
- package/genres.yaml +17 -0
- package/package.json +4 -4
- package/profiles/listing.yaml +1 -1
- package/profiles/property-sale.yaml +21 -0
- package/rules/closing-balance-mismatch.yaml +3 -3
- package/rules/date-range-reversed.yaml +4 -4
- package/rules/due-before-issue.yaml +9 -5
- package/rules/fact-conflict.yaml +2 -2
- package/rules/formula-amount-mismatch.yaml +67 -0
- package/rules/heading-echo.yaml +2 -2
- package/rules/installment-total-mismatch.yaml +9 -4
- package/rules/internal-jargon.yaml +2 -2
- package/rules/latin-spacing.yaml +4 -4
- package/rules/list-item-form-mix.yaml +1 -1
- package/rules/name-variant.yaml +5 -2
- package/rules/no-lead.yaml +2 -2
- package/rules/number-without-unit.yaml +2 -2
- package/rules/orthographic-variant.yaml +3 -3
- package/rules/reference-number-variant.yaml +3 -3
- package/rules/toki-as-condition.yaml +71 -0
- package/rules/total-mismatch.yaml +4 -4
- package/rules/unknown-word.yaml +2 -2
- package/rules/use-before-definition.yaml +2 -2
- package/src/api.ts +11 -2
- package/src/cli-text.ts +6 -4
- package/src/commands/grade.ts +75 -23
- package/src/company-kana-spelling.ts +22 -2
- package/src/company-name-ending.ts +50 -0
- package/src/company-names.ts +52 -17
- package/src/cross-run.ts +1 -0
- package/src/custom/no-rewrite.ts +2 -0
- package/src/custom/parse.ts +18 -2
- package/src/detectors/age-words.ts +12 -0
- package/src/detectors/agreement-slip.ts +3 -0
- package/src/detectors/annual-pay-mismatch.ts +3 -18
- package/src/detectors/blank-structure.ts +4 -3
- package/src/detectors/closing-balance-mismatch.ts +1 -0
- package/src/detectors/code-fences.ts +10 -0
- package/src/detectors/condition-toki.ts +60 -0
- package/src/detectors/conjugated-entry.ts +53 -0
- package/src/detectors/content-morphemes.ts +2 -1
- package/src/detectors/cross-facts.ts +2 -1
- package/src/detectors/custom.ts +9 -2
- package/src/detectors/document-shape.ts +8 -0
- package/src/detectors/due-date.ts +17 -7
- package/src/detectors/fact-consistency.ts +13 -17
- package/src/detectors/form-line.ts +47 -0
- package/src/detectors/formula-amount.ts +43 -0
- package/src/detectors/heading-claim.ts +76 -0
- package/src/detectors/heading-echo.ts +32 -1
- package/src/detectors/installment-total-mismatch.ts +35 -24
- package/src/detectors/list-parallel.ts +46 -7
- package/src/detectors/name-variant.ts +45 -8
- package/src/detectors/number-unit.ts +35 -9
- package/src/detectors/orthographic-variant.ts +5 -3
- package/src/detectors/orthography.ts +17 -8
- package/src/detectors/per-unit-words.ts +24 -0
- package/src/detectors/period-multiple-mismatch.ts +2 -18
- package/src/detectors/reference-number-variant.ts +27 -5
- package/src/detectors/registry/condition-toki.ts +4 -0
- package/src/detectors/registry/formula-amount.ts +4 -0
- package/src/detectors/rent-multiple-mismatch.ts +2 -18
- package/src/detectors/row-sentence-amounts.ts +5 -20
- package/src/detectors/stated-amounts.ts +20 -0
- package/src/detectors/step-time-sum.ts +2 -1
- package/src/detectors/structure-tree.ts +7 -2
- package/src/detectors/team.ts +13 -7
- package/src/detectors/time-order.ts +6 -3
- package/src/detectors/unknown-word.ts +20 -2
- package/src/detectors/use-interval.ts +8 -4
- package/src/detectors/use-maximum.ts +3 -1
- package/src/eval.ts +3 -3
- package/src/extension/document-view.ts +18 -9
- package/src/extension/entry-genres.ts +15 -0
- package/src/extension/module-detector.ts +9 -4
- package/src/extension/plugin-parse.ts +29 -7
- package/src/extension/problem-text.ts +4 -0
- package/src/facts/age-values.ts +24 -2
- package/src/facts/column-units.ts +67 -0
- package/src/facts/fact-values.ts +11 -1
- package/src/facts/labelled-facts.ts +16 -2
- package/src/facts/per-unit.ts +105 -0
- package/src/facts/row-sentence-amounts.ts +4 -26
- package/src/facts/table-facts.ts +7 -4
- package/src/facts/term-values.ts +94 -13
- package/src/fix-plan/text.ts +2 -2
- package/src/grade/baseline.ts +20 -12
- package/src/grade/expect-read.ts +65 -0
- package/src/grade/expect.ts +74 -0
- package/src/grade/grade-item.ts +14 -3
- package/src/grade/item.ts +48 -8
- package/src/grade/result.ts +5 -0
- package/src/grade/results-read.ts +7 -2
- package/src/grade/rubric-file.ts +46 -0
- package/src/grade/rubric.ts +77 -47
- package/src/grade/setup.ts +3 -3
- package/src/grade/stamp.ts +10 -2
- package/src/grade/task-rubric.ts +39 -0
- package/src/grade/text.ts +27 -5
- package/src/lexicon-genres.ts +11 -0
- package/src/name-char-reading.ts +16 -0
- package/src/name-end-letter.ts +52 -0
- package/src/name-neighbours.ts +2 -1
- package/src/name-separator.ts +32 -0
- package/src/name-variants.ts +54 -13
- package/src/number-name.ts +12 -3
- package/src/plugin.ts +6 -0
- package/src/profile/cue-lines.ts +14 -0
- package/src/profile/parse.ts +11 -2
- package/src/reference-numbers.ts +1 -1
- package/src/run.ts +1 -0
- package/src/spaced-person-name.ts +27 -2
- package/src/spacing-code.ts +13 -2
- package/src/spacing-minority.ts +17 -0
- package/src/structure/closing-balance.ts +46 -8
- package/src/structure/date-range.ts +4 -3
- package/src/structure/definition-use.ts +24 -1
- package/src/structure/document-context.ts +46 -0
- package/src/structure/fenced-lines.ts +28 -0
- package/src/structure/formula-amount.ts +236 -0
- package/src/structure/installment-fee.ts +201 -0
- package/src/structure/installment-total.ts +6 -6
- package/src/structure/labelled-date-order.ts +105 -21
- package/src/structure/labelled-order.ts +21 -10
- package/src/structure/line-amount-label.ts +2 -1
- package/src/structure/line-amount.ts +1 -11
- package/src/structure/prose-total.ts +81 -2
- package/src/structure/runs.ts +10 -1
- package/src/structure/signed-date-line.ts +78 -0
- package/src/structure/start-end-range.ts +120 -0
- package/src/structure/tax.ts +6 -3
- package/src/structure/total-line.ts +12 -6
- package/src/structure/total.ts +42 -3
- package/src/subject-codes.ts +105 -0
- package/src/unlabelled-references.ts +24 -16
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { codeFences } from "../detectors/code-fences.ts";
|
|
2
|
+
|
|
3
|
+
const FENCE_MARK = /`{3}|~{3}/u;
|
|
4
|
+
|
|
5
|
+
const NONE: ReadonlySet<number> = new Set();
|
|
6
|
+
|
|
7
|
+
/** The index of the line holding an offset of the joined lines. */
|
|
8
|
+
const lineIndexAt = (starts: readonly number[], offset: number): number => starts.findLastIndex((start) => start <= offset);
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The indexes of the lines of fenced code blocks (``` or ~~~), the fences included. A table or a list written in one is an
|
|
12
|
+
* example, not the document's own values. The blocks are the ones the Markdown parser reads (code-fences.ts): a fence in
|
|
13
|
+
* a list item is one, a line indented as code or a fence in a quotation is not, and an unclosed fence runs to the end.
|
|
14
|
+
*/
|
|
15
|
+
export const fencedLineIndexes = (texts: readonly string[]): ReadonlySet<number> => {
|
|
16
|
+
if (!texts.some((text) => FENCE_MARK.test(text))) return NONE;
|
|
17
|
+
const starts: number[] = [];
|
|
18
|
+
texts.reduce((start, text) => {
|
|
19
|
+
starts.push(start);
|
|
20
|
+
return start + text.length + 1;
|
|
21
|
+
}, 0);
|
|
22
|
+
const fenced = new Set<number>();
|
|
23
|
+
codeFences(texts.join("\n")).forEach((fence) => {
|
|
24
|
+
const last = lineIndexAt(starts, Math.max(fence.start, fence.end - 1));
|
|
25
|
+
for (let index = lineIndexAt(starts, fence.start); index <= last; index += 1) fenced.add(index);
|
|
26
|
+
});
|
|
27
|
+
return fenced;
|
|
28
|
+
};
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import type { StructureIssue } from "./issues.ts";
|
|
2
|
+
import { linesOf, type Line } from "./lines.ts";
|
|
3
|
+
import { alternation, intervalOf, type Interval, type StatedAmount } from "./stated-multiple.ts";
|
|
4
|
+
import { statedTaxRates, type TaxPriceWord } from "./tax-price.ts";
|
|
5
|
+
import { roundedMatches } from "./tax.ts";
|
|
6
|
+
import { escapeRegExp } from "../orthography.ts";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* formula-amount-mismatch: a row whose label writes the formula of its amount (仲介手数料(売買代金の3%+60,000円、消費税込み)
|
|
10
|
+
* | 1,650,000円, "Commission (5% of the sale price) | $16,200.00") is worked out from the amount the document labels with that
|
|
11
|
+
* name (売買代金 | 48,000,000円) and compared with the row's amount. A label that says the tax is included adds the rate
|
|
12
|
+
* the label states, else the one rate the document states beside a tax word, else the profile's rate. The words come from the
|
|
13
|
+
* lexicons. Pure.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export type FormulaWords = {
|
|
17
|
+
/** Words between a name and its rate (売買代金の3%). */
|
|
18
|
+
readonly nameRate: readonly string[];
|
|
19
|
+
/** Words between a rate and its name (3% of the sale price). */
|
|
20
|
+
readonly rateName: readonly string[];
|
|
21
|
+
/** Words before a fixed amount added to the rate's part (+60,000円, plus $500). */
|
|
22
|
+
readonly plus: readonly string[];
|
|
23
|
+
/** The marks of a percent (%, %, percent). */
|
|
24
|
+
readonly percent: readonly string[];
|
|
25
|
+
/** The words of a price with tax and of a tax rate (税込み, 消費税), as tax-mismatch reads them. */
|
|
26
|
+
readonly tax: readonly TaxPriceWord[];
|
|
27
|
+
/** The names of a tax (消費税, VAT) and the words that say it is included (込み, included): 消費税8%込み, VAT included. */
|
|
28
|
+
readonly taxNames: readonly string[];
|
|
29
|
+
readonly includedWords: readonly string[];
|
|
30
|
+
/** Words that say a tax is left out although a word of inclusion stands in them (not included, 含まない). */
|
|
31
|
+
readonly notIncluded: readonly string[];
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type FormulaText = {
|
|
35
|
+
readonly text: string;
|
|
36
|
+
readonly amounts: readonly StatedAmount[];
|
|
37
|
+
/** The tax rate a label's tax-included word means when neither the label nor the document states one (the profile's). */
|
|
38
|
+
readonly defaultTaxRate: number | undefined;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/** The rate and the name it is of, with where they stand in the label. */
|
|
42
|
+
type Formula = { readonly start: number; readonly end: number; readonly rate: number; readonly name: string };
|
|
43
|
+
type Named = Omit<Formula, "rate">;
|
|
44
|
+
|
|
45
|
+
/** A line ending in one amount after a separator, and the label before that separator. */
|
|
46
|
+
type Row = { readonly line: Line; readonly label: string; readonly amount: StatedAmount };
|
|
47
|
+
|
|
48
|
+
/** What the row's amount is worked out from. */
|
|
49
|
+
type Terms = { readonly formula: Formula; readonly base: StatedAmount; readonly fixed: StatedAmount | undefined; readonly tax: number };
|
|
50
|
+
|
|
51
|
+
const CENTS = 100;
|
|
52
|
+
const HUNDRED = 100;
|
|
53
|
+
const NAME_MAX = 40;
|
|
54
|
+
const AFTER_AMOUNT = /^[\s||]*$/u;
|
|
55
|
+
const SEPARATOR = /[||::]\s*$/u;
|
|
56
|
+
const NAME_EDGE = "()()、,,;;||::「」『』";
|
|
57
|
+
const LAST_EDGE = new RegExp(`[${escapeRegExp(NAME_EDGE)}][^${escapeRegExp(NAME_EDGE)}]*$`, "u");
|
|
58
|
+
const LIST_MARK = /^(?:[-*+・]|\d+[.)])\s+/u;
|
|
59
|
+
const DIGITS = /[0-90-9]/u;
|
|
60
|
+
const NUMBER = /\d[\d,]*(?:\.\d+)?/u;
|
|
61
|
+
/** Four digits or more with no comma: the row groups none, so the expected amount is not grouped either. */
|
|
62
|
+
const UNGROUPED = /\d{4}/u;
|
|
63
|
+
|
|
64
|
+
const numberOf = (written: string): number => Number(written.normalize("NFKC"));
|
|
65
|
+
|
|
66
|
+
const percentPattern = (words: FormulaWords): RegExp | undefined => {
|
|
67
|
+
const marks = alternation(words.percent);
|
|
68
|
+
return marks === undefined ? undefined : new RegExp(`(?<![0-90-9.,.,])([0-90-9]+(?:[..][0-90-9]+)?)\\s?(?:${marks})`, "giu");
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const amountsWithin = (amounts: readonly StatedAmount[], start: number, end: number): StatedAmount[] =>
|
|
72
|
+
amounts.filter((amount) => amount.offset >= start && amount.end <= end);
|
|
73
|
+
|
|
74
|
+
/** The line's last amount, with only a separator before it and nothing after it, and the text before that separator. */
|
|
75
|
+
const rowOf = (text: string, line: Line, amounts: readonly StatedAmount[]): Row | undefined => {
|
|
76
|
+
const end = line.start + line.text.length;
|
|
77
|
+
const amount = amountsWithin(amounts, line.start, end).at(-1);
|
|
78
|
+
if (amount === undefined || amount.scale !== undefined || !AFTER_AMOUNT.test(text.slice(amount.end, end))) return undefined;
|
|
79
|
+
const before = text.slice(line.start, amount.offset);
|
|
80
|
+
const separator = SEPARATOR.exec(before);
|
|
81
|
+
return separator === null ? undefined : { line, label: before.slice(0, separator.index), amount };
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const isName = (name: string): boolean => name !== "" && name.length <= NAME_MAX && !DIGITS.test(name);
|
|
85
|
+
|
|
86
|
+
/** 売買代金の3%: the name runs back from the word to the nearest bracket, comma or cell edge. */
|
|
87
|
+
const nameBefore = (label: string, rateStart: number, words: FormulaWords): Named | undefined => {
|
|
88
|
+
const head = label.slice(0, rateStart).trimEnd();
|
|
89
|
+
const word = words.nameRate.find((each) => head.toLowerCase().endsWith(each.toLowerCase()));
|
|
90
|
+
if (word === undefined) return undefined;
|
|
91
|
+
const upTo = head.slice(0, head.length - word.length);
|
|
92
|
+
const edge = LAST_EDGE.exec(upTo);
|
|
93
|
+
const from = edge === null ? 0 : edge.index + 1;
|
|
94
|
+
const raw = upTo.slice(from).replace(LIST_MARK, "");
|
|
95
|
+
const name = raw.trim();
|
|
96
|
+
return isName(name) ? { start: upTo.length - raw.trimStart().length, end: rateStart, name } : undefined;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/** 3% of the sale price: the name runs on from the word to a bracket, a comma, a plus word or a tax phrase (incl. tax). */
|
|
100
|
+
const nameAfter = (label: string, rateEnd: number, words: FormulaWords): Named | undefined => {
|
|
101
|
+
const joiner = alternation(words.rateName);
|
|
102
|
+
const word = joiner === undefined ? null : new RegExp(`^\\s*(?:${joiner})\\s+`, "iu").exec(label.slice(rateEnd));
|
|
103
|
+
if (word === null) return undefined;
|
|
104
|
+
const from = rateEnd + word[0].length;
|
|
105
|
+
const taxPhrases = words.tax.filter((each) => each.role !== "rate").map((each) => each.pattern);
|
|
106
|
+
const stops = [`[${escapeRegExp(NAME_EDGE)}]`, alternation([...words.plus, ...taxPhrases]) ?? "(?!)"];
|
|
107
|
+
const stop = new RegExp(stops.join("|"), "iu").exec(label.slice(from));
|
|
108
|
+
const name = label.slice(from, stop === null ? label.length : from + stop.index).trim();
|
|
109
|
+
return isName(name) ? { start: from, end: from + name.length, name } : undefined;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/** The one rate in the label that names what it is of; two such rates are not read. */
|
|
113
|
+
const formulaIn = (label: string, words: FormulaWords): Formula | undefined => {
|
|
114
|
+
const pattern = percentPattern(words);
|
|
115
|
+
if (pattern === undefined) return undefined;
|
|
116
|
+
const found = [...label.matchAll(pattern)].flatMap((match) => {
|
|
117
|
+
const rateEnd = match.index + match[0].length;
|
|
118
|
+
const named = nameBefore(label, match.index, words) ?? nameAfter(label, rateEnd, words);
|
|
119
|
+
if (named === undefined) return [];
|
|
120
|
+
return [{ start: Math.min(named.start, match.index), end: Math.max(named.end, rateEnd), rate: numberOf(match[1] ?? ""), name: named.name }];
|
|
121
|
+
});
|
|
122
|
+
const others = [...label.matchAll(pattern)].length - found.length;
|
|
123
|
+
return found.length === 1 && others <= statedTaxRates([label], words.tax).length ? found[0] : undefined;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The amount right after a plus word after the formula (+60,000円), or none. Undefined when the label holds any other
|
|
128
|
+
* amount, or a plus word with no amount after it: the formula is then not all written in the label.
|
|
129
|
+
*/
|
|
130
|
+
const fixedOf = (text: string, row: Row, formula: Formula, amounts: readonly StatedAmount[], words: FormulaWords): StatedAmount[] | undefined => {
|
|
131
|
+
const inLabel = amountsWithin(amounts, row.line.start, row.line.start + row.label.length);
|
|
132
|
+
const plus = alternation(words.plus);
|
|
133
|
+
const after = row.line.start + formula.end;
|
|
134
|
+
const gap = plus === undefined ? null : new RegExp(`^\\s*(?:${plus})\\s*`, "iu").exec(text.slice(after, row.line.start + row.label.length));
|
|
135
|
+
const fixed = gap === null ? undefined : inLabel.find((amount) => amount.offset === after + gap[0].length);
|
|
136
|
+
if (gap !== null && fixed === undefined) return undefined;
|
|
137
|
+
const parts = fixed === undefined ? [] : [fixed];
|
|
138
|
+
return inLabel.length === parts.length ? parts : undefined;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* A word of a price with tax (税込み, incl. tax), or a tax's name with a word of inclusion (消費税8%込み, VAT included); not
|
|
143
|
+
* when the label also says the tax is left out (税別, excl. tax, tax not included).
|
|
144
|
+
*/
|
|
145
|
+
const isTaxIncluded = (label: string, words: FormulaWords): boolean => {
|
|
146
|
+
const lower = label.normalize("NFKC").toLowerCase();
|
|
147
|
+
const holds = (word: string): boolean => lower.includes(word.normalize("NFKC").toLowerCase());
|
|
148
|
+
const roleHeld = (role: TaxPriceWord["role"]): boolean => words.tax.some((word) => word.role === role && holds(word.pattern));
|
|
149
|
+
if (roleHeld("excluded") || words.notIncluded.some(holds)) return false;
|
|
150
|
+
return roleHeld("included") || (words.taxNames.some(holds) && words.includedWords.some(holds));
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
/** 0 without a tax-included word; else the label's one rate, the document's one rate, or the profile's. Undefined when unknown. */
|
|
154
|
+
const taxRateOf = (row: Row, others: readonly string[], text: FormulaText, words: FormulaWords): number | undefined => {
|
|
155
|
+
if (!isTaxIncluded(row.label, words)) return 0;
|
|
156
|
+
const inLabel = statedTaxRates([row.label], words.tax);
|
|
157
|
+
if (inLabel.length > 0) return inLabel.length === 1 ? inLabel[0] : undefined;
|
|
158
|
+
const stated = statedTaxRates(others, words.tax);
|
|
159
|
+
if (stated.length > 0) return stated.length === 1 ? stated[0] : undefined;
|
|
160
|
+
return text.defaultTaxRate;
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
/** The amount the document labels with the name (売買代金 | 48,000,000円, Sale price: $324,000), when every such label agrees. */
|
|
164
|
+
const baseOf = (text: string, lines: readonly Line[], row: Row, name: string, amounts: readonly StatedAmount[]): StatedAmount | undefined => {
|
|
165
|
+
const label = new RegExp(`^[ \\t]*(?:[-*+][ \\t]+)?(?:[||][ \\t]*)?(?:\\*\\*)?${escapeRegExp(name)}(?:\\*\\*)?[ \\t]*[||::][ \\t]*`, "iu");
|
|
166
|
+
const found = lines.flatMap((line) => {
|
|
167
|
+
const match = line === row.line ? null : label.exec(line.text);
|
|
168
|
+
const amount = match === null ? undefined : amounts.find((each) => each.offset === line.start + match[0].length);
|
|
169
|
+
return amount === undefined || amount.currency !== row.amount.currency ? [] : [amount];
|
|
170
|
+
});
|
|
171
|
+
const [first] = found;
|
|
172
|
+
return found.every((amount) => amount.value === first?.value && amount.scale === first.scale) ? first : undefined;
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
/** Before tax: rate × base + fixed. The base is the interval its written digits allow; a formula's fixed part is exact (+6万円 is 60,000). */
|
|
176
|
+
const beforeTax = (text: string, terms: Terms): Interval => {
|
|
177
|
+
const base = intervalOf(terms.base, text.slice(terms.base.offset, terms.base.end));
|
|
178
|
+
const fixed = terms.fixed?.value ?? 0;
|
|
179
|
+
return { low: (base.low * terms.formula.rate) / HUNDRED + fixed, high: (base.high * terms.formula.rate) / HUNDRED + fixed };
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const ROUNDINGS: readonly ((value: number) => number)[] = [(value) => value, Math.floor, Math.round, Math.ceil];
|
|
183
|
+
|
|
184
|
+
/** Whether the written amount is the formula's, rounded down, to nearest or up, before tax or after it, to the unit or the cent. */
|
|
185
|
+
export const formulaMatches = (written: number, before: Interval, tax: number): boolean => {
|
|
186
|
+
const withTax = (value: number): number => (value * (HUNDRED + tax)) / HUNDRED;
|
|
187
|
+
// A base written with a word of scale (4,800万円) is a range; any amount in it, rounded to the unit, is right.
|
|
188
|
+
if (before.low < before.high && written >= Math.floor(withTax(before.low)) && written <= Math.ceil(withTax(before.high))) return true;
|
|
189
|
+
const candidates = [before.low, before.high].flatMap((value) => ROUNDINGS.map((round) => withTax(round(value))));
|
|
190
|
+
return candidates.some((candidate) => roundedMatches(Math.round(written * CENTS), candidate * CENTS));
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
/** The expected amount as the row writes it: its unit, its grouping and its decimals. */
|
|
194
|
+
const shownLike = (written: string, value: number): string => {
|
|
195
|
+
const decimals = NUMBER.exec(written.normalize("NFKC"))?.[0].split(".")[1]?.length ?? 0;
|
|
196
|
+
const grouped = written.includes(",") || !UNGROUPED.test(written.normalize("NFKC"));
|
|
197
|
+
const number = value.toLocaleString("en-US", { minimumFractionDigits: decimals, maximumFractionDigits: decimals, useGrouping: grouped });
|
|
198
|
+
return written.normalize("NFKC").replace(NUMBER, number);
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const issueOf = (text: string, row: Row, terms: Terms): StructureIssue[] => {
|
|
202
|
+
const before = beforeTax(text, terms);
|
|
203
|
+
if (formulaMatches(row.amount.value, before, terms.tax)) return [];
|
|
204
|
+
const expected = (((before.low + before.high) / 2) * (HUNDRED + terms.tax)) / HUNDRED;
|
|
205
|
+
const values = {
|
|
206
|
+
written: text.slice(row.amount.offset, row.amount.end),
|
|
207
|
+
expected: shownLike(text.slice(row.amount.offset, row.amount.end), Math.round(expected)),
|
|
208
|
+
formula: text.slice(row.line.start + terms.formula.start, terms.fixed?.end ?? row.line.start + terms.formula.end),
|
|
209
|
+
name: terms.formula.name,
|
|
210
|
+
base: text.slice(terms.base.offset, terms.base.end),
|
|
211
|
+
};
|
|
212
|
+
return [{ offset: row.amount.offset, values }];
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const termsOf = (input: FormulaText, lines: readonly Line[], row: Row, words: FormulaWords): Terms | undefined => {
|
|
216
|
+
const formula = formulaIn(row.label, words);
|
|
217
|
+
if (formula === undefined) return undefined;
|
|
218
|
+
const fixedList = fixedOf(input.text, row, formula, input.amounts, words);
|
|
219
|
+
const [fixed] = fixedList ?? [];
|
|
220
|
+
if (fixedList === undefined || (fixed !== undefined && fixed.currency !== row.amount.currency)) return undefined;
|
|
221
|
+
const base = baseOf(input.text, lines, row, formula.name, input.amounts);
|
|
222
|
+
const others = lines.filter((line) => line !== row.line).map((line) => line.text);
|
|
223
|
+
const tax = taxRateOf(row, others, input, words);
|
|
224
|
+
return base === undefined || tax === undefined ? undefined : { formula, base, fixed, tax };
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
/** Every row whose label writes its formula and whose amount is not what the formula gives on the named amount. */
|
|
228
|
+
export const formulaAmountMismatches = (input: FormulaText, words: FormulaWords): StructureIssue[] => {
|
|
229
|
+
if (words.percent.length === 0 || words.nameRate.length + words.rateName.length === 0) return [];
|
|
230
|
+
const lines = linesOf(input.text);
|
|
231
|
+
return lines.flatMap((line) => {
|
|
232
|
+
const row = rowOf(input.text, line, input.amounts);
|
|
233
|
+
const terms = row === undefined ? undefined : termsOf(input, lines, row, words);
|
|
234
|
+
return row === undefined || terms === undefined ? [] : issueOf(input.text, row, terms);
|
|
235
|
+
});
|
|
236
|
+
};
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { escapeRegExp } from "../orthography.ts";
|
|
2
|
+
import type { StructureIssue } from "./issues.ts";
|
|
3
|
+
import {
|
|
4
|
+
alternation,
|
|
5
|
+
describe,
|
|
6
|
+
hasWord,
|
|
7
|
+
intervalOf,
|
|
8
|
+
isPlainGap,
|
|
9
|
+
isRough,
|
|
10
|
+
isTotalOf,
|
|
11
|
+
lineBounds,
|
|
12
|
+
occurrences,
|
|
13
|
+
sentenceAround,
|
|
14
|
+
sumOf,
|
|
15
|
+
writtenLike,
|
|
16
|
+
type InstallmentAmount,
|
|
17
|
+
type InstallmentWords,
|
|
18
|
+
type Part,
|
|
19
|
+
} from "./installment-total.ts";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* installment-total-mismatch, the fee form: numbered instalments listed one per row or item (第1回:32,000円, | 1st
|
|
23
|
+
* payment | £150 |, Final payment: $60) that do not add up to the fee stated in the same section (旅行代金は、1人あたり
|
|
24
|
+
* 62,000円; "The trip costs £300 per student"). Compared only when the section states one fee, every instalment of the
|
|
25
|
+
* series is read (numbered 1, 2, … with no gap or repeat, each with one plain amount), the section names a payment
|
|
26
|
+
* (積立金, instalments), and every amount is in the fee's currency. A deposit counts only as an item of the series; one
|
|
27
|
+
* named anywhere else stops the comparison. An instalment as large as the fee says the fee is per payment (a monthly
|
|
28
|
+
* fee), not split, and is not compared. Words come from the lexicons. Pure.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
type Span = { readonly start: number; readonly end: number };
|
|
32
|
+
|
|
33
|
+
export type InstallmentSeriesWords = {
|
|
34
|
+
/** Item labels with {n} for the instalment's number (第{n}回, {n} payment, Instalment {n}). */
|
|
35
|
+
readonly numbered: readonly string[];
|
|
36
|
+
/** Item labels of the last instalment, numbered after the others (最終回, Final payment). */
|
|
37
|
+
readonly finals: readonly string[];
|
|
38
|
+
/** Numbers written as words that {n} may be (first, 一) and their values. */
|
|
39
|
+
readonly ordinals: readonly { readonly word: string; readonly value: number }[];
|
|
40
|
+
/** Letters after a number that make it an ordinal (1st). */
|
|
41
|
+
readonly ordinalSuffixes: readonly string[];
|
|
42
|
+
/** Words that say the section is about paying (積立金, instalment); one must stand in the section. */
|
|
43
|
+
readonly payments: readonly string[];
|
|
44
|
+
/** A deposit (申込金, deposit): an item of the series, or a reason not to compare. */
|
|
45
|
+
readonly deposits: readonly string[];
|
|
46
|
+
/** Words that state the fee (旅行代金, costs). */
|
|
47
|
+
readonly fees: readonly string[];
|
|
48
|
+
/** Words that may stand between a fee word and its amount (1人あたり, per student). */
|
|
49
|
+
readonly feeLinks: readonly string[];
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/** One read item of a series: its number (undefined for a deposit, Infinity for a final one) and its amount. */
|
|
53
|
+
type Item = { readonly line: Span; readonly number: number | undefined; readonly amount: InstallmentAmount | undefined };
|
|
54
|
+
|
|
55
|
+
/** What may stand before an item label on its line: a table bar, a list mark, spaces. */
|
|
56
|
+
const ITEM_LEAD = /^[\s||\-*+・•]*$/u;
|
|
57
|
+
const DIGITS = "[0-90-9]+";
|
|
58
|
+
const FINAL = Number.POSITIVE_INFINITY;
|
|
59
|
+
|
|
60
|
+
const numberPattern = (series: InstallmentSeriesWords): string => {
|
|
61
|
+
const suffix = alternation(series.ordinalSuffixes);
|
|
62
|
+
const words = alternation(series.ordinals.map((entry) => entry.word));
|
|
63
|
+
const digits = suffix === undefined ? DIGITS : `${DIGITS}(?:${suffix})?`;
|
|
64
|
+
return words === undefined ? digits : `${digits}|${words}`;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const numberOf = (written: string, series: InstallmentSeriesWords): number | undefined => {
|
|
68
|
+
const digits = /^[0-90-9]+/u.exec(written)?.[0];
|
|
69
|
+
if (digits !== undefined) return Number(digits.normalize("NFKC"));
|
|
70
|
+
return series.ordinals.find((entry) => entry.word.toLowerCase() === written.toLowerCase())?.value;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const labelRegExp = (label: string, number: string): RegExp => {
|
|
74
|
+
const [before = "", after = ""] = label.split("{n}").map(escapeRegExp);
|
|
75
|
+
const edgeStart = /^[A-Za-z]/u.test(label) ? "(?<![A-Za-z])" : "";
|
|
76
|
+
const edgeEnd = /[A-Za-z]$/u.test(label) ? "(?![A-Za-z])" : "";
|
|
77
|
+
return new RegExp(`${edgeStart}${before}(${number})${after}${edgeEnd}`, "giu");
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
type Label = Span & { readonly number: number | undefined };
|
|
81
|
+
|
|
82
|
+
const numberedLabels = (text: string, series: InstallmentSeriesWords): Label[] =>
|
|
83
|
+
series.numbered.flatMap((label) =>
|
|
84
|
+
[...text.matchAll(labelRegExp(label, numberPattern(series)))].map((match) => ({
|
|
85
|
+
start: match.index,
|
|
86
|
+
end: match.index + match[0].length,
|
|
87
|
+
number: numberOf(match[1] ?? "", series),
|
|
88
|
+
})),
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const wordLabels = (text: string, words: readonly string[], number: number | undefined): Label[] =>
|
|
92
|
+
occurrences(text, words).map((span) => ({ ...span, number }));
|
|
93
|
+
|
|
94
|
+
const startsItem = (text: string, label: Span): boolean => ITEM_LEAD.test(text.slice(lineBounds(text, label.start).start, label.start));
|
|
95
|
+
|
|
96
|
+
/** The longest label at each place wins (Final payment over a bare payment), and only labels that open a row or an item. */
|
|
97
|
+
const itemLabels = (text: string, series: InstallmentSeriesWords): Label[] => {
|
|
98
|
+
const all = [...numberedLabels(text, series), ...wordLabels(text, series.finals, FINAL), ...wordLabels(text, series.deposits, undefined)];
|
|
99
|
+
const longest = all.filter(
|
|
100
|
+
(label) =>
|
|
101
|
+
!all.some((other) => other !== label && other.start <= label.start && other.end >= label.end && other.end - other.start > label.end - label.start),
|
|
102
|
+
);
|
|
103
|
+
return longest.filter((label) => startsItem(text, label)).toSorted((left, right) => left.start - right.start);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/** A label's amount: the one amount of its line, after a gap of separators, links and payment words; else unread. */
|
|
107
|
+
const itemOf = (text: string, label: Label, amounts: readonly InstallmentAmount[], words: InstallmentWords, series: InstallmentSeriesWords): Item => {
|
|
108
|
+
const line = lineBounds(text, label.start);
|
|
109
|
+
const onLine = amounts.filter((amount) => amount.offset >= label.end && amount.end <= line.end);
|
|
110
|
+
const [amount] = onLine;
|
|
111
|
+
const read =
|
|
112
|
+
onLine.length === 1 &&
|
|
113
|
+
amount !== undefined &&
|
|
114
|
+
isPlainGap(text.slice(label.end, amount.offset), [...words.links, ...series.payments]) &&
|
|
115
|
+
!isRough(text, { start: amount.offset, end: amount.end }, words);
|
|
116
|
+
return { line, number: label.number, amount: read ? amount : undefined };
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
/** Whether the numbered items run 1, 2, …, k (a final one only last), each read; deposits may stand anywhere. */
|
|
120
|
+
export const isWholeSeries = (items: readonly Item[]): boolean => {
|
|
121
|
+
const numbers = items.flatMap((item) => (item.number === undefined ? [] : [item.number]));
|
|
122
|
+
const finalAt = numbers.indexOf(FINAL);
|
|
123
|
+
const counted = numbers.filter((number) => number !== FINAL);
|
|
124
|
+
const inOrder = counted.every((number, index) => number === index + 1);
|
|
125
|
+
const finalLast = finalAt === -1 || finalAt === numbers.length - 1;
|
|
126
|
+
return items.length >= 2 && counted.length >= 1 && inOrder && finalLast && items.every((item) => item.amount !== undefined);
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
/** The sections of the text: from one heading to the next; the whole text when there are none. */
|
|
130
|
+
export const sectionsOf = (text: string, headingStarts: readonly number[]): Span[] => {
|
|
131
|
+
const starts = [0, ...headingStarts.filter((start) => start > 0 && start < text.length).toSorted((left, right) => left - right)];
|
|
132
|
+
return starts.map((start, index) => ({ start, end: starts[index + 1] ?? text.length }));
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
/** Every fee the section states: a fee word, then only separators, links and per-person words before an amount. */
|
|
136
|
+
export const feesIn = (
|
|
137
|
+
text: string,
|
|
138
|
+
section: Span,
|
|
139
|
+
amounts: readonly InstallmentAmount[],
|
|
140
|
+
words: InstallmentWords,
|
|
141
|
+
series: InstallmentSeriesWords,
|
|
142
|
+
): InstallmentAmount[] => {
|
|
143
|
+
const body = text.slice(section.start, section.end);
|
|
144
|
+
return occurrences(body, series.fees).flatMap((span) => {
|
|
145
|
+
const at = { start: section.start + span.start, end: section.start + span.end };
|
|
146
|
+
const sentence = sentenceAround(text, at);
|
|
147
|
+
const amount = amounts.find((candidate) => candidate.offset >= at.end && candidate.end <= sentence.end);
|
|
148
|
+
if (amount === undefined || !isPlainGap(text.slice(at.end, amount.offset), [...words.links, ...series.feeLinks])) return [];
|
|
149
|
+
return [amount];
|
|
150
|
+
});
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const within = (span: Span, section: Span): boolean => span.start >= section.start && span.end <= section.end;
|
|
154
|
+
|
|
155
|
+
const sameAmount = (left: InstallmentAmount, right: InstallmentAmount): boolean => left.value === right.value && left.currency === right.currency;
|
|
156
|
+
|
|
157
|
+
const partOf = (text: string, amount: InstallmentAmount): Part => ({ count: 1, amount, written: text.slice(amount.offset, amount.end) });
|
|
158
|
+
|
|
159
|
+
/** The comparison of one section: its series of instalments against its one fee; undefined when anything is not read. */
|
|
160
|
+
const sectionIssue = (
|
|
161
|
+
text: string,
|
|
162
|
+
section: Span,
|
|
163
|
+
items: readonly Item[],
|
|
164
|
+
amounts: readonly InstallmentAmount[],
|
|
165
|
+
words: InstallmentWords,
|
|
166
|
+
series: InstallmentSeriesWords,
|
|
167
|
+
): StructureIssue | undefined => {
|
|
168
|
+
const own = items.filter((item) => within(item.line, section));
|
|
169
|
+
if (!isWholeSeries(own) || !hasWord(text.slice(section.start, section.end), series.payments)) return undefined;
|
|
170
|
+
const fees = feesIn(text, section, amounts, words, series).filter((fee) => !own.some((item) => within({ start: fee.offset, end: fee.end }, item.line)));
|
|
171
|
+
const [fee] = fees;
|
|
172
|
+
if (fee === undefined || !fees.every((other) => sameAmount(other, fee)) || isRough(text, { start: fee.offset, end: fee.end }, words)) return undefined;
|
|
173
|
+
const parts = own.flatMap((item) => (item.amount === undefined ? [] : [partOf(text, item.amount)]));
|
|
174
|
+
const [first] = parts;
|
|
175
|
+
if (first === undefined || parts.some((part) => part.amount.currency !== fee.currency || part.amount.value >= fee.value)) return undefined;
|
|
176
|
+
const written = text.slice(fee.offset, fee.end);
|
|
177
|
+
const sum = sumOf(parts);
|
|
178
|
+
const stated = fee.scale === undefined ? { low: fee.value, high: fee.value } : intervalOf(fee, written);
|
|
179
|
+
if (isTotalOf(stated, sum)) return undefined;
|
|
180
|
+
return { offset: first.amount.offset, values: { payments: describe(parts), sum: writtenLike((sum.low + sum.high) / 2, first), total: written } };
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
/** Whether a deposit is named outside the items of a series: then whether it is part of the fee cannot be read. */
|
|
184
|
+
const depositOutside = (text: string, items: readonly Item[], series: InstallmentSeriesWords): boolean =>
|
|
185
|
+
occurrences(text, series.deposits).some((span) => !items.some((item) => item.number === undefined && within(span, item.line)));
|
|
186
|
+
|
|
187
|
+
/** Every section whose numbered instalments do not add up to its stated fee; reported at the first instalment. */
|
|
188
|
+
export const installmentFeeMismatches = (
|
|
189
|
+
text: string,
|
|
190
|
+
amounts: readonly InstallmentAmount[],
|
|
191
|
+
headingStarts: readonly number[],
|
|
192
|
+
words: InstallmentWords,
|
|
193
|
+
series: InstallmentSeriesWords,
|
|
194
|
+
): StructureIssue[] => {
|
|
195
|
+
const items = itemLabels(text, series).map((label) => itemOf(text, label, amounts, words, series));
|
|
196
|
+
if (items.length < 2 || depositOutside(text, items, series)) return [];
|
|
197
|
+
return sectionsOf(text, headingStarts).flatMap((section) => {
|
|
198
|
+
const issue = sectionIssue(text, section, items, amounts, words, series);
|
|
199
|
+
return issue === undefined ? [] : [issue];
|
|
200
|
+
});
|
|
201
|
+
};
|
|
@@ -87,7 +87,7 @@ const wordPattern = (word: string): string => {
|
|
|
87
87
|
};
|
|
88
88
|
|
|
89
89
|
/** One alternation of the words, longest first so 初回お支払額 wins over 初回. Undefined when there are none. */
|
|
90
|
-
const alternation = (words: readonly string[]): string | undefined =>
|
|
90
|
+
export const alternation = (words: readonly string[]): string | undefined =>
|
|
91
91
|
words.length === 0
|
|
92
92
|
? undefined
|
|
93
93
|
: words
|
|
@@ -95,7 +95,7 @@ const alternation = (words: readonly string[]): string | undefined =>
|
|
|
95
95
|
.map(wordPattern)
|
|
96
96
|
.join("|");
|
|
97
97
|
|
|
98
|
-
const occurrences = (text: string, words: readonly string[]): Span[] => {
|
|
98
|
+
export const occurrences = (text: string, words: readonly string[]): Span[] => {
|
|
99
99
|
const pattern = alternation(words);
|
|
100
100
|
if (pattern === undefined) return [];
|
|
101
101
|
return [...text.matchAll(new RegExp(pattern, "giu"))].map((match) => ({ start: match.index, end: match.index + match[0].length }));
|
|
@@ -109,7 +109,7 @@ const withoutWords = (text: string, words: readonly string[]): string => {
|
|
|
109
109
|
/** Whether only separators and these words stand in a gap on one line (「:」, "の", " of "). */
|
|
110
110
|
export const isPlainGap = (gap: string, words: readonly string[]): boolean => !gap.includes("\n") && withoutWords(gap, words).replace(SEPARATORS, "") === "";
|
|
111
111
|
|
|
112
|
-
const lineBounds = (text: string, offset: number): Span => {
|
|
112
|
+
export const lineBounds = (text: string, offset: number): Span => {
|
|
113
113
|
const end = text.indexOf("\n", offset);
|
|
114
114
|
return { start: text.lastIndexOf("\n", offset - 1) + 1, end: end === -1 ? text.length : end };
|
|
115
115
|
};
|
|
@@ -142,7 +142,7 @@ const countPattern = (words: InstallmentWords): string => {
|
|
|
142
142
|
return numberWords === undefined ? DIGITS : `${DIGITS}|${numberWords}`;
|
|
143
143
|
};
|
|
144
144
|
|
|
145
|
-
const hasWord = (text: string, words: readonly string[]): boolean => occurrences(text, words).length > 0;
|
|
145
|
+
export const hasWord = (text: string, words: readonly string[]): boolean => occurrences(text, words).length > 0;
|
|
146
146
|
|
|
147
147
|
/** How far around a value its marks are looked for. */
|
|
148
148
|
const MARK_REACH = 16;
|
|
@@ -166,7 +166,7 @@ const opensRange = (text: string, value: Span, connectors: readonly string[]): b
|
|
|
166
166
|
return connectors.some((connector) => after.startsWith(connector.toLowerCase()) && /^\s*[$€£¥¥0-90-9]/u.test(after.slice(connector.length)));
|
|
167
167
|
};
|
|
168
168
|
|
|
169
|
-
const isRough = (text: string, value: Span, words: InstallmentWords): boolean =>
|
|
169
|
+
export const isRough = (text: string, value: Span, words: InstallmentWords): boolean =>
|
|
170
170
|
isMarked(text, value, words.markers) || opensRange(text, value, words.connectors);
|
|
171
171
|
|
|
172
172
|
type Label = Span & { readonly kind: LabelKind };
|
|
@@ -315,7 +315,7 @@ export const writtenLike = (value: number, part: Part): string => {
|
|
|
315
315
|
return part.amount.scale === undefined ? part.written.replace(/[0-90-9][0-90-9,,..]*/u, digits) : digits;
|
|
316
316
|
};
|
|
317
317
|
|
|
318
|
-
const describe = (parts: readonly Part[]): string =>
|
|
318
|
+
export const describe = (parts: readonly Part[]): string =>
|
|
319
319
|
parts.map((part) => (part.count === 1 ? part.written : `${part.written} × ${String(part.count)}`)).join(" + ");
|
|
320
320
|
|
|
321
321
|
const regularOf = (parts: readonly Part[]): Part | undefined => parts.toSorted((left, right) => right.count - left.count)[0];
|