shelving
Version:
Toolkit for using data in JavaScript.
135 lines (134 loc) • 5.18 kB
JavaScript
import { isArray } from "./array.js";
/**
* Compare two unknown values in ascending order.
* - Allows values of different types to be ranked for sorting.
*
* 1. Numbers and dates (ascending order: -Infinity, negative, zero, positive, Infinity, NaN)
* 2. Strings (locale-aware order)
* 3. `true`
* 4. `false`
* 5. `null`
* 6. Unsorted values (objects that can't be converted to number or string, symbols, NaN, etc)
* 7. `undefined`
*
* @param x The first value to rank.
* @param y The second value to rank.
*
* @returns Number below zero if `a` is higher, number above zero if `b` is higher, or `0` if they're equally sorted.
*/
export function compareAscending(left, right) {
// Exactly equal is easy.
if (left === right)
return 0;
// Switch for type.
if (typeof left === "number" && !Number.isNaN(left)) {
// Number rank.
if (typeof right === "number" && !Number.isNaN(right) && right < left)
return 1;
}
else if (typeof left === "string") {
// String rank (uses locale-aware comparison).
if (typeof right === "string")
return left.localeCompare(right);
// right is higher if number.
if (typeof right === "number")
return 1;
}
else if (left === true) {
// right is higher if number, string.
if ((typeof right === "number" && !Number.isNaN(right)) || typeof right === "string")
return 1;
}
else if (left === false) {
// right is higher if number, string.
if ((typeof right === "number" && !Number.isNaN(right)) || typeof right === "string" || right === true)
return 1;
}
else if (left === null) {
// right is higher if number, string, boolean.
if ((typeof right === "number" && !Number.isNaN(right)) || typeof right === "string" || typeof right === "boolean")
return 1;
}
else if (typeof left !== "undefined") {
// Anything else, e.g. object, array, symbol, NaN.
// right is higher if number, string, boolean, null.
if ((typeof right === "number" && !Number.isNaN(right)) || typeof right === "string" || typeof right === "boolean" || right === null)
return 1;
// Undefined is lower, anything else is equal.
if (typeof right !== "undefined")
return 0;
}
else {
// Both undefined.
if (typeof right === "undefined")
return 0;
// right is higher if not undefined
return 1;
}
// Otherwise a is higher.
return -1;
}
/** Compare two unknown values in descending order. */
export function compareDescending(left, right) {
return 0 - compareAscending(left, right);
}
/**
* Quick sort algorithm.
* DH: We implement our own sorting algorithm so that:
* 1. We can calculate the return true/false (whether it changed).
* 2. We can use our own comparison function by default.
*
* @param items The actual list of items that's sorted in place.
* @param compare A rank function that takes a left/right value and returns 1/0/-1 (like `Array.prototype.sort()`).
* @param leftPointer Index in the set of items to start sorting on.
* @param rightPointer Index in the set of items to stop sorting on.
*
* @return `true` if the array order changed, and `false` otherwise.
*/
function _quicksort(items, compare, args, leftPointer = 0, rightPointer = items.length - 1) {
// Have any swaps been made?
let changed = false;
// Calculate the middle value.
const pivot = Math.floor((leftPointer + rightPointer) / 2);
// biome-ignore lint/style/noNonNullAssertion: Pointers keep track of this.
const middle = items[pivot];
// Partitioning.
let l = leftPointer;
let r = rightPointer;
while (l <= r) {
// biome-ignore lint/style/noNonNullAssertion: Pointers keep track of this.
while (compare(items[l], middle, ...args) < 0)
l++;
// biome-ignore lint/style/noNonNullAssertion: Pointers keep track of this.
while (compare(items[r], middle, ...args) > 0)
r--;
if (l <= r) {
if (l < r) {
changed = true;
// biome-ignore lint/style/noNonNullAssertion: Pointers keep track of this.
[items[l], items[r]] = [items[r], items[l]];
}
l++;
r--;
}
}
// Sort the lower and upper segments.
if (leftPointer < l - 1 && _quicksort(items, compare, args, leftPointer, l - 1))
changed = true;
if (l < rightPointer && _quicksort(items, compare, args, l, rightPointer))
changed = true;
// Changes were made.
return changed;
}
/** Sort an iterable set of items using a ranker (defaults to sorting in ascending order). */
export function sortArray(input, compare = compareAscending, ...args) {
if (isArray(input)) {
if (input.length < 2)
return input;
const output = Array.from(input);
return _quicksort(output, compare, args) ? output : input;
}
const output = Array.from(input);
_quicksort(output, compare, args);
return output;
}