未验证 提交 a8cbcfa3 编写于 作者: R rjtee 提交者: GitHub

This closes #1306 and closes #1615 (#1698)

- Support adjust formula on inserting/deleting columns/rows
上级 05689d6a
......@@ -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)
}
......@@ -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"},
......
......@@ -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 {
......
......@@ -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
......
......@@ -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"`
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册