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, 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"],
@@ -45,6 +138,7 @@ const units = [
45
138
  unit: "day",
46
139
  singular: "día",
47
140
  plural: "días",
141
+ gender: "masculine",
48
142
  current: "hoy",
49
143
  previous: "ayer",
50
144
  next: "mañana",
@@ -55,6 +149,7 @@ const units = [
55
149
  unit: "week",
56
150
  singular: "semana",
57
151
  plural: "semanas",
152
+ gender: "feminine",
58
153
  current: "esta semana",
59
154
  previous: "la semana pasada",
60
155
  next: "la próxima semana",
@@ -65,6 +160,7 @@ const units = [
65
160
  unit: "month",
66
161
  singular: "mes",
67
162
  plural: "meses",
163
+ gender: "masculine",
68
164
  current: "este mes",
69
165
  previous: "el mes pasado",
70
166
  next: "el próximo mes",
@@ -75,6 +171,7 @@ const units = [
75
171
  unit: "quarter",
76
172
  singular: "trimestre",
77
173
  plural: "trimestres",
174
+ gender: "masculine",
78
175
  current: "este trimestre",
79
176
  previous: "el trimestre pasado",
80
177
  next: "el próximo trimestre",
@@ -85,6 +182,7 @@ const units = [
85
182
  unit: "year",
86
183
  singular: "año",
87
184
  plural: "años",
185
+ gender: "masculine",
88
186
  current: "este año",
89
187
  previous: "el año pasado",
90
188
  next: "el próximo año",
@@ -92,21 +190,89 @@ const units = [
92
190
  remaining: "resto del año"
93
191
  }
94
192
  ];
95
- const title = (value) => `${value.slice(0, 1).toLocaleUpperCase("es")}${value.slice(1)}`;
193
+ const gendered = (entry, masculine, feminine) => entry.gender === "feminine" ? feminine : masculine;
194
+ const definiteArticle = (entry, plural = false) => gendered(entry, plural ? "los" : "el", plural ? "las" : "la");
195
+ const indefiniteArticle = (entry) => gendered(entry, "un", "una");
196
+ const withDe = (period) => period.startsWith("el ") ? `del ${period.slice(3)}` : `de ${period}`;
197
+ const afterDe = (period) => {
198
+ if (period.startsWith("del ")) return `el ${period.slice(4)}`;
199
+ return period.startsWith("de ") ? period.slice(3) : period;
200
+ };
201
+ const rollingAdjective = (entry, future, plural) => {
202
+ if (future) return gendered(entry, plural ? "próximos" : "próximo", plural ? "próximas" : "próxima");
203
+ return gendered(entry, plural ? "últimos" : "último", plural ? "últimas" : "última");
204
+ };
96
205
  const unitAliases = [
97
- ["día", "day"],
98
- ["dias", "day"],
99
- ["días", "day"],
100
- ["semana", "week"],
101
- ["semanas", "week"],
102
- ["mes", "month"],
103
- ["meses", "month"],
104
- ["trimestre", "quarter"],
105
- ["trimestres", "quarter"],
106
- ["año", "year"],
107
- ["ano", "year"],
108
- ["años", "year"],
109
- ["anos", "year"]
206
+ [
207
+ "día",
208
+ "day",
209
+ false
210
+ ],
211
+ [
212
+ "dia",
213
+ "day",
214
+ false
215
+ ],
216
+ [
217
+ "días",
218
+ "day",
219
+ true
220
+ ],
221
+ [
222
+ "dias",
223
+ "day",
224
+ true
225
+ ],
226
+ [
227
+ "semana",
228
+ "week",
229
+ false
230
+ ],
231
+ [
232
+ "semanas",
233
+ "week",
234
+ true
235
+ ],
236
+ [
237
+ "mes",
238
+ "month",
239
+ false
240
+ ],
241
+ [
242
+ "meses",
243
+ "month",
244
+ true
245
+ ],
246
+ [
247
+ "trimestre",
248
+ "quarter",
249
+ false
250
+ ],
251
+ [
252
+ "trimestres",
253
+ "quarter",
254
+ true
255
+ ],
256
+ [
257
+ "año",
258
+ "year",
259
+ false
260
+ ],
261
+ [
262
+ "ano",
263
+ "year",
264
+ false
265
+ ],
266
+ [
267
+ "años",
268
+ "year",
269
+ true
270
+ ],
271
+ [
272
+ "anos",
273
+ "year",
274
+ true
275
+ ]
110
276
  ];
111
277
  const periodAliases = [
112
278
  ...units.flatMap((entry) => [
@@ -129,6 +295,44 @@ const periodAliases = [
129
295
  entry.next
130
296
  ]
131
297
  ]),
298
+ ...units.filter((entry) => entry.unit !== "day").flatMap((entry) => [
299
+ [
300
+ `${entry.singular} actual`,
301
+ entry.unit,
302
+ 0,
303
+ entry.current
304
+ ],
305
+ [
306
+ `${definiteArticle(entry)} ${entry.singular} anterior`,
307
+ entry.unit,
308
+ -1,
309
+ entry.previous
310
+ ],
311
+ [
312
+ `${entry.singular} anterior`,
313
+ entry.unit,
314
+ -1,
315
+ entry.previous
316
+ ],
317
+ [
318
+ `${definiteArticle(entry)} ${entry.singular} siguiente`,
319
+ entry.unit,
320
+ 1,
321
+ entry.next
322
+ ],
323
+ [
324
+ `${entry.singular} siguiente`,
325
+ entry.unit,
326
+ 1,
327
+ entry.next
328
+ ],
329
+ [
330
+ `${rollingAdjective(entry, true, false)} ${entry.singular}`,
331
+ entry.unit,
332
+ 1,
333
+ entry.next
334
+ ]
335
+ ]),
132
336
  [
133
337
  "semana pasada",
134
338
  "week",
@@ -237,12 +441,36 @@ const periodAliases = [
237
441
  -2,
238
442
  "anteayer"
239
443
  ],
444
+ [
445
+ "día antes de ayer",
446
+ "day",
447
+ -2,
448
+ "anteayer"
449
+ ],
450
+ [
451
+ "el día antes de ayer",
452
+ "day",
453
+ -2,
454
+ "anteayer"
455
+ ],
240
456
  [
241
457
  "pasado mañana",
242
458
  "day",
243
459
  2,
244
460
  "pasado mañana"
245
461
  ],
462
+ [
463
+ "día después de mañana",
464
+ "day",
465
+ 2,
466
+ "pasado mañana"
467
+ ],
468
+ [
469
+ "el día después de mañana",
470
+ "day",
471
+ 2,
472
+ "pasado mañana"
473
+ ],
246
474
  [
247
475
  "la semana anterior a la pasada",
248
476
  "week",
@@ -327,17 +555,30 @@ const monthNumber = (value) => {
327
555
  const short = monthAbbreviations.findIndex((aliases) => aliases.some((alias) => alias === normalized));
328
556
  return short === -1 ? void 0 : short + 1;
329
557
  };
558
+ const currentDateLabel = (day, month) => `${day} de ${textAt(months, month - 1)}`;
330
559
  const parseNamedDate = (input) => {
331
- const named = String$1.match(/^([0-3]?\d)(?: de)? ([a-záéíóúñ]+\.?)(?: de)? (\d{4})$/u)(input);
560
+ const named = String$1.match(/^(?:el (?:día )?)?([0-3]?\d)(?: de)? ([a-záéíóúñ]+\.?)(?:(?: de| del)? (\d{4}))$/u)(input);
332
561
  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}`);
333
562
  const numeric = String$1.match(/^([0-3]?\d)[./-]([01]?\d)[./-](\d{4})$/u)(input);
334
- if (Option.isNone(numeric)) return Option.none();
335
- const year = validYear(textAt(numeric.value, 3));
336
- const month = Number(textAt(numeric.value, 2));
337
- const day = Number(textAt(numeric.value, 1));
338
- if (year === void 0 || month < 1 || month > 12) return Option.none();
339
- const value = isoDate(year, month, day);
340
- return isIsoDate(value) && value !== "9999-12-31" ? Option.some(fixedDatePeriod(value, `${day} de ${textAt(months, month - 1)} de ${year}`)) : Option.none();
563
+ if (Option.isSome(numeric)) {
564
+ const year = validYear(textAt(numeric.value, 3));
565
+ const month = Number(textAt(numeric.value, 2));
566
+ const day = Number(textAt(numeric.value, 1));
567
+ if (year !== void 0 && month >= 1 && month <= 12) {
568
+ const value = isoDate(year, month, day);
569
+ if (isIsoDate(value) && value !== "9999-12-31") return Option.some(fixedDatePeriod(value, `${day} de ${textAt(months, month - 1)} de ${year}`));
570
+ }
571
+ }
572
+ const current = String$1.match(/^(?:el (?:día )?)?([0-3]?\d)(?: de)? ([a-záéíóúñ]+\.?)$/u)(input);
573
+ if (Option.isSome(current)) return namedCurrentYearDatePeriod(textAt(current.value, 2), textAt(current.value, 1), monthNumber, currentDateLabel);
574
+ const relative = String$1.match(/^(?:el (?:día )?)?([0-3]?\d) (de .+|del .+)$/u)(input);
575
+ if (Option.isNone(relative)) return Option.none();
576
+ const periodText = afterDe(textAt(relative.value, 2));
577
+ const alias = periodAliases.find((entry) => entry[0] === periodText && entry[1] === "month");
578
+ if (alias === void 0) return Option.none();
579
+ const day = Number(textAt(relative.value, 1));
580
+ const month = relativePeriod(alias[1], alias[2], alias[3]);
581
+ return periodDay(month, day, `${day} ${withDe(alias[3])}`);
341
582
  };
342
583
  const quarterNumber = (value) => {
343
584
  if ((value.startsWith("q") || value.startsWith("t")) && value.length === 2) return Number(value.slice(1));
@@ -365,12 +606,13 @@ const parseQuarter = (input) => {
365
606
  const quarter = quarterNumber(textAt(standalone.value, 1));
366
607
  return quarter === void 0 ? Option.none() : Option.some(quarterOfRelativeYear(quarter, 0, `T${quarter}`));
367
608
  };
368
- const parseBasePeriod = (input) => {
369
- if (isIsoDate(input) && input !== "9999-12-31") return Option.some(fixedDatePeriod(input, input));
370
- const namedDate = parseNamedDate(input);
371
- if (Option.isSome(namedDate)) return namedDate;
372
- const quarter = parseQuarter(input);
373
- 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;
374
616
  const yearMatch = String$1.match(/^(?:(?:el )?año )?(\d{4})$/u)(input);
375
617
  if (Option.isSome(yearMatch)) {
376
618
  const year = validYear(textAt(yearMatch.value, 1));
@@ -380,7 +622,27 @@ const parseBasePeriod = (input) => {
380
622
  if (Option.isSome(monthYear)) {
381
623
  const month = monthNumber(textAt(monthYear.value, 1));
382
624
  const year = validYear(textAt(monthYear.value, 2));
383
- if (month !== void 0 && year !== void 0) return Option.some(fixedMonthPeriod(year, month, `${title(textAt(months, month - 1))} de ${year}`));
625
+ if (month !== void 0 && year !== void 0) return Option.some(fixedMonthPeriod(year, month, `${textAt(months, month - 1)} de ${year}`));
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
+ }
384
646
  }
385
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);
386
648
  if (Option.isSome(relativeMonth)) {
@@ -389,11 +651,11 @@ const parseBasePeriod = (input) => {
389
651
  const direction = relativeYearDirection(yearText);
390
652
  if (month !== void 0) {
391
653
  const canonicalYear = relativeYearName(direction);
392
- return Option.some(monthOfRelativeYear(month, direction, `${title(textAt(months, month - 1))} del ${canonicalYear}`));
654
+ return Option.some(monthOfRelativeYear(month, direction, `${textAt(months, month - 1)} del ${canonicalYear}`));
393
655
  }
394
656
  }
395
657
  const standaloneMonth = monthNumber(input);
396
- if (standaloneMonth !== void 0) return Option.some(monthOfRelativeYear(standaloneMonth, 0, title(textAt(months, standaloneMonth - 1))));
658
+ if (standaloneMonth !== void 0) return Option.some(monthOfRelativeYear(standaloneMonth, 0, textAt(months, standaloneMonth - 1)));
397
659
  const alias = periodAliases.find((entry) => entry[0] === input);
398
660
  if (alias !== void 0) return Option.some(relativePeriod(alias[1], alias[2], alias[3]));
399
661
  if ([
@@ -405,16 +667,34 @@ const parseBasePeriod = (input) => {
405
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"));
406
668
  if (input === "el fin de semana anterior al pasado") return Option.some(relativeWeekend(-2, input));
407
669
  if (input === "el fin de semana después del próximo") return Option.some(relativeWeekend(2, input));
408
- 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));
409
672
  };
410
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
+ }
411
688
  const edge = String$1.match(/^(?:el )?(inicio|comienzo|principio|fin|final)(?: de| del) (.+)$/u)(input);
412
689
  if (Option.isSome(edge)) {
413
690
  const edgeName = textAt(edge.value, 1);
414
- 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));
415
695
  if (Option.isSome(period)) {
416
696
  const isEnd = edgeName === "fin" || edgeName === "final";
417
- const canonical = `${isEnd ? "final" : "inicio"} de ${period.value.canonical}`;
697
+ const canonical = `${isEnd ? "final" : "inicio"} ${withDe(period.value.canonical)}`;
418
698
  return Option.some(isEnd ? periodEndDay(period.value, canonical) : periodStartDay(period.value, canonical));
419
699
  }
420
700
  }
@@ -425,45 +705,120 @@ const parsePeriod = (input) => {
425
705
  "todo el ",
426
706
  "toda la "
427
707
  ].find((prefix) => input.startsWith(prefix));
428
- 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);
710
+ };
711
+ const countedUnit = (value, amount) => {
712
+ const alias = unitAliases.find((entry) => entry[0] === value && entry[2] === (amount !== 1));
713
+ return alias === void 0 ? void 0 : units.find((entry) => entry.unit === alias[1]);
714
+ };
715
+ const writtenAmount = (value) => value === "un" || value === "una" ? Option.some(1) : parseTrailingCount(value);
716
+ const agreesWithArticle = (entry, article, plural = false) => article.length === 0 || article === definiteArticle(entry, plural);
717
+ const agreesWithIndefiniteArticle = (entry, article) => article === indefiniteArticle(entry);
718
+ const withoutAcute = (value) => value.normalize("NFD").replaceAll("́", "");
719
+ const agreesWithRollingModifier = (entry, modifier, future) => {
720
+ if (modifier === "anteriores" || modifier === "siguientes") return true;
721
+ if (!future && modifier === gendered(entry, "pasados", "pasadas")) return true;
722
+ const expected = rollingAdjective(entry, future, true);
723
+ return modifier === expected || modifier === withoutAcute(expected);
724
+ };
725
+ const agreesWithSingularModifier = (entry, modifier, future) => {
726
+ if (entry === void 0) return false;
727
+ if (future && modifier === "siguiente") return true;
728
+ const expected = rollingAdjective(entry, future, false);
729
+ return modifier === expected || modifier === withoutAcute(expected);
429
730
  };
430
- const countedUnit = (value) => unitAliases.find((entry) => entry[0] === value)?.[1];
431
731
  const parseCalendarOffset = (input) => {
432
- const past = String$1.match(/^hace ([1-9]\d*) (día|días|dias|semana|semanas|mes|meses|trimestre|trimestres|año|años|ano|anos)$/u)(input);
433
- const future = String$1.match(/^(?:dentro de|en) ([1-9]\d*) (día|días|dias|semana|semanas|mes|meses|trimestre|trimestres|año|años|ano|anos)$/u)(input);
434
- const match = Option.firstSomeOf([past, future]);
732
+ 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);
435
733
  if (Option.isNone(match)) return Option.none();
436
- const amount = parseTrailingCount(textAt(match.value, 1));
437
- const unit = countedUnit(textAt(match.value, 2));
438
- if (Option.isNone(amount) || unit === void 0) return Option.none();
439
- const entry = units.find((item) => item.unit === unit);
440
- if (entry === void 0) return Option.none();
441
- const direction = Option.isSome(past) ? -amount.value : amount.value;
442
- const canonical = direction < 0 ? `hace ${amount.value} ${amount.value === 1 ? entry.singular : entry.plural}` : `dentro de ${amount.value} ${amount.value === 1 ? entry.singular : entry.plural}`;
443
- return Option.some(candidate(periodRange(relativePeriod(unit, direction, canonical)), canonical));
734
+ const amount = writtenAmount(textAt(match.value, 2));
735
+ if (Option.isNone(amount)) return Option.none();
736
+ const entry = countedUnit(textAt(match.value, 3), amount.value);
737
+ const amountText = textAt(match.value, 2);
738
+ if (entry === void 0 || (amountText === "un" || amountText === "una") && !agreesWithIndefiniteArticle(entry, amountText)) return Option.none();
739
+ const isPast = textAt(match.value, 1) === "hace";
740
+ const quantity = amount.value === 1 ? indefiniteArticle(entry) : String(amount.value);
741
+ const noun = amount.value === 1 ? entry.singular : entry.plural;
742
+ const canonical = `${isPast ? "hace" : "dentro de"} ${quantity} ${noun}`;
743
+ const direction = isPast ? -amount.value : amount.value;
744
+ return Option.some(relativePeriod(entry.unit, direction, canonical));
444
745
  };
445
- const parseRollingPeriod = (input) => {
446
- const sinceAgo = String$1.match(/^desde hace ([1-9]\d*) (día|días|dias|semana|semanas|mes|meses|trimestre|trimestres|año|años|ano|anos)$/u)(input);
447
- const past = String$1.match(/^(?:(?:los|las) )?(?:últimos|ultimos|últimas|ultimas|pasados|pasadas|anteriores) ([1-9]\d*) (día|días|dias|semana|semanas|mes|meses|trimestre|trimestres|año|años|ano|anos)$/u)(input);
448
- const bare = String$1.match(/^([1-9]\d*) (día|días|dias|semana|semanas|mes|meses|trimestre|trimestres|año|años|ano|anos)$/u)(input);
449
- const future = String$1.match(/^(?:(?:los|las) )?(?:próximos|proximos|próximas|proximas|siguientes) ([1-9]\d*) (día|días|dias|semana|semanas|mes|meses|trimestre|trimestres|año|años|ano|anos)$/u)(input);
450
- const match = Option.firstSomeOf([
451
- sinceAgo,
452
- past,
453
- bare,
454
- future
455
- ]);
746
+ const rollingCanonical = (entry, amount, future) => {
747
+ if (amount > 1) return `${rollingAdjective(entry, future, true)} ${amount} ${entry.plural}`;
748
+ const article = indefiniteArticle(entry);
749
+ return future ? `desde ahora durante ${article} ${entry.singular}` : `desde hace ${article} ${entry.singular}`;
750
+ };
751
+ const rollingCandidate = (entry, amount, future) => candidate(future ? futureRange(amount, entry.unit) : trailingRange(amount, entry.unit), rollingCanonical(entry, amount, future));
752
+ const parseRollingSince = (input) => {
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);
754
+ if (Option.isSome(since)) {
755
+ const amount = writtenAmount(textAt(since.value, 1));
756
+ if (Option.isSome(amount)) {
757
+ const entry = countedUnit(textAt(since.value, 2), amount.value);
758
+ const amountText = textAt(since.value, 1);
759
+ if (entry !== void 0 && (amountText !== "un" && amountText !== "una" || agreesWithIndefiniteArticle(entry, amountText))) return Option.some(rollingCandidate(entry, amount.value, false));
760
+ }
761
+ }
762
+ return Option.none();
763
+ };
764
+ const parseSingularRolling = (input) => {
765
+ const futureSingular = String$1.match(/^desde ahora durante (un|una) (día|dia|semana|mes|trimestre|año|ano)$/u)(input);
766
+ if (Option.isSome(futureSingular)) {
767
+ const entry = countedUnit(textAt(futureSingular.value, 2), 1);
768
+ if (entry !== void 0 && agreesWithIndefiniteArticle(entry, textAt(futureSingular.value, 1))) return Option.some(rollingCandidate(entry, 1, true));
769
+ }
770
+ 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);
771
+ if (Option.isSome(lastSingular)) {
772
+ const entry = countedUnit(textAt(lastSingular.value, 3), 1);
773
+ const modifier = textAt(lastSingular.value, 2);
774
+ if (entry !== void 0 && agreesWithArticle(entry, textAt(lastSingular.value, 1)) && agreesWithSingularModifier(entry, modifier, false)) return Option.some(rollingCandidate(entry, 1, false));
775
+ }
776
+ 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);
777
+ if (Option.isSome(nextSingular)) {
778
+ const entry = countedUnit(textAt(nextSingular.value, 3), 1);
779
+ const modifier = textAt(nextSingular.value, 2);
780
+ if (entry !== void 0 && agreesWithArticle(entry, textAt(nextSingular.value, 1)) && agreesWithSingularModifier(entry, modifier, true)) return Option.some(rollingCandidate(entry, 1, true));
781
+ }
782
+ return Option.none();
783
+ };
784
+ const parseCountedRolling = (input) => {
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);
786
+ if (Option.isSome(counted)) {
787
+ const amount = parseTrailingCount(textAt(counted.value, 3));
788
+ if (Option.isSome(amount) && amount.value > 1) {
789
+ const entry = countedUnit(textAt(counted.value, 4), amount.value);
790
+ const modifier = textAt(counted.value, 2);
791
+ const future = modifier.startsWith("próxim") || modifier.startsWith("proxim") || modifier === "siguientes";
792
+ if (entry !== void 0 && agreesWithArticle(entry, textAt(counted.value, 1), true) && agreesWithRollingModifier(entry, modifier, future)) return Option.some(rollingCandidate(entry, amount.value, future));
793
+ }
794
+ }
795
+ 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);
796
+ if (Option.isNone(bare)) return Option.none();
797
+ const amount = parseTrailingCount(textAt(bare.value, 1));
798
+ if (Option.isNone(amount)) return Option.none();
799
+ const entry = countedUnit(textAt(bare.value, 2), amount.value);
800
+ return entry === void 0 ? Option.none() : Option.some(rollingCandidate(entry, amount.value, false));
801
+ };
802
+ const parseRollingPeriod = (input) => Option.firstSomeOf([
803
+ parseRollingSince(input),
804
+ parseSingularRolling(input),
805
+ parseCountedRolling(input)
806
+ ]);
807
+ const parseElidedDateRange = (input) => {
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);
809
+ const dashed = String$1.match(/^([0-3]?\d)[–—-]([0-3]?\d) (.+)$/u)(input);
810
+ const match = Option.firstSomeOf([joined, dashed]);
456
811
  if (Option.isNone(match)) return Option.none();
457
- const amount = parseTrailingCount(textAt(match.value, 1));
458
- const unit = countedUnit(textAt(match.value, 2));
459
- if (Option.isNone(amount) || unit === void 0) return Option.none();
460
- const entry = units.find((item) => item.unit === unit);
461
- if (entry === void 0) return Option.none();
462
- const isFuture = Option.isSome(future);
463
- const range = isFuture ? futureRange(amount.value, unit) : trailingRange(amount.value, unit);
464
- const direction = isFuture ? "próximos" : "últimos";
465
- const noun = amount.value === 1 ? entry.singular : entry.plural;
466
- return Option.some(candidate(range, `${direction} ${amount.value} ${noun}`));
812
+ const isJoined = Option.isSome(joined);
813
+ const lowerDay = textAt(match.value, 1) || textAt(match.value, 2);
814
+ const upperDay = textAt(match.value, isJoined ? 3 : 2);
815
+ const period = afterDe(textAt(match.value, isJoined ? 4 : 3));
816
+ return joinedPeriodCandidate(`desde ${lowerDay} ${withDe(period)} hasta ${upperDay} ${withDe(period)}`, [["desde ", " hasta "]], parsePeriod, (lower, upper) => `desde ${lower} hasta ${upper}`);
817
+ };
818
+ const inclusiveBoundaryCandidate = (input) => {
819
+ const match = String$1.match(/^hasta (.+) inclusive$/u)(input);
820
+ if (Option.isNone(match)) return Option.none();
821
+ return parsePeriod(textAt(match.value, 1)).pipe(Option.map((value) => periodBoundaryCandidate(value, "through", `hasta ${value.canonical} inclusive`)));
467
822
  };
468
823
  const boundaryCandidate = (input) => openBoundaryCandidate(input, [
469
824
  ["hasta antes de ", "before"],
@@ -486,12 +841,12 @@ const parseSpanish = (input) => {
486
841
  "a partir de ahora",
487
842
  "de ahora en adelante"
488
843
  ].includes(input)) return Option.some(candidate(fromNowRange(), "desde ahora"));
489
- const offset = parseCalendarOffset(input);
490
- if (Option.isSome(offset)) return offset;
491
844
  const rolling = parseRollingPeriod(input);
492
845
  if (Option.isSome(rolling)) return rolling;
493
846
  const toDate = toDatePhrases.find((entry) => entry.phrase === input);
494
847
  if (toDate !== void 0) return Option.some(candidate(periodToDateRange(toDate.entry.unit), toDate.entry.toDate));
848
+ const elided = parseElidedDateRange(input);
849
+ if (Option.isSome(elided)) return elided;
495
850
  const nowBounded = joinedNowCandidate(input, [
496
851
  ["desde ", " hasta ahora"],
497
852
  ["desde ", " hasta hoy"],
@@ -499,12 +854,17 @@ const parseSpanish = (input) => {
499
854
  ], [
500
855
  "desde ahora hasta ",
501
856
  "desde hoy hasta ",
502
- "entre hoy y "
857
+ "entre hoy y ",
858
+ "entre hoy y el ",
859
+ "ahora hasta "
503
860
  ], parsePeriod, (period) => `desde ${period} hasta ahora`, (period) => `desde ahora hasta ${period}`);
504
861
  if (Option.isSome(nowBounded)) return nowBounded;
505
862
  const bounded = joinedPeriodCandidate(input, [
863
+ ["desde el ", " hasta el "],
506
864
  ["desde ", " hasta "],
865
+ ["del ", " al "],
507
866
  ["de ", " a "],
867
+ ["entre el ", " y el "],
508
868
  ["entre ", " y "],
509
869
  ["", " - "],
510
870
  ["", " – "],
@@ -512,18 +872,32 @@ const parseSpanish = (input) => {
512
872
  ["", " hasta "]
513
873
  ], parsePeriod, (lower, upper) => `desde ${lower} hasta ${upper}`);
514
874
  if (Option.isSome(bounded)) return bounded;
875
+ const inclusive = inclusiveBoundaryCandidate(input);
876
+ if (Option.isSome(inclusive)) return inclusive;
515
877
  const boundary = boundaryCandidate(input);
516
878
  if (Option.isSome(boundary)) return boundary;
517
- return Option.map(parsePeriod(input), (period) => candidate(periodRange(period), period.canonical));
879
+ return parsePeriod(input).pipe(Option.map((period) => candidate(periodRange(period), period.canonical)));
518
880
  };
519
- const staticPeriodPhrases = [
520
- ...periodAliases.map((entry) => entry[0]),
881
+ const weekendPhrases = [
521
882
  "este fin de semana",
522
883
  "el fin de semana pasado",
523
884
  "el próximo fin de semana",
524
885
  "el fin de semana anterior al pasado",
525
- "el fin de semana después del próximo",
526
- ...periodAliases.flatMap((entry) => [`inicio de ${entry[0]}`, `final de ${entry[0]}`]),
886
+ "el fin de semana después del próximo"
887
+ ];
888
+ const edgePeriodPhrases = [
889
+ ...units.filter((entry) => entry.unit !== "day").flatMap((entry) => [
890
+ entry.current,
891
+ entry.previous,
892
+ entry.next
893
+ ]),
894
+ ...weekendPhrases,
895
+ ...months
896
+ ].flatMap((period) => [`inicio ${withDe(period)}`, `final ${withDe(period)}`]);
897
+ const staticPeriodPhrases = [
898
+ ...periodAliases.map((entry) => entry[0]),
899
+ ...weekendPhrases,
900
+ ...edgePeriodPhrases,
527
901
  ...[
528
902
  1,
529
903
  2,
@@ -537,13 +911,19 @@ const staticPeriodPhrases = [
537
911
  ]),
538
912
  ...months.flatMap((month) => [
539
913
  month,
914
+ `pasado ${month}`,
915
+ `próximo ${month}`,
916
+ `${month} pasado`,
917
+ `${month} próximo`,
540
918
  `${month} del año pasado`,
541
919
  `${month} del próximo año`
542
- ])
920
+ ]),
921
+ ...nextWeekdays.map((entry) => entry.phrase)
543
922
  ];
544
923
  const staticPeriods = periodsFromPhrases(staticPeriodPhrases, parsePeriod);
545
924
  const boundaryPrefixes = [
546
925
  "desde ",
926
+ "a partir de ",
547
927
  "antes de ",
548
928
  "hasta ",
549
929
  "después de "
@@ -553,10 +933,21 @@ const countedSuggestions = (input) => {
553
933
  if (amount === void 0) return [];
554
934
  return units.flatMap((entry) => {
555
935
  const noun = amount === 1 ? entry.singular : entry.plural;
936
+ if (amount === 1) {
937
+ const article = indefiniteArticle(entry);
938
+ return [
939
+ `${amount} ${noun}`,
940
+ `${rollingAdjective(entry, false, false)} ${noun}`,
941
+ `desde hace ${article} ${noun}`,
942
+ `desde ahora durante ${article} ${noun}`,
943
+ `hace ${article} ${noun}`,
944
+ `dentro de ${article} ${noun}`
945
+ ];
946
+ }
556
947
  return [
557
- `últimos ${amount} ${noun}`,
948
+ `${rollingAdjective(entry, false, true)} ${amount} ${noun}`,
558
949
  `${amount} ${noun}`,
559
- `próximos ${amount} ${noun}`,
950
+ `${rollingAdjective(entry, true, true)} ${amount} ${noun}`,
560
951
  `hace ${amount} ${noun}`,
561
952
  `dentro de ${amount} ${noun}`
562
953
  ];
@@ -565,6 +956,12 @@ const countedSuggestions = (input) => {
565
956
  const spanishSuggestionPhrases = [
566
957
  ...units.map((entry) => entry.toDate),
567
958
  ...units.map((entry) => entry.remaining),
959
+ ...units.flatMap((entry) => [
960
+ `${rollingAdjective(entry, false, false)} ${entry.singular}`,
961
+ `desde hace ${indefiniteArticle(entry)} ${entry.singular}`,
962
+ `desde ahora durante ${indefiniteArticle(entry)} ${entry.singular}`,
963
+ `durante ${definiteArticle(entry)} ${rollingAdjective(entry, true, false)} ${entry.singular}`
964
+ ]),
568
965
  ...staticPeriodPhrases,
569
966
  ...prefixNaturalPhrases(staticPeriodPhrases, boundaryPrefixes),
570
967
  "hasta ahora",
@@ -580,6 +977,17 @@ const suggestSpanish = (input, limit) => {
580
977
  ], limit);
581
978
  };
582
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
+ }
583
991
  const offset = calendarPeriodOffset(range);
584
992
  if (Option.isSome(offset) && Math.abs(offset.value.amount) > 1) {
585
993
  const entry = units.find((unit) => unit.unit === offset.value.unit);
@@ -592,26 +1000,25 @@ const renderSpanish = (range) => {
592
1000
  const future = futurePeriod(range);
593
1001
  if (Option.isSome(future)) {
594
1002
  const entry = units.find((unit) => unit.unit === future.value.unit);
595
- if (entry !== void 0) {
596
- const noun = future.value.amount === 1 ? entry.singular : entry.plural;
597
- return Option.some(`próximos ${future.value.amount} ${noun}`);
598
- }
1003
+ if (entry !== void 0) return Option.some(rollingCanonical(entry, future.value.amount, true));
599
1004
  }
600
1005
  const trailing = trailingPeriod(range);
601
1006
  if (Option.isSome(trailing)) {
602
1007
  const entry = units.find((unit) => unit.unit === trailing.value.unit);
603
- if (entry !== void 0) {
604
- const noun = trailing.value.amount === 1 ? entry.singular : entry.plural;
605
- return Option.some(`últimos ${trailing.value.amount} ${noun}`);
606
- }
1008
+ if (entry !== void 0) return Option.some(rollingCanonical(entry, trailing.value.amount, false));
607
1009
  }
608
- const periods = [...staticPeriods, ...periodsFromPhrases([...datedPeriods(range, months), ...datedQuarterPeriods(range)], parsePeriod)];
1010
+ const periods = [
1011
+ ...staticPeriods,
1012
+ ...currentYearDatePeriods(range, currentDateLabel),
1013
+ ...periodsFromPhrases([...datedPeriods(range, months), ...datedQuarterPeriods(range)], parsePeriod)
1014
+ ];
609
1015
  return renderPeriodRange(range, [...units.map((entry) => candidate(periodToDateRange(entry.unit), entry.toDate)), ...units.map((entry) => candidate(remainingPeriodRange(entry.unit), entry.remaining))], periods, (period) => `desde ${period}`, (period) => `antes de ${period}`, (period) => `hasta ${period}`, (period) => `después de ${period}`, (lower, upper) => `desde ${lower} hasta ${upper}`, (period) => `desde ${period} hasta ahora`, (period) => `desde ahora hasta ${period}`, () => "hasta ahora", () => "desde ahora");
610
1016
  };
611
1017
  const SpanishContribution = new BaseLanguageContribution({
612
1018
  locale: "es",
613
1019
  vocabulary: [
614
1020
  ...months,
1021
+ ...weekdays,
615
1022
  ...monthAbbreviations.flatMap((aliases) => aliases),
616
1023
  ...units.flatMap((entry) => [
617
1024
  entry.singular,
@@ -645,8 +1052,8 @@ const SpanishContribution = new BaseLanguageContribution({
645
1052
  "resto",
646
1053
  "últimos"
647
1054
  ],
648
- normalize: normalizeNaturalText,
649
- correct: correctWhitespaceSeparatedText,
1055
+ normalize: normalizeSpanish,
1056
+ correct: correctSpanish,
650
1057
  parseExact: parseSpanish,
651
1058
  suggest: suggestSpanish,
652
1059
  render: renderSpanish