Browse Source

Merge pull request #439 from ryho/master

Improvements to XLSX Data Validation
Ryan Hollis 6 years ago
parent
commit
3933b9e372
10 changed files with 394 additions and 243 deletions
  1. 3 2
      col.go
  2. 31 8
      data_validation.go
  3. 259 0
      data_validation_test.go
  4. 0 195
      datavalidation_test.go
  5. 33 11
      lib.go
  6. 12 5
      lib_test.go
  7. 3 3
      sheet.go
  8. 7 0
      stream_file_builder.go
  9. 1 1
      stream_test.go
  10. 45 18
      xmlWorksheet.go

+ 3 - 2
col.go

@@ -2,7 +2,8 @@ package xlsx
 
 // Default column width in excel
 const ColWidth = 9.5
-const Excel2006MaxRowIndex = 1048576
+const Excel2006MaxRowCount = 1048576
+const Excel2006MaxRowIndex = Excel2006MaxRowCount - 1
 const Excel2006MinRowIndex = 1
 
 type Col struct {
@@ -62,7 +63,7 @@ func (c *Col) SetDataValidation(dd *xlsxCellDataValidation, start, end int) {
 
 	if 0 == end {
 		end = Excel2006MinRowIndex
-	} else if end < Excel2006MaxRowIndex {
+	} else if end < Excel2006MaxRowCount {
 		end = end + 1
 	}
 

+ 31 - 8
datavalidation.go → data_validation.go

@@ -14,7 +14,7 @@ const (
 	DataValidationTypeCustom
 	DataValidationTypeDate
 	DataValidationTypeDecimal
-	typeList //inline use
+	dataValidationTypeList //inline use
 	DataValidationTypeTextLeng
 	DataValidationTypeTime
 	// DataValidationTypeWhole Integer
@@ -62,16 +62,15 @@ const (
 )
 
 // NewXlsxCellDataValidation return data validation struct
-func NewXlsxCellDataValidation(allowBlank, ShowInputMessage, showErrorMessage bool) *xlsxCellDataValidation {
+func NewXlsxCellDataValidation(allowBlank bool) *xlsxCellDataValidation {
 	return &xlsxCellDataValidation{
-		AllowBlank:       allowBlank,
-		ShowErrorMessage: showErrorMessage,
-		ShowInputMessage: ShowInputMessage,
+		AllowBlank: allowBlank,
 	}
 }
 
 // SetError set error notice
 func (dd *xlsxCellDataValidation) SetError(style DataValidationErrorStyle, title, msg *string) {
+	dd.ShowErrorMessage = true
 	dd.Error = msg
 	dd.ErrorTitle = title
 	strStyle := styleStop
@@ -89,18 +88,42 @@ func (dd *xlsxCellDataValidation) SetError(style DataValidationErrorStyle, title
 
 // SetInput set prompt notice
 func (dd *xlsxCellDataValidation) SetInput(title, msg *string) {
+	dd.ShowInputMessage = true
 	dd.PromptTitle = title
 	dd.Prompt = msg
 }
 
-// SetDropList data validation list
+// SetDropList sets a hard coded list of values that the drop down will choose from.
+// List validations do not work in Apple Numbers.
 func (dd *xlsxCellDataValidation) SetDropList(keys []string) error {
 	formula := "\"" + strings.Join(keys, ",") + "\""
 	if dataValidationFormulaStrLen < len(formula) {
 		return fmt.Errorf(dataValidationFormulaStrLenErr)
 	}
 	dd.Formula1 = formula
-	dd.Type = convDataValidationType(typeList)
+	dd.Type = convDataValidationType(dataValidationTypeList)
+	return nil
+}
+
+// SetInFileList is like SetDropList, excel that instead of having a hard coded list,
+// a reference to a part of the file is accepted and the list is automatically taken from there.
+// Setting y2 to -1 will select all the way to the end of the column. Selecting to the end of the
+// column will cause Google Sheets to spin indefinitely while trying to load the possible drop down
+// values (more than 5 minutes).
+// List validations do not work in Apple Numbers.
+func (dd *xlsxCellDataValidation) SetInFileList(sheet string, x1, y1, x2, y2 int) error {
+	start := GetCellIDStringFromCoordsWithFixed(x1, y1, true, true)
+	if y2 < 0 {
+		y2 = Excel2006MaxRowIndex
+	}
+
+	end := GetCellIDStringFromCoordsWithFixed(x2, y2, true, true)
+	// Escape single quotes in the file name.
+	// Single quotes are escaped by replacing them with two single quotes.
+	sheet = strings.Replace(sheet, "'", "''", -1)
+	formula := "'" + sheet + "'" + externalSheetBangChar + start + cellRangeChar + end
+	dd.Formula1 = formula
+	dd.Type = convDataValidationType(dataValidationTypeList)
 	return nil
 }
 
@@ -138,7 +161,7 @@ func convDataValidationType(t DataValidationType) string {
 		DataValidationTypeCustom:   "custom",
 		DataValidationTypeDate:     "date",
 		DataValidationTypeDecimal:  "decimal",
-		typeList:                   "list",
+		dataValidationTypeList:     "list",
 		DataValidationTypeTextLeng: "textLength",
 		DataValidationTypeTime:     "time",
 		DataValidationTypeWhole:    "whole",

+ 259 - 0
data_validation_test.go

@@ -0,0 +1,259 @@
+package xlsx
+
+import (
+	"bytes"
+	"fmt"
+
+	. "gopkg.in/check.v1"
+)
+
+type DataValidationSuite struct{}
+
+var _ = Suite(&DataValidationSuite{})
+
+func (d *DataValidationSuite) TestDataValidation(t *C) {
+	var file *File
+	var sheet *Sheet
+	var row *Row
+	var cell *Cell
+	var err error
+	var title = "cell"
+	var msg = "cell msg"
+
+	file = NewFile()
+	sheet, err = file.AddSheet("Sheet1")
+	if err != nil {
+		fmt.Printf(err.Error())
+	}
+	row = sheet.AddRow()
+	cell = row.AddCell()
+	cell.Value = "a1"
+
+	dd := NewXlsxCellDataValidation(true)
+	err = dd.SetDropList([]string{"a1", "a2", "a3"})
+	t.Assert(err, IsNil)
+
+	dd.SetInput(&title, &msg)
+	cell.SetDataValidation(dd)
+
+	dd = NewXlsxCellDataValidation(true)
+	err = dd.SetDropList([]string{"c1", "c2", "c3"})
+	t.Assert(err, IsNil)
+	title = "col c"
+	dd.SetInput(&title, &msg)
+	sheet.Col(2).SetDataValidation(dd, 0, 0)
+
+	dd = NewXlsxCellDataValidation(true)
+	err = dd.SetDropList([]string{"d", "d1", "d2"})
+	t.Assert(err, IsNil)
+	title = "col d range"
+	dd.SetInput(&title, &msg)
+	sheet.Col(3).SetDataValidation(dd, 3, 7)
+
+	dd = NewXlsxCellDataValidation(true)
+	err = dd.SetDropList([]string{"e1", "e2", "e3"})
+	t.Assert(err, IsNil)
+	title = "col e start 3"
+	dd.SetInput(&title, &msg)
+	sheet.Col(4).SetDataValidationWithStart(dd, 1)
+
+	index := 5
+	rowIndex := 1
+	dd = NewXlsxCellDataValidation(true)
+	err = dd.SetRange(15, 4, DataValidationTypeTextLeng, DataValidationOperatorBetween)
+	t.Assert(err, IsNil)
+	sheet.Cell(rowIndex, index).SetDataValidation(dd)
+	index++
+
+	dd = NewXlsxCellDataValidation(true)
+	err = dd.SetRange(10, 1, DataValidationTypeTextLeng, DataValidationOperatorEqual)
+	t.Assert(err, IsNil)
+	sheet.Cell(rowIndex, index).SetDataValidation(dd)
+	index++
+
+	dd = NewXlsxCellDataValidation(true)
+	err = dd.SetRange(10, 1, DataValidationTypeTextLeng, DataValidationOperatorGreaterThanOrEqual)
+	t.Assert(err, IsNil)
+	sheet.Cell(rowIndex, index).SetDataValidation(dd)
+	index++
+
+	dd = NewXlsxCellDataValidation(true)
+	err = dd.SetRange(10, 1, DataValidationTypeTextLeng, DataValidationOperatorGreaterThan)
+	t.Assert(err, IsNil)
+	sheet.Cell(rowIndex, index).SetDataValidation(dd)
+	index++
+
+	dd = NewXlsxCellDataValidation(true)
+	err = dd.SetRange(10, 1, DataValidationTypeTextLeng, DataValidationOperatorLessThan)
+	t.Assert(err, IsNil)
+	sheet.Cell(rowIndex, index).SetDataValidation(dd)
+	index++
+
+	dd = NewXlsxCellDataValidation(true)
+	err = dd.SetRange(10, 1, DataValidationTypeTextLeng, DataValidationOperatorLessThanOrEqual)
+	t.Assert(err, IsNil)
+	sheet.Cell(rowIndex, index).SetDataValidation(dd)
+	index++
+
+	dd = NewXlsxCellDataValidation(true)
+	err = dd.SetRange(10, 1, DataValidationTypeTextLeng, DataValidationOperatorNotEqual)
+	t.Assert(err, IsNil)
+	sheet.Cell(rowIndex, index).SetDataValidation(dd)
+	index++
+
+	dd = NewXlsxCellDataValidation(true)
+	err = dd.SetRange(10, 1, DataValidationTypeTextLeng, DataValidationOperatorNotBetween)
+	t.Assert(err, IsNil)
+	sheet.Cell(rowIndex, index).SetDataValidation(dd)
+	index++
+
+	rowIndex++
+	index = 5
+
+	dd = NewXlsxCellDataValidation(true)
+	err = dd.SetRange(4, 15, DataValidationTypeWhole, DataValidationOperatorBetween)
+	t.Assert(err, IsNil)
+	sheet.Cell(rowIndex, index).SetDataValidation(dd)
+	index++
+
+	dd = NewXlsxCellDataValidation(true)
+	err = dd.SetRange(10, 1, DataValidationTypeWhole, DataValidationOperatorEqual)
+	t.Assert(err, IsNil)
+	sheet.Cell(rowIndex, index).SetDataValidation(dd)
+	index++
+
+	dd = NewXlsxCellDataValidation(true)
+	err = dd.SetRange(10, 1, DataValidationTypeWhole, DataValidationOperatorGreaterThanOrEqual)
+	t.Assert(err, IsNil)
+	sheet.Cell(rowIndex, index).SetDataValidation(dd)
+	index++
+
+	dd = NewXlsxCellDataValidation(true)
+	err = dd.SetRange(10, 1, DataValidationTypeWhole, DataValidationOperatorGreaterThan)
+	t.Assert(err, IsNil)
+	sheet.Cell(rowIndex, index).SetDataValidation(dd)
+	index++
+
+	dd = NewXlsxCellDataValidation(true)
+	err = dd.SetRange(10, 1, DataValidationTypeWhole, DataValidationOperatorLessThan)
+	t.Assert(err, IsNil)
+	sheet.Cell(rowIndex, index).SetDataValidation(dd)
+	index++
+
+	dd = NewXlsxCellDataValidation(true)
+	err = dd.SetRange(10, 1, DataValidationTypeWhole, DataValidationOperatorLessThanOrEqual)
+	t.Assert(err, IsNil)
+	sheet.Cell(rowIndex, index).SetDataValidation(dd)
+	index++
+
+	dd = NewXlsxCellDataValidation(true)
+	err = dd.SetRange(10, 1, DataValidationTypeWhole, DataValidationOperatorNotEqual)
+	t.Assert(err, IsNil)
+	sheet.Cell(rowIndex, index).SetDataValidation(dd)
+	index++
+
+	dd = NewXlsxCellDataValidation(true)
+	err = dd.SetRange(10, 50, DataValidationTypeWhole, DataValidationOperatorNotBetween)
+	if err != nil {
+		t.Fatal(err)
+	}
+	sheet.Cell(rowIndex, index).SetDataValidation(dd)
+	index++
+
+	dd = NewXlsxCellDataValidation(true)
+	err = dd.SetDropList([]string{"1", "2", "4"})
+	t.Assert(err, IsNil)
+	dd1 := NewXlsxCellDataValidation(true)
+	err = dd1.SetDropList([]string{"11", "22", "44"})
+	t.Assert(err, IsNil)
+	dd2 := NewXlsxCellDataValidation(true)
+	err = dd2.SetDropList([]string{"111", "222", "444"})
+	t.Assert(err, IsNil)
+	sheet.Col(12).SetDataValidation(dd, 2, 10)
+	sheet.Col(12).SetDataValidation(dd1, 3, 4)
+	sheet.Col(12).SetDataValidation(dd2, 5, 7)
+
+	dd = NewXlsxCellDataValidation(true)
+	err = dd.SetDropList([]string{"1", "2", "4"})
+	t.Assert(err, IsNil)
+	dd1 = NewXlsxCellDataValidation(true)
+	err = dd1.SetDropList([]string{"11", "22", "44"})
+	t.Assert(err, IsNil)
+	sheet.Col(13).SetDataValidation(dd, 2, 10)
+	sheet.Col(13).SetDataValidation(dd1, 1, 2)
+
+	dd = NewXlsxCellDataValidation(true)
+	err = dd.SetDropList([]string{"1", "2", "4"})
+	t.Assert(err, IsNil)
+	dd1 = NewXlsxCellDataValidation(true)
+	err = dd1.SetDropList([]string{"11", "22", "44"})
+	t.Assert(err, IsNil)
+	sheet.Col(14).SetDataValidation(dd, 2, 10)
+	sheet.Col(14).SetDataValidation(dd1, 1, 5)
+
+	dd = NewXlsxCellDataValidation(true)
+	err = dd.SetDropList([]string{"1", "2", "4"})
+	if err != nil {
+		t.Fatal(err)
+	}
+	dd1 = NewXlsxCellDataValidation(true)
+	err = dd1.SetDropList([]string{"11", "22", "44"})
+	t.Assert(err, IsNil)
+	sheet.Col(15).SetDataValidation(dd, 2, 10)
+	sheet.Col(15).SetDataValidation(dd1, 1, 10)
+
+	dd = NewXlsxCellDataValidation(true)
+	err = dd.SetDropList([]string{"1", "2", "4"})
+	t.Assert(err, IsNil)
+	dd1 = NewXlsxCellDataValidation(true)
+	err = dd1.SetDropList([]string{"11", "22", "44"})
+	t.Assert(err, IsNil)
+	dd2 = NewXlsxCellDataValidation(true)
+	err = dd2.SetDropList([]string{"111", "222", "444"})
+	t.Assert(err, IsNil)
+	sheet.Col(16).SetDataValidation(dd, 10, 20)
+	sheet.Col(16).SetDataValidation(dd1, 2, 4)
+	sheet.Col(16).SetDataValidation(dd2, 21, 30)
+
+	dd = NewXlsxCellDataValidation(true)
+	err = dd.SetDropList([]string{"d", "d1", "d2"})
+	t.Assert(err, IsNil)
+	title = "col d range"
+	dd.SetInput(&title, &msg)
+	sheet.Col(3).SetDataValidation(dd, 3, Excel2006MaxRowCount)
+
+	dest := &bytes.Buffer{}
+	err = file.Write(dest)
+	t.Assert(err, IsNil)
+	// Read and write the file that was just saved.
+	file, err = OpenBinary(dest.Bytes())
+	t.Assert(err, IsNil)
+	dest = &bytes.Buffer{}
+	err = file.Write(dest)
+	t.Assert(err, IsNil)
+}
+
+func (d *DataValidationSuite) TestDataValidation2(t *C) {
+	// Show error and show info start disabled, but automatically get enabled when setting a message
+	dd := NewXlsxCellDataValidation(true)
+	t.Assert(dd.ShowErrorMessage, Equals, false)
+	t.Assert(dd.ShowInputMessage, Equals, false)
+
+	str := "you got an error"
+	dd.SetError(StyleStop, &str, &str)
+	t.Assert(dd.ShowErrorMessage, Equals, true)
+	t.Assert(dd.ShowInputMessage, Equals, false)
+
+	str = "hello"
+	dd.SetInput(&str, &str)
+	t.Assert(dd.ShowInputMessage, Equals, true)
+
+	// Check the formula created by this function
+	// The sheet name needs single quotes, the single quote in the name gets escaped,
+	// and all references are fixed.
+	err := dd.SetInFileList("Sheet ' 2", 2, 1, 3, 10)
+	t.Assert(err, IsNil)
+	expectedFormula := "'Sheet '' 2'!$C$2:$D$11"
+	t.Assert(dd.Formula1, Equals, expectedFormula)
+	t.Assert(dd.Type, Equals, "list")
+}

+ 0 - 195
datavalidation_test.go

@@ -1,195 +0,0 @@
-package xlsx
-
-import (
-	"fmt"
-	"testing"
-)
-
-func TestDataValidation(t *testing.T) {
-	var file *File
-	var sheet *Sheet
-	var row *Row
-	var cell *Cell
-	var err error
-	var title string = "cell"
-	var msg string = "cell msg"
-
-	file = NewFile()
-	sheet, err = file.AddSheet("Sheet1")
-	if err != nil {
-		fmt.Printf(err.Error())
-	}
-	row = sheet.AddRow()
-	cell = row.AddCell()
-	cell.Value = "a1"
-
-	dd := NewXlsxCellDataValidation(true, true, true)
-	dd.SetDropList([]string{"a1", "a2", "a3"})
-
-	dd.SetInput(&title, &msg)
-	cell.SetDataValidation(dd)
-
-	dd = NewXlsxCellDataValidation(true, true, true)
-	dd.SetDropList([]string{"c1", "c2", "c3"})
-	title = "col c"
-	dd.SetInput(&title, &msg)
-	sheet.Col(2).SetDataValidation(dd, 0, 0)
-
-	dd = NewXlsxCellDataValidation(true, true, true)
-	dd.SetDropList([]string{"d", "d1", "d2"})
-	title = "col d range"
-	dd.SetInput(&title, &msg)
-	sheet.Col(3).SetDataValidation(dd, 3, 7)
-
-	dd = NewXlsxCellDataValidation(true, true, true)
-	dd.SetDropList([]string{"e1", "e2", "e3"})
-	title = "col e start 3"
-	dd.SetInput(&title, &msg)
-	sheet.Col(4).SetDataValidationWithStart(dd, 1)
-
-	index := 5
-	rowIndex := 1
-	dd = NewXlsxCellDataValidation(true, true, true)
-	dd.SetRange(15, 4, DataValidationTypeTextLeng, DataValidationOperatorBetween)
-	sheet.Cell(rowIndex, index).SetDataValidation(dd)
-	index++
-
-	dd = NewXlsxCellDataValidation(true, true, true)
-	dd.SetRange(10, 1, DataValidationTypeTextLeng, DataValidationOperatorEqual)
-	sheet.Cell(rowIndex, index).SetDataValidation(dd)
-	index++
-
-	dd = NewXlsxCellDataValidation(true, true, true)
-	dd.SetRange(10, 1, DataValidationTypeTextLeng, DataValidationOperatorGreaterThanOrEqual)
-	sheet.Cell(rowIndex, index).SetDataValidation(dd)
-	index++
-
-	dd = NewXlsxCellDataValidation(true, true, true)
-	dd.SetRange(10, 1, DataValidationTypeTextLeng, DataValidationOperatorGreaterThan)
-	sheet.Cell(rowIndex, index).SetDataValidation(dd)
-	index++
-
-	dd = NewXlsxCellDataValidation(true, true, true)
-	dd.SetRange(10, 1, DataValidationTypeTextLeng, DataValidationOperatorLessThan)
-	sheet.Cell(rowIndex, index).SetDataValidation(dd)
-	index++
-
-	dd = NewXlsxCellDataValidation(true, true, true)
-	dd.SetRange(10, 1, DataValidationTypeTextLeng, DataValidationOperatorLessThanOrEqual)
-	sheet.Cell(rowIndex, index).SetDataValidation(dd)
-	index++
-
-	dd = NewXlsxCellDataValidation(true, true, true)
-	dd.SetRange(10, 1, DataValidationTypeTextLeng, DataValidationOperatorNotEqual)
-	sheet.Cell(rowIndex, index).SetDataValidation(dd)
-	index++
-
-	dd = NewXlsxCellDataValidation(true, true, true)
-	dd.SetRange(10, 1, DataValidationTypeTextLeng, DataValidationOperatorNotBetween)
-	sheet.Cell(rowIndex, index).SetDataValidation(dd)
-	index++
-
-	rowIndex++
-	index = 5
-
-	dd = NewXlsxCellDataValidation(true, true, true)
-	dd.SetRange(4, 15, DataValidationTypeWhole, DataValidationOperatorBetween)
-	sheet.Cell(rowIndex, index).SetDataValidation(dd)
-	index++
-
-	dd = NewXlsxCellDataValidation(true, true, true)
-	dd.SetRange(10, 1, DataValidationTypeWhole, DataValidationOperatorEqual)
-	sheet.Cell(rowIndex, index).SetDataValidation(dd)
-	index++
-
-	dd = NewXlsxCellDataValidation(true, true, true)
-	dd.SetRange(10, 1, DataValidationTypeWhole, DataValidationOperatorGreaterThanOrEqual)
-	sheet.Cell(rowIndex, index).SetDataValidation(dd)
-	index++
-
-	dd = NewXlsxCellDataValidation(true, true, true)
-	dd.SetRange(10, 1, DataValidationTypeWhole, DataValidationOperatorGreaterThan)
-	sheet.Cell(rowIndex, index).SetDataValidation(dd)
-	index++
-
-	dd = NewXlsxCellDataValidation(true, true, true)
-	dd.SetRange(10, 1, DataValidationTypeWhole, DataValidationOperatorLessThan)
-	sheet.Cell(rowIndex, index).SetDataValidation(dd)
-	index++
-
-	dd = NewXlsxCellDataValidation(true, true, true)
-	dd.SetRange(10, 1, DataValidationTypeWhole, DataValidationOperatorLessThanOrEqual)
-	sheet.Cell(rowIndex, index).SetDataValidation(dd)
-	index++
-
-	dd = NewXlsxCellDataValidation(true, true, true)
-	dd.SetRange(10, 1, DataValidationTypeWhole, DataValidationOperatorNotEqual)
-	sheet.Cell(rowIndex, index).SetDataValidation(dd)
-	index++
-
-	dd = NewXlsxCellDataValidation(true, true, true)
-	dd.SetRange(10, 50, DataValidationTypeWhole, DataValidationOperatorNotBetween)
-	sheet.Cell(rowIndex, index).SetDataValidation(dd)
-	index++
-
-	if err != nil {
-		fmt.Printf(err.Error())
-	}
-
-	dd = NewXlsxCellDataValidation(true, true, true)
-	dd.SetDropList([]string{"1", "2", "4"})
-	dd1 := NewXlsxCellDataValidation(true, true, true)
-	dd1.SetDropList([]string{"11", "22", "44"})
-	dd2 := NewXlsxCellDataValidation(true, true, true)
-	dd2.SetDropList([]string{"111", "222", "444"})
-	sheet.Col(12).SetDataValidation(dd, 2, 10)
-	sheet.Col(12).SetDataValidation(dd1, 3, 4)
-	sheet.Col(12).SetDataValidation(dd2, 5, 7)
-
-	dd = NewXlsxCellDataValidation(true, true, true)
-	dd.SetDropList([]string{"1", "2", "4"})
-	dd1 = NewXlsxCellDataValidation(true, true, true)
-	dd1.SetDropList([]string{"11", "22", "44"})
-	sheet.Col(13).SetDataValidation(dd, 2, 10)
-	sheet.Col(13).SetDataValidation(dd1, 1, 2)
-
-	dd = NewXlsxCellDataValidation(true, true, true)
-	dd.SetDropList([]string{"1", "2", "4"})
-	dd1 = NewXlsxCellDataValidation(true, true, true)
-	dd1.SetDropList([]string{"11", "22", "44"})
-	sheet.Col(14).SetDataValidation(dd, 2, 10)
-	sheet.Col(14).SetDataValidation(dd1, 1, 5)
-
-	dd = NewXlsxCellDataValidation(true, true, true)
-	dd.SetDropList([]string{"1", "2", "4"})
-	dd1 = NewXlsxCellDataValidation(true, true, true)
-	dd1.SetDropList([]string{"11", "22", "44"})
-	sheet.Col(15).SetDataValidation(dd, 2, 10)
-	sheet.Col(15).SetDataValidation(dd1, 1, 10)
-
-	dd = NewXlsxCellDataValidation(true, true, true)
-	dd.SetDropList([]string{"1", "2", "4"})
-	dd1 = NewXlsxCellDataValidation(true, true, true)
-	dd1.SetDropList([]string{"11", "22", "44"})
-	dd2 = NewXlsxCellDataValidation(true, true, true)
-	dd2.SetDropList([]string{"111", "222", "444"})
-	sheet.Col(16).SetDataValidation(dd, 10, 20)
-	sheet.Col(16).SetDataValidation(dd1, 2, 4)
-	sheet.Col(16).SetDataValidation(dd2, 21, 30)
-
-	file.Save("datavalidation.xlsx")
-
-}
-
-func TestReadDataValidation(t *testing.T) {
-	file, err := OpenFile("datavalidation.xlsx")
-	if nil != err {
-		t.Errorf(err.Error())
-		return
-	}
-	err = file.Save("datavalidation_read.xlsx")
-	if nil != err {
-		t.Errorf(err.Error())
-		return
-	}
-}

+ 33 - 11
lib.go

@@ -13,7 +13,10 @@ import (
 )
 
 const (
-	sheetEnding = `</sheetData></worksheet>`
+	sheetEnding           = `</sheetData></worksheet>`
+	fixedCellRefChar      = "$"
+	cellRangeChar         = ":"
+	externalSheetBangChar = "!"
 )
 
 // XLSXReaderError is the standard error type for otherwise undefined
@@ -33,7 +36,7 @@ func (e *XLSXReaderError) Error() string {
 // the range string "1:3" yield the upper and lower integers 1 and 3.
 func getRangeFromString(rangeString string) (lower int, upper int, error error) {
 	var parts []string
-	parts = strings.SplitN(rangeString, ":", 2)
+	parts = strings.SplitN(rangeString, cellRangeChar, 2)
 	if parts[0] == "" {
 		error = errors.New(fmt.Sprintf("Invalid range '%s'\n", rangeString))
 	}
@@ -146,6 +149,12 @@ func ColIndexToLetters(colRef int) string {
 	return formatColumnName(smooshBase26Slice(parts))
 }
 
+// RowIndexToString is used to convert a zero based, numeric row
+// indentifier into its string representation.
+func RowIndexToString(rowRef int) string {
+	return strconv.Itoa(rowRef + 1)
+}
+
 // letterOnlyMapF is used in conjunction with strings.Map to return
 // only the characters A-Z and a-z in a string
 func letterOnlyMapF(rune rune) rune {
@@ -184,9 +193,22 @@ func GetCoordsFromCellIDString(cellIDString string) (x, y int, error error) {
 // GetCellIDStringFromCoords returns the Excel format cell name that
 // represents a pair of zero based cartesian coordinates.
 func GetCellIDStringFromCoords(x, y int) string {
-	letterPart := ColIndexToLetters(x)
-	numericPart := y + 1
-	return fmt.Sprintf("%s%d", letterPart, numericPart)
+	return GetCellIDStringFromCoordsWithFixed(x, y, false, false)
+}
+
+// GetCellIDStringFromCoordsWithFixed returns the Excel format cell name that
+// represents a pair of zero based cartesian coordinates.
+// It can specify either value as fixed.
+func GetCellIDStringFromCoordsWithFixed(x, y int, xFixed, yFixed bool) string {
+	xStr := ColIndexToLetters(x)
+	if xFixed {
+		xStr = fixedCellRefChar + xStr
+	}
+	yStr := RowIndexToString(y)
+	if yFixed {
+		yStr = fixedCellRefChar + yStr
+	}
+	return xStr + yStr
 }
 
 // getMaxMinFromDimensionRef return the zero based cartesian maximum
@@ -195,7 +217,7 @@ func GetCellIDStringFromCoords(x, y int) string {
 // returns "0,0", "1,1".
 func getMaxMinFromDimensionRef(ref string) (minx, miny, maxx, maxy int, err error) {
 	var parts []string
-	parts = strings.Split(ref, ":")
+	parts = strings.Split(ref, cellRangeChar)
 	minx, miny, err = GetCoordsFromCellIDString(parts[0])
 	if err != nil {
 		return -1, -1, -1, -1, err
@@ -397,10 +419,10 @@ func shiftCell(cellID string, dx, dy int) string {
 	fx, fy, _ := GetCoordsFromCellIDString(cellID)
 
 	// Is fixed column?
-	fixedCol := strings.Index(cellID, "$") == 0
+	fixedCol := strings.Index(cellID, fixedCellRefChar) == 0
 
 	// Is fixed row?
-	fixedRow := strings.LastIndex(cellID, "$") > 0
+	fixedRow := strings.LastIndex(cellID, fixedCellRefChar) > 0
 
 	if !fixedCol {
 		// Shift column
@@ -515,7 +537,7 @@ func readRowsFromSheet(Worksheet *xlsxWorksheet, file *File, sheet *Sheet, rowLi
 		return nil, nil, 0, 0
 	}
 	reftable = file.referenceTable
-	if len(Worksheet.Dimension.Ref) > 0 && len(strings.Split(Worksheet.Dimension.Ref, ":")) == 2 && rowLimit == NoRowLimit {
+	if len(Worksheet.Dimension.Ref) > 0 && len(strings.Split(Worksheet.Dimension.Ref, cellRangeChar)) == 2 && rowLimit == NoRowLimit {
 		minCol, _, maxCol, maxRow, err = getMaxMinFromDimensionRef(Worksheet.Dimension.Ref)
 	} else {
 		minCol, _, maxCol, maxRow, err = calculateMaxMinFromWorksheet(Worksheet)
@@ -571,7 +593,7 @@ func readRowsFromSheet(Worksheet *xlsxWorksheet, file *File, sheet *Sheet, rowLi
 			insertRowIndex++
 		}
 		// range is not empty and only one range exist
-		if len(rawrow.Spans) != 0 && strings.Count(rawrow.Spans, ":") == 1 {
+		if len(rawrow.Spans) != 0 && strings.Count(rawrow.Spans, cellRangeChar) == 1 {
 			row = makeRowFromSpan(rawrow.Spans, sheet)
 		} else {
 			row = makeRowFromRaw(rawrow, sheet)
@@ -703,7 +725,7 @@ func readSheetFromFile(sc chan *indexedSheet, index int, rsheet xlsxSheet, fi *F
 		for _, dd := range worksheet.DataValidations.DataValidattion {
 			sqrefArr := strings.Split(dd.Sqref, " ")
 			for _, sqref := range sqrefArr {
-				parts := strings.Split(sqref, ":")
+				parts := strings.Split(sqref, cellRangeChar)
 
 				minCol, minRow, err := GetCoordsFromCellIDString(parts[0])
 				if nil != err {

+ 12 - 5
lib_test.go

@@ -1322,10 +1322,12 @@ func (l *LibSuite) TestRowNotOverwrittenWhenFollowedByEmptyRow(c *C) {
 	sharedstringsXML := bytes.NewBufferString(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?><sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"/>`)
 
 	worksheet := new(xlsxWorksheet)
-	xml.NewDecoder(sheetXML).Decode(worksheet)
+	err := xml.NewDecoder(sheetXML).Decode(worksheet)
+	c.Assert(err, IsNil)
 
 	sst := new(xlsxSST)
-	xml.NewDecoder(sharedstringsXML).Decode(sst)
+	err = xml.NewDecoder(sharedstringsXML).Decode(sst)
+	c.Assert(err, IsNil)
 
 	file := new(File)
 	file.referenceTable = MakeSharedStringRefTable(sst)
@@ -1342,10 +1344,12 @@ func (l *LibSuite) TestRowNotOverwrittenWhenFollowedByEmptyRow(c *C) {
 func (l *LibSuite) TestRoundTripFileWithNoSheetCols(c *C) {
 	originalXlFile, err := OpenFile("testdocs/original.xlsx")
 	c.Assert(err, IsNil)
-	originalXlFile.Save("testdocs/after_write.xlsx")
+	err = originalXlFile.Save("testdocs/after_write.xlsx")
+	c.Assert(err, IsNil)
 	_, err = OpenFile("testdocs/after_write.xlsx")
 	c.Assert(err, IsNil)
-	os.Remove("testdocs/after_write.xlsx")
+	err = os.Remove("testdocs/after_write.xlsx")
+	c.Assert(err, IsNil)
 }
 
 func (l *LibSuite) TestReadRestEmptyRowsFromSheet(c *C) {
@@ -1690,6 +1694,9 @@ func TestFuzzCrashers(t *testing.T) {
 	}
 
 	for _, f := range crashers {
-		OpenBinary([]byte(f))
+		_, err := OpenBinary([]byte(f))
+		if err == nil {
+			t.Fatal("Expected a well formed error from opening this file")
+		}
 	}
 }

+ 3 - 3
sheet.go

@@ -285,9 +285,9 @@ func (s *Sheet) makeXLSXSheet(refTable *RefTable, styles *xlsxStyleSheet) *xlsxW
 			colName := ColIndexToLetters(c)
 			for _, dd := range col.DataValidation {
 				if dd.minRow == dd.maxRow {
-					dd.Sqref = fmt.Sprintf("%s%d", colName, dd.minRow)
+					dd.Sqref = colName + RowIndexToString(dd.minRow)
 				} else {
-					dd.Sqref = fmt.Sprintf("%s%d:%s%d", colName, dd.minRow, colName, dd.maxRow)
+					dd.Sqref = colName + RowIndexToString(dd.minRow) + cellRangeChar + colName + RowIndexToString(dd.maxRow)
 				}
 				worksheet.DataValidations.DataValidattion = append(worksheet.DataValidations.DataValidattion, dd)
 
@@ -379,7 +379,7 @@ func (s *Sheet) makeXLSXSheet(refTable *RefTable, styles *xlsxStyleSheet) *xlsxW
 				endCol := c + cell.HMerge
 				endRow := r + cell.VMerge
 				end := GetCellIDStringFromCoords(endCol, endRow)
-				mc.Ref = start + ":" + end
+				mc.Ref = start + cellRangeChar + end
 				if worksheet.MergeCells == nil {
 					worksheet.MergeCells = &xlsxMergeCells{}
 				}

+ 7 - 0
stream_file_builder.go

@@ -119,6 +119,13 @@ func (sb *StreamFileBuilder) AddSheet(name string, headers []string, cellTypes [
 	return nil
 }
 
+// AddValidation will add a validation to a specific column.
+func (sb *StreamFileBuilder) AddValidation(sheetIndex, colIndex, rowStartIndex int, validation *xlsxCellDataValidation) {
+	sheet := sb.xlsxFile.Sheets[sheetIndex]
+	column := sheet.Col(colIndex)
+	column.SetDataValidationWithStart(validation, rowStartIndex)
+}
+
 // Build begins streaming the XLSX file to the io, by writing all the XLSX metadata. It creates a StreamFile struct
 // that can be used to write the rows to the sheets.
 func (sb *StreamFileBuilder) Build() (*StreamFile, error) {

+ 1 - 1
stream_test.go

@@ -16,7 +16,7 @@ const (
 
 type StreamSuite struct{}
 
-var _ = Suite(&SheetSuite{})
+var _ = Suite(&StreamSuite{})
 
 func (s *StreamSuite) TestTestsShouldMakeRealFilesShouldBeFalse(t *C) {
 	if TestsShouldMakeRealFiles {

+ 45 - 18
xmlWorksheet.go

@@ -230,23 +230,50 @@ type xlsxCellDataValidations struct {
 	Count           int                       `xml:"count,attr"`
 }
 
-// xlsxCellDataValidation single data validation
+// xlsxCellDataValidation
+// A single item of data validation defined on a range of the worksheet.
+// The list validation type would more commonly be called "a drop down box."
 type xlsxCellDataValidation struct {
-	AllowBlank       bool    `xml:"allowBlank,attr"`         // allow empty
-	ShowInputMessage bool    `xml:"showInputMessage,attr"`   // 1, true,0,false,  select cell,  Whether the input message is displayed
-	ShowErrorMessage bool    `xml:"showErrorMessage,attr"`   // 1, true,0,false,  input error value, Whether the error message is displayed
-	ErrorStyle       *string `xml:"errorStyle,attr"`         //error icon style, warning, infomation,stop
-	ErrorTitle       *string `xml:"errorTitle,attr"`         // error title
-	Operator         string  `xml:"operator,attr,omitempty"` //
-	Error            *string `xml:"error,attr"`              // input error value,  notice message
-	PromptTitle      *string `xml:"promptTitle,attr"`
-	Prompt           *string `xml:"prompt,attr"`
-	Type             string  `xml:"type,attr"`            //data type, none,custom,date,decimal,list, textLength,time,whole
-	Sqref            string  `xml:"sqref,attr,omitempty"` //Validity of data validation rules, cell and range, eg: A1 OR A1:A20
-	Formula1         string  `xml:"formula1"`             // data validation role
-	Formula2         string  `xml:"formula2,omitempty"`   //data validation role
-	minRow           int     //`xml:"-"`
-	maxRow           int     //`xml:"-"`
+	// A boolean value indicating whether the data validation allows the use of empty or blank
+	//entries. 1 means empty entries are OK and do not violate the validation constraints.
+	AllowBlank bool `xml:"allowBlank,attr,omitempty"`
+	// A boolean value indicating whether to display the input prompt message.
+	ShowInputMessage bool `xml:"showInputMessage,attr,omitempty"`
+	// A boolean value indicating whether to display the error alert message when an invalid
+	// value has been entered, according to the criteria specified.
+	ShowErrorMessage bool `xml:"showErrorMessage,attr,omitempty"`
+	// The style of error alert used for this data validation.
+	// warning, infomation, or stop
+	// Stop will prevent the user from entering data that does not pass validation.
+	ErrorStyle *string `xml:"errorStyle,attr"`
+	// Title bar text of error alert.
+	ErrorTitle *string `xml:"errorTitle,attr"`
+	// The relational operator used with this data validation.
+	// The possible values for this can be equal, notEqual, lessThan, etc.
+	// This only applies to certain validation types.
+	Operator string `xml:"operator,attr,omitempty"`
+	// Message text of error alert.
+	Error *string `xml:"error,attr"`
+	// Title bar text of input prompt.
+	PromptTitle *string `xml:"promptTitle,attr"`
+	// Message text of input prompt.
+	Prompt *string `xml:"prompt,attr"`
+	// The type of data validation.
+	// none, custom, date, decimal, list, textLength, time, whole
+	Type string `xml:"type,attr"`
+	// Range over which data validation is applied.
+	// Cell or range, eg: A1 OR A1:A20
+	Sqref string `xml:"sqref,attr,omitempty"`
+	// The first formula in the Data Validation dropdown. It is used as a bounds for 'between' and
+	// 'notBetween' relational operators, and the only formula used for other relational operators
+	// (equal, notEqual, lessThan, lessThanOrEqual, greaterThan, greaterThanOrEqual), or for custom
+	// or list type data validation. The content can be a formula or a constant or a list series (comma separated values).
+	Formula1 string `xml:"formula1"`
+	// The second formula in the DataValidation dropdown. It is used as a bounds for 'between' and
+	// 'notBetween' relational operators only.
+	Formula2 string `xml:"formula2,omitempty"`
+	minRow   int    //`xml:"-"`
+	maxRow   int    //`xml:"-"`
 	//minCol         int     `xml:"-"` //spare
 	//maxCol         int     `xml:"-"` //spare
 }
@@ -286,8 +313,8 @@ func (mc *xlsxMergeCells) getExtent(cellRef string) (int, int, error) {
 		return 0, 0, nil
 	}
 	for _, cell := range mc.Cells {
-		if strings.HasPrefix(cell.Ref, cellRef+":") {
-			parts := strings.Split(cell.Ref, ":")
+		if strings.HasPrefix(cell.Ref, cellRef+cellRangeChar) {
+			parts := strings.Split(cell.Ref, cellRangeChar)
 			startx, starty, err := GetCoordsFromCellIDString(parts[0])
 			if err != nil {
 				return -1, -1, err