Skip to content

Commit

Permalink
fix #262; handle invalid calls to quantile, quickselect
Browse files Browse the repository at this point in the history
  • Loading branch information
mbostock committed Dec 3, 2022
1 parent 04f3423 commit 2e7ffcc
Show file tree
Hide file tree
Showing 4 changed files with 59 additions and 7 deletions.
12 changes: 6 additions & 6 deletions src/quantile.js
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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),
Expand Down
8 changes: 7 additions & 1 deletion src/quickselect.js
Expand Up @@ -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) {
Expand Down
7 changes: 7 additions & 0 deletions test/quantile-test.js
Expand Up @@ -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);
Expand Down
39 changes: 39 additions & 0 deletions 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]);
});

0 comments on commit 2e7ffcc

Please sign in to comment.