From 98766fff5d5478d3e76570c358efc667bf1c2053 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Sun, 19 May 2019 09:20:14 -0700 Subject: [PATCH 1/8] Rename scan to leastIndex. --- src/{scan.js => leastIndex.js} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/{scan.js => leastIndex.js} (82%) diff --git a/src/scan.js b/src/leastIndex.js similarity index 82% rename from src/scan.js rename to src/leastIndex.js index f965311d..c089f483 100644 --- a/src/scan.js +++ b/src/leastIndex.js @@ -1,6 +1,6 @@ import ascending from "./ascending.js"; -export default function scan(values, compare = ascending) { +export default function leastIndex(values, compare = ascending) { let min; let minIndex; let index = -1; From 715a487ef8f9ccf54b715efbb2a7decfdd6e4564 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Sun, 19 May 2019 09:23:29 -0700 Subject: [PATCH 2/8] Add least. --- src/least.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 src/least.js diff --git a/src/least.js b/src/least.js new file mode 100644 index 00000000..53d470bc --- /dev/null +++ b/src/least.js @@ -0,0 +1,15 @@ +import ascending from "./ascending.js"; + +export default function least(values, compare = ascending) { + let min; + let defined = false; + for (const value of values) { + if (defined + ? compare(value, min) < 0 + : compare(value, value) === 0) { + min = value; + defined = true; + } + } + return min; +} From d44179f15c40f303fb32d03d84338e4ba491bbc1 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Sun, 19 May 2019 09:24:47 -0700 Subject: [PATCH 3/8] Expose least, leastIndex. --- src/index.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/index.js b/src/index.js index 927a6625..d1fc6077 100644 --- a/src/index.js +++ b/src/index.js @@ -6,7 +6,7 @@ export {default as descending} from "./descending.js"; export {default as deviation} from "./deviation.js"; export {default as extent} from "./extent.js"; export {default as group} from "./group.js"; -export {default as bin, default as histogram} from "./bin.js"; +export {default as bin, default as histogram} from "./bin.js"; // Deprecated alias: histogram. export {default as thresholdFreedmanDiaconis} from "./threshold/freedmanDiaconis.js"; export {default as thresholdScott} from "./threshold/scott.js"; export {default as thresholdSturges} from "./threshold/sturges.js"; @@ -21,7 +21,8 @@ export {default as quantile} from "./quantile.js"; export {default as quickselect} from "./quickselect.js"; export {default as range} from "./range.js"; export {default as rollup} from "./rollup.js"; -export {default as scan} from "./scan.js"; +export {default as least} from "./least.js"; +export {default as scan, default as leastIndex} from "./leastIndex.js"; // Deprecated alias: scan. export {default as shuffle} from "./shuffle.js"; export {default as sum} from "./sum.js"; export {default as ticks, tickIncrement, tickStep} from "./ticks.js"; From b517659484622461d45a91b207cd3ed0e7c51c60 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Sun, 19 May 2019 09:25:43 -0700 Subject: [PATCH 4/8] Update leastIndex test. --- test/leastIndex-test.js | 46 +++++++++++++++++++++++++++++++++++++++++ test/scan-test.js | 46 ----------------------------------------- 2 files changed, 46 insertions(+), 46 deletions(-) create mode 100644 test/leastIndex-test.js delete mode 100644 test/scan-test.js diff --git a/test/leastIndex-test.js b/test/leastIndex-test.js new file mode 100644 index 00000000..c134a761 --- /dev/null +++ b/test/leastIndex-test.js @@ -0,0 +1,46 @@ +var tape = require("tape"), + arrays = require("../"); + +tape("leastIndex(array) compares using natural order", function(test) { + test.strictEqual(arrays.leastIndex([0, 1]), 0); + test.strictEqual(arrays.leastIndex([1, 0]), 1); + test.strictEqual(arrays.leastIndex([0, "1"]), 0); + test.strictEqual(arrays.leastIndex(["1", 0]), 1); + test.strictEqual(arrays.leastIndex(["10", "2"]), 0); + test.strictEqual(arrays.leastIndex(["2", "10"]), 1); + test.strictEqual(arrays.leastIndex(["10", "2", NaN]), 0); + test.strictEqual(arrays.leastIndex([NaN, "10", "2"]), 1); + test.strictEqual(arrays.leastIndex(["2", NaN, "10"]), 2); + test.strictEqual(arrays.leastIndex([2, NaN, 10]), 0); + test.strictEqual(arrays.leastIndex([10, 2, NaN]), 1); + test.strictEqual(arrays.leastIndex([NaN, 10, 2]), 2); + test.end(); +}); + +tape("leastIndex(array, compare) compares using the specified compare function", function(test) { + var a = {name: "a"}, b = {name: "b"}; + test.strictEqual(arrays.leastIndex([a, b], function(a, b) { return a.name.localeCompare(b.name); }), 0); + test.strictEqual(arrays.leastIndex([1, 0], arrays.descending), 0); + test.strictEqual(arrays.leastIndex(["1", 0], arrays.descending), 0); + test.strictEqual(arrays.leastIndex(["2", "10"], arrays.descending), 0); + test.strictEqual(arrays.leastIndex(["2", NaN, "10"], arrays.descending), 0); + test.strictEqual(arrays.leastIndex([2, NaN, 10], arrays.descending), 2); + test.end(); +}); + +tape("leastIndex(array) returns undefined if the array is empty", function(test) { + test.strictEqual(arrays.leastIndex([]), undefined); + test.end(); +}); + +tape("leastIndex(array) returns undefined if the array contains only incomparable values", function(test) { + test.strictEqual(arrays.leastIndex([NaN, undefined]), undefined); + test.strictEqual(arrays.leastIndex([NaN, "foo"], function(a, b) { return a - b; }), undefined); + test.end(); +}); + +tape("leastIndex(array) returns the first of equal values", function(test) { + test.strictEqual(arrays.leastIndex([2, 2, 1, 1, 0, 0, 0, 3, 0]), 4); + test.strictEqual(arrays.leastIndex([3, 2, 2, 1, 1, 0, 0, 0, 3, 0], arrays.descending), 0); + test.end(); +}); diff --git a/test/scan-test.js b/test/scan-test.js deleted file mode 100644 index 8289f354..00000000 --- a/test/scan-test.js +++ /dev/null @@ -1,46 +0,0 @@ -var tape = require("tape"), - arrays = require("../"); - -tape("scan(array) compares using natural order", function(test) { - test.strictEqual(arrays.scan([0, 1]), 0); - test.strictEqual(arrays.scan([1, 0]), 1); - test.strictEqual(arrays.scan([0, "1"]), 0); - test.strictEqual(arrays.scan(["1", 0]), 1); - test.strictEqual(arrays.scan(["10", "2"]), 0); - test.strictEqual(arrays.scan(["2", "10"]), 1); - test.strictEqual(arrays.scan(["10", "2", NaN]), 0); - test.strictEqual(arrays.scan([NaN, "10", "2"]), 1); - test.strictEqual(arrays.scan(["2", NaN, "10"]), 2); - test.strictEqual(arrays.scan([2, NaN, 10]), 0); - test.strictEqual(arrays.scan([10, 2, NaN]), 1); - test.strictEqual(arrays.scan([NaN, 10, 2]), 2); - test.end(); -}); - -tape("scan(array, compare) compares using the specified compare function", function(test) { - var a = {name: "a"}, b = {name: "b"}; - test.strictEqual(arrays.scan([a, b], function(a, b) { return a.name.localeCompare(b.name); }), 0); - test.strictEqual(arrays.scan([1, 0], arrays.descending), 0); - test.strictEqual(arrays.scan(["1", 0], arrays.descending), 0); - test.strictEqual(arrays.scan(["2", "10"], arrays.descending), 0); - test.strictEqual(arrays.scan(["2", NaN, "10"], arrays.descending), 0); - test.strictEqual(arrays.scan([2, NaN, 10], arrays.descending), 2); - test.end(); -}); - -tape("scan(array) returns undefined if the array is empty", function(test) { - test.strictEqual(arrays.scan([]), undefined); - test.end(); -}); - -tape("scan(array) returns undefined if the array contains only incomparable values", function(test) { - test.strictEqual(arrays.scan([NaN, undefined]), undefined); - test.strictEqual(arrays.scan([NaN, "foo"], function(a, b) { return a - b; }), undefined); - test.end(); -}); - -tape("scan(array) returns the first of equal values", function(test) { - test.strictEqual(arrays.scan([2, 2, 1, 1, 0, 0, 0, 3, 0]), 4); - test.strictEqual(arrays.scan([3, 2, 2, 1, 1, 0, 0, 0, 3, 0], arrays.descending), 0); - test.end(); -}); From 3c76a2d1fb6db0b259df7678db6d9de826c9248e Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Sun, 19 May 2019 21:14:50 -0700 Subject: [PATCH 5/8] Add {min,max}Index. --- src/extent.js | 10 ++-- src/index.js | 7 ++- src/leastIndex.js | 4 +- src/max.js | 8 ++- src/maxIndex.js | 22 ++++++++ src/min.js | 8 ++- src/minIndex.js | 22 ++++++++ src/scan.js | 6 +++ test/leastIndex-test.js | 10 ++-- test/maxIndex-test.js | 110 ++++++++++++++++++++++++++++++++++++++++ test/minIndex-test.js | 110 ++++++++++++++++++++++++++++++++++++++++ 11 files changed, 293 insertions(+), 24 deletions(-) create mode 100644 src/maxIndex.js create mode 100644 src/minIndex.js create mode 100644 src/scan.js create mode 100644 test/maxIndex-test.js create mode 100644 test/minIndex-test.js diff --git a/src/extent.js b/src/extent.js index 44402daf..2e3738d6 100644 --- a/src/extent.js +++ b/src/extent.js @@ -2,10 +2,10 @@ export default function(values, valueof) { let min; let max; if (valueof === undefined) { - for (let value of values) { - if (value != null && value >= value) { + for (const value of values) { + if (value != null) { if (min === undefined) { - min = max = value; + if (value >= value) min = max = value; } else { if (min > value) min = value; if (max < value) max = value; @@ -15,9 +15,9 @@ export default function(values, valueof) { } else { let index = -1; for (let value of values) { - if ((value = valueof(value, ++index, values)) != null && value >= value) { + if ((value = valueof(value, ++index, values)) != null) { if (min === undefined) { - min = max = value; + if (value >= value) min = max = value; } else { if (min > value) min = value; if (max < value) max = value; diff --git a/src/index.js b/src/index.js index d1fc6077..004ea8e8 100644 --- a/src/index.js +++ b/src/index.js @@ -6,15 +6,17 @@ export {default as descending} from "./descending.js"; export {default as deviation} from "./deviation.js"; export {default as extent} from "./extent.js"; export {default as group} from "./group.js"; -export {default as bin, default as histogram} from "./bin.js"; // Deprecated alias: histogram. +export {default as bin, default as histogram} from "./bin.js"; // Deprecated; use bin. export {default as thresholdFreedmanDiaconis} from "./threshold/freedmanDiaconis.js"; export {default as thresholdScott} from "./threshold/scott.js"; export {default as thresholdSturges} from "./threshold/sturges.js"; export {default as max} from "./max.js"; +export {default as maxIndex} from "./maxIndex.js"; export {default as mean} from "./mean.js"; export {default as median} from "./median.js"; export {default as merge} from "./merge.js"; export {default as min} from "./min.js"; +export {default as minIndex} from "./minIndex.js"; export {default as pairs} from "./pairs.js"; export {default as permute} from "./permute.js"; export {default as quantile} from "./quantile.js"; @@ -22,7 +24,8 @@ export {default as quickselect} from "./quickselect.js"; export {default as range} from "./range.js"; export {default as rollup} from "./rollup.js"; export {default as least} from "./least.js"; -export {default as scan, default as leastIndex} from "./leastIndex.js"; // Deprecated alias: scan. +export {default as leastIndex} from "./leastIndex.js"; +export {default as scan} from "./scan.js"; // Deprecated; use leastIndex. export {default as shuffle} from "./shuffle.js"; export {default as sum} from "./sum.js"; export {default as ticks, tickIncrement, tickStep} from "./ticks.js"; diff --git a/src/leastIndex.js b/src/leastIndex.js index c089f483..3abf9058 100644 --- a/src/leastIndex.js +++ b/src/leastIndex.js @@ -2,11 +2,11 @@ import ascending from "./ascending.js"; export default function leastIndex(values, compare = ascending) { let min; - let minIndex; + let minIndex = -1; let index = -1; for (const value of values) { ++index; - if (minIndex === undefined + if (minIndex < 0 ? compare(value, value) === 0 : compare(value, min) < 0) { min = value; diff --git a/src/max.js b/src/max.js index 4ff90e58..ce287368 100644 --- a/src/max.js +++ b/src/max.js @@ -1,10 +1,9 @@ export default function max(values, valueof) { let max; if (valueof === undefined) { - for (let value of values) { + for (const value of values) { if (value != null - && value >= value - && (max === undefined || max < value)) { + && (max < value || (max === undefined && value >= value))) { max = value; } } @@ -12,8 +11,7 @@ export default function max(values, valueof) { let index = -1; for (let value of values) { if ((value = valueof(value, ++index, values)) != null - && value >= value - && (max === undefined || max < value)) { + && (max < value || (max === undefined && value >= value))) { max = value; } } diff --git a/src/maxIndex.js b/src/maxIndex.js new file mode 100644 index 00000000..87da1a2c --- /dev/null +++ b/src/maxIndex.js @@ -0,0 +1,22 @@ +export default function maxIndex(values, valueof) { + let max; + let maxIndex = -1; + let index = -1; + if (valueof === undefined) { + for (const value of values) { + ++index; + if (value != null + && (max < value || (max === undefined && value >= value))) { + max = value, maxIndex = index; + } + } + } else { + for (let value of values) { + if ((value = valueof(value, ++index, values)) != null + && (max < value || (max === undefined && value >= value))) { + max = value, maxIndex = index; + } + } + } + return maxIndex; +} diff --git a/src/min.js b/src/min.js index 018417ed..df88bfb2 100644 --- a/src/min.js +++ b/src/min.js @@ -1,10 +1,9 @@ export default function min(values, valueof) { let min; if (valueof === undefined) { - for (let value of values) { + for (const value of values) { if (value != null - && value >= value - && (min === undefined || min > value)) { + && (min > value || (min === undefined && value >= value))) { min = value; } } @@ -12,8 +11,7 @@ export default function min(values, valueof) { let index = -1; for (let value of values) { if ((value = valueof(value, ++index, values)) != null - && value >= value - && (min === undefined || min > value)) { + && (min > value || (min === undefined && value >= value))) { min = value; } } diff --git a/src/minIndex.js b/src/minIndex.js new file mode 100644 index 00000000..5c07d1ea --- /dev/null +++ b/src/minIndex.js @@ -0,0 +1,22 @@ +export default function minIndex(values, valueof) { + let min; + let minIndex = -1; + let index = -1; + if (valueof === undefined) { + for (const value of values) { + ++index; + if (value != null + && (min > value || (min === undefined && value >= value))) { + min = value, minIndex = index; + } + } + } else { + for (let value of values) { + if ((value = valueof(value, ++index, values)) != null + && (min > value || (min === undefined && value >= value))) { + min = value, minIndex = index; + } + } + } + return minIndex; +} diff --git a/src/scan.js b/src/scan.js new file mode 100644 index 00000000..9c538f8a --- /dev/null +++ b/src/scan.js @@ -0,0 +1,6 @@ +import leastIndex from "./leastIndex.js"; + +export default function scan(values, compare) { + const index = leastIndex(values, compare); + return index < 0 ? undefined : index; +} diff --git a/test/leastIndex-test.js b/test/leastIndex-test.js index c134a761..472fcb03 100644 --- a/test/leastIndex-test.js +++ b/test/leastIndex-test.js @@ -28,14 +28,14 @@ tape("leastIndex(array, compare) compares using the specified compare function", test.end(); }); -tape("leastIndex(array) returns undefined if the array is empty", function(test) { - test.strictEqual(arrays.leastIndex([]), undefined); +tape("leastIndex(array) returns -1 if the array is empty", function(test) { + test.strictEqual(arrays.leastIndex([]), -1); test.end(); }); -tape("leastIndex(array) returns undefined if the array contains only incomparable values", function(test) { - test.strictEqual(arrays.leastIndex([NaN, undefined]), undefined); - test.strictEqual(arrays.leastIndex([NaN, "foo"], function(a, b) { return a - b; }), undefined); +tape("leastIndex(array) returns -1 if the array contains only incomparable values", function(test) { + test.strictEqual(arrays.leastIndex([NaN, undefined]), -1); + test.strictEqual(arrays.leastIndex([NaN, "foo"], function(a, b) { return a - b; }), -1); test.end(); }); diff --git a/test/maxIndex-test.js b/test/maxIndex-test.js new file mode 100644 index 00000000..f91b7371 --- /dev/null +++ b/test/maxIndex-test.js @@ -0,0 +1,110 @@ +var tape = require("tape"), + arrays = require("../"); + +tape("maxIndex(array) returns the index of the greatest numeric value for numbers", function(test) { + test.deepEqual(arrays.maxIndex([1]), 0); + test.deepEqual(arrays.maxIndex([5, 1, 2, 3, 4]), 0); + test.deepEqual(arrays.maxIndex([20, 3]), 0); + test.deepEqual(arrays.maxIndex([3, 20]), 1); + test.end(); +}); + +tape("maxIndex(array) returns the greatest lexicographic value for strings", function(test) { + test.deepEqual(arrays.maxIndex(["c", "a", "b"]), 0); + test.deepEqual(arrays.maxIndex(["20", "3"]), 1); + test.deepEqual(arrays.maxIndex(["3", "20"]), 0); + test.end(); +}); + +tape("maxIndex(array) ignores null, undefined and NaN", function(test) { + var o = {valueOf: function() { return NaN; }}; + test.deepEqual(arrays.maxIndex([NaN, 1, 2, 3, 4, 5]), 5); + test.deepEqual(arrays.maxIndex([o, 1, 2, 3, 4, 5]), 5); + test.deepEqual(arrays.maxIndex([1, 2, 3, 4, 5, NaN]), 4); + test.deepEqual(arrays.maxIndex([1, 2, 3, 4, 5, o]), 4); + test.deepEqual(arrays.maxIndex([10, null, 3, undefined, 5, NaN]), 0); + test.deepEqual(arrays.maxIndex([-1, null, -3, undefined, -5, NaN]), 0); + test.end(); +}); + +tape("maxIndex(array) compares heterogenous types as numbers", function(test) { + test.equal(arrays.maxIndex([20, "3"]), 0); + test.equal(arrays.maxIndex(["20", 3]), 0); + test.equal(arrays.maxIndex([3, "20"]), 1); + test.equal(arrays.maxIndex(["3", 20]), 1); + test.end(); +}); + +tape("maxIndex(array) returns -1 if the array contains no numbers", function(test) { + test.equal(arrays.maxIndex([]), -1); + test.equal(arrays.maxIndex([null]), -1); + test.equal(arrays.maxIndex([undefined]), -1); + test.equal(arrays.maxIndex([NaN]), -1); + test.equal(arrays.maxIndex([NaN, NaN]), -1); + test.end(); +}); + +tape("maxIndex(array, f) returns the greatest numeric value for numbers", function(test) { + test.deepEqual(arrays.maxIndex([1].map(box), unbox), 0); + test.deepEqual(arrays.maxIndex([5, 1, 2, 3, 4].map(box), unbox), 0); + test.deepEqual(arrays.maxIndex([20, 3].map(box), unbox), 0); + test.deepEqual(arrays.maxIndex([3, 20].map(box), unbox), 1); + test.end(); +}); + +tape("maxIndex(array, f) returns the greatest lexicographic value for strings", function(test) { + test.deepEqual(arrays.maxIndex(["c", "a", "b"].map(box), unbox), 0); + test.deepEqual(arrays.maxIndex(["20", "3"].map(box), unbox), 1); + test.deepEqual(arrays.maxIndex(["3", "20"].map(box), unbox), 0); + test.end(); +}); + +tape("maxIndex(array, f) ignores null, undefined and NaN", function(test) { + var o = {valueOf: function() { return NaN; }}; + test.deepEqual(arrays.maxIndex([NaN, 1, 2, 3, 4, 5].map(box), unbox), 5); + test.deepEqual(arrays.maxIndex([o, 1, 2, 3, 4, 5].map(box), unbox), 5); + test.deepEqual(arrays.maxIndex([1, 2, 3, 4, 5, NaN].map(box), unbox), 4); + test.deepEqual(arrays.maxIndex([1, 2, 3, 4, 5, o].map(box), unbox), 4); + test.deepEqual(arrays.maxIndex([10, null, 3, undefined, 5, NaN].map(box), unbox), 0); + test.deepEqual(arrays.maxIndex([-1, null, -3, undefined, -5, NaN].map(box), unbox), 0); + test.end(); +}); + +tape("maxIndex(array, f) compares heterogenous types as numbers", function(test) { + test.equal(arrays.maxIndex([20, "3"].map(box), unbox), 0); + test.equal(arrays.maxIndex(["20", 3].map(box), unbox), 0); + test.equal(arrays.maxIndex([3, "20"].map(box), unbox), 1); + test.equal(arrays.maxIndex(["3", 20].map(box), unbox), 1); + test.end(); +}); + +tape("maxIndex(array, f) returns -1 if the array contains no observed values", function(test) { + test.equal(arrays.maxIndex([].map(box), unbox), -1); + test.equal(arrays.maxIndex([null].map(box), unbox), -1); + test.equal(arrays.maxIndex([undefined].map(box), unbox), -1); + test.equal(arrays.maxIndex([NaN].map(box), unbox), -1); + test.equal(arrays.maxIndex([NaN, NaN].map(box), unbox), -1); + test.end(); +}); + +tape("maxIndex(array, f) passes the accessor d, i, and array", function(test) { + var results = [], array = ["a", "b", "c"]; + arrays.maxIndex(array, function(d, i, array) { results.push([d, i, array]); }); + test.deepEqual(results, [["a", 0, array], ["b", 1, array], ["c", 2, array]]); + test.end(); +}); + +tape("maxIndex(array, f) uses the global context", function(test) { + var results = []; + arrays.maxIndex([1, 2], function() { results.push(this); }); + test.deepEqual(results, [global, global]); + test.end(); +}); + +function box(value) { + return {value: value}; +} + +function unbox(box) { + return box.value; +} diff --git a/test/minIndex-test.js b/test/minIndex-test.js new file mode 100644 index 00000000..bbc84bdc --- /dev/null +++ b/test/minIndex-test.js @@ -0,0 +1,110 @@ +var tape = require("tape"), + arrays = require("../"); + +tape("minIndex(array) returns the index of the least numeric value for numbers", function(test) { + test.deepEqual(arrays.minIndex([1]), 0); + test.deepEqual(arrays.minIndex([5, 1, 2, 3, 4]), 1); + test.deepEqual(arrays.minIndex([20, 3]), 1); + test.deepEqual(arrays.minIndex([3, 20]), 0); + test.end(); +}); + +tape("minIndex(array) returns the index of the least lexicographic value for strings", function(test) { + test.deepEqual(arrays.minIndex(["c", "a", "b"]), 1); + test.deepEqual(arrays.minIndex(["20", "3"]), 0); + test.deepEqual(arrays.minIndex(["3", "20"]), 1); + test.end(); +}); + +tape("minIndex(array) ignores null, undefined and NaN", function(test) { + var o = {valueOf: function() { return NaN; }}; + test.deepEqual(arrays.minIndex([NaN, 1, 2, 3, 4, 5]), 1); + test.deepEqual(arrays.minIndex([o, 1, 2, 3, 4, 5]), 1); + test.deepEqual(arrays.minIndex([1, 2, 3, 4, 5, NaN]), 0); + test.deepEqual(arrays.minIndex([1, 2, 3, 4, 5, o]), 0); + test.deepEqual(arrays.minIndex([10, null, 3, undefined, 5, NaN]), 2); + test.deepEqual(arrays.minIndex([-1, null, -3, undefined, -5, NaN]), 4); + test.end(); +}); + +tape("minIndex(array) compares heterogenous types as numbers", function(test) { + test.equal(arrays.minIndex([20, "3"]), 1); + test.equal(arrays.minIndex(["20", 3]), 1); + test.equal(arrays.minIndex([3, "20"]), 0); + test.equal(arrays.minIndex(["3", 20]), 0); + test.end(); +}); + +tape("minIndex(array) returns -1 if the array contains no numbers", function(test) { + test.equal(arrays.minIndex([]), -1); + test.equal(arrays.minIndex([null]), -1); + test.equal(arrays.minIndex([undefined]), -1); + test.equal(arrays.minIndex([NaN]), -1); + test.equal(arrays.minIndex([NaN, NaN]), -1); + test.end(); +}); + +tape("minIndex(array, f) returns the index of the least numeric value for numbers", function(test) { + test.deepEqual(arrays.minIndex([1].map(box), unbox), 0); + test.deepEqual(arrays.minIndex([5, 1, 2, 3, 4].map(box), unbox), 1); + test.deepEqual(arrays.minIndex([20, 3].map(box), unbox), 1); + test.deepEqual(arrays.minIndex([3, 20].map(box), unbox), 0); + test.end(); +}); + +tape("minIndex(array, f) returns the index of the least lexicographic value for strings", function(test) { + test.deepEqual(arrays.minIndex(["c", "a", "b"].map(box), unbox), 1); + test.deepEqual(arrays.minIndex(["20", "3"].map(box), unbox), 0); + test.deepEqual(arrays.minIndex(["3", "20"].map(box), unbox), 1); + test.end(); +}); + +tape("minIndex(array, f) ignores null, undefined and NaN", function(test) { + var o = {valueOf: function() { return NaN; }}; + test.deepEqual(arrays.minIndex([NaN, 1, 2, 3, 4, 5].map(box), unbox), 1); + test.deepEqual(arrays.minIndex([o, 1, 2, 3, 4, 5].map(box), unbox), 1); + test.deepEqual(arrays.minIndex([1, 2, 3, 4, 5, NaN].map(box), unbox), 0); + test.deepEqual(arrays.minIndex([1, 2, 3, 4, 5, o].map(box), unbox), 0); + test.deepEqual(arrays.minIndex([10, null, 3, undefined, 5, NaN].map(box), unbox), 2); + test.deepEqual(arrays.minIndex([-1, null, -3, undefined, -5, NaN].map(box), unbox), 4); + test.end(); +}); + +tape("minIndex(array, f) compares heterogenous types as numbers", function(test) { + test.equal(arrays.minIndex([20, "3"].map(box), unbox), 1); + test.equal(arrays.minIndex(["20", 3].map(box), unbox), 1); + test.equal(arrays.minIndex([3, "20"].map(box), unbox), 0); + test.equal(arrays.minIndex(["3", 20].map(box), unbox), 0); + test.end(); +}); + +tape("minIndex(array, f) returns -1 if the array contains no observed values", function(test) { + test.equal(arrays.minIndex([].map(box), unbox), -1); + test.equal(arrays.minIndex([null].map(box), unbox), -1); + test.equal(arrays.minIndex([undefined].map(box), unbox), -1); + test.equal(arrays.minIndex([NaN].map(box), unbox), -1); + test.equal(arrays.minIndex([NaN, NaN].map(box), unbox), -1); + test.end(); +}); + +tape("minIndex(array, f) passes the accessor d, i, and array", function(test) { + var results = [], array = ["a", "b", "c"]; + arrays.minIndex(array, function(d, i, array) { results.push([d, i, array]); }); + test.deepEqual(results, [["a", 0, array], ["b", 1, array], ["c", 2, array]]); + test.end(); +}); + +tape("minIndex(array, f) uses the global context", function(test) { + var results = []; + arrays.minIndex([1, 2], function() { results.push(this); }); + test.deepEqual(results, [global, global]); + test.end(); +}); + +function box(value) { + return {value: value}; +} + +function unbox(box) { + return box.value; +} From faa1b81e657441b053dfd64b0054208277eda182 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Sun, 19 May 2019 21:19:43 -0700 Subject: [PATCH 6/8] Tests for least. --- test/least-test.js | 58 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 test/least-test.js diff --git a/test/least-test.js b/test/least-test.js new file mode 100644 index 00000000..531c78b7 --- /dev/null +++ b/test/least-test.js @@ -0,0 +1,58 @@ +var tape = require("tape"), + arrays = require("../"); + +tape("least(array) compares using natural order", function(test) { + test.strictEqual(arrays.least([0, 1]), 0); + test.strictEqual(arrays.least([1, 0]), 0); + test.strictEqual(arrays.least([0, "1"]), 0); + test.strictEqual(arrays.least(["1", 0]), 0); + test.strictEqual(arrays.least(["10", "2"]), "10"); + test.strictEqual(arrays.least(["2", "10"]), "10"); + test.strictEqual(arrays.least(["10", "2", NaN]), "10"); + test.strictEqual(arrays.least([NaN, "10", "2"]), "10"); + test.strictEqual(arrays.least(["2", NaN, "10"]), "10"); + test.strictEqual(arrays.least([2, NaN, 10]), 2); + test.strictEqual(arrays.least([10, 2, NaN]), 2); + test.strictEqual(arrays.least([NaN, 10, 2]), 2); + test.end(); +}); + +tape("least(array, compare) compares using the specified compare function", function(test) { + var a = {name: "a"}, b = {name: "b"}; + test.deepEqual(arrays.least([a, b], function(a, b) { return a.name.localeCompare(b.name); }), {name: "a"}); + test.strictEqual(arrays.least([1, 0], arrays.descending), 1); + test.strictEqual(arrays.least(["1", 0], arrays.descending), "1"); + test.strictEqual(arrays.least(["2", "10"], arrays.descending), "2"); + test.strictEqual(arrays.least(["2", NaN, "10"], arrays.descending), "2"); + test.strictEqual(arrays.least([2, NaN, 10], arrays.descending), 10); + test.end(); +}); + +tape("least(array) returns undefined if the array is empty", function(test) { + test.strictEqual(arrays.least([]), undefined); + test.end(); +}); + +tape("least(array) returns undefined if the array contains only incomparable values", function(test) { + test.strictEqual(arrays.least([NaN, undefined]), undefined); + test.strictEqual(arrays.least([NaN, "foo"], function(a, b) { return a - b; }), undefined); + test.end(); +}); + +tape("least(array) returns the first of equal values", function(test) { + test.deepEqual(arrays.least([2, 2, 1, 1, 0, 0, 0, 3, 0].map(box), ascendingValue), {value: 0, index: 4}); + test.deepEqual(arrays.least([3, 2, 2, 1, 1, 0, 0, 0, 3, 0].map(box), descendingValue), {value: 3, index: 0}); + test.end(); +}); + +function box(value, index) { + return {value, index}; +} + +function ascendingValue(a, b) { + return a.value - b.value; +} + +function descendingValue(a, b) { + return b.value - a.value; +} From 8fe859622d783f6d74812f7016fc3af0e9486bfe Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Sun, 19 May 2019 21:23:52 -0700 Subject: [PATCH 7/8] Update README. --- README.md | 38 +++++++++++++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index bc717f3d..1c6c21d5 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,13 @@ Returns the minimum value in the given *iterable* using natural order. If the it Unlike the built-in [Math.min](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Math/min), this method ignores undefined, null and NaN values; this is useful for ignoring missing data. In addition, elements are compared using natural order rather than numeric order. For example, the minimum of the strings [“20”, “3”] is “20”, while the minimum of the numbers [20, 3] is 3. -See also [scan](#scan) and [extent](#extent). +See also [extent](#extent). + +# d3.minIndex(iterable[, accessor]) · [Source](https://github.com/d3/d3-array/blob/master/src/minIndex.js) + +Returns the index of the minimum value in the given *iterable* using natural order. If the iterable contains no comparable values, returns -1. An optional *accessor* function may be specified, which is equivalent to calling Array.from before computing the minimum value. + +Unlike the built-in [Math.min](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Math/min), this method ignores undefined, null and NaN values; this is useful for ignoring missing data. In addition, elements are compared using natural order rather than numeric order. For example, the minimum of the strings [“20”, “3”] is “20”, while the minimum of the numbers [20, 3] is 3. # d3.max(iterable[, accessor]) · [Source](https://github.com/d3/d3-array/blob/master/src/max.js) @@ -68,7 +74,13 @@ Returns the maximum value in the given *iterable* using natural order. If the it Unlike the built-in [Math.max](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Math/max), this method ignores undefined values; this is useful for ignoring missing data. In addition, elements are compared using natural order rather than numeric order. For example, the maximum of the strings [“20”, “3”] is “3”, while the maximum of the numbers [20, 3] is 20. -See also [scan](#scan) and [extent](#extent). +See also [extent](#extent). + +# d3.maxIndex(iterable[, accessor]) · [Source](https://github.com/d3/d3-array/blob/master/src/maxIndex.js) + +Returns the index of the maximum value in the given *iterable* using natural order. If the iterable contains no comparable values, returns -1. An optional *accessor* function may be specified, which is equivalent to calling Array.from before computing the maximum value. + +Unlike the built-in [Math.max](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Math/max), this method ignores undefined values; this is useful for ignoring missing data. In addition, elements are compared using natural order rather than numeric order. For example, the maximum of the strings [“20”, “3”] is “3”, while the maximum of the numbers [20, 3] is 20. # d3.extent(iterable[, accessor]) · [Source](https://github.com/d3/d3-array/blob/master/src/extent.js) @@ -114,9 +126,21 @@ Returns the standard deviation, defined as the square root of the [bias-correcte Methods for searching arrays for a specific element. -# d3.scan(iterable[, comparator]) · [Source](https://github.com/d3/d3-array/blob/master/src/scan.js) +# d3.least(iterable[, comparator]) · [Source](https://github.com/d3/d3-array/blob/master/src/least.js) -Performs a linear scan of the specified *iterable*, returning the index of the least element according to the specified *comparator*. If the given *iterable* contains no comparable elements (*i.e.*, the comparator returns NaN when comparing each element to itself), returns undefined. If *comparator* is not specified, it defaults to [ascending](#ascending). For example: +Returns the least element of the specified *iterable* according to the specified *comparator*. If the given *iterable* contains no comparable elements (*i.e.*, the comparator returns NaN when comparing each element to itself), returns undefined. If *comparator* is not specified, it defaults to [ascending](#ascending). For example: + +```js +const array = [{foo: 42}, {foo: 91}]; +d3.scan(array, (a, b) => a.foo - b.foo); // {foo: 42} +d3.scan(array, (a, b) => b.foo - a.foo); // {foo: 91} +``` + +This function is similar to [min](#min), except it allows the use of a comparator rather than an accessor. + +# d3.leastIndex(iterable[, comparator]) · [Source](https://github.com/d3/d3-array/blob/master/src/leastIndex.js) + +Returns the index of the least element of the specified *iterable* according to the specified *comparator*. If the given *iterable* contains no comparable elements (*i.e.*, the comparator returns NaN when comparing each element to itself), returns -1. If *comparator* is not specified, it defaults to [ascending](#ascending). For example: ```js const array = [{foo: 42}, {foo: 91}]; @@ -124,7 +148,11 @@ d3.scan(array, (a, b) => a.foo - b.foo); // 0 d3.scan(array, (a, b) => b.foo - a.foo); // 1 ``` -This function is similar to [min](#min), except it allows the use of a comparator rather than an accessor and it returns the index instead of the accessed value. See also [bisect](#bisect). +This function is similar to [minIndex](#minIndex), except it allows the use of a comparator rather than an accessor. + +# d3.scan(iterable[, comparator]) · [Source](https://github.com/d3/d3-array/blob/master/src/scan.js) + +Deprecated; use [leastIndex](#leastIndex) instead. # d3.bisectLeft(array, x[, lo[, hi]]) · [Source](https://github.com/d3/d3-array/blob/master/src/bisect.js) From dfe6f59e8b231fb049f10fd9dc2584875312d5ea Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Sun, 19 May 2019 21:25:58 -0700 Subject: [PATCH 8/8] Update README; restore scan test. --- README.md | 8 ++++---- test/scan-test.js | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 4 deletions(-) create mode 100644 test/scan-test.js diff --git a/README.md b/README.md index 1c6c21d5..f8d22398 100644 --- a/README.md +++ b/README.md @@ -132,8 +132,8 @@ Returns the least element of the specified *iterable* according to the specified ```js const array = [{foo: 42}, {foo: 91}]; -d3.scan(array, (a, b) => a.foo - b.foo); // {foo: 42} -d3.scan(array, (a, b) => b.foo - a.foo); // {foo: 91} +d3.least(array, (a, b) => a.foo - b.foo); // {foo: 42} +d3.least(array, (a, b) => b.foo - a.foo); // {foo: 91} ``` This function is similar to [min](#min), except it allows the use of a comparator rather than an accessor. @@ -144,8 +144,8 @@ Returns the index of the least element of the specified *iterable* according to ```js const array = [{foo: 42}, {foo: 91}]; -d3.scan(array, (a, b) => a.foo - b.foo); // 0 -d3.scan(array, (a, b) => b.foo - a.foo); // 1 +d3.leastIndex(array, (a, b) => a.foo - b.foo); // 0 +d3.leastIndex(array, (a, b) => b.foo - a.foo); // 1 ``` This function is similar to [minIndex](#minIndex), except it allows the use of a comparator rather than an accessor. diff --git a/test/scan-test.js b/test/scan-test.js new file mode 100644 index 00000000..8289f354 --- /dev/null +++ b/test/scan-test.js @@ -0,0 +1,46 @@ +var tape = require("tape"), + arrays = require("../"); + +tape("scan(array) compares using natural order", function(test) { + test.strictEqual(arrays.scan([0, 1]), 0); + test.strictEqual(arrays.scan([1, 0]), 1); + test.strictEqual(arrays.scan([0, "1"]), 0); + test.strictEqual(arrays.scan(["1", 0]), 1); + test.strictEqual(arrays.scan(["10", "2"]), 0); + test.strictEqual(arrays.scan(["2", "10"]), 1); + test.strictEqual(arrays.scan(["10", "2", NaN]), 0); + test.strictEqual(arrays.scan([NaN, "10", "2"]), 1); + test.strictEqual(arrays.scan(["2", NaN, "10"]), 2); + test.strictEqual(arrays.scan([2, NaN, 10]), 0); + test.strictEqual(arrays.scan([10, 2, NaN]), 1); + test.strictEqual(arrays.scan([NaN, 10, 2]), 2); + test.end(); +}); + +tape("scan(array, compare) compares using the specified compare function", function(test) { + var a = {name: "a"}, b = {name: "b"}; + test.strictEqual(arrays.scan([a, b], function(a, b) { return a.name.localeCompare(b.name); }), 0); + test.strictEqual(arrays.scan([1, 0], arrays.descending), 0); + test.strictEqual(arrays.scan(["1", 0], arrays.descending), 0); + test.strictEqual(arrays.scan(["2", "10"], arrays.descending), 0); + test.strictEqual(arrays.scan(["2", NaN, "10"], arrays.descending), 0); + test.strictEqual(arrays.scan([2, NaN, 10], arrays.descending), 2); + test.end(); +}); + +tape("scan(array) returns undefined if the array is empty", function(test) { + test.strictEqual(arrays.scan([]), undefined); + test.end(); +}); + +tape("scan(array) returns undefined if the array contains only incomparable values", function(test) { + test.strictEqual(arrays.scan([NaN, undefined]), undefined); + test.strictEqual(arrays.scan([NaN, "foo"], function(a, b) { return a - b; }), undefined); + test.end(); +}); + +tape("scan(array) returns the first of equal values", function(test) { + test.strictEqual(arrays.scan([2, 2, 1, 1, 0, 0, 0, 3, 0]), 4); + test.strictEqual(arrays.scan([3, 2, 2, 1, 1, 0, 0, 0, 3, 0], arrays.descending), 0); + test.end(); +});