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, 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/es.ts
10
10
  const months = [
@@ -45,6 +45,7 @@ const units = [
45
45
  unit: "day",
46
46
  singular: "día",
47
47
  plural: "días",
48
+ gender: "masculine",
48
49
  current: "hoy",
49
50
  previous: "ayer",
50
51
  next: "mañana",
@@ -55,6 +56,7 @@ const units = [
55
56
  unit: "week",
56
57
  singular: "semana",
57
58
  plural: "semanas",
59
+ gender: "feminine",
58
60
  current: "esta semana",
59
61
  previous: "la semana pasada",
60
62
  next: "la próxima semana",
@@ -65,6 +67,7 @@ const units = [
65
67
  unit: "month",
66
68
  singular: "mes",
67
69
  plural: "meses",
70
+ gender: "masculine",
68
71
  current: "este mes",
69
72
  previous: "el mes pasado",
70
73
  next: "el próximo mes",
@@ -75,6 +78,7 @@ const units = [
75
78
  unit: "quarter",
76
79
  singular: "trimestre",
77
80
  plural: "trimestres",
81
+ gender: "masculine",
78
82
  current: "este trimestre",
79
83
  previous: "el trimestre pasado",
80
84
  next: "el próximo trimestre",
@@ -85,6 +89,7 @@ const units = [
85
89
  unit: "year",
86
90
  singular: "año",
87
91
  plural: "años",
92
+ gender: "masculine",
88
93
  current: "este año",
89
94
  previous: "el año pasado",
90
95
  next: "el próximo año",
@@ -92,21 +97,89 @@ const units = [
92
97
  remaining: "resto del año"
93
98
  }
94
99
  ];
95
- const title = (value) => `${value.slice(0, 1).toLocaleUpperCase("es")}${value.slice(1)}`;
100
+ const gendered = (entry, masculine, feminine) => entry.gender === "feminine" ? feminine : masculine;
101
+ const definiteArticle = (entry, plural = false) => gendered(entry, plural ? "los" : "el", plural ? "las" : "la");
102
+ const indefiniteArticle = (entry) => gendered(entry, "un", "una");
103
+ const withDe = (period) => period.startsWith("el ") ? `del ${period.slice(3)}` : `de ${period}`;
104
+ const afterDe = (period) => {
105
+ if (period.startsWith("del ")) return `el ${period.slice(4)}`;
106
+ return period.startsWith("de ") ? period.slice(3) : period;
107
+ };
108
+ const rollingAdjective = (entry, future, plural) => {
109
+ if (future) return gendered(entry, plural ? "próximos" : "próximo", plural ? "próximas" : "próxima");
110
+ return gendered(entry, plural ? "últimos" : "último", plural ? "últimas" : "última");
111
+ };
96
112
  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"]
113
+ [
114
+ "día",
115
+ "day",
116
+ false
117
+ ],
118
+ [
119
+ "dia",
120
+ "day",
121
+ false
122
+ ],
123
+ [
124
+ "días",
125
+ "day",
126
+ true
127
+ ],
128
+ [
129
+ "dias",
130
+ "day",
131
+ true
132
+ ],
133
+ [
134
+ "semana",
135
+ "week",
136
+ false
137
+ ],
138
+ [
139
+ "semanas",
140
+ "week",
141
+ true
142
+ ],
143
+ [
144
+ "mes",
145
+ "month",
146
+ false
147
+ ],
148
+ [
149
+ "meses",
150
+ "month",
151
+ true
152
+ ],
153
+ [
154
+ "trimestre",
155
+ "quarter",
156
+ false
157
+ ],
158
+ [
159
+ "trimestres",
160
+ "quarter",
161
+ true
162
+ ],
163
+ [
164
+ "año",
165
+ "year",
166
+ false
167
+ ],
168
+ [
169
+ "ano",
170
+ "year",
171
+ false
172
+ ],
173
+ [
174
+ "años",
175
+ "year",
176
+ true
177
+ ],
178
+ [
179
+ "anos",
180
+ "year",
181
+ true
182
+ ]
110
183
  ];
111
184
  const periodAliases = [
112
185
  ...units.flatMap((entry) => [
@@ -129,6 +202,44 @@ const periodAliases = [
129
202
  entry.next
130
203
  ]
131
204
  ]),
205
+ ...units.filter((entry) => entry.unit !== "day").flatMap((entry) => [
206
+ [
207
+ `${entry.singular} actual`,
208
+ entry.unit,
209
+ 0,
210
+ entry.current
211
+ ],
212
+ [
213
+ `${definiteArticle(entry)} ${entry.singular} anterior`,
214
+ entry.unit,
215
+ -1,
216
+ entry.previous
217
+ ],
218
+ [
219
+ `${entry.singular} anterior`,
220
+ entry.unit,
221
+ -1,
222
+ entry.previous
223
+ ],
224
+ [
225
+ `${definiteArticle(entry)} ${entry.singular} siguiente`,
226
+ entry.unit,
227
+ 1,
228
+ entry.next
229
+ ],
230
+ [
231
+ `${entry.singular} siguiente`,
232
+ entry.unit,
233
+ 1,
234
+ entry.next
235
+ ],
236
+ [
237
+ `${rollingAdjective(entry, true, false)} ${entry.singular}`,
238
+ entry.unit,
239
+ 1,
240
+ entry.next
241
+ ]
242
+ ]),
132
243
  [
133
244
  "semana pasada",
134
245
  "week",
@@ -237,12 +348,36 @@ const periodAliases = [
237
348
  -2,
238
349
  "anteayer"
239
350
  ],
351
+ [
352
+ "día antes de ayer",
353
+ "day",
354
+ -2,
355
+ "anteayer"
356
+ ],
357
+ [
358
+ "el día antes de ayer",
359
+ "day",
360
+ -2,
361
+ "anteayer"
362
+ ],
240
363
  [
241
364
  "pasado mañana",
242
365
  "day",
243
366
  2,
244
367
  "pasado mañana"
245
368
  ],
369
+ [
370
+ "día después de mañana",
371
+ "day",
372
+ 2,
373
+ "pasado mañana"
374
+ ],
375
+ [
376
+ "el día después de mañana",
377
+ "day",
378
+ 2,
379
+ "pasado mañana"
380
+ ],
246
381
  [
247
382
  "la semana anterior a la pasada",
248
383
  "week",
@@ -327,17 +462,30 @@ const monthNumber = (value) => {
327
462
  const short = monthAbbreviations.findIndex((aliases) => aliases.some((alias) => alias === normalized));
328
463
  return short === -1 ? void 0 : short + 1;
329
464
  };
465
+ const currentDateLabel = (day, month) => `${day} de ${textAt(months, month - 1)}`;
330
466
  const parseNamedDate = (input) => {
331
- const named = String$1.match(/^([0-3]?\d)(?: de)? ([a-záéíóúñ]+\.?)(?: de)? (\d{4})$/u)(input);
467
+ const named = String$1.match(/^(?:el (?:día )?)?([0-3]?\d)(?: de)? ([a-záéíóúñ]+\.?)(?:(?: de| del)? (\d{4}))$/u)(input);
332
468
  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
469
  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();
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
+ }
479
+ const current = String$1.match(/^(?:el (?:día )?)?([0-3]?\d)(?: de)? ([a-záéíóúñ]+\.?)$/u)(input);
480
+ if (Option.isSome(current)) return namedCurrentYearDatePeriod(textAt(current.value, 2), textAt(current.value, 1), monthNumber, currentDateLabel);
481
+ const relative = String$1.match(/^(?:el (?:día )?)?([0-3]?\d) (de .+|del .+)$/u)(input);
482
+ if (Option.isNone(relative)) return Option.none();
483
+ const periodText = afterDe(textAt(relative.value, 2));
484
+ const alias = periodAliases.find((entry) => entry[0] === periodText && entry[1] === "month");
485
+ if (alias === void 0) return Option.none();
486
+ const day = Number(textAt(relative.value, 1));
487
+ const month = relativePeriod(alias[1], alias[2], alias[3]);
488
+ return periodDay(month, day, `${day} ${withDe(alias[3])}`);
341
489
  };
342
490
  const quarterNumber = (value) => {
343
491
  if ((value.startsWith("q") || value.startsWith("t")) && value.length === 2) return Number(value.slice(1));
@@ -366,7 +514,8 @@ const parseQuarter = (input) => {
366
514
  return quarter === void 0 ? Option.none() : Option.some(quarterOfRelativeYear(quarter, 0, `T${quarter}`));
367
515
  };
368
516
  const parseBasePeriod = (input) => {
369
- if (isIsoDate(input) && input !== "9999-12-31") return Option.some(fixedDatePeriod(input, input));
517
+ const absoluteDate = absoluteDatePeriod(input, "es");
518
+ if (Option.isSome(absoluteDate)) return absoluteDate;
370
519
  const namedDate = parseNamedDate(input);
371
520
  if (Option.isSome(namedDate)) return namedDate;
372
521
  const quarter = parseQuarter(input);
@@ -380,7 +529,7 @@ const parseBasePeriod = (input) => {
380
529
  if (Option.isSome(monthYear)) {
381
530
  const month = monthNumber(textAt(monthYear.value, 1));
382
531
  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}`));
532
+ if (month !== void 0 && year !== void 0) return Option.some(fixedMonthPeriod(year, month, `${textAt(months, month - 1)} de ${year}`));
384
533
  }
385
534
  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
535
  if (Option.isSome(relativeMonth)) {
@@ -389,11 +538,11 @@ const parseBasePeriod = (input) => {
389
538
  const direction = relativeYearDirection(yearText);
390
539
  if (month !== void 0) {
391
540
  const canonicalYear = relativeYearName(direction);
392
- return Option.some(monthOfRelativeYear(month, direction, `${title(textAt(months, month - 1))} del ${canonicalYear}`));
541
+ return Option.some(monthOfRelativeYear(month, direction, `${textAt(months, month - 1)} del ${canonicalYear}`));
393
542
  }
394
543
  }
395
544
  const standaloneMonth = monthNumber(input);
396
- if (standaloneMonth !== void 0) return Option.some(monthOfRelativeYear(standaloneMonth, 0, title(textAt(months, standaloneMonth - 1))));
545
+ if (standaloneMonth !== void 0) return Option.some(monthOfRelativeYear(standaloneMonth, 0, textAt(months, standaloneMonth - 1)));
397
546
  const alias = periodAliases.find((entry) => entry[0] === input);
398
547
  if (alias !== void 0) return Option.some(relativePeriod(alias[1], alias[2], alias[3]));
399
548
  if ([
@@ -414,7 +563,7 @@ const parsePeriod = (input) => {
414
563
  const period = parseBasePeriod(textAt(edge.value, 2));
415
564
  if (Option.isSome(period)) {
416
565
  const isEnd = edgeName === "fin" || edgeName === "final";
417
- const canonical = `${isEnd ? "final" : "inicio"} de ${period.value.canonical}`;
566
+ const canonical = `${isEnd ? "final" : "inicio"} ${withDe(period.value.canonical)}`;
418
567
  return Option.some(isEnd ? periodEndDay(period.value, canonical) : periodStartDay(period.value, canonical));
419
568
  }
420
569
  }
@@ -427,43 +576,106 @@ const parsePeriod = (input) => {
427
576
  ].find((prefix) => input.startsWith(prefix));
428
577
  return parseBasePeriod(wrapper === void 0 ? input : input.slice(wrapper.length));
429
578
  };
430
- const countedUnit = (value) => unitAliases.find((entry) => entry[0] === value)?.[1];
579
+ const countedUnit = (value, amount) => {
580
+ const alias = unitAliases.find((entry) => entry[0] === value && entry[2] === (amount !== 1));
581
+ return alias === void 0 ? void 0 : units.find((entry) => entry.unit === alias[1]);
582
+ };
583
+ const writtenAmount = (value) => value === "un" || value === "una" ? Option.some(1) : parseTrailingCount(value);
584
+ const agreesWithArticle = (entry, article, plural = false) => article.length === 0 || article === definiteArticle(entry, plural);
585
+ const agreesWithIndefiniteArticle = (entry, article) => article === indefiniteArticle(entry);
586
+ const withoutAcute = (value) => value.normalize("NFD").replaceAll("́", "");
587
+ const agreesWithRollingModifier = (entry, modifier, future) => {
588
+ if (modifier === "anteriores" || modifier === "siguientes") return true;
589
+ if (!future && modifier === gendered(entry, "pasados", "pasadas")) return true;
590
+ const expected = rollingAdjective(entry, future, true);
591
+ return modifier === expected || modifier === withoutAcute(expected);
592
+ };
593
+ const agreesWithSingularModifier = (entry, modifier, future) => {
594
+ if (entry === void 0) return false;
595
+ if (future && modifier === "siguiente") return true;
596
+ const expected = rollingAdjective(entry, future, false);
597
+ return modifier === expected || modifier === withoutAcute(expected);
598
+ };
431
599
  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]);
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);
435
601
  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));
602
+ const amount = writtenAmount(textAt(match.value, 2));
603
+ if (Option.isNone(amount)) return Option.none();
604
+ const entry = countedUnit(textAt(match.value, 3), amount.value);
605
+ const amountText = textAt(match.value, 2);
606
+ if (entry === void 0 || (amountText === "un" || amountText === "una") && !agreesWithIndefiniteArticle(entry, amountText)) return Option.none();
607
+ const isPast = textAt(match.value, 1) === "hace";
608
+ const quantity = amount.value === 1 ? indefiniteArticle(entry) : String(amount.value);
609
+ const noun = amount.value === 1 ? entry.singular : entry.plural;
610
+ const canonical = `${isPast ? "hace" : "dentro de"} ${quantity} ${noun}`;
611
+ const direction = isPast ? -amount.value : amount.value;
612
+ return Option.some(candidate(periodRange(relativePeriod(entry.unit, direction, canonical)), canonical));
444
613
  };
614
+ const rollingCanonical = (entry, amount, future) => {
615
+ if (amount > 1) return `${rollingAdjective(entry, future, true)} ${amount} ${entry.plural}`;
616
+ const article = indefiniteArticle(entry);
617
+ return future ? `desde ahora durante ${article} ${entry.singular}` : `desde hace ${article} ${entry.singular}`;
618
+ };
619
+ const rollingCandidate = (entry, amount, future) => candidate(future ? futureRange(amount, entry.unit) : trailingRange(amount, entry.unit), rollingCanonical(entry, amount, future));
445
620
  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
- ]);
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);
622
+ if (Option.isSome(since)) {
623
+ const amount = writtenAmount(textAt(since.value, 1));
624
+ if (Option.isSome(amount)) {
625
+ const entry = countedUnit(textAt(since.value, 2), amount.value);
626
+ const amountText = textAt(since.value, 1);
627
+ if (entry !== void 0 && (amountText !== "un" && amountText !== "una" || agreesWithIndefiniteArticle(entry, amountText))) return Option.some(rollingCandidate(entry, amount.value, false));
628
+ }
629
+ }
630
+ const futureSingular = String$1.match(/^desde ahora durante (un|una) (día|dia|semana|mes|trimestre|año|ano)$/u)(input);
631
+ if (Option.isSome(futureSingular)) {
632
+ const entry = countedUnit(textAt(futureSingular.value, 2), 1);
633
+ if (entry !== void 0 && agreesWithIndefiniteArticle(entry, textAt(futureSingular.value, 1))) return Option.some(rollingCandidate(entry, 1, true));
634
+ }
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);
636
+ if (Option.isSome(lastSingular)) {
637
+ const entry = countedUnit(textAt(lastSingular.value, 3), 1);
638
+ const modifier = textAt(lastSingular.value, 2);
639
+ if (entry !== void 0 && agreesWithArticle(entry, textAt(lastSingular.value, 1)) && agreesWithSingularModifier(entry, modifier, false)) return Option.some(rollingCandidate(entry, 1, false));
640
+ }
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);
642
+ if (Option.isSome(nextSingular)) {
643
+ const entry = countedUnit(textAt(nextSingular.value, 3), 1);
644
+ const modifier = textAt(nextSingular.value, 2);
645
+ if (entry !== void 0 && agreesWithArticle(entry, textAt(nextSingular.value, 1)) && agreesWithSingularModifier(entry, modifier, true)) return Option.some(rollingCandidate(entry, 1, true));
646
+ }
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);
648
+ if (Option.isSome(counted)) {
649
+ const amount = parseTrailingCount(textAt(counted.value, 3));
650
+ if (Option.isSome(amount) && amount.value > 1) {
651
+ const entry = countedUnit(textAt(counted.value, 4), amount.value);
652
+ const modifier = textAt(counted.value, 2);
653
+ const future = modifier.startsWith("próxim") || modifier.startsWith("proxim") || modifier === "siguientes";
654
+ if (entry !== void 0 && agreesWithArticle(entry, textAt(counted.value, 1), true) && agreesWithRollingModifier(entry, modifier, future)) return Option.some(rollingCandidate(entry, amount.value, future));
655
+ }
656
+ }
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);
658
+ if (Option.isNone(bare)) return Option.none();
659
+ const amount = parseTrailingCount(textAt(bare.value, 1));
660
+ if (Option.isNone(amount)) return Option.none();
661
+ const entry = countedUnit(textAt(bare.value, 2), amount.value);
662
+ return entry === void 0 ? Option.none() : Option.some(rollingCandidate(entry, amount.value, false));
663
+ };
664
+ const parseElidedDateRange = (input) => {
665
+ 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
+ const dashed = String$1.match(/^([0-3]?\d)[–—-]([0-3]?\d) (.+)$/u)(input);
667
+ const match = Option.firstSomeOf([joined, dashed]);
456
668
  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}`));
669
+ const isJoined = Option.isSome(joined);
670
+ const lowerDay = textAt(match.value, 1) || textAt(match.value, 2);
671
+ const upperDay = textAt(match.value, isJoined ? 3 : 2);
672
+ const period = afterDe(textAt(match.value, isJoined ? 4 : 3));
673
+ return joinedPeriodCandidate(`desde ${lowerDay} ${withDe(period)} hasta ${upperDay} ${withDe(period)}`, [["desde ", " hasta "]], parsePeriod, (lower, upper) => `desde ${lower} hasta ${upper}`);
674
+ };
675
+ const inclusiveBoundaryCandidate = (input) => {
676
+ const match = String$1.match(/^hasta (.+) inclusive$/u)(input);
677
+ if (Option.isNone(match)) return Option.none();
678
+ return parsePeriod(textAt(match.value, 1)).pipe(Option.map((value) => periodBoundaryCandidate(value, "through", `hasta ${value.canonical} inclusive`)));
467
679
  };
468
680
  const boundaryCandidate = (input) => openBoundaryCandidate(input, [
469
681
  ["hasta antes de ", "before"],
@@ -492,6 +704,8 @@ const parseSpanish = (input) => {
492
704
  if (Option.isSome(rolling)) return rolling;
493
705
  const toDate = toDatePhrases.find((entry) => entry.phrase === input);
494
706
  if (toDate !== void 0) return Option.some(candidate(periodToDateRange(toDate.entry.unit), toDate.entry.toDate));
707
+ const elided = parseElidedDateRange(input);
708
+ if (Option.isSome(elided)) return elided;
495
709
  const nowBounded = joinedNowCandidate(input, [
496
710
  ["desde ", " hasta ahora"],
497
711
  ["desde ", " hasta hoy"],
@@ -499,12 +713,16 @@ const parseSpanish = (input) => {
499
713
  ], [
500
714
  "desde ahora hasta ",
501
715
  "desde hoy hasta ",
502
- "entre hoy y "
716
+ "entre hoy y ",
717
+ "entre hoy y el "
503
718
  ], parsePeriod, (period) => `desde ${period} hasta ahora`, (period) => `desde ahora hasta ${period}`);
504
719
  if (Option.isSome(nowBounded)) return nowBounded;
505
720
  const bounded = joinedPeriodCandidate(input, [
721
+ ["desde el ", " hasta el "],
506
722
  ["desde ", " hasta "],
723
+ ["del ", " al "],
507
724
  ["de ", " a "],
725
+ ["entre el ", " y el "],
508
726
  ["entre ", " y "],
509
727
  ["", " - "],
510
728
  ["", " – "],
@@ -512,18 +730,32 @@ const parseSpanish = (input) => {
512
730
  ["", " hasta "]
513
731
  ], parsePeriod, (lower, upper) => `desde ${lower} hasta ${upper}`);
514
732
  if (Option.isSome(bounded)) return bounded;
733
+ const inclusive = inclusiveBoundaryCandidate(input);
734
+ if (Option.isSome(inclusive)) return inclusive;
515
735
  const boundary = boundaryCandidate(input);
516
736
  if (Option.isSome(boundary)) return boundary;
517
- return Option.map(parsePeriod(input), (period) => candidate(periodRange(period), period.canonical));
737
+ return parsePeriod(input).pipe(Option.map((period) => candidate(periodRange(period), period.canonical)));
518
738
  };
519
- const staticPeriodPhrases = [
520
- ...periodAliases.map((entry) => entry[0]),
739
+ const weekendPhrases = [
521
740
  "este fin de semana",
522
741
  "el fin de semana pasado",
523
742
  "el próximo fin de semana",
524
743
  "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]}`]),
744
+ "el fin de semana después del próximo"
745
+ ];
746
+ const edgePeriodPhrases = [
747
+ ...units.filter((entry) => entry.unit !== "day").flatMap((entry) => [
748
+ entry.current,
749
+ entry.previous,
750
+ entry.next
751
+ ]),
752
+ ...weekendPhrases,
753
+ ...months
754
+ ].flatMap((period) => [`inicio ${withDe(period)}`, `final ${withDe(period)}`]);
755
+ const staticPeriodPhrases = [
756
+ ...periodAliases.map((entry) => entry[0]),
757
+ ...weekendPhrases,
758
+ ...edgePeriodPhrases,
527
759
  ...[
528
760
  1,
529
761
  2,
@@ -544,6 +776,7 @@ const staticPeriodPhrases = [
544
776
  const staticPeriods = periodsFromPhrases(staticPeriodPhrases, parsePeriod);
545
777
  const boundaryPrefixes = [
546
778
  "desde ",
779
+ "a partir de ",
547
780
  "antes de ",
548
781
  "hasta ",
549
782
  "después de "
@@ -553,10 +786,21 @@ const countedSuggestions = (input) => {
553
786
  if (amount === void 0) return [];
554
787
  return units.flatMap((entry) => {
555
788
  const noun = amount === 1 ? entry.singular : entry.plural;
789
+ if (amount === 1) {
790
+ const article = indefiniteArticle(entry);
791
+ return [
792
+ `${amount} ${noun}`,
793
+ `${rollingAdjective(entry, false, false)} ${noun}`,
794
+ `desde hace ${article} ${noun}`,
795
+ `desde ahora durante ${article} ${noun}`,
796
+ `hace ${article} ${noun}`,
797
+ `dentro de ${article} ${noun}`
798
+ ];
799
+ }
556
800
  return [
557
- `últimos ${amount} ${noun}`,
801
+ `${rollingAdjective(entry, false, true)} ${amount} ${noun}`,
558
802
  `${amount} ${noun}`,
559
- `próximos ${amount} ${noun}`,
803
+ `${rollingAdjective(entry, true, true)} ${amount} ${noun}`,
560
804
  `hace ${amount} ${noun}`,
561
805
  `dentro de ${amount} ${noun}`
562
806
  ];
@@ -565,6 +809,12 @@ const countedSuggestions = (input) => {
565
809
  const spanishSuggestionPhrases = [
566
810
  ...units.map((entry) => entry.toDate),
567
811
  ...units.map((entry) => entry.remaining),
812
+ ...units.flatMap((entry) => [
813
+ `${rollingAdjective(entry, false, false)} ${entry.singular}`,
814
+ `desde hace ${indefiniteArticle(entry)} ${entry.singular}`,
815
+ `desde ahora durante ${indefiniteArticle(entry)} ${entry.singular}`,
816
+ `durante ${definiteArticle(entry)} ${rollingAdjective(entry, true, false)} ${entry.singular}`
817
+ ]),
568
818
  ...staticPeriodPhrases,
569
819
  ...prefixNaturalPhrases(staticPeriodPhrases, boundaryPrefixes),
570
820
  "hasta ahora",
@@ -592,20 +842,18 @@ const renderSpanish = (range) => {
592
842
  const future = futurePeriod(range);
593
843
  if (Option.isSome(future)) {
594
844
  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
- }
845
+ if (entry !== void 0) return Option.some(rollingCanonical(entry, future.value.amount, true));
599
846
  }
600
847
  const trailing = trailingPeriod(range);
601
848
  if (Option.isSome(trailing)) {
602
849
  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
- }
850
+ if (entry !== void 0) return Option.some(rollingCanonical(entry, trailing.value.amount, false));
607
851
  }
608
- const periods = [...staticPeriods, ...periodsFromPhrases([...datedPeriods(range, months), ...datedQuarterPeriods(range)], parsePeriod)];
852
+ const periods = [
853
+ ...staticPeriods,
854
+ ...currentYearDatePeriods(range, currentDateLabel),
855
+ ...periodsFromPhrases([...datedPeriods(range, months), ...datedQuarterPeriods(range)], parsePeriod)
856
+ ];
609
857
  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
858
  };
611
859
  const SpanishContribution = new BaseLanguageContribution({