@omgbase/core 0.19.0 → 0.23.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/ingest.d.ts +2 -0
- package/dist/src/core/ingest.js +13 -0
- package/dist/src/core/read/document.d.ts +13 -9
- package/dist/src/core/read/document.js +18 -8
- 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 +37 -4
- package/dist/src/core/store/gc.js +62 -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 +24 -2
- package/dist/src/core/store/size.js +33 -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/arguments.d.ts +10 -2
- package/dist/src/mcp/arguments.js +37 -3
- 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 +58 -8
- package/dist/src/mcp/server.js +51 -24
- package/dist/src/mutate/effects.d.ts +23 -5
- package/dist/src/mutate/effects.js +111 -22
- package/dist/src/mutate/known-ids.d.ts +19 -0
- package/dist/src/mutate/known-ids.js +47 -7
- package/dist/src/mutate/ops.d.ts +1 -0
- package/dist/src/mutate/ops.js +20 -1
- package/dist/src/mutate/select.d.ts +0 -11
- package/dist/src/mutate/select.js +33 -9
- package/dist/src/oqx-js/context.d.ts +9 -2
- package/dist/src/oqx-js/context.js +92 -30
- 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.d.ts +40 -0
- package/dist/src/oqx-js/names.js +358 -0
- package/dist/src/oqx-js/planner.d.ts +3 -3
- package/dist/src/oqx-js/planner.js +30 -11
- package/dist/src/oqx-js/root-row.d.ts +15 -4
- package/dist/src/oqx-js/root-row.js +18 -10
- package/dist/src/oqx-js/run.d.ts +19 -1
- package/dist/src/oqx-js/run.js +97 -13
- package/dist/src/oqx-js/sql/translate.js +1 -1
- package/dist/src/spec-versions.d.ts +5 -5
- package/dist/src/spec-versions.js +4 -4
- package/dist/src/sync/observe.js +9 -3
- package/package.json +2 -2
|
@@ -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
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Query, ReferenceStep } from "@omgbase/oqx";
|
|
2
|
+
import { type Kind } from "./context.js";
|
|
3
|
+
import type { Store } from "../core/store/store.js";
|
|
4
|
+
/** What to do with a tier-3 name on an open kind: record it in `hints` (every read) or refuse the query (a write selector). */
|
|
5
|
+
export type UnknownNames = "hint" | "refuse";
|
|
6
|
+
/** The relations of each kind and the kind of the rows they yield (spec/surface §1.2). */
|
|
7
|
+
export declare const RELATIONS: Readonly<Record<Kind, Readonly<Record<string, Kind>>>>;
|
|
8
|
+
/** The self and reach-through handles of each kind and the kind of the row they reach (§1.2). */
|
|
9
|
+
export declare const HANDLES: Readonly<Record<Kind, Readonly<Record<string, Kind>>>>;
|
|
10
|
+
/** The vocabulary of a kind in a repository (cached on the head commit; the SQL runs only when a name is not intrinsic). */
|
|
11
|
+
export declare function vocabulary(store: Store, repoId: string, target: Kind): {
|
|
12
|
+
present: boolean;
|
|
13
|
+
keys: readonly string[];
|
|
14
|
+
};
|
|
15
|
+
/** The kind of the rows a scope holds, from its row chain (AST.md §5b); null when the chain reaches a value no kind describes. */
|
|
16
|
+
export declare function kindOfScope(steps: readonly ReferenceStep[]): Kind | null;
|
|
17
|
+
/** Optimal string alignment distance (Damerau–Levenshtein with adjacent transpositions, each edit 1). */
|
|
18
|
+
export declare function editDistance(a: string, b: string): number;
|
|
19
|
+
/**
|
|
20
|
+
* The nearest match of `name` among `candidates` (§1.5): case-insensitive; a
|
|
21
|
+
* candidate qualifies when its distance is ≤ 2, or when one of the two is a
|
|
22
|
+
* substring of the other and the shorter has at least three characters; the
|
|
23
|
+
* lowest distance wins, ties broken by the shorter candidate, then
|
|
24
|
+
* alphabetically. Null when nothing qualifies.
|
|
25
|
+
*/
|
|
26
|
+
export declare function nearest(name: string, candidates: readonly string[]): string | null;
|
|
27
|
+
/** The error of a closed kind: `` `target` is not a field of links (fields: id, path, …); did you mean `…`? ``. */
|
|
28
|
+
export declare function fieldErrorMessage(store: Store, repoId: string, target: Kind, name: string): string;
|
|
29
|
+
/** The hint of an open kind: `` `preview` is not a field of blocks and no block in this repository carries a property by that name; did you mean `…`? ``.
|
|
30
|
+
* Under a source handle the name is an authored lookup by construction, so the
|
|
31
|
+
* hint says only that: `` no document in this repository carries a property `path` (`data.path`); did you mean `…`? ``,
|
|
32
|
+
* the nearest match drawn from the authored keys alone. */
|
|
33
|
+
export declare function propertyHintMessage(store: Store, repoId: string, target: Kind, name: string, handle?: string): string;
|
|
34
|
+
/**
|
|
35
|
+
* Judge every name the query reads. Returns the hints (one per offending name
|
|
36
|
+
* of an open kind, first-reference order, deduplicated) under `"hint"`; throws
|
|
37
|
+
* `FilterInvalid` for a tier-3 name on a closed kind in either mode, and for
|
|
38
|
+
* one on an open kind under `"refuse"`.
|
|
39
|
+
*/
|
|
40
|
+
export declare function checkNames(store: Store, repoId: string, raw: Query, mode: UnknownNames): string[];
|