Skip to content

This closes #2303, add File.Recalc for whole-workbook recalc#2312

Open
krystophny wants to merge 3 commits into
qax-os:masterfrom
sloppy-org:2303-recalc-workbook
Open

This closes #2303, add File.Recalc for whole-workbook recalc#2312
krystophny wants to merge 3 commits into
qax-os:masterfrom
sloppy-org:2303-recalc-workbook

Conversation

@krystophny
Copy link
Copy Markdown

@krystophny krystophny commented Apr 23, 2026

Description

Add (*File).Recalc() error, built on top of RecalcCell from the companion PR #2311. Recalc walks every formula cell in the workbook, calls RecalcCell on each, and returns an errors.Join-aggregated error listing any per-cell failures wrapped as "<sheet>!<cell>: <cause>". Cells that did compute are still persisted when later cells fail.

Public surface introduced by this PR:

  • (*File).Recalc() error

No existing API is changed or removed.

Related Issue

Closes umbrella #2303. Depends on #2311 (introduces RecalcCell); this PR's branch is stacked on that PR's branch so CI runs against the combined state. It will rebase cleanly on master once #2311 merges.

Documentation

Reference-site entries for this PR ship in a companion docs PR: xuri/excelize-doc#31. That PR covers the English version; other language versions follow the project's usual rollout cadence.

Motivation and Context

Users who open a workbook, mutate inputs, and save it back currently have no way to refresh the cached values downstream consumers rely on. CalcCellValue returns a formatted string but does not persist it, and every SetCell* writer removes the formula while writing a new value, which makes them useless for this purpose. The result is that LibreOffice-headless, Numbers, and other readers that trust <v> without recomputing see stale results after a round trip through excelize. Recalc closes that gap with a single entry point and zero cost to callers who do not use it.

Design notes reviewers are likely to ask about:

  • Fixed-point iteration. Not needed. calcCellValue already recurses into referenced cells during evaluation, so a single pass converges chained dependencies across sheets. The calc cache is cleared before and after the sweep to avoid cross-call contamination.
  • Error model. Failures never abort the pass. Each failure is fmt.Errorf("<sheet>!<cell>: %w", cause); the collection is returned via errors.Join(...). Callers descend with errors.Is / errors.As through the standard Unwrap() []error method the joined error provides. This replaces the typed RecalcError / RecalcCellError surface that an earlier draft proposed — no new exported error types are added here.

Scoping via a RecalcOptions{Sheet, Ref} value is intentionally deferred to #2308 so this PR keeps the API surface minimal; the zero-option form covers the umbrella's stated requirement.

How Has This Been Tested

$ go test -run 'TestRecalc' -timeout 60s -v .
=== RUN   TestRecalcCellTypes
--- PASS: TestRecalcCellTypes (0.00s)
=== RUN   TestRecalcCellNoFormula
--- PASS: TestRecalcCellNoFormula (0.00s)
=== RUN   TestRecalcCellNoTypeSRegression
--- PASS: TestRecalcCellNoTypeSRegression (0.00s)
=== RUN   TestRecalcCellSharedFormulaBand
--- PASS: TestRecalcCellSharedFormulaBand (0.00s)
=== RUN   TestRecalcCellCircularReferenceBounded
--- PASS: TestRecalcCellCircularReferenceBounded (0.00s)
=== RUN   TestRecalcCellRoundTrip
--- PASS: TestRecalcCellRoundTrip (0.00s)
=== RUN   TestRecalcChainedAcrossSheet
--- PASS: TestRecalcChainedAcrossSheet (0.00s)
=== RUN   TestRecalcIdempotent
--- PASS: TestRecalcIdempotent (0.00s)
=== RUN   TestRecalcWorkbookWithoutFormulas
--- PASS: TestRecalcWorkbookWithoutFormulas (0.00s)
=== RUN   TestRecalcAggregatesFailures
--- PASS: TestRecalcAggregatesFailures (0.00s)
=== RUN   TestRecalcNonASCIISheetName
--- PASS: TestRecalcNonASCIISheetName (0.00s)
PASS
ok  	github.com/xuri/excelize/v2	0.014s

Full suite: go test -skip TestZip64 -timeout 10m . passes cleanly. gofmt -s -l . and go vet ./... are clean. ARM cross-builds (GOARM=5|6|7, arm64, android/arm64) all build.

Types of changes

  • Docs change / refactoring / dependency upgrade
  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist

Documentation for the new exported surface lives in the doc comment on Recalc.

CalcCellValue returns a formatted string but does not touch the
stored workbook. Every SetCell* writer strips the <f> element, so
callers that need to refresh the cached value of a formula cell
without losing the formula currently have no path. Readers that
honour <v> without recomputing (LibreOffice headless, Numbers,
etc.) therefore see stale numbers after any excelize round trip.

Add (*File).RecalcCell(sheet, cell string) error. It evaluates
the formula via the existing recursive calc engine, then writes
the typed result into <v>/<t> through a private
setCellCachedValue helper that leaves <f>, the shared-formula
master, ref span, and si index intact. A new sentinel
ErrCellNoFormula is returned when the target cell has no
formula. Dependency resolution reuses calcCellValue, so chained
formulas across cells converge in one pass; circular references
are bounded by the workbook's existing MaxCalcIterations option.

Signed-off-by: Christopher Albert <albert@tugraz.at>
Users who open a workbook, mutate inputs, and save it back have
had no way to refresh the cached values their downstream readers
depend on. LibreOffice headless, Numbers, and readers that trust
<v> without recomputing therefore see stale results after any
excelize round trip.

Build (*File).Recalc on top of the single-cell RecalcCell added
in the previous commit. It walks every formula cell in the
workbook and calls RecalcCell on each, preserving <f> and shared
formula grouping while refreshing <v>/<t> through the private
setCellCachedValue helper. Dependency resolution reuses the
existing recursive calc engine, so chained formulas across
sheets converge in a single pass; the calc cache is cleared
before and after the sweep to avoid cross-call contamination.

Failures never abort the pass. Each failing cell contributes an
fmt.Errorf("<sheet>!<cell>: %w", cause) entry to an
errors.Join(...) result; cells that did compute are still
persisted. Callers can descend with errors.Is / errors.As
through the Unwrap() []error provided by the joined error.

Signed-off-by: Christopher Albert <albert@tugraz.at>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/L Denotes a PR that changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants