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.
@@ -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 declare function parse(text: string, ctx: TimeContext): ParseResult;
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;
@@ -1,13 +1,43 @@
1
- import { EN_RULES } from './en.js';
1
+ import { compilePack } from '../packs/index.js';
2
+ import { languageDef } from './languages.js';
2
3
  import { tokenize } from './tokenizer.js';
3
- const CONNECTORS = ['at', 'on', 'in', 'of', 'the', 'for', 'from', 'around', 'about', '-'];
4
- export function parse(text, ctx) {
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 EN_RULES) {
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
- let merged = mergeAdjacent(raw, tokens);
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
- dateExpr = stripDayPeriod(dateExpr) ?? dateExpr;
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' && (CONNECTORS.includes(t.value) || REDUNDANT_BETWEEN.has(t.value)));
279
+ return between.every((t) => t.type === 'word' && (connectors.includes(t.value) || REDUNDANT_BETWEEN.has(t.value)));
230
280
  }
@@ -0,0 +1,7 @@
1
+ import type { Rule } from './en.js';
2
+ export declare const JA_RULE_ENTRIES: readonly {
3
+ name: string;
4
+ rule: Rule;
5
+ }[];
6
+ /** Particles transparent between merged date & time parts ("明日の朝8時"). */
7
+ export declare const JA_CONNECTORS: string[];