chronolizer 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +184 -119
  2. package/dist/ast/constructors.d.mts +135 -0
  3. package/dist/ast/constructors.mjs +25 -0
  4. package/dist/ast/fold.d.mts +13 -0
  5. package/dist/ast/fold.mjs +58 -0
  6. package/dist/ast/normalize.d.mts +42 -0
  7. package/dist/ast/normalize.mjs +34 -0
  8. package/dist/ast/schemas.d.mts +80 -0
  9. package/dist/ast/schemas.mjs +65 -0
  10. package/dist/filter/codec.d.mts +92 -0
  11. package/dist/filter/codec.mjs +48 -0
  12. package/dist/filter/errors.d.mts +14 -0
  13. package/dist/filter/errors.mjs +10 -0
  14. package/dist/filter/expression.d.mts +8 -0
  15. package/dist/filter/expression.mjs +76 -0
  16. package/dist/filter/schema.d.mts +13 -0
  17. package/dist/filter/schema.mjs +11 -0
  18. package/dist/filter/transformation.d.mts +37 -0
  19. package/dist/filter/transformation.mjs +20 -0
  20. package/dist/index.d.mts +22 -969
  21. package/dist/index.mjs +22 -2305
  22. package/dist/language/errors.d.mts +38 -0
  23. package/dist/language/errors.mjs +30 -0
  24. package/dist/language/model.d.mts +239 -0
  25. package/dist/language/model.mjs +50 -0
  26. package/dist/language/registry.d.mts +17 -0
  27. package/dist/language/registry.mjs +116 -0
  28. package/dist/locales/cs.d.mts +10 -0
  29. package/dist/locales/cs.mjs +746 -0
  30. package/dist/locales/de.d.mts +10 -0
  31. package/dist/locales/de.mjs +1039 -0
  32. package/dist/locales/en.d.mts +10 -0
  33. package/dist/locales/en.mjs +598 -0
  34. package/dist/locales/es.d.mts +10 -0
  35. package/dist/locales/es.mjs +908 -0
  36. package/dist/locales/fr.d.mts +10 -0
  37. package/dist/locales/fr.mjs +901 -0
  38. package/dist/locales/nl.d.mts +10 -0
  39. package/dist/locales/nl.mjs +791 -0
  40. package/dist/locales/pl.d.mts +10 -0
  41. package/dist/locales/pl.mjs +885 -0
  42. package/dist/locales/shared.mjs +395 -0
  43. package/dist/locales/tr.d.mts +10 -0
  44. package/dist/locales/tr.mjs +725 -0
  45. package/dist/natural/correction.d.mts +13 -0
  46. package/dist/natural/correction.mjs +73 -0
  47. package/dist/natural/format.d.mts +47 -0
  48. package/dist/natural/format.mjs +14 -0
  49. package/dist/natural/parse.d.mts +99 -0
  50. package/dist/natural/parse.mjs +66 -0
  51. package/dist/natural/policy.mjs +6 -0
  52. package/dist/natural/suggest.d.mts +53 -0
  53. package/dist/natural/suggest.mjs +24 -0
  54. package/dist/natural/suggestion.d.mts +4 -0
  55. package/dist/natural/suggestion.mjs +79 -0
  56. package/dist/natural/text.d.mts +5 -0
  57. package/dist/natural/text.mjs +5 -0
  58. package/dist/resolve/resolve.d.mts +79 -0
  59. package/dist/resolve/resolve.mjs +58 -0
  60. package/dist/resolve/schema.d.mts +59 -0
  61. package/dist/resolve/schema.mjs +28 -0
  62. package/package.json +15 -3
@@ -0,0 +1,13 @@
1
+ //#region src/natural/correction.d.ts
2
+ declare const correctWhitespaceSeparatedText: (input: string, vocabulary: ReadonlyArray<string>) => {
3
+ readonly text: string;
4
+ readonly corrections: readonly {
5
+ readonly original: string;
6
+ readonly replacement: string;
7
+ readonly distance: number;
8
+ readonly offset: number;
9
+ }[];
10
+ readonly cost: number;
11
+ }[];
12
+ //#endregion
13
+ export { correctWhitespaceSeparatedText };
@@ -0,0 +1,73 @@
1
+ import { Correction, NaturalCorrectionCandidate } from "../language/model.mjs";
2
+ import { naturalWords } from "./text.mjs";
3
+ import { Option, String } from "effect";
4
+ //#region src/natural/correction.ts
5
+ const isProtectedValue = (word) => Option.isSome(String.match(/^\d+$/u)(word)) || Option.isSome(String.match(/^\d{4}-\d{2}-\d{2}$/u)(word));
6
+ const damerauLevenshteinDistance = (left, right) => {
7
+ const fallback = left.length + right.length;
8
+ let previousPrevious;
9
+ let previous = Array.from({ length: right.length + 1 }, (_, index) => index);
10
+ for (let row = 1; row <= left.length; row += 1) {
11
+ const current = [row];
12
+ for (let column = 1; column <= right.length; column += 1) {
13
+ const substitution = left.charAt(row - 1) === right.charAt(column - 1) ? 0 : 1;
14
+ const distance = Math.min((previous[column] ?? fallback) + 1, (current[column - 1] ?? fallback) + 1, (previous[column - 1] ?? fallback) + substitution);
15
+ const transposed = previousPrevious !== void 0 && row > 1 && column > 1 && left.charAt(row - 1) === right.charAt(column - 2) && left.charAt(row - 2) === right.charAt(column - 1) ? (previousPrevious[column - 2] ?? fallback) + 1 : fallback;
16
+ current.push(Math.min(distance, transposed));
17
+ }
18
+ previousPrevious = previous;
19
+ previous = current;
20
+ }
21
+ return previous[right.length] ?? fallback;
22
+ };
23
+ const replacementsFor = (word, vocabulary) => {
24
+ if (vocabulary.includes(word) || isProtectedValue(word)) return [{
25
+ word,
26
+ distance: 0
27
+ }];
28
+ if (word.length <= 3) return [];
29
+ const maximum = word.length >= 6 ? 2 : 1;
30
+ const matches = vocabulary.filter((candidate) => Math.abs(candidate.length - word.length) <= maximum).map((candidate) => ({
31
+ word: candidate,
32
+ distance: damerauLevenshteinDistance(word, candidate)
33
+ })).filter((candidate) => candidate.distance <= maximum);
34
+ if (matches.length === 0) return [];
35
+ const minimum = Math.min(...matches.map((candidate) => candidate.distance));
36
+ return matches.filter((candidate) => candidate.distance === minimum).slice(0, 4);
37
+ };
38
+ const correctWhitespaceSeparatedText = (input, vocabulary) => {
39
+ const words = naturalWords(input);
40
+ let partials = [{
41
+ words: [],
42
+ corrections: [],
43
+ cost: 0,
44
+ offset: 0
45
+ }];
46
+ for (const word of words) {
47
+ const replacements = replacementsFor(word, vocabulary);
48
+ if (replacements.length === 0) return [];
49
+ const next = [];
50
+ for (const partial of partials) for (const replacement of replacements) {
51
+ const correction = replacement.distance === 0 ? partial.corrections : [...partial.corrections, Correction.make({
52
+ original: word,
53
+ replacement: replacement.word,
54
+ distance: replacement.distance,
55
+ offset: partial.offset
56
+ })];
57
+ next.push({
58
+ words: [...partial.words, replacement.word],
59
+ corrections: correction,
60
+ cost: partial.cost + replacement.distance,
61
+ offset: partial.offset + word.length + 1
62
+ });
63
+ }
64
+ partials = next.slice(0, 32);
65
+ }
66
+ return partials.filter((partial) => partial.corrections.length > 0).map((partial) => NaturalCorrectionCandidate.make({
67
+ text: partial.words.join(" "),
68
+ corrections: partial.corrections,
69
+ cost: partial.cost
70
+ }));
71
+ };
72
+ //#endregion
73
+ export { correctWhitespaceSeparatedText, damerauLevenshteinDistance };
@@ -0,0 +1,47 @@
1
+ import { InstantExpr } from "../ast/schemas.mjs";
2
+ import { NaturalLanguageRenderError, UnsupportedLocaleError } from "../language/errors.mjs";
3
+ import { LanguageRegistry } from "../language/registry.mjs";
4
+ import { Effect } from "effect";
5
+ //#region src/natural/format.d.ts
6
+ interface FormatNaturalOptions {
7
+ readonly locale: string;
8
+ }
9
+ declare const formatNatural: (range: {
10
+ readonly _tag: "DateRange";
11
+ readonly lower: {
12
+ readonly _tag: "GreaterThan";
13
+ readonly value: InstantExpr;
14
+ } | {
15
+ readonly _tag: "GreaterThanOrEqual";
16
+ readonly value: InstantExpr;
17
+ };
18
+ readonly upper: {
19
+ readonly _tag: "LessThan";
20
+ readonly value: InstantExpr;
21
+ } | {
22
+ readonly _tag: "LessThanOrEqual";
23
+ readonly value: InstantExpr;
24
+ };
25
+ } | {
26
+ readonly _tag: "DateRange";
27
+ readonly lower: {
28
+ readonly _tag: "GreaterThan";
29
+ readonly value: InstantExpr;
30
+ } | {
31
+ readonly _tag: "GreaterThanOrEqual";
32
+ readonly value: InstantExpr;
33
+ };
34
+ readonly upper?: never;
35
+ } | {
36
+ readonly _tag: "DateRange";
37
+ readonly lower?: never;
38
+ readonly upper: {
39
+ readonly _tag: "LessThan";
40
+ readonly value: InstantExpr;
41
+ } | {
42
+ readonly _tag: "LessThanOrEqual";
43
+ readonly value: InstantExpr;
44
+ };
45
+ }, options: FormatNaturalOptions) => Effect.Effect<string, NaturalLanguageRenderError | UnsupportedLocaleError, LanguageRegistry>;
46
+ //#endregion
47
+ export { FormatNaturalOptions, formatNatural };
@@ -0,0 +1,14 @@
1
+ import { NaturalLanguageRenderError } from "../language/errors.mjs";
2
+ import { LanguageRegistry } from "../language/registry.mjs";
3
+ import { Effect, Option } from "effect";
4
+ //#region src/natural/format.ts
5
+ const formatNatural = Effect.fn("chronolizer.formatNatural")(function* (range, options) {
6
+ const rendered = (yield* (yield* LanguageRegistry).resolve(options.locale)).render(range);
7
+ if (Option.isSome(rendered)) return rendered.value;
8
+ return yield* new NaturalLanguageRenderError({
9
+ locale: options.locale,
10
+ message: "The range has no canonical natural-language form in this locale"
11
+ });
12
+ });
13
+ //#endregion
14
+ export { formatNatural };
@@ -0,0 +1,99 @@
1
+ import { InstantExpr } from "../ast/schemas.mjs";
2
+ import { NaturalLanguageParseError, UnsupportedLocaleError } from "../language/errors.mjs";
3
+ import { LanguageRegistry } from "../language/registry.mjs";
4
+ import "../index.mjs";
5
+ import { Effect } from "effect";
6
+ //#region src/natural/parse.d.ts
7
+ interface ParseNaturalOptions {
8
+ readonly locale: string;
9
+ readonly typoMode?: "strict" | "tolerant";
10
+ readonly allowFuture?: boolean;
11
+ }
12
+ declare const parseNatural: (input: string, options: ParseNaturalOptions) => Effect.Effect<{
13
+ readonly range: {
14
+ readonly _tag: "DateRange";
15
+ readonly lower: {
16
+ readonly _tag: "GreaterThan";
17
+ readonly value: InstantExpr;
18
+ } | {
19
+ readonly _tag: "GreaterThanOrEqual";
20
+ readonly value: InstantExpr;
21
+ };
22
+ readonly upper: {
23
+ readonly _tag: "LessThan";
24
+ readonly value: InstantExpr;
25
+ } | {
26
+ readonly _tag: "LessThanOrEqual";
27
+ readonly value: InstantExpr;
28
+ };
29
+ } | {
30
+ readonly _tag: "DateRange";
31
+ readonly lower: {
32
+ readonly _tag: "GreaterThan";
33
+ readonly value: InstantExpr;
34
+ } | {
35
+ readonly _tag: "GreaterThanOrEqual";
36
+ readonly value: InstantExpr;
37
+ };
38
+ readonly upper?: never;
39
+ } | {
40
+ readonly _tag: "DateRange";
41
+ readonly lower?: never;
42
+ readonly upper: {
43
+ readonly _tag: "LessThan";
44
+ readonly value: InstantExpr;
45
+ } | {
46
+ readonly _tag: "LessThanOrEqual";
47
+ readonly value: InstantExpr;
48
+ };
49
+ };
50
+ readonly quality: "ambiguous" | "corrected" | "exact";
51
+ readonly corrections: readonly {
52
+ readonly original: string;
53
+ readonly replacement: string;
54
+ readonly distance: number;
55
+ readonly offset: number;
56
+ }[];
57
+ readonly alternatives: readonly {
58
+ readonly range: {
59
+ readonly _tag: "DateRange";
60
+ readonly lower: {
61
+ readonly _tag: "GreaterThan";
62
+ readonly value: InstantExpr;
63
+ } | {
64
+ readonly _tag: "GreaterThanOrEqual";
65
+ readonly value: InstantExpr;
66
+ };
67
+ readonly upper: {
68
+ readonly _tag: "LessThan";
69
+ readonly value: InstantExpr;
70
+ } | {
71
+ readonly _tag: "LessThanOrEqual";
72
+ readonly value: InstantExpr;
73
+ };
74
+ } | {
75
+ readonly _tag: "DateRange";
76
+ readonly lower: {
77
+ readonly _tag: "GreaterThan";
78
+ readonly value: InstantExpr;
79
+ } | {
80
+ readonly _tag: "GreaterThanOrEqual";
81
+ readonly value: InstantExpr;
82
+ };
83
+ readonly upper?: never;
84
+ } | {
85
+ readonly _tag: "DateRange";
86
+ readonly lower?: never;
87
+ readonly upper: {
88
+ readonly _tag: "LessThan";
89
+ readonly value: InstantExpr;
90
+ } | {
91
+ readonly _tag: "LessThanOrEqual";
92
+ readonly value: InstantExpr;
93
+ };
94
+ };
95
+ readonly canonical: string;
96
+ }[];
97
+ }, NaturalLanguageParseError | UnsupportedLocaleError, LanguageRegistry>;
98
+ //#endregion
99
+ export { ParseNaturalOptions, parseNatural };
@@ -0,0 +1,66 @@
1
+ import { rangeKey } from "../filter/codec.mjs";
2
+ import { NaturalLanguageParseError } from "../language/errors.mjs";
3
+ import { NaturalAlternative, NaturalParseResult } from "../language/model.mjs";
4
+ import { LanguageRegistry } from "../language/registry.mjs";
5
+ import { applyFuturePolicy } from "./policy.mjs";
6
+ import { Array, Effect, Order, Result } from "effect";
7
+ //#region src/natural/parse.ts
8
+ const distinctCandidates = (candidates) => Array.dedupeWith(candidates, (left, right) => rangeKey(left.range) === rangeKey(right.range));
9
+ const parseQuality = (hasAlternatives, hasCorrections) => {
10
+ if (hasAlternatives) return "ambiguous";
11
+ if (hasCorrections) return "corrected";
12
+ return "exact";
13
+ };
14
+ const resultFromCandidates = (candidates, corrections) => {
15
+ const distinct = distinctCandidates(candidates);
16
+ const selected = Array.headNonEmpty(distinct);
17
+ const alternatives = Array.tailNonEmpty(distinct).map((candidate) => NaturalAlternative.make({
18
+ canonical: candidate.canonical,
19
+ range: candidate.range
20
+ }));
21
+ return NaturalParseResult.make({
22
+ range: selected.range,
23
+ quality: parseQuality(alternatives.length > 0, corrections.length > 0),
24
+ corrections,
25
+ alternatives
26
+ });
27
+ };
28
+ const parseNatural = Effect.fn("chronolizer.parseNatural")(function* (input, options) {
29
+ const language = yield* (yield* LanguageRegistry).resolve(options.locale);
30
+ const normalized = language.normalize(input, language.locale);
31
+ if (normalized.length === 0) return yield* new NaturalLanguageParseError({
32
+ input,
33
+ locale: options.locale,
34
+ message: "The complete input must contain a date-range expression"
35
+ });
36
+ const exact = language.parseExact(normalized);
37
+ const allowedExact = applyFuturePolicy(exact, options.allowFuture);
38
+ if (Array.isReadonlyArrayNonEmpty(allowedExact)) return resultFromCandidates(allowedExact, []);
39
+ if (options.typoMode !== "tolerant") {
40
+ 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";
41
+ return yield* new NaturalLanguageParseError({
42
+ input,
43
+ locale: options.locale,
44
+ message
45
+ });
46
+ }
47
+ const corrected = language.correct?.(normalized, language.vocabulary) ?? [];
48
+ const parsedCorrections = Array.filterMap(corrected, (correction) => {
49
+ const candidates = applyFuturePolicy(language.parseExact(correction.text), options.allowFuture);
50
+ return Array.isReadonlyArrayNonEmpty(candidates) ? Result.succeed({
51
+ correction,
52
+ candidates
53
+ }) : Result.failVoid;
54
+ });
55
+ const successful = Array.sortWith(parsedCorrections, (entry) => entry.correction.cost, Order.Number);
56
+ if (!Array.isReadonlyArrayNonEmpty(successful)) return yield* new NaturalLanguageParseError({
57
+ input,
58
+ locale: options.locale,
59
+ message: "No conservative typo correction produced a complete expression"
60
+ });
61
+ const first = Array.headNonEmpty(successful);
62
+ const best = Array.prepend(Array.takeWhile(Array.tailNonEmpty(successful), (entry) => entry.correction.cost === first.correction.cost), first);
63
+ return resultFromCandidates(Array.flatMap(best, (entry) => entry.candidates), first.correction.corrections);
64
+ });
65
+ //#endregion
66
+ export { parseNatural };
@@ -0,0 +1,6 @@
1
+ import { containsPositiveShift, isCurrentPeriod } from "../ast/fold.mjs";
2
+ import { Array } from "effect";
3
+ //#region src/natural/policy.ts
4
+ const applyFuturePolicy = (candidates, allowFuture) => allowFuture === false ? Array.filter(candidates, (candidate) => isCurrentPeriod(candidate.range) || !containsPositiveShift(candidate.range)) : candidates;
5
+ //#endregion
6
+ export { applyFuturePolicy };
@@ -0,0 +1,53 @@
1
+ import { InstantExpr } from "../ast/schemas.mjs";
2
+ import { UnsupportedLocaleError } from "../language/errors.mjs";
3
+ import { LanguageRegistry } from "../language/registry.mjs";
4
+ import "../index.mjs";
5
+ import { Effect } from "effect";
6
+ //#region src/natural/suggest.d.ts
7
+ interface SuggestNaturalOptions {
8
+ readonly locale: string;
9
+ readonly limit?: number;
10
+ readonly allowFuture?: boolean;
11
+ }
12
+ declare const suggestNatural: (input: string, options: SuggestNaturalOptions) => Effect.Effect<{
13
+ readonly text: string;
14
+ readonly range: {
15
+ readonly _tag: "DateRange";
16
+ readonly lower: {
17
+ readonly _tag: "GreaterThan";
18
+ readonly value: InstantExpr;
19
+ } | {
20
+ readonly _tag: "GreaterThanOrEqual";
21
+ readonly value: InstantExpr;
22
+ };
23
+ readonly upper: {
24
+ readonly _tag: "LessThan";
25
+ readonly value: InstantExpr;
26
+ } | {
27
+ readonly _tag: "LessThanOrEqual";
28
+ readonly value: InstantExpr;
29
+ };
30
+ } | {
31
+ readonly _tag: "DateRange";
32
+ readonly lower: {
33
+ readonly _tag: "GreaterThan";
34
+ readonly value: InstantExpr;
35
+ } | {
36
+ readonly _tag: "GreaterThanOrEqual";
37
+ readonly value: InstantExpr;
38
+ };
39
+ readonly upper?: never;
40
+ } | {
41
+ readonly _tag: "DateRange";
42
+ readonly lower?: never;
43
+ readonly upper: {
44
+ readonly _tag: "LessThan";
45
+ readonly value: InstantExpr;
46
+ } | {
47
+ readonly _tag: "LessThanOrEqual";
48
+ readonly value: InstantExpr;
49
+ };
50
+ };
51
+ }[], UnsupportedLocaleError, LanguageRegistry>;
52
+ //#endregion
53
+ export { SuggestNaturalOptions, suggestNatural };
@@ -0,0 +1,24 @@
1
+ import { rangeKey } from "../filter/codec.mjs";
2
+ import { NaturalSuggestion } from "../language/model.mjs";
3
+ import { LanguageRegistry } from "../language/registry.mjs";
4
+ import { applyFuturePolicy } from "./policy.mjs";
5
+ import { Array, Effect } from "effect";
6
+ //#region src/natural/suggest.ts
7
+ const suggestionLimit = (limit) => {
8
+ if (limit === void 0) return 10;
9
+ if (!Number.isSafeInteger(limit) || limit <= 0) return 0;
10
+ return Math.min(limit, 100);
11
+ };
12
+ const suggestNatural = Effect.fn("chronolizer.suggestNatural")(function* (input, options) {
13
+ const language = yield* (yield* LanguageRegistry).resolve(options.locale);
14
+ const normalized = language.normalize(input, language.locale);
15
+ const limit = suggestionLimit(options.limit);
16
+ if (limit === 0) return [];
17
+ const suggestions = Array.flatMap(Array.dedupe(language.suggest(normalized, limit * 2)), (text) => applyFuturePolicy(language.parseExact(language.normalize(text, language.locale)), options.allowFuture).map((candidate) => NaturalSuggestion.make({
18
+ text: candidate.canonical,
19
+ range: candidate.range
20
+ })));
21
+ return Array.take(Array.dedupeWith(suggestions, (left, right) => rangeKey(left.range) === rangeKey(right.range)), limit);
22
+ });
23
+ //#endregion
24
+ export { suggestNatural };
@@ -0,0 +1,4 @@
1
+ //#region src/natural/suggestion.d.ts
2
+ declare const completeNaturalPhrases: (input: string, phrases: ReadonlyArray<string>, limit: number) => string[];
3
+ //#endregion
4
+ export { completeNaturalPhrases };
@@ -0,0 +1,79 @@
1
+ import { damerauLevenshteinDistance } from "./correction.mjs";
2
+ import { Array as Array$1, Option, String } from "effect";
3
+ //#region src/natural/suggestion.ts
4
+ const fuzzyPrefixDistance = (input, target) => {
5
+ const lengths = Array$1.dedupe([
6
+ Math.max(1, input.length - 1),
7
+ input.length,
8
+ Math.min(target.length, input.length + 1)
9
+ ]);
10
+ return Math.min(...lengths.map((length) => damerauLevenshteinDistance(input, target.slice(0, length))));
11
+ };
12
+ const completionCost = (input, target) => {
13
+ if (input === target || target.startsWith(input)) return 0;
14
+ if (input.length < 3 || input[0] !== target[0]) return void 0;
15
+ const maximum = input.length >= 5 ? 2 : 1;
16
+ const distance = fuzzyPrefixDistance(input, target);
17
+ return distance <= maximum ? distance : void 0;
18
+ };
19
+ const phraseScore = (input, phrase) => {
20
+ if (input === phrase) return 0;
21
+ if (phrase.startsWith(input)) return 1;
22
+ const inputWords = input.split(" ");
23
+ const phraseWords = phrase.split(" ");
24
+ if (inputWords.length > phraseWords.length) return void 0;
25
+ let cost = 0;
26
+ for (const [index, inputWord] of inputWords.entries()) {
27
+ const targetWord = phraseWords[index];
28
+ if (targetWord === void 0) return void 0;
29
+ const wordCost = completionCost(inputWord, targetWord);
30
+ if (wordCost === void 0) return void 0;
31
+ cost += wordCost;
32
+ }
33
+ return cost === 0 ? 2 : 2 + cost;
34
+ };
35
+ const completeYearPrefix = (input) => {
36
+ const match = String.match(/(?:^| )(\d{3,4})$/u)(input);
37
+ if (Option.isNone(match)) return [];
38
+ const prefix = match.value[1] ?? "";
39
+ if (prefix.length === 4) {
40
+ const year = Number(prefix);
41
+ return year >= 1 && year <= 9998 ? [prefix] : [];
42
+ }
43
+ return Array.from({ length: 10 }, (_, digit) => `${prefix}${digit}`).filter((year) => {
44
+ const value = Number(year);
45
+ return value >= 1 && value <= 9998;
46
+ });
47
+ };
48
+ const fixedCalendarPeriodPhrases = (input, months) => completeYearPrefix(input).flatMap((year) => [
49
+ year,
50
+ ...months.map((month) => `${month} ${year}`),
51
+ ...[
52
+ 1,
53
+ 2,
54
+ 3,
55
+ 4
56
+ ].map((quarter) => `q${quarter} ${year}`)
57
+ ]);
58
+ const prefixNaturalPhrases = (phrases, prefixes) => phrases.flatMap((phrase) => prefixes.map((prefix) => `${prefix}${phrase}`));
59
+ const naturalCount = (input) => {
60
+ const match = String.match(/(?:^| )([1-9]\d*)(?: |$)/u)(input);
61
+ if (Option.isNone(match)) return void 0;
62
+ const value = Number(match.value[1]);
63
+ return Number.isSafeInteger(value) ? value : void 0;
64
+ };
65
+ const completeNaturalPhrases = (input, phrases, limit) => {
66
+ const ranked = [];
67
+ for (const [index, phrase] of Array$1.dedupe(phrases).entries()) {
68
+ const score = phraseScore(input, phrase);
69
+ if (score !== void 0) ranked.push({
70
+ phrase,
71
+ score,
72
+ index
73
+ });
74
+ }
75
+ const sorted = ranked.sort((left, right) => left.score - right.score || left.index - right.index);
76
+ return (sorted.some((entry) => entry.score <= 1) ? sorted.filter((entry) => entry.score <= 1) : sorted).slice(0, limit).map((entry) => entry.phrase);
77
+ };
78
+ //#endregion
79
+ export { completeNaturalPhrases, fixedCalendarPeriodPhrases, naturalCount, prefixNaturalPhrases };
@@ -0,0 +1,5 @@
1
+ //#region src/natural/text.d.ts
2
+ declare const normalizeNaturalText: (input: string, locale: string) => string;
3
+ declare const naturalWords: (input: string) => string[];
4
+ //#endregion
5
+ export { naturalWords, normalizeNaturalText };
@@ -0,0 +1,5 @@
1
+ //#region src/natural/text.ts
2
+ const normalizeNaturalText = (input, locale) => input.normalize("NFKC").toLocaleLowerCase(locale).trim().replace(/\s+/gu, " ");
3
+ const naturalWords = (input) => input.length === 0 ? [] : input.split(" ");
4
+ //#endregion
5
+ export { naturalWords, normalizeNaturalText };
@@ -0,0 +1,79 @@
1
+ import { InstantExpr } from "../ast/schemas.mjs";
2
+ import { ResolutionError } from "./schema.mjs";
3
+ import { DateTime, Effect } from "effect";
4
+ //#region src/resolve/resolve.d.ts
5
+ declare const resolve: (range: {
6
+ readonly _tag: "DateRange";
7
+ readonly lower: {
8
+ readonly _tag: "GreaterThan";
9
+ readonly value: InstantExpr;
10
+ } | {
11
+ readonly _tag: "GreaterThanOrEqual";
12
+ readonly value: InstantExpr;
13
+ };
14
+ readonly upper: {
15
+ readonly _tag: "LessThan";
16
+ readonly value: InstantExpr;
17
+ } | {
18
+ readonly _tag: "LessThanOrEqual";
19
+ readonly value: InstantExpr;
20
+ };
21
+ } | {
22
+ readonly _tag: "DateRange";
23
+ readonly lower: {
24
+ readonly _tag: "GreaterThan";
25
+ readonly value: InstantExpr;
26
+ } | {
27
+ readonly _tag: "GreaterThanOrEqual";
28
+ readonly value: InstantExpr;
29
+ };
30
+ readonly upper?: never;
31
+ } | {
32
+ readonly _tag: "DateRange";
33
+ readonly lower?: never;
34
+ readonly upper: {
35
+ readonly _tag: "LessThan";
36
+ readonly value: InstantExpr;
37
+ } | {
38
+ readonly _tag: "LessThanOrEqual";
39
+ readonly value: InstantExpr;
40
+ };
41
+ }) => Effect.Effect<{
42
+ readonly _tag: "ResolvedDateRange";
43
+ readonly lower: {
44
+ readonly _tag: "GreaterThan";
45
+ readonly value: DateTime.Zoned;
46
+ } | {
47
+ readonly _tag: "GreaterThanOrEqual";
48
+ readonly value: DateTime.Zoned;
49
+ };
50
+ readonly upper: {
51
+ readonly _tag: "LessThan";
52
+ readonly value: DateTime.Zoned;
53
+ } | {
54
+ readonly _tag: "LessThanOrEqual";
55
+ readonly value: DateTime.Zoned;
56
+ };
57
+ } | {
58
+ readonly _tag: "ResolvedDateRange";
59
+ readonly lower: {
60
+ readonly _tag: "GreaterThan";
61
+ readonly value: DateTime.Zoned;
62
+ } | {
63
+ readonly _tag: "GreaterThanOrEqual";
64
+ readonly value: DateTime.Zoned;
65
+ };
66
+ readonly upper?: never;
67
+ } | {
68
+ readonly _tag: "ResolvedDateRange";
69
+ readonly lower?: never;
70
+ readonly upper: {
71
+ readonly _tag: "LessThan";
72
+ readonly value: DateTime.Zoned;
73
+ } | {
74
+ readonly _tag: "LessThanOrEqual";
75
+ readonly value: DateTime.Zoned;
76
+ };
77
+ }, ResolutionError, DateTime.CurrentTimeZone>;
78
+ //#endregion
79
+ export { resolve };
@@ -0,0 +1,58 @@
1
+ import { foldInstant } from "../ast/fold.mjs";
2
+ import { ResolutionError, ResolvedDateRange, ResolvedGreaterThan, ResolvedGreaterThanOrEqual, ResolvedLessThan, ResolvedLessThanOrEqual } from "./schema.mjs";
3
+ import { DateTime, Effect, Match, Option } from "effect";
4
+ //#region src/resolve/resolve.ts
5
+ const shiftDateTime = (value, amount, unit) => Match.value(unit).pipe(Match.when("day", () => DateTime.add(value, { days: amount })), Match.when("week", () => DateTime.add(value, { weeks: amount })), Match.when("month", () => DateTime.add(value, { months: amount })), Match.when("quarter", () => DateTime.add(value, { months: amount * 3 })), Match.when("year", () => DateTime.add(value, { years: amount })), Match.exhaustive);
6
+ const startOfDateTime = (value, unit) => {
7
+ if (unit === "quarter") {
8
+ const month = DateTime.getPart(value, "month");
9
+ const quarterMonth = Math.floor((month - 1) / 3) * 3 + 1;
10
+ return DateTime.startOf(DateTime.setParts(value, { month: quarterMonth }), "month");
11
+ }
12
+ return DateTime.startOf(value, unit, { weekStartsOn: 1 });
13
+ };
14
+ const literalInZone = (value, zone) => {
15
+ const zoned = DateTime.makeZoned({
16
+ year: Number(value.slice(0, 4)),
17
+ month: Number(value.slice(5, 7)),
18
+ day: Number(value.slice(8, 10))
19
+ }, {
20
+ timeZone: zone,
21
+ adjustForTimeZone: true
22
+ });
23
+ return Option.match(zoned, {
24
+ onNone: () => Effect.fail(new ResolutionError({ message: `Cannot resolve the date ${value}` })),
25
+ onSome: Effect.succeed
26
+ });
27
+ };
28
+ const evaluateInstant = (expression, reference, zone) => foldInstant(expression, {
29
+ now: () => Effect.succeed(reference),
30
+ dateLiteral: (value) => literalInZone(value, zone),
31
+ shift: (base, amount, unit) => Effect.map(base, (value) => shiftDateTime(value, amount, unit)),
32
+ startOf: (base, unit) => Effect.map(base, (value) => startOfDateTime(value, unit))
33
+ });
34
+ const resolveLower = (bound, reference, zone) => Match.valueTags(bound, {
35
+ GreaterThan: (value) => Effect.map(evaluateInstant(value.value, reference, zone), (resolved) => ResolvedGreaterThan.make({ value: resolved })),
36
+ GreaterThanOrEqual: (value) => Effect.map(evaluateInstant(value.value, reference, zone), (resolved) => ResolvedGreaterThanOrEqual.make({ value: resolved }))
37
+ });
38
+ const resolveUpper = (bound, reference, zone) => Match.valueTags(bound, {
39
+ LessThan: (value) => Effect.map(evaluateInstant(value.value, reference, zone), (resolved) => ResolvedLessThan.make({ value: resolved })),
40
+ LessThanOrEqual: (value) => Effect.map(evaluateInstant(value.value, reference, zone), (resolved) => ResolvedLessThanOrEqual.make({ value: resolved }))
41
+ });
42
+ const resolve = Effect.fn("chronolizer.resolve")(function* (range) {
43
+ const zone = yield* DateTime.CurrentTimeZone;
44
+ const reference = yield* DateTime.nowInCurrentZone;
45
+ if (range.lower !== void 0 && range.upper !== void 0) {
46
+ const lower = yield* resolveLower(range.lower, reference, zone);
47
+ const upper = yield* resolveUpper(range.upper, reference, zone);
48
+ if (DateTime.toEpochMillis(lower.value) >= DateTime.toEpochMillis(upper.value)) return yield* new ResolutionError({ message: "The lower range endpoint must be before the upper endpoint" });
49
+ return ResolvedDateRange.make({
50
+ lower,
51
+ upper
52
+ });
53
+ }
54
+ if (range.lower !== void 0) return ResolvedDateRange.make({ lower: yield* resolveLower(range.lower, reference, zone) });
55
+ return ResolvedDateRange.make({ upper: yield* resolveUpper(range.upper, reference, zone) });
56
+ });
57
+ //#endregion
58
+ export { resolve };