chronolizer 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,7 +4,7 @@ import { normalizeNaturalText } from "../natural/text.mjs";
4
4
  import { defineLanguagePlugin, languagePluginsLayer } from "../language/registry.mjs";
5
5
  import { correctWhitespaceSeparatedText } from "../natural/correction.mjs";
6
6
  import { completeNaturalPhrases, fixedCalendarPeriodPhrases, naturalCount, prefixNaturalPhrases } from "../natural/suggestion.mjs";
7
- import { calendarPeriodOffset, candidate, datedPeriods, datedQuarterPeriods, fixedDatePeriod, fixedMonthPeriod, fixedQuarterPeriod, fixedYearPeriod, fromNowRange, futurePeriod, futureRange, isoDate, joinedNowCandidate, joinedPeriodCandidate, monthOfRelativeYear, namedDatePeriod, openBoundaryCandidate, parseTrailingCount, periodEndDay, periodRange, periodStartDay, periodToDateRange, periodsFromPhrases, quarterOfRelativeYear, relativePeriod, relativeWeekend, remainingPeriodRange, renderPeriodRange, textAt, trailingPeriod, trailingRange, untilNowRange, validYear } from "./shared.mjs";
7
+ import { absoluteDatePeriod, calendarPeriodOffset, candidate, currentYearDatePeriods, datedPeriods, datedQuarterPeriods, fixedDatePeriod, fixedMonthPeriod, fixedQuarterPeriod, fixedYearPeriod, fromNowRange, futurePeriod, futureRange, isoDate, joinedNowCandidate, joinedPeriodCandidate, monthOfRelativeYear, namedCurrentYearDatePeriod, namedDatePeriod, openBoundaryCandidate, parseTrailingCount, periodDay, periodEndDay, periodRange, periodStartDay, periodToDateRange, periodsFromPhrases, quarterOfRelativeYear, relativePeriod, relativeWeekend, remainingPeriodRange, renderPeriodRange, textAt, trailingPeriod, trailingRange, untilNowRange, validYear } from "./shared.mjs";
8
8
  import { Effect, Option, String as String$1 } from "effect";
9
9
  //#region src/locales/nl.ts
10
10
  const months = [
@@ -40,6 +40,7 @@ const units = [
40
40
  unit: "day",
41
41
  singular: "dag",
42
42
  plural: "dagen",
43
+ article: "de",
43
44
  current: "vandaag",
44
45
  previous: "gisteren",
45
46
  next: "morgen",
@@ -50,6 +51,7 @@ const units = [
50
51
  unit: "week",
51
52
  singular: "week",
52
53
  plural: "weken",
54
+ article: "de",
53
55
  current: "deze week",
54
56
  previous: "vorige week",
55
57
  next: "volgende week",
@@ -60,6 +62,7 @@ const units = [
60
62
  unit: "month",
61
63
  singular: "maand",
62
64
  plural: "maanden",
65
+ article: "de",
63
66
  current: "deze maand",
64
67
  previous: "vorige maand",
65
68
  next: "volgende maand",
@@ -70,6 +73,7 @@ const units = [
70
73
  unit: "quarter",
71
74
  singular: "kwartaal",
72
75
  plural: "kwartalen",
76
+ article: "het",
73
77
  current: "dit kwartaal",
74
78
  previous: "vorig kwartaal",
75
79
  next: "volgend kwartaal",
@@ -80,6 +84,7 @@ const units = [
80
84
  unit: "year",
81
85
  singular: "jaar",
82
86
  plural: "jaar",
87
+ article: "het",
83
88
  current: "dit jaar",
84
89
  previous: "vorig jaar",
85
90
  next: "volgend jaar",
@@ -87,18 +92,58 @@ const units = [
87
92
  remaining: "rest van het jaar"
88
93
  }
89
94
  ];
90
- const title = (value) => `${value.slice(0, 1).toLocaleUpperCase("nl")}${value.slice(1)}`;
95
+ const afterVan = (period) => period.startsWith("van ") ? period.slice(4) : period;
91
96
  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"]
97
+ [
98
+ "dag",
99
+ "day",
100
+ "singular"
101
+ ],
102
+ [
103
+ "dagen",
104
+ "day",
105
+ "plural"
106
+ ],
107
+ [
108
+ "week",
109
+ "week",
110
+ "singular"
111
+ ],
112
+ [
113
+ "weken",
114
+ "week",
115
+ "plural"
116
+ ],
117
+ [
118
+ "maand",
119
+ "month",
120
+ "singular"
121
+ ],
122
+ [
123
+ "maanden",
124
+ "month",
125
+ "plural"
126
+ ],
127
+ [
128
+ "kwartaal",
129
+ "quarter",
130
+ "singular"
131
+ ],
132
+ [
133
+ "kwartalen",
134
+ "quarter",
135
+ "plural"
136
+ ],
137
+ [
138
+ "jaar",
139
+ "year",
140
+ "both"
141
+ ],
142
+ [
143
+ "jaren",
144
+ "year",
145
+ "plural"
146
+ ]
102
147
  ];
103
148
  const periodAliases = [
104
149
  ...units.flatMap((entry) => [
@@ -300,17 +345,30 @@ const monthNumber = (value) => {
300
345
  return short === -1 ? void 0 : short + 1;
301
346
  };
302
347
  const dateLabel = (day, month, year) => `${day} ${textAt(months, month - 1)} ${year}`;
348
+ const currentDateLabel = (day, month) => `${day} ${textAt(months, month - 1)}`;
303
349
  const parseNamedDate = (input) => {
304
350
  const named = String$1.match(/^(?:de )?([0-3]?\d)(?:e|ste|de)?(?: van)? ([a-z]+\.?)(?: van)? (\d{4})$/u)(input);
305
351
  if (Option.isSome(named)) return namedDatePeriod(textAt(named.value, 3), textAt(named.value, 2), textAt(named.value, 1), monthNumber, dateLabel);
306
352
  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();
353
+ if (Option.isSome(numeric)) {
354
+ const year = validYear(textAt(numeric.value, 3));
355
+ const month = Number(textAt(numeric.value, 2));
356
+ const day = Number(textAt(numeric.value, 1));
357
+ if (year !== void 0 && month >= 1 && month <= 12) {
358
+ const value = isoDate(year, month, day);
359
+ if (isIsoDate(value) && value !== "9999-12-31") return Option.some(fixedDatePeriod(value, dateLabel(day, month, year)));
360
+ }
361
+ }
362
+ const current = String$1.match(/^(?:de )?([0-3]?\d)(?:e|ste|de)?(?: van)? ([a-z]+\.?)$/u)(input);
363
+ if (Option.isSome(current)) return namedCurrentYearDatePeriod(textAt(current.value, 2), textAt(current.value, 1), monthNumber, currentDateLabel);
364
+ const relative = String$1.match(/^(?:de )?([0-3]?\d)(?:e|ste|de)? van (.+)$/u)(input);
365
+ if (Option.isNone(relative)) return Option.none();
366
+ const periodText = textAt(relative.value, 2);
367
+ const alias = periodAliases.find((entry) => entry[0] === periodText && entry[1] === "month");
368
+ if (alias === void 0) return Option.none();
369
+ const day = Number(textAt(relative.value, 1));
370
+ const month = relativePeriod(alias[1], alias[2], alias[3]);
371
+ return periodDay(month, day, `${day} van ${alias[3]}`);
314
372
  };
315
373
  const quarterNumber = (value) => {
316
374
  if ((value.startsWith("q") || value.startsWith("k")) && value.length === 2) return Number(value.slice(1));
@@ -338,7 +396,8 @@ const parseQuarter = (input) => {
338
396
  return quarter === void 0 ? Option.none() : Option.some(quarterOfRelativeYear(quarter, 0, `K${quarter}`));
339
397
  };
340
398
  const parseBasePeriod = (input) => {
341
- if (isIsoDate(input) && input !== "9999-12-31") return Option.some(fixedDatePeriod(input, input));
399
+ const absoluteDate = absoluteDatePeriod(input, "nl");
400
+ if (Option.isSome(absoluteDate)) return absoluteDate;
342
401
  const namedDate = parseNamedDate(input);
343
402
  if (Option.isSome(namedDate)) return namedDate;
344
403
  const quarter = parseQuarter(input);
@@ -352,7 +411,7 @@ const parseBasePeriod = (input) => {
352
411
  if (Option.isSome(monthYear)) {
353
412
  const month = monthNumber(textAt(monthYear.value, 1));
354
413
  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}`));
414
+ if (month !== void 0 && year !== void 0) return Option.some(fixedMonthPeriod(year, month, `${textAt(months, month - 1)} ${year}`));
356
415
  }
357
416
  const relativeMonth = String$1.match(/^([a-z]+\.?)(?: van)? (vorig jaar|volgend jaar|dit jaar)$/u)(input);
358
417
  const relativeMonthYearFirst = String$1.match(/^(vorig jaar|volgend jaar|dit jaar) ([a-z]+\.?)$/u)(input);
@@ -363,10 +422,10 @@ const parseBasePeriod = (input) => {
363
422
  const yearText = textAt(relativeMatch.value, yearFirst ? 1 : 2);
364
423
  const month = monthNumber(monthText);
365
424
  const direction = relativeYearDirection(yearText);
366
- if (month !== void 0) return Option.some(monthOfRelativeYear(month, direction, `${title(textAt(months, month - 1))} ${relativeYearName(direction)}`));
425
+ if (month !== void 0) return Option.some(monthOfRelativeYear(month, direction, `${textAt(months, month - 1)} ${relativeYearName(direction)}`));
367
426
  }
368
427
  const standaloneMonth = monthNumber(input);
369
- if (standaloneMonth !== void 0) return Option.some(monthOfRelativeYear(standaloneMonth, 0, title(textAt(months, standaloneMonth - 1))));
428
+ if (standaloneMonth !== void 0) return Option.some(monthOfRelativeYear(standaloneMonth, 0, textAt(months, standaloneMonth - 1)));
370
429
  const alias = periodAliases.find((entry) => entry[0] === input);
371
430
  if (alias !== void 0) return Option.some(relativePeriod(alias[1], alias[2], alias[3]));
372
431
  if ([
@@ -408,32 +467,87 @@ const parsePeriod = (input) => {
408
467
  ].find((prefix) => input.startsWith(prefix));
409
468
  return parseBasePeriod(wrapper === void 0 ? input : input.slice(wrapper.length));
410
469
  };
411
- const countedUnit = (value) => unitAliases.find((entry) => entry[0] === value)?.[1];
470
+ const countedUnit = (value, amount) => {
471
+ const expected = amount === 1 ? "singular" : "plural";
472
+ const alias = unitAliases.find((entry) => entry[0] === value && (entry[2] === expected || entry[2] === "both"));
473
+ return alias === void 0 ? void 0 : units.find((entry) => entry.unit === alias[1]);
474
+ };
412
475
  const countedUnitPattern = "dag|dagen|week|weken|maand|maanden|kwartaal|kwartalen|jaar|jaren";
413
476
  const countedPattern = (source) => new RegExp(source.replace("UNIT", countedUnitPattern), "u");
414
477
  const calendarPastPattern = countedPattern("^([1-9]\\d*) (UNIT) geleden$");
415
478
  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)$")];
479
+ const rollingPastPatterns = [countedPattern("^(?:(?:de|in de) )?(?:afgelopen|laatste|vorige) ([1-9]\\d*) (UNIT)$"), countedPattern("^([1-9]\\d*) (?:afgelopen|laatste|vorige) (UNIT)$")];
480
+ const rollingFuturePatterns = [countedPattern("^(?:(?:de|in de|binnen de) )?(?:komende|volgende|aankomende) ([1-9]\\d*) (UNIT)$"), countedPattern("^([1-9]\\d*) (?:komende|volgende|aankomende) (UNIT)$")];
418
481
  const rollingSincePattern = countedPattern("^(?:sinds|gedurende) ([1-9]\\d*) (UNIT)$");
419
482
  const rollingBarePattern = countedPattern("^([1-9]\\d*) (UNIT)$");
420
483
  const firstPatternMatch = (input, patterns) => Option.firstSomeOf(patterns.map((pattern) => String$1.match(pattern)(input)));
484
+ const singularRollingCanonical = (entry, future) => future ? `vanaf nu gedurende een ${entry.singular}` : `sinds een ${entry.singular}`;
485
+ const singularRollingPhrases = units.flatMap((entry) => [
486
+ {
487
+ phrase: `${entry.article} afgelopen ${entry.singular}`,
488
+ entry,
489
+ future: false
490
+ },
491
+ {
492
+ phrase: `${entry.article} laatste ${entry.singular}`,
493
+ entry,
494
+ future: false
495
+ },
496
+ {
497
+ phrase: `sinds een ${entry.singular}`,
498
+ entry,
499
+ future: false
500
+ },
501
+ {
502
+ phrase: `gedurende ${entry.article} komende ${entry.singular}`,
503
+ entry,
504
+ future: true
505
+ },
506
+ {
507
+ phrase: `vanaf nu gedurende een ${entry.singular}`,
508
+ entry,
509
+ future: true
510
+ }
511
+ ]);
512
+ const singularCalendarOffsets = units.flatMap((entry) => [
513
+ {
514
+ phrase: `een ${entry.singular} geleden`,
515
+ entry,
516
+ direction: -1
517
+ },
518
+ {
519
+ phrase: `over een ${entry.singular}`,
520
+ entry,
521
+ direction: 1
522
+ },
523
+ {
524
+ phrase: `binnen een ${entry.singular}`,
525
+ entry,
526
+ direction: 1
527
+ }
528
+ ]);
421
529
  const parseCalendarOffset = (input) => {
530
+ const singular = singularCalendarOffsets.find((entry) => entry.phrase === input);
531
+ if (singular !== void 0) return Option.some(candidate(periodRange(relativePeriod(singular.entry.unit, singular.direction, singular.phrase)), singular.phrase));
422
532
  const past = String$1.match(calendarPastPattern)(input);
423
533
  const future = firstPatternMatch(input, calendarFuturePatterns);
424
534
  const match = Option.firstSomeOf([past, future]);
425
535
  if (Option.isNone(match)) return Option.none();
426
536
  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);
537
+ if (Option.isNone(amount)) return Option.none();
538
+ const entry = countedUnit(textAt(match.value, 2), amount.value);
430
539
  if (entry === void 0) return Option.none();
431
540
  const direction = Option.isSome(past) ? -amount.value : amount.value;
432
541
  const noun = amount.value === 1 ? entry.singular : entry.plural;
433
542
  const canonical = direction < 0 ? `${amount.value} ${noun} geleden` : `over ${amount.value} ${noun}`;
434
- return Option.some(candidate(periodRange(relativePeriod(unit, direction, canonical)), canonical));
543
+ return Option.some(candidate(periodRange(relativePeriod(entry.unit, direction, canonical)), canonical));
435
544
  };
436
545
  const parseRollingPeriod = (input) => {
546
+ const singular = singularRollingPhrases.find((entry) => entry.phrase === input);
547
+ if (singular !== void 0) {
548
+ const range = singular.future ? futureRange(1, singular.entry.unit) : trailingRange(1, singular.entry.unit);
549
+ return Option.some(candidate(range, singularRollingCanonical(singular.entry, singular.future)));
550
+ }
437
551
  const since = String$1.match(rollingSincePattern)(input);
438
552
  const past = firstPatternMatch(input, rollingPastPatterns);
439
553
  const bare = String$1.match(rollingBarePattern)(input);
@@ -446,15 +560,26 @@ const parseRollingPeriod = (input) => {
446
560
  ]);
447
561
  if (Option.isNone(match)) return Option.none();
448
562
  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);
563
+ if (Option.isNone(amount)) return Option.none();
564
+ const entry = countedUnit(textAt(match.value, 2), amount.value);
452
565
  if (entry === void 0) return Option.none();
453
566
  const isFuture = Option.isSome(future);
454
- const range = isFuture ? futureRange(amount.value, unit) : trailingRange(amount.value, unit);
567
+ const range = isFuture ? futureRange(amount.value, entry.unit) : trailingRange(amount.value, entry.unit);
568
+ if (amount.value === 1) return Option.some(candidate(range, singularRollingCanonical(entry, isFuture)));
455
569
  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}`));
570
+ return Option.some(candidate(range, `de ${modifier} ${amount.value} ${entry.plural}`));
571
+ };
572
+ const parseElidedDateRange = (input) => {
573
+ 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);
574
+ const dashed = String$1.match(/^([0-3]?\d)[–—-]([0-3]?\d) (.+)$/u)(input);
575
+ const match = Option.firstSomeOf([joined, dashed]);
576
+ if (Option.isNone(match)) return Option.none();
577
+ const isJoined = Option.isSome(joined);
578
+ const lowerDay = textAt(match.value, 1) || textAt(match.value, 2);
579
+ const upperDay = textAt(match.value, isJoined ? 3 : 2);
580
+ const period = afterVan(textAt(match.value, isJoined ? 4 : 3));
581
+ const suffix = periodAliases.some((entry) => entry[0] === period && entry[1] === "month") ? `van ${period}` : period;
582
+ return joinedPeriodCandidate(`van ${lowerDay} ${suffix} tot en met ${upperDay} ${suffix}`, [["van ", " tot en met "]], parsePeriod, (lower, upper) => `van ${lower} tot en met ${upper}`);
458
583
  };
459
584
  const boundaryCandidate = (input) => openBoundaryCandidate(input, [
460
585
  ["tot voor ", "before"],
@@ -485,6 +610,8 @@ const parseDutch = (input) => {
485
610
  if (Option.isSome(rolling)) return rolling;
486
611
  const toDate = toDatePhrases.find((entry) => entry.phrase === input);
487
612
  if (toDate !== void 0) return Option.some(candidate(periodToDateRange(toDate.entry.unit), toDate.entry.toDate));
613
+ const elided = parseElidedDateRange(input);
614
+ if (Option.isSome(elided)) return elided;
488
615
  const nowBounded = joinedNowCandidate(input, [
489
616
  ["vanaf ", " tot nu toe"],
490
617
  ["sinds ", " tot nu toe"],
@@ -508,16 +635,28 @@ const parseDutch = (input) => {
508
635
  if (Option.isSome(bounded)) return bounded;
509
636
  const boundary = boundaryCandidate(input);
510
637
  if (Option.isSome(boundary)) return boundary;
511
- return Option.map(parsePeriod(input), (period) => candidate(periodRange(period), period.canonical));
638
+ return parsePeriod(input).pipe(Option.map((period) => candidate(periodRange(period), period.canonical)));
512
639
  };
513
- const staticPeriodPhrases = [
514
- ...periodAliases.map((entry) => entry[0]),
640
+ const weekendPhrases = [
515
641
  "dit weekend",
516
642
  "vorig weekend",
517
643
  "volgend weekend",
518
644
  "het weekend voor het vorige",
519
- "het weekend na het volgende",
520
- ...periodAliases.flatMap((entry) => [`begin van ${entry[0]}`, `eind van ${entry[0]}`]),
645
+ "het weekend na het volgende"
646
+ ];
647
+ const edgePeriodPhrases = [
648
+ ...units.filter((entry) => entry.unit !== "day").flatMap((entry) => [
649
+ entry.current,
650
+ entry.previous,
651
+ entry.next
652
+ ]),
653
+ ...weekendPhrases,
654
+ ...months
655
+ ].flatMap((period) => [`begin van ${period}`, `eind van ${period}`]);
656
+ const staticPeriodPhrases = [
657
+ ...periodAliases.map((entry) => entry[0]),
658
+ ...weekendPhrases,
659
+ ...edgePeriodPhrases,
521
660
  ...[
522
661
  1,
523
662
  2,
@@ -563,6 +702,8 @@ const countedSuggestions = (input) => {
563
702
  const dutchSuggestionPhrases = [
564
703
  ...units.map((entry) => entry.toDate),
565
704
  ...units.map((entry) => entry.remaining),
705
+ ...singularRollingPhrases.map((entry) => entry.phrase),
706
+ ...singularCalendarOffsets.map((entry) => entry.phrase),
566
707
  ...staticPeriodPhrases,
567
708
  ...prefixNaturalPhrases(staticPeriodPhrases, boundaryPrefixes),
568
709
  "tot nu toe",
@@ -590,20 +731,18 @@ const renderDutch = (range) => {
590
731
  const future = futurePeriod(range);
591
732
  if (Option.isSome(future)) {
592
733
  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
- }
734
+ if (entry !== void 0) return Option.some(future.value.amount === 1 ? singularRollingCanonical(entry, true) : `de komende ${future.value.amount} ${entry.plural}`);
597
735
  }
598
736
  const trailing = trailingPeriod(range);
599
737
  if (Option.isSome(trailing)) {
600
738
  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
- }
739
+ if (entry !== void 0) return Option.some(trailing.value.amount === 1 ? singularRollingCanonical(entry, false) : `de afgelopen ${trailing.value.amount} ${entry.plural}`);
605
740
  }
606
- const periods = [...staticPeriods, ...periodsFromPhrases([...datedPeriods(range, months), ...datedQuarterPeriods(range).map((phrase) => phrase.startsWith("Q") ? `K${phrase.slice(1)}` : phrase)], parsePeriod)];
741
+ const periods = [
742
+ ...staticPeriods,
743
+ ...currentYearDatePeriods(range, currentDateLabel),
744
+ ...periodsFromPhrases([...datedPeriods(range, months), ...datedQuarterPeriods(range).map((phrase) => phrase.startsWith("Q") ? `K${phrase.slice(1)}` : phrase)], parsePeriod)
745
+ ];
607
746
  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
747
  };
609
748
  const DutchContribution = new BaseLanguageContribution({