Skip to content

Commit a8cbcfa

Browse files
authored
This closes qax-os#1306 and closes qax-os#1615 (qax-os#1698)
- Support adjust formula on inserting/deleting columns/rows
1 parent 05689d6 commit a8cbcfa

File tree

8 files changed

+186
-36
lines changed

8 files changed

+186
-36
lines changed

adjust.go

Lines changed: 118 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import (
1616
"encoding/xml"
1717
"io"
1818
"strings"
19+
20+
"github.com/xuri/efp"
1921
)
2022

2123
type adjustDirection bool
@@ -42,9 +44,9 @@ func (f *File) adjustHelper(sheet string, dir adjustDirection, num, offset int)
4244
}
4345
sheetID := f.getSheetID(sheet)
4446
if dir == rows {
45-
err = f.adjustRowDimensions(ws, num, offset)
47+
err = f.adjustRowDimensions(sheet, ws, num, offset)
4648
} else {
47-
err = f.adjustColDimensions(ws, num, offset)
49+
err = f.adjustColDimensions(sheet, ws, num, offset)
4850
}
4951
if err != nil {
5052
return err
@@ -116,7 +118,7 @@ func (f *File) adjustCols(ws *xlsxWorksheet, col, offset int) error {
116118

117119
// adjustColDimensions provides a function to update column dimensions when
118120
// inserting or deleting rows or columns.
119-
func (f *File) adjustColDimensions(ws *xlsxWorksheet, col, offset int) error {
121+
func (f *File) adjustColDimensions(sheet string, ws *xlsxWorksheet, col, offset int) error {
120122
for rowIdx := range ws.SheetData.Row {
121123
for _, v := range ws.SheetData.Row[rowIdx].C {
122124
if cellCol, _, _ := CellNameToCoordinates(v.R); col <= cellCol {
@@ -131,50 +133,61 @@ func (f *File) adjustColDimensions(ws *xlsxWorksheet, col, offset int) error {
131133
if cellCol, cellRow, _ := CellNameToCoordinates(v.R); col <= cellCol {
132134
if newCol := cellCol + offset; newCol > 0 {
133135
ws.SheetData.Row[rowIdx].C[colIdx].R, _ = CoordinatesToCellName(newCol, cellRow)
134-
_ = f.adjustFormula(ws.SheetData.Row[rowIdx].C[colIdx].F, columns, offset, false)
135136
}
136137
}
138+
if err := f.adjustFormula(sheet, ws.SheetData.Row[rowIdx].C[colIdx].F, columns, col, offset, false); err != nil {
139+
return err
140+
}
137141
}
138142
}
139143
return f.adjustCols(ws, col, offset)
140144
}
141145

142146
// adjustRowDimensions provides a function to update row dimensions when
143147
// inserting or deleting rows or columns.
144-
func (f *File) adjustRowDimensions(ws *xlsxWorksheet, row, offset int) error {
148+
func (f *File) adjustRowDimensions(sheet string, ws *xlsxWorksheet, row, offset int) error {
145149
totalRows := len(ws.SheetData.Row)
146150
if totalRows == 0 {
147151
return nil
148152
}
149153
lastRow := &ws.SheetData.Row[totalRows-1]
150-
if newRow := lastRow.R + offset; lastRow.R >= row && newRow > 0 && newRow >= TotalRows {
154+
if newRow := lastRow.R + offset; lastRow.R >= row && newRow > 0 && newRow > TotalRows {
151155
return ErrMaxRows
152156
}
153157
for i := 0; i < len(ws.SheetData.Row); i++ {
154158
r := &ws.SheetData.Row[i]
155159
if newRow := r.R + offset; r.R >= row && newRow > 0 {
156-
f.adjustSingleRowDimensions(r, newRow, offset, false)
160+
if err := f.adjustSingleRowDimensions(sheet, r, row, offset, false); err != nil {
161+
return err
162+
}
157163
}
158164
}
159165
return nil
160166
}
161167

162168
// adjustSingleRowDimensions provides a function to adjust single row dimensions.
163-
func (f *File) adjustSingleRowDimensions(r *xlsxRow, num, offset int, si bool) {
164-
r.R = num
169+
func (f *File) adjustSingleRowDimensions(sheet string, r *xlsxRow, num, offset int, si bool) error {
170+
r.R += offset
165171
for i, col := range r.C {
166172
colName, _, _ := SplitCellName(col.R)
167-
r.C[i].R, _ = JoinCellName(colName, num)
168-
_ = f.adjustFormula(col.F, rows, offset, si)
173+
r.C[i].R, _ = JoinCellName(colName, r.R)
174+
if err := f.adjustFormula(sheet, col.F, rows, num, offset, si); err != nil {
175+
return err
176+
}
169177
}
178+
return nil
170179
}
171180

172-
// adjustFormula provides a function to adjust shared formula reference.
173-
func (f *File) adjustFormula(formula *xlsxF, dir adjustDirection, offset int, si bool) error {
174-
if formula != nil && formula.Ref != "" {
175-
coordinates, err := rangeRefToCoordinates(formula.Ref)
181+
// adjustFormula provides a function to adjust formula reference and shared
182+
// formula reference.
183+
func (f *File) adjustFormula(sheet string, formula *xlsxF, dir adjustDirection, num, offset int, si bool) error {
184+
if formula == nil {
185+
return nil
186+
}
187+
adjustRef := func(ref string) (string, error) {
188+
coordinates, err := rangeRefToCoordinates(ref)
176189
if err != nil {
177-
return err
190+
return ref, err
178191
}
179192
if dir == columns {
180193
coordinates[0] += offset
@@ -183,16 +196,72 @@ func (f *File) adjustFormula(formula *xlsxF, dir adjustDirection, offset int, si
183196
coordinates[1] += offset
184197
coordinates[3] += offset
185198
}
186-
if formula.Ref, err = f.coordinatesToRangeRef(coordinates); err != nil {
199+
return f.coordinatesToRangeRef(coordinates)
200+
}
201+
var err error
202+
if formula.Ref != "" {
203+
if formula.Ref, err = adjustRef(formula.Ref); err != nil {
187204
return err
188205
}
189206
if si && formula.Si != nil {
190207
formula.Si = intPtr(*formula.Si + 1)
191208
}
192209
}
210+
if formula.T == STCellFormulaTypeArray {
211+
formula.Content, err = adjustRef(strings.TrimPrefix(formula.Content, "="))
212+
return err
213+
}
214+
if formula.Content != "" && !strings.ContainsAny(formula.Content, "[:]") {
215+
content, err := f.adjustFormulaRef(sheet, formula.Content, dir, num, offset)
216+
if err != nil {
217+
return err
218+
}
219+
formula.Content = content
220+
}
193221
return nil
194222
}
195223

224+
// adjustFormulaRef returns adjusted formula text by giving adjusting direction
225+
// and the base number of column or row, and offset.
226+
func (f *File) adjustFormulaRef(sheet string, text string, dir adjustDirection, num, offset int) (string, error) {
227+
var (
228+
formulaText string
229+
definedNames []string
230+
ps = efp.ExcelParser()
231+
)
232+
for _, definedName := range f.GetDefinedName() {
233+
if definedName.Scope == "Workbook" || definedName.Scope == sheet {
234+
definedNames = append(definedNames, definedName.Name)
235+
}
236+
}
237+
for _, token := range ps.Parse(text) {
238+
if token.TType == efp.TokenTypeOperand && token.TSubType == efp.TokenSubTypeRange {
239+
if inStrSlice(definedNames, token.TValue, true) != -1 {
240+
formulaText += token.TValue
241+
continue
242+
}
243+
c, r, err := CellNameToCoordinates(token.TValue)
244+
if err != nil {
245+
return formulaText, err
246+
}
247+
if dir == columns && c >= num {
248+
c += offset
249+
}
250+
if dir == rows {
251+
r += offset
252+
}
253+
cell, err := CoordinatesToCellName(c, r, strings.Contains(token.TValue, "$"))
254+
if err != nil {
255+
return formulaText, err
256+
}
257+
formulaText += cell
258+
continue
259+
}
260+
formulaText += token.TValue
261+
}
262+
return formulaText, nil
263+
}
264+
196265
// adjustHyperlinks provides a function to update hyperlinks when inserting or
197266
// deleting rows or columns.
198267
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,
260329
return
261330
}
262331
// Remove the table when deleting the header row of the table
263-
if dir == rows && num == coordinates[0] {
332+
if dir == rows && num == coordinates[0] && offset == -1 {
264333
ws.TableParts.TableParts = append(ws.TableParts.TableParts[:idx], ws.TableParts.TableParts[idx+1:]...)
265334
ws.TableParts.Count = len(ws.TableParts.TableParts)
266335
idx--
@@ -316,8 +385,8 @@ func (f *File) adjustAutoFilter(ws *xlsxWorksheet, dir adjustDirection, num, off
316385
}
317386

318387
// adjustAutoFilterHelper provides a function for adjusting auto filter to
319-
// compare and calculate cell reference by the given adjust direction, operation
320-
// reference and offset.
388+
// compare and calculate cell reference by the giving adjusting direction,
389+
// operation reference and offset.
321390
func (f *File) adjustAutoFilterHelper(dir adjustDirection, coordinates []int, num, offset int) []int {
322391
if dir == rows {
323392
if coordinates[1] >= num {
@@ -422,13 +491,34 @@ func (f *File) deleteMergeCell(ws *xlsxWorksheet, idx int) {
422491
}
423492
}
424493

494+
// adjustCalcChainRef update the cell reference in calculation chain when
495+
// inserting or deleting rows or columns.
496+
func (f *File) adjustCalcChainRef(i, c, r, offset int, dir adjustDirection) {
497+
if dir == rows {
498+
if rn := r + offset; rn > 0 {
499+
f.CalcChain.C[i].R, _ = CoordinatesToCellName(c, rn)
500+
}
501+
return
502+
}
503+
if nc := c + offset; nc > 0 {
504+
f.CalcChain.C[i].R, _ = CoordinatesToCellName(nc, r)
505+
}
506+
}
507+
425508
// adjustCalcChain provides a function to update the calculation chain when
426509
// inserting or deleting rows or columns.
427510
func (f *File) adjustCalcChain(dir adjustDirection, num, offset, sheetID int) error {
428511
if f.CalcChain == nil {
429512
return nil
430513
}
514+
// If sheet ID is omitted, it is assumed to be the same as the i value of
515+
// the previous cell.
516+
var prevSheetID int
431517
for index, c := range f.CalcChain.C {
518+
if c.I == 0 {
519+
c.I = prevSheetID
520+
}
521+
prevSheetID = c.I
432522
if c.I != sheetID {
433523
continue
434524
}
@@ -437,14 +527,18 @@ func (f *File) adjustCalcChain(dir adjustDirection, num, offset, sheetID int) er
437527
return err
438528
}
439529
if dir == rows && num <= rowNum {
440-
if newRow := rowNum + offset; newRow > 0 {
441-
f.CalcChain.C[index].R, _ = CoordinatesToCellName(colNum, newRow)
530+
if num == rowNum && offset == -1 {
531+
_ = f.deleteCalcChain(c.I, c.R)
532+
continue
442533
}
534+
f.adjustCalcChainRef(index, colNum, rowNum, offset, dir)
443535
}
444536
if dir == columns && num <= colNum {
445-
if newCol := colNum + offset; newCol > 0 {
446-
f.CalcChain.C[index].R, _ = CoordinatesToCellName(newCol, rowNum)
537+
if num == colNum && offset == -1 {
538+
_ = f.deleteCalcChain(c.I, c.R)
539+
continue
447540
}
541+
f.adjustCalcChainRef(index, colNum, rowNum, offset, dir)
448542
}
449543
}
450544
return nil

adjust_test.go

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -357,13 +357,18 @@ func TestAdjustHelper(t *testing.T) {
357357
func TestAdjustCalcChain(t *testing.T) {
358358
f := NewFile()
359359
f.CalcChain = &xlsxCalcChain{
360-
C: []xlsxCalcChainC{
361-
{R: "B2", I: 2}, {R: "B2", I: 1},
362-
},
360+
C: []xlsxCalcChainC{{R: "B2", I: 2}, {R: "B2", I: 1}, {R: "A1", I: 1}},
363361
}
364362
assert.NoError(t, f.InsertCols("Sheet1", "A", 1))
365363
assert.NoError(t, f.InsertRows("Sheet1", 1, 1))
366364

365+
f.CalcChain = &xlsxCalcChain{
366+
C: []xlsxCalcChainC{{R: "B2", I: 1}, {R: "B3"}, {R: "A1"}},
367+
}
368+
assert.NoError(t, f.RemoveRow("Sheet1", 3))
369+
assert.NoError(t, f.RemoveCol("Sheet1", "B"))
370+
371+
f.CalcChain = &xlsxCalcChain{C: []xlsxCalcChainC{{R: "B2", I: 2}, {R: "B2", I: 1}}}
367372
f.CalcChain.C[1].R = "invalid coordinates"
368373
assert.Equal(t, f.InsertCols("Sheet1", "A", 1), newCellNameToCoordinatesError("invalid coordinates", newInvalidCellNameError("invalid coordinates")))
369374
f.CalcChain = nil
@@ -449,19 +454,52 @@ func TestAdjustCols(t *testing.T) {
449454
func TestAdjustFormula(t *testing.T) {
450455
f := NewFile()
451456
formulaType, ref := STCellFormulaTypeShared, "C1:C5"
452-
assert.NoError(t, f.SetCellFormula("Sheet1", "C1", "=A1+B1", FormulaOpts{Ref: &ref, Type: &formulaType}))
457+
assert.NoError(t, f.SetCellFormula("Sheet1", "C1", "A1+B1", FormulaOpts{Ref: &ref, Type: &formulaType}))
453458
assert.NoError(t, f.DuplicateRowTo("Sheet1", 1, 10))
454459
assert.NoError(t, f.InsertCols("Sheet1", "B", 1))
455460
assert.NoError(t, f.InsertRows("Sheet1", 1, 1))
456-
for cell, expected := range map[string]string{"D2": "=A1+B1", "D3": "=A2+B2", "D11": "=A1+B1"} {
461+
for cell, expected := range map[string]string{"D2": "A2+C2", "D3": "A3+C3", "D11": "A11+C11"} {
457462
formula, err := f.GetCellFormula("Sheet1", cell)
458463
assert.NoError(t, err)
459464
assert.Equal(t, expected, formula)
460465
}
461466
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAdjustFormula.xlsx")))
462467
assert.NoError(t, f.Close())
463468

464-
assert.NoError(t, f.adjustFormula(nil, rows, 0, false))
465-
assert.Equal(t, f.adjustFormula(&xlsxF{Ref: "-"}, rows, 0, false), ErrParameterInvalid)
466-
assert.Equal(t, f.adjustFormula(&xlsxF{Ref: "XFD1:XFD1"}, columns, 1, false), ErrColumnNumber)
469+
assert.NoError(t, f.adjustFormula("Sheet1", nil, rows, 0, 0, false))
470+
assert.Equal(t, ErrParameterInvalid, f.adjustFormula("Sheet1", &xlsxF{Ref: "-"}, rows, 0, 0, false))
471+
assert.Equal(t, ErrColumnNumber, f.adjustFormula("Sheet1", &xlsxF{Ref: "XFD1:XFD1"}, columns, 0, 1, false))
472+
473+
_, err := f.adjustFormulaRef("Sheet1", "XFE1", columns, 0, 1)
474+
assert.Equal(t, ErrColumnNumber, err)
475+
_, err = f.adjustFormulaRef("Sheet1", "XFD1", columns, 0, 1)
476+
assert.Equal(t, ErrColumnNumber, err)
477+
478+
f = NewFile()
479+
assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "XFD1"))
480+
assert.Equal(t, ErrColumnNumber, f.InsertCols("Sheet1", "A", 1))
481+
482+
assert.NoError(t, f.SetCellFormula("Sheet1", "B2", fmt.Sprintf("A%d", TotalRows)))
483+
assert.Equal(t, ErrMaxRows, f.InsertRows("Sheet1", 1, 1))
484+
485+
// Test adjust formula with defined name in formula text
486+
f = NewFile()
487+
assert.NoError(t, f.SetDefinedName(&DefinedName{
488+
Name: "Amount",
489+
RefersTo: "Sheet1!$B$2",
490+
}))
491+
assert.NoError(t, f.SetCellFormula("Sheet1", "B2", "Amount+B3"))
492+
assert.NoError(t, f.RemoveRow("Sheet1", 1))
493+
formula, err := f.GetCellFormula("Sheet1", "B1")
494+
assert.NoError(t, err)
495+
assert.Equal(t, "Amount+B2", formula)
496+
497+
// Test adjust formula with array formula
498+
f = NewFile()
499+
formulaType, reference := STCellFormulaTypeArray, "A3:A3"
500+
assert.NoError(t, f.SetCellFormula("Sheet1", "A3", "=A1:A2", FormulaOpts{Ref: &reference, Type: &formulaType}))
501+
assert.NoError(t, f.InsertRows("Sheet1", 1, 1))
502+
formula, err = f.GetCellFormula("Sheet1", "A4")
503+
assert.NoError(t, err)
504+
assert.Equal(t, "A2:A3", formula)
467505
}

calc.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14454,7 +14454,7 @@ func (fn *formulaFuncs) ADDRESS(argsList *list.List) formulaArg {
1445414454
if rowNum.Type != ArgNumber {
1445514455
return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
1445614456
}
14457-
if rowNum.Number >= TotalRows {
14457+
if rowNum.Number > TotalRows {
1445814458
return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
1445914459
}
1446014460
colNum := argsList.Front().Next().Value.(formulaArg).ToNumber()

calc_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3970,7 +3970,7 @@ func TestCalcCellValue(t *testing.T) {
39703970
"=ADDRESS(1,1,0,TRUE)": {"#NUM!", "#NUM!"},
39713971
"=ADDRESS(1,16385,2,TRUE)": {"#VALUE!", "#VALUE!"},
39723972
"=ADDRESS(1,16385,3,TRUE)": {"#VALUE!", "#VALUE!"},
3973-
"=ADDRESS(1048576,1,1,TRUE)": {"#VALUE!", "#VALUE!"},
3973+
"=ADDRESS(1048577,1,1,TRUE)": {"#VALUE!", "#VALUE!"},
39743974
// CHOOSE
39753975
"=CHOOSE()": {"#VALUE!", "CHOOSE requires 2 arguments"},
39763976
"=CHOOSE(\"index_num\",0)": {"#VALUE!", "CHOOSE requires first argument of type number"},

lib.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,9 @@ func CoordinatesToCellName(col, row int, abs ...bool) (string, error) {
270270
if col < 1 || row < 1 {
271271
return "", newCoordinatesToCellNameError(col, row)
272272
}
273+
if row > TotalRows {
274+
return "", ErrMaxRows
275+
}
273276
sign := ""
274277
for _, a := range abs {
275278
if a {

rows.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -652,7 +652,7 @@ func (f *File) DuplicateRowTo(sheet string, row, row2 int) error {
652652
}
653653

654654
rowCopy.C = append(make([]xlsxC, 0, len(rowCopy.C)), rowCopy.C...)
655-
f.adjustSingleRowDimensions(&rowCopy, row2, row2-row, true)
655+
_ = f.adjustSingleRowDimensions(sheet, &rowCopy, row, row2-row, true)
656656

657657
if idx2 != -1 {
658658
ws.SheetData.Row[idx2] = rowCopy

rows_test.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -870,6 +870,21 @@ func TestDuplicateRow(t *testing.T) {
870870
f := NewFile()
871871
// Test duplicate row with invalid sheet name
872872
assert.EqualError(t, f.DuplicateRowTo("Sheet:1", 1, 2), ErrSheetNameInvalid.Error())
873+
874+
f = NewFile()
875+
assert.NoError(t, f.SetDefinedName(&DefinedName{
876+
Name: "Amount",
877+
RefersTo: "Sheet1!$B$1",
878+
}))
879+
assert.NoError(t, f.SetCellFormula("Sheet1", "A1", "Amount+C1"))
880+
assert.NoError(t, f.SetCellValue("Sheet1", "A10", "A10"))
881+
assert.NoError(t, f.DuplicateRowTo("Sheet1", 1, 10))
882+
formula, err := f.GetCellFormula("Sheet1", "A10")
883+
assert.NoError(t, err)
884+
assert.Equal(t, "Amount+C10", formula)
885+
value, err := f.GetCellValue("Sheet1", "A11")
886+
assert.NoError(t, err)
887+
assert.Equal(t, "A10", value)
873888
}
874889

875890
func TestDuplicateRowTo(t *testing.T) {

0 commit comments

Comments
 (0)