Stable Sort By Multiple Keys
Sort records by several key selectors and directions with a guaranteed original-index tiebreak, so equal rows never swap positions.
Code
function compareValues(left, right) {
if (left === right || Object.is(left, right)) return 0;
if (left === null || left === undefined) return 1;
if (right === null || right === undefined) return -1;
if (typeof left === "number" && typeof right === "number") {
if (Number.isNaN(left)) return Number.isNaN(right) ? 0 : 1;
if (Number.isNaN(right)) return -1;
return left - right;
}
if (left instanceof Date && right instanceof Date) {
return left.getTime() - right.getTime();
}
const leftText = String(left);
const rightText = String(right);
if (leftText === rightText) return 0;
return leftText < rightText ? -1 : 1;
}
export function stableSortBy(items, selectors) {
if (!Array.isArray(items)) throw new TypeError("items must be an array");
if (!Array.isArray(selectors) || selectors.length === 0) {
throw new TypeError("selectors must be a non-empty array");
}
const keys = selectors.map((entry) => {
if (typeof entry === "function") return { select: entry, direction: "asc" };
if (Array.isArray(entry) && typeof entry[0] === "function") {
const direction = entry[1] ?? "asc";
if (direction !== "asc" && direction !== "desc") {
throw new RangeError("direction must be asc or desc");
}
return { select: entry[0], direction };
}
throw new TypeError("selector must be a function or [function, direction]");
});
return items
.map((value, index) => ({ value, index }))
.sort((left, right) => {
for (const { select, direction } of keys) {
const result = compareValues(select(left.value), select(right.value));
if (result !== 0) return direction === "desc" ? -result : result;
}
return left.index - right.index;
})
.map((entry) => entry.value);
}Report a problem with this exampleOpens a pre-filled issue on GitHub.