diff --git a/cell.go b/cell.go index 9af93f6356..b5b6ed45d3 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 c0dc33b97f..f3b3642e46 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 ebbcef6c27..f0a3405582 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 b7aa1ba578..9d6e88d402 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 7fa8255bbf..efd0f634e5 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 a435452aff..1bfdda2936 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 da75dee5b5..1e2f3249c8 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 0000000000..a724405e45 --- /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 0000000000..b64287b8a3 --- /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 d30eeb1dd7..d7e9c94a2f 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 7678b847b3..6dea20e694 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 3597c3676a..de3444fb07 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)},