Skip to content

Commit

Permalink
This closes qax-os#1219, fixes cell value reading issue, improves per…
Browse files Browse the repository at this point in the history
…formance, 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 qax-os#1212
- Improve performance for set sheet row and the merging cells, fix performance impact when resolving qax-os#1129
  • Loading branch information
xuri authored May 2, 2022
1 parent 25e1459 commit 17d1695
Show file tree
Hide file tree
Showing 8 changed files with 63 additions and 40 deletions.
23 changes: 14 additions & 9 deletions cell.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -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
}
}
Expand Down
2 changes: 1 addition & 1 deletion cell_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
20 changes: 9 additions & 11 deletions date.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
33 changes: 23 additions & 10 deletions date_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand All @@ -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)
})
}
Expand All @@ -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) {
Expand Down
7 changes: 2 additions & 5 deletions merge.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,7 @@

package excelize

import (
"strings"
)
import "strings"

// Rect gets merged cell rectangle coordinates sequence.
func (mc *xlsxMergeCell) Rect() ([]int, error) {
Expand Down Expand Up @@ -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.
Expand Down
7 changes: 4 additions & 3 deletions numfmt.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
5 changes: 5 additions & 0 deletions rows_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")))
}

Expand Down
6 changes: 5 additions & 1 deletion stream.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 17d1695

Please sign in to comment.