'use strict'; /** * Reducer-shape stats helpers shared across the platform. * * These were duplicated as static helpers on `Channel` and as instance * methods on the older `measurement/specificClass.js`. Consolidated here so * any consumer (outlier detection, monster summaries, future analytics) * can import a single canonical implementation. * * Stream-shape filters (low/high/band-pass, kalman, savitzky-golay) stay * on Channel as static helpers — they're pipeline state, not reducers. */ function mean(arr) { if (!arr.length) return 0; return arr.reduce((a, b) => a + b, 0) / arr.length; } // Sample std dev (n-1 denominator). A single sample has no variance to // estimate, so we return 0 rather than NaN — callers (e.g. z-score) treat // 0 as "no spread yet" and skip rejection. function stdDev(arr) { if (arr.length <= 1) return 0; const m = mean(arr); const variance = arr.reduce((s, v) => s + (v - m) ** 2, 0) / (arr.length - 1); return Math.sqrt(variance); } function median(arr) { if (!arr.length) return 0; const sorted = [...arr].sort((a, b) => a - b); const mid = Math.floor(sorted.length / 2); return sorted.length % 2 !== 0 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2; } function mad(arr) { if (!arr.length) return 0; const med = median(arr); return median(arr.map((v) => Math.abs(v - med))); } // Degenerate-range pass-through matches Channel._lerp: callers rely on it // for early-warmup paths where input bounds haven't separated yet. function lerp(value, iMin, iMax, oMin, oMax) { if (iMin >= iMax) return value; return oMin + ((value - iMin) * (oMax - oMin)) / (iMax - iMin); } module.exports = { mean, stdDev, median, mad, lerp };