@jarenjs/linq 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.
package/README.md CHANGED
@@ -52,6 +52,13 @@ played by the provider seam below.
52
52
  spatial measurements are `geoArea`/`geoLength` because `length` on
53
53
  this surface is already `$string-length` — the mapping table says so
54
54
  in its own row.
55
+ - **Time is spellable.** The whole RFC 3339 date family is on the surface —
56
+ `e.on.startOf('month')`, `e.on.dateAdd(3, 'day')`, `e.on.week()` — and so are
57
+ the five time-series operators: `rows.all().resample({ every: 'PT1H', fill:
58
+ 'locf' })`, `rows.all().rolling({ width: 60000 })` and
59
+ `left.all().asof(right, { by: '$.symbol' })`. A series spec is a literal and
60
+ is embedded verbatim, so every rule about what it may say stays in the
61
+ compiler rather than being restated here.
55
62
  - **Meaning is spellable too.** `m.embedding.similarity(query)` emits
56
63
  §8.15's `$similarity`, and k-nearest is the chain it already looks
57
64
  like — `.orderByDescending(..., { empty: 'least' }).thenBy(m => m.id)
@@ -114,7 +114,9 @@ is part of THIS design.
114
114
  | `OfType<S>` | `$valid` filter with a JSON Schema literal | native | `(schema)` → `Seq<S>`; needs `compileTypeTest` (`JL0003`) |
115
115
  | `Cast<S>` | `$assert` per item | native | `(schema)` → `Seq<S>`; needs `compileTypeTest` (`JL0003`) |
116
116
  | `Zip` | — no positional co-iteration in the grammar | unsupported (`JL0006`) | — |
117
- | expression methods | `eq ne lt le gt ge` → `$eq…$ge`; `and or not`; `add sub mul div idiv mod neg`; `startsWith endsWith contains matches upper lower length concat substring replace` → §8.7; `count sum avg min max` → §8.8 (aggregates as expressions, e.g. over a group); `year month day epoch` → §8.13; `exists isEmpty`; `at all get` | native | on `Expr<…>`, per the typed-surface order |
117
+ | expression methods | `eq ne lt le gt ge` → `$eq…$ge`; `and or not`; `add sub mul div idiv mod neg`; `startsWith endsWith contains matches upper lower length concat substring replace` → §8.7; `count sum avg min max` → §8.8 (aggregates as expressions, e.g. over a group); `exists isEmpty`; `at all get` | native | on `Expr<…>`, per the typed-surface order |
118
+ | date family (§8.13) | the whole family, one method per operator. Components `year month day hours minutes seconds offset week weekYear quarter weekday`; instants `epoch datetime`; predicates `isDate isTime isDatetime isDuration`; arithmetic `startOf(unit) endOf(unit) dateAdd(duration \| amount, unit?) dateSub(…) dateDiff(to, unit) dateFormat(pattern)`. `dateAdd`/`dateSub`/`dateFormat` carry the prefix because `add`, `sub` and `format` are taken or ambiguous on this surface — the same reason §8.14 spells `geoArea`. There is no `now()`: §8.13 has no clock, and a fluent surface does not get to add one | native | on `DateTimeExpr` (the `DateTime` brand) and on `UnknownExpr` |
119
+ | series family (§8.16) | `overlaps(other)` → `$overlaps`; `timeBucket(every, origin?, context?)` → `$time-bucket`; `resample(spec)`, `rolling(spec)` and `asof(right, spec?)` → the three sequence operators. A **spec is a literal** and is embedded verbatim — it is read once when the query compiles, so a spec built from the row is `JL0005`, and every rule about what it may *say* stays in the compiler (`JQ0003`). Note that a member literally named `at` is read with `get('at')`: `at(index)` is path navigation on this surface | native | on `ArrayExpr`/fanned paths for the three sequence operators, on `Expr<…>` for the two scalar ones |
118
120
  | spatial family (§8.14) | `bbox geoArea geoLength centroid` → `$bbox $area $length $centroid`; `distance within bboxIntersects` → `$distance $within $bbox-intersects`; `geohash(precision?)` → `$geohash` (optional arity, like `substring`); `geoParse geoText geohashBounds geohashNeighbours` → the conversion family; `geoSimplify(tolerance)` → `$geo-simplify`. A plain JSON polygon embeds as a literal (`p.at.within(poly)`); `.params({ region })` makes it an external instead | native | on `Expr<…>`, per the typed-surface order |
119
121
  | vector family (§8.15) | `similarity(other)` → `$similarity`. The other operand is an array of numbers: a captured one embeds as a literal, `.params({ query })` binds it at call time. There is no `knn` method — k-nearest is `orderByDescending(...).take(k)`, which is the composition the emitted document already is | native | on `Expr<…>`, per the typed-surface order |
120
122
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@jarenjs/linq",
3
3
  "private": false,
4
- "version": "0.46.5",
4
+ "version": "0.49.2",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
7
7
  "types": "./types/index.d.ts",
@@ -47,7 +47,7 @@
47
47
  "prepack": "npm run build:types"
48
48
  },
49
49
  "dependencies": {
50
- "@jarenjs/core": "^0.46.5",
51
- "@jarenjs/json": "^0.46.5"
50
+ "@jarenjs/core": "^0.49.2",
51
+ "@jarenjs/json": "^0.49.2"
52
52
  }
53
53
  }
package/src/expression.js CHANGED
@@ -171,6 +171,43 @@ const unary = (op) => function (/** @type {any} */ record) {
171
171
  return makeExpr({ [op]: record.doc }, record.epoch, false);
172
172
  };
173
173
 
174
+ /**
175
+ * `$date-add` / `$date-sub`: `[date, duration]` or `[date, amount, unit]`.
176
+ * @param {any} record
177
+ * @param {any} amount - an ISO 8601 duration, or a number of units
178
+ * @param {any} [unit] - the calendar unit, when `amount` is a number
179
+ * @returns {any[]}
180
+ */
181
+ function shiftArgs(record, amount, unit) {
182
+ return unit === undefined
183
+ ? [record.doc, toExpression(amount)]
184
+ : [record.doc, toExpression(amount), toExpression(unit)];
185
+ }
186
+
187
+ /**
188
+ * A §8.16 spec: captured data, embedded verbatim.
189
+ *
190
+ * These are the one place this surface hands the compiler something it
191
+ * must NOT evaluate — a width, an aggregate, a fill policy and a row
192
+ * selector are read once when the query compiles, which is what makes
193
+ * them checkable at all. `toExpression` would turn `{ every: 'PT1H' }`
194
+ * into a map constructor and `{ at: '$.on' }` into a path; the spec is
195
+ * therefore embedded as it was written, and every rule about what it may
196
+ * contain stays where it already is, in the query compiler (`JQ0003`).
197
+ *
198
+ * @param {any} spec
199
+ * @param {string} method - for the message
200
+ * @returns {any} the spec, verbatim
201
+ */
202
+ function literalSpec(spec, method) {
203
+ if (!isPlainJson(spec) || spec === null || typeof spec !== 'object' || Array.isArray(spec)) {
204
+ throw new LinqBuildError('JL0005',
205
+ `${method}() takes a plain literal spec object; it is read once when the query`
206
+ + ' compiles, so it cannot be an expression or carry a captured value');
207
+ }
208
+ return spec;
209
+ }
210
+
174
211
  /**
175
212
  * The operator methods, name → builder(record, ...args). One table so
176
213
  * the mapping in LINQ-FORMAT.md §4 has exactly one code counterpart.
@@ -212,10 +249,65 @@ const METHODS = {
212
249
  // `(u, g) => ({ n: g.count() })`)
213
250
  count: unary('$count'), sum: unary('$sum'), avg: unary('$avg'),
214
251
  min: unary('$min'), max: unary('$max'),
215
- // §8.13 dates (the scalar component family; the full date surface
216
- // arrives with the relational order)
252
+ // §8.13 dates the whole family, not a corner of it. A date in this
253
+ // suite is an RFC 3339 STRING, so every one of these is an ordinary
254
+ // string operator with a calendar's worth of rules behind it, and
255
+ // every one lowers to the operator of the same name: there is no
256
+ // LINQ-only date semantics to learn and nothing here a hand-written
257
+ // document could not have said.
258
+ //
259
+ // `dateFormat` rather than `format`, and `dateAdd`/`dateSub` rather
260
+ // than `add`/`sub`, because `add` is already `$add` on this surface —
261
+ // the same reason §8.14 spells `geoArea`. Where no method name is
262
+ // taken, the operator's own name is used unprefixed (`week`,
263
+ // `quarter`, `startOf`).
217
264
  year: unary('$year'), month: unary('$month'), day: unary('$day'),
218
- epoch: unary('$epoch'),
265
+ hours: unary('$hours'), minutes: unary('$minutes'), seconds: unary('$seconds'),
266
+ offset: unary('$offset'), epoch: unary('$epoch'), datetime: unary('$datetime'),
267
+ week: unary('$week'), weekYear: unary('$week-year'),
268
+ quarter: unary('$quarter'), weekday: unary('$weekday'),
269
+ isDate: unary('$is-date'), isTime: unary('$is-time'),
270
+ isDatetime: unary('$is-datetime'), isDuration: unary('$is-duration'),
271
+ startOf: binary('$start-of'), endOf: binary('$end-of'),
272
+ dateFormat: binary('$date-format'),
273
+ dateAdd(record, amount, unit) {
274
+ return makeExpr({ '$date-add': shiftArgs(record, amount, unit) }, record.epoch, false);
275
+ },
276
+ dateSub(record, amount, unit) {
277
+ return makeExpr({ '$date-sub': shiftArgs(record, amount, unit) }, record.epoch, false);
278
+ },
279
+ dateDiff(record, to, unit) {
280
+ return makeExpr(
281
+ { '$date-diff': [record.doc, toExpression(to), toExpression(unit)] },
282
+ record.epoch, false);
283
+ },
284
+ // §8.16 time series. The three sequence-valued operators take a
285
+ // VERBATIM spec literal, so the argument is embedded with `$const`'s
286
+ // discipline - it is captured data, never an expression - and the
287
+ // compiler owns every rule about what it may say.
288
+ overlaps: binary('$overlaps'),
289
+ timeBucket(record, every, origin, context) {
290
+ const args = [record.doc, toExpression(every)];
291
+ if (origin !== undefined || context !== undefined)
292
+ args.push(origin === undefined ? null : toExpression(origin));
293
+ if (context !== undefined)
294
+ args.push(literalSpec(context, 'timeBucket'));
295
+ return makeExpr({ '$time-bucket': args }, record.epoch, false);
296
+ },
297
+ resample(record, spec) {
298
+ return makeExpr({ $resample: [record.doc, literalSpec(spec, 'resample')] },
299
+ record.epoch, false);
300
+ },
301
+ rolling(record, spec) {
302
+ return makeExpr({ $rolling: [record.doc, literalSpec(spec, 'rolling')] },
303
+ record.epoch, false);
304
+ },
305
+ asof(record, right, spec) {
306
+ const args = [record.doc, toExpression(right)];
307
+ if (spec !== undefined)
308
+ args.push(literalSpec(spec, 'asof'));
309
+ return makeExpr({ $asof: args }, record.epoch, false);
310
+ },
219
311
  // §8.14 spatial. `geoArea`/`geoLength` rather than `area`/`length`:
220
312
  // `length` is already `$string-length` on this surface and renaming a
221
313
  // shipped method for symmetry is a breaking change for a cosmetic
package/types/index.d.ts CHANGED
@@ -14,9 +14,9 @@
14
14
 
15
15
  /**
16
16
  * The nominal date-time brand: annotate a model property as `DateTime`
17
- * and the date operators (`year()`, `month()`, `day()`, `epoch()`)
18
- * become available on its expression without making EVERY string a
19
- * date. Purely a type-level marker — the runtime value is a plain
17
+ * and the whole §8.13 date family (`year()`, `startOf()`, `dateAdd()`,
18
+ * `timeBucket()`, …) becomes available on its expression without making
19
+ * EVERY string a date. Purely a type-level marker — the runtime value is a plain
20
20
  * RFC 3339 string; there is no constructor and no runtime cost.
21
21
  */
22
22
  export type DateTime = string & { readonly __jarenTag: 'date-time' };
@@ -59,6 +59,10 @@ export interface NumberExpr extends ExprBase<number>, EqExpr<number> {
59
59
  idiv(value: number | NumberExpr): NumberExpr;
60
60
  mod(value: number | NumberExpr): NumberExpr;
61
61
  neg(): NumberExpr;
62
+ /** An epoch is a number, so the two instant operators live here too. */
63
+ datetime(): DateTimeExpr;
64
+ timeBucket(every: string | number, origin?: string | number | null,
65
+ context?: CalendarContext): NumberExpr;
62
66
  }
63
67
 
64
68
  export interface StringExpr extends ExprBase<string>, EqExpr<string> {
@@ -79,25 +83,165 @@ export interface StringExpr extends ExprBase<string>, EqExpr<string> {
79
83
  replace(pattern: string, replacement: string): StringExpr;
80
84
  }
81
85
 
82
- /** A `DateTime`-branded string: the string surface plus the date
83
- * component operators. */
84
- export interface DateTimeExpr extends ExprBase<DateTime> {
86
+ /** A calendar unit, as QUERY-FORMAT §8.13 fixes it. The unit is DATA
87
+ * rather than vocabulary, so an unknown one is a runtime `JQ2001`; this
88
+ * type is what keeps the common spelling mistake a compile error. */
89
+ export type DateUnit =
90
+ | 'year' | 'quarter' | 'month' | 'week' | 'day'
91
+ | 'hour' | 'minute' | 'second' | 'millisecond';
92
+
93
+ /** The date family (`$is-date` … `$date-format`), available on any
94
+ * expression whose value carries a date. Every method lowers to the §8.13
95
+ * operator of the same name; there is no LINQ-only date semantics. */
96
+ export interface DateMethods {
97
+ /** Lexical date components; a value with no date half is `JQ2001`. */
98
+ year(): NumberExpr;
99
+ month(): NumberExpr;
100
+ day(): NumberExpr;
101
+ /** Lexical time components; `seconds` carries its fraction. */
102
+ hours(): NumberExpr;
103
+ minutes(): NumberExpr;
104
+ seconds(): NumberExpr;
105
+ /** Minutes east of UTC; a bare `full-date` yields the empty sequence. */
106
+ offset(): NumberExpr;
107
+ /** ISO 8601 week number, and its week-numbering year. */
108
+ week(): NumberExpr;
109
+ weekYear(): NumberExpr;
110
+ /** Calendar quarter 1-4; ISO weekday 1 (Monday) to 7 (Sunday). */
111
+ quarter(): NumberExpr;
112
+ weekday(): NumberExpr;
113
+ /** Epoch milliseconds (`$epoch`) — the one shift to UTC. */
114
+ epoch(): NumberExpr;
115
+ /** The inverse: epoch milliseconds → a canonical UTC `date-time`. */
116
+ datetime(): DateTimeExpr;
117
+ /** The RFC 3339 lexical-form predicates; these never raise. */
118
+ isDate(): BoolExpr;
119
+ isTime(): BoolExpr;
120
+ isDatetime(): BoolExpr;
121
+ isDuration(): BoolExpr;
122
+ /** Truncate to a unit, keeping the lexical form (`$start-of`/`$end-of`). */
123
+ startOf(unit: DateUnit): DateTimeExpr;
124
+ endOf(unit: DateUnit): DateTimeExpr;
125
+ /** Shift by an ISO 8601 duration, or by an amount and a unit. */
126
+ dateAdd(duration: string): DateTimeExpr;
127
+ dateAdd(amount: number | NumberExpr, unit: DateUnit): DateTimeExpr;
128
+ dateSub(duration: string): DateTimeExpr;
129
+ dateSub(amount: number | NumberExpr, unit: DateUnit): DateTimeExpr;
130
+ /** Whole units from this value to another; negative when it precedes. */
131
+ dateDiff(to: string | DateTimeExpr, unit: DateUnit): NumberExpr;
132
+ /** Render through a Unicode LDML pattern (`yyyy-MM-dd`). */
133
+ dateFormat(pattern: string): StringExpr;
134
+ /** The instant labelling the bucket this one falls in (`$time-bucket`). */
135
+ timeBucket(every: string | number, origin?: string | number | null,
136
+ context?: CalendarContext): NumberExpr;
137
+ }
138
+
139
+ /** A `DateTime`-branded string: the string surface plus the whole §8.13
140
+ * date family. */
141
+ export interface DateTimeExpr extends ExprBase<DateTime>, DateMethods {
85
142
  eq(value: string | DateTimeExpr | null): BoolExpr;
86
143
  ne(value: string | DateTimeExpr | null): BoolExpr;
87
144
  lt(value: string | DateTimeExpr): BoolExpr;
88
145
  le(value: string | DateTimeExpr): BoolExpr;
89
146
  gt(value: string | DateTimeExpr): BoolExpr;
90
147
  ge(value: string | DateTimeExpr): BoolExpr;
91
- year(): NumberExpr;
92
- month(): NumberExpr;
93
- day(): NumberExpr;
94
- /** Epoch milliseconds (`$epoch`). */
95
- epoch(): NumberExpr;
96
148
  }
97
149
 
98
- export interface ArrayExpr<E> extends ExprBase<E[]> {
150
+ /** The wall clock a calendar boundary falls on (QUERY-FORMAT §8.16).
151
+ * UTC is the default; a named `zone` needs an injected `zoneProvider`. */
152
+ export interface CalendarContext {
153
+ zone?: string;
154
+ offset?: number;
155
+ disambiguation?: 'reject' | 'earlier' | 'later';
156
+ }
157
+
158
+ /** The aggregates `$resample` and `$rolling` share. */
159
+ export type SeriesAggregate =
160
+ 'sum' | 'mean' | 'min' | 'max' | 'first' | 'last' | 'count';
161
+
162
+ /** What an EMPTY bucket says, and nothing else. */
163
+ export type SeriesFill = 'omit' | 'null' | 'zero' | 'locf' | 'linear';
164
+
165
+ /** A width: an ISO 8601 duration, or a count of milliseconds. */
166
+ export type SeriesSpan = string | number;
167
+
168
+ /** An instant: epoch milliseconds, or an RFC 3339 string. */
169
+ export type SeriesInstant = string | number;
170
+
171
+ /** A row selector: a singular path whose `$` is the ROW rather than the
172
+ * document (`'$.on'`, `"$['recorded at']"`). */
173
+ export type RowSelector = string;
174
+
175
+ /** The `$resample` spec — a literal, read once when the query compiles. */
176
+ export interface ResampleSpec extends CalendarContext {
177
+ every: SeriesSpan;
178
+ origin?: SeriesInstant;
179
+ start?: SeriesInstant;
180
+ end?: SeriesInstant;
181
+ aggregate?: SeriesAggregate;
182
+ fill?: SeriesFill;
183
+ at?: RowSelector;
184
+ value?: RowSelector;
185
+ }
186
+
187
+ /** The `$rolling` spec — a window measured in time, not in rows. */
188
+ export interface RollingSpec extends CalendarContext {
189
+ width: SeriesSpan;
190
+ aggregate?: SeriesAggregate;
191
+ minPeriods?: number;
192
+ at?: RowSelector;
193
+ value?: RowSelector;
194
+ }
195
+
196
+ /** The `$asof` spec — every member optional: backward, unkeyed, unbounded. */
197
+ export interface AsOfSpec {
198
+ direction?: 'backward' | 'forward' | 'nearest';
199
+ tolerance?: SeriesSpan;
200
+ by?: RowSelector;
201
+ leftAt?: RowSelector;
202
+ rightAt?: RowSelector;
203
+ }
204
+
205
+ /** One canonical sample the series operators answer with. */
206
+ export interface SeriesBucket {
207
+ at: number;
208
+ value: number | null;
209
+ count: number;
210
+ }
211
+
212
+ /** One row of an as-of join; `right` is `null` when nothing matched, and
213
+ * the row stays in the answer. */
214
+ export interface AsOfMatch {
215
+ left: unknown;
216
+ right: unknown;
217
+ distance: number | null;
218
+ }
219
+
220
+ /** A half-open interval: `[start, end)`, in epoch milliseconds. */
221
+ export interface Interval {
222
+ start: SeriesInstant;
223
+ end: SeriesInstant;
224
+ }
225
+
226
+ /** The §8.16 operators that take a whole series and answer another one.
227
+ * Available wherever a MANY-cardinality expression is (an array member,
228
+ * or a fanned path). */
229
+ export interface SeriesMethods {
230
+ /** Sorted `{at, value, count}` buckets, one per `every` (`$resample`). */
231
+ resample(spec: ResampleSpec): Expr<SeriesBucket[]> & AggregatableExpr;
232
+ /** One row per input instant, over a window measured in time. */
233
+ rolling(spec: RollingSpec): Expr<SeriesBucket[]> & AggregatableExpr;
234
+ /** The right row that was current when each left row happened. */
235
+ asof(right: ExprBase<unknown> | readonly unknown[], spec?: AsOfSpec):
236
+ Expr<AsOfMatch[]> & AggregatableExpr;
237
+ }
238
+
239
+ export interface ArrayExpr<E> extends ExprBase<E[]>, SeriesMethods {
99
240
  eq(value: readonly E[] | ArrayExpr<E> | null): BoolExpr;
100
241
  ne(value: readonly E[] | ArrayExpr<E> | null): BoolExpr;
242
+ /** Do two half-open `{start, end}` intervals share an instant?
243
+ * Touching spans do not. */
244
+ overlaps(other: Interval | ExprBase<unknown>): BoolExpr;
101
245
  /** Fan the elements out (`[*]`) — a MANY-cardinality expression the
102
246
  * aggregates apply to (`u.tags.all().count()`). */
103
247
  all(): Expr<E> & AggregatableExpr;
@@ -126,7 +270,8 @@ export type ObjectExpr<T> = ExprBase<T> & EqExpr<T> & {
126
270
  /** The honest top: everything is available, nothing is precise. Used
127
271
  * where inference ends (dynamic `get`, post-operator members, unknown
128
272
  * elements) — wide, never wrong. */
129
- export interface UnknownExpr extends ExprBase<unknown>, AggregatableExpr {
273
+ export interface UnknownExpr
274
+ extends ExprBase<unknown>, AggregatableExpr, DateMethods, SeriesMethods {
130
275
  eq(value: unknown): BoolExpr;
131
276
  ne(value: unknown): BoolExpr;
132
277
  lt(value: unknown): BoolExpr;
@@ -153,12 +298,10 @@ export interface UnknownExpr extends ExprBase<unknown>, AggregatableExpr {
153
298
  concat(value: unknown): UnknownExpr;
154
299
  substring(start: number, length?: number): UnknownExpr;
155
300
  replace(pattern: string, replacement: string): UnknownExpr;
156
- year(): NumberExpr;
157
- month(): NumberExpr;
158
- day(): NumberExpr;
159
- epoch(): NumberExpr;
160
301
  all(): UnknownExpr;
161
302
  at(index: number): UnknownExpr;
303
+ /** Do two half-open `{start, end}` intervals share an instant? */
304
+ overlaps(other: Interval | ExprBase<unknown>): BoolExpr;
162
305
  }
163
306
 
164
307
  /** Value type → expression type. Order matters: the DateTime brand is