diff --git a/cell_test.go b/cell_test.go index fb1e8ef585..48a1c2fb77 100644 --- a/cell_test.go +++ b/cell_test.go @@ -224,7 +224,7 @@ func TestGetCellValue(t *testing.T) { f.checked = nil cells := []string{"A3", "A4", "B4", "A7", "B7"} rows, err := f.GetRows("Sheet1") - assert.Equal(t, [][]string{nil, nil, {"A3"}, {"A4", "B4"}, nil, nil, {"A7", "B7"}, {"A8", "B8"}}, rows) + assert.Equal(t, [][]Cell{nil, nil, {Cell{Value: "A3"}}, {Cell{Value: "A4"}, Cell{Value: "B4"}}, nil, nil, {Cell{Value: "A7"}, Cell{Value: "B7"}}, {Cell{Value: "A8"}, Cell{Value: "B8"}}}, rows) assert.NoError(t, err) for _, cell := range cells { value, err := f.GetCellValue("Sheet1", cell) @@ -246,21 +246,21 @@ func TestGetCellValue(t *testing.T) { f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `A2B2`))) f.checked = nil rows, err = f.GetRows("Sheet1") - assert.Equal(t, [][]string{nil, {"A2", "B2"}}, rows) + assert.Equal(t, [][]Cell{nil, {Cell{Value: "A2"}, Cell{Value: "B2"}}}, rows) assert.NoError(t, err) f.Sheet.Delete("xl/worksheets/sheet1.xml") f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `A1B1`))) f.checked = nil rows, err = f.GetRows("Sheet1") - assert.Equal(t, [][]string{{"A1", "B1"}}, rows) + assert.Equal(t, [][]Cell{{Cell{Value: "A1"}, Cell{Value: "B1"}}}, rows) assert.NoError(t, err) f.Sheet.Delete("xl/worksheets/sheet1.xml") f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `A3A4B4A7B7A8B8`))) f.checked = nil rows, err = f.GetRows("Sheet1") - assert.Equal(t, [][]string{{"A3"}, {"A4", "B4"}, nil, nil, nil, nil, {"A7", "B7"}, {"A8", "B8"}}, rows) + assert.Equal(t, [][]Cell{{Cell{Value: "A3"}}, {Cell{Value: "A4"}, Cell{Value: "B4"}}, nil, nil, nil, nil, {Cell{Value: "A7"}, Cell{Value: "B7"}}, {Cell{Value: "A8"}, Cell{Value: "B8"}}}, rows) assert.NoError(t, err) f.Sheet.Delete("xl/worksheets/sheet1.xml") @@ -270,13 +270,13 @@ func TestGetCellValue(t *testing.T) { assert.Equal(t, "H6", cell) assert.NoError(t, err) rows, err = f.GetRows("Sheet1") - assert.Equal(t, [][]string{ - {"A6", "B6", "C6"}, + assert.Equal(t, [][]Cell{ + {Cell{Value: "A6"}, Cell{Value: "B6"}, Cell{Value: "C6"}}, nil, - {"100", "B3"}, - {"", "", "", "", "", "F4"}, + {Cell{Value: int64(100)}, Cell{Value: "B3"}}, + {Cell{}, Cell{}, Cell{}, Cell{}, Cell{}, Cell{Value: "F4"}}, nil, - {"", "", "", "", "", "", "", "H6"}, + {Cell{}, Cell{}, Cell{}, Cell{}, Cell{}, Cell{}, Cell{}, Cell{Value: "H6"}}, }, rows) assert.NoError(t, err) @@ -314,36 +314,36 @@ func TestGetCellValue(t *testing.T) { `))) f.checked = nil rows, err = f.GetRows("Sheet1") - assert.Equal(t, [][]string{{ - "2422.3", - "2422.3", - "12.4", - "964", - "1101.6", - "275.4", - "68.9", - "44385.2083333333", - "5.1", - "5.11", - "5.1", - "5.111", - "5.1111", - "2422.012345678", - "2422.0123456789", - "12.012345678901", - "964", - "1101.6", - "275.4", - "68.9", - "0.08888", - "0.00004", - "2422.3", - "1101.6", - "275.4", - "68.9", - "1.1", - "1234567890123_4", - "123456789_0123_4", + assert.Equal(t, [][]Cell{{ + Cell{Value: 2422.3}, + Cell{Value: 2422.3}, + Cell{Value: 12.4}, + Cell{Value: int64(964)}, + Cell{Value: 1101.6}, + Cell{Value: 275.4}, + Cell{Value: 68.9}, + Cell{Value: 44385.2083333333}, + Cell{Value: 5.1}, + Cell{Value: 5.11}, + Cell{Value: 5.1}, + Cell{Value: 5.111}, + Cell{Value: 5.1111}, + Cell{Value: 2422.012345678}, + Cell{Value: 2422.0123456789}, + Cell{Value: 12.012345678901}, + Cell{Value: int64(964)}, + Cell{Value: 1101.6}, + Cell{Value: 275.4}, + Cell{Value: 68.9}, + Cell{Value: 0.08888}, + Cell{Value: 0.00004}, + Cell{Value: 2422.3}, + Cell{Value: 1101.6}, + Cell{Value: 275.4}, + Cell{Value: 68.9}, + Cell{Value: 1.1}, + Cell{Value: "1234567890123_4"}, + Cell{Value: "123456789_0123_4"}, }}, rows) assert.NoError(t, err) } diff --git a/excelize_test.go b/excelize_test.go index f1b9903cbb..20368d0275 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -1114,12 +1114,12 @@ func TestSharedStrings(t *testing.T) { if !assert.NoError(t, err) { t.FailNow() } - assert.Equal(t, "A", rows[0][0]) + assert.Equal(t, Cell{Value: "A"}, rows[0][0]) rows, err = f.GetRows("Sheet2") if !assert.NoError(t, err) { t.FailNow() } - assert.Equal(t, "Test Weight (Kgs)", rows[0][0]) + assert.Equal(t, Cell{Value: "Test Weight (Kgs)"}, rows[0][0]) assert.NoError(t, f.Close()) } diff --git a/lib.go b/lib.go index 99118ff079..75d24a30c4 100644 --- a/lib.go +++ b/lib.go @@ -698,6 +698,9 @@ func (f *File) addSheetNameSpace(sheet string, ns xml.Attr) { // the precision for the numeric. func isNumeric(s string) (bool, int) { dot, e, n, p := false, false, false, 0 + if s == "" { + return false, 0 + } for i, v := range s { if v == '.' { if dot { diff --git a/rows.go b/rows.go index 853c8f7df3..e661670615 100644 --- a/rows.go +++ b/rows.go @@ -49,12 +49,12 @@ import ( // fmt.Println() // } // -func (f *File) GetRows(sheet string, opts ...Options) ([][]string, error) { +func (f *File) GetRows(sheet string, opts ...Options) ([][]Cell, error) { rows, err := f.Rows(sheet) if err != nil { return nil, err } - results, cur, max := make([][]string, 0, 64), 0, 0 + results, cur, max := make([][]Cell, 0, 64), 0, 0 for rows.Next() { cur++ row, err := rows.Columns(opts...) @@ -74,12 +74,14 @@ type Rows struct { err error curRow, seekRow int needClose, rawCellValue bool - sheet string + sheetPath string + sheetName string f *File tempFile *os.File sst *xlsxSST decoder *xml.Decoder token xml.Token + rowOpts RowOpts } // Next will return true if find the next row element. @@ -101,6 +103,17 @@ func (rows *Rows) Next() bool { rows.curRow = rowNum } rows.token = token + ro := RowOpts{} + if styleID, err := attrValToInt("s", xmlElement.Attr); err == nil && styleID > 0 && styleID < MaxCellStyles { + ro.StyleID = styleID + } + if hidden, err := attrValToBool("hidden", xmlElement.Attr); err == nil { + ro.Hidden = hidden + } + if height, err := attrValToFloat("ht", xmlElement.Attr); err == nil { + ro.Height = height + } + rows.rowOpts = ro return true } case xml.EndElement: @@ -111,6 +124,13 @@ func (rows *Rows) Next() bool { } } +// GetStyleID will return the RowOpts of the current row. +// +// rowOpts := rows.GetRowOpts() +func (rows *Rows) GetRowOpts() RowOpts { + return rows.rowOpts +} + // Error will return the error when the error occurs. func (rows *Rows) Error() error { return rows.err @@ -128,7 +148,7 @@ func (rows *Rows) Close() error { // Columns return the current row's column values. This fetches the worksheet // data as a stream, returns each cell in a row as is, and will not skip empty // rows in the tail of the worksheet. -func (rows *Rows) Columns(opts ...Options) ([]string, error) { +func (rows *Rows) Columns(opts ...Options) ([]Cell, error) { if rows.curRow > rows.seekRow { return nil, nil } @@ -171,9 +191,9 @@ func (rows *Rows) Columns(opts ...Options) ([]string, error) { } // appendSpace append blank characters to slice by given length and source slice. -func appendSpace(l int, s []string) []string { +func appendSpace(l int, s []Cell) []Cell { for i := 1; i < l; i++ { - s = append(s, "") + s = append(s, Cell{}) } return s } @@ -192,7 +212,7 @@ type rowXMLIterator struct { err error inElement string cellCol int - columns []string + columns []Cell } // rowXMLHandler parse the row XML element of the worksheet. @@ -206,10 +226,18 @@ func (rows *Rows) rowXMLHandler(rowIterator *rowXMLIterator, xmlElement *xml.Sta return } } + //blank := rowIterator.cellCol - len(rowIterator.columns) + //if val, _ := colCell.getValueFrom(rows.f, rows.sst, raw); val != "" || colCell.F != nil { + // rowIterator.columns = append(appendSpace(blank, rowIterator.columns), val) + //} blank := rowIterator.cellCol - len(rowIterator.columns) - if val, _ := colCell.getValueFrom(rows.f, rows.sst, raw); val != "" || colCell.F != nil { - rowIterator.columns = append(appendSpace(blank, rowIterator.columns), val) + var formula string + if colCell.F != nil { + formula, _ = rows.f.GetCellFormula(rows.sheetName, colCell.R) } + //if val, _ := colCell.getTypedValueFrom(rows.f, rows.sst); val != "" || colCell.F != nil { + val, _ := colCell.getTypedValueFrom(rows.f, rows.sst) + rowIterator.columns = append(appendSpace(blank, rowIterator.columns), Cell{Value: val, StyleID: colCell.S, Formula: formula}) } } @@ -249,7 +277,7 @@ func (f *File) Rows(sheet string) (*Rows, error) { f.saveFileList(name, f.replaceNameSpaceBytes(name, output)) } var err error - rows := Rows{f: f, sheet: name} + rows := Rows{f: f, sheetPath: name, sheetName: sheet} rows.needClose, rows.decoder, rows.tempFile, err = f.xmlDecoder(name) return &rows, err } @@ -475,6 +503,61 @@ func (c *xlsxC) getValueFrom(f *File, d *xlsxSST, raw bool) (string, error) { } } +func (c *xlsxC) getTypedValueFrom(f *File, d *xlsxSST) (interface{}, error) { + f.Lock() + defer f.Unlock() + switch c.T { + case "b": + if c.V == "1" { + return true, nil + } else if c.V == "0" { + return false, nil + } + case "s": + if c.V != "" { + xlsxSI := 0 + xlsxSI, _ = strconv.Atoi(c.V) + if _, ok := f.tempFiles.Load(defaultXMLPathSharedStrings); ok { + return f.getFromStringItem(xlsxSI), nil + } + if len(d.SI) > xlsxSI { + return d.SI[xlsxSI].String(), nil + } + } + case "str": + return c.V, nil + case "inlineStr": + if c.IS != nil { + return c.IS.String(), nil + } + return c.V, nil + default: + if isNum, precision := isNumeric(c.V); isNum { + var precisionV string + if precision == 0 { + precisionV = roundPrecision(c.V, 15) + } else { + precisionV = roundPrecision(c.V, -1) + } + + vi, erri := strconv.ParseInt(precisionV, 10, 64) + vf, errf := strconv.ParseFloat(precisionV, 64) + if erri == nil { + return vi, nil + } else if errf == nil { + return vf, nil + } else { + return precisionV, nil + } + } else { + return nil, nil + } + // TODO: add support for other possible values of T (https://stackoverflow.com/questions/18334314/what-do-excel-xml-cell-attribute-values-mean) + } + + return c.V, nil +} + // roundPrecision provides a function to format floating-point number text // with precision, if the given text couldn't be parsed to float, this will // return the original string. diff --git a/rows_test.go b/rows_test.go index 4fe28517cd..fbff60441e 100644 --- a/rows_test.go +++ b/rows_test.go @@ -24,11 +24,11 @@ func TestRows(t *testing.T) { t.FailNow() } - var collectedRows [][]string + var collectedRows [][]Cell for rows.Next() { columns, err := rows.Columns() assert.NoError(t, err) - collectedRows = append(collectedRows, trimSliceSpace(columns)) + collectedRows = append(collectedRows, columns) } if !assert.NoError(t, rows.Error()) { t.FailNow() @@ -37,9 +37,6 @@ func TestRows(t *testing.T) { returnedRows, err := f.GetRows(sheet2) assert.NoError(t, err) - for i := range returnedRows { - returnedRows[i] = trimSliceSpace(returnedRows[i]) - } if !assert.Equal(t, collectedRows, returnedRows) { t.FailNow() } @@ -72,12 +69,30 @@ func TestRowsIterator(t *testing.T) { rows, err := f.Rows(sheetName) require.NoError(t, err) + expectedCells := [][]Cell{ + {Cell{Value: "Monitor", StyleID: 1}, Cell{StyleID: 1}, Cell{Value: "Brand", StyleID: 2}, Cell{StyleID: 2}, Cell{Value: "inlineStr"}}, + {Cell{Value: "> 23 Inch", StyleID: 1}, Cell{Value: int64(19), StyleID: 1}, Cell{Value: "HP", StyleID: 3}, Cell{Value: int64(200), StyleID: 4}}, + {Cell{Value: "20-23 Inch", StyleID: 1}, Cell{Value: int64(24), StyleID: 1}, Cell{Value: "DELL", StyleID: 3}, Cell{Value: int64(450), StyleID: 4}}, + {Cell{Value: "17-20 Inch", StyleID: 1}, Cell{Value: int64(56), StyleID: 1}, Cell{Value: "Lenove", StyleID: 3}, Cell{Value: int64(200), StyleID: 4}}, + {Cell{Value: "< 17 Inch", StyleID: 5}, Cell{Value: int64(21), StyleID: 1}, Cell{Value: "SONY", StyleID: 3}, Cell{Value: int64(510), StyleID: 4}}, + {Cell{}, Cell{}, Cell{Value: "Acer", StyleID: 3}, Cell{Value: int64(315), StyleID: 4}}, + {Cell{}, Cell{}, Cell{Value: "IBM", StyleID: 3}, Cell{Value: int64(127), StyleID: 4}}, + {Cell{}, Cell{}, Cell{Value: "ASUS", StyleID: 4}, Cell{Value: int64(89), StyleID: 4}}, + {Cell{}, Cell{}, Cell{Value: "Apple", StyleID: 4}, Cell{Value: int64(348), StyleID: 4}}, + {Cell{}, Cell{}, Cell{Value: "SAMSUNG", StyleID: 4}, Cell{Value: int64(53), StyleID: 4}}, + {Cell{}, Cell{}, Cell{Value: "Other", StyleID: 4}, Cell{Value: int64(37), StyleID: 4}, Cell{Formula: "B2+B3", StyleID: 4}, Cell{Formula: "IF(B2>0, (D2/B2)*100, 0)", StyleID: 4}, Cell{Formula: "IF(B2>0, (D2/B2)*100, 0)", StyleID: 4}, Cell{Formula: "IF(D2>0, (F2/D2)*100, 0)", StyleID: 4}, Cell{Formula: "IF(D2>0, (F2/D2)*100, 0)", StyleID: 4}}, + } + gotCells := [][]Cell{} for rows.Next() { rowCount++ require.True(t, rowCount <= expectedNumRow, "rowCount is greater than expected") + cols, err := rows.Columns() + require.NoError(t, err) + gotCells = append(gotCells, cols) } assert.Equal(t, expectedNumRow, rowCount) + assert.Equal(t, expectedCells, gotCells) assert.NoError(t, rows.Close()) assert.NoError(t, f.Close()) @@ -96,6 +111,28 @@ func TestRowsIterator(t *testing.T) { assert.Equal(t, expectedNumRow, rowCount) } +func TestRowsGetRowOpts(t *testing.T) { + sheetName := "Sheet2" + expectedRowStyleID1 := RowOpts{Height: 17.0, Hidden: false, StyleID: 1} + expectedRowStyleID2 := RowOpts{Height: 17.0, Hidden: false, StyleID: 0} + expectedRowStyleID3 := RowOpts{Height: 17.0, Hidden: false, StyleID: 2} + f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) + require.NoError(t, err) + + rows, err := f.Rows(sheetName) + require.NoError(t, err) + + rows.Next() + got := rows.GetRowOpts() + assert.Equal(t, expectedRowStyleID1, got) + rows.Next() + got = rows.GetRowOpts() + assert.Equal(t, expectedRowStyleID2, got) + rows.Next() + got = rows.GetRowOpts() + assert.Equal(t, expectedRowStyleID3, got) +} + func TestRowsError(t *testing.T) { f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) if !assert.NoError(t, err) { diff --git a/sheet.go b/sheet.go index 01dd1672b9..e984dbd065 100644 --- a/sheet.go +++ b/sheet.go @@ -928,6 +928,34 @@ func attrValToInt(name string, attrs []xml.Attr) (val int, err error) { return } +// attrValToFloat provides a function to convert the local names to a float64 +// by given XML attributes and specified names. +func attrValToFloat(name string, attrs []xml.Attr) (val float64, err error) { + for _, attr := range attrs { + if attr.Name.Local == name { + val, err = strconv.ParseFloat(attr.Value, 64) + if err != nil { + return + } + } + } + return +} + +// attrValToBool provides a function to convert the local names to a boot +// by given XML attributes and specified names. +func attrValToBool(name string, attrs []xml.Attr) (val bool, err error) { + for _, attr := range attrs { + if attr.Name.Local == name { + val, err = strconv.ParseBool(attr.Value) + if err != nil { + return + } + } + } + return +} + // SetHeaderFooter provides a function to set headers and footers by given // worksheet name and the control characters. // diff --git a/stream.go b/stream.go index 1a1af24412..52e65a46c8 100644 --- a/stream.go +++ b/stream.go @@ -327,6 +327,9 @@ func (sw *StreamWriter) SetRow(axis string, values []interface{}, opts ...RowOpt } fmt.Fprintf(&sw.rawData, ``, row, attrs) for i, val := range values { + if val == nil { + continue + } axis, err := CoordinatesToCellName(col+i, row) if err != nil { return err diff --git a/stream_test.go b/stream_test.go index 6843e2064f..8f6a5b4cf5 100644 --- a/stream_test.go +++ b/stream_test.go @@ -209,6 +209,17 @@ func TestSetRow(t *testing.T) { assert.EqualError(t, streamWriter.SetRow("A", []interface{}{}), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) } +func TestSetRowNilValues(t *testing.T) { + file := NewFile() + streamWriter, err := file.NewStreamWriter("Sheet1") + assert.NoError(t, err) + streamWriter.SetRow("A1", []interface{}{nil, nil, Cell{Value: "foo"}}) + streamWriter.Flush() + ws, err := file.workSheetReader("Sheet1") + assert.NoError(t, err) + assert.NotEqual(t, ws.SheetData.Row[0].C[0].XMLName.Local, "c") +} + func TestSetCellValFunc(t *testing.T) { f := NewFile() sw, err := f.NewStreamWriter("Sheet1") diff --git a/test/Book1.xlsx b/test/Book1.xlsx index 6a497e33af..ed3e292954 100644 Binary files a/test/Book1.xlsx and b/test/Book1.xlsx differ diff --git a/xmlDrawing.go b/xmlDrawing.go index 3e54b7207f..04956d5745 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -107,6 +107,7 @@ const ( MaxFieldLength = 255 MaxColumnWidth = 255 MaxRowHeight = 409 + MaxCellStyles = 64000 MinFontSize = 1 TotalRows = 1048576 MinColumns = 1