chronolizer 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -45
- package/dist/filter/expression.mjs +24 -18
- package/dist/language/model.mjs +1 -1
- package/dist/language/registry.mjs +32 -12
- package/dist/locales/cs.mjs +186 -17
- package/dist/locales/de.mjs +133 -20
- package/dist/locales/en.mjs +151 -35
- package/dist/locales/es.mjs +178 -19
- package/dist/locales/fr.mjs +150 -19
- package/dist/locales/nl.mjs +118 -19
- package/dist/locales/pl.mjs +156 -18
- package/dist/locales/shared.mjs +64 -1
- package/dist/locales/tr.mjs +138 -24
- package/dist/natural/correction.d.mts +1 -1
- package/dist/natural/correction.mjs +31 -5
- package/dist/natural/format.mjs +1 -1
- package/dist/natural/parse.mjs +5 -4
- package/dist/natural/suggest.mjs +19 -3
- package/dist/natural/suggestion.mjs +11 -5
- package/dist/resolve/resolve.mjs +1 -1
- package/package.json +1 -1
package/dist/locales/tr.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { normalizeNaturalText } from "../natural/text.mjs";
|
|
|
4
4
|
import { defineLanguagePlugin, languagePluginsLayer } from "../language/registry.mjs";
|
|
5
5
|
import { correctWhitespaceSeparatedText } from "../natural/correction.mjs";
|
|
6
6
|
import { completeNaturalPhrases, fixedCalendarPeriodPhrases, naturalCount, prefixNaturalPhrases } from "../natural/suggestion.mjs";
|
|
7
|
-
import { absoluteDatePeriod, calendarPeriodOffset, candidate, currentYearDatePeriods, datedPeriods, datedQuarterPeriods, fixedDatePeriod, fixedMonthPeriod, fixedQuarterPeriod, fixedYearPeriod, fromNowRange, futurePeriod, futureRange, isoDate, joinedNowCandidate, joinedPeriodCandidate, monthOfRelativeYear, namedCurrentYearDatePeriod, namedDatePeriod, openBoundaryCandidate, parseTrailingCount, periodBoundaryCandidate, periodEndDay, periodRange, periodStartDay, periodToDateRange, periodsFromPhrases, quarterOfRelativeYear, relativePeriod, relativeWeekend, remainingPeriodRange, renderPeriodRange, textAt, trailingPeriod, trailingRange, untilNowRange, validYear } from "./shared.mjs";
|
|
7
|
+
import { absoluteDatePeriod, calendarPeriodOffset, candidate, compileCountAliasNormalizer, compoundCountAliases, countAliasVocabulary, currentYearDatePeriods, datedPeriods, datedQuarterPeriods, decimalTens, decomposeShiftedPeriodRange, fixedDatePeriod, fixedMonthPeriod, fixedQuarterPeriod, fixedYearPeriod, fromNowRange, futurePeriod, futureRange, isoDate, joinedNowCandidate, joinedPeriodCandidate, monthOfRelativeYear, namedCurrentYearDatePeriod, namedDatePeriod, openBoundaryCandidate, parseTrailingCount, periodBoundaryCandidate, periodEndDay, periodRange, periodStartDay, periodToDateRange, periodsFromPhrases, quarterOfRelativeYear, relativePeriod, relativeWeekday, relativeWeekend, remainingPeriodRange, renderPeriodRange, sequentialCountAliases, shiftPeriod, textAt, trailingPeriod, trailingRange, untilNowRange, validYear } from "./shared.mjs";
|
|
8
8
|
import { Effect, Option, String as String$1 } from "effect";
|
|
9
9
|
//#region src/locales/tr.ts
|
|
10
10
|
const months = [
|
|
@@ -21,6 +21,61 @@ const months = [
|
|
|
21
21
|
"kasım",
|
|
22
22
|
"aralık"
|
|
23
23
|
];
|
|
24
|
+
const weekdays = [
|
|
25
|
+
"pazartesi",
|
|
26
|
+
"salı",
|
|
27
|
+
"çarşamba",
|
|
28
|
+
"perşembe",
|
|
29
|
+
"cuma",
|
|
30
|
+
"cumartesi",
|
|
31
|
+
"pazar"
|
|
32
|
+
];
|
|
33
|
+
const nextWeekdayPhrases = weekdays.map((weekday) => `gelecek ${weekday}`);
|
|
34
|
+
const turkishCountWords = [
|
|
35
|
+
["iki"],
|
|
36
|
+
["üç", "uc"],
|
|
37
|
+
["dört", "dort"],
|
|
38
|
+
["beş", "bes"],
|
|
39
|
+
["altı", "alti"],
|
|
40
|
+
["yedi"],
|
|
41
|
+
["sekiz"],
|
|
42
|
+
["dokuz"],
|
|
43
|
+
["on"],
|
|
44
|
+
["on bir"],
|
|
45
|
+
["on iki"],
|
|
46
|
+
["on üç"],
|
|
47
|
+
["on dört"],
|
|
48
|
+
["on beş"],
|
|
49
|
+
["on altı"],
|
|
50
|
+
["on yedi"],
|
|
51
|
+
["on sekiz"],
|
|
52
|
+
["on dokuz"],
|
|
53
|
+
["yirmi"]
|
|
54
|
+
];
|
|
55
|
+
const turkishCountOnes = [
|
|
56
|
+
[1, "bir"],
|
|
57
|
+
[2, "iki"],
|
|
58
|
+
[3, "üç"],
|
|
59
|
+
[4, "dört"],
|
|
60
|
+
[5, "beş"],
|
|
61
|
+
[6, "altı"],
|
|
62
|
+
[7, "yedi"],
|
|
63
|
+
[8, "sekiz"],
|
|
64
|
+
[9, "dokuz"]
|
|
65
|
+
];
|
|
66
|
+
const turkishCountAliases = [...sequentialCountAliases(turkishCountWords, 2), ...compoundCountAliases(decimalTens([
|
|
67
|
+
"yirmi",
|
|
68
|
+
"otuz",
|
|
69
|
+
"kırk",
|
|
70
|
+
"elli",
|
|
71
|
+
"altmış",
|
|
72
|
+
"yetmiş",
|
|
73
|
+
"seksen",
|
|
74
|
+
"doksan"
|
|
75
|
+
]), turkishCountOnes, (ten, one) => [`${ten} ${one}`])];
|
|
76
|
+
const normalizeTurkishCounts = compileCountAliasNormalizer(turkishCountAliases);
|
|
77
|
+
const turkishCountVocabulary = new Set(countAliasVocabulary(turkishCountAliases));
|
|
78
|
+
const correctTurkish = (input, vocabulary) => correctWhitespaceSeparatedText(input, vocabulary, turkishCountVocabulary);
|
|
24
79
|
const monthAbbreviations = [
|
|
25
80
|
["oca"],
|
|
26
81
|
["şub", "sub"],
|
|
@@ -340,13 +395,13 @@ const parseQuarter = (input) => {
|
|
|
340
395
|
const quarter = quarterNumber(textAt(standalone.value, 1));
|
|
341
396
|
return quarter === void 0 ? Option.none() : Option.some(quarterOfRelativeYear(quarter, 0, `Ç${quarter}`));
|
|
342
397
|
};
|
|
343
|
-
const
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
if (Option.isSome(
|
|
398
|
+
const parseDatedPeriod = (input) => {
|
|
399
|
+
const knownPeriod = Option.firstSomeOf([
|
|
400
|
+
absoluteDatePeriod(input, "tr"),
|
|
401
|
+
parseNamedDate(input),
|
|
402
|
+
parseQuarter(input)
|
|
403
|
+
]);
|
|
404
|
+
if (Option.isSome(knownPeriod)) return knownPeriod;
|
|
350
405
|
const yearMatch = String$1.match(/^(?:yıl |yılı )?(\d{4})$/u)(input);
|
|
351
406
|
if (Option.isSome(yearMatch)) {
|
|
352
407
|
const year = validYear(textAt(yearMatch.value, 1));
|
|
@@ -358,6 +413,20 @@ const parseBasePeriod = (input) => {
|
|
|
358
413
|
const year = validYear(textAt(monthYear.value, 2));
|
|
359
414
|
if (month !== void 0 && year !== void 0) return Option.some(fixedMonthPeriod(year, month, `${title(textAt(months, month - 1))} ${year}`));
|
|
360
415
|
}
|
|
416
|
+
return Option.none();
|
|
417
|
+
};
|
|
418
|
+
const parseBasePeriod = (input) => {
|
|
419
|
+
const datedPeriod = parseDatedPeriod(input);
|
|
420
|
+
if (Option.isSome(datedPeriod)) return datedPeriod;
|
|
421
|
+
const prefixedRelativeMonth = String$1.match(/^(geçen|bu|gelecek) ([a-zçğıöşü]+\.?)$/u)(input);
|
|
422
|
+
if (Option.isSome(prefixedRelativeMonth)) {
|
|
423
|
+
const month = monthNumber(textAt(prefixedRelativeMonth.value, 2));
|
|
424
|
+
if (month !== void 0) {
|
|
425
|
+
const modifier = textAt(prefixedRelativeMonth.value, 1);
|
|
426
|
+
const direction = relativeYearDirection(modifier);
|
|
427
|
+
return Option.some(monthOfRelativeYear(month, direction, `${modifier} ${title(textAt(months, month - 1))}`));
|
|
428
|
+
}
|
|
429
|
+
}
|
|
361
430
|
const relativeMonth = String$1.match(/^([a-zçğıöşü]+\.?) (geçen yıl|gelecek yıl|bu yıl)$/u)(input);
|
|
362
431
|
const relativeMonthYearFirst = String$1.match(/^(geçen yıl|gelecek yıl|bu yıl) ([a-zçğıöşü]+\.?)$/u)(input);
|
|
363
432
|
const relativeMatch = Option.firstSomeOf([relativeMonth, relativeMonthYearFirst]);
|
|
@@ -378,16 +447,38 @@ const parseBasePeriod = (input) => {
|
|
|
378
447
|
if (["gelecek hafta sonu", "önümüzdeki hafta sonu"].includes(input)) return Option.some(relativeWeekend(1, "gelecek hafta sonu"));
|
|
379
448
|
if (input === "geçen hafta sonundan önceki hafta sonu") return Option.some(relativeWeekend(-2, input));
|
|
380
449
|
if (input === "gelecek hafta sonundan sonraki hafta sonu") return Option.some(relativeWeekend(2, input));
|
|
381
|
-
|
|
450
|
+
const weekday = nextWeekdayPhrases.indexOf(input);
|
|
451
|
+
return weekday === -1 ? Option.none() : Option.some(relativeWeekday(weekday, 1, nextWeekdayPhrases[weekday] ?? input));
|
|
382
452
|
};
|
|
383
|
-
const
|
|
453
|
+
const parsePeriodEdge = (input) => {
|
|
384
454
|
const edge = String$1.match(/^(.+?)(?:ın|in|un|ün) (başı|başlangıcı|sonu)$/u)(input);
|
|
385
|
-
if (Option.
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
455
|
+
if (Option.isNone(edge)) return Option.none();
|
|
456
|
+
const periodText = textAt(edge.value, 1);
|
|
457
|
+
const basePeriod = parseBasePeriod(periodText);
|
|
458
|
+
const implicitUnit = unitAliases.find((entry) => entry[0] === periodText)?.[1];
|
|
459
|
+
const implicit = units.find((entry) => entry.unit === implicitUnit);
|
|
460
|
+
const period = Option.isSome(basePeriod) || implicit === void 0 ? basePeriod : Option.some(relativePeriod(implicit.unit, 0, implicit.current));
|
|
461
|
+
if (Option.isNone(period)) return Option.none();
|
|
462
|
+
const isEnd = textAt(edge.value, 2) === "sonu";
|
|
463
|
+
const canonical = `${period.value.canonical} ${isEnd ? "sonu" : "başı"}`;
|
|
464
|
+
return Option.some(isEnd ? periodEndDay(period.value, canonical) : periodStartDay(period.value, canonical));
|
|
465
|
+
};
|
|
466
|
+
const parsePeriod = (input) => {
|
|
467
|
+
const prefix = String$1.match(/^([1-9]\d*) (gün|gun|hafta|ay|çeyrek|ceyrek|yıl|yil|sene) (önce|sonra) (.+)$/u)(input);
|
|
468
|
+
const suffix = String$1.match(/^(.+) ([1-9]\d*) (gün|gun|hafta|ay|çeyrek|ceyrek|yıl|yil|sene) (önce|sonra)$/u)(input);
|
|
469
|
+
const shifted = Option.firstSomeOf([prefix, suffix]);
|
|
470
|
+
if (Option.isSome(shifted)) {
|
|
471
|
+
const prefixOrder = Option.isSome(prefix);
|
|
472
|
+
const amount = parseTrailingCount(textAt(shifted.value, prefixOrder ? 1 : 2));
|
|
473
|
+
const alias = unitAliases.find((unit) => unit[0] === textAt(shifted.value, prefixOrder ? 2 : 3));
|
|
474
|
+
const entry = alias === void 0 ? void 0 : units.find((unit) => unit.unit === alias[1]);
|
|
475
|
+
const period = parsePeriod(textAt(shifted.value, prefixOrder ? 4 : 1));
|
|
476
|
+
if (Option.isSome(amount) && entry !== void 0 && Option.isSome(period)) {
|
|
477
|
+
const past = textAt(shifted.value, prefixOrder ? 3 : 4) === "önce";
|
|
478
|
+
const direction = past ? -amount.value : amount.value;
|
|
479
|
+
const noun = amount.value === 1 ? entry.singular : entry.plural;
|
|
480
|
+
const canonical = `${amount.value} ${noun} ${past ? "önce" : "sonra"} ${period.value.canonical}`;
|
|
481
|
+
return Option.some(shiftPeriod(period.value, direction, entry.unit, canonical));
|
|
391
482
|
}
|
|
392
483
|
}
|
|
393
484
|
const wrapper = [
|
|
@@ -395,7 +486,11 @@ const parsePeriod = (input) => {
|
|
|
395
486
|
"içinde ",
|
|
396
487
|
"tüm "
|
|
397
488
|
].find((prefix) => input.startsWith(prefix));
|
|
398
|
-
return
|
|
489
|
+
return Option.firstSomeOf([
|
|
490
|
+
parsePeriodEdge(input),
|
|
491
|
+
parseBasePeriod(wrapper === void 0 ? input : input.slice(wrapper.length)),
|
|
492
|
+
parseCalendarOffset(input)
|
|
493
|
+
]);
|
|
399
494
|
};
|
|
400
495
|
const countedUnit = (value) => unitAliases.find((entry) => entry[0] === value)?.[1];
|
|
401
496
|
const countedUnitPattern = "gün|gun|hafta|ay|çeyrek|ceyrek|yıl|yil|sene";
|
|
@@ -446,7 +541,7 @@ const singularCalendarOffsets = units.flatMap((entry) => [{
|
|
|
446
541
|
}]);
|
|
447
542
|
const parseCalendarOffset = (input) => {
|
|
448
543
|
const singular = singularCalendarOffsets.find((entry) => entry.phrase === input);
|
|
449
|
-
if (singular !== void 0) return Option.some(
|
|
544
|
+
if (singular !== void 0) return Option.some(relativePeriod(singular.entry.unit, singular.direction, singular.phrase));
|
|
450
545
|
const past = String$1.match(calendarPastPattern)(input);
|
|
451
546
|
const future = firstPatternMatch(input, calendarFuturePatterns);
|
|
452
547
|
const match = Option.firstSomeOf([past, future]);
|
|
@@ -459,7 +554,7 @@ const parseCalendarOffset = (input) => {
|
|
|
459
554
|
const direction = Option.isSome(past) ? -amount.value : amount.value;
|
|
460
555
|
const noun = amount.value === 1 ? entry.singular : entry.plural;
|
|
461
556
|
const canonical = direction < 0 ? `${amount.value} ${noun} önce` : `${amount.value} ${noun} sonra`;
|
|
462
|
-
return Option.some(
|
|
557
|
+
return Option.some(relativePeriod(unit, direction, canonical));
|
|
463
558
|
};
|
|
464
559
|
const parseRollingPeriod = (input) => {
|
|
465
560
|
const singular = singularRollingPhrases.find((entry) => entry.phrase === input);
|
|
@@ -543,8 +638,6 @@ const parseTurkish = (input) => {
|
|
|
543
638
|
"şu andan itibaren",
|
|
544
639
|
"bundan sonra"
|
|
545
640
|
].includes(input)) return Option.some(candidate(fromNowRange(), "bugünden itibaren"));
|
|
546
|
-
const offset = parseCalendarOffset(input);
|
|
547
|
-
if (Option.isSome(offset)) return offset;
|
|
548
641
|
const rolling = parseRollingPeriod(input);
|
|
549
642
|
if (Option.isSome(rolling)) return rolling;
|
|
550
643
|
const toDate = toDatePhrases.find((entry) => entry.phrase === input);
|
|
@@ -552,7 +645,12 @@ const parseTurkish = (input) => {
|
|
|
552
645
|
const elided = parseElidedDateRange(input);
|
|
553
646
|
if (Option.isSome(elided)) return elided;
|
|
554
647
|
const joinedInput = input.endsWith(" arası") ? input.slice(0, -6) : input;
|
|
555
|
-
const nowBounded = joinedNowCandidate(joinedInput, [["", " ile bugün"], ["", " ve bugün"]], [
|
|
648
|
+
const nowBounded = joinedNowCandidate(joinedInput, [["", " ile bugün"], ["", " ve bugün"]], [
|
|
649
|
+
"bugün ile ",
|
|
650
|
+
"bugün ve ",
|
|
651
|
+
"şimdi ile ",
|
|
652
|
+
"şimdi ve "
|
|
653
|
+
], parsePeriod, (period) => `${period} ile bugün arası`, (period) => `bugün ile ${period} arası`);
|
|
556
654
|
if (Option.isSome(nowBounded)) return nowBounded;
|
|
557
655
|
const bounded = joinedPeriodCandidate(joinedInput, [
|
|
558
656
|
["", " ile "],
|
|
@@ -591,6 +689,8 @@ const staticPeriodPhrases = [
|
|
|
591
689
|
"geçen yılın sonu",
|
|
592
690
|
"gelecek yılın başı",
|
|
593
691
|
"gelecek yılın sonu",
|
|
692
|
+
...nextWeekdayPhrases,
|
|
693
|
+
...months.flatMap((month) => [`geçen ${month}`, `gelecek ${month}`]),
|
|
594
694
|
...[
|
|
595
695
|
1,
|
|
596
696
|
2,
|
|
@@ -651,6 +751,17 @@ const suggestTurkish = (input, limit) => {
|
|
|
651
751
|
], limit);
|
|
652
752
|
};
|
|
653
753
|
const renderTurkish = (range) => {
|
|
754
|
+
const shifted = decomposeShiftedPeriodRange(range);
|
|
755
|
+
if (Option.isSome(shifted)) {
|
|
756
|
+
const base = renderTurkish(shifted.value.baseRange);
|
|
757
|
+
const entry = units.find((unit) => unit.unit === shifted.value.unit);
|
|
758
|
+
if (Option.isSome(base) && entry !== void 0) {
|
|
759
|
+
const amount = Math.abs(shifted.value.amount);
|
|
760
|
+
const noun = amount === 1 ? entry.singular : entry.plural;
|
|
761
|
+
const direction = shifted.value.amount < 0 ? "önce" : "sonra";
|
|
762
|
+
return Option.some(`${amount} ${noun} ${direction} ${base.value}`);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
654
765
|
const offset = calendarPeriodOffset(range);
|
|
655
766
|
if (Option.isSome(offset) && Math.abs(offset.value.amount) > 1) {
|
|
656
767
|
const entry = units.find((unit) => unit.unit === offset.value.unit);
|
|
@@ -677,11 +788,12 @@ const renderTurkish = (range) => {
|
|
|
677
788
|
];
|
|
678
789
|
return renderPeriodRange(range, [...units.map((entry) => candidate(periodToDateRange(entry.unit), entry.toDate)), ...units.map((entry) => candidate(remainingPeriodRange(entry.unit), entry.remaining))], periods, (period) => `${period} itibarıyla`, (period) => `${period} öncesi`, (period) => `${period} sonuna kadar`, (period) => `${period} sonrası`, (lower, upper) => `${lower} ile ${upper} arası`, (period) => `${period} ile bugün arası`, (period) => `bugün ile ${period} arası`, () => "şimdiye kadar", () => "bugünden itibaren");
|
|
679
790
|
};
|
|
680
|
-
const normalizeTurkish = (input, locale) => normalizeNaturalText(input, locale).replaceAll("’", "'");
|
|
791
|
+
const normalizeTurkish = (input, locale) => normalizeTurkishCounts(normalizeNaturalText(input, locale).replaceAll("’", "'"));
|
|
681
792
|
const TurkishContribution = new BaseLanguageContribution({
|
|
682
793
|
locale: "tr",
|
|
683
794
|
vocabulary: [
|
|
684
795
|
...months,
|
|
796
|
+
...weekdays,
|
|
685
797
|
...monthAbbreviations.flatMap((aliases) => aliases),
|
|
686
798
|
...units.flatMap((entry) => [
|
|
687
799
|
entry.singular,
|
|
@@ -698,6 +810,7 @@ const TurkishContribution = new BaseLanguageContribution({
|
|
|
698
810
|
"gelecek",
|
|
699
811
|
"geçen",
|
|
700
812
|
"geri",
|
|
813
|
+
"ile",
|
|
701
814
|
"itibaren",
|
|
702
815
|
"itibarıyla",
|
|
703
816
|
"kadar",
|
|
@@ -708,10 +821,11 @@ const TurkishContribution = new BaseLanguageContribution({
|
|
|
708
821
|
"son",
|
|
709
822
|
"sonra",
|
|
710
823
|
"sonrası",
|
|
824
|
+
"şimdi",
|
|
711
825
|
"şimdiye"
|
|
712
826
|
],
|
|
713
827
|
normalize: normalizeTurkish,
|
|
714
|
-
correct:
|
|
828
|
+
correct: correctTurkish,
|
|
715
829
|
parseExact: parseTurkish,
|
|
716
830
|
suggest: suggestTurkish,
|
|
717
831
|
render: renderTurkish
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
//#region src/natural/correction.d.ts
|
|
2
|
-
declare const correctWhitespaceSeparatedText: (input: string, vocabulary: ReadonlyArray<string>) => {
|
|
2
|
+
declare const correctWhitespaceSeparatedText: (input: string, vocabulary: ReadonlyArray<string>, segmentationVocabulary?: ReadonlySet<string>) => {
|
|
3
3
|
readonly text: string;
|
|
4
4
|
readonly corrections: readonly {
|
|
5
5
|
readonly original: string;
|
|
@@ -20,22 +20,48 @@ const damerauLevenshteinDistance = (left, right) => {
|
|
|
20
20
|
}
|
|
21
21
|
return previous[right.length] ?? fallback;
|
|
22
22
|
};
|
|
23
|
-
const
|
|
23
|
+
const segmentedReplacements = (word, vocabulary) => {
|
|
24
|
+
const segmentations = Array.from({ length: word.length + 1 }, () => []);
|
|
25
|
+
const initial = segmentations[0];
|
|
26
|
+
if (initial !== void 0) initial.push([]);
|
|
27
|
+
for (let start = 0; start < word.length; start += 1) {
|
|
28
|
+
const prefixes = segmentations[start] ?? [];
|
|
29
|
+
if (prefixes.length === 0) continue;
|
|
30
|
+
for (let end = start + 1; end <= word.length; end += 1) {
|
|
31
|
+
const part = word.slice(start, end);
|
|
32
|
+
if (!vocabulary.has(part)) continue;
|
|
33
|
+
const target = segmentations[end];
|
|
34
|
+
if (target === void 0) continue;
|
|
35
|
+
for (const prefix of prefixes) {
|
|
36
|
+
if (target.length >= 4) break;
|
|
37
|
+
target.push([...prefix, part]);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return (segmentations[word.length] ?? []).filter((parts) => parts.length > 1).map((parts) => ({
|
|
42
|
+
word: parts.join(" "),
|
|
43
|
+
distance: parts.length - 1
|
|
44
|
+
}));
|
|
45
|
+
};
|
|
46
|
+
const replacementsFor = (word, vocabulary, segmentationVocabulary) => {
|
|
24
47
|
if (vocabulary.includes(word) || isProtectedValue(word)) return [{
|
|
25
48
|
word,
|
|
26
49
|
distance: 0
|
|
27
50
|
}];
|
|
28
|
-
|
|
51
|
+
const segmented = segmentedReplacements(word, segmentationVocabulary);
|
|
52
|
+
if (word.length <= 3) return segmented;
|
|
29
53
|
const maximum = word.length >= 6 ? 2 : 1;
|
|
30
|
-
const
|
|
54
|
+
const fuzzy = vocabulary.filter((candidate) => Math.abs(candidate.length - word.length) <= maximum).map((candidate) => ({
|
|
31
55
|
word: candidate,
|
|
32
56
|
distance: damerauLevenshteinDistance(word, candidate)
|
|
33
57
|
})).filter((candidate) => candidate.distance <= maximum);
|
|
58
|
+
const matches = [...segmented, ...fuzzy];
|
|
34
59
|
if (matches.length === 0) return [];
|
|
35
60
|
const minimum = Math.min(...matches.map((candidate) => candidate.distance));
|
|
36
61
|
return matches.filter((candidate) => candidate.distance === minimum).slice(0, 4);
|
|
37
62
|
};
|
|
38
|
-
const
|
|
63
|
+
const emptySegmentationVocabulary = /* @__PURE__ */ new Set();
|
|
64
|
+
const correctWhitespaceSeparatedText = (input, vocabulary, segmentationVocabulary = emptySegmentationVocabulary) => {
|
|
39
65
|
const words = naturalWords(input);
|
|
40
66
|
let partials = [{
|
|
41
67
|
words: [],
|
|
@@ -44,7 +70,7 @@ const correctWhitespaceSeparatedText = (input, vocabulary) => {
|
|
|
44
70
|
offset: 0
|
|
45
71
|
}];
|
|
46
72
|
for (const word of words) {
|
|
47
|
-
const replacements = replacementsFor(word, vocabulary);
|
|
73
|
+
const replacements = replacementsFor(word, vocabulary, segmentationVocabulary);
|
|
48
74
|
if (replacements.length === 0) return [];
|
|
49
75
|
const next = [];
|
|
50
76
|
for (const partial of partials) for (const replacement of replacements) {
|
package/dist/natural/format.mjs
CHANGED
|
@@ -7,7 +7,7 @@ const formatNatural = Effect.fn("chronolizer.formatNatural")(function* (range, o
|
|
|
7
7
|
if (Option.isSome(rendered)) return rendered.value;
|
|
8
8
|
return yield* new NaturalLanguageRenderError({
|
|
9
9
|
locale: options.locale,
|
|
10
|
-
message: "The range has no
|
|
10
|
+
message: "The range has no standard natural-language form for this language"
|
|
11
11
|
});
|
|
12
12
|
});
|
|
13
13
|
//#endregion
|
package/dist/natural/parse.mjs
CHANGED
|
@@ -31,13 +31,13 @@ const parseNatural = Effect.fn("chronolizer.parseNatural")(function* (input, opt
|
|
|
31
31
|
if (normalized.length === 0) return yield* new NaturalLanguageParseError({
|
|
32
32
|
input,
|
|
33
33
|
locale: options.locale,
|
|
34
|
-
message: "The complete input must contain a date
|
|
34
|
+
message: "The complete input must contain a date range"
|
|
35
35
|
});
|
|
36
36
|
const exact = language.parseExact(normalized);
|
|
37
37
|
const allowedExact = applyFuturePolicy(exact, options.allowFuture);
|
|
38
38
|
if (Array.isReadonlyArrayNonEmpty(allowedExact)) return resultFromCandidates(allowedExact, []);
|
|
39
39
|
if (options.typoMode !== "tolerant") {
|
|
40
|
-
const message = exact.length > 0 && options.allowFuture === false ? "
|
|
40
|
+
const message = exact.length > 0 && options.allowFuture === false ? "This date range is in the future, but future ranges are disabled" : "The complete input is not a supported date range";
|
|
41
41
|
return yield* new NaturalLanguageParseError({
|
|
42
42
|
input,
|
|
43
43
|
locale: options.locale,
|
|
@@ -46,7 +46,8 @@ const parseNatural = Effect.fn("chronolizer.parseNatural")(function* (input, opt
|
|
|
46
46
|
}
|
|
47
47
|
const corrected = language.correct?.(normalized, language.vocabulary) ?? [];
|
|
48
48
|
const parsedCorrections = Array.filterMap(corrected, (correction) => {
|
|
49
|
-
const
|
|
49
|
+
const correctedText = language.normalize(correction.text, language.locale);
|
|
50
|
+
const candidates = applyFuturePolicy(language.parseExact(correctedText), options.allowFuture);
|
|
50
51
|
return Array.isReadonlyArrayNonEmpty(candidates) ? Result.succeed({
|
|
51
52
|
correction,
|
|
52
53
|
candidates
|
|
@@ -56,7 +57,7 @@ const parseNatural = Effect.fn("chronolizer.parseNatural")(function* (input, opt
|
|
|
56
57
|
if (!Array.isReadonlyArrayNonEmpty(successful)) return yield* new NaturalLanguageParseError({
|
|
57
58
|
input,
|
|
58
59
|
locale: options.locale,
|
|
59
|
-
message: "No
|
|
60
|
+
message: "No supported date range was found after spelling correction"
|
|
60
61
|
});
|
|
61
62
|
const first = Array.headNonEmpty(successful);
|
|
62
63
|
const best = Array.prepend(Array.takeWhile(Array.tailNonEmpty(successful), (entry) => entry.correction.cost === first.correction.cost), first);
|
package/dist/natural/suggest.mjs
CHANGED
|
@@ -1,24 +1,40 @@
|
|
|
1
1
|
import { rangeKey } from "../filter/codec.mjs";
|
|
2
2
|
import { NaturalSuggestion } from "../language/model.mjs";
|
|
3
|
+
import { naturalWords } from "./text.mjs";
|
|
3
4
|
import { LanguageRegistry } from "../language/registry.mjs";
|
|
4
5
|
import { applyFuturePolicy } from "./policy.mjs";
|
|
5
|
-
import { Array, Effect } from "effect";
|
|
6
|
+
import { Array as Array$1, Effect } from "effect";
|
|
6
7
|
//#region src/natural/suggest.ts
|
|
7
8
|
const suggestionLimit = (limit) => {
|
|
8
9
|
if (limit === void 0) return 10;
|
|
9
10
|
if (!Number.isSafeInteger(limit) || limit <= 0) return 0;
|
|
10
11
|
return Math.min(limit, 100);
|
|
11
12
|
};
|
|
13
|
+
const singleWordCompletions = (input, vocabulary) => {
|
|
14
|
+
const words = naturalWords(input);
|
|
15
|
+
if (words.length === 0 || words.length > 12) return [];
|
|
16
|
+
const positions = Array.from({ length: words.length + 1 }, (_, index) => index);
|
|
17
|
+
const insertions = Array$1.flatMap(positions, (index) => Array$1.flatMap(vocabulary, (word) => word.length === 0 || word.includes(" ") ? [] : [[
|
|
18
|
+
...words.slice(0, index),
|
|
19
|
+
word,
|
|
20
|
+
...words.slice(index)
|
|
21
|
+
].join(" ")]));
|
|
22
|
+
return Array$1.dedupe(insertions);
|
|
23
|
+
};
|
|
12
24
|
const suggestNatural = Effect.fn("chronolizer.suggestNatural")(function* (input, options) {
|
|
13
25
|
const language = yield* (yield* LanguageRegistry).resolve(options.locale);
|
|
14
26
|
const normalized = language.normalize(input, language.locale);
|
|
15
27
|
const limit = suggestionLimit(options.limit);
|
|
16
28
|
if (limit === 0) return [];
|
|
17
|
-
const
|
|
29
|
+
const suggestionsFrom = (texts) => Array$1.flatMap(Array$1.dedupe(texts), (text) => applyFuturePolicy(language.parseExact(language.normalize(text, language.locale)), options.allowFuture).map((candidate) => NaturalSuggestion.make({
|
|
18
30
|
text: candidate.canonical,
|
|
19
31
|
range: candidate.range
|
|
20
32
|
})));
|
|
21
|
-
|
|
33
|
+
const distinct = (suggestions) => Array$1.dedupeWith(suggestions, (left, right) => rangeKey(left.range) === rangeKey(right.range));
|
|
34
|
+
const suggested = distinct(suggestionsFrom([...language.suggest(normalized, limit * 2), normalized]));
|
|
35
|
+
if (suggested.length > 0) return Array$1.take(suggested, limit);
|
|
36
|
+
const completionVocabulary = Array$1.dedupe([...Array$1.flatMap(language.suggest("", 100), naturalWords), ...language.vocabulary]);
|
|
37
|
+
return Array$1.take(distinct(suggestionsFrom(singleWordCompletions(normalized, completionVocabulary))), limit);
|
|
22
38
|
});
|
|
23
39
|
//#endregion
|
|
24
40
|
export { suggestNatural };
|
|
@@ -23,14 +23,20 @@ const phraseScore = (input, phrase) => {
|
|
|
23
23
|
const phraseWords = phrase.split(" ");
|
|
24
24
|
if (inputWords.length > phraseWords.length) return void 0;
|
|
25
25
|
let cost = 0;
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
let addedWords = 0;
|
|
27
|
+
let phraseIndex = 0;
|
|
28
|
+
for (const inputWord of inputWords) {
|
|
29
|
+
let wordCost = completionCost(inputWord, phraseWords[phraseIndex] ?? "");
|
|
30
|
+
while (wordCost === void 0 && addedWords < 3) {
|
|
31
|
+
phraseIndex += 1;
|
|
32
|
+
addedWords += 1;
|
|
33
|
+
wordCost = completionCost(inputWord, phraseWords[phraseIndex] ?? "");
|
|
34
|
+
}
|
|
30
35
|
if (wordCost === void 0) return void 0;
|
|
31
36
|
cost += wordCost;
|
|
37
|
+
phraseIndex += 1;
|
|
32
38
|
}
|
|
33
|
-
return
|
|
39
|
+
return 2 + cost + addedWords;
|
|
34
40
|
};
|
|
35
41
|
const completeYearPrefix = (input) => {
|
|
36
42
|
const match = String.match(/(?:^| )(\d{3,4})$/u)(input);
|
package/dist/resolve/resolve.mjs
CHANGED
|
@@ -45,7 +45,7 @@ const resolve = Effect.fn("chronolizer.resolve")(function* (range) {
|
|
|
45
45
|
if (range.lower !== void 0 && range.upper !== void 0) {
|
|
46
46
|
const lower = yield* resolveLower(range.lower, reference, zone);
|
|
47
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
|
|
48
|
+
if (DateTime.toEpochMillis(lower.value) >= DateTime.toEpochMillis(upper.value)) return yield* new ResolutionError({ message: "The range start must be before the range end" });
|
|
49
49
|
return ResolvedDateRange.make({
|
|
50
50
|
lower,
|
|
51
51
|
upper
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chronolizer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
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",
|