Narabe.Position (Narabe v0.1.0)

Copy Markdown View Source

Position keys: a text per item that sorts between its neighbours, so that moving an item writes that one row, and two people moving at once both land.

iex> Narabe.Position.between(nil, nil)
"a0"
iex> Narabe.Position.between("a0", nil)
"a1"
iex> Narabe.Position.between("a0", "a1")
"a0V"
iex> Narabe.Position.between(nil, "a0")
"Zz"

It is fractional indexing, as rocicorp's fractional-indexing (CC0): base 62 (0-9A-Za-z), compared byte by byte. A key is an integer part (a head letter that says its length, then digits) and a fraction that never ends in 0. Keys after the last count up ("a1", "a2", …, "b00"), keys before the first count down, and a key between two close ones grows a fraction. The browser's twin (assets/src/position.ts) gives the same keys, held to the same answers by one fixture.

In the database

Keys compare byte by byte, which is not every database's default:

  • Postgres: the column needs COLLATE "C" (Narabe.Migration.position/2 adds it); a text column in a locale's collation sorts "a" and "B" the other way.
  • MySQL: a binary collation (utf8mb4_bin).
  • SQLite: its default (BINARY) is right.

Two moves into the same gap at the same moment get the same key: order by the key, then by id (Narabe.Ecto.ordered/2), and the order is the same on every screen. Keys grow only when items keep landing in the same gap; Narabe.Ecto.rebalance/3 spreads a list's keys out again.

Summary

Types

t()

A position key.

Functions

A key between a and b; nil is the start or the end.

n keys between a and b, in order, spread so that none grows long: for several items moved at once, or a list's keys written anew.

Whether a text is a position key.

Types

t()

@type t() :: String.t()

A position key.

Functions

between(a, b)

@spec between(t() | nil, t() | nil) :: t()

A key between a and b; nil is the start or the end.

Raises ArgumentError when a is not before b, or either is not a key.

between_many(a, b, n)

@spec between_many(t() | nil, t() | nil, non_neg_integer()) :: [t()]

n keys between a and b, in order, spread so that none grows long: for several items moved at once, or a list's keys written anew.

iex> Narabe.Position.between_many(nil, nil, 3)
["a0", "a1", "a2"]
iex> Narabe.Position.between_many("a0", "a1", 3)
["a0G", "a0V", "a0l"]

valid?(key)

@spec valid?(term()) :: boolean()

Whether a text is a position key.

iex> Narabe.Position.valid?("a0V")
true
iex> Narabe.Position.valid?("a0V0")
false