diff --git a/package.json b/package.json index fb53403e..b945a980 100644 --- a/package.json +++ b/package.json @@ -88,19 +88,19 @@ }, "homepage": "https://github.com/mljs/spectra-processing#readme", "devDependencies": { - "@types/node": "^22.13.1", - "@vitest/coverage-v8": "^3.0.5", + "@types/node": "^22.13.4", + "@vitest/coverage-v8": "^3.0.6", "cheminfo-build": "^1.2.1", - "eslint": "^9.19.0", + "eslint": "^9.20.1", "eslint-config-cheminfo-typescript": "^17.0.0", "jest-matcher-deep-close-to": "^3.0.2", "jscpd": "^4.0.5", "ml-spectra-fitting": "^4.2.4", - "prettier": "^3.4.2", + "prettier": "^3.5.1", "rimraf": "^6.0.1", "spectrum-generator": "^8.0.12", "typescript": "^5.7.3", - "vitest": "^3.0.5" + "vitest": "^3.0.6" }, "dependencies": { "binary-search": "^1.3.6", diff --git a/src/x/__tests__/xMedian.test.ts b/src/x/__tests__/xMedian.test.ts index 0281ba76..03e41848 100644 --- a/src/x/__tests__/xMedian.test.ts +++ b/src/x/__tests__/xMedian.test.ts @@ -16,15 +16,16 @@ test('should return the median', () => { expect(xMedian([1, 2, 1])).toBe(1); expect(xMedian([3, 2, 1])).toBe(2); expect(xMedian(data)).toBeCloseTo(0.5, 1); + expect(xMedian([3, 2, 1, 4, 5])).toBe(3); + expect(xMedian([3, 2, 1, 6, 4, 5])).toBe(3); + expect(xMedian([3, 2, 1, 6, 4, 5], { exact: false })).toBe(3); + expect(xMedian([3, 2, 1, 6, 4, 5], { exact: true })).toBe(3.5); + expect(xMedian([1, 2, 4, 6, 3, 5], { exact: true })).toBe(3.5); + expect(xMedian([3, 2, 1, 4, 5], { exact: true })).toBe(3); }); test('should return the median with typed array', () => { - const array = new Uint16Array(5); - array[0] = 4; - array[1] = 1; - array[2] = 2; - array[3] = 3; - array[4] = 0; + const array = Uint16Array.from([4, 1, 2, 3, 0]); expect(xMedian(array)).toBe(2); }); diff --git a/src/x/xMedian.ts b/src/x/xMedian.ts index bfdf7720..7f60f520 100644 --- a/src/x/xMedian.ts +++ b/src/x/xMedian.ts @@ -1,12 +1,25 @@ import type { NumberArray } from 'cheminfo-types'; import { isAnyArray } from 'is-any-array'; +interface XMedianOptions { + /** + * If true, the function will return the average of the two middle values. + * If false, the function will return the lower index of the two middle values. + * @default false + */ + exact?: boolean; +} + /** * Calculates the median of an array. * @param input - Array containing values + * @param options * @returns - median */ -export function xMedian(input: NumberArray): number { +export function xMedian( + input: NumberArray, + options: XMedianOptions = {}, +): number { if (!isAnyArray(input)) { throw new TypeError('input must be an array'); } @@ -15,25 +28,35 @@ export function xMedian(input: NumberArray): number { throw new TypeError('input must not be empty'); } + const { exact = false } = options || {}; const array = input.slice(); + const middleIndex = calcMiddle(0, array.length - 1); + + const median = quickSelect(array, middleIndex); + if (array.length % 2 === 1 || !exact) { + return median; + } + const medianNext = quickSelect(array, middleIndex + 1); + return (median + medianNext) / 2; +} + +function quickSelect(array: NumberArray, middleIndex: number) { let low = 0; let high = array.length - 1; let middle = 0; let currentLow = 0; let currentHigh = 0; - const median = calcMiddle(low, high); - while (true) { if (high <= low) { - return array[median]; + return array[middleIndex]; } if (high === low + 1) { if (array[low] > array[high]) { swap(array, low, high); } - return array[median]; + return array[middleIndex]; } // Find median of low, middle and high items; swap into position low @@ -65,10 +88,10 @@ export function xMedian(input: NumberArray): number { swap(array, low, currentHigh); // Re-set active partition - if (currentHigh <= median) { + if (currentHigh <= middleIndex) { low = currentLow; } - if (currentHigh >= median) { + if (currentHigh >= middleIndex) { high = currentHigh - 1; } }