Skip to content

Commit 8990800

Browse files
authored
This closes #2057, support calculation cache to improve CalcCellValue performance (#2144)
1 parent 8a99deb commit 8990800

File tree

9 files changed

+148
-2
lines changed

9 files changed

+148
-2
lines changed

adjust.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ func (f *File) adjustHelper(sheet string, dir adjustDirection, num, offset int)
7575
if err != nil {
7676
return err
7777
}
78+
f.clearCalcCache()
7879
sheetID := f.getSheetID(sheet)
7980
if dir == rows {
8081
err = f.adjustRowDimensions(sheet, ws, num, offset)

calc.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -839,6 +839,14 @@ type formulaFuncs struct {
839839
// Z.TEST
840840
// ZTEST
841841
func (f *File) CalcCellValue(sheet, cell string, opts ...Options) (result string, err error) {
842+
cacheKey := fmt.Sprintf("%s!%s", sheet, cell)
843+
f.calcCacheMu.RLock()
844+
if cachedResult, found := f.calcCache.Load(cacheKey); found {
845+
f.calcCacheMu.RUnlock()
846+
return cachedResult.(string), nil
847+
}
848+
f.calcCacheMu.RUnlock()
849+
842850
options := f.getOptions(opts...)
843851
var (
844852
rawCellValue = options.RawCellValue
@@ -861,14 +869,29 @@ func (f *File) CalcCellValue(sheet, cell string, opts ...Options) (result string
861869
_, precision, decimal := isNumeric(token.Value())
862870
if precision > 15 {
863871
result, err = f.formattedValue(&xlsxC{S: styleIdx, V: strings.ToUpper(strconv.FormatFloat(decimal, 'G', 15, 64))}, rawCellValue, CellTypeNumber)
872+
if err == nil {
873+
f.calcCacheMu.Lock()
874+
f.calcCache.Store(cacheKey, result)
875+
f.calcCacheMu.Unlock()
876+
}
864877
return
865878
}
866879
if !strings.HasPrefix(result, "0") {
867880
result, err = f.formattedValue(&xlsxC{S: styleIdx, V: strings.ToUpper(strconv.FormatFloat(decimal, 'f', -1, 64))}, rawCellValue, CellTypeNumber)
868881
}
882+
if err == nil {
883+
f.calcCacheMu.Lock()
884+
f.calcCache.Store(cacheKey, result)
885+
f.calcCacheMu.Unlock()
886+
}
869887
return
870888
}
871889
result, err = f.formattedValue(&xlsxC{S: styleIdx, V: token.Value()}, rawCellValue, CellTypeInlineString)
890+
if err == nil {
891+
f.calcCacheMu.Lock()
892+
f.calcCache.Store(cacheKey, result)
893+
f.calcCacheMu.Unlock()
894+
}
872895
return
873896
}
874897

@@ -1312,6 +1335,13 @@ func calcDiv(rOpd, lOpd formulaArg, opdStack *Stack) error {
13121335
return nil
13131336
}
13141337

1338+
// clearCalcCache clear all calculation cache.
1339+
func (f *File) clearCalcCache() {
1340+
f.calcCacheMu.Lock()
1341+
f.calcCache.Clear()
1342+
f.calcCacheMu.Unlock()
1343+
}
1344+
13151345
// calculate evaluate basic arithmetic operations.
13161346
func calculate(opdStack *Stack, opt efp.Token) error {
13171347
if opt.TValue == "-" && opt.TType == efp.TokenTypeOperatorPrefix {

calc_test.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6632,3 +6632,105 @@ func TestParseToken(t *testing.T) {
66326632
efp.Token{TSubType: efp.TokenSubTypeRange, TValue: "1A"}, nil, nil,
66336633
).Error())
66346634
}
6635+
6636+
func TestCalcCellValueCache(t *testing.T) {
6637+
t.Run("for_calc_call_value_with_cache", func(t *testing.T) {
6638+
f := NewFile()
6639+
assert.NoError(t, f.SetCellValue("Sheet1", "A1", 40))
6640+
assert.NoError(t, f.SetCellValue("Sheet1", "A2", 50))
6641+
assert.NoError(t, f.SetCellFormula("Sheet1", "A3", "A1+A2"))
6642+
6643+
result1, err := f.CalcCellValue("Sheet1", "A3")
6644+
assert.NoError(t, err)
6645+
assert.Equal(t, "90", result1)
6646+
6647+
result2, err := f.CalcCellValue("Sheet1", "A3")
6648+
assert.NoError(t, err)
6649+
assert.Equal(t, result1, result2, "cached result should be consistent")
6650+
6651+
assert.NoError(t, f.SetCellValue("Sheet1", "A1", 60))
6652+
6653+
result3, err := f.CalcCellValue("Sheet1", "A3")
6654+
assert.NoError(t, err)
6655+
assert.Equal(t, "110", result3)
6656+
assert.NotEqual(t, result1, result3, "result should be updated after cache clear")
6657+
})
6658+
t.Run("for_calc_call_value_with_multiple_dependent_cells", func(t *testing.T) {
6659+
f := NewFile()
6660+
assert.NoError(t, f.SetCellValue("Sheet1", "A1", 10))
6661+
assert.NoError(t, f.SetCellValue("Sheet1", "A2", 10))
6662+
assert.NoError(t, f.SetCellFormula("Sheet1", "A3", "A1+A2"))
6663+
assert.NoError(t, f.SetCellFormula("Sheet1", "A4", "A3*3"))
6664+
assert.NoError(t, f.SetCellFormula("Sheet1", "A5", "A3+A4"))
6665+
6666+
result3, err := f.CalcCellValue("Sheet1", "A3")
6667+
assert.NoError(t, err)
6668+
assert.Equal(t, "20", result3)
6669+
6670+
result4, err := f.CalcCellValue("Sheet1", "A4")
6671+
assert.NoError(t, err)
6672+
assert.Equal(t, "60", result4)
6673+
6674+
result5, err := f.CalcCellValue("Sheet1", "A5")
6675+
assert.NoError(t, err)
6676+
assert.Equal(t, "80", result5)
6677+
6678+
assert.NoError(t, f.SetCellValue("Sheet1", "A1", 20))
6679+
6680+
newResult3, err := f.CalcCellValue("Sheet1", "A3")
6681+
assert.NoError(t, err)
6682+
assert.Equal(t, "30", newResult3)
6683+
assert.NotEqual(t, result3, newResult3, "A3 should be updated")
6684+
6685+
newResult5, err := f.CalcCellValue("Sheet1", "A5")
6686+
assert.NoError(t, err)
6687+
assert.Equal(t, "120", newResult5)
6688+
assert.NotEqual(t, result5, newResult5, "A5 should be updated")
6689+
})
6690+
t.Run("for_clear_calculation_cache", func(t *testing.T) {
6691+
f := NewFile()
6692+
assert.NoError(t, f.SetCellValue("Sheet1", "A1", 10))
6693+
assert.NoError(t, f.SetCellFormula("Sheet1", "A2", "A1*2"))
6694+
6695+
result1, err := f.CalcCellValue("Sheet1", "A2")
6696+
assert.NoError(t, err)
6697+
assert.Equal(t, "20", result1)
6698+
6699+
result2, err := f.CalcCellValue("Sheet1", "A2")
6700+
assert.NoError(t, err)
6701+
assert.Equal(t, result1, result2, "results should be consistent from cache")
6702+
6703+
cases := []struct {
6704+
name string
6705+
fn func() error
6706+
}{
6707+
{"SetCellValue", func() error { return f.SetCellValue("Sheet1", "B1", 100) }},
6708+
{"SetCellInt", func() error { return f.SetCellInt("Sheet1", "B2", 200) }},
6709+
{"SetCellUint", func() error { return f.SetCellUint("Sheet1", "B3", 300) }},
6710+
{"SetCellFloat", func() error { return f.SetCellFloat("Sheet1", "B4", 3.14, 2, 64) }},
6711+
{"SetCellStr", func() error { return f.SetCellStr("Sheet1", "B5", "test") }},
6712+
{"SetCellBool", func() error { return f.SetCellBool("Sheet1", "B6", true) }},
6713+
{"SetCellDefault", func() error { return f.SetCellDefault("Sheet1", "B7", "default") }},
6714+
{"SetCellFormula", func() error { return f.SetCellFormula("Sheet1", "B8", "=1+1") }},
6715+
{"SetCellHyperLink", func() error {
6716+
return f.SetCellHyperLink("Sheet1", "B9", "https://github.com/xuri/excelize", "External")
6717+
}},
6718+
{"SetCellRichText", func() error {
6719+
runs := []RichTextRun{{Text: "Rich", Font: &Font{Bold: true}}}
6720+
return f.SetCellRichText("Sheet1", "B10", runs)
6721+
}},
6722+
{"SetSheetRow", func() error { return f.SetSheetRow("Sheet1", "C1", &[]interface{}{1, 2, 3}) }},
6723+
{"SetSheetCol", func() error { return f.SetSheetCol("Sheet1", "D1", &[]interface{}{4, 5, 6}) }},
6724+
}
6725+
for _, tc := range cases {
6726+
t.Run(tc.name, func(t *testing.T) {
6727+
_, err := f.CalcCellValue("Sheet1", "A2")
6728+
assert.NoError(t, err)
6729+
assert.NoError(t, tc.fn())
6730+
result, err := f.CalcCellValue("Sheet1", "A2")
6731+
assert.NoError(t, err)
6732+
assert.Equal(t, "20", result, "calculation should still work after cache clear")
6733+
})
6734+
}
6735+
})
6736+
}

cell.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ func (c *xlsxC) hasValue() bool {
182182

183183
// removeFormula delete formula for the cell.
184184
func (f *File) removeFormula(c *xlsxC, ws *xlsxWorksheet, sheet string) error {
185+
f.clearCalcCache()
185186
if c.F != nil && c.Vm == nil {
186187
sheetID := f.getSheetID(sheet)
187188
if err := f.deleteCalcChain(sheetID, c.R); err != nil {
@@ -794,6 +795,7 @@ func (f *File) SetCellFormula(sheet, cell, formula string, opts ...FormulaOpts)
794795
if err != nil {
795796
return err
796797
}
798+
f.clearCalcCache()
797799
if formula == "" {
798800
ws.deleteSharedFormula(c)
799801
c.F = nil
@@ -1369,6 +1371,7 @@ func (f *File) SetCellRichText(sheet, cell string, runs []RichTextRun) error {
13691371
if si.R, err = setRichText(runs); err != nil {
13701372
return err
13711373
}
1374+
f.clearCalcCache()
13721375
for idx, strItem := range sst.SI {
13731376
if reflect.DeepEqual(strItem, si) {
13741377
c.T, c.V = "s", strconv.Itoa(idx)

excelize.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ type File struct {
4141
streams map[string]*StreamWriter
4242
tempFiles sync.Map
4343
xmlAttr sync.Map
44+
calcCache sync.Map
45+
calcCacheMu sync.RWMutex
4446
CalcChain *xlsxCalcChain
4547
CharsetReader func(charset string, input io.Reader) (rdr io.Reader, err error)
4648
Comments map[string]*xlsxComments

merge.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ func (f *File) UnmergeCell(sheet, topLeftCell, bottomRightCell string) error {
115115
if err = ws.mergeOverlapCells(); err != nil {
116116
return err
117117
}
118+
f.clearCalcCache()
118119
i := 0
119120
for _, mergeCell := range ws.MergeCells.Cells {
120121
if rect2, _ := rangeRefToCoordinates(mergeCell.Ref); isOverlap(rect1, rect2) {

pivotTable.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ func (f *File) AddPivotTable(opts *PivotTableOptions) error {
163163
if err != nil {
164164
return err
165165
}
166-
166+
f.clearCalcCache()
167167
pivotTableID := f.countPivotTables() + 1
168168
pivotCacheID := f.countPivotCache() + 1
169169

@@ -1062,6 +1062,7 @@ func (f *File) DeletePivotTable(sheet, name string) error {
10621062
if err != nil {
10631063
return err
10641064
}
1065+
f.clearCalcCache()
10651066
pivotTableCaches := map[string]int{}
10661067
pivotTables, _ := f.getPivotTables()
10671068
for _, sheetPivotTables := range pivotTables {

sheet.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,7 @@ func (f *File) SetSheetName(source, target string) error {
384384
if target == source {
385385
return err
386386
}
387+
f.clearCalcCache()
387388
wb, _ := f.workbookReader()
388389
for k, v := range wb.Sheets.Sheet {
389390
if v.Name == source {
@@ -579,7 +580,7 @@ func (f *File) DeleteSheet(sheet string) error {
579580
if idx, _ := f.GetSheetIndex(sheet); f.SheetCount == 1 || idx == -1 {
580581
return nil
581582
}
582-
583+
f.clearCalcCache()
583584
wb, _ := f.workbookReader()
584585
wbRels, _ := f.relsReader(f.getWorkbookRelsPath())
585586
activeSheetName := f.GetSheetName(f.GetActiveSheetIndex())
@@ -768,6 +769,7 @@ func (f *File) copySheet(from, to int) error {
768769
if err != nil {
769770
return err
770771
}
772+
f.clearCalcCache()
771773
worksheet := &xlsxWorksheet{}
772774
deepcopy.Copy(worksheet, sheet)
773775
toSheetID := strconv.Itoa(f.getSheetID(f.GetSheetName(to)))
@@ -1771,6 +1773,7 @@ func (f *File) SetDefinedName(definedName *DefinedName) error {
17711773
if err != nil {
17721774
return err
17731775
}
1776+
f.clearCalcCache()
17741777
d := xlsxDefinedName{
17751778
Name: definedName.Name,
17761779
Comment: definedName.Comment,
@@ -1813,6 +1816,7 @@ func (f *File) DeleteDefinedName(definedName *DefinedName) error {
18131816
if err != nil {
18141817
return err
18151818
}
1819+
f.clearCalcCache()
18161820
if wb.DefinedNames != nil {
18171821
for idx, dn := range wb.DefinedNames.DefinedName {
18181822
scope := "Workbook"

table.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ func (f *File) AddTable(sheet string, table *Table) error {
118118
return err
119119
}
120120
f.addSheetNameSpace(sheet, SourceRelationship)
121+
f.clearCalcCache()
121122
if err = f.addTable(sheet, tableXML, coordinates[0], coordinates[1], coordinates[2], coordinates[3], tableID, options); err != nil {
122123
return err
123124
}
@@ -177,6 +178,7 @@ func (f *File) DeleteTable(name string) error {
177178
if err != nil {
178179
return err
179180
}
181+
f.clearCalcCache()
180182
for sheet, tables := range tbls {
181183
for _, table := range tables {
182184
if table.Name != name {

0 commit comments

Comments
 (0)