Skip to content

orderBy

Edit

Stably sorts a copied array from independently directed selector values.

orderBy.ts
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);
};
Terminal
wget -O src/lib/orderBy.ts https://raw.githubusercontent.com/jrTilak/lazykit/HEAD/registry/functions/orderBy.ts
orderBy.example.ts
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, NaN or 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.