"encoding/xml"
"io"
"strings"
+
+ "github.com/xuri/efp"
)
type adjustDirection bool
}
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
// 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 {
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
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) {
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--
}
// 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 {
}
}
+// 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
}
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
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
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)
}
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()
"=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"},
if col < 1 || row < 1 {
return "", newCoordinatesToCellNameError(col, row)
}
+ if row > TotalRows {
+ return "", ErrMaxRows
+ }
sign := ""
for _, a := range abs {
if a {
}
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
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) {
// | 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"`