diff --git a/cell.go b/cell.go index 61dbed6216..2ab05dc3ca 100644 --- a/cell.go +++ b/cell.go @@ -1365,8 +1365,8 @@ func (f *File) formattedValue(c *xlsxC, raw bool, cellType CellType) (string, er if wb != nil && wb.WorkbookPr != nil { date1904 = wb.WorkbookPr.Date1904 } - if ok := builtInNumFmtFunc[numFmtID]; ok != nil { - return ok(c.V, builtInNumFmt[numFmtID], date1904, cellType), err + if fmtCode, ok := builtInNumFmt[numFmtID]; ok { + return format(c.V, fmtCode, date1904, cellType), err } if styleSheet.NumFmts == nil { return c.V, err diff --git a/cell_test.go b/cell_test.go index 89ec1733ac..ec7e5a32f0 100644 --- a/cell_test.go +++ b/cell_test.go @@ -873,9 +873,7 @@ func TestFormattedValue(t *testing.T) { assert.NoError(t, err) assert.Equal(t, "311", result) - for _, fn := range builtInNumFmtFunc { - assert.Equal(t, "0_0", fn("0_0", "", false, CellTypeNumber)) - } + assert.Equal(t, "0_0", format("0_0", "", false, CellTypeNumber)) // Test format value with unsupported charset workbook f.WorkBook = nil @@ -889,9 +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") - for _, fn := range builtInNumFmtFunc { - assert.Equal(t, fn("text", "0", false, CellTypeNumber), "text") - } + assert.Equal(t, "text", format("text", "0", false, CellTypeNumber)) } func TestFormattedValueNilXfs(t *testing.T) { diff --git a/excelize_test.go b/excelize_test.go index 17d16f0d4b..59ce3dfc42 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -747,33 +747,33 @@ func TestSetCellStyleNumberFormat(t *testing.T) { // Test only set fill and number format for a cell col := []string{"L", "M", "N", "O", "P"} - data := []int{0, 1, 2, 3, 4, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49} + idxTbl := []int{0, 1, 2, 3, 4, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49} value := []string{"37947.7500001", "-37947.7500001", "0.007", "2.1", "String"} expected := [][]string{ - {"37947.7500001", "37948", "37947.75", "37,948", "37947.75", "3794775%", "3794775.00%", "3.79E+04", "37947.7500001", "37947.7500001", "11-22-03", "22-Nov-03", "22-Nov", "Nov-03", "6:00 pm", "6:00:00 pm", "18:00", "18:00:00", "11/22/03 18:00", "37,948 ", "37,948 ", "37,947.75 ", "37,947.75 ", "37947.7500001", "37947.7500001", "37947.7500001", "37947.7500001", "00:00", "910746:00:00", "37947.7500001", "3.79E+04", "37947.7500001"}, - {"-37947.7500001", "-37948", "-37947.75", "-37,948", "-37947.75", "-3794775%", "-3794775.00%", "-3.79E+04", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "(37,948)", "(37,948)", "(37,947.75)", "(37,947.75)", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-3.79E+04", "-37947.7500001"}, - {"0.007", "0", "0.01", "0", "0.01", "1%", "0.70%", "7.00E-03", "0.007", "0.007", "12-30-99", "30-Dec-99", "30-Dec", "Dec-99", "0:10 am", "0:10:04 am", "00:10", "00:10:04", "12/30/99 00:10", "0 ", "0 ", "0.01 ", "0.01 ", "0.007", "0.007", "0.007", "0.007", "10:04", "0:10:04", "0.007", "7.00E-03", "0.007"}, - {"2.1", "2", "2.10", "2", "2.10", "210%", "210.00%", "2.10E+00", "2.1", "2.1", "01-01-00", "1-Jan-00", "1-Jan", "Jan-00", "2:24 am", "2:24:00 am", "02:24", "02:24:00", "1/1/00 02:24", "2 ", "2 ", "2.10 ", "2.10 ", "2.1", "2.1", "2.1", "2.1", "24:00", "50:24:00", "2.1", "2.10E+00", "2.1"}, + {"37947.7500001", "37948", "37947.75", "37,948", "37,947.75", "3794775%", "3794775.00%", "3.79E+04", "37947.7500001", "37947.7500001", "11-22-03", "22-Nov-03", "22-Nov", "Nov-03", "6:00 pm", "6:00:00 pm", "18:00", "18:00:00", "11/22/03 18:00", "37,948 ", "37,948 ", "37,947.75 ", "37,947.75 ", "37947.7500001", "37947.7500001", "37947.7500001", "37947.7500001", "00:00", "910746:00:00", "0000.0", "37947.7500001", "37947.7500001"}, + {"-37947.7500001", "-37948", "-37947.75", "-37,948", "-37,947.75", "-3794775%", "-3794775.00%", "-3.79E+04", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "(37,948)", "(37,948)", "(37,947.75)", "(37,947.75)", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001"}, + {"0.007", "0", "0.01", "0", "0.01", "1%", "0.70%", "7.00E-03", "0.007", "0.007", "12-30-99", "30-Dec-99", "30-Dec", "Dec-99", "0:10 am", "0:10:04 am", "00:10", "00:10:04", "12/30/99 00:10", "0 ", "0 ", "0.01 ", "0.01 ", "0.007", "0.007", "0.007", "0.007", "10:04", "0:10:04", "1004.0", "0.007", "0.007"}, + {"2.1", "2", "2.10", "2", "2.10", "210%", "210.00%", "2.10E+00", "2.1", "2.1", "01-01-00", "1-Jan-00", "1-Jan", "Jan-00", "2:24 am", "2:24:00 am", "02:24", "02:24:00", "1/1/00 02:24", "2 ", "2 ", "2.10 ", "2.10 ", "2.1", "2.1", "2.1", "2.1", "24:00", "50:24:00", "2400.0", "2.1", "2.1"}, {"String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String"}, } - for i, v := range value { - for k, d := range data { - c := col[i] + strconv.Itoa(k+1) + for c, v := range value { + for r, idx := range idxTbl { + cell := col[c] + strconv.Itoa(r+1) var val float64 val, err = strconv.ParseFloat(v, 64) if err != nil { - assert.NoError(t, f.SetCellValue("Sheet2", c, v)) + assert.NoError(t, f.SetCellValue("Sheet2", cell, v)) } else { - assert.NoError(t, f.SetCellValue("Sheet2", c, val)) + assert.NoError(t, f.SetCellValue("Sheet2", cell, val)) } - style, err := f.NewStyle(&Style{Fill: Fill{Type: "gradient", Color: []string{"FFFFFF", "E0EBF5"}, Shading: 5}, NumFmt: d}) + style, err := f.NewStyle(&Style{Fill: Fill{Type: "gradient", Color: []string{"FFFFFF", "E0EBF5"}, Shading: 5}, NumFmt: idx}) if !assert.NoError(t, err) { t.FailNow() } - assert.NoError(t, f.SetCellStyle("Sheet2", c, c, style)) - cellValue, err := f.GetCellValue("Sheet2", c) - assert.Equal(t, expected[i][k], cellValue, fmt.Sprintf("Sheet2!%s value: %s, number format: %d", c, value[i], k)) + assert.NoError(t, f.SetCellStyle("Sheet2", cell, cell, style)) + cellValue, err := f.GetCellValue("Sheet2", cell) + assert.Equal(t, expected[c][r], cellValue, fmt.Sprintf("Sheet2!%s value: %s, number format: %s c: %d r: %d", cell, value[c], builtInNumFmt[idx], c, r)) assert.NoError(t, err) } } @@ -997,7 +997,7 @@ func TestConditionalFormat(t *testing.T) { f := NewFile() sheet1 := f.GetSheetName(0) - fillCells(f, sheet1, 10, 15) + assert.NoError(t, fillCells(f, sheet1, 10, 15)) var format1, format2, format3, format4 int var err error @@ -1612,15 +1612,16 @@ func prepareTestBook4() (*File, error) { return f, nil } -func fillCells(f *File, sheet string, colCount, rowCount int) { +func fillCells(f *File, sheet string, colCount, rowCount int) error { for col := 1; col <= colCount; col++ { for row := 1; row <= rowCount; row++ { cell, _ := CoordinatesToCellName(col, row) if err := f.SetCellStr(sheet, cell, cell); err != nil { - fmt.Println(err) + return err } } } + return nil } func BenchmarkOpenFile(b *testing.B) { diff --git a/go.mod b/go.mod index 12b024e54a..a266969a31 100644 --- a/go.mod +++ b/go.mod @@ -6,8 +6,8 @@ require ( github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 github.com/richardlehane/mscfb v1.0.4 github.com/stretchr/testify v1.8.0 - github.com/xuri/efp v0.0.0-20220603152613-6918739fd470 - github.com/xuri/nfp v0.0.0-20220409054826-5e722a1d9e22 + github.com/xuri/efp v0.0.0-20230422071738-01f4e37c47e9 + github.com/xuri/nfp v0.0.0-20230428090735-b50b0f0358f4 golang.org/x/crypto v0.8.0 golang.org/x/image v0.5.0 golang.org/x/net v0.9.0 diff --git a/go.sum b/go.sum index 3c5a9eb918..c57411bd7f 100644 --- a/go.sum +++ b/go.sum @@ -15,10 +15,10 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/xuri/efp v0.0.0-20220603152613-6918739fd470 h1:6932x8ltq1w4utjmfMPVj09jdMlkY0aiA6+Skbtl3/c= -github.com/xuri/efp v0.0.0-20220603152613-6918739fd470/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= -github.com/xuri/nfp v0.0.0-20220409054826-5e722a1d9e22 h1:OAmKAfT06//esDdpi/DZ8Qsdt4+M5+ltca05dA5bG2M= -github.com/xuri/nfp v0.0.0-20220409054826-5e722a1d9e22/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= +github.com/xuri/efp v0.0.0-20230422071738-01f4e37c47e9 h1:ge5g8vsTQclA5lXDi+PuiAFw5GMIlMHOB/5e1hsf96E= +github.com/xuri/efp v0.0.0-20230422071738-01f4e37c47e9/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= +github.com/xuri/nfp v0.0.0-20230428090735-b50b0f0358f4 h1:YoU/1S7L25dvNepEir3Fg2aU9iGmDyE4gWKoEswWXts= +github.com/xuri/nfp v0.0.0-20230428090735-b50b0f0358f4/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= diff --git a/numfmt.go b/numfmt.go index 29702302a4..5f5f180fe4 100644 --- a/numfmt.go +++ b/numfmt.go @@ -21,7 +21,8 @@ import ( "github.com/xuri/nfp" ) -// languageInfo defined the required fields of localization support for number format. +// languageInfo defined the required fields of localization support for number +// format. type languageInfo struct { apFmt string tags []string @@ -31,13 +32,16 @@ type languageInfo struct { // numberFormat directly maps the number format parser runtime required // fields. type numberFormat struct { - cellType CellType - section []nfp.Section - t time.Time - sectionIdx int - date1904, isNumeric, hours, seconds bool - number float64 - ap, localCode, result, value, valueSectionType string + cellType CellType + section []nfp.Section + t time.Time + sectionIdx int + date1904, isNumeric, hours, seconds bool + number float64 + ap, localCode, result, value, valueSectionType string + fracHolder, fracPadding, intHolder, intPadding, expBaseLen int + percent int + useCommaSep, usePointer, usePositive, useScientificNotation bool } var ( @@ -47,12 +51,33 @@ var ( nfp.TokenTypeColor, nfp.TokenTypeCurrencyLanguage, nfp.TokenTypeDateTimes, + nfp.TokenTypeDecimalPoint, nfp.TokenTypeElapsedDateTimes, + nfp.TokenTypeExponential, nfp.TokenTypeGeneral, + nfp.TokenTypeHashPlaceHolder, nfp.TokenTypeLiteral, + nfp.TokenTypePercent, nfp.TokenTypeTextPlaceHolder, + nfp.TokenTypeThousandsSeparator, nfp.TokenTypeZeroPlaceHolder, } + // supportedNumberTokenTypes list the supported number token types. + supportedNumberTokenTypes = []string{ + nfp.TokenTypeColor, + nfp.TokenTypeDecimalPoint, + nfp.TokenTypeHashPlaceHolder, + nfp.TokenTypeLiteral, + nfp.TokenTypePercent, + nfp.TokenTypeThousandsSeparator, + nfp.TokenTypeZeroPlaceHolder, + } + // supportedDateTimeTokenTypes list the supported date and time token types. + supportedDateTimeTokenTypes = []string{ + nfp.TokenTypeCurrencyLanguage, + nfp.TokenTypeDateTimes, + nfp.TokenTypeElapsedDateTimes, + } // supportedLanguageInfo directly maps the supported language ID and tags. supportedLanguageInfo = map[string]languageInfo{ "36": {tags: []string{"af"}, localMonth: localMonthsNameAfrikaans, apFmt: apFmtAfrikaans}, @@ -373,15 +398,172 @@ func format(value, numFmt string, date1904 bool, cellType CellType) string { return value } -// positiveHandler will be handling positive selection for a number format -// expression. -func (nf *numberFormat) positiveHandler() (result string) { +// getNumberPartLen returns the length of integer and fraction parts for the +// numeric. +func getNumberPartLen(n float64) (int, int) { + parts := strings.Split(strconv.FormatFloat(math.Abs(n), 'f', -1, 64), ".") + if len(parts) == 2 { + return len(parts[0]), len(parts[1]) + } + return len(parts[0]), 0 +} + +// getNumberFmtConf generate the number format padding and place holder +// configurations. +func (nf *numberFormat) getNumberFmtConf() { + for _, token := range nf.section[nf.sectionIdx].Items { + if token.TType == nfp.TokenTypeHashPlaceHolder { + if nf.usePointer { + nf.fracHolder += len(token.TValue) + } else { + nf.intHolder += len(token.TValue) + } + } + if token.TType == nfp.TokenTypeExponential { + nf.useScientificNotation = true + } + if token.TType == nfp.TokenTypeThousandsSeparator { + nf.useCommaSep = true + } + if token.TType == nfp.TokenTypePercent { + nf.percent += len(token.TValue) + } + if token.TType == nfp.TokenTypeDecimalPoint { + nf.usePointer = true + } + if token.TType == nfp.TokenTypeZeroPlaceHolder { + if nf.usePointer { + if nf.useScientificNotation { + nf.expBaseLen += len(token.TValue) + continue + } + nf.fracPadding += len(token.TValue) + continue + } + nf.intPadding += len(token.TValue) + } + } +} + +// printNumberLiteral apply literal tokens for the pre-formatted text. +func (nf *numberFormat) printNumberLiteral(text string) string { + var result string + var useZeroPlaceHolder bool + if nf.usePositive { + result += "-" + } + for _, token := range nf.section[nf.sectionIdx].Items { + if token.TType == nfp.TokenTypeLiteral { + result += token.TValue + } + if !useZeroPlaceHolder && token.TType == nfp.TokenTypeZeroPlaceHolder { + useZeroPlaceHolder = true + result += text + } + } + return result +} + +// printCommaSep format number with thousands separator. +func printCommaSep(text string) string { + var ( + target strings.Builder + subStr = strings.Split(text, ".") + length = len(subStr[0]) + ) + for i := 0; i < length; i++ { + if i > 0 && (length-i)%3 == 0 { + target.WriteString(",") + } + target.WriteString(string(text[i])) + } + if len(subStr) == 2 { + target.WriteString(".") + target.WriteString(subStr[1]) + } + return target.String() +} + +// printBigNumber format number which precision great than 15 with fraction +// zero padding and percentage symbol. +func (nf *numberFormat) printBigNumber(decimal float64, fracLen int) string { + var exp float64 + if nf.percent > 0 { + exp = 1 + } + result := strings.TrimLeft(strconv.FormatFloat(decimal*math.Pow(100, exp), 'f', -1, 64), "-") + if nf.useCommaSep { + result = printCommaSep(result) + } + if fracLen > 0 { + if parts := strings.Split(result, "."); len(parts) == 2 { + fracPartLen := len(parts[1]) + if fracPartLen < fracLen { + result = fmt.Sprintf("%s%s", result, strings.Repeat("0", fracLen-fracPartLen)) + } + if fracPartLen > fracLen { + result = fmt.Sprintf("%s.%s", parts[0], parts[1][:fracLen]) + } + } else { + result = fmt.Sprintf("%s.%s", result, strings.Repeat("0", fracLen)) + } + } + if nf.percent > 0 { + return fmt.Sprintf("%s%%", result) + } + return result +} + +// numberHandler handling number format expression for positive and negative +// numeric. +func (nf *numberFormat) numberHandler() string { + var ( + num = nf.number + intPart, fracPart = getNumberPartLen(nf.number) + intLen, fracLen int + result string + ) + nf.getNumberFmtConf() + if intLen = intPart; nf.intPadding > intPart { + intLen = nf.intPadding + } + if fracLen = fracPart; fracPart > nf.fracHolder+nf.fracPadding { + fracLen = nf.fracHolder + nf.fracPadding + } + if nf.fracPadding > fracPart { + fracLen = nf.fracPadding + } + if isNum, precision, decimal := isNumeric(nf.value); isNum { + if precision > 15 && intLen+fracLen > 15 { + return nf.printNumberLiteral(nf.printBigNumber(decimal, fracLen)) + } + } + paddingLen := intLen + fracLen + if fracLen > 0 { + paddingLen++ + } + flag := "f" + if nf.useScientificNotation { + if nf.expBaseLen != 2 { + return nf.value + } + flag = "E" + } + fmtCode := fmt.Sprintf("%%0%d.%d%s%s", paddingLen, fracLen, flag, strings.Repeat("%%", nf.percent)) + if nf.percent > 0 { + num *= math.Pow(100, float64(nf.percent)) + } + if result = fmt.Sprintf(fmtCode, math.Abs(num)); nf.useCommaSep { + result = printCommaSep(result) + } + return nf.printNumberLiteral(result) +} + +// dateTimeHandler handling data and time number format expression for a +// positive numeric. +func (nf *numberFormat) dateTimeHandler() (result string) { nf.t, nf.hours, nf.seconds = timeFromExcelTime(nf.number, nf.date1904), false, false for i, token := range nf.section[nf.sectionIdx].Items { - if inStrSlice(supportedTokenTypes, token.TType, true) == -1 || token.TType == nfp.TokenTypeGeneral { - result = nf.value - return - } if token.TType == nfp.TokenTypeCurrencyLanguage { if err := nf.currencyLanguageHandler(i, token); err != nil { result = nf.value @@ -398,27 +580,46 @@ func (nf *numberFormat) positiveHandler() (result string) { nf.result += token.TValue continue } - if token.TType == nfp.TokenTypeZeroPlaceHolder && token.TValue == strings.Repeat("0", len(token.TValue)) { - if isNum, precision, decimal := isNumeric(nf.value); isNum { - if nf.number < 1 { - nf.result += "0" - continue - } - if precision > 15 { - nf.result += strconv.FormatFloat(decimal, 'f', -1, 64) - } else { - nf.result += fmt.Sprintf("%.f", nf.number) - } - continue + if token.TType == nfp.TokenTypeDecimalPoint { + nf.result += "." + } + if token.TType == nfp.TokenTypeZeroPlaceHolder { + zeroHolderLen := len(token.TValue) + if zeroHolderLen > 3 { + zeroHolderLen = 3 } + nf.result += strings.Repeat("0", zeroHolderLen) } } - result = nf.result - return + return nf.result } -// currencyLanguageHandler will be handling currency and language types tokens for a number -// format expression. +// positiveHandler will be handling positive selection for a number format +// expression. +func (nf *numberFormat) positiveHandler() string { + var fmtNum bool + for _, token := range nf.section[nf.sectionIdx].Items { + if inStrSlice(supportedTokenTypes, token.TType, true) == -1 || token.TType == nfp.TokenTypeGeneral { + return nf.value + } + if inStrSlice(supportedNumberTokenTypes, token.TType, true) != -1 { + fmtNum = true + } + if inStrSlice(supportedDateTimeTokenTypes, token.TType, true) != -1 { + if fmtNum || nf.number < 0 { + return nf.value + } + return nf.dateTimeHandler() + } + } + if fmtNum { + return nf.numberHandler() + } + return nf.value +} + +// 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) { for _, part := range token.Parts { if inStrSlice(supportedTokenTypes, part.Token.TType, true) == -1 { @@ -566,7 +767,8 @@ func localMonthsNameKorean(t time.Time, abbr int) string { return strconv.Itoa(int(t.Month())) } -// localMonthsNameTraditionalMongolian returns the Traditional Mongolian name of the month. +// localMonthsNameTraditionalMongolian returns the Traditional Mongolian name of +// the month. func localMonthsNameTraditionalMongolian(t time.Time, abbr int) string { if abbr == 5 { return "M" @@ -912,32 +1114,23 @@ func (nf *numberFormat) secondsNext(i int) bool { // negativeHandler will be handling negative selection for a number format // expression. func (nf *numberFormat) negativeHandler() (result string) { + fmtNum := true for _, token := range nf.section[nf.sectionIdx].Items { if inStrSlice(supportedTokenTypes, token.TType, true) == -1 || token.TType == nfp.TokenTypeGeneral { - result = nf.value - return + return nf.value } - if token.TType == nfp.TokenTypeLiteral { - nf.result += token.TValue + if inStrSlice(supportedNumberTokenTypes, token.TType, true) != -1 { continue } - if token.TType == nfp.TokenTypeZeroPlaceHolder && token.TValue == strings.Repeat("0", len(token.TValue)) { - if isNum, precision, decimal := isNumeric(nf.value); isNum { - if math.Abs(nf.number) < 1 { - nf.result += "0" - continue - } - if precision > 15 { - nf.result += strings.TrimLeft(strconv.FormatFloat(decimal, 'f', -1, 64), "-") - } else { - nf.result += fmt.Sprintf("%.f", math.Abs(nf.number)) - } - continue - } + if inStrSlice(supportedDateTimeTokenTypes, token.TType, true) != -1 { + return nf.value } + fmtNum = false } - result = nf.result - return + if fmtNum { + return nf.numberHandler() + } + return nf.value } // zeroHandler will be handling zero selection for a number format expression. @@ -973,6 +1166,16 @@ func (nf *numberFormat) getValueSectionType(value string) (float64, string) { return number, nfp.TokenSectionPositive } if number < 0 { + var hasNeg bool + for _, sec := range nf.section { + if sec.Type == nfp.TokenSectionNegative { + hasNeg = true + } + } + if !hasNeg { + nf.usePositive = true + return number, nfp.TokenSectionPositive + } return number, nfp.TokenSectionNegative } return number, nfp.TokenSectionZero diff --git a/numfmt_test.go b/numfmt_test.go index 1e6f6bb86a..bf5cbd280f 100644 --- a/numfmt_test.go +++ b/numfmt_test.go @@ -1004,6 +1004,40 @@ func TestNumFmt(t *testing.T) { {"-8.0450685976001E+21", "0_);[Red]\\(0\\)", "(8045068597600100000000)"}, {"-8.0450685976001E-21", "0_);[Red]\\(0\\)", "(0)"}, {"-8.04506", "0_);[Red]\\(0\\)", "(8)"}, + {"1234.5678", "0", "1235"}, + {"1234.5678", "0.00", "1234.57"}, + {"1234.5678", "#,##0", "1,235"}, + {"1234.5678", "#,##0.00", "1,234.57"}, + {"1234.5678", "0%", "123457%"}, + {"1234.5678", "#,##0 ;(#,##0)", "1,235 "}, + {"1234.5678", "#,##0 ;[red](#,##0)", "1,235 "}, + {"1234.5678", "#,##0.00;(#,##0.00)", "1,234.57"}, + {"1234.5678", "#,##0.00;[red](#,##0.00)", "1,234.57"}, + {"-1234.5678", "0.00", "-1234.57"}, + {"-1234.5678", "0.00;-0.00", "-1234.57"}, + {"-1234.5678", "0.00%%", "-12345678.00%%"}, + {"2.1", "mmss.0000", "2400.000"}, + {"1234.5678", "0.00###", "1234.5678"}, + {"1234.5678", "00000.00###", "01234.5678"}, + {"-1234.5678", "00000.00###;;", ""}, + {"1234.5678", "0.00000", "1234.56780"}, + {"8.8888666665555487", "0.00000", "8.88887"}, + {"8.8888666665555493e+19", "#,000.00", "88,888,666,665,555,500,000.00"}, + {"8.8888666665555493e+19", "0.00000", "88888666665555500000.00000"}, + {"37947.7500001", "0.00000000E+00", "3.79477500E+04"}, + {"1.234E-16", "0.00000000000000000000", "0.00000000000000012340"}, + {"1.234E-16", "0.000000000000000000", "0.000000000000000123"}, + {"1.234E-16", "0.000000000000000000%", "0.000000000000012340%"}, + {"1.234E-16", "0.000000000000000000%%%%", "0.000000000000012340%"}, + // Unsupported number format + {"37947.7500001", "0.00000000E+000", "37947.7500001"}, + // Invalid number format + {"123", "x0.00s", "123"}, + {"-123", "x0.00s", "-123"}, + {"-1234.5678", ";E+;", "-1234.5678"}, + {"1234.5678", "E+;", "1234.5678"}, + {"1234.5678", "00000.00###s", "1234.5678"}, + {"-1234.5678", "00000.00###;s;", "-1234.5678"}, } { result := format(item[0], item[1], false, CellTypeNumber) assert.Equal(t, item[2], result, item) diff --git a/rows_test.go b/rows_test.go index 48a2735e57..4a91ab9945 100644 --- a/rows_test.go +++ b/rows_test.go @@ -1114,7 +1114,7 @@ func TestNumberFormats(t *testing.T) { assert.NoError(t, f.SetCellValue("Sheet1", cell, value)) result, err := f.GetCellValue("Sheet1", cell) assert.NoError(t, err) - assert.Equal(t, expected, result) + assert.Equal(t, expected, result, cell) } assert.NoError(t, f.SaveAs(filepath.Join("test", "TestNumberFormats.xlsx"))) } diff --git a/styles.go b/styles.go index db7b5609fc..cb1210e165 100644 --- a/styles.go +++ b/styles.go @@ -34,7 +34,7 @@ var builtInNumFmt = map[int]string{ 4: "#,##0.00", 9: "0%", 10: "0.00%", - 11: "0.00e+00", + 11: "0.00E+00", 12: "# ?/?", 13: "# ??/??", 14: "mm-dd-yy", @@ -48,8 +48,8 @@ var builtInNumFmt = map[int]string{ 22: "m/d/yy hh:mm", 37: "#,##0 ;(#,##0)", 38: "#,##0 ;[red](#,##0)", - 39: "#,##0.00;(#,##0.00)", - 40: "#,##0.00;[red](#,##0.00)", + 39: "#,##0.00 ;(#,##0.00)", + 40: "#,##0.00 ;[red](#,##0.00)", 41: `_(* #,##0_);_(* \(#,##0\);_(* "-"_);_(@_)`, 42: `_("$"* #,##0_);_("$"* \(#,##0\);_("$"* "-"_);_(@_)`, 43: `_(* #,##0.00_);_(* \(#,##0.00\);_(* "-"??_);_(@_)`, @@ -57,7 +57,7 @@ var builtInNumFmt = map[int]string{ 45: "mm:ss", 46: "[h]:mm:ss", 47: "mmss.0", - 48: "##0.0e+0", + 48: "##0.0E+0", 49: "@", } @@ -751,43 +751,6 @@ var currencyNumFmt = map[int]string{ 634: "[$ZWR]\\ #,##0.00", } -// builtInNumFmtFunc defined the format conversion functions map. Partial format -// code doesn't support currently and will return original string. -var builtInNumFmtFunc = map[int]func(v, format string, date1904 bool, cellType CellType) string{ - 0: format, - 1: formatToInt, - 2: formatToFloat, - 3: formatToIntSeparator, - 4: formatToFloat, - 9: formatToC, - 10: formatToD, - 11: formatToE, - 12: format, // Doesn't support currently - 13: format, // Doesn't support currently - 14: format, - 15: format, - 16: format, - 17: format, - 18: format, - 19: format, - 20: format, - 21: format, - 22: format, - 37: formatToA, - 38: formatToA, - 39: formatToB, - 40: formatToB, - 41: format, // Doesn't support currently - 42: format, // Doesn't support currently - 43: format, // Doesn't support currently - 44: format, // Doesn't support currently - 45: format, - 46: format, - 47: format, - 48: formatToE, - 49: format, -} - // validType defined the list of valid validation types. var validType = map[string]string{ "cell": "cellIs", @@ -869,172 +832,6 @@ var operatorType = map[string]string{ "greaterThanOrEqual": "greater than or equal to", } -// printCommaSep format number with thousands separator. -func printCommaSep(text string) string { - var ( - target strings.Builder - subStr = strings.Split(text, ".") - length = len(subStr[0]) - ) - for i := 0; i < length; i++ { - if i > 0 && (length-i)%3 == 0 { - target.WriteString(",") - } - target.WriteString(string(text[i])) - } - if len(subStr) == 2 { - target.WriteString(".") - target.WriteString(subStr[1]) - } - return target.String() -} - -// formatToInt provides a function to convert original string to integer -// format as string type by given built-in number formats code and cell -// string. -func formatToInt(v, format string, date1904 bool, cellType CellType) string { - if strings.Contains(v, "_") || cellType == CellTypeSharedString || cellType == CellTypeInlineString { - return v - } - f, err := strconv.ParseFloat(v, 64) - if err != nil { - return v - } - return strconv.FormatFloat(math.Round(f), 'f', -1, 64) -} - -// formatToFloat provides a function to convert original string to float -// format as string type by given built-in number formats code and cell -// string. -func formatToFloat(v, format string, date1904 bool, cellType CellType) string { - if strings.Contains(v, "_") || cellType == CellTypeSharedString || cellType == CellTypeInlineString { - return v - } - f, err := strconv.ParseFloat(v, 64) - if err != nil { - return v - } - source := strconv.FormatFloat(f, 'f', -1, 64) - if !strings.Contains(source, ".") { - return source + ".00" - } - return fmt.Sprintf("%.2f", f) -} - -// formatToIntSeparator provides a function to convert original string to -// integer format as string type by given built-in number formats code and cell -// string. -func formatToIntSeparator(v, format string, date1904 bool, cellType CellType) string { - if strings.Contains(v, "_") || cellType == CellTypeSharedString || cellType == CellTypeInlineString { - return v - } - f, err := strconv.ParseFloat(v, 64) - if err != nil { - return v - } - return printCommaSep(strconv.FormatFloat(math.Round(f), 'f', -1, 64)) -} - -// formatToA provides a function to convert original string to special format -// as string type by given built-in number formats code and cell string. -func formatToA(v, format string, date1904 bool, cellType CellType) string { - if strings.Contains(v, "_") || cellType == CellTypeSharedString || cellType == CellTypeInlineString { - return v - } - f, err := strconv.ParseFloat(v, 64) - if err != nil { - return v - } - var target strings.Builder - if f < 0 { - target.WriteString("(") - } - target.WriteString(printCommaSep(strconv.FormatFloat(math.Abs(math.Round(f)), 'f', -1, 64))) - if f < 0 { - target.WriteString(")") - } else { - target.WriteString(" ") - } - return target.String() -} - -// formatToB provides a function to convert original string to special format -// as string type by given built-in number formats code and cell string. -func formatToB(v, format string, date1904 bool, cellType CellType) string { - if strings.Contains(v, "_") || cellType == CellTypeSharedString || cellType == CellTypeInlineString { - return v - } - f, err := strconv.ParseFloat(v, 64) - if err != nil { - return v - } - var target strings.Builder - if f < 0 { - target.WriteString("(") - } - source := strconv.FormatFloat(math.Abs(f), 'f', -1, 64) - var text string - if !strings.Contains(source, ".") { - text = printCommaSep(source + ".00") - } else { - text = printCommaSep(fmt.Sprintf("%.2f", math.Abs(f))) - } - target.WriteString(text) - if f < 0 { - target.WriteString(")") - } else { - target.WriteString(" ") - } - return target.String() -} - -// formatToC provides a function to convert original string to special format -// as string type by given built-in number formats code and cell string. -func formatToC(v, format string, date1904 bool, cellType CellType) string { - if strings.Contains(v, "_") || cellType == CellTypeSharedString || cellType == CellTypeInlineString { - return v - } - f, err := strconv.ParseFloat(v, 64) - if err != nil { - return v - } - source := strconv.FormatFloat(f, 'f', -1, 64) - if !strings.Contains(source, ".") { - return source + "00%" - } - return fmt.Sprintf("%.f%%", f*100) -} - -// formatToD provides a function to convert original string to special format -// as string type by given built-in number formats code and cell string. -func formatToD(v, format string, date1904 bool, cellType CellType) string { - if strings.Contains(v, "_") || cellType == CellTypeSharedString || cellType == CellTypeInlineString { - return v - } - f, err := strconv.ParseFloat(v, 64) - if err != nil { - return v - } - source := strconv.FormatFloat(f, 'f', -1, 64) - if !strings.Contains(source, ".") { - return source + "00.00%" - } - return fmt.Sprintf("%.2f%%", f*100) -} - -// formatToE provides a function to convert original string to special format -// as string type by given built-in number formats code and cell string. -func formatToE(v, format string, date1904 bool, cellType CellType) string { - if strings.Contains(v, "_") || cellType == CellTypeSharedString || cellType == CellTypeInlineString { - return v - } - f, err := strconv.ParseFloat(v, 64) - if err != nil { - return v - } - return fmt.Sprintf("%.2E", f) -} - // stylesReader provides a function to get the pointer to the structure after // deserialization of xl/styles.xml. func (f *File) stylesReader() (*xlsxStyleSheet, error) {