Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: AddDataValidation() and DeleteDataValidation() are concurrency safe, and add some unit test and code optimization. #1828

Merged
merged 6 commits into from
Mar 1, 2024
66 changes: 48 additions & 18 deletions datavalidation.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,31 @@ func NewDataValidation(allowBlank bool) *DataValidation {
}
}

// newXlsxDataValidation is a internal function to create xlsxDataValidation by given data validation object.
func newXlsxDataValidation(dv *DataValidation) *xlsxDataValidation {
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}
}
return dataValidation
}

// SetError set error notice.
func (dv *DataValidation) SetError(style DataValidationErrorStyle, title, msg string) {
dv.Error = &msg
Expand Down Expand Up @@ -256,30 +281,33 @@ func (f *File) AddDataValidation(sheet string, dv *DataValidation) error {
if err != nil {
return err
}
f.mu.Lock()
JackMin1314 marked this conversation as resolved.
Show resolved Hide resolved
defer f.mu.Unlock()
JackMin1314 marked this conversation as resolved.
Show resolved Hide resolved
if nil == ws.DataValidations {
ws.DataValidations = new(xlsxDataValidations)
}
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,
dataValidation := newXlsxDataValidation(dv)
ws.DataValidations.DataValidation = append(ws.DataValidations.DataValidation, dataValidation)
ws.DataValidations.Count = len(ws.DataValidations.DataValidation)
return err
}

// BatchAddDataValidation is a function supports batch append data validation to the worksheet, reference to AddDataValidation().
xuri marked this conversation as resolved.
Show resolved Hide resolved
func (f *File) BatchAddDataValidation(sheet string, dvs []*DataValidation) error {
ws, err := f.workSheetReader(sheet)
if err != nil {
return err
}
if dv.Formula1 != "" {
dataValidation.Formula1 = &xlsxInnerXML{Content: dv.Formula1}
f.mu.Lock()
defer f.mu.Unlock()
if nil == ws.DataValidations {
ws.DataValidations = new(xlsxDataValidations)
}
if dv.Formula2 != "" {
dataValidation.Formula2 = &xlsxInnerXML{Content: dv.Formula2}
dataValidations := make([]*xlsxDataValidation, len(dvs))
for i := 0; i < len(dvs); i++ {
dataValidations[i] = newXlsxDataValidation(dvs[i])
}
ws.DataValidations.DataValidation = append(ws.DataValidations.DataValidation, dataValidation)
ws.DataValidations.DataValidation = append(ws.DataValidations.DataValidation, dataValidations...)
ws.DataValidations.Count = len(ws.DataValidations.DataValidation)
return err
}
Expand Down Expand Up @@ -330,6 +358,8 @@ func (f *File) DeleteDataValidation(sheet string, sqref ...string) error {
if err != nil {
return err
}
f.mu.Lock()
defer f.mu.Unlock()
if ws.DataValidations == nil {
return nil
}
Expand Down
121 changes: 121 additions & 0 deletions datavalidation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,12 @@
package excelize

import (
"fmt"
"math"
"path/filepath"
"strconv"
"strings"
"sync"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -106,6 +109,124 @@ func TestDataValidation(t *testing.T) {
assert.Equal(t, []*DataValidation(nil), dataValidations)
}

func TestConcurrentAddDataValidation(t *testing.T) {
xuri marked this conversation as resolved.
Show resolved Hide resolved
var (
resultFile = filepath.Join("test", "TestConcurrentAddDataValidation.xlsx")
f = NewFile()
sheet1 = "Sheet1"
dataValidationLen = 1000
)

// data validation list
dvs := make([]*DataValidation, dataValidationLen)
for i := 0; i < dataValidationLen; i++ {
dvi := NewDataValidation(true)
dvi.Sqref = fmt.Sprintf("A%d:B%d", i+1, i+1)
dvi.SetRange(10, 20, DataValidationTypeWhole, DataValidationOperatorGreaterThan)
dvi.SetInput(fmt.Sprintf("title:%d", i+1), strconv.Itoa(i+1))
dvs[i] = dvi
}
assert.Len(t, dvs, dataValidationLen)
// simulated concurrency
var wg sync.WaitGroup
wg.Add(dataValidationLen)
for i := 0; i < dataValidationLen; i++ {
go func(i int) {
f.AddDataValidation(sheet1, dvs[i])
wg.Done()
}(i)
}
wg.Wait()
// Test the length of data validation after concurrent
dataValidations, err := f.GetDataValidations(sheet1)
assert.NoError(t, err)
assert.Len(t, dataValidations, dataValidationLen)
assert.NoError(t, f.SaveAs(resultFile))

// Test the length of data validation after calling DeleteDataValidation()
f.DeleteDataValidation(sheet1)
dataValidations, err = f.GetDataValidations(sheet1)
assert.NoError(t, err)
assert.Len(t, dataValidations, 0)
// Test the length of data validation after calling BatchAddDataValidation()
f.BatchAddDataValidation(sheet1, dvs)
dataValidations, err = f.GetDataValidations(sheet1)
assert.NoError(t, err)
assert.Len(t, dataValidations, dataValidationLen)
assert.NoError(t, f.SaveAs(resultFile))
}

func TestBatchAddDataValidation(t *testing.T) {
resultFile := filepath.Join("test", "TestBatchAddDataValidation.xlsx")
f := NewFile()
// data validation 1
dv1 := NewDataValidation(true)
dv1.Sqref = "A1:B2"
assert.NoError(t, dv1.SetRange(10, 20, DataValidationTypeWhole, DataValidationOperatorBetween))
dv1.SetError(DataValidationErrorStyleStop, "error title", "error body")
dv1.SetError(DataValidationErrorStyleWarning, "error title", "error body")
dv1.SetError(DataValidationErrorStyleInformation, "error title", "error body")
// data validation 2
dv2 := NewDataValidation(true)
dv2.Sqref = "A3:B4"
assert.NoError(t, dv2.SetRange(10, 20, DataValidationTypeWhole, DataValidationOperatorGreaterThan))
dv2.SetInput("input title", "input body")
// data validation 3
dv3 := NewDataValidation(true)
dv3.Sqref = "A1:B1"
assert.NoError(t, dv3.SetRange("INDIRECT($A$2)", "INDIRECT($A$3)", DataValidationTypeWhole, DataValidationOperatorBetween))
dv3.SetError(DataValidationErrorStyleStop, "error title", "error body")
// data validation 4
dv4 := NewDataValidation(true)
dv4.Sqref = "A5:B6"
_, err := f.NewSheet("Sheet2")
assert.NoError(t, err)
assert.NoError(t, f.SetSheetRow("Sheet2", "A2", &[]interface{}{"B2", 1}))
assert.NoError(t, f.SetSheetRow("Sheet2", "A3", &[]interface{}{"B3", 3}))
for _, listValid := range [][]string{
{"1", "2", "3"},
{"=A1"},
{strings.Repeat("&", MaxFieldLength)},
{strings.Repeat("\u4E00", MaxFieldLength)},
{strings.Repeat("\U0001F600", 100), strings.Repeat("\u4E01", 50), "<&>"},
{`A<`, `B>`, `C"`, "D\t", `E'`, `F`},
} {
dv4.Formula1 = ""
assert.NoError(t, dv4.SetDropList(listValid),
"SetDropList failed for valid input %v", listValid)
assert.NotEqual(t, "", dv4.Formula1,
"Formula1 should not be empty for valid input %v", listValid)
}
assert.Equal(t, `"A&lt;,B&gt;,C"",D ,E',F"`, dv4.Formula1)

// Test batch append data to the worksheet
assert.NoError(t, f.BatchAddDataValidation("Sheet1", []*DataValidation{dv1, dv2, dv3}))
assert.NoError(t, f.BatchAddDataValidation("Sheet2", []*DataValidation{dv4}))

// Test the length of data validation of above worksheet.
dataValidations, err := f.GetDataValidations("Sheet1")
assert.NoError(t, err)
assert.Len(t, dataValidations, 3)
assert.NoError(t, f.SaveAs(resultFile))
dataValidations, err = f.GetDataValidations("Sheet2")
assert.NoError(t, err)
assert.Len(t, dataValidations, 1)

// Test get data validation on no exists worksheet
_, err = f.GetDataValidations("SheetN")
assert.EqualError(t, err, "sheet SheetN does not exist")
// Test get data validation with invalid sheet name
_, err = f.GetDataValidations("Sheet:1")
assert.EqualError(t, err, ErrSheetNameInvalid.Error())

assert.NoError(t, f.SaveAs(resultFile))
// Test get data validation on a worksheet without data validation settings
f = NewFile()
dataValidations, err = f.GetDataValidations("Sheet1")
assert.NoError(t, err)
assert.Equal(t, []*DataValidation(nil), dataValidations)
}

func TestDataValidationError(t *testing.T) {
resultFile := filepath.Join("test", "TestDataValidationError.xlsx")

Expand Down