chaffjs 0.33.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/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 +4 -2
- 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/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/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.map +1 -1
- package/dist/detectors/formula-amount.js +2 -12
- package/dist/detectors/formula-amount.js.map +1 -1
- 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 +2 -12
- 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/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/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.map +1 -1
- package/dist/detectors/structure-tree.js +3 -0
- 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/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/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/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/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 +3 -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/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/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/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/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 +3 -3
- package/profiles/listing.yaml +1 -1
- package/rules/closing-balance-mismatch.yaml +3 -3
- package/rules/due-before-issue.yaml +9 -5
- package/rules/fact-conflict.yaml +2 -2
- package/rules/heading-echo.yaml +2 -2
- 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/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 +4 -2
- 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/custom/no-rewrite.ts +2 -0
- package/src/custom/parse.ts +18 -2
- package/src/detectors/age-words.ts +12 -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 +2 -18
- package/src/detectors/heading-claim.ts +76 -0
- package/src/detectors/heading-echo.ts +32 -1
- package/src/detectors/installment-total-mismatch.ts +3 -18
- 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/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/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 +3 -0
- 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/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/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/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/profile/cue-lines.ts +14 -0
- package/src/profile/parse.ts +3 -2
- package/src/reference-numbers.ts +1 -1
- 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/definition-use.ts +24 -1
- package/src/structure/document-context.ts +46 -0
- package/src/structure/fenced-lines.ts +28 -0
- 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/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
|
@@ -2,7 +2,7 @@ import type { Detector, Finding, ProseDocument, StructureNode } from "../plugin.
|
|
|
2
2
|
import { countedFacts, countedPhraseAt, type CountedPhrase } from "../facts/counted-facts.ts";
|
|
3
3
|
import { nameSpans } from "../compare/proper-nouns.ts";
|
|
4
4
|
import { factValues, type FactValue } from "../facts/fact-values.ts";
|
|
5
|
-
import {
|
|
5
|
+
import { attributePhrasesOf, labelledFacts, type Fact, type FactWords } from "../facts/labelled-facts.ts";
|
|
6
6
|
import { tableFacts, type TotalColumns } from "../facts/table-facts.ts";
|
|
7
7
|
import { withCellKeys } from "../facts/cell-keys.ts";
|
|
8
8
|
import { verbQuantityFacts, type VerbQuantityWords } from "../facts/verb-quantity-facts.ts";
|
|
@@ -21,11 +21,13 @@ import { overlapsAny, spanIndex } from "../compare/spans.ts";
|
|
|
21
21
|
import { documentTermConflicts, isTermName, termHomes, type TermWord, type TermWords } from "../facts/document-terms.ts";
|
|
22
22
|
import { termValues, type PlacedWord, type TermValueWords } from "../facts/term-values.ts";
|
|
23
23
|
import { inDocumentOrder } from "../structure/issues.ts";
|
|
24
|
-
import { ageValues
|
|
24
|
+
import { ageValues } from "../facts/age-values.ts";
|
|
25
|
+
import { ageWordsOf } from "./age-words.ts";
|
|
25
26
|
import { qualifiedKeyOf, qualifiedKeys, type QualifierWords } from "../facts/qualified-labels.ts";
|
|
26
27
|
import { rowSentenceFindings } from "./row-sentence-amounts.ts";
|
|
27
28
|
import type { RateWord, RateWords } from "../facts/rate-values.ts";
|
|
28
29
|
import { wholeCountFindings } from "./whole-count-findings.ts";
|
|
30
|
+
import { perUnitsOf, perUnitValues } from "./per-unit-words.ts";
|
|
29
31
|
|
|
30
32
|
const patternsOf = (doc: ProseDocument, id: string): string[] => (doc.lexicons[id] ?? []).map((entry) => entry.pattern);
|
|
31
33
|
|
|
@@ -51,7 +53,7 @@ export const factWordsOf = (doc: ProseDocument): FactWords => ({
|
|
|
51
53
|
leads: patternsOf(doc, "fact-label-lead"),
|
|
52
54
|
rates: rateWordsOf(doc),
|
|
53
55
|
readings: { verbs: patternsOf(doc, "fact-reading-verb"), gauges: patternsOf(doc, "fact-reading-gauge"), clauses: patternsOf(doc, "fact-reading-clause") },
|
|
54
|
-
attributes: (doc.lexicons["fact-attribute"] ?? [])
|
|
56
|
+
attributes: attributePhrasesOf(doc.lexicons["fact-attribute"] ?? []),
|
|
55
57
|
});
|
|
56
58
|
|
|
57
59
|
const placedWordsOf = (doc: ProseDocument, id: string): PlacedWord[] =>
|
|
@@ -71,6 +73,10 @@ const termValueWordsOf = (doc: ProseDocument): TermValueWords => ({
|
|
|
71
73
|
selves: placedWordsOf(doc, "fact-term-self"),
|
|
72
74
|
starts: placedWordsOf(doc, "fact-term-start"),
|
|
73
75
|
valueEnds: patternsOf(doc, "fact-value-end"),
|
|
76
|
+
restating: patternsOf(doc, "fact-term-restating"),
|
|
77
|
+
frames: patternsOf(doc, "fact-term-frame"),
|
|
78
|
+
conditions: patternsOf(doc, "fact-amount-condition"),
|
|
79
|
+
qualifiers: qualifierWordsOf(doc),
|
|
74
80
|
});
|
|
75
81
|
|
|
76
82
|
const totalColumnsOf = (doc: ProseDocument): TotalColumns => {
|
|
@@ -89,15 +95,6 @@ const durationWordsOf = (doc: ProseDocument): DurationWord[] => [
|
|
|
89
95
|
),
|
|
90
96
|
];
|
|
91
97
|
|
|
92
|
-
const ageWordsAt = (doc: ProseDocument, id: string): AgeWord[] =>
|
|
93
|
-
(doc.lexicons[id] ?? []).map((entry): AgeWord => ({ pattern: entry.pattern, position: entry.position ?? "before" }));
|
|
94
|
-
|
|
95
|
-
const ageWordsOf = (doc: ProseDocument): AgeWords => ({
|
|
96
|
-
marks: ageWordsAt(doc, "fact-age-mark"),
|
|
97
|
-
limits: ageWordsAt(doc, "fact-age-limit"),
|
|
98
|
-
joiners: patternsOf(doc, "fact-age-joiner"),
|
|
99
|
-
});
|
|
100
|
-
|
|
101
98
|
const qualifierWordsOf = (doc: ProseDocument): QualifierWords => ({
|
|
102
99
|
templates: patternsOf(doc, "fact-label-qualifier"),
|
|
103
100
|
determiners: patternsOf(doc, "fact-label-drop"),
|
|
@@ -123,6 +120,7 @@ const withQualifiedKeys = <T extends Fact>(doc: ProseDocument, facts: readonly T
|
|
|
123
120
|
/**
|
|
124
121
|
* 単位の語彙表の量(410 g、1.2 kg)と期間(3 months)と年齢(満70歳まで、aged 20 to 70)も値として読む。木が単位を読まない量は、
|
|
125
122
|
* 数だけでは升や文の値にならない。年齢と重なる期間(30 years old の 30 years)と、期間と重なる量(3 months の 3 m)は読まない。
|
|
123
|
+
* 数量には何あたりか(1kmあたり15円)を付ける。何あたりの値は、それを掛けた金額とは比べない。
|
|
126
124
|
*/
|
|
127
125
|
const readFacts = (doc: ProseDocument, tree: StructureNode): ScopedFact[] => {
|
|
128
126
|
const tagged = factValues(tree, doc.source, nameSpans(doc));
|
|
@@ -134,7 +132,7 @@ const readFacts = (doc: ProseDocument, tree: StructureNode): ScopedFact[] => {
|
|
|
134
132
|
durationWordsOf(doc),
|
|
135
133
|
);
|
|
136
134
|
const taken = spanIndex([...ages, ...durations]);
|
|
137
|
-
const values = valuesWith(tree, doc, [...measuredOf(doc).filter((value) => !overlapsAny(taken, value)), ...ages, ...durations]);
|
|
135
|
+
const values = perUnitsOf(doc, valuesWith(tree, doc, [...measuredOf(doc).filter((value) => !overlapsAny(taken, value)), ...ages, ...durations]));
|
|
138
136
|
const terms = termValues(doc.source, values, termValueWordsOf(doc));
|
|
139
137
|
const termed = new Set(terms.map((fact) => fact.value.start));
|
|
140
138
|
const labelled = labelledFacts(doc.source, values, { ...factWordsOf(doc), qualifiers: qualifierWordsOf(doc) }).filter(
|
|
@@ -263,8 +261,7 @@ const countedPhrasesOf = (doc: ProseDocument): CountedPhrase[] =>
|
|
|
263
261
|
const countedFactsOf = (doc: ProseDocument): ScopedFact[] => {
|
|
264
262
|
const tree = doc.structure;
|
|
265
263
|
if (tree === undefined) return [];
|
|
266
|
-
|
|
267
|
-
return scopedFacts(countedFacts(doc.source, values, countedPhrasesOf(doc)), tree, doc.source, patternsOf(doc, "summary-heading"));
|
|
264
|
+
return scopedFacts(countedFacts(doc.source, perUnitValues(doc, tree), countedPhrasesOf(doc)), tree, doc.source, patternsOf(doc, "summary-heading"));
|
|
268
265
|
};
|
|
269
266
|
|
|
270
267
|
const wordsAt = (doc: ProseDocument, id: string): WordAt[] =>
|
|
@@ -319,8 +316,7 @@ const pairFinding =
|
|
|
319
316
|
const conditionPairFindings = (doc: ProseDocument): Finding[] => {
|
|
320
317
|
const tree = doc.structure;
|
|
321
318
|
if (tree === undefined) return [];
|
|
322
|
-
|
|
323
|
-
return conditionPairConflicts(doc.source, changeSentencesOf(doc, tree), values, changeWordsOf(doc)).map(pairFinding(doc));
|
|
319
|
+
return conditionPairConflicts(doc.source, changeSentencesOf(doc, tree), perUnitValues(doc, tree), changeWordsOf(doc)).map(pairFinding(doc));
|
|
324
320
|
};
|
|
325
321
|
|
|
326
322
|
const eventWordsOf = (doc: ProseDocument): EventWord[] =>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Lines at the top of a form or a letter that are not sentences: a field ("Invoice number: INV-2026-0318",
|
|
2
|
+
// 「見積番号:RF-731852」) or a name standing alone (a letterhead, 「有限会社つばめ野リフォーム」). Pure.
|
|
3
|
+
import { labelLine } from "./number-unit.ts";
|
|
4
|
+
import type { Token } from "../plugin.ts";
|
|
5
|
+
|
|
6
|
+
/** Parts of speech a name is made of: no verb, adjective, determiner or particle. */
|
|
7
|
+
const NAME_POS: ReadonlySet<string> = new Set(["NOUN", "PROPN", "NUM", "PUNCT", "SYM", "X"]);
|
|
8
|
+
/** A value with one of these says something ("Note: this page is a sample."); it is a sentence behind a label. */
|
|
9
|
+
const PREDICATE_POS: ReadonlySet<string> = new Set(["VERB", "AUX"]);
|
|
10
|
+
|
|
11
|
+
const isNameOnly = (tokens: readonly Token[]): boolean => tokens.some((token) => token.pos !== "PUNCT") && tokens.every((token) => NAME_POS.has(token.pos));
|
|
12
|
+
|
|
13
|
+
/** "Label: value" whose value states nothing: a number, a date, a name. */
|
|
14
|
+
const isField = (line: string, start: number, tokens: readonly Token[]): boolean => {
|
|
15
|
+
const field = labelLine(line, start);
|
|
16
|
+
return field !== undefined && !tokens.some((token) => token.span.start >= field.at && PREDICATE_POS.has(token.pos));
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type Line = { readonly text: string; readonly start: number };
|
|
20
|
+
|
|
21
|
+
const linesOf = (text: string, start: number): Line[] =>
|
|
22
|
+
text.split("\n").reduce<Line[]>((lines, line) => {
|
|
23
|
+
const last = lines.at(-1);
|
|
24
|
+
return [...lines, { text: line, start: last === undefined ? start : last.start + last.text.length + 1 }];
|
|
25
|
+
}, []);
|
|
26
|
+
|
|
27
|
+
const isFormLine = (line: Line, tokens: readonly Token[]): boolean => {
|
|
28
|
+
const inLine = tokens.filter((token) => token.span.start >= line.start && token.span.start < line.start + line.text.length);
|
|
29
|
+
return isNameOnly(inLine) || isField(line.text, line.start, inLine);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Every line of the block gives a field and its value, or only a name: the top of a form or a letterhead, not a sentence.
|
|
34
|
+
* `start` is where the text starts in the coordinates of the tokens' spans. Without tokens no block is read as a form.
|
|
35
|
+
*/
|
|
36
|
+
export const isFormBlock = (text: string, start: number, tokens: readonly Token[] | undefined): boolean => {
|
|
37
|
+
const lines = linesOf(text, start).filter((line) => line.text.trim() !== "");
|
|
38
|
+
return tokens !== undefined && lines.length > 0 && lines.every((line) => isFormLine(line, tokens));
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/** What a form block gives: the tokens after each field's label, and every token of a name line. */
|
|
42
|
+
export const formValueTokens = (text: string, start: number, tokens: readonly Token[]): Token[] =>
|
|
43
|
+
linesOf(text, start).flatMap((line) => {
|
|
44
|
+
const field = labelLine(line.text, line.start);
|
|
45
|
+
const from = field?.at ?? line.start;
|
|
46
|
+
return tokens.filter((token) => token.span.start >= from && token.span.start < line.start + line.text.length);
|
|
47
|
+
});
|
|
@@ -4,12 +4,10 @@
|
|
|
4
4
|
// currency-notation's, read with amount-multiplier's words of scale. The profile may give the tax rate a document leaves out.
|
|
5
5
|
import type { Detector, Finding, ProseDocument } from "../plugin.ts";
|
|
6
6
|
import { formulaAmountMismatches, type FormulaWords } from "../structure/formula-amount.ts";
|
|
7
|
-
import type { StatedAmount } from "../structure/stated-multiple.ts";
|
|
8
7
|
import type { TaxPriceRole, TaxPriceWord } from "../structure/tax-price.ts";
|
|
9
|
-
import { scaledAmountOf, scaleWordsOf } from "./amount-scale.ts";
|
|
10
|
-
import { amountsIn, formsOf } from "./currency-notation.ts";
|
|
11
8
|
import { proseAndTablesOf } from "../table-text.ts";
|
|
12
9
|
import { quoteAround } from "./quote-around.ts";
|
|
10
|
+
import { statedAmountsOf } from "./stated-amounts.ts";
|
|
13
11
|
|
|
14
12
|
const TAX_ROLES: readonly string[] = ["excluded", "included", "rate"] satisfies readonly TaxPriceRole[];
|
|
15
13
|
const isTaxRole = (group: string | undefined): group is TaxPriceRole => group !== undefined && TAX_ROLES.includes(group);
|
|
@@ -31,23 +29,9 @@ const wordsOf = (doc: ProseDocument): FormulaWords => ({
|
|
|
31
29
|
notIncluded: patternsOf(doc, "formula-amount-word", "not-included"),
|
|
32
30
|
});
|
|
33
31
|
|
|
34
|
-
const amountsOf = (text: string, doc: ProseDocument): StatedAmount[] => {
|
|
35
|
-
const scales = scaleWordsOf(doc);
|
|
36
|
-
return amountsIn(
|
|
37
|
-
text,
|
|
38
|
-
formsOf(doc),
|
|
39
|
-
scales.map((scale) => scale.word),
|
|
40
|
-
).flatMap((amount) => {
|
|
41
|
-
const read = scaledAmountOf(text, amount, scales);
|
|
42
|
-
return read === undefined
|
|
43
|
-
? []
|
|
44
|
-
: [{ offset: read.offset, end: amount.offset + amount.written.length, currency: read.currency, value: read.value, scale: read.unit }];
|
|
45
|
-
});
|
|
46
|
-
};
|
|
47
|
-
|
|
48
32
|
export const formulaAmount: Detector = (doc): Finding[] => {
|
|
49
33
|
const text = proseAndTablesOf(doc);
|
|
50
|
-
const input = { text, amounts:
|
|
34
|
+
const input = { text, amounts: statedAmountsOf(text, doc), defaultTaxRate: doc.profile?.taxRate };
|
|
51
35
|
return formulaAmountMismatches(input, wordsOf(doc)).map((issue) => ({
|
|
52
36
|
rule: "formula-amount-mismatch",
|
|
53
37
|
severity: "warning",
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Whether a first sentence says something about its heading's subject, rather than announcing or restating it. Pure;
|
|
2
|
+
// heading-echo reads it. The words that only announce or point (covers, below, 説明, 以下), the copulas and the
|
|
3
|
+
// negations come from the language package's heading-announcer lexicon.
|
|
4
|
+
import { isContent } from "./content-morphemes.ts";
|
|
5
|
+
import type { LexiconEntry, Token } from "../plugin.ts";
|
|
6
|
+
|
|
7
|
+
type Side = "before" | "after";
|
|
8
|
+
|
|
9
|
+
export type AnnouncerWords = {
|
|
10
|
+
/** Words that announce the section or point at it: they carry no claim of their own. */
|
|
11
|
+
readonly announcing: ReadonlySet<string>;
|
|
12
|
+
/** Copulas (be, です), each with the side its complement stands on. */
|
|
13
|
+
readonly copulas: ReadonlyMap<string, Side>;
|
|
14
|
+
/** Words that deny what a copula links (There are no issues, is not in the manual): the denial is the claim. */
|
|
15
|
+
readonly negations: ReadonlySet<string>;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const patternsOf = (entries: readonly LexiconEntry[], group: string | undefined): Set<string> =>
|
|
19
|
+
new Set(entries.filter((entry) => entry.group === group).map((entry) => entry.pattern.toLowerCase()));
|
|
20
|
+
|
|
21
|
+
export const announcerWords = (entries: readonly LexiconEntry[]): AnnouncerWords => ({
|
|
22
|
+
announcing: patternsOf(entries, undefined),
|
|
23
|
+
copulas: new Map(entries.flatMap((entry): [string, Side][] => (entry.group === "copula" ? [[entry.pattern.toLowerCase(), entry.position ?? "after"]] : []))),
|
|
24
|
+
negations: patternsOf(entries, "negation"),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const baseForm = (token: Token): string => (token.lemma ?? token.surface).toLowerCase();
|
|
28
|
+
|
|
29
|
+
/** Between a copula and its complement: not, very, the (is not the, are very). */
|
|
30
|
+
const BRIDGING_POS = new Set(["ADV", "PART", "DET"]);
|
|
31
|
+
|
|
32
|
+
/** What a copula at `at` links to, on its complement's side: the bridging words, then the complement when there is one. */
|
|
33
|
+
const linkedBy = (tokens: readonly Token[], at: number, side: Side): Token[] => {
|
|
34
|
+
const outward = side === "after" ? tokens.slice(at + 1) : tokens.slice(0, at).reverse();
|
|
35
|
+
const complementAt = outward.findIndex((token) => !BRIDGING_POS.has(token.pos));
|
|
36
|
+
return complementAt === -1 ? outward : outward.slice(0, complementAt + 1);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/** An -ing form or a past participle names or modifies (Moving through the process, An object representing a server); it predicates only after a copula. */
|
|
40
|
+
const NON_FINITE = new Set(["Ger", "Part"]);
|
|
41
|
+
|
|
42
|
+
const isFiniteVerb = (token: Token): boolean => token.pos === "VERB" && !NON_FINITE.has(token.features?.["VerbForm"] ?? "");
|
|
43
|
+
|
|
44
|
+
type Predicate = { readonly words: readonly Token[]; readonly negations: readonly Token[] };
|
|
45
|
+
|
|
46
|
+
const copulaPredicate = (tokens: readonly Token[], at: number, side: Side, words: AnnouncerWords): Predicate => {
|
|
47
|
+
const linked = linkedBy(tokens, at, side);
|
|
48
|
+
return { words: linked.filter((word) => !BRIDGING_POS.has(word.pos)), negations: linked.filter((word) => words.negations.has(baseForm(word))) };
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/** A finite verb that is no copula, a word turned into a verb by a light verb (変更し), or a copula's complement. */
|
|
52
|
+
const predicatesOf = (tokens: readonly Token[], words: AnnouncerWords): Predicate[] =>
|
|
53
|
+
tokens.flatMap((token, index): Predicate[] => {
|
|
54
|
+
const side = words.copulas.get(baseForm(token));
|
|
55
|
+
if (side !== undefined) return [copulaPredicate(tokens, index, side, words)];
|
|
56
|
+
const lightVerbNext = tokens[index + 1]?.features?.["VerbType"] === "Light";
|
|
57
|
+
return isFiniteVerb(token) || lightVerbNext ? [{ words: [token], negations: [] }] : [];
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The sentence has a predicate of its own: a word that is not one of the heading's words and does not only announce or
|
|
62
|
+
* point (covers, below, 説明, 以下), or a denial the heading does not make. "All charges include sales tax." says
|
|
63
|
+
* something about charges; "This section covers installation." and "The full reference is in the manual." do not.
|
|
64
|
+
*/
|
|
65
|
+
/** A code name in a heading holds words of its own (restoreFixtures, cache_key, dry-run): the sentence that spells them out repeats them. */
|
|
66
|
+
const CODE_NAME_PART = /\p{Lu}?[\p{Ll}\p{Nd}]+|\p{Lu}+(?!\p{Ll})/gu;
|
|
67
|
+
|
|
68
|
+
const headingWords = (heading: readonly Token[]): Set<string> =>
|
|
69
|
+
new Set(heading.flatMap((token) => [baseForm(token), ...(token.surface.match(CODE_NAME_PART) ?? []).map((part) => part.toLowerCase())]));
|
|
70
|
+
|
|
71
|
+
export const makesClaim = (heading: readonly Token[], sentence: readonly Token[], words: AnnouncerWords): boolean => {
|
|
72
|
+
const said = headingWords(heading);
|
|
73
|
+
const unsaid = (token: Token): boolean => !said.has(baseForm(token));
|
|
74
|
+
const ownWord = (token: Token): boolean => isContent(token) && unsaid(token) && !words.announcing.has(baseForm(token)) && !words.copulas.has(baseForm(token));
|
|
75
|
+
return predicatesOf(sentence, words).some((predicate) => predicate.negations.some(unsaid) || predicate.words.some(ownWord));
|
|
76
|
+
};
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { lengthOf, proseText } from "../measure.ts";
|
|
2
2
|
import { newContentMorphemes } from "./content-morphemes.ts";
|
|
3
3
|
import { echoedHeadingUnits, trigrams } from "./heading-overlap.ts";
|
|
4
|
+
import { formValueTokens, isFormBlock } from "./form-line.ts";
|
|
4
5
|
import { titleTokens } from "../heading-label.ts";
|
|
6
|
+
import { announcerWords, makesClaim } from "./heading-claim.ts";
|
|
5
7
|
import { handsOver } from "./lead-in.ts";
|
|
6
8
|
import { holdsLink, namesLocation } from "./location-pointer.ts";
|
|
7
9
|
import { withoutQuotedVariants } from "./quoted-variant.ts";
|
|
@@ -65,6 +67,17 @@ const pointsElsewhere = (doc: ProseDocument, section: Section): boolean => {
|
|
|
65
67
|
return first !== undefined && (holdsLink(doc.source, first.span, doc.links) || namesLocation(doc.source.slice(first.span.start, first.span.end)));
|
|
66
68
|
};
|
|
67
69
|
|
|
70
|
+
/**
|
|
71
|
+
* 最初の文が、見出しに無い述語で見出しの主題について何かを述べている(## Charges → All charges include sales tax.)。
|
|
72
|
+
* 予告や指示の語(covers, below, 説明, 以下)だけを述語にする文は、見出しを言い直しているだけで繰り返し。
|
|
73
|
+
*/
|
|
74
|
+
const statesClaim = (doc: ProseDocument, section: Section): boolean => {
|
|
75
|
+
const first = section.firstSentence;
|
|
76
|
+
if (first?.tokens === undefined || section.headingTokens === undefined) return false;
|
|
77
|
+
const heading = titleTokens(section.heading, measuredHeading(section), section.headingTokens);
|
|
78
|
+
return makesClaim(heading, first.tokens, announcerWords(doc.lexicons["heading-announcer"] ?? []));
|
|
79
|
+
};
|
|
80
|
+
|
|
68
81
|
/** A figure, with its thousands separators (1,320,000), compared without them. */
|
|
69
82
|
const FIGURE = /\p{Nd}+(?:[,,]\p{Nd}{3})*/gu;
|
|
70
83
|
/** Note marks are not figures: [^1], ※1, <sup>1</sup>. */
|
|
@@ -81,6 +94,17 @@ export const statesFigure = (heading: string, sentence: string): boolean => {
|
|
|
81
94
|
return figuresOf(sentence).some((figure) => !inHeading.has(figure));
|
|
82
95
|
};
|
|
83
96
|
|
|
97
|
+
/**
|
|
98
|
+
* 最初の文が、見出しに無い名前や値を書いた、名前だけの行(レターヘッド)か項目と値の行(Customer: Laura Pennington)。
|
|
99
|
+
* 書式の頭で、見出しを言い直す文ではない。見出しをそのまま写した行(### 環境情報学研究科 → 環境情報学研究科、詳細:〈見出し〉)は繰り返し。
|
|
100
|
+
*/
|
|
101
|
+
const opensForm = (section: Section): boolean => {
|
|
102
|
+
const first = section.firstSentence;
|
|
103
|
+
if (first?.tokens === undefined || section.headingTokens === undefined || !isFormBlock(first.text, first.span.start, first.tokens)) return false;
|
|
104
|
+
const heading = titleTokens(section.heading, measuredHeading(section), section.headingTokens);
|
|
105
|
+
return newContentMorphemes(heading, formValueTokens(first.text, first.span.start, first.tokens)) > 0;
|
|
106
|
+
};
|
|
107
|
+
|
|
84
108
|
/** 見出しとの重なりを測る文。用語集や表記の手引きは見出しの語の別の書き方を引用する(Not “datacentre”)ので、それは数えない。 */
|
|
85
109
|
const echoedText = (section: Section): string => withoutQuotedVariants(section.firstSentence?.text ?? "", measuredHeading(section));
|
|
86
110
|
|
|
@@ -88,9 +112,16 @@ export const headingEcho: Detector = (doc, options): Finding[] => {
|
|
|
88
112
|
const leadIns = (doc.lexicons["lead-in"] ?? []).map((entry) => entry.pattern);
|
|
89
113
|
return doc.sections
|
|
90
114
|
.filter((section) => section.heading.length > 0 && section.firstSentence !== undefined && addsLittle(section, doc.lengthUnit))
|
|
91
|
-
.filter(
|
|
115
|
+
.filter(
|
|
116
|
+
(section) =>
|
|
117
|
+
!leadsIn(doc, section, leadIns) &&
|
|
118
|
+
!pointsElsewhere(doc, section) &&
|
|
119
|
+
!statesFigure(section.heading, section.firstSentence?.text ?? "") &&
|
|
120
|
+
!opensForm(section),
|
|
121
|
+
)
|
|
92
122
|
.map((section) => ({ section, overlap: Math.round(containment(trigrams(measuredHeading(section)), trigrams(echoedText(section))) * 100) }))
|
|
93
123
|
.filter(({ overlap }) => overlap >= options.limit)
|
|
124
|
+
.filter(({ section }) => !statesClaim(doc, section))
|
|
94
125
|
.map(({ section, overlap }) => ({
|
|
95
126
|
rule: "heading-echo",
|
|
96
127
|
severity: "warning",
|
|
@@ -4,12 +4,11 @@
|
|
|
4
4
|
// and amount-range-word.
|
|
5
5
|
import type { Detector, Finding, Lexicon, ProseDocument } from "../plugin.ts";
|
|
6
6
|
import { installmentFeeMismatches, type InstallmentSeriesWords } from "../structure/installment-fee.ts";
|
|
7
|
-
import { installmentTotalMismatches, type
|
|
8
|
-
import { scaledAmountOf, scaleWordsOf } from "./amount-scale.ts";
|
|
9
|
-
import { amountsIn, formsOf } from "./currency-notation.ts";
|
|
7
|
+
import { installmentTotalMismatches, type InstallmentWords, type PositionedWord } from "../structure/installment-total.ts";
|
|
10
8
|
import type { StructureIssue } from "../structure/issues.ts";
|
|
11
9
|
import { proseAndTablesOf } from "../table-text.ts";
|
|
12
10
|
import { quoteAround } from "./quote-around.ts";
|
|
11
|
+
import { statedAmountsOf } from "./stated-amounts.ts";
|
|
13
12
|
|
|
14
13
|
const entriesOf = (doc: ProseDocument, lexicon: string, group?: string): Lexicon =>
|
|
15
14
|
(doc.lexicons[lexicon] ?? []).filter((entry) => group === undefined || entry.group === group);
|
|
@@ -57,20 +56,6 @@ const seriesWordsOf = (doc: ProseDocument): InstallmentSeriesWords => ({
|
|
|
57
56
|
feeLinks: patternsOf(doc, "installment-word", "fee-link"),
|
|
58
57
|
});
|
|
59
58
|
|
|
60
|
-
const amountsOf = (text: string, doc: ProseDocument): InstallmentAmount[] => {
|
|
61
|
-
const scales = scaleWordsOf(doc);
|
|
62
|
-
return amountsIn(
|
|
63
|
-
text,
|
|
64
|
-
formsOf(doc),
|
|
65
|
-
scales.map((scale) => scale.word),
|
|
66
|
-
).flatMap((amount) => {
|
|
67
|
-
const read = scaledAmountOf(text, amount, scales);
|
|
68
|
-
return read === undefined
|
|
69
|
-
? []
|
|
70
|
-
: [{ offset: read.offset, end: amount.offset + amount.written.length, currency: read.currency, value: read.value, scale: read.unit }];
|
|
71
|
-
});
|
|
72
|
-
};
|
|
73
|
-
|
|
74
59
|
const findingOf = (text: string, issue: StructureIssue, variant?: string): Finding => ({
|
|
75
60
|
rule: "installment-total-mismatch",
|
|
76
61
|
severity: "warning",
|
|
@@ -83,7 +68,7 @@ const findingOf = (text: string, issue: StructureIssue, variant?: string): Findi
|
|
|
83
68
|
|
|
84
69
|
export const installmentTotalMismatch: Detector = (doc): Finding[] => {
|
|
85
70
|
const text = proseAndTablesOf(doc);
|
|
86
|
-
const amounts =
|
|
71
|
+
const amounts = statedAmountsOf(text, doc);
|
|
87
72
|
const words = wordsOf(doc);
|
|
88
73
|
const loans = installmentTotalMismatches(text, amounts, words);
|
|
89
74
|
const headings = (doc.markup?.headings ?? []).map((heading) => heading.start);
|
|
@@ -63,18 +63,56 @@ const isTitleCase = (text: string): boolean =>
|
|
|
63
63
|
.split(/\s+/u)
|
|
64
64
|
.every((word) => TITLE_SMALL_WORDS.has(word) || /^\p{Lu}/u.test(word));
|
|
65
65
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
/** A determiner that opens an object (the, a, your): no noun phrase goes on from its head with one. */
|
|
67
|
+
const opensObject = (token: Token | undefined): boolean => token?.features?.["PronType"] === "Art" || token?.features?.["Poss"] === "Yes";
|
|
68
|
+
|
|
69
|
+
const AFTER_ADJECTIVE = new Set(["ADP", "PART"]);
|
|
70
|
+
|
|
71
|
+
/** A noun or adjective, or an ordinary word the tagger took for a name because it is capitalised (Toggle). */
|
|
72
|
+
const isNounOrAdjective = (token: Token): boolean =>
|
|
73
|
+
token.pos === "NOUN" || token.pos === "ADJ" || (token.pos === "PROPN" && token.features?.["OrdinaryWord"] === "Yes");
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The head of an item whose first two words overrule the first word's tag, or undefined when they do not. "Open the console"
|
|
77
|
+
* and "Press the button" open with a verb. "Clean up after closing" and "Bake in the oven" may open with a verb or an
|
|
78
|
+
* adjective phrase (Close to the station), and "Washing machine space" with a compound noun or a gerund and its object
|
|
79
|
+
* (Washing dishes): neither is judged.
|
|
80
|
+
*/
|
|
81
|
+
const headFromNextWord = (first: Token, next: Token | undefined): "verb" | "unjudged" | undefined => {
|
|
82
|
+
if (isNounOrAdjective(first) && opensObject(next)) return "verb";
|
|
83
|
+
if (first.pos === "ADJ" && AFTER_ADJECTIVE.has(next?.pos ?? "")) return "unjudged";
|
|
84
|
+
return first.features?.["VerbForm"] === "Ger" && next?.pos === "NOUN" ? "unjudged" : undefined;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/** The head as the first word's tag gives it. */
|
|
88
|
+
const headFromTag = (first: Token): string | undefined => {
|
|
89
|
+
if (isUnsure(first)) return undefined;
|
|
69
90
|
// A participle (Generated files, Nested lists) modifies the noun after it; the item is a noun phrase, but the tag does not say so.
|
|
70
91
|
if (first.pos === "VERB") return first.features?.["VerbForm"] === "Part" ? undefined : "verb";
|
|
71
92
|
return NOUN_HEAD.has(first.pos) ? "noun" : undefined;
|
|
72
93
|
};
|
|
73
94
|
|
|
74
|
-
|
|
75
|
-
|
|
95
|
+
const englishHead = (sentences: readonly Sentence[], instructionVerbs: ReadonlySet<string>): string | undefined => {
|
|
96
|
+
const tokens = sentences[0]?.tokens ?? [];
|
|
97
|
+
const at = tokens.findIndex((token) => !SKIPPED_AT_END.has(token.pos));
|
|
98
|
+
const first = tokens[at];
|
|
99
|
+
if (at === -1 || first === undefined || isTitleCase(sentences[0]?.text ?? "")) return undefined;
|
|
100
|
+
// The word right after, punctuation included: in "Next, the candidate" the comma ends the opener.
|
|
101
|
+
const next = tokens[at + 1];
|
|
102
|
+
// Before a common noun the word may head a compound (Click fraud, Scroll bar), so its tag decides.
|
|
103
|
+
if (instructionVerbs.has(first.surface.toLowerCase()) && next?.pos !== "NOUN") return "verb";
|
|
104
|
+
const overruled = headFromNextWord(first, next);
|
|
105
|
+
if (overruled === undefined) return headFromTag(first);
|
|
106
|
+
return overruled === "verb" ? "verb" : undefined;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* An item's form on an axis, or undefined when its words do not tell. Japanese items have no head axis (the verb comes last).
|
|
111
|
+
* instructionVerbs: verbs the tagger does not know as verbs (click), from the adapter's instruction-verb word list.
|
|
112
|
+
*/
|
|
113
|
+
export const formOf = (axis: Axis, sentences: readonly Sentence[], language: string, instructionVerbs: ReadonlySet<string> = new Set()): string | undefined => {
|
|
76
114
|
if (language === "ja") return axis === "ending" ? japaneseEnding(sentences) : undefined;
|
|
77
|
-
return axis === "ending" ? englishEnding(sentences) : englishHead(sentences);
|
|
115
|
+
return axis === "ending" ? englishEnding(sentences) : englishHead(sentences, instructionVerbs);
|
|
78
116
|
};
|
|
79
117
|
|
|
80
118
|
const within = (inner: Span, outer: Span): boolean => inner.start >= outer.start && inner.end <= outer.end;
|
|
@@ -131,9 +169,10 @@ const listFindings = (doc: ProseDocument, list: BulletList, limit: number): Find
|
|
|
131
169
|
const span = list.itemSpans[index];
|
|
132
170
|
return span !== undefined && isJudged(doc.source.slice(span.start, sentences.at(-1)?.span.end ?? span.end), sentences);
|
|
133
171
|
});
|
|
172
|
+
const instructionVerbs = new Set((doc.lexicons["instruction-verb"] ?? []).map((entry) => entry.pattern.toLowerCase()));
|
|
134
173
|
return AXES.flatMap((axis) => {
|
|
135
174
|
const items = judged.flatMap((sentences) => {
|
|
136
|
-
const form = formOf(axis, sentences, doc.language);
|
|
175
|
+
const form = formOf(axis, sentences, doc.language, instructionVerbs);
|
|
137
176
|
return form === undefined ? [] : [{ sentences, form }];
|
|
138
177
|
});
|
|
139
178
|
const odd = minorityWithin(items, (item) => item.form === FORMS[axis][0], limit);
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import type { Detector, Finding, ProseDocument } from "../plugin.ts";
|
|
2
2
|
import { cuedNamesIn, mentionsIn, nameVariants, suffixedNamesIn, type NameMention, type VariantChars } from "../name-variants.ts";
|
|
3
|
+
import type { WordUse } from "../name-end-letter.ts";
|
|
3
4
|
import { withKnownNeighbours } from "../name-neighbours.ts";
|
|
4
5
|
import type { SpellingInput } from "../name-spelling-chars.ts";
|
|
5
6
|
import { lookalikeSlipsIn, type LookalikeChars } from "../name-lookalike-chars.ts";
|
|
6
|
-
import { readingOfWords, type CharReadings } from "../name-char-reading.ts";
|
|
7
|
+
import { givenNameReadingOf, readingOfWords, type CharReadings } from "../name-char-reading.ts";
|
|
7
8
|
import { nameCueAt, type NameCues } from "../name-cue.ts";
|
|
8
9
|
import { quoteAt } from "./structure-tree.ts";
|
|
9
|
-
import { companyMentionsIn, companyVariants, type CompanyForm, type IsProper } from "../company-names.ts";
|
|
10
|
+
import { companyMentionsIn, companyVariants, type CompanyForm, type IsProper, type PredicateEnds } from "../company-names.ts";
|
|
10
11
|
import { kanaSpelledCompanies, type ReadWord } from "../company-kana-spelling.ts";
|
|
11
12
|
import { proseAndTablesOf } from "../table-text.ts";
|
|
12
13
|
import { tableBodyCells } from "../facts/table-facts.ts";
|
|
@@ -33,6 +34,18 @@ const variantCharsOf = (doc: ProseDocument): VariantChars =>
|
|
|
33
34
|
const charReadingsOf = (doc: ProseDocument): CharReadings =>
|
|
34
35
|
new Map((doc.lexicons["name-char-reading"] ?? []).flatMap((entry): [string, string][] => (entry.group === undefined ? [] : [[entry.pattern, entry.group]])));
|
|
35
36
|
|
|
37
|
+
/**
|
|
38
|
+
* 名の中での字の読み(建太 の 建 を ケン)は、語彙表 given-name-char-reading が組(group)に言う。解析器が読めない字の読み
|
|
39
|
+
* (name-char-reading)も名の中で同じに読む。
|
|
40
|
+
*/
|
|
41
|
+
const givenCharReadingsOf = (doc: ProseDocument, chars: CharReadings): CharReadings =>
|
|
42
|
+
new Map([
|
|
43
|
+
...chars,
|
|
44
|
+
...(doc.lexicons["given-name-char-reading"] ?? []).flatMap((entry): [string, string][] =>
|
|
45
|
+
entry.group === undefined ? [] : [[entry.pattern, entry.group]],
|
|
46
|
+
),
|
|
47
|
+
]);
|
|
48
|
+
|
|
36
49
|
const patternsOf = (doc: ProseDocument, id: string): string[] => (doc.lexicons[id] ?? []).map((entry) => entry.pattern);
|
|
37
50
|
|
|
38
51
|
/** 会社の形の語(株式会社、Inc)は語彙表 company-form が、同じ形の組(group)と名前のどちら側に立つか(position)とともに言う。 */
|
|
@@ -124,6 +137,11 @@ const kanaStopsOf = (doc: ProseDocument): Parameters<typeof companyMentionsIn>[3
|
|
|
124
137
|
return { particles: inGroup("particle"), openers: inGroup("opener"), ends: inGroup("end") };
|
|
125
138
|
};
|
|
126
139
|
|
|
140
|
+
/** 品詞解析が形容詞・動詞・助動詞と読んだ語の終わり。い で終わるひらがな(ながい食品)を会社の名前の頭に入れないため。 */
|
|
141
|
+
const PREDICATE_POS: ReadonlySet<string> = new Set(["ADJ", "VERB", "AUX"]);
|
|
142
|
+
const predicateEndsOf = (doc: ProseDocument): PredicateEnds =>
|
|
143
|
+
new Set(tokensOf(doc).flatMap((token) => (PREDICATE_POS.has(token.pos) ? [token.span.end] : [])));
|
|
144
|
+
|
|
127
145
|
const readWordsOf = (doc: ProseDocument): ReadWord[] =>
|
|
128
146
|
tokensOf(doc).map((token) => ({ start: token.span.start, end: token.span.end, surface: token.surface, reading: token.reading }));
|
|
129
147
|
|
|
@@ -133,7 +151,7 @@ const readWordsOf = (doc: ProseDocument): ReadWord[] =>
|
|
|
133
151
|
*/
|
|
134
152
|
const companyFindings = (doc: ProseDocument, prose: string, reported: readonly Reported[]): Reported[] => {
|
|
135
153
|
const stops = kanaStopsOf(doc);
|
|
136
|
-
const mentions = companyMentionsIn(prose, [...companyFormsOf(doc), ...foreignFormsOf(doc)], properOf(doc), stops);
|
|
154
|
+
const mentions = companyMentionsIn(prose, [...companyFormsOf(doc), ...foreignFormsOf(doc)], properOf(doc), stops, predicateEndsOf(doc));
|
|
137
155
|
const variants = companyVariants(mentions).map(({ mention, usual, kind }) => ({
|
|
138
156
|
offset: mention.offset,
|
|
139
157
|
name: mention.surface,
|
|
@@ -220,21 +238,25 @@ const wordsCovering = (tokens: readonly Token[], startAt: ReadonlyMap<number, nu
|
|
|
220
238
|
|
|
221
239
|
/**
|
|
222
240
|
* 空白を挟んだ姓と名で、敬称か肩書きが付くか、人を書く欄にあるもの(高瀬 誠部長、氏名:森川 大輔)。語は解析器の切り方のまま、
|
|
223
|
-
*
|
|
241
|
+
* 読みは語の読みをつなぐ。名を名の中での字の読みで読み直した読みも持つ(givenNameReading)。解析器の語の切れ目と姓・名の端が
|
|
242
|
+
* 合わないものは読まない。
|
|
224
243
|
*/
|
|
225
244
|
const spacedNameMentionsOf = (doc: ProseDocument, prose: string, charReadings: CharReadings): NameMention[] => {
|
|
226
245
|
const cues = { suffixes: patternsOf(doc, "person-suffix"), titles: patternsOf(doc, "person-title"), labels: patternsOf(doc, "person-label") };
|
|
227
246
|
const tokens = tokensOf(doc);
|
|
228
247
|
const startAt = new Map(tokens.map((token, index) => [token.span.start, index]));
|
|
248
|
+
const givenChars = givenCharReadingsOf(doc, charReadings);
|
|
229
249
|
return spacedPersonNamesIn(prose, cues).flatMap(({ surname, given }): NameMention[] => {
|
|
230
250
|
const surnameWords = wordsCovering(tokens, startAt, surname);
|
|
231
251
|
const givenWords = wordsCovering(tokens, startAt, given);
|
|
232
252
|
if (surnameWords === undefined || givenWords === undefined) return [];
|
|
233
253
|
const words = [...surnameWords, ...givenWords];
|
|
234
254
|
const surface = `${prose.slice(surname.start, surname.end)} ${prose.slice(given.start, given.end)}`;
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
255
|
+
const surnameReading = readingOfWords(surnameWords, charReadings);
|
|
256
|
+
const givenReading = givenNameReadingOf(givenWords, givenChars, charReadings);
|
|
257
|
+
const givenNameReading = surnameReading === undefined || givenReading === undefined ? undefined : surnameReading + givenReading;
|
|
258
|
+
const reading = readingOfWords(words, charReadings);
|
|
259
|
+
return [{ surface, offset: surname.start, reading, givenNameReading, words: words.map((word) => word.surface), person: true, cue: "person" }];
|
|
238
260
|
});
|
|
239
261
|
};
|
|
240
262
|
|
|
@@ -311,12 +333,27 @@ const labelledNamesOf = (doc: ProseDocument, mentions: readonly NameMention[]):
|
|
|
311
333
|
);
|
|
312
334
|
};
|
|
313
335
|
|
|
336
|
+
const LATIN_WORD = /^[A-Za-z]+$/u;
|
|
337
|
+
const LOWERCASE_WORD = /^[a-z]+$/u;
|
|
338
|
+
|
|
339
|
+
/** 文書が小文字で書くか、品詞解析が普通名詞と読む英字の語(文頭の Hurricane)。 */
|
|
340
|
+
const isCommonToken = (token: Token): boolean => LOWERCASE_WORD.test(token.surface) || (token.pos === "NOUN" && LATIN_WORD.test(token.surface));
|
|
341
|
+
|
|
342
|
+
/** 文書での英字の語の使い方。名前と読んだ語のうち小文字で書けばふつうの語と、普通名詞として書いた語。 */
|
|
343
|
+
const wordUseOf = (doc: ProseDocument): WordUse => {
|
|
344
|
+
const tokens = tokensOf(doc);
|
|
345
|
+
return {
|
|
346
|
+
ordinary: new Set(tokens.flatMap((token) => (isOrdinaryToken(token) ? [token.surface.toLowerCase()] : []))),
|
|
347
|
+
common: new Set(tokens.flatMap((token) => (isCommonToken(token) ? [token.surface.toLowerCase()] : []))),
|
|
348
|
+
};
|
|
349
|
+
};
|
|
350
|
+
|
|
314
351
|
/** 同じ名前を、文書の中で少しだけ違う形に書いた所(GitHub と Github、山田太郎 と 山田太朗)。少ないほうを指す。 */
|
|
315
352
|
export const nameVariant: Detector = (doc): Finding[] => {
|
|
316
353
|
const prose = doc.prose ?? doc.source;
|
|
317
354
|
const chars = variantCharsOf(doc);
|
|
318
355
|
const mentions = labelledNamesOf(doc, ordinaryNamesOf(doc, nameMentionsOf(doc, prose, chars)));
|
|
319
|
-
const variants = nameVariants(mentions, chars, spellingInputOf(doc))
|
|
356
|
+
const variants = nameVariants(mentions, chars, spellingInputOf(doc), wordUseOf(doc))
|
|
320
357
|
.map(({ mention, usual, kind }): Reported => ({ offset: mention.offset, name: mention.surface, usual, kind }))
|
|
321
358
|
.filter(notSpacingOnly);
|
|
322
359
|
const lookalikes = lookalikeSlipsIn(mentions, lookalikeCharsOf(doc)).map(({ mention, usual }) => ({
|
|
@@ -135,12 +135,38 @@ export const unitGaps = (values: readonly Measured[]): UnitGap[] => {
|
|
|
135
135
|
const unitsOf = (doc: ProseDocument): Set<string> =>
|
|
136
136
|
new Set(UNIT_LEXICONS.flatMap((name) => (doc.lexicons[name] ?? []).flatMap((entry) => [entry.pattern, entry.pattern.toLowerCase()])));
|
|
137
137
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
138
|
+
/** Lexicon piece-count-column: the headings of a column that counts whatever its row is, and the units of a hired period. */
|
|
139
|
+
export type PieceCount = { readonly columns: ReadonlySet<string>; readonly periods: ReadonlySet<string> };
|
|
140
|
+
|
|
141
|
+
const WHOLE_NUMBER = /^\d[\d,]*$/u;
|
|
142
|
+
const TABLE_KIND = /^table \d+: /u;
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* A bare whole number in a quantity column whose other rows count a hired period (4 days, 2 nights): the count of a
|
|
146
|
+
* piece the row hires or buys (Child seat | 1), which has no unit to lose.
|
|
147
|
+
*/
|
|
148
|
+
export const isPieceCount = (gap: UnitGap, count: PieceCount): boolean =>
|
|
149
|
+
TABLE_KIND.test(gap.value.kind) &&
|
|
150
|
+
count.columns.has(gap.value.name.toLowerCase()) &&
|
|
151
|
+
count.periods.has(gap.unit.toLowerCase()) &&
|
|
152
|
+
WHOLE_NUMBER.test(gap.value.written.normalize("NFKC"));
|
|
153
|
+
|
|
154
|
+
const pieceCountOf = (doc: ProseDocument): PieceCount => {
|
|
155
|
+
const entries = doc.lexicons["piece-count-column"] ?? [];
|
|
156
|
+
const patterns = (group: string): Set<string> => new Set(entries.filter((entry) => entry.group === group).map((entry) => entry.pattern.toLowerCase()));
|
|
157
|
+
return { columns: patterns("column"), periods: patterns("period") };
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
export const numberWithoutUnit: Detector = (doc: ProseDocument): Finding[] => {
|
|
161
|
+
const count = pieceCountOf(doc);
|
|
162
|
+
return unitGaps(measuredValues(doc.source, doc.prose ?? doc.source, unitsOf(doc)))
|
|
163
|
+
.filter((gap) => !isPieceCount(gap, count))
|
|
164
|
+
.map((gap) => ({
|
|
165
|
+
rule: "number-without-unit",
|
|
166
|
+
severity: "warning",
|
|
167
|
+
line: 0,
|
|
168
|
+
column: 0,
|
|
169
|
+
quote: quoteAt(doc.source, gap.value.offset),
|
|
170
|
+
values: { value: gap.value.written, name: gap.value.name, unit: gap.unit, offset: gap.value.offset },
|
|
171
|
+
}));
|
|
172
|
+
};
|