Skip to content

@absoluteui/helpers/array

Grouping, chunking, sorting, searching, pagination, immutable edits, and tree building for arrays.

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

Immutable helpers for the lists an application shows: group, sort, page, summarise, and edit arrays without mutating them, and turn flat parent/child rows into trees. Every function returns a new array. For query-style chains over large or lazy sequences, see helpers/linq; the Helpers and services guide covers the shared conventions.

How do I group employees by department and show one page of the result?

Section titled “How do I group employees by department and show one page of the result?”

Sort first (stable, multi-key, locale-aware), then page with paginate; groupBy and sumBy give the per-department figures for a summary row.

import { groupBy, paginate, sortBy, sumBy } from '@absoluteui/helpers/array';
const staff = [
{ name: 'Asha', dept: 'Finance', salary: 5200 },
{ name: 'Ben', dept: 'HR', salary: 4100 },
{ name: 'Chen', dept: 'Finance', salary: 6100 },
{ name: 'Dana', dept: 'IT', salary: 7000 },
{ name: 'Eli', dept: 'HR', salary: 3900 },
];
const sorted = sortBy(staff, [{ by: (e) => e.dept }, { by: (e) => e.salary, order: 'desc' }], { locale: 'en' });
const page = paginate(sorted, 1, 2);
page.items.map((e) => e.name); // => ['Chen', 'Asha']
[page.pageCount, page.hasNext]; // => [3, true]
const byDept = groupBy(staff, (e) => e.dept);
sumBy(byDept.HR ?? [], (e) => e.salary); // => 8000

How do I update a list held in state without mutating it?

Section titled “How do I update a list held in state without mutating it?”

upsertBy replaces the row with the same key or appends it; removeAt, move, and toggleItem cover deletes, drag-reorder, and multi-select.

import { move, toggleItem, upsertBy } from '@absoluteui/helpers/array';
const rows = [{ id: 1, status: 'draft' }, { id: 2, status: 'draft' }];
const saved = upsertBy(rows, { id: 2, status: 'submitted' }, (r) => r.id);
saved.map((r) => r.status); // => ['draft', 'submitted']
rows[1]?.status; // => 'draft'
move(['Mon', 'Tue', 'Wed'], 2, 0); // => ['Wed', 'Mon', 'Tue']
toggleItem([3, 7], 7); // => [3]

How do I build an org tree from flat rows?

Section titled “How do I build an org tree from flat rows?”

buildTree links rows by id and parentId (field names or selectors) and reports orphans and cycles instead of looping; flattenTree turns it back into indented rows.

import { buildTree, flattenTree } from '@absoluteui/helpers/array';
const people = [
{ id: 'ceo', parentId: null, name: 'Maya' },
{ id: 'cfo', parentId: 'ceo', name: 'Omar' },
{ id: 'acct', parentId: 'cfo', name: 'Lin' },
{ id: 'temp', parentId: 'gone', name: 'Sam' },
];
const tree = buildTree(people, { orphans: 'omit' });
tree.orphans.map((p) => p.name); // => ['Sam']
flattenTree(tree.roots).map((e) => e.depth); // => [0, 1, 2]

chunk splits work into fixed-size batches, for example to import rows 500 at a time; partition separates valid from invalid rows in one pass.

import { chunk, partition } from '@absoluteui/helpers/array';
const ids = Array.from({ length: 1200 }, (_, i) => i + 1);
chunk(ids, 500).map((batch) => batch.length); // => [500, 500, 200]
const [valid, invalid] = partition(['E-1', '', 'E-3'], (id) => id !== '');
[valid.length, invalid.length]; // => [2, 1]

Sizes and indexes are validated, and generated arrays are capped at MAX_GENERATED_LENGTH:

import { chunk, range } from '@absoluteui/helpers/array';
chunk([1, 2, 3], 0); // => throws HLP_ARRAY_INVALID_SIZE
range(0, 1e9); // => throws HLP_ARRAY_TOO_LARGE

arraysEqual

Shallow element-wise equality (default Object.is).

function arraysEqual<T>(a: readonly T[], b: readonly T[], equals?: (x: T, y: T) => boolean): boolean;

Example:

arraysEqual([1, 2], [1, 2]); // => true

averageBy

Mean of fn over items, or undefined for an empty array.

function averageBy<T>(items: readonly T[], fn: (item: T, index: number) => number): number | undefined;

Example:

averageBy([{ n: 2 }, { n: 4 }], (x) => x.n); // => 3

binarySearch

Index of target in a sorted array, or ~insertionPoint (negative) when absent.

function binarySearch<T>(sorted: readonly T[], target: T, compare?: Comparer<T>): number;

Example:

binarySearch([1, 3, 5], 3); // => 1
~binarySearch([1, 3, 5], 4); // => 2

bottomN

The n smallest items, smallest first (stable for ties). O(len log n).

function bottomN<T>(items: readonly T[], n: number, compare?: Comparer<T>): T[];

Example:

bottomN([5, 1, 9, 3], 2); // => [1, 3]

chunk

Splits into chunks of size; the last chunk may be shorter.

function chunk<T>(items: readonly T[], size: number): T[][];

Example:

chunk([1, 2, 3, 4, 5], 2); // => [[1, 2], [3, 4], [5]]

compact

Removes null, undefined, and false (keeps 0, '', and NaN).

function compact<T>(items: readonly T[]): Exclude<T, null | undefined | false>[];

Example:

compact([0, null, 'a', false, undefined]); // => [0, 'a']

countBy

Counts items per key into a null-prototype record.

function countBy<T, K extends PropertyKey>(items: Iterable<T>, keyFn: (item: T, index: number) => K): Partial<Record<K, number>>;

Example:

countBy(['a', 'bb', 'cc'], (s) => s.length); // => { 1: 1, 2: 2 }

dropWhile

Items after the leading run where predicate holds.

function dropWhile<T>(items: readonly T[], predicate: (item: T, index: number) => boolean): T[];

Example:

dropWhile([1, 2, 5, 1], (n) => n < 3); // => [5, 1]

first

First item, or undefined.

function first<T>(items: readonly T[]): T | undefined;

Example:

first([1, 2]); // => 1

flatten

Flattens nested arrays up to depth (default 1).

function flatten<A extends readonly unknown[], D extends number = 1>(items: A, depth?: D): FlatArray<A, D>[];

Example:

flatten([1, [2, [3]]]); // => [1, 2, [3]]

flattenDeep

Flattens all nesting levels.

function flattenDeep<A extends readonly unknown[]>(items: A): FlatArray<A, 20>[];

Example:

flattenDeep([1, [2, [3, [4]]]]); // => [1, 2, 3, 4]

groupBy

Groups into a null-prototype record keyed by keyFn (native Object.groupBy when present).

function groupBy<T, K extends PropertyKey>(items: Iterable<T>, keyFn: (item: T, index: number) => K): Partial<Record<K, T[]>>;

Example:

groupBy(['one', 'two', 'three'], (s) => s.length); // => { 3: ['one', 'two'], 5: ['three'] }

groupByToMap

Groups into a Map (native Map.groupBy when present); keys use SameValueZero.

function groupByToMap<T, K>(items: Iterable<T>, keyFn: (item: T, index: number) => K): Map<K, T[]>;

Example:

groupByToMap([{ d: 1 }, { d: 2 }, { d: 1 }], (x) => x.d).get(1)?.length; // => 2

insertAt

Inserts items before index (index === length appends; negative counts from the end).

function insertAt<T>(items: readonly T[], index: number, ...values: T[]): T[];

Example:

insertAt([1, 4], 1, 2, 3); // => [1, 2, 3, 4]

intersperse

Inserts separator between items.

function intersperse<T, S>(items: readonly T[], separator: S): (T | S)[];

Example:

intersperse(['a', 'b', 'c'], ','); // => ['a', ',', 'b', ',', 'c']

keyBy

Indexes items by key into a null-prototype record; later items win.

function keyBy<T, K extends PropertyKey>(items: Iterable<T>, keyFn: (item: T, index: number) => K): Partial<Record<K, T>>;

Example:

keyBy([{ id: 'a' }, { id: 'b' }], (x) => x.id); // => { a: { id: 'a' }, b: { id: 'b' } }

last

Last item, or undefined.

function last<T>(items: readonly T[]): T | undefined;

Example:

last([1, 2]); // => 2

lowerBound

First index whose element is not less than target.

function lowerBound<T>(sorted: readonly T[], target: T, compare?: Comparer<T>): number;

Example:

lowerBound([1, 2, 2, 3], 2); // => 1

maxBy

Item with the largest key (first on ties; empty keys ignored), or undefined.

function maxBy<T>(items: readonly T[], fn: (item: T) => unknown): T | undefined;

Example:

maxBy([{ n: 2 }, { n: 1 }], (x) => x.n); // => { n: 2 }

minBy

Item with the smallest key (first on ties; empty keys ignored), or undefined.

function minBy<T>(items: readonly T[], fn: (item: T) => unknown): T | undefined;

Example:

minBy([{ n: 2 }, { n: 1 }], (x) => x.n); // => { n: 1 }

move

Moves the item at from to to (negative indexes count from the end).

function move<T>(items: readonly T[], from: number, to: number): T[];

Example:

move(['a', 'b', 'c'], 0, 2); // => ['b', 'c', 'a']

nth

Item at index (negative counts from the end), or undefined.

function nth<T>(items: readonly T[], index: number): T | undefined;

Example:

nth([1, 2, 3], -2); // => 2

paginate

Slices one 1-based page. Pages past the end have no items.

function paginate<T>(items: readonly T[], page: number, pageSize: number): Page<T>;

Example:

paginate(range(25), 3, 10); // => { items: [20, 21, 22, 23, 24], total: 25, page: 3, pageSize: 10, pageCount: 3, hasNext: false, hasPrev: true }

pairwise

Adjacent pairs.

function pairwise<T>(items: readonly T[]): [T, T][];

Example:

pairwise([1, 2, 3]); // => [[1, 2], [2, 3]]

partition

Splits into [matching, rest], preserving order.

function partition<T, S extends T>(items: readonly T[], predicate: (item: T, index: number) => item is S): [S[], Exclude<T, S>[]];
function partition<T>(items: readonly T[], predicate: (item: T, index: number) => boolean): [T[], T[]];

Example:

partition([1, 2, 3, 4], (n) => n % 2 === 0); // => [[2, 4], [1, 3]]

range

Numbers from start (inclusive) to end (exclusive) by step. range(n) is 0..n-1. Throws HLP_ARRAY_INVALID_RANGE for non-finite input or a zero step, and HLP_ARRAY_TOO_LARGE above MAX_GENERATED_LENGTH.

function range(start: number, end?: number, step?: number): number[];

Example:

range(3); // => [0, 1, 2]
range(10, 0, -5); // => [10, 5]
range(0, 1, 0.25); // => [0, 0.25, 0.5, 0.75]

removeAt

Removes count items starting at index (negative counts from the end).

function removeAt<T>(items: readonly T[], index: number, count?: number): T[];

Example:

removeAt([1, 2, 3, 4], 1, 2); // => [1, 4]

repeat

An array of count copies of value.

function repeat<T>(value: T, count: number): T[];

Example:

repeat('x', 3); // => ['x', 'x', 'x']

replaceAt

Replaces the item at index (negative counts from the end).

function replaceAt<T>(items: readonly T[], index: number, value: T): T[];

Example:

replaceAt([1, 2, 3], -1, 9); // => [1, 2, 9]

rotate

Rotates by n positions: positive moves items toward the end, negative toward the start.

function rotate<T>(items: readonly T[], n: number): T[];

Example:

rotate([1, 2, 3, 4], 1); // => [4, 1, 2, 3]
rotate([1, 2, 3, 4], -1); // => [2, 3, 4, 1]

sortBy

Stable multi-key sort. Keys are evaluated once per item.

function sortBy<T>(items: readonly T[], keys: ((item: T) => unknown) | SortKey<T> | readonly (((item: T) => unknown) | SortKey<T>)[], options?: SortByOptions): T[];

Example:

const people = [{ name: 'Ada', dept: 'Vertrieb', salary: 50 }, { name: 'Ben', dept: 'Ärzte', salary: 40 }, { name: 'Cy', dept: 'Vertrieb', salary: 70 }];
sortBy(people, [{ by: (p) => p.dept }, { by: (p) => p.salary, order: 'desc' }], { locale: 'de' }).map((p) => p.name); // => ['Ben', 'Cy', 'Ada']

splitAt

Splits into [before, fromIndex] (negative counts from the end; out of range clamps).

function splitAt<T>(items: readonly T[], index: number): [T[], T[]];

Example:

splitAt([1, 2, 3], 1); // => [[1], [2, 3]]

sumBy

Sum of fn over items with compensated (Neumaier) summation.

function sumBy<T>(items: readonly T[], fn: (item: T, index: number) => number): number;

Example:

sumBy([{ n: 1e100 }, { n: 1 }, { n: -1e100 }], (x) => x.n); // => 1 // plain addition gives 0

swap

Swaps two items.

function swap<T>(items: readonly T[], i: number, j: number): T[];

Example:

swap([1, 2, 3], 0, 2); // => [3, 2, 1]

takeWhile

Leading items while predicate holds.

function takeWhile<T>(items: readonly T[], predicate: (item: T, index: number) => boolean): T[];

Example:

takeWhile([1, 2, 5, 1], (n) => n < 3); // => [1, 2]

times

Calls fn count times and collects the results.

function times<T>(count: number, fn: (index: number) => T): T[];

Example:

times(3, (i) => i * 2); // => [0, 2, 4]

toggleItem

Removes every item equal to item, or appends it when absent.

function toggleItem<T>(items: readonly T[], item: T, equals?: (a: T, b: T) => boolean): T[];

Example:

toggleItem([1, 2], 2); // => [1]
toggleItem([1, 2], 3); // => [1, 2, 3]

topN

The n largest items, largest first (stable for ties). O(len log n).

function topN<T>(items: readonly T[], n: number, compare?: Comparer<T>): T[];

Example:

topN([5, 1, 9, 3], 2); // => [9, 5]
const users = [{ name: 'Ada', score: 7 }, { name: 'Ben', score: 9 }, { name: 'Cy', score: 4 }];
topN(users, 2, (a, b) => a.score - b.score).map((u) => u.name); // => ['Ben', 'Ada']

uniq

Removes duplicates (SameValueZero), keeping first occurrences.

function uniq<T>(items: readonly T[]): T[];

Example:

uniq([1, 2, 1, NaN, NaN]); // => [1, 2, NaN]

uniqBy

Removes items with a duplicate key, keeping first occurrences.

function uniqBy<T>(items: readonly T[], keyFn: (item: T) => unknown): T[];

Example:

uniqBy([{ id: 1 }, { id: 1 }, { id: 2 }], (x) => x.id); // => [{ id: 1 }, { id: 2 }]

unzip

Inverse of zip: splits tuples into one array per position (width of the longest row).

function unzip<const T extends readonly unknown[]>(rows: readonly T[]): {
-readonly [K in keyof T]: T[K][];
};

Example:

unzip([[1, 'a'], [2, 'b']] as [number, string][]); // => [[1, 2], ['a', 'b']]

upperBound

First index whose element is greater than target.

function upperBound<T>(sorted: readonly T[], target: T, compare?: Comparer<T>): number;

Example:

upperBound([1, 2, 2, 3], 2); // => 3

upsertBy

Replaces the first item with the same key, or appends.

function upsertBy<T>(items: readonly T[], item: T, keyFn: (item: T) => unknown): T[];

Example:

upsertBy([{ id: 1, v: 'a' }], { id: 1, v: 'b' }, (x) => x.id); // => [{ id: 1, v: 'b' }]

windowed

Sliding windows of size, advancing by step; only full windows are returned.

function windowed<T>(items: readonly T[], size: number, step?: number): T[][];

Example:

windowed([1, 2, 3, 4], 2); // => [[1, 2], [2, 3], [3, 4]]
windowed([1, 2, 3, 4, 5], 2, 2); // => [[1, 2], [3, 4]]

zip

Combines arrays element-wise into tuples; stops at the shortest array.

function zip<const T extends readonly (readonly unknown[])[]>(...arrays: T): {
-readonly [K in keyof T]: T[K][number];
}[];

Example:

zip([1, 2, 3], ['a', 'b']); // => [[1, 'a'], [2, 'b']]

zipWith

Combines two arrays element-wise with fn; stops at the shorter array.

function zipWith<A, B, R>(a: readonly A[], b: readonly B[], fn: (a: A, b: B, index: number) => R): R[];

Example:

zipWith([1, 2], [10, 20], (a, b) => a + b); // => [11, 22]

MAX_GENERATED_LENGTH

Maximum element count produced by range, times, and repeat.

const MAX_GENERATED_LENGTH = 10000000;

Page

One page of a list.

interface Page<T> {
readonly items: T[];
readonly total: number;
readonly page: number;
readonly pageSize: number;
readonly pageCount: number;
readonly hasNext: boolean;
readonly hasPrev: boolean;
}

SortByOptions

interface SortByOptions {
readonly collator?: Intl.Collator;
readonly locale?: string | readonly string[];
readonly nulls?: 'first' | 'last';
}

SortKey

One sort key: a selector with direction, null placement, and string ordering.

interface SortKey<T> {
readonly by: (item: T) => unknown;
readonly order?: 'asc' | 'desc';
readonly nulls?: 'first' | 'last';
readonly collator?: Intl.Collator;
readonly compare?: Comparer<unknown>;
}

Comparer

Element comparer: negative, zero, or positive.

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

buildTree

Builds a tree from id/parentId records in input order. Throws HLP_ARRAY_DUPLICATE_ID on repeated ids.

function buildTree<T extends object>(items: readonly T[], options?: BuildTreeOptions<T>): BuildTreeResult<T>;

Example:

const { roots } = buildTree([{ id: 1, parentId: null }, { id: 2, parentId: 1 }]);
roots[0]?.children[0]?.item.id; // => 2

flattenTree

Flattens a tree into entries with depth and parent. Throws HLP_ARRAY_TREE_CYCLE when a node is reached twice.

function flattenTree<N>(roots: Iterable<N>, options?: FlattenTreeOptions<N>): FlatTreeEntry<N>[];

Example:

const rows = [{ id: 1, parentId: null, name: 'CEO' }, { id: 2, parentId: 1, name: 'CTO' }, { id: 3, parentId: 1, name: 'CFO' }];
flattenTree(buildTree(rows).roots).map((e) => `${' '.repeat(e.depth)}${e.node.item.name}`); // => ['CEO', ' CTO', ' CFO']

BuildTreeOptions

interface BuildTreeOptions<T> {
readonly id?: KeySpec<T>;
readonly parentId?: KeySpec<T>;
readonly orphans?: 'root' | 'omit';
}

BuildTreeResult

interface BuildTreeResult<T> {
readonly roots: TreeNode<T>[];
readonly orphans: T[];
readonly cycles: T[];
}

FlattenTreeOptions

interface FlattenTreeOptions<N> {
readonly children?: (node: N) => Iterable<N> | null | undefined;
readonly order?: 'depth' | 'breadth';
}

FlatTreeEntry

interface FlatTreeEntry<N> {
readonly node: N;
readonly depth: number;
readonly parent: N | undefined;
}

TreeNode

A tree node wrapping the original item.

interface TreeNode<T> {
readonly item: T;
readonly children: TreeNode<T>[];
}