README.md

# LexSortKey

Lexicographically sortable keys for ordered sequences in Elixir.

This library implements fractional indexing as outlined in:
- [Real-time Editing of Ordered Sequences](https://www.figma.com/blog/realtime-editing-of-ordered-sequences/) (Figma)
- [Implementing Fractional Indexing](https://observablehq.com/@dgreensp/implementing-fractional-indexing)

## Use Cases

- Real-time collaborative editing (like Figma, Notion, Linear)
- Ordered lists in databases without reindexing
- Distributed systems where clients generate keys independently
- Any scenario requiring insertion at arbitrary positions

## Installation

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

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

## Quick Start

```elixir
# 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:

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

## API Reference

### Basic Operations

```elixir
# 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

```elixir
# 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:

```elixir
# 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

- **Sequential insertions best case**: Inserting on first or last position keeps the keys sweet and short. 1M keys is 5 chars and takes few usec per key to generate.
- **Sequential insertions worst case**: But in worst case scenario keys can grow logarithmically. After 1000 sequential inserts on a second position, max key length is ~169 characters.
- **Distributed insertions**: Using `n_between_distributed/3` keeps keys much shorter (~4 characters for 1000 keys).
- **Rebalancing**: Call `rebalance/1` periodically to reset key lengths to optimal (~2-3 characters).

```elixir
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](LICENSE) for details.