From c7acf4fafef429b67ca79f496d2709271c840dcd Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 11 Nov 2023 00:04:05 +0800 Subject: [PATCH] Support update data validations on inserting/deleting columns/rows --- adjust.go | 66 +++++++++++++++++++++++++++++-- adjust_test.go | 72 +++++++++++++++++++++++++++++++++- datavalidation.go | 89 ++++++++++++++++++++++++++++++++++-------- datavalidation_test.go | 3 +- picture_test.go | 4 +- pivotTable_test.go | 2 +- shape_test.go | 12 +++--- sheetpr_test.go | 8 ++-- sparkline_test.go | 30 +++++++------- stream_test.go | 28 ++++++------- styles_test.go | 10 ++--- table_test.go | 20 +++++----- vml_test.go | 6 +-- xmlWorksheet.go | 60 ++++++++++++++++++---------- 14 files changed, 308 insertions(+), 102 deletions(-) diff --git a/adjust.go b/adjust.go index 11da692..5cdb711 100644 --- a/adjust.go +++ b/adjust.go @@ -30,10 +30,13 @@ const ( ) // adjustHelperFunc defines functions to adjust helper. -var adjustHelperFunc = [8]func(*File, *xlsxWorksheet, string, adjustDirection, int, int, int) error{ +var adjustHelperFunc = [9]func(*File, *xlsxWorksheet, string, adjustDirection, int, int, int) error{ func(f *File, ws *xlsxWorksheet, sheet string, dir adjustDirection, num, offset, sheetID int) error { return f.adjustConditionalFormats(ws, sheet, dir, num, offset, sheetID) }, + func(f *File, ws *xlsxWorksheet, sheet string, dir adjustDirection, num, offset, sheetID int) error { + return f.adjustDataValidations(ws, sheet, dir, num, offset, sheetID) + }, func(f *File, ws *xlsxWorksheet, sheet string, dir adjustDirection, num, offset, sheetID int) error { return f.adjustDefinedNames(ws, sheet, dir, num, offset, sheetID) }, @@ -66,7 +69,7 @@ var adjustHelperFunc = [8]func(*File, *xlsxWorksheet, string, adjustDirection, i // row: Index number of the row we're inserting/deleting before // offset: Number of rows/column to insert/delete negative values indicate deletion // -// TODO: adjustComments, adjustDataValidations, adjustPageBreaks, adjustProtectedCells +// TODO: adjustComments, adjustPageBreaks, adjustProtectedCells func (f *File) adjustHelper(sheet string, dir adjustDirection, num, offset int) error { ws, err := f.workSheetReader(sheet) if err != nil { @@ -369,7 +372,10 @@ func (f *File) adjustFormulaOperand(sheet, sheetN string, keepRelative bool, tok sheetName, cell = tokens[0], tokens[1] operand = escapeSheetName(sheetName) + "!" } - if sheet != sheetN && sheet != sheetName { + if sheetName == "" { + sheetName = sheetN + } + if sheet != sheetName { return operand + cell, err } for _, r := range cell { @@ -804,6 +810,60 @@ func (f *File) adjustConditionalFormats(ws *xlsxWorksheet, sheet string, dir adj return nil } +// adjustDataValidations updates the range of data validations for the worksheet +// when inserting or deleting rows or columns. +func (f *File) adjustDataValidations(ws *xlsxWorksheet, sheet string, dir adjustDirection, num, offset, sheetID int) error { + for _, sheetN := range f.GetSheetList() { + worksheet, err := f.workSheetReader(sheetN) + if err != nil { + if err.Error() == newNotWorksheetError(sheetN).Error() { + continue + } + return err + } + if worksheet.DataValidations == nil { + return nil + } + for i := 0; i < len(worksheet.DataValidations.DataValidation); i++ { + dv := worksheet.DataValidations.DataValidation[i] + if dv == nil { + continue + } + if sheet == sheetN { + ref, del, err := f.adjustCellRef(dv.Sqref, dir, num, offset) + if err != nil { + return err + } + if del { + worksheet.DataValidations.DataValidation = append(worksheet.DataValidations.DataValidation[:i], + worksheet.DataValidations.DataValidation[i+1:]...) + i-- + continue + } + worksheet.DataValidations.DataValidation[i].Sqref = ref + } + if worksheet.DataValidations.DataValidation[i].Formula1 != nil { + formula := unescapeDataValidationFormula(worksheet.DataValidations.DataValidation[i].Formula1.Content) + if formula, err = f.adjustFormulaRef(sheet, sheetN, formula, false, dir, num, offset); err != nil { + return err + } + worksheet.DataValidations.DataValidation[i].Formula1 = &xlsxInnerXML{Content: formulaEscaper.Replace(formula)} + } + if worksheet.DataValidations.DataValidation[i].Formula2 != nil { + formula := unescapeDataValidationFormula(worksheet.DataValidations.DataValidation[i].Formula2.Content) + if formula, err = f.adjustFormulaRef(sheet, sheetN, formula, false, dir, num, offset); err != nil { + return err + } + worksheet.DataValidations.DataValidation[i].Formula2 = &xlsxInnerXML{Content: formulaEscaper.Replace(formula)} + } + } + if worksheet.DataValidations.Count = len(worksheet.DataValidations.DataValidation); worksheet.DataValidations.Count == 0 { + worksheet.DataValidations = nil + } + } + return nil +} + // adjustDrawings updates the starting anchor of the two cell anchor pictures // and charts object when inserting or deleting rows or columns. func (from *xlsxFrom) adjustDrawings(dir adjustDirection, num, offset int, editAs string) (bool, error) { diff --git a/adjust_test.go b/adjust_test.go index 80c1582..769affe 100644 --- a/adjust_test.go +++ b/adjust_test.go @@ -743,7 +743,7 @@ func TestAdjustFormula(t *testing.T) { assert.NoError(t, f.InsertRows("Sheet1", 2, 1)) formula, err = f.GetCellFormula("Sheet1", "B1") assert.NoError(t, err) - assert.Equal(t, "SUM('Sheet 1'!A3,A5)", formula) + assert.Equal(t, "SUM('Sheet 1'!A2,A5)", formula) f = NewFile() // Test adjust formula on insert col in the middle of the range @@ -993,6 +993,76 @@ func TestAdjustConditionalFormats(t *testing.T) { assert.NoError(t, f.RemoveCol("Sheet1", "B")) } +func TestAdjustDataValidations(t *testing.T) { + f := NewFile() + dv := NewDataValidation(true) + dv.Sqref = "B1" + assert.NoError(t, dv.SetDropList([]string{"1", "2", "3"})) + assert.NoError(t, f.AddDataValidation("Sheet1", dv)) + assert.NoError(t, f.RemoveCol("Sheet1", "B")) + dvs, err := f.GetDataValidations("Sheet1") + assert.NoError(t, err) + assert.Len(t, dvs, 0) + + assert.NoError(t, f.SetCellValue("Sheet1", "F2", 1)) + assert.NoError(t, f.SetCellValue("Sheet1", "F3", 2)) + dv = NewDataValidation(true) + dv.Sqref = "C2:D3" + dv.SetSqrefDropList("$F$2:$F$3") + assert.NoError(t, f.AddDataValidation("Sheet1", dv)) + + assert.NoError(t, f.AddChartSheet("Chart1", &Chart{Type: Line})) + _, err = f.NewSheet("Sheet2") + assert.NoError(t, err) + assert.NoError(t, f.SetSheetRow("Sheet2", "C1", &[]interface{}{1, 10})) + dv = NewDataValidation(true) + dv.Sqref = "C5:D6" + assert.NoError(t, dv.SetRange("Sheet2!C1", "Sheet2!D1", DataValidationTypeWhole, DataValidationOperatorBetween)) + dv.SetError(DataValidationErrorStyleStop, "error title", "error body") + assert.NoError(t, f.AddDataValidation("Sheet1", dv)) + assert.NoError(t, f.RemoveCol("Sheet1", "B")) + assert.NoError(t, f.RemoveCol("Sheet2", "B")) + dvs, err = f.GetDataValidations("Sheet1") + assert.NoError(t, err) + assert.Equal(t, "B2:C3", dvs[0].Sqref) + assert.Equal(t, "$E$2:$E$3", dvs[0].Formula1) + assert.Equal(t, "B5:C6", dvs[1].Sqref) + assert.Equal(t, "Sheet2!B1", dvs[1].Formula1) + assert.Equal(t, "Sheet2!C1", dvs[1].Formula2) + + dv = NewDataValidation(true) + dv.Sqref = "C8:D10" + assert.NoError(t, dv.SetDropList([]string{`A<`, `B>`, `C"`, "D\t", `E'`, `F`})) + assert.NoError(t, f.AddDataValidation("Sheet1", dv)) + assert.NoError(t, f.RemoveCol("Sheet1", "B")) + dvs, err = f.GetDataValidations("Sheet1") + assert.NoError(t, err) + assert.Equal(t, "\"A<,B>,C\",D\t,E',F\"", dvs[2].Formula1) + + dv = NewDataValidation(true) + dv.Sqref = "C5:D6" + assert.NoError(t, dv.SetRange("Sheet1!A1048576", "Sheet1!XFD1", DataValidationTypeWhole, DataValidationOperatorBetween)) + dv.SetError(DataValidationErrorStyleStop, "error title", "error body") + assert.NoError(t, f.AddDataValidation("Sheet1", dv)) + assert.Equal(t, ErrColumnNumber, f.InsertCols("Sheet1", "A", 1)) + assert.Equal(t, ErrMaxRows, f.InsertRows("Sheet1", 1, 1)) + + ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml") + assert.True(t, ok) + ws.(*xlsxWorksheet).DataValidations.DataValidation[0].Sqref = "-" + assert.Equal(t, newCellNameToCoordinatesError("-", newInvalidCellNameError("-")), f.RemoveCol("Sheet1", "B")) + + ws.(*xlsxWorksheet).DataValidations.DataValidation[0] = nil + assert.NoError(t, f.RemoveCol("Sheet1", "B")) + + ws.(*xlsxWorksheet).DataValidations = nil + assert.NoError(t, f.RemoveCol("Sheet1", "B")) + + f.Sheet.Delete("xl/worksheets/sheet1.xml") + f.Pkg.Store("xl/worksheets/sheet1.xml", MacintoshCyrillicCharset) + assert.EqualError(t, f.adjustDataValidations(nil, "Sheet1", columns, 0, 0, 1), "XML syntax error on line 1: invalid UTF-8") +} + func TestAdjustDrawings(t *testing.T) { f := NewFile() // Test add pictures to sheet with positioning diff --git a/datavalidation.go b/datavalidation.go index 56740e6..4d2f360 100644 --- a/datavalidation.go +++ b/datavalidation.go @@ -75,7 +75,11 @@ var ( `&`, `&`, `<`, `<`, `>`, `>`, - `"`, `""`, + ) + formulaUnescaper = strings.NewReplacer( + `&`, `&`, + `<`, `<`, + `>`, `>`, ) // dataValidationTypeMap defined supported data validation types. dataValidationTypeMap = map[DataValidationType]string{ @@ -101,11 +105,6 @@ var ( } ) -const ( - formula1Name = "formula1" - formula2Name = "formula2" -) - // NewDataValidation return data validation struct. func NewDataValidation(allowBlank bool) *DataValidation { return &DataValidation{ @@ -151,36 +150,40 @@ func (dv *DataValidation) SetDropList(keys []string) error { if MaxFieldLength < len(utf16.Encode([]rune(formula))) { return ErrDataValidationFormulaLength } - dv.Formula1 = fmt.Sprintf(`<%[2]s>"%[1]s"`, formulaEscaper.Replace(formula), formula1Name) dv.Type = dataValidationTypeMap[DataValidationTypeList] + if strings.HasPrefix(formula, "=") { + dv.Formula1 = formulaEscaper.Replace(formula) + return nil + } + dv.Formula1 = fmt.Sprintf(`"%s"`, strings.NewReplacer(`"`, `""`).Replace(formulaEscaper.Replace(formula))) return nil } // SetRange provides function to set data validation range in drop list, only // accepts int, float64, string or []string data type formula argument. func (dv *DataValidation) SetRange(f1, f2 interface{}, t DataValidationType, o DataValidationOperator) error { - genFormula := func(name string, val interface{}) (string, error) { + genFormula := func(val interface{}) (string, error) { var formula string switch v := val.(type) { case int: - formula = fmt.Sprintf("<%s>%d", name, v, name) + formula = fmt.Sprintf("%d", v) case float64: if math.Abs(v) > math.MaxFloat32 { return formula, ErrDataValidationRange } - formula = fmt.Sprintf("<%s>%.17g", name, v, name) + formula = fmt.Sprintf("%.17g", v) case string: - formula = fmt.Sprintf("<%s>%s", name, v, name) + formula = v default: return formula, ErrParameterInvalid } return formula, nil } - formula1, err := genFormula(formula1Name, f1) + formula1, err := genFormula(f1) if err != nil { return err } - formula2, err := genFormula(formula2Name, f2) + formula2, err := genFormula(f2) if err != nil { return err } @@ -205,7 +208,7 @@ func (dv *DataValidation) SetRange(f1, f2 interface{}, t DataValidationType, o D // dv.SetSqrefDropList("$E$1:$E$3") // err := f.AddDataValidation("Sheet1", dv) func (dv *DataValidation) SetSqrefDropList(sqref string) { - dv.Formula1 = fmt.Sprintf("%s", sqref) + dv.Formula1 = sqref dv.Type = dataValidationTypeMap[DataValidationTypeList] } @@ -256,7 +259,27 @@ func (f *File) AddDataValidation(sheet string, dv *DataValidation) error { if nil == ws.DataValidations { ws.DataValidations = new(xlsxDataValidations) } - ws.DataValidations.DataValidation = append(ws.DataValidations.DataValidation, dv) + dataValidation := &xlsxDataValidation{ + 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 != "" { + dataValidation.Formula1 = &xlsxInnerXML{Content: dv.Formula1} + } + if dv.Formula2 != "" { + dataValidation.Formula2 = &xlsxInnerXML{Content: dv.Formula2} + } + ws.DataValidations.DataValidation = append(ws.DataValidations.DataValidation, dataValidation) ws.DataValidations.Count = len(ws.DataValidations.DataValidation) return err } @@ -270,7 +293,33 @@ func (f *File) GetDataValidations(sheet string) ([]*DataValidation, error) { if ws.DataValidations == nil || len(ws.DataValidations.DataValidation) == 0 { return nil, err } - return ws.DataValidations.DataValidation, err + 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) + } + dvs = append(dvs, dataValidation) + } + } + return dvs, err } // DeleteDataValidation delete data validation by given worksheet name and @@ -351,3 +400,11 @@ func (f *File) squashSqref(cells [][]int) []string { } return append(refs, ref) } + +// unescapeDataValidationFormula returns unescaped data validation formula. +func unescapeDataValidationFormula(val string) string { + if strings.HasPrefix(val, "\"") { // Text detection + return strings.NewReplacer(`""`, `"`).Replace(formulaUnescaper.Replace(val)) + } + return formulaUnescaper.Replace(val) +} diff --git a/datavalidation_test.go b/datavalidation_test.go index 2f45fd9..c331ebe 100644 --- a/datavalidation_test.go +++ b/datavalidation_test.go @@ -71,6 +71,7 @@ func TestDataValidation(t *testing.T) { dv.Sqref = "A5:B6" for _, listValid := range [][]string{ {"1", "2", "3"}, + {"=A1"}, {strings.Repeat("&", MaxFieldLength)}, {strings.Repeat("\u4E00", MaxFieldLength)}, {strings.Repeat("\U0001F600", 100), strings.Repeat("\u4E01", 50), "<&>"}, @@ -82,7 +83,7 @@ func TestDataValidation(t *testing.T) { assert.NotEqual(t, "", dv.Formula1, "Formula1 should not be empty for valid input %v", listValid) } - assert.Equal(t, `"A<,B>,C"",D ,E',F"`, dv.Formula1) + assert.Equal(t, `"A<,B>,C"",D ,E',F"`, dv.Formula1) assert.NoError(t, f.AddDataValidation("Sheet1", dv)) dataValidations, err = f.GetDataValidations("Sheet1") diff --git a/picture_test.go b/picture_test.go index 5422046..b98941f 100644 --- a/picture_test.go +++ b/picture_test.go @@ -294,9 +294,9 @@ func TestDeletePicture(t *testing.T) { // Test delete picture on not exists worksheet assert.EqualError(t, f.DeletePicture("SheetN", "A1"), "sheet SheetN does not exist") // Test delete picture with invalid sheet name - assert.EqualError(t, f.DeletePicture("Sheet:1", "A1"), ErrSheetNameInvalid.Error()) + assert.Equal(t, ErrSheetNameInvalid, f.DeletePicture("Sheet:1", "A1")) // Test delete picture with invalid coordinates - assert.EqualError(t, f.DeletePicture("Sheet1", ""), newCellNameToCoordinatesError("", newInvalidCellNameError("")).Error()) + assert.Equal(t, newCellNameToCoordinatesError("", newInvalidCellNameError("")), f.DeletePicture("Sheet1", "")) assert.NoError(t, f.Close()) // Test delete picture on no chart worksheet assert.NoError(t, NewFile().DeletePicture("Sheet1", "A1")) diff --git a/pivotTable_test.go b/pivotTable_test.go index 6cbdf55..49bc7d9 100644 --- a/pivotTable_test.go +++ b/pivotTable_test.go @@ -174,7 +174,7 @@ func TestPivotTable(t *testing.T) { })) // Test empty pivot table options - assert.EqualError(t, f.AddPivotTable(nil), ErrParameterRequired.Error()) + assert.Equal(t, ErrParameterRequired, f.AddPivotTable(nil)) // Test add pivot table with custom name which exceeds the max characters limit assert.Equal(t, ErrNameLength, f.AddPivotTable(&PivotTableOptions{ DataRange: "dataRange", diff --git a/shape_test.go b/shape_test.go index 2a9fa08..57c7501 100644 --- a/shape_test.go +++ b/shape_test.go @@ -42,16 +42,16 @@ func TestAddShape(t *testing.T) { }, }, ), "sheet Sheet3 does not exist") - assert.EqualError(t, f.AddShape("Sheet3", nil), ErrParameterInvalid.Error()) - assert.EqualError(t, f.AddShape("Sheet1", &Shape{Cell: "A1"}), ErrParameterInvalid.Error()) - assert.EqualError(t, f.AddShape("Sheet1", &Shape{ + assert.Equal(t, ErrParameterInvalid, f.AddShape("Sheet3", nil)) + assert.Equal(t, ErrParameterInvalid, f.AddShape("Sheet1", &Shape{Cell: "A1"})) + assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), f.AddShape("Sheet1", &Shape{ Cell: "A", Type: "rect", Paragraph: []RichTextRun{ {Text: "Rectangle", Font: &Font{Color: "CD5C5C"}}, {Text: "Shape", Font: &Font{Bold: true, Color: "2980B9"}}, }, - }), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) + })) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddShape1.xlsx"))) // Test add first shape for given sheet @@ -79,14 +79,14 @@ func TestAddShape(t *testing.T) { })) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddShape2.xlsx"))) // Test add shape with invalid sheet name - assert.EqualError(t, f.AddShape("Sheet:1", &Shape{ + assert.Equal(t, ErrSheetNameInvalid, f.AddShape("Sheet:1", &Shape{ Cell: "A30", Type: "rect", Paragraph: []RichTextRun{ {Text: "Rectangle", Font: &Font{Color: "CD5C5C"}}, {Text: "Shape", Font: &Font{Bold: true, Color: "2980B9"}}, }, - }), ErrSheetNameInvalid.Error()) + })) // Test add shape with unsupported charset style sheet f.Styles = nil f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset) diff --git a/sheetpr_test.go b/sheetpr_test.go index 5491e78..63b7323 100644 --- a/sheetpr_test.go +++ b/sheetpr_test.go @@ -30,7 +30,7 @@ func TestSetPageMargins(t *testing.T) { // Test set page margins on not exists worksheet assert.EqualError(t, f.SetPageMargins("SheetN", nil), "sheet SheetN does not exist") // Test set page margins with invalid sheet name - assert.EqualError(t, f.SetPageMargins("Sheet:1", nil), ErrSheetNameInvalid.Error()) + assert.Equal(t, ErrSheetNameInvalid, f.SetPageMargins("Sheet:1", nil)) } func TestGetPageMargins(t *testing.T) { @@ -40,7 +40,7 @@ func TestGetPageMargins(t *testing.T) { assert.EqualError(t, err, "sheet SheetN does not exist") // Test get page margins with invalid sheet name _, err = f.GetPageMargins("Sheet:1") - assert.EqualError(t, err, ErrSheetNameInvalid.Error()) + assert.Equal(t, ErrSheetNameInvalid, err) } func TestSetSheetProps(t *testing.T) { @@ -88,7 +88,7 @@ func TestSetSheetProps(t *testing.T) { // Test set worksheet properties on not exists worksheet assert.EqualError(t, f.SetSheetProps("SheetN", nil), "sheet SheetN does not exist") // Test set worksheet properties with invalid sheet name - assert.EqualError(t, f.SetSheetProps("Sheet:1", nil), ErrSheetNameInvalid.Error()) + assert.Equal(t, ErrSheetNameInvalid, f.SetSheetProps("Sheet:1", nil)) } func TestGetSheetProps(t *testing.T) { @@ -98,5 +98,5 @@ func TestGetSheetProps(t *testing.T) { assert.EqualError(t, err, "sheet SheetN does not exist") // Test get worksheet properties with invalid sheet name _, err = f.GetSheetProps("Sheet:1") - assert.EqualError(t, err, ErrSheetNameInvalid.Error()) + assert.Equal(t, ErrSheetNameInvalid, err) } diff --git a/sparkline_test.go b/sparkline_test.go index 048ed2b..27da1e6 100644 --- a/sparkline_test.go +++ b/sparkline_test.go @@ -224,46 +224,46 @@ func TestAddSparkline(t *testing.T) { Range: []string{"Sheet2!A3:E3"}, }), "sheet SheetN does not exist") - assert.EqualError(t, f.AddSparkline("Sheet1", nil), ErrParameterRequired.Error()) + assert.Equal(t, ErrParameterRequired, f.AddSparkline("Sheet1", nil)) // Test add sparkline with invalid sheet name - assert.EqualError(t, f.AddSparkline("Sheet:1", &SparklineOptions{ + assert.Equal(t, ErrSheetNameInvalid, f.AddSparkline("Sheet:1", &SparklineOptions{ Location: []string{"F3"}, Range: []string{"Sheet2!A3:E3"}, Type: "win_loss", Negative: true, - }), ErrSheetNameInvalid.Error()) + })) - assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOptions{ + assert.Equal(t, ErrSparklineLocation, f.AddSparkline("Sheet1", &SparklineOptions{ Range: []string{"Sheet2!A3:E3"}, - }), ErrSparklineLocation.Error()) + })) - assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOptions{ + assert.Equal(t, ErrSparklineRange, f.AddSparkline("Sheet1", &SparklineOptions{ Location: []string{"F3"}, - }), ErrSparklineRange.Error()) + })) - assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOptions{ + assert.Equal(t, ErrSparkline, f.AddSparkline("Sheet1", &SparklineOptions{ Location: []string{"F2", "F3"}, Range: []string{"Sheet2!A3:E3"}, - }), ErrSparkline.Error()) + })) - assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOptions{ + assert.Equal(t, ErrSparklineType, f.AddSparkline("Sheet1", &SparklineOptions{ Location: []string{"F3"}, Range: []string{"Sheet2!A3:E3"}, Type: "unknown_type", - }), ErrSparklineType.Error()) + })) - assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOptions{ + assert.Equal(t, ErrSparklineStyle, f.AddSparkline("Sheet1", &SparklineOptions{ Location: []string{"F3"}, Range: []string{"Sheet2!A3:E3"}, Style: -1, - }), ErrSparklineStyle.Error()) + })) - assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOptions{ + assert.Equal(t, ErrSparklineStyle, f.AddSparkline("Sheet1", &SparklineOptions{ Location: []string{"F3"}, Range: []string{"Sheet2!A3:E3"}, Style: -1, - }), ErrSparklineStyle.Error()) + })) // Test creating a conditional format with existing extension lists ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml") assert.True(t, ok) diff --git a/stream_test.go b/stream_test.go index 2f68d62..da3fd14 100644 --- a/stream_test.go +++ b/stream_test.go @@ -73,7 +73,7 @@ func TestStreamWriter(t *testing.T) { })) assert.NoError(t, streamWriter.SetRow("A6", []interface{}{time.Now()})) assert.NoError(t, streamWriter.SetRow("A7", nil, RowOpts{Height: 20, Hidden: true, StyleID: styleID})) - assert.EqualError(t, streamWriter.SetRow("A8", nil, RowOpts{Height: MaxRowHeight + 1}), ErrMaxRowHeight.Error()) + assert.Equal(t, ErrMaxRowHeight, streamWriter.SetRow("A8", nil, RowOpts{Height: MaxRowHeight + 1})) for rowID := 10; rowID <= 51200; rowID++ { row := make([]interface{}, 50) @@ -158,11 +158,11 @@ func TestStreamSetColWidth(t *testing.T) { streamWriter, err := file.NewStreamWriter("Sheet1") assert.NoError(t, err) assert.NoError(t, streamWriter.SetColWidth(3, 2, 20)) - assert.ErrorIs(t, streamWriter.SetColWidth(0, 3, 20), ErrColumnNumber) - assert.ErrorIs(t, streamWriter.SetColWidth(MaxColumns+1, 3, 20), ErrColumnNumber) - assert.EqualError(t, streamWriter.SetColWidth(1, 3, MaxColumnWidth+1), ErrColumnWidth.Error()) + assert.Equal(t, ErrColumnNumber, streamWriter.SetColWidth(0, 3, 20)) + assert.Equal(t, ErrColumnNumber, streamWriter.SetColWidth(MaxColumns+1, 3, 20)) + assert.Equal(t, ErrColumnWidth, streamWriter.SetColWidth(1, 3, MaxColumnWidth+1)) assert.NoError(t, streamWriter.SetRow("A1", []interface{}{"A", "B", "C"})) - assert.ErrorIs(t, streamWriter.SetColWidth(2, 3, 20), ErrStreamSetColWidth) + assert.Equal(t, ErrStreamSetColWidth, streamWriter.SetColWidth(2, 3, 20)) } func TestStreamSetPanes(t *testing.T) { @@ -183,9 +183,9 @@ func TestStreamSetPanes(t *testing.T) { streamWriter, err := file.NewStreamWriter("Sheet1") assert.NoError(t, err) assert.NoError(t, streamWriter.SetPanes(paneOpts)) - assert.EqualError(t, streamWriter.SetPanes(nil), ErrParameterInvalid.Error()) + assert.Equal(t, ErrParameterInvalid, streamWriter.SetPanes(nil)) assert.NoError(t, streamWriter.SetRow("A1", []interface{}{"A", "B", "C"})) - assert.ErrorIs(t, streamWriter.SetPanes(paneOpts), ErrStreamSetPanes) + assert.Equal(t, ErrStreamSetPanes, streamWriter.SetPanes(paneOpts)) } func TestStreamTable(t *testing.T) { @@ -220,10 +220,10 @@ func TestStreamTable(t *testing.T) { assert.NoError(t, streamWriter.AddTable(&Table{Range: "A1:C1"})) // Test add table with illegal cell reference - assert.EqualError(t, streamWriter.AddTable(&Table{Range: "A:B1"}), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) - assert.EqualError(t, streamWriter.AddTable(&Table{Range: "A1:B"}), newCellNameToCoordinatesError("B", newInvalidCellNameError("B")).Error()) + assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), streamWriter.AddTable(&Table{Range: "A:B1"})) + assert.Equal(t, newCellNameToCoordinatesError("B", newInvalidCellNameError("B")), streamWriter.AddTable(&Table{Range: "A1:B"})) // Test add table with invalid table name - assert.EqualError(t, streamWriter.AddTable(&Table{Range: "A:B1", Name: "1Table"}), newInvalidNameError("1Table").Error()) + assert.Equal(t, newInvalidNameError("1Table"), streamWriter.AddTable(&Table{Range: "A:B1", Name: "1Table"})) // Test add table with unsupported charset content types file.ContentTypes = nil file.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset) @@ -239,7 +239,7 @@ func TestStreamMergeCells(t *testing.T) { assert.NoError(t, err) assert.NoError(t, streamWriter.MergeCell("A1", "D1")) // Test merge cells with illegal cell reference - assert.EqualError(t, streamWriter.MergeCell("A", "D1"), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) + assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), streamWriter.MergeCell("A", "D1")) assert.NoError(t, streamWriter.Flush()) // Save spreadsheet by the given path assert.NoError(t, file.SaveAs(filepath.Join("test", "TestStreamMergeCells.xlsx"))) @@ -270,7 +270,7 @@ func TestNewStreamWriter(t *testing.T) { assert.EqualError(t, err, "sheet SheetN does not exist") // Test new stream write with invalid sheet name _, err = file.NewStreamWriter("Sheet:1") - assert.EqualError(t, err, ErrSheetNameInvalid.Error()) + assert.Equal(t, ErrSheetNameInvalid, err) } func TestStreamMarshalAttrs(t *testing.T) { @@ -288,10 +288,10 @@ func TestStreamSetRow(t *testing.T) { }() streamWriter, err := file.NewStreamWriter("Sheet1") assert.NoError(t, err) - assert.EqualError(t, streamWriter.SetRow("A", []interface{}{}), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) + assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), streamWriter.SetRow("A", []interface{}{})) // Test set row with non-ascending row number assert.NoError(t, streamWriter.SetRow("A1", []interface{}{})) - assert.EqualError(t, streamWriter.SetRow("A1", []interface{}{}), newStreamSetRowError(1).Error()) + assert.Equal(t, newStreamSetRowError(1), streamWriter.SetRow("A1", []interface{}{})) // Test set row with unsupported charset workbook file.WorkBook = nil file.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset) diff --git a/styles_test.go b/styles_test.go index 87efb08..c3ba1e9 100644 --- a/styles_test.go +++ b/styles_test.go @@ -190,7 +190,7 @@ func TestSetConditionalFormat(t *testing.T) { ws.(*xlsxWorksheet).ExtLst = &xlsxExtLst{Ext: ""} assert.EqualError(t, f.SetConditionalFormat("Sheet1", "A1:A2", condFmts), "XML syntax error on line 1: element closed by ") // Test creating a conditional format with invalid icon set style - assert.EqualError(t, f.SetConditionalFormat("Sheet1", "A1:A2", []ConditionalFormatOptions{{Type: "icon_set", IconStyle: "unknown"}}), ErrParameterInvalid.Error()) + assert.Equal(t, ErrParameterInvalid, f.SetConditionalFormat("Sheet1", "A1:A2", []ConditionalFormatOptions{{Type: "icon_set", IconStyle: "unknown"}})) // Test unsupported conditional formatting rule types for _, val := range []string{ "date", @@ -235,7 +235,7 @@ func TestGetConditionalFormats(t *testing.T) { assert.EqualError(t, err, "sheet SheetN does not exist") // Test get conditional formats with invalid sheet name _, err = f.GetConditionalFormats("Sheet:1") - assert.EqualError(t, err, ErrSheetNameInvalid.Error()) + assert.Equal(t, ErrSheetNameInvalid, err) } func TestUnsetConditionalFormat(t *testing.T) { @@ -249,7 +249,7 @@ func TestUnsetConditionalFormat(t *testing.T) { // Test unset conditional format on not exists worksheet assert.EqualError(t, f.UnsetConditionalFormat("SheetN", "A1:A10"), "sheet SheetN does not exist") // Test unset conditional format with invalid sheet name - assert.EqualError(t, f.UnsetConditionalFormat("Sheet:1", "A1:A10"), ErrSheetNameInvalid.Error()) + assert.Equal(t, ErrSheetNameInvalid, f.UnsetConditionalFormat("Sheet:1", "A1:A10")) // Save spreadsheet by the given path assert.NoError(t, f.SaveAs(filepath.Join("test", "TestUnsetConditionalFormat.xlsx"))) } @@ -469,9 +469,9 @@ func TestSetCellStyle(t *testing.T) { // Test set cell style on not exists worksheet assert.EqualError(t, f.SetCellStyle("SheetN", "A1", "A2", 1), "sheet SheetN does not exist") // Test set cell style with invalid style ID - assert.EqualError(t, f.SetCellStyle("Sheet1", "A1", "A2", -1), newInvalidStyleID(-1).Error()) + assert.Equal(t, newInvalidStyleID(-1), f.SetCellStyle("Sheet1", "A1", "A2", -1)) // Test set cell style with not exists style ID - assert.EqualError(t, f.SetCellStyle("Sheet1", "A1", "A2", 10), newInvalidStyleID(10).Error()) + assert.Equal(t, newInvalidStyleID(10), f.SetCellStyle("Sheet1", "A1", "A2", 10)) // Test set cell style with unsupported charset style sheet f.Styles = nil f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset) diff --git a/table_test.go b/table_test.go index 69e3ad0..cda9cb0 100644 --- a/table_test.go +++ b/table_test.go @@ -45,7 +45,7 @@ func TestAddTable(t *testing.T) { assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddTable.xlsx"))) // Test add table with invalid sheet name - assert.EqualError(t, f.AddTable("Sheet:1", &Table{Range: "B26:A21"}), ErrSheetNameInvalid.Error()) + assert.Equal(t, ErrSheetNameInvalid, f.AddTable("Sheet:1", &Table{Range: "B26:A21"})) // Test addTable with illegal cell reference f = NewFile() assert.Equal(t, newCoordinatesToCellNameError(0, 0), f.addTable("sheet1", "", 0, 0, 0, 0, 0, nil)) @@ -64,13 +64,13 @@ func TestAddTable(t *testing.T) { {name: "\u0f5f\u0fb3\u0f0b\u0f21", err: newInvalidNameError("\u0f5f\u0fb3\u0f0b\u0f21")}, {name: strings.Repeat("c", MaxFieldLength+1), err: ErrNameLength}, } { - assert.EqualError(t, f.AddTable("Sheet1", &Table{ + assert.Equal(t, cases.err, f.AddTable("Sheet1", &Table{ Range: "A1:B2", Name: cases.name, - }), cases.err.Error()) - assert.EqualError(t, f.SetDefinedName(&DefinedName{ + })) + assert.Equal(t, cases.err, f.SetDefinedName(&DefinedName{ Name: cases.name, RefersTo: "Sheet1!$A$2:$D$5", - }), cases.err.Error()) + })) } // Test check duplicate table name with unsupported charset table parts f = NewFile() @@ -115,9 +115,9 @@ func TestDeleteTable(t *testing.T) { assert.NoError(t, f.DeleteTable("Table2")) assert.NoError(t, f.DeleteTable("Table1")) // Test delete table with invalid table name - assert.EqualError(t, f.DeleteTable("Table 1"), newInvalidNameError("Table 1").Error()) + assert.Equal(t, newInvalidNameError("Table 1"), f.DeleteTable("Table 1")) // Test delete table with no exist table name - assert.EqualError(t, f.DeleteTable("Table"), newNoExistTableError("Table").Error()) + assert.Equal(t, newNoExistTableError("Table"), f.DeleteTable("Table")) // Test delete table with unsupported charset f.Sheet.Delete("xl/worksheets/sheet1.xml") f.Pkg.Store("xl/worksheets/sheet1.xml", MacintoshCyrillicCharset) @@ -164,10 +164,10 @@ func TestAutoFilter(t *testing.T) { } // Test add auto filter with invalid sheet name - assert.EqualError(t, f.AutoFilter("Sheet:1", "A1:B1", nil), ErrSheetNameInvalid.Error()) + assert.Equal(t, ErrSheetNameInvalid, f.AutoFilter("Sheet:1", "A1:B1", nil)) // Test add auto filter with illegal cell reference - assert.EqualError(t, f.AutoFilter("Sheet1", "A:B1", nil), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) - assert.EqualError(t, f.AutoFilter("Sheet1", "A1:B", nil), newCellNameToCoordinatesError("B", newInvalidCellNameError("B")).Error()) + assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), f.AutoFilter("Sheet1", "A:B1", nil)) + assert.Equal(t, newCellNameToCoordinatesError("B", newInvalidCellNameError("B")), f.AutoFilter("Sheet1", "A1:B", nil)) // Test add auto filter with unsupported charset workbook f.WorkBook = nil f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset) diff --git a/vml_test.go b/vml_test.go index 8e38fbe..50e9a04 100644 --- a/vml_test.go +++ b/vml_test.go @@ -35,7 +35,7 @@ func TestAddComment(t *testing.T) { // Test add comment on not exists worksheet assert.EqualError(t, f.AddComment("SheetN", Comment{Cell: "B7", Author: "Excelize", Paragraph: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment."}}}), "sheet SheetN does not exist") // Test add comment on with illegal cell reference - assert.EqualError(t, f.AddComment("Sheet1", Comment{Cell: "A", Author: "Excelize", Paragraph: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment."}}}), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) + assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), f.AddComment("Sheet1", Comment{Cell: "A", Author: "Excelize", Paragraph: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment."}}})) comments, err := f.GetComments("Sheet1") assert.NoError(t, err) assert.Len(t, comments, 2) @@ -57,7 +57,7 @@ func TestAddComment(t *testing.T) { assert.Len(t, comments, 0) // Test add comments with invalid sheet name - assert.EqualError(t, f.AddComment("Sheet:1", Comment{Cell: "A1", Author: "Excelize", Text: "This is a comment."}), ErrSheetNameInvalid.Error()) + assert.Equal(t, ErrSheetNameInvalid, f.AddComment("Sheet:1", Comment{Cell: "A1", Author: "Excelize", Text: "This is a comment."})) // Test add comments with unsupported charset f.Comments["xl/comments2.xml"] = nil @@ -105,7 +105,7 @@ func TestDeleteComment(t *testing.T) { assert.Len(t, comments, 0) // Test delete comment with invalid sheet name - assert.EqualError(t, f.DeleteComment("Sheet:1", "A1"), ErrSheetNameInvalid.Error()) + assert.Equal(t, ErrSheetNameInvalid, f.DeleteComment("Sheet:1", "A1")) // Test delete all comments in a worksheet assert.NoError(t, f.DeleteComment("Sheet2", "A41")) assert.NoError(t, f.DeleteComment("Sheet2", "C41")) diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 07085bb..6eb860f 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -419,31 +419,31 @@ type xlsxMergeCells struct { // xlsxDataValidations expresses all data validation information for cells in a // sheet which have data validation features applied. type xlsxDataValidations struct { - XMLName xml.Name `xml:"dataValidations"` - Count int `xml:"count,attr,omitempty"` - DisablePrompts bool `xml:"disablePrompts,attr,omitempty"` - XWindow int `xml:"xWindow,attr,omitempty"` - YWindow int `xml:"yWindow,attr,omitempty"` - DataValidation []*DataValidation `xml:"dataValidation"` + XMLName xml.Name `xml:"dataValidations"` + Count int `xml:"count,attr,omitempty"` + DisablePrompts bool `xml:"disablePrompts,attr,omitempty"` + XWindow int `xml:"xWindow,attr,omitempty"` + YWindow int `xml:"yWindow,attr,omitempty"` + DataValidation []*xlsxDataValidation `xml:"dataValidation"` } // DataValidation directly maps the single item of data validation defined // on a range of the worksheet. -type DataValidation 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,attr"` - Type string `xml:"type,attr,omitempty"` - Formula1 string `xml:",innerxml"` - Formula2 string `xml:",innerxml"` +type xlsxDataValidation 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,attr"` + Type string `xml:"type,attr,omitempty"` + Formula1 *xlsxInnerXML `xml:"formula1"` + Formula2 *xlsxInnerXML `xml:"formula2"` } // xlsxC collection represents a cell in the worksheet. Information about the @@ -835,6 +835,24 @@ type xlsxX14Sparkline struct { Sqref string `xml:"xm:sqref"` } +// DataValidation directly maps the settings of the data validation rule. +type DataValidation struct { + AllowBlank bool + Error *string + ErrorStyle *string + ErrorTitle *string + Operator string + Prompt *string + PromptTitle *string + ShowDropDown bool + ShowErrorMessage bool + ShowInputMessage bool + Sqref string + Type string + Formula1 string + Formula2 string +} + // SparklineOptions directly maps the settings of the sparkline. type SparklineOptions struct { Location []string