Browse Source

Merge pull request #477 from tealeg/fix-worksheet-cols

Fix worksheet cols
Geoffrey J. Teale 6 years ago
parent
commit
1eff72e036
25 changed files with 1860 additions and 831 deletions
  1. 12 12
      cell.go
  2. 357 65
      col.go
  3. 579 0
      col_test.go
  4. 18 8
      data_validation.go
  5. 118 120
      data_validation_test.go
  6. 8 8
      date.go
  7. 1 0
      file.go
  8. 4 3
      file_test.go
  9. 3 2
      format_code.go
  10. 1 1
      go.mod
  11. 4 0
      go.sum
  12. 22 64
      lib.go
  13. 24 26
      lib_test.go
  14. 1 2
      row.go
  15. 194 120
      sheet.go
  16. 67 50
      sheet_test.go
  17. 56 27
      stream_file.go
  18. 77 89
      stream_file_builder.go
  19. 14 0
      stream_file_builder_test.go
  20. 5 5
      stream_style.go
  21. 101 77
      stream_style_test.go
  22. 158 117
      stream_test.go
  23. 1 1
      write.go
  24. 12 8
      xmlStyle.go
  25. 23 26
      xmlWorksheet.go

+ 12 - 12
cell.go

@@ -66,7 +66,7 @@ type Cell struct {
 	HMerge         int
 	VMerge         int
 	cellType       CellType
-	DataValidation *xlsxCellDataValidation
+	DataValidation *xlsxDataValidation
 }
 
 // CellInterface defines the public API of the Cell.
@@ -388,31 +388,31 @@ func (c *Cell) FormattedValue() (string, error) {
 }
 
 // SetDataValidation set data validation
-func (c *Cell) SetDataValidation(dd *xlsxCellDataValidation) {
+func (c *Cell) SetDataValidation(dd *xlsxDataValidation) {
 	c.DataValidation = dd
 }
 
-// CellMetadata represents anything attributable to a cell
+// StreamingCellMetadata represents anything attributable to a cell
 // except for the cell data itself. For example, it is used
 // in StreamFileBuilder.AddSheetWithDefaultColumnMetadata to
 // associate default attributes for cells in a particular column
-type CellMetadata struct {
+type StreamingCellMetadata struct {
 	cellType    CellType
 	streamStyle StreamStyle
 }
 
 var (
-	DefaultStringCellMetadata  CellMetadata
-	DefaultNumericCellMetadata CellMetadata
-	DefaultDecimalCellMetadata CellMetadata
-	DefaultIntegerCellMetadata CellMetadata
-	DefaultDateCellMetadata    CellMetadata
+	DefaultStringStreamingCellMetadata  StreamingCellMetadata
+	DefaultNumericStreamingCellMetadata StreamingCellMetadata
+	DefaultDecimalStreamingCellMetadata StreamingCellMetadata
+	DefaultIntegerStreamingCellMetadata StreamingCellMetadata
+	DefaultDateStreamingCellMetadata    StreamingCellMetadata
 )
 
-func MakeCellMetadata(cellType CellType, streamStyle StreamStyle) CellMetadata {
-	return CellMetadata{cellType, streamStyle}
+func MakeStreamingCellMetadata(cellType CellType, streamStyle StreamStyle) StreamingCellMetadata {
+	return StreamingCellMetadata{cellType, streamStyle}
 }
 
-func (cm CellMetadata) Ptr() *CellMetadata {
+func (cm StreamingCellMetadata) Ptr() *StreamingCellMetadata {
 	return &cm
 }

+ 357 - 65
col.go

@@ -6,17 +6,41 @@ const Excel2006MaxRowCount = 1048576
 const Excel2006MaxRowIndex = Excel2006MaxRowCount - 1
 
 type Col struct {
-	Min             int
-	Max             int
-	Hidden          bool
-	Width           float64
-	Collapsed       bool
-	OutlineLevel    uint8
-	numFmt          string
-	parsedNumFmt    *parsedNumberFormat
-	style           *Style
-	DataValidation  []*xlsxCellDataValidation
-	defaultCellType *CellType
+	Min          int
+	Max          int
+	Hidden       bool
+	Width        float64
+	Collapsed    bool
+	OutlineLevel uint8
+	BestFit      bool
+	CustomWidth  bool
+	Phonetic     bool
+	numFmt       string
+	parsedNumFmt *parsedNumberFormat
+	style        *Style
+	outXfID      int
+}
+
+// NewColForRange return a pointer to a new Col, which will apply to
+// columns in the range min to max (inclusive).  Note, in order for
+// this Col to do anything useful you must set some of its parameters
+// and then apply it to a Sheet by calling sheet.SetColParameters.
+func NewColForRange(min, max int) *Col {
+	if max < min {
+		// Nice try ;-)
+		return &Col{Min: max, Max: min}
+	}
+
+	return &Col{Min: min, Max: max}
+}
+
+// SetWidth sets the width of columns that have this Col applied to
+// them.  The width is expressed as the number of characters of the
+// maximum digit width of the numbers 0-9 as rendered in the normal
+// style's font.
+func (c *Col) SetWidth(width float64) {
+	c.Width = width
+	c.CustomWidth = true
 }
 
 // SetType will set the format string of a column based on the type that you want to set it to.
@@ -42,13 +66,6 @@ func (c *Col) SetType(cellType CellType) {
 	}
 }
 
-// SetCellMetadata sets the CellMetadata related attributes
-// of a Col
-func (c *Col) SetCellMetadata(cellMetadata CellMetadata) {
-	c.defaultCellType = &cellMetadata.cellType
-	c.SetStreamStyle(cellMetadata.streamStyle)
-}
-
 // GetStyle returns the Style associated with a Col
 func (c *Col) GetStyle() *Style {
 	return c.style
@@ -59,53 +76,6 @@ func (c *Col) SetStyle(style *Style) {
 	c.style = style
 }
 
-// SetDataValidation set data validation with zero based start and end.
-// Set end to -1 for all rows.
-func (c *Col) SetDataValidation(dd *xlsxCellDataValidation, start, end int) {
-	if end < 0 {
-		end = Excel2006MaxRowIndex
-	}
-
-	dd.minRow = start
-	dd.maxRow = end
-
-	tmpDD := make([]*xlsxCellDataValidation, 0)
-	for _, item := range c.DataValidation {
-		if item.maxRow < dd.minRow {
-			tmpDD = append(tmpDD, item) //No intersection
-		} else if item.minRow > dd.maxRow {
-			tmpDD = append(tmpDD, item) //No intersection
-		} else if dd.minRow <= item.minRow && dd.maxRow >= item.maxRow {
-			continue //union , item can be ignored
-		} else if dd.minRow >= item.minRow {
-			//Split into three or two, Newly added object, intersect with the current object in the lower half
-			tmpSplit := new(xlsxCellDataValidation)
-			*tmpSplit = *item
-
-			if dd.minRow > item.minRow { //header whetherneed to split
-				item.maxRow = dd.minRow - 1
-				tmpDD = append(tmpDD, item)
-			}
-			if dd.maxRow < tmpSplit.maxRow { //footer whetherneed to split
-				tmpSplit.minRow = dd.maxRow + 1
-				tmpDD = append(tmpDD, tmpSplit)
-			}
-
-		} else {
-			item.minRow = dd.maxRow + 1
-			tmpDD = append(tmpDD, item)
-		}
-	}
-	tmpDD = append(tmpDD, dd)
-	c.DataValidation = tmpDD
-}
-
-// SetDataValidationWithStart set data validation with a zero basd start row.
-// This will apply to the rest of the rest of the column.
-func (c *Col) SetDataValidationWithStart(dd *xlsxCellDataValidation, start int) {
-	c.SetDataValidation(dd, start, -1)
-}
-
 // SetStreamStyle sets the style and number format id to the ones specified in the given StreamStyle
 func (c *Col) SetStreamStyle(style StreamStyle) {
 	c.style = style.style
@@ -119,3 +89,325 @@ func (c *Col) GetStreamStyle() StreamStyle {
 	// returning 0 which maps to formatCode "general"
 	return StreamStyle{builtInNumFmtInv[c.numFmt], c.style}
 }
+
+func (c *Col) SetOutlineLevel(outlineLevel uint8) {
+	c.OutlineLevel = outlineLevel
+}
+
+// copyToRange is an internal convenience function to make a copy of a
+// Col with a different Min and Max value, it is not intended as a
+// general purpose Col copying function as you must still insert the
+// resulting Col into the Col Store.
+func (c *Col) copyToRange(min, max int) *Col {
+	return &Col{
+		Min:          min,
+		Max:          max,
+		Hidden:       c.Hidden,
+		Width:        c.Width,
+		Collapsed:    c.Collapsed,
+		OutlineLevel: c.OutlineLevel,
+		BestFit:      c.BestFit,
+		CustomWidth:  c.CustomWidth,
+		Phonetic:     c.Phonetic,
+		numFmt:       c.numFmt,
+		parsedNumFmt: c.parsedNumFmt,
+		style:        c.style,
+	}
+}
+
+type ColStoreNode struct {
+	Col  *Col
+	Prev *ColStoreNode
+	Next *ColStoreNode
+}
+
+//
+func (csn *ColStoreNode) findNodeForColNum(num int) *ColStoreNode {
+	switch {
+	case num >= csn.Col.Min && num <= csn.Col.Max:
+		return csn
+
+	case num < csn.Col.Min:
+		if csn.Prev == nil {
+			return nil
+		}
+		if csn.Prev.Col.Max < num {
+			return nil
+		}
+		return csn.Prev.findNodeForColNum(num)
+
+	case num > csn.Col.Max:
+		if csn.Next == nil {
+			return nil
+		}
+		if csn.Next.Col.Min > num {
+			return nil
+		}
+		return csn.Next.findNodeForColNum(num)
+	}
+	return nil
+}
+
+// ColStore is the working store of Col definitions, it will simplify all Cols added to it, to ensure there ar no overlapping definitions.
+type ColStore struct {
+	Root *ColStoreNode
+	Len  int
+}
+
+// Add a Col to the ColStore. If it overwrites all, or part of some
+// existing Col's range of columns the that Col will be adjusted
+// and/or split to make room for the new Col.
+func (cs *ColStore) Add(col *Col) *ColStoreNode {
+	newNode := &ColStoreNode{Col: col}
+	if cs.Root == nil {
+		cs.Root = newNode
+		cs.Len = 1
+		return newNode
+	}
+	cs.makeWay(cs.Root, newNode)
+	return newNode
+}
+
+func (cs *ColStore) FindColByIndex(index int) *Col {
+	csn := cs.findNodeForColNum(index)
+	if csn != nil {
+		return csn.Col
+	}
+	return nil
+}
+
+func (cs *ColStore) findNodeForColNum(num int) *ColStoreNode {
+	if cs.Root == nil {
+		return nil
+	}
+	return cs.Root.findNodeForColNum(num)
+}
+
+func (cs *ColStore) removeNode(node *ColStoreNode) {
+	if node.Prev != nil {
+		if node.Next != nil {
+			node.Prev.Next = node.Next
+		} else {
+			node.Prev.Next = nil
+		}
+
+	}
+	if node.Next != nil {
+		if node.Prev != nil {
+			node.Next.Prev = node.Prev
+		} else {
+			node.Next.Prev = nil
+		}
+	}
+	if cs.Root == node {
+		switch {
+		case node.Prev != nil:
+			cs.Root = node.Prev
+		case node.Next != nil:
+			cs.Root = node.Next
+		default:
+			cs.Root = nil
+		}
+	}
+	node.Next = nil
+	node.Prev = nil
+	cs.Len -= 1
+}
+
+// makeWay will adjust the Min and Max of this ColStoreNode's Col to
+// make way for a new ColStoreNode's Col. If necessary it will
+// generate an additional ColStoreNode with a new Col covering the
+// "tail" portion of this ColStoreNode's Col should the new node lay
+// completely within the range of this one, but without reaching its
+// maximum extent.
+func (cs *ColStore) makeWay(node1, node2 *ColStoreNode) {
+	switch {
+	case node1.Col.Max < node2.Col.Min:
+		// The node2 starts after node1 ends, there's no overlap
+		//
+		// Node1 |----|
+		// Node2        |----|
+		if node1.Next != nil {
+			if node1.Next.Col.Min <= node2.Col.Max {
+				cs.makeWay(node1.Next, node2)
+				return
+			}
+			cs.addNode(node1, node2, node1.Next)
+			return
+		}
+		cs.addNode(node1, node2, nil)
+		return
+
+	case node1.Col.Min > node2.Col.Max:
+		// Node2 ends before node1 begins, there's no overlap
+		//
+		// Node1         |-----|
+		// Node2  |----|
+		if node1.Prev != nil {
+			if node1.Prev.Col.Max >= node2.Col.Min {
+				cs.makeWay(node1.Prev, node2)
+				return
+			}
+			cs.addNode(node1.Prev, node2, node1)
+			return
+		}
+		cs.addNode(nil, node2, node1)
+		return
+
+	case node1.Col.Min == node2.Col.Min && node1.Col.Max == node2.Col.Max:
+		// Exact match
+		//
+		// Node1 |xxx|
+		// Node2 |---|
+
+		prev := node1.Prev
+		next := node1.Next
+		cs.removeNode(node1)
+		cs.addNode(prev, node2, next)
+		// Remove node may have set the root to nil
+		if cs.Root == nil {
+			cs.Root = node2
+		}
+		return
+
+	case node1.Col.Min > node2.Col.Min && node1.Col.Max < node2.Col.Max:
+		// Node2 envelopes node1
+		//
+		// Node1  |xx|
+		// Node2 |----|
+
+		prev := node1.Prev
+		next := node1.Next
+		cs.removeNode(node1)
+		switch {
+		case prev == node2:
+			node2.Next = next
+		case next == node2:
+			node2.Prev = prev
+		default:
+			cs.addNode(prev, node2, next)
+		}
+
+		if node2.Prev != nil && node2.Prev.Col.Max >= node2.Col.Min {
+			cs.makeWay(prev, node2)
+		}
+		if node2.Next != nil && node2.Next.Col.Min <= node2.Col.Max {
+			cs.makeWay(next, node2)
+		}
+
+		if cs.Root == nil {
+			cs.Root = node2
+		}
+
+	case node1.Col.Min < node2.Col.Min && node1.Col.Max > node2.Col.Max:
+		// Node2 bisects node1:
+		//
+		// Node1 |---xx---|
+		// Node2    |--|
+		newCol := node1.Col.copyToRange(node2.Col.Max+1, node1.Col.Max)
+		newNode := &ColStoreNode{Col: newCol}
+		cs.addNode(node1, newNode, node1.Next)
+		node1.Col.Max = node2.Col.Min - 1
+		cs.addNode(node1, node2, newNode)
+		return
+
+	case node1.Col.Max >= node2.Col.Min && node1.Col.Min < node2.Col.Min:
+		// Node2 overlaps node1 at some point above it's minimum:
+		//
+		//  Node1  |----xx|
+		//  Node2      |-------|
+		next := node1.Next
+		node1.Col.Max = node2.Col.Min - 1
+		if next == node2 {
+			return
+		}
+		cs.addNode(node1, node2, next)
+		if next != nil && next.Col.Min <= node2.Col.Max {
+			cs.makeWay(next, node2)
+		}
+		return
+
+	case node1.Col.Min <= node2.Col.Max && node1.Col.Min > node2.Col.Min:
+		// Node2 overlaps node1 at some point below it's maximum:
+		//
+		// Node1:     |------|
+		// Node2: |----xx|
+		prev := node1.Prev
+		node1.Col.Min = node2.Col.Max + 1
+		if prev == node2 {
+			return
+		}
+		cs.addNode(prev, node2, node1)
+		if prev != nil && prev.Col.Max >= node2.Col.Min {
+			cs.makeWay(node1.Prev, node2)
+		}
+		return
+	}
+	return
+}
+
+func (cs *ColStore) addNode(prev, this, next *ColStoreNode) {
+	if prev != nil {
+		prev.Next = this
+	}
+	this.Prev = prev
+	this.Next = next
+	if next != nil {
+		next.Prev = this
+	}
+	cs.Len += 1
+}
+
+func (cs *ColStore) getOrMakeColsForRange(start *ColStoreNode, min, max int) []*Col {
+	cols := []*Col{}
+	var csn *ColStoreNode
+	var newCol *Col
+	switch {
+	case start == nil:
+		newCol = NewColForRange(min, max)
+		csn = cs.Add(newCol)
+	case start.Col.Min <= min && start.Col.Max >= min:
+		csn = start
+	case start.Col.Min < min && start.Col.Max < min:
+		if start.Next != nil {
+			return cs.getOrMakeColsForRange(start.Next, min, max)
+		}
+		newCol = NewColForRange(min, max)
+		csn = cs.Add(newCol)
+	case start.Col.Min > min:
+		if start.Col.Min > max {
+			newCol = NewColForRange(min, max)
+		} else {
+			newCol = NewColForRange(min, start.Col.Min-1)
+		}
+		csn = cs.Add(newCol)
+	}
+
+	cols = append(cols, csn.Col)
+	if csn.Col.Max >= max {
+		return cols
+	}
+	cols = append(cols, cs.getOrMakeColsForRange(csn.Next, csn.Col.Max+1, max)...)
+	return cols
+}
+
+func chainOp(csn *ColStoreNode, fn func(idx int, col *Col)) {
+	for csn.Prev != nil {
+		csn = csn.Prev
+	}
+
+	var i int
+	for i = 0; csn.Next != nil; i++ {
+		fn(i, csn.Col)
+		csn = csn.Next
+	}
+	fn(i+1, csn.Col)
+}
+
+// ForEach calls the function fn for each Col defined in the ColStore.
+func (cs *ColStore) ForEach(fn func(idx int, col *Col)) {
+	if cs.Root == nil {
+		return
+	}
+	chainOp(cs.Root, fn)
+}

+ 579 - 0
col_test.go

@@ -1 +1,580 @@
 package xlsx
+
+import (
+	"testing"
+
+	qt "github.com/frankban/quicktest"
+	. "gopkg.in/check.v1"
+)
+
+var notNil = qt.Not(qt.IsNil)
+
+func TestNewColForRange(t *testing.T) {
+	c := qt.New(t)
+	col := NewColForRange(30, 45)
+	c.Assert(col, notNil)
+	c.Assert(col.Min, qt.Equals, 30)
+	c.Assert(col.Max, qt.Equals, 45)
+
+	// Auto fix the min/max
+	col = NewColForRange(45, 30)
+	c.Assert(col, notNil)
+	c.Assert(col.Min, qt.Equals, 30)
+	c.Assert(col.Max, qt.Equals, 45)
+}
+
+func TestCol(t *testing.T) {
+	c := qt.New(t)
+	c.Run("SetType", func(c *qt.C) {
+		expectations := map[CellType]string{
+			CellTypeString:        builtInNumFmt[builtInNumFmtIndex_STRING],
+			CellTypeNumeric:       builtInNumFmt[builtInNumFmtIndex_INT],
+			CellTypeBool:          builtInNumFmt[builtInNumFmtIndex_GENERAL],
+			CellTypeInline:        builtInNumFmt[builtInNumFmtIndex_STRING],
+			CellTypeError:         builtInNumFmt[builtInNumFmtIndex_GENERAL],
+			CellTypeDate:          builtInNumFmt[builtInNumFmtIndex_GENERAL],
+			CellTypeStringFormula: builtInNumFmt[builtInNumFmtIndex_STRING],
+		}
+
+		assertSetType := func(cellType CellType, expectation string) {
+			col := &Col{}
+			col.SetType(cellType)
+			c.Assert(col.numFmt, qt.Equals, expectation)
+		}
+		for k, v := range expectations {
+			assertSetType(k, v)
+		}
+	})
+	c.Run("SetWidth", func(c *qt.C) {
+		col := &Col{}
+		col.SetWidth(20.2)
+		c.Assert(col.Width, qt.Equals, 20.2)
+		c.Assert(col.CustomWidth, qt.Equals, true)
+	})
+
+	c.Run("copyToRange", func(c *qt.C) {
+		nf := &parsedNumberFormat{}
+		s := &Style{}
+		c1 := &Col{
+			Min:          1,
+			Max:          11,
+			Hidden:       true,
+			Width:        300.4,
+			Collapsed:    true,
+			OutlineLevel: 2,
+			numFmt:       "-0.00",
+			parsedNumFmt: nf,
+			style:        s,
+		}
+
+		c2 := c1.copyToRange(4, 10)
+		c.Assert(c2.Min, qt.Equals, 4)
+		c.Assert(c2.Max, qt.Equals, 10)
+		c.Assert(c2.Hidden, qt.Equals, c1.Hidden)
+		c.Assert(c2.Width, qt.Equals, c1.Width)
+		c.Assert(c2.Collapsed, qt.Equals, c1.Collapsed)
+		c.Assert(c2.OutlineLevel, qt.Equals, c1.OutlineLevel)
+		c.Assert(c2.numFmt, qt.Equals, c1.numFmt)
+		c.Assert(c2.parsedNumFmt, qt.Equals, c1.parsedNumFmt)
+		c.Assert(c2.style, qt.Equals, c1.style)
+	})
+
+}
+
+type ColStoreSuite struct{}
+
+var _ = Suite(&ColStoreSuite{})
+
+func (css *ColStoreSuite) TestAddRootNode(c *C) {
+	col := &Col{Min: 1, Max: 1}
+	cs := ColStore{}
+	cs.Add(col)
+	c.Assert(cs.Len, Equals, 1)
+	c.Assert(cs.Root.Col, Equals, col)
+}
+
+func TestMakeWay(t *testing.T) {
+	c := qt.New(t)
+	assertWayMade := func(cols []*Col, chainFunc func(*ColStore)) {
+
+		cs := &ColStore{}
+		for _, col := range cols {
+			_ = cs.Add(col)
+		}
+		chainFunc(cs)
+	}
+
+	// Col1: |--|
+	// Col2:    |--|
+	assertWayMade([]*Col{{Min: 1, Max: 2}, {Min: 3, Max: 4}},
+		func(cs *ColStore) {
+			c.Assert(cs.Len, qt.Equals, 2)
+			root := cs.Root
+			c.Assert(root.Col.Min, qt.Equals, 1)
+			c.Assert(root.Col.Max, qt.Equals, 2)
+			c.Assert(root.Prev, qt.IsNil)
+			c.Assert(root.Next, notNil)
+			node2 := root.Next
+			c.Assert(node2.Prev, qt.Equals, root)
+			c.Assert(node2.Next, qt.IsNil)
+			c.Assert(node2.Col.Min, qt.Equals, 3)
+			c.Assert(node2.Col.Max, qt.Equals, 4)
+		})
+
+	// Col1:    |--|
+	// Col2: |--|
+	assertWayMade([]*Col{{Min: 3, Max: 4}, {Min: 1, Max: 2}},
+		func(cs *ColStore) {
+			root := cs.Root
+			c.Assert(cs.Len, qt.Equals, 2)
+			c.Assert(root.Col.Min, qt.Equals, 3)
+			c.Assert(root.Col.Max, qt.Equals, 4)
+			c.Assert(root.Prev, notNil)
+			c.Assert(root.Next, qt.IsNil)
+			node2 := root.Prev
+			c.Assert(node2.Next, qt.Equals, root)
+			c.Assert(node2.Prev, qt.IsNil)
+			c.Assert(node2.Col.Min, qt.Equals, 1)
+			c.Assert(node2.Col.Max, qt.Equals, 2)
+		})
+
+	// Col1: |--x|
+	// Col2:   |--|
+	assertWayMade([]*Col{{Min: 1, Max: 3}, {Min: 3, Max: 4}},
+		func(cs *ColStore) {
+			root := cs.Root
+			c.Assert(cs.Len, qt.Equals, 2)
+			c.Assert(root.Col.Min, qt.Equals, 1)
+			c.Assert(root.Col.Max, qt.Equals, 2)
+			c.Assert(root.Prev, qt.IsNil)
+			c.Assert(root.Next, notNil)
+			node2 := root.Next
+			c.Assert(node2.Prev, qt.Equals, root)
+			c.Assert(node2.Next, qt.IsNil)
+			c.Assert(node2.Col.Min, qt.Equals, 3)
+			c.Assert(node2.Col.Max, qt.Equals, 4)
+		})
+
+	// Col1:  |x-|
+	// Col2: |--|
+	assertWayMade([]*Col{{Min: 2, Max: 3}, {Min: 1, Max: 2}},
+		func(cs *ColStore) {
+			root := cs.Root
+			c.Assert(cs.Len, qt.Equals, 2)
+			c.Assert(root.Col.Min, qt.Equals, 3)
+			c.Assert(root.Col.Max, qt.Equals, 3)
+			c.Assert(root.Prev, notNil)
+			c.Assert(root.Next, qt.IsNil)
+			node2 := root.Prev
+			c.Assert(node2.Next, qt.Equals, root)
+			c.Assert(node2.Prev, qt.IsNil)
+			c.Assert(node2.Col.Min, qt.Equals, 1)
+			c.Assert(node2.Col.Max, qt.Equals, 2)
+		})
+
+	// Col1: |---xx---|
+	// Col2:    |--|
+	assertWayMade([]*Col{{Min: 1, Max: 8}, {Min: 4, Max: 5}},
+		func(cs *ColStore) {
+			root := cs.Root
+			c.Assert(cs.Len, qt.Equals, 3)
+			c.Assert(root.Prev, qt.IsNil)
+			c.Assert(root.Next, notNil)
+			c.Assert(root.Col.Min, qt.Equals, 1)
+			c.Assert(root.Col.Max, qt.Equals, 3)
+			node2 := root.Next
+			c.Assert(node2.Prev, qt.Equals, root)
+			c.Assert(node2.Col.Min, qt.Equals, 4)
+			c.Assert(node2.Col.Max, qt.Equals, 5)
+			c.Assert(node2.Next, notNil)
+			node3 := node2.Next
+			c.Assert(node3.Prev, qt.Equals, node2)
+			c.Assert(node3.Next, qt.IsNil)
+			c.Assert(node3.Col.Min, qt.Equals, 6)
+			c.Assert(node3.Col.Max, qt.Equals, 8)
+		})
+
+	// Col1: |xx|
+	// Col2: |--|
+	assertWayMade([]*Col{{Min: 1, Max: 2, Width: 40.1}, {Min: 1, Max: 2, Width: 10.0}},
+		func(cs *ColStore) {
+			root := cs.Root
+			c.Assert(cs.Len, qt.Equals, 1)
+			c.Assert(root, notNil)
+			c.Assert(root.Prev, qt.IsNil)
+			c.Assert(root.Next, qt.IsNil)
+			c.Assert(root.Col.Min, qt.Equals, 1)
+			c.Assert(root.Col.Max, qt.Equals, 2)
+			// This is how we establish we have the new node, and not the old one
+			c.Assert(root.Col.Width, qt.Equals, 10.0)
+		})
+
+	// Col1:  |xx|
+	// Col2: |----|
+	assertWayMade([]*Col{{Min: 2, Max: 3, Width: 40.1}, {Min: 1, Max: 4, Width: 10.0}},
+		func(cs *ColStore) {
+			root := cs.Root
+			c.Assert(cs.Len, qt.Equals, 1)
+			c.Assert(root.Prev, qt.IsNil)
+			c.Assert(root.Next, qt.IsNil)
+			c.Assert(root.Col.Min, qt.Equals, 1)
+			c.Assert(root.Col.Max, qt.Equals, 4)
+			// This is how we establish we have the new node, and not the old one
+			c.Assert(root.Col.Width, qt.Equals, 10.0)
+		})
+
+	// Col1: |--|
+	// Col2:    |--|
+	// Col3:       |--|
+	assertWayMade([]*Col{{Min: 1, Max: 2}, {Min: 3, Max: 4}, {Min: 5, Max: 6}},
+		func(cs *ColStore) {
+			root := cs.Root
+			c.Assert(cs.Len, qt.Equals, 3)
+			c.Assert(root.Prev, qt.IsNil)
+			c.Assert(root.Next, notNil)
+			c.Assert(root.Col.Min, qt.Equals, 1)
+			c.Assert(root.Col.Max, qt.Equals, 2)
+			node2 := root.Next
+			c.Assert(node2.Prev, qt.Equals, root)
+			c.Assert(node2.Col.Min, qt.Equals, 3)
+			c.Assert(node2.Col.Max, qt.Equals, 4)
+			c.Assert(node2.Next, notNil)
+			node3 := node2.Next
+			c.Assert(node3.Prev, qt.Equals, node2)
+			c.Assert(node3.Next, qt.IsNil)
+			c.Assert(node3.Col.Min, qt.Equals, 5)
+			c.Assert(node3.Col.Max, qt.Equals, 6)
+		})
+
+	// Col1:       |--|
+	// Col2:    |--|
+	// Col3: |--|
+	assertWayMade([]*Col{{Min: 5, Max: 6}, {Min: 3, Max: 4}, {Min: 1, Max: 2}},
+		func(cs *ColStore) {
+			root := cs.Root
+			c.Assert(cs.Len, qt.Equals, 3)
+			c.Assert(root.Prev, notNil)
+			c.Assert(root.Next, qt.IsNil)
+			c.Assert(root.Col.Min, qt.Equals, 5)
+			c.Assert(root.Col.Max, qt.Equals, 6)
+			node2 := root.Prev
+			c.Assert(node2.Next, qt.Equals, root)
+			c.Assert(node2.Col.Min, qt.Equals, 3)
+			c.Assert(node2.Col.Max, qt.Equals, 4)
+			c.Assert(node2.Prev, notNil)
+			node3 := node2.Prev
+			c.Assert(node3.Next, qt.Equals, node2)
+			c.Assert(node3.Prev, qt.IsNil)
+			c.Assert(node3.Col.Min, qt.Equals, 1)
+			c.Assert(node3.Col.Max, qt.Equals, 2)
+		})
+
+	// Col1: |--|
+	// Col2:          |--|
+	// Col3:     |--|
+	assertWayMade([]*Col{{Min: 1, Max: 2}, {Min: 10, Max: 11}, {Min: 5, Max: 6}},
+		func(cs *ColStore) {
+			root := cs.Root
+			c.Assert(cs.Len, qt.Equals, 3)
+			c.Assert(root.Prev, qt.IsNil)
+			c.Assert(root.Next, notNil)
+			c.Assert(root.Col.Min, qt.Equals, 1)
+			c.Assert(root.Col.Max, qt.Equals, 2)
+			node2 := root.Next
+			c.Assert(node2.Prev, qt.Equals, root)
+			c.Assert(node2.Col.Min, qt.Equals, 5)
+			c.Assert(node2.Col.Max, qt.Equals, 6)
+			c.Assert(node2.Next, notNil)
+			node3 := node2.Next
+			c.Assert(node3.Prev, qt.Equals, node2)
+			c.Assert(node3.Next, qt.IsNil)
+			c.Assert(node3.Col.Min, qt.Equals, 10)
+			c.Assert(node3.Col.Max, qt.Equals, 11)
+		})
+
+	// Col1: |-x|
+	// Col2:        |x-|
+	// Col3:  |-------|
+	assertWayMade([]*Col{
+		{Min: 1, Max: 2}, {Min: 8, Max: 9}, {Min: 2, Max: 8}},
+		func(cs *ColStore) {
+			root := cs.Root
+			c.Assert(cs.Len, qt.Equals, 3)
+			c.Assert(root.Prev, qt.IsNil)
+			c.Assert(root.Next, notNil)
+			c.Assert(root.Col.Min, qt.Equals, 1)
+			c.Assert(root.Col.Max, qt.Equals, 1)
+			node2 := root.Next
+			c.Assert(node2.Prev, qt.Equals, root)
+			c.Assert(node2.Next, notNil)
+			c.Assert(node2.Col.Min, qt.Equals, 2)
+			c.Assert(node2.Col.Max, qt.Equals, 8)
+			node3 := node2.Next
+			c.Assert(node3.Prev, qt.Equals, node2)
+			c.Assert(node3.Next, qt.IsNil)
+			c.Assert(node3.Col.Min, qt.Equals, 9)
+			c.Assert(node3.Col.Max, qt.Equals, 9)
+		})
+
+	// Col1: |-x|
+	// Col2:        |--|
+	// Col3:  |-----|
+	assertWayMade([]*Col{
+		{Min: 1, Max: 2}, {Min: 8, Max: 9}, {Min: 2, Max: 7}},
+		func(cs *ColStore) {
+			root := cs.Root
+			c.Assert(cs.Len, qt.Equals, 3)
+			c.Assert(root.Prev, qt.IsNil)
+			c.Assert(root.Next, notNil)
+			c.Assert(root.Col.Min, qt.Equals, 1)
+			c.Assert(root.Col.Max, qt.Equals, 1)
+			node2 := root.Next
+			c.Assert(node2.Prev, qt.Equals, root)
+			c.Assert(node2.Next, notNil)
+			c.Assert(node2.Col.Min, qt.Equals, 2)
+			c.Assert(node2.Col.Max, qt.Equals, 7)
+			node3 := node2.Next
+			c.Assert(node3.Prev, qt.Equals, node2)
+			c.Assert(node3.Next, qt.IsNil)
+			c.Assert(node3.Col.Min, qt.Equals, 8)
+			c.Assert(node3.Col.Max, qt.Equals, 9)
+		})
+
+	// Col1: |--|
+	// Col2:        |x-|
+	// Col3:    |-----|
+	assertWayMade([]*Col{
+		{Min: 1, Max: 2}, {Min: 8, Max: 9}, {Min: 3, Max: 8}},
+		func(cs *ColStore) {
+			root := cs.Root
+			c.Assert(cs.Len, qt.Equals, 3)
+			c.Assert(root.Prev, qt.IsNil)
+			c.Assert(root.Next, notNil)
+			c.Assert(root.Col.Min, qt.Equals, 1)
+			c.Assert(root.Col.Max, qt.Equals, 2)
+			node2 := root.Next
+			c.Assert(node2.Prev, qt.Equals, root)
+			c.Assert(node2.Next, notNil)
+			c.Assert(node2.Col.Min, qt.Equals, 3)
+			c.Assert(node2.Col.Max, qt.Equals, 8)
+			node3 := node2.Next
+			c.Assert(node3.Prev, qt.Equals, node2)
+			c.Assert(node3.Next, qt.IsNil)
+			c.Assert(node3.Col.Min, qt.Equals, 9)
+			c.Assert(node3.Col.Max, qt.Equals, 9)
+		})
+
+	// Col1: |--|
+	// Col2:   |xx|
+	// Col3:     |--|
+	// Col4:   |--|
+	assertWayMade(
+		[]*Col{
+			{Min: 1, Max: 2},
+			{Min: 3, Max: 4, Width: 1.0},
+			{Min: 5, Max: 6},
+			{Min: 3, Max: 4, Width: 2.0},
+		},
+		func(cs *ColStore) {
+			root := cs.Root
+			c.Assert(cs.Len, qt.Equals, 3)
+			c.Assert(root.Prev, qt.IsNil)
+			c.Assert(root.Next, notNil)
+			c.Assert(root.Col.Min, qt.Equals, 1)
+			c.Assert(root.Col.Max, qt.Equals, 2)
+			node2 := root.Next
+			c.Assert(node2.Prev, qt.Equals, root)
+			c.Assert(node2.Next, notNil)
+			c.Assert(node2.Col.Min, qt.Equals, 3)
+			c.Assert(node2.Col.Max, qt.Equals, 4)
+			c.Assert(node2.Col.Width, qt.Equals, 2.0) // We have the later version
+			node3 := node2.Next
+			c.Assert(node3.Prev, qt.Equals, node2)
+			c.Assert(node3.Next, qt.IsNil)
+			c.Assert(node3.Col.Min, qt.Equals, 5)
+			c.Assert(node3.Col.Max, qt.Equals, 6)
+		})
+
+	// Col1: |-x|
+	// Col2:   |xx|
+	// Col3:     |x-|
+	// Col4:  |----|
+	assertWayMade(
+		[]*Col{
+			{Min: 1, Max: 2, Width: 1.0},
+			{Min: 3, Max: 4, Width: 2.0},
+			{Min: 5, Max: 6, Width: 3.0},
+			{Min: 2, Max: 5, Width: 4.0},
+		},
+		func(cs *ColStore) {
+			root := cs.Root
+			c.Assert(cs.Len, qt.Equals, 3)
+			c.Assert(root.Prev, qt.IsNil)
+			c.Assert(root.Next, notNil)
+			c.Assert(root.Col.Min, qt.Equals, 1)
+			c.Assert(root.Col.Max, qt.Equals, 1)
+			c.Assert(root.Col.Width, qt.Equals, 1.0)
+			node2 := root.Next
+			c.Assert(node2.Prev, qt.Equals, root)
+			c.Assert(node2.Next, notNil)
+			c.Assert(node2.Col.Min, qt.Equals, 2)
+			c.Assert(node2.Col.Max, qt.Equals, 5)
+			c.Assert(node2.Col.Width, qt.Equals, 4.0)
+			node3 := node2.Next
+			c.Assert(node3.Prev, qt.Equals, node2)
+			c.Assert(node3.Next, qt.IsNil)
+			c.Assert(node3.Col.Min, qt.Equals, 6)
+			c.Assert(node3.Col.Max, qt.Equals, 6)
+			c.Assert(node3.Col.Width, qt.Equals, 3.0)
+		})
+
+}
+
+func (css *ColStoreSuite) TestFindNodeForCol(c *C) {
+
+	assertNodeFound := func(cs *ColStore, num int, col *Col) {
+		node := cs.findNodeForColNum(num)
+		if col == nil {
+			c.Assert(node, IsNil)
+			return
+		}
+		c.Assert(node, NotNil)
+		c.Assert(node.Col, Equals, col)
+	}
+
+	cs := &ColStore{}
+	col0 := &Col{Min: 1, Max: 1}
+	cs.Add(col0)
+	col1 := &Col{Min: 2, Max: 2}
+	cs.Add(col1)
+	col2 := &Col{Min: 3, Max: 3}
+	cs.Add(col2)
+	col3 := &Col{Min: 4, Max: 4}
+	cs.Add(col3)
+	col4 := &Col{Min: 5, Max: 5}
+	cs.Add(col4)
+	col5 := &Col{Min: 100, Max: 125}
+	cs.Add(col5)
+
+	assertNodeFound(cs, 0, nil)
+	assertNodeFound(cs, 1, col0)
+	assertNodeFound(cs, 2, col1)
+	assertNodeFound(cs, 3, col2)
+	assertNodeFound(cs, 4, col3)
+	assertNodeFound(cs, 5, col4)
+	assertNodeFound(cs, 6, nil)
+	assertNodeFound(cs, 99, nil)
+	assertNodeFound(cs, 100, col5)
+	assertNodeFound(cs, 110, col5)
+	assertNodeFound(cs, 125, col5)
+	assertNodeFound(cs, 126, nil)
+}
+
+func (css *ColStoreSuite) TestRemoveNode(c *C) {
+
+	assertChain := func(cs *ColStore, chain []*Col) {
+		node := cs.Root
+		for _, col := range chain {
+			c.Assert(node, NotNil)
+			c.Assert(node.Col.Min, Equals, col.Min)
+			c.Assert(node.Col.Max, Equals, col.Max)
+			node = node.Next
+		}
+		c.Assert(node, IsNil)
+	}
+
+	cs := &ColStore{}
+	col0 := &Col{Min: 1, Max: 1}
+	cs.Add(col0)
+	col1 := &Col{Min: 2, Max: 2}
+	cs.Add(col1)
+	col2 := &Col{Min: 3, Max: 3}
+	cs.Add(col2)
+	col3 := &Col{Min: 4, Max: 4}
+	cs.Add(col3)
+	col4 := &Col{Min: 5, Max: 5}
+	cs.Add(col4)
+	c.Assert(cs.Len, Equals, 5)
+
+	cs.removeNode(cs.findNodeForColNum(5))
+	c.Assert(cs.Len, Equals, 4)
+	assertChain(cs, []*Col{col0, col1, col2, col3})
+
+	cs.removeNode(cs.findNodeForColNum(1))
+	c.Assert(cs.Len, Equals, 3)
+	assertChain(cs, []*Col{col1, col2, col3})
+}
+
+func (css *ColStoreSuite) TestForEach(c *C) {
+	cs := &ColStore{}
+	col0 := &Col{Min: 1, Max: 1, Hidden: true}
+	cs.Add(col0)
+	col1 := &Col{Min: 2, Max: 2}
+	cs.Add(col1)
+	col2 := &Col{Min: 3, Max: 3}
+	cs.Add(col2)
+	col3 := &Col{Min: 4, Max: 4}
+	cs.Add(col3)
+	col4 := &Col{Min: 5, Max: 5}
+	cs.Add(col4)
+	cs.ForEach(func(index int, col *Col) {
+		col.Phonetic = true
+	})
+
+	c.Assert(col0.Phonetic, Equals, true)
+	c.Assert(col1.Phonetic, Equals, true)
+	c.Assert(col2.Phonetic, Equals, true)
+	c.Assert(col3.Phonetic, Equals, true)
+	c.Assert(col4.Phonetic, Equals, true)
+}
+
+func (css *ColStoreSuite) TestGetOrMakeColsForRange(c *C) {
+	assertCols := func(min, max int, initalCols, expectedCols []*Col) {
+		cs := &ColStore{}
+		for _, col := range initalCols {
+			cs.Add(col)
+		}
+		result := cs.getOrMakeColsForRange(cs.Root, min, max)
+		c.Assert(result, HasLen, len(expectedCols))
+		for i := 0; i < len(expectedCols); i++ {
+			got := result[i]
+			expected := expectedCols[i]
+			c.Assert(got.Min, Equals, expected.Min)
+			c.Assert(got.Max, Equals, expected.Max)
+		}
+	}
+
+	// make everything
+	assertCols(1, 11, nil, []*Col{{Min: 1, Max: 11}})
+
+	// get everything, one col
+	assertCols(1, 11, []*Col{{Min: 1, Max: 11}}, []*Col{{Min: 1, Max: 11}})
+
+	// get everything, many cols
+	assertCols(1, 11,
+		[]*Col{
+			{Min: 1, Max: 4},
+			{Min: 5, Max: 8},
+			{Min: 9, Max: 11},
+		},
+		[]*Col{
+			{Min: 1, Max: 4},
+			{Min: 5, Max: 8},
+			{Min: 9, Max: 11},
+		},
+	)
+
+	// make missing col
+	assertCols(1, 11,
+		[]*Col{
+			{Min: 1, Max: 4},
+			{Min: 9, Max: 11},
+		},
+		[]*Col{
+			{Min: 1, Max: 4},
+			{Min: 5, Max: 8},
+			{Min: 9, Max: 11},
+		},
+	)
+
+}

+ 18 - 8
data_validation.go

@@ -61,15 +61,25 @@ const (
 	DataValidationOperatorNotEqual
 )
 
-// NewXlsxCellDataValidation return data validation struct
-func NewXlsxCellDataValidation(allowBlank bool) *xlsxCellDataValidation {
-	return &xlsxCellDataValidation{
+// NewDataValidation return data validation struct
+func NewDataValidation(startRow, startCol, endRow, endCol int, allowBlank bool) *xlsxDataValidation {
+	startX := ColIndexToLetters(startCol)
+	startY := RowIndexToString(startRow)
+	endX := ColIndexToLetters(endCol)
+	endY := RowIndexToString(endRow)
+
+	sqref := startX + startY
+	if startX != endX || startY != endY {
+		sqref += ":" + endX + endY
+	}
+	return &xlsxDataValidation{
 		AllowBlank: allowBlank,
+		Sqref:      sqref,
 	}
 }
 
 // SetError set error notice
-func (dd *xlsxCellDataValidation) SetError(style DataValidationErrorStyle, title, msg *string) {
+func (dd *xlsxDataValidation) SetError(style DataValidationErrorStyle, title, msg *string) {
 	dd.ShowErrorMessage = true
 	dd.Error = msg
 	dd.ErrorTitle = title
@@ -87,7 +97,7 @@ func (dd *xlsxCellDataValidation) SetError(style DataValidationErrorStyle, title
 }
 
 // SetInput set prompt notice
-func (dd *xlsxCellDataValidation) SetInput(title, msg *string) {
+func (dd *xlsxDataValidation) SetInput(title, msg *string) {
 	dd.ShowInputMessage = true
 	dd.PromptTitle = title
 	dd.Prompt = msg
@@ -95,7 +105,7 @@ func (dd *xlsxCellDataValidation) SetInput(title, msg *string) {
 
 // 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 {
+func (dd *xlsxDataValidation) SetDropList(keys []string) error {
 	formula := "\"" + strings.Join(keys, ",") + "\""
 	if dataValidationFormulaStrLen < len(formula) {
 		return fmt.Errorf(dataValidationFormulaStrLenErr)
@@ -111,7 +121,7 @@ func (dd *xlsxCellDataValidation) SetDropList(keys []string) error {
 // 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 {
+func (dd *xlsxDataValidation) SetInFileList(sheet string, x1, y1, x2, y2 int) error {
 	start := GetCellIDStringFromCoordsWithFixed(x1, y1, true, true)
 	if y2 < 0 {
 		y2 = Excel2006MaxRowIndex
@@ -128,7 +138,7 @@ func (dd *xlsxCellDataValidation) SetInFileList(sheet string, x1, y1, x2, y2 int
 }
 
 // SetDropList data validation range
-func (dd *xlsxCellDataValidation) SetRange(f1, f2 int, t DataValidationType, o DataValidationOperator) error {
+func (dd *xlsxDataValidation) SetRange(f1, f2 int, t DataValidationType, o DataValidationOperator) error {
 	formula1 := fmt.Sprintf("%d", f1)
 	formula2 := fmt.Sprintf("%d", f2)
 

+ 118 - 120
data_validation_test.go

@@ -3,15 +3,12 @@ package xlsx
 import (
 	"bytes"
 	"fmt"
+	"testing"
 
-	. "gopkg.in/check.v1"
+	qt "github.com/frankban/quicktest"
 )
 
-type DataValidationSuite struct{}
-
-var _ = Suite(&DataValidationSuite{})
-
-func (d *DataValidationSuite) TestDataValidation(t *C) {
+func TestDataValidation(t *testing.T) {
 	var file *File
 	var sheet *Sheet
 	var row *Row
@@ -20,6 +17,8 @@ func (d *DataValidationSuite) TestDataValidation(t *C) {
 	var title = "cell"
 	var msg = "cell msg"
 
+	c := qt.New(t)
+
 	file = NewFile()
 	sheet, err = file.AddSheet("Sheet1")
 	if err != nil {
@@ -29,240 +28,239 @@ func (d *DataValidationSuite) TestDataValidation(t *C) {
 	cell = row.AddCell()
 	cell.Value = "a1"
 
-	dd := NewXlsxCellDataValidation(true)
+	dd := NewDataValidation(0, 0, 0, 0, true)
 	err = dd.SetDropList([]string{"a1", "a2", "a3"})
-	t.Assert(err, IsNil)
+	c.Assert(err, qt.IsNil)
 
 	dd.SetInput(&title, &msg)
 	cell.SetDataValidation(dd)
 
-	dd = NewXlsxCellDataValidation(true)
+	dd = NewDataValidation(2, 0, 2, 0, true)
 	err = dd.SetDropList([]string{"c1", "c2", "c3"})
-	t.Assert(err, IsNil)
+	c.Assert(err, qt.IsNil)
 	title = "col c"
 	dd.SetInput(&title, &msg)
-	sheet.Col(2).SetDataValidation(dd, 0, 0)
+	sheet.AddDataValidation(dd)
 
-	dd = NewXlsxCellDataValidation(true)
+	dd = NewDataValidation(3, 3, 3, 7, true)
 	err = dd.SetDropList([]string{"d", "d1", "d2"})
-	t.Assert(err, IsNil)
+	c.Assert(err, qt.IsNil)
 	title = "col d range"
 	dd.SetInput(&title, &msg)
-	sheet.Col(3).SetDataValidation(dd, 3, 7)
+	sheet.AddDataValidation(dd)
 
-	dd = NewXlsxCellDataValidation(true)
+	dd = NewDataValidation(4, 1, 4, Excel2006MaxRowIndex, true)
 	err = dd.SetDropList([]string{"e1", "e2", "e3"})
-	t.Assert(err, IsNil)
+	c.Assert(err, qt.IsNil)
 	title = "col e start 3"
 	dd.SetInput(&title, &msg)
-	sheet.Col(4).SetDataValidationWithStart(dd, 1)
+	sheet.AddDataValidation(dd)
 
 	index := 5
 	rowIndex := 1
-	dd = NewXlsxCellDataValidation(true)
+	dd = NewDataValidation(rowIndex, index, rowIndex, index, true)
 	err = dd.SetRange(15, 4, DataValidationTypeTextLeng, DataValidationOperatorBetween)
-	t.Assert(err, IsNil)
-	sheet.Cell(rowIndex, index).SetDataValidation(dd)
+	c.Assert(err, qt.IsNil)
+	sheet.AddDataValidation(dd)
 	index++
 
-	dd = NewXlsxCellDataValidation(true)
+	dd = NewDataValidation(rowIndex, index, rowIndex, index, true)
 	err = dd.SetRange(10, 1, DataValidationTypeTextLeng, DataValidationOperatorEqual)
-	t.Assert(err, IsNil)
-	sheet.Cell(rowIndex, index).SetDataValidation(dd)
+	c.Assert(err, qt.IsNil)
+	sheet.AddDataValidation(dd)
 	index++
 
-	dd = NewXlsxCellDataValidation(true)
+	dd = NewDataValidation(rowIndex, index, rowIndex, index, true)
 	err = dd.SetRange(10, 1, DataValidationTypeTextLeng, DataValidationOperatorGreaterThanOrEqual)
-	t.Assert(err, IsNil)
-	sheet.Cell(rowIndex, index).SetDataValidation(dd)
+	c.Assert(err, qt.IsNil)
+	sheet.AddDataValidation(dd)
 	index++
 
-	dd = NewXlsxCellDataValidation(true)
+	dd = NewDataValidation(rowIndex, index, rowIndex, index, true)
 	err = dd.SetRange(10, 1, DataValidationTypeTextLeng, DataValidationOperatorGreaterThan)
-	t.Assert(err, IsNil)
-	sheet.Cell(rowIndex, index).SetDataValidation(dd)
+	c.Assert(err, qt.IsNil)
+	sheet.AddDataValidation(dd)
 	index++
 
-	dd = NewXlsxCellDataValidation(true)
+	dd = NewDataValidation(rowIndex, index, rowIndex, index, true)
 	err = dd.SetRange(10, 1, DataValidationTypeTextLeng, DataValidationOperatorLessThan)
-	t.Assert(err, IsNil)
-	sheet.Cell(rowIndex, index).SetDataValidation(dd)
+	c.Assert(err, qt.IsNil)
+	sheet.AddDataValidation(dd)
 	index++
 
-	dd = NewXlsxCellDataValidation(true)
+	dd = NewDataValidation(rowIndex, index, rowIndex, index, true)
 	err = dd.SetRange(10, 1, DataValidationTypeTextLeng, DataValidationOperatorLessThanOrEqual)
-	t.Assert(err, IsNil)
-	sheet.Cell(rowIndex, index).SetDataValidation(dd)
+	c.Assert(err, qt.IsNil)
+	sheet.AddDataValidation(dd)
 	index++
 
-	dd = NewXlsxCellDataValidation(true)
+	dd = NewDataValidation(rowIndex, index, rowIndex, index, true)
 	err = dd.SetRange(10, 1, DataValidationTypeTextLeng, DataValidationOperatorNotEqual)
-	t.Assert(err, IsNil)
-	sheet.Cell(rowIndex, index).SetDataValidation(dd)
+	c.Assert(err, qt.IsNil)
+	sheet.AddDataValidation(dd)
 	index++
 
-	dd = NewXlsxCellDataValidation(true)
+	dd = NewDataValidation(rowIndex, index, rowIndex, index, true)
 	err = dd.SetRange(10, 1, DataValidationTypeTextLeng, DataValidationOperatorNotBetween)
-	t.Assert(err, IsNil)
-	sheet.Cell(rowIndex, index).SetDataValidation(dd)
+	c.Assert(err, qt.IsNil)
+	sheet.AddDataValidation(dd)
 	index++
 
 	rowIndex++
 	index = 5
 
-	dd = NewXlsxCellDataValidation(true)
+	dd = NewDataValidation(rowIndex, index, rowIndex, index, true)
 	err = dd.SetRange(4, 15, DataValidationTypeWhole, DataValidationOperatorBetween)
-	t.Assert(err, IsNil)
-	sheet.Cell(rowIndex, index).SetDataValidation(dd)
+	c.Assert(err, qt.IsNil)
+	sheet.AddDataValidation(dd)
 	index++
 
-	dd = NewXlsxCellDataValidation(true)
+	dd = NewDataValidation(rowIndex, index, rowIndex, index, true)
 	err = dd.SetRange(10, 1, DataValidationTypeWhole, DataValidationOperatorEqual)
-	t.Assert(err, IsNil)
-	sheet.Cell(rowIndex, index).SetDataValidation(dd)
+	c.Assert(err, qt.IsNil)
+	sheet.AddDataValidation(dd)
 	index++
 
-	dd = NewXlsxCellDataValidation(true)
+	dd = NewDataValidation(rowIndex, index, rowIndex, index, true)
 	err = dd.SetRange(10, 1, DataValidationTypeWhole, DataValidationOperatorGreaterThanOrEqual)
-	t.Assert(err, IsNil)
-	sheet.Cell(rowIndex, index).SetDataValidation(dd)
+	c.Assert(err, qt.IsNil)
+	sheet.AddDataValidation(dd)
 	index++
 
-	dd = NewXlsxCellDataValidation(true)
+	dd = NewDataValidation(rowIndex, index, rowIndex, index, true)
 	err = dd.SetRange(10, 1, DataValidationTypeWhole, DataValidationOperatorGreaterThan)
-	t.Assert(err, IsNil)
-	sheet.Cell(rowIndex, index).SetDataValidation(dd)
+	c.Assert(err, qt.IsNil)
+	sheet.AddDataValidation(dd)
 	index++
 
-	dd = NewXlsxCellDataValidation(true)
+	dd = NewDataValidation(rowIndex, index, rowIndex, index, true)
 	err = dd.SetRange(10, 1, DataValidationTypeWhole, DataValidationOperatorLessThan)
-	t.Assert(err, IsNil)
-	sheet.Cell(rowIndex, index).SetDataValidation(dd)
+	c.Assert(err, qt.IsNil)
+	sheet.AddDataValidation(dd)
 	index++
 
-	dd = NewXlsxCellDataValidation(true)
+	dd = NewDataValidation(rowIndex, index, rowIndex, index, true)
 	err = dd.SetRange(10, 1, DataValidationTypeWhole, DataValidationOperatorLessThanOrEqual)
-	t.Assert(err, IsNil)
-	sheet.Cell(rowIndex, index).SetDataValidation(dd)
+	c.Assert(err, qt.IsNil)
+	sheet.AddDataValidation(dd)
 	index++
 
-	dd = NewXlsxCellDataValidation(true)
+	dd = NewDataValidation(rowIndex, index, rowIndex, index, true)
 	err = dd.SetRange(10, 1, DataValidationTypeWhole, DataValidationOperatorNotEqual)
-	t.Assert(err, IsNil)
-	sheet.Cell(rowIndex, index).SetDataValidation(dd)
+	c.Assert(err, qt.IsNil)
+	sheet.AddDataValidation(dd)
 	index++
 
-	dd = NewXlsxCellDataValidation(true)
+	dd = NewDataValidation(rowIndex, index, rowIndex, index, true)
 	err = dd.SetRange(10, 50, DataValidationTypeWhole, DataValidationOperatorNotBetween)
 	if err != nil {
 		t.Fatal(err)
 	}
-	sheet.Cell(rowIndex, index).SetDataValidation(dd)
+	sheet.AddDataValidation(dd)
 	index++
 
-	dd = NewXlsxCellDataValidation(true)
+	dd = NewDataValidation(12, 2, 12, 10, true)
 	err = dd.SetDropList([]string{"1", "2", "4"})
-	t.Assert(err, IsNil)
-	dd1 := NewXlsxCellDataValidation(true)
+	c.Assert(err, qt.IsNil)
+	dd1 := NewDataValidation(12, 3, 12, 4, true)
 	err = dd1.SetDropList([]string{"11", "22", "44"})
-	t.Assert(err, IsNil)
-	dd2 := NewXlsxCellDataValidation(true)
+	c.Assert(err, qt.IsNil)
+	dd2 := NewDataValidation(12, 5, 12, 7, 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)
+	c.Assert(err, qt.IsNil)
+	sheet.AddDataValidation(dd)
+	sheet.AddDataValidation(dd1)
+	sheet.AddDataValidation(dd2)
 
-	dd = NewXlsxCellDataValidation(true)
+	dd = NewDataValidation(13, 2, 13, 10, true)
 	err = dd.SetDropList([]string{"1", "2", "4"})
-	t.Assert(err, IsNil)
-	dd1 = NewXlsxCellDataValidation(true)
+	c.Assert(err, qt.IsNil)
+	dd1 = NewDataValidation(13, 1, 13, 2, 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)
+	c.Assert(err, qt.IsNil)
+	sheet.AddDataValidation(dd)
+	sheet.AddDataValidation(dd1)
 
-	dd = NewXlsxCellDataValidation(true)
+	dd = NewDataValidation(14, 2, 14, 10, true)
 	err = dd.SetDropList([]string{"1", "2", "4"})
-	t.Assert(err, IsNil)
-	dd1 = NewXlsxCellDataValidation(true)
+	c.Assert(err, qt.IsNil)
+	dd1 = NewDataValidation(14, 1, 14, 5, 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)
+	c.Assert(err, qt.IsNil)
+	sheet.AddDataValidation(dd)
+	sheet.AddDataValidation(dd1)
 
-	dd = NewXlsxCellDataValidation(true)
+	dd = NewDataValidation(15, 2, 15, 10, true)
 	err = dd.SetDropList([]string{"1", "2", "4"})
 	if err != nil {
 		t.Fatal(err)
 	}
-	dd1 = NewXlsxCellDataValidation(true)
+	dd1 = NewDataValidation(15, 1, 15, 10, 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)
+	c.Assert(err, qt.IsNil)
+	sheet.AddDataValidation(dd)
+	sheet.AddDataValidation(dd1)
 
-	dd = NewXlsxCellDataValidation(true)
+	dd = NewDataValidation(16, 10, 16, 20, true)
 	err = dd.SetDropList([]string{"1", "2", "4"})
-	t.Assert(err, IsNil)
-	dd1 = NewXlsxCellDataValidation(true)
+	c.Assert(err, qt.IsNil)
+	dd1 = NewDataValidation(16, 2, 16, 4, true)
 	err = dd1.SetDropList([]string{"11", "22", "44"})
-	t.Assert(err, IsNil)
-	dd2 = NewXlsxCellDataValidation(true)
+	c.Assert(err, qt.IsNil)
+	dd2 = NewDataValidation(16, 12, 16, 30, 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)
+	c.Assert(err, qt.IsNil)
+	sheet.AddDataValidation(dd)
+	sheet.AddDataValidation(dd1)
+	sheet.AddDataValidation(dd2)
 
-	dd = NewXlsxCellDataValidation(true)
+	dd = NewDataValidation(3, 3, 3, Excel2006MaxRowIndex, true)
 	err = dd.SetDropList([]string{"d", "d1", "d2"})
-	t.Assert(err, IsNil)
+	c.Assert(err, qt.IsNil)
 	title = "col d range"
 	dd.SetInput(&title, &msg)
-	sheet.Col(3).SetDataValidation(dd, 3, Excel2006MaxRowIndex)
+	sheet.AddDataValidation(dd)
 
-	dd = NewXlsxCellDataValidation(true)
+	dd = NewDataValidation(3, 4, 3, Excel2006MaxRowIndex, true)
 	err = dd.SetDropList([]string{"d", "d1", "d2"})
-	t.Assert(err, IsNil)
+	c.Assert(err, qt.IsNil)
 	title = "col d range"
 	dd.SetInput(&title, &msg)
-	sheet.Col(3).SetDataValidation(dd, 4, -1)
-	maxRow := sheet.Col(3).DataValidation[len(sheet.Col(3).DataValidation)-1].maxRow
-	t.Assert(maxRow, Equals, Excel2006MaxRowIndex)
+	sheet.AddDataValidation(dd)
 
 	dest := &bytes.Buffer{}
 	err = file.Write(dest)
-	t.Assert(err, IsNil)
+	c.Assert(err, qt.IsNil)
 	// Read and write the file that was just saved.
 	file, err = OpenBinary(dest.Bytes())
-	t.Assert(err, IsNil)
+	c.Assert(err, qt.IsNil)
 	dest = &bytes.Buffer{}
 	err = file.Write(dest)
-	t.Assert(err, IsNil)
+	c.Assert(err, qt.IsNil)
 }
 
-func (d *DataValidationSuite) TestDataValidation2(t *C) {
+func TestDataValidation2(t *testing.T) {
+	c := qt.New(t)
 	// 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)
+	dd := NewDataValidation(0, 0, 0, 0, true)
+	c.Assert(dd.ShowErrorMessage, qt.Equals, false)
+	c.Assert(dd.ShowInputMessage, qt.Equals, false)
 
 	str := "you got an error"
 	dd.SetError(StyleStop, &str, &str)
-	t.Assert(dd.ShowErrorMessage, Equals, true)
-	t.Assert(dd.ShowInputMessage, Equals, false)
+	c.Assert(dd.ShowErrorMessage, qt.Equals, true)
+	c.Assert(dd.ShowInputMessage, qt.Equals, false)
 
 	str = "hello"
 	dd.SetInput(&str, &str)
-	t.Assert(dd.ShowInputMessage, Equals, true)
+	c.Assert(dd.ShowInputMessage, qt.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)
+	c.Assert(err, qt.IsNil)
 	expectedFormula := "'Sheet '' 2'!$C$2:$D$11"
-	t.Assert(dd.Formula1, Equals, expectedFormula)
-	t.Assert(dd.Type, Equals, "list")
+	c.Assert(dd.Formula1, qt.Equals, expectedFormula)
+	c.Assert(dd.Type, qt.Equals, "list")
 }

+ 8 - 8
date.go

@@ -6,11 +6,11 @@ import (
 )
 
 const (
-	MJD_0 float64 = 2400000.5
+	MJD_0      float64 = 2400000.5
 	MJD_JD2000 float64 = 51544.5
 
-	secondsInADay = float64((24*time.Hour)/time.Second)
-	nanosInADay = float64((24*time.Hour)/time.Nanosecond)
+	secondsInADay = float64((24 * time.Hour) / time.Second)
+	nanosInADay   = float64((24 * time.Hour) / time.Nanosecond)
 )
 
 var (
@@ -25,8 +25,8 @@ var (
 	excel1900Epoc = time.Date(1899, time.December, 30, 0, 0, 0, 0, time.UTC)
 	excel1904Epoc = time.Date(1904, time.January, 1, 0, 0, 0, 0, time.UTC)
 	// Days between epocs, including both off by one errors for 1900.
-	daysBetween1970And1900 = float64(unixEpoc.Sub(excel1900Epoc)/(24 * time.Hour))
-	daysBetween1970And1904 = float64(unixEpoc.Sub(excel1904Epoc)/(24 * time.Hour))
+	daysBetween1970And1900 = float64(unixEpoc.Sub(excel1900Epoc) / (24 * time.Hour))
+	daysBetween1970And1904 = float64(unixEpoc.Sub(excel1904Epoc) / (24 * time.Hour))
 )
 
 func TimeToUTCTime(t time.Time) time.Time {
@@ -124,7 +124,7 @@ func TimeFromExcelTime(excelTime float64, date1904 bool) time.Time {
 		date = excel1900Epoc
 	}
 	durationPart := time.Duration(nanosInADay * floatPart)
-	return date.AddDate(0,0, wholeDaysPart).Add(durationPart)
+	return date.AddDate(0, 0, wholeDaysPart).Add(durationPart)
 }
 
 // TimeToExcelTime will convert a time.Time into Excel's float representation, in either 1900 or 1904
@@ -132,9 +132,9 @@ func TimeFromExcelTime(excelTime float64, date1904 bool) time.Time {
 // TODO should this should handle Julian dates?
 func TimeToExcelTime(t time.Time, date1904 bool) float64 {
 	// Get the number of days since the unix epoc
-	daysSinceUnixEpoc := float64(t.Unix())/secondsInADay
+	daysSinceUnixEpoc := float64(t.Unix()) / secondsInADay
 	// Get the number of nanoseconds in days since Unix() is in seconds.
-	nanosPart := float64(t.Nanosecond())/nanosInADay
+	nanosPart := float64(t.Nanosecond()) / nanosInADay
 	// Add both together plus the number of days difference between unix and Excel epocs.
 	var offsetDays float64
 	if date1904 {

+ 1 - 0
file.go

@@ -173,6 +173,7 @@ func (f *File) AddSheet(sheetName string) (*Sheet, error) {
 		Name:     sheetName,
 		File:     f,
 		Selected: len(f.Sheets) == 0,
+		Cols:     &ColStore{},
 	}
 	f.Sheet[sheetName] = sheet
 	f.Sheets = append(f.Sheets, sheet)

+ 4 - 3
file_test.go

@@ -467,12 +467,12 @@ func (l *FileSuite) TestMarshalFile(c *C) {
 
 	// sheets
 	expectedSheet1 := `<?xml version="1.0" encoding="UTF-8"?>
-<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><sheetPr filterMode="false"><pageSetUpPr fitToPage="false"></pageSetUpPr></sheetPr><dimension ref="A1"></dimension><sheetViews><sheetView windowProtection="false" showFormulas="false" showGridLines="true" showRowColHeaders="true" showZeros="true" rightToLeft="false" tabSelected="true" showOutlineSymbols="true" defaultGridColor="true" view="normal" topLeftCell="A1" colorId="64" zoomScale="100" zoomScaleNormal="100" zoomScalePageLayoutView="100" workbookViewId="0"><selection pane="topLeft" activeCell="A1" activeCellId="0" sqref="A1"></selection></sheetView></sheetViews><sheetFormatPr defaultRowHeight="12.85"></sheetFormatPr><cols><col collapsed="false" hidden="false" max="1" min="1" style="1" width="9.5"></col></cols><sheetData><row r="1"><c r="A1" s="1" t="s"><v>0</v></c></row></sheetData><printOptions headings="false" gridLines="false" gridLinesSet="true" horizontalCentered="false" verticalCentered="false"></printOptions><pageMargins left="0.7875" right="0.7875" top="1.05277777777778" bottom="1.05277777777778" header="0.7875" footer="0.7875"></pageMargins><pageSetup paperSize="9" scale="100" firstPageNumber="1" fitToWidth="1" fitToHeight="1" pageOrder="downThenOver" orientation="portrait" usePrinterDefaults="false" blackAndWhite="false" draft="false" cellComments="none" useFirstPageNumber="true" horizontalDpi="300" verticalDpi="300" copies="1"></pageSetup><headerFooter differentFirst="false" differentOddEven="false"><oddHeader>&amp;C&amp;&#34;Times New Roman,Regular&#34;&amp;12&amp;A</oddHeader><oddFooter>&amp;C&amp;&#34;Times New Roman,Regular&#34;&amp;12Page &amp;P</oddFooter></headerFooter></worksheet>`
+<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><sheetPr filterMode="false"><pageSetUpPr fitToPage="false"></pageSetUpPr></sheetPr><dimension ref="A1"></dimension><sheetViews><sheetView windowProtection="false" showFormulas="false" showGridLines="true" showRowColHeaders="true" showZeros="true" rightToLeft="false" tabSelected="true" showOutlineSymbols="true" defaultGridColor="true" view="normal" topLeftCell="A1" colorId="64" zoomScale="100" zoomScaleNormal="100" zoomScalePageLayoutView="100" workbookViewId="0"><selection pane="topLeft" activeCell="A1" activeCellId="0" sqref="A1"></selection></sheetView></sheetViews><sheetFormatPr defaultRowHeight="12.85"></sheetFormatPr><sheetData><row r="1"><c r="A1" t="s"><v>0</v></c></row></sheetData><printOptions headings="false" gridLines="false" gridLinesSet="true" horizontalCentered="false" verticalCentered="false"></printOptions><pageMargins left="0.7875" right="0.7875" top="1.05277777777778" bottom="1.05277777777778" header="0.7875" footer="0.7875"></pageMargins><pageSetup paperSize="9" scale="100" firstPageNumber="1" fitToWidth="1" fitToHeight="1" pageOrder="downThenOver" orientation="portrait" usePrinterDefaults="false" blackAndWhite="false" draft="false" cellComments="none" useFirstPageNumber="true" horizontalDpi="300" verticalDpi="300" copies="1"></pageSetup><headerFooter differentFirst="false" differentOddEven="false"><oddHeader>&amp;C&amp;&#34;Times New Roman,Regular&#34;&amp;12&amp;A</oddHeader><oddFooter>&amp;C&amp;&#34;Times New Roman,Regular&#34;&amp;12Page &amp;P</oddFooter></headerFooter></worksheet>`
 
 	c.Assert(parts["xl/worksheets/sheet1.xml"], Equals, expectedSheet1)
 
 	expectedSheet2 := `<?xml version="1.0" encoding="UTF-8"?>
-<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><sheetPr filterMode="false"><pageSetUpPr fitToPage="false"></pageSetUpPr></sheetPr><dimension ref="A1"></dimension><sheetViews><sheetView windowProtection="false" showFormulas="false" showGridLines="true" showRowColHeaders="true" showZeros="true" rightToLeft="false" tabSelected="false" showOutlineSymbols="true" defaultGridColor="true" view="normal" topLeftCell="A1" colorId="64" zoomScale="100" zoomScaleNormal="100" zoomScalePageLayoutView="100" workbookViewId="0"><selection pane="topLeft" activeCell="A1" activeCellId="0" sqref="A1"></selection></sheetView></sheetViews><sheetFormatPr defaultRowHeight="12.85"></sheetFormatPr><cols><col collapsed="false" hidden="false" max="1" min="1" style="1" width="9.5"></col></cols><sheetData><row r="1"><c r="A1" s="1" t="s"><v>0</v></c></row></sheetData><printOptions headings="false" gridLines="false" gridLinesSet="true" horizontalCentered="false" verticalCentered="false"></printOptions><pageMargins left="0.7875" right="0.7875" top="1.05277777777778" bottom="1.05277777777778" header="0.7875" footer="0.7875"></pageMargins><pageSetup paperSize="9" scale="100" firstPageNumber="1" fitToWidth="1" fitToHeight="1" pageOrder="downThenOver" orientation="portrait" usePrinterDefaults="false" blackAndWhite="false" draft="false" cellComments="none" useFirstPageNumber="true" horizontalDpi="300" verticalDpi="300" copies="1"></pageSetup><headerFooter differentFirst="false" differentOddEven="false"><oddHeader>&amp;C&amp;&#34;Times New Roman,Regular&#34;&amp;12&amp;A</oddHeader><oddFooter>&amp;C&amp;&#34;Times New Roman,Regular&#34;&amp;12Page &amp;P</oddFooter></headerFooter></worksheet>`
+<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><sheetPr filterMode="false"><pageSetUpPr fitToPage="false"></pageSetUpPr></sheetPr><dimension ref="A1"></dimension><sheetViews><sheetView windowProtection="false" showFormulas="false" showGridLines="true" showRowColHeaders="true" showZeros="true" rightToLeft="false" tabSelected="false" showOutlineSymbols="true" defaultGridColor="true" view="normal" topLeftCell="A1" colorId="64" zoomScale="100" zoomScaleNormal="100" zoomScalePageLayoutView="100" workbookViewId="0"><selection pane="topLeft" activeCell="A1" activeCellId="0" sqref="A1"></selection></sheetView></sheetViews><sheetFormatPr defaultRowHeight="12.85"></sheetFormatPr><sheetData><row r="1"><c r="A1" t="s"><v>0</v></c></row></sheetData><printOptions headings="false" gridLines="false" gridLinesSet="true" horizontalCentered="false" verticalCentered="false"></printOptions><pageMargins left="0.7875" right="0.7875" top="1.05277777777778" bottom="1.05277777777778" header="0.7875" footer="0.7875"></pageMargins><pageSetup paperSize="9" scale="100" firstPageNumber="1" fitToWidth="1" fitToHeight="1" pageOrder="downThenOver" orientation="portrait" usePrinterDefaults="false" blackAndWhite="false" draft="false" cellComments="none" useFirstPageNumber="true" horizontalDpi="300" verticalDpi="300" copies="1"></pageSetup><headerFooter differentFirst="false" differentOddEven="false"><oddHeader>&amp;C&amp;&#34;Times New Roman,Regular&#34;&amp;12&amp;A</oddHeader><oddFooter>&amp;C&amp;&#34;Times New Roman,Regular&#34;&amp;12Page &amp;P</oddFooter></headerFooter></worksheet>`
 
 	c.Assert(parts["xl/worksheets/sheet2.xml"], Equals, expectedSheet2)
 
@@ -847,7 +847,8 @@ func (l *FileSuite) TestMarshalFile(c *C) {
 	// For now we only allow simple string data in the
 	// spreadsheet.  Style support will follow.
 	expectedStyles := `<?xml version="1.0" encoding="UTF-8"?>
-<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><fonts count="1"><font><sz val="12"/><name val="Verdana"/><family val="0"/><charset val="0"/></font></fonts><fills count="2"><fill><patternFill patternType="none"/></fill><fill><patternFill patternType="lightGray"/></fill></fills><borders count="1"><border><left style="none"></left><right style="none"></right><top style="none"></top><bottom style="none"></bottom></border></borders><cellXfs count="2"><xf applyAlignment="0" applyBorder="0" applyFont="0" applyFill="0" applyNumberFormat="0" applyProtection="0" borderId="0" fillId="0" fontId="0" numFmtId="0"><alignment horizontal="general" indent="0" shrinkToFit="0" textRotation="0" vertical="bottom" wrapText="0"/></xf><xf applyAlignment="0" applyBorder="0" applyFont="0" applyFill="0" applyNumberFormat="0" applyProtection="0" borderId="0" fillId="0" fontId="0" numFmtId="0"><alignment horizontal="general" indent="0" shrinkToFit="0" textRotation="0" vertical="bottom" wrapText="0"/></xf></cellXfs></styleSheet>`
+<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><borders count="1"><border><left style="none"></left><right style="none"></right><top style="none"></top><bottom style="none"></bottom></border></borders><cellStyleXfs count="1"><xf applyAlignment="0" applyBorder="0" applyFont="0" applyFill="0" applyNumberFormat="0" applyProtection="0" borderId="0" fillId="0" fontId="0" numFmtId="0"><alignment horizontal="general" indent="0" shrinkToFit="0" textRotation="0" vertical="bottom" wrapText="0"/></xf></cellStyleXfs><cellXfs count="1"><xf applyAlignment="0" applyBorder="0" applyFont="0" applyFill="0" applyNumberFormat="0" applyProtection="0" borderId="0" fillId="0" fontId="0" numFmtId="0"><alignment horizontal="general" indent="0" shrinkToFit="0" textRotation="0" vertical="bottom" wrapText="0"/></xf></cellXfs></styleSheet>`
+
 	c.Assert(parts["xl/styles.xml"], Equals, expectedStyles)
 }
 

+ 3 - 2
format_code.go

@@ -219,8 +219,9 @@ func generalNumericScientific(value string, allowScientific bool) (string, error
 	return strconv.FormatFloat(f, 'f', -1, 64), nil
 }
 
-// Format strings are a little strange to compare because empty string needs to be taken as general, and general needs
-// to be compared case insensitively.
+// Format strings are a little strange to compare because empty string
+// needs to be taken as general, and general needs to be compared case
+// insensitively.
 func compareFormatString(fmt1, fmt2 string) bool {
 	if fmt1 == fmt2 {
 		return true

+ 1 - 1
go.mod

@@ -3,6 +3,6 @@ module github.com/tealeg/xlsx
 go 1.12
 
 require (
-	github.com/kr/pretty v0.1.0 // indirect
+	github.com/frankban/quicktest v1.5.0
 	gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15
 )

+ 4 - 0
go.sum

@@ -1,3 +1,7 @@
+github.com/frankban/quicktest v1.5.0 h1:Tb4jWdSpdjKzTUicPnY61PZxKbDoGa7ABbrReT3gQVY=
+github.com/frankban/quicktest v1.5.0/go.mod h1:jaStnuzAqU1AJdCO0l53JDCJrVDKcS03DbaAcR7Ks/o=
+github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=

+ 22 - 64
lib.go

@@ -523,9 +523,9 @@ func fillCellDataFromInlineString(rawcell xlsxC, cell *Cell) {
 // rows from a XSLXWorksheet, populates them with Cells and resolves
 // the value references from the reference table and stores them in
 // the rows and columns.
-func readRowsFromSheet(Worksheet *xlsxWorksheet, file *File, sheet *Sheet, rowLimit int) ([]*Row, []*Col, int, int) {
+func readRowsFromSheet(Worksheet *xlsxWorksheet, file *File, sheet *Sheet, rowLimit int) ([]*Row, *ColStore, int, int) {
 	var rows []*Row
-	var cols []*Col
+	var cols *ColStore
 	var row *Row
 	var minCol, maxCol, maxRow, colCount, rowCount int
 	var reftable *RefTable
@@ -549,34 +549,28 @@ func readRowsFromSheet(Worksheet *xlsxWorksheet, file *File, sheet *Sheet, rowLi
 	rowCount = maxRow + 1
 	colCount = maxCol + 1
 	rows = make([]*Row, rowCount)
-	cols = make([]*Col, colCount)
-	for i := range cols {
-		cols[i] = &Col{
-			Hidden: false,
-		}
-	}
+	cols = &ColStore{}
 
 	if Worksheet.Cols != nil {
 		// Columns can apply to a range, for convenience we expand the
 		// ranges out into individual column definitions.
 		for _, rawcol := range Worksheet.Cols.Col {
-			// Note, below, that sometimes column definitions can
-			// exist outside the defined dimensions of the
-			// spreadsheet - we deliberately exclude these
-			// columns.
-			for i := rawcol.Min; i <= rawcol.Max && i <= colCount; i++ {
-				col := &Col{
-					Min:          rawcol.Min,
-					Max:          rawcol.Max,
-					Hidden:       rawcol.Hidden,
-					Width:        rawcol.Width,
-					OutlineLevel: rawcol.OutlineLevel}
-				cols[i-1] = col
-				if file.styles != nil {
-					col.style = file.styles.getStyle(rawcol.Style)
-					col.numFmt, col.parsedNumFmt = file.styles.getNumberFormat(rawcol.Style)
-				}
+			col := &Col{
+				Min:          rawcol.Min,
+				Max:          rawcol.Max,
+				Hidden:       rawcol.Hidden,
+				Width:        rawcol.Width,
+				OutlineLevel: rawcol.OutlineLevel,
+				BestFit:      rawcol.BestFit,
+				CustomWidth:  rawcol.CustomWidth,
+				Phonetic:     rawcol.Phonetic,
+				Collapsed:    rawcol.Collapsed,
+			}
+			if file.styles != nil {
+				col.style = file.styles.getStyle(rawcol.Style)
+				col.numFmt, col.parsedNumFmt = file.styles.getNumberFormat(rawcol.Style)
 			}
+			cols.Add(col)
 		}
 	}
 
@@ -640,7 +634,9 @@ func readRowsFromSheet(Worksheet *xlsxWorksheet, file *File, sheet *Sheet, rowLi
 				}
 				cell.date1904 = file.Date1904
 				// Cell is considered hidden if the row or the column of this cell is hidden
-				cell.Hidden = rawrow.Hidden || (len(cols) > cellX && cols[cellX].Hidden)
+				//
+				col := cols.FindColByIndex(cellX + 1)
+				cell.Hidden = rawrow.Hidden || (col != nil && col.Hidden)
 				insertColIndex++
 			}
 		}
@@ -723,45 +719,7 @@ func readSheetFromFile(sc chan *indexedSheet, index int, rsheet xlsxSheet, fi *F
 	sheet.SheetFormat.OutlineLevelRow = worksheet.SheetFormatPr.OutlineLevelRow
 	if nil != worksheet.DataValidations {
 		for _, dd := range worksheet.DataValidations.DataValidation {
-			sqrefArr := strings.Split(dd.Sqref, " ")
-			for _, sqref := range sqrefArr {
-				parts := strings.Split(sqref, cellRangeChar)
-
-				minCol, minRow, err := GetCoordsFromCellIDString(parts[0])
-				if nil != err {
-					return fmt.Errorf("data validation %s", err.Error())
-				}
-
-				if 2 == len(parts) {
-					maxCol, maxRow, err := GetCoordsFromCellIDString(parts[1])
-					if nil != err {
-						return fmt.Errorf("data validation %s", err.Error())
-					}
-
-					if minCol == maxCol && minRow == maxRow {
-						newDD := new(xlsxCellDataValidation)
-						*newDD = *dd
-						newDD.Sqref = ""
-						sheet.Cell(minRow, minCol).SetDataValidation(newDD)
-					} else {
-						// one col mutli dd , error todo
-						for i := minCol; i <= maxCol; i++ {
-							newDD := new(xlsxCellDataValidation)
-							*newDD = *dd
-							newDD.Sqref = ""
-							sheet.Col(i).SetDataValidation(dd, minRow, maxRow)
-						}
-
-					}
-				} else {
-					newDD := new(xlsxCellDataValidation)
-					*newDD = *dd
-					newDD.Sqref = ""
-					sheet.Cell(minRow, minCol).SetDataValidation(dd)
-
-				}
-			}
-
+			sheet.AddDataValidation(dd)
 		}
 
 	}

+ 24 - 26
lib_test.go

@@ -7,6 +7,7 @@ import (
 	"strings"
 	"testing"
 
+	qt "github.com/frankban/quicktest"
 	. "gopkg.in/check.v1"
 )
 
@@ -360,10 +361,8 @@ func (l *LibSuite) TestReadRowsFromSheet(c *C) {
 	c.Assert(cell1.Value, Equals, "Foo")
 	cell2 := row.Cells[1]
 	c.Assert(cell2.Value, Equals, "Bar")
-	col := cols[0]
-	c.Assert(col.Min, Equals, 0)
-	c.Assert(col.Max, Equals, 0)
-	c.Assert(col.Hidden, Equals, false)
+	col := cols.FindColByIndex(0)
+	c.Assert(col, IsNil)
 	c.Assert(len(worksheet.SheetViews.SheetView), Equals, 1)
 	sheetView := worksheet.SheetViews.SheetView[0]
 	c.Assert(sheetView.Pane, NotNil)
@@ -662,11 +661,11 @@ func (l *LibSuite) TestReadRowsFromSheetWithLeadingEmptyCols(c *C) {
 		c.Assert(val, Equals, "DEF")
 	}
 
-	c.Assert(len(cols), Equals, 4)
-	c.Assert(cols[0].Width, Equals, 0.0)
-	c.Assert(cols[1].Width, Equals, 0.0)
-	c.Assert(cols[2].Width, Equals, 17.0)
-	c.Assert(cols[3].Width, Equals, 18.0)
+	c.Assert(cols.Len, Equals, 2)
+	c.Assert(cols.FindColByIndex(1), IsNil)
+	c.Assert(cols.FindColByIndex(2), IsNil)
+	c.Assert(cols.FindColByIndex(3).Width, Equals, 17.0)
+	c.Assert(cols.FindColByIndex(4).Width, Equals, 18.0)
 }
 
 func (l *LibSuite) TestReadRowsFromSheetWithEmptyCells(c *C) {
@@ -771,10 +770,8 @@ func (l *LibSuite) TestReadRowsFromSheetWithEmptyCells(c *C) {
 	cell3 := row.Cells[2]
 	c.Assert(cell3.Value, Equals, "Yes")
 
-	col := cols[0]
-	c.Assert(col.Min, Equals, 0)
-	c.Assert(col.Max, Equals, 0)
-	c.Assert(col.Hidden, Equals, false)
+	col := cols.FindColByIndex(0)
+	c.Assert(col, IsNil)
 }
 
 func (l *LibSuite) TestReadRowsFromSheetWithTrailingEmptyCells(c *C) {
@@ -1023,7 +1020,8 @@ func (l *LibSuite) TestReadRowsFromSheetWithMultipleTypes(c *C) {
 	c.Assert(cell6.Value, Equals, "#DIV/0!")
 }
 
-func (l *LibSuite) TestReadRowsFromSheetWithHiddenColumn(c *C) {
+func TestReadRowsFromSheetWithHiddenColumn(t *testing.T) {
+	c := qt.New(t)
 	var sharedstringsXML = bytes.NewBufferString(`
 		<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
 		<sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
@@ -1049,37 +1047,37 @@ func (l *LibSuite) TestReadRowsFromSheetWithHiddenColumn(c *C) {
 		    </sheetData><drawing r:id="rId1"/></worksheet>`)
 	worksheet := new(xlsxWorksheet)
 	err := xml.NewDecoder(sheetxml).Decode(worksheet)
-	c.Assert(err, IsNil)
+	c.Assert(err, qt.IsNil)
 	sst := new(xlsxSST)
 	err = xml.NewDecoder(sharedstringsXML).Decode(sst)
-	c.Assert(err, IsNil)
+	c.Assert(err, qt.IsNil)
 	file := new(File)
 	file.referenceTable = MakeSharedStringRefTable(sst)
 	sheet := new(Sheet)
 	rows, _, maxCols, maxRows := readRowsFromSheet(worksheet, file, sheet, NoRowLimit)
-	c.Assert(maxRows, Equals, 1)
-	c.Assert(maxCols, Equals, 2)
+	c.Assert(maxRows, qt.Equals, 1)
+	c.Assert(maxCols, qt.Equals, 2)
 	row := rows[0]
-	c.Assert(row.Sheet, Equals, sheet)
-	c.Assert(len(row.Cells), Equals, 2)
+	c.Assert(row.Sheet, qt.Equals, sheet)
+	c.Assert(len(row.Cells), qt.Equals, 2)
 
 	cell1 := row.Cells[0]
-	c.Assert(cell1.Type(), Equals, CellTypeString)
+	c.Assert(cell1.Type(), qt.Equals, CellTypeString)
 	if val, err := cell1.FormattedValue(); err != nil {
 		c.Error(err)
 	} else {
-		c.Assert(val, Equals, "This is a test.")
+		c.Assert(val, qt.Equals, "This is a test.")
 	}
-	c.Assert(cell1.Hidden, Equals, false)
+	c.Assert(cell1.Hidden, qt.Equals, false)
 
 	cell2 := row.Cells[1]
-	c.Assert(cell2.Type(), Equals, CellTypeString)
+	c.Assert(cell2.Type(), qt.Equals, CellTypeString)
 	if val, err := cell2.FormattedValue(); err != nil {
 		c.Error(err)
 	} else {
-		c.Assert(val, Equals, "This should be invisible.")
+		c.Assert(val, qt.Equals, "This should be invisible.")
 	}
-	c.Assert(cell2.Hidden, Equals, true)
+	c.Assert(cell2.Hidden, qt.Equals, true)
 }
 
 // When converting the xlsxRow to a Row we create a as many cells as we find.

+ 1 - 2
row.go

@@ -22,6 +22,5 @@ func (r *Row) SetHeightCM(ht float64) {
 func (r *Row) AddCell() *Cell {
 	cell := NewCell(r)
 	r.Cells = append(r.Cells, cell)
-	r.Sheet.maybeAddCol(len(r.Cells))
 	return cell
-}
+}

+ 194 - 120
sheet.go

@@ -9,17 +9,18 @@ import (
 // Sheet is a high level structure intended to provide user access to
 // the contents of a particular sheet within an XLSX file.
 type Sheet struct {
-	Name        string
-	File        *File
-	Rows        []*Row
-	Cols        []*Col
-	MaxRow      int
-	MaxCol      int
-	Hidden      bool
-	Selected    bool
-	SheetViews  []SheetView
-	SheetFormat SheetFormat
-	AutoFilter  *AutoFilter
+	Name            string
+	File            *File
+	Rows            []*Row
+	Cols            *ColStore
+	MaxRow          int
+	MaxCol          int
+	Hidden          bool
+	Selected        bool
+	SheetViews      []SheetView
+	SheetFormat     SheetFormat
+	AutoFilter      *AutoFilter
+	DataValidations []*xlsxDataValidation
 }
 
 type SheetView struct {
@@ -74,6 +75,11 @@ func (s *Sheet) AddRowAtIndex(index int) (*Row, error) {
 	return row, nil
 }
 
+// Add a DataValidation to a range of cells
+func (s *Sheet) AddDataValidation(dv *xlsxDataValidation) {
+	s.DataValidations = append(s.DataValidations, dv)
+}
+
 // Removes a row at a specific index
 func (s *Sheet) RemoveRowAtIndex(index int) error {
 	if index < 0 || index >= len(s.Rows) {
@@ -102,31 +108,12 @@ func (s *Sheet) Row(idx int) *Row {
 	return s.Rows[idx]
 }
 
-// Make sure we always have as many Cols as we do cells.
-func (s *Sheet) maybeAddCol(cellCount int) {
-	if cellCount > s.MaxCol {
-		loopCnt := cellCount - s.MaxCol
-		currIndex := s.MaxCol + 1
-		for i := 0; i < loopCnt; i++ {
-
-			col := &Col{
-				style:     NewStyle(),
-				Min:       currIndex,
-				Max:       currIndex,
-				Hidden:    false,
-				Collapsed: false}
-			s.Cols = append(s.Cols, col)
-			currIndex++
-		}
-
-		s.MaxCol = cellCount
-	}
-}
-
-// Make sure we always have as many Cols as we do cells.
+// Return the Col that applies to this Column index, or return nil if no such Col exists
 func (s *Sheet) Col(idx int) *Col {
-	s.maybeAddCol(idx + 1)
-	return s.Cols[idx]
+	if s.Cols == nil {
+		panic("trying to use uninitialised ColStore")
+	}
+	return s.Cols.FindColByIndex(idx + 1)
 }
 
 // Get a Cell by passing it's cartesian coordinates (zero based) as
@@ -153,17 +140,81 @@ func (sh *Sheet) Cell(row, col int) *Cell {
 	return r.Cells[col]
 }
 
-//Set the width of a single column or multiple columns.
-func (s *Sheet) SetColWidth(startcol, endcol int, width float64) error {
-	if startcol > endcol {
-		return fmt.Errorf("Could not set width for range %d-%d: startcol must be less than endcol.", startcol, endcol)
+//Set the parameters of a column.  Parameters are passed as a pointer
+//to a Col structure which you much construct yourself.
+func (s *Sheet) SetColParameters(col *Col) {
+	if s.Cols == nil {
+		panic("trying to use uninitialised ColStore")
+	}
+	s.Cols.Add(col)
+}
+
+func (s *Sheet) setCol(min, max int, setter func(col *Col)) {
+	if s.Cols == nil {
+		panic("trying to use uninitialised ColStore")
 	}
-	end := endcol + 1
-	s.maybeAddCol(end)
-	for ; startcol < end; startcol++ {
-		s.Cols[startcol].Width = width
+
+	cols := s.Cols.getOrMakeColsForRange(s.Cols.Root, min, max)
+
+	for _, col := range cols {
+		switch {
+		case col.Min < min && col.Max > max:
+			// The column completely envelops the range,
+			// so we'll split it into three parts and only
+			// set the width on the part within the range.
+			// The ColStore will do most of this work for
+			// us, we just need to create the new Col
+			// based on the old one.
+			newCol := col.copyToRange(min, max)
+			setter(newCol)
+			s.Cols.Add(newCol)
+		case col.Min < min:
+			// If this column crosses the minimum boundary
+			// of the range we must split it and only
+			// apply the change within the range.  Again,
+			// we can lean on the ColStore to deal with
+			// the rest we just need to make the new
+			// Col.
+			newCol := col.copyToRange(min, col.Max)
+			setter(newCol)
+			s.Cols.Add(newCol)
+		case col.Max > max:
+			// Likewise if a col definition crosses the
+			// maximum boundary of the range, it must also
+			// be split
+			newCol := col.copyToRange(col.Min, max)
+			setter(newCol)
+			s.Cols.Add(newCol)
+		default:
+			newCol := col.copyToRange(min, max)
+			setter(newCol)
+			s.Cols.Add(newCol)
+
+		}
 	}
-	return nil
+	return
+}
+
+// Set the width of a range of columns.
+func (s *Sheet) SetColWidth(min, max int, width float64) {
+	s.setCol(min, max, func(col *Col) {
+		col.SetWidth(width)
+	})
+}
+
+// Set the outline level for a range of columns.
+func (s *Sheet) SetOutlineLevel(minCol, maxCol int, outlineLevel uint8) {
+	s.setCol(minCol, maxCol, func(col *Col) {
+		col.SetOutlineLevel(outlineLevel)
+	})
+}
+
+// Set the type for a range of columns.
+func (s *Sheet) SetType(minCol, maxCol int, cellType CellType) {
+	s.setCol(minCol, maxCol, func(col *Col) {
+		col.SetType(cellType)
+	})
+
 }
 
 // When merging cells, the cell may be the 'original' or the 'covered'.
@@ -199,19 +250,7 @@ func (s *Sheet) handleMerged() {
 	}
 }
 
-// Dump sheet to its XML representation, intended for internal use only
-func (s *Sheet) makeXLSXSheet(refTable *RefTable, styles *xlsxStyleSheet) *xlsxWorksheet {
-	worksheet := newXlsxWorksheet()
-	xSheet := xlsxSheetData{}
-	maxRow := 0
-	maxCell := 0
-	var maxLevelCol, maxLevelRow uint8
-
-	// Scan through the sheet and see if there are any merged cells. If there
-	// are, we may need to extend the size of the sheet. There needs to be
-	// phantom cells underlying the area covered by the merged cell
-	s.handleMerged()
-
+func (s *Sheet) makeSheetView(worksheet *xlsxWorksheet) {
 	for index, sheetView := range s.SheetViews {
 		if sheetView.Pane != nil {
 			worksheet.SheetViews.SheetView[index].Pane = &xlsxPane{
@@ -224,75 +263,72 @@ func (s *Sheet) makeXLSXSheet(refTable *RefTable, styles *xlsxStyleSheet) *xlsxW
 
 		}
 	}
-
 	if s.Selected {
 		worksheet.SheetViews.SheetView[0].TabSelected = true
 	}
 
+}
+
+func (s *Sheet) makeSheetFormatPr(worksheet *xlsxWorksheet) {
 	if s.SheetFormat.DefaultRowHeight != 0 {
 		worksheet.SheetFormatPr.DefaultRowHeight = s.SheetFormat.DefaultRowHeight
 	}
 	worksheet.SheetFormatPr.DefaultColWidth = s.SheetFormat.DefaultColWidth
+}
 
-	colsXfIdList := make([]int, len(s.Cols))
-	for c, col := range s.Cols {
-		XfId := 0
-		if col.Min == 0 {
-			col.Min = 1
-		}
-		if col.Max == 0 {
-			col.Max = 1
-		}
-		style := col.GetStyle()
-		//col's style always not nil
-		if style != nil {
-			xNumFmt := styles.newNumFmt(col.numFmt)
-			XfId = handleStyleForXLSX(style, xNumFmt.NumFmtId, styles)
-		}
-		colsXfIdList[c] = XfId
-
-		var customWidth bool
-		if col.Width == 0 {
-			col.Width = ColWidth
-			customWidth = false
-		} else {
-			customWidth = true
-		}
-		// When the cols content is empty, the cols flag is not output in the xml file.
-		if worksheet.Cols == nil {
-			worksheet.Cols = &xlsxCols{Col: []xlsxCol{}}
-		}
-		worksheet.Cols.Col = append(worksheet.Cols.Col,
-			xlsxCol{Min: col.Min,
-				Max:          col.Max,
-				Hidden:       col.Hidden,
-				Width:        col.Width,
-				CustomWidth:  customWidth,
-				Collapsed:    col.Collapsed,
-				OutlineLevel: col.OutlineLevel,
-				Style:        XfId,
-			})
-
-		if col.OutlineLevel > maxLevelCol {
-			maxLevelCol = col.OutlineLevel
-		}
-		if nil != col.DataValidation {
-			if nil == worksheet.DataValidations {
-				worksheet.DataValidations = &xlsxCellDataValidations{}
+//
+func (s *Sheet) makeCols(worksheet *xlsxWorksheet, styles *xlsxStyleSheet) (maxLevelCol uint8) {
+	maxLevelCol = 0
+	if s.Cols == nil {
+		panic("trying to use uninitialised ColStore")
+	}
+	s.Cols.ForEach(
+		func(c int, col *Col) {
+			XfId := 0
+			style := col.GetStyle()
+
+			hasNumFmt := len(col.numFmt) > 0
+			if style == nil && hasNumFmt {
+				style = NewStyle()
 			}
-			colName := ColIndexToLetters(c)
-			for _, dd := range col.DataValidation {
-				if dd.minRow == dd.maxRow {
-					dd.Sqref = colName + RowIndexToString(dd.minRow)
-				} else {
-					dd.Sqref = colName + RowIndexToString(dd.minRow) + cellRangeChar + colName + RowIndexToString(dd.maxRow)
-				}
-				worksheet.DataValidations.DataValidation = append(worksheet.DataValidations.DataValidation, dd)
 
+			if hasNumFmt {
+				xNumFmt := styles.newNumFmt(col.numFmt)
+				XfId = handleStyleForXLSX(style, xNumFmt.NumFmtId, styles)
 			}
-			worksheet.DataValidations.Count = len(worksheet.DataValidations.DataValidation)
-		}
-	}
+			col.outXfID = XfId
+
+			// When the cols content is empty, the cols flag is not output in the xml file.
+			if worksheet.Cols == nil {
+				worksheet.Cols = &xlsxCols{Col: []xlsxCol{}}
+			}
+			worksheet.Cols.Col = append(worksheet.Cols.Col,
+				xlsxCol{
+					Min:          col.Min,
+					Max:          col.Max,
+					Hidden:       col.Hidden,
+					Width:        col.Width,
+					CustomWidth:  col.CustomWidth,
+					Collapsed:    col.Collapsed,
+					OutlineLevel: col.OutlineLevel,
+					Style:        XfId,
+					BestFit:      col.BestFit,
+					Phonetic:     col.Phonetic,
+				})
+
+			if col.OutlineLevel > maxLevelCol {
+				maxLevelCol = col.OutlineLevel
+			}
+		})
+
+	return maxLevelCol
+}
+
+func (s *Sheet) makeRows(worksheet *xlsxWorksheet, styles *xlsxStyleSheet, refTable *RefTable, maxLevelCol uint8) {
+	maxRow := 0
+	maxCell := 0
+	var maxLevelRow uint8
+	xSheet := xlsxSheetData{}
 
 	for r, row := range s.Rows {
 		if r > maxRow {
@@ -309,15 +345,25 @@ func (s *Sheet) makeXLSXSheet(refTable *RefTable, styles *xlsxStyleSheet) *xlsxW
 			maxLevelRow = row.OutlineLevel
 		}
 		for c, cell := range row.Cells {
-			XfId := colsXfIdList[c]
+			var XfId int
+
+			col := s.Col(c)
+			if col != nil {
+				XfId = col.outXfID
+			}
 
 			// generate NumFmtId and add new NumFmt
 			xNumFmt := styles.newNumFmt(cell.NumFmt)
 
 			style := cell.style
-			if style != nil {
+			switch {
+			case style != nil:
 				XfId = handleStyleForXLSX(style, xNumFmt.NumFmtId, styles)
-			} else if len(cell.NumFmt) > 0 && !compareFormatString(s.Cols[c].numFmt, cell.NumFmt) {
+			case len(cell.NumFmt) == 0:
+				// Do nothing
+			case col == nil:
+				XfId = handleNumFmtIdForXLSX(xNumFmt.NumFmtId, styles)
+			case !compareFormatString(col.numFmt, cell.NumFmt):
 				XfId = handleNumFmtIdForXLSX(xNumFmt.NumFmtId, styles)
 			}
 
@@ -363,7 +409,7 @@ func (s *Sheet) makeXLSXSheet(refTable *RefTable, styles *xlsxStyleSheet) *xlsxW
 			xRow.C = append(xRow.C, xC)
 			if nil != cell.DataValidation {
 				if nil == worksheet.DataValidations {
-					worksheet.DataValidations = &xlsxCellDataValidations{}
+					worksheet.DataValidations = &xlsxDataValidations{}
 				}
 				cell.DataValidation.Sqref = xC.R
 				worksheet.DataValidations.DataValidation = append(worksheet.DataValidations.DataValidation, cell.DataValidation)
@@ -386,14 +432,12 @@ func (s *Sheet) makeXLSXSheet(refTable *RefTable, styles *xlsxStyleSheet) *xlsxW
 		}
 		xSheet.Row = append(xSheet.Row, xRow)
 	}
-
 	// Update sheet format with the freshly determined max levels
 	s.SheetFormat.OutlineLevelCol = maxLevelCol
 	s.SheetFormat.OutlineLevelRow = maxLevelRow
 	// .. and then also apply this to the xml worksheet
 	worksheet.SheetFormatPr.OutlineLevelCol = s.SheetFormat.OutlineLevelCol
 	worksheet.SheetFormatPr.OutlineLevelRow = s.SheetFormat.OutlineLevelRow
-
 	if worksheet.MergeCells != nil {
 		worksheet.MergeCells.Count = len(worksheet.MergeCells.Cells)
 	}
@@ -409,6 +453,36 @@ func (s *Sheet) makeXLSXSheet(refTable *RefTable, styles *xlsxStyleSheet) *xlsxW
 		dimension.Ref = "A1"
 	}
 	worksheet.Dimension = dimension
+
+}
+
+func (s *Sheet) makeDataValidations(worksheet *xlsxWorksheet) {
+	if len(s.DataValidations) > 0 {
+		if worksheet.DataValidations == nil {
+			worksheet.DataValidations = &xlsxDataValidations{}
+		}
+		for _, dv := range s.DataValidations {
+			worksheet.DataValidations.DataValidation = append(worksheet.DataValidations.DataValidation, dv)
+		}
+		worksheet.DataValidations.Count = len(worksheet.DataValidations.DataValidation)
+	}
+}
+
+// Dump sheet to its XML representation, intended for internal use only
+func (s *Sheet) makeXLSXSheet(refTable *RefTable, styles *xlsxStyleSheet) *xlsxWorksheet {
+	worksheet := newXlsxWorksheet()
+
+	// Scan through the sheet and see if there are any merged cells. If there
+	// are, we may need to extend the size of the sheet. There needs to be
+	// phantom cells underlying the area covered by the merged cell
+	s.handleMerged()
+
+	s.makeSheetView(worksheet)
+	s.makeSheetFormatPr(worksheet)
+	maxLevelCol := s.makeCols(worksheet, styles)
+	s.makeDataValidations(worksheet)
+	s.makeRows(worksheet, styles, refTable, maxLevelCol)
+
 	return worksheet
 }
 

File diff suppressed because it is too large
+ 67 - 50
sheet_test.go


+ 56 - 27
stream_file.go

@@ -9,14 +9,17 @@ import (
 )
 
 type StreamFile struct {
-	xlsxFile       *File
-	sheetXmlPrefix []string
-	sheetXmlSuffix []string
-	zipWriter      *zip.Writer
-	currentSheet   *streamSheet
-	styleIds       [][]int
-	styleIdMap     map[StreamStyle]int
-	err            error
+	xlsxFile               *File
+	sheetXmlPrefix         []string
+	sheetXmlSuffix         []string
+	zipWriter              *zip.Writer
+	currentSheet           *streamSheet
+	styleIds               [][]int
+	styleIdMap             map[StreamStyle]int
+	streamingCellMetadatas map[int]*StreamingCellMetadata
+	sheetStreamStyles      map[int]cellStreamStyle
+	sheetDefaultCellType   map[int]defaultCellType
+	err                    error
 }
 
 type streamSheet struct {
@@ -56,8 +59,8 @@ func (sf *StreamFile) Write(cells []string) error {
 // WriteWithColumnDefaultMetadata will write a row of cells to the current sheet. Every call to WriteWithColumnDefaultMetadata
 // on the same sheet must contain the same number of cells as the header provided when the sheet was created or
 // an error will be returned. This function will always trigger a flush on success. Each cell will be encoded with the
-// default CellMetadata of the column that it belongs to. However, if the cell data string cannot be
-// parsed into the cell type in CellMetadata, we fall back on encoding the cell as a string and giving it a default
+// default StreamingCellMetadata of the column that it belongs to. However, if the cell data string cannot be
+// parsed into the cell type in StreamingCellMetadata, we fall back on encoding the cell as a string and giving it a default
 // string style
 func (sf *StreamFile) WriteWithColumnDefaultMetadata(cells []string) error {
 	if sf.err != nil {
@@ -122,9 +125,14 @@ func (sf *StreamFile) write(cells []string) error {
 	if sf.currentSheet == nil {
 		return NoCurrentSheetError
 	}
-	if len(cells) != sf.currentSheet.columnCount {
-		return WrongNumberOfRowsError
+	cellCount := len(cells)
+	if cellCount != sf.currentSheet.columnCount {
+		if sf.currentSheet.columnCount != 0 {
+			return WrongNumberOfRowsError
+		}
+		sf.currentSheet.columnCount = cellCount
 	}
+
 	sf.currentSheet.rowCount++
 	if err := sf.currentSheet.write(`<row r="` + strconv.Itoa(sf.currentSheet.rowCount) + `">`); err != nil {
 		return err
@@ -170,38 +178,56 @@ func (sf *StreamFile) writeWithColumnDefaultMetadata(cells []string) error {
 	if sf.currentSheet == nil {
 		return NoCurrentSheetError
 	}
-	if len(cells) != sf.currentSheet.columnCount {
-		return WrongNumberOfRowsError
-	}
 
-	currentSheet := sf.xlsxFile.Sheets[sf.currentSheet.index-1]
+	sheetIndex := sf.currentSheet.index - 1
+	currentSheet := sf.xlsxFile.Sheets[sheetIndex]
 
 	var streamCells []StreamCell
-	for colIndex, col := range currentSheet.Cols {
 
+	if currentSheet.Cols == nil {
+		panic("trying to use uninitialised ColStore")
+	}
+
+	if len(cells) != sf.currentSheet.columnCount {
+		if sf.currentSheet.columnCount != 0 {
+			return WrongNumberOfRowsError
+
+		}
+		sf.currentSheet.columnCount = len(cells)
+	}
+
+	cSS := sf.sheetStreamStyles[sheetIndex]
+	cDCT := sf.sheetDefaultCellType[sheetIndex]
+	for ci, c := range cells {
 		// TODO: Legacy code paths like `StreamFileBuilder.AddSheet` could
 		// leave style empty and if cell data cannot be parsed into cell type then
 		// we need a sensible default StreamStyle to fall back to
 		style := StreamStyleDefaultString
-
 		// Because `cellData` could be anything we need to attempt to
 		// parse into the default cell type and if parsing fails fall back
 		// to some sensible default
-		defaultType := col.defaultCellType
-		// TODO: Again `CellType` could be nil if sheet was created through
-		// legacy code path so, like style, hardcoding for now
-		cellType := defaultType.fallbackTo(cells[colIndex], CellTypeString)
-		if defaultType != nil && *defaultType == cellType {
-			style = col.GetStreamStyle()
+		cellType := CellTypeInline
+		if dct, ok := cDCT[ci]; ok {
+			defaultType := dct
+			cellType = defaultType.fallbackTo(cells[ci], CellTypeString)
+			if ss, ok := cSS[ci]; ok {
+				// TODO: Again `CellType` could be nil if sheet was created through
+				// legacy code path so, like style, hardcoding for now
+				if defaultType != nil && *defaultType == cellType {
+					style = ss
+				}
+			}
+
 		}
 
 		streamCells = append(
 			streamCells,
 			NewStreamCell(
-				cells[colIndex],
+				c,
 				style,
 				cellType,
 			))
+
 	}
 	return sf.writeS(streamCells)
 }
@@ -211,7 +237,10 @@ func (sf *StreamFile) writeS(cells []StreamCell) error {
 		return NoCurrentSheetError
 	}
 	if len(cells) != sf.currentSheet.columnCount {
-		return WrongNumberOfRowsError
+		if sf.currentSheet.columnCount != 0 {
+			return WrongNumberOfRowsError
+		}
+		sf.currentSheet.columnCount = len(cells)
 	}
 
 	sf.currentSheet.rowCount++
@@ -328,7 +357,7 @@ func (sf *StreamFile) NextSheet() error {
 	sheetIndex++
 	sf.currentSheet = &streamSheet{
 		index:       sheetIndex,
-		columnCount: len(sf.xlsxFile.Sheets[sheetIndex-1].Cols),
+		columnCount: sf.xlsxFile.Sheets[sheetIndex-1].MaxCol,
 		styleIds:    sf.styleIds[sheetIndex-1],
 		rowCount:    len(sf.xlsxFile.Sheets[sheetIndex-1].Rows),
 	}

+ 77 - 89
stream_file_builder.go

@@ -33,25 +33,30 @@ package xlsx
 import (
 	"archive/zip"
 	"errors"
-	"fmt"
 	"io"
 	"os"
 	"strconv"
 	"strings"
 )
 
+type cellStreamStyle map[int]StreamStyle
+type defaultCellType map[int]*CellType
+
 type StreamFileBuilder struct {
-	built                          bool
-	firstSheetAdded                bool
-	customStylesAdded              bool
-	xlsxFile                       *File
-	zipWriter                      *zip.Writer
-	cellTypeToStyleIds             map[CellType]int
-	maxStyleId                     int
-	styleIds                       [][]int
-	customStreamStyles             map[StreamStyle]struct{}
-	styleIdMap                     map[StreamStyle]int
-	defaultColumnCellMetadataAdded bool
+	built                                   bool
+	firstSheetAdded                         bool
+	customStylesAdded                       bool
+	xlsxFile                                *File
+	zipWriter                               *zip.Writer
+	cellTypeToStyleIds                      map[CellType]int
+	maxStyleId                              int
+	styleIds                                [][]int
+	customStreamStyles                      map[StreamStyle]struct{}
+	styleIdMap                              map[StreamStyle]int
+	streamingCellMetadatas                  map[int]*StreamingCellMetadata
+	sheetStreamStyles                       map[int]cellStreamStyle
+	sheetDefaultCellType                    map[int]defaultCellType
+	defaultColumnStreamingCellMetadataAdded bool
 }
 
 const (
@@ -70,12 +75,15 @@ var BuiltStreamFileBuilderError = errors.New("StreamFileBuilder has already been
 // NewStreamFileBuilder creates an StreamFileBuilder that will write to the the provided io.writer
 func NewStreamFileBuilder(writer io.Writer) *StreamFileBuilder {
 	return &StreamFileBuilder{
-		zipWriter:          zip.NewWriter(writer),
-		xlsxFile:           NewFile(),
-		cellTypeToStyleIds: make(map[CellType]int),
-		maxStyleId:         initMaxStyleId,
-		customStreamStyles: make(map[StreamStyle]struct{}),
-		styleIdMap:         make(map[StreamStyle]int),
+		zipWriter:              zip.NewWriter(writer),
+		xlsxFile:               NewFile(),
+		cellTypeToStyleIds:     make(map[CellType]int),
+		maxStyleId:             initMaxStyleId,
+		customStreamStyles:     make(map[StreamStyle]struct{}),
+		styleIdMap:             make(map[StreamStyle]int),
+		streamingCellMetadatas: make(map[int]*StreamingCellMetadata),
+		sheetStreamStyles:      make(map[int]cellStreamStyle),
+		sheetDefaultCellType:   make(map[int]defaultCellType),
 	}
 }
 
@@ -92,13 +100,11 @@ func NewStreamFileBuilderForPath(path string) (*StreamFileBuilder, error) {
 // AddSheet will add sheets with the given name with the provided headers. The headers cannot be edited later, and all
 // rows written to the sheet must contain the same number of cells as the header. Sheet names must be unique, or an
 // error will be thrown.
-func (sb *StreamFileBuilder) AddSheet(name string, headers []string, cellTypes []*CellType) error {
+func (sb *StreamFileBuilder) AddSheet(name string, cellTypes []*CellType) error {
 	if sb.built {
 		return BuiltStreamFileBuilderError
 	}
-	if len(cellTypes) > len(headers) {
-		return errors.New("cellTypes is longer than headers")
-	}
+
 	sheet, err := sb.xlsxFile.AddSheet(name)
 	if err != nil {
 		// Set built on error so that all subsequent calls to the builder will also fail.
@@ -106,12 +112,7 @@ func (sb *StreamFileBuilder) AddSheet(name string, headers []string, cellTypes [
 		return err
 	}
 	sb.styleIds = append(sb.styleIds, []int{})
-	row := sheet.AddRow()
-	if count := row.WriteSlice(&headers, -1); count != len(headers) {
-		// Set built on error so that all subsequent calls to the builder will also fail.
-		sb.built = true
-		return errors.New("failed to write headers")
-	}
+
 	for i, cellType := range cellTypes {
 		var cellStyleIndex int
 		var ok bool
@@ -127,49 +128,47 @@ func (sb *StreamFileBuilder) AddSheet(name string, headers []string, cellTypes [
 				cellStyleIndex = sb.maxStyleId
 				sb.cellTypeToStyleIds[*cellType] = sb.maxStyleId
 			}
-			sheet.Cols[i].SetType(*cellType)
+			sheet.SetType(i+1, i+1, *cellType)
+
 		}
 		sb.styleIds[len(sb.styleIds)-1] = append(sb.styleIds[len(sb.styleIds)-1], cellStyleIndex)
 	}
 	return nil
 }
 
-func (sb *StreamFileBuilder) AddSheetWithDefaultColumnMetadata(name string, headers []string, columnsDefaultCellMetadata []*CellMetadata) error {
+func (sb *StreamFileBuilder) AddSheetWithDefaultColumnMetadata(name string, columnsDefaultStreamingCellMetadata []*StreamingCellMetadata) error {
 	if sb.built {
 		return BuiltStreamFileBuilderError
 	}
-	if len(columnsDefaultCellMetadata) > len(headers) {
-		return errors.New("columnsDefaultCellMetadata is longer than headers")
-	}
-	sheet, err := sb.xlsxFile.AddSheet(name)
+	_, err := sb.xlsxFile.AddSheet(name)
 	if err != nil {
 		// Set built on error so that all subsequent calls to the builder will also fail.
 		sb.built = true
 		return err
 	}
 	sb.styleIds = append(sb.styleIds, []int{})
-	row := sheet.AddRow()
-	if count := row.WriteSlice(&headers, -1); count != len(headers) {
-		// Set built on error so that all subsequent calls to the builder will also fail.
-		sb.built = true
-		return errors.New("failed to write headers")
-	}
-	for i, cellMetadata := range columnsDefaultCellMetadata {
+	sheetIndex := len(sb.xlsxFile.Sheets) - 1
+
+	cSS := make(cellStreamStyle)
+	dCT := make(defaultCellType)
+	for i, streamingCellMetadata := range columnsDefaultStreamingCellMetadata {
 		var cellStyleIndex int
 		var ok bool
-		if cellMetadata != nil {
+		if streamingCellMetadata != nil {
 			// Exact same logic as `AddSheet` to ensure compatibility as much as possible
 			// with the `AddSheet` + `StreamFile.Write` code path
-			cellStyleIndex, ok = sb.cellTypeToStyleIds[cellMetadata.cellType]
+			cellStyleIndex, ok = sb.cellTypeToStyleIds[streamingCellMetadata.cellType]
 			if !ok {
 				sb.maxStyleId++
 				cellStyleIndex = sb.maxStyleId
-				sb.cellTypeToStyleIds[cellMetadata.cellType] = sb.maxStyleId
+				sb.cellTypeToStyleIds[streamingCellMetadata.cellType] = sb.maxStyleId
 			}
 
 			// Add streamStyle and set default cell metadata on col
-			sb.customStreamStyles[cellMetadata.streamStyle] = struct{}{}
-			sheet.Cols[i].SetCellMetadata(*cellMetadata)
+			sb.customStreamStyles[streamingCellMetadata.streamStyle] = struct{}{}
+			sb.streamingCellMetadatas[i+1] = streamingCellMetadata
+			cSS[i] = streamingCellMetadata.streamStyle
+			dCT[i] = streamingCellMetadata.cellType.Ptr()
 		}
 		sb.styleIds[len(sb.styleIds)-1] = append(sb.styleIds[len(sb.styleIds)-1], cellStyleIndex)
 	}
@@ -179,7 +178,9 @@ func (sb *StreamFileBuilder) AddSheetWithDefaultColumnMetadata(name string, head
 	sb.customStylesAdded = true
 	// Hack to ensure the `dimension` tag on each `worksheet` xml is stripped. Otherwise only the first
 	// row of each worksheet will be read back rather than all rows
-	sb.defaultColumnCellMetadataAdded = true
+	sb.defaultColumnStreamingCellMetadataAdded = true
+	sb.sheetStreamStyles[sheetIndex] = cSS
+	sb.sheetDefaultCellType[sheetIndex] = dCT
 	return nil
 }
 
@@ -211,23 +212,28 @@ func (sb *StreamFileBuilder) AddSheetS(name string, columnStyles []StreamStyle)
 	// Is needed for stream file to work but is not needed for streaming with styles
 	sb.styleIds = append(sb.styleIds, []int{})
 
-	sheet.maybeAddCol(len(columnStyles))
+	if sheet.Cols == nil {
+		panic("trying to use uninitialised ColStore")
+	}
 
+	cSS := make(map[int]StreamStyle)
 	// Set default column styles based on the cel styles in the first row
 	// Set the default column width to 11. This makes enough places for the
 	// default date style cells to display the dates correctly
 	for i, colStyle := range columnStyles {
-		sheet.Cols[i].SetStreamStyle(colStyle)
-		sheet.Cols[i].Width = 11
+		colNum := i + 1
+		cSS[colNum] = colStyle
+		sheet.SetColWidth(colNum, colNum, 11)
 	}
+	sheetIndex := len(sb.xlsxFile.Sheets) - 1
+	sb.sheetStreamStyles[sheetIndex] = cSS
 	return nil
 }
 
-// AddValidation will add a validation to a specific column.
-func (sb *StreamFileBuilder) AddValidation(sheetIndex, colIndex, rowStartIndex int, validation *xlsxCellDataValidation) {
+// AddValidation will add a validation to a sheet.
+func (sb *StreamFileBuilder) AddValidation(sheetIndex int, validation *xlsxDataValidation) {
 	sheet := sb.xlsxFile.Sheets[sheetIndex]
-	column := sheet.Col(colIndex)
-	column.SetDataValidationWithStart(validation, rowStartIndex)
+	sheet.AddDataValidation(validation)
 }
 
 // Build begins streaming the XLSX file to the io, by writing all the XLSX metadata. It creates a StreamFile struct
@@ -251,23 +257,26 @@ func (sb *StreamFileBuilder) Build() (*StreamFile, error) {
 	}
 
 	es := &StreamFile{
-		zipWriter:      sb.zipWriter,
-		xlsxFile:       sb.xlsxFile,
-		sheetXmlPrefix: make([]string, len(sb.xlsxFile.Sheets)),
-		sheetXmlSuffix: make([]string, len(sb.xlsxFile.Sheets)),
-		styleIds:       sb.styleIds,
-		styleIdMap:     sb.styleIdMap,
+		zipWriter:              sb.zipWriter,
+		xlsxFile:               sb.xlsxFile,
+		sheetXmlPrefix:         make([]string, len(sb.xlsxFile.Sheets)),
+		sheetXmlSuffix:         make([]string, len(sb.xlsxFile.Sheets)),
+		styleIds:               sb.styleIds,
+		styleIdMap:             sb.styleIdMap,
+		streamingCellMetadatas: sb.streamingCellMetadatas,
+		sheetStreamStyles:      sb.sheetStreamStyles,
+		sheetDefaultCellType:   sb.sheetDefaultCellType,
 	}
 	for path, data := range parts {
 		// If the part is a sheet, don't write it yet. We only want to write the XLSX metadata files, since at this
 		// point the sheets are still empty. The sheet files will be written later as their rows come in.
 		if strings.HasPrefix(path, sheetFilePathPrefix) {
-			// sb.defaultColumnCellMetadataAdded is a hack because neither the `AddSheet` nor `AddSheetS` codepaths
+			// sb.default ColumnStreamingCellMetadataAdded is a hack because neither the `AddSheet` nor `AddSheetS` codepaths
 			// actually encode a valid worksheet dimension. `AddSheet` encodes an empty one: "" and `AddSheetS` encodes
 			// an effectively empty one: "A1". `AddSheetWithDefaultColumnMetadata` uses logic from both paths which results
 			// in an effectively invalid dimension being encoded which, upon read, results in only reading in the header of
 			// a given worksheet and non of the rows that follow
-			if err := sb.processEmptySheetXML(es, path, data, !sb.customStylesAdded || sb.defaultColumnCellMetadataAdded); err != nil {
+			if err := sb.processEmptySheetXML(es, path, data, !sb.customStylesAdded || sb.defaultColumnStreamingCellMetadataAdded); err != nil {
 				return nil, err
 			}
 			continue
@@ -290,7 +299,7 @@ func (sb *StreamFileBuilder) Build() (*StreamFile, error) {
 
 func (sb *StreamFileBuilder) marshalStyles() (string, error) {
 
-	for streamStyle, _ := range sb.customStreamStyles {
+	for streamStyle := range sb.customStreamStyles {
 		XfId := handleStyleForXLSX(streamStyle.style, streamStyle.xNumFmtId, sb.xlsxFile.styles)
 		sb.styleIdMap[streamStyle] = XfId
 	}
@@ -344,10 +353,7 @@ func (sb *StreamFileBuilder) processEmptySheetXML(sf *StreamFile, path, data str
 	// Remove the Dimension tag. Since more rows are going to be written to the sheet, it will be wrong.
 	// It is valid to for a sheet to be missing a Dimension tag, but it is not valid for it to be wrong.
 	if removeDimensionTagFlag {
-		data, err = removeDimensionTag(data, sf.xlsxFile.Sheets[sheetIndex])
-		if err != nil {
-			return err
-		}
+		data = removeDimensionTag(data)
 	}
 
 	// Split the sheet at the end of its SheetData tag so that more rows can be added inside.
@@ -381,28 +387,10 @@ func getSheetIndex(sf *StreamFile, path string) (int, error) {
 // removeDimensionTag will return the passed in XLSX Spreadsheet XML with the dimension tag removed.
 // data is the XML data for the sheet
 // sheet is the Sheet struct that the XML was created from.
-// Can return an error if the XML's dimension tag does not match what is expected based on the provided Sheet
-func removeDimensionTag(data string, sheet *Sheet) (string, error) {
-	x := len(sheet.Cols) - 1
-	y := len(sheet.Rows) - 1
-	if x < 0 {
-		x = 0
-	}
-	if y < 0 {
-		y = 0
-	}
-	var dimensionRef string
-	if x == 0 && y == 0 {
-		dimensionRef = "A1"
-	} else {
-		endCoordinate := GetCellIDStringFromCoords(x, y)
-		dimensionRef = "A1:" + endCoordinate
-	}
-	dataParts := strings.Split(data, fmt.Sprintf(dimensionTag, dimensionRef))
-	if len(dataParts) != 2 {
-		return "", errors.New("unexpected Sheet XML: dimension tag not found")
-	}
-	return dataParts[0] + dataParts[1], nil
+func removeDimensionTag(data string) string {
+	start := strings.Index(data, "<dimension")
+	end := strings.Index(data, "</dimension>") + 12
+	return data[0:start] + data[end:]
 }
 
 // splitSheetIntoPrefixAndSuffix will split the provided XML sheet into a prefix and a suffix so that

+ 14 - 0
stream_file_builder_test.go

@@ -0,0 +1,14 @@
+package xlsx
+
+import (
+	"testing"
+
+	qt "github.com/frankban/quicktest"
+)
+
+func TestRemoveDimensionTag(t *testing.T) {
+	c := qt.New(t)
+	out := removeDimensionTag(`<foo><dimension ref="A1:Z20"></dimension></foo>`)
+	c.Assert("<foo></foo>", qt.Equals, out)
+
+}

+ 5 - 5
stream_style.go

@@ -75,11 +75,11 @@ func init() {
 
 	StreamStyleDefaultDecimal = MakeDecimalStyle(DefaultFont(), DefaultFill(), DefaultAlignment(), DefaultBorder())
 
-	DefaultStringCellMetadata = CellMetadata{CellTypeString, StreamStyleDefaultString}
-	DefaultNumericCellMetadata = CellMetadata{CellTypeNumeric, StreamStyleDefaultString}
-	DefaultDecimalCellMetadata = CellMetadata{CellTypeNumeric, StreamStyleDefaultDecimal}
-	DefaultIntegerCellMetadata = CellMetadata{CellTypeNumeric, StreamStyleDefaultInteger}
-	DefaultDateCellMetadata = CellMetadata{CellTypeDate, StreamStyleDefaultDate}
+	DefaultStringStreamingCellMetadata = StreamingCellMetadata{CellTypeString, StreamStyleDefaultString}
+	DefaultNumericStreamingCellMetadata = StreamingCellMetadata{CellTypeNumeric, StreamStyleDefaultString}
+	DefaultDecimalStreamingCellMetadata = StreamingCellMetadata{CellTypeNumeric, StreamStyleDefaultDecimal}
+	DefaultIntegerStreamingCellMetadata = StreamingCellMetadata{CellTypeNumeric, StreamStyleDefaultInteger}
+	DefaultDateStreamingCellMetadata = StreamingCellMetadata{CellTypeDate, StreamStyleDefaultDate}
 }
 
 // MakeStyle creates a new StreamStyle and add it to the styles that will be streamed.

+ 101 - 77
stream_style_test.go

@@ -4,10 +4,10 @@ import (
 	"bytes"
 	"errors"
 	"fmt"
-	. "gopkg.in/check.v1"
 	"io"
 	"reflect"
 	"strconv"
+	"testing"
 	"time"
 )
 
@@ -15,17 +15,13 @@ const (
 	StyleStreamTestsShouldMakeRealFiles = false
 )
 
-type StreamStyleSuite struct{}
-
-var _ = Suite(&StreamStyleSuite{})
-
-func (s *StreamStyleSuite) TestStreamTestsShouldMakeRealFilesShouldBeFalse(t *C) {
+func TestStreamTestsShouldMakeRealFilesShouldBeFalse(t *testing.T) {
 	if StyleStreamTestsShouldMakeRealFiles {
 		t.Fatal("TestsShouldMakeRealFiles should only be true for local debugging. Don't forget to switch back before commiting.")
 	}
 }
 
-func (s *StreamStyleSuite) TestXlsxStreamWriteWithStyle(t *C) {
+func TestXlsxStreamWriteWithStyle(t *testing.T) {
 	// When shouldMakeRealFiles is set to true this test will make actual XLSX files in the file system.
 	// This is useful to ensure files open in Excel, Numbers, Google Docs, etc.
 	// In case of issues you can use "Open XML SDK 2.5" to diagnose issues in generated XLSX files:
@@ -43,10 +39,18 @@ func (s *StreamStyleSuite) TestXlsxStreamWriteWithStyle(t *C) {
 			},
 			workbookData: [][][]StreamCell{
 				{
-					{NewStyledStringStreamCell("1", StreamStyleUnderlinedString), NewStyledStringStreamCell("25", StreamStyleItalicString),
-						NewStyledStringStreamCell("A", StreamStyleBoldString), NewStringStreamCell("B")},
-					{NewIntegerStreamCell(1234), NewStyledIntegerStreamCell(98, StreamStyleBoldInteger),
-						NewStyledIntegerStreamCell(34, StreamStyleItalicInteger), NewStyledIntegerStreamCell(26, StreamStyleUnderlinedInteger)},
+					{
+						NewStyledStringStreamCell("1", StreamStyleUnderlinedString),
+						NewStyledStringStreamCell("25", StreamStyleItalicString),
+						NewStyledStringStreamCell("A", StreamStyleBoldString),
+						NewStringStreamCell("B"),
+					},
+					{
+						NewIntegerStreamCell(1234),
+						NewStyledIntegerStreamCell(98, StreamStyleBoldInteger),
+						NewStyledIntegerStreamCell(34, StreamStyleItalicInteger),
+						NewStyledIntegerStreamCell(26, StreamStyleUnderlinedInteger),
+					},
 				},
 			},
 		},
@@ -263,55 +267,63 @@ func (s *StreamStyleSuite) TestXlsxStreamWriteWithStyle(t *C) {
 	}
 
 	for i, testCase := range testCases {
-		var filePath string
-		var buffer bytes.Buffer
-		if StyleStreamTestsShouldMakeRealFiles {
-			filePath = fmt.Sprintf("WorkbookWithStyle%d.xlsx", i)
-		}
+		t.Run(testCase.testName, func(t *testing.T) {
+			var filePath string
+			var buffer bytes.Buffer
+			if StyleStreamTestsShouldMakeRealFiles {
+				filePath = fmt.Sprintf("WorkbookWithStyle%d.xlsx", i)
+			}
 
-		err := writeStreamFileWithStyle(filePath, &buffer, testCase.sheetNames, testCase.workbookData, StyleStreamTestsShouldMakeRealFiles, []StreamStyle{})
-		if err != testCase.expectedError && err.Error() != testCase.expectedError.Error() {
-			t.Fatalf("Error differs from expected error. Error: %v, Expected Error: %v ", err, testCase.expectedError)
-		}
-		if testCase.expectedError != nil {
-			//return
-			continue
-		}
-		// read the file back with the xlsx package
-		var bufReader *bytes.Reader
-		var size int64
-		if !StyleStreamTestsShouldMakeRealFiles {
-			bufReader = bytes.NewReader(buffer.Bytes())
-			size = bufReader.Size()
-		}
-		actualSheetNames, actualWorkbookData, actualWorkbookCells := readXLSXFileS(t, filePath, bufReader, size, StyleStreamTestsShouldMakeRealFiles)
-		// check if data was able to be read correctly
-		if !reflect.DeepEqual(actualSheetNames, testCase.sheetNames) {
-			t.Fatal("Expected sheet names to be equal")
-		}
+			err := writeStreamFileWithStyle(filePath, &buffer, testCase.sheetNames, testCase.workbookData, StyleStreamTestsShouldMakeRealFiles, []StreamStyle{})
+			switch {
+			case err == nil && testCase.expectedError != nil:
+				t.Fatalf("Expected error but none was returned")
+			case err != nil && testCase.expectedError == nil:
+				t.Fatalf("Unexpected error: %q", err.Error())
+			case err != testCase.expectedError && err.Error() != testCase.expectedError.Error():
+				t.Fatalf("Error differs from expected error. Error: %v, Expected Error: %v ", err, testCase.expectedError)
+			case err != nil:
+				// We got an expected error
+				return
+			}
 
-		expectedWorkbookDataStrings := [][][]string{}
-		for j, _ := range testCase.workbookData {
-			expectedWorkbookDataStrings = append(expectedWorkbookDataStrings, [][]string{})
-			for k, _ := range testCase.workbookData[j] {
-				if len(testCase.workbookData[j][k]) == 0 {
-					expectedWorkbookDataStrings[j] = append(expectedWorkbookDataStrings[j], nil)
-				} else {
-					expectedWorkbookDataStrings[j] = append(expectedWorkbookDataStrings[j], []string{})
-					for _, cell := range testCase.workbookData[j][k] {
-						expectedWorkbookDataStrings[j][k] = append(expectedWorkbookDataStrings[j][k], cell.cellData)
+			// read the file back with the xlsx package
+			var bufReader *bytes.Reader
+			var size int64
+			if !StyleStreamTestsShouldMakeRealFiles {
+				bufReader = bytes.NewReader(buffer.Bytes())
+				size = bufReader.Size()
+			}
+			actualSheetNames, actualWorkbookData, actualWorkbookCells := readXLSXFileS(t, filePath, bufReader, size, StyleStreamTestsShouldMakeRealFiles)
+			// check if data was able to be read correctly
+			if !reflect.DeepEqual(actualSheetNames, testCase.sheetNames) {
+				t.Fatal("Expected sheet names to be equal")
+			}
+
+			expectedWorkbookDataStrings := [][][]string{}
+			for j := range testCase.workbookData {
+				expectedWorkbookDataStrings = append(expectedWorkbookDataStrings, [][]string{})
+				for k := range testCase.workbookData[j] {
+					if len(testCase.workbookData[j][k]) == 0 {
+						expectedWorkbookDataStrings[j] = append(expectedWorkbookDataStrings[j], nil)
+					} else {
+						expectedWorkbookDataStrings[j] = append(expectedWorkbookDataStrings[j], []string{})
+						for _, cell := range testCase.workbookData[j][k] {
+							expectedWorkbookDataStrings[j][k] = append(expectedWorkbookDataStrings[j][k], cell.cellData)
+						}
 					}
 				}
+
+			}
+			if !reflect.DeepEqual(actualWorkbookData, expectedWorkbookDataStrings) {
+				t.Fatal("Expected workbook data to be equal")
 			}
 
-		}
-		if !reflect.DeepEqual(actualWorkbookData, expectedWorkbookDataStrings) {
-			t.Fatal("Expected workbook data to be equal")
-		}
+			if err := checkForCorrectCellStyles(actualWorkbookCells, testCase.workbookData); err != nil {
+				t.Fatal("Expected styles to be equal")
+			}
 
-		if err := checkForCorrectCellStyles(actualWorkbookCells, testCase.workbookData); err != nil {
-			t.Fatal("Expected styles to be equal")
-		}
+		})
 	}
 }
 
@@ -380,7 +392,7 @@ func writeStreamFileWithStyle(filePath string, fileBuffer io.Writer, sheetNames
 }
 
 // readXLSXFileS will read the file using the xlsx package.
-func readXLSXFileS(t *C, filePath string, fileBuffer io.ReaderAt, size int64, shouldMakeRealFiles bool) ([]string, [][][]string, [][][]Cell) {
+func readXLSXFileS(t *testing.T, filePath string, fileBuffer io.ReaderAt, size int64, shouldMakeRealFiles bool) ([]string, [][][]string, [][][]Cell) {
 	var readFile *File
 	var err error
 	if shouldMakeRealFiles {
@@ -419,24 +431,26 @@ func readXLSXFileS(t *C, filePath string, fileBuffer io.ReaderAt, size int64, sh
 	return sheetNames, actualWorkbookData, actualWorkBookCells
 }
 
-func (s *StreamStyleSuite) TestDates(t *C) {
+func TestStreamStyleDates(t *testing.T) {
 	var filePath string
 	var buffer bytes.Buffer
 	if StyleStreamTestsShouldMakeRealFiles {
 		filePath = fmt.Sprintf("Workbook_Date_test.xlsx")
 	}
 
+	// We use a fixed time to avoid weird errors around midnight
+	fixedTime := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)
 	sheetNames := []string{"Sheet1"}
 	workbookData := [][][]StreamCell{
 		{
 			{NewStringStreamCell("Date:")},
-			{NewDateStreamCell(time.Now())},
+			{NewDateStreamCell(fixedTime)},
 		},
 	}
 
 	err := writeStreamFileWithStyle(filePath, &buffer, sheetNames, workbookData, StyleStreamTestsShouldMakeRealFiles, []StreamStyle{})
 	if err != nil {
-		t.Fatal("Error during writing")
+		t.Fatalf("Error during writing: %s", err.Error())
 	}
 
 	// read the file back with the xlsx package
@@ -453,7 +467,7 @@ func (s *StreamStyleSuite) TestDates(t *C) {
 	}
 
 	expectedWorkbookDataStrings := [][][]string{}
-	for j, _ := range workbookData {
+	for j := range workbookData {
 		expectedWorkbookDataStrings = append(expectedWorkbookDataStrings, [][]string{})
 		for range workbookData[j] {
 			expectedWorkbookDataStrings[j] = append(expectedWorkbookDataStrings[j], []string{})
@@ -461,7 +475,7 @@ func (s *StreamStyleSuite) TestDates(t *C) {
 	}
 
 	expectedWorkbookDataStrings[0][0] = append(expectedWorkbookDataStrings[0][0], workbookData[0][0][0].cellData)
-	year, month, day := time.Now().Date()
+	year, month, day := fixedTime.Date()
 	monthString := strconv.Itoa(int(month))
 	if int(month) < 10 {
 		monthString = "0" + monthString
@@ -470,11 +484,21 @@ func (s *StreamStyleSuite) TestDates(t *C) {
 	if day < 10 {
 		dayString = "0" + dayString
 	}
+	yearString := strconv.Itoa(year - 2000)
+	if (year - 2000) < 10 {
+		yearString = "0" + yearString
+	}
 	expectedWorkbookDataStrings[0][1] = append(expectedWorkbookDataStrings[0][1],
-		monthString+"-"+dayString+"-"+strconv.Itoa(year-2000))
+		monthString+"-"+dayString+"-"+yearString)
 
 	if !reflect.DeepEqual(actualWorkbookData, expectedWorkbookDataStrings) {
-		t.Fatal("Expected workbook data to be equal")
+		t.Fatalf(`Expected workbook data to be equal:
+    Expected:
+%s
+
+    Actual:
+%s
+`, expectedWorkbookDataStrings, actualWorkbookData)
 	}
 
 	if err := checkForCorrectCellStyles(actualWorkbookCells, workbookData); err != nil {
@@ -482,7 +506,7 @@ func (s *StreamStyleSuite) TestDates(t *C) {
 	}
 }
 
-func (s *StreamSuite) TestMakeNewStylesAndUseIt(t *C) {
+func TestMakeNewStylesAndUseIt(t *testing.T) {
 	var filePath string
 	var buffer bytes.Buffer
 	if StyleStreamTestsShouldMakeRealFiles {
@@ -528,9 +552,9 @@ func (s *StreamSuite) TestMakeNewStylesAndUseIt(t *C) {
 	}
 
 	expectedWorkbookDataStrings := [][][]string{}
-	for j, _ := range workbookData {
+	for j := range workbookData {
 		expectedWorkbookDataStrings = append(expectedWorkbookDataStrings, [][]string{})
-		for k, _ := range workbookData[j] {
+		for k := range workbookData[j] {
 			expectedWorkbookDataStrings[j] = append(expectedWorkbookDataStrings[j], []string{})
 			for _, cell := range workbookData[j][k] {
 				expectedWorkbookDataStrings[j][k] = append(expectedWorkbookDataStrings[j][k], cell.cellData)
@@ -547,7 +571,7 @@ func (s *StreamSuite) TestMakeNewStylesAndUseIt(t *C) {
 	}
 }
 
-func (s *StreamSuite) TestNewTypes(t *C) {
+func TestStreamNewTypes(t *testing.T) {
 	var filePath string
 	var buffer bytes.Buffer
 	if StyleStreamTestsShouldMakeRealFiles {
@@ -583,9 +607,9 @@ func (s *StreamSuite) TestNewTypes(t *C) {
 	}
 
 	expectedWorkbookDataStrings := [][][]string{}
-	for j, _ := range workbookData {
+	for j := range workbookData {
 		expectedWorkbookDataStrings = append(expectedWorkbookDataStrings, [][]string{})
-		for k, _ := range workbookData[j] {
+		for k := range workbookData[j] {
 			expectedWorkbookDataStrings[j] = append(expectedWorkbookDataStrings[j], []string{})
 			for _, cell := range workbookData[j][k] {
 				if cell.cellData == "1" {
@@ -606,7 +630,7 @@ func (s *StreamSuite) TestNewTypes(t *C) {
 	}
 }
 
-func (s *StreamStyleSuite) TestCloseWithNothingWrittenToSheetsWithStyle(t *C) {
+func TestStreamCloseWithNothingWrittenToSheetsWithStyle(t *testing.T) {
 	buffer := bytes.NewBuffer(nil)
 	file := NewStreamFileBuilder(buffer)
 
@@ -668,7 +692,7 @@ func (s *StreamStyleSuite) TestCloseWithNothingWrittenToSheetsWithStyle(t *C) {
 	}
 }
 
-func (s *StreamStyleSuite) TestBuildErrorsAfterBuildWithStyle(t *C) {
+func TestStreamBuildErrorsAfterBuildWithStyle(t *testing.T) {
 	file := NewStreamFileBuilder(bytes.NewBuffer(nil))
 
 	defaultStyles := []StreamStyle{StreamStyleDefaultString, StreamStyleBoldString, StreamStyleItalicInteger, StreamStyleUnderlinedString,
@@ -698,7 +722,7 @@ func (s *StreamStyleSuite) TestBuildErrorsAfterBuildWithStyle(t *C) {
 	}
 }
 
-func (s *StreamStyleSuite) TestAddSheetSWithErrorsAfterBuild(t *C) {
+func TestStreamAddSheetSWithErrorsAfterBuild(t *testing.T) {
 	file := NewStreamFileBuilder(bytes.NewBuffer(nil))
 
 	defaultStyles := []StreamStyle{StreamStyleDefaultString, StreamStyleBoldString, StreamStyleItalicInteger, StreamStyleUnderlinedString,
@@ -728,7 +752,7 @@ func (s *StreamStyleSuite) TestAddSheetSWithErrorsAfterBuild(t *C) {
 	}
 }
 
-func (s *StreamStyleSuite) TestNoStylesAddSheetSError(t *C) {
+func TestStreamNoStylesAddSheetSError(t *testing.T) {
 	buffer := bytes.NewBuffer(nil)
 	file := NewStreamFileBuilder(buffer)
 
@@ -749,7 +773,7 @@ func (s *StreamStyleSuite) TestNoStylesAddSheetSError(t *C) {
 	}
 }
 
-func (s *StreamStyleSuite) TestNoStylesWriteSError(t *C) {
+func TestStreamNoStylesWriteSError(t *testing.T) {
 	buffer := bytes.NewBuffer(nil)
 	var filePath string
 
@@ -762,16 +786,16 @@ func (s *StreamStyleSuite) TestNoStylesWriteSError(t *C) {
 	}
 
 	err := writeStreamFileWithStyle(filePath, buffer, sheetNames, workbookData, StyleStreamTestsShouldMakeRealFiles, []StreamStyle{})
-	if err.Error() != "trying to make use of a style that has not been added" {
-		t.Fatal("Error differs from expected error")
+	expected := "trying to make use of a style that has not been added"
+	if err.Error() != expected {
+		t.Fatalf("Error differs from expected error: Expected %q got %q", err.Error(), expected)
 	}
 
-
 }
 
 func checkForCorrectCellStyles(actualCells [][][]Cell, expectedCells [][][]StreamCell) error {
-	for i, _ := range actualCells {
-		for j, _ := range actualCells[i] {
+	for i := range actualCells {
+		for j := range actualCells[i] {
 			for k, actualCell := range actualCells[i][j] {
 				expectedCell := expectedCells[i][j][k]
 				if err := compareCellStyles(actualCell, expectedCell); err != nil {

+ 158 - 117
stream_test.go

@@ -6,6 +6,7 @@ import (
 	"io"
 	"reflect"
 	"strings"
+	"testing"
 
 	. "gopkg.in/check.v1"
 )
@@ -24,7 +25,7 @@ func (s *StreamSuite) TestTestsShouldMakeRealFilesShouldBeFalse(t *C) {
 	}
 }
 
-func (s *StreamSuite) TestXlsxStreamWrite(t *C) {
+func TestXlsxStreamWrite(t *testing.T) {
 	// When shouldMakeRealFiles is set to true this test will make actual XLSX files in the file system.
 	// This is useful to ensure files open in Excel, Numbers, Google Docs, etc.
 	// In case of issues you can use "Open XML SDK 2.5" to diagnose issues in generated XLSX files:
@@ -234,37 +235,45 @@ func (s *StreamSuite) TestXlsxStreamWrite(t *C) {
 		},
 	}
 	for i, testCase := range testCases {
-		var filePath string
-		var buffer bytes.Buffer
-		if TestsShouldMakeRealFiles {
-			filePath = fmt.Sprintf("Workbook%d.xlsx", i)
-		}
-		err := writeStreamFile(filePath, &buffer, testCase.sheetNames, testCase.workbookData, testCase.headerTypes, TestsShouldMakeRealFiles)
-		if err != testCase.expectedError && err.Error() != testCase.expectedError.Error() {
-			t.Fatalf("Error differs from expected error. Error: %v, Expected Error: %v ", err, testCase.expectedError)
-		}
-		if testCase.expectedError != nil {
-			return
-		}
-		// read the file back with the xlsx package
-		var bufReader *bytes.Reader
-		var size int64
-		if !TestsShouldMakeRealFiles {
-			bufReader = bytes.NewReader(buffer.Bytes())
-			size = bufReader.Size()
-		}
-		actualSheetNames, actualWorkbookData, _ := readXLSXFile(t, filePath, bufReader, size, TestsShouldMakeRealFiles)
-		// check if data was able to be read correctly
-		if !reflect.DeepEqual(actualSheetNames, testCase.sheetNames) {
-			t.Fatal("Expected sheet names to be equal")
-		}
-		if !reflect.DeepEqual(actualWorkbookData, testCase.workbookData) {
-			t.Fatal("Expected workbook data to be equal")
-		}
+		t.Run(testCase.testName, func(t *testing.T) {
+			var filePath string
+			var buffer bytes.Buffer
+			if TestsShouldMakeRealFiles {
+				filePath = fmt.Sprintf("Workbook%d.xlsx", i)
+			}
+			err := writeStreamFile(filePath, &buffer, testCase.sheetNames, testCase.workbookData, testCase.headerTypes, TestsShouldMakeRealFiles)
+			switch {
+			case err != nil && testCase.expectedError == nil:
+				t.Fatalf("Unexpected error: %v", err.Error())
+			case err == nil && testCase.expectedError != nil:
+				t.Fatalf("Error is nil, but expected error was: %v", testCase.expectedError)
+			case err != nil && testCase.expectedError != nil && err.Error() != testCase.expectedError.Error():
+				t.Fatalf("Error differs from expected error. Error: %v, Expected Error: %v ", err, testCase.expectedError)
+			}
+			if testCase.expectedError != nil {
+				return
+			}
+			// read the file back with the xlsx package
+			var bufReader *bytes.Reader
+			var size int64
+			if !TestsShouldMakeRealFiles {
+				bufReader = bytes.NewReader(buffer.Bytes())
+				size = bufReader.Size()
+			}
+			actualSheetNames, actualWorkbookData, _ := readXLSXFile(t, filePath, bufReader, size, TestsShouldMakeRealFiles)
+			// check if data was able to be read correctly
+			if !reflect.DeepEqual(actualSheetNames, testCase.sheetNames) {
+				t.Fatal("Expected sheet names to be equal")
+			}
+			if !reflect.DeepEqual(actualWorkbookData, testCase.workbookData) {
+				t.Fatal("Expected workbook data to be equal")
+			}
+
+		})
 	}
 }
 
-func (s *StreamSuite) TestXlsxStreamWriteWithDefaultCellType(t *C) {
+func TestXlsxStreamWriteWithDefaultCellType(t *testing.T) {
 	// When shouldMakeRealFiles is set to true this test will make actual XLSX files in the file system.
 	// This is useful to ensure files open in Excel, Numbers, Google Docs, etc.
 	// In case of issues you can use "Open XML SDK 2.5" to diagnose issues in generated XLSX files:
@@ -274,7 +283,7 @@ func (s *StreamSuite) TestXlsxStreamWriteWithDefaultCellType(t *C) {
 		sheetNames           []string
 		workbookData         [][][]string
 		expectedWorkbookData [][][]string
-		headerTypes          [][]*CellMetadata
+		headerTypes          [][]*StreamingCellMetadata
 		expectedError        error
 	}{
 		{
@@ -296,8 +305,8 @@ func (s *StreamSuite) TestXlsxStreamWriteWithDefaultCellType(t *C) {
 					{"123", "Taco", "string", "0000000123"},
 				},
 			},
-			headerTypes: [][]*CellMetadata{
-				{DefaultStringCellMetadata.Ptr(), DefaultStringCellMetadata.Ptr(), DefaultDecimalCellMetadata.Ptr(), DefaultStringCellMetadata.Ptr()},
+			headerTypes: [][]*StreamingCellMetadata{
+				{DefaultStringStreamingCellMetadata.Ptr(), DefaultStringStreamingCellMetadata.Ptr(), DefaultDecimalStreamingCellMetadata.Ptr(), DefaultStringStreamingCellMetadata.Ptr()},
 			},
 		},
 		{
@@ -317,8 +326,8 @@ func (s *StreamSuite) TestXlsxStreamWriteWithDefaultCellType(t *C) {
 					{"1234.00"},
 				},
 			},
-			headerTypes: [][]*CellMetadata{
-				{DefaultDecimalCellMetadata.Ptr()},
+			headerTypes: [][]*StreamingCellMetadata{
+				{DefaultDecimalStreamingCellMetadata.Ptr()},
 			},
 		},
 		{
@@ -358,14 +367,14 @@ func (s *StreamSuite) TestXlsxStreamWriteWithDefaultCellType(t *C) {
 					{"2357", "Margarita", "700"},
 				},
 			},
-			headerTypes: [][]*CellMetadata{
-				{DefaultIntegerCellMetadata.Ptr(), nil, DefaultDecimalCellMetadata.Ptr(), nil},
-				{DefaultIntegerCellMetadata.Ptr(), nil, DefaultDecimalCellMetadata.Ptr(), DefaultStringCellMetadata.Ptr(), DefaultIntegerCellMetadata.Ptr()},
+			headerTypes: [][]*StreamingCellMetadata{
+				{DefaultIntegerStreamingCellMetadata.Ptr(), nil, DefaultDecimalStreamingCellMetadata.Ptr(), nil},
+				{DefaultIntegerStreamingCellMetadata.Ptr(), nil, DefaultDecimalStreamingCellMetadata.Ptr(), DefaultStringStreamingCellMetadata.Ptr(), DefaultIntegerStreamingCellMetadata.Ptr()},
 				{nil, nil, nil},
 			},
 		},
 		{
-			testName: "Two Sheets with same the name",
+			testName: "Two Sheets with the same name",
 			sheetNames: []string{
 				"Sheet 1", "Sheet 1",
 			},
@@ -441,14 +450,25 @@ func (s *StreamSuite) TestXlsxStreamWriteWithDefaultCellType(t *C) {
 				{{}},
 				{{}},
 			},
-			headerTypes: [][]*CellMetadata{
-				{DefaultIntegerCellMetadata.Ptr(), DefaultStringCellMetadata.Ptr(), DefaultIntegerCellMetadata.Ptr(), DefaultStringCellMetadata.Ptr()},
+			headerTypes: [][]*StreamingCellMetadata{
+				{DefaultIntegerStreamingCellMetadata.Ptr(), DefaultStringStreamingCellMetadata.Ptr(), DefaultIntegerStreamingCellMetadata.Ptr(), DefaultStringStreamingCellMetadata.Ptr()},
 				{nil},
 				{nil, nil},
 				{nil},
 				{nil},
 				{nil},
 			},
+			expectedWorkbookData: [][][]string{
+				{
+					{"Token", "Name", "Price", "SKU"},
+					{"123", "Taco", "300", "0000000123"},
+				},
+				{{}},
+				{{"Id", "Unit Cost"}},
+				{{}},
+				{{}},
+				{{}},
+			},
 		},
 		{
 			testName: "Two Sheets, only writes to one, should not error and should still create a valid file",
@@ -462,8 +482,16 @@ func (s *StreamSuite) TestXlsxStreamWriteWithDefaultCellType(t *C) {
 				},
 				{{}},
 			},
-			headerTypes: [][]*CellMetadata{
-				{DefaultDateCellMetadata.Ptr(), DefaultDateCellMetadata.Ptr(), DefaultDateCellMetadata.Ptr(), DefaultStringCellMetadata.Ptr()},
+			expectedWorkbookData: [][][]string{
+				{
+					{"Token", "Name", "Price", "SKU"},
+					{"123", "Taco", "300", "0000000123"},
+				},
+				{{}},
+			},
+
+			headerTypes: [][]*StreamingCellMetadata{
+				{DefaultDateStreamingCellMetadata.Ptr(), DefaultDateStreamingCellMetadata.Ptr(), DefaultDateStreamingCellMetadata.Ptr(), DefaultStringStreamingCellMetadata.Ptr()},
 				{nil},
 			},
 		},
@@ -501,8 +529,8 @@ func (s *StreamSuite) TestXlsxStreamWriteWithDefaultCellType(t *C) {
 					{"789", "Burritos", "400", "754", "789", "Burritos", "400", "754", "789", "Burritos", "400", "754", "789", "Burritos", "400", "754", "789", "Burritos", "400", "754", "789", "Burritos", "400", "754"},
 				},
 			},
-			headerTypes: [][]*CellMetadata{
-				{DefaultIntegerCellMetadata.Ptr(), DefaultStringCellMetadata.Ptr(), DefaultIntegerCellMetadata.Ptr(), DefaultStringCellMetadata.Ptr(), DefaultStringCellMetadata.Ptr(), DefaultStringCellMetadata.Ptr(), DefaultStringCellMetadata.Ptr(), DefaultStringCellMetadata.Ptr(), DefaultStringCellMetadata.Ptr(), DefaultStringCellMetadata.Ptr(), DefaultStringCellMetadata.Ptr(), DefaultStringCellMetadata.Ptr(), DefaultStringCellMetadata.Ptr(), DefaultStringCellMetadata.Ptr(), DefaultStringCellMetadata.Ptr(), DefaultStringCellMetadata.Ptr(), DefaultStringCellMetadata.Ptr(), DefaultStringCellMetadata.Ptr(), DefaultStringCellMetadata.Ptr(), DefaultStringCellMetadata.Ptr(), DefaultStringCellMetadata.Ptr(), DefaultStringCellMetadata.Ptr(), DefaultStringCellMetadata.Ptr(), DefaultStringCellMetadata.Ptr()},
+			headerTypes: [][]*StreamingCellMetadata{
+				{DefaultIntegerStreamingCellMetadata.Ptr(), DefaultStringStreamingCellMetadata.Ptr(), DefaultIntegerStreamingCellMetadata.Ptr(), DefaultStringStreamingCellMetadata.Ptr(), DefaultStringStreamingCellMetadata.Ptr(), DefaultStringStreamingCellMetadata.Ptr(), DefaultStringStreamingCellMetadata.Ptr(), DefaultStringStreamingCellMetadata.Ptr(), DefaultStringStreamingCellMetadata.Ptr(), DefaultStringStreamingCellMetadata.Ptr(), DefaultStringStreamingCellMetadata.Ptr(), DefaultStringStreamingCellMetadata.Ptr(), DefaultStringStreamingCellMetadata.Ptr(), DefaultStringStreamingCellMetadata.Ptr(), DefaultStringStreamingCellMetadata.Ptr(), DefaultStringStreamingCellMetadata.Ptr(), DefaultStringStreamingCellMetadata.Ptr(), DefaultStringStreamingCellMetadata.Ptr(), DefaultStringStreamingCellMetadata.Ptr(), DefaultStringStreamingCellMetadata.Ptr(), DefaultStringStreamingCellMetadata.Ptr(), DefaultStringStreamingCellMetadata.Ptr(), DefaultStringStreamingCellMetadata.Ptr(), DefaultStringStreamingCellMetadata.Ptr()},
 			},
 		},
 		{
@@ -525,47 +553,67 @@ func (s *StreamSuite) TestXlsxStreamWriteWithDefaultCellType(t *C) {
 					{"123", "Taco", "300", "0000000123"},
 				},
 			},
-			headerTypes: [][]*CellMetadata{
-				{DefaultStringCellMetadata.Ptr(), DefaultStringCellMetadata.Ptr(), DefaultStringCellMetadata.Ptr(), DefaultStringCellMetadata.Ptr(), DefaultStringCellMetadata.Ptr()},
+			headerTypes: [][]*StreamingCellMetadata{
+				{DefaultStringStreamingCellMetadata.Ptr(), DefaultStringStreamingCellMetadata.Ptr(), DefaultStringStreamingCellMetadata.Ptr(), DefaultStringStreamingCellMetadata.Ptr()},
 			},
 		},
 	}
 	for i, testCase := range testCases {
+		// if testCase.testName != "Lots of Sheets, only writes rows to one, only writes headers to one, should not error and should still create a valid file" {
+		// 	continue
+		// }
+		t.Run(testCase.testName, func(t *testing.T) {
 
-		var filePath string
-		var buffer bytes.Buffer
-		if TestsShouldMakeRealFiles {
-			filePath = fmt.Sprintf("WorkbookTyped%d.xlsx", i)
-		}
-		err := writeStreamFileWithDefaultMetadata(filePath, &buffer, testCase.sheetNames, testCase.workbookData, testCase.headerTypes, TestsShouldMakeRealFiles)
-		if err != testCase.expectedError && err.Error() != testCase.expectedError.Error() {
-			t.Fatalf("Error differs from expected error. Error: %v, Expected Error: %v ", err, testCase.expectedError)
-		}
-		if testCase.expectedError != nil {
-			return
-		}
-		// read the file back with the xlsx package
-		var bufReader *bytes.Reader
-		var size int64
-		if !TestsShouldMakeRealFiles {
-			bufReader = bytes.NewReader(buffer.Bytes())
-			size = bufReader.Size()
-		}
-		actualSheetNames, actualWorkbookData, workbookCellTypes := readXLSXFile(t, filePath, bufReader, size, TestsShouldMakeRealFiles)
-		verifyCellTypesInColumnMatchHeaderType(t, workbookCellTypes, testCase.headerTypes, testCase.workbookData)
-		// check if data was able to be read correctly
-		if !reflect.DeepEqual(actualSheetNames, testCase.sheetNames) {
-			t.Fatal("Expected sheet names to be equal")
-		}
-		if !reflect.DeepEqual(actualWorkbookData, testCase.expectedWorkbookData) {
-			t.Fatal("Expected workbook data to be equal")
-		}
+			var filePath string
+			var buffer bytes.Buffer
+			if TestsShouldMakeRealFiles {
+				filePath = fmt.Sprintf("WorkbookTyped%d.xlsx", i)
+			}
+			err := writeStreamFileWithDefaultMetadata(filePath, &buffer, testCase.sheetNames, testCase.workbookData, testCase.headerTypes, TestsShouldMakeRealFiles)
+			switch {
+			case err == nil && testCase.expectedError != nil:
+				t.Fatalf("Expected an error, but nil was returned\n")
+			case err != nil && testCase.expectedError == nil:
+				t.Fatalf("Unexpected error: %q", err.Error())
+			case err != testCase.expectedError && err.Error() != testCase.expectedError.Error():
+				t.Fatalf("Error differs from expected error. Error: %v, Expected Error: %v ", err, testCase.expectedError)
+			case err != nil:
+				// We got an error we expected
+				return
+			}
+
+			// read the file back with the xlsx package
+			var bufReader *bytes.Reader
+			var size int64
+			if !TestsShouldMakeRealFiles {
+				bufReader = bytes.NewReader(buffer.Bytes())
+				size = bufReader.Size()
+			}
+			actualSheetNames, actualWorkbookData, workbookCellTypes := readXLSXFile(t, filePath, bufReader, size, TestsShouldMakeRealFiles)
+			verifyCellTypesInColumnMatchHeaderType(t, workbookCellTypes, testCase.headerTypes, testCase.workbookData)
+			// check if data was able to be read correctly
+			if !reflect.DeepEqual(actualSheetNames, testCase.sheetNames) {
+				t.Fatal("Expected sheet names to be equal")
+			}
+			if testCase.expectedWorkbookData == nil {
+				testCase.expectedWorkbookData = testCase.workbookData
+			}
+			if !reflect.DeepEqual(actualWorkbookData, testCase.expectedWorkbookData) {
+				t.Log("expected: \n")
+				t.Logf("%s\n", testCase.expectedWorkbookData)
+				t.Log("\n")
+				t.Log("result: \n")
+				t.Logf("%s\n", actualWorkbookData)
+				t.Log("\n")
+				t.Fatal("Expected workbook data to be equal")
+			}
+		})
 	}
 }
 
 // Ensures that the cell type of all cells in each column across all sheets matches the provided header types
 // in each corresponding sheet
-func verifyCellTypesInColumnMatchHeaderType(t *C, workbookCellTypes [][][]CellType, headerMetadata [][]*CellMetadata, workbookData [][][]string) {
+func verifyCellTypesInColumnMatchHeaderType(t *testing.T, workbookCellTypes [][][]CellType, headerMetadata [][]*StreamingCellMetadata, workbookData [][][]string) {
 
 	numSheets := len(workbookCellTypes)
 	numHeaders := len(headerMetadata)
@@ -576,7 +624,7 @@ func verifyCellTypesInColumnMatchHeaderType(t *C, workbookCellTypes [][][]CellTy
 	for sheetI, headers := range headerMetadata {
 		var sanitizedHeaders []CellType
 		for _, header := range headers {
-			if header == (*CellMetadata)(nil) || header.cellType == CellTypeString {
+			if header == (*StreamingCellMetadata)(nil) || header.cellType == CellTypeString {
 				sanitizedHeaders = append(sanitizedHeaders, CellTypeInline)
 			} else {
 				sanitizedHeaders = append(sanitizedHeaders, header.cellType)
@@ -603,7 +651,7 @@ func verifyCellTypesInColumnMatchHeaderType(t *C, workbookCellTypes [][][]CellTy
 
 // The purpose of TestXlsxStyleBehavior is to ensure that initMaxStyleId has the correct starting value
 // and that the logic in AddSheet() that predicts Style IDs is correct.
-func (s *StreamSuite) TestXlsxStyleBehavior(t *C) {
+func TestXlsxStyleBehavior(t *testing.T) {
 	file := NewFile()
 	sheet, err := file.AddSheet("Sheet 1")
 	if err != nil {
@@ -620,10 +668,9 @@ func (s *StreamSuite) TestXlsxStyleBehavior(t *C) {
 		t.Fatal("no style sheet")
 	}
 	// Created an XLSX file with only the default style.
-	// We expect that the number of styles is one more than our max index constant.
-	// This means the library adds two styles by default.
-	if !strings.Contains(styleSheet, fmt.Sprintf(`<cellXfs count="%d">`, initMaxStyleId+1)) {
-		t.Fatal("Expected sheet to have two styles")
+	// This means the library adds a style by default, but no others are created
+	if !strings.Contains(styleSheet, fmt.Sprintf(`<cellXfs count="%d">`, initMaxStyleId)) {
+		t.Fatal("Expected sheet to have one style")
 	}
 
 	file = NewFile()
@@ -636,19 +683,20 @@ func (s *StreamSuite) TestXlsxStyleBehavior(t *C) {
 	if count := row.WriteSlice(&rowData, -1); count != len(rowData) {
 		t.Fatal("not enough cells written")
 	}
-	sheet.Cols[0].SetType(CellTypeString)
-	sheet.Cols[1].SetType(CellTypeString)
-	sheet.Cols[3].SetType(CellTypeNumeric)
-	sheet.Cols[4].SetType(CellTypeString)
+	sheet.SetType(0, 4, CellTypeString)
+	sheet.SetType(3, 3, CellTypeNumeric)
 	parts, err = file.MarshallParts()
 	styleSheet, ok = parts["xl/styles.xml"]
 	if !ok {
 		t.Fatal("no style sheet")
 	}
-	// Created an XLSX file with two distinct cell types, which should create two new styles.
-	// The same cell type was added three times, this should be coalesced into the same style rather than
-	// recreating the style. This XLSX stream library depends on this behavior when predicting the next style id.
-	if !strings.Contains(styleSheet, fmt.Sprintf(`<cellXfs count="%d">`, initMaxStyleId+1+2)) {
+	// Created an XLSX file with two distinct cell types, which
+	// should create two new styles.  The same cell type was added
+	// three times, this should be coalesced into the same style
+	// rather than recreating the style. This XLSX stream library
+	// depends on this behaviour when predicting the next style
+	// id.
+	if !strings.Contains(styleSheet, fmt.Sprintf(`<cellXfs count="%d">`, initMaxStyleId+2)) {
 		t.Fatal("Expected sheet to have four styles")
 	}
 }
@@ -666,12 +714,11 @@ func writeStreamFile(filePath string, fileBuffer io.Writer, sheetNames []string,
 		file = NewStreamFileBuilder(fileBuffer)
 	}
 	for i, sheetName := range sheetNames {
-		header := workbookData[i][0]
 		var sheetHeaderTypes []*CellType
 		if i < len(headerTypes) {
 			sheetHeaderTypes = headerTypes[i]
 		}
-		err := file.AddSheet(sheetName, header, sheetHeaderTypes)
+		err := file.AddSheet(sheetName, sheetHeaderTypes)
 		if err != nil {
 			return err
 		}
@@ -687,10 +734,7 @@ func writeStreamFile(filePath string, fileBuffer io.Writer, sheetNames []string,
 				return err
 			}
 		}
-		for i, row := range sheetData {
-			if i == 0 {
-				continue
-			}
+		for _, row := range sheetData {
 			err = streamFile.Write(row)
 			if err != nil {
 				return err
@@ -705,7 +749,7 @@ func writeStreamFile(filePath string, fileBuffer io.Writer, sheetNames []string,
 }
 
 // writeStreamFileWithDefaultMetadata is the same thing as writeStreamFile but with headerMetadata instead of headerTypes
-func writeStreamFileWithDefaultMetadata(filePath string, fileBuffer io.Writer, sheetNames []string, workbookData [][][]string, headerMetadata [][]*CellMetadata, shouldMakeRealFiles bool) error {
+func writeStreamFileWithDefaultMetadata(filePath string, fileBuffer io.Writer, sheetNames []string, workbookData [][][]string, headerMetadata [][]*StreamingCellMetadata, shouldMakeRealFiles bool) error {
 	var file *StreamFileBuilder
 	var err error
 	if shouldMakeRealFiles {
@@ -716,13 +760,13 @@ func writeStreamFileWithDefaultMetadata(filePath string, fileBuffer io.Writer, s
 	} else {
 		file = NewStreamFileBuilder(fileBuffer)
 	}
+
 	for i, sheetName := range sheetNames {
-		header := workbookData[i][0]
-		var sheetHeaderTypes []*CellMetadata
+		var sheetHeaderTypes []*StreamingCellMetadata
 		if i < len(headerMetadata) {
 			sheetHeaderTypes = headerMetadata[i]
 		}
-		err := file.AddSheetWithDefaultColumnMetadata(sheetName, header, sheetHeaderTypes)
+		err := file.AddSheetWithDefaultColumnMetadata(sheetName, sheetHeaderTypes)
 		if err != nil {
 			return err
 		}
@@ -738,10 +782,8 @@ func writeStreamFileWithDefaultMetadata(filePath string, fileBuffer io.Writer, s
 				return err
 			}
 		}
-		for i, row := range sheetData {
-			if i == 0 {
-				continue
-			}
+
+		for _, row := range sheetData {
 			err = streamFile.WriteWithColumnDefaultMetadata(row)
 			if err != nil {
 				return err
@@ -756,7 +798,7 @@ func writeStreamFileWithDefaultMetadata(filePath string, fileBuffer io.Writer, s
 }
 
 // readXLSXFile will read the file using the xlsx package.
-func readXLSXFile(t *C, filePath string, fileBuffer io.ReaderAt, size int64, shouldMakeRealFiles bool) ([]string, [][][]string, [][][]CellType) {
+func readXLSXFile(t *testing.T, filePath string, fileBuffer io.ReaderAt, size int64, shouldMakeRealFiles bool) ([]string, [][][]string, [][][]CellType) {
 	var readFile *File
 	var err error
 	if shouldMakeRealFiles {
@@ -800,11 +842,11 @@ func readXLSXFile(t *C, filePath string, fileBuffer io.ReaderAt, size int64, sho
 func (s *StreamSuite) TestAddSheetErrorsAfterBuild(t *C) {
 	file := NewStreamFileBuilder(bytes.NewBuffer(nil))
 
-	err := file.AddSheet("Sheet1", []string{"Header"}, nil)
+	err := file.AddSheet("Sheet1", nil)
 	if err != nil {
 		t.Fatal(err)
 	}
-	err = file.AddSheet("Sheet2", []string{"Header2"}, nil)
+	err = file.AddSheet("Sheet2", nil)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -813,7 +855,7 @@ func (s *StreamSuite) TestAddSheetErrorsAfterBuild(t *C) {
 	if err != nil {
 		t.Fatal(err)
 	}
-	err = file.AddSheet("Sheet3", []string{"Header3"}, nil)
+	err = file.AddSheet("Sheet3", nil)
 	if err != BuiltStreamFileBuilderError {
 		t.Fatal(err)
 	}
@@ -822,11 +864,11 @@ func (s *StreamSuite) TestAddSheetErrorsAfterBuild(t *C) {
 func (s *StreamSuite) TestBuildErrorsAfterBuild(t *C) {
 	file := NewStreamFileBuilder(bytes.NewBuffer(nil))
 
-	err := file.AddSheet("Sheet1", []string{"Header"}, nil)
+	err := file.AddSheet("Sheet1", nil)
 	if err != nil {
 		t.Fatal(err)
 	}
-	err = file.AddSheet("Sheet2", []string{"Header2"}, nil)
+	err = file.AddSheet("Sheet2", nil)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -841,20 +883,17 @@ func (s *StreamSuite) TestBuildErrorsAfterBuild(t *C) {
 	}
 }
 
-func (s *StreamSuite) TestCloseWithNothingWrittenToSheets(t *C) {
+func TestCloseWithNothingWrittenToSheets(t *testing.T) {
 	buffer := bytes.NewBuffer(nil)
 	file := NewStreamFileBuilder(buffer)
 
 	sheetNames := []string{"Sheet1", "Sheet2"}
-	workbookData := [][][]string{
-		{{"Header1", "Header2"}},
-		{{"Header3", "Header4"}},
-	}
-	err := file.AddSheet(sheetNames[0], workbookData[0][0], nil)
+	expectedWorkbookData := [][][]string{{}, {}}
+	err := file.AddSheet(sheetNames[0], nil)
 	if err != nil {
 		t.Fatal(err)
 	}
-	err = file.AddSheet(sheetNames[1], workbookData[1][0], nil)
+	err = file.AddSheet(sheetNames[1], nil)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -875,7 +914,9 @@ func (s *StreamSuite) TestCloseWithNothingWrittenToSheets(t *C) {
 	if !reflect.DeepEqual(actualSheetNames, sheetNames) {
 		t.Fatal("Expected sheet names to be equal")
 	}
-	if !reflect.DeepEqual(actualWorkbookData, workbookData) {
+	if !reflect.DeepEqual(actualWorkbookData, expectedWorkbookData) {
+		t.Logf("Expected:\n%s\n\n", expectedWorkbookData)
+		t.Logf("Actual:\n%s\n\n", actualWorkbookData)
 		t.Fatal("Expected workbook data to be equal")
 	}
 }

+ 1 - 1
write.go

@@ -42,7 +42,7 @@ func (r *Row) WriteSlice(e interface{}, cols int) int {
 		case fmt.Stringer: // check Stringer first
 			cell := r.AddCell()
 			cell.SetString(t.String())
-		case sql.NullString:  // check null sql types nulls = ''
+		case sql.NullString: // check null sql types nulls = ''
 			cell := r.AddCell()
 			if cell.SetString(``); t.Valid {
 				cell.SetValue(t.String)

+ 12 - 8
xmlStyle.go

@@ -103,7 +103,7 @@ type xlsxStyleSheet struct {
 
 	theme *theme
 
-	sync.RWMutex // protects the following
+	sync.RWMutex      // protects the following
 	styleCache        map[int]*Style
 	numFmtRefTable    map[int]xlsxNumFmt
 	parsedNumFmtTable map[string]*parsedNumberFormat
@@ -130,9 +130,10 @@ func (styles *xlsxStyleSheet) reset() {
 			Bottom: xlsxLine{Style: "none"},
 		})
 
-	styles.CellStyleXfs = &xlsxCellStyleXfs{}
+	// add 0th CellStyleXf by default, as required by the standard
+	styles.CellStyleXfs = &xlsxCellStyleXfs{Count: 1, Xf: []xlsxXf{{}}}
 
-	// add default xf
+	// add 0th CellXf by default, as required by the standard
 	styles.CellXfs = xlsxCellXfs{Count: 1, Xf: []xlsxXf{{}}}
 	styles.NumFmts = xlsxNumFmts{}
 }
@@ -150,10 +151,9 @@ func (styles *xlsxStyleSheet) getStyle(styleIndex int) *Style {
 	var namedStyleXf xlsxXf
 
 	xfCount := styles.CellXfs.Count
-	if styleIndex > -1 && xfCount > 0 && styleIndex <= xfCount {
+	if styleIndex > -1 && xfCount > 0 && styleIndex < xfCount {
 		xf := styles.CellXfs.Xf[styleIndex]
-
-		if xf.XfId != nil && styles.CellStyleXfs != nil {
+		if xf.XfId != nil && styles.CellStyleXfs != nil && *xf.XfId < len(styles.CellStyleXfs.Xf) {
 			namedStyleXf = styles.CellStyleXfs.Xf[*xf.XfId]
 			style.NamedStyleIndex = xf.XfId
 		} else {
@@ -231,13 +231,17 @@ func (styles *xlsxStyleSheet) argbValue(color xlsxColor) string {
 // have an id less than 164. This is a possibly incomplete list comprised of as
 // many of them as I could find.
 func getBuiltinNumberFormat(numFmtId int) string {
-	return builtInNumFmt[numFmtId]
+	nmfmt, ok := builtInNumFmt[numFmtId]
+	if !ok {
+		return ""
+	}
+	return nmfmt
 }
 
 func (styles *xlsxStyleSheet) getNumberFormat(styleIndex int) (string, *parsedNumberFormat) {
 	var numberFormat string = "general"
 	if styles.CellXfs.Xf != nil {
-		if styleIndex > -1 && styleIndex <= styles.CellXfs.Count {
+		if styleIndex > -1 && styleIndex < styles.CellXfs.Count {
 			xf := styles.CellXfs.Xf[styleIndex]
 			if builtin := getBuiltinNumberFormat(xf.NumFmtId); builtin != "" {
 				numberFormat = builtin

+ 23 - 26
xmlWorksheet.go

@@ -10,20 +10,20 @@ import (
 // currently I have not checked it for completeness - it does as much
 // as I need.
 type xlsxWorksheet struct {
-	XMLName         xml.Name                 `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main worksheet"`
-	SheetPr         xlsxSheetPr              `xml:"sheetPr"`
-	Dimension       xlsxDimension            `xml:"dimension"`
-	SheetViews      xlsxSheetViews           `xml:"sheetViews"`
-	SheetFormatPr   xlsxSheetFormatPr        `xml:"sheetFormatPr"`
-	Cols            *xlsxCols                `xml:"cols,omitempty"`
-	SheetData       xlsxSheetData            `xml:"sheetData"`
-	DataValidations *xlsxCellDataValidations `xml:"dataValidations"`
-	AutoFilter      *xlsxAutoFilter          `xml:"autoFilter,omitempty"`
-	MergeCells      *xlsxMergeCells          `xml:"mergeCells,omitempty"`
-	PrintOptions    xlsxPrintOptions         `xml:"printOptions"`
-	PageMargins     xlsxPageMargins          `xml:"pageMargins"`
-	PageSetUp       xlsxPageSetUp            `xml:"pageSetup"`
-	HeaderFooter    xlsxHeaderFooter         `xml:"headerFooter"`
+	XMLName         xml.Name             `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main worksheet"`
+	SheetPr         xlsxSheetPr          `xml:"sheetPr"`
+	Dimension       xlsxDimension        `xml:"dimension"`
+	SheetViews      xlsxSheetViews       `xml:"sheetViews"`
+	SheetFormatPr   xlsxSheetFormatPr    `xml:"sheetFormatPr"`
+	Cols            *xlsxCols            `xml:"cols,omitempty"`
+	SheetData       xlsxSheetData        `xml:"sheetData"`
+	DataValidations *xlsxDataValidations `xml:"dataValidations"`
+	AutoFilter      *xlsxAutoFilter      `xml:"autoFilter,omitempty"`
+	MergeCells      *xlsxMergeCells      `xml:"mergeCells,omitempty"`
+	PrintOptions    xlsxPrintOptions     `xml:"printOptions"`
+	PageMargins     xlsxPageMargins      `xml:"pageMargins"`
+	PageSetUp       xlsxPageSetUp        `xml:"pageSetup"`
+	HeaderFooter    xlsxHeaderFooter     `xml:"headerFooter"`
 }
 
 // xlsxHeaderFooter directly maps the headerFooter element in the namespace
@@ -205,6 +205,8 @@ type xlsxCol struct {
 	Width        float64 `xml:"width,attr"`
 	CustomWidth  bool    `xml:"customWidth,attr,omitempty"`
 	OutlineLevel uint8   `xml:"outlineLevel,attr,omitempty"`
+	BestFit      bool    `xml:"bestFit,attr,omitempty"`
+	Phonetic     bool    `xml:"phonetic,attr,omitempty"`
 }
 
 // xlsxDimension directly maps the dimension element in the namespace
@@ -224,16 +226,16 @@ type xlsxSheetData struct {
 	Row     []xlsxRow `xml:"row"`
 }
 
-// xlsxCellDataValidations  excel cell data validation
-type xlsxCellDataValidations struct {
-	DataValidation []*xlsxCellDataValidation `xml:"dataValidation"`
-	Count          int                       `xml:"count,attr"`
+// xlsxDataValidations  excel cell data validation
+type xlsxDataValidations struct {
+	DataValidation []*xlsxDataValidation `xml:"dataValidation"`
+	Count          int                   `xml:"count,attr"`
 }
 
-// xlsxCellDataValidation
+// xlsxDataValidation
 // 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 {
+type xlsxDataValidation struct {
 	// 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"`
@@ -272,11 +274,6 @@ type xlsxCellDataValidation struct {
 	// 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 and maxRow are zero indexed
-	minRow int //`xml:"-"`
-	maxRow int //`xml:"-"`
-	//minCol         int     `xml:"-"` //spare
-	//maxCol         int     `xml:"-"` //spare
 }
 
 // xlsxRow directly maps the row element in the namespace
@@ -302,7 +299,7 @@ type xlsxMergeCell struct {
 }
 
 type xlsxMergeCells struct {
-	XMLName xml.Name //`xml:"mergeCells,omitempty"`
+	XMLName xml.Name        //`xml:"mergeCells,omitempty"`
 	Count   int             `xml:"count,attr,omitempty"`
 	Cells   []xlsxMergeCell `xml:"mergeCell,omitempty"`
 }

Some files were not shown because too many files changed in this diff