forked from p30928647/excelize
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
This commit is contained in:
parent
773d4afa32
commit
eed431e0fc
23
cell.go
23
cell.go
|
@ -211,9 +211,12 @@ func (f *File) setCellTimeFunc(sheet, axis string, value time.Time) error {
|
||||||
ws.Lock()
|
ws.Lock()
|
||||||
cellData.S = f.prepareCellStyle(ws, col, row, cellData.S)
|
cellData.S = f.prepareCellStyle(ws, col, row, cellData.S)
|
||||||
ws.Unlock()
|
ws.Unlock()
|
||||||
|
date1904, wb := false, f.workbookReader()
|
||||||
|
if wb != nil && wb.WorkbookPr != nil {
|
||||||
|
date1904 = wb.WorkbookPr.Date1904
|
||||||
|
}
|
||||||
var isNum bool
|
var isNum bool
|
||||||
cellData.T, cellData.V, isNum, err = setCellTime(value)
|
cellData.T, cellData.V, isNum, err = setCellTime(value, date1904)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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
|
// setCellTime prepares cell type and Excel time by given Go time.Time type
|
||||||
// timestamp.
|
// 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
|
var excelTime float64
|
||||||
_, offset := value.In(value.Location()).Zone()
|
_, offset := value.In(value.Location()).Zone()
|
||||||
value = value.Add(time.Duration(offset) * time.Second)
|
value = value.Add(time.Duration(offset) * time.Second)
|
||||||
if excelTime, err = timeToExcelTime(value); err != nil {
|
if excelTime, err = timeToExcelTime(value, date1904); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
isNum = excelTime > 0
|
isNum = excelTime > 0
|
||||||
|
@ -1122,8 +1125,7 @@ func (f *File) formattedValue(s int, v string, raw bool) string {
|
||||||
if wb != nil && wb.WorkbookPr != nil {
|
if wb != nil && wb.WorkbookPr != nil {
|
||||||
date1904 = wb.WorkbookPr.Date1904
|
date1904 = wb.WorkbookPr.Date1904
|
||||||
}
|
}
|
||||||
ok := builtInNumFmtFunc[numFmtID]
|
if ok := builtInNumFmtFunc[numFmtID]; ok != nil {
|
||||||
if ok != nil {
|
|
||||||
return ok(v, builtInNumFmt[numFmtID], date1904)
|
return ok(v, builtInNumFmt[numFmtID], date1904)
|
||||||
}
|
}
|
||||||
if styleSheet == nil || styleSheet.NumFmts == nil {
|
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
|
// prepareCellStyle provides a function to prepare style index of cell in
|
||||||
// worksheet by given column index and style index.
|
// worksheet by given column index and style index.
|
||||||
func (f *File) prepareCellStyle(ws *xlsxWorksheet, col, row, style int) int {
|
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 {
|
for _, c := range ws.Cols.Col {
|
||||||
if c.Min <= col && col <= c.Max && c.Style != 0 {
|
if c.Min <= col && col <= c.Max && c.Style != 0 {
|
||||||
return c.Style
|
return c.Style
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for rowIdx := range ws.SheetData.Row {
|
if row <= len(ws.SheetData.Row) {
|
||||||
if styleID := ws.SheetData.Row[rowIdx].S; style == 0 && styleID != 0 {
|
if styleID := ws.SheetData.Row[row-1].S; styleID != 0 {
|
||||||
return styleID
|
return styleID
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -192,7 +192,7 @@ func TestSetCellTime(t *testing.T) {
|
||||||
} {
|
} {
|
||||||
timezone, err := time.LoadLocation(location)
|
timezone, err := time.LoadLocation(location)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
_, b, isNum, err := setCellTime(date.In(timezone))
|
_, b, isNum, err := setCellTime(date.In(timezone), false)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, true, isNum)
|
assert.Equal(t, true, isNum)
|
||||||
assert.Equal(t, expected, b)
|
assert.Equal(t, expected, b)
|
||||||
|
|
20
date.go
20
date.go
|
@ -32,21 +32,19 @@ var (
|
||||||
)
|
)
|
||||||
|
|
||||||
// timeToExcelTime provides a function to convert time to Excel time.
|
// timeToExcelTime provides a function to convert time to Excel time.
|
||||||
func timeToExcelTime(t time.Time) (float64, error) {
|
func timeToExcelTime(t time.Time, date1904 bool) (float64, error) {
|
||||||
// TODO in future this should probably also handle date1904 and like TimeFromExcelTime
|
date := excelMinTime1900
|
||||||
|
if date1904 {
|
||||||
if t.Before(excelMinTime1900) {
|
date = excel1904Epoc
|
||||||
|
}
|
||||||
|
if t.Before(date) {
|
||||||
return 0, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
|
tt, diff, result := t, t.Sub(date), 0.0
|
||||||
tt := t
|
|
||||||
diff := t.Sub(excelMinTime1900)
|
|
||||||
result := float64(0)
|
|
||||||
|
|
||||||
for diff >= maxDuration {
|
for diff >= maxDuration {
|
||||||
result += float64(maxDuration / dayNanoseconds)
|
result += float64(maxDuration / dayNanoseconds)
|
||||||
tt = tt.Add(-maxDuration)
|
tt = tt.Add(-maxDuration)
|
||||||
diff = tt.Sub(excelMinTime1900)
|
diff = tt.Sub(date)
|
||||||
}
|
}
|
||||||
|
|
||||||
rem := diff % dayNanoseconds
|
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
|
// 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.
|
// program that had the majority market share at the time; Lotus 1-2-3.
|
||||||
// https://www.myonlinetraininghub.com/excel-date-and-time
|
// https://www.myonlinetraininghub.com/excel-date-and-time
|
||||||
if t.After(excelBuggyPeriodStart) {
|
if !date1904 && t.After(excelBuggyPeriodStart) {
|
||||||
result++
|
result++
|
||||||
}
|
}
|
||||||
return result, nil
|
return result, nil
|
||||||
|
|
33
date_test.go
33
date_test.go
|
@ -40,7 +40,7 @@ var excelTimeInputList = []dateTest{
|
||||||
func TestTimeToExcelTime(t *testing.T) {
|
func TestTimeToExcelTime(t *testing.T) {
|
||||||
for i, test := range trueExpectedDateList {
|
for i, test := range trueExpectedDateList {
|
||||||
t.Run(fmt.Sprintf("TestData%d", i+1), func(t *testing.T) {
|
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.NoError(t, err)
|
||||||
assert.Equalf(t, test.ExcelValue, excelTime,
|
assert.Equalf(t, test.ExcelValue, excelTime,
|
||||||
"Time: %s", test.GoValue.String())
|
"Time: %s", test.GoValue.String())
|
||||||
|
@ -55,7 +55,7 @@ func TestTimeToExcelTime_Timezone(t *testing.T) {
|
||||||
}
|
}
|
||||||
for i, test := range trueExpectedDateList {
|
for i, test := range trueExpectedDateList {
|
||||||
t.Run(fmt.Sprintf("TestData%d", i+1), func(t *testing.T) {
|
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)
|
assert.NoError(t, err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -71,21 +71,34 @@ func TestTimeFromExcelTime(t *testing.T) {
|
||||||
for min := 0; min < 60; min++ {
|
for min := 0; min < 60; min++ {
|
||||||
for sec := 0; sec < 60; sec++ {
|
for sec := 0; sec < 60; sec++ {
|
||||||
date := time.Date(2021, time.December, 30, hour, min, sec, 0, time.UTC)
|
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)
|
assert.NoError(t, err)
|
||||||
dateOut := timeFromExcelTime(excelTime, false)
|
date1900Out := timeFromExcelTime(excel1900Time, false)
|
||||||
assert.EqualValues(t, hour, dateOut.Hour())
|
assert.EqualValues(t, hour, date1900Out.Hour())
|
||||||
assert.EqualValues(t, min, dateOut.Minute())
|
assert.EqualValues(t, min, date1900Out.Minute())
|
||||||
assert.EqualValues(t, sec, dateOut.Second())
|
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) {
|
func TestTimeFromExcelTime_1904(t *testing.T) {
|
||||||
_, _ = shiftJulianToNoon(1, -0.6)
|
julianDays, julianFraction := shiftJulianToNoon(1, -0.6)
|
||||||
timeFromExcelTime(61, true)
|
assert.Equal(t, julianDays, 0.0)
|
||||||
timeFromExcelTime(62, true)
|
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) {
|
func TestExcelDateToTime(t *testing.T) {
|
||||||
|
|
7
merge.go
7
merge.go
|
@ -11,9 +11,7 @@
|
||||||
|
|
||||||
package excelize
|
package excelize
|
||||||
|
|
||||||
import (
|
import "strings"
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Rect gets merged cell rectangle coordinates sequence.
|
// Rect gets merged cell rectangle coordinates sequence.
|
||||||
func (mc *xlsxMergeCell) Rect() ([]int, error) {
|
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 = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: ref, rect: rect}}}
|
||||||
}
|
}
|
||||||
ws.MergeCells.Count = len(ws.MergeCells.Cells)
|
ws.MergeCells.Count = len(ws.MergeCells.Cells)
|
||||||
styleID, _ := f.GetCellStyle(sheet, hCell)
|
return err
|
||||||
return f.SetCellStyle(sheet, hCell, vCell, styleID)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmergeCell provides a function to unmerge a given coordinate area.
|
// UnmergeCell provides a function to unmerge a given coordinate area.
|
||||||
|
|
|
@ -939,10 +939,11 @@ func (nf *numberFormat) textHandler() (result string) {
|
||||||
// getValueSectionType returns its applicable number format expression section
|
// getValueSectionType returns its applicable number format expression section
|
||||||
// based on the given value.
|
// based on the given value.
|
||||||
func (nf *numberFormat) getValueSectionType(value string) (float64, string) {
|
func (nf *numberFormat) getValueSectionType(value string) (float64, string) {
|
||||||
number, err := strconv.ParseFloat(value, 64)
|
isNum, _ := isNumeric(value)
|
||||||
if err != nil {
|
if !isNum {
|
||||||
return number, nfp.TokenSectionText
|
return 0, nfp.TokenSectionText
|
||||||
}
|
}
|
||||||
|
number, _ := strconv.ParseFloat(value, 64)
|
||||||
if number > 0 {
|
if number > 0 {
|
||||||
return number, nfp.TokenSectionPositive
|
return number, nfp.TokenSectionPositive
|
||||||
}
|
}
|
||||||
|
|
|
@ -928,6 +928,11 @@ func TestSetRowStyle(t *testing.T) {
|
||||||
cellStyleID, err := f.GetCellStyle("Sheet1", "B2")
|
cellStyleID, err := f.GetCellStyle("Sheet1", "B2")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, style2, cellStyleID)
|
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")))
|
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetRowStyle.xlsx")))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -440,7 +440,11 @@ func (sw *StreamWriter) setCellValFunc(c *xlsxC, val interface{}) (err error) {
|
||||||
c.T, c.V = setCellDuration(val)
|
c.T, c.V = setCellDuration(val)
|
||||||
case time.Time:
|
case time.Time:
|
||||||
var isNum bool
|
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 {
|
if isNum && c.S == 0 {
|
||||||
style, _ := sw.File.NewStyle(&Style{NumFmt: 22})
|
style, _ := sw.File.NewStyle(&Style{NumFmt: 22})
|
||||||
c.S = style
|
c.S = style
|
||||||
|
|
Loading…
Reference in New Issue