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

// RecalcOptions narrows the scope of Recalc.
type RecalcOptions struct {
// Sheet limits recalc to a single worksheet when non-empty. An
// empty value recalculates every formula in the workbook.
Sheet string
// Ref limits recalc to cells inside the given A1-style range
// (e.g. B2:D10). Requires Sheet to be set.
Ref string
}

// Recalc evaluates every formula in scope 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()
//
// Scope is controlled by an optional RecalcOptions value. The default
// (no options) walks the whole workbook. Setting Sheet narrows to one
// worksheet; setting Ref (requires Sheet) narrows further to an A1
// range.
//
// 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(opts ...RecalcOptions) error {
var o RecalcOptions
if len(opts) > 0 {
o = opts[0]
}
cells, err := f.collectRecalcCells(o)
if err != nil {
return err
}
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...)
}

// recalcTarget identifies a formula cell scheduled for recalculation.
type recalcTarget struct{ sheet, cell string }

// collectRecalcCells walks the workbook and returns every formula
// cell inside the scope specified by RecalcOptions. The workbook is
// walked in sheet-list order; within a sheet, cells are visited by
// row then column. Ref is parsed relative to Sheet's origin.
func (f *File) collectRecalcCells(o RecalcOptions) ([]recalcTarget, error) {
if o.Ref != "" && o.Sheet == "" {
return nil, fmt.Errorf("recalc: Ref requires Sheet: %w", ErrParameterRequired)
}
var sheets []string
if o.Sheet != "" {
if idx, err := f.GetSheetIndex(o.Sheet); err != nil || idx < 0 {
return nil, fmt.Errorf("recalc: sheet %q not found", o.Sheet)
}
sheets = []string{o.Sheet}
} else {
sheets = f.GetSheetList()
}
inScope := func(_, _ int) bool { return true }
if o.Ref != "" {
coords, err := rangeRefToCoordinates(o.Ref)
if err != nil {
return nil, fmt.Errorf("recalc: invalid Ref: %w", err)
}
_ = sortCoordinates(coords)
colStart, rowStart, colEnd, rowEnd := coords[0], coords[1], coords[2], coords[3]
inScope = func(col, row int) bool {
return col >= colStart && col <= colEnd && row >= rowStart && row <= rowEnd
}
}
var cells []recalcTarget
for _, sn := range sheets {
ws, err := f.workSheetReader(sn)
if err != nil {
return nil, err
}
for r := range ws.SheetData.Row {
row := &ws.SheetData.Row[r]
for i := range row.C {
if row.C[i].F == nil {
continue
}
col, rowNum, cellErr := CellNameToCoordinates(row.C[i].R)
if cellErr != nil {
continue
}
if !inScope(col, rowNum) {
continue
}
cells = append(cells, recalcTarget{sheet: sn, cell: row.C[i].R})
}
}
}
return cells, nil
}

// 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