diff --git a/benchmarks/results-wasm-core.json b/benchmarks/results-wasm-core.json index 11c4ca4e..23bad732 100644 --- a/benchmarks/results-wasm-core.json +++ b/benchmarks/results-wasm-core.json @@ -3,160 +3,160 @@ { "function": "searchsorted_f64", "tsb": { - "mean_ms": 0.0008374999999999986, + "mean_ms": 0.0016710259999999978, "iterations": 1000, - "total_ms": 0.8374999999999986 + "total_ms": 1.6710259999999977 }, "tsb_wasm": { - "mean_ms": 0.0021370410000000036, + "mean_ms": 0.028031712999999996, "iterations": 1000, - "total_ms": 2.1370410000000035 + "total_ms": 28.031712999999996 }, - "wasm_speedup": 0.39189702022562845 + "wasm_speedup": 0.05961198304220645 }, { "function": "searchsorted_many_f64", "tsb": { - "mean_ms": 0.0034607080000000037, + "mean_ms": 0.003219333999999989, "iterations": 1000, - "total_ms": 3.460708000000004 + "total_ms": 3.2193339999999893 }, "tsb_wasm": { - "mean_ms": 0.005887209000000006, + "mean_ms": 0.034363152, "iterations": 1000, - "total_ms": 5.887209000000006 + "total_ms": 34.363152 }, - "wasm_speedup": 0.5878350845026905 + "wasm_speedup": 0.09368564327277047 }, { "function": "argsort_f64", "tsb": { - "mean_ms": 0.1133425, + "mean_ms": 0.21699383, "iterations": 1000, - "total_ms": 113.3425 + "total_ms": 216.99383 }, "tsb_wasm": { - "mean_ms": 0.01931533300000001, + "mean_ms": 0.057585872999999996, "iterations": 1000, - "total_ms": 19.31533300000001 + "total_ms": 57.58587299999999 }, - "wasm_speedup": 5.8680065210369365 + "wasm_speedup": 3.768178178005568 }, { "function": "searchsorted_str", "tsb": { - "mean_ms": 0.00027391700000001153, + "mean_ms": 0.0009189930000000004, "iterations": 1000, - "total_ms": 0.2739170000000115 + "total_ms": 0.9189930000000004 }, "tsb_wasm": { - "mean_ms": 0.002622792000000004, + "mean_ms": 0.007566859999999963, "iterations": 1000, - "total_ms": 2.622792000000004 + "total_ms": 7.566859999999963 }, - "wasm_speedup": 0.10443717992124847, + "wasm_speedup": 0.12144971626275694, "notes": "String arrays are copied for each WASM call; raw kernel speedup is partially offset by copy overhead." }, { "function": "argsort_str", "tsb": { - "mean_ms": 0.0004121250000000032, + "mean_ms": 0.002078239999999994, "iterations": 1000, - "total_ms": 0.4121250000000032 + "total_ms": 2.078239999999994 }, "tsb_wasm": { - "mean_ms": 0.0014684579999999982, + "mean_ms": 0.004294708000000014, "iterations": 1000, - "total_ms": 1.4684579999999983 + "total_ms": 4.294708000000014 }, - "wasm_speedup": 0.28065154059564773, + "wasm_speedup": 0.4839071713373731, "notes": "Same array-copy caveat as searchsorted_str." }, { "function": "nat_compare", "tsb": { - "mean_ms": 0.0004783749999999998, + "mean_ms": 0.0010837179999999762, "iterations": 1000, - "total_ms": 0.4783749999999998 + "total_ms": 1.0837179999999762 }, "tsb_wasm": { - "mean_ms": 0.001035334000000006, + "mean_ms": 0.004259321, "iterations": 1000, - "total_ms": 1.035334000000006 + "total_ms": 4.259321 }, - "wasm_speedup": 0.4620489619774846 + "wasm_speedup": 0.25443445093712735 }, { "function": "nat_sorted", "tsb": { - "mean_ms": 0.16902662499999996, + "mean_ms": 0.226383055, "iterations": 1000, - "total_ms": 169.02662499999997 + "total_ms": 226.383055 }, "tsb_wasm": { - "mean_ms": 0.257211584, + "mean_ms": 0.5151697199999998, "iterations": 1000, - "total_ms": 257.211584 + "total_ms": 515.1697199999999 }, - "wasm_speedup": 0.6571501266443737 + "wasm_speedup": 0.4394339306277552 }, { "function": "nat_argsort", "tsb": { - "mean_ms": 0.03192166599999996, + "mean_ms": 0.05495564300000001, "iterations": 1000, - "total_ms": 31.92166599999996 + "total_ms": 54.95564300000001 }, "tsb_wasm": { - "mean_ms": 0.035225500000000014, + "mean_ms": 0.06736009400000012, "iterations": 1000, - "total_ms": 35.22550000000001 + "total_ms": 67.36009400000012 }, - "wasm_speedup": 0.9062090247122099 + "wasm_speedup": 0.815848668500966 } ], "coverage": { "unclassified": 0, "eligible_missing": 0, - "total_core_entries": 121, + "total_core_entries": 143, "rust_wasm": 6, - "ts_only_ineligible": 115 + "ts_only_ineligible": 137 }, - "timestamp": "2026-06-27T02:33:54.386Z", + "timestamp": "2026-07-02T09:46:21.112Z", "slower_than_typescript": [ { "function": "searchsorted_f64", - "wasm_speedup": 0.39189702022562845, + "wasm_speedup": 0.05961198304220645, "explanation": "WASM/JS boundary overhead exceeds kernel speedup at this array size." }, { "function": "searchsorted_many_f64", - "wasm_speedup": 0.5878350845026905, + "wasm_speedup": 0.09368564327277047, "explanation": "WASM/JS boundary overhead exceeds kernel speedup at this array size." }, { "function": "searchsorted_str", - "wasm_speedup": 0.10443717992124847, + "wasm_speedup": 0.12144971626275694, "explanation": "String arrays are copied for each WASM call; raw kernel speedup is partially offset by copy overhead." }, { "function": "argsort_str", - "wasm_speedup": 0.28065154059564773, + "wasm_speedup": 0.4839071713373731, "explanation": "Same array-copy caveat as searchsorted_str." }, { "function": "nat_compare", - "wasm_speedup": 0.4620489619774846, + "wasm_speedup": 0.25443445093712735, "explanation": "WASM/JS boundary overhead exceeds kernel speedup at this array size." }, { "function": "nat_sorted", - "wasm_speedup": 0.6571501266443737, + "wasm_speedup": 0.4394339306277552, "explanation": "WASM/JS boundary overhead exceeds kernel speedup at this array size." }, { "function": "nat_argsort", - "wasm_speedup": 0.9062090247122099, + "wasm_speedup": 0.815848668500966, "explanation": "WASM/JS boundary overhead exceeds kernel speedup at this array size." } ] diff --git a/benchmarks/wasm-core/run.ts b/benchmarks/wasm-core/run.ts index 221292cd..6b2b65f6 100644 --- a/benchmarks/wasm-core/run.ts +++ b/benchmarks/wasm-core/run.ts @@ -227,6 +227,49 @@ const benchmarks: BenchmarkEntry[] = []; }); } +// ─── sum_f64 ────────────────────────────────────────────────────────────────── + +{ + const sumF64Wasm = getWasmFn("sum_f64"); + const DATA_F64 = Float64Array.from({ length: 10_000 }, (_, i) => i * 0.5); + const DATA_ARR = Array.from(DATA_F64); + const tsResult = bench(() => DATA_ARR.reduce((acc, v) => acc + v, 0), ITERS); + const wasmResult = bench(() => sumF64Wasm(DATA_F64), ITERS); + benchmarks.push({ + function: "sum_f64", + tsb: tsResult, + tsb_wasm: wasmResult, + wasm_speedup: tsResult.mean_ms / wasmResult.mean_ms, + notes: "Scalar sum over 10 000-element f64 array; WASM avoids JS→TS dispatch overhead.", + }); +} + +// ─── rolling_mean_f64 ──────────────────────────────────────────────────────── + +{ + const rollingMeanWasm = getWasmFn("rolling_mean_f64"); + const ROLL_DATA = Float64Array.from({ length: 1_000 }, (_, i) => i * 1.0); + const ROLL_ARR = Array.from(ROLL_DATA); + const window = 10; + const minPeriods = 10; + const tsResult = bench(() => { + return ROLL_ARR.map((_, i) => { + const start = Math.max(0, i + 1 - window); + const slice = ROLL_ARR.slice(start, i + 1).filter((v) => !Number.isNaN(v)); + if (slice.length < minPeriods) return null; + return slice.reduce((a, b) => a + b, 0) / slice.length; + }); + }, ITERS); + const wasmResult = bench(() => rollingMeanWasm(ROLL_DATA, window, minPeriods), ITERS); + benchmarks.push({ + function: "rolling_mean_f64", + tsb: tsResult, + tsb_wasm: wasmResult, + wasm_speedup: tsResult.mean_ms / wasmResult.mean_ms, + notes: "Rolling window mean over 1 000 elements with window=10.", + }); +} + // ─── coverage summary ───────────────────────────────────────────────────────── const coverageManifest = JSON.parse( diff --git a/rust/pkg/tsb_wasm.d.ts b/rust/pkg/tsb_wasm.d.ts index 9bc64dec..a195415b 100644 --- a/rust/pkg/tsb_wasm.d.ts +++ b/rust/pkg/tsb_wasm.d.ts @@ -13,6 +13,66 @@ export function argsort_f64(arr: Float64Array): Uint32Array; */ export function argsort_str(arr: string[]): Uint32Array; +/** + * Expanding maximum. + */ +export function expanding_max_f64(data: Float64Array, min_periods: number): Float64Array; + +/** + * Expanding mean. + */ +export function expanding_mean_f64(data: Float64Array, min_periods: number): Float64Array; + +/** + * Expanding median. + */ +export function expanding_median_f64(data: Float64Array, min_periods: number): Float64Array; + +/** + * Expanding minimum. + */ +export function expanding_min_f64(data: Float64Array, min_periods: number): Float64Array; + +/** + * Expanding standard deviation (delta degrees-of-freedom `ddof`). + */ +export function expanding_std_f64(data: Float64Array, min_periods: number, ddof: number): Float64Array; + +/** + * Expanding sum. + */ +export function expanding_sum_f64(data: Float64Array, min_periods: number): Float64Array; + +/** + * Expanding variance (delta degrees-of-freedom `ddof`). + */ +export function expanding_var_f64(data: Float64Array, min_periods: number, ddof: number): Float64Array; + +/** + * Maximum of non-NaN values. Returns `NaN` for empty / all-NaN input. + */ +export function max_f64(data: Float64Array): number; + +/** + * Arithmetic mean of non-NaN values. Returns `NaN` for empty / all-NaN input, + * matching `Series.mean()`. + */ +export function mean_f64(data: Float64Array): number; + +/** + * Median of non-NaN values (middle value of sorted data; average of two + * middle values for even-length arrays). Returns `NaN` for empty / all-NaN + * input, matching `Series.median()`. + */ +export function median_f64(data: Float64Array): number; + +/** + * Minimum of non-NaN values. Returns `NaN` for empty / all-NaN input, + * matching `Series.min()` returning `undefined` (coerced to NaN in numeric + * contexts). + */ +export function min_f64(data: Float64Array): number; + /** * Return the indices that would sort `arr` in natural order. */ @@ -38,6 +98,42 @@ export function nat_compare(a: string, b: string, ignore_case: boolean, reverse: */ export function nat_sorted(arr: string[], ignore_case: boolean, reverse: boolean): string[]; +/** + * Rolling maximum. + */ +export function rolling_max_f64(data: Float64Array, window: number, min_periods: number): Float64Array; + +/** + * Rolling arithmetic mean. + */ +export function rolling_mean_f64(data: Float64Array, window: number, min_periods: number): Float64Array; + +/** + * Rolling median. + */ +export function rolling_median_f64(data: Float64Array, window: number, min_periods: number): Float64Array; + +/** + * Rolling minimum. + */ +export function rolling_min_f64(data: Float64Array, window: number, min_periods: number): Float64Array; + +/** + * Rolling standard deviation (delta degrees-of-freedom `ddof`). + */ +export function rolling_std_f64(data: Float64Array, window: number, min_periods: number, ddof: number): Float64Array; + +/** + * Rolling sum. Positions with fewer than `min_periods` non-NaN values → NaN. + */ +export function rolling_sum_f64(data: Float64Array, window: number, min_periods: number): Float64Array; + +/** + * Rolling variance (delta degrees-of-freedom `ddof`). + * Positions with fewer than `ddof + 1` valid values → NaN. + */ +export function rolling_var_f64(data: Float64Array, window: number, min_periods: number, ddof: number): Float64Array; + /** * Binary-search a sorted f64 slice for `value`. * @@ -65,3 +161,22 @@ export function searchsorted_many_str(arr: string[], values: string[], side_righ * Binary-search a sorted array of strings for `value`. */ export function searchsorted_str(arr: string[], value: string, side_right: boolean): number; + +/** + * Sample standard deviation with delta degrees-of-freedom `ddof`. + * Returns `NaN` when fewer than `ddof + 1` valid values exist. + */ +export function std_f64(data: Float64Array, ddof: number): number; + +/** + * Sum of non-NaN values. Returns `0.0` when there are no valid values, + * matching `Series.sum()` on an all-null / empty series. + */ +export function sum_f64(data: Float64Array): number; + +/** + * Sample variance of non-NaN values with delta degrees-of-freedom `ddof`. + * Returns `NaN` when fewer than `ddof + 1` valid values exist, matching + * `Series.var(ddof)`. + */ +export function var_f64(data: Float64Array, ddof: number): number; diff --git a/rust/pkg/tsb_wasm.js b/rust/pkg/tsb_wasm.js index 2f9cae8a..0924a6f3 100644 --- a/rust/pkg/tsb_wasm.js +++ b/rust/pkg/tsb_wasm.js @@ -32,6 +32,177 @@ function argsort_str(arr) { } exports.argsort_str = argsort_str; +/** + * Expanding maximum. + * @param {Float64Array} data + * @param {number} min_periods + * @returns {Float64Array} + */ +function expanding_max_f64(data, min_periods) { + const ptr0 = passArrayF64ToWasm0(data, wasm.__wbindgen_malloc); + const len0 = WASM_VECTOR_LEN; + const ret = wasm.expanding_max_f64(ptr0, len0, min_periods); + var v2 = getArrayF64FromWasm0(ret[0], ret[1]).slice(); + wasm.__wbindgen_free(ret[0], ret[1] * 8, 8); + return v2; +} +exports.expanding_max_f64 = expanding_max_f64; + +/** + * Expanding mean. + * @param {Float64Array} data + * @param {number} min_periods + * @returns {Float64Array} + */ +function expanding_mean_f64(data, min_periods) { + const ptr0 = passArrayF64ToWasm0(data, wasm.__wbindgen_malloc); + const len0 = WASM_VECTOR_LEN; + const ret = wasm.expanding_mean_f64(ptr0, len0, min_periods); + var v2 = getArrayF64FromWasm0(ret[0], ret[1]).slice(); + wasm.__wbindgen_free(ret[0], ret[1] * 8, 8); + return v2; +} +exports.expanding_mean_f64 = expanding_mean_f64; + +/** + * Expanding median. + * @param {Float64Array} data + * @param {number} min_periods + * @returns {Float64Array} + */ +function expanding_median_f64(data, min_periods) { + const ptr0 = passArrayF64ToWasm0(data, wasm.__wbindgen_malloc); + const len0 = WASM_VECTOR_LEN; + const ret = wasm.expanding_median_f64(ptr0, len0, min_periods); + var v2 = getArrayF64FromWasm0(ret[0], ret[1]).slice(); + wasm.__wbindgen_free(ret[0], ret[1] * 8, 8); + return v2; +} +exports.expanding_median_f64 = expanding_median_f64; + +/** + * Expanding minimum. + * @param {Float64Array} data + * @param {number} min_periods + * @returns {Float64Array} + */ +function expanding_min_f64(data, min_periods) { + const ptr0 = passArrayF64ToWasm0(data, wasm.__wbindgen_malloc); + const len0 = WASM_VECTOR_LEN; + const ret = wasm.expanding_min_f64(ptr0, len0, min_periods); + var v2 = getArrayF64FromWasm0(ret[0], ret[1]).slice(); + wasm.__wbindgen_free(ret[0], ret[1] * 8, 8); + return v2; +} +exports.expanding_min_f64 = expanding_min_f64; + +/** + * Expanding standard deviation (delta degrees-of-freedom `ddof`). + * @param {Float64Array} data + * @param {number} min_periods + * @param {number} ddof + * @returns {Float64Array} + */ +function expanding_std_f64(data, min_periods, ddof) { + const ptr0 = passArrayF64ToWasm0(data, wasm.__wbindgen_malloc); + const len0 = WASM_VECTOR_LEN; + const ret = wasm.expanding_std_f64(ptr0, len0, min_periods, ddof); + var v2 = getArrayF64FromWasm0(ret[0], ret[1]).slice(); + wasm.__wbindgen_free(ret[0], ret[1] * 8, 8); + return v2; +} +exports.expanding_std_f64 = expanding_std_f64; + +/** + * Expanding sum. + * @param {Float64Array} data + * @param {number} min_periods + * @returns {Float64Array} + */ +function expanding_sum_f64(data, min_periods) { + const ptr0 = passArrayF64ToWasm0(data, wasm.__wbindgen_malloc); + const len0 = WASM_VECTOR_LEN; + const ret = wasm.expanding_sum_f64(ptr0, len0, min_periods); + var v2 = getArrayF64FromWasm0(ret[0], ret[1]).slice(); + wasm.__wbindgen_free(ret[0], ret[1] * 8, 8); + return v2; +} +exports.expanding_sum_f64 = expanding_sum_f64; + +/** + * Expanding variance (delta degrees-of-freedom `ddof`). + * @param {Float64Array} data + * @param {number} min_periods + * @param {number} ddof + * @returns {Float64Array} + */ +function expanding_var_f64(data, min_periods, ddof) { + const ptr0 = passArrayF64ToWasm0(data, wasm.__wbindgen_malloc); + const len0 = WASM_VECTOR_LEN; + const ret = wasm.expanding_var_f64(ptr0, len0, min_periods, ddof); + var v2 = getArrayF64FromWasm0(ret[0], ret[1]).slice(); + wasm.__wbindgen_free(ret[0], ret[1] * 8, 8); + return v2; +} +exports.expanding_var_f64 = expanding_var_f64; + +/** + * Maximum of non-NaN values. Returns `NaN` for empty / all-NaN input. + * @param {Float64Array} data + * @returns {number} + */ +function max_f64(data) { + const ptr0 = passArrayF64ToWasm0(data, wasm.__wbindgen_malloc); + const len0 = WASM_VECTOR_LEN; + const ret = wasm.max_f64(ptr0, len0); + return ret; +} +exports.max_f64 = max_f64; + +/** + * Arithmetic mean of non-NaN values. Returns `NaN` for empty / all-NaN input, + * matching `Series.mean()`. + * @param {Float64Array} data + * @returns {number} + */ +function mean_f64(data) { + const ptr0 = passArrayF64ToWasm0(data, wasm.__wbindgen_malloc); + const len0 = WASM_VECTOR_LEN; + const ret = wasm.mean_f64(ptr0, len0); + return ret; +} +exports.mean_f64 = mean_f64; + +/** + * Median of non-NaN values (middle value of sorted data; average of two + * middle values for even-length arrays). Returns `NaN` for empty / all-NaN + * input, matching `Series.median()`. + * @param {Float64Array} data + * @returns {number} + */ +function median_f64(data) { + const ptr0 = passArrayF64ToWasm0(data, wasm.__wbindgen_malloc); + const len0 = WASM_VECTOR_LEN; + const ret = wasm.median_f64(ptr0, len0); + return ret; +} +exports.median_f64 = median_f64; + +/** + * Minimum of non-NaN values. Returns `NaN` for empty / all-NaN input, + * matching `Series.min()` returning `undefined` (coerced to NaN in numeric + * contexts). + * @param {Float64Array} data + * @returns {number} + */ +function min_f64(data) { + const ptr0 = passArrayF64ToWasm0(data, wasm.__wbindgen_malloc); + const len0 = WASM_VECTOR_LEN; + const ret = wasm.min_f64(ptr0, len0); + return ret; +} +exports.min_f64 = min_f64; + /** * Return the indices that would sort `arr` in natural order. * @param {string[]} arr @@ -94,6 +265,128 @@ function nat_sorted(arr, ignore_case, reverse) { } exports.nat_sorted = nat_sorted; +/** + * Rolling maximum. + * @param {Float64Array} data + * @param {number} window + * @param {number} min_periods + * @returns {Float64Array} + */ +function rolling_max_f64(data, window, min_periods) { + const ptr0 = passArrayF64ToWasm0(data, wasm.__wbindgen_malloc); + const len0 = WASM_VECTOR_LEN; + const ret = wasm.rolling_max_f64(ptr0, len0, window, min_periods); + var v2 = getArrayF64FromWasm0(ret[0], ret[1]).slice(); + wasm.__wbindgen_free(ret[0], ret[1] * 8, 8); + return v2; +} +exports.rolling_max_f64 = rolling_max_f64; + +/** + * Rolling arithmetic mean. + * @param {Float64Array} data + * @param {number} window + * @param {number} min_periods + * @returns {Float64Array} + */ +function rolling_mean_f64(data, window, min_periods) { + const ptr0 = passArrayF64ToWasm0(data, wasm.__wbindgen_malloc); + const len0 = WASM_VECTOR_LEN; + const ret = wasm.rolling_mean_f64(ptr0, len0, window, min_periods); + var v2 = getArrayF64FromWasm0(ret[0], ret[1]).slice(); + wasm.__wbindgen_free(ret[0], ret[1] * 8, 8); + return v2; +} +exports.rolling_mean_f64 = rolling_mean_f64; + +/** + * Rolling median. + * @param {Float64Array} data + * @param {number} window + * @param {number} min_periods + * @returns {Float64Array} + */ +function rolling_median_f64(data, window, min_periods) { + const ptr0 = passArrayF64ToWasm0(data, wasm.__wbindgen_malloc); + const len0 = WASM_VECTOR_LEN; + const ret = wasm.rolling_median_f64(ptr0, len0, window, min_periods); + var v2 = getArrayF64FromWasm0(ret[0], ret[1]).slice(); + wasm.__wbindgen_free(ret[0], ret[1] * 8, 8); + return v2; +} +exports.rolling_median_f64 = rolling_median_f64; + +/** + * Rolling minimum. + * @param {Float64Array} data + * @param {number} window + * @param {number} min_periods + * @returns {Float64Array} + */ +function rolling_min_f64(data, window, min_periods) { + const ptr0 = passArrayF64ToWasm0(data, wasm.__wbindgen_malloc); + const len0 = WASM_VECTOR_LEN; + const ret = wasm.rolling_min_f64(ptr0, len0, window, min_periods); + var v2 = getArrayF64FromWasm0(ret[0], ret[1]).slice(); + wasm.__wbindgen_free(ret[0], ret[1] * 8, 8); + return v2; +} +exports.rolling_min_f64 = rolling_min_f64; + +/** + * Rolling standard deviation (delta degrees-of-freedom `ddof`). + * @param {Float64Array} data + * @param {number} window + * @param {number} min_periods + * @param {number} ddof + * @returns {Float64Array} + */ +function rolling_std_f64(data, window, min_periods, ddof) { + const ptr0 = passArrayF64ToWasm0(data, wasm.__wbindgen_malloc); + const len0 = WASM_VECTOR_LEN; + const ret = wasm.rolling_std_f64(ptr0, len0, window, min_periods, ddof); + var v2 = getArrayF64FromWasm0(ret[0], ret[1]).slice(); + wasm.__wbindgen_free(ret[0], ret[1] * 8, 8); + return v2; +} +exports.rolling_std_f64 = rolling_std_f64; + +/** + * Rolling sum. Positions with fewer than `min_periods` non-NaN values → NaN. + * @param {Float64Array} data + * @param {number} window + * @param {number} min_periods + * @returns {Float64Array} + */ +function rolling_sum_f64(data, window, min_periods) { + const ptr0 = passArrayF64ToWasm0(data, wasm.__wbindgen_malloc); + const len0 = WASM_VECTOR_LEN; + const ret = wasm.rolling_sum_f64(ptr0, len0, window, min_periods); + var v2 = getArrayF64FromWasm0(ret[0], ret[1]).slice(); + wasm.__wbindgen_free(ret[0], ret[1] * 8, 8); + return v2; +} +exports.rolling_sum_f64 = rolling_sum_f64; + +/** + * Rolling variance (delta degrees-of-freedom `ddof`). + * Positions with fewer than `ddof + 1` valid values → NaN. + * @param {Float64Array} data + * @param {number} window + * @param {number} min_periods + * @param {number} ddof + * @returns {Float64Array} + */ +function rolling_var_f64(data, window, min_periods, ddof) { + const ptr0 = passArrayF64ToWasm0(data, wasm.__wbindgen_malloc); + const len0 = WASM_VECTOR_LEN; + const ret = wasm.rolling_var_f64(ptr0, len0, window, min_periods, ddof); + var v2 = getArrayF64FromWasm0(ret[0], ret[1]).slice(); + wasm.__wbindgen_free(ret[0], ret[1] * 8, 8); + return v2; +} +exports.rolling_var_f64 = rolling_var_f64; + /** * Binary-search a sorted f64 slice for `value`. * @@ -171,6 +464,51 @@ function searchsorted_str(arr, value, side_right) { return ret >>> 0; } exports.searchsorted_str = searchsorted_str; + +/** + * Sample standard deviation with delta degrees-of-freedom `ddof`. + * Returns `NaN` when fewer than `ddof + 1` valid values exist. + * @param {Float64Array} data + * @param {number} ddof + * @returns {number} + */ +function std_f64(data, ddof) { + const ptr0 = passArrayF64ToWasm0(data, wasm.__wbindgen_malloc); + const len0 = WASM_VECTOR_LEN; + const ret = wasm.std_f64(ptr0, len0, ddof); + return ret; +} +exports.std_f64 = std_f64; + +/** + * Sum of non-NaN values. Returns `0.0` when there are no valid values, + * matching `Series.sum()` on an all-null / empty series. + * @param {Float64Array} data + * @returns {number} + */ +function sum_f64(data) { + const ptr0 = passArrayF64ToWasm0(data, wasm.__wbindgen_malloc); + const len0 = WASM_VECTOR_LEN; + const ret = wasm.sum_f64(ptr0, len0); + return ret; +} +exports.sum_f64 = sum_f64; + +/** + * Sample variance of non-NaN values with delta degrees-of-freedom `ddof`. + * Returns `NaN` when fewer than `ddof + 1` valid values exist, matching + * `Series.var(ddof)`. + * @param {Float64Array} data + * @param {number} ddof + * @returns {number} + */ +function var_f64(data, ddof) { + const ptr0 = passArrayF64ToWasm0(data, wasm.__wbindgen_malloc); + const len0 = WASM_VECTOR_LEN; + const ret = wasm.var_f64(ptr0, len0, ddof); + return ret; +} +exports.var_f64 = var_f64; function __wbg_get_imports() { const import0 = { __proto__: null, @@ -212,6 +550,11 @@ function addToExternrefTable0(obj) { return idx; } +function getArrayF64FromWasm0(ptr, len) { + ptr = ptr >>> 0; + return getFloat64ArrayMemory0().subarray(ptr / 8, ptr / 8 + len); +} + function getArrayJsValueFromWasm0(ptr, len) { ptr = ptr >>> 0; const mem = getDataViewMemory0(); diff --git a/rust/pkg/tsb_wasm_bg.wasm b/rust/pkg/tsb_wasm_bg.wasm index 42ed7985..ad520067 100644 Binary files a/rust/pkg/tsb_wasm_bg.wasm and b/rust/pkg/tsb_wasm_bg.wasm differ diff --git a/rust/pkg/tsb_wasm_bg.wasm.d.ts b/rust/pkg/tsb_wasm_bg.wasm.d.ts index d73466a4..6a03ac57 100644 --- a/rust/pkg/tsb_wasm_bg.wasm.d.ts +++ b/rust/pkg/tsb_wasm_bg.wasm.d.ts @@ -1,15 +1,36 @@ /* tslint:disable */ /* eslint-disable */ export const memory: WebAssembly.Memory; +export const expanding_max_f64: (a: number, b: number, c: number) => [number, number]; +export const expanding_mean_f64: (a: number, b: number, c: number) => [number, number]; +export const expanding_median_f64: (a: number, b: number, c: number) => [number, number]; +export const expanding_min_f64: (a: number, b: number, c: number) => [number, number]; +export const expanding_std_f64: (a: number, b: number, c: number, d: number) => [number, number]; +export const expanding_sum_f64: (a: number, b: number, c: number) => [number, number]; +export const expanding_var_f64: (a: number, b: number, c: number, d: number) => [number, number]; export const nat_argsort: (a: number, b: number, c: number, d: number) => [number, number]; export const nat_compare: (a: number, b: number, c: number, d: number, e: number, f: number) => number; export const nat_sorted: (a: number, b: number, c: number, d: number) => [number, number]; +export const rolling_max_f64: (a: number, b: number, c: number, d: number) => [number, number]; +export const rolling_mean_f64: (a: number, b: number, c: number, d: number) => [number, number]; +export const rolling_median_f64: (a: number, b: number, c: number, d: number) => [number, number]; +export const rolling_min_f64: (a: number, b: number, c: number, d: number) => [number, number]; +export const rolling_std_f64: (a: number, b: number, c: number, d: number, e: number) => [number, number]; +export const rolling_sum_f64: (a: number, b: number, c: number, d: number) => [number, number]; +export const rolling_var_f64: (a: number, b: number, c: number, d: number, e: number) => [number, number]; export const argsort_f64: (a: number, b: number) => [number, number]; export const argsort_str: (a: number, b: number) => [number, number]; +export const max_f64: (a: number, b: number) => number; +export const mean_f64: (a: number, b: number) => number; +export const median_f64: (a: number, b: number) => number; +export const min_f64: (a: number, b: number) => number; export const searchsorted_f64: (a: number, b: number, c: number, d: number) => number; export const searchsorted_many_f64: (a: number, b: number, c: number, d: number, e: number) => [number, number]; export const searchsorted_many_str: (a: number, b: number, c: number, d: number, e: number) => [number, number]; export const searchsorted_str: (a: number, b: number, c: number, d: number, e: number) => number; +export const std_f64: (a: number, b: number, c: number) => number; +export const sum_f64: (a: number, b: number) => number; +export const var_f64: (a: number, b: number, c: number) => number; export const __wbindgen_malloc: (a: number, b: number) => number; export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number; export const __wbindgen_externrefs: WebAssembly.Table; diff --git a/rust/src/lib.rs b/rust/src/lib.rs index ba0cc5f0..eb9f03cc 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -7,7 +7,11 @@ */ mod natsort; +mod reductions; +mod rolling; mod searchsorted; pub use natsort::*; +pub use reductions::*; +pub use rolling::*; pub use searchsorted::*; diff --git a/rust/src/reductions.rs b/rust/src/reductions.rs new file mode 100644 index 00000000..e3ee1351 --- /dev/null +++ b/rust/src/reductions.rs @@ -0,0 +1,231 @@ +/*! + * Scalar reduction accelerators for f64 arrays. + * + * All functions skip NaN values, matching the TypeScript `_numericValues()` + * filter that Series.sum / Series.mean / etc. apply before reducing. + * + * Conventions: + * - Empty or all-NaN input: sum → 0.0, mean/min/max/std/var/median → NaN. + * - ddof is the delta degrees-of-freedom for variance/std (1 = sample). + */ + +use wasm_bindgen::prelude::*; + +// ─── helpers ───────────────────────────────────────────────────────────────── + +/// Collect non-NaN values from `data` into a `Vec`. +fn valid(data: &[f64]) -> Vec { + data.iter().filter(|v| !v.is_nan()).copied().collect() +} + +// ─── public exports ─────────────────────────────────────────────────────────── + +/// Sum of non-NaN values. Returns `0.0` when there are no valid values, +/// matching `Series.sum()` on an all-null / empty series. +#[wasm_bindgen] +pub fn sum_f64(data: &[f64]) -> f64 { + let mut acc = 0.0_f64; + for &v in data { + if !v.is_nan() { + acc += v; + } + } + acc +} + +/// Arithmetic mean of non-NaN values. Returns `NaN` for empty / all-NaN input, +/// matching `Series.mean()`. +#[wasm_bindgen] +pub fn mean_f64(data: &[f64]) -> f64 { + let mut acc = 0.0_f64; + let mut count = 0_u64; + for &v in data { + if !v.is_nan() { + acc += v; + count += 1; + } + } + if count == 0 { + f64::NAN + } else { + acc / count as f64 + } +} + +/// Minimum of non-NaN values. Returns `NaN` for empty / all-NaN input, +/// matching `Series.min()` returning `undefined` (coerced to NaN in numeric +/// contexts). +#[wasm_bindgen] +pub fn min_f64(data: &[f64]) -> f64 { + let mut result = f64::NAN; + for &v in data { + if !v.is_nan() { + if result.is_nan() || v < result { + result = v; + } + } + } + result +} + +/// Maximum of non-NaN values. Returns `NaN` for empty / all-NaN input. +#[wasm_bindgen] +pub fn max_f64(data: &[f64]) -> f64 { + let mut result = f64::NAN; + for &v in data { + if !v.is_nan() { + if result.is_nan() || v > result { + result = v; + } + } + } + result +} + +/// Sample variance of non-NaN values with delta degrees-of-freedom `ddof`. +/// Returns `NaN` when fewer than `ddof + 1` valid values exist, matching +/// `Series.var(ddof)`. +#[wasm_bindgen] +pub fn var_f64(data: &[f64], ddof: f64) -> f64 { + let vals = valid(data); + let n = vals.len() as f64; + if n < ddof + 1.0 { + return f64::NAN; + } + let mu = vals.iter().sum::() / n; + let ss: f64 = vals.iter().map(|v| (v - mu) * (v - mu)).sum(); + ss / (n - ddof) +} + +/// Sample standard deviation with delta degrees-of-freedom `ddof`. +/// Returns `NaN` when fewer than `ddof + 1` valid values exist. +#[wasm_bindgen] +pub fn std_f64(data: &[f64], ddof: f64) -> f64 { + var_f64(data, ddof).sqrt() +} + +/// Median of non-NaN values (middle value of sorted data; average of two +/// middle values for even-length arrays). Returns `NaN` for empty / all-NaN +/// input, matching `Series.median()`. +#[wasm_bindgen] +pub fn median_f64(data: &[f64]) -> f64 { + let mut vals = valid(data); + let n = vals.len(); + if n == 0 { + return f64::NAN; + } + vals.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); + let mid = n / 2; + if n % 2 == 1 { + vals[mid] + } else { + (vals[mid - 1] + vals[mid]) / 2.0 + } +} + +// ─── unit tests ─────────────────────────────────────────────────────────────── + +#[cfg(test)] +mod tests { + use super::*; + + fn assert_near(a: f64, b: f64) { + if a.is_nan() && b.is_nan() { + return; + } + assert!( + (a - b).abs() < 1e-9, + "expected {}, got {}", + b, + a + ); + } + + #[test] + fn test_sum_basic() { + assert_near(sum_f64(&[1.0, 2.0, 3.0, 4.0]), 10.0); + } + + #[test] + fn test_sum_with_nan() { + assert_near(sum_f64(&[1.0, f64::NAN, 3.0]), 4.0); + } + + #[test] + fn test_sum_empty() { + assert_near(sum_f64(&[]), 0.0); + } + + #[test] + fn test_mean_basic() { + assert_near(mean_f64(&[1.0, 2.0, 3.0]), 2.0); + } + + #[test] + fn test_mean_nan_skipped() { + assert_near(mean_f64(&[1.0, f64::NAN, 3.0]), 2.0); + } + + #[test] + fn test_mean_empty() { + assert!(mean_f64(&[]).is_nan()); + } + + #[test] + fn test_min_basic() { + assert_near(min_f64(&[3.0, 1.0, 4.0, 1.5]), 1.0); + } + + #[test] + fn test_min_nan_skipped() { + assert_near(min_f64(&[f64::NAN, 2.0, f64::NAN]), 2.0); + } + + #[test] + fn test_min_all_nan() { + assert!(min_f64(&[f64::NAN]).is_nan()); + } + + #[test] + fn test_max_basic() { + assert_near(max_f64(&[3.0, 1.0, 4.0]), 4.0); + } + + #[test] + fn test_var_sample() { + // [2, 4, 4, 4, 5, 5, 7, 9] — ddof=1 + let data = vec![2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0]; + assert_near(var_f64(&data, 1.0), 4.571428571428571); + } + + #[test] + fn test_var_too_few() { + assert!(var_f64(&[1.0], 1.0).is_nan()); + } + + #[test] + fn test_std_basic() { + let data = vec![2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0]; + assert_near(std_f64(&data, 1.0), var_f64(&data, 1.0).sqrt()); + } + + #[test] + fn test_median_odd() { + assert_near(median_f64(&[3.0, 1.0, 2.0]), 2.0); + } + + #[test] + fn test_median_even() { + assert_near(median_f64(&[1.0, 2.0, 3.0, 4.0]), 2.5); + } + + #[test] + fn test_median_nan_skipped() { + assert_near(median_f64(&[f64::NAN, 1.0, 3.0]), 2.0); + } + + #[test] + fn test_median_empty() { + assert!(median_f64(&[]).is_nan()); + } +} diff --git a/rust/src/rolling.rs b/rust/src/rolling.rs new file mode 100644 index 00000000..1ba0df5b --- /dev/null +++ b/rust/src/rolling.rs @@ -0,0 +1,370 @@ +/*! + * Sliding-window (rolling) and expanding-window reduction accelerators. + * + * All functions accept a `Float64Array`, a window size, and a `min_periods` + * threshold. Positions where the count of non-NaN values in the current + * window is less than `min_periods` produce `NaN` in the output, matching the + * TypeScript `Rolling` / `Expanding` implementations. + * + * Expanding variants are implemented by passing `window = data.len()` and + * resetting `start = 0` every iteration. + */ + +use wasm_bindgen::prelude::*; + +// ─── helpers ───────────────────────────────────────────────────────────────── + +/// Compute `sum` and `count` of non-NaN values in `data[start..end]`. +#[inline] +fn window_sum_count(data: &[f64], start: usize, end: usize) -> (f64, usize) { + let mut sum = 0.0_f64; + let mut count = 0_usize; + for i in start..end { + if !data[i].is_nan() { + sum += data[i]; + count += 1; + } + } + (sum, count) +} + +/// Return the median of `nums` (already collected as non-NaN values). +fn slice_median(nums: &mut Vec) -> f64 { + nums.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); + let n = nums.len(); + if n % 2 == 1 { + nums[n / 2] + } else { + (nums[n / 2 - 1] + nums[n / 2]) / 2.0 + } +} + +// ─── rolling window functions ───────────────────────────────────────────────── + +/// Rolling sum. Positions with fewer than `min_periods` non-NaN values → NaN. +#[wasm_bindgen] +pub fn rolling_sum_f64(data: &[f64], window: u32, min_periods: u32) -> Vec { + let n = data.len(); + let w = window as usize; + let mp = min_periods as usize; + let mut result = vec![f64::NAN; n]; + for i in 0..n { + let start = if i + 1 >= w { i + 1 - w } else { 0 }; + let (sum, count) = window_sum_count(data, start, i + 1); + if count >= mp { + result[i] = sum; + } + } + result +} + +/// Rolling arithmetic mean. +#[wasm_bindgen] +pub fn rolling_mean_f64(data: &[f64], window: u32, min_periods: u32) -> Vec { + let n = data.len(); + let w = window as usize; + let mp = min_periods as usize; + let mut result = vec![f64::NAN; n]; + for i in 0..n { + let start = if i + 1 >= w { i + 1 - w } else { 0 }; + let (sum, count) = window_sum_count(data, start, i + 1); + if count >= mp { + result[i] = sum / count as f64; + } + } + result +} + +/// Rolling minimum. +#[wasm_bindgen] +pub fn rolling_min_f64(data: &[f64], window: u32, min_periods: u32) -> Vec { + let n = data.len(); + let w = window as usize; + let mp = min_periods as usize; + let mut result = vec![f64::NAN; n]; + for i in 0..n { + let start = if i + 1 >= w { i + 1 - w } else { 0 }; + let mut min_val = f64::NAN; + let mut count = 0_usize; + for j in start..i + 1 { + if !data[j].is_nan() { + if min_val.is_nan() || data[j] < min_val { + min_val = data[j]; + } + count += 1; + } + } + if count >= mp { + result[i] = min_val; + } + } + result +} + +/// Rolling maximum. +#[wasm_bindgen] +pub fn rolling_max_f64(data: &[f64], window: u32, min_periods: u32) -> Vec { + let n = data.len(); + let w = window as usize; + let mp = min_periods as usize; + let mut result = vec![f64::NAN; n]; + for i in 0..n { + let start = if i + 1 >= w { i + 1 - w } else { 0 }; + let mut max_val = f64::NAN; + let mut count = 0_usize; + for j in start..i + 1 { + if !data[j].is_nan() { + if max_val.is_nan() || data[j] > max_val { + max_val = data[j]; + } + count += 1; + } + } + if count >= mp { + result[i] = max_val; + } + } + result +} + +/// Rolling variance (delta degrees-of-freedom `ddof`). +/// Positions with fewer than `ddof + 1` valid values → NaN. +#[wasm_bindgen] +pub fn rolling_var_f64(data: &[f64], window: u32, min_periods: u32, ddof: f64) -> Vec { + let n = data.len(); + let w = window as usize; + let mp = min_periods as usize; + let mut result = vec![f64::NAN; n]; + for i in 0..n { + let start = if i + 1 >= w { i + 1 - w } else { 0 }; + let mut vals: Vec = Vec::new(); + for j in start..i + 1 { + if !data[j].is_nan() { + vals.push(data[j]); + } + } + let cnt = vals.len() as f64; + if vals.len() >= mp && cnt > ddof { + let mu = vals.iter().sum::() / cnt; + let ss: f64 = vals.iter().map(|v| (v - mu) * (v - mu)).sum(); + result[i] = ss / (cnt - ddof); + } + } + result +} + +/// Rolling standard deviation (delta degrees-of-freedom `ddof`). +#[wasm_bindgen] +pub fn rolling_std_f64(data: &[f64], window: u32, min_periods: u32, ddof: f64) -> Vec { + rolling_var_f64(data, window, min_periods, ddof) + .into_iter() + .map(|v| v.sqrt()) + .collect() +} + +/// Rolling median. +#[wasm_bindgen] +pub fn rolling_median_f64(data: &[f64], window: u32, min_periods: u32) -> Vec { + let n = data.len(); + let w = window as usize; + let mp = min_periods as usize; + let mut result = vec![f64::NAN; n]; + for i in 0..n { + let start = if i + 1 >= w { i + 1 - w } else { 0 }; + let mut vals: Vec = (start..i + 1) + .filter(|&j| !data[j].is_nan()) + .map(|j| data[j]) + .collect(); + if vals.len() >= mp { + result[i] = slice_median(&mut vals); + } + } + result +} + +// ─── expanding window variants ──────────────────────────────────────────────── +// These reuse the rolling functions with a window equal to the full array +// length, making the window grow from left for each position (expanding). + +/// Expanding sum. +#[wasm_bindgen] +pub fn expanding_sum_f64(data: &[f64], min_periods: u32) -> Vec { + let n = data.len() as u32; + rolling_sum_f64(data, n, min_periods) +} + +/// Expanding mean. +#[wasm_bindgen] +pub fn expanding_mean_f64(data: &[f64], min_periods: u32) -> Vec { + let n = data.len() as u32; + rolling_mean_f64(data, n, min_periods) +} + +/// Expanding minimum. +#[wasm_bindgen] +pub fn expanding_min_f64(data: &[f64], min_periods: u32) -> Vec { + let n = data.len() as u32; + rolling_min_f64(data, n, min_periods) +} + +/// Expanding maximum. +#[wasm_bindgen] +pub fn expanding_max_f64(data: &[f64], min_periods: u32) -> Vec { + let n = data.len() as u32; + rolling_max_f64(data, n, min_periods) +} + +/// Expanding variance (delta degrees-of-freedom `ddof`). +#[wasm_bindgen] +pub fn expanding_var_f64(data: &[f64], min_periods: u32, ddof: f64) -> Vec { + let n = data.len() as u32; + rolling_var_f64(data, n, min_periods, ddof) +} + +/// Expanding standard deviation (delta degrees-of-freedom `ddof`). +#[wasm_bindgen] +pub fn expanding_std_f64(data: &[f64], min_periods: u32, ddof: f64) -> Vec { + expanding_var_f64(data, min_periods, ddof) + .into_iter() + .map(|v| v.sqrt()) + .collect() +} + +/// Expanding median. +#[wasm_bindgen] +pub fn expanding_median_f64(data: &[f64], min_periods: u32) -> Vec { + let n = data.len() as u32; + rolling_median_f64(data, n, min_periods) +} + +// ─── unit tests ─────────────────────────────────────────────────────────────── + +#[cfg(test)] +mod tests { + use super::*; + + fn assert_near(a: f64, b: f64) { + if a.is_nan() && b.is_nan() { + return; + } + assert!(!a.is_nan(), "expected {}, got NaN", b); + assert!(!b.is_nan(), "expected NaN, got {}", a); + assert!( + (a - b).abs() < 1e-9, + "expected {}, got {}", + b, + a + ); + } + + fn assert_vec_near(actual: &[f64], expected: &[f64]) { + assert_eq!(actual.len(), expected.len()); + for (i, (&a, &e)) in actual.iter().zip(expected.iter()).enumerate() { + if e.is_nan() { + assert!(a.is_nan(), "pos {}: expected NaN, got {}", i, a); + } else { + assert_near(a, e); + } + } + } + + #[test] + fn test_rolling_sum_basic() { + let data = vec![1.0, 2.0, 3.0, 4.0, 5.0]; + let result = rolling_sum_f64(&data, 3, 3); + assert_vec_near( + &result, + &[f64::NAN, f64::NAN, 6.0, 9.0, 12.0], + ); + } + + #[test] + fn test_rolling_sum_min_periods_1() { + let data = vec![1.0, 2.0, 3.0]; + let result = rolling_sum_f64(&data, 3, 1); + assert_vec_near(&result, &[1.0, 3.0, 6.0]); + } + + #[test] + fn test_rolling_mean_basic() { + let data = vec![1.0, 2.0, 3.0, 4.0, 5.0]; + let result = rolling_mean_f64(&data, 3, 3); + assert_vec_near( + &result, + &[f64::NAN, f64::NAN, 2.0, 3.0, 4.0], + ); + } + + #[test] + fn test_rolling_mean_with_nan() { + let data = vec![1.0, f64::NAN, 3.0, 4.0, 5.0]; + let result = rolling_mean_f64(&data, 3, 2); + // window [1,NaN]: 1 valid < 2 → NaN + // window [1,NaN,3]: 2 valid → (1+3)/2 = 2 + // window [NaN,3,4]: 2 valid → 3.5 + // window [3,4,5]: 3 valid → 4 + assert_vec_near( + &result, + &[f64::NAN, f64::NAN, 2.0, 3.5, 4.0], + ); + } + + #[test] + fn test_rolling_min_basic() { + let data = vec![3.0, 1.0, 2.0, 5.0, 4.0]; + let result = rolling_min_f64(&data, 3, 3); + assert_vec_near( + &result, + &[f64::NAN, f64::NAN, 1.0, 1.0, 2.0], + ); + } + + #[test] + fn test_rolling_max_basic() { + let data = vec![1.0, 3.0, 2.0, 5.0, 4.0]; + let result = rolling_max_f64(&data, 3, 3); + assert_vec_near( + &result, + &[f64::NAN, f64::NAN, 3.0, 5.0, 5.0], + ); + } + + #[test] + fn test_rolling_var_basic() { + let data = vec![1.0, 2.0, 3.0, 4.0]; + let result = rolling_var_f64(&data, 3, 3, 1.0); + // var([1,2,3]) = 1, var([2,3,4]) = 1 + assert_vec_near(&result, &[f64::NAN, f64::NAN, 1.0, 1.0]); + } + + #[test] + fn test_rolling_median_basic() { + let data = vec![1.0, 3.0, 2.0, 4.0, 5.0]; + let result = rolling_median_f64(&data, 3, 3); + assert_vec_near( + &result, + &[f64::NAN, f64::NAN, 2.0, 3.0, 4.0], + ); + } + + #[test] + fn test_expanding_sum() { + let data = vec![1.0, 2.0, 3.0]; + let result = expanding_sum_f64(&data, 1); + assert_vec_near(&result, &[1.0, 3.0, 6.0]); + } + + #[test] + fn test_expanding_mean() { + let data = vec![1.0, 2.0, 3.0]; + let result = expanding_mean_f64(&data, 1); + assert_vec_near(&result, &[1.0, 1.5, 2.0]); + } + + #[test] + fn test_expanding_with_nan() { + let data = vec![1.0, f64::NAN, 3.0]; + let result = expanding_sum_f64(&data, 1); + assert_vec_near(&result, &[1.0, 1.0, 4.0]); + } +} diff --git a/scripts/wasm-coverage-check.ts b/scripts/wasm-coverage-check.ts index 08cf5e4b..aff6cb01 100644 --- a/scripts/wasm-coverage-check.ts +++ b/scripts/wasm-coverage-check.ts @@ -1,9 +1,12 @@ /** * Rust/WASM coverage check script. * - * Verifies that `wasm-coverage.json` contains no unclassified entries and no - * eligible functions that are missing implementations. Exits with a non-zero - * code and a descriptive error on any violation. + * Verifies that `wasm-coverage.json`: + * 1. Contains no unclassified entries and no eligible functions without implementations. + * 2. Covers every value export from `src/core/index.ts`. + * 3. Covers every top-level value export from `src/index.ts` whose source is under `src/core/`. + * + * Exits with a non-zero code and a descriptive error on any violation. * * Usage: bun run wasm:coverage */ @@ -13,7 +16,8 @@ import { resolve, dirname } from "node:path"; import { fileURLToPath } from "node:url"; const __dir = dirname(fileURLToPath(import.meta.url)); -const manifestPath = resolve(__dir, "..", "wasm-coverage.json"); +const repoRoot = resolve(__dir, ".."); +const manifestPath = resolve(repoRoot, "wasm-coverage.json"); let manifest: unknown; try { @@ -46,6 +50,56 @@ if (!isManifest(manifest)) { const { entries, summary } = manifest; +// ─── extract live export surface ───────────────────────────────────────────── + +/** + * Parse value (non-type) export names from a TypeScript barrel file. + * + * Recognises patterns: + * export { Name1, Name2 } from "..."; + * export {\n Name1,\n Name2,\n} from "..."; + * + * Skips `export type { ... }` blocks entirely. + * + * Optionally filters to only exports whose source path matches a predicate. + */ +function parseValueExports( + source: string, + sourceFilter?: (fromPath: string) => boolean, +): Set { + const result = new Set(); + // Match export blocks (possibly multi-line) with their "from" clause + const exportBlockRe = + /^export\s+(type\s+)?\{([\s\S]*?)\}\s*from\s*["']([^"']+)["']/gm; + let m: RegExpExecArray | null; + while ((m = exportBlockRe.exec(source)) !== null) { + const isTypeExport = m[1] !== undefined; // "type " present + if (isTypeExport) continue; + const fromPath = m[3]; + if (sourceFilter && !sourceFilter(fromPath)) continue; + const names = m[2].split(",").map((s) => s.trim()).filter(Boolean); + for (const name of names) { + // Skip inline comments and empty tokens + const clean = name.replace(/\/\/.*$/, "").trim(); + if (!clean || clean.startsWith("//")) continue; + result.add(clean); + } + } + return result; +} + +// All value exports from src/core/index.ts +const coreIndexSource = readFileSync(resolve(repoRoot, "src/core/index.ts"), "utf-8"); +const coreIndexExports = parseValueExports(coreIndexSource); + +// Top-level value exports from src/index.ts that come from src/core/** paths +// (includes both "./core/index.ts" re-exports and direct "./core/foo.ts" exports) +const indexSource = readFileSync(resolve(repoRoot, "src/index.ts"), "utf-8"); +const topLevelCoreExports = parseValueExports( + indexSource, + (fromPath) => fromPath.startsWith("./core/"), +); + // ─── validate each entry ────────────────────────────────────────────────────── const validStatuses = new Set(["rust-wasm", "ts-only-ineligible"]); @@ -63,6 +117,15 @@ const eligibleMissing = entries.filter( const countedRustWasm = entries.filter((e) => e.status === "rust-wasm").length; const countedTsOnly = entries.filter((e) => e.status === "ts-only-ineligible").length; +// ─── export-audit: every live export must be in the manifest ───────────────── + +const manifestNames = new Set(entries.map((e) => e.name)); + +// Union of all core exports that the manifest must cover +const requiredNames = new Set([...coreIndexExports, ...topLevelCoreExports]); + +const missingFromManifest = [...requiredNames].filter((n) => !manifestNames.has(n)); + // ─── report ─────────────────────────────────────────────────────────────────── let failed = false; @@ -114,6 +177,15 @@ if (entries.length !== summary.total_core_entries) { failed = true; } +if (missingFromManifest.length > 0) { + console.error( + `ERROR: ${missingFromManifest.length} core export(s) are present in the live source but missing from the manifest:\n` + + missingFromManifest.map((n) => ` - ${n}`).join("\n") + + "\n Add each missing name to wasm-coverage.json with a status of rust-wasm or ts-only-ineligible.", + ); + failed = true; +} + if (failed) { process.exit(1); } @@ -126,3 +198,5 @@ console.log(` rust-wasm : ${summary.rust_wasm}`); console.log(` ts-only-ineligible : ${summary.ts_only_ineligible}`); console.log(` unclassified : ${summary.unclassified}`); console.log(` eligible_missing : ${summary.eligible_missing}`); +console.log(` live exports audited: ${requiredNames.size} (${coreIndexExports.size} core/index + ${topLevelCoreExports.size} top-level core re-exports, ${requiredNames.size} unique)`); +console.log(` missing from manifest: 0`); diff --git a/src/core/arrays/datetime_array.ts b/src/core/arrays/datetime_array.ts index df0d808d..8cd4ad8f 100644 --- a/src/core/arrays/datetime_array.ts +++ b/src/core/arrays/datetime_array.ts @@ -250,7 +250,7 @@ export class DatetimeArray { const data = this._data; const mask = this._mask; return { - next() { + next(): IteratorResult { if (i >= data.length) { return { value: null, done: true }; } diff --git a/src/core/arrays/masked_array.ts b/src/core/arrays/masked_array.ts index 238082a4..f645ca6e 100644 --- a/src/core/arrays/masked_array.ts +++ b/src/core/arrays/masked_array.ts @@ -174,7 +174,7 @@ export abstract class MaskedArray { const data = this._data; const mask = this._mask; return { - next() { + next(): IteratorResult { if (i >= data.length) { return { value: null, done: true }; } diff --git a/src/core/arrays/timedelta_array.ts b/src/core/arrays/timedelta_array.ts index 60851c75..2f151175 100644 --- a/src/core/arrays/timedelta_array.ts +++ b/src/core/arrays/timedelta_array.ts @@ -308,7 +308,7 @@ export class TimedeltaArray { const data = this._data; const mask = this._mask; return { - next() { + next(): IteratorResult { if (i >= data.length) { return { value: null, done: true }; } diff --git a/src/core/options.ts b/src/core/options.ts index 628b5cce..a89ab58e 100644 --- a/src/core/options.ts +++ b/src/core/options.ts @@ -210,7 +210,10 @@ function _makeProxy(prefix: string): OptionsProxy { // If there is a direct match, return its value const k = _normalizeKey(key); if (_registry.has(k)) { - return _registry.get(k)!.currentValue; + const entry = _registry.get(k); + if (entry !== undefined) { + return entry.currentValue; + } } // Otherwise return a nested proxy for deeper access return _makeProxy(k); diff --git a/src/core/series.ts b/src/core/series.ts index c34d2569..3b4b1608 100644 --- a/src/core/series.ts +++ b/src/core/series.ts @@ -34,36 +34,6 @@ function defaultIndex(n: number): Index