diff --git a/adjust.go b/adjust.go index e1c0e15..d766b3e 100644 --- a/adjust.go +++ b/adjust.go @@ -11,6 +11,13 @@ package excelize +import ( + "bytes" + "encoding/xml" + "io" + "strings" +) + type adjustDirection bool const ( @@ -41,6 +48,7 @@ func (f *File) adjustHelper(sheet string, dir adjustDirection, num, offset int) f.adjustColDimensions(ws, num, offset) } f.adjustHyperlinks(ws, sheet, dir, num, offset) + f.adjustTable(ws, sheet, dir, num, offset) if err = f.adjustMergeCells(ws, dir, num, offset); err != nil { return err } @@ -138,6 +146,54 @@ func (f *File) adjustHyperlinks(ws *xlsxWorksheet, sheet string, dir adjustDirec } } +// adjustTable provides a function to update the table when inserting or +// deleting rows or columns. +func (f *File) adjustTable(ws *xlsxWorksheet, sheet string, dir adjustDirection, num, offset int) { + if ws.TableParts == nil || len(ws.TableParts.TableParts) == 0 { + return + } + for idx := 0; idx < len(ws.TableParts.TableParts); idx++ { + tbl := ws.TableParts.TableParts[idx] + target := f.getSheetRelationshipsTargetByID(sheet, tbl.RID) + tableXML := strings.ReplaceAll(target, "..", "xl") + content, ok := f.Pkg.Load(tableXML) + if !ok { + continue + } + t := xlsxTable{} + if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(content.([]byte)))). + Decode(&t); err != nil && err != io.EOF { + return + } + coordinates, err := areaRefToCoordinates(t.Ref) + if err != nil { + return + } + // Remove the table when deleting the header row of the table + if dir == rows && num == coordinates[0] { + ws.TableParts.TableParts = append(ws.TableParts.TableParts[:idx], ws.TableParts.TableParts[idx+1:]...) + ws.TableParts.Count = len(ws.TableParts.TableParts) + idx-- + continue + } + coordinates = f.adjustAutoFilterHelper(dir, coordinates, num, offset) + x1, y1, x2, y2 := coordinates[0], coordinates[1], coordinates[2], coordinates[3] + if y2-y1 < 2 || x2-x1 < 1 { + ws.TableParts.TableParts = append(ws.TableParts.TableParts[:idx], ws.TableParts.TableParts[idx+1:]...) + ws.TableParts.Count = len(ws.TableParts.TableParts) + idx-- + continue + } + t.Ref, _ = f.coordinatesToAreaRef([]int{x1, y1, x2, y2}) + if t.AutoFilter != nil { + t.AutoFilter.Ref = t.Ref + } + _, _ = f.setTableHeader(sheet, x1, y1, x2) + table, _ := xml.Marshal(t) + f.saveFileList(tableXML, table) + } +} + // adjustAutoFilter provides a function to update the auto filter when // inserting or deleting rows or columns. func (f *File) adjustAutoFilter(ws *xlsxWorksheet, dir adjustDirection, num, offset int) error { @@ -182,10 +238,13 @@ func (f *File) adjustAutoFilterHelper(dir adjustDirection, coordinates []int, nu if coordinates[3] >= num { coordinates[3] += offset } - } else { - if coordinates[2] >= num { - coordinates[2] += offset - } + return coordinates + } + if coordinates[0] >= num { + coordinates[0] += offset + } + if coordinates[2] >= num { + coordinates[2] += offset } return coordinates } diff --git a/adjust_test.go b/adjust_test.go index ab6bedc..1d80705 100644 --- a/adjust_test.go +++ b/adjust_test.go @@ -1,6 +1,8 @@ package excelize import ( + "fmt" + "path/filepath" "testing" "github.com/stretchr/testify/assert" @@ -281,7 +283,7 @@ func TestAdjustAutoFilter(t *testing.T) { Ref: "A1:A3", }, }, rows, 1, -1)) - // testing adjustAutoFilter with illegal cell coordinates. + // Test adjustAutoFilter with illegal cell coordinates. assert.EqualError(t, f.adjustAutoFilter(&xlsxWorksheet{ AutoFilter: &xlsxAutoFilter{ Ref: "A:B1", @@ -294,6 +296,36 @@ func TestAdjustAutoFilter(t *testing.T) { }, rows, 0, 0), newCellNameToCoordinatesError("B", newInvalidCellNameError("B")).Error()) } +func TestAdjustTable(t *testing.T) { + f, sheetName := NewFile(), "Sheet1" + for idx, tableRange := range [][]string{{"B2", "C3"}, {"E3", "F5"}, {"H5", "H8"}, {"J5", "K9"}} { + assert.NoError(t, f.AddTable(sheetName, tableRange[0], tableRange[1], fmt.Sprintf(`{ + "table_name": "table%d", + "table_style": "TableStyleMedium2", + "show_first_column": true, + "show_last_column": true, + "show_row_stripes": false, + "show_column_stripes": true + }`, idx))) + } + assert.NoError(t, f.RemoveRow(sheetName, 2)) + assert.NoError(t, f.RemoveRow(sheetName, 3)) + assert.NoError(t, f.RemoveCol(sheetName, "H")) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAdjustTable.xlsx"))) + + f = NewFile() + assert.NoError(t, f.AddTable(sheetName, "A1", "D5", "")) + // Test adjust table with non-table part + f.Pkg.Delete("xl/tables/table1.xml") + assert.NoError(t, f.RemoveRow(sheetName, 1)) + // Test adjust table with unsupported charset + f.Pkg.Store("xl/tables/table1.xml", MacintoshCyrillicCharset) + assert.NoError(t, f.RemoveRow(sheetName, 1)) + // Test adjust table with invalid table range reference + f.Pkg.Store("xl/tables/table1.xml", []byte(``)) + assert.NoError(t, f.RemoveRow(sheetName, 1)) +} + func TestAdjustHelper(t *testing.T) { f := NewFile() f.NewSheet("Sheet2") @@ -303,10 +335,10 @@ func TestAdjustHelper(t *testing.T) { f.Sheet.Store("xl/worksheets/sheet2.xml", &xlsxWorksheet{ AutoFilter: &xlsxAutoFilter{Ref: "A1:B"}, }) - // testing adjustHelper with illegal cell coordinates. + // Test adjustHelper with illegal cell coordinates. assert.EqualError(t, f.adjustHelper("Sheet1", rows, 0, 0), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) assert.EqualError(t, f.adjustHelper("Sheet2", rows, 0, 0), newCellNameToCoordinatesError("B", newInvalidCellNameError("B")).Error()) - // testing adjustHelper on not exists worksheet. + // Test adjustHelper on not exists worksheet. assert.EqualError(t, f.adjustHelper("SheetN", rows, 0, 0), "sheet SheetN is not exist") } diff --git a/cell.go b/cell.go index 286085b..97425c5 100644 --- a/cell.go +++ b/cell.go @@ -169,6 +169,21 @@ func (c *xlsxC) hasValue() bool { return c.S != 0 || c.V != "" || c.F != nil || c.T != "" } +// removeFormula delete formula for the cell. +func (c *xlsxC) removeFormula(ws *xlsxWorksheet) { + if c.F != nil && c.F.T == STCellFormulaTypeShared && c.F.Ref != "" { + si := c.F.Si + for r, row := range ws.SheetData.Row { + for col, cell := range row.C { + if cell.F != nil && cell.F.Si != nil && *cell.F.Si == *si { + ws.SheetData.Row[r].C[col].F = nil + } + } + } + } + c.F = nil +} + // setCellIntFunc is a wrapper of SetCellInt. func (f *File) setCellIntFunc(sheet, axis string, value interface{}) error { var err error @@ -266,7 +281,8 @@ func (f *File) SetCellInt(sheet, axis string, value int) error { defer ws.Unlock() cellData.S = f.prepareCellStyle(ws, col, row, cellData.S) cellData.T, cellData.V = setCellInt(value) - cellData.F, cellData.IS = nil, nil + cellData.removeFormula(ws) + cellData.IS = nil return err } @@ -292,7 +308,8 @@ func (f *File) SetCellBool(sheet, axis string, value bool) error { defer ws.Unlock() cellData.S = f.prepareCellStyle(ws, col, row, cellData.S) cellData.T, cellData.V = setCellBool(value) - cellData.F, cellData.IS = nil, nil + cellData.removeFormula(ws) + cellData.IS = nil return err } @@ -330,7 +347,8 @@ func (f *File) SetCellFloat(sheet, axis string, value float64, precision, bitSiz defer ws.Unlock() cellData.S = f.prepareCellStyle(ws, col, row, cellData.S) cellData.T, cellData.V = setCellFloat(value, precision, bitSize) - cellData.F, cellData.IS = nil, nil + cellData.removeFormula(ws) + cellData.IS = nil return err } @@ -356,7 +374,8 @@ func (f *File) SetCellStr(sheet, axis, value string) error { defer ws.Unlock() cellData.S = f.prepareCellStyle(ws, col, row, cellData.S) cellData.T, cellData.V, err = f.setCellString(value) - cellData.F, cellData.IS = nil, nil + cellData.removeFormula(ws) + cellData.IS = nil return err } @@ -455,7 +474,8 @@ func (f *File) SetCellDefault(sheet, axis, value string) error { defer ws.Unlock() cellData.S = f.prepareCellStyle(ws, col, row, cellData.S) cellData.T, cellData.V = setCellDefault(value) - cellData.F, cellData.IS = nil, nil + cellData.removeFormula(ws) + cellData.IS = nil return err } diff --git a/picture_test.go b/picture_test.go index 60c6ac1..3ac1afb 100644 --- a/picture_test.go +++ b/picture_test.go @@ -173,7 +173,7 @@ func TestGetPicture(t *testing.T) { } func TestAddDrawingPicture(t *testing.T) { - // testing addDrawingPicture with illegal cell coordinates. + // Test addDrawingPicture with illegal cell coordinates. f := NewFile() assert.EqualError(t, f.addDrawingPicture("sheet1", "", "A", "", 0, 0, 0, 0, nil), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) } diff --git a/rows_test.go b/rows_test.go index 014b2d8..4fe2851 100644 --- a/rows_test.go +++ b/rows_test.go @@ -322,7 +322,7 @@ func TestInsertRow(t *testing.T) { assert.NoError(t, f.SaveAs(filepath.Join("test", "TestInsertRow.xlsx"))) } -// Testing internal structure state after insert operations. It is important +// Test internal structure state after insert operations. It is important // for insert workflow to be constant to avoid side effect with functions // related to internal structure. func TestInsertRowInEmptyFile(t *testing.T) { diff --git a/sheet.go b/sheet.go index 2a722c9..6dda811 100644 --- a/sheet.go +++ b/sheet.go @@ -478,12 +478,11 @@ func (f *File) SetSheetBackground(sheet, picture string) error { } // DeleteSheet provides a function to delete worksheet in a workbook by given -// worksheet name, the sheet names are not case-sensitive. The sheet names are -// not case-sensitive. Use this method with caution, which will affect -// changes in references such as formulas, charts, and so on. If there is any -// referenced value of the deleted worksheet, it will cause a file error when -// you open it. This function will be invalid when only the one worksheet is -// left. +// worksheet name, the sheet names are not case-sensitive. Use this method +// with caution, which will affect changes in references such as formulas, +// charts, and so on. If there is any referenced value of the deleted +// worksheet, it will cause a file error when you open it. This function will +// be invalid when only one worksheet is left func (f *File) DeleteSheet(name string) { if f.SheetCount == 1 || f.GetSheetIndex(name) == -1 { return diff --git a/table.go b/table.go index 413118c..84445b7 100644 --- a/table.go +++ b/table.go @@ -129,28 +129,18 @@ func (f *File) addSheetTable(sheet string, rID int) error { return err } -// addTable provides a function to add table by given worksheet name, -// coordinate area and format set. -func (f *File) addTable(sheet, tableXML string, x1, y1, x2, y2, i int, formatSet *formatTable) error { - // Correct the minimum number of rows, the table at least two lines. - if y1 == y2 { - y2++ - } - - // Correct table reference coordinate area, such correct C1:B3 to B1:C3. - ref, err := f.coordinatesToAreaRef([]int{x1, y1, x2, y2}) - if err != nil { - return err - } - - var tableColumn []*xlsxTableColumn - - idx := 0 +// setTableHeader provides a function to set cells value in header row for the +// table. +func (f *File) setTableHeader(sheet string, x1, y1, x2 int) ([]*xlsxTableColumn, error) { + var ( + tableColumns []*xlsxTableColumn + idx int + ) for i := x1; i <= x2; i++ { idx++ cell, err := CoordinatesToCellName(i, y1) if err != nil { - return err + return tableColumns, err } name, _ := f.GetCellValue(sheet, cell) if _, err := strconv.Atoi(name); err == nil { @@ -160,11 +150,28 @@ func (f *File) addTable(sheet, tableXML string, x1, y1, x2, y2, i int, formatSet name = "Column" + strconv.Itoa(idx) _ = f.SetCellStr(sheet, cell, name) } - tableColumn = append(tableColumn, &xlsxTableColumn{ + tableColumns = append(tableColumns, &xlsxTableColumn{ ID: idx, Name: name, }) } + return tableColumns, nil +} + +// addTable provides a function to add table by given worksheet name, +// coordinate area and format set. +func (f *File) addTable(sheet, tableXML string, x1, y1, x2, y2, i int, formatSet *formatTable) error { + // Correct the minimum number of rows, the table at least two lines. + if y1 == y2 { + y2++ + } + + // Correct table reference coordinate area, such correct C1:B3 to B1:C3. + ref, err := f.coordinatesToAreaRef([]int{x1, y1, x2, y2}) + if err != nil { + return err + } + tableColumns, _ := f.setTableHeader(sheet, x1, y1, x2) name := formatSet.TableName if name == "" { name = "Table" + strconv.Itoa(i) @@ -179,8 +186,8 @@ func (f *File) addTable(sheet, tableXML string, x1, y1, x2, y2, i int, formatSet Ref: ref, }, TableColumns: &xlsxTableColumns{ - Count: idx, - TableColumn: tableColumn, + Count: len(tableColumns), + TableColumn: tableColumns, }, TableStyleInfo: &xlsxTableStyleInfo{ Name: formatSet.TableStyle, diff --git a/table_test.go b/table_test.go index 0a74b1b..5941c50 100644 --- a/table_test.go +++ b/table_test.go @@ -45,6 +45,12 @@ func TestAddTable(t *testing.T) { assert.EqualError(t, f.addTable("sheet1", "", 1, 1, 0, 0, 0, nil), "invalid cell coordinates [0, 0]") } +func TestSetTableHeader(t *testing.T) { + f := NewFile() + _, err := f.setTableHeader("Sheet1", 1, 0, 1) + assert.EqualError(t, err, "invalid cell coordinates [1, 0]") +} + func TestAutoFilter(t *testing.T) { outFile := filepath.Join("test", "TestAutoFilter%d.xlsx") @@ -72,7 +78,7 @@ func TestAutoFilter(t *testing.T) { }) } - // testing AutoFilter with illegal cell coordinates. + // Test AutoFilter with illegal cell coordinates. assert.EqualError(t, f.AutoFilter("Sheet1", "A", "B1", ""), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) assert.EqualError(t, f.AutoFilter("Sheet1", "A1", "B", ""), newCellNameToCoordinatesError("B", newInvalidCellNameError("B")).Error()) }