chronolizer 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,10 +1,9 @@
1
- import { isIsoDate } from "../ast/schemas.mjs";
2
1
  import { BaseLanguageContribution } from "../language/model.mjs";
3
2
  import { normalizeNaturalText } from "../natural/text.mjs";
4
3
  import { defineLanguagePlugin, languagePluginsLayer } from "../language/registry.mjs";
5
4
  import { correctWhitespaceSeparatedText } from "../natural/correction.mjs";
6
5
  import { completeNaturalPhrases, fixedCalendarPeriodPhrases, naturalCount, prefixNaturalPhrases } from "../natural/suggestion.mjs";
7
- import { absoluteDatePeriod, calendarPeriodOffset, candidate, currentYearDatePeriods, datedPeriods, datedQuarterPeriods, fixedDatePeriod, fixedMonthPeriod, fixedQuarterPeriod, fixedYearPeriod, fromNowRange, futurePeriod, futureRange, isoDate, joinedNowCandidate, joinedPeriodCandidate, monthOfRelativeYear, namedCurrentYearDatePeriod, namedDatePeriod, openBoundaryCandidate, parseTrailingCount, periodBoundaryCandidate, periodDay, periodEndDay, periodRange, periodStartDay, periodToDateRange, periodsFromPhrases, quarterOfRelativeYear, relativePeriod, relativeWeekend, remainingPeriodRange, renderPeriodRange, textAt, trailingPeriod, trailingRange, untilNowRange, validYear } from "./shared.mjs";
6
+ import { absoluteDatePeriod, calendarPeriodOffset, candidate, compileCountAliasNormalizer, compoundCountAliases, countAliasVocabulary, currentYearDatePeriods, datedPeriods, datedQuarterPeriods, decimalTens, decomposeShiftedPeriodRange, fixedMonthPeriod, fixedQuarterPeriod, fixedYearPeriod, fromNowRange, futurePeriod, futureRange, joinedNowCandidate, joinedPeriodCandidate, monthOfRelativeYear, namedCurrentYearDatePeriod, namedDatePeriod, openBoundaryCandidate, parseTrailingCount, periodBoundaryCandidate, periodDay, periodEndDay, periodRange, periodStartDay, periodToDateRange, periodsFromPhrases, quarterOfRelativeYear, relativePeriod, relativeWeekday, relativeWeekend, remainingPeriodRange, renderPeriodRange, sequentialCountAliases, shiftPeriod, textAt, trailingPeriod, trailingRange, untilNowRange, validYear } from "./shared.mjs";
8
7
  import { Effect, Option, String as String$1 } from "effect";
9
8
  //#region src/locales/es.ts
10
9
  const months = [
@@ -21,6 +20,99 @@ const months = [
21
20
  "noviembre",
22
21
  "diciembre"
23
22
  ];
23
+ const weekdays = [
24
+ "lunes",
25
+ "martes",
26
+ "miércoles",
27
+ "jueves",
28
+ "viernes",
29
+ "sábado",
30
+ "domingo"
31
+ ];
32
+ const nextWeekdays = weekdays.flatMap((weekday, day) => [
33
+ {
34
+ phrase: `próximo ${weekday}`,
35
+ day,
36
+ canonical: `el próximo ${weekday}`
37
+ },
38
+ {
39
+ phrase: `el próximo ${weekday}`,
40
+ day,
41
+ canonical: `el próximo ${weekday}`
42
+ },
43
+ {
44
+ phrase: `proximo ${weekday}`,
45
+ day,
46
+ canonical: `el próximo ${weekday}`
47
+ },
48
+ {
49
+ phrase: `el proximo ${weekday}`,
50
+ day,
51
+ canonical: `el próximo ${weekday}`
52
+ }
53
+ ]);
54
+ const spanishCountWords = [
55
+ ["dos"],
56
+ ["tres"],
57
+ ["cuatro"],
58
+ ["cinco"],
59
+ ["seis"],
60
+ ["siete"],
61
+ ["ocho"],
62
+ ["nueve"],
63
+ ["diez"],
64
+ ["once"],
65
+ ["doce"],
66
+ ["trece"],
67
+ ["catorce"],
68
+ ["quince"],
69
+ ["dieciséis", "dieciseis"],
70
+ ["diecisiete"],
71
+ ["dieciocho"],
72
+ ["diecinueve"],
73
+ ["veinte"]
74
+ ];
75
+ const spanishCountOnes = [
76
+ [1, "uno"],
77
+ [2, "dos"],
78
+ [3, "tres"],
79
+ [4, "cuatro"],
80
+ [5, "cinco"],
81
+ [6, "seis"],
82
+ [7, "siete"],
83
+ [8, "ocho"],
84
+ [9, "nueve"]
85
+ ];
86
+ const spanishCountAliases = [...sequentialCountAliases(spanishCountWords, 2), ...compoundCountAliases(decimalTens([
87
+ "veinte",
88
+ "treinta",
89
+ "cuarenta",
90
+ "cincuenta",
91
+ "sesenta",
92
+ "setenta",
93
+ "ochenta",
94
+ "noventa"
95
+ ]), spanishCountOnes, (ten, one, amount) => {
96
+ if (amount === 21) return [
97
+ "veintiuno",
98
+ "veintiún",
99
+ "veintiun",
100
+ "veintiuna"
101
+ ];
102
+ if (amount === 22) return ["veintidós", "veintidos"];
103
+ if (amount === 23) return ["veintitrés", "veintitres"];
104
+ if (amount === 26) return ["veintiséis", "veintiseis"];
105
+ if (amount < 30) return [`veinti${one}`];
106
+ return (one === "uno" ? [
107
+ one,
108
+ "un",
109
+ "una"
110
+ ] : [one]).map((variant) => `${ten} y ${variant}`);
111
+ })];
112
+ const normalizeSpanishCounts = compileCountAliasNormalizer(spanishCountAliases);
113
+ const spanishCountVocabulary = new Set(countAliasVocabulary(spanishCountAliases));
114
+ const correctSpanish = (input, vocabulary) => correctWhitespaceSeparatedText(input, vocabulary, spanishCountVocabulary);
115
+ const normalizeSpanish = (input, locale) => normalizeSpanishCounts(normalizeNaturalText(input, locale));
24
116
  const monthAbbreviations = [
25
117
  ["ene"],
26
118
  ["feb"],
@@ -467,15 +559,7 @@ const parseNamedDate = (input) => {
467
559
  const named = String$1.match(/^(?:el (?:día )?)?([0-3]?\d)(?: de)? ([a-záéíóúñ]+\.?)(?:(?: de| del)? (\d{4}))$/u)(input);
468
560
  if (Option.isSome(named)) return namedDatePeriod(textAt(named.value, 3), textAt(named.value, 2), textAt(named.value, 1), monthNumber, (day, month, year) => `${day} de ${textAt(months, month - 1)} de ${year}`);
469
561
  const numeric = String$1.match(/^([0-3]?\d)[./-]([01]?\d)[./-](\d{4})$/u)(input);
470
- if (Option.isSome(numeric)) {
471
- const year = validYear(textAt(numeric.value, 3));
472
- const month = Number(textAt(numeric.value, 2));
473
- const day = Number(textAt(numeric.value, 1));
474
- if (year !== void 0 && month >= 1 && month <= 12) {
475
- const value = isoDate(year, month, day);
476
- if (isIsoDate(value) && value !== "9999-12-31") return Option.some(fixedDatePeriod(value, `${day} de ${textAt(months, month - 1)} de ${year}`));
477
- }
478
- }
562
+ if (Option.isSome(numeric)) return namedDatePeriod(textAt(numeric.value, 3), textAt(numeric.value, 2), textAt(numeric.value, 1), Number, (day, month, year) => `${day} de ${textAt(months, month - 1)} de ${year}`);
479
563
  const current = String$1.match(/^(?:el (?:día )?)?([0-3]?\d)(?: de)? ([a-záéíóúñ]+\.?)$/u)(input);
480
564
  if (Option.isSome(current)) return namedCurrentYearDatePeriod(textAt(current.value, 2), textAt(current.value, 1), monthNumber, currentDateLabel);
481
565
  const relative = String$1.match(/^(?:el (?:día )?)?([0-3]?\d) (de .+|del .+)$/u)(input);
@@ -513,13 +597,13 @@ const parseQuarter = (input) => {
513
597
  const quarter = quarterNumber(textAt(standalone.value, 1));
514
598
  return quarter === void 0 ? Option.none() : Option.some(quarterOfRelativeYear(quarter, 0, `T${quarter}`));
515
599
  };
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;
600
+ const parseDatedPeriod = (input) => {
601
+ const knownPeriod = Option.firstSomeOf([
602
+ absoluteDatePeriod(input, "es"),
603
+ parseNamedDate(input),
604
+ parseQuarter(input)
605
+ ]);
606
+ if (Option.isSome(knownPeriod)) return knownPeriod;
523
607
  const yearMatch = String$1.match(/^(?:(?:el )?año )?(\d{4})$/u)(input);
524
608
  if (Option.isSome(yearMatch)) {
525
609
  const year = validYear(textAt(yearMatch.value, 1));
@@ -531,6 +615,26 @@ const parseBasePeriod = (input) => {
531
615
  const year = validYear(textAt(monthYear.value, 2));
532
616
  if (month !== void 0 && year !== void 0) return Option.some(fixedMonthPeriod(year, month, `${textAt(months, month - 1)} de ${year}`));
533
617
  }
618
+ return Option.none();
619
+ };
620
+ const parseBasePeriod = (input) => {
621
+ const datedPeriod = parseDatedPeriod(input);
622
+ if (Option.isSome(datedPeriod)) return datedPeriod;
623
+ const prefixedRelativeMonth = String$1.match(/^(pasado|próximo|proximo|este) ([a-záéíóúñ]+\.?)$/u)(input);
624
+ const suffixedRelativeMonth = String$1.match(/^([a-záéíóúñ]+\.?) (pasado|próximo|proximo)$/u)(input);
625
+ const shortRelativeMonth = Option.firstSomeOf([prefixedRelativeMonth, suffixedRelativeMonth]);
626
+ if (Option.isSome(shortRelativeMonth)) {
627
+ const prefixed = Option.isSome(prefixedRelativeMonth);
628
+ const month = monthNumber(textAt(shortRelativeMonth.value, prefixed ? 2 : 1));
629
+ if (month !== void 0) {
630
+ const modifier = textAt(shortRelativeMonth.value, prefixed ? 1 : 2);
631
+ const direction = relativeYearDirection(modifier);
632
+ let canonicalModifier = "este";
633
+ if (direction < 0) canonicalModifier = "pasado";
634
+ if (direction > 0) canonicalModifier = "próximo";
635
+ return Option.some(monthOfRelativeYear(month, direction, `${canonicalModifier} ${textAt(months, month - 1)}`));
636
+ }
637
+ }
534
638
  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
639
  if (Option.isSome(relativeMonth)) {
536
640
  const month = monthNumber(textAt(relativeMonth.value, 1));
@@ -554,13 +658,31 @@ const parseBasePeriod = (input) => {
554
658
  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
659
  if (input === "el fin de semana anterior al pasado") return Option.some(relativeWeekend(-2, input));
556
660
  if (input === "el fin de semana después del próximo") return Option.some(relativeWeekend(2, input));
557
- return Option.none();
661
+ const weekday = nextWeekdays.find((entry) => entry.phrase === input);
662
+ return weekday === void 0 ? Option.none() : Option.some(relativeWeekday(weekday.day, 1, weekday.canonical));
558
663
  };
559
664
  const parsePeriod = (input) => {
665
+ 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);
666
+ if (Option.isSome(shifted)) {
667
+ const amount = parseTrailingCount(textAt(shifted.value, 3));
668
+ const alias = unitAliases.find((unit) => unit[0] === textAt(shifted.value, 4));
669
+ const entry = alias === void 0 ? void 0 : units.find((unit) => unit.unit === alias[1]);
670
+ const period = parsePeriod(textAt(shifted.value, 1));
671
+ if (Option.isSome(amount) && entry !== void 0 && Option.isSome(period)) {
672
+ const past = textAt(shifted.value, 2) === "hace";
673
+ const direction = past ? -amount.value : amount.value;
674
+ const noun = amount.value === 1 ? entry.singular : entry.plural;
675
+ const canonical = `${period.value.canonical} ${past ? "hace" : "dentro de"} ${amount.value} ${noun}`;
676
+ return Option.some(shiftPeriod(period.value, direction, entry.unit, canonical));
677
+ }
678
+ }
560
679
  const edge = String$1.match(/^(?:el )?(inicio|comienzo|principio|fin|final)(?: de| del) (.+)$/u)(input);
561
680
  if (Option.isSome(edge)) {
562
681
  const edgeName = textAt(edge.value, 1);
563
- const period = parseBasePeriod(textAt(edge.value, 2));
682
+ const periodText = textAt(edge.value, 2);
683
+ const basePeriod = parseBasePeriod(periodText);
684
+ const implicit = units.find((entry) => entry.singular === periodText);
685
+ const period = Option.isSome(basePeriod) || implicit === void 0 ? basePeriod : Option.some(relativePeriod(implicit.unit, 0, implicit.current));
564
686
  if (Option.isSome(period)) {
565
687
  const isEnd = edgeName === "fin" || edgeName === "final";
566
688
  const canonical = `${isEnd ? "final" : "inicio"} ${withDe(period.value.canonical)}`;
@@ -574,7 +696,8 @@ const parsePeriod = (input) => {
574
696
  "todo el ",
575
697
  "toda la "
576
698
  ].find((prefix) => input.startsWith(prefix));
577
- return parseBasePeriod(wrapper === void 0 ? input : input.slice(wrapper.length));
699
+ const base = parseBasePeriod(wrapper === void 0 ? input : input.slice(wrapper.length));
700
+ return Option.isSome(base) ? base : parseCalendarOffset(input);
578
701
  };
579
702
  const countedUnit = (value, amount) => {
580
703
  const alias = unitAliases.find((entry) => entry[0] === value && entry[2] === (amount !== 1));
@@ -597,7 +720,7 @@ const agreesWithSingularModifier = (entry, modifier, future) => {
597
720
  return modifier === expected || modifier === withoutAcute(expected);
598
721
  };
599
722
  const parseCalendarOffset = (input) => {
600
- const match = String$1.match(/^(hace|dentro de|en) ([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);
723
+ const match = String$1.match(/^(hace|dentro de|en) ([1-9]\d*|un|una) ([^ ]+)$/u)(input);
601
724
  if (Option.isNone(match)) return Option.none();
602
725
  const amount = writtenAmount(textAt(match.value, 2));
603
726
  if (Option.isNone(amount)) return Option.none();
@@ -609,7 +732,7 @@ const parseCalendarOffset = (input) => {
609
732
  const noun = amount.value === 1 ? entry.singular : entry.plural;
610
733
  const canonical = `${isPast ? "hace" : "dentro de"} ${quantity} ${noun}`;
611
734
  const direction = isPast ? -amount.value : amount.value;
612
- return Option.some(candidate(periodRange(relativePeriod(entry.unit, direction, canonical)), canonical));
735
+ return Option.some(relativePeriod(entry.unit, direction, canonical));
613
736
  };
614
737
  const rollingCanonical = (entry, amount, future) => {
615
738
  if (amount > 1) return `${rollingAdjective(entry, future, true)} ${amount} ${entry.plural}`;
@@ -617,8 +740,8 @@ const rollingCanonical = (entry, amount, future) => {
617
740
  return future ? `desde ahora durante ${article} ${entry.singular}` : `desde hace ${article} ${entry.singular}`;
618
741
  };
619
742
  const rollingCandidate = (entry, amount, future) => candidate(future ? futureRange(amount, entry.unit) : trailingRange(amount, entry.unit), rollingCanonical(entry, amount, future));
620
- const parseRollingPeriod = (input) => {
621
- 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);
743
+ const parseRollingSince = (input) => {
744
+ const since = String$1.match(/^desde hace ([1-9]\d*|un|una) ([^ ]+)$/u)(input);
622
745
  if (Option.isSome(since)) {
623
746
  const amount = writtenAmount(textAt(since.value, 1));
624
747
  if (Option.isSome(amount)) {
@@ -627,40 +750,51 @@ const parseRollingPeriod = (input) => {
627
750
  if (entry !== void 0 && (amountText !== "un" && amountText !== "una" || agreesWithIndefiniteArticle(entry, amountText))) return Option.some(rollingCandidate(entry, amount.value, false));
628
751
  }
629
752
  }
630
- const futureSingular = String$1.match(/^desde ahora durante (un|una) (día|dia|semana|mes|trimestre|año|ano)$/u)(input);
753
+ return Option.none();
754
+ };
755
+ const parseSingularRolling = (input) => {
756
+ const futureSingular = String$1.match(/^desde ahora durante (un|una) ([^ ]+)$/u)(input);
631
757
  if (Option.isSome(futureSingular)) {
632
758
  const entry = countedUnit(textAt(futureSingular.value, 2), 1);
633
759
  if (entry !== void 0 && agreesWithIndefiniteArticle(entry, textAt(futureSingular.value, 1))) return Option.some(rollingCandidate(entry, 1, true));
634
760
  }
635
- const lastSingular = String$1.match(/^(?:(?:durante|en) )?(?:este )?(?:(el|la) )?(último|ultimo|última|ultima) (día|dia|semana|mes|trimestre|año|ano)$/u)(input);
761
+ const lastSingular = String$1.match(/^(?:(?:durante|en) )?(?:este )?(?:(el|la) )?([uú]ltim[oa]) ([^ ]+)$/u)(input);
636
762
  if (Option.isSome(lastSingular)) {
637
763
  const entry = countedUnit(textAt(lastSingular.value, 3), 1);
638
764
  const modifier = textAt(lastSingular.value, 2);
639
765
  if (entry !== void 0 && agreesWithArticle(entry, textAt(lastSingular.value, 1)) && agreesWithSingularModifier(entry, modifier, false)) return Option.some(rollingCandidate(entry, 1, false));
640
766
  }
641
- const nextSingular = String$1.match(/^durante (el|la) (próximo|proximo|próxima|proxima|siguiente) (día|dia|semana|mes|trimestre|año|ano)$/u)(input);
767
+ const nextSingular = String$1.match(/^durante (el|la) (pr[oó]xim[oa]|siguiente) ([^ ]+)$/u)(input);
642
768
  if (Option.isSome(nextSingular)) {
643
769
  const entry = countedUnit(textAt(nextSingular.value, 3), 1);
644
770
  const modifier = textAt(nextSingular.value, 2);
645
771
  if (entry !== void 0 && agreesWithArticle(entry, textAt(nextSingular.value, 1)) && agreesWithSingularModifier(entry, modifier, true)) return Option.some(rollingCandidate(entry, 1, true));
646
772
  }
647
- 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);
773
+ return Option.none();
774
+ };
775
+ const parseCountedRolling = (input) => {
776
+ const counted = String$1.match(/^(?:(?:durante|en) )?(?:(los|las) )?([uú]ltim(?:os|as)|pasad(?:os|as)|anteriores|pr[oó]xim(?:os|as)|siguientes) ([1-9]\d*) ([^ ]+)$/u)(input);
648
777
  if (Option.isSome(counted)) {
649
778
  const amount = parseTrailingCount(textAt(counted.value, 3));
650
779
  if (Option.isSome(amount) && amount.value > 1) {
651
780
  const entry = countedUnit(textAt(counted.value, 4), amount.value);
652
781
  const modifier = textAt(counted.value, 2);
653
- const future = modifier.startsWith("próxim") || modifier.startsWith("proxim") || modifier === "siguientes";
782
+ const future = modifier.includes("xim") || modifier === "siguientes";
654
783
  if (entry !== void 0 && agreesWithArticle(entry, textAt(counted.value, 1), true) && agreesWithRollingModifier(entry, modifier, future)) return Option.some(rollingCandidate(entry, amount.value, future));
655
784
  }
656
785
  }
657
- const bare = String$1.match(/^([1-9]\d*) (día|dia|días|dias|semana|semanas|mes|meses|trimestre|trimestres|año|años|ano|anos)$/u)(input);
786
+ const bare = String$1.match(/^([1-9]\d*) ([^ ]+)$/u)(input);
658
787
  if (Option.isNone(bare)) return Option.none();
659
788
  const amount = parseTrailingCount(textAt(bare.value, 1));
660
789
  if (Option.isNone(amount)) return Option.none();
661
790
  const entry = countedUnit(textAt(bare.value, 2), amount.value);
662
791
  return entry === void 0 ? Option.none() : Option.some(rollingCandidate(entry, amount.value, false));
663
792
  };
793
+ const parseRollingPeriod = (input) => Option.firstSomeOf([
794
+ parseRollingSince(input),
795
+ parseSingularRolling(input),
796
+ parseCountedRolling(input)
797
+ ]);
664
798
  const parseElidedDateRange = (input) => {
665
799
  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
800
  const dashed = String$1.match(/^([0-3]?\d)[–—-]([0-3]?\d) (.+)$/u)(input);
@@ -698,8 +832,6 @@ const parseSpanish = (input) => {
698
832
  "a partir de ahora",
699
833
  "de ahora en adelante"
700
834
  ].includes(input)) return Option.some(candidate(fromNowRange(), "desde ahora"));
701
- const offset = parseCalendarOffset(input);
702
- if (Option.isSome(offset)) return offset;
703
835
  const rolling = parseRollingPeriod(input);
704
836
  if (Option.isSome(rolling)) return rolling;
705
837
  const toDate = toDatePhrases.find((entry) => entry.phrase === input);
@@ -714,7 +846,8 @@ const parseSpanish = (input) => {
714
846
  "desde ahora hasta ",
715
847
  "desde hoy hasta ",
716
848
  "entre hoy y ",
717
- "entre hoy y el "
849
+ "entre hoy y el ",
850
+ "ahora hasta "
718
851
  ], parsePeriod, (period) => `desde ${period} hasta ahora`, (period) => `desde ahora hasta ${period}`);
719
852
  if (Option.isSome(nowBounded)) return nowBounded;
720
853
  const bounded = joinedPeriodCandidate(input, [
@@ -769,9 +902,14 @@ const staticPeriodPhrases = [
769
902
  ]),
770
903
  ...months.flatMap((month) => [
771
904
  month,
905
+ `pasado ${month}`,
906
+ `próximo ${month}`,
907
+ `${month} pasado`,
908
+ `${month} próximo`,
772
909
  `${month} del año pasado`,
773
910
  `${month} del próximo año`
774
- ])
911
+ ]),
912
+ ...nextWeekdays.map((entry) => entry.phrase)
775
913
  ];
776
914
  const staticPeriods = periodsFromPhrases(staticPeriodPhrases, parsePeriod);
777
915
  const boundaryPrefixes = [
@@ -830,6 +968,17 @@ const suggestSpanish = (input, limit) => {
830
968
  ], limit);
831
969
  };
832
970
  const renderSpanish = (range) => {
971
+ const shifted = decomposeShiftedPeriodRange(range);
972
+ if (Option.isSome(shifted)) {
973
+ const base = renderSpanish(shifted.value.baseRange);
974
+ const entry = units.find((unit) => unit.unit === shifted.value.unit);
975
+ if (Option.isSome(base) && entry !== void 0) {
976
+ const amount = Math.abs(shifted.value.amount);
977
+ const noun = amount === 1 ? entry.singular : entry.plural;
978
+ const direction = shifted.value.amount < 0 ? "hace" : "dentro de";
979
+ return Option.some(`${base.value} ${direction} ${amount} ${noun}`);
980
+ }
981
+ }
833
982
  const offset = calendarPeriodOffset(range);
834
983
  if (Option.isSome(offset) && Math.abs(offset.value.amount) > 1) {
835
984
  const entry = units.find((unit) => unit.unit === offset.value.unit);
@@ -860,6 +1009,7 @@ const SpanishContribution = new BaseLanguageContribution({
860
1009
  locale: "es",
861
1010
  vocabulary: [
862
1011
  ...months,
1012
+ ...weekdays,
863
1013
  ...monthAbbreviations.flatMap((aliases) => aliases),
864
1014
  ...units.flatMap((entry) => [
865
1015
  entry.singular,
@@ -893,8 +1043,8 @@ const SpanishContribution = new BaseLanguageContribution({
893
1043
  "resto",
894
1044
  "últimos"
895
1045
  ],
896
- normalize: normalizeNaturalText,
897
- correct: correctWhitespaceSeparatedText,
1046
+ normalize: normalizeSpanish,
1047
+ correct: correctSpanish,
898
1048
  parseExact: parseSpanish,
899
1049
  suggest: suggestSpanish,
900
1050
  render: renderSpanish