@omgbase/core 0.21.0 → 0.24.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.
- package/dist/src/core/ids.d.ts +1 -1
- package/dist/src/core/read/document.d.ts +4 -1
- package/dist/src/core/read/document.js +18 -6
- package/dist/src/core/store/archive.d.ts +196 -0
- package/dist/src/core/store/archive.js +606 -0
- package/dist/src/core/store/gc.d.ts +25 -4
- package/dist/src/core/store/gc.js +43 -28
- package/dist/src/core/store/rebuild.d.ts +2 -0
- package/dist/src/core/store/rebuild.js +8 -2
- package/dist/src/core/store/retention.d.ts +138 -0
- package/dist/src/core/store/retention.js +409 -0
- package/dist/src/core/store/schema.d.ts +3 -2
- package/dist/src/core/store/schema.js +30 -4
- package/dist/src/core/store/size.d.ts +21 -2
- package/dist/src/core/store/size.js +24 -3
- package/dist/src/core/store/store.d.ts +1 -0
- package/dist/src/core/store/store.js +53 -1
- package/dist/src/graph/history.d.ts +2 -0
- package/dist/src/graph/history.js +8 -4
- package/dist/src/index.d.ts +3 -1
- package/dist/src/index.js +3 -0
- package/dist/src/mcp/errors.d.ts +1 -1
- package/dist/src/mcp/errors.js +4 -0
- package/dist/src/mcp/reference.d.ts +1 -1
- package/dist/src/mcp/reference.js +49 -11
- package/dist/src/mcp/server.js +2 -2
- package/dist/src/mutate/select.js +5 -1
- package/dist/src/oqx-js/context.js +39 -8
- package/dist/src/oqx-js/dates.d.ts +105 -0
- package/dist/src/oqx-js/dates.js +427 -0
- package/dist/src/oqx-js/names.js +14 -3
- package/dist/src/oqx-js/planner.js +21 -2
- package/dist/src/oqx-js/run.d.ts +1 -0
- package/dist/src/oqx-js/run.js +82 -2
- package/dist/src/oqx-js/sql/translate.js +12 -5
- package/dist/src/oqx-js/store-index.js +1 -1
- package/dist/src/spec-versions.d.ts +4 -4
- package/dist/src/spec-versions.js +3 -3
- package/package.json +2 -2
|
@@ -23,6 +23,7 @@ import { sanitizeFtsQuery } from "../search/fts-query.js";
|
|
|
23
23
|
import { cosineBytes } from "../core/vec.js";
|
|
24
24
|
import { ctxHashHex } from "../search/embeddings.js";
|
|
25
25
|
import { makeBlockContextResolver } from "../search/tasks.js";
|
|
26
|
+
import { dateFunction, dateMethod } from "./dates.js";
|
|
26
27
|
const TARGET = Symbol("oqx.target");
|
|
27
28
|
const ROOT_ROW = Symbol("oqx.rootRow");
|
|
28
29
|
const PROP_SOURCE = Symbol("oqx.propSource");
|
|
@@ -44,7 +45,9 @@ function tagAll(rows, t) {
|
|
|
44
45
|
// (`children`, `blocks`, `out_links`, `doc`, `frontmatter`, …) always followed:
|
|
45
46
|
// a bare name that is an engine attribute reads the engine's value, never the
|
|
46
47
|
// author's. The attributes: every row's `id`/`path`; docs' and blocks' `raw`/
|
|
47
|
-
// `hash`/`updated_at
|
|
48
|
+
// `hash`/`updated_at`/`created_at` (4.10: the commit `ts` of the thing's first
|
|
49
|
+
// revision / earliest `block_changes` row — "first observed by omgbase", not a
|
|
50
|
+
// filesystem time); docs' `title` (the computed title) and `tags` (authored
|
|
48
51
|
// tags ∪ body hashtags) and `format`; a foreign key named for what it is
|
|
49
52
|
// (`doc_id`, `block_id`, `via_id`, `dst_id`, `dst_path`, `uri`), a column its
|
|
50
53
|
// column (`type`, `text`, `depth`, `ordinal`, `kind`, `predicate`, `field`).
|
|
@@ -59,14 +62,16 @@ function tagAll(rows, t) {
|
|
|
59
62
|
// target name — `docs`, `links`, … — is a loud error of its own (root-row.ts).
|
|
60
63
|
// `revisions` on a document is a RELATION (its revision list, §1.2, 4.7) in
|
|
61
64
|
// `rel.docs` below, and names.ts's `RELATIONS.docs` knows it as one.
|
|
62
|
-
const DOC_ATTRIBUTES = new Set(["id", "path", "raw", "hash", "updated_at", "title", "tags", "format"]);
|
|
63
|
-
const BLOCK_ATTRIBUTES = new Set(["id", "path", "raw", "hash", "updated_at", "doc_id", "type", "text", "attrs", "depth", "ordinal"]);
|
|
65
|
+
const DOC_ATTRIBUTES = new Set(["id", "path", "raw", "hash", "updated_at", "created_at", "title", "tags", "format"]);
|
|
66
|
+
const BLOCK_ATTRIBUTES = new Set(["id", "path", "raw", "hash", "updated_at", "created_at", "doc_id", "type", "text", "attrs", "depth", "ordinal"]);
|
|
64
67
|
const NODE_ATTRIBUTES = new Set(["id", "path", "doc_id", "block_id", "kind", "name", "value", "attrs"]);
|
|
65
68
|
const EDGE_ATTRIBUTES = new Set(["id", "path", "doc_id", "block_id", "via_id", "dst_id", "dst_path", "uri", "field", "dst_kind", "predicate", "provenance", "anchor"]);
|
|
66
69
|
// The history target (spec/surface §1.2, 4.7): a revision row's own columns
|
|
67
70
|
// under the names `docs_history` already uses (`rev`, `seq`, `commit`, `origin`,
|
|
68
|
-
// `actor`, `hash`), the commit's `ts` and `reason`, and
|
|
69
|
-
|
|
71
|
+
// `actor`, `hash`), the commit's `ts` and `reason`, `is_current`, and (4.9)
|
|
72
|
+
// `archived` — the revision's bodies live in an archive segment (spec/store §7
|
|
73
|
+
// "Retention"; `docs_read { at }` reads through it when attached).
|
|
74
|
+
const REVISION_ATTRIBUTES = new Set(["id", "path", "rev", "seq", "doc_id", "commit", "ts", "origin", "actor", "reason", "hash", "is_current", "archived"]);
|
|
70
75
|
/** The engine attributes per kind (spec/surface §1.2 table). Exported so the
|
|
71
76
|
* translator's tests pin the two against each other. */
|
|
72
77
|
export const ATTRIBUTES = {
|
|
@@ -78,7 +83,7 @@ export const ATTRIBUTES = {
|
|
|
78
83
|
// `revisions` table carries no `repo_id`, so the repository guard joins `docs`.
|
|
79
84
|
// `is_current` is `docs_history`'s `isCurrent`: the revision is the document's
|
|
80
85
|
// `current_rev`.
|
|
81
|
-
const REVISION_COLS = "r.rev_id, r.doc_id, r.seq, r.rendered_hash, r.path, r.commit_id, c.ts, c.origin, c.actor, c.reason, (d.current_rev = r.rev_id) AS is_current";
|
|
86
|
+
const REVISION_COLS = "r.rev_id, r.doc_id, r.seq, r.rendered_hash, r.path, r.commit_id, c.ts, c.origin, c.actor, c.reason, (d.current_rev = r.rev_id) AS is_current, (r.archive IS NOT NULL) AS archived";
|
|
82
87
|
const REVISION_FROM = "revisions r JOIN docs d ON d.doc_id = r.doc_id JOIN commits c ON c.commit_id = r.commit_id";
|
|
83
88
|
/** The source handles of a document (§1.2): `data` is the merged authored view
|
|
84
89
|
* (frontmatter + inline), the other two one source each. */
|
|
@@ -461,6 +466,12 @@ export function makeStoreContext(store, repoId, opts = {}) {
|
|
|
461
466
|
case "path": return referencePath(String(row.path));
|
|
462
467
|
case "hash": return row.file_hash == null ? null : hex(row.file_hash);
|
|
463
468
|
case "updated_at": return scalar(`SELECT c.ts FROM revisions r JOIN commits c ON c.commit_id = r.commit_id WHERE r.rev_id = ?`, row.current_rev) ?? null;
|
|
469
|
+
// `created_at` (4.10): the commit `ts` of the document's first revision
|
|
470
|
+
// (`seq = 1`, the row `revisions first { ts values }` reads) — when
|
|
471
|
+
// omgbase first observed the document, not a filesystem time. The
|
|
472
|
+
// retention squash (spec/store §7.1, 15.0) keeps every revision and
|
|
473
|
+
// commit row, so the value survives archiving.
|
|
474
|
+
case "created_at": return scalar(`SELECT c.ts FROM revisions r JOIN commits c ON c.commit_id = r.commit_id WHERE r.doc_id = ? AND r.seq = 1`, row.doc_id) ?? null;
|
|
464
475
|
// `raw`: the exact file bytes (spec/store §6.1, frontmatter included).
|
|
465
476
|
case "raw": return docsRead(store, String(row.doc_id))?.raw ?? null;
|
|
466
477
|
// `title`: the computed title (spec/properties §3.3 — the authored
|
|
@@ -484,6 +495,13 @@ export function makeStoreContext(store, repoId, opts = {}) {
|
|
|
484
495
|
case "raw": return row.raw_hash == null ? null : scalar(`SELECT bytes FROM blobs WHERE hash = ?`, row.raw_hash)?.toString("utf8") ?? null;
|
|
485
496
|
case "hash": return row.raw_hash == null ? null : hex(row.raw_hash);
|
|
486
497
|
case "updated_at": return scalar(`SELECT MAX(c.ts) FROM block_changes bc JOIN commits c ON c.commit_id = bc.commit_id WHERE bc.block_id = ?`, row.block_id) ?? null;
|
|
498
|
+
// `created_at` (4.10): the EARLIEST commit `ts` among the block's
|
|
499
|
+
// `block_changes` — the same join as `updated_at` under MIN: the commit
|
|
500
|
+
// that first wrote the id (`inserted`, or the split/merge lineage row
|
|
501
|
+
// that minted it). Store 14.2 keeps that row and the 15.0 squash keeps
|
|
502
|
+
// each block's earliest row (spec/store §7.1), so it survives archiving;
|
|
503
|
+
// a resurrected block keeps its original creation.
|
|
504
|
+
case "created_at": return scalar(`SELECT MIN(c.ts) FROM block_changes bc JOIN commits c ON c.commit_id = bc.commit_id WHERE bc.block_id = ?`, row.block_id) ?? null;
|
|
487
505
|
case "doc_id": return row.doc_id;
|
|
488
506
|
case "type": return row.type;
|
|
489
507
|
case "text": return row.text;
|
|
@@ -537,6 +555,7 @@ export function makeStoreContext(store, repoId, opts = {}) {
|
|
|
537
555
|
case "reason": return row.reason ?? null;
|
|
538
556
|
case "hash": return hex(row.rendered_hash);
|
|
539
557
|
case "is_current": return row.is_current === 1 || row.is_current === true;
|
|
558
|
+
case "archived": return row.archived === 1 || row.archived === true;
|
|
540
559
|
}
|
|
541
560
|
return undefined;
|
|
542
561
|
};
|
|
@@ -888,6 +907,13 @@ export function makeStoreContext(store, repoId, opts = {}) {
|
|
|
888
907
|
}
|
|
889
908
|
if (name === "refs")
|
|
890
909
|
return { handled: true, value: refs(args[0]) };
|
|
910
|
+
// `date(x)` / `dur(x)` (spec/surface §1.3 "Dates and durations", 4.8):
|
|
911
|
+
// pure over their argument except the named points, which read the host
|
|
912
|
+
// clock (pinned by the spec clock seam). run.ts folds a literal argument
|
|
913
|
+
// before planning, so the arm here serves the non-literal ones.
|
|
914
|
+
const dated = dateFunction(name, args);
|
|
915
|
+
if (dated.handled)
|
|
916
|
+
return dated;
|
|
891
917
|
if (name === "range") {
|
|
892
918
|
const s = args[0];
|
|
893
919
|
if (semantics.isRange(s))
|
|
@@ -898,12 +924,17 @@ export function makeStoreContext(store, repoId, opts = {}) {
|
|
|
898
924
|
const fn = semantics.BUILTIN_FUNCTIONS[name];
|
|
899
925
|
return fn ? { handled: true, value: fn(args) } : { handled: false };
|
|
900
926
|
},
|
|
901
|
-
// Domain methods first
|
|
902
|
-
//
|
|
927
|
+
// Domain methods first, then the date methods (`plus`/`minus`/`until`/the
|
|
928
|
+
// fields/`format` on a date string, `dur_text`/`dur_iso` on a number —
|
|
929
|
+
// §1.3 4.8; total: any other receiver is absent); fall back to oqx-js
|
|
930
|
+
// builtin methods (contains/startsWith/endsWith/matches/size/lower/upper).
|
|
903
931
|
callMethod(name, recv, args) {
|
|
904
932
|
const r = method(name, recv, args);
|
|
905
933
|
if (r.handled)
|
|
906
934
|
return r;
|
|
935
|
+
const dated = dateMethod(name, recv, args);
|
|
936
|
+
if (dated.handled)
|
|
937
|
+
return dated;
|
|
907
938
|
const fn = semantics.BUILTIN_METHODS[name];
|
|
908
939
|
return fn ? { handled: true, value: fn(recv, args) } : { handled: false };
|
|
909
940
|
},
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { CallResult } from "@omgbase/oqx";
|
|
2
|
+
/** A parsed date: the instant, the offset it was WRITTEN in (minutes east of
|
|
3
|
+
* UTC; `0` for `Z` and for a date-only value), whether it is date-only, and
|
|
4
|
+
* whether it carried a fraction of a second as written (kept on render). */
|
|
5
|
+
export interface DateValue {
|
|
6
|
+
ms: number;
|
|
7
|
+
offset: number;
|
|
8
|
+
dateOnly: boolean;
|
|
9
|
+
fraction: boolean;
|
|
10
|
+
}
|
|
11
|
+
/** Parse an ISO-8601 date / datetime string; `null` for anything else (a
|
|
12
|
+
* non-string, a malformed string, an out-of-range field such as month 13 or
|
|
13
|
+
* hour 24). */
|
|
14
|
+
export declare function parseDate(x: unknown): DateValue | null;
|
|
15
|
+
interface Fields {
|
|
16
|
+
year: number;
|
|
17
|
+
month: number;
|
|
18
|
+
day: number;
|
|
19
|
+
hour: number;
|
|
20
|
+
minute: number;
|
|
21
|
+
second: number;
|
|
22
|
+
millis: number;
|
|
23
|
+
weekday: number;
|
|
24
|
+
}
|
|
25
|
+
/** The fields of the value in the offset it was written in (`weekday` 1 = Monday … 7 = Sunday). */
|
|
26
|
+
export declare function fieldsOf(d: DateValue): Fields;
|
|
27
|
+
/** The canonical string: `YYYY-MM-DD` for a date-only value; otherwise
|
|
28
|
+
* `YYYY-MM-DDThh:mm:ss` + `.fff` when the value carries a fraction (as written,
|
|
29
|
+
* or because arithmetic produced one) + `Z` for UTC or the written `±hh:mm`. */
|
|
30
|
+
export declare function renderDate(d: DateValue): string | null;
|
|
31
|
+
/** `date(x)` on a string that is not a named point: the canonical form, or null. */
|
|
32
|
+
export declare function canonicalDate(x: unknown): string | null;
|
|
33
|
+
/** The named points `date()` resolves against the clock. */
|
|
34
|
+
export declare const NAMED_POINTS: ReadonlySet<string>;
|
|
35
|
+
/** Resolve a named point at the instant `nowMs` (UTC): `now` is the instant
|
|
36
|
+
* with milliseconds (the form the store stamps commits in); the others are
|
|
37
|
+
* date-only — `sow`/`eow` the Monday/Sunday of the current week, `som`/`eom`
|
|
38
|
+
* the first/last day of the month, `soy`/`eoy` of the year. Null for a name
|
|
39
|
+
* that is not a point. */
|
|
40
|
+
export declare function namedPoint(name: string, nowMs: number): DateValue | null;
|
|
41
|
+
/** `date(x)`: a named point against the clock, else the canonical ISO form, else null. */
|
|
42
|
+
export declare function dateOf(x: unknown, nowMs?: number): string | null;
|
|
43
|
+
/** A parsed duration: the calendar counts `years` and `months` kept apart from
|
|
44
|
+
* the fixed-length `seconds`, so `plus` can be calendar-aware while `dur()`
|
|
45
|
+
* applies the 365-day / 30-day conventions. */
|
|
46
|
+
export interface Duration {
|
|
47
|
+
years: number;
|
|
48
|
+
months: number;
|
|
49
|
+
seconds: number;
|
|
50
|
+
}
|
|
51
|
+
/** Days per month / year under the fixed conventions `dur()` applies. */
|
|
52
|
+
export declare const MONTH_SECONDS: number;
|
|
53
|
+
export declare const YEAR_SECONDS: number;
|
|
54
|
+
/** Parse a duration string: Dataview's `<number> <unit>` pairs, each pair
|
|
55
|
+
* separated from the next by spaces, a comma, or nothing (`"1h30m"`, `"2 days,
|
|
56
|
+
* 3 hours"`; a trailing separator is malformed), units case-insensitive:
|
|
57
|
+
* y|yr|yrs|year|years, mo|mos|month|months, w|wk|wks|week|weeks, d|day|days,
|
|
58
|
+
* h|hr|hrs|hour|hours, m|min|mins|minute|minutes, s|sec|secs|second|seconds —
|
|
59
|
+
* or an ISO-8601 duration (`P1DT2H`, `PT90M`, `P2W`). Null when it is neither. */
|
|
60
|
+
export declare function parseDuration(x: unknown): Duration | null;
|
|
61
|
+
/** A duration's length in seconds under the conventions: a year is 365 days, a month 30. */
|
|
62
|
+
export declare function durationSeconds(d: Duration): number;
|
|
63
|
+
/** `dur(x)`: a number is that many seconds; a string is parsed; else null. */
|
|
64
|
+
export declare function durOf(x: unknown): number | null;
|
|
65
|
+
/** Shift a date by a whole number of calendar `months` (the month field moves
|
|
66
|
+
* and the day is clamped to the new month's length) and then by `seconds`. A
|
|
67
|
+
* date-only value stays date-only when the seconds are a whole number of days;
|
|
68
|
+
* otherwise it becomes a datetime in its offset (UTC for a date-only receiver). */
|
|
69
|
+
export declare function shiftDate(d: DateValue, months: number, seconds: number): DateValue;
|
|
70
|
+
/** A duration as `plus` applies it: the WHOLE years and months are calendar
|
|
71
|
+
* months; a fractional year or month count is not calendar — its fraction is
|
|
72
|
+
* 365-day / 30-day seconds — and joins the fixed-length seconds. */
|
|
73
|
+
export declare function calendarParts(dur: Duration): {
|
|
74
|
+
months: number;
|
|
75
|
+
seconds: number;
|
|
76
|
+
};
|
|
77
|
+
/** `d.plus(x)` (`sign` 1) / `d.minus(x)` (`sign` -1). */
|
|
78
|
+
export declare function plusMinus(recv: unknown, arg: unknown, sign: 1 | -1): string | null;
|
|
79
|
+
/** `a.until(b)`: seconds from `a` to `b` (negative when `b` is earlier), by instant. */
|
|
80
|
+
export declare function until(a: unknown, b: unknown): number | null;
|
|
81
|
+
/** `d.format(pattern)`: `yyyy` year, `MM` month, `dd` day, `HH` hour, `mm`
|
|
82
|
+
* minute, `ss` second (two digits each), `MMM` the English month abbreviation
|
|
83
|
+
* (`Jan`), `EEE` the English weekday abbreviation (`Mon`); every other
|
|
84
|
+
* character is literal. Fields are read in the written offset; a date-only
|
|
85
|
+
* value has `00` time fields. */
|
|
86
|
+
export declare function formatDate(recv: unknown, pattern: unknown): string | null;
|
|
87
|
+
/** `n.dur_text()`: `"2d 3h"`, `"1h 30m"`, `"45s"` — days, hours, minutes,
|
|
88
|
+
* seconds, zero parts omitted, `"0s"` for zero, a leading `-` for a negative
|
|
89
|
+
* duration, a fractional second kept (`"1.5s"`). Null for a non-number. */
|
|
90
|
+
export declare function durText(n: unknown): string | null;
|
|
91
|
+
/** `n.dur_iso()`: the ISO-8601 duration `P[nD][T[nH][nM][nS]]` — `PT1H30M`,
|
|
92
|
+
* `P2DT3H`, `PT45S`, `PT0S` for zero, a leading `-` for a negative duration.
|
|
93
|
+
* Null for a non-number. */
|
|
94
|
+
export declare function durIso(n: unknown): string | null;
|
|
95
|
+
/** The free functions and their arity. */
|
|
96
|
+
export declare const DATE_FUNCTIONS: Readonly<Record<string, number>>;
|
|
97
|
+
/** The methods and their arity: on a date string, and (`dur_text`/`dur_iso`) on a number. */
|
|
98
|
+
export declare const DATE_METHODS: Readonly<Record<string, number>>;
|
|
99
|
+
/** `callFunction` arm: `date(x)` / `dur(x)`; `{ handled: false }` for any other name. */
|
|
100
|
+
export declare function dateFunction(name: string, args: unknown[], nowMs?: number): CallResult;
|
|
101
|
+
/** `callMethod` arm: the date methods on a string receiver and the duration
|
|
102
|
+
* renderers on a number; a receiver of any other kind yields absent (the
|
|
103
|
+
* methods are total). `{ handled: false }` for any other name. */
|
|
104
|
+
export declare function dateMethod(name: string, recv: unknown, args: unknown[]): CallResult;
|
|
105
|
+
export {};
|
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
// Dates and durations for the query binding (spec/surface §1.3 "Dates and
|
|
2
|
+
// durations", 4.8): the Dataview-parity function set, served as omgbase HOST
|
|
3
|
+
// functions through `@omgbase/oqx`'s `callFunction` / `callMethod` seam.
|
|
4
|
+
//
|
|
5
|
+
// Two value conventions and no new value kinds:
|
|
6
|
+
// • a DATE is an ISO-8601 string — `YYYY-MM-DD` (date-only) or
|
|
7
|
+
// `YYYY-MM-DDThh:mm:ss[.fff](Z|±hh:mm)` — exactly what the store writes for
|
|
8
|
+
// `updated_at`/`ts` and what OQX already orders chronologically by code
|
|
9
|
+
// point (spec/oqx SEMANTICS §6);
|
|
10
|
+
// • a DURATION is a number of seconds.
|
|
11
|
+
// Arithmetic between them is METHODS (`d.plus(s)`, `a.until(b)`), never the
|
|
12
|
+
// `+`/`-` operators: `+` with a string operand concatenates (SEMANTICS §7), so
|
|
13
|
+
// `"2026-01-05" + 86400` is `"2026-01-0586400"` and must stay so.
|
|
14
|
+
//
|
|
15
|
+
// Everything is total: a value that is not a date (or a duration) yields
|
|
16
|
+
// absent, so a `where` over mixed data filters quietly. Only a wrong ARITY is
|
|
17
|
+
// an error (`filter_invalid`, the surface's eval error). The one impure input
|
|
18
|
+
// — the named points `today`, `now`, `sow`… — reads the HOST clock, UTC, through
|
|
19
|
+
// `Date.now()`, which the spec clock seam (`core/clock.ts`, `OMGBASE_SPEC_CLOCK`)
|
|
20
|
+
// pins; the language itself has no clock (the fixtures run under the corpus
|
|
21
|
+
// clock, so `date("today")` is `2026-09-27` there).
|
|
22
|
+
//
|
|
23
|
+
// The pure half (parse / render / shift / until / fields / format / the duration
|
|
24
|
+
// grammar) is exported for the unit tests and the Rust twin's reading; the two
|
|
25
|
+
// `CallResult` arms at the bottom are what context.ts and run.ts's constant fold
|
|
26
|
+
// call.
|
|
27
|
+
import { FilterInvalid } from "../search/cel/parser.js";
|
|
28
|
+
const DAY_MS = 86_400_000;
|
|
29
|
+
// `YYYY-MM-DD`, optionally `T`/`t`/space + `hh:mm[:ss[.fff…]]` + optional offset
|
|
30
|
+
// (`Z`, `±hh:mm`, `±hhmm`); a time without an offset is UTC.
|
|
31
|
+
const ISO_RE = /^(\d{4})-(\d{2})-(\d{2})(?:[Tt ](\d{2}):(\d{2})(?::(\d{2})(?:[.,](\d{1,9}))?)?(Z|z|[+-]\d{2}:?\d{2})?)?$/;
|
|
32
|
+
function daysInMonth(year, month) {
|
|
33
|
+
// month 1..12; day 0 of the next month is the last day of this one.
|
|
34
|
+
return new Date(Date.UTC(year, month, 0)).getUTCDate();
|
|
35
|
+
}
|
|
36
|
+
/** Parse an ISO-8601 date / datetime string; `null` for anything else (a
|
|
37
|
+
* non-string, a malformed string, an out-of-range field such as month 13 or
|
|
38
|
+
* hour 24). */
|
|
39
|
+
export function parseDate(x) {
|
|
40
|
+
if (typeof x !== "string")
|
|
41
|
+
return null;
|
|
42
|
+
const m = ISO_RE.exec(x.trim());
|
|
43
|
+
if (!m)
|
|
44
|
+
return null;
|
|
45
|
+
const year = Number(m[1]), month = Number(m[2]), day = Number(m[3]);
|
|
46
|
+
if (month < 1 || month > 12 || day < 1 || day > daysInMonth(year, month))
|
|
47
|
+
return null;
|
|
48
|
+
if (m[4] === undefined) {
|
|
49
|
+
return { ms: Date.UTC(year, month - 1, day), offset: 0, dateOnly: true, fraction: false };
|
|
50
|
+
}
|
|
51
|
+
const hour = Number(m[4]), minute = Number(m[5]), second = m[6] === undefined ? 0 : Number(m[6]);
|
|
52
|
+
if (hour > 23 || minute > 59 || second > 59)
|
|
53
|
+
return null;
|
|
54
|
+
const fraction = m[7] !== undefined;
|
|
55
|
+
const millis = fraction ? Number(m[7].padEnd(3, "0").slice(0, 3)) : 0;
|
|
56
|
+
let offset = 0;
|
|
57
|
+
const z = m[8];
|
|
58
|
+
if (z !== undefined && z !== "Z" && z !== "z") {
|
|
59
|
+
const sign = z[0] === "-" ? -1 : 1;
|
|
60
|
+
const digits = z.slice(1).replace(":", "");
|
|
61
|
+
const oh = Number(digits.slice(0, 2)), om = Number(digits.slice(2, 4));
|
|
62
|
+
if (oh > 23 || om > 59)
|
|
63
|
+
return null;
|
|
64
|
+
offset = sign * (oh * 60 + om);
|
|
65
|
+
}
|
|
66
|
+
const local = Date.UTC(year, month - 1, day, hour, minute, second, millis);
|
|
67
|
+
return { ms: local - offset * 60_000, offset, dateOnly: false, fraction };
|
|
68
|
+
}
|
|
69
|
+
/** The fields of the value in the offset it was written in (`weekday` 1 = Monday … 7 = Sunday). */
|
|
70
|
+
export function fieldsOf(d) {
|
|
71
|
+
const t = new Date(d.ms + d.offset * 60_000);
|
|
72
|
+
return {
|
|
73
|
+
year: t.getUTCFullYear(), month: t.getUTCMonth() + 1, day: t.getUTCDate(),
|
|
74
|
+
hour: t.getUTCHours(), minute: t.getUTCMinutes(), second: t.getUTCSeconds(), millis: t.getUTCMilliseconds(),
|
|
75
|
+
weekday: ((t.getUTCDay() + 6) % 7) + 1,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
const pad = (n, w) => String(n).padStart(w, "0");
|
|
79
|
+
/** The canonical string: `YYYY-MM-DD` for a date-only value; otherwise
|
|
80
|
+
* `YYYY-MM-DDThh:mm:ss` + `.fff` when the value carries a fraction (as written,
|
|
81
|
+
* or because arithmetic produced one) + `Z` for UTC or the written `±hh:mm`. */
|
|
82
|
+
export function renderDate(d) {
|
|
83
|
+
if (!Number.isFinite(d.ms))
|
|
84
|
+
return null;
|
|
85
|
+
const f = fieldsOf(d);
|
|
86
|
+
const ymd = `${pad(f.year, 4)}-${pad(f.month, 2)}-${pad(f.day, 2)}`;
|
|
87
|
+
if (d.dateOnly)
|
|
88
|
+
return ymd;
|
|
89
|
+
let s = `${ymd}T${pad(f.hour, 2)}:${pad(f.minute, 2)}:${pad(f.second, 2)}`;
|
|
90
|
+
if (d.fraction || f.millis !== 0)
|
|
91
|
+
s += `.${pad(f.millis, 3)}`;
|
|
92
|
+
if (d.offset === 0)
|
|
93
|
+
return `${s}Z`;
|
|
94
|
+
const a = Math.abs(d.offset);
|
|
95
|
+
return `${s}${d.offset < 0 ? "-" : "+"}${pad(Math.floor(a / 60), 2)}:${pad(a % 60, 2)}`;
|
|
96
|
+
}
|
|
97
|
+
/** `date(x)` on a string that is not a named point: the canonical form, or null. */
|
|
98
|
+
export function canonicalDate(x) {
|
|
99
|
+
const d = parseDate(x);
|
|
100
|
+
return d === null ? null : renderDate(d);
|
|
101
|
+
}
|
|
102
|
+
// ---- named points (the host clock, UTC) --------------------------------------------
|
|
103
|
+
/** The named points `date()` resolves against the clock. */
|
|
104
|
+
export const NAMED_POINTS = new Set(["now", "today", "yesterday", "tomorrow", "sow", "eow", "som", "eom", "soy", "eoy"]);
|
|
105
|
+
const dateOnly = (ms) => ({ ms, offset: 0, dateOnly: true, fraction: false });
|
|
106
|
+
/** Resolve a named point at the instant `nowMs` (UTC): `now` is the instant
|
|
107
|
+
* with milliseconds (the form the store stamps commits in); the others are
|
|
108
|
+
* date-only — `sow`/`eow` the Monday/Sunday of the current week, `som`/`eom`
|
|
109
|
+
* the first/last day of the month, `soy`/`eoy` of the year. Null for a name
|
|
110
|
+
* that is not a point. */
|
|
111
|
+
export function namedPoint(name, nowMs) {
|
|
112
|
+
const t = new Date(nowMs);
|
|
113
|
+
const y = t.getUTCFullYear(), mo = t.getUTCMonth(), d = t.getUTCDate();
|
|
114
|
+
const today = Date.UTC(y, mo, d);
|
|
115
|
+
switch (name) {
|
|
116
|
+
case "now": return { ms: nowMs, offset: 0, dateOnly: false, fraction: true };
|
|
117
|
+
case "today": return dateOnly(today);
|
|
118
|
+
case "yesterday": return dateOnly(today - DAY_MS);
|
|
119
|
+
case "tomorrow": return dateOnly(today + DAY_MS);
|
|
120
|
+
case "sow": return dateOnly(today - ((t.getUTCDay() + 6) % 7) * DAY_MS);
|
|
121
|
+
case "eow": return dateOnly(today + (6 - (t.getUTCDay() + 6) % 7) * DAY_MS);
|
|
122
|
+
case "som": return dateOnly(Date.UTC(y, mo, 1));
|
|
123
|
+
case "eom": return dateOnly(Date.UTC(y, mo + 1, 0));
|
|
124
|
+
case "soy": return dateOnly(Date.UTC(y, 0, 1));
|
|
125
|
+
case "eoy": return dateOnly(Date.UTC(y, 11, 31));
|
|
126
|
+
default: return null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/** `date(x)`: a named point against the clock, else the canonical ISO form, else null. */
|
|
130
|
+
export function dateOf(x, nowMs = Date.now()) {
|
|
131
|
+
if (typeof x !== "string")
|
|
132
|
+
return null;
|
|
133
|
+
const key = x.trim().toLowerCase();
|
|
134
|
+
if (NAMED_POINTS.has(key))
|
|
135
|
+
return renderDate(namedPoint(key, nowMs));
|
|
136
|
+
return canonicalDate(x);
|
|
137
|
+
}
|
|
138
|
+
const UNIT_SECONDS = {
|
|
139
|
+
w: 604_800, wk: 604_800, wks: 604_800, week: 604_800, weeks: 604_800,
|
|
140
|
+
d: 86_400, day: 86_400, days: 86_400,
|
|
141
|
+
h: 3600, hr: 3600, hrs: 3600, hour: 3600, hours: 3600,
|
|
142
|
+
m: 60, min: 60, mins: 60, minute: 60, minutes: 60,
|
|
143
|
+
s: 1, sec: 1, secs: 1, second: 1, seconds: 1,
|
|
144
|
+
};
|
|
145
|
+
const UNIT_YEARS = new Set(["y", "yr", "yrs", "year", "years"]);
|
|
146
|
+
const UNIT_MONTHS = new Set(["mo", "mos", "month", "months"]);
|
|
147
|
+
/** Days per month / year under the fixed conventions `dur()` applies. */
|
|
148
|
+
export const MONTH_SECONDS = 30 * 86_400;
|
|
149
|
+
export const YEAR_SECONDS = 365 * 86_400;
|
|
150
|
+
const NUM = String.raw `(\d+(?:\.\d+)?)`;
|
|
151
|
+
const ISO_DUR_RE = new RegExp(`^([+-])?P(?:${NUM}Y)?(?:${NUM}M)?(?:${NUM}W)?(?:${NUM}D)?(?:T(?:${NUM}H)?(?:${NUM}M)?(?:${NUM}S)?)?$`, "i");
|
|
152
|
+
const PAIR_RE = /^(-?\d+(?:\.\d+)?)\s*([a-zA-Z]+)/;
|
|
153
|
+
/** Parse a duration string: Dataview's `<number> <unit>` pairs, each pair
|
|
154
|
+
* separated from the next by spaces, a comma, or nothing (`"1h30m"`, `"2 days,
|
|
155
|
+
* 3 hours"`; a trailing separator is malformed), units case-insensitive:
|
|
156
|
+
* y|yr|yrs|year|years, mo|mos|month|months, w|wk|wks|week|weeks, d|day|days,
|
|
157
|
+
* h|hr|hrs|hour|hours, m|min|mins|minute|minutes, s|sec|secs|second|seconds —
|
|
158
|
+
* or an ISO-8601 duration (`P1DT2H`, `PT90M`, `P2W`). Null when it is neither. */
|
|
159
|
+
export function parseDuration(x) {
|
|
160
|
+
if (typeof x !== "string")
|
|
161
|
+
return null;
|
|
162
|
+
const s = x.trim();
|
|
163
|
+
if (s === "")
|
|
164
|
+
return null;
|
|
165
|
+
const iso = ISO_DUR_RE.exec(s);
|
|
166
|
+
if (iso) {
|
|
167
|
+
const [, sign, y, mo, w, d, h, mi, sec] = iso;
|
|
168
|
+
if ([y, mo, w, d, h, mi, sec].every((v) => v === undefined))
|
|
169
|
+
return null;
|
|
170
|
+
const n = (v) => (v === undefined ? 0 : Number(v));
|
|
171
|
+
const k = sign === "-" ? -1 : 1;
|
|
172
|
+
return {
|
|
173
|
+
years: k * n(y),
|
|
174
|
+
months: k * n(mo),
|
|
175
|
+
seconds: k * (n(w) * 604_800 + n(d) * 86_400 + n(h) * 3600 + n(mi) * 60 + n(sec)),
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
let rest = s;
|
|
179
|
+
let years = 0, months = 0, seconds = 0;
|
|
180
|
+
for (;;) {
|
|
181
|
+
const m = PAIR_RE.exec(rest);
|
|
182
|
+
if (!m)
|
|
183
|
+
return null;
|
|
184
|
+
const n = Number(m[1]);
|
|
185
|
+
const unit = m[2].toLowerCase();
|
|
186
|
+
if (Object.hasOwn(UNIT_SECONDS, unit))
|
|
187
|
+
seconds += n * UNIT_SECONDS[unit];
|
|
188
|
+
else if (UNIT_MONTHS.has(unit))
|
|
189
|
+
months += n;
|
|
190
|
+
else if (UNIT_YEARS.has(unit))
|
|
191
|
+
years += n;
|
|
192
|
+
else
|
|
193
|
+
return null;
|
|
194
|
+
rest = rest.slice(m[0].length);
|
|
195
|
+
if (rest === "")
|
|
196
|
+
break;
|
|
197
|
+
// the separator: spaces, one comma among them, or none at all (`1h30m`)
|
|
198
|
+
rest = rest.replace(/^\s*,?\s*/, "");
|
|
199
|
+
if (rest === "")
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
return { years, months, seconds };
|
|
203
|
+
}
|
|
204
|
+
/** A duration's length in seconds under the conventions: a year is 365 days, a month 30. */
|
|
205
|
+
export function durationSeconds(d) {
|
|
206
|
+
return d.years * YEAR_SECONDS + d.months * MONTH_SECONDS + d.seconds;
|
|
207
|
+
}
|
|
208
|
+
/** `dur(x)`: a number is that many seconds; a string is parsed; else null. */
|
|
209
|
+
export function durOf(x) {
|
|
210
|
+
if (typeof x === "number")
|
|
211
|
+
return Number.isFinite(x) ? x : null;
|
|
212
|
+
const d = parseDuration(x);
|
|
213
|
+
return d === null ? null : durationSeconds(d);
|
|
214
|
+
}
|
|
215
|
+
// ---- arithmetic -------------------------------------------------------------------
|
|
216
|
+
/** Shift a date by a whole number of calendar `months` (the month field moves
|
|
217
|
+
* and the day is clamped to the new month's length) and then by `seconds`. A
|
|
218
|
+
* date-only value stays date-only when the seconds are a whole number of days;
|
|
219
|
+
* otherwise it becomes a datetime in its offset (UTC for a date-only receiver). */
|
|
220
|
+
export function shiftDate(d, months, seconds) {
|
|
221
|
+
let ms = d.ms;
|
|
222
|
+
if (months !== 0) {
|
|
223
|
+
const f = fieldsOf(d);
|
|
224
|
+
const total = f.year * 12 + (f.month - 1) + months;
|
|
225
|
+
const year = Math.floor(total / 12), month = ((total % 12) + 12) % 12 + 1;
|
|
226
|
+
const day = Math.min(f.day, daysInMonth(year, month));
|
|
227
|
+
ms = Date.UTC(year, month - 1, day, f.hour, f.minute, f.second, f.millis) - d.offset * 60_000;
|
|
228
|
+
}
|
|
229
|
+
const deltaMs = Math.round(seconds * 1000);
|
|
230
|
+
ms += deltaMs;
|
|
231
|
+
const dateOnly = d.dateOnly && deltaMs % DAY_MS === 0;
|
|
232
|
+
return { ms, offset: d.offset, dateOnly, fraction: d.fraction };
|
|
233
|
+
}
|
|
234
|
+
/** The argument of `plus`/`minus` as a duration: a number of seconds (never
|
|
235
|
+
* calendar) or a duration string (months/years calendar-aware). Null otherwise. */
|
|
236
|
+
function durationArg(x) {
|
|
237
|
+
if (typeof x === "number")
|
|
238
|
+
return Number.isFinite(x) ? { years: 0, months: 0, seconds: x } : null;
|
|
239
|
+
return parseDuration(x);
|
|
240
|
+
}
|
|
241
|
+
/** A duration as `plus` applies it: the WHOLE years and months are calendar
|
|
242
|
+
* months; a fractional year or month count is not calendar — its fraction is
|
|
243
|
+
* 365-day / 30-day seconds — and joins the fixed-length seconds. */
|
|
244
|
+
export function calendarParts(dur) {
|
|
245
|
+
const wy = Math.trunc(dur.years), wm = Math.trunc(dur.months);
|
|
246
|
+
return {
|
|
247
|
+
months: wy * 12 + wm,
|
|
248
|
+
seconds: dur.seconds + (dur.years - wy) * YEAR_SECONDS + (dur.months - wm) * MONTH_SECONDS,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
/** `d.plus(x)` (`sign` 1) / `d.minus(x)` (`sign` -1). */
|
|
252
|
+
export function plusMinus(recv, arg, sign) {
|
|
253
|
+
const d = parseDate(recv);
|
|
254
|
+
const dur = durationArg(arg);
|
|
255
|
+
if (d === null || dur === null)
|
|
256
|
+
return null;
|
|
257
|
+
const { months, seconds } = calendarParts(dur);
|
|
258
|
+
return renderDate(shiftDate(d, sign * months, sign * seconds));
|
|
259
|
+
}
|
|
260
|
+
/** `a.until(b)`: seconds from `a` to `b` (negative when `b` is earlier), by instant. */
|
|
261
|
+
export function until(a, b) {
|
|
262
|
+
const da = parseDate(a), db = parseDate(b);
|
|
263
|
+
if (da === null || db === null)
|
|
264
|
+
return null;
|
|
265
|
+
return (db.ms - da.ms) / 1000;
|
|
266
|
+
}
|
|
267
|
+
// ---- format -------------------------------------------------------------------------
|
|
268
|
+
const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
269
|
+
const WEEKDAYS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
|
|
270
|
+
/** The `format` tokens, longest first (a scan takes the longest token at each position). */
|
|
271
|
+
const FORMAT_TOKENS = ["yyyy", "MMM", "EEE", "MM", "dd", "HH", "mm", "ss"];
|
|
272
|
+
/** `d.format(pattern)`: `yyyy` year, `MM` month, `dd` day, `HH` hour, `mm`
|
|
273
|
+
* minute, `ss` second (two digits each), `MMM` the English month abbreviation
|
|
274
|
+
* (`Jan`), `EEE` the English weekday abbreviation (`Mon`); every other
|
|
275
|
+
* character is literal. Fields are read in the written offset; a date-only
|
|
276
|
+
* value has `00` time fields. */
|
|
277
|
+
export function formatDate(recv, pattern) {
|
|
278
|
+
const d = parseDate(recv);
|
|
279
|
+
if (d === null || typeof pattern !== "string")
|
|
280
|
+
return null;
|
|
281
|
+
const f = fieldsOf(d);
|
|
282
|
+
let out = "";
|
|
283
|
+
let i = 0;
|
|
284
|
+
while (i < pattern.length) {
|
|
285
|
+
const tok = FORMAT_TOKENS.find((t) => pattern.startsWith(t, i));
|
|
286
|
+
if (tok === undefined) {
|
|
287
|
+
out += pattern[i];
|
|
288
|
+
i++;
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
switch (tok) {
|
|
292
|
+
case "yyyy":
|
|
293
|
+
out += pad(f.year, 4);
|
|
294
|
+
break;
|
|
295
|
+
case "MMM":
|
|
296
|
+
out += MONTHS[f.month - 1];
|
|
297
|
+
break;
|
|
298
|
+
case "EEE":
|
|
299
|
+
out += WEEKDAYS[f.weekday - 1];
|
|
300
|
+
break;
|
|
301
|
+
case "MM":
|
|
302
|
+
out += pad(f.month, 2);
|
|
303
|
+
break;
|
|
304
|
+
case "dd":
|
|
305
|
+
out += pad(f.day, 2);
|
|
306
|
+
break;
|
|
307
|
+
case "HH":
|
|
308
|
+
out += pad(f.hour, 2);
|
|
309
|
+
break;
|
|
310
|
+
case "mm":
|
|
311
|
+
out += pad(f.minute, 2);
|
|
312
|
+
break;
|
|
313
|
+
case "ss":
|
|
314
|
+
out += pad(f.second, 2);
|
|
315
|
+
break;
|
|
316
|
+
}
|
|
317
|
+
i += tok.length;
|
|
318
|
+
}
|
|
319
|
+
return out;
|
|
320
|
+
}
|
|
321
|
+
// ---- duration rendering -----------------------------------------------------------------
|
|
322
|
+
function splitSeconds(n) {
|
|
323
|
+
const neg = n < 0;
|
|
324
|
+
const a = Math.round(Math.abs(n) * 1000) / 1000;
|
|
325
|
+
const d = Math.floor(a / 86_400);
|
|
326
|
+
const h = Math.floor((a % 86_400) / 3600);
|
|
327
|
+
const m = Math.floor((a % 3600) / 60);
|
|
328
|
+
const s = Math.round((a % 60) * 1000) / 1000;
|
|
329
|
+
return { neg, d, h, m, s };
|
|
330
|
+
}
|
|
331
|
+
/** `n.dur_text()`: `"2d 3h"`, `"1h 30m"`, `"45s"` — days, hours, minutes,
|
|
332
|
+
* seconds, zero parts omitted, `"0s"` for zero, a leading `-` for a negative
|
|
333
|
+
* duration, a fractional second kept (`"1.5s"`). Null for a non-number. */
|
|
334
|
+
export function durText(n) {
|
|
335
|
+
if (typeof n !== "number" || !Number.isFinite(n))
|
|
336
|
+
return null;
|
|
337
|
+
const { neg, d, h, m, s } = splitSeconds(n);
|
|
338
|
+
const parts = [];
|
|
339
|
+
if (d)
|
|
340
|
+
parts.push(`${d}d`);
|
|
341
|
+
if (h)
|
|
342
|
+
parts.push(`${h}h`);
|
|
343
|
+
if (m)
|
|
344
|
+
parts.push(`${m}m`);
|
|
345
|
+
if (s)
|
|
346
|
+
parts.push(`${s}s`);
|
|
347
|
+
if (parts.length === 0)
|
|
348
|
+
return "0s";
|
|
349
|
+
return (neg ? "-" : "") + parts.join(" ");
|
|
350
|
+
}
|
|
351
|
+
/** `n.dur_iso()`: the ISO-8601 duration `P[nD][T[nH][nM][nS]]` — `PT1H30M`,
|
|
352
|
+
* `P2DT3H`, `PT45S`, `PT0S` for zero, a leading `-` for a negative duration.
|
|
353
|
+
* Null for a non-number. */
|
|
354
|
+
export function durIso(n) {
|
|
355
|
+
if (typeof n !== "number" || !Number.isFinite(n))
|
|
356
|
+
return null;
|
|
357
|
+
const { neg, d, h, m, s } = splitSeconds(n);
|
|
358
|
+
let out = "P";
|
|
359
|
+
if (d)
|
|
360
|
+
out += `${d}D`;
|
|
361
|
+
if (h || m || s) {
|
|
362
|
+
out += "T";
|
|
363
|
+
if (h)
|
|
364
|
+
out += `${h}H`;
|
|
365
|
+
if (m)
|
|
366
|
+
out += `${m}M`;
|
|
367
|
+
if (s)
|
|
368
|
+
out += `${s}S`;
|
|
369
|
+
}
|
|
370
|
+
if (out === "P")
|
|
371
|
+
out = "PT0S";
|
|
372
|
+
return (neg ? "-" : "") + out;
|
|
373
|
+
}
|
|
374
|
+
// ---- the OQX arms ---------------------------------------------------------------------
|
|
375
|
+
/** The free functions and their arity. */
|
|
376
|
+
export const DATE_FUNCTIONS = { date: 1, dur: 1 };
|
|
377
|
+
/** The methods and their arity: on a date string, and (`dur_text`/`dur_iso`) on a number. */
|
|
378
|
+
export const DATE_METHODS = {
|
|
379
|
+
plus: 1, minus: 1, until: 1, format: 1,
|
|
380
|
+
year: 0, month: 0, day: 0, weekday: 0, hour: 0, minute: 0, second: 0, date: 0,
|
|
381
|
+
dur_text: 0, dur_iso: 0,
|
|
382
|
+
};
|
|
383
|
+
function arity(spelling, want, got) {
|
|
384
|
+
if (got !== want)
|
|
385
|
+
throw new FilterInvalid(`${spelling} takes exactly ${want} argument${want === 1 ? "" : "s"}, got ${got}`, "OQX");
|
|
386
|
+
}
|
|
387
|
+
/** `callFunction` arm: `date(x)` / `dur(x)`; `{ handled: false }` for any other name. */
|
|
388
|
+
export function dateFunction(name, args, nowMs = Date.now()) {
|
|
389
|
+
if (!Object.hasOwn(DATE_FUNCTIONS, name))
|
|
390
|
+
return { handled: false };
|
|
391
|
+
arity(`${name}()`, DATE_FUNCTIONS[name], args.length);
|
|
392
|
+
return { handled: true, value: name === "date" ? dateOf(args[0], nowMs) : durOf(args[0]) };
|
|
393
|
+
}
|
|
394
|
+
/** `callMethod` arm: the date methods on a string receiver and the duration
|
|
395
|
+
* renderers on a number; a receiver of any other kind yields absent (the
|
|
396
|
+
* methods are total). `{ handled: false }` for any other name. */
|
|
397
|
+
export function dateMethod(name, recv, args) {
|
|
398
|
+
if (!Object.hasOwn(DATE_METHODS, name))
|
|
399
|
+
return { handled: false };
|
|
400
|
+
arity(`.${name}()`, DATE_METHODS[name], args.length);
|
|
401
|
+
const field = (k) => {
|
|
402
|
+
const d = parseDate(recv);
|
|
403
|
+
return d === null ? null : fieldsOf(d)[k];
|
|
404
|
+
};
|
|
405
|
+
switch (name) {
|
|
406
|
+
case "plus": return { handled: true, value: plusMinus(recv, args[0], 1) };
|
|
407
|
+
case "minus": return { handled: true, value: plusMinus(recv, args[0], -1) };
|
|
408
|
+
case "until": return { handled: true, value: until(recv, args[0]) };
|
|
409
|
+
case "format": return { handled: true, value: formatDate(recv, args[0]) };
|
|
410
|
+
case "year":
|
|
411
|
+
case "month":
|
|
412
|
+
case "day":
|
|
413
|
+
case "weekday":
|
|
414
|
+
case "hour":
|
|
415
|
+
case "minute":
|
|
416
|
+
case "second":
|
|
417
|
+
return { handled: true, value: field(name) };
|
|
418
|
+
case "date": {
|
|
419
|
+
const d = parseDate(recv);
|
|
420
|
+
return { handled: true, value: d === null ? null : renderDate({ ...d, dateOnly: true }) };
|
|
421
|
+
}
|
|
422
|
+
case "dur_text": return { handled: true, value: durText(recv) };
|
|
423
|
+
case "dur_iso": return { handled: true, value: durIso(recv) };
|
|
424
|
+
default: return { handled: false };
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
//# sourceMappingURL=dates.js.map
|