pub struct Workbook { /* private fields */ }Expand description
An engine-locked spreadsheet workbook — a pure value object (no hidden state, no callbacks). Schema spec §2.
All five document fields are always serialized, even when empty. Field
declaration order (engine, names, sheets, tables, version)
matches canonical (JCS) key order.
graph_cache, spill_anchor_cache and authored_cell_index_cache are not
part of the document: they are derived state the workbook memoizes across
recalculations (see the graph_cache, spill_anchor_cache and
authored_cell_index_cache module docs). pre_image_stats is likewise
not document content — it is instrumentation for the last incremental
recalc’s own bookkeeping (see the pre_image_stats module docs). All four
are skipped by serde, ignored by PartialEq, and contribute nothing to
Hash, so the value object is exactly what it was before any of them
existed.
Implementations§
Source§impl Workbook
impl Workbook
Sourcepub fn set(
&mut self,
sheet: &str,
addr: Address,
input: CellInput,
) -> Result<Option<Cell>, WorkbookError>
pub fn set( &mut self, sheet: &str, addr: Address, input: CellInput, ) -> Result<Option<Cell>, WorkbookError>
Writes input at addr on the sheet named sheet (case-insensitive),
returning the cell previously there (if any).
A literal is stored as a value-only cell; a formula is stored verbatim
(with its leading =) carrying a Value::Empty result until the next
recalc — set validates only the formula’s syntax against the
workbook’s locked engine, never evaluating it (recalc is P3.3).
Enforces, eagerly, the per-mutation caps of scope ADR Decision 5: a
formula’s length, a text/array value’s size, and — when the write
introduces a new populated cell — the per-workbook cell count.
Errors if the sheet does not exist, the input is an empty literal (schema spec §4 — clear instead), the formula is syntactically invalid for the locked engine, or any cap would be exceeded.
Auto-expand-by-append (structured-references spec §4,
truecalc/core#861): if the written address lands exactly one row
below a table’s current range, within that table’s column span, the
matching table’s ref is extended by one row — unless doing so would
overlap another table’s range, in which case the expansion is
silently skipped and the write still succeeds as an ordinary cell
write. At most one table can match, since table ranges never overlap
(§4) and this only looks at the row immediately adjacent to a table,
not inside any range.
Sourcepub fn get(&self, sheet: &str, addr: Address) -> Option<&Cell>
pub fn get(&self, sheet: &str, addr: Address) -> Option<&Cell>
The authored cell at addr on the sheet named sheet
(case-insensitive), or None if no cell is authored there.
This returns only authored cells — a literal or a formula physically
present in cells. A spilled cell (one materialized by a spill anchor,
schema spec §5) is not authored and has no Cell to borrow, so
get returns None for it; use resolved to read the
effective value at any address (authored or spilled) and learn the
spill anchor. Keeping get authored-only preserves the structural
distinguishability rule of §5 (a cell is authored iff it has an entry).
Sourcepub fn resolved(&self, sheet: &str, addr: Address) -> Option<Resolved>
pub fn resolved(&self, sheet: &str, addr: Address) -> Option<Resolved>
The effective value at addr on the sheet named sheet
(case-insensitive), resolving through array spills (schema spec §5).
Returns None only if addr is genuinely empty — neither authored nor
covered by a spill. Otherwise the returned Resolved carries the
value and, for a spilled cell, the anchor it spilled from (the
runtime spilledFrom view of §5 — never serialized). For an authored
cell anchor is None. A blocked-spill anchor is just an authored
formula cell whose value is the blocked-spill error, so it resolves as
an ordinary authored cell with no anchor.
Resolution reads the stored grid (the last recalc’s results): a spilling anchor stores its full array (§6), and this reconstructs the spilled element by the five-line rule of §5. It does not recalc.
Sourcepub fn spill_anchor(&self, sheet: &str, addr: Address) -> Option<Address>
pub fn spill_anchor(&self, sheet: &str, addr: Address) -> Option<Address>
The spill anchor that materializes addr on the sheet named sheet
(case-insensitive), or None if addr is authored or empty (schema
spec §5). Convenience over resolved when only the
anchor identity (the spilledFrom view) is needed.
Sourcepub fn clear(&mut self, sheet: &str, addr: Address) -> Option<Cell>
pub fn clear(&mut self, sheet: &str, addr: Address) -> Option<Cell>
Removes the cell at addr on the sheet named sheet (case-insensitive),
returning it if present. Clearing is removing the entry, never writing
an empty value (schema spec §4). Returns None if the sheet or cell is
absent.
Sourcepub fn total_cells(&self) -> usize
pub fn total_cells(&self) -> usize
The total number of populated cells across every sheet — the quantity the per-workbook cell cap (scope ADR Decision 5) bounds.
Sourcepub fn define_name(
&mut self,
name: &str,
r: &str,
) -> Result<&NamedRange, WorkbookError>
pub fn define_name( &mut self, name: &str, r: &str, ) -> Result<&NamedRange, WorkbookError>
Defines a new workbook-scoped named range name pointing at the
canonical reference r (Sheet!A1 / Sheet!A1:B2), returning the
stored NamedRange.
Validates everything from_json checks for a name (schema spec §7): the
name’s shape, the ref’s canonical form, that the target sheet exists
(no dangling ref), that the name does not already exist
(case-insensitively) as either a named range or a table
(structured-references spec §4), and that the named-range cap
(Decision 5) is not exceeded. To replace an existing name use
redefine_name.
Sourcepub fn redefine_name(
&mut self,
name: &str,
r: &str,
) -> Result<&NamedRange, WorkbookError>
pub fn redefine_name( &mut self, name: &str, r: &str, ) -> Result<&NamedRange, WorkbookError>
Redefines the existing named range name (case-insensitive) to point at
the canonical reference r, returning the updated NamedRange. The
name’s identity and original casing are preserved; only the ref
changes.
Validates the ref exactly as define_name does,
including that the target sheet exists (no dangling ref). Errors if no
name currently matches name (case-insensitively) or if the ref is
not a valid canonical reference to an existing sheet.
Sourcepub fn remove_name(&mut self, name: &str) -> Option<NamedRange>
pub fn remove_name(&mut self, name: &str) -> Option<NamedRange>
Removes the named range name (case-insensitive), returning it if it
existed, or None otherwise.
Sourcepub fn name(&self, name: &str) -> Option<&NamedRange>
pub fn name(&self, name: &str) -> Option<&NamedRange>
The named range called name (case-insensitive), or None. Listing is
names.
Sourcepub fn define_table(
&mut self,
name: &str,
r: &str,
) -> Result<&Table, WorkbookError>
pub fn define_table( &mut self, name: &str, r: &str, ) -> Result<&Table, WorkbookError>
Defines a new workbook-scoped table name over the canonical range
r (Sheet!A1:B2 — a table ref is always a range, never the
single-cell form), returning the stored Table.
Validates the name’s shape, that r is a canonical range referencing
an existing sheet (no dangling ref), that the name does not already
collide with an existing table or named range (case-insensitively),
that the range does not overlap an existing table’s range, and the
table count cap (Decision 5). Unlike Workbook::from_json, this
does not validate the header row’s column names — a table may
legitimately be defined ahead of its header cells being written
(define the shape first, fill the headers in later). A table defined
over a headerless or malformed-header region therefore succeeds here,
but the workbook will fail to reload (from_json’s load-time
validation does check header content) if serialized before real
header text is written at the range’s first row
(structured-references spec §4). To replace an existing table’s
range use redefine_table.
Sourcepub fn redefine_table(
&mut self,
name: &str,
r: &str,
) -> Result<&Table, WorkbookError>
pub fn redefine_table( &mut self, name: &str, r: &str, ) -> Result<&Table, WorkbookError>
Redefines the existing table name (case-insensitive) to point at
the canonical range r, returning the updated Table. The name’s
identity and original casing are preserved; only the ref changes.
Validates the ref exactly as define_table
does, including that the target sheet exists (no dangling ref) and
that the new range does not overlap another table’s range. Errors if
no table currently matches name (case-insensitively) or if the
ref is not a valid canonical range to an existing sheet.
Sourcepub fn remove_table(&mut self, name: &str) -> Option<Table>
pub fn remove_table(&mut self, name: &str) -> Option<Table>
Removes the table name (case-insensitive), returning it if it
existed, or None otherwise.
Source§impl Workbook
impl Workbook
Sourcepub fn recalc(&mut self, ctx: &RecalcContext) -> Vec<Change>
pub fn recalc(&mut self, ctx: &RecalcContext) -> Vec<Change>
Recomputes every formula cell in dependency order against ctx,
writing each new result back into the grid and returning the ordered
list of cells whose value changed.
Formula cells are evaluated in topological order (precedents first), so
each reads its inputs already current. Cells on a dependency cycle —
and any cell that cannot be ordered because it (transitively) reads one
— take the circular-dependency error (CIRCULAR_ERROR); recalc always
terminates. Volatile functions are pinned by ctx (scope ADR
Decision 3).
Changes are returned sorted by (sheet tab index, row, column).
Sourcepub fn recalc_incremental(
&mut self,
ctx: &RecalcContext,
edited: &[(String, Address)],
) -> Vec<Change>
pub fn recalc_incremental( &mut self, ctx: &RecalcContext, edited: &[(String, Address)], ) -> Vec<Change>
Recomputes only the formula cells affected by an edit and returns the ordered changes.
edited lists the cells a mutation touched (the cell written, or — for
a named-range retarget — the name’s old and new target cells; callers
pass whatever changed). The recalc closure is the transitive
direct_dependents of those
cells, plus every volatile formula cell (always dirty, scope ADR
Decision 3). Everything outside the closure keeps its stored result.
The result is identical to the subset of recalc’s
output for the same edits — the incremental ≡ full guarantee.
Sourcepub fn trace_cell(
&self,
sheet: &str,
addr: Address,
ctx: &RecalcContext,
hook: &mut dyn EvalHook,
) -> Value
pub fn trace_cell( &self, sheet: &str, addr: Address, ctx: &RecalcContext, hook: &mut dyn EvalHook, ) -> Value
Explains one cell’s value against the currently stored grid (issue
#743): evaluates addr’s formula once through hook, resolving every
precedent read to its stored value (the same grid-backed
Resolver semantics recalc uses), and returns the value — provably
the same value recalc/recalc_incremental would write for this cell,
provided the grid is already current for its precedents.
This is a point-in-time explain, not a recalc: unlike
Workbook::recalc, trace_cell does not recompute anything
transitively — a precedent’s value is whatever is already on the grid
(or, for a cell inside another anchor’s placed spill, the
reconstructed spilled element — schema spec §5). If the grid is stale
relative to unapplied edits, trace_cell faithfully explains the
stale value; call recalc or recalc_incremental first if the
caller needs a fresh grid.
Two pieces of recalc’s behavior can’t be reproduced from the target
cell in isolation, so trace_cell matches them explicitly rather than
diverging (an on-demand, single-cell call — a user clicking a cell —
can afford this; see the two call sites below):
- Spill occupancy (schema spec §5): an array result is only stored
if its target rectangle is free on the current grid; otherwise
recalcstoresBLOCKED_SPILL_ERRORinstead, exactly likeWorkbook::place_spillapplies for a real recompute. - Dependency cycles:
recalcnever evaluates a cycle member’s formula at all — it short-circuits straight toCIRCULAR_ERROR(seeDependencyGraph::cycle_cellsandrecompute). Evaluating the formula anyway would diverge whenever it catches the error (e.g.IFERROR), since its precedents’ stored values already carry the propagated error butrecalcnever gave the formula the chance to run.
addr need not be a formula cell: a literal (or empty, or spilled
non-anchor) cell has no expression to trace, so this returns its
resolved value directly without invoking hook — hook observes no
events in that case, by design (there is nothing to walk). Passing a
hook is optional in the sense that evaluating with hook = None’s
counterpart, Engine::evaluate_with_resolver_at_keyed, produces this
same value: trace_cell adds observation, it does not change what gets
computed.
Source§impl Workbook
impl Workbook
Sourcepub fn new(engine: EngineFlavor) -> Self
pub fn new(engine: EngineFlavor) -> Self
Creates an empty workbook locked to engine.
The engine flavor is required at creation and immutable for the workbook’s lifetime (ADR 2026-04-27-engine-flavor-explicit-everywhere): there is no default and no setter.
Sourcepub fn engine(&self) -> EngineFlavor
pub fn engine(&self) -> EngineFlavor
The engine flavor every formula in this workbook targets.
Sourcepub fn sheets(&self) -> &[Worksheet]
pub fn sheets(&self) -> &[Worksheet]
The worksheets, in tab order (array position is tab position).
Sourcepub fn sheets_mut(&mut self) -> &mut Vec<Worksheet>
pub fn sheets_mut(&mut self) -> &mut Vec<Worksheet>
Mutable access to the worksheets.
Invalidates the dependency-graph cache on the borrow: what the caller
does with a &mut Vec<Worksheet> is unobservable from here, so the
only sound assumption is that it changed the graph. Invalidates the
spill-anchor cache for the same reason: an unobserved write can add or
remove an array-valued cell just as easily as it can add or remove a
formula. Invalidates the authored-cell-index cache for the same reason
again: an unobserved write can add or remove an authored cell just as
easily.
Sourcepub fn names(&self) -> &[NamedRange]
pub fn names(&self) -> &[NamedRange]
The workbook-scoped named ranges.
Sourcepub fn names_mut(&mut self) -> &mut Vec<NamedRange>
pub fn names_mut(&mut self) -> &mut Vec<NamedRange>
Mutable access to the named ranges.
Invalidates the dependency-graph cache on the borrow (see
sheets_mut).
Sourcepub fn tables_mut(&mut self) -> &mut Vec<Table>
pub fn tables_mut(&mut self) -> &mut Vec<Table>
Mutable access to the table declarations.
Invalidates the dependency-graph cache on the borrow (see
sheets_mut).
Sourcepub fn sheet(&self, name: &str) -> Option<&Worksheet>
pub fn sheet(&self, name: &str) -> Option<&Worksheet>
The worksheet named name (case-insensitive, simple case folding per
schema spec §2), or None if no sheet matches.
Sourcepub fn sheet_mut(&mut self, name: &str) -> Option<&mut Worksheet>
pub fn sheet_mut(&mut self, name: &str) -> Option<&mut Worksheet>
Mutable access to the worksheet named name (case-insensitive).
Invalidates the dependency-graph cache, the spill-anchor cache and the
authored-cell-index cache on the borrow (see
sheets_mut).
Sourcepub fn sheet_index(&self, name: &str) -> Option<usize>
pub fn sheet_index(&self, name: &str) -> Option<usize>
The tab position (0-based array index) of the sheet named name
(case-insensitive per schema spec §2), or None if no sheet matches.
Sourcepub fn add_sheet(&mut self, sheet: Worksheet) -> Result<usize, WorkbookError>
pub fn add_sheet(&mut self, sheet: Worksheet) -> Result<usize, WorkbookError>
Appends sheet after the last tab and returns its 0-based position.
Errors if the name collides with an existing sheet under simple case folding (schema spec §2), is empty or too long (schema spec §3), or would exceed the per-workbook sheet cap (scope ADR Decision 5).
Sourcepub fn insert_sheet(
&mut self,
index: usize,
sheet: Worksheet,
) -> Result<(), WorkbookError>
pub fn insert_sheet( &mut self, index: usize, sheet: Worksheet, ) -> Result<(), WorkbookError>
Inserts sheet at tab position index, shifting later tabs right.
index == sheets().len() appends. Position semantics: array index is
tab position (schema spec §2 — order is significant).
Errors on a duplicate name (case-insensitive, §2), an empty/too-long
name (§3), the sheet cap (Decision 5), or index out of 0..=len.
Sourcepub fn remove_sheet(&mut self, name: &str) -> Option<Worksheet>
pub fn remove_sheet(&mut self, name: &str) -> Option<Worksheet>
Removes and returns the sheet named name (case-insensitive),
shifting later tabs left, or None if no sheet matches.
A workbook-scoped named range or table may now dangle to the removed
sheet. The dangling-ref invariant is re-checked at
to_json and from_json (schema
spec §7) — including, since issue #969, at to_json, which the earlier
wording claimed but the code did not do. A workbook left holding a
dangling ref therefore fails to save, rather than saving cleanly
and failing at some later load.
Removal deliberately does not tidy up for you. It returns
Option<Worksheet> and so has no channel to report what it discarded,
and dropping a name or table the caller still wants is a silent loss
they cannot detect; refusing the save names the offending range
instead. Drop the refs you no longer want with
remove_name / remove_table,
or repoint them with redefine_name /
redefine_table.
Sourcepub fn rename_sheet(
&mut self,
from: &str,
to: &str,
) -> Result<(), WorkbookError>
pub fn rename_sheet( &mut self, from: &str, to: &str, ) -> Result<(), WorkbookError>
Renames the sheet currently named from (case-insensitive) to to,
repointing everything in the document that named the old sheet.
A rename is holistic: the workbook owns the dangling-ref invariant across it (issue #969). Three things move together —
- the sheet’s own name;
- every
NamedRangeandTablerefwhose sheet token resolves to this sheet (case-insensitively, §2). The A1 part is untouched and the new sheet token is re-emitted in canonical quoting, so arefthat was canonical stays canonical (§7); - every formula that qualifies a cell/range reference with the old
name, rewritten via
Engine::rename_sheet_refs: unqualified refs, refs to other sheets, string literals, function names and defined names are left alone. A formula that does not parse has no references to rewrite and is left verbatim — formula text carries no document invariant, andfrom_jsondoes not validate it either.
§Errors
Beyond the name rules — from does not exist, to is empty or too
long (§3), to collides with a different sheet (§2) — a rename is
refused when the rewrite itself would produce a document
from_json rejects. That is the whole point of the
operation, so it errors rather than writing one:
- a rewritten formula longer than the formula cap (Decision 5). A sheet name may be 100 scalar values, so a rename can multiply a formula’s length; the check mirrors the one the rest of the mutation API applies at the point of change;
- a repointed
Tablelanding on a range another table already occupies (structured-references spec §4). Reachable because a table may legitimately be left dangling byremove_sheet, and a later rename can move a live table onto it; - a table that was dangling at
tocoming alive on this sheet over a header row that is not a valid table header (§4). A table that moves with the sheet keeps reading the cells it always read, sodefine_table’s deliberate “declare the shape now, write the headers later” allowance is untouched; a table that adopts a sheet nobody chose for it is a different thing, andfrom_jsonchecks those column names.
Every error is decided before anything is written, so a rejected rename leaves the document exactly as it was.
A pure case change of the same sheet is allowed (it does not collide with itself) and repoints refs and formulas to the new casing.
§The one case-folding asymmetry
Sheet identity in this crate is Unicode simple case folding, and
the ref rewrite above uses it. Engine::rename_sheet_refs matches a
formula’s sheet qualifier with str::to_uppercase() instead
(truecalc-core does not depend on icu_casemap). The two agree on
every name whose characters case-map one-to-one, and disagree where they
do not: simple_fold("ß") == "ß", so Maß and MASS are different
sheets under §2 and both may exist, while to_uppercase collapses them.
The consequence is sharper than “a formula is left unrewritten”.
Renaming MASS re-points a formula’s Maß!A1 qualifier at the new
name, so a formula can be silently moved onto a different, still
existing sheet and quietly compute different numbers, while a
NamedRange spelled 'Maß'!A1 is correctly left alone — refs and
formulas end up disagreeing about the same rename. fi (U+FB01) versus
fi is the same shape. It cannot break the §7 dangling-ref rule — refs
use simple folding and are exact — and an over-long rewrite it causes is
refused by the formula-cap check above rather than written, so it does
not produce an unloadable document. It is simply wrong, and the fix
belongs in the matcher rather than here —
see the test divergent_case_folding_repoints_a_formula_at_another_sheet,
which pins the current behaviour so it is not rediscovered as a mystery.
Sourcepub fn move_sheet(
&mut self,
from: usize,
to: usize,
) -> Result<(), WorkbookError>
pub fn move_sheet( &mut self, from: usize, to: usize, ) -> Result<(), WorkbookError>
Moves the sheet at tab position from to position to, shifting the
sheets in between (schema spec §2 — array position is tab position).
Errors if either index is out of 0..len.
Sourcepub fn cached_graph_entry(&self) -> Option<Arc<CachedGraph>>
pub fn cached_graph_entry(&self) -> Option<Arc<CachedGraph>>
The cached dependency graph and evaluation order, if the cache is warm.
Warm means “equal to a build against the workbook as it is now” — see
the graph_cache module docs for the invalidation contract that
maintains it. pub, not pub(crate), so a read-only, host-facing
graph query that only has &Workbook to work with (the wasm
precedentsOf/dependentsOf binding) can reuse a warm cache instead
of building its own copy — the same constraint
trace_cell documents for itself: it can read a
warm entry but, taking &self, cannot populate a cold one.
Sourcepub fn drop_derived_state(&mut self)
pub fn drop_derived_state(&mut self)
Releases every cached derived-state entry the workbook holds — today
the dependency graph (reclaiming the ~545 B/cell (wasm32) / ~856 B/cell
(native) it retains for every formula cell — see the limits module
docs for the multi-workbook arithmetic this exists for), the
spill-anchor-rectangle map, and the authored-cell index.
The workbook itself is unchanged: the next recalc / recalc_incremental
/ explain call simply rebuilds whatever it needs, exactly as it would
after a mutation the owning cache’s module invalidates on (graph_builds
/ anchor_builds / authored_index_builds ticks up by one).
Named for what it releases, not for the mechanism, and kept apart from
invalidate_graph_cache /
invalidate_anchor_cache /
invalidate_authored_index_cache
(all pub(crate)) on purpose: those are this crate’s word for “a
mutation made the entry stale, it must rebuild before next use” — an
internal correctness call the workbook makes about itself. This is a
different call: a still-valid cache the host chooses to give back
for its memory. The name says what a caller gets (memory back), not
how, so it keeps meaning “every derived cache” as more join it.
Sourcepub fn from_json(bytes: &[u8]) -> Result<Self, WorkbookError>
pub fn from_json(bytes: &[u8]) -> Result<Self, WorkbookError>
Parses a workbook from JSON bytes, enforcing every document-level rule of the schema (schema spec §1–§10) and the resource limits of the scope ADR (Decision 5).
Accepts any schema-valid JSON — pretty-printed, reordered keys, extra
whitespace are all fine; only the content must be valid (schema spec
§8: non-canonical-but-valid input is accepted, output is always
canonical). Beyond the serde layer’s checks (unknown fields, value
encodings incl. NaN/Inf and -0, empty-literal, exact version match),
this enforces the rules serde cannot express:
- §1 duplicate object keys are rejected; a UTF-8 BOM and invalid
UTF-8 are rejected at the byte boundary (hence
&[u8], not&str); - §2/§3 sheet names are non-empty, ≤ 100 scalar values, and unique under Unicode simple case folding;
- §3 cell keys match
^[A-Z]{1,3}[1-9][0-9]{0,7}$and lie within the address bounds; - §5 spill rectangles are document-valid (no authored cell inside an anchor’s rectangle, no overlapping rectangles, none out of bounds);
- §7 named-range names and
refs are valid and canonical, names are unique case-insensitively, and norefdangles to a missing sheet; - Decision 5 input size and all structural limits are enforced (the
input-size and cell-count caps on
wasm32only — see thelimitsmodule docs).
Sourcepub fn to_json(&self) -> Result<String, WorkbookError>
pub fn to_json(&self) -> Result<String, WorkbookError>
Serializes the workbook to its canonical RFC 8785 (JCS) byte form
(schema spec §8): one line, no insignificant whitespace, no trailing
newline, object keys sorted by UTF-16 code units, ECMAScript number
formatting, names sorted by name.
Errors if a named range or table ref dangles to a sheet the workbook
does not have — the §7 invariant remove_sheet
and rename_sheet are the ways to break, checked
here so a document that cannot be loaded cannot be written — if a value
is non-finite (forbidden, schema spec §8.4), or if the canonical bytes
exceed the 100 MiB cap — enforced on wasm32 only, see the
limits module docs (scope ADR Decision 5).