Algorithm Search⌘K

Gap buffer in Python

Array with an empty gap at the edit point, making local insertions and deletions cheap.

File
gap_buffer.py, 91 lines
Level
I0 not checked yet
Toolchain
Python 3.14, pytest 9
Test framework
pytest
Run the tests
python3 -m pytest
gap_buffer.pyRawGitHub
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/gap-buffer/. The authors are listed in prov/DCWD8D.jsonl.

from typing import Any, Generic, Iterator, TypeVar

T = TypeVar("T")


class GapBuffer(Generic[T]):
    """A sequence in one array with a gap of free cells at the cursor.

    The text is a[0:g] followed by a[e:cap]; the cursor is at position g.
    insert and delete at the cursor cost O(1) amortized; move_to(p) costs
    O(|p - g| + 1); get(i) costs O(1). Space: cap cells. The buffer never
    shrinks, so cap <= max(initial capacity, 2 * largest n so far).
    """

    def __init__(self, capacity: int = 1) -> None:
        cap = max(1, capacity)
        self.a: list[Any] = [None] * cap
        self.g = 0
        self.e = cap

    def __len__(self) -> int:
        return len(self.a) - (self.e - self.g)

    def __iter__(self) -> Iterator[T]:
        for i in range(len(self)):
            yield self.get(i)

    def cursor(self) -> int:
        return self.g

    def move_to(self, p: int) -> int:
        """Move the cursor to position p, 0 <= p <= n. Return the cells moved."""
        if not 0 <= p <= len(self):
            raise IndexError("position out of range")
        moved = 0
        while self.g > p:  # @line L1
            self.e -= 1
            self.g -= 1
            self.a[self.e] = self.a[self.g]
            moved += 1
        while self.g < p:  # @line L2
            self.a[self.g] = self.a[self.e]
            self.g += 1
            self.e += 1
            moved += 1
        return moved

    def insert(self, x: T) -> None:
        """Insert x before the cursor and move the cursor past it. O(1) amortized."""
        if self.g == self.e:  # @line L3
            self._grow()
        self.a[self.g] = x  # @line L4
        self.g += 1

    def delete_before(self) -> T:
        """Delete the item before the cursor (backspace). Cost O(1)."""
        if self.g == 0:  # @line L5
            raise IndexError("no item before the cursor")
        self.g -= 1
        return self.a[self.g]

    def delete_after(self) -> T:
        """Delete the item after the cursor. Cost O(1)."""
        if self.e == len(self.a):  # @line L6
            raise IndexError("no item after the cursor")
        x = self.a[self.e]
        self.e += 1
        return x

    def get(self, i: int) -> T:
        """Return the item at position i. Cost O(1)."""
        if not 0 <= i < len(self):
            raise IndexError("position out of range")
        return self.a[i] if i < self.g else self.a[i + self.e - self.g]  # @line L7

    def _grow(self) -> None:
        cap = len(self.a)
        new_cap = 2 * cap  # @line L8
        b: list[Any] = [None] * new_cap
        b[: self.g] = self.a[: self.g]
        tail = cap - self.e
        b[new_cap - tail :] = self.a[self.e :]
        self.a = b
        self.e = new_cap - tail

    def check_invariant(self) -> None:
        assert 0 <= self.g <= self.e <= len(self.a)
        assert len(self.a) >= 1
test_gap_buffer.py 69 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/gap-buffer/. The authors are listed in prov/DCWD8D.jsonl.

import random

import pytest

from gap_buffer import GapBuffer


def test_page_example() -> None:
    B: GapBuffer[str] = GapBuffer(4)
    for c in "cat":
        B.insert(c)
    assert (B.g, B.e) == (3, 4)
    assert B.move_to(1) == 2 and (B.g, B.e) == (1, 2)
    B.insert("h")
    assert "".join(B) == "chat" and B.g == B.e == 2
    assert B.move_to(4) == 2
    B.insert("s")
    assert len(B.a) == 8 and (B.g, B.e) == (5, 8)
    B.insert("!")
    assert B.move_to(0) == 6 and (B.g, B.e) == (0, 2)
    assert B.delete_after() == "c"
    assert "".join(B) == "hats!" and B.get(4) == "!"
    B.check_invariant()


def test_edges() -> None:
    B: GapBuffer[int] = GapBuffer(0)
    assert len(B.a) == 1 and len(B) == 0
    with pytest.raises(IndexError):
        B.delete_before()
    with pytest.raises(IndexError):
        B.delete_after()
    with pytest.raises(IndexError):
        B.move_to(1)
    with pytest.raises(IndexError):
        B.get(0)
    B.insert(7)
    B.insert(8)
    assert list(B) == [7, 8]
    assert B.delete_before() == 8
    B.move_to(0)
    assert B.delete_after() == 7 and len(B) == 0
    B.check_invariant()


def test_random_against_list() -> None:
    rng = random.Random(1980)
    for _ in range(600):
        B: GapBuffer[int] = GapBuffer(rng.randrange(0, 4))
        model: list[int] = []
        cur = 0
        for _ in range(rng.randrange(1, 60)):
            op = rng.randrange(4)
            if op == 0:
                x = rng.randrange(100)
                B.insert(x); model.insert(cur, x); cur += 1
            elif op == 1:
                p = rng.randrange(len(model) + 1)
                assert B.move_to(p) == abs(p - cur)
                cur = p
            elif op == 2 and cur > 0:
                assert B.delete_before() == model.pop(cur - 1); cur -= 1
            elif op == 3 and cur < len(model):
                assert B.delete_after() == model.pop(cur)
            B.check_invariant()
            assert list(B) == model and B.cursor() == cur

What the next level needs

  • the tests did not run in this build

Read the full page about Gap buffer