chronolizer 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,7 +3,7 @@ import { normalizeNaturalText } from "../natural/text.mjs";
3
3
  import { defineLanguagePlugin, languagePluginsLayer } from "../language/registry.mjs";
4
4
  import { correctWhitespaceSeparatedText } from "../natural/correction.mjs";
5
5
  import { completeNaturalPhrases, fixedCalendarPeriodPhrases, naturalCount, prefixNaturalPhrases } from "../natural/suggestion.mjs";
6
- import { absoluteDatePeriod, calendarPeriodOffset, candidate, currentYearDatePeriods, datedPeriods, datedQuarterPeriods, fixedMonthPeriod, fixedQuarterPeriod, fixedYearPeriod, fromNowRange, futurePeriod, futureRange, joinedNowCandidate, joinedPeriodCandidate, monthOfRelativeYear, namedCurrentYearDatePeriod, namedDatePeriod, openBoundaryCandidate, parseTrailingCount, periodEndDay, periodPreviousDay, 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";
7
7
  import { Effect, Option, String as String$1 } from "effect";
8
8
  //#region src/locales/en.ts
9
9
  const months = [
@@ -34,12 +34,59 @@ const monthAbbreviations = [
34
34
  ["nov"],
35
35
  ["dec"]
36
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}`);
37
47
  const quarterNames = [
38
48
  "first",
39
49
  "second",
40
50
  "third",
41
51
  "fourth"
42
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));
43
90
  const units = [
44
91
  [
45
92
  "day",
@@ -170,13 +217,13 @@ const parseNamedDate = (input) => {
170
217
  if (Option.isNone(current)) return Option.none();
171
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);
172
219
  };
173
- const parseBasePeriod = (input) => {
174
- const absoluteDate = absoluteDatePeriod(input, "en");
175
- if (Option.isSome(absoluteDate)) return absoluteDate;
176
- const namedDate = parseNamedDate(input);
177
- if (Option.isSome(namedDate)) return namedDate;
178
- const quarter = parseQuarter(input);
179
- 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;
180
227
  const yearMatch = String$1.match(/^(?:(?:the )?(?:calendar )?year )?(\d{4})$/u)(input);
181
228
  if (Option.isSome(yearMatch)) {
182
229
  const year = validYear(textAt(yearMatch.value, 1));
@@ -188,6 +235,15 @@ const parseBasePeriod = (input) => {
188
235
  const year = validYear(textAt(monthYear.value, 2));
189
236
  if (month !== void 0 && year !== void 0) return Option.some(fixedMonthPeriod(year, month, `${title(textAt(months, month - 1))} ${year}`));
190
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
+ }
191
247
  const relativeMonth = String$1.match(/^([a-z]+\.?) (?:of )?(last|this|next) year$/u)(input);
192
248
  if (Option.isSome(relativeMonth)) {
193
249
  const directionText = textAt(relativeMonth.value, 2);
@@ -197,20 +253,15 @@ const parseBasePeriod = (input) => {
197
253
  }
198
254
  const standaloneMonth = monthNumber(input);
199
255
  if (standaloneMonth !== void 0) return Option.some(monthOfRelativeYear(standaloneMonth, 0, title(textAt(months, standaloneMonth - 1))));
200
- if (input === "today") return Option.some(relativePeriod("day", 0, "today"));
201
- if (input === "yesterday") return Option.some(relativePeriod("day", -1, "yesterday"));
202
- if (input === "tomorrow") return Option.some(relativePeriod("day", 1, "tomorrow"));
203
- if (input === "weekend" || input === "the weekend" || input === "this weekend") return Option.some(relativeWeekend(0, "this weekend"));
204
- if (input === "last weekend" || input === "previous weekend") return Option.some(relativeWeekend(-1, "last weekend"));
205
- if (input === "next weekend" || input === "coming weekend") return Option.some(relativeWeekend(1, "next weekend"));
206
- if (input === "the weekend before last") return Option.some(relativeWeekend(-2, "the weekend before last"));
207
- if (input === "the weekend after next") return Option.some(relativeWeekend(2, "the weekend after next"));
208
- const articlePeriod = String$1.match(/^the (day|week|month|quarter|year)$/u)(input);
256
+ return Option.none();
257
+ };
258
+ const parseRelativeUnit = (input) => {
259
+ const articlePeriod = String$1.match(/^the ([^ ]+)$/u)(input);
209
260
  if (Option.isSome(articlePeriod)) {
210
261
  const entry = units.find((unit) => unit[0] === textAt(articlePeriod.value, 1));
211
262
  if (entry !== void 0) return Option.some(relativePeriod(entry[1], 0, currentPeriod(entry[0])));
212
263
  }
213
- const outerRelative = String$1.match(/^(?:the )?(day|week|month|quarter|year) (before last|after next)$/u)(input);
264
+ const outerRelative = String$1.match(/^(?:the )?([^ ]+) (before last|after next)$/u)(input);
214
265
  if (Option.isSome(outerRelative)) {
215
266
  const entry = units.find((unit) => unit[0] === textAt(outerRelative.value, 1));
216
267
  if (entry !== void 0) {
@@ -230,22 +281,69 @@ const parseBasePeriod = (input) => {
230
281
  }
231
282
  return Option.none();
232
283
  };
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) => {
300
+ const edge = String$1.match(/^(?:the )?(start|beginning|end) of (.+)$/u)(input);
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
+ };
233
312
  const parsePeriod = (input) => {
313
+ const shifted = String$1.match(/^(.+) ([1-9]\d*) ([^ ]+) (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*) ([^ ]+) (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
+ }
340
+ }
341
+ }
234
342
  const previousDay = String$1.match(/^(?:the )?day before (.+)$/u)(input);
235
343
  if (Option.isSome(previousDay)) {
236
344
  const period = parseBasePeriod(textAt(previousDay.value, 1));
237
345
  if (Option.isSome(period)) return Option.some(periodPreviousDay(period.value, `the day before ${period.value.canonical}`));
238
346
  }
239
- const edge = String$1.match(/^(?:the )?(start|beginning|end) of (.+)$/u)(input);
240
- if (Option.isSome(edge)) {
241
- const period = parseBasePeriod(textAt(edge.value, 2));
242
- if (Option.isSome(period)) {
243
- const edgeName = textAt(edge.value, 1);
244
- const name = edgeName === "beginning" ? "start" : edgeName;
245
- const canonical = `${name} of ${period.value.canonical}`;
246
- return Option.some(name === "end" ? periodEndDay(period.value, canonical) : periodStartDay(period.value, canonical));
247
- }
248
- }
249
347
  const wrapper = [
250
348
  "during ",
251
349
  "in ",
@@ -253,7 +351,11 @@ const parsePeriod = (input) => {
253
351
  "all of ",
254
352
  "the whole of "
255
353
  ].find((prefix) => input.startsWith(prefix));
256
- 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
+ ]);
257
359
  };
258
360
  const boundaryCandidate = (input) => openBoundaryCandidate(input, [
259
361
  ["up to and including ", "through"],
@@ -287,7 +389,7 @@ const boundaryCandidate = (input) => openBoundaryCandidate(input, [
287
389
  ], parsePeriod);
288
390
  const countedUnit = (value, amount) => units.find((entry) => value === (amount === 1 ? entry[0] : entry[2]));
289
391
  const parseCalendarOffset = (input) => {
290
- const singular = String$1.match(/^(?:(?:a|one) (day|week|month|quarter|year) (ago|prior|from now)|in (?:a|one) (day|week|month|quarter|year))$/u)(input);
392
+ const singular = String$1.match(/^(?:(?:a|one) ([^ ]+) (ago|prior|from now)|in (?:a|one) ([^ ]+))$/u)(input);
291
393
  if (Option.isSome(singular)) {
292
394
  const unitText = textAt(singular.value, 1) || textAt(singular.value, 3);
293
395
  const entry = units.find((unit) => unit[0] === unitText);
@@ -296,12 +398,12 @@ const parseCalendarOffset = (input) => {
296
398
  const isPast = directionText === "ago" || directionText === "prior";
297
399
  const direction = isPast ? -1 : 1;
298
400
  const canonical = isPast ? `1 ${entry[0]} ago` : `in 1 ${entry[0]}`;
299
- return Option.some(candidate(periodRange(relativePeriod(entry[1], direction, canonical)), canonical));
401
+ return Option.some(relativePeriod(entry[1], direction, canonical));
300
402
  }
301
403
  }
302
- const past = String$1.match(/^([1-9]\d*) (day|days|week|weeks|month|months|quarter|quarters|year|years) (?:ago|prior)$/u)(input);
303
- const futureIn = String$1.match(/^in ([1-9]\d*) (day|days|week|weeks|month|months|quarter|quarters|year|years)$/u)(input);
304
- const futureFromNow = String$1.match(/^([1-9]\d*) (day|days|week|weeks|month|months|quarter|quarters|year|years) from now$/u)(input);
404
+ const past = String$1.match(/^([1-9]\d*) ([^ ]+) (?:ago|prior)$/u)(input);
405
+ const futureIn = String$1.match(/^in ([1-9]\d*) ([^ ]+)$/u)(input);
406
+ const futureFromNow = String$1.match(/^([1-9]\d*) ([^ ]+) from now$/u)(input);
305
407
  const match = Option.firstSomeOf([
306
408
  past,
307
409
  futureIn,
@@ -315,13 +417,13 @@ const parseCalendarOffset = (input) => {
315
417
  if (entry === void 0) return Option.none();
316
418
  const direction = Option.isSome(past) ? -amount.value : amount.value;
317
419
  const canonical = direction < 0 ? `${amount.value} ${unitText} ago` : `in ${amount.value} ${unitText}`;
318
- return Option.some(candidate(periodRange(relativePeriod(entry[1], direction, canonical)), canonical));
420
+ return Option.some(relativePeriod(entry[1], direction, canonical));
319
421
  };
320
422
  const parseRollingPeriod = (input) => {
321
- 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);
322
- const singularPast = String$1.match(/^(?:past|the past|in the past|over the past|this past) (day|week|month|quarter|year)$/u)(input);
323
- 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);
324
- const singularFuture = String$1.match(/^(?:in|over|within) the (?:next|coming) (day|week|month|quarter|year)$/u)(input);
423
+ const past = String$1.match(/^(?:(?:last|previous|past) |(?:in|over) the (?:last|previous|past) )?([1-9]\d*) ([^ ]+)$/u)(input);
424
+ const singularPast = String$1.match(/^(?:past|the past|in the past|over the past|this past) ([^ ]+)$/u)(input);
425
+ const future = String$1.match(/^(?:(?:next|coming|within) |(?:in|over|within) the (?:next|coming) )([1-9]\d*) ([^ ]+)$/u)(input);
426
+ const singularFuture = String$1.match(/^(?:in|over|within) the (?:next|coming) ([^ ]+)$/u)(input);
325
427
  const match = Option.firstSomeOf([
326
428
  past,
327
429
  singularPast,
@@ -371,8 +473,6 @@ const parseEnglish = (input) => {
371
473
  "from now on",
372
474
  "starting now"
373
475
  ].includes(input)) return Option.some(candidate(fromNowRange(), "from now"));
374
- const offset = parseCalendarOffset(input);
375
- if (Option.isSome(offset)) return offset;
376
476
  const rolling = parseRollingPeriod(input);
377
477
  if (Option.isSome(rolling)) return rolling;
378
478
  const toDate = toDatePhrases.find((entry) => entry.phrase === input);
@@ -389,7 +489,8 @@ const parseEnglish = (input) => {
389
489
  "from now to ",
390
490
  "from now until ",
391
491
  "from now through ",
392
- "between now and "
492
+ "between now and ",
493
+ "now to "
393
494
  ], parsePeriod, (period) => `from ${period} to now`, (period) => `from now to ${period}`);
394
495
  if (Option.isSome(nowBounded)) return nowBounded;
395
496
  const bounded = joinedPeriodCandidate(input, [
@@ -457,9 +558,12 @@ const staticPeriodPhrases = [
457
558
  ]),
458
559
  ...months.flatMap((month) => [
459
560
  month,
561
+ `last ${month}`,
562
+ `next ${month}`,
460
563
  `${month} of last year`,
461
564
  `${month} of next year`
462
- ])
565
+ ]),
566
+ ...nextWeekdayPhrases
463
567
  ];
464
568
  const staticPeriods = periodsFromPhrases(staticPeriodPhrases, parsePeriod);
465
569
  const boundaryPrefixes = [
@@ -501,6 +605,17 @@ const suggestEnglish = (input, limit) => {
501
605
  ], limit);
502
606
  };
503
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
+ }
504
619
  const offset = calendarPeriodOffset(range);
505
620
  if (Option.isSome(offset) && Math.abs(offset.value.amount) > 1) {
506
621
  const entry = units.find((unit) => unit[1] === offset.value.unit);
@@ -524,12 +639,13 @@ const renderEnglish = (range) => {
524
639
  ...currentYearDatePeriods(range, currentDateLabel),
525
640
  ...periodsFromPhrases([...datedPeriods(range, months), ...datedQuarterPeriods(range)], parsePeriod)
526
641
  ];
527
- 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");
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, ...periods.map((period) => periodPreviousDay(period, `the day before ${period.canonical}`))], (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");
528
643
  };
529
644
  const EnglishContribution = new BaseLanguageContribution({
530
645
  locale: "en",
531
646
  vocabulary: [
532
647
  ...months,
648
+ ...weekdays,
533
649
  ...quarterNames,
534
650
  "q1",
535
651
  "q2",
@@ -583,8 +699,8 @@ const EnglishContribution = new BaseLanguageContribution({
583
699
  "weekend",
584
700
  "yesterday"
585
701
  ],
586
- normalize: normalizeNaturalText,
587
- correct: correctWhitespaceSeparatedText,
702
+ normalize: normalizeEnglish,
703
+ correct: correctEnglish,
588
704
  parseExact: parseEnglish,
589
705
  suggest: suggestEnglish,
590
706
  render: renderEnglish