From 2e7ffcc092a5148f038bddad67bc47263ebae67d Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Sat, 3 Dec 2022 09:06:45 -0800 Subject: [PATCH] fix #262; handle invalid calls to quantile, quickselect --- src/quantile.js | 12 ++++++------ src/quickselect.js | 8 +++++++- test/quantile-test.js | 7 +++++++ test/quickselect-test.js | 39 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 59 insertions(+), 7 deletions(-) create mode 100644 test/quickselect-test.js diff --git a/src/quantile.js b/src/quantile.js index 163343a..61d0a4a 100644 --- a/src/quantile.js +++ b/src/quantile.js @@ -9,8 +9,8 @@ import greatest from "./greatest.js"; export default function quantile(values, p, valueof) { values = Float64Array.from(numbers(values, valueof)); - if (!(n = values.length)) return; - if ((p = +p) <= 0 || n < 2) return min(values); + if (!(n = values.length) || isNaN(p = +p)) return; + if (p <= 0 || n < 2) return min(values); if (p >= 1) return max(values); var n, i = (n - 1) * p, @@ -21,8 +21,8 @@ export default function quantile(values, p, valueof) { } export function quantileSorted(values, p, valueof = number) { - if (!(n = values.length)) return; - if ((p = +p) <= 0 || n < 2) return +valueof(values[0], 0, values); + if (!(n = values.length) || isNaN(p = +p)) return; + if (p <= 0 || n < 2) return +valueof(values[0], 0, values); if (p >= 1) return +valueof(values[n - 1], n - 1, values); var n, i = (n - 1) * p, @@ -34,8 +34,8 @@ export function quantileSorted(values, p, valueof = number) { export function quantileIndex(values, p, valueof) { values = Float64Array.from(numbers(values, valueof)); - if (!(n = values.length)) return; - if ((p = +p) <= 0 || n < 2) return minIndex(values); + if (!(n = values.length) || isNaN(p = +p)) return; + if (p <= 0 || n < 2) return minIndex(values); if (p >= 1) return maxIndex(values); var n, i = Math.floor((n - 1) * p), diff --git a/src/quickselect.js b/src/quickselect.js index e4b6c84..19dbb46 100644 --- a/src/quickselect.js +++ b/src/quickselect.js @@ -2,7 +2,13 @@ import {ascendingDefined, compareDefined} from "./sort.js"; // Based on https://github.com/mourner/quickselect // ISC license, Copyright 2018 Vladimir Agafonkin. -export default function quickselect(array, k, left = 0, right = array.length - 1, compare) { +export default function quickselect(array, k, left = 0, right = Infinity, compare) { + k = Math.floor(k); + left = Math.floor(Math.max(0, left)); + right = Math.floor(Math.min(array.length - 1, right)); + + if (!(left <= k && k <= right)) return array; + compare = compare === undefined ? ascendingDefined : compareDefined(compare); while (right > left) { diff --git a/test/quantile-test.js b/test/quantile-test.js index adebec1..f323cbd 100644 --- a/test/quantile-test.js +++ b/test/quantile-test.js @@ -71,6 +71,13 @@ it("quantile(array, p) returns the last value for p = 1", () => { assert.strictEqual(quantile(data, 1), 4); }); +it("quantile(array, p) returns undefined if p is not a number", () => { + assert.strictEqual(quantile([1, 2, 3]), undefined); + assert.strictEqual(quantile([1, 2, 3], "no"), undefined); + assert.strictEqual(quantile([1, 2, 3], NaN), undefined); + assert.strictEqual(quantile([1, 2, 3], null), 1); // +null is 0 +}); + it("quantile(array, p, f) observes the specified accessor", () => { assert.strictEqual(quantile([1, 2, 3, 4].map(box), 0.5, unbox), 2.5); assert.strictEqual(quantile([1, 2, 3, 4].map(box), 0, unbox), 1); diff --git a/test/quickselect-test.js b/test/quickselect-test.js new file mode 100644 index 0000000..d279fb3 --- /dev/null +++ b/test/quickselect-test.js @@ -0,0 +1,39 @@ +import assert from "assert"; +import {quickselect} from "../src/index.js"; + +it("quickselect(array, k) does nothing if k is not a number", () => { + const array = [3, 1, 2]; + assert.strictEqual(quickselect(array), array); + assert.deepStrictEqual(array, [3, 1, 2]); + assert.strictEqual(quickselect(array, NaN), array); + assert.deepStrictEqual(array, [3, 1, 2]); + assert.strictEqual(quickselect(array, "no"), array); + assert.deepStrictEqual(array, [3, 1, 2]); + assert.strictEqual(quickselect(array, undefined), array); + assert.deepStrictEqual(array, [3, 1, 2]); + assert.strictEqual(quickselect(array, null), array); // coerced to zero + assert.deepStrictEqual(array, [1, 2, 3]); +}); + +it("quickselect(array, k) does nothing if k is less than left", () => { + const array = [3, 1, 2]; + assert.strictEqual(quickselect(array, -1), array); + assert.deepStrictEqual(array, [3, 1, 2]); + assert.strictEqual(quickselect(array, -0.5), array); + assert.deepStrictEqual(array, [3, 1, 2]); +}); + +it("quickselect(array, k) does nothing if k is greater than right", () => { + const array = [3, 1, 2]; + assert.strictEqual(quickselect(array, 3), array); + assert.deepStrictEqual(array, [3, 1, 2]); + assert.strictEqual(quickselect(array, 3.4), array); + assert.deepStrictEqual(array, [3, 1, 2]); +}); + +it("quickselect(array, k) implicitly floors k, left, and right", () => { + assert.deepStrictEqual(quickselect([3, 1, 2], 0.5), [1, 2, 3]); + assert.deepStrictEqual(quickselect([3, 1, 2, 5, 4], 4.1), [3, 1, 2, 4, 5]); + assert.deepStrictEqual(quickselect([3, 1, 2], 0, 0.5), [1, 2, 3]); + assert.deepStrictEqual(quickselect([3, 1, 2], 0, 0, 2.5), [1, 2, 3]); +});