Skip to content

Commit 326360d

Browse files
feat: add percentile algorithm
1 parent 5c39e87 commit 326360d

2 files changed

Lines changed: 85 additions & 0 deletions

File tree

‎Maths/Percentile.js‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/**
2+
* @function percentile
3+
* @description Calculates the nth percentile of an array of numbers.
4+
* The percentile indicates the value below which a given percentage
5+
* of observations fall.
6+
* @param {number[]} arr - An array of numbers
7+
* @param {number} p - The percentile to calculate (0-100)
8+
* @returns {number} - The nth percentile value
9+
* @see https://en.wikipedia.org/wiki/Percentile
10+
* @example percentile([1, 2, 3, 4, 5], 50) => 3
11+
* @example percentile([1, 2, 3, 4, 5], 75) => 4
12+
*/
13+
const percentile = (arr, p) => {
14+
if (!Array.isArray(arr) || arr.length === 0) {
15+
throw new TypeError('First argument must be a non-empty array')
16+
}
17+
if (!arr.every((n) => typeof n === 'number' && !isNaN(n))) {
18+
throw new TypeError('All elements must be numbers')
19+
}
20+
if (typeof p !== 'number' || isNaN(p) || p < 0 || p > 100) {
21+
throw new TypeError('Percentile must be a number between 0 and 100')
22+
}
23+
const sorted = [...arr].sort((a, b) => a - b)
24+
if (p === 0) return sorted[0]
25+
if (p === 100) return sorted[sorted.length - 1]
26+
const index = (p / 100) * (sorted.length - 1)
27+
const lower = Math.floor(index)
28+
const upper = Math.ceil(index)
29+
return sorted[lower] + (sorted[upper] - sorted[lower]) * (index - lower)
30+
}
31+
32+
export { percentile }

‎Maths/test/Percentile.test.js‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { percentile } from '../Percentile.js'
2+
3+
describe('percentile', () => {
4+
test('percentile([1,2,3,4,5], 50) returns 3', () => {
5+
expect(percentile([1, 2, 3, 4, 5], 50)).toBe(3)
6+
})
7+
8+
test('percentile([1,2,3,4,5], 0) returns 1', () => {
9+
expect(percentile([1, 2, 3, 4, 5], 0)).toBe(1)
10+
})
11+
12+
test('percentile([1,2,3,4,5], 100) returns 5', () => {
13+
expect(percentile([1, 2, 3, 4, 5], 100)).toBe(5)
14+
})
15+
16+
test('percentile([1,2,3,4,5], 75) returns 4', () => {
17+
expect(percentile([1, 2, 3, 4, 5], 75)).toBe(4)
18+
})
19+
20+
test('percentile([1,2,3,4,5], 25) returns 2', () => {
21+
expect(percentile([1, 2, 3, 4, 5], 25)).toBe(2)
22+
})
23+
24+
test('percentile works with unsorted array', () => {
25+
expect(percentile([5, 3, 1, 4, 2], 50)).toBe(3)
26+
})
27+
28+
test('percentile single element returns that element', () => {
29+
expect(percentile([5], 50)).toBe(5)
30+
})
31+
32+
test('percentile does not mutate original array', () => {
33+
const arr = [5, 3, 1, 4, 2]
34+
percentile(arr, 50)
35+
expect(arr).toEqual([5, 3, 1, 4, 2])
36+
})
37+
38+
test('throws TypeError for empty array', () => {
39+
expect(() => percentile([], 50)).toThrow(TypeError)
40+
})
41+
42+
test('throws TypeError for invalid percentile > 100', () => {
43+
expect(() => percentile([1, 2, 3], 101)).toThrow(TypeError)
44+
})
45+
46+
test('throws TypeError for invalid percentile < 0', () => {
47+
expect(() => percentile([1, 2, 3], -1)).toThrow(TypeError)
48+
})
49+
50+
test('throws TypeError for non-array input', () => {
51+
expect(() => percentile('123', 50)).toThrow(TypeError)
52+
})
53+
})

0 commit comments

Comments
 (0)