nl2time 0.1.1 → 0.3.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 +41 -2
- package/dist/corpus/runner.d.ts +3 -0
- package/dist/corpus/runner.js +1 -1
- package/dist/engine/resolve.js +50 -14
- package/dist/index.d.ts +5 -1
- package/dist/index.js +5 -1
- package/dist/packs/index.d.ts +39 -0
- package/dist/packs/index.js +156 -0
- package/dist/parse/de.d.ts +9 -0
- package/dist/parse/de.js +1301 -0
- package/dist/parse/en.d.ts +9 -0
- package/dist/parse/en.js +258 -35
- package/dist/parse/es.d.ts +9 -0
- package/dist/parse/es.js +3286 -0
- package/dist/parse/fr.d.ts +9 -0
- package/dist/parse/fr.js +2358 -0
- package/dist/parse/index.d.ts +16 -1
- package/dist/parse/index.js +63 -13
- package/dist/parse/ja.d.ts +7 -0
- package/dist/parse/ja.js +1207 -0
- package/dist/parse/languages.d.ts +17 -0
- package/dist/parse/languages.js +25 -0
- package/dist/parse/latin.d.ts +97 -0
- package/dist/parse/latin.js +692 -0
- package/dist/parse/tokenizer.js +30 -1
- package/dist/parse/zh.d.ts +6 -0
- package/dist/parse/zh.js +1360 -0
- package/package.json +3 -3
package/dist/parse/index.d.ts
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import type { TimeContext } from '../context.js';
|
|
7
7
|
import type { TimeExpr } from '../ir/types.js';
|
|
8
|
+
import { type DomainPack } from '../packs/index.js';
|
|
9
|
+
import { type Rule } from './en.js';
|
|
8
10
|
export interface ParseMatch {
|
|
9
11
|
/** The matched source text span. */
|
|
10
12
|
text: string;
|
|
@@ -17,4 +19,17 @@ export interface ParseMatch {
|
|
|
17
19
|
export interface ParseResult {
|
|
18
20
|
matches: ParseMatch[];
|
|
19
21
|
}
|
|
20
|
-
export
|
|
22
|
+
export interface ParseOptions {
|
|
23
|
+
/** Declarative domain packs (docs/extending.md); their rules run first. */
|
|
24
|
+
packs?: DomainPack[];
|
|
25
|
+
/** Extra code-level rules; run after packs, before built-ins. */
|
|
26
|
+
rules?: Rule[];
|
|
27
|
+
/** Built-in rule names to disable (see EN_RULE_ENTRIES). */
|
|
28
|
+
disable?: string[];
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Precompiled parser for hot paths — compiles packs/rule lists once per
|
|
32
|
+
* language instead of per call.
|
|
33
|
+
*/
|
|
34
|
+
export declare function createParser(opts?: ParseOptions): (text: string, ctx: TimeContext) => ParseResult;
|
|
35
|
+
export declare function parse(text: string, ctx: TimeContext, opts?: ParseOptions): ParseResult;
|
package/dist/parse/index.js
CHANGED
|
@@ -1,13 +1,43 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { compilePack } from '../packs/index.js';
|
|
2
|
+
import { languageDef } from './languages.js';
|
|
2
3
|
import { tokenize } from './tokenizer.js';
|
|
3
|
-
|
|
4
|
-
|
|
4
|
+
function buildRules(opts, language) {
|
|
5
|
+
const base = languageDef(language).ruleEntries;
|
|
6
|
+
if (!opts || (!opts.packs?.length && !opts.rules?.length && !opts.disable?.length)) {
|
|
7
|
+
return base.map((e) => e.rule);
|
|
8
|
+
}
|
|
9
|
+
const disabled = new Set([...(opts.disable ?? []), ...(opts.packs ?? []).flatMap((p) => p.disable ?? [])]);
|
|
10
|
+
return [
|
|
11
|
+
...(opts.packs ?? []).map(compilePack),
|
|
12
|
+
...(opts.rules ?? []),
|
|
13
|
+
...base.filter((e) => !disabled.has(e.name)).map((e) => e.rule),
|
|
14
|
+
];
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Precompiled parser for hot paths — compiles packs/rule lists once per
|
|
18
|
+
* language instead of per call.
|
|
19
|
+
*/
|
|
20
|
+
export function createParser(opts = {}) {
|
|
21
|
+
const cache = new Map();
|
|
22
|
+
return (text, ctx) => {
|
|
23
|
+
let rules = cache.get(ctx.language);
|
|
24
|
+
if (!rules) {
|
|
25
|
+
rules = buildRules(opts, ctx.language);
|
|
26
|
+
cache.set(ctx.language, rules);
|
|
27
|
+
}
|
|
28
|
+
return parseWithRules(text, ctx, rules);
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export function parse(text, ctx, opts) {
|
|
32
|
+
return parseWithRules(text, ctx, buildRules(opts, ctx.language));
|
|
33
|
+
}
|
|
34
|
+
function parseWithRules(text, ctx, rules) {
|
|
5
35
|
const tokens = tokenize(text);
|
|
6
36
|
const raw = [];
|
|
7
37
|
let i = 0;
|
|
8
38
|
while (i < tokens.length) {
|
|
9
39
|
let best;
|
|
10
|
-
for (const rule of
|
|
40
|
+
for (const rule of rules) {
|
|
11
41
|
const m = rule(tokens, i, ctx);
|
|
12
42
|
if (!m)
|
|
13
43
|
continue;
|
|
@@ -24,9 +54,10 @@ export function parse(text, ctx) {
|
|
|
24
54
|
}
|
|
25
55
|
}
|
|
26
56
|
// Merge to fixpoint: [date, duration, time] needs two passes.
|
|
27
|
-
|
|
57
|
+
const connectors = languageDef(ctx.language).connectors;
|
|
58
|
+
let merged = mergeAdjacent(raw, tokens, connectors);
|
|
28
59
|
for (let pass = 0; pass < 2; pass += 1) {
|
|
29
|
-
const again = mergeAdjacent(merged, tokens);
|
|
60
|
+
const again = mergeAdjacent(merged, tokens, connectors);
|
|
30
61
|
if (again.length === merged.length)
|
|
31
62
|
break;
|
|
32
63
|
merged = again;
|
|
@@ -52,7 +83,7 @@ export function parse(text, ctx) {
|
|
|
52
83
|
* separated by a connector token) into intersect(date, time) — "yesterday at
|
|
53
84
|
* 9pm", "next Tuesday morning", "May 29 at noon".
|
|
54
85
|
*/
|
|
55
|
-
function mergeAdjacent(matches, tokens) {
|
|
86
|
+
function mergeAdjacent(matches, tokens, connectors) {
|
|
56
87
|
const out = [];
|
|
57
88
|
let k = 0;
|
|
58
89
|
while (k < matches.length) {
|
|
@@ -88,7 +119,7 @@ function mergeAdjacent(matches, tokens) {
|
|
|
88
119
|
isWeekExpr(cur.expr) &&
|
|
89
120
|
next.expr.op === 'seek' &&
|
|
90
121
|
next.expr.target.kind === 'weekday' &&
|
|
91
|
-
isAdjacent(cur, next, tokens)) {
|
|
122
|
+
isAdjacent(cur, next, tokens, connectors)) {
|
|
92
123
|
out.push({
|
|
93
124
|
expr: { op: 'seek', base: cur.expr, dir: 'next', target: next.expr.target, n: 1 },
|
|
94
125
|
consumed: next.tokenEnd - cur.tokenStart,
|
|
@@ -100,13 +131,16 @@ function mergeAdjacent(matches, tokens) {
|
|
|
100
131
|
k += 2;
|
|
101
132
|
continue;
|
|
102
133
|
}
|
|
103
|
-
if (next && canMerge(cur, next) && isAdjacent(cur, next, tokens)) {
|
|
134
|
+
if (next && canMerge(cur, next) && isAdjacent(cur, next, tokens, connectors)) {
|
|
104
135
|
const [datePart, timePart] = cur.role === 'time' ? [next, cur] : [cur, next];
|
|
105
136
|
let dateExpr = datePart.expr;
|
|
106
137
|
let timeExpr = inferMeridiem(dateExpr, timePart.expr);
|
|
107
138
|
// A day-period on the date side pushes its meridiem into an explicit
|
|
108
139
|
// time range and then steps aside: "Monday morning 6-8" is Monday
|
|
109
|
-
// 06:00–08:00, not clipped to the morning boundaries.
|
|
140
|
+
// 06:00–08:00, not clipped to the morning boundaries. A cross-midnight
|
|
141
|
+
// range ("last night between 11pm and 1am") keeps the period part:
|
|
142
|
+
// its window extends past midnight, so the range can roll into the
|
|
143
|
+
// next day instead of being clipped at the day anchor's end (#19).
|
|
110
144
|
if (timeExpr.op === 'between') {
|
|
111
145
|
const period = findDayPeriod(dateExpr);
|
|
112
146
|
if (period) {
|
|
@@ -115,7 +149,11 @@ function mergeAdjacent(matches, tokens) {
|
|
|
115
149
|
start: forceMeridiem(timeExpr.start, period),
|
|
116
150
|
end: forceMeridiem(timeExpr.end, period),
|
|
117
151
|
};
|
|
118
|
-
|
|
152
|
+
const sm = literalMinuteOfDay(timeExpr.start);
|
|
153
|
+
const em = literalMinuteOfDay(timeExpr.end);
|
|
154
|
+
const wraps = sm !== undefined && em !== undefined && em <= sm;
|
|
155
|
+
if (!wraps)
|
|
156
|
+
dateExpr = stripDayPeriod(dateExpr) ?? dateExpr;
|
|
119
157
|
}
|
|
120
158
|
}
|
|
121
159
|
out.push({
|
|
@@ -180,6 +218,18 @@ function inferMeridiem(dateExpr, timeExpr) {
|
|
|
180
218
|
time: { ...timeExpr.time, meridiem },
|
|
181
219
|
};
|
|
182
220
|
}
|
|
221
|
+
/** Minutes-since-midnight of a literal time with a known meridiem, if any. */
|
|
222
|
+
function literalMinuteOfDay(expr) {
|
|
223
|
+
if (expr.op !== 'literal' || expr.time?.hour === undefined)
|
|
224
|
+
return undefined;
|
|
225
|
+
const h = expr.time.hour % 12;
|
|
226
|
+
const min = expr.time.minute ?? 0;
|
|
227
|
+
if (expr.time.meridiem === 'am')
|
|
228
|
+
return h * 60 + min;
|
|
229
|
+
if (expr.time.meridiem === 'pm')
|
|
230
|
+
return (h + 12) * 60 + min;
|
|
231
|
+
return undefined;
|
|
232
|
+
}
|
|
183
233
|
function forceMeridiem(expr, period) {
|
|
184
234
|
if (expr.op !== 'literal' || !expr.time || (expr.time.meridiem && expr.time.meridiem !== 'unknown')) {
|
|
185
235
|
return expr;
|
|
@@ -220,11 +270,11 @@ const REDUNDANT_BETWEEN = new Set([
|
|
|
220
270
|
'mon', 'tue', 'tues', 'wed', 'weds', 'thu', 'thur', 'thurs', 'fri', 'sat', 'sun',
|
|
221
271
|
'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday',
|
|
222
272
|
]);
|
|
223
|
-
function isAdjacent(a, b, tokens) {
|
|
273
|
+
function isAdjacent(a, b, tokens, connectors) {
|
|
224
274
|
const between = tokens.slice(a.tokenEnd, b.tokenStart);
|
|
225
275
|
if (between.length > 2)
|
|
226
276
|
return false;
|
|
227
277
|
// A parenthesized weekday between date and time ("May/22 (Tue) 11:30") is
|
|
228
278
|
// redundant with the date and doesn't block the merge.
|
|
229
|
-
return between.every((t) => t.type === 'word' && (
|
|
279
|
+
return between.every((t) => t.type === 'word' && (connectors.includes(t.value) || REDUNDANT_BETWEEN.has(t.value)));
|
|
230
280
|
}
|