orderBy
EditStably sorts a copied array from independently directed selector values.
export type OrderByValue = string | number | bigint | boolean | Date | null | undefined;export type OrderByDirection = "asc" | "desc";
export type OrderByRule<T> = { select: (this: void, value: T) => OrderByValue; order?: OrderByDirection;};
const rank = (value: OrderByValue): number => { if (value == null) return 6; if (value instanceof Date) return Number.isNaN(value.getTime()) ? 5 : 4; if (typeof value === "number") return Number.isNaN(value) ? 5 : 0; if (typeof value === "bigint") return 1; if (typeof value === "string") return 2; return 3;};
const compare = (left: OrderByValue, right: OrderByValue): number => { const leftRank = rank(left); const rightRank = rank(right); if (leftRank !== rightRank) return leftRank - rightRank; if (leftRank >= 5 || Object.is(left, right)) return 0;
if (left instanceof Date && right instanceof Date) { const a = left.getTime(); const b = right.getTime(); return a < b ? -1 : a > b ? 1 : 0; } if (typeof left === "number" && typeof right === "number") { return left < right ? -1 : left > right ? 1 : 0; } if (typeof left === "bigint" && typeof right === "bigint") { return left < right ? -1 : left > right ? 1 : 0; } if (typeof left === "string" && typeof right === "string") { return left < right ? -1 : left > right ? 1 : 0; } if (typeof left === "boolean" && typeof right === "boolean") return left ? 1 : -1; return 0;};
/** Stably sorts a copy using independently directed values computed once per item. */export const orderBy = <T>( array: readonly T[], rules: readonly OrderByRule<T>[]): T[] => { const normalizedRules: Array<Required<OrderByRule<T>>> = []; for (let index = 0; index < rules.length; index += 1) { if (!Object.hasOwn(rules, index)) { throw new TypeError("rules must not contain empty slots"); } const rule = rules[index]; if (!rule || typeof rule.select !== "function") { throw new TypeError("every rule must contain a select function"); } const { select, order = "asc" } = rule; if (order !== "asc" && order !== "desc") { throw new RangeError('order must be "asc" or "desc"'); } normalizedRules.push({ select, order }); }
const entries: Array<{ value: T; index: number }> = []; for (let index = 0; index < array.length; index += 1) { if (Object.hasOwn(array, index)) { entries.push({ value: array[index] as T, index }); } }
return entries .map(({ value, index }) => ({ value, index, selected: normalizedRules.map(({ select }) => select(value)) })) .sort((left, right) => { for (const [index, { order }] of normalizedRules.entries()) { const result = compare(left.selected[index], right.selected[index]); if (result !== 0) return order === "desc" ? -result : result; } return left.index - right.index; }) .map(({ value }) => value);};Download
Section titled “Download”wget -O src/lib/orderBy.ts https://raw.githubusercontent.com/jrTilak/lazykit/HEAD/registry/functions/orderBy.tstemp_dir="$(mktemp -d)" && bunx degit jrTilak/lazykit/registry/functions "$temp_dir" && mv "$temp_dir/orderBy.ts" src/lib/orderBy.ts && rm -r "$temp_dir"Examples
Section titled “Examples”import { orderBy, type OrderByRule } from "./orderBy";
type User = { name: string; score: number };
const rules = [ { select: (user: User) => user.score, order: "desc" }] satisfies ReadonlyArray<OrderByRule<User>>;
const users: User[] = orderBy( [{ name: "Ada", score: 8 }, { name: "Lin", score: 10 }], rules);// [{ name: "Lin", score: 10 }, { name: "Ada", score: 8 }]array(readonly T[]) — Values to sort. Empty slots are skipped.rules— Dense selector and optional"asc"or"desc"direction pairs. Sparse rule lists are rejected.- Every selector is evaluated exactly once per input item.
- Selectors are invoked without a receiver.
- Ascending mixed values use the fixed order number, bigint, string, boolean, valid
date,
NaNor invalid date, then nullish. Descending reverses those groups. - Equal values and values within the same special group retain source order.
- Returns a new
T[]; an empty rule list returns a shallow copy.