fractional-indexing

repository·main·Indexed 20 days ago

https://github.com/rocicorp/fractional-indexing

A library for generating fractional indexes to enable realtime editing of ordered sequences. It allows inserting items between existing items without re-indexing the entire list using functions like generateKeyBetween and generateNKeysBetween. Version 4.0.0 provides support for custom alphabets via digits and intDigits, and is byte-for-byte compatible with implementations in Go, Python, Kotlin, and Ruby.

Tokens
2.8K
Snippets
8
Records
9
Agent score
19%

What's inside fractional-indexing

  1. Configure custom alphabets using `digits` and `intDigits`

    main

    You can customize the character sets used for generating keys.

    The digits alphabet

    Controls the digit values of a key. Defaults to BASE_62_DIGITS (0-9A-Za-z). Requirements:

    1. Must be single-byte (ASCII/Latin-1, not multi-byte like Greek).
    2. Must be sorted in ascending character-code order with no duplicates.

    The intDigits alphabet (Integer Heads)

    Every key starts with a 'head' (a magnitude/length marker) drawn from intDigits. The alphabet is split in half: the first half are negative-length heads, the second half are positive-length heads. It must have an even length.

    Behavior Patterns

    • Self-headed (Default): If you provide digits but omit intDigits, intDigits defaults to digits. This is useful for custom bases like base-10.
    • Classic Latin Heads: If you omit digits entirely, intDigits defaults to BASE_52_DIGITS (A-Z for negative, a-z for positive). This produces the classic format (e.g., a0, Z9).
    • Hybrid: To use a custom digits alphabet while keeping classic Latin heads, pass BASE_52_DIGITS explicitly as the intDigits argument.
    import { generateNKeysBetween, BASE_52_DIGITS } from "fractional-indexing";
    
    // self-headed (intDigits defaults to digits):
    generateNKeysBetween(null, null, 4, "0123456789"); // ["50", "51", "52", "53"]
    
    // Latin heads (intDigits set explicitly):
    generateNKeysBetween(null, null, 4, "0123456789", BASE_52_DIGITS); // ["a0", "a1", "a2", "a3"]
  2. Sort fractional indexes correctly

    main

    Fractional indexes are case-sensitive. Do not use String.prototype.localeCompare, as it is case-insensitive and will result in incorrect ordering.

    Instead, use native string comparison operators (<, >, ===) or Array.prototype.sort which uses lexicographic comparison by default.

    const arr = [
      { id: "todo_1", fractionalIndex: "YzZ" },
      { id: "todo_2", fractionalIndex: "Yza" }
    ];
    
    // Correct: Use native comparison
    const sorted = arr.toSorted((a, b) =>
      a.fractionalIndex < b.fractionalIndex
        ? -1
        : a.fractionalIndex > b.fractionalIndex
          ? 1
          : 0,
    );
  3. How fractional indexing heads and digits work together

    main

    Fractional indexing in this library uses a two-part key structure: an integer part (the "head") and a fractional part.

    1. The Head (intDigits): The first character of every key is a magnitude marker drawn from the intDigits alphabet. This head determines the length and sign of the integer part.
      • The first half of intDigits represents negative-length heads.
      • The second half represents positive-length heads.
      • The two characters straddling the midpoint of intDigits represent the shortest possible integer parts (length 2).
    2. The Digits (digits): These define the values used in the fractional part of the key.

    Configuration Patterns:

    • Self-headed keys: If you provide digits (e.g., '0123456789'), intDigits defaults to those same digits. This results in keys like "50" or "600".
    • Classic heads: If you omit digits entirely, the library uses BASE_52_DIGITS (A-Z and a-z) for the heads, resulting in the classic form like "a0" or "Zz".
    // Self-headed (base 10)
    // Head is from '0123456789', fractional part is from '0123456789'
    generateKeyBetween(null, null, "0123456789"); // => "50"
    
    // Classic heads (A-Z/a-z)
    // Head is from 'A-Z/a-z', fractional part is from '0123456789...z'
    generateKeyBetween(null, null, undefined, undefined); // => "a0"
  4. Generate a single key with `generateKeyBetween`

    main

    Use generateKeyBetween to create a single fractional index between two existing points. You can pass null or undefined to represent the boundaries of the sequence (start or end).

    Signature:

    generateKeyBetween(
      a: string | null | undefined, // start
      b: string | null | undefined, // end
      digits?: string, // digit alphabet, defaults to BASE_62_DIGITS (0-9A-Za-z)
      intDigits?: string, // integer-head alphabet, defaults to `digits`
    ): string;
    import { generateKeyBetween } from 'fractional-indexing';
    
    const first = generateKeyBetween(null, null); // "a0"
    
    // Insert after 1st
    const second = generateKeyBetween(first, null); // "a1"
    
    // Insert after 2nd
    const third = generateKeyBetween(second, null); // "a2"
    
    // Insert before 1st
    const zeroth = generateKeyBetween(null, first); // "Zz"
    
    // Insert in between 2nd and 3rd (midpoint)
    const secondAndHalf = generateKeyBetween(second, third); // "a1V"
  5. Generate multiple keys with `generateNKeysBetween`

    main

    Use generateNKeysBetween when you need to insert multiple keys at a known position. This method spaces out the indexes more evenly than calling generateKeyBetween multiple times, which leads to shorter keys and better performance.

    Signature:

    generateNKeysBetween(
      a: string | null | undefined, // start
      b: string | null | undefined, // end
      n: number, // number of keys to generate evenly between start and end
      digits?: string, // digit alphabet, defaults to BASE_62_DIGITS (0-9A-Za-z)
      intDigits?: string, // integer-head alphabet, defaults to `digits`
    ): string[];
    import { generateNKeysBetween } from 'fractional-indexing';
    
    const first = generateNKeysBetween(null, null, 2); // ['a0', 'a1']
    
    // Insert two keys after 2nd
    // (Assuming first[1] is the 2nd key)
    generateNKeysBetween(first[1], null, 2); // ['a2', 'a3']
    
    // Insert two keys before 1st
    // (Assuming first[0] is the 1st key)
    generateNKeysBetween(null, first[0], 2); // ['Zy', 'Zz']
    
    // Insert two keys in between 1st and 2nd (midpoints)
    // (Assuming second and third are existing keys)
    generateNKeysBetween(second, third, 2); // ['a0G', 'a0V']
  6. Cross-language compatibility and Random Jitter

    main

    This implementation is designed to be byte-for-byte compatible with other implementations in:

    • Go (rocicorp/fracdex)
    • Python (httpie/fractional-indexing-python)
    • Kotlin (darvelo/fractional-indexing-kotlin)
    • Ruby (kazu-2020/fractional_indexer)

    To minimize collisions during concurrent generation, you can use random jitter. The TypeScript implementation nathanhleung/jittered-fractional-indexing extends this package's functionality to support this.

  7. Generate multiple fractional index keys with `generateNKeysBetween`

    main

    Use generateNKeysBetween to generate an array of n distinct, sorted keys within a range.

    Parameters:

    • a: The lower bound key (or null).
    • b: The upper bound key (or null).
    • n: The number of keys to generate (must be $\ge 0$).
    • digits (optional): The fractional alphabet.
    • intDigits (optional): The integer-part head alphabet.

    Behavior:

    • If n === 0, returns [].
    • If a and b are both null, returns keys starting from the smallest possible positive key.
    • If one boundary is null, it returns consecutive integer-based keys.
    • If both boundaries are provided, it returns relatively short keys distributed between them.
    import { generateNKeysBetween } from 'fractional-indexing';
    
    // Generate 5 keys between 'a' and 'b'
    const keys = generateNKeysBetween('a', 'b', 5);
  8. Generate a single fractional index key with `generateKeyBetween`

    main

    Use generateKeyBetween to create a new order key that lexicographically sorts between two existing keys (or boundaries).

    Parameters:

    • a: The lower bound key (or null for the start of the range).
    • b: The upper bound key (or null for the end of the range).
    • digits (optional): The alphabet used for the fractional part. Must be single-byte characters in strictly ascending order. If omitted, it defaults to BASE_62_DIGITS.
    • intDigits (optional): The alphabet used for the integer-part 'head' marker. Must be an even-length string of single-byte characters in strictly ascending order. If omitted, it defaults to digits (if provided) or BASE_52_DIGITS.

    Key Behaviors:

    • If both a and b are null, it returns the shortest possible positive key.
    • If digits is provided, keys become "self-headed" (the head is drawn from digits).
    • If digits is omitted, keys use the classic BASE_52_DIGITS (A-Z/a-z) head markers.
    • The function handles a > b by swapping them internally.
    import { generateKeyBetween } from 'fractional-indexing';
    
    // Base 10 example: heads come from the digits themselves
    // generateKeyBetween(null, null, "0123456789") => "50"
    const key = generateKeyBetween(null, null, "0123456789");
  9. Reference: Default Alphabets

    main

    The library provides two constant alphabets for use with the API:

    • BASE_62_DIGITS: 0-9, A-Z, and a-z.
    • BASE_52_DIGITS: A-Z and a-z (derived from BASE_62_DIGITS.slice(10)).
    export const BASE_62_DIGITS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
    export const BASE_52_DIGITS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";