chronolizer 0.4.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.
package/README.md CHANGED
@@ -230,10 +230,12 @@ A date filter has at least one bound. It can have one lower bound and one upper
230
230
  | `lte` | On or before the value |
231
231
 
232
232
  An expression starts with `now` or an ISO date. Operations run from left to right.
233
+ Calendar shifts are not combined or canceled. For example, `2025-01-31||+1M+1M`
234
+ resolves to March 28, not March 31. Each month shift clamps the day to the last
235
+ valid day of that month. Time-zone gaps can also affect intermediate dates.
233
236
 
234
237
  ```text
235
- expression := anchor operation*
236
- anchor := "now" | YYYY-MM-DD | YYYY-MM-DD "||"
238
+ expression := "now" operation* | YYYY-MM-DD | YYYY-MM-DD "||" operation+
237
239
  operation := ("+" | "-") positiveInteger unit | "/" unit
238
240
  unit := "d" | "w" | "M" | "q" | "y"
239
241
  ```
package/dist/ast/fold.mjs CHANGED
@@ -1,19 +1,17 @@
1
- import { DateLiteral, GreaterThanOrEqual, LessThan, Now, Shift, StartOf } from "./schemas.mjs";
2
- import { Match, Schema, absurd } from "effect";
1
+ import { GreaterThanOrEqual, LessThan, Now, Shift, StartOf } from "./schemas.mjs";
2
+ import { Match, Schema } from "effect";
3
3
  //#region src/ast/fold.ts
4
- const isDateLiteral = Schema.is(DateLiteral);
5
4
  const isGreaterThanOrEqual = Schema.is(GreaterThanOrEqual);
6
5
  const isLessThan = Schema.is(LessThan);
7
6
  const isNow = Schema.is(Now);
8
7
  const isShift = Schema.is(Shift);
9
8
  const isStartOf = Schema.is(StartOf);
10
- const foldInstant = (expression, algebra) => {
11
- if (isNow(expression)) return algebra.now();
12
- if (isDateLiteral(expression)) return algebra.dateLiteral(expression.value);
13
- if (isShift(expression)) return algebra.shift(foldInstant(expression.base, algebra), expression.amount, expression.unit);
14
- if (isStartOf(expression)) return algebra.startOf(foldInstant(expression.base, algebra), expression.unit);
15
- return absurd(expression);
16
- };
9
+ const foldInstant = (expression, algebra) => Match.typeTags()({
10
+ Now: () => algebra.now(),
11
+ DateLiteral: (literal) => algebra.dateLiteral(literal.value),
12
+ Shift: (operation) => algebra.shift(foldInstant(operation.base, algebra), operation.amount, operation.unit),
13
+ StartOf: (operation) => algebra.startOf(foldInstant(operation.base, algebra), operation.unit)
14
+ })(expression);
17
15
  const shiftOffset = (base, amount, unit) => Match.value(unit).pipe(Match.when("year", () => ({
18
16
  ...base,
19
17
  months: base.months + amount * 12
@@ -1,21 +1,12 @@
1
- import { Shift } from "./schemas.mjs";
2
1
  import { boundedRange, dateLiteral, greaterThan, greaterThanOrEqual, lessThan, lessThanOrEqual, lowerOpenRange, now, shift, startOf, upperOpenRange } from "./constructors.mjs";
3
- import { Match, Schema } from "effect";
2
+ import { foldInstant } from "./fold.mjs";
3
+ import { Match } from "effect";
4
4
  //#region src/ast/normalize.ts
5
- const isShift = Schema.is(Shift);
6
- const normalizeInstant = (expression) => Match.valueTags(expression, {
7
- Now: () => now(),
8
- DateLiteral: (literal) => dateLiteral(literal.value),
9
- StartOf: (operation) => startOf(normalizeInstant(operation.base), operation.unit),
10
- Shift: (operation) => {
11
- const base = normalizeInstant(operation.base);
12
- if (operation.amount === 0) return base;
13
- if (isShift(base) && base.unit === operation.unit) {
14
- const amount = base.amount + operation.amount;
15
- if (Number.isSafeInteger(amount)) return normalizeInstant(shift(base.base, amount, operation.unit));
16
- }
17
- return shift(base, operation.amount, operation.unit);
18
- }
5
+ const normalizeInstant = (expression) => foldInstant(expression, {
6
+ now,
7
+ dateLiteral,
8
+ startOf,
9
+ shift: (base, amount, unit) => amount === 0 ? base : shift(base, amount, unit)
19
10
  });
20
11
  const normalizeLower = (bound) => Match.valueTags(bound, {
21
12
  GreaterThan: (value) => greaterThan(normalizeInstant(value.value)),
@@ -31,13 +31,12 @@ const formatUpper = Match.typeTags()({
31
31
  LessThanOrEqual: (bound) => DateFilter.make({ lte: formatInstantExpression(bound.value) })
32
32
  });
33
33
  const formatFilter = (range) => {
34
- const normalized = normalizeRange(range);
35
- if (normalized.lower !== void 0 && normalized.upper !== void 0) return DateFilter.make({
36
- ...formatLower(normalized.lower),
37
- ...formatUpper(normalized.upper)
34
+ if (range.lower !== void 0 && range.upper !== void 0) return DateFilter.make({
35
+ ...formatLower(range.lower),
36
+ ...formatUpper(range.upper)
38
37
  });
39
- if (normalized.lower !== void 0) return formatLower(normalized.lower);
40
- return formatUpper(normalized.upper);
38
+ if (range.lower !== void 0) return formatLower(range.lower);
39
+ return formatUpper(range.upper);
41
40
  };
42
41
  const rangeKey = (range) => {
43
42
  const filter = formatFilter(range);
@@ -1,7 +1,6 @@
1
1
  import { isIsoDate } from "../ast/schemas.mjs";
2
2
  import { dateLiteral, now, shift, startOf } from "../ast/constructors.mjs";
3
3
  import { foldInstant } from "../ast/fold.mjs";
4
- import { normalizeInstant } from "../ast/normalize.mjs";
5
4
  import { FilterExpressionParseError } from "./errors.mjs";
6
5
  import { Effect, Match } from "effect";
7
6
  //#region src/filter/expression.ts
@@ -66,7 +65,7 @@ const appendOperation = (base, operation) => ({
66
65
  text: `${base.text}${base.fixedAnchor ? "||" : ""}${operation}`,
67
66
  fixedAnchor: false
68
67
  });
69
- const formatInstantExpression = (expression) => foldInstant(normalizeInstant(expression), {
68
+ const formatInstantExpression = (expression) => foldInstant(expression, {
70
69
  now: () => ({
71
70
  text: "now",
72
71
  fixedAnchor: false
@@ -75,7 +74,7 @@ const formatInstantExpression = (expression) => foldInstant(normalizeInstant(exp
75
74
  text: value,
76
75
  fixedAnchor: true
77
76
  }),
78
- shift: (base, amount, unit) => appendOperation(base, `${amount < 0 ? "-" : "+"}${Math.abs(amount)}${symbolFromUnit(unit)}`),
77
+ shift: (base, amount, unit) => amount === 0 ? base : appendOperation(base, `${amount < 0 ? "-" : "+"}${Math.abs(amount)}${symbolFromUnit(unit)}`),
79
78
  startOf: (base, unit) => appendOperation(base, `/${symbolFromUnit(unit)}`)
80
79
  }).text;
81
80
  //#endregion
@@ -86,11 +86,13 @@ const createRegistry = Effect.fn(function* () {
86
86
  })));
87
87
  });
88
88
  const resolve = Effect.fn(function* (locale) {
89
+ const cached = (yield* Ref.get(state)).compiledLanguages.get(locale);
90
+ if (cached !== void 0) return cached;
89
91
  const canonical = canonicalBaseLocale(locale);
90
92
  if (Option.isSome(canonical)) {
91
93
  const compiled = yield* Ref.modify(state, (current) => {
92
- const cached = current.compiledLanguages.get(canonical.value);
93
- if (cached !== void 0) return [Option.some(cached), current];
94
+ const canonicalCached = current.compiledLanguages.get(canonical.value);
95
+ if (canonicalCached !== void 0) return [Option.some(canonicalCached), current];
94
96
  const language = compileLanguage(canonical.value, current.entries);
95
97
  if (Option.isNone(language)) return [language, current];
96
98
  const cache = new Map(current.compiledLanguages);
@@ -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 { absoluteDatePeriod, calendarPeriodOffset, candidate, compileCountAliasNormalizer, compoundCountAliases, countAliasVocabulary, currentYearDatePeriods, datedPeriods, datedQuarterPeriods, decimalTens, decomposeShiftedPeriodRange, fixedDatePeriod, fixedMonthPeriod, fixedQuarterPeriod, fixedYearPeriod, fromNowRange, futurePeriod, futureRange, isoDate, joinedNowCandidate, joinedPeriodCandidate, monthOfRelativeYear, namedCurrentYearDatePeriod, namedDatePeriod, openBoundaryCandidate, parseTrailingCount, periodEndDay, periodRange, periodStartDay, periodToDateRange, periodsFromPhrases, quarterOfRelativeYear, relativePeriod, relativeWeekday, relativeWeekend, remainingPeriodRange, renderPeriodRange, sequentialCountAliases, shiftPeriod, 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, 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/cs.ts
10
9
  const months = [
@@ -486,15 +485,7 @@ const parseNamedDate = (input) => {
486
485
  const named = String$1.match(/^([0-3]?\d)\.?(?: )([a-záčďéěíňóřšťúůýž]+\.?) (\d{4})$/u)(input);
487
486
  if (Option.isSome(named)) return namedDatePeriod(textAt(named.value, 3), textAt(named.value, 2), textAt(named.value, 1), monthNumber, dateLabel);
488
487
  const numeric = String$1.match(/^([0-3]?\d)(?:\. ?|[/-])([01]?\d)(?:\. ?|[/-])(\d{4})$/u)(input);
489
- if (Option.isSome(numeric)) {
490
- const year = validYear(textAt(numeric.value, 3));
491
- const month = Number(textAt(numeric.value, 2));
492
- const day = Number(textAt(numeric.value, 1));
493
- if (year !== void 0 && month >= 1 && month <= 12) {
494
- const value = isoDate(year, month, day);
495
- if (isIsoDate(value) && value !== "9999-12-31") return Option.some(fixedDatePeriod(value, dateLabel(day, month, year)));
496
- }
497
- }
488
+ if (Option.isSome(numeric)) return namedDatePeriod(textAt(numeric.value, 3), textAt(numeric.value, 2), textAt(numeric.value, 1), Number, dateLabel);
498
489
  const current = String$1.match(/^([0-3]?\d)\.? ([a-záčďéěíňóřšťúůýž]+\.?)$/u)(input);
499
490
  return Option.isSome(current) ? namedCurrentYearDatePeriod(textAt(current.value, 2), textAt(current.value, 1), monthNumber, currentDateLabel) : Option.none();
500
491
  };
@@ -616,14 +607,12 @@ const parsePeriod = (input) => {
616
607
  return Option.isSome(base) ? base : parseCalendarOffset(input);
617
608
  };
618
609
  const countedUnit = (value) => unitAliases.find((entry) => entry[0] === value)?.[1];
619
- const countedUnitPattern = "den|dny|dnů|dnem|týden|týdny|týdnů|týdnem|měsíc|měsíce|měsíců|měsícem|měsíci|čtvrtletí|čtvrtletím|čtvrtletími|rok|roky|let|rokem|lety";
620
- const countedPattern = (source) => new RegExp(source.replace("UNIT", countedUnitPattern), "u");
621
- const calendarPastPattern = countedPattern("^před ([1-9]\\d*) (UNIT)$");
622
- const calendarFuturePatterns = [countedPattern("^za ([1-9]\\d*) (UNIT)$")];
623
- const rollingPastPatterns = [countedPattern("^(?:poslední|posledních|uplynulé|uplynulých) ([1-9]\\d*) (UNIT)$")];
624
- const rollingFuturePatterns = [countedPattern("^(?:příští|následující) ([1-9]\\d*) (UNIT)$")];
625
- const rollingSincePattern = countedPattern("^za poslední ([1-9]\\d*) (UNIT)$");
626
- const rollingBarePattern = countedPattern("^([1-9]\\d*) (UNIT)$");
610
+ const calendarPastPattern = /^před ([1-9]\d*) ([^ ]+)$/u;
611
+ const calendarFuturePatterns = [/^za ([1-9]\d*) ([^ ]+)$/u];
612
+ const rollingPastPatterns = [/^(?:poslední|posledních|uplynulé|uplynulých) ([1-9]\d*) ([^ ]+)$/u];
613
+ const rollingFuturePatterns = [/^(?:příští|následující) ([1-9]\d*) ([^ ]+)$/u];
614
+ const rollingSincePattern = /^za poslední ([1-9]\d*) ([^ ]+)$/u;
615
+ const rollingBarePattern = /^([1-9]\d*) ([^ ]+)$/u;
627
616
  const firstPatternMatch = (input, patterns) => Option.firstSomeOf(patterns.map((pattern) => String$1.match(pattern)(input)));
628
617
  const singularRollingPhrases = units.flatMap((entry) => [
629
618
  {
@@ -742,7 +731,7 @@ const parseCzech = (input) => {
742
731
  "dnešek až "
743
732
  ], parsePeriod, (period) => `od ${period} do dneška`, (period) => `od dneška do ${period}`);
744
733
  if (Option.isSome(nowBounded)) return nowBounded;
745
- const bounded = joinedPeriodCandidate(input, [
734
+ const bounded = joinedPeriodCandidate(input.replace(/ včetně$/u, ""), [
746
735
  ["od ", " do "],
747
736
  ["mezi ", " a "],
748
737
  ["", " - "],