From 2512d3c1ac590e52d809f3bd8f39f3cf8272f7f9 Mon Sep 17 00:00:00 2001 From: DengY11 <212294929@qq.com> Date: Sun, 25 May 2025 21:30:50 +0800 Subject: [PATCH 1/5] add calc cache --- calc.go | 32 ++++++++ calc_test.go | 206 +++++++++++++++++++++++++++++++++++++++------------ cell.go | 12 +++ excelize.go | 2 + 4 files changed, 203 insertions(+), 49 deletions(-) diff --git a/calc.go b/calc.go index c36e500942..2aff20bef4 100644 --- a/calc.go +++ b/calc.go @@ -814,6 +814,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 @@ -836,14 +844,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 } @@ -18842,3 +18865,12 @@ func (fn *formulaFuncs) DISPIMG(argsList *list.List) formulaArg { } return argsList.Front().Value.(formulaArg) } + +func (f *File) clearCalcCache() { + f.calcCacheMu.Lock() + defer f.calcCacheMu.Unlock() + f.calcCache.Range(func(key, value interface{}) bool { + f.calcCache.Delete(key) + return true + }) +} diff --git a/calc_test.go b/calc_test.go index 5f24cd3544..8b6b1e6f9d 100644 --- a/calc_test.go +++ b/calc_test.go @@ -777,25 +777,25 @@ func TestCalcCellValue(t *testing.T) { "=ROUNDUP(-11.111,-1)": "-20", "=ROUNDUP(ROUNDUP(100,1),-1)": "100", // SEARCH - "=SEARCH(\"s\",F1)": "1", - "=SEARCH(\"s\",F1,2)": "5", - "=SEARCH(\"e\",F1)": "4", - "=SEARCH(\"e*\",F1)": "4", - "=SEARCH(\"?e\",F1)": "3", - "=SEARCH(\"??e\",F1)": "2", - "=SEARCH(6,F2)": "2", + "=SEARCH(\"s\",F1)": "1", + "=SEARCH(\"s\",F1,2)": "5", + "=SEARCH(\"e\",F1)": "4", + "=SEARCH(\"e*\",F1)": "4", + "=SEARCH(\"?e\",F1)": "3", + "=SEARCH(\"??e\",F1)": "2", + "=SEARCH(6,F2)": "2", "=SEARCH(\"?\",\"你好world\")": "1", "=SEARCH(\"?l\",\"你好world\")": "5", "=SEARCH(\"?+\",\"你好 1+2\")": "4", "=SEARCH(\" ?+\",\"你好 1+2\")": "3", // SEARCHB - "=SEARCHB(\"s\",F1)": "1", - "=SEARCHB(\"s\",F1,2)": "5", - "=SEARCHB(\"e\",F1)": "4", - "=SEARCHB(\"e*\",F1)": "4", - "=SEARCHB(\"?e\",F1)": "3", - "=SEARCHB(\"??e\",F1)": "2", - "=SEARCHB(6,F2)": "2", + "=SEARCHB(\"s\",F1)": "1", + "=SEARCHB(\"s\",F1,2)": "5", + "=SEARCHB(\"e\",F1)": "4", + "=SEARCHB(\"e*\",F1)": "4", + "=SEARCHB(\"?e\",F1)": "3", + "=SEARCHB(\"??e\",F1)": "2", + "=SEARCHB(6,F2)": "2", "=SEARCHB(\"?\",\"你好world\")": "5", "=SEARCHB(\"?l\",\"你好world\")": "7", "=SEARCHB(\"?+\",\"你好 1+2\")": "6", @@ -1764,16 +1764,16 @@ func TestCalcCellValue(t *testing.T) { "=FINDB(\"\",\"Original Text\",2)": "2", "=FINDB(\"s\",\"Sales\",2)": "5", // LEFT - "=LEFT(\"Original Text\")": "O", - "=LEFT(\"Original Text\",4)": "Orig", - "=LEFT(\"Original Text\",0)": "", - "=LEFT(\"Original Text\",13)": "Original Text", - "=LEFT(\"Original Text\",20)": "Original Text", - "=LEFT(\"オリジナルテキスト\")": "オ", - "=LEFT(\"オリジナルテキスト\",2)": "オリ", - "=LEFT(\"オリジナルテキスト\",5)": "オリジナル", - "=LEFT(\"オリジナルテキスト\",7)": "オリジナルテキ", - "=LEFT(\"オリジナルテキスト\",20)": "オリジナルテキスト", + "=LEFT(\"Original Text\")": "O", + "=LEFT(\"Original Text\",4)": "Orig", + "=LEFT(\"Original Text\",0)": "", + "=LEFT(\"Original Text\",13)": "Original Text", + "=LEFT(\"Original Text\",20)": "Original Text", + "=LEFT(\"オリジナルテキスト\")": "オ", + "=LEFT(\"オリジナルテキスト\",2)": "オリ", + "=LEFT(\"オリジナルテキスト\",5)": "オリジナル", + "=LEFT(\"オリジナルテキスト\",7)": "オリジナルテキ", + "=LEFT(\"オリジナルテキスト\",20)": "オリジナルテキスト", // LEFTB "=LEFTB(\"Original Text\")": "O", "=LEFTB(\"Original Text\",4)": "Orig", @@ -1781,16 +1781,16 @@ func TestCalcCellValue(t *testing.T) { "=LEFTB(\"Original Text\",13)": "Original Text", "=LEFTB(\"Original Text\",20)": "Original Text", // LEN - "=LEN(\"\")": "0", - "=LEN(D1)": "5", - "=LEN(\"テキスト\")": "4", - "=LEN(\"オリジナルテキスト\")": "9", - "=LEN(7+LEN(A1&B1&C1))": "1", - "=LEN(8+LEN(A1+(C1-B1)))": "2", + "=LEN(\"\")": "0", + "=LEN(D1)": "5", + "=LEN(\"テキスト\")": "4", + "=LEN(\"オリジナルテキスト\")": "9", + "=LEN(7+LEN(A1&B1&C1))": "1", + "=LEN(8+LEN(A1+(C1-B1)))": "2", // LENB - "=LENB(\"\")": "0", - "=LENB(D1)": "5", - "=LENB(\"テキスト\")": "8", + "=LENB(\"\")": "0", + "=LENB(D1)": "5", + "=LENB(\"テキスト\")": "8", "=LENB(\"オリジナルテキスト\")": "18", // LOWER "=LOWER(\"test\")": "test", @@ -1803,7 +1803,7 @@ func TestCalcCellValue(t *testing.T) { "=MID(\"255 years\",3,1)": "5", "=MID(\"text\",3,6)": "xt", "=MID(\"text\",6,0)": "", - "=MID(\"你好World\",5,1)": "r", + "=MID(\"你好World\",5,1)": "r", "=MID(\"\u30AA\u30EA\u30B8\u30CA\u30EB\u30C6\u30AD\u30B9\u30C8\",6,4)": "\u30C6\u30AD\u30B9\u30C8", "=MID(\"\u30AA\u30EA\u30B8\u30CA\u30EB\u30C6\u30AD\u30B9\u30C8\",3,5)": "\u30B8\u30CA\u30EB\u30C6\u30AD", // MIDB @@ -1812,7 +1812,7 @@ func TestCalcCellValue(t *testing.T) { "=MIDB(\"255 years\",3,1)": "5", "=MIDB(\"text\",3,6)": "xt", "=MIDB(\"text\",6,0)": "", - "=MIDB(\"你好World\",5,1)": "W", + "=MIDB(\"你好World\",5,1)": "W", "=MIDB(\"\u30AA\u30EA\u30B8\u30CA\u30EB\u30C6\u30AD\u30B9\u30C8\",6,4)": "\u30B8\u30CA", "=MIDB(\"\u30AA\u30EA\u30B8\u30CA\u30EB\u30C6\u30AD\u30B9\u30C8\",3,5)": "\u30EA\u30B8\xe3", // PROPER @@ -1835,16 +1835,16 @@ func TestCalcCellValue(t *testing.T) { "=REPT(\"*\",1)": "*", "=REPT(\"**\",2)": "****", // RIGHT - "=RIGHT(\"Original Text\")": "t", - "=RIGHT(\"Original Text\",4)": "Text", - "=RIGHT(\"Original Text\",0)": "", - "=RIGHT(\"Original Text\",13)": "Original Text", - "=RIGHT(\"Original Text\",20)": "Original Text", - "=RIGHT(\"オリジナルテキスト\")": "ト", - "=RIGHT(\"オリジナルテキスト\",2)": "スト", - "=RIGHT(\"オリジナルテキスト\",4)": "テキスト", - "=RIGHT(\"オリジナルテキスト\",7)": "ジナルテキスト", - "=RIGHT(\"オリジナルテキスト\",20)": "オリジナルテキスト", + "=RIGHT(\"Original Text\")": "t", + "=RIGHT(\"Original Text\",4)": "Text", + "=RIGHT(\"Original Text\",0)": "", + "=RIGHT(\"Original Text\",13)": "Original Text", + "=RIGHT(\"Original Text\",20)": "Original Text", + "=RIGHT(\"オリジナルテキスト\")": "ト", + "=RIGHT(\"オリジナルテキスト\",2)": "スト", + "=RIGHT(\"オリジナルテキスト\",4)": "テキスト", + "=RIGHT(\"オリジナルテキスト\",7)": "ジナルテキスト", + "=RIGHT(\"オリジナルテキスト\",20)": "オリジナルテキスト", // RIGHTB "=RIGHTB(\"Original Text\")": "t", "=RIGHTB(\"Original Text\",4)": "Text", @@ -2806,11 +2806,11 @@ func TestCalcCellValue(t *testing.T) { "=SEARCH(2,A1)": {"#VALUE!", "#VALUE!"}, "=SEARCH(1,A1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, // SEARCHB - "=SEARCHB()": {"#VALUE!", "SEARCHB requires at least 2 arguments"}, - "=SEARCHB(1,A1,1,1)": {"#VALUE!", "SEARCHB allows at most 3 arguments"}, - "=SEARCHB(2,A1)": {"#VALUE!", "#VALUE!"}, + "=SEARCHB()": {"#VALUE!", "SEARCHB requires at least 2 arguments"}, + "=SEARCHB(1,A1,1,1)": {"#VALUE!", "SEARCHB allows at most 3 arguments"}, + "=SEARCHB(2,A1)": {"#VALUE!", "#VALUE!"}, "=SEARCHB(\"?w\",\"你好world\")": {"#VALUE!", "#VALUE!"}, - "=SEARCHB(1,A1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=SEARCHB(1,A1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, // SEC "=_xlfn.SEC()": {"#VALUE!", "SEC requires 1 numeric argument"}, "=_xlfn.SEC(\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, @@ -6502,3 +6502,111 @@ func TestParseToken(t *testing.T) { efp.Token{TSubType: efp.TokenSubTypeRange, TValue: "1A"}, nil, nil, ).Error()) } + +// TestCalcCellValueCache tests the calculation cache functionality +func TestCalcCellValueCache(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") +} + +// TestCalcCacheMultipleCells tests cache functionality with multiple dependent cells +func TestCalcCacheMultipleCells(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") +} + +// TestSetFunctionsClearCache tests that all Set functions properly clear the cache +func TestSetFunctionsClearCache(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") + + testCases := []struct { + name string + setFunc 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", "http://example.com", "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 testCases { + t.Run(tc.name, func(t *testing.T) { + // Ensure cache is built + _, err := f.CalcCellValue("Sheet1", "A2") + assert.NoError(t, err) + assert.NoError(t, tc.setFunc()) + result, err := f.CalcCellValue("Sheet1", "A2") + assert.NoError(t, err) + assert.Equal(t, "20", result, "calculation should still work after cache clear") + }) + } +} diff --git a/cell.go b/cell.go index b389f73c7b..a51fac9ee3 100644 --- a/cell.go +++ b/cell.go @@ -128,6 +128,7 @@ func (f *File) GetCellType(sheet, cell string) (CellType, error) { // the cell value as number 0 or 60, then create and bind the date-time number // format style for the cell. func (f *File) SetCellValue(sheet, cell string, value interface{}) error { + f.clearCalcCache() var err error switch v := value.(type) { case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: @@ -292,6 +293,7 @@ func setCellDuration(value time.Duration) (t string, v string) { // SetCellInt provides a function to set int type value of a cell by given // worksheet name, cell reference and cell value. func (f *File) SetCellInt(sheet, cell string, value int64) error { + f.clearCalcCache() f.mu.Lock() ws, err := f.workSheetReader(sheet) if err != nil { @@ -320,6 +322,7 @@ func setCellInt(value int64) (t string, v string) { // SetCellUint provides a function to set uint type value of a cell by given // worksheet name, cell reference and cell value. func (f *File) SetCellUint(sheet, cell string, value uint64) error { + f.clearCalcCache() f.mu.Lock() ws, err := f.workSheetReader(sheet) if err != nil { @@ -349,6 +352,7 @@ func setCellUint(value uint64) (t string, v string) { // SetCellBool provides a function to set bool type value of a cell by given // worksheet name, cell reference and cell value. func (f *File) SetCellBool(sheet, cell string, value bool) error { + f.clearCalcCache() f.mu.Lock() ws, err := f.workSheetReader(sheet) if err != nil { @@ -389,6 +393,7 @@ func setCellBool(value bool) (t string, v string) { // var x float32 = 1.325 // f.SetCellFloat("Sheet1", "A1", float64(x), 2, 32) func (f *File) SetCellFloat(sheet, cell string, value float64, precision, bitSize int) error { + f.clearCalcCache() if math.IsNaN(value) || math.IsInf(value, 0) { return f.SetCellStr(sheet, cell, fmt.Sprint(value)) } @@ -424,6 +429,7 @@ func (c *xlsxC) setCellFloat(value float64, precision, bitSize int) { // SetCellStr provides a function to set string type value of a cell. Total // number of characters that a cell can contain 32767 characters. func (f *File) SetCellStr(sheet, cell, value string) error { + f.clearCalcCache() f.mu.Lock() ws, err := f.workSheetReader(sheet) if err != nil { @@ -650,6 +656,7 @@ func (c *xlsxC) getValueFrom(f *File, d *xlsxSST, raw bool) (string, error) { // SetCellDefault provides a function to set string type value of a cell as // default format without escaping the cell. func (f *File) SetCellDefault(sheet, cell, value string) error { + f.clearCalcCache() f.mu.Lock() ws, err := f.workSheetReader(sheet) if err != nil { @@ -787,6 +794,7 @@ type FormulaOpts struct { // } // } func (f *File) SetCellFormula(sheet, cell, formula string, opts ...FormulaOpts) error { + f.clearCalcCache() ws, err := f.workSheetReader(sheet) if err != nil { return err @@ -1044,6 +1052,7 @@ func (f *File) removeHyperLink(ws *xlsxWorksheet, sheet, cell string) error { // // err := f.SetCellHyperLink("Sheet1", "A3", "Sheet1!A40", "Location") func (f *File) SetCellHyperLink(sheet, cell, link, linkType string, opts ...HyperlinkOpts) error { + f.clearCalcCache() // Check for correct cell name if _, _, err := SplitCellName(cell); err != nil { return err @@ -1350,6 +1359,7 @@ func setRichText(runs []RichTextRun) ([]xlsxR, error) { // } // } func (f *File) SetCellRichText(sheet, cell string, runs []RichTextRun) error { + f.clearCalcCache() ws, err := f.workSheetReader(sheet) if err != nil { return err @@ -1390,6 +1400,7 @@ func (f *File) SetCellRichText(sheet, cell string, runs []RichTextRun) error { // // err := f.SetSheetRow("Sheet1", "B6", &[]interface{}{"1", nil, 2}) func (f *File) SetSheetRow(sheet, cell string, slice interface{}) error { + f.clearCalcCache() return f.setSheetCells(sheet, cell, slice, rows) } @@ -1399,6 +1410,7 @@ func (f *File) SetSheetRow(sheet, cell string, slice interface{}) error { // // err := f.SetSheetCol("Sheet1", "B6", &[]interface{}{"1", nil, 2}) func (f *File) SetSheetCol(sheet, cell string, slice interface{}) error { + f.clearCalcCache() return f.setSheetCells(sheet, cell, slice, columns) } diff --git a/excelize.go b/excelize.go index 61bb6d3489..6b978e3919 100644 --- a/excelize.go +++ b/excelize.go @@ -40,6 +40,8 @@ type File struct { streams map[string]*StreamWriter tempFiles sync.Map xmlAttr sync.Map + calcCache sync.Map + calcCacheMu sync.RWMutex CalcChain *xlsxCalcChain CharsetReader charsetTranscoderFn Comments map[string]*xlsxComments From 93c378ed587ffb236030764a57f962074728c63e Mon Sep 17 00:00:00 2001 From: DengY11 <212294929@qq.com> Date: Tue, 27 May 2025 21:29:35 +0800 Subject: [PATCH 2/5] consider more scenario needed del cache --- col.go | 2 ++ merge.go | 2 ++ rows.go | 4 ++++ sheet.go | 4 ++++ 4 files changed, 12 insertions(+) diff --git a/col.go b/col.go index d8c3d0dbe9..9ffc0fceaf 100644 --- a/col.go +++ b/col.go @@ -748,6 +748,7 @@ func (f *File) GetColWidth(sheet, col string) (float64, error) { // worksheet, it will cause a file error when you open it. The excelize only // partially updates these references currently. func (f *File) InsertCols(sheet, col string, n int) error { + f.clearCalcCache() num, err := ColumnNameToNumber(col) if err != nil { return err @@ -768,6 +769,7 @@ func (f *File) InsertCols(sheet, col string, n int) error { // worksheet, it will cause a file error when you open it. The excelize only // partially updates these references currently. func (f *File) RemoveCol(sheet, col string) error { + f.clearCalcCache() num, err := ColumnNameToNumber(col) if err != nil { return err diff --git a/merge.go b/merge.go index 9f2d25b04d..73bbb6695b 100644 --- a/merge.go +++ b/merge.go @@ -50,6 +50,7 @@ func (mc *xlsxMergeCell) Rect() ([]int, error) { // |A8(x3,y4) C8(x4,y4)| // +------------------------+ func (f *File) MergeCell(sheet, topLeftCell, bottomRightCell string) error { + f.clearCalcCache() rect, err := rangeRefToCoordinates(topLeftCell + ":" + bottomRightCell) if err != nil { return err @@ -94,6 +95,7 @@ func (f *File) MergeCell(sheet, topLeftCell, bottomRightCell string) error { // // Attention: overlapped range will also be unmerged. func (f *File) UnmergeCell(sheet, topLeftCell, bottomRightCell string) error { + f.clearCalcCache() ws, err := f.workSheetReader(sheet) if err != nil { return err diff --git a/rows.go b/rows.go index 5dbfaf86ea..f652ba2915 100644 --- a/rows.go +++ b/rows.go @@ -631,6 +631,7 @@ func (f *File) GetRowOutlineLevel(sheet string, row int) (uint8, error) { // worksheet, it will cause a file error when you open it. The excelize only // partially updates these references currently. func (f *File) RemoveRow(sheet string, row int) error { + f.clearCalcCache() if row < 1 { return newInvalidRowNumberError(row) } @@ -676,6 +677,7 @@ func (f *File) RemoveRow(sheet string, row int) error { // worksheet, it will cause a file error when you open it. The excelize only // partially updates these references currently. func (f *File) InsertRows(sheet string, row, n int) error { + f.clearCalcCache() if row < 1 { return newInvalidRowNumberError(row) } @@ -697,6 +699,7 @@ func (f *File) InsertRows(sheet string, row, n int) error { // worksheet, it will cause a file error when you open it. The excelize only // partially updates these references currently. func (f *File) DuplicateRow(sheet string, row int) error { + f.clearCalcCache() return f.DuplicateRowTo(sheet, row, row+1) } @@ -710,6 +713,7 @@ func (f *File) DuplicateRow(sheet string, row int) error { // worksheet, it will cause a file error when you open it. The excelize only // partially updates these references currently. func (f *File) DuplicateRowTo(sheet string, row, row2 int) error { + f.clearCalcCache() if row < 1 { return newInvalidRowNumberError(row) } diff --git a/sheet.go b/sheet.go index 65f2a4b008..8fc10d48a9 100644 --- a/sheet.go +++ b/sheet.go @@ -373,6 +373,7 @@ func (f *File) getActiveSheetID() int { // sheet name in the formula or reference associated with the cell. So there // may be problem formula error or reference missing. func (f *File) SetSheetName(source, target string) error { + f.clearCalcCache() var err error if err = checkSheetName(source); err != nil { return err @@ -572,6 +573,7 @@ func (f *File) setSheetBackground(sheet, extension string, file []byte) error { // value of the deleted worksheet, it will cause a file error when you open // it. This function will be invalid when only one worksheet is left. func (f *File) DeleteSheet(sheet string) error { + f.clearCalcCache() if err := checkSheetName(sheet); err != nil { return err } @@ -626,6 +628,7 @@ func (f *File) DeleteSheet(sheet string) error { // // err := f.MoveSheet("Sheet2", "Sheet1") func (f *File) MoveSheet(source, target string) error { + f.clearCalcCache() if strings.EqualFold(source, target) { return nil } @@ -753,6 +756,7 @@ func (f *File) getSheetRelationshipsTargetByID(sheet, rID string) string { // } // err := f.CopySheet(1, index) func (f *File) CopySheet(from, to int) error { + f.clearCalcCache() if from < 0 || to < 0 || from == to || f.GetSheetName(from) == "" || f.GetSheetName(to) == "" { return ErrSheetIdx } From 840ad9c3b046e6b6fe32547c6957ba602dfc0611 Mon Sep 17 00:00:00 2001 From: DengY11 <212294929@qq.com> Date: Mon, 2 Jun 2025 20:02:05 +0800 Subject: [PATCH 3/5] resolve conflict --- calc_test.go | 98 ++++++++++++++++++++++++++-------------------------- 1 file changed, 49 insertions(+), 49 deletions(-) diff --git a/calc_test.go b/calc_test.go index 8b6b1e6f9d..70d6f0aaef 100644 --- a/calc_test.go +++ b/calc_test.go @@ -777,25 +777,25 @@ func TestCalcCellValue(t *testing.T) { "=ROUNDUP(-11.111,-1)": "-20", "=ROUNDUP(ROUNDUP(100,1),-1)": "100", // SEARCH - "=SEARCH(\"s\",F1)": "1", - "=SEARCH(\"s\",F1,2)": "5", - "=SEARCH(\"e\",F1)": "4", - "=SEARCH(\"e*\",F1)": "4", - "=SEARCH(\"?e\",F1)": "3", - "=SEARCH(\"??e\",F1)": "2", - "=SEARCH(6,F2)": "2", + "=SEARCH(\"s\",F1)": "1", + "=SEARCH(\"s\",F1,2)": "5", + "=SEARCH(\"e\",F1)": "4", + "=SEARCH(\"e*\",F1)": "4", + "=SEARCH(\"?e\",F1)": "3", + "=SEARCH(\"??e\",F1)": "2", + "=SEARCH(6,F2)": "2", "=SEARCH(\"?\",\"你好world\")": "1", "=SEARCH(\"?l\",\"你好world\")": "5", "=SEARCH(\"?+\",\"你好 1+2\")": "4", "=SEARCH(\" ?+\",\"你好 1+2\")": "3", // SEARCHB - "=SEARCHB(\"s\",F1)": "1", - "=SEARCHB(\"s\",F1,2)": "5", - "=SEARCHB(\"e\",F1)": "4", - "=SEARCHB(\"e*\",F1)": "4", - "=SEARCHB(\"?e\",F1)": "3", - "=SEARCHB(\"??e\",F1)": "2", - "=SEARCHB(6,F2)": "2", + "=SEARCHB(\"s\",F1)": "1", + "=SEARCHB(\"s\",F1,2)": "5", + "=SEARCHB(\"e\",F1)": "4", + "=SEARCHB(\"e*\",F1)": "4", + "=SEARCHB(\"?e\",F1)": "3", + "=SEARCHB(\"??e\",F1)": "2", + "=SEARCHB(6,F2)": "2", "=SEARCHB(\"?\",\"你好world\")": "5", "=SEARCHB(\"?l\",\"你好world\")": "7", "=SEARCHB(\"?+\",\"你好 1+2\")": "6", @@ -1764,16 +1764,16 @@ func TestCalcCellValue(t *testing.T) { "=FINDB(\"\",\"Original Text\",2)": "2", "=FINDB(\"s\",\"Sales\",2)": "5", // LEFT - "=LEFT(\"Original Text\")": "O", - "=LEFT(\"Original Text\",4)": "Orig", - "=LEFT(\"Original Text\",0)": "", - "=LEFT(\"Original Text\",13)": "Original Text", - "=LEFT(\"Original Text\",20)": "Original Text", - "=LEFT(\"オリジナルテキスト\")": "オ", - "=LEFT(\"オリジナルテキスト\",2)": "オリ", - "=LEFT(\"オリジナルテキスト\",5)": "オリジナル", - "=LEFT(\"オリジナルテキスト\",7)": "オリジナルテキ", - "=LEFT(\"オリジナルテキスト\",20)": "オリジナルテキスト", + "=LEFT(\"Original Text\")": "O", + "=LEFT(\"Original Text\",4)": "Orig", + "=LEFT(\"Original Text\",0)": "", + "=LEFT(\"Original Text\",13)": "Original Text", + "=LEFT(\"Original Text\",20)": "Original Text", + "=LEFT(\"オリジナルテキスト\")": "オ", + "=LEFT(\"オリジナルテキスト\",2)": "オリ", + "=LEFT(\"オリジナルテキスト\",5)": "オリジナル", + "=LEFT(\"オリジナルテキスト\",7)": "オリジナルテキ", + "=LEFT(\"オリジナルテキスト\",20)": "オリジナルテキスト", // LEFTB "=LEFTB(\"Original Text\")": "O", "=LEFTB(\"Original Text\",4)": "Orig", @@ -1781,16 +1781,16 @@ func TestCalcCellValue(t *testing.T) { "=LEFTB(\"Original Text\",13)": "Original Text", "=LEFTB(\"Original Text\",20)": "Original Text", // LEN - "=LEN(\"\")": "0", - "=LEN(D1)": "5", - "=LEN(\"テキスト\")": "4", - "=LEN(\"オリジナルテキスト\")": "9", - "=LEN(7+LEN(A1&B1&C1))": "1", - "=LEN(8+LEN(A1+(C1-B1)))": "2", + "=LEN(\"\")": "0", + "=LEN(D1)": "5", + "=LEN(\"テキスト\")": "4", + "=LEN(\"オリジナルテキスト\")": "9", + "=LEN(7+LEN(A1&B1&C1))": "1", + "=LEN(8+LEN(A1+(C1-B1)))": "2", // LENB - "=LENB(\"\")": "0", - "=LENB(D1)": "5", - "=LENB(\"テキスト\")": "8", + "=LENB(\"\")": "0", + "=LENB(D1)": "5", + "=LENB(\"テキスト\")": "8", "=LENB(\"オリジナルテキスト\")": "18", // LOWER "=LOWER(\"test\")": "test", @@ -1803,7 +1803,7 @@ func TestCalcCellValue(t *testing.T) { "=MID(\"255 years\",3,1)": "5", "=MID(\"text\",3,6)": "xt", "=MID(\"text\",6,0)": "", - "=MID(\"你好World\",5,1)": "r", + "=MID(\"你好World\",5,1)": "r", "=MID(\"\u30AA\u30EA\u30B8\u30CA\u30EB\u30C6\u30AD\u30B9\u30C8\",6,4)": "\u30C6\u30AD\u30B9\u30C8", "=MID(\"\u30AA\u30EA\u30B8\u30CA\u30EB\u30C6\u30AD\u30B9\u30C8\",3,5)": "\u30B8\u30CA\u30EB\u30C6\u30AD", // MIDB @@ -1812,7 +1812,7 @@ func TestCalcCellValue(t *testing.T) { "=MIDB(\"255 years\",3,1)": "5", "=MIDB(\"text\",3,6)": "xt", "=MIDB(\"text\",6,0)": "", - "=MIDB(\"你好World\",5,1)": "W", + "=MIDB(\"你好World\",5,1)": "W", "=MIDB(\"\u30AA\u30EA\u30B8\u30CA\u30EB\u30C6\u30AD\u30B9\u30C8\",6,4)": "\u30B8\u30CA", "=MIDB(\"\u30AA\u30EA\u30B8\u30CA\u30EB\u30C6\u30AD\u30B9\u30C8\",3,5)": "\u30EA\u30B8\xe3", // PROPER @@ -1835,16 +1835,16 @@ func TestCalcCellValue(t *testing.T) { "=REPT(\"*\",1)": "*", "=REPT(\"**\",2)": "****", // RIGHT - "=RIGHT(\"Original Text\")": "t", - "=RIGHT(\"Original Text\",4)": "Text", - "=RIGHT(\"Original Text\",0)": "", - "=RIGHT(\"Original Text\",13)": "Original Text", - "=RIGHT(\"Original Text\",20)": "Original Text", - "=RIGHT(\"オリジナルテキスト\")": "ト", - "=RIGHT(\"オリジナルテキスト\",2)": "スト", - "=RIGHT(\"オリジナルテキスト\",4)": "テキスト", - "=RIGHT(\"オリジナルテキスト\",7)": "ジナルテキスト", - "=RIGHT(\"オリジナルテキスト\",20)": "オリジナルテキスト", + "=RIGHT(\"Original Text\")": "t", + "=RIGHT(\"Original Text\",4)": "Text", + "=RIGHT(\"Original Text\",0)": "", + "=RIGHT(\"Original Text\",13)": "Original Text", + "=RIGHT(\"Original Text\",20)": "Original Text", + "=RIGHT(\"オリジナルテキスト\")": "ト", + "=RIGHT(\"オリジナルテキスト\",2)": "スト", + "=RIGHT(\"オリジナルテキスト\",4)": "テキスト", + "=RIGHT(\"オリジナルテキスト\",7)": "ジナルテキスト", + "=RIGHT(\"オリジナルテキスト\",20)": "オリジナルテキスト", // RIGHTB "=RIGHTB(\"Original Text\")": "t", "=RIGHTB(\"Original Text\",4)": "Text", @@ -2806,11 +2806,11 @@ func TestCalcCellValue(t *testing.T) { "=SEARCH(2,A1)": {"#VALUE!", "#VALUE!"}, "=SEARCH(1,A1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, // SEARCHB - "=SEARCHB()": {"#VALUE!", "SEARCHB requires at least 2 arguments"}, - "=SEARCHB(1,A1,1,1)": {"#VALUE!", "SEARCHB allows at most 3 arguments"}, - "=SEARCHB(2,A1)": {"#VALUE!", "#VALUE!"}, + "=SEARCHB()": {"#VALUE!", "SEARCHB requires at least 2 arguments"}, + "=SEARCHB(1,A1,1,1)": {"#VALUE!", "SEARCHB allows at most 3 arguments"}, + "=SEARCHB(2,A1)": {"#VALUE!", "#VALUE!"}, "=SEARCHB(\"?w\",\"你好world\")": {"#VALUE!", "#VALUE!"}, - "=SEARCHB(1,A1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=SEARCHB(1,A1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, // SEC "=_xlfn.SEC()": {"#VALUE!", "SEC requires 1 numeric argument"}, "=_xlfn.SEC(\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, From 7597dc2b3842b874b517f1d6f253ad5d6ff6cf12 Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 27 Nov 2025 15:26:07 +0800 Subject: [PATCH 4/5] Fix code review issues --- adjust.go | 1 + calc.go | 16 ++--- calc_test.go | 178 ++++++++++++++++++++++++-------------------------- cell.go | 15 +---- col.go | 2 - merge.go | 4 +- pivotTable.go | 3 +- rows.go | 3 - sheet.go | 10 +-- table.go | 2 + 10 files changed, 108 insertions(+), 126 deletions(-) diff --git a/adjust.go b/adjust.go index cc8efb6825..05695736f9 100644 --- a/adjust.go +++ b/adjust.go @@ -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) diff --git a/calc.go b/calc.go index 6b82ea97de..f49be77c2f 100644 --- a/calc.go +++ b/calc.go @@ -1335,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 { @@ -19141,12 +19148,3 @@ func (fn *formulaFuncs) DISPIMG(argsList *list.List) formulaArg { } return argsList.Front().Value.(formulaArg) } - -func (f *File) clearCalcCache() { - f.calcCacheMu.Lock() - defer f.calcCacheMu.Unlock() - f.calcCache.Range(func(key, value interface{}) bool { - f.calcCache.Delete(key) - return true - }) -} diff --git a/calc_test.go b/calc_test.go index dc4d51e328..e7b55a14a1 100644 --- a/calc_test.go +++ b/calc_test.go @@ -6633,110 +6633,104 @@ func TestParseToken(t *testing.T) { ).Error()) } -// TestCalcCellValueCache tests the calculation cache functionality func TestCalcCellValueCache(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") -} - -// TestCalcCacheMultipleCells tests cache functionality with multiple dependent cells -func TestCalcCacheMultipleCells(t *testing.T) { - f := NewFile() + 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")) - 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")) + result1, err := f.CalcCellValue("Sheet1", "A3") + assert.NoError(t, err) + assert.Equal(t, "90", result1) - result3, err := f.CalcCellValue("Sheet1", "A3") - assert.NoError(t, err) - assert.Equal(t, "20", result3) + result2, err := f.CalcCellValue("Sheet1", "A3") + assert.NoError(t, err) + assert.Equal(t, result1, result2, "cached result should be consistent") - result4, err := f.CalcCellValue("Sheet1", "A4") - assert.NoError(t, err) - assert.Equal(t, "60", result4) + assert.NoError(t, f.SetCellValue("Sheet1", "A1", 60)) - result5, err := f.CalcCellValue("Sheet1", "A5") - assert.NoError(t, err) - assert.Equal(t, "80", result5) + 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")) - assert.NoError(t, f.SetCellValue("Sheet1", "A1", 20)) + result3, err := f.CalcCellValue("Sheet1", "A3") + assert.NoError(t, err) + assert.Equal(t, "20", result3) - newResult3, err := f.CalcCellValue("Sheet1", "A3") - assert.NoError(t, err) - assert.Equal(t, "30", newResult3) - assert.NotEqual(t, result3, newResult3, "A3 should be updated") + result4, err := f.CalcCellValue("Sheet1", "A4") + assert.NoError(t, err) + assert.Equal(t, "60", result4) - newResult5, err := f.CalcCellValue("Sheet1", "A5") - assert.NoError(t, err) - assert.Equal(t, "120", newResult5) - assert.NotEqual(t, result5, newResult5, "A5 should be updated") -} + result5, err := f.CalcCellValue("Sheet1", "A5") + assert.NoError(t, err) + assert.Equal(t, "80", result5) -// TestSetFunctionsClearCache tests that all Set functions properly clear the cache -func TestSetFunctionsClearCache(t *testing.T) { - f := NewFile() + assert.NoError(t, f.SetCellValue("Sheet1", "A1", 20)) - assert.NoError(t, f.SetCellValue("Sheet1", "A1", 10)) - assert.NoError(t, f.SetCellFormula("Sheet1", "A2", "A1*2")) + newResult3, err := f.CalcCellValue("Sheet1", "A3") + assert.NoError(t, err) + assert.Equal(t, "30", newResult3) + assert.NotEqual(t, result3, newResult3, "A3 should be updated") - result1, err := f.CalcCellValue("Sheet1", "A2") - assert.NoError(t, err) - assert.Equal(t, "20", result1) + 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")) - result2, err := f.CalcCellValue("Sheet1", "A2") - assert.NoError(t, err) - assert.Equal(t, result1, result2, "results should be consistent from cache") + result1, err := f.CalcCellValue("Sheet1", "A2") + assert.NoError(t, err) + assert.Equal(t, "20", result1) - testCases := []struct { - name string - setFunc 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", "http://example.com", "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}) }}, - } + result2, err := f.CalcCellValue("Sheet1", "A2") + assert.NoError(t, err) + assert.Equal(t, result1, result2, "results should be consistent from cache") - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - // Ensure cache is built - _, err := f.CalcCellValue("Sheet1", "A2") - assert.NoError(t, err) - assert.NoError(t, tc.setFunc()) - result, err := f.CalcCellValue("Sheet1", "A2") - assert.NoError(t, err) - assert.Equal(t, "20", result, "calculation should still work after cache clear") - }) - } + 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") + }) + } + }) } diff --git a/cell.go b/cell.go index 82c319a5ec..54266bc73b 100644 --- a/cell.go +++ b/cell.go @@ -127,7 +127,6 @@ func (f *File) GetCellType(sheet, cell string) (CellType, error) { // the cell value as number 0 or 60, then create and bind the date-time number // format style for the cell. func (f *File) SetCellValue(sheet, cell string, value interface{}) error { - f.clearCalcCache() var err error switch v := value.(type) { case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: @@ -183,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 { @@ -292,7 +292,6 @@ func setCellDuration(value time.Duration) (t string, v string) { // SetCellInt provides a function to set int type value of a cell by given // worksheet name, cell reference and cell value. func (f *File) SetCellInt(sheet, cell string, value int64) error { - f.clearCalcCache() f.mu.Lock() ws, err := f.workSheetReader(sheet) if err != nil { @@ -321,7 +320,6 @@ func setCellInt(value int64) (t string, v string) { // SetCellUint provides a function to set uint type value of a cell by given // worksheet name, cell reference and cell value. func (f *File) SetCellUint(sheet, cell string, value uint64) error { - f.clearCalcCache() f.mu.Lock() ws, err := f.workSheetReader(sheet) if err != nil { @@ -351,7 +349,6 @@ func setCellUint(value uint64) (t string, v string) { // SetCellBool provides a function to set bool type value of a cell by given // worksheet name, cell reference and cell value. func (f *File) SetCellBool(sheet, cell string, value bool) error { - f.clearCalcCache() f.mu.Lock() ws, err := f.workSheetReader(sheet) if err != nil { @@ -392,7 +389,6 @@ func setCellBool(value bool) (t string, v string) { // var x float32 = 1.325 // f.SetCellFloat("Sheet1", "A1", float64(x), 2, 32) func (f *File) SetCellFloat(sheet, cell string, value float64, precision, bitSize int) error { - f.clearCalcCache() if math.IsNaN(value) || math.IsInf(value, 0) { return f.SetCellStr(sheet, cell, fmt.Sprint(value)) } @@ -428,7 +424,6 @@ func (c *xlsxC) setCellFloat(value float64, precision, bitSize int) { // SetCellStr provides a function to set string type value of a cell. Total // number of characters that a cell can contain 32767 characters. func (f *File) SetCellStr(sheet, cell, value string) error { - f.clearCalcCache() f.mu.Lock() ws, err := f.workSheetReader(sheet) if err != nil { @@ -655,7 +650,6 @@ func (c *xlsxC) getValueFrom(f *File, d *xlsxSST, raw bool) (string, error) { // SetCellDefault provides a function to set string type value of a cell as // default format without escaping the cell. func (f *File) SetCellDefault(sheet, cell, value string) error { - f.clearCalcCache() f.mu.Lock() ws, err := f.workSheetReader(sheet) if err != nil { @@ -793,7 +787,6 @@ type FormulaOpts struct { // } // } func (f *File) SetCellFormula(sheet, cell, formula string, opts ...FormulaOpts) error { - f.clearCalcCache() ws, err := f.workSheetReader(sheet) if err != nil { return err @@ -802,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 @@ -1051,7 +1045,6 @@ func (f *File) removeHyperLink(ws *xlsxWorksheet, sheet, cell string) error { // // err := f.SetCellHyperLink("Sheet1", "A3", "Sheet1!A40", "Location") func (f *File) SetCellHyperLink(sheet, cell, link, linkType string, opts ...HyperlinkOpts) error { - f.clearCalcCache() // Check for correct cell name if _, _, err := SplitCellName(cell); err != nil { return err @@ -1358,7 +1351,6 @@ func setRichText(runs []RichTextRun) ([]xlsxR, error) { // } // } func (f *File) SetCellRichText(sheet, cell string, runs []RichTextRun) error { - f.clearCalcCache() ws, err := f.workSheetReader(sheet) if err != nil { return err @@ -1379,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) @@ -1399,7 +1392,6 @@ func (f *File) SetCellRichText(sheet, cell string, runs []RichTextRun) error { // // err := f.SetSheetRow("Sheet1", "B6", &[]interface{}{"1", nil, 2}) func (f *File) SetSheetRow(sheet, cell string, slice interface{}) error { - f.clearCalcCache() return f.setSheetCells(sheet, cell, slice, rows) } @@ -1409,7 +1401,6 @@ func (f *File) SetSheetRow(sheet, cell string, slice interface{}) error { // // err := f.SetSheetCol("Sheet1", "B6", &[]interface{}{"1", nil, 2}) func (f *File) SetSheetCol(sheet, cell string, slice interface{}) error { - f.clearCalcCache() return f.setSheetCells(sheet, cell, slice, columns) } diff --git a/col.go b/col.go index b88ef80e4b..96fae2188d 100644 --- a/col.go +++ b/col.go @@ -748,7 +748,6 @@ func (f *File) GetColWidth(sheet, col string) (float64, error) { // worksheet, it will cause a file error when you open it. The excelize only // partially updates these references currently. func (f *File) InsertCols(sheet, col string, n int) error { - f.clearCalcCache() num, err := ColumnNameToNumber(col) if err != nil { return err @@ -769,7 +768,6 @@ func (f *File) InsertCols(sheet, col string, n int) error { // worksheet, it will cause a file error when you open it. The excelize only // partially updates these references currently. func (f *File) RemoveCol(sheet, col string) error { - f.clearCalcCache() num, err := ColumnNameToNumber(col) if err != nil { return err diff --git a/merge.go b/merge.go index 9068da1dbf..4c51877d18 100644 --- a/merge.go +++ b/merge.go @@ -50,7 +50,6 @@ func (mc *xlsxMergeCell) Rect() ([]int, error) { // |A8(x3,y4) C8(x4,y4)| // +------------------------+ func (f *File) MergeCell(sheet, topLeftCell, bottomRightCell string) error { - f.clearCalcCache() rect, err := rangeRefToCoordinates(topLeftCell + ":" + bottomRightCell) if err != nil { return err @@ -67,6 +66,7 @@ func (f *File) MergeCell(sheet, topLeftCell, bottomRightCell string) error { } ws.mu.Lock() defer ws.mu.Unlock() + f.clearCalcCache() for col := rect[0]; col <= rect[2]; col++ { for row := rect[1]; row <= rect[3]; row++ { if col == rect[0] && row == rect[1] { @@ -95,7 +95,6 @@ func (f *File) MergeCell(sheet, topLeftCell, bottomRightCell string) error { // // Attention: overlapped range will also be unmerged. func (f *File) UnmergeCell(sheet, topLeftCell, bottomRightCell string) error { - f.clearCalcCache() ws, err := f.workSheetReader(sheet) if err != nil { return err @@ -117,6 +116,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) { diff --git a/pivotTable.go b/pivotTable.go index f2196575ac..fbf6aab7f6 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -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 @@ -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 { diff --git a/rows.go b/rows.go index 46a7abefa2..4e6ea8788a 100644 --- a/rows.go +++ b/rows.go @@ -680,7 +680,6 @@ func (f *File) RemoveRow(sheet string, row int) error { // worksheet, it will cause a file error when you open it. The excelize only // partially updates these references currently. func (f *File) InsertRows(sheet string, row, n int) error { - f.clearCalcCache() if row < 1 { return newInvalidRowNumberError(row) } @@ -702,7 +701,6 @@ func (f *File) InsertRows(sheet string, row, n int) error { // worksheet, it will cause a file error when you open it. The excelize only // partially updates these references currently. func (f *File) DuplicateRow(sheet string, row int) error { - f.clearCalcCache() return f.DuplicateRowTo(sheet, row, row+1) } @@ -716,7 +714,6 @@ func (f *File) DuplicateRow(sheet string, row int) error { // worksheet, it will cause a file error when you open it. The excelize only // partially updates these references currently. func (f *File) DuplicateRowTo(sheet string, row, row2 int) error { - f.clearCalcCache() if row < 1 { return newInvalidRowNumberError(row) } diff --git a/sheet.go b/sheet.go index 5eaf2532e3..e041705e95 100644 --- a/sheet.go +++ b/sheet.go @@ -374,7 +374,6 @@ func (f *File) getActiveSheetID() int { // sheet name in the formula or reference associated with the cell. So there // may be problem formula error or reference missing. func (f *File) SetSheetName(source, target string) error { - f.clearCalcCache() var err error if err = checkSheetName(source); err != nil { return err @@ -385,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 { @@ -574,14 +574,13 @@ func (f *File) setSheetBackground(sheet, extension string, file []byte) error { // value of the deleted worksheet, it will cause a file error when you open // it. This function will be invalid when only one worksheet is left. func (f *File) DeleteSheet(sheet string) error { - f.clearCalcCache() if err := checkSheetName(sheet); err != nil { return err } 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()) @@ -629,7 +628,6 @@ func (f *File) DeleteSheet(sheet string) error { // // err := f.MoveSheet("Sheet2", "Sheet1") func (f *File) MoveSheet(source, target string) error { - f.clearCalcCache() if strings.EqualFold(source, target) { return nil } @@ -757,7 +755,6 @@ func (f *File) getSheetRelationshipsTargetByID(sheet, rID string) string { // } // err = f.CopySheet(0, index) func (f *File) CopySheet(from, to int) error { - f.clearCalcCache() if from < 0 || to < 0 || from == to || f.GetSheetName(from) == "" || f.GetSheetName(to) == "" { return ErrSheetIdx } @@ -772,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))) @@ -1775,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, @@ -1817,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" diff --git a/table.go b/table.go index cf6a6b0470..8409f64253 100644 --- a/table.go +++ b/table.go @@ -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 } @@ -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 { From 83aeae4f3bfa45b9b9fad90dcb7010c5b6884978 Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 27 Nov 2025 15:38:07 +0800 Subject: [PATCH 5/5] Remove unnecessary clearCalcCache call --- merge.go | 1 - rows.go | 1 - 2 files changed, 2 deletions(-) diff --git a/merge.go b/merge.go index 4c51877d18..cf77ad2b4b 100644 --- a/merge.go +++ b/merge.go @@ -66,7 +66,6 @@ func (f *File) MergeCell(sheet, topLeftCell, bottomRightCell string) error { } ws.mu.Lock() defer ws.mu.Unlock() - f.clearCalcCache() for col := rect[0]; col <= rect[2]; col++ { for row := rect[1]; row <= rect[3]; row++ { if col == rect[0] && row == rect[1] { diff --git a/rows.go b/rows.go index 4e6ea8788a..5a994c8d52 100644 --- a/rows.go +++ b/rows.go @@ -634,7 +634,6 @@ func (f *File) GetRowOutlineLevel(sheet string, row int) (uint8, error) { // worksheet, it will cause a file error when you open it. The excelize only // partially updates these references currently. func (f *File) RemoveRow(sheet string, row int) error { - f.clearCalcCache() if row < 1 { return newInvalidRowNumberError(row) }