chronolizer 0.3.0 → 0.4.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 +46 -45
- package/dist/filter/expression.mjs +24 -18
- package/dist/language/model.mjs +1 -1
- package/dist/language/registry.mjs +32 -12
- package/dist/locales/cs.mjs +186 -17
- package/dist/locales/de.mjs +133 -20
- package/dist/locales/en.mjs +151 -35
- package/dist/locales/es.mjs +178 -19
- package/dist/locales/fr.mjs +150 -19
- package/dist/locales/nl.mjs +118 -19
- package/dist/locales/pl.mjs +156 -18
- package/dist/locales/shared.mjs +64 -1
- package/dist/locales/tr.mjs +138 -24
- package/dist/natural/correction.d.mts +1 -1
- package/dist/natural/correction.mjs +31 -5
- package/dist/natural/format.mjs +1 -1
- package/dist/natural/parse.mjs +5 -4
- package/dist/natural/suggest.mjs +19 -3
- package/dist/natural/suggestion.mjs +11 -5
- package/dist/resolve/resolve.mjs +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Chronolizer
|
|
2
2
|
|
|
3
|
-
Chronolizer is an Effect 4 library that converts natural-language date ranges to compact date
|
|
3
|
+
Chronolizer is an Effect 4 library that converts natural-language date ranges to compact date filters and back. It supports ranges without a start or end, autocomplete, spelling correction, and eight languages.
|
|
4
4
|
|
|
5
5
|
```text
|
|
6
6
|
year to date → { gte: "now/y", lte: "now" }
|
|
@@ -9,11 +9,11 @@ since January 2025 → { gte: "2025-01-01" }
|
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
> [!NOTE]
|
|
12
|
-
> Chronolizer currently uses `effect@4.0.0-rc.112`.
|
|
12
|
+
> Chronolizer currently uses `effect@4.0.0-rc.112`. Effect 4 is not stable yet, so its API can change.
|
|
13
13
|
|
|
14
14
|
## Install Chronolizer
|
|
15
15
|
|
|
16
|
-
Chronolizer requires Effect 4 and
|
|
16
|
+
Chronolizer requires Effect 4 and uses ES modules. Install both packages:
|
|
17
17
|
|
|
18
18
|
```sh
|
|
19
19
|
pnpm add chronolizer effect@4.0.0-rc.112
|
|
@@ -21,7 +21,7 @@ pnpm add chronolizer effect@4.0.0-rc.112
|
|
|
21
21
|
|
|
22
22
|
## Parse your first date range
|
|
23
23
|
|
|
24
|
-
English is available from the main package
|
|
24
|
+
English is available from the main package.
|
|
25
25
|
|
|
26
26
|
```ts
|
|
27
27
|
import { EnglishLanguageLayer, formatFilter, parseNatural } from "chronolizer";
|
|
@@ -43,7 +43,7 @@ Effect.runPromise(program);
|
|
|
43
43
|
|
|
44
44
|
### Parse other languages
|
|
45
45
|
|
|
46
|
-
Import each non-English language
|
|
46
|
+
Import each non-English language separately. This keeps unused language code out of your build.
|
|
47
47
|
|
|
48
48
|
```ts
|
|
49
49
|
import { formatFilter, parseNatural } from "chronolizer";
|
|
@@ -60,7 +60,7 @@ Effect.runPromise(program);
|
|
|
60
60
|
// { gte: "now/y", lte: "now" }
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
-
Use `languagePluginsLayer`
|
|
63
|
+
Use `languagePluginsLayer` to combine languages:
|
|
64
64
|
|
|
65
65
|
```ts
|
|
66
66
|
import { EnglishLanguage, languagePluginsLayer, parseNatural } from "chronolizer";
|
|
@@ -77,7 +77,7 @@ Effect.runPromise(program);
|
|
|
77
77
|
|
|
78
78
|
### Format a range as natural language
|
|
79
79
|
|
|
80
|
-
`formatNatural` returns
|
|
80
|
+
`formatNatural` returns one standard phrase for a range. It preserves the meaning, but it does not reproduce the original wording. Absolute days use the language's numeric `Intl.DateTimeFormat` form.
|
|
81
81
|
|
|
82
82
|
```ts
|
|
83
83
|
import { formatNatural, parseFilter } from "chronolizer";
|
|
@@ -96,7 +96,7 @@ Effect.runPromise(program);
|
|
|
96
96
|
|
|
97
97
|
### Add autocomplete
|
|
98
98
|
|
|
99
|
-
`suggestNatural` returns valid
|
|
99
|
+
`suggestNatural` returns valid standard phrases and their date ranges.
|
|
100
100
|
|
|
101
101
|
```ts
|
|
102
102
|
import { EnglishLanguageLayer, suggestNatural } from "chronolizer";
|
|
@@ -115,7 +115,7 @@ The default limit is 10. The maximum limit is 100. A nonpositive or invalid limi
|
|
|
115
115
|
|
|
116
116
|
### Accept spelling errors
|
|
117
117
|
|
|
118
|
-
Set `typoMode` to `"tolerant"`
|
|
118
|
+
Set `typoMode` to `"tolerant"` to correct close spelling errors:
|
|
119
119
|
|
|
120
120
|
```ts
|
|
121
121
|
const program = parseNatural("januray of last yaer", {
|
|
@@ -124,11 +124,11 @@ const program = parseNatural("januray of last yaer", {
|
|
|
124
124
|
});
|
|
125
125
|
```
|
|
126
126
|
|
|
127
|
-
The
|
|
127
|
+
The result contains its `quality`, applied `corrections`, and any `alternatives` with a different meaning. Strict mode is the default and does not correct the input.
|
|
128
128
|
|
|
129
|
-
### Exclude future
|
|
129
|
+
### Exclude future ranges
|
|
130
130
|
|
|
131
|
-
Set `allowFuture` to `false` to reject
|
|
131
|
+
Set `allowFuture` to `false` to reject periods after the current period:
|
|
132
132
|
|
|
133
133
|
```ts
|
|
134
134
|
const program = parseNatural("next 3 months", {
|
|
@@ -137,7 +137,7 @@ const program = parseNatural("next 3 months", {
|
|
|
137
137
|
});
|
|
138
138
|
```
|
|
139
139
|
|
|
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
|
|
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 ranges that end at the current time.
|
|
141
141
|
|
|
142
142
|
The option does not compare fixed dates, such as `January 2099`, with the current date. Parsing does not read the clock.
|
|
143
143
|
|
|
@@ -145,7 +145,7 @@ The same option is available in `suggestNatural`.
|
|
|
145
145
|
|
|
146
146
|
### Resolve a range to dates
|
|
147
147
|
|
|
148
|
-
Provide an Effect time zone when you
|
|
148
|
+
Provide an Effect time zone when you calculate ranges based on the current date:
|
|
149
149
|
|
|
150
150
|
```ts
|
|
151
151
|
import { parseFilter, resolve } from "chronolizer";
|
|
@@ -159,7 +159,7 @@ const program = parseFilter({ gte: "now/y", lte: "now" }).pipe(
|
|
|
159
159
|
Effect.runPromise(program);
|
|
160
160
|
```
|
|
161
161
|
|
|
162
|
-
`resolve` uses the Effect clock and `DateTime.CurrentTimeZone`. It
|
|
162
|
+
`resolve` uses the Effect clock and `DateTime.CurrentTimeZone`. It uses only the time zone that you provide.
|
|
163
163
|
|
|
164
164
|
### Validate an external filter
|
|
165
165
|
|
|
@@ -176,18 +176,19 @@ const program = Schema.decodeUnknownEffect(DateFilter)(externalInput).pipe(
|
|
|
176
176
|
Effect.runPromise(program);
|
|
177
177
|
```
|
|
178
178
|
|
|
179
|
-
## Supported
|
|
179
|
+
## Supported input
|
|
180
180
|
|
|
181
|
-
|
|
181
|
+
You can parse:
|
|
182
182
|
|
|
183
|
-
-
|
|
184
|
-
-
|
|
185
|
-
-
|
|
186
|
-
- period
|
|
187
|
-
- fixed months, quarters, years, and
|
|
183
|
+
- days, weeks, months, quarters, and years;
|
|
184
|
+
- ranges with a length, such as `last 3 months`;
|
|
185
|
+
- single periods in the past or future, such as `30 months ago`;
|
|
186
|
+
- ranges from a period start to now, such as `year to date`;
|
|
187
|
+
- fixed months, quarters, years, and date ranges;
|
|
188
188
|
- named days with or without a year, such as `January 12` and `12th of January`;
|
|
189
|
-
-
|
|
190
|
-
-
|
|
189
|
+
- ranges without one end, such as `since January 2025` and `from January 12`;
|
|
190
|
+
- written counts from one through ninety-nine, such as `two weeks ago` and `twenty-one weeks ago`;
|
|
191
|
+
- combined phrases, such as `the day before January 12` and `yesterday two weeks ago`;
|
|
191
192
|
- period starts, period ends, and weekends;
|
|
192
193
|
- abbreviated month names and common equivalent phrases.
|
|
193
194
|
|
|
@@ -206,16 +207,16 @@ Examples:
|
|
|
206
207
|
|
|
207
208
|
## Supported languages
|
|
208
209
|
|
|
209
|
-
| Language |
|
|
210
|
-
| -------- |
|
|
211
|
-
| English | `en`
|
|
212
|
-
| German | `de`
|
|
213
|
-
| Spanish | `es`
|
|
214
|
-
| French | `fr`
|
|
215
|
-
| Dutch | `nl`
|
|
216
|
-
| Turkish | `tr`
|
|
217
|
-
| Czech | `cs`
|
|
218
|
-
| Polish | `pl`
|
|
210
|
+
| Language | Code | Import |
|
|
211
|
+
| -------- | ---- | ----------------------------------------- |
|
|
212
|
+
| English | `en` | `chronolizer` or `chronolizer/locales/en` |
|
|
213
|
+
| German | `de` | `chronolizer/locales/de` |
|
|
214
|
+
| Spanish | `es` | `chronolizer/locales/es` |
|
|
215
|
+
| French | `fr` | `chronolizer/locales/fr` |
|
|
216
|
+
| Dutch | `nl` | `chronolizer/locales/nl` |
|
|
217
|
+
| Turkish | `tr` | `chronolizer/locales/tr` |
|
|
218
|
+
| Czech | `cs` | `chronolizer/locales/cs` |
|
|
219
|
+
| Polish | `pl` | `chronolizer/locales/pl` |
|
|
219
220
|
|
|
220
221
|
## Date filter reference
|
|
221
222
|
|
|
@@ -239,22 +240,22 @@ unit := "d" | "w" | "M" | "q" | "y"
|
|
|
239
240
|
|
|
240
241
|
`/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
242
|
|
|
242
|
-
|
|
243
|
+
A complete calendar period includes its start but excludes the start of the next period. Weeks start on Monday.
|
|
243
244
|
|
|
244
245
|
A named day without a year uses the current calendar year. Write the year for February 29 because parsing does not read the clock.
|
|
245
246
|
|
|
246
247
|
## Main API
|
|
247
248
|
|
|
248
|
-
| Export | Purpose
|
|
249
|
-
| --------------------- |
|
|
250
|
-
| `parseNatural` | Parse complete natural-language input to a
|
|
251
|
-
| `formatNatural` |
|
|
252
|
-
| `suggestNatural` | Return autocomplete suggestions and
|
|
253
|
-
| `parseFilter` | Parse a date filter to a
|
|
254
|
-
| `formatFilter` | Format a
|
|
255
|
-
| `resolve` |
|
|
256
|
-
| `DateFilter` | Validate external date-filter data with Effect Schema
|
|
257
|
-
| `DateRangeFromFilter` |
|
|
249
|
+
| Export | Purpose |
|
|
250
|
+
| --------------------- | ---------------------------------------------------------- |
|
|
251
|
+
| `parseNatural` | Parse complete natural-language input to a date range |
|
|
252
|
+
| `formatNatural` | Write a supported range as one standard phrase |
|
|
253
|
+
| `suggestNatural` | Return autocomplete suggestions and date ranges |
|
|
254
|
+
| `parseFilter` | Parse a date filter to a date range |
|
|
255
|
+
| `formatFilter` | Format a date range as a date filter |
|
|
256
|
+
| `resolve` | Calculate dates with an Effect clock and time zone |
|
|
257
|
+
| `DateFilter` | Validate external date-filter data with Effect Schema |
|
|
258
|
+
| `DateRangeFromFilter` | Convert a filter in both directions with one Effect Schema |
|
|
258
259
|
|
|
259
260
|
## License
|
|
260
261
|
|
|
@@ -14,25 +14,31 @@ const failAt = (input, offset, expected) => Effect.fail(new FilterExpressionPars
|
|
|
14
14
|
}));
|
|
15
15
|
const isDigit = (value) => value >= "0" && value <= "9";
|
|
16
16
|
const isFirstPositiveDigit = (value) => value >= "1" && value <= "9";
|
|
17
|
+
const parseAnchor = Effect.fn(function* (input) {
|
|
18
|
+
if (input.startsWith("now")) return {
|
|
19
|
+
expression: now(),
|
|
20
|
+
cursor: 3,
|
|
21
|
+
fixed: false
|
|
22
|
+
};
|
|
23
|
+
const candidate = input.slice(0, 10);
|
|
24
|
+
if (!isIsoDate(candidate)) return yield* failAt(input, 0, "\"now\" or an ISO date (YYYY-MM-DD)");
|
|
25
|
+
return {
|
|
26
|
+
expression: dateLiteral(candidate),
|
|
27
|
+
cursor: 10,
|
|
28
|
+
fixed: true
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
const operationStart = Effect.fn(function* (input, anchor) {
|
|
32
|
+
if (!anchor.fixed || anchor.cursor === input.length) return anchor.cursor;
|
|
33
|
+
if (input.slice(anchor.cursor, anchor.cursor + 2) !== "||") return yield* failAt(input, anchor.cursor, "\"||\" before date operations");
|
|
34
|
+
const cursor = anchor.cursor + 2;
|
|
35
|
+
if (cursor === input.length) return yield* failAt(input, cursor, "an operation beginning with \"+\", \"-\", or \"/\"");
|
|
36
|
+
return cursor;
|
|
37
|
+
});
|
|
17
38
|
const parseInstantExpression = Effect.fn(function* (input) {
|
|
18
|
-
|
|
19
|
-
let expression;
|
|
20
|
-
let
|
|
21
|
-
if (input.startsWith("now")) {
|
|
22
|
-
expression = now();
|
|
23
|
-
cursor = 3;
|
|
24
|
-
} else {
|
|
25
|
-
const candidate = input.slice(0, 10);
|
|
26
|
-
if (!isIsoDate(candidate)) return yield* failAt(input, 0, "\"now\" or an ISO date (YYYY-MM-DD)");
|
|
27
|
-
expression = dateLiteral(candidate);
|
|
28
|
-
cursor = 10;
|
|
29
|
-
fixedAnchor = true;
|
|
30
|
-
}
|
|
31
|
-
if (fixedAnchor && cursor < input.length) {
|
|
32
|
-
if (input.slice(cursor, cursor + 2) !== "||") return yield* failAt(input, cursor, "\"||\" before date operations");
|
|
33
|
-
cursor += 2;
|
|
34
|
-
if (cursor === input.length) return yield* failAt(input, cursor, "an operation beginning with \"+\", \"-\", or \"/\"");
|
|
35
|
-
}
|
|
39
|
+
const anchor = yield* parseAnchor(input);
|
|
40
|
+
let expression = anchor.expression;
|
|
41
|
+
let cursor = yield* operationStart(input, anchor);
|
|
36
42
|
while (cursor < input.length) {
|
|
37
43
|
const operator = input[cursor];
|
|
38
44
|
if (operator === "/") {
|
package/dist/language/model.mjs
CHANGED
|
@@ -35,7 +35,7 @@ const NaturalSuggestion = Schema.Struct({
|
|
|
35
35
|
var BaseLanguageContribution = class extends Data.TaggedClass("BaseLanguage") {};
|
|
36
36
|
var LanguageExtensionContribution = class extends Data.TaggedClass("LanguageExtension") {};
|
|
37
37
|
const canonicalBaseLocale = (input) => Result.getSuccess(Result.try(() => new Intl.Locale(input).baseName));
|
|
38
|
-
const Locale = Schema.String.check(Schema.makeFilter((value) => Option.contains(canonicalBaseLocale(value), value), { expected: "a
|
|
38
|
+
const Locale = Schema.String.check(Schema.makeFilter((value) => Option.contains(canonicalBaseLocale(value), value), { expected: "a standard BCP 47 language code" })).annotate({ identifier: "Locale" });
|
|
39
39
|
const BaseLanguageMetadata = Schema.TaggedStruct("BaseLanguage", {
|
|
40
40
|
locale: Locale,
|
|
41
41
|
vocabulary: Schema.Array(Schema.String)
|
|
@@ -45,28 +45,34 @@ const compileLanguage = (locale, registered) => {
|
|
|
45
45
|
}));
|
|
46
46
|
};
|
|
47
47
|
const createRegistry = Effect.fn(function* () {
|
|
48
|
-
const
|
|
48
|
+
const state = yield* Ref.make({
|
|
49
|
+
entries: [],
|
|
50
|
+
compiledLanguages: /* @__PURE__ */ new Map()
|
|
51
|
+
});
|
|
49
52
|
const register = Effect.fn(function* (pluginId, contribution) {
|
|
50
53
|
if (pluginId.length === 0 || !Schema.is(LanguageContributionMetadata)(contribution)) return yield* new LanguageRegistrationError({
|
|
51
54
|
pluginId,
|
|
52
55
|
locale: contribution.locale,
|
|
53
|
-
message: "
|
|
56
|
+
message: "The plugin name or language settings are invalid"
|
|
54
57
|
});
|
|
55
58
|
const token = Symbol(pluginId);
|
|
56
|
-
yield* Effect.acquireRelease(Ref.modify(
|
|
59
|
+
yield* Effect.acquireRelease(Ref.modify(state, (current) => {
|
|
57
60
|
const conflictingBase = Match.valueTags(contribution, {
|
|
58
|
-
BaseLanguage: (base) => Array.findFirst(current, (entry) => Match.valueTags(entry.contribution, {
|
|
61
|
+
BaseLanguage: (base) => Array.findFirst(current.entries, (entry) => Match.valueTags(entry.contribution, {
|
|
59
62
|
BaseLanguage: (registeredBase) => registeredBase.locale === base.locale,
|
|
60
63
|
LanguageExtension: () => false
|
|
61
64
|
})),
|
|
62
65
|
LanguageExtension: () => Option.none()
|
|
63
66
|
});
|
|
64
67
|
return Option.match(conflictingBase, {
|
|
65
|
-
onNone: () => [Result.succeed(token),
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
onNone: () => [Result.succeed(token), {
|
|
69
|
+
entries: Array.append(current.entries, {
|
|
70
|
+
token,
|
|
71
|
+
pluginId,
|
|
72
|
+
contribution
|
|
73
|
+
}),
|
|
74
|
+
compiledLanguages: /* @__PURE__ */ new Map()
|
|
75
|
+
}],
|
|
70
76
|
onSome: (conflict) => [Result.fail(new LanguageConflictError({
|
|
71
77
|
locale: contribution.locale,
|
|
72
78
|
firstPluginId: conflict.pluginId,
|
|
@@ -74,12 +80,26 @@ const createRegistry = Effect.fn(function* () {
|
|
|
74
80
|
message: "Only one base language can be registered for a locale"
|
|
75
81
|
})), current]
|
|
76
82
|
});
|
|
77
|
-
}).pipe(Effect.flatMap((result) => Effect.fromResult(result))), (registeredToken) => Ref.update(
|
|
83
|
+
}).pipe(Effect.flatMap((result) => Effect.fromResult(result))), (registeredToken) => Ref.update(state, (current) => ({
|
|
84
|
+
entries: Array.filter(current.entries, (entry) => entry.token !== registeredToken),
|
|
85
|
+
compiledLanguages: /* @__PURE__ */ new Map()
|
|
86
|
+
})));
|
|
78
87
|
});
|
|
79
88
|
const resolve = Effect.fn(function* (locale) {
|
|
80
89
|
const canonical = canonicalBaseLocale(locale);
|
|
81
90
|
if (Option.isSome(canonical)) {
|
|
82
|
-
const compiled =
|
|
91
|
+
const compiled = yield* Ref.modify(state, (current) => {
|
|
92
|
+
const cached = current.compiledLanguages.get(canonical.value);
|
|
93
|
+
if (cached !== void 0) return [Option.some(cached), current];
|
|
94
|
+
const language = compileLanguage(canonical.value, current.entries);
|
|
95
|
+
if (Option.isNone(language)) return [language, current];
|
|
96
|
+
const cache = new Map(current.compiledLanguages);
|
|
97
|
+
cache.set(canonical.value, language.value);
|
|
98
|
+
return [language, {
|
|
99
|
+
entries: current.entries,
|
|
100
|
+
compiledLanguages: cache
|
|
101
|
+
}];
|
|
102
|
+
});
|
|
83
103
|
if (Option.isSome(compiled)) return compiled.value;
|
|
84
104
|
}
|
|
85
105
|
return yield* new UnsupportedLocaleError({ locale });
|
|
@@ -102,7 +122,7 @@ const createPluginRegistry = Effect.fn(function* (plugins) {
|
|
|
102
122
|
if (duplicate !== void 0) return yield* new LanguageRegistrationError({
|
|
103
123
|
pluginId: duplicate,
|
|
104
124
|
locale: "*",
|
|
105
|
-
message: "Plugin
|
|
125
|
+
message: "Plugin names must be unique"
|
|
106
126
|
});
|
|
107
127
|
const registry = yield* createRegistry();
|
|
108
128
|
const context = { register: registry.register };
|
package/dist/locales/cs.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { normalizeNaturalText } from "../natural/text.mjs";
|
|
|
4
4
|
import { defineLanguagePlugin, languagePluginsLayer } from "../language/registry.mjs";
|
|
5
5
|
import { correctWhitespaceSeparatedText } from "../natural/correction.mjs";
|
|
6
6
|
import { completeNaturalPhrases, fixedCalendarPeriodPhrases, naturalCount, prefixNaturalPhrases } from "../natural/suggestion.mjs";
|
|
7
|
-
import { absoluteDatePeriod, calendarPeriodOffset, candidate, currentYearDatePeriods, datedPeriods, datedQuarterPeriods, fixedDatePeriod, fixedMonthPeriod, fixedQuarterPeriod, fixedYearPeriod, fromNowRange, futurePeriod, futureRange, isoDate, joinedNowCandidate, joinedPeriodCandidate, monthOfRelativeYear, namedCurrentYearDatePeriod, namedDatePeriod, openBoundaryCandidate, parseTrailingCount, periodEndDay, periodRange, periodStartDay, periodToDateRange, periodsFromPhrases, quarterOfRelativeYear, relativePeriod, relativeWeekend, remainingPeriodRange, renderPeriodRange, textAt, trailingPeriod, trailingRange, untilNowRange, validYear } from "./shared.mjs";
|
|
7
|
+
import { absoluteDatePeriod, calendarPeriodOffset, candidate, compileCountAliasNormalizer, compoundCountAliases, countAliasVocabulary, currentYearDatePeriods, datedPeriods, datedQuarterPeriods, decimalTens, decomposeShiftedPeriodRange, fixedDatePeriod, fixedMonthPeriod, fixedQuarterPeriod, fixedYearPeriod, fromNowRange, futurePeriod, futureRange, isoDate, joinedNowCandidate, joinedPeriodCandidate, monthOfRelativeYear, namedCurrentYearDatePeriod, namedDatePeriod, openBoundaryCandidate, parseTrailingCount, periodEndDay, periodRange, periodStartDay, periodToDateRange, periodsFromPhrases, quarterOfRelativeYear, relativePeriod, relativeWeekday, relativeWeekend, remainingPeriodRange, renderPeriodRange, sequentialCountAliases, shiftPeriod, textAt, trailingPeriod, trailingRange, untilNowRange, validYear } from "./shared.mjs";
|
|
8
8
|
import { Effect, Option, String as String$1 } from "effect";
|
|
9
9
|
//#region src/locales/cs.ts
|
|
10
10
|
const months = [
|
|
@@ -21,6 +21,123 @@ const months = [
|
|
|
21
21
|
"listopad",
|
|
22
22
|
"prosinec"
|
|
23
23
|
];
|
|
24
|
+
const weekdays = [
|
|
25
|
+
"pondělí",
|
|
26
|
+
"úterý",
|
|
27
|
+
"středa",
|
|
28
|
+
"čtvrtek",
|
|
29
|
+
"pátek",
|
|
30
|
+
"sobota",
|
|
31
|
+
"neděle"
|
|
32
|
+
];
|
|
33
|
+
const weekdayGenitives = [
|
|
34
|
+
"pondělí",
|
|
35
|
+
"úterý",
|
|
36
|
+
"středy",
|
|
37
|
+
"čtvrtka",
|
|
38
|
+
"pátku",
|
|
39
|
+
"soboty",
|
|
40
|
+
"neděle"
|
|
41
|
+
];
|
|
42
|
+
const nextWeekdays = weekdays.flatMap((weekday, day) => [{
|
|
43
|
+
phrase: `příští ${weekday}`,
|
|
44
|
+
day,
|
|
45
|
+
canonical: `příští ${weekday}`
|
|
46
|
+
}, {
|
|
47
|
+
phrase: `příštího ${textAt(weekdayGenitives, day)}`,
|
|
48
|
+
day,
|
|
49
|
+
canonical: `příští ${weekday}`
|
|
50
|
+
}]);
|
|
51
|
+
const czechCountWords = [
|
|
52
|
+
[
|
|
53
|
+
"dva",
|
|
54
|
+
"dvě",
|
|
55
|
+
"dvou",
|
|
56
|
+
"dvěma"
|
|
57
|
+
],
|
|
58
|
+
[
|
|
59
|
+
"tři",
|
|
60
|
+
"tří",
|
|
61
|
+
"třemi"
|
|
62
|
+
],
|
|
63
|
+
[
|
|
64
|
+
"čtyři",
|
|
65
|
+
"čtyř",
|
|
66
|
+
"čtyřmi"
|
|
67
|
+
],
|
|
68
|
+
["pět", "pěti"],
|
|
69
|
+
["šest", "šesti"],
|
|
70
|
+
["sedm", "sedmi"],
|
|
71
|
+
["osm", "osmi"],
|
|
72
|
+
["devět", "devíti"],
|
|
73
|
+
["deset", "deseti"],
|
|
74
|
+
["jedenáct", "jedenácti"],
|
|
75
|
+
["dvanáct", "dvanácti"],
|
|
76
|
+
["třináct", "třinácti"],
|
|
77
|
+
["čtrnáct", "čtrnácti"],
|
|
78
|
+
["patnáct", "patnácti"],
|
|
79
|
+
["šestnáct", "šestnácti"],
|
|
80
|
+
["sedmnáct", "sedmnácti"],
|
|
81
|
+
["osmnáct", "osmnácti"],
|
|
82
|
+
["devatenáct", "devatenácti"],
|
|
83
|
+
["dvacet", "dvaceti"]
|
|
84
|
+
];
|
|
85
|
+
const czechCountOnes = [
|
|
86
|
+
[1, "jedna"],
|
|
87
|
+
[2, "dva"],
|
|
88
|
+
[3, "tři"],
|
|
89
|
+
[4, "čtyři"],
|
|
90
|
+
[5, "pět"],
|
|
91
|
+
[6, "šest"],
|
|
92
|
+
[7, "sedm"],
|
|
93
|
+
[8, "osm"],
|
|
94
|
+
[9, "devět"]
|
|
95
|
+
];
|
|
96
|
+
const czechObliqueCountOnes = [
|
|
97
|
+
[1, "jedním"],
|
|
98
|
+
[2, "dvěma"],
|
|
99
|
+
[3, "třemi"],
|
|
100
|
+
[4, "čtyřmi"],
|
|
101
|
+
[5, "pěti"],
|
|
102
|
+
[6, "šesti"],
|
|
103
|
+
[7, "sedmi"],
|
|
104
|
+
[8, "osmi"],
|
|
105
|
+
[9, "devíti"]
|
|
106
|
+
];
|
|
107
|
+
const czechCountAliases = [
|
|
108
|
+
...sequentialCountAliases([[
|
|
109
|
+
"jeden",
|
|
110
|
+
"jedna",
|
|
111
|
+
"jedno",
|
|
112
|
+
"jedním",
|
|
113
|
+
"jednou"
|
|
114
|
+
]], 1),
|
|
115
|
+
...sequentialCountAliases(czechCountWords, 2),
|
|
116
|
+
...compoundCountAliases(decimalTens([
|
|
117
|
+
"dvacet",
|
|
118
|
+
"třicet",
|
|
119
|
+
"čtyřicet",
|
|
120
|
+
"padesát",
|
|
121
|
+
"šedesát",
|
|
122
|
+
"sedmdesát",
|
|
123
|
+
"osmdesát",
|
|
124
|
+
"devadesát"
|
|
125
|
+
]), czechCountOnes, (ten, one) => [`${ten} ${one}`]),
|
|
126
|
+
...compoundCountAliases(decimalTens([
|
|
127
|
+
"dvaceti",
|
|
128
|
+
"třiceti",
|
|
129
|
+
"čtyřiceti",
|
|
130
|
+
"padesáti",
|
|
131
|
+
"šedesáti",
|
|
132
|
+
"sedmdesáti",
|
|
133
|
+
"osmdesáti",
|
|
134
|
+
"devadesáti"
|
|
135
|
+
]), czechObliqueCountOnes, (ten, one) => [`${ten} ${one}`])
|
|
136
|
+
];
|
|
137
|
+
const normalizeCzechCounts = compileCountAliasNormalizer(czechCountAliases);
|
|
138
|
+
const czechCountVocabulary = new Set(countAliasVocabulary(czechCountAliases));
|
|
139
|
+
const correctCzech = (input, vocabulary) => correctWhitespaceSeparatedText(input, vocabulary, czechCountVocabulary);
|
|
140
|
+
const normalizeCzech = (input, locale) => normalizeCzechCounts(normalizeNaturalText(input, locale));
|
|
24
141
|
const monthGenitives = [
|
|
25
142
|
"ledna",
|
|
26
143
|
"února",
|
|
@@ -406,13 +523,13 @@ const parseQuarter = (input) => {
|
|
|
406
523
|
const quarter = quarterNumber(textAt(standalone.value, 1));
|
|
407
524
|
return quarter === void 0 ? Option.none() : Option.some(quarterOfRelativeYear(quarter, 0, `Q${quarter}`));
|
|
408
525
|
};
|
|
409
|
-
const
|
|
410
|
-
const
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
if (Option.isSome(
|
|
526
|
+
const parseDatedPeriod = (input) => {
|
|
527
|
+
const knownPeriod = Option.firstSomeOf([
|
|
528
|
+
absoluteDatePeriod(input, "cs"),
|
|
529
|
+
parseNamedDate(input),
|
|
530
|
+
parseQuarter(input)
|
|
531
|
+
]);
|
|
532
|
+
if (Option.isSome(knownPeriod)) return knownPeriod;
|
|
416
533
|
const yearMatch = String$1.match(/^(?:rok |roku )?(\d{4})$/u)(input);
|
|
417
534
|
if (Option.isSome(yearMatch)) {
|
|
418
535
|
const year = validYear(textAt(yearMatch.value, 1));
|
|
@@ -424,6 +541,20 @@ const parseBasePeriod = (input) => {
|
|
|
424
541
|
const year = validYear(textAt(monthYear.value, 2));
|
|
425
542
|
if (month !== void 0 && year !== void 0) return Option.some(fixedMonthPeriod(year, month, `${textAt(months, month - 1)} ${year}`));
|
|
426
543
|
}
|
|
544
|
+
return Option.none();
|
|
545
|
+
};
|
|
546
|
+
const parseBasePeriod = (input) => {
|
|
547
|
+
const datedPeriod = parseDatedPeriod(input);
|
|
548
|
+
if (Option.isSome(datedPeriod)) return datedPeriod;
|
|
549
|
+
const prefixedRelativeMonth = String$1.match(/^(minulý|tento|příští) ([a-záčďéěíňóřšťúůýž]+\.?)$/u)(input);
|
|
550
|
+
if (Option.isSome(prefixedRelativeMonth)) {
|
|
551
|
+
const month = monthNumber(textAt(prefixedRelativeMonth.value, 2));
|
|
552
|
+
if (month !== void 0) {
|
|
553
|
+
const modifier = textAt(prefixedRelativeMonth.value, 1);
|
|
554
|
+
const direction = relativeYearDirection(modifier);
|
|
555
|
+
return Option.some(monthOfRelativeYear(month, direction, `${modifier} ${textAt(months, month - 1)}`));
|
|
556
|
+
}
|
|
557
|
+
}
|
|
427
558
|
const relativeMonth = String$1.match(/^([a-záčďéěíňóřšťúůýž]+\.?) (minulého roku|příštího roku|tohoto roku)$/u)(input);
|
|
428
559
|
const relativeMonthYearFirst = String$1.match(/^(minulého roku|příštího roku|tohoto roku) ([a-záčďéěíňóřšťúůýž]+\.?)$/u)(input);
|
|
429
560
|
const relativeMatch = Option.firstSomeOf([relativeMonth, relativeMonthYearFirst]);
|
|
@@ -444,12 +575,31 @@ const parseBasePeriod = (input) => {
|
|
|
444
575
|
if (["příští víkend", "následující víkend"].includes(input)) return Option.some(relativeWeekend(1, "příští víkend"));
|
|
445
576
|
if (input === "předminulý víkend") return Option.some(relativeWeekend(-2, input));
|
|
446
577
|
if (input === "přespříští víkend") return Option.some(relativeWeekend(2, input));
|
|
447
|
-
|
|
578
|
+
const weekday = nextWeekdays.find((entry) => entry.phrase === input);
|
|
579
|
+
return weekday === void 0 ? Option.none() : Option.some(relativeWeekday(weekday.day, 1, weekday.canonical));
|
|
448
580
|
};
|
|
449
581
|
const parsePeriod = (input) => {
|
|
582
|
+
const shifted = String$1.match(/^(.+) (před|za) ([1-9]\d*) (den|dny|dnů|dnem|týden|týdny|týdnů|týdnem|měsíc|měsíce|měsíců|měsícem|měsíci|čtvrtletí|čtvrtletím|čtvrtletími|rok|roky|let|rokem|lety)$/u)(input);
|
|
583
|
+
if (Option.isSome(shifted)) {
|
|
584
|
+
const amount = parseTrailingCount(textAt(shifted.value, 3));
|
|
585
|
+
const alias = unitAliases.find((unit) => unit[0] === textAt(shifted.value, 4));
|
|
586
|
+
const entry = alias === void 0 ? void 0 : units.find((unit) => unit.unit === alias[1]);
|
|
587
|
+
const period = parsePeriod(textAt(shifted.value, 1));
|
|
588
|
+
if (Option.isSome(amount) && entry !== void 0 && Option.isSome(period)) {
|
|
589
|
+
const past = textAt(shifted.value, 2) === "před";
|
|
590
|
+
const direction = past ? -amount.value : amount.value;
|
|
591
|
+
const pastNoun = amount.value === 1 ? entry.pastSingular : entry.pastPlural;
|
|
592
|
+
const noun = past ? pastNoun : countNoun(amount.value, entry);
|
|
593
|
+
const canonical = `${period.value.canonical} ${past ? "před" : "za"} ${amount.value} ${noun}`;
|
|
594
|
+
return Option.some(shiftPeriod(period.value, direction, entry.unit, canonical));
|
|
595
|
+
}
|
|
596
|
+
}
|
|
450
597
|
const edge = String$1.match(/^(začátek|počátek|konec) (.+)$/u)(input);
|
|
451
598
|
if (Option.isSome(edge)) {
|
|
452
|
-
const
|
|
599
|
+
const periodText = textAt(edge.value, 2);
|
|
600
|
+
const basePeriod = parseBasePeriod(periodText);
|
|
601
|
+
const implicit = units.find((entry) => entry.durationGenitive === periodText);
|
|
602
|
+
const period = Option.isSome(basePeriod) || implicit === void 0 ? basePeriod : Option.some(relativePeriod(implicit.unit, 0, implicit.current));
|
|
453
603
|
if (Option.isSome(period)) {
|
|
454
604
|
const isEnd = textAt(edge.value, 1) === "konec";
|
|
455
605
|
const canonical = `${isEnd ? "konec" : "začátek"} ${period.value.canonical}`;
|
|
@@ -462,7 +612,8 @@ const parsePeriod = (input) => {
|
|
|
462
612
|
"celý ",
|
|
463
613
|
"celé "
|
|
464
614
|
].find((prefix) => input.startsWith(prefix));
|
|
465
|
-
|
|
615
|
+
const base = parseBasePeriod(wrapper === void 0 ? input : input.slice(wrapper.length));
|
|
616
|
+
return Option.isSome(base) ? base : parseCalendarOffset(input);
|
|
466
617
|
};
|
|
467
618
|
const countedUnit = (value) => unitAliases.find((entry) => entry[0] === value)?.[1];
|
|
468
619
|
const countedUnitPattern = "den|dny|dnů|dnem|týden|týdny|týdnů|týdnem|měsíc|měsíce|měsíců|měsícem|měsíci|čtvrtletí|čtvrtletím|čtvrtletími|rok|roky|let|rokem|lety";
|
|
@@ -512,7 +663,7 @@ const parseCalendarOffset = (input) => {
|
|
|
512
663
|
const futureNoun = countNoun(amount.value, entry);
|
|
513
664
|
const pastNoun = amount.value === 1 ? entry.pastSingular : entry.pastPlural;
|
|
514
665
|
const canonical = direction < 0 ? `před ${amount.value} ${pastNoun}` : `za ${amount.value} ${futureNoun}`;
|
|
515
|
-
return Option.some(
|
|
666
|
+
return Option.some(relativePeriod(unit, direction, canonical));
|
|
516
667
|
};
|
|
517
668
|
const parseRollingPeriod = (input) => {
|
|
518
669
|
const singular = singularRollingPhrases.find((entry) => entry.phrase === input);
|
|
@@ -579,15 +730,17 @@ const parseCzech = (input) => {
|
|
|
579
730
|
"ode dneška",
|
|
580
731
|
"od teď"
|
|
581
732
|
].includes(input)) return Option.some(candidate(fromNowRange(), "od nynějška"));
|
|
582
|
-
const offset = parseCalendarOffset(input);
|
|
583
|
-
if (Option.isSome(offset)) return offset;
|
|
584
733
|
const rolling = parseRollingPeriod(input);
|
|
585
734
|
if (Option.isSome(rolling)) return rolling;
|
|
586
735
|
const toDate = toDatePhrases.find((entry) => entry.phrase === input);
|
|
587
736
|
if (toDate !== void 0) return Option.some(candidate(periodToDateRange(toDate.entry.unit), toDate.entry.toDate));
|
|
588
737
|
const elided = parseElidedDateRange(input);
|
|
589
738
|
if (Option.isSome(elided)) return elided;
|
|
590
|
-
const nowBounded = joinedNowCandidate(input, [["od ", " do dneška"], ["mezi ", " a dneškem"]], [
|
|
739
|
+
const nowBounded = joinedNowCandidate(input, [["od ", " do dneška"], ["mezi ", " a dneškem"]], [
|
|
740
|
+
"od dneška do ",
|
|
741
|
+
"mezi dneškem a ",
|
|
742
|
+
"dnešek až "
|
|
743
|
+
], parsePeriod, (period) => `od ${period} do dneška`, (period) => `od dneška do ${period}`);
|
|
591
744
|
if (Option.isSome(nowBounded)) return nowBounded;
|
|
592
745
|
const bounded = joinedPeriodCandidate(input, [
|
|
593
746
|
["od ", " do "],
|
|
@@ -609,6 +762,8 @@ const staticPeriodPhrases = [
|
|
|
609
762
|
"předminulý víkend",
|
|
610
763
|
"přespříští víkend",
|
|
611
764
|
...periodAliases.flatMap((entry) => [`začátek ${entry[0]}`, `konec ${entry[0]}`]),
|
|
765
|
+
...nextWeekdays.map((entry) => entry.phrase),
|
|
766
|
+
...months.flatMap((month) => [`minulý ${month}`, `příští ${month}`]),
|
|
612
767
|
...[
|
|
613
768
|
1,
|
|
614
769
|
2,
|
|
@@ -670,6 +825,18 @@ const suggestCzech = (input, limit) => {
|
|
|
670
825
|
], limit);
|
|
671
826
|
};
|
|
672
827
|
const renderCzech = (range) => {
|
|
828
|
+
const shifted = decomposeShiftedPeriodRange(range);
|
|
829
|
+
if (Option.isSome(shifted)) {
|
|
830
|
+
const base = renderCzech(shifted.value.baseRange);
|
|
831
|
+
const entry = units.find((unit) => unit.unit === shifted.value.unit);
|
|
832
|
+
if (Option.isSome(base) && entry !== void 0) {
|
|
833
|
+
const amount = Math.abs(shifted.value.amount);
|
|
834
|
+
const pastNoun = amount === 1 ? entry.pastSingular : entry.pastPlural;
|
|
835
|
+
const noun = shifted.value.amount < 0 ? pastNoun : countNoun(amount, entry);
|
|
836
|
+
const direction = shifted.value.amount < 0 ? "před" : "za";
|
|
837
|
+
return Option.some(`${base.value} ${direction} ${amount} ${noun}`);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
673
840
|
const offset = calendarPeriodOffset(range);
|
|
674
841
|
if (Option.isSome(offset) && Math.abs(offset.value.amount) > 1) {
|
|
675
842
|
const entry = units.find((unit) => unit.unit === offset.value.unit);
|
|
@@ -701,6 +868,8 @@ const CzechContribution = new BaseLanguageContribution({
|
|
|
701
868
|
locale: "cs",
|
|
702
869
|
vocabulary: [
|
|
703
870
|
...months,
|
|
871
|
+
...weekdays,
|
|
872
|
+
...weekdayGenitives,
|
|
704
873
|
...monthAbbreviations.flatMap((aliases) => aliases),
|
|
705
874
|
...units.flatMap((entry) => [
|
|
706
875
|
entry.singular,
|
|
@@ -731,8 +900,8 @@ const CzechContribution = new BaseLanguageContribution({
|
|
|
731
900
|
"začátek",
|
|
732
901
|
"zbytek"
|
|
733
902
|
],
|
|
734
|
-
normalize:
|
|
735
|
-
correct:
|
|
903
|
+
normalize: normalizeCzech,
|
|
904
|
+
correct: correctCzech,
|
|
736
905
|
parseExact: parseCzech,
|
|
737
906
|
suggest: suggestCzech,
|
|
738
907
|
render: renderCzech
|