chronolizer 0.2.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 +169 -167
- package/dist/ast/constructors.d.mts +3 -8
- package/dist/ast/fold.d.mts +2 -1
- package/dist/ast/fold.mjs +38 -31
- package/dist/ast/normalize.mjs +4 -2
- package/dist/ast/schemas.d.mts +18 -25
- package/dist/ast/schemas.mjs +8 -17
- package/dist/filter/expression.mjs +3 -20
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/language/model.d.mts +33 -33
- package/dist/language/model.mjs +4 -7
- package/dist/language/registry.mjs +3 -21
- package/dist/locales/cs.mjs +70 -17
- package/dist/locales/de.mjs +237 -26
- package/dist/locales/en.mjs +69 -21
- package/dist/locales/es.mjs +325 -77
- package/dist/locales/fr.mjs +215 -51
- package/dist/locales/nl.mjs +189 -50
- package/dist/locales/pl.mjs +295 -121
- package/dist/locales/shared.mjs +129 -23
- package/dist/locales/tr.mjs +86 -21
- package/dist/natural/parse.d.mts +1 -1
- package/dist/natural/policy.mjs +2 -2
- package/dist/resolve/resolve.mjs +1 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,258 +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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
+
```
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
> [!NOTE]
|
|
12
|
+
> Chronolizer currently uses `effect@4.0.0-rc.112`. The API can change while Effect v4 has release-candidate status.
|
|
10
13
|
|
|
11
|
-
##
|
|
14
|
+
## Install Chronolizer
|
|
12
15
|
|
|
13
|
-
Chronolizer
|
|
16
|
+
Chronolizer requires Effect 4 and ESM. Install both packages as direct dependencies.
|
|
14
17
|
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
import { GermanLanguageLayer } from "chronolizer/locales/de";
|
|
18
|
+
```sh
|
|
19
|
+
pnpm add chronolizer effect@4.0.0-rc.112
|
|
18
20
|
```
|
|
19
21
|
|
|
20
|
-
|
|
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.
|
|
22
|
+
## Parse your first date range
|
|
25
23
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
- Effect Schema owns the date AST, filter data, results, and errors.
|
|
29
|
-
- Complete periods are half-open: `[start, next period start)`.
|
|
30
|
-
- Open ranges are supported.
|
|
31
|
-
- Weeks always start on Monday.
|
|
32
|
-
- Parsing does not read the clock or host time zone.
|
|
33
|
-
- Resolution requires `DateTime.CurrentTimeZone`.
|
|
34
|
-
- Natural rendering is canonical. It does not reproduce the source wording.
|
|
35
|
-
- Typo correction is conservative and optional. It does not change numbers, ISO dates, or short ambiguous words.
|
|
36
|
-
|
|
37
|
-
## Natural language to filter
|
|
24
|
+
English is available from the main package entry.
|
|
38
25
|
|
|
39
26
|
```ts
|
|
40
27
|
import { EnglishLanguageLayer, formatFilter, parseNatural } from "chronolizer";
|
|
41
28
|
import { Effect } from "effect";
|
|
42
29
|
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
return formatFilter(result.range);
|
|
50
|
-
}, Effect.provide(EnglishLanguageLayer));
|
|
51
|
-
|
|
52
|
-
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
|
+
);
|
|
53
35
|
|
|
36
|
+
Effect.runPromise(program);
|
|
54
37
|
// { gte: "now-1y/y", lt: "now-1y/y+1M" }
|
|
55
38
|
```
|
|
56
39
|
|
|
57
|
-
|
|
40
|
+
`parseNatural` parses the complete input. It does not extract a date range from a longer sentence.
|
|
58
41
|
|
|
59
|
-
|
|
60
|
-
year to date -> { gte: "now/y", lte: "now" }
|
|
61
|
-
January 2025 -> { gte: "2025-01-01", lt: "2025-02-01" }
|
|
62
|
-
since January 2025 -> { gte: "2025-01-01" }
|
|
63
|
-
before January 2025 -> { lt: "2025-01-01" }
|
|
64
|
-
through January 2025 -> { lt: "2025-02-01" }
|
|
65
|
-
last 3 months -> { gte: "now-3M", lte: "now" }
|
|
66
|
-
30 months ago -> { gte: "now-30M/M", lt: "now-30M/M+1M" }
|
|
67
|
-
01 January 2025 - 31 January 2025
|
|
68
|
-
-> { gte: "2025-01-01", lt: "2025-02-01" }
|
|
69
|
-
Q1 2025 -> { gte: "2025-01-01", lt: "2025-04-01" }
|
|
70
|
-
Januar letzten Jahres -> { gte: "now-1y/y", lt: "now-1y/y+1M" }
|
|
71
|
-
die letzten 3 Monate -> { gte: "now-3M", lte: "now" }
|
|
72
|
-
seit Jahresbeginn -> { gte: "now/y", lte: "now" }
|
|
73
|
-
seit Januar 2025 -> { gte: "2025-01-01" }
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
Chronolizer parses the complete input. It does not extract a date phrase from a larger sentence.
|
|
77
|
-
|
|
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.
|
|
42
|
+
## Common tasks
|
|
79
43
|
|
|
80
|
-
|
|
44
|
+
### Parse other languages
|
|
81
45
|
|
|
82
|
-
|
|
46
|
+
Import each non-English language from its own package entry. This keeps unused languages out of your bundle.
|
|
83
47
|
|
|
84
48
|
```ts
|
|
85
|
-
import {
|
|
49
|
+
import { formatFilter, parseNatural } from "chronolizer";
|
|
50
|
+
import { GermanLanguageLayer } from "chronolizer/locales/de";
|
|
86
51
|
import { Effect } from "effect";
|
|
87
52
|
|
|
88
|
-
const program =
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
+
);
|
|
92
58
|
|
|
93
|
-
|
|
59
|
+
Effect.runPromise(program);
|
|
60
|
+
// { gte: "now/y", lte: "now" }
|
|
94
61
|
```
|
|
95
62
|
|
|
96
|
-
|
|
63
|
+
Use `languagePluginsLayer` when one application needs more than one language:
|
|
97
64
|
|
|
98
|
-
|
|
65
|
+
```ts
|
|
66
|
+
import { EnglishLanguage, languagePluginsLayer, parseNatural } from "chronolizer";
|
|
67
|
+
import { GermanLanguage } from "chronolizer/locales/de";
|
|
68
|
+
import { PolishLanguage } from "chronolizer/locales/pl";
|
|
69
|
+
import { Effect } from "effect";
|
|
99
70
|
|
|
100
|
-
|
|
71
|
+
const Languages = languagePluginsLayer([EnglishLanguage, GermanLanguage, PolishLanguage]);
|
|
101
72
|
|
|
102
|
-
|
|
73
|
+
const program = parseNatural("letzten Monat", { locale: "de" }).pipe(Effect.provide(Languages));
|
|
103
74
|
|
|
104
|
-
|
|
105
|
-
const program = parseNatural("next 3 months", {
|
|
106
|
-
locale: "en",
|
|
107
|
-
allowFuture: false,
|
|
108
|
-
});
|
|
75
|
+
Effect.runPromise(program);
|
|
109
76
|
```
|
|
110
77
|
|
|
111
|
-
|
|
78
|
+
### Format a range as natural language
|
|
112
79
|
|
|
113
|
-
|
|
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.
|
|
114
81
|
|
|
115
82
|
```ts
|
|
116
83
|
import { formatNatural, parseFilter } from "chronolizer";
|
|
117
84
|
import { GermanLanguageLayer } from "chronolizer/locales/de";
|
|
118
85
|
import { Effect } from "effect";
|
|
119
86
|
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
return yield* formatNatural(range, { locale: "de" });
|
|
127
|
-
}, Effect.provide(GermanLanguageLayer));
|
|
128
|
-
|
|
129
|
-
const program = run();
|
|
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
|
+
);
|
|
130
92
|
|
|
93
|
+
Effect.runPromise(program);
|
|
131
94
|
// "Januar 2025"
|
|
132
95
|
```
|
|
133
96
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
## Validate external filters
|
|
97
|
+
### Add autocomplete
|
|
137
98
|
|
|
138
|
-
`
|
|
99
|
+
`suggestNatural` returns valid canonical phrases and their semantic ranges.
|
|
139
100
|
|
|
140
101
|
```ts
|
|
141
|
-
import {
|
|
142
|
-
import { Effect
|
|
102
|
+
import { EnglishLanguageLayer, suggestNatural } from "chronolizer";
|
|
103
|
+
import { Effect } from "effect";
|
|
143
104
|
|
|
144
|
-
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
|
+
);
|
|
145
109
|
|
|
146
|
-
|
|
110
|
+
Effect.runPromise(program);
|
|
111
|
+
// [{ text: "last month", range: ... }, ...]
|
|
147
112
|
```
|
|
148
113
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
## Compact expression syntax
|
|
114
|
+
The default limit is 10. The maximum limit is 100. A nonpositive or invalid limit returns no suggestions.
|
|
152
115
|
|
|
153
|
-
|
|
154
|
-
expression := anchor operation*
|
|
155
|
-
anchor := "now" | YYYY-MM-DD | YYYY-MM-DD "||"
|
|
156
|
-
operation := ("+" | "-") positiveInteger unit | "/" unit
|
|
157
|
-
unit := "d" | "w" | "M" | "q" | "y"
|
|
158
|
-
```
|
|
116
|
+
### Accept spelling errors
|
|
159
117
|
|
|
160
|
-
|
|
118
|
+
Set `typoMode` to `"tolerant"` for conservative correction:
|
|
161
119
|
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
2025-01-01||+1M
|
|
120
|
+
```ts
|
|
121
|
+
const program = parseNatural("januray of last yaer", {
|
|
122
|
+
locale: "en",
|
|
123
|
+
typoMode: "tolerant",
|
|
124
|
+
});
|
|
168
125
|
```
|
|
169
126
|
|
|
170
|
-
|
|
127
|
+
The successful value contains its `quality`, applied `corrections`, and any semantic `alternatives`. Strict mode is the default and does not correct the input.
|
|
128
|
+
|
|
129
|
+
### Exclude future relative ranges
|
|
171
130
|
|
|
172
|
-
|
|
131
|
+
Set `allowFuture` to `false` to reject explicit relative future ranges:
|
|
173
132
|
|
|
174
133
|
```ts
|
|
175
|
-
|
|
176
|
-
|
|
134
|
+
const program = parseNatural("next 3 months", {
|
|
135
|
+
locale: "en",
|
|
136
|
+
allowFuture: false,
|
|
137
|
+
});
|
|
138
|
+
```
|
|
177
139
|
|
|
178
|
-
|
|
179
|
-
const range = yield* parseFilter({ gte: "now/y", lte: "now" });
|
|
180
|
-
return yield* resolve(range);
|
|
181
|
-
}, DateTime.withCurrentZoneNamed("Europe/Berlin"));
|
|
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.
|
|
182
141
|
|
|
183
|
-
|
|
184
|
-
```
|
|
142
|
+
The option does not compare fixed dates, such as `January 2099`, with the current date. Parsing does not read the clock.
|
|
185
143
|
|
|
186
|
-
The
|
|
144
|
+
The same option is available in `suggestNatural`.
|
|
187
145
|
|
|
188
|
-
|
|
146
|
+
### Resolve a range to dates
|
|
189
147
|
|
|
190
|
-
|
|
191
|
-
const program = parseNatural("januray of last yaer", {
|
|
192
|
-
locale: "en",
|
|
193
|
-
typoMode: "tolerant",
|
|
194
|
-
});
|
|
195
|
-
```
|
|
148
|
+
Provide an Effect time zone when you resolve relative expressions:
|
|
196
149
|
|
|
197
|
-
|
|
150
|
+
```ts
|
|
151
|
+
import { parseFilter, resolve } from "chronolizer";
|
|
152
|
+
import { DateTime, Effect } from "effect";
|
|
198
153
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
154
|
+
const program = parseFilter({ gte: "now/y", lte: "now" }).pipe(
|
|
155
|
+
Effect.flatMap(resolve),
|
|
156
|
+
DateTime.withCurrentZoneNamed("Europe/Berlin"),
|
|
157
|
+
);
|
|
202
158
|
|
|
203
|
-
|
|
159
|
+
Effect.runPromise(program);
|
|
160
|
+
```
|
|
204
161
|
|
|
205
|
-
|
|
162
|
+
`resolve` uses the Effect clock and `DateTime.CurrentTimeZone`. It does not use the host time zone without your decision.
|
|
206
163
|
|
|
207
|
-
|
|
164
|
+
### Validate an external filter
|
|
208
165
|
|
|
209
|
-
|
|
166
|
+
Use the exported Effect Schema before you parse unknown data:
|
|
210
167
|
|
|
211
168
|
```ts
|
|
212
|
-
import {
|
|
213
|
-
import {
|
|
214
|
-
import { PolishLanguage } from "chronolizer/locales/pl";
|
|
215
|
-
import { Effect } from "effect";
|
|
169
|
+
import { DateFilter, parseFilter } from "chronolizer";
|
|
170
|
+
import { Effect, Schema } from "effect";
|
|
216
171
|
|
|
217
|
-
const
|
|
172
|
+
const program = Schema.decodeUnknownEffect(DateFilter)(externalInput).pipe(
|
|
173
|
+
Effect.flatMap(parseFilter),
|
|
174
|
+
);
|
|
218
175
|
|
|
219
|
-
|
|
220
|
-
locale: "de",
|
|
221
|
-
}).pipe(Effect.provide(Languages));
|
|
176
|
+
Effect.runPromise(program);
|
|
222
177
|
```
|
|
223
178
|
|
|
224
|
-
|
|
179
|
+
## Supported expressions
|
|
225
180
|
|
|
226
|
-
|
|
181
|
+
Supported input families include:
|
|
227
182
|
|
|
228
|
-
|
|
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.
|
|
229
193
|
|
|
230
|
-
|
|
231
|
-
- autocomplete phrase generation;
|
|
232
|
-
- optional text normalization;
|
|
233
|
-
- its typo-correction strategy, which can be disabled;
|
|
234
|
-
- vocabulary shared with registered language extensions.
|
|
194
|
+
Examples:
|
|
235
195
|
|
|
236
|
-
|
|
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.
|
|
237
232
|
|
|
238
|
-
|
|
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
|
+
```
|
|
239
239
|
|
|
240
|
-
|
|
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`.
|
|
241
241
|
|
|
242
|
-
-
|
|
243
|
-
- `GermanLanguage`
|
|
244
|
-
- `SpanishLanguage`
|
|
245
|
-
- `FrenchLanguage`
|
|
246
|
-
- `DutchLanguage`
|
|
247
|
-
- `TurkishLanguage`
|
|
248
|
-
- `CzechLanguage`
|
|
249
|
-
- `PolishLanguage`
|
|
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.
|
|
250
243
|
|
|
251
|
-
|
|
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.
|
|
252
245
|
|
|
253
|
-
|
|
246
|
+
## Main API
|
|
254
247
|
|
|
255
|
-
|
|
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 |
|
|
256
258
|
|
|
257
259
|
## License
|
|
258
260
|
|
|
@@ -1,12 +1,7 @@
|
|
|
1
|
-
import { InstantExpr, IsoDate, LowerBound, Shift, StartOf, Unit, UpperBound } from "./schemas.mjs";
|
|
1
|
+
import { DateLiteral, InstantExpr, IsoDate, LowerBound, Now, Shift, StartOf, Unit, UpperBound } from "./schemas.mjs";
|
|
2
2
|
//#region src/ast/constructors.d.ts
|
|
3
|
-
declare const now: () =>
|
|
4
|
-
|
|
5
|
-
};
|
|
6
|
-
declare const dateLiteral: (value: IsoDate) => {
|
|
7
|
-
readonly _tag: "DateLiteral";
|
|
8
|
-
readonly value: string;
|
|
9
|
-
};
|
|
3
|
+
declare const now: () => Now;
|
|
4
|
+
declare const dateLiteral: (value: IsoDate) => DateLiteral;
|
|
10
5
|
declare const shift: (base: InstantExpr, amount: number, unit: Unit) => Shift;
|
|
11
6
|
declare const startOf: (base: InstantExpr, unit: Unit) => StartOf;
|
|
12
7
|
declare const greaterThan: (value: InstantExpr) => {
|
package/dist/ast/fold.d.mts
CHANGED
|
@@ -8,5 +8,6 @@ interface InstantAlgebra<A> {
|
|
|
8
8
|
}
|
|
9
9
|
declare const foldInstant: <A>(expression: InstantExpr, algebra: InstantAlgebra<A>) => A;
|
|
10
10
|
declare const containsPositiveShift: (range: DateRangeExpr) => boolean;
|
|
11
|
+
declare const isCurrentPeriod: (range: DateRangeExpr) => boolean;
|
|
11
12
|
//#endregion
|
|
12
|
-
export { InstantAlgebra, containsPositiveShift, foldInstant };
|
|
13
|
+
export { InstantAlgebra, containsPositiveShift, foldInstant, isCurrentPeriod };
|
package/dist/ast/fold.mjs
CHANGED
|
@@ -1,36 +1,35 @@
|
|
|
1
|
+
import { DateLiteral, GreaterThanOrEqual, LessThan, Now, Shift, StartOf } from "./schemas.mjs";
|
|
2
|
+
import { Match, Schema, absurd } from "effect";
|
|
1
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);
|
|
2
10
|
const foldInstant = (expression, algebra) => {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
}
|
|
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);
|
|
33
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);
|
|
34
33
|
const containsPositiveShiftInstant = (expression) => {
|
|
35
34
|
const offset = foldInstant(expression, {
|
|
36
35
|
now: () => ({
|
|
@@ -47,5 +46,13 @@ const containsPositiveShiftInstant = (expression) => {
|
|
|
47
46
|
return offset.months > 0 || offset.days > 0;
|
|
48
47
|
};
|
|
49
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
|
+
};
|
|
50
57
|
//#endregion
|
|
51
|
-
export { containsPositiveShift, foldInstant };
|
|
58
|
+
export { containsPositiveShift, foldInstant, isCurrentPeriod };
|
package/dist/ast/normalize.mjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { Shift } from "./schemas.mjs";
|
|
1
2
|
import { boundedRange, dateLiteral, greaterThan, greaterThanOrEqual, lessThan, lessThanOrEqual, lowerOpenRange, now, shift, startOf, upperOpenRange } from "./constructors.mjs";
|
|
2
|
-
import { Match } from "effect";
|
|
3
|
+
import { Match, Schema } from "effect";
|
|
3
4
|
//#region src/ast/normalize.ts
|
|
5
|
+
const isShift = Schema.is(Shift);
|
|
4
6
|
const normalizeInstant = (expression) => Match.valueTags(expression, {
|
|
5
7
|
Now: () => now(),
|
|
6
8
|
DateLiteral: (literal) => dateLiteral(literal.value),
|
|
@@ -8,7 +10,7 @@ const normalizeInstant = (expression) => Match.valueTags(expression, {
|
|
|
8
10
|
Shift: (operation) => {
|
|
9
11
|
const base = normalizeInstant(operation.base);
|
|
10
12
|
if (operation.amount === 0) return base;
|
|
11
|
-
if (base
|
|
13
|
+
if (isShift(base) && base.unit === operation.unit) {
|
|
12
14
|
const amount = base.amount + operation.amount;
|
|
13
15
|
if (Number.isSafeInteger(amount)) return normalizeInstant(shift(base.base, amount, operation.unit));
|
|
14
16
|
}
|
package/dist/ast/schemas.d.mts
CHANGED
|
@@ -2,35 +2,28 @@ import { Schema } from "effect";
|
|
|
2
2
|
//#region src/ast/schemas.d.ts
|
|
3
3
|
declare const Unit: Schema.Literals<readonly ["day", "week", "month", "quarter", "year"]>;
|
|
4
4
|
type Unit = typeof Unit.Type;
|
|
5
|
-
declare const daysInMonth: (year: number, month: number) =>
|
|
5
|
+
declare const daysInMonth: (year: number, month: number) => number;
|
|
6
6
|
declare const isIsoDate: (value: string) => boolean;
|
|
7
7
|
declare const IsoDate: Schema.String;
|
|
8
8
|
type IsoDate = typeof IsoDate.Type;
|
|
9
|
-
|
|
10
|
-
|
|
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", {
|
|
9
|
+
declare const Now_base: Schema.Class<Now, Schema.TaggedStruct<"Now", {}>, {}>;
|
|
10
|
+
declare class Now extends Now_base {}
|
|
11
|
+
declare const DateLiteral_base: Schema.Class<DateLiteral, Schema.TaggedStruct<"DateLiteral", {
|
|
30
12
|
readonly value: Schema.String;
|
|
31
|
-
}>;
|
|
32
|
-
declare
|
|
33
|
-
declare const
|
|
13
|
+
}>, {}>;
|
|
14
|
+
declare class DateLiteral extends DateLiteral_base {}
|
|
15
|
+
declare const Shift_base: Schema.Class<Shift, Schema.TaggedStruct<"Shift", {
|
|
16
|
+
readonly base: Schema.suspend<Schema.Codec<InstantExpr, InstantExpr, never, never>>;
|
|
17
|
+
readonly amount: Schema.Int;
|
|
18
|
+
readonly unit: Schema.Literals<readonly ["day", "week", "month", "quarter", "year"]>;
|
|
19
|
+
}>, {}>;
|
|
20
|
+
declare class Shift extends Shift_base {}
|
|
21
|
+
declare const StartOf_base: Schema.Class<StartOf, Schema.TaggedStruct<"StartOf", {
|
|
22
|
+
readonly base: Schema.suspend<Schema.Codec<InstantExpr, InstantExpr, never, never>>;
|
|
23
|
+
readonly unit: Schema.Literals<readonly ["day", "week", "month", "quarter", "year"]>;
|
|
24
|
+
}>, {}>;
|
|
25
|
+
declare class StartOf extends StartOf_base {}
|
|
26
|
+
type InstantExpr = Now | DateLiteral | Shift | StartOf;
|
|
34
27
|
declare const InstantExpr: Schema.Codec<InstantExpr>;
|
|
35
28
|
declare const GreaterThan: Schema.TaggedStruct<"GreaterThan", {
|
|
36
29
|
readonly value: Schema.Codec<InstantExpr, InstantExpr, never, never>;
|