Skip to content

feat: add formula shared index #2118

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Apr 26, 2025
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
91 changes: 64 additions & 27 deletions cell.go
Original file line number Diff line number Diff line change
Expand Up @@ -689,7 +689,8 @@ func (f *File) getCellFormula(sheet, cell string, transformed bool) (string, err
return "", false, nil
}
if c.F.T == STCellFormulaTypeShared && c.F.Si != nil {
return getSharedFormula(x, *c.F.Si, c.R), true, nil
formula, err := getSharedFormula(x, *c.F.Si, c.R)
return formula, true, err
}
return c.F.Content, true, nil
})
Expand Down Expand Up @@ -793,6 +794,7 @@ func (f *File) SetCellFormula(sheet, cell, formula string, opts ...FormulaOpts)
return err
}
if formula == "" {
ws.deleteSharedFormula(c)
c.F = nil
return f.deleteCalcChain(f.getSheetID(sheet), cell)
}
Expand All @@ -815,7 +817,8 @@ func (f *File) SetCellFormula(sheet, cell, formula string, opts ...FormulaOpts)
}
}
if c.F.T == STCellFormulaTypeShared {
if err = ws.setSharedFormula(*opt.Ref); err != nil {
ws.deleteSharedFormula(c)
if err = ws.setSharedFormula(cell, *opt.Ref); err != nil {
return err
}
}
Expand Down Expand Up @@ -890,22 +893,28 @@ func (f *File) setArrayFormulaCells() error {
}

// setSharedFormula set shared formula for the cells.
func (ws *xlsxWorksheet) setSharedFormula(ref string) error {
func (ws *xlsxWorksheet) setSharedFormula(cell, ref string) error {
coordinates, err := rangeRefToCoordinates(ref)
if err != nil {
return err
}
_ = sortCoordinates(coordinates)
cnt := ws.countSharedFormula()
for c := coordinates[0]; c <= coordinates[2]; c++ {
for r := coordinates[1]; r <= coordinates[3]; r++ {
ws.prepareSheetXML(c, r)
cell := &ws.SheetData.Row[r-1].C[c-1]
if cell.F == nil {
cell.F = &xlsxF{}
si := ws.countSharedFormula()
for col := coordinates[0]; col <= coordinates[2]; col++ {
for rol := coordinates[1]; rol <= coordinates[3]; rol++ {
ws.prepareSheetXML(col, rol)
c := &ws.SheetData.Row[rol-1].C[col-1]
if c.F == nil {
c.F = &xlsxF{}
}
cell.F.T = STCellFormulaTypeShared
cell.F.Si = &cnt
c.F.T = STCellFormulaTypeShared
if c.R == cell {
if c.F.Ref != "" {
si = *c.F.Si
continue
}
}
c.F.Si = &si
}
}
return err
Expand All @@ -923,6 +932,23 @@ func (ws *xlsxWorksheet) countSharedFormula() (count int) {
return
}

// deleteSharedFormula delete shared formula cell from worksheet shared formula
// index cache and remove all shared cells formula which refer to the cell which
// containing the formula.
func (ws *xlsxWorksheet) deleteSharedFormula(c *xlsxC) {
if c.F != nil && c.F.Si != nil && c.F.Ref != "" {
si := *c.F.Si
ws.formulaSI.Delete(si)
for r, row := range ws.SheetData.Row {
for c, cell := range row.C {
if cell.F != nil && cell.F.Si != nil && *cell.F.Si == si && cell.F.Ref == "" {
ws.SheetData.Row[r].C[c].F = nil
}
}
}
}
}

// GetCellHyperLink gets a cell hyperlink based on the given worksheet name and
// cell reference. If the cell has a hyperlink, it will return 'true' and
// the link address, otherwise it will return 'false' and an empty link
Expand Down Expand Up @@ -1640,18 +1666,27 @@ func isOverlap(rect1, rect2 []int) bool {
cellInRange([]int{rect2[2], rect2[3]}, rect1)
}

// parseSharedFormula generate dynamic part of shared formula for target cell
// by given column and rows distance and origin shared formula.
func parseSharedFormula(dCol, dRow int, orig string) string {
// convertSharedFormula creates a non shared formula from the shared formula
// counterpart by given cell reference which not containing the formula.
func (c *xlsxC) convertSharedFormula(cell string) (string, error) {
col, row, err := CellNameToCoordinates(cell)
if err != nil {
return "", err
}
sharedCol, sharedRow, err := CellNameToCoordinates(c.R)
if err != nil {
return "", err
}
dCol, dRow := col-sharedCol, row-sharedRow
ps := efp.ExcelParser()
tokens := ps.Parse(string(orig))
for i := 0; i < len(tokens); i++ {
tokens := ps.Parse(c.F.Content)
for i := range tokens {
token := tokens[i]
if token.TType == efp.TokenTypeOperand && token.TSubType == efp.TokenSubTypeRange {
tokens[i].TValue = shiftCell(token.TValue, dCol, dRow)
}
}
return ps.Render()
return ps.Render(), nil
}

// getSharedFormula find a cell contains the same formula as another cell,
Expand All @@ -1662,21 +1697,23 @@ func parseSharedFormula(dCol, dRow int, orig string) string {
//
// Note that this function not validate ref tag to check the cell whether in
// allow range reference, and always return origin shared formula.
func getSharedFormula(ws *xlsxWorksheet, si int, cell string) string {
for row := 0; row < len(ws.SheetData.Row); row++ {
func getSharedFormula(ws *xlsxWorksheet, si int, cell string) (string, error) {
val, ok := ws.formulaSI.Load(si)

if ok {
return val.(*xlsxC).convertSharedFormula(cell)
}
for row := range ws.SheetData.Row {
r := &ws.SheetData.Row[row]
for column := 0; column < len(r.C); column++ {
for column := range r.C {
c := &r.C[column]
if c.F != nil && c.F.Ref != "" && c.F.T == STCellFormulaTypeShared && c.F.Si != nil && *c.F.Si == si {
col, row, _ := CellNameToCoordinates(cell)
sharedCol, sharedRow, _ := CellNameToCoordinates(c.R)
dCol := col - sharedCol
dRow := row - sharedRow
return parseSharedFormula(dCol, dRow, c.F.Content)
ws.formulaSI.Store(si, c)
return c.convertSharedFormula(cell)
}
}
}
return ""
return "", nil
}

// shiftCell returns the cell shifted according to dCol and dRow taking into
Expand Down
83 changes: 81 additions & 2 deletions cell_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -563,7 +563,7 @@ func TestGetValueFrom(t *testing.T) {
assert.NoError(t, err)
value, err := c.getValueFrom(f, sst, false)
assert.NoError(t, err)
assert.Equal(t, "", value)
assert.Empty(t, value)

c = xlsxC{T: "s", V: " 1 "}
value, err = c.getValueFrom(f, &xlsxSST{Count: 1, SI: []xlsxSI{{}, {T: &xlsxT{Val: "s"}}}}, false)
Expand Down Expand Up @@ -602,13 +602,17 @@ func TestGetCellFormula(t *testing.T) {
formula, err := f.GetCellFormula("Sheet1", "B3")
assert.NoError(t, err)
assert.Equal(t, expected, formula)
// Test get shared formula form cache
formula, err = f.GetCellFormula("Sheet1", "B3")
assert.NoError(t, err)
assert.Equal(t, expected, formula)
}

f.Sheet.Delete("xl/worksheets/sheet1.xml")
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>`))
formula, err := f.GetCellFormula("Sheet1", "B2")
assert.NoError(t, err)
assert.Equal(t, "", formula)
assert.Empty(t, formula)

// Test get array formula with invalid cell range reference
f = NewFile()
Expand All @@ -628,6 +632,81 @@ func TestGetCellFormula(t *testing.T) {
f.Sheet.Delete("xl/worksheets/sheet1.xml")
f.Pkg.Store("xl/worksheets/sheet1.xml", MacintoshCyrillicCharset)
assert.EqualError(t, f.setArrayFormulaCells(), "XML syntax error on line 1: invalid UTF-8")

// Test get shared formula after updated refer cell formula, the shared
// formula cell reference range covered the previous.
f = NewFile()
formulaType, ref = STCellFormulaTypeShared, "C2:C6"
assert.NoError(t, f.SetCellFormula("Sheet1", "C2", "=A2+B2", FormulaOpts{Ref: &ref, Type: &formulaType}))
formula, err = f.GetCellFormula("Sheet1", "C2")
assert.NoError(t, err)
assert.Equal(t, "A2+B2", formula)
formula, err = f.GetCellFormula("Sheet1", "C6")
assert.NoError(t, err)
assert.Equal(t, "A6+B6", formula)

formulaType, ref = STCellFormulaTypeShared, "C2:C8"
assert.NoError(t, f.SetCellFormula("Sheet1", "C2", "=A2*B2", FormulaOpts{Ref: &ref, Type: &formulaType}))
formula, err = f.GetCellFormula("Sheet1", "C2")
assert.NoError(t, err)
assert.Equal(t, "A2*B2", formula)
formula, err = f.GetCellFormula("Sheet1", "C8")
assert.NoError(t, err)
assert.Equal(t, "A8*B8", formula)
assert.NoError(t, f.Close())

// Test get shared formula after updated refer cell formula, the shared
// formula cell reference range not over the previous.
f = NewFile()
formulaType, ref = STCellFormulaTypeShared, "C2:C6"
assert.NoError(t, f.SetCellFormula("Sheet1", "C2", "=A2+B2", FormulaOpts{Ref: &ref, Type: &formulaType}))
formula, err = f.GetCellFormula("Sheet1", "C2")
assert.NoError(t, err)
assert.Equal(t, "A2+B2", formula)
formula, err = f.GetCellFormula("Sheet1", "C6")
assert.NoError(t, err)
assert.Equal(t, "A6+B6", formula)

formulaType, ref = STCellFormulaTypeShared, "C2:C4"
assert.NoError(t, f.SetCellFormula("Sheet1", "C2", "=A2*B2", FormulaOpts{Ref: &ref, Type: &formulaType}))
formula, err = f.GetCellFormula("Sheet1", "C2")
assert.NoError(t, err)
assert.Equal(t, "A2*B2", formula)
formula, err = f.GetCellFormula("Sheet1", "C6")
assert.NoError(t, err)
assert.Empty(t, formula)

// Test get shared formula after remove refer cell formula
f = NewFile()
formulaType, ref = STCellFormulaTypeShared, "C2:C6"
assert.NoError(t, f.SetCellFormula("Sheet1", "C2", "=A2+B2", FormulaOpts{Ref: &ref, Type: &formulaType}))

assert.NoError(t, f.SetCellFormula("Sheet1", "C2", ""))

formula, err = f.GetCellFormula("Sheet1", "C2")
assert.NoError(t, err)
assert.Empty(t, formula)
formula, err = f.GetCellFormula("Sheet1", "C6")
assert.NoError(t, err)
assert.Empty(t, formula)

formulaType, ref = STCellFormulaTypeShared, "C2:C8"
assert.NoError(t, f.SetCellFormula("Sheet1", "C2", "=A2*B2", FormulaOpts{Ref: &ref, Type: &formulaType}))
formula, err = f.GetCellFormula("Sheet1", "C2")
assert.NoError(t, err)
assert.Equal(t, "A2*B2", formula)
formula, err = f.GetCellFormula("Sheet1", "C8")
assert.NoError(t, err)
assert.Equal(t, "A8*B8", formula)
assert.NoError(t, f.Close())
}

func TestConvertSharedFormula(t *testing.T) {
c := xlsxC{R: "A"}
_, err := c.convertSharedFormula("A")
assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), err)
_, err = c.convertSharedFormula("A1")
assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), err)
}

func ExampleFile_SetCellFloat() {
Expand Down
1 change: 1 addition & 0 deletions col.go
Original file line number Diff line number Diff line change
Expand Up @@ -782,6 +782,7 @@ func (f *File) RemoveCol(sheet, col string) error {
if err != nil {
return err
}
ws.formulaSI.Clear()
for rowIdx := range ws.SheetData.Row {
rowData := &ws.SheetData.Row[rowIdx]
for colIdx := range rowData.C {
Expand Down
2 changes: 1 addition & 1 deletion datavalidation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ func TestDataValidation(t *testing.T) {
dv.Formula1 = ""
assert.NoError(t, dv.SetDropList(listValid),
"SetDropList failed for valid input %v", listValid)
assert.NotEqual(t, "", dv.Formula1,
assert.NotEmpty(t, dv.Formula1,
"Formula1 should not be empty for valid input %v", listValid)
}
assert.Equal(t, `"A&lt;,B&gt;,C"",D ,E',F"`, dv.Formula1)
Expand Down
12 changes: 6 additions & 6 deletions excelize_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,13 @@ func TestOpenFile(t *testing.T) {

f.SetActiveSheet(2)
// Test get cell formula with given rows number
_, err = f.GetCellFormula("Sheet1", "B19")
formula, err := f.GetCellFormula("Sheet1", "B19")
assert.NoError(t, err)
assert.Equal(t, "SUM(Sheet2!D2,Sheet2!D11)", formula)
// Test get cell formula with illegal worksheet name
_, err = f.GetCellFormula("Sheet2", "B20")
assert.NoError(t, err)
_, err = f.GetCellFormula("Sheet1", "B20")
formula, err = f.GetCellFormula("Sheet2", "B20")
assert.NoError(t, err)
assert.Empty(t, formula)

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

func TestGetSheetComments(t *testing.T) {
f := NewFile()
assert.Equal(t, "", f.getSheetComments("sheet0"))
assert.Empty(t, f.getSheetComments("sheet0"))
}

func TestGetActiveSheetIndex(t *testing.T) {
Expand Down Expand Up @@ -1414,7 +1414,7 @@ func TestProtectSheet(t *testing.T) {
assert.NoError(t, f.UnprotectSheet(sheetName, "password"))
// Test protect worksheet with empty password
assert.NoError(t, f.ProtectSheet(sheetName, &SheetProtectionOptions{}))
assert.Equal(t, "", ws.SheetProtection.Password)
assert.Empty(t, ws.SheetProtection.Password)
// Test protect worksheet with password exceeds the limit length
assert.EqualError(t, f.ProtectSheet(sheetName, &SheetProtectionOptions{
AlgorithmName: "MD4",
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ go 1.23.0
require (
github.com/richardlehane/mscfb v1.0.4
github.com/stretchr/testify v1.10.0
github.com/tiendc/go-deepcopy v1.5.1
github.com/tiendc/go-deepcopy v1.5.2
github.com/xuri/efp v0.0.0-20250227110027-3491fafc2b79
github.com/xuri/nfp v0.0.0-20250226145837-86d5fc24b2ba
golang.org/x/crypto v0.36.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM
github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tiendc/go-deepcopy v1.5.1 h1:5ymXIB8ReIywehne6oy3HgywC8LicXYucPBNnj5QQxE=
github.com/tiendc/go-deepcopy v1.5.1/go.mod h1:toXoeQoUqXOOS/X4sKuiAoSk6elIdqc0pN7MTgOOo2I=
github.com/tiendc/go-deepcopy v1.5.2 h1:fzTSgAOzxw4MFuDzvyxRDUsdwA7qs7FBTvgXVj28NpQ=
github.com/tiendc/go-deepcopy v1.5.2/go.mod h1:toXoeQoUqXOOS/X4sKuiAoSk6elIdqc0pN7MTgOOo2I=
github.com/xuri/efp v0.0.0-20250227110027-3491fafc2b79 h1:78nKszZqigiBRBVcoe/AuPzyLTWW5B+ltBaUX1rlIXA=
github.com/xuri/efp v0.0.0-20250227110027-3491fafc2b79/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
github.com/xuri/nfp v0.0.0-20250226145837-86d5fc24b2ba h1:DhIu6n3qU0joqG9f4IO6a/Gkerd+flXrmlJ+0yX2W8U=
Expand Down
4 changes: 2 additions & 2 deletions lib_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,12 +95,12 @@ func TestColumnNumberToName_OK(t *testing.T) {
func TestColumnNumberToName_Error(t *testing.T) {
out, err := ColumnNumberToName(-1)
if assert.Error(t, err) {
assert.Equal(t, "", out)
assert.Empty(t, out)
}

out, err = ColumnNumberToName(0)
if assert.Error(t, err) {
assert.Equal(t, "", out)
assert.Empty(t, out)
}

_, err = ColumnNumberToName(MaxColumns + 1)
Expand Down
4 changes: 2 additions & 2 deletions merge_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ func TestMergeCell(t *testing.T) {
assert.NoError(t, err)
// Merged cell ref is single coordinate
value, err = f.GetCellValue("Sheet2", "A6")
assert.Equal(t, "", value)
assert.Empty(t, value)
assert.NoError(t, err)
value, err = f.GetCellFormula("Sheet1", "G12")
assert.Equal(t, "SUM(Sheet1!B19,Sheet1!C19)", value)
Expand Down Expand Up @@ -104,7 +104,7 @@ func TestMergeCellOverlap(t *testing.T) {
assert.Len(t, mc, 1)
assert.Equal(t, "A1", mc[0].GetStartAxis())
assert.Equal(t, "D3", mc[0].GetEndAxis())
assert.Equal(t, "", mc[0].GetCellValue())
assert.Empty(t, mc[0].GetCellValue())
assert.NoError(t, f.Close())
}

Expand Down
3 changes: 2 additions & 1 deletion rows.go
Original file line number Diff line number Diff line change
Expand Up @@ -632,11 +632,12 @@ func (f *File) RemoveRow(sheet string, row int) error {
if err != nil {
return err
}
ws.formulaSI.Clear()
if row > len(ws.SheetData.Row) {
return f.adjustHelper(sheet, rows, row, -1)
}
keep := 0
for rowIdx := 0; rowIdx < len(ws.SheetData.Row); rowIdx++ {
for rowIdx := range ws.SheetData.Row {
v := &ws.SheetData.Row[rowIdx]
if v.R != row {
ws.SheetData.Row[keep] = *v
Expand Down
Loading