@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 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 99 §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:
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 (99 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 and `$similarity`) is cataloged in [QUERY-FORMAT.md §8](./docs/QUERY-FORMAT.md#8-operators).
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 99 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).
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. Two 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), and **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.
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 98 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.
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;
@@ -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
- Two rules make these predictable:
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.46.4",
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.46.4"
93
+ "@jarenjs/core": "^0.49.2"
94
94
  }
95
95
  }