Skip to content

Commit 76eeba8

Browse files
committed
UnsetConditionalFormat support remove 3 triangles, 3 stars, 5 boxes icon sets conditional format
- Support remove conditional format in worksheet extension list - Update dependencies modules - Update unit tests
1 parent 23a03c5 commit 76eeba8

File tree

5 files changed

+174
-18
lines changed

5 files changed

+174
-18
lines changed

go.mod

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,20 @@ module github.com/xuri/excelize/v2
33
go 1.24.0
44

55
require (
6-
github.com/richardlehane/mscfb v1.0.5
6+
github.com/richardlehane/mscfb v1.0.6
77
github.com/stretchr/testify v1.11.1
88
github.com/tiendc/go-deepcopy v1.7.2
99
github.com/xuri/efp v0.0.1
1010
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9
11-
golang.org/x/crypto v0.46.0
11+
golang.org/x/crypto v0.47.0
1212
golang.org/x/image v0.25.0
13-
golang.org/x/net v0.48.0
13+
golang.org/x/net v0.49.0
1414
golang.org/x/text v0.33.0
1515
)
1616

1717
require (
1818
github.com/davecgh/go-spew v1.1.1 // indirect
1919
github.com/pmezard/go-difflib v1.0.0 // indirect
20-
github.com/richardlehane/msoleps v1.0.4 // indirect
20+
github.com/richardlehane/msoleps v1.0.6 // indirect
2121
gopkg.in/yaml.v3 v3.0.1 // indirect
2222
)

go.sum

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
22
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
33
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
44
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
5-
github.com/richardlehane/mscfb v1.0.5 h1:OoQkDV2Bf2bIoSacCfJhSwm7BJN05fYFkwFUpxExtdY=
6-
github.com/richardlehane/mscfb v1.0.5/go.mod h1:pe0+IUIc0AHh0+teNzBlJCtSyZdFOGgV4ZK9bsoV+Jo=
7-
github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00=
8-
github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
5+
github.com/richardlehane/mscfb v1.0.6 h1:eN3bvvZCp00bs7Zf52bxNwAx5lJDBK1tCuH19qq5aC8=
6+
github.com/richardlehane/mscfb v1.0.6/go.mod h1:pe0+IUIc0AHh0+teNzBlJCtSyZdFOGgV4ZK9bsoV+Jo=
7+
github.com/richardlehane/msoleps v1.0.6 h1:9BvkpjvD+iUBalUY4esMwv6uBkfOip/Lzvd93jvR9gg=
8+
github.com/richardlehane/msoleps v1.0.6/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
99
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
1010
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
1111
github.com/tiendc/go-deepcopy v1.7.2 h1:Ut2yYR7W9tWjTQitganoIue4UGxZwCcJy3orjrrIj44=
@@ -14,12 +14,12 @@ github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8=
1414
github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
1515
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 h1:+C0TIdyyYmzadGaL/HBLbf3WdLgC29pgyhTjAT/0nuE=
1616
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
17-
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
18-
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
17+
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
18+
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
1919
golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ=
2020
golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs=
21-
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
22-
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
21+
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
22+
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
2323
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
2424
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
2525
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=

styles.go

Lines changed: 121 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3249,10 +3249,130 @@ func (f *File) UnsetConditionalFormat(sheet, rangeRef string) error {
32493249
for i, cf := range ws.ConditionalFormatting {
32503250
if cf.SQRef == rangeRef {
32513251
ws.ConditionalFormatting = append(ws.ConditionalFormatting[:i], ws.ConditionalFormatting[i+1:]...)
3252+
}
3253+
}
3254+
if ws.ExtLst != nil {
3255+
var condFmtsBytes, extLstBytes []byte
3256+
decodeExtLst := new(decodeExtLst)
3257+
if err = f.xmlNewDecoder(strings.NewReader("<extLst>" + ws.ExtLst.Ext + "</extLst>")).
3258+
Decode(decodeExtLst); err != nil && err != io.EOF {
32523259
return err
32533260
}
3261+
for idx := 0; idx < len(decodeExtLst.Ext); idx++ {
3262+
ext := decodeExtLst.Ext[idx]
3263+
if ext.URI == ExtURIConditionalFormattings {
3264+
decodeCondFmts := new(decodeX14ConditionalFormattingRules)
3265+
_ = f.xmlNewDecoder(strings.NewReader(ext.Content)).Decode(decodeCondFmts)
3266+
condFmtsBytes, _ = decodeCondFmts.deleteCfRule(rangeRef)
3267+
decodeExtLst.Ext[idx].Content = string(condFmtsBytes)
3268+
if len(decodeExtLst.Ext[idx].Content) == 57 { // empty x14:conditionalFormattings element
3269+
decodeExtLst.Ext = append(decodeExtLst.Ext[:idx], decodeExtLst.Ext[idx+1:]...)
3270+
idx--
3271+
}
3272+
}
3273+
}
3274+
sort.Slice(decodeExtLst.Ext, func(i, j int) bool {
3275+
return inStrSlice(worksheetExtURIPriority, decodeExtLst.Ext[i].URI, false) <
3276+
inStrSlice(worksheetExtURIPriority, decodeExtLst.Ext[j].URI, false)
3277+
})
3278+
extLstBytes, err = xml.Marshal(decodeExtLst)
3279+
ws.ExtLst = &xlsxExtLst{Ext: strings.TrimSuffix(strings.TrimPrefix(string(extLstBytes), "<extLst>"), "</extLst>")}
3280+
if len(ws.ExtLst.Ext) == 0 {
3281+
ws.ExtLst = nil
3282+
}
32543283
}
3255-
return nil
3284+
return err
3285+
}
3286+
3287+
// deleteCfRule provides a function to delete conditional formatting rule by
3288+
// given range reference and return the updated x14:conditionalFormattings
3289+
// element content.
3290+
func (r *decodeX14ConditionalFormattingRules) deleteCfRule(rangeRef string) ([]byte, error) {
3291+
condFmts := &xlsxX14ConditionalFormattings{}
3292+
for _, condFmt := range r.CondFmt {
3293+
if condFmt.Sqref == rangeRef {
3294+
continue
3295+
}
3296+
x14ConditionalFormatting := xlsxX14ConditionalFormatting{
3297+
XMLNSXM: NameSpaceSpreadSheetExcel2006Main.Value,
3298+
Pivot: condFmt.Pivot,
3299+
Sqref: condFmt.Sqref,
3300+
ExtLst: condFmt.ExtLst,
3301+
}
3302+
for _, rule := range condFmt.CfRule {
3303+
x14CfRule := &xlsxX14CfRule{
3304+
Type: rule.Type,
3305+
Priority: rule.Priority,
3306+
StopIfTrue: rule.StopIfTrue,
3307+
AboveAverage: rule.AboveAverage,
3308+
Percent: rule.Percent,
3309+
Bottom: rule.Bottom,
3310+
Operator: rule.Operator,
3311+
Text: rule.Text,
3312+
TimePeriod: rule.TimePeriod,
3313+
Rank: rule.Rank,
3314+
StdDev: rule.StdDev,
3315+
EqualAverage: rule.EqualAverage,
3316+
ActivePresent: rule.ActivePresent,
3317+
ID: rule.ID,
3318+
F: rule.F,
3319+
ColorScale: rule.ColorScale,
3320+
Dxf: rule.Dxf,
3321+
ExtLst: rule.ExtLst,
3322+
}
3323+
if rule.DataBar != nil {
3324+
x14DataBar := &xlsx14DataBar{
3325+
MaxLength: rule.DataBar.MaxLength,
3326+
MinLength: rule.DataBar.MinLength,
3327+
Border: rule.DataBar.Border,
3328+
ShowValue: rule.DataBar.ShowValue,
3329+
Direction: rule.DataBar.Direction,
3330+
BorderColor: rule.DataBar.BorderColor,
3331+
NegativeFillColor: rule.DataBar.NegativeFillColor,
3332+
AxisColor: rule.DataBar.AxisColor,
3333+
}
3334+
if rule.DataBar.Gradient != nil {
3335+
x14DataBar.Gradient = *rule.DataBar.Gradient
3336+
}
3337+
if rule.DataBar.Cfvo != nil {
3338+
for _, cfvo := range rule.DataBar.Cfvo {
3339+
x14DataBar.Cfvo = append(x14DataBar.Cfvo, &xlsxCfvo{
3340+
Gte: cfvo.Gte,
3341+
Type: cfvo.Type,
3342+
Val: cfvo.Val,
3343+
ExtLst: cfvo.ExtLst,
3344+
})
3345+
}
3346+
}
3347+
x14CfRule.DataBar = x14DataBar
3348+
}
3349+
if rule.IconSet != nil {
3350+
x14IconSet := &xlsx14IconSet{
3351+
IconSet: rule.IconSet.IconSet,
3352+
ShowValue: rule.IconSet.ShowValue,
3353+
Percent: rule.IconSet.Percent,
3354+
Reverse: rule.IconSet.Reverse,
3355+
Custom: rule.IconSet.Custom,
3356+
CfIcon: rule.IconSet.CfIcon,
3357+
}
3358+
if rule.IconSet.Cfvo != nil {
3359+
for _, cfvo := range rule.IconSet.Cfvo {
3360+
x14IconSet.Cfvo = append(x14IconSet.Cfvo, &xlsx14Cfvo{
3361+
Type: cfvo.Type,
3362+
Gte: cfvo.Gte,
3363+
F: cfvo.F,
3364+
ExtLst: cfvo.ExtLst,
3365+
})
3366+
}
3367+
}
3368+
x14CfRule.IconSet = x14IconSet
3369+
}
3370+
x14ConditionalFormatting.CfRule = append(x14ConditionalFormatting.CfRule, x14CfRule)
3371+
}
3372+
condFmtBytes, _ := xml.Marshal(x14ConditionalFormatting)
3373+
condFmts.Content += string(condFmtBytes)
3374+
}
3375+
return xml.Marshal(condFmts)
32563376
}
32573377

32583378
// drawCondFmtCellIs provides a function to create conditional formatting rule

styles_test.go

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,25 @@ func TestGetConditionalFormats(t *testing.T) {
290290
assert.Equal(t, format, opts[fmt.Sprintf("%s1:%s10", col, col)])
291291
}
292292
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestGetConditionalFormats.xlsx")))
293+
// Test unset all conditional formats
294+
f, err = OpenFile(filepath.Join("test", "TestGetConditionalFormats.xlsx"))
295+
assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOptions{
296+
Location: []string{"C1"},
297+
Range: []string{"Sheet1!A1:B1"},
298+
}))
299+
for _, rangeRef := range []string{"Y1:Y10", "Z1:Z10", "AG1:AG10", "AH1:AH10", "AI1:AI10"} {
300+
assert.NoError(t, f.UnsetConditionalFormat("Sheet1", rangeRef))
301+
}
302+
assert.NoError(t, err)
303+
assert.NoError(t, f.Close())
304+
// Test unset conditional formats with invalid extension list characters
305+
f, err = OpenFile(filepath.Join("test", "TestGetConditionalFormats.xlsx"))
306+
assert.NoError(t, err)
307+
w, err := f.workSheetReader("Sheet1")
308+
assert.NoError(t, err)
309+
w.ExtLst = &xlsxExtLst{Ext: fmt.Sprintf(`<ext uri="%s"><x14:conditionalFormattings></ext>`, ExtURIConditionalFormattings)}
310+
assert.EqualError(t, f.UnsetConditionalFormat("Sheet1", "Y1:Y10"), "XML syntax error on line 1: element <conditionalFormattings> closed by </ext>")
311+
assert.NoError(t, f.Close())
293312
// Test get multiple conditional formats
294313
f = NewFile()
295314
expected := []ConditionalFormatOptions{
@@ -310,9 +329,9 @@ func TestGetConditionalFormats(t *testing.T) {
310329
_, err = f.GetConditionalFormats("Sheet:1")
311330
assert.Equal(t, ErrSheetNameInvalid, err)
312331
// Test get conditional formats with invalid extension list characters
313-
ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml")
314-
assert.True(t, ok)
315-
ws.(*xlsxWorksheet).ExtLst = &xlsxExtLst{Ext: fmt.Sprintf(`<ext uri="%s"><x14:conditionalFormattings></ext>`, ExtURIConditionalFormattings)}
332+
ws, err := f.workSheetReader("Sheet1")
333+
assert.NoError(t, err)
334+
ws.ExtLst = &xlsxExtLst{Ext: fmt.Sprintf(`<ext uri="%s"><x14:conditionalFormattings></ext>`, ExtURIConditionalFormattings)}
316335
_, err = f.GetConditionalFormats("Sheet1")
317336
assert.EqualError(t, err, "XML syntax error on line 1: element <conditionalFormattings> closed by </ext>")
318337
}
@@ -329,6 +348,23 @@ func TestUnsetConditionalFormat(t *testing.T) {
329348
assert.EqualError(t, f.UnsetConditionalFormat("SheetN", "A1:A10"), "sheet SheetN does not exist")
330349
// Test unset conditional format with invalid sheet name
331350
assert.Equal(t, ErrSheetNameInvalid, f.UnsetConditionalFormat("Sheet:1", "A1:A10"))
351+
// Test unset conditional format from extLst
352+
assert.NoError(t, f.SetConditionalFormat("Sheet1", "B1:B10", []ConditionalFormatOptions{{Type: "icon_set", IconStyle: "3Stars"}}))
353+
assert.NoError(t, f.SetConditionalFormat("Sheet1", "C1:C10", []ConditionalFormatOptions{{Type: "icon_set", IconStyle: "5Boxes"}}))
354+
condFmts, err := f.GetConditionalFormats("Sheet1")
355+
assert.NoError(t, err)
356+
assert.Len(t, condFmts, 2)
357+
// Unset the first extLst conditional format
358+
assert.NoError(t, f.UnsetConditionalFormat("Sheet1", "B1:B10"))
359+
condFmts, err = f.GetConditionalFormats("Sheet1")
360+
assert.NoError(t, err)
361+
assert.Len(t, condFmts, 1)
362+
assert.NotNil(t, condFmts["C1:C10"])
363+
// Unset the last extLst conditional format
364+
assert.NoError(t, f.UnsetConditionalFormat("Sheet1", "C1:C10"))
365+
condFmts, err = f.GetConditionalFormats("Sheet1")
366+
assert.NoError(t, err)
367+
assert.Len(t, condFmts, 0)
332368
// Save spreadsheet by the given path
333369
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestUnsetConditionalFormat.xlsx")))
334370
}

xmlWorksheet.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -802,7 +802,7 @@ type decodeX14CfRule struct {
802802
Type string `xml:"type,attr,omitempty"`
803803
Priority int `xml:"priority,attr,omitempty"`
804804
StopIfTrue bool `xml:"stopIfTrue,attr,omitempty"`
805-
AboveAverage bool `xml:"aboveAverage,attr,omitempty"`
805+
AboveAverage *bool `xml:"aboveAverage,attr"`
806806
Percent bool `xml:"percent,attr,omitempty"`
807807
Bottom bool `xml:"bottom,attr,omitempty"`
808808
Operator string `xml:"operator,attr,omitempty"`
@@ -853,7 +853,7 @@ type decodeX14Cfvo struct {
853853
XMLName xml.Name `xml:"cfvo"`
854854
Type string `xml:"type,attr"`
855855
Gte *bool `xml:"gte,attr"`
856-
F string `xml:"xm:f"`
856+
F string `xml:"f"`
857857
ExtLst *xlsxExtLst `xml:"extLst"`
858858
}
859859

0 commit comments

Comments
 (0)