Skip to content

Statistics Calculator — JavaScript 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 JavaScript implementation — the same logic the interactive tool runs, in a shareable, citable form.

/**
 * statistics - polyglot showcase port (JavaScript).
 *
 * Pure descriptive-statistics logic for the Statistics Calculator tool on
 * CosmoDev (dev.cosmolabs.org). Ported from the canonical TypeScript source at
 * src/lib/statistics.ts so the tool page can display the same logic across
 * six languages.
 *
 * Self-contained: standard library only, no external dependencies.
 *
 * License/usage: display source - part of CosmoDev's polyglot tool pages.
 */

/**
 * @typedef {Object} ParseResult
 * @property {number[]} values  Finite numbers, in the order they appeared.
 * @property {string[]} invalid Tokens that could not be parsed as finite numbers.
 */

/**
 * @typedef {Object} Stats
 * @property {number} count
 * @property {number} sum
 * @property {number} mean
 * @property {number} median
 * @property {number[]} mode  Most frequent value(s), ascending. [] when no mode.
 * @property {number} min
 * @property {number} max
 * @property {number} range
 * @property {number} variance
 * @property {number} stddev
 * @property {number} q1
 * @property {number} q3
 * @property {number} iqr
 */

/**
 * Split 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.
 *
 * @param {string} input
 * @returns {ParseResult}
 */
export function parseNumbers(input) {
  if (!input.trim()) return { values: [], invalid: [] };

  // Split on any run of commas/whitespace; drop the empty strings that
  // leading/trailing/doubled separators would otherwise produce.
  const tokens = input.split(/[\s,]+/).filter((t) => t.length > 0);

  const values = [];
  const invalid = [];
  for (const tok of tokens) {
    // Number() accepts decimals, exponents, signs, and hex (0x...). Anything
    // it rejects - or that overflows to ±Infinity - is reported as invalid.
    const n = Number(tok);
    if (Number.isFinite(n)) values.push(n);
    else invalid.push(tok);
  }
  return { values, invalid };
}

/**
 * Linear-interpolation quantile - the R-7 convention used by NumPy, Excel's
 * PERCENTILE, and most stats textbooks. `sorted` must be ascending and
 * non-empty; `p` is in [0, 1].
 *
 * @param {number[]} sorted
 * @param {number} p
 * @returns {number}
 */
function quantile(sorted, p) {
  const n = sorted.length;
  // Position along the n-1 gaps between the n sorted samples.
  const h = (n - 1) * p;
  const lower = Math.floor(h);
  const upper = Math.ceil(h);
  if (lower === upper) return sorted[lower];
  // Interpolate between the two bracketing samples.
  return sorted[lower] + (h - lower) * (sorted[upper] - sorted[lower]);
}

/**
 * Most frequent value(s), ascending.
 *
 * Returns [] when there is no mode: 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.
 *
 * @param {number[]} values
 * @returns {number[]}
 */
function computeMode(values) {
  const freq = new Map();
  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(([, count]) => count === max)
    .map(([value]) => value);

  // All distinct values share the max frequency → uniform → no mode.
  if (modes.length === freq.size) return [];

  return modes.sort((a, b) => a - b);
}

/**
 * All-NaN Stats block for the empty-input case (count 0). Reused (shallow-
 * copied) so the empty path stays allocation-light.
 */
const EMPTY_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
 * (divide by n−1); with `sample = false` they use the population estimator
 * (divide by n). Empty input returns count 0 with every numeric field NaN and
 * `mode: []` - never throws.
 *
 * @param {number[]} values
 * @param {boolean} [sample=true]
 * @returns {Stats}
 */
export function summarize(values, sample = true) {
  const n = values.length;
  if (n === 0) return { ...EMPTY_STATS };

  // Sort a copy (not the caller's array) so min/max/quantiles are cheap.
  const sorted = [...values].sort((a, b) => a - b);

  // Sum is accumulated over the ORIGINAL order to match the TS reference and
  // keep floating-point summation deterministic across languages.
  const sum = values.reduce((acc, x) => acc + x, 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 (original order).
  const ss = values.reduce((acc, x) => acc + (x - mean) ** 2, 0);
  // Sample variance is undefined for n < 2; population variance is always ss/n.
  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 →