forked from p30928647/excelize
- Introduced NFP (number format parser) dependencies module - Initialize custom dates and times number format support - Dependencies module upgraded
This commit is contained in:
parent
3f8f4f52e6
commit
4b64b26c52
11
cell.go
11
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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
9
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
|
||||
)
|
||||
|
|
20
go.sum
20
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=
|
||||
|
|
13
lib.go
13
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 (
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
185
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 {
|
||||
|
|
|
@ -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)},
|
||||
|
|
Loading…
Reference in New Issue