Skip to content

Commit 7ac37ed

Browse files
authored
Fix data validation issues (#975)
* Fix `SetDropList` to allow XML special characters * This closes #971, allow quotation marks in SetDropList() This patch included a XML entity mapping table instead of xml.EscapeText() to be fully compatible with Microsoft Excel. * This closes #972, allow more than 255 bytes of validation formulas This patch changed the string length calculation unit of data validation formulas from UTF-8 bytes to UTF-16 code units. * Add unit tests for SetDropList() * Fix: allow MaxFloat64 to be used in validation range 17 decimal significant digits should be more than enough to represent every IEEE-754 double-precision float number without losing precision, and numbers in this form will never reach the Excel limitation of 255 UTF-16 code units.
1 parent 7dbf88f commit 7ac37ed

File tree

4 files changed

+67
-22
lines changed

4 files changed

+67
-22
lines changed

datavalidation.go

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ package excelize
1313

1414
import (
1515
"fmt"
16+
"math"
1617
"strings"
1718
"unicode/utf16"
1819
)
@@ -35,10 +36,8 @@ const (
3536
)
3637

3738
const (
38-
// dataValidationFormulaStrLen 255 characters+ 2 quotes
39-
dataValidationFormulaStrLen = 257
40-
// dataValidationFormulaStrLenErr
41-
dataValidationFormulaStrLenErr = "data validation must be 0-255 characters"
39+
// dataValidationFormulaStrLen 255 characters
40+
dataValidationFormulaStrLen = 255
4241
)
4342

4443
// DataValidationErrorStyle defined the style of data validation error alert.
@@ -75,6 +74,15 @@ const (
7574
DataValidationOperatorNotEqual
7675
)
7776

77+
// formulaEscaper mimics the Excel escaping rules for data validation,
78+
// which converts `"` to `""` instead of `"`.
79+
var formulaEscaper = strings.NewReplacer(
80+
`&`, `&`,
81+
`<`, `&lt;`,
82+
`>`, `&gt;`,
83+
`"`, `""`,
84+
)
85+
7886
// NewDataValidation return data validation struct.
7987
func NewDataValidation(allowBlank bool) *DataValidation {
8088
return &DataValidation{
@@ -111,25 +119,22 @@ func (dd *DataValidation) SetInput(title, msg string) {
111119

112120
// SetDropList data validation list.
113121
func (dd *DataValidation) SetDropList(keys []string) error {
114-
formula := "\"" + strings.Join(keys, ",") + "\""
122+
formula := strings.Join(keys, ",")
115123
if dataValidationFormulaStrLen < len(utf16.Encode([]rune(formula))) {
116-
return fmt.Errorf(dataValidationFormulaStrLenErr)
124+
return ErrDataValidationFormulaLenth
117125
}
118-
dd.Formula1 = formula
126+
dd.Formula1 = fmt.Sprintf(`<formula1>"%s"</formula1>`, formulaEscaper.Replace(formula))
119127
dd.Type = convDataValidationType(typeList)
120128
return nil
121129
}
122130

123131
// SetRange provides function to set data validation range in drop list.
124132
func (dd *DataValidation) SetRange(f1, f2 float64, t DataValidationType, o DataValidationOperator) error {
125-
formula1 := fmt.Sprintf("%f", f1)
126-
formula2 := fmt.Sprintf("%f", f2)
127-
if dataValidationFormulaStrLen < len(utf16.Encode([]rune(dd.Formula1))) || dataValidationFormulaStrLen < len(utf16.Encode([]rune(dd.Formula2))) {
128-
return fmt.Errorf(dataValidationFormulaStrLenErr)
133+
if math.Abs(f1) > math.MaxFloat32 || math.Abs(f2) > math.MaxFloat32 {
134+
return ErrDataValidationRange
129135
}
130-
131-
dd.Formula1 = formula1
132-
dd.Formula2 = formula2
136+
dd.Formula1 = fmt.Sprintf("<formula1>%.17g</formula1>", f1)
137+
dd.Formula2 = fmt.Sprintf("<formula2>%.17g</formula2>", f2)
133138
dd.Type = convDataValidationType(t)
134139
dd.Operator = convDataValidationOperatior(o)
135140
return nil

datavalidation_test.go

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
package excelize
1111

1212
import (
13+
"math"
1314
"path/filepath"
1415
"strings"
1516
"testing"
@@ -40,7 +41,20 @@ func TestDataValidation(t *testing.T) {
4041

4142
dvRange = NewDataValidation(true)
4243
dvRange.Sqref = "A5:B6"
43-
assert.NoError(t, dvRange.SetDropList([]string{"1", "2", "3"}))
44+
for _, listValid := range [][]string{
45+
{"1", "2", "3"},
46+
{strings.Repeat("&", 255)},
47+
{strings.Repeat("\u4E00", 255)},
48+
{strings.Repeat("\U0001F600", 100), strings.Repeat("\u4E01", 50), "<&>"},
49+
{`A<`, `B>`, `C"`, "D\t", `E'`, `F`},
50+
} {
51+
dvRange.Formula1 = ""
52+
assert.NoError(t, dvRange.SetDropList(listValid),
53+
"SetDropList failed for valid input %v", listValid)
54+
assert.NotEqual(t, "", dvRange.Formula1,
55+
"Formula1 should not be empty for valid input %v", listValid)
56+
}
57+
assert.Equal(t, `<formula1>"A&lt;,B&gt;,C"",D ,E',F"</formula1>`, dvRange.Formula1)
4458
assert.NoError(t, f.AddDataValidation("Sheet1", dvRange))
4559
assert.NoError(t, f.SaveAs(resultFile))
4660
}
@@ -62,24 +76,44 @@ func TestDataValidationError(t *testing.T) {
6276
assert.EqualError(t, err, "cross-sheet sqref cell are not supported")
6377

6478
assert.NoError(t, f.AddDataValidation("Sheet1", dvRange))
65-
assert.NoError(t, f.SaveAs(resultFile))
6679

6780
dvRange = NewDataValidation(true)
6881
err = dvRange.SetDropList(make([]string, 258))
6982
if dvRange.Formula1 != "" {
7083
t.Errorf("data validation error. Formula1 must be empty!")
7184
return
7285
}
73-
assert.EqualError(t, err, "data validation must be 0-255 characters")
86+
assert.EqualError(t, err, ErrDataValidationFormulaLenth.Error())
7487
assert.NoError(t, dvRange.SetRange(10, 20, DataValidationTypeWhole, DataValidationOperatorGreaterThan))
7588
dvRange.SetSqref("A9:B10")
7689

7790
assert.NoError(t, f.AddDataValidation("Sheet1", dvRange))
78-
assert.NoError(t, f.SaveAs(resultFile))
7991

8092
// Test width invalid data validation formula.
81-
dvRange.Formula1 = strings.Repeat("s", dataValidationFormulaStrLen+22)
82-
assert.EqualError(t, dvRange.SetRange(10, 20, DataValidationTypeWhole, DataValidationOperatorGreaterThan), "data validation must be 0-255 characters")
93+
prevFormula1 := dvRange.Formula1
94+
for _, keys := range [][]string{
95+
make([]string, 257),
96+
{strings.Repeat("s", 256)},
97+
{strings.Repeat("\u4E00", 256)},
98+
{strings.Repeat("\U0001F600", 128)},
99+
{strings.Repeat("\U0001F600", 127), "s"},
100+
} {
101+
err = dvRange.SetDropList(keys)
102+
assert.Equal(t, prevFormula1, dvRange.Formula1,
103+
"Formula1 should be unchanged for invalid input %v", keys)
104+
assert.EqualError(t, err, ErrDataValidationFormulaLenth.Error())
105+
}
106+
assert.NoError(t, f.AddDataValidation("Sheet1", dvRange))
107+
assert.NoError(t, dvRange.SetRange(
108+
-math.MaxFloat32, math.MaxFloat32,
109+
DataValidationTypeWhole, DataValidationOperatorGreaterThan))
110+
assert.EqualError(t, dvRange.SetRange(
111+
-math.MaxFloat64, math.MaxFloat32,
112+
DataValidationTypeWhole, DataValidationOperatorGreaterThan), ErrDataValidationRange.Error())
113+
assert.EqualError(t, dvRange.SetRange(
114+
math.SmallestNonzeroFloat64, math.MaxFloat64,
115+
DataValidationTypeWhole, DataValidationOperatorGreaterThan), ErrDataValidationRange.Error())
116+
assert.NoError(t, f.SaveAs(resultFile))
83117

84118
// Test add data validation on no exists worksheet.
85119
f = NewFile()

errors.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,4 +105,10 @@ var (
105105
ErrSheetIdx = errors.New("invalid worksheet index")
106106
// ErrGroupSheets defined the error message on group sheets.
107107
ErrGroupSheets = errors.New("group worksheet must contain an active worksheet")
108+
// ErrDataValidationFormulaLenth defined the error message for receiving a
109+
// data validation formula length that exceeds the limit.
110+
ErrDataValidationFormulaLenth = errors.New("data validation must be 0-255 characters")
111+
// ErrDataValidationRange defined the error message on set decimal range
112+
// exceeds limit.
113+
ErrDataValidationRange = errors.New("data validation range exceeds limit")
108114
)

xmlWorksheet.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -436,8 +436,8 @@ type DataValidation struct {
436436
ShowInputMessage bool `xml:"showInputMessage,attr,omitempty"`
437437
Sqref string `xml:"sqref,attr"`
438438
Type string `xml:"type,attr,omitempty"`
439-
Formula1 string `xml:"formula1,omitempty"`
440-
Formula2 string `xml:"formula2,omitempty"`
439+
Formula1 string `xml:",innerxml"`
440+
Formula2 string `xml:",innerxml"`
441441
}
442442

443443
// xlsxC collection represents a cell in the worksheet. Information about the

0 commit comments

Comments
 (0)