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 CHANGED
@@ -1,258 +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, 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
-
7
- ## Status
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
- This package uses `effect@4.0.0-rc.112`. Its API can change while Effect v4 is in release-candidate status.
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
- ## Package entry points
14
+ ## Install Chronolizer
12
15
 
13
- Chronolizer is side-effect free and publishes separate ESM entry points:
16
+ Chronolizer requires Effect 4 and ESM. Install both packages as direct dependencies.
14
17
 
15
- ```ts
16
- import { formatNatural, parseFilter, parseNatural, resolve, suggestNatural } from "chronolizer";
17
- import { GermanLanguageLayer } from "chronolizer/locales/de";
18
+ ```sh
19
+ pnpm add chronolizer effect@4.0.0-rc.112
18
20
  ```
19
21
 
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.
22
+ ## Parse your first date range
25
23
 
26
- ## Main concepts
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 run = Effect.fn(function* () {
44
- const result = yield* parseNatural("January of last year", {
45
- locale: "en",
46
- typoMode: "strict",
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
- Other examples:
40
+ `parseNatural` parses the complete input. It does not extract a date range from a longer sentence.
58
41
 
59
- ```text
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
- ## Autocomplete natural language
44
+ ### Parse other languages
81
45
 
82
- Use `suggestNatural()` for autocomplete fields. Each Schema-owned suggestion contains canonical display text and the semantic range that it represents:
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 { EnglishLanguageLayer, suggestNatural } from "chronolizer";
49
+ import { formatFilter, parseNatural } from "chronolizer";
50
+ import { GermanLanguageLayer } from "chronolizer/locales/de";
86
51
  import { Effect } from "effect";
87
52
 
88
- const program = suggestNatural("last m", {
89
- locale: "en",
90
- limit: 5,
91
- }).pipe(Effect.provide(EnglishLanguageLayer));
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
- // [{ text: "last month", range: ... }]
59
+ Effect.runPromise(program);
60
+ // { gte: "now/y", lte: "now" }
94
61
  ```
95
62
 
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()`.
63
+ Use `languagePluginsLayer` when one application needs more than one language:
97
64
 
98
- The default limit is 10. The maximum accepted limit is 100. A nonpositive or invalid limit returns no suggestions.
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
- ### Exclude positive relative ranges
71
+ const Languages = languagePluginsLayer([EnglishLanguage, GermanLanguage, PolishLanguage]);
101
72
 
102
- Set `allowFuture: false` to reject expressions whose relative range extends after `now`:
73
+ const program = parseNatural("letzten Monat", { locale: "de" }).pipe(Effect.provide(Languages));
103
74
 
104
- ```ts
105
- const program = parseNatural("next 3 months", {
106
- locale: "en",
107
- allowFuture: false,
108
- });
75
+ Effect.runPromise(program);
109
76
  ```
110
77
 
111
- 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.
78
+ ### Format a range as natural language
112
79
 
113
- ## Filter to natural language
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 run = Effect.fn(function* () {
121
- const range = yield* parseFilter({
122
- gte: "2025-01-01",
123
- lt: "2025-02-01",
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
- Natural language is many-to-one. Chronolizer therefore guarantees semantic round trips, not the original words.
135
-
136
- ## Validate external filters
97
+ ### Add autocomplete
137
98
 
138
- `parseFilter` accepts a validated `DateFilter`. Decode external data with the exported Schema first:
99
+ `suggestNatural` returns valid canonical phrases and their semantic ranges.
139
100
 
140
101
  ```ts
141
- import { DateFilter, parseFilter } from "chronolizer";
142
- import { Effect, Schema } from "effect";
102
+ import { EnglishLanguageLayer, suggestNatural } from "chronolizer";
103
+ import { Effect } from "effect";
143
104
 
144
- 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
+ );
145
109
 
146
- const program = decodeDateFilter(externalInput).pipe(Effect.flatMap(parseFilter));
110
+ Effect.runPromise(program);
111
+ // [{ text: "last month", range: ... }, ...]
147
112
  ```
148
113
 
149
- A filter has at most one lower bound (`gt` or `gte`), at most one upper bound (`lt` or `lte`), and at least one bound.
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
- ```text
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
- Examples:
118
+ Set `typoMode` to `"tolerant"` for conservative correction:
161
119
 
162
- ```text
163
- now/y
164
- now-1y/y
165
- now-1y/y+1M
166
- 2025-01-01
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
- Operations run from left to right. `/unit` floors to the start of the calendar unit. Fixed dates require `||` before operations.
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
- ## Resolve with an explicit time zone
131
+ Set `allowFuture` to `false` to reject explicit relative future ranges:
173
132
 
174
133
  ```ts
175
- import { parseFilter, resolve } from "chronolizer";
176
- import { DateTime, Effect } from "effect";
134
+ const program = parseNatural("next 3 months", {
135
+ locale: "en",
136
+ allowFuture: false,
137
+ });
138
+ ```
177
139
 
178
- const run = Effect.fn(function* () {
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
- const program = run();
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 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.
144
+ The same option is available in `suggestNatural`.
187
145
 
188
- ## Tolerant parsing
146
+ ### Resolve a range to dates
189
147
 
190
- ```ts
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
- The result reports:
150
+ ```ts
151
+ import { parseFilter, resolve } from "chronolizer";
152
+ import { DateTime, Effect } from "effect";
198
153
 
199
- - `quality`: `exact`, `corrected`, or `ambiguous`;
200
- - each correction and edit distance;
201
- - semantic alternatives for equal-cost ties.
154
+ const program = parseFilter({ gte: "now/y", lte: "now" }).pipe(
155
+ Effect.flatMap(resolve),
156
+ DateTime.withCurrentZoneNamed("Europe/Berlin"),
157
+ );
202
158
 
203
- Strict mode never runs correction.
159
+ Effect.runPromise(program);
160
+ ```
204
161
 
205
- ## Language plugins
162
+ `resolve` uses the Effect clock and `DateTime.CurrentTimeZone`. It does not use the host time zone without your decision.
206
163
 
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.
164
+ ### Validate an external filter
208
165
 
209
- Combine all required language plugins in one registry Layer:
166
+ Use the exported Effect Schema before you parse unknown data:
210
167
 
211
168
  ```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";
169
+ import { DateFilter, parseFilter } from "chronolizer";
170
+ import { Effect, Schema } from "effect";
216
171
 
217
- const Languages = languagePluginsLayer([EnglishLanguage, GermanLanguage, PolishLanguage]);
172
+ const program = Schema.decodeUnknownEffect(DateFilter)(externalInput).pipe(
173
+ Effect.flatMap(parseFilter),
174
+ );
218
175
 
219
- const program = parseNatural("letzten Monat", {
220
- locale: "de",
221
- }).pipe(Effect.provide(Languages));
176
+ Effect.runPromise(program);
222
177
  ```
223
178
 
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.
179
+ ## Supported expressions
225
180
 
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`.
181
+ Supported input families include:
227
182
 
228
- Each base language owns:
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
- - exact parsing and canonical rendering;
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
- 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.
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
- `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.
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
- Built-in plugins:
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
- - `EnglishLanguage`
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
- Import every non-English language from its locale subpath. This keeps the main bundle small and makes language selection explicit.
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
- Chinese and Japanese language packs are not included yet.
246
+ ## Main API
254
247
 
255
- 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 |
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
- readonly _tag: "Now";
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) => {
@@ -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
- 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
- }
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 };
@@ -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._tag === "Shift" && base.unit === operation.unit) {
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
  }
@@ -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) => 28 | 29 | 30 | 31;
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
- 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", {
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 const Shift: Schema.Codec<Shift>;
33
- declare const StartOf: Schema.Codec<StartOf>;
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>;