order and compare are the sorting a Table
does, kept in a module of their own. Ordering is a fact about values rather than
about rendering, and separating it from the table that draws the rows leaves the
two rules that matter here — where a missing reading goes, and whose array gets
reordered — as something a test can exercise. A rule nothing can exercise is a
rule nobody checks.
Rank
type Rank = string | number | bigint | nullA rank is the value a column orders on: objective, comparable, and separate from
what the cell draws. A cell renders a badge, an address, a price with a currency
on it; none of those sort. Keeping the two apart is why Column in the table
carries cell and sort as different functions over the same row.
bigint is in the union because chain quantities arrive as bigint, and
Number(1n << 64n) is lossy — 2^64 and 2^64 + 1 are the same double, so a
column of wei ordered through Number reports two different balances as a tie.
compare uses < between the two number kinds directly, which is defined
across them; subtracting them would throw.
null is the reading that does not exist, and it is deliberately not zero. A
market with no resting bid is not a market bid at nothing, and sorting the two
together prices a hole.
Missing last, whichever way the column points
function order<R>(rows: R[], rank: (row: R) => Rank, sense: Sense): R[]A null rank sorts last in both directions. order applies the direction to
comparisons between two present ranks and leaves a comparison involving null
alone, so a row with nothing to order on stays at the foot of the column.
const rows = [
{ id: 'c', price: 3 },
{ id: 'a', price: 1 },
{ id: 'gone', price: null },
{ id: 'b', price: 2 },
]
order(rows, (r) => r.price, 'ascending') // a, b, c, gone
order(rows, (r) => r.price, 'descending') // c, b, a, goneReversed along with everything else, the holes would gather at the top of a descending column, which is where a reader takes the first row for the largest value in the venue.
Sense is spelled as aria-sort spells it:
type Sense = 'ascending' | 'descending'so the value a column holds is the value its header cell publishes, and nothing between the two has to translate.
A copy, and stable
order returns a sorted copy. Sorting in place would reorder the array the
caller handed in, and the table's promise that rows keep the order they were
read in until somebody asks otherwise would last exactly one click — the third
click, which returns a column to its given order, would have nothing to return
to. Array.prototype.sort is stable, so rows whose ranks compare equal keep the
order they arrived in.
Installation
npm install @luxfi/uiimport { compare, order, type Rank, type Sense } from '@luxfi/ui/read'These are functions over values. They render nothing, so they need neither a provider nor a token sheet.
Usage
import { compare, order, type Rank, type Sense } from '@luxfi/ui/read'Inside a table, order is already applied and what a caller writes is the rank:
Column.sort returns one, and that same prop is what makes the heading a
control.
const columns: Column<Market>[] = [
{ key: 'pair', head: 'Market', cell: (m) => m.pair, sort: (m) => m.pair },
{
key: 'bid',
head: 'Best bid',
numeric: true,
// The cell says the bid is missing in words; the rank says it with null.
cell: (m) => (m.bid === null ? <Mark>no bid</Mark> : quote(m.bid)),
sort: (m) => m.bid,
},
]Called directly, for a list that is not a table — a set of cards, a chart's series, a select's options:
const [sense, setSense] = useState<Sense>('descending')
const shown = order(pools, (p) => p.liquidity, sense)compare is exported for the case where the ordering is not one column: a
comparator that falls back to a second rank when the first ties, or a sort
somewhere else that has to agree with this one about where a hole goes.
const bySizeThenAge = (a: Position, b: Position) =>
compare(a.size, b.size) || compare(a.opened, b.opened)Signatures
compare(a, b)
| Parameter | Type | Description |
|---|---|---|
a | Rank | The left rank. |
b | Rank | The right rank. |
Returns -1, 0 or 1 for ascending order, with null after every present
rank. Strings compare with localeCompare; a number and a bigint compare as
quantities.
order(rows, rank, sense)
| Parameter | Type | Description |
|---|---|---|
rows | R[] | The rows to order. Not modified. |
rank | (row: R) => Rank | What this ordering is on. |
sense | Sense | 'ascending' or 'descending', as aria-sort spells it. |
Returns a new array: the rows sorted by rank in sense, stable in ties, with
a null rank last in either sense.
Types
| Type | Definition | Description |
|---|---|---|
Rank | string | number | bigint | null | The value a column orders on. null is the reading that does not exist. |
Sense | 'ascending' | 'descending' | Direction, in the words aria-sort uses. |
What it does not do
It orders what it is given and knows nothing about where the rows came from, so
a venue larger than one response is ordered a page at a time and needs its own
sort at the source. It pins no locale: text goes through localeCompare under
whatever the runtime's default is, and an order produced on a server can differ
from the order the same rows take in a browser. It compares within a kind rather
than across — a column whose rank is a number for some rows and a string for
others falls to text ordering, where '10' precedes 9, and a number and a
bigint of equal value are not equal to ===, so neither is placed before the
other. Ranks are read once per comparison rather than once per row, so a rank
that is expensive, or that answers differently each time it is called, is the
caller's problem. And it holds no opinion about which column a list should open
sorted by, which a reader will read as the natural order of the data.