From 0da3d66d853ff498dd0c40fabc61a61e53cd9aff Mon Sep 17 00:00:00 2001 From: JiaZhengOng Date: Tue, 28 Jul 2026 18:23:47 +0000 Subject: [PATCH] fix(xlsx): external-workbook references fall back to their cached value A formula referencing another workbook (=[1]Sheet!A1) can't be resolved without the linked file and surfaces as #REF!, which poisons every dependent total. Excel keeps the cell's last cached ; fall through to it when an external ref errors, so SUMs over linked cells compute instead of cascading #REF!. Genuine (non-external) errors still propagate. --- src/officecli/Core/Formula/FormulaEvaluator.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/officecli/Core/Formula/FormulaEvaluator.cs b/src/officecli/Core/Formula/FormulaEvaluator.cs index d9f03efec..1564439a2 100644 --- a/src/officecli/Core/Formula/FormulaEvaluator.cs +++ b/src/officecli/Core/Formula/FormulaEvaluator.cs @@ -1194,6 +1194,10 @@ private void RestoreBindings(Dictionary snapshot) // ==================== Cell & Range Resolution ==================== + // External-workbook reference marker in a stored formula (the "[1]" in + // "[1]Sheet!A1"); such refs can't be resolved without the linked file. + private static readonly Regex ExternalRefRe = new(@"\[\d+\]", RegexOptions.Compiled); + internal FormulaResult? ResolveCellResult(string cellRef) { cellRef = StripDollar(cellRef).ToUpperInvariant(); @@ -1250,7 +1254,12 @@ private void RestoreBindings(Dictionary snapshot) { var circularBefore = _session.CircularHits; var evaluated = EvaluateFormula(ModernFunctionQualifier.Unqualify(cell.CellFormula.Text)); - if (evaluated != null) + // An external-workbook reference (=[1]Sheet!A1) can't be resolved without + // the linked file and surfaces as #REF!. Excel keeps the last cached + // for such cells, so fall through to it rather than poisoning every + // dependent total with #REF!. Genuine errors (no external link) still propagate. + if (evaluated != null + && !(evaluated.IsError && ExternalRefRe.IsMatch(cell.CellFormula.Text))) { // Memoize only clean results: no live bindings (see lookup // guard above) and no circular fallback during this