From f44153ea4679247070d6f1e31bb0934a10bebb31 Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 25 Oct 2022 10:24:45 +0800 Subject: [PATCH] This closes #1377, stream writer writes inline string type for string cell value - Add `CellTypeFormula`, `CellTypeInlineString`, `CellTypeSharedString` and remove `CellTypeString` in `CellType` enumeration - Unit tests updated --- calc_test.go | 4 +- cell.go | 151 +++++++++++++++++++++++++++++++++++++++++--------- cell_test.go | 23 ++++---- col_test.go | 6 +- rows.go | 77 ------------------------- rows_test.go | 4 +- sheet_test.go | 6 +- stream.go | 38 +++++++++---- 8 files changed, 172 insertions(+), 137 deletions(-) diff --git a/calc_test.go b/calc_test.go index 1a8b8c62ec..5d61712f9b 100644 --- a/calc_test.go +++ b/calc_test.go @@ -5223,8 +5223,8 @@ func TestCalcXLOOKUP(t *testing.T) { "=XLOOKUP(29,C2:H2,C3:H3,NA(),-1,1)": "D3", } for formula, expected := range formulaList { - assert.NoError(t, f.SetCellFormula("Sheet1", "D3", formula)) - result, err := f.CalcCellValue("Sheet1", "D3") + assert.NoError(t, f.SetCellFormula("Sheet1", "D4", formula)) + result, err := f.CalcCellValue("Sheet1", "D4") assert.NoError(t, err, formula) assert.Equal(t, expected, result, formula) } diff --git a/cell.go b/cell.go index 3fcbb7b3a4..6ed7f48599 100644 --- a/cell.go +++ b/cell.go @@ -30,8 +30,10 @@ const ( CellTypeBool CellTypeDate CellTypeError + CellTypeFormula + CellTypeInlineString CellTypeNumber - CellTypeString + CellTypeSharedString ) const ( @@ -51,9 +53,9 @@ var cellTypes = map[string]CellType{ "d": CellTypeDate, "n": CellTypeNumber, "e": CellTypeError, - "s": CellTypeString, - "str": CellTypeString, - "inlineStr": CellTypeString, + "s": CellTypeSharedString, + "str": CellTypeFormula, + "inlineStr": CellTypeInlineString, } // GetCellValue provides a function to get formatted value from cell by given @@ -235,8 +237,7 @@ func (f *File) setCellTimeFunc(sheet, cell string, value time.Time) error { date1904 = wb.WorkbookPr.Date1904 } var isNum bool - c.T, c.V, isNum, err = setCellTime(value, date1904) - if err != nil { + if isNum, err = c.setCellTime(value, date1904); err != nil { return err } if isNum { @@ -247,7 +248,7 @@ func (f *File) setCellTimeFunc(sheet, cell string, value time.Time) error { // setCellTime prepares cell type and Excel time by given Go time.Time type // timestamp. -func setCellTime(value time.Time, date1904 bool) (t string, b string, isNum bool, err error) { +func (c *xlsxC) setCellTime(value time.Time, date1904 bool) (isNum bool, err error) { var excelTime float64 _, offset := value.In(value.Location()).Zone() value = value.Add(time.Duration(offset) * time.Second) @@ -256,9 +257,9 @@ func setCellTime(value time.Time, date1904 bool) (t string, b string, isNum bool } isNum = excelTime > 0 if isNum { - t, b = setCellDefault(strconv.FormatFloat(excelTime, 'f', -1, 64)) + c.setCellDefault(strconv.FormatFloat(excelTime, 'f', -1, 64)) } else { - t, b = setCellDefault(value.Format(time.RFC3339Nano)) + c.setCellDefault(value.Format(time.RFC3339Nano)) } return } @@ -435,14 +436,14 @@ func (f *File) setSharedString(val string) (int, error) { sst.Count++ sst.UniqueCount++ t := xlsxT{Val: val} - _, val, t.Space = setCellStr(val) + val, t.Space = trimCellValue(val) sst.SI = append(sst.SI, xlsxSI{T: &t}) f.sharedStringsMap[val] = sst.UniqueCount - 1 return sst.UniqueCount - 1, nil } -// setCellStr provides a function to set string type to cell. -func setCellStr(value string) (t string, v string, ns xml.Attr) { +// trimCellValue provides a function to set string type to cell. +func trimCellValue(value string) (v string, ns xml.Attr) { if len(value) > TotalCellChars { value = value[:TotalCellChars] } @@ -458,10 +459,117 @@ func setCellStr(value string) (t string, v string, ns xml.Attr) { } } } - t, v = "str", bstrMarshal(value) + v = bstrMarshal(value) return } +// setCellValue set cell data type and value for (inline) rich string cell or +// formula cell. +func (c *xlsxC) setCellValue(val string) { + if c.F != nil { + c.setStr(val) + return + } + c.setInlineStr(val) +} + +// setInlineStr set cell data type and value which containing an (inline) rich +// string. +func (c *xlsxC) setInlineStr(val string) { + c.T, c.V, c.IS = "inlineStr", "", &xlsxSI{T: &xlsxT{}} + c.IS.T.Val, c.IS.T.Space = trimCellValue(val) +} + +// setStr set cell data type and value which containing a formula string. +func (c *xlsxC) setStr(val string) { + c.T, c.IS = "str", nil + c.V, c.XMLSpace = trimCellValue(val) +} + +// getCellDate parse cell value which containing a boolean. +func (c *xlsxC) getCellBool(f *File, raw bool) (string, error) { + if !raw { + if c.V == "1" { + return "TRUE", nil + } + if c.V == "0" { + return "FALSE", nil + } + } + return f.formattedValue(c.S, c.V, raw), nil +} + +// setCellDefault prepares cell type and string type cell value by a given +// string. +func (c *xlsxC) setCellDefault(value string) { + if ok, _, _ := isNumeric(value); !ok { + c.setInlineStr(value) + c.IS.T.Val = value + return + } + c.V = value +} + +// getCellDate parse cell value which contains a date in the ISO 8601 format. +func (c *xlsxC) getCellDate(f *File, raw bool) (string, error) { + if !raw { + layout := "20060102T150405.999" + if strings.HasSuffix(c.V, "Z") { + layout = "20060102T150405Z" + if strings.Contains(c.V, "-") { + layout = "2006-01-02T15:04:05Z" + } + } else if strings.Contains(c.V, "-") { + layout = "2006-01-02 15:04:05Z" + } + if timestamp, err := time.Parse(layout, strings.ReplaceAll(c.V, ",", ".")); err == nil { + excelTime, _ := timeToExcelTime(timestamp, false) + c.V = strconv.FormatFloat(excelTime, 'G', 15, 64) + } + } + return f.formattedValue(c.S, c.V, raw), nil +} + +// getValueFrom return a value from a column/row cell, this function is +// intended to be used with for range on rows an argument with the spreadsheet +// opened file. +func (c *xlsxC) getValueFrom(f *File, d *xlsxSST, raw bool) (string, error) { + f.Lock() + defer f.Unlock() + switch c.T { + case "b": + return c.getCellBool(f, raw) + case "d": + return c.getCellDate(f, raw) + case "s": + if c.V != "" { + xlsxSI := 0 + xlsxSI, _ = strconv.Atoi(c.V) + if _, ok := f.tempFiles.Load(defaultXMLPathSharedStrings); ok { + return f.formattedValue(c.S, f.getFromStringItem(xlsxSI), raw), nil + } + if len(d.SI) > xlsxSI { + return f.formattedValue(c.S, d.SI[xlsxSI].String(), raw), nil + } + } + return f.formattedValue(c.S, c.V, raw), nil + case "inlineStr": + if c.IS != nil { + return f.formattedValue(c.S, c.IS.String(), raw), nil + } + return f.formattedValue(c.S, c.V, raw), nil + default: + if isNum, precision, decimal := isNumeric(c.V); isNum && !raw { + if precision > 15 { + c.V = strconv.FormatFloat(decimal, 'G', 15, 64) + } else { + c.V = strconv.FormatFloat(decimal, 'f', -1, 64) + } + } + return f.formattedValue(c.S, c.V, raw), nil + } +} + // SetCellDefault provides a function to set string type value of a cell as // default format without escaping the cell. func (f *File) SetCellDefault(sheet, cell, value string) error { @@ -476,22 +584,11 @@ func (f *File) SetCellDefault(sheet, cell, value string) error { ws.Lock() defer ws.Unlock() c.S = f.prepareCellStyle(ws, col, row, c.S) - c.T, c.V = setCellDefault(value) - c.IS = nil + c.setCellDefault(value) f.removeFormula(c, ws, sheet) return err } -// setCellDefault prepares cell type and string type cell value by a given -// string. -func setCellDefault(value string) (t string, v string) { - if ok, _, _ := isNumeric(value); !ok { - t = "str" - } - v = value - return -} - // GetCellFormula provides a function to get formula from cell by given // worksheet name and cell reference in spreadsheet. func (f *File) GetCellFormula(sheet, cell string) (string, error) { @@ -625,7 +722,7 @@ func (f *File) SetCellFormula(sheet, cell, formula string, opts ...FormulaOpts) c.F.Ref = *opt.Ref } } - c.IS = nil + c.T, c.IS = "str", nil return err } @@ -900,7 +997,7 @@ func setRichText(runs []RichTextRun) ([]xlsxR, error) { return textRuns, ErrCellCharsLength } run := xlsxR{T: &xlsxT{}} - _, run.T.Val, run.T.Space = setCellStr(textRun.Text) + run.T.Val, run.T.Space = trimCellValue(textRun.Text) fnt := textRun.Font if fnt != nil { run.RPr = newRpr(fnt) diff --git a/cell_test.go b/cell_test.go index 980058a30e..f7412111d4 100644 --- a/cell_test.go +++ b/cell_test.go @@ -224,10 +224,11 @@ func TestSetCellTime(t *testing.T) { } { timezone, err := time.LoadLocation(location) assert.NoError(t, err) - _, b, isNum, err := setCellTime(date.In(timezone), false) + c := &xlsxC{} + isNum, err := c.setCellTime(date.In(timezone), false) assert.NoError(t, err) assert.Equal(t, true, isNum) - assert.Equal(t, expected, b) + assert.Equal(t, expected, c.V) } } @@ -237,7 +238,7 @@ func TestGetCellValue(t *testing.T) { sheetData := `%s` f.Sheet.Delete("xl/worksheets/sheet1.xml") - f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `A3A4B4A7B7A8B8`))) + f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `A3A4B4A7B7A8B8`))) f.checked = nil cells := []string{"A3", "A4", "B4", "A7", "B7"} rows, err := f.GetRows("Sheet1") @@ -253,35 +254,35 @@ func TestGetCellValue(t *testing.T) { assert.NoError(t, err) f.Sheet.Delete("xl/worksheets/sheet1.xml") - f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `A2B2`))) + f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `A2B2`))) f.checked = nil cell, err := f.GetCellValue("Sheet1", "A2") assert.Equal(t, "A2", cell) assert.NoError(t, err) f.Sheet.Delete("xl/worksheets/sheet1.xml") - f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `A2B2`))) + 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.NoError(t, err) f.Sheet.Delete("xl/worksheets/sheet1.xml") - f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `A1B1`))) + 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.NoError(t, err) f.Sheet.Delete("xl/worksheets/sheet1.xml") - f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `A3A4B4A7B7A8B8`))) + 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.NoError(t, err) f.Sheet.Delete("xl/worksheets/sheet1.xml") - f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `H6r0A6F4A6B6C6100B3`))) + f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `H6r0A6F4A6B6C6100B3`))) f.checked = nil cell, err = f.GetCellValue("Sheet1", "H6") assert.Equal(t, "H6", cell) @@ -326,8 +327,8 @@ func TestGetCellValue(t *testing.T) { 275.39999999999998 68.900000000000006 1.1000000000000001 - 1234567890123_4 - 123456789_0123_4 + 1234567890123_4 + 123456789_0123_4 +0.0000000000000000002399999999999992E-4 7.2399999999999992E-2 20200208T080910.123 @@ -386,7 +387,7 @@ func TestGetCellType(t *testing.T) { assert.NoError(t, f.SetCellValue("Sheet1", "A1", "A1")) cellType, err = f.GetCellType("Sheet1", "A1") assert.NoError(t, err) - assert.Equal(t, CellTypeString, cellType) + assert.Equal(t, CellTypeSharedString, cellType) _, err = f.GetCellType("Sheet1", "A") assert.EqualError(t, err, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) } diff --git a/col_test.go b/col_test.go index 75c191b93a..f786335709 100644 --- a/col_test.go +++ b/col_test.go @@ -109,12 +109,12 @@ func TestGetColsError(t *testing.T) { f = NewFile() f.Sheet.Delete("xl/worksheets/sheet1.xml") - f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`B`)) + f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`B`)) f.checked = nil _, err = f.GetCols("Sheet1") assert.EqualError(t, err, `strconv.Atoi: parsing "A": invalid syntax`) - f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`B`)) + f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`B`)) _, err = f.GetCols("Sheet1") assert.EqualError(t, err, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) @@ -124,7 +124,7 @@ func TestGetColsError(t *testing.T) { cols.totalRows = 2 cols.totalCols = 2 cols.curCol = 1 - cols.sheetXML = []byte(`A`) + cols.sheetXML = []byte(`A`) _, err = cols.Rows() assert.EqualError(t, err, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) diff --git a/rows.go b/rows.go index 9f791cb80b..4f05f24314 100644 --- a/rows.go +++ b/rows.go @@ -20,8 +20,6 @@ import ( "math" "os" "strconv" - "strings" - "time" "github.com/mohae/deepcopy" ) @@ -449,81 +447,6 @@ func (f *File) sharedStringsReader() *xlsxSST { return f.SharedStrings } -// getCellDate parse cell value which containing a boolean. -func (c *xlsxC) getCellBool(f *File, raw bool) (string, error) { - if !raw { - if c.V == "1" { - return "TRUE", nil - } - if c.V == "0" { - return "FALSE", nil - } - } - return f.formattedValue(c.S, c.V, raw), nil -} - -// getCellDate parse cell value which contains a date in the ISO 8601 format. -func (c *xlsxC) getCellDate(f *File, raw bool) (string, error) { - if !raw { - layout := "20060102T150405.999" - if strings.HasSuffix(c.V, "Z") { - layout = "20060102T150405Z" - if strings.Contains(c.V, "-") { - layout = "2006-01-02T15:04:05Z" - } - } else if strings.Contains(c.V, "-") { - layout = "2006-01-02 15:04:05Z" - } - if timestamp, err := time.Parse(layout, strings.ReplaceAll(c.V, ",", ".")); err == nil { - excelTime, _ := timeToExcelTime(timestamp, false) - c.V = strconv.FormatFloat(excelTime, 'G', 15, 64) - } - } - return f.formattedValue(c.S, c.V, raw), nil -} - -// getValueFrom return a value from a column/row cell, this function is -// intended to be used with for range on rows an argument with the spreadsheet -// opened file. -func (c *xlsxC) getValueFrom(f *File, d *xlsxSST, raw bool) (string, error) { - f.Lock() - defer f.Unlock() - switch c.T { - case "b": - return c.getCellBool(f, raw) - case "d": - return c.getCellDate(f, raw) - case "s": - if c.V != "" { - xlsxSI := 0 - xlsxSI, _ = strconv.Atoi(c.V) - if _, ok := f.tempFiles.Load(defaultXMLPathSharedStrings); ok { - return f.formattedValue(c.S, f.getFromStringItem(xlsxSI), raw), nil - } - if len(d.SI) > xlsxSI { - return f.formattedValue(c.S, d.SI[xlsxSI].String(), raw), nil - } - } - return f.formattedValue(c.S, c.V, raw), nil - case "str": - return f.formattedValue(c.S, c.V, raw), nil - case "inlineStr": - if c.IS != nil { - return f.formattedValue(c.S, c.IS.String(), raw), nil - } - return f.formattedValue(c.S, c.V, raw), nil - default: - if isNum, precision, decimal := isNumeric(c.V); isNum && !raw { - if precision > 15 { - c.V = strconv.FormatFloat(decimal, 'G', 15, 64) - } else { - c.V = strconv.FormatFloat(decimal, 'f', -1, 64) - } - } - return f.formattedValue(c.S, c.V, raw), nil - } -} - // SetRowVisible 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: // diff --git a/rows_test.go b/rows_test.go index 423932f8ac..81572e1852 100644 --- a/rows_test.go +++ b/rows_test.go @@ -203,12 +203,12 @@ func TestColumns(t *testing.T) { _, err = rows.Columns() assert.NoError(t, err) - rows.decoder = f.xmlNewDecoder(bytes.NewReader([]byte(`1B`))) + rows.decoder = f.xmlNewDecoder(bytes.NewReader([]byte(`1B`))) assert.True(t, rows.Next()) _, err = rows.Columns() assert.EqualError(t, err, `strconv.Atoi: parsing "A": invalid syntax`) - rows.decoder = f.xmlNewDecoder(bytes.NewReader([]byte(`1B`))) + rows.decoder = f.xmlNewDecoder(bytes.NewReader([]byte(`1B`))) _, err = rows.Columns() assert.NoError(t, err) diff --git a/sheet_test.go b/sheet_test.go index 6e87de9cb0..4e1e44818b 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -76,18 +76,18 @@ func TestSearchSheet(t *testing.T) { f = NewFile() f.Sheet.Delete("xl/worksheets/sheet1.xml") - f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`A`)) + f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`A`)) f.checked = nil result, err = f.SearchSheet("Sheet1", "A") assert.EqualError(t, err, "strconv.Atoi: parsing \"A\": invalid syntax") assert.Equal(t, []string(nil), result) - f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`A`)) + f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`A`)) result, err = f.SearchSheet("Sheet1", "A") assert.EqualError(t, err, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) assert.Equal(t, []string(nil), result) - f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`A`)) + f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`A`)) result, err = f.SearchSheet("Sheet1", "A") assert.EqualError(t, err, "invalid cell reference [1, 0]") assert.Equal(t, []string(nil), result) diff --git a/stream.go b/stream.go index aaa45893f8..fa78d8bb91 100644 --- a/stream.go +++ b/stream.go @@ -263,7 +263,7 @@ func (sw *StreamWriter) getRowValues(hRow, hCol, vCol int) (res []string, err er if col < hCol || col > vCol { continue } - res[col-hCol] = c.V + res[col-hCol], _ = c.getValueFrom(sw.File, nil, false) } return res, nil } @@ -462,7 +462,7 @@ func (sw *StreamWriter) MergeCell(hCell, vCell string) error { // setCellFormula provides a function to set formula of a cell. func setCellFormula(c *xlsxC, formula string) { if formula != "" { - c.F = &xlsxF{Content: formula} + c.T, c.F = "str", &xlsxF{Content: formula} } } @@ -477,9 +477,9 @@ func (sw *StreamWriter) setCellValFunc(c *xlsxC, val interface{}) error { case float64: c.T, c.V = setCellFloat(val, -1, 64) case string: - c.T, c.V, c.XMLSpace = setCellStr(val) + c.setCellValue(val) case []byte: - c.T, c.V, c.XMLSpace = setCellStr(string(val)) + c.setCellValue(string(val)) case time.Duration: c.T, c.V = setCellDuration(val) case time.Time: @@ -488,20 +488,19 @@ func (sw *StreamWriter) setCellValFunc(c *xlsxC, val interface{}) error { if wb != nil && wb.WorkbookPr != nil { date1904 = wb.WorkbookPr.Date1904 } - c.T, c.V, isNum, err = setCellTime(val, date1904) - if isNum && c.S == 0 { + if isNum, err = c.setCellTime(val, date1904); isNum && c.S == 0 { style, _ := sw.File.NewStyle(&Style{NumFmt: 22}) c.S = style } case bool: c.T, c.V = setCellBool(val) case nil: - c.T, c.V, c.XMLSpace = setCellStr("") + c.setCellValue("") case []RichTextRun: c.T, c.IS = "inlineStr", &xlsxSI{} c.IS.R, err = setRichText(val) default: - c.T, c.V, c.XMLSpace = setCellStr(fmt.Sprint(val)) + c.setCellValue(fmt.Sprint(val)) } return err } @@ -569,10 +568,25 @@ func writeCell(buf *bufferedWriter, c xlsxC) { _, _ = buf.WriteString(``) } if c.IS != nil { - is, _ := xml.Marshal(c.IS.R) - _, _ = buf.WriteString(``) - _, _ = buf.Write(is) - _, _ = buf.WriteString(``) + if len(c.IS.R) > 0 { + is, _ := xml.Marshal(c.IS.R) + _, _ = buf.WriteString(``) + _, _ = buf.Write(is) + _, _ = buf.WriteString(``) + } + if c.IS.T != nil { + _, _ = buf.WriteString(``) + _, _ = buf.Write([]byte(c.IS.T.Val)) + _, _ = buf.WriteString(``) + } } _, _ = buf.WriteString(``) }