pond-ts 0.28.0 → 0.29.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 CHANGED
@@ -7,7 +7,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
7
7
  file covers both packages. Pre-1.0: minor bumps may include new features and
8
8
  type-level changes; patch bumps are strictly additive.
9
9
 
10
- [Unreleased]: https://github.com/pjm17971/pond-ts/compare/v0.28.0...HEAD
10
+ [Unreleased]: https://github.com/pjm17971/pond-ts/compare/v0.29.0...HEAD
11
+ [0.29.0]: https://github.com/pjm17971/pond-ts/compare/v0.28.0...v0.29.0
11
12
  [0.28.0]: https://github.com/pjm17971/pond-ts/compare/v0.27.0...v0.28.0
12
13
  [0.27.0]: https://github.com/pjm17971/pond-ts/compare/v0.26.0...v0.27.0
13
14
  [0.26.0]: https://github.com/pjm17971/pond-ts/compare/v0.25.0...v0.26.0
@@ -22,6 +23,35 @@ type-level changes; patch bumps are strictly additive.
22
23
 
23
24
  ## [Unreleased]
24
25
 
26
+ ## [0.29.0] — 2026-06-17
27
+
28
+ ### Added
29
+
30
+ - **`byColumn({ edges, inclusive })`** — `inclusive: '(]'` makes edge bins
31
+ upper-inclusive (`(eᵢ, eᵢ₊₁]`), for Coggan power / HR zones where a sample on a
32
+ zone's top edge belongs to the lower zone (the first edge becomes an exclusive
33
+ floor). Defaults to `'[)'` (unchanged — lower-inclusive `[eᵢ, eᵢ₊₁)`). (estela
34
+ F-geo-2 zone inclusivity.)
35
+ - **`'mean'` reducer alias for `'avg'`** — `'mean'` is now an accepted built-in
36
+ reducer name across `aggregate` / `rolling` / `byColumn` / `rollingByColumn` /
37
+ `reduce` (and the live equivalents), at **both runtime and the type level**: it
38
+ resolves to the `avg` kernel and classifies as numeric output
39
+ (`number | undefined`), exactly like `'avg'`. Matches the column API's
40
+ `Float64Column.mean()`. (estela F-reducer-naming.)
41
+
42
+ ### Fixed
43
+
44
+ - **`RowForSchema` honors `required: false`** — a **value** column declared
45
+ `required: false` now accepts `undefined` in its tuple-row cell at the type
46
+ level (matching the runtime, which records it as missing), so optional cells no
47
+ longer need an `as never` cast. The **key (first) column stays required** even
48
+ if marked `required: false` (the constructor always requires it). `null` is
49
+ still not admitted for tuple rows (only the JSON object-row path takes `null`).
50
+ Correspondingly, **`.rows` / `toRows()` now type an optional cell as
51
+ `… | undefined`** (`NormalizedRowForSchema`), so reading a possibly-missing
52
+ cell is no longer unsoundly typed as present — a type tightening on output for
53
+ schemas that use `required: false`. (estela F-geo-row-optional; Codex-hardened.)
54
+
25
55
  ## [0.28.0] — 2026-06-17
26
56
 
27
57
  ### Added
@@ -11,6 +11,7 @@ export type BinSpec = {
11
11
  origin?: number;
12
12
  } | {
13
13
  edges: readonly number[];
14
+ inclusive?: '[)' | '(]';
14
15
  };
15
16
  /** One value-bin's record: its `[start, end)` range plus the mapped aggregates. */
16
17
  export type BinRecord = {
@@ -45,21 +45,45 @@ export function computeByColumn(store, binColName, spec, columnSpecs) {
45
45
  }
46
46
  }
47
47
  const last = edges.length - 1;
48
- binOf = (v) => {
49
- if (v < edges[0] || v >= edges[last])
50
- return NaN; // out of range drop
51
- // rightmost edge <= v, clamped to a valid bin [0, last)
52
- let lo = 0;
53
- let hi = last; // bins are [0, last)
54
- while (lo < hi) {
55
- const mid = (lo + hi + 1) >>> 1;
56
- if (edges[mid] <= v)
57
- lo = mid;
58
- else
59
- hi = mid - 1;
60
- }
61
- return lo;
62
- };
48
+ // `inclusive` picks the bin a value exactly on an interior edge falls into.
49
+ // `'[)'` (default): bins are `[eᵢ, eᵢ₊₁)`, lower-inclusive a boundary value
50
+ // goes to the bin ABOVE; range is `[e₀, eₙ)`. `'(]'`: bins are `(eᵢ, eᵢ₊₁]`,
51
+ // upper-inclusive (Coggan power/HR zones a sample at exactly a zone's top
52
+ // edge is the LOWER zone) — a boundary value goes to the bin BELOW; range is
53
+ // `(e₀, eₙ]`, so the first edge is an exclusive floor (set it below your
54
+ // minimum to keep the minimum in bin 0).
55
+ binOf =
56
+ spec.inclusive === '(]'
57
+ ? (v) => {
58
+ if (v <= edges[0] || v > edges[last])
59
+ return NaN; // out of (e₀, eₙ]
60
+ // rightmost edge STRICTLY less than v, clamped to a valid bin [0, last)
61
+ let lo = 0;
62
+ let hi = last;
63
+ while (lo < hi) {
64
+ const mid = (lo + hi + 1) >>> 1;
65
+ if (edges[mid] < v)
66
+ lo = mid;
67
+ else
68
+ hi = mid - 1;
69
+ }
70
+ return lo;
71
+ }
72
+ : (v) => {
73
+ if (v < edges[0] || v >= edges[last])
74
+ return NaN; // out of [e₀, eₙ)
75
+ // rightmost edge <= v, clamped to a valid bin [0, last)
76
+ let lo = 0;
77
+ let hi = last;
78
+ while (lo < hi) {
79
+ const mid = (lo + hi + 1) >>> 1;
80
+ if (edges[mid] <= v)
81
+ lo = mid;
82
+ else
83
+ hi = mid - 1;
84
+ }
85
+ return lo;
86
+ };
63
87
  rangeOf = (i) => ({ start: edges[i], end: edges[i + 1] });
64
88
  }
65
89
  else {
@@ -539,8 +539,11 @@ export declare class TimeSeries<S extends SeriesSchema> {
539
539
  * source (cumulative distance / work) yields contiguous ranges (per-km
540
540
  * splits, elevation-vs-distance profile); a non-monotonic source (power)
541
541
  * yields a histogram (distribution).
542
- * - `{ edges }` — explicit ascending edges `[e₀ … eₙ]` → `n` bins, bin `i` =
543
- * `[eᵢ, eᵢ₊₁)` (e.g. FTP / Coggan power zones). Always emits all `n` bins.
542
+ * - `{ edges, inclusive? }` — explicit ascending edges `[e₀ … eₙ]` → `n` bins.
543
+ * `inclusive` defaults to `'[)'` (bin `i` = `[eᵢ, eᵢ₊₁)`, lower-inclusive);
544
+ * pass `'(]'` for upper-inclusive bins (`(eᵢ, eᵢ₊₁]`) — Coggan power / HR
545
+ * zones, where a sample exactly on a zone's top edge belongs to the lower
546
+ * zone (the first edge becomes an exclusive floor). Always emits all `n` bins.
544
547
  *
545
548
  * A row whose bin value is missing / non-finite (or, for `edges`, outside
546
549
  * `[e₀, eₙ)`) contributes to no bin. The reducer non-finite policy still
@@ -872,6 +872,10 @@ export class TimeSeries {
872
872
  }
873
873
  /** Example: `series.rows`. Returns the normalized row view of the series. */
874
874
  get rows() {
875
+ // `toRows` returns runtime-normalized rows (Time/Interval keys, `undefined`
876
+ // for missing cells); the double cast is the existing trust point — the
877
+ // `RowForSchema`→`NormalizedRowForSchema` conditional types no longer overlap
878
+ // structurally for a direct cast now that both honor `required: false`.
875
879
  return toRows(this.schema, this.events);
876
880
  }
877
881
  /** Example: `series.toRows()`. Returns normalized row arrays using `Time`/`TimeRange`/`Interval` keys and `undefined` for missing payload values. */
@@ -1391,8 +1395,11 @@ export class TimeSeries {
1391
1395
  * source (cumulative distance / work) yields contiguous ranges (per-km
1392
1396
  * splits, elevation-vs-distance profile); a non-monotonic source (power)
1393
1397
  * yields a histogram (distribution).
1394
- * - `{ edges }` — explicit ascending edges `[e₀ … eₙ]` → `n` bins, bin `i` =
1395
- * `[eᵢ, eᵢ₊₁)` (e.g. FTP / Coggan power zones). Always emits all `n` bins.
1398
+ * - `{ edges, inclusive? }` — explicit ascending edges `[e₀ … eₙ]` → `n` bins.
1399
+ * `inclusive` defaults to `'[)'` (bin `i` = `[eᵢ, eᵢ₊₁)`, lower-inclusive);
1400
+ * pass `'(]'` for upper-inclusive bins (`(eᵢ, eᵢ₊₁]`) — Coggan power / HR
1401
+ * zones, where a sample exactly on a zone's top edge belongs to the lower
1402
+ * zone (the first edge becomes an exclusive floor). Always emits all `n` bins.
1396
1403
  *
1397
1404
  * A row whose bin value is missing / non-finite (or, for `edges`, outside
1398
1405
  * `[e₀, eₙ)`) contributes to no bin. The reducer non-finite policy still
@@ -33,6 +33,11 @@ export function resolveReducer(operation) {
33
33
  const r = registry[operation];
34
34
  if (r)
35
35
  return r;
36
+ // `'mean'` is an accepted alias for `'avg'` in aggregate/rolling mappings,
37
+ // matching the column API (`Float64Column.mean()` → the `'avg'` kernel). Keeps
38
+ // the registry free of a duplicate entry. (estela F-reducer-naming.)
39
+ if (operation === 'mean')
40
+ return avg;
36
41
  const q = parsePercentile(operation);
37
42
  if (q !== undefined)
38
43
  return percentileReducer(q);
@@ -1,5 +1,5 @@
1
1
  import type { AppendColumn, ArrayColumnNameForSchema, ColumnDef, ColumnValue, OptionalizeColumns, ReplaceColumnKind, ScalarKind, SeriesSchema, ValueColumn, ValueColumnsForSchema } from './series.js';
2
- export type AggregateFunction = 'sum' | 'avg' | 'min' | 'max' | 'count' | 'first' | 'last' | 'median' | 'stdev' | 'difference' | 'keep' | 'unique' | 'samples' | `p${number}` | `top${number}`;
2
+ export type AggregateFunction = 'sum' | 'avg' | 'mean' | 'min' | 'max' | 'count' | 'first' | 'last' | 'median' | 'stdev' | 'difference' | 'keep' | 'unique' | 'samples' | `p${number}` | `top${number}`;
3
3
  /**
4
4
  * Custom aggregate reducers receive every value in a bucket (including
5
5
  * `undefined`) and return a single result. The return type is widened to
@@ -152,7 +152,7 @@ type UnifiedOutputKind<Columns extends readonly ValueColumn[], K extends string,
152
152
  * reducer function (or otherwise unrecognized) — the spec branch passes
153
153
  * `ScalarKind`, the shorthand branch passes the source column's kind.
154
154
  */
155
- type ReducerOutputKind<Columns extends readonly ValueColumn[], From extends string, Using, Fallback extends ScalarKind> = Using extends 'sum' | 'avg' | 'count' | 'min' | 'max' | 'median' | 'stdev' | 'difference' | `p${number}` ? 'number' : Using extends 'unique' | 'samples' | `top${number}` ? 'array' : Using extends 'first' | 'last' | 'keep' ? From extends Columns[number]['name'] ? ColumnKindByName<Columns, From> : ScalarKind : Fallback;
155
+ type ReducerOutputKind<Columns extends readonly ValueColumn[], From extends string, Using, Fallback extends ScalarKind> = Using extends 'sum' | 'avg' | 'mean' | 'count' | 'min' | 'max' | 'median' | 'stdev' | 'difference' | `p${number}` ? 'number' : Using extends 'unique' | 'samples' | `top${number}` ? 'array' : Using extends 'first' | 'last' | 'keep' ? From extends Columns[number]['name'] ? ColumnKindByName<Columns, From> : ScalarKind : Fallback;
156
156
  /**
157
157
  * Union of typed `ColumnDef`s — one per **output key** in the mapping
158
158
  * (not per source column). Used as the `...Rest` of the schema tuple;
@@ -209,7 +209,7 @@ export type AggregateOutputMapResultSchema<S extends SeriesSchema, Mapping> = Ag
209
209
  * Aggregate functions that always produce a numeric result regardless of
210
210
  * source column kind. Matches the reducer registry's `outputKind: 'number'`.
211
211
  */
212
- type NumericAggregateFunction = 'sum' | 'avg' | 'count' | 'min' | 'max' | 'median' | 'stdev' | 'difference' | `p${number}`;
212
+ type NumericAggregateFunction = 'sum' | 'avg' | 'mean' | 'count' | 'min' | 'max' | 'median' | 'stdev' | 'difference' | `p${number}`;
213
213
  /**
214
214
  * Output column kind for `arrayAggregate(col, reducer, { kind? })`.
215
215
  * Numeric reducers → `'number'`, `'unique'` → `'array'`, `'first'`/`'last'`/
@@ -20,7 +20,9 @@ export type TimeSeriesInput<S extends SeriesSchema> = {
20
20
  sort?: boolean;
21
21
  };
22
22
  export type NormalizedRowForSchema<S extends readonly ColumnDef<string, string>[]> = {
23
- [I in keyof S]: S[I] extends ColumnDef<any, infer K> ? NormalizedValueForKind<K> : never;
23
+ [I in keyof S]: S[I] extends ColumnDef<any, infer K> ? I extends '0' ? NormalizedValueForKind<K> : S[I] extends {
24
+ required: false;
25
+ } ? NormalizedValueForKind<K> | undefined : NormalizedValueForKind<K> : never;
24
26
  };
25
27
  type DataValueForColumn<C extends ColumnDef<string, string>> = C extends ColumnDef<any, infer K> ? C['required'] extends false ? NormalizedValueForKind<K> | undefined : NormalizedValueForKind<K> : never;
26
28
  type NormalizedDataValueForColumn<C extends ColumnDef<string, string>> = C extends ColumnDef<any, infer K> ? K extends FirstColKind ? EventKeyForKind<K> : C['required'] extends false ? NormalizedValueForKind<K> | undefined : NormalizedValueForKind<K> : never;
@@ -73,7 +73,7 @@ export type ReduceResult<S extends SeriesSchema, Mapping> = {
73
73
  * and shorthand branches. `From` is the spec's `from` or the shorthand
74
74
  * key; `Using` is the reducer.
75
75
  */
76
- type ReduceValueForReducer<S extends SeriesSchema, From extends string, Using> = Using extends 'sum' | 'avg' | 'count' | 'min' | 'max' | 'median' | 'stdev' | 'difference' | `p${number}` ? number | undefined : Using extends 'unique' | 'samples' | `top${number}` ? From extends ValueColumnsForSchema<S>[number]['name'] ? ColumnByName<S, From>['kind'] extends 'number' ? ReadonlyArray<number> | undefined : ColumnByName<S, From>['kind'] extends 'string' ? ReadonlyArray<string> | undefined : ColumnByName<S, From>['kind'] extends 'boolean' ? ReadonlyArray<boolean> | undefined : ReadonlyArray<string | number | boolean> | undefined : ReadonlyArray<string | number | boolean> | undefined : Using extends 'first' | 'last' | 'keep' ? From extends ValueColumnsForSchema<S>[number]['name'] ? ColumnByName<S, From>['kind'] extends 'number' ? number | undefined : ColumnByName<S, From>['kind'] extends 'string' ? string | undefined : ColumnByName<S, From>['kind'] extends 'boolean' ? boolean | undefined : ColumnValue | undefined : ColumnValue | undefined : ColumnValue | undefined;
76
+ type ReduceValueForReducer<S extends SeriesSchema, From extends string, Using> = Using extends 'sum' | 'avg' | 'mean' | 'count' | 'min' | 'max' | 'median' | 'stdev' | 'difference' | `p${number}` ? number | undefined : Using extends 'unique' | 'samples' | `top${number}` ? From extends ValueColumnsForSchema<S>[number]['name'] ? ColumnByName<S, From>['kind'] extends 'number' ? ReadonlyArray<number> | undefined : ColumnByName<S, From>['kind'] extends 'string' ? ReadonlyArray<string> | undefined : ColumnByName<S, From>['kind'] extends 'boolean' ? ReadonlyArray<boolean> | undefined : ReadonlyArray<string | number | boolean> | undefined : ReadonlyArray<string | number | boolean> | undefined : Using extends 'first' | 'last' | 'keep' ? From extends ValueColumnsForSchema<S>[number]['name'] ? ColumnByName<S, From>['kind'] extends 'number' ? number | undefined : ColumnByName<S, From>['kind'] extends 'string' ? string | undefined : ColumnByName<S, From>['kind'] extends 'boolean' ? boolean | undefined : ColumnValue | undefined : ColumnValue | undefined : ColumnValue | undefined;
77
77
  /**
78
78
  * Value type for an explicit `kind` on a reduce spec. Mirrors
79
79
  * `NormalizedValueForKind` but stays inline here for the same TS2394
@@ -90,7 +90,7 @@ type FusedColumnKind<S extends SeriesSchema, Key extends string, Value> = Value
90
90
  * Reducer-string-to-output-kind dispatch, shared by both AggregateMap
91
91
  * and AggregateOutputMap entry shapes.
92
92
  */
93
- type FusedReducerKind<S extends SeriesSchema, From extends string, Using> = Using extends 'sum' | 'avg' | 'count' | 'min' | 'max' | 'median' | 'stdev' | 'difference' | `p${number}` ? 'number' : Using extends 'unique' | 'samples' | `top${number}` ? 'array' : Using extends 'first' | 'last' | 'keep' ? FromColumnKind<S, From> : ScalarKind;
93
+ type FusedReducerKind<S extends SeriesSchema, From extends string, Using> = Using extends 'sum' | 'avg' | 'mean' | 'count' | 'min' | 'max' | 'median' | 'stdev' | 'difference' | `p${number}` ? 'number' : Using extends 'unique' | 'samples' | `top${number}` ? 'array' : Using extends 'first' | 'last' | 'keep' ? FromColumnKind<S, From> : ScalarKind;
94
94
  /**
95
95
  * Look up the kind of a value column by name. Used by source-
96
96
  * preserving reducers (`first` / `last` / `keep`) where the output
@@ -22,6 +22,14 @@ export type ColumnDef<Name extends string, Kind extends string> = {
22
22
  kind: Kind;
23
23
  required?: boolean;
24
24
  };
25
+ /**
26
+ * The key (first) column of a schema. Its **name must equal its kind** —
27
+ * `time` / `timeRange` / `interval`. So `{ name: 'time', kind: 'time' }` is the
28
+ * only valid time key; `{ name: 'at', kind: 'time' }` does **not** typecheck
29
+ * (the error surfaces as a name/literal-type mismatch, e.g. `'"at"' is not
30
+ * assignable to '"time"'` — read it as "the key column must be named for its
31
+ * kind", not as a value error). Value columns, by contrast, take any name.
32
+ */
25
33
  export type FirstColumn = ColumnDef<'time', 'time'> | ColumnDef<'interval', 'interval'> | ColumnDef<'timeRange', 'timeRange'>;
26
34
  export type ValueColumn<Name extends string = string> = ColumnDef<Name, ScalarKind>;
27
35
  export type SeriesSchema = readonly [FirstColumn, ...ValueColumn[]];
@@ -32,8 +40,18 @@ export type ValueColumnsForSchema<S extends SeriesSchema> = S extends readonly [
32
40
  export type ValueForKind<K extends string> = K extends 'time' ? TimestampInput | Time : K extends 'interval' ? IntervalInput | Interval : K extends 'timeRange' ? TimeRangeInput | TimeRange : K extends 'number' ? number : K extends 'string' ? string : K extends 'boolean' ? boolean : K extends 'array' ? ArrayValue : never;
33
41
  export type NormalizedValueForKind<K extends string> = K extends 'time' ? Time : K extends 'timeRange' ? TimeRange : K extends 'interval' ? Interval : K extends 'number' ? number : K extends 'string' ? string : K extends 'boolean' ? boolean : K extends 'array' ? ArrayValue : never;
34
42
  export type KindForValue<V extends ScalarValue> = V extends number ? 'number' : V extends string ? 'string' : 'boolean';
43
+ /**
44
+ * Tuple-row input type for a schema. A column declared `required: false`
45
+ * accepts `undefined` in its cell (a missing value — the constructor records
46
+ * it in the validity bitmap), matching the runtime's intake. `null` is **not**
47
+ * admitted: the tuple-row constructor rejects it (only the JSON object-row path
48
+ * accepts `null`), so the type stays honest to what intake actually takes —
49
+ * pass `undefined` for a missing tuple cell. (estela F-geo-row-optional.)
50
+ */
35
51
  export type RowForSchema<S extends readonly ColumnDef<string, string>[]> = {
36
- [I in keyof S]: S[I] extends ColumnDef<any, infer K> ? ValueForKind<K> : never;
52
+ [I in keyof S]: S[I] extends ColumnDef<any, infer K> ? I extends '0' ? ValueForKind<K> : S[I] extends {
53
+ required: false;
54
+ } ? ValueForKind<K> | undefined : ValueForKind<K> : never;
37
55
  };
38
56
  export type NumericColumnNameForSchema<S extends SeriesSchema> = Extract<ValueColumnsForSchema<S>[number], ColumnDef<string, 'number'>>['name'];
39
57
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pond-ts",
3
- "version": "0.28.0",
3
+ "version": "0.29.0",
4
4
  "description": "TypeScript-first time series primitives",
5
5
  "license": "MIT",
6
6
  "repository": {