chronolizer 0.3.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 { 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";
7
+ import { absoluteDatePeriod, calendarPeriodOffset, candidate, compileCountAliasNormalizer, compoundCountAliases, countAliasVocabulary, currentYearDatePeriods, datedPeriods, datedQuarterPeriods, decimalTens, decomposeShiftedPeriodRange, fixedDatePeriod, fixedMonthPeriod, fixedQuarterPeriod, fixedYearPeriod, fromNowRange, futurePeriod, futureRange, isoDate, joinedNowCandidate, joinedPeriodCandidate, monthOfRelativeYear, namedCurrentYearDatePeriod, namedDatePeriod, openBoundaryCandidate, parseTrailingCount, periodBoundaryCandidate, 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/es.ts
10
10
  const months = [
@@ -21,6 +21,99 @@ const months = [
21
21
  "noviembre",
22
22
  "diciembre"
23
23
  ];
24
+ const weekdays = [
25
+ "lunes",
26
+ "martes",
27
+ "miércoles",
28
+ "jueves",
29
+ "viernes",
30
+ "sábado",
31
+ "domingo"
32
+ ];
33
+ const nextWeekdays = weekdays.flatMap((weekday, day) => [
34
+ {
35
+ phrase: `próximo ${weekday}`,
36
+ day,
37
+ canonical: `el próximo ${weekday}`
38
+ },
39
+ {
40
+ phrase: `el próximo ${weekday}`,
41
+ day,
42
+ canonical: `el próximo ${weekday}`
43
+ },
44
+ {
45
+ phrase: `proximo ${weekday}`,
46
+ day,
47
+ canonical: `el próximo ${weekday}`
48
+ },
49
+ {
50
+ phrase: `el proximo ${weekday}`,
51
+ day,
52
+ canonical: `el próximo ${weekday}`
53
+ }
54
+ ]);
55
+ const spanishCountWords = [
56
+ ["dos"],
57
+ ["tres"],
58
+ ["cuatro"],
59
+ ["cinco"],
60
+ ["seis"],
61
+ ["siete"],
62
+ ["ocho"],
63
+ ["nueve"],
64
+ ["diez"],
65
+ ["once"],
66
+ ["doce"],
67
+ ["trece"],
68
+ ["catorce"],
69
+ ["quince"],
70
+ ["dieciséis", "dieciseis"],
71
+ ["diecisiete"],
72
+ ["dieciocho"],
73
+ ["diecinueve"],
74
+ ["veinte"]
75
+ ];
76
+ const spanishCountOnes = [
77
+ [1, "uno"],
78
+ [2, "dos"],
79
+ [3, "tres"],
80
+ [4, "cuatro"],
81
+ [5, "cinco"],
82
+ [6, "seis"],
83
+ [7, "siete"],
84
+ [8, "ocho"],
85
+ [9, "nueve"]
86
+ ];
87
+ const spanishCountAliases = [...sequentialCountAliases(spanishCountWords, 2), ...compoundCountAliases(decimalTens([
88
+ "veinte",
89
+ "treinta",
90
+ "cuarenta",
91
+ "cincuenta",
92
+ "sesenta",
93
+ "setenta",
94
+ "ochenta",
95
+ "noventa"
96
+ ]), spanishCountOnes, (ten, one, amount) => {
97
+ if (amount === 21) return [
98
+ "veintiuno",
99
+ "veintiún",
100
+ "veintiun",
101
+ "veintiuna"
102
+ ];
103
+ if (amount === 22) return ["veintidós", "veintidos"];
104
+ if (amount === 23) return ["veintitrés", "veintitres"];
105
+ if (amount === 26) return ["veintiséis", "veintiseis"];
106
+ if (amount < 30) return [`veinti${one}`];
107
+ return (one === "uno" ? [
108
+ one,
109
+ "un",
110
+ "una"
111
+ ] : [one]).map((variant) => `${ten} y ${variant}`);
112
+ })];
113
+ const normalizeSpanishCounts = compileCountAliasNormalizer(spanishCountAliases);
114
+ const spanishCountVocabulary = new Set(countAliasVocabulary(spanishCountAliases));
115
+ const correctSpanish = (input, vocabulary) => correctWhitespaceSeparatedText(input, vocabulary, spanishCountVocabulary);
116
+ const normalizeSpanish = (input, locale) => normalizeSpanishCounts(normalizeNaturalText(input, locale));
24
117
  const monthAbbreviations = [
25
118
  ["ene"],
26
119
  ["feb"],
@@ -513,13 +606,13 @@ const parseQuarter = (input) => {
513
606
  const quarter = quarterNumber(textAt(standalone.value, 1));
514
607
  return quarter === void 0 ? Option.none() : Option.some(quarterOfRelativeYear(quarter, 0, `T${quarter}`));
515
608
  };
516
- const parseBasePeriod = (input) => {
517
- const absoluteDate = absoluteDatePeriod(input, "es");
518
- if (Option.isSome(absoluteDate)) return absoluteDate;
519
- const namedDate = parseNamedDate(input);
520
- if (Option.isSome(namedDate)) return namedDate;
521
- const quarter = parseQuarter(input);
522
- if (Option.isSome(quarter)) return quarter;
609
+ const parseDatedPeriod = (input) => {
610
+ const knownPeriod = Option.firstSomeOf([
611
+ absoluteDatePeriod(input, "es"),
612
+ parseNamedDate(input),
613
+ parseQuarter(input)
614
+ ]);
615
+ if (Option.isSome(knownPeriod)) return knownPeriod;
523
616
  const yearMatch = String$1.match(/^(?:(?:el )?año )?(\d{4})$/u)(input);
524
617
  if (Option.isSome(yearMatch)) {
525
618
  const year = validYear(textAt(yearMatch.value, 1));
@@ -531,6 +624,26 @@ const parseBasePeriod = (input) => {
531
624
  const year = validYear(textAt(monthYear.value, 2));
532
625
  if (month !== void 0 && year !== void 0) return Option.some(fixedMonthPeriod(year, month, `${textAt(months, month - 1)} de ${year}`));
533
626
  }
627
+ return Option.none();
628
+ };
629
+ const parseBasePeriod = (input) => {
630
+ const datedPeriod = parseDatedPeriod(input);
631
+ if (Option.isSome(datedPeriod)) return datedPeriod;
632
+ const prefixedRelativeMonth = String$1.match(/^(pasado|próximo|proximo|este) ([a-záéíóúñ]+\.?)$/u)(input);
633
+ const suffixedRelativeMonth = String$1.match(/^([a-záéíóúñ]+\.?) (pasado|próximo|proximo)$/u)(input);
634
+ const shortRelativeMonth = Option.firstSomeOf([prefixedRelativeMonth, suffixedRelativeMonth]);
635
+ if (Option.isSome(shortRelativeMonth)) {
636
+ const prefixed = Option.isSome(prefixedRelativeMonth);
637
+ const month = monthNumber(textAt(shortRelativeMonth.value, prefixed ? 2 : 1));
638
+ if (month !== void 0) {
639
+ const modifier = textAt(shortRelativeMonth.value, prefixed ? 1 : 2);
640
+ const direction = relativeYearDirection(modifier);
641
+ let canonicalModifier = "este";
642
+ if (direction < 0) canonicalModifier = "pasado";
643
+ if (direction > 0) canonicalModifier = "próximo";
644
+ return Option.some(monthOfRelativeYear(month, direction, `${canonicalModifier} ${textAt(months, month - 1)}`));
645
+ }
646
+ }
534
647
  const relativeMonth = String$1.match(/^([a-záéíóúñ]+\.?) (del año pasado|del ano pasado|del próximo año|del proximo año|de este año|de este ano)$/u)(input);
535
648
  if (Option.isSome(relativeMonth)) {
536
649
  const month = monthNumber(textAt(relativeMonth.value, 1));
@@ -554,13 +667,31 @@ const parseBasePeriod = (input) => {
554
667
  if (["el próximo fin de semana", "el proximo fin de semana"].includes(input)) return Option.some(relativeWeekend(1, "el próximo fin de semana"));
555
668
  if (input === "el fin de semana anterior al pasado") return Option.some(relativeWeekend(-2, input));
556
669
  if (input === "el fin de semana después del próximo") return Option.some(relativeWeekend(2, input));
557
- return Option.none();
670
+ const weekday = nextWeekdays.find((entry) => entry.phrase === input);
671
+ return weekday === void 0 ? Option.none() : Option.some(relativeWeekday(weekday.day, 1, weekday.canonical));
558
672
  };
559
673
  const parsePeriod = (input) => {
674
+ const shifted = String$1.match(/^(.+) (hace|dentro de|en) ([1-9]\d*) (día|dia|días|dias|semana|semanas|mes|meses|trimestre|trimestres|año|años|ano|anos)$/u)(input);
675
+ if (Option.isSome(shifted)) {
676
+ const amount = parseTrailingCount(textAt(shifted.value, 3));
677
+ const alias = unitAliases.find((unit) => unit[0] === textAt(shifted.value, 4));
678
+ const entry = alias === void 0 ? void 0 : units.find((unit) => unit.unit === alias[1]);
679
+ const period = parsePeriod(textAt(shifted.value, 1));
680
+ if (Option.isSome(amount) && entry !== void 0 && Option.isSome(period)) {
681
+ const past = textAt(shifted.value, 2) === "hace";
682
+ const direction = past ? -amount.value : amount.value;
683
+ const noun = amount.value === 1 ? entry.singular : entry.plural;
684
+ const canonical = `${period.value.canonical} ${past ? "hace" : "dentro de"} ${amount.value} ${noun}`;
685
+ return Option.some(shiftPeriod(period.value, direction, entry.unit, canonical));
686
+ }
687
+ }
560
688
  const edge = String$1.match(/^(?:el )?(inicio|comienzo|principio|fin|final)(?: de| del) (.+)$/u)(input);
561
689
  if (Option.isSome(edge)) {
562
690
  const edgeName = textAt(edge.value, 1);
563
- const period = parseBasePeriod(textAt(edge.value, 2));
691
+ const periodText = textAt(edge.value, 2);
692
+ const basePeriod = parseBasePeriod(periodText);
693
+ const implicit = units.find((entry) => entry.singular === periodText);
694
+ const period = Option.isSome(basePeriod) || implicit === void 0 ? basePeriod : Option.some(relativePeriod(implicit.unit, 0, implicit.current));
564
695
  if (Option.isSome(period)) {
565
696
  const isEnd = edgeName === "fin" || edgeName === "final";
566
697
  const canonical = `${isEnd ? "final" : "inicio"} ${withDe(period.value.canonical)}`;
@@ -574,7 +705,8 @@ const parsePeriod = (input) => {
574
705
  "todo el ",
575
706
  "toda la "
576
707
  ].find((prefix) => input.startsWith(prefix));
577
- return parseBasePeriod(wrapper === void 0 ? input : input.slice(wrapper.length));
708
+ const base = parseBasePeriod(wrapper === void 0 ? input : input.slice(wrapper.length));
709
+ return Option.isSome(base) ? base : parseCalendarOffset(input);
578
710
  };
579
711
  const countedUnit = (value, amount) => {
580
712
  const alias = unitAliases.find((entry) => entry[0] === value && entry[2] === (amount !== 1));
@@ -609,7 +741,7 @@ const parseCalendarOffset = (input) => {
609
741
  const noun = amount.value === 1 ? entry.singular : entry.plural;
610
742
  const canonical = `${isPast ? "hace" : "dentro de"} ${quantity} ${noun}`;
611
743
  const direction = isPast ? -amount.value : amount.value;
612
- return Option.some(candidate(periodRange(relativePeriod(entry.unit, direction, canonical)), canonical));
744
+ return Option.some(relativePeriod(entry.unit, direction, canonical));
613
745
  };
614
746
  const rollingCanonical = (entry, amount, future) => {
615
747
  if (amount > 1) return `${rollingAdjective(entry, future, true)} ${amount} ${entry.plural}`;
@@ -617,7 +749,7 @@ const rollingCanonical = (entry, amount, future) => {
617
749
  return future ? `desde ahora durante ${article} ${entry.singular}` : `desde hace ${article} ${entry.singular}`;
618
750
  };
619
751
  const rollingCandidate = (entry, amount, future) => candidate(future ? futureRange(amount, entry.unit) : trailingRange(amount, entry.unit), rollingCanonical(entry, amount, future));
620
- const parseRollingPeriod = (input) => {
752
+ const parseRollingSince = (input) => {
621
753
  const since = String$1.match(/^desde hace ([1-9]\d*|un|una) (día|dia|días|dias|semana|semanas|mes|meses|trimestre|trimestres|año|años|ano|anos)$/u)(input);
622
754
  if (Option.isSome(since)) {
623
755
  const amount = writtenAmount(textAt(since.value, 1));
@@ -627,6 +759,9 @@ const parseRollingPeriod = (input) => {
627
759
  if (entry !== void 0 && (amountText !== "un" && amountText !== "una" || agreesWithIndefiniteArticle(entry, amountText))) return Option.some(rollingCandidate(entry, amount.value, false));
628
760
  }
629
761
  }
762
+ return Option.none();
763
+ };
764
+ const parseSingularRolling = (input) => {
630
765
  const futureSingular = String$1.match(/^desde ahora durante (un|una) (día|dia|semana|mes|trimestre|año|ano)$/u)(input);
631
766
  if (Option.isSome(futureSingular)) {
632
767
  const entry = countedUnit(textAt(futureSingular.value, 2), 1);
@@ -644,6 +779,9 @@ const parseRollingPeriod = (input) => {
644
779
  const modifier = textAt(nextSingular.value, 2);
645
780
  if (entry !== void 0 && agreesWithArticle(entry, textAt(nextSingular.value, 1)) && agreesWithSingularModifier(entry, modifier, true)) return Option.some(rollingCandidate(entry, 1, true));
646
781
  }
782
+ return Option.none();
783
+ };
784
+ const parseCountedRolling = (input) => {
647
785
  const counted = String$1.match(/^(?:(?:durante|en) )?(?:(los|las) )?(últimos|ultimos|últimas|ultimas|pasados|pasadas|anteriores|próximos|proximos|próximas|proximas|siguientes) ([1-9]\d*) (día|dia|días|dias|semana|semanas|mes|meses|trimestre|trimestres|año|años|ano|anos)$/u)(input);
648
786
  if (Option.isSome(counted)) {
649
787
  const amount = parseTrailingCount(textAt(counted.value, 3));
@@ -661,6 +799,11 @@ const parseRollingPeriod = (input) => {
661
799
  const entry = countedUnit(textAt(bare.value, 2), amount.value);
662
800
  return entry === void 0 ? Option.none() : Option.some(rollingCandidate(entry, amount.value, false));
663
801
  };
802
+ const parseRollingPeriod = (input) => Option.firstSomeOf([
803
+ parseRollingSince(input),
804
+ parseSingularRolling(input),
805
+ parseCountedRolling(input)
806
+ ]);
664
807
  const parseElidedDateRange = (input) => {
665
808
  const joined = String$1.match(/^(?:(?:del|desde(?: el)?) ([0-3]?\d) (?:al|hasta(?: el)?)|entre(?: el)? ([0-3]?\d) y(?: el)?) ([0-3]?\d) (.+)$/u)(input);
666
809
  const dashed = String$1.match(/^([0-3]?\d)[–—-]([0-3]?\d) (.+)$/u)(input);
@@ -698,8 +841,6 @@ const parseSpanish = (input) => {
698
841
  "a partir de ahora",
699
842
  "de ahora en adelante"
700
843
  ].includes(input)) return Option.some(candidate(fromNowRange(), "desde ahora"));
701
- const offset = parseCalendarOffset(input);
702
- if (Option.isSome(offset)) return offset;
703
844
  const rolling = parseRollingPeriod(input);
704
845
  if (Option.isSome(rolling)) return rolling;
705
846
  const toDate = toDatePhrases.find((entry) => entry.phrase === input);
@@ -714,7 +855,8 @@ const parseSpanish = (input) => {
714
855
  "desde ahora hasta ",
715
856
  "desde hoy hasta ",
716
857
  "entre hoy y ",
717
- "entre hoy y el "
858
+ "entre hoy y el ",
859
+ "ahora hasta "
718
860
  ], parsePeriod, (period) => `desde ${period} hasta ahora`, (period) => `desde ahora hasta ${period}`);
719
861
  if (Option.isSome(nowBounded)) return nowBounded;
720
862
  const bounded = joinedPeriodCandidate(input, [
@@ -769,9 +911,14 @@ const staticPeriodPhrases = [
769
911
  ]),
770
912
  ...months.flatMap((month) => [
771
913
  month,
914
+ `pasado ${month}`,
915
+ `próximo ${month}`,
916
+ `${month} pasado`,
917
+ `${month} próximo`,
772
918
  `${month} del año pasado`,
773
919
  `${month} del próximo año`
774
- ])
920
+ ]),
921
+ ...nextWeekdays.map((entry) => entry.phrase)
775
922
  ];
776
923
  const staticPeriods = periodsFromPhrases(staticPeriodPhrases, parsePeriod);
777
924
  const boundaryPrefixes = [
@@ -830,6 +977,17 @@ const suggestSpanish = (input, limit) => {
830
977
  ], limit);
831
978
  };
832
979
  const renderSpanish = (range) => {
980
+ const shifted = decomposeShiftedPeriodRange(range);
981
+ if (Option.isSome(shifted)) {
982
+ const base = renderSpanish(shifted.value.baseRange);
983
+ const entry = units.find((unit) => unit.unit === shifted.value.unit);
984
+ if (Option.isSome(base) && entry !== void 0) {
985
+ const amount = Math.abs(shifted.value.amount);
986
+ const noun = amount === 1 ? entry.singular : entry.plural;
987
+ const direction = shifted.value.amount < 0 ? "hace" : "dentro de";
988
+ return Option.some(`${base.value} ${direction} ${amount} ${noun}`);
989
+ }
990
+ }
833
991
  const offset = calendarPeriodOffset(range);
834
992
  if (Option.isSome(offset) && Math.abs(offset.value.amount) > 1) {
835
993
  const entry = units.find((unit) => unit.unit === offset.value.unit);
@@ -860,6 +1018,7 @@ const SpanishContribution = new BaseLanguageContribution({
860
1018
  locale: "es",
861
1019
  vocabulary: [
862
1020
  ...months,
1021
+ ...weekdays,
863
1022
  ...monthAbbreviations.flatMap((aliases) => aliases),
864
1023
  ...units.flatMap((entry) => [
865
1024
  entry.singular,
@@ -893,8 +1052,8 @@ const SpanishContribution = new BaseLanguageContribution({
893
1052
  "resto",
894
1053
  "últimos"
895
1054
  ],
896
- normalize: normalizeNaturalText,
897
- correct: correctWhitespaceSeparatedText,
1055
+ normalize: normalizeSpanish,
1056
+ correct: correctSpanish,
898
1057
  parseExact: parseSpanish,
899
1058
  suggest: suggestSpanish,
900
1059
  render: renderSpanish
@@ -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 { 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";
7
+ import { absoluteDatePeriod, calendarPeriodOffset, candidate, compileCountAliasNormalizer, countAliasVocabulary, currentYearDatePeriods, datedPeriods, datedQuarterPeriods, decomposeShiftedPeriodRange, fixedDatePeriod, fixedMonthPeriod, fixedQuarterPeriod, fixedYearPeriod, fromNowRange, futurePeriod, futureRange, isoDate, 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
8
  import { Effect, Option, String as String$1 } from "effect";
9
9
  //#region src/locales/fr.ts
10
10
  const months = [
@@ -21,6 +21,89 @@ const months = [
21
21
  "novembre",
22
22
  "décembre"
23
23
  ];
24
+ const weekdays = [
25
+ "lundi",
26
+ "mardi",
27
+ "mercredi",
28
+ "jeudi",
29
+ "vendredi",
30
+ "samedi",
31
+ "dimanche"
32
+ ];
33
+ const nextWeekdayPhrases = weekdays.map((weekday) => `${weekday} prochain`);
34
+ const frenchCountWords = [
35
+ ["deux"],
36
+ ["trois"],
37
+ ["quatre"],
38
+ ["cinq"],
39
+ ["six"],
40
+ ["sept"],
41
+ ["huit"],
42
+ ["neuf"],
43
+ ["dix"],
44
+ ["onze"],
45
+ ["douze"],
46
+ ["treize"],
47
+ ["quatorze"],
48
+ ["quinze"],
49
+ ["seize"],
50
+ ["dix-sept"],
51
+ ["dix-huit"],
52
+ ["dix-neuf"],
53
+ ["vingt"]
54
+ ];
55
+ const frenchCompoundCountAliases = () => {
56
+ const words = [
57
+ "",
58
+ "un",
59
+ "deux",
60
+ "trois",
61
+ "quatre",
62
+ "cinq",
63
+ "six",
64
+ "sept",
65
+ "huit",
66
+ "neuf",
67
+ "dix",
68
+ "onze",
69
+ "douze",
70
+ "treize",
71
+ "quatorze",
72
+ "quinze",
73
+ "seize",
74
+ "dix-sept",
75
+ "dix-huit",
76
+ "dix-neuf"
77
+ ];
78
+ const aliases = [];
79
+ for (let amount = 21; amount <= 99; amount += 1) {
80
+ const tens = Math.floor(amount / 10);
81
+ const remainder = amount % 10;
82
+ let phrase = "";
83
+ if (tens <= 6) {
84
+ const tensWord = [
85
+ "",
86
+ "",
87
+ "vingt",
88
+ "trente",
89
+ "quarante",
90
+ "cinquante",
91
+ "soixante"
92
+ ][tens];
93
+ if (tensWord === void 0) continue;
94
+ 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
  [
@@ -461,13 +544,13 @@ const parseQuarter = (input) => {
461
544
  const quarter = quarterNumber(textAt(standalone.value, 1));
462
545
  return quarter === void 0 ? Option.none() : Option.some(quarterOfRelativeYear(quarter, 0, `T${quarter}`));
463
546
  };
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;
547
+ const parseDatedPeriod = (input) => {
548
+ const knownPeriod = Option.firstSomeOf([
549
+ absoluteDatePeriod(input, "fr"),
550
+ parseNamedDate(input),
551
+ parseQuarter(input)
552
+ ]);
553
+ if (Option.isSome(knownPeriod)) return knownPeriod;
471
554
  const yearMatch = String$1.match(/^(?:(?:l')?année |annee )?(\d{4})$/u)(input);
472
555
  if (Option.isSome(yearMatch)) {
473
556
  const year = validYear(textAt(yearMatch.value, 1));
@@ -479,6 +562,20 @@ const parseBasePeriod = (input) => {
479
562
  const year = validYear(textAt(monthYear.value, 2));
480
563
  if (month !== void 0 && year !== void 0) return Option.some(fixedMonthPeriod(year, month, `${textAt(months, month - 1)} ${year}`));
481
564
  }
565
+ return Option.none();
566
+ };
567
+ const parseBasePeriod = (input) => {
568
+ const datedPeriod = parseDatedPeriod(input);
569
+ if (Option.isSome(datedPeriod)) return datedPeriod;
570
+ const suffixedRelativeMonth = String$1.match(/^([a-zàâçéèêëîïôûùüÿœ]+\.?) (dernier|prochain)$/u)(input);
571
+ if (Option.isSome(suffixedRelativeMonth)) {
572
+ const month = monthNumber(textAt(suffixedRelativeMonth.value, 1));
573
+ if (month !== void 0) {
574
+ const modifier = textAt(suffixedRelativeMonth.value, 2);
575
+ const direction = modifier === "dernier" ? -1 : 1;
576
+ return Option.some(monthOfRelativeYear(month, direction, `${textAt(months, month - 1)} ${modifier}`));
577
+ }
578
+ }
482
579
  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
580
  if (Option.isSome(relativeMonth)) {
484
581
  const month = monthNumber(textAt(relativeMonth.value, 1));
@@ -509,13 +606,32 @@ const parseBasePeriod = (input) => {
509
606
  ].includes(input)) return Option.some(relativeWeekend(1, "le week-end prochain"));
510
607
  if (input === "l'avant-dernier week-end") return Option.some(relativeWeekend(-2, input));
511
608
  if (input === "le week-end après le prochain") return Option.some(relativeWeekend(2, input));
512
- return Option.none();
609
+ const weekday = nextWeekdayPhrases.indexOf(input);
610
+ return weekday === -1 ? Option.none() : Option.some(relativeWeekday(weekday, 1, nextWeekdayPhrases[weekday] ?? input));
513
611
  };
514
612
  const parsePeriod = (input) => {
613
+ 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);
614
+ if (Option.isSome(shifted)) {
615
+ const amount = parseTrailingCount(textAt(shifted.value, 3));
616
+ const alias = unitAliases.find((unit) => unit[0] === textAt(shifted.value, 4));
617
+ const entry = alias === void 0 ? void 0 : units.find((unit) => unit.unit === alias[1]);
618
+ const period = parsePeriod(textAt(shifted.value, 1));
619
+ if (Option.isSome(amount) && entry !== void 0 && Option.isSome(period)) {
620
+ const past = textAt(shifted.value, 2) === "il y a";
621
+ const direction = past ? -amount.value : amount.value;
622
+ const noun = amount.value === 1 ? entry.singular : entry.plural;
623
+ const canonical = `${period.value.canonical} ${past ? "il y a" : "dans"} ${amount.value} ${noun}`;
624
+ return Option.some(shiftPeriod(period.value, direction, entry.unit, canonical));
625
+ }
626
+ }
515
627
  const edge = String$1.match(/^(?:le |la |l')?(début|debut|commencement|fin)(?: du | de la | de l'| de )(.+)$/u)(input);
516
628
  if (Option.isSome(edge)) {
517
629
  const edgeName = textAt(edge.value, 1);
518
- const period = parseBasePeriod(textAt(edge.value, 2));
630
+ const periodText = textAt(edge.value, 2);
631
+ const basePeriod = parseBasePeriod(periodText);
632
+ const implicitUnit = unitAliases.find((entry) => entry[0] === periodText)?.[1];
633
+ const implicit = units.find((entry) => entry.unit === implicitUnit);
634
+ const period = Option.isSome(basePeriod) || implicit === void 0 ? basePeriod : Option.some(relativePeriod(implicit.unit, 0, implicit.current));
519
635
  if (Option.isSome(period)) {
520
636
  const isEnd = edgeName === "fin";
521
637
  const canonical = `${isEnd ? "fin" : "début"} ${withDe(period.value.canonical)}`;
@@ -529,7 +645,8 @@ const parsePeriod = (input) => {
529
645
  "tout le ",
530
646
  "toute la "
531
647
  ].find((prefix) => input.startsWith(prefix));
532
- return parseBasePeriod(wrapper === void 0 ? input : input.slice(wrapper.length));
648
+ const base = parseBasePeriod(wrapper === void 0 ? input : input.slice(wrapper.length));
649
+ return Option.isSome(base) ? base : parseCalendarOffset(input);
533
650
  };
534
651
  const countedUnit = (value, amount) => {
535
652
  const plural = value === "mois" || value.endsWith("s");
@@ -623,7 +740,7 @@ const singularCalendarOffsets = units.flatMap((entry) => {
623
740
  });
624
741
  const parseCalendarOffset = (input) => {
625
742
  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));
743
+ if (singular !== void 0) return Option.some(relativePeriod(singular.entry.unit, singular.direction, singular.phrase));
627
744
  const past = String$1.match(calendarPastPattern)(input);
628
745
  const future = String$1.match(calendarFuturePattern)(input);
629
746
  const match = Option.firstSomeOf([past, future]);
@@ -635,7 +752,7 @@ const parseCalendarOffset = (input) => {
635
752
  const direction = Option.isSome(past) ? -amount.value : amount.value;
636
753
  const noun = amount.value === 1 ? entry.singular : entry.plural;
637
754
  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));
755
+ return Option.some(relativePeriod(entry.unit, direction, canonical));
639
756
  };
640
757
  const parseRollingPeriod = (input) => {
641
758
  const singular = singularRollingPhrases.find((entry) => entry.phrase === input);
@@ -708,8 +825,6 @@ const parseFrench = (input) => {
708
825
  "à partir de maintenant",
709
826
  "désormais"
710
827
  ].includes(input)) return Option.some(candidate(fromNowRange(), "depuis maintenant"));
711
- const offset = parseCalendarOffset(input);
712
- if (Option.isSome(offset)) return offset;
713
828
  const rolling = parseRollingPeriod(input);
714
829
  if (Option.isSome(rolling)) return rolling;
715
830
  const toDate = toDatePhrases.find((entry) => entry.phrase === input);
@@ -723,7 +838,8 @@ const parseFrench = (input) => {
723
838
  ], [
724
839
  "d'aujourd'hui à ",
725
840
  "de maintenant à ",
726
- "entre aujourd'hui et "
841
+ "entre aujourd'hui et ",
842
+ "maintenant à "
727
843
  ], parsePeriod, (period) => `depuis ${period} jusqu'à maintenant`, (period) => `de maintenant à ${period}`);
728
844
  if (Option.isSome(nowBounded)) return nowBounded;
729
845
  const bounded = joinedPeriodCandidate(input, [
@@ -775,9 +891,12 @@ const staticPeriodPhrases = [
775
891
  ]),
776
892
  ...months.flatMap((month) => [
777
893
  month,
894
+ `${month} dernier`,
895
+ `${month} prochain`,
778
896
  `${month} de l'année dernière`,
779
897
  `${month} de l'année prochaine`
780
- ])
898
+ ]),
899
+ ...nextWeekdayPhrases
781
900
  ];
782
901
  const staticPeriods = periodsFromPhrases(staticPeriodPhrases, parsePeriod);
783
902
  const boundaryPrefixes = [
@@ -826,6 +945,17 @@ const suggestFrench = (input, limit) => {
826
945
  ], limit);
827
946
  };
828
947
  const renderFrench = (range) => {
948
+ const shifted = decomposeShiftedPeriodRange(range);
949
+ if (Option.isSome(shifted)) {
950
+ const base = renderFrench(shifted.value.baseRange);
951
+ const entry = units.find((unit) => unit.unit === shifted.value.unit);
952
+ if (Option.isSome(base) && entry !== void 0) {
953
+ const amount = Math.abs(shifted.value.amount);
954
+ const noun = amount === 1 ? entry.singular : entry.plural;
955
+ const direction = shifted.value.amount < 0 ? "il y a" : "dans";
956
+ return Option.some(`${base.value} ${direction} ${amount} ${noun}`);
957
+ }
958
+ }
829
959
  const offset = calendarPeriodOffset(range);
830
960
  if (Option.isSome(offset) && Math.abs(offset.value.amount) > 1) {
831
961
  const entry = units.find((unit) => unit.unit === offset.value.unit);
@@ -852,11 +982,12 @@ const renderFrench = (range) => {
852
982
  ];
853
983
  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
984
  };
855
- const normalizeFrench = (input, locale) => normalizeNaturalText(input, locale).replaceAll("’", "'");
985
+ const normalizeFrench = (input, locale) => normalizeFrenchCounts(normalizeNaturalText(input, locale).replaceAll("’", "'"));
856
986
  const FrenchContribution = new BaseLanguageContribution({
857
987
  locale: "fr",
858
988
  vocabulary: [
859
989
  ...months,
990
+ ...weekdays,
860
991
  ...monthAbbreviations.flatMap((aliases) => aliases),
861
992
  ...units.flatMap((entry) => [
862
993
  entry.singular,
@@ -887,7 +1018,7 @@ const FrenchContribution = new BaseLanguageContribution({
887
1018
  "jusqu'à"
888
1019
  ],
889
1020
  normalize: normalizeFrench,
890
- correct: correctWhitespaceSeparatedText,
1021
+ correct: correctFrench,
891
1022
  parseExact: parseFrench,
892
1023
  suggest: suggestFrench,
893
1024
  render: renderFrench