diff --git a/Maths/EgyptianFraction.js b/Maths/EgyptianFraction.js new file mode 100644 index 0000000000..85b72c04d6 --- /dev/null +++ b/Maths/EgyptianFraction.js @@ -0,0 +1,41 @@ +/** + * @function egyptianFraction + * @description Represents a fraction as a sum of distinct unit fractions + * (fractions with numerator 1) using the greedy algorithm. + * For example, 2/3 = 1/2 + 1/6. + * @param {number} numerator - The numerator of the fraction + * @param {number} denominator - The denominator of the fraction + * @returns {number[]} - Array of denominators of unit fractions + * @see https://en.wikipedia.org/wiki/Egyptian_fraction + * @example egyptianFraction(2, 3) => [2, 6] + * @example egyptianFraction(3, 4) => [2, 4] + */ +const egyptianFraction = (numerator, denominator) => { + if ( + !Number.isInteger(numerator) || + !Number.isInteger(denominator) || + numerator < 1 || + denominator < 1 + ) { + throw new TypeError('Both inputs must be positive integers') + } + if (numerator >= denominator) { + throw new RangeError('Numerator must be less than denominator') + } + const result = [] + let num = numerator + let den = denominator + while (num > 0) { + const unitDen = Math.ceil(den / num) + result.push(unitDen) + num = num * unitDen - den + den = den * unitDen + const gcd = (a, b) => (b === 0 ? a : gcd(b, a % b)) + const g = gcd(num, den) + num = num / g + den = den / g + } + return result +} + +export { egyptianFraction } diff --git a/Maths/InterquartileRange.js b/Maths/InterquartileRange.js new file mode 100644 index 0000000000..691460487f --- /dev/null +++ b/Maths/InterquartileRange.js @@ -0,0 +1,31 @@ +/** + * @function interquartileRange + * @description Calculates the interquartile range (IQR) of an array. + * IQR is the difference between the 75th and 25th percentiles, + * representing the middle 50% spread of the data. + * @param {number[]} arr - An array of numbers + * @returns {number} - The interquartile range + * @see https://en.wikipedia.org/wiki/Interquartile_range + * @example interquartileRange([1, 2, 3, 4, 5]) => 2 + * @example interquartileRange([1, 2, 3, 4, 5, 6, 7]) => 3 + */ +const interquartileRange = (arr) => { + if (!Array.isArray(arr) || arr.length === 0) { + throw new TypeError('Input must be a non-empty array') + } + if (!arr.every((n) => typeof n === 'number' && !isNaN(n))) { + throw new TypeError('All elements must be numbers') + } + const sorted = [...arr].sort((a, b) => a - b) + const getPercentile = (data, p) => { + if (p === 0) return data[0] + if (p === 100) return data[data.length - 1] + const index = (p / 100) * (data.length - 1) + const lower = Math.floor(index) + const upper = Math.ceil(index) + return data[lower] + (data[upper] - data[lower]) * (index - lower) + } + return getPercentile(sorted, 75) - getPercentile(sorted, 25) +} + +export { interquartileRange } diff --git a/Maths/test/EgyptianFraction.test.js b/Maths/test/EgyptianFraction.test.js new file mode 100644 index 0000000000..723c6c6f2f --- /dev/null +++ b/Maths/test/EgyptianFraction.test.js @@ -0,0 +1,49 @@ +import { egyptianFraction } from '../EgyptianFraction.js' + +describe('egyptianFraction', () => { + test('egyptianFraction(2, 3) returns [2, 6]', () => { + expect(egyptianFraction(2, 3)).toEqual([2, 6]) + }) + + test('egyptianFraction(3, 4) returns [2, 4]', () => { + expect(egyptianFraction(3, 4)).toEqual([2, 4]) + }) + + test('egyptianFraction(1, 2) returns [2]', () => { + expect(egyptianFraction(1, 2)).toEqual([2]) + }) + + test('result unit fractions sum to original fraction', () => { + const denoms = egyptianFraction(2, 3) + const sum = denoms.reduce((acc, d) => acc + 1 / d, 0) + expect(sum).toBeCloseTo(2 / 3, 10) + }) + + test('result unit fractions sum for 3/4', () => { + const denoms = egyptianFraction(3, 4) + const sum = denoms.reduce((acc, d) => acc + 1 / d, 0) + expect(sum).toBeCloseTo(3 / 4, 10) + }) + + test('all denominators are distinct', () => { + const denoms = egyptianFraction(2, 3) + const unique = new Set(denoms) + expect(unique.size).toBe(denoms.length) + }) + + test('throws TypeError for zero numerator', () => { + expect(() => egyptianFraction(0, 3)).toThrow(TypeError) + }) + + test('throws TypeError for negative input', () => { + expect(() => egyptianFraction(-2, 3)).toThrow(TypeError) + }) + + test('throws RangeError when numerator >= denominator', () => { + expect(() => egyptianFraction(3, 3)).toThrow(RangeError) + }) + + test('throws TypeError for float input', () => { + expect(() => egyptianFraction(2.5, 3)).toThrow(TypeError) + }) +}) diff --git a/Maths/test/InterquartileRange.test.js b/Maths/test/InterquartileRange.test.js new file mode 100644 index 0000000000..e851cb8654 --- /dev/null +++ b/Maths/test/InterquartileRange.test.js @@ -0,0 +1,41 @@ +import { interquartileRange } from '../InterquartileRange.js' + +describe('interquartileRange', () => { + test('interquartileRange([1,2,3,4,5]) returns 2', () => { + expect(interquartileRange([1, 2, 3, 4, 5])).toBeCloseTo(2) + }) + + test('interquartileRange([1,2,3,4,5,6,7]) returns 3', () => { + expect(interquartileRange([1, 2, 3, 4, 5, 6, 7])).toBeCloseTo(3) + }) + + test('interquartileRange identical elements returns 0', () => { + expect(interquartileRange([5, 5, 5, 5])).toBe(0) + }) + + test('interquartileRange works with unsorted array', () => { + expect(interquartileRange([5, 3, 1, 4, 2])).toBeCloseTo(2) + }) + + test('interquartileRange works with negative numbers', () => { + expect(interquartileRange([-3, -2, -1, 0, 1, 2, 3])).toBeCloseTo(3) + }) + + test('interquartileRange does not mutate original array', () => { + const arr = [5, 3, 1, 4, 2] + interquartileRange(arr) + expect(arr).toEqual([5, 3, 1, 4, 2]) + }) + + test('throws TypeError for empty array', () => { + expect(() => interquartileRange([])).toThrow(TypeError) + }) + + test('throws TypeError for non-array input', () => { + expect(() => interquartileRange('123')).toThrow(TypeError) + }) + + test('throws TypeError for array with non-numbers', () => { + expect(() => interquartileRange([1, 'two', 3])).toThrow(TypeError) + }) +})