UNPKG

ts-order

Version:

Type-safe Order utility for delarative, composable, and immutable multi-key ordering logic

240 lines (239 loc) 5.57 kB
//#region src/comparator/index.ts /** * Convenience alias for `-1`, indicating that `a` should come before `b` inside a comparator function. */ const A_BEFORE_B = -1; /** * Convenience alias for `1`, indicating that `a` should come after `b` inside a comparator function. */ const A_AFTER_B = 1; /** * Convenience alias for `0` indicating that `a` is equal to `b` inside a comparator function. */ const EQUAL = 0; /** * Inverts the result of a comparator so that higher values come first. * * @example * ```ts * ['c', 'a', 'b'].sort(reverse(string)); // ['c', 'b', 'a'] * ``` */ function reverse(compareFn) { return (a, b) => compareFn(b, a); } /** * Wraps a comparator so `null` and `undefined` sort before defined values. * * @example * ```ts * [null, 'b', 'a'].sort(nullsFirst(string)); // [null, 'a', 'b'] * ``` */ function nullsFirst(compareFn) { return (a, b) => { if (a == null && b == null) return 0; if (a == null) return -1; if (b == null) return 1; return compareFn(a, b); }; } /** * Wraps a comparator so `null` and `undefined` sort after defined values. * * @example * ```ts * ['b', null, 'a'].sort(nullsLast(string)); // ['a', 'b', null] * ``` */ function nullsLast(compareFn) { return (a, b) => { if (a == null && b == null) return 0; if (a == null) return 1; if (b == null) return -1; return compareFn(a, b); }; } /** * Wraps a comparator so `NaN` numbers sort before other values. * * @example * ```ts * [NaN, 2, 1].sort(nansFirst(number)); // [NaN, 1, 2] * ``` */ function nansFirst(compareFn) { return (a, b) => { const isANaN = typeof a === "number" && Number.isNaN(a); const isBNaN = typeof b === "number" && Number.isNaN(b); if (isANaN && isBNaN) return 0; if (isANaN) return -1; if (isBNaN) return 1; return compareFn(a, b); }; } /** * Wraps a comparator so `NaN` numbers sort after other values. * * @example * ```ts * [2, NaN, 1].sort(nansLast(number)); // [1, 2, NaN] * ``` */ function nansLast(compareFn) { return (a, b) => { const isANaN = typeof a === "number" && Number.isNaN(a); const isBNaN = typeof b === "number" && Number.isNaN(b); if (isANaN && isBNaN) return 0; if (isANaN) return 1; if (isBNaN) return -1; return compareFn(a, b); }; } /** * Basic three-way comparator using JavaScript relational operators (i.e. `a < b`, `a > b`). * * @example * ```ts * ['b', 'a', 'c'].sort(compare); // ['a', 'b', 'c'] * ``` */ function compare(a, b) { if (a < b) return -1; if (a > b) return 1; return 0; } /** * Locale-aware string comparator using `Intl.Collator`. * * @example * ```ts * ['ä', 'a', 'z'].sort(localeString); // ['a', 'ä', 'z'] in de-DE locale * ``` */ function localeString(a, b) { return a.localeCompare(b); } /** * Numeric comparator that places `NaN` before finite numbers. * * @example * ```ts * [3, NaN, 1].sort(number); // [NaN, 1, 3] * ``` */ function number(a, b) { const isANaN = Number.isNaN(a); const isBNaN = Number.isNaN(b); if (isANaN && isBNaN) return 0; if (isANaN) return -1; if (isBNaN) return 1; return a - b; } /** * Boolean comparator treating `false` as 0 and `true` as 1. * * @example * ```ts * [true, false].sort(boolean); // [false, true] * ``` */ function boolean(a, b) { return Number(a) - Number(b); } /** * Date comparator that falls back to placing invalid dates first. * * @example * ```ts * [new Date('2020'), new Date('2010')].sort(date); // [2010, 2020] * ``` */ function date(a, b) { const aTime = a.getTime(); const bTime = b.getTime(); const isANaN = Number.isNaN(aTime); const isBNaN = Number.isNaN(bTime); if (isANaN && isBNaN) return 0; if (isANaN) return -1; if (isBNaN) return 1; return aTime - bTime; } /** * Builds a comparator by projecting values through `key` before comparing. * * @example * ```ts * // sorts users by age ascending * users.sort(by(user => user.age)); * ``` */ function by(key, options) { const compareFn = options?.compare ?? compare; const direction = options?.direction ?? "asc"; const predicate = options?.predicate; const compareFnWithDirection = direction === "asc" ? compareFn : reverse(compareFn); if (!predicate) return (a, b) => compareFnWithDirection(key(a), key(b)); return (a, b) => { if (!predicate(a) || !predicate(b)) return 0; return compareFnWithDirection(key(a), key(b)); }; } /** * Chains comparators, returning the first non-zero comparison result. * * @example * ```ts * // Sort users by last name, then first name * users.sort( * order( * by((u) => u.last), * by((u) => u.first), * ), * ); * ``` */ function order(...comparators) { return (a, b) => { for (let i = 0; i < comparators.length; i++) { const r = comparators[i](a, b); if (r !== 0) return r; } return 0; }; } /** * Adapts a comparator to work on mapped values. * * @example * ```ts * // Sort strings by their length * ['aa', 'b'].sort(map((value: string) => value.length)); // ['b', 'aa'] * ``` */ function map(map$1, compareFn = compare) { return (a, b) => compareFn(map$1(a), map$1(b)); } /** * Runs a comparator only when both values satisfy a predicate. * * @example * ```ts * people.sort( * when( * (person) => person.age >= 18, * by((person) => person.age), * ), * ); * ``` */ function when(predicate, compareFn) { return (a, b) => { const aMatch = predicate(a); const bMatch = predicate(b); if (aMatch && bMatch) return compareFn(a, b); return 0; }; } //#endregion export { A_AFTER_B, A_BEFORE_B, EQUAL, boolean, by, compare, compare as string, date, localeString, map, nansFirst, nansLast, nullsFirst, nullsLast, number, order, reverse, when };