Skip to content

@absoluteui/helpers/compare

Comparers, natural ordering, equality, range checks, coalescing, and pattern matching.

import { ... } from '@absoluteui/helpers/compare';

Comparers and predicates for sorting, range checks, fallbacks, and small decision tables. compareValues orders numbers, strings, bigints, and dates consistently and puts null/undefined last in either direction, so grids sort predictably with incomplete data. For sorting arrays in one call, sortBy in helpers/array builds on the same rules.

How do I sort a grid by several columns with empty values last?

Section titled “How do I sort a grid by several columns with empty values last?”

Build a comparer with by for each column and chain them with composeComparers (or thenBy). Pass an Intl.Collator for language-correct string order.

import { by, composeComparers } from '@absoluteui/helpers/compare';
const rows = [
{ name: 'Ölund', dept: 'IT', hired: new Date('2021-03-01') },
{ name: 'Adams', dept: null, hired: new Date('2019-07-15') },
{ name: 'Zhou', dept: 'IT', hired: new Date('2023-01-09') },
{ name: 'Berg', dept: 'HR', hired: new Date('2020-11-30') },
];
const collator = new Intl.Collator('sv');
rows.sort(composeComparers(
by((r) => r.dept, { collator }),
by((r) => r.hired, { direction: 'desc' }),
));
rows.map((r) => r.name); // => ['Berg', 'Zhou', 'Ölund', 'Adams']

How do I sort document names the way people expect?

Section titled “How do I sort document names the way people expect?”

naturalCompare compares digit runs as numbers, so “Payslip 2” comes before “Payslip 10”.

import { naturalCompare } from '@absoluteui/helpers/compare';
const files = ['Payslip 10.pdf', 'Payslip 2.pdf', 'payslip 1.pdf'];
[...files].sort((a, b) => naturalCompare(a, b, { sensitivity: 'base' })); // => ['payslip 1.pdf', 'Payslip 2.pdf', 'Payslip 10.pdf']
[...files].sort(); // => ['Payslip 10.pdf', 'Payslip 2.pdf', 'payslip 1.pdf']

How do I check that a value falls inside a period or band?

Section titled “How do I check that a value falls inside a period or band?”

between works for numbers, strings, and dates, with inclusive or half-open bounds; missing values are never inside.

import { between } from '@absoluteui/helpers/compare';
const payPeriod = [new Date('2026-10-01T00:00:00Z'), new Date('2026-11-01T00:00:00Z')] as const;
const inPeriod = (d: Date | null) => between(d, payPeriod[0], payPeriod[1], { inclusive: 'left' });
inPeriod(new Date('2026-10-31T23:59:00Z')); // => true
inPeriod(new Date('2026-11-01T00:00:00Z')); // => false
inPeriod(null); // => false
between(48_000, 45_000, 60_000); // => true

How do I pick a display value or map a status without long if chains?

Section titled “How do I pick a display value or map a status without long if chains?”

coalesceBlank returns the first value that is not null, undefined, or blank text; match evaluates cases in order and stops at the first that fits.

import { coalesceBlank, match } from '@absoluteui/helpers/compare';
const employee = { preferredName: ' ', firstName: 'Asha', login: 'arao' };
coalesceBlank(employee.preferredName, employee.firstName, employee.login); // => 'Asha'
const balanceLabel = (days: number): string =>
match<number, string>(days)
.with((d) => d < 0, () => 'Overdrawn')
.with(0, () => 'None left')
.with((d) => d <= 3, (d) => `Low (${d})`)
.otherwise((d) => `${d} days`);
[-1, 0, 2, 14].map(balanceLabel); // => ['Overdrawn', 'None left', 'Low (2)', '14 days']

allPass, anyPass, and complement build one predicate from several, for example the filters a user switched on.

import { allPass, complement } from '@absoluteui/helpers/compare';
interface Emp { name: string; dept: string; active: boolean }
const staff: Emp[] = [
{ name: 'Asha', dept: 'HR', active: true },
{ name: 'Ben', dept: 'IT', active: true },
{ name: 'Chen', dept: 'HR', active: false },
];
const isHr = (e: Emp) => e.dept === 'HR';
const isInactive = (e: Emp) => !e.active;
staff.filter(allPass([isHr, complement(isInactive)])).map((e) => e.name); // => ['Asha']

allPass

Predicate that passes when all predicates pass (short-circuit; empty -> true).

function allPass<T>(predicates: readonly ((value: T) => boolean)[]): (value: T) => boolean;

Example:

allPass([(n: number) => n > 0, (n) => n < 10])(5) // => true

anyPass

Predicate that passes when any predicate passes (empty -> false).

function anyPass<T>(predicates: readonly ((value: T) => boolean)[]): (value: T) => boolean;

Example:

anyPass([(n: number) => n < 0, (n) => n > 10])(11) // => true

between

True when value lies between min and max (numbers, bigints, strings, dates). Null-like values are never between.

function between<T>(value: T, min: T, max: T, options?: BetweenOptions): boolean;

Example:

between(5, 1, 5) // => true

Example:

between(5, 1, 5, { inclusive: 'left' }) // => false

by

Builds a comparer from a key selector. Nulls stay where nulls says in either direction.

function by<T>(selector: (item: T) => unknown, options?: ByOptions): Comparer<T>;

Example:

const people = [{ name: 'Ada', age: 36 }, { name: 'Ben', age: 41 }, { name: 'Cy', age: 29 }];
people.sort(by((p) => p.age, { direction: 'desc' })).map((p) => p.name); // => ['Ben', 'Ada', 'Cy']

coalesce

First value that is not null/undefined.

function coalesce<T>(...values: readonly T[]): NonNullable<T> | undefined;

Example:

coalesce(undefined, null, 0, 1) // => 0

coalesceBlank

First value that is not nullish and not a blank (whitespace-only) string.

function coalesceBlank<T>(...values: readonly T[]): NonNullable<T> | undefined;

Example:

coalesceBlank(' ', null, 'x') // => 'x'

compareValues

Compares numbers, bigints, strings, dates and booleans ascending; mixed types order boolean < number/bigint < string < date < other. Null-like values (null, undefined, NaN, invalid dates) go last by default.

function compareValues(a: unknown, b: unknown, options?: CompareOptions): number;

Example:

[3, null, 1].sort(compareValues) // => [1, 3, null]

complement

Negates a predicate.

function complement<A extends unknown[]>(predicate: (...args: A) => boolean): (...args: A) => boolean;

Example:

[1, 2].filter(complement((n: number) => n % 2 === 0)) // => [1]

composeComparers

Combines comparers: the first non-zero result wins.

function composeComparers<T>(...comparers: readonly Comparer<T>[]): Comparer<T>;

Example:

const rows = [{ dept: 'IT', salary: 50 }, { dept: 'HR', salary: 40 }, { dept: 'IT', salary: 70 }];
rows.sort(composeComparers(by((r) => r.dept), by((r) => r.salary, { direction: 'desc' }))).map((r) => r.salary); // => [40, 70, 50]

cond

Returns a function that runs the handler of the first passing predicate, else fallback (or undefined).

function cond<T, R>(pairs: readonly (readonly [(value: T) => boolean, (value: T) => R])[], fallback?: (value: T) => R): (value: T) => R | undefined;

Example:

const grade = cond([[(n: number) => n >= 90, () => 'A'], [(n) => n >= 80, () => 'B']], () => 'C');

equals

Equality: SameValueZero by default, structural with { deep: true } (see deepEqual).

function equals(a: unknown, b: unknown, options?: {
readonly deep?: boolean;
}): boolean;

Example:

equals({ a: 1 }, { a: 1 }, { deep: true }) // => true

ifElse

Returns a function choosing between two branches.

function ifElse<T, A, B>(predicate: (value: T) => boolean, onTrue: (value: T) => A, onFalse: (value: T) => B): (value: T) => A | B;

Example:

ifElse((n: number) => n % 2 === 0, () => 'even', () => 'odd')(3) // => 'odd'

inRange

Half-open numeric range test start &lt;= value &lt; end.

function inRange(value: number, start: number, end: number): boolean;

Example:

inRange(3, 0, 3) // => false

isAscending

True when the array is in ascending order.

function isAscending<T>(items: readonly T[], options?: OrderCheckOptions<T>): boolean;

Example:

isAscending([1, 2, 2, 3]) // => true

isDescending

True when the array is in descending order.

function isDescending<T>(items: readonly T[], options?: OrderCheckOptions<T>): boolean;

Example:

isDescending([3, 2, 1], { strict: true }) // => true

isEqualBy

Equality on a selected key (SameValueZero, or deep with { deep: true }).

function isEqualBy<T>(selector: (value: T) => unknown, options?: {
readonly deep?: boolean;
}): (a: T, b: T) => boolean;

Example:

isEqualBy((u: { id: number }) => u.id)({ id: 1 }, { id: 1 }) // => true

match

Minimal pattern matching; the first matching case wins and later cases are not evaluated.

function match<T, R = unknown>(value: T): Matcher<T, R>;

Example:

const label = (status: number): string =>
match<number, string>(status).with(200, () => 'ok').with((s) => s >= 500, () => 'server').otherwise(() => 'other');
label(200); // => 'ok'
label(503); // => 'server'
label(404); // => 'other'

maxOf

Largest item by comparer (first wins on ties), or undefined when empty.

function maxOf<T>(items: Iterable<T>, comparer?: Comparer<T>): T | undefined;

Example:

maxOf(['b', 'a'], naturalCompare) // => 'b'

minOf

Smallest item by comparer (first wins on ties), or undefined when empty.

function minOf<T>(items: Iterable<T>, comparer?: Comparer<T>): T | undefined;

Example:

minOf([3, 1, 2]) // => 1

naturalCompare

Numeric-aware, locale-aware string comparison (Intl.Collator with numeric: true, cached).

function naturalCompare(a: string, b: string, options?: NaturalCompareOptions): number;

Example:

['file10', 'file2'].sort(naturalCompare) // => ['file2', 'file10']

nonePass

Predicate that passes when no predicate passes.

function nonePass<T>(predicates: readonly ((value: T) => boolean)[]): (value: T) => boolean;

Example:

nonePass([(s: string) => s === ''])('x') // => true

onePass

Predicate that passes when exactly one predicate passes (n-ary xor on predicates).

function onePass<T>(predicates: readonly ((value: T) => boolean)[]): (value: T) => boolean;

Example:

onePass([(n: number) => n > 0, (n) => n > 5])(3) // => true

reverseComparer

Reverses a comparer.

function reverseComparer<T>(comparer: Comparer<T>): Comparer<T>;

Example:

[1, 3, 2].sort(reverseComparer(compareValues)) // => [3, 2, 1]

thenBy

Adds a key-selector tie-breaker to a comparer (use composeComparers to chain comparers).

function thenBy<T>(primary: Comparer<T>, selector: (item: T) => unknown, options?: ByOptions): Comparer<T>;

Example:

const rows = [{ first: 'Ben', last: 'Kay' }, { first: 'Ada', last: 'Lee' }, { first: 'Al', last: 'Kay' }];
rows.sort(thenBy(by((r) => r.last), (r) => r.first)).map((r) => r.first); // => ['Al', 'Ben', 'Ada']

unless

Returns a function that applies fn unless predicate holds.

function unless<T, R>(predicate: (value: T) => boolean, fn: (value: T) => R): (value: T) => T | R;

Example:

unless((s: string) => s.endsWith('/'), (s) => `${s}/`)('a') // => 'a/'

when

Returns a function that applies fn when predicate holds, else returns the input.

function when<T, R>(predicate: (value: T) => boolean, fn: (value: T) => R): (value: T) => T | R;

Example:

[1, -2].map(when((n: number) => n < 0, (n) => -n)) // => [1, 2]

BetweenOptions

interface BetweenOptions extends CompareOptions {
readonly inclusive?: 'both' | 'none' | 'left' | 'right' | boolean;
}

ByOptions

interface ByOptions extends CompareOptions {
readonly direction?: 'asc' | 'desc';
}

CompareOptions

interface CompareOptions {
readonly nulls?: NullsOrder;
readonly collator?: Intl.Collator;
}

Matcher

Fluent matcher returned by match.

interface Matcher<T, R> {
with<U extends T>(predicate: (value: T) => value is U, handler: (value: U) => R): Matcher<T, R>;
with(test: ((value: T) => boolean) | T, handler: (value: T) => R): Matcher<T, R>;
otherwise(handler: (value: T) => R): R;
}

NaturalCompareOptions

interface NaturalCompareOptions {
readonly locale?: string | readonly string[];
readonly sensitivity?: Intl.CollatorOptions['sensitivity'];
}

OrderCheckOptions

interface OrderCheckOptions<T> {
readonly comparer?: Comparer<T>;
readonly strict?: boolean;
}

Comparer

Standard comparator: negative, zero or positive.

type Comparer<T> = (a: T, b: T) => number;

NullsOrder

Where null/undefined (and invalid numbers/dates) sort.

type NullsOrder = 'first' | 'last';