impl/typescript.ts
2,444 bytes · the TypeScript implementation · view raw
import { type MovingAverageKind } from "./stats_moving_average_types.ts";
const POW10 = [1, 10, 100, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11, 1e12];
// Half away from zero on the binary64 value, then -0 becomes 0. Only floor,
// * and / are used, so every language rounds the same double the same way.
function roundTo(x: number, decimals: number): number {
const scale = POW10[decimals];
const y = Math.abs(x) * scale;
let r = Math.floor(y);
if (y - r >= 0.5) r += 1;
const out = r / scale;
return (x < 0 ? -out : out) + 0;
}
// Summed afresh, left to right, so the result never depends on the history
// a running sum would carry.
function windowMean(values: readonly number[], end: number, window: number): number {
let sum = 0;
for (let i = end - window + 1; i <= end; i++) sum += values[i];
return sum / window;
}
/**
* Simple or exponential moving average, one value per full window.
*
* The exponential average is seeded with the simple average of the first
* window and carried unrounded; only the returned values are rounded.
*/
export function movingAverage(
values: readonly number[],
window: number,
kind: MovingAverageKind,
decimals: number,
): readonly number[] {
if (!Array.isArray(values)) throw new TypeError("values must be a list of numbers");
for (const v of values) {
if (typeof v !== "number" || !Number.isFinite(v)) {
throw new TypeError(`values must be finite numbers, received ${v}`);
}
}
if (!Number.isInteger(window) || window < 1) {
throw new RangeError(`window must be a whole number of 1 or greater, received ${window}`);
}
if (kind !== "simple" && kind !== "exponential") {
throw new RangeError(`unknown moving average kind "${kind}"`);
}
if (!Number.isInteger(decimals) || decimals < 0 || decimals > 12) {
throw new RangeError(`decimals must be a whole number from 0 to 12, received ${decimals}`);
}
const out: number[] = [];
if (values.length < window) return out;
if (kind === "simple") {
for (let end = window - 1; end < values.length; end++) {
out.push(roundTo(windowMean(values, end, window), decimals));
}
return out;
}
const alpha = 2 / (window + 1);
let ema = windowMean(values, window - 1, window);
out.push(roundTo(ema, decimals));
for (let i = window; i < values.length; i++) {
ema = ema + alpha * (values[i] - ema);
out.push(roundTo(ema, decimals));
}
return out;
}