Ref #660, support placeholder, padding and rounds numbers by specified number format code

- Remove built-in number formats functions
- Update unit tests
- Upgrade dependencies package
This commit is contained in:
xuri 2023-04-30 11:10:51 +08:00
parent 65fc25e7a6
commit 7c221cf295
No known key found for this signature in database
GPG Key ID: BA5E5BB1C948EDF7
9 changed files with 321 additions and 290 deletions

View File

@ -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

View File

@ -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) {

View File

@ -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) {

4
go.mod
View File

@ -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

8
go.sum
View File

@ -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=

305
numfmt.go
View File

@ -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

View File

@ -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)

View File

@ -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")))
}

211
styles.go
View File

@ -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) {