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 →