chronolizer 0.3.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.
@@ -1,36 +1,46 @@
1
1
  import { damerauLevenshteinDistance } from "./correction.mjs";
2
- import { Array as Array$1, Option, String } from "effect";
2
+ import { Option, String } from "effect";
3
3
  //#region src/natural/suggestion.ts
4
- const fuzzyPrefixDistance = (input, target) => {
5
- const lengths = Array$1.dedupe([
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(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;
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
- 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);
26
+ let addedWords = 0;
27
+ let phraseStart = 0;
28
+ for (const inputWord of inputWords) {
29
+ let phraseEnd = phrase.indexOf(" ", phraseStart);
30
+ if (phraseEnd === -1) phraseEnd = phrase.length;
31
+ let wordCost = completionCost(inputWord, phrase, phraseStart, phraseEnd, workspace);
32
+ while (wordCost === void 0 && addedWords < 3) {
33
+ phraseStart = phraseEnd + 1;
34
+ phraseEnd = phrase.indexOf(" ", phraseStart);
35
+ if (phraseEnd === -1) phraseEnd = phrase.length;
36
+ addedWords += 1;
37
+ wordCost = completionCost(inputWord, phrase, phraseStart, phraseEnd, workspace);
38
+ }
30
39
  if (wordCost === void 0) return void 0;
31
40
  cost += wordCost;
41
+ phraseStart = phraseEnd + 1;
32
42
  }
33
- return cost === 0 ? 2 : 2 + cost;
43
+ return 2 + cost + addedWords;
34
44
  };
35
45
  const completeYearPrefix = (input) => {
36
46
  const match = String.match(/(?:^| )(\d{3,4})$/u)(input);
@@ -64,16 +74,30 @@ const naturalCount = (input) => {
64
74
  };
65
75
  const completeNaturalPhrases = (input, phrases, limit) => {
66
76
  const ranked = [];
67
- for (const [index, phrase] of Array$1.dedupe(phrases).entries()) {
68
- const score = phraseScore(input, phrase);
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);
69
86
  if (score !== void 0) ranked.push({
70
87
  phrase,
71
88
  score,
72
89
  index
73
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);
74
99
  }
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);
100
+ return matches;
77
101
  };
78
102
  //#endregion
79
103
  export { completeNaturalPhrases, fixedCalendarPeriodPhrases, naturalCount, prefixNaturalPhrases };
@@ -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
- const quarterMonth = Math.floor((month - 1) / 3) * 3 + 1;
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
  };
@@ -45,7 +44,7 @@ const resolve = Effect.fn("chronolizer.resolve")(function* (range) {
45
44
  if (range.lower !== void 0 && range.upper !== void 0) {
46
45
  const lower = yield* resolveLower(range.lower, reference, zone);
47
46
  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" });
47
+ if (DateTime.toEpochMillis(lower.value) >= DateTime.toEpochMillis(upper.value)) return yield* new ResolutionError({ message: "The range start must be before the range end" });
49
48
  return ResolvedDateRange.make({
50
49
  lower,
51
50
  upper
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chronolizer",
3
- "version": "0.3.0",
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",