OSDN Git Service

This closes #1306 and closes #1615 (#1698)
authorrjtee <62975067+TeeRenJing@users.noreply.github.com>
Mon, 23 Oct 2023 16:05:52 +0000 (00:05 +0800)
committerGitHub <noreply@github.com>
Mon, 23 Oct 2023 16:05:52 +0000 (00:05 +0800)
- Support adjust formula on inserting/deleting columns/rows

adjust.go
adjust_test.go
calc.go
calc_test.go
lib.go
rows.go
rows_test.go
xmlCalcChain.go

index 5f40897..3708401 100644 (file)
--- a/adjust.go
+++ b/adjust.go
@@ -16,6 +16,8 @@ import (
        "encoding/xml"
        "io"
        "strings"
+
+       "github.com/xuri/efp"
 )
 
 type adjustDirection bool
@@ -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
@@ -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 {
@@ -131,9 +133,11 @@ 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)
@@ -141,40 +145,49 @@ func (f *File) adjustColDimensions(ws *xlsxWorksheet, col, offset int) error {
 
 // 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
@@ -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) {
@@ -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--
@@ -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 {
@@ -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
                }
@@ -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
index f6147e6..793659f 100644 (file)
@@ -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
@@ -449,11 +454,11 @@ 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)
@@ -461,7 +466,40 @@ func TestAdjustFormula(t *testing.T) {
        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)
 }
diff --git a/calc.go b/calc.go
index 1320238..1c1d8e9 100644 (file)
--- a/calc.go
+++ b/calc.go
@@ -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()
index 5e97a0e..336d085 100644 (file)
@@ -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"},
diff --git a/lib.go b/lib.go
index a694463..bc56422 100644 (file)
--- a/lib.go
+++ b/lib.go
@@ -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 {
diff --git a/rows.go b/rows.go
index 972707d..88d1f66 100644 (file)
--- a/rows.go
+++ b/rows.go
@@ -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
index 768f8b0..3e49580 100644 (file)
@@ -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) {
index 3631565..9c1d1ee 100644 (file)
@@ -76,7 +76,7 @@ type xlsxCalcChain struct {
 //                               | boolean datatype.
 type xlsxCalcChainC struct {
        R string `xml:"r,attr"`
-       I int    `xml:"i,attr"`
+       I int    `xml:"i,attr,omitempty"`
        L bool   `xml:"l,attr,omitempty"`
        S bool   `xml:"s,attr,omitempty"`
        T bool   `xml:"t,attr,omitempty"`