diff --git a/Maths/WeightedAverage.js b/Maths/WeightedAverage.js new file mode 100644 index 0000000000..53ccf2e2ab --- /dev/null +++ b/Maths/WeightedAverage.js @@ -0,0 +1,37 @@ +/** + * @function weightedAverage + * @description Calculates the weighted average of an array of numbers + * given their corresponding weights. Each value is multiplied by its + * weight and the sum is divided by the total weight. + * @param {number[]} values - An array of numbers + * @param {number[]} weights - An array of corresponding weights + * @returns {number} - The weighted average + * @see https://en.wikipedia.org/wiki/Weighted_arithmetic_mean + * @example weightedAverage([1, 2, 3], [1, 1, 1]) => 2 + * @example weightedAverage([1, 2, 3], [1, 2, 3]) => 2.333 + */ +const weightedAverage = (values, weights) => { + if (!Array.isArray(values) || !Array.isArray(weights)) { + throw new TypeError('Both inputs must be arrays') + } + if (values.length === 0 || weights.length === 0) { + throw new TypeError('Arrays must not be empty') + } + if (values.length !== weights.length) { + throw new TypeError('Values and weights must have the same length') + } + if (!values.every((n) => typeof n === 'number' && !isNaN(n))) { + throw new TypeError('All values must be numbers') + } + if (!weights.every((n) => typeof n === 'number' && !isNaN(n) && n >= 0)) { + throw new TypeError('All weights must be non-negative numbers') + } + const totalWeight = weights.reduce((sum, w) => sum + w, 0) + if (totalWeight === 0) { + throw new RangeError('Total weight must not be zero') + } + const weightedSum = values.reduce((sum, v, i) => sum + v * weights[i], 0) + return weightedSum / totalWeight +} + +export { weightedAverage } diff --git a/Maths/test/WeightedAverage.test.js b/Maths/test/WeightedAverage.test.js new file mode 100644 index 0000000000..26c20c7359 --- /dev/null +++ b/Maths/test/WeightedAverage.test.js @@ -0,0 +1,47 @@ +import { weightedAverage } from '../WeightedAverage.js' + +describe('weightedAverage', () => { + test('equal weights gives arithmetic mean', () => { + expect(weightedAverage([1, 2, 3], [1, 1, 1])).toBeCloseTo(2) + }) + + test('weightedAverage([1,2,3],[1,2,3]) returns ~2.333', () => { + expect(weightedAverage([1, 2, 3], [1, 2, 3])).toBeCloseTo(2.333, 2) + }) + + test('weightedAverage single element returns that element', () => { + expect(weightedAverage([5], [1])).toBeCloseTo(5) + }) + + test('weightedAverage with zero weight ignores that element', () => { + expect(weightedAverage([1, 2, 3], [0, 1, 0])).toBeCloseTo(2) + }) + + test('weightedAverage([10, 20], [1, 3]) returns 17.5', () => { + expect(weightedAverage([10, 20], [1, 3])).toBeCloseTo(17.5) + }) + + test('weightedAverage([1, 2], [3, 1]) returns 1.25', () => { + expect(weightedAverage([1, 2], [3, 1])).toBeCloseTo(1.25) + }) + + test('throws TypeError for non-array input', () => { + expect(() => weightedAverage('123', [1, 2])).toThrow(TypeError) + }) + + test('throws TypeError for empty arrays', () => { + expect(() => weightedAverage([], [])).toThrow(TypeError) + }) + + test('throws TypeError for different length arrays', () => { + expect(() => weightedAverage([1, 2], [1])).toThrow(TypeError) + }) + + test('throws TypeError for negative weights', () => { + expect(() => weightedAverage([1, 2], [-1, 1])).toThrow(TypeError) + }) + + test('throws RangeError for all zero weights', () => { + expect(() => weightedAverage([1, 2], [0, 0])).toThrow(RangeError) + }) +})