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.
- package/README.md +50 -47
- 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 +26 -21
- package/dist/language/model.mjs +1 -1
- package/dist/language/registry.mjs +34 -12
- package/dist/locales/cs.mjs +194 -36
- package/dist/locales/de.mjs +307 -490
- package/dist/locales/en.mjs +162 -46
- package/dist/locales/es.mjs +187 -37
- package/dist/locales/fr.mjs +168 -52
- package/dist/locales/nl.mjs +125 -37
- package/dist/locales/pl.mjs +163 -36
- package/dist/locales/shared.mjs +68 -5
- package/dist/locales/tr.mjs +144 -42
- package/dist/natural/correction.d.mts +1 -1
- package/dist/natural/correction.mjs +97 -28
- package/dist/natural/format.mjs +1 -1
- package/dist/natural/parse.mjs +6 -5
- package/dist/natural/suggest.mjs +27 -3
- package/dist/natural/suggestion.mjs +50 -26
- package/dist/resolve/resolve.mjs +2 -3
- package/package.json +2 -1
package/dist/locales/tr.mjs
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import { isIsoDate } from "../ast/schemas.mjs";
|
|
2
1
|
import { BaseLanguageContribution } from "../language/model.mjs";
|
|
3
2
|
import { normalizeNaturalText } from "../natural/text.mjs";
|
|
4
3
|
import { defineLanguagePlugin, languagePluginsLayer } from "../language/registry.mjs";
|
|
5
4
|
import { correctWhitespaceSeparatedText } from "../natural/correction.mjs";
|
|
6
5
|
import { completeNaturalPhrases, fixedCalendarPeriodPhrases, naturalCount, prefixNaturalPhrases } from "../natural/suggestion.mjs";
|
|
7
|
-
import { absoluteDatePeriod, calendarPeriodOffset, candidate, currentYearDatePeriods, datedPeriods, datedQuarterPeriods,
|
|
6
|
+
import { absoluteDatePeriod, calendarPeriodOffset, candidate, compileCountAliasNormalizer, compoundCountAliases, countAliasVocabulary, currentYearDatePeriods, datedPeriods, datedQuarterPeriods, decimalTens, decomposeShiftedPeriodRange, fixedMonthPeriod, fixedQuarterPeriod, fixedYearPeriod, fromNowRange, futurePeriod, futureRange, 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
7
|
import { Effect, Option, String as String$1 } from "effect";
|
|
9
8
|
//#region src/locales/tr.ts
|
|
10
9
|
const months = [
|
|
@@ -21,6 +20,61 @@ const months = [
|
|
|
21
20
|
"kasım",
|
|
22
21
|
"aralık"
|
|
23
22
|
];
|
|
23
|
+
const weekdays = [
|
|
24
|
+
"pazartesi",
|
|
25
|
+
"salı",
|
|
26
|
+
"çarşamba",
|
|
27
|
+
"perşembe",
|
|
28
|
+
"cuma",
|
|
29
|
+
"cumartesi",
|
|
30
|
+
"pazar"
|
|
31
|
+
];
|
|
32
|
+
const nextWeekdayPhrases = weekdays.map((weekday) => `gelecek ${weekday}`);
|
|
33
|
+
const turkishCountWords = [
|
|
34
|
+
["iki"],
|
|
35
|
+
["üç", "uc"],
|
|
36
|
+
["dört", "dort"],
|
|
37
|
+
["beş", "bes"],
|
|
38
|
+
["altı", "alti"],
|
|
39
|
+
["yedi"],
|
|
40
|
+
["sekiz"],
|
|
41
|
+
["dokuz"],
|
|
42
|
+
["on"],
|
|
43
|
+
["on bir"],
|
|
44
|
+
["on iki"],
|
|
45
|
+
["on üç"],
|
|
46
|
+
["on dört"],
|
|
47
|
+
["on beş"],
|
|
48
|
+
["on altı"],
|
|
49
|
+
["on yedi"],
|
|
50
|
+
["on sekiz"],
|
|
51
|
+
["on dokuz"],
|
|
52
|
+
["yirmi"]
|
|
53
|
+
];
|
|
54
|
+
const turkishCountOnes = [
|
|
55
|
+
[1, "bir"],
|
|
56
|
+
[2, "iki"],
|
|
57
|
+
[3, "üç"],
|
|
58
|
+
[4, "dört"],
|
|
59
|
+
[5, "beş"],
|
|
60
|
+
[6, "altı"],
|
|
61
|
+
[7, "yedi"],
|
|
62
|
+
[8, "sekiz"],
|
|
63
|
+
[9, "dokuz"]
|
|
64
|
+
];
|
|
65
|
+
const turkishCountAliases = [...sequentialCountAliases(turkishCountWords, 2), ...compoundCountAliases(decimalTens([
|
|
66
|
+
"yirmi",
|
|
67
|
+
"otuz",
|
|
68
|
+
"kırk",
|
|
69
|
+
"elli",
|
|
70
|
+
"altmış",
|
|
71
|
+
"yetmiş",
|
|
72
|
+
"seksen",
|
|
73
|
+
"doksan"
|
|
74
|
+
]), turkishCountOnes, (ten, one) => [`${ten} ${one}`])];
|
|
75
|
+
const normalizeTurkishCounts = compileCountAliasNormalizer(turkishCountAliases);
|
|
76
|
+
const turkishCountVocabulary = new Set(countAliasVocabulary(turkishCountAliases));
|
|
77
|
+
const correctTurkish = (input, vocabulary) => correctWhitespaceSeparatedText(input, vocabulary, turkishCountVocabulary);
|
|
24
78
|
const monthAbbreviations = [
|
|
25
79
|
["oca"],
|
|
26
80
|
["şub", "sub"],
|
|
@@ -303,15 +357,7 @@ const parseNamedDate = (input) => {
|
|
|
303
357
|
const named = String$1.match(/^([0-3]?\d)(?:\.)? ([a-zçğıöşü]+\.?)(?:,)? (\d{4})$/u)(input);
|
|
304
358
|
if (Option.isSome(named)) return namedDatePeriod(textAt(named.value, 3), textAt(named.value, 2), textAt(named.value, 1), monthNumber, dateLabel);
|
|
305
359
|
const numeric = String$1.match(/^([0-3]?\d)[./-]([01]?\d)[./-](\d{4})$/u)(input);
|
|
306
|
-
if (Option.isSome(numeric))
|
|
307
|
-
const year = validYear(textAt(numeric.value, 3));
|
|
308
|
-
const month = Number(textAt(numeric.value, 2));
|
|
309
|
-
const day = Number(textAt(numeric.value, 1));
|
|
310
|
-
if (year !== void 0 && month >= 1 && month <= 12) {
|
|
311
|
-
const value = isoDate(year, month, day);
|
|
312
|
-
if (isIsoDate(value) && value !== "9999-12-31") return Option.some(fixedDatePeriod(value, dateLabel(day, month, year)));
|
|
313
|
-
}
|
|
314
|
-
}
|
|
360
|
+
if (Option.isSome(numeric)) return namedDatePeriod(textAt(numeric.value, 3), textAt(numeric.value, 2), textAt(numeric.value, 1), Number, dateLabel);
|
|
315
361
|
const current = String$1.match(/^([0-3]?\d)(?:\.)? ([a-zçğıöşü]+\.?)$/u)(input);
|
|
316
362
|
return Option.isSome(current) ? namedCurrentYearDatePeriod(textAt(current.value, 2), textAt(current.value, 1), monthNumber, currentDateLabel) : Option.none();
|
|
317
363
|
};
|
|
@@ -340,13 +386,13 @@ const parseQuarter = (input) => {
|
|
|
340
386
|
const quarter = quarterNumber(textAt(standalone.value, 1));
|
|
341
387
|
return quarter === void 0 ? Option.none() : Option.some(quarterOfRelativeYear(quarter, 0, `Ç${quarter}`));
|
|
342
388
|
};
|
|
343
|
-
const
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
if (Option.isSome(
|
|
389
|
+
const parseDatedPeriod = (input) => {
|
|
390
|
+
const knownPeriod = Option.firstSomeOf([
|
|
391
|
+
absoluteDatePeriod(input, "tr"),
|
|
392
|
+
parseNamedDate(input),
|
|
393
|
+
parseQuarter(input)
|
|
394
|
+
]);
|
|
395
|
+
if (Option.isSome(knownPeriod)) return knownPeriod;
|
|
350
396
|
const yearMatch = String$1.match(/^(?:yıl |yılı )?(\d{4})$/u)(input);
|
|
351
397
|
if (Option.isSome(yearMatch)) {
|
|
352
398
|
const year = validYear(textAt(yearMatch.value, 1));
|
|
@@ -358,6 +404,20 @@ const parseBasePeriod = (input) => {
|
|
|
358
404
|
const year = validYear(textAt(monthYear.value, 2));
|
|
359
405
|
if (month !== void 0 && year !== void 0) return Option.some(fixedMonthPeriod(year, month, `${title(textAt(months, month - 1))} ${year}`));
|
|
360
406
|
}
|
|
407
|
+
return Option.none();
|
|
408
|
+
};
|
|
409
|
+
const parseBasePeriod = (input) => {
|
|
410
|
+
const datedPeriod = parseDatedPeriod(input);
|
|
411
|
+
if (Option.isSome(datedPeriod)) return datedPeriod;
|
|
412
|
+
const prefixedRelativeMonth = String$1.match(/^(geçen|bu|gelecek) ([a-zçğıöşü]+\.?)$/u)(input);
|
|
413
|
+
if (Option.isSome(prefixedRelativeMonth)) {
|
|
414
|
+
const month = monthNumber(textAt(prefixedRelativeMonth.value, 2));
|
|
415
|
+
if (month !== void 0) {
|
|
416
|
+
const modifier = textAt(prefixedRelativeMonth.value, 1);
|
|
417
|
+
const direction = relativeYearDirection(modifier);
|
|
418
|
+
return Option.some(monthOfRelativeYear(month, direction, `${modifier} ${title(textAt(months, month - 1))}`));
|
|
419
|
+
}
|
|
420
|
+
}
|
|
361
421
|
const relativeMonth = String$1.match(/^([a-zçğıöşü]+\.?) (geçen yıl|gelecek yıl|bu yıl)$/u)(input);
|
|
362
422
|
const relativeMonthYearFirst = String$1.match(/^(geçen yıl|gelecek yıl|bu yıl) ([a-zçğıöşü]+\.?)$/u)(input);
|
|
363
423
|
const relativeMatch = Option.firstSomeOf([relativeMonth, relativeMonthYearFirst]);
|
|
@@ -378,16 +438,37 @@ const parseBasePeriod = (input) => {
|
|
|
378
438
|
if (["gelecek hafta sonu", "önümüzdeki hafta sonu"].includes(input)) return Option.some(relativeWeekend(1, "gelecek hafta sonu"));
|
|
379
439
|
if (input === "geçen hafta sonundan önceki hafta sonu") return Option.some(relativeWeekend(-2, input));
|
|
380
440
|
if (input === "gelecek hafta sonundan sonraki hafta sonu") return Option.some(relativeWeekend(2, input));
|
|
381
|
-
|
|
441
|
+
const weekday = nextWeekdayPhrases.indexOf(input);
|
|
442
|
+
return weekday === -1 ? Option.none() : Option.some(relativeWeekday(weekday, 1, nextWeekdayPhrases[weekday] ?? input));
|
|
382
443
|
};
|
|
383
|
-
const
|
|
444
|
+
const parsePeriodEdge = (input) => {
|
|
384
445
|
const edge = String$1.match(/^(.+?)(?:ın|in|un|ün) (başı|başlangıcı|sonu)$/u)(input);
|
|
385
|
-
if (Option.
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
446
|
+
if (Option.isNone(edge)) return Option.none();
|
|
447
|
+
const periodText = textAt(edge.value, 1);
|
|
448
|
+
const basePeriod = parseBasePeriod(periodText);
|
|
449
|
+
const implicitUnit = unitAliases.find((entry) => entry[0] === periodText)?.[1];
|
|
450
|
+
const implicit = units.find((entry) => entry.unit === implicitUnit);
|
|
451
|
+
const period = Option.isSome(basePeriod) || implicit === void 0 ? basePeriod : Option.some(relativePeriod(implicit.unit, 0, implicit.current));
|
|
452
|
+
if (Option.isNone(period)) return Option.none();
|
|
453
|
+
const isEnd = textAt(edge.value, 2) === "sonu";
|
|
454
|
+
return Option.some(isEnd ? periodEndDay(period.value, input) : periodStartDay(period.value, input));
|
|
455
|
+
};
|
|
456
|
+
const parsePeriod = (input) => {
|
|
457
|
+
const prefix = String$1.match(/^([1-9]\d*) (gün|gun|hafta|ay|çeyrek|ceyrek|yıl|yil|sene) (önce|sonra) (.+)$/u)(input);
|
|
458
|
+
const suffix = String$1.match(/^(.+) ([1-9]\d*) (gün|gun|hafta|ay|çeyrek|ceyrek|yıl|yil|sene) (önce|sonra)$/u)(input);
|
|
459
|
+
const shifted = Option.firstSomeOf([prefix, suffix]);
|
|
460
|
+
if (Option.isSome(shifted)) {
|
|
461
|
+
const prefixOrder = Option.isSome(prefix);
|
|
462
|
+
const amount = parseTrailingCount(textAt(shifted.value, prefixOrder ? 1 : 2));
|
|
463
|
+
const alias = unitAliases.find((unit) => unit[0] === textAt(shifted.value, prefixOrder ? 2 : 3));
|
|
464
|
+
const entry = alias === void 0 ? void 0 : units.find((unit) => unit.unit === alias[1]);
|
|
465
|
+
const period = parsePeriod(textAt(shifted.value, prefixOrder ? 4 : 1));
|
|
466
|
+
if (Option.isSome(amount) && entry !== void 0 && Option.isSome(period)) {
|
|
467
|
+
const past = textAt(shifted.value, prefixOrder ? 3 : 4) === "önce";
|
|
468
|
+
const direction = past ? -amount.value : amount.value;
|
|
469
|
+
const noun = amount.value === 1 ? entry.singular : entry.plural;
|
|
470
|
+
const canonical = `${amount.value} ${noun} ${past ? "önce" : "sonra"} ${period.value.canonical}`;
|
|
471
|
+
return Option.some(shiftPeriod(period.value, direction, entry.unit, canonical));
|
|
391
472
|
}
|
|
392
473
|
}
|
|
393
474
|
const wrapper = [
|
|
@@ -395,17 +476,19 @@ const parsePeriod = (input) => {
|
|
|
395
476
|
"içinde ",
|
|
396
477
|
"tüm "
|
|
397
478
|
].find((prefix) => input.startsWith(prefix));
|
|
398
|
-
return
|
|
479
|
+
return Option.firstSomeOf([
|
|
480
|
+
parsePeriodEdge(input),
|
|
481
|
+
parseBasePeriod(wrapper === void 0 ? input : input.slice(wrapper.length)),
|
|
482
|
+
parseCalendarOffset(input)
|
|
483
|
+
]);
|
|
399
484
|
};
|
|
400
485
|
const countedUnit = (value) => unitAliases.find((entry) => entry[0] === value)?.[1];
|
|
401
|
-
const
|
|
402
|
-
const
|
|
403
|
-
const
|
|
404
|
-
const
|
|
405
|
-
const
|
|
406
|
-
const
|
|
407
|
-
const rollingSincePattern = countedPattern("^([1-9]\\d*) (UNIT) boyunca$");
|
|
408
|
-
const rollingBarePattern = countedPattern("^([1-9]\\d*) (UNIT)$");
|
|
486
|
+
const calendarPastPattern = /^([1-9]\d*) ([^ ]+) önce$/u;
|
|
487
|
+
const calendarFuturePatterns = [/^([1-9]\d*) ([^ ]+) sonra$/u, /^([1-9]\d*) ([^ ]+) içinde$/u];
|
|
488
|
+
const rollingPastPatterns = [/^(?:son|geçen|önceki|geçtiğimiz) ([1-9]\d*) ([^ ]+)$/u, /^([1-9]\d*) (?:son|geçen|önceki) ([^ ]+)$/u];
|
|
489
|
+
const rollingFuturePatterns = [/^(?:gelecek|önümüzdeki|sonraki) ([1-9]\d*) ([^ ]+)$/u, /^([1-9]\d*) (?:gelecek|sonraki) ([^ ]+)$/u];
|
|
490
|
+
const rollingSincePattern = /^([1-9]\d*) ([^ ]+) boyunca$/u;
|
|
491
|
+
const rollingBarePattern = /^([1-9]\d*) ([^ ]+)$/u;
|
|
409
492
|
const firstPatternMatch = (input, patterns) => Option.firstSomeOf(patterns.map((pattern) => String$1.match(pattern)(input)));
|
|
410
493
|
const singularRollingCanonical = (entry, future) => future ? `önümüzdeki bir ${entry.singular}` : `son bir ${entry.singular}`;
|
|
411
494
|
const singularRollingPhrases = units.flatMap((entry) => [
|
|
@@ -446,7 +529,7 @@ const singularCalendarOffsets = units.flatMap((entry) => [{
|
|
|
446
529
|
}]);
|
|
447
530
|
const parseCalendarOffset = (input) => {
|
|
448
531
|
const singular = singularCalendarOffsets.find((entry) => entry.phrase === input);
|
|
449
|
-
if (singular !== void 0) return Option.some(
|
|
532
|
+
if (singular !== void 0) return Option.some(relativePeriod(singular.entry.unit, singular.direction, singular.phrase));
|
|
450
533
|
const past = String$1.match(calendarPastPattern)(input);
|
|
451
534
|
const future = firstPatternMatch(input, calendarFuturePatterns);
|
|
452
535
|
const match = Option.firstSomeOf([past, future]);
|
|
@@ -459,7 +542,7 @@ const parseCalendarOffset = (input) => {
|
|
|
459
542
|
const direction = Option.isSome(past) ? -amount.value : amount.value;
|
|
460
543
|
const noun = amount.value === 1 ? entry.singular : entry.plural;
|
|
461
544
|
const canonical = direction < 0 ? `${amount.value} ${noun} önce` : `${amount.value} ${noun} sonra`;
|
|
462
|
-
return Option.some(
|
|
545
|
+
return Option.some(relativePeriod(unit, direction, canonical));
|
|
463
546
|
};
|
|
464
547
|
const parseRollingPeriod = (input) => {
|
|
465
548
|
const singular = singularRollingPhrases.find((entry) => entry.phrase === input);
|
|
@@ -543,8 +626,6 @@ const parseTurkish = (input) => {
|
|
|
543
626
|
"şu andan itibaren",
|
|
544
627
|
"bundan sonra"
|
|
545
628
|
].includes(input)) return Option.some(candidate(fromNowRange(), "bugünden itibaren"));
|
|
546
|
-
const offset = parseCalendarOffset(input);
|
|
547
|
-
if (Option.isSome(offset)) return offset;
|
|
548
629
|
const rolling = parseRollingPeriod(input);
|
|
549
630
|
if (Option.isSome(rolling)) return rolling;
|
|
550
631
|
const toDate = toDatePhrases.find((entry) => entry.phrase === input);
|
|
@@ -552,7 +633,12 @@ const parseTurkish = (input) => {
|
|
|
552
633
|
const elided = parseElidedDateRange(input);
|
|
553
634
|
if (Option.isSome(elided)) return elided;
|
|
554
635
|
const joinedInput = input.endsWith(" arası") ? input.slice(0, -6) : input;
|
|
555
|
-
const nowBounded = joinedNowCandidate(joinedInput, [["", " ile bugün"], ["", " ve bugün"]], [
|
|
636
|
+
const nowBounded = joinedNowCandidate(joinedInput, [["", " ile bugün"], ["", " ve bugün"]], [
|
|
637
|
+
"bugün ile ",
|
|
638
|
+
"bugün ve ",
|
|
639
|
+
"şimdi ile ",
|
|
640
|
+
"şimdi ve "
|
|
641
|
+
], parsePeriod, (period) => `${period} ile bugün arası`, (period) => `bugün ile ${period} arası`);
|
|
556
642
|
if (Option.isSome(nowBounded)) return nowBounded;
|
|
557
643
|
const bounded = joinedPeriodCandidate(joinedInput, [
|
|
558
644
|
["", " ile "],
|
|
@@ -591,6 +677,8 @@ const staticPeriodPhrases = [
|
|
|
591
677
|
"geçen yılın sonu",
|
|
592
678
|
"gelecek yılın başı",
|
|
593
679
|
"gelecek yılın sonu",
|
|
680
|
+
...nextWeekdayPhrases,
|
|
681
|
+
...months.flatMap((month) => [`geçen ${month}`, `gelecek ${month}`]),
|
|
594
682
|
...[
|
|
595
683
|
1,
|
|
596
684
|
2,
|
|
@@ -651,6 +739,17 @@ const suggestTurkish = (input, limit) => {
|
|
|
651
739
|
], limit);
|
|
652
740
|
};
|
|
653
741
|
const renderTurkish = (range) => {
|
|
742
|
+
const shifted = decomposeShiftedPeriodRange(range);
|
|
743
|
+
if (Option.isSome(shifted)) {
|
|
744
|
+
const base = renderTurkish(shifted.value.baseRange);
|
|
745
|
+
const entry = units.find((unit) => unit.unit === shifted.value.unit);
|
|
746
|
+
if (Option.isSome(base) && entry !== void 0) {
|
|
747
|
+
const amount = Math.abs(shifted.value.amount);
|
|
748
|
+
const noun = amount === 1 ? entry.singular : entry.plural;
|
|
749
|
+
const direction = shifted.value.amount < 0 ? "önce" : "sonra";
|
|
750
|
+
return Option.some(`${amount} ${noun} ${direction} ${base.value}`);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
654
753
|
const offset = calendarPeriodOffset(range);
|
|
655
754
|
if (Option.isSome(offset) && Math.abs(offset.value.amount) > 1) {
|
|
656
755
|
const entry = units.find((unit) => unit.unit === offset.value.unit);
|
|
@@ -677,11 +776,12 @@ const renderTurkish = (range) => {
|
|
|
677
776
|
];
|
|
678
777
|
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
778
|
};
|
|
680
|
-
const normalizeTurkish = (input, locale) => normalizeNaturalText(input, locale).replaceAll("’", "'");
|
|
779
|
+
const normalizeTurkish = (input, locale) => normalizeTurkishCounts(normalizeNaturalText(input, locale).replaceAll("’", "'"));
|
|
681
780
|
const TurkishContribution = new BaseLanguageContribution({
|
|
682
781
|
locale: "tr",
|
|
683
782
|
vocabulary: [
|
|
684
783
|
...months,
|
|
784
|
+
...weekdays,
|
|
685
785
|
...monthAbbreviations.flatMap((aliases) => aliases),
|
|
686
786
|
...units.flatMap((entry) => [
|
|
687
787
|
entry.singular,
|
|
@@ -698,6 +798,7 @@ const TurkishContribution = new BaseLanguageContribution({
|
|
|
698
798
|
"gelecek",
|
|
699
799
|
"geçen",
|
|
700
800
|
"geri",
|
|
801
|
+
"ile",
|
|
701
802
|
"itibaren",
|
|
702
803
|
"itibarıyla",
|
|
703
804
|
"kadar",
|
|
@@ -708,10 +809,11 @@ const TurkishContribution = new BaseLanguageContribution({
|
|
|
708
809
|
"son",
|
|
709
810
|
"sonra",
|
|
710
811
|
"sonrası",
|
|
812
|
+
"şimdi",
|
|
711
813
|
"şimdiye"
|
|
712
814
|
],
|
|
713
815
|
normalize: normalizeTurkish,
|
|
714
|
-
correct:
|
|
816
|
+
correct: correctTurkish,
|
|
715
817
|
parseExact: parseTurkish,
|
|
716
818
|
suggest: suggestTurkish,
|
|
717
819
|
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;
|
|
@@ -1,50 +1,119 @@
|
|
|
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;
|
|
37
|
+
}
|
|
38
|
+
return previous[right.length] ?? outside;
|
|
39
|
+
};
|
|
40
|
+
const segmentedReplacements = (word, vocabulary) => {
|
|
41
|
+
const segmentations = Array.from({ length: word.length + 1 }, () => []);
|
|
42
|
+
const initial = segmentations[0];
|
|
43
|
+
if (initial !== void 0) initial.push([]);
|
|
44
|
+
for (let start = 0; start < word.length; start += 1) {
|
|
45
|
+
const prefixes = segmentations[start] ?? [];
|
|
46
|
+
if (prefixes.length === 0) continue;
|
|
47
|
+
for (let end = start + 1; end <= word.length; end += 1) {
|
|
48
|
+
const part = word.slice(start, end);
|
|
49
|
+
if (!vocabulary.has(part)) continue;
|
|
50
|
+
const target = segmentations[end];
|
|
51
|
+
if (target === void 0) continue;
|
|
52
|
+
for (const prefix of prefixes) {
|
|
53
|
+
if (target.length >= 4) break;
|
|
54
|
+
target.push([...prefix, part]);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
20
57
|
}
|
|
21
|
-
return
|
|
58
|
+
return (segmentations[word.length] ?? []).filter((parts) => parts.length > 1).map((parts) => ({
|
|
59
|
+
word: parts.join(" "),
|
|
60
|
+
distance: parts.length - 1
|
|
61
|
+
}));
|
|
62
|
+
};
|
|
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;
|
|
22
70
|
};
|
|
23
|
-
const replacementsFor = (word, vocabulary) => {
|
|
24
|
-
if (vocabulary.
|
|
71
|
+
const replacementsFor = (word, vocabulary, segmentationVocabulary, workspace) => {
|
|
72
|
+
if (vocabulary.has(word) || isProtectedValue(word)) return [{
|
|
25
73
|
word,
|
|
26
74
|
distance: 0
|
|
27
75
|
}];
|
|
28
|
-
|
|
76
|
+
const segmented = segmentedReplacements(word, segmentationVocabulary);
|
|
77
|
+
if (word.length <= 3) return segmented;
|
|
29
78
|
const maximum = word.length >= 6 ? 2 : 1;
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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;
|
|
37
99
|
};
|
|
38
|
-
const
|
|
100
|
+
const emptySegmentationVocabulary = /* @__PURE__ */ new Set();
|
|
101
|
+
const correctWhitespaceSeparatedText = (input, vocabulary, segmentationVocabulary = emptySegmentationVocabulary) => {
|
|
39
102
|
const words = naturalWords(input);
|
|
103
|
+
const wordsInVocabulary = vocabularySet(vocabulary);
|
|
104
|
+
const workspace = [
|
|
105
|
+
[],
|
|
106
|
+
[],
|
|
107
|
+
[]
|
|
108
|
+
];
|
|
40
109
|
let partials = [{
|
|
41
|
-
|
|
110
|
+
text: "",
|
|
42
111
|
corrections: [],
|
|
43
112
|
cost: 0,
|
|
44
113
|
offset: 0
|
|
45
114
|
}];
|
|
46
115
|
for (const word of words) {
|
|
47
|
-
const replacements = replacementsFor(word,
|
|
116
|
+
const replacements = replacementsFor(word, wordsInVocabulary, segmentationVocabulary, workspace);
|
|
48
117
|
if (replacements.length === 0) return [];
|
|
49
118
|
const next = [];
|
|
50
119
|
for (const partial of partials) for (const replacement of replacements) {
|
|
@@ -55,7 +124,7 @@ const correctWhitespaceSeparatedText = (input, vocabulary) => {
|
|
|
55
124
|
offset: partial.offset
|
|
56
125
|
})];
|
|
57
126
|
next.push({
|
|
58
|
-
|
|
127
|
+
text: partial.text.length === 0 ? replacement.word : `${partial.text} ${replacement.word}`,
|
|
59
128
|
corrections: correction,
|
|
60
129
|
cost: partial.cost + replacement.distance,
|
|
61
130
|
offset: partial.offset + word.length + 1
|
|
@@ -64,7 +133,7 @@ const correctWhitespaceSeparatedText = (input, vocabulary) => {
|
|
|
64
133
|
partials = next.slice(0, 32);
|
|
65
134
|
}
|
|
66
135
|
return partials.filter((partial) => partial.corrections.length > 0).map((partial) => NaturalCorrectionCandidate.make({
|
|
67
|
-
text: partial.
|
|
136
|
+
text: partial.text,
|
|
68
137
|
corrections: partial.corrections,
|
|
69
138
|
cost: partial.cost
|
|
70
139
|
}));
|
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
|
@@ -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";
|
|
@@ -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,48 @@
|
|
|
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) => {
|
|
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
|
+
};
|
|
42
|
+
const suggested = distinct(suggestionsFrom([...language.suggest(normalized, limit * 2), normalized]));
|
|
43
|
+
if (suggested.length > 0) return Array$1.take(suggested, limit);
|
|
44
|
+
const completionVocabulary = Array$1.dedupe([...Array$1.flatMap(language.suggest("", 100), naturalWords), ...language.vocabulary]);
|
|
45
|
+
return Array$1.take(distinct(suggestionsFrom(singleWordCompletions(normalized, completionVocabulary))), limit);
|
|
22
46
|
});
|
|
23
47
|
//#endregion
|
|
24
48
|
export { suggestNatural };
|