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 { if wb != nil && wb.WorkbookPr != nil {
date1904 = wb.WorkbookPr.Date1904 date1904 = wb.WorkbookPr.Date1904
} }
if ok := builtInNumFmtFunc[numFmtID]; ok != nil { if fmtCode, ok := builtInNumFmt[numFmtID]; ok {
return ok(c.V, builtInNumFmt[numFmtID], date1904, cellType), err return format(c.V, fmtCode, date1904, cellType), err
} }
if styleSheet.NumFmts == nil { if styleSheet.NumFmts == nil {
return c.V, err return c.V, err

View File

@ -873,9 +873,7 @@ func TestFormattedValue(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "311", result) assert.Equal(t, "311", result)
for _, fn := range builtInNumFmtFunc { assert.Equal(t, "0_0", format("0_0", "", false, CellTypeNumber))
assert.Equal(t, "0_0", fn("0_0", "", false, CellTypeNumber))
}
// Test format value with unsupported charset workbook // Test format value with unsupported charset workbook
f.WorkBook = nil f.WorkBook = nil
@ -889,9 +887,7 @@ func TestFormattedValue(t *testing.T) {
_, err = f.formattedValue(&xlsxC{S: 1, V: "43528"}, false, CellTypeNumber) _, err = f.formattedValue(&xlsxC{S: 1, V: "43528"}, false, CellTypeNumber)
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
for _, fn := range builtInNumFmtFunc { assert.Equal(t, "text", format("text", "0", false, CellTypeNumber))
assert.Equal(t, fn("text", "0", false, CellTypeNumber), "text")
}
} }
func TestFormattedValueNilXfs(t *testing.T) { 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 // Test only set fill and number format for a cell
col := []string{"L", "M", "N", "O", "P"} 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"} value := []string{"37947.7500001", "-37947.7500001", "0.007", "2.1", "String"}
expected := [][]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", "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", "-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"}, {"-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", "0.007", "7.00E-03", "0.007"}, {"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", "2.1", "2.10E+00", "2.1"}, {"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"}, {"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 c, v := range value {
for k, d := range data { for r, idx := range idxTbl {
c := col[i] + strconv.Itoa(k+1) cell := col[c] + strconv.Itoa(r+1)
var val float64 var val float64
val, err = strconv.ParseFloat(v, 64) val, err = strconv.ParseFloat(v, 64)
if err != nil { if err != nil {
assert.NoError(t, f.SetCellValue("Sheet2", c, v)) assert.NoError(t, f.SetCellValue("Sheet2", cell, v))
} else { } 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) { if !assert.NoError(t, err) {
t.FailNow() t.FailNow()
} }
assert.NoError(t, f.SetCellStyle("Sheet2", c, c, style)) assert.NoError(t, f.SetCellStyle("Sheet2", cell, cell, style))
cellValue, err := f.GetCellValue("Sheet2", c) cellValue, err := f.GetCellValue("Sheet2", cell)
assert.Equal(t, expected[i][k], cellValue, fmt.Sprintf("Sheet2!%s value: %s, number format: %d", c, value[i], k)) 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) assert.NoError(t, err)
} }
} }
@ -997,7 +997,7 @@ func TestConditionalFormat(t *testing.T) {
f := NewFile() f := NewFile()
sheet1 := f.GetSheetName(0) sheet1 := f.GetSheetName(0)
fillCells(f, sheet1, 10, 15) assert.NoError(t, fillCells(f, sheet1, 10, 15))
var format1, format2, format3, format4 int var format1, format2, format3, format4 int
var err error var err error
@ -1612,15 +1612,16 @@ func prepareTestBook4() (*File, error) {
return f, nil 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 col := 1; col <= colCount; col++ {
for row := 1; row <= rowCount; row++ { for row := 1; row <= rowCount; row++ {
cell, _ := CoordinatesToCellName(col, row) cell, _ := CoordinatesToCellName(col, row)
if err := f.SetCellStr(sheet, cell, cell); err != nil { if err := f.SetCellStr(sheet, cell, cell); err != nil {
fmt.Println(err) return err
} }
} }
} }
return nil
} }
func BenchmarkOpenFile(b *testing.B) { 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/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826
github.com/richardlehane/mscfb v1.0.4 github.com/richardlehane/mscfb v1.0.4
github.com/stretchr/testify v1.8.0 github.com/stretchr/testify v1.8.0
github.com/xuri/efp v0.0.0-20220603152613-6918739fd470 github.com/xuri/efp v0.0.0-20230422071738-01f4e37c47e9
github.com/xuri/nfp v0.0.0-20220409054826-5e722a1d9e22 github.com/xuri/nfp v0.0.0-20230428090735-b50b0f0358f4
golang.org/x/crypto v0.8.0 golang.org/x/crypto v0.8.0
golang.org/x/image v0.5.0 golang.org/x/image v0.5.0
golang.org/x/net v0.9.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.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 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 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-20230422071738-01f4e37c47e9 h1:ge5g8vsTQclA5lXDi+PuiAFw5GMIlMHOB/5e1hsf96E=
github.com/xuri/efp v0.0.0-20220603152613-6918739fd470/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= github.com/xuri/efp v0.0.0-20230422071738-01f4e37c47e9/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-20230428090735-b50b0f0358f4 h1:YoU/1S7L25dvNepEir3Fg2aU9iGmDyE4gWKoEswWXts=
github.com/xuri/nfp v0.0.0-20220409054826-5e722a1d9e22/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= 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= 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-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 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" "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 { type languageInfo struct {
apFmt string apFmt string
tags []string tags []string
@ -31,13 +32,16 @@ type languageInfo struct {
// numberFormat directly maps the number format parser runtime required // numberFormat directly maps the number format parser runtime required
// fields. // fields.
type numberFormat struct { type numberFormat struct {
cellType CellType cellType CellType
section []nfp.Section section []nfp.Section
t time.Time t time.Time
sectionIdx int sectionIdx int
date1904, isNumeric, hours, seconds bool date1904, isNumeric, hours, seconds bool
number float64 number float64
ap, localCode, result, value, valueSectionType string ap, localCode, result, value, valueSectionType string
fracHolder, fracPadding, intHolder, intPadding, expBaseLen int
percent int
useCommaSep, usePointer, usePositive, useScientificNotation bool
} }
var ( var (
@ -47,12 +51,33 @@ var (
nfp.TokenTypeColor, nfp.TokenTypeColor,
nfp.TokenTypeCurrencyLanguage, nfp.TokenTypeCurrencyLanguage,
nfp.TokenTypeDateTimes, nfp.TokenTypeDateTimes,
nfp.TokenTypeDecimalPoint,
nfp.TokenTypeElapsedDateTimes, nfp.TokenTypeElapsedDateTimes,
nfp.TokenTypeExponential,
nfp.TokenTypeGeneral, nfp.TokenTypeGeneral,
nfp.TokenTypeHashPlaceHolder,
nfp.TokenTypeLiteral, nfp.TokenTypeLiteral,
nfp.TokenTypePercent,
nfp.TokenTypeTextPlaceHolder, nfp.TokenTypeTextPlaceHolder,
nfp.TokenTypeThousandsSeparator,
nfp.TokenTypeZeroPlaceHolder, 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 directly maps the supported language ID and tags.
supportedLanguageInfo = map[string]languageInfo{ supportedLanguageInfo = map[string]languageInfo{
"36": {tags: []string{"af"}, localMonth: localMonthsNameAfrikaans, apFmt: apFmtAfrikaans}, "36": {tags: []string{"af"}, localMonth: localMonthsNameAfrikaans, apFmt: apFmtAfrikaans},
@ -373,15 +398,172 @@ func format(value, numFmt string, date1904 bool, cellType CellType) string {
return value return value
} }
// positiveHandler will be handling positive selection for a number format // getNumberPartLen returns the length of integer and fraction parts for the
// expression. // numeric.
func (nf *numberFormat) positiveHandler() (result string) { 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 nf.t, nf.hours, nf.seconds = timeFromExcelTime(nf.number, nf.date1904), false, false
for i, token := range nf.section[nf.sectionIdx].Items { 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 token.TType == nfp.TokenTypeCurrencyLanguage {
if err := nf.currencyLanguageHandler(i, token); err != nil { if err := nf.currencyLanguageHandler(i, token); err != nil {
result = nf.value result = nf.value
@ -398,27 +580,46 @@ func (nf *numberFormat) positiveHandler() (result string) {
nf.result += token.TValue nf.result += token.TValue
continue continue
} }
if token.TType == nfp.TokenTypeZeroPlaceHolder && token.TValue == strings.Repeat("0", len(token.TValue)) { if token.TType == nfp.TokenTypeDecimalPoint {
if isNum, precision, decimal := isNumeric(nf.value); isNum { nf.result += "."
if nf.number < 1 { }
nf.result += "0" if token.TType == nfp.TokenTypeZeroPlaceHolder {
continue zeroHolderLen := len(token.TValue)
} if zeroHolderLen > 3 {
if precision > 15 { zeroHolderLen = 3
nf.result += strconv.FormatFloat(decimal, 'f', -1, 64)
} else {
nf.result += fmt.Sprintf("%.f", nf.number)
}
continue
} }
nf.result += strings.Repeat("0", zeroHolderLen)
} }
} }
result = nf.result return nf.result
return
} }
// currencyLanguageHandler will be handling currency and language types tokens for a number // positiveHandler will be handling positive selection for a number format
// format expression. // 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) { func (nf *numberFormat) currencyLanguageHandler(i int, token nfp.Token) (err error) {
for _, part := range token.Parts { for _, part := range token.Parts {
if inStrSlice(supportedTokenTypes, part.Token.TType, true) == -1 { 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())) 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 { func localMonthsNameTraditionalMongolian(t time.Time, abbr int) string {
if abbr == 5 { if abbr == 5 {
return "M" return "M"
@ -912,32 +1114,23 @@ func (nf *numberFormat) secondsNext(i int) bool {
// negativeHandler will be handling negative selection for a number format // negativeHandler will be handling negative selection for a number format
// expression. // expression.
func (nf *numberFormat) negativeHandler() (result string) { func (nf *numberFormat) negativeHandler() (result string) {
fmtNum := true
for _, token := range nf.section[nf.sectionIdx].Items { for _, token := range nf.section[nf.sectionIdx].Items {
if inStrSlice(supportedTokenTypes, token.TType, true) == -1 || token.TType == nfp.TokenTypeGeneral { if inStrSlice(supportedTokenTypes, token.TType, true) == -1 || token.TType == nfp.TokenTypeGeneral {
result = nf.value return nf.value
return
} }
if token.TType == nfp.TokenTypeLiteral { if inStrSlice(supportedNumberTokenTypes, token.TType, true) != -1 {
nf.result += token.TValue
continue continue
} }
if token.TType == nfp.TokenTypeZeroPlaceHolder && token.TValue == strings.Repeat("0", len(token.TValue)) { if inStrSlice(supportedDateTimeTokenTypes, token.TType, true) != -1 {
if isNum, precision, decimal := isNumeric(nf.value); isNum { return nf.value
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
}
} }
fmtNum = false
} }
result = nf.result if fmtNum {
return return nf.numberHandler()
}
return nf.value
} }
// zeroHandler will be handling zero selection for a number format expression. // 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 return number, nfp.TokenSectionPositive
} }
if number < 0 { 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.TokenSectionNegative
} }
return number, nfp.TokenSectionZero 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\\)", "(8045068597600100000000)"},
{"-8.0450685976001E-21", "0_);[Red]\\(0\\)", "(0)"}, {"-8.0450685976001E-21", "0_);[Red]\\(0\\)", "(0)"},
{"-8.04506", "0_);[Red]\\(0\\)", "(8)"}, {"-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) result := format(item[0], item[1], false, CellTypeNumber)
assert.Equal(t, item[2], result, item) 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)) assert.NoError(t, f.SetCellValue("Sheet1", cell, value))
result, err := f.GetCellValue("Sheet1", cell) result, err := f.GetCellValue("Sheet1", cell)
assert.NoError(t, err) 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"))) 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", 4: "#,##0.00",
9: "0%", 9: "0%",
10: "0.00%", 10: "0.00%",
11: "0.00e+00", 11: "0.00E+00",
12: "# ?/?", 12: "# ?/?",
13: "# ??/??", 13: "# ??/??",
14: "mm-dd-yy", 14: "mm-dd-yy",
@ -48,8 +48,8 @@ var builtInNumFmt = map[int]string{
22: "m/d/yy hh:mm", 22: "m/d/yy hh:mm",
37: "#,##0 ;(#,##0)", 37: "#,##0 ;(#,##0)",
38: "#,##0 ;[red](#,##0)", 38: "#,##0 ;[red](#,##0)",
39: "#,##0.00;(#,##0.00)", 39: "#,##0.00 ;(#,##0.00)",
40: "#,##0.00;[red](#,##0.00)", 40: "#,##0.00 ;[red](#,##0.00)",
41: `_(* #,##0_);_(* \(#,##0\);_(* "-"_);_(@_)`, 41: `_(* #,##0_);_(* \(#,##0\);_(* "-"_);_(@_)`,
42: `_("$"* #,##0_);_("$"* \(#,##0\);_("$"* "-"_);_(@_)`, 42: `_("$"* #,##0_);_("$"* \(#,##0\);_("$"* "-"_);_(@_)`,
43: `_(* #,##0.00_);_(* \(#,##0.00\);_(* "-"??_);_(@_)`, 43: `_(* #,##0.00_);_(* \(#,##0.00\);_(* "-"??_);_(@_)`,
@ -57,7 +57,7 @@ var builtInNumFmt = map[int]string{
45: "mm:ss", 45: "mm:ss",
46: "[h]:mm:ss", 46: "[h]:mm:ss",
47: "mmss.0", 47: "mmss.0",
48: "##0.0e+0", 48: "##0.0E+0",
49: "@", 49: "@",
} }
@ -751,43 +751,6 @@ var currencyNumFmt = map[int]string{
634: "[$ZWR]\\ #,##0.00", 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. // validType defined the list of valid validation types.
var validType = map[string]string{ var validType = map[string]string{
"cell": "cellIs", "cell": "cellIs",
@ -869,172 +832,6 @@ var operatorType = map[string]string{
"greaterThanOrEqual": "greater than or equal to", "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 // stylesReader provides a function to get the pointer to the structure after
// deserialization of xl/styles.xml. // deserialization of xl/styles.xml.
func (f *File) stylesReader() (*xlsxStyleSheet, error) { func (f *File) stylesReader() (*xlsxStyleSheet, error) {