LexSortKey

Lexicographically sortable keys for ordered sequences in Elixir.

This library implements fractional indexing as outlined in:

Use Cases

Installation

Add lex_sort_key to your list of dependencies in mix.exs:

def deps do
  [
    {:lex_sort_key, "~> 0.1.0"}
  ]
end

Quick Start

# Create initial keys for a list
keys = LexSortKey.n_first(3)
# => ["a0", "a1", "a2"]

# Insert between two keys
{:ok, between} = LexSortKey.between("a0", "a1")
# => {:ok, "a0V"}

# Keys sort correctly as strings
Enum.sort(["a1", "a0V", "a0"])
# => ["a0", "a0V", "a1"]

How It Works

Keys consist of two parts: a variable-length integer and an optional fractional part.

Variable-Length Integer Encoding

The first character determines the length, enabling correct lexicographic sorting:

Prefix Length Range Example
Z 2 chars 62 values Z0 to Zz
a 2 chars 62 values a0 to az
b 3 chars 3,844 values b00 to bzz
c 4 chars 238,328 values c000 to czzz

Uppercase letters (A-Z) represent values before a0, lowercase (a-z) represent values after.

Fractional Part

When inserting between adjacent integers, a fractional part is appended:

LexSortKey.between("a0", "a1")    # => "a0V"
LexSortKey.between("a0", "a0V")   # => "a0F"
LexSortKey.between("a0V", "a1")   # => "a0k"

API Reference

Basic Operations

# Get the first key (zero key)
LexSortKey.first()
# => "a0"

# Generate key after another
{:ok, key} = LexSortKey.key_after("a0")
# => {:ok, "a1"}

# Generate key before another
{:ok, key} = LexSortKey.key_before("a1")
# => {:ok, "a0"}

# Generate key between two keys
{:ok, key} = LexSortKey.between("a0", "a1")
# => {:ok, "a0V"}

Bulk Operations

# Generate N consecutive keys starting from a0
LexSortKey.n_first(5)
# => ["a0", "a1", "a2", "a3", "a4"]

# Generate N keys after a given key
{:ok, keys} = LexSortKey.n_after("a0", 3)
# => {:ok, ["a1", "a2", "a3"]}

# Generate N keys before a given key
{:ok, keys} = LexSortKey.n_before("a2", 2)
# => {:ok, ["a0", "a0V"]}

# Generate N keys between two keys
{:ok, keys} = LexSortKey.n_between("a0", "a1", 3)
# => {:ok, ["a0V", "a0k", "a0s"]}

# Distributed insertion (shorter keys)
{:ok, keys} = LexSortKey.n_between_distributed("a0", "a1", 3)
# => {:ok, ["a0F", "a0V", "a0k"]}

# Efficient prepending
{:ok, keys} = LexSortKey.prepend_n("a0", 3)
# => {:ok, ["Zx", "Zy", "Zz"]}

Rebalancing

After many insertions, keys can grow long. Rebalance to get compact keys:

# Long keys from repeated insertions
long_keys = ["a0", "a0VVVVVV", "a0VVVVVk", "a1"]

# Rebalance to short keys
{:ok, short_keys} = LexSortKey.rebalance(long_keys)
# => {:ok, ["a0", "a1", "a2", "a3"]}

# Rebalance with bounds (preserves surrounding keys)
{:ok, keys} = LexSortKey.rebalance(keys, lower_bound: "a0", upper_bound: "b00")

# Rebalance only a portion of the list
{:ok, keys} = LexSortKey.rebalance_range(keys, 1..3)

Performance Considerations

iex()> Enum.reduce(1..1000, nil, fn _, acc -> LexSortKey.key_before(acc) |> elem(1)  end) |> IO.puts
Ykt
iex()> Enum.reduce(1..1000, nil, fn _, acc -> LexSortKey.key_after(acc) |> elem(1)  end) |> IO.puts
bF7
iex()> Enum.reduce(1..1000, nil, fn _, acc -> LexSortKey.between("a0", acc) |> elem(1)  end) |> IO.puts
a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003
:timer.tc(fn -> Enum.reduce(1..1000000, nil, fn _, acc -> LexSortKey.key_after(acc) |> elem(1)  end) end)
{3345962, "d3B81"}

License

MIT License. See LICENSE for details.