Skip to content

Statistics Calculator — TypeScript source

Compute descriptive statistics - count, sum, mean, median, mode, min/max, range, variance, standard deviation, and quartiles (Q1/Q3/IQR) - from any list of numbers. Tolerates mixed separators and flags unparseable tokens. Choose sample (n−1) or population (n) variance. Everything runs 100% client-side.

This is the TypeScript implementation — the same logic the interactive tool runs, in a shareable, citable form.

// Pure logic for the Statistics Calculator. No React, no DOM - the unit-test
// surface. Fully deterministic: every function depends only on its inputs.

/** Outcome of splitting a free-form number list into valid + invalid tokens. */
export interface ParseResult {
  /** Finite numbers, in the order they appeared. */
  values: number[];
  /** Tokens that could not be parsed as finite numbers, in order. */
  invalid: string[];
}

/** Descriptive statistics over a sample of numbers. Numeric fields are `NaN`
 *  when `count === 0`; `mode` is `[]` when there is no mode. */
export interface Stats {
  count: number;
  sum: number;
  mean: number;
  median: number;
  /** Most frequent value(s), ascending. `[]` when uniform / no mode. */
  mode: number[];
  min: number;
  max: number;
  range: number;
  variance: number;
  stddev: number;
  q1: number;
  q3: number;
  iqr: number;
}

/**
 * Parse a free-form number list into finite values and unparseable tokens.
 * Separators are any run of whitespace and/or commas. Tokens like `Infinity`
 * and `NaN` are not finite, so they land in `invalid`. Empty input yields no
 * values and no invalid tokens.
 */
export function parseNumbers(input: string): ParseResult {
  if (!input.trim()) return { values: [], invalid: [] };
  const tokens = input.split(/[\s,]+/).filter((t) => t.length > 0);
  const values: number[] = [];
  const invalid: string[] = [];
  for (const tok of tokens) {
    const n = Number(tok);
    if (Number.isFinite(n)) values.push(n);
    else invalid.push(tok);
  }
  return { values, invalid };
}

/**
 * Linear-interpolation quantile (R-7 / NumPy / Excel PERCENTILE convention).
 * `sorted` must be ascending and non-empty; `p` in [0, 1].
 */
function quantile(sorted: number[], p: number): number {
  const n = sorted.length;
  const h = (n - 1) * p;
  const lower = Math.floor(h);
  const upper = Math.ceil(h);
  if (lower === upper) return sorted[lower];
  return sorted[lower] + (h - lower) * (sorted[upper] - sorted[lower]);
}

/**
 * Most frequent value(s), ascending. Returns `[]` when there is no mode - i.e.
 * when every value is distinct, or when all distinct values share the same
 * frequency (a flat / uniform distribution with ≥2 distinct values). A single
 * repeated value (e.g. `[5,5,5]`) does have a mode: `[5]`.
 */
function computeMode(values: number[]): number[] {
  const freq = new Map<number, number>();
  for (const v of values) freq.set(v, (freq.get(v) ?? 0) + 1);
  // A single distinct value is always the mode (covers [7] and [5,5,5]).
  if (freq.size === 1) return [...freq.keys()];
  const max = Math.max(...freq.values());
  const modes = [...freq.entries()].filter(([, c]) => c === max).map(([v]) => v);
  // All distinct values share the max frequency → uniform → no mode.
  if (modes.length === freq.size) return [];
  return modes.sort((a, b) => a - b);
}

/** An all-`NaN` Stats block for the empty-input case (count 0). */
const EMPTY_STATS: Stats = {
  count: 0,
  sum: NaN,
  mean: NaN,
  median: NaN,
  mode: [],
  min: NaN,
  max: NaN,
  range: NaN,
  variance: NaN,
  stddev: NaN,
  q1: NaN,
  q3: NaN,
  iqr: NaN,
};

/**
 * Compute descriptive statistics over `values`. With `sample = true` (default)
 * variance/stddev use the sample estimator (n−1); with `sample = false` they
 * use the population estimator (n). Empty input returns count 0 with every
 * numeric field `NaN` and `mode: []` - never throws.
 */
export function summarize(values: number[], sample = true): Stats {
  const n = values.length;
  if (n === 0) return { ...EMPTY_STATS };

  const sorted = [...values].sort((a, b) => a - b);
  const sum = values.reduce((a, b) => a + b, 0);
  const mean = sum / n;
  const min = sorted[0];
  const max = sorted[n - 1];
  const median = quantile(sorted, 0.5);
  const q1 = quantile(sorted, 0.25);
  const q3 = quantile(sorted, 0.75);

  // Sum of squared deviations from the mean.
  const ss = values.reduce((a, x) => a + (x - mean) ** 2, 0);
  const variance = sample ? (n >= 2 ? ss / (n - 1) : NaN) : ss / n;
  const stddev = Number.isFinite(variance) ? Math.sqrt(variance) : NaN;

  return {
    count: n,
    sum,
    mean,
    median,
    mode: computeMode(values),
    min,
    max,
    range: max - min,
    variance,
    stddev,
    q1,
    q3,
    iqr: q3 - q1,
  };
}

Also available in 13 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →