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
1 change: 1 addition & 0 deletions adjust.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ func (f *File) adjustHelper(sheet string, dir adjustDirection, num, offset int)
if err != nil {
return err
}
f.clearCalcCache()
sheetID := f.getSheetID(sheet)
if dir == rows {
err = f.adjustRowDimensions(sheet, ws, num, offset)
Expand Down
30 changes: 30 additions & 0 deletions calc.go
Original file line number Diff line number Diff line change
Expand Up @@ -839,6 +839,14 @@ 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)
f.calcCacheMu.RLock()
if cachedResult, found := f.calcCache.Load(cacheKey); found {
f.calcCacheMu.RUnlock()
return cachedResult.(string), nil
}
f.calcCacheMu.RUnlock()

options := f.getOptions(opts...)
var (
rawCellValue = options.RawCellValue
Expand All @@ -861,14 +869,29 @@ func (f *File) CalcCellValue(sheet, cell string, opts ...Options) (result string
_, precision, decimal := isNumeric(token.Value())
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.calcCacheMu.Lock()
f.calcCache.Store(cacheKey, result)
f.calcCacheMu.Unlock()
}
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.calcCacheMu.Lock()
f.calcCache.Store(cacheKey, result)
f.calcCacheMu.Unlock()
}
return
}
result, err = f.formattedValue(&xlsxC{S: styleIdx, V: token.Value()}, rawCellValue, CellTypeInlineString)
if err == nil {
f.calcCacheMu.Lock()
f.calcCache.Store(cacheKey, result)
f.calcCacheMu.Unlock()
}
return
}

Expand Down Expand Up @@ -1312,6 +1335,13 @@ func calcDiv(rOpd, lOpd formulaArg, opdStack *Stack) error {
return nil
}

// clearCalcCache clear all calculation cache.
func (f *File) clearCalcCache() {
f.calcCacheMu.Lock()
f.calcCache.Clear()
f.calcCacheMu.Unlock()
}

// calculate evaluate basic arithmetic operations.
func calculate(opdStack *Stack, opt efp.Token) error {
if opt.TValue == "-" && opt.TType == efp.TokenTypeOperatorPrefix {
Expand Down
102 changes: 102 additions & 0 deletions calc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6632,3 +6632,105 @@ func TestParseToken(t *testing.T) {
efp.Token{TSubType: efp.TokenSubTypeRange, TValue: "1A"}, nil, nil,
).Error())
}

func TestCalcCellValueCache(t *testing.T) {
t.Run("for_calc_call_value_with_cache", func(t *testing.T) {
f := NewFile()
assert.NoError(t, f.SetCellValue("Sheet1", "A1", 40))
assert.NoError(t, f.SetCellValue("Sheet1", "A2", 50))
assert.NoError(t, f.SetCellFormula("Sheet1", "A3", "A1+A2"))

result1, err := f.CalcCellValue("Sheet1", "A3")
assert.NoError(t, err)
assert.Equal(t, "90", result1)

result2, err := f.CalcCellValue("Sheet1", "A3")
assert.NoError(t, err)
assert.Equal(t, result1, result2, "cached result should be consistent")

assert.NoError(t, f.SetCellValue("Sheet1", "A1", 60))

result3, err := f.CalcCellValue("Sheet1", "A3")
assert.NoError(t, err)
assert.Equal(t, "110", result3)
assert.NotEqual(t, result1, result3, "result should be updated after cache clear")
})
t.Run("for_calc_call_value_with_multiple_dependent_cells", func(t *testing.T) {
f := NewFile()
assert.NoError(t, f.SetCellValue("Sheet1", "A1", 10))
assert.NoError(t, f.SetCellValue("Sheet1", "A2", 10))
assert.NoError(t, f.SetCellFormula("Sheet1", "A3", "A1+A2"))
assert.NoError(t, f.SetCellFormula("Sheet1", "A4", "A3*3"))
assert.NoError(t, f.SetCellFormula("Sheet1", "A5", "A3+A4"))

result3, err := f.CalcCellValue("Sheet1", "A3")
assert.NoError(t, err)
assert.Equal(t, "20", result3)

result4, err := f.CalcCellValue("Sheet1", "A4")
assert.NoError(t, err)
assert.Equal(t, "60", result4)

result5, err := f.CalcCellValue("Sheet1", "A5")
assert.NoError(t, err)
assert.Equal(t, "80", result5)

assert.NoError(t, f.SetCellValue("Sheet1", "A1", 20))

newResult3, err := f.CalcCellValue("Sheet1", "A3")
assert.NoError(t, err)
assert.Equal(t, "30", newResult3)
assert.NotEqual(t, result3, newResult3, "A3 should be updated")

newResult5, err := f.CalcCellValue("Sheet1", "A5")
assert.NoError(t, err)
assert.Equal(t, "120", newResult5)
assert.NotEqual(t, result5, newResult5, "A5 should be updated")
})
t.Run("for_clear_calculation_cache", func(t *testing.T) {
f := NewFile()
assert.NoError(t, f.SetCellValue("Sheet1", "A1", 10))
assert.NoError(t, f.SetCellFormula("Sheet1", "A2", "A1*2"))

result1, err := f.CalcCellValue("Sheet1", "A2")
assert.NoError(t, err)
assert.Equal(t, "20", result1)

result2, err := f.CalcCellValue("Sheet1", "A2")
assert.NoError(t, err)
assert.Equal(t, result1, result2, "results should be consistent from cache")

cases := []struct {
name string
fn func() error
}{
{"SetCellValue", func() error { return f.SetCellValue("Sheet1", "B1", 100) }},
{"SetCellInt", func() error { return f.SetCellInt("Sheet1", "B2", 200) }},
{"SetCellUint", func() error { return f.SetCellUint("Sheet1", "B3", 300) }},
{"SetCellFloat", func() error { return f.SetCellFloat("Sheet1", "B4", 3.14, 2, 64) }},
{"SetCellStr", func() error { return f.SetCellStr("Sheet1", "B5", "test") }},
{"SetCellBool", func() error { return f.SetCellBool("Sheet1", "B6", true) }},
{"SetCellDefault", func() error { return f.SetCellDefault("Sheet1", "B7", "default") }},
{"SetCellFormula", func() error { return f.SetCellFormula("Sheet1", "B8", "=1+1") }},
{"SetCellHyperLink", func() error {
return f.SetCellHyperLink("Sheet1", "B9", "https://github.com/xuri/excelize", "External")
}},
{"SetCellRichText", func() error {
runs := []RichTextRun{{Text: "Rich", Font: &Font{Bold: true}}}
return f.SetCellRichText("Sheet1", "B10", runs)
}},
{"SetSheetRow", func() error { return f.SetSheetRow("Sheet1", "C1", &[]interface{}{1, 2, 3}) }},
{"SetSheetCol", func() error { return f.SetSheetCol("Sheet1", "D1", &[]interface{}{4, 5, 6}) }},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
_, err := f.CalcCellValue("Sheet1", "A2")
assert.NoError(t, err)
assert.NoError(t, tc.fn())
result, err := f.CalcCellValue("Sheet1", "A2")
assert.NoError(t, err)
assert.Equal(t, "20", result, "calculation should still work after cache clear")
})
}
})
}
3 changes: 3 additions & 0 deletions cell.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +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.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 @@ -794,6 +795,7 @@ func (f *File) SetCellFormula(sheet, cell, formula string, opts ...FormulaOpts)
if err != nil {
return err
}
f.clearCalcCache()
if formula == "" {
ws.deleteSharedFormula(c)
c.F = nil
Expand Down Expand Up @@ -1369,6 +1371,7 @@ func (f *File) SetCellRichText(sheet, cell string, runs []RichTextRun) error {
if si.R, err = setRichText(runs); err != nil {
return err
}
f.clearCalcCache()
for idx, strItem := range sst.SI {
if reflect.DeepEqual(strItem, si) {
c.T, c.V = "s", strconv.Itoa(idx)
Expand Down
2 changes: 2 additions & 0 deletions excelize.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ type File struct {
streams map[string]*StreamWriter
tempFiles sync.Map
xmlAttr sync.Map
calcCache sync.Map
calcCacheMu sync.RWMutex
CalcChain *xlsxCalcChain
CharsetReader func(charset string, input io.Reader) (rdr io.Reader, err error)
Comments map[string]*xlsxComments
Expand Down
1 change: 1 addition & 0 deletions merge.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ func (f *File) UnmergeCell(sheet, topLeftCell, bottomRightCell string) error {
if err = ws.mergeOverlapCells(); err != nil {
return err
}
f.clearCalcCache()
i := 0
for _, mergeCell := range ws.MergeCells.Cells {
if rect2, _ := rangeRefToCoordinates(mergeCell.Ref); isOverlap(rect1, rect2) {
Expand Down
3 changes: 2 additions & 1 deletion 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.clearCalcCache()
pivotTableID := f.countPivotTables() + 1
pivotCacheID := f.countPivotCache() + 1

Expand Down Expand Up @@ -1062,6 +1062,7 @@ func (f *File) DeletePivotTable(sheet, name string) error {
if err != nil {
return err
}
f.clearCalcCache()
pivotTableCaches := map[string]int{}
pivotTables, _ := f.getPivotTables()
for _, sheetPivotTables := range pivotTables {
Expand Down
6 changes: 5 additions & 1 deletion sheet.go
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,7 @@ func (f *File) SetSheetName(source, target string) error {
if target == source {
return err
}
f.clearCalcCache()
wb, _ := f.workbookReader()
for k, v := range wb.Sheets.Sheet {
if v.Name == source {
Expand Down Expand Up @@ -579,7 +580,7 @@ func (f *File) DeleteSheet(sheet string) error {
if idx, _ := f.GetSheetIndex(sheet); f.SheetCount == 1 || idx == -1 {
return nil
}

f.clearCalcCache()
wb, _ := f.workbookReader()
wbRels, _ := f.relsReader(f.getWorkbookRelsPath())
activeSheetName := f.GetSheetName(f.GetActiveSheetIndex())
Expand Down Expand Up @@ -768,6 +769,7 @@ func (f *File) copySheet(from, to int) error {
if err != nil {
return err
}
f.clearCalcCache()
worksheet := &xlsxWorksheet{}
deepcopy.Copy(worksheet, sheet)
toSheetID := strconv.Itoa(f.getSheetID(f.GetSheetName(to)))
Expand Down Expand Up @@ -1771,6 +1773,7 @@ func (f *File) SetDefinedName(definedName *DefinedName) error {
if err != nil {
return err
}
f.clearCalcCache()
d := xlsxDefinedName{
Name: definedName.Name,
Comment: definedName.Comment,
Expand Down Expand Up @@ -1813,6 +1816,7 @@ func (f *File) DeleteDefinedName(definedName *DefinedName) error {
if err != nil {
return err
}
f.clearCalcCache()
if wb.DefinedNames != nil {
for idx, dn := range wb.DefinedNames.DefinedName {
scope := "Workbook"
Expand Down
2 changes: 2 additions & 0 deletions table.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ func (f *File) AddTable(sheet string, table *Table) error {
return err
}
f.addSheetNameSpace(sheet, SourceRelationship)
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 @@ -177,6 +178,7 @@ func (f *File) DeleteTable(name string) error {
if err != nil {
return err
}
f.clearCalcCache()
for sheet, tables := range tbls {
for _, table := range tables {
if table.Name != name {
Expand Down
Loading