From 9c079e5eec19a94553c6a1e5389be375e38a9446 Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 21 Sep 2023 00:06:31 +0800 Subject: [PATCH] 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 --- calc.go | 23 ++++++ calc_test.go | 13 +++ cell.go | 2 + drawing.go | 13 +-- go.mod | 8 +- go.sum | 19 ++--- numfmt.go | 20 ++--- numfmt_test.go | 1 + slicer.go | 2 +- styles.go | 209 ++++++++++++++++++------------------------------- table.go | 13 +++ table_test.go | 9 +++ xmlDrawing.go | 1 + xmlTable.go | 29 +++++++ 14 files changed, 199 insertions(+), 163 deletions(-) diff --git a/calc.go b/calc.go index 4ae2b3e..bd8776e 100644 --- a/calc.go +++ b/calc.go @@ -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: diff --git a/calc_test.go b/calc_test.go index 6042715..0b08db1 100644 --- a/calc_test.go +++ b/calc_test.go @@ -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!"}, diff --git a/cell.go b/cell.go index 36265d9..53bcc21 100644 --- a/cell.go +++ b/cell.go @@ -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) diff --git a/drawing.go b/drawing.go index afe9d4d..045506c 100644 --- a/drawing.go +++ b/drawing.go @@ -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)))). diff --git a/go.mod b/go.mod index 733011f..1c30e60 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index d82097a..3b314f2 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/numfmt.go b/numfmt.go index 83f40f1..8ef69e0 100644 --- a/numfmt.go +++ b/numfmt.go @@ -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\"日\"", }, diff --git a/numfmt_test.go b/numfmt_test.go index 7b8fa91..2433364 100644 --- a/numfmt_test.go +++ b/numfmt_test.go @@ -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;@", "星期二"}, diff --git a/slicer.go b/slicer.go index 435c0f9..c62b337 100644 --- a/slicer.go +++ b/slicer.go @@ -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. // diff --git a/styles.go b/styles.go index 7f0601e..5285295 100644 --- a/styles.go +++ b/styles.go @@ -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 { diff --git a/table.go b/table.go index f365a63..32667cd 100644 --- a/table.go +++ b/table.go @@ -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)))). diff --git a/table_test.go b/table_test.go index d81a52b..994cd72 100644 --- a/table_test.go +++ b/table_test.go @@ -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("")) + 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) { diff --git a/xmlDrawing.go b/xmlDrawing.go index 13ed87f..e5dc384 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -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"` diff --git a/xmlTable.go b/xmlTable.go index 16fd284..7c509ed 100644 --- a/xmlTable.go +++ b/xmlTable.go @@ -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