nl2time 0.1.0 → 0.3.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.
@@ -15,4 +15,13 @@ export interface RuleMatch {
15
15
  }
16
16
  export type Rule = (tokens: Token[], i: number, ctx: TimeContext) => RuleMatch | undefined;
17
17
  /** Rules in priority order (first match at a position wins ties by length then order). */
18
+ /**
19
+ * Built-in English rules with stable public names (docs/extending.md).
20
+ * Order matters: the scanner prefers longer matches, then higher confidence,
21
+ * then earlier list position — so names are also the disable/override handle.
22
+ */
23
+ export declare const EN_RULE_ENTRIES: readonly {
24
+ name: string;
25
+ rule: Rule;
26
+ }[];
18
27
  export declare const EN_RULES: readonly Rule[];
package/dist/parse/en.js CHANGED
@@ -125,12 +125,17 @@ function yearAt(tokens, i) {
125
125
  ? t.value
126
126
  : undefined;
127
127
  }
128
- /** "in the morning" / "at night" / "tonight" lookahead. Returns period + consumed. */
129
- function readPeriodSuffix(tokens, i) {
128
+ /** "in the morning" / "at night" / "this morning" / "tonight" lookahead.
129
+ * Returns period + consumed. "this <period>" binds a preceding bare hour
130
+ * exactly like "in the <period>" ("8 this morning" → 8am): the period
131
+ * supplies the meridiem, and the reference day is the anchor either way.
132
+ * Deliberately NOT "last night": that also shifts the day, which a
133
+ * meridiem-only suffix can't express (#9). */
134
+ function readPeriodSuffix(tokens, i, allowThis = true) {
130
135
  let at = i;
131
136
  if (word(tokens[at]) === 'in' || word(tokens[at]) === 'at')
132
137
  at += 1;
133
- if (word(tokens[at]) === 'the')
138
+ if (word(tokens[at]) === 'the' || (allowThis && word(tokens[at]) === 'this'))
134
139
  at += 1;
135
140
  const w = word(tokens[at]);
136
141
  if (w === 'tonight')
@@ -143,6 +148,12 @@ function readPeriodSuffix(tokens, i) {
143
148
  function meridiemFor(period) {
144
149
  return period === 'morning' ? 'am' : 'pm';
145
150
  }
151
+ /** Words that license a following bare number as a clock time
152
+ * ("at 8 this morning", "by 5 this afternoon"). */
153
+ const TIME_PREPOSITIONS = new Set([
154
+ 'at', 'around', 'about', 'by', 'from', 'until', 'till', 'to',
155
+ 'before', 'after', 'since',
156
+ ]);
146
157
  // --- rules -----------------------------------------------------------------
147
158
  /** today / yesterday / tomorrow / tonight / day before yesterday / day after tomorrow */
148
159
  const ruleDeicticDay = (tokens, i) => {
@@ -2353,7 +2364,17 @@ const ruleClockTime = (tokens, i, ctx) => {
2353
2364
  if (after === "o'clock" || after === 'oclock') {
2354
2365
  return withPeriodSuffix({ hour: hn.value, meridiem: 'unknown' }, hn.consumed + 1);
2355
2366
  }
2356
- const ps = readPeriodSuffix(tokens, i + hn.consumed + mc);
2367
+ // A BARE number before "this <period>" is only a clock time when the
2368
+ // number isn't already bound by a preceding noun — "8 this morning"
2369
+ // is 8am, but in "building 4 this afternoon" the 4 is a building
2370
+ // number and the period keeps its range reading (Recognizers
2371
+ // rt-dtm-0523). Utterance start or a time-preposition before the
2372
+ // number licenses the binding; an arbitrary preceding word does not.
2373
+ // The in/at forms ("4 in the afternoon") are explicit enough to bind
2374
+ // regardless, as before.
2375
+ const prevWord = word(tokens[i - 1]);
2376
+ const allowThis = i === 0 || prevWord === undefined || TIME_PREPOSITIONS.has(prevWord);
2377
+ const ps = readPeriodSuffix(tokens, i + hn.consumed + mc, allowThis);
2357
2378
  if (ps) {
2358
2379
  const time = { hour: hn.value, meridiem: meridiemFor(ps.period) };
2359
2380
  if (minute !== undefined)
@@ -2383,6 +2404,25 @@ const ruleClockTime = (tokens, i, ctx) => {
2383
2404
  role: 'time',
2384
2405
  };
2385
2406
  }
2407
+ // Day-shifting period phrases: "10 last night", "8 yesterday morning",
2408
+ // "5 tomorrow afternoon" (#10). Emit the bare hour with meridiem
2409
+ // 'unknown' and stop — the following day-part matches separately and the
2410
+ // refiner merges them, with the period supplying the meridiem and the
2411
+ // deictic supplying the day shift. Same noun-guard as "this <period>":
2412
+ // "the count was 4 last night" keeps the period's range reading.
2413
+ const relNext = word(tokens[i + hn.consumed]);
2414
+ const isDayShifter = relNext !== undefined &&
2415
+ (REL_SYNONYMS[relNext] !== undefined ||
2416
+ relNext === 'yesterday' || relNext === 'tomorrow' || relNext === 'today');
2417
+ const relPeriodWord = isDayShifter ? word(tokens[i + hn.consumed + 1]) : undefined;
2418
+ if (allowThis && relPeriodWord !== undefined && PERIOD_WORDS[relPeriodWord]) {
2419
+ return {
2420
+ expr: { op: 'literal', time: { hour: hn.value, meridiem: 'unknown' } },
2421
+ consumed: hn.consumed,
2422
+ confidence: 0.95,
2423
+ role: 'time',
2424
+ };
2425
+ }
2386
2426
  if (['at', 'from', 'around', 'about'].includes(word(tokens[i - 1]) ?? '')) {
2387
2427
  return {
2388
2428
  expr: { op: 'literal', time: { hour: hn.value, meridiem: 'unknown' } },
@@ -2675,34 +2715,40 @@ const ruleAllUnit = (tokens, i) => {
2675
2715
  return { expr: { op: 'amount', amount: amountFor(unit, 1) }, consumed: 2, confidence: 0.9, role: 'duration' };
2676
2716
  };
2677
2717
  /** Rules in priority order (first match at a position wins ties by length then order). */
2678
- export const EN_RULES = [
2679
- ruleRange,
2680
- ruleWeekOf,
2681
- ruleNthWeekdayOf,
2682
- ruleDayOfScope,
2683
- ruleWeekendOf,
2684
- ruleHoliday,
2685
- ruleEarlyLate,
2686
- ruleEndOf,
2687
- ruleDeicticDay,
2688
- ruleWithin,
2689
- ruleNAfterDate,
2690
- ruleUnitAfterNext,
2691
- rulePastN,
2692
- ruleNextN,
2693
- ruleTheNPastNext,
2694
- ruleTheUnit,
2695
- ruleLastThisNext,
2696
- ruleAgoIn,
2697
- ruleDuration,
2698
- ruleFractionDuration,
2699
- ruleAllUnit,
2700
- ruleDecadeCentury,
2701
- ruleBareYear,
2702
- ruleCalendarDate,
2703
- ruleClockTime,
2704
- ruleWeekdayDay,
2705
- ruleWeekdayAlone,
2706
- rulePeriodAlone,
2707
- ruleNow,
2718
+ /**
2719
+ * Built-in English rules with stable public names (docs/extending.md).
2720
+ * Order matters: the scanner prefers longer matches, then higher confidence,
2721
+ * then earlier list position — so names are also the disable/override handle.
2722
+ */
2723
+ export const EN_RULE_ENTRIES = [
2724
+ { name: 'range', rule: ruleRange },
2725
+ { name: 'week-of', rule: ruleWeekOf },
2726
+ { name: 'nth-weekday-of', rule: ruleNthWeekdayOf },
2727
+ { name: 'day-of-scope', rule: ruleDayOfScope },
2728
+ { name: 'weekend-of', rule: ruleWeekendOf },
2729
+ { name: 'holiday', rule: ruleHoliday },
2730
+ { name: 'early-late', rule: ruleEarlyLate },
2731
+ { name: 'end-of', rule: ruleEndOf },
2732
+ { name: 'deictic-day', rule: ruleDeicticDay },
2733
+ { name: 'within', rule: ruleWithin },
2734
+ { name: 'n-after-date', rule: ruleNAfterDate },
2735
+ { name: 'unit-after-next', rule: ruleUnitAfterNext },
2736
+ { name: 'past-n', rule: rulePastN },
2737
+ { name: 'next-n', rule: ruleNextN },
2738
+ { name: 'the-n-past-next', rule: ruleTheNPastNext },
2739
+ { name: 'the-unit', rule: ruleTheUnit },
2740
+ { name: 'last-this-next', rule: ruleLastThisNext },
2741
+ { name: 'ago-in', rule: ruleAgoIn },
2742
+ { name: 'duration', rule: ruleDuration },
2743
+ { name: 'fraction-duration', rule: ruleFractionDuration },
2744
+ { name: 'all-unit', rule: ruleAllUnit },
2745
+ { name: 'decade-century', rule: ruleDecadeCentury },
2746
+ { name: 'bare-year', rule: ruleBareYear },
2747
+ { name: 'calendar-date', rule: ruleCalendarDate },
2748
+ { name: 'clock-time', rule: ruleClockTime },
2749
+ { name: 'weekday-day', rule: ruleWeekdayDay },
2750
+ { name: 'weekday-alone', rule: ruleWeekdayAlone },
2751
+ { name: 'period-alone', rule: rulePeriodAlone },
2752
+ { name: 'now', rule: ruleNow },
2708
2753
  ];
2754
+ export const EN_RULES = EN_RULE_ENTRIES.map((e) => e.rule);
@@ -0,0 +1,9 @@
1
+ import type { Rule } from './en.js';
2
+ import { type LatinLexicon } from './latin.js';
3
+ export declare const ES_LEXICON: LatinLexicon;
4
+ export declare const ES_RULE_ENTRIES: {
5
+ name: string;
6
+ rule: Rule;
7
+ }[];
8
+ /** Connector words allowed between merged date & time parts. */
9
+ export declare const ES_CONNECTORS: string[];