Skip to content

Commit c7acf4f

Browse files
committed
Support update data validations on inserting/deleting columns/rows
1 parent e014a8b commit c7acf4f

14 files changed

+308
-102
lines changed

adjust.go

+63-3
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,13 @@ const (
3030
)
3131

3232
// adjustHelperFunc defines functions to adjust helper.
33-
var adjustHelperFunc = [8]func(*File, *xlsxWorksheet, string, adjustDirection, int, int, int) error{
33+
var adjustHelperFunc = [9]func(*File, *xlsxWorksheet, string, adjustDirection, int, int, int) error{
3434
func(f *File, ws *xlsxWorksheet, sheet string, dir adjustDirection, num, offset, sheetID int) error {
3535
return f.adjustConditionalFormats(ws, sheet, dir, num, offset, sheetID)
3636
},
37+
func(f *File, ws *xlsxWorksheet, sheet string, dir adjustDirection, num, offset, sheetID int) error {
38+
return f.adjustDataValidations(ws, sheet, dir, num, offset, sheetID)
39+
},
3740
func(f *File, ws *xlsxWorksheet, sheet string, dir adjustDirection, num, offset, sheetID int) error {
3841
return f.adjustDefinedNames(ws, sheet, dir, num, offset, sheetID)
3942
},
@@ -66,7 +69,7 @@ var adjustHelperFunc = [8]func(*File, *xlsxWorksheet, string, adjustDirection, i
6669
// row: Index number of the row we're inserting/deleting before
6770
// offset: Number of rows/column to insert/delete negative values indicate deletion
6871
//
69-
// TODO: adjustComments, adjustDataValidations, adjustPageBreaks, adjustProtectedCells
72+
// TODO: adjustComments, adjustPageBreaks, adjustProtectedCells
7073
func (f *File) adjustHelper(sheet string, dir adjustDirection, num, offset int) error {
7174
ws, err := f.workSheetReader(sheet)
7275
if err != nil {
@@ -369,7 +372,10 @@ func (f *File) adjustFormulaOperand(sheet, sheetN string, keepRelative bool, tok
369372
sheetName, cell = tokens[0], tokens[1]
370373
operand = escapeSheetName(sheetName) + "!"
371374
}
372-
if sheet != sheetN && sheet != sheetName {
375+
if sheetName == "" {
376+
sheetName = sheetN
377+
}
378+
if sheet != sheetName {
373379
return operand + cell, err
374380
}
375381
for _, r := range cell {
@@ -804,6 +810,60 @@ func (f *File) adjustConditionalFormats(ws *xlsxWorksheet, sheet string, dir adj
804810
return nil
805811
}
806812

813+
// adjustDataValidations updates the range of data validations for the worksheet
814+
// when inserting or deleting rows or columns.
815+
func (f *File) adjustDataValidations(ws *xlsxWorksheet, sheet string, dir adjustDirection, num, offset, sheetID int) error {
816+
for _, sheetN := range f.GetSheetList() {
817+
worksheet, err := f.workSheetReader(sheetN)
818+
if err != nil {
819+
if err.Error() == newNotWorksheetError(sheetN).Error() {
820+
continue
821+
}
822+
return err
823+
}
824+
if worksheet.DataValidations == nil {
825+
return nil
826+
}
827+
for i := 0; i < len(worksheet.DataValidations.DataValidation); i++ {
828+
dv := worksheet.DataValidations.DataValidation[i]
829+
if dv == nil {
830+
continue
831+
}
832+
if sheet == sheetN {
833+
ref, del, err := f.adjustCellRef(dv.Sqref, dir, num, offset)
834+
if err != nil {
835+
return err
836+
}
837+
if del {
838+
worksheet.DataValidations.DataValidation = append(worksheet.DataValidations.DataValidation[:i],
839+
worksheet.DataValidations.DataValidation[i+1:]...)
840+
i--
841+
continue
842+
}
843+
worksheet.DataValidations.DataValidation[i].Sqref = ref
844+
}
845+
if worksheet.DataValidations.DataValidation[i].Formula1 != nil {
846+
formula := unescapeDataValidationFormula(worksheet.DataValidations.DataValidation[i].Formula1.Content)
847+
if formula, err = f.adjustFormulaRef(sheet, sheetN, formula, false, dir, num, offset); err != nil {
848+
return err
849+
}
850+
worksheet.DataValidations.DataValidation[i].Formula1 = &xlsxInnerXML{Content: formulaEscaper.Replace(formula)}
851+
}
852+
if worksheet.DataValidations.DataValidation[i].Formula2 != nil {
853+
formula := unescapeDataValidationFormula(worksheet.DataValidations.DataValidation[i].Formula2.Content)
854+
if formula, err = f.adjustFormulaRef(sheet, sheetN, formula, false, dir, num, offset); err != nil {
855+
return err
856+
}
857+
worksheet.DataValidations.DataValidation[i].Formula2 = &xlsxInnerXML{Content: formulaEscaper.Replace(formula)}
858+
}
859+
}
860+
if worksheet.DataValidations.Count = len(worksheet.DataValidations.DataValidation); worksheet.DataValidations.Count == 0 {
861+
worksheet.DataValidations = nil
862+
}
863+
}
864+
return nil
865+
}
866+
807867
// adjustDrawings updates the starting anchor of the two cell anchor pictures
808868
// and charts object when inserting or deleting rows or columns.
809869
func (from *xlsxFrom) adjustDrawings(dir adjustDirection, num, offset int, editAs string) (bool, error) {

adjust_test.go

+71-1
Original file line numberDiff line numberDiff line change
@@ -743,7 +743,7 @@ func TestAdjustFormula(t *testing.T) {
743743
assert.NoError(t, f.InsertRows("Sheet1", 2, 1))
744744
formula, err = f.GetCellFormula("Sheet1", "B1")
745745
assert.NoError(t, err)
746-
assert.Equal(t, "SUM('Sheet 1'!A3,A5)", formula)
746+
assert.Equal(t, "SUM('Sheet 1'!A2,A5)", formula)
747747

748748
f = NewFile()
749749
// Test adjust formula on insert col in the middle of the range
@@ -993,6 +993,76 @@ func TestAdjustConditionalFormats(t *testing.T) {
993993
assert.NoError(t, f.RemoveCol("Sheet1", "B"))
994994
}
995995

996+
func TestAdjustDataValidations(t *testing.T) {
997+
f := NewFile()
998+
dv := NewDataValidation(true)
999+
dv.Sqref = "B1"
1000+
assert.NoError(t, dv.SetDropList([]string{"1", "2", "3"}))
1001+
assert.NoError(t, f.AddDataValidation("Sheet1", dv))
1002+
assert.NoError(t, f.RemoveCol("Sheet1", "B"))
1003+
dvs, err := f.GetDataValidations("Sheet1")
1004+
assert.NoError(t, err)
1005+
assert.Len(t, dvs, 0)
1006+
1007+
assert.NoError(t, f.SetCellValue("Sheet1", "F2", 1))
1008+
assert.NoError(t, f.SetCellValue("Sheet1", "F3", 2))
1009+
dv = NewDataValidation(true)
1010+
dv.Sqref = "C2:D3"
1011+
dv.SetSqrefDropList("$F$2:$F$3")
1012+
assert.NoError(t, f.AddDataValidation("Sheet1", dv))
1013+
1014+
assert.NoError(t, f.AddChartSheet("Chart1", &Chart{Type: Line}))
1015+
_, err = f.NewSheet("Sheet2")
1016+
assert.NoError(t, err)
1017+
assert.NoError(t, f.SetSheetRow("Sheet2", "C1", &[]interface{}{1, 10}))
1018+
dv = NewDataValidation(true)
1019+
dv.Sqref = "C5:D6"
1020+
assert.NoError(t, dv.SetRange("Sheet2!C1", "Sheet2!D1", DataValidationTypeWhole, DataValidationOperatorBetween))
1021+
dv.SetError(DataValidationErrorStyleStop, "error title", "error body")
1022+
assert.NoError(t, f.AddDataValidation("Sheet1", dv))
1023+
assert.NoError(t, f.RemoveCol("Sheet1", "B"))
1024+
assert.NoError(t, f.RemoveCol("Sheet2", "B"))
1025+
dvs, err = f.GetDataValidations("Sheet1")
1026+
assert.NoError(t, err)
1027+
assert.Equal(t, "B2:C3", dvs[0].Sqref)
1028+
assert.Equal(t, "$E$2:$E$3", dvs[0].Formula1)
1029+
assert.Equal(t, "B5:C6", dvs[1].Sqref)
1030+
assert.Equal(t, "Sheet2!B1", dvs[1].Formula1)
1031+
assert.Equal(t, "Sheet2!C1", dvs[1].Formula2)
1032+
1033+
dv = NewDataValidation(true)
1034+
dv.Sqref = "C8:D10"
1035+
assert.NoError(t, dv.SetDropList([]string{`A<`, `B>`, `C"`, "D\t", `E'`, `F`}))
1036+
assert.NoError(t, f.AddDataValidation("Sheet1", dv))
1037+
assert.NoError(t, f.RemoveCol("Sheet1", "B"))
1038+
dvs, err = f.GetDataValidations("Sheet1")
1039+
assert.NoError(t, err)
1040+
assert.Equal(t, "\"A<,B>,C\",D\t,E',F\"", dvs[2].Formula1)
1041+
1042+
dv = NewDataValidation(true)
1043+
dv.Sqref = "C5:D6"
1044+
assert.NoError(t, dv.SetRange("Sheet1!A1048576", "Sheet1!XFD1", DataValidationTypeWhole, DataValidationOperatorBetween))
1045+
dv.SetError(DataValidationErrorStyleStop, "error title", "error body")
1046+
assert.NoError(t, f.AddDataValidation("Sheet1", dv))
1047+
assert.Equal(t, ErrColumnNumber, f.InsertCols("Sheet1", "A", 1))
1048+
assert.Equal(t, ErrMaxRows, f.InsertRows("Sheet1", 1, 1))
1049+
1050+
ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml")
1051+
assert.True(t, ok)
1052+
ws.(*xlsxWorksheet).DataValidations.DataValidation[0].Sqref = "-"
1053+
assert.Equal(t, newCellNameToCoordinatesError("-", newInvalidCellNameError("-")), f.RemoveCol("Sheet1", "B"))
1054+
1055+
ws.(*xlsxWorksheet).DataValidations.DataValidation[0] = nil
1056+
assert.NoError(t, f.RemoveCol("Sheet1", "B"))
1057+
1058+
ws.(*xlsxWorksheet).DataValidations = nil
1059+
assert.NoError(t, f.RemoveCol("Sheet1", "B"))
1060+
1061+
f.Sheet.Delete("xl/worksheets/sheet1.xml")
1062+
f.Pkg.Store("xl/worksheets/sheet1.xml", MacintoshCyrillicCharset)
1063+
assert.EqualError(t, f.adjustDataValidations(nil, "Sheet1", columns, 0, 0, 1), "XML syntax error on line 1: invalid UTF-8")
1064+
}
1065+
9961066
func TestAdjustDrawings(t *testing.T) {
9971067
f := NewFile()
9981068
// Test add pictures to sheet with positioning

datavalidation.go

+73-16
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,11 @@ var (
7575
`&`, `&amp;`,
7676
`<`, `&lt;`,
7777
`>`, `&gt;`,
78-
`"`, `""`,
78+
)
79+
formulaUnescaper = strings.NewReplacer(
80+
`&amp;`, `&`,
81+
`&lt;`, `<`,
82+
`&gt;`, `>`,
7983
)
8084
// dataValidationTypeMap defined supported data validation types.
8185
dataValidationTypeMap = map[DataValidationType]string{
@@ -101,11 +105,6 @@ var (
101105
}
102106
)
103107

104-
const (
105-
formula1Name = "formula1"
106-
formula2Name = "formula2"
107-
)
108-
109108
// NewDataValidation return data validation struct.
110109
func NewDataValidation(allowBlank bool) *DataValidation {
111110
return &DataValidation{
@@ -151,36 +150,40 @@ func (dv *DataValidation) SetDropList(keys []string) error {
151150
if MaxFieldLength < len(utf16.Encode([]rune(formula))) {
152151
return ErrDataValidationFormulaLength
153152
}
154-
dv.Formula1 = fmt.Sprintf(`<%[2]s>"%[1]s"</%[2]s>`, formulaEscaper.Replace(formula), formula1Name)
155153
dv.Type = dataValidationTypeMap[DataValidationTypeList]
154+
if strings.HasPrefix(formula, "=") {
155+
dv.Formula1 = formulaEscaper.Replace(formula)
156+
return nil
157+
}
158+
dv.Formula1 = fmt.Sprintf(`"%s"`, strings.NewReplacer(`"`, `""`).Replace(formulaEscaper.Replace(formula)))
156159
return nil
157160
}
158161

159162
// SetRange provides function to set data validation range in drop list, only
160163
// accepts int, float64, string or []string data type formula argument.
161164
func (dv *DataValidation) SetRange(f1, f2 interface{}, t DataValidationType, o DataValidationOperator) error {
162-
genFormula := func(name string, val interface{}) (string, error) {
165+
genFormula := func(val interface{}) (string, error) {
163166
var formula string
164167
switch v := val.(type) {
165168
case int:
166-
formula = fmt.Sprintf("<%s>%d</%s>", name, v, name)
169+
formula = fmt.Sprintf("%d", v)
167170
case float64:
168171
if math.Abs(v) > math.MaxFloat32 {
169172
return formula, ErrDataValidationRange
170173
}
171-
formula = fmt.Sprintf("<%s>%.17g</%s>", name, v, name)
174+
formula = fmt.Sprintf("%.17g", v)
172175
case string:
173-
formula = fmt.Sprintf("<%s>%s</%s>", name, v, name)
176+
formula = v
174177
default:
175178
return formula, ErrParameterInvalid
176179
}
177180
return formula, nil
178181
}
179-
formula1, err := genFormula(formula1Name, f1)
182+
formula1, err := genFormula(f1)
180183
if err != nil {
181184
return err
182185
}
183-
formula2, err := genFormula(formula2Name, f2)
186+
formula2, err := genFormula(f2)
184187
if err != nil {
185188
return err
186189
}
@@ -205,7 +208,7 @@ func (dv *DataValidation) SetRange(f1, f2 interface{}, t DataValidationType, o D
205208
// dv.SetSqrefDropList("$E$1:$E$3")
206209
// err := f.AddDataValidation("Sheet1", dv)
207210
func (dv *DataValidation) SetSqrefDropList(sqref string) {
208-
dv.Formula1 = fmt.Sprintf("<formula1>%s</formula1>", sqref)
211+
dv.Formula1 = sqref
209212
dv.Type = dataValidationTypeMap[DataValidationTypeList]
210213
}
211214

@@ -256,7 +259,27 @@ func (f *File) AddDataValidation(sheet string, dv *DataValidation) error {
256259
if nil == ws.DataValidations {
257260
ws.DataValidations = new(xlsxDataValidations)
258261
}
259-
ws.DataValidations.DataValidation = append(ws.DataValidations.DataValidation, dv)
262+
dataValidation := &xlsxDataValidation{
263+
AllowBlank: dv.AllowBlank,
264+
Error: dv.Error,
265+
ErrorStyle: dv.ErrorStyle,
266+
ErrorTitle: dv.ErrorTitle,
267+
Operator: dv.Operator,
268+
Prompt: dv.Prompt,
269+
PromptTitle: dv.PromptTitle,
270+
ShowDropDown: dv.ShowDropDown,
271+
ShowErrorMessage: dv.ShowErrorMessage,
272+
ShowInputMessage: dv.ShowInputMessage,
273+
Sqref: dv.Sqref,
274+
Type: dv.Type,
275+
}
276+
if dv.Formula1 != "" {
277+
dataValidation.Formula1 = &xlsxInnerXML{Content: dv.Formula1}
278+
}
279+
if dv.Formula2 != "" {
280+
dataValidation.Formula2 = &xlsxInnerXML{Content: dv.Formula2}
281+
}
282+
ws.DataValidations.DataValidation = append(ws.DataValidations.DataValidation, dataValidation)
260283
ws.DataValidations.Count = len(ws.DataValidations.DataValidation)
261284
return err
262285
}
@@ -270,7 +293,33 @@ func (f *File) GetDataValidations(sheet string) ([]*DataValidation, error) {
270293
if ws.DataValidations == nil || len(ws.DataValidations.DataValidation) == 0 {
271294
return nil, err
272295
}
273-
return ws.DataValidations.DataValidation, err
296+
var dvs []*DataValidation
297+
for _, dv := range ws.DataValidations.DataValidation {
298+
if dv != nil {
299+
dataValidation := &DataValidation{
300+
AllowBlank: dv.AllowBlank,
301+
Error: dv.Error,
302+
ErrorStyle: dv.ErrorStyle,
303+
ErrorTitle: dv.ErrorTitle,
304+
Operator: dv.Operator,
305+
Prompt: dv.Prompt,
306+
PromptTitle: dv.PromptTitle,
307+
ShowDropDown: dv.ShowDropDown,
308+
ShowErrorMessage: dv.ShowErrorMessage,
309+
ShowInputMessage: dv.ShowInputMessage,
310+
Sqref: dv.Sqref,
311+
Type: dv.Type,
312+
}
313+
if dv.Formula1 != nil {
314+
dataValidation.Formula1 = unescapeDataValidationFormula(dv.Formula1.Content)
315+
}
316+
if dv.Formula2 != nil {
317+
dataValidation.Formula2 = unescapeDataValidationFormula(dv.Formula2.Content)
318+
}
319+
dvs = append(dvs, dataValidation)
320+
}
321+
}
322+
return dvs, err
274323
}
275324

276325
// DeleteDataValidation delete data validation by given worksheet name and
@@ -351,3 +400,11 @@ func (f *File) squashSqref(cells [][]int) []string {
351400
}
352401
return append(refs, ref)
353402
}
403+
404+
// unescapeDataValidationFormula returns unescaped data validation formula.
405+
func unescapeDataValidationFormula(val string) string {
406+
if strings.HasPrefix(val, "\"") { // Text detection
407+
return strings.NewReplacer(`""`, `"`).Replace(formulaUnescaper.Replace(val))
408+
}
409+
return formulaUnescaper.Replace(val)
410+
}

datavalidation_test.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ func TestDataValidation(t *testing.T) {
7171
dv.Sqref = "A5:B6"
7272
for _, listValid := range [][]string{
7373
{"1", "2", "3"},
74+
{"=A1"},
7475
{strings.Repeat("&", MaxFieldLength)},
7576
{strings.Repeat("\u4E00", MaxFieldLength)},
7677
{strings.Repeat("\U0001F600", 100), strings.Repeat("\u4E01", 50), "<&>"},
@@ -82,7 +83,7 @@ func TestDataValidation(t *testing.T) {
8283
assert.NotEqual(t, "", dv.Formula1,
8384
"Formula1 should not be empty for valid input %v", listValid)
8485
}
85-
assert.Equal(t, `<formula1>"A&lt;,B&gt;,C"",D ,E',F"</formula1>`, dv.Formula1)
86+
assert.Equal(t, `"A&lt;,B&gt;,C"",D ,E',F"`, dv.Formula1)
8687
assert.NoError(t, f.AddDataValidation("Sheet1", dv))
8788

8889
dataValidations, err = f.GetDataValidations("Sheet1")

picture_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -294,9 +294,9 @@ func TestDeletePicture(t *testing.T) {
294294
// Test delete picture on not exists worksheet
295295
assert.EqualError(t, f.DeletePicture("SheetN", "A1"), "sheet SheetN does not exist")
296296
// Test delete picture with invalid sheet name
297-
assert.EqualError(t, f.DeletePicture("Sheet:1", "A1"), ErrSheetNameInvalid.Error())
297+
assert.Equal(t, ErrSheetNameInvalid, f.DeletePicture("Sheet:1", "A1"))
298298
// Test delete picture with invalid coordinates
299-
assert.EqualError(t, f.DeletePicture("Sheet1", ""), newCellNameToCoordinatesError("", newInvalidCellNameError("")).Error())
299+
assert.Equal(t, newCellNameToCoordinatesError("", newInvalidCellNameError("")), f.DeletePicture("Sheet1", ""))
300300
assert.NoError(t, f.Close())
301301
// Test delete picture on no chart worksheet
302302
assert.NoError(t, NewFile().DeletePicture("Sheet1", "A1"))

pivotTable_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ func TestPivotTable(t *testing.T) {
174174
}))
175175

176176
// Test empty pivot table options
177-
assert.EqualError(t, f.AddPivotTable(nil), ErrParameterRequired.Error())
177+
assert.Equal(t, ErrParameterRequired, f.AddPivotTable(nil))
178178
// Test add pivot table with custom name which exceeds the max characters limit
179179
assert.Equal(t, ErrNameLength, f.AddPivotTable(&PivotTableOptions{
180180
DataRange: "dataRange",

0 commit comments

Comments
 (0)