chronolizer 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,7 +4,7 @@ import { normalizeNaturalText } from "../natural/text.mjs";
4
4
  import { defineLanguagePlugin, languagePluginsLayer } from "../language/registry.mjs";
5
5
  import { correctWhitespaceSeparatedText } from "../natural/correction.mjs";
6
6
  import { completeNaturalPhrases, fixedCalendarPeriodPhrases, naturalCount, prefixNaturalPhrases } from "../natural/suggestion.mjs";
7
- import { calendarPeriodOffset, candidate, datedPeriods, datedQuarterPeriods, fixedDatePeriod, fixedMonthPeriod, fixedQuarterPeriod, fixedYearPeriod, fromNowRange, futurePeriod, futureRange, isoDate, joinedNowCandidate, joinedPeriodCandidate, monthOfRelativeYear, namedDatePeriod, openBoundaryCandidate, parseTrailingCount, periodBoundaryCandidate, periodEndDay, periodRange, periodStartDay, periodToDateRange, periodsFromPhrases, quarterOfRelativeYear, relativePeriod, relativeWeekend, remainingPeriodRange, renderPeriodRange, textAt, trailingPeriod, trailingRange, untilNowRange, validYear } from "./shared.mjs";
7
+ import { absoluteDatePeriod, calendarPeriodOffset, candidate, currentYearDatePeriods, datedPeriods, datedQuarterPeriods, fixedDatePeriod, fixedMonthPeriod, fixedQuarterPeriod, fixedYearPeriod, fromNowRange, futurePeriod, futureRange, isoDate, joinedNowCandidate, joinedPeriodCandidate, monthOfRelativeYear, namedCurrentYearDatePeriod, namedDatePeriod, openBoundaryCandidate, parseTrailingCount, periodBoundaryCandidate, periodDay, periodEndDay, periodRange, periodStartDay, periodToDateRange, periodsFromPhrases, quarterOfRelativeYear, relativePeriod, relativeWeekend, remainingPeriodRange, renderPeriodRange, textAt, trailingPeriod, trailingRange, untilNowRange, validYear } from "./shared.mjs";
8
8
  import { Effect, Option, String as String$1 } from "effect";
9
9
  //#region src/locales/fr.ts
10
10
  const months = [
@@ -50,6 +50,9 @@ const units = [
50
50
  unit: "day",
51
51
  singular: "jour",
52
52
  plural: "jours",
53
+ rollingSingular: "jour",
54
+ rollingPlural: "jours",
55
+ rollingGender: "masculine",
53
56
  current: "aujourd'hui",
54
57
  previous: "hier",
55
58
  next: "demain",
@@ -60,6 +63,9 @@ const units = [
60
63
  unit: "week",
61
64
  singular: "semaine",
62
65
  plural: "semaines",
66
+ rollingSingular: "semaine",
67
+ rollingPlural: "semaines",
68
+ rollingGender: "feminine",
63
69
  current: "cette semaine",
64
70
  previous: "la semaine dernière",
65
71
  next: "la semaine prochaine",
@@ -70,6 +76,9 @@ const units = [
70
76
  unit: "month",
71
77
  singular: "mois",
72
78
  plural: "mois",
79
+ rollingSingular: "mois",
80
+ rollingPlural: "mois",
81
+ rollingGender: "masculine",
73
82
  current: "ce mois-ci",
74
83
  previous: "le mois dernier",
75
84
  next: "le mois prochain",
@@ -80,6 +89,9 @@ const units = [
80
89
  unit: "quarter",
81
90
  singular: "trimestre",
82
91
  plural: "trimestres",
92
+ rollingSingular: "trimestre",
93
+ rollingPlural: "trimestres",
94
+ rollingGender: "masculine",
83
95
  current: "ce trimestre",
84
96
  previous: "le trimestre dernier",
85
97
  next: "le trimestre prochain",
@@ -90,6 +102,9 @@ const units = [
90
102
  unit: "year",
91
103
  singular: "an",
92
104
  plural: "ans",
105
+ rollingSingular: "année",
106
+ rollingPlural: "années",
107
+ rollingGender: "feminine",
93
108
  current: "cette année",
94
109
  previous: "l'année dernière",
95
110
  next: "l'année prochaine",
@@ -97,7 +112,41 @@ const units = [
97
112
  remaining: "reste de l'année"
98
113
  }
99
114
  ];
100
- const title = (value) => `${value.slice(0, 1).toLocaleUpperCase("fr")}${value.slice(1)}`;
115
+ const rollingArticle = (entry) => entry.rollingGender === "feminine" ? "la" : "le";
116
+ const indefiniteArticle = (entry) => entry.unit === "week" ? "une" : "un";
117
+ const rollingModifier = (entry, future, plural) => {
118
+ if (future) {
119
+ if (entry.rollingGender === "feminine") return plural ? "prochaines" : "prochaine";
120
+ return plural ? "prochains" : "prochain";
121
+ }
122
+ if (entry.rollingGender === "feminine") return plural ? "dernières" : "dernière";
123
+ return plural ? "derniers" : "dernier";
124
+ };
125
+ const withDe = (period) => {
126
+ if (period.startsWith("le ")) return `du ${period.slice(3)}`;
127
+ if (period.startsWith("les ")) return `des ${period.slice(4)}`;
128
+ if (period.startsWith("la ")) return `de la ${period.slice(3)}`;
129
+ if (period.startsWith("l'")) return `de ${period}`;
130
+ return `de ${period}`;
131
+ };
132
+ const withA = (period) => {
133
+ if (period.startsWith("le ")) return `au ${period.slice(3)}`;
134
+ if (period.startsWith("les ")) return `aux ${period.slice(4)}`;
135
+ if (period.startsWith("la ")) return `à la ${period.slice(3)}`;
136
+ if (period.startsWith("l'")) return `à ${period}`;
137
+ return `à ${period}`;
138
+ };
139
+ const afterDe = (period) => {
140
+ if (period.startsWith("du ")) return `le ${period.slice(3)}`;
141
+ if (period.startsWith("de la ")) return `la ${period.slice(6)}`;
142
+ if (period.startsWith("de l'")) return `l'${period.slice(5)}`;
143
+ return period.startsWith("de ") ? period.slice(3) : period;
144
+ };
145
+ const startsWithDay = (period) => period[0] !== void 0 && period[0] >= "0" && period[0] <= "9";
146
+ const rangeLabel = (lower, upper) => {
147
+ return `${startsWithDay(lower) ? `du ${lower}` : withDe(lower)} ${startsWithDay(upper) ? `au ${upper}` : withA(upper)}`;
148
+ };
149
+ const untilLabel = (period) => `jusqu'${withA(period)}`;
101
150
  const unitAliases = [
102
151
  ["jour", "day"],
103
152
  ["jours", "day"],
@@ -362,17 +411,30 @@ const monthNumber = (value) => {
362
411
  return short === -1 ? void 0 : short + 1;
363
412
  };
364
413
  const dateLabel = (day, month, year) => `${day === 1 ? "1er" : day} ${textAt(months, month - 1)} ${year}`;
414
+ const currentDateLabel = (day, month) => `${day === 1 ? "1er" : day} ${textAt(months, month - 1)}`;
365
415
  const parseNamedDate = (input) => {
366
- const named = String$1.match(/^(?:le )?(1er|[0-3]?\d)(?: de)? ([a-zàâçéèêëîïôûùüÿœ]+\.?)(?: de)? (\d{4})$/u)(input);
416
+ const named = String$1.match(/^(?:le )?(1er|[0-3]?\d)(?: de)? ([a-zàâçéèêëîïôûùüÿœ]+\.?),?(?: de)? (\d{4})$/u)(input);
367
417
  if (Option.isSome(named)) return namedDatePeriod(textAt(named.value, 3), textAt(named.value, 2), textAt(named.value, 1).replace(/er$/u, ""), monthNumber, dateLabel);
368
418
  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();
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
+ }
428
+ const current = String$1.match(/^(?:le )?(1er|[0-3]?\d)(?: de)? ([a-zàâçéèêëîïôûùüÿœ]+\.?)$/u)(input);
429
+ if (Option.isSome(current)) return namedCurrentYearDatePeriod(textAt(current.value, 2), textAt(current.value, 1).replace(/er$/u, ""), monthNumber, currentDateLabel);
430
+ const relative = String$1.match(/^(?:le )?(1er|[0-3]?\d) (de .+|du .+)$/u)(input);
431
+ if (Option.isNone(relative)) return Option.none();
432
+ const periodText = afterDe(textAt(relative.value, 2));
433
+ const alias = periodAliases.find((entry) => entry[0] === periodText && entry[1] === "month");
434
+ if (alias === void 0) return Option.none();
435
+ const day = Number(textAt(relative.value, 1).replace(/er$/u, ""));
436
+ const month = relativePeriod(alias[1], alias[2], alias[3]);
437
+ return periodDay(month, day, `${day === 1 ? "1er" : day} ${withDe(alias[3])}`);
376
438
  };
377
439
  const quarterNumber = (value) => {
378
440
  if ((value.startsWith("q") || value.startsWith("t")) && value.length === 2) return Number(value.slice(1));
@@ -400,7 +462,8 @@ const parseQuarter = (input) => {
400
462
  return quarter === void 0 ? Option.none() : Option.some(quarterOfRelativeYear(quarter, 0, `T${quarter}`));
401
463
  };
402
464
  const parseBasePeriod = (input) => {
403
- if (isIsoDate(input) && input !== "9999-12-31") return Option.some(fixedDatePeriod(input, input));
465
+ const absoluteDate = absoluteDatePeriod(input, "fr");
466
+ if (Option.isSome(absoluteDate)) return absoluteDate;
404
467
  const namedDate = parseNamedDate(input);
405
468
  if (Option.isSome(namedDate)) return namedDate;
406
469
  const quarter = parseQuarter(input);
@@ -414,17 +477,17 @@ const parseBasePeriod = (input) => {
414
477
  if (Option.isSome(monthYear)) {
415
478
  const month = monthNumber(textAt(monthYear.value, 1));
416
479
  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}`));
480
+ if (month !== void 0 && year !== void 0) return Option.some(fixedMonthPeriod(year, month, `${textAt(months, month - 1)} ${year}`));
418
481
  }
419
482
  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
483
  if (Option.isSome(relativeMonth)) {
421
484
  const month = monthNumber(textAt(relativeMonth.value, 1));
422
485
  const yearText = textAt(relativeMonth.value, 2);
423
486
  const direction = relativeYearDirection(yearText);
424
- if (month !== void 0) return Option.some(monthOfRelativeYear(month, direction, `${title(textAt(months, month - 1))} de ${relativeYearName(direction)}`));
487
+ if (month !== void 0) return Option.some(monthOfRelativeYear(month, direction, `${textAt(months, month - 1)} de ${relativeYearName(direction)}`));
425
488
  }
426
489
  const standaloneMonth = monthNumber(input);
427
- if (standaloneMonth !== void 0) return Option.some(monthOfRelativeYear(standaloneMonth, 0, title(textAt(months, standaloneMonth - 1))));
490
+ if (standaloneMonth !== void 0) return Option.some(monthOfRelativeYear(standaloneMonth, 0, textAt(months, standaloneMonth - 1)));
428
491
  const alias = periodAliases.find((entry) => entry[0] === input);
429
492
  if (alias !== void 0) return Option.some(relativePeriod(alias[1], alias[2], alias[3]));
430
493
  if ([
@@ -455,7 +518,7 @@ const parsePeriod = (input) => {
455
518
  const period = parseBasePeriod(textAt(edge.value, 2));
456
519
  if (Option.isSome(period)) {
457
520
  const isEnd = edgeName === "fin";
458
- const canonical = `${isEnd ? "fin" : "début"} de ${period.value.canonical}`;
521
+ const canonical = `${isEnd ? "fin" : "début"} ${withDe(period.value.canonical)}`;
459
522
  return Option.some(isEnd ? periodEndDay(period.value, canonical) : periodStartDay(period.value, canonical));
460
523
  }
461
524
  }
@@ -468,7 +531,27 @@ const parsePeriod = (input) => {
468
531
  ].find((prefix) => input.startsWith(prefix));
469
532
  return parseBasePeriod(wrapper === void 0 ? input : input.slice(wrapper.length));
470
533
  };
471
- const countedUnit = (value) => unitAliases.find((entry) => entry[0] === value)?.[1];
534
+ const countedUnit = (value, amount) => {
535
+ const plural = value === "mois" || value.endsWith("s");
536
+ if (value !== "mois" && plural === (amount === 1)) return void 0;
537
+ const unit = unitAliases.find((entry) => entry[0] === value)?.[1];
538
+ return unit === void 0 ? void 0 : units.find((entry) => entry.unit === unit);
539
+ };
540
+ 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);
542
+ if (Option.isNone(match)) return true;
543
+ 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);
554
+ };
472
555
  const countedUnitPattern = "jour|jours|semaine|semaines|mois|trimestre|trimestres|an|ans|année|annee|années|annees";
473
556
  const compileCountedPattern = (source) => new RegExp(source.replace("UNIT", countedUnitPattern), "u");
474
557
  const calendarPastPattern = compileCountedPattern("^il y a ([1-9]\\d*) (UNIT)$");
@@ -477,38 +560,89 @@ const rollingSincePattern = compileCountedPattern("^depuis ([1-9]\\d*) (UNIT)$")
477
560
  const rollingBarePattern = compileCountedPattern("^([1-9]\\d*) (UNIT)$");
478
561
  const rollingPastPatterns = [
479
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)$"),
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)$"),
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)$"),
481
564
  compileCountedPattern("^([1-9]\\d*) (UNIT) (?:derniers|dernières|dernieres|passés|passées|passes|précédents|précédentes|precedents|precedentes)$")
482
565
  ];
483
566
  const rollingFuturePatterns = [
484
567
  compileCountedPattern("^(?:(?:les|la) )?(?:prochains|prochaines|suivants|suivantes) ([1-9]\\d*) (UNIT)$"),
485
- compileCountedPattern("^(?:(?:les|la) )?([1-9]\\d*) (?:prochains|prochaines|suivants|suivantes) (UNIT)$"),
568
+ compileCountedPattern("^(?:(?:les|la|au cours des|pendant les) )?([1-9]\\d*) (?:prochains|prochaines|suivants|suivantes) (UNIT)$"),
486
569
  compileCountedPattern("^([1-9]\\d*) (UNIT) (?:prochains|prochaines|suivants|suivantes|à venir)$")
487
570
  ];
488
571
  const firstPatternMatch = (input, patterns) => Option.firstSomeOf(patterns.map((pattern) => String$1.match(pattern)(input)));
489
- const isFeminineUnit = (unit) => unit === "week";
490
572
  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}`;
573
+ if (amount === 1) return singularRollingCanonical(entry, future);
574
+ return `les ${amount} ${rollingModifier(entry, future, true)} ${entry.rollingPlural}`;
495
575
  };
576
+ const singularRollingCanonical = (entry, future) => {
577
+ const article = indefiniteArticle(entry);
578
+ return future ? `à partir de maintenant pendant ${article} ${entry.singular}` : `depuis ${article} ${entry.singular}`;
579
+ };
580
+ const singularRollingPhrases = units.flatMap((entry) => {
581
+ const period = `${rollingArticle(entry)} ${rollingModifier(entry, false, false)} ${entry.rollingSingular}`;
582
+ const futurePeriod = `${rollingArticle(entry)} ${rollingModifier(entry, true, false)} ${entry.rollingSingular}`;
583
+ const article = indefiniteArticle(entry);
584
+ return [
585
+ {
586
+ phrase: period,
587
+ entry,
588
+ future: false
589
+ },
590
+ {
591
+ phrase: `au cours ${withDe(period)}`,
592
+ entry,
593
+ future: false
594
+ },
595
+ {
596
+ phrase: `depuis ${article} ${entry.singular}`,
597
+ entry,
598
+ future: false
599
+ },
600
+ {
601
+ phrase: `pendant ${futurePeriod}`,
602
+ entry,
603
+ future: true
604
+ },
605
+ {
606
+ phrase: `à partir de maintenant pendant ${article} ${entry.singular}`,
607
+ entry,
608
+ future: true
609
+ }
610
+ ];
611
+ });
612
+ const singularCalendarOffsets = units.flatMap((entry) => {
613
+ const quantity = `${indefiniteArticle(entry)} ${entry.singular}`;
614
+ return [{
615
+ phrase: `il y a ${quantity}`,
616
+ entry,
617
+ direction: -1
618
+ }, {
619
+ phrase: `dans ${quantity}`,
620
+ entry,
621
+ direction: 1
622
+ }];
623
+ });
496
624
  const parseCalendarOffset = (input) => {
625
+ 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));
497
627
  const past = String$1.match(calendarPastPattern)(input);
498
628
  const future = String$1.match(calendarFuturePattern)(input);
499
629
  const match = Option.firstSomeOf([past, future]);
500
630
  if (Option.isNone(match)) return Option.none();
501
631
  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);
632
+ if (Option.isNone(amount)) return Option.none();
633
+ const entry = countedUnit(textAt(match.value, 2), amount.value);
505
634
  if (entry === void 0) return Option.none();
506
635
  const direction = Option.isSome(past) ? -amount.value : amount.value;
507
636
  const noun = amount.value === 1 ? entry.singular : entry.plural;
508
637
  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));
638
+ return Option.some(candidate(periodRange(relativePeriod(entry.unit, direction, canonical)), canonical));
510
639
  };
511
640
  const parseRollingPeriod = (input) => {
641
+ const singular = singularRollingPhrases.find((entry) => entry.phrase === input);
642
+ if (singular !== void 0) {
643
+ const range = singular.future ? futureRange(1, singular.entry.unit) : trailingRange(1, singular.entry.unit);
644
+ return Option.some(candidate(range, singularRollingCanonical(singular.entry, singular.future)));
645
+ }
512
646
  const since = String$1.match(rollingSincePattern)(input);
513
647
  const past = firstPatternMatch(input, rollingPastPatterns);
514
648
  const bare = String$1.match(rollingBarePattern)(input);
@@ -521,19 +655,31 @@ const parseRollingPeriod = (input) => {
521
655
  ]);
522
656
  if (Option.isNone(match)) return Option.none();
523
657
  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();
658
+ if (Option.isNone(amount)) return Option.none();
659
+ const noun = textAt(match.value, 2);
660
+ const entry = countedUnit(noun, amount.value);
661
+ if (entry === void 0 || !modifierAgreesWithNoun(input, noun)) return Option.none();
528
662
  const isFuture = Option.isSome(future);
529
- const range = isFuture ? futureRange(amount.value, unit) : trailingRange(amount.value, unit);
663
+ const range = isFuture ? futureRange(amount.value, entry.unit) : trailingRange(amount.value, entry.unit);
530
664
  return Option.some(candidate(range, relativeCountPhrase(amount.value, entry, isFuture)));
531
665
  };
666
+ const parseElidedDateRange = (input) => {
667
+ 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);
668
+ const dashed = String$1.match(/^(1er|[0-3]?\d)[–—-](1er|[0-3]?\d) (.+)$/u)(input);
669
+ const match = Option.firstSomeOf([joined, dashed]);
670
+ if (Option.isNone(match)) return Option.none();
671
+ const isJoined = Option.isSome(joined);
672
+ const lowerDay = textAt(match.value, 1) || textAt(match.value, 2);
673
+ const upperDay = textAt(match.value, isJoined ? 3 : 2);
674
+ const period = afterDe(textAt(match.value, isJoined ? 4 : 3));
675
+ const datePeriod = periodAliases.some((entry) => entry[0] === period && entry[1] === "month") ? withDe(period) : period;
676
+ return joinedPeriodCandidate(`du ${lowerDay} ${datePeriod} au ${upperDay} ${datePeriod}`, [["du ", " au "]], parsePeriod, rangeLabel);
677
+ };
532
678
  const boundaryCandidate = (input) => {
533
679
  const included = String$1.match(/^jusqu'à (.+) (?:inclus|incluse|inclusivement)$/u)(input);
534
680
  if (Option.isSome(included)) {
535
681
  const period = parsePeriod(textAt(included.value, 1));
536
- if (Option.isSome(period)) return Option.some(periodBoundaryCandidate(period.value, "through", `jusqu'à ${period.value.canonical}`));
682
+ if (Option.isSome(period)) return Option.some(periodBoundaryCandidate(period.value, "through", untilLabel(period.value.canonical)));
537
683
  }
538
684
  return openBoundaryCandidate(input, [
539
685
  ["jusqu'avant ", "before"],
@@ -568,6 +714,8 @@ const parseFrench = (input) => {
568
714
  if (Option.isSome(rolling)) return rolling;
569
715
  const toDate = toDatePhrases.find((entry) => entry.phrase === input);
570
716
  if (toDate !== void 0) return Option.some(candidate(periodToDateRange(toDate.entry.unit), toDate.entry.toDate));
717
+ const elided = parseElidedDateRange(input);
718
+ if (Option.isSome(elided)) return elided;
571
719
  const nowBounded = joinedNowCandidate(input, [
572
720
  ["depuis ", " jusqu'à maintenant"],
573
721
  ["depuis ", " jusqu'à aujourd'hui"],
@@ -588,20 +736,32 @@ const parseFrench = (input) => {
588
736
  ["", " — "],
589
737
  ["", " jusqu'à "],
590
738
  ["", " au "]
591
- ], parsePeriod, (lower, upper) => `du ${lower} au ${upper}`);
739
+ ], parsePeriod, rangeLabel);
592
740
  if (Option.isSome(bounded)) return bounded;
593
741
  const boundary = boundaryCandidate(input);
594
742
  if (Option.isSome(boundary)) return boundary;
595
- return Option.map(parsePeriod(input), (period) => candidate(periodRange(period), period.canonical));
743
+ return parsePeriod(input).pipe(Option.map((period) => candidate(periodRange(period), period.canonical)));
596
744
  };
597
- const staticPeriodPhrases = [
598
- ...periodAliases.map((entry) => entry[0]),
745
+ const weekendPhrases = [
599
746
  "ce week-end",
600
747
  "le week-end dernier",
601
748
  "le week-end prochain",
602
749
  "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]}`]),
750
+ "le week-end après le prochain"
751
+ ];
752
+ const edgePeriodPhrases = [
753
+ ...units.filter((entry) => entry.unit !== "day").flatMap((entry) => [
754
+ entry.current,
755
+ entry.previous,
756
+ entry.next
757
+ ]),
758
+ ...weekendPhrases,
759
+ ...months
760
+ ].flatMap((period) => [`début ${withDe(period)}`, `fin ${withDe(period)}`]);
761
+ const staticPeriodPhrases = [
762
+ ...periodAliases.map((entry) => entry[0]),
763
+ ...weekendPhrases,
764
+ ...edgePeriodPhrases,
605
765
  ...[
606
766
  1,
607
767
  2,
@@ -631,15 +791,16 @@ const countedSuggestions = (input) => {
631
791
  if (amount === void 0) return [];
632
792
  return units.flatMap((entry) => {
633
793
  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";
794
+ const rollingNoun = amount === 1 ? entry.rollingSingular : entry.rollingPlural;
795
+ const past = rollingModifier(entry, false, amount !== 1);
796
+ const future = rollingModifier(entry, true, amount !== 1);
636
797
  return [
637
798
  relativeCountPhrase(amount, entry, false),
638
- `${amount} ${past} ${noun}`,
799
+ `${amount} ${past} ${rollingNoun}`,
639
800
  `${amount} ${noun}`,
640
801
  relativeCountPhrase(amount, entry, true),
641
- `${amount} ${future} ${noun}`,
642
- `${amount} ${noun} à venir`,
802
+ `${amount} ${future} ${rollingNoun}`,
803
+ `${amount} ${rollingNoun} à venir`,
643
804
  `il y a ${amount} ${noun}`,
644
805
  `dans ${amount} ${noun}`
645
806
  ];
@@ -648,6 +809,8 @@ const countedSuggestions = (input) => {
648
809
  const frenchSuggestionPhrases = [
649
810
  ...units.map((entry) => entry.toDate),
650
811
  ...units.map((entry) => entry.remaining),
812
+ ...singularRollingPhrases.map((entry) => entry.phrase),
813
+ ...singularCalendarOffsets.map((entry) => entry.phrase),
651
814
  ...staticPeriodPhrases,
652
815
  ...prefixNaturalPhrases(staticPeriodPhrases, boundaryPrefixes),
653
816
  "jusqu'à maintenant",
@@ -675,18 +838,19 @@ const renderFrench = (range) => {
675
838
  const future = futurePeriod(range);
676
839
  if (Option.isSome(future)) {
677
840
  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
- }
841
+ if (entry !== void 0) return Option.some(future.value.amount === 1 ? singularRollingCanonical(entry, true) : relativeCountPhrase(future.value.amount, entry, true));
682
842
  }
683
843
  const trailing = trailingPeriod(range);
684
844
  if (Option.isSome(trailing)) {
685
845
  const entry = units.find((unit) => unit.unit === trailing.value.unit);
686
- if (entry !== void 0) return Option.some(relativeCountPhrase(trailing.value.amount, entry, false));
846
+ if (entry !== void 0) return Option.some(trailing.value.amount === 1 ? singularRollingCanonical(entry, false) : relativeCountPhrase(trailing.value.amount, entry, false));
687
847
  }
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");
848
+ const periods = [
849
+ ...staticPeriods,
850
+ ...currentYearDatePeriods(range, currentDateLabel),
851
+ ...periodsFromPhrases([...datedPeriods(range, months), ...datedQuarterPeriods(range)], parsePeriod)
852
+ ];
853
+ 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
854
  };
691
855
  const normalizeFrench = (input, locale) => normalizeNaturalText(input, locale).replaceAll("’", "'");
692
856
  const FrenchContribution = new BaseLanguageContribution({