chronolizer 0.2.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 { 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, 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"],
@@ -45,6 +81,10 @@ const unitPhrases = [
45
81
  next: "morgen",
46
82
  compound: "tages",
47
83
  currentGenitive: "dieses tages",
84
+ previousGenitive: "letzten tages",
85
+ nextGenitive: "nächsten tages",
86
+ indefiniteDative: "einem tag",
87
+ indefiniteGenitive: "eines tages",
48
88
  dative: "tagen",
49
89
  genitive: "des tages"
50
90
  },
@@ -57,6 +97,10 @@ const unitPhrases = [
57
97
  next: "nächste woche",
58
98
  compound: "wochen",
59
99
  currentGenitive: "dieser woche",
100
+ previousGenitive: "letzter woche",
101
+ nextGenitive: "nächster woche",
102
+ indefiniteDative: "einer woche",
103
+ indefiniteGenitive: "einer woche",
60
104
  dative: "wochen",
61
105
  genitive: "der woche"
62
106
  },
@@ -69,6 +113,10 @@ const unitPhrases = [
69
113
  next: "nächster monat",
70
114
  compound: "monats",
71
115
  currentGenitive: "dieses monats",
116
+ previousGenitive: "letzten monats",
117
+ nextGenitive: "nächsten monats",
118
+ indefiniteDative: "einem monat",
119
+ indefiniteGenitive: "eines monats",
72
120
  dative: "monaten",
73
121
  genitive: "des monats"
74
122
  },
@@ -81,6 +129,10 @@ const unitPhrases = [
81
129
  next: "nächstes quartal",
82
130
  compound: "quartals",
83
131
  currentGenitive: "dieses quartals",
132
+ previousGenitive: "letzten quartals",
133
+ nextGenitive: "nächsten quartals",
134
+ indefiniteDative: "einem quartal",
135
+ indefiniteGenitive: "eines quartals",
84
136
  dative: "quartalen",
85
137
  genitive: "des quartals"
86
138
  },
@@ -93,11 +145,33 @@ const unitPhrases = [
93
145
  next: "nächstes jahr",
94
146
  compound: "jahres",
95
147
  currentGenitive: "dieses jahres",
148
+ previousGenitive: "letzten jahres",
149
+ nextGenitive: "nächsten jahres",
150
+ indefiniteDative: "einem jahr",
151
+ indefiniteGenitive: "eines jahres",
96
152
  dative: "jahren",
97
153
  genitive: "des jahres"
98
154
  }
99
155
  ];
100
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
+ }]);
101
175
  const canonicalToDate = (entry) => `seit ${title(entry.compound)}beginn`;
102
176
  const canonicalRelative = (value) => {
103
177
  const separator = value.lastIndexOf(" ");
@@ -141,7 +215,125 @@ const currentBoundaryPhrases = unitPhrases.flatMap((entry) => [
141
215
  entry,
142
216
  phrase
143
217
  })));
218
+ const declinedBoundaryPeriods = [
219
+ [
220
+ "diese woche",
221
+ "week",
222
+ 0,
223
+ "dieser woche",
224
+ "diese woche"
225
+ ],
226
+ [
227
+ "letzte woche",
228
+ "week",
229
+ -1,
230
+ "letzter woche",
231
+ "letzte woche"
232
+ ],
233
+ [
234
+ "nächste woche",
235
+ "week",
236
+ 1,
237
+ "nächster woche",
238
+ "nächste woche"
239
+ ],
240
+ [
241
+ "dieser monat",
242
+ "month",
243
+ 0,
244
+ "diesem monat",
245
+ "diesen monat"
246
+ ],
247
+ [
248
+ "letzter monat",
249
+ "month",
250
+ -1,
251
+ "letztem monat",
252
+ "letzten monat"
253
+ ],
254
+ [
255
+ "nächster monat",
256
+ "month",
257
+ 1,
258
+ "nächstem monat",
259
+ "nächsten monat"
260
+ ],
261
+ [
262
+ "dieses quartal",
263
+ "quarter",
264
+ 0,
265
+ "diesem quartal",
266
+ "dieses quartal"
267
+ ],
268
+ [
269
+ "letztes quartal",
270
+ "quarter",
271
+ -1,
272
+ "letztem quartal",
273
+ "letztes quartal"
274
+ ],
275
+ [
276
+ "nächstes quartal",
277
+ "quarter",
278
+ 1,
279
+ "nächstem quartal",
280
+ "nächstes quartal"
281
+ ],
282
+ [
283
+ "dieses jahr",
284
+ "year",
285
+ 0,
286
+ "diesem jahr",
287
+ "dieses jahr"
288
+ ],
289
+ [
290
+ "letztes jahr",
291
+ "year",
292
+ -1,
293
+ "letztem jahr",
294
+ "letztes jahr"
295
+ ],
296
+ [
297
+ "nächstes jahr",
298
+ "year",
299
+ 1,
300
+ "nächstem jahr",
301
+ "nächstes jahr"
302
+ ]
303
+ ];
144
304
  const relativeAliases = [
305
+ ...unitPhrases.flatMap((entry) => [
306
+ [
307
+ entry.currentGenitive,
308
+ entry.unit,
309
+ 0,
310
+ entry.current
311
+ ],
312
+ [
313
+ entry.previousGenitive,
314
+ entry.unit,
315
+ -1,
316
+ entry.previous
317
+ ],
318
+ [
319
+ entry.nextGenitive,
320
+ entry.unit,
321
+ 1,
322
+ entry.next
323
+ ]
324
+ ]),
325
+ ...declinedBoundaryPeriods.map(([canonical, unit, direction, dative]) => [
326
+ dative,
327
+ unit,
328
+ direction,
329
+ canonical
330
+ ]),
331
+ [
332
+ "vorgestern",
333
+ "day",
334
+ -2,
335
+ "vorgestern"
336
+ ],
145
337
  [
146
338
  "vorletzten tag",
147
339
  "day",
@@ -414,6 +606,20 @@ const monthNumber = (value) => {
414
606
  const shortIndex = monthAbbreviations.findIndex((aliases) => aliases.some((alias) => alias === normalized));
415
607
  return shortIndex === -1 ? void 0 : shortIndex + 1;
416
608
  };
609
+ const currentDateLabel = (day, month) => `${day}. ${title(textAt(months, month - 1))}`;
610
+ const withRelativeCase = (canonical, grammaticalCase) => {
611
+ const period = declinedBoundaryPeriods.find((entry) => canonical === canonicalRelative(entry[0]));
612
+ if (period === void 0) return canonical;
613
+ return canonicalRelative(period[grammaticalCase === "dative" ? 3 : 4]);
614
+ };
615
+ const withRelativeGenitive = (canonical) => {
616
+ for (const entry of unitPhrases) {
617
+ if (canonical === canonicalRelative(entry.current)) return canonicalRelative(entry.currentGenitive);
618
+ if (canonical === canonicalRelative(entry.previous)) return canonicalRelative(entry.previousGenitive);
619
+ if (canonical === canonicalRelative(entry.next)) return canonicalRelative(entry.nextGenitive);
620
+ }
621
+ return canonical;
622
+ };
417
623
  const parseNamedDate = (input) => {
418
624
  const named = String$1.match(/^([0-3]?\d)\.? ([a-zäöüß]+\.?) (\d{4})$/u)(input);
419
625
  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 +633,8 @@ const parseNamedDate = (input) => {
427
633
  if (isIsoDate(value) && value !== "9999-12-31") return Option.some(fixedDatePeriod(value, `${day}. ${title(textAt(months, month - 1))} ${year}`));
428
634
  }
429
635
  }
430
- return Option.none();
636
+ const current = String$1.match(/^([0-3]?\d)\.? ([a-zäöüß]+\.?)$/u)(input);
637
+ return Option.isSome(current) ? namedCurrentYearDatePeriod(textAt(current.value, 2), textAt(current.value, 1), monthNumber, currentDateLabel) : Option.none();
431
638
  };
432
639
  const quarterNames = [
433
640
  "erstes",
@@ -442,8 +649,8 @@ const quarterNumber = (value) => {
442
649
  return index === -1 ? void 0 : index + 1;
443
650
  };
444
651
  const relativeDirection = (value) => {
445
- if (value === "letzten") return -1;
446
- if (value === "nächsten") return 1;
652
+ if (value.startsWith("letzt")) return -1;
653
+ if (value.startsWith("nächst")) return 1;
447
654
  return 0;
448
655
  };
449
656
  const parseQuarter = (input) => {
@@ -471,12 +678,13 @@ const parseQuarter = (input) => {
471
678
  const quarter = quarterNumber(textAt(standalone.value, 1));
472
679
  return quarter === void 0 ? Option.none() : Option.some(quarterOfRelativeYear(quarter, 0, `Q${quarter}`));
473
680
  };
474
- const parseBasePeriod = (input) => {
475
- if (isIsoDate(input) && input !== "9999-12-31") return Option.some(fixedDatePeriod(input, input));
476
- const namedDate = parseNamedDate(input);
477
- if (Option.isSome(namedDate)) return namedDate;
478
- const quarter = parseQuarter(input);
479
- 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;
480
688
  const yearMatch = String$1.match(/^(?:(?:das )?(?:kalender)?jahr )?(\d{4})$/u)(input);
481
689
  if (Option.isSome(yearMatch)) {
482
690
  const year = validYear(textAt(yearMatch.value, 1));
@@ -488,6 +696,18 @@ const parseBasePeriod = (input) => {
488
696
  const year = validYear(textAt(monthYear.value, 2));
489
697
  if (month !== void 0 && year !== void 0) return Option.some(fixedMonthPeriod(year, month, `${title(textAt(months, month - 1))} ${year}`));
490
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
+ }
491
711
  const relativeMonth = String$1.match(/^([a-zäöüß]+\.?) (letzten|dieses|nächsten) jahres$/u)(input);
492
712
  if (Option.isSome(relativeMonth)) {
493
713
  const directionText = textAt(relativeMonth.value, 2);
@@ -497,11 +717,18 @@ const parseBasePeriod = (input) => {
497
717
  }
498
718
  const standaloneMonth = monthNumber(input);
499
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;
500
725
  if (input === "wochenende" || input === "dieses wochenende" || input === "am wochenende") return Option.some(relativeWeekend(0, "dieses Wochenende"));
501
726
  if (input === "letztes wochenende" || input === "vergangenes wochenende") return Option.some(relativeWeekend(-1, "letztes Wochenende"));
502
727
  if (input === "nächstes wochenende" || input === "kommendes wochenende") return Option.some(relativeWeekend(1, "nächstes Wochenende"));
503
728
  if (input === "vorletztes wochenende") return Option.some(relativeWeekend(-2, "vorletztes Wochenende"));
504
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));
505
732
  const relative = unitPhrases.find((entry) => entry.current === input || entry.previous === input || entry.next === input);
506
733
  if (relative !== void 0) {
507
734
  let direction = 0;
@@ -514,29 +741,52 @@ const parseBasePeriod = (input) => {
514
741
  return Option.none();
515
742
  };
516
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
+ }
517
762
  const edge = String$1.match(/^(anfang|beginn|ende) (.+)$/u)(input);
518
763
  if (Option.isSome(edge)) {
519
764
  const edgeName = textAt(edge.value, 1);
520
- 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));
521
769
  if (Option.isSome(period)) {
522
- const canonical = `${edgeName === "ende" ? "Ende" : "Anfang"} ${period.value.canonical}`;
770
+ const canonical = `${edgeName === "ende" ? "Ende" : "Anfang"} ${withRelativeGenitive(period.value.canonical)}`;
523
771
  return Option.some(edgeName === "ende" ? periodEndDay(period.value, canonical) : periodStartDay(period.value, canonical));
524
772
  }
525
773
  }
526
774
  const wrapper = [
527
775
  "im ",
776
+ "am ",
528
777
  "für ",
529
778
  "während "
530
779
  ].find((prefix) => input.startsWith(prefix));
531
- 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);
532
782
  };
533
783
  const currentBoundaryCandidate = (input) => {
534
784
  const match = currentBoundaryPhrases.find((entry) => entry.phrase === input);
535
785
  if (match === void 0) return Option.none();
536
786
  const period = relativePeriod(match.entry.unit, 0, match.entry.current);
537
- return Option.some(periodBoundaryCandidate(period, match.boundary, match.phrase));
787
+ return Option.some(periodBoundaryCandidate(period, match.boundary, canonicalRelative(match.phrase)));
538
788
  };
539
- const boundaryCandidate = (input) => openBoundaryCandidate(input, [
789
+ const boundaryCandidate = (input) => Option.map(openBoundaryCandidate(input, [
540
790
  ["seit dem beginn von ", "since"],
541
791
  ["seit dem anfang von ", "since"],
542
792
  ["seit dem jahr ", "since"],
@@ -563,12 +813,22 @@ const boundaryCandidate = (input) => openBoundaryCandidate(input, [
563
813
  ["nach ", "after"],
564
814
  ["bis ", "before"],
565
815
  ["ab ", "since"]
566
- ], parsePeriod);
816
+ ], parsePeriod), (result) => candidate(result.range, canonicalRelative(input)));
567
817
  const countedUnit = (value, amount, dative = false) => unitPhrases.find((entry) => {
568
818
  if (amount === 1) return value === entry.noun;
569
819
  return value === (dative ? entry.dative : entry.plural);
570
820
  });
571
821
  const parseCalendarOffset = (input) => {
822
+ const singular = String$1.match(/^(vor|in) (einem tag|einer woche|einem monat|einem quartal|einem jahr)$/u)(input);
823
+ if (Option.isSome(singular)) {
824
+ const entry = unitPhrases.find((unit) => unit.indefiniteDative === textAt(singular.value, 2));
825
+ if (entry !== void 0) {
826
+ const isPast = textAt(singular.value, 1) === "vor";
827
+ const direction = isPast ? -1 : 1;
828
+ const canonical = `${isPast ? "vor" : "in"} ${canonicalRelative(entry.indefiniteDative)}`;
829
+ return Option.some(relativePeriod(entry.unit, direction, canonical));
830
+ }
831
+ }
572
832
  const prefixed = String$1.match(/^(vor|in) ([1-9]\d*) (tag|tagen|woche|wochen|monat|monaten|quartal|quartalen|jahr|jahren)$/u)(input);
573
833
  const prior = String$1.match(/^([1-9]\d*) (tag|tage|woche|wochen|monat|monate|quartal|quartale|jahr|jahre) zuvor$/u)(input);
574
834
  const match = Option.firstSomeOf([prefixed, prior]);
@@ -584,53 +844,79 @@ const parseCalendarOffset = (input) => {
584
844
  const directionName = isPast ? "vor" : "in";
585
845
  const noun = amount.value === 1 ? entry.noun : entry.dative;
586
846
  const canonical = `${directionName} ${amount.value} ${title(noun)}`;
587
- return Option.some(candidate(periodRange(relativePeriod(entry.unit, direction, canonical)), canonical));
847
+ return Option.some(relativePeriod(entry.unit, direction, canonical));
588
848
  };
589
849
  const parseRollingPeriod = (input) => {
590
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);
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);
851
+ 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);
852
+ const singularSince = String$1.match(/^seit (einem tag|einer woche|einem monat|einem quartal|einem jahr)$/u)(input);
592
853
  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
854
  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
855
  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);
856
+ const singularFuture = String$1.match(/^innerhalb (eines tages|einer woche|eines monats|eines quartals|eines jahres)$/u)(input);
595
857
  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
858
  const match = Option.firstSomeOf([
597
859
  past,
598
860
  pastDative,
861
+ singularSince,
599
862
  pastGenitive,
600
863
  future,
601
864
  futureDative,
865
+ singularFuture,
602
866
  futureGenitive
603
867
  ]);
604
868
  if (Option.isNone(match)) return Option.none();
605
- const amount = parseTrailingCount(textAt(match.value, 1));
869
+ const isSingular = Option.isSome(singularSince) || Option.isSome(singularFuture);
870
+ const amount = parseTrailingCount(isSingular ? "1" : textAt(match.value, 1));
606
871
  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);
872
+ const usesDative = Option.isSome(pastDative) || Option.isSome(singularSince) || Option.isSome(futureDative);
873
+ const sinceEntry = Option.isSome(singularSince) ? unitPhrases.find((unit) => unit.indefiniteDative === textAt(match.value, 1)) : void 0;
874
+ const futureEntry = Option.isSome(singularFuture) ? unitPhrases.find((unit) => unit.indefiniteGenitive === textAt(match.value, 1)) : void 0;
875
+ const countedEntry = isSingular ? void 0 : countedUnit(textAt(match.value, 2), amount.value, usesDative);
876
+ const entry = sinceEntry ?? futureEntry ?? countedEntry;
609
877
  if (entry === void 0) return Option.none();
610
- const isFuture = Option.isSome(future) || Option.isSome(futureDative) || Option.isSome(futureGenitive);
878
+ const isFuture = Option.isSome(future) || Option.isSome(futureDative) || Option.isSome(singularFuture) || Option.isSome(futureGenitive);
611
879
  const range = isFuture ? futureRange(amount.value, entry.unit) : trailingRange(amount.value, entry.unit);
880
+ if (amount.value === 1) {
881
+ const phrase = isFuture ? `innerhalb ${canonicalRelative(entry.indefiniteGenitive)}` : `seit ${canonicalRelative(entry.indefiniteDative)}`;
882
+ return Option.some(candidate(range, phrase));
883
+ }
612
884
  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)}`));
885
+ return Option.some(candidate(range, `${direction} ${amount.value} ${title(entry.plural)}`));
886
+ };
887
+ const parseElidedDateRange = (input) => {
888
+ const match = String$1.match(/^(?:vom )?([0-3]?\d)\.(?: bis |[–—-])(?:zum )?([0-3]?\d)\.? ([a-zäöüß]+\.?)(?: (\d{4}))?$/u)(input);
889
+ if (Option.isNone(match)) return Option.none();
890
+ const lowerDay = textAt(match.value, 1);
891
+ const upperDay = textAt(match.value, 2);
892
+ const month = textAt(match.value, 3);
893
+ const year = textAt(match.value, 4);
894
+ const suffix = year.length === 0 ? "" : ` ${year}`;
895
+ return joinedPeriodCandidate(`von ${lowerDay}. ${month}${suffix} bis ${upperDay}. ${month}${suffix}`, [["von ", " bis "]], parsePeriod, (lower, upper) => `von ${lower} bis ${upper}`);
615
896
  };
616
897
  const parseGerman = (input) => {
617
898
  const remaining = remainingPeriodPhrases.find((entry) => entry.phrase === input);
618
899
  if (remaining !== void 0) return Option.some(candidate(remainingPeriodRange(remaining.entry.unit), `Rest ${canonicalRelative(remaining.entry.genitive)}`));
619
900
  if (["bis heute", "bis jetzt"].includes(input)) return Option.some(candidate(untilNowRange(), "bis heute"));
620
901
  if (["ab jetzt", "von jetzt an"].includes(input)) return Option.some(candidate(fromNowRange(), "ab jetzt"));
621
- const offset = parseCalendarOffset(input);
622
- if (Option.isSome(offset)) return offset;
623
902
  const rolling = parseRollingPeriod(input);
624
903
  if (Option.isSome(rolling)) return rolling;
625
904
  const toDate = toDatePhrases.find((entry) => entry.phrase === input);
626
905
  if (toDate !== void 0) return Option.some(candidate(periodToDateRange(toDate.entry.unit), canonicalToDate(toDate.entry)));
627
906
  const currentBoundary = currentBoundaryCandidate(input);
628
907
  if (Option.isSome(currentBoundary)) return currentBoundary;
908
+ const elided = parseElidedDateRange(input);
909
+ if (Option.isSome(elided)) return elided;
629
910
  const nowBounded = joinedNowCandidate(input, [
630
911
  ["von ", " bis heute"],
631
912
  ["vom ", " bis heute"],
632
913
  ["zwischen ", " und heute"]
633
- ], ["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}`);
634
920
  if (Option.isSome(nowBounded)) return nowBounded;
635
921
  const bounded = joinedPeriodCandidate(input, [
636
922
  ["von ", " bis einschließlich "],
@@ -657,9 +943,10 @@ const parseGerman = (input) => {
657
943
  if (Option.isSome(bounded)) return bounded;
658
944
  const boundary = boundaryCandidate(input);
659
945
  if (Option.isSome(boundary)) return boundary;
660
- return Option.map(parsePeriod(input), (period) => candidate(periodRange(period), period.canonical));
946
+ return parsePeriod(input).pipe(Option.map((period) => candidate(periodRange(period), period.canonical)));
661
947
  };
662
948
  const staticPeriodPhrases = [
949
+ "vorgestern",
663
950
  "dieses wochenende",
664
951
  "letztes wochenende",
665
952
  "nächstes wochenende",
@@ -669,12 +956,12 @@ const staticPeriodPhrases = [
669
956
  entry.current,
670
957
  entry.previous,
671
958
  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}`
959
+ `anfang ${entry.currentGenitive}`,
960
+ `ende ${entry.currentGenitive}`,
961
+ `anfang ${entry.previousGenitive}`,
962
+ `ende ${entry.previousGenitive}`,
963
+ `anfang ${entry.nextGenitive}`,
964
+ `ende ${entry.nextGenitive}`
678
965
  ]),
679
966
  ...[
680
967
  1,
@@ -688,9 +975,12 @@ const staticPeriodPhrases = [
688
975
  ]),
689
976
  ...months.flatMap((month) => [
690
977
  month,
978
+ `letzter ${month}`,
979
+ `nächster ${month}`,
691
980
  `${month} letzten jahres`,
692
981
  `${month} nächsten jahres`
693
- ])
982
+ ]),
983
+ ...nextWeekdays.map((entry) => entry.phrase)
694
984
  ];
695
985
  const staticPeriods = periodsFromPhrases(staticPeriodPhrases, parsePeriod);
696
986
  const boundaryPrefixes = [
@@ -719,8 +1009,21 @@ const countedSuggestions = (input) => {
719
1009
  const germanSuggestionPhrases = [
720
1010
  ...unitPhrases.map(canonicalToDate),
721
1011
  ...unitPhrases.map((entry) => `rest ${entry.genitive}`),
1012
+ ...unitPhrases.flatMap((entry) => [
1013
+ `seit ${entry.indefiniteDative}`,
1014
+ `innerhalb ${entry.indefiniteGenitive}`,
1015
+ `vor ${entry.indefiniteDative}`,
1016
+ `in ${entry.indefiniteDative}`
1017
+ ]),
722
1018
  ...staticPeriodPhrases,
723
- ...prefixNaturalPhrases(staticPeriodPhrases, boundaryPrefixes),
1019
+ ...currentBoundaryPhrases.map((entry) => entry.phrase),
1020
+ ...declinedBoundaryPeriods.flatMap(([, , , dative, accusative]) => [
1021
+ `seit ${dative}`,
1022
+ `vor ${dative}`,
1023
+ `bis einschließlich ${accusative}`,
1024
+ `nach ${dative}`
1025
+ ]),
1026
+ ...prefixNaturalPhrases(months, boundaryPrefixes),
724
1027
  "bis heute",
725
1028
  "ab jetzt"
726
1029
  ].map((phrase) => normalizeNaturalText(phrase, "de"));
@@ -734,6 +1037,17 @@ const suggestGerman = (input, limit) => {
734
1037
  ], limit);
735
1038
  };
736
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
+ }
737
1051
  const offset = calendarPeriodOffset(range);
738
1052
  if (Option.isSome(offset) && Math.abs(offset.value.amount) > 1) {
739
1053
  const entry = unitPhrases.find((unit) => unit.unit === offset.value.unit);
@@ -742,20 +1056,26 @@ const renderGerman = (range) => {
742
1056
  const future = futurePeriod(range);
743
1057
  if (Option.isSome(future)) {
744
1058
  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)}`);
1059
+ if (entry !== void 0) return Option.some(future.value.amount === 1 ? `innerhalb ${canonicalRelative(entry.indefiniteGenitive)}` : `nächste ${future.value.amount} ${title(entry.plural)}`);
746
1060
  }
747
1061
  const trailing = trailingPeriod(range);
748
1062
  if (Option.isSome(trailing)) {
749
1063
  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)}`);
1064
+ if (entry !== void 0) return Option.some(trailing.value.amount === 1 ? `seit ${canonicalRelative(entry.indefiniteDative)}` : `letzte ${trailing.value.amount} ${title(entry.plural)}`);
751
1065
  }
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");
1066
+ const dated = [...datedPeriods(range, months), ...datedQuarterPeriods(range)];
1067
+ const periods = [
1068
+ ...staticPeriods,
1069
+ ...currentYearDatePeriods(range, currentDateLabel),
1070
+ ...periodsFromPhrases([...dated, ...dated.flatMap((phrase) => [`anfang ${phrase}`, `ende ${phrase}`])], parsePeriod)
1071
+ ];
1072
+ 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
1073
  };
755
1074
  const GermanContribution = new BaseLanguageContribution({
756
1075
  locale: "de",
757
1076
  vocabulary: [
758
1077
  ...months,
1078
+ ...weekdays,
759
1079
  ...quarterNames,
760
1080
  "q1",
761
1081
  "q2",
@@ -770,7 +1090,11 @@ const GermanContribution = new BaseLanguageContribution({
770
1090
  entry.noun,
771
1091
  entry.plural,
772
1092
  entry.dative,
1093
+ ...entry.indefiniteDative.split(" "),
1094
+ ...entry.indefiniteGenitive.split(" "),
773
1095
  ...entry.currentGenitive.split(" "),
1096
+ ...entry.previousGenitive.split(" "),
1097
+ ...entry.nextGenitive.split(" "),
774
1098
  ...entry.current.split(" "),
775
1099
  ...entry.previous.split(" "),
776
1100
  ...entry.next.split(" ")
@@ -813,8 +1137,8 @@ const GermanContribution = new BaseLanguageContribution({
813
1137
  "zwischen",
814
1138
  "zuvor"
815
1139
  ],
816
- normalize: normalizeNaturalText,
817
- correct: correctWhitespaceSeparatedText,
1140
+ normalize: normalizeGerman,
1141
+ correct: correctGerman,
818
1142
  parseExact: parseGerman,
819
1143
  suggest: suggestGerman,
820
1144
  render: renderGerman