Skip to content
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

Update adjustFormula #1698

Merged
merged 4 commits into from
Oct 23, 2023
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
142 changes: 118 additions & 24 deletions adjust.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import (
"encoding/xml"
"io"
"strings"

"github.com/xuri/efp"
)

type adjustDirection bool
Expand All @@ -42,9 +44,9 @@ func (f *File) adjustHelper(sheet string, dir adjustDirection, num, offset int)
}
sheetID := f.getSheetID(sheet)
if dir == rows {
err = f.adjustRowDimensions(ws, num, offset)
err = f.adjustRowDimensions(sheet, ws, num, offset)
} else {
err = f.adjustColDimensions(ws, num, offset)
err = f.adjustColDimensions(sheet, ws, num, offset)
}
if err != nil {
return err
Expand Down Expand Up @@ -116,7 +118,7 @@ func (f *File) adjustCols(ws *xlsxWorksheet, col, offset int) error {

// adjustColDimensions provides a function to update column dimensions when
// inserting or deleting rows or columns.
func (f *File) adjustColDimensions(ws *xlsxWorksheet, col, offset int) error {
func (f *File) adjustColDimensions(sheet string, ws *xlsxWorksheet, col, offset int) error {
for rowIdx := range ws.SheetData.Row {
for _, v := range ws.SheetData.Row[rowIdx].C {
if cellCol, _, _ := CellNameToCoordinates(v.R); col <= cellCol {
Expand All @@ -131,50 +133,61 @@ func (f *File) adjustColDimensions(ws *xlsxWorksheet, col, offset int) error {
if cellCol, cellRow, _ := CellNameToCoordinates(v.R); col <= cellCol {
if newCol := cellCol + offset; newCol > 0 {
ws.SheetData.Row[rowIdx].C[colIdx].R, _ = CoordinatesToCellName(newCol, cellRow)
_ = f.adjustFormula(ws.SheetData.Row[rowIdx].C[colIdx].F, columns, offset, false)
}
}
if err := f.adjustFormula(sheet, ws.SheetData.Row[rowIdx].C[colIdx].F, columns, col, offset, false); err != nil {
return err
}
}
}
return f.adjustCols(ws, col, offset)
}

// adjustRowDimensions provides a function to update row dimensions when
// inserting or deleting rows or columns.
func (f *File) adjustRowDimensions(ws *xlsxWorksheet, row, offset int) error {
func (f *File) adjustRowDimensions(sheet string, ws *xlsxWorksheet, row, offset int) error {
totalRows := len(ws.SheetData.Row)
if totalRows == 0 {
return nil
}
lastRow := &ws.SheetData.Row[totalRows-1]
if newRow := lastRow.R + offset; lastRow.R >= row && newRow > 0 && newRow >= TotalRows {
if newRow := lastRow.R + offset; lastRow.R >= row && newRow > 0 && newRow > TotalRows {
return ErrMaxRows
}
for i := 0; i < len(ws.SheetData.Row); i++ {
r := &ws.SheetData.Row[i]
if newRow := r.R + offset; r.R >= row && newRow > 0 {
f.adjustSingleRowDimensions(r, newRow, offset, false)
if err := f.adjustSingleRowDimensions(sheet, r, row, offset, false); err != nil {
return err
}
}
}
return nil
}

// adjustSingleRowDimensions provides a function to adjust single row dimensions.
func (f *File) adjustSingleRowDimensions(r *xlsxRow, num, offset int, si bool) {
r.R = num
func (f *File) adjustSingleRowDimensions(sheet string, r *xlsxRow, num, offset int, si bool) error {
r.R += offset
for i, col := range r.C {
colName, _, _ := SplitCellName(col.R)
r.C[i].R, _ = JoinCellName(colName, num)
_ = f.adjustFormula(col.F, rows, offset, si)
r.C[i].R, _ = JoinCellName(colName, r.R)
if err := f.adjustFormula(sheet, col.F, rows, num, offset, si); err != nil {
return err
}
}
return nil
}

// adjustFormula provides a function to adjust shared formula reference.
func (f *File) adjustFormula(formula *xlsxF, dir adjustDirection, offset int, si bool) error {
if formula != nil && formula.Ref != "" {
coordinates, err := rangeRefToCoordinates(formula.Ref)
// adjustFormula provides a function to adjust formula reference and shared
// formula reference.
func (f *File) adjustFormula(sheet string, formula *xlsxF, dir adjustDirection, num, offset int, si bool) error {
if formula == nil {
return nil
}
adjustRef := func(ref string) (string, error) {
coordinates, err := rangeRefToCoordinates(ref)
if err != nil {
return err
return ref, err
}
if dir == columns {
coordinates[0] += offset
Expand All @@ -183,16 +196,72 @@ func (f *File) adjustFormula(formula *xlsxF, dir adjustDirection, offset int, si
coordinates[1] += offset
coordinates[3] += offset
}
if formula.Ref, err = f.coordinatesToRangeRef(coordinates); err != nil {
return f.coordinatesToRangeRef(coordinates)
}
var err error
if formula.Ref != "" {
if formula.Ref, err = adjustRef(formula.Ref); err != nil {
return err
}
if si && formula.Si != nil {
formula.Si = intPtr(*formula.Si + 1)
}
}
if formula.T == STCellFormulaTypeArray {
formula.Content, err = adjustRef(strings.TrimPrefix(formula.Content, "="))
return err
}
if formula.Content != "" && !strings.ContainsAny(formula.Content, "[:]") {
content, err := f.adjustFormulaRef(sheet, formula.Content, dir, num, offset)
if err != nil {
return err
}
formula.Content = content
}
return nil
}

// adjustFormulaRef returns adjusted formula text by giving adjusting direction
// and the base number of column or row, and offset.
func (f *File) adjustFormulaRef(sheet string, text string, dir adjustDirection, num, offset int) (string, error) {
var (
formulaText string
definedNames []string
ps = efp.ExcelParser()
)
for _, definedName := range f.GetDefinedName() {
if definedName.Scope == "Workbook" || definedName.Scope == sheet {
definedNames = append(definedNames, definedName.Name)
}
}
for _, token := range ps.Parse(text) {
if token.TType == efp.TokenTypeOperand && token.TSubType == efp.TokenSubTypeRange {
if inStrSlice(definedNames, token.TValue, true) != -1 {
formulaText += token.TValue
continue
}
c, r, err := CellNameToCoordinates(token.TValue)
if err != nil {
return formulaText, err
}
if dir == columns && c >= num {
c += offset
}
if dir == rows {
r += offset
}
cell, err := CoordinatesToCellName(c, r, strings.Contains(token.TValue, "$"))
if err != nil {
return formulaText, err
}
formulaText += cell
continue
}
formulaText += token.TValue
}
return formulaText, nil
}

// adjustHyperlinks provides a function to update hyperlinks when inserting or
// deleting rows or columns.
func (f *File) adjustHyperlinks(ws *xlsxWorksheet, sheet string, dir adjustDirection, num, offset int) {
Expand Down Expand Up @@ -260,7 +329,7 @@ func (f *File) adjustTable(ws *xlsxWorksheet, sheet string, dir adjustDirection,
return
}
// Remove the table when deleting the header row of the table
if dir == rows && num == coordinates[0] {
if dir == rows && num == coordinates[0] && offset == -1 {
ws.TableParts.TableParts = append(ws.TableParts.TableParts[:idx], ws.TableParts.TableParts[idx+1:]...)
ws.TableParts.Count = len(ws.TableParts.TableParts)
idx--
Expand Down Expand Up @@ -316,8 +385,8 @@ func (f *File) adjustAutoFilter(ws *xlsxWorksheet, dir adjustDirection, num, off
}

// adjustAutoFilterHelper provides a function for adjusting auto filter to
// compare and calculate cell reference by the given adjust direction, operation
// reference and offset.
// compare and calculate cell reference by the giving adjusting direction,
// operation reference and offset.
func (f *File) adjustAutoFilterHelper(dir adjustDirection, coordinates []int, num, offset int) []int {
if dir == rows {
if coordinates[1] >= num {
Expand Down Expand Up @@ -422,13 +491,34 @@ func (f *File) deleteMergeCell(ws *xlsxWorksheet, idx int) {
}
}

// adjustCalcChainRef update the cell reference in calculation chain when
// inserting or deleting rows or columns.
func (f *File) adjustCalcChainRef(i, c, r, offset int, dir adjustDirection) {
if dir == rows {
if rn := r + offset; rn > 0 {
f.CalcChain.C[i].R, _ = CoordinatesToCellName(c, rn)
}
return
}
if nc := c + offset; nc > 0 {
f.CalcChain.C[i].R, _ = CoordinatesToCellName(nc, r)
}
}

// adjustCalcChain provides a function to update the calculation chain when
// inserting or deleting rows or columns.
func (f *File) adjustCalcChain(dir adjustDirection, num, offset, sheetID int) error {
if f.CalcChain == nil {
return nil
}
// If sheet ID is omitted, it is assumed to be the same as the i value of
// the previous cell.
var prevSheetID int
for index, c := range f.CalcChain.C {
if c.I == 0 {
c.I = prevSheetID
}
prevSheetID = c.I
if c.I != sheetID {
continue
}
Expand All @@ -437,14 +527,18 @@ func (f *File) adjustCalcChain(dir adjustDirection, num, offset, sheetID int) er
return err
}
if dir == rows && num <= rowNum {
if newRow := rowNum + offset; newRow > 0 {
f.CalcChain.C[index].R, _ = CoordinatesToCellName(colNum, newRow)
if num == rowNum && offset == -1 {
_ = f.deleteCalcChain(c.I, c.R)
continue
}
f.adjustCalcChainRef(index, colNum, rowNum, offset, dir)
}
if dir == columns && num <= colNum {
if newCol := colNum + offset; newCol > 0 {
f.CalcChain.C[index].R, _ = CoordinatesToCellName(newCol, rowNum)
if num == colNum && offset == -1 {
_ = f.deleteCalcChain(c.I, c.R)
continue
}
f.adjustCalcChainRef(index, colNum, rowNum, offset, dir)
}
}
return nil
Expand Down
54 changes: 46 additions & 8 deletions adjust_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -357,13 +357,18 @@ func TestAdjustHelper(t *testing.T) {
func TestAdjustCalcChain(t *testing.T) {
f := NewFile()
f.CalcChain = &xlsxCalcChain{
C: []xlsxCalcChainC{
{R: "B2", I: 2}, {R: "B2", I: 1},
},
C: []xlsxCalcChainC{{R: "B2", I: 2}, {R: "B2", I: 1}, {R: "A1", I: 1}},
}
assert.NoError(t, f.InsertCols("Sheet1", "A", 1))
assert.NoError(t, f.InsertRows("Sheet1", 1, 1))

f.CalcChain = &xlsxCalcChain{
C: []xlsxCalcChainC{{R: "B2", I: 1}, {R: "B3"}, {R: "A1"}},
}
assert.NoError(t, f.RemoveRow("Sheet1", 3))
assert.NoError(t, f.RemoveCol("Sheet1", "B"))

f.CalcChain = &xlsxCalcChain{C: []xlsxCalcChainC{{R: "B2", I: 2}, {R: "B2", I: 1}}}
f.CalcChain.C[1].R = "invalid coordinates"
assert.Equal(t, f.InsertCols("Sheet1", "A", 1), newCellNameToCoordinatesError("invalid coordinates", newInvalidCellNameError("invalid coordinates")))
f.CalcChain = nil
Expand Down Expand Up @@ -449,19 +454,52 @@ func TestAdjustCols(t *testing.T) {
func TestAdjustFormula(t *testing.T) {
f := NewFile()
formulaType, ref := STCellFormulaTypeShared, "C1:C5"
assert.NoError(t, f.SetCellFormula("Sheet1", "C1", "=A1+B1", FormulaOpts{Ref: &ref, Type: &formulaType}))
assert.NoError(t, f.SetCellFormula("Sheet1", "C1", "A1+B1", FormulaOpts{Ref: &ref, Type: &formulaType}))
assert.NoError(t, f.DuplicateRowTo("Sheet1", 1, 10))
assert.NoError(t, f.InsertCols("Sheet1", "B", 1))
assert.NoError(t, f.InsertRows("Sheet1", 1, 1))
for cell, expected := range map[string]string{"D2": "=A1+B1", "D3": "=A2+B2", "D11": "=A1+B1"} {
for cell, expected := range map[string]string{"D2": "A2+C2", "D3": "A3+C3", "D11": "A11+C11"} {
formula, err := f.GetCellFormula("Sheet1", cell)
assert.NoError(t, err)
assert.Equal(t, expected, formula)
}
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAdjustFormula.xlsx")))
assert.NoError(t, f.Close())

assert.NoError(t, f.adjustFormula(nil, rows, 0, false))
assert.Equal(t, f.adjustFormula(&xlsxF{Ref: "-"}, rows, 0, false), ErrParameterInvalid)
assert.Equal(t, f.adjustFormula(&xlsxF{Ref: "XFD1:XFD1"}, columns, 1, false), ErrColumnNumber)
assert.NoError(t, f.adjustFormula("Sheet1", nil, rows, 0, 0, false))
assert.Equal(t, ErrParameterInvalid, f.adjustFormula("Sheet1", &xlsxF{Ref: "-"}, rows, 0, 0, false))
assert.Equal(t, ErrColumnNumber, f.adjustFormula("Sheet1", &xlsxF{Ref: "XFD1:XFD1"}, columns, 0, 1, false))

_, err := f.adjustFormulaRef("Sheet1", "XFE1", columns, 0, 1)
assert.Equal(t, ErrColumnNumber, err)
_, err = f.adjustFormulaRef("Sheet1", "XFD1", columns, 0, 1)
assert.Equal(t, ErrColumnNumber, err)

f = NewFile()
assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "XFD1"))
assert.Equal(t, ErrColumnNumber, f.InsertCols("Sheet1", "A", 1))

assert.NoError(t, f.SetCellFormula("Sheet1", "B2", fmt.Sprintf("A%d", TotalRows)))
assert.Equal(t, ErrMaxRows, f.InsertRows("Sheet1", 1, 1))

// Test adjust formula with defined name in formula text
f = NewFile()
assert.NoError(t, f.SetDefinedName(&DefinedName{
Name: "Amount",
RefersTo: "Sheet1!$B$2",
}))
assert.NoError(t, f.SetCellFormula("Sheet1", "B2", "Amount+B3"))
assert.NoError(t, f.RemoveRow("Sheet1", 1))
formula, err := f.GetCellFormula("Sheet1", "B1")
assert.NoError(t, err)
assert.Equal(t, "Amount+B2", formula)

// Test adjust formula with array formula
f = NewFile()
formulaType, reference := STCellFormulaTypeArray, "A3:A3"
assert.NoError(t, f.SetCellFormula("Sheet1", "A3", "=A1:A2", FormulaOpts{Ref: &reference, Type: &formulaType}))
assert.NoError(t, f.InsertRows("Sheet1", 1, 1))
formula, err = f.GetCellFormula("Sheet1", "A4")
assert.NoError(t, err)
assert.Equal(t, "A2:A3", formula)
}
2 changes: 1 addition & 1 deletion calc.go
Original file line number Diff line number Diff line change
Expand Up @@ -14454,7 +14454,7 @@ func (fn *formulaFuncs) ADDRESS(argsList *list.List) formulaArg {
if rowNum.Type != ArgNumber {
return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
}
if rowNum.Number >= TotalRows {
if rowNum.Number > TotalRows {
return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
}
colNum := argsList.Front().Next().Value.(formulaArg).ToNumber()
Expand Down
2 changes: 1 addition & 1 deletion calc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3970,7 +3970,7 @@ func TestCalcCellValue(t *testing.T) {
"=ADDRESS(1,1,0,TRUE)": {"#NUM!", "#NUM!"},
"=ADDRESS(1,16385,2,TRUE)": {"#VALUE!", "#VALUE!"},
"=ADDRESS(1,16385,3,TRUE)": {"#VALUE!", "#VALUE!"},
"=ADDRESS(1048576,1,1,TRUE)": {"#VALUE!", "#VALUE!"},
"=ADDRESS(1048577,1,1,TRUE)": {"#VALUE!", "#VALUE!"},
// CHOOSE
"=CHOOSE()": {"#VALUE!", "CHOOSE requires 2 arguments"},
"=CHOOSE(\"index_num\",0)": {"#VALUE!", "CHOOSE requires first argument of type number"},
Expand Down
3 changes: 3 additions & 0 deletions lib.go
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,9 @@ func CoordinatesToCellName(col, row int, abs ...bool) (string, error) {
if col < 1 || row < 1 {
return "", newCoordinatesToCellNameError(col, row)
}
if row > TotalRows {
return "", ErrMaxRows
}
sign := ""
for _, a := range abs {
if a {
Expand Down
2 changes: 1 addition & 1 deletion rows.go
Original file line number Diff line number Diff line change
Expand Up @@ -652,7 +652,7 @@ func (f *File) DuplicateRowTo(sheet string, row, row2 int) error {
}

rowCopy.C = append(make([]xlsxC, 0, len(rowCopy.C)), rowCopy.C...)
f.adjustSingleRowDimensions(&rowCopy, row2, row2-row, true)
_ = f.adjustSingleRowDimensions(sheet, &rowCopy, row, row2-row, true)

if idx2 != -1 {
ws.SheetData.Row[idx2] = rowCopy
Expand Down
15 changes: 15 additions & 0 deletions rows_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -870,6 +870,21 @@ func TestDuplicateRow(t *testing.T) {
f := NewFile()
// Test duplicate row with invalid sheet name
assert.EqualError(t, f.DuplicateRowTo("Sheet:1", 1, 2), ErrSheetNameInvalid.Error())

f = NewFile()
assert.NoError(t, f.SetDefinedName(&DefinedName{
Name: "Amount",
RefersTo: "Sheet1!$B$1",
}))
assert.NoError(t, f.SetCellFormula("Sheet1", "A1", "Amount+C1"))
assert.NoError(t, f.SetCellValue("Sheet1", "A10", "A10"))
assert.NoError(t, f.DuplicateRowTo("Sheet1", 1, 10))
formula, err := f.GetCellFormula("Sheet1", "A10")
assert.NoError(t, err)
assert.Equal(t, "Amount+C10", formula)
value, err := f.GetCellValue("Sheet1", "A11")
assert.NoError(t, err)
assert.Equal(t, "A10", value)
}

func TestDuplicateRowTo(t *testing.T) {
Expand Down
Loading
Loading