nl2time 0.3.1 → 0.3.3
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 +2 -2
- package/dist/engine/resolve.js +6 -1
- package/dist/parse/en.js +30 -11
- package/package.json +6 -5
package/README.md
CHANGED
|
@@ -120,11 +120,11 @@ Every imported conformance case carries machine-readable provenance (upstream pr
|
|
|
120
120
|
pip install nl2time
|
|
121
121
|
```
|
|
122
122
|
|
|
123
|
-
The Python engine port lives in [`python/`](python/) (`whenever`-based time model): IR validation, `TimeContext`, and the full deterministic resolver at **100% bit-exact parity** with the JS reference across all 2,
|
|
123
|
+
The Python engine port lives in [`python/`](python/) (`whenever`-based time model): IR validation, `TimeContext`, and the full deterministic resolver at **100% bit-exact parity** with the JS reference across all 2,790 engine-parity fixtures (`corpus/ir/`), enforced in CI on every push. Parsers and `describe()` are JS-only so far — [docs/porting.md](docs/porting.md) has the strategy and the divergence gates.
|
|
124
124
|
|
|
125
125
|
## Release history
|
|
126
126
|
|
|
127
|
-
See **[CHANGELOG.md](CHANGELOG.md)** and [GitHub releases](https://github.com/AndyFooBlah/nl2time/releases). Current: v0.3.
|
|
127
|
+
See **[CHANGELOG.md](CHANGELOG.md)** and [GitHub releases](https://github.com/AndyFooBlah/nl2time/releases). Current: v0.3.3 (JS) · py-v0.1.2 (Python engine). Releases publish to npm/PyPI automatically via OIDC trusted publishing — see [docs/RELEASING.md](docs/RELEASING.md).
|
|
128
128
|
|
|
129
129
|
## Development
|
|
130
130
|
|
package/dist/engine/resolve.js
CHANGED
|
@@ -179,7 +179,12 @@ function evalExprInner(expr, ctx, anchor) {
|
|
|
179
179
|
if (e.type !== 'interval')
|
|
180
180
|
continue;
|
|
181
181
|
const isPoint = Temporal_compare(e.zi.start, e.zi.end) === 0;
|
|
182
|
-
|
|
182
|
+
// Clock-time semantics need a fine grain AND a sub-day extent: a
|
|
183
|
+
// multi-day interval whose edges are snap points ("end of year" =
|
|
184
|
+
// Jul 1 → Jan 1) carries grain 'instant' but is a calendar period,
|
|
185
|
+
// not a time of day.
|
|
186
|
+
const timeGrained = GRAIN_ORDER.indexOf(e.zi.grain) < GRAIN_ORDER.indexOf('day') &&
|
|
187
|
+
(isPoint || Temporal_compare(e.zi.start.add({ days: 1 }), e.zi.end) > 0);
|
|
183
188
|
// Day-grain-or-coarser ends are conversationally inclusive:
|
|
184
189
|
// "between July 4th and July 10th" covers the 10th. Emit the
|
|
185
190
|
// inclusive reading (end operand contributes its END) first, with
|
package/dist/parse/en.js
CHANGED
|
@@ -612,9 +612,9 @@ function buildRange(a, b, scope, period, connWord) {
|
|
|
612
612
|
}
|
|
613
613
|
/**
|
|
614
614
|
* Open-range connectors (#18): "since X" → [start(X), now]; "until X" /
|
|
615
|
-
* "till X" / "through X" / "up to X" / "by X" (deadline) →
|
|
616
|
-
* These only fire when the connector opens the range —
|
|
617
|
-
* "from A through B" are consumed earlier by the range rule.
|
|
615
|
+
* "till X" / "through X" / "up to X" / "by X" / "before X" (deadline) →
|
|
616
|
+
* [now, end(X)]. These only fire when the connector opens the range —
|
|
617
|
+
* "A until B" and "from A through B" are consumed earlier by the range rule.
|
|
618
618
|
*/
|
|
619
619
|
const ruleOpenRange = (tokens, i, ctx) => {
|
|
620
620
|
const w = word(tokens[i]);
|
|
@@ -622,7 +622,7 @@ const ruleOpenRange = (tokens, i, ctx) => {
|
|
|
622
622
|
let at = i + 1;
|
|
623
623
|
if (w === 'since')
|
|
624
624
|
mode = 'since';
|
|
625
|
-
else if (w === 'until' || w === 'till' || w === 'through' || w === 'thru' || w === 'by') {
|
|
625
|
+
else if (w === 'until' || w === 'till' || w === 'through' || w === 'thru' || w === 'by' || w === 'before') {
|
|
626
626
|
mode = 'until';
|
|
627
627
|
}
|
|
628
628
|
else if (w === 'up' && word(tokens[i + 1]) === 'to') {
|
|
@@ -631,10 +631,13 @@ const ruleOpenRange = (tokens, i, ctx) => {
|
|
|
631
631
|
}
|
|
632
632
|
if (!mode)
|
|
633
633
|
return undefined;
|
|
634
|
-
// "by the 15th": a bare article only before an
|
|
635
|
-
//
|
|
636
|
-
|
|
634
|
+
// "by the 15th" / "by the end of the month": a bare article only before an
|
|
635
|
+
// explicit calendar date or an edge phrase (#29) — NOT "through the
|
|
636
|
+
// week"-style prose, which stays unmatched.
|
|
637
|
+
if (word(tokens[at]) === 'the' &&
|
|
638
|
+
(ruleCalendarDate(tokens, at + 1, ctx) || ruleEndOf(tokens, at + 1, ctx))) {
|
|
637
639
|
at += 1;
|
|
640
|
+
}
|
|
638
641
|
const inner = ruleDeicticDay(tokens, at, ctx) ??
|
|
639
642
|
ruleLastThisNext(tokens, at, ctx) ??
|
|
640
643
|
ruleHoliday(tokens, at, ctx) ??
|
|
@@ -646,13 +649,29 @@ const ruleOpenRange = (tokens, i, ctx) => {
|
|
|
646
649
|
ruleWeekdayAlone(tokens, at, ctx);
|
|
647
650
|
if (!inner || inner.role === 'duration')
|
|
648
651
|
return undefined;
|
|
649
|
-
// Boundary points ("by EOD"
|
|
650
|
-
//
|
|
652
|
+
// Boundary points ("by EOD") keep their point reading — the deadline
|
|
653
|
+
// instant is the answer there, not a range.
|
|
651
654
|
if (inner.expr.op === 'snap' && inner.expr.edge !== undefined)
|
|
652
655
|
return undefined;
|
|
656
|
+
// Late-part edge phrases ("end of month/year/quarter/week") parse as the
|
|
657
|
+
// period's late part — between(offset(start), end). As a deadline the
|
|
658
|
+
// answer is the period's END point, not the whole late part: using the
|
|
659
|
+
// interval would make the engine emit the inclusive/exclusive pair (#17)
|
|
660
|
+
// as a spurious second candidate whenever `now` precedes the late-part
|
|
661
|
+
// start (#29). Symmetrically, "since the beginning of the month" opens at
|
|
662
|
+
// the period's START point.
|
|
663
|
+
let target = inner.expr;
|
|
664
|
+
if (target.op === 'between') {
|
|
665
|
+
if (mode === 'until' && target.end.op === 'snap' && target.end.edge === 'end') {
|
|
666
|
+
target = target.end;
|
|
667
|
+
}
|
|
668
|
+
else if (mode === 'since' && target.start.op === 'snap' && target.start.edge === 'start') {
|
|
669
|
+
target = target.start;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
653
672
|
const expr = mode === 'since'
|
|
654
|
-
? { op: 'between', start:
|
|
655
|
-
: { op: 'between', start: NOW, end:
|
|
673
|
+
? { op: 'between', start: target, end: NOW }
|
|
674
|
+
: { op: 'between', start: NOW, end: target };
|
|
656
675
|
return { expr, consumed: at + inner.consumed - i, confidence: 0.9, role: 'datetime' };
|
|
657
676
|
};
|
|
658
677
|
/** "week of April 10th", "the first/last week of July", "the week starting on Feb 4". */
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nl2time",
|
|
3
|
-
"version": "0.3.
|
|
4
|
-
"description": "Bidirectional natural language
|
|
3
|
+
"version": "0.3.3",
|
|
4
|
+
"description": "Bidirectional natural language ⇄ date/time: parse expressions like 'last week' into concrete intervals, and describe instants as '9pm last night' — deterministic core, optional LLM assist.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./dist/index.js",
|
|
@@ -32,7 +32,8 @@
|
|
|
32
32
|
"typecheck": "tsc --noEmit",
|
|
33
33
|
"import:recognizers": "node scripts/import-recognizers.mjs",
|
|
34
34
|
"baselines": "npm run build && node scripts/update-baselines.mjs",
|
|
35
|
-
"eval": "npm run build && node scripts/eval.mjs"
|
|
35
|
+
"eval": "npm run build && node scripts/eval.mjs",
|
|
36
|
+
"fixtures": "npm run build && node scripts/generate-ir-fixtures.mjs"
|
|
36
37
|
},
|
|
37
38
|
"keywords": [
|
|
38
39
|
"date",
|
|
@@ -55,6 +56,6 @@
|
|
|
55
56
|
"vitest": "^3.0.0"
|
|
56
57
|
},
|
|
57
58
|
"engines": {
|
|
58
|
-
"node": ">=
|
|
59
|
+
"node": ">=22"
|
|
59
60
|
}
|
|
60
|
-
}
|
|
61
|
+
}
|