@jarenjs/json 0.46.4 → 0.49.2
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/ARCHITECTURE.md +1 -1
- package/README.md +47 -4
- package/dist/types/query/operators.d.ts +42 -0
- package/dist/types/query/series.d.ts +164 -0
- package/docs/QUERY-FORMAT.md +172 -4
- package/package.json +2 -2
- package/schemas/jaren-jslt.draft-07.schema.json +236 -0
- package/schemas/jaren-jslt.llm-profile.schema.json +229 -0
- package/schemas/jaren-jslt.schema.json +236 -0
- package/schemas/jaren-query.draft-07.schema.json +236 -0
- package/schemas/jaren-query.llm-profile.schema.json +229 -0
- package/schemas/jaren-query.schema.json +122 -2
- package/src/query/normalize.js +44 -1
- package/src/query/operators.js +332 -3
- package/src/query/series.js +415 -0
package/ARCHITECTURE.md
CHANGED
|
@@ -163,7 +163,7 @@ Grouping, `$distinct` and the `$orderby` machinery need a total, deterministic e
|
|
|
163
163
|
|
|
164
164
|
### The operator registry
|
|
165
165
|
|
|
166
|
-
All
|
|
166
|
+
All 104 §8 operators live in one table in `operators.js` — the query-language analogue of `path.js`'s `FUNCTIONS` table. A test derives the count from the registry and asserts it against QUERY-FORMAT §8 *and* against every committed document that states a number — this file, the README twice, `site.md` and `@jarenjs/linq`'s architecture — so an operator cannot be added without moving all of them in the same change:
|
|
167
167
|
|
|
168
168
|
```javascript
|
|
169
169
|
'$substring': {
|
package/README.md
CHANGED
|
@@ -386,13 +386,13 @@ queryJson({
|
|
|
386
386
|
}, [1, 2, 3, 4, 5]); // [2, 3, 4] — a 3-point moving average
|
|
387
387
|
```
|
|
388
388
|
|
|
389
|
-
The operator library (
|
|
389
|
+
The operator library (104 operators: comparisons, IEEE-double arithmetic, logic, strings with I-Regexp `$match`/`$search`/`$replace`, aggregates, sequence tools like `$distinct`/`$subsequence`/`$range`, type predicates and casts, `$coalesce`, the RFC 3339 date family, the spatial family, `$similarity` and the time-series family) is cataloged in [QUERY-FORMAT.md §8](./docs/QUERY-FORMAT.md#8-operators).
|
|
390
390
|
|
|
391
|
-
**Extending the vocabulary (host opt-in).** The
|
|
391
|
+
**Extending the vocabulary (host opt-in).** The 104 are closed, but a host can add more the way `@jarenjs/validate` gains formats from `@jarenjs/formats`: `createJsltRegistry().use(mathPack).use(financePack)` composes packs of pure `@jarenjs/core` functions into a compiler, so `{ "$sqrt": "$.variance" }` and `{ "$npv": ["$.rate", "$.cashflows[*]"] }` work in a stylesheet, a bare query, and `@jarenjs/linq` — while a document compiled *without* the registry still rejects them. Aggregators fold a `seq` operand to an array before the pure call. See [JSLT-FORMAT.md §13](./docs/JSLT-FORMAT.md#13-registered-operators-host-opt-in-non-normative).
|
|
392
392
|
|
|
393
393
|
**Dates are RFC 3339 strings** ([§8.13](./docs/QUERY-FORMAT.md#813-dates-and-times)): `$is-date`/`$is-time`/`$is-datetime`/`$is-duration` test the lexical forms, `$year`…`$seconds` and `$offset` read components *lexically, in the value's own offset* (so "group by month" means what you expect), `$week`/`$week-year`/`$quarter`/`$weekday` add the derived calendar fields, and `$epoch`/`$datetime` convert to and from epoch milliseconds — the one place a value is shifted to UTC, and therefore the way to compare instants across offsets. There is deliberately no `current-dateTime`: a compiled query is cached by document identity and saved as a rule, so it must answer the same for the same input forever.
|
|
394
394
|
|
|
395
|
-
Dates also *move*. `$date-add`/`$date-sub` shift by an ISO 8601 duration or by an amount and a unit, `$start-of`/`$end-of` truncate to a calendar unit, `$date-diff` counts whole units, and `$date-format` renders through a Unicode LDML pattern compiled once with the query.
|
|
395
|
+
Dates also *move*. `$date-add`/`$date-sub` shift by an ISO 8601 duration or by an amount and a unit, `$start-of`/`$end-of` truncate to a calendar unit, `$date-diff` counts whole units, and `$date-format` renders through a Unicode LDML pattern compiled once with the query. Three rules keep it predictable: the **lexical form is preserved** (a date stays a date, and a date-time keeps its own offset instead of being normalized to UTC); **month arithmetic clamps**, so 31 January plus a month is 28 February — with `$date-diff` counting to match, so adding its answer back never overshoots; and **an operation needing a half the value has not got is refused**, so adding hours to a date, or asking a time for its end of month, is a `JQ2001` rather than an invented answer. Fractions apply where they convert exactly: `P1.5D` is thirty-six hours on a date-time, half a month is refused.
|
|
396
396
|
|
|
397
397
|
```js
|
|
398
398
|
queryJson({
|
|
@@ -406,7 +406,7 @@ queryJson({
|
|
|
406
406
|
|
|
407
407
|
Bucketing by week is the shape components alone cannot express, because a week boundary is arithmetic rather than a field. Patterns stay locale-independent — `MMMM` and `EEEE` are rejected rather than silently rendered in English — because localized text belongs to the presentation layer, not to a query.
|
|
408
408
|
|
|
409
|
-
**Geography is GeoJSON** ([§8.14](./docs/QUERY-FORMAT.md#814-spatial)), for the same reason dates are RFC 3339 strings: it is what the document already holds. Thirteen of the
|
|
409
|
+
**Geography is GeoJSON** ([§8.14](./docs/QUERY-FORMAT.md#814-spatial)), for the same reason dates are RFC 3339 strings: it is what the document already holds. Thirteen of the 104 operators are spatial — eight measurements and predicates, five conversions — and JSLT and JTLT inherit every one. Operands are a bare `[longitude, latitude]` position, a geometry, a `Feature` or a `FeatureCollection`, and wrappers unwrap for you. `$distance`, `$area` and `$length` answer in metres on the WGS 84 sphere — never planar, because a Euclidean answer over raw degrees is wrong by two thirds over a kilometre at Dutch latitudes. `$within` tests containment, `$bbox`/`$centroid` measure, and `$bbox-intersects` is named for exactly what it tests, since an `$intersects` that compared only boxes would be a lie the first time two L-shapes shared one.
|
|
410
410
|
|
|
411
411
|
```js
|
|
412
412
|
queryJson({
|
|
@@ -481,6 +481,49 @@ breaks ties by identity, so the same document answers with the same rows every
|
|
|
481
481
|
time it runs. `@jarenjs/linq` spells the whole chain fluently as
|
|
482
482
|
`.orderByDescending(m => m.embedding.similarity(q), { empty: 'least' }).take(10)`.
|
|
483
483
|
|
|
484
|
+
### Time series, in five operators
|
|
485
|
+
|
|
486
|
+
A series is what a document already holds when something has been measured
|
|
487
|
+
repeatedly — records with an instant and a reading — so five operators cover
|
|
488
|
+
the questions a `$for` phrase can ask but cannot answer in one pass
|
|
489
|
+
([§8.16](./docs/QUERY-FORMAT.md#816-time-series)):
|
|
490
|
+
|
|
491
|
+
```javascript
|
|
492
|
+
queryJson({ $resample: ['$.readings[*]',
|
|
493
|
+
{ every: 'PT1H', aggregate: 'mean', fill: 'linear' }] }, data);
|
|
494
|
+
// [{ at: 1767225600000, value: 4.5, count: 3600 }, …]
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
`$overlaps` tests two half-open `{start, end}` intervals (touching spans do
|
|
498
|
+
*not* overlap), `$time-bucket` labels the bucket an instant falls in,
|
|
499
|
+
`$resample` aggregates a series into buckets with an explicit fill policy,
|
|
500
|
+
`$rolling` aggregates over a window measured in **time** rather than in rows,
|
|
501
|
+
and `$asof` answers "what was current when this happened" for every left row.
|
|
502
|
+
Each is a call into [`@jarenjs/core/series`](../core/README.md) and nothing
|
|
503
|
+
else — the same kernel a chart and an indexed database read.
|
|
504
|
+
|
|
505
|
+
The second operand is a **verbatim literal**, not an expression, and that is
|
|
506
|
+
what makes it checkable: the width, the aggregate, the fill policy and the row
|
|
507
|
+
selectors are read once when the query compiles, so an unknown member, a bad
|
|
508
|
+
duration or a wildcard where a selector belongs is `JQ0003` with the near miss
|
|
509
|
+
named. Only what the *data* decides — a row that is not a sample, an instant
|
|
510
|
+
that names none — is `JQ2001`. A left row with no as-of match keeps
|
|
511
|
+
`right: null` and **stays in the answer**: no match is data.
|
|
512
|
+
|
|
513
|
+
```javascript
|
|
514
|
+
queryJson({ $asof: ['$.trades[*]', '$.quotes[*]',
|
|
515
|
+
{ by: '$.symbol', direction: 'backward', tolerance: 'PT1M' }] }, data);
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
Instants are epoch milliseconds on both sides; `$epoch` and `$datetime` are the
|
|
519
|
+
conversions, and a row that spells its instant elsewhere is read with a
|
|
520
|
+
selector (`{ at: '$.on' }`) rather than rewritten first. Calendar widths need a
|
|
521
|
+
wall clock: UTC and `{ offset }` need nothing, and a **named zone needs an
|
|
522
|
+
injected `zoneProvider`** — this suite bundles no time-zone database, and a
|
|
523
|
+
document that asked for one without it is refused rather than quietly answered
|
|
524
|
+
in UTC. Nothing here reads a clock: there is no `$now`, for the same reason
|
|
525
|
+
§8.13 has no `current-dateTime`.
|
|
526
|
+
|
|
484
527
|
### External parameters
|
|
485
528
|
|
|
486
529
|
A variable no phrase binds is an **external**: use is the declaration. The compiled query exposes the collected names and takes bindings at call time:
|
|
@@ -694,6 +694,48 @@ export declare const OPERATORS: Readonly<{
|
|
|
694
694
|
result: typeof resultEmptyPropagates;
|
|
695
695
|
compile: (gets: any, args: any) => (f: any) => string | typeof EMPTY;
|
|
696
696
|
};
|
|
697
|
+
$overlaps: {
|
|
698
|
+
params: Readonly<{
|
|
699
|
+
kinds: readonly string[];
|
|
700
|
+
min: 2;
|
|
701
|
+
}>;
|
|
702
|
+
result: (cards: any) => 1 | 2;
|
|
703
|
+
resultType: typeof RT_BOOLEAN;
|
|
704
|
+
compile: (gets: any, args: any) => (f: any) => boolean | typeof EMPTY;
|
|
705
|
+
};
|
|
706
|
+
'$time-bucket': {
|
|
707
|
+
params: Readonly<{
|
|
708
|
+
kinds: readonly string[];
|
|
709
|
+
min: 2;
|
|
710
|
+
}>;
|
|
711
|
+
result: (cards: any) => 1 | 2;
|
|
712
|
+
resultType: typeof RT_INTEGER;
|
|
713
|
+
compile: (gets: any, args: any, docPath: any, node: any) => (f: any) => any;
|
|
714
|
+
};
|
|
715
|
+
$resample: {
|
|
716
|
+
params: Readonly<{
|
|
717
|
+
kinds: readonly string[];
|
|
718
|
+
min: 2;
|
|
719
|
+
}>;
|
|
720
|
+
result: typeof RESULT_MANY;
|
|
721
|
+
compile: (gets: any, args: any, docPath: any, node: any) => (f: any) => any;
|
|
722
|
+
};
|
|
723
|
+
$rolling: {
|
|
724
|
+
params: Readonly<{
|
|
725
|
+
kinds: readonly string[];
|
|
726
|
+
min: 2;
|
|
727
|
+
}>;
|
|
728
|
+
result: typeof RESULT_MANY;
|
|
729
|
+
compile: (gets: any, args: any, docPath: any, node: any) => (f: any) => any;
|
|
730
|
+
};
|
|
731
|
+
$asof: {
|
|
732
|
+
params: Readonly<{
|
|
733
|
+
kinds: readonly string[];
|
|
734
|
+
min: 2;
|
|
735
|
+
}>;
|
|
736
|
+
result: typeof RESULT_MANY;
|
|
737
|
+
compile: (gets: any, args: any, docPath: any) => (f: any) => any;
|
|
738
|
+
};
|
|
697
739
|
}>;
|
|
698
740
|
declare function coalesceCard(cards: any): 0 | 1 | 2 | 3;
|
|
699
741
|
declare function compileCoalesce(gets: any): any;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { JsonQueryRuntimeError } from './errors.js';
|
|
2
|
+
/**
|
|
3
|
+
* The calendar context every clock-reading spec carries, and the one
|
|
4
|
+
* `$time-bucket` takes as its own literal. `zone` is an IANA name — only
|
|
5
|
+
* `'UTC'` resolves without a provider (D7: this suite bundles no tzdb) —
|
|
6
|
+
* `offset` is minutes east of UTC, and `disambiguation` says what a
|
|
7
|
+
* local time that happens twice, or never, resolves to.
|
|
8
|
+
*/
|
|
9
|
+
export declare const CLOCK_MEMBERS: readonly string[];
|
|
10
|
+
/** `$resample`: the D5 bucket contract, plus where a row keeps its members. */
|
|
11
|
+
export declare const RESAMPLE_MEMBERS: readonly string[];
|
|
12
|
+
/** `$rolling`: a window measured in time, and how much of one counts. */
|
|
13
|
+
export declare const ROLLING_MEMBERS: readonly string[];
|
|
14
|
+
/**
|
|
15
|
+
* `$asof`: which way to look, how far, and what makes two rows
|
|
16
|
+
* comparable. The one list NOT read from the kernel: `asOfJoin` takes
|
|
17
|
+
* nested `left`/`right` selector records, and a spec that stays one
|
|
18
|
+
* flat literal is what makes "compiled once" true — so the document
|
|
19
|
+
* spells `by`, `leftAt` and `rightAt`, and `compileAsOfSpec` below is
|
|
20
|
+
* the single place that translation happens.
|
|
21
|
+
*/
|
|
22
|
+
export declare const ASOF_MEMBERS: readonly string[];
|
|
23
|
+
/**
|
|
24
|
+
* One captured spec literal, confirmed to be an object naming only
|
|
25
|
+
* admitted members.
|
|
26
|
+
* @param {any} value - the raw node's frozen value
|
|
27
|
+
* @param {readonly string[]} allowed - the closed member list
|
|
28
|
+
* @param {string} operator - the operator's name, for the message
|
|
29
|
+
* @param {string} docPath - the spec argument's pointer
|
|
30
|
+
* @returns {any} the same object
|
|
31
|
+
* @throws {JsonQueryCompileError} JQ0003
|
|
32
|
+
*/
|
|
33
|
+
export declare function requireSpec(value: any, allowed: readonly string[], operator: string, docPath: string): any;
|
|
34
|
+
/**
|
|
35
|
+
* A spec member that must be one of a closed set of names.
|
|
36
|
+
* @param {any} value
|
|
37
|
+
* @param {readonly string[]} allowed
|
|
38
|
+
* @param {string} member
|
|
39
|
+
* @param {string} docPath
|
|
40
|
+
* @returns {string}
|
|
41
|
+
*/
|
|
42
|
+
export declare function requireEnum(value: any, allowed: readonly string[], member: string, docPath: string): string;
|
|
43
|
+
/**
|
|
44
|
+
* A spec member that must be a finite number.
|
|
45
|
+
* @param {any} value
|
|
46
|
+
* @param {string} member
|
|
47
|
+
* @param {string} docPath
|
|
48
|
+
* @returns {number}
|
|
49
|
+
*/
|
|
50
|
+
export declare function requireNumber(value: any, member: string, docPath: string): number;
|
|
51
|
+
/**
|
|
52
|
+
* A spec member naming an instant: epoch milliseconds or an RFC 3339
|
|
53
|
+
* string. The kernel converts it; this only refuses what is not one of
|
|
54
|
+
* the two spellings, so the message names the member rather than a row.
|
|
55
|
+
* @param {any} value
|
|
56
|
+
* @param {string} member
|
|
57
|
+
* @param {string} docPath
|
|
58
|
+
* @returns {number | string}
|
|
59
|
+
*/
|
|
60
|
+
export declare function requireInstant(value: any, member: string, docPath: string): number | string;
|
|
61
|
+
/**
|
|
62
|
+
* A spec member naming a span: a finite count of milliseconds or an ISO
|
|
63
|
+
* 8601 duration string. The kernel decides whether the duration is one
|
|
64
|
+
* it can walk; this refuses the shapes that are not spans at all.
|
|
65
|
+
* @param {any} value
|
|
66
|
+
* @param {string} member
|
|
67
|
+
* @param {string} docPath
|
|
68
|
+
* @returns {number | string}
|
|
69
|
+
*/
|
|
70
|
+
export declare function requireSpan(value: any, member: string, docPath: string): number | string;
|
|
71
|
+
/**
|
|
72
|
+
* A **row selector**: the query language's singular-path spelling, with
|
|
73
|
+
* `$` reading as the ROW rather than as the document — `'$.on'`,
|
|
74
|
+
* `'$[\'recorded at\']'`, `'$.meta.at'`. Compiled once into a direct
|
|
75
|
+
* property walk (`compileSingularGetter`), so nothing is parsed per row.
|
|
76
|
+
*
|
|
77
|
+
* A bare member name is the common case, and it is handed to the kernel
|
|
78
|
+
* as a name rather than as a closure: `canonicalSeries`'s no-copy fast
|
|
79
|
+
* path is only available to a series already spelled `at`/`value`, and
|
|
80
|
+
* a closure would take it away from every caller who did not need one.
|
|
81
|
+
*
|
|
82
|
+
* @param {any} value - the raw spec member
|
|
83
|
+
* @param {string} member - its name, for the message
|
|
84
|
+
* @param {string} docPath - the spec argument's pointer
|
|
85
|
+
* @returns {string | ((item: any) => any)} a member name, or a reader
|
|
86
|
+
* @throws {JsonQueryCompileError} JQ0003 for a non-singular or invalid path
|
|
87
|
+
*/
|
|
88
|
+
export declare function compileSelector(value: any, member: string, docPath: string): string | ((item: any) => any);
|
|
89
|
+
/**
|
|
90
|
+
* The wall clock a spec's calendar boundaries fall on, resolved once.
|
|
91
|
+
*
|
|
92
|
+
* UTC and a fixed offset need nothing. A named zone needs the tzdb this
|
|
93
|
+
* suite deliberately does not bundle (D7), and a JSON document cannot
|
|
94
|
+
* carry one — so the provider arrives through the compilation's
|
|
95
|
+
* `zoneProvider` option, and a named zone without one is a refusal
|
|
96
|
+
* naming the seam rather than a silent fall back to UTC that is right
|
|
97
|
+
* for eight months of the year.
|
|
98
|
+
*
|
|
99
|
+
* @param {any} spec - the captured spec (or calendar context)
|
|
100
|
+
* @param {any} provider - `options.zoneProvider`, or null
|
|
101
|
+
* @param {string} docPath
|
|
102
|
+
* @returns {{ zone?: string, offset?: number, provider?: any, disambiguation?: string }}
|
|
103
|
+
* the clock options the kernel takes
|
|
104
|
+
* @throws {JsonQueryCompileError} JQ0003
|
|
105
|
+
*/
|
|
106
|
+
export declare function compileClock(spec: any, provider: any, docPath: string): {
|
|
107
|
+
zone?: string;
|
|
108
|
+
offset?: number;
|
|
109
|
+
provider?: any;
|
|
110
|
+
disambiguation?: string;
|
|
111
|
+
};
|
|
112
|
+
/** The seven aggregates D5 fixes, for both `$resample` and `$rolling`. */
|
|
113
|
+
export declare const AGGREGATES: readonly string[];
|
|
114
|
+
/** The five fill policies D5 fixes. */
|
|
115
|
+
export declare const FILLS: readonly string[];
|
|
116
|
+
/** Which way an as-of join looks for its match. */
|
|
117
|
+
export declare const DIRECTIONS: readonly string[];
|
|
118
|
+
/**
|
|
119
|
+
* Copy the members a spec authored into the shape the kernel takes,
|
|
120
|
+
* compiling each through its own rule. Members absent from the document
|
|
121
|
+
* stay absent, so the kernel's own defaults are the only defaults.
|
|
122
|
+
* @param {any} spec - the captured literal
|
|
123
|
+
* @param {any} provider - `options.zoneProvider`, or null
|
|
124
|
+
* @param {string} docPath
|
|
125
|
+
* @param {Record<string, (value: any, member: string, docPath: string) => any>} rules
|
|
126
|
+
* @returns {any} the kernel spec
|
|
127
|
+
*/
|
|
128
|
+
export declare function buildKernelSpec(spec: any, provider: any, docPath: string, rules: Record<string, (value: any, member: string, docPath: string) => any>): any;
|
|
129
|
+
/**
|
|
130
|
+
* The rows a series operand carries.
|
|
131
|
+
*
|
|
132
|
+
* Both spellings a document actually has work, and neither is ambiguous
|
|
133
|
+
* because a sample is an object: a path that fans out (`$.rows[*]`)
|
|
134
|
+
* arrives as a sequence of rows, and a path that does not (`$.rows`)
|
|
135
|
+
* arrives as the one array item that holds them. The empty sequence is
|
|
136
|
+
* an empty series — no rows is data, not an error.
|
|
137
|
+
*
|
|
138
|
+
* @param {any} v - the evaluated operand
|
|
139
|
+
* @param {string} docPath
|
|
140
|
+
* @returns {any[]}
|
|
141
|
+
* @throws {JsonQueryRuntimeError} JQ2001 when it is not rows at all
|
|
142
|
+
*/
|
|
143
|
+
export declare function seriesArg(v: any, docPath: string): any[];
|
|
144
|
+
/**
|
|
145
|
+
* One half-open interval operand: a record with a `start` and an `end`.
|
|
146
|
+
* The kernel decides whether the bounds name instants and whether the
|
|
147
|
+
* span is a span; this only refuses what is not a record.
|
|
148
|
+
* @param {any} v
|
|
149
|
+
* @param {string} docPath
|
|
150
|
+
* @returns {any}
|
|
151
|
+
* @throws {JsonQueryRuntimeError} JQ2001
|
|
152
|
+
*/
|
|
153
|
+
export declare function intervalArg(v: any, docPath: string): any;
|
|
154
|
+
/**
|
|
155
|
+
* The kernel's refusals, in this language's vocabulary. Everything the
|
|
156
|
+
* temporal kernel throws is a `TypeError` about the DATA it was handed —
|
|
157
|
+
* a row that is not a sample, an instant that names none, a local time
|
|
158
|
+
* that never happened — which is exactly what `JQ2001` is for. A host
|
|
159
|
+
* failure that is not a `TypeError` is not laundered.
|
|
160
|
+
* @param {unknown} e
|
|
161
|
+
* @param {string} docPath
|
|
162
|
+
* @returns {JsonQueryRuntimeError}
|
|
163
|
+
*/
|
|
164
|
+
export declare function seriesRefusal(e: unknown, docPath: string): JsonQueryRuntimeError;
|
package/docs/QUERY-FORMAT.md
CHANGED
|
@@ -1233,7 +1233,7 @@ by an ISO 8601 duration (`[date, "P1M"]`) or by an amount and a unit
|
|
|
1233
1233
|
`hour`, `minute`, `second`, `millisecond` — so an unknown one is `JQ2001`
|
|
1234
1234
|
rather than a compile error.
|
|
1235
1235
|
|
|
1236
|
-
|
|
1236
|
+
Three rules make these predictable:
|
|
1237
1237
|
|
|
1238
1238
|
- **The lexical form is preserved.** A `full-date` shifted by a day is still
|
|
1239
1239
|
a `full-date`, and a `date-time` keeps its own offset rather than being
|
|
@@ -1245,6 +1245,19 @@ Two rules make these predictable:
|
|
|
1245
1245
|
because the alternative — overflowing into March — makes adding a month
|
|
1246
1246
|
non-monotonic. `$date-diff` counts months to match, so adding its result
|
|
1247
1247
|
back never overshoots: `2026-01-31` to `2026-02-28` is **one** month.
|
|
1248
|
+
- **An operation that needs a half the value has not got is refused.** A unit
|
|
1249
|
+
names a half: `year` through `day` read the calendar, `hour` through
|
|
1250
|
+
`millisecond` read the clock. Adding hours to a `full-date`, adding a day
|
|
1251
|
+
to a `full-time`, or truncating either to the unit it does not carry is
|
|
1252
|
+
`JQ2001` — there is no answer to give, and the alternative to refusing is
|
|
1253
|
+
to invent one. `$start-of`/`$end-of` `day` is the boundary both forms have:
|
|
1254
|
+
a `full-time` truncates to midnight and to `23:59:59.999`.
|
|
1255
|
+
|
|
1256
|
+
A **fraction** is a quantity only where the unit converts exactly.
|
|
1257
|
+
`[date, 1.5, "day"]` and `[date, "P1.5D"]` are thirty-six hours, applied as
|
|
1258
|
+
whole milliseconds — so both need a value with a clock to land on. Half a
|
|
1259
|
+
month is not a length and is `JQ2001`; half a *year* is six months exactly,
|
|
1260
|
+
and applies.
|
|
1248
1261
|
|
|
1249
1262
|
`$date-format` renders a value through a **Unicode LDML** pattern
|
|
1250
1263
|
(`yyyy-MM-dd`, not moment's `YYYY-MM-DD`); a literal pattern compiles once
|
|
@@ -1264,9 +1277,9 @@ the presentation layer, not to a query.
|
|
|
1264
1277
|
| `$quarter` `$weekday` | calendar quarter 1-4; ISO weekday 1 (Monday) to 7 (Sunday) |
|
|
1265
1278
|
| `$epoch` | date or date-time → milliseconds since the epoch (UTC); a `full-time` → `JQ2001` |
|
|
1266
1279
|
| `$datetime` | epoch milliseconds → canonical UTC `date-time`; out of RFC 3339 range → `JQ2001` |
|
|
1267
|
-
| `$date-add` `$date-sub` | `[date, duration]` or `[date, amount, unit]` → a value of the same lexical form |
|
|
1268
|
-
| `$start-of` `$end-of` | `[date, unit]` → the unit's first / last instant, in the same lexical form |
|
|
1269
|
-
| `$date-diff` | `[from, to, unit]` → whole units, negative when `to` precedes `from` |
|
|
1280
|
+
| `$date-add` `$date-sub` | `[date, duration]` or `[date, amount, unit]` → a value of the same lexical form; a fraction that has no exact conversion, or a unit the value has no half for, is `JQ2001` |
|
|
1281
|
+
| `$start-of` `$end-of` | `[date, unit]` → the unit's first / last instant, in the same lexical form; a sub-day unit on a `full-date`, or `week` and coarser on a `full-time`, is `JQ2001` |
|
|
1282
|
+
| `$date-diff` | `[from, to, unit]` → whole units, negative when `to` precedes `from`; a value with no date half is `JQ2001` for every unit |
|
|
1270
1283
|
| `$date-format` | `[date, pattern]` → the value rendered through an LDML pattern |
|
|
1271
1284
|
|
|
1272
1285
|
Every operator that *produces* a date produces it in the same canonical
|
|
@@ -1525,6 +1538,161 @@ A filter is the same key in a `$where`, and it composes with the ordering:
|
|
|
1525
1538
|
threshold filter for free.
|
|
1526
1539
|
|
|
1527
1540
|
|
|
1541
|
+
### 8.16 Time series
|
|
1542
|
+
|
|
1543
|
+
A time series is what a JSON document already holds when something has been
|
|
1544
|
+
measured repeatedly: **records with an instant and a reading**. There is no
|
|
1545
|
+
series type to construct and nothing to declare — `{"at": 1767225600000,
|
|
1546
|
+
"value": 4.5}` is a sample, and `{"start": …, "end": …}` is an interval.
|
|
1547
|
+
|
|
1548
|
+
These five operators are the vocabulary for questions a `$for` phrase can ask
|
|
1549
|
+
but cannot answer in one pass: *do these two spans collide*, *which bucket
|
|
1550
|
+
does this instant fall in*, *what does an hour of this look like*, *what was
|
|
1551
|
+
the average over the last five minutes at every point*, and *what was the
|
|
1552
|
+
current value when this happened*. Each is a call into
|
|
1553
|
+
[`@jarenjs/core/series`](../../core/README.md) and nothing else — the same
|
|
1554
|
+
kernel a chart and an indexed database read — so a document, a fluent chain
|
|
1555
|
+
and a stored plan cannot answer the same question differently.
|
|
1556
|
+
|
|
1557
|
+
| Operator | Definition |
|
|
1558
|
+
|---|---|
|
|
1559
|
+
| `$overlaps` | `[a, b]` → do two half-open `{start, end}` intervals share an instant; touching spans do **not** overlap |
|
|
1560
|
+
| `$time-bucket` | `[at, every, origin?, context?]` → the instant labelling the bucket `at` falls in |
|
|
1561
|
+
| `$resample` | `[series, spec]` → sorted `{at, value, count}` buckets, one per `every` |
|
|
1562
|
+
| `$rolling` | `[series, spec]` → one `{at, value, count}` per input instant, over a window measured in **time** |
|
|
1563
|
+
| `$asof` | `[left, right, spec?]` → one `{left, right, distance}` per left row: the right row that was current when it happened |
|
|
1564
|
+
|
|
1565
|
+
**Instants are epoch milliseconds** on both sides of these operators — what an
|
|
1566
|
+
indexed column stores and what arithmetic wants. A source row may spell its
|
|
1567
|
+
instant as an RFC 3339 string and it is converted once, at the door; every
|
|
1568
|
+
`at` these operators *produce* is a number. `$epoch` and `$datetime` are the
|
|
1569
|
+
two conversions, and they already exist:
|
|
1570
|
+
`{"$datetime": {"$time-bucket": [{"$epoch": "$e.on"}, "PT1H"]}}`.
|
|
1571
|
+
|
|
1572
|
+
**A series operand is either spelling.** A path that fans out (`$.rows[*]`)
|
|
1573
|
+
arrives as a sequence of rows; one that does not (`$.rows`) arrives as the
|
|
1574
|
+
array holding them. The empty sequence is an empty series — no rows is data.
|
|
1575
|
+
|
|
1576
|
+
#### Specs are literals
|
|
1577
|
+
|
|
1578
|
+
The second operand of `$resample`, `$rolling` and `$asof` — and the fourth of
|
|
1579
|
+
`$time-bucket` — is a **verbatim JSON literal**, not an expression. Nothing
|
|
1580
|
+
inside it is evaluated and nothing inside it can vary per row, which is what
|
|
1581
|
+
lets the width, the aggregate, the fill policy, the wall clock and the row
|
|
1582
|
+
selectors be read exactly once when the query compiles.
|
|
1583
|
+
|
|
1584
|
+
Each spec is **closed**: it admits the members below and no others. An unknown
|
|
1585
|
+
member is `JQ0003` with the near miss named, because a `minPeriod` silently
|
|
1586
|
+
ignored is the bug that costs an afternoon. So is a bad duration, a bad
|
|
1587
|
+
aggregate, a bad path and a zone with no provider. Only what the *data*
|
|
1588
|
+
decides — a row that is not a sample, an instant that names none, a local time
|
|
1589
|
+
that never happened — is `JQ2001`, against the operand that carried it.
|
|
1590
|
+
|
|
1591
|
+
| Spec | Members |
|
|
1592
|
+
|---|---|
|
|
1593
|
+
| `$resample` | `every` (required), `origin`, `start`, `end`, `aggregate`, `fill`, `at`, `value`, and the calendar context |
|
|
1594
|
+
| `$rolling` | `width` (required), `aggregate`, `minPeriods`, `at`, `value`, and the calendar context |
|
|
1595
|
+
| `$asof` | `direction`, `tolerance`, `by`, `leftAt`, `rightAt` |
|
|
1596
|
+
| calendar context | `zone`, `offset`, `disambiguation` |
|
|
1597
|
+
|
|
1598
|
+
`every`, `width` and `tolerance` are an ISO 8601 duration (`"PT1H"`, `"P1M"`)
|
|
1599
|
+
or a count of milliseconds. `origin`, `start` and `end` are epoch milliseconds
|
|
1600
|
+
or an RFC 3339 string. `aggregate` is one of `sum`, `mean`, `min`, `max`,
|
|
1601
|
+
`first`, `last`, `count` (default `mean`); `fill` is one of `omit`, `null`,
|
|
1602
|
+
`zero`, `locf`, `linear` (default `omit`); `direction` is `backward` (default),
|
|
1603
|
+
`forward` or `nearest`.
|
|
1604
|
+
|
|
1605
|
+
`at`, `value`, `by`, `leftAt` and `rightAt` are **row selectors**: a singular
|
|
1606
|
+
path where `$` reads as the *row* rather than as the document, so a series
|
|
1607
|
+
spelled `{"on": …, "reading": …}` is read with `{"at": "$.on", "value":
|
|
1608
|
+
"$.reading"}` and nothing has to be rewritten first. Wildcards, descendants
|
|
1609
|
+
and filters are refused (`JQ0003`) — a selector names one member.
|
|
1610
|
+
|
|
1611
|
+
#### Buckets, fill and the count
|
|
1612
|
+
|
|
1613
|
+
`$resample` labels every bucket at its **start** and reports `count`, the
|
|
1614
|
+
number of source rows that fell in it — duplicates and gaps included. The six
|
|
1615
|
+
value aggregates skip a `null` reading, so `value` is `null` exactly when
|
|
1616
|
+
there was nothing to measure and `count` says whether that was because nobody
|
|
1617
|
+
reported or everybody reported a gap. `aggregate: "count"` returns that same
|
|
1618
|
+
number as `value`.
|
|
1619
|
+
|
|
1620
|
+
`fill` decides what an **empty** bucket says, and nothing else. `omit` leaves
|
|
1621
|
+
it out; `null` and `zero` emit it with that value; `locf` carries the last
|
|
1622
|
+
measured bucket forward; `linear` interpolates between its two neighbours.
|
|
1623
|
+
Neither `locf` nor `linear` extrapolates: with no measured bucket on the side
|
|
1624
|
+
it needs, the bucket stays `null`. A bucket that held rows and no numbers is a
|
|
1625
|
+
*measurement*, not an absence, and is never an anchor for either.
|
|
1626
|
+
|
|
1627
|
+
```json
|
|
1628
|
+
{ "$resample": [ "$.readings[*]",
|
|
1629
|
+
{ "every": "PT1H", "aggregate": "mean", "fill": "linear" } ] }
|
|
1630
|
+
```
|
|
1631
|
+
|
|
1632
|
+
#### Windows measured in time
|
|
1633
|
+
|
|
1634
|
+
`$rolling` answers one row per input instant, over the half-open window
|
|
1635
|
+
ending at it. `minPeriods` (default 1) withholds a value — `null`, with the
|
|
1636
|
+
real `count` — for a window that is short of that many readings, which is how
|
|
1637
|
+
a leading partial window stays visible instead of being quietly averaged.
|
|
1638
|
+
|
|
1639
|
+
Rows sharing an instant share a window and share an answer: the window is a
|
|
1640
|
+
function of the instant it ends at, never of arrival order. That is the one
|
|
1641
|
+
place `$rolling` differs from `$for`'s `$window` (§6.4), which counts **rows**
|
|
1642
|
+
and is unchanged.
|
|
1643
|
+
|
|
1644
|
+
```json
|
|
1645
|
+
{ "$rolling": [ "$.samples[*]", { "width": "PT5M", "aggregate": "mean", "minPeriods": 30 } ] }
|
|
1646
|
+
```
|
|
1647
|
+
|
|
1648
|
+
#### As-of, and no-match as data
|
|
1649
|
+
|
|
1650
|
+
`$asof` walks both sides once and answers `{left, right, distance}` per left
|
|
1651
|
+
row. `backward` takes the last right row at or before the left instant,
|
|
1652
|
+
`forward` the first at or after, `nearest` the closer of the two with ties
|
|
1653
|
+
going backward. At an equal instant the **last** right row wins, because "as
|
|
1654
|
+
of" means the later reading. `tolerance` is the furthest a match may be —
|
|
1655
|
+
beyond it there is no match, not a distant one — and `by` joins within groups.
|
|
1656
|
+
|
|
1657
|
+
A left row with nothing to match is `{"left": …, "right": null, "distance":
|
|
1658
|
+
null}`. **It stays in the answer**: no match is data, and a join that dropped
|
|
1659
|
+
the row would be answering a question nobody asked.
|
|
1660
|
+
|
|
1661
|
+
```json
|
|
1662
|
+
{ "$asof": [ "$.trades[*]", "$.quotes[*]",
|
|
1663
|
+
{ "by": "$.symbol", "direction": "backward", "tolerance": "PT1M" } ] }
|
|
1664
|
+
```
|
|
1665
|
+
|
|
1666
|
+
#### Calendar boundaries and the zone seam
|
|
1667
|
+
|
|
1668
|
+
A fixed width (`PT15M`, `P1D` on UTC or an offset) is integer arithmetic. A
|
|
1669
|
+
calendar width (`P1M`, `P1Y`, and a day on a *named* zone, where the day the
|
|
1670
|
+
clock changed is 23 or 25 hours long) walks a wall clock instead. A width
|
|
1671
|
+
mixing the two families (`P1MT1H`) is refused.
|
|
1672
|
+
|
|
1673
|
+
UTC is the default and needs nothing. `{"offset": 120}` is a constant number
|
|
1674
|
+
of minutes east and is exact. A **named zone needs a time-zone database this
|
|
1675
|
+
suite deliberately does not bundle**, and a JSON document cannot carry one, so
|
|
1676
|
+
`{"zone": "Europe/Amsterdam"}` compiles only when the host injected
|
|
1677
|
+
`options.zoneProvider` — an object with `toParts(epoch, zone)` and
|
|
1678
|
+
`toEpoch(parts, zone, disambiguation)`. Without it the document is refused
|
|
1679
|
+
(`JQ0003`) naming the seam, rather than falling back to UTC and being right
|
|
1680
|
+
for eight months of the year.
|
|
1681
|
+
|
|
1682
|
+
`disambiguation` says what a local time that happens twice, or never, resolves
|
|
1683
|
+
to: `reject` (the default — it is an error), `earlier` or `later`.
|
|
1684
|
+
|
|
1685
|
+
```json
|
|
1686
|
+
{ "$resample": [ "$.readings[*]",
|
|
1687
|
+
{ "every": "P1M", "zone": "Europe/Amsterdam", "aggregate": "sum" } ] }
|
|
1688
|
+
```
|
|
1689
|
+
|
|
1690
|
+
**Nothing here reads a clock.** `$resample`'s window, when `start`/`end` are
|
|
1691
|
+
absent, derives from the data; there is no `$now`, for the same reason §8.13
|
|
1692
|
+
has no `current-dateTime`. A compiled query gives the same answer for the same
|
|
1693
|
+
document forever.
|
|
1694
|
+
|
|
1695
|
+
|
|
1528
1696
|
## 9. Variables, scoping, and external parameters
|
|
1529
1697
|
|
|
1530
1698
|
1. Variables are introduced by `$for`, `$let`, `$at`, `$count`, `$groupby`
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jarenjs/json",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.49.2",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
7
7
|
"types": "./dist/types/index.d.ts",
|
|
@@ -90,6 +90,6 @@
|
|
|
90
90
|
"prepack": "npm run build:types"
|
|
91
91
|
},
|
|
92
92
|
"dependencies": {
|
|
93
|
-
"@jarenjs/core": "^0.
|
|
93
|
+
"@jarenjs/core": "^0.49.2"
|
|
94
94
|
}
|
|
95
95
|
}
|