chaffjs 0.29.0 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/announced-count.d.ts +2 -0
- package/dist/announced-count.d.ts.map +1 -1
- package/dist/announced-count.js +4 -3
- package/dist/announced-count.js.map +1 -1
- package/dist/contacts/addresses.d.ts +21 -0
- package/dist/contacts/addresses.d.ts.map +1 -0
- package/dist/contacts/addresses.js +144 -0
- package/dist/contacts/addresses.js.map +1 -0
- package/dist/contacts/emails.d.ts +11 -0
- package/dist/contacts/emails.d.ts.map +1 -0
- package/dist/contacts/emails.js +51 -0
- package/dist/contacts/emails.js.map +1 -0
- package/dist/contacts/variants.d.ts +14 -0
- package/dist/contacts/variants.d.ts.map +1 -0
- package/dist/contacts/variants.js +25 -0
- package/dist/contacts/variants.js.map +1 -0
- package/dist/derived/battery-runtime.d.ts +85 -0
- package/dist/derived/battery-runtime.d.ts.map +1 -0
- package/dist/derived/battery-runtime.js +217 -0
- package/dist/derived/battery-runtime.js.map +1 -0
- package/dist/derived/period-counts.d.ts +48 -0
- package/dist/derived/period-counts.d.ts.map +1 -0
- package/dist/derived/period-counts.js +128 -0
- package/dist/derived/period-counts.js.map +1 -0
- package/dist/derived/session-hours.d.ts +26 -0
- package/dist/derived/session-hours.d.ts.map +1 -0
- package/dist/derived/session-hours.js +29 -0
- package/dist/derived/session-hours.js.map +1 -0
- package/dist/derived/time-lengths.d.ts +37 -0
- package/dist/derived/time-lengths.d.ts.map +1 -0
- package/dist/derived/time-lengths.js +171 -0
- package/dist/derived/time-lengths.js.map +1 -0
- package/dist/derived/unit-counts.d.ts +7 -0
- package/dist/derived/unit-counts.d.ts.map +1 -0
- package/dist/derived/unit-counts.js +13 -0
- package/dist/derived/unit-counts.js.map +1 -0
- package/dist/derived/use-interval.d.ts +33 -0
- package/dist/derived/use-interval.d.ts.map +1 -0
- package/dist/derived/use-interval.js +93 -0
- package/dist/derived/use-interval.js.map +1 -0
- package/dist/derived/working-hours.d.ts +36 -0
- package/dist/derived/working-hours.d.ts.map +1 -0
- package/dist/derived/working-hours.js +55 -0
- package/dist/derived/working-hours.js.map +1 -0
- package/dist/detectors/acronym-expansion.d.ts +1 -1
- package/dist/detectors/acronym-expansion.d.ts.map +1 -1
- package/dist/detectors/acronym-expansion.js +46 -2
- package/dist/detectors/acronym-expansion.js.map +1 -1
- package/dist/detectors/announced-count.d.ts.map +1 -1
- package/dist/detectors/announced-count.js +1 -0
- package/dist/detectors/announced-count.js.map +1 -1
- package/dist/detectors/capacity-runtime.d.ts +7 -0
- package/dist/detectors/capacity-runtime.d.ts.map +1 -0
- package/dist/detectors/capacity-runtime.js +98 -0
- package/dist/detectors/capacity-runtime.js.map +1 -0
- package/dist/detectors/capitalised-term.d.ts +44 -0
- package/dist/detectors/capitalised-term.d.ts.map +1 -0
- package/dist/detectors/capitalised-term.js +101 -0
- package/dist/detectors/capitalised-term.js.map +1 -0
- package/dist/detectors/contact-variant.d.ts +3 -0
- package/dist/detectors/contact-variant.d.ts.map +1 -0
- package/dist/detectors/contact-variant.js +21 -0
- package/dist/detectors/contact-variant.js.map +1 -0
- package/dist/detectors/count-over-capacity.d.ts +4 -0
- package/dist/detectors/count-over-capacity.d.ts.map +1 -0
- package/dist/detectors/count-over-capacity.js +26 -0
- package/dist/detectors/count-over-capacity.js.map +1 -0
- package/dist/detectors/date-outside-period.d.ts.map +1 -1
- package/dist/detectors/date-outside-period.js +5 -1
- package/dist/detectors/date-outside-period.js.map +1 -1
- package/dist/detectors/date-stamp-order.d.ts +3 -0
- package/dist/detectors/date-stamp-order.d.ts.map +1 -0
- package/dist/detectors/date-stamp-order.js +24 -0
- package/dist/detectors/date-stamp-order.js.map +1 -0
- package/dist/detectors/defined-term-form.d.ts.map +1 -1
- package/dist/detectors/defined-term-form.js +57 -7
- package/dist/detectors/defined-term-form.js.map +1 -1
- package/dist/detectors/definition-use.d.ts +1 -1
- package/dist/detectors/definition-use.d.ts.map +1 -1
- package/dist/detectors/definition-use.js +4 -2
- package/dist/detectors/definition-use.js.map +1 -1
- package/dist/detectors/derived-numbers.d.ts +11 -1
- package/dist/detectors/derived-numbers.d.ts.map +1 -1
- package/dist/detectors/derived-numbers.js +74 -20
- package/dist/detectors/derived-numbers.js.map +1 -1
- package/dist/detectors/due-date.d.ts.map +1 -1
- package/dist/detectors/due-date.js +6 -0
- package/dist/detectors/due-date.js.map +1 -1
- package/dist/detectors/en-shape.d.ts.map +1 -1
- package/dist/detectors/en-shape.js +7 -4
- package/dist/detectors/en-shape.js.map +1 -1
- package/dist/detectors/event-deadline.d.ts +3 -0
- package/dist/detectors/event-deadline.d.ts.map +1 -0
- package/dist/detectors/event-deadline.js +24 -0
- package/dist/detectors/event-deadline.js.map +1 -0
- package/dist/detectors/fact-consistency.d.ts +1 -1
- package/dist/detectors/fact-consistency.d.ts.map +1 -1
- package/dist/detectors/fact-consistency.js +52 -3
- package/dist/detectors/fact-consistency.js.map +1 -1
- package/dist/detectors/heading-case.d.ts +5 -2
- package/dist/detectors/heading-case.d.ts.map +1 -1
- package/dist/detectors/heading-case.js +12 -4
- package/dist/detectors/heading-case.js.map +1 -1
- package/dist/detectors/heading-echo.d.ts +5 -0
- package/dist/detectors/heading-echo.d.ts.map +1 -1
- package/dist/detectors/heading-echo.js +14 -1
- package/dist/detectors/heading-echo.js.map +1 -1
- package/dist/detectors/layout-code.d.ts +5 -0
- package/dist/detectors/layout-code.d.ts.map +1 -0
- package/dist/detectors/layout-code.js +124 -0
- package/dist/detectors/layout-code.js.map +1 -0
- package/dist/detectors/measured-facts.d.ts +8 -0
- package/dist/detectors/measured-facts.d.ts.map +1 -0
- package/dist/detectors/measured-facts.js +18 -0
- package/dist/detectors/measured-facts.js.map +1 -0
- package/dist/detectors/name-variant-model-codes.d.ts +11 -0
- package/dist/detectors/name-variant-model-codes.d.ts.map +1 -0
- package/dist/detectors/name-variant-model-codes.js +21 -0
- package/dist/detectors/name-variant-model-codes.js.map +1 -0
- package/dist/detectors/name-variant.d.ts.map +1 -1
- package/dist/detectors/name-variant.js +78 -10
- package/dist/detectors/name-variant.js.map +1 -1
- package/dist/detectors/orthographic-variant.d.ts.map +1 -1
- package/dist/detectors/orthographic-variant.js +62 -7
- package/dist/detectors/orthographic-variant.js.map +1 -1
- package/dist/detectors/orthography.d.ts.map +1 -1
- package/dist/detectors/orthography.js +12 -6
- package/dist/detectors/orthography.js.map +1 -1
- package/dist/detectors/party-order.d.ts +3 -0
- package/dist/detectors/party-order.d.ts.map +1 -0
- package/dist/detectors/party-order.js +46 -0
- package/dist/detectors/party-order.js.map +1 -0
- package/dist/detectors/quantity-range.d.ts +3 -0
- package/dist/detectors/quantity-range.d.ts.map +1 -0
- package/dist/detectors/quantity-range.js +35 -0
- package/dist/detectors/quantity-range.js.map +1 -0
- package/dist/detectors/range-band.d.ts +5 -0
- package/dist/detectors/range-band.d.ts.map +1 -0
- package/dist/detectors/range-band.js +80 -0
- package/dist/detectors/range-band.js.map +1 -0
- package/dist/detectors/registry/capacity-runtime-mismatch.d.ts +3 -0
- package/dist/detectors/registry/capacity-runtime-mismatch.d.ts.map +1 -0
- package/dist/detectors/registry/capacity-runtime-mismatch.js +3 -0
- package/dist/detectors/registry/capacity-runtime-mismatch.js.map +1 -0
- package/dist/detectors/registry/connection-time-short.d.ts +3 -0
- package/dist/detectors/registry/connection-time-short.d.ts.map +1 -0
- package/dist/detectors/registry/connection-time-short.js +3 -0
- package/dist/detectors/registry/connection-time-short.js.map +1 -0
- package/dist/detectors/registry/contact-variant.d.ts +3 -0
- package/dist/detectors/registry/contact-variant.d.ts.map +1 -0
- package/dist/detectors/registry/contact-variant.js +3 -0
- package/dist/detectors/registry/contact-variant.js.map +1 -0
- package/dist/detectors/registry/count-over-capacity.d.ts +3 -0
- package/dist/detectors/registry/count-over-capacity.d.ts.map +1 -0
- package/dist/detectors/registry/count-over-capacity.js +3 -0
- package/dist/detectors/registry/count-over-capacity.js.map +1 -0
- package/dist/detectors/registry/date-stamp-order.d.ts +3 -0
- package/dist/detectors/registry/date-stamp-order.d.ts.map +1 -0
- package/dist/detectors/registry/date-stamp-order.js +3 -0
- package/dist/detectors/registry/date-stamp-order.js.map +1 -0
- package/dist/detectors/registry/duration-product-mismatch.d.ts +3 -0
- package/dist/detectors/registry/duration-product-mismatch.d.ts.map +1 -0
- package/dist/detectors/registry/duration-product-mismatch.js +3 -0
- package/dist/detectors/registry/duration-product-mismatch.js.map +1 -0
- package/dist/detectors/registry/event-deadline.d.ts +3 -0
- package/dist/detectors/registry/event-deadline.d.ts.map +1 -0
- package/dist/detectors/registry/event-deadline.js +3 -0
- package/dist/detectors/registry/event-deadline.js.map +1 -0
- package/dist/detectors/registry/interval-count-mismatch.d.ts +3 -0
- package/dist/detectors/registry/interval-count-mismatch.d.ts.map +1 -0
- package/dist/detectors/registry/interval-count-mismatch.js +3 -0
- package/dist/detectors/registry/interval-count-mismatch.js.map +1 -0
- package/dist/detectors/registry/layout-code-mismatch.d.ts +3 -0
- package/dist/detectors/registry/layout-code-mismatch.d.ts.map +1 -0
- package/dist/detectors/registry/layout-code-mismatch.js +3 -0
- package/dist/detectors/registry/layout-code-mismatch.js.map +1 -0
- package/dist/detectors/registry/party-order.d.ts +3 -0
- package/dist/detectors/registry/party-order.d.ts.map +1 -0
- package/dist/detectors/registry/party-order.js +3 -0
- package/dist/detectors/registry/party-order.js.map +1 -0
- package/dist/detectors/registry/quantity-range.d.ts +3 -0
- package/dist/detectors/registry/quantity-range.d.ts.map +1 -0
- package/dist/detectors/registry/quantity-range.js +3 -0
- package/dist/detectors/registry/quantity-range.js.map +1 -0
- package/dist/detectors/registry/range-band-mismatch.d.ts +3 -0
- package/dist/detectors/registry/range-band-mismatch.d.ts.map +1 -0
- package/dist/detectors/registry/range-band-mismatch.js +3 -0
- package/dist/detectors/registry/range-band-mismatch.js.map +1 -0
- package/dist/detectors/registry/rent-multiple-mismatch.d.ts +3 -0
- package/dist/detectors/registry/rent-multiple-mismatch.d.ts.map +1 -0
- package/dist/detectors/registry/rent-multiple-mismatch.js +3 -0
- package/dist/detectors/registry/rent-multiple-mismatch.js.map +1 -0
- package/dist/detectors/registry/requirement-lists.d.ts +3 -0
- package/dist/detectors/registry/requirement-lists.d.ts.map +1 -0
- package/dist/detectors/registry/requirement-lists.js +3 -0
- package/dist/detectors/registry/requirement-lists.js.map +1 -0
- package/dist/detectors/registry/return-type.d.ts +3 -0
- package/dist/detectors/registry/return-type.d.ts.map +1 -0
- package/dist/detectors/registry/return-type.js +3 -0
- package/dist/detectors/registry/return-type.js.map +1 -0
- package/dist/detectors/registry/scaled-quantity-mismatch.d.ts +3 -0
- package/dist/detectors/registry/scaled-quantity-mismatch.d.ts.map +1 -0
- package/dist/detectors/registry/scaled-quantity-mismatch.js +3 -0
- package/dist/detectors/registry/scaled-quantity-mismatch.js.map +1 -0
- package/dist/detectors/registry/step-time-sum-mismatch.d.ts +3 -0
- package/dist/detectors/registry/step-time-sum-mismatch.d.ts.map +1 -0
- package/dist/detectors/registry/step-time-sum-mismatch.js +3 -0
- package/dist/detectors/registry/step-time-sum-mismatch.js.map +1 -0
- package/dist/detectors/registry/time-range-reversed.d.ts +3 -0
- package/dist/detectors/registry/time-range-reversed.d.ts.map +1 -0
- package/dist/detectors/registry/time-range-reversed.js +3 -0
- package/dist/detectors/registry/time-range-reversed.js.map +1 -0
- package/dist/detectors/registry/unlisted-item-used.d.ts +3 -0
- package/dist/detectors/registry/unlisted-item-used.d.ts.map +1 -0
- package/dist/detectors/registry/unlisted-item-used.js +3 -0
- package/dist/detectors/registry/unlisted-item-used.js.map +1 -0
- package/dist/detectors/registry/walk-time-distance-mismatch.d.ts +3 -0
- package/dist/detectors/registry/walk-time-distance-mismatch.d.ts.map +1 -0
- package/dist/detectors/registry/walk-time-distance-mismatch.js +3 -0
- package/dist/detectors/registry/walk-time-distance-mismatch.js.map +1 -0
- package/dist/detectors/rent-multiple-mismatch.d.ts +3 -0
- package/dist/detectors/rent-multiple-mismatch.d.ts.map +1 -0
- package/dist/detectors/rent-multiple-mismatch.js +39 -0
- package/dist/detectors/rent-multiple-mismatch.js.map +1 -0
- package/dist/detectors/requirement-lists.d.ts +3 -0
- package/dist/detectors/requirement-lists.d.ts.map +1 -0
- package/dist/detectors/requirement-lists.js +46 -0
- package/dist/detectors/requirement-lists.js.map +1 -0
- package/dist/detectors/return-type.d.ts +3 -0
- package/dist/detectors/return-type.d.ts.map +1 -0
- package/dist/detectors/return-type.js +24 -0
- package/dist/detectors/return-type.js.map +1 -0
- package/dist/detectors/scaled-quantity.d.ts +3 -0
- package/dist/detectors/scaled-quantity.d.ts.map +1 -0
- package/dist/detectors/scaled-quantity.js +37 -0
- package/dist/detectors/scaled-quantity.js.map +1 -0
- package/dist/detectors/signals.d.ts.map +1 -1
- package/dist/detectors/signals.js +3 -1
- package/dist/detectors/signals.js.map +1 -1
- package/dist/detectors/step-time-sum.d.ts +3 -0
- package/dist/detectors/step-time-sum.d.ts.map +1 -0
- package/dist/detectors/step-time-sum.js +46 -0
- package/dist/detectors/step-time-sum.js.map +1 -0
- package/dist/detectors/structure-tree.d.ts +2 -0
- package/dist/detectors/structure-tree.d.ts.map +1 -1
- package/dist/detectors/structure-tree.js +31 -6
- package/dist/detectors/structure-tree.js.map +1 -1
- package/dist/detectors/structure.js +1 -1
- package/dist/detectors/structure.js.map +1 -1
- package/dist/detectors/technical-docs.d.ts +5 -0
- package/dist/detectors/technical-docs.d.ts.map +1 -1
- package/dist/detectors/technical-docs.js +12 -0
- package/dist/detectors/technical-docs.js.map +1 -1
- package/dist/detectors/time-order.d.ts +2 -0
- package/dist/detectors/time-order.d.ts.map +1 -1
- package/dist/detectors/time-order.js +33 -5
- package/dist/detectors/time-order.js.map +1 -1
- package/dist/detectors/undefined-term.d.ts.map +1 -1
- package/dist/detectors/undefined-term.js +19 -3
- package/dist/detectors/undefined-term.js.map +1 -1
- package/dist/detectors/unit-consistency.d.ts +1 -1
- package/dist/detectors/unit-consistency.d.ts.map +1 -1
- package/dist/detectors/unit-consistency.js +46 -38
- package/dist/detectors/unit-consistency.js.map +1 -1
- package/dist/detectors/unlisted-items.d.ts +3 -0
- package/dist/detectors/unlisted-items.d.ts.map +1 -0
- package/dist/detectors/unlisted-items.js +57 -0
- package/dist/detectors/unlisted-items.js.map +1 -0
- package/dist/detectors/use-count-words.d.ts +8 -0
- package/dist/detectors/use-count-words.d.ts.map +1 -0
- package/dist/detectors/use-count-words.js +20 -0
- package/dist/detectors/use-count-words.js.map +1 -0
- package/dist/detectors/use-interval.d.ts +3 -0
- package/dist/detectors/use-interval.d.ts.map +1 -0
- package/dist/detectors/use-interval.js +39 -0
- package/dist/detectors/use-interval.js.map +1 -0
- package/dist/detectors/walk-time-distance-mismatch.d.ts +4 -0
- package/dist/detectors/walk-time-distance-mismatch.d.ts.map +1 -0
- package/dist/detectors/walk-time-distance-mismatch.js +38 -0
- package/dist/detectors/walk-time-distance-mismatch.js.map +1 -0
- package/dist/document.d.ts.map +1 -1
- package/dist/document.js +4 -1
- package/dist/document.js.map +1 -1
- package/dist/facts/approximate-tolerance.d.ts +7 -0
- package/dist/facts/approximate-tolerance.d.ts.map +1 -0
- package/dist/facts/approximate-tolerance.js +16 -0
- package/dist/facts/approximate-tolerance.js.map +1 -0
- package/dist/facts/item-amounts.d.ts +18 -0
- package/dist/facts/item-amounts.d.ts.map +1 -0
- package/dist/facts/item-amounts.js +67 -0
- package/dist/facts/item-amounts.js.map +1 -0
- package/dist/facts/labelled-facts.d.ts +4 -0
- package/dist/facts/labelled-facts.d.ts.map +1 -1
- package/dist/facts/labelled-facts.js +21 -6
- package/dist/facts/labelled-facts.js.map +1 -1
- package/dist/facts/measure-units.d.ts +5 -0
- package/dist/facts/measure-units.d.ts.map +1 -0
- package/dist/facts/measure-units.js +28 -0
- package/dist/facts/measure-units.js.map +1 -0
- package/dist/facts/measures.d.ts +23 -4
- package/dist/facts/measures.d.ts.map +1 -1
- package/dist/facts/measures.js +68 -19
- package/dist/facts/measures.js.map +1 -1
- package/dist/facts/retention-periods.d.ts +51 -0
- package/dist/facts/retention-periods.d.ts.map +1 -0
- package/dist/facts/retention-periods.js +148 -0
- package/dist/facts/retention-periods.js.map +1 -0
- package/dist/facts/table-facts.d.ts +2 -0
- package/dist/facts/table-facts.d.ts.map +1 -1
- package/dist/facts/table-facts.js +2 -0
- package/dist/facts/table-facts.js.map +1 -1
- package/dist/facts/unit-conflicts.d.ts +3 -2
- package/dist/facts/unit-conflicts.d.ts.map +1 -1
- package/dist/facts/unit-conflicts.js +15 -7
- package/dist/facts/unit-conflicts.js.map +1 -1
- package/dist/facts/unit-equivalents.d.ts +13 -0
- package/dist/facts/unit-equivalents.d.ts.map +1 -0
- package/dist/facts/unit-equivalents.js +56 -0
- package/dist/facts/unit-equivalents.js.map +1 -0
- package/dist/facts/unit-tolerance.d.ts +4 -0
- package/dist/facts/unit-tolerance.d.ts.map +1 -0
- package/dist/facts/unit-tolerance.js +13 -0
- package/dist/facts/unit-tolerance.js.map +1 -0
- package/dist/facts/unit-word.d.ts +8 -0
- package/dist/facts/unit-word.d.ts.map +1 -0
- package/dist/facts/unit-word.js +34 -0
- package/dist/facts/unit-word.js.map +1 -0
- package/dist/facts/written-precision.d.ts +10 -0
- package/dist/facts/written-precision.d.ts.map +1 -0
- package/dist/facts/written-precision.js +20 -0
- package/dist/facts/written-precision.js.map +1 -0
- package/dist/kana-spelling.d.ts +12 -0
- package/dist/kana-spelling.d.ts.map +1 -1
- package/dist/kana-spelling.js +23 -0
- package/dist/kana-spelling.js.map +1 -1
- package/dist/model-codes.d.ts +23 -0
- package/dist/model-codes.d.ts.map +1 -0
- package/dist/model-codes.js +81 -0
- package/dist/model-codes.js.map +1 -0
- package/dist/name-spelling-chars.d.ts +16 -0
- package/dist/name-spelling-chars.d.ts.map +1 -0
- package/dist/name-spelling-chars.js +14 -0
- package/dist/name-spelling-chars.js.map +1 -0
- package/dist/name-variants.d.ts +8 -2
- package/dist/name-variants.d.ts.map +1 -1
- package/dist/name-variants.js +72 -7
- package/dist/name-variants.js.map +1 -1
- package/dist/name-word-order.d.ts +49 -0
- package/dist/name-word-order.d.ts.map +1 -0
- package/dist/name-word-order.js +114 -0
- package/dist/name-word-order.js.map +1 -0
- package/dist/phone-numbers.d.ts.map +1 -1
- package/dist/phone-numbers.js +22 -5
- package/dist/phone-numbers.js.map +1 -1
- package/dist/place-names.d.ts +12 -5
- package/dist/place-names.d.ts.map +1 -1
- package/dist/place-names.js +34 -14
- package/dist/place-names.js.map +1 -1
- package/dist/plugin.d.ts +12 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/preamble-paragraphs.d.ts +2 -1
- package/dist/preamble-paragraphs.d.ts.map +1 -1
- package/dist/preamble-paragraphs.js +16 -1
- package/dist/preamble-paragraphs.js.map +1 -1
- package/dist/profile/parse.d.ts.map +1 -1
- package/dist/profile/parse.js +11 -0
- package/dist/profile/parse.js.map +1 -1
- package/dist/return-type.d.ts +33 -0
- package/dist/return-type.d.ts.map +1 -0
- package/dist/return-type.js +302 -0
- package/dist/return-type.js.map +1 -0
- package/dist/spacing-code.d.ts +6 -0
- package/dist/spacing-code.d.ts.map +1 -0
- package/dist/spacing-code.js +16 -0
- package/dist/spacing-code.js.map +1 -0
- package/dist/structure/amount-range.d.ts +2 -2
- package/dist/structure/amount-range.d.ts.map +1 -1
- package/dist/structure/amount-range.js +12 -5
- package/dist/structure/amount-range.js.map +1 -1
- package/dist/structure/capacity-count.d.ts +59 -0
- package/dist/structure/capacity-count.d.ts.map +1 -0
- package/dist/structure/capacity-count.js +118 -0
- package/dist/structure/capacity-count.js.map +1 -0
- package/dist/structure/connection-times.d.ts +56 -0
- package/dist/structure/connection-times.d.ts.map +1 -0
- package/dist/structure/connection-times.js +165 -0
- package/dist/structure/connection-times.js.map +1 -0
- package/dist/structure/counted-amounts.d.ts +6 -0
- package/dist/structure/counted-amounts.d.ts.map +1 -1
- package/dist/structure/counted-amounts.js +4 -4
- package/dist/structure/counted-amounts.js.map +1 -1
- package/dist/structure/date-order-tie.d.ts +13 -0
- package/dist/structure/date-order-tie.d.ts.map +1 -0
- package/dist/structure/date-order-tie.js +61 -0
- package/dist/structure/date-order-tie.js.map +1 -0
- package/dist/structure/date-order.d.ts +2 -2
- package/dist/structure/date-order.d.ts.map +1 -1
- package/dist/structure/date-order.js +26 -9
- package/dist/structure/date-order.js.map +1 -1
- package/dist/structure/date-outside-period.d.ts +8 -0
- package/dist/structure/date-outside-period.d.ts.map +1 -1
- package/dist/structure/date-outside-period.js +52 -3
- package/dist/structure/date-outside-period.js.map +1 -1
- package/dist/structure/date-range.d.ts +6 -2
- package/dist/structure/date-range.d.ts.map +1 -1
- package/dist/structure/date-range.js +37 -9
- package/dist/structure/date-range.js.map +1 -1
- package/dist/structure/deadline-after-event.d.ts +23 -0
- package/dist/structure/deadline-after-event.d.ts.map +1 -0
- package/dist/structure/deadline-after-event.js +42 -0
- package/dist/structure/deadline-after-event.js.map +1 -0
- package/dist/structure/definition-use.d.ts +1 -1
- package/dist/structure/definition-use.d.ts.map +1 -1
- package/dist/structure/definition-use.js +55 -2
- package/dist/structure/definition-use.js.map +1 -1
- package/dist/structure/due-date.d.ts.map +1 -1
- package/dist/structure/due-date.js +5 -27
- package/dist/structure/due-date.js.map +1 -1
- package/dist/structure/labelled-date-order.d.ts +38 -0
- package/dist/structure/labelled-date-order.d.ts.map +1 -0
- package/dist/structure/labelled-date-order.js +41 -0
- package/dist/structure/labelled-date-order.js.map +1 -0
- package/dist/structure/layout-codes.d.ts +68 -0
- package/dist/structure/layout-codes.d.ts.map +1 -0
- package/dist/structure/layout-codes.js +161 -0
- package/dist/structure/layout-codes.js.map +1 -0
- package/dist/structure/leg-times.d.ts +2 -0
- package/dist/structure/leg-times.d.ts.map +1 -1
- package/dist/structure/leg-times.js +13 -0
- package/dist/structure/leg-times.js.map +1 -1
- package/dist/structure/measured-amounts.d.ts +17 -0
- package/dist/structure/measured-amounts.d.ts.map +1 -0
- package/dist/structure/measured-amounts.js +110 -0
- package/dist/structure/measured-amounts.js.map +1 -0
- package/dist/structure/party-order.d.ts +33 -0
- package/dist/structure/party-order.d.ts.map +1 -0
- package/dist/structure/party-order.js +79 -0
- package/dist/structure/party-order.js.map +1 -0
- package/dist/structure/quantity-range.d.ts +8 -0
- package/dist/structure/quantity-range.d.ts.map +1 -0
- package/dist/structure/quantity-range.js +36 -0
- package/dist/structure/quantity-range.js.map +1 -0
- package/dist/structure/range-bands.d.ts +75 -0
- package/dist/structure/range-bands.d.ts.map +1 -0
- package/dist/structure/range-bands.js +244 -0
- package/dist/structure/range-bands.js.map +1 -0
- package/dist/structure/rent-multiple.d.ts +76 -0
- package/dist/structure/rent-multiple.d.ts.map +1 -0
- package/dist/structure/rent-multiple.js +186 -0
- package/dist/structure/rent-multiple.js.map +1 -0
- package/dist/structure/requirement-lists.d.ts +37 -0
- package/dist/structure/requirement-lists.d.ts.map +1 -0
- package/dist/structure/requirement-lists.js +89 -0
- package/dist/structure/requirement-lists.js.map +1 -0
- package/dist/structure/scaled-quantities.d.ts +50 -0
- package/dist/structure/scaled-quantities.d.ts.map +1 -0
- package/dist/structure/scaled-quantities.js +153 -0
- package/dist/structure/scaled-quantities.js.map +1 -0
- package/dist/structure/stamp-order.d.ts +16 -0
- package/dist/structure/stamp-order.d.ts.map +1 -0
- package/dist/structure/stamp-order.js +78 -0
- package/dist/structure/stamp-order.js.map +1 -0
- package/dist/structure/step-times.d.ts +49 -0
- package/dist/structure/step-times.d.ts.map +1 -0
- package/dist/structure/step-times.js +128 -0
- package/dist/structure/step-times.js.map +1 -0
- package/dist/structure/term-deadline.d.ts +19 -0
- package/dist/structure/term-deadline.d.ts.map +1 -0
- package/dist/structure/term-deadline.js +28 -0
- package/dist/structure/term-deadline.js.map +1 -0
- package/dist/structure/time-order.d.ts +21 -0
- package/dist/structure/time-order.d.ts.map +1 -1
- package/dist/structure/time-order.js +5 -6
- package/dist/structure/time-order.js.map +1 -1
- package/dist/structure/time-range.d.ts +16 -0
- package/dist/structure/time-range.d.ts.map +1 -0
- package/dist/structure/time-range.js +57 -0
- package/dist/structure/time-range.js.map +1 -0
- package/dist/structure/total-line.d.ts +16 -0
- package/dist/structure/total-line.d.ts.map +1 -0
- package/dist/structure/total-line.js +44 -0
- package/dist/structure/total-line.js.map +1 -0
- package/dist/structure/total.d.ts +7 -1
- package/dist/structure/total.d.ts.map +1 -1
- package/dist/structure/total.js +28 -5
- package/dist/structure/total.js.map +1 -1
- package/dist/structure/unlisted-items.d.ts +70 -0
- package/dist/structure/unlisted-items.d.ts.map +1 -0
- package/dist/structure/unlisted-items.js +152 -0
- package/dist/structure/unlisted-items.js.map +1 -0
- package/dist/structure/version-order.d.ts +2 -0
- package/dist/structure/version-order.d.ts.map +1 -1
- package/dist/structure/version-order.js +10 -0
- package/dist/structure/version-order.js.map +1 -1
- package/dist/structure/walk-time.d.ts +87 -0
- package/dist/structure/walk-time.d.ts.map +1 -0
- package/dist/structure/walk-time.js +195 -0
- package/dist/structure/walk-time.js.map +1 -0
- package/dist/surname-near.d.ts +6 -0
- package/dist/surname-near.d.ts.map +1 -0
- package/dist/surname-near.js +31 -0
- package/dist/surname-near.js.map +1 -0
- package/dist/table-cells.d.ts +13 -0
- package/dist/table-cells.d.ts.map +1 -0
- package/dist/table-cells.js +36 -0
- package/dist/table-cells.js.map +1 -0
- package/dist/table-names.d.ts +42 -0
- package/dist/table-names.d.ts.map +1 -0
- package/dist/table-names.js +94 -0
- package/dist/table-names.js.map +1 -0
- package/package.json +3 -3
- package/profiles/listing.yaml +20 -0
- package/rules/announced-count-mismatch.yaml +2 -2
- package/rules/capacity-runtime-mismatch.yaml +66 -0
- package/rules/connection-time-short.yaml +52 -0
- package/rules/contact-variant.yaml +76 -0
- package/rules/count-over-capacity.yaml +55 -0
- package/rules/date-outside-period.yaml +4 -4
- package/rules/date-stamp-order.yaml +82 -0
- package/rules/deadline-after-event.yaml +67 -0
- package/rules/defined-name-repeated.yaml +2 -2
- package/rules/due-before-issue.yaml +11 -7
- package/rules/duration-mismatch.yaml +7 -4
- package/rules/duration-product-mismatch.yaml +73 -0
- package/rules/fact-conflict.yaml +9 -4
- package/rules/heading-echo.yaml +2 -2
- package/rules/interval-count-mismatch.yaml +54 -0
- package/rules/latin-spacing.yaml +2 -2
- package/rules/layout-code-mismatch.yaml +68 -0
- package/rules/name-variant.yaml +14 -2
- package/rules/no-em-dash.yaml +2 -2
- package/rules/party-order.yaml +84 -0
- package/rules/preamble-length.yaml +2 -2
- package/rules/quantity-range-reversed.yaml +53 -0
- package/rules/range-band-mismatch.yaml +70 -0
- package/rules/rent-multiple-mismatch.yaml +61 -0
- package/rules/required-listed-as-preferred.yaml +102 -0
- package/rules/return-type-mismatch.yaml +79 -0
- package/rules/scaled-quantity-mismatch.yaml +69 -0
- package/rules/step-time-sum-mismatch.yaml +65 -0
- package/rules/time-range-reversed.yaml +48 -0
- package/rules/title-case-consistency.yaml +2 -2
- package/rules/total-mismatch.yaml +2 -2
- package/rules/undefined-term.yaml +21 -7
- package/rules/unit-mismatch.yaml +12 -4
- package/rules/unlisted-item-used.yaml +90 -0
- package/rules/unused-definition.yaml +2 -2
- package/rules/use-before-definition.yaml +2 -2
- package/rules/walk-time-distance-mismatch.yaml +55 -0
- package/src/announced-count.ts +6 -3
- package/src/contacts/addresses.ts +179 -0
- package/src/contacts/emails.ts +63 -0
- package/src/contacts/variants.ts +33 -0
- package/src/derived/battery-runtime.ts +284 -0
- package/src/derived/period-counts.ts +178 -0
- package/src/derived/session-hours.ts +62 -0
- package/src/derived/time-lengths.ts +229 -0
- package/src/derived/unit-counts.ts +18 -0
- package/src/derived/use-interval.ts +151 -0
- package/src/derived/working-hours.ts +97 -0
- package/src/detectors/acronym-expansion.ts +57 -2
- package/src/detectors/announced-count.ts +1 -0
- package/src/detectors/capacity-runtime.ts +128 -0
- package/src/detectors/capitalised-term.ts +147 -0
- package/src/detectors/contact-variant.ts +24 -0
- package/src/detectors/count-over-capacity.ts +37 -0
- package/src/detectors/date-outside-period.ts +5 -1
- package/src/detectors/date-stamp-order.ts +28 -0
- package/src/detectors/defined-term-form.ts +63 -7
- package/src/detectors/definition-use.ts +6 -2
- package/src/detectors/derived-numbers.ts +97 -24
- package/src/detectors/due-date.ts +10 -2
- package/src/detectors/en-shape.ts +11 -4
- package/src/detectors/event-deadline.ts +29 -0
- package/src/detectors/fact-consistency.ts +64 -3
- package/src/detectors/heading-case.ts +13 -4
- package/src/detectors/heading-echo.ts +17 -1
- package/src/detectors/layout-code.ts +174 -0
- package/src/detectors/measured-facts.ts +20 -0
- package/src/detectors/name-variant-model-codes.ts +27 -0
- package/src/detectors/name-variant.ts +99 -15
- package/src/detectors/orthographic-variant.ts +78 -9
- package/src/detectors/orthography.ts +16 -6
- package/src/detectors/party-order.ts +50 -0
- package/src/detectors/quantity-range.ts +47 -0
- package/src/detectors/range-band.ts +125 -0
- package/src/detectors/registry/capacity-runtime-mismatch.ts +4 -0
- package/src/detectors/registry/connection-time-short.ts +4 -0
- package/src/detectors/registry/contact-variant.ts +4 -0
- package/src/detectors/registry/count-over-capacity.ts +4 -0
- package/src/detectors/registry/date-stamp-order.ts +4 -0
- package/src/detectors/registry/duration-product-mismatch.ts +4 -0
- package/src/detectors/registry/event-deadline.ts +4 -0
- package/src/detectors/registry/interval-count-mismatch.ts +4 -0
- package/src/detectors/registry/layout-code-mismatch.ts +4 -0
- package/src/detectors/registry/party-order.ts +4 -0
- package/src/detectors/registry/quantity-range.ts +4 -0
- package/src/detectors/registry/range-band-mismatch.ts +4 -0
- package/src/detectors/registry/rent-multiple-mismatch.ts +4 -0
- package/src/detectors/registry/requirement-lists.ts +4 -0
- package/src/detectors/registry/return-type.ts +4 -0
- package/src/detectors/registry/scaled-quantity-mismatch.ts +4 -0
- package/src/detectors/registry/step-time-sum-mismatch.ts +4 -0
- package/src/detectors/registry/time-range-reversed.ts +4 -0
- package/src/detectors/registry/unlisted-item-used.ts +4 -0
- package/src/detectors/registry/walk-time-distance-mismatch.ts +4 -0
- package/src/detectors/rent-multiple-mismatch.ts +56 -0
- package/src/detectors/requirement-lists.ts +65 -0
- package/src/detectors/return-type.ts +30 -0
- package/src/detectors/scaled-quantity.ts +49 -0
- package/src/detectors/signals.ts +3 -1
- package/src/detectors/step-time-sum.ts +55 -0
- package/src/detectors/structure-tree.ts +42 -6
- package/src/detectors/structure.ts +1 -1
- package/src/detectors/technical-docs.ts +12 -0
- package/src/detectors/time-order.ts +45 -6
- package/src/detectors/undefined-term.ts +20 -3
- package/src/detectors/unit-consistency.ts +50 -38
- package/src/detectors/unlisted-items.ts +72 -0
- package/src/detectors/use-count-words.ts +32 -0
- package/src/detectors/use-interval.ts +51 -0
- package/src/detectors/walk-time-distance-mismatch.ts +45 -0
- package/src/document.ts +4 -1
- package/src/facts/approximate-tolerance.ts +19 -0
- package/src/facts/item-amounts.ts +94 -0
- package/src/facts/labelled-facts.ts +29 -7
- package/src/facts/measure-units.ts +31 -0
- package/src/facts/measures.ts +113 -25
- package/src/facts/retention-periods.ts +216 -0
- package/src/facts/table-facts.ts +3 -0
- package/src/facts/unit-conflicts.ts +22 -9
- package/src/facts/unit-equivalents.ts +70 -0
- package/src/facts/unit-tolerance.ts +17 -0
- package/src/facts/unit-word.ts +35 -0
- package/src/facts/written-precision.ts +23 -0
- package/src/kana-spelling.ts +25 -0
- package/src/model-codes.ts +104 -0
- package/src/name-spelling-chars.ts +34 -0
- package/src/name-variants.ts +83 -7
- package/src/name-word-order.ts +149 -0
- package/src/phone-numbers.ts +26 -6
- package/src/place-names.ts +53 -14
- package/src/plugin.ts +7 -0
- package/src/preamble-paragraphs.ts +21 -2
- package/src/profile/parse.ts +10 -0
- package/src/return-type.ts +361 -0
- package/src/spacing-code.ts +26 -0
- package/src/structure/amount-range.ts +16 -7
- package/src/structure/capacity-count.ts +196 -0
- package/src/structure/connection-times.ts +226 -0
- package/src/structure/counted-amounts.ts +5 -4
- package/src/structure/date-order-tie.ts +86 -0
- package/src/structure/date-order.ts +41 -14
- package/src/structure/date-outside-period.ts +77 -7
- package/src/structure/date-range.ts +45 -9
- package/src/structure/deadline-after-event.ts +67 -0
- package/src/structure/definition-use.ts +59 -2
- package/src/structure/due-date.ts +8 -32
- package/src/structure/labelled-date-order.ts +84 -0
- package/src/structure/layout-codes.ts +210 -0
- package/src/structure/leg-times.ts +15 -0
- package/src/structure/measured-amounts.ts +138 -0
- package/src/structure/party-order.ts +115 -0
- package/src/structure/quantity-range.ts +54 -0
- package/src/structure/range-bands.ts +316 -0
- package/src/structure/rent-multiple.ts +277 -0
- package/src/structure/requirement-lists.ts +145 -0
- package/src/structure/scaled-quantities.ts +225 -0
- package/src/structure/stamp-order.ts +100 -0
- package/src/structure/step-times.ts +169 -0
- package/src/structure/term-deadline.ts +35 -0
- package/src/structure/time-order.ts +8 -7
- package/src/structure/time-range.ts +64 -0
- package/src/structure/total-line.ts +63 -0
- package/src/structure/total.ts +41 -6
- package/src/structure/unlisted-items.ts +207 -0
- package/src/structure/version-order.ts +10 -0
- package/src/structure/walk-time.ts +285 -0
- package/src/surname-near.ts +31 -0
- package/src/table-cells.ts +41 -0
- package/src/table-names.ts +113 -0
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { escapeRegExp } from "../orthography.ts";
|
|
2
|
+
import type { StructureIssue } from "./issues.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* rent-multiple-mismatch: a deposit, key money or guarantee given both as a number of months of rent and as an amount that
|
|
6
|
+
* is not that many months of the listing's rent (敷金 2か月(98,000円) with 賃料 98,000円; "Deposit: Six months' rent
|
|
7
|
+
* ($145,000)" with Rent $21,500). The rent is the nearest one stated before the deposit (several listings on one page each
|
|
8
|
+
* state their own); a document whose deposits come before any rent uses the rent only when it states one. When the document
|
|
9
|
+
* also states a fee beside the rent (管理費, service charge), the multiple may be of the rent alone or of the rent and the
|
|
10
|
+
* fee, since listings do both and seldom say which. A months count without an amount, an amount without a months count, a
|
|
11
|
+
* range of months, and two different pairs in one statement are not compared. The words come from the lexicons. Pure.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** One amount of money as read: its currency, its value, and the value of the word of scale written in it (万), if any. */
|
|
15
|
+
export type RentAmount = {
|
|
16
|
+
readonly offset: number;
|
|
17
|
+
readonly end: number;
|
|
18
|
+
readonly currency: string;
|
|
19
|
+
readonly value: number;
|
|
20
|
+
readonly scale: number | undefined;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type RentMultipleWords = {
|
|
24
|
+
/** Labels of the monthly rent (賃料, Rent). */
|
|
25
|
+
readonly rents: readonly string[];
|
|
26
|
+
/** Labels of a monthly fee paid with the rent (管理費, Service charge). */
|
|
27
|
+
readonly fees: readonly string[];
|
|
28
|
+
/** Labels of an amount stated in months of rent (敷金, 礼金, Security deposit). */
|
|
29
|
+
readonly multiples: readonly string[];
|
|
30
|
+
/** The word of a month after a count (か月, months). */
|
|
31
|
+
readonly months: readonly string[];
|
|
32
|
+
/** Words that may stand between a label or a months count and its amount (は, 分, of, equal to). */
|
|
33
|
+
readonly links: readonly string[];
|
|
34
|
+
/** Words that say an amount is not per month (per week, 年額, per sq ft); a rent line with one is not read. */
|
|
35
|
+
readonly periods: readonly string[];
|
|
36
|
+
/** Words that make the amount something other than the plain multiple (税込, tax included). */
|
|
37
|
+
readonly skips: readonly string[];
|
|
38
|
+
/** Marks and words that join the two ends of a range (〜, to). */
|
|
39
|
+
readonly connectors: readonly string[];
|
|
40
|
+
/** Numbers written as words (six, 一) and their values. */
|
|
41
|
+
readonly numberWords: readonly { readonly word: string; readonly value: number }[];
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/** An amount read as the interval its written digits allow (9.8万円 is anything from 97,500 to 98,500). */
|
|
45
|
+
export type Interval = { readonly low: number; readonly high: number };
|
|
46
|
+
|
|
47
|
+
/** A rent or fee as stated. range is undefined when the label has amounts that are not one monthly amount (a change, per week). */
|
|
48
|
+
type Stated = { readonly offset: number; readonly currency: string; readonly range: Interval | undefined; readonly written: string };
|
|
49
|
+
|
|
50
|
+
type Months = { readonly start: number; readonly end: number; readonly count: number };
|
|
51
|
+
|
|
52
|
+
/** Rounding to the currency's unit (a yen, a dollar) is allowed on top of the written precision. */
|
|
53
|
+
const UNIT_SLACK = 1;
|
|
54
|
+
|
|
55
|
+
const LATIN = /[A-Za-z]/u;
|
|
56
|
+
const SEPARATORS = /[\s||::==、,,・//'’()()[\][]-]/gu;
|
|
57
|
+
const PARENTHETICAL_WITHOUT_DIGITS = /[((][^()()0-90-9]*[))]/gu;
|
|
58
|
+
/** The possessive after a months count (2 months' rent, 1 month's rent). */
|
|
59
|
+
const POSSESSIVE = /['’]s?(?![A-Za-z])/gu;
|
|
60
|
+
const RANGE_START = /[0-90-9A-Za-z一二三四五六七八九十]\s*$/u;
|
|
61
|
+
const SENTENCE_END = /[。!?;;]|[.!?](?=\s|$)/u;
|
|
62
|
+
const DIGITS = "[0-90-9]+(?:[..][0-90-9]+)?";
|
|
63
|
+
|
|
64
|
+
const wordPattern = (word: string): string => {
|
|
65
|
+
const edgeStart = LATIN.test(word.charAt(0)) ? "(?<![A-Za-z])" : "";
|
|
66
|
+
const edgeEnd = LATIN.test(word.charAt(word.length - 1)) ? "(?![A-Za-z])" : "";
|
|
67
|
+
return `${edgeStart}${escapeRegExp(word)}${edgeEnd}`;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/** One alternation of the words, longest first so 月額賃料 wins over 賃料. Undefined when there are none. */
|
|
71
|
+
const alternation = (words: readonly string[]): string | undefined =>
|
|
72
|
+
words.length === 0
|
|
73
|
+
? undefined
|
|
74
|
+
: words
|
|
75
|
+
.toSorted((left, right) => right.length - left.length)
|
|
76
|
+
.map(wordPattern)
|
|
77
|
+
.join("|");
|
|
78
|
+
|
|
79
|
+
const occurrences = (text: string, words: readonly string[]): { readonly start: number; readonly end: number }[] => {
|
|
80
|
+
const pattern = alternation(words);
|
|
81
|
+
if (pattern === undefined) return [];
|
|
82
|
+
return [...text.matchAll(new RegExp(pattern, "giu"))].map((match) => ({ start: match.index, end: match.index + match[0].length }));
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const withoutWords = (text: string, words: readonly string[]): string => {
|
|
86
|
+
const pattern = alternation(words);
|
|
87
|
+
return pattern === undefined ? text : text.replace(new RegExp(pattern, "giu"), "");
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/** Whether only separators, bracketed words and link words stand in the gap (「 | 」, " (monthly): ", "の", "'s rent ("). */
|
|
91
|
+
export const isPlainGap = (gap: string, words: readonly string[]): boolean =>
|
|
92
|
+
withoutWords(gap.replace(PARENTHETICAL_WITHOUT_DIGITS, "").replace(POSSESSIVE, ""), words).replace(SEPARATORS, "") === "";
|
|
93
|
+
|
|
94
|
+
const decimalsOf = (written: string): number => /[..]([0-90-9]+)/u.exec(written)?.[1]?.length ?? 0;
|
|
95
|
+
|
|
96
|
+
/** The interval an amount's digits allow: a word of scale (9.8万) leaves half its last digit either way; plain digits are exact. */
|
|
97
|
+
export const intervalOf = (amount: RentAmount, written: string): Interval => {
|
|
98
|
+
const half = amount.scale === undefined ? 0 : (amount.scale * 10 ** -decimalsOf(written)) / 2;
|
|
99
|
+
return { low: amount.value - half, high: amount.value + half };
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const lineBounds = (text: string, offset: number): { readonly start: number; readonly end: number } => {
|
|
103
|
+
const end = text.indexOf("\n", offset);
|
|
104
|
+
return { start: text.lastIndexOf("\n", offset - 1) + 1, end: end === -1 ? text.length : end };
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const amountsBetween = (amounts: readonly RentAmount[], start: number, end: number): RentAmount[] =>
|
|
108
|
+
amounts.filter((amount) => amount.offset >= start && amount.end <= end);
|
|
109
|
+
|
|
110
|
+
/** A label preceded by a months count (2 months' rent, First month's rent) names a multiple, not the rent. */
|
|
111
|
+
const followsMonths = (before: string, words: RentMultipleWords): boolean => {
|
|
112
|
+
const trimmed = before.replace(/['’]s?\s*$/u, "").trimEnd();
|
|
113
|
+
return words.months.some((month) => trimmed.toLowerCase().endsWith(month.toLowerCase()));
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/** The second end of a range that starts with this amount (85,000円〜90,000円), if one follows. */
|
|
117
|
+
const rangeEnd = (text: string, first: RentAmount, next: RentAmount | undefined, words: RentMultipleWords): RentAmount | undefined => {
|
|
118
|
+
if (next === undefined || next.currency !== first.currency || next.value < first.value) return undefined;
|
|
119
|
+
const joint = text.slice(first.end, next.offset).trim();
|
|
120
|
+
return words.connectors.some((connector) => connector.toLowerCase() === joint.toLowerCase()) ? next : undefined;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/** Where a label's own amount must be: before the next label of any kind on its line (賃料 98,000円 管理費 6,000円). */
|
|
124
|
+
const labelledEnd = (text: string, from: number, lineEnd: number, words: RentMultipleWords): number => {
|
|
125
|
+
const [next] = occurrences(text.slice(from, lineEnd), [...words.rents, ...words.fees, ...words.multiples]);
|
|
126
|
+
return next === undefined ? lineEnd : from + next.start;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* A rent or fee stated with one amount or one range of amounts. With a second amount (a change from one to another, two
|
|
131
|
+
* units) or a word of another period on the line (per week, 年額), it is stated but its monthly amount is not known.
|
|
132
|
+
*/
|
|
133
|
+
const statedAt = (
|
|
134
|
+
text: string,
|
|
135
|
+
label: { readonly start: number; readonly end: number },
|
|
136
|
+
amounts: readonly RentAmount[],
|
|
137
|
+
words: RentMultipleWords,
|
|
138
|
+
): Stated[] => {
|
|
139
|
+
const line = lineBounds(text, label.start);
|
|
140
|
+
const [first, second, ...rest] = amountsBetween(amounts, label.end, labelledEnd(text, label.end, line.end, words));
|
|
141
|
+
if (first === undefined || !isPlainGap(text.slice(label.end, first.offset), words.links)) return [];
|
|
142
|
+
if (followsMonths(text.slice(line.start, label.start), words)) return [];
|
|
143
|
+
const last = rangeEnd(text, first, second, words) ?? first;
|
|
144
|
+
const written = text.slice(first.offset, last.end);
|
|
145
|
+
const unclear = (second !== undefined && last !== second) || rest.length > 0 || occurrences(text.slice(line.start, line.end), words.periods).length > 0;
|
|
146
|
+
if (unclear) return [{ offset: label.start, currency: first.currency, range: undefined, written }];
|
|
147
|
+
const range = { low: intervalOf(first, text.slice(first.offset, first.end)).low, high: intervalOf(last, text.slice(last.offset, last.end)).high };
|
|
148
|
+
return [{ offset: label.start, currency: first.currency, range, written }];
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
/** Each label of the kind followed on its line by an amount (賃料 | 98,000円, Rent: $1,600 to $1,700), in document order. */
|
|
152
|
+
const statedAmounts = (text: string, labels: readonly string[], amounts: readonly RentAmount[], words: RentMultipleWords): Stated[] =>
|
|
153
|
+
occurrences(text, labels).flatMap((label) => statedAt(text, label, amounts, words));
|
|
154
|
+
|
|
155
|
+
const numberWordValue = (word: string, words: RentMultipleWords): number | undefined =>
|
|
156
|
+
words.numberWords.find((entry) => entry.word.toLowerCase() === word.toLowerCase())?.value;
|
|
157
|
+
|
|
158
|
+
const countOf = (written: string, words: RentMultipleWords): number => numberWordValue(written, words) ?? Number(written.normalize("NFKC"));
|
|
159
|
+
|
|
160
|
+
/** The months counts in a piece of text (2か月, Six months), with the offset of the piece. */
|
|
161
|
+
export const monthsIn = (piece: string, offset: number, words: RentMultipleWords): Months[] => {
|
|
162
|
+
const marker = alternation(words.months);
|
|
163
|
+
if (marker === undefined) return [];
|
|
164
|
+
const numberWords = alternation(words.numberWords.map((entry) => entry.word));
|
|
165
|
+
const count = numberWords === undefined ? DIGITS : `${DIGITS}|${numberWords}`;
|
|
166
|
+
const pattern = new RegExp(`(?<![0-90-9.,,.])(${count})\\s?(?:${marker})`, "giu");
|
|
167
|
+
return [...piece.matchAll(pattern)].map((match) => ({
|
|
168
|
+
start: offset + match.index,
|
|
169
|
+
end: offset + match.index + match[0].length,
|
|
170
|
+
count: countOf(match[1] ?? "", words),
|
|
171
|
+
}));
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
/** Whether a months count is the upper end of a range (1〜2か月). */
|
|
175
|
+
const endsRange = (text: string, months: Months, words: RentMultipleWords): boolean => {
|
|
176
|
+
const before = text.slice(Math.max(0, months.start - 12), months.start).trimEnd();
|
|
177
|
+
return words.connectors.some((connector) => before.endsWith(connector) && RANGE_START.test(before.slice(0, -connector.length)));
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
type Pair = { readonly months: Months; readonly amount: RentAmount };
|
|
181
|
+
|
|
182
|
+
const gapBetween = (text: string, months: Months, amount: RentAmount): string =>
|
|
183
|
+
amount.offset >= months.end ? text.slice(months.end, amount.offset) : text.slice(amount.end, months.start);
|
|
184
|
+
|
|
185
|
+
/** A months count and an amount standing together, either way round (1か月(98,000円), $3,200 (2 months' rent)). */
|
|
186
|
+
const pairsIn = (text: string, months: readonly Months[], amounts: readonly RentAmount[], words: RentMultipleWords): Pair[] => {
|
|
187
|
+
const between = [...words.links, ...words.rents];
|
|
188
|
+
return months.flatMap((count) =>
|
|
189
|
+
amounts
|
|
190
|
+
.filter((amount) => (amount.offset >= count.end || amount.end <= count.start) && isPlainGap(gapBetween(text, count, amount), between))
|
|
191
|
+
.map((amount) => ({ months: count, amount })),
|
|
192
|
+
);
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
type Statement = { readonly label: number; readonly pair: Pair };
|
|
196
|
+
|
|
197
|
+
/** Where the statement of one label ends: at the next label of its kind, the end of the sentence, or the end of the line. */
|
|
198
|
+
const statementEnd = (text: string, from: number, nextLabel: number | undefined): number => {
|
|
199
|
+
const line = lineBounds(text, from);
|
|
200
|
+
const sentence = SENTENCE_END.exec(text.slice(from, line.end));
|
|
201
|
+
return Math.min(line.end, nextLabel ?? line.end, sentence === null ? line.end : from + sentence.index);
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
/** One label's months count and amount, when the statement holds exactly one such pair and no word that makes it another. */
|
|
205
|
+
const statementAt = (
|
|
206
|
+
text: string,
|
|
207
|
+
label: { readonly start: number; readonly end: number },
|
|
208
|
+
next: number | undefined,
|
|
209
|
+
amounts: readonly RentAmount[],
|
|
210
|
+
words: RentMultipleWords,
|
|
211
|
+
): Statement[] => {
|
|
212
|
+
const end = statementEnd(text, label.end, next);
|
|
213
|
+
const piece = text.slice(label.end, end);
|
|
214
|
+
if (occurrences(piece, words.skips).length > 0) return [];
|
|
215
|
+
const months = monthsIn(piece, label.end, words);
|
|
216
|
+
if (months.some((count) => endsRange(text, count, words))) return [];
|
|
217
|
+
const pairs = pairsIn(text, months, amountsBetween(amounts, label.end, end), words);
|
|
218
|
+
const [pair] = pairs;
|
|
219
|
+
const single = pairs.every((other) => other.months.count === pair?.months.count && other.amount.value === pair.amount.value);
|
|
220
|
+
return pair === undefined || !single ? [] : [{ label: label.start, pair }];
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
/** Every label of a multiple with one months count and one amount (敷金 | 2か月(98,000円)). */
|
|
224
|
+
const multipleStatements = (text: string, amounts: readonly RentAmount[], words: RentMultipleWords): Statement[] => {
|
|
225
|
+
const labels = occurrences(text, words.multiples);
|
|
226
|
+
return labels.flatMap((label, index) => statementAt(text, label, labels[index + 1]?.start, amounts, words));
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
/** The rent a statement at this offset is of: the nearest before it, or, before any, the one rent the document states. */
|
|
230
|
+
export const rentFor = <T extends Stated>(rents: readonly T[], offset: number): T | undefined => {
|
|
231
|
+
const before = rents.filter((rent) => rent.offset < offset).at(-1);
|
|
232
|
+
if (before !== undefined) return before;
|
|
233
|
+
const [first] = rents;
|
|
234
|
+
return rents.every((rent) => rent.range !== undefined && rent.range.low === first?.range?.low && rent.range.high === first.range.high) ? first : undefined;
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
/** The fees of the listing a rent belongs to: those after it and before the next rent; for the first rent, else those before it. */
|
|
238
|
+
export const feesFor = <T extends Stated>(rent: Stated, rents: readonly Stated[], fees: readonly T[]): T[] => {
|
|
239
|
+
const index = rents.indexOf(rent);
|
|
240
|
+
const next = rents[index + 1]?.offset ?? Number.POSITIVE_INFINITY;
|
|
241
|
+
const ofCurrency = fees.filter((fee) => fee.currency === rent.currency);
|
|
242
|
+
const after = ofCurrency.filter((fee) => fee.offset > rent.offset && fee.offset < next);
|
|
243
|
+
return after.length > 0 || index > 0 ? after : ofCurrency.filter((fee) => fee.offset < rent.offset);
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const plus = (left: Interval, right: Interval): Interval => ({ low: left.low + right.low, high: left.high + right.high });
|
|
247
|
+
|
|
248
|
+
/** The monthly amounts a multiple may be of: the rent alone, the rent with each fee, and the rent with every fee. */
|
|
249
|
+
export const monthlyBases = (rent: Interval, fees: readonly Interval[]): Interval[] => {
|
|
250
|
+
const withEach = fees.map((fee) => plus(rent, fee));
|
|
251
|
+
const withAll = fees.length > 1 ? [fees.reduce(plus, rent)] : [];
|
|
252
|
+
return [rent, ...withEach, ...withAll];
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
/** Whether the amount is the count of months of one of the bases, within the written precision and a unit's rounding. */
|
|
256
|
+
export const isMultiple = (count: number, amount: Interval, bases: readonly Interval[]): boolean =>
|
|
257
|
+
bases.some((base) => count * base.low - UNIT_SLACK <= amount.high && amount.low <= count * base.high + UNIT_SLACK);
|
|
258
|
+
|
|
259
|
+
const mismatchOf = (text: string, statement: Statement, rents: readonly Stated[], fees: readonly Stated[]): StructureIssue[] => {
|
|
260
|
+
const rent = rentFor(rents, statement.label);
|
|
261
|
+
const { months, amount } = statement.pair;
|
|
262
|
+
if (rent?.range === undefined || rent.currency !== amount.currency) return [];
|
|
263
|
+
const feeRanges = feesFor(rent, rents, fees).map((fee) => fee.range);
|
|
264
|
+
if (!feeRanges.every((range) => range !== undefined)) return [];
|
|
265
|
+
const bases = monthlyBases(rent.range, feeRanges);
|
|
266
|
+
if (isMultiple(months.count, intervalOf(amount, text.slice(amount.offset, amount.end)), bases)) return [];
|
|
267
|
+
const values = { months: text.slice(months.start, months.end), amount: text.slice(amount.offset, amount.end), rent: rent.written };
|
|
268
|
+
return [{ offset: Math.min(months.start, amount.offset), values }];
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
/** Every multiple of rent whose amount is not that many months of the listing's rent (or of the rent and its fee). */
|
|
272
|
+
export const rentMultipleMismatches = (text: string, amounts: readonly RentAmount[], words: RentMultipleWords): StructureIssue[] => {
|
|
273
|
+
const rents = statedAmounts(text, words.rents, amounts, words);
|
|
274
|
+
if (rents.length === 0) return [];
|
|
275
|
+
const fees = statedAmounts(text, words.fees, amounts, words);
|
|
276
|
+
return multipleStatements(text, amounts, words).flatMap((statement) => mismatchOf(text, statement, rents, fees));
|
|
277
|
+
};
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// required-listed-as-preferred: one skill or qualification listed under a required heading or label and again under a
|
|
2
|
+
// preferred one. The headings and labels, the units and words of a length of experience, and the words that only end an
|
|
3
|
+
// item come from the lexicons. Pure.
|
|
4
|
+
import { escapeRegExp } from "../orthography.ts";
|
|
5
|
+
import type { StructureIssue } from "./issues.ts";
|
|
6
|
+
|
|
7
|
+
/** A heading, a line standing alone (a label if it is one of the words), or one list item, in document order. */
|
|
8
|
+
export type RequirementBlock =
|
|
9
|
+
| { readonly kind: "heading"; readonly depth: number; readonly text: string; readonly offset: number }
|
|
10
|
+
| { readonly kind: "line"; readonly text: string; readonly offset: number }
|
|
11
|
+
| { readonly kind: "item"; readonly text: string; readonly offset: number };
|
|
12
|
+
|
|
13
|
+
export type ValuedWord = { readonly word: string; readonly value: number };
|
|
14
|
+
|
|
15
|
+
export type RequirementWords = {
|
|
16
|
+
/** Headings and labels of what an applicant must have (必須, Required) and of what is welcome (歓迎, Preferred). */
|
|
17
|
+
readonly required: readonly string[];
|
|
18
|
+
readonly preferred: readonly string[];
|
|
19
|
+
/** Units of a length of experience, valued in months (年 12, month 1). */
|
|
20
|
+
readonly units: readonly ValuedWord[];
|
|
21
|
+
/** Words that stand before or after a length and belong to it (at least, 以上, of). */
|
|
22
|
+
readonly before: readonly string[];
|
|
23
|
+
readonly after: readonly string[];
|
|
24
|
+
/** Numbers written as words (three, 三). */
|
|
25
|
+
readonly numbers: readonly ValuedWord[];
|
|
26
|
+
/** Words that only end an item and are not the skill (の方, こと). */
|
|
27
|
+
readonly endings: readonly string[];
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
type Kind = "required" | "preferred";
|
|
31
|
+
|
|
32
|
+
/** container is the offset of the heading holding the lists (a posting, 応募の条件), -1 for none; only one container's items are compared. */
|
|
33
|
+
type Requirement = { readonly kind: Kind; readonly text: string; readonly offset: number; readonly container: number };
|
|
34
|
+
|
|
35
|
+
/** The skill an item names with its length of experience taken out, and that length in months (0 when none). */
|
|
36
|
+
type Reading = { readonly core: string; readonly months: number };
|
|
37
|
+
|
|
38
|
+
const MARKS = /[*_`~]/gu;
|
|
39
|
+
|
|
40
|
+
const LATIN = /^[a-z]/u;
|
|
41
|
+
|
|
42
|
+
const plain = (text: string): string => text.normalize("NFKC").toLowerCase().replace(MARKS, "").replace(/\s+/gu, " ").trim();
|
|
43
|
+
|
|
44
|
+
/** A Latin word is matched at word edges; other scripts as written. */
|
|
45
|
+
const wordPattern = (word: string): string => {
|
|
46
|
+
const escaped = escapeRegExp(plain(word));
|
|
47
|
+
return LATIN.test(plain(word)) ? `(?<![a-z])${escaped}(?![a-z])` : escaped;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const alternation = (words: readonly string[]): string =>
|
|
51
|
+
words
|
|
52
|
+
.toSorted((left, right) => right.length - left.length)
|
|
53
|
+
.map(wordPattern)
|
|
54
|
+
.join("|");
|
|
55
|
+
|
|
56
|
+
/** The text without the characters at either end that the test accepts (no backtracking regex on long lines). */
|
|
57
|
+
const trimWhere = (text: string, isEdge: (char: string) => boolean): string => {
|
|
58
|
+
const chars = [...text];
|
|
59
|
+
const start = chars.findIndex((char) => !isEdge(char));
|
|
60
|
+
return start === -1 ? "" : chars.slice(start, chars.findLastIndex((char) => !isEdge(char)) + 1).join("");
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const LABEL_EDGE = /[\s【[[「((】]\]」))::]/u;
|
|
64
|
+
|
|
65
|
+
const asLabel = (text: string): string => trimWhere(plain(text).replace(/[-‐–]/gu, " "), (char) => LABEL_EDGE.test(char)).replace(/\s+/gu, " ");
|
|
66
|
+
|
|
67
|
+
const kindOfLabel = (label: string, words: RequirementWords): Kind | undefined => {
|
|
68
|
+
if (label === "") return undefined;
|
|
69
|
+
if (words.required.some((word) => asLabel(word) === label)) return "required";
|
|
70
|
+
return words.preferred.some((word) => asLabel(word) === label) ? "preferred" : undefined;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const HEADING_PARTS = /[・//、,,((::]/u;
|
|
74
|
+
|
|
75
|
+
/** A heading names its kind by its first part (必須スキル・経験, 歓迎(あれば尚可)); a line standing alone, by all of it. */
|
|
76
|
+
const kindOfHeading = (text: string, words: RequirementWords): Kind | undefined => kindOfLabel(asLabel(text.split(HEADING_PARTS)[0] ?? ""), words);
|
|
77
|
+
|
|
78
|
+
type Scope = { readonly depth: number; readonly kind: Kind | undefined; readonly offset: number };
|
|
79
|
+
|
|
80
|
+
type Walk = { readonly scopes: readonly Scope[]; readonly label: Kind | undefined; readonly items: readonly Requirement[] };
|
|
81
|
+
|
|
82
|
+
/** A heading opens a scope that its subheadings inherit unless they name their own kind; a label holds until the next heading or label. */
|
|
83
|
+
const step = (walk: Walk, block: RequirementBlock, words: RequirementWords): Walk => {
|
|
84
|
+
if (block.kind === "heading") {
|
|
85
|
+
const outer = walk.scopes.filter((scope) => scope.depth < block.depth);
|
|
86
|
+
const kind = kindOfHeading(block.text, words) ?? outer.at(-1)?.kind;
|
|
87
|
+
return { ...walk, scopes: [...outer, { depth: block.depth, kind, offset: block.offset }], label: undefined };
|
|
88
|
+
}
|
|
89
|
+
if (block.kind === "line") {
|
|
90
|
+
const kind = kindOfLabel(asLabel(block.text), words);
|
|
91
|
+
return kind === undefined ? walk : { ...walk, label: kind };
|
|
92
|
+
}
|
|
93
|
+
const kind = walk.label ?? walk.scopes.at(-1)?.kind;
|
|
94
|
+
const container = walk.scopes.findLast((scope) => scope.kind === undefined)?.offset ?? -1;
|
|
95
|
+
return kind === undefined ? walk : { ...walk, items: [...walk.items, { kind, text: block.text.trim(), offset: block.offset, container }] };
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const numberPattern = (words: RequirementWords): string =>
|
|
99
|
+
[String.raw`\d+(?:\.\d+)?`, alternation(words.numbers.map((entry) => entry.word))].filter((part) => part !== "").join("|");
|
|
100
|
+
|
|
101
|
+
const lengthPattern = (words: RequirementWords): RegExp | undefined => {
|
|
102
|
+
if (words.units.length === 0) return undefined;
|
|
103
|
+
const before = words.before.length === 0 ? "" : `(?:(?:${alternation(words.before)})\\s*)?`;
|
|
104
|
+
const after = words.after.length === 0 ? "" : `(?:\\s*(?:${alternation(words.after)}))*`;
|
|
105
|
+
return new RegExp(`${before}(${numberPattern(words)})\\s*\\+?\\s*(${alternation(words.units.map((unit) => unit.word))})['’]?${after}`, "gu");
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const valueOf = (written: string, words: readonly ValuedWord[]): number => words.find((entry) => plain(entry.word) === written)?.value ?? Number(written);
|
|
109
|
+
|
|
110
|
+
/** Punctuation, symbols and spaces, but not the + and # that end a name (C++, C#). */
|
|
111
|
+
const PUNCTUATION = /(?<![\p{L}\p{N}+#])[+#]|[^\p{L}\p{N}+#]/gu;
|
|
112
|
+
|
|
113
|
+
const EDGE_PUNCTUATION = /[\p{P}\s]/u;
|
|
114
|
+
|
|
115
|
+
const withoutEnding = (written: string, endings: readonly string[]): string => {
|
|
116
|
+
const text = trimWhere(written, (char) => char !== "#" && EDGE_PUNCTUATION.test(char));
|
|
117
|
+
const ending = endings.map(plain).find((word) => word !== "" && text.endsWith(word) && text.length > word.length);
|
|
118
|
+
return ending === undefined ? text : withoutEnding(text.slice(0, -ending.length), endings);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const readingOf = (text: string, words: RequirementWords): Reading => {
|
|
122
|
+
const written = plain(text);
|
|
123
|
+
const pattern = lengthPattern(words);
|
|
124
|
+
const lengths = pattern === undefined ? [] : [...written.matchAll(pattern)];
|
|
125
|
+
const months = lengths.map((match) => valueOf(match[1] ?? "", words.numbers) * (words.units.find((unit) => plain(unit.word) === match[2])?.value ?? 0));
|
|
126
|
+
const rest = pattern === undefined ? written : written.replace(pattern, " ");
|
|
127
|
+
return { core: withoutEnding(rest.replace(/\s+/gu, " ").trim(), words.endings).replace(PUNCTUATION, ""), months: Math.max(0, ...months) };
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/** A preferred item repeats a required one when it names the same skill and asks for no longer experience. */
|
|
131
|
+
const repeats = (preferred: Reading, required: Reading): boolean =>
|
|
132
|
+
preferred.core !== "" && preferred.core === required.core && preferred.months <= required.months;
|
|
133
|
+
|
|
134
|
+
/** The preferred items that repeat a required item of the same document, once each, in document order. */
|
|
135
|
+
export const requiredListedAsPreferred = (blocks: readonly RequirementBlock[], words: RequirementWords): StructureIssue[] => {
|
|
136
|
+
const { items } = blocks.reduce<Walk>((walk, block) => step(walk, block, words), { scopes: [], label: undefined, items: [] });
|
|
137
|
+
const required = items.filter((item) => item.kind === "required").map((item) => ({ item, reading: readingOf(item.text, words) }));
|
|
138
|
+
return items
|
|
139
|
+
.filter((item) => item.kind === "preferred")
|
|
140
|
+
.flatMap((item) => {
|
|
141
|
+
const reading = readingOf(item.text, words);
|
|
142
|
+
const match = required.find((candidate) => candidate.item.container === item.container && repeats(reading, candidate.reading));
|
|
143
|
+
return match === undefined ? [] : [{ offset: item.offset, values: { item: item.text, required: match.item.text } }];
|
|
144
|
+
});
|
|
145
|
+
};
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import type { Span } from "../plugin.ts";
|
|
2
|
+
import { escapeRegExp } from "../orthography.ts";
|
|
3
|
+
import { cellsOf, tablesOf, type Cell } from "../facts/table-facts.ts";
|
|
4
|
+
import { measuredValues, type MeasureUnit } from "../facts/measures.ts";
|
|
5
|
+
import { withoutEdgeMarks } from "../facts/trim-marks.ts";
|
|
6
|
+
import { linesOf, type Line } from "./lines.ts";
|
|
7
|
+
import type { Mark } from "./time-marks.ts";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A table with one column per number of servings or batch size (4人分 / 8人分, Serves 4 / Serves 8, ×1 / ×2), and the one
|
|
11
|
+
* row that does not grow by the columns' ratio while the others do. The words come from the language packages. Pure.
|
|
12
|
+
*/
|
|
13
|
+
export type ScaleWords = {
|
|
14
|
+
/** What marks a column as a number of servings (人分 after the number, Serves before it). */
|
|
15
|
+
readonly columns: readonly Mark[];
|
|
16
|
+
/** An amount that is left to the cook (少々, to taste): its row may stay as it is. */
|
|
17
|
+
readonly unscaled: readonly string[];
|
|
18
|
+
/** Spellings of one unit (teaspoon, teaspoons, tsp), each named by its group. */
|
|
19
|
+
readonly unitForms: readonly { readonly pattern: string; readonly group: string }[];
|
|
20
|
+
/** Units with a factor to a base unit (g and kg), so 1 kg and 1000 g read as one amount. */
|
|
21
|
+
readonly measures: readonly MeasureUnit[];
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/** amount is in the base unit when the unit has a factor (1 kg is 1000); written is the number as written (1). */
|
|
25
|
+
export type Amount = { readonly amount: number; readonly written: number; readonly unit: string };
|
|
26
|
+
|
|
27
|
+
export type ScaleSlip = {
|
|
28
|
+
/** The row's first cell: the item. */
|
|
29
|
+
readonly item: Cell;
|
|
30
|
+
/** The cell that does not scale, and the column it is in. */
|
|
31
|
+
readonly cell: Cell;
|
|
32
|
+
readonly heading: string;
|
|
33
|
+
/** The cell it is compared with, and that column. */
|
|
34
|
+
readonly other: Cell;
|
|
35
|
+
readonly otherHeading: string;
|
|
36
|
+
/** What the cell would hold if it scaled like the other rows, in the unit it is written in. */
|
|
37
|
+
readonly expected: number;
|
|
38
|
+
readonly scaledRows: number;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/** Fewer rows than this that scale exactly are no evidence that the table scales at all. */
|
|
42
|
+
export const MIN_SCALED_ROWS = 3;
|
|
43
|
+
const SAME = 1e-9;
|
|
44
|
+
const MIN_COLUMNS = 2;
|
|
45
|
+
|
|
46
|
+
const NUMBER_TEXT = "[0-9]+(?:\\.[0-9]+)?";
|
|
47
|
+
|
|
48
|
+
/** The number of servings a column heading names (4人分, Serves 8, ×2), or undefined. */
|
|
49
|
+
export const servingsOf = (heading: string, marks: readonly Mark[]): number | undefined => {
|
|
50
|
+
const text = withoutEdgeMarks(heading.normalize("NFKC").trim()).toLowerCase();
|
|
51
|
+
const read = marks
|
|
52
|
+
.map((mark) => {
|
|
53
|
+
const word = escapeRegExp(mark.pattern.normalize("NFKC").toLowerCase());
|
|
54
|
+
const pattern = mark.position === "before" ? `^${word}\\s*(${NUMBER_TEXT})$` : `^(${NUMBER_TEXT})\\s*${word}$`;
|
|
55
|
+
return new RegExp(pattern, "u").exec(text)?.[1];
|
|
56
|
+
})
|
|
57
|
+
.find((number) => number !== undefined);
|
|
58
|
+
const servings = read === undefined ? undefined : Number(read);
|
|
59
|
+
return servings === undefined || !Number.isFinite(servings) || servings <= 0 ? undefined : servings;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/** A whole or decimal number (1,000 and 1.5), with a fraction after it (1 1/2) or as a fraction (1/2, ½). */
|
|
63
|
+
const WHOLE = String.raw`[0-9]+(?:,[0-9]{3})*(?:\.[0-9]+)?`;
|
|
64
|
+
const FRACTION = String.raw`(?:\s([0-9]+)[/⁄]([0-9]+)|[/⁄]([0-9]+))?`;
|
|
65
|
+
const AMOUNT = new RegExp(String.raw`(?<![\p{N}.,/⁄])(${WHOLE})${FRACTION}(?![\p{N}/⁄])`, "gu");
|
|
66
|
+
|
|
67
|
+
const valueOf = (match: RegExpMatchArray): number => {
|
|
68
|
+
const whole = Number((match[1] ?? "").replace(/,/gu, ""));
|
|
69
|
+
if (match[4] !== undefined) return whole / Number(match[4]);
|
|
70
|
+
return match[2] === undefined ? whole : whole + Number(match[2]) / Number(match[3]);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const LETTER_BEFORE = "(?<!\\p{L})";
|
|
74
|
+
const LETTER_AFTER = "(?!\\p{L})";
|
|
75
|
+
|
|
76
|
+
/** The cell's text with its number taken out and each spelling of a unit written as its group: "2 teaspoons" is "#teaspoon". */
|
|
77
|
+
const unitKey = (text: string, numberAt: Span, forms: ScaleWords["unitForms"]): string => {
|
|
78
|
+
const rest = `${text.slice(0, numberAt.start)}#${text.slice(numberAt.end)}`.toLowerCase();
|
|
79
|
+
const groups = new Map(forms.map((form) => [form.pattern.toLowerCase(), form.group]));
|
|
80
|
+
const spellings = [...groups.keys()].filter((pattern) => pattern !== "").toSorted((left, right) => right.length - left.length);
|
|
81
|
+
const grouped =
|
|
82
|
+
spellings.length === 0
|
|
83
|
+
? rest
|
|
84
|
+
: rest.replace(
|
|
85
|
+
new RegExp(`${LETTER_BEFORE}(?:${spellings.map(escapeRegExp).join("|")})${LETTER_AFTER}`, "gu"),
|
|
86
|
+
(spelling) => `<${groups.get(spelling) ?? spelling}>`,
|
|
87
|
+
);
|
|
88
|
+
return grouped.replace(/\s+/gu, "");
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/** NFKC writes ½ as 1⁄2, so 1½ would read as 11⁄2: a space keeps the whole number apart (1 1⁄2). */
|
|
92
|
+
const VULGAR_AFTER_DIGIT = /([0-90-9])([¼½¾⅐-⅞])/gu;
|
|
93
|
+
|
|
94
|
+
const normalized = (cellText: string): string => withoutEdgeMarks(cellText.replace(VULGAR_AFTER_DIGIT, "$1 $2").normalize("NFKC").trim());
|
|
95
|
+
|
|
96
|
+
/** One cell's amount and the unit it is in. undefined when the cell holds no number, or more than one (1と1/2, 2 (300 g)). */
|
|
97
|
+
export const amountOf = (cellText: string, words: ScaleWords): Amount | undefined => {
|
|
98
|
+
const text = normalized(cellText);
|
|
99
|
+
const numbers = [...text.matchAll(AMOUNT)];
|
|
100
|
+
const [only] = numbers;
|
|
101
|
+
if (numbers.length !== 1 || only === undefined) return undefined;
|
|
102
|
+
const written = valueOf(only);
|
|
103
|
+
if (!Number.isFinite(written)) return undefined;
|
|
104
|
+
const [measured] = measuredValues(text, words.measures);
|
|
105
|
+
const factor = measured?.factors[0];
|
|
106
|
+
if (measured !== undefined && factor !== undefined && measured.start === only.index && measured.end === text.length) {
|
|
107
|
+
return { amount: measured.amount * factor, written: measured.amount, unit: `=${measured.dimension}` };
|
|
108
|
+
}
|
|
109
|
+
return { amount: written, written, unit: unitKey(text, { start: only.index, end: only.index + only[0].length }, words.unitForms) };
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const isUnscaled = (text: string, unscaled: readonly string[]): boolean => {
|
|
113
|
+
const lower = text.normalize("NFKC").toLowerCase();
|
|
114
|
+
return unscaled.some((word) => word !== "" && lower.includes(word.normalize("NFKC").toLowerCase()));
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
type Column = { readonly index: number; readonly servings: number; readonly heading: string };
|
|
118
|
+
|
|
119
|
+
type Row = { readonly item: Cell; readonly cells: readonly Cell[] };
|
|
120
|
+
|
|
121
|
+
/** The rows of one table whose item is named, as cells. */
|
|
122
|
+
const rowsOf = (rows: readonly Line[]): Row[] =>
|
|
123
|
+
rows.flatMap((line) => {
|
|
124
|
+
const cells = cellsOf(line);
|
|
125
|
+
const [item] = cells;
|
|
126
|
+
return item === undefined || withoutEdgeMarks(item.text) === "" ? [] : [{ item, cells }];
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
type Pair = {
|
|
130
|
+
readonly row: Row;
|
|
131
|
+
readonly baseCell: Cell;
|
|
132
|
+
readonly cell: Cell;
|
|
133
|
+
readonly from: Amount;
|
|
134
|
+
readonly to: Amount;
|
|
135
|
+
readonly scales: boolean;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
/** Each row's two cells, when both are amounts in one unit and the row is not left to the cook. */
|
|
139
|
+
const readablePairs = (rows: readonly Row[], base: Column, other: Column, words: ScaleWords): Pair[] =>
|
|
140
|
+
rows.flatMap((row): Pair[] => {
|
|
141
|
+
const [baseCell, cell] = [row.cells[base.index], row.cells[other.index]];
|
|
142
|
+
if (baseCell === undefined || cell === undefined) return [];
|
|
143
|
+
if (isUnscaled(baseCell.text, words.unscaled) || isUnscaled(cell.text, words.unscaled)) return [];
|
|
144
|
+
const [from, to] = [amountOf(baseCell.text, words), amountOf(cell.text, words)];
|
|
145
|
+
if (from === undefined || to === undefined || from.unit !== to.unit) return [];
|
|
146
|
+
const scaled = (from.amount * other.servings) / base.servings;
|
|
147
|
+
return [{ row, baseCell, cell, from, to, scales: Math.abs(to.amount - scaled) <= SAME * Math.max(Math.abs(scaled), 1) }];
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
type Departure = { readonly pair: Pair; readonly scaled: number };
|
|
151
|
+
|
|
152
|
+
/** The one row of a column that departs from the base column while at least MIN_SCALED_ROWS others scale exactly. */
|
|
153
|
+
const departureOf = (rows: readonly Row[], base: Column, other: Column, words: ScaleWords): Departure | undefined => {
|
|
154
|
+
const pairs = readablePairs(rows, base, other, words);
|
|
155
|
+
const departing = pairs.filter((pair) => !pair.scales);
|
|
156
|
+
const [only] = departing;
|
|
157
|
+
const scaled = pairs.length - departing.length;
|
|
158
|
+
return departing.length === 1 && only !== undefined && scaled >= MIN_SCALED_ROWS ? { pair: only, scaled } : undefined;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
/** What `amount` would be if it were `ratio` times `from`, in the unit `amount` is written in. */
|
|
162
|
+
export const expectedOf = (from: Amount, amount: Amount, ratio: number): number => {
|
|
163
|
+
const perWritten = amount.written === 0 || amount.amount === 0 ? 1 : amount.amount / amount.written;
|
|
164
|
+
return (from.amount * ratio) / perWritten;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const columnSlip = (base: Column, other: Column, { pair, scaled }: Departure): ScaleSlip => ({
|
|
168
|
+
item: pair.row.item,
|
|
169
|
+
cell: pair.cell,
|
|
170
|
+
heading: other.heading,
|
|
171
|
+
other: pair.baseCell,
|
|
172
|
+
otherHeading: base.heading,
|
|
173
|
+
expected: expectedOf(pair.from, pair.to, other.servings / base.servings),
|
|
174
|
+
scaledRows: scaled,
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const baseSlip = (base: Column, other: Column, { pair, scaled }: Departure): ScaleSlip => ({
|
|
178
|
+
item: pair.row.item,
|
|
179
|
+
cell: pair.baseCell,
|
|
180
|
+
heading: base.heading,
|
|
181
|
+
other: pair.cell,
|
|
182
|
+
otherHeading: other.heading,
|
|
183
|
+
expected: expectedOf(pair.to, pair.from, base.servings / other.servings),
|
|
184
|
+
scaledRows: scaled,
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
type Found = { readonly other: Column; readonly departure: Departure | undefined };
|
|
188
|
+
|
|
189
|
+
/** Whether the row's cells outside the first column scale with each other (a row fixed in every column does not). */
|
|
190
|
+
const othersAgree = (found: readonly Found[]): boolean => {
|
|
191
|
+
const perServing = found.map(({ other, departure }) => (departure === undefined ? Number.NaN : departure.pair.to.amount / other.servings));
|
|
192
|
+
const [first] = perServing;
|
|
193
|
+
return first !== undefined && perServing.every((value) => Math.abs(value - first) <= SAME * Math.max(Math.abs(first), 1));
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* One table's slips. Each column is compared with the first servings column. When the same row departs in every other column
|
|
198
|
+
* (three or more columns) and those columns scale with each other, it is the first column's cell that is off, and that cell is
|
|
199
|
+
* reported once; a row that departs everywhere without that (the same amount in every column) may be fixed on purpose.
|
|
200
|
+
*/
|
|
201
|
+
const tableSlips = (columns: readonly Column[], rows: readonly Row[], words: ScaleWords): ScaleSlip[] => {
|
|
202
|
+
const [base, ...others] = columns;
|
|
203
|
+
if (base === undefined) return [];
|
|
204
|
+
const found: Found[] = others.map((other) => ({ other, departure: departureOf(rows, base, other, words) }));
|
|
205
|
+
const [first] = found;
|
|
206
|
+
const offRows = new Set(found.map(({ departure }) => departure?.pair.row));
|
|
207
|
+
if (others.length > 1 && offRows.size === 1 && first?.departure !== undefined) {
|
|
208
|
+
return othersAgree(found) ? [baseSlip(base, first.other, first.departure)] : [];
|
|
209
|
+
}
|
|
210
|
+
return found.flatMap(({ other, departure }) => (departure === undefined ? [] : [columnSlip(base, other, departure)]));
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const columnsOf = (header: Line, marks: readonly Mark[]): Column[] =>
|
|
214
|
+
cellsOf(header).flatMap((cell, index): Column[] => {
|
|
215
|
+
const servings = index === 0 ? undefined : servingsOf(cell.text, marks);
|
|
216
|
+
return servings === undefined ? [] : [{ index, servings, heading: withoutEdgeMarks(cell.text.trim()) }];
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
/** Every table of the source with two or more servings columns, and the cells in them that do not scale. */
|
|
220
|
+
export const scaleSlips = (source: string, words: ScaleWords): ScaleSlip[] =>
|
|
221
|
+
tablesOf(linesOf(source)).flatMap((table) => {
|
|
222
|
+
const columns = columnsOf(table.header, words.columns);
|
|
223
|
+
const distinct = new Set(columns.map((column) => column.servings));
|
|
224
|
+
return distinct.size === columns.length && columns.length >= MIN_COLUMNS ? tableSlips(columns, rowsOf(table.rows), words) : [];
|
|
225
|
+
});
|