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
156 changes: 156 additions & 0 deletions recalc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
// 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 (
"errors"
"fmt"
"strconv"
)

// Recalc evaluates every formula in the workbook and persists each
// result into the cell's cached <v>/<t> via RecalcCell. The <f>
// element and any shared-formula grouping are preserved. Typical use:
//
// f, _ := excelize.OpenFile(path)
// defer f.Close()
// _ = f.Recalc()
// _ = f.Save()
//
// Dependency resolution uses the same recursive evaluator as
// CalcCellValue, so chained formulas across sheets converge in a
// single pass. Circular references are bounded by the workbook's
// MaxCalcIterations option, matching existing calc engine behaviour.
//
// When any formula cannot be evaluated, Recalc continues with the
// remaining cells. Each failure is wrapped as
// fmt.Errorf("<sheet>!<cell>: %w", err); Recalc returns the joined
// collection via errors.Join so errors.Is / errors.As descend into
// the underlying causes. Cells that did compute are still persisted.
func (f *File) Recalc() error {
type target struct{ sheet, cell string }
var cells []target
for _, sn := range f.GetSheetList() {
ws, err := f.workSheetReader(sn)
if err != nil {
return err
}
for r := range ws.SheetData.Row {
row := &ws.SheetData.Row[r]
for i := range row.C {
if row.C[i].F == nil {
continue
}
cells = append(cells, target{sheet: sn, cell: row.C[i].R})
}
}
}
f.clearCalcCache()
var failures []error
for _, t := range cells {
if err := f.RecalcCell(t.sheet, t.cell); err != nil {
failures = append(failures, fmt.Errorf("%s!%s: %w", t.sheet, t.cell, err))
}
}
f.clearCalcCache()
return errors.Join(failures...)
}

// 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 = "", ""
}
}
Loading