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,17 +1,76 @@
1
- import { InstantExpr, Unit, daysInMonth, isIsoDate } from "../ast/schemas.mjs";
1
+ import { GreaterThanOrEqual, InstantExpr, LessThan, LessThanOrEqual, Now, Shift, StartOf, Unit, daysInMonth, isIsoDate } from "../ast/schemas.mjs";
2
2
  import { boundedRange, dateLiteral, greaterThanOrEqual, lessThan, lessThanOrEqual, lowerOpenRange, now, shift, startOf, upperOpenRange } from "../ast/constructors.mjs";
3
3
  import { formatInstantExpression } from "../filter/expression.mjs";
4
4
  import { formatFilter, rangeKey } from "../filter/codec.mjs";
5
5
  import { NaturalCandidate } from "../language/model.mjs";
6
- import { Option, Schema, String as String$1 } from "effect";
6
+ import { Match, Option, RegExp as RegExp$1, Schema, String as String$1 } from "effect";
7
7
  //#region src/locales/shared.ts
8
8
  const textAt = (values, index) => values[index] ?? "";
9
+ const sequentialCountAliases = (words, start) => words.flatMap((aliases, index) => aliases.map((phrase) => [phrase, start + index]));
10
+ const decimalTens = (words) => words.map((word, index) => [20 + index * 10, word]);
11
+ const compoundCountAliases = (tens, ones, join) => tens.flatMap(([tensAmount, tensWord]) => ones.flatMap(([oneAmount, oneWord]) => {
12
+ const amount = tensAmount + oneAmount;
13
+ return join(tensWord, oneWord, amount).map((phrase) => [phrase, amount]);
14
+ }));
15
+ const countAliasVocabulary = (aliases) => [...new Set(aliases.flatMap(([phrase]) => phrase.split(" ")))];
16
+ const compileCountAliasNormalizer = (aliases) => {
17
+ const distinct = /* @__PURE__ */ new Map();
18
+ for (const [phrase, amount] of aliases) if (!distinct.has(phrase)) distinct.set(phrase, amount);
19
+ const byFirstWord = /* @__PURE__ */ new Map();
20
+ for (const [phrase, amount] of distinct) {
21
+ const parts = phrase.split(" ");
22
+ const first = parts[0] ?? "";
23
+ const entries = byFirstWord.get(first) ?? [];
24
+ byFirstWord.set(first, [...entries, {
25
+ amount,
26
+ parts
27
+ }]);
28
+ }
29
+ for (const [first, entries] of byFirstWord) byFirstWord.set(first, [...entries].sort((left, right) => right.parts.length - left.parts.length));
30
+ return (input) => {
31
+ const words = input.split(" ");
32
+ const output = [];
33
+ for (let index = 0; index < words.length;) {
34
+ const alias = (byFirstWord.get(words[index] ?? "") ?? []).find((entry) => entry.parts.every((part, offset) => words[index + offset] === part));
35
+ if (alias === void 0) {
36
+ output.push(words[index] ?? "");
37
+ index += 1;
38
+ } else {
39
+ output.push(String(alias.amount));
40
+ index += alias.parts.length;
41
+ }
42
+ }
43
+ return output.join(" ");
44
+ };
45
+ };
9
46
  const Period = Schema.Struct({
10
47
  start: InstantExpr,
11
48
  end: InstantExpr,
12
49
  canonical: Schema.String
13
50
  });
51
+ const isGreaterThanOrEqual = Schema.is(GreaterThanOrEqual);
52
+ const isLessThan = Schema.is(LessThan);
53
+ const isLessThanOrEqual = Schema.is(LessThanOrEqual);
54
+ const isNow = Schema.is(Now);
55
+ const isShift = Schema.is(Shift);
56
+ const isStartOf = Schema.is(StartOf);
14
57
  const periodRange = (period) => boundedRange(greaterThanOrEqual(period.start), lessThan(period.end));
58
+ const shiftPeriod = (period, amount, unit, canonical) => Period.make({
59
+ start: shift(period.start, amount, unit),
60
+ end: shift(period.end, amount, unit),
61
+ canonical
62
+ });
63
+ const decomposeShiftedPeriodRange = (range) => {
64
+ if (!isGreaterThanOrEqual(range.lower) || !isLessThan(range.upper)) return Option.none();
65
+ const start = range.lower.value;
66
+ const end = range.upper.value;
67
+ if (!isShift(start) || !isShift(end) || start.amount === 0 || start.amount !== end.amount || start.unit !== end.unit) return Option.none();
68
+ return Option.some({
69
+ amount: start.amount,
70
+ unit: start.unit,
71
+ baseRange: boundedRange(greaterThanOrEqual(start.base), lessThan(end.base))
72
+ });
73
+ };
15
74
  const periodToDateRange = (unit) => boundedRange(greaterThanOrEqual(startOf(now(), unit)), lessThanOrEqual(now()));
16
75
  const fromNowRange = () => lowerOpenRange(greaterThanOrEqual(now()));
17
76
  const untilNowRange = () => upperOpenRange(lessThanOrEqual(now()));
@@ -29,6 +88,30 @@ const periodEndDay = (period, canonical) => Period.make({
29
88
  end: period.end,
30
89
  canonical
31
90
  });
91
+ const periodDay = (period, day, canonical) => {
92
+ if (!Number.isInteger(day) || day < 1 || day > 28) return Option.none();
93
+ const start = day === 1 ? period.start : shift(period.start, day - 1, "day");
94
+ return Option.some(Period.make({
95
+ start,
96
+ end: shift(start, 1, "day"),
97
+ canonical
98
+ }));
99
+ };
100
+ const periodPreviousDay = (period, canonical) => Period.make({
101
+ start: shift(period.start, -1, "day"),
102
+ end: period.start,
103
+ canonical
104
+ });
105
+ const relativeWeekday = (day, direction, canonical) => {
106
+ const weekBase = direction === 0 ? now() : shift(now(), direction, "week");
107
+ const weekStart = startOf(weekBase, "week");
108
+ const start = day === 0 ? weekStart : shift(weekStart, day, "day");
109
+ return Period.make({
110
+ start,
111
+ end: shift(start, 1, "day"),
112
+ canonical
113
+ });
114
+ };
32
115
  const relativeWeekend = (direction, canonical) => {
33
116
  const weekBase = direction === 0 ? now() : shift(now(), direction, "week");
34
117
  const weekStart = startOf(weekBase, "week");
@@ -43,11 +126,7 @@ const TrailingCount = Schema.Int.check(Schema.isBetween({
43
126
  minimum: 1,
44
127
  maximum: Number.MAX_SAFE_INTEGER
45
128
  }));
46
- const TrailingPeriod = Schema.Struct({
47
- amount: TrailingCount,
48
- unit: Unit
49
- });
50
- const FuturePeriod = Schema.Struct({
129
+ const CountedPeriod = Schema.Struct({
51
130
  amount: TrailingCount,
52
131
  unit: Unit
53
132
  });
@@ -66,15 +145,17 @@ const parseTrailingCount = (value) => {
66
145
  const trailingRange = (amount, unit) => boundedRange(greaterThanOrEqual(shift(now(), -amount, unit)), lessThanOrEqual(now()));
67
146
  const futureRange = (amount, unit) => boundedRange(greaterThanOrEqual(now()), lessThanOrEqual(shift(now(), amount, unit)));
68
147
  const trailingPeriod = (range) => {
69
- if (range.lower?._tag !== "GreaterThanOrEqual" || range.upper?._tag !== "LessThanOrEqual" || range.lower.value._tag !== "Shift" || range.lower.value.amount >= 0 || range.lower.value.base._tag !== "Now" || range.upper.value._tag !== "Now") return Option.none();
70
- return Option.some(TrailingPeriod.make({
148
+ if (!isGreaterThanOrEqual(range.lower) || !isLessThanOrEqual(range.upper)) return Option.none();
149
+ if (!isShift(range.lower.value) || range.lower.value.amount >= 0 || !isNow(range.lower.value.base) || !isNow(range.upper.value)) return Option.none();
150
+ return Option.some(CountedPeriod.make({
71
151
  amount: -range.lower.value.amount,
72
152
  unit: range.lower.value.unit
73
153
  }));
74
154
  };
75
155
  const futurePeriod = (range) => {
76
- if (range.lower?._tag !== "GreaterThanOrEqual" || range.upper?._tag !== "LessThanOrEqual" || range.lower.value._tag !== "Now" || range.upper.value._tag !== "Shift" || range.upper.value.amount <= 0 || range.upper.value.base._tag !== "Now") return Option.none();
77
- return Option.some(FuturePeriod.make({
156
+ if (!isGreaterThanOrEqual(range.lower) || !isLessThanOrEqual(range.upper)) return Option.none();
157
+ if (!isNow(range.lower.value) || !isShift(range.upper.value) || range.upper.value.amount <= 0 || !isNow(range.upper.value.base)) return Option.none();
158
+ return Option.some(CountedPeriod.make({
78
159
  amount: range.upper.value.amount,
79
160
  unit: range.upper.value.unit
80
161
  }));
@@ -100,6 +181,50 @@ const previousDay = (year, month, day) => {
100
181
  if (month > 1) return isoDate(year, month - 1, daysInMonth(year, month - 1));
101
182
  return isoDate(year - 1, 12, 31);
102
183
  };
184
+ const absoluteDateProfiles = /* @__PURE__ */ new Map();
185
+ const absoluteDateProfile = (locale) => {
186
+ const cached = absoluteDateProfiles.get(locale);
187
+ if (cached !== void 0) return cached;
188
+ const formatter = new Intl.DateTimeFormat(locale, {
189
+ day: "numeric",
190
+ month: "numeric",
191
+ year: "numeric",
192
+ numberingSystem: "latn",
193
+ timeZone: "UTC"
194
+ });
195
+ const parts = [];
196
+ const pattern = formatter.formatToParts(/* @__PURE__ */ new Date("2006-11-22T00:00:00.000Z")).map((part) => {
197
+ if (part.type === "day" || part.type === "month" || part.type === "year") {
198
+ parts.push(part.type);
199
+ return part.type === "year" ? "([0-9]{4})" : "([0-9]{1,2})";
200
+ }
201
+ return RegExp$1.escape(part.value);
202
+ }).join("");
203
+ const profile = {
204
+ formatter,
205
+ pattern: new RegExp(`^${pattern}$`, "u"),
206
+ parts
207
+ };
208
+ absoluteDateProfiles.set(locale, profile);
209
+ return profile;
210
+ };
211
+ const formatAbsoluteDate = (value, locale) => absoluteDateProfile(locale).formatter.format(/* @__PURE__ */ new Date(`${value}T00:00:00.000Z`));
212
+ const parseLocalizedAbsoluteDate = (input, locale) => {
213
+ const profile = absoluteDateProfile(locale);
214
+ const match = String$1.match(profile.pattern)(input);
215
+ if (Option.isNone(match)) return Option.none();
216
+ let day = "";
217
+ let month = "";
218
+ let year = "";
219
+ for (const [index, part] of profile.parts.entries()) {
220
+ const value = textAt(match.value, index + 1);
221
+ if (part === "day") day = value;
222
+ if (part === "month") month = value;
223
+ if (part === "year") year = value;
224
+ }
225
+ const value = isoDate(Number(year), Number(month), Number(day));
226
+ return isIsoDate(value) ? Option.some(value) : Option.none();
227
+ };
103
228
  const fixedDatePeriod = (value, canonical) => {
104
229
  const year = Number(value.slice(0, 4));
105
230
  const month = Number(value.slice(5, 7));
@@ -110,6 +235,9 @@ const fixedDatePeriod = (value, canonical) => {
110
235
  canonical
111
236
  });
112
237
  };
238
+ const absoluteDatePeriod = (input, locale) => {
239
+ return (isIsoDate(input) ? Option.some(input) : parseLocalizedAbsoluteDate(input, locale)).pipe(Option.filter((date) => date !== "9999-12-31"), Option.map((date) => fixedDatePeriod(date, formatAbsoluteDate(date, locale))));
240
+ };
113
241
  const namedDatePeriod = (yearText, monthText, dayText, monthNumber, canonical) => {
114
242
  const year = validYear(yearText);
115
243
  const month = monthNumber(monthText);
@@ -119,6 +247,51 @@ const namedDatePeriod = (yearText, monthText, dayText, monthNumber, canonical) =
119
247
  if (!isIsoDate(value) || value === "9999-12-31") return Option.none();
120
248
  return Option.some(fixedDatePeriod(value, canonical(day, month, year)));
121
249
  };
250
+ const minimumDaysInMonth = (month) => daysInMonth(1, month);
251
+ const currentYearDatePeriod = (month, day, canonical) => {
252
+ const yearStart = startOf(now(), "year");
253
+ const monthStart = month === 1 ? yearStart : shift(yearStart, month - 1, "month");
254
+ const start = day === 1 ? monthStart : shift(monthStart, day - 1, "day");
255
+ return Period.make({
256
+ start,
257
+ end: shift(start, 1, "day"),
258
+ canonical
259
+ });
260
+ };
261
+ const namedCurrentYearDatePeriod = (monthText, dayText, monthNumber, canonical) => {
262
+ const month = monthNumber(monthText);
263
+ const day = Number(dayText);
264
+ if (month === void 0 || !Number.isInteger(day) || day < 1 || day > minimumDaysInMonth(month)) return Option.none();
265
+ return Option.some(currentYearDatePeriod(month, day, canonical(day, month)));
266
+ };
267
+ const currentYearDate = (expression) => {
268
+ const match = String$1.match(/^now\/y(?:\+([1-9]\d*)M)?(?:\+([1-9]\d*)d)?$/u)(expression);
269
+ if (Option.isNone(match)) return Option.none();
270
+ const month = Number(textAt(match.value, 1) || "0") + 1;
271
+ const offset = Number(textAt(match.value, 2) || "0");
272
+ return month >= 1 && month <= 12 ? Option.some([month, offset]) : Option.none();
273
+ };
274
+ const currentYearDatePeriods = (range, canonical) => {
275
+ const filter = formatFilter(range);
276
+ const expressions = [
277
+ filter.gt,
278
+ filter.gte,
279
+ filter.lt,
280
+ filter.lte
281
+ ];
282
+ const dates = /* @__PURE__ */ new Map();
283
+ for (const expression of expressions) {
284
+ if (expression === void 0) continue;
285
+ const date = currentYearDate(expression);
286
+ if (Option.isNone(date)) continue;
287
+ const [month, offset] = date.value;
288
+ for (const day of [offset, offset + 1]) {
289
+ if (day < 1 || day > minimumDaysInMonth(month)) continue;
290
+ dates.set(`${month}-${day}`, [month, day]);
291
+ }
292
+ }
293
+ return [...dates.values()].map(([month, day]) => currentYearDatePeriod(month, day, canonical(day, month)));
294
+ };
122
295
  const fixedMonthPeriod = (year, month, canonical) => {
123
296
  const nextYear = month === 12 ? year + 1 : year;
124
297
  const nextMonth = month === 12 ? 1 : month + 1;
@@ -164,10 +337,13 @@ const monthOfRelativeYear = (month, direction, canonical) => {
164
337
  });
165
338
  };
166
339
  const calendarPeriodOffset = (range) => {
167
- if (range.lower?._tag !== "GreaterThanOrEqual" || range.upper?._tag !== "LessThan" || range.lower.value._tag !== "StartOf" || range.lower.value.base._tag !== "Shift" || range.lower.value.base.amount === 0 || range.lower.value.base.base._tag !== "Now" || range.lower.value.unit !== range.lower.value.base.unit || range.upper.value._tag !== "Shift" || range.upper.value.amount !== 1 || range.upper.value.unit !== range.lower.value.unit || formatInstantExpression(range.upper.value.base) !== formatInstantExpression(range.lower.value)) return Option.none();
340
+ if (!isGreaterThanOrEqual(range.lower) || !isLessThan(range.upper)) return Option.none();
341
+ const start = range.lower.value;
342
+ const end = range.upper.value;
343
+ if (!isStartOf(start) || !isShift(start.base) || start.base.amount === 0 || !isNow(start.base.base) || start.unit !== start.base.unit || !isShift(end) || end.amount !== 1 || end.unit !== start.unit || formatInstantExpression(end.base) !== formatInstantExpression(start)) return Option.none();
168
344
  return Option.some(CalendarPeriodOffset.make({
169
- amount: range.lower.value.base.amount,
170
- unit: range.lower.value.unit
345
+ amount: start.base.amount,
346
+ unit: start.unit
171
347
  }));
172
348
  };
173
349
  const candidate = (range, canonical) => NaturalCandidate.make({
@@ -201,14 +377,7 @@ const joinedPeriodCandidate = (input, joins, parsePeriod, canonical) => {
201
377
  }
202
378
  return Option.none();
203
379
  };
204
- const periodBoundaryCandidate = (period, boundary, canonical) => {
205
- switch (boundary) {
206
- case "since": return candidate(lowerOpenRange(greaterThanOrEqual(period.start)), canonical);
207
- case "before": return candidate(upperOpenRange(lessThan(period.start)), canonical);
208
- case "through": return candidate(upperOpenRange(lessThan(period.end)), canonical);
209
- case "after": return candidate(lowerOpenRange(greaterThanOrEqual(period.end)), canonical);
210
- }
211
- };
380
+ const periodBoundaryCandidate = (period, boundary, canonical) => Match.value(boundary).pipe(Match.when("since", () => candidate(lowerOpenRange(greaterThanOrEqual(period.start)), canonical)), Match.when("before", () => candidate(upperOpenRange(lessThan(period.start)), canonical)), Match.when("through", () => candidate(upperOpenRange(lessThan(period.end)), canonical)), Match.when("after", () => candidate(lowerOpenRange(greaterThanOrEqual(period.end)), canonical)), Match.exhaustive);
212
381
  const openBoundaryCandidate = (input, boundaries, parsePeriod) => {
213
382
  for (const boundary of boundaries) {
214
383
  if (!input.startsWith(boundary[0])) continue;
@@ -286,4 +455,4 @@ const renderPeriodRange = (range, toDate, periods, since, before, through, after
286
455
  return Option.none();
287
456
  };
288
457
  //#endregion
289
- export { Period, calendarPeriodOffset, candidate, datedPeriods, datedQuarterPeriods, fixedDatePeriod, fixedMonthPeriod, fixedQuarterPeriod, fixedYearPeriod, fromNowRange, futurePeriod, futureRange, isoDate, joinedNowCandidate, joinedPeriodCandidate, monthOfRelativeYear, namedDatePeriod, openBoundaryCandidate, parseTrailingCount, periodBoundaryCandidate, periodEndDay, periodRange, periodStartDay, periodToDateRange, periodsFromPhrases, quarterOfRelativeYear, relativePeriod, relativeWeekend, remainingPeriodRange, renderPeriodRange, textAt, trailingPeriod, trailingRange, untilNowRange, validYear };
458
+ export { Period, absoluteDatePeriod, calendarPeriodOffset, candidate, compileCountAliasNormalizer, compoundCountAliases, countAliasVocabulary, currentYearDatePeriods, datedPeriods, datedQuarterPeriods, decimalTens, decomposeShiftedPeriodRange, fixedDatePeriod, fixedMonthPeriod, fixedQuarterPeriod, fixedYearPeriod, formatAbsoluteDate, fromNowRange, futurePeriod, futureRange, isoDate, joinedNowCandidate, joinedPeriodCandidate, monthOfRelativeYear, namedCurrentYearDatePeriod, namedDatePeriod, openBoundaryCandidate, parseTrailingCount, periodBoundaryCandidate, periodDay, periodEndDay, periodPreviousDay, periodRange, periodStartDay, periodToDateRange, periodsFromPhrases, quarterOfRelativeYear, relativePeriod, relativeWeekday, relativeWeekend, remainingPeriodRange, renderPeriodRange, sequentialCountAliases, shiftPeriod, textAt, trailingPeriod, trailingRange, untilNowRange, validYear };