Skip to content

Commit

Permalink
Support update data validations on inserting/deleting columns/rows
Browse files Browse the repository at this point in the history
  • Loading branch information
xuri committed Nov 10, 2023
1 parent e014a8b commit c7acf4f
Show file tree
Hide file tree
Showing 14 changed files with 308 additions and 102 deletions.
66 changes: 63 additions & 3 deletions adjust.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
},
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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) {
Expand Down
72 changes: 71 additions & 1 deletion adjust_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
89 changes: 73 additions & 16 deletions datavalidation.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,11 @@ var (
`&`, `&amp;`,
`<`, `&lt;`,
`>`, `&gt;`,
`"`, `""`,
)
formulaUnescaper = strings.NewReplacer(
`&amp;`, `&`,
`&lt;`, `<`,
`&gt;`, `>`,
)
// dataValidationTypeMap defined supported data validation types.
dataValidationTypeMap = map[DataValidationType]string{
Expand All @@ -101,11 +105,6 @@ var (
}
)

const (
formula1Name = "formula1"
formula2Name = "formula2"
)

// NewDataValidation return data validation struct.
func NewDataValidation(allowBlank bool) *DataValidation {
return &DataValidation{
Expand Down Expand Up @@ -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"</%[2]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</%s>", name, v, name)
formula = fmt.Sprintf("%d", v)
case float64:
if math.Abs(v) > math.MaxFloat32 {
return formula, ErrDataValidationRange
}
formula = fmt.Sprintf("<%s>%.17g</%s>", name, v, name)
formula = fmt.Sprintf("%.17g", v)
case string:
formula = fmt.Sprintf("<%s>%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
}
Expand All @@ -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("<formula1>%s</formula1>", sqref)
dv.Formula1 = sqref
dv.Type = dataValidationTypeMap[DataValidationTypeList]
}

Expand Down Expand Up @@ -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
}
Expand All @@ -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
Expand Down Expand Up @@ -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)
}
3 changes: 2 additions & 1 deletion datavalidation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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), "<&>"},
Expand All @@ -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, `<formula1>"A&lt;,B&gt;,C"",D ,E',F"</formula1>`, dv.Formula1)
assert.Equal(t, `"A&lt;,B&gt;,C"",D ,E',F"`, dv.Formula1)
assert.NoError(t, f.AddDataValidation("Sheet1", dv))

dataValidations, err = f.GetDataValidations("Sheet1")
Expand Down
4 changes: 2 additions & 2 deletions picture_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand Down
2 changes: 1 addition & 1 deletion pivotTable_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading

0 comments on commit c7acf4f

Please sign in to comment.