Skip to content

Commit

Permalink
This closes qax-os#660, supports currency string, and switches argume…
Browse files Browse the repository at this point in the history
…nt for the number format code

- Support round millisecond for the date time
- Update built-in number formats mapping
- Update unit tests
- Upgrade dependencies package
  • Loading branch information
xuri committed May 4, 2023
1 parent 7c221cf commit bbdb83a
Show file tree
Hide file tree
Showing 8 changed files with 146 additions and 58 deletions.
4 changes: 2 additions & 2 deletions col_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand All @@ -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"))
Expand Down
6 changes: 3 additions & 3 deletions excelize_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
}

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
152 changes: 108 additions & 44 deletions numfmt.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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,
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
useZeroPlaceHolder = true
result += text
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.
Expand All @@ -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 {
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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
return nf.numberHandler()
}

// currencyLanguageHandler will be handling currency and language types tokens
Expand All @@ -626,11 +693,16 @@ func (nf *numberFormat) currencyLanguageHandler(i int, token nfp.Token) (err err
err = ErrUnsupportedNumberFormat
return
}
if _, ok := supportedLanguageInfo[strings.ToUpper(part.Token.TValue)]; !ok {
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
}
nf.localCode = strings.ToUpper(part.Token.TValue)
}
return
}
Expand Down Expand Up @@ -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:
nf.result += strconv.Itoa(nf.t.Second())
return
default:
nf.result += fmt.Sprintf("%02d", nf.t.Second())
return
}
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
}
nf.result += fmt.Sprintf("%02d", nf.t.Second())
}

// elapsedDateTimesHandler will be handling elapsed date and times types tokens
Expand Down Expand Up @@ -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
return nf.numberHandler()
}

// zeroHandler will be handling zero selection for a number format expression.
Expand Down
Loading

0 comments on commit bbdb83a

Please sign in to comment.