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.

JavascriptData Integrity

Code

snippet
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);
}