Files
generalFunctions/src/stats/index.js
znetsixe 47faf94048 Phase 1 wave 1: domain + nodered + stats infra (additive)
Adds platform infrastructure used by the upcoming refactor of
nodeClass / specificClass across all 12 nodes:

- src/domain/UnitPolicy.js     — extracted from rotatingMachine/MGC
- src/domain/ChildRouter.js    — declarative event routing on top of childRegistrationUtils
- src/domain/LatestWinsGate.js — extracted from MGC dispatch gate
- src/domain/HealthStatus.js   — standardised {level, flags, message, source}
- src/nodered/statusBadge.js   — compose / error / idle / byState / text helpers
- src/stats/index.js           — mean / stdDev / median / mad / lerp

All additive — no existing exports change shape.
56 unit tests pass under node:test.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 18:27:29 +02:00

53 lines
1.7 KiB
JavaScript

'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 };