chronolizer 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,7 +4,7 @@ import { normalizeNaturalText } from "../natural/text.mjs";
4
4
  import { defineLanguagePlugin, languagePluginsLayer } from "../language/registry.mjs";
5
5
  import { correctWhitespaceSeparatedText } from "../natural/correction.mjs";
6
6
  import { completeNaturalPhrases, fixedCalendarPeriodPhrases, naturalCount, prefixNaturalPhrases } from "../natural/suggestion.mjs";
7
- import { calendarPeriodOffset, candidate, datedPeriods, datedQuarterPeriods, fixedDatePeriod, fixedMonthPeriod, fixedQuarterPeriod, fixedYearPeriod, fromNowRange, futurePeriod, futureRange, isoDate, joinedNowCandidate, joinedPeriodCandidate, monthOfRelativeYear, namedDatePeriod, openBoundaryCandidate, parseTrailingCount, periodBoundaryCandidate, 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
  [
@@ -50,6 +133,9 @@ const units = [
50
133
  unit: "day",
51
134
  singular: "jour",
52
135
  plural: "jours",
136
+ rollingSingular: "jour",
137
+ rollingPlural: "jours",
138
+ rollingGender: "masculine",
53
139
  current: "aujourd'hui",
54
140
  previous: "hier",
55
141
  next: "demain",
@@ -60,6 +146,9 @@ const units = [
60
146
  unit: "week",
61
147
  singular: "semaine",
62
148
  plural: "semaines",
149
+ rollingSingular: "semaine",
150
+ rollingPlural: "semaines",
151
+ rollingGender: "feminine",
63
152
  current: "cette semaine",
64
153
  previous: "la semaine dernière",
65
154
  next: "la semaine prochaine",
@@ -70,6 +159,9 @@ const units = [
70
159
  unit: "month",
71
160
  singular: "mois",
72
161
  plural: "mois",
162
+ rollingSingular: "mois",
163
+ rollingPlural: "mois",
164
+ rollingGender: "masculine",
73
165
  current: "ce mois-ci",
74
166
  previous: "le mois dernier",
75
167
  next: "le mois prochain",
@@ -80,6 +172,9 @@ const units = [
80
172
  unit: "quarter",
81
173
  singular: "trimestre",
82
174
  plural: "trimestres",
175
+ rollingSingular: "trimestre",
176
+ rollingPlural: "trimestres",
177
+ rollingGender: "masculine",
83
178
  current: "ce trimestre",
84
179
  previous: "le trimestre dernier",
85
180
  next: "le trimestre prochain",
@@ -90,6 +185,9 @@ const units = [
90
185
  unit: "year",
91
186
  singular: "an",
92
187
  plural: "ans",
188
+ rollingSingular: "année",
189
+ rollingPlural: "années",
190
+ rollingGender: "feminine",
93
191
  current: "cette année",
94
192
  previous: "l'année dernière",
95
193
  next: "l'année prochaine",
@@ -97,7 +195,41 @@ const units = [
97
195
  remaining: "reste de l'année"
98
196
  }
99
197
  ];
100
- const title = (value) => `${value.slice(0, 1).toLocaleUpperCase("fr")}${value.slice(1)}`;
198
+ const rollingArticle = (entry) => entry.rollingGender === "feminine" ? "la" : "le";
199
+ const indefiniteArticle = (entry) => entry.unit === "week" ? "une" : "un";
200
+ const rollingModifier = (entry, future, plural) => {
201
+ if (future) {
202
+ if (entry.rollingGender === "feminine") return plural ? "prochaines" : "prochaine";
203
+ return plural ? "prochains" : "prochain";
204
+ }
205
+ if (entry.rollingGender === "feminine") return plural ? "dernières" : "dernière";
206
+ return plural ? "derniers" : "dernier";
207
+ };
208
+ const withDe = (period) => {
209
+ if (period.startsWith("le ")) return `du ${period.slice(3)}`;
210
+ if (period.startsWith("les ")) return `des ${period.slice(4)}`;
211
+ if (period.startsWith("la ")) return `de la ${period.slice(3)}`;
212
+ if (period.startsWith("l'")) return `de ${period}`;
213
+ return `de ${period}`;
214
+ };
215
+ const withA = (period) => {
216
+ if (period.startsWith("le ")) return `au ${period.slice(3)}`;
217
+ if (period.startsWith("les ")) return `aux ${period.slice(4)}`;
218
+ if (period.startsWith("la ")) return `à la ${period.slice(3)}`;
219
+ if (period.startsWith("l'")) return `à ${period}`;
220
+ return `à ${period}`;
221
+ };
222
+ const afterDe = (period) => {
223
+ if (period.startsWith("du ")) return `le ${period.slice(3)}`;
224
+ if (period.startsWith("de la ")) return `la ${period.slice(6)}`;
225
+ if (period.startsWith("de l'")) return `l'${period.slice(5)}`;
226
+ return period.startsWith("de ") ? period.slice(3) : period;
227
+ };
228
+ const startsWithDay = (period) => period[0] !== void 0 && period[0] >= "0" && period[0] <= "9";
229
+ const rangeLabel = (lower, upper) => {
230
+ return `${startsWithDay(lower) ? `du ${lower}` : withDe(lower)} ${startsWithDay(upper) ? `au ${upper}` : withA(upper)}`;
231
+ };
232
+ const untilLabel = (period) => `jusqu'${withA(period)}`;
101
233
  const unitAliases = [
102
234
  ["jour", "day"],
103
235
  ["jours", "day"],
@@ -362,17 +494,30 @@ const monthNumber = (value) => {
362
494
  return short === -1 ? void 0 : short + 1;
363
495
  };
364
496
  const dateLabel = (day, month, year) => `${day === 1 ? "1er" : day} ${textAt(months, month - 1)} ${year}`;
497
+ const currentDateLabel = (day, month) => `${day === 1 ? "1er" : day} ${textAt(months, month - 1)}`;
365
498
  const parseNamedDate = (input) => {
366
- const named = String$1.match(/^(?:le )?(1er|[0-3]?\d)(?: de)? ([a-zàâçéèêëîïôûùüÿœ]+\.?)(?: de)? (\d{4})$/u)(input);
499
+ const named = String$1.match(/^(?:le )?(1er|[0-3]?\d)(?: de)? ([a-zàâçéèêëîïôûùüÿœ]+\.?),?(?: de)? (\d{4})$/u)(input);
367
500
  if (Option.isSome(named)) return namedDatePeriod(textAt(named.value, 3), textAt(named.value, 2), textAt(named.value, 1).replace(/er$/u, ""), monthNumber, dateLabel);
368
501
  const numeric = String$1.match(/^([0-3]?\d)[./-]([01]?\d)[./-](\d{4})$/u)(input);
369
- if (Option.isNone(numeric)) return Option.none();
370
- const year = validYear(textAt(numeric.value, 3));
371
- const month = Number(textAt(numeric.value, 2));
372
- const day = Number(textAt(numeric.value, 1));
373
- if (year === void 0 || month < 1 || month > 12) return Option.none();
374
- const value = isoDate(year, month, day);
375
- return isIsoDate(value) && value !== "9999-12-31" ? Option.some(fixedDatePeriod(value, dateLabel(day, month, year))) : Option.none();
502
+ if (Option.isSome(numeric)) {
503
+ const year = validYear(textAt(numeric.value, 3));
504
+ const month = Number(textAt(numeric.value, 2));
505
+ const day = Number(textAt(numeric.value, 1));
506
+ if (year !== void 0 && month >= 1 && month <= 12) {
507
+ const value = isoDate(year, month, day);
508
+ if (isIsoDate(value) && value !== "9999-12-31") return Option.some(fixedDatePeriod(value, dateLabel(day, month, year)));
509
+ }
510
+ }
511
+ const current = String$1.match(/^(?:le )?(1er|[0-3]?\d)(?: de)? ([a-zàâçéèêëîïôûùüÿœ]+\.?)$/u)(input);
512
+ if (Option.isSome(current)) return namedCurrentYearDatePeriod(textAt(current.value, 2), textAt(current.value, 1).replace(/er$/u, ""), monthNumber, currentDateLabel);
513
+ const relative = String$1.match(/^(?:le )?(1er|[0-3]?\d) (de .+|du .+)$/u)(input);
514
+ if (Option.isNone(relative)) return Option.none();
515
+ const periodText = afterDe(textAt(relative.value, 2));
516
+ const alias = periodAliases.find((entry) => entry[0] === periodText && entry[1] === "month");
517
+ if (alias === void 0) return Option.none();
518
+ const day = Number(textAt(relative.value, 1).replace(/er$/u, ""));
519
+ const month = relativePeriod(alias[1], alias[2], alias[3]);
520
+ return periodDay(month, day, `${day === 1 ? "1er" : day} ${withDe(alias[3])}`);
376
521
  };
377
522
  const quarterNumber = (value) => {
378
523
  if ((value.startsWith("q") || value.startsWith("t")) && value.length === 2) return Number(value.slice(1));
@@ -399,12 +544,13 @@ const parseQuarter = (input) => {
399
544
  const quarter = quarterNumber(textAt(standalone.value, 1));
400
545
  return quarter === void 0 ? Option.none() : Option.some(quarterOfRelativeYear(quarter, 0, `T${quarter}`));
401
546
  };
402
- const parseBasePeriod = (input) => {
403
- if (isIsoDate(input) && input !== "9999-12-31") return Option.some(fixedDatePeriod(input, input));
404
- const namedDate = parseNamedDate(input);
405
- if (Option.isSome(namedDate)) return namedDate;
406
- const quarter = parseQuarter(input);
407
- 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;
408
554
  const yearMatch = String$1.match(/^(?:(?:l')?année |annee )?(\d{4})$/u)(input);
409
555
  if (Option.isSome(yearMatch)) {
410
556
  const year = validYear(textAt(yearMatch.value, 1));
@@ -414,17 +560,31 @@ const parseBasePeriod = (input) => {
414
560
  if (Option.isSome(monthYear)) {
415
561
  const month = monthNumber(textAt(monthYear.value, 1));
416
562
  const year = validYear(textAt(monthYear.value, 2));
417
- if (month !== void 0 && year !== void 0) return Option.some(fixedMonthPeriod(year, month, `${title(textAt(months, month - 1))} ${year}`));
563
+ if (month !== void 0 && year !== void 0) return Option.some(fixedMonthPeriod(year, month, `${textAt(months, month - 1)} ${year}`));
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
+ }
418
578
  }
419
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);
420
580
  if (Option.isSome(relativeMonth)) {
421
581
  const month = monthNumber(textAt(relativeMonth.value, 1));
422
582
  const yearText = textAt(relativeMonth.value, 2);
423
583
  const direction = relativeYearDirection(yearText);
424
- if (month !== void 0) return Option.some(monthOfRelativeYear(month, direction, `${title(textAt(months, month - 1))} de ${relativeYearName(direction)}`));
584
+ if (month !== void 0) return Option.some(monthOfRelativeYear(month, direction, `${textAt(months, month - 1)} de ${relativeYearName(direction)}`));
425
585
  }
426
586
  const standaloneMonth = monthNumber(input);
427
- if (standaloneMonth !== void 0) return Option.some(monthOfRelativeYear(standaloneMonth, 0, title(textAt(months, standaloneMonth - 1))));
587
+ if (standaloneMonth !== void 0) return Option.some(monthOfRelativeYear(standaloneMonth, 0, textAt(months, standaloneMonth - 1)));
428
588
  const alias = periodAliases.find((entry) => entry[0] === input);
429
589
  if (alias !== void 0) return Option.some(relativePeriod(alias[1], alias[2], alias[3]));
430
590
  if ([
@@ -446,16 +606,35 @@ const parseBasePeriod = (input) => {
446
606
  ].includes(input)) return Option.some(relativeWeekend(1, "le week-end prochain"));
447
607
  if (input === "l'avant-dernier week-end") return Option.some(relativeWeekend(-2, input));
448
608
  if (input === "le week-end après le prochain") return Option.some(relativeWeekend(2, input));
449
- return Option.none();
609
+ const weekday = nextWeekdayPhrases.indexOf(input);
610
+ return weekday === -1 ? Option.none() : Option.some(relativeWeekday(weekday, 1, nextWeekdayPhrases[weekday] ?? input));
450
611
  };
451
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
+ }
452
627
  const edge = String$1.match(/^(?:le |la |l')?(début|debut|commencement|fin)(?: du | de la | de l'| de )(.+)$/u)(input);
453
628
  if (Option.isSome(edge)) {
454
629
  const edgeName = textAt(edge.value, 1);
455
- 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));
456
635
  if (Option.isSome(period)) {
457
636
  const isEnd = edgeName === "fin";
458
- const canonical = `${isEnd ? "fin" : "début"} de ${period.value.canonical}`;
637
+ const canonical = `${isEnd ? "fin" : "début"} ${withDe(period.value.canonical)}`;
459
638
  return Option.some(isEnd ? periodEndDay(period.value, canonical) : periodStartDay(period.value, canonical));
460
639
  }
461
640
  }
@@ -466,9 +645,30 @@ const parsePeriod = (input) => {
466
645
  "tout le ",
467
646
  "toute la "
468
647
  ].find((prefix) => input.startsWith(prefix));
469
- 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);
650
+ };
651
+ const countedUnit = (value, amount) => {
652
+ const plural = value === "mois" || value.endsWith("s");
653
+ if (value !== "mois" && plural === (amount === 1)) return void 0;
654
+ const unit = unitAliases.find((entry) => entry[0] === value)?.[1];
655
+ return unit === void 0 ? void 0 : units.find((entry) => entry.unit === unit);
656
+ };
657
+ const modifierAgreesWithNoun = (input, noun) => {
658
+ 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);
659
+ if (Option.isNone(match)) return true;
660
+ const modifier = textAt(match.value, 1);
661
+ return (noun.startsWith("semaine") || noun.startsWith("année") || noun.startsWith("annee")) === [
662
+ "dernières",
663
+ "dernieres",
664
+ "passées",
665
+ "passes",
666
+ "précédentes",
667
+ "precedentes",
668
+ "prochaines",
669
+ "suivantes"
670
+ ].includes(modifier);
470
671
  };
471
- const countedUnit = (value) => unitAliases.find((entry) => entry[0] === value)?.[1];
472
672
  const countedUnitPattern = "jour|jours|semaine|semaines|mois|trimestre|trimestres|an|ans|année|annee|années|annees";
473
673
  const compileCountedPattern = (source) => new RegExp(source.replace("UNIT", countedUnitPattern), "u");
474
674
  const calendarPastPattern = compileCountedPattern("^il y a ([1-9]\\d*) (UNIT)$");
@@ -477,38 +677,89 @@ const rollingSincePattern = compileCountedPattern("^depuis ([1-9]\\d*) (UNIT)$")
477
677
  const rollingBarePattern = compileCountedPattern("^([1-9]\\d*) (UNIT)$");
478
678
  const rollingPastPatterns = [
479
679
  compileCountedPattern("^(?:(?:les|la) )?(?:derniers|dernières|dernieres|passés|passées|passes|précédents|précédentes|precedents|precedentes) ([1-9]\\d*) (UNIT)$"),
480
- compileCountedPattern("^(?:(?:les|la) )?([1-9]\\d*) (?:derniers|dernières|dernieres|passés|passées|passes|précédents|précédentes|precedents|precedentes) (UNIT)$"),
680
+ 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)$"),
481
681
  compileCountedPattern("^([1-9]\\d*) (UNIT) (?:derniers|dernières|dernieres|passés|passées|passes|précédents|précédentes|precedents|precedentes)$")
482
682
  ];
483
683
  const rollingFuturePatterns = [
484
684
  compileCountedPattern("^(?:(?:les|la) )?(?:prochains|prochaines|suivants|suivantes) ([1-9]\\d*) (UNIT)$"),
485
- compileCountedPattern("^(?:(?:les|la) )?([1-9]\\d*) (?:prochains|prochaines|suivants|suivantes) (UNIT)$"),
685
+ compileCountedPattern("^(?:(?:les|la|au cours des|pendant les) )?([1-9]\\d*) (?:prochains|prochaines|suivants|suivantes) (UNIT)$"),
486
686
  compileCountedPattern("^([1-9]\\d*) (UNIT) (?:prochains|prochaines|suivants|suivantes|à venir)$")
487
687
  ];
488
688
  const firstPatternMatch = (input, patterns) => Option.firstSomeOf(patterns.map((pattern) => String$1.match(pattern)(input)));
489
- const isFeminineUnit = (unit) => unit === "week";
490
689
  const relativeCountPhrase = (amount, entry, future) => {
491
- const noun = amount === 1 ? entry.singular : entry.plural;
492
- let modifier = future ? "prochains" : "derniers";
493
- if (isFeminineUnit(entry.unit)) modifier = future ? "prochaines" : "dernières";
494
- return `les ${amount} ${modifier} ${noun}`;
690
+ if (amount === 1) return singularRollingCanonical(entry, future);
691
+ return `les ${amount} ${rollingModifier(entry, future, true)} ${entry.rollingPlural}`;
495
692
  };
693
+ const singularRollingCanonical = (entry, future) => {
694
+ const article = indefiniteArticle(entry);
695
+ return future ? `à partir de maintenant pendant ${article} ${entry.singular}` : `depuis ${article} ${entry.singular}`;
696
+ };
697
+ const singularRollingPhrases = units.flatMap((entry) => {
698
+ const period = `${rollingArticle(entry)} ${rollingModifier(entry, false, false)} ${entry.rollingSingular}`;
699
+ const futurePeriod = `${rollingArticle(entry)} ${rollingModifier(entry, true, false)} ${entry.rollingSingular}`;
700
+ const article = indefiniteArticle(entry);
701
+ return [
702
+ {
703
+ phrase: period,
704
+ entry,
705
+ future: false
706
+ },
707
+ {
708
+ phrase: `au cours ${withDe(period)}`,
709
+ entry,
710
+ future: false
711
+ },
712
+ {
713
+ phrase: `depuis ${article} ${entry.singular}`,
714
+ entry,
715
+ future: false
716
+ },
717
+ {
718
+ phrase: `pendant ${futurePeriod}`,
719
+ entry,
720
+ future: true
721
+ },
722
+ {
723
+ phrase: `à partir de maintenant pendant ${article} ${entry.singular}`,
724
+ entry,
725
+ future: true
726
+ }
727
+ ];
728
+ });
729
+ const singularCalendarOffsets = units.flatMap((entry) => {
730
+ const quantity = `${indefiniteArticle(entry)} ${entry.singular}`;
731
+ return [{
732
+ phrase: `il y a ${quantity}`,
733
+ entry,
734
+ direction: -1
735
+ }, {
736
+ phrase: `dans ${quantity}`,
737
+ entry,
738
+ direction: 1
739
+ }];
740
+ });
496
741
  const parseCalendarOffset = (input) => {
742
+ const singular = singularCalendarOffsets.find((entry) => entry.phrase === input);
743
+ if (singular !== void 0) return Option.some(relativePeriod(singular.entry.unit, singular.direction, singular.phrase));
497
744
  const past = String$1.match(calendarPastPattern)(input);
498
745
  const future = String$1.match(calendarFuturePattern)(input);
499
746
  const match = Option.firstSomeOf([past, future]);
500
747
  if (Option.isNone(match)) return Option.none();
501
748
  const amount = parseTrailingCount(textAt(match.value, 1));
502
- const unit = countedUnit(textAt(match.value, 2));
503
- if (Option.isNone(amount) || unit === void 0) return Option.none();
504
- const entry = units.find((item) => item.unit === unit);
749
+ if (Option.isNone(amount)) return Option.none();
750
+ const entry = countedUnit(textAt(match.value, 2), amount.value);
505
751
  if (entry === void 0) return Option.none();
506
752
  const direction = Option.isSome(past) ? -amount.value : amount.value;
507
753
  const noun = amount.value === 1 ? entry.singular : entry.plural;
508
754
  const canonical = direction < 0 ? `il y a ${amount.value} ${noun}` : `dans ${amount.value} ${noun}`;
509
- return Option.some(candidate(periodRange(relativePeriod(unit, direction, canonical)), canonical));
755
+ return Option.some(relativePeriod(entry.unit, direction, canonical));
510
756
  };
511
757
  const parseRollingPeriod = (input) => {
758
+ const singular = singularRollingPhrases.find((entry) => entry.phrase === input);
759
+ if (singular !== void 0) {
760
+ const range = singular.future ? futureRange(1, singular.entry.unit) : trailingRange(1, singular.entry.unit);
761
+ return Option.some(candidate(range, singularRollingCanonical(singular.entry, singular.future)));
762
+ }
512
763
  const since = String$1.match(rollingSincePattern)(input);
513
764
  const past = firstPatternMatch(input, rollingPastPatterns);
514
765
  const bare = String$1.match(rollingBarePattern)(input);
@@ -521,19 +772,31 @@ const parseRollingPeriod = (input) => {
521
772
  ]);
522
773
  if (Option.isNone(match)) return Option.none();
523
774
  const amount = parseTrailingCount(textAt(match.value, 1));
524
- const unit = countedUnit(textAt(match.value, 2));
525
- if (Option.isNone(amount) || unit === void 0) return Option.none();
526
- const entry = units.find((item) => item.unit === unit);
527
- if (entry === void 0) return Option.none();
775
+ if (Option.isNone(amount)) return Option.none();
776
+ const noun = textAt(match.value, 2);
777
+ const entry = countedUnit(noun, amount.value);
778
+ if (entry === void 0 || !modifierAgreesWithNoun(input, noun)) return Option.none();
528
779
  const isFuture = Option.isSome(future);
529
- const range = isFuture ? futureRange(amount.value, unit) : trailingRange(amount.value, unit);
780
+ const range = isFuture ? futureRange(amount.value, entry.unit) : trailingRange(amount.value, entry.unit);
530
781
  return Option.some(candidate(range, relativeCountPhrase(amount.value, entry, isFuture)));
531
782
  };
783
+ const parseElidedDateRange = (input) => {
784
+ const joined = String$1.match(/^(?:du (1er|[0-3]?\d) au|entre le (1er|[0-3]?\d) et le) (1er|[0-3]?\d) (.+)$/u)(input);
785
+ const dashed = String$1.match(/^(1er|[0-3]?\d)[–—-](1er|[0-3]?\d) (.+)$/u)(input);
786
+ const match = Option.firstSomeOf([joined, dashed]);
787
+ if (Option.isNone(match)) return Option.none();
788
+ const isJoined = Option.isSome(joined);
789
+ const lowerDay = textAt(match.value, 1) || textAt(match.value, 2);
790
+ const upperDay = textAt(match.value, isJoined ? 3 : 2);
791
+ const period = afterDe(textAt(match.value, isJoined ? 4 : 3));
792
+ const datePeriod = periodAliases.some((entry) => entry[0] === period && entry[1] === "month") ? withDe(period) : period;
793
+ return joinedPeriodCandidate(`du ${lowerDay} ${datePeriod} au ${upperDay} ${datePeriod}`, [["du ", " au "]], parsePeriod, rangeLabel);
794
+ };
532
795
  const boundaryCandidate = (input) => {
533
796
  const included = String$1.match(/^jusqu'à (.+) (?:inclus|incluse|inclusivement)$/u)(input);
534
797
  if (Option.isSome(included)) {
535
798
  const period = parsePeriod(textAt(included.value, 1));
536
- if (Option.isSome(period)) return Option.some(periodBoundaryCandidate(period.value, "through", `jusqu'à ${period.value.canonical}`));
799
+ if (Option.isSome(period)) return Option.some(periodBoundaryCandidate(period.value, "through", untilLabel(period.value.canonical)));
537
800
  }
538
801
  return openBoundaryCandidate(input, [
539
802
  ["jusqu'avant ", "before"],
@@ -562,12 +825,12 @@ const parseFrench = (input) => {
562
825
  "à partir de maintenant",
563
826
  "désormais"
564
827
  ].includes(input)) return Option.some(candidate(fromNowRange(), "depuis maintenant"));
565
- const offset = parseCalendarOffset(input);
566
- if (Option.isSome(offset)) return offset;
567
828
  const rolling = parseRollingPeriod(input);
568
829
  if (Option.isSome(rolling)) return rolling;
569
830
  const toDate = toDatePhrases.find((entry) => entry.phrase === input);
570
831
  if (toDate !== void 0) return Option.some(candidate(periodToDateRange(toDate.entry.unit), toDate.entry.toDate));
832
+ const elided = parseElidedDateRange(input);
833
+ if (Option.isSome(elided)) return elided;
571
834
  const nowBounded = joinedNowCandidate(input, [
572
835
  ["depuis ", " jusqu'à maintenant"],
573
836
  ["depuis ", " jusqu'à aujourd'hui"],
@@ -575,7 +838,8 @@ const parseFrench = (input) => {
575
838
  ], [
576
839
  "d'aujourd'hui à ",
577
840
  "de maintenant à ",
578
- "entre aujourd'hui et "
841
+ "entre aujourd'hui et ",
842
+ "maintenant à "
579
843
  ], parsePeriod, (period) => `depuis ${period} jusqu'à maintenant`, (period) => `de maintenant à ${period}`);
580
844
  if (Option.isSome(nowBounded)) return nowBounded;
581
845
  const bounded = joinedPeriodCandidate(input, [
@@ -588,20 +852,32 @@ const parseFrench = (input) => {
588
852
  ["", " — "],
589
853
  ["", " jusqu'à "],
590
854
  ["", " au "]
591
- ], parsePeriod, (lower, upper) => `du ${lower} au ${upper}`);
855
+ ], parsePeriod, rangeLabel);
592
856
  if (Option.isSome(bounded)) return bounded;
593
857
  const boundary = boundaryCandidate(input);
594
858
  if (Option.isSome(boundary)) return boundary;
595
- return Option.map(parsePeriod(input), (period) => candidate(periodRange(period), period.canonical));
859
+ return parsePeriod(input).pipe(Option.map((period) => candidate(periodRange(period), period.canonical)));
596
860
  };
597
- const staticPeriodPhrases = [
598
- ...periodAliases.map((entry) => entry[0]),
861
+ const weekendPhrases = [
599
862
  "ce week-end",
600
863
  "le week-end dernier",
601
864
  "le week-end prochain",
602
865
  "l'avant-dernier week-end",
603
- "le week-end après le prochain",
604
- ...periodAliases.flatMap((entry) => [`début de ${entry[0]}`, `fin de ${entry[0]}`]),
866
+ "le week-end après le prochain"
867
+ ];
868
+ const edgePeriodPhrases = [
869
+ ...units.filter((entry) => entry.unit !== "day").flatMap((entry) => [
870
+ entry.current,
871
+ entry.previous,
872
+ entry.next
873
+ ]),
874
+ ...weekendPhrases,
875
+ ...months
876
+ ].flatMap((period) => [`début ${withDe(period)}`, `fin ${withDe(period)}`]);
877
+ const staticPeriodPhrases = [
878
+ ...periodAliases.map((entry) => entry[0]),
879
+ ...weekendPhrases,
880
+ ...edgePeriodPhrases,
605
881
  ...[
606
882
  1,
607
883
  2,
@@ -615,9 +891,12 @@ const staticPeriodPhrases = [
615
891
  ]),
616
892
  ...months.flatMap((month) => [
617
893
  month,
894
+ `${month} dernier`,
895
+ `${month} prochain`,
618
896
  `${month} de l'année dernière`,
619
897
  `${month} de l'année prochaine`
620
- ])
898
+ ]),
899
+ ...nextWeekdayPhrases
621
900
  ];
622
901
  const staticPeriods = periodsFromPhrases(staticPeriodPhrases, parsePeriod);
623
902
  const boundaryPrefixes = [
@@ -631,15 +910,16 @@ const countedSuggestions = (input) => {
631
910
  if (amount === void 0) return [];
632
911
  return units.flatMap((entry) => {
633
912
  const noun = amount === 1 ? entry.singular : entry.plural;
634
- const past = isFeminineUnit(entry.unit) ? "dernières" : "derniers";
635
- const future = isFeminineUnit(entry.unit) ? "prochaines" : "prochains";
913
+ const rollingNoun = amount === 1 ? entry.rollingSingular : entry.rollingPlural;
914
+ const past = rollingModifier(entry, false, amount !== 1);
915
+ const future = rollingModifier(entry, true, amount !== 1);
636
916
  return [
637
917
  relativeCountPhrase(amount, entry, false),
638
- `${amount} ${past} ${noun}`,
918
+ `${amount} ${past} ${rollingNoun}`,
639
919
  `${amount} ${noun}`,
640
920
  relativeCountPhrase(amount, entry, true),
641
- `${amount} ${future} ${noun}`,
642
- `${amount} ${noun} à venir`,
921
+ `${amount} ${future} ${rollingNoun}`,
922
+ `${amount} ${rollingNoun} à venir`,
643
923
  `il y a ${amount} ${noun}`,
644
924
  `dans ${amount} ${noun}`
645
925
  ];
@@ -648,6 +928,8 @@ const countedSuggestions = (input) => {
648
928
  const frenchSuggestionPhrases = [
649
929
  ...units.map((entry) => entry.toDate),
650
930
  ...units.map((entry) => entry.remaining),
931
+ ...singularRollingPhrases.map((entry) => entry.phrase),
932
+ ...singularCalendarOffsets.map((entry) => entry.phrase),
651
933
  ...staticPeriodPhrases,
652
934
  ...prefixNaturalPhrases(staticPeriodPhrases, boundaryPrefixes),
653
935
  "jusqu'à maintenant",
@@ -663,6 +945,17 @@ const suggestFrench = (input, limit) => {
663
945
  ], limit);
664
946
  };
665
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
+ }
666
959
  const offset = calendarPeriodOffset(range);
667
960
  if (Option.isSome(offset) && Math.abs(offset.value.amount) > 1) {
668
961
  const entry = units.find((unit) => unit.unit === offset.value.unit);
@@ -675,24 +968,26 @@ const renderFrench = (range) => {
675
968
  const future = futurePeriod(range);
676
969
  if (Option.isSome(future)) {
677
970
  const entry = units.find((unit) => unit.unit === future.value.unit);
678
- if (entry !== void 0) {
679
- const noun = future.value.amount === 1 ? entry.singular : entry.plural;
680
- return Option.some(`${future.value.amount} ${noun} à venir`);
681
- }
971
+ if (entry !== void 0) return Option.some(future.value.amount === 1 ? singularRollingCanonical(entry, true) : relativeCountPhrase(future.value.amount, entry, true));
682
972
  }
683
973
  const trailing = trailingPeriod(range);
684
974
  if (Option.isSome(trailing)) {
685
975
  const entry = units.find((unit) => unit.unit === trailing.value.unit);
686
- if (entry !== void 0) return Option.some(relativeCountPhrase(trailing.value.amount, entry, false));
976
+ if (entry !== void 0) return Option.some(trailing.value.amount === 1 ? singularRollingCanonical(entry, false) : relativeCountPhrase(trailing.value.amount, entry, false));
687
977
  }
688
- const periods = [...staticPeriods, ...periodsFromPhrases([...datedPeriods(range, months), ...datedQuarterPeriods(range)], parsePeriod)];
689
- 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}`, (period) => `jusqu'à ${period}`, (period) => `après ${period}`, (lower, upper) => `du ${lower} au ${upper}`, (period) => `depuis ${period} jusqu'à maintenant`, (period) => `de maintenant à ${period}`, () => "jusqu'à maintenant", () => "depuis maintenant");
978
+ const periods = [
979
+ ...staticPeriods,
980
+ ...currentYearDatePeriods(range, currentDateLabel),
981
+ ...periodsFromPhrases([...datedPeriods(range, months), ...datedQuarterPeriods(range)], parsePeriod)
982
+ ];
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");
690
984
  };
691
- const normalizeFrench = (input, locale) => normalizeNaturalText(input, locale).replaceAll("’", "'");
985
+ const normalizeFrench = (input, locale) => normalizeFrenchCounts(normalizeNaturalText(input, locale).replaceAll("’", "'"));
692
986
  const FrenchContribution = new BaseLanguageContribution({
693
987
  locale: "fr",
694
988
  vocabulary: [
695
989
  ...months,
990
+ ...weekdays,
696
991
  ...monthAbbreviations.flatMap((aliases) => aliases),
697
992
  ...units.flatMap((entry) => [
698
993
  entry.singular,
@@ -723,7 +1018,7 @@ const FrenchContribution = new BaseLanguageContribution({
723
1018
  "jusqu'à"
724
1019
  ],
725
1020
  normalize: normalizeFrench,
726
- correct: correctWhitespaceSeparatedText,
1021
+ correct: correctFrench,
727
1022
  parseExact: parseFrench,
728
1023
  suggest: suggestFrench,
729
1024
  render: renderFrench