diff --git a/file_test.go b/file_test.go index 9e8fb0ab..41b329f1 100644 --- a/file_test.go +++ b/file_test.go @@ -61,6 +61,52 @@ func TestFile(t *testing.T) { c.Assert(xlsxFile, qt.Not(qt.IsNil)) }) + csRunO(c, "TestFileWithEmptyCols", func(c *qt.C, option FileOption) { + f, err := OpenFile("./testdocs/empty_rows.xlsx", option) + c.Assert(err, qt.IsNil) + sheet, ok := f.Sheet["EmptyCols"] + c.Assert(ok, qt.Equals, true) + + cell, err := sheet.Cell(0, 0) + c.Assert(err, qt.Equals, nil) + if val, err := cell.FormattedValue(); err != nil { + c.Error(err) + } else { + c.Assert(val, qt.Equals, "") + } + cell, err = sheet.Cell(0, 2) + c.Assert(err, qt.Equals, nil) + + if val, err := cell.FormattedValue(); err != nil { + c.Error(err) + } else { + c.Assert(val, qt.Equals, "C1") + } + }) + + csRunO(c, "TestFileWithEmptyCols", func(c *qt.C, option FileOption) { + f, err := OpenFile("./testdocs/empty_rows.xlsx", option) + c.Assert(err, qt.IsNil) + sheet, ok := f.Sheet["EmptyCols"] + c.Assert(ok, qt.Equals, true) + + cell, err := sheet.Cell(0, 0) + c.Assert(err, qt.Equals, nil) + if val, err := cell.FormattedValue(); err != nil { + c.Error(err) + } else { + c.Assert(val, qt.Equals, "") + } + cell, err = sheet.Cell(0, 2) + c.Assert(err, qt.Equals, nil) + + if val, err := cell.FormattedValue(); err != nil { + c.Error(err) + } else { + c.Assert(val, qt.Equals, "C1") + } + }) + csRunO(c, "TestPartialReadsWithFewSharedStringsOnlyPartiallyReads", func(c *qt.C, option FileOption) { // This test verifies that a large file is only partially read when using a small row limit. // This file is 11,228,530 bytes, but only 14,020 bytes get read out when using a row limit of 10. @@ -1034,6 +1080,19 @@ func TestGetStyleFromZipFile(t *testing.T) { func TestSliceReader(t *testing.T) { c := qt.New(t) + fileToSliceCheckOutput := func(c *qt.C, output [][][]string) { + c.Assert(len(output), qt.Equals, 3) + c.Assert(len(output[0]), qt.Equals, 2) + c.Assert(len(output[0][0]), qt.Equals, 2) + c.Assert(output[0][0][0], qt.Equals, "Foo") + c.Assert(output[0][0][1], qt.Equals, "Bar") + c.Assert(len(output[0][1]), qt.Equals, 2) + c.Assert(output[0][1][0], qt.Equals, "Baz") + c.Assert(output[0][1][1], qt.Equals, "Quuk") + c.Assert(len(output[1]), qt.Equals, 0) + c.Assert(len(output[2]), qt.Equals, 0) + } + csRunO(c, "TestFileToSlice", func(c *qt.C, option FileOption) { output, err := FileToSlice("./testdocs/testfile.xlsx", option) c.Assert(err, qt.IsNil) @@ -1068,63 +1127,15 @@ func TestSliceReader(t *testing.T) { c.Assert(output[0][2][0], qt.Equals, "01.01.2016") }) - csRunO(c, "TestFileWithEmptyRows", func(c *qt.C, option FileOption) { - f, err := OpenFile("./testdocs/empty_rows.xlsx", option) - c.Assert(err, qt.IsNil) - sheet, ok := f.Sheet["EmptyRows"] - c.Assert(ok, qt.Equals, true) - - cell, err := sheet.Cell(0, 0) - c.Assert(err, qt.Equals, nil) - if val, err := cell.FormattedValue(); err != nil { - c.Error(err) - } else { - c.Assert(val, qt.Equals, "") - } - cell, err = sheet.Cell(2, 0) - c.Assert(err, qt.Equals, nil) - - if val, err := cell.FormattedValue(); err != nil { - c.Error(err) - } else { - c.Assert(val, qt.Equals, "A3") - } - }) - - csRunO(c, "TestFileWithEmptyCols", func(c *qt.C, option FileOption) { - f, err := OpenFile("./testdocs/empty_rows.xlsx", option) - c.Assert(err, qt.IsNil) - sheet, ok := f.Sheet["EmptyCols"] - c.Assert(ok, qt.Equals, true) - - cell, err := sheet.Cell(0, 0) - c.Assert(err, qt.Equals, nil) - if val, err := cell.FormattedValue(); err != nil { - c.Error(err) - } else { - c.Assert(val, qt.Equals, "") - } - cell, err = sheet.Cell(0, 2) + csRunO(c, "TestFileToSliceEmptyCells", func(c *qt.C, option FileOption) { + output, err := FileToSlice("./testdocs/empty_cells.xlsx", option) c.Assert(err, qt.Equals, nil) - - if val, err := cell.FormattedValue(); err != nil { - c.Error(err) - } else { - c.Assert(val, qt.Equals, "C1") + c.Assert(output, qt.HasLen, 1) + sheetSlice := output[0] + c.Assert(sheetSlice, qt.HasLen, 4) + for _, rowSlice := range sheetSlice { + c.Assert(rowSlice, qt.HasLen, 4) } }) } - -func fileToSliceCheckOutput(c *qt.C, output [][][]string) { - c.Assert(len(output), qt.Equals, 3) - c.Assert(len(output[0]), qt.Equals, 2) - c.Assert(len(output[0][0]), qt.Equals, 2) - c.Assert(output[0][0][0], qt.Equals, "Foo") - c.Assert(output[0][0][1], qt.Equals, "Bar") - c.Assert(len(output[0][1]), qt.Equals, 2) - c.Assert(output[0][1][0], qt.Equals, "Baz") - c.Assert(output[0][1][1], qt.Equals, "Quuk") - c.Assert(len(output[1]), qt.Equals, 0) - c.Assert(len(output[2]), qt.Equals, 0) -} diff --git a/row.go b/row.go index 5e6c918e..456db91e 100644 --- a/row.go +++ b/row.go @@ -99,23 +99,63 @@ func (r *Row) GetCell(colIdx int) *Cell { return cell } +// cellVisitorFlags contains flags that can be set by CellVisitorOption implementations to modify the behaviour of ForEachCell +type cellVisitorFlags struct { + // skipEmptyCells indicates if we should skip nil cells. + skipEmptyCells bool +} + +// CellVisitorOption describes a function that can set values in a +// cellVisitorFlags struct to affect the way ForEachCell operates +type CellVisitorOption func(flags *cellVisitorFlags) + +// SkipEmptyCells can be passed as an option to Row.ForEachCell in +// order to make it skip over empty cells in the sheet. +func SkipEmptyCells(flags *cellVisitorFlags) { + flags.skipEmptyCells = true +} + // ForEachCell will call the provided CellVisitorFunc for each -// currently defined cell in the Row. -func (r *Row) ForEachCell(cvf CellVisitorFunc) error { - fn := func(c *Cell) error { - if c != nil { - c.Row = r - return cvf(c) +// currently defined cell in the Row. Optionally you may pass one or +// more CellVisitorOption to affect how ForEachCell operates. For +// example you may wish to pass SkipEmptyCells to only visit cells +// which are populated. +func (r *Row) ForEachCell(cvf CellVisitorFunc, option ...CellVisitorOption) error { + flags := &cellVisitorFlags{} + for _, opt := range option { + opt(flags) + } + fn := func(ci int, c *Cell) error { + if c == nil { + if flags.skipEmptyCells { + return nil + } + c = r.GetCell(ci) + } + if c.Value == "" && flags.skipEmptyCells { + return nil } - return nil + c.Row = r + return cvf(c) } - for _, cell := range r.cells { - err := fn(cell) + for ci, cell := range r.cells { + err := fn(ci, cell) if err != nil { return err } } + cellCount := len(r.cells) + if !flags.skipEmptyCells { + for ci := cellCount; ci < r.Sheet.MaxCol; ci++ { + c := r.GetCell(ci) + err := cvf(c) + if err != nil { + return err + } + + } + } return nil } diff --git a/row_test.go b/row_test.go index aab74640..820f4f46 100644 --- a/row_test.go +++ b/row_test.go @@ -33,4 +33,52 @@ func TestRow(t *testing.T) { c.Assert(cell.Value, qt.Equals, cell2.Value) }) + csRunO(c, "TestForEachCell", func(c *qt.C, option FileOption) { + var f *File + f, err := OpenFile("./testdocs/empty_cells.xlsx", option) + c.Assert(err, qt.Equals, nil) + sheet := f.Sheets[0] + c.Run("NoOptions", func(c *qt.C) { + output := [][]string{} + err := sheet.ForEachRow(func(r *Row) error { + cells := []string{} + err := r.ForEachCell(func(c *Cell) error { + cells = append(cells, c.Value) + return nil + }) + if err != nil { + return err + } + c.Assert(cells, qt.HasLen, 4) + output = append(output, cells) + return nil + }) + c.Assert(err, qt.Equals, nil) + }) + + c.Run("SkipEmptyCells", func(c *qt.C) { + output := [][]string{} + err := sheet.ForEachRow(func(r *Row) error { + cells := []string{} + err := r.ForEachCell(func(c *Cell) error { + cells = append(cells, c.Value) + return nil + }, SkipEmptyCells) + if err != nil { + return err + } + output = append(output, cells) + return nil + }) + c.Assert(err, qt.Equals, nil) + c.Assert(output, qt.DeepEquals, + [][]string{ + {"B1", "C1", "D1"}, + {"A2", "C2", "D2"}, + {"A3", "B3", "D3"}, + {"A4", "B4", "C4"}, + }) + }) + + }) } diff --git a/sheet.go b/sheet.go index 98d2a8d3..abca37b5 100644 --- a/sheet.go +++ b/sheet.go @@ -126,22 +126,45 @@ func (s *Sheet) setCurrentRow(r *Row) { s.currentRow = r } +// rowVisitorFlags contains flags that can be set by a RowVisitorOption to affect the behaviour of sheet.ForEachRow +type rowVisitorFlags struct { + skipEmptyRows bool +} + +// RowVisitorOption defines the call signature of functions that can be passed as options to the Sheet.ForEachRow function to affect its behaviour. +type RowVisitorOption func(flags *rowVisitorFlags) + +// SkipEmptyRows can be passed to the Sheet.ForEachRow function to +// cause it to skip over empty Rows. +func SkipEmptyRows(flags *rowVisitorFlags) { + flags.skipEmptyRows = true +} + +// A RowVisitor function should be provided by the user when calling +// Sheet.ForEachRow, it will be called once for every Row visited. type RowVisitor func(r *Row) error -func (s *Sheet) ForEachRow(rv RowVisitor) error { +func (s *Sheet) ForEachRow(rv RowVisitor, options ...RowVisitorOption) error { + flags := &rowVisitorFlags{} + for _, opt := range options { + opt(flags) + } if s.currentRow != nil { s.cellStore.WriteRow(s.currentRow) } - for i := 0; i <= s.MaxRow; i++ { + for i := 0; i < s.MaxRow; i++ { r, err := s.cellStore.ReadRow(makeRowKey(s, i)) if err != nil { if _, ok := err.(*RowNotFoundError); !ok { return err } - continue + if flags.skipEmptyRows { + continue + } + r = &Row{num: i} } - if r == nil { + if r.cellCount == 0 && flags.skipEmptyRows { continue } r.Sheet = s @@ -180,7 +203,8 @@ func (s *Sheet) AddRowAtIndex(index int) (*Row, error) { s.cellStore.WriteRow(s.currentRow) } - for i := index; i < s.MaxRow; i++ { + // We move rows in reverse order to avoid overwriting anyting + for i := (s.MaxRow - 1); i >= index; i-- { nRow, err := s.cellStore.ReadRow(makeRowKey(s, i)) if err != nil { continue @@ -390,9 +414,9 @@ func (s *Sheet) handleMerged() { merged[coord] = cell } return nil - }) + }, SkipEmptyCells) - }) + }, SkipEmptyRows) // This loop iterates over all cells that should be merged and applies the correct // borders to them depending on their position. If any cells required by the merge @@ -494,11 +518,13 @@ func (s *Sheet) prepSheetForMarshalling(maxLevelCol uint8) { func (s *Sheet) prepWorksheetFromRows(worksheet *xlsxWorksheet, relations *xlsxWorksheetRels) error { var maxCell, maxRow int - err := s.ForEachRow(func(row *Row) error { + + prepRow := func(row *Row) error { if row.num > maxRow { maxRow = row.num } - return row.ForEachCell(func(cell *Cell) error { + + prepCell := func(cell *Cell) error { if cell.num > maxCell { maxCell = cell.num } @@ -549,8 +575,12 @@ func (s *Sheet) prepWorksheetFromRows(worksheet *xlsxWorksheet, relations *xlsxW worksheet.MergeCells.addCell(mc) } return nil - }) - }) + } + + return row.ForEachCell(prepCell, SkipEmptyCells) + } + + err := s.ForEachRow(prepRow, SkipEmptyRows) if err != nil { return err } @@ -573,13 +603,12 @@ func (s *Sheet) prepWorksheetFromRows(worksheet *xlsxWorksheet, relations *xlsxW return nil } -func (s *Sheet) makeRows(worksheet *xlsxWorksheet, styles *xlsxStyleSheet, refTable *RefTable, relations *xlsxWorksheetRels, maxLevelCol uint8) { +func (s *Sheet) makeRows(worksheet *xlsxWorksheet, styles *xlsxStyleSheet, refTable *RefTable, relations *xlsxWorksheetRels, maxLevelCol uint8) error { maxRow := 0 maxCell := 0 var maxLevelRow uint8 xSheet := xlsxSheetData{} - - s.ForEachRow(func(row *Row) error { + makeR := func(row *Row) error { r := row.num if r > maxRow { maxRow = r @@ -594,7 +623,7 @@ func (s *Sheet) makeRows(worksheet *xlsxWorksheet, styles *xlsxStyleSheet, refTa if xRow.OutlineLevel > maxLevelRow { maxLevelRow = xRow.OutlineLevel } - row.ForEachCell(func(cell *Cell) error { + makeC := func(cell *Cell) error { var XfId int c := cell.num @@ -707,10 +736,19 @@ func (s *Sheet) makeRows(worksheet *xlsxWorksheet, styles *xlsxStyleSheet, refTa worksheet.MergeCells.addCell(mc) } return nil - }) + } + err := row.ForEachCell(makeC, SkipEmptyCells) + if err != nil { + return err + } xSheet.Row = append(xSheet.Row, xRow) return nil - }) + } + + err := s.ForEachRow(makeR, SkipEmptyRows) + if err != nil { + return err + } // Update sheet format with the freshly determined max levels s.SheetFormat.OutlineLevelCol = maxLevelCol @@ -733,6 +771,7 @@ func (s *Sheet) makeRows(worksheet *xlsxWorksheet, styles *xlsxStyleSheet, refTa dimension.Ref = "A1" } worksheet.Dimension = dimension + return nil } func (s *Sheet) makeDataValidations(worksheet *xlsxWorksheet) { diff --git a/sheet_test.go b/sheet_test.go index 5969b61a..13aecbb3 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -68,6 +68,7 @@ func TestSheet(t *testing.T) { c.Run("InsertARowAtBeginning", func(c *qt.C) { sheet, err := setUp() c.Assert(err, qt.Equals, nil) + assertRow(c, sheet, []string{"Row 0", "Row 1", "Row 2"}) rowNewStart, err := sheet.AddRowAtIndex(0) c.Assert(err, qt.IsNil) cellNewStart := rowNewStart.AddCell() @@ -423,8 +424,6 @@ func TestSheet(t *testing.T) { } } - // obtained := parts["xl/styles.xml"] - shouldbe := ` ` diff --git a/stream_style_test.go b/stream_style_test.go index 362de81e..dbf9a247 100644 --- a/stream_style_test.go +++ b/stream_style_test.go @@ -299,22 +299,21 @@ func TestXlsxStreamWriteWithStyle(t *testing.T) { actualSheetNames, actualWorkbookData, actualWorkbookCells := readXLSXFileS(t, filePath, bufReader, size, StyleStreamTestsShouldMakeRealFiles, option) // check if data was able to be read correctly c.Assert(actualSheetNames, qt.DeepEquals, testCase.sheetNames) - expectedWorkbookDataStrings := [][][]string{} + expectedWorkbookDataStrings := make([][][]string, len(testCase.workbookData)) for j := range testCase.workbookData { - expectedWorkbookDataStrings = append(expectedWorkbookDataStrings, [][]string{}) + expectedWorkbookDataStrings[j] = make([][]string, len(testCase.workbookData[j])) for k := range testCase.workbookData[j] { - if len(testCase.workbookData[j][k]) == 0 { - expectedWorkbookDataStrings[j] = append(expectedWorkbookDataStrings[j], nil) - } else { - expectedWorkbookDataStrings[j] = append(expectedWorkbookDataStrings[j], []string{}) - for _, cell := range testCase.workbookData[j][k] { - expectedWorkbookDataStrings[j][k] = append(expectedWorkbookDataStrings[j][k], cell.cellData) - } + expectedWorkbookDataStrings[j][k] = make([]string, len(testCase.workbookData[j][k])) + for l, cell := range testCase.workbookData[j][k] { + expectedWorkbookDataStrings[j][k][l] = cell.cellData + } } - } - c.Assert(actualWorkbookData, qt.DeepEquals, expectedWorkbookDataStrings) + + c.Assert(fmt.Sprintf("%+v", actualWorkbookData), + qt.Equals, + fmt.Sprintf("%+v", expectedWorkbookDataStrings)) c.Assert(checkForCorrectCellStyles(actualWorkbookCells, testCase.workbookData), qt.Equals, nil) @@ -419,7 +418,7 @@ func readXLSXFileS(t *testing.T, filePath string, fileBuffer io.ReaderAt, size i } data = append(data, str) return nil - }) + }, SkipEmptyCells) if err != nil { return err } diff --git a/stream_test.go b/stream_test.go index d0770295..27c687fa 100644 --- a/stream_test.go +++ b/stream_test.go @@ -256,7 +256,7 @@ func TestXlsxStreamWrite(t *testing.T) { actualSheetNames, actualWorkbookData, _ := readXLSXFile(t, filePath, bufReader, size, TestsShouldMakeRealFiles, option) // check if data was able to be read correctly c.Assert(actualSheetNames, qt.DeepEquals, testCase.sheetNames) - c.Assert(actualWorkbookData, qt.DeepEquals, testCase.workbookData) + c.Assert(fmt.Sprintf("%+v", actualWorkbookData), qt.Equals, fmt.Sprintf("%+v", testCase.workbookData)) }) } } @@ -584,7 +584,7 @@ func TestXlsxStreamWriteWithDefaultCellType(t *testing.T) { testCase.expectedWorkbookData = testCase.workbookData } - c.Assert(actualWorkbookData, qt.DeepEquals, testCase.expectedWorkbookData) + c.Assert(fmt.Sprintf("%+v", actualWorkbookData), qt.Equals, fmt.Sprintf("%+v", testCase.expectedWorkbookData)) }) } } diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 411d1b67..6ce07fc4 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -771,17 +771,8 @@ func (worksheet *xlsxWorksheet) WriteXML(xw *xmlwriter.Writer, s *Sheet, styles return err } return xw.Flush() - // err := xw.StartElem(xmlwriter.Elem{Name: "row"}) - // if err != nil { - // return err - // } - // err = xw.EndElem("row") - // if err != nil { - // return err - // } - // return xw.Flush() - - }), + + }, SkipEmptyRows), xw.EndElem("sheetData"), xw.EndElem(output.Name), xw.Flush(),