chronolizer 0.3.0 → 0.4.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.
@@ -4,7 +4,7 @@ import { normalizeNaturalText } from "../natural/text.mjs";
4
4
  import { defineLanguagePlugin, languagePluginsLayer } from "../language/registry.mjs";
5
5
  import { correctWhitespaceSeparatedText } from "../natural/correction.mjs";
6
6
  import { completeNaturalPhrases, fixedCalendarPeriodPhrases, naturalCount, prefixNaturalPhrases } from "../natural/suggestion.mjs";
7
- import { absoluteDatePeriod, calendarPeriodOffset, candidate, currentYearDatePeriods, datedPeriods, datedQuarterPeriods, fixedDatePeriod, fixedMonthPeriod, fixedQuarterPeriod, fixedYearPeriod, fromNowRange, futurePeriod, futureRange, isoDate, joinedNowCandidate, joinedPeriodCandidate, monthOfRelativeYear, namedCurrentYearDatePeriod, namedDatePeriod, openBoundaryCandidate, parseTrailingCount, periodBoundaryCandidate, periodEndDay, periodRange, periodStartDay, periodToDateRange, periodsFromPhrases, quarterOfRelativeYear, relativePeriod, relativeWeekend, remainingPeriodRange, renderPeriodRange, textAt, trailingPeriod, trailingRange, untilNowRange, validYear } from "./shared.mjs";
7
+ import { absoluteDatePeriod, calendarPeriodOffset, candidate, compileCountAliasNormalizer, compoundCountAliases, countAliasVocabulary, currentYearDatePeriods, datedPeriods, datedQuarterPeriods, decimalTens, decomposeShiftedPeriodRange, fixedDatePeriod, fixedMonthPeriod, fixedQuarterPeriod, fixedYearPeriod, fromNowRange, futurePeriod, futureRange, isoDate, joinedNowCandidate, joinedPeriodCandidate, monthOfRelativeYear, namedCurrentYearDatePeriod, namedDatePeriod, openBoundaryCandidate, parseTrailingCount, periodBoundaryCandidate, periodEndDay, periodRange, periodStartDay, periodToDateRange, periodsFromPhrases, quarterOfRelativeYear, relativePeriod, relativeWeekday, relativeWeekend, remainingPeriodRange, renderPeriodRange, sequentialCountAliases, shiftPeriod, textAt, trailingPeriod, trailingRange, untilNowRange, validYear } from "./shared.mjs";
8
8
  import { Effect, Option, String as String$1 } from "effect";
9
9
  //#region src/locales/de.ts
10
10
  const months = [
@@ -21,6 +21,42 @@ const months = [
21
21
  "november",
22
22
  "dezember"
23
23
  ];
24
+ const germanCountWords = [
25
+ "zwei",
26
+ "drei",
27
+ "vier",
28
+ "fünf",
29
+ "sechs",
30
+ "sieben",
31
+ "acht",
32
+ "neun",
33
+ "zehn",
34
+ "elf",
35
+ "zwölf",
36
+ "dreizehn",
37
+ "vierzehn",
38
+ "fünfzehn",
39
+ "sechzehn",
40
+ "siebzehn",
41
+ "achtzehn",
42
+ "neunzehn",
43
+ "zwanzig"
44
+ ];
45
+ const germanCountOnes = [[1, "ein"], ...germanCountWords.slice(0, 8).map((word, index) => [index + 2, word])];
46
+ const germanCountAliases = [...sequentialCountAliases(germanCountWords.map((word) => [word]), 2), ...compoundCountAliases(decimalTens([
47
+ "zwanzig",
48
+ "dreißig",
49
+ "vierzig",
50
+ "fünfzig",
51
+ "sechzig",
52
+ "siebzig",
53
+ "achtzig",
54
+ "neunzig"
55
+ ]), germanCountOnes, (ten, one) => [`${one}und${ten}`])];
56
+ const normalizeGermanCounts = compileCountAliasNormalizer(germanCountAliases);
57
+ const germanCountVocabulary = new Set(countAliasVocabulary(germanCountAliases));
58
+ const correctGerman = (input, vocabulary) => correctWhitespaceSeparatedText(input, vocabulary, germanCountVocabulary);
59
+ const normalizeGerman = (input, locale) => normalizeGermanCounts(normalizeNaturalText(input, locale));
24
60
  const monthAbbreviations = [
25
61
  ["jan"],
26
62
  ["feb"],
@@ -118,6 +154,24 @@ const unitPhrases = [
118
154
  }
119
155
  ];
120
156
  const title = (value) => `${value.slice(0, 1).toLocaleUpperCase("de")}${value.slice(1)}`;
157
+ const weekdays = [
158
+ "montag",
159
+ "dienstag",
160
+ "mittwoch",
161
+ "donnerstag",
162
+ "freitag",
163
+ "samstag",
164
+ "sonntag"
165
+ ];
166
+ const nextWeekdays = weekdays.flatMap((weekday, day) => [{
167
+ phrase: `nächster ${weekday}`,
168
+ day,
169
+ canonical: `nächster ${title(weekday)}`
170
+ }, {
171
+ phrase: `nächsten ${weekday}`,
172
+ day,
173
+ canonical: `nächster ${title(weekday)}`
174
+ }]);
121
175
  const canonicalToDate = (entry) => `seit ${title(entry.compound)}beginn`;
122
176
  const canonicalRelative = (value) => {
123
177
  const separator = value.lastIndexOf(" ");
@@ -595,8 +649,8 @@ const quarterNumber = (value) => {
595
649
  return index === -1 ? void 0 : index + 1;
596
650
  };
597
651
  const relativeDirection = (value) => {
598
- if (value === "letzten") return -1;
599
- if (value === "nächsten") return 1;
652
+ if (value.startsWith("letzt")) return -1;
653
+ if (value.startsWith("nächst")) return 1;
600
654
  return 0;
601
655
  };
602
656
  const parseQuarter = (input) => {
@@ -624,13 +678,13 @@ const parseQuarter = (input) => {
624
678
  const quarter = quarterNumber(textAt(standalone.value, 1));
625
679
  return quarter === void 0 ? Option.none() : Option.some(quarterOfRelativeYear(quarter, 0, `Q${quarter}`));
626
680
  };
627
- const parseBasePeriod = (input) => {
628
- const absoluteDate = absoluteDatePeriod(input, "de");
629
- if (Option.isSome(absoluteDate)) return absoluteDate;
630
- const namedDate = parseNamedDate(input);
631
- if (Option.isSome(namedDate)) return namedDate;
632
- const quarter = parseQuarter(input);
633
- if (Option.isSome(quarter)) return quarter;
681
+ const parseDatedPeriod = (input) => {
682
+ const knownPeriod = Option.firstSomeOf([
683
+ absoluteDatePeriod(input, "de"),
684
+ parseNamedDate(input),
685
+ parseQuarter(input)
686
+ ]);
687
+ if (Option.isSome(knownPeriod)) return knownPeriod;
634
688
  const yearMatch = String$1.match(/^(?:(?:das )?(?:kalender)?jahr )?(\d{4})$/u)(input);
635
689
  if (Option.isSome(yearMatch)) {
636
690
  const year = validYear(textAt(yearMatch.value, 1));
@@ -642,6 +696,18 @@ const parseBasePeriod = (input) => {
642
696
  const year = validYear(textAt(monthYear.value, 2));
643
697
  if (month !== void 0 && year !== void 0) return Option.some(fixedMonthPeriod(year, month, `${title(textAt(months, month - 1))} ${year}`));
644
698
  }
699
+ return Option.none();
700
+ };
701
+ const parseRelativeMonth = (input) => {
702
+ const prefixedRelativeMonth = String$1.match(/^(letzter|dieser|nächster) ([a-zäöüß]+\.?)$/u)(input);
703
+ if (Option.isSome(prefixedRelativeMonth)) {
704
+ const month = monthNumber(textAt(prefixedRelativeMonth.value, 2));
705
+ if (month !== void 0) {
706
+ const modifier = textAt(prefixedRelativeMonth.value, 1);
707
+ const direction = relativeDirection(modifier);
708
+ return Option.some(monthOfRelativeYear(month, direction, `${modifier} ${title(textAt(months, month - 1))}`));
709
+ }
710
+ }
645
711
  const relativeMonth = String$1.match(/^([a-zäöüß]+\.?) (letzten|dieses|nächsten) jahres$/u)(input);
646
712
  if (Option.isSome(relativeMonth)) {
647
713
  const directionText = textAt(relativeMonth.value, 2);
@@ -651,11 +717,18 @@ const parseBasePeriod = (input) => {
651
717
  }
652
718
  const standaloneMonth = monthNumber(input);
653
719
  if (standaloneMonth !== void 0) return Option.some(monthOfRelativeYear(standaloneMonth, 0, title(textAt(months, standaloneMonth - 1))));
720
+ return Option.none();
721
+ };
722
+ const parseBasePeriod = (input) => {
723
+ const knownPeriod = Option.firstSomeOf([parseDatedPeriod(input), parseRelativeMonth(input)]);
724
+ if (Option.isSome(knownPeriod)) return knownPeriod;
654
725
  if (input === "wochenende" || input === "dieses wochenende" || input === "am wochenende") return Option.some(relativeWeekend(0, "dieses Wochenende"));
655
726
  if (input === "letztes wochenende" || input === "vergangenes wochenende") return Option.some(relativeWeekend(-1, "letztes Wochenende"));
656
727
  if (input === "nächstes wochenende" || input === "kommendes wochenende") return Option.some(relativeWeekend(1, "nächstes Wochenende"));
657
728
  if (input === "vorletztes wochenende") return Option.some(relativeWeekend(-2, "vorletztes Wochenende"));
658
729
  if (input === "übernächstes wochenende") return Option.some(relativeWeekend(2, "übernächstes Wochenende"));
730
+ const weekday = nextWeekdays.find((entry) => entry.phrase === input);
731
+ if (weekday !== void 0) return Option.some(relativeWeekday(weekday.day, 1, weekday.canonical));
659
732
  const relative = unitPhrases.find((entry) => entry.current === input || entry.previous === input || entry.next === input);
660
733
  if (relative !== void 0) {
661
734
  let direction = 0;
@@ -668,10 +741,31 @@ const parseBasePeriod = (input) => {
668
741
  return Option.none();
669
742
  };
670
743
  const parsePeriod = (input) => {
744
+ const shifted = String$1.match(/^(.+) (vor|in) ([1-9]\d*) (tag|tagen|woche|wochen|monat|monaten|quartal|quartalen|jahr|jahren)$/u)(input);
745
+ if (Option.isSome(shifted)) {
746
+ const amount = parseTrailingCount(textAt(shifted.value, 3));
747
+ const unitText = textAt(shifted.value, 4);
748
+ const entry = unitPhrases.find((unit) => [
749
+ unit.noun,
750
+ unit.plural,
751
+ unit.dative
752
+ ].includes(unitText));
753
+ const period = parsePeriod(textAt(shifted.value, 1));
754
+ if (Option.isSome(amount) && entry !== void 0 && Option.isSome(period)) {
755
+ const past = textAt(shifted.value, 2) === "vor";
756
+ const direction = past ? -amount.value : amount.value;
757
+ const noun = amount.value === 1 ? entry.noun : entry.dative;
758
+ const canonical = `${period.value.canonical} ${past ? "vor" : "in"} ${amount.value} ${title(noun)}`;
759
+ return Option.some(shiftPeriod(period.value, direction, entry.unit, canonical));
760
+ }
761
+ }
671
762
  const edge = String$1.match(/^(anfang|beginn|ende) (.+)$/u)(input);
672
763
  if (Option.isSome(edge)) {
673
764
  const edgeName = textAt(edge.value, 1);
674
- const period = parseBasePeriod(textAt(edge.value, 2));
765
+ const periodText = textAt(edge.value, 2);
766
+ const basePeriod = parseBasePeriod(periodText);
767
+ const implicit = unitPhrases.find((entry) => entry.genitive === periodText);
768
+ const period = Option.isSome(basePeriod) || implicit === void 0 ? basePeriod : Option.some(relativePeriod(implicit.unit, 0, implicit.current));
675
769
  if (Option.isSome(period)) {
676
770
  const canonical = `${edgeName === "ende" ? "Ende" : "Anfang"} ${withRelativeGenitive(period.value.canonical)}`;
677
771
  return Option.some(edgeName === "ende" ? periodEndDay(period.value, canonical) : periodStartDay(period.value, canonical));
@@ -683,7 +777,8 @@ const parsePeriod = (input) => {
683
777
  "für ",
684
778
  "während "
685
779
  ].find((prefix) => input.startsWith(prefix));
686
- return parseBasePeriod(wrapper === void 0 ? input : input.slice(wrapper.length));
780
+ const base = parseBasePeriod(wrapper === void 0 ? input : input.slice(wrapper.length));
781
+ return Option.isSome(base) ? base : parseCalendarOffset(input);
687
782
  };
688
783
  const currentBoundaryCandidate = (input) => {
689
784
  const match = currentBoundaryPhrases.find((entry) => entry.phrase === input);
@@ -731,7 +826,7 @@ const parseCalendarOffset = (input) => {
731
826
  const isPast = textAt(singular.value, 1) === "vor";
732
827
  const direction = isPast ? -1 : 1;
733
828
  const canonical = `${isPast ? "vor" : "in"} ${canonicalRelative(entry.indefiniteDative)}`;
734
- return Option.some(candidate(periodRange(relativePeriod(entry.unit, direction, canonical)), canonical));
829
+ return Option.some(relativePeriod(entry.unit, direction, canonical));
735
830
  }
736
831
  }
737
832
  const prefixed = String$1.match(/^(vor|in) ([1-9]\d*) (tag|tagen|woche|wochen|monat|monaten|quartal|quartalen|jahr|jahren)$/u)(input);
@@ -749,7 +844,7 @@ const parseCalendarOffset = (input) => {
749
844
  const directionName = isPast ? "vor" : "in";
750
845
  const noun = amount.value === 1 ? entry.noun : entry.dative;
751
846
  const canonical = `${directionName} ${amount.value} ${title(noun)}`;
752
- return Option.some(candidate(periodRange(relativePeriod(entry.unit, direction, canonical)), canonical));
847
+ return Option.some(relativePeriod(entry.unit, direction, canonical));
753
848
  };
754
849
  const parseRollingPeriod = (input) => {
755
850
  const past = String$1.match(/^(?:(?:die letzten|letzten|letzte|vergangene|vorherige) )?([1-9]\d*) (tag|tage|woche|wochen|monat|monate|quartal|quartale|jahr|jahre)$/u)(input);
@@ -804,8 +899,6 @@ const parseGerman = (input) => {
804
899
  if (remaining !== void 0) return Option.some(candidate(remainingPeriodRange(remaining.entry.unit), `Rest ${canonicalRelative(remaining.entry.genitive)}`));
805
900
  if (["bis heute", "bis jetzt"].includes(input)) return Option.some(candidate(untilNowRange(), "bis heute"));
806
901
  if (["ab jetzt", "von jetzt an"].includes(input)) return Option.some(candidate(fromNowRange(), "ab jetzt"));
807
- const offset = parseCalendarOffset(input);
808
- if (Option.isSome(offset)) return offset;
809
902
  const rolling = parseRollingPeriod(input);
810
903
  if (Option.isSome(rolling)) return rolling;
811
904
  const toDate = toDatePhrases.find((entry) => entry.phrase === input);
@@ -818,7 +911,12 @@ const parseGerman = (input) => {
818
911
  ["von ", " bis heute"],
819
912
  ["vom ", " bis heute"],
820
913
  ["zwischen ", " und heute"]
821
- ], ["von heute bis ", "zwischen heute und "], parsePeriod, (period) => `von ${period} bis heute`, (period) => `von heute bis ${period}`);
914
+ ], [
915
+ "von heute bis ",
916
+ "zwischen heute und ",
917
+ "heute bis ",
918
+ "jetzt bis "
919
+ ], parsePeriod, (period) => `von ${period} bis heute`, (period) => `von heute bis ${period}`);
822
920
  if (Option.isSome(nowBounded)) return nowBounded;
823
921
  const bounded = joinedPeriodCandidate(input, [
824
922
  ["von ", " bis einschließlich "],
@@ -877,9 +975,12 @@ const staticPeriodPhrases = [
877
975
  ]),
878
976
  ...months.flatMap((month) => [
879
977
  month,
978
+ `letzter ${month}`,
979
+ `nächster ${month}`,
880
980
  `${month} letzten jahres`,
881
981
  `${month} nächsten jahres`
882
- ])
982
+ ]),
983
+ ...nextWeekdays.map((entry) => entry.phrase)
883
984
  ];
884
985
  const staticPeriods = periodsFromPhrases(staticPeriodPhrases, parsePeriod);
885
986
  const boundaryPrefixes = [
@@ -936,6 +1037,17 @@ const suggestGerman = (input, limit) => {
936
1037
  ], limit);
937
1038
  };
938
1039
  const renderGerman = (range) => {
1040
+ const shifted = decomposeShiftedPeriodRange(range);
1041
+ if (Option.isSome(shifted)) {
1042
+ const base = renderGerman(shifted.value.baseRange);
1043
+ const entry = unitPhrases.find((unit) => unit.unit === shifted.value.unit);
1044
+ if (Option.isSome(base) && entry !== void 0) {
1045
+ const amount = Math.abs(shifted.value.amount);
1046
+ const noun = amount === 1 ? entry.noun : entry.dative;
1047
+ const direction = shifted.value.amount < 0 ? "vor" : "in";
1048
+ return Option.some(`${base.value} ${direction} ${amount} ${title(noun)}`);
1049
+ }
1050
+ }
939
1051
  const offset = calendarPeriodOffset(range);
940
1052
  if (Option.isSome(offset) && Math.abs(offset.value.amount) > 1) {
941
1053
  const entry = unitPhrases.find((unit) => unit.unit === offset.value.unit);
@@ -963,6 +1075,7 @@ const GermanContribution = new BaseLanguageContribution({
963
1075
  locale: "de",
964
1076
  vocabulary: [
965
1077
  ...months,
1078
+ ...weekdays,
966
1079
  ...quarterNames,
967
1080
  "q1",
968
1081
  "q2",
@@ -1024,8 +1137,8 @@ const GermanContribution = new BaseLanguageContribution({
1024
1137
  "zwischen",
1025
1138
  "zuvor"
1026
1139
  ],
1027
- normalize: normalizeNaturalText,
1028
- correct: correctWhitespaceSeparatedText,
1140
+ normalize: normalizeGerman,
1141
+ correct: correctGerman,
1029
1142
  parseExact: parseGerman,
1030
1143
  suggest: suggestGerman,
1031
1144
  render: renderGerman
@@ -3,7 +3,7 @@ import { normalizeNaturalText } from "../natural/text.mjs";
3
3
  import { defineLanguagePlugin, languagePluginsLayer } from "../language/registry.mjs";
4
4
  import { correctWhitespaceSeparatedText } from "../natural/correction.mjs";
5
5
  import { completeNaturalPhrases, fixedCalendarPeriodPhrases, naturalCount, prefixNaturalPhrases } from "../natural/suggestion.mjs";
6
- import { absoluteDatePeriod, calendarPeriodOffset, candidate, currentYearDatePeriods, datedPeriods, datedQuarterPeriods, fixedMonthPeriod, fixedQuarterPeriod, fixedYearPeriod, fromNowRange, futurePeriod, futureRange, joinedNowCandidate, joinedPeriodCandidate, monthOfRelativeYear, namedCurrentYearDatePeriod, namedDatePeriod, openBoundaryCandidate, parseTrailingCount, periodEndDay, periodPreviousDay, periodRange, periodStartDay, periodToDateRange, periodsFromPhrases, quarterOfRelativeYear, relativePeriod, relativeWeekend, remainingPeriodRange, renderPeriodRange, textAt, trailingPeriod, trailingRange, untilNowRange, validYear } from "./shared.mjs";
6
+ import { absoluteDatePeriod, calendarPeriodOffset, candidate, compileCountAliasNormalizer, compoundCountAliases, countAliasVocabulary, currentYearDatePeriods, datedPeriods, datedQuarterPeriods, decimalTens, decomposeShiftedPeriodRange, fixedMonthPeriod, fixedQuarterPeriod, fixedYearPeriod, fromNowRange, futurePeriod, futureRange, joinedNowCandidate, joinedPeriodCandidate, monthOfRelativeYear, namedCurrentYearDatePeriod, namedDatePeriod, openBoundaryCandidate, parseTrailingCount, periodEndDay, periodPreviousDay, periodRange, periodStartDay, periodToDateRange, periodsFromPhrases, quarterOfRelativeYear, relativePeriod, relativeWeekday, relativeWeekend, remainingPeriodRange, renderPeriodRange, sequentialCountAliases, shiftPeriod, textAt, trailingPeriod, trailingRange, untilNowRange, validYear } from "./shared.mjs";
7
7
  import { Effect, Option, String as String$1 } from "effect";
8
8
  //#region src/locales/en.ts
9
9
  const months = [
@@ -34,12 +34,59 @@ const monthAbbreviations = [
34
34
  ["nov"],
35
35
  ["dec"]
36
36
  ];
37
+ const weekdays = [
38
+ "monday",
39
+ "tuesday",
40
+ "wednesday",
41
+ "thursday",
42
+ "friday",
43
+ "saturday",
44
+ "sunday"
45
+ ];
46
+ const nextWeekdayPhrases = weekdays.map((weekday) => `next ${weekday}`);
37
47
  const quarterNames = [
38
48
  "first",
39
49
  "second",
40
50
  "third",
41
51
  "fourth"
42
52
  ];
53
+ const englishCountWords = [
54
+ "one",
55
+ "two",
56
+ "three",
57
+ "four",
58
+ "five",
59
+ "six",
60
+ "seven",
61
+ "eight",
62
+ "nine",
63
+ "ten",
64
+ "eleven",
65
+ "twelve",
66
+ "thirteen",
67
+ "fourteen",
68
+ "fifteen",
69
+ "sixteen",
70
+ "seventeen",
71
+ "eighteen",
72
+ "nineteen",
73
+ "twenty"
74
+ ];
75
+ const englishCountOnes = englishCountWords.slice(0, 9).map((word, index) => [index + 1, word]);
76
+ const englishCountAliases = [...sequentialCountAliases(englishCountWords.map((word) => [word]), 1), ...compoundCountAliases(decimalTens([
77
+ "twenty",
78
+ "thirty",
79
+ "forty",
80
+ "fifty",
81
+ "sixty",
82
+ "seventy",
83
+ "eighty",
84
+ "ninety"
85
+ ]), englishCountOnes, (ten, one) => [`${ten} ${one}`, `${ten}-${one}`])];
86
+ const normalizeEnglishCounts = compileCountAliasNormalizer(englishCountAliases);
87
+ const englishCountVocabulary = new Set(countAliasVocabulary(englishCountAliases));
88
+ const correctEnglish = (input, vocabulary) => correctWhitespaceSeparatedText(input, vocabulary, englishCountVocabulary);
89
+ const normalizeEnglish = (input, locale) => normalizeEnglishCounts(normalizeNaturalText(input, locale));
43
90
  const units = [
44
91
  [
45
92
  "day",
@@ -170,13 +217,13 @@ const parseNamedDate = (input) => {
170
217
  if (Option.isNone(current)) return Option.none();
171
218
  return Option.isSome(currentDayFirst) ? namedCurrentYearDatePeriod(textAt(current.value, 2), textAt(current.value, 1), monthNumber, currentDateLabel) : namedCurrentYearDatePeriod(textAt(current.value, 1), textAt(current.value, 2), monthNumber, currentDateLabel);
172
219
  };
173
- const parseBasePeriod = (input) => {
174
- const absoluteDate = absoluteDatePeriod(input, "en");
175
- if (Option.isSome(absoluteDate)) return absoluteDate;
176
- const namedDate = parseNamedDate(input);
177
- if (Option.isSome(namedDate)) return namedDate;
178
- const quarter = parseQuarter(input);
179
- if (Option.isSome(quarter)) return quarter;
220
+ const parseDatedPeriod = (input) => {
221
+ const knownPeriod = Option.firstSomeOf([
222
+ absoluteDatePeriod(input, "en"),
223
+ parseNamedDate(input),
224
+ parseQuarter(input)
225
+ ]);
226
+ if (Option.isSome(knownPeriod)) return knownPeriod;
180
227
  const yearMatch = String$1.match(/^(?:(?:the )?(?:calendar )?year )?(\d{4})$/u)(input);
181
228
  if (Option.isSome(yearMatch)) {
182
229
  const year = validYear(textAt(yearMatch.value, 1));
@@ -188,6 +235,15 @@ const parseBasePeriod = (input) => {
188
235
  const year = validYear(textAt(monthYear.value, 2));
189
236
  if (month !== void 0 && year !== void 0) return Option.some(fixedMonthPeriod(year, month, `${title(textAt(months, month - 1))} ${year}`));
190
237
  }
238
+ return Option.none();
239
+ };
240
+ const parseRelativeMonth = (input) => {
241
+ const prefixedRelativeMonth = String$1.match(/^(last|this|next) ([a-z]+\.?)$/u)(input);
242
+ if (Option.isSome(prefixedRelativeMonth)) {
243
+ const directionText = textAt(prefixedRelativeMonth.value, 1);
244
+ const month = monthNumber(textAt(prefixedRelativeMonth.value, 2));
245
+ if (month !== void 0) return Option.some(monthOfRelativeYear(month, relativeDirection(directionText), `${directionText} ${title(textAt(months, month - 1))}`));
246
+ }
191
247
  const relativeMonth = String$1.match(/^([a-z]+\.?) (?:of )?(last|this|next) year$/u)(input);
192
248
  if (Option.isSome(relativeMonth)) {
193
249
  const directionText = textAt(relativeMonth.value, 2);
@@ -197,14 +253,9 @@ const parseBasePeriod = (input) => {
197
253
  }
198
254
  const standaloneMonth = monthNumber(input);
199
255
  if (standaloneMonth !== void 0) return Option.some(monthOfRelativeYear(standaloneMonth, 0, title(textAt(months, standaloneMonth - 1))));
200
- if (input === "today") return Option.some(relativePeriod("day", 0, "today"));
201
- if (input === "yesterday") return Option.some(relativePeriod("day", -1, "yesterday"));
202
- if (input === "tomorrow") return Option.some(relativePeriod("day", 1, "tomorrow"));
203
- if (input === "weekend" || input === "the weekend" || input === "this weekend") return Option.some(relativeWeekend(0, "this weekend"));
204
- if (input === "last weekend" || input === "previous weekend") return Option.some(relativeWeekend(-1, "last weekend"));
205
- if (input === "next weekend" || input === "coming weekend") return Option.some(relativeWeekend(1, "next weekend"));
206
- if (input === "the weekend before last") return Option.some(relativeWeekend(-2, "the weekend before last"));
207
- if (input === "the weekend after next") return Option.some(relativeWeekend(2, "the weekend after next"));
256
+ return Option.none();
257
+ };
258
+ const parseRelativeUnit = (input) => {
208
259
  const articlePeriod = String$1.match(/^the (day|week|month|quarter|year)$/u)(input);
209
260
  if (Option.isSome(articlePeriod)) {
210
261
  const entry = units.find((unit) => unit[0] === textAt(articlePeriod.value, 1));
@@ -230,22 +281,69 @@ const parseBasePeriod = (input) => {
230
281
  }
231
282
  return Option.none();
232
283
  };
284
+ const parseBasePeriod = (input) => {
285
+ const knownPeriod = Option.firstSomeOf([parseDatedPeriod(input), parseRelativeMonth(input)]);
286
+ if (Option.isSome(knownPeriod)) return knownPeriod;
287
+ if (input === "today") return Option.some(relativePeriod("day", 0, "today"));
288
+ if (input === "yesterday") return Option.some(relativePeriod("day", -1, "yesterday"));
289
+ if (input === "tomorrow") return Option.some(relativePeriod("day", 1, "tomorrow"));
290
+ if (input === "weekend" || input === "the weekend" || input === "this weekend") return Option.some(relativeWeekend(0, "this weekend"));
291
+ if (input === "last weekend" || input === "previous weekend") return Option.some(relativeWeekend(-1, "last weekend"));
292
+ if (input === "next weekend" || input === "coming weekend") return Option.some(relativeWeekend(1, "next weekend"));
293
+ if (input === "the weekend before last") return Option.some(relativeWeekend(-2, "the weekend before last"));
294
+ if (input === "the weekend after next") return Option.some(relativeWeekend(2, "the weekend after next"));
295
+ const weekday = nextWeekdayPhrases.indexOf(input);
296
+ if (weekday !== -1) return Option.some(relativeWeekday(weekday, 1, `next ${title(textAt(weekdays, weekday))}`));
297
+ return parseRelativeUnit(input);
298
+ };
299
+ const parsePeriodEdge = (input) => {
300
+ const edge = String$1.match(/^(?:the )?(start|beginning|end) of (.+)$/u)(input);
301
+ if (Option.isNone(edge)) return Option.none();
302
+ const periodText = textAt(edge.value, 2);
303
+ const basePeriod = parseBasePeriod(periodText);
304
+ const implicit = units.find((entry) => entry[0] === periodText);
305
+ const period = Option.isSome(basePeriod) || implicit === void 0 ? basePeriod : Option.some(relativePeriod(implicit[1], 0, currentPeriod(implicit[0])));
306
+ if (Option.isNone(period)) return Option.none();
307
+ const edgeName = textAt(edge.value, 1);
308
+ const name = edgeName === "beginning" ? "start" : edgeName;
309
+ const canonical = `${name} of ${period.value.canonical}`;
310
+ return Option.some(name === "end" ? periodEndDay(period.value, canonical) : periodStartDay(period.value, canonical));
311
+ };
233
312
  const parsePeriod = (input) => {
313
+ const shifted = String$1.match(/^(.+) ([1-9]\d*) (day|days|week|weeks|month|months|quarter|quarters|year|years) (ago|prior|from now)$/u)(input);
314
+ if (Option.isSome(shifted)) {
315
+ const amount = parseTrailingCount(textAt(shifted.value, 2));
316
+ const unitText = textAt(shifted.value, 3);
317
+ const period = parsePeriod(textAt(shifted.value, 1));
318
+ if (Option.isSome(amount) && Option.isSome(period)) {
319
+ const entry = countedUnit(unitText, amount.value);
320
+ if (entry !== void 0) {
321
+ const past = textAt(shifted.value, 4) !== "from now";
322
+ const direction = past ? -amount.value : amount.value;
323
+ const suffix = past ? "ago" : "from now";
324
+ const canonical = `${period.value.canonical} ${amount.value} ${unitText} ${suffix}`;
325
+ return Option.some(shiftPeriod(period.value, direction, entry[1], canonical));
326
+ }
327
+ }
328
+ }
329
+ const relativeShift = String$1.match(/^([1-9]\d*) (day|days|week|weeks|month|months|quarter|quarters|year|years) (before|after) (.+)$/u)(input);
330
+ if (Option.isSome(relativeShift)) {
331
+ const amount = parseTrailingCount(textAt(relativeShift.value, 1));
332
+ const unitText = textAt(relativeShift.value, 2);
333
+ const period = parsePeriod(textAt(relativeShift.value, 4));
334
+ if (Option.isSome(amount) && Option.isSome(period)) {
335
+ const entry = countedUnit(unitText, amount.value);
336
+ if (entry !== void 0) {
337
+ const direction = textAt(relativeShift.value, 3) === "before" ? -amount.value : amount.value;
338
+ return Option.some(shiftPeriod(period.value, direction, entry[1], input));
339
+ }
340
+ }
341
+ }
234
342
  const previousDay = String$1.match(/^(?:the )?day before (.+)$/u)(input);
235
343
  if (Option.isSome(previousDay)) {
236
344
  const period = parseBasePeriod(textAt(previousDay.value, 1));
237
345
  if (Option.isSome(period)) return Option.some(periodPreviousDay(period.value, `the day before ${period.value.canonical}`));
238
346
  }
239
- const edge = String$1.match(/^(?:the )?(start|beginning|end) of (.+)$/u)(input);
240
- if (Option.isSome(edge)) {
241
- const period = parseBasePeriod(textAt(edge.value, 2));
242
- if (Option.isSome(period)) {
243
- const edgeName = textAt(edge.value, 1);
244
- const name = edgeName === "beginning" ? "start" : edgeName;
245
- const canonical = `${name} of ${period.value.canonical}`;
246
- return Option.some(name === "end" ? periodEndDay(period.value, canonical) : periodStartDay(period.value, canonical));
247
- }
248
- }
249
347
  const wrapper = [
250
348
  "during ",
251
349
  "in ",
@@ -253,7 +351,11 @@ const parsePeriod = (input) => {
253
351
  "all of ",
254
352
  "the whole of "
255
353
  ].find((prefix) => input.startsWith(prefix));
256
- return parseBasePeriod(wrapper === void 0 ? input : input.slice(wrapper.length));
354
+ return Option.firstSomeOf([
355
+ parsePeriodEdge(input),
356
+ parseBasePeriod(wrapper === void 0 ? input : input.slice(wrapper.length)),
357
+ parseCalendarOffset(input)
358
+ ]);
257
359
  };
258
360
  const boundaryCandidate = (input) => openBoundaryCandidate(input, [
259
361
  ["up to and including ", "through"],
@@ -296,7 +398,7 @@ const parseCalendarOffset = (input) => {
296
398
  const isPast = directionText === "ago" || directionText === "prior";
297
399
  const direction = isPast ? -1 : 1;
298
400
  const canonical = isPast ? `1 ${entry[0]} ago` : `in 1 ${entry[0]}`;
299
- return Option.some(candidate(periodRange(relativePeriod(entry[1], direction, canonical)), canonical));
401
+ return Option.some(relativePeriod(entry[1], direction, canonical));
300
402
  }
301
403
  }
302
404
  const past = String$1.match(/^([1-9]\d*) (day|days|week|weeks|month|months|quarter|quarters|year|years) (?:ago|prior)$/u)(input);
@@ -315,7 +417,7 @@ const parseCalendarOffset = (input) => {
315
417
  if (entry === void 0) return Option.none();
316
418
  const direction = Option.isSome(past) ? -amount.value : amount.value;
317
419
  const canonical = direction < 0 ? `${amount.value} ${unitText} ago` : `in ${amount.value} ${unitText}`;
318
- return Option.some(candidate(periodRange(relativePeriod(entry[1], direction, canonical)), canonical));
420
+ return Option.some(relativePeriod(entry[1], direction, canonical));
319
421
  };
320
422
  const parseRollingPeriod = (input) => {
321
423
  const past = String$1.match(/^(?:(?:last|previous|past) |(?:in|over) the (?:last|previous|past) )?([1-9]\d*) (day|days|week|weeks|month|months|quarter|quarters|year|years)$/u)(input);
@@ -371,8 +473,6 @@ const parseEnglish = (input) => {
371
473
  "from now on",
372
474
  "starting now"
373
475
  ].includes(input)) return Option.some(candidate(fromNowRange(), "from now"));
374
- const offset = parseCalendarOffset(input);
375
- if (Option.isSome(offset)) return offset;
376
476
  const rolling = parseRollingPeriod(input);
377
477
  if (Option.isSome(rolling)) return rolling;
378
478
  const toDate = toDatePhrases.find((entry) => entry.phrase === input);
@@ -389,7 +489,8 @@ const parseEnglish = (input) => {
389
489
  "from now to ",
390
490
  "from now until ",
391
491
  "from now through ",
392
- "between now and "
492
+ "between now and ",
493
+ "now to "
393
494
  ], parsePeriod, (period) => `from ${period} to now`, (period) => `from now to ${period}`);
394
495
  if (Option.isSome(nowBounded)) return nowBounded;
395
496
  const bounded = joinedPeriodCandidate(input, [
@@ -457,9 +558,12 @@ const staticPeriodPhrases = [
457
558
  ]),
458
559
  ...months.flatMap((month) => [
459
560
  month,
561
+ `last ${month}`,
562
+ `next ${month}`,
460
563
  `${month} of last year`,
461
564
  `${month} of next year`
462
- ])
565
+ ]),
566
+ ...nextWeekdayPhrases
463
567
  ];
464
568
  const staticPeriods = periodsFromPhrases(staticPeriodPhrases, parsePeriod);
465
569
  const boundaryPrefixes = [
@@ -501,6 +605,17 @@ const suggestEnglish = (input, limit) => {
501
605
  ], limit);
502
606
  };
503
607
  const renderEnglish = (range) => {
608
+ const shifted = decomposeShiftedPeriodRange(range);
609
+ if (Option.isSome(shifted)) {
610
+ const base = renderEnglish(shifted.value.baseRange);
611
+ const entry = units.find((unit) => unit[1] === shifted.value.unit);
612
+ if (Option.isSome(base) && entry !== void 0) {
613
+ const amount = Math.abs(shifted.value.amount);
614
+ const noun = amount === 1 ? entry[0] : entry[2];
615
+ const suffix = shifted.value.amount < 0 ? "ago" : "from now";
616
+ return Option.some(`${base.value} ${amount} ${noun} ${suffix}`);
617
+ }
618
+ }
504
619
  const offset = calendarPeriodOffset(range);
505
620
  if (Option.isSome(offset) && Math.abs(offset.value.amount) > 1) {
506
621
  const entry = units.find((unit) => unit[1] === offset.value.unit);
@@ -530,6 +645,7 @@ const EnglishContribution = new BaseLanguageContribution({
530
645
  locale: "en",
531
646
  vocabulary: [
532
647
  ...months,
648
+ ...weekdays,
533
649
  ...quarterNames,
534
650
  "q1",
535
651
  "q2",
@@ -583,8 +699,8 @@ const EnglishContribution = new BaseLanguageContribution({
583
699
  "weekend",
584
700
  "yesterday"
585
701
  ],
586
- normalize: normalizeNaturalText,
587
- correct: correctWhitespaceSeparatedText,
702
+ normalize: normalizeEnglish,
703
+ correct: correctEnglish,
588
704
  parseExact: parseEnglish,
589
705
  suggest: suggestEnglish,
590
706
  render: renderEnglish