From 12c1e2481e3f9f3c3c12a938484f04b12d5dede8 Mon Sep 17 00:00:00 2001 From: Veniamin Albaev Date: Wed, 6 Mar 2019 16:40:45 +0300 Subject: [PATCH] Implement consistent row addressing by Excel row number starting with 1 (#350) * Implement consistent row addressing by Excel row number starting with 1 1. Added second versions for all row manipulation methods with zero-based row addressing. 2. Fixed methods documentation to explicitly describe which row addressing used in method. 3. Added WARNING to README.md. 4. Cosmetic change: All row test moved to file `rows_test.go`. * TravisCI: go1.12 added to tests matrix * BACKWARD INCOMPARTIBLE: Use only Excel numbering logic from 1 row * README updated --- .travis.yml | 1 + README.md | 5 + excelize_test.go | 424 --------------------------------------- rows.go | 111 ++++++----- rows_test.go | 507 +++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 575 insertions(+), 473 deletions(-) create mode 100644 rows_test.go diff --git a/.travis.yml b/.travis.yml index c2f0f905..6c061a87 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,6 +8,7 @@ go: - 1.9.x - 1.10.x - 1.11.x + - 1.12.x os: - linux diff --git a/README.md b/README.md index 86d1e8a3..c4936b6c 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,11 @@ Excelize is a library written in pure Go and providing a set of functions that allow you to write to and read from XLSX files. Support reads and writes XLSX file generated by Microsoft Excel™ 2007 and later. Support save file without losing original charts of XLSX. This library needs Go version 1.8 or later. The full API docs can be seen using go's built-in documentation tool, or online at [godoc.org](https://godoc.org/github.com/360EntSecGroup-Skylar/excelize) and [docs reference](https://xuri.me/excelize/). +**WARNING!** + +From version 1.5 all row manipulation methods uses Excel row numbering starting with `1` instead of zero-based numbering +which take place in some methods in eraler versions. + ## Basic Usage ### Installation diff --git a/excelize_test.go b/excelize_test.go index ed6f073c..abee199f 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -287,19 +287,6 @@ func TestColWidth(t *testing.T) { convertRowHeightToPixels(0) } -func TestRowHeight(t *testing.T) { - xlsx := NewFile() - xlsx.SetRowHeight("Sheet1", 1, 50) - xlsx.SetRowHeight("Sheet1", 4, 90) - t.Log(xlsx.GetRowHeight("Sheet1", 1)) - t.Log(xlsx.GetRowHeight("Sheet1", 0)) - err := xlsx.SaveAs(filepath.Join("test", "TestRowHeight.xlsx")) - if !assert.NoError(t, err) { - t.FailNow() - } - convertColWidthToPixels(0) -} - func TestSetCellHyperLink(t *testing.T) { xlsx, err := OpenFile(filepath.Join("test", "Book1.xlsx")) if err != nil { @@ -1112,335 +1099,6 @@ func TestRemoveCol(t *testing.T) { assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestRemoveCol.xlsx"))) } -func TestInsertRow(t *testing.T) { - xlsx := NewFile() - for j := 1; j <= 10; j++ { - for i := 0; i <= 10; i++ { - axis := ToAlphaString(i) + strconv.Itoa(j) - xlsx.SetCellStr("Sheet1", axis, axis) - } - } - xlsx.SetCellHyperLink("Sheet1", "A5", "https://github.com/360EntSecGroup-Skylar/excelize", "External") - xlsx.InsertRow("Sheet1", -1) - xlsx.InsertRow("Sheet1", 4) - - assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestInsertRow.xlsx"))) -} - -// Testing internal sructure 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) { - xlsx := NewFile() - sheet1 := xlsx.GetSheetName(1) - r := xlsx.workSheetReader(sheet1) - xlsx.InsertRow(sheet1, 0) - assert.Len(t, r.SheetData.Row, 0) - xlsx.InsertRow(sheet1, 1) - assert.Len(t, r.SheetData.Row, 0) - xlsx.InsertRow(sheet1, 99) - assert.Len(t, r.SheetData.Row, 0) - assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestInsertRowInEmptyFile.xlsx"))) -} - -func TestDuplicateRow(t *testing.T) { - const sheet = "Sheet1" - outFile := filepath.Join("test", "TestDuplicateRow.%s.xlsx") - - cells := map[string]string{ - "A1": "A1 Value", - "A2": "A2 Value", - "A3": "A3 Value", - "B1": "B1 Value", - "B2": "B2 Value", - "B3": "B3 Value", - } - - newFileWithDefaults := func() *File { - f := NewFile() - for cell, val := range cells { - f.SetCellStr(sheet, cell, val) - - } - return f - } - - t.Run("FromSingleRow", func(t *testing.T) { - xlsx := NewFile() - xlsx.SetCellStr(sheet, "A1", cells["A1"]) - xlsx.SetCellStr(sheet, "B1", cells["B1"]) - - xlsx.DuplicateRow(sheet, 1) - if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.FromSingleRow_1"))) { - t.FailNow() - } - expect := map[string]string{ - "A1": cells["A1"], "B1": cells["B1"], - "A2": cells["A1"], "B2": cells["B1"], - } - for cell, val := range expect { - if !assert.Equal(t, val, xlsx.GetCellValue(sheet, cell), cell) { - t.FailNow() - } - } - - xlsx.DuplicateRow(sheet, 2) - if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.FromSingleRow_2"))) { - t.FailNow() - } - expect = map[string]string{ - "A1": cells["A1"], "B1": cells["B1"], - "A2": cells["A1"], "B2": cells["B1"], - "A3": cells["A1"], "B3": cells["B1"], - } - for cell, val := range expect { - if !assert.Equal(t, val, xlsx.GetCellValue(sheet, cell), cell) { - t.FailNow() - } - } - }) - - t.Run("UpdateDuplicatedRows", func(t *testing.T) { - xlsx := NewFile() - xlsx.SetCellStr(sheet, "A1", cells["A1"]) - xlsx.SetCellStr(sheet, "B1", cells["B1"]) - - xlsx.DuplicateRow(sheet, 1) - - xlsx.SetCellStr(sheet, "A2", cells["A2"]) - xlsx.SetCellStr(sheet, "B2", cells["B2"]) - - if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.UpdateDuplicatedRows"))) { - t.FailNow() - } - expect := map[string]string{ - "A1": cells["A1"], "B1": cells["B1"], - "A2": cells["A2"], "B2": cells["B2"], - } - for cell, val := range expect { - if !assert.Equal(t, val, xlsx.GetCellValue(sheet, cell), cell) { - t.FailNow() - } - } - }) - - t.Run("FirstOfMultipleRows", func(t *testing.T) { - xlsx := newFileWithDefaults() - - xlsx.DuplicateRow(sheet, 1) - - if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.FirstOfMultipleRows"))) { - t.FailNow() - } - expect := map[string]string{ - "A1": cells["A1"], "B1": cells["B1"], - "A2": cells["A1"], "B2": cells["B1"], - "A3": cells["A2"], "B3": cells["B2"], - "A4": cells["A3"], "B4": cells["B3"], - } - for cell, val := range expect { - if !assert.Equal(t, val, xlsx.GetCellValue(sheet, cell), cell) { - t.FailNow() - } - } - }) - - t.Run("ZeroWithNoRows", func(t *testing.T) { - xlsx := NewFile() - - xlsx.DuplicateRow(sheet, 0) - - if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.ZeroWithNoRows"))) { - t.FailNow() - } - assert.Equal(t, "", xlsx.GetCellValue(sheet, "A1")) - assert.Equal(t, "", xlsx.GetCellValue(sheet, "B1")) - assert.Equal(t, "", xlsx.GetCellValue(sheet, "A2")) - assert.Equal(t, "", xlsx.GetCellValue(sheet, "B2")) - expect := map[string]string{ - "A1": "", "B1": "", - "A2": "", "B2": "", - } - for cell, val := range expect { - if !assert.Equal(t, val, xlsx.GetCellValue(sheet, cell), cell) { - t.FailNow() - } - } - }) - - t.Run("MiddleRowOfEmptyFile", func(t *testing.T) { - xlsx := NewFile() - - xlsx.DuplicateRow(sheet, 99) - - if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.MiddleRowOfEmptyFile"))) { - t.FailNow() - } - expect := map[string]string{ - "A98": "", - "A99": "", - "A100": "", - } - for cell, val := range expect { - if !assert.Equal(t, val, xlsx.GetCellValue(sheet, cell), cell) { - t.FailNow() - } - } - }) - - t.Run("WithLargeOffsetToMiddleOfData", func(t *testing.T) { - xlsx := newFileWithDefaults() - - xlsx.DuplicateRowTo(sheet, 1, 3) - - if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.WithLargeOffsetToMiddleOfData"))) { - t.FailNow() - } - expect := map[string]string{ - "A1": cells["A1"], "B1": cells["B1"], - "A2": cells["A2"], "B2": cells["B2"], - "A3": cells["A1"], "B3": cells["B1"], - "A4": cells["A3"], "B4": cells["B3"], - } - for cell, val := range expect { - if !assert.Equal(t, val, xlsx.GetCellValue(sheet, cell), cell) { - t.FailNow() - } - } - }) - - t.Run("WithLargeOffsetToEmptyRows", func(t *testing.T) { - xlsx := newFileWithDefaults() - - xlsx.DuplicateRowTo(sheet, 1, 7) - - if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.WithLargeOffsetToEmptyRows"))) { - t.FailNow() - } - expect := map[string]string{ - "A1": cells["A1"], "B1": cells["B1"], - "A2": cells["A2"], "B2": cells["B2"], - "A3": cells["A3"], "B3": cells["B3"], - "A7": cells["A1"], "B7": cells["B1"], - } - for cell, val := range expect { - if !assert.Equal(t, val, xlsx.GetCellValue(sheet, cell), cell) { - t.FailNow() - } - } - }) - - t.Run("InsertBefore", func(t *testing.T) { - xlsx := newFileWithDefaults() - - xlsx.DuplicateRowTo(sheet, 2, 1) - - if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.InsertBefore"))) { - t.FailNow() - } - - expect := map[string]string{ - "A1": cells["A2"], "B1": cells["B2"], - "A2": cells["A1"], "B2": cells["B1"], - "A3": cells["A2"], "B3": cells["B2"], - "A4": cells["A3"], "B4": cells["B3"], - } - for cell, val := range expect { - if !assert.Equal(t, val, xlsx.GetCellValue(sheet, cell), cell) { - t.FailNow() - } - } - }) - - t.Run("InsertBeforeWithLargeOffset", func(t *testing.T) { - xlsx := newFileWithDefaults() - - xlsx.DuplicateRowTo(sheet, 3, 1) - - if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.InsertBeforeWithLargeOffset"))) { - t.FailNow() - } - - expect := map[string]string{ - "A1": cells["A3"], "B1": cells["B3"], - "A2": cells["A1"], "B2": cells["B1"], - "A3": cells["A2"], "B3": cells["B2"], - "A4": cells["A3"], "B4": cells["B3"], - } - for cell, val := range expect { - if !assert.Equal(t, val, xlsx.GetCellValue(sheet, cell)) { - t.FailNow() - } - } - }) -} - -func TestDuplicateRowInvalidRownum(t *testing.T) { - const sheet = "Sheet1" - outFile := filepath.Join("test", "TestDuplicateRowInvalidRownum.%s.xlsx") - - cells := map[string]string{ - "A1": "A1 Value", - "A2": "A2 Value", - "A3": "A3 Value", - "B1": "B1 Value", - "B2": "B2 Value", - "B3": "B3 Value", - } - - testRows := []int{-2, -1} - - testRowPairs := []struct { - row1 int - row2 int - }{ - {-1, -1}, - {-1, 0}, - {-1, 1}, - {0, -1}, - {0, 0}, - {0, 1}, - {1, -1}, - {1, 1}, - {1, 0}, - } - - for i, row := range testRows { - name := fmt.Sprintf("TestRow_%d", i+1) - t.Run(name, func(t *testing.T) { - xlsx := NewFile() - for col, val := range cells { - xlsx.SetCellStr(sheet, col, val) - } - xlsx.DuplicateRow(sheet, row) - - for col, val := range cells { - if !assert.Equal(t, val, xlsx.GetCellValue(sheet, col)) { - t.FailNow() - } - } - assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, name))) - }) - } - - for i, pair := range testRowPairs { - name := fmt.Sprintf("TestRowPair_%d", i+1) - t.Run(name, func(t *testing.T) { - xlsx := NewFile() - for col, val := range cells { - xlsx.SetCellStr(sheet, col, val) - } - xlsx.DuplicateRowTo(sheet, pair.row1, pair.row2) - - for col, val := range cells { - if !assert.Equal(t, val, xlsx.GetCellValue(sheet, col)) { - t.FailNow() - } - } - assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, name))) - }) - } -} - func TestSetPane(t *testing.T) { xlsx := NewFile() xlsx.SetPanes("Sheet1", `{"freeze":false,"split":false}`) @@ -1455,33 +1113,6 @@ func TestSetPane(t *testing.T) { assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestSetPane.xlsx"))) } -func TestRemoveRow(t *testing.T) { - xlsx := NewFile() - for j := 1; j <= 10; j++ { - for i := 0; i <= 10; i++ { - axis := ToAlphaString(i) + strconv.Itoa(j) - xlsx.SetCellStr("Sheet1", axis, axis) - } - } - xlsx.SetCellHyperLink("Sheet1", "A5", "https://github.com/360EntSecGroup-Skylar/excelize", "External") - xlsx.RemoveRow("Sheet1", -1) - xlsx.RemoveRow("Sheet1", 4) - xlsx.MergeCell("Sheet1", "B3", "B5") - xlsx.RemoveRow("Sheet1", 2) - xlsx.RemoveRow("Sheet1", 4) - - err := xlsx.AutoFilter("Sheet1", "A2", "A2", `{"column":"A","expression":"x != blanks"}`) - if !assert.NoError(t, err) { - t.FailNow() - } - - xlsx.RemoveRow("Sheet1", 0) - xlsx.RemoveRow("Sheet1", 1) - xlsx.RemoveRow("Sheet1", 0) - - assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestRemoveRow.xlsx"))) -} - func TestConditionalFormat(t *testing.T) { xlsx := NewFile() for j := 1; j <= 10; j++ { @@ -1604,50 +1235,6 @@ func TestSetSheetRow(t *testing.T) { assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestSetSheetRow.xlsx"))) } -func TestRows(t *testing.T) { - xlsx, err := OpenFile(filepath.Join("test", "Book1.xlsx")) - if !assert.NoError(t, err) { - t.FailNow() - } - - rows, err := xlsx.Rows("Sheet2") - if !assert.NoError(t, err) { - t.FailNow() - } - - rowStrs := make([][]string, 0) - var i = 0 - for rows.Next() { - i++ - columns := rows.Columns() - rowStrs = append(rowStrs, columns) - } - - if !assert.NoError(t, rows.Error()) { - t.FailNow() - } - - dstRows := xlsx.GetRows("Sheet2") - if !assert.Equal(t, len(rowStrs), len(dstRows)) { - t.FailNow() - } - - for i := 0; i < len(rowStrs); i++ { - if !assert.Equal(t, trimSliceSpace(dstRows[i]), trimSliceSpace(rowStrs[i])) { - t.FailNow() - } - } -} - -func TestRowsError(t *testing.T) { - xlsx, err := OpenFile(filepath.Join("test", "Book1.xlsx")) - if !assert.NoError(t, err) { - t.FailNow() - } - _, err = xlsx.Rows("SheetN") - assert.EqualError(t, err, "Sheet SheetN is not exist") -} - func TestOutlineLevel(t *testing.T) { xlsx := NewFile() xlsx.NewSheet("Sheet2") @@ -1733,17 +1320,6 @@ func TestUnprotectSheet(t *testing.T) { assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestUnprotectSheet.xlsx"))) } -func trimSliceSpace(s []string) []string { - for { - if len(s) > 0 && s[len(s)-1] == "" { - s = s[:len(s)-1] - } else { - break - } - } - return s -} - func prepareTestBook1() (*File, error) { xlsx, err := OpenFile(filepath.Join("test", "Book1.xlsx")) if err != nil { diff --git a/rows.go b/rows.go index aebc9794..79d8e46b 100644 --- a/rows.go +++ b/rows.go @@ -203,6 +203,9 @@ func (f *File) getTotalRowsCols(name string) (int, int) { // func (f *File) SetRowHeight(sheet string, row int, height float64) { xlsx := f.workSheetReader(sheet) + if row < 1 { + return + } cells := 0 rowIdx := row - 1 completeRow(xlsx, row, cells) @@ -230,6 +233,9 @@ func (f *File) getRowHeight(sheet string, row int) int { // func (f *File) GetRowHeight(sheet string, row int) float64 { xlsx := f.workSheetReader(sheet) + if row < 1 || row > len(xlsx.SheetData.Row) { + return defaultRowHeightPixels // it will be better to use 0, but we take care with BC + } for _, v := range xlsx.SheetData.Row { if v.R == row && v.Ht != 0 { return v.Ht @@ -279,80 +285,88 @@ func (xlsx *xlsxC) getValueFrom(f *File, d *xlsxSST) (string, error) { } } -// SetRowVisible provides a function to set visible of a single row by given -// worksheet name and row index. For example, hide row 2 in Sheet1: +// SetRowVisible2 provides a function to set visible of a single row by given +// worksheet name and Excel row number. For example, hide row 2 in Sheet1: // // xlsx.SetRowVisible("Sheet1", 2, false) // -func (f *File) SetRowVisible(sheet string, rowIndex int, visible bool) { +func (f *File) SetRowVisible(sheet string, row int, visible bool) { xlsx := f.workSheetReader(sheet) - rows := rowIndex + 1 - cells := 0 - completeRow(xlsx, rows, cells) - if visible { - xlsx.SheetData.Row[rowIndex].Hidden = false + if row < 1 { return } - xlsx.SheetData.Row[rowIndex].Hidden = true + cells := 0 + completeRow(xlsx, row, cells) + rowIdx := row - 1 + if visible { + xlsx.SheetData.Row[rowIdx].Hidden = false + return + } + xlsx.SheetData.Row[rowIdx].Hidden = true } -// GetRowVisible provides a function to get visible of a single row by given -// worksheet name and row index. For example, get visible state of row 2 in +// GetRowVisible2 provides a function to get visible of a single row by given +// worksheet name and Excel row number. +// For example, get visible state of row 2 in // Sheet1: // // xlsx.GetRowVisible("Sheet1", 2) // -func (f *File) GetRowVisible(sheet string, rowIndex int) bool { +func (f *File) GetRowVisible(sheet string, row int) bool { xlsx := f.workSheetReader(sheet) - rows := rowIndex + 1 + if row < 1 || row > len(xlsx.SheetData.Row) { + return false + } + rowIndex := row - 1 cells := 0 - completeRow(xlsx, rows, cells) + completeRow(xlsx, row, cells) return !xlsx.SheetData.Row[rowIndex].Hidden } // SetRowOutlineLevel provides a function to set outline level number of a -// single row by given worksheet name and row index. For example, outline row +// single row by given worksheet name and Excel row number. For example, outline row // 2 in Sheet1 to level 1: // // xlsx.SetRowOutlineLevel("Sheet1", 2, 1) // -func (f *File) SetRowOutlineLevel(sheet string, rowIndex int, level uint8) { +func (f *File) SetRowOutlineLevel(sheet string, row int, level uint8) { xlsx := f.workSheetReader(sheet) - rows := rowIndex + 1 + if row < 1 { + return + } cells := 0 - completeRow(xlsx, rows, cells) - xlsx.SheetData.Row[rowIndex].OutlineLevel = level + completeRow(xlsx, row, cells) + xlsx.SheetData.Row[row-1].OutlineLevel = level } // GetRowOutlineLevel provides a function to get outline level number of a -// single row by given worksheet name and row index. For example, get outline -// number of row 2 in Sheet1: +// single row by given worksheet name and Exce row number. +// For example, get outline number of row 2 in Sheet1: // // xlsx.GetRowOutlineLevel("Sheet1", 2) // -func (f *File) GetRowOutlineLevel(sheet string, rowIndex int) uint8 { +func (f *File) GetRowOutlineLevel(sheet string, row int) uint8 { xlsx := f.workSheetReader(sheet) - rows := rowIndex + 1 - cells := 0 - completeRow(xlsx, rows, cells) - return xlsx.SheetData.Row[rowIndex].OutlineLevel + if row < 1 || row > len(xlsx.SheetData.Row) { + return 0 + } + return xlsx.SheetData.Row[row-1].OutlineLevel } -// RemoveRow provides a function to remove single row by given worksheet name -// and row index. For example, remove row 3 in Sheet1: +// RemoveRow2 provides a function to remove single row by given worksheet name +// and Excel row number. For example, remove row 3 in Sheet1: // -// xlsx.RemoveRow("Sheet1", 2) +// xlsx.RemoveRow("Sheet1", 3) // // 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 // worksheet, it will cause a file error when you open it. The excelize only // partially updates these references currently. func (f *File) RemoveRow(sheet string, row int) { - if row < 0 { + xlsx := f.workSheetReader(sheet) + if row < 1 || row > len(xlsx.SheetData.Row) { return } - xlsx := f.workSheetReader(sheet) - row++ for i, r := range xlsx.SheetData.Row { if r.R == row { xlsx.SheetData.Row = append(xlsx.SheetData.Row[:i], xlsx.SheetData.Row[i+1:]...) @@ -362,20 +376,19 @@ func (f *File) RemoveRow(sheet string, row int) { } } -// InsertRow provides a function to insert a new row after given row index. -// For example, create a new row before row 3 in Sheet1: +// InsertRow2 provides a function to insert a new row after given Excel row number +// starting from 1. For example, create a new row before row 3 in Sheet1: // -// xlsx.InsertRow("Sheet1", 2) +// xlsx.InsertRow("Sheet1", 3) // func (f *File) InsertRow(sheet string, row int) { - if row < 0 { + if row < 1 { return } - row++ f.adjustHelper(sheet, -1, row, 1) } -// DuplicateRow inserts a copy of specified row below specified +// DuplicateRow inserts a copy of specified row (by it Excel row number) below // // xlsx.DuplicateRow("Sheet1", 2) // @@ -387,8 +400,8 @@ func (f *File) DuplicateRow(sheet string, row int) { f.DuplicateRowTo(sheet, row, row+1) } -// DuplicateRowTo inserts a copy of specified row at specified row position -// moving down exists rows after target position +// DuplicateRowTo inserts a copy of specified row by it Excel number +// to specified row position moving down exists rows after target position // // xlsx.DuplicateRowTo("Sheet1", 2, 7) // @@ -397,18 +410,18 @@ func (f *File) DuplicateRow(sheet string, row int) { // worksheet, it will cause a file error when you open it. The excelize only // partially updates these references currently. func (f *File) DuplicateRowTo(sheet string, row, row2 int) { - if row <= 0 || row2 <= 0 || row == row2 { + xlsx := f.workSheetReader(sheet) + + if row < 1 || row > len(xlsx.SheetData.Row) || row2 < 1 || row == row2 { return } - ws := f.workSheetReader(sheet) - var ok bool var rowCopy xlsxRow - for i, r := range ws.SheetData.Row { + for i, r := range xlsx.SheetData.Row { if r.R == row { - rowCopy = ws.SheetData.Row[i] + rowCopy = xlsx.SheetData.Row[i] ok = true break } @@ -420,13 +433,13 @@ func (f *File) DuplicateRowTo(sheet string, row, row2 int) { f.adjustHelper(sheet, -1, row2, 1) idx2 := -1 - for i, r := range ws.SheetData.Row { + for i, r := range xlsx.SheetData.Row { if r.R == row2 { idx2 = i break } } - if idx2 == -1 && len(ws.SheetData.Row) >= row2 { + if idx2 == -1 && len(xlsx.SheetData.Row) >= row2 { return } @@ -434,9 +447,9 @@ func (f *File) DuplicateRowTo(sheet string, row, row2 int) { f.ajustSingleRowDimensions(&rowCopy, row2) if idx2 != -1 { - ws.SheetData.Row[idx2] = rowCopy + xlsx.SheetData.Row[idx2] = rowCopy } else { - ws.SheetData.Row = append(ws.SheetData.Row, rowCopy) + xlsx.SheetData.Row = append(xlsx.SheetData.Row, rowCopy) } } diff --git a/rows_test.go b/rows_test.go new file mode 100644 index 00000000..26fcb473 --- /dev/null +++ b/rows_test.go @@ -0,0 +1,507 @@ +package excelize + +import ( + "fmt" + "path/filepath" + "strconv" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestRows(t *testing.T) { + xlsx, err := OpenFile(filepath.Join("test", "Book1.xlsx")) + if !assert.NoError(t, err) { + t.FailNow() + } + + rows, err := xlsx.Rows("Sheet2") + if !assert.NoError(t, err) { + t.FailNow() + } + + rowStrs := make([][]string, 0) + var i = 0 + for rows.Next() { + i++ + columns := rows.Columns() + rowStrs = append(rowStrs, columns) + } + + if !assert.NoError(t, rows.Error()) { + t.FailNow() + } + + dstRows := xlsx.GetRows("Sheet2") + if !assert.Equal(t, len(rowStrs), len(dstRows)) { + t.FailNow() + } + + for i := 0; i < len(rowStrs); i++ { + if !assert.Equal(t, trimSliceSpace(dstRows[i]), trimSliceSpace(rowStrs[i])) { + t.FailNow() + } + } +} + +func TestRowsError(t *testing.T) { + xlsx, err := OpenFile(filepath.Join("test", "Book1.xlsx")) + if !assert.NoError(t, err) { + t.FailNow() + } + _, err = xlsx.Rows("SheetN") + assert.EqualError(t, err, "Sheet SheetN is not exist") +} + +func TestRowHeight(t *testing.T) { + xlsx := NewFile() + sheet1 := xlsx.GetSheetName(1) + + xlsx.SetRowHeight(sheet1, 0, defaultRowHeightPixels+1.0) // should no effect + assert.Equal(t, defaultRowHeightPixels, xlsx.GetRowHeight("Sheet1", 0)) + + xlsx.SetRowHeight(sheet1, 1, 111.0) + assert.Equal(t, 111.0, xlsx.GetRowHeight(sheet1, 1)) + + xlsx.SetRowHeight(sheet1, 4, 444.0) + assert.Equal(t, 444.0, xlsx.GetRowHeight(sheet1, 4)) + + err := xlsx.SaveAs(filepath.Join("test", "TestRowHeight.xlsx")) + if !assert.NoError(t, err) { + t.FailNow() + } + + convertColWidthToPixels(0) +} + +func TestRemoveRow(t *testing.T) { + xlsx := NewFile() + sheet1 := xlsx.GetSheetName(1) + r := xlsx.workSheetReader(sheet1) + + const ( + cellCount = 10 + rowCount = 10 + ) + for j := 1; j <= cellCount; j++ { + for i := 1; i <= rowCount; i++ { + axis := ToAlphaString(i) + strconv.Itoa(j) + xlsx.SetCellStr(sheet1, axis, axis) + } + } + xlsx.SetCellHyperLink(sheet1, "A5", "https://github.com/360EntSecGroup-Skylar/excelize", "External") + + xlsx.RemoveRow(sheet1, -1) + if !assert.Len(t, r.SheetData.Row, rowCount) { + t.FailNow() + } + + xlsx.RemoveRow(sheet1, 0) + if !assert.Len(t, r.SheetData.Row, rowCount) { + t.FailNow() + } + + xlsx.RemoveRow(sheet1, 4) + if !assert.Len(t, r.SheetData.Row, rowCount-1) { + t.FailNow() + } + + xlsx.MergeCell(sheet1, "B3", "B5") + + xlsx.RemoveRow(sheet1, 2) + if !assert.Len(t, r.SheetData.Row, rowCount-2) { + t.FailNow() + } + + xlsx.RemoveRow(sheet1, 4) + if !assert.Len(t, r.SheetData.Row, rowCount-3) { + t.FailNow() + } + + err := xlsx.AutoFilter(sheet1, "A2", "A2", `{"column":"A","expression":"x != blanks"}`) + if !assert.NoError(t, err) { + t.FailNow() + } + + xlsx.RemoveRow(sheet1, 1) + if !assert.Len(t, r.SheetData.Row, rowCount-4) { + t.FailNow() + } + + xlsx.RemoveRow(sheet1, 2) + if !assert.Len(t, r.SheetData.Row, rowCount-5) { + t.FailNow() + } + + xlsx.RemoveRow(sheet1, 1) + if !assert.Len(t, r.SheetData.Row, rowCount-6) { + t.FailNow() + } + + assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestRemoveRow.xlsx"))) +} + +func TestInsertRow(t *testing.T) { + xlsx := NewFile() + sheet1 := xlsx.GetSheetName(1) + r := xlsx.workSheetReader(sheet1) + + const ( + cellCount = 10 + rowCount = 10 + ) + for j := 1; j <= cellCount; j++ { + for i := 1; i < rowCount; i++ { + axis := ToAlphaString(i) + strconv.Itoa(j) + xlsx.SetCellStr(sheet1, axis, axis) + } + } + xlsx.SetCellHyperLink(sheet1, "A5", "https://github.com/360EntSecGroup-Skylar/excelize", "External") + + xlsx.InsertRow(sheet1, -1) + if !assert.Len(t, r.SheetData.Row, rowCount) { + t.FailNow() + } + + xlsx.InsertRow(sheet1, 0) + if !assert.Len(t, r.SheetData.Row, rowCount) { + t.FailNow() + } + + xlsx.InsertRow(sheet1, 1) + if !assert.Len(t, r.SheetData.Row, rowCount+1) { + t.FailNow() + } + + xlsx.InsertRow(sheet1, 4) + if !assert.Len(t, r.SheetData.Row, rowCount+2) { + t.FailNow() + } + + assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestInsertRow.xlsx"))) +} + +// Testing internal sructure 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) { + xlsx := NewFile() + sheet1 := xlsx.GetSheetName(1) + r := xlsx.workSheetReader(sheet1) + xlsx.InsertRow(sheet1, 1) + assert.Len(t, r.SheetData.Row, 0) + xlsx.InsertRow(sheet1, 2) + assert.Len(t, r.SheetData.Row, 0) + xlsx.InsertRow(sheet1, 99) + assert.Len(t, r.SheetData.Row, 0) + assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestInsertRowInEmptyFile.xlsx"))) +} + +func TestDuplicateRow(t *testing.T) { + const sheet = "Sheet1" + outFile := filepath.Join("test", "TestDuplicateRow.%s.xlsx") + + cells := map[string]string{ + "A1": "A1 Value", + "A2": "A2 Value", + "A3": "A3 Value", + "B1": "B1 Value", + "B2": "B2 Value", + "B3": "B3 Value", + } + + newFileWithDefaults := func() *File { + f := NewFile() + for cell, val := range cells { + f.SetCellStr(sheet, cell, val) + + } + return f + } + + t.Run("FromSingleRow", func(t *testing.T) { + xlsx := NewFile() + xlsx.SetCellStr(sheet, "A1", cells["A1"]) + xlsx.SetCellStr(sheet, "B1", cells["B1"]) + + xlsx.DuplicateRow(sheet, 1) + if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.FromSingleRow_1"))) { + t.FailNow() + } + expect := map[string]string{ + "A1": cells["A1"], "B1": cells["B1"], + "A2": cells["A1"], "B2": cells["B1"], + } + for cell, val := range expect { + if !assert.Equal(t, val, xlsx.GetCellValue(sheet, cell), cell) { + t.FailNow() + } + } + + xlsx.DuplicateRow(sheet, 2) + if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.FromSingleRow_2"))) { + t.FailNow() + } + expect = map[string]string{ + "A1": cells["A1"], "B1": cells["B1"], + "A2": cells["A1"], "B2": cells["B1"], + "A3": cells["A1"], "B3": cells["B1"], + } + for cell, val := range expect { + if !assert.Equal(t, val, xlsx.GetCellValue(sheet, cell), cell) { + t.FailNow() + } + } + }) + + t.Run("UpdateDuplicatedRows", func(t *testing.T) { + xlsx := NewFile() + xlsx.SetCellStr(sheet, "A1", cells["A1"]) + xlsx.SetCellStr(sheet, "B1", cells["B1"]) + + xlsx.DuplicateRow(sheet, 1) + + xlsx.SetCellStr(sheet, "A2", cells["A2"]) + xlsx.SetCellStr(sheet, "B2", cells["B2"]) + + if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.UpdateDuplicatedRows"))) { + t.FailNow() + } + expect := map[string]string{ + "A1": cells["A1"], "B1": cells["B1"], + "A2": cells["A2"], "B2": cells["B2"], + } + for cell, val := range expect { + if !assert.Equal(t, val, xlsx.GetCellValue(sheet, cell), cell) { + t.FailNow() + } + } + }) + + t.Run("FirstOfMultipleRows", func(t *testing.T) { + xlsx := newFileWithDefaults() + + xlsx.DuplicateRow(sheet, 1) + + if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.FirstOfMultipleRows"))) { + t.FailNow() + } + expect := map[string]string{ + "A1": cells["A1"], "B1": cells["B1"], + "A2": cells["A1"], "B2": cells["B1"], + "A3": cells["A2"], "B3": cells["B2"], + "A4": cells["A3"], "B4": cells["B3"], + } + for cell, val := range expect { + if !assert.Equal(t, val, xlsx.GetCellValue(sheet, cell), cell) { + t.FailNow() + } + } + }) + + t.Run("ZeroWithNoRows", func(t *testing.T) { + xlsx := NewFile() + + xlsx.DuplicateRow(sheet, 0) + + if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.ZeroWithNoRows"))) { + t.FailNow() + } + assert.Equal(t, "", xlsx.GetCellValue(sheet, "A1")) + assert.Equal(t, "", xlsx.GetCellValue(sheet, "B1")) + assert.Equal(t, "", xlsx.GetCellValue(sheet, "A2")) + assert.Equal(t, "", xlsx.GetCellValue(sheet, "B2")) + expect := map[string]string{ + "A1": "", "B1": "", + "A2": "", "B2": "", + } + for cell, val := range expect { + if !assert.Equal(t, val, xlsx.GetCellValue(sheet, cell), cell) { + t.FailNow() + } + } + }) + + t.Run("MiddleRowOfEmptyFile", func(t *testing.T) { + xlsx := NewFile() + + xlsx.DuplicateRow(sheet, 99) + + if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.MiddleRowOfEmptyFile"))) { + t.FailNow() + } + expect := map[string]string{ + "A98": "", + "A99": "", + "A100": "", + } + for cell, val := range expect { + if !assert.Equal(t, val, xlsx.GetCellValue(sheet, cell), cell) { + t.FailNow() + } + } + }) + + t.Run("WithLargeOffsetToMiddleOfData", func(t *testing.T) { + xlsx := newFileWithDefaults() + + xlsx.DuplicateRowTo(sheet, 1, 3) + + if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.WithLargeOffsetToMiddleOfData"))) { + t.FailNow() + } + expect := map[string]string{ + "A1": cells["A1"], "B1": cells["B1"], + "A2": cells["A2"], "B2": cells["B2"], + "A3": cells["A1"], "B3": cells["B1"], + "A4": cells["A3"], "B4": cells["B3"], + } + for cell, val := range expect { + if !assert.Equal(t, val, xlsx.GetCellValue(sheet, cell), cell) { + t.FailNow() + } + } + }) + + t.Run("WithLargeOffsetToEmptyRows", func(t *testing.T) { + xlsx := newFileWithDefaults() + + xlsx.DuplicateRowTo(sheet, 1, 7) + + if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.WithLargeOffsetToEmptyRows"))) { + t.FailNow() + } + expect := map[string]string{ + "A1": cells["A1"], "B1": cells["B1"], + "A2": cells["A2"], "B2": cells["B2"], + "A3": cells["A3"], "B3": cells["B3"], + "A7": cells["A1"], "B7": cells["B1"], + } + for cell, val := range expect { + if !assert.Equal(t, val, xlsx.GetCellValue(sheet, cell), cell) { + t.FailNow() + } + } + }) + + t.Run("InsertBefore", func(t *testing.T) { + xlsx := newFileWithDefaults() + + xlsx.DuplicateRowTo(sheet, 2, 1) + + if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.InsertBefore"))) { + t.FailNow() + } + + expect := map[string]string{ + "A1": cells["A2"], "B1": cells["B2"], + "A2": cells["A1"], "B2": cells["B1"], + "A3": cells["A2"], "B3": cells["B2"], + "A4": cells["A3"], "B4": cells["B3"], + } + for cell, val := range expect { + if !assert.Equal(t, val, xlsx.GetCellValue(sheet, cell), cell) { + t.FailNow() + } + } + }) + + t.Run("InsertBeforeWithLargeOffset", func(t *testing.T) { + xlsx := newFileWithDefaults() + + xlsx.DuplicateRowTo(sheet, 3, 1) + + if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.InsertBeforeWithLargeOffset"))) { + t.FailNow() + } + + expect := map[string]string{ + "A1": cells["A3"], "B1": cells["B3"], + "A2": cells["A1"], "B2": cells["B1"], + "A3": cells["A2"], "B3": cells["B2"], + "A4": cells["A3"], "B4": cells["B3"], + } + for cell, val := range expect { + if !assert.Equal(t, val, xlsx.GetCellValue(sheet, cell)) { + t.FailNow() + } + } + }) +} + +func TestDuplicateRowInvalidRownum(t *testing.T) { + const sheet = "Sheet1" + outFile := filepath.Join("test", "TestDuplicateRowInvalidRownum.%s.xlsx") + + cells := map[string]string{ + "A1": "A1 Value", + "A2": "A2 Value", + "A3": "A3 Value", + "B1": "B1 Value", + "B2": "B2 Value", + "B3": "B3 Value", + } + + testRows := []int{-2, -1} + + testRowPairs := []struct { + row1 int + row2 int + }{ + {-1, -1}, + {-1, 0}, + {-1, 1}, + {0, -1}, + {0, 0}, + {0, 1}, + {1, -1}, + {1, 1}, + {1, 0}, + } + + for i, row := range testRows { + name := fmt.Sprintf("TestRow_%d", i+1) + t.Run(name, func(t *testing.T) { + xlsx := NewFile() + for col, val := range cells { + xlsx.SetCellStr(sheet, col, val) + } + xlsx.DuplicateRow(sheet, row) + + for col, val := range cells { + if !assert.Equal(t, val, xlsx.GetCellValue(sheet, col)) { + t.FailNow() + } + } + assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, name))) + }) + } + + for i, pair := range testRowPairs { + name := fmt.Sprintf("TestRowPair_%d", i+1) + t.Run(name, func(t *testing.T) { + xlsx := NewFile() + for col, val := range cells { + xlsx.SetCellStr(sheet, col, val) + } + xlsx.DuplicateRowTo(sheet, pair.row1, pair.row2) + + for col, val := range cells { + if !assert.Equal(t, val, xlsx.GetCellValue(sheet, col)) { + t.FailNow() + } + } + assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, name))) + }) + } +} + +func trimSliceSpace(s []string) []string { + for { + if len(s) > 0 && s[len(s)-1] == "" { + s = s[:len(s)-1] + } else { + break + } + } + return s +}