chronolizer 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/dist/ast/fold.mjs +8 -10
- package/dist/ast/normalize.mjs +7 -16
- package/dist/filter/codec.mjs +5 -6
- package/dist/filter/expression.mjs +2 -3
- package/dist/language/registry.mjs +4 -2
- package/dist/locales/cs.mjs +9 -20
- package/dist/locales/de.mjs +199 -495
- package/dist/locales/en.mjs +13 -13
- package/dist/locales/es.mjs +10 -19
- package/dist/locales/fr.mjs +20 -35
- package/dist/locales/nl.mjs +8 -19
- package/dist/locales/pl.mjs +8 -19
- package/dist/locales/shared.mjs +7 -7
- package/dist/locales/tr.mjs +9 -21
- package/dist/natural/correction.mjs +70 -27
- package/dist/natural/parse.mjs +1 -1
- package/dist/natural/suggest.mjs +9 -1
- package/dist/natural/suggestion.mjs +44 -26
- package/dist/resolve/resolve.mjs +1 -2
- package/package.json +2 -1
|
@@ -1,24 +1,41 @@
|
|
|
1
1
|
import { Correction, NaturalCorrectionCandidate } from "../language/model.mjs";
|
|
2
2
|
import { naturalWords } from "./text.mjs";
|
|
3
|
-
import { Option, String } from "effect";
|
|
4
3
|
//#region src/natural/correction.ts
|
|
5
|
-
const isProtectedValue = (word) =>
|
|
6
|
-
const damerauLevenshteinDistance = (left, right)
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
4
|
+
const isProtectedValue = (word) => /^(?:\d+|\d{4}-\d{2}-\d{2})$/u.test(word);
|
|
5
|
+
const damerauLevenshteinDistance = (left, right, maximum = Math.max(left.length, right.length), workspace = [
|
|
6
|
+
[],
|
|
7
|
+
[],
|
|
8
|
+
[]
|
|
9
|
+
]) => {
|
|
10
|
+
const outside = maximum + 1;
|
|
11
|
+
if (Math.abs(left.length - right.length) > maximum) return outside;
|
|
12
|
+
const width = right.length + 1;
|
|
13
|
+
let [previousPrevious, previous, current] = workspace;
|
|
14
|
+
for (const row of workspace) {
|
|
15
|
+
row.length = width;
|
|
16
|
+
row.fill(outside);
|
|
17
|
+
}
|
|
18
|
+
for (let column = 0; column <= Math.min(right.length, maximum); column += 1) previous[column] = column;
|
|
10
19
|
for (let row = 1; row <= left.length; row += 1) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
20
|
+
current.fill(outside);
|
|
21
|
+
if (row <= maximum) current[0] = row;
|
|
22
|
+
const firstColumn = Math.max(1, row - maximum);
|
|
23
|
+
const lastColumn = Math.min(right.length, row + maximum);
|
|
24
|
+
let rowMinimum = row <= maximum ? row : outside;
|
|
25
|
+
for (let column = firstColumn; column <= lastColumn; column += 1) {
|
|
26
|
+
const substitution = left.charCodeAt(row - 1) === right.charCodeAt(column - 1) ? 0 : 1;
|
|
27
|
+
let distance = Math.min((previous[column] ?? outside) + 1, (current[column - 1] ?? outside) + 1, (previous[column - 1] ?? outside) + substitution);
|
|
28
|
+
if (row > 1 && column > 1 && left.charCodeAt(row - 1) === right.charCodeAt(column - 2) && left.charCodeAt(row - 2) === right.charCodeAt(column - 1)) distance = Math.min(distance, (previousPrevious[column - 2] ?? outside) + 1);
|
|
29
|
+
current[column] = distance;
|
|
30
|
+
rowMinimum = Math.min(rowMinimum, distance);
|
|
17
31
|
}
|
|
32
|
+
if (rowMinimum > maximum) return outside;
|
|
33
|
+
const reusable = previousPrevious;
|
|
18
34
|
previousPrevious = previous;
|
|
19
35
|
previous = current;
|
|
36
|
+
current = reusable;
|
|
20
37
|
}
|
|
21
|
-
return previous[right.length] ??
|
|
38
|
+
return previous[right.length] ?? outside;
|
|
22
39
|
};
|
|
23
40
|
const segmentedReplacements = (word, vocabulary) => {
|
|
24
41
|
const segmentations = Array.from({ length: word.length + 1 }, () => []);
|
|
@@ -43,34 +60,60 @@ const segmentedReplacements = (word, vocabulary) => {
|
|
|
43
60
|
distance: parts.length - 1
|
|
44
61
|
}));
|
|
45
62
|
};
|
|
46
|
-
const
|
|
47
|
-
|
|
63
|
+
const vocabularySets = /* @__PURE__ */ new WeakMap();
|
|
64
|
+
const vocabularySet = (vocabulary) => {
|
|
65
|
+
const cached = vocabularySets.get(vocabulary);
|
|
66
|
+
if (cached !== void 0) return cached;
|
|
67
|
+
const words = new Set(vocabulary);
|
|
68
|
+
vocabularySets.set(vocabulary, words);
|
|
69
|
+
return words;
|
|
70
|
+
};
|
|
71
|
+
const replacementsFor = (word, vocabulary, segmentationVocabulary, workspace) => {
|
|
72
|
+
if (vocabulary.has(word) || isProtectedValue(word)) return [{
|
|
48
73
|
word,
|
|
49
74
|
distance: 0
|
|
50
75
|
}];
|
|
51
76
|
const segmented = segmentedReplacements(word, segmentationVocabulary);
|
|
52
77
|
if (word.length <= 3) return segmented;
|
|
53
78
|
const maximum = word.length >= 6 ? 2 : 1;
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
79
|
+
let minimum = Number.POSITIVE_INFINITY;
|
|
80
|
+
const matches = [];
|
|
81
|
+
const addMatch = (replacement, distance) => {
|
|
82
|
+
if (distance > minimum) return;
|
|
83
|
+
if (distance < minimum) {
|
|
84
|
+
minimum = distance;
|
|
85
|
+
matches.length = 0;
|
|
86
|
+
}
|
|
87
|
+
if (matches.length < 4) matches.push({
|
|
88
|
+
word: replacement,
|
|
89
|
+
distance
|
|
90
|
+
});
|
|
91
|
+
};
|
|
92
|
+
for (const replacement of segmented) addMatch(replacement.word, replacement.distance);
|
|
93
|
+
for (const candidate of vocabulary) {
|
|
94
|
+
if (Math.abs(candidate.length - word.length) > maximum) continue;
|
|
95
|
+
const distance = damerauLevenshteinDistance(word, candidate, maximum, workspace);
|
|
96
|
+
if (distance <= maximum) addMatch(candidate, distance);
|
|
97
|
+
}
|
|
98
|
+
return matches;
|
|
62
99
|
};
|
|
63
100
|
const emptySegmentationVocabulary = /* @__PURE__ */ new Set();
|
|
64
101
|
const correctWhitespaceSeparatedText = (input, vocabulary, segmentationVocabulary = emptySegmentationVocabulary) => {
|
|
65
102
|
const words = naturalWords(input);
|
|
103
|
+
const wordsInVocabulary = vocabularySet(vocabulary);
|
|
104
|
+
const workspace = [
|
|
105
|
+
[],
|
|
106
|
+
[],
|
|
107
|
+
[]
|
|
108
|
+
];
|
|
66
109
|
let partials = [{
|
|
67
|
-
|
|
110
|
+
text: "",
|
|
68
111
|
corrections: [],
|
|
69
112
|
cost: 0,
|
|
70
113
|
offset: 0
|
|
71
114
|
}];
|
|
72
115
|
for (const word of words) {
|
|
73
|
-
const replacements = replacementsFor(word,
|
|
116
|
+
const replacements = replacementsFor(word, wordsInVocabulary, segmentationVocabulary, workspace);
|
|
74
117
|
if (replacements.length === 0) return [];
|
|
75
118
|
const next = [];
|
|
76
119
|
for (const partial of partials) for (const replacement of replacements) {
|
|
@@ -81,7 +124,7 @@ const correctWhitespaceSeparatedText = (input, vocabulary, segmentationVocabular
|
|
|
81
124
|
offset: partial.offset
|
|
82
125
|
})];
|
|
83
126
|
next.push({
|
|
84
|
-
|
|
127
|
+
text: partial.text.length === 0 ? replacement.word : `${partial.text} ${replacement.word}`,
|
|
85
128
|
corrections: correction,
|
|
86
129
|
cost: partial.cost + replacement.distance,
|
|
87
130
|
offset: partial.offset + word.length + 1
|
|
@@ -90,7 +133,7 @@ const correctWhitespaceSeparatedText = (input, vocabulary, segmentationVocabular
|
|
|
90
133
|
partials = next.slice(0, 32);
|
|
91
134
|
}
|
|
92
135
|
return partials.filter((partial) => partial.corrections.length > 0).map((partial) => NaturalCorrectionCandidate.make({
|
|
93
|
-
text: partial.
|
|
136
|
+
text: partial.text,
|
|
94
137
|
corrections: partial.corrections,
|
|
95
138
|
cost: partial.cost
|
|
96
139
|
}));
|
package/dist/natural/parse.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { LanguageRegistry } from "../language/registry.mjs";
|
|
|
5
5
|
import { applyFuturePolicy } from "./policy.mjs";
|
|
6
6
|
import { Array, Effect, Order, Result } from "effect";
|
|
7
7
|
//#region src/natural/parse.ts
|
|
8
|
-
const distinctCandidates = (candidates) => Array.dedupeWith(candidates, (left, right) => rangeKey(left.range) === rangeKey(right.range));
|
|
8
|
+
const distinctCandidates = (candidates) => candidates.length === 1 ? candidates : Array.dedupeWith(candidates, (left, right) => rangeKey(left.range) === rangeKey(right.range));
|
|
9
9
|
const parseQuality = (hasAlternatives, hasCorrections) => {
|
|
10
10
|
if (hasAlternatives) return "ambiguous";
|
|
11
11
|
if (hasCorrections) return "corrected";
|
package/dist/natural/suggest.mjs
CHANGED
|
@@ -30,7 +30,15 @@ const suggestNatural = Effect.fn("chronolizer.suggestNatural")(function* (input,
|
|
|
30
30
|
text: candidate.canonical,
|
|
31
31
|
range: candidate.range
|
|
32
32
|
})));
|
|
33
|
-
const distinct = (suggestions) =>
|
|
33
|
+
const distinct = (suggestions) => {
|
|
34
|
+
const keys = /* @__PURE__ */ new Set();
|
|
35
|
+
return suggestions.filter((suggestion) => {
|
|
36
|
+
const key = rangeKey(suggestion.range);
|
|
37
|
+
if (keys.has(key)) return false;
|
|
38
|
+
keys.add(key);
|
|
39
|
+
return true;
|
|
40
|
+
});
|
|
41
|
+
};
|
|
34
42
|
const suggested = distinct(suggestionsFrom([...language.suggest(normalized, limit * 2), normalized]));
|
|
35
43
|
if (suggested.length > 0) return Array$1.take(suggested, limit);
|
|
36
44
|
const completionVocabulary = Array$1.dedupe([...Array$1.flatMap(language.suggest("", 100), naturalWords), ...language.vocabulary]);
|
|
@@ -1,40 +1,44 @@
|
|
|
1
1
|
import { damerauLevenshteinDistance } from "./correction.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { Option, String } from "effect";
|
|
3
3
|
//#region src/natural/suggestion.ts
|
|
4
|
-
const
|
|
5
|
-
const
|
|
4
|
+
const completionCost = (input, target, targetStart, targetEnd, workspace) => {
|
|
5
|
+
const targetLength = targetEnd - targetStart;
|
|
6
|
+
if (input.length <= targetLength && target.startsWith(input, targetStart)) return 0;
|
|
7
|
+
if (input.length < 3 || input.charCodeAt(0) !== target.charCodeAt(targetStart)) return void 0;
|
|
8
|
+
const maximum = input.length >= 5 ? 2 : 1;
|
|
9
|
+
let minimum = maximum + 1;
|
|
10
|
+
let previousLength = -1;
|
|
11
|
+
for (const length of [
|
|
6
12
|
Math.max(1, input.length - 1),
|
|
7
13
|
input.length,
|
|
8
|
-
Math.min(
|
|
9
|
-
])
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const maximum = input.length >= 5 ? 2 : 1;
|
|
16
|
-
const distance = fuzzyPrefixDistance(input, target);
|
|
17
|
-
return distance <= maximum ? distance : void 0;
|
|
14
|
+
Math.min(targetLength, input.length + 1)
|
|
15
|
+
]) {
|
|
16
|
+
if (length === previousLength) continue;
|
|
17
|
+
previousLength = length;
|
|
18
|
+
minimum = Math.min(minimum, damerauLevenshteinDistance(input, target.slice(targetStart, targetStart + length), maximum, workspace));
|
|
19
|
+
}
|
|
20
|
+
return minimum <= maximum ? minimum : void 0;
|
|
18
21
|
};
|
|
19
|
-
const phraseScore = (input, phrase) => {
|
|
22
|
+
const phraseScore = (input, inputWords, phrase, workspace) => {
|
|
20
23
|
if (input === phrase) return 0;
|
|
21
24
|
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
25
|
let cost = 0;
|
|
26
26
|
let addedWords = 0;
|
|
27
|
-
let
|
|
27
|
+
let phraseStart = 0;
|
|
28
28
|
for (const inputWord of inputWords) {
|
|
29
|
-
let
|
|
29
|
+
let phraseEnd = phrase.indexOf(" ", phraseStart);
|
|
30
|
+
if (phraseEnd === -1) phraseEnd = phrase.length;
|
|
31
|
+
let wordCost = completionCost(inputWord, phrase, phraseStart, phraseEnd, workspace);
|
|
30
32
|
while (wordCost === void 0 && addedWords < 3) {
|
|
31
|
-
|
|
33
|
+
phraseStart = phraseEnd + 1;
|
|
34
|
+
phraseEnd = phrase.indexOf(" ", phraseStart);
|
|
35
|
+
if (phraseEnd === -1) phraseEnd = phrase.length;
|
|
32
36
|
addedWords += 1;
|
|
33
|
-
wordCost = completionCost(inputWord,
|
|
37
|
+
wordCost = completionCost(inputWord, phrase, phraseStart, phraseEnd, workspace);
|
|
34
38
|
}
|
|
35
39
|
if (wordCost === void 0) return void 0;
|
|
36
40
|
cost += wordCost;
|
|
37
|
-
|
|
41
|
+
phraseStart = phraseEnd + 1;
|
|
38
42
|
}
|
|
39
43
|
return 2 + cost + addedWords;
|
|
40
44
|
};
|
|
@@ -70,16 +74,30 @@ const naturalCount = (input) => {
|
|
|
70
74
|
};
|
|
71
75
|
const completeNaturalPhrases = (input, phrases, limit) => {
|
|
72
76
|
const ranked = [];
|
|
73
|
-
|
|
74
|
-
|
|
77
|
+
const inputWords = input.split(" ");
|
|
78
|
+
const workspace = [
|
|
79
|
+
[],
|
|
80
|
+
[],
|
|
81
|
+
[]
|
|
82
|
+
];
|
|
83
|
+
let index = 0;
|
|
84
|
+
for (const phrase of new Set(phrases)) {
|
|
85
|
+
const score = phraseScore(input, inputWords, phrase, workspace);
|
|
75
86
|
if (score !== void 0) ranked.push({
|
|
76
87
|
phrase,
|
|
77
88
|
score,
|
|
78
89
|
index
|
|
79
90
|
});
|
|
91
|
+
index += 1;
|
|
92
|
+
}
|
|
93
|
+
ranked.sort((left, right) => left.score - right.score || left.index - right.index);
|
|
94
|
+
const maximumScore = (ranked[0]?.score ?? 2) <= 1 ? 1 : Number.POSITIVE_INFINITY;
|
|
95
|
+
const matches = [];
|
|
96
|
+
for (const entry of ranked) {
|
|
97
|
+
if (entry.score > maximumScore || matches.length >= limit) break;
|
|
98
|
+
matches.push(entry.phrase);
|
|
80
99
|
}
|
|
81
|
-
|
|
82
|
-
return (sorted.some((entry) => entry.score <= 1) ? sorted.filter((entry) => entry.score <= 1) : sorted).slice(0, limit).map((entry) => entry.phrase);
|
|
100
|
+
return matches;
|
|
83
101
|
};
|
|
84
102
|
//#endregion
|
|
85
103
|
export { completeNaturalPhrases, fixedCalendarPeriodPhrases, naturalCount, prefixNaturalPhrases };
|
package/dist/resolve/resolve.mjs
CHANGED
|
@@ -6,8 +6,7 @@ const shiftDateTime = (value, amount, unit) => Match.value(unit).pipe(Match.when
|
|
|
6
6
|
const startOfDateTime = (value, unit) => {
|
|
7
7
|
if (unit === "quarter") {
|
|
8
8
|
const month = DateTime.getPart(value, "month");
|
|
9
|
-
|
|
10
|
-
return DateTime.startOf(DateTime.setParts(value, { month: quarterMonth }), "month");
|
|
9
|
+
return DateTime.startOf(DateTime.subtract(value, { months: (month - 1) % 3 }), "month");
|
|
11
10
|
}
|
|
12
11
|
return DateTime.startOf(value, unit, { weekStartsOn: 1 });
|
|
13
12
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chronolizer",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Bidirectional natural-language date ranges for Effect.",
|
|
5
5
|
"homepage": "https://github.com/tobimori/chronolizer#readme",
|
|
6
6
|
"bugs": "https://github.com/tobimori/chronolizer/issues",
|
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
}
|
|
55
55
|
},
|
|
56
56
|
"scripts": {
|
|
57
|
+
"benchmark": "pnpm build && node tools/benchmark.mjs",
|
|
57
58
|
"build": "vp pack",
|
|
58
59
|
"check:bundle-size": "node tools/check-bundle-size.mjs",
|
|
59
60
|
"check:tree-shaking": "node tools/check-tree-shaking.mjs",
|