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