From eed431e0fc2f61b13e7745857a41cb47d9f7f810 Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 2 May 2022 12:30:18 +0800 Subject: [PATCH] This closes #1219, fixes cell value reading issue, improves performance, and 1904 date system support - Fix incorrect cell data types casting results when number formatting - Support set cell value on 1904 date system enabled, ref #1212 - Improve performance for set sheet row and the merging cells, fix performance impact when resolving #1129 --- cell.go | 23 ++++++++++++++--------- cell_test.go | 2 +- date.go | 20 +++++++++----------- date_test.go | 33 +++++++++++++++++++++++---------- merge.go | 7 ++----- numfmt.go | 7 ++++--- rows_test.go | 5 +++++ stream.go | 6 +++++- 8 files changed, 63 insertions(+), 40 deletions(-) diff --git a/cell.go b/cell.go index 70832cef..80c03efe 100644 --- a/cell.go +++ b/cell.go @@ -211,9 +211,12 @@ func (f *File) setCellTimeFunc(sheet, axis string, value time.Time) error { ws.Lock() cellData.S = f.prepareCellStyle(ws, col, row, cellData.S) ws.Unlock() - + date1904, wb := false, f.workbookReader() + if wb != nil && wb.WorkbookPr != nil { + date1904 = wb.WorkbookPr.Date1904 + } var isNum bool - cellData.T, cellData.V, isNum, err = setCellTime(value) + cellData.T, cellData.V, isNum, err = setCellTime(value, date1904) if err != nil { return err } @@ -225,11 +228,11 @@ func (f *File) setCellTimeFunc(sheet, axis string, value time.Time) error { // setCellTime prepares cell type and Excel time by given Go time.Time type // timestamp. -func setCellTime(value time.Time) (t string, b string, isNum bool, err error) { +func setCellTime(value time.Time, date1904 bool) (t string, b string, isNum bool, err error) { var excelTime float64 _, offset := value.In(value.Location()).Zone() value = value.Add(time.Duration(offset) * time.Second) - if excelTime, err = timeToExcelTime(value); err != nil { + if excelTime, err = timeToExcelTime(value, date1904); err != nil { return } isNum = excelTime > 0 @@ -1122,8 +1125,7 @@ func (f *File) formattedValue(s int, v string, raw bool) string { if wb != nil && wb.WorkbookPr != nil { date1904 = wb.WorkbookPr.Date1904 } - ok := builtInNumFmtFunc[numFmtID] - if ok != nil { + if ok := builtInNumFmtFunc[numFmtID]; ok != nil { return ok(v, builtInNumFmt[numFmtID], date1904) } if styleSheet == nil || styleSheet.NumFmts == nil { @@ -1140,15 +1142,18 @@ func (f *File) formattedValue(s int, v string, raw bool) string { // prepareCellStyle provides a function to prepare style index of cell in // worksheet by given column index and style index. func (f *File) prepareCellStyle(ws *xlsxWorksheet, col, row, style int) int { - if ws.Cols != nil && style == 0 { + if style != 0 { + return style + } + if ws.Cols != nil { for _, c := range ws.Cols.Col { if c.Min <= col && col <= c.Max && c.Style != 0 { return c.Style } } } - for rowIdx := range ws.SheetData.Row { - if styleID := ws.SheetData.Row[rowIdx].S; style == 0 && styleID != 0 { + if row <= len(ws.SheetData.Row) { + if styleID := ws.SheetData.Row[row-1].S; styleID != 0 { return styleID } } diff --git a/cell_test.go b/cell_test.go index 77179cc2..8ed8e1f6 100644 --- a/cell_test.go +++ b/cell_test.go @@ -192,7 +192,7 @@ func TestSetCellTime(t *testing.T) { } { timezone, err := time.LoadLocation(location) assert.NoError(t, err) - _, b, isNum, err := setCellTime(date.In(timezone)) + _, b, isNum, err := setCellTime(date.In(timezone), false) assert.NoError(t, err) assert.Equal(t, true, isNum) assert.Equal(t, expected, b) diff --git a/date.go b/date.go index 1574af78..3e81319d 100644 --- a/date.go +++ b/date.go @@ -32,21 +32,19 @@ var ( ) // timeToExcelTime provides a function to convert time to Excel time. -func timeToExcelTime(t time.Time) (float64, error) { - // TODO in future this should probably also handle date1904 and like TimeFromExcelTime - - if t.Before(excelMinTime1900) { +func timeToExcelTime(t time.Time, date1904 bool) (float64, error) { + date := excelMinTime1900 + if date1904 { + date = excel1904Epoc + } + if t.Before(date) { return 0, nil } - - tt := t - diff := t.Sub(excelMinTime1900) - result := float64(0) - + tt, diff, result := t, t.Sub(date), 0.0 for diff >= maxDuration { result += float64(maxDuration / dayNanoseconds) tt = tt.Add(-maxDuration) - diff = tt.Sub(excelMinTime1900) + diff = tt.Sub(date) } rem := diff % dayNanoseconds @@ -57,7 +55,7 @@ func timeToExcelTime(t time.Time) (float64, error) { // Microsoft intentionally included this bug in Excel so that it would remain compatible with the spreadsheet // program that had the majority market share at the time; Lotus 1-2-3. // https://www.myonlinetraininghub.com/excel-date-and-time - if t.After(excelBuggyPeriodStart) { + if !date1904 && t.After(excelBuggyPeriodStart) { result++ } return result, nil diff --git a/date_test.go b/date_test.go index cc21e58a..4091e378 100644 --- a/date_test.go +++ b/date_test.go @@ -40,7 +40,7 @@ var excelTimeInputList = []dateTest{ func TestTimeToExcelTime(t *testing.T) { for i, test := range trueExpectedDateList { t.Run(fmt.Sprintf("TestData%d", i+1), func(t *testing.T) { - excelTime, err := timeToExcelTime(test.GoValue) + excelTime, err := timeToExcelTime(test.GoValue, false) assert.NoError(t, err) assert.Equalf(t, test.ExcelValue, excelTime, "Time: %s", test.GoValue.String()) @@ -55,7 +55,7 @@ func TestTimeToExcelTime_Timezone(t *testing.T) { } for i, test := range trueExpectedDateList { t.Run(fmt.Sprintf("TestData%d", i+1), func(t *testing.T) { - _, err := timeToExcelTime(test.GoValue.In(location)) + _, err := timeToExcelTime(test.GoValue.In(location), false) assert.NoError(t, err) }) } @@ -71,21 +71,34 @@ func TestTimeFromExcelTime(t *testing.T) { for min := 0; min < 60; min++ { for sec := 0; sec < 60; sec++ { date := time.Date(2021, time.December, 30, hour, min, sec, 0, time.UTC) - excelTime, err := timeToExcelTime(date) + // Test use 1900 date system + excel1900Time, err := timeToExcelTime(date, false) assert.NoError(t, err) - dateOut := timeFromExcelTime(excelTime, false) - assert.EqualValues(t, hour, dateOut.Hour()) - assert.EqualValues(t, min, dateOut.Minute()) - assert.EqualValues(t, sec, dateOut.Second()) + date1900Out := timeFromExcelTime(excel1900Time, false) + assert.EqualValues(t, hour, date1900Out.Hour()) + assert.EqualValues(t, min, date1900Out.Minute()) + assert.EqualValues(t, sec, date1900Out.Second()) + // Test use 1904 date system + excel1904Time, err := timeToExcelTime(date, true) + assert.NoError(t, err) + date1904Out := timeFromExcelTime(excel1904Time, true) + assert.EqualValues(t, hour, date1904Out.Hour()) + assert.EqualValues(t, min, date1904Out.Minute()) + assert.EqualValues(t, sec, date1904Out.Second()) } } } } func TestTimeFromExcelTime_1904(t *testing.T) { - _, _ = shiftJulianToNoon(1, -0.6) - timeFromExcelTime(61, true) - timeFromExcelTime(62, true) + julianDays, julianFraction := shiftJulianToNoon(1, -0.6) + assert.Equal(t, julianDays, 0.0) + assert.Equal(t, julianFraction, 0.9) + julianDays, julianFraction = shiftJulianToNoon(1, 0.1) + assert.Equal(t, julianDays, 1.0) + assert.Equal(t, julianFraction, 0.6) + assert.Equal(t, timeFromExcelTime(61, true), time.Date(1904, time.March, 2, 0, 0, 0, 0, time.UTC)) + assert.Equal(t, timeFromExcelTime(62, true), time.Date(1904, time.March, 3, 0, 0, 0, 0, time.UTC)) } func TestExcelDateToTime(t *testing.T) { diff --git a/merge.go b/merge.go index 376b68b2..0f57826e 100644 --- a/merge.go +++ b/merge.go @@ -11,9 +11,7 @@ package excelize -import ( - "strings" -) +import "strings" // Rect gets merged cell rectangle coordinates sequence. func (mc *xlsxMergeCell) Rect() ([]int, error) { @@ -70,8 +68,7 @@ func (f *File) MergeCell(sheet, hCell, vCell string) error { ws.MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: ref, rect: rect}}} } ws.MergeCells.Count = len(ws.MergeCells.Cells) - styleID, _ := f.GetCellStyle(sheet, hCell) - return f.SetCellStyle(sheet, hCell, vCell, styleID) + return err } // UnmergeCell provides a function to unmerge a given coordinate area. diff --git a/numfmt.go b/numfmt.go index 5503027a..2052fd9b 100644 --- a/numfmt.go +++ b/numfmt.go @@ -939,10 +939,11 @@ func (nf *numberFormat) textHandler() (result string) { // getValueSectionType returns its applicable number format expression section // based on the given value. func (nf *numberFormat) getValueSectionType(value string) (float64, string) { - number, err := strconv.ParseFloat(value, 64) - if err != nil { - return number, nfp.TokenSectionText + isNum, _ := isNumeric(value) + if !isNum { + return 0, nfp.TokenSectionText } + number, _ := strconv.ParseFloat(value, 64) if number > 0 { return number, nfp.TokenSectionPositive } diff --git a/rows_test.go b/rows_test.go index ae308381..014b2d85 100644 --- a/rows_test.go +++ b/rows_test.go @@ -928,6 +928,11 @@ func TestSetRowStyle(t *testing.T) { cellStyleID, err := f.GetCellStyle("Sheet1", "B2") assert.NoError(t, err) assert.Equal(t, style2, cellStyleID) + // Test cell inheritance rows style + assert.NoError(t, f.SetCellValue("Sheet1", "C1", nil)) + cellStyleID, err = f.GetCellStyle("Sheet1", "C1") + assert.NoError(t, err) + assert.Equal(t, style2, cellStyleID) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetRowStyle.xlsx"))) } diff --git a/stream.go b/stream.go index c2eda68a..e1a12bec 100644 --- a/stream.go +++ b/stream.go @@ -440,7 +440,11 @@ func (sw *StreamWriter) setCellValFunc(c *xlsxC, val interface{}) (err error) { c.T, c.V = setCellDuration(val) case time.Time: var isNum bool - c.T, c.V, isNum, err = setCellTime(val) + date1904, wb := false, sw.File.workbookReader() + if wb != nil && wb.WorkbookPr != nil { + date1904 = wb.WorkbookPr.Date1904 + } + c.T, c.V, isNum, err = setCellTime(val, date1904) if isNum && c.S == 0 { style, _ := sw.File.NewStyle(&Style{NumFmt: 22}) c.S = style