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

Support to adjust formula cross worksheet on inserting/deleting columns/rows (#1705)

上级 5bba8f98
......@@ -17,6 +17,7 @@ import (
"io"
"strconv"
"strings"
"unicode"
"github.com/xuri/efp"
)
......@@ -128,15 +129,24 @@ func (f *File) adjustColDimensions(sheet string, ws *xlsxWorksheet, col, offset
}
}
}
for rowIdx := range ws.SheetData.Row {
for colIdx, v := range ws.SheetData.Row[rowIdx].C {
if cellCol, cellRow, _ := CellNameToCoordinates(v.R); col <= cellCol {
if newCol := cellCol + offset; newCol > 0 {
ws.SheetData.Row[rowIdx].C[colIdx].R, _ = CoordinatesToCellName(newCol, cellRow)
}
for _, sheetN := range f.GetSheetList() {
worksheet, err := f.workSheetReader(sheetN)
if err != nil {
if err.Error() == newNotWorksheetError(sheetN).Error() {
continue
}
if err := f.adjustFormula(sheet, ws.SheetData.Row[rowIdx].C[colIdx].F, columns, col, offset, false); err != nil {
return err
return err
}
for rowIdx := range worksheet.SheetData.Row {
for colIdx, v := range worksheet.SheetData.Row[rowIdx].C {
if cellCol, cellRow, _ := CellNameToCoordinates(v.R); sheetN == sheet && col <= cellCol {
if newCol := cellCol + offset; newCol > 0 {
worksheet.SheetData.Row[rowIdx].C[colIdx].R, _ = CoordinatesToCellName(newCol, cellRow)
}
}
if err := f.adjustFormula(sheet, sheetN, worksheet.SheetData.Row[rowIdx].C[colIdx].F, columns, col, offset, false); err != nil {
return err
}
}
}
}
......@@ -146,6 +156,25 @@ func (f *File) adjustColDimensions(sheet string, ws *xlsxWorksheet, col, offset
// adjustRowDimensions provides a function to update row dimensions when
// inserting or deleting rows or columns.
func (f *File) adjustRowDimensions(sheet string, ws *xlsxWorksheet, row, offset int) error {
for _, sheetN := range f.GetSheetList() {
if sheetN == sheet {
continue
}
worksheet, err := f.workSheetReader(sheetN)
if err != nil {
if err.Error() == newNotWorksheetError(sheetN).Error() {
continue
}
return err
}
numOfRows := len(worksheet.SheetData.Row)
for i := 0; i < numOfRows; i++ {
r := &worksheet.SheetData.Row[i]
if err = f.adjustSingleRowFormulas(sheet, sheetN, r, row, offset, false); err != nil {
return err
}
}
}
totalRows := len(ws.SheetData.Row)
if totalRows == 0 {
return nil
......@@ -160,7 +189,7 @@ func (f *File) adjustRowDimensions(sheet string, ws *xlsxWorksheet, row, offset
if newRow := r.R + offset; r.R >= row && newRow > 0 {
r.adjustSingleRowDimensions(offset)
}
if err := f.adjustSingleRowFormulas(sheet, r, row, offset, false); err != nil {
if err := f.adjustSingleRowFormulas(sheet, sheet, r, row, offset, false); err != nil {
return err
}
}
......@@ -177,9 +206,9 @@ func (r *xlsxRow) adjustSingleRowDimensions(offset int) {
}
// adjustSingleRowFormulas provides a function to adjust single row formulas.
func (f *File) adjustSingleRowFormulas(sheet string, r *xlsxRow, num, offset int, si bool) error {
func (f *File) adjustSingleRowFormulas(sheet, sheetN 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 {
if err := f.adjustFormula(sheet, sheetN, col.F, rows, num, offset, si); err != nil {
return err
}
}
......@@ -215,12 +244,12 @@ func (f *File) adjustCellRef(ref string, dir adjustDirection, num, offset int) (
// 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 {
func (f *File) adjustFormula(sheet, sheetN 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 != "" && sheet == sheetN {
if formula.Ref, err = f.adjustCellRef(formula.Ref, dir, num, offset); err != nil {
return err
}
......@@ -229,7 +258,7 @@ func (f *File) adjustFormula(sheet string, formula *xlsxF, dir adjustDirection,
}
}
if formula.Content != "" {
if formula.Content, err = f.adjustFormulaRef(sheet, formula.Content, dir, num, offset); err != nil {
if formula.Content, err = f.adjustFormulaRef(sheet, sheetN, formula.Content, dir, num, offset); err != nil {
return err
}
}
......@@ -246,11 +275,19 @@ func isFunctionStart(token efp.Token) bool {
return token.TType == efp.TokenTypeFunction && token.TSubType == efp.TokenSubTypeStart
}
// escapeSheetName enclose sheet name in single quotation marks if the giving
// worksheet name includes spaces or non-alphabetical characters.
func escapeSheetName(name string) string {
if strings.IndexFunc(name, func(r rune) bool {
return !unicode.IsLetter(r) && !unicode.IsNumber(r)
}) != -1 {
return string(efp.QuoteSingle) + name + string(efp.QuoteSingle)
}
return name
}
// 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
......@@ -264,9 +301,6 @@ func adjustFormulaColumnName(name string, dir adjustDirection, num, offset int)
// 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
......@@ -300,16 +334,19 @@ func adjustFormulaOperandRef(row, col, operand string, dir adjustDirection, num
}
// adjustFormulaOperand adjust range operand tokens for the formula.
func (f *File) adjustFormulaOperand(token efp.Token, dir adjustDirection, num int, offset int) (string, error) {
func (f *File) adjustFormulaOperand(sheet, sheetN string, token efp.Token, dir adjustDirection, num int, offset int) (string, error) {
var (
err error
sheet, col, row, operand string
cell = token.TValue
tokens = strings.Split(token.TValue, "!")
err error
sheetName, col, row, operand string
cell = token.TValue
tokens = strings.Split(token.TValue, "!")
)
if len(tokens) == 2 { // have a worksheet
sheet, cell = tokens[0], tokens[1]
operand = string(efp.QuoteSingle) + sheet + string(efp.QuoteSingle) + "!"
sheetName, cell = tokens[0], tokens[1]
operand = escapeSheetName(sheetName) + "!"
}
if sheet != sheetN && sheet != sheetName {
return operand + cell, err
}
for _, r := range cell {
if ('A' <= r && r <= 'Z') || ('a' <= r && r <= 'z') {
......@@ -333,19 +370,13 @@ func (f *File) adjustFormulaOperand(token efp.Token, dir adjustDirection, num in
}
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
_, _, operand, err = adjustFormulaOperandRef(row, col, operand, dir, num, offset)
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) {
func (f *File) adjustFormulaRef(sheet, sheetN, formula string, dir adjustDirection, num, offset int) (string, error) {
var (
val string
definedNames []string
......@@ -366,7 +397,7 @@ func (f *File) adjustFormulaRef(sheet, formula string, dir adjustDirection, num,
val += token.TValue
continue
}
operand, err := f.adjustFormulaOperand(token, dir, num, offset)
operand, err := f.adjustFormulaOperand(sheet, sheetN, token, dir, num, offset)
if err != nil {
return val, err
}
......@@ -382,7 +413,7 @@ func (f *File) adjustFormulaRef(sheet, formula string, dir adjustDirection, num,
continue
}
if token.TType == efp.TokenTypeOperand && token.TSubType == efp.TokenSubTypeText {
val += string(efp.QuoteDouble) + token.TValue + string(efp.QuoteDouble)
val += string(efp.QuoteDouble) + strings.ReplaceAll(token.TValue, "\"", "\"\"") + string(efp.QuoteDouble)
continue
}
val += token.TValue
......@@ -420,7 +451,7 @@ func (f *File) adjustHyperlinks(ws *xlsxWorksheet, sheet string, dir adjustDirec
}
for i := range ws.Hyperlinks.Hyperlink {
link := &ws.Hyperlinks.Hyperlink[i] // get reference
link.Ref, _ = f.adjustFormulaRef(sheet, link.Ref, dir, num, offset)
link.Ref, _ = f.adjustFormulaRef(sheet, sheet, link.Ref, dir, num, offset)
}
}
......
......@@ -457,6 +457,12 @@ func TestAdjustColDimensions(t *testing.T) {
assert.NoError(t, err)
assert.NoError(t, f.SetCellFormula("Sheet1", "C3", "A1+B1"))
assert.Equal(t, ErrColumnNumber, f.adjustColDimensions("Sheet1", ws, 1, MaxColumns))
_, err = f.NewSheet("Sheet2")
assert.NoError(t, err)
f.Sheet.Delete("xl/worksheets/sheet2.xml")
f.Pkg.Store("xl/worksheets/sheet2.xml", MacintoshCyrillicCharset)
assert.EqualError(t, f.adjustColDimensions("Sheet2", ws, 2, 1), "XML syntax error on line 1: invalid UTF-8")
}
func TestAdjustRowDimensions(t *testing.T) {
......@@ -465,6 +471,20 @@ func TestAdjustRowDimensions(t *testing.T) {
assert.NoError(t, err)
assert.NoError(t, f.SetCellFormula("Sheet1", "C3", "A1+B1"))
assert.Equal(t, ErrMaxRows, f.adjustRowDimensions("Sheet1", ws, 1, TotalRows))
_, err = f.NewSheet("Sheet2")
assert.NoError(t, err)
f.Sheet.Delete("xl/worksheets/sheet2.xml")
f.Pkg.Store("xl/worksheets/sheet2.xml", MacintoshCyrillicCharset)
assert.EqualError(t, f.adjustRowDimensions("Sheet1", ws, 2, 1), "XML syntax error on line 1: invalid UTF-8")
f = NewFile()
_, err = f.NewSheet("Sheet2")
assert.NoError(t, err)
ws, err = f.workSheetReader("Sheet1")
assert.NoError(t, err)
assert.NoError(t, f.SetCellFormula("Sheet1", "B2", fmt.Sprintf("Sheet2!A%d", TotalRows)))
assert.Equal(t, ErrMaxRows, f.adjustRowDimensions("Sheet2", ws, 1, TotalRows))
}
func TestAdjustHyperlinks(t *testing.T) {
......@@ -523,13 +543,13 @@ 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("Sheet1", nil, 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))
assert.NoError(t, f.adjustFormula("Sheet1", "Sheet1", nil, rows, 0, 0, false))
assert.Equal(t, newCellNameToCoordinatesError("-", newInvalidCellNameError("-")), f.adjustFormula("Sheet1", "Sheet1", &xlsxF{Ref: "-"}, rows, 0, 0, false))
assert.Equal(t, ErrColumnNumber, f.adjustFormula("Sheet1", "Sheet1", &xlsxF{Ref: "XFD1:XFD1"}, columns, 0, 1, false))
_, err := f.adjustFormulaRef("Sheet1", "XFE1", columns, 0, 1)
_, err := f.adjustFormulaRef("Sheet1", "Sheet1", "XFE1", columns, 0, 1)
assert.Equal(t, ErrColumnNumber, err)
_, err = f.adjustFormulaRef("Sheet1", "XFD1", columns, 0, 1)
_, err = f.adjustFormulaRef("Sheet1", "Sheet1", "XFD1", columns, 0, 1)
assert.Equal(t, ErrColumnNumber, err)
f = NewFile()
......@@ -699,17 +719,17 @@ func TestAdjustFormula(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('Sheet1'!A2,A3)"))
assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "SUM('Sheet 1'!A2,A3)"))
assert.NoError(t, f.InsertRows("Sheet1", 3, 1))
formula, err = f.GetCellFormula("Sheet1", "B1")
assert.NoError(t, err)
assert.Equal(t, "SUM('Sheet1'!A2,A4)", formula)
assert.Equal(t, "SUM('Sheet 1'!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('Sheet1'!A3,A5)", formula)
assert.Equal(t, "SUM('Sheet 1'!A3,A5)", formula)
f = NewFile()
// Test adjust formula on insert col in the middle of the range
......@@ -767,4 +787,143 @@ func TestAdjustFormula(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, "SUM(D:F)", formula)
})
t.Run("for_all_worksheet_cells_with_rows_insert", func(t *testing.T) {
f := NewFile()
_, err := f.NewSheet("Sheet2")
assert.NoError(t, err)
// Tests formulas referencing Sheet2 should update but those referencing the original sheet should not
tbl := [][]string{
{"B1", "Sheet2!A1+Sheet2!A2", "Sheet2!A1+Sheet2!A3", "Sheet2!A2+Sheet2!A4"},
{"C1", "A1+A2", "A1+A2", "A1+A2"},
{"D1", "Sheet2!B1:B2", "Sheet2!B1:B3", "Sheet2!B2:B4"},
{"E1", "B1:B2", "B1:B2", "B1:B2"},
{"F1", "SUM(Sheet2!C1:C2)", "SUM(Sheet2!C1:C3)", "SUM(Sheet2!C2:C4)"},
{"G1", "SUM(C1:C2)", "SUM(C1:C2)", "SUM(C1:C2)"},
{"H1", "SUM(Sheet2!D1,Sheet2!D2)", "SUM(Sheet2!D1,Sheet2!D3)", "SUM(Sheet2!D2,Sheet2!D4)"},
{"I1", "SUM(D1,D2)", "SUM(D1,D2)", "SUM(D1,D2)"},
}
for _, preset := range tbl {
assert.NoError(t, f.SetCellFormula("Sheet1", preset[0], preset[1]))
}
// Test adjust formula on insert row in the middle of the range
assert.NoError(t, f.InsertRows("Sheet2", 2, 1))
for _, preset := range tbl {
formula, err := f.GetCellFormula("Sheet1", preset[0])
assert.NoError(t, err)
assert.Equal(t, preset[2], formula)
}
// Test adjust formula on insert row in the top of the range
assert.NoError(t, f.InsertRows("Sheet2", 1, 1))
for _, preset := range tbl {
formula, err := f.GetCellFormula("Sheet1", preset[0])
assert.NoError(t, err)
assert.Equal(t, preset[3], formula)
}
})
t.Run("for_all_worksheet_cells_with_cols_insert", func(t *testing.T) {
f := NewFile()
_, err := f.NewSheet("Sheet2")
assert.NoError(t, err)
tbl := [][]string{
{"A1", "Sheet2!A1+Sheet2!B1", "Sheet2!A1+Sheet2!C1", "Sheet2!B1+Sheet2!D1"},
{"A2", "A1+B1", "A1+B1", "A1+B1"},
{"A3", "Sheet2!A2:B2", "Sheet2!A2:C2", "Sheet2!B2:D2"},
{"A4", "A2:B2", "A2:B2", "A2:B2"},
{"A5", "SUM(Sheet2!A3:B3)", "SUM(Sheet2!A3:C3)", "SUM(Sheet2!B3:D3)"},
{"A6", "SUM(A3:B3)", "SUM(A3:B3)", "SUM(A3:B3)"},
{"A7", "SUM(Sheet2!A4,Sheet2!B4)", "SUM(Sheet2!A4,Sheet2!C4)", "SUM(Sheet2!B4,Sheet2!D4)"},
{"A8", "SUM(A4,B4)", "SUM(A4,B4)", "SUM(A4,B4)"},
}
for _, preset := range tbl {
assert.NoError(t, f.SetCellFormula("Sheet1", preset[0], preset[1]))
}
// Test adjust formula on insert column in the middle of the range
assert.NoError(t, f.InsertCols("Sheet2", "B", 1))
for _, preset := range tbl {
formula, err := f.GetCellFormula("Sheet1", preset[0])
assert.NoError(t, err)
assert.Equal(t, preset[2], formula)
}
// Test adjust formula on insert column in the top of the range
assert.NoError(t, f.InsertCols("Sheet2", "A", 1))
for _, preset := range tbl {
formula, err := f.GetCellFormula("Sheet1", preset[0])
assert.NoError(t, err)
assert.Equal(t, preset[3], formula)
}
})
t.Run("for_cross_sheet_ref_with_rows_insert)", func(t *testing.T) {
f := NewFile()
_, err := f.NewSheet("Sheet2")
assert.NoError(t, err)
_, err = f.NewSheet("Sheet3")
assert.NoError(t, err)
// Tests formulas referencing Sheet2 should update but those referencing
// the original sheet or Sheet 3 should not update
tbl := [][]string{
{"B1", "Sheet2!A1+Sheet2!A2+Sheet1!A3+Sheet1!A4", "Sheet2!A1+Sheet2!A3+Sheet1!A3+Sheet1!A4", "Sheet2!A2+Sheet2!A4+Sheet1!A3+Sheet1!A4"},
{"C1", "Sheet2!B1+Sheet2!B2+B3+B4", "Sheet2!B1+Sheet2!B3+B3+B4", "Sheet2!B2+Sheet2!B4+B3+B4"},
{"D1", "Sheet2!C1+Sheet2!C2+Sheet3!A3+Sheet3!A4", "Sheet2!C1+Sheet2!C3+Sheet3!A3+Sheet3!A4", "Sheet2!C2+Sheet2!C4+Sheet3!A3+Sheet3!A4"},
{"E1", "SUM(Sheet2!D1:D2,Sheet1!A3:A4)", "SUM(Sheet2!D1:D3,Sheet1!A3:A4)", "SUM(Sheet2!D2:D4,Sheet1!A3:A4)"},
{"F1", "SUM(Sheet2!E1:E2,A3:A4)", "SUM(Sheet2!E1:E3,A3:A4)", "SUM(Sheet2!E2:E4,A3:A4)"},
{"G1", "SUM(Sheet2!F1:F2,Sheet3!A3:A4)", "SUM(Sheet2!F1:F3,Sheet3!A3:A4)", "SUM(Sheet2!F2:F4,Sheet3!A3:A4)"},
}
for _, preset := range tbl {
assert.NoError(t, f.SetCellFormula("Sheet1", preset[0], preset[1]))
}
// Test adjust formula on insert row in the middle of the range
assert.NoError(t, f.InsertRows("Sheet2", 2, 1))
for _, preset := range tbl {
formula, err := f.GetCellFormula("Sheet1", preset[0])
assert.NoError(t, err)
assert.Equal(t, preset[2], formula)
}
// Test adjust formula on insert row in the top of the range
assert.NoError(t, f.InsertRows("Sheet2", 1, 1))
for _, preset := range tbl {
formula, err := f.GetCellFormula("Sheet1", preset[0])
assert.NoError(t, err)
assert.Equal(t, preset[3], formula)
}
})
t.Run("for_cross_sheet_ref_with_cols_insert)", func(t *testing.T) {
f := NewFile()
_, err := f.NewSheet("Sheet2")
assert.NoError(t, err)
_, err = f.NewSheet("Sheet3")
assert.NoError(t, err)
// Tests formulas referencing Sheet2 should update but those referencing
// the original sheet or Sheet 3 should not update
tbl := [][]string{
{"A1", "Sheet2!A1+Sheet2!B1+Sheet1!C1+Sheet1!D1", "Sheet2!A1+Sheet2!C1+Sheet1!C1+Sheet1!D1", "Sheet2!B1+Sheet2!D1+Sheet1!C1+Sheet1!D1"},
{"A2", "Sheet2!A2+Sheet2!B2+C2+D2", "Sheet2!A2+Sheet2!C2+C2+D2", "Sheet2!B2+Sheet2!D2+C2+D2"},
{"A3", "Sheet2!A3+Sheet2!B3+Sheet3!C3+Sheet3!D3", "Sheet2!A3+Sheet2!C3+Sheet3!C3+Sheet3!D3", "Sheet2!B3+Sheet2!D3+Sheet3!C3+Sheet3!D3"},
{"A4", "SUM(Sheet2!A4:B4,Sheet1!C4:D4)", "SUM(Sheet2!A4:C4,Sheet1!C4:D4)", "SUM(Sheet2!B4:D4,Sheet1!C4:D4)"},
{"A5", "SUM(Sheet2!A5:B5,C5:D5)", "SUM(Sheet2!A5:C5,C5:D5)", "SUM(Sheet2!B5:D5,C5:D5)"},
{"A6", "SUM(Sheet2!A6:B6,Sheet3!C6:D6)", "SUM(Sheet2!A6:C6,Sheet3!C6:D6)", "SUM(Sheet2!B6:D6,Sheet3!C6:D6)"},
}
for _, preset := range tbl {
assert.NoError(t, f.SetCellFormula("Sheet1", preset[0], preset[1]))
}
// Test adjust formula on insert row in the middle of the range
assert.NoError(t, f.InsertCols("Sheet2", "B", 1))
for _, preset := range tbl {
formula, err := f.GetCellFormula("Sheet1", preset[0])
assert.NoError(t, err)
assert.Equal(t, preset[2], formula)
}
// Test adjust formula on insert row in the top of the range
assert.NoError(t, f.InsertCols("Sheet2", "A", 1))
for _, preset := range tbl {
formula, err := f.GetCellFormula("Sheet1", preset[0])
assert.NoError(t, err)
assert.Equal(t, preset[3], formula)
}
})
t.Run("for_cross_sheet_ref_with_chart_sheet)", func(t *testing.T) {
assert.NoError(t, f.AddChartSheet("Chart1", &Chart{Type: Line}))
assert.NoError(t, f.InsertRows("Sheet1", 2, 1))
assert.NoError(t, f.InsertCols("Sheet1", "A", 1))
})
}
......@@ -653,7 +653,7 @@ func (f *File) DuplicateRowTo(sheet string, row, row2 int) error {
rowCopy.C = append(make([]xlsxC, 0, len(rowCopy.C)), rowCopy.C...)
rowCopy.adjustSingleRowDimensions(row2 - row)
_ = f.adjustSingleRowFormulas(sheet, &rowCopy, row, row2-row, true)
_ = f.adjustSingleRowFormulas(sheet, 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.
先完成此消息的编辑!
想要评论请 注册