Functional Weave
Code in Python

stats.moving-average@1.0.0

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