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.
@@ -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 { calendarPeriodOffset, candidate, datedPeriods, datedQuarterPeriods, fixedDatePeriod, fixedMonthPeriod, fixedQuarterPeriod, fixedYearPeriod, fromNowRange, futurePeriod, futureRange, joinedNowCandidate, joinedPeriodCandidate, monthOfRelativeYear, namedDatePeriod, openBoundaryCandidate, parseTrailingCount, 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, periodEndDay, periodPreviousDay, 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/en.ts
10
9
  const months = [
@@ -35,12 +34,59 @@ const monthAbbreviations = [
35
34
  ["nov"],
36
35
  ["dec"]
37
36
  ];
37
+ const weekdays = [
38
+ "monday",
39
+ "tuesday",
40
+ "wednesday",
41
+ "thursday",
42
+ "friday",
43
+ "saturday",
44
+ "sunday"
45
+ ];
46
+ const nextWeekdayPhrases = weekdays.map((weekday) => `next ${weekday}`);
38
47
  const quarterNames = [
39
48
  "first",
40
49
  "second",
41
50
  "third",
42
51
  "fourth"
43
52
  ];
53
+ const englishCountWords = [
54
+ "one",
55
+ "two",
56
+ "three",
57
+ "four",
58
+ "five",
59
+ "six",
60
+ "seven",
61
+ "eight",
62
+ "nine",
63
+ "ten",
64
+ "eleven",
65
+ "twelve",
66
+ "thirteen",
67
+ "fourteen",
68
+ "fifteen",
69
+ "sixteen",
70
+ "seventeen",
71
+ "eighteen",
72
+ "nineteen",
73
+ "twenty"
74
+ ];
75
+ const englishCountOnes = englishCountWords.slice(0, 9).map((word, index) => [index + 1, word]);
76
+ const englishCountAliases = [...sequentialCountAliases(englishCountWords.map((word) => [word]), 1), ...compoundCountAliases(decimalTens([
77
+ "twenty",
78
+ "thirty",
79
+ "forty",
80
+ "fifty",
81
+ "sixty",
82
+ "seventy",
83
+ "eighty",
84
+ "ninety"
85
+ ]), englishCountOnes, (ten, one) => [`${ten} ${one}`, `${ten}-${one}`])];
86
+ const normalizeEnglishCounts = compileCountAliasNormalizer(englishCountAliases);
87
+ const englishCountVocabulary = new Set(countAliasVocabulary(englishCountAliases));
88
+ const correctEnglish = (input, vocabulary) => correctWhitespaceSeparatedText(input, vocabulary, englishCountVocabulary);
89
+ const normalizeEnglish = (input, locale) => normalizeEnglishCounts(normalizeNaturalText(input, locale));
44
90
  const units = [
45
91
  [
46
92
  "day",
@@ -159,19 +205,25 @@ const parseQuarter = (input) => {
159
205
  const quarter = quarterNumber(textAt(standaloneMatch.value, 1));
160
206
  return quarter === void 0 ? Option.none() : Option.some(quarterOfRelativeYear(quarter, 0, `Q${quarter}`));
161
207
  };
208
+ const currentDateLabel = (day, month) => `${day} ${title(textAt(months, month - 1))}`;
162
209
  const parseNamedDate = (input) => {
163
- const dayFirst = String$1.match(/^([0-3]?\d)(?:st|nd|rd|th)? ([a-z]+\.?) (\d{4})$/u)(input);
210
+ const dayFirst = String$1.match(/^(?:the )?([0-3]?\d)(?:st|nd|rd|th)?(?: of)? ([a-z]+\.?),? (\d{4})$/u)(input);
164
211
  if (Option.isSome(dayFirst)) return namedDatePeriod(textAt(dayFirst.value, 3), textAt(dayFirst.value, 2), textAt(dayFirst.value, 1), monthNumber, (day, month, year) => `${day} ${title(textAt(months, month - 1))} ${year}`);
165
- const monthFirst = String$1.match(/^([a-z]+\.?) ([0-3]?\d)(?:st|nd|rd|th)?,? (\d{4})$/u)(input);
212
+ const monthFirst = String$1.match(/^([a-z]+\.?) (?:the )?([0-3]?\d)(?:st|nd|rd|th)?,? (\d{4})$/u)(input);
166
213
  if (Option.isSome(monthFirst)) return namedDatePeriod(textAt(monthFirst.value, 3), textAt(monthFirst.value, 1), textAt(monthFirst.value, 2), monthNumber, (day, month, year) => `${day} ${title(textAt(months, month - 1))} ${year}`);
167
- return Option.none();
214
+ const currentDayFirst = String$1.match(/^(?:the )?([0-3]?\d)(?:st|nd|rd|th)?(?: of)? ([a-z]+\.?)$/u)(input);
215
+ const currentMonthFirst = String$1.match(/^([a-z]+\.?) (?:the )?([0-3]?\d)(?:st|nd|rd|th)?$/u)(input);
216
+ const current = Option.firstSomeOf([currentDayFirst, currentMonthFirst]);
217
+ if (Option.isNone(current)) return Option.none();
218
+ return Option.isSome(currentDayFirst) ? namedCurrentYearDatePeriod(textAt(current.value, 2), textAt(current.value, 1), monthNumber, currentDateLabel) : namedCurrentYearDatePeriod(textAt(current.value, 1), textAt(current.value, 2), monthNumber, currentDateLabel);
168
219
  };
169
- const parseBasePeriod = (input) => {
170
- if (isIsoDate(input) && input !== "9999-12-31") return Option.some(fixedDatePeriod(input, input));
171
- const namedDate = parseNamedDate(input);
172
- if (Option.isSome(namedDate)) return namedDate;
173
- const quarter = parseQuarter(input);
174
- if (Option.isSome(quarter)) return quarter;
220
+ const parseDatedPeriod = (input) => {
221
+ const knownPeriod = Option.firstSomeOf([
222
+ absoluteDatePeriod(input, "en"),
223
+ parseNamedDate(input),
224
+ parseQuarter(input)
225
+ ]);
226
+ if (Option.isSome(knownPeriod)) return knownPeriod;
175
227
  const yearMatch = String$1.match(/^(?:(?:the )?(?:calendar )?year )?(\d{4})$/u)(input);
176
228
  if (Option.isSome(yearMatch)) {
177
229
  const year = validYear(textAt(yearMatch.value, 1));
@@ -183,6 +235,15 @@ const parseBasePeriod = (input) => {
183
235
  const year = validYear(textAt(monthYear.value, 2));
184
236
  if (month !== void 0 && year !== void 0) return Option.some(fixedMonthPeriod(year, month, `${title(textAt(months, month - 1))} ${year}`));
185
237
  }
238
+ return Option.none();
239
+ };
240
+ const parseRelativeMonth = (input) => {
241
+ const prefixedRelativeMonth = String$1.match(/^(last|this|next) ([a-z]+\.?)$/u)(input);
242
+ if (Option.isSome(prefixedRelativeMonth)) {
243
+ const directionText = textAt(prefixedRelativeMonth.value, 1);
244
+ const month = monthNumber(textAt(prefixedRelativeMonth.value, 2));
245
+ if (month !== void 0) return Option.some(monthOfRelativeYear(month, relativeDirection(directionText), `${directionText} ${title(textAt(months, month - 1))}`));
246
+ }
186
247
  const relativeMonth = String$1.match(/^([a-z]+\.?) (?:of )?(last|this|next) year$/u)(input);
187
248
  if (Option.isSome(relativeMonth)) {
188
249
  const directionText = textAt(relativeMonth.value, 2);
@@ -192,14 +253,9 @@ const parseBasePeriod = (input) => {
192
253
  }
193
254
  const standaloneMonth = monthNumber(input);
194
255
  if (standaloneMonth !== void 0) return Option.some(monthOfRelativeYear(standaloneMonth, 0, title(textAt(months, standaloneMonth - 1))));
195
- if (input === "today") return Option.some(relativePeriod("day", 0, "today"));
196
- if (input === "yesterday") return Option.some(relativePeriod("day", -1, "yesterday"));
197
- if (input === "tomorrow") return Option.some(relativePeriod("day", 1, "tomorrow"));
198
- if (input === "weekend" || input === "the weekend" || input === "this weekend") return Option.some(relativeWeekend(0, "this weekend"));
199
- if (input === "last weekend" || input === "previous weekend") return Option.some(relativeWeekend(-1, "last weekend"));
200
- if (input === "next weekend" || input === "coming weekend") return Option.some(relativeWeekend(1, "next weekend"));
201
- if (input === "the weekend before last") return Option.some(relativeWeekend(-2, "the weekend before last"));
202
- if (input === "the weekend after next") return Option.some(relativeWeekend(2, "the weekend after next"));
256
+ return Option.none();
257
+ };
258
+ const parseRelativeUnit = (input) => {
203
259
  const articlePeriod = String$1.match(/^the (day|week|month|quarter|year)$/u)(input);
204
260
  if (Option.isSome(articlePeriod)) {
205
261
  const entry = units.find((unit) => unit[0] === textAt(articlePeriod.value, 1));
@@ -213,7 +269,7 @@ const parseBasePeriod = (input) => {
213
269
  return Option.some(relativePeriod(entry[1], direction, input));
214
270
  }
215
271
  }
216
- const relative = String$1.match(/^(?:the )?(last|previous|this|current|next|coming|upcoming) ([a-z]+)$/u)(input);
272
+ const relative = String$1.match(/^(?:the )?(last|previous|this|current|next|coming|upcoming) (?:calendar )?([a-z]+)$/u)(input);
217
273
  if (Option.isSome(relative)) {
218
274
  const unitText = textAt(relative.value, 2);
219
275
  const unit = units.find((entry) => entry[0] === unitText)?.[1];
@@ -225,17 +281,69 @@ const parseBasePeriod = (input) => {
225
281
  }
226
282
  return Option.none();
227
283
  };
228
- const parsePeriod = (input) => {
284
+ const parseBasePeriod = (input) => {
285
+ const knownPeriod = Option.firstSomeOf([parseDatedPeriod(input), parseRelativeMonth(input)]);
286
+ if (Option.isSome(knownPeriod)) return knownPeriod;
287
+ if (input === "today") return Option.some(relativePeriod("day", 0, "today"));
288
+ if (input === "yesterday") return Option.some(relativePeriod("day", -1, "yesterday"));
289
+ if (input === "tomorrow") return Option.some(relativePeriod("day", 1, "tomorrow"));
290
+ if (input === "weekend" || input === "the weekend" || input === "this weekend") return Option.some(relativeWeekend(0, "this weekend"));
291
+ if (input === "last weekend" || input === "previous weekend") return Option.some(relativeWeekend(-1, "last weekend"));
292
+ if (input === "next weekend" || input === "coming weekend") return Option.some(relativeWeekend(1, "next weekend"));
293
+ if (input === "the weekend before last") return Option.some(relativeWeekend(-2, "the weekend before last"));
294
+ if (input === "the weekend after next") return Option.some(relativeWeekend(2, "the weekend after next"));
295
+ const weekday = nextWeekdayPhrases.indexOf(input);
296
+ if (weekday !== -1) return Option.some(relativeWeekday(weekday, 1, `next ${title(textAt(weekdays, weekday))}`));
297
+ return parseRelativeUnit(input);
298
+ };
299
+ const parsePeriodEdge = (input) => {
229
300
  const edge = String$1.match(/^(?:the )?(start|beginning|end) of (.+)$/u)(input);
230
- if (Option.isSome(edge)) {
231
- const period = parseBasePeriod(textAt(edge.value, 2));
232
- if (Option.isSome(period)) {
233
- const edgeName = textAt(edge.value, 1);
234
- const name = edgeName === "beginning" ? "start" : edgeName;
235
- const canonical = `${name} of ${period.value.canonical}`;
236
- return Option.some(name === "end" ? periodEndDay(period.value, canonical) : periodStartDay(period.value, canonical));
301
+ if (Option.isNone(edge)) return Option.none();
302
+ const periodText = textAt(edge.value, 2);
303
+ const basePeriod = parseBasePeriod(periodText);
304
+ const implicit = units.find((entry) => entry[0] === periodText);
305
+ const period = Option.isSome(basePeriod) || implicit === void 0 ? basePeriod : Option.some(relativePeriod(implicit[1], 0, currentPeriod(implicit[0])));
306
+ if (Option.isNone(period)) return Option.none();
307
+ const edgeName = textAt(edge.value, 1);
308
+ const name = edgeName === "beginning" ? "start" : edgeName;
309
+ const canonical = `${name} of ${period.value.canonical}`;
310
+ return Option.some(name === "end" ? periodEndDay(period.value, canonical) : periodStartDay(period.value, canonical));
311
+ };
312
+ const parsePeriod = (input) => {
313
+ const shifted = String$1.match(/^(.+) ([1-9]\d*) (day|days|week|weeks|month|months|quarter|quarters|year|years) (ago|prior|from now)$/u)(input);
314
+ if (Option.isSome(shifted)) {
315
+ const amount = parseTrailingCount(textAt(shifted.value, 2));
316
+ const unitText = textAt(shifted.value, 3);
317
+ const period = parsePeriod(textAt(shifted.value, 1));
318
+ if (Option.isSome(amount) && Option.isSome(period)) {
319
+ const entry = countedUnit(unitText, amount.value);
320
+ if (entry !== void 0) {
321
+ const past = textAt(shifted.value, 4) !== "from now";
322
+ const direction = past ? -amount.value : amount.value;
323
+ const suffix = past ? "ago" : "from now";
324
+ const canonical = `${period.value.canonical} ${amount.value} ${unitText} ${suffix}`;
325
+ return Option.some(shiftPeriod(period.value, direction, entry[1], canonical));
326
+ }
327
+ }
328
+ }
329
+ const relativeShift = String$1.match(/^([1-9]\d*) (day|days|week|weeks|month|months|quarter|quarters|year|years) (before|after) (.+)$/u)(input);
330
+ if (Option.isSome(relativeShift)) {
331
+ const amount = parseTrailingCount(textAt(relativeShift.value, 1));
332
+ const unitText = textAt(relativeShift.value, 2);
333
+ const period = parsePeriod(textAt(relativeShift.value, 4));
334
+ if (Option.isSome(amount) && Option.isSome(period)) {
335
+ const entry = countedUnit(unitText, amount.value);
336
+ if (entry !== void 0) {
337
+ const direction = textAt(relativeShift.value, 3) === "before" ? -amount.value : amount.value;
338
+ return Option.some(shiftPeriod(period.value, direction, entry[1], input));
339
+ }
237
340
  }
238
341
  }
342
+ const previousDay = String$1.match(/^(?:the )?day before (.+)$/u)(input);
343
+ if (Option.isSome(previousDay)) {
344
+ const period = parseBasePeriod(textAt(previousDay.value, 1));
345
+ if (Option.isSome(period)) return Option.some(periodPreviousDay(period.value, `the day before ${period.value.canonical}`));
346
+ }
239
347
  const wrapper = [
240
348
  "during ",
241
349
  "in ",
@@ -243,10 +351,16 @@ const parsePeriod = (input) => {
243
351
  "all of ",
244
352
  "the whole of "
245
353
  ].find((prefix) => input.startsWith(prefix));
246
- return parseBasePeriod(wrapper === void 0 ? input : input.slice(wrapper.length));
354
+ return Option.firstSomeOf([
355
+ parsePeriodEdge(input),
356
+ parseBasePeriod(wrapper === void 0 ? input : input.slice(wrapper.length)),
357
+ parseCalendarOffset(input)
358
+ ]);
247
359
  };
248
360
  const boundaryCandidate = (input) => openBoundaryCandidate(input, [
249
361
  ["up to and including ", "through"],
362
+ ["on or before ", "through"],
363
+ ["on or after ", "since"],
250
364
  ["up to including ", "through"],
251
365
  ["since the beginning of ", "since"],
252
366
  ["since the start of ", "since"],
@@ -263,6 +377,8 @@ const boundaryCandidate = (input) => openBoundaryCandidate(input, [
263
377
  ["before start of ", "before"],
264
378
  ["starting ", "since"],
265
379
  ["through ", "through"],
380
+ ["earlier than ", "before"],
381
+ ["later than ", "after"],
266
382
  ["before ", "before"],
267
383
  ["since ", "since"],
268
384
  ["after ", "after"],
@@ -282,7 +398,7 @@ const parseCalendarOffset = (input) => {
282
398
  const isPast = directionText === "ago" || directionText === "prior";
283
399
  const direction = isPast ? -1 : 1;
284
400
  const canonical = isPast ? `1 ${entry[0]} ago` : `in 1 ${entry[0]}`;
285
- return Option.some(candidate(periodRange(relativePeriod(entry[1], direction, canonical)), canonical));
401
+ return Option.some(relativePeriod(entry[1], direction, canonical));
286
402
  }
287
403
  }
288
404
  const past = String$1.match(/^([1-9]\d*) (day|days|week|weeks|month|months|quarter|quarters|year|years) (?:ago|prior)$/u)(input);
@@ -301,26 +417,51 @@ const parseCalendarOffset = (input) => {
301
417
  if (entry === void 0) return Option.none();
302
418
  const direction = Option.isSome(past) ? -amount.value : amount.value;
303
419
  const canonical = direction < 0 ? `${amount.value} ${unitText} ago` : `in ${amount.value} ${unitText}`;
304
- return Option.some(candidate(periodRange(relativePeriod(entry[1], direction, canonical)), canonical));
420
+ return Option.some(relativePeriod(entry[1], direction, canonical));
305
421
  };
306
422
  const parseRollingPeriod = (input) => {
307
423
  const past = String$1.match(/^(?:(?:last|previous|past) |(?:in|over) the (?:last|previous|past) )?([1-9]\d*) (day|days|week|weeks|month|months|quarter|quarters|year|years)$/u)(input);
424
+ const singularPast = String$1.match(/^(?:past|the past|in the past|over the past|this past) (day|week|month|quarter|year)$/u)(input);
308
425
  const future = String$1.match(/^(?:(?:next|coming|within) |(?:in|over|within) the (?:next|coming) )([1-9]\d*) (day|days|week|weeks|month|months|quarter|quarters|year|years)$/u)(input);
309
- const match = Option.firstSomeOf([past, future]);
426
+ const singularFuture = String$1.match(/^(?:in|over|within) the (?:next|coming) (day|week|month|quarter|year)$/u)(input);
427
+ const match = Option.firstSomeOf([
428
+ past,
429
+ singularPast,
430
+ future,
431
+ singularFuture
432
+ ]);
310
433
  if (Option.isNone(match)) return Option.none();
311
- const amount = parseTrailingCount(textAt(match.value, 1));
434
+ const singular = Option.isSome(singularPast) || Option.isSome(singularFuture);
435
+ const amount = parseTrailingCount(singular ? "1" : textAt(match.value, 1));
312
436
  if (Option.isNone(amount)) return Option.none();
313
- const entry = countedUnit(textAt(match.value, 2), amount.value);
437
+ const unitText = textAt(match.value, singular ? 1 : 2);
438
+ const entry = countedUnit(unitText, amount.value);
314
439
  if (entry === void 0) return Option.none();
315
- const isFuture = Option.isSome(future);
440
+ const isFuture = Option.isSome(future) || Option.isSome(singularFuture);
316
441
  const range = isFuture ? futureRange(amount.value, entry[1]) : trailingRange(amount.value, entry[1]);
442
+ if (amount.value === 1) {
443
+ const canonical = isFuture ? `within the next ${entry[0]}` : `past ${entry[0]}`;
444
+ return Option.some(candidate(range, canonical));
445
+ }
317
446
  const direction = isFuture ? "next" : "last";
318
- const noun = amount.value === 1 ? entry[0] : entry[2];
319
- return Option.some(candidate(range, `${direction} ${amount.value} ${noun}`));
447
+ return Option.some(candidate(range, `${direction} ${amount.value} ${entry[2]}`));
448
+ };
449
+ const parseElidedDateRange = (input) => {
450
+ const monthFirst = String$1.match(/^([a-z]+\.?) ([0-3]?\d)(?:st|nd|rd|th)?(?: (?:to|through) |[–—-])([0-3]?\d)(?:st|nd|rd|th)?,?(?: (\d{4}))?$/u)(input);
451
+ const dayFirst = String$1.match(/^([0-3]?\d)(?:st|nd|rd|th)?(?: (?:to|through) |[–—-])([0-3]?\d)(?:st|nd|rd|th)? ([a-z]+\.?)(?: (\d{4}))?$/u)(input);
452
+ const match = Option.firstSomeOf([monthFirst, dayFirst]);
453
+ if (Option.isNone(match)) return Option.none();
454
+ const isMonthFirst = Option.isSome(monthFirst);
455
+ const month = textAt(match.value, isMonthFirst ? 1 : 3);
456
+ const lowerDay = textAt(match.value, isMonthFirst ? 2 : 1);
457
+ const upperDay = textAt(match.value, isMonthFirst ? 3 : 2);
458
+ const year = textAt(match.value, 4);
459
+ const suffix = year.length === 0 ? "" : ` ${year}`;
460
+ return joinedPeriodCandidate(`from ${lowerDay} ${month}${suffix} to ${upperDay} ${month}${suffix}`, [["from ", " to "]], parsePeriod, (lower, upper) => `from ${lower} to ${upper}`);
320
461
  };
321
462
  const parseEnglish = (input) => {
322
463
  const remaining = remainingPeriodPhrases.find((entry) => entry.phrase === input);
323
- if (remaining !== void 0) return Option.some(candidate(remainingPeriodRange(remaining.entry[1]), `rest of ${remaining.entry[0]}`));
464
+ if (remaining !== void 0) return Option.some(candidate(remainingPeriodRange(remaining.entry[1]), `rest of the ${remaining.entry[0]}`));
324
465
  if ([
325
466
  "until now",
326
467
  "till now",
@@ -332,22 +473,24 @@ const parseEnglish = (input) => {
332
473
  "from now on",
333
474
  "starting now"
334
475
  ].includes(input)) return Option.some(candidate(fromNowRange(), "from now"));
335
- const offset = parseCalendarOffset(input);
336
- if (Option.isSome(offset)) return offset;
337
476
  const rolling = parseRollingPeriod(input);
338
477
  if (Option.isSome(rolling)) return rolling;
339
478
  const toDate = toDatePhrases.find((entry) => entry.phrase === input);
340
479
  if (toDate !== void 0) return Option.some(candidate(periodToDateRange(toDate.entry[1]), `${toDate.entry[0]} to date`));
480
+ const elided = parseElidedDateRange(input);
481
+ if (Option.isSome(elided)) return elided;
341
482
  const nowBounded = joinedNowCandidate(input, [
342
483
  ["from ", " to now"],
343
484
  ["from ", " until now"],
344
485
  ["from ", " through now"],
486
+ ["from ", " to date"],
345
487
  ["between ", " and now"]
346
488
  ], [
347
489
  "from now to ",
348
490
  "from now until ",
349
491
  "from now through ",
350
- "between now and "
492
+ "between now and ",
493
+ "now to "
351
494
  ], parsePeriod, (period) => `from ${period} to now`, (period) => `from now to ${period}`);
352
495
  if (Option.isSome(nowBounded)) return nowBounded;
353
496
  const bounded = joinedPeriodCandidate(input, [
@@ -380,18 +523,19 @@ const parseEnglish = (input) => {
380
523
  if (Option.isSome(bounded)) return bounded;
381
524
  const boundary = boundaryCandidate(input);
382
525
  if (Option.isSome(boundary)) return boundary;
383
- return Option.map(parsePeriod(input), (period) => candidate(periodRange(period), period.canonical));
526
+ return parsePeriod(input).pipe(Option.map((period) => candidate(periodRange(period), period.canonical)));
384
527
  };
385
528
  const staticPeriodPhrases = [
386
529
  "today",
387
530
  "yesterday",
531
+ "the day before yesterday",
388
532
  "tomorrow",
389
533
  "this weekend",
390
534
  "last weekend",
391
535
  "next weekend",
392
536
  "the weekend before last",
393
537
  "the weekend after next",
394
- ...units.flatMap((entry) => [
538
+ ...units.filter((entry) => entry[1] !== "day").flatMap((entry) => [
395
539
  `this ${entry[0]}`,
396
540
  `last ${entry[0]}`,
397
541
  `next ${entry[0]}`,
@@ -414,9 +558,12 @@ const staticPeriodPhrases = [
414
558
  ]),
415
559
  ...months.flatMap((month) => [
416
560
  month,
561
+ `last ${month}`,
562
+ `next ${month}`,
417
563
  `${month} of last year`,
418
564
  `${month} of next year`
419
- ])
565
+ ]),
566
+ ...nextWeekdayPhrases
420
567
  ];
421
568
  const staticPeriods = periodsFromPhrases(staticPeriodPhrases, parsePeriod);
422
569
  const boundaryPrefixes = [
@@ -441,7 +588,8 @@ const countedSuggestions = (input) => {
441
588
  };
442
589
  const englishSuggestionPhrases = [
443
590
  ...units.map((entry) => `${entry[0]} to date`),
444
- ...units.map((entry) => `rest of ${entry[0]}`),
591
+ ...remainingPeriodPhrases.map((entry) => entry.phrase),
592
+ ...units.flatMap((entry) => [`past ${entry[0]}`, `within the next ${entry[0]}`]),
445
593
  ...staticPeriodPhrases,
446
594
  ...prefixNaturalPhrases(staticPeriodPhrases, boundaryPrefixes),
447
595
  "until now",
@@ -457,6 +605,17 @@ const suggestEnglish = (input, limit) => {
457
605
  ], limit);
458
606
  };
459
607
  const renderEnglish = (range) => {
608
+ const shifted = decomposeShiftedPeriodRange(range);
609
+ if (Option.isSome(shifted)) {
610
+ const base = renderEnglish(shifted.value.baseRange);
611
+ const entry = units.find((unit) => unit[1] === shifted.value.unit);
612
+ if (Option.isSome(base) && entry !== void 0) {
613
+ const amount = Math.abs(shifted.value.amount);
614
+ const noun = amount === 1 ? entry[0] : entry[2];
615
+ const suffix = shifted.value.amount < 0 ? "ago" : "from now";
616
+ return Option.some(`${base.value} ${amount} ${noun} ${suffix}`);
617
+ }
618
+ }
460
619
  const offset = calendarPeriodOffset(range);
461
620
  if (Option.isSome(offset) && Math.abs(offset.value.amount) > 1) {
462
621
  const entry = units.find((unit) => unit[1] === offset.value.unit);
@@ -468,20 +627,25 @@ const renderEnglish = (range) => {
468
627
  const future = futurePeriod(range);
469
628
  if (Option.isSome(future)) {
470
629
  const entry = units.find((unit) => unit[1] === future.value.unit);
471
- if (entry !== void 0) return Option.some(`next ${future.value.amount} ${future.value.amount === 1 ? entry[0] : entry[2]}`);
630
+ if (entry !== void 0) return Option.some(future.value.amount === 1 ? `within the next ${entry[0]}` : `next ${future.value.amount} ${entry[2]}`);
472
631
  }
473
632
  const trailing = trailingPeriod(range);
474
633
  if (Option.isSome(trailing)) {
475
634
  const entry = units.find((unit) => unit[1] === trailing.value.unit);
476
- if (entry !== void 0) return Option.some(trailing.value.amount === 1 ? `1 ${entry[0]}` : `last ${trailing.value.amount} ${entry[2]}`);
635
+ if (entry !== void 0) return Option.some(trailing.value.amount === 1 ? `past ${entry[0]}` : `last ${trailing.value.amount} ${entry[2]}`);
477
636
  }
478
- const periods = [...staticPeriods, ...periodsFromPhrases([...datedPeriods(range, months), ...datedQuarterPeriods(range)], parsePeriod)];
479
- return renderPeriodRange(range, [...units.map((entry) => candidate(periodToDateRange(entry[1]), `${entry[0]} to date`)), ...units.map((entry) => candidate(remainingPeriodRange(entry[1]), `rest of ${entry[0]}`))], periods, (period) => `since ${period}`, (period) => `before ${period}`, (period) => `through ${period}`, (period) => `after ${period}`, (lower, upper) => `from ${lower} to ${upper}`, (period) => `from ${period} to now`, (period) => `from now to ${period}`, () => "until now", () => "from now");
637
+ const periods = [
638
+ ...staticPeriods,
639
+ ...currentYearDatePeriods(range, currentDateLabel),
640
+ ...periodsFromPhrases([...datedPeriods(range, months), ...datedQuarterPeriods(range)], parsePeriod)
641
+ ];
642
+ return renderPeriodRange(range, [...units.map((entry) => candidate(periodToDateRange(entry[1]), `${entry[0]} to date`)), ...units.map((entry) => candidate(remainingPeriodRange(entry[1]), `rest of the ${entry[0]}`))], periods, (period) => `since ${period}`, (period) => `before ${period}`, (period) => `through ${period}`, (period) => `after ${period}`, (lower, upper) => `from ${lower} to ${upper}`, (period) => `from ${period} to now`, (period) => `from now to ${period}`, () => "until now", () => "from now");
480
643
  };
481
644
  const EnglishContribution = new BaseLanguageContribution({
482
645
  locale: "en",
483
646
  vocabulary: [
484
647
  ...months,
648
+ ...weekdays,
485
649
  ...quarterNames,
486
650
  "q1",
487
651
  "q2",
@@ -535,8 +699,8 @@ const EnglishContribution = new BaseLanguageContribution({
535
699
  "weekend",
536
700
  "yesterday"
537
701
  ],
538
- normalize: normalizeNaturalText,
539
- correct: correctWhitespaceSeparatedText,
702
+ normalize: normalizeEnglish,
703
+ correct: correctEnglish,
540
704
  parseExact: parseEnglish,
541
705
  suggest: suggestEnglish,
542
706
  render: renderEnglish