Skip to content

Commit b9f2c9e

Browse files
authored
This closes #1961, add shared formula cell cache for speedup calculation (#2118)
- Reuse formula shared index when update refer cell formula - Fix shared cell not been updated on update refer cell formula with new range - Upgrade dependencies package go-deepcopy to v1.5.2 - Update unit tests
1 parent 0f19d7f commit b9f2c9e

14 files changed

+177
-58
lines changed

cell.go

+64-27
Original file line numberDiff line numberDiff line change
@@ -689,7 +689,8 @@ func (f *File) getCellFormula(sheet, cell string, transformed bool) (string, err
689689
return "", false, nil
690690
}
691691
if c.F.T == STCellFormulaTypeShared && c.F.Si != nil {
692-
return getSharedFormula(x, *c.F.Si, c.R), true, nil
692+
formula, err := getSharedFormula(x, *c.F.Si, c.R)
693+
return formula, true, err
693694
}
694695
return c.F.Content, true, nil
695696
})
@@ -793,6 +794,7 @@ func (f *File) SetCellFormula(sheet, cell, formula string, opts ...FormulaOpts)
793794
return err
794795
}
795796
if formula == "" {
797+
ws.deleteSharedFormula(c)
796798
c.F = nil
797799
return f.deleteCalcChain(f.getSheetID(sheet), cell)
798800
}
@@ -815,7 +817,8 @@ func (f *File) SetCellFormula(sheet, cell, formula string, opts ...FormulaOpts)
815817
}
816818
}
817819
if c.F.T == STCellFormulaTypeShared {
818-
if err = ws.setSharedFormula(*opt.Ref); err != nil {
820+
ws.deleteSharedFormula(c)
821+
if err = ws.setSharedFormula(cell, *opt.Ref); err != nil {
819822
return err
820823
}
821824
}
@@ -890,22 +893,28 @@ func (f *File) setArrayFormulaCells() error {
890893
}
891894

892895
// setSharedFormula set shared formula for the cells.
893-
func (ws *xlsxWorksheet) setSharedFormula(ref string) error {
896+
func (ws *xlsxWorksheet) setSharedFormula(cell, ref string) error {
894897
coordinates, err := rangeRefToCoordinates(ref)
895898
if err != nil {
896899
return err
897900
}
898901
_ = sortCoordinates(coordinates)
899-
cnt := ws.countSharedFormula()
900-
for c := coordinates[0]; c <= coordinates[2]; c++ {
901-
for r := coordinates[1]; r <= coordinates[3]; r++ {
902-
ws.prepareSheetXML(c, r)
903-
cell := &ws.SheetData.Row[r-1].C[c-1]
904-
if cell.F == nil {
905-
cell.F = &xlsxF{}
902+
si := ws.countSharedFormula()
903+
for col := coordinates[0]; col <= coordinates[2]; col++ {
904+
for rol := coordinates[1]; rol <= coordinates[3]; rol++ {
905+
ws.prepareSheetXML(col, rol)
906+
c := &ws.SheetData.Row[rol-1].C[col-1]
907+
if c.F == nil {
908+
c.F = &xlsxF{}
906909
}
907-
cell.F.T = STCellFormulaTypeShared
908-
cell.F.Si = &cnt
910+
c.F.T = STCellFormulaTypeShared
911+
if c.R == cell {
912+
if c.F.Ref != "" {
913+
si = *c.F.Si
914+
continue
915+
}
916+
}
917+
c.F.Si = &si
909918
}
910919
}
911920
return err
@@ -923,6 +932,23 @@ func (ws *xlsxWorksheet) countSharedFormula() (count int) {
923932
return
924933
}
925934

935+
// deleteSharedFormula delete shared formula cell from worksheet shared formula
936+
// index cache and remove all shared cells formula which refer to the cell which
937+
// containing the formula.
938+
func (ws *xlsxWorksheet) deleteSharedFormula(c *xlsxC) {
939+
if c.F != nil && c.F.Si != nil && c.F.Ref != "" {
940+
si := *c.F.Si
941+
ws.formulaSI.Delete(si)
942+
for r, row := range ws.SheetData.Row {
943+
for c, cell := range row.C {
944+
if cell.F != nil && cell.F.Si != nil && *cell.F.Si == si && cell.F.Ref == "" {
945+
ws.SheetData.Row[r].C[c].F = nil
946+
}
947+
}
948+
}
949+
}
950+
}
951+
926952
// GetCellHyperLink gets a cell hyperlink based on the given worksheet name and
927953
// cell reference. If the cell has a hyperlink, it will return 'true' and
928954
// the link address, otherwise it will return 'false' and an empty link
@@ -1640,18 +1666,27 @@ func isOverlap(rect1, rect2 []int) bool {
16401666
cellInRange([]int{rect2[2], rect2[3]}, rect1)
16411667
}
16421668

1643-
// parseSharedFormula generate dynamic part of shared formula for target cell
1644-
// by given column and rows distance and origin shared formula.
1645-
func parseSharedFormula(dCol, dRow int, orig string) string {
1669+
// convertSharedFormula creates a non shared formula from the shared formula
1670+
// counterpart by given cell reference which not containing the formula.
1671+
func (c *xlsxC) convertSharedFormula(cell string) (string, error) {
1672+
col, row, err := CellNameToCoordinates(cell)
1673+
if err != nil {
1674+
return "", err
1675+
}
1676+
sharedCol, sharedRow, err := CellNameToCoordinates(c.R)
1677+
if err != nil {
1678+
return "", err
1679+
}
1680+
dCol, dRow := col-sharedCol, row-sharedRow
16461681
ps := efp.ExcelParser()
1647-
tokens := ps.Parse(string(orig))
1648-
for i := 0; i < len(tokens); i++ {
1682+
tokens := ps.Parse(c.F.Content)
1683+
for i := range tokens {
16491684
token := tokens[i]
16501685
if token.TType == efp.TokenTypeOperand && token.TSubType == efp.TokenSubTypeRange {
16511686
tokens[i].TValue = shiftCell(token.TValue, dCol, dRow)
16521687
}
16531688
}
1654-
return ps.Render()
1689+
return ps.Render(), nil
16551690
}
16561691

16571692
// getSharedFormula find a cell contains the same formula as another cell,
@@ -1662,21 +1697,23 @@ func parseSharedFormula(dCol, dRow int, orig string) string {
16621697
//
16631698
// Note that this function not validate ref tag to check the cell whether in
16641699
// allow range reference, and always return origin shared formula.
1665-
func getSharedFormula(ws *xlsxWorksheet, si int, cell string) string {
1666-
for row := 0; row < len(ws.SheetData.Row); row++ {
1700+
func getSharedFormula(ws *xlsxWorksheet, si int, cell string) (string, error) {
1701+
val, ok := ws.formulaSI.Load(si)
1702+
1703+
if ok {
1704+
return val.(*xlsxC).convertSharedFormula(cell)
1705+
}
1706+
for row := range ws.SheetData.Row {
16671707
r := &ws.SheetData.Row[row]
1668-
for column := 0; column < len(r.C); column++ {
1708+
for column := range r.C {
16691709
c := &r.C[column]
16701710
if c.F != nil && c.F.Ref != "" && c.F.T == STCellFormulaTypeShared && c.F.Si != nil && *c.F.Si == si {
1671-
col, row, _ := CellNameToCoordinates(cell)
1672-
sharedCol, sharedRow, _ := CellNameToCoordinates(c.R)
1673-
dCol := col - sharedCol
1674-
dRow := row - sharedRow
1675-
return parseSharedFormula(dCol, dRow, c.F.Content)
1711+
ws.formulaSI.Store(si, c)
1712+
return c.convertSharedFormula(cell)
16761713
}
16771714
}
16781715
}
1679-
return ""
1716+
return "", nil
16801717
}
16811718

16821719
// shiftCell returns the cell shifted according to dCol and dRow taking into

cell_test.go

+81-2
Original file line numberDiff line numberDiff line change
@@ -563,7 +563,7 @@ func TestGetValueFrom(t *testing.T) {
563563
assert.NoError(t, err)
564564
value, err := c.getValueFrom(f, sst, false)
565565
assert.NoError(t, err)
566-
assert.Equal(t, "", value)
566+
assert.Empty(t, value)
567567

568568
c = xlsxC{T: "s", V: " 1 "}
569569
value, err = c.getValueFrom(f, &xlsxSST{Count: 1, SI: []xlsxSI{{}, {T: &xlsxT{Val: "s"}}}}, false)
@@ -602,13 +602,17 @@ func TestGetCellFormula(t *testing.T) {
602602
formula, err := f.GetCellFormula("Sheet1", "B3")
603603
assert.NoError(t, err)
604604
assert.Equal(t, expected, formula)
605+
// Test get shared formula form cache
606+
formula, err = f.GetCellFormula("Sheet1", "B3")
607+
assert.NoError(t, err)
608+
assert.Equal(t, expected, formula)
605609
}
606610

607611
f.Sheet.Delete("xl/worksheets/sheet1.xml")
608612
f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><sheetData><row r="2"><c r="B2"><f t="shared" si="0"></f></c></row></sheetData></worksheet>`))
609613
formula, err := f.GetCellFormula("Sheet1", "B2")
610614
assert.NoError(t, err)
611-
assert.Equal(t, "", formula)
615+
assert.Empty(t, formula)
612616

613617
// Test get array formula with invalid cell range reference
614618
f = NewFile()
@@ -628,6 +632,81 @@ func TestGetCellFormula(t *testing.T) {
628632
f.Sheet.Delete("xl/worksheets/sheet1.xml")
629633
f.Pkg.Store("xl/worksheets/sheet1.xml", MacintoshCyrillicCharset)
630634
assert.EqualError(t, f.setArrayFormulaCells(), "XML syntax error on line 1: invalid UTF-8")
635+
636+
// Test get shared formula after updated refer cell formula, the shared
637+
// formula cell reference range covered the previous.
638+
f = NewFile()
639+
formulaType, ref = STCellFormulaTypeShared, "C2:C6"
640+
assert.NoError(t, f.SetCellFormula("Sheet1", "C2", "=A2+B2", FormulaOpts{Ref: &ref, Type: &formulaType}))
641+
formula, err = f.GetCellFormula("Sheet1", "C2")
642+
assert.NoError(t, err)
643+
assert.Equal(t, "A2+B2", formula)
644+
formula, err = f.GetCellFormula("Sheet1", "C6")
645+
assert.NoError(t, err)
646+
assert.Equal(t, "A6+B6", formula)
647+
648+
formulaType, ref = STCellFormulaTypeShared, "C2:C8"
649+
assert.NoError(t, f.SetCellFormula("Sheet1", "C2", "=A2*B2", FormulaOpts{Ref: &ref, Type: &formulaType}))
650+
formula, err = f.GetCellFormula("Sheet1", "C2")
651+
assert.NoError(t, err)
652+
assert.Equal(t, "A2*B2", formula)
653+
formula, err = f.GetCellFormula("Sheet1", "C8")
654+
assert.NoError(t, err)
655+
assert.Equal(t, "A8*B8", formula)
656+
assert.NoError(t, f.Close())
657+
658+
// Test get shared formula after updated refer cell formula, the shared
659+
// formula cell reference range not over the previous.
660+
f = NewFile()
661+
formulaType, ref = STCellFormulaTypeShared, "C2:C6"
662+
assert.NoError(t, f.SetCellFormula("Sheet1", "C2", "=A2+B2", FormulaOpts{Ref: &ref, Type: &formulaType}))
663+
formula, err = f.GetCellFormula("Sheet1", "C2")
664+
assert.NoError(t, err)
665+
assert.Equal(t, "A2+B2", formula)
666+
formula, err = f.GetCellFormula("Sheet1", "C6")
667+
assert.NoError(t, err)
668+
assert.Equal(t, "A6+B6", formula)
669+
670+
formulaType, ref = STCellFormulaTypeShared, "C2:C4"
671+
assert.NoError(t, f.SetCellFormula("Sheet1", "C2", "=A2*B2", FormulaOpts{Ref: &ref, Type: &formulaType}))
672+
formula, err = f.GetCellFormula("Sheet1", "C2")
673+
assert.NoError(t, err)
674+
assert.Equal(t, "A2*B2", formula)
675+
formula, err = f.GetCellFormula("Sheet1", "C6")
676+
assert.NoError(t, err)
677+
assert.Empty(t, formula)
678+
679+
// Test get shared formula after remove refer cell formula
680+
f = NewFile()
681+
formulaType, ref = STCellFormulaTypeShared, "C2:C6"
682+
assert.NoError(t, f.SetCellFormula("Sheet1", "C2", "=A2+B2", FormulaOpts{Ref: &ref, Type: &formulaType}))
683+
684+
assert.NoError(t, f.SetCellFormula("Sheet1", "C2", ""))
685+
686+
formula, err = f.GetCellFormula("Sheet1", "C2")
687+
assert.NoError(t, err)
688+
assert.Empty(t, formula)
689+
formula, err = f.GetCellFormula("Sheet1", "C6")
690+
assert.NoError(t, err)
691+
assert.Empty(t, formula)
692+
693+
formulaType, ref = STCellFormulaTypeShared, "C2:C8"
694+
assert.NoError(t, f.SetCellFormula("Sheet1", "C2", "=A2*B2", FormulaOpts{Ref: &ref, Type: &formulaType}))
695+
formula, err = f.GetCellFormula("Sheet1", "C2")
696+
assert.NoError(t, err)
697+
assert.Equal(t, "A2*B2", formula)
698+
formula, err = f.GetCellFormula("Sheet1", "C8")
699+
assert.NoError(t, err)
700+
assert.Equal(t, "A8*B8", formula)
701+
assert.NoError(t, f.Close())
702+
}
703+
704+
func TestConvertSharedFormula(t *testing.T) {
705+
c := xlsxC{R: "A"}
706+
_, err := c.convertSharedFormula("A")
707+
assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), err)
708+
_, err = c.convertSharedFormula("A1")
709+
assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), err)
631710
}
632711

633712
func ExampleFile_SetCellFloat() {

col.go

+1
Original file line numberDiff line numberDiff line change
@@ -782,6 +782,7 @@ func (f *File) RemoveCol(sheet, col string) error {
782782
if err != nil {
783783
return err
784784
}
785+
ws.formulaSI.Clear()
785786
for rowIdx := range ws.SheetData.Row {
786787
rowData := &ws.SheetData.Row[rowIdx]
787788
for colIdx := range rowData.C {

datavalidation_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ func TestDataValidation(t *testing.T) {
8181
dv.Formula1 = ""
8282
assert.NoError(t, dv.SetDropList(listValid),
8383
"SetDropList failed for valid input %v", listValid)
84-
assert.NotEqual(t, "", dv.Formula1,
84+
assert.NotEmpty(t, dv.Formula1,
8585
"Formula1 should not be empty for valid input %v", listValid)
8686
}
8787
assert.Equal(t, `"A&lt;,B&gt;,C"",D ,E',F"`, dv.Formula1)

excelize_test.go

+6-6
Original file line numberDiff line numberDiff line change
@@ -86,13 +86,13 @@ func TestOpenFile(t *testing.T) {
8686

8787
f.SetActiveSheet(2)
8888
// Test get cell formula with given rows number
89-
_, err = f.GetCellFormula("Sheet1", "B19")
89+
formula, err := f.GetCellFormula("Sheet1", "B19")
9090
assert.NoError(t, err)
91+
assert.Equal(t, "SUM(Sheet2!D2,Sheet2!D11)", formula)
9192
// Test get cell formula with illegal worksheet name
92-
_, err = f.GetCellFormula("Sheet2", "B20")
93-
assert.NoError(t, err)
94-
_, err = f.GetCellFormula("Sheet1", "B20")
93+
formula, err = f.GetCellFormula("Sheet2", "B20")
9594
assert.NoError(t, err)
95+
assert.Empty(t, formula)
9696

9797
// Test get cell formula with illegal rows number
9898
_, err = f.GetCellFormula("Sheet1", "B")
@@ -1060,7 +1060,7 @@ func TestCopySheetError(t *testing.T) {
10601060

10611061
func TestGetSheetComments(t *testing.T) {
10621062
f := NewFile()
1063-
assert.Equal(t, "", f.getSheetComments("sheet0"))
1063+
assert.Empty(t, f.getSheetComments("sheet0"))
10641064
}
10651065

10661066
func TestGetActiveSheetIndex(t *testing.T) {
@@ -1414,7 +1414,7 @@ func TestProtectSheet(t *testing.T) {
14141414
assert.NoError(t, f.UnprotectSheet(sheetName, "password"))
14151415
// Test protect worksheet with empty password
14161416
assert.NoError(t, f.ProtectSheet(sheetName, &SheetProtectionOptions{}))
1417-
assert.Equal(t, "", ws.SheetProtection.Password)
1417+
assert.Empty(t, ws.SheetProtection.Password)
14181418
// Test protect worksheet with password exceeds the limit length
14191419
assert.EqualError(t, f.ProtectSheet(sheetName, &SheetProtectionOptions{
14201420
AlgorithmName: "MD4",

go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ go 1.23.0
55
require (
66
github.com/richardlehane/mscfb v1.0.4
77
github.com/stretchr/testify v1.10.0
8-
github.com/tiendc/go-deepcopy v1.5.1
8+
github.com/tiendc/go-deepcopy v1.5.2
99
github.com/xuri/efp v0.0.0-20250227110027-3491fafc2b79
1010
github.com/xuri/nfp v0.0.0-20250226145837-86d5fc24b2ba
1111
golang.org/x/crypto v0.36.0

go.sum

+2-2
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM
99
github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
1010
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
1111
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
12-
github.com/tiendc/go-deepcopy v1.5.1 h1:5ymXIB8ReIywehne6oy3HgywC8LicXYucPBNnj5QQxE=
13-
github.com/tiendc/go-deepcopy v1.5.1/go.mod h1:toXoeQoUqXOOS/X4sKuiAoSk6elIdqc0pN7MTgOOo2I=
12+
github.com/tiendc/go-deepcopy v1.5.2 h1:fzTSgAOzxw4MFuDzvyxRDUsdwA7qs7FBTvgXVj28NpQ=
13+
github.com/tiendc/go-deepcopy v1.5.2/go.mod h1:toXoeQoUqXOOS/X4sKuiAoSk6elIdqc0pN7MTgOOo2I=
1414
github.com/xuri/efp v0.0.0-20250227110027-3491fafc2b79 h1:78nKszZqigiBRBVcoe/AuPzyLTWW5B+ltBaUX1rlIXA=
1515
github.com/xuri/efp v0.0.0-20250227110027-3491fafc2b79/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
1616
github.com/xuri/nfp v0.0.0-20250226145837-86d5fc24b2ba h1:DhIu6n3qU0joqG9f4IO6a/Gkerd+flXrmlJ+0yX2W8U=

lib_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -95,12 +95,12 @@ func TestColumnNumberToName_OK(t *testing.T) {
9595
func TestColumnNumberToName_Error(t *testing.T) {
9696
out, err := ColumnNumberToName(-1)
9797
if assert.Error(t, err) {
98-
assert.Equal(t, "", out)
98+
assert.Empty(t, out)
9999
}
100100

101101
out, err = ColumnNumberToName(0)
102102
if assert.Error(t, err) {
103-
assert.Equal(t, "", out)
103+
assert.Empty(t, out)
104104
}
105105

106106
_, err = ColumnNumberToName(MaxColumns + 1)

merge_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ func TestMergeCell(t *testing.T) {
3535
assert.NoError(t, err)
3636
// Merged cell ref is single coordinate
3737
value, err = f.GetCellValue("Sheet2", "A6")
38-
assert.Equal(t, "", value)
38+
assert.Empty(t, value)
3939
assert.NoError(t, err)
4040
value, err = f.GetCellFormula("Sheet1", "G12")
4141
assert.Equal(t, "SUM(Sheet1!B19,Sheet1!C19)", value)
@@ -104,7 +104,7 @@ func TestMergeCellOverlap(t *testing.T) {
104104
assert.Len(t, mc, 1)
105105
assert.Equal(t, "A1", mc[0].GetStartAxis())
106106
assert.Equal(t, "D3", mc[0].GetEndAxis())
107-
assert.Equal(t, "", mc[0].GetCellValue())
107+
assert.Empty(t, mc[0].GetCellValue())
108108
assert.NoError(t, f.Close())
109109
}
110110

rows.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -632,11 +632,12 @@ func (f *File) RemoveRow(sheet string, row int) error {
632632
if err != nil {
633633
return err
634634
}
635+
ws.formulaSI.Clear()
635636
if row > len(ws.SheetData.Row) {
636637
return f.adjustHelper(sheet, rows, row, -1)
637638
}
638639
keep := 0
639-
for rowIdx := 0; rowIdx < len(ws.SheetData.Row); rowIdx++ {
640+
for rowIdx := range ws.SheetData.Row {
640641
v := &ws.SheetData.Row[rowIdx]
641642
if v.R != row {
642643
ws.SheetData.Row[keep] = *v

0 commit comments

Comments
 (0)