This fix #1665, supports getting formula string cell value

- Improve compatibility for absolute path drawing part
- Fix incorrect table ID generated in the workbook which contains single table cells
- Fix missing relationship parts in the content types in some cases
- Upgrade number format parser to fix missing literal tokens in some cases
- Update built-in zh-cn and zh-tw language number format
- Ref #65, init new formula function: TEXT
- Remove duplicate style-related variables
- Update the unit tests
This commit is contained in:
xuri 2023-09-21 00:06:31 +08:00
parent 744236b4b8
commit 9c079e5eec
No known key found for this signature in database
GPG Key ID: BA5E5BB1C948EDF7
14 changed files with 199 additions and 163 deletions

23
calc.go
View File

@ -757,6 +757,7 @@ type formulaFuncs struct {
// TBILLPRICE
// TBILLYIELD
// TDIST
// TEXT
// TEXTJOIN
// TIME
// TIMEVALUE
@ -14035,6 +14036,28 @@ func (fn *formulaFuncs) SUBSTITUTE(argsList *list.List) formulaArg {
return newStringFormulaArg(pre + targetText.Value() + post)
}
// TEXT function converts a supplied numeric value into text, in a
// user-specified format. The syntax of the function is:
//
// TEXT(value,format_text)
func (fn *formulaFuncs) TEXT(argsList *list.List) formulaArg {
if argsList.Len() != 2 {
return newErrorFormulaArg(formulaErrorVALUE, "TEXT requires 2 arguments")
}
value, fmtText := argsList.Front().Value.(formulaArg), argsList.Back().Value.(formulaArg)
if value.Type == ArgError {
return value
}
if fmtText.Type == ArgError {
return fmtText
}
cellType := CellTypeNumber
if num := value.ToNumber(); num.Type != ArgNumber {
cellType = CellTypeSharedString
}
return newStringFormulaArg(format(value.Value(), fmtText.Value(), false, cellType, nil))
}
// TEXTJOIN function joins together a series of supplied text strings into one
// combined text string. The user can specify a delimiter to add between the
// individual text items, if required. The syntax of the function is:

View File

@ -1834,6 +1834,15 @@ func TestCalcCellValue(t *testing.T) {
"=SUBSTITUTE(\"abab\",\"x\",\"X\",2)": "abab",
"=SUBSTITUTE(\"John is 5 years old\",\"John\",\"Jack\")": "Jack is 5 years old",
"=SUBSTITUTE(\"John is 5 years old\",\"5\",\"6\")": "John is 6 years old",
// TEXT
"=TEXT(\"07/07/2015\",\"mm/dd/yyyy\")": "07/07/2015",
"=TEXT(42192,\"mm/dd/yyyy\")": "07/07/2015",
"=TEXT(42192,\"mmm dd yyyy\")": "Jul 07 2015",
"=TEXT(0.75,\"hh:mm\")": "18:00",
"=TEXT(36.363636,\"0.00\")": "36.36",
"=TEXT(567.9,\"$#,##0.00\")": "$567.90",
"=TEXT(-5,\"+ $#,##0.00;- $#,##0.00;$0.00\")": "- $5.00",
"=TEXT(5,\"+ $#,##0.00;- $#,##0.00;$0.00\")": "+ $5.00",
// TEXTJOIN
"=TEXTJOIN(\"-\",TRUE,1,2,3,4)": "1-2-3-4",
"=TEXTJOIN(A4,TRUE,A1:B2)": "1040205",
@ -3866,6 +3875,10 @@ func TestCalcCellValue(t *testing.T) {
"=SUBSTITUTE()": {"#VALUE!", "SUBSTITUTE requires 3 or 4 arguments"},
"=SUBSTITUTE(\"\",\"\",\"\",\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
"=SUBSTITUTE(\"\",\"\",\"\",0)": {"#VALUE!", "instance_num should be > 0"},
// TEXT
"=TEXT()": {"#VALUE!", "TEXT requires 2 arguments"},
"=TEXT(NA(),\"\")": {"#N/A", "#N/A"},
"=TEXT(0,NA())": {"#N/A", "#N/A"},
// TEXTJOIN
"=TEXTJOIN()": {"#VALUE!", "TEXTJOIN requires at least 3 arguments"},
"=TEXTJOIN(\"\",\"\",1)": {"#VALUE!", "#VALUE!"},

View File

@ -590,6 +590,8 @@ func (c *xlsxC) getValueFrom(f *File, d *xlsxSST, raw bool) (string, error) {
}
}
return f.formattedValue(c, raw, CellTypeSharedString)
case "str":
return c.V, nil
case "inlineStr":
if c.IS != nil {
return f.formattedValue(&xlsxC{S: c.S, V: c.IS.String()}, raw, CellTypeInlineString)

View File

@ -25,8 +25,9 @@ import (
func (f *File) prepareDrawing(ws *xlsxWorksheet, drawingID int, sheet, drawingXML string) (int, string) {
sheetRelationshipsDrawingXML := "../drawings/drawing" + strconv.Itoa(drawingID) + ".xml"
if ws.Drawing != nil {
// The worksheet already has a picture or chart relationships, use the relationships drawing ../drawings/drawing%d.xml.
sheetRelationshipsDrawingXML = f.getSheetRelationshipsTargetByID(sheet, ws.Drawing.RID)
// The worksheet already has a picture or chart relationships, use the
// relationships drawing ../drawings/drawing%d.xml or /xl/drawings/drawing%d.xml.
sheetRelationshipsDrawingXML = strings.ReplaceAll(f.getSheetRelationshipsTargetByID(sheet, ws.Drawing.RID), "/xl/drawings/", "../drawings/")
drawingID, _ = strconv.Atoi(strings.TrimSuffix(strings.TrimPrefix(sheetRelationshipsDrawingXML, "../drawings/drawing"), ".xml"))
drawingXML = strings.ReplaceAll(sheetRelationshipsDrawingXML, "..", "xl")
} else {
@ -1247,9 +1248,11 @@ func (f *File) drawingParser(path string) (*xlsxWsDr, int, error) {
)
_, ok = f.Drawings.Load(path)
if !ok {
content := xlsxWsDr{}
content.A = NameSpaceDrawingML.Value
content.Xdr = NameSpaceDrawingMLSpreadSheet.Value
content := xlsxWsDr{
NS: NameSpaceDrawingMLSpreadSheet.Value,
Xdr: NameSpaceDrawingMLSpreadSheet.Value,
A: NameSpaceDrawingML.Value,
}
if _, ok = f.Pkg.Load(path); ok { // Append Model
decodeWsDr := decodeWsDr{}
if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(path)))).

8
go.mod
View File

@ -8,9 +8,9 @@ require (
github.com/richardlehane/msoleps v1.0.3 // indirect
github.com/stretchr/testify v1.8.0
github.com/xuri/efp v0.0.0-20230802181842-ad255f2331ca
github.com/xuri/nfp v0.0.0-20230819163627-dc951e3ffe1a
golang.org/x/crypto v0.12.0
github.com/xuri/nfp v0.0.0-20230918160701-e5a3f5b24785
golang.org/x/crypto v0.13.0
golang.org/x/image v0.11.0
golang.org/x/net v0.14.0
golang.org/x/text v0.12.0
golang.org/x/net v0.15.0
golang.org/x/text v0.13.0
)

19
go.sum
View File

@ -17,13 +17,13 @@ github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PK
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/xuri/efp v0.0.0-20230802181842-ad255f2331ca h1:uvPMDVyP7PXMMioYdyPH+0O+Ta/UO1WFfNYMO3Wz0eg=
github.com/xuri/efp v0.0.0-20230802181842-ad255f2331ca/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
github.com/xuri/nfp v0.0.0-20230819163627-dc951e3ffe1a h1:Mw2VNrNNNjDtw68VsEj2+st+oCSn4Uz7vZw6TbhcV1o=
github.com/xuri/nfp v0.0.0-20230819163627-dc951e3ffe1a/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
github.com/xuri/nfp v0.0.0-20230918160701-e5a3f5b24785 h1:FG9hcK7lhf3w/Y2NRUKy/mopsH0Oy6P1rib1KWXAie0=
github.com/xuri/nfp v0.0.0-20230918160701-e5a3f5b24785/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/image v0.11.0 h1:ds2RoQvBvYTiJkwpSFDwCcDFNX7DqjL2WsUgTNk0Ooo=
golang.org/x/image v0.11.0/go.mod h1:bglhjqbqVuEb9e9+eNR45Jfu7D+T4Qan+NhQk8Ck2P8=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
@ -33,8 +33,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -45,19 +45,20 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/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/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=

View File

@ -108,16 +108,16 @@ var (
31: "yyyy\"年\"m\"月\"d\"日\"",
32: "hh\"時\"mm\"分\"",
33: "hh\"時\"mm\"分\"ss\"秒\"",
34: "上午/下午 hh\"時\"mm\"分\"",
35: "上午/下午 hh\"時\"mm\"分\"ss\"秒\"",
34: "上午/下午hh\"時\"mm\"分\"",
35: "上午/下午hh\"時\"mm\"分\"ss\"秒\"",
36: "[$-404]e/m/d",
50: "[$-404]e/m/d",
51: "[$-404]e\"年\"m\"月\"d\"日\"",
52: "上午/下午 hh\"時\"mm\"分\"",
53: "上午/下午 hh\"時\"mm\"分\"ss\"秒\"",
52: "上午/下午hh\"時\"mm\"分\"",
53: "上午/下午hh\"時\"mm\"分\"ss\"秒\"",
54: "[$-404]e\"年\"m\"月\"d\"日\"",
55: "上午/下午 hh\"時\"mm\"分\"",
56: "上午/下午 hh\"時\"mm\"分\"ss\"秒\"",
55: "上午/下午hh\"時\"mm\"分\"",
56: "上午/下午hh\"時\"mm\"分\"ss\"秒\"",
57: "[$-404]e/m/d",
58: "[$-404]e\"年\"m\"月\"d\"日\"",
},
@ -129,16 +129,16 @@ var (
31: "yyyy\"年\"m\"月\"d\"日\"",
32: "h\"时\"mm\"分\"",
33: "h\"时\"mm\"分\"ss\"秒\"",
34: "上午/下午 h\"时\"mm\"分\"",
35: "上午/下午 h\"时\"mm\"分\"ss\"秒\"",
34: "上午/下午h\"时\"mm\"分\"",
35: "上午/下午h\"时\"mm\"分\"ss\"秒\"",
36: "yyyy\"年\"m\"月\"",
50: "yyyy\"年\"m\"月\"",
51: "m\"月\"d\"日\"",
52: "yyyy\"年\"m\"月\"",
53: "m\"月\"d\"日\"",
54: "m\"月\"d\"日\"",
55: "上午/下午 h\"时\"mm\"分\"",
56: "上午/下午 h\"时\"mm\"分\"ss\"秒\"",
55: "上午/下午h\"时\"mm\"分\"",
56: "上午/下午h\"时\"mm\"分\"ss\"秒\"",
57: "yyyy\"年\"m\"月\"",
58: "m\"月\"d\"日\"",
},

View File

@ -72,6 +72,7 @@ func TestNumFmt(t *testing.T) {
{"43528", "[$-111]MM/DD/YYYY", "43528"},
{"43528", "[$US-409]MM/DD/YYYY", "US03/04/2019"},
{"43543.586539351854", "AM/PM h h:mm", "PM 14 2:04"},
{"45186", "DD.MM.YYYY", "17.09.2023"},
{"text", "AM/PM h h:mm", "text"},
{"43466.189571759256", "[$-404]aaa;@", "週二"},
{"43466.189571759256", "[$-404]aaaa;@", "星期二"},

View File

@ -36,7 +36,7 @@ import (
// Caption specifies the caption of the slicer, this setting is optional.
//
// Macro used for set macro for the slicer, the workbook extension should be
// XLSM or XLTM
// XLSM or XLTM.
//
// Width specifies the width of the slicer, this setting is optional.
//

209
styles.go
View File

@ -1096,6 +1096,73 @@ var (
{Stop: []*xlsxGradientFillStop{{}, {Position: 1}}, Type: "path", Bottom: 1, Left: 1, Right: 1, Top: 1},
{Stop: []*xlsxGradientFillStop{{}, {Position: 1}}, Type: "path", Bottom: 0.5, Left: 0.5, Right: 0.5, Top: 0.5},
}
// getXfIDFuncs provides a function to get xfID by given style.
getXfIDFuncs = map[string]func(int, xlsxXf, *Style) bool{
"numFmt": func(numFmtID int, xf xlsxXf, style *Style) bool {
if style.CustomNumFmt == nil && numFmtID == -1 {
return xf.NumFmtID != nil && *xf.NumFmtID == 0
}
if style.NegRed || (style.DecimalPlaces != nil && *style.DecimalPlaces != 2) {
return false
}
return xf.NumFmtID != nil && *xf.NumFmtID == numFmtID
},
"font": func(fontID int, xf xlsxXf, style *Style) bool {
if style.Font == nil {
return (xf.FontID == nil || *xf.FontID == 0) && (xf.ApplyFont == nil || !*xf.ApplyFont)
}
return xf.FontID != nil && *xf.FontID == fontID && xf.ApplyFont != nil && *xf.ApplyFont
},
"fill": func(fillID int, xf xlsxXf, style *Style) bool {
if style.Fill.Type == "" {
return (xf.FillID == nil || *xf.FillID == 0) && (xf.ApplyFill == nil || !*xf.ApplyFill)
}
return xf.FillID != nil && *xf.FillID == fillID && xf.ApplyFill != nil && *xf.ApplyFill
},
"border": func(borderID int, xf xlsxXf, style *Style) bool {
if len(style.Border) == 0 {
return (xf.BorderID == nil || *xf.BorderID == 0) && (xf.ApplyBorder == nil || !*xf.ApplyBorder)
}
return xf.BorderID != nil && *xf.BorderID == borderID && xf.ApplyBorder != nil && *xf.ApplyBorder
},
"alignment": func(ID int, xf xlsxXf, style *Style) bool {
if style.Alignment == nil {
return xf.ApplyAlignment == nil || !*xf.ApplyAlignment
}
return reflect.DeepEqual(xf.Alignment, newAlignment(style))
},
"protection": func(ID int, xf xlsxXf, style *Style) bool {
if style.Protection == nil {
return xf.ApplyProtection == nil || !*xf.ApplyProtection
}
return reflect.DeepEqual(xf.Protection, newProtection(style)) && xf.ApplyProtection != nil && *xf.ApplyProtection
},
}
// drawContFmtFunc defines functions to create conditional formats.
drawContFmtFunc = map[string]func(p int, ct, GUID string, fmtCond *ConditionalFormatOptions) (*xlsxCfRule, *xlsxX14CfRule){
"cellIs": drawCondFmtCellIs,
"top10": drawCondFmtTop10,
"aboveAverage": drawCondFmtAboveAverage,
"duplicateValues": drawCondFmtDuplicateUniqueValues,
"uniqueValues": drawCondFmtDuplicateUniqueValues,
"2_color_scale": drawCondFmtColorScale,
"3_color_scale": drawCondFmtColorScale,
"dataBar": drawCondFmtDataBar,
"expression": drawCondFmtExp,
"iconSet": drawCondFmtIconSet,
}
// extractContFmtFunc defines functions to get conditional formats.
extractContFmtFunc = map[string]func(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions{
"cellIs": extractCondFmtCellIs,
"top10": extractCondFmtTop10,
"aboveAverage": extractCondFmtAboveAverage,
"duplicateValues": extractCondFmtDuplicateUniqueValues,
"uniqueValues": extractCondFmtDuplicateUniqueValues,
"colorScale": extractCondFmtColorScale,
"dataBar": extractCondFmtDataBar,
"expression": extractCondFmtExp,
"iconSet": extractCondFmtIconSet,
}
)
// getThemeColor provides a function to convert theme color or index color to
@ -1329,49 +1396,6 @@ func (f *File) GetStyle(idx int) (*Style, error) {
return style, nil
}
// getXfIDFuncs provides a function to get xfID by given style.
var getXfIDFuncs = map[string]func(int, xlsxXf, *Style) bool{
"numFmt": func(numFmtID int, xf xlsxXf, style *Style) bool {
if style.CustomNumFmt == nil && numFmtID == -1 {
return xf.NumFmtID != nil && *xf.NumFmtID == 0
}
if style.NegRed || (style.DecimalPlaces != nil && *style.DecimalPlaces != 2) {
return false
}
return xf.NumFmtID != nil && *xf.NumFmtID == numFmtID
},
"font": func(fontID int, xf xlsxXf, style *Style) bool {
if style.Font == nil {
return (xf.FontID == nil || *xf.FontID == 0) && (xf.ApplyFont == nil || !*xf.ApplyFont)
}
return xf.FontID != nil && *xf.FontID == fontID && xf.ApplyFont != nil && *xf.ApplyFont
},
"fill": func(fillID int, xf xlsxXf, style *Style) bool {
if style.Fill.Type == "" {
return (xf.FillID == nil || *xf.FillID == 0) && (xf.ApplyFill == nil || !*xf.ApplyFill)
}
return xf.FillID != nil && *xf.FillID == fillID && xf.ApplyFill != nil && *xf.ApplyFill
},
"border": func(borderID int, xf xlsxXf, style *Style) bool {
if len(style.Border) == 0 {
return (xf.BorderID == nil || *xf.BorderID == 0) && (xf.ApplyBorder == nil || !*xf.ApplyBorder)
}
return xf.BorderID != nil && *xf.BorderID == borderID && xf.ApplyBorder != nil && *xf.ApplyBorder
},
"alignment": func(ID int, xf xlsxXf, style *Style) bool {
if style.Alignment == nil {
return xf.ApplyAlignment == nil || !*xf.ApplyAlignment
}
return reflect.DeepEqual(xf.Alignment, newAlignment(style))
},
"protection": func(ID int, xf xlsxXf, style *Style) bool {
if style.Protection == nil {
return xf.ApplyProtection == nil || !*xf.ApplyProtection
}
return reflect.DeepEqual(xf.Protection, newProtection(style)) && xf.ApplyProtection != nil && *xf.ApplyProtection
},
}
// getStyleID provides a function to get styleID by given style. If given
// style does not exist, will return -1.
func (f *File) getStyleID(ss *xlsxStyleSheet, style *Style) (int, error) {
@ -1740,54 +1764,13 @@ func getFillID(styleSheet *xlsxStyleSheet, style *Style) (fillID int) {
// newFills provides a function to add fill elements in the styles.xml by
// given cell format settings.
func newFills(style *Style, fg bool) *xlsxFill {
patterns := []string{
"none",
"solid",
"mediumGray",
"darkGray",
"lightGray",
"darkHorizontal",
"darkVertical",
"darkDown",
"darkUp",
"darkGrid",
"darkTrellis",
"lightHorizontal",
"lightVertical",
"lightDown",
"lightUp",
"lightGrid",
"lightTrellis",
"gray125",
"gray0625",
}
variants := []xlsxGradientFill{
{Degree: 90, Stop: []*xlsxGradientFillStop{{}, {Position: 1}}},
{Degree: 270, Stop: []*xlsxGradientFillStop{{}, {Position: 1}}},
{Degree: 90, Stop: []*xlsxGradientFillStop{{}, {Position: 0.5}, {Position: 1}}},
{Stop: []*xlsxGradientFillStop{{}, {Position: 1}}},
{Degree: 180, Stop: []*xlsxGradientFillStop{{}, {Position: 1}}},
{Stop: []*xlsxGradientFillStop{{}, {Position: 0.5}, {Position: 1}}},
{Degree: 45, Stop: []*xlsxGradientFillStop{{}, {Position: 1}}},
{Degree: 255, Stop: []*xlsxGradientFillStop{{}, {Position: 1}}},
{Degree: 45, Stop: []*xlsxGradientFillStop{{}, {Position: 0.5}, {Position: 1}}},
{Degree: 135, Stop: []*xlsxGradientFillStop{{}, {Position: 1}}},
{Degree: 315, Stop: []*xlsxGradientFillStop{{}, {Position: 1}}},
{Degree: 135, Stop: []*xlsxGradientFillStop{{}, {Position: 0.5}, {Position: 1}}},
{Stop: []*xlsxGradientFillStop{{}, {Position: 1}}, Type: "path"},
{Stop: []*xlsxGradientFillStop{{}, {Position: 1}}, Type: "path", Left: 1, Right: 1},
{Stop: []*xlsxGradientFillStop{{}, {Position: 1}}, Type: "path", Bottom: 1, Top: 1},
{Stop: []*xlsxGradientFillStop{{}, {Position: 1}}, Type: "path", Bottom: 1, Left: 1, Right: 1, Top: 1},
{Stop: []*xlsxGradientFillStop{{}, {Position: 1}}, Type: "path", Bottom: 0.5, Left: 0.5, Right: 0.5, Top: 0.5},
}
var fill xlsxFill
switch style.Fill.Type {
case "gradient":
if len(style.Fill.Color) != 2 || style.Fill.Shading < 0 || style.Fill.Shading > 16 {
break
}
gradient := variants[style.Fill.Shading]
gradient := styleFillVariants[style.Fill.Shading]
gradient.Stop[0].Color.RGB = getPaletteColor(style.Fill.Color[0])
gradient.Stop[1].Color.RGB = getPaletteColor(style.Fill.Color[1])
if len(gradient.Stop) == 3 {
@ -1802,7 +1785,7 @@ func newFills(style *Style, fg bool) *xlsxFill {
break
}
var pattern xlsxPatternFill
pattern.PatternType = patterns[style.Fill.Pattern]
pattern.PatternType = styleFillPatterns[style.Fill.Pattern]
if fg {
if pattern.FgColor == nil {
pattern.FgColor = new(xlsxColor)
@ -1871,23 +1854,6 @@ func getBorderID(styleSheet *xlsxStyleSheet, style *Style) (borderID int) {
// newBorders provides a function to add border elements in the styles.xml by
// given borders format settings.
func newBorders(style *Style) *xlsxBorder {
styles := []string{
"none",
"thin",
"medium",
"dashed",
"dotted",
"thick",
"double",
"hair",
"mediumDashed",
"dashDot",
"mediumDashDot",
"dashDotDot",
"mediumDashDotDot",
"slantDashDot",
}
var border xlsxBorder
for _, v := range style.Border {
if 0 <= v.Style && v.Style < 14 {
@ -1895,23 +1861,23 @@ func newBorders(style *Style) *xlsxBorder {
color.RGB = getPaletteColor(v.Color)
switch v.Type {
case "left":
border.Left.Style = styles[v.Style]
border.Left.Style = styleBorders[v.Style]
border.Left.Color = &color
case "right":
border.Right.Style = styles[v.Style]
border.Right.Style = styleBorders[v.Style]
border.Right.Color = &color
case "top":
border.Top.Style = styles[v.Style]
border.Top.Style = styleBorders[v.Style]
border.Top.Color = &color
case "bottom":
border.Bottom.Style = styles[v.Style]
border.Bottom.Style = styleBorders[v.Style]
border.Bottom.Color = &color
case "diagonalUp":
border.Diagonal.Style = styles[v.Style]
border.Diagonal.Style = styleBorders[v.Style]
border.Diagonal.Color = &color
border.DiagonalUp = true
case "diagonalDown":
border.Diagonal.Style = styles[v.Style]
border.Diagonal.Style = styleBorders[v.Style]
border.Diagonal.Color = &color
border.DiagonalDown = true
}
@ -2551,19 +2517,6 @@ func (f *File) SetCellStyle(sheet, hCell, vCell string, styleID int) error {
// cells. When this parameter is set then subsequent rules are not evaluated
// if the current rule is true.
func (f *File) SetConditionalFormat(sheet, rangeRef string, opts []ConditionalFormatOptions) error {
drawContFmtFunc := map[string]func(p int, ct, GUID string, fmtCond *ConditionalFormatOptions) (*xlsxCfRule, *xlsxX14CfRule){
"cellIs": drawCondFmtCellIs,
"top10": drawCondFmtTop10,
"aboveAverage": drawCondFmtAboveAverage,
"duplicateValues": drawCondFmtDuplicateUniqueValues,
"uniqueValues": drawCondFmtDuplicateUniqueValues,
"2_color_scale": drawCondFmtColorScale,
"3_color_scale": drawCondFmtColorScale,
"dataBar": drawCondFmtDataBar,
"expression": drawCondFmtExp,
"iconSet": drawCondFmtIconSet,
}
ws, err := f.workSheetReader(sheet)
if err != nil {
return err
@ -2833,18 +2786,6 @@ func extractCondFmtIconSet(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatO
// GetConditionalFormats returns conditional format settings by given worksheet
// name.
func (f *File) GetConditionalFormats(sheet string) (map[string][]ConditionalFormatOptions, error) {
extractContFmtFunc := map[string]func(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions{
"cellIs": extractCondFmtCellIs,
"top10": extractCondFmtTop10,
"aboveAverage": extractCondFmtAboveAverage,
"duplicateValues": extractCondFmtDuplicateUniqueValues,
"uniqueValues": extractCondFmtDuplicateUniqueValues,
"colorScale": extractCondFmtColorScale,
"dataBar": extractCondFmtDataBar,
"expression": extractCondFmtExp,
"iconSet": extractCondFmtIconSet,
}
conditionalFormats := make(map[string][]ConditionalFormatOptions)
ws, err := f.workSheetReader(sheet)
if err != nil {

View File

@ -216,6 +216,19 @@ func (f *File) DeleteTable(name string) error {
func (f *File) countTables() int {
count := 0
f.Pkg.Range(func(k, v interface{}) bool {
if strings.Contains(k.(string), "xl/tables/tableSingleCells") {
var cells xlsxSingleXmlCells
if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(v.([]byte)))).
Decode(&cells); err != nil && err != io.EOF {
count++
return true
}
for _, cell := range cells.SingleXmlCell {
if count < cell.ID {
count = cell.ID
}
}
}
if strings.Contains(k.(string), "xl/tables/table") {
var t xlsxTable
if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(v.([]byte)))).

View File

@ -76,6 +76,15 @@ func TestAddTable(t *testing.T) {
f = NewFile()
f.Pkg.Store("xl/tables/table1.xml", MacintoshCyrillicCharset)
assert.NoError(t, f.AddTable("Sheet1", &Table{Range: "A1:B2"}))
assert.NoError(t, f.Close())
f = NewFile()
// Test add table with workbook with single cells parts
f.Pkg.Store("xl/tables/tableSingleCells1.xml", []byte("<singleXmlCells><singleXmlCell id=\"2\" r=\"A1\" connectionId=\"2\" /></singleXmlCells>"))
assert.NoError(t, f.AddTable("Sheet1", &Table{Range: "A1:B2"}))
// Test add table with workbook with unsupported charset single cells parts
f.Pkg.Store("xl/tables/tableSingleCells1.xml", MacintoshCyrillicCharset)
assert.NoError(t, f.AddTable("Sheet1", &Table{Range: "A1:B2"}))
assert.NoError(t, f.Close())
}
func TestGetTables(t *testing.T) {

View File

@ -242,6 +242,7 @@ type xlsxPoint2D struct {
type xlsxWsDr struct {
mu sync.Mutex
XMLName xml.Name `xml:"xdr:wsDr"`
NS string `xml:"xmlns,attr,omitempty"`
A string `xml:"xmlns:a,attr,omitempty"`
Xdr string `xml:"xmlns:xdr,attr,omitempty"`
R string `xml:"xmlns:r,attr,omitempty"`

View File

@ -196,6 +196,35 @@ type xlsxTableStyleInfo struct {
ShowColumnStripes bool `xml:"showColumnStripes,attr"`
}
// xlsxSingleXmlCells is a single cell table is generated from an XML mapping.
// These really just look like regular cells to the spreadsheet user, but shall
// be implemented as Tables "under the covers."
type xlsxSingleXmlCells struct {
XMLName xml.Name `xml:"singleXmlCells"`
SingleXmlCell []xlsxSingleXmlCell `xml:"singleXmlCell"`
}
// xlsxSingleXmlCell is a element represents the table properties for a single
// cell XML table.
type xlsxSingleXmlCell struct {
XMLName xml.Name `xml:"singleXmlCell"`
ID int `xml:"id,attr"`
R string `xml:"r,attr"`
ConnectionID int `xml:"connectionId,attr"`
XMLCellPr xlsxXmlCellPr `xml:"xmlCellPr"`
ExtLst *xlsxInnerXML `xml:"extLst"`
}
// xlsxXmlCellPr is a element stores the XML properties for the cell of a single
// cell xml table.
type xlsxXmlCellPr struct {
XMLName xml.Name `xml:"xmlCellPr"`
ID int `xml:"id,attr"`
UniqueName string `xml:"uniqueName,attr,omitempty"`
XMLPr *xlsxInnerXML `xml:"xmlPr"`
ExtLst *xlsxInnerXML `xml:"extLst"`
}
// Table directly maps the format settings of the table.
type Table struct {
tID int