Skip to main content

Workbook

Struct Workbook 

Source
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

Source

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.

Source

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).

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

pub fn name(&self, name: &str) -> Option<&NamedRange>

The named range called name (case-insensitive), or None. Listing is names.

Source

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.

Source

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.

Source

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

pub fn table(&self, name: &str) -> Option<&Table>

The table called name (case-insensitive), or None. Listing is tables.

Source§

impl Workbook

Source

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).

Source

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.

Source

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 recalc stores BLOCKED_SPILL_ERROR instead, exactly like Workbook::place_spill applies for a real recompute.
  • Dependency cycles: recalc never evaluates a cycle member’s formula at all — it short-circuits straight to CIRCULAR_ERROR (see DependencyGraph::cycle_cells and recompute). Evaluating the formula anyway would diverge whenever it catches the error (e.g. IFERROR), since its precedents’ stored values already carry the propagated error but recalc never 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 hookhook 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

Source

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.

Source

pub fn engine(&self) -> EngineFlavor

The engine flavor every formula in this workbook targets.

Source

pub fn version(&self) -> &str

The schema version of this workbook document.

Source

pub fn sheets(&self) -> &[Worksheet]

The worksheets, in tab order (array position is tab position).

Source

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.

Source

pub fn names(&self) -> &[NamedRange]

The workbook-scoped named ranges.

Source

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).

Source

pub fn tables(&self) -> &[Table]

The workbook-scoped table declarations.

Source

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).

Source

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.

Source

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).

Source

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.

Source

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).

Source

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.

Source

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.

Source

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 NamedRange and Table ref whose 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 a ref that 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, and from_json does 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 Table landing on a range another table already occupies (structured-references spec §4). Reachable because a table may legitimately be left dangling by remove_sheet, and a later rename can move a live table onto it;
  • a table that was dangling at to coming 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, so define_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, and from_json checks 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. (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.

Source

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.

Source

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.

Source

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.

Source

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 no ref dangles to a missing sheet;
  • Decision 5 input size and all structural limits are enforced (the input-size and cell-count caps on wasm32 only — see the limits module docs).
Source

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).

Trait Implementations§

Source§

impl Clone for Workbook

Source§

fn clone(&self) -> Workbook

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for Workbook

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl<'de> Deserialize<'de> for Workbook

Source§

fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>
where __D: Deserializer<'de>,

Deserialize this value from the given Serde deserializer. Read more
Source§

impl Hash for Workbook

Source§

fn hash<__H: Hasher>(&self, state: &mut __H)

Feeds this value into the given Hasher. Read more
1.3.0 · Source§

fn hash_slice<H>(data: &[Self], state: &mut H)
where H: Hasher, Self: Sized,

Feeds a slice of this type into the given Hasher. Read more
Source§

impl PartialEq for Workbook

Source§

fn eq(&self, other: &Workbook) -> bool

Equality operator ==. Read more
1.0.0 (const: unstable) · Source§

fn ne(&self, other: &Rhs) -> bool

Inequality operator !=. Read more
Source§

impl Serialize for Workbook

Source§

fn serialize<__S>(&self, __serializer: __S) -> Result<__S::Ok, __S::Error>
where __S: Serializer,

Serialize this value into the given Serde serializer. Read more
Source§

impl StructuralPartialEq for Workbook

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> DeserializeOwned for T
where T: for<'de> Deserialize<'de>,

Source§

impl<T> ErasedDestructor for T
where T: 'static,

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.