Skip to main contentSkip to navigation

How rows order when some of them have nothing to order on — missing last in both directions, over a copy.

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.

Loading...

Rank

type Rank = string | number | bigint | null

A 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.

It asks < twice. 1n === 1 is false, so a column that reads a chain and falls back to a cached figure holds two values that are the same quantity and not the same value — and deciding equality by identity alone answers "greater" for that pair whichever way it is asked. That is not an ordering, and a browser handed one shuffles the rows it cannot separate: four equal values sorted descending came back in the order BDAC. The second < is what decides equality where === could not.

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, gone

Reversed 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/ui
import { 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)

ParameterTypeDescription
aRankThe left rank.
bRankThe 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, and two of equal quantity compare as one.

order(rows, rank, sense)

ParameterTypeDescription
rowsR[]The rows to order. Not modified.
rank(row: R) => RankWhat this ordering is on.
senseSense'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

TypeDefinitionDescription
Rankstring | number | bigint | nullThe 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. 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.