This closes #660, supports currency string, and switches argument for the number format code

- Support round millisecond for the date time
- Update built-in number formats mapping
- Update unit tests
- Upgrade dependencies package
This commit is contained in:
xuri 2023-05-04 02:52:26 +00:00 committed by GitHub
parent 7c221cf295
commit bbdb83abf0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 146 additions and 58 deletions

View File

@ -407,7 +407,7 @@ func TestInsertCols(t *testing.T) {
f := NewFile()
sheet1 := f.GetSheetName(0)
fillCells(f, sheet1, 10, 10)
assert.NoError(t, fillCells(f, sheet1, 10, 10))
assert.NoError(t, f.SetCellHyperLink(sheet1, "A5", "https://github.com/xuri/excelize", "External"))
assert.NoError(t, f.MergeCell(sheet1, "A1", "C3"))
@ -430,7 +430,7 @@ func TestRemoveCol(t *testing.T) {
f := NewFile()
sheet1 := f.GetSheetName(0)
fillCells(f, sheet1, 10, 15)
assert.NoError(t, fillCells(f, sheet1, 10, 15))
assert.NoError(t, f.SetCellHyperLink(sheet1, "A5", "https://github.com/xuri/excelize", "External"))
assert.NoError(t, f.SetCellHyperLink(sheet1, "C5", "https://github.com", "External"))

View File

@ -750,10 +750,10 @@ func TestSetCellStyleNumberFormat(t *testing.T) {
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", "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", "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", "00:00.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"},
{"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:05 AM", "00:10", "00:10:05", "12/30/99 00:10", "0 ", "0 ", "0.01 ", "0.01 ", "0.007", "0.007", "0.007", "0.007", "10:05", "0:10:05", "10:04.8", "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", "24:00.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"},
}

2
go.mod
View File

@ -7,7 +7,7 @@ require (
github.com/richardlehane/mscfb v1.0.4
github.com/stretchr/testify v1.8.0
github.com/xuri/efp v0.0.0-20230422071738-01f4e37c47e9
github.com/xuri/nfp v0.0.0-20230428090735-b50b0f0358f4
github.com/xuri/nfp v0.0.0-20230503010013-3f38cdbb0b83
golang.org/x/crypto v0.8.0
golang.org/x/image v0.5.0
golang.org/x/net v0.9.0

4
go.sum
View File

@ -17,8 +17,8 @@ github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PK
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
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/xuri/nfp v0.0.0-20230503010013-3f38cdbb0b83 h1:xVwnvkzzi+OiwhIkWOXvh1skFI6bagk8OvGuazM80Rw=
github.com/xuri/nfp v0.0.0-20230503010013-3f38cdbb0b83/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=

132
numfmt.go
View File

@ -36,9 +36,10 @@ type numberFormat struct {
section []nfp.Section
t time.Time
sectionIdx int
date1904, isNumeric, hours, seconds bool
date1904, isNumeric, hours, seconds, useMillisecond bool
number float64
ap, localCode, result, value, valueSectionType string
switchArgument, currencyString string
fracHolder, fracPadding, intHolder, intPadding, expBaseLen int
percent int
useCommaSep, usePointer, usePositive, useScientificNotation bool
@ -47,6 +48,7 @@ type numberFormat struct {
var (
// supportedTokenTypes list the supported number format token types currently.
supportedTokenTypes = []string{
nfp.TokenSubTypeCurrencyString,
nfp.TokenSubTypeLanguageInfo,
nfp.TokenTypeColor,
nfp.TokenTypeCurrencyLanguage,
@ -58,23 +60,20 @@ var (
nfp.TokenTypeHashPlaceHolder,
nfp.TokenTypeLiteral,
nfp.TokenTypePercent,
nfp.TokenTypeSwitchArgument,
nfp.TokenTypeTextPlaceHolder,
nfp.TokenTypeThousandsSeparator,
nfp.TokenTypeZeroPlaceHolder,
}
// supportedNumberTokenTypes list the supported number token types.
supportedNumberTokenTypes = []string{
nfp.TokenTypeColor,
nfp.TokenTypeDecimalPoint,
nfp.TokenTypeExponential,
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,
}
@ -357,6 +356,30 @@ var (
apFmtYi = "\ua3b8\ua111/\ua06f\ua2d2"
// apFmtWelsh defined the AM/PM name in the Welsh.
apFmtWelsh = "yb/yh"
// switchArgumentFunc defined the switch argument printer function
switchArgumentFunc = map[string]func(s string) string{
"[DBNum1]": func(s string) string {
r := strings.NewReplacer(
"0", "\u25cb", "1", "\u4e00", "2", "\u4e8c", "3", "\u4e09", "4", "\u56db",
"5", "\u4e94", "6", "\u516d", "7", "\u4e03", "8", "\u516b", "9", "\u4e5d",
)
return r.Replace(s)
},
"[DBNum2]": func(s string) string {
r := strings.NewReplacer(
"0", "\u96f6", "1", "\u58f9", "2", "\u8d30", "3", "\u53c1", "4", "\u8086",
"5", "\u4f0d", "6", "\u9646", "7", "\u67d2", "8", "\u634c", "9", "\u7396",
)
return r.Replace(s)
},
"[DBNum3]": func(s string) string {
r := strings.NewReplacer(
"0", "\uff10", "1", "\uff11", "2", "\uff12", "3", "\uff13", "4", "\uff14",
"5", "\uff15", "6", "\uff16", "7", "\uff17", "8", "\uff18", "9", "\uff19",
)
return r.Replace(s)
},
}
)
// prepareNumberic split the number into two before and after parts by a
@ -431,6 +454,9 @@ func (nf *numberFormat) getNumberFmtConf() {
if token.TType == nfp.TokenTypeDecimalPoint {
nf.usePointer = true
}
if token.TType == nfp.TokenTypeSwitchArgument {
nf.switchArgument = token.TValue
}
if token.TType == nfp.TokenTypeZeroPlaceHolder {
if nf.usePointer {
if nf.useScientificNotation {
@ -448,20 +474,34 @@ func (nf *numberFormat) getNumberFmtConf() {
// printNumberLiteral apply literal tokens for the pre-formatted text.
func (nf *numberFormat) printNumberLiteral(text string) string {
var result string
var useZeroPlaceHolder bool
var useLiteral, useZeroPlaceHolder bool
if nf.usePositive {
result += "-"
}
for _, token := range nf.section[nf.sectionIdx].Items {
for i, token := range nf.section[nf.sectionIdx].Items {
if token.TType == nfp.TokenTypeCurrencyLanguage {
if err := nf.currencyLanguageHandler(i, token); err != nil {
return nf.value
}
result += nf.currencyString
}
if token.TType == nfp.TokenTypeLiteral {
if useZeroPlaceHolder {
useLiteral = true
}
result += token.TValue
}
if !useZeroPlaceHolder && token.TType == nfp.TokenTypeZeroPlaceHolder {
if token.TType == nfp.TokenTypeZeroPlaceHolder {
if useLiteral && useZeroPlaceHolder {
return nf.value
}
if !useZeroPlaceHolder {
useZeroPlaceHolder = true
result += text
}
}
return result
}
return nf.printSwitchArgument(result)
}
// printCommaSep format number with thousands separator.
@ -484,6 +524,17 @@ func printCommaSep(text string) string {
return target.String()
}
// printSwitchArgument format number with switch argument.
func (nf *numberFormat) printSwitchArgument(text string) string {
if nf.switchArgument == "" {
return text
}
if fn, ok := switchArgumentFunc[nf.switchArgument]; ok {
return fn(text)
}
return nf.value
}
// printBigNumber format number which precision great than 15 with fraction
// zero padding and percentage symbol.
func (nf *numberFormat) printBigNumber(decimal float64, fracLen int) string {
@ -561,14 +612,14 @@ func (nf *numberFormat) numberHandler() string {
// dateTimeHandler handling data and time number format expression for a
// positive numeric.
func (nf *numberFormat) dateTimeHandler() (result string) {
func (nf *numberFormat) dateTimeHandler() string {
nf.t, nf.hours, nf.seconds = timeFromExcelTime(nf.number, nf.date1904), false, false
for i, token := range nf.section[nf.sectionIdx].Items {
if token.TType == nfp.TokenTypeCurrencyLanguage {
if err := nf.currencyLanguageHandler(i, token); err != nil {
result = nf.value
return
return nf.value
}
nf.result += nf.currencyString
}
if token.TType == nfp.TokenTypeDateTimes {
nf.dateTimesHandler(i, token)
@ -583,15 +634,18 @@ func (nf *numberFormat) dateTimeHandler() (result string) {
if token.TType == nfp.TokenTypeDecimalPoint {
nf.result += "."
}
if token.TType == nfp.TokenTypeSwitchArgument {
nf.switchArgument = token.TValue
}
if token.TType == nfp.TokenTypeZeroPlaceHolder {
zeroHolderLen := len(token.TValue)
if zeroHolderLen > 3 {
zeroHolderLen = 3
}
nf.result += strings.Repeat("0", zeroHolderLen)
nf.result += fmt.Sprintf("%03d", nf.t.Nanosecond()/1e6)[:zeroHolderLen]
}
}
return nf.result
return nf.printSwitchArgument(nf.result)
}
// positiveHandler will be handling positive selection for a number format
@ -609,13 +663,26 @@ func (nf *numberFormat) positiveHandler() string {
if fmtNum || nf.number < 0 {
return nf.value
}
var useDateTimeTokens bool
for _, token := range nf.section[nf.sectionIdx].Items {
if inStrSlice(supportedDateTimeTokenTypes, token.TType, false) != -1 {
if useDateTimeTokens && nf.useMillisecond {
return nf.value
}
useDateTimeTokens = true
}
if inStrSlice(supportedNumberTokenTypes, token.TType, false) != -1 {
if token.TType == nfp.TokenTypeZeroPlaceHolder {
nf.useMillisecond = true
continue
}
return nf.value
}
}
return nf.dateTimeHandler()
}
}
if fmtNum {
return nf.numberHandler()
}
return nf.value
}
// currencyLanguageHandler will be handling currency and language types tokens
@ -626,12 +693,17 @@ func (nf *numberFormat) currencyLanguageHandler(i int, token nfp.Token) (err err
err = ErrUnsupportedNumberFormat
return
}
if part.Token.TType == nfp.TokenSubTypeLanguageInfo {
if _, ok := supportedLanguageInfo[strings.ToUpper(part.Token.TValue)]; !ok {
err = ErrUnsupportedNumberFormat
return
}
nf.localCode = strings.ToUpper(part.Token.TValue)
}
if part.Token.TType == nfp.TokenSubTypeCurrencyString {
nf.currencyString = part.Token.TValue
}
}
return
}
@ -1039,17 +1111,17 @@ func (nf *numberFormat) minutesHandler(token nfp.Token) {
// secondsHandler will be handling seconds in the date and times types tokens
// for a number format expression.
func (nf *numberFormat) secondsHandler(token nfp.Token) {
nf.seconds = strings.Contains(strings.ToUpper(token.TValue), "S")
if nf.seconds {
switch len(token.TValue) {
case 1:
if nf.seconds = strings.Contains(strings.ToUpper(token.TValue), "S"); !nf.seconds {
return
}
if !nf.useMillisecond {
nf.t = nf.t.Add(time.Duration(math.Round(float64(nf.t.Nanosecond())/1e9)) * time.Second)
}
if len(token.TValue) == 1 {
nf.result += strconv.Itoa(nf.t.Second())
return
default:
}
nf.result += fmt.Sprintf("%02d", nf.t.Second())
return
}
}
}
// elapsedDateTimesHandler will be handling elapsed date and times types tokens
@ -1114,23 +1186,15 @@ 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 {
return nf.value
}
if inStrSlice(supportedNumberTokenTypes, token.TType, true) != -1 {
continue
}
if inStrSlice(supportedDateTimeTokenTypes, token.TType, true) != -1 {
return nf.value
}
fmtNum = false
}
if fmtNum {
return nf.numberHandler()
}
return nf.value
}
// zeroHandler will be handling zero selection for a number format expression.

View File

@ -4,6 +4,7 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/xuri/nfp"
)
func TestNumFmt(t *testing.T) {
@ -67,7 +68,7 @@ func TestNumFmt(t *testing.T) {
{"43528", "[$-409]MM/DD/YYYY", "03/04/2019"},
{"43528", "[$-409]MM/DD/YYYY am/pm", "03/04/2019 AM"},
{"43528", "[$-111]MM/DD/YYYY", "43528"},
{"43528", "[$US-409]MM/DD/YYYY", "43528"},
{"43528", "[$US-409]MM/DD/YYYY", "US03/04/2019"},
{"43543.586539351854", "AM/PM h h:mm", "PM 14 2:04"},
{"text", "AM/PM h h:mm", "text"},
{"44562.189571759256", "[$-36]mmm dd yyyy h:mm AM/PM", "Jan. 01 2022 4:32 vm."},
@ -1017,6 +1018,18 @@ func TestNumFmt(t *testing.T) {
{"-1234.5678", "0.00;-0.00", "-1234.57"},
{"-1234.5678", "0.00%%", "-12345678.00%%"},
{"2.1", "mmss.0000", "2400.000"},
{"0.007", "[h]:mm:ss.0", "0:10:04.8"},
{"0.007", "[h]:mm:ss.00", "0:10:04.80"},
{"0.007", "[h]:mm:ss.000", "0:10:04.800"},
{"0.007", "[h]:mm:ss.0000", "0:10:04.800"},
{"123", "[h]:mm,:ss.0", "2952:00,:00.0"},
{"123", "yy-.dd", "00-.02"},
{"123", "[DBNum1][$-804]yyyy\"年\"m\"月\";@", "\u4e00\u4e5d\u25cb\u25cb\u5e74\u4e94\u6708"},
{"123", "[DBNum2][$-804]yyyy\"年\"m\"月\";@", "\u58f9\u7396\u96f6\u96f6\u5e74\u4f0d\u6708"},
{"123", "[DBNum3][$-804]yyyy\"年\"m\"月\";@", "\uff11\uff19\uff10\uff10\u5e74\uff15\u6708"},
{"1234567890", "[DBNum1][$-804]0.00", "\u4e00\u4e8c\u4e09\u56db\u4e94\u516d\u4e03\u516b\u4e5d\u25cb.\u25cb\u25cb"},
{"1234567890", "[DBNum2][$-804]0.00", "\u58f9\u8d30\u53c1\u8086\u4f0d\u9646\u67d2\u634c\u7396\u96f6.\u96f6\u96f6"},
{"1234567890", "[DBNum3][$-804]0.00", "\uff11\uff12\uff13\uff14\uff15\uff16\uff17\uff18\uff19\uff10.\uff10\uff10"},
{"1234.5678", "0.00###", "1234.5678"},
{"1234.5678", "00000.00###", "01234.5678"},
{"-1234.5678", "00000.00###;;", ""},
@ -1029,14 +1042,23 @@ func TestNumFmt(t *testing.T) {
{"1.234E-16", "0.000000000000000000", "0.000000000000000123"},
{"1.234E-16", "0.000000000000000000%", "0.000000000000012340%"},
{"1.234E-16", "0.000000000000000000%%%%", "0.000000000000012340%"},
{"1234.5678", "[$$-409]#,##0.00", "$1,234.57"},
// Unsupported number format
{"37947.7500001", "0.00000000E+000", "37947.7500001"},
{"123", "[$kr.-46F]#,##0.00", "123"},
{"123", "[$kr.-46F]MM/DD/YYYY", "123"},
{"123", "[DBNum4][$-804]yyyy\"年\"m\"月\";@", "123"},
// Invalid number format
{"123", "x0.00s", "123"},
{"123", "[h]:m00m:ss", "123"},
{"123", "yy-00dd", "123"},
{"123", "yy-##dd", "123"},
{"123", "xx[h]:mm,:ss.0xx", "xx2952:00,:00.0xx"},
{"-123", "x0.00s", "-123"},
{"-1234.5678", ";E+;", "-1234.5678"},
{"1234.5678", "E+;", "1234.5678"},
{"1234.5678", "00000.00###s", "1234.5678"},
{"1234.5678", "0.0xxx00", "1234.5678"},
{"-1234.5678", "00000.00###;s;", "-1234.5678"},
} {
result := format(item[0], item[1], false, CellTypeNumber)
@ -1055,4 +1077,6 @@ func TestNumFmt(t *testing.T) {
assert.Equal(t, item[2], result, item)
}
}
nf := numberFormat{}
assert.Equal(t, ErrUnsupportedNumberFormat, nf.currencyLanguageHandler(0, nfp.Token{Parts: []nfp.Part{{}}}))
}

View File

@ -305,7 +305,7 @@ func TestRemoveRow(t *testing.T) {
colCount = 10
rowCount = 10
)
fillCells(f, sheet1, colCount, rowCount)
assert.NoError(t, fillCells(f, sheet1, colCount, rowCount))
assert.NoError(t, f.SetCellHyperLink(sheet1, "A5", "https://github.com/xuri/excelize", "External"))
@ -368,7 +368,7 @@ func TestInsertRows(t *testing.T) {
colCount = 10
rowCount = 10
)
fillCells(f, sheet1, colCount, rowCount)
assert.NoError(t, fillCells(f, sheet1, colCount, rowCount))
assert.NoError(t, f.SetCellHyperLink(sheet1, "A5", "https://github.com/xuri/excelize", "External"))

View File

@ -41,8 +41,8 @@ var builtInNumFmt = map[int]string{
15: "d-mmm-yy",
16: "d-mmm",
17: "mmm-yy",
18: "h:mm am/pm",
19: "h:mm:ss am/pm",
18: "h:mm AM/PM",
19: "h:mm:ss AM/PM",
20: "hh:mm",
21: "hh:mm:ss",
22: "m/d/yy hh:mm",
@ -56,7 +56,7 @@ var builtInNumFmt = map[int]string{
44: `_("$"* #,##0.00_);_("$"* \(#,##0.00\);_("$"* "-"??_);_(@_)`,
45: "mm:ss",
46: "[h]:mm:ss",
47: "mmss.0",
47: "mm:ss.0",
48: "##0.0E+0",
49: "@",
}