From dfdd97c0a7770cb83501b717d9084b634314de40 Mon Sep 17 00:00:00 2001 From: fudali Date: Sat, 6 May 2023 20:34:18 +0800 Subject: [PATCH] This closes #1199, support apply number format by system date and time options - Add new options `ShortDateFmtCode`, `LongDateFmtCode` and `LongTimeFmtCode` - Update unit tests --- cell.go | 13 +++++++++++-- cell_test.go | 4 ++-- excelize.go | 15 +++++++++++++++ file.go | 3 ++- numfmt.go | 33 +++++++++++++++++++++++---------- numfmt_test.go | 23 ++++++++++++++++++++--- rows_test.go | 9 +++++++++ 7 files changed, 82 insertions(+), 18 deletions(-) diff --git a/cell.go b/cell.go index 2ab05dc3..064eba49 100644 --- a/cell.go +++ b/cell.go @@ -1336,6 +1336,15 @@ func (f *File) getCellStringFunc(sheet, cell string, fn func(x *xlsxWorksheet, c return "", nil } +// applyBuiltInNumFmt provides a function to returns a value after formatted +// with built-in number format code, or specified sort date format code. +func (f *File) applyBuiltInNumFmt(c *xlsxC, fmtCode string, numFmtID int, date1904 bool, cellType CellType) string { + if numFmtID == 14 && f.options != nil && f.options.ShortDateFmtCode != "" { + fmtCode = f.options.ShortDateFmtCode + } + return format(c.V, fmtCode, date1904, cellType, f.options) +} + // formattedValue provides a function to returns a value after formatted. If // it is possible to apply a format to the cell value, it will do so, if not // then an error will be returned, along with the raw value of the cell. @@ -1366,14 +1375,14 @@ func (f *File) formattedValue(c *xlsxC, raw bool, cellType CellType) (string, er date1904 = wb.WorkbookPr.Date1904 } if fmtCode, ok := builtInNumFmt[numFmtID]; ok { - return format(c.V, fmtCode, date1904, cellType), err + return f.applyBuiltInNumFmt(c, fmtCode, numFmtID, date1904, cellType), err } if styleSheet.NumFmts == nil { return c.V, err } for _, xlsxFmt := range styleSheet.NumFmts.NumFmt { if xlsxFmt.NumFmtID == numFmtID { - return format(c.V, xlsxFmt.FormatCode, date1904, cellType), err + return format(c.V, xlsxFmt.FormatCode, date1904, cellType, f.options), err } } return c.V, err diff --git a/cell_test.go b/cell_test.go index ec7e5a32..7b53d86d 100644 --- a/cell_test.go +++ b/cell_test.go @@ -873,7 +873,7 @@ func TestFormattedValue(t *testing.T) { assert.NoError(t, err) assert.Equal(t, "311", result) - assert.Equal(t, "0_0", format("0_0", "", false, CellTypeNumber)) + assert.Equal(t, "0_0", format("0_0", "", false, CellTypeNumber, nil)) // Test format value with unsupported charset workbook f.WorkBook = nil @@ -887,7 +887,7 @@ func TestFormattedValue(t *testing.T) { _, err = f.formattedValue(&xlsxC{S: 1, V: "43528"}, false, CellTypeNumber) assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") - assert.Equal(t, "text", format("text", "0", false, CellTypeNumber)) + assert.Equal(t, "text", format("text", "0", false, CellTypeNumber, nil)) } func TestFormattedValueNilXfs(t *testing.T) { diff --git a/excelize.go b/excelize.go index 6c7ce2a1..7d84e579 100644 --- a/excelize.go +++ b/excelize.go @@ -79,12 +79,27 @@ type charsetTranscoderFn func(charset string, input io.Reader) (rdr io.Reader, e // temporary directory when the file size is over this value, this value // should be less than or equal to UnzipSizeLimit, the default value is // 16MB. +// +// ShortDateFmtCode specifies the short date number format code. In the +// spreadsheet applications, date formats display date and time serial numbers +// as date values. Date formats that begin with an asterisk (*) respond to +// changes in regional date and time settings that are specified for the +// operating system. Formats without an asterisk are not affected by operating +// system settings. The ShortDateFmtCode used for specifies apply date formats +// that begin with an asterisk. +// +// LongDateFmtCode specifies the long date number format code. +// +// LongTimeFmtCode specifies the long time number format code. type Options struct { MaxCalcIterations uint Password string RawCellValue bool UnzipSizeLimit int64 UnzipXMLSizeLimit int64 + ShortDateFmtCode string + LongDateFmtCode string + LongTimeFmtCode string } // OpenFile take the name of an spreadsheet file and returns a populated diff --git a/file.go b/file.go index 416c9343..19333ea3 100644 --- a/file.go +++ b/file.go @@ -26,7 +26,7 @@ import ( // For example: // // f := NewFile() -func NewFile() *File { +func NewFile(opts ...Options) *File { f := newFile() f.Pkg.Store("_rels/.rels", []byte(xml.Header+templateRels)) f.Pkg.Store(defaultXMLPathDocPropsApp, []byte(xml.Header+templateDocpropsApp)) @@ -49,6 +49,7 @@ func NewFile() *File { ws, _ := f.workSheetReader("Sheet1") f.Sheet.Store("xl/worksheets/sheet1.xml", ws) f.Theme, _ = f.themeReader() + f.options = getOptions(opts...) return f } diff --git a/numfmt.go b/numfmt.go index 283a4f88..5e8155ef 100644 --- a/numfmt.go +++ b/numfmt.go @@ -32,6 +32,7 @@ type languageInfo struct { // numberFormat directly maps the number format parser runtime required // fields. type numberFormat struct { + opts *Options cellType CellType section []nfp.Section t time.Time @@ -396,9 +397,9 @@ func (nf *numberFormat) prepareNumberic(value string) { // format provides a function to return a string parse by number format // expression. If the given number format is not supported, this will return // the original cell value. -func format(value, numFmt string, date1904 bool, cellType CellType) string { +func format(value, numFmt string, date1904 bool, cellType CellType, opts *Options) string { p := nfp.NumberFormatParser() - nf := numberFormat{section: p.Parse(numFmt), value: value, date1904: date1904, cellType: cellType} + nf := numberFormat{opts: opts, section: p.Parse(numFmt), value: value, date1904: date1904, cellType: cellType} nf.number, nf.valueSectionType = nf.getValueSectionType(value) nf.prepareNumberic(value) for i, section := range nf.section { @@ -480,7 +481,7 @@ func (nf *numberFormat) printNumberLiteral(text string) string { } for i, token := range nf.section[nf.sectionIdx].Items { if token.TType == nfp.TokenTypeCurrencyLanguage { - if err := nf.currencyLanguageHandler(i, token); err != nil { + if err, changeNumFmtCode := nf.currencyLanguageHandler(i, token); err != nil || changeNumFmtCode { return nf.value } result += nf.currencyString @@ -616,7 +617,7 @@ func (nf *numberFormat) dateTimeHandler() string { nf.t, nf.hours, nf.seconds = timeFromExcelTime(nf.number, nf.date1904), false, false for i, token := range nf.section[nf.sectionIdx].Items { if token.TType == nfp.TokenTypeCurrencyLanguage { - if err := nf.currencyLanguageHandler(i, token); err != nil { + if err, changeNumFmtCode := nf.currencyLanguageHandler(i, token); err != nil || changeNumFmtCode { return nf.value } nf.result += nf.currencyString @@ -687,16 +688,28 @@ func (nf *numberFormat) positiveHandler() string { // currencyLanguageHandler will be handling currency and language types tokens // for a number format expression. -func (nf *numberFormat) currencyLanguageHandler(i int, token nfp.Token) (err error) { +func (nf *numberFormat) currencyLanguageHandler(i int, token nfp.Token) (error, bool) { for _, part := range token.Parts { if inStrSlice(supportedTokenTypes, part.Token.TType, true) == -1 { - err = ErrUnsupportedNumberFormat - return + return ErrUnsupportedNumberFormat, false } if part.Token.TType == nfp.TokenSubTypeLanguageInfo { + if strings.EqualFold(part.Token.TValue, "F800") { // [$-x-sysdate] + if nf.opts != nil && nf.opts.LongDateFmtCode != "" { + nf.value = format(nf.value, nf.opts.LongDateFmtCode, nf.date1904, nf.cellType, nf.opts) + return nil, true + } + part.Token.TValue = "409" + } + if strings.EqualFold(part.Token.TValue, "F400") { // [$-x-systime] + if nf.opts != nil && nf.opts.LongTimeFmtCode != "" { + nf.value = format(nf.value, nf.opts.LongTimeFmtCode, nf.date1904, nf.cellType, nf.opts) + return nil, true + } + part.Token.TValue = "409" + } if _, ok := supportedLanguageInfo[strings.ToUpper(part.Token.TValue)]; !ok { - err = ErrUnsupportedNumberFormat - return + return ErrUnsupportedNumberFormat, false } nf.localCode = strings.ToUpper(part.Token.TValue) } @@ -704,7 +717,7 @@ func (nf *numberFormat) currencyLanguageHandler(i int, token nfp.Token) (err err nf.currencyString = part.Token.TValue } } - return + return nil, false } // localAmPm return AM/PM name by supported language ID. diff --git a/numfmt_test.go b/numfmt_test.go index a8c90047..21a657f6 100644 --- a/numfmt_test.go +++ b/numfmt_test.go @@ -995,6 +995,8 @@ func TestNumFmt(t *testing.T) { {"44835.18957170139", "[$-435]mmmmm dd yyyy h:mm AM/PM", "O 01 2022 4:32 AM"}, {"44866.18957170139", "[$-435]mmmmm dd yyyy h:mm AM/PM", "N 01 2022 4:32 AM"}, {"44896.18957170139", "[$-435]mmmmm dd yyyy h:mm AM/PM", "D 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-F800]dddd, mmmm dd, yyyy", "Tuesday, March 19, 2019"}, + {"43543.503206018519", "[$-F400]h:mm:ss AM/PM", "12:04:37 PM"}, {"text_", "General", "text_"}, {"text_", "\"=====\"@@@\"--\"@\"----\"", "=====text_text_text_--text_----"}, {"0.0450685976001E+21", "0_);[Red]\\(0\\)", "45068597600100000000"}, @@ -1061,9 +1063,22 @@ func TestNumFmt(t *testing.T) { {"1234.5678", "0.0xxx00", "1234.5678"}, {"-1234.5678", "00000.00###;s;", "-1234.5678"}, } { - result := format(item[0], item[1], false, CellTypeNumber) + result := format(item[0], item[1], false, CellTypeNumber, nil) assert.Equal(t, item[2], result, item) } + // Test format number with specified date and time format code + for _, item := range [][]string{ + {"43543.503206018519", "[$-F800]dddd, mmmm dd, yyyy", "2019年3月19日"}, + {"43543.503206018519", "[$-F400]h:mm:ss AM/PM", "12:04:37"}, + } { + result := format(item[0], item[1], false, CellTypeNumber, &Options{ + ShortDateFmtCode: "yyyy/m/d", + LongDateFmtCode: "yyyy\"年\"M\"月\"d\"日\"", + LongTimeFmtCode: "H:mm:ss", + }) + assert.Equal(t, item[2], result, item) + } + // Test format number with string data type cell value for _, cellType := range []CellType{CellTypeSharedString, CellTypeInlineString} { for _, item := range [][]string{ {"1234.5678", "General", "1234.5678"}, @@ -1073,10 +1088,12 @@ func TestNumFmt(t *testing.T) { {"1234.5678", "0_);[Red]\\(0\\)", "1234.5678"}, {"1234.5678", "\"text\"@", "text1234.5678"}, } { - result := format(item[0], item[1], false, cellType) + result := format(item[0], item[1], false, cellType, nil) assert.Equal(t, item[2], result, item) } } nf := numberFormat{} - assert.Equal(t, ErrUnsupportedNumberFormat, nf.currencyLanguageHandler(0, nfp.Token{Parts: []nfp.Part{{}}})) + err, changeNumFmtCode := nf.currencyLanguageHandler(0, nfp.Token{Parts: []nfp.Part{{}}}) + assert.Equal(t, ErrUnsupportedNumberFormat, err) + assert.False(t, changeNumFmtCode) } diff --git a/rows_test.go b/rows_test.go index 5a9dc824..f836cc05 100644 --- a/rows_test.go +++ b/rows_test.go @@ -1117,6 +1117,15 @@ func TestNumberFormats(t *testing.T) { assert.Equal(t, expected, result, cell) } assert.NoError(t, f.SaveAs(filepath.Join("test", "TestNumberFormats.xlsx"))) + + f = NewFile(Options{ShortDateFmtCode: "yyyy/m/d"}) + assert.NoError(t, f.SetCellValue("Sheet1", "A1", 43543.503206018519)) + numFmt14, err := f.NewStyle(&Style{NumFmt: 14}) + assert.NoError(t, err) + assert.NoError(t, f.SetCellStyle("Sheet1", "A1", "A1", numFmt14)) + result, err := f.GetCellValue("Sheet1", "A1") + assert.NoError(t, err) + assert.Equal(t, "2019/3/19", result, "A1") } func BenchmarkRows(b *testing.B) {