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, 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, periodDay, 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/nl.ts
10
10
  const months = [
@@ -21,6 +21,54 @@ const months = [
21
21
  "november",
22
22
  "december"
23
23
  ];
24
+ const weekdays = [
25
+ "maandag",
26
+ "dinsdag",
27
+ "woensdag",
28
+ "donderdag",
29
+ "vrijdag",
30
+ "zaterdag",
31
+ "zondag"
32
+ ];
33
+ const nextWeekdayPhrases = weekdays.map((weekday) => `volgende ${weekday}`);
34
+ const dutchCountWords = [
35
+ "twee",
36
+ "drie",
37
+ "vier",
38
+ "vijf",
39
+ "zes",
40
+ "zeven",
41
+ "acht",
42
+ "negen",
43
+ "tien",
44
+ "elf",
45
+ "twaalf",
46
+ "dertien",
47
+ "veertien",
48
+ "vijftien",
49
+ "zestien",
50
+ "zeventien",
51
+ "achttien",
52
+ "negentien",
53
+ "twintig"
54
+ ];
55
+ const dutchCountOnes = [[1, "een"], ...dutchCountWords.slice(0, 8).map((word, index) => [index + 2, word])];
56
+ const dutchCountAliases = [...sequentialCountAliases(dutchCountWords.map((word) => [word]), 2), ...compoundCountAliases(decimalTens([
57
+ "twintig",
58
+ "dertig",
59
+ "veertig",
60
+ "vijftig",
61
+ "zestig",
62
+ "zeventig",
63
+ "tachtig",
64
+ "negentig"
65
+ ]), dutchCountOnes, (ten, one) => {
66
+ return [`${one === "twee" || one === "drie" ? `${one}ën` : `${one}en`}${ten}`];
67
+ })];
68
+ const normalizeDutchCounts = compileCountAliasNormalizer(dutchCountAliases);
69
+ const dutchCountVocabulary = new Set(countAliasVocabulary(dutchCountAliases));
70
+ const correctDutch = (input, vocabulary) => correctWhitespaceSeparatedText(input, vocabulary, dutchCountVocabulary);
71
+ const normalizeDutch = (input, locale) => normalizeDutchCounts(normalizeNaturalText(input, locale));
24
72
  const monthAbbreviations = [
25
73
  ["jan"],
26
74
  ["feb"],
@@ -40,6 +88,7 @@ const units = [
40
88
  unit: "day",
41
89
  singular: "dag",
42
90
  plural: "dagen",
91
+ article: "de",
43
92
  current: "vandaag",
44
93
  previous: "gisteren",
45
94
  next: "morgen",
@@ -50,6 +99,7 @@ const units = [
50
99
  unit: "week",
51
100
  singular: "week",
52
101
  plural: "weken",
102
+ article: "de",
53
103
  current: "deze week",
54
104
  previous: "vorige week",
55
105
  next: "volgende week",
@@ -60,6 +110,7 @@ const units = [
60
110
  unit: "month",
61
111
  singular: "maand",
62
112
  plural: "maanden",
113
+ article: "de",
63
114
  current: "deze maand",
64
115
  previous: "vorige maand",
65
116
  next: "volgende maand",
@@ -70,6 +121,7 @@ const units = [
70
121
  unit: "quarter",
71
122
  singular: "kwartaal",
72
123
  plural: "kwartalen",
124
+ article: "het",
73
125
  current: "dit kwartaal",
74
126
  previous: "vorig kwartaal",
75
127
  next: "volgend kwartaal",
@@ -80,6 +132,7 @@ const units = [
80
132
  unit: "year",
81
133
  singular: "jaar",
82
134
  plural: "jaar",
135
+ article: "het",
83
136
  current: "dit jaar",
84
137
  previous: "vorig jaar",
85
138
  next: "volgend jaar",
@@ -87,18 +140,58 @@ const units = [
87
140
  remaining: "rest van het jaar"
88
141
  }
89
142
  ];
90
- const title = (value) => `${value.slice(0, 1).toLocaleUpperCase("nl")}${value.slice(1)}`;
143
+ const afterVan = (period) => period.startsWith("van ") ? period.slice(4) : period;
91
144
  const unitAliases = [
92
- ["dag", "day"],
93
- ["dagen", "day"],
94
- ["week", "week"],
95
- ["weken", "week"],
96
- ["maand", "month"],
97
- ["maanden", "month"],
98
- ["kwartaal", "quarter"],
99
- ["kwartalen", "quarter"],
100
- ["jaar", "year"],
101
- ["jaren", "year"]
145
+ [
146
+ "dag",
147
+ "day",
148
+ "singular"
149
+ ],
150
+ [
151
+ "dagen",
152
+ "day",
153
+ "plural"
154
+ ],
155
+ [
156
+ "week",
157
+ "week",
158
+ "singular"
159
+ ],
160
+ [
161
+ "weken",
162
+ "week",
163
+ "plural"
164
+ ],
165
+ [
166
+ "maand",
167
+ "month",
168
+ "singular"
169
+ ],
170
+ [
171
+ "maanden",
172
+ "month",
173
+ "plural"
174
+ ],
175
+ [
176
+ "kwartaal",
177
+ "quarter",
178
+ "singular"
179
+ ],
180
+ [
181
+ "kwartalen",
182
+ "quarter",
183
+ "plural"
184
+ ],
185
+ [
186
+ "jaar",
187
+ "year",
188
+ "both"
189
+ ],
190
+ [
191
+ "jaren",
192
+ "year",
193
+ "plural"
194
+ ]
102
195
  ];
103
196
  const periodAliases = [
104
197
  ...units.flatMap((entry) => [
@@ -300,17 +393,30 @@ const monthNumber = (value) => {
300
393
  return short === -1 ? void 0 : short + 1;
301
394
  };
302
395
  const dateLabel = (day, month, year) => `${day} ${textAt(months, month - 1)} ${year}`;
396
+ const currentDateLabel = (day, month) => `${day} ${textAt(months, month - 1)}`;
303
397
  const parseNamedDate = (input) => {
304
398
  const named = String$1.match(/^(?:de )?([0-3]?\d)(?:e|ste|de)?(?: van)? ([a-z]+\.?)(?: van)? (\d{4})$/u)(input);
305
399
  if (Option.isSome(named)) return namedDatePeriod(textAt(named.value, 3), textAt(named.value, 2), textAt(named.value, 1), monthNumber, dateLabel);
306
400
  const numeric = String$1.match(/^([0-3]?\d)[./-]([01]?\d)[./-](\d{4})$/u)(input);
307
- if (Option.isNone(numeric)) return Option.none();
308
- const year = validYear(textAt(numeric.value, 3));
309
- const month = Number(textAt(numeric.value, 2));
310
- const day = Number(textAt(numeric.value, 1));
311
- if (year === void 0 || month < 1 || month > 12) return Option.none();
312
- const value = isoDate(year, month, day);
313
- return isIsoDate(value) && value !== "9999-12-31" ? Option.some(fixedDatePeriod(value, dateLabel(day, month, year))) : Option.none();
401
+ if (Option.isSome(numeric)) {
402
+ const year = validYear(textAt(numeric.value, 3));
403
+ const month = Number(textAt(numeric.value, 2));
404
+ const day = Number(textAt(numeric.value, 1));
405
+ if (year !== void 0 && month >= 1 && month <= 12) {
406
+ const value = isoDate(year, month, day);
407
+ if (isIsoDate(value) && value !== "9999-12-31") return Option.some(fixedDatePeriod(value, dateLabel(day, month, year)));
408
+ }
409
+ }
410
+ const current = String$1.match(/^(?:de )?([0-3]?\d)(?:e|ste|de)?(?: van)? ([a-z]+\.?)$/u)(input);
411
+ if (Option.isSome(current)) return namedCurrentYearDatePeriod(textAt(current.value, 2), textAt(current.value, 1), monthNumber, currentDateLabel);
412
+ const relative = String$1.match(/^(?:de )?([0-3]?\d)(?:e|ste|de)? van (.+)$/u)(input);
413
+ if (Option.isNone(relative)) return Option.none();
414
+ const periodText = textAt(relative.value, 2);
415
+ const alias = periodAliases.find((entry) => entry[0] === periodText && entry[1] === "month");
416
+ if (alias === void 0) return Option.none();
417
+ const day = Number(textAt(relative.value, 1));
418
+ const month = relativePeriod(alias[1], alias[2], alias[3]);
419
+ return periodDay(month, day, `${day} van ${alias[3]}`);
314
420
  };
315
421
  const quarterNumber = (value) => {
316
422
  if ((value.startsWith("q") || value.startsWith("k")) && value.length === 2) return Number(value.slice(1));
@@ -337,12 +443,13 @@ const parseQuarter = (input) => {
337
443
  const quarter = quarterNumber(textAt(standalone.value, 1));
338
444
  return quarter === void 0 ? Option.none() : Option.some(quarterOfRelativeYear(quarter, 0, `K${quarter}`));
339
445
  };
340
- const parseBasePeriod = (input) => {
341
- if (isIsoDate(input) && input !== "9999-12-31") return Option.some(fixedDatePeriod(input, input));
342
- const namedDate = parseNamedDate(input);
343
- if (Option.isSome(namedDate)) return namedDate;
344
- const quarter = parseQuarter(input);
345
- if (Option.isSome(quarter)) return quarter;
446
+ const parseDatedPeriod = (input) => {
447
+ const knownPeriod = Option.firstSomeOf([
448
+ absoluteDatePeriod(input, "nl"),
449
+ parseNamedDate(input),
450
+ parseQuarter(input)
451
+ ]);
452
+ if (Option.isSome(knownPeriod)) return knownPeriod;
346
453
  const yearMatch = String$1.match(/^(?:het jaar |jaar )?(\d{4})$/u)(input);
347
454
  if (Option.isSome(yearMatch)) {
348
455
  const year = validYear(textAt(yearMatch.value, 1));
@@ -352,7 +459,21 @@ const parseBasePeriod = (input) => {
352
459
  if (Option.isSome(monthYear)) {
353
460
  const month = monthNumber(textAt(monthYear.value, 1));
354
461
  const year = validYear(textAt(monthYear.value, 2));
355
- if (month !== void 0 && year !== void 0) return Option.some(fixedMonthPeriod(year, month, `${title(textAt(months, month - 1))} ${year}`));
462
+ if (month !== void 0 && year !== void 0) return Option.some(fixedMonthPeriod(year, month, `${textAt(months, month - 1)} ${year}`));
463
+ }
464
+ return Option.none();
465
+ };
466
+ const parseBasePeriod = (input) => {
467
+ const datedPeriod = parseDatedPeriod(input);
468
+ if (Option.isSome(datedPeriod)) return datedPeriod;
469
+ const prefixedRelativeMonth = String$1.match(/^(vorige|deze|volgende) ([a-z]+\.?)$/u)(input);
470
+ if (Option.isSome(prefixedRelativeMonth)) {
471
+ const month = monthNumber(textAt(prefixedRelativeMonth.value, 2));
472
+ if (month !== void 0) {
473
+ const modifier = textAt(prefixedRelativeMonth.value, 1);
474
+ const direction = relativeYearDirection(modifier);
475
+ return Option.some(monthOfRelativeYear(month, direction, `${modifier} ${textAt(months, month - 1)}`));
476
+ }
356
477
  }
357
478
  const relativeMonth = String$1.match(/^([a-z]+\.?)(?: van)? (vorig jaar|volgend jaar|dit jaar)$/u)(input);
358
479
  const relativeMonthYearFirst = String$1.match(/^(vorig jaar|volgend jaar|dit jaar) ([a-z]+\.?)$/u)(input);
@@ -363,10 +484,10 @@ const parseBasePeriod = (input) => {
363
484
  const yearText = textAt(relativeMatch.value, yearFirst ? 1 : 2);
364
485
  const month = monthNumber(monthText);
365
486
  const direction = relativeYearDirection(yearText);
366
- if (month !== void 0) return Option.some(monthOfRelativeYear(month, direction, `${title(textAt(months, month - 1))} ${relativeYearName(direction)}`));
487
+ if (month !== void 0) return Option.some(monthOfRelativeYear(month, direction, `${textAt(months, month - 1)} ${relativeYearName(direction)}`));
367
488
  }
368
489
  const standaloneMonth = monthNumber(input);
369
- if (standaloneMonth !== void 0) return Option.some(monthOfRelativeYear(standaloneMonth, 0, title(textAt(months, standaloneMonth - 1))));
490
+ if (standaloneMonth !== void 0) return Option.some(monthOfRelativeYear(standaloneMonth, 0, textAt(months, standaloneMonth - 1)));
370
491
  const alias = periodAliases.find((entry) => entry[0] === input);
371
492
  if (alias !== void 0) return Option.some(relativePeriod(alias[1], alias[2], alias[3]));
372
493
  if ([
@@ -386,13 +507,33 @@ const parseBasePeriod = (input) => {
386
507
  ].includes(input)) return Option.some(relativeWeekend(1, "volgend weekend"));
387
508
  if (input === "het weekend voor het vorige") return Option.some(relativeWeekend(-2, input));
388
509
  if (input === "het weekend na het volgende") return Option.some(relativeWeekend(2, input));
389
- return Option.none();
510
+ const weekday = nextWeekdayPhrases.indexOf(input);
511
+ return weekday === -1 ? Option.none() : Option.some(relativeWeekday(weekday, 1, nextWeekdayPhrases[weekday] ?? input));
390
512
  };
391
513
  const parsePeriod = (input) => {
514
+ const shifted = String$1.match(/^(.+) (?:(?:over|binnen) ([1-9]\d*) (dag|dagen|week|weken|maand|maanden|kwartaal|kwartalen|jaar|jaren)|([1-9]\d*) (dag|dagen|week|weken|maand|maanden|kwartaal|kwartalen|jaar|jaren) (geleden|later))$/u)(input);
515
+ if (Option.isSome(shifted)) {
516
+ const suffixAmount = textAt(shifted.value, 4);
517
+ const amount = parseTrailingCount(suffixAmount || textAt(shifted.value, 2));
518
+ const unitText = textAt(shifted.value, suffixAmount.length > 0 ? 5 : 3);
519
+ const alias = unitAliases.find((unit) => unit[0] === unitText);
520
+ const entry = alias === void 0 ? void 0 : units.find((unit) => unit.unit === alias[1]);
521
+ const period = parsePeriod(textAt(shifted.value, 1));
522
+ if (Option.isSome(amount) && entry !== void 0 && Option.isSome(period)) {
523
+ const past = textAt(shifted.value, 6) === "geleden";
524
+ const direction = past ? -amount.value : amount.value;
525
+ const noun = amount.value === 1 ? entry.singular : entry.plural;
526
+ const canonical = past ? `${period.value.canonical} ${amount.value} ${noun} geleden` : `${period.value.canonical} over ${amount.value} ${noun}`;
527
+ return Option.some(shiftPeriod(period.value, direction, entry.unit, canonical));
528
+ }
529
+ }
392
530
  const edge = String$1.match(/^(?:het )?(begin|eind|einde)(?: van)? (.+)$/u)(input);
393
531
  if (Option.isSome(edge)) {
394
532
  const edgeName = textAt(edge.value, 1);
395
- const period = parseBasePeriod(textAt(edge.value, 2));
533
+ const periodText = textAt(edge.value, 2);
534
+ const basePeriod = parseBasePeriod(periodText);
535
+ const implicit = units.find((entry) => `${entry.article} ${entry.singular}` === periodText);
536
+ const period = Option.isSome(basePeriod) || implicit === void 0 ? basePeriod : Option.some(relativePeriod(implicit.unit, 0, implicit.current));
396
537
  if (Option.isSome(period)) {
397
538
  const isEnd = edgeName === "eind" || edgeName === "einde";
398
539
  const canonical = `${isEnd ? "eind" : "begin"} van ${period.value.canonical}`;
@@ -406,34 +547,90 @@ const parsePeriod = (input) => {
406
547
  "de hele ",
407
548
  "het hele "
408
549
  ].find((prefix) => input.startsWith(prefix));
409
- return parseBasePeriod(wrapper === void 0 ? input : input.slice(wrapper.length));
550
+ const base = parseBasePeriod(wrapper === void 0 ? input : input.slice(wrapper.length));
551
+ return Option.isSome(base) ? base : parseCalendarOffset(input);
552
+ };
553
+ const countedUnit = (value, amount) => {
554
+ const expected = amount === 1 ? "singular" : "plural";
555
+ const alias = unitAliases.find((entry) => entry[0] === value && (entry[2] === expected || entry[2] === "both"));
556
+ return alias === void 0 ? void 0 : units.find((entry) => entry.unit === alias[1]);
410
557
  };
411
- const countedUnit = (value) => unitAliases.find((entry) => entry[0] === value)?.[1];
412
558
  const countedUnitPattern = "dag|dagen|week|weken|maand|maanden|kwartaal|kwartalen|jaar|jaren";
413
559
  const countedPattern = (source) => new RegExp(source.replace("UNIT", countedUnitPattern), "u");
414
560
  const calendarPastPattern = countedPattern("^([1-9]\\d*) (UNIT) geleden$");
415
561
  const calendarFuturePatterns = [countedPattern("^(?:over|binnen) ([1-9]\\d*) (UNIT)$"), countedPattern("^([1-9]\\d*) (UNIT) later$")];
416
- const rollingPastPatterns = [countedPattern("^(?:de )?(?:afgelopen|laatste|vorige) ([1-9]\\d*) (UNIT)$"), countedPattern("^([1-9]\\d*) (?:afgelopen|laatste|vorige) (UNIT)$")];
417
- const rollingFuturePatterns = [countedPattern("^(?:de )?(?:komende|volgende|aankomende) ([1-9]\\d*) (UNIT)$"), countedPattern("^([1-9]\\d*) (?:komende|volgende|aankomende) (UNIT)$")];
562
+ const rollingPastPatterns = [countedPattern("^(?:(?:de|in de) )?(?:afgelopen|laatste|vorige) ([1-9]\\d*) (UNIT)$"), countedPattern("^([1-9]\\d*) (?:afgelopen|laatste|vorige) (UNIT)$")];
563
+ const rollingFuturePatterns = [countedPattern("^(?:(?:de|in de|binnen de) )?(?:komende|volgende|aankomende) ([1-9]\\d*) (UNIT)$"), countedPattern("^([1-9]\\d*) (?:komende|volgende|aankomende) (UNIT)$")];
418
564
  const rollingSincePattern = countedPattern("^(?:sinds|gedurende) ([1-9]\\d*) (UNIT)$");
419
565
  const rollingBarePattern = countedPattern("^([1-9]\\d*) (UNIT)$");
420
566
  const firstPatternMatch = (input, patterns) => Option.firstSomeOf(patterns.map((pattern) => String$1.match(pattern)(input)));
567
+ const singularRollingCanonical = (entry, future) => future ? `vanaf nu gedurende een ${entry.singular}` : `sinds een ${entry.singular}`;
568
+ const singularRollingPhrases = units.flatMap((entry) => [
569
+ {
570
+ phrase: `${entry.article} afgelopen ${entry.singular}`,
571
+ entry,
572
+ future: false
573
+ },
574
+ {
575
+ phrase: `${entry.article} laatste ${entry.singular}`,
576
+ entry,
577
+ future: false
578
+ },
579
+ {
580
+ phrase: `sinds een ${entry.singular}`,
581
+ entry,
582
+ future: false
583
+ },
584
+ {
585
+ phrase: `gedurende ${entry.article} komende ${entry.singular}`,
586
+ entry,
587
+ future: true
588
+ },
589
+ {
590
+ phrase: `vanaf nu gedurende een ${entry.singular}`,
591
+ entry,
592
+ future: true
593
+ }
594
+ ]);
595
+ const singularCalendarOffsets = units.flatMap((entry) => [
596
+ {
597
+ phrase: `een ${entry.singular} geleden`,
598
+ entry,
599
+ direction: -1
600
+ },
601
+ {
602
+ phrase: `over een ${entry.singular}`,
603
+ entry,
604
+ direction: 1
605
+ },
606
+ {
607
+ phrase: `binnen een ${entry.singular}`,
608
+ entry,
609
+ direction: 1
610
+ }
611
+ ]);
421
612
  const parseCalendarOffset = (input) => {
613
+ const singular = singularCalendarOffsets.find((entry) => entry.phrase === input);
614
+ if (singular !== void 0) return Option.some(relativePeriod(singular.entry.unit, singular.direction, singular.phrase));
422
615
  const past = String$1.match(calendarPastPattern)(input);
423
616
  const future = firstPatternMatch(input, calendarFuturePatterns);
424
617
  const match = Option.firstSomeOf([past, future]);
425
618
  if (Option.isNone(match)) return Option.none();
426
619
  const amount = parseTrailingCount(textAt(match.value, 1));
427
- const unit = countedUnit(textAt(match.value, 2));
428
- if (Option.isNone(amount) || unit === void 0) return Option.none();
429
- const entry = units.find((item) => item.unit === unit);
620
+ if (Option.isNone(amount)) return Option.none();
621
+ const entry = countedUnit(textAt(match.value, 2), amount.value);
430
622
  if (entry === void 0) return Option.none();
431
623
  const direction = Option.isSome(past) ? -amount.value : amount.value;
432
624
  const noun = amount.value === 1 ? entry.singular : entry.plural;
433
625
  const canonical = direction < 0 ? `${amount.value} ${noun} geleden` : `over ${amount.value} ${noun}`;
434
- return Option.some(candidate(periodRange(relativePeriod(unit, direction, canonical)), canonical));
626
+ return Option.some(relativePeriod(entry.unit, direction, canonical));
435
627
  };
436
628
  const parseRollingPeriod = (input) => {
629
+ const singular = singularRollingPhrases.find((entry) => entry.phrase === input);
630
+ if (singular !== void 0) {
631
+ const range = singular.future ? futureRange(1, singular.entry.unit) : trailingRange(1, singular.entry.unit);
632
+ return Option.some(candidate(range, singularRollingCanonical(singular.entry, singular.future)));
633
+ }
437
634
  const since = String$1.match(rollingSincePattern)(input);
438
635
  const past = firstPatternMatch(input, rollingPastPatterns);
439
636
  const bare = String$1.match(rollingBarePattern)(input);
@@ -446,15 +643,26 @@ const parseRollingPeriod = (input) => {
446
643
  ]);
447
644
  if (Option.isNone(match)) return Option.none();
448
645
  const amount = parseTrailingCount(textAt(match.value, 1));
449
- const unit = countedUnit(textAt(match.value, 2));
450
- if (Option.isNone(amount) || unit === void 0) return Option.none();
451
- const entry = units.find((item) => item.unit === unit);
646
+ if (Option.isNone(amount)) return Option.none();
647
+ const entry = countedUnit(textAt(match.value, 2), amount.value);
452
648
  if (entry === void 0) return Option.none();
453
649
  const isFuture = Option.isSome(future);
454
- const range = isFuture ? futureRange(amount.value, unit) : trailingRange(amount.value, unit);
650
+ const range = isFuture ? futureRange(amount.value, entry.unit) : trailingRange(amount.value, entry.unit);
651
+ if (amount.value === 1) return Option.some(candidate(range, singularRollingCanonical(entry, isFuture)));
455
652
  const modifier = isFuture ? "komende" : "afgelopen";
456
- const noun = amount.value === 1 ? entry.singular : entry.plural;
457
- return Option.some(candidate(range, `de ${modifier} ${amount.value} ${noun}`));
653
+ return Option.some(candidate(range, `de ${modifier} ${amount.value} ${entry.plural}`));
654
+ };
655
+ const parseElidedDateRange = (input) => {
656
+ const joined = String$1.match(/^(?:van ([0-3]?\d) tot(?: en met)?|tussen (?:de )?([0-3]?\d)(?:e)? en(?: de)?) ([0-3]?\d)(?:e)? (.+)$/u)(input);
657
+ const dashed = String$1.match(/^([0-3]?\d)[–—-]([0-3]?\d) (.+)$/u)(input);
658
+ const match = Option.firstSomeOf([joined, dashed]);
659
+ if (Option.isNone(match)) return Option.none();
660
+ const isJoined = Option.isSome(joined);
661
+ const lowerDay = textAt(match.value, 1) || textAt(match.value, 2);
662
+ const upperDay = textAt(match.value, isJoined ? 3 : 2);
663
+ const period = afterVan(textAt(match.value, isJoined ? 4 : 3));
664
+ const suffix = periodAliases.some((entry) => entry[0] === period && entry[1] === "month") ? `van ${period}` : period;
665
+ return joinedPeriodCandidate(`van ${lowerDay} ${suffix} tot en met ${upperDay} ${suffix}`, [["van ", " tot en met "]], parsePeriod, (lower, upper) => `van ${lower} tot en met ${upper}`);
458
666
  };
459
667
  const boundaryCandidate = (input) => openBoundaryCandidate(input, [
460
668
  ["tot voor ", "before"],
@@ -479,12 +687,12 @@ const parseDutch = (input) => {
479
687
  "sinds nu",
480
688
  "voortaan"
481
689
  ].includes(input)) return Option.some(candidate(fromNowRange(), "vanaf nu"));
482
- const offset = parseCalendarOffset(input);
483
- if (Option.isSome(offset)) return offset;
484
690
  const rolling = parseRollingPeriod(input);
485
691
  if (Option.isSome(rolling)) return rolling;
486
692
  const toDate = toDatePhrases.find((entry) => entry.phrase === input);
487
693
  if (toDate !== void 0) return Option.some(candidate(periodToDateRange(toDate.entry.unit), toDate.entry.toDate));
694
+ const elided = parseElidedDateRange(input);
695
+ if (Option.isSome(elided)) return elided;
488
696
  const nowBounded = joinedNowCandidate(input, [
489
697
  ["vanaf ", " tot nu toe"],
490
698
  ["sinds ", " tot nu toe"],
@@ -492,7 +700,8 @@ const parseDutch = (input) => {
492
700
  ], [
493
701
  "vanaf nu tot en met ",
494
702
  "van vandaag tot en met ",
495
- "tussen vandaag en "
703
+ "tussen vandaag en ",
704
+ "nu tot en met "
496
705
  ], parsePeriod, (period) => `vanaf ${period} tot nu toe`, (period) => `vanaf nu tot en met ${period}`);
497
706
  if (Option.isSome(nowBounded)) return nowBounded;
498
707
  const bounded = joinedPeriodCandidate(input, [
@@ -508,16 +717,28 @@ const parseDutch = (input) => {
508
717
  if (Option.isSome(bounded)) return bounded;
509
718
  const boundary = boundaryCandidate(input);
510
719
  if (Option.isSome(boundary)) return boundary;
511
- return Option.map(parsePeriod(input), (period) => candidate(periodRange(period), period.canonical));
720
+ return parsePeriod(input).pipe(Option.map((period) => candidate(periodRange(period), period.canonical)));
512
721
  };
513
- const staticPeriodPhrases = [
514
- ...periodAliases.map((entry) => entry[0]),
722
+ const weekendPhrases = [
515
723
  "dit weekend",
516
724
  "vorig weekend",
517
725
  "volgend weekend",
518
726
  "het weekend voor het vorige",
519
- "het weekend na het volgende",
520
- ...periodAliases.flatMap((entry) => [`begin van ${entry[0]}`, `eind van ${entry[0]}`]),
727
+ "het weekend na het volgende"
728
+ ];
729
+ const edgePeriodPhrases = [
730
+ ...units.filter((entry) => entry.unit !== "day").flatMap((entry) => [
731
+ entry.current,
732
+ entry.previous,
733
+ entry.next
734
+ ]),
735
+ ...weekendPhrases,
736
+ ...months
737
+ ].flatMap((period) => [`begin van ${period}`, `eind van ${period}`]);
738
+ const staticPeriodPhrases = [
739
+ ...periodAliases.map((entry) => entry[0]),
740
+ ...weekendPhrases,
741
+ ...edgePeriodPhrases,
521
742
  ...[
522
743
  1,
523
744
  2,
@@ -531,9 +752,12 @@ const staticPeriodPhrases = [
531
752
  ]),
532
753
  ...months.flatMap((month) => [
533
754
  month,
755
+ `vorige ${month}`,
756
+ `volgende ${month}`,
534
757
  `${month} vorig jaar`,
535
758
  `${month} volgend jaar`
536
- ])
759
+ ]),
760
+ ...nextWeekdayPhrases
537
761
  ];
538
762
  const staticPeriods = periodsFromPhrases(staticPeriodPhrases, parsePeriod);
539
763
  const boundaryPrefixes = [
@@ -563,6 +787,8 @@ const countedSuggestions = (input) => {
563
787
  const dutchSuggestionPhrases = [
564
788
  ...units.map((entry) => entry.toDate),
565
789
  ...units.map((entry) => entry.remaining),
790
+ ...singularRollingPhrases.map((entry) => entry.phrase),
791
+ ...singularCalendarOffsets.map((entry) => entry.phrase),
566
792
  ...staticPeriodPhrases,
567
793
  ...prefixNaturalPhrases(staticPeriodPhrases, boundaryPrefixes),
568
794
  "tot nu toe",
@@ -578,6 +804,16 @@ const suggestDutch = (input, limit) => {
578
804
  ], limit);
579
805
  };
580
806
  const renderDutch = (range) => {
807
+ const shifted = decomposeShiftedPeriodRange(range);
808
+ if (Option.isSome(shifted)) {
809
+ const base = renderDutch(shifted.value.baseRange);
810
+ const entry = units.find((unit) => unit.unit === shifted.value.unit);
811
+ if (Option.isSome(base) && entry !== void 0) {
812
+ const amount = Math.abs(shifted.value.amount);
813
+ const noun = amount === 1 ? entry.singular : entry.plural;
814
+ return Option.some(shifted.value.amount < 0 ? `${base.value} ${amount} ${noun} geleden` : `${base.value} over ${amount} ${noun}`);
815
+ }
816
+ }
581
817
  const offset = calendarPeriodOffset(range);
582
818
  if (Option.isSome(offset) && Math.abs(offset.value.amount) > 1) {
583
819
  const entry = units.find((unit) => unit.unit === offset.value.unit);
@@ -590,26 +826,25 @@ const renderDutch = (range) => {
590
826
  const future = futurePeriod(range);
591
827
  if (Option.isSome(future)) {
592
828
  const entry = units.find((unit) => unit.unit === future.value.unit);
593
- if (entry !== void 0) {
594
- const noun = future.value.amount === 1 ? entry.singular : entry.plural;
595
- return Option.some(`de komende ${future.value.amount} ${noun}`);
596
- }
829
+ if (entry !== void 0) return Option.some(future.value.amount === 1 ? singularRollingCanonical(entry, true) : `de komende ${future.value.amount} ${entry.plural}`);
597
830
  }
598
831
  const trailing = trailingPeriod(range);
599
832
  if (Option.isSome(trailing)) {
600
833
  const entry = units.find((unit) => unit.unit === trailing.value.unit);
601
- if (entry !== void 0) {
602
- const noun = trailing.value.amount === 1 ? entry.singular : entry.plural;
603
- return Option.some(`de afgelopen ${trailing.value.amount} ${noun}`);
604
- }
834
+ if (entry !== void 0) return Option.some(trailing.value.amount === 1 ? singularRollingCanonical(entry, false) : `de afgelopen ${trailing.value.amount} ${entry.plural}`);
605
835
  }
606
- const periods = [...staticPeriods, ...periodsFromPhrases([...datedPeriods(range, months), ...datedQuarterPeriods(range).map((phrase) => phrase.startsWith("Q") ? `K${phrase.slice(1)}` : phrase)], parsePeriod)];
836
+ const periods = [
837
+ ...staticPeriods,
838
+ ...currentYearDatePeriods(range, currentDateLabel),
839
+ ...periodsFromPhrases([...datedPeriods(range, months), ...datedQuarterPeriods(range).map((phrase) => phrase.startsWith("Q") ? `K${phrase.slice(1)}` : phrase)], parsePeriod)
840
+ ];
607
841
  return renderPeriodRange(range, [...units.map((entry) => candidate(periodToDateRange(entry.unit), entry.toDate)), ...units.map((entry) => candidate(remainingPeriodRange(entry.unit), entry.remaining))], periods, (period) => `vanaf ${period}`, (period) => `voor ${period}`, (period) => `tot en met ${period}`, (period) => `na ${period}`, (lower, upper) => `van ${lower} tot en met ${upper}`, (period) => `vanaf ${period} tot nu toe`, (period) => `vanaf nu tot en met ${period}`, () => "tot nu toe", () => "vanaf nu");
608
842
  };
609
843
  const DutchContribution = new BaseLanguageContribution({
610
844
  locale: "nl",
611
845
  vocabulary: [
612
846
  ...months,
847
+ ...weekdays,
613
848
  ...monthAbbreviations.flatMap((aliases) => aliases),
614
849
  ...units.flatMap((entry) => [
615
850
  entry.singular,
@@ -624,10 +859,13 @@ const DutchContribution = new BaseLanguageContribution({
624
859
  "begin",
625
860
  "binnen",
626
861
  "eind",
862
+ "en",
627
863
  "geleden",
628
864
  "komende",
629
865
  "laatste",
866
+ "met",
630
867
  "na",
868
+ "nu",
631
869
  "over",
632
870
  "rest",
633
871
  "sinds",
@@ -637,8 +875,8 @@ const DutchContribution = new BaseLanguageContribution({
637
875
  "volgend",
638
876
  "voor"
639
877
  ],
640
- normalize: normalizeNaturalText,
641
- correct: correctWhitespaceSeparatedText,
878
+ normalize: normalizeDutch,
879
+ correct: correctDutch,
642
880
  parseExact: parseDutch,
643
881
  suggest: suggestDutch,
644
882
  render: renderDutch