chronolizer 0.3.0 → 0.4.1

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.
@@ -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 { 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";
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, periodBoundaryCandidate, periodEndDay, periodRange, periodStartDay, periodToDateRange, periodsFromPhrases, quarterOfRelativeYear, relativePeriod, relativeWeekday, relativeWeekend, remainingPeriodRange, renderPeriodRange, sequentialCountAliases, shiftPeriod, textAt, trailingPeriod, trailingRange, untilNowRange, validYear } from "./shared.mjs";
8
7
  import { Effect, Option, String as String$1 } from "effect";
9
8
  //#region src/locales/de.ts
10
9
  const months = [
@@ -21,10 +20,63 @@ const months = [
21
20
  "november",
22
21
  "dezember"
23
22
  ];
23
+ const minimumMonthDays = [
24
+ 31,
25
+ 28,
26
+ 31,
27
+ 30,
28
+ 31,
29
+ 30,
30
+ 31,
31
+ 31,
32
+ 30,
33
+ 31,
34
+ 30,
35
+ 31
36
+ ];
37
+ const germanCountWords = [
38
+ "zwei",
39
+ "drei",
40
+ "vier",
41
+ "fünf",
42
+ "sechs",
43
+ "sieben",
44
+ "acht",
45
+ "neun",
46
+ "zehn",
47
+ "elf",
48
+ "zwölf",
49
+ "dreizehn",
50
+ "vierzehn",
51
+ "fünfzehn",
52
+ "sechzehn",
53
+ "siebzehn",
54
+ "achtzehn",
55
+ "neunzehn",
56
+ "zwanzig"
57
+ ];
58
+ const germanCountOnes = [[1, "ein"], ...germanCountWords.slice(0, 8).map((word, index) => [index + 2, word])];
59
+ const germanCountAliases = [...sequentialCountAliases(germanCountWords.map((word) => [word]), 2), ...compoundCountAliases(decimalTens([
60
+ "zwanzig",
61
+ "dreißig",
62
+ "vierzig",
63
+ "fünfzig",
64
+ "sechzig",
65
+ "siebzig",
66
+ "achtzig",
67
+ "neunzig"
68
+ ]), germanCountOnes, (ten, one) => [`${one}und${ten}`])];
69
+ const normalizeGermanCounts = compileCountAliasNormalizer(germanCountAliases);
70
+ const germanCountVocabulary = new Set(countAliasVocabulary(germanCountAliases));
71
+ const correctGerman = (input, vocabulary) => correctWhitespaceSeparatedText(input, vocabulary, germanCountVocabulary);
24
72
  const monthAbbreviations = [
25
73
  ["jan"],
26
74
  ["feb"],
27
- ["mär", "mrz"],
75
+ [
76
+ "mär",
77
+ "mrz",
78
+ "maerz"
79
+ ],
28
80
  ["apr"],
29
81
  ["mai"],
30
82
  ["jun"],
@@ -118,6 +170,24 @@ const unitPhrases = [
118
170
  }
119
171
  ];
120
172
  const title = (value) => `${value.slice(0, 1).toLocaleUpperCase("de")}${value.slice(1)}`;
173
+ const weekdays = [
174
+ "montag",
175
+ "dienstag",
176
+ "mittwoch",
177
+ "donnerstag",
178
+ "freitag",
179
+ "samstag",
180
+ "sonntag"
181
+ ];
182
+ const nextWeekdays = weekdays.flatMap((weekday, day) => [{
183
+ phrase: `nächster ${weekday}`,
184
+ day,
185
+ canonical: `nächster ${title(weekday)}`
186
+ }, {
187
+ phrase: `nächsten ${weekday}`,
188
+ day,
189
+ canonical: `nächster ${title(weekday)}`
190
+ }]);
121
191
  const canonicalToDate = (entry) => `seit ${title(entry.compound)}beginn`;
122
192
  const canonicalRelative = (value) => {
123
193
  const separator = value.lastIndexOf(" ");
@@ -161,390 +231,45 @@ const currentBoundaryPhrases = unitPhrases.flatMap((entry) => [
161
231
  entry,
162
232
  phrase
163
233
  })));
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
- ];
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
- ],
283
- [
284
- "vorletzten tag",
285
- "day",
286
- -2,
287
- "vorletzter tag"
288
- ],
289
- [
290
- "übernächsten tag",
291
- "day",
292
- 2,
293
- "übernächster tag"
294
- ],
295
- [
296
- "vorletzte woche",
297
- "week",
298
- -2,
299
- "vorletzte woche"
300
- ],
301
- [
302
- "übernächste woche",
303
- "week",
304
- 2,
305
- "übernächste woche"
306
- ],
307
- [
308
- "vorletzten monat",
309
- "month",
310
- -2,
311
- "vorletzter monat"
312
- ],
313
- [
314
- "übernächsten monat",
315
- "month",
316
- 2,
317
- "übernächster monat"
318
- ],
319
- [
320
- "vorletztes quartal",
321
- "quarter",
322
- -2,
323
- "vorletztes quartal"
324
- ],
325
- [
326
- "übernächstes quartal",
327
- "quarter",
328
- 2,
329
- "übernächstes quartal"
330
- ],
331
- [
332
- "vorletztes jahr",
333
- "year",
334
- -2,
335
- "vorletztes jahr"
336
- ],
337
- [
338
- "übernächstes jahr",
339
- "year",
340
- 2,
341
- "übernächstes jahr"
342
- ],
343
- [
344
- "letzten tag",
345
- "day",
346
- -1,
347
- "gestern"
348
- ],
349
- [
350
- "vergangenen tag",
351
- "day",
352
- -1,
353
- "gestern"
354
- ],
355
- [
356
- "vorherigen tag",
357
- "day",
358
- -1,
359
- "gestern"
360
- ],
361
- [
362
- "diesen tag",
363
- "day",
364
- 0,
365
- "heute"
366
- ],
367
- [
368
- "heutigen tag",
369
- "day",
370
- 0,
371
- "heute"
372
- ],
373
- [
374
- "nächsten tag",
375
- "day",
376
- 1,
377
- "morgen"
378
- ],
379
- [
380
- "kommenden tag",
381
- "day",
382
- 1,
383
- "morgen"
384
- ],
385
- [
386
- "vergangene woche",
387
- "week",
388
- -1,
389
- "letzte woche"
390
- ],
391
- [
392
- "vorherige woche",
393
- "week",
394
- -1,
395
- "letzte woche"
396
- ],
397
- [
398
- "aktuelle woche",
399
- "week",
400
- 0,
401
- "diese woche"
402
- ],
403
- [
404
- "laufende woche",
405
- "week",
406
- 0,
407
- "diese woche"
408
- ],
409
- [
410
- "kommende woche",
411
- "week",
412
- 1,
413
- "nächste woche"
414
- ],
415
- [
416
- "folgende woche",
417
- "week",
418
- 1,
419
- "nächste woche"
420
- ],
421
- [
422
- "letzten monat",
423
- "month",
424
- -1,
425
- "letzter monat"
426
- ],
427
- [
428
- "vergangenen monat",
429
- "month",
430
- -1,
431
- "letzter monat"
432
- ],
433
- [
434
- "vorherigen monat",
435
- "month",
436
- -1,
437
- "letzter monat"
438
- ],
439
- [
440
- "diesen monat",
441
- "month",
442
- 0,
443
- "dieser monat"
444
- ],
445
- [
446
- "aktuellen monat",
447
- "month",
448
- 0,
449
- "dieser monat"
450
- ],
451
- [
452
- "laufenden monat",
453
- "month",
454
- 0,
455
- "dieser monat"
456
- ],
457
- [
458
- "nächsten monat",
459
- "month",
460
- 1,
461
- "nächster monat"
462
- ],
463
- [
464
- "kommenden monat",
465
- "month",
466
- 1,
467
- "nächster monat"
468
- ],
469
- [
470
- "folgenden monat",
471
- "month",
472
- 1,
473
- "nächster monat"
474
- ],
475
- [
476
- "vergangenes quartal",
477
- "quarter",
478
- -1,
479
- "letztes quartal"
480
- ],
481
- [
482
- "vorheriges quartal",
483
- "quarter",
484
- -1,
485
- "letztes quartal"
486
- ],
487
- [
488
- "aktuelles quartal",
489
- "quarter",
490
- 0,
491
- "dieses quartal"
492
- ],
493
- [
494
- "laufendes quartal",
495
- "quarter",
496
- 0,
497
- "dieses quartal"
498
- ],
499
- [
500
- "kommendes quartal",
501
- "quarter",
502
- 1,
503
- "nächstes quartal"
504
- ],
505
- [
506
- "folgendes quartal",
507
- "quarter",
508
- 1,
509
- "nächstes quartal"
510
- ],
511
- [
512
- "vergangenes jahr",
513
- "year",
514
- -1,
515
- "letztes jahr"
516
- ],
517
- [
518
- "vorheriges jahr",
519
- "year",
520
- -1,
521
- "letztes jahr"
522
- ],
523
- [
524
- "aktuelles jahr",
525
- "year",
526
- 0,
527
- "dieses jahr"
528
- ],
529
- [
530
- "laufendes jahr",
531
- "year",
532
- 0,
533
- "dieses jahr"
534
- ],
535
- [
536
- "kommendes jahr",
537
- "year",
538
- 1,
539
- "nächstes jahr"
540
- ],
541
- [
542
- "folgendes jahr",
543
- "year",
544
- 1,
545
- "nächstes jahr"
546
- ]
234
+ const relativeModifierStems = [
235
+ ["vorletzt", -2],
236
+ ["übernächst", 2],
237
+ ["vergangen", -1],
238
+ ["vorherig", -1],
239
+ ["letzt", -1],
240
+ ["heutig", 0],
241
+ ["dies", 0],
242
+ ["aktuell", 0],
243
+ ["laufend", 0],
244
+ ["nächst", 1],
245
+ ["kommend", 1],
246
+ ["folgend", 1]
547
247
  ];
248
+ const relativeModifierVocabulary = relativeModifierStems.flatMap(([stem]) => [
249
+ "e",
250
+ "er",
251
+ "es",
252
+ "en",
253
+ "em"
254
+ ].map((ending) => `${stem}${ending}`));
255
+ const relativeUnitCanonical = (entry, direction) => {
256
+ if (direction === -1) return entry.previous;
257
+ if (direction === 0) return entry.current;
258
+ if (direction === 1) return entry.next;
259
+ return `${direction < 0 ? "vor" : "in"} 2 ${title(entry.dative)}`;
260
+ };
261
+ const parseModifiedUnit = (input) => {
262
+ const match = String$1.match(/^([a-zäöüß]+) ([a-zäöüß]+)$/u)(input);
263
+ if (Option.isNone(match)) return Option.none();
264
+ const modifier = relativeModifierStems.find(([stem]) => textAt(match.value, 1).startsWith(stem));
265
+ const entry = unitPhrases.find((unit) => [
266
+ unit.noun,
267
+ unit.plural,
268
+ unit.dative,
269
+ unit.compound
270
+ ].includes(textAt(match.value, 2)));
271
+ return modifier === void 0 || entry === void 0 ? Option.none() : Option.some(relativePeriod(entry.unit, modifier[1], canonicalRelative(relativeUnitCanonical(entry, modifier[1]))));
272
+ };
548
273
  const monthNumber = (value) => {
549
274
  const normalized = value.endsWith(".") ? value.slice(0, -1) : value;
550
275
  const fullIndex = months.findIndex((month) => month === normalized);
@@ -552,11 +277,26 @@ const monthNumber = (value) => {
552
277
  const shortIndex = monthAbbreviations.findIndex((aliases) => aliases.some((alias) => alias === normalized));
553
278
  return shortIndex === -1 ? void 0 : shortIndex + 1;
554
279
  };
280
+ const normalizeGerman = (input, locale) => {
281
+ const normalized = normalizeGermanCounts(normalizeNaturalText(input, locale));
282
+ if (normalized.endsWith(" auflaufend")) return `seit ${normalized.slice(0, -11)}`;
283
+ const fullMonth = String$1.match(/^(?:([a-zäöüß]+\.?) per )?(?:(0?1)\.-)?([0-3]?\d)\.([01]?\d)\.?$/u)(normalized);
284
+ if (Option.isNone(fullMonth)) return normalized;
285
+ const named = textAt(fullMonth.value, 1);
286
+ const lower = textAt(fullMonth.value, 2);
287
+ if (named.length === 0 && lower.length === 0) return normalized;
288
+ const numericMonth = Number(textAt(fullMonth.value, 4));
289
+ const month = named.length === 0 ? numericMonth : monthNumber(named);
290
+ return month !== void 0 && month === numericMonth && Number(textAt(fullMonth.value, 3)) === minimumMonthDays[month - 1] ? textAt(months, month - 1) : normalized;
291
+ };
555
292
  const currentDateLabel = (day, month) => `${day}. ${title(textAt(months, month - 1))}`;
556
293
  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]);
294
+ const separator = canonical.indexOf(" ");
295
+ if (separator === -1) return canonical;
296
+ const modifier = canonical.slice(0, separator);
297
+ if (!relativeModifierStems.some(([stem]) => modifier.startsWith(stem))) return canonical;
298
+ if (grammaticalCase === "dative") return `${modifier.endsWith("e") ? `${modifier}r` : `${modifier.slice(0, -2)}em`}${canonical.slice(separator)}`;
299
+ return `${modifier.endsWith("er") ? `${modifier.slice(0, -2)}en` : modifier}${canonical.slice(separator)}`;
560
300
  };
561
301
  const withRelativeGenitive = (canonical) => {
562
302
  for (const entry of unitPhrases) {
@@ -570,15 +310,7 @@ const parseNamedDate = (input) => {
570
310
  const named = String$1.match(/^([0-3]?\d)\.? ([a-zäöüß]+\.?) (\d{4})$/u)(input);
571
311
  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}`);
572
312
  const numeric = String$1.match(/^([0-3]?\d)\.([01]?\d)\.(\d{4})$/u)(input);
573
- if (Option.isSome(numeric)) {
574
- const year = validYear(textAt(numeric.value, 3));
575
- const day = Number(textAt(numeric.value, 1));
576
- const month = Number(textAt(numeric.value, 2));
577
- if (year !== void 0 && month >= 1 && month <= 12) {
578
- const value = isoDate(year, month, day);
579
- if (isIsoDate(value) && value !== "9999-12-31") return Option.some(fixedDatePeriod(value, `${day}. ${title(textAt(months, month - 1))} ${year}`));
580
- }
581
- }
313
+ if (Option.isSome(numeric)) return namedDatePeriod(textAt(numeric.value, 3), textAt(numeric.value, 2), textAt(numeric.value, 1), Number, (day, month, year) => `${day}. ${title(textAt(months, month - 1))} ${year}`);
582
314
  const current = String$1.match(/^([0-3]?\d)\.? ([a-zäöüß]+\.?)$/u)(input);
583
315
  return Option.isSome(current) ? namedCurrentYearDatePeriod(textAt(current.value, 2), textAt(current.value, 1), monthNumber, currentDateLabel) : Option.none();
584
316
  };
@@ -595,8 +327,8 @@ const quarterNumber = (value) => {
595
327
  return index === -1 ? void 0 : index + 1;
596
328
  };
597
329
  const relativeDirection = (value) => {
598
- if (value === "letzten") return -1;
599
- if (value === "nächsten") return 1;
330
+ if (value.startsWith("letzt")) return -1;
331
+ if (value.startsWith("nächst")) return 1;
600
332
  return 0;
601
333
  };
602
334
  const parseQuarter = (input) => {
@@ -624,13 +356,13 @@ const parseQuarter = (input) => {
624
356
  const quarter = quarterNumber(textAt(standalone.value, 1));
625
357
  return quarter === void 0 ? Option.none() : Option.some(quarterOfRelativeYear(quarter, 0, `Q${quarter}`));
626
358
  };
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;
359
+ const parseDatedPeriod = (input) => {
360
+ const knownPeriod = Option.firstSomeOf([
361
+ absoluteDatePeriod(input, "de"),
362
+ parseNamedDate(input),
363
+ parseQuarter(input)
364
+ ]);
365
+ if (Option.isSome(knownPeriod)) return knownPeriod;
634
366
  const yearMatch = String$1.match(/^(?:(?:das )?(?:kalender)?jahr )?(\d{4})$/u)(input);
635
367
  if (Option.isSome(yearMatch)) {
636
368
  const year = validYear(textAt(yearMatch.value, 1));
@@ -642,6 +374,22 @@ const parseBasePeriod = (input) => {
642
374
  const year = validYear(textAt(monthYear.value, 2));
643
375
  if (month !== void 0 && year !== void 0) return Option.some(fixedMonthPeriod(year, month, `${title(textAt(months, month - 1))} ${year}`));
644
376
  }
377
+ return Option.none();
378
+ };
379
+ const parseRelativeMonth = (input) => {
380
+ const prefixedRelativeMonth = String$1.match(/^(letzt|dies|nächst)(?:er|en|em) ([a-zäöüß]+\.?)$/u)(input);
381
+ if (Option.isSome(prefixedRelativeMonth)) {
382
+ const month = monthNumber(textAt(prefixedRelativeMonth.value, 2));
383
+ if (month !== void 0) {
384
+ const direction = relativeDirection(textAt(prefixedRelativeMonth.value, 1));
385
+ const modifier = textAt([
386
+ "letzter",
387
+ "dieser",
388
+ "nächster"
389
+ ], direction + 1);
390
+ return Option.some(monthOfRelativeYear(month, direction, `${modifier} ${title(textAt(months, month - 1))}`));
391
+ }
392
+ }
645
393
  const relativeMonth = String$1.match(/^([a-zäöüß]+\.?) (letzten|dieses|nächsten) jahres$/u)(input);
646
394
  if (Option.isSome(relativeMonth)) {
647
395
  const directionText = textAt(relativeMonth.value, 2);
@@ -651,11 +399,18 @@ const parseBasePeriod = (input) => {
651
399
  }
652
400
  const standaloneMonth = monthNumber(input);
653
401
  if (standaloneMonth !== void 0) return Option.some(monthOfRelativeYear(standaloneMonth, 0, title(textAt(months, standaloneMonth - 1))));
402
+ return Option.none();
403
+ };
404
+ const parseBasePeriod = (input) => {
405
+ const knownPeriod = Option.firstSomeOf([parseDatedPeriod(input), parseRelativeMonth(input)]);
406
+ if (Option.isSome(knownPeriod)) return knownPeriod;
654
407
  if (input === "wochenende" || input === "dieses wochenende" || input === "am wochenende") return Option.some(relativeWeekend(0, "dieses Wochenende"));
655
408
  if (input === "letztes wochenende" || input === "vergangenes wochenende") return Option.some(relativeWeekend(-1, "letztes Wochenende"));
656
409
  if (input === "nächstes wochenende" || input === "kommendes wochenende") return Option.some(relativeWeekend(1, "nächstes Wochenende"));
657
410
  if (input === "vorletztes wochenende") return Option.some(relativeWeekend(-2, "vorletztes Wochenende"));
658
411
  if (input === "übernächstes wochenende") return Option.some(relativeWeekend(2, "übernächstes Wochenende"));
412
+ const weekday = nextWeekdays.find((entry) => entry.phrase === input);
413
+ if (weekday !== void 0) return Option.some(relativeWeekday(weekday.day, 1, weekday.canonical));
659
414
  const relative = unitPhrases.find((entry) => entry.current === input || entry.previous === input || entry.next === input);
660
415
  if (relative !== void 0) {
661
416
  let direction = 0;
@@ -663,15 +418,66 @@ const parseBasePeriod = (input) => {
663
418
  if (input === relative.next) direction = 1;
664
419
  return Option.some(relativePeriod(relative.unit, direction, canonicalRelative(input)));
665
420
  }
666
- const alias = relativeAliases.find((entry) => entry[0] === input);
667
- if (alias !== void 0) return Option.some(relativePeriod(alias[1], alias[2], canonicalRelative(alias[3])));
668
- return Option.none();
421
+ if (input === "vorgestern") return Option.some(relativePeriod("day", -2, "vorgestern"));
422
+ return parseModifiedUnit(input);
423
+ };
424
+ const countedUnit = (value, amount, dative = false) => unitPhrases.find((entry) => {
425
+ if (amount === 1) return value === entry.noun;
426
+ return value === (dative ? entry.dative : entry.plural);
427
+ });
428
+ const parseCalendarOffsetValue = (input) => {
429
+ const singular = String$1.match(/^(vor|in) (ein[^ ]* [^ ]+)$/u)(input);
430
+ if (Option.isSome(singular)) {
431
+ const entry = unitPhrases.find((unit) => unit.indefiniteDative === textAt(singular.value, 2));
432
+ if (entry !== void 0) {
433
+ const past = textAt(singular.value, 1) === "vor";
434
+ return Option.some({
435
+ amount: past ? -1 : 1,
436
+ canonical: `${past ? "vor" : "in"} ${canonicalRelative(entry.indefiniteDative)}`,
437
+ entry
438
+ });
439
+ }
440
+ }
441
+ const prefixed = String$1.match(/^(vor|in) ([1-9]\d*) ([^ ]+)$/u)(input);
442
+ const prior = String$1.match(/^([1-9]\d*) ([^ ]+) zuvor$/u)(input);
443
+ const match = Option.firstSomeOf([prefixed, prior]);
444
+ if (Option.isNone(match)) return Option.none();
445
+ const amountText = textAt(match.value, Option.isSome(prefixed) ? 2 : 1);
446
+ const unitText = textAt(match.value, Option.isSome(prefixed) ? 3 : 2);
447
+ const amount = parseTrailingCount(amountText);
448
+ if (Option.isNone(amount)) return Option.none();
449
+ const entry = countedUnit(unitText, amount.value, Option.isSome(prefixed));
450
+ if (entry === void 0) return Option.none();
451
+ const past = Option.isSome(prior) || Option.isSome(prefixed) && textAt(match.value, 1) === "vor";
452
+ const direction = past ? "vor" : "in";
453
+ const noun = amount.value === 1 ? entry.noun : entry.dative;
454
+ return Option.some({
455
+ amount: past ? -amount.value : amount.value,
456
+ canonical: `${direction} ${amount.value} ${title(noun)}`,
457
+ entry
458
+ });
669
459
  };
670
460
  const parsePeriod = (input) => {
461
+ let separator = input.indexOf(" ");
462
+ while (separator !== -1) {
463
+ const offsetStart = separator + 1;
464
+ const firstOffsetCharacter = input[offsetStart] ?? "";
465
+ if (input.startsWith("vor ", offsetStart) || input.startsWith("in ", offsetStart) || firstOffsetCharacter >= "1" && firstOffsetCharacter <= "9" && input.endsWith(" zuvor")) {
466
+ const offset = parseCalendarOffsetValue(input.slice(offsetStart));
467
+ if (Option.isSome(offset)) {
468
+ const period = parsePeriod(input.slice(0, separator));
469
+ if (Option.isSome(period)) return Option.some(shiftPeriod(period.value, offset.value.amount, offset.value.entry.unit, `${period.value.canonical} ${offset.value.canonical}`));
470
+ }
471
+ }
472
+ separator = input.indexOf(" ", offsetStart);
473
+ }
671
474
  const edge = String$1.match(/^(anfang|beginn|ende) (.+)$/u)(input);
672
475
  if (Option.isSome(edge)) {
673
476
  const edgeName = textAt(edge.value, 1);
674
- const period = parseBasePeriod(textAt(edge.value, 2));
477
+ const periodText = textAt(edge.value, 2);
478
+ const basePeriod = parseBasePeriod(periodText);
479
+ const implicit = unitPhrases.find((entry) => entry.genitive === periodText);
480
+ const period = Option.isSome(basePeriod) || implicit === void 0 ? basePeriod : Option.some(relativePeriod(implicit.unit, 0, implicit.current));
675
481
  if (Option.isSome(period)) {
676
482
  const canonical = `${edgeName === "ende" ? "Ende" : "Anfang"} ${withRelativeGenitive(period.value.canonical)}`;
677
483
  return Option.some(edgeName === "ende" ? periodEndDay(period.value, canonical) : periodStartDay(period.value, canonical));
@@ -683,7 +489,8 @@ const parsePeriod = (input) => {
683
489
  "für ",
684
490
  "während "
685
491
  ].find((prefix) => input.startsWith(prefix));
686
- return parseBasePeriod(wrapper === void 0 ? input : input.slice(wrapper.length));
492
+ const base = parseBasePeriod(wrapper === void 0 ? input : input.slice(wrapper.length));
493
+ return Option.isSome(base) ? base : parseCalendarOffset(input);
687
494
  };
688
495
  const currentBoundaryCandidate = (input) => {
689
496
  const match = currentBoundaryPhrases.find((entry) => entry.phrase === input);
@@ -719,50 +526,19 @@ const boundaryCandidate = (input) => Option.map(openBoundaryCandidate(input, [
719
526
  ["bis ", "before"],
720
527
  ["ab ", "since"]
721
528
  ], parsePeriod), (result) => candidate(result.range, canonicalRelative(input)));
722
- const countedUnit = (value, amount, dative = false) => unitPhrases.find((entry) => {
723
- if (amount === 1) return value === entry.noun;
724
- return value === (dative ? entry.dative : entry.plural);
725
- });
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
- }
737
- const prefixed = String$1.match(/^(vor|in) ([1-9]\d*) (tag|tagen|woche|wochen|monat|monaten|quartal|quartalen|jahr|jahren)$/u)(input);
738
- const prior = String$1.match(/^([1-9]\d*) (tag|tage|woche|wochen|monat|monate|quartal|quartale|jahr|jahre) zuvor$/u)(input);
739
- const match = Option.firstSomeOf([prefixed, prior]);
740
- if (Option.isNone(match)) return Option.none();
741
- const amountText = textAt(match.value, Option.isSome(prefixed) ? 2 : 1);
742
- const unitText = textAt(match.value, Option.isSome(prefixed) ? 3 : 2);
743
- const amount = parseTrailingCount(amountText);
744
- if (Option.isNone(amount)) return Option.none();
745
- const entry = countedUnit(unitText, amount.value, Option.isSome(prefixed));
746
- if (entry === void 0) return Option.none();
747
- const isPast = Option.isSome(prior) || Option.isSome(prefixed) && textAt(match.value, 1) === "vor";
748
- const direction = isPast ? -amount.value : amount.value;
749
- const directionName = isPast ? "vor" : "in";
750
- const noun = amount.value === 1 ? entry.noun : entry.dative;
751
- const canonical = `${directionName} ${amount.value} ${title(noun)}`;
752
- return Option.some(candidate(periodRange(relativePeriod(entry.unit, direction, canonical)), canonical));
753
- };
529
+ const parseCalendarOffset = (input) => parseCalendarOffsetValue(input).pipe(Option.map((offset) => relativePeriod(offset.entry.unit, offset.amount, offset.canonical)));
754
530
  const parseRollingPeriod = (input) => {
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);
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);
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);
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);
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);
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);
531
+ const past = String$1.match(/^(?:(?:die letzten|letzten|letzte|vergangene|vorherige) )?([1-9]\d*) ([^ ]+)$/u)(input);
532
+ const pastDative = String$1.match(/^(?:(?:in den letzten|in den vergangenen|in den vorherigen) |seit )([1-9]\d*) ([^ ]+)$/u)(input);
533
+ const singularSince = String$1.match(/^seit (ein[^ ]* [^ ]+)$/u)(input);
534
+ const pastGenitive = String$1.match(/^während der letzten ([1-9]\d*) ([^ ]+)$/u)(input);
535
+ const future = String$1.match(/^(?:die nächsten|nächsten|nächste|kommende) ([1-9]\d*) ([^ ]+)$/u)(input);
536
+ const futureDative = String$1.match(/^(?:in den nächsten|in den kommenden|innerhalb von) ([1-9]\d*) ([^ ]+)$/u)(input);
537
+ const singularFuture = String$1.match(/^innerhalb (ein[^ ]* [^ ]+)$/u)(input);
538
+ const futureGenitive = String$1.match(/^innerhalb der nächsten ([1-9]\d*) ([^ ]+)$/u)(input);
763
539
  const match = Option.firstSomeOf([
764
- past,
765
540
  pastDative,
541
+ past,
766
542
  singularSince,
767
543
  pastGenitive,
768
544
  future,
@@ -799,13 +575,42 @@ const parseElidedDateRange = (input) => {
799
575
  const suffix = year.length === 0 ? "" : ` ${year}`;
800
576
  return joinedPeriodCandidate(`von ${lowerDay}. ${month}${suffix} bis ${upperDay}. ${month}${suffix}`, [["von ", " bis "]], parsePeriod, (lower, upper) => `von ${lower} bis ${upper}`);
801
577
  };
578
+ const boundedJoins = [
579
+ ["von ", " bis einschließlich "],
580
+ ["vom ", " bis einschließlich "],
581
+ ["von ", " bis zum "],
582
+ ["vom ", " bis zum "],
583
+ ["von ", " bis "],
584
+ ["vom ", " bis "],
585
+ ["zwischen ", " und "],
586
+ ["", " bis einschließlich "],
587
+ ["", " bis "],
588
+ ["", " - "],
589
+ ["", " – "],
590
+ ["", " — "],
591
+ ["zwischen ", "-"],
592
+ ["", "-"],
593
+ ["zwischen ", "–"],
594
+ ["", "–"],
595
+ ["zwischen ", "—"],
596
+ ["", "—"],
597
+ ["zwischen ", "~"],
598
+ ["", "~"]
599
+ ];
600
+ const sharedRelativeYearRange = (input) => {
601
+ const match = String$1.match(/^(.+) (letzten|dieses|nächsten) jahres$/u)(input);
602
+ if (Option.isNone(match)) return Option.none();
603
+ const year = `${textAt(match.value, 2)} jahres`;
604
+ return joinedPeriodCandidate(textAt(match.value, 1), boundedJoins, (periodInput) => {
605
+ const inherited = parsePeriod(`${periodInput} ${year}`);
606
+ return Option.isSome(inherited) ? inherited : parsePeriod(periodInput);
607
+ }, (lower, upper) => `von ${lower} bis ${upper}`);
608
+ };
802
609
  const parseGerman = (input) => {
803
610
  const remaining = remainingPeriodPhrases.find((entry) => entry.phrase === input);
804
611
  if (remaining !== void 0) return Option.some(candidate(remainingPeriodRange(remaining.entry.unit), `Rest ${canonicalRelative(remaining.entry.genitive)}`));
805
612
  if (["bis heute", "bis jetzt"].includes(input)) return Option.some(candidate(untilNowRange(), "bis heute"));
806
613
  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
614
  const rolling = parseRollingPeriod(input);
810
615
  if (Option.isSome(rolling)) return rolling;
811
616
  const toDate = toDatePhrases.find((entry) => entry.phrase === input);
@@ -814,34 +619,20 @@ const parseGerman = (input) => {
814
619
  if (Option.isSome(currentBoundary)) return currentBoundary;
815
620
  const elided = parseElidedDateRange(input);
816
621
  if (Option.isSome(elided)) return elided;
622
+ const inheritedYear = sharedRelativeYearRange(input);
623
+ if (Option.isSome(inheritedYear)) return inheritedYear;
817
624
  const nowBounded = joinedNowCandidate(input, [
818
625
  ["von ", " bis heute"],
819
626
  ["vom ", " bis heute"],
820
627
  ["zwischen ", " und heute"]
821
- ], ["von heute bis ", "zwischen heute und "], parsePeriod, (period) => `von ${period} bis heute`, (period) => `von heute bis ${period}`);
628
+ ], [
629
+ "von heute bis ",
630
+ "zwischen heute und ",
631
+ "heute bis ",
632
+ "jetzt bis "
633
+ ], parsePeriod, (period) => `von ${period} bis heute`, (period) => `von heute bis ${period}`);
822
634
  if (Option.isSome(nowBounded)) return nowBounded;
823
- const bounded = joinedPeriodCandidate(input, [
824
- ["von ", " bis einschließlich "],
825
- ["vom ", " bis einschließlich "],
826
- ["von ", " bis zum "],
827
- ["vom ", " bis zum "],
828
- ["von ", " bis "],
829
- ["vom ", " bis "],
830
- ["zwischen ", " und "],
831
- ["", " bis einschließlich "],
832
- ["", " bis "],
833
- ["", " - "],
834
- ["", " – "],
835
- ["", " — "],
836
- ["zwischen ", "-"],
837
- ["", "-"],
838
- ["zwischen ", "–"],
839
- ["", "–"],
840
- ["zwischen ", "—"],
841
- ["", "—"],
842
- ["zwischen ", "~"],
843
- ["", "~"]
844
- ], parsePeriod, (lower, upper) => `von ${lower} bis ${upper}`);
635
+ const bounded = joinedPeriodCandidate(input, boundedJoins, parsePeriod, (lower, upper) => `von ${lower} bis ${upper}`);
845
636
  if (Option.isSome(bounded)) return bounded;
846
637
  const boundary = boundaryCandidate(input);
847
638
  if (Option.isSome(boundary)) return boundary;
@@ -877,9 +668,13 @@ const staticPeriodPhrases = [
877
668
  ]),
878
669
  ...months.flatMap((month) => [
879
670
  month,
671
+ `letzter ${month}`,
672
+ `nächster ${month}`,
880
673
  `${month} letzten jahres`,
881
- `${month} nächsten jahres`
882
- ])
674
+ `${month} nächsten jahres`,
675
+ `${month} vor einem jahr`
676
+ ]),
677
+ ...nextWeekdays.map((entry) => entry.phrase)
883
678
  ];
884
679
  const staticPeriods = periodsFromPhrases(staticPeriodPhrases, parsePeriod);
885
680
  const boundaryPrefixes = [
@@ -916,12 +711,20 @@ const germanSuggestionPhrases = [
916
711
  ]),
917
712
  ...staticPeriodPhrases,
918
713
  ...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
- ]),
714
+ ...unitPhrases.flatMap((entry) => [
715
+ entry.current,
716
+ entry.previous,
717
+ entry.next
718
+ ].flatMap((period) => {
719
+ const dative = withRelativeCase(canonicalRelative(period), "dative");
720
+ const accusative = withRelativeCase(canonicalRelative(period), "accusative");
721
+ return [
722
+ `seit ${dative}`,
723
+ `vor ${dative}`,
724
+ `bis einschließlich ${accusative}`,
725
+ `nach ${dative}`
726
+ ];
727
+ })),
925
728
  ...prefixNaturalPhrases(months, boundaryPrefixes),
926
729
  "bis heute",
927
730
  "ab jetzt"
@@ -936,6 +739,17 @@ const suggestGerman = (input, limit) => {
936
739
  ], limit);
937
740
  };
938
741
  const renderGerman = (range) => {
742
+ const shifted = decomposeShiftedPeriodRange(range);
743
+ if (Option.isSome(shifted)) {
744
+ const base = renderGerman(shifted.value.baseRange);
745
+ const entry = unitPhrases.find((unit) => unit.unit === shifted.value.unit);
746
+ if (Option.isSome(base) && entry !== void 0) {
747
+ const amount = Math.abs(shifted.value.amount);
748
+ const offset = amount === 1 ? canonicalRelative(entry.indefiniteDative) : `${amount} ${title(entry.dative)}`;
749
+ const direction = shifted.value.amount < 0 ? "vor" : "in";
750
+ return Option.some(`${base.value} ${direction} ${offset}`);
751
+ }
752
+ }
939
753
  const offset = calendarPeriodOffset(range);
940
754
  if (Option.isSome(offset) && Math.abs(offset.value.amount) > 1) {
941
755
  const entry = unitPhrases.find((unit) => unit.unit === offset.value.unit);
@@ -957,12 +771,13 @@ const renderGerman = (range) => {
957
771
  ...currentYearDatePeriods(range, currentDateLabel),
958
772
  ...periodsFromPhrases([...dated, ...dated.flatMap((phrase) => [`anfang ${phrase}`, `ende ${phrase}`])], parsePeriod)
959
773
  ];
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");
774
+ 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 ${withRelativeCase(lower, "dative")} bis ${withRelativeCase(upper, "accusative")}`, (period) => `von ${period} bis heute`, (period) => `von heute bis ${period}`, () => "bis heute", () => "ab jetzt");
961
775
  };
962
776
  const GermanContribution = new BaseLanguageContribution({
963
777
  locale: "de",
964
778
  vocabulary: [
965
779
  ...months,
780
+ ...weekdays,
966
781
  ...quarterNames,
967
782
  "q1",
968
783
  "q2",
@@ -972,7 +787,7 @@ const GermanContribution = new BaseLanguageContribution({
972
787
  ...toDatePhrases.flatMap((entry) => entry.phrase.split(" ")),
973
788
  ...remainingPeriodPhrases.flatMap((entry) => entry.phrase.split(" ")),
974
789
  ...currentBoundaryPhrases.flatMap((entry) => entry.phrase.split(" ")),
975
- ...relativeAliases.flatMap((entry) => entry[0].split(" ")),
790
+ ...relativeModifierVocabulary,
976
791
  ...unitPhrases.flatMap((entry) => [
977
792
  entry.noun,
978
793
  entry.plural,
@@ -1010,6 +825,8 @@ const GermanContribution = new BaseLanguageContribution({
1010
825
  "letzten",
1011
826
  "nach",
1012
827
  "nächste",
828
+ "auflaufend",
829
+ "per",
1013
830
  "nächsten",
1014
831
  "seit",
1015
832
  "vor",
@@ -1024,8 +841,8 @@ const GermanContribution = new BaseLanguageContribution({
1024
841
  "zwischen",
1025
842
  "zuvor"
1026
843
  ],
1027
- normalize: normalizeNaturalText,
1028
- correct: correctWhitespaceSeparatedText,
844
+ normalize: normalizeGerman,
845
+ correct: correctGerman,
1029
846
  parseExact: parseGerman,
1030
847
  suggest: suggestGerman,
1031
848
  render: renderGerman