- Support adjust formula on inserting/deleting columns/rows
This commit is contained in:
parent
05689d6ade
commit
a8cbcfa39b
142
adjust.go
142
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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
2
calc.go
2
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()
|
||||
|
|
|
@ -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"},
|
||||
|
|
3
lib.go
3
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 {
|
||||
|
|
2
rows.go
2
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
|
||||
|
|
15
rows_test.go
15
rows_test.go
|
@ -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) {
|
||||
|
|
|
@ -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"`
|
||||
|
|
Loading…
Reference in New Issue