Ref: #660, #764, #1093, #1112, #1133 This improve number format support

- Introduced NFP (number format parser) dependencies module
- Initialize custom dates and times number format support
- Dependencies module upgraded
This commit is contained in:
xuri 2022-02-13 00:06:30 +08:00
parent 3f8f4f52e6
commit 4b64b26c52
No known key found for this signature in database
GPG Key ID: BA5E5BB1C948EDF7
12 changed files with 484 additions and 215 deletions

11
cell.go
View File

@ -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 {

View File

@ -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
}

View File

@ -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")

9
go.mod
View File

@ -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
)

20
go.sum
View File

@ -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=

13
lib.go
View File

@ -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 (

View File

@ -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) {

356
numfmt.go Normal file
View File

@ -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
}

76
numfmt_test.go Normal file
View File

@ -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)
}
}

View File

@ -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)
}
}

185
styles.go
View File

@ -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 {

View File

@ -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)},