@jarenjs/json 0.46.5 → 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.
@@ -57,6 +57,14 @@ import {
57
57
  compileDateFormat,
58
58
  } from '@jarenjs/core/dates';
59
59
  import { isVector, cosineSimilarity } from '@jarenjs/core/vector';
60
+ import {
61
+ toEpoch,
62
+ overlapsInterval,
63
+ compileBuckets,
64
+ resampleSeries,
65
+ rollingSeries,
66
+ asOfJoin,
67
+ } from '@jarenjs/core/series';
60
68
  import {
61
69
  isPosition,
62
70
  bboxOf,
@@ -83,6 +91,13 @@ import {
83
91
  CARD_ZERO, CARD_ONE, CARD_OPT, CARD_MANY, joinCard, sumCard,
84
92
  } from './normalize.js';
85
93
  import { compileExistsTest } from './compile.js';
94
+ import {
95
+ CLOCK_MEMBERS, RESAMPLE_MEMBERS, ROLLING_MEMBERS, ASOF_MEMBERS,
96
+ AGGREGATES, FILLS, DIRECTIONS,
97
+ requireSpec, requireEnum, requireNumber, requireInstant, requireSpan,
98
+ compileSelector, compileClock, buildKernelSpec,
99
+ seriesArg, intervalArg, seriesRefusal,
100
+ } from './series.js';
86
101
 
87
102
  const hasOwn = Object.hasOwn;
88
103
 
@@ -99,6 +114,13 @@ const ARGS_0N = Object.freeze({ kinds: Object.freeze(['expr']), min: 0, variadic
99
114
  const ARGS_1N = Object.freeze({ kinds: Object.freeze(['expr']), min: 1, variadic: true });
100
115
  // the schema operators $valid/$assert: [expr, schema] (section 8.11)
101
116
  const ARGS_EXPR_SCHEMA = Object.freeze({ kinds: Object.freeze(['expr', 'schema']), min: 2 });
117
+ // the series operators (section 8.16): the operand(s), then a spec
118
+ // captured VERBATIM - a literal, so the width, the aggregate, the fill
119
+ // and the clock are read once at compile time and never per row
120
+ const ARGS_EXPR_SPEC = Object.freeze({ kinds: Object.freeze(['expr', 'raw']), min: 2 });
121
+ const ARGS_ASOF = Object.freeze({ kinds: Object.freeze(['expr', 'expr', 'raw']), min: 2 });
122
+ const ARGS_TIME_BUCKET = Object.freeze({
123
+ kinds: Object.freeze(['expr', 'expr', 'expr', 'raw']), min: 2 });
102
124
 
103
125
  const RESULT_ONE = () => CARD_ONE;
104
126
  const RESULT_OPT = () => CARD_OPT;
@@ -676,6 +698,17 @@ function unitArg(v, docPath) {
676
698
  return v;
677
699
  }
678
700
 
701
+ // The kernel refuses an operation whose unit reads a half the value has
702
+ // not got, and a fraction of a unit that has no exact length. Those are
703
+ // data-level refusals here - the operand decided them, not the query -
704
+ // so they surface as JQ2001 against the date operand, the same way a
705
+ // dynamic `$date-format` pattern does.
706
+ function dateRefusal(e, docPath) {
707
+ if (!(e instanceof TypeError))
708
+ throw e;
709
+ return runtimeError('JQ2001', e.message, docPath);
710
+ }
711
+
679
712
  // The shared shape of $date-add / $date-sub: [date, duration] applies an
680
713
  // ISO 8601 duration, [date, amount, unit] applies one unit. Both return
681
714
  // the same lexical form they were given, so a full-date stays a
@@ -707,12 +740,22 @@ function dateShiftEntry(sign) {
707
740
  + (typeof second === 'string' ? JSON.stringify(second) : describeItem(second)),
708
741
  secondPath);
709
742
  }
710
- return formatRFC3339Parts(addDuration(parts, duration, sign));
743
+ try {
744
+ return formatRFC3339Parts(addDuration(parts, duration, sign));
745
+ }
746
+ catch (e) {
747
+ throw dateRefusal(e, datePath);
748
+ }
711
749
  }
712
750
  if (typeof second !== 'number')
713
751
  throw runtimeError('JQ2001', `expected a number of units, got ${describeItem(second)}`, secondPath);
714
752
  const unit = unitArg(unitGet(f), unitPath);
715
- return formatRFC3339Parts(addToParts(parts, sign * second, unit));
753
+ try {
754
+ return formatRFC3339Parts(addToParts(parts, sign * second, unit));
755
+ }
756
+ catch (e) {
757
+ throw dateRefusal(e, datePath);
758
+ }
716
759
  };
717
760
  },
718
761
  };
@@ -735,7 +778,13 @@ function dateTruncEntry(truncate) {
735
778
  if (value === EMPTY)
736
779
  return EMPTY;
737
780
  const parts = dateParts(value, datePath);
738
- return formatRFC3339Parts(truncate(parts, unitArg(unitGet(f), unitPath)));
781
+ const unit = unitArg(unitGet(f), unitPath);
782
+ try {
783
+ return formatRFC3339Parts(truncate(parts, unit));
784
+ }
785
+ catch (e) {
786
+ throw dateRefusal(e, datePath);
787
+ }
739
788
  };
740
789
  },
741
790
  };
@@ -876,6 +925,120 @@ function vectorArg(v, docPath) {
876
925
 
877
926
  //#endregion
878
927
 
928
+ //#region series operators (section 8.16)
929
+ // Five operators, and not one loop among them. Interval overlap,
930
+ // bucketing, resampling, a window measured in time and an as-of join all
931
+ // exist once already, in `@jarenjs/core/series`, where the chart engine
932
+ // and the database read them too; these entries carry a document's
933
+ // literal spec across to that kernel and its answer back.
934
+ //
935
+ // The split between the two error families is the same one section 8.14
936
+ // draws, moved one level up. Everything a document AUTHORED - the width,
937
+ // the aggregate, the fill policy, the wall clock, the path a row keeps
938
+ // its instant at - is checked when the query compiles (`JQ0003`), so a
939
+ // misspelled member is a broken document rather than a surprise on the
940
+ // ten-thousandth row. Everything the DATA decides - a row that is not a
941
+ // sample, an instant that names none, a local time that never happened -
942
+ // is `JQ2001` against the operand that carried it. An as-of row with no
943
+ // match is neither: it is `right: null`, which is an answer.
944
+ //
945
+ // The instants in and out are epoch milliseconds, because that is what
946
+ // D3 makes canonical and what an indexed column stores. `$epoch` and
947
+ // `$datetime` are the two conversions, and they already exist.
948
+
949
+ /** The default clock: UTC, which needs no context at all. */
950
+ const EMPTY_CLOCK = Object.freeze({});
951
+
952
+ /** The default spec: every kernel default, none of them restated here. */
953
+ const EMPTY_SPEC = Object.freeze({});
954
+
955
+ /** How each `$resample` spec member is read. */
956
+ const RESAMPLE_RULES = Object.freeze({
957
+ every: requireSpan,
958
+ origin: requireInstant,
959
+ start: requireInstant,
960
+ end: requireInstant,
961
+ aggregate: (v, m, p) => requireEnum(v, AGGREGATES, m, p),
962
+ fill: (v, m, p) => requireEnum(v, FILLS, m, p),
963
+ at: null, value: null, // selectors; buildKernelSpec compiles these
964
+ });
965
+
966
+ /** How each `$rolling` spec member is read. */
967
+ const ROLLING_RULES = Object.freeze({
968
+ width: requireSpan,
969
+ aggregate: (v, m, p) => requireEnum(v, AGGREGATES, m, p),
970
+ minPeriods: requireNumber,
971
+ at: null, value: null,
972
+ });
973
+
974
+ // The shared shape of $resample and $rolling: [series, spec]. The spec
975
+ // compiles once, into the record the kernel takes, and the closure does
976
+ // nothing per call but read its rows and hand them over.
977
+ function seriesEntry(name, members, rules, required, kernel) {
978
+ return {
979
+ params: ARGS_EXPR_SPEC,
980
+ result: RESULT_MANY,
981
+ compile: (gets, args, docPath, node) => {
982
+ const seriesGet = gets[0];
983
+ const seriesPath = args[0].docPath;
984
+ const specNode = args[1];
985
+ const specPath = specNode.docPath;
986
+ const spec = requireSpec(specNode.value, members, name, specPath);
987
+ if (!hasOwn(spec, required)) {
988
+ throw new JsonQueryCompileError('JQ0003',
989
+ `'${name}' needs a spec member '${required}'`, specPath);
990
+ }
991
+ const kernelSpec = Object.freeze(
992
+ buildKernelSpec(spec, node.zoneProvider ?? null, specPath, rules));
993
+ // the kernel owns the remaining rules - a width mixing the
994
+ // calendar and clock families, a minPeriods that is not a whole
995
+ // number, a tolerance that is not a fixed width. Running it over
996
+ // no rows AT COMPILE TIME asks it all of them without copying one
997
+ // of them here, so a literal spec that cannot work is a broken
998
+ // document rather than a surprise on the first row
999
+ try {
1000
+ kernel([], kernelSpec);
1001
+ }
1002
+ catch (e) {
1003
+ if (!(e instanceof TypeError))
1004
+ throw e;
1005
+ throw new JsonQueryCompileError('JQ0003',
1006
+ `'${name}' spec: ${e.message}`, specPath, { cause: e });
1007
+ }
1008
+ return (f) => {
1009
+ const rows = seriesArg(seriesGet(f), seriesPath);
1010
+ try {
1011
+ return seqOf(kernel(rows, kernelSpec));
1012
+ }
1013
+ catch (e) {
1014
+ throw seriesRefusal(e, specPath);
1015
+ }
1016
+ };
1017
+ },
1018
+ };
1019
+ }
1020
+
1021
+ // $asof's spec in the kernel's spelling: `by` is one key for both sides,
1022
+ // and `leftAt`/`rightAt` say where each side keeps its instant when the
1023
+ // two documents disagree about the name.
1024
+ function asOfSpec(spec, docPath) {
1025
+ /** @type {any} */
1026
+ const out = {};
1027
+ if (hasOwn(spec, 'direction'))
1028
+ out.direction = requireEnum(spec.direction, DIRECTIONS, 'direction', docPath);
1029
+ if (hasOwn(spec, 'tolerance'))
1030
+ out.tolerance = requireSpan(spec.tolerance, 'tolerance', docPath);
1031
+ if (hasOwn(spec, 'by'))
1032
+ out.key = compileSelector(spec.by, 'by', docPath);
1033
+ if (hasOwn(spec, 'leftAt'))
1034
+ out.left = Object.freeze({ at: compileSelector(spec.leftAt, 'leftAt', docPath) });
1035
+ if (hasOwn(spec, 'rightAt'))
1036
+ out.right = Object.freeze({ at: compileSelector(spec.rightAt, 'rightAt', docPath) });
1037
+ return Object.freeze(out);
1038
+ }
1039
+
1040
+ //#endregion
1041
+
879
1042
  // the RFC 3339 type tests, shaped like the section 8.10 $is-* family:
880
1043
  // one item of the right lexical form, never an error
881
1044
  function dateTestEntry(test) {
@@ -1845,6 +2008,13 @@ export const OPERATORS = Object.freeze({
1845
2008
  // months, quarters and years have no fixed width, so they are
1846
2009
  // counted on the calendar; everything else divides an exact span
1847
2010
  if (unit === 'month' || unit === 'quarter' || unit === 'year') {
2011
+ if (from.year < 0 || to.year < 0) {
2012
+ // the fixed-width branch below refuses this through NaN; the
2013
+ // calendar branch has to say so itself, or a full-time pair
2014
+ // measures zero months apart
2015
+ throw runtimeError('JQ2001', 'cannot measure a span from a value with no date',
2016
+ from.year < 0 ? fromPath : toPath);
2017
+ }
1848
2018
  const months = monthsBetween(from, to);
1849
2019
  return unit === 'month' ? months
1850
2020
  : Math.trunc(months / (unit === 'quarter' ? 3 : 12));
@@ -1990,6 +2160,165 @@ export const OPERATORS = Object.freeze({
1990
2160
  },
1991
2161
 
1992
2162
  //#endregion
2163
+
2164
+ //#region section 8.16 - time series
2165
+
2166
+ '$overlaps': { // do two half-open intervals share an instant?
2167
+ params: ARGS_2,
2168
+ result: (cards) => (cards[0] === CARD_ONE && cards[1] === CARD_ONE ? CARD_ONE : CARD_OPT),
2169
+ resultType: RT_BOOLEAN,
2170
+ compile: (gets, args) => {
2171
+ const leftGet = gets[0];
2172
+ const leftPath = args[0].docPath;
2173
+ const rightGet = gets[1];
2174
+ const rightPath = args[1].docPath;
2175
+ return (f) => {
2176
+ // a row with no span is not overlapping and not an error: the
2177
+ // empty sequence propagates, as it does for every date operator
2178
+ const left = leftGet(f);
2179
+ if (left === EMPTY)
2180
+ return EMPTY;
2181
+ const right = rightGet(f);
2182
+ if (right === EMPTY)
2183
+ return EMPTY;
2184
+ try {
2185
+ return overlapsInterval(intervalArg(left, leftPath), intervalArg(right, rightPath));
2186
+ }
2187
+ catch (e) {
2188
+ throw seriesRefusal(e, leftPath);
2189
+ }
2190
+ };
2191
+ },
2192
+ },
2193
+
2194
+ '$time-bucket': { // the instant labelling the bucket an instant falls in
2195
+ params: ARGS_TIME_BUCKET,
2196
+ // every EXPRESSION operand propagates the empty sequence, so the
2197
+ // answer is exactly-one only when none of them can be empty - a
2198
+ // CARD_ONE declaration over an optional width would let the internal
2199
+ // empty marker escape into an array or object constructor
2200
+ result: (cards) => (cards.every((c) => c === CARD_ONE) ? CARD_ONE : CARD_OPT),
2201
+ resultType: RT_INTEGER,
2202
+ compile: (gets, args, docPath, node) => {
2203
+ const atGet = gets[0];
2204
+ const atPath = args[0].docPath;
2205
+ const everyGet = gets[1];
2206
+ const everyPath = args[1].docPath;
2207
+ const originGet = args.length >= 3 ? gets[2] : null;
2208
+ // the calendar context is the one literal position: a clock read
2209
+ // per row could not be resolved once, and a named zone needs a
2210
+ // provider a JSON document has no way to carry
2211
+ const clock = args.length === 4
2212
+ ? compileClock(
2213
+ requireSpec(args[3].value, CLOCK_MEMBERS, '$time-bucket', args[3].docPath),
2214
+ node.zoneProvider ?? null, args[3].docPath)
2215
+ : EMPTY_CLOCK;
2216
+ // the ladder is a function of (every, origin) alone, and both are
2217
+ // almost always literal - so the memo makes a computed width
2218
+ // CORRECT, and costs a constant pair of comparisons when it is not
2219
+ let lastEvery;
2220
+ let lastOrigin;
2221
+ let buckets = null;
2222
+ // the common case is a literal width and a literal origin, and
2223
+ // then the ladder belongs to the query rather than to a row: a
2224
+ // duration neither family recognizes is a broken document
2225
+ if (args[1].kind === 'literal' && (originGet === null || args[2].kind === 'literal')) {
2226
+ lastEvery = args[1].value;
2227
+ lastOrigin = originGet === null || args[2].value === null ? EMPTY : args[2].value;
2228
+ try {
2229
+ buckets = compileBuckets(
2230
+ lastOrigin === EMPTY ? { every: lastEvery } : { every: lastEvery, origin: lastOrigin },
2231
+ clock);
2232
+ }
2233
+ catch (e) {
2234
+ if (!(e instanceof TypeError))
2235
+ throw e;
2236
+ throw new JsonQueryCompileError('JQ0003',
2237
+ `'$time-bucket': ${e.message}`, everyPath, { cause: e });
2238
+ }
2239
+ }
2240
+ return (f) => {
2241
+ const at = atGet(f);
2242
+ if (at === EMPTY)
2243
+ return EMPTY;
2244
+ const every = everyGet(f);
2245
+ if (every === EMPTY)
2246
+ return EMPTY;
2247
+ // an absent origin is the ladder's own default; `null` spells it
2248
+ // for a caller who has a fourth argument to give
2249
+ let origin = originGet === null ? EMPTY : originGet(f);
2250
+ if (origin === null)
2251
+ origin = EMPTY;
2252
+ if (buckets === null || every !== lastEvery || origin !== lastOrigin) {
2253
+ if (typeof every !== 'string' && typeof every !== 'number') {
2254
+ throw runtimeError('JQ2001',
2255
+ `expected a bucket width, got ${describeItem(every)}`, everyPath);
2256
+ }
2257
+ try {
2258
+ buckets = compileBuckets(origin === EMPTY ? { every } : { every, origin }, clock);
2259
+ }
2260
+ catch (e) {
2261
+ buckets = null;
2262
+ throw seriesRefusal(e, everyPath);
2263
+ }
2264
+ lastEvery = every;
2265
+ lastOrigin = origin;
2266
+ }
2267
+ try {
2268
+ return buckets.floor(toEpoch(at));
2269
+ }
2270
+ catch (e) {
2271
+ throw seriesRefusal(e, atPath);
2272
+ }
2273
+ };
2274
+ },
2275
+ },
2276
+
2277
+ '$resample': seriesEntry('$resample', RESAMPLE_MEMBERS, RESAMPLE_RULES, 'every', resampleSeries),
2278
+
2279
+ '$rolling': seriesEntry('$rolling', ROLLING_MEMBERS, ROLLING_RULES, 'width', rollingSeries),
2280
+
2281
+ '$asof': { // the value that was current when this happened
2282
+ params: ARGS_ASOF,
2283
+ result: RESULT_MANY,
2284
+ // no clock: a join is instant arithmetic, so there is no calendar
2285
+ // boundary to fall on and no zone provider to resolve
2286
+ compile: (gets, args, docPath) => {
2287
+ const leftGet = gets[0];
2288
+ const leftPath = args[0].docPath;
2289
+ const rightGet = gets[1];
2290
+ const rightPath = args[1].docPath;
2291
+ const specNode = args.length === 3 ? args[2] : null;
2292
+ const spec = specNode === null
2293
+ ? EMPTY_SPEC
2294
+ : asOfSpec(requireSpec(specNode.value, ASOF_MEMBERS, '$asof', specNode.docPath),
2295
+ specNode.docPath);
2296
+ const specPath = specNode === null ? docPath : specNode.docPath;
2297
+ try { // the direction and the tolerance, asked of the kernel once
2298
+ asOfJoin([], [], spec);
2299
+ }
2300
+ catch (e) {
2301
+ if (!(e instanceof TypeError))
2302
+ throw e;
2303
+ throw new JsonQueryCompileError('JQ0003',
2304
+ `'$asof' spec: ${e.message}`, specPath, { cause: e });
2305
+ }
2306
+ return (f) => {
2307
+ const left = seriesArg(leftGet(f), leftPath);
2308
+ const right = seriesArg(rightGet(f), rightPath);
2309
+ try {
2310
+ // a left row with nothing at or before it is `right: null`;
2311
+ // no match is data, and the row stays in the answer
2312
+ return seqOf(asOfJoin(left, right, spec));
2313
+ }
2314
+ catch (e) {
2315
+ throw seriesRefusal(e, specPath);
2316
+ }
2317
+ };
2318
+ },
2319
+ },
2320
+
2321
+ //#endregion
1993
2322
  });
1994
2323
 
1995
2324
  // singleton type predicates (section 8.10): true iff the operand is one