summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cell.go11
-rw-r--r--comment.go2
-rw-r--r--errors.go3
-rw-r--r--go.mod9
-rw-r--r--go.sum20
-rw-r--r--lib.go13
-rw-r--r--lib_test.go2
-rw-r--r--numfmt.go356
-rw-r--r--numfmt_test.go76
-rw-r--r--pivotTable.go2
-rw-r--r--styles.go185
-rw-r--r--styles_test.go20
12 files changed, 484 insertions, 215 deletions
diff --git a/cell.go b/cell.go
index 9af93f6..b5b6ed4 100644
--- a/cell.go
+++ b/cell.go
@@ -1116,21 +1116,12 @@ func (f *File) formattedValue(s int, v string, raw bool) string {
}
for _, xlsxFmt := range styleSheet.NumFmts.NumFmt {
if xlsxFmt.NumFmtID == numFmtID {
- format := strings.ToLower(xlsxFmt.FormatCode)
- if isTimeNumFmt(format) {
- return parseTime(v, format)
- }
- return precise
+ return format(v, xlsxFmt.FormatCode)
}
}
return precise
}
-// isTimeNumFmt determine if the given number format expression is a time number format.
-func isTimeNumFmt(format string) bool {
- return strings.Contains(format, "y") || strings.Contains(format, "m") || strings.Contains(strings.Replace(format, "red", "", -1), "d") || strings.Contains(format, "h")
-}
-
// prepareCellStyle provides a function to prepare style index of cell in
// worksheet by given column index and style index.
func (f *File) prepareCellStyle(ws *xlsxWorksheet, col, row, style int) int {
diff --git a/comment.go b/comment.go
index c0dc33b..f3b3642 100644
--- a/comment.go
+++ b/comment.go
@@ -256,7 +256,7 @@ func (f *File) addComment(commentsXML, cell string, formatSet *formatComment) {
if comments == nil {
comments = &xlsxComments{Authors: xlsxAuthor{Author: []string{formatSet.Author}}}
}
- if inStrSlice(comments.Authors.Author, formatSet.Author) == -1 {
+ if inStrSlice(comments.Authors.Author, formatSet.Author, true) == -1 {
comments.Authors.Author = append(comments.Authors.Author, formatSet.Author)
authorID = len(comments.Authors.Author) - 1
}
diff --git a/errors.go b/errors.go
index ebbcef6..f0a3405 100644
--- a/errors.go
+++ b/errors.go
@@ -123,6 +123,9 @@ var (
// ErrUnsupportedHashAlgorithm defined the error message on unsupported
// hash algorithm.
ErrUnsupportedHashAlgorithm = errors.New("unsupported hash algorithm")
+ // ErrUnsupportedNumberFormat defined the error message on unsupported number format
+ // expression.
+ ErrUnsupportedNumberFormat = errors.New("unsupported number format token")
// ErrPasswordLengthInvalid defined the error message on invalid password
// length.
ErrPasswordLengthInvalid = errors.New("password length invalid")
diff --git a/go.mod b/go.mod
index b7aa1ba..9d6e88d 100644
--- a/go.mod
+++ b/go.mod
@@ -4,11 +4,12 @@ go 1.15
require (
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826
- github.com/richardlehane/mscfb v1.0.3
+ github.com/richardlehane/mscfb v1.0.4
github.com/stretchr/testify v1.7.0
- github.com/xuri/efp v0.0.0-20210322160811-ab561f5b45e3
- golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3
+ github.com/xuri/efp v0.0.0-20220201101309-d64cf20d930d
+ github.com/xuri/nfp v0.0.0-20220210053112-1df76b07693e
+ golang.org/x/crypto v0.0.0-20220210151621-f4118a5b28e2
golang.org/x/image v0.0.0-20211028202545-6944b10bf410
- golang.org/x/net v0.0.0-20220107192237-5cfca573fb4d
+ golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd
golang.org/x/text v0.3.7
)
diff --git a/go.sum b/go.sum
index 7fa8255..efd0f63 100644
--- a/go.sum
+++ b/go.sum
@@ -4,26 +4,30 @@ github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/richardlehane/mscfb v1.0.3 h1:rD8TBkYWkObWO0oLDFCbwMeZ4KoalxQy+QgniCj3nKI=
-github.com/richardlehane/mscfb v1.0.3/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk=
+github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM=
+github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk=
github.com/richardlehane/msoleps v1.0.1 h1:RfrALnSNXzmXLbGct/P2b4xkFz4e8Gmj/0Vj9M9xC1o=
github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/xuri/efp v0.0.0-20210322160811-ab561f5b45e3 h1:EpI0bqf/eX9SdZDwlMmahKM+CDBgNbsXMhsN28XrM8o=
-github.com/xuri/efp v0.0.0-20210322160811-ab561f5b45e3/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
-golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 h1:0es+/5331RGQPcXlMfP+WrnIIS6dNnNRe0WB02W0F4M=
-golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+github.com/xuri/efp v0.0.0-20220201101309-d64cf20d930d h1:zFggKNM0CSDVuK4Gzd7RNw5hFCHOETKZ7Nb5MHw+bCE=
+github.com/xuri/efp v0.0.0-20220201101309-d64cf20d930d/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
+github.com/xuri/nfp v0.0.0-20220210053112-1df76b07693e h1:8Bg6HoC/EdUGR3Y9Vx12XoD/RfMta06hFamKO+NK7Bc=
+github.com/xuri/nfp v0.0.0-20220210053112-1df76b07693e/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
+golang.org/x/crypto v0.0.0-20220210151621-f4118a5b28e2 h1:XdAboW3BNMv9ocSCOk/u1MFioZGzCNkiJZ19v9Oe3Ig=
+golang.org/x/crypto v0.0.0-20220210151621-f4118a5b28e2/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/image v0.0.0-20211028202545-6944b10bf410 h1:hTftEOvwiOq2+O8k2D5/Q7COC7k5Qcrgc2TFURJYnvQ=
golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.0.0-20220107192237-5cfca573fb4d h1:62NvYBuaanGXR2ZOfwDFkhhl6X1DUgf8qg3GuQvxZsE=
-golang.org/x/net v0.0.0-20220107192237-5cfca573fb4d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd h1:O7DYs+zxREGLKzKoMQrtrEacpb0ZVXA5rIwylE2Xchk=
+golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
diff --git a/lib.go b/lib.go
index a435452..1bfdda2 100644
--- a/lib.go
+++ b/lib.go
@@ -376,8 +376,11 @@ func inCoordinates(a [][]int, x []int) int {
// inStrSlice provides a method to check if an element is present in an array,
// and return the index of its location, otherwise return -1.
-func inStrSlice(a []string, x string) int {
+func inStrSlice(a []string, x string, caseSensitive bool) int {
for idx, n := range a {
+ if !caseSensitive && strings.EqualFold(x, n) {
+ return idx
+ }
if x == n {
return idx
}
@@ -658,7 +661,7 @@ func (f *File) addNameSpaces(path string, ns xml.Attr) {
// by the given attribute.
func (f *File) setIgnorableNameSpace(path string, index int, ns xml.Attr) {
ignorableNS := []string{"c14", "cdr14", "a14", "pic14", "x14", "xdr14", "x14ac", "dsp", "mso14", "dgm14", "x15", "x12ac", "x15ac", "xr", "xr2", "xr3", "xr4", "xr5", "xr6", "xr7", "xr8", "xr9", "xr10", "xr11", "xr12", "xr13", "xr14", "xr15", "x15", "x16", "x16r2", "mo", "mx", "mv", "o", "v"}
- if inStrSlice(strings.Fields(f.xmlAttr[path][index].Value), ns.Name.Local) == -1 && inStrSlice(ignorableNS, ns.Name.Local) != -1 {
+ if inStrSlice(strings.Fields(f.xmlAttr[path][index].Value), ns.Name.Local, true) == -1 && inStrSlice(ignorableNS, ns.Name.Local, true) != -1 {
f.xmlAttr[path][index].Value = strings.TrimSpace(fmt.Sprintf("%s %s", f.xmlAttr[path][index].Value, ns.Name.Local))
}
}
@@ -672,8 +675,7 @@ func (f *File) addSheetNameSpace(sheet string, ns xml.Attr) {
// isNumeric determines whether an expression is a valid numeric type and get
// the precision for the numeric.
func isNumeric(s string) (bool, int) {
- dot := false
- p := 0
+ dot, n, p := false, false, 0
for i, v := range s {
if v == '.' {
if dot {
@@ -686,10 +688,11 @@ func isNumeric(s string) (bool, int) {
}
return false, 0
} else if dot {
+ n = true
p++
}
}
- return true, p
+ return n, p
}
var (
diff --git a/lib_test.go b/lib_test.go
index da75dee..1e2f324 100644
--- a/lib_test.go
+++ b/lib_test.go
@@ -234,7 +234,7 @@ func TestSortCoordinates(t *testing.T) {
}
func TestInStrSlice(t *testing.T) {
- assert.EqualValues(t, -1, inStrSlice([]string{}, ""))
+ assert.EqualValues(t, -1, inStrSlice([]string{}, "", true))
}
func TestBoolValMarshal(t *testing.T) {
diff --git a/numfmt.go b/numfmt.go
new file mode 100644
index 0000000..a724405
--- /dev/null
+++ b/numfmt.go
@@ -0,0 +1,356 @@
+// Copyright 2016 - 2022 The excelize Authors. All rights reserved. Use of
+// this source code is governed by a BSD-style license that can be found in
+// the LICENSE file.
+//
+// Package excelize providing a set of functions that allow you to write to
+// and read from XLSX / XLSM / XLTM files. Supports reading and writing
+// spreadsheet documents generated by Microsoft Excelâ„¢ 2007 and later. Supports
+// complex components by high compatibility, and provided streaming API for
+// generating or reading data from a worksheet with huge amounts of data. This
+// library needs Go version 1.15 or later.
+
+package excelize
+
+import (
+ "fmt"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/xuri/nfp"
+)
+
+// supportedTokenTypes list the supported number format token types currently.
+var supportedTokenTypes = []string{
+ nfp.TokenTypeCurrencyLanguage,
+ nfp.TokenTypeDateTimes,
+ nfp.TokenTypeElapsedDateTimes,
+ nfp.TokenTypeGeneral,
+ nfp.TokenTypeLiteral,
+ nfp.TokenSubTypeLanguageInfo,
+}
+
+// numberFormat directly maps the number format parser runtime required
+// fields.
+type numberFormat struct {
+ section []nfp.Section
+ t time.Time
+ sectionIdx int
+ isNumberic, hours, seconds bool
+ number float64
+ ap, afterPoint, beforePoint, localCode, result, value, valueSectionType string
+}
+
+// prepareNumberic split the number into two before and after parts by a
+// decimal point.
+func (nf *numberFormat) prepareNumberic(value string) {
+ prec := 0
+ if nf.isNumberic, prec = isNumeric(value); !nf.isNumberic {
+ return
+ }
+ nf.beforePoint, nf.afterPoint = value[:len(value)-prec-1], value[len(value)-prec:]
+}
+
+// format provides a function to return a string parse by number format
+// expression. If the given number format is not supported, this will return
+// the original cell value.
+func format(value, numFmt string) string {
+ p := nfp.NumberFormatParser()
+ nf := numberFormat{section: p.Parse(numFmt), value: value}
+ nf.number, nf.valueSectionType = nf.getValueSectionType(value)
+ nf.prepareNumberic(value)
+ for i, section := range nf.section {
+ nf.sectionIdx = i
+ if section.Type != nf.valueSectionType {
+ continue
+ }
+ switch section.Type {
+ case nfp.TokenSectionPositive:
+ return nf.positiveHandler()
+ case nfp.TokenSectionNegative:
+ return nf.negativeHandler()
+ case nfp.TokenSectionZero:
+ return nf.zeroHandler()
+ default:
+ return nf.textHandler()
+ }
+ }
+ return value
+}
+
+// positiveHandler will be handling positive selection for a number format
+// expression.
+func (nf *numberFormat) positiveHandler() (result string) {
+ nf.t, nf.hours, nf.seconds = timeFromExcelTime(nf.number, false), false, false
+ for i, token := range nf.section[nf.sectionIdx].Items {
+ if inStrSlice(supportedTokenTypes, token.TType, true) == -1 || token.TType == nfp.TokenTypeGeneral {
+ result = fmt.Sprint(nf.number)
+ return
+ }
+ if token.TType == nfp.TokenTypeCurrencyLanguage {
+ if err := nf.currencyLanguageHandler(i, token); err != nil {
+ result = fmt.Sprint(nf.number)
+ return
+ }
+ }
+ if token.TType == nfp.TokenTypeDateTimes {
+ nf.dateTimesHandler(i, token)
+ }
+ if token.TType == nfp.TokenTypeElapsedDateTimes {
+ nf.elapsedDateTimesHandler(token)
+ }
+ if token.TType == nfp.TokenTypeLiteral {
+ nf.result += token.TValue
+ continue
+ }
+ }
+ result = nf.result
+ return
+}
+
+// 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 {
+ err = ErrUnsupportedNumberFormat
+ return
+ }
+ if nf.localCode = part.Token.TValue; nf.localCode != "409" {
+ err = ErrUnsupportedNumberFormat
+ return
+ }
+ }
+ return
+}
+
+// dateTimesHandler will be handling date and times types tokens for a number
+// format expression.
+func (nf *numberFormat) dateTimesHandler(i int, token nfp.Token) {
+ if idx := inStrSlice(nfp.AmPm, strings.ToUpper(token.TValue), false); idx != -1 {
+ if nf.ap == "" {
+ nextHours := nf.hoursNext(i)
+ aps := strings.Split(token.TValue, "/")
+ nf.ap = aps[0]
+ if nextHours > 12 {
+ nf.ap = aps[1]
+ }
+ }
+ nf.result += nf.ap
+ return
+ }
+ if strings.Contains(strings.ToUpper(token.TValue), "M") {
+ l := len(token.TValue)
+ if l == 1 && !nf.hours && !nf.secondsNext(i) {
+ nf.result += strconv.Itoa(int(nf.t.Month()))
+ return
+ }
+ if l == 2 && !nf.hours && !nf.secondsNext(i) {
+ nf.result += fmt.Sprintf("%02d", int(nf.t.Month()))
+ return
+ }
+ if l == 3 {
+ nf.result += nf.t.Month().String()[:3]
+ return
+ }
+ if l == 4 || l > 5 {
+ nf.result += nf.t.Month().String()
+ return
+ }
+ if l == 5 {
+ nf.result += nf.t.Month().String()[:1]
+ return
+ }
+ }
+ nf.yearsHandler(i, token)
+ nf.daysHandler(i, token)
+ nf.hoursHandler(i, token)
+ nf.minutesHandler(token)
+ nf.secondsHandler(token)
+}
+
+// yearsHandler will be handling years in the date and times types tokens for a
+// number format expression.
+func (nf *numberFormat) yearsHandler(i int, token nfp.Token) {
+ years := strings.Contains(strings.ToUpper(token.TValue), "Y")
+ if years && len(token.TValue) <= 2 {
+ nf.result += strconv.Itoa(nf.t.Year())[2:]
+ return
+ }
+ if years && len(token.TValue) > 2 {
+ nf.result += strconv.Itoa(nf.t.Year())
+ return
+ }
+}
+
+// daysHandler will be handling days in the date and times types tokens for a
+// number format expression.
+func (nf *numberFormat) daysHandler(i int, token nfp.Token) {
+ if strings.Contains(strings.ToUpper(token.TValue), "D") {
+ switch len(token.TValue) {
+ case 1:
+ nf.result += strconv.Itoa(nf.t.Day())
+ return
+ case 2:
+ nf.result += fmt.Sprintf("%02d", nf.t.Day())
+ return
+ case 3:
+ nf.result += nf.t.Weekday().String()[:3]
+ return
+ default:
+ nf.result += nf.t.Weekday().String()
+ return
+ }
+ }
+}
+
+// hoursHandler will be handling hours in the date and times types tokens for a
+// number format expression.
+func (nf *numberFormat) hoursHandler(i int, token nfp.Token) {
+ nf.hours = strings.Contains(strings.ToUpper(token.TValue), "H")
+ if nf.hours {
+ h := nf.t.Hour()
+ ap, ok := nf.apNext(i)
+ if ok {
+ nf.ap = ap[0]
+ if h > 12 {
+ h -= 12
+ nf.ap = ap[1]
+ }
+ }
+ if nf.ap != "" && nf.hoursNext(i) == -1 && h > 12 {
+ h -= 12
+ }
+ switch len(token.TValue) {
+ case 1:
+ nf.result += strconv.Itoa(h)
+ return
+ default:
+ nf.result += fmt.Sprintf("%02d", h)
+ return
+ }
+ }
+}
+
+// minutesHandler will be handling minutes in the date and times types tokens
+// for a number format expression.
+func (nf *numberFormat) minutesHandler(token nfp.Token) {
+ if strings.Contains(strings.ToUpper(token.TValue), "M") {
+ nf.hours = false
+ switch len(token.TValue) {
+ case 1:
+ nf.result += strconv.Itoa(nf.t.Minute())
+ return
+ default:
+ nf.result += fmt.Sprintf("%02d", nf.t.Minute())
+ return
+ }
+ }
+}
+
+// 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
+ }
+ }
+}
+
+// elapsedDateTimesHandler will be handling elapsed date and times types tokens
+// for a number format expression.
+func (nf *numberFormat) elapsedDateTimesHandler(token nfp.Token) {
+ if strings.Contains(strings.ToUpper(token.TValue), "H") {
+ nf.result += fmt.Sprintf("%.f", nf.t.Sub(excel1900Epoc).Hours())
+ return
+ }
+ if strings.Contains(strings.ToUpper(token.TValue), "M") {
+ nf.result += fmt.Sprintf("%.f", nf.t.Sub(excel1900Epoc).Minutes())
+ return
+ }
+ if strings.Contains(strings.ToUpper(token.TValue), "S") {
+ nf.result += fmt.Sprintf("%.f", nf.t.Sub(excel1900Epoc).Seconds())
+ return
+ }
+}
+
+// hoursNext detects if a token of type hours exists after a given tokens list.
+func (nf *numberFormat) hoursNext(i int) int {
+ tokens := nf.section[nf.sectionIdx].Items
+ for idx := i + 1; idx < len(tokens); idx++ {
+ if tokens[idx].TType == nfp.TokenTypeDateTimes {
+ if strings.Contains(strings.ToUpper(tokens[idx].TValue), "H") {
+ t := timeFromExcelTime(nf.number, false)
+ return t.Hour()
+ }
+ }
+ }
+ return -1
+}
+
+// apNext detects if a token of type AM/PM exists after a given tokens list.
+func (nf *numberFormat) apNext(i int) ([]string, bool) {
+ tokens := nf.section[nf.sectionIdx].Items
+ for idx := i + 1; idx < len(tokens); idx++ {
+ if tokens[idx].TType == nfp.TokenTypeDateTimes {
+ if strings.Contains(strings.ToUpper(tokens[idx].TValue), "H") {
+ return nil, false
+ }
+ if i := inStrSlice(nfp.AmPm, tokens[idx].TValue, false); i != -1 {
+ return strings.Split(tokens[idx].TValue, "/"), true
+ }
+ }
+ }
+ return nil, false
+}
+
+// secondsNext detects if a token of type seconds exists after a given tokens
+// list.
+func (nf *numberFormat) secondsNext(i int) bool {
+ tokens := nf.section[nf.sectionIdx].Items
+ for idx := i + 1; idx < len(tokens); idx++ {
+ if tokens[idx].TType == nfp.TokenTypeDateTimes {
+ return strings.Contains(strings.ToUpper(tokens[idx].TValue), "S")
+ }
+ }
+ return false
+}
+
+// negativeHandler will be handling negative selection for a number format
+// expression.
+func (nf *numberFormat) negativeHandler() string {
+ return fmt.Sprint(nf.number)
+}
+
+// zeroHandler will be handling zero selection for a number format expression.
+func (nf *numberFormat) zeroHandler() string {
+ return fmt.Sprint(nf.number)
+}
+
+// textHandler will be handling text selection for a number format expression.
+func (nf *numberFormat) textHandler() string {
+ return fmt.Sprint(nf.value)
+}
+
+// getValueSectionType returns its applicable number format expression section
+// based on the given value.
+func (nf *numberFormat) getValueSectionType(value string) (float64, string) {
+ number, err := strconv.ParseFloat(value, 64)
+ if err != nil {
+ return number, nfp.TokenSectionText
+ }
+ if number > 0 {
+ return number, nfp.TokenSectionPositive
+ }
+ if number < 0 {
+ return number, nfp.TokenSectionNegative
+ }
+ return number, nfp.TokenSectionZero
+}
diff --git a/numfmt_test.go b/numfmt_test.go
new file mode 100644
index 0000000..b64287b
--- /dev/null
+++ b/numfmt_test.go
@@ -0,0 +1,76 @@
+package excelize
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestNumFmt(t *testing.T) {
+ for _, item := range [][]string{
+ {"123", "general", "123"},
+ {"43528", "y", "19"},
+ {"43528", "Y", "19"},
+ {"43528", "yy", "19"},
+ {"43528", "YY", "19"},
+ {"43528", "yyy", "2019"},
+ {"43528", "YYY", "2019"},
+ {"43528", "yyyy", "2019"},
+ {"43528", "YYYY", "2019"},
+ {"43528", "yyyyy", "2019"},
+ {"43528", "YYYYY", "2019"},
+ {"43528", "m", "3"},
+ {"43528", "mm", "03"},
+ {"43528", "mmm", "Mar"},
+ {"43528", "mmmm", "March"},
+ {"43528", "mmmmm", "M"},
+ {"43528", "mmmmmm", "March"},
+ {"43528", "d", "4"},
+ {"43528", "dd", "04"},
+ {"43528", "ddd", "Mon"},
+ {"43528", "dddd", "Monday"},
+ {"43528", "h", "0"},
+ {"43528", "hh", "00"},
+ {"43528", "hhh", "00"},
+ {"43543.544872685183", "hhmm", "1304"},
+ {"43543.544872685183", "mmhhmmmm", "0313March"},
+ {"43543.544872685183", "mm hh mm mm", "03 13 04 03"},
+ {"43543.544872685183", "mm hh m m", "03 13 4 3"},
+ {"43543.544872685183", "m s", "4 37"},
+ {"43528", "[h]", "1044672"},
+ {"43528", "[m]", "62680320"},
+ {"43528", "s", "0"},
+ {"43528", "ss", "00"},
+ {"43528", "[s]", "3760819200"},
+ {"43543.544872685183", "h:mm:ss AM/PM", "1:04:37 PM"},
+ {"43543.544872685183", "AM/PM h:mm:ss", "PM 1:04:37"},
+ {"43543.086539351854", "hh:mm:ss AM/PM", "02:04:37 AM"},
+ {"43543.086539351854", "AM/PM hh:mm:ss", "AM 02:04:37"},
+ {"43543.086539351854", "AM/PM hh:mm:ss a/p", "AM 02:04:37 a"},
+ {"43528", "YYYY", "2019"},
+ {"43528", "", "43528"},
+ {"43528.2123", "YYYY-MM-DD hh:mm:ss", "2019-03-04 05:05:42"},
+ {"43528.2123", "YYYY-MM-DD hh:mm:ss;YYYY-MM-DD hh:mm:ss", "2019-03-04 05:05:42"},
+ {"43528.2123", "M/D/YYYY h:m:s", "3/4/2019 5:5:42"},
+ {"43528.003958333335", "m/d/yyyy h:m:s", "3/4/2019 0:5:42"},
+ {"43528.003958333335", "M/D/YYYY h:mm:s", "3/4/2019 0:05:42"},
+ {"0.64583333333333337", "h:mm:ss am/pm", "3:30:00 pm"},
+ {"43528.003958333335", "h:mm", "0:05"},
+ {"6.9444444444444444E-5", "h:m", "0:0"},
+ {"6.9444444444444444E-5", "h:mm", "0:00"},
+ {"6.9444444444444444E-5", "h:m", "0:0"},
+ {"0.50070601851851848", "h:m", "12:1"},
+ {"0.97952546296296295", "h:m", "23:30"},
+ {"43528", "mmmm", "March"},
+ {"43528", "dddd", "Monday"},
+ {"0", ";;;", "0"},
+ {"43528", "[$-409]MM/DD/YYYY", "03/04/2019"},
+ {"43528", "[$-111]MM/DD/YYYY", "43528"},
+ {"43528", "[$US-409]MM/DD/YYYY", "43528"},
+ {"43543.586539351854", "AM/PM h h:mm", "PM 14 2:04"},
+ {"text", "AM/PM h h:mm", "text"},
+ } {
+ result := format(item[0], item[1])
+ assert.Equal(t, item[2], result, item)
+ }
+}
diff --git a/pivotTable.go b/pivotTable.go
index d30eeb1..d7e9c94 100644
--- a/pivotTable.go
+++ b/pivotTable.go
@@ -632,7 +632,7 @@ func (f *File) getPivotFieldsIndex(fields []PivotTableField, opt *PivotTableOpti
return pivotFieldsIndex, err
}
for _, field := range fields {
- if pos := inStrSlice(orders, field.Data); pos != -1 {
+ if pos := inStrSlice(orders, field.Data, true); pos != -1 {
pivotFieldsIndex = append(pivotFieldsIndex, pos)
}
}
diff --git a/styles.go b/styles.go
index 7678b84..6dea20e 100644
--- a/styles.go
+++ b/styles.go
@@ -20,7 +20,6 @@ import (
"log"
"math"
"reflect"
- "regexp"
"strconv"
"strings"
)
@@ -756,7 +755,7 @@ var currencyNumFmt = map[int]string{
// 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 string, format string) string{
- 0: formatToString,
+ 0: format,
1: formatToInt,
2: formatToFloat,
3: formatToInt,
@@ -764,30 +763,30 @@ var builtInNumFmtFunc = map[int]func(v string, format string) string{
9: formatToC,
10: formatToD,
11: formatToE,
- 12: formatToString, // Doesn't support currently
- 13: formatToString, // Doesn't support currently
- 14: parseTime,
- 15: parseTime,
- 16: parseTime,
- 17: parseTime,
- 18: parseTime,
- 19: parseTime,
- 20: parseTime,
- 21: parseTime,
- 22: parseTime,
+ 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: formatToString, // Doesn't support currently
- 42: formatToString, // Doesn't support currently
- 43: formatToString, // Doesn't support currently
- 44: formatToString, // Doesn't support currently
- 45: parseTime,
- 46: parseTime,
- 47: parseTime,
+ 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: formatToString,
+ 49: format,
}
// validType defined the list of valid validation types.
@@ -845,12 +844,6 @@ var criteriaType = map[string]string{
"continue month": "continueMonth",
}
-// formatToString provides a function to return original string by given
-// built-in number formats code and cell string.
-func formatToString(v string, format string) string {
- return v
-}
-
// formatToInt provides a function to convert original string to integer
// format as string type by given built-in number formats code and cell
// string.
@@ -933,144 +926,6 @@ func formatToE(v string, format string) string {
return fmt.Sprintf("%.2E", f)
}
-// parseTime provides a function to returns a string parsed using time.Time.
-// Replace Excel placeholders with Go time placeholders. For example, replace
-// yyyy with 2006. These are in a specific order, due to the fact that m is
-// used in month, minute, and am/pm. It would be easier to fix that with
-// regular expressions, but if it's possible to keep this simple it would be
-// easier to maintain. Full-length month and days (e.g. March, Tuesday) have
-// letters in them that would be replaced by other characters below (such as
-// the 'h' in March, or the 'd' in Tuesday) below. First we convert them to
-// arbitrary characters unused in Excel Date formats, and then at the end,
-// turn them to what they should actually be. Based off:
-// http://www.ozgrid.com/Excel/CustomFormats.htm
-func parseTime(v string, format string) string {
- var (
- f float64
- err error
- goFmt string
- )
- f, err = strconv.ParseFloat(v, 64)
- if err != nil {
- return v
- }
- val := timeFromExcelTime(f, false)
-
- if format == "" {
- return v
- }
-
- goFmt = format
-
- if strings.Contains(goFmt, "[") {
- re := regexp.MustCompile(`\[.+\]`)
- goFmt = re.ReplaceAllLiteralString(goFmt, "")
- }
-
- // use only first variant
- if strings.Contains(goFmt, ";") {
- goFmt = goFmt[:strings.IndexByte(goFmt, ';')]
- }
-
- replacements := []struct{ xltime, gotime string }{
- {"YYYY", "2006"},
- {"YY", "06"},
- {"MM", "01"},
- {"M", "1"},
- {"DD", "02"},
- {"D", "2"},
- {"yyyy", "2006"},
- {"yy", "06"},
- {"MMMM", "%%%%"},
- {"mmmm", "%%%%"},
- {"DDDD", "&&&&"},
- {"dddd", "&&&&"},
- {"DD", "02"},
- {"dd", "02"},
- {"D", "2"},
- {"d", "2"},
- {"MMM", "Jan"},
- {"mmm", "Jan"},
- {"MMSS", "0405"},
- {"mmss", "0405"},
- {"SS", "05"},
- {"ss", "05"},
- {"s", "5"},
- {"MM:", "04:"},
- {"mm:", "04:"},
- {":MM", ":04"},
- {":mm", ":04"},
- {"m:", "4:"},
- {":m", ":4"},
- {"MM", "01"},
- {"mm", "01"},
- {"AM/PM", "PM"},
- {"am/pm", "PM"},
- {"M/", "1/"},
- {"m/", "1/"},
- {"%%%%", "January"},
- {"&&&&", "Monday"},
- }
-
- replacementsGlobal := []struct{ xltime, gotime string }{
- {"\\-", "-"},
- {"\\ ", " "},
- {"\\.", "."},
- {"\\", ""},
- {"\"", ""},
- }
- // It is the presence of the "am/pm" indicator that determines if this is
- // a 12 hour or 24 hours time format, not the number of 'h' characters.
- var padding bool
- if val.Hour() == 0 && !strings.Contains(format, "hh") && !strings.Contains(format, "HH") {
- padding = true
- }
- if is12HourTime(format) {
- goFmt = strings.Replace(goFmt, "hh", "3", 1)
- goFmt = strings.Replace(goFmt, "h", "3", 1)
- goFmt = strings.Replace(goFmt, "HH", "3", 1)
- goFmt = strings.Replace(goFmt, "H", "3", 1)
- } else {
- goFmt = strings.Replace(goFmt, "hh", "15", 1)
- goFmt = strings.Replace(goFmt, "HH", "15", 1)
- if 0 < val.Hour() && val.Hour() < 12 {
- goFmt = strings.Replace(goFmt, "h", "3", 1)
- goFmt = strings.Replace(goFmt, "H", "3", 1)
- } else {
- goFmt = strings.Replace(goFmt, "h", "15", 1)
- goFmt = strings.Replace(goFmt, "H", "15", 1)
- }
- }
-
- for _, repl := range replacements {
- goFmt = strings.Replace(goFmt, repl.xltime, repl.gotime, 1)
- }
- for _, repl := range replacementsGlobal {
- goFmt = strings.Replace(goFmt, repl.xltime, repl.gotime, -1)
- }
- // If the hour is optional, strip it out, along with the possible dangling
- // colon that would remain.
- if val.Hour() < 1 {
- goFmt = strings.Replace(goFmt, "]:", "]", 1)
- goFmt = strings.Replace(goFmt, "[03]", "", 1)
- goFmt = strings.Replace(goFmt, "[3]", "", 1)
- goFmt = strings.Replace(goFmt, "[15]", "", 1)
- } else {
- goFmt = strings.Replace(goFmt, "[3]", "3", 1)
- goFmt = strings.Replace(goFmt, "[15]", "15", 1)
- }
- s := val.Format(goFmt)
- if padding {
- s = strings.Replace(s, "00:", "0:", 1)
- }
- return s
-}
-
-// is12HourTime checks whether an Excel time format string is a 12 hours form.
-func is12HourTime(format string) bool {
- return strings.Contains(format, "am/pm") || strings.Contains(format, "AM/PM") || strings.Contains(format, "a/p") || strings.Contains(format, "A/P")
-}
-
// stylesReader provides a function to get the pointer to the structure after
// deserialization of xl/styles.xml.
func (f *File) stylesReader() *xlsxStyleSheet {
diff --git a/styles_test.go b/styles_test.go
index 3597c36..de3444f 100644
--- a/styles_test.go
+++ b/styles_test.go
@@ -325,26 +325,6 @@ func TestGetFillID(t *testing.T) {
assert.Equal(t, -1, getFillID(NewFile().stylesReader(), &Style{Fill: Fill{Type: "unknown"}}))
}
-func TestParseTime(t *testing.T) {
- assert.Equal(t, "2019", parseTime("43528", "YYYY"))
- assert.Equal(t, "43528", parseTime("43528", ""))
-
- assert.Equal(t, "2019-03-04 05:05:42", parseTime("43528.2123", "YYYY-MM-DD hh:mm:ss"))
- assert.Equal(t, "2019-03-04 05:05:42", parseTime("43528.2123", "YYYY-MM-DD hh:mm:ss;YYYY-MM-DD hh:mm:ss"))
- assert.Equal(t, "3/4/2019 5:5:42", parseTime("43528.2123", "M/D/YYYY h:m:s"))
- assert.Equal(t, "3/4/2019 0:5:42", parseTime("43528.003958333335", "m/d/yyyy h:m:s"))
- assert.Equal(t, "3/4/2019 0:05:42", parseTime("43528.003958333335", "M/D/YYYY h:mm:s"))
- assert.Equal(t, "3:30:00 PM", parseTime("0.64583333333333337", "h:mm:ss am/pm"))
- assert.Equal(t, "0:05", parseTime("43528.003958333335", "h:mm"))
- assert.Equal(t, "0:0", parseTime("6.9444444444444444E-5", "h:m"))
- assert.Equal(t, "0:00", parseTime("6.9444444444444444E-5", "h:mm"))
- assert.Equal(t, "0:0", parseTime("6.9444444444444444E-5", "h:m"))
- assert.Equal(t, "12:1", parseTime("0.50070601851851848", "h:m"))
- assert.Equal(t, "23:30", parseTime("0.97952546296296295", "h:m"))
- assert.Equal(t, "March", parseTime("43528", "mmmm"))
- assert.Equal(t, "Monday", parseTime("43528", "dddd"))
-}
-
func TestThemeColor(t *testing.T) {
for _, clr := range [][]string{
{"FF000000", ThemeColor("000000", -0.1)},