chronolizer 0.1.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/dist/index.mjs ADDED
@@ -0,0 +1,2305 @@
1
+ import { Array as Array$1, Context, Data, DateTime, Effect, Layer, Match, Option, Order, Ref, Result, Schema, SchemaIssue, SchemaTransformation, String as String$1 } from "effect";
2
+ //#region src/ast/schemas.ts
3
+ const Unit = Schema.Literals([
4
+ "day",
5
+ "week",
6
+ "month",
7
+ "quarter",
8
+ "year"
9
+ ]);
10
+ const isLeapYear = (year) => year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
11
+ const daysInMonth = (year, month) => {
12
+ switch (month) {
13
+ case 2: return isLeapYear(year) ? 29 : 28;
14
+ case 4:
15
+ case 6:
16
+ case 9:
17
+ case 11: return 30;
18
+ default: return 31;
19
+ }
20
+ };
21
+ const isIsoDate = (value) => {
22
+ if (Option.isNone(String$1.match(/^\d{4}-\d{2}-\d{2}$/)(value))) return false;
23
+ const year = Number(value.slice(0, 4));
24
+ const month = Number(value.slice(5, 7));
25
+ const day = Number(value.slice(8, 10));
26
+ return month >= 1 && month <= 12 && day >= 1 && day <= daysInMonth(year, month);
27
+ };
28
+ const IsoDate = Schema.String.check(Schema.makeFilter(isIsoDate, { expected: "an ISO calendar date (YYYY-MM-DD)" })).annotate({ identifier: "IsoDate" });
29
+ const Now = Schema.TaggedStruct("Now", {});
30
+ const DateLiteral = Schema.TaggedStruct("DateLiteral", { value: IsoDate });
31
+ const ShiftAmount = Schema.Int.check(Schema.isBetween({
32
+ minimum: Number.MIN_SAFE_INTEGER,
33
+ maximum: Number.MAX_SAFE_INTEGER
34
+ }));
35
+ const Shift = Schema.TaggedStruct("Shift", {
36
+ base: Schema.suspend(() => InstantExpr),
37
+ amount: ShiftAmount,
38
+ unit: Unit
39
+ });
40
+ const StartOf = Schema.TaggedStruct("StartOf", {
41
+ base: Schema.suspend(() => InstantExpr),
42
+ unit: Unit
43
+ });
44
+ const InstantExpr = Schema.suspend(() => Schema.Union([
45
+ Now,
46
+ DateLiteral,
47
+ Shift,
48
+ StartOf
49
+ ])).annotate({ identifier: "InstantExpr" });
50
+ const GreaterThan = Schema.TaggedStruct("GreaterThan", { value: InstantExpr });
51
+ const GreaterThanOrEqual = Schema.TaggedStruct("GreaterThanOrEqual", { value: InstantExpr });
52
+ const LessThan = Schema.TaggedStruct("LessThan", { value: InstantExpr });
53
+ const LessThanOrEqual = Schema.TaggedStruct("LessThanOrEqual", { value: InstantExpr });
54
+ const LowerBound = Schema.Union([GreaterThan, GreaterThanOrEqual]);
55
+ const UpperBound = Schema.Union([LessThan, LessThanOrEqual]);
56
+ const BoundedDateRange = Schema.TaggedStruct("DateRange", {
57
+ lower: LowerBound,
58
+ upper: UpperBound
59
+ });
60
+ const LowerOpenDateRange = Schema.TaggedStruct("DateRange", {
61
+ lower: LowerBound,
62
+ upper: Schema.optionalKey(Schema.Never)
63
+ });
64
+ const UpperOpenDateRange = Schema.TaggedStruct("DateRange", {
65
+ lower: Schema.optionalKey(Schema.Never),
66
+ upper: UpperBound
67
+ });
68
+ const DateRangeExpr = Schema.Union([
69
+ BoundedDateRange,
70
+ LowerOpenDateRange,
71
+ UpperOpenDateRange
72
+ ]).annotate({ identifier: "DateRangeExpr" });
73
+ //#endregion
74
+ //#region src/ast/constructors.ts
75
+ const now = () => Now.make({});
76
+ const dateLiteral = (value) => DateLiteral.make({ value });
77
+ const shift = (base, amount, unit) => Shift.make({
78
+ base,
79
+ amount,
80
+ unit
81
+ });
82
+ const startOf = (base, unit) => StartOf.make({
83
+ base,
84
+ unit
85
+ });
86
+ const greaterThan = (value) => GreaterThan.make({ value });
87
+ const greaterThanOrEqual = (value) => GreaterThanOrEqual.make({ value });
88
+ const lessThan = (value) => LessThan.make({ value });
89
+ const lessThanOrEqual = (value) => LessThanOrEqual.make({ value });
90
+ const boundedRange = (lower, upper) => DateRangeExpr.make({
91
+ lower,
92
+ upper
93
+ });
94
+ const lowerOpenRange = (lower) => DateRangeExpr.make({ lower });
95
+ const upperOpenRange = (upper) => DateRangeExpr.make({ upper });
96
+ //#endregion
97
+ //#region src/ast/fold.ts
98
+ const foldInstant = (expression, algebra) => {
99
+ switch (expression._tag) {
100
+ case "Now": return algebra.now();
101
+ case "DateLiteral": return algebra.dateLiteral(expression.value);
102
+ case "Shift": return algebra.shift(foldInstant(expression.base, algebra), expression.amount, expression.unit);
103
+ case "StartOf": return algebra.startOf(foldInstant(expression.base, algebra), expression.unit);
104
+ }
105
+ };
106
+ const shiftOffset = (base, amount, unit) => {
107
+ switch (unit) {
108
+ case "year": return {
109
+ ...base,
110
+ months: base.months + amount * 12
111
+ };
112
+ case "quarter": return {
113
+ ...base,
114
+ months: base.months + amount * 3
115
+ };
116
+ case "month": return {
117
+ ...base,
118
+ months: base.months + amount
119
+ };
120
+ case "week": return {
121
+ ...base,
122
+ days: base.days + amount * 7
123
+ };
124
+ case "day": return {
125
+ ...base,
126
+ days: base.days + amount
127
+ };
128
+ }
129
+ };
130
+ const containsPositiveShiftInstant = (expression) => {
131
+ const offset = foldInstant(expression, {
132
+ now: () => ({
133
+ months: 0,
134
+ days: 0
135
+ }),
136
+ dateLiteral: () => ({
137
+ months: 0,
138
+ days: 0
139
+ }),
140
+ shift: shiftOffset,
141
+ startOf: (base) => base
142
+ });
143
+ return offset.months > 0 || offset.days > 0;
144
+ };
145
+ const containsPositiveShift = (range) => range.lower !== void 0 && containsPositiveShiftInstant(range.lower.value) || range.upper !== void 0 && containsPositiveShiftInstant(range.upper.value);
146
+ //#endregion
147
+ //#region src/ast/normalize.ts
148
+ const normalizeInstant = (expression) => Match.valueTags(expression, {
149
+ Now: () => now(),
150
+ DateLiteral: (literal) => dateLiteral(literal.value),
151
+ StartOf: (operation) => startOf(normalizeInstant(operation.base), operation.unit),
152
+ Shift: (operation) => {
153
+ const base = normalizeInstant(operation.base);
154
+ if (operation.amount === 0) return base;
155
+ if (base._tag === "Shift" && base.unit === operation.unit) {
156
+ const amount = base.amount + operation.amount;
157
+ if (Number.isSafeInteger(amount)) return normalizeInstant(shift(base.base, amount, operation.unit));
158
+ }
159
+ return shift(base, operation.amount, operation.unit);
160
+ }
161
+ });
162
+ const normalizeLower = (bound) => Match.valueTags(bound, {
163
+ GreaterThan: (value) => greaterThan(normalizeInstant(value.value)),
164
+ GreaterThanOrEqual: (value) => greaterThanOrEqual(normalizeInstant(value.value))
165
+ });
166
+ const normalizeUpper = (bound) => Match.valueTags(bound, {
167
+ LessThan: (value) => lessThan(normalizeInstant(value.value)),
168
+ LessThanOrEqual: (value) => lessThanOrEqual(normalizeInstant(value.value))
169
+ });
170
+ const normalizeRange = (range) => {
171
+ if (range.lower !== void 0 && range.upper !== void 0) return boundedRange(normalizeLower(range.lower), normalizeUpper(range.upper));
172
+ if (range.lower !== void 0) return lowerOpenRange(normalizeLower(range.lower));
173
+ return upperOpenRange(normalizeUpper(range.upper));
174
+ };
175
+ //#endregion
176
+ //#region src/filter/errors.ts
177
+ var FilterExpressionParseError = class extends Schema.TaggedError()("FilterExpressionParseError", {
178
+ input: Schema.String,
179
+ offset: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
180
+ expected: Schema.String
181
+ }) {};
182
+ var InvalidDateFilterError = class extends Schema.TaggedError()("InvalidDateFilterError", { message: Schema.String }) {};
183
+ //#endregion
184
+ //#region src/filter/expression.ts
185
+ const unitFromSymbol = (symbol) => {
186
+ switch (symbol) {
187
+ case "d": return "day";
188
+ case "w": return "week";
189
+ case "M": return "month";
190
+ case "q": return "quarter";
191
+ case "y": return "year";
192
+ default: return;
193
+ }
194
+ };
195
+ const symbolFromUnit = (unit) => {
196
+ switch (unit) {
197
+ case "day": return "d";
198
+ case "week": return "w";
199
+ case "month": return "M";
200
+ case "quarter": return "q";
201
+ case "year": return "y";
202
+ }
203
+ };
204
+ const failAt = (input, offset, expected) => Effect.fail(new FilterExpressionParseError({
205
+ input,
206
+ offset,
207
+ expected
208
+ }));
209
+ const isDigit = (value) => value >= "0" && value <= "9";
210
+ const isFirstPositiveDigit = (value) => value >= "1" && value <= "9";
211
+ const parseInstantExpression = Effect.fn(function* (input) {
212
+ let cursor = 0;
213
+ let expression;
214
+ let fixedAnchor = false;
215
+ if (input.startsWith("now")) {
216
+ expression = now();
217
+ cursor = 3;
218
+ } else {
219
+ const candidate = input.slice(0, 10);
220
+ if (!isIsoDate(candidate)) return yield* failAt(input, 0, "\"now\" or an ISO date (YYYY-MM-DD)");
221
+ expression = dateLiteral(candidate);
222
+ cursor = 10;
223
+ fixedAnchor = true;
224
+ }
225
+ if (fixedAnchor && cursor < input.length) {
226
+ if (input.slice(cursor, cursor + 2) !== "||") return yield* failAt(input, cursor, "\"||\" before date operations");
227
+ cursor += 2;
228
+ if (cursor === input.length) return yield* failAt(input, cursor, "an operation beginning with \"+\", \"-\", or \"/\"");
229
+ }
230
+ while (cursor < input.length) {
231
+ const operator = input[cursor];
232
+ if (operator === "/") {
233
+ const unit = unitFromSymbol(input[cursor + 1] ?? "");
234
+ if (unit === void 0) return yield* failAt(input, cursor + 1, "a date unit: d, w, M, q, or y");
235
+ expression = startOf(expression, unit);
236
+ cursor += 2;
237
+ continue;
238
+ }
239
+ if (operator !== "+" && operator !== "-") return yield* failAt(input, cursor, "an operation beginning with \"+\", \"-\", or \"/\"");
240
+ const amountStart = cursor + 1;
241
+ if (!isFirstPositiveDigit(input[amountStart] ?? "")) return yield* failAt(input, amountStart, "a positive integer without a leading zero");
242
+ cursor = amountStart + 1;
243
+ while (cursor < input.length && isDigit(input[cursor] ?? "")) cursor += 1;
244
+ const amount = Number(input.slice(amountStart, cursor));
245
+ if (!Number.isSafeInteger(amount)) return yield* failAt(input, amountStart, "a safe positive integer");
246
+ const unit = unitFromSymbol(input[cursor] ?? "");
247
+ if (unit === void 0) return yield* failAt(input, cursor, "a date unit: d, w, M, q, or y");
248
+ expression = shift(expression, operator === "+" ? amount : -amount, unit);
249
+ cursor += 1;
250
+ }
251
+ return expression;
252
+ });
253
+ const appendOperation = (base, operation) => ({
254
+ text: `${base.text}${base.fixedAnchor ? "||" : ""}${operation}`,
255
+ fixedAnchor: false
256
+ });
257
+ const formatInstantExpression = (expression) => foldInstant(normalizeInstant(expression), {
258
+ now: () => ({
259
+ text: "now",
260
+ fixedAnchor: false
261
+ }),
262
+ dateLiteral: (value) => ({
263
+ text: value,
264
+ fixedAnchor: true
265
+ }),
266
+ shift: (base, amount, unit) => appendOperation(base, `${amount < 0 ? "-" : "+"}${Math.abs(amount)}${symbolFromUnit(unit)}`),
267
+ startOf: (base, unit) => appendOperation(base, `/${symbolFromUnit(unit)}`)
268
+ }).text;
269
+ //#endregion
270
+ //#region src/filter/schema.ts
271
+ const DateExpressionString = Schema.String.check(Schema.isMinLength(1)).annotate({ identifier: "DateExpressionString" });
272
+ const DateFilter = Schema.Struct({
273
+ gt: Schema.optionalKey(DateExpressionString),
274
+ gte: Schema.optionalKey(DateExpressionString),
275
+ lt: Schema.optionalKey(DateExpressionString),
276
+ lte: Schema.optionalKey(DateExpressionString)
277
+ }).check(Schema.makeFilter((filter) => !(filter.gt !== void 0 && filter.gte !== void 0), { expected: "at most one lower date bound" }), Schema.makeFilter((filter) => !(filter.lt !== void 0 && filter.lte !== void 0), { expected: "at most one upper date bound" }), Schema.makeFilter((filter) => filter.gt !== void 0 || filter.gte !== void 0 || filter.lt !== void 0 || filter.lte !== void 0, { expected: "at least one date bound" })).annotate({ identifier: "DateFilter" });
278
+ //#endregion
279
+ //#region src/filter/codec.ts
280
+ const parseLower = (filter) => {
281
+ if (filter.gt !== void 0) return Effect.map(parseInstantExpression(filter.gt), greaterThan);
282
+ if (filter.gte !== void 0) return Effect.map(parseInstantExpression(filter.gte), greaterThanOrEqual);
283
+ return Effect.void;
284
+ };
285
+ const parseUpper = (filter) => {
286
+ if (filter.lt !== void 0) return Effect.map(parseInstantExpression(filter.lt), lessThan);
287
+ if (filter.lte !== void 0) return Effect.map(parseInstantExpression(filter.lte), lessThanOrEqual);
288
+ return Effect.void;
289
+ };
290
+ const parseFilter = Effect.fn("chronolizer.parseFilter")(function* (filter) {
291
+ const lower = yield* parseLower(filter);
292
+ const upper = yield* parseUpper(filter);
293
+ if (lower !== void 0 && upper !== void 0) return normalizeRange(boundedRange(lower, upper));
294
+ if (lower !== void 0) return normalizeRange(lowerOpenRange(lower));
295
+ if (upper !== void 0) return normalizeRange(upperOpenRange(upper));
296
+ return yield* new InvalidDateFilterError({ message: "A date filter must contain at least one bound" });
297
+ });
298
+ const formatLower = Match.typeTags()({
299
+ GreaterThan: (bound) => DateFilter.make({ gt: formatInstantExpression(bound.value) }),
300
+ GreaterThanOrEqual: (bound) => DateFilter.make({ gte: formatInstantExpression(bound.value) })
301
+ });
302
+ const formatUpper = Match.typeTags()({
303
+ LessThan: (bound) => DateFilter.make({ lt: formatInstantExpression(bound.value) }),
304
+ LessThanOrEqual: (bound) => DateFilter.make({ lte: formatInstantExpression(bound.value) })
305
+ });
306
+ const formatFilter = (range) => {
307
+ const normalized = normalizeRange(range);
308
+ if (normalized.lower !== void 0 && normalized.upper !== void 0) return DateFilter.make({
309
+ ...formatLower(normalized.lower),
310
+ ...formatUpper(normalized.upper)
311
+ });
312
+ if (normalized.lower !== void 0) return formatLower(normalized.lower);
313
+ return formatUpper(normalized.upper);
314
+ };
315
+ const rangeKey = (range) => {
316
+ const filter = formatFilter(range);
317
+ return `${filter.gt ?? ""}|${filter.gte ?? ""}|${filter.lt ?? ""}|${filter.lte ?? ""}`;
318
+ };
319
+ const completePeriod = (start, end) => boundedRange(greaterThanOrEqual(start), lessThan(end));
320
+ //#endregion
321
+ //#region src/filter/transformation.ts
322
+ const expressionIssue = (input, offset, expected) => new SchemaIssue.Forbidden({ message: `Invalid date expression at offset ${offset}: expected ${expected}; input: ${input}` });
323
+ const InstantExpressionFromString = Schema.String.pipe(Schema.decodeTo(InstantExpr, SchemaTransformation.transformOrFail({
324
+ decode: (input) => Effect.mapError(parseInstantExpression(input), (error) => expressionIssue(error.input, error.offset, error.expected)),
325
+ encode: (expression) => Effect.succeed(formatInstantExpression(expression))
326
+ })));
327
+ const DateRangeFromFilter = DateFilter.pipe(Schema.decodeTo(DateRangeExpr, SchemaTransformation.transformOrFail({
328
+ decode: (filter) => Effect.mapError(parseFilter(filter), (error) => new SchemaIssue.Forbidden({ message: error._tag === "FilterExpressionParseError" ? `Invalid filter expression at offset ${error.offset}: expected ${error.expected}` : error.message })),
329
+ encode: (range) => Effect.succeed(formatFilter(range))
330
+ })));
331
+ //#endregion
332
+ //#region src/language/errors.ts
333
+ var UnsupportedLocaleError = class extends Schema.TaggedError()("UnsupportedLocaleError", { locale: Schema.String }) {};
334
+ var LanguageConflictError = class extends Schema.TaggedError()("LanguageConflictError", {
335
+ locale: Schema.String,
336
+ firstPluginId: Schema.String,
337
+ secondPluginId: Schema.String,
338
+ message: Schema.String
339
+ }) {};
340
+ var LanguageRegistrationError = class extends Schema.TaggedError()("LanguageRegistrationError", {
341
+ pluginId: Schema.String,
342
+ locale: Schema.String,
343
+ message: Schema.String
344
+ }) {};
345
+ var NaturalLanguageParseError = class extends Schema.TaggedError()("NaturalLanguageParseError", {
346
+ input: Schema.String,
347
+ locale: Schema.String,
348
+ message: Schema.String
349
+ }) {};
350
+ var AmbiguousNaturalLanguageError = class extends Schema.TaggedError()("AmbiguousNaturalLanguageError", {
351
+ input: Schema.String,
352
+ locale: Schema.String,
353
+ alternatives: Schema.Array(Schema.String)
354
+ }) {};
355
+ var NaturalLanguageRenderError = class extends Schema.TaggedError()("NaturalLanguageRenderError", {
356
+ locale: Schema.String,
357
+ message: Schema.String
358
+ }) {};
359
+ //#endregion
360
+ //#region src/language/model.ts
361
+ const ParseQuality = Schema.Literals([
362
+ "exact",
363
+ "corrected",
364
+ "ambiguous"
365
+ ]);
366
+ const Correction = Schema.Struct({
367
+ original: Schema.String,
368
+ replacement: Schema.String,
369
+ distance: Schema.Int.check(Schema.isGreaterThanOrEqualTo(1)),
370
+ offset: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))
371
+ });
372
+ const NaturalCorrectionCandidate = Schema.Struct({
373
+ text: Schema.String,
374
+ corrections: Schema.Array(Correction),
375
+ cost: Schema.Int.check(Schema.isGreaterThanOrEqualTo(1))
376
+ });
377
+ const NaturalAlternative = Schema.Struct({
378
+ canonical: Schema.String,
379
+ range: DateRangeExpr
380
+ });
381
+ const NaturalParseResult = Schema.Struct({
382
+ range: DateRangeExpr,
383
+ quality: ParseQuality,
384
+ corrections: Schema.Array(Correction),
385
+ alternatives: Schema.Array(NaturalAlternative)
386
+ });
387
+ const NaturalCandidate = Schema.Struct({
388
+ range: DateRangeExpr,
389
+ canonical: Schema.String
390
+ });
391
+ var BaseLanguageContribution = class extends Data.TaggedClass("BaseLanguage") {};
392
+ var LanguageExtensionContribution = class extends Data.TaggedClass("LanguageExtension") {};
393
+ const canonicalBaseLocale = (input) => Result.getSuccess(Result.try(() => new Intl.Locale(input).baseName));
394
+ const Locale = Schema.String.check(Schema.makeFilter((value) => Option.contains(canonicalBaseLocale(value), value), { expected: "a canonical BCP 47 base locale identifier" })).annotate({ identifier: "Locale" });
395
+ const BaseLanguageMetadata = Schema.TaggedStruct("BaseLanguage", {
396
+ locale: Locale,
397
+ vocabulary: Schema.Array(Schema.String)
398
+ });
399
+ const LanguageExtensionMetadata = Schema.TaggedStruct("LanguageExtension", {
400
+ locale: Locale,
401
+ priority: Schema.Int,
402
+ vocabulary: Schema.Array(Schema.String)
403
+ });
404
+ const LanguageContributionMetadata = Schema.Union([BaseLanguageMetadata, LanguageExtensionMetadata]);
405
+ //#endregion
406
+ //#region src/natural/text.ts
407
+ const normalizeNaturalText = (input, locale) => input.normalize("NFKC").toLocaleLowerCase(locale).trim().replace(/\s+/gu, " ");
408
+ const naturalWords = (input) => input.length === 0 ? [] : input.split(" ");
409
+ //#endregion
410
+ //#region src/language/registry.ts
411
+ var LanguageRegistry = class extends Context.Service()("chronolizer/LanguageRegistry") {};
412
+ const metadataOf = (contribution) => Match.valueTags(contribution, {
413
+ BaseLanguage: (base) => BaseLanguageMetadata.make({
414
+ locale: base.locale,
415
+ vocabulary: base.vocabulary
416
+ }),
417
+ LanguageExtension: (extension) => LanguageExtensionMetadata.make({
418
+ locale: extension.locale,
419
+ priority: extension.priority,
420
+ vocabulary: extension.vocabulary
421
+ })
422
+ });
423
+ const localeCandidates = (locale) => {
424
+ const candidates = [locale];
425
+ let parent = locale;
426
+ while (parent.includes("-")) {
427
+ parent = parent.slice(0, parent.lastIndexOf("-"));
428
+ candidates.push(parent);
429
+ }
430
+ return candidates;
431
+ };
432
+ const compileLanguage = (locale, registered) => {
433
+ const locales = localeCandidates(locale);
434
+ const base = Array$1.sort(Array$1.filterMap(registered, (entry) => {
435
+ const contribution = entry.contribution;
436
+ return contribution._tag === "BaseLanguage" && locales.includes(contribution.locale) ? Result.succeed({
437
+ pluginId: entry.pluginId,
438
+ contribution
439
+ }) : Result.failVoid;
440
+ }), Order.mapInput(Order.Number, (entry) => locales.indexOf(entry.contribution.locale)))[0];
441
+ if (base === void 0) return Option.none();
442
+ const extensions = Array$1.sort(Array$1.filterMap(registered, (entry) => {
443
+ const contribution = entry.contribution;
444
+ return contribution._tag === "LanguageExtension" && locales.includes(contribution.locale) ? Result.succeed({
445
+ pluginId: entry.pluginId,
446
+ contribution
447
+ }) : Result.failVoid;
448
+ }), Order.combine(Order.mapInput(Order.flip(Order.Number), (entry) => entry.contribution.priority), Order.mapInput(Order.String, (entry) => entry.pluginId)));
449
+ const vocabulary = Object.freeze(Array$1.dedupe([...base.contribution.vocabulary, ...Array$1.flatMap(extensions, (entry) => entry.contribution.vocabulary)]));
450
+ const parsers = Object.freeze([...extensions.map((entry) => entry.contribution.parseExact), base.contribution.parseExact]);
451
+ const parseExact = (input) => {
452
+ const candidates = [];
453
+ for (const parser of parsers) {
454
+ const candidate = parser(input);
455
+ if (Option.isSome(candidate)) candidates.push(candidate.value);
456
+ }
457
+ return candidates;
458
+ };
459
+ return Option.some(Object.freeze({
460
+ locale: base.contribution.locale,
461
+ vocabulary,
462
+ normalize: base.contribution.normalize ?? normalizeNaturalText,
463
+ correct: base.contribution.correct,
464
+ parseExact,
465
+ render: base.contribution.render
466
+ }));
467
+ };
468
+ const createRegistry = Effect.fn(function* () {
469
+ const entries = yield* Ref.make([]);
470
+ const register = Effect.fn(function* (pluginId, contribution) {
471
+ if (pluginId.length === 0 || !Schema.is(LanguageContributionMetadata)(metadataOf(contribution))) return yield* new LanguageRegistrationError({
472
+ pluginId,
473
+ locale: contribution.locale,
474
+ message: "Invalid plugin identifier or contribution metadata"
475
+ });
476
+ const token = Symbol(pluginId);
477
+ yield* Effect.acquireRelease(Ref.modify(entries, (current) => {
478
+ const conflictingBase = Array$1.findFirst(current, (entry) => contribution._tag === "BaseLanguage" && entry.contribution._tag === "BaseLanguage" && entry.contribution.locale === contribution.locale);
479
+ return Option.match(conflictingBase, {
480
+ onNone: () => [Result.succeed(token), Array$1.append(current, {
481
+ token,
482
+ pluginId,
483
+ contribution
484
+ })],
485
+ onSome: (conflict) => [Result.fail(new LanguageConflictError({
486
+ locale: contribution.locale,
487
+ firstPluginId: conflict.pluginId,
488
+ secondPluginId: pluginId,
489
+ message: "Only one base language can be registered for a locale"
490
+ })), current]
491
+ });
492
+ }).pipe(Effect.flatMap((result) => Effect.fromResult(result))), (registeredToken) => Ref.update(entries, (items) => Array$1.filter(items, (entry) => entry.token !== registeredToken)));
493
+ });
494
+ const resolve = Effect.fn(function* (locale) {
495
+ const canonical = canonicalBaseLocale(locale);
496
+ if (Option.isSome(canonical)) {
497
+ const compiled = compileLanguage(canonical.value, yield* Ref.get(entries));
498
+ if (Option.isSome(compiled)) return compiled.value;
499
+ }
500
+ return yield* new UnsupportedLocaleError({ locale });
501
+ });
502
+ return LanguageRegistry.of({
503
+ register,
504
+ resolve
505
+ });
506
+ });
507
+ const LanguageRegistryLayer = Layer.effect(LanguageRegistry, createRegistry());
508
+ const duplicatePluginId = (plugins) => {
509
+ const ids = /* @__PURE__ */ new Set();
510
+ for (const plugin of plugins) {
511
+ if (ids.has(plugin.id)) return plugin.id;
512
+ ids.add(plugin.id);
513
+ }
514
+ };
515
+ const createPluginRegistry = Effect.fn(function* (plugins) {
516
+ const duplicate = duplicatePluginId(plugins);
517
+ if (duplicate !== void 0) return yield* new LanguageRegistrationError({
518
+ pluginId: duplicate,
519
+ locale: "*",
520
+ message: "Plugin identifiers must be unique"
521
+ });
522
+ const registry = yield* createRegistry();
523
+ const context = { register: registry.register };
524
+ const ordered = Array$1.sortWith(plugins, (plugin) => plugin.id, Order.String);
525
+ for (const plugin of ordered) yield* plugin.effect(context);
526
+ return registry;
527
+ });
528
+ const languagePluginsLayer = (plugins) => Layer.effect(LanguageRegistry, createPluginRegistry(plugins));
529
+ const defineLanguagePlugin = (plugin) => plugin;
530
+ //#endregion
531
+ //#region src/natural/correction.ts
532
+ const isProtectedToken = (word) => Option.isSome(String$1.match(/^\d+$/u)(word)) || Option.isSome(String$1.match(/^\d{4}-\d{2}-\d{2}$/u)(word)) || word.length <= 3;
533
+ const damerauLevenshtein = (left, right) => {
534
+ const rows = left.length + 1;
535
+ const columns = right.length + 1;
536
+ const matrix = Array.from({ length: rows }, () => Array(columns).fill(0));
537
+ for (let row = 0; row < rows; row += 1) matrix[row][0] = row;
538
+ for (let column = 0; column < columns; column += 1) matrix[0][column] = column;
539
+ for (let row = 1; row < rows; row += 1) for (let column = 1; column < columns; column += 1) {
540
+ const substitution = left[row - 1] === right[column - 1] ? 0 : 1;
541
+ matrix[row][column] = Math.min(matrix[row - 1][column] + 1, matrix[row][column - 1] + 1, matrix[row - 1][column - 1] + substitution);
542
+ if (row > 1 && column > 1 && left[row - 1] === right[column - 2] && left[row - 2] === right[column - 1]) matrix[row][column] = Math.min(matrix[row][column], matrix[row - 2][column - 2] + 1);
543
+ }
544
+ return matrix[left.length][right.length];
545
+ };
546
+ const replacementsFor = (word, vocabulary) => {
547
+ if (vocabulary.includes(word)) return [{
548
+ word,
549
+ distance: 0
550
+ }];
551
+ if (isProtectedToken(word)) return [];
552
+ const maximum = word.length >= 6 ? 2 : 1;
553
+ const matches = vocabulary.filter((candidate) => Math.abs(candidate.length - word.length) <= maximum).map((candidate) => ({
554
+ word: candidate,
555
+ distance: damerauLevenshtein(word, candidate)
556
+ })).filter((candidate) => candidate.distance <= maximum);
557
+ if (matches.length === 0) return [];
558
+ const minimum = Math.min(...matches.map((candidate) => candidate.distance));
559
+ return matches.filter((candidate) => candidate.distance === minimum).slice(0, 4);
560
+ };
561
+ const correctWhitespaceSeparatedText = (input, vocabulary) => {
562
+ const words = naturalWords(input);
563
+ let partials = [{
564
+ words: [],
565
+ corrections: [],
566
+ cost: 0,
567
+ offset: 0
568
+ }];
569
+ for (const word of words) {
570
+ const replacements = replacementsFor(word, vocabulary);
571
+ if (replacements.length === 0) return [];
572
+ const next = [];
573
+ for (const partial of partials) for (const replacement of replacements) {
574
+ const correction = replacement.distance === 0 ? partial.corrections : [...partial.corrections, Correction.make({
575
+ original: word,
576
+ replacement: replacement.word,
577
+ distance: replacement.distance,
578
+ offset: partial.offset
579
+ })];
580
+ next.push({
581
+ words: [...partial.words, replacement.word],
582
+ corrections: correction,
583
+ cost: partial.cost + replacement.distance,
584
+ offset: partial.offset + word.length + 1
585
+ });
586
+ }
587
+ partials = next.slice(0, 32);
588
+ }
589
+ return partials.filter((partial) => partial.corrections.length > 0).map((partial) => NaturalCorrectionCandidate.make({
590
+ text: partial.words.join(" "),
591
+ corrections: partial.corrections,
592
+ cost: partial.cost
593
+ }));
594
+ };
595
+ //#endregion
596
+ //#region src/locales/shared.ts
597
+ const Period = Schema.Struct({
598
+ start: InstantExpr,
599
+ end: InstantExpr,
600
+ canonical: Schema.String
601
+ });
602
+ const periodRange = (period) => boundedRange(greaterThanOrEqual(period.start), lessThan(period.end));
603
+ const periodToDateRange = (unit) => boundedRange(greaterThanOrEqual(startOf(now(), unit)), lessThanOrEqual(now()));
604
+ const fromNowRange = () => lowerOpenRange(greaterThanOrEqual(now()));
605
+ const untilNowRange = () => upperOpenRange(lessThanOrEqual(now()));
606
+ const remainingPeriodRange = (unit) => {
607
+ const start = startOf(now(), unit);
608
+ return boundedRange(greaterThanOrEqual(now()), lessThan(shift(start, 1, unit)));
609
+ };
610
+ const periodStartDay = (period, canonical) => Period.make({
611
+ start: period.start,
612
+ end: shift(period.start, 1, "day"),
613
+ canonical
614
+ });
615
+ const periodEndDay = (period, canonical) => Period.make({
616
+ start: shift(period.end, -1, "day"),
617
+ end: period.end,
618
+ canonical
619
+ });
620
+ const relativeWeekend = (direction, canonical) => {
621
+ const weekBase = direction === 0 ? now() : shift(now(), direction, "week");
622
+ const weekStart = startOf(weekBase, "week");
623
+ const start = shift(weekStart, 5, "day");
624
+ return Period.make({
625
+ start,
626
+ end: shift(start, 2, "day"),
627
+ canonical
628
+ });
629
+ };
630
+ const TrailingCount = Schema.Int.check(Schema.isBetween({
631
+ minimum: 1,
632
+ maximum: Number.MAX_SAFE_INTEGER
633
+ }));
634
+ const TrailingPeriod = Schema.Struct({
635
+ amount: TrailingCount,
636
+ unit: Unit
637
+ });
638
+ const FuturePeriod = Schema.Struct({
639
+ amount: TrailingCount,
640
+ unit: Unit
641
+ });
642
+ const CalendarPeriodOffset = Schema.Struct({
643
+ amount: Schema.Int,
644
+ unit: Unit
645
+ });
646
+ const validYear = (value) => {
647
+ const year = Number(value);
648
+ return Number.isInteger(year) && year >= 1 && year <= 9998 ? year : void 0;
649
+ };
650
+ const parseTrailingCount = (value) => {
651
+ const amount = Number(value);
652
+ return Schema.is(TrailingCount)(amount) ? Option.some(amount) : Option.none();
653
+ };
654
+ const trailingRange = (amount, unit) => boundedRange(greaterThanOrEqual(shift(now(), -amount, unit)), lessThanOrEqual(now()));
655
+ const futureRange = (amount, unit) => boundedRange(greaterThanOrEqual(now()), lessThanOrEqual(shift(now(), amount, unit)));
656
+ const trailingPeriod = (range) => {
657
+ if (range.lower?._tag !== "GreaterThanOrEqual" || range.upper?._tag !== "LessThanOrEqual" || range.lower.value._tag !== "Shift" || range.lower.value.amount >= 0 || range.lower.value.base._tag !== "Now" || range.upper.value._tag !== "Now") return Option.none();
658
+ return Option.some(TrailingPeriod.make({
659
+ amount: -range.lower.value.amount,
660
+ unit: range.lower.value.unit
661
+ }));
662
+ };
663
+ const futurePeriod = (range) => {
664
+ if (range.lower?._tag !== "GreaterThanOrEqual" || range.upper?._tag !== "LessThanOrEqual" || range.lower.value._tag !== "Now" || range.upper.value._tag !== "Shift" || range.upper.value.amount <= 0 || range.upper.value.base._tag !== "Now") return Option.none();
665
+ return Option.some(FuturePeriod.make({
666
+ amount: range.upper.value.amount,
667
+ unit: range.upper.value.unit
668
+ }));
669
+ };
670
+ const relativePeriod = (unit, direction, canonical) => {
671
+ const base = direction === 0 ? now() : shift(now(), direction, unit);
672
+ const start = startOf(base, unit);
673
+ return Period.make({
674
+ start,
675
+ end: shift(start, 1, unit),
676
+ canonical
677
+ });
678
+ };
679
+ const pad = (value) => String(value).padStart(2, "0");
680
+ const isoDate = (year, month, day) => `${String(year).padStart(4, "0")}-${pad(month)}-${pad(day)}`;
681
+ const nextDay = (year, month, day) => {
682
+ if (day < daysInMonth(year, month)) return isoDate(year, month, day + 1);
683
+ if (month < 12) return isoDate(year, month + 1, 1);
684
+ return isoDate(year + 1, 1, 1);
685
+ };
686
+ const previousDay = (year, month, day) => {
687
+ if (day > 1) return isoDate(year, month, day - 1);
688
+ if (month > 1) return isoDate(year, month - 1, daysInMonth(year, month - 1));
689
+ return isoDate(year - 1, 12, 31);
690
+ };
691
+ const fixedDatePeriod = (value, canonical) => {
692
+ const year = Number(value.slice(0, 4));
693
+ const month = Number(value.slice(5, 7));
694
+ const day = Number(value.slice(8, 10));
695
+ return Period.make({
696
+ start: dateLiteral(value),
697
+ end: dateLiteral(nextDay(year, month, day)),
698
+ canonical
699
+ });
700
+ };
701
+ const namedDatePeriod = (yearText, monthText, dayText, monthNumber, canonical) => {
702
+ const year = validYear(yearText);
703
+ const month = monthNumber(monthText);
704
+ const day = Number(dayText);
705
+ if (year === void 0 || month === void 0) return Option.none();
706
+ const value = isoDate(year, month, day);
707
+ if (!isIsoDate(value) || value === "9999-12-31") return Option.none();
708
+ return Option.some(fixedDatePeriod(value, canonical(day, month, year)));
709
+ };
710
+ const fixedMonthPeriod = (year, month, canonical) => {
711
+ const nextYear = month === 12 ? year + 1 : year;
712
+ const nextMonth = month === 12 ? 1 : month + 1;
713
+ return Period.make({
714
+ start: dateLiteral(isoDate(year, month, 1)),
715
+ end: dateLiteral(isoDate(nextYear, nextMonth, 1)),
716
+ canonical
717
+ });
718
+ };
719
+ const fixedYearPeriod = (year, canonical) => Period.make({
720
+ start: dateLiteral(isoDate(year, 1, 1)),
721
+ end: dateLiteral(isoDate(year + 1, 1, 1)),
722
+ canonical
723
+ });
724
+ const fixedQuarterPeriod = (year, quarter, canonical) => {
725
+ const month = (quarter - 1) * 3 + 1;
726
+ const nextYear = quarter === 4 ? year + 1 : year;
727
+ const nextMonth = quarter === 4 ? 1 : month + 3;
728
+ return Period.make({
729
+ start: dateLiteral(isoDate(year, month, 1)),
730
+ end: dateLiteral(isoDate(nextYear, nextMonth, 1)),
731
+ canonical
732
+ });
733
+ };
734
+ const quarterOfRelativeYear = (quarter, direction, canonical) => {
735
+ const yearBase = direction === 0 ? now() : shift(now(), direction, "year");
736
+ const yearStart = startOf(yearBase, "year");
737
+ const start = quarter === 1 ? yearStart : shift(yearStart, (quarter - 1) * 3, "month");
738
+ return Period.make({
739
+ start,
740
+ end: shift(start, 1, "quarter"),
741
+ canonical
742
+ });
743
+ };
744
+ const monthOfRelativeYear = (month, direction, canonical) => {
745
+ const yearBase = direction === 0 ? now() : shift(now(), direction, "year");
746
+ const yearStart = startOf(yearBase, "year");
747
+ const start = month === 1 ? yearStart : shift(yearStart, month - 1, "month");
748
+ return Period.make({
749
+ start,
750
+ end: shift(start, 1, "month"),
751
+ canonical
752
+ });
753
+ };
754
+ const calendarPeriodOffset = (range) => {
755
+ if (range.lower?._tag !== "GreaterThanOrEqual" || range.upper?._tag !== "LessThan" || range.lower.value._tag !== "StartOf" || range.lower.value.base._tag !== "Shift" || range.lower.value.base.amount === 0 || range.lower.value.base.base._tag !== "Now" || range.lower.value.unit !== range.lower.value.base.unit || range.upper.value._tag !== "Shift" || range.upper.value.amount !== 1 || range.upper.value.unit !== range.lower.value.unit || formatInstantExpression(range.upper.value.base) !== formatInstantExpression(range.lower.value)) return Option.none();
756
+ return Option.some(CalendarPeriodOffset.make({
757
+ amount: range.lower.value.base.amount,
758
+ unit: range.lower.value.unit
759
+ }));
760
+ };
761
+ const candidate = (range, canonical) => NaturalCandidate.make({
762
+ range,
763
+ canonical
764
+ });
765
+ const joinedNowCandidate = (input, periodToNow, nowToPeriod, parsePeriod, canonicalToNow, canonicalFromNow) => {
766
+ for (const [prefix, suffix] of periodToNow) {
767
+ if (!input.startsWith(prefix) || !input.endsWith(suffix)) continue;
768
+ const period = parsePeriod(input.slice(prefix.length, -suffix.length));
769
+ if (Option.isNone(period)) continue;
770
+ return Option.some(candidate(boundedRange(greaterThanOrEqual(period.value.start), lessThanOrEqual(now())), canonicalToNow(period.value.canonical)));
771
+ }
772
+ for (const prefix of nowToPeriod) {
773
+ if (!input.startsWith(prefix)) continue;
774
+ const period = parsePeriod(input.slice(prefix.length));
775
+ if (Option.isNone(period)) continue;
776
+ return Option.some(candidate(boundedRange(greaterThanOrEqual(now()), lessThan(period.value.end)), canonicalFromNow(period.value.canonical)));
777
+ }
778
+ return Option.none();
779
+ };
780
+ const joinedPeriodCandidate = (input, joins, parsePeriod, canonical) => {
781
+ for (const [prefix, separator] of joins) {
782
+ if (!input.startsWith(prefix)) continue;
783
+ const separatorIndex = input.indexOf(separator, prefix.length);
784
+ if (separatorIndex === -1) continue;
785
+ const lower = parsePeriod(input.slice(prefix.length, separatorIndex));
786
+ const upper = parsePeriod(input.slice(separatorIndex + separator.length));
787
+ if (Option.isNone(lower) || Option.isNone(upper)) continue;
788
+ return Option.some(candidate(boundedRange(greaterThanOrEqual(lower.value.start), lessThan(upper.value.end)), canonical(lower.value.canonical, upper.value.canonical)));
789
+ }
790
+ return Option.none();
791
+ };
792
+ const periodBoundaryCandidate = (period, boundary, canonical) => {
793
+ switch (boundary) {
794
+ case "since": return candidate(lowerOpenRange(greaterThanOrEqual(period.start)), canonical);
795
+ case "before": return candidate(upperOpenRange(lessThan(period.start)), canonical);
796
+ case "through": return candidate(upperOpenRange(lessThan(period.end)), canonical);
797
+ case "after": return candidate(lowerOpenRange(greaterThanOrEqual(period.end)), canonical);
798
+ }
799
+ };
800
+ const openBoundaryCandidate = (input, boundaries, parsePeriod) => {
801
+ for (const boundary of boundaries) {
802
+ if (!input.startsWith(boundary[0])) continue;
803
+ const period = parsePeriod(input.slice(boundary[0].length));
804
+ if (Option.isNone(period)) continue;
805
+ const canonical = `${boundary[0]}${period.value.canonical}`;
806
+ return Option.some(periodBoundaryCandidate(period.value, boundary[1], canonical));
807
+ }
808
+ return Option.none();
809
+ };
810
+ const expressionDates = (range) => {
811
+ const filter = formatFilter(range);
812
+ const expressions = [
813
+ filter.gt,
814
+ filter.gte,
815
+ filter.lt,
816
+ filter.lte
817
+ ];
818
+ const dates = /* @__PURE__ */ new Set();
819
+ for (const expression of expressions) {
820
+ if (expression === void 0) continue;
821
+ const match = String$1.match(/^(\d{4})-(\d{2})-(\d{2})/u)(expression);
822
+ if (Option.isSome(match)) dates.add(match.value[0]);
823
+ }
824
+ return [...dates];
825
+ };
826
+ const datedQuarterPeriods = (range) => {
827
+ return [...new Set(expressionDates(range).map((date) => date.slice(0, 4)))].flatMap((year) => [
828
+ 1,
829
+ 2,
830
+ 3,
831
+ 4
832
+ ].map((quarter) => `q${quarter} ${year}`));
833
+ };
834
+ const datedPeriods = (range, months) => {
835
+ const dates = expressionDates(range);
836
+ return [...[...new Set(dates.map((date) => date.slice(0, 4)))].flatMap((year) => [...months.map((month) => `${month} ${year}`), year]), ...dates.flatMap((date) => date === "0000-01-01" ? [date] : [date, previousDay(Number(date.slice(0, 4)), Number(date.slice(5, 7)), Number(date.slice(8, 10)))])];
837
+ };
838
+ const periodsFromPhrases = (phrases, parsePeriod) => phrases.flatMap((phrase) => Option.match(parsePeriod(phrase), {
839
+ onNone: () => [],
840
+ onSome: (period) => [period]
841
+ }));
842
+ const renderPeriodRange = (range, toDate, periods, since, before, through, after, between, toNow, fromNow, untilNow, afterNow) => {
843
+ const expected = rangeKey(range);
844
+ for (const entry of toDate) if (rangeKey(entry.range) === expected) return Option.some(entry.canonical);
845
+ const filter = formatFilter(range);
846
+ if (filter.lte === "now" && filter.gt === void 0 && filter.gte === void 0) return Option.some(untilNow());
847
+ if (filter.gte === "now" && filter.lt === void 0 && filter.lte === void 0) return Option.some(afterNow());
848
+ if (filter.gte !== void 0 && filter.lte === "now") {
849
+ const period = periods.find((candidate) => formatInstantExpression(candidate.start) === filter.gte);
850
+ if (period !== void 0) return Option.some(toNow(period.canonical));
851
+ }
852
+ if (filter.gte === "now" && filter.lt !== void 0) {
853
+ const period = periods.find((candidate) => formatInstantExpression(candidate.end) === filter.lt);
854
+ if (period !== void 0) return Option.some(fromNow(period.canonical));
855
+ }
856
+ if (filter.gte !== void 0 && filter.lt !== void 0) {
857
+ const exact = periods.find((period) => formatInstantExpression(period.start) === filter.gte && formatInstantExpression(period.end) === filter.lt);
858
+ if (exact !== void 0) return Option.some(exact.canonical);
859
+ const lower = periods.find((period) => formatInstantExpression(period.start) === filter.gte)?.canonical;
860
+ const upper = periods.find((period) => formatInstantExpression(period.end) === filter.lt)?.canonical;
861
+ return lower === void 0 || upper === void 0 ? Option.none() : Option.some(between(lower, upper));
862
+ }
863
+ if (filter.gte !== void 0 && filter.lt === void 0 && filter.lte === void 0) for (const period of periods) {
864
+ if (formatInstantExpression(period.start) === filter.gte) return Option.some(since(period.canonical));
865
+ if (formatInstantExpression(period.end) === filter.gte) return Option.some(after(period.canonical));
866
+ }
867
+ if (filter.lt !== void 0 && filter.gt === void 0 && filter.gte === void 0) for (const period of periods) {
868
+ if (formatInstantExpression(period.start) === filter.lt) return Option.some(before(period.canonical));
869
+ if (formatInstantExpression(period.end) === filter.lt) return Option.some(through(period.canonical));
870
+ }
871
+ return Option.none();
872
+ };
873
+ //#endregion
874
+ //#region src/locales/de.ts
875
+ const months$1 = [
876
+ "januar",
877
+ "februar",
878
+ "märz",
879
+ "april",
880
+ "mai",
881
+ "juni",
882
+ "juli",
883
+ "august",
884
+ "september",
885
+ "oktober",
886
+ "november",
887
+ "dezember"
888
+ ];
889
+ const monthAbbreviations$1 = [
890
+ ["jan"],
891
+ ["feb"],
892
+ ["mär", "mrz"],
893
+ ["apr"],
894
+ ["mai"],
895
+ ["jun"],
896
+ ["jul"],
897
+ ["aug"],
898
+ ["sep", "sept"],
899
+ ["okt"],
900
+ ["nov"],
901
+ ["dez"]
902
+ ];
903
+ const unitPhrases = [
904
+ {
905
+ unit: "day",
906
+ noun: "tag",
907
+ plural: "tage",
908
+ current: "heute",
909
+ previous: "gestern",
910
+ next: "morgen",
911
+ compound: "tages",
912
+ currentGenitive: "dieses tages",
913
+ dative: "tagen",
914
+ genitive: "des tages"
915
+ },
916
+ {
917
+ unit: "week",
918
+ noun: "woche",
919
+ plural: "wochen",
920
+ current: "diese woche",
921
+ previous: "letzte woche",
922
+ next: "nächste woche",
923
+ compound: "wochen",
924
+ currentGenitive: "dieser woche",
925
+ dative: "wochen",
926
+ genitive: "der woche"
927
+ },
928
+ {
929
+ unit: "month",
930
+ noun: "monat",
931
+ plural: "monate",
932
+ current: "dieser monat",
933
+ previous: "letzter monat",
934
+ next: "nächster monat",
935
+ compound: "monats",
936
+ currentGenitive: "dieses monats",
937
+ dative: "monaten",
938
+ genitive: "des monats"
939
+ },
940
+ {
941
+ unit: "quarter",
942
+ noun: "quartal",
943
+ plural: "quartale",
944
+ current: "dieses quartal",
945
+ previous: "letztes quartal",
946
+ next: "nächstes quartal",
947
+ compound: "quartals",
948
+ currentGenitive: "dieses quartals",
949
+ dative: "quartalen",
950
+ genitive: "des quartals"
951
+ },
952
+ {
953
+ unit: "year",
954
+ noun: "jahr",
955
+ plural: "jahre",
956
+ current: "dieses jahr",
957
+ previous: "letztes jahr",
958
+ next: "nächstes jahr",
959
+ compound: "jahres",
960
+ currentGenitive: "dieses jahres",
961
+ dative: "jahren",
962
+ genitive: "des jahres"
963
+ }
964
+ ];
965
+ const title$1 = (value) => `${value.slice(0, 1).toLocaleUpperCase("de")}${value.slice(1)}`;
966
+ const canonicalToDate = (entry) => `seit ${title$1(entry.compound)}beginn`;
967
+ const canonicalRelative = (value) => {
968
+ const separator = value.lastIndexOf(" ");
969
+ if (separator === -1) return value;
970
+ return `${value.slice(0, separator + 1)}${title$1(value.slice(separator + 1))}`;
971
+ };
972
+ const remainingPeriodPhrases$1 = unitPhrases.flatMap((entry) => [`rest ${entry.genitive}`, `rest ${entry.currentGenitive}`].map((phrase) => ({
973
+ entry,
974
+ phrase
975
+ })));
976
+ const toDatePhrases$1 = unitPhrases.flatMap((entry) => [
977
+ `seit ${entry.compound}beginn`,
978
+ `seit ${entry.compound}anfang`,
979
+ `seit beginn ${entry.genitive}`,
980
+ `seit anfang ${entry.genitive}`,
981
+ `seit beginn ${entry.currentGenitive}`,
982
+ `seit anfang ${entry.currentGenitive}`,
983
+ `vom ${entry.compound}beginn bis heute`,
984
+ `vom ${entry.compound}anfang bis heute`,
985
+ `vom beginn ${entry.genitive} bis heute`,
986
+ `vom anfang ${entry.genitive} bis heute`,
987
+ `vom beginn ${entry.currentGenitive} bis heute`,
988
+ `vom anfang ${entry.currentGenitive} bis heute`,
989
+ `${entry.noun} bis heute`,
990
+ `${entry.current} bisher`,
991
+ `bisher ${entry.current}`
992
+ ].map((phrase) => ({
993
+ entry,
994
+ phrase
995
+ })));
996
+ const currentBoundaryPhrases = unitPhrases.flatMap((entry) => [
997
+ [`vor ${entry.compound}beginn`, "before"],
998
+ [`vor ${entry.compound}anfang`, "before"],
999
+ [`bis ${entry.compound}beginn`, "before"],
1000
+ [`bis ${entry.compound}anfang`, "before"],
1001
+ [`bis ${entry.compound}ende`, "through"],
1002
+ [`nach ${entry.compound}ende`, "after"],
1003
+ [`ab ${entry.compound}ende`, "after"]
1004
+ ].map(([phrase, boundary]) => ({
1005
+ boundary,
1006
+ entry,
1007
+ phrase
1008
+ })));
1009
+ const relativeAliases = [
1010
+ [
1011
+ "vorletzten tag",
1012
+ "day",
1013
+ -2,
1014
+ "vorletzter tag"
1015
+ ],
1016
+ [
1017
+ "übernächsten tag",
1018
+ "day",
1019
+ 2,
1020
+ "übernächster tag"
1021
+ ],
1022
+ [
1023
+ "vorletzte woche",
1024
+ "week",
1025
+ -2,
1026
+ "vorletzte woche"
1027
+ ],
1028
+ [
1029
+ "übernächste woche",
1030
+ "week",
1031
+ 2,
1032
+ "übernächste woche"
1033
+ ],
1034
+ [
1035
+ "vorletzten monat",
1036
+ "month",
1037
+ -2,
1038
+ "vorletzter monat"
1039
+ ],
1040
+ [
1041
+ "übernächsten monat",
1042
+ "month",
1043
+ 2,
1044
+ "übernächster monat"
1045
+ ],
1046
+ [
1047
+ "vorletztes quartal",
1048
+ "quarter",
1049
+ -2,
1050
+ "vorletztes quartal"
1051
+ ],
1052
+ [
1053
+ "übernächstes quartal",
1054
+ "quarter",
1055
+ 2,
1056
+ "übernächstes quartal"
1057
+ ],
1058
+ [
1059
+ "vorletztes jahr",
1060
+ "year",
1061
+ -2,
1062
+ "vorletztes jahr"
1063
+ ],
1064
+ [
1065
+ "übernächstes jahr",
1066
+ "year",
1067
+ 2,
1068
+ "übernächstes jahr"
1069
+ ],
1070
+ [
1071
+ "letzten tag",
1072
+ "day",
1073
+ -1,
1074
+ "gestern"
1075
+ ],
1076
+ [
1077
+ "vergangenen tag",
1078
+ "day",
1079
+ -1,
1080
+ "gestern"
1081
+ ],
1082
+ [
1083
+ "vorherigen tag",
1084
+ "day",
1085
+ -1,
1086
+ "gestern"
1087
+ ],
1088
+ [
1089
+ "diesen tag",
1090
+ "day",
1091
+ 0,
1092
+ "heute"
1093
+ ],
1094
+ [
1095
+ "heutigen tag",
1096
+ "day",
1097
+ 0,
1098
+ "heute"
1099
+ ],
1100
+ [
1101
+ "nächsten tag",
1102
+ "day",
1103
+ 1,
1104
+ "morgen"
1105
+ ],
1106
+ [
1107
+ "kommenden tag",
1108
+ "day",
1109
+ 1,
1110
+ "morgen"
1111
+ ],
1112
+ [
1113
+ "vergangene woche",
1114
+ "week",
1115
+ -1,
1116
+ "letzte woche"
1117
+ ],
1118
+ [
1119
+ "vorherige woche",
1120
+ "week",
1121
+ -1,
1122
+ "letzte woche"
1123
+ ],
1124
+ [
1125
+ "aktuelle woche",
1126
+ "week",
1127
+ 0,
1128
+ "diese woche"
1129
+ ],
1130
+ [
1131
+ "laufende woche",
1132
+ "week",
1133
+ 0,
1134
+ "diese woche"
1135
+ ],
1136
+ [
1137
+ "kommende woche",
1138
+ "week",
1139
+ 1,
1140
+ "nächste woche"
1141
+ ],
1142
+ [
1143
+ "folgende woche",
1144
+ "week",
1145
+ 1,
1146
+ "nächste woche"
1147
+ ],
1148
+ [
1149
+ "letzten monat",
1150
+ "month",
1151
+ -1,
1152
+ "letzter monat"
1153
+ ],
1154
+ [
1155
+ "vergangenen monat",
1156
+ "month",
1157
+ -1,
1158
+ "letzter monat"
1159
+ ],
1160
+ [
1161
+ "vorherigen monat",
1162
+ "month",
1163
+ -1,
1164
+ "letzter monat"
1165
+ ],
1166
+ [
1167
+ "diesen monat",
1168
+ "month",
1169
+ 0,
1170
+ "dieser monat"
1171
+ ],
1172
+ [
1173
+ "aktuellen monat",
1174
+ "month",
1175
+ 0,
1176
+ "dieser monat"
1177
+ ],
1178
+ [
1179
+ "laufenden monat",
1180
+ "month",
1181
+ 0,
1182
+ "dieser monat"
1183
+ ],
1184
+ [
1185
+ "nächsten monat",
1186
+ "month",
1187
+ 1,
1188
+ "nächster monat"
1189
+ ],
1190
+ [
1191
+ "kommenden monat",
1192
+ "month",
1193
+ 1,
1194
+ "nächster monat"
1195
+ ],
1196
+ [
1197
+ "folgenden monat",
1198
+ "month",
1199
+ 1,
1200
+ "nächster monat"
1201
+ ],
1202
+ [
1203
+ "vergangenes quartal",
1204
+ "quarter",
1205
+ -1,
1206
+ "letztes quartal"
1207
+ ],
1208
+ [
1209
+ "vorheriges quartal",
1210
+ "quarter",
1211
+ -1,
1212
+ "letztes quartal"
1213
+ ],
1214
+ [
1215
+ "aktuelles quartal",
1216
+ "quarter",
1217
+ 0,
1218
+ "dieses quartal"
1219
+ ],
1220
+ [
1221
+ "laufendes quartal",
1222
+ "quarter",
1223
+ 0,
1224
+ "dieses quartal"
1225
+ ],
1226
+ [
1227
+ "kommendes quartal",
1228
+ "quarter",
1229
+ 1,
1230
+ "nächstes quartal"
1231
+ ],
1232
+ [
1233
+ "folgendes quartal",
1234
+ "quarter",
1235
+ 1,
1236
+ "nächstes quartal"
1237
+ ],
1238
+ [
1239
+ "vergangenes jahr",
1240
+ "year",
1241
+ -1,
1242
+ "letztes jahr"
1243
+ ],
1244
+ [
1245
+ "vorheriges jahr",
1246
+ "year",
1247
+ -1,
1248
+ "letztes jahr"
1249
+ ],
1250
+ [
1251
+ "aktuelles jahr",
1252
+ "year",
1253
+ 0,
1254
+ "dieses jahr"
1255
+ ],
1256
+ [
1257
+ "laufendes jahr",
1258
+ "year",
1259
+ 0,
1260
+ "dieses jahr"
1261
+ ],
1262
+ [
1263
+ "kommendes jahr",
1264
+ "year",
1265
+ 1,
1266
+ "nächstes jahr"
1267
+ ],
1268
+ [
1269
+ "folgendes jahr",
1270
+ "year",
1271
+ 1,
1272
+ "nächstes jahr"
1273
+ ]
1274
+ ];
1275
+ const monthNumber$1 = (value) => {
1276
+ const normalized = value.endsWith(".") ? value.slice(0, -1) : value;
1277
+ const fullIndex = months$1.findIndex((month) => month === normalized);
1278
+ if (fullIndex !== -1) return fullIndex + 1;
1279
+ const shortIndex = monthAbbreviations$1.findIndex((aliases) => aliases.some((alias) => alias === normalized));
1280
+ return shortIndex === -1 ? void 0 : shortIndex + 1;
1281
+ };
1282
+ const parseNamedDate$1 = (input) => {
1283
+ const named = String$1.match(/^([0-3]?\d)\.? ([a-zäöüß]+\.?) (\d{4})$/u)(input);
1284
+ if (Option.isSome(named)) return namedDatePeriod(named.value[3], named.value[2], named.value[1], monthNumber$1, (day, month, year) => `${day}. ${title$1(months$1[month - 1])} ${year}`);
1285
+ const numeric = String$1.match(/^([0-3]?\d)\.([01]?\d)\.(\d{4})$/u)(input);
1286
+ if (Option.isSome(numeric)) {
1287
+ const year = validYear(numeric.value[3]);
1288
+ const day = Number(numeric.value[1]);
1289
+ const month = Number(numeric.value[2]);
1290
+ if (year !== void 0 && month >= 1 && month <= 12) {
1291
+ const value = isoDate(year, month, day);
1292
+ if (isIsoDate(value) && value !== "9999-12-31") return Option.some(fixedDatePeriod(value, `${day}. ${title$1(months$1[month - 1])} ${year}`));
1293
+ }
1294
+ }
1295
+ return Option.none();
1296
+ };
1297
+ const quarterNames$1 = [
1298
+ "erstes",
1299
+ "zweites",
1300
+ "drittes",
1301
+ "viertes"
1302
+ ];
1303
+ const quarterNumber$1 = (value) => {
1304
+ if (value.startsWith("q")) return Number(value.slice(1));
1305
+ if (value.endsWith(".")) return Number(value.slice(0, -1));
1306
+ const index = quarterNames$1.findIndex((name) => name === value);
1307
+ return index === -1 ? void 0 : index + 1;
1308
+ };
1309
+ const relativeDirection$1 = (value) => {
1310
+ if (value === "letzten") return -1;
1311
+ if (value === "nächsten") return 1;
1312
+ return 0;
1313
+ };
1314
+ const parseQuarter$1 = (input) => {
1315
+ const fixed = String$1.match(/^(q[1-4]|[1-4]\.|erstes|zweites|drittes|viertes)(?: quartal)? (\d{4})$/u)(input);
1316
+ const reversed = String$1.match(/^(\d{4}) (q[1-4])$/u)(input);
1317
+ const relative = String$1.match(/^(q[1-4]|[1-4]\.|erstes|zweites|drittes|viertes)(?: quartal)? (letzten|dieses|nächsten) jahres$/u)(input);
1318
+ const standalone = String$1.match(/^(q[1-4]|[1-4]\.|erstes|zweites|drittes|viertes)(?: quartal)?$/u)(input);
1319
+ if (Option.isSome(fixed) || Option.isSome(reversed)) {
1320
+ const match = [fixed, reversed].find(Option.isSome)?.value;
1321
+ if (match === void 0) return Option.none();
1322
+ const quarterText = Option.isSome(reversed) ? match[2] : match[1];
1323
+ const yearText = Option.isSome(reversed) ? match[1] : match[2];
1324
+ const quarter = quarterNumber$1(quarterText);
1325
+ const year = validYear(yearText);
1326
+ if (quarter !== void 0 && year !== void 0) return Option.some(fixedQuarterPeriod(year, quarter, `Q${quarter} ${year}`));
1327
+ }
1328
+ if (Option.isSome(relative)) {
1329
+ const quarter = quarterNumber$1(relative.value[1]);
1330
+ if (quarter !== void 0) {
1331
+ const direction = relativeDirection$1(relative.value[2]);
1332
+ return Option.some(quarterOfRelativeYear(quarter, direction, `Q${quarter} ${relative.value[2]} Jahres`));
1333
+ }
1334
+ }
1335
+ if (Option.isNone(standalone)) return Option.none();
1336
+ const quarter = quarterNumber$1(standalone.value[1]);
1337
+ return quarter === void 0 ? Option.none() : Option.some(quarterOfRelativeYear(quarter, 0, `Q${quarter}`));
1338
+ };
1339
+ const parseBasePeriod$1 = (input) => {
1340
+ if (isIsoDate(input) && input !== "9999-12-31") return Option.some(fixedDatePeriod(input, input));
1341
+ const namedDate = parseNamedDate$1(input);
1342
+ if (Option.isSome(namedDate)) return namedDate;
1343
+ const quarter = parseQuarter$1(input);
1344
+ if (Option.isSome(quarter)) return quarter;
1345
+ const yearMatch = String$1.match(/^(?:(?:das )?(?:kalender)?jahr )?(\d{4})$/u)(input);
1346
+ if (Option.isSome(yearMatch)) {
1347
+ const year = validYear(yearMatch.value[1]);
1348
+ if (year !== void 0) return Option.some(fixedYearPeriod(year, String(year)));
1349
+ }
1350
+ const monthYear = String$1.match(/^([a-zäöüß]+\.?) (\d{4})$/u)(input);
1351
+ if (Option.isSome(monthYear)) {
1352
+ const month = monthNumber$1(monthYear.value[1]);
1353
+ const year = validYear(monthYear.value[2]);
1354
+ if (month !== void 0 && year !== void 0) return Option.some(fixedMonthPeriod(year, month, `${title$1(months$1[month - 1])} ${year}`));
1355
+ }
1356
+ const relativeMonth = String$1.match(/^([a-zäöüß]+\.?) (letzten|dieses|nächsten) jahres$/u)(input);
1357
+ if (Option.isSome(relativeMonth)) {
1358
+ const month = monthNumber$1(relativeMonth.value[1]);
1359
+ const direction = relativeDirection$1(relativeMonth.value[2]);
1360
+ if (month !== void 0) return Option.some(monthOfRelativeYear(month, direction, `${title$1(months$1[month - 1])} ${relativeMonth.value[2]} Jahres`));
1361
+ }
1362
+ const standaloneMonth = monthNumber$1(input);
1363
+ if (standaloneMonth !== void 0) return Option.some(monthOfRelativeYear(standaloneMonth, 0, title$1(months$1[standaloneMonth - 1])));
1364
+ if (input === "wochenende" || input === "dieses wochenende" || input === "am wochenende") return Option.some(relativeWeekend(0, "dieses Wochenende"));
1365
+ if (input === "letztes wochenende" || input === "vergangenes wochenende") return Option.some(relativeWeekend(-1, "letztes Wochenende"));
1366
+ if (input === "nächstes wochenende" || input === "kommendes wochenende") return Option.some(relativeWeekend(1, "nächstes Wochenende"));
1367
+ if (input === "vorletztes wochenende") return Option.some(relativeWeekend(-2, "vorletztes Wochenende"));
1368
+ if (input === "übernächstes wochenende") return Option.some(relativeWeekend(2, "übernächstes Wochenende"));
1369
+ const relative = unitPhrases.find((entry) => entry.current === input || entry.previous === input || entry.next === input);
1370
+ if (relative !== void 0) {
1371
+ let direction = 0;
1372
+ if (input === relative.previous) direction = -1;
1373
+ if (input === relative.next) direction = 1;
1374
+ return Option.some(relativePeriod(relative.unit, direction, canonicalRelative(input)));
1375
+ }
1376
+ const alias = relativeAliases.find((entry) => entry[0] === input);
1377
+ if (alias !== void 0) return Option.some(relativePeriod(alias[1], alias[2], canonicalRelative(alias[3])));
1378
+ return Option.none();
1379
+ };
1380
+ const parsePeriod$1 = (input) => {
1381
+ const edge = String$1.match(/^(anfang|beginn|ende) (.+)$/u)(input);
1382
+ if (Option.isSome(edge)) {
1383
+ const period = parseBasePeriod$1(edge.value[2]);
1384
+ if (Option.isSome(period)) {
1385
+ const canonical = `${edge.value[1] === "ende" ? "Ende" : "Anfang"} ${period.value.canonical}`;
1386
+ return Option.some(edge.value[1] === "ende" ? periodEndDay(period.value, canonical) : periodStartDay(period.value, canonical));
1387
+ }
1388
+ }
1389
+ const wrapper = [
1390
+ "im ",
1391
+ "für ",
1392
+ "während "
1393
+ ].find((prefix) => input.startsWith(prefix));
1394
+ return parseBasePeriod$1(wrapper === void 0 ? input : input.slice(wrapper.length));
1395
+ };
1396
+ const currentBoundaryCandidate = (input) => {
1397
+ const match = currentBoundaryPhrases.find((entry) => entry.phrase === input);
1398
+ if (match === void 0) return Option.none();
1399
+ const period = relativePeriod(match.entry.unit, 0, match.entry.current);
1400
+ return Option.some(periodBoundaryCandidate(period, match.boundary, match.phrase));
1401
+ };
1402
+ const boundaryCandidate$1 = (input) => openBoundaryCandidate(input, [
1403
+ ["seit dem beginn von ", "since"],
1404
+ ["seit dem anfang von ", "since"],
1405
+ ["seit dem jahr ", "since"],
1406
+ ["ab dem jahr ", "since"],
1407
+ ["bis zum beginn von ", "before"],
1408
+ ["bis zum anfang von ", "before"],
1409
+ ["bis zum jahr ", "before"],
1410
+ ["bis zum ende von ", "through"],
1411
+ ["nach dem ende von ", "after"],
1412
+ ["bis einschließlich ", "through"],
1413
+ ["seit beginn ", "since"],
1414
+ ["seit anfang ", "since"],
1415
+ ["ab beginn ", "since"],
1416
+ ["ab anfang ", "since"],
1417
+ ["vor beginn ", "before"],
1418
+ ["vor anfang ", "before"],
1419
+ ["bis beginn ", "before"],
1420
+ ["bis anfang ", "before"],
1421
+ ["bis ende ", "through"],
1422
+ ["nach ende ", "after"],
1423
+ ["ab ende ", "after"],
1424
+ ["seit ", "since"],
1425
+ ["vor ", "before"],
1426
+ ["nach ", "after"],
1427
+ ["bis ", "before"],
1428
+ ["ab ", "since"]
1429
+ ], parsePeriod$1);
1430
+ const countedUnit$1 = (value, amount, dative = false) => unitPhrases.find((entry) => {
1431
+ if (amount === 1) return value === entry.noun;
1432
+ return value === (dative ? entry.dative : entry.plural);
1433
+ });
1434
+ const parseCalendarOffset$1 = (input) => {
1435
+ const prefixed = String$1.match(/^(vor|in) ([1-9]\d*) (tag|tagen|woche|wochen|monat|monaten|quartal|quartalen|jahr|jahren)$/u)(input);
1436
+ const prior = String$1.match(/^([1-9]\d*) (tag|tage|woche|wochen|monat|monate|quartal|quartale|jahr|jahre) zuvor$/u)(input);
1437
+ const match = [prefixed, prior].find(Option.isSome)?.value;
1438
+ if (match === void 0) return Option.none();
1439
+ const amountText = Option.isSome(prefixed) ? match[2] : match[1];
1440
+ const unitText = Option.isSome(prefixed) ? match[3] : match[2];
1441
+ const amount = parseTrailingCount(amountText);
1442
+ if (Option.isNone(amount)) return Option.none();
1443
+ const entry = countedUnit$1(unitText, amount.value, Option.isSome(prefixed));
1444
+ if (entry === void 0) return Option.none();
1445
+ const isPast = Option.isSome(prior) || Option.isSome(prefixed) && match[1] === "vor";
1446
+ const direction = isPast ? -amount.value : amount.value;
1447
+ const directionName = isPast ? "vor" : "in";
1448
+ const noun = amount.value === 1 ? entry.noun : entry.dative;
1449
+ const canonical = `${directionName} ${amount.value} ${title$1(noun)}`;
1450
+ return Option.some(candidate(periodRange(relativePeriod(entry.unit, direction, canonical)), canonical));
1451
+ };
1452
+ const parseRollingPeriod$1 = (input) => {
1453
+ const past = String$1.match(/^(?:(?:die letzten|letzten|letzte|vergangene|vorherige) )?([1-9]\d*) (tag|tage|woche|wochen|monat|monate|quartal|quartale|jahr|jahre)$/u)(input);
1454
+ const pastDative = String$1.match(/^(?:in den letzten|in den vergangenen|in den vorherigen) ([1-9]\d*) (tag|tagen|woche|wochen|monat|monaten|quartal|quartalen|jahr|jahren)$/u)(input);
1455
+ const pastGenitive = String$1.match(/^während der letzten ([1-9]\d*) (tag|tage|woche|wochen|monat|monate|quartal|quartale|jahr|jahre)$/u)(input);
1456
+ const future = String$1.match(/^(?:die nächsten|nächsten|nächste|kommende) ([1-9]\d*) (tag|tage|woche|wochen|monat|monate|quartal|quartale|jahr|jahre)$/u)(input);
1457
+ const futureDative = String$1.match(/^(?:in den nächsten|in den kommenden|innerhalb von) ([1-9]\d*) (tag|tagen|woche|wochen|monat|monaten|quartal|quartalen|jahr|jahren)$/u)(input);
1458
+ const futureGenitive = String$1.match(/^innerhalb der nächsten ([1-9]\d*) (tag|tage|woche|wochen|monat|monate|quartal|quartale|jahr|jahre)$/u)(input);
1459
+ const match = [
1460
+ past,
1461
+ pastDative,
1462
+ pastGenitive,
1463
+ future,
1464
+ futureDative,
1465
+ futureGenitive
1466
+ ].find(Option.isSome)?.value;
1467
+ if (match === void 0) return Option.none();
1468
+ const amount = parseTrailingCount(match[1]);
1469
+ if (Option.isNone(amount)) return Option.none();
1470
+ const usesDative = Option.isSome(pastDative) || Option.isSome(futureDative);
1471
+ const entry = countedUnit$1(match[2], amount.value, usesDative);
1472
+ if (entry === void 0) return Option.none();
1473
+ const isFuture = Option.isSome(future) || Option.isSome(futureDative) || Option.isSome(futureGenitive);
1474
+ const range = isFuture ? futureRange(amount.value, entry.unit) : trailingRange(amount.value, entry.unit);
1475
+ const direction = isFuture ? "nächste" : "letzte";
1476
+ const noun = amount.value === 1 ? entry.noun : entry.plural;
1477
+ return Option.some(candidate(range, `${direction} ${amount.value} ${title$1(noun)}`));
1478
+ };
1479
+ const parseGerman = (input) => {
1480
+ const remaining = remainingPeriodPhrases$1.find((entry) => entry.phrase === input);
1481
+ if (remaining !== void 0) return Option.some(candidate(remainingPeriodRange(remaining.entry.unit), `Rest ${canonicalRelative(remaining.entry.genitive)}`));
1482
+ if (["bis heute", "bis jetzt"].includes(input)) return Option.some(candidate(untilNowRange(), "bis heute"));
1483
+ if (["ab jetzt", "von jetzt an"].includes(input)) return Option.some(candidate(fromNowRange(), "ab jetzt"));
1484
+ const offset = parseCalendarOffset$1(input);
1485
+ if (Option.isSome(offset)) return offset;
1486
+ const rolling = parseRollingPeriod$1(input);
1487
+ if (Option.isSome(rolling)) return rolling;
1488
+ const toDate = toDatePhrases$1.find((entry) => entry.phrase === input);
1489
+ if (toDate !== void 0) return Option.some(candidate(periodToDateRange(toDate.entry.unit), canonicalToDate(toDate.entry)));
1490
+ const currentBoundary = currentBoundaryCandidate(input);
1491
+ if (Option.isSome(currentBoundary)) return currentBoundary;
1492
+ const nowBounded = joinedNowCandidate(input, [
1493
+ ["von ", " bis heute"],
1494
+ ["vom ", " bis heute"],
1495
+ ["zwischen ", " und heute"]
1496
+ ], ["von heute bis ", "zwischen heute und "], parsePeriod$1, (period) => `von ${period} bis heute`, (period) => `von heute bis ${period}`);
1497
+ if (Option.isSome(nowBounded)) return nowBounded;
1498
+ const bounded = joinedPeriodCandidate(input, [
1499
+ ["von ", " bis einschließlich "],
1500
+ ["vom ", " bis einschließlich "],
1501
+ ["von ", " bis zum "],
1502
+ ["vom ", " bis zum "],
1503
+ ["von ", " bis "],
1504
+ ["vom ", " bis "],
1505
+ ["zwischen ", " und "],
1506
+ ["", " bis einschließlich "],
1507
+ ["", " bis "],
1508
+ ["", " - "],
1509
+ ["", " – "],
1510
+ ["", " — "],
1511
+ ["zwischen ", "-"],
1512
+ ["", "-"],
1513
+ ["zwischen ", "–"],
1514
+ ["", "–"],
1515
+ ["zwischen ", "—"],
1516
+ ["", "—"],
1517
+ ["zwischen ", "~"],
1518
+ ["", "~"]
1519
+ ], parsePeriod$1, (lower, upper) => `von ${lower} bis ${upper}`);
1520
+ if (Option.isSome(bounded)) return bounded;
1521
+ const boundary = boundaryCandidate$1(input);
1522
+ if (Option.isSome(boundary)) return boundary;
1523
+ return Option.map(parsePeriod$1(input), (period) => candidate(periodRange(period), period.canonical));
1524
+ };
1525
+ const staticPeriods$1 = periodsFromPhrases([
1526
+ "dieses wochenende",
1527
+ "letztes wochenende",
1528
+ "nächstes wochenende",
1529
+ "vorletztes wochenende",
1530
+ "übernächstes wochenende",
1531
+ ...unitPhrases.flatMap((entry) => [
1532
+ entry.current,
1533
+ entry.previous,
1534
+ entry.next,
1535
+ `anfang ${entry.current}`,
1536
+ `ende ${entry.current}`,
1537
+ `anfang ${entry.previous}`,
1538
+ `ende ${entry.previous}`,
1539
+ `anfang ${entry.next}`,
1540
+ `ende ${entry.next}`
1541
+ ]),
1542
+ ...[
1543
+ 1,
1544
+ 2,
1545
+ 3,
1546
+ 4
1547
+ ].flatMap((quarter) => [
1548
+ `q${quarter}`,
1549
+ `q${quarter} letzten jahres`,
1550
+ `q${quarter} nächsten jahres`
1551
+ ]),
1552
+ ...months$1.flatMap((month) => [
1553
+ month,
1554
+ `${month} letzten jahres`,
1555
+ `${month} nächsten jahres`
1556
+ ])
1557
+ ], parsePeriod$1);
1558
+ const renderGerman = (range) => {
1559
+ const offset = calendarPeriodOffset(range);
1560
+ if (Option.isSome(offset) && Math.abs(offset.value.amount) > 1) {
1561
+ const entry = unitPhrases.find((unit) => unit.unit === offset.value.unit);
1562
+ if (entry !== void 0) return Option.some(`${offset.value.amount < 0 ? "vor" : "in"} ${Math.abs(offset.value.amount)} ${title$1(Math.abs(offset.value.amount) === 1 ? entry.noun : entry.dative)}`);
1563
+ }
1564
+ const future = futurePeriod(range);
1565
+ if (Option.isSome(future)) {
1566
+ const entry = unitPhrases.find((unit) => unit.unit === future.value.unit);
1567
+ if (entry !== void 0) return Option.some(`nächste ${future.value.amount} ${title$1(future.value.amount === 1 ? entry.noun : entry.plural)}`);
1568
+ }
1569
+ const trailing = trailingPeriod(range);
1570
+ if (Option.isSome(trailing)) {
1571
+ const entry = unitPhrases.find((unit) => unit.unit === trailing.value.unit);
1572
+ if (entry !== void 0) return Option.some(trailing.value.amount === 1 ? `1 ${title$1(entry.noun)}` : `letzte ${trailing.value.amount} ${title$1(entry.plural)}`);
1573
+ }
1574
+ const periods = [...staticPeriods$1, ...periodsFromPhrases([...datedPeriods(range, months$1), ...datedQuarterPeriods(range)], parsePeriod$1)];
1575
+ return renderPeriodRange(range, [...unitPhrases.map((entry) => candidate(periodToDateRange(entry.unit), canonicalToDate(entry))), ...unitPhrases.map((entry) => candidate(remainingPeriodRange(entry.unit), `Rest ${canonicalRelative(entry.genitive)}`))], periods, (period) => `seit ${period}`, (period) => `vor ${period}`, (period) => `bis einschließlich ${period}`, (period) => `nach ${period}`, (lower, upper) => `von ${lower} bis ${upper}`, (period) => `von ${period} bis heute`, (period) => `von heute bis ${period}`, () => "bis heute", () => "ab jetzt");
1576
+ };
1577
+ const GermanContribution = new BaseLanguageContribution({
1578
+ locale: "de",
1579
+ vocabulary: [
1580
+ ...months$1,
1581
+ ...quarterNames$1,
1582
+ "q1",
1583
+ "q2",
1584
+ "q3",
1585
+ "q4",
1586
+ ...monthAbbreviations$1.flatMap((aliases) => aliases),
1587
+ ...toDatePhrases$1.flatMap((entry) => entry.phrase.split(" ")),
1588
+ ...remainingPeriodPhrases$1.flatMap((entry) => entry.phrase.split(" ")),
1589
+ ...currentBoundaryPhrases.flatMap((entry) => entry.phrase.split(" ")),
1590
+ ...relativeAliases.flatMap((entry) => entry[0].split(" ")),
1591
+ ...unitPhrases.flatMap((entry) => [
1592
+ entry.noun,
1593
+ entry.plural,
1594
+ entry.dative,
1595
+ ...entry.currentGenitive.split(" "),
1596
+ ...entry.current.split(" "),
1597
+ ...entry.previous.split(" "),
1598
+ ...entry.next.split(" ")
1599
+ ]),
1600
+ "ab",
1601
+ "an",
1602
+ "anfang",
1603
+ "beginn",
1604
+ "bisher",
1605
+ "bis",
1606
+ "dem",
1607
+ "den",
1608
+ "die",
1609
+ "dieses",
1610
+ "einschließlich",
1611
+ "ende",
1612
+ "für",
1613
+ "im",
1614
+ "innerhalb",
1615
+ "jetzt",
1616
+ "kalenderjahr",
1617
+ "jahres",
1618
+ "kommende",
1619
+ "kommenden",
1620
+ "letzte",
1621
+ "letzten",
1622
+ "nach",
1623
+ "nächste",
1624
+ "nächsten",
1625
+ "seit",
1626
+ "vor",
1627
+ "vorherige",
1628
+ "vorherigen",
1629
+ "vergangene",
1630
+ "vergangenen",
1631
+ "vom",
1632
+ "von",
1633
+ "während",
1634
+ "wochenende",
1635
+ "zwischen",
1636
+ "zuvor"
1637
+ ],
1638
+ normalize: normalizeNaturalText,
1639
+ correct: correctWhitespaceSeparatedText,
1640
+ parseExact: parseGerman,
1641
+ render: renderGerman
1642
+ });
1643
+ const GermanLanguage = defineLanguagePlugin({
1644
+ id: "chronolizer/language-de",
1645
+ effect: (context) => Effect.asVoid(context.register("chronolizer/language-de", GermanContribution))
1646
+ });
1647
+ const GermanLanguageLayer = languagePluginsLayer([GermanLanguage]);
1648
+ //#endregion
1649
+ //#region src/locales/en.ts
1650
+ const months = [
1651
+ "january",
1652
+ "february",
1653
+ "march",
1654
+ "april",
1655
+ "may",
1656
+ "june",
1657
+ "july",
1658
+ "august",
1659
+ "september",
1660
+ "october",
1661
+ "november",
1662
+ "december"
1663
+ ];
1664
+ const monthAbbreviations = [
1665
+ ["jan"],
1666
+ ["feb"],
1667
+ ["mar"],
1668
+ ["apr"],
1669
+ ["may"],
1670
+ ["jun"],
1671
+ ["jul"],
1672
+ ["aug"],
1673
+ ["sep", "sept"],
1674
+ ["oct"],
1675
+ ["nov"],
1676
+ ["dec"]
1677
+ ];
1678
+ const quarterNames = [
1679
+ "first",
1680
+ "second",
1681
+ "third",
1682
+ "fourth"
1683
+ ];
1684
+ const units = [
1685
+ [
1686
+ "day",
1687
+ "day",
1688
+ "days"
1689
+ ],
1690
+ [
1691
+ "week",
1692
+ "week",
1693
+ "weeks"
1694
+ ],
1695
+ [
1696
+ "month",
1697
+ "month",
1698
+ "months"
1699
+ ],
1700
+ [
1701
+ "quarter",
1702
+ "quarter",
1703
+ "quarters"
1704
+ ],
1705
+ [
1706
+ "year",
1707
+ "year",
1708
+ "years"
1709
+ ]
1710
+ ];
1711
+ const title = (value) => `${value.slice(0, 1).toLocaleUpperCase("en")}${value.slice(1)}`;
1712
+ const currentPeriod = (unit) => unit === "day" ? "today" : `this ${unit}`;
1713
+ const remainingPeriodPhrases = units.flatMap((entry) => [
1714
+ `rest of the ${entry[0]}`,
1715
+ `rest of ${entry[0]}`,
1716
+ `rest of ${currentPeriod(entry[0])}`,
1717
+ `remainder of the ${entry[0]}`,
1718
+ `remaining ${entry[0]}`
1719
+ ].map((phrase) => ({
1720
+ entry,
1721
+ phrase
1722
+ })));
1723
+ const toDateAbbreviations = [
1724
+ "dtd",
1725
+ "wtd",
1726
+ "mtd",
1727
+ "qtd",
1728
+ "ytd"
1729
+ ];
1730
+ const toDatePhrases = units.flatMap((entry, index) => [
1731
+ `${entry[0]} to date`,
1732
+ `${entry[0]}-to-date`,
1733
+ toDateAbbreviations[index],
1734
+ `since the start of the ${entry[0]}`,
1735
+ `since the beginning of the ${entry[0]}`,
1736
+ `from the start of the ${entry[0]} to now`,
1737
+ `from the beginning of the ${entry[0]} to now`,
1738
+ `${currentPeriod(entry[0])} so far`,
1739
+ `so far ${currentPeriod(entry[0])}`,
1740
+ `${currentPeriod(entry[0])} until now`,
1741
+ `${currentPeriod(entry[0])} up to now`
1742
+ ].map((phrase) => ({
1743
+ entry,
1744
+ phrase
1745
+ })));
1746
+ const monthNumber = (value) => {
1747
+ const normalized = value.endsWith(".") ? value.slice(0, -1) : value;
1748
+ const fullIndex = months.findIndex((month) => month === normalized);
1749
+ if (fullIndex !== -1) return fullIndex + 1;
1750
+ const shortIndex = monthAbbreviations.findIndex((aliases) => aliases.some((alias) => alias === normalized));
1751
+ return shortIndex === -1 ? void 0 : shortIndex + 1;
1752
+ };
1753
+ const relativeDirection = (value) => {
1754
+ if (value === "last" || value === "previous") return -1;
1755
+ if (value === "next" || value === "coming" || value === "upcoming") return 1;
1756
+ return 0;
1757
+ };
1758
+ const relativeDirectionName = (direction) => {
1759
+ if (direction < 0) return "last";
1760
+ if (direction > 0) return "next";
1761
+ return "this";
1762
+ };
1763
+ const quarterNumber = (value) => {
1764
+ if (value.startsWith("q")) return Number(value.slice(1));
1765
+ const index = quarterNames.findIndex((name) => name === value);
1766
+ return index === -1 ? void 0 : index + 1;
1767
+ };
1768
+ const parseQuarter = (input) => {
1769
+ const fixed = String$1.match(/^(q[1-4])(?: of)? (\d{4})$/u)(input);
1770
+ const reversed = String$1.match(/^(\d{4}) (q[1-4])$/u)(input);
1771
+ const namedFixed = String$1.match(/^(first|second|third|fourth) quarter(?: of)? (\d{4})$/u)(input);
1772
+ const relative = String$1.match(/^(q[1-4])(?: of)? (last|this|next) year$/u)(input);
1773
+ const namedRelative = String$1.match(/^(first|second|third|fourth) quarter(?: of)? (last|this|next) year$/u)(input);
1774
+ const standalone = String$1.match(/^(q[1-4])$/u)(input);
1775
+ const namedStandalone = String$1.match(/^(first|second|third|fourth) quarter$/u)(input);
1776
+ if (Option.isSome(fixed) || Option.isSome(reversed) || Option.isSome(namedFixed)) {
1777
+ const match = [
1778
+ fixed,
1779
+ reversed,
1780
+ namedFixed
1781
+ ].find(Option.isSome)?.value;
1782
+ if (match === void 0) return Option.none();
1783
+ const quarterText = Option.isSome(reversed) ? match[2] : match[1];
1784
+ const yearText = Option.isSome(reversed) ? match[1] : match[2];
1785
+ const quarter = quarterNumber(quarterText);
1786
+ const year = validYear(yearText);
1787
+ if (quarter !== void 0 && year !== void 0) return Option.some(fixedQuarterPeriod(year, quarter, `Q${quarter} ${year}`));
1788
+ }
1789
+ if (Option.isSome(relative) || Option.isSome(namedRelative)) {
1790
+ const match = [relative, namedRelative].find(Option.isSome)?.value;
1791
+ if (match === void 0) return Option.none();
1792
+ const quarter = quarterNumber(match[1]);
1793
+ if (quarter !== void 0) {
1794
+ const direction = relativeDirection(match[2]);
1795
+ return Option.some(quarterOfRelativeYear(quarter, direction, `Q${quarter} of ${match[2]} year`));
1796
+ }
1797
+ }
1798
+ const match = [standalone, namedStandalone].find(Option.isSome)?.value;
1799
+ if (match === void 0) return Option.none();
1800
+ const quarter = quarterNumber(match[1]);
1801
+ return quarter === void 0 ? Option.none() : Option.some(quarterOfRelativeYear(quarter, 0, `Q${quarter}`));
1802
+ };
1803
+ const parseNamedDate = (input) => {
1804
+ const dayFirst = String$1.match(/^([0-3]?\d)(?:st|nd|rd|th)? ([a-z]+\.?) (\d{4})$/u)(input);
1805
+ if (Option.isSome(dayFirst)) return namedDatePeriod(dayFirst.value[3], dayFirst.value[2], dayFirst.value[1], monthNumber, (day, month, year) => `${day} ${title(months[month - 1])} ${year}`);
1806
+ const monthFirst = String$1.match(/^([a-z]+\.?) ([0-3]?\d)(?:st|nd|rd|th)?,? (\d{4})$/u)(input);
1807
+ if (Option.isSome(monthFirst)) return namedDatePeriod(monthFirst.value[3], monthFirst.value[1], monthFirst.value[2], monthNumber, (day, month, year) => `${day} ${title(months[month - 1])} ${year}`);
1808
+ return Option.none();
1809
+ };
1810
+ const parseBasePeriod = (input) => {
1811
+ if (isIsoDate(input) && input !== "9999-12-31") return Option.some(fixedDatePeriod(input, input));
1812
+ const namedDate = parseNamedDate(input);
1813
+ if (Option.isSome(namedDate)) return namedDate;
1814
+ const quarter = parseQuarter(input);
1815
+ if (Option.isSome(quarter)) return quarter;
1816
+ const yearMatch = String$1.match(/^(?:(?:the )?(?:calendar )?year )?(\d{4})$/u)(input);
1817
+ if (Option.isSome(yearMatch)) {
1818
+ const year = validYear(yearMatch.value[1]);
1819
+ if (year !== void 0) return Option.some(fixedYearPeriod(year, String(year)));
1820
+ }
1821
+ const monthYear = String$1.match(/^([a-z]+\.?) (\d{4})$/u)(input);
1822
+ if (Option.isSome(monthYear)) {
1823
+ const month = monthNumber(monthYear.value[1]);
1824
+ const year = validYear(monthYear.value[2]);
1825
+ if (month !== void 0 && year !== void 0) return Option.some(fixedMonthPeriod(year, month, `${title(months[month - 1])} ${year}`));
1826
+ }
1827
+ const relativeMonth = String$1.match(/^([a-z]+\.?) (?:of )?(last|this|next) year$/u)(input);
1828
+ if (Option.isSome(relativeMonth)) {
1829
+ const month = monthNumber(relativeMonth.value[1]);
1830
+ const direction = relativeDirection(relativeMonth.value[2]);
1831
+ if (month !== void 0) return Option.some(monthOfRelativeYear(month, direction, `${title(months[month - 1])} of ${relativeMonth.value[2]} year`));
1832
+ }
1833
+ const standaloneMonth = monthNumber(input);
1834
+ if (standaloneMonth !== void 0) return Option.some(monthOfRelativeYear(standaloneMonth, 0, title(months[standaloneMonth - 1])));
1835
+ if (input === "today") return Option.some(relativePeriod("day", 0, "today"));
1836
+ if (input === "yesterday") return Option.some(relativePeriod("day", -1, "yesterday"));
1837
+ if (input === "tomorrow") return Option.some(relativePeriod("day", 1, "tomorrow"));
1838
+ if (input === "weekend" || input === "the weekend" || input === "this weekend") return Option.some(relativeWeekend(0, "this weekend"));
1839
+ if (input === "last weekend" || input === "previous weekend") return Option.some(relativeWeekend(-1, "last weekend"));
1840
+ if (input === "next weekend" || input === "coming weekend") return Option.some(relativeWeekend(1, "next weekend"));
1841
+ if (input === "the weekend before last") return Option.some(relativeWeekend(-2, "the weekend before last"));
1842
+ if (input === "the weekend after next") return Option.some(relativeWeekend(2, "the weekend after next"));
1843
+ const articlePeriod = String$1.match(/^the (day|week|month|quarter|year)$/u)(input);
1844
+ if (Option.isSome(articlePeriod)) {
1845
+ const entry = units.find((unit) => unit[0] === articlePeriod.value[1]);
1846
+ if (entry !== void 0) return Option.some(relativePeriod(entry[1], 0, currentPeriod(entry[0])));
1847
+ }
1848
+ const outerRelative = String$1.match(/^(?:the )?(day|week|month|quarter|year) (before last|after next)$/u)(input);
1849
+ if (Option.isSome(outerRelative)) {
1850
+ const entry = units.find((unit) => unit[0] === outerRelative.value[1]);
1851
+ if (entry !== void 0) {
1852
+ const direction = outerRelative.value[2] === "before last" ? -2 : 2;
1853
+ return Option.some(relativePeriod(entry[1], direction, input));
1854
+ }
1855
+ }
1856
+ const relative = String$1.match(/^(?:the )?(last|previous|this|current|next|coming|upcoming) ([a-z]+)$/u)(input);
1857
+ if (Option.isSome(relative)) {
1858
+ const unit = units.find((entry) => entry[0] === relative.value[2])?.[1];
1859
+ if (unit !== void 0) {
1860
+ const direction = relativeDirection(relative.value[1]);
1861
+ const canonicalDirection = relativeDirectionName(direction);
1862
+ return Option.some(relativePeriod(unit, direction, `${canonicalDirection} ${relative.value[2]}`));
1863
+ }
1864
+ }
1865
+ return Option.none();
1866
+ };
1867
+ const parsePeriod = (input) => {
1868
+ const edge = String$1.match(/^(?:the )?(start|beginning|end) of (.+)$/u)(input);
1869
+ if (Option.isSome(edge)) {
1870
+ const period = parseBasePeriod(edge.value[2]);
1871
+ if (Option.isSome(period)) {
1872
+ const name = edge.value[1] === "beginning" ? "start" : edge.value[1];
1873
+ const canonical = `${name} of ${period.value.canonical}`;
1874
+ return Option.some(name === "end" ? periodEndDay(period.value, canonical) : periodStartDay(period.value, canonical));
1875
+ }
1876
+ }
1877
+ const wrapper = [
1878
+ "during ",
1879
+ "in ",
1880
+ "for ",
1881
+ "all of ",
1882
+ "the whole of "
1883
+ ].find((prefix) => input.startsWith(prefix));
1884
+ return parseBasePeriod(wrapper === void 0 ? input : input.slice(wrapper.length));
1885
+ };
1886
+ const boundaryCandidate = (input) => openBoundaryCandidate(input, [
1887
+ ["up to and including ", "through"],
1888
+ ["up to including ", "through"],
1889
+ ["since the beginning of ", "since"],
1890
+ ["since the start of ", "since"],
1891
+ ["from the beginning of ", "since"],
1892
+ ["from the start of ", "since"],
1893
+ ["starting from ", "since"],
1894
+ ["through the end of ", "through"],
1895
+ ["until the end of ", "through"],
1896
+ ["after the end of ", "after"],
1897
+ ["until the start of ", "before"],
1898
+ ["before the beginning of ", "before"],
1899
+ ["before the start of ", "before"],
1900
+ ["before beginning of ", "before"],
1901
+ ["before start of ", "before"],
1902
+ ["starting ", "since"],
1903
+ ["through ", "through"],
1904
+ ["before ", "before"],
1905
+ ["since ", "since"],
1906
+ ["after ", "after"],
1907
+ ["until ", "before"],
1908
+ ["till ", "before"],
1909
+ ["up to ", "before"],
1910
+ ["from ", "since"]
1911
+ ], parsePeriod);
1912
+ const countedUnit = (value, amount) => units.find((entry) => value === (amount === 1 ? entry[0] : entry[2]));
1913
+ const parseCalendarOffset = (input) => {
1914
+ const singular = String$1.match(/^(?:(?:a|one) (day|week|month|quarter|year) (ago|prior|from now)|in (?:a|one) (day|week|month|quarter|year))$/u)(input);
1915
+ if (Option.isSome(singular)) {
1916
+ const unitText = singular.value[1] ?? singular.value[3];
1917
+ const entry = units.find((unit) => unit[0] === unitText);
1918
+ if (entry !== void 0) {
1919
+ const isPast = singular.value[2] === "ago" || singular.value[2] === "prior";
1920
+ const direction = isPast ? -1 : 1;
1921
+ const canonical = isPast ? `1 ${entry[0]} ago` : `in 1 ${entry[0]}`;
1922
+ return Option.some(candidate(periodRange(relativePeriod(entry[1], direction, canonical)), canonical));
1923
+ }
1924
+ }
1925
+ const past = String$1.match(/^([1-9]\d*) (day|days|week|weeks|month|months|quarter|quarters|year|years) (?:ago|prior)$/u)(input);
1926
+ const match = [
1927
+ past,
1928
+ String$1.match(/^in ([1-9]\d*) (day|days|week|weeks|month|months|quarter|quarters|year|years)$/u)(input),
1929
+ String$1.match(/^([1-9]\d*) (day|days|week|weeks|month|months|quarter|quarters|year|years) from now$/u)(input)
1930
+ ].find(Option.isSome)?.value;
1931
+ if (match === void 0) return Option.none();
1932
+ const amount = parseTrailingCount(match[1]);
1933
+ if (Option.isNone(amount)) return Option.none();
1934
+ const entry = countedUnit(match[2], amount.value);
1935
+ if (entry === void 0) return Option.none();
1936
+ const unitText = match[2];
1937
+ const direction = Option.isSome(past) ? -amount.value : amount.value;
1938
+ const canonical = direction < 0 ? `${amount.value} ${unitText} ago` : `in ${amount.value} ${unitText}`;
1939
+ return Option.some(candidate(periodRange(relativePeriod(entry[1], direction, canonical)), canonical));
1940
+ };
1941
+ const parseRollingPeriod = (input) => {
1942
+ const past = String$1.match(/^(?:(?:last|previous|past) |(?:in|over) the (?:last|previous|past) )?([1-9]\d*) (day|days|week|weeks|month|months|quarter|quarters|year|years)$/u)(input);
1943
+ const future = String$1.match(/^(?:(?:next|coming|within) |(?:in|over|within) the (?:next|coming) )([1-9]\d*) (day|days|week|weeks|month|months|quarter|quarters|year|years)$/u)(input);
1944
+ const match = [past, future].find(Option.isSome)?.value;
1945
+ if (match === void 0) return Option.none();
1946
+ const amount = parseTrailingCount(match[1]);
1947
+ if (Option.isNone(amount)) return Option.none();
1948
+ const entry = countedUnit(match[2], amount.value);
1949
+ if (entry === void 0) return Option.none();
1950
+ const isFuture = Option.isSome(future);
1951
+ const range = isFuture ? futureRange(amount.value, entry[1]) : trailingRange(amount.value, entry[1]);
1952
+ const direction = isFuture ? "next" : "last";
1953
+ const noun = amount.value === 1 ? entry[0] : entry[2];
1954
+ return Option.some(candidate(range, `${direction} ${amount.value} ${noun}`));
1955
+ };
1956
+ const parseEnglish = (input) => {
1957
+ const remaining = remainingPeriodPhrases.find((entry) => entry.phrase === input);
1958
+ if (remaining !== void 0) return Option.some(candidate(remainingPeriodRange(remaining.entry[1]), `rest of ${remaining.entry[0]}`));
1959
+ if ([
1960
+ "until now",
1961
+ "till now",
1962
+ "up to now",
1963
+ "through now"
1964
+ ].includes(input)) return Option.some(candidate(untilNowRange(), "until now"));
1965
+ if ([
1966
+ "from now",
1967
+ "from now on",
1968
+ "starting now"
1969
+ ].includes(input)) return Option.some(candidate(fromNowRange(), "from now"));
1970
+ const offset = parseCalendarOffset(input);
1971
+ if (Option.isSome(offset)) return offset;
1972
+ const rolling = parseRollingPeriod(input);
1973
+ if (Option.isSome(rolling)) return rolling;
1974
+ const toDate = toDatePhrases.find((entry) => entry.phrase === input);
1975
+ if (toDate !== void 0) return Option.some(candidate(periodToDateRange(toDate.entry[1]), `${toDate.entry[0]} to date`));
1976
+ const nowBounded = joinedNowCandidate(input, [
1977
+ ["from ", " to now"],
1978
+ ["from ", " until now"],
1979
+ ["from ", " through now"],
1980
+ ["between ", " and now"]
1981
+ ], [
1982
+ "from now to ",
1983
+ "from now until ",
1984
+ "from now through ",
1985
+ "between now and "
1986
+ ], parsePeriod, (period) => `from ${period} to now`, (period) => `from now to ${period}`);
1987
+ if (Option.isSome(nowBounded)) return nowBounded;
1988
+ const bounded = joinedPeriodCandidate(input, [
1989
+ ["from ", " to and including "],
1990
+ ["from ", " through and including "],
1991
+ ["from ", " to "],
1992
+ ["from ", " until "],
1993
+ ["from ", " through "],
1994
+ ["from ", " till "],
1995
+ ["between ", " and "],
1996
+ ["between ", " through "],
1997
+ ["", " to and including "],
1998
+ ["", " through and including "],
1999
+ ["", " to "],
2000
+ ["", " until "],
2001
+ ["", " through "],
2002
+ ["", " till "],
2003
+ ["", " - "],
2004
+ ["", " – "],
2005
+ ["", " — "],
2006
+ ["between ", "-"],
2007
+ ["", "-"],
2008
+ ["between ", "–"],
2009
+ ["", "–"],
2010
+ ["between ", "—"],
2011
+ ["", "—"],
2012
+ ["between ", "~"],
2013
+ ["", "~"]
2014
+ ], parsePeriod, (lower, upper) => `from ${lower} to ${upper}`);
2015
+ if (Option.isSome(bounded)) return bounded;
2016
+ const boundary = boundaryCandidate(input);
2017
+ if (Option.isSome(boundary)) return boundary;
2018
+ return Option.map(parsePeriod(input), (period) => candidate(periodRange(period), period.canonical));
2019
+ };
2020
+ const staticPeriods = periodsFromPhrases([
2021
+ "today",
2022
+ "yesterday",
2023
+ "tomorrow",
2024
+ "this weekend",
2025
+ "last weekend",
2026
+ "next weekend",
2027
+ "the weekend before last",
2028
+ "the weekend after next",
2029
+ ...units.flatMap((entry) => [
2030
+ `this ${entry[0]}`,
2031
+ `last ${entry[0]}`,
2032
+ `next ${entry[0]}`,
2033
+ `start of this ${entry[0]}`,
2034
+ `end of this ${entry[0]}`,
2035
+ `start of last ${entry[0]}`,
2036
+ `end of last ${entry[0]}`,
2037
+ `start of next ${entry[0]}`,
2038
+ `end of next ${entry[0]}`
2039
+ ]),
2040
+ ...[
2041
+ 1,
2042
+ 2,
2043
+ 3,
2044
+ 4
2045
+ ].flatMap((quarter) => [
2046
+ `q${quarter}`,
2047
+ `q${quarter} of last year`,
2048
+ `q${quarter} of next year`
2049
+ ]),
2050
+ ...months.flatMap((month) => [
2051
+ month,
2052
+ `${month} of last year`,
2053
+ `${month} of next year`
2054
+ ])
2055
+ ], parsePeriod);
2056
+ const renderEnglish = (range) => {
2057
+ const offset = calendarPeriodOffset(range);
2058
+ if (Option.isSome(offset) && Math.abs(offset.value.amount) > 1) {
2059
+ const entry = units.find((unit) => unit[1] === offset.value.unit);
2060
+ if (entry !== void 0) {
2061
+ const noun = offset.value.amount === -1 || offset.value.amount === 1 ? entry[0] : entry[2];
2062
+ return Option.some(offset.value.amount < 0 ? `${-offset.value.amount} ${noun} ago` : `in ${offset.value.amount} ${noun}`);
2063
+ }
2064
+ }
2065
+ const future = futurePeriod(range);
2066
+ if (Option.isSome(future)) {
2067
+ const entry = units.find((unit) => unit[1] === future.value.unit);
2068
+ if (entry !== void 0) return Option.some(`next ${future.value.amount} ${future.value.amount === 1 ? entry[0] : entry[2]}`);
2069
+ }
2070
+ const trailing = trailingPeriod(range);
2071
+ if (Option.isSome(trailing)) {
2072
+ const entry = units.find((unit) => unit[1] === trailing.value.unit);
2073
+ if (entry !== void 0) return Option.some(trailing.value.amount === 1 ? `1 ${entry[0]}` : `last ${trailing.value.amount} ${entry[2]}`);
2074
+ }
2075
+ const periods = [...staticPeriods, ...periodsFromPhrases([...datedPeriods(range, months), ...datedQuarterPeriods(range)], parsePeriod)];
2076
+ return renderPeriodRange(range, [...units.map((entry) => candidate(periodToDateRange(entry[1]), `${entry[0]} to date`)), ...units.map((entry) => candidate(remainingPeriodRange(entry[1]), `rest of ${entry[0]}`))], periods, (period) => `since ${period}`, (period) => `before ${period}`, (period) => `through ${period}`, (period) => `after ${period}`, (lower, upper) => `from ${lower} to ${upper}`, (period) => `from ${period} to now`, (period) => `from now to ${period}`, () => "until now", () => "from now");
2077
+ };
2078
+ const EnglishContribution = new BaseLanguageContribution({
2079
+ locale: "en",
2080
+ vocabulary: [
2081
+ ...months,
2082
+ ...quarterNames,
2083
+ "q1",
2084
+ "q2",
2085
+ "q3",
2086
+ "q4",
2087
+ ...monthAbbreviations.flatMap((aliases) => aliases),
2088
+ ...toDatePhrases.flatMap((entry) => entry.phrase.split(" ")),
2089
+ ...remainingPeriodPhrases.flatMap((entry) => entry.phrase.split(" ")),
2090
+ ...units.flatMap((entry) => [entry[0], entry[2]]),
2091
+ "after",
2092
+ "ago",
2093
+ "all",
2094
+ "one",
2095
+ "and",
2096
+ "beginning",
2097
+ "before",
2098
+ "calendar",
2099
+ "coming",
2100
+ "current",
2101
+ "date",
2102
+ "during",
2103
+ "end",
2104
+ "far",
2105
+ "for",
2106
+ "from",
2107
+ "in",
2108
+ "including",
2109
+ "last",
2110
+ "next",
2111
+ "now",
2112
+ "on",
2113
+ "over",
2114
+ "of",
2115
+ "past",
2116
+ "previous",
2117
+ "prior",
2118
+ "since",
2119
+ "so",
2120
+ "start",
2121
+ "starting",
2122
+ "this",
2123
+ "through",
2124
+ "till",
2125
+ "whole",
2126
+ "to",
2127
+ "until",
2128
+ "upcoming",
2129
+ "within",
2130
+ "today",
2131
+ "tomorrow",
2132
+ "weekend",
2133
+ "yesterday"
2134
+ ],
2135
+ normalize: normalizeNaturalText,
2136
+ correct: correctWhitespaceSeparatedText,
2137
+ parseExact: parseEnglish,
2138
+ render: renderEnglish
2139
+ });
2140
+ const EnglishLanguage = defineLanguagePlugin({
2141
+ id: "chronolizer/language-en",
2142
+ effect: (context) => Effect.asVoid(context.register("chronolizer/language-en", EnglishContribution))
2143
+ });
2144
+ const EnglishLanguageLayer = languagePluginsLayer([EnglishLanguage]);
2145
+ //#endregion
2146
+ //#region src/locales/default.ts
2147
+ const DefaultLanguageLayer = languagePluginsLayer([EnglishLanguage, GermanLanguage]);
2148
+ //#endregion
2149
+ //#region src/natural/api.ts
2150
+ const distinctCandidates = (candidates) => Array$1.dedupeWith(candidates, (left, right) => rangeKey(left.range) === rangeKey(right.range));
2151
+ const applyFuturePolicy = (candidates, allowFuture) => allowFuture === false ? Array$1.filter(candidates, (candidate) => !containsPositiveShift(candidate.range)) : candidates;
2152
+ const parseQuality = (hasAlternatives, hasCorrections) => {
2153
+ if (hasAlternatives) return "ambiguous";
2154
+ if (hasCorrections) return "corrected";
2155
+ return "exact";
2156
+ };
2157
+ const resultFromCandidates = (candidates, corrections) => {
2158
+ const distinct = Array$1.sortWith(distinctCandidates(candidates), (candidate) => candidate.canonical, Order.String);
2159
+ const selected = Array$1.headNonEmpty(distinct);
2160
+ const alternatives = Array$1.tailNonEmpty(distinct).map((candidate) => NaturalAlternative.make({
2161
+ canonical: candidate.canonical,
2162
+ range: candidate.range
2163
+ }));
2164
+ return NaturalParseResult.make({
2165
+ range: selected.range,
2166
+ quality: parseQuality(alternatives.length > 0, corrections.length > 0),
2167
+ corrections,
2168
+ alternatives
2169
+ });
2170
+ };
2171
+ const parseNatural = Effect.fn("chronolizer.parseNatural")(function* (input, options) {
2172
+ const language = yield* (yield* LanguageRegistry).resolve(options.locale);
2173
+ const normalized = language.normalize(input, language.locale);
2174
+ if (normalized.length === 0) return yield* new NaturalLanguageParseError({
2175
+ input,
2176
+ locale: options.locale,
2177
+ message: "The complete input must contain a date-range expression"
2178
+ });
2179
+ const exact = language.parseExact(normalized);
2180
+ const allowedExact = applyFuturePolicy(exact, options.allowFuture);
2181
+ if (Array$1.isReadonlyArrayNonEmpty(allowedExact)) return resultFromCandidates(allowedExact, []);
2182
+ if (options.typoMode !== "tolerant") {
2183
+ const message = exact.length > 0 && options.allowFuture === false ? "The expression contains a positive relative shift, but future ranges are disabled" : "The complete input is not a supported date-range expression";
2184
+ return yield* new NaturalLanguageParseError({
2185
+ input,
2186
+ locale: options.locale,
2187
+ message
2188
+ });
2189
+ }
2190
+ const corrected = language.correct?.(normalized, language.vocabulary) ?? [];
2191
+ const parsedCorrections = Array$1.filterMap(corrected, (correction) => {
2192
+ const candidates = applyFuturePolicy(language.parseExact(correction.text), options.allowFuture);
2193
+ return Array$1.isReadonlyArrayNonEmpty(candidates) ? Result.succeed({
2194
+ correction,
2195
+ candidates
2196
+ }) : Result.failVoid;
2197
+ });
2198
+ const successful = Array$1.sortWith(parsedCorrections, (entry) => entry.correction.cost, Order.Number);
2199
+ if (!Array$1.isReadonlyArrayNonEmpty(successful)) return yield* new NaturalLanguageParseError({
2200
+ input,
2201
+ locale: options.locale,
2202
+ message: "No conservative typo correction produced a complete expression"
2203
+ });
2204
+ const first = Array$1.headNonEmpty(successful);
2205
+ const best = Array$1.prepend(Array$1.filter(Array$1.tailNonEmpty(successful), (entry) => entry.correction.cost === first.correction.cost), first);
2206
+ return resultFromCandidates(Array$1.flatMap(best, (entry) => entry.candidates), first.correction.corrections);
2207
+ });
2208
+ const formatNatural = Effect.fn("chronolizer.formatNatural")(function* (range, options) {
2209
+ const rendered = (yield* (yield* LanguageRegistry).resolve(options.locale)).render(range);
2210
+ if (Option.isSome(rendered)) return rendered.value;
2211
+ return yield* new NaturalLanguageRenderError({
2212
+ locale: options.locale,
2213
+ message: "The range has no canonical natural-language form in this locale"
2214
+ });
2215
+ });
2216
+ //#endregion
2217
+ //#region src/resolve/schema.ts
2218
+ const ResolvedGreaterThan = Schema.TaggedStruct("GreaterThan", { value: Schema.DateTimeZoned });
2219
+ const ResolvedGreaterThanOrEqual = Schema.TaggedStruct("GreaterThanOrEqual", { value: Schema.DateTimeZoned });
2220
+ const ResolvedLessThan = Schema.TaggedStruct("LessThan", { value: Schema.DateTimeZoned });
2221
+ const ResolvedLessThanOrEqual = Schema.TaggedStruct("LessThanOrEqual", { value: Schema.DateTimeZoned });
2222
+ const ResolvedLowerBound = Schema.Union([ResolvedGreaterThan, ResolvedGreaterThanOrEqual]);
2223
+ const ResolvedUpperBound = Schema.Union([ResolvedLessThan, ResolvedLessThanOrEqual]);
2224
+ const ResolvedBoundedDateRange = Schema.TaggedStruct("ResolvedDateRange", {
2225
+ lower: ResolvedLowerBound,
2226
+ upper: ResolvedUpperBound
2227
+ });
2228
+ const ResolvedLowerOpenDateRange = Schema.TaggedStruct("ResolvedDateRange", {
2229
+ lower: ResolvedLowerBound,
2230
+ upper: Schema.optionalKey(Schema.Never)
2231
+ });
2232
+ const ResolvedUpperOpenDateRange = Schema.TaggedStruct("ResolvedDateRange", {
2233
+ lower: Schema.optionalKey(Schema.Never),
2234
+ upper: ResolvedUpperBound
2235
+ });
2236
+ const ResolvedDateRange = Schema.Union([
2237
+ ResolvedBoundedDateRange,
2238
+ ResolvedLowerOpenDateRange,
2239
+ ResolvedUpperOpenDateRange
2240
+ ]).annotate({ identifier: "ResolvedDateRange" });
2241
+ var ResolutionError = class extends Schema.TaggedError()("ResolutionError", { message: Schema.String }) {};
2242
+ //#endregion
2243
+ //#region src/resolve/resolve.ts
2244
+ const shiftDateTime = (value, amount, unit) => {
2245
+ switch (unit) {
2246
+ case "day": return DateTime.add(value, { days: amount });
2247
+ case "week": return DateTime.add(value, { weeks: amount });
2248
+ case "month": return DateTime.add(value, { months: amount });
2249
+ case "quarter": return DateTime.add(value, { months: amount * 3 });
2250
+ case "year": return DateTime.add(value, { years: amount });
2251
+ }
2252
+ };
2253
+ const startOfDateTime = (value, unit) => {
2254
+ if (unit === "quarter") {
2255
+ const month = DateTime.getPart(value, "month");
2256
+ const quarterMonth = Math.floor((month - 1) / 3) * 3 + 1;
2257
+ return DateTime.startOf(DateTime.setParts(value, { month: quarterMonth }), "month");
2258
+ }
2259
+ return DateTime.startOf(value, unit, { weekStartsOn: 1 });
2260
+ };
2261
+ const literalInZone = (value, zone) => {
2262
+ const zoned = DateTime.makeZoned({
2263
+ year: Number(value.slice(0, 4)),
2264
+ month: Number(value.slice(5, 7)),
2265
+ day: Number(value.slice(8, 10))
2266
+ }, {
2267
+ timeZone: zone,
2268
+ adjustForTimeZone: true
2269
+ });
2270
+ return Option.match(zoned, {
2271
+ onNone: () => Effect.fail(new ResolutionError({ message: `Cannot resolve the date ${value}` })),
2272
+ onSome: Effect.succeed
2273
+ });
2274
+ };
2275
+ const evaluateInstant = (expression, reference, zone) => foldInstant(expression, {
2276
+ now: () => Effect.succeed(reference),
2277
+ dateLiteral: (value) => literalInZone(value, zone),
2278
+ shift: (base, amount, unit) => Effect.map(base, (value) => shiftDateTime(value, amount, unit)),
2279
+ startOf: (base, unit) => Effect.map(base, (value) => startOfDateTime(value, unit))
2280
+ });
2281
+ const resolveLower = (bound, reference, zone) => Match.valueTags(bound, {
2282
+ GreaterThan: (value) => Effect.map(evaluateInstant(value.value, reference, zone), (resolved) => ResolvedGreaterThan.make({ value: resolved })),
2283
+ GreaterThanOrEqual: (value) => Effect.map(evaluateInstant(value.value, reference, zone), (resolved) => ResolvedGreaterThanOrEqual.make({ value: resolved }))
2284
+ });
2285
+ const resolveUpper = (bound, reference, zone) => Match.valueTags(bound, {
2286
+ LessThan: (value) => Effect.map(evaluateInstant(value.value, reference, zone), (resolved) => ResolvedLessThan.make({ value: resolved })),
2287
+ LessThanOrEqual: (value) => Effect.map(evaluateInstant(value.value, reference, zone), (resolved) => ResolvedLessThanOrEqual.make({ value: resolved }))
2288
+ });
2289
+ const resolve = Effect.fn("chronolizer.resolve")(function* (range) {
2290
+ const zone = yield* DateTime.CurrentTimeZone;
2291
+ const reference = yield* DateTime.nowInCurrentZone;
2292
+ if (range.lower !== void 0 && range.upper !== void 0) {
2293
+ const lower = yield* resolveLower(range.lower, reference, zone);
2294
+ const upper = yield* resolveUpper(range.upper, reference, zone);
2295
+ if (DateTime.toEpochMillis(lower.value) >= DateTime.toEpochMillis(upper.value)) return yield* new ResolutionError({ message: "The lower range endpoint must be before the upper endpoint" });
2296
+ return ResolvedDateRange.make({
2297
+ lower,
2298
+ upper
2299
+ });
2300
+ }
2301
+ if (range.lower !== void 0) return ResolvedDateRange.make({ lower: yield* resolveLower(range.lower, reference, zone) });
2302
+ return ResolvedDateRange.make({ upper: yield* resolveUpper(range.upper, reference, zone) });
2303
+ });
2304
+ //#endregion
2305
+ export { AmbiguousNaturalLanguageError, BaseLanguageContribution, BaseLanguageMetadata, Correction, DateExpressionString, DateFilter, DateLiteral, DateRangeExpr, DateRangeFromFilter, DefaultLanguageLayer, EnglishContribution, EnglishLanguage, EnglishLanguageLayer, FilterExpressionParseError, GermanContribution, GermanLanguage, GermanLanguageLayer, GreaterThan, GreaterThanOrEqual, InstantExpr, InstantExpressionFromString, InvalidDateFilterError, IsoDate, LanguageConflictError, LanguageContributionMetadata, LanguageExtensionContribution, LanguageExtensionMetadata, LanguageRegistrationError, LanguageRegistry, LanguageRegistryLayer, LessThan, LessThanOrEqual, Locale, LowerBound, NaturalAlternative, NaturalCandidate, NaturalCorrectionCandidate, NaturalLanguageParseError, NaturalLanguageRenderError, NaturalParseResult, Now, ParseQuality, ResolutionError, ResolvedDateRange, ResolvedGreaterThan, ResolvedGreaterThanOrEqual, ResolvedLessThan, ResolvedLessThanOrEqual, ResolvedLowerBound, ResolvedUpperBound, Shift, StartOf, Unit, UnsupportedLocaleError, UpperBound, boundedRange, canonicalBaseLocale, completePeriod, containsPositiveShift, correctWhitespaceSeparatedText, dateLiteral, daysInMonth, defineLanguagePlugin, foldInstant, formatFilter, formatInstantExpression, formatNatural, greaterThan, greaterThanOrEqual, isIsoDate, languagePluginsLayer, lessThan, lessThanOrEqual, lowerOpenRange, naturalWords, normalizeInstant, normalizeNaturalText, normalizeRange, now, parseFilter, parseInstantExpression, parseNatural, rangeKey, resolve, shift, startOf, upperOpenRange };