Skip to content

engine

The workbook model: a Grid of Sheets, each a sparse dict[(col, row)] -> Cell, plus the array type ranges evaluate to, the reference parsing and adjustment rules, and JSON load/save.

engine

Mode

Bases: IntEnum

Grid

Grid()
Source code in src/gridcalc/engine.py
def __init__(self) -> None:
    self.sheets: list[Sheet] = [Sheet()]
    self.active: int = 0
    self.vc: int = 0
    self.vr: int = 0
    self.tc: int = 0
    self.tr: int = 0
    self.fmt: str = ""
    self.dirty: int = 0
    self.cw: int = CW_DEFAULT
    self.filename: str | None = None
    self.names: list[NamedRange] = []
    # Named ranges bound into the PYTHON-mode eval globals on the last
    # recalc, so a name that disappears can be unbound again.
    # Workbook-persistent LP model definitions. Maps a user-chosen
    # name (or "default" for the convention slot) to an OptModel
    # holding the spec strings the user typed for sense/objective/
    # vars/constraints/bounds. Loaded from "models" in the JSON;
    # serialized back on jsonsave; consumed by the `:opt` dispatcher.
    self.models: dict[str, Any] = {}
    self.code: str = ""
    self.mc: int = -1
    self.mr: int = -1
    self._eval_globals: dict[str, Any] = _make_eval_globals()
    self.requires: list[str] = []
    self.libs: list[str] = []
    self._module_errors: list[str] = []
    self.code_error: str | None = None
    self.mode: Mode = Mode.PYTHON
    # Topological recalc bookkeeping. Workbook-wide dep graph keyed by
    # (sheet, c, r) 3-tuples; `sheet` is the sheet name, never None for
    # entries that _refresh_deps installs (only `extract_refs` may emit
    # None transiently for unsheeted refs, but `_refresh_deps` always
    # passes a concrete sheet via formula_sheet).
    self._dep_of: dict[tuple[str | None, int, int], set[tuple[str | None, int, int]]] = {}
    self._subscribers: dict[tuple[str | None, int, int], set[tuple[str | None, int, int]]] = {}
    self._volatile: set[tuple[str | None, int, int]] = set()
    # Anchors currently rejected with #SPILL!. A blocked anchor has no
    # dependency on the cell blocking it, so any edit re-attempts every
    # blocked anchor -- cheap, since the set is normally empty.
    self._spill_blocked: set[tuple[str | None, int, int]] = set()
    # Set True by `_rebuild_dep_graph`; remains True while
    # `_refresh_deps`/`_clear_deps` maintain the graph incrementally.
    # Reset to False on mode entry into EXCEL/HYBRID (LEGACY skips
    # graph maintenance, so the graph is stale on mode transition).
    # `_recalc_topo` skips its rebuild call when this flag is True.
    self._dep_graph_built: bool = False
add_sheet
add_sheet(name: str) -> Sheet

Append a new sheet. Returns the sheet.

NOTE (phase 1): the dep graph keys are still (c, r) tuples and do not carry sheet identity. Until phase 2 (sheet-qualified references) lands, formulas on different sheets that touch the same (c, r) collide in the dep graph. Treat multi-sheet workbooks as preview-only until then.

Source code in src/gridcalc/engine.py
def add_sheet(self, name: str) -> Sheet:
    """Append a new sheet. Returns the sheet.

    NOTE (phase 1): the dep graph keys are still ``(c, r)`` tuples
    and do not carry sheet identity. Until phase 2 (sheet-qualified
    references) lands, formulas on different sheets that touch the
    same ``(c, r)`` collide in the dep graph. Treat multi-sheet
    workbooks as preview-only until then.
    """
    if any(s.name == name for s in self.sheets):
        raise ValueError(f"sheet {name!r} already exists")
    sh = Sheet(name=name)
    self.sheets.append(sh)
    return sh
move_sheet
move_sheet(name: str, new_idx: int) -> None

Reorder name to new_idx (zero-based).

Active-sheet identity is preserved: if the active sheet is the one being moved, it follows; if some other sheet is active, its index is recomputed so the same sheet stays active.

Dep graph keys carry sheet names rather than indices, so reordering doesn't invalidate the graph -- no rebuild needed.

Source code in src/gridcalc/engine.py
def move_sheet(self, name: str, new_idx: int) -> None:
    """Reorder ``name`` to ``new_idx`` (zero-based).

    Active-sheet identity is preserved: if the active sheet is the
    one being moved, it follows; if some other sheet is active, its
    index is recomputed so the same sheet stays active.

    Dep graph keys carry sheet names rather than indices, so
    reordering doesn't invalidate the graph -- no rebuild needed.
    """
    if not (0 <= new_idx < len(self.sheets)):
        raise IndexError(new_idx)
    cur_idx = next((i for i, s in enumerate(self.sheets) if s.name == name), -1)
    if cur_idx < 0:
        raise KeyError(name)
    if cur_idx == new_idx:
        return
    active_sheet = self._active
    sh = self.sheets.pop(cur_idx)
    self.sheets.insert(new_idx, sh)
    # Restore active by identity.
    self.active = self.sheets.index(active_sheet)
rename_sheet
rename_sheet(old: str, new: str) -> None

Rename a sheet and rewrite formula text that references the old name.

Walks every formula cell on every sheet and rewrites any <old>! sheet prefix to <new>!. Skips matches inside double-quoted string literals so a user formula like ="Other!A1" is left untouched. Invalidates the cached AST on each rewritten cell so the next recalc re-parses with the new sheet name.

Caller is responsible for rebuilding the dep graph and triggering a recalc; cmd_sheet does both.

Source code in src/gridcalc/engine.py
def rename_sheet(self, old: str, new: str) -> None:
    """Rename a sheet and rewrite formula text that references the old name.

    Walks every formula cell on every sheet and rewrites any
    ``<old>!`` sheet prefix to ``<new>!``. Skips matches inside
    double-quoted string literals so a user formula like
    ``="Other!A1"`` is left untouched. Invalidates the cached AST
    on each rewritten cell so the next recalc re-parses with the
    new sheet name.

    Caller is responsible for rebuilding the dep graph and
    triggering a recalc; ``cmd_sheet`` does both.
    """
    if old == new:
        return
    if any(s.name == new for s in self.sheets):
        raise ValueError(f"sheet {new!r} already exists")
    target = next((s for s in self.sheets if s.name == old), None)
    if target is None:
        raise KeyError(old)
    target.name = new
    # Rewrite formula text that references the old sheet name.
    for sh in self.sheets:
        for cl in sh._cells.values():
            if cl.type != FORMULA:
                continue
            rewritten = _rewrite_sheet_prefix(cl.text, old, new)
            if rewritten != cl.text:
                cl.text = rewritten
                cl.ast = None
                cl.ast_text = ""
next_sheet
next_sheet() -> None

Advance the active sheet by one, wrapping at the end. No-op on a single-sheet workbook.

Source code in src/gridcalc/engine.py
def next_sheet(self) -> None:
    """Advance the active sheet by one, wrapping at the end. No-op
    on a single-sheet workbook."""
    n = len(self.sheets)
    if n <= 1:
        return
    self.active = (self.active + 1) % n
prev_sheet
prev_sheet() -> None

Retreat the active sheet by one, wrapping at the start. No-op on a single-sheet workbook.

Source code in src/gridcalc/engine.py
def prev_sheet(self) -> None:
    """Retreat the active sheet by one, wrapping at the start.
    No-op on a single-sheet workbook."""
    n = len(self.sheets)
    if n <= 1:
        return
    self.active = (self.active - 1) % n
load_lib
load_lib(name: str) -> None

Load a formula lib's builtins into the eval namespace.

Source code in src/gridcalc/engine.py
def load_lib(self, name: str) -> None:
    """Load a formula lib's builtins into the eval namespace."""
    if not name:
        return
    from .libs import get_lib_builtins

    self._eval_globals.update(get_lib_builtins(name))
load_requires
load_requires(
    modules: list[str], allow_unknown: bool = False
) -> None

Load required modules into the eval namespace.

allow_unknown passes through to load_modules: unclassified modules are refused unless the caller has explicitly approved them.

Source code in src/gridcalc/engine.py
def load_requires(self, modules: list[str], allow_unknown: bool = False) -> None:
    """Load required modules into the eval namespace.

    ``allow_unknown`` passes through to ``load_modules``: unclassified
    modules are refused unless the caller has explicitly approved them.
    """
    if not modules:
        return
    mods, errors = load_modules(modules, allow_unknown=allow_unknown)
    self._eval_globals.update(mods)
    self._module_errors = errors
clear_all
clear_all() -> None

Remove all cells from the grid.

Source code in src/gridcalc/engine.py
def clear_all(self) -> None:
    """Remove all cells from the grid."""
    self._cells.clear()
    self._dep_of.clear()
    self._subscribers.clear()
    self._volatile.clear()
    self._spill_blocked.clear()
setcells_bulk
setcells_bulk(
    cells: Iterable[tuple[int, int, str]],
) -> None

Set many cells, deferring recalc until all are written.

Each tuple is (col, row, text). Out-of-bounds entries are ignored. Roughly N x faster than calling setcell() N times because recalc() runs once instead of after every cell.

Source code in src/gridcalc/engine.py
def setcells_bulk(self, cells: Iterable[tuple[int, int, str]]) -> None:
    """Set many cells, deferring recalc until all are written.

    Each tuple is (col, row, text). Out-of-bounds entries are ignored.
    Roughly N x faster than calling setcell() N times because recalc()
    runs once instead of after every cell.
    """
    changed: set[tuple[int, int]] = set()
    for c, r, text in cells:
        changed |= self._spill_predirty(c, r)
        if self._setcell_no_recalc(c, r, text):
            changed.add((c, r))
    if changed:
        self.recalc(changed | self._blocked_anchors_active())
can_insert
can_insert(axis: str, at: int, count: int = 1) -> bool

True when inserting count lines at at would lose no data.

The sheet is a fixed NROW x NCOL grid, so an insert near the end pushes the last lines past the edge. Silently dropping them is data loss the user never asked for and cannot see, so callers check first and refuse. axis is "R" for rows or "C" for columns.

Source code in src/gridcalc/engine.py
def can_insert(self, axis: str, at: int, count: int = 1) -> bool:
    """True when inserting ``count`` lines at ``at`` would lose no data.

    The sheet is a fixed NROW x NCOL grid, so an insert near the end pushes
    the last lines past the edge. Silently dropping them is data loss the
    user never asked for and cannot see, so callers check first and refuse.
    ``axis`` is "R" for rows or "C" for columns.
    """
    limit = NROW if axis == "R" else NCOL
    edge = limit - count  # a line at or past this is pushed off the end
    for (c, r), cl in self._cells.items():
        if cl.type == EMPTY:
            continue
        pos = r if axis == "R" else c
        if pos >= at and pos >= edge:
            return False
    return True
insertrow
insertrow(at: int) -> bool

Insert one row at at. False, without mutating, if that would push a populated row off the bottom of the sheet.

Source code in src/gridcalc/engine.py
def insertrow(self, at: int) -> bool:
    """Insert one row at ``at``. False, without mutating, if that would push
    a populated row off the bottom of the sheet."""
    if not self.can_insert("R", at):
        return False
    self._drop_all_spills()
    new_cells: dict[tuple[int, int], Cell] = {}
    for (c, r), cl in self._cells.items():
        if r >= at:
            new_cells[(c, r + 1)] = cl
        else:
            new_cells[(c, r)] = cl
    self._cells = new_cells
    self._shiftrefs("R", at, +1)
    self._shift_names("R", at, +1)
    self._rebuild_dep_graph()
    self.dirty = 1
    return True
insertcol
insertcol(at: int) -> bool

Insert one column at at. False, without mutating, if that would push a populated column off the right edge of the sheet.

Source code in src/gridcalc/engine.py
def insertcol(self, at: int) -> bool:
    """Insert one column at ``at``. False, without mutating, if that would
    push a populated column off the right edge of the sheet."""
    if not self.can_insert("C", at):
        return False
    self._drop_all_spills()
    new_cells: dict[tuple[int, int], Cell] = {}
    for (c, r), cl in self._cells.items():
        if c >= at:
            new_cells[(c + 1, r)] = cl
        else:
            new_cells[(c, r)] = cl
    self._cells = new_cells
    self._shiftrefs("C", at, +1)
    self._shift_names("C", at, +1)
    self._shift_widths(at, +1)
    self._rebuild_dep_graph()
    self.dirty = 1
    return True
csvsave
csvsave(filename: str) -> int

Export evaluated cell values to CSV.

Source code in src/gridcalc/engine.py
def csvsave(self, filename: str) -> int:
    """Export evaluated cell values to CSV."""
    maxr = -1
    maxc = -1
    for (c, r), sc in self._cells.items():
        if sc.type != EMPTY:
            if r > maxr:
                maxr = r
            if c > maxc:
                maxc = c

    if maxr < 0:
        try:
            with open(filename, "w", newline="") as f:
                f.write("")
        except OSError:
            return -1
        return 0

    try:
        with open(filename, "w", newline="") as f:
            writer = csv.writer(f)
            for r in range(maxr + 1):
                row: list[str] = []
                for c in range(maxc + 1):
                    cl = self._cells.get((c, r))
                    if not cl or cl.type == EMPTY:
                        row.append("")
                    elif cl.type == LABEL:
                        row.append(cl.text)
                    elif cl.type in (NUM, FORMULA):
                        if isinstance(cl.val, float) and math.isnan(cl.val):
                            row.append("")
                        elif abs(cl.val) < 1e15 and cl.val == int(cl.val):
                            row.append(str(int(cl.val)))
                        else:
                            row.append(f"{cl.val:g}")
                    else:
                        row.append("")
                writer.writerow(row)
    except OSError:
        return -1
    return 0
csvload
csvload(filename: str) -> int

Import cells from a CSV file. Numbers become NUM cells, rest become LABELs.

Source code in src/gridcalc/engine.py
def csvload(self, filename: str) -> int:
    """Import cells from a CSV file. Numbers become NUM cells, rest become LABELs."""
    try:
        with open(filename, newline="") as f:
            content = f.read()
    except OSError:
        return -1

    reader = csv.reader(StringIO(content))
    for r_idx, row in enumerate(reader):
        if r_idx >= NROW:
            break
        for c_idx, val in enumerate(row):
            if c_idx >= NCOL:
                break
            val = val.strip()
            if not val:
                continue
            self.setcell(c_idx, r_idx, val)
    return 0
pdload
pdload(filename: str, header: bool = True) -> int

Load a file into grid cells using pandas for type inference.

Supports CSV, TSV, Excel (.xlsx/.xls), JSON, and Parquet. Column headers become labels in row 0 when header=True.

Source code in src/gridcalc/engine.py
def pdload(self, filename: str, header: bool = True) -> int:
    """Load a file into grid cells using pandas for type inference.

    Supports CSV, TSV, Excel (.xlsx/.xls), JSON, and Parquet.
    Column headers become labels in row 0 when header=True.
    """
    import pandas as pd  # noqa: I001

    ext = filename.rsplit(".", 1)[-1].lower() if "." in filename else ""
    pd_header: int | None = 0 if header else None
    try:
        if ext in ("xlsx", "xls"):
            df = pd.read_excel(filename, header=pd_header)
        elif ext == "parquet":
            df = pd.read_parquet(filename)
        elif ext == "json":
            df = pd.read_json(filename)
        elif ext in ("tsv", "tab"):
            df = pd.read_csv(filename, sep="\t", header=pd_header)
        else:
            df = pd.read_csv(filename, header=pd_header)
    except Exception:
        return -1

    if df.shape[0] >= NROW or df.shape[1] >= NCOL:
        # Truncate to grid limits
        df = df.iloc[: NROW - (1 if header else 0), :NCOL]

    r_offset = 0
    if header:
        for c_idx, col_name_str in enumerate(df.columns):
            if c_idx >= NCOL:
                break
            self.setcell(c_idx, 0, str(col_name_str))
        r_offset = 1

    for r_idx in range(len(df)):
        if r_idx + r_offset >= NROW:
            break
        for c_idx in range(len(df.columns)):
            if c_idx >= NCOL:
                break
            val = df.iloc[r_idx, c_idx]
            if pd.isna(val):
                continue
            if isinstance(val, (int, float)):
                if isinstance(val, int) or (
                    isinstance(val, float) and val == int(val) and abs(val) < 1e15
                ):
                    self.setcell(c_idx, r_idx + r_offset, str(int(val)))
                else:
                    self.setcell(c_idx, r_idx + r_offset, f"{val:g}")
            else:
                self.setcell(c_idx, r_idx + r_offset, str(val))
    return 0
pdsave
pdsave(filename: str) -> int

Export grid cells to a file using pandas.

Supports CSV, TSV, Excel (.xlsx), JSON, and Parquet. Row 0 is used as column headers.

Source code in src/gridcalc/engine.py
def pdsave(self, filename: str) -> int:
    """Export grid cells to a file using pandas.

    Supports CSV, TSV, Excel (.xlsx), JSON, and Parquet.
    Row 0 is used as column headers.
    """
    import pandas as pd  # noqa: I001

    maxr = -1
    maxc = -1
    for (c, r), sc in self._cells.items():
        if sc.type != EMPTY:
            if r > maxr:
                maxr = r
            if c > maxc:
                maxc = c

    if maxr < 0:
        return -1

    # Build column headers from row 0
    columns: list[str] = []
    for c in range(maxc + 1):
        cl = self._cells.get((c, 0))
        if cl and cl.type != EMPTY:
            columns.append(cl.text if cl.type == LABEL else str(cl.val))
        else:
            columns.append(col_name(c))

    # Build data from row 1 onward
    data: list[list[Any]] = []
    for r in range(1, maxr + 1):
        row: list[Any] = []
        for c in range(maxc + 1):
            cl = self._cells.get((c, r))
            if not cl or cl.type == EMPTY:
                row.append(None)
            elif cl.type == LABEL:
                row.append(cl.text)
            elif cl.type in (NUM, FORMULA):
                if isinstance(cl.val, float) and math.isnan(cl.val):
                    row.append(None)
                else:
                    row.append(cl.val)
            else:
                row.append(None)
        data.append(row)

    df = pd.DataFrame(data, columns=columns)
    ext = filename.rsplit(".", 1)[-1].lower() if "." in filename else ""
    try:
        if ext in ("xlsx", "xls"):
            df.to_excel(filename, index=False)
        elif ext == "parquet":
            df.to_parquet(filename, index=False)
        elif ext == "json":
            df.to_json(filename, orient="records", indent=2)
        elif ext in ("tsv", "tab"):
            df.to_csv(filename, sep="\t", index=False)
        else:
            df.to_csv(filename, index=False)
    except Exception:
        return -1
    return 0

Sheet

Sheet(name: str = 'Sheet1')

A single named sheet's cell store, cycle set, and cursor.

Workbook-level state (mode, code, named ranges, dep graph, etc.) lives on Grid; each Sheet only owns the data that varies per-tab. Grid exposes _cells / cells / cc / cr / _circular as properties that delegate to sheets[active] so existing single-sheet code keeps working unchanged.

Source code in src/gridcalc/engine.py
def __init__(self, name: str = "Sheet1") -> None:
    self.name: str = name
    self._cells: dict[tuple[int, int], Cell] = {}
    self._circular: set[tuple[int, int]] = set()
    self.cc: int = 0
    self.cr: int = 0
    # Per-column display widths in pixels, keyed by column index; absent
    # columns use the frontend's default. A *pixel* map is deliberately
    # not the same thing as `Grid.cw`, which is a uniform width in
    # character cells: the curses renderer lays columns out by multiplying
    # that one number and has no notion of a per-column size, so it
    # ignores this. Written by the web view, which does, and carried
    # through save/load so a resize survives the session.
    self.widths: dict[int, int] = {}

Cell

Cell()
Source code in src/gridcalc/engine.py
def __init__(self) -> None:
    self.type: int = EMPTY
    self.val: float = 0.0
    self.sval: str | None = None
    self.arr: list[float] | None = None
    # When arr holds a 2D Vec result, arr_cols is the column count.
    # None means 1D (or no array).
    self.arr_cols: int | None = None
    self.matrix: Any = None
    # Spill bookkeeping. `spill_parent` is the (col, row) anchor of a
    # SPILL cell; None for every other cell. `spill_shape` is the
    # (rows, cols) rectangle a spilling anchor currently occupies;
    # None when the cell is not an anchor or is not spilling.
    self.spill_parent: tuple[int, int] | None = None
    self.spill_shape: tuple[int, int] | None = None
    self.text: str = ""
    self.fmt: str = ""
    self.bold: int = 0
    self.underline: int = 0
    self.italic: int = 0
    self.fmtstr: str = ""
    self.ast: Any = None
    self.ast_text: str = ""
    self.err: Any = None
    self.err_msg: str | None = None

Vec

Vec(data: Iterable[Any], cols: int | None = None)
Source code in src/gridcalc/engine.py
def __init__(self, data: Iterable[Any], cols: int | None = None) -> None:
    self.data: list[Any] = list(data)
    # Number of columns when this Vec materialises a 2D range (row-major).
    # None means shape is unknown / treat as 1D. Set by _eval_range when
    # building from a RangeRef so INDEX(rng, row, col) can re-index.
    self.cols: int | None = cols
rows property
rows: int

Row count. For 1D Vecs this is the flat length.

shape property
shape: tuple[int, int]

(rows, cols). 1D Vecs report (len, 1).

at
at(r: int, c: int) -> Any

1-based 2D access. Treats a 1D Vec as a column vector (n×1) so at(i, 1) walks the flat data.

Source code in src/gridcalc/engine.py
def at(self, r: int, c: int) -> Any:
    """1-based 2D access. Treats a 1D Vec as a column vector (n×1)
    so ``at(i, 1)`` walks the flat data."""
    rows, cols = self.shape
    if not 1 <= r <= rows or not 1 <= c <= cols:
        raise IndexError(f"Vec.at({r},{c}) out of range for shape {self.shape}")
    if not self.is_2d:
        return self.data[r - 1]
    assert self.cols is not None  # noqa: S101 -- type-narrowing after is_2d guard
    return self.data[(r - 1) * self.cols + (c - 1)]
row
row(i: int) -> Vec

1-based row extraction. Returns a 1D Vec.

A 1D Vec is treated as a column vector (n×1), so row(i) returns a 1-element Vec for valid i.

Source code in src/gridcalc/engine.py
def row(self, i: int) -> Vec:
    """1-based row extraction. Returns a 1D Vec.

    A 1D Vec is treated as a column vector (n×1), so ``row(i)``
    returns a 1-element Vec for valid ``i``.
    """
    rows, _ = self.shape
    if not 1 <= i <= rows:
        raise IndexError(f"Vec.row({i}) out of range for shape {self.shape}")
    if not self.is_2d:
        return Vec([self.data[i - 1]])
    assert self.cols is not None  # noqa: S101 -- type-narrowing after is_2d guard
    start = (i - 1) * self.cols
    return Vec(self.data[start : start + self.cols])
col
col(j: int) -> Vec

1-based column extraction. Returns a 1D Vec.

A 1D Vec is treated as a column vector (n×1), so col(1) returns the whole vec; other indices raise.

Source code in src/gridcalc/engine.py
def col(self, j: int) -> Vec:
    """1-based column extraction. Returns a 1D Vec.

    A 1D Vec is treated as a column vector (n×1), so ``col(1)``
    returns the whole vec; other indices raise.
    """
    _, cols = self.shape
    if not 1 <= j <= cols:
        raise IndexError(f"Vec.col({j}) out of range for shape {self.shape}")
    if not self.is_2d:
        return Vec(list(self.data))
    assert self.cols is not None  # noqa: S101 -- type-narrowing after is_2d guard
    return Vec([self.data[i * self.cols + (j - 1)] for i in range(self.rows)])
iter_rows
iter_rows() -> Iterator[list[Any]]

Iterate rows as plain lists. A 1D Vec is treated as column-shaped (n×1), so each element yields its own 1-element row.

Source code in src/gridcalc/engine.py
def iter_rows(self) -> Iterator[list[Any]]:
    """Iterate rows as plain ``list``s. A 1D Vec is treated as
    column-shaped (n×1), so each element yields its own 1-element row."""
    if not self.is_2d:
        for v in self.data:
            yield [v]
        return
    assert self.cols is not None  # noqa: S101 -- type-narrowing after is_2d guard
    for i in range(self.rows):
        yield list(self.data[i * self.cols : (i + 1) * self.cols])

NamedRange

NamedRange(
    name: str = "",
    c1: int = 0,
    r1: int = 0,
    c2: int = 0,
    r2: int = 0,
    sheet: str | None = None,
)
Source code in src/gridcalc/engine.py
def __init__(
    self,
    name: str = "",
    c1: int = 0,
    r1: int = 0,
    c2: int = 0,
    r2: int = 0,
    sheet: str | None = None,
) -> None:
    self.name = name
    self.c1 = c1
    self.r1 = r1
    self.c2 = c2
    self.r2 = r2
    # Sheet the range lives on, or None for a sheet-agnostic name that
    # resolves against whichever sheet the referencing formula is on
    # (the historical gridcalc behaviour; xlsx imports set it explicitly).
    self.sheet = sheet

ref

ref(s: str) -> tuple[int, int, int] | None

Parse a cell reference. Returns (chars_consumed, col, row) or None.

Source code in src/gridcalc/engine.py
def ref(s: str) -> tuple[int, int, int] | None:
    """Parse a cell reference. Returns (chars_consumed, col, row) or None."""
    result = refabs(s)
    if result is None:
        return None
    n, col, row, _, _ = result
    return (n, col, row)

refabs

refabs(s: str) -> RefMatch | None

Parse a cell reference at the start of s.

Returns a RefMatch (still tuple-unpackable as n, col, row, abs_col, abs_row), or None if no ref matches.

Source code in src/gridcalc/engine.py
def refabs(s: str) -> RefMatch | None:
    """Parse a cell reference at the start of `s`.

    Returns a `RefMatch` (still tuple-unpackable as
    `n, col, row, abs_col, abs_row`), or None if no ref matches.
    """
    m = _REF_RE.match(s)
    if not m:
        return None
    absc = 1 if m.group(1) == "$" else 0
    letters = m.group(2).upper()
    absr = 1 if m.group(3) == "$" else 0
    rownum = int(m.group(4))
    if rownum <= 0:
        return None
    col = 0
    for ch in letters:
        col = col * 26 + (ord(ch) - ord("A") + 1)
    col -= 1
    row = rownum - 1
    return RefMatch(m.end(), col, row, absc, absr)

col_name

col_name(c: int) -> str
Source code in src/gridcalc/engine.py
def col_name(c: int) -> str:
    if c < 26:
        return chr(ord("A") + c)
    return chr(ord("A") + c // 26 - 1) + chr(ord("A") + c % 26)

cellname

cellname(c: int, r: int) -> str
Source code in src/gridcalc/engine.py
def cellname(c: int, r: int) -> str:
    return f"{col_name(c)}{r + 1}"

adjust_refs

adjust_refs(text: str, dcol: int, drow: int) -> str

Shift every relative cell reference in text by (dcol, drow).

Absolute ($-prefixed) columns/rows are left unchanged. Used both by replicate (copy a formula across the grid) and by the frontends' paste, so the two share one definition of reference adjustment.

Source code in src/gridcalc/engine.py
def adjust_refs(text: str, dcol: int, drow: int) -> str:
    """Shift every relative cell reference in ``text`` by ``(dcol, drow)``.

    Absolute (``$``-prefixed) columns/rows are left unchanged. Used both by
    replicate (copy a formula across the grid) and by the frontends' paste, so
    the two share one definition of reference adjustment.
    """
    out = []
    i = 0
    while i < len(text):
        end = _skip_quoted(text, i)
        if end is not None:
            out.append(text[i:end])  # a literal is copied through untouched
            i = end
            continue
        result = refabs(text[i:])
        if result:
            n, rc, rr, ac, ar = result
            if not ac:
                rc += dcol
            if not ar:
                rr += drow
            out.append(_emitref(rc, rr, ac, ar))
            i += n
        else:
            out.append(text[i])
            i += 1
    return "".join(out)