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.
- package/README.md +184 -119
- package/dist/ast/constructors.d.mts +135 -0
- package/dist/ast/constructors.mjs +25 -0
- package/dist/ast/fold.d.mts +13 -0
- package/dist/ast/fold.mjs +58 -0
- package/dist/ast/normalize.d.mts +42 -0
- package/dist/ast/normalize.mjs +34 -0
- package/dist/ast/schemas.d.mts +80 -0
- package/dist/ast/schemas.mjs +65 -0
- package/dist/filter/codec.d.mts +92 -0
- package/dist/filter/codec.mjs +48 -0
- package/dist/filter/errors.d.mts +14 -0
- package/dist/filter/errors.mjs +10 -0
- package/dist/filter/expression.d.mts +8 -0
- package/dist/filter/expression.mjs +76 -0
- package/dist/filter/schema.d.mts +13 -0
- package/dist/filter/schema.mjs +11 -0
- package/dist/filter/transformation.d.mts +37 -0
- package/dist/filter/transformation.mjs +20 -0
- package/dist/index.d.mts +22 -969
- package/dist/index.mjs +22 -2305
- package/dist/language/errors.d.mts +38 -0
- package/dist/language/errors.mjs +30 -0
- package/dist/language/model.d.mts +239 -0
- package/dist/language/model.mjs +50 -0
- package/dist/language/registry.d.mts +17 -0
- package/dist/language/registry.mjs +116 -0
- package/dist/locales/cs.d.mts +10 -0
- package/dist/locales/cs.mjs +746 -0
- package/dist/locales/de.d.mts +10 -0
- package/dist/locales/de.mjs +1039 -0
- package/dist/locales/en.d.mts +10 -0
- package/dist/locales/en.mjs +598 -0
- package/dist/locales/es.d.mts +10 -0
- package/dist/locales/es.mjs +908 -0
- package/dist/locales/fr.d.mts +10 -0
- package/dist/locales/fr.mjs +901 -0
- package/dist/locales/nl.d.mts +10 -0
- package/dist/locales/nl.mjs +791 -0
- package/dist/locales/pl.d.mts +10 -0
- package/dist/locales/pl.mjs +885 -0
- package/dist/locales/shared.mjs +395 -0
- package/dist/locales/tr.d.mts +10 -0
- package/dist/locales/tr.mjs +725 -0
- package/dist/natural/correction.d.mts +13 -0
- package/dist/natural/correction.mjs +73 -0
- package/dist/natural/format.d.mts +47 -0
- package/dist/natural/format.mjs +14 -0
- package/dist/natural/parse.d.mts +99 -0
- package/dist/natural/parse.mjs +66 -0
- package/dist/natural/policy.mjs +6 -0
- package/dist/natural/suggest.d.mts +53 -0
- package/dist/natural/suggest.mjs +24 -0
- package/dist/natural/suggestion.d.mts +4 -0
- package/dist/natural/suggestion.mjs +79 -0
- package/dist/natural/text.d.mts +5 -0
- package/dist/natural/text.mjs +5 -0
- package/dist/resolve/resolve.d.mts +79 -0
- package/dist/resolve/resolve.mjs +58 -0
- package/dist/resolve/schema.d.mts +59 -0
- package/dist/resolve/schema.mjs +28 -0
- package/package.json +15 -3
package/README.md
CHANGED
|
@@ -1,195 +1,260 @@
|
|
|
1
1
|
# Chronolizer
|
|
2
2
|
|
|
3
|
-
Chronolizer is an Effect
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
14
|
+
## Install Chronolizer
|
|
10
15
|
|
|
11
|
-
|
|
16
|
+
Chronolizer requires Effect 4 and ESM. Install both packages as direct dependencies.
|
|
12
17
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
##
|
|
22
|
+
## Parse your first date range
|
|
23
|
+
|
|
24
|
+
English is available from the main package entry.
|
|
23
25
|
|
|
24
26
|
```ts
|
|
25
|
-
import {
|
|
27
|
+
import { EnglishLanguageLayer, formatFilter, parseNatural } from "chronolizer";
|
|
26
28
|
import { Effect } from "effect";
|
|
27
29
|
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
40
|
+
`parseNatural` parses the complete input. It does not extract a date range from a longer sentence.
|
|
43
41
|
|
|
44
|
-
|
|
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
|
-
|
|
44
|
+
### Parse other languages
|
|
62
45
|
|
|
63
|
-
|
|
46
|
+
Import each non-English language from its own package entry. This keeps unused languages out of your bundle.
|
|
64
47
|
|
|
65
|
-
|
|
48
|
+
```ts
|
|
49
|
+
import { formatFilter, parseNatural } from "chronolizer";
|
|
50
|
+
import { GermanLanguageLayer } from "chronolizer/locales/de";
|
|
51
|
+
import { Effect } from "effect";
|
|
66
52
|
|
|
67
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
locale: "en",
|
|
72
|
-
allowFuture: false,
|
|
73
|
-
});
|
|
59
|
+
Effect.runPromise(program);
|
|
60
|
+
// { gte: "now/y", lte: "now" }
|
|
74
61
|
```
|
|
75
62
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
## Filter to natural language
|
|
63
|
+
Use `languagePluginsLayer` when one application needs more than one language:
|
|
79
64
|
|
|
80
65
|
```ts
|
|
81
|
-
import {
|
|
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
|
|
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
|
-
|
|
91
|
-
}, Effect.provide(DefaultLanguageLayer));
|
|
73
|
+
const program = parseNatural("letzten Monat", { locale: "de" }).pipe(Effect.provide(Languages));
|
|
92
74
|
|
|
93
|
-
|
|
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
|
-
|
|
97
|
+
### Add autocomplete
|
|
99
98
|
|
|
100
|
-
|
|
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 {
|
|
106
|
-
import { Effect
|
|
102
|
+
import { EnglishLanguageLayer, suggestNatural } from "chronolizer";
|
|
103
|
+
import { Effect } from "effect";
|
|
107
104
|
|
|
108
|
-
const
|
|
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
|
-
|
|
110
|
+
Effect.runPromise(program);
|
|
111
|
+
// [{ text: "last month", range: ... }, ...]
|
|
111
112
|
```
|
|
112
113
|
|
|
113
|
-
|
|
114
|
+
The default limit is 10. The maximum limit is 100. A nonpositive or invalid limit returns no suggestions.
|
|
114
115
|
|
|
115
|
-
|
|
116
|
+
### Accept spelling errors
|
|
116
117
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
154
|
+
const program = parseFilter({ gte: "now/y", lte: "now" }).pipe(
|
|
155
|
+
Effect.flatMap(resolve),
|
|
156
|
+
DateTime.withCurrentZoneNamed("Europe/Berlin"),
|
|
157
|
+
);
|
|
146
158
|
|
|
147
|
-
|
|
159
|
+
Effect.runPromise(program);
|
|
148
160
|
```
|
|
149
161
|
|
|
150
|
-
|
|
162
|
+
`resolve` uses the Effect clock and `DateTime.CurrentTimeZone`. It does not use the host time zone without your decision.
|
|
151
163
|
|
|
152
|
-
|
|
164
|
+
### Validate an external filter
|
|
165
|
+
|
|
166
|
+
Use the exported Effect Schema before you parse unknown data:
|
|
153
167
|
|
|
154
168
|
```ts
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
typoMode: "tolerant",
|
|
158
|
-
});
|
|
159
|
-
```
|
|
169
|
+
import { DateFilter, parseFilter } from "chronolizer";
|
|
170
|
+
import { Effect, Schema } from "effect";
|
|
160
171
|
|
|
161
|
-
|
|
172
|
+
const program = Schema.decodeUnknownEffect(DateFilter)(externalInput).pipe(
|
|
173
|
+
Effect.flatMap(parseFilter),
|
|
174
|
+
);
|
|
162
175
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
- semantic alternatives for equal-cost ties.
|
|
176
|
+
Effect.runPromise(program);
|
|
177
|
+
```
|
|
166
178
|
|
|
167
|
-
|
|
179
|
+
## Supported expressions
|
|
168
180
|
|
|
169
|
-
|
|
181
|
+
Supported input families include:
|
|
170
182
|
|
|
171
|
-
|
|
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
|
-
|
|
194
|
+
Examples:
|
|
174
195
|
|
|
175
|
-
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
-
|
|
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
|
-
`
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
246
|
+
## Main API
|
|
191
247
|
|
|
192
|
-
|
|
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 };
|