Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ jobs:
strategy:
matrix:
go-version: [1.24.x, 1.25.x]
os: [ubuntu-24.04, macos-13, windows-latest]
os: [ubuntu-24.04, macos-15-intel, windows-latest]
targetplatform: [x86, x64]

runs-on: ${{ matrix.os }}
Expand Down
2 changes: 1 addition & 1 deletion adjust.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ func (f *File) adjustHelper(sheet string, dir adjustDirection, num, offset int)
if err != nil {
return err
}
f.calcCache.Clear()
f.clearCalcCache()
sheetID := f.getSheetID(sheet)
if dir == rows {
err = f.adjustRowDimensions(sheet, ws, num, offset)
Expand Down
80 changes: 59 additions & 21 deletions calc.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,8 @@ var (
return fmt.Sprintf("R[%d]C[%d]", row, col), nil
},
}
formulaFormats = []*regexp.Regexp{
formulaFnNameReplacer = strings.NewReplacer("_xlfn.", "", ".", "dot")
formulaFormats = []*regexp.Regexp{
regexp.MustCompile(`^(\d+)$`),
regexp.MustCompile(`^=(.*)$`),
regexp.MustCompile(`^<>(.*)$`),
Expand Down Expand Up @@ -839,8 +840,8 @@ type formulaFuncs struct {
// Z.TEST
// ZTEST
func (f *File) CalcCellValue(sheet, cell string, opts ...Options) (result string, err error) {
cacheKey := fmt.Sprintf("%s!%s", sheet, cell)
if cachedResult, found := f.calcCache.Load(cacheKey); found {
entry := sheet + "!" + cell
if cachedResult, ok := f.calcCache.Load(entry); ok {
return cachedResult.(string), nil
}
options := f.getOptions(opts...)
Expand All @@ -850,7 +851,7 @@ func (f *File) CalcCellValue(sheet, cell string, opts ...Options) (result string
token formulaArg
)
if token, err = f.calcCellValue(&calcContext{
entry: fmt.Sprintf("%s!%s", sheet, cell),
entry: entry,
maxCalcIterations: options.MaxCalcIterations,
iterations: make(map[string]uint),
iterationsCache: make(map[string]formulaArg),
Expand All @@ -866,25 +867,31 @@ func (f *File) CalcCellValue(sheet, cell string, opts ...Options) (result string
if precision > 15 {
result, err = f.formattedValue(&xlsxC{S: styleIdx, V: strings.ToUpper(strconv.FormatFloat(decimal, 'G', 15, 64))}, rawCellValue, CellTypeNumber)
if err == nil {
f.calcCache.Store(cacheKey, result)
f.calcCache.Store(entry, result)
}
return
}
if !strings.HasPrefix(result, "0") {
result, err = f.formattedValue(&xlsxC{S: styleIdx, V: strings.ToUpper(strconv.FormatFloat(decimal, 'f', -1, 64))}, rawCellValue, CellTypeNumber)
}
if err == nil {
f.calcCache.Store(cacheKey, result)
f.calcCache.Store(entry, result)
}
return
}
result, err = f.formattedValue(&xlsxC{S: styleIdx, V: token.Value()}, rawCellValue, CellTypeInlineString)
if err == nil {
f.calcCache.Store(cacheKey, result)
f.calcCache.Store(entry, result)
}
return
}

// clearCalcCache clear all calculation related caches.
func (f *File) clearCalcCache() {
f.calcCache.Clear()
f.formulaArgCache.Clear()
}

// calcCellValue calculate cell value by given context, worksheet name and cell
// reference.
func (f *File) calcCellValue(ctx *calcContext, sheet, cell string) (result formulaArg, err error) {
Expand Down Expand Up @@ -1106,8 +1113,8 @@ func (f *File) evalInfixExpFunc(ctx *calcContext, sheet, cell string, token, nex
}
prepareEvalInfixExp(opfStack, opftStack, opfdStack, argsStack)
// call formula function to evaluate
arg := callFuncByName(&formulaFuncs{f: f, sheet: sheet, cell: cell, ctx: ctx}, strings.NewReplacer(
"_xlfn.", "", ".", "dot").Replace(opfStack.Peek().(efp.Token).TValue),
arg := callFuncByName(&formulaFuncs{f: f, sheet: sheet, cell: cell, ctx: ctx},
formulaFnNameReplacer.Replace(opfStack.Peek().(efp.Token).TValue),
[]reflect.Value{reflect.ValueOf(argsStack.Peek().(*list.List))})
if arg.Type == ArgError && opfStack.Len() == 1 {
return arg
Expand Down Expand Up @@ -1651,7 +1658,11 @@ func (f *File) cellResolver(ctx *calcContext, sheet, cell string) (formulaArg, e
value string
err error
)
ref := fmt.Sprintf("%s!%s", sheet, cell)
ref := sheet + "!" + cell
if cached, ok := f.formulaArgCache.Load(ref); ok {
return cached.(formulaArg), err
}

if formula, _ := f.getCellFormula(sheet, cell, true); len(formula) != 0 {
ctx.mu.Lock()
if ctx.entry != ref {
Expand All @@ -1660,6 +1671,7 @@ func (f *File) cellResolver(ctx *calcContext, sheet, cell string) (formulaArg, e
ctx.mu.Unlock()
arg, _ = f.calcCellValue(ctx, sheet, cell)
ctx.iterationsCache[ref] = arg
f.formulaArgCache.Store(ref, arg)
return arg, nil
}
ctx.mu.Unlock()
Expand All @@ -1674,29 +1686,29 @@ func (f *File) cellResolver(ctx *calcContext, sheet, cell string) (formulaArg, e
cellType, _ := f.GetCellType(sheet, cell)
switch cellType {
case CellTypeBool:
return arg.ToBool(), err
arg = arg.ToBool()
case CellTypeNumber, CellTypeUnset:
if arg.Value() == "" {
return newEmptyFormulaArg(), err
arg = newEmptyFormulaArg()
} else {
arg = arg.ToNumber()
}
return arg.ToNumber(), err
case CellTypeInlineString, CellTypeSharedString:
return arg, err
case CellTypeFormula:
if value != "" {
return arg, err
if value == "" {
arg = newEmptyFormulaArg()
}
return newEmptyFormulaArg(), err
case CellTypeDate:
if value, err = f.GetCellValue(sheet, cell); err == nil {
if num := newStringFormulaArg(value).ToNumber(); num.Type == ArgNumber {
return num, err
arg = num
}
}
return arg, err
default:
return newErrorFormulaArg(value, value), err
arg = newErrorFormulaArg(value, value)
}
f.formulaArgCache.Store(ref, arg)
return arg, err
}

// rangeResolver extract value as string from given reference and range list.
Expand Down Expand Up @@ -1735,13 +1747,39 @@ func (f *File) rangeResolver(ctx *calcContext, cellRefs, cellRanges *list.List)
return
}

// Detect whole column/row reference, limit to actual data range
if valueRange[1] == TotalRows {
actualMaxRow := 0
for _, rowData := range ws.SheetData.Row {
if rowData.R > actualMaxRow {
actualMaxRow = rowData.R
}
}
if actualMaxRow > 0 && actualMaxRow < TotalRows {
valueRange[1] = actualMaxRow
}
}
if valueRange[3] == MaxColumns {
actualMaxCol := 0
for _, rowData := range ws.SheetData.Row {
for _, cell := range rowData.C {
col, _, err := CellNameToCoordinates(cell.R)
if err == nil && col > actualMaxCol {
actualMaxCol = col
}
}
}
if actualMaxCol > 0 && actualMaxCol < MaxColumns {
valueRange[3] = actualMaxCol
}
}

for row := valueRange[0]; row <= valueRange[1]; row++ {
colMax := 0
if row <= len(ws.SheetData.Row) {
rowData := &ws.SheetData.Row[row-1]
colMax = min(valueRange[3], len(rowData.C))
}

var matrixRow []formulaArg
for col := valueRange[2]; col <= valueRange[3]; col++ {
value := newEmptyFormulaArg()
Expand Down
14 changes: 14 additions & 0 deletions calc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6474,6 +6474,20 @@ func TestCalcRangeResolver(t *testing.T) {
cellRefs.PushBack(cellRef{Col: 1, Row: TotalRows + 1, Sheet: "SheetN"})
_, err = f.rangeResolver(&calcContext{}, cellRefs, cellRanges)
assert.Equal(t, ErrMaxRows, err)
t.Run("for_range_resolver_error", func(t *testing.T) {
f := NewFile()
assert.NoError(t, f.SetCellValue("Sheet1", "A1", "test"))
cellRefs := list.New()
cellRanges := list.New()
cellRanges.PushBack(cellRange{
From: cellRef{Col: 1, Row: 1, Sheet: "Sheet1"},
To: cellRef{Col: 1, Row: 1, Sheet: "Sheet1"},
})
f.SharedStrings = nil
f.Pkg.Store(defaultXMLPathSharedStrings, MacintoshCyrillicCharset)
_, err := f.rangeResolver(&calcContext{}, cellRefs, cellRanges)
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
})
}

func TestCalcBahttextAppendDigit(t *testing.T) {
Expand Down
6 changes: 3 additions & 3 deletions cell.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ func (c *xlsxC) hasValue() bool {

// removeFormula delete formula for the cell.
func (f *File) removeFormula(c *xlsxC, ws *xlsxWorksheet, sheet string) error {
f.calcCache.Clear()
f.clearCalcCache()
if c.F != nil && c.Vm == nil {
sheetID := f.getSheetID(sheet)
if err := f.deleteCalcChain(sheetID, c.R); err != nil {
Expand Down Expand Up @@ -795,7 +795,7 @@ func (f *File) SetCellFormula(sheet, cell, formula string, opts ...FormulaOpts)
if err != nil {
return err
}
f.calcCache.Clear()
f.clearCalcCache()
if formula == "" {
ws.deleteSharedFormula(c)
c.F = nil
Expand Down Expand Up @@ -1371,7 +1371,7 @@ func (f *File) SetCellRichText(sheet, cell string, runs []RichTextRun) error {
if si.R, err = setRichText(runs); err != nil {
return err
}
f.calcCache.Clear()
f.clearCalcCache()
for idx, strItem := range sst.SI {
if reflect.DeepEqual(strItem, si) {
c.T, c.V = "s", strconv.Itoa(idx)
Expand Down
1 change: 1 addition & 0 deletions excelize.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ type File struct {
tempFiles sync.Map
xmlAttr sync.Map
calcCache sync.Map
formulaArgCache sync.Map
CalcChain *xlsxCalcChain
CharsetReader func(charset string, input io.Reader) (rdr io.Reader, err error)
Comments map[string]*xlsxComments
Expand Down
2 changes: 1 addition & 1 deletion merge.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ func (f *File) UnmergeCell(sheet, topLeftCell, bottomRightCell string) error {
if err = ws.mergeOverlapCells(); err != nil {
return err
}
f.calcCache.Clear()
f.clearCalcCache()
i := 0
for _, mergeCell := range ws.MergeCells.Cells {
if rect2, _ := rangeRefToCoordinates(mergeCell.Ref); isOverlap(rect1, rect2) {
Expand Down
4 changes: 2 additions & 2 deletions pivotTable.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ func (f *File) AddPivotTable(opts *PivotTableOptions) error {
if err != nil {
return err
}
f.calcCache.Clear()
f.clearCalcCache()
pivotTableID := f.countPivotTables() + 1
pivotCacheID := f.countPivotCache() + 1

Expand Down Expand Up @@ -1062,7 +1062,7 @@ func (f *File) DeletePivotTable(sheet, name string) error {
if err != nil {
return err
}
f.calcCache.Clear()
f.clearCalcCache()
pivotTableCaches := map[string]int{}
pivotTables, _ := f.getPivotTables()
for _, sheetPivotTables := range pivotTables {
Expand Down
10 changes: 5 additions & 5 deletions sheet.go
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,7 @@ func (f *File) SetSheetName(source, target string) error {
if target == source {
return err
}
f.calcCache.Clear()
f.clearCalcCache()
wb, _ := f.workbookReader()
for k, v := range wb.Sheets.Sheet {
if v.Name == source {
Expand Down Expand Up @@ -580,7 +580,7 @@ func (f *File) DeleteSheet(sheet string) error {
if idx, _ := f.GetSheetIndex(sheet); f.SheetCount == 1 || idx == -1 {
return nil
}
f.calcCache.Clear()
f.clearCalcCache()
wb, _ := f.workbookReader()
wbRels, _ := f.relsReader(f.getWorkbookRelsPath())
activeSheetName := f.GetSheetName(f.GetActiveSheetIndex())
Expand Down Expand Up @@ -769,7 +769,7 @@ func (f *File) copySheet(from, to int) error {
if err != nil {
return err
}
f.calcCache.Clear()
f.clearCalcCache()
worksheet := &xlsxWorksheet{}
deepcopy.Copy(worksheet, sheet)
toSheetID := strconv.Itoa(f.getSheetID(f.GetSheetName(to)))
Expand Down Expand Up @@ -1773,7 +1773,7 @@ func (f *File) SetDefinedName(definedName *DefinedName) error {
if err != nil {
return err
}
f.calcCache.Clear()
f.clearCalcCache()
d := xlsxDefinedName{
Name: definedName.Name,
Comment: definedName.Comment,
Expand Down Expand Up @@ -1816,7 +1816,7 @@ func (f *File) DeleteDefinedName(definedName *DefinedName) error {
if err != nil {
return err
}
f.calcCache.Clear()
f.clearCalcCache()
if wb.DefinedNames != nil {
for idx, dn := range wb.DefinedNames.DefinedName {
scope := "Workbook"
Expand Down
4 changes: 2 additions & 2 deletions table.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ func (f *File) AddTable(sheet string, table *Table) error {
return err
}
f.addSheetNameSpace(sheet, SourceRelationship)
f.calcCache.Clear()
f.clearCalcCache()
if err = f.addTable(sheet, tableXML, coordinates[0], coordinates[1], coordinates[2], coordinates[3], tableID, options); err != nil {
return err
}
Expand Down Expand Up @@ -178,7 +178,7 @@ func (f *File) DeleteTable(name string) error {
if err != nil {
return err
}
f.calcCache.Clear()
f.clearCalcCache()
for sheet, tables := range tbls {
for _, table := range tables {
if table.Name != name {
Expand Down