ts-order
Version:
Type-safe Order utility for delarative, composable, and immutable multi-key ordering logic
240 lines (239 loc) • 5.57 kB
JavaScript
//#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 };