chronolizer 0.2.0 → 0.3.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 { calendarPeriodOffset, candidate, datedPeriods, datedQuarterPeriods, fixedDatePeriod, fixedMonthPeriod, fixedQuarterPeriod, fixedYearPeriod, fromNowRange, futurePeriod, futureRange, isoDate, joinedNowCandidate, joinedPeriodCandidate, monthOfRelativeYear, 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, 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";
8
8
  import { Effect, Option, String as String$1 } from "effect";
9
9
  //#region src/locales/de.ts
10
10
  const months = [
@@ -45,6 +45,10 @@ const unitPhrases = [
45
45
  next: "morgen",
46
46
  compound: "tages",
47
47
  currentGenitive: "dieses tages",
48
+ previousGenitive: "letzten tages",
49
+ nextGenitive: "nächsten tages",
50
+ indefiniteDative: "einem tag",
51
+ indefiniteGenitive: "eines tages",
48
52
  dative: "tagen",
49
53
  genitive: "des tages"
50
54
  },
@@ -57,6 +61,10 @@ const unitPhrases = [
57
61
  next: "nächste woche",
58
62
  compound: "wochen",
59
63
  currentGenitive: "dieser woche",
64
+ previousGenitive: "letzter woche",
65
+ nextGenitive: "nächster woche",
66
+ indefiniteDative: "einer woche",
67
+ indefiniteGenitive: "einer woche",
60
68
  dative: "wochen",
61
69
  genitive: "der woche"
62
70
  },
@@ -69,6 +77,10 @@ const unitPhrases = [
69
77
  next: "nächster monat",
70
78
  compound: "monats",
71
79
  currentGenitive: "dieses monats",
80
+ previousGenitive: "letzten monats",
81
+ nextGenitive: "nächsten monats",
82
+ indefiniteDative: "einem monat",
83
+ indefiniteGenitive: "eines monats",
72
84
  dative: "monaten",
73
85
  genitive: "des monats"
74
86
  },
@@ -81,6 +93,10 @@ const unitPhrases = [
81
93
  next: "nächstes quartal",
82
94
  compound: "quartals",
83
95
  currentGenitive: "dieses quartals",
96
+ previousGenitive: "letzten quartals",
97
+ nextGenitive: "nächsten quartals",
98
+ indefiniteDative: "einem quartal",
99
+ indefiniteGenitive: "eines quartals",
84
100
  dative: "quartalen",
85
101
  genitive: "des quartals"
86
102
  },
@@ -93,6 +109,10 @@ const unitPhrases = [
93
109
  next: "nächstes jahr",
94
110
  compound: "jahres",
95
111
  currentGenitive: "dieses jahres",
112
+ previousGenitive: "letzten jahres",
113
+ nextGenitive: "nächsten jahres",
114
+ indefiniteDative: "einem jahr",
115
+ indefiniteGenitive: "eines jahres",
96
116
  dative: "jahren",
97
117
  genitive: "des jahres"
98
118
  }
@@ -141,7 +161,125 @@ const currentBoundaryPhrases = unitPhrases.flatMap((entry) => [
141
161
  entry,
142
162
  phrase
143
163
  })));
164
+ const declinedBoundaryPeriods = [
165
+ [
166
+ "diese woche",
167
+ "week",
168
+ 0,
169
+ "dieser woche",
170
+ "diese woche"
171
+ ],
172
+ [
173
+ "letzte woche",
174
+ "week",
175
+ -1,
176
+ "letzter woche",
177
+ "letzte woche"
178
+ ],
179
+ [
180
+ "nächste woche",
181
+ "week",
182
+ 1,
183
+ "nächster woche",
184
+ "nächste woche"
185
+ ],
186
+ [
187
+ "dieser monat",
188
+ "month",
189
+ 0,
190
+ "diesem monat",
191
+ "diesen monat"
192
+ ],
193
+ [
194
+ "letzter monat",
195
+ "month",
196
+ -1,
197
+ "letztem monat",
198
+ "letzten monat"
199
+ ],
200
+ [
201
+ "nächster monat",
202
+ "month",
203
+ 1,
204
+ "nächstem monat",
205
+ "nächsten monat"
206
+ ],
207
+ [
208
+ "dieses quartal",
209
+ "quarter",
210
+ 0,
211
+ "diesem quartal",
212
+ "dieses quartal"
213
+ ],
214
+ [
215
+ "letztes quartal",
216
+ "quarter",
217
+ -1,
218
+ "letztem quartal",
219
+ "letztes quartal"
220
+ ],
221
+ [
222
+ "nächstes quartal",
223
+ "quarter",
224
+ 1,
225
+ "nächstem quartal",
226
+ "nächstes quartal"
227
+ ],
228
+ [
229
+ "dieses jahr",
230
+ "year",
231
+ 0,
232
+ "diesem jahr",
233
+ "dieses jahr"
234
+ ],
235
+ [
236
+ "letztes jahr",
237
+ "year",
238
+ -1,
239
+ "letztem jahr",
240
+ "letztes jahr"
241
+ ],
242
+ [
243
+ "nächstes jahr",
244
+ "year",
245
+ 1,
246
+ "nächstem jahr",
247
+ "nächstes jahr"
248
+ ]
249
+ ];
144
250
  const relativeAliases = [
251
+ ...unitPhrases.flatMap((entry) => [
252
+ [
253
+ entry.currentGenitive,
254
+ entry.unit,
255
+ 0,
256
+ entry.current
257
+ ],
258
+ [
259
+ entry.previousGenitive,
260
+ entry.unit,
261
+ -1,
262
+ entry.previous
263
+ ],
264
+ [
265
+ entry.nextGenitive,
266
+ entry.unit,
267
+ 1,
268
+ entry.next
269
+ ]
270
+ ]),
271
+ ...declinedBoundaryPeriods.map(([canonical, unit, direction, dative]) => [
272
+ dative,
273
+ unit,
274
+ direction,
275
+ canonical
276
+ ]),
277
+ [
278
+ "vorgestern",
279
+ "day",
280
+ -2,
281
+ "vorgestern"
282
+ ],
145
283
  [
146
284
  "vorletzten tag",
147
285
  "day",
@@ -414,6 +552,20 @@ const monthNumber = (value) => {
414
552
  const shortIndex = monthAbbreviations.findIndex((aliases) => aliases.some((alias) => alias === normalized));
415
553
  return shortIndex === -1 ? void 0 : shortIndex + 1;
416
554
  };
555
+ const currentDateLabel = (day, month) => `${day}. ${title(textAt(months, month - 1))}`;
556
+ const withRelativeCase = (canonical, grammaticalCase) => {
557
+ const period = declinedBoundaryPeriods.find((entry) => canonical === canonicalRelative(entry[0]));
558
+ if (period === void 0) return canonical;
559
+ return canonicalRelative(period[grammaticalCase === "dative" ? 3 : 4]);
560
+ };
561
+ const withRelativeGenitive = (canonical) => {
562
+ for (const entry of unitPhrases) {
563
+ if (canonical === canonicalRelative(entry.current)) return canonicalRelative(entry.currentGenitive);
564
+ if (canonical === canonicalRelative(entry.previous)) return canonicalRelative(entry.previousGenitive);
565
+ if (canonical === canonicalRelative(entry.next)) return canonicalRelative(entry.nextGenitive);
566
+ }
567
+ return canonical;
568
+ };
417
569
  const parseNamedDate = (input) => {
418
570
  const named = String$1.match(/^([0-3]?\d)\.? ([a-zäöüß]+\.?) (\d{4})$/u)(input);
419
571
  if (Option.isSome(named)) return namedDatePeriod(textAt(named.value, 3), textAt(named.value, 2), textAt(named.value, 1), monthNumber, (day, month, year) => `${day}. ${title(textAt(months, month - 1))} ${year}`);
@@ -427,7 +579,8 @@ const parseNamedDate = (input) => {
427
579
  if (isIsoDate(value) && value !== "9999-12-31") return Option.some(fixedDatePeriod(value, `${day}. ${title(textAt(months, month - 1))} ${year}`));
428
580
  }
429
581
  }
430
- return Option.none();
582
+ const current = String$1.match(/^([0-3]?\d)\.? ([a-zäöüß]+\.?)$/u)(input);
583
+ return Option.isSome(current) ? namedCurrentYearDatePeriod(textAt(current.value, 2), textAt(current.value, 1), monthNumber, currentDateLabel) : Option.none();
431
584
  };
432
585
  const quarterNames = [
433
586
  "erstes",
@@ -472,7 +625,8 @@ const parseQuarter = (input) => {
472
625
  return quarter === void 0 ? Option.none() : Option.some(quarterOfRelativeYear(quarter, 0, `Q${quarter}`));
473
626
  };
474
627
  const parseBasePeriod = (input) => {
475
- if (isIsoDate(input) && input !== "9999-12-31") return Option.some(fixedDatePeriod(input, input));
628
+ const absoluteDate = absoluteDatePeriod(input, "de");
629
+ if (Option.isSome(absoluteDate)) return absoluteDate;
476
630
  const namedDate = parseNamedDate(input);
477
631
  if (Option.isSome(namedDate)) return namedDate;
478
632
  const quarter = parseQuarter(input);
@@ -519,12 +673,13 @@ const parsePeriod = (input) => {
519
673
  const edgeName = textAt(edge.value, 1);
520
674
  const period = parseBasePeriod(textAt(edge.value, 2));
521
675
  if (Option.isSome(period)) {
522
- const canonical = `${edgeName === "ende" ? "Ende" : "Anfang"} ${period.value.canonical}`;
676
+ const canonical = `${edgeName === "ende" ? "Ende" : "Anfang"} ${withRelativeGenitive(period.value.canonical)}`;
523
677
  return Option.some(edgeName === "ende" ? periodEndDay(period.value, canonical) : periodStartDay(period.value, canonical));
524
678
  }
525
679
  }
526
680
  const wrapper = [
527
681
  "im ",
682
+ "am ",
528
683
  "für ",
529
684
  "während "
530
685
  ].find((prefix) => input.startsWith(prefix));
@@ -534,9 +689,9 @@ const currentBoundaryCandidate = (input) => {
534
689
  const match = currentBoundaryPhrases.find((entry) => entry.phrase === input);
535
690
  if (match === void 0) return Option.none();
536
691
  const period = relativePeriod(match.entry.unit, 0, match.entry.current);
537
- return Option.some(periodBoundaryCandidate(period, match.boundary, match.phrase));
692
+ return Option.some(periodBoundaryCandidate(period, match.boundary, canonicalRelative(match.phrase)));
538
693
  };
539
- const boundaryCandidate = (input) => openBoundaryCandidate(input, [
694
+ const boundaryCandidate = (input) => Option.map(openBoundaryCandidate(input, [
540
695
  ["seit dem beginn von ", "since"],
541
696
  ["seit dem anfang von ", "since"],
542
697
  ["seit dem jahr ", "since"],
@@ -563,12 +718,22 @@ const boundaryCandidate = (input) => openBoundaryCandidate(input, [
563
718
  ["nach ", "after"],
564
719
  ["bis ", "before"],
565
720
  ["ab ", "since"]
566
- ], parsePeriod);
721
+ ], parsePeriod), (result) => candidate(result.range, canonicalRelative(input)));
567
722
  const countedUnit = (value, amount, dative = false) => unitPhrases.find((entry) => {
568
723
  if (amount === 1) return value === entry.noun;
569
724
  return value === (dative ? entry.dative : entry.plural);
570
725
  });
571
726
  const parseCalendarOffset = (input) => {
727
+ const singular = String$1.match(/^(vor|in) (einem tag|einer woche|einem monat|einem quartal|einem jahr)$/u)(input);
728
+ if (Option.isSome(singular)) {
729
+ const entry = unitPhrases.find((unit) => unit.indefiniteDative === textAt(singular.value, 2));
730
+ if (entry !== void 0) {
731
+ const isPast = textAt(singular.value, 1) === "vor";
732
+ const direction = isPast ? -1 : 1;
733
+ const canonical = `${isPast ? "vor" : "in"} ${canonicalRelative(entry.indefiniteDative)}`;
734
+ return Option.some(candidate(periodRange(relativePeriod(entry.unit, direction, canonical)), canonical));
735
+ }
736
+ }
572
737
  const prefixed = String$1.match(/^(vor|in) ([1-9]\d*) (tag|tagen|woche|wochen|monat|monaten|quartal|quartalen|jahr|jahren)$/u)(input);
573
738
  const prior = String$1.match(/^([1-9]\d*) (tag|tage|woche|wochen|monat|monate|quartal|quartale|jahr|jahre) zuvor$/u)(input);
574
739
  const match = Option.firstSomeOf([prefixed, prior]);
@@ -588,30 +753,51 @@ const parseCalendarOffset = (input) => {
588
753
  };
589
754
  const parseRollingPeriod = (input) => {
590
755
  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);
591
- const pastDative = String$1.match(/^(?:in den letzten|in den vergangenen|in den vorherigen) ([1-9]\d*) (tag|tagen|woche|wochen|monat|monaten|quartal|quartalen|jahr|jahren)$/u)(input);
756
+ const pastDative = String$1.match(/^(?:(?:in den letzten|in den vergangenen|in den vorherigen) |seit )([1-9]\d*) (tag|tagen|woche|wochen|monat|monaten|quartal|quartalen|jahr|jahren)$/u)(input);
757
+ const singularSince = String$1.match(/^seit (einem tag|einer woche|einem monat|einem quartal|einem jahr)$/u)(input);
592
758
  const pastGenitive = String$1.match(/^während der letzten ([1-9]\d*) (tag|tage|woche|wochen|monat|monate|quartal|quartale|jahr|jahre)$/u)(input);
593
759
  const future = String$1.match(/^(?:die nächsten|nächsten|nächste|kommende) ([1-9]\d*) (tag|tage|woche|wochen|monat|monate|quartal|quartale|jahr|jahre)$/u)(input);
594
760
  const futureDative = String$1.match(/^(?:in den nächsten|in den kommenden|innerhalb von) ([1-9]\d*) (tag|tagen|woche|wochen|monat|monaten|quartal|quartalen|jahr|jahren)$/u)(input);
761
+ const singularFuture = String$1.match(/^innerhalb (eines tages|einer woche|eines monats|eines quartals|eines jahres)$/u)(input);
595
762
  const futureGenitive = String$1.match(/^innerhalb der nächsten ([1-9]\d*) (tag|tage|woche|wochen|monat|monate|quartal|quartale|jahr|jahre)$/u)(input);
596
763
  const match = Option.firstSomeOf([
597
764
  past,
598
765
  pastDative,
766
+ singularSince,
599
767
  pastGenitive,
600
768
  future,
601
769
  futureDative,
770
+ singularFuture,
602
771
  futureGenitive
603
772
  ]);
604
773
  if (Option.isNone(match)) return Option.none();
605
- const amount = parseTrailingCount(textAt(match.value, 1));
774
+ const isSingular = Option.isSome(singularSince) || Option.isSome(singularFuture);
775
+ const amount = parseTrailingCount(isSingular ? "1" : textAt(match.value, 1));
606
776
  if (Option.isNone(amount)) return Option.none();
607
- const usesDative = Option.isSome(pastDative) || Option.isSome(futureDative);
608
- const entry = countedUnit(textAt(match.value, 2), amount.value, usesDative);
777
+ const usesDative = Option.isSome(pastDative) || Option.isSome(singularSince) || Option.isSome(futureDative);
778
+ const sinceEntry = Option.isSome(singularSince) ? unitPhrases.find((unit) => unit.indefiniteDative === textAt(match.value, 1)) : void 0;
779
+ const futureEntry = Option.isSome(singularFuture) ? unitPhrases.find((unit) => unit.indefiniteGenitive === textAt(match.value, 1)) : void 0;
780
+ const countedEntry = isSingular ? void 0 : countedUnit(textAt(match.value, 2), amount.value, usesDative);
781
+ const entry = sinceEntry ?? futureEntry ?? countedEntry;
609
782
  if (entry === void 0) return Option.none();
610
- const isFuture = Option.isSome(future) || Option.isSome(futureDative) || Option.isSome(futureGenitive);
783
+ const isFuture = Option.isSome(future) || Option.isSome(futureDative) || Option.isSome(singularFuture) || Option.isSome(futureGenitive);
611
784
  const range = isFuture ? futureRange(amount.value, entry.unit) : trailingRange(amount.value, entry.unit);
785
+ if (amount.value === 1) {
786
+ const phrase = isFuture ? `innerhalb ${canonicalRelative(entry.indefiniteGenitive)}` : `seit ${canonicalRelative(entry.indefiniteDative)}`;
787
+ return Option.some(candidate(range, phrase));
788
+ }
612
789
  const direction = isFuture ? "nächste" : "letzte";
613
- const noun = amount.value === 1 ? entry.noun : entry.plural;
614
- return Option.some(candidate(range, `${direction} ${amount.value} ${title(noun)}`));
790
+ return Option.some(candidate(range, `${direction} ${amount.value} ${title(entry.plural)}`));
791
+ };
792
+ const parseElidedDateRange = (input) => {
793
+ const match = String$1.match(/^(?:vom )?([0-3]?\d)\.(?: bis |[–—-])(?:zum )?([0-3]?\d)\.? ([a-zäöüß]+\.?)(?: (\d{4}))?$/u)(input);
794
+ if (Option.isNone(match)) return Option.none();
795
+ const lowerDay = textAt(match.value, 1);
796
+ const upperDay = textAt(match.value, 2);
797
+ const month = textAt(match.value, 3);
798
+ const year = textAt(match.value, 4);
799
+ const suffix = year.length === 0 ? "" : ` ${year}`;
800
+ return joinedPeriodCandidate(`von ${lowerDay}. ${month}${suffix} bis ${upperDay}. ${month}${suffix}`, [["von ", " bis "]], parsePeriod, (lower, upper) => `von ${lower} bis ${upper}`);
615
801
  };
616
802
  const parseGerman = (input) => {
617
803
  const remaining = remainingPeriodPhrases.find((entry) => entry.phrase === input);
@@ -626,6 +812,8 @@ const parseGerman = (input) => {
626
812
  if (toDate !== void 0) return Option.some(candidate(periodToDateRange(toDate.entry.unit), canonicalToDate(toDate.entry)));
627
813
  const currentBoundary = currentBoundaryCandidate(input);
628
814
  if (Option.isSome(currentBoundary)) return currentBoundary;
815
+ const elided = parseElidedDateRange(input);
816
+ if (Option.isSome(elided)) return elided;
629
817
  const nowBounded = joinedNowCandidate(input, [
630
818
  ["von ", " bis heute"],
631
819
  ["vom ", " bis heute"],
@@ -657,9 +845,10 @@ const parseGerman = (input) => {
657
845
  if (Option.isSome(bounded)) return bounded;
658
846
  const boundary = boundaryCandidate(input);
659
847
  if (Option.isSome(boundary)) return boundary;
660
- return Option.map(parsePeriod(input), (period) => candidate(periodRange(period), period.canonical));
848
+ return parsePeriod(input).pipe(Option.map((period) => candidate(periodRange(period), period.canonical)));
661
849
  };
662
850
  const staticPeriodPhrases = [
851
+ "vorgestern",
663
852
  "dieses wochenende",
664
853
  "letztes wochenende",
665
854
  "nächstes wochenende",
@@ -669,12 +858,12 @@ const staticPeriodPhrases = [
669
858
  entry.current,
670
859
  entry.previous,
671
860
  entry.next,
672
- `anfang ${entry.current}`,
673
- `ende ${entry.current}`,
674
- `anfang ${entry.previous}`,
675
- `ende ${entry.previous}`,
676
- `anfang ${entry.next}`,
677
- `ende ${entry.next}`
861
+ `anfang ${entry.currentGenitive}`,
862
+ `ende ${entry.currentGenitive}`,
863
+ `anfang ${entry.previousGenitive}`,
864
+ `ende ${entry.previousGenitive}`,
865
+ `anfang ${entry.nextGenitive}`,
866
+ `ende ${entry.nextGenitive}`
678
867
  ]),
679
868
  ...[
680
869
  1,
@@ -719,8 +908,21 @@ const countedSuggestions = (input) => {
719
908
  const germanSuggestionPhrases = [
720
909
  ...unitPhrases.map(canonicalToDate),
721
910
  ...unitPhrases.map((entry) => `rest ${entry.genitive}`),
911
+ ...unitPhrases.flatMap((entry) => [
912
+ `seit ${entry.indefiniteDative}`,
913
+ `innerhalb ${entry.indefiniteGenitive}`,
914
+ `vor ${entry.indefiniteDative}`,
915
+ `in ${entry.indefiniteDative}`
916
+ ]),
722
917
  ...staticPeriodPhrases,
723
- ...prefixNaturalPhrases(staticPeriodPhrases, boundaryPrefixes),
918
+ ...currentBoundaryPhrases.map((entry) => entry.phrase),
919
+ ...declinedBoundaryPeriods.flatMap(([, , , dative, accusative]) => [
920
+ `seit ${dative}`,
921
+ `vor ${dative}`,
922
+ `bis einschließlich ${accusative}`,
923
+ `nach ${dative}`
924
+ ]),
925
+ ...prefixNaturalPhrases(months, boundaryPrefixes),
724
926
  "bis heute",
725
927
  "ab jetzt"
726
928
  ].map((phrase) => normalizeNaturalText(phrase, "de"));
@@ -742,15 +944,20 @@ const renderGerman = (range) => {
742
944
  const future = futurePeriod(range);
743
945
  if (Option.isSome(future)) {
744
946
  const entry = unitPhrases.find((unit) => unit.unit === future.value.unit);
745
- if (entry !== void 0) return Option.some(`nächste ${future.value.amount} ${title(future.value.amount === 1 ? entry.noun : entry.plural)}`);
947
+ if (entry !== void 0) return Option.some(future.value.amount === 1 ? `innerhalb ${canonicalRelative(entry.indefiniteGenitive)}` : `nächste ${future.value.amount} ${title(entry.plural)}`);
746
948
  }
747
949
  const trailing = trailingPeriod(range);
748
950
  if (Option.isSome(trailing)) {
749
951
  const entry = unitPhrases.find((unit) => unit.unit === trailing.value.unit);
750
- if (entry !== void 0) return Option.some(trailing.value.amount === 1 ? `1 ${title(entry.noun)}` : `letzte ${trailing.value.amount} ${title(entry.plural)}`);
952
+ if (entry !== void 0) return Option.some(trailing.value.amount === 1 ? `seit ${canonicalRelative(entry.indefiniteDative)}` : `letzte ${trailing.value.amount} ${title(entry.plural)}`);
751
953
  }
752
- const periods = [...staticPeriods, ...periodsFromPhrases([...datedPeriods(range, months), ...datedQuarterPeriods(range)], parsePeriod)];
753
- return renderPeriodRange(range, [...unitPhrases.map((entry) => candidate(periodToDateRange(entry.unit), canonicalToDate(entry))), ...unitPhrases.map((entry) => candidate(remainingPeriodRange(entry.unit), `Rest ${canonicalRelative(entry.genitive)}`))], periods, (period) => `seit ${period}`, (period) => `vor ${period}`, (period) => `bis einschließlich ${period}`, (period) => `nach ${period}`, (lower, upper) => `von ${lower} bis ${upper}`, (period) => `von ${period} bis heute`, (period) => `von heute bis ${period}`, () => "bis heute", () => "ab jetzt");
954
+ const dated = [...datedPeriods(range, months), ...datedQuarterPeriods(range)];
955
+ const periods = [
956
+ ...staticPeriods,
957
+ ...currentYearDatePeriods(range, currentDateLabel),
958
+ ...periodsFromPhrases([...dated, ...dated.flatMap((phrase) => [`anfang ${phrase}`, `ende ${phrase}`])], parsePeriod)
959
+ ];
960
+ return renderPeriodRange(range, [...unitPhrases.map((entry) => candidate(periodToDateRange(entry.unit), canonicalToDate(entry))), ...unitPhrases.map((entry) => candidate(remainingPeriodRange(entry.unit), `Rest ${canonicalRelative(entry.genitive)}`))], periods, (period) => `seit ${withRelativeCase(period, "dative")}`, (period) => `vor ${withRelativeCase(period, "dative")}`, (period) => `bis einschließlich ${withRelativeCase(period, "accusative")}`, (period) => `nach ${withRelativeCase(period, "dative")}`, (lower, upper) => `von ${lower} bis ${upper}`, (period) => `von ${period} bis heute`, (period) => `von heute bis ${period}`, () => "bis heute", () => "ab jetzt");
754
961
  };
755
962
  const GermanContribution = new BaseLanguageContribution({
756
963
  locale: "de",
@@ -770,7 +977,11 @@ const GermanContribution = new BaseLanguageContribution({
770
977
  entry.noun,
771
978
  entry.plural,
772
979
  entry.dative,
980
+ ...entry.indefiniteDative.split(" "),
981
+ ...entry.indefiniteGenitive.split(" "),
773
982
  ...entry.currentGenitive.split(" "),
983
+ ...entry.previousGenitive.split(" "),
984
+ ...entry.nextGenitive.split(" "),
774
985
  ...entry.current.split(" "),
775
986
  ...entry.previous.split(" "),
776
987
  ...entry.next.split(" ")
@@ -1,10 +1,9 @@
1
- import { isIsoDate } from "../ast/schemas.mjs";
2
1
  import { BaseLanguageContribution } from "../language/model.mjs";
3
2
  import { normalizeNaturalText } from "../natural/text.mjs";
4
3
  import { defineLanguagePlugin, languagePluginsLayer } from "../language/registry.mjs";
5
4
  import { correctWhitespaceSeparatedText } from "../natural/correction.mjs";
6
5
  import { completeNaturalPhrases, fixedCalendarPeriodPhrases, naturalCount, prefixNaturalPhrases } from "../natural/suggestion.mjs";
7
- import { calendarPeriodOffset, candidate, datedPeriods, datedQuarterPeriods, fixedDatePeriod, fixedMonthPeriod, fixedQuarterPeriod, fixedYearPeriod, fromNowRange, futurePeriod, futureRange, joinedNowCandidate, joinedPeriodCandidate, monthOfRelativeYear, namedDatePeriod, openBoundaryCandidate, parseTrailingCount, periodEndDay, periodRange, periodStartDay, periodToDateRange, periodsFromPhrases, quarterOfRelativeYear, relativePeriod, relativeWeekend, remainingPeriodRange, renderPeriodRange, textAt, trailingPeriod, trailingRange, untilNowRange, validYear } from "./shared.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";
8
7
  import { Effect, Option, String as String$1 } from "effect";
9
8
  //#region src/locales/en.ts
10
9
  const months = [
@@ -159,15 +158,21 @@ const parseQuarter = (input) => {
159
158
  const quarter = quarterNumber(textAt(standaloneMatch.value, 1));
160
159
  return quarter === void 0 ? Option.none() : Option.some(quarterOfRelativeYear(quarter, 0, `Q${quarter}`));
161
160
  };
161
+ const currentDateLabel = (day, month) => `${day} ${title(textAt(months, month - 1))}`;
162
162
  const parseNamedDate = (input) => {
163
- const dayFirst = String$1.match(/^([0-3]?\d)(?:st|nd|rd|th)? ([a-z]+\.?) (\d{4})$/u)(input);
163
+ const dayFirst = String$1.match(/^(?:the )?([0-3]?\d)(?:st|nd|rd|th)?(?: of)? ([a-z]+\.?),? (\d{4})$/u)(input);
164
164
  if (Option.isSome(dayFirst)) return namedDatePeriod(textAt(dayFirst.value, 3), textAt(dayFirst.value, 2), textAt(dayFirst.value, 1), monthNumber, (day, month, year) => `${day} ${title(textAt(months, month - 1))} ${year}`);
165
- const monthFirst = String$1.match(/^([a-z]+\.?) ([0-3]?\d)(?:st|nd|rd|th)?,? (\d{4})$/u)(input);
165
+ const monthFirst = String$1.match(/^([a-z]+\.?) (?:the )?([0-3]?\d)(?:st|nd|rd|th)?,? (\d{4})$/u)(input);
166
166
  if (Option.isSome(monthFirst)) return namedDatePeriod(textAt(monthFirst.value, 3), textAt(monthFirst.value, 1), textAt(monthFirst.value, 2), monthNumber, (day, month, year) => `${day} ${title(textAt(months, month - 1))} ${year}`);
167
- return Option.none();
167
+ const currentDayFirst = String$1.match(/^(?:the )?([0-3]?\d)(?:st|nd|rd|th)?(?: of)? ([a-z]+\.?)$/u)(input);
168
+ const currentMonthFirst = String$1.match(/^([a-z]+\.?) (?:the )?([0-3]?\d)(?:st|nd|rd|th)?$/u)(input);
169
+ const current = Option.firstSomeOf([currentDayFirst, currentMonthFirst]);
170
+ if (Option.isNone(current)) return Option.none();
171
+ 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);
168
172
  };
169
173
  const parseBasePeriod = (input) => {
170
- if (isIsoDate(input) && input !== "9999-12-31") return Option.some(fixedDatePeriod(input, input));
174
+ const absoluteDate = absoluteDatePeriod(input, "en");
175
+ if (Option.isSome(absoluteDate)) return absoluteDate;
171
176
  const namedDate = parseNamedDate(input);
172
177
  if (Option.isSome(namedDate)) return namedDate;
173
178
  const quarter = parseQuarter(input);
@@ -213,7 +218,7 @@ const parseBasePeriod = (input) => {
213
218
  return Option.some(relativePeriod(entry[1], direction, input));
214
219
  }
215
220
  }
216
- const relative = String$1.match(/^(?:the )?(last|previous|this|current|next|coming|upcoming) ([a-z]+)$/u)(input);
221
+ const relative = String$1.match(/^(?:the )?(last|previous|this|current|next|coming|upcoming) (?:calendar )?([a-z]+)$/u)(input);
217
222
  if (Option.isSome(relative)) {
218
223
  const unitText = textAt(relative.value, 2);
219
224
  const unit = units.find((entry) => entry[0] === unitText)?.[1];
@@ -226,6 +231,11 @@ const parseBasePeriod = (input) => {
226
231
  return Option.none();
227
232
  };
228
233
  const parsePeriod = (input) => {
234
+ const previousDay = String$1.match(/^(?:the )?day before (.+)$/u)(input);
235
+ if (Option.isSome(previousDay)) {
236
+ const period = parseBasePeriod(textAt(previousDay.value, 1));
237
+ if (Option.isSome(period)) return Option.some(periodPreviousDay(period.value, `the day before ${period.value.canonical}`));
238
+ }
229
239
  const edge = String$1.match(/^(?:the )?(start|beginning|end) of (.+)$/u)(input);
230
240
  if (Option.isSome(edge)) {
231
241
  const period = parseBasePeriod(textAt(edge.value, 2));
@@ -247,6 +257,8 @@ const parsePeriod = (input) => {
247
257
  };
248
258
  const boundaryCandidate = (input) => openBoundaryCandidate(input, [
249
259
  ["up to and including ", "through"],
260
+ ["on or before ", "through"],
261
+ ["on or after ", "since"],
250
262
  ["up to including ", "through"],
251
263
  ["since the beginning of ", "since"],
252
264
  ["since the start of ", "since"],
@@ -263,6 +275,8 @@ const boundaryCandidate = (input) => openBoundaryCandidate(input, [
263
275
  ["before start of ", "before"],
264
276
  ["starting ", "since"],
265
277
  ["through ", "through"],
278
+ ["earlier than ", "before"],
279
+ ["later than ", "after"],
266
280
  ["before ", "before"],
267
281
  ["since ", "since"],
268
282
  ["after ", "after"],
@@ -305,22 +319,47 @@ const parseCalendarOffset = (input) => {
305
319
  };
306
320
  const parseRollingPeriod = (input) => {
307
321
  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);
322
+ const singularPast = String$1.match(/^(?:past|the past|in the past|over the past|this past) (day|week|month|quarter|year)$/u)(input);
308
323
  const future = String$1.match(/^(?:(?:next|coming|within) |(?:in|over|within) the (?:next|coming) )([1-9]\d*) (day|days|week|weeks|month|months|quarter|quarters|year|years)$/u)(input);
309
- const match = Option.firstSomeOf([past, future]);
324
+ const singularFuture = String$1.match(/^(?:in|over|within) the (?:next|coming) (day|week|month|quarter|year)$/u)(input);
325
+ const match = Option.firstSomeOf([
326
+ past,
327
+ singularPast,
328
+ future,
329
+ singularFuture
330
+ ]);
310
331
  if (Option.isNone(match)) return Option.none();
311
- const amount = parseTrailingCount(textAt(match.value, 1));
332
+ const singular = Option.isSome(singularPast) || Option.isSome(singularFuture);
333
+ const amount = parseTrailingCount(singular ? "1" : textAt(match.value, 1));
312
334
  if (Option.isNone(amount)) return Option.none();
313
- const entry = countedUnit(textAt(match.value, 2), amount.value);
335
+ const unitText = textAt(match.value, singular ? 1 : 2);
336
+ const entry = countedUnit(unitText, amount.value);
314
337
  if (entry === void 0) return Option.none();
315
- const isFuture = Option.isSome(future);
338
+ const isFuture = Option.isSome(future) || Option.isSome(singularFuture);
316
339
  const range = isFuture ? futureRange(amount.value, entry[1]) : trailingRange(amount.value, entry[1]);
340
+ if (amount.value === 1) {
341
+ const canonical = isFuture ? `within the next ${entry[0]}` : `past ${entry[0]}`;
342
+ return Option.some(candidate(range, canonical));
343
+ }
317
344
  const direction = isFuture ? "next" : "last";
318
- const noun = amount.value === 1 ? entry[0] : entry[2];
319
- return Option.some(candidate(range, `${direction} ${amount.value} ${noun}`));
345
+ return Option.some(candidate(range, `${direction} ${amount.value} ${entry[2]}`));
346
+ };
347
+ const parseElidedDateRange = (input) => {
348
+ const monthFirst = String$1.match(/^([a-z]+\.?) ([0-3]?\d)(?:st|nd|rd|th)?(?: (?:to|through) |[–—-])([0-3]?\d)(?:st|nd|rd|th)?,?(?: (\d{4}))?$/u)(input);
349
+ const dayFirst = String$1.match(/^([0-3]?\d)(?:st|nd|rd|th)?(?: (?:to|through) |[–—-])([0-3]?\d)(?:st|nd|rd|th)? ([a-z]+\.?)(?: (\d{4}))?$/u)(input);
350
+ const match = Option.firstSomeOf([monthFirst, dayFirst]);
351
+ if (Option.isNone(match)) return Option.none();
352
+ const isMonthFirst = Option.isSome(monthFirst);
353
+ const month = textAt(match.value, isMonthFirst ? 1 : 3);
354
+ const lowerDay = textAt(match.value, isMonthFirst ? 2 : 1);
355
+ const upperDay = textAt(match.value, isMonthFirst ? 3 : 2);
356
+ const year = textAt(match.value, 4);
357
+ const suffix = year.length === 0 ? "" : ` ${year}`;
358
+ return joinedPeriodCandidate(`from ${lowerDay} ${month}${suffix} to ${upperDay} ${month}${suffix}`, [["from ", " to "]], parsePeriod, (lower, upper) => `from ${lower} to ${upper}`);
320
359
  };
321
360
  const parseEnglish = (input) => {
322
361
  const remaining = remainingPeriodPhrases.find((entry) => entry.phrase === input);
323
- if (remaining !== void 0) return Option.some(candidate(remainingPeriodRange(remaining.entry[1]), `rest of ${remaining.entry[0]}`));
362
+ if (remaining !== void 0) return Option.some(candidate(remainingPeriodRange(remaining.entry[1]), `rest of the ${remaining.entry[0]}`));
324
363
  if ([
325
364
  "until now",
326
365
  "till now",
@@ -338,10 +377,13 @@ const parseEnglish = (input) => {
338
377
  if (Option.isSome(rolling)) return rolling;
339
378
  const toDate = toDatePhrases.find((entry) => entry.phrase === input);
340
379
  if (toDate !== void 0) return Option.some(candidate(periodToDateRange(toDate.entry[1]), `${toDate.entry[0]} to date`));
380
+ const elided = parseElidedDateRange(input);
381
+ if (Option.isSome(elided)) return elided;
341
382
  const nowBounded = joinedNowCandidate(input, [
342
383
  ["from ", " to now"],
343
384
  ["from ", " until now"],
344
385
  ["from ", " through now"],
386
+ ["from ", " to date"],
345
387
  ["between ", " and now"]
346
388
  ], [
347
389
  "from now to ",
@@ -380,18 +422,19 @@ const parseEnglish = (input) => {
380
422
  if (Option.isSome(bounded)) return bounded;
381
423
  const boundary = boundaryCandidate(input);
382
424
  if (Option.isSome(boundary)) return boundary;
383
- return Option.map(parsePeriod(input), (period) => candidate(periodRange(period), period.canonical));
425
+ return parsePeriod(input).pipe(Option.map((period) => candidate(periodRange(period), period.canonical)));
384
426
  };
385
427
  const staticPeriodPhrases = [
386
428
  "today",
387
429
  "yesterday",
430
+ "the day before yesterday",
388
431
  "tomorrow",
389
432
  "this weekend",
390
433
  "last weekend",
391
434
  "next weekend",
392
435
  "the weekend before last",
393
436
  "the weekend after next",
394
- ...units.flatMap((entry) => [
437
+ ...units.filter((entry) => entry[1] !== "day").flatMap((entry) => [
395
438
  `this ${entry[0]}`,
396
439
  `last ${entry[0]}`,
397
440
  `next ${entry[0]}`,
@@ -441,7 +484,8 @@ const countedSuggestions = (input) => {
441
484
  };
442
485
  const englishSuggestionPhrases = [
443
486
  ...units.map((entry) => `${entry[0]} to date`),
444
- ...units.map((entry) => `rest of ${entry[0]}`),
487
+ ...remainingPeriodPhrases.map((entry) => entry.phrase),
488
+ ...units.flatMap((entry) => [`past ${entry[0]}`, `within the next ${entry[0]}`]),
445
489
  ...staticPeriodPhrases,
446
490
  ...prefixNaturalPhrases(staticPeriodPhrases, boundaryPrefixes),
447
491
  "until now",
@@ -468,15 +512,19 @@ const renderEnglish = (range) => {
468
512
  const future = futurePeriod(range);
469
513
  if (Option.isSome(future)) {
470
514
  const entry = units.find((unit) => unit[1] === future.value.unit);
471
- if (entry !== void 0) return Option.some(`next ${future.value.amount} ${future.value.amount === 1 ? entry[0] : entry[2]}`);
515
+ if (entry !== void 0) return Option.some(future.value.amount === 1 ? `within the next ${entry[0]}` : `next ${future.value.amount} ${entry[2]}`);
472
516
  }
473
517
  const trailing = trailingPeriod(range);
474
518
  if (Option.isSome(trailing)) {
475
519
  const entry = units.find((unit) => unit[1] === trailing.value.unit);
476
- if (entry !== void 0) return Option.some(trailing.value.amount === 1 ? `1 ${entry[0]}` : `last ${trailing.value.amount} ${entry[2]}`);
520
+ if (entry !== void 0) return Option.some(trailing.value.amount === 1 ? `past ${entry[0]}` : `last ${trailing.value.amount} ${entry[2]}`);
477
521
  }
478
- const periods = [...staticPeriods, ...periodsFromPhrases([...datedPeriods(range, months), ...datedQuarterPeriods(range)], parsePeriod)];
479
- return renderPeriodRange(range, [...units.map((entry) => candidate(periodToDateRange(entry[1]), `${entry[0]} to date`)), ...units.map((entry) => candidate(remainingPeriodRange(entry[1]), `rest of ${entry[0]}`))], periods, (period) => `since ${period}`, (period) => `before ${period}`, (period) => `through ${period}`, (period) => `after ${period}`, (lower, upper) => `from ${lower} to ${upper}`, (period) => `from ${period} to now`, (period) => `from now to ${period}`, () => "until now", () => "from now");
522
+ const periods = [
523
+ ...staticPeriods,
524
+ ...currentYearDatePeriods(range, currentDateLabel),
525
+ ...periodsFromPhrases([...datedPeriods(range, months), ...datedQuarterPeriods(range)], parsePeriod)
526
+ ];
527
+ return renderPeriodRange(range, [...units.map((entry) => candidate(periodToDateRange(entry[1]), `${entry[0]} to date`)), ...units.map((entry) => candidate(remainingPeriodRange(entry[1]), `rest of the ${entry[0]}`))], periods, (period) => `since ${period}`, (period) => `before ${period}`, (period) => `through ${period}`, (period) => `after ${period}`, (lower, upper) => `from ${lower} to ${upper}`, (period) => `from ${period} to now`, (period) => `from now to ${period}`, () => "until now", () => "from now");
480
528
  };
481
529
  const EnglishContribution = new BaseLanguageContribution({
482
530
  locale: "en",