From 5af7e2ec13809a7a4254a277cfbebdf835b5f523 Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 5 Mar 2024 21:07:59 +0800 Subject: [PATCH] This closes #1835, support get data validations which storage in the extension lists --- cell_test.go | 2 +- datavalidation.go | 149 +++++++++++++++-------------------------- datavalidation_test.go | 42 +++++++----- excelize.go | 11 --- templates.go | 2 +- xmlWorkbook.go | 37 ---------- xmlWorksheet.go | 1 + 7 files changed, 84 insertions(+), 160 deletions(-) diff --git a/cell_test.go b/cell_test.go index 42472cfd72..61178c29a3 100644 --- a/cell_test.go +++ b/cell_test.go @@ -91,7 +91,7 @@ func TestConcurrency(t *testing.T) { // Concurrency add data validation dv := NewDataValidation(true) dv.Sqref = fmt.Sprintf("A%d:B%d", val, val) - dv.SetRange(10, 20, DataValidationTypeWhole, DataValidationOperatorGreaterThan) + assert.NoError(t, dv.SetRange(10, 20, DataValidationTypeWhole, DataValidationOperatorGreaterThan)) dv.SetInput(fmt.Sprintf("title:%d", val), strconv.Itoa(val)) assert.NoError(t, f.AddDataValidation("Sheet1", dv)) // Concurrency delete data validation with reference sequence diff --git a/datavalidation.go b/datavalidation.go index 1d746114d2..40ffd1950f 100644 --- a/datavalidation.go +++ b/datavalidation.go @@ -13,6 +13,7 @@ package excelize import ( "fmt" + "io" "math" "strings" "unicode/utf16" @@ -293,110 +294,70 @@ func (f *File) GetDataValidations(sheet string) ([]*DataValidation, error) { if err != nil { return nil, err } - if ws.DataValidations == nil || len(ws.DataValidations.DataValidation) == 0 { - return nil, err + var ( + dataValidations []*DataValidation + decodeExtLst = new(decodeExtLst) + decodeDataValidations *xlsxDataValidations + ext *xlsxExt + ) + if ws.DataValidations != nil { + dataValidations = append(dataValidations, getDataValidations(ws.DataValidations)...) } - var dvs []*DataValidation - for _, dv := range ws.DataValidations.DataValidation { - if dv != nil { - dataValidation := &DataValidation{ - AllowBlank: dv.AllowBlank, - Error: dv.Error, - ErrorStyle: dv.ErrorStyle, - ErrorTitle: dv.ErrorTitle, - Operator: dv.Operator, - Prompt: dv.Prompt, - PromptTitle: dv.PromptTitle, - ShowDropDown: dv.ShowDropDown, - ShowErrorMessage: dv.ShowErrorMessage, - ShowInputMessage: dv.ShowInputMessage, - Sqref: dv.Sqref, - Type: dv.Type, - } - if dv.Formula1 != nil { - dataValidation.Formula1 = unescapeDataValidationFormula(dv.Formula1.Content) - } - if dv.Formula2 != nil { - dataValidation.Formula2 = unescapeDataValidationFormula(dv.Formula2.Content) + if ws.ExtLst != nil { + if err = f.xmlNewDecoder(strings.NewReader("" + ws.ExtLst.Ext + "")). + Decode(decodeExtLst); err != nil && err != io.EOF { + return dataValidations, err + } + for _, ext = range decodeExtLst.Ext { + if ext.URI == ExtURIDataValidations { + decodeDataValidations = new(xlsxDataValidations) + _ = f.xmlNewDecoder(strings.NewReader(ext.Content)).Decode(decodeDataValidations) + dataValidations = append(dataValidations, getDataValidations(decodeDataValidations)...) } - dvs = append(dvs, dataValidation) } } - return dvs, err + return dataValidations, err } -// GetAllLinkedDataValidations The GetDataValidations method sometimes fails to retrieve all data validations completely, including those linked to other pages. -// This method serves as a temporary fix for this issue, allowing the retrieval of associated data validation content from other sheets through the Ext structure. -// Please note that this is an interim solution and the Ext structure may contain more than just the data validation components. -func (f *File) GetAllLinkedDataValidations(sheet string) ([]*DataValidation, error) { - ws, err := f.workSheetReader(sheet) - if err != nil { - return nil, err - } - if ws.DataValidations == nil || len(ws.DataValidations.DataValidation) == 0 { - return nil, err +// getDataValidations returns data validations list by given worksheet data +// validations. +func getDataValidations(dvs *xlsxDataValidations) []*DataValidation { + if dvs == nil { + return nil } - var dvs []*DataValidation - if ws.ExtLst != nil { - extStruct, err := ParseXMLToStruct(strings.NewReader(ws.ExtLst.Ext)) - if err != nil { - fmt.Println("Error parsing XML to struct:", err) - } else { - if extStruct != nil && extStruct.LinkDataValidations != nil { - for _, v := range extStruct.LinkDataValidations.DataValidation { - linkDataValidation := &xlsxDataValidation{ - AllowBlank: v.AllowBlank, - Error: v.Error, - ErrorStyle: v.ErrorStyle, - ErrorTitle: v.ErrorTitle, - Operator: v.Operator, - Prompt: v.Prompt, - PromptTitle: v.PromptTitle, - ShowDropDown: v.ShowDropDown, - ShowErrorMessage: v.ShowErrorMessage, - ShowInputMessage: v.ShowInputMessage, - Sqref: v.Sqref, - Type: v.Type, - } - if v.Formula1 != nil { - linkDataValidation.Formula1 = &xlsxInnerXML{Content: v.Formula1.F} - } - if v.Formula2 != nil { - linkDataValidation.Formula2 = &xlsxInnerXML{Content: v.Formula2.F} - } - ws.DataValidations.DataValidation = append(ws.DataValidations.DataValidation, linkDataValidation) - } - - } + var dataValidations []*DataValidation + for _, dv := range dvs.DataValidation { + if dv == nil { + continue } - //ws.DataValidations.DataValidation = append(ws.DataValidations.DataValidation, ws.ExtLst.DataValidations.DataValidation...) - } - for _, dv := range ws.DataValidations.DataValidation { - if dv != nil { - dataValidation := &DataValidation{ - AllowBlank: dv.AllowBlank, - Error: dv.Error, - ErrorStyle: dv.ErrorStyle, - ErrorTitle: dv.ErrorTitle, - Operator: dv.Operator, - Prompt: dv.Prompt, - PromptTitle: dv.PromptTitle, - ShowDropDown: dv.ShowDropDown, - ShowErrorMessage: dv.ShowErrorMessage, - ShowInputMessage: dv.ShowInputMessage, - Sqref: dv.Sqref, - Type: dv.Type, - } - if dv.Formula1 != nil { - dataValidation.Formula1 = unescapeDataValidationFormula(dv.Formula1.Content) - } - if dv.Formula2 != nil { - dataValidation.Formula2 = unescapeDataValidationFormula(dv.Formula2.Content) - } - dvs = append(dvs, dataValidation) + dataValidation := &DataValidation{ + AllowBlank: dv.AllowBlank, + Error: dv.Error, + ErrorStyle: dv.ErrorStyle, + ErrorTitle: dv.ErrorTitle, + Operator: dv.Operator, + Prompt: dv.Prompt, + PromptTitle: dv.PromptTitle, + ShowDropDown: dv.ShowDropDown, + ShowErrorMessage: dv.ShowErrorMessage, + ShowInputMessage: dv.ShowInputMessage, + Sqref: dv.Sqref, + Type: dv.Type, + } + if dv.Formula1 != nil { + dataValidation.Formula1 = unescapeDataValidationFormula(dv.Formula1.Content) + } + if dv.Formula2 != nil { + dataValidation.Formula2 = unescapeDataValidationFormula(dv.Formula2.Content) + } + if dv.XMSqref != "" { + dataValidation.Sqref = dv.XMSqref + dataValidation.Formula1 = strings.TrimSuffix(strings.TrimPrefix(dataValidation.Formula1, ""), "") + dataValidation.Formula2 = strings.TrimSuffix(strings.TrimPrefix(dataValidation.Formula2, ""), "") } + dataValidations = append(dataValidations, dataValidation) } - return dvs, err + return dataValidations } // DeleteDataValidation delete data validation by given worksheet name and diff --git a/datavalidation_test.go b/datavalidation_test.go index 14b664c1ad..7508ba33b6 100644 --- a/datavalidation_test.go +++ b/datavalidation_test.go @@ -12,7 +12,7 @@ package excelize import ( - "log" + "fmt" "math" "path/filepath" "strings" @@ -21,21 +21,6 @@ import ( "github.com/stretchr/testify/assert" ) -func TestGetA(t *testing.T) { - file, err := OpenFile("file2.xlsx") - if err != nil { - panic(err) - } - validations, err := file.GetAllLinkedDataValidations("5") - if err != nil { - panic(err) - } - - for i := range validations { - log.Println(validations[i].Type) - } -} - func TestDataValidation(t *testing.T) { resultFile := filepath.Join("test", "TestDataValidation.xlsx") @@ -120,6 +105,31 @@ func TestDataValidation(t *testing.T) { dataValidations, err = f.GetDataValidations("Sheet1") assert.NoError(t, err) assert.Equal(t, []*DataValidation(nil), dataValidations) + + // Test get data validations which storage in the extension lists + f = NewFile() + ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml") + assert.True(t, ok) + ws.(*xlsxWorksheet).ExtLst = &xlsxExtLst{Ext: fmt.Sprintf(`Sheet1!$B$1:$B$5A7:B8`, ExtURIDataValidations, NameSpaceSpreadSheetX14.Value)} + dataValidations, err = f.GetDataValidations("Sheet1") + assert.NoError(t, err) + assert.Equal(t, []*DataValidation{ + { + AllowBlank: true, + Type: "list", + Formula1: "Sheet1!$B$1:$B$5", + Sqref: "A7:B8", + }, + }, dataValidations) + + // Test get data validations with invalid extension list characters + ws.(*xlsxWorksheet).ExtLst = &xlsxExtLst{Ext: fmt.Sprintf(``, ExtURIDataValidations, NameSpaceSpreadSheetX14.Value)} + _, err = f.GetDataValidations("Sheet1") + assert.EqualError(t, err, "XML syntax error on line 1: element closed by ") + + // Test get validations without validations + assert.Nil(t, getDataValidations(nil)) + assert.Nil(t, getDataValidations(&xlsxDataValidations{DataValidation: []*xlsxDataValidation{nil}})) } func TestDataValidationError(t *testing.T) { diff --git a/excelize.go b/excelize.go index 0d9631f299..87ef22dd52 100644 --- a/excelize.go +++ b/excelize.go @@ -588,14 +588,3 @@ func (f *File) setContentTypePartProjectExtensions(contentType string) error { } return err } - -// ParseXMLToStruct takes an XML reader and parses it into the Ext struct. -func ParseXMLToStruct(r io.Reader) (*Ext, error) { - var ext Ext - decoder := xml.NewDecoder(r) - err := decoder.Decode(&ext) - if err != nil { - return nil, err - } - return &ext, nil -} diff --git a/templates.go b/templates.go index 60c895d856..0f21be54bc 100644 --- a/templates.go +++ b/templates.go @@ -103,7 +103,7 @@ const ( ExtURIConditionalFormattingRuleID = "{B025F937-C7B1-47D3-B67F-A62EFF666E3E}" ExtURIConditionalFormattings = "{78C0D931-6437-407d-A8EE-F0AAD7539E65}" ExtURIDataModel = "{FCE2AD5D-F65C-4FA6-A056-5C36A1767C68}" - ExtURIDataValidations = "{CCE6A557-97BC-4B89-ADB6-D9C93CAAB3DF}" + ExtURIDataValidations = "{CCE6A557-97BC-4b89-ADB6-D9C93CAAB3DF}" ExtURIDrawingBlip = "{28A0092B-C50C-407E-A947-70E740481C1C}" ExtURIExternalLinkPr = "{FCE6A71B-6B00-49CD-AB44-F6B1AE7CDE65}" ExtURIIgnoredErrors = "{01252117-D84E-4E92-8308-4BE1C098FCBB}" diff --git a/xmlWorkbook.go b/xmlWorkbook.go index b48aa195fa..6d489e9e83 100644 --- a/xmlWorkbook.go +++ b/xmlWorkbook.go @@ -231,43 +231,6 @@ type xlsxExt struct { xmlns []xml.Attr } -// Ext The Ext structure is used to parse XML and obtain relevant content from the *xlsxExtLst structure. -type Ext struct { - XMLName xml.Name `xml:"ext"` - URI string `xml:"uri,attr"` - LinkDataValidations *LinkDataValidations `xml:"http://schemas.microsoft.com/office/spreadsheetml/2009/9/main dataValidations"` -} - -// LinkDataValidations used to Ext -type LinkDataValidations struct { - Count int `xml:"count,attr"` - DataValidation []*LinkDataValidation `xml:"dataValidation"` -} - -// LinkDataValidation used to LinkDataValidations -type LinkDataValidation struct { - AllowBlank bool `xml:"allowBlank,attr"` - Error *string `xml:"error,attr"` - ErrorStyle *string `xml:"errorStyle,attr"` - ErrorTitle *string `xml:"errorTitle,attr"` - Operator string `xml:"operator,attr,omitempty"` - Prompt *string `xml:"prompt,attr"` - PromptTitle *string `xml:"promptTitle,attr"` - ShowDropDown bool `xml:"showDropDown,attr,omitempty"` - ShowErrorMessage bool `xml:"showErrorMessage,attr,omitempty"` - ShowInputMessage bool `xml:"showInputMessage,attr,omitempty"` - Sqref string `xml:"sqref"` - UID string `xml:"uid,attr"` - Type string `xml:"type,attr"` - Formula1 *Formula `xml:"formula1"` - Formula2 *Formula `xml:"formula2"` -} - -// Formula used to LinkDataValidation -type Formula struct { - F string `xml:"f"` -} - // xlsxAlternateContent is a container for a sequence of multiple // representations of a given piece of content. The program reading the file // should only process one of these, and the one chosen should be based on diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 83e08054f5..c5e06b8293 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -441,6 +441,7 @@ type xlsxDataValidation struct { ShowErrorMessage bool `xml:"showErrorMessage,attr,omitempty"` ShowInputMessage bool `xml:"showInputMessage,attr,omitempty"` Sqref string `xml:"sqref,attr"` + XMSqref string `xml:"sqref,omitempty"` Type string `xml:"type,attr,omitempty"` Formula1 *xlsxInnerXML `xml:"formula1"` Formula2 *xlsxInnerXML `xml:"formula2"`