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

Support adjust formula for entire cols/rows reference (#1702)

- Update the unit tests
上级 a8cbcfa3
......@@ -15,6 +15,7 @@ import (
"bytes"
"encoding/xml"
"io"
"strconv"
"strings"
"github.com/xuri/efp"
......@@ -154,23 +155,31 @@ func (f *File) adjustRowDimensions(sheet string, ws *xlsxWorksheet, row, offset
if newRow := lastRow.R + offset; lastRow.R >= row && newRow > 0 && newRow > TotalRows {
return ErrMaxRows
}
for i := 0; i < len(ws.SheetData.Row); i++ {
numOfRows := len(ws.SheetData.Row)
for i := 0; i < numOfRows; i++ {
r := &ws.SheetData.Row[i]
if newRow := r.R + offset; r.R >= row && newRow > 0 {
if err := f.adjustSingleRowDimensions(sheet, r, row, offset, false); err != nil {
return err
}
r.adjustSingleRowDimensions(offset)
}
if err := f.adjustSingleRowFormulas(sheet, r, row, offset, false); err != nil {
return err
}
}
return nil
}
// adjustSingleRowDimensions provides a function to adjust single row dimensions.
func (f *File) adjustSingleRowDimensions(sheet string, r *xlsxRow, num, offset int, si bool) error {
func (r *xlsxRow) adjustSingleRowDimensions(offset int) {
r.R += offset
for i, col := range r.C {
colName, _, _ := SplitCellName(col.R)
r.C[i].R, _ = JoinCellName(colName, r.R)
}
}
// adjustSingleRowFormulas provides a function to adjust single row formulas.
func (f *File) adjustSingleRowFormulas(sheet string, r *xlsxRow, num, offset int, si bool) error {
for _, col := range r.C {
if err := f.adjustFormula(sheet, col.F, rows, num, offset, si); err != nil {
return err
}
......@@ -178,54 +187,151 @@ func (f *File) adjustSingleRowDimensions(sheet string, r *xlsxRow, num, offset i
return nil
}
// 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
// adjustCellRef provides a function to adjust cell reference.
func (f *File) adjustCellRef(ref string, dir adjustDirection, num, offset int) (string, error) {
if !strings.Contains(ref, ":") {
ref += ":" + ref
}
adjustRef := func(ref string) (string, error) {
coordinates, err := rangeRefToCoordinates(ref)
if err != nil {
return ref, err
}
if dir == columns {
coordinates, err := rangeRefToCoordinates(ref)
if err != nil {
return ref, err
}
if dir == columns {
if coordinates[0] >= num {
coordinates[0] += offset
}
if coordinates[2] >= num {
coordinates[2] += offset
} else {
}
} else {
if coordinates[1] >= num {
coordinates[1] += offset
}
if coordinates[3] >= num {
coordinates[3] += offset
}
return f.coordinatesToRangeRef(coordinates)
}
return f.coordinatesToRangeRef(coordinates)
}
// 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
}
var err error
if formula.Ref != "" {
if formula.Ref, err = adjustRef(formula.Ref); err != nil {
if formula.Ref, err = f.adjustCellRef(formula.Ref, dir, num, offset); 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 {
if formula.Content != "" {
if formula.Content, err = f.adjustFormulaRef(sheet, formula.Content, dir, num, offset); 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) {
// isFunctionStop provides a function to check if token is a function stop.
func isFunctionStop(token efp.Token) bool {
return token.TType == efp.TokenTypeFunction && token.TSubType == efp.TokenSubTypeStop
}
// isFunctionStart provides a function to check if token is a function start.
func isFunctionStart(token efp.Token) bool {
return token.TType == efp.TokenTypeFunction && token.TSubType == efp.TokenSubTypeStart
}
// adjustFormulaColumnName adjust column name in the formula reference.
func adjustFormulaColumnName(name string, dir adjustDirection, num, offset int) (string, error) {
if name == "" {
return name, nil
}
col, err := ColumnNameToNumber(name)
if err != nil {
return name, err
}
if dir == columns && col >= num {
col += offset
return ColumnNumberToName(col)
}
return name, nil
}
// adjustFormulaRowNumber adjust row number in the formula reference.
func adjustFormulaRowNumber(name string, dir adjustDirection, num, offset int) (string, error) {
if name == "" {
return name, nil
}
row, _ := strconv.Atoi(name)
if dir == rows && row >= num {
row += offset
if row > TotalRows {
return name, ErrMaxRows
}
return strconv.Itoa(row), nil
}
return name, nil
}
// adjustFormulaOperand adjust range operand tokens for the formula.
func (f *File) adjustFormulaOperand(token efp.Token, dir adjustDirection, num int, offset int) (string, error) {
var col, row, operand string
for _, r := range token.TValue {
if ('A' <= r && r <= 'Z') || ('a' <= r && r <= 'z') {
col += string(r)
continue
}
if '0' <= r && r <= '9' {
row += string(r)
if col != "" {
name, err := adjustFormulaColumnName(col, dir, num, offset)
if err != nil {
return operand, err
}
operand += name
col = ""
}
continue
}
if row != "" {
name, err := adjustFormulaRowNumber(row, dir, num, offset)
if err != nil {
return operand, err
}
operand += name
row = ""
}
if col != "" {
name, err := adjustFormulaColumnName(col, dir, num, offset)
if err != nil {
return operand, err
}
operand += name
col = ""
}
operand += string(r)
}
name, err := adjustFormulaColumnName(col, dir, num, offset)
if err != nil {
return operand, err
}
operand += name
name, err = adjustFormulaRowNumber(row, dir, num, offset)
operand += name
return operand, err
}
// adjustFormulaRef returns adjusted formula by giving adjusting direction and
// the base number of column or row, and offset.
func (f *File) adjustFormulaRef(sheet, formula string, dir adjustDirection, num, offset int) (string, error) {
var (
formulaText string
val string
definedNames []string
ps = efp.ExcelParser()
)
......@@ -234,32 +340,34 @@ func (f *File) adjustFormulaRef(sheet string, text string, dir adjustDirection,
definedNames = append(definedNames, definedName.Name)
}
}
for _, token := range ps.Parse(text) {
for _, token := range ps.Parse(formula) {
if token.TType == efp.TokenTypeOperand && token.TSubType == efp.TokenSubTypeRange {
if inStrSlice(definedNames, token.TValue, true) != -1 {
formulaText += token.TValue
val += 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
if strings.ContainsAny(token.TValue, "[]") {
val += token.TValue
continue
}
cell, err := CoordinatesToCellName(c, r, strings.Contains(token.TValue, "$"))
operand, err := f.adjustFormulaOperand(token, dir, num, offset)
if err != nil {
return formulaText, err
return val, err
}
formulaText += cell
val += operand
continue
}
if isFunctionStart(token) {
val += token.TValue + string(efp.ParenOpen)
continue
}
if isFunctionStop(token) {
val += token.TValue + string(efp.ParenClose)
continue
}
formulaText += token.TValue
val += token.TValue
}
return formulaText, nil
return val, nil
}
// adjustHyperlinks provides a function to update hyperlinks when inserting or
......
......@@ -451,6 +451,63 @@ func TestAdjustCols(t *testing.T) {
assert.NoError(t, f.Close())
}
func TestAdjustColDimensions(t *testing.T) {
f := NewFile()
ws, err := f.workSheetReader("Sheet1")
assert.NoError(t, err)
assert.NoError(t, f.SetCellFormula("Sheet1", "C3", "A1+B1"))
assert.Equal(t, ErrColumnNumber, f.adjustColDimensions("Sheet1", ws, 1, MaxColumns))
}
func TestAdjustRowDimensions(t *testing.T) {
f := NewFile()
ws, err := f.workSheetReader("Sheet1")
assert.NoError(t, err)
assert.NoError(t, f.SetCellFormula("Sheet1", "C3", "A1+B1"))
assert.Equal(t, ErrMaxRows, f.adjustRowDimensions("Sheet1", ws, 1, TotalRows))
}
func TestAdjustHyperlinks(t *testing.T) {
f := NewFile()
ws, err := f.workSheetReader("Sheet1")
assert.NoError(t, err)
assert.NoError(t, f.SetCellFormula("Sheet1", "C3", "A1+B1"))
f.adjustHyperlinks(ws, "Sheet1", rows, 3, -1)
// Test adjust hyperlinks location with positive offset
assert.NoError(t, f.SetCellHyperLink("Sheet1", "F5", "Sheet1!A1", "Location"))
assert.NoError(t, f.InsertRows("Sheet1", 1, 1))
link, target, err := f.GetCellHyperLink("Sheet1", "F6")
assert.NoError(t, err)
assert.True(t, link)
assert.Equal(t, target, "Sheet1!A1")
// Test adjust hyperlinks location with negative offset
assert.NoError(t, f.RemoveRow("Sheet1", 1))
link, target, err = f.GetCellHyperLink("Sheet1", "F5")
assert.NoError(t, err)
assert.True(t, link)
assert.Equal(t, target, "Sheet1!A1")
// Test adjust hyperlinks location on remove row
assert.NoError(t, f.RemoveRow("Sheet1", 5))
link, target, err = f.GetCellHyperLink("Sheet1", "F5")
assert.NoError(t, err)
assert.False(t, link)
assert.Empty(t, target)
// Test adjust hyperlinks location on remove column
assert.NoError(t, f.SetCellHyperLink("Sheet1", "F5", "Sheet1!A1", "Location"))
assert.NoError(t, f.RemoveCol("Sheet1", "F"))
link, target, err = f.GetCellHyperLink("Sheet1", "F5")
assert.NoError(t, err)
assert.False(t, link)
assert.Empty(t, target)
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAdjustHyperlinks.xlsx")))
assert.NoError(t, f.Close())
}
func TestAdjustFormula(t *testing.T) {
f := NewFile()
formulaType, ref := STCellFormulaTypeShared, "C1:C5"
......@@ -467,7 +524,7 @@ func TestAdjustFormula(t *testing.T) {
assert.NoError(t, f.Close())
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, newCellNameToCoordinatesError("-", newInvalidCellNameError("-")), 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)
......@@ -482,6 +539,18 @@ func TestAdjustFormula(t *testing.T) {
assert.NoError(t, f.SetCellFormula("Sheet1", "B2", fmt.Sprintf("A%d", TotalRows)))
assert.Equal(t, ErrMaxRows, f.InsertRows("Sheet1", 1, 1))
f = NewFile()
assert.NoError(t, f.SetCellFormula("Sheet1", "B3", "SUM(1048576:1:2)"))
assert.Equal(t, ErrMaxRows, f.InsertRows("Sheet1", 1, 1))
f = NewFile()
assert.NoError(t, f.SetCellFormula("Sheet1", "B3", "SUM(XFD:A:B)"))
assert.Equal(t, ErrColumnNumber, f.InsertCols("Sheet1", "A", 1))
f = NewFile()
assert.NoError(t, f.SetCellFormula("Sheet1", "B3", "SUM(A:B:XFD)"))
assert.Equal(t, ErrColumnNumber, f.InsertCols("Sheet1", "A", 1))
// Test adjust formula with defined name in formula text
f = NewFile()
assert.NoError(t, f.SetDefinedName(&DefinedName{
......@@ -497,9 +566,205 @@ func TestAdjustFormula(t *testing.T) {
// 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.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)
// Test adjust formula on duplicate row with array formula
f = NewFile()
formulaType, reference = STCellFormulaTypeArray, "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)
// Test adjust formula on duplicate row with relative and absolute cell references
f = NewFile()
assert.NoError(t, f.SetCellFormula("Sheet1", "B10", "A$10+$A11"))
assert.NoError(t, f.DuplicateRowTo("Sheet1", 10, 2))
formula, err = f.GetCellFormula("Sheet1", "B2")
assert.NoError(t, err)
assert.Equal(t, "A$2+$A3", formula)
t.Run("for_cells_affected_directly", func(t *testing.T) {
// Test insert row in middle of range with relative and absolute cell references
f := NewFile()
assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "$A1+A$2"))
assert.NoError(t, f.InsertRows("Sheet1", 2, 1))
formula, err := f.GetCellFormula("Sheet1", "B1")
assert.NoError(t, err)
assert.Equal(t, "$A1+A$3", formula)
assert.NoError(t, f.RemoveRow("Sheet1", 2))
formula, err = f.GetCellFormula("Sheet1", "B1")
assert.NoError(t, err)
assert.Equal(t, "$A1+A$2", formula)
// Test insert column in middle of range
f = NewFile()
assert.NoError(t, f.SetCellFormula("Sheet1", "A1", "B1+C1"))
assert.NoError(t, f.InsertCols("Sheet1", "C", 1))
formula, err = f.GetCellFormula("Sheet1", "A1")
assert.NoError(t, err)
assert.Equal(t, "B1+D1", formula)
assert.NoError(t, f.RemoveCol("Sheet1", "C"))
formula, err = f.GetCellFormula("Sheet1", "A1")
assert.NoError(t, err)
assert.Equal(t, "B1+C1", formula)
// Test insert row and column in a rectangular range
f = NewFile()
assert.NoError(t, f.SetCellFormula("Sheet1", "A1", "D4+D5+E4+E5"))
assert.NoError(t, f.InsertCols("Sheet1", "E", 1))
assert.NoError(t, f.InsertRows("Sheet1", 5, 1))
formula, err = f.GetCellFormula("Sheet1", "A1")
assert.NoError(t, err)
assert.Equal(t, "D4+D6+F4+F6", formula)
// Test insert row in middle of range
f = NewFile()
formulaType, reference := STCellFormulaTypeArray, "B1:B1"
assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "A1:A2", FormulaOpts{Ref: &reference, Type: &formulaType}))
assert.NoError(t, f.InsertRows("Sheet1", 2, 1))
formula, err = f.GetCellFormula("Sheet1", "B1")
assert.NoError(t, err)
assert.Equal(t, "A1:A3", formula)
assert.NoError(t, f.RemoveRow("Sheet1", 2))
formula, err = f.GetCellFormula("Sheet1", "B1")
assert.NoError(t, err)
assert.Equal(t, "A1:A2", formula)
// Test insert column in middle of range
f = NewFile()
formulaType, reference = STCellFormulaTypeArray, "A1:A1"
assert.NoError(t, f.SetCellFormula("Sheet1", "A1", "B1:C1", FormulaOpts{Ref: &reference, Type: &formulaType}))
assert.NoError(t, f.InsertCols("Sheet1", "C", 1))
formula, err = f.GetCellFormula("Sheet1", "A1")
assert.NoError(t, err)
assert.Equal(t, "B1:D1", formula)
assert.NoError(t, f.RemoveCol("Sheet1", "C"))
formula, err = f.GetCellFormula("Sheet1", "A1")
assert.NoError(t, err)
assert.Equal(t, "B1:C1", formula)
// Test insert row and column in a rectangular range
f = NewFile()
formulaType, reference = STCellFormulaTypeArray, "A1:A1"
assert.NoError(t, f.SetCellFormula("Sheet1", "A1", "D4:E5", FormulaOpts{Ref: &reference, Type: &formulaType}))
assert.NoError(t, f.InsertCols("Sheet1", "E", 1))
assert.NoError(t, f.InsertRows("Sheet1", 5, 1))
formula, err = f.GetCellFormula("Sheet1", "A1")
assert.NoError(t, err)
assert.Equal(t, "D4:F6", formula)
})
t.Run("for_cells_affected_indirectly", func(t *testing.T) {
f := NewFile()
assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "A3+A4"))
assert.NoError(t, f.InsertRows("Sheet1", 2, 1))
formula, err := f.GetCellFormula("Sheet1", "B1")
assert.NoError(t, err)
assert.Equal(t, "A4+A5", formula)
assert.NoError(t, f.RemoveRow("Sheet1", 2))
formula, err = f.GetCellFormula("Sheet1", "B1")
assert.NoError(t, err)
assert.Equal(t, "A3+A4", formula)
f = NewFile()
assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "D3+D4"))
assert.NoError(t, f.InsertCols("Sheet1", "C", 1))
formula, err = f.GetCellFormula("Sheet1", "B1")
assert.NoError(t, err)
assert.Equal(t, "E3+E4", formula)
assert.NoError(t, f.RemoveCol("Sheet1", "C"))
formula, err = f.GetCellFormula("Sheet1", "B1")
assert.NoError(t, err)
assert.Equal(t, "D3+D4", formula)
})
t.Run("for_entire_cols_rows_reference", func(t *testing.T) {
f := NewFile()
// Test adjust formula on insert row in the middle of the range
assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "SUM(A2:A3:A4,,Table1[])"))
assert.NoError(t, f.InsertRows("Sheet1", 3, 1))
formula, err := f.GetCellFormula("Sheet1", "B1")
assert.NoError(t, err)
assert.Equal(t, "SUM(A2:A4:A5,,Table1[])", formula)
// Test adjust formula on insert at the top of the range
assert.NoError(t, f.InsertRows("Sheet1", 2, 1))
formula, err = f.GetCellFormula("Sheet1", "B1")
assert.NoError(t, err)
assert.Equal(t, "SUM(A3:A5:A6,,Table1[])", formula)
f = NewFile()
// Test adjust formula on insert row in the middle of the range
assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "SUM(A2,A3)"))
assert.NoError(t, f.InsertRows("Sheet1", 3, 1))
formula, err = f.GetCellFormula("Sheet1", "B1")
assert.NoError(t, err)
assert.Equal(t, "SUM(A2,A4)", formula)
// Test adjust formula on insert row at the top of the range
assert.NoError(t, f.InsertRows("Sheet1", 2, 1))
formula, err = f.GetCellFormula("Sheet1", "B1")
assert.NoError(t, err)
assert.Equal(t, "SUM(A3,A5)", formula)
f = NewFile()
// Test adjust formula on insert col in the middle of the range
assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "SUM(C3:D3)"))
assert.NoError(t, f.InsertCols("Sheet1", "D", 1))
formula, err = f.GetCellFormula("Sheet1", "B1")
assert.NoError(t, err)
assert.Equal(t, "SUM(C3:E3)", formula)
// Test adjust formula on insert at the top of the range
assert.NoError(t, f.InsertCols("Sheet1", "C", 1))
formula, err = f.GetCellFormula("Sheet1", "B1")
assert.NoError(t, err)
assert.Equal(t, "SUM(D3:F3)", formula)
f = NewFile()
// Test adjust formula on insert column in the middle of the range
assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "SUM(C3,D3)"))
assert.NoError(t, f.InsertCols("Sheet1", "D", 1))
formula, err = f.GetCellFormula("Sheet1", "B1")
assert.NoError(t, err)
assert.Equal(t, "SUM(C3,E3)", formula)
// Test adjust formula on insert column at the top of the range
assert.NoError(t, f.InsertCols("Sheet1", "C", 1))
formula, err = f.GetCellFormula("Sheet1", "B1")
assert.NoError(t, err)
assert.Equal(t, "SUM(D3,F3)", formula)
f = NewFile()
// Test adjust formula on insert row in the middle of the range (range of whole row)
assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "SUM(2:3)"))
assert.NoError(t, f.InsertRows("Sheet1", 3, 1))
formula, err = f.GetCellFormula("Sheet1", "B1")
assert.NoError(t, err)
assert.Equal(t, "SUM(2:4)", formula)
// Test adjust formula on insert row at the top of the range (range of whole row)
assert.NoError(t, f.InsertRows("Sheet1", 2, 1))
formula, err = f.GetCellFormula("Sheet1", "B1")
assert.NoError(t, err)
assert.Equal(t, "SUM(3:5)", formula)
f = NewFile()
// Test adjust formula on insert row in the middle of the range (range of whole column)
assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "SUM(C:D)"))
assert.NoError(t, f.InsertCols("Sheet1", "D", 1))
formula, err = f.GetCellFormula("Sheet1", "B1")
assert.NoError(t, err)
assert.Equal(t, "SUM(C:E)", formula)
// Test adjust formula on insert row at the top of the range (range of whole column)
assert.NoError(t, f.InsertCols("Sheet1", "C", 1))
formula, err = f.GetCellFormula("Sheet1", "B1")
assert.NoError(t, err)
assert.Equal(t, "SUM(D:F)", formula)
})
}
......@@ -652,7 +652,8 @@ func (f *File) DuplicateRowTo(sheet string, row, row2 int) error {
}
rowCopy.C = append(make([]xlsxC, 0, len(rowCopy.C)), rowCopy.C...)
_ = f.adjustSingleRowDimensions(sheet, &rowCopy, row, row2-row, true)
rowCopy.adjustSingleRowDimensions(row2 - row)
_ = f.adjustSingleRowFormulas(sheet, &rowCopy, row, row2-row, true)
if idx2 != -1 {
ws.SheetData.Row[idx2] = rowCopy
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册