Skip to content

Commit a063a76

Browse files
committed
Add functions to calculate mean, median, and mode
Since I remembered nothing from high-school maths class, I brushed up on my understanding of these operations using MathsIsFun.com's pages on how to perform these operations on paper.
1 parent 2ed2e63 commit a063a76

File tree

5 files changed

+190
-1
lines changed

5 files changed

+190
-1
lines changed

.eslintrc.json

+3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
"overrides": [{
55
"files": ["lib/{binary,text}.mjs"],
66
"rules": {"multiline-ternary": 0}
7+
},{
8+
"files": ["lib/math.mjs"],
9+
"rules": {"eqeqeq": 0}
710
},{
811
"files": ["lib/misc.mjs"],
912
"rules": {"spaced-comment": ["error", "always", {"markers": ["@cc_on"]}]}

.mocharc.cjs

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
module.exports = {
44
require: [
5-
"chai/register-expect",
5+
"chai/register-expect.js",
66
"mocha-when/register",
77
],
88
};

index.d.ts

+3
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,9 @@ export declare function isplit(input: string, pattern?: RegExp | string): string
107107
export declare function keyGrep(subject: object, pattern: RegExp | string): object;
108108
export declare function ls(paths?: string[], options?: {filter?: RegExp | Function; ignore?: RegExp | Function; recurse?: number; followSymlinks?: boolean}): Promise<Map<string, fs.Stats>>;
109109
export declare function mark(input: string, ranges: any[][] | any[] | string | number): string;
110+
export declare function mean(...values: number[] | bigint[]): number;
111+
export declare function median(...values: number[] | bigint[]): number;
112+
export declare function mode(...values: number[] | bigint[]): (number|bigint)[];
110113
export declare function nearest(subject: Node, selector: string, ignoreSelf?: boolean): Element;
111114
export declare function nerf(fn: Function, context?: object): Function;
112115
export declare function normalise(value: number): number[];

lib/math.mjs

+63
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,69 @@ export function distance(a, b){
194194
}
195195

196196

197+
/**
198+
* Compute the average of a set of numbers.
199+
*
200+
* @example mean(6, 11, 7) == 8;
201+
* @param {...(Number|BigInt)} values
202+
* @return {Number}
203+
*/
204+
export function mean(...values){
205+
let result = 0;
206+
const {length} = values;
207+
for(let i = 0; i < length; result += parseFloat(values[i++]));
208+
return result /= length;
209+
}
210+
211+
212+
/**
213+
* Retrieve the “middle” of a list of numbers.
214+
*
215+
* @example median(12, 3, 5) == 5;
216+
* @example median(1, 3, 4, 5) == 3.5;
217+
* @param {...(Number|BigInt)} values
218+
* @return {Number}
219+
*/
220+
export function median(...values){
221+
values = values.map(Number).sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
222+
const mid = values.length / 2;
223+
return mid % 1
224+
? values[Math.floor(mid)]
225+
: (values[mid] + values[mid - 1]) / 2;
226+
}
227+
228+
229+
/**
230+
* Retrieve the most frequently-occuring values in a set.
231+
*
232+
* Strings are converted to numeric values using {@linkcode parseFloat};
233+
* bigints whose magnitudes fall below {@link Number.MAX_VALUE} are also
234+
* converted to floating-point values. Identicality is determined using
235+
* {@link https://mdn.io/JS/Operators/Equality|loose equality comparison}.
236+
*
237+
* @see {@link https://www.mathsisfun.com/mode.html}
238+
* @example mode(1, 2, 3, 1) == [1];
239+
* @example mode(1, 2, 2n, 3, "3") == [2, 3];
240+
* @param {...(Number|BigInt)} values
241+
* @return {Array<Number|BigInt>}
242+
*/
243+
export function mode(...values){
244+
const freq = [];
245+
const {length} = values;
246+
for(let i = 0; i < length;){
247+
const n = values[i++];
248+
const f = freq.find(f => f[0] == n);
249+
f ? f[1]++ : freq.push([n, 1]);
250+
}
251+
// Sort frequencies in descending order
252+
freq.sort(([, a], [, b]) => a < b ? 1 : a > b ? -1 : 0);
253+
254+
// Resolve mode(s)
255+
freq.splice(freq.findIndex(([, f]) => f < freq[0][1]));
256+
return freq.map(f => f[0]).sort((a, b) => a < b ? -1 : !!(a > b));
257+
}
258+
259+
197260
/**
198261
* Convert a number to normalised scientific notation.
199262
*

test/math.mjs

+120
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,115 @@ describe("Mathematical functions", () => {
178178
it("measures empty distances", () => expect(distance([32, 4], [32, 4])).to.equal(0));
179179
});
180180

181+
describe("mean()", () => {
182+
const {mean} = utils;
183+
it("averages sorted values", () => {
184+
expect(mean(1, 2, 3)).to.equal(2);
185+
expect(mean(0, 10)) .to.equal(5);
186+
expect(mean(-5, 5)) .to.equal(0);
187+
expect(mean(1n, 5n)) .to.equal(3);
188+
expect(mean(1, 10n)) .to.equal(5.5);
189+
});
190+
it("averages unsorted values", () => {
191+
expect(mean(3, 1, 2)) .to.equal(2);
192+
expect(mean(1024, 0)) .to.equal(512);
193+
expect(mean(5, -5, 6)).to.equal(2);
194+
expect(mean(6, 11, 7)).to.equal(8);
195+
expect(mean(50n, 25n)).to.equal(37.5);
196+
expect(mean(512n, 0)) .to.equal(256);
197+
});
198+
it("accepts numeric strings as input", () => {
199+
expect(mean("1", "3")) .to.equal(2);
200+
expect(mean(1, "100")) .to.equal(50.5);
201+
expect(mean(-1n, "1")) .to.equal(0);
202+
expect(mean("-5", 0)) .to.equal(-2.5);
203+
expect(mean(0n, "5", 7)).to.equal(4);
204+
});
205+
});
206+
207+
describe("median()", () => {
208+
const {median} = utils;
209+
const oddLists = [
210+
[[3, 5, 12], 5],
211+
[[3, 5, 7, 12, 13, 14, 21, 23, 23, 23, 23, 29, 39, 40, 56], 23],
212+
[[10, 11, 13, 15, 16, 23, 26], 15],
213+
];
214+
const evenLists = [
215+
[[1, 2], 1.5],
216+
[[1, 3, 4, 5], 3.5],
217+
[[3, 5, 7, 12, 13, 14, 21, 23, 23, 23, 23, 29, 40, 56], 22],
218+
[[0, 50, 60, 100], 55],
219+
[[23.5, 24], 23.75],
220+
];
221+
it("retrieves an odd-sized list's middle value", () => {
222+
for(const [input, expected] of oddLists)
223+
expect(median(...numSort(input))).to.equal(expected);
224+
});
225+
it("averages an even-sized list's two middlemost values", () => {
226+
for(const [input, expected] of evenLists)
227+
expect(median(...numSort(input))).to.equal(expected);
228+
});
229+
it("doesn't require lists to be pre-sorted", () => {
230+
for(const [input, expected] of oddLists.concat(evenLists)){
231+
const i = Math.round(input.length / 2);
232+
const a = input.slice(0, i).reverse();
233+
const b = input.slice(i) .reverse();
234+
expect(median(...b.concat(a))).to.equal(expected);
235+
}
236+
});
237+
it("coerces non-numeric values", () => {
238+
let calls = 0;
239+
const num = 0xBABEFACE;
240+
const obj = {
241+
__proto__: null,
242+
toString: () => "Invalid number",
243+
valueOf: () => (++calls, num),
244+
};
245+
expect(median(...[obj, 0, num * 2])).to.equal(num);
246+
expect(calls).to.equal(1);
247+
});
248+
});
249+
250+
describe("mode()", () => {
251+
const {mode} = utils;
252+
const lists = {
253+
__proto__: null,
254+
unimodal: [
255+
[[1, 2, 3, 1], 1],
256+
[[6, 3, 9, 6, 6, 5, 9, 3], 6],
257+
[[3, 7, 5, 13, 20, 23, 39, 23, 40, 23, 14, 12, 56, 23, 29], 23],
258+
[[19, 8, 29, 35, 19, 28, 15], 19],
259+
],
260+
bimodal: [
261+
[[1, 3, 3, 3, 4, 4, 6, 6, 6, 9], [3, 6]],
262+
[[7, 2, 15, 11, 15, 9, 13, 0, 10, 1, 12, 2, 0, 5, 15, 5, 2, 3], [2, 15]],
263+
],
264+
multimodal: [
265+
[[1, 3, 3, 7, 7, 7, 3, 1, 0, 1], [1, 3, 7]],
266+
[[
267+
2.4, 7, 3.2, -3.5, 0.8, 0.9, 2.6, -5.4, 7, 5.3, 3.4, 0.7, -1.2, 6.4,
268+
7, 2.3, 1.3, -0.9, 4.1, 5.4, 2.5, 3.7, 0.5, 3.4, 2.3, 0.5, -1.4, 3.6,
269+
0.8, -3.9, 0.5, -0.3, 1.5, 0.8,
270+
], [0.5, 0.8, 7]],
271+
],
272+
};
273+
for(const type of ["unimodal", "bimodal", "multimodal"])
274+
for(const sort of ["sorted", "unsorted"])
275+
it(`isolates ${type} distributions from ${sort} lists`, () => {
276+
for(let [input, expected] of lists[type]){
277+
if("sorted" === sort) input = numSort(input);
278+
if("number" === typeof expected) expected = [expected];
279+
expect(mode(...input)).to.eql(expected);
280+
}
281+
});
282+
it("returns modes in a predictable order", () => {
283+
const input = [-10, 8, 8, 8, 0.1, 0.1, 0.1, 7, 7, 7, 10];
284+
const expected = [0.1, 7, 8];
285+
expect(mode(...input)).to.eql(expected);
286+
expect(mode(...numSort(input))).to.eql(expected);
287+
});
288+
});
289+
181290
describe("normalise()", () => {
182291
const {normalise} = utils;
183292
const cmp = (input, expected) => {
@@ -480,4 +589,15 @@ describe("Mathematical functions", () => {
480589
expect(sum(-4, 2n)).to.equal(-2);
481590
});
482591
});
592+
593+
/**
594+
* Return a copy of an array with entries sorted numerically.
595+
* @example numSort([3, 200, 10]) == [3, 10, 200];
596+
* @param {Array} list
597+
* @return {Array}
598+
* @internal
599+
*/
600+
function numSort(list){
601+
return list.slice().sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
602+
}
483603
});

0 commit comments

Comments
 (0)