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, periodDay, periodEndDay, periodRange, periodStartDay, periodToDateRange, periodsFromPhrases, quarterOfRelativeYear, relativePeriod, relativeWeekend, remainingPeriodRange, renderPeriodRange, textAt, trailingPeriod, trailingRange, untilNowRange, validYear } from "./shared.mjs";
6
+ import { absoluteDatePeriod, calendarPeriodOffset, candidate, compileCountAliasNormalizer, countAliasVocabulary, currentYearDatePeriods, datedPeriods, datedQuarterPeriods, decomposeShiftedPeriodRange, fixedMonthPeriod, fixedQuarterPeriod, fixedYearPeriod, fromNowRange, futurePeriod, futureRange, joinedNowCandidate, joinedPeriodCandidate, monthOfRelativeYear, namedCurrentYearDatePeriod, namedDatePeriod, openBoundaryCandidate, parseTrailingCount, periodBoundaryCandidate, periodDay, 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/fr.ts
10
9
  const months = [
@@ -21,6 +20,90 @@ const months = [
21
20
  "novembre",
22
21
  "décembre"
23
22
  ];
23
+ const weekdays = [
24
+ "lundi",
25
+ "mardi",
26
+ "mercredi",
27
+ "jeudi",
28
+ "vendredi",
29
+ "samedi",
30
+ "dimanche"
31
+ ];
32
+ const nextWeekdayPhrases = weekdays.map((weekday) => `${weekday} prochain`);
33
+ const frenchCountWords = [
34
+ ["deux"],
35
+ ["trois"],
36
+ ["quatre"],
37
+ ["cinq"],
38
+ ["six"],
39
+ ["sept"],
40
+ ["huit"],
41
+ ["neuf"],
42
+ ["dix"],
43
+ ["onze"],
44
+ ["douze"],
45
+ ["treize"],
46
+ ["quatorze"],
47
+ ["quinze"],
48
+ ["seize"],
49
+ ["dix-sept"],
50
+ ["dix-huit"],
51
+ ["dix-neuf"],
52
+ ["vingt"]
53
+ ];
54
+ const frenchCompoundCountAliases = () => {
55
+ const words = [
56
+ "",
57
+ "un",
58
+ "deux",
59
+ "trois",
60
+ "quatre",
61
+ "cinq",
62
+ "six",
63
+ "sept",
64
+ "huit",
65
+ "neuf",
66
+ "dix",
67
+ "onze",
68
+ "douze",
69
+ "treize",
70
+ "quatorze",
71
+ "quinze",
72
+ "seize",
73
+ "dix-sept",
74
+ "dix-huit",
75
+ "dix-neuf"
76
+ ];
77
+ const aliases = [];
78
+ for (let amount = 21; amount <= 99; amount += 1) {
79
+ const tens = Math.floor(amount / 10);
80
+ const remainder = amount % 10;
81
+ let phrase = "";
82
+ if (tens <= 6) {
83
+ const tensWord = [
84
+ "",
85
+ "",
86
+ "vingt",
87
+ "trente",
88
+ "quarante",
89
+ "cinquante",
90
+ "soixante"
91
+ ][tens];
92
+ if (tensWord === void 0) continue;
93
+ if (remainder === 0) phrase = tensWord;
94
+ else phrase = remainder === 1 ? `${tensWord} et un` : `${tensWord}-${words[remainder] ?? ""}`;
95
+ } else if (tens === 7) phrase = remainder === 1 ? "soixante et onze" : `soixante-${words[10 + remainder] ?? ""}`;
96
+ else if (tens === 8) phrase = remainder === 0 ? "quatre-vingts" : `quatre-vingt-${words[remainder] ?? ""}`;
97
+ else phrase = `quatre-vingt-${words[10 + remainder] ?? ""}`;
98
+ aliases.push([phrase, amount], [phrase.replaceAll("-", " "), amount]);
99
+ if (amount % 10 === 1) aliases.push([phrase.replace(/un$/u, "une"), amount]);
100
+ }
101
+ return aliases;
102
+ };
103
+ const frenchCountAliases = [...sequentialCountAliases(frenchCountWords, 2), ...frenchCompoundCountAliases()];
104
+ const normalizeFrenchCounts = compileCountAliasNormalizer(frenchCountAliases);
105
+ const frenchCountVocabulary = new Set(countAliasVocabulary(frenchCountAliases));
106
+ const correctFrench = (input, vocabulary) => correctWhitespaceSeparatedText(input, vocabulary, frenchCountVocabulary);
24
107
  const monthAbbreviations = [
25
108
  ["jan", "janv"],
26
109
  [
@@ -416,15 +499,7 @@ const parseNamedDate = (input) => {
416
499
  const named = String$1.match(/^(?:le )?(1er|[0-3]?\d)(?: de)? ([a-zàâçéèêëîïôûùüÿœ]+\.?),?(?: de)? (\d{4})$/u)(input);
417
500
  if (Option.isSome(named)) return namedDatePeriod(textAt(named.value, 3), textAt(named.value, 2), textAt(named.value, 1).replace(/er$/u, ""), monthNumber, dateLabel);
418
501
  const numeric = String$1.match(/^([0-3]?\d)[./-]([01]?\d)[./-](\d{4})$/u)(input);
419
- if (Option.isSome(numeric)) {
420
- const year = validYear(textAt(numeric.value, 3));
421
- const month = Number(textAt(numeric.value, 2));
422
- const day = Number(textAt(numeric.value, 1));
423
- if (year !== void 0 && month >= 1 && month <= 12) {
424
- const value = isoDate(year, month, day);
425
- if (isIsoDate(value) && value !== "9999-12-31") return Option.some(fixedDatePeriod(value, dateLabel(day, month, year)));
426
- }
427
- }
502
+ if (Option.isSome(numeric)) return namedDatePeriod(textAt(numeric.value, 3), textAt(numeric.value, 2), textAt(numeric.value, 1), Number, dateLabel);
428
503
  const current = String$1.match(/^(?:le )?(1er|[0-3]?\d)(?: de)? ([a-zàâçéèêëîïôûùüÿœ]+\.?)$/u)(input);
429
504
  if (Option.isSome(current)) return namedCurrentYearDatePeriod(textAt(current.value, 2), textAt(current.value, 1).replace(/er$/u, ""), monthNumber, currentDateLabel);
430
505
  const relative = String$1.match(/^(?:le )?(1er|[0-3]?\d) (de .+|du .+)$/u)(input);
@@ -461,13 +536,13 @@ const parseQuarter = (input) => {
461
536
  const quarter = quarterNumber(textAt(standalone.value, 1));
462
537
  return quarter === void 0 ? Option.none() : Option.some(quarterOfRelativeYear(quarter, 0, `T${quarter}`));
463
538
  };
464
- const parseBasePeriod = (input) => {
465
- const absoluteDate = absoluteDatePeriod(input, "fr");
466
- if (Option.isSome(absoluteDate)) return absoluteDate;
467
- const namedDate = parseNamedDate(input);
468
- if (Option.isSome(namedDate)) return namedDate;
469
- const quarter = parseQuarter(input);
470
- if (Option.isSome(quarter)) return quarter;
539
+ const parseDatedPeriod = (input) => {
540
+ const knownPeriod = Option.firstSomeOf([
541
+ absoluteDatePeriod(input, "fr"),
542
+ parseNamedDate(input),
543
+ parseQuarter(input)
544
+ ]);
545
+ if (Option.isSome(knownPeriod)) return knownPeriod;
471
546
  const yearMatch = String$1.match(/^(?:(?:l')?année |annee )?(\d{4})$/u)(input);
472
547
  if (Option.isSome(yearMatch)) {
473
548
  const year = validYear(textAt(yearMatch.value, 1));
@@ -479,6 +554,20 @@ const parseBasePeriod = (input) => {
479
554
  const year = validYear(textAt(monthYear.value, 2));
480
555
  if (month !== void 0 && year !== void 0) return Option.some(fixedMonthPeriod(year, month, `${textAt(months, month - 1)} ${year}`));
481
556
  }
557
+ return Option.none();
558
+ };
559
+ const parseBasePeriod = (input) => {
560
+ const datedPeriod = parseDatedPeriod(input);
561
+ if (Option.isSome(datedPeriod)) return datedPeriod;
562
+ const suffixedRelativeMonth = String$1.match(/^([a-zàâçéèêëîïôûùüÿœ]+\.?) (dernier|prochain)$/u)(input);
563
+ if (Option.isSome(suffixedRelativeMonth)) {
564
+ const month = monthNumber(textAt(suffixedRelativeMonth.value, 1));
565
+ if (month !== void 0) {
566
+ const modifier = textAt(suffixedRelativeMonth.value, 2);
567
+ const direction = modifier === "dernier" ? -1 : 1;
568
+ return Option.some(monthOfRelativeYear(month, direction, `${textAt(months, month - 1)} ${modifier}`));
569
+ }
570
+ }
482
571
  const relativeMonth = String$1.match(/^([a-zàâçéèêëîïôûùüÿœ]+\.?) de (l'année dernière|l'annee derniere|l'année prochaine|l'annee prochaine|cette année|cette annee)$/u)(input);
483
572
  if (Option.isSome(relativeMonth)) {
484
573
  const month = monthNumber(textAt(relativeMonth.value, 1));
@@ -509,13 +598,32 @@ const parseBasePeriod = (input) => {
509
598
  ].includes(input)) return Option.some(relativeWeekend(1, "le week-end prochain"));
510
599
  if (input === "l'avant-dernier week-end") return Option.some(relativeWeekend(-2, input));
511
600
  if (input === "le week-end après le prochain") return Option.some(relativeWeekend(2, input));
512
- return Option.none();
601
+ const weekday = nextWeekdayPhrases.indexOf(input);
602
+ return weekday === -1 ? Option.none() : Option.some(relativeWeekday(weekday, 1, nextWeekdayPhrases[weekday] ?? input));
513
603
  };
514
604
  const parsePeriod = (input) => {
605
+ const shifted = String$1.match(/^(.+) (il y a|dans) ([1-9]\d*) (jour|jours|semaine|semaines|mois|trimestre|trimestres|an|ans|année|annee|années|annees)$/u)(input);
606
+ if (Option.isSome(shifted)) {
607
+ const amount = parseTrailingCount(textAt(shifted.value, 3));
608
+ const alias = unitAliases.find((unit) => unit[0] === textAt(shifted.value, 4));
609
+ const entry = alias === void 0 ? void 0 : units.find((unit) => unit.unit === alias[1]);
610
+ const period = parsePeriod(textAt(shifted.value, 1));
611
+ if (Option.isSome(amount) && entry !== void 0 && Option.isSome(period)) {
612
+ const past = textAt(shifted.value, 2) === "il y a";
613
+ const direction = past ? -amount.value : amount.value;
614
+ const noun = amount.value === 1 ? entry.singular : entry.plural;
615
+ const canonical = `${period.value.canonical} ${past ? "il y a" : "dans"} ${amount.value} ${noun}`;
616
+ return Option.some(shiftPeriod(period.value, direction, entry.unit, canonical));
617
+ }
618
+ }
515
619
  const edge = String$1.match(/^(?:le |la |l')?(début|debut|commencement|fin)(?: du | de la | de l'| de )(.+)$/u)(input);
516
620
  if (Option.isSome(edge)) {
517
621
  const edgeName = textAt(edge.value, 1);
518
- const period = parseBasePeriod(textAt(edge.value, 2));
622
+ const periodText = textAt(edge.value, 2);
623
+ const basePeriod = parseBasePeriod(periodText);
624
+ const implicitUnit = unitAliases.find((entry) => entry[0] === periodText)?.[1];
625
+ const implicit = units.find((entry) => entry.unit === implicitUnit);
626
+ const period = Option.isSome(basePeriod) || implicit === void 0 ? basePeriod : Option.some(relativePeriod(implicit.unit, 0, implicit.current));
519
627
  if (Option.isSome(period)) {
520
628
  const isEnd = edgeName === "fin";
521
629
  const canonical = `${isEnd ? "fin" : "début"} ${withDe(period.value.canonical)}`;
@@ -529,7 +637,8 @@ const parsePeriod = (input) => {
529
637
  "tout le ",
530
638
  "toute la "
531
639
  ].find((prefix) => input.startsWith(prefix));
532
- return parseBasePeriod(wrapper === void 0 ? input : input.slice(wrapper.length));
640
+ const base = parseBasePeriod(wrapper === void 0 ? input : input.slice(wrapper.length));
641
+ return Option.isSome(base) ? base : parseCalendarOffset(input);
533
642
  };
534
643
  const countedUnit = (value, amount) => {
535
644
  const plural = value === "mois" || value.endsWith("s");
@@ -537,36 +646,29 @@ const countedUnit = (value, amount) => {
537
646
  const unit = unitAliases.find((entry) => entry[0] === value)?.[1];
538
647
  return unit === void 0 ? void 0 : units.find((entry) => entry.unit === unit);
539
648
  };
649
+ const rollingPastModifierPattern = "derni(?:ers|ères|eres)|pass(?:és|ées|es)|pr[eé]c[eé]dent(?:s|es)";
650
+ const rollingFutureModifierPattern = "prochain(?:s|es)|suivant(?:s|es)";
651
+ const rollingModifierMatchPattern = new RegExp(`(?:^| )(${`${rollingPastModifierPattern}|${rollingFutureModifierPattern}`})(?: |$)`, "u");
540
652
  const modifierAgreesWithNoun = (input, noun) => {
541
- const match = String$1.match(/(?:^| )(derniers|dernières|dernieres|passés|passées|passes|précédents|précédentes|precedents|precedentes|prochains|prochaines|suivants|suivantes)(?: |$)/u)(input);
653
+ const match = String$1.match(rollingModifierMatchPattern)(input);
542
654
  if (Option.isNone(match)) return true;
543
655
  const modifier = textAt(match.value, 1);
544
- return (noun.startsWith("semaine") || noun.startsWith("année") || noun.startsWith("annee")) === [
545
- "dernières",
546
- "dernieres",
547
- "passées",
548
- "passes",
549
- "précédentes",
550
- "precedentes",
551
- "prochaines",
552
- "suivantes"
553
- ].includes(modifier);
656
+ return (noun.startsWith("semaine") || noun.startsWith("année") || noun.startsWith("annee")) === modifier.endsWith("es");
554
657
  };
555
- const countedUnitPattern = "jour|jours|semaine|semaines|mois|trimestre|trimestres|an|ans|année|annee|années|annees";
556
- const compileCountedPattern = (source) => new RegExp(source.replace("UNIT", countedUnitPattern), "u");
557
- const calendarPastPattern = compileCountedPattern("^il y a ([1-9]\\d*) (UNIT)$");
558
- const calendarFuturePattern = compileCountedPattern("^dans ([1-9]\\d*) (UNIT)$");
559
- const rollingSincePattern = compileCountedPattern("^depuis ([1-9]\\d*) (UNIT)$");
560
- const rollingBarePattern = compileCountedPattern("^([1-9]\\d*) (UNIT)$");
658
+ const compilePattern = (source) => new RegExp(source, "u");
659
+ const calendarPastPattern = /^il y a ([1-9]\d*) ([^ ]+)$/u;
660
+ const calendarFuturePattern = /^dans ([1-9]\d*) ([^ ]+)$/u;
661
+ const rollingSincePattern = /^depuis ([1-9]\d*) ([^ ]+)$/u;
662
+ const rollingBarePattern = /^([1-9]\d*) ([^ ]+)$/u;
561
663
  const rollingPastPatterns = [
562
- compileCountedPattern("^(?:(?:les|la) )?(?:derniers|dernières|dernieres|passés|passées|passes|précédents|précédentes|precedents|precedentes) ([1-9]\\d*) (UNIT)$"),
563
- compileCountedPattern("^(?:(?:les|la|au cours des|pendant les) )?([1-9]\\d*) (?:derniers|dernières|dernieres|passés|passées|passes|précédents|précédentes|precedents|precedentes) (UNIT)$"),
564
- compileCountedPattern("^([1-9]\\d*) (UNIT) (?:derniers|dernières|dernieres|passés|passées|passes|précédents|précédentes|precedents|precedentes)$")
664
+ compilePattern(`^(?:(?:les|la) )?(?:${rollingPastModifierPattern}) ([1-9]\\d*) ([^ ]+)$`),
665
+ compilePattern(`^(?:(?:les|la|au cours des|pendant les) )?([1-9]\\d*) (?:${rollingPastModifierPattern}) ([^ ]+)$`),
666
+ compilePattern(`^([1-9]\\d*) ([^ ]+) (?:${rollingPastModifierPattern})$`)
565
667
  ];
566
668
  const rollingFuturePatterns = [
567
- compileCountedPattern("^(?:(?:les|la) )?(?:prochains|prochaines|suivants|suivantes) ([1-9]\\d*) (UNIT)$"),
568
- compileCountedPattern("^(?:(?:les|la|au cours des|pendant les) )?([1-9]\\d*) (?:prochains|prochaines|suivants|suivantes) (UNIT)$"),
569
- compileCountedPattern("^([1-9]\\d*) (UNIT) (?:prochains|prochaines|suivants|suivantes|à venir)$")
669
+ compilePattern(`^(?:(?:les|la) )?(?:${rollingFutureModifierPattern}) ([1-9]\\d*) ([^ ]+)$`),
670
+ compilePattern(`^(?:(?:les|la|au cours des|pendant les) )?([1-9]\\d*) (?:${rollingFutureModifierPattern}) ([^ ]+)$`),
671
+ compilePattern(`^([1-9]\\d*) ([^ ]+) (?:${rollingFutureModifierPattern}|à venir)$`)
570
672
  ];
571
673
  const firstPatternMatch = (input, patterns) => Option.firstSomeOf(patterns.map((pattern) => String$1.match(pattern)(input)));
572
674
  const relativeCountPhrase = (amount, entry, future) => {
@@ -623,7 +725,7 @@ const singularCalendarOffsets = units.flatMap((entry) => {
623
725
  });
624
726
  const parseCalendarOffset = (input) => {
625
727
  const singular = singularCalendarOffsets.find((entry) => entry.phrase === input);
626
- if (singular !== void 0) return Option.some(candidate(periodRange(relativePeriod(singular.entry.unit, singular.direction, singular.phrase)), singular.phrase));
728
+ if (singular !== void 0) return Option.some(relativePeriod(singular.entry.unit, singular.direction, singular.phrase));
627
729
  const past = String$1.match(calendarPastPattern)(input);
628
730
  const future = String$1.match(calendarFuturePattern)(input);
629
731
  const match = Option.firstSomeOf([past, future]);
@@ -635,7 +737,7 @@ const parseCalendarOffset = (input) => {
635
737
  const direction = Option.isSome(past) ? -amount.value : amount.value;
636
738
  const noun = amount.value === 1 ? entry.singular : entry.plural;
637
739
  const canonical = direction < 0 ? `il y a ${amount.value} ${noun}` : `dans ${amount.value} ${noun}`;
638
- return Option.some(candidate(periodRange(relativePeriod(entry.unit, direction, canonical)), canonical));
740
+ return Option.some(relativePeriod(entry.unit, direction, canonical));
639
741
  };
640
742
  const parseRollingPeriod = (input) => {
641
743
  const singular = singularRollingPhrases.find((entry) => entry.phrase === input);
@@ -708,8 +810,6 @@ const parseFrench = (input) => {
708
810
  "à partir de maintenant",
709
811
  "désormais"
710
812
  ].includes(input)) return Option.some(candidate(fromNowRange(), "depuis maintenant"));
711
- const offset = parseCalendarOffset(input);
712
- if (Option.isSome(offset)) return offset;
713
813
  const rolling = parseRollingPeriod(input);
714
814
  if (Option.isSome(rolling)) return rolling;
715
815
  const toDate = toDatePhrases.find((entry) => entry.phrase === input);
@@ -723,7 +823,8 @@ const parseFrench = (input) => {
723
823
  ], [
724
824
  "d'aujourd'hui à ",
725
825
  "de maintenant à ",
726
- "entre aujourd'hui et "
826
+ "entre aujourd'hui et ",
827
+ "maintenant à "
727
828
  ], parsePeriod, (period) => `depuis ${period} jusqu'à maintenant`, (period) => `de maintenant à ${period}`);
728
829
  if (Option.isSome(nowBounded)) return nowBounded;
729
830
  const bounded = joinedPeriodCandidate(input, [
@@ -775,9 +876,12 @@ const staticPeriodPhrases = [
775
876
  ]),
776
877
  ...months.flatMap((month) => [
777
878
  month,
879
+ `${month} dernier`,
880
+ `${month} prochain`,
778
881
  `${month} de l'année dernière`,
779
882
  `${month} de l'année prochaine`
780
- ])
883
+ ]),
884
+ ...nextWeekdayPhrases
781
885
  ];
782
886
  const staticPeriods = periodsFromPhrases(staticPeriodPhrases, parsePeriod);
783
887
  const boundaryPrefixes = [
@@ -826,6 +930,17 @@ const suggestFrench = (input, limit) => {
826
930
  ], limit);
827
931
  };
828
932
  const renderFrench = (range) => {
933
+ const shifted = decomposeShiftedPeriodRange(range);
934
+ if (Option.isSome(shifted)) {
935
+ const base = renderFrench(shifted.value.baseRange);
936
+ const entry = units.find((unit) => unit.unit === shifted.value.unit);
937
+ if (Option.isSome(base) && entry !== void 0) {
938
+ const amount = Math.abs(shifted.value.amount);
939
+ const noun = amount === 1 ? entry.singular : entry.plural;
940
+ const direction = shifted.value.amount < 0 ? "il y a" : "dans";
941
+ return Option.some(`${base.value} ${direction} ${amount} ${noun}`);
942
+ }
943
+ }
829
944
  const offset = calendarPeriodOffset(range);
830
945
  if (Option.isSome(offset) && Math.abs(offset.value.amount) > 1) {
831
946
  const entry = units.find((unit) => unit.unit === offset.value.unit);
@@ -852,11 +967,12 @@ const renderFrench = (range) => {
852
967
  ];
853
968
  return renderPeriodRange(range, [...units.map((entry) => candidate(periodToDateRange(entry.unit), entry.toDate)), ...units.map((entry) => candidate(remainingPeriodRange(entry.unit), entry.remaining))], periods, (period) => `depuis ${period}`, (period) => `avant ${period}`, untilLabel, (period) => `après ${period}`, rangeLabel, (period) => `depuis ${period} jusqu'à maintenant`, (period) => `de maintenant à ${period}`, () => "jusqu'à maintenant", () => "depuis maintenant");
854
969
  };
855
- const normalizeFrench = (input, locale) => normalizeNaturalText(input, locale).replaceAll("’", "'");
970
+ const normalizeFrench = (input, locale) => normalizeFrenchCounts(normalizeNaturalText(input, locale).replaceAll("’", "'"));
856
971
  const FrenchContribution = new BaseLanguageContribution({
857
972
  locale: "fr",
858
973
  vocabulary: [
859
974
  ...months,
975
+ ...weekdays,
860
976
  ...monthAbbreviations.flatMap((aliases) => aliases),
861
977
  ...units.flatMap((entry) => [
862
978
  entry.singular,
@@ -887,7 +1003,7 @@ const FrenchContribution = new BaseLanguageContribution({
887
1003
  "jusqu'à"
888
1004
  ],
889
1005
  normalize: normalizeFrench,
890
- correct: correctWhitespaceSeparatedText,
1006
+ correct: correctFrench,
891
1007
  parseExact: parseFrench,
892
1008
  suggest: suggestFrench,
893
1009
  render: renderFrench
@@ -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, periodDay, 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, periodDay, 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/nl.ts
10
9
  const months = [
@@ -21,6 +20,54 @@ const months = [
21
20
  "november",
22
21
  "december"
23
22
  ];
23
+ const weekdays = [
24
+ "maandag",
25
+ "dinsdag",
26
+ "woensdag",
27
+ "donderdag",
28
+ "vrijdag",
29
+ "zaterdag",
30
+ "zondag"
31
+ ];
32
+ const nextWeekdayPhrases = weekdays.map((weekday) => `volgende ${weekday}`);
33
+ const dutchCountWords = [
34
+ "twee",
35
+ "drie",
36
+ "vier",
37
+ "vijf",
38
+ "zes",
39
+ "zeven",
40
+ "acht",
41
+ "negen",
42
+ "tien",
43
+ "elf",
44
+ "twaalf",
45
+ "dertien",
46
+ "veertien",
47
+ "vijftien",
48
+ "zestien",
49
+ "zeventien",
50
+ "achttien",
51
+ "negentien",
52
+ "twintig"
53
+ ];
54
+ const dutchCountOnes = [[1, "een"], ...dutchCountWords.slice(0, 8).map((word, index) => [index + 2, word])];
55
+ const dutchCountAliases = [...sequentialCountAliases(dutchCountWords.map((word) => [word]), 2), ...compoundCountAliases(decimalTens([
56
+ "twintig",
57
+ "dertig",
58
+ "veertig",
59
+ "vijftig",
60
+ "zestig",
61
+ "zeventig",
62
+ "tachtig",
63
+ "negentig"
64
+ ]), dutchCountOnes, (ten, one) => {
65
+ return [`${one === "twee" || one === "drie" ? `${one}ën` : `${one}en`}${ten}`];
66
+ })];
67
+ const normalizeDutchCounts = compileCountAliasNormalizer(dutchCountAliases);
68
+ const dutchCountVocabulary = new Set(countAliasVocabulary(dutchCountAliases));
69
+ const correctDutch = (input, vocabulary) => correctWhitespaceSeparatedText(input, vocabulary, dutchCountVocabulary);
70
+ const normalizeDutch = (input, locale) => normalizeDutchCounts(normalizeNaturalText(input, locale));
24
71
  const monthAbbreviations = [
25
72
  ["jan"],
26
73
  ["feb"],
@@ -350,15 +397,7 @@ const parseNamedDate = (input) => {
350
397
  const named = String$1.match(/^(?:de )?([0-3]?\d)(?:e|ste|de)?(?: van)? ([a-z]+\.?)(?: van)? (\d{4})$/u)(input);
351
398
  if (Option.isSome(named)) return namedDatePeriod(textAt(named.value, 3), textAt(named.value, 2), textAt(named.value, 1), monthNumber, dateLabel);
352
399
  const numeric = String$1.match(/^([0-3]?\d)[./-]([01]?\d)[./-](\d{4})$/u)(input);
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
- }
400
+ if (Option.isSome(numeric)) return namedDatePeriod(textAt(numeric.value, 3), textAt(numeric.value, 2), textAt(numeric.value, 1), Number, dateLabel);
362
401
  const current = String$1.match(/^(?:de )?([0-3]?\d)(?:e|ste|de)?(?: van)? ([a-z]+\.?)$/u)(input);
363
402
  if (Option.isSome(current)) return namedCurrentYearDatePeriod(textAt(current.value, 2), textAt(current.value, 1), monthNumber, currentDateLabel);
364
403
  const relative = String$1.match(/^(?:de )?([0-3]?\d)(?:e|ste|de)? van (.+)$/u)(input);
@@ -395,13 +434,13 @@ const parseQuarter = (input) => {
395
434
  const quarter = quarterNumber(textAt(standalone.value, 1));
396
435
  return quarter === void 0 ? Option.none() : Option.some(quarterOfRelativeYear(quarter, 0, `K${quarter}`));
397
436
  };
398
- const parseBasePeriod = (input) => {
399
- const absoluteDate = absoluteDatePeriod(input, "nl");
400
- if (Option.isSome(absoluteDate)) return absoluteDate;
401
- const namedDate = parseNamedDate(input);
402
- if (Option.isSome(namedDate)) return namedDate;
403
- const quarter = parseQuarter(input);
404
- if (Option.isSome(quarter)) return quarter;
437
+ const parseDatedPeriod = (input) => {
438
+ const knownPeriod = Option.firstSomeOf([
439
+ absoluteDatePeriod(input, "nl"),
440
+ parseNamedDate(input),
441
+ parseQuarter(input)
442
+ ]);
443
+ if (Option.isSome(knownPeriod)) return knownPeriod;
405
444
  const yearMatch = String$1.match(/^(?:het jaar |jaar )?(\d{4})$/u)(input);
406
445
  if (Option.isSome(yearMatch)) {
407
446
  const year = validYear(textAt(yearMatch.value, 1));
@@ -413,6 +452,20 @@ const parseBasePeriod = (input) => {
413
452
  const year = validYear(textAt(monthYear.value, 2));
414
453
  if (month !== void 0 && year !== void 0) return Option.some(fixedMonthPeriod(year, month, `${textAt(months, month - 1)} ${year}`));
415
454
  }
455
+ return Option.none();
456
+ };
457
+ const parseBasePeriod = (input) => {
458
+ const datedPeriod = parseDatedPeriod(input);
459
+ if (Option.isSome(datedPeriod)) return datedPeriod;
460
+ const prefixedRelativeMonth = String$1.match(/^(vorige|deze|volgende) ([a-z]+\.?)$/u)(input);
461
+ if (Option.isSome(prefixedRelativeMonth)) {
462
+ const month = monthNumber(textAt(prefixedRelativeMonth.value, 2));
463
+ if (month !== void 0) {
464
+ const modifier = textAt(prefixedRelativeMonth.value, 1);
465
+ const direction = relativeYearDirection(modifier);
466
+ return Option.some(monthOfRelativeYear(month, direction, `${modifier} ${textAt(months, month - 1)}`));
467
+ }
468
+ }
416
469
  const relativeMonth = String$1.match(/^([a-z]+\.?)(?: van)? (vorig jaar|volgend jaar|dit jaar)$/u)(input);
417
470
  const relativeMonthYearFirst = String$1.match(/^(vorig jaar|volgend jaar|dit jaar) ([a-z]+\.?)$/u)(input);
418
471
  const relativeMatch = Option.firstSomeOf([relativeMonth, relativeMonthYearFirst]);
@@ -445,13 +498,33 @@ const parseBasePeriod = (input) => {
445
498
  ].includes(input)) return Option.some(relativeWeekend(1, "volgend weekend"));
446
499
  if (input === "het weekend voor het vorige") return Option.some(relativeWeekend(-2, input));
447
500
  if (input === "het weekend na het volgende") return Option.some(relativeWeekend(2, input));
448
- return Option.none();
501
+ const weekday = nextWeekdayPhrases.indexOf(input);
502
+ return weekday === -1 ? Option.none() : Option.some(relativeWeekday(weekday, 1, nextWeekdayPhrases[weekday] ?? input));
449
503
  };
450
504
  const parsePeriod = (input) => {
505
+ 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);
506
+ if (Option.isSome(shifted)) {
507
+ const suffixAmount = textAt(shifted.value, 4);
508
+ const amount = parseTrailingCount(suffixAmount || textAt(shifted.value, 2));
509
+ const unitText = textAt(shifted.value, suffixAmount.length > 0 ? 5 : 3);
510
+ const alias = unitAliases.find((unit) => unit[0] === unitText);
511
+ const entry = alias === void 0 ? void 0 : units.find((unit) => unit.unit === alias[1]);
512
+ const period = parsePeriod(textAt(shifted.value, 1));
513
+ if (Option.isSome(amount) && entry !== void 0 && Option.isSome(period)) {
514
+ const past = textAt(shifted.value, 6) === "geleden";
515
+ const direction = past ? -amount.value : amount.value;
516
+ const noun = amount.value === 1 ? entry.singular : entry.plural;
517
+ const canonical = past ? `${period.value.canonical} ${amount.value} ${noun} geleden` : `${period.value.canonical} over ${amount.value} ${noun}`;
518
+ return Option.some(shiftPeriod(period.value, direction, entry.unit, canonical));
519
+ }
520
+ }
451
521
  const edge = String$1.match(/^(?:het )?(begin|eind|einde)(?: van)? (.+)$/u)(input);
452
522
  if (Option.isSome(edge)) {
453
523
  const edgeName = textAt(edge.value, 1);
454
- const period = parseBasePeriod(textAt(edge.value, 2));
524
+ const periodText = textAt(edge.value, 2);
525
+ const basePeriod = parseBasePeriod(periodText);
526
+ const implicit = units.find((entry) => `${entry.article} ${entry.singular}` === periodText);
527
+ const period = Option.isSome(basePeriod) || implicit === void 0 ? basePeriod : Option.some(relativePeriod(implicit.unit, 0, implicit.current));
455
528
  if (Option.isSome(period)) {
456
529
  const isEnd = edgeName === "eind" || edgeName === "einde";
457
530
  const canonical = `${isEnd ? "eind" : "begin"} van ${period.value.canonical}`;
@@ -465,21 +538,20 @@ const parsePeriod = (input) => {
465
538
  "de hele ",
466
539
  "het hele "
467
540
  ].find((prefix) => input.startsWith(prefix));
468
- return parseBasePeriod(wrapper === void 0 ? input : input.slice(wrapper.length));
541
+ const base = parseBasePeriod(wrapper === void 0 ? input : input.slice(wrapper.length));
542
+ return Option.isSome(base) ? base : parseCalendarOffset(input);
469
543
  };
470
544
  const countedUnit = (value, amount) => {
471
545
  const expected = amount === 1 ? "singular" : "plural";
472
546
  const alias = unitAliases.find((entry) => entry[0] === value && (entry[2] === expected || entry[2] === "both"));
473
547
  return alias === void 0 ? void 0 : units.find((entry) => entry.unit === alias[1]);
474
548
  };
475
- const countedUnitPattern = "dag|dagen|week|weken|maand|maanden|kwartaal|kwartalen|jaar|jaren";
476
- const countedPattern = (source) => new RegExp(source.replace("UNIT", countedUnitPattern), "u");
477
- const calendarPastPattern = countedPattern("^([1-9]\\d*) (UNIT) geleden$");
478
- const calendarFuturePatterns = [countedPattern("^(?:over|binnen) ([1-9]\\d*) (UNIT)$"), countedPattern("^([1-9]\\d*) (UNIT) later$")];
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)$")];
481
- const rollingSincePattern = countedPattern("^(?:sinds|gedurende) ([1-9]\\d*) (UNIT)$");
482
- const rollingBarePattern = countedPattern("^([1-9]\\d*) (UNIT)$");
549
+ const calendarPastPattern = /^([1-9]\d*) ([^ ]+) geleden$/u;
550
+ const calendarFuturePatterns = [/^(?:over|binnen) ([1-9]\d*) ([^ ]+)$/u, /^([1-9]\d*) ([^ ]+) later$/u];
551
+ const rollingPastPatterns = [/^(?:(?:de|in de) )?(?:afgelopen|laatste|vorige) ([1-9]\d*) ([^ ]+)$/u, /^([1-9]\d*) (?:afgelopen|laatste|vorige) ([^ ]+)$/u];
552
+ const rollingFuturePatterns = [/^(?:(?:de|in de|binnen de) )?(?:komende|volgende|aankomende) ([1-9]\d*) ([^ ]+)$/u, /^([1-9]\d*) (?:komende|volgende|aankomende) ([^ ]+)$/u];
553
+ const rollingSincePattern = /^(?:sinds|gedurende) ([1-9]\d*) ([^ ]+)$/u;
554
+ const rollingBarePattern = /^([1-9]\d*) ([^ ]+)$/u;
483
555
  const firstPatternMatch = (input, patterns) => Option.firstSomeOf(patterns.map((pattern) => String$1.match(pattern)(input)));
484
556
  const singularRollingCanonical = (entry, future) => future ? `vanaf nu gedurende een ${entry.singular}` : `sinds een ${entry.singular}`;
485
557
  const singularRollingPhrases = units.flatMap((entry) => [
@@ -528,7 +600,7 @@ const singularCalendarOffsets = units.flatMap((entry) => [
528
600
  ]);
529
601
  const parseCalendarOffset = (input) => {
530
602
  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));
603
+ if (singular !== void 0) return Option.some(relativePeriod(singular.entry.unit, singular.direction, singular.phrase));
532
604
  const past = String$1.match(calendarPastPattern)(input);
533
605
  const future = firstPatternMatch(input, calendarFuturePatterns);
534
606
  const match = Option.firstSomeOf([past, future]);
@@ -540,7 +612,7 @@ const parseCalendarOffset = (input) => {
540
612
  const direction = Option.isSome(past) ? -amount.value : amount.value;
541
613
  const noun = amount.value === 1 ? entry.singular : entry.plural;
542
614
  const canonical = direction < 0 ? `${amount.value} ${noun} geleden` : `over ${amount.value} ${noun}`;
543
- return Option.some(candidate(periodRange(relativePeriod(entry.unit, direction, canonical)), canonical));
615
+ return Option.some(relativePeriod(entry.unit, direction, canonical));
544
616
  };
545
617
  const parseRollingPeriod = (input) => {
546
618
  const singular = singularRollingPhrases.find((entry) => entry.phrase === input);
@@ -604,8 +676,6 @@ const parseDutch = (input) => {
604
676
  "sinds nu",
605
677
  "voortaan"
606
678
  ].includes(input)) return Option.some(candidate(fromNowRange(), "vanaf nu"));
607
- const offset = parseCalendarOffset(input);
608
- if (Option.isSome(offset)) return offset;
609
679
  const rolling = parseRollingPeriod(input);
610
680
  if (Option.isSome(rolling)) return rolling;
611
681
  const toDate = toDatePhrases.find((entry) => entry.phrase === input);
@@ -619,7 +689,8 @@ const parseDutch = (input) => {
619
689
  ], [
620
690
  "vanaf nu tot en met ",
621
691
  "van vandaag tot en met ",
622
- "tussen vandaag en "
692
+ "tussen vandaag en ",
693
+ "nu tot en met "
623
694
  ], parsePeriod, (period) => `vanaf ${period} tot nu toe`, (period) => `vanaf nu tot en met ${period}`);
624
695
  if (Option.isSome(nowBounded)) return nowBounded;
625
696
  const bounded = joinedPeriodCandidate(input, [
@@ -670,9 +741,12 @@ const staticPeriodPhrases = [
670
741
  ]),
671
742
  ...months.flatMap((month) => [
672
743
  month,
744
+ `vorige ${month}`,
745
+ `volgende ${month}`,
673
746
  `${month} vorig jaar`,
674
747
  `${month} volgend jaar`
675
- ])
748
+ ]),
749
+ ...nextWeekdayPhrases
676
750
  ];
677
751
  const staticPeriods = periodsFromPhrases(staticPeriodPhrases, parsePeriod);
678
752
  const boundaryPrefixes = [
@@ -719,6 +793,16 @@ const suggestDutch = (input, limit) => {
719
793
  ], limit);
720
794
  };
721
795
  const renderDutch = (range) => {
796
+ const shifted = decomposeShiftedPeriodRange(range);
797
+ if (Option.isSome(shifted)) {
798
+ const base = renderDutch(shifted.value.baseRange);
799
+ const entry = units.find((unit) => unit.unit === shifted.value.unit);
800
+ if (Option.isSome(base) && entry !== void 0) {
801
+ const amount = Math.abs(shifted.value.amount);
802
+ const noun = amount === 1 ? entry.singular : entry.plural;
803
+ return Option.some(shifted.value.amount < 0 ? `${base.value} ${amount} ${noun} geleden` : `${base.value} over ${amount} ${noun}`);
804
+ }
805
+ }
722
806
  const offset = calendarPeriodOffset(range);
723
807
  if (Option.isSome(offset) && Math.abs(offset.value.amount) > 1) {
724
808
  const entry = units.find((unit) => unit.unit === offset.value.unit);
@@ -749,6 +833,7 @@ const DutchContribution = new BaseLanguageContribution({
749
833
  locale: "nl",
750
834
  vocabulary: [
751
835
  ...months,
836
+ ...weekdays,
752
837
  ...monthAbbreviations.flatMap((aliases) => aliases),
753
838
  ...units.flatMap((entry) => [
754
839
  entry.singular,
@@ -763,10 +848,13 @@ const DutchContribution = new BaseLanguageContribution({
763
848
  "begin",
764
849
  "binnen",
765
850
  "eind",
851
+ "en",
766
852
  "geleden",
767
853
  "komende",
768
854
  "laatste",
855
+ "met",
769
856
  "na",
857
+ "nu",
770
858
  "over",
771
859
  "rest",
772
860
  "sinds",
@@ -776,8 +864,8 @@ const DutchContribution = new BaseLanguageContribution({
776
864
  "volgend",
777
865
  "voor"
778
866
  ],
779
- normalize: normalizeNaturalText,
780
- correct: correctWhitespaceSeparatedText,
867
+ normalize: normalizeDutch,
868
+ correct: correctDutch,
781
869
  parseExact: parseDutch,
782
870
  suggest: suggestDutch,
783
871
  render: renderDutch