Source code for greater_tables.engine.structure

"""Table structure: row break depths, stub rowspans, head colspans.

The single implementation of the hierarchy logic (greater_tables derived it
independently per backend). Everything here is pure functions over plain
Python sequences — no pandas mutation, no rendering concerns.
"""

from __future__ import annotations

from typing import Hashable, Sequence

__all__ = ['break_depths', 'column_break_depths', 'head_groups', 'stub_layout']


[docs] def break_depths(tuples: Sequence[tuple]) -> list[int | None]: """Per row, the shallowest level whose value changed vs. the row above. Row 0 is None (top edge is structural). A row identical to its predecessor is also None — no boundary at all. """ out: list[int | None] = [None] for prev, cur in zip(tuples, tuples[1:]): depth = None for level, (a, b) in enumerate(zip(prev, cur)): if a != b: depth = level break out.append(depth) return out
[docs] def stub_layout(depths: Sequence[int | None], n_levels: int, sparsify: bool) -> list[dict[int, int]]: """Which stub cells each row emits, and with what rowspan. Returns one dict per row mapping stub level → rowspan for the levels that start a run at that row (HTML ``<td rowspan>`` semantics: covered rows omit the cell entirely). Without sparsify every row emits every level with span 1. """ n_rows = len(depths) if not sparsify: return [{lvl: 1 for lvl in range(n_levels)} for _ in range(n_rows)] out: list[dict[int, int]] = [] for i in range(n_rows): cells: dict[int, int] = {} for level in range(n_levels): starts = i == 0 or (depths[i] is not None and depths[i] <= level) if not starts: continue span = 1 for j in range(i + 1, n_rows): if depths[j] is not None and depths[j] <= level: break span += 1 cells[level] = span out.append(cells) return out
[docs] def head_groups(names: Sequence[tuple], level: int, merge: bool) -> list[tuple[str, int]]: """Header cells for one level: (text, colspan) runs over data columns. Adjacent columns group only while their *full prefix* through ``level`` matches — the same leaf label under different parents never merges. The deepest level is never merged (distinct columns stay distinct); callers pass ``merge=False`` there and when sparsify_columns is off. """ out: list[tuple[str, int]] = [] last_prefix: tuple | None = None for name in names: text = str(name[level]) if level < len(name) else '' prefix = name[:level + 1] if merge and out and prefix == last_prefix: out[-1] = (out[-1][0], out[-1][1] + 1) else: out.append((text, 1)) last_prefix = prefix return out
[docs] def column_break_depths(names: Sequence[tuple]) -> dict[int, int]: """Data-column positions carrying a column-group boundary at their left edge. Depth = shallowest head level that changes between column j-1 and j. Only genuine *group* boundaries are reported (depth ≤ n_levels − 2); leaf-level changes are just "next column". """ out: dict[int, int] = {} if not names: return out n_levels = max(len(n) for n in names) for j in range(1, len(names)): prev, cur = names[j - 1], names[j] for level in range(min(len(prev), len(cur))): if prev[level] != cur[level]: if level <= n_levels - 2: out[j] = level break return out