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 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-math filters and back. It supports open ranges, autocomplete, typo correction, and eight languages.
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`. The API can change while Effect v4 has release-candidate status.
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 ESM. Install both packages as direct dependencies.
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 entry.
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 from its own package entry. This keeps unused languages out of your bundle.
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` when one application needs more than one language:
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 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.
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 canonical phrases and their semantic ranges.
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"` for conservative correction:
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 successful value contains its `quality`, applied `corrections`, and any semantic `alternatives`. Strict mode is the default and does not correct the input.
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 relative ranges
129
+ ### Exclude future ranges
130
130
 
131
- Set `allowFuture` to `false` to reject explicit relative future ranges:
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 period-to-date ranges.
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 resolve relative expressions:
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 does not use the host time zone without your decision.
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 expressions
179
+ ## Supported input
180
180
 
181
- Supported input families include:
181
+ You can parse:
182
182
 
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;
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
- - open ranges, such as `since January 2025` and `from January 12`;
190
- - compositional boundaries, such as `the day before January 12`;
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 | 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` |
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
- Complete calendar periods use a half-open interval: the lower bound is inclusive and the next period start is exclusive. Weeks start on Monday.
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 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 |
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
- let cursor = 0;
19
- let expression;
20
- let fixedAnchor = false;
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 === "/") {
@@ -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 canonical BCP 47 base locale identifier" })).annotate({ identifier: "Locale" });
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 entries = yield* Ref.make([]);
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: "Invalid plugin identifier or contribution metadata"
56
+ message: "The plugin name or language settings are invalid"
54
57
  });
55
58
  const token = Symbol(pluginId);
56
- yield* Effect.acquireRelease(Ref.modify(entries, (current) => {
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), Array.append(current, {
66
- token,
67
- pluginId,
68
- contribution
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(entries, (items) => Array.filter(items, (entry) => entry.token !== registeredToken)));
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 = compileLanguage(canonical.value, yield* Ref.get(entries));
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 identifiers must be unique"
125
+ message: "Plugin names must be unique"
106
126
  });
107
127
  const registry = yield* createRegistry();
108
128
  const context = { register: registry.register };
@@ -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 parseBasePeriod = (input) => {
410
- const absoluteDate = absoluteDatePeriod(input, "cs");
411
- if (Option.isSome(absoluteDate)) return absoluteDate;
412
- const namedDate = parseNamedDate(input);
413
- if (Option.isSome(namedDate)) return namedDate;
414
- const quarter = parseQuarter(input);
415
- if (Option.isSome(quarter)) return quarter;
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
- return Option.none();
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 period = parseBasePeriod(textAt(edge.value, 2));
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
- return parseBasePeriod(wrapper === void 0 ? input : input.slice(wrapper.length));
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(candidate(periodRange(relativePeriod(unit, direction, canonical)), canonical));
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"]], ["od dneška do ", "mezi dneškem a "], parsePeriod, (period) => `od ${period} do dneška`, (period) => `od dneška do ${period}`);
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: normalizeNaturalText,
735
- correct: correctWhitespaceSeparatedText,
903
+ normalize: normalizeCzech,
904
+ correct: correctCzech,
736
905
  parseExact: parseCzech,
737
906
  suggest: suggestCzech,
738
907
  render: renderCzech