chronolizer 0.1.0 → 0.2.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.
Files changed (62) hide show
  1. package/README.md +70 -7
  2. package/dist/ast/constructors.d.mts +140 -0
  3. package/dist/ast/constructors.mjs +25 -0
  4. package/dist/ast/fold.d.mts +12 -0
  5. package/dist/ast/fold.mjs +51 -0
  6. package/dist/ast/normalize.d.mts +42 -0
  7. package/dist/ast/normalize.mjs +32 -0
  8. package/dist/ast/schemas.d.mts +87 -0
  9. package/dist/ast/schemas.mjs +74 -0
  10. package/dist/filter/codec.d.mts +92 -0
  11. package/dist/filter/codec.mjs +48 -0
  12. package/dist/filter/errors.d.mts +14 -0
  13. package/dist/filter/errors.mjs +10 -0
  14. package/dist/filter/expression.d.mts +8 -0
  15. package/dist/filter/expression.mjs +93 -0
  16. package/dist/filter/schema.d.mts +13 -0
  17. package/dist/filter/schema.mjs +11 -0
  18. package/dist/filter/transformation.d.mts +37 -0
  19. package/dist/filter/transformation.mjs +20 -0
  20. package/dist/index.d.mts +22 -969
  21. package/dist/index.mjs +22 -2305
  22. package/dist/language/errors.d.mts +38 -0
  23. package/dist/language/errors.mjs +30 -0
  24. package/dist/language/model.d.mts +239 -0
  25. package/dist/language/model.mjs +53 -0
  26. package/dist/language/registry.d.mts +17 -0
  27. package/dist/language/registry.mjs +134 -0
  28. package/dist/locales/cs.d.mts +10 -0
  29. package/dist/locales/cs.mjs +693 -0
  30. package/dist/locales/de.d.mts +10 -0
  31. package/dist/locales/de.mjs +828 -0
  32. package/dist/locales/en.d.mts +10 -0
  33. package/dist/locales/en.mjs +550 -0
  34. package/dist/locales/es.d.mts +10 -0
  35. package/dist/locales/es.mjs +660 -0
  36. package/dist/locales/fr.d.mts +10 -0
  37. package/dist/locales/fr.mjs +737 -0
  38. package/dist/locales/nl.d.mts +10 -0
  39. package/dist/locales/nl.mjs +652 -0
  40. package/dist/locales/pl.d.mts +10 -0
  41. package/dist/locales/pl.mjs +711 -0
  42. package/dist/locales/shared.mjs +289 -0
  43. package/dist/locales/tr.d.mts +10 -0
  44. package/dist/locales/tr.mjs +660 -0
  45. package/dist/natural/correction.d.mts +13 -0
  46. package/dist/natural/correction.mjs +73 -0
  47. package/dist/natural/format.d.mts +47 -0
  48. package/dist/natural/format.mjs +14 -0
  49. package/dist/natural/parse.d.mts +99 -0
  50. package/dist/natural/parse.mjs +66 -0
  51. package/dist/natural/policy.mjs +6 -0
  52. package/dist/natural/suggest.d.mts +53 -0
  53. package/dist/natural/suggest.mjs +24 -0
  54. package/dist/natural/suggestion.d.mts +4 -0
  55. package/dist/natural/suggestion.mjs +79 -0
  56. package/dist/natural/text.d.mts +5 -0
  57. package/dist/natural/text.mjs +5 -0
  58. package/dist/resolve/resolve.d.mts +79 -0
  59. package/dist/resolve/resolve.mjs +66 -0
  60. package/dist/resolve/schema.d.mts +59 -0
  61. package/dist/resolve/schema.mjs +28 -0
  62. package/package.json +15 -3
package/README.md CHANGED
@@ -2,12 +2,27 @@
2
2
 
3
3
  Chronolizer is an Effect v4 library for bidirectional natural-language date ranges.
4
4
 
5
- It converts complete English and German date-range expressions to a small date-math format. It also renders supported ranges to canonical natural language and resolves the AST with an explicit Effect time zone.
5
+ It converts complete English, German, Spanish, French, Dutch, Turkish, Czech, and Polish date-range expressions to a small date-math format. It also renders supported ranges to canonical natural language and resolves the AST with an explicit Effect time zone.
6
6
 
7
7
  ## Status
8
8
 
9
9
  This package uses `effect@4.0.0-rc.112`. Its API can change while Effect v4 is in release-candidate status.
10
10
 
11
+ ## Package entry points
12
+
13
+ Chronolizer is side-effect free and publishes separate ESM entry points:
14
+
15
+ ```ts
16
+ import { formatNatural, parseFilter, parseNatural, resolve, suggestNatural } from "chronolizer";
17
+ import { GermanLanguageLayer } from "chronolizer/locales/de";
18
+ ```
19
+
20
+ - `chronolizer` is the single main API entry. It also includes the English language pack.
21
+ - `chronolizer/locales/en`, `chronolizer/locales/de`, `chronolizer/locales/es`, `chronolizer/locales/fr`, `chronolizer/locales/nl`, `chronolizer/locales/tr`, `chronolizer/locales/cs`, and `chronolizer/locales/pl` expose language packs separately.
22
+ - Every non-English language must be imported from its own locale subpath.
23
+
24
+ The package declares `sideEffects: false`, so consumer bundlers can remove unused main API exports. The package build runs Publint and Are the Types Wrong. CI checks transitive gzip limits and verifies that the main entry does not load non-English locales.
25
+
11
26
  ## Main concepts
12
27
 
13
28
  - Effect Schema owns the date AST, filter data, results, and errors.
@@ -22,7 +37,7 @@ This package uses `effect@4.0.0-rc.112`. Its API can change while Effect v4 is i
22
37
  ## Natural language to filter
23
38
 
24
39
  ```ts
25
- import { DefaultLanguageLayer, formatFilter, parseNatural } from "chronolizer";
40
+ import { EnglishLanguageLayer, formatFilter, parseNatural } from "chronolizer";
26
41
  import { Effect } from "effect";
27
42
 
28
43
  const run = Effect.fn(function* () {
@@ -32,7 +47,7 @@ const run = Effect.fn(function* () {
32
47
  });
33
48
 
34
49
  return formatFilter(result.range);
35
- }, Effect.provide(DefaultLanguageLayer));
50
+ }, Effect.provide(EnglishLanguageLayer));
36
51
 
37
52
  const program = run();
38
53
 
@@ -60,7 +75,27 @@ seit Januar 2025 -> { gte: "2025-01-01" }
60
75
 
61
76
  Chronolizer parses the complete input. It does not extract a date phrase from a larger sentence.
62
77
 
63
- Supported families include named and abbreviated months, named dates, quarters, weekends, period starts and ends, past and future rolling windows, calendar offsets, open boundaries, `now`-bounded ranges, and explicit inclusive connectors. English and German use their own grammar and canonical forms.
78
+ Supported families include named and abbreviated months, named dates, quarters, weekends, period starts and ends, past and future rolling windows, calendar offsets, open boundaries, `now`-bounded ranges, and explicit inclusive connectors. Each language uses its own grammar and canonical forms.
79
+
80
+ ## Autocomplete natural language
81
+
82
+ Use `suggestNatural()` for autocomplete fields. Each Schema-owned suggestion contains canonical display text and the semantic range that it represents:
83
+
84
+ ```ts
85
+ import { EnglishLanguageLayer, suggestNatural } from "chronolizer";
86
+ import { Effect } from "effect";
87
+
88
+ const program = suggestNatural("last m", {
89
+ locale: "en",
90
+ limit: 5,
91
+ }).pipe(Effect.provide(EnglishLanguageLayer));
92
+
93
+ // [{ text: "last month", range: ... }]
94
+ ```
95
+
96
+ Suggestions support incomplete words, bounded spelling errors, numeric rolling periods, and partial four-digit years. For example, `january 202` suggests `January 2020`, `January 2021`, and later matching years. Every returned suggestion is checked by the exact parser. `allowFuture: false` applies the same positive-relative-range policy as `parseNatural()`.
97
+
98
+ The default limit is 10. The maximum accepted limit is 100. A nonpositive or invalid limit returns no suggestions.
64
99
 
65
100
  ### Exclude positive relative ranges
66
101
 
@@ -78,7 +113,8 @@ This option rejects relative forms such as `next month`, `this year`, and `in 3
78
113
  ## Filter to natural language
79
114
 
80
115
  ```ts
81
- import { DefaultLanguageLayer, formatNatural, parseFilter } from "chronolizer";
116
+ import { formatNatural, parseFilter } from "chronolizer";
117
+ import { GermanLanguageLayer } from "chronolizer/locales/de";
82
118
  import { Effect } from "effect";
83
119
 
84
120
  const run = Effect.fn(function* () {
@@ -88,7 +124,7 @@ const run = Effect.fn(function* () {
88
124
  });
89
125
 
90
126
  return yield* formatNatural(range, { locale: "de" });
91
- }, Effect.provide(DefaultLanguageLayer));
127
+ }, Effect.provide(GermanLanguageLayer));
92
128
 
93
129
  const program = run();
94
130
 
@@ -170,22 +206,49 @@ Strict mode never runs correction.
170
206
 
171
207
  `LanguageRegistry` is an Effect service. A language is a scoped plugin contribution. `languagePluginsLayer` validates plugin identifiers, rejects conflicting base languages, applies deterministic extension order, and removes registrations when the Layer scope closes.
172
208
 
209
+ Combine all required language plugins in one registry Layer:
210
+
211
+ ```ts
212
+ import { EnglishLanguage, languagePluginsLayer, parseNatural } from "chronolizer";
213
+ import { GermanLanguage } from "chronolizer/locales/de";
214
+ import { PolishLanguage } from "chronolizer/locales/pl";
215
+ import { Effect } from "effect";
216
+
217
+ const Languages = languagePluginsLayer([EnglishLanguage, GermanLanguage, PolishLanguage]);
218
+
219
+ const program = parseNatural("letzten Monat", {
220
+ locale: "de",
221
+ }).pipe(Effect.provide(Languages));
222
+ ```
223
+
224
+ Importing a language plugin does not register it globally. Do not merge individual `*LanguageLayer` values for multi-language use because each one creates a separate registry. Use `languagePluginsLayer([...plugins])` instead.
225
+
173
226
  Language identifiers use canonical BCP 47 base tags. Lookup removes one subtag at a time. For example, `zh-Hant-TW` tries `zh-Hant-TW`, `zh-Hant`, and then `zh`.
174
227
 
175
228
  Each base language owns:
176
229
 
177
230
  - exact parsing and canonical rendering;
231
+ - autocomplete phrase generation;
178
232
  - optional text normalization;
179
233
  - its typo-correction strategy, which can be disabled;
180
234
  - vocabulary shared with registered language extensions.
181
235
 
236
+ Language extensions can add both exact parsers and autocomplete phrases. The registry combines extension suggestions in the same deterministic order as extension parsers. The core accepts only suggestions that one of the compiled exact parsers can parse.
237
+
182
238
  `normalizeNaturalText` and `correctWhitespaceSeparatedText` are available for languages that use whitespace-separated words. A compact-script language can provide character, dictionary, or `Intl.Segmenter` based correction without changing Chronolizer core.
183
239
 
184
240
  Built-in plugins:
185
241
 
186
242
  - `EnglishLanguage`
187
243
  - `GermanLanguage`
188
- - `DefaultLanguageLayer`
244
+ - `SpanishLanguage`
245
+ - `FrenchLanguage`
246
+ - `DutchLanguage`
247
+ - `TurkishLanguage`
248
+ - `CzechLanguage`
249
+ - `PolishLanguage`
250
+
251
+ Import every non-English language from its locale subpath. This keeps the main bundle small and makes language selection explicit.
189
252
 
190
253
  Chinese and Japanese language packs are not included yet.
191
254
 
@@ -0,0 +1,140 @@
1
+ import { InstantExpr, IsoDate, LowerBound, Shift, StartOf, Unit, UpperBound } from "./schemas.mjs";
2
+ //#region src/ast/constructors.d.ts
3
+ declare const now: () => {
4
+ readonly _tag: "Now";
5
+ };
6
+ declare const dateLiteral: (value: IsoDate) => {
7
+ readonly _tag: "DateLiteral";
8
+ readonly value: string;
9
+ };
10
+ declare const shift: (base: InstantExpr, amount: number, unit: Unit) => Shift;
11
+ declare const startOf: (base: InstantExpr, unit: Unit) => StartOf;
12
+ declare const greaterThan: (value: InstantExpr) => {
13
+ readonly _tag: "GreaterThan";
14
+ readonly value: InstantExpr;
15
+ };
16
+ declare const greaterThanOrEqual: (value: InstantExpr) => {
17
+ readonly _tag: "GreaterThanOrEqual";
18
+ readonly value: InstantExpr;
19
+ };
20
+ declare const lessThan: (value: InstantExpr) => {
21
+ readonly _tag: "LessThan";
22
+ readonly value: InstantExpr;
23
+ };
24
+ declare const lessThanOrEqual: (value: InstantExpr) => {
25
+ readonly _tag: "LessThanOrEqual";
26
+ readonly value: InstantExpr;
27
+ };
28
+ declare const boundedRange: (lower: LowerBound, upper: UpperBound) => {
29
+ readonly _tag: "DateRange";
30
+ readonly lower: {
31
+ readonly _tag: "GreaterThan";
32
+ readonly value: InstantExpr;
33
+ } | {
34
+ readonly _tag: "GreaterThanOrEqual";
35
+ readonly value: InstantExpr;
36
+ };
37
+ readonly upper: {
38
+ readonly _tag: "LessThan";
39
+ readonly value: InstantExpr;
40
+ } | {
41
+ readonly _tag: "LessThanOrEqual";
42
+ readonly value: InstantExpr;
43
+ };
44
+ } | {
45
+ readonly _tag: "DateRange";
46
+ readonly lower: {
47
+ readonly _tag: "GreaterThan";
48
+ readonly value: InstantExpr;
49
+ } | {
50
+ readonly _tag: "GreaterThanOrEqual";
51
+ readonly value: InstantExpr;
52
+ };
53
+ readonly upper?: never;
54
+ } | {
55
+ readonly _tag: "DateRange";
56
+ readonly lower?: never;
57
+ readonly upper: {
58
+ readonly _tag: "LessThan";
59
+ readonly value: InstantExpr;
60
+ } | {
61
+ readonly _tag: "LessThanOrEqual";
62
+ readonly value: InstantExpr;
63
+ };
64
+ };
65
+ declare const lowerOpenRange: (lower: LowerBound) => {
66
+ readonly _tag: "DateRange";
67
+ readonly lower: {
68
+ readonly _tag: "GreaterThan";
69
+ readonly value: InstantExpr;
70
+ } | {
71
+ readonly _tag: "GreaterThanOrEqual";
72
+ readonly value: InstantExpr;
73
+ };
74
+ readonly upper: {
75
+ readonly _tag: "LessThan";
76
+ readonly value: InstantExpr;
77
+ } | {
78
+ readonly _tag: "LessThanOrEqual";
79
+ readonly value: InstantExpr;
80
+ };
81
+ } | {
82
+ readonly _tag: "DateRange";
83
+ readonly lower: {
84
+ readonly _tag: "GreaterThan";
85
+ readonly value: InstantExpr;
86
+ } | {
87
+ readonly _tag: "GreaterThanOrEqual";
88
+ readonly value: InstantExpr;
89
+ };
90
+ readonly upper?: never;
91
+ } | {
92
+ readonly _tag: "DateRange";
93
+ readonly lower?: never;
94
+ readonly upper: {
95
+ readonly _tag: "LessThan";
96
+ readonly value: InstantExpr;
97
+ } | {
98
+ readonly _tag: "LessThanOrEqual";
99
+ readonly value: InstantExpr;
100
+ };
101
+ };
102
+ declare const upperOpenRange: (upper: UpperBound) => {
103
+ readonly _tag: "DateRange";
104
+ readonly lower: {
105
+ readonly _tag: "GreaterThan";
106
+ readonly value: InstantExpr;
107
+ } | {
108
+ readonly _tag: "GreaterThanOrEqual";
109
+ readonly value: InstantExpr;
110
+ };
111
+ readonly upper: {
112
+ readonly _tag: "LessThan";
113
+ readonly value: InstantExpr;
114
+ } | {
115
+ readonly _tag: "LessThanOrEqual";
116
+ readonly value: InstantExpr;
117
+ };
118
+ } | {
119
+ readonly _tag: "DateRange";
120
+ readonly lower: {
121
+ readonly _tag: "GreaterThan";
122
+ readonly value: InstantExpr;
123
+ } | {
124
+ readonly _tag: "GreaterThanOrEqual";
125
+ readonly value: InstantExpr;
126
+ };
127
+ readonly upper?: never;
128
+ } | {
129
+ readonly _tag: "DateRange";
130
+ readonly lower?: never;
131
+ readonly upper: {
132
+ readonly _tag: "LessThan";
133
+ readonly value: InstantExpr;
134
+ } | {
135
+ readonly _tag: "LessThanOrEqual";
136
+ readonly value: InstantExpr;
137
+ };
138
+ };
139
+ //#endregion
140
+ export { boundedRange, dateLiteral, greaterThan, greaterThanOrEqual, lessThan, lessThanOrEqual, lowerOpenRange, now, shift, startOf, upperOpenRange };
@@ -0,0 +1,25 @@
1
+ import { DateLiteral, DateRangeExpr, GreaterThan, GreaterThanOrEqual, LessThan, LessThanOrEqual, Now, Shift, StartOf } from "./schemas.mjs";
2
+ //#region src/ast/constructors.ts
3
+ const now = () => Now.make({});
4
+ const dateLiteral = (value) => DateLiteral.make({ value });
5
+ const shift = (base, amount, unit) => Shift.make({
6
+ base,
7
+ amount,
8
+ unit
9
+ });
10
+ const startOf = (base, unit) => StartOf.make({
11
+ base,
12
+ unit
13
+ });
14
+ const greaterThan = (value) => GreaterThan.make({ value });
15
+ const greaterThanOrEqual = (value) => GreaterThanOrEqual.make({ value });
16
+ const lessThan = (value) => LessThan.make({ value });
17
+ const lessThanOrEqual = (value) => LessThanOrEqual.make({ value });
18
+ const boundedRange = (lower, upper) => DateRangeExpr.make({
19
+ lower,
20
+ upper
21
+ });
22
+ const lowerOpenRange = (lower) => DateRangeExpr.make({ lower });
23
+ const upperOpenRange = (upper) => DateRangeExpr.make({ upper });
24
+ //#endregion
25
+ export { boundedRange, dateLiteral, greaterThan, greaterThanOrEqual, lessThan, lessThanOrEqual, lowerOpenRange, now, shift, startOf, upperOpenRange };
@@ -0,0 +1,12 @@
1
+ import { DateRangeExpr, InstantExpr, IsoDate, Unit } from "./schemas.mjs";
2
+ //#region src/ast/fold.d.ts
3
+ interface InstantAlgebra<A> {
4
+ readonly now: () => A;
5
+ readonly dateLiteral: (value: IsoDate) => A;
6
+ readonly shift: (base: A, amount: number, unit: Unit) => A;
7
+ readonly startOf: (base: A, unit: Unit) => A;
8
+ }
9
+ declare const foldInstant: <A>(expression: InstantExpr, algebra: InstantAlgebra<A>) => A;
10
+ declare const containsPositiveShift: (range: DateRangeExpr) => boolean;
11
+ //#endregion
12
+ export { InstantAlgebra, containsPositiveShift, foldInstant };
@@ -0,0 +1,51 @@
1
+ //#region src/ast/fold.ts
2
+ const foldInstant = (expression, algebra) => {
3
+ switch (expression._tag) {
4
+ case "Now": return algebra.now();
5
+ case "DateLiteral": return algebra.dateLiteral(expression.value);
6
+ case "Shift": return algebra.shift(foldInstant(expression.base, algebra), expression.amount, expression.unit);
7
+ case "StartOf": return algebra.startOf(foldInstant(expression.base, algebra), expression.unit);
8
+ }
9
+ };
10
+ const shiftOffset = (base, amount, unit) => {
11
+ switch (unit) {
12
+ case "year": return {
13
+ ...base,
14
+ months: base.months + amount * 12
15
+ };
16
+ case "quarter": return {
17
+ ...base,
18
+ months: base.months + amount * 3
19
+ };
20
+ case "month": return {
21
+ ...base,
22
+ months: base.months + amount
23
+ };
24
+ case "week": return {
25
+ ...base,
26
+ days: base.days + amount * 7
27
+ };
28
+ case "day": return {
29
+ ...base,
30
+ days: base.days + amount
31
+ };
32
+ }
33
+ };
34
+ const containsPositiveShiftInstant = (expression) => {
35
+ const offset = foldInstant(expression, {
36
+ now: () => ({
37
+ months: 0,
38
+ days: 0
39
+ }),
40
+ dateLiteral: () => ({
41
+ months: 0,
42
+ days: 0
43
+ }),
44
+ shift: shiftOffset,
45
+ startOf: (base) => base
46
+ });
47
+ return offset.months > 0 || offset.days > 0;
48
+ };
49
+ const containsPositiveShift = (range) => range.lower !== void 0 && containsPositiveShiftInstant(range.lower.value) || range.upper !== void 0 && containsPositiveShiftInstant(range.upper.value);
50
+ //#endregion
51
+ export { containsPositiveShift, foldInstant };
@@ -0,0 +1,42 @@
1
+ import { DateRangeExpr, InstantExpr } from "./schemas.mjs";
2
+ //#region src/ast/normalize.d.ts
3
+ declare const normalizeInstant: (expression: InstantExpr) => InstantExpr;
4
+ declare const normalizeRange: (range: DateRangeExpr) => {
5
+ readonly _tag: "DateRange";
6
+ readonly lower: {
7
+ readonly _tag: "GreaterThan";
8
+ readonly value: InstantExpr;
9
+ } | {
10
+ readonly _tag: "GreaterThanOrEqual";
11
+ readonly value: InstantExpr;
12
+ };
13
+ readonly upper: {
14
+ readonly _tag: "LessThan";
15
+ readonly value: InstantExpr;
16
+ } | {
17
+ readonly _tag: "LessThanOrEqual";
18
+ readonly value: InstantExpr;
19
+ };
20
+ } | {
21
+ readonly _tag: "DateRange";
22
+ readonly lower: {
23
+ readonly _tag: "GreaterThan";
24
+ readonly value: InstantExpr;
25
+ } | {
26
+ readonly _tag: "GreaterThanOrEqual";
27
+ readonly value: InstantExpr;
28
+ };
29
+ readonly upper?: never;
30
+ } | {
31
+ readonly _tag: "DateRange";
32
+ readonly lower?: never;
33
+ readonly upper: {
34
+ readonly _tag: "LessThan";
35
+ readonly value: InstantExpr;
36
+ } | {
37
+ readonly _tag: "LessThanOrEqual";
38
+ readonly value: InstantExpr;
39
+ };
40
+ };
41
+ //#endregion
42
+ export { normalizeInstant, normalizeRange };
@@ -0,0 +1,32 @@
1
+ import { boundedRange, dateLiteral, greaterThan, greaterThanOrEqual, lessThan, lessThanOrEqual, lowerOpenRange, now, shift, startOf, upperOpenRange } from "./constructors.mjs";
2
+ import { Match } from "effect";
3
+ //#region src/ast/normalize.ts
4
+ const normalizeInstant = (expression) => Match.valueTags(expression, {
5
+ Now: () => now(),
6
+ DateLiteral: (literal) => dateLiteral(literal.value),
7
+ StartOf: (operation) => startOf(normalizeInstant(operation.base), operation.unit),
8
+ Shift: (operation) => {
9
+ const base = normalizeInstant(operation.base);
10
+ if (operation.amount === 0) return base;
11
+ if (base._tag === "Shift" && base.unit === operation.unit) {
12
+ const amount = base.amount + operation.amount;
13
+ if (Number.isSafeInteger(amount)) return normalizeInstant(shift(base.base, amount, operation.unit));
14
+ }
15
+ return shift(base, operation.amount, operation.unit);
16
+ }
17
+ });
18
+ const normalizeLower = (bound) => Match.valueTags(bound, {
19
+ GreaterThan: (value) => greaterThan(normalizeInstant(value.value)),
20
+ GreaterThanOrEqual: (value) => greaterThanOrEqual(normalizeInstant(value.value))
21
+ });
22
+ const normalizeUpper = (bound) => Match.valueTags(bound, {
23
+ LessThan: (value) => lessThan(normalizeInstant(value.value)),
24
+ LessThanOrEqual: (value) => lessThanOrEqual(normalizeInstant(value.value))
25
+ });
26
+ const normalizeRange = (range) => {
27
+ if (range.lower !== void 0 && range.upper !== void 0) return boundedRange(normalizeLower(range.lower), normalizeUpper(range.upper));
28
+ if (range.lower !== void 0) return lowerOpenRange(normalizeLower(range.lower));
29
+ return upperOpenRange(normalizeUpper(range.upper));
30
+ };
31
+ //#endregion
32
+ export { normalizeInstant, normalizeRange };
@@ -0,0 +1,87 @@
1
+ import { Schema } from "effect";
2
+ //#region src/ast/schemas.d.ts
3
+ declare const Unit: Schema.Literals<readonly ["day", "week", "month", "quarter", "year"]>;
4
+ type Unit = typeof Unit.Type;
5
+ declare const daysInMonth: (year: number, month: number) => 28 | 29 | 30 | 31;
6
+ declare const isIsoDate: (value: string) => boolean;
7
+ declare const IsoDate: Schema.String;
8
+ type IsoDate = typeof IsoDate.Type;
9
+ interface Now {
10
+ readonly _tag: "Now";
11
+ }
12
+ interface DateLiteral {
13
+ readonly _tag: "DateLiteral";
14
+ readonly value: IsoDate;
15
+ }
16
+ interface Shift {
17
+ readonly _tag: "Shift";
18
+ readonly base: InstantExpr;
19
+ readonly amount: number;
20
+ readonly unit: Unit;
21
+ }
22
+ interface StartOf {
23
+ readonly _tag: "StartOf";
24
+ readonly base: InstantExpr;
25
+ readonly unit: Unit;
26
+ }
27
+ type InstantExpr = Now | DateLiteral | Shift | StartOf;
28
+ declare const Now: Schema.TaggedStruct<"Now", {}>;
29
+ declare const DateLiteral: Schema.TaggedStruct<"DateLiteral", {
30
+ readonly value: Schema.String;
31
+ }>;
32
+ declare const Shift: Schema.Codec<Shift>;
33
+ declare const StartOf: Schema.Codec<StartOf>;
34
+ declare const InstantExpr: Schema.Codec<InstantExpr>;
35
+ declare const GreaterThan: Schema.TaggedStruct<"GreaterThan", {
36
+ readonly value: Schema.Codec<InstantExpr, InstantExpr, never, never>;
37
+ }>;
38
+ declare const GreaterThanOrEqual: Schema.TaggedStruct<"GreaterThanOrEqual", {
39
+ readonly value: Schema.Codec<InstantExpr, InstantExpr, never, never>;
40
+ }>;
41
+ declare const LessThan: Schema.TaggedStruct<"LessThan", {
42
+ readonly value: Schema.Codec<InstantExpr, InstantExpr, never, never>;
43
+ }>;
44
+ declare const LessThanOrEqual: Schema.TaggedStruct<"LessThanOrEqual", {
45
+ readonly value: Schema.Codec<InstantExpr, InstantExpr, never, never>;
46
+ }>;
47
+ declare const LowerBound: Schema.Union<readonly [Schema.TaggedStruct<"GreaterThan", {
48
+ readonly value: Schema.Codec<InstantExpr, InstantExpr, never, never>;
49
+ }>, Schema.TaggedStruct<"GreaterThanOrEqual", {
50
+ readonly value: Schema.Codec<InstantExpr, InstantExpr, never, never>;
51
+ }>]>;
52
+ type LowerBound = typeof LowerBound.Type;
53
+ declare const UpperBound: Schema.Union<readonly [Schema.TaggedStruct<"LessThan", {
54
+ readonly value: Schema.Codec<InstantExpr, InstantExpr, never, never>;
55
+ }>, Schema.TaggedStruct<"LessThanOrEqual", {
56
+ readonly value: Schema.Codec<InstantExpr, InstantExpr, never, never>;
57
+ }>]>;
58
+ type UpperBound = typeof UpperBound.Type;
59
+ declare const DateRangeExpr: Schema.Union<readonly [Schema.TaggedStruct<"DateRange", {
60
+ readonly lower: Schema.Union<readonly [Schema.TaggedStruct<"GreaterThan", {
61
+ readonly value: Schema.Codec<InstantExpr, InstantExpr, never, never>;
62
+ }>, Schema.TaggedStruct<"GreaterThanOrEqual", {
63
+ readonly value: Schema.Codec<InstantExpr, InstantExpr, never, never>;
64
+ }>]>;
65
+ readonly upper: Schema.Union<readonly [Schema.TaggedStruct<"LessThan", {
66
+ readonly value: Schema.Codec<InstantExpr, InstantExpr, never, never>;
67
+ }>, Schema.TaggedStruct<"LessThanOrEqual", {
68
+ readonly value: Schema.Codec<InstantExpr, InstantExpr, never, never>;
69
+ }>]>;
70
+ }>, Schema.TaggedStruct<"DateRange", {
71
+ readonly lower: Schema.Union<readonly [Schema.TaggedStruct<"GreaterThan", {
72
+ readonly value: Schema.Codec<InstantExpr, InstantExpr, never, never>;
73
+ }>, Schema.TaggedStruct<"GreaterThanOrEqual", {
74
+ readonly value: Schema.Codec<InstantExpr, InstantExpr, never, never>;
75
+ }>]>;
76
+ readonly upper: Schema.optionalKey<Schema.Never>;
77
+ }>, Schema.TaggedStruct<"DateRange", {
78
+ readonly lower: Schema.optionalKey<Schema.Never>;
79
+ readonly upper: Schema.Union<readonly [Schema.TaggedStruct<"LessThan", {
80
+ readonly value: Schema.Codec<InstantExpr, InstantExpr, never, never>;
81
+ }>, Schema.TaggedStruct<"LessThanOrEqual", {
82
+ readonly value: Schema.Codec<InstantExpr, InstantExpr, never, never>;
83
+ }>]>;
84
+ }>]>;
85
+ type DateRangeExpr = typeof DateRangeExpr.Type;
86
+ //#endregion
87
+ export { DateLiteral, DateRangeExpr, GreaterThan, GreaterThanOrEqual, InstantExpr, IsoDate, LessThan, LessThanOrEqual, LowerBound, Now, Shift, StartOf, Unit, UpperBound, daysInMonth, isIsoDate };
@@ -0,0 +1,74 @@
1
+ import { Option, Schema, String } from "effect";
2
+ //#region src/ast/schemas.ts
3
+ const Unit = Schema.Literals([
4
+ "day",
5
+ "week",
6
+ "month",
7
+ "quarter",
8
+ "year"
9
+ ]);
10
+ const isLeapYear = (year) => year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
11
+ const daysInMonth = (year, month) => {
12
+ switch (month) {
13
+ case 2: return isLeapYear(year) ? 29 : 28;
14
+ case 4:
15
+ case 6:
16
+ case 9:
17
+ case 11: return 30;
18
+ default: return 31;
19
+ }
20
+ };
21
+ const isIsoDate = (value) => {
22
+ if (Option.isNone(String.match(/^\d{4}-\d{2}-\d{2}$/)(value))) return false;
23
+ const year = Number(value.slice(0, 4));
24
+ const month = Number(value.slice(5, 7));
25
+ const day = Number(value.slice(8, 10));
26
+ return month >= 1 && month <= 12 && day >= 1 && day <= daysInMonth(year, month);
27
+ };
28
+ const IsoDate = Schema.String.check(Schema.makeFilter(isIsoDate, { expected: "an ISO calendar date (YYYY-MM-DD)" })).annotate({ identifier: "IsoDate" });
29
+ const Now = Schema.TaggedStruct("Now", {});
30
+ const DateLiteral = Schema.TaggedStruct("DateLiteral", { value: IsoDate });
31
+ const ShiftAmount = Schema.Int.check(Schema.isBetween({
32
+ minimum: Number.MIN_SAFE_INTEGER,
33
+ maximum: Number.MAX_SAFE_INTEGER
34
+ }));
35
+ const Shift = Schema.TaggedStruct("Shift", {
36
+ base: Schema.suspend(() => InstantExpr),
37
+ amount: ShiftAmount,
38
+ unit: Unit
39
+ });
40
+ const StartOf = Schema.TaggedStruct("StartOf", {
41
+ base: Schema.suspend(() => InstantExpr),
42
+ unit: Unit
43
+ });
44
+ const InstantExpr = Schema.suspend(() => Schema.Union([
45
+ Now,
46
+ DateLiteral,
47
+ Shift,
48
+ StartOf
49
+ ])).annotate({ identifier: "InstantExpr" });
50
+ const GreaterThan = Schema.TaggedStruct("GreaterThan", { value: InstantExpr });
51
+ const GreaterThanOrEqual = Schema.TaggedStruct("GreaterThanOrEqual", { value: InstantExpr });
52
+ const LessThan = Schema.TaggedStruct("LessThan", { value: InstantExpr });
53
+ const LessThanOrEqual = Schema.TaggedStruct("LessThanOrEqual", { value: InstantExpr });
54
+ const LowerBound = Schema.Union([GreaterThan, GreaterThanOrEqual]);
55
+ const UpperBound = Schema.Union([LessThan, LessThanOrEqual]);
56
+ const BoundedDateRange = Schema.TaggedStruct("DateRange", {
57
+ lower: LowerBound,
58
+ upper: UpperBound
59
+ });
60
+ const LowerOpenDateRange = Schema.TaggedStruct("DateRange", {
61
+ lower: LowerBound,
62
+ upper: Schema.optionalKey(Schema.Never)
63
+ });
64
+ const UpperOpenDateRange = Schema.TaggedStruct("DateRange", {
65
+ lower: Schema.optionalKey(Schema.Never),
66
+ upper: UpperBound
67
+ });
68
+ const DateRangeExpr = Schema.Union([
69
+ BoundedDateRange,
70
+ LowerOpenDateRange,
71
+ UpperOpenDateRange
72
+ ]).annotate({ identifier: "DateRangeExpr" });
73
+ //#endregion
74
+ export { DateLiteral, DateRangeExpr, GreaterThan, GreaterThanOrEqual, InstantExpr, IsoDate, LessThan, LessThanOrEqual, LowerBound, Now, Shift, StartOf, Unit, UpperBound, daysInMonth, isIsoDate };