Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ var (
// ErrCellCharsLength defined the error message for receiving a cell
// characters length that exceeds the limit.
ErrCellCharsLength = fmt.Errorf("cell value must be 0-%d characters", TotalCellChars)
// ErrCellNoFormula defined the error message on attempting to refresh the
// cached value of a cell that does not hold a formula.
ErrCellNoFormula = errors.New("cell does not contain a formula")
// ErrCellStyles defined the error message on cell styles exceeds the limit.
ErrCellStyles = fmt.Errorf("the cell styles exceeds the %d limit", MaxCellStyles)
// ErrColumnNumber defined the error message on receive an invalid column
Expand Down
104 changes: 104 additions & 0 deletions recalc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Copyright 2016 - 2026 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
// Package excelize providing a set of functions that allow you to write to and
// read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and
// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
// Supports complex components by high compatibility, and provided streaming
// API for generating or reading data from a worksheet with huge amounts of
// data. This library needs Go version 1.25.0 or later.

package excelize

import "strconv"

// RecalcCell evaluates the formula in the given cell and persists the
// typed result into its cached <v>/<t> pair. The cell's <f> element,
// and its shared-formula master, ref span, and si index when present,
// are left untouched. Dependency resolution uses the same recursive
// evaluator as CalcCellValue, so a single call converges a chain of
// formulas that feed the target. Circular references are bounded by
// the workbook's MaxCalcIterations option.
//
// Returns ErrCellNoFormula when the target cell does not hold a
// formula. Any evaluation error from the calc engine is returned
// unwrapped so callers can match on it directly.
func (f *File) RecalcCell(sheet, cell string) error {
ctx := &calcContext{
entry: sheet + "!" + cell,
maxCalcIterations: f.options.MaxCalcIterations,
iterations: make(map[string]uint),
iterationsCache: make(map[string]formulaArg),
}
arg, err := f.calcCellValue(ctx, sheet, cell)
if err != nil {
return err
}
return f.setCellCachedValue(sheet, cell, arg)
}

// setCellCachedValue writes the cached value of a formula cell from a
// typed formulaArg without touching the cell's formula. The cell must
// already hold an <f> element or ErrCellNoFormula is returned. For
// shared-formula children the child's <v>/<t> are updated in isolation:
// the master's formula body and sibling children remain untouched.
func (f *File) setCellCachedValue(sheet, cell string, v formulaArg) error {
f.mu.Lock()
ws, err := f.workSheetReader(sheet)
if err != nil {
f.mu.Unlock()
return err
}
f.mu.Unlock()
ws.mu.Lock()
defer ws.mu.Unlock()
c, _, _, err := ws.prepareCell(cell)
if err != nil {
return err
}
if c.F == nil {
return ErrCellNoFormula
}
setCachedArg(c, v)
return nil
}

// setCachedArg maps a typed formulaArg onto the xlsxC value/type pair
// exactly as Excel would store the cached result of a formula: numeric
// booleans as t="b", plain numbers as t="" (implicit numeric), strings
// as t="str" (the inline shape Excel uses for formula results), errors
// as t="e" with the error code in <v>. ArgMatrix and ArgList collapse
// to their first scalar element, matching formulaArg.Value. ArgEmpty
// and ArgUnknown both clear the cache. Any inline-string remnant from
// a prior cache is cleared so a string-to-number transition does not
// leak the old <is>.
func setCachedArg(c *xlsxC, v formulaArg) {
c.IS = nil
switch v.Type {
case ArgNumber:
if v.Boolean {
c.T, c.V = setCellBool(v.Number != 0)
return
}
c.T, c.V = "", strconv.FormatFloat(v.Number, 'f', -1, 64)
case ArgString:
c.T, c.V = "str", v.String
case ArgError:
c.T, c.V = "e", v.Error
case ArgEmpty, ArgUnknown:
c.T, c.V = "", ""
case ArgMatrix:
if head := v.ToList(); len(head) > 0 && head[0].Type != ArgMatrix {
setCachedArg(c, head[0])
return
}
c.T, c.V = "", ""
case ArgList:
if len(v.List) > 0 && v.List[0].Type != ArgMatrix && v.List[0].Type != ArgList {
setCachedArg(c, v.List[0])
return
}
c.T, c.V = "", ""
}
}
191 changes: 191 additions & 0 deletions recalc_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
// Copyright 2016 - 2026 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.

package excelize

import (
"strings"
"testing"
"time"

"github.com/stretchr/testify/assert"
)

// cellXML fetches the raw xlsxC for a cell so the test can inspect
// <f>, <v>, and <t> directly without going through GetCellValue's
// formatting layer.
func cellXML(t *testing.T, f *File, sheet, addr string) *xlsxC {
t.Helper()
ws, err := f.workSheetReader(sheet)
assert.NoError(t, err)
col, row, err := CellNameToCoordinates(addr)
assert.NoError(t, err)
assert.LessOrEqual(t, row, len(ws.SheetData.Row), addr)
rowData := ws.SheetData.Row[row-1]
assert.LessOrEqual(t, col, len(rowData.C), addr)
return &rowData.C[col-1]
}

func TestRecalcCellTypes(t *testing.T) {
cases := []struct {
name string
formula string
inputs map[string]int
wantT string
wantV string
}{
{"numeric", "SUM(A1:A2)", map[string]int{"A1": 10, "A2": 32}, "", "42"},
{"boolean", "A1>0", map[string]int{"A1": 5}, "b", "1"},
{"chained", "A1*3", map[string]int{"A1": 7}, "", "21"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
f := NewFile()
for k, v := range tc.inputs {
assert.NoError(t, f.SetCellInt("Sheet1", k, int64(v)))
}
assert.NoError(t, f.SetCellFormula("Sheet1", "B1", tc.formula))
assert.NoError(t, f.RecalcCell("Sheet1", "B1"))
c := cellXML(t, f, "Sheet1", "B1")
assert.Equal(t, tc.formula, c.F.Content, "formula preserved")
assert.Equal(t, tc.wantT, c.T)
assert.Equal(t, tc.wantV, c.V)
})
}
}

func TestRecalcCellNoFormula(t *testing.T) {
f := NewFile()
assert.NoError(t, f.SetCellInt("Sheet1", "A1", 5))
assert.ErrorIs(t, f.RecalcCell("Sheet1", "A1"), ErrCellNoFormula)
}

func TestRecalcCellReturnsFormulaError(t *testing.T) {
f := NewFile()
assert.NoError(t, f.SetCellFormula("Sheet1", "A1", "NONEXISTENTFUNC(1)"))
assert.Error(t, f.RecalcCell("Sheet1", "A1"))
}

func TestRecalcCellNoTypeSRegression(t *testing.T) {
// Previously a recalc path round-tripped numeric results through
// string, storing cells as t="s" (shared string). Downstream
// aggregates over that blob silently summed to 0. RecalcCell must
// persist numeric results with an implicit t attribute.
f := NewFile()
assert.NoError(t, f.SetCellInt("Sheet1", "A1", 3))
assert.NoError(t, f.SetCellInt("Sheet1", "A2", 4))
assert.NoError(t, f.SetCellFormula("Sheet1", "A3", "SUM(A1:A2)"))
assert.NoError(t, f.RecalcCell("Sheet1", "A3"))
c := cellXML(t, f, "Sheet1", "A3")
assert.NotEqual(t, "s", c.T)
assert.Equal(t, "", c.T)
assert.Equal(t, "7", c.V)
}

func TestRecalcCellSharedFormulaBand(t *testing.T) {
// Row 40 shared-formula group with master G40; RecalcCell on each
// member must leave the shared metadata intact.
f := NewFile()
for col, v := range map[string]int64{"G": 2, "H": 3, "I": 4, "J": 5} {
assert.NoError(t, f.SetCellInt("Sheet1", col+"1", v))
}
ref := "G40:J40"
sharedType := STCellFormulaTypeShared
assert.NoError(t, f.SetCellFormula("Sheet1", "G40", "SUM(G1:G39)", FormulaOpts{
Type: &sharedType,
Ref: &ref,
}))
master := cellXML(t, f, "Sheet1", "G40")
si := *master.F.Si

for _, addr := range []string{"G40", "H40", "I40", "J40"} {
assert.NoError(t, f.RecalcCell("Sheet1", addr))
}

master = cellXML(t, f, "Sheet1", "G40")
assert.Equal(t, "SUM(G1:G39)", master.F.Content)
assert.Equal(t, ref, master.F.Ref)
assert.Equal(t, si, *master.F.Si)
assert.Equal(t, "2", master.V)
for _, child := range []string{"H40", "I40", "J40"} {
c := cellXML(t, f, "Sheet1", child)
assert.Equal(t, sharedType, c.F.T, child)
assert.Equal(t, "", c.F.Ref, child)
assert.Equal(t, si, *c.F.Si, child)
}
}

func TestRecalcCellCircularReferenceBounded(t *testing.T) {
f := NewFile()
assert.NoError(t, f.SetCellFormula("Sheet1", "A1", "B1+1"))
assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "A1+1"))
done := make(chan struct{})
go func() {
_ = f.RecalcCell("Sheet1", "A1")
close(done)
}()
select {
case <-done:
case <-time.After(2 * time.Second):
t.Fatal("RecalcCell did not terminate on circular reference")
}
}

func TestRecalcCellRoundTrip(t *testing.T) {
f := NewFile()
assert.NoError(t, f.SetCellInt("Sheet1", "A1", 2))
assert.NoError(t, f.SetCellInt("Sheet1", "A2", 3))
assert.NoError(t, f.SetCellFormula("Sheet1", "A3", "A1*A2"))
assert.NoError(t, f.RecalcCell("Sheet1", "A3"))

buf, err := f.WriteToBuffer()
assert.NoError(t, err)
f2, err := OpenReader(strings.NewReader(buf.String()))
assert.NoError(t, err)
defer func() { _ = f2.Close() }()

formula, err := f2.GetCellFormula("Sheet1", "A3")
assert.NoError(t, err)
assert.Equal(t, "A1*A2", formula)
value, err := f2.GetCellValue("Sheet1", "A3", Options{RawCellValue: true})
assert.NoError(t, err)
assert.Equal(t, "6", value)
}

func TestSetCellCachedValueRejectsBadTarget(t *testing.T) {
f := NewFile()

assert.Error(t, f.setCellCachedValue("Nope", "A1", newNumberFormulaArg(1)))
assert.Error(t, f.setCellCachedValue("Sheet1", "bad-ref", newNumberFormulaArg(1)))
}

func TestSetCachedArgTypes(t *testing.T) {
cases := []struct {
name string
arg formulaArg
wantT string
wantV string
}{
{"string", newStringFormulaArg("ready"), "str", "ready"},
{"formula_error", newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE), "e", formulaErrorVALUE},
{"empty", newEmptyFormulaArg(), "", ""},
{"unknown", formulaArg{Type: ArgUnknown}, "", ""},
{"matrix_scalar_head", newMatrixFormulaArg([][]formulaArg{{newStringFormulaArg("top")}}), "str", "top"},
{"matrix_nested_head", newMatrixFormulaArg([][]formulaArg{{newMatrixFormulaArg([][]formulaArg{{newNumberFormulaArg(1)}})}}), "", ""},
{"list_scalar_head", newListFormulaArg([]formulaArg{newNumberFormulaArg(12)}), "", "12"},
{"list_nested_head", newListFormulaArg([]formulaArg{newListFormulaArg([]formulaArg{newNumberFormulaArg(2)})}), "", ""},
{"list_empty", newListFormulaArg(nil), "", ""},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
c := &xlsxC{T: "inlineStr", V: "stale", IS: &xlsxSI{}}

setCachedArg(c, tc.arg)

assert.Nil(t, c.IS)
assert.Equal(t, tc.wantT, c.T)
assert.Equal(t, tc.wantV, c.V)
})
}
}