chronolizer 0.1.0 → 0.3.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 +184 -119
  2. package/dist/ast/constructors.d.mts +135 -0
  3. package/dist/ast/constructors.mjs +25 -0
  4. package/dist/ast/fold.d.mts +13 -0
  5. package/dist/ast/fold.mjs +58 -0
  6. package/dist/ast/normalize.d.mts +42 -0
  7. package/dist/ast/normalize.mjs +34 -0
  8. package/dist/ast/schemas.d.mts +80 -0
  9. package/dist/ast/schemas.mjs +65 -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 +76 -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 +50 -0
  26. package/dist/language/registry.d.mts +17 -0
  27. package/dist/language/registry.mjs +116 -0
  28. package/dist/locales/cs.d.mts +10 -0
  29. package/dist/locales/cs.mjs +746 -0
  30. package/dist/locales/de.d.mts +10 -0
  31. package/dist/locales/de.mjs +1039 -0
  32. package/dist/locales/en.d.mts +10 -0
  33. package/dist/locales/en.mjs +598 -0
  34. package/dist/locales/es.d.mts +10 -0
  35. package/dist/locales/es.mjs +908 -0
  36. package/dist/locales/fr.d.mts +10 -0
  37. package/dist/locales/fr.mjs +901 -0
  38. package/dist/locales/nl.d.mts +10 -0
  39. package/dist/locales/nl.mjs +791 -0
  40. package/dist/locales/pl.d.mts +10 -0
  41. package/dist/locales/pl.mjs +885 -0
  42. package/dist/locales/shared.mjs +395 -0
  43. package/dist/locales/tr.d.mts +10 -0
  44. package/dist/locales/tr.mjs +725 -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 +58 -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
@@ -1,195 +1,260 @@
1
1
  # Chronolizer
2
2
 
3
- Chronolizer is an Effect v4 library for bidirectional natural-language date ranges.
3
+ Chronolizer is an Effect 4 library that converts natural-language date ranges to compact date-math filters and back. It supports open ranges, autocomplete, typo correction, and eight languages.
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
+ ```text
6
+ year to date → { gte: "now/y", lte: "now" }
7
+ January of last year → { gte: "now-1y/y", lt: "now-1y/y+1M" }
8
+ since January 2025 → { gte: "2025-01-01" }
9
+ ```
6
10
 
7
- ## Status
11
+ > [!NOTE]
12
+ > Chronolizer currently uses `effect@4.0.0-rc.112`. The API can change while Effect v4 has release-candidate status.
8
13
 
9
- This package uses `effect@4.0.0-rc.112`. Its API can change while Effect v4 is in release-candidate status.
14
+ ## Install Chronolizer
10
15
 
11
- ## Main concepts
16
+ Chronolizer requires Effect 4 and ESM. Install both packages as direct dependencies.
12
17
 
13
- - Effect Schema owns the date AST, filter data, results, and errors.
14
- - Complete periods are half-open: `[start, next period start)`.
15
- - Open ranges are supported.
16
- - Weeks always start on Monday.
17
- - Parsing does not read the clock or host time zone.
18
- - Resolution requires `DateTime.CurrentTimeZone`.
19
- - Natural rendering is canonical. It does not reproduce the source wording.
20
- - Typo correction is conservative and optional. It does not change numbers, ISO dates, or short ambiguous words.
18
+ ```sh
19
+ pnpm add chronolizer effect@4.0.0-rc.112
20
+ ```
21
21
 
22
- ## Natural language to filter
22
+ ## Parse your first date range
23
+
24
+ English is available from the main package entry.
23
25
 
24
26
  ```ts
25
- import { DefaultLanguageLayer, formatFilter, parseNatural } from "chronolizer";
27
+ import { EnglishLanguageLayer, formatFilter, parseNatural } from "chronolizer";
26
28
  import { Effect } from "effect";
27
29
 
28
- const run = Effect.fn(function* () {
29
- const result = yield* parseNatural("January of last year", {
30
- locale: "en",
31
- typoMode: "strict",
32
- });
33
-
34
- return formatFilter(result.range);
35
- }, Effect.provide(DefaultLanguageLayer));
36
-
37
- const program = run();
30
+ const program = parseNatural("January of last year", { locale: "en" }).pipe(
31
+ Effect.map(({ range }) => formatFilter(range)),
32
+ Effect.tap((filter) => Effect.log("Date filter", filter)),
33
+ Effect.provide(EnglishLanguageLayer),
34
+ );
38
35
 
36
+ Effect.runPromise(program);
39
37
  // { gte: "now-1y/y", lt: "now-1y/y+1M" }
40
38
  ```
41
39
 
42
- Other examples:
40
+ `parseNatural` parses the complete input. It does not extract a date range from a longer sentence.
43
41
 
44
- ```text
45
- year to date -> { gte: "now/y", lte: "now" }
46
- January 2025 -> { gte: "2025-01-01", lt: "2025-02-01" }
47
- since January 2025 -> { gte: "2025-01-01" }
48
- before January 2025 -> { lt: "2025-01-01" }
49
- through January 2025 -> { lt: "2025-02-01" }
50
- last 3 months -> { gte: "now-3M", lte: "now" }
51
- 30 months ago -> { gte: "now-30M/M", lt: "now-30M/M+1M" }
52
- 01 January 2025 - 31 January 2025
53
- -> { gte: "2025-01-01", lt: "2025-02-01" }
54
- Q1 2025 -> { gte: "2025-01-01", lt: "2025-04-01" }
55
- Januar letzten Jahres -> { gte: "now-1y/y", lt: "now-1y/y+1M" }
56
- die letzten 3 Monate -> { gte: "now-3M", lte: "now" }
57
- seit Jahresbeginn -> { gte: "now/y", lte: "now" }
58
- seit Januar 2025 -> { gte: "2025-01-01" }
59
- ```
42
+ ## Common tasks
60
43
 
61
- Chronolizer parses the complete input. It does not extract a date phrase from a larger sentence.
44
+ ### Parse other languages
62
45
 
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.
46
+ Import each non-English language from its own package entry. This keeps unused languages out of your bundle.
64
47
 
65
- ### Exclude positive relative ranges
48
+ ```ts
49
+ import { formatFilter, parseNatural } from "chronolizer";
50
+ import { GermanLanguageLayer } from "chronolizer/locales/de";
51
+ import { Effect } from "effect";
66
52
 
67
- Set `allowFuture: false` to reject expressions whose relative range extends after `now`:
53
+ const program = parseNatural("seit Jahresbeginn", { locale: "de" }).pipe(
54
+ Effect.map(({ range }) => formatFilter(range)),
55
+ Effect.tap((filter) => Effect.log("Datumsfilter", filter)),
56
+ Effect.provide(GermanLanguageLayer),
57
+ );
68
58
 
69
- ```ts
70
- const program = parseNatural("next 3 months", {
71
- locale: "en",
72
- allowFuture: false,
73
- });
59
+ Effect.runPromise(program);
60
+ // { gte: "now/y", lte: "now" }
74
61
  ```
75
62
 
76
- This option rejects relative forms such as `next month`, `this year`, and `in 3 years`. It does not classify fixed dates such as `January 2099`, because parsing does not read the clock.
77
-
78
- ## Filter to natural language
63
+ Use `languagePluginsLayer` when one application needs more than one language:
79
64
 
80
65
  ```ts
81
- import { DefaultLanguageLayer, formatNatural, parseFilter } from "chronolizer";
66
+ import { EnglishLanguage, languagePluginsLayer, parseNatural } from "chronolizer";
67
+ import { GermanLanguage } from "chronolizer/locales/de";
68
+ import { PolishLanguage } from "chronolizer/locales/pl";
82
69
  import { Effect } from "effect";
83
70
 
84
- const run = Effect.fn(function* () {
85
- const range = yield* parseFilter({
86
- gte: "2025-01-01",
87
- lt: "2025-02-01",
88
- });
71
+ const Languages = languagePluginsLayer([EnglishLanguage, GermanLanguage, PolishLanguage]);
89
72
 
90
- return yield* formatNatural(range, { locale: "de" });
91
- }, Effect.provide(DefaultLanguageLayer));
73
+ const program = parseNatural("letzten Monat", { locale: "de" }).pipe(Effect.provide(Languages));
92
74
 
93
- const program = run();
75
+ Effect.runPromise(program);
76
+ ```
94
77
 
78
+ ### Format a range as natural language
79
+
80
+ `formatNatural` returns the canonical phrase for a range. It preserves the meaning, but it does not reproduce the original wording. Absolute days use the locale's numeric `Intl.DateTimeFormat` form.
81
+
82
+ ```ts
83
+ import { formatNatural, parseFilter } from "chronolizer";
84
+ import { GermanLanguageLayer } from "chronolizer/locales/de";
85
+ import { Effect } from "effect";
86
+
87
+ const program = parseFilter({ gte: "2025-01-01", lt: "2025-02-01" }).pipe(
88
+ Effect.flatMap((range) => formatNatural(range, { locale: "de" })),
89
+ Effect.tap((text) => Effect.log("Date range", text)),
90
+ Effect.provide(GermanLanguageLayer),
91
+ );
92
+
93
+ Effect.runPromise(program);
95
94
  // "Januar 2025"
96
95
  ```
97
96
 
98
- Natural language is many-to-one. Chronolizer therefore guarantees semantic round trips, not the original words.
97
+ ### Add autocomplete
99
98
 
100
- ## Validate external filters
101
-
102
- `parseFilter` accepts a validated `DateFilter`. Decode external data with the exported Schema first:
99
+ `suggestNatural` returns valid canonical phrases and their semantic ranges.
103
100
 
104
101
  ```ts
105
- import { DateFilter, parseFilter } from "chronolizer";
106
- import { Effect, Schema } from "effect";
102
+ import { EnglishLanguageLayer, suggestNatural } from "chronolizer";
103
+ import { Effect } from "effect";
107
104
 
108
- const decodeDateFilter = Schema.decodeUnknownEffect(DateFilter);
105
+ const program = suggestNatural("last m", { locale: "en", limit: 5 }).pipe(
106
+ Effect.tap((suggestions) => Effect.log("Suggestions", suggestions)),
107
+ Effect.provide(EnglishLanguageLayer),
108
+ );
109
109
 
110
- const program = decodeDateFilter(externalInput).pipe(Effect.flatMap(parseFilter));
110
+ Effect.runPromise(program);
111
+ // [{ text: "last month", range: ... }, ...]
111
112
  ```
112
113
 
113
- A filter has at most one lower bound (`gt` or `gte`), at most one upper bound (`lt` or `lte`), and at least one bound.
114
+ The default limit is 10. The maximum limit is 100. A nonpositive or invalid limit returns no suggestions.
114
115
 
115
- ## Compact expression syntax
116
+ ### Accept spelling errors
116
117
 
117
- ```text
118
- expression := anchor operation*
119
- anchor := "now" | YYYY-MM-DD | YYYY-MM-DD "||"
120
- operation := ("+" | "-") positiveInteger unit | "/" unit
121
- unit := "d" | "w" | "M" | "q" | "y"
118
+ Set `typoMode` to `"tolerant"` for conservative correction:
119
+
120
+ ```ts
121
+ const program = parseNatural("januray of last yaer", {
122
+ locale: "en",
123
+ typoMode: "tolerant",
124
+ });
122
125
  ```
123
126
 
124
- Examples:
127
+ The successful value contains its `quality`, applied `corrections`, and any semantic `alternatives`. Strict mode is the default and does not correct the input.
125
128
 
126
- ```text
127
- now/y
128
- now-1y/y
129
- now-1y/y+1M
130
- 2025-01-01
131
- 2025-01-01||+1M
129
+ ### Exclude future relative ranges
130
+
131
+ Set `allowFuture` to `false` to reject explicit relative future ranges:
132
+
133
+ ```ts
134
+ const program = parseNatural("next 3 months", {
135
+ locale: "en",
136
+ allowFuture: false,
137
+ });
132
138
  ```
133
139
 
134
- Operations run from left to right. `/unit` floors to the start of the calendar unit. Fixed dates require `||` before operations.
140
+ This option rejects forms such as `next month`, `next 3 weeks`, and `in 3 years`. It keeps complete current calendar periods such as `today`, `this week`, and `this month`. It does not change them to period-to-date ranges.
141
+
142
+ The option does not compare fixed dates, such as `January 2099`, with the current date. Parsing does not read the clock.
143
+
144
+ The same option is available in `suggestNatural`.
145
+
146
+ ### Resolve a range to dates
135
147
 
136
- ## Resolve with an explicit time zone
148
+ Provide an Effect time zone when you resolve relative expressions:
137
149
 
138
150
  ```ts
139
151
  import { parseFilter, resolve } from "chronolizer";
140
152
  import { DateTime, Effect } from "effect";
141
153
 
142
- const run = Effect.fn(function* () {
143
- const range = yield* parseFilter({ gte: "now/y", lte: "now" });
144
- return yield* resolve(range);
145
- }, DateTime.withCurrentZoneNamed("Europe/Berlin"));
154
+ const program = parseFilter({ gte: "now/y", lte: "now" }).pipe(
155
+ Effect.flatMap(resolve),
156
+ DateTime.withCurrentZoneNamed("Europe/Berlin"),
157
+ );
146
158
 
147
- const program = run();
159
+ Effect.runPromise(program);
148
160
  ```
149
161
 
150
- The resolver uses Effect Clock and `DateTime.CurrentTimeZone`. It never uses the host local time zone without an explicit caller decision. Named zones use the runtime ICU time-zone data.
162
+ `resolve` uses the Effect clock and `DateTime.CurrentTimeZone`. It does not use the host time zone without your decision.
151
163
 
152
- ## Tolerant parsing
164
+ ### Validate an external filter
165
+
166
+ Use the exported Effect Schema before you parse unknown data:
153
167
 
154
168
  ```ts
155
- const program = parseNatural("januray of last yaer", {
156
- locale: "en",
157
- typoMode: "tolerant",
158
- });
159
- ```
169
+ import { DateFilter, parseFilter } from "chronolizer";
170
+ import { Effect, Schema } from "effect";
160
171
 
161
- The result reports:
172
+ const program = Schema.decodeUnknownEffect(DateFilter)(externalInput).pipe(
173
+ Effect.flatMap(parseFilter),
174
+ );
162
175
 
163
- - `quality`: `exact`, `corrected`, or `ambiguous`;
164
- - each correction and edit distance;
165
- - semantic alternatives for equal-cost ties.
176
+ Effect.runPromise(program);
177
+ ```
166
178
 
167
- Strict mode never runs correction.
179
+ ## Supported expressions
168
180
 
169
- ## Language plugins
181
+ Supported input families include:
170
182
 
171
- `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.
183
+ - calendar days, weeks, months, quarters, and years;
184
+ - rolling ranges, such as `last 3 months`;
185
+ - calendar offsets, such as `30 months ago`;
186
+ - period-to-date ranges, such as `year to date`;
187
+ - fixed months, quarters, years, and explicit date intervals;
188
+ - named days with or without a year, such as `January 12` and `12th of January`;
189
+ - open ranges, such as `since January 2025` and `from January 12`;
190
+ - compositional boundaries, such as `the day before January 12`;
191
+ - period starts, period ends, and weekends;
192
+ - abbreviated month names and common equivalent phrases.
172
193
 
173
- 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`.
194
+ Examples:
174
195
 
175
- Each base language owns:
196
+ | Input | Filter |
197
+ | ----------------------------------- | ------------------------------------------ |
198
+ | `today` | `{ gte: "now/d", lt: "now/d+1d" }` |
199
+ | `last 3 months` | `{ gte: "now-3M", lte: "now" }` |
200
+ | `30 months ago` | `{ gte: "now-30M/M", lt: "now-30M/M+1M" }` |
201
+ | `January 2025` | `{ gte: "2025-01-01", lt: "2025-02-01" }` |
202
+ | `January 12` | `{ gte: "now/y+11d", lt: "now/y+12d" }` |
203
+ | `Q1 2025` | `{ gte: "2025-01-01", lt: "2025-04-01" }` |
204
+ | `01 January 2025 - 31 January 2025` | `{ gte: "2025-01-01", lt: "2025-02-01" }` |
205
+ | `through January 2025` | `{ lt: "2025-02-01" }` |
206
+
207
+ ## Supported languages
208
+
209
+ | Language | Locale | Import |
210
+ | -------- | ------ | ----------------------------------------- |
211
+ | English | `en` | `chronolizer` or `chronolizer/locales/en` |
212
+ | German | `de` | `chronolizer/locales/de` |
213
+ | Spanish | `es` | `chronolizer/locales/es` |
214
+ | French | `fr` | `chronolizer/locales/fr` |
215
+ | Dutch | `nl` | `chronolizer/locales/nl` |
216
+ | Turkish | `tr` | `chronolizer/locales/tr` |
217
+ | Czech | `cs` | `chronolizer/locales/cs` |
218
+ | Polish | `pl` | `chronolizer/locales/pl` |
219
+
220
+ ## Date filter reference
221
+
222
+ A date filter has at least one bound. It can have one lower bound and one upper bound.
223
+
224
+ | Key | Meaning |
225
+ | ----- | --------------------------- |
226
+ | `gt` | After the value, exclusive |
227
+ | `gte` | On or after the value |
228
+ | `lt` | Before the value, exclusive |
229
+ | `lte` | On or before the value |
230
+
231
+ An expression starts with `now` or an ISO date. Operations run from left to right.
176
232
 
177
- - exact parsing and canonical rendering;
178
- - optional text normalization;
179
- - its typo-correction strategy, which can be disabled;
180
- - vocabulary shared with registered language extensions.
233
+ ```text
234
+ expression := anchor operation*
235
+ anchor := "now" | YYYY-MM-DD | YYYY-MM-DD "||"
236
+ operation := ("+" | "-") positiveInteger unit | "/" unit
237
+ unit := "d" | "w" | "M" | "q" | "y"
238
+ ```
181
239
 
182
- `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.
240
+ `/unit` moves a value to the start of its calendar unit. Add `||` before operations on a fixed date, for example `2025-01-01||+1M`.
183
241
 
184
- Built-in plugins:
242
+ Complete calendar periods use a half-open interval: the lower bound is inclusive and the next period start is exclusive. Weeks start on Monday.
185
243
 
186
- - `EnglishLanguage`
187
- - `GermanLanguage`
188
- - `DefaultLanguageLayer`
244
+ A named day without a year uses the current calendar year. Write the year for February 29 because parsing does not read the clock.
189
245
 
190
- Chinese and Japanese language packs are not included yet.
246
+ ## Main API
191
247
 
192
- Business calendars, holidays, times of day, recurrence, and sentence extraction are outside v1. A future business-day feature will use an injected calendar service.
248
+ | Export | Purpose |
249
+ | --------------------- | ------------------------------------------------------------------- |
250
+ | `parseNatural` | Parse complete natural-language input to a semantic range |
251
+ | `formatNatural` | Render a supported range as a canonical phrase |
252
+ | `suggestNatural` | Return autocomplete suggestions and semantic ranges |
253
+ | `parseFilter` | Parse a date filter to a semantic range |
254
+ | `formatFilter` | Format a semantic range as a date filter |
255
+ | `resolve` | Resolve a semantic range with an Effect clock and time zone |
256
+ | `DateFilter` | Validate external date-filter data with Effect Schema |
257
+ | `DateRangeFromFilter` | Decode and encode a filter through one Effect Schema transformation |
193
258
 
194
259
  ## License
195
260
 
@@ -0,0 +1,135 @@
1
+ import { DateLiteral, InstantExpr, IsoDate, LowerBound, Now, Shift, StartOf, Unit, UpperBound } from "./schemas.mjs";
2
+ //#region src/ast/constructors.d.ts
3
+ declare const now: () => Now;
4
+ declare const dateLiteral: (value: IsoDate) => DateLiteral;
5
+ declare const shift: (base: InstantExpr, amount: number, unit: Unit) => Shift;
6
+ declare const startOf: (base: InstantExpr, unit: Unit) => StartOf;
7
+ declare const greaterThan: (value: InstantExpr) => {
8
+ readonly _tag: "GreaterThan";
9
+ readonly value: InstantExpr;
10
+ };
11
+ declare const greaterThanOrEqual: (value: InstantExpr) => {
12
+ readonly _tag: "GreaterThanOrEqual";
13
+ readonly value: InstantExpr;
14
+ };
15
+ declare const lessThan: (value: InstantExpr) => {
16
+ readonly _tag: "LessThan";
17
+ readonly value: InstantExpr;
18
+ };
19
+ declare const lessThanOrEqual: (value: InstantExpr) => {
20
+ readonly _tag: "LessThanOrEqual";
21
+ readonly value: InstantExpr;
22
+ };
23
+ declare const boundedRange: (lower: LowerBound, upper: UpperBound) => {
24
+ readonly _tag: "DateRange";
25
+ readonly lower: {
26
+ readonly _tag: "GreaterThan";
27
+ readonly value: InstantExpr;
28
+ } | {
29
+ readonly _tag: "GreaterThanOrEqual";
30
+ readonly value: InstantExpr;
31
+ };
32
+ readonly upper: {
33
+ readonly _tag: "LessThan";
34
+ readonly value: InstantExpr;
35
+ } | {
36
+ readonly _tag: "LessThanOrEqual";
37
+ readonly value: InstantExpr;
38
+ };
39
+ } | {
40
+ readonly _tag: "DateRange";
41
+ readonly lower: {
42
+ readonly _tag: "GreaterThan";
43
+ readonly value: InstantExpr;
44
+ } | {
45
+ readonly _tag: "GreaterThanOrEqual";
46
+ readonly value: InstantExpr;
47
+ };
48
+ readonly upper?: never;
49
+ } | {
50
+ readonly _tag: "DateRange";
51
+ readonly lower?: never;
52
+ readonly upper: {
53
+ readonly _tag: "LessThan";
54
+ readonly value: InstantExpr;
55
+ } | {
56
+ readonly _tag: "LessThanOrEqual";
57
+ readonly value: InstantExpr;
58
+ };
59
+ };
60
+ declare const lowerOpenRange: (lower: LowerBound) => {
61
+ readonly _tag: "DateRange";
62
+ readonly lower: {
63
+ readonly _tag: "GreaterThan";
64
+ readonly value: InstantExpr;
65
+ } | {
66
+ readonly _tag: "GreaterThanOrEqual";
67
+ readonly value: InstantExpr;
68
+ };
69
+ readonly upper: {
70
+ readonly _tag: "LessThan";
71
+ readonly value: InstantExpr;
72
+ } | {
73
+ readonly _tag: "LessThanOrEqual";
74
+ readonly value: InstantExpr;
75
+ };
76
+ } | {
77
+ readonly _tag: "DateRange";
78
+ readonly lower: {
79
+ readonly _tag: "GreaterThan";
80
+ readonly value: InstantExpr;
81
+ } | {
82
+ readonly _tag: "GreaterThanOrEqual";
83
+ readonly value: InstantExpr;
84
+ };
85
+ readonly upper?: never;
86
+ } | {
87
+ readonly _tag: "DateRange";
88
+ readonly lower?: never;
89
+ readonly upper: {
90
+ readonly _tag: "LessThan";
91
+ readonly value: InstantExpr;
92
+ } | {
93
+ readonly _tag: "LessThanOrEqual";
94
+ readonly value: InstantExpr;
95
+ };
96
+ };
97
+ declare const upperOpenRange: (upper: UpperBound) => {
98
+ readonly _tag: "DateRange";
99
+ readonly lower: {
100
+ readonly _tag: "GreaterThan";
101
+ readonly value: InstantExpr;
102
+ } | {
103
+ readonly _tag: "GreaterThanOrEqual";
104
+ readonly value: InstantExpr;
105
+ };
106
+ readonly upper: {
107
+ readonly _tag: "LessThan";
108
+ readonly value: InstantExpr;
109
+ } | {
110
+ readonly _tag: "LessThanOrEqual";
111
+ readonly value: InstantExpr;
112
+ };
113
+ } | {
114
+ readonly _tag: "DateRange";
115
+ readonly lower: {
116
+ readonly _tag: "GreaterThan";
117
+ readonly value: InstantExpr;
118
+ } | {
119
+ readonly _tag: "GreaterThanOrEqual";
120
+ readonly value: InstantExpr;
121
+ };
122
+ readonly upper?: never;
123
+ } | {
124
+ readonly _tag: "DateRange";
125
+ readonly lower?: never;
126
+ readonly upper: {
127
+ readonly _tag: "LessThan";
128
+ readonly value: InstantExpr;
129
+ } | {
130
+ readonly _tag: "LessThanOrEqual";
131
+ readonly value: InstantExpr;
132
+ };
133
+ };
134
+ //#endregion
135
+ 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,13 @@
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
+ declare const isCurrentPeriod: (range: DateRangeExpr) => boolean;
12
+ //#endregion
13
+ export { InstantAlgebra, containsPositiveShift, foldInstant, isCurrentPeriod };
@@ -0,0 +1,58 @@
1
+ import { DateLiteral, GreaterThanOrEqual, LessThan, Now, Shift, StartOf } from "./schemas.mjs";
2
+ import { Match, Schema, absurd } from "effect";
3
+ //#region src/ast/fold.ts
4
+ const isDateLiteral = Schema.is(DateLiteral);
5
+ const isGreaterThanOrEqual = Schema.is(GreaterThanOrEqual);
6
+ const isLessThan = Schema.is(LessThan);
7
+ const isNow = Schema.is(Now);
8
+ const isShift = Schema.is(Shift);
9
+ 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
+ };
17
+ const shiftOffset = (base, amount, unit) => Match.value(unit).pipe(Match.when("year", () => ({
18
+ ...base,
19
+ months: base.months + amount * 12
20
+ })), Match.when("quarter", () => ({
21
+ ...base,
22
+ months: base.months + amount * 3
23
+ })), Match.when("month", () => ({
24
+ ...base,
25
+ months: base.months + amount
26
+ })), Match.when("week", () => ({
27
+ ...base,
28
+ days: base.days + amount * 7
29
+ })), Match.when("day", () => ({
30
+ ...base,
31
+ days: base.days + amount
32
+ })), Match.exhaustive);
33
+ const containsPositiveShiftInstant = (expression) => {
34
+ const offset = foldInstant(expression, {
35
+ now: () => ({
36
+ months: 0,
37
+ days: 0
38
+ }),
39
+ dateLiteral: () => ({
40
+ months: 0,
41
+ days: 0
42
+ }),
43
+ shift: shiftOffset,
44
+ startOf: (base) => base
45
+ });
46
+ return offset.months > 0 || offset.days > 0;
47
+ };
48
+ const containsPositiveShift = (range) => range.lower !== void 0 && containsPositiveShiftInstant(range.lower.value) || range.upper !== void 0 && containsPositiveShiftInstant(range.upper.value);
49
+ const isCurrentPeriod = (range) => {
50
+ if (!isGreaterThanOrEqual(range.lower) || !isLessThan(range.upper)) return false;
51
+ const start = range.lower.value;
52
+ const end = range.upper.value;
53
+ if (!isStartOf(start) || !isNow(start.base) || !isShift(end)) return false;
54
+ if (end.amount !== 1 || !isStartOf(end.base) || !isNow(end.base.base)) return false;
55
+ return start.unit === end.unit && start.unit === end.base.unit;
56
+ };
57
+ //#endregion
58
+ export { containsPositiveShift, foldInstant, isCurrentPeriod };
@@ -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 };