Checkpoint before integration and package validation. Assisted-by: SurmAgent:unknown [read] [edit] [bash]
260 lines
7.7 KiB
JavaScript
260 lines
7.7 KiB
JavaScript
import { assert } from "./assert.js";
|
|
|
|
/* SortCompare calls comparefn for every pair it compares; there is no
|
|
allowance for skipping the call when the two values happen to be the same
|
|
value. Code that uses the comparator for its side effects depends on it. */
|
|
|
|
/* the jQuery uniqueSort() pattern: the comparator is handed the same object
|
|
twice and records that it saw a duplicate */
|
|
{
|
|
const o = {};
|
|
let dups = false;
|
|
const r = [o, o].sort((a, b) => { if (a === b) dups = true; return 0; });
|
|
assert(dups, true);
|
|
assert(r.length, 2);
|
|
assert(r[0] === o, true);
|
|
assert(r[1] === o, true);
|
|
}
|
|
|
|
/* identical primitives get the same treatment */
|
|
{
|
|
const seen = [];
|
|
[1, 1].sort((a, b) => { seen.push(a, b); return 0; });
|
|
assert(seen.length, 2);
|
|
assert(seen[0], 1);
|
|
assert(seen[1], 1);
|
|
}
|
|
|
|
/* toSorted() sorts through the same path */
|
|
{
|
|
const o = {};
|
|
let dups = false;
|
|
const r = [o, o].toSorted((a, b) => { if (a === b) dups = true; return 0; });
|
|
assert(dups, true);
|
|
assert(r.length, 2);
|
|
assert(r[0] === o, true);
|
|
}
|
|
|
|
/* an exception from the comparator is not swallowed for identical values */
|
|
{
|
|
const o = {};
|
|
let err;
|
|
try {
|
|
[o, o].sort(() => { throw new RangeError("boom"); });
|
|
} catch (e) {
|
|
err = e;
|
|
}
|
|
assert(err instanceof RangeError, true);
|
|
assert(err.message, "boom");
|
|
}
|
|
|
|
/* the same holds beyond the insertion sort cutoff: every comparison of the
|
|
all-identical array reaches the comparator */
|
|
{
|
|
const o = {};
|
|
let calls = 0;
|
|
const r = new Array(100).fill(o).sort((x, y) => {
|
|
if (x === o && y === o) calls++;
|
|
return 0;
|
|
});
|
|
assert(calls >= 99, true); /* at least one comparison per element */
|
|
assert(r.length, 100);
|
|
assert(r.every(v => v === o), true);
|
|
}
|
|
|
|
/* the shortcut compared the two values bit for bit, so it caught every type
|
|
whose JSValue is the value itself or a shared pointer, not just objects */
|
|
{
|
|
function calls(arr) {
|
|
let n = 0;
|
|
Array.prototype.sort.call(arr, () => { n++; return 0; });
|
|
return n;
|
|
}
|
|
|
|
const s = "abc";
|
|
const sym = Symbol("s");
|
|
const o = {};
|
|
assert(calls([s, s]), 1, "same string");
|
|
assert(calls([NaN, NaN]), 1, "same NaN");
|
|
assert(calls([1n, 1n]), 1, "same bigint");
|
|
assert(calls([sym, sym]), 1, "same symbol");
|
|
assert(calls([true, true]), 1, "same boolean");
|
|
assert(calls([null, null]), 1, "same null");
|
|
assert(calls([o, o]), 1, "same object");
|
|
assert(calls([o, o, o]), 2, "three identical objects");
|
|
|
|
/* an array-like sorted through .call() takes the same path */
|
|
assert(calls({ length: 2, 0: o, 1: o }), 1, "array-like");
|
|
}
|
|
|
|
/* undefined and holes are still sorted to the end without ever reaching the
|
|
comparator: that is SortCompare's own rule, not the shortcut */
|
|
{
|
|
let n = 0;
|
|
const cmp = () => { n++; return 0; };
|
|
|
|
n = 0;
|
|
assert([undefined, undefined].sort(cmp).length, 2);
|
|
assert(n, 0, "two undefined");
|
|
|
|
n = 0;
|
|
const mixed = [undefined, 1].sort(cmp);
|
|
assert(n, 0, "undefined and a value");
|
|
assert(mixed[0], 1);
|
|
assert(mixed[1], undefined);
|
|
|
|
n = 0;
|
|
const holes = new Array(3);
|
|
holes[0] = 1;
|
|
holes.sort(cmp);
|
|
assert(n, 0, "holes");
|
|
assert(holes[0], 1);
|
|
assert(1 in holes, false);
|
|
}
|
|
|
|
/* typed arrays sort through a different comparison function that never had
|
|
the shortcut; it must keep calling the comparator too */
|
|
{
|
|
for (const Ctor of [Int8Array, Uint8Array, Int32Array, Float64Array]) {
|
|
let n = 0;
|
|
const t = new Ctor([1, 1, 1]);
|
|
t.sort(() => { n++; return 0; });
|
|
assert(n, 2, Ctor.name);
|
|
|
|
n = 0;
|
|
new Ctor([1, 1, 1]).toSorted(() => { n++; return 0; });
|
|
assert(n, 2, Ctor.name + " toSorted");
|
|
}
|
|
|
|
/* including a bigint typed array */
|
|
let n = 0;
|
|
new BigInt64Array([1n, 1n, 1n]).sort(() => { n++; return 0; });
|
|
assert(n, 2, "BigInt64Array");
|
|
}
|
|
|
|
/* now that identical values reach the comparator too, there are more calls
|
|
from which user code can reach back into the array being sorted */
|
|
{
|
|
/* shrinking the array from the comparator */
|
|
{
|
|
const a = [3, 1, 3, 1, 3, 1, 3, 1];
|
|
let calls = 0;
|
|
a.sort((x, y) => {
|
|
calls++;
|
|
if (calls === 2) a.length = 3;
|
|
return x - y;
|
|
});
|
|
assert(calls > 0, true);
|
|
/* sort collects the elements before it compares any of them, so the
|
|
truncation is undone when the sorted list is written back */
|
|
assert(a.length, 8);
|
|
assert(a.join(","), "1,1,1,1,3,3,3,3");
|
|
}
|
|
|
|
/* growing it */
|
|
{
|
|
const a = [2, 2, 2, 2];
|
|
let calls = 0;
|
|
a.sort((x, y) => {
|
|
if (++calls === 1) a.push(1, 1);
|
|
return x - y;
|
|
});
|
|
assert(a.length, 6);
|
|
assert(a.every(v => v === 1 || v === 2), true);
|
|
}
|
|
|
|
/* deleting elements, which turns them into holes that sort to the end */
|
|
{
|
|
const a = [1, 1, 1, 1, 1];
|
|
a.sort((x, y) => {
|
|
delete a[4];
|
|
return x - y;
|
|
});
|
|
assert(a.length, 5);
|
|
}
|
|
|
|
/* reversing it under the sort's feet */
|
|
{
|
|
const a = [1, 1, 2, 2, 3, 3];
|
|
let calls = 0;
|
|
const out = a.sort((x, y) => {
|
|
if (++calls === 3) a.reverse();
|
|
return x - y;
|
|
});
|
|
assert(out, a);
|
|
assert(a.length, 6);
|
|
}
|
|
|
|
/* a comparator that sorts the same array again */
|
|
{
|
|
const a = [2, 2, 1, 1];
|
|
let depth = 0;
|
|
a.sort(function cmp(x, y) {
|
|
if (depth === 0) {
|
|
depth++;
|
|
a.slice().sort(cmp);
|
|
depth--;
|
|
}
|
|
return x - y;
|
|
});
|
|
assert(a.join(","), "1,1,2,2");
|
|
}
|
|
}
|
|
|
|
/* the comparator's return value is coerced with ToNumber, and anything that
|
|
is not less than or greater than zero leaves the order alone */
|
|
{
|
|
const mk = () => [{ i: 0 }, { i: 1 }, { i: 2 }, { i: 3 }];
|
|
const order = a => a.map(v => v.i).join(",");
|
|
|
|
assert(order(mk().sort(() => NaN)), "0,1,2,3");
|
|
assert(order(mk().sort(() => undefined)), "0,1,2,3");
|
|
assert(order(mk().sort(() => "")), "0,1,2,3");
|
|
assert(order(mk().sort(() => null)), "0,1,2,3");
|
|
assert(order(mk().sort(() => -0)), "0,1,2,3");
|
|
assert(order(mk().sort(() => "0")), "0,1,2,3");
|
|
assert(order(mk().sort(() => false)), "0,1,2,3");
|
|
assert(order(mk().sort(() => 0.5)), "3,2,1,0");
|
|
assert(order(mk().sort(() => "-1")), "0,1,2,3");
|
|
|
|
/* a comparator that is not callable is a TypeError before any call */
|
|
for (const bad of [null, 1, "x", true, {}, Symbol()]) {
|
|
let threw = false;
|
|
try {
|
|
mk().sort(bad);
|
|
} catch (e) {
|
|
threw = e instanceof TypeError;
|
|
}
|
|
assert(threw, true, String(bad));
|
|
}
|
|
/* undefined means the default comparator, and is not an error */
|
|
assert(mk().sort(undefined).length, 4);
|
|
}
|
|
|
|
/* a long run of values the shortcut used to skip entirely still sorts, is
|
|
still stable, and calls the comparator for every comparison it makes */
|
|
{
|
|
const n = 2000;
|
|
const a = [];
|
|
for (let i = 0; i < n; i++)
|
|
a.push({ key: i % 3, i });
|
|
let calls = 0;
|
|
a.sort((x, y) => { calls++; return x.key - y.key; });
|
|
assert(calls > 0, true);
|
|
assert(a.length, n);
|
|
for (let i = 1; i < n; i++) {
|
|
assert(a[i - 1].key <= a[i].key, true, `order at ${i}`);
|
|
if (a[i - 1].key === a[i].key)
|
|
assert(a[i - 1].i < a[i].i, true, `stability at ${i}`);
|
|
}
|
|
|
|
/* the same array where every element is the identical object */
|
|
const same = {};
|
|
const b = new Array(n).fill(same);
|
|
let same_calls = 0;
|
|
b.sort(() => { same_calls++; return 0; });
|
|
assert(same_calls > 0, true);
|
|
assert(b.length, n);
|
|
assert(b.every(v => v === same), true);
|
|
}
|