pond-ts 0.24.0 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +70 -1
- package/dist/batch/aggregate-columns.js +18 -10
- package/dist/batch/operators/column-builders.d.ts +31 -0
- package/dist/batch/operators/column-builders.js +103 -0
- package/dist/batch/time-series.d.ts +8 -0
- package/dist/batch/time-series.js +79 -66
- package/dist/batch/validate.js +7 -1
- package/dist/column.js +91 -21
- package/dist/columnar/chunked-column.d.ts +10 -0
- package/dist/columnar/chunked-column.js +29 -3
- package/dist/columnar/column.d.ts +19 -1
- package/dist/columnar/column.js +40 -5
- package/dist/reducers/avg.js +40 -13
- package/dist/reducers/count.js +30 -4
- package/dist/reducers/index.js +34 -2
- package/dist/reducers/max.js +37 -10
- package/dist/reducers/min.js +37 -23
- package/dist/reducers/percentile.d.ts +12 -28
- package/dist/reducers/percentile.js +49 -43
- package/dist/reducers/stdev.js +115 -29
- package/dist/reducers/sum.js +29 -5
- package/package.json +1 -1
package/dist/column.js
CHANGED
|
@@ -76,33 +76,60 @@ Float64Column.prototype.minMax = function () {
|
|
|
76
76
|
const n = this.length;
|
|
77
77
|
if (n === 0)
|
|
78
78
|
return undefined;
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
lo
|
|
79
|
+
if (this.allFinite) {
|
|
80
|
+
// Provably finite (the chart axis hot path) → no per-element finite
|
|
81
|
+
// guard; the seeded comparison is unambiguous without NaN.
|
|
82
|
+
let i = 0;
|
|
83
|
+
let lo;
|
|
84
|
+
let hi;
|
|
85
|
+
if (!v) {
|
|
86
|
+
lo = values[0];
|
|
87
|
+
hi = lo;
|
|
88
|
+
for (i = 1; i < n; i += 1) {
|
|
89
|
+
const x = values[i];
|
|
90
|
+
lo = lo <= x ? lo : x;
|
|
91
|
+
hi = hi >= x ? hi : x;
|
|
92
|
+
}
|
|
93
|
+
return [lo, hi];
|
|
94
|
+
}
|
|
95
|
+
while (i < n && !v.isDefined(i))
|
|
96
|
+
i += 1;
|
|
97
|
+
if (i >= n)
|
|
98
|
+
return undefined;
|
|
99
|
+
lo = values[i];
|
|
84
100
|
hi = lo;
|
|
85
|
-
for (i
|
|
101
|
+
for (i += 1; i < n; i += 1) {
|
|
102
|
+
if (!v.isDefined(i))
|
|
103
|
+
continue;
|
|
86
104
|
const x = values[i];
|
|
87
105
|
lo = lo <= x ? lo : x;
|
|
88
106
|
hi = hi >= x ? hi : x;
|
|
89
107
|
}
|
|
90
108
|
return [lo, hi];
|
|
91
109
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if (!v.isDefined(i))
|
|
110
|
+
// Guarded: skip non-finite (and missing) cells, matching the reducer
|
|
111
|
+
// `min`/`max` (reducer non-finite policy — docs/notes/reducer-nan-policy.md).
|
|
112
|
+
// All-non-finite (or empty) → undefined.
|
|
113
|
+
let lo;
|
|
114
|
+
let hi;
|
|
115
|
+
for (let i = 0; i < n; i += 1) {
|
|
116
|
+
if (v && !v.isDefined(i))
|
|
100
117
|
continue;
|
|
101
118
|
const x = values[i];
|
|
102
|
-
|
|
103
|
-
|
|
119
|
+
if (!Number.isFinite(x))
|
|
120
|
+
continue;
|
|
121
|
+
if (lo === undefined) {
|
|
122
|
+
lo = x;
|
|
123
|
+
hi = x;
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
if (x < lo)
|
|
127
|
+
lo = x;
|
|
128
|
+
if (x > hi)
|
|
129
|
+
hi = x;
|
|
130
|
+
}
|
|
104
131
|
}
|
|
105
|
-
return [lo, hi];
|
|
132
|
+
return lo === undefined ? undefined : [lo, hi];
|
|
106
133
|
};
|
|
107
134
|
Float64Column.prototype.hasMissing = function () {
|
|
108
135
|
if (!this.validity)
|
|
@@ -228,6 +255,44 @@ Float64Column.prototype.bin = function (bins, reducer) {
|
|
|
228
255
|
// underlying buffer.
|
|
229
256
|
const values = this._values;
|
|
230
257
|
const validity = this.validity;
|
|
258
|
+
if (!this.allFinite) {
|
|
259
|
+
// Guarded path: skip non-finite values (reducer non-finite policy —
|
|
260
|
+
// docs/notes/reducer-nan-policy.md), matching `minMax()` / the reducer
|
|
261
|
+
// min/max. A bin with no finite value (all-missing or all-non-finite)
|
|
262
|
+
// keeps the NaN empty-bin sentinel.
|
|
263
|
+
for (let b = 0; b < bins; b += 1) {
|
|
264
|
+
const start = Math.floor((b * n) / bins);
|
|
265
|
+
const end = Math.floor(((b + 1) * n) / bins);
|
|
266
|
+
let loVal;
|
|
267
|
+
let hiVal;
|
|
268
|
+
for (let i = start; i < end; i += 1) {
|
|
269
|
+
if (validity !== undefined && !validity.isDefined(i))
|
|
270
|
+
continue;
|
|
271
|
+
const x = values[i];
|
|
272
|
+
if (!Number.isFinite(x))
|
|
273
|
+
continue;
|
|
274
|
+
if (loVal === undefined) {
|
|
275
|
+
loVal = x;
|
|
276
|
+
hiVal = x;
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
if (x < loVal)
|
|
280
|
+
loVal = x;
|
|
281
|
+
if (x > hiVal)
|
|
282
|
+
hiVal = x;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
if (loVal === undefined) {
|
|
286
|
+
lo[b] = NaN;
|
|
287
|
+
hi[b] = NaN;
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
lo[b] = loVal;
|
|
291
|
+
hi[b] = hiVal;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return { lo, hi };
|
|
295
|
+
}
|
|
231
296
|
if (validity === undefined) {
|
|
232
297
|
for (let b = 0; b < bins; b += 1) {
|
|
233
298
|
const start = Math.floor((b * n) / bins);
|
|
@@ -568,10 +633,15 @@ ChunkedFloat64Column.prototype.percentile = function (q) {
|
|
|
568
633
|
return materializeChunkedFloat64(this).percentile(q);
|
|
569
634
|
};
|
|
570
635
|
ChunkedFloat64Column.prototype.count = function () {
|
|
571
|
-
//
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
636
|
+
// The O(1) defined-count is correct only when the column is provably
|
|
637
|
+
// finite. Non-finite cells are skipped (reducer non-finite policy —
|
|
638
|
+
// docs/notes/reducer-nan-policy.md), so when finiteness isn't proven,
|
|
639
|
+
// delegate to the materialized packed `count()` (which routes through the
|
|
640
|
+
// count reducer and skips non-finite) — matching packed `Float64Column`.
|
|
641
|
+
if (this.allFinite) {
|
|
642
|
+
return this.validity ? this.validity.definedCount : this.length;
|
|
643
|
+
}
|
|
644
|
+
return materializeChunkedFloat64(this).count();
|
|
575
645
|
};
|
|
576
646
|
ChunkedFloat64Column.prototype.minMax = function () {
|
|
577
647
|
return materializeChunkedFloat64(this).minMax();
|
|
@@ -72,6 +72,16 @@ export declare class ChunkedFloat64Column {
|
|
|
72
72
|
readonly chunks: ReadonlyArray<Float64Column>;
|
|
73
73
|
readonly chunkOffsets: Int32Array;
|
|
74
74
|
readonly validity?: ValidityBitmap;
|
|
75
|
+
/**
|
|
76
|
+
* `true` iff **every** chunk is itself `allFinite` — the merged
|
|
77
|
+
* column is all-finite IFF each input is (an AND). Defaults to
|
|
78
|
+
* `false` whenever any chunk's flag is `false` (or absent), which is
|
|
79
|
+
* the safe direction: a chunk that didn't prove finiteness keeps the
|
|
80
|
+
* whole column on the guarded reducer path. Mirrors
|
|
81
|
+
* `Float64Column.allFinite`'s safety contract — see it for why a
|
|
82
|
+
* wrong `true` is unsafe.
|
|
83
|
+
*/
|
|
84
|
+
readonly allFinite: boolean;
|
|
75
85
|
constructor(chunks: ReadonlyArray<Float64Column>);
|
|
76
86
|
/**
|
|
77
87
|
* Reads cell `i` by binary-searching `chunkOffsets` to find the
|
|
@@ -192,6 +192,16 @@ export class ChunkedFloat64Column {
|
|
|
192
192
|
chunks;
|
|
193
193
|
chunkOffsets;
|
|
194
194
|
validity;
|
|
195
|
+
/**
|
|
196
|
+
* `true` iff **every** chunk is itself `allFinite` — the merged
|
|
197
|
+
* column is all-finite IFF each input is (an AND). Defaults to
|
|
198
|
+
* `false` whenever any chunk's flag is `false` (or absent), which is
|
|
199
|
+
* the safe direction: a chunk that didn't prove finiteness keeps the
|
|
200
|
+
* whole column on the guarded reducer path. Mirrors
|
|
201
|
+
* `Float64Column.allFinite`'s safety contract — see it for why a
|
|
202
|
+
* wrong `true` is unsafe.
|
|
203
|
+
*/
|
|
204
|
+
allFinite;
|
|
195
205
|
constructor(chunks) {
|
|
196
206
|
assertChunkKinds(chunks, 'number', 'ChunkedFloat64Column');
|
|
197
207
|
const { length, chunkOffsets, validity } = buildOffsetsAndAggregateValidity(chunks, 'ChunkedFloat64Column');
|
|
@@ -202,6 +212,16 @@ export class ChunkedFloat64Column {
|
|
|
202
212
|
this.chunkOffsets = chunkOffsets;
|
|
203
213
|
if (validity !== undefined)
|
|
204
214
|
this.validity = validity;
|
|
215
|
+
// AND across chunks: all-finite IFF every chunk is. An empty chunk
|
|
216
|
+
// list is vacuously all-finite (no non-finite cell exists).
|
|
217
|
+
let allFinite = true;
|
|
218
|
+
for (let c = 0; c < chunks.length; c += 1) {
|
|
219
|
+
if (!chunks[c].allFinite) {
|
|
220
|
+
allFinite = false;
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
this.allFinite = allFinite;
|
|
205
225
|
}
|
|
206
226
|
/**
|
|
207
227
|
* Reads cell `i` by binary-searching `chunkOffsets` to find the
|
|
@@ -287,10 +307,13 @@ export class ChunkedFloat64Column {
|
|
|
287
307
|
out[i] = this.chunks[c]._values[local];
|
|
288
308
|
validBits[i >> 3] |= 1 << (i & 7);
|
|
289
309
|
}
|
|
310
|
+
// A gather only reads existing defined cells (out-of-range / invalid
|
|
311
|
+
// slots are skipped → marked invalid below), so the result is finite
|
|
312
|
+
// whenever this chunked column is. Propagate the AND-of-chunks flag.
|
|
290
313
|
if (!hasInvalid) {
|
|
291
|
-
return new Float64Column(out, indices.length);
|
|
314
|
+
return new Float64Column(out, indices.length, undefined, this.allFinite);
|
|
292
315
|
}
|
|
293
|
-
return new Float64Column(out, indices.length, validityFromBits(validBits, indices.length));
|
|
316
|
+
return new Float64Column(out, indices.length, validityFromBits(validBits, indices.length), this.allFinite);
|
|
294
317
|
}
|
|
295
318
|
}
|
|
296
319
|
/* -------------------------------------------------------------------------- */
|
|
@@ -631,7 +654,10 @@ export function materializeChunkedFloat64(chunked) {
|
|
|
631
654
|
out.set(chunk._values.subarray(0, chunk.length), cursor);
|
|
632
655
|
cursor += chunk.length;
|
|
633
656
|
}
|
|
634
|
-
|
|
657
|
+
// Compacting doesn't touch values, so finiteness carries over from
|
|
658
|
+
// the chunked column (which is the AND of its chunks). This is what
|
|
659
|
+
// lets a `concatSorted(...).reduce(...)` take the reducer fast path.
|
|
660
|
+
return new Float64Column(out, chunked.length, chunked.validity, chunked.allFinite);
|
|
635
661
|
}
|
|
636
662
|
/**
|
|
637
663
|
* Compacts a `ChunkedBooleanColumn` into a plain `BooleanColumn`.
|
|
@@ -128,7 +128,25 @@ export declare class Float64Column implements ColumnBase<number, 'number'> {
|
|
|
128
128
|
readonly storage: "packed";
|
|
129
129
|
readonly length: number;
|
|
130
130
|
readonly validity?: ValidityBitmap;
|
|
131
|
-
|
|
131
|
+
/**
|
|
132
|
+
* **Safety contract.** `true` MUST mean "every *defined* cell is
|
|
133
|
+
* finite — no `NaN`, no `±Infinity`" (missing cells, tracked by
|
|
134
|
+
* `validity`, are irrelevant). It is a promise the producer makes
|
|
135
|
+
* so reducers can skip the per-element `Number.isFinite` guard that
|
|
136
|
+
* the reducer non-finite policy (docs/notes/reducer-nan-policy.md)
|
|
137
|
+
* otherwise requires on every numeric `reduceColumn`.
|
|
138
|
+
*
|
|
139
|
+
* A wrongly-`true` flag makes `reduceColumn` take the unguarded fast
|
|
140
|
+
* path and silently include a non-finite cell → wrong result. So the
|
|
141
|
+
* **default is `false`** (conservative: guarded path, correct-but-
|
|
142
|
+
* slower) and a producer sets `true` ONLY where finiteness is proven
|
|
143
|
+
* — either data-derived by inspecting every cell, validated upstream
|
|
144
|
+
* (strict intake), or propagated from a source column that was
|
|
145
|
+
* itself `allFinite`. When unsure, leave it `false`: a missed `true`
|
|
146
|
+
* is slower, never wrong.
|
|
147
|
+
*/
|
|
148
|
+
readonly allFinite: boolean;
|
|
149
|
+
constructor(values: Float64Array, length: number, validity?: ValidityBitmap, allFinite?: boolean);
|
|
132
150
|
read(i: number): number | undefined;
|
|
133
151
|
scan(fn: (value: number, i: number) => void, options?: ScanOptions): void;
|
|
134
152
|
sliceByRange(start: number, end: number): Float64Column;
|
package/dist/columnar/column.js
CHANGED
|
@@ -41,7 +41,25 @@ export class Float64Column {
|
|
|
41
41
|
*/
|
|
42
42
|
_values;
|
|
43
43
|
validity;
|
|
44
|
-
|
|
44
|
+
/**
|
|
45
|
+
* **Safety contract.** `true` MUST mean "every *defined* cell is
|
|
46
|
+
* finite — no `NaN`, no `±Infinity`" (missing cells, tracked by
|
|
47
|
+
* `validity`, are irrelevant). It is a promise the producer makes
|
|
48
|
+
* so reducers can skip the per-element `Number.isFinite` guard that
|
|
49
|
+
* the reducer non-finite policy (docs/notes/reducer-nan-policy.md)
|
|
50
|
+
* otherwise requires on every numeric `reduceColumn`.
|
|
51
|
+
*
|
|
52
|
+
* A wrongly-`true` flag makes `reduceColumn` take the unguarded fast
|
|
53
|
+
* path and silently include a non-finite cell → wrong result. So the
|
|
54
|
+
* **default is `false`** (conservative: guarded path, correct-but-
|
|
55
|
+
* slower) and a producer sets `true` ONLY where finiteness is proven
|
|
56
|
+
* — either data-derived by inspecting every cell, validated upstream
|
|
57
|
+
* (strict intake), or propagated from a source column that was
|
|
58
|
+
* itself `allFinite`. When unsure, leave it `false`: a missed `true`
|
|
59
|
+
* is slower, never wrong.
|
|
60
|
+
*/
|
|
61
|
+
allFinite;
|
|
62
|
+
constructor(values, length, validity, allFinite = false) {
|
|
45
63
|
validateColumnLength(length, 'Float64Column');
|
|
46
64
|
if (length > values.length) {
|
|
47
65
|
throw new RangeError(`Float64Column buffer underflow: length ${length} exceeds values.length ${values.length}`);
|
|
@@ -53,6 +71,7 @@ export class Float64Column {
|
|
|
53
71
|
this.length = length;
|
|
54
72
|
if (validity !== undefined)
|
|
55
73
|
this.validity = validity;
|
|
74
|
+
this.allFinite = allFinite;
|
|
56
75
|
}
|
|
57
76
|
read(i) {
|
|
58
77
|
if (i < 0 || i >= this.length)
|
|
@@ -88,7 +107,8 @@ export class Float64Column {
|
|
|
88
107
|
}
|
|
89
108
|
const valuesSlice = this._values.subarray(lo, hi);
|
|
90
109
|
const validitySlice = validitySliceByRange(this.validity, lo, hi, this.length);
|
|
91
|
-
|
|
110
|
+
// A contiguous slice of an all-finite column is all-finite.
|
|
111
|
+
return new Float64Column(valuesSlice, hi - lo, validitySlice, this.allFinite);
|
|
92
112
|
}
|
|
93
113
|
sliceByIndices(indices) {
|
|
94
114
|
const out = new Float64Array(indices.length);
|
|
@@ -99,7 +119,10 @@ export class Float64Column {
|
|
|
99
119
|
out[i] = idx >= 0 && idx < this.length ? this._values[idx] : 0;
|
|
100
120
|
}
|
|
101
121
|
const validity = validityGatherByIndices(this.validity, indices, this.length);
|
|
102
|
-
|
|
122
|
+
// A gathered subset of finite cells is finite. Out-of-range slots
|
|
123
|
+
// read 0 (finite) and are marked invalid by the validity gather, so
|
|
124
|
+
// they can't violate the contract either way.
|
|
125
|
+
return new Float64Column(out, indices.length, validity, this.allFinite);
|
|
103
126
|
}
|
|
104
127
|
}
|
|
105
128
|
/* -------------------------------------------------------------------------- */
|
|
@@ -201,15 +224,27 @@ export function float64ColumnFromArray(source) {
|
|
|
201
224
|
const length = source.length;
|
|
202
225
|
validateColumnLength(length, 'Float64Column');
|
|
203
226
|
const values = new Float64Array(length);
|
|
227
|
+
// Data-derive `allFinite` in the copy loop: only *defined* cells
|
|
228
|
+
// count (a missing slot doesn't make the column non-finite). Finite
|
|
229
|
+
// data → `true` (reducers take the fast path); an overflow / NaN cell
|
|
230
|
+
// → `false` (guarded path, correct). See the field's safety contract.
|
|
231
|
+
let allFinite = true;
|
|
204
232
|
for (let i = 0; i < length; i += 1) {
|
|
205
233
|
const v = source[i];
|
|
206
|
-
|
|
234
|
+
if (typeof v === 'number') {
|
|
235
|
+
values[i] = v;
|
|
236
|
+
if (!Number.isFinite(v))
|
|
237
|
+
allFinite = false;
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
values[i] = 0;
|
|
241
|
+
}
|
|
207
242
|
}
|
|
208
243
|
const validity = validityFromPredicate(length, (i) => {
|
|
209
244
|
const v = source[i];
|
|
210
245
|
return typeof v === 'number';
|
|
211
246
|
});
|
|
212
|
-
return new Float64Column(values, length, validity);
|
|
247
|
+
return new Float64Column(values, length, validity, allFinite);
|
|
213
248
|
}
|
|
214
249
|
/**
|
|
215
250
|
* Builds a `BooleanColumn` from an array of `boolean | null | undefined`
|
package/dist/reducers/avg.js
CHANGED
|
@@ -8,24 +8,51 @@ export const avg = {
|
|
|
8
8
|
reduceColumn(col) {
|
|
9
9
|
const values = col._values;
|
|
10
10
|
const validity = col.validity;
|
|
11
|
+
let s = 0;
|
|
12
|
+
let n = 0;
|
|
13
|
+
// Fast path: every defined cell is finite (`Float64Column.allFinite`),
|
|
14
|
+
// so every defined cell is a valid contributor — plain accumulate, and
|
|
15
|
+
// the divisor is `definedCount` (or `col.length`). Drops the
|
|
16
|
+
// per-element finite guard the reducer non-finite policy
|
|
17
|
+
// (docs/notes/reducer-nan-policy.md) otherwise requires.
|
|
18
|
+
if (col.allFinite) {
|
|
19
|
+
if (validity === undefined) {
|
|
20
|
+
for (let i = 0; i < col.length; i += 1)
|
|
21
|
+
s += values[i];
|
|
22
|
+
return col.length === 0 ? undefined : s / col.length;
|
|
23
|
+
}
|
|
24
|
+
const bits = validity.bits;
|
|
25
|
+
for (let i = 0; i < col.length; i += 1) {
|
|
26
|
+
if ((bits[i >> 3] & (1 << (i & 7))) !== 0)
|
|
27
|
+
s += values[i];
|
|
28
|
+
}
|
|
29
|
+
const count = validity.definedCount;
|
|
30
|
+
return count === 0 ? undefined : s / count;
|
|
31
|
+
}
|
|
32
|
+
// Guarded path: divide by the count of *finite* contributors, not
|
|
33
|
+
// `definedCount` — a non-finite cell is skipped per policy, so it must
|
|
34
|
+
// not inflate the divisor.
|
|
11
35
|
if (validity === undefined) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
36
|
+
for (let i = 0; i < col.length; i += 1) {
|
|
37
|
+
const v = values[i];
|
|
38
|
+
if (Number.isFinite(v)) {
|
|
39
|
+
s += v;
|
|
40
|
+
n += 1;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return n === 0 ? undefined : s / n;
|
|
18
44
|
}
|
|
19
|
-
const definedCount = validity.definedCount;
|
|
20
|
-
if (definedCount === 0)
|
|
21
|
-
return undefined;
|
|
22
45
|
const bits = validity.bits;
|
|
23
|
-
let s = 0;
|
|
24
46
|
for (let i = 0; i < col.length; i += 1) {
|
|
25
|
-
if ((bits[i >> 3] & (1 << (i & 7))) !== 0)
|
|
26
|
-
|
|
47
|
+
if ((bits[i >> 3] & (1 << (i & 7))) !== 0) {
|
|
48
|
+
const v = values[i];
|
|
49
|
+
if (Number.isFinite(v)) {
|
|
50
|
+
s += v;
|
|
51
|
+
n += 1;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
27
54
|
}
|
|
28
|
-
return s /
|
|
55
|
+
return n === 0 ? undefined : s / n;
|
|
29
56
|
},
|
|
30
57
|
bucketState() {
|
|
31
58
|
let s = 0;
|
package/dist/reducers/count.js
CHANGED
|
@@ -24,10 +24,36 @@ export const count = {
|
|
|
24
24
|
return defined.length;
|
|
25
25
|
},
|
|
26
26
|
reduceColumn(col) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
//
|
|
30
|
-
|
|
27
|
+
const values = col._values;
|
|
28
|
+
const validity = col.validity;
|
|
29
|
+
// Fast path: every defined cell is finite (`Float64Column.allFinite`),
|
|
30
|
+
// so "defined" and "defined AND finite" coincide — the O(1) shortcut
|
|
31
|
+
// is exact again (`definedCount`, or `col.length` when no bitmap).
|
|
32
|
+
// This is the O(N)→O(1) recovery the non-finite policy cost count
|
|
33
|
+
// (docs/notes/reducer-nan-policy.md).
|
|
34
|
+
if (col.allFinite) {
|
|
35
|
+
return validity === undefined ? col.length : validity.definedCount;
|
|
36
|
+
}
|
|
37
|
+
// Guarded path: O(N) scan, NOT the `definedCount` shortcut — the
|
|
38
|
+
// non-finite policy excludes non-finite cells, and `definedCount`
|
|
39
|
+
// counts them as present, so a defined-but-NaN cell would over-count.
|
|
40
|
+
// Walk and count valid AND finite cells.
|
|
41
|
+
let n = 0;
|
|
42
|
+
if (validity === undefined) {
|
|
43
|
+
for (let i = 0; i < col.length; i += 1) {
|
|
44
|
+
if (Number.isFinite(values[i]))
|
|
45
|
+
n += 1;
|
|
46
|
+
}
|
|
47
|
+
return n;
|
|
48
|
+
}
|
|
49
|
+
const bits = validity.bits;
|
|
50
|
+
for (let i = 0; i < col.length; i += 1) {
|
|
51
|
+
if ((bits[i >> 3] & (1 << (i & 7))) !== 0 &&
|
|
52
|
+
Number.isFinite(values[i])) {
|
|
53
|
+
n += 1;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return n;
|
|
31
57
|
},
|
|
32
58
|
bucketState() {
|
|
33
59
|
let n = 0;
|
package/dist/reducers/index.js
CHANGED
|
@@ -41,6 +41,38 @@ export function resolveReducer(operation) {
|
|
|
41
41
|
return topReducer(n);
|
|
42
42
|
throw new TypeError(`unsupported aggregate reducer: ${operation}`);
|
|
43
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* Non-finite numerics (`NaN` / `±Infinity`) are treated as **missing** by
|
|
46
|
+
* every built-in reducer — the reducer non-finite policy
|
|
47
|
+
* (docs/notes/reducer-nan-policy.md). Row intake keeps user data finite; these
|
|
48
|
+
* values only arise inside computed columns (e.g. `cumulative` overflow) or
|
|
49
|
+
* trusted construction, and skipping them keeps every reducer consistent
|
|
50
|
+
* across all four execution paths. Mapping to `undefined` here lets the
|
|
51
|
+
* existing skip-missing logic in each incremental state do the work — so the
|
|
52
|
+
* policy holds for both the batch and live incremental paths without touching
|
|
53
|
+
* any reducer body. Custom-function reducers are intentionally NOT wrapped:
|
|
54
|
+
* they receive values as-is (the escape hatch decides its own semantics).
|
|
55
|
+
*/
|
|
56
|
+
function finiteOrMissing(value) {
|
|
57
|
+
return typeof value === 'number' && !Number.isFinite(value)
|
|
58
|
+
? undefined
|
|
59
|
+
: value;
|
|
60
|
+
}
|
|
61
|
+
/** Wrap a bucket state so non-finite numerics are skipped. {@link finiteOrMissing} */
|
|
62
|
+
function skipNonFiniteBucket(state) {
|
|
63
|
+
return {
|
|
64
|
+
add: (value) => state.add(finiteOrMissing(value)),
|
|
65
|
+
snapshot: () => state.snapshot(),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/** Wrap a rolling state so non-finite numerics are skipped. {@link finiteOrMissing} */
|
|
69
|
+
function skipNonFiniteRolling(state) {
|
|
70
|
+
return {
|
|
71
|
+
add: (index, value) => state.add(index, finiteOrMissing(value)),
|
|
72
|
+
remove: (index, value) => state.remove(index, finiteOrMissing(value)),
|
|
73
|
+
snapshot: () => state.snapshot(),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
44
76
|
/**
|
|
45
77
|
* Build an `AggregateBucketState` for a reducer that may be either a
|
|
46
78
|
* built-in name (string) or a custom function. Built-ins use their
|
|
@@ -52,7 +84,7 @@ export function resolveReducer(operation) {
|
|
|
52
84
|
*/
|
|
53
85
|
export function bucketStateFor(reducer) {
|
|
54
86
|
if (typeof reducer === 'string') {
|
|
55
|
-
return resolveReducer(reducer).bucketState();
|
|
87
|
+
return skipNonFiniteBucket(resolveReducer(reducer).bucketState());
|
|
56
88
|
}
|
|
57
89
|
// Custom-function adapter: buffer values, call fn at snapshot time.
|
|
58
90
|
const items = [];
|
|
@@ -90,7 +122,7 @@ export function bucketStateFor(reducer) {
|
|
|
90
122
|
*/
|
|
91
123
|
export function rollingStateFor(reducer) {
|
|
92
124
|
if (typeof reducer === 'string') {
|
|
93
|
-
return resolveReducer(reducer).rollingState();
|
|
125
|
+
return skipNonFiniteRolling(resolveReducer(reducer).rollingState());
|
|
94
126
|
}
|
|
95
127
|
// Custom-function adapter: Map keyed by event index for O(1) remove.
|
|
96
128
|
const items = new Map();
|
package/dist/reducers/max.js
CHANGED
|
@@ -7,20 +7,46 @@ export const max = {
|
|
|
7
7
|
: numeric.reduce((a, b) => (a >= b ? a : b));
|
|
8
8
|
},
|
|
9
9
|
reduceColumn(col) {
|
|
10
|
-
// **NaN parity with row API.** Mirror the row-API expression
|
|
11
|
-
// numeric.reduce((a, b) => a >= b ? a : b)
|
|
12
|
-
// exactly. See `min.ts` for the full rationale. Closed Codex
|
|
13
|
-
// review finding on PR #153.
|
|
14
10
|
const values = col._values;
|
|
15
11
|
const validity = col.validity;
|
|
16
12
|
let hi;
|
|
17
|
-
|
|
18
|
-
|
|
13
|
+
// Fast path: every defined cell is finite (`Float64Column.allFinite`),
|
|
14
|
+
// so we seed `hi` from the first defined cell and run a plain `v > hi`
|
|
15
|
+
// compare with NO per-element `Number.isFinite` guard and NO in-loop
|
|
16
|
+
// `hi === undefined` check (the seed hoists it out). No NaN to mishandle,
|
|
17
|
+
// also sidesteps the position-dependent `a>=b?a:b` extremum bug the policy
|
|
18
|
+
// fixed (docs/notes/reducer-nan-policy.md). The pre-policy column loop,
|
|
19
|
+
// recovered.
|
|
20
|
+
if (col.allFinite) {
|
|
21
|
+
const len = col.length;
|
|
22
|
+
if (len === 0)
|
|
19
23
|
return undefined;
|
|
20
|
-
|
|
21
|
-
|
|
24
|
+
if (validity === undefined) {
|
|
25
|
+
hi = values[0];
|
|
26
|
+
for (let i = 1; i < len; i += 1) {
|
|
27
|
+
const v = values[i];
|
|
28
|
+
if (v > hi)
|
|
29
|
+
hi = v;
|
|
30
|
+
}
|
|
31
|
+
return hi;
|
|
32
|
+
}
|
|
33
|
+
const bits = validity.bits;
|
|
34
|
+
for (let i = 0; i < len; i += 1) {
|
|
35
|
+
if ((bits[i >> 3] & (1 << (i & 7))) === 0)
|
|
36
|
+
continue;
|
|
37
|
+
const v = values[i];
|
|
38
|
+
if (hi === undefined || v > hi)
|
|
39
|
+
hi = v;
|
|
40
|
+
}
|
|
41
|
+
return hi;
|
|
42
|
+
}
|
|
43
|
+
// Guarded path: skip non-finite cells (reducer non-finite policy) —
|
|
44
|
+
// matches `bucketState`'s `v > hi`.
|
|
45
|
+
if (validity === undefined) {
|
|
46
|
+
for (let i = 0; i < col.length; i += 1) {
|
|
22
47
|
const v = values[i];
|
|
23
|
-
|
|
48
|
+
if (Number.isFinite(v) && (hi === undefined || v > hi))
|
|
49
|
+
hi = v;
|
|
24
50
|
}
|
|
25
51
|
return hi;
|
|
26
52
|
}
|
|
@@ -29,7 +55,8 @@ export const max = {
|
|
|
29
55
|
if ((bits[i >> 3] & (1 << (i & 7))) === 0)
|
|
30
56
|
continue;
|
|
31
57
|
const v = values[i];
|
|
32
|
-
|
|
58
|
+
if (Number.isFinite(v) && (hi === undefined || v > hi))
|
|
59
|
+
hi = v;
|
|
33
60
|
}
|
|
34
61
|
return hi;
|
|
35
62
|
},
|
package/dist/reducers/min.js
CHANGED
|
@@ -7,33 +7,46 @@ export const min = {
|
|
|
7
7
|
: numeric.reduce((a, b) => (a <= b ? a : b));
|
|
8
8
|
},
|
|
9
9
|
reduceColumn(col) {
|
|
10
|
-
// **NaN parity with row API.** The row-API path uses
|
|
11
|
-
// numeric.reduce((a, b) => a <= b ? a : b)
|
|
12
|
-
// which has surprising NaN behavior: on `[1, NaN, 2]` it returns
|
|
13
|
-
// `2` because the first `1 <= NaN` is false (returning NaN),
|
|
14
|
-
// then `NaN <= 2` is also false (returning 2). The "natural"
|
|
15
|
-
// column-side loop `if (v < lo) lo = v` would instead return
|
|
16
|
-
// `1` (NaN comparisons always false → NaN is skipped). The two
|
|
17
|
-
// paths diverge on NaN-bearing input.
|
|
18
|
-
//
|
|
19
|
-
// We mirror the row-API comparison expression exactly to
|
|
20
|
-
// preserve the parity claim, even though both paths exhibit
|
|
21
|
-
// surprising results on NaN (which can only reach a `kind:
|
|
22
|
-
// 'number'` column via trusted construction — `assertCellKind`
|
|
23
|
-
// rejects it at public intake). Closed Codex review finding on
|
|
24
|
-
// PR #153. A principled "filter NaN consistently across both
|
|
25
|
-
// paths" fix is a separate concern tracked in the followup
|
|
26
|
-
// issue.
|
|
27
10
|
const values = col._values;
|
|
28
11
|
const validity = col.validity;
|
|
29
12
|
let lo;
|
|
30
|
-
|
|
31
|
-
|
|
13
|
+
// Fast path: every defined cell is finite (`Float64Column.allFinite`),
|
|
14
|
+
// so we seed `lo` from the first defined cell and run a plain `v < lo`
|
|
15
|
+
// compare with NO per-element `Number.isFinite` guard and NO in-loop
|
|
16
|
+
// `lo === undefined` check (the seed hoists it out). There is no NaN to
|
|
17
|
+
// mishandle, so this also sidesteps the position-dependent `a<=b?a:b`
|
|
18
|
+
// extremum bug the policy fixed (docs/notes/reducer-nan-policy.md). This
|
|
19
|
+
// is the pre-policy column loop, recovered.
|
|
20
|
+
if (col.allFinite) {
|
|
21
|
+
const len = col.length;
|
|
22
|
+
if (len === 0)
|
|
32
23
|
return undefined;
|
|
33
|
-
|
|
34
|
-
|
|
24
|
+
if (validity === undefined) {
|
|
25
|
+
lo = values[0];
|
|
26
|
+
for (let i = 1; i < len; i += 1) {
|
|
27
|
+
const v = values[i];
|
|
28
|
+
if (v < lo)
|
|
29
|
+
lo = v;
|
|
30
|
+
}
|
|
31
|
+
return lo;
|
|
32
|
+
}
|
|
33
|
+
const bits = validity.bits;
|
|
34
|
+
for (let i = 0; i < len; i += 1) {
|
|
35
|
+
if ((bits[i >> 3] & (1 << (i & 7))) === 0)
|
|
36
|
+
continue;
|
|
37
|
+
const v = values[i];
|
|
38
|
+
if (lo === undefined || v < lo)
|
|
39
|
+
lo = v;
|
|
40
|
+
}
|
|
41
|
+
return lo;
|
|
42
|
+
}
|
|
43
|
+
// Guarded path: skip non-finite cells (reducer non-finite policy) —
|
|
44
|
+
// matches `bucketState`'s `v < lo`.
|
|
45
|
+
if (validity === undefined) {
|
|
46
|
+
for (let i = 0; i < col.length; i += 1) {
|
|
35
47
|
const v = values[i];
|
|
36
|
-
|
|
48
|
+
if (Number.isFinite(v) && (lo === undefined || v < lo))
|
|
49
|
+
lo = v;
|
|
37
50
|
}
|
|
38
51
|
return lo;
|
|
39
52
|
}
|
|
@@ -42,7 +55,8 @@ export const min = {
|
|
|
42
55
|
if ((bits[i >> 3] & (1 << (i & 7))) === 0)
|
|
43
56
|
continue;
|
|
44
57
|
const v = values[i];
|
|
45
|
-
|
|
58
|
+
if (Number.isFinite(v) && (lo === undefined || v < lo))
|
|
59
|
+
lo = v;
|
|
46
60
|
}
|
|
47
61
|
return lo;
|
|
48
62
|
},
|