From d6bcf72c3930404f563736524dc4dec18638c369 Mon Sep 17 00:00:00 2001 From: "Geoffrey J. Teale" Date: Sat, 28 Sep 2019 01:43:27 +0200 Subject: [PATCH 01/13] Initial test case for sequential cols --- col.go | 42 ++++++++++++++++++++++++++++++++++++++++++ col_test.go | 28 ++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/col.go b/col.go index a249405d..cefa900f 100644 --- a/col.go +++ b/col.go @@ -119,3 +119,45 @@ func (c *Col) GetStreamStyle() StreamStyle { // returning 0 which maps to formatCode "general" return StreamStyle{builtInNumFmtInv[c.numFmt], c.style} } + +type colStoreNode struct { + Col *Col + Prev *colStoreNode + Next *colStoreNode +} + +func (csn *colStoreNode) placeNode(node *colStoreNode) error { + switch { + case csn.Col.Max < node.Col.Min: + if csn.Next == nil { + csn.Next = node + return nil + } + } + + if node.Col.Min <= csn.Col.Min { + if csn.Prev == nil { + csn.Prev = node + } else { + err := csn.Prev.placeNode(node) + if err != nil { + return err + } + } + } + return nil +} + +type ColStore struct { + Root *colStoreNode +} + +// +func (cs *ColStore) Add(col *Col) error { + newNode := &colStoreNode{Col: col} + if cs.Root == nil { + cs.Root = newNode + return nil + } + return cs.Root.placeNode(newNode) +} diff --git a/col_test.go b/col_test.go index 60c28683..769a43c0 100644 --- a/col_test.go +++ b/col_test.go @@ -1 +1,29 @@ package xlsx + +import ( + . "gopkg.in/check.v1" +) + +type ColStoreSuite struct{} + +var _ = Suite(&ColStoreSuite{}) + +func (css *ColStoreSuite) TestAddOneNode(c *C) { + col := &Col{Min: 0, Max: 1} + cs := ColStore{} + err := cs.Add(col) + c.Assert(err, IsNil) + c.Assert(cs.Root.Col, Equals, col) +} + +func (css *ColStoreSuite) TestAddTwoNonOverlappingSequentialNodes(c *C) { + col1 := &Col{Min: 0, Max: 1} + col2 := &Col{Min: 2, Max: 4} + cs := ColStore{} + err := cs.Add(col1) + c.Assert(err, IsNil) + err = cs.Add(col2) + c.Assert(err, IsNil) + c.Assert(cs.Root.Col, Equals, col1) + c.Assert(cs.Root.Next.Col, Equals, col2) +} From 66648b2a4027648caca9abd28c35e1ffd7e38090 Mon Sep 17 00:00:00 2001 From: "Geoffrey J. Teale" Date: Sat, 28 Sep 2019 03:49:18 +0200 Subject: [PATCH 02/13] 1 node and 2 node test cases pass --- col.go | 117 +++++++++++++++++++++++++++++++------ col_test.go | 162 +++++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 248 insertions(+), 31 deletions(-) diff --git a/col.go b/col.go index cefa900f..4e648fc8 100644 --- a/col.go +++ b/col.go @@ -120,32 +120,112 @@ func (c *Col) GetStreamStyle() StreamStyle { return StreamStyle{builtInNumFmtInv[c.numFmt], c.style} } +// 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 ColStore. +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, + numFmt: c.numFmt, + parsedNumFmt: c.parsedNumFmt, + style: c.style, + DataValidation: append([]*xlsxCellDataValidation{}, c.DataValidation...), + defaultCellType: c.defaultCellType, + } +} + type colStoreNode struct { Col *Col Prev *colStoreNode Next *colStoreNode } -func (csn *colStoreNode) placeNode(node *colStoreNode) error { +// 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 (csn *colStoreNode) makeWay(node *colStoreNode) { switch { case csn.Col.Max < node.Col.Min: - if csn.Next == nil { - csn.Next = node - return nil + // The new node starts after this one ends, there's no overlap + // + // Node1 |----| + // Node2 |----| + if csn.Next != nil { + csn.Next.makeWay(node) + return } - } - - if node.Col.Min <= csn.Col.Min { - if csn.Prev == nil { - csn.Prev = node - } else { - err := csn.Prev.placeNode(node) - if err != nil { - return err - } + csn.Next = node + node.Prev = csn + return + + case csn.Col.Min > node.Col.Max: + // The new node ends before this one begins, there's no overlap + // + // Node1 |-----| + // Node2 |----| + if csn.Prev != nil { + csn.Prev.makeWay(node) + return + } + csn.Prev = node + node.Next = csn + return + + case csn.Col.Min < node.Col.Min && csn.Col.Max > node.Col.Max: + // The new node bisects this one: + // + // Node1 |---xx---| + // Node2 |--| + newCol := csn.Col.copyToRange(node.Col.Max+1, csn.Col.Max) + newNode := &colStoreNode{Col: newCol, Prev: node, Next: csn.Next} + csn.Col.Max = node.Col.Min - 1 + csn.Next = node + node.Prev = csn + node.Next = newNode + return + + case csn.Col.Max >= node.Col.Min && csn.Col.Min < node.Col.Min: + // The new node overlaps this one at some point above it's minimum: + // + // Node1 |----xx| + // Node2 |-------| + csn.Col.Max = node.Col.Min - 1 + if csn.Next != nil { + // Break the link to this node, which prevents + // us looping back and forth forever + csn.Next.Prev = nil + csn.Next.makeWay(node) + } + csn.Next = node + node.Prev = csn + return + + case csn.Col.Min <= node.Col.Max && csn.Col.Min > node.Col.Min: + // The new node overlaps this one at some point below it's maximum: + // + // Node1: |------| + // Node2: |----xx| + csn.Col.Min = node.Col.Max + 1 + if csn.Prev != nil { + // Break the link to this node, which prevents + // us looping back and forth forever + csn.Prev.Next = nil + csn.Prev.makeWay(node) } + csn.Prev = node + node.Next = csn + return } - return nil + return } type ColStore struct { @@ -153,11 +233,12 @@ type ColStore struct { } // -func (cs *ColStore) Add(col *Col) error { +func (cs *ColStore) Add(col *Col) { newNode := &colStoreNode{Col: col} if cs.Root == nil { cs.Root = newNode - return nil + return } - return cs.Root.placeNode(newNode) + cs.Root.makeWay(newNode) + return } diff --git a/col_test.go b/col_test.go index 769a43c0..44ce17cf 100644 --- a/col_test.go +++ b/col_test.go @@ -4,26 +4,162 @@ import ( . "gopkg.in/check.v1" ) +type ColSuite struct{} + +var _ = Suite(&ColSuite{}) + +func (cs *ColSuite) TestCopyToRange(c *C) { + nf := &parsedNumberFormat{} + s := &Style{} + cdv1 := &xlsxCellDataValidation{} + cdv2 := &xlsxCellDataValidation{} + ct := CellTypeBool.Ptr() + c1 := &Col{ + Min: 0, + Max: 10, + Hidden: true, + Width: 300.4, + Collapsed: true, + OutlineLevel: 2, + numFmt: "-0.00", + parsedNumFmt: nf, + style: s, + DataValidation: []*xlsxCellDataValidation{cdv1, cdv2}, + defaultCellType: ct, + } + + c2 := c1.copyToRange(4, 10) + c.Assert(c2.Min, Equals, 4) + c.Assert(c2.Max, Equals, 10) + c.Assert(c2.Hidden, Equals, c1.Hidden) + c.Assert(c2.Width, Equals, c1.Width) + c.Assert(c2.Collapsed, Equals, c1.Collapsed) + c.Assert(c2.OutlineLevel, Equals, c1.OutlineLevel) + c.Assert(c2.numFmt, Equals, c1.numFmt) + c.Assert(c2.parsedNumFmt, Equals, c1.parsedNumFmt) + c.Assert(c2.style, Equals, c1.style) + c.Assert(c2.DataValidation, HasLen, 2) + c.Assert(c2.DataValidation[0], Equals, c1.DataValidation[0]) + c.Assert(c2.DataValidation[1], Equals, c1.DataValidation[1]) + c.Assert(c2.defaultCellType, Equals, c1.defaultCellType) +} + type ColStoreSuite struct{} var _ = Suite(&ColStoreSuite{}) -func (css *ColStoreSuite) TestAddOneNode(c *C) { +func (css *ColStoreSuite) TestAddRootNode(c *C) { col := &Col{Min: 0, Max: 1} cs := ColStore{} - err := cs.Add(col) - c.Assert(err, IsNil) + cs.Add(col) c.Assert(cs.Root.Col, Equals, col) } -func (css *ColStoreSuite) TestAddTwoNonOverlappingSequentialNodes(c *C) { - col1 := &Col{Min: 0, Max: 1} - col2 := &Col{Min: 2, Max: 4} - cs := ColStore{} - err := cs.Add(col1) - c.Assert(err, IsNil) - err = cs.Add(col2) - c.Assert(err, IsNil) - c.Assert(cs.Root.Col, Equals, col1) - c.Assert(cs.Root.Next.Col, Equals, col2) +func (css *ColStoreSuite) TestMakeWay(c *C) { + assertWayMade := func(cols []*Col, chainFunc func(root *colStoreNode)) { + + cs := ColStore{} + for _, col := range cols { + cs.Add(col) + } + chainFunc(cs.Root) + } + + // Col1: |--| + // Col2: |--| + assertWayMade([]*Col{&Col{Min: 0, Max: 1}, &Col{Min: 2, Max: 3}}, + func(root *colStoreNode) { + c.Assert(root.Col.Min, Equals, 0) + c.Assert(root.Col.Max, Equals, 1) + c.Assert(root.Prev, IsNil) + c.Assert(root.Next, NotNil) + node2 := root.Next + c.Assert(node2.Prev, Equals, root) + c.Assert(node2.Next, IsNil) + c.Assert(node2.Col.Min, Equals, 2) + c.Assert(node2.Col.Max, Equals, 3) + }) + + // Col1: |--| + // Col2: |--| + assertWayMade([]*Col{&Col{Min: 2, Max: 3}, &Col{Min: 0, Max: 1}}, + func(root *colStoreNode) { + c.Assert(root.Col.Min, Equals, 2) + c.Assert(root.Col.Max, Equals, 3) + c.Assert(root.Prev, NotNil) + c.Assert(root.Next, IsNil) + node2 := root.Prev + c.Assert(node2.Next, Equals, root) + c.Assert(node2.Prev, IsNil) + c.Assert(node2.Col.Min, Equals, 0) + c.Assert(node2.Col.Max, Equals, 1) + }) + + // Col1: |--x| + // Col2: |--| + assertWayMade([]*Col{&Col{Min: 0, Max: 2}, &Col{Min: 2, Max: 3}}, + func(root *colStoreNode) { + c.Assert(root.Col.Min, Equals, 0) + c.Assert(root.Col.Max, Equals, 1) + c.Assert(root.Prev, IsNil) + c.Assert(root.Next, NotNil) + node2 := root.Next + c.Assert(node2.Prev, Equals, root) + c.Assert(node2.Next, IsNil) + c.Assert(node2.Col.Min, Equals, 2) + c.Assert(node2.Col.Max, Equals, 3) + }) + + // Col1: |x-| + // Col2: |--| + assertWayMade([]*Col{&Col{Min: 1, Max: 2}, &Col{Min: 0, Max: 1}}, + func(root *colStoreNode) { + c.Assert(root.Col.Min, Equals, 2) + c.Assert(root.Col.Max, Equals, 2) + c.Assert(root.Prev, NotNil) + c.Assert(root.Next, IsNil) + node2 := root.Prev + c.Assert(node2.Next, Equals, root) + c.Assert(node2.Prev, IsNil) + c.Assert(node2.Col.Min, Equals, 0) + c.Assert(node2.Col.Max, Equals, 1) + }) + + // Col1: |---xx---| + // Col2: |--| + assertWayMade([]*Col{&Col{Min: 0, Max: 7}, &Col{Min: 3, Max: 4}}, + func(root *colStoreNode) { + c.Assert(root.Prev, IsNil) + c.Assert(root.Next, NotNil) + node2 := root.Next + c.Assert(node2.Prev, Equals, root) + c.Assert(node2.Col.Min, Equals, 3) + c.Assert(node2.Col.Max, Equals, 4) + c.Assert(node2.Next, NotNil) + node3 := node2.Next + c.Assert(node3.Prev, Equals, node2) + c.Assert(node3.Next, IsNil) + c.Assert(node3.Col.Min, Equals, 5) + c.Assert(node3.Col.Max, Equals, 7) + }) + + // Col1: |--| + // Col2: |--| + // Col3: | + assertWayMade([]*Col{&Col{Min: 0, Max: 7}, &Col{Min: 3, Max: 4}}, + func(root *colStoreNode) { + c.Assert(root.Prev, IsNil) + c.Assert(root.Next, NotNil) + node2 := root.Next + c.Assert(node2.Prev, Equals, root) + c.Assert(node2.Col.Min, Equals, 3) + c.Assert(node2.Col.Max, Equals, 4) + c.Assert(node2.Next, NotNil) + node3 := node2.Next + c.Assert(node3.Prev, Equals, node2) + c.Assert(node3.Next, IsNil) + c.Assert(node3.Col.Min, Equals, 5) + c.Assert(node3.Col.Max, Equals, 7) + }) + } From a7f296edfdfd33d648e00943f7195d564ccb539f Mon Sep 17 00:00:00 2001 From: "Geoffrey J. Teale" Date: Sat, 28 Sep 2019 04:17:15 +0200 Subject: [PATCH 03/13] Handle exact duplicate Col ranges --- col.go | 126 ++++++++++++++++++++++++++++++---------------------- col_test.go | 42 +++++++++++------- 2 files changed, 101 insertions(+), 67 deletions(-) diff --git a/col.go b/col.go index 4e648fc8..93ab7480 100644 --- a/col.go +++ b/col.go @@ -146,99 +146,121 @@ type colStoreNode struct { Next *colStoreNode } +// 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 +} + +// 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) { + newNode := &colStoreNode{Col: col} + if cs.Root == nil { + cs.Root = newNode + return + } + cs.makeWay(cs.Root, newNode) + return +} + // 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 (csn *colStoreNode) makeWay(node *colStoreNode) { +func (cs *ColStore) makeWay(node1, node2 *colStoreNode) { switch { - case csn.Col.Max < node.Col.Min: - // The new node starts after this one ends, there's no overlap + case node1.Col.Max < node2.Col.Min: + // The new node2 starts after this one ends, there's no overlap // // Node1 |----| // Node2 |----| - if csn.Next != nil { - csn.Next.makeWay(node) + if node1.Next != nil { + cs.makeWay(node1.Next, node2) return } - csn.Next = node - node.Prev = csn + node1.Next = node2 + node2.Prev = node1 return - case csn.Col.Min > node.Col.Max: - // The new node ends before this one begins, there's no overlap + case node1.Col.Min > node2.Col.Max: + // The new node2 ends before this one begins, there's no overlap // // Node1 |-----| // Node2 |----| - if csn.Prev != nil { - csn.Prev.makeWay(node) + if node1.Prev != nil { + cs.makeWay(node1.Prev, node2) return } - csn.Prev = node - node.Next = csn + node1.Prev = node2 + node2.Next = node1 return - case csn.Col.Min < node.Col.Min && csn.Col.Max > node.Col.Max: - // The new node bisects this one: + case node1.Col.Min == node2.Col.Min && node1.Col.Max == node2.Col.Max: + // Exact match + // + // Node1 |xxx| + // Node2 |---| + if node1.Prev != nil { + node1.Prev.Next = node2 + node2.Prev = node1.Prev + node1.Prev = nil + } + if node1.Next != nil { + node1.Next.Prev = node2 + node2.Next = node1.Next + node1.Next = nil + } + if cs.Root == node1 { + cs.Root = node2 + } + + case node1.Col.Min < node2.Col.Min && node1.Col.Max > node2.Col.Max: + // The new node2 bisects this one: // // Node1 |---xx---| // Node2 |--| - newCol := csn.Col.copyToRange(node.Col.Max+1, csn.Col.Max) - newNode := &colStoreNode{Col: newCol, Prev: node, Next: csn.Next} - csn.Col.Max = node.Col.Min - 1 - csn.Next = node - node.Prev = csn - node.Next = newNode + newCol := node1.Col.copyToRange(node2.Col.Max+1, node1.Col.Max) + newNode := &colStoreNode{Col: newCol, Prev: node2, Next: node1.Next} + node1.Col.Max = node2.Col.Min - 1 + node1.Next = node2 + node2.Prev = node1 + node2.Next = newNode return - case csn.Col.Max >= node.Col.Min && csn.Col.Min < node.Col.Min: - // The new node overlaps this one at some point above it's minimum: + case node1.Col.Max >= node2.Col.Min && node1.Col.Min < node2.Col.Min: + // The new node2 overlaps this one at some point above it's minimum: // // Node1 |----xx| // Node2 |-------| - csn.Col.Max = node.Col.Min - 1 - if csn.Next != nil { + node1.Col.Max = node2.Col.Min - 1 + if node1.Next != nil { // Break the link to this node, which prevents // us looping back and forth forever - csn.Next.Prev = nil - csn.Next.makeWay(node) + node1.Next.Prev = nil + cs.makeWay(node1.Next, node2) } - csn.Next = node - node.Prev = csn + node1.Next = node2 + node2.Prev = node1 return - case csn.Col.Min <= node.Col.Max && csn.Col.Min > node.Col.Min: - // The new node overlaps this one at some point below it's maximum: + case node1.Col.Min <= node2.Col.Max && node1.Col.Min > node2.Col.Min: + // The new node2 overlaps this one at some point below it's maximum: // // Node1: |------| // Node2: |----xx| - csn.Col.Min = node.Col.Max + 1 - if csn.Prev != nil { + node1.Col.Min = node2.Col.Max + 1 + if node1.Prev != nil { // Break the link to this node, which prevents // us looping back and forth forever - csn.Prev.Next = nil - csn.Prev.makeWay(node) + node1.Prev.Next = nil + cs.makeWay(node1.Prev, node2) } - csn.Prev = node - node.Next = csn - return - } - return -} - -type ColStore struct { - Root *colStoreNode -} - -// -func (cs *ColStore) Add(col *Col) { - newNode := &colStoreNode{Col: col} - if cs.Root == nil { - cs.Root = newNode + node1.Prev = node2 + node2.Next = node1 return } - cs.Root.makeWay(newNode) return } diff --git a/col_test.go b/col_test.go index 44ce17cf..c98e26f7 100644 --- a/col_test.go +++ b/col_test.go @@ -143,23 +143,35 @@ func (css *ColStoreSuite) TestMakeWay(c *C) { c.Assert(node3.Col.Max, Equals, 7) }) - // Col1: |--| - // Col2: |--| - // Col3: | - assertWayMade([]*Col{&Col{Min: 0, Max: 7}, &Col{Min: 3, Max: 4}}, + // Col1: |xx| + // Col2: |--| + assertWayMade([]*Col{&Col{Min: 0, Max: 1, Width: 40.1}, &Col{Min: 0, Max: 1, Width: 10.0}}, func(root *colStoreNode) { c.Assert(root.Prev, IsNil) - c.Assert(root.Next, NotNil) - node2 := root.Next - c.Assert(node2.Prev, Equals, root) - c.Assert(node2.Col.Min, Equals, 3) - c.Assert(node2.Col.Max, Equals, 4) - c.Assert(node2.Next, NotNil) - node3 := node2.Next - c.Assert(node3.Prev, Equals, node2) - c.Assert(node3.Next, IsNil) - c.Assert(node3.Col.Min, Equals, 5) - c.Assert(node3.Col.Max, Equals, 7) + c.Assert(root.Next, IsNil) + c.Assert(root.Col.Min, Equals, 0) + c.Assert(root.Col.Max, Equals, 1) + // This is how we establish we have the new node, and not the old one + c.Assert(root.Col.Width, Equals, 10.0) }) + // Col1: |--| + // Col2: |--| + // Col3: | + // assertWayMade([]*Col{&Col{Min: 0, Max: 7}, &Col{Min: 3, Max: 4}}, + // func(root *colStoreNode) { + // c.Assert(root.Prev, IsNil) + // c.Assert(root.Next, NotNil) + // node2 := root.Next + // c.Assert(node2.Prev, Equals, root) + // c.Assert(node2.Col.Min, Equals, 3) + // c.Assert(node2.Col.Max, Equals, 4) + // c.Assert(node2.Next, NotNil) + // node3 := node2.Next + // c.Assert(node3.Prev, Equals, node2) + // c.Assert(node3.Next, IsNil) + // c.Assert(node3.Col.Min, Equals, 5) + // c.Assert(node3.Col.Max, Equals, 7) + // }) + } From df7d0272b54d79e7d663c311f49ab745df4908a5 Mon Sep 17 00:00:00 2001 From: "Geoffrey J. Teale" Date: Sat, 28 Sep 2019 04:54:18 +0200 Subject: [PATCH 04/13] Test 2 node envelope case --- col.go | 34 +++++++++++++++++++++++++++++----- col_test.go | 12 ++++++++++++ 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/col.go b/col.go index 93ab7480..9533aa12 100644 --- a/col.go +++ b/col.go @@ -173,7 +173,7 @@ func (cs *ColStore) Add(col *Col) { func (cs *ColStore) makeWay(node1, node2 *colStoreNode) { switch { case node1.Col.Max < node2.Col.Min: - // The new node2 starts after this one ends, there's no overlap + // The node2 starts after node1 ends, there's no overlap // // Node1 |----| // Node2 |----| @@ -186,7 +186,7 @@ func (cs *ColStore) makeWay(node1, node2 *colStoreNode) { return case node1.Col.Min > node2.Col.Max: - // The new node2 ends before this one begins, there's no overlap + // Node2 ends before node1 begins, there's no overlap // // Node1 |-----| // Node2 |----| @@ -216,9 +216,33 @@ func (cs *ColStore) makeWay(node1, node2 *colStoreNode) { if cs.Root == node1 { cs.Root = node2 } + case node1.Col.Min > node2.Col.Min && node1.Col.Max < node2.Col.Max: + // Node2 envelopes node1 + // + // Node1 |xx| + // Node2 |----| + if cs.Root == node1 { + cs.Root = node2 + } + if node1.Prev != nil { + node1.Prev.Next = nil + if node1.Next != nil { + node1.Prev.Next = node1.Next + node1.Next = nil + } + cs.makeWay(node1.Prev, node2) + node1.Prev = nil + return + } + if node1.Next != nil { + node1.Next.Prev = nil + cs.makeWay(node1.Next, node2) + node1.Next = nil + return + } case node1.Col.Min < node2.Col.Min && node1.Col.Max > node2.Col.Max: - // The new node2 bisects this one: + // Node2 bisects node1: // // Node1 |---xx---| // Node2 |--| @@ -231,7 +255,7 @@ func (cs *ColStore) makeWay(node1, node2 *colStoreNode) { return case node1.Col.Max >= node2.Col.Min && node1.Col.Min < node2.Col.Min: - // The new node2 overlaps this one at some point above it's minimum: + // Node2 overlaps node1 at some point above it's minimum: // // Node1 |----xx| // Node2 |-------| @@ -247,7 +271,7 @@ func (cs *ColStore) makeWay(node1, node2 *colStoreNode) { return case node1.Col.Min <= node2.Col.Max && node1.Col.Min > node2.Col.Min: - // The new node2 overlaps this one at some point below it's maximum: + // Node2 overlaps node1 at some point below it's maximum: // // Node1: |------| // Node2: |----xx| diff --git a/col_test.go b/col_test.go index c98e26f7..35991150 100644 --- a/col_test.go +++ b/col_test.go @@ -155,6 +155,18 @@ func (css *ColStoreSuite) TestMakeWay(c *C) { c.Assert(root.Col.Width, Equals, 10.0) }) + // Col1: |xx| + // Col2: |----| + assertWayMade([]*Col{&Col{Min: 1, Max: 2, Width: 40.1}, &Col{Min: 0, Max: 3, Width: 10.0}}, + func(root *colStoreNode) { + c.Assert(root.Prev, IsNil) + c.Assert(root.Next, IsNil) + c.Assert(root.Col.Min, Equals, 0) + c.Assert(root.Col.Max, Equals, 3) + // This is how we establish we have the new node, and not the old one + c.Assert(root.Col.Width, Equals, 10.0) + }) + // Col1: |--| // Col2: |--| // Col3: | From da7fcd25e52f4473c3a1b35d5e850e937d23b70e Mon Sep 17 00:00:00 2001 From: "Geoffrey J. Teale" Date: Sat, 28 Sep 2019 22:05:00 +0200 Subject: [PATCH 05/13] first 3 section test passing --- col.go | 133 +++++++++++++++++++++++++++++++++++++++++----------- col_test.go | 105 ++++++++++++++++++++++++++++++++++------- 2 files changed, 195 insertions(+), 43 deletions(-) diff --git a/col.go b/col.go index 9533aa12..9d854ef3 100644 --- a/col.go +++ b/col.go @@ -146,6 +146,33 @@ type colStoreNode struct { 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 @@ -164,6 +191,45 @@ func (cs *ColStore) Add(col *Col) { return } +// +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 +} + // 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 @@ -178,7 +244,12 @@ func (cs *ColStore) makeWay(node1, node2 *colStoreNode) { // Node1 |----| // Node2 |----| if node1.Next != nil { - cs.makeWay(node1.Next, node2) + next := node1.Next + if next.Col.Min >= node2.Col.Min { + node1.Next = node2 + node2.Prev = node1 + } + cs.makeWay(next, node2) return } node1.Next = node2 @@ -191,7 +262,12 @@ func (cs *ColStore) makeWay(node1, node2 *colStoreNode) { // Node1 |-----| // Node2 |----| if node1.Prev != nil { - cs.makeWay(node1.Prev, node2) + prev := node1.Prev + if prev.Col.Max <= node2.Col.Max { + node1.Prev = node2 + node2.Next = node1 + } + cs.makeWay(prev, node2) return } node1.Prev = node2 @@ -203,42 +279,45 @@ func (cs *ColStore) makeWay(node1, node2 *colStoreNode) { // // Node1 |xxx| // Node2 |---| - if node1.Prev != nil { - node1.Prev.Next = node2 - node2.Prev = node1.Prev - node1.Prev = nil + + prev := node1.Prev + next := node1.Next + cs.removeNode(node1) + if prev != nil { + prev.Next = node2 + node2.Prev = prev } - if node1.Next != nil { - node1.Next.Prev = node2 - node2.Next = node1.Next - node1.Next = nil + if next != nil { + next.Prev = node2 + node2.Next = next } - if cs.Root == node1 { + // 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 |----| - if cs.Root == node1 { - cs.Root = node2 + + prev := node1.Prev + next := node1.Next + cs.removeNode(node1) + + if prev != nil { + prev.Next = nil + cs.makeWay(prev, node2) } - if node1.Prev != nil { - node1.Prev.Next = nil - if node1.Next != nil { - node1.Prev.Next = node1.Next - node1.Next = nil - } - cs.makeWay(node1.Prev, node2) - node1.Prev = nil - return + if next != nil { + next.Prev = nil + cs.makeWay(next, node2) } - if node1.Next != nil { - node1.Next.Prev = nil - cs.makeWay(node1.Next, node2) - node1.Next = nil - return + + if cs.Root == nil { + cs.Root = node2 } case node1.Col.Min < node2.Col.Min && node1.Col.Max > node2.Col.Max: diff --git a/col_test.go b/col_test.go index 35991150..7f0ee97e 100644 --- a/col_test.go +++ b/col_test.go @@ -147,6 +147,7 @@ func (css *ColStoreSuite) TestMakeWay(c *C) { // Col2: |--| assertWayMade([]*Col{&Col{Min: 0, Max: 1, Width: 40.1}, &Col{Min: 0, Max: 1, Width: 10.0}}, func(root *colStoreNode) { + c.Assert(root, NotNil) c.Assert(root.Prev, IsNil) c.Assert(root.Next, IsNil) c.Assert(root.Col.Min, Equals, 0) @@ -169,21 +170,93 @@ func (css *ColStoreSuite) TestMakeWay(c *C) { // Col1: |--| // Col2: |--| - // Col3: | - // assertWayMade([]*Col{&Col{Min: 0, Max: 7}, &Col{Min: 3, Max: 4}}, - // func(root *colStoreNode) { - // c.Assert(root.Prev, IsNil) - // c.Assert(root.Next, NotNil) - // node2 := root.Next - // c.Assert(node2.Prev, Equals, root) - // c.Assert(node2.Col.Min, Equals, 3) - // c.Assert(node2.Col.Max, Equals, 4) - // c.Assert(node2.Next, NotNil) - // node3 := node2.Next - // c.Assert(node3.Prev, Equals, node2) - // c.Assert(node3.Next, IsNil) - // c.Assert(node3.Col.Min, Equals, 5) - // c.Assert(node3.Col.Max, Equals, 7) - // }) + // Col3: |--| + assertWayMade([]*Col{&Col{Min: 0, Max: 1}, &Col{Min: 9, Max: 10}, &Col{Min: 4, Max: 5}}, + func(root *colStoreNode) { + c.Assert(root.Prev, IsNil) + c.Assert(root.Next, NotNil) + node2 := root.Next + c.Assert(node2.Prev, Equals, root) + c.Assert(node2.Col.Min, Equals, 4) + c.Assert(node2.Col.Max, Equals, 5) + c.Assert(node2.Next, NotNil) + node3 := node2.Next + c.Assert(node3.Prev, Equals, node2) + c.Assert(node3.Next, IsNil) + c.Assert(node3.Col.Min, Equals, 9) + c.Assert(node3.Col.Max, Equals, 10) + }) + +} + +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: 0, Max: 0} + cs.Add(col0) + col1 := &Col{Min: 1, Max: 1} + cs.Add(col1) + col2 := &Col{Min: 2, Max: 2} + cs.Add(col2) + col3 := &Col{Min: 3, Max: 3} + cs.Add(col3) + col4 := &Col{Min: 4, Max: 4} + cs.Add(col4) + col5 := &Col{Min: 100, Max: 125} + cs.Add(col5) + + assertNodeFound(cs, -1, nil) + assertNodeFound(cs, 0, col0) + assertNodeFound(cs, 1, col1) + assertNodeFound(cs, 2, col2) + assertNodeFound(cs, 3, col3) + assertNodeFound(cs, 4, col4) + assertNodeFound(cs, 5, 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: 0, Max: 0} + cs.Add(col0) + col1 := &Col{Min: 1, Max: 1} + cs.Add(col1) + col2 := &Col{Min: 2, Max: 2} + cs.Add(col2) + col3 := &Col{Min: 3, Max: 3} + cs.Add(col3) + col4 := &Col{Min: 4, Max: 4} + cs.Add(col4) + + cs.removeNode(cs.findNodeForColNum(4)) + assertChain(cs, []*Col{col0, col1, col2, col3}) + cs.removeNode(cs.findNodeForColNum(0)) + assertChain(cs, []*Col{col1, col2, col3}) } From 415119782e2393f6011085c4e41c390b781a2beb Mon Sep 17 00:00:00 2001 From: "Geoffrey J. Teale" Date: Sat, 28 Sep 2019 22:36:55 +0200 Subject: [PATCH 06/13] Further tests cases for ColStore.makeWay --- col_test.go | 168 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 168 insertions(+) diff --git a/col_test.go b/col_test.go index 7f0ee97e..6d1ebc3e 100644 --- a/col_test.go +++ b/col_test.go @@ -168,6 +168,48 @@ func (css *ColStoreSuite) TestMakeWay(c *C) { c.Assert(root.Col.Width, Equals, 10.0) }) + // Col1: |--| + // Col2: |--| + // Col3: |--| + assertWayMade([]*Col{&Col{Min: 0, Max: 1}, &Col{Min: 2, Max: 3}, &Col{Min: 4, Max: 5}}, + func(root *colStoreNode) { + c.Assert(root.Prev, IsNil) + c.Assert(root.Next, NotNil) + c.Assert(root.Col.Min, Equals, 0) + c.Assert(root.Col.Max, Equals, 1) + node2 := root.Next + c.Assert(node2.Prev, Equals, root) + c.Assert(node2.Col.Min, Equals, 2) + c.Assert(node2.Col.Max, Equals, 3) + c.Assert(node2.Next, NotNil) + node3 := node2.Next + c.Assert(node3.Prev, Equals, node2) + c.Assert(node3.Next, IsNil) + c.Assert(node3.Col.Min, Equals, 4) + c.Assert(node3.Col.Max, Equals, 5) + }) + + // Col1: |--| + // Col2: |--| + // Col3: |--| + assertWayMade([]*Col{&Col{Min: 4, Max: 5}, &Col{Min: 2, Max: 3}, &Col{Min: 0, Max: 1}}, + func(root *colStoreNode) { + c.Assert(root.Prev, NotNil) + c.Assert(root.Next, IsNil) + c.Assert(root.Col.Min, Equals, 4) + c.Assert(root.Col.Max, Equals, 5) + node2 := root.Prev + c.Assert(node2.Next, Equals, root) + c.Assert(node2.Col.Min, Equals, 2) + c.Assert(node2.Col.Max, Equals, 3) + c.Assert(node2.Prev, NotNil) + node3 := node2.Prev + c.Assert(node3.Next, Equals, node2) + c.Assert(node3.Prev, IsNil) + c.Assert(node3.Col.Min, Equals, 0) + c.Assert(node3.Col.Max, Equals, 1) + }) + // Col1: |--| // Col2: |--| // Col3: |--| @@ -187,6 +229,132 @@ func (css *ColStoreSuite) TestMakeWay(c *C) { c.Assert(node3.Col.Max, Equals, 10) }) + // Col1: |-x| + // Col2: |x-| + // Col3: |-------| + assertWayMade([]*Col{ + &Col{Min: 0, Max: 1}, &Col{Min: 7, Max: 8}, &Col{Min: 1, Max: 7}}, + func(root *colStoreNode) { + c.Assert(root.Prev, IsNil) + c.Assert(root.Next, NotNil) + c.Assert(root.Col.Min, Equals, 0) + c.Assert(root.Col.Max, Equals, 0) + node2 := root.Next + c.Assert(node2.Prev, Equals, root) + c.Assert(node2.Next, NotNil) + c.Assert(node2.Col.Min, Equals, 1) + c.Assert(node2.Col.Max, Equals, 7) + node3 := node2.Next + c.Assert(node3.Prev, Equals, node2) + c.Assert(node3.Next, IsNil) + c.Assert(node3.Col.Min, Equals, 8) + c.Assert(node3.Col.Max, Equals, 8) + }) + + // Col1: |-x| + // Col2: |--| + // Col3: |-----| + assertWayMade([]*Col{ + &Col{Min: 0, Max: 1}, &Col{Min: 7, Max: 8}, &Col{Min: 1, Max: 6}}, + func(root *colStoreNode) { + c.Assert(root.Prev, IsNil) + c.Assert(root.Next, NotNil) + c.Assert(root.Col.Min, Equals, 0) + c.Assert(root.Col.Max, Equals, 0) + node2 := root.Next + c.Assert(node2.Prev, Equals, root) + c.Assert(node2.Next, NotNil) + c.Assert(node2.Col.Min, Equals, 1) + c.Assert(node2.Col.Max, Equals, 6) + node3 := node2.Next + c.Assert(node3.Prev, Equals, node2) + c.Assert(node3.Next, IsNil) + c.Assert(node3.Col.Min, Equals, 7) + c.Assert(node3.Col.Max, Equals, 8) + }) + + // Col1: |--| + // Col2: |x-| + // Col3: |-----| + assertWayMade([]*Col{ + &Col{Min: 0, Max: 1}, &Col{Min: 7, Max: 8}, &Col{Min: 2, Max: 7}}, + func(root *colStoreNode) { + c.Assert(root.Prev, IsNil) + c.Assert(root.Next, NotNil) + c.Assert(root.Col.Min, Equals, 0) + c.Assert(root.Col.Max, Equals, 1) + node2 := root.Next + c.Assert(node2.Prev, Equals, root) + c.Assert(node2.Next, NotNil) + c.Assert(node2.Col.Min, Equals, 2) + c.Assert(node2.Col.Max, Equals, 7) + node3 := node2.Next + c.Assert(node3.Prev, Equals, node2) + c.Assert(node3.Next, IsNil) + c.Assert(node3.Col.Min, Equals, 8) + c.Assert(node3.Col.Max, Equals, 8) + }) + + // Col1: |--| + // Col2: |xx| + // Col3: |--| + // Col4: |--| + assertWayMade( + []*Col{ + &Col{Min: 0, Max: 1}, + &Col{Min: 2, Max: 3, Width: 1.0}, + &Col{Min: 4, Max: 5}, + &Col{Min: 2, Max: 3, Width: 2.0}, + }, + func(root *colStoreNode) { + c.Assert(root.Prev, IsNil) + c.Assert(root.Next, NotNil) + c.Assert(root.Col.Min, Equals, 0) + c.Assert(root.Col.Max, Equals, 1) + node2 := root.Next + c.Assert(node2.Prev, Equals, root) + c.Assert(node2.Next, NotNil) + c.Assert(node2.Col.Min, Equals, 2) + c.Assert(node2.Col.Max, Equals, 3) + c.Assert(node2.Col.Width, Equals, 2.0) // We have the later version + node3 := node2.Next + c.Assert(node3.Prev, Equals, node2) + c.Assert(node3.Next, IsNil) + c.Assert(node3.Col.Min, Equals, 4) + c.Assert(node3.Col.Max, Equals, 5) + }) + + // Col1: |-x| + // Col2: |xx| + // Col3: |x-| + // Col4: |----| + assertWayMade( + []*Col{ + &Col{Min: 0, Max: 1, Width: 1.0}, + &Col{Min: 2, Max: 3, Width: 2.0}, + &Col{Min: 4, Max: 5, Width: 3.0}, + &Col{Min: 1, Max: 4, Width: 4.0}, + }, + func(root *colStoreNode) { + c.Assert(root.Prev, IsNil) + c.Assert(root.Next, NotNil) + c.Assert(root.Col.Min, Equals, 0) + c.Assert(root.Col.Max, Equals, 0) + c.Assert(root.Col.Width, Equals, 1.0) + node2 := root.Next + c.Assert(node2.Prev, Equals, root) + c.Assert(node2.Next, NotNil) + c.Assert(node2.Col.Min, Equals, 1) + c.Assert(node2.Col.Max, Equals, 4) + c.Assert(node2.Col.Width, Equals, 4.0) + node3 := node2.Next + c.Assert(node3.Prev, Equals, node2) + c.Assert(node3.Next, IsNil) + c.Assert(node3.Col.Min, Equals, 5) + c.Assert(node3.Col.Max, Equals, 5) + c.Assert(node3.Col.Width, Equals, 3.0) + }) + } func (css *ColStoreSuite) TestFindNodeForCol(c *C) { From c72a01546f5727e65dd2e401d84848dbe47c110c Mon Sep 17 00:00:00 2001 From: "Geoffrey J. Teale" Date: Tue, 8 Oct 2019 21:37:24 +0200 Subject: [PATCH 07/13] Most of the tests are passing --- col.go | 226 +++++++++---- col_test.go | 657 ++++++++++++++++++++++-------------- data_validation_test.go | 136 ++++---- file.go | 1 + file_test.go | 7 +- format_code.go | 5 +- go.mod | 2 +- go.sum | 4 + lib.go | 46 ++- lib_test.go | 50 ++- row.go | 3 +- sheet.go | 315 +++++++++++------ sheet_test.go | 154 +++++---- stream_file.go | 47 ++- stream_file_builder.go | 49 +-- stream_file_builder_test.go | 14 + stream_style_test.go | 144 ++++---- stream_test.go | 183 ++++++---- xmlStyle.go | 2 +- xmlWorksheet.go | 4 +- 20 files changed, 1245 insertions(+), 804 deletions(-) create mode 100644 stream_file_builder_test.go diff --git a/col.go b/col.go index 9d854ef3..dc01a553 100644 --- a/col.go +++ b/col.go @@ -12,11 +12,37 @@ type Col struct { Width float64 Collapsed bool OutlineLevel uint8 + BestFit bool + CustomWidth bool + Phonetic bool numFmt string parsedNumFmt *parsedNumberFormat style *Style DataValidation []*xlsxCellDataValidation defaultCellType *CellType + 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. @@ -120,10 +146,14 @@ func (c *Col) GetStreamStyle() StreamStyle { 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 ColStore. +// resulting Col into the Col Store. func (c *Col) copyToRange(min, max int) *Col { return &Col{ Min: min, @@ -132,6 +162,9 @@ func (c *Col) copyToRange(min, max int) *Col { 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, @@ -140,14 +173,14 @@ func (c *Col) copyToRange(min, max int) *Col { } } -type colStoreNode struct { +type ColStoreNode struct { Col *Col - Prev *colStoreNode - Next *colStoreNode + Prev *ColStoreNode + Next *ColStoreNode } // -func (csn *colStoreNode) findNodeForColNum(num int) *colStoreNode { +func (csn *ColStoreNode) findNodeForColNum(num int) *ColStoreNode { switch { case num >= csn.Col.Min && num <= csn.Col.Max: return csn @@ -175,32 +208,40 @@ func (csn *colStoreNode) findNodeForColNum(num int) *colStoreNode { // 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 + 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) { - newNode := &colStoreNode{Col: col} +func (cs *ColStore) Add(col *Col) *ColStoreNode { + newNode := &ColStoreNode{Col: col} if cs.Root == nil { cs.Root = newNode - return + cs.Len = 1 + return newNode } cs.makeWay(cs.Root, newNode) - return + return newNode } -// -func (cs *ColStore) findNodeForColNum(num int) *colStoreNode { +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) { +func (cs *ColStore) removeNode(node *ColStoreNode) { if node.Prev != nil { if node.Next != nil { node.Prev.Next = node.Next @@ -228,15 +269,16 @@ func (cs *ColStore) removeNode(node *colStoreNode) { } 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 +// 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) { +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 @@ -244,16 +286,14 @@ func (cs *ColStore) makeWay(node1, node2 *colStoreNode) { // Node1 |----| // Node2 |----| if node1.Next != nil { - next := node1.Next - if next.Col.Min >= node2.Col.Min { - node1.Next = node2 - node2.Prev = node1 + if node1.Next.Col.Min <= node2.Col.Max { + cs.makeWay(node1.Next, node2) + return } - cs.makeWay(next, node2) + cs.addNode(node1, node2, node1.Next) return } - node1.Next = node2 - node2.Prev = node1 + cs.addNode(node1, node2, nil) return case node1.Col.Min > node2.Col.Max: @@ -262,16 +302,14 @@ func (cs *ColStore) makeWay(node1, node2 *colStoreNode) { // Node1 |-----| // Node2 |----| if node1.Prev != nil { - prev := node1.Prev - if prev.Col.Max <= node2.Col.Max { - node1.Prev = node2 - node2.Next = node1 + if node1.Prev.Col.Max >= node2.Col.Min { + cs.makeWay(node1.Prev, node2) + return } - cs.makeWay(prev, node2) + cs.addNode(node1.Prev, node2, node1) return } - node1.Prev = node2 - node2.Next = node1 + cs.addNode(nil, node2, node1) return case node1.Col.Min == node2.Col.Min && node1.Col.Max == node2.Col.Max: @@ -283,14 +321,7 @@ func (cs *ColStore) makeWay(node1, node2 *colStoreNode) { prev := node1.Prev next := node1.Next cs.removeNode(node1) - if prev != nil { - prev.Next = node2 - node2.Prev = prev - } - if next != nil { - next.Prev = node2 - node2.Next = next - } + cs.addNode(prev, node2, next) // Remove node may have set the root to nil if cs.Root == nil { cs.Root = node2 @@ -306,13 +337,19 @@ func (cs *ColStore) makeWay(node1, node2 *colStoreNode) { 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 prev != nil { - prev.Next = nil + if node2.Prev != nil && node2.Prev.Col.Max >= node2.Col.Min { cs.makeWay(prev, node2) } - if next != nil { - next.Prev = nil + if node2.Next != nil && node2.Next.Col.Min <= node2.Col.Max { cs.makeWay(next, node2) } @@ -326,11 +363,10 @@ func (cs *ColStore) makeWay(node1, node2 *colStoreNode) { // Node1 |---xx---| // Node2 |--| newCol := node1.Col.copyToRange(node2.Col.Max+1, node1.Col.Max) - newNode := &colStoreNode{Col: newCol, Prev: node2, Next: node1.Next} + newNode := &ColStoreNode{Col: newCol} + cs.addNode(node1, newNode, node1.Next) node1.Col.Max = node2.Col.Min - 1 - node1.Next = node2 - node2.Prev = node1 - node2.Next = newNode + cs.addNode(node1, node2, newNode) return case node1.Col.Max >= node2.Col.Min && node1.Col.Min < node2.Col.Min: @@ -338,15 +374,15 @@ func (cs *ColStore) makeWay(node1, node2 *colStoreNode) { // // Node1 |----xx| // Node2 |-------| + next := node1.Next node1.Col.Max = node2.Col.Min - 1 - if node1.Next != nil { - // Break the link to this node, which prevents - // us looping back and forth forever - node1.Next.Prev = nil - cs.makeWay(node1.Next, node2) + if next == node2 { + return + } + cs.addNode(node1, node2, next) + if next != nil && next.Col.Min <= node2.Col.Max { + cs.makeWay(next, node2) } - node1.Next = node2 - node2.Prev = node1 return case node1.Col.Min <= node2.Col.Max && node1.Col.Min > node2.Col.Min: @@ -354,16 +390,82 @@ func (cs *ColStore) makeWay(node1, node2 *colStoreNode) { // // Node1: |------| // Node2: |----xx| + prev := node1.Prev node1.Col.Min = node2.Col.Max + 1 - if node1.Prev != nil { - // Break the link to this node, which prevents - // us looping back and forth forever - node1.Prev.Next = nil + if prev == node2 { + return + } + cs.addNode(prev, node2, node1) + if prev != nil && prev.Col.Max >= node2.Col.Min { cs.makeWay(node1.Prev, node2) } - node1.Prev = node2 - node2.Next = node1 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) +} diff --git a/col_test.go b/col_test.go index 6d1ebc3e..a7cd439d 100644 --- a/col_test.go +++ b/col_test.go @@ -1,47 +1,93 @@ package xlsx import ( + "testing" + + qt "github.com/frankban/quicktest" . "gopkg.in/check.v1" ) -type ColSuite struct{} - -var _ = Suite(&ColSuite{}) - -func (cs *ColSuite) TestCopyToRange(c *C) { - nf := &parsedNumberFormat{} - s := &Style{} - cdv1 := &xlsxCellDataValidation{} - cdv2 := &xlsxCellDataValidation{} - ct := CellTypeBool.Ptr() - c1 := &Col{ - Min: 0, - Max: 10, - Hidden: true, - Width: 300.4, - Collapsed: true, - OutlineLevel: 2, - numFmt: "-0.00", - parsedNumFmt: nf, - style: s, - DataValidation: []*xlsxCellDataValidation{cdv1, cdv2}, - defaultCellType: ct, - } +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{} + cdv1 := &xlsxCellDataValidation{} + cdv2 := &xlsxCellDataValidation{} + ct := CellTypeBool.Ptr() + c1 := &Col{ + Min: 1, + Max: 11, + Hidden: true, + Width: 300.4, + Collapsed: true, + OutlineLevel: 2, + numFmt: "-0.00", + parsedNumFmt: nf, + style: s, + DataValidation: []*xlsxCellDataValidation{cdv1, cdv2}, + defaultCellType: ct, + } + + 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) + c.Assert(c2.DataValidation, qt.HasLen, 2) + c.Assert(c2.DataValidation[0], qt.Equals, c1.DataValidation[0]) + c.Assert(c2.DataValidation[1], qt.Equals, c1.DataValidation[1]) + c.Assert(c2.defaultCellType, qt.Equals, c1.defaultCellType) + }) - c2 := c1.copyToRange(4, 10) - c.Assert(c2.Min, Equals, 4) - c.Assert(c2.Max, Equals, 10) - c.Assert(c2.Hidden, Equals, c1.Hidden) - c.Assert(c2.Width, Equals, c1.Width) - c.Assert(c2.Collapsed, Equals, c1.Collapsed) - c.Assert(c2.OutlineLevel, Equals, c1.OutlineLevel) - c.Assert(c2.numFmt, Equals, c1.numFmt) - c.Assert(c2.parsedNumFmt, Equals, c1.parsedNumFmt) - c.Assert(c2.style, Equals, c1.style) - c.Assert(c2.DataValidation, HasLen, 2) - c.Assert(c2.DataValidation[0], Equals, c1.DataValidation[0]) - c.Assert(c2.DataValidation[1], Equals, c1.DataValidation[1]) - c.Assert(c2.defaultCellType, Equals, c1.defaultCellType) } type ColStoreSuite struct{} @@ -49,250 +95,282 @@ type ColStoreSuite struct{} var _ = Suite(&ColStoreSuite{}) func (css *ColStoreSuite) TestAddRootNode(c *C) { - col := &Col{Min: 0, Max: 1} + 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 (css *ColStoreSuite) TestMakeWay(c *C) { - assertWayMade := func(cols []*Col, chainFunc func(root *colStoreNode)) { +func TestMakeWay(t *testing.T) { + c := qt.New(t) + assertWayMade := func(cols []*Col, chainFunc func(*ColStore)) { - cs := ColStore{} + cs := &ColStore{} for _, col := range cols { - cs.Add(col) + _ = cs.Add(col) } - chainFunc(cs.Root) + chainFunc(cs) } // Col1: |--| // Col2: |--| - assertWayMade([]*Col{&Col{Min: 0, Max: 1}, &Col{Min: 2, Max: 3}}, - func(root *colStoreNode) { - c.Assert(root.Col.Min, Equals, 0) - c.Assert(root.Col.Max, Equals, 1) - c.Assert(root.Prev, IsNil) - c.Assert(root.Next, NotNil) + assertWayMade([]*Col{&Col{Min: 1, Max: 2}, &Col{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, Equals, root) - c.Assert(node2.Next, IsNil) - c.Assert(node2.Col.Min, Equals, 2) - c.Assert(node2.Col.Max, Equals, 3) + 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{&Col{Min: 2, Max: 3}, &Col{Min: 0, Max: 1}}, - func(root *colStoreNode) { - c.Assert(root.Col.Min, Equals, 2) - c.Assert(root.Col.Max, Equals, 3) - c.Assert(root.Prev, NotNil) - c.Assert(root.Next, IsNil) + assertWayMade([]*Col{&Col{Min: 3, Max: 4}, &Col{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, Equals, root) - c.Assert(node2.Prev, IsNil) - c.Assert(node2.Col.Min, Equals, 0) - c.Assert(node2.Col.Max, Equals, 1) + 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{&Col{Min: 0, Max: 2}, &Col{Min: 2, Max: 3}}, - func(root *colStoreNode) { - c.Assert(root.Col.Min, Equals, 0) - c.Assert(root.Col.Max, Equals, 1) - c.Assert(root.Prev, IsNil) - c.Assert(root.Next, NotNil) + assertWayMade([]*Col{&Col{Min: 1, Max: 3}, &Col{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, Equals, root) - c.Assert(node2.Next, IsNil) - c.Assert(node2.Col.Min, Equals, 2) - c.Assert(node2.Col.Max, Equals, 3) + 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{&Col{Min: 1, Max: 2}, &Col{Min: 0, Max: 1}}, - func(root *colStoreNode) { - c.Assert(root.Col.Min, Equals, 2) - c.Assert(root.Col.Max, Equals, 2) - c.Assert(root.Prev, NotNil) - c.Assert(root.Next, IsNil) + assertWayMade([]*Col{&Col{Min: 2, Max: 3}, &Col{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, Equals, root) - c.Assert(node2.Prev, IsNil) - c.Assert(node2.Col.Min, Equals, 0) - c.Assert(node2.Col.Max, Equals, 1) + 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{&Col{Min: 0, Max: 7}, &Col{Min: 3, Max: 4}}, - func(root *colStoreNode) { - c.Assert(root.Prev, IsNil) - c.Assert(root.Next, NotNil) + assertWayMade([]*Col{&Col{Min: 1, Max: 8}, &Col{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, Equals, root) - c.Assert(node2.Col.Min, Equals, 3) - c.Assert(node2.Col.Max, Equals, 4) - c.Assert(node2.Next, NotNil) + 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, Equals, node2) - c.Assert(node3.Next, IsNil) - c.Assert(node3.Col.Min, Equals, 5) - c.Assert(node3.Col.Max, Equals, 7) + 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{&Col{Min: 0, Max: 1, Width: 40.1}, &Col{Min: 0, Max: 1, Width: 10.0}}, - func(root *colStoreNode) { - c.Assert(root, NotNil) - c.Assert(root.Prev, IsNil) - c.Assert(root.Next, IsNil) - c.Assert(root.Col.Min, Equals, 0) - c.Assert(root.Col.Max, Equals, 1) + assertWayMade([]*Col{&Col{Min: 1, Max: 2, Width: 40.1}, &Col{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, Equals, 10.0) + c.Assert(root.Col.Width, qt.Equals, 10.0) }) // Col1: |xx| // Col2: |----| - assertWayMade([]*Col{&Col{Min: 1, Max: 2, Width: 40.1}, &Col{Min: 0, Max: 3, Width: 10.0}}, - func(root *colStoreNode) { - c.Assert(root.Prev, IsNil) - c.Assert(root.Next, IsNil) - c.Assert(root.Col.Min, Equals, 0) - c.Assert(root.Col.Max, Equals, 3) + assertWayMade([]*Col{&Col{Min: 2, Max: 3, Width: 40.1}, &Col{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, Equals, 10.0) + c.Assert(root.Col.Width, qt.Equals, 10.0) }) // Col1: |--| // Col2: |--| // Col3: |--| - assertWayMade([]*Col{&Col{Min: 0, Max: 1}, &Col{Min: 2, Max: 3}, &Col{Min: 4, Max: 5}}, - func(root *colStoreNode) { - c.Assert(root.Prev, IsNil) - c.Assert(root.Next, NotNil) - c.Assert(root.Col.Min, Equals, 0) - c.Assert(root.Col.Max, Equals, 1) + assertWayMade([]*Col{&Col{Min: 1, Max: 2}, &Col{Min: 3, Max: 4}, &Col{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, Equals, root) - c.Assert(node2.Col.Min, Equals, 2) - c.Assert(node2.Col.Max, Equals, 3) - c.Assert(node2.Next, NotNil) + 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, Equals, node2) - c.Assert(node3.Next, IsNil) - c.Assert(node3.Col.Min, Equals, 4) - c.Assert(node3.Col.Max, Equals, 5) + 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{&Col{Min: 4, Max: 5}, &Col{Min: 2, Max: 3}, &Col{Min: 0, Max: 1}}, - func(root *colStoreNode) { - c.Assert(root.Prev, NotNil) - c.Assert(root.Next, IsNil) - c.Assert(root.Col.Min, Equals, 4) - c.Assert(root.Col.Max, Equals, 5) + assertWayMade([]*Col{&Col{Min: 5, Max: 6}, &Col{Min: 3, Max: 4}, &Col{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, Equals, root) - c.Assert(node2.Col.Min, Equals, 2) - c.Assert(node2.Col.Max, Equals, 3) - c.Assert(node2.Prev, NotNil) + 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, Equals, node2) - c.Assert(node3.Prev, IsNil) - c.Assert(node3.Col.Min, Equals, 0) - c.Assert(node3.Col.Max, Equals, 1) + 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{&Col{Min: 0, Max: 1}, &Col{Min: 9, Max: 10}, &Col{Min: 4, Max: 5}}, - func(root *colStoreNode) { - c.Assert(root.Prev, IsNil) - c.Assert(root.Next, NotNil) + assertWayMade([]*Col{&Col{Min: 1, Max: 2}, &Col{Min: 10, Max: 11}, &Col{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, Equals, root) - c.Assert(node2.Col.Min, Equals, 4) - c.Assert(node2.Col.Max, Equals, 5) - c.Assert(node2.Next, NotNil) + 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, Equals, node2) - c.Assert(node3.Next, IsNil) - c.Assert(node3.Col.Min, Equals, 9) - c.Assert(node3.Col.Max, Equals, 10) + 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{ - &Col{Min: 0, Max: 1}, &Col{Min: 7, Max: 8}, &Col{Min: 1, Max: 7}}, - func(root *colStoreNode) { - c.Assert(root.Prev, IsNil) - c.Assert(root.Next, NotNil) - c.Assert(root.Col.Min, Equals, 0) - c.Assert(root.Col.Max, Equals, 0) + &Col{Min: 1, Max: 2}, &Col{Min: 8, Max: 9}, &Col{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, Equals, root) - c.Assert(node2.Next, NotNil) - c.Assert(node2.Col.Min, Equals, 1) - c.Assert(node2.Col.Max, Equals, 7) + 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, Equals, node2) - c.Assert(node3.Next, IsNil) - c.Assert(node3.Col.Min, Equals, 8) - c.Assert(node3.Col.Max, Equals, 8) + 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{ - &Col{Min: 0, Max: 1}, &Col{Min: 7, Max: 8}, &Col{Min: 1, Max: 6}}, - func(root *colStoreNode) { - c.Assert(root.Prev, IsNil) - c.Assert(root.Next, NotNil) - c.Assert(root.Col.Min, Equals, 0) - c.Assert(root.Col.Max, Equals, 0) + &Col{Min: 1, Max: 2}, &Col{Min: 8, Max: 9}, &Col{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, Equals, root) - c.Assert(node2.Next, NotNil) - c.Assert(node2.Col.Min, Equals, 1) - c.Assert(node2.Col.Max, Equals, 6) + 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, Equals, node2) - c.Assert(node3.Next, IsNil) - c.Assert(node3.Col.Min, Equals, 7) - c.Assert(node3.Col.Max, Equals, 8) + 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{ - &Col{Min: 0, Max: 1}, &Col{Min: 7, Max: 8}, &Col{Min: 2, Max: 7}}, - func(root *colStoreNode) { - c.Assert(root.Prev, IsNil) - c.Assert(root.Next, NotNil) - c.Assert(root.Col.Min, Equals, 0) - c.Assert(root.Col.Max, Equals, 1) + &Col{Min: 1, Max: 2}, &Col{Min: 8, Max: 9}, &Col{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, Equals, root) - c.Assert(node2.Next, NotNil) - c.Assert(node2.Col.Min, Equals, 2) - c.Assert(node2.Col.Max, Equals, 7) + 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, Equals, node2) - c.Assert(node3.Next, IsNil) - c.Assert(node3.Col.Min, Equals, 8) - c.Assert(node3.Col.Max, Equals, 8) + 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: |--| @@ -301,27 +379,29 @@ func (css *ColStoreSuite) TestMakeWay(c *C) { // Col4: |--| assertWayMade( []*Col{ - &Col{Min: 0, Max: 1}, - &Col{Min: 2, Max: 3, Width: 1.0}, - &Col{Min: 4, Max: 5}, - &Col{Min: 2, Max: 3, Width: 2.0}, + &Col{Min: 1, Max: 2}, + &Col{Min: 3, Max: 4, Width: 1.0}, + &Col{Min: 5, Max: 6}, + &Col{Min: 3, Max: 4, Width: 2.0}, }, - func(root *colStoreNode) { - c.Assert(root.Prev, IsNil) - c.Assert(root.Next, NotNil) - c.Assert(root.Col.Min, Equals, 0) - c.Assert(root.Col.Max, Equals, 1) + 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, Equals, root) - c.Assert(node2.Next, NotNil) - c.Assert(node2.Col.Min, Equals, 2) - c.Assert(node2.Col.Max, Equals, 3) - c.Assert(node2.Col.Width, Equals, 2.0) // We have the later version + 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, Equals, node2) - c.Assert(node3.Next, IsNil) - c.Assert(node3.Col.Min, Equals, 4) - c.Assert(node3.Col.Max, Equals, 5) + 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| @@ -330,29 +410,31 @@ func (css *ColStoreSuite) TestMakeWay(c *C) { // Col4: |----| assertWayMade( []*Col{ - &Col{Min: 0, Max: 1, Width: 1.0}, - &Col{Min: 2, Max: 3, Width: 2.0}, - &Col{Min: 4, Max: 5, Width: 3.0}, - &Col{Min: 1, Max: 4, Width: 4.0}, + &Col{Min: 1, Max: 2, Width: 1.0}, + &Col{Min: 3, Max: 4, Width: 2.0}, + &Col{Min: 5, Max: 6, Width: 3.0}, + &Col{Min: 2, Max: 5, Width: 4.0}, }, - func(root *colStoreNode) { - c.Assert(root.Prev, IsNil) - c.Assert(root.Next, NotNil) - c.Assert(root.Col.Min, Equals, 0) - c.Assert(root.Col.Max, Equals, 0) - c.Assert(root.Col.Width, Equals, 1.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, Equals, root) - c.Assert(node2.Next, NotNil) - c.Assert(node2.Col.Min, Equals, 1) - c.Assert(node2.Col.Max, Equals, 4) - c.Assert(node2.Col.Width, Equals, 4.0) + 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, Equals, node2) - c.Assert(node3.Next, IsNil) - c.Assert(node3.Col.Min, Equals, 5) - c.Assert(node3.Col.Max, Equals, 5) - c.Assert(node3.Col.Width, Equals, 3.0) + 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) }) } @@ -370,26 +452,26 @@ func (css *ColStoreSuite) TestFindNodeForCol(c *C) { } cs := &ColStore{} - col0 := &Col{Min: 0, Max: 0} + col0 := &Col{Min: 1, Max: 1} cs.Add(col0) - col1 := &Col{Min: 1, Max: 1} + col1 := &Col{Min: 2, Max: 2} cs.Add(col1) - col2 := &Col{Min: 2, Max: 2} + col2 := &Col{Min: 3, Max: 3} cs.Add(col2) - col3 := &Col{Min: 3, Max: 3} + col3 := &Col{Min: 4, Max: 4} cs.Add(col3) - col4 := &Col{Min: 4, Max: 4} + col4 := &Col{Min: 5, Max: 5} cs.Add(col4) col5 := &Col{Min: 100, Max: 125} cs.Add(col5) - assertNodeFound(cs, -1, nil) - assertNodeFound(cs, 0, col0) - assertNodeFound(cs, 1, col1) - assertNodeFound(cs, 2, col2) - assertNodeFound(cs, 3, col3) - assertNodeFound(cs, 4, col4) - assertNodeFound(cs, 5, nil) + 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) @@ -411,20 +493,97 @@ func (css *ColStoreSuite) TestRemoveNode(c *C) { } cs := &ColStore{} - col0 := &Col{Min: 0, Max: 0} + col0 := &Col{Min: 1, Max: 1} cs.Add(col0) - col1 := &Col{Min: 1, Max: 1} + col1 := &Col{Min: 2, Max: 2} cs.Add(col1) - col2 := &Col{Min: 2, Max: 2} + col2 := &Col{Min: 3, Max: 3} cs.Add(col2) - col3 := &Col{Min: 3, Max: 3} + col3 := &Col{Min: 4, Max: 4} cs.Add(col3) - col4 := &Col{Min: 4, Max: 4} + col4 := &Col{Min: 5, Max: 5} cs.Add(col4) + c.Assert(cs.Len, Equals, 5) - cs.removeNode(cs.findNodeForColNum(4)) + cs.removeNode(cs.findNodeForColNum(5)) + c.Assert(cs.Len, Equals, 4) assertChain(cs, []*Col{col0, col1, col2, col3}) - cs.removeNode(cs.findNodeForColNum(0)) + 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{&Col{Min: 1, Max: 11}}) + + // get everything, one col + assertCols(1, 11, []*Col{&Col{Min: 1, Max: 11}}, []*Col{&Col{Min: 1, Max: 11}}) + + // get everything, many cols + assertCols(1, 11, + []*Col{ + &Col{Min: 1, Max: 4}, + &Col{Min: 5, Max: 8}, + &Col{Min: 9, Max: 11}, + }, + []*Col{ + &Col{Min: 1, Max: 4}, + &Col{Min: 5, Max: 8}, + &Col{Min: 9, Max: 11}, + }, + ) + + // make missing col + assertCols(1, 11, + []*Col{ + &Col{Min: 1, Max: 4}, + &Col{Min: 9, Max: 11}, + }, + []*Col{ + &Col{Min: 1, Max: 4}, + &Col{Min: 5, Max: 8}, + &Col{Min: 9, Max: 11}, + }, + ) + +} diff --git a/data_validation_test.go b/data_validation_test.go index 2c826aa4..05e01df3 100644 --- a/data_validation_test.go +++ b/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 { @@ -31,79 +30,79 @@ func (d *DataValidationSuite) TestDataValidation(t *C) { dd := NewXlsxCellDataValidation(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) 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.SetDataValidation(2, 2, dd, 0, 0) dd = NewXlsxCellDataValidation(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.SetDataValidation(3, 3, dd, 3, 7) dd = NewXlsxCellDataValidation(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.SetDataValidationWithStart(4, 4, dd, 1) index := 5 rowIndex := 1 dd = NewXlsxCellDataValidation(true) err = dd.SetRange(15, 4, DataValidationTypeTextLeng, DataValidationOperatorBetween) - t.Assert(err, IsNil) + c.Assert(err, qt.IsNil) sheet.Cell(rowIndex, index).SetDataValidation(dd) index++ dd = NewXlsxCellDataValidation(true) err = dd.SetRange(10, 1, DataValidationTypeTextLeng, DataValidationOperatorEqual) - t.Assert(err, IsNil) + c.Assert(err, qt.IsNil) sheet.Cell(rowIndex, index).SetDataValidation(dd) index++ dd = NewXlsxCellDataValidation(true) err = dd.SetRange(10, 1, DataValidationTypeTextLeng, DataValidationOperatorGreaterThanOrEqual) - t.Assert(err, IsNil) + c.Assert(err, qt.IsNil) sheet.Cell(rowIndex, index).SetDataValidation(dd) index++ dd = NewXlsxCellDataValidation(true) err = dd.SetRange(10, 1, DataValidationTypeTextLeng, DataValidationOperatorGreaterThan) - t.Assert(err, IsNil) + c.Assert(err, qt.IsNil) sheet.Cell(rowIndex, index).SetDataValidation(dd) index++ dd = NewXlsxCellDataValidation(true) err = dd.SetRange(10, 1, DataValidationTypeTextLeng, DataValidationOperatorLessThan) - t.Assert(err, IsNil) + c.Assert(err, qt.IsNil) sheet.Cell(rowIndex, index).SetDataValidation(dd) index++ dd = NewXlsxCellDataValidation(true) err = dd.SetRange(10, 1, DataValidationTypeTextLeng, DataValidationOperatorLessThanOrEqual) - t.Assert(err, IsNil) + c.Assert(err, qt.IsNil) sheet.Cell(rowIndex, index).SetDataValidation(dd) index++ dd = NewXlsxCellDataValidation(true) err = dd.SetRange(10, 1, DataValidationTypeTextLeng, DataValidationOperatorNotEqual) - t.Assert(err, IsNil) + c.Assert(err, qt.IsNil) sheet.Cell(rowIndex, index).SetDataValidation(dd) index++ dd = NewXlsxCellDataValidation(true) err = dd.SetRange(10, 1, DataValidationTypeTextLeng, DataValidationOperatorNotBetween) - t.Assert(err, IsNil) + c.Assert(err, qt.IsNil) sheet.Cell(rowIndex, index).SetDataValidation(dd) index++ @@ -112,43 +111,43 @@ func (d *DataValidationSuite) TestDataValidation(t *C) { dd = NewXlsxCellDataValidation(true) err = dd.SetRange(4, 15, DataValidationTypeWhole, DataValidationOperatorBetween) - t.Assert(err, IsNil) + c.Assert(err, qt.IsNil) sheet.Cell(rowIndex, index).SetDataValidation(dd) index++ dd = NewXlsxCellDataValidation(true) err = dd.SetRange(10, 1, DataValidationTypeWhole, DataValidationOperatorEqual) - t.Assert(err, IsNil) + c.Assert(err, qt.IsNil) sheet.Cell(rowIndex, index).SetDataValidation(dd) index++ dd = NewXlsxCellDataValidation(true) err = dd.SetRange(10, 1, DataValidationTypeWhole, DataValidationOperatorGreaterThanOrEqual) - t.Assert(err, IsNil) + c.Assert(err, qt.IsNil) sheet.Cell(rowIndex, index).SetDataValidation(dd) index++ dd = NewXlsxCellDataValidation(true) err = dd.SetRange(10, 1, DataValidationTypeWhole, DataValidationOperatorGreaterThan) - t.Assert(err, IsNil) + c.Assert(err, qt.IsNil) sheet.Cell(rowIndex, index).SetDataValidation(dd) index++ dd = NewXlsxCellDataValidation(true) err = dd.SetRange(10, 1, DataValidationTypeWhole, DataValidationOperatorLessThan) - t.Assert(err, IsNil) + c.Assert(err, qt.IsNil) sheet.Cell(rowIndex, index).SetDataValidation(dd) index++ dd = NewXlsxCellDataValidation(true) err = dd.SetRange(10, 1, DataValidationTypeWhole, DataValidationOperatorLessThanOrEqual) - t.Assert(err, IsNil) + c.Assert(err, qt.IsNil) sheet.Cell(rowIndex, index).SetDataValidation(dd) index++ dd = NewXlsxCellDataValidation(true) err = dd.SetRange(10, 1, DataValidationTypeWhole, DataValidationOperatorNotEqual) - t.Assert(err, IsNil) + c.Assert(err, qt.IsNil) sheet.Cell(rowIndex, index).SetDataValidation(dd) index++ @@ -162,34 +161,34 @@ func (d *DataValidationSuite) TestDataValidation(t *C) { dd = NewXlsxCellDataValidation(true) err = dd.SetDropList([]string{"1", "2", "4"}) - t.Assert(err, IsNil) + c.Assert(err, qt.IsNil) dd1 := NewXlsxCellDataValidation(true) err = dd1.SetDropList([]string{"11", "22", "44"}) - t.Assert(err, IsNil) + c.Assert(err, qt.IsNil) dd2 := NewXlsxCellDataValidation(true) err = dd2.SetDropList([]string{"111", "222", "444"}) - t.Assert(err, IsNil) - sheet.Col(12).SetDataValidation(dd, 2, 10) - sheet.Col(12).SetDataValidation(dd1, 3, 4) - sheet.Col(12).SetDataValidation(dd2, 5, 7) + c.Assert(err, qt.IsNil) + sheet.SetDataValidation(12, 12, dd, 2, 10) + sheet.SetDataValidation(12, 12, dd1, 3, 4) + sheet.SetDataValidation(12, 12, dd2, 5, 7) dd = NewXlsxCellDataValidation(true) err = dd.SetDropList([]string{"1", "2", "4"}) - t.Assert(err, IsNil) + c.Assert(err, qt.IsNil) dd1 = NewXlsxCellDataValidation(true) err = dd1.SetDropList([]string{"11", "22", "44"}) - t.Assert(err, IsNil) - sheet.Col(13).SetDataValidation(dd, 2, 10) - sheet.Col(13).SetDataValidation(dd1, 1, 2) + c.Assert(err, qt.IsNil) + sheet.SetDataValidation(13, 13, dd, 2, 10) + sheet.SetDataValidation(13, 13, dd1, 1, 2) dd = NewXlsxCellDataValidation(true) err = dd.SetDropList([]string{"1", "2", "4"}) - t.Assert(err, IsNil) + c.Assert(err, qt.IsNil) dd1 = NewXlsxCellDataValidation(true) err = dd1.SetDropList([]string{"11", "22", "44"}) - t.Assert(err, IsNil) - sheet.Col(14).SetDataValidation(dd, 2, 10) - sheet.Col(14).SetDataValidation(dd1, 1, 5) + c.Assert(err, qt.IsNil) + sheet.SetDataValidation(14, 14, dd, 2, 10) + sheet.SetDataValidation(14, 14, dd1, 1, 5) dd = NewXlsxCellDataValidation(true) err = dd.SetDropList([]string{"1", "2", "4"}) @@ -198,71 +197,72 @@ func (d *DataValidationSuite) TestDataValidation(t *C) { } dd1 = NewXlsxCellDataValidation(true) err = dd1.SetDropList([]string{"11", "22", "44"}) - t.Assert(err, IsNil) - sheet.Col(15).SetDataValidation(dd, 2, 10) - sheet.Col(15).SetDataValidation(dd1, 1, 10) + c.Assert(err, qt.IsNil) + sheet.SetDataValidation(15, 15, dd, 2, 10) + sheet.SetDataValidation(15, 15, dd1, 1, 10) dd = NewXlsxCellDataValidation(true) err = dd.SetDropList([]string{"1", "2", "4"}) - t.Assert(err, IsNil) + c.Assert(err, qt.IsNil) dd1 = NewXlsxCellDataValidation(true) err = dd1.SetDropList([]string{"11", "22", "44"}) - t.Assert(err, IsNil) + c.Assert(err, qt.IsNil) dd2 = NewXlsxCellDataValidation(true) err = dd2.SetDropList([]string{"111", "222", "444"}) - t.Assert(err, IsNil) - sheet.Col(16).SetDataValidation(dd, 10, 20) - sheet.Col(16).SetDataValidation(dd1, 2, 4) - sheet.Col(16).SetDataValidation(dd2, 21, 30) + c.Assert(err, qt.IsNil) + sheet.SetDataValidation(16, 16, dd, 10, 20) + sheet.SetDataValidation(16, 16, dd1, 2, 4) + sheet.SetDataValidation(16, 16, dd2, 21, 30) dd = NewXlsxCellDataValidation(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.SetDataValidation(3, 3, dd, 3, Excel2006MaxRowIndex) dd = NewXlsxCellDataValidation(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) + sheet.SetDataValidation(3, 3, dd, 4, -1) maxRow := sheet.Col(3).DataValidation[len(sheet.Col(3).DataValidation)-1].maxRow - t.Assert(maxRow, Equals, Excel2006MaxRowIndex) + c.Assert(maxRow, qt.Equals, Excel2006MaxRowIndex) 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) + 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") } diff --git a/file.go b/file.go index bf4aa352..23d63fbe 100644 --- a/file.go +++ b/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) diff --git a/file_test.go b/file_test.go index 3d428352..a666ce2f 100644 --- a/file_test.go +++ b/file_test.go @@ -467,12 +467,12 @@ func (l *FileSuite) TestMarshalFile(c *C) { // sheets expectedSheet1 := ` -0&C&"Times New Roman,Regular"&12&A&C&"Times New Roman,Regular"&12Page &P` +0&C&"Times New Roman,Regular"&12&A&C&"Times New Roman,Regular"&12Page &P` c.Assert(parts["xl/worksheets/sheet1.xml"], Equals, expectedSheet1) expectedSheet2 := ` -0&C&"Times New Roman,Regular"&12&A&C&"Times New Roman,Regular"&12Page &P` +0&C&"Times New Roman,Regular"&12&A&C&"Times New Roman,Regular"&12Page &P` 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 := ` -` +` + c.Assert(parts["xl/styles.xml"], Equals, expectedStyles) } diff --git a/format_code.go b/format_code.go index 5b15a050..2c7206b6 100644 --- a/format_code.go +++ b/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 diff --git a/go.mod b/go.mod index decd7a3e..3e5ce782 100644 --- a/go.mod +++ b/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 ) diff --git a/go.sum b/go.sum index 1456afa8..0945b087 100644 --- a/go.sum +++ b/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= diff --git a/lib.go b/lib.go index 4992e237..5144fe40 100644 --- a/lib.go +++ b/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++ } } diff --git a/lib_test.go b/lib_test.go index 45ac656b..9344738c 100644 --- a/lib_test.go +++ b/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(` @@ -1049,37 +1047,37 @@ func (l *LibSuite) TestReadRowsFromSheetWithHiddenColumn(c *C) { `) 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. diff --git a/row.go b/row.go index f5ba0310..37f5a53c 100644 --- a/row.go +++ b/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 -} \ No newline at end of file +} diff --git a/sheet.go b/sheet.go index 2b6c73b2..44c8b103 100644 --- a/sheet.go +++ b/sheet.go @@ -12,7 +12,7 @@ type Sheet struct { Name string File *File Rows []*Row - Cols []*Col + Cols *ColStore MaxRow int MaxCol int Hidden bool @@ -102,31 +102,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 +134,109 @@ 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") } - end := endcol + 1 - s.maybeAddCol(end) - for ; startcol < end; startcol++ { - s.Cols[startcol].Width = width + 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") } - return nil + + 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 +} + +// 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 data validation for a range of columns. +func (s *Sheet) SetDataValidation(minCol, maxCol int, dd *xlsxCellDataValidation, minRow, maxRow int) { + s.setCol(minCol, maxCol, func(col *Col) { + col.SetDataValidation(dd, minRow, maxRow) + }) +} + +// Set the data validation for a range of columns. +func (s *Sheet) SetDataValidationWithStart(minCol, maxCol int, dd *xlsxCellDataValidation, start int) { + s.setCol(minCol, maxCol, func(col *Col) { + col.SetDataValidation(dd, start, -1) + }) +} + +// 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) + }) + +} + +// Set the cell metadata for a range of columns. +func (s *Sheet) SetCellMetadata(minCol, maxCol int, cm CellMetadata) { + s.setCol(minCol, maxCol, func(col *Col) { + col.SetCellMetadata(cm) + }) +} + +// Set the stream style for a range of columns. +func (s *Sheet) SetStreamStyle(minCol, maxCol int, ss StreamStyle) { + s.setCol(minCol, maxCol, func(col *Col) { + col.SetStreamStyle(ss) + }) } // When merging cells, the cell may be the 'original' or the 'covered'. @@ -199,19 +272,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 +285,88 @@ 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() + } + + if hasNumFmt { + xNumFmt := styles.newNumFmt(col.numFmt) + XfId = handleStyleForXLSX(style, xNumFmt.NumFmtId, styles) } - 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) + 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 + } + if nil != col.DataValidation { + if nil == worksheet.DataValidations { + worksheet.DataValidations = &xlsxCellDataValidations{} } - worksheet.DataValidations.DataValidation = append(worksheet.DataValidations.DataValidation, dd) + 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) + } + worksheet.DataValidations.Count = len(worksheet.DataValidations.DataValidation) } - worksheet.DataValidations.Count = len(worksheet.DataValidations.DataValidation) - } - } + + }) + 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 +383,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) } @@ -386,14 +470,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 +491,23 @@ func (s *Sheet) makeXLSXSheet(refTable *RefTable, styles *xlsxStyleSheet) *xlsxW dimension.Ref = "A1" } worksheet.Dimension = dimension + +} + +// 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.makeRows(worksheet, styles, refTable, maxLevelCol) + return worksheet } diff --git a/sheet_test.go b/sheet_test.go index 89d5d21c..2ab7d771 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -3,7 +3,9 @@ package xlsx import ( "bytes" "encoding/xml" + "testing" + qt "github.com/frankban/quicktest" . "gopkg.in/check.v1" ) @@ -134,7 +136,8 @@ func (s *SheetSuite) TestMakeXLSXSheetFromRows(c *C) { } // Test if the NumFmts assigned properly according the FormatCode in cell. -func (s *SheetSuite) TestMakeXLSXSheetWithNumFormats(c *C) { +func TestMakeXLSXSheetWithNumFormats(t *testing.T) { + c := qt.New(t) file := NewFile() sheet, _ := file.AddSheet("Sheet1") row := sheet.AddRow() @@ -159,26 +162,27 @@ func (s *SheetSuite) TestMakeXLSXSheetWithNumFormats(c *C) { styles := newXlsxStyleSheet(nil) worksheet := sheet.makeXLSXSheet(refTable, styles) - c.Assert(styles.CellStyleXfs, IsNil) + c.Assert(styles.CellStyleXfs, qt.IsNil) - c.Assert(styles.CellXfs.Count, Equals, 4) - c.Assert(styles.CellXfs.Xf[0].NumFmtId, Equals, 0) - c.Assert(styles.CellXfs.Xf[1].NumFmtId, Equals, 1) - c.Assert(styles.CellXfs.Xf[2].NumFmtId, Equals, 14) - c.Assert(styles.CellXfs.Xf[3].NumFmtId, Equals, 164) - c.Assert(styles.NumFmts.Count, Equals, 1) - c.Assert(styles.NumFmts.NumFmt[0].NumFmtId, Equals, 164) - c.Assert(styles.NumFmts.NumFmt[0].FormatCode, Equals, "hh:mm:ss") + c.Assert(styles.CellXfs.Count, qt.Equals, 4) + c.Assert(styles.CellXfs.Xf[0].NumFmtId, qt.Equals, 0) + c.Assert(styles.CellXfs.Xf[1].NumFmtId, qt.Equals, 1) + c.Assert(styles.CellXfs.Xf[2].NumFmtId, qt.Equals, 14) + c.Assert(styles.CellXfs.Xf[3].NumFmtId, qt.Equals, 164) + c.Assert(styles.NumFmts.Count, qt.Equals, 1) + c.Assert(styles.NumFmts.NumFmt[0].NumFmtId, qt.Equals, 164) + c.Assert(styles.NumFmts.NumFmt[0].FormatCode, qt.Equals, "hh:mm:ss") // Finally we check that the cell points to the right CellXf / // CellStyleXf. - c.Assert(worksheet.SheetData.Row[0].C[0].S, Equals, 0) - c.Assert(worksheet.SheetData.Row[0].C[1].S, Equals, 1) + c.Assert(worksheet.SheetData.Row[0].C[0].S, qt.Equals, 0) + c.Assert(worksheet.SheetData.Row[0].C[1].S, qt.Equals, 1) } // When we create the xlsxSheet we also populate the xlsxStyles struct // with style information. -func (s *SheetSuite) TestMakeXLSXSheetAlsoPopulatesXLSXSTyles(c *C) { +func TestMakeXLSXSheetAlsoPopulatesXLSXSTyles(t *testing.T) { + c := qt.New(t) file := NewFile() sheet, _ := file.AddSheet("Sheet1") row := sheet.AddRow() @@ -204,34 +208,32 @@ func (s *SheetSuite) TestMakeXLSXSheetAlsoPopulatesXLSXSTyles(c *C) { styles := newXlsxStyleSheet(nil) worksheet := sheet.makeXLSXSheet(refTable, styles) - c.Assert(styles.Fonts.Count, Equals, 2) - c.Assert(styles.Fonts.Font[0].Sz.Val, Equals, "12") - c.Assert(styles.Fonts.Font[0].Name.Val, Equals, "Verdana") - c.Assert(styles.Fonts.Font[1].Sz.Val, Equals, "10") - c.Assert(styles.Fonts.Font[1].Name.Val, Equals, "Verdana") + c.Assert(styles.Fonts.Count, qt.Equals, 1) + c.Assert(styles.Fonts.Font[0].Sz.Val, qt.Equals, "10") + c.Assert(styles.Fonts.Font[0].Name.Val, qt.Equals, "Verdana") - c.Assert(styles.Fills.Count, Equals, 3) - c.Assert(styles.Fills.Fill[0].PatternFill.PatternType, Equals, "none") - c.Assert(styles.Fills.Fill[0].PatternFill.FgColor.RGB, Equals, "") - c.Assert(styles.Fills.Fill[0].PatternFill.BgColor.RGB, Equals, "") + c.Assert(styles.Fills.Count, qt.Equals, 2) + c.Assert(styles.Fills.Fill[0].PatternFill.PatternType, qt.Equals, "solid") + c.Assert(styles.Fills.Fill[0].PatternFill.FgColor.RGB, qt.Equals, "FFFFFFFF") + c.Assert(styles.Fills.Fill[0].PatternFill.BgColor.RGB, qt.Equals, "00000000") - c.Assert(styles.Borders.Count, Equals, 2) - c.Assert(styles.Borders.Border[1].Left.Style, Equals, "none") - c.Assert(styles.Borders.Border[1].Right.Style, Equals, "thin") - c.Assert(styles.Borders.Border[1].Top.Style, Equals, "none") - c.Assert(styles.Borders.Border[1].Bottom.Style, Equals, "thin") + c.Assert(styles.Borders.Count, qt.Equals, 1) + c.Assert(styles.Borders.Border[0].Left.Style, qt.Equals, "none") + c.Assert(styles.Borders.Border[0].Right.Style, qt.Equals, "thin") + c.Assert(styles.Borders.Border[0].Top.Style, qt.Equals, "none") + c.Assert(styles.Borders.Border[0].Bottom.Style, qt.Equals, "thin") - c.Assert(styles.CellStyleXfs, IsNil) + c.Assert(styles.CellStyleXfs, qt.IsNil) - c.Assert(styles.CellXfs.Count, Equals, 2) - c.Assert(styles.CellXfs.Xf[0].FontId, Equals, 0) - c.Assert(styles.CellXfs.Xf[0].FillId, Equals, 0) - c.Assert(styles.CellXfs.Xf[0].BorderId, Equals, 0) + c.Assert(styles.CellXfs.Count, qt.Equals, 1) + c.Assert(styles.CellXfs.Xf[0].FontId, qt.Equals, 0) + c.Assert(styles.CellXfs.Xf[0].FillId, qt.Equals, 0) + c.Assert(styles.CellXfs.Xf[0].BorderId, qt.Equals, 0) // Finally we check that the cell points to the right CellXf / // CellStyleXf. - c.Assert(worksheet.SheetData.Row[0].C[0].S, Equals, 1) - c.Assert(worksheet.SheetData.Row[0].C[1].S, Equals, 1) + c.Assert(worksheet.SheetData.Row[0].C[0].S, qt.Equals, 0) + c.Assert(worksheet.SheetData.Row[0].C[1].S, qt.Equals, 0) } // If the column width is not customised, the xslxCol.CustomWidth field is set to 0. @@ -245,23 +247,23 @@ func (s *SheetSuite) TestMakeXLSXSheetDefaultsCustomColWidth(c *C) { refTable := NewSharedStringRefTable() styles := newXlsxStyleSheet(nil) worksheet := sheet.makeXLSXSheet(refTable, styles) - c.Assert(worksheet.Cols.Col[0].CustomWidth, Equals, false) + c.Assert(worksheet.Cols, IsNil) } // If the column width is customised, the xslxCol.CustomWidth field is set to 1. -func (s *SheetSuite) TestMakeXLSXSheetSetsCustomColWidth(c *C) { +func TestMakeXLSXSheetSetsCustomColWidth(t *testing.T) { + c := qt.New(t) file := NewFile() sheet, _ := file.AddSheet("Sheet1") row := sheet.AddRow() cell1 := row.AddCell() cell1.Value = "A cell!" - err := sheet.SetColWidth(0, 1, 10.5) - c.Assert(err, IsNil) + sheet.SetColWidth(1, 1, 10.5) refTable := NewSharedStringRefTable() styles := newXlsxStyleSheet(nil) worksheet := sheet.makeXLSXSheet(refTable, styles) - c.Assert(worksheet.Cols.Col[1].CustomWidth, Equals, true) + c.Assert(worksheet.Cols.Col[0].CustomWidth, qt.Equals, true) } func (s *SheetSuite) TestMarshalSheet(c *C) { @@ -282,7 +284,7 @@ func (s *SheetSuite) TestMarshalSheet(c *C) { c.Assert(err, IsNil) expectedXLSXSheet := ` -0&C&"Times New Roman,Regular"&12&A&C&"Times New Roman,Regular"&12Page &P` +0&C&"Times New Roman,Regular"&12&A&C&"Times New Roman,Regular"&12Page &P` c.Assert(output.String(), Equals, expectedXLSXSheet) } @@ -307,21 +309,40 @@ func (s *SheetSuite) TestMarshalSheetWithMultipleCells(c *C) { c.Assert(err, IsNil) expectedXLSXSheet := ` -01&C&"Times New Roman,Regular"&12&A&C&"Times New Roman,Regular"&12Page &P` +01&C&"Times New Roman,Regular"&12&A&C&"Times New Roman,Regular"&12Page &P` c.Assert(output.String(), Equals, expectedXLSXSheet) } -func (s *SheetSuite) TestSetColWidth(c *C) { +func TestSetColWidth(t *testing.T) { + c := qt.New(t) + file := NewFile() + sheet, _ := file.AddSheet("Sheet1") + sheet.SetColWidth(1, 1, 10.5) + sheet.SetColWidth(2, 6, 11) + c.Assert(sheet.Cols.FindColByIndex(1).Width, qt.Equals, 10.5) + c.Assert(sheet.Cols.FindColByIndex(1).Max, qt.Equals, 1) + c.Assert(sheet.Cols.FindColByIndex(1).Min, qt.Equals, 1) + c.Assert(sheet.Cols.FindColByIndex(2).Width, qt.Equals, float64(11)) + c.Assert(sheet.Cols.FindColByIndex(2).Max, qt.Equals, 6) + c.Assert(sheet.Cols.FindColByIndex(2).Min, qt.Equals, 2) +} + +func TestSetDataValidation(t *testing.T) { + c := qt.New(t) file := NewFile() sheet, _ := file.AddSheet("Sheet1") - _ = sheet.SetColWidth(0, 0, 10.5) - _ = sheet.SetColWidth(1, 5, 11) - c.Assert(sheet.Cols[0].Width, Equals, 10.5) - c.Assert(sheet.Cols[0].Max, Equals, 1) - c.Assert(sheet.Cols[0].Min, Equals, 1) - c.Assert(sheet.Cols[1].Width, Equals, float64(11)) - c.Assert(sheet.Cols[1].Max, Equals, 2) - c.Assert(sheet.Cols[1].Min, Equals, 2) + + dd := NewXlsxCellDataValidation(true) + err := dd.SetDropList([]string{"a1", "a2", "a3"}) + c.Assert(err, qt.IsNil) + + sheet.SetDataValidation(0, 10, dd, 0, 0) + col := sheet.Cols.FindColByIndex(0) + c.Assert(col, notNil) + c.Assert(col.Min, qt.Equals, 0) + c.Assert(col.Max, qt.Equals, 10) + c.Assert(col.DataValidation, qt.HasLen, 1) + c.Assert(col.DataValidation[0], qt.Equals, dd) } func (s *SheetSuite) TestSetRowHeightCM(c *C) { @@ -403,13 +424,14 @@ func (s *SheetSuite) TestAlignment(c *C) { obtained := parts["xl/styles.xml"] shouldbe := ` -` +` expected := bytes.NewBufferString(shouldbe) c.Assert(obtained, Equals, expected.String()) } -func (s *SheetSuite) TestBorder(c *C) { +func TestBorder(t *testing.T) { + c := qt.New(t) file := NewFile() sheet, _ := file.AddSheet("Sheet1") row := sheet.AddRow() @@ -425,15 +447,16 @@ func (s *SheetSuite) TestBorder(c *C) { styles := newXlsxStyleSheet(nil) worksheet := sheet.makeXLSXSheet(refTable, styles) - c.Assert(styles.Borders.Border[1].Left.Style, Equals, "thin") - c.Assert(styles.Borders.Border[1].Right.Style, Equals, "thin") - c.Assert(styles.Borders.Border[1].Top.Style, Equals, "thin") - c.Assert(styles.Borders.Border[1].Bottom.Style, Equals, "thin") + c.Assert(styles.Borders.Border[0].Left.Style, qt.Equals, "thin") + c.Assert(styles.Borders.Border[0].Right.Style, qt.Equals, "thin") + c.Assert(styles.Borders.Border[0].Top.Style, qt.Equals, "thin") + c.Assert(styles.Borders.Border[0].Bottom.Style, qt.Equals, "thin") - c.Assert(worksheet.SheetData.Row[0].C[0].S, Equals, 1) + c.Assert(worksheet.SheetData.Row[0].C[0].S, qt.Equals, 0) } -func (s *SheetSuite) TestOutlineLevels(c *C) { +func TestOutlineLevels(t *testing.T) { + c := qt.New(t) file := NewFile() sheet, _ := file.AddSheet("Sheet1") @@ -458,20 +481,19 @@ func (s *SheetSuite) TestOutlineLevels(c *C) { // Add some groups r1.OutlineLevel = 1 r2.OutlineLevel = 2 - sheet.Col(0).OutlineLevel = 1 + sheet.SetOutlineLevel(1, 1, 1) refTable := NewSharedStringRefTable() styles := newXlsxStyleSheet(nil) worksheet := sheet.makeXLSXSheet(refTable, styles) - c.Assert(worksheet.SheetFormatPr.OutlineLevelCol, Equals, uint8(1)) - c.Assert(worksheet.SheetFormatPr.OutlineLevelRow, Equals, uint8(2)) + c.Assert(worksheet.SheetFormatPr.OutlineLevelCol, qt.Equals, uint8(1)) + c.Assert(worksheet.SheetFormatPr.OutlineLevelRow, qt.Equals, uint8(2)) - c.Assert(worksheet.Cols.Col[0].OutlineLevel, Equals, uint8(1)) - c.Assert(worksheet.Cols.Col[1].OutlineLevel, Equals, uint8(0)) - c.Assert(worksheet.SheetData.Row[0].OutlineLevel, Equals, uint8(1)) - c.Assert(worksheet.SheetData.Row[1].OutlineLevel, Equals, uint8(2)) - c.Assert(worksheet.SheetData.Row[2].OutlineLevel, Equals, uint8(0)) + c.Assert(worksheet.Cols.Col[0].OutlineLevel, qt.Equals, uint8(1)) + c.Assert(worksheet.SheetData.Row[0].OutlineLevel, qt.Equals, uint8(1)) + c.Assert(worksheet.SheetData.Row[1].OutlineLevel, qt.Equals, uint8(2)) + c.Assert(worksheet.SheetData.Row[2].OutlineLevel, qt.Equals, uint8(0)) } func (s *SheetSuite) TestAutoFilter(c *C) { diff --git a/stream_file.go b/stream_file.go index 4186fbb7..27b1c6d0 100644 --- a/stream_file.go +++ b/stream_file.go @@ -59,11 +59,11 @@ func (sf *StreamFile) Write(cells []string) error { // 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 // string style -func (sf *StreamFile) WriteWithColumnDefaultMetadata(cells []string) error { +func (sf *StreamFile) WriteWithColumnDefaultMetadata(cells []string, cellCount int) error { if sf.err != nil { return sf.err } - err := sf.writeWithColumnDefaultMetadata(cells) + err := sf.writeWithColumnDefaultMetadata(cells, cellCount) if err != nil { sf.err = err return err @@ -125,6 +125,7 @@ func (sf *StreamFile) write(cells []string) error { if len(cells) != sf.currentSheet.columnCount { return WrongNumberOfRowsError } + sf.currentSheet.rowCount++ if err := sf.currentSheet.write(``); err != nil { return err @@ -165,43 +166,52 @@ func (sf *StreamFile) write(cells []string) error { return sf.zipWriter.Flush() } -func (sf *StreamFile) writeWithColumnDefaultMetadata(cells []string) error { +func (sf *StreamFile) writeWithColumnDefaultMetadata(cells []string, cellCount int) error { if sf.currentSheet == nil { return NoCurrentSheetError } - if len(cells) != sf.currentSheet.columnCount { - return WrongNumberOfRowsError - } currentSheet := sf.xlsxFile.Sheets[sf.currentSheet.index-1] var streamCells []StreamCell - for colIndex, col := range currentSheet.Cols { + if currentSheet.Cols == nil { + panic("trying to use uninitialised ColStore") + } + + if len(cells) != cellCount { + return WrongNumberOfRowsError + } + + for ci, c := range cells { + col := currentSheet.Col(ci) // 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 col != nil { + 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[col.Min-1], CellTypeString) + if defaultType != nil && *defaultType == cellType { + style = col.GetStreamStyle() + } } streamCells = append( streamCells, NewStreamCell( - cells[colIndex], + c, style, cellType, )) + } return sf.writeS(streamCells) } @@ -211,7 +221,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 +341,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), } diff --git a/stream_file_builder.go b/stream_file_builder.go index 26f010b5..1dd7d4cf 100644 --- a/stream_file_builder.go +++ b/stream_file_builder.go @@ -33,7 +33,6 @@ package xlsx import ( "archive/zip" "errors" - "fmt" "io" "os" "strconv" @@ -127,7 +126,8 @@ 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) } @@ -154,6 +154,7 @@ func (sb *StreamFileBuilder) AddSheetWithDefaultColumnMetadata(name string, head sb.built = true return errors.New("failed to write headers") } + for i, cellMetadata := range columnsDefaultCellMetadata { var cellStyleIndex int var ok bool @@ -169,7 +170,7 @@ func (sb *StreamFileBuilder) AddSheetWithDefaultColumnMetadata(name string, head // Add streamStyle and set default cell metadata on col sb.customStreamStyles[cellMetadata.streamStyle] = struct{}{} - sheet.Cols[i].SetCellMetadata(*cellMetadata) + sheet.SetCellMetadata(i+1, i+1, *cellMetadata) } sb.styleIds[len(sb.styleIds)-1] = append(sb.styleIds[len(sb.styleIds)-1], cellStyleIndex) } @@ -211,14 +212,17 @@ 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") + } // 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 + sheet.SetStreamStyle(colNum, colNum, colStyle) + sheet.SetColWidth(colNum, colNum, 11) } return nil } @@ -262,7 +266,7 @@ func (sb *StreamFileBuilder) Build() (*StreamFile, error) { // 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 ColumnCellMetadataAdded 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 @@ -344,10 +348,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 +382,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, "") + 12 + return data[0:start] + data[end:len(data)] } // splitSheetIntoPrefixAndSuffix will split the provided XML sheet into a prefix and a suffix so that diff --git a/stream_file_builder_test.go b/stream_file_builder_test.go new file mode 100644 index 00000000..2d580af6 --- /dev/null +++ b/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(``) + c.Assert("", qt.Equals, out) + +} diff --git a/stream_style_test.go b/stream_style_test.go index e2443ebe..7341a3ee 100644 --- a/stream_style_test.go +++ b/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,7 +431,7 @@ 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 { @@ -436,7 +448,7 @@ func (s *StreamStyleSuite) TestDates(t *C) { 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 @@ -482,7 +494,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 { @@ -547,7 +559,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 { @@ -606,7 +618,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 +680,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 +710,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 +740,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 +761,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,11 +774,11 @@ 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 { diff --git a/stream_test.go b/stream_test.go index 2f3ee25a..a638673a 100644 --- a/stream_test.go +++ b/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,48 @@ 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") + if testCase.testName != "One Sheet" { + continue } + 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: @@ -365,7 +377,7 @@ func (s *StreamSuite) TestXlsxStreamWriteWithDefaultCellType(t *C) { }, }, { - testName: "Two Sheets with same the name", + testName: "Two Sheets with the same name", sheetNames: []string{ "Sheet 1", "Sheet 1", }, @@ -531,41 +543,58 @@ func (s *StreamSuite) TestXlsxStreamWriteWithDefaultCellType(t *C) { }, } for i, testCase := range testCases { - - 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") + if testCase.testName != "One Sheet, too few columns in row 1" { + 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) + 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 !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 [][]*CellMetadata, workbookData [][][]string) { numSheets := len(workbookCellTypes) numHeaders := len(headerMetadata) @@ -603,7 +632,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 +649,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(``, 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(``, initMaxStyleId)) { + t.Fatal("Expected sheet to have one style") } file = NewFile() @@ -636,19 +664,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(``, 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(``, initMaxStyleId+2)) { t.Fatal("Expected sheet to have four styles") } } @@ -665,8 +694,10 @@ func writeStreamFile(filePath string, fileBuffer io.Writer, sheetNames []string, } else { file = NewStreamFileBuilder(fileBuffer) } + var headerLen []int for i, sheetName := range sheetNames { header := workbookData[i][0] + headerLen = append(headerLen, len(header)) var sheetHeaderTypes []*CellType if i < len(headerTypes) { sheetHeaderTypes = headerTypes[i] @@ -687,6 +718,7 @@ func writeStreamFile(filePath string, fileBuffer io.Writer, sheetNames []string, return err } } + streamFile.currentSheet.columnCount = headerLen[i] for i, row := range sheetData { if i == 0 { continue @@ -716,8 +748,10 @@ func writeStreamFileWithDefaultMetadata(filePath string, fileBuffer io.Writer, s } else { file = NewStreamFileBuilder(fileBuffer) } + var headerLen []int for i, sheetName := range sheetNames { header := workbookData[i][0] + headerLen = append(headerLen, len(header)) var sheetHeaderTypes []*CellMetadata if i < len(headerMetadata) { sheetHeaderTypes = headerMetadata[i] @@ -738,11 +772,12 @@ func writeStreamFileWithDefaultMetadata(filePath string, fileBuffer io.Writer, s return err } } + cellCount := headerLen[i] for i, row := range sheetData { if i == 0 { continue } - err = streamFile.WriteWithColumnDefaultMetadata(row) + err = streamFile.WriteWithColumnDefaultMetadata(row, cellCount) if err != nil { return err } @@ -756,7 +791,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 { @@ -841,7 +876,7 @@ func (s *StreamSuite) TestBuildErrorsAfterBuild(t *C) { } } -func (s *StreamSuite) TestCloseWithNothingWrittenToSheets(t *C) { +func TestCloseWithNothingWrittenToSheets(t *testing.T) { buffer := bytes.NewBuffer(nil) file := NewStreamFileBuilder(buffer) diff --git a/xmlStyle.go b/xmlStyle.go index 5b80fa9d..a508bd14 100644 --- a/xmlStyle.go +++ b/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 diff --git a/xmlWorksheet.go b/xmlWorksheet.go index a989774b..17fb54b0 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -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 @@ -302,7 +304,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"` } From 9589f975db04cae9f94f8978f93311f1ae21799f Mon Sep 17 00:00:00 2001 From: "Geoffrey J. Teale" Date: Tue, 8 Oct 2019 22:21:16 +0200 Subject: [PATCH 08/13] Rename xlsxCellDataValidation to xlsxDataValidation --- cell.go | 4 +- col.go | 85 +++++++++++++++++++++--------------------- col_test.go | 6 +-- data_validation.go | 14 +++---- file_test.go | 2 +- lib.go | 6 +-- sheet.go | 8 ++-- sheet_test.go | 2 +- stream_file_builder.go | 2 +- xmlStyle.go | 5 ++- xmlWorksheet.go | 40 ++++++++++---------- 11 files changed, 87 insertions(+), 87 deletions(-) diff --git a/cell.go b/cell.go index 0cf3f1fd..d07b593e 100644 --- a/cell.go +++ b/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,7 +388,7 @@ 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 } diff --git a/col.go b/col.go index dc01a553..bcf3b7b5 100644 --- a/col.go +++ b/col.go @@ -18,7 +18,6 @@ type Col struct { numFmt string parsedNumFmt *parsedNumberFormat style *Style - DataValidation []*xlsxCellDataValidation defaultCellType *CellType outXfID int } @@ -85,50 +84,50 @@ 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 -} +// // SetDataValidation set data validation with zero based start and end. +// // Set end to -1 for all rows. +// func (c *Col) SetDataValidation(dd *xlsxDataValidation, start, end int) { +// if end < 0 { +// end = Excel2006MaxRowIndex +// } + +// dd.minRow = start +// dd.maxRow = end + +// tmpDD := make([]*xlsxDataValidation, 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(xlsxDataValidation) +// *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) { +func (c *Col) SetDataValidationWithStart(dd *xlsxDataValidation, start int) { c.SetDataValidation(dd, start, -1) } @@ -168,7 +167,7 @@ func (c *Col) copyToRange(min, max int) *Col { numFmt: c.numFmt, parsedNumFmt: c.parsedNumFmt, style: c.style, - DataValidation: append([]*xlsxCellDataValidation{}, c.DataValidation...), + DataValidation: append([]*xlsxDataValidation{}, c.DataValidation...), defaultCellType: c.defaultCellType, } } diff --git a/col_test.go b/col_test.go index a7cd439d..b94badce 100644 --- a/col_test.go +++ b/col_test.go @@ -55,8 +55,8 @@ func TestCol(t *testing.T) { c.Run("copyToRange", func(c *qt.C) { nf := &parsedNumberFormat{} s := &Style{} - cdv1 := &xlsxCellDataValidation{} - cdv2 := &xlsxCellDataValidation{} + cdv1 := &xlsxDataValidation{} + cdv2 := &xlsxDataValidation{} ct := CellTypeBool.Ptr() c1 := &Col{ Min: 1, @@ -68,7 +68,7 @@ func TestCol(t *testing.T) { numFmt: "-0.00", parsedNumFmt: nf, style: s, - DataValidation: []*xlsxCellDataValidation{cdv1, cdv2}, + DataValidation: []*xlsxDataValidation{cdv1, cdv2}, defaultCellType: ct, } diff --git a/data_validation.go b/data_validation.go index dc4a2599..bfcedd63 100644 --- a/data_validation.go +++ b/data_validation.go @@ -62,14 +62,14 @@ const ( ) // NewXlsxCellDataValidation return data validation struct -func NewXlsxCellDataValidation(allowBlank bool) *xlsxCellDataValidation { - return &xlsxCellDataValidation{ +func NewXlsxCellDataValidation(allowBlank bool) *xlsxDataValidation { + return &xlsxDataValidation{ AllowBlank: allowBlank, } } // 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 +87,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 +95,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 +111,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 +128,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) diff --git a/file_test.go b/file_test.go index a666ce2f..d2274223 100644 --- a/file_test.go +++ b/file_test.go @@ -847,7 +847,7 @@ func (l *FileSuite) TestMarshalFile(c *C) { // For now we only allow simple string data in the // spreadsheet. Style support will follow. expectedStyles := ` -` +` c.Assert(parts["xl/styles.xml"], Equals, expectedStyles) } diff --git a/lib.go b/lib.go index 5144fe40..1d6f6bb7 100644 --- a/lib.go +++ b/lib.go @@ -735,14 +735,14 @@ func readSheetFromFile(sc chan *indexedSheet, index int, rsheet xlsxSheet, fi *F } if minCol == maxCol && minRow == maxRow { - newDD := new(xlsxCellDataValidation) + newDD := new(xlsxDataValidation) *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 := new(xlsxDataValidation) *newDD = *dd newDD.Sqref = "" sheet.Col(i).SetDataValidation(dd, minRow, maxRow) @@ -750,7 +750,7 @@ func readSheetFromFile(sc chan *indexedSheet, index int, rsheet xlsxSheet, fi *F } } else { - newDD := new(xlsxCellDataValidation) + newDD := new(xlsxDataValidation) *newDD = *dd newDD.Sqref = "" sheet.Cell(minRow, minCol).SetDataValidation(dd) diff --git a/sheet.go b/sheet.go index 44c8b103..c42569e7 100644 --- a/sheet.go +++ b/sheet.go @@ -197,14 +197,14 @@ func (s *Sheet) SetColWidth(min, max int, width float64) { } // Set the data validation for a range of columns. -func (s *Sheet) SetDataValidation(minCol, maxCol int, dd *xlsxCellDataValidation, minRow, maxRow int) { +func (s *Sheet) SetDataValidation(minCol, maxCol int, dd *xlsxDataValidation, minRow, maxRow int) { s.setCol(minCol, maxCol, func(col *Col) { col.SetDataValidation(dd, minRow, maxRow) }) } // Set the data validation for a range of columns. -func (s *Sheet) SetDataValidationWithStart(minCol, maxCol int, dd *xlsxCellDataValidation, start int) { +func (s *Sheet) SetDataValidationWithStart(minCol, maxCol int, dd *xlsxDataValidation, start int) { s.setCol(minCol, maxCol, func(col *Col) { col.SetDataValidation(dd, start, -1) }) @@ -343,7 +343,7 @@ func (s *Sheet) makeCols(worksheet *xlsxWorksheet, styles *xlsxStyleSheet) (maxL } if nil != col.DataValidation { if nil == worksheet.DataValidations { - worksheet.DataValidations = &xlsxCellDataValidations{} + worksheet.DataValidations = &xlsxDataValidations{} } colName := ColIndexToLetters(c) for _, dd := range col.DataValidation { @@ -447,7 +447,7 @@ func (s *Sheet) makeRows(worksheet *xlsxWorksheet, styles *xlsxStyleSheet, refTa 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) diff --git a/sheet_test.go b/sheet_test.go index 2ab7d771..7f2528de 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -424,7 +424,7 @@ func (s *SheetSuite) TestAlignment(c *C) { obtained := parts["xl/styles.xml"] shouldbe := ` -` +` expected := bytes.NewBufferString(shouldbe) c.Assert(obtained, Equals, expected.String()) diff --git a/stream_file_builder.go b/stream_file_builder.go index 1dd7d4cf..cee119f6 100644 --- a/stream_file_builder.go +++ b/stream_file_builder.go @@ -228,7 +228,7 @@ func (sb *StreamFileBuilder) AddSheetS(name string, columnStyles []StreamStyle) } // AddValidation will add a validation to a specific column. -func (sb *StreamFileBuilder) AddValidation(sheetIndex, colIndex, rowStartIndex int, validation *xlsxCellDataValidation) { +func (sb *StreamFileBuilder) AddValidation(sheetIndex, colIndex, rowStartIndex int, validation *xlsxDataValidation) { sheet := sb.xlsxFile.Sheets[sheetIndex] column := sheet.Col(colIndex) column.SetDataValidationWithStart(validation, rowStartIndex) diff --git a/xmlStyle.go b/xmlStyle.go index a508bd14..9fa7af5a 100644 --- a/xmlStyle.go +++ b/xmlStyle.go @@ -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{} } diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 17fb54b0..6f1c6674 100644 --- a/xmlWorksheet.go +++ b/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 @@ -226,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"` From b27fdc7e50f8fcd257502bd8f6e47390d3e4da4e Mon Sep 17 00:00:00 2001 From: "Geoffrey J. Teale" Date: Thu, 10 Oct 2019 04:36:07 +0200 Subject: [PATCH 09/13] Disassociate CellMetadata, StreamStyle and DefaultCellType from Col --- cell.go | 20 +++--- col.go | 107 ++++++++----------------------- col_test.go | 27 +++----- data_validation.go | 14 +++- data_validation_test.go | 138 ++++++++++++++++++++-------------------- lib.go | 77 +++++++++++----------- sheet.go | 97 ++++++++++++---------------- sheet_test.go | 12 ++-- stream_file.go | 68 ++++++++++++-------- stream_file_builder.go | 121 ++++++++++++++++++----------------- stream_style.go | 10 +-- stream_style_test.go | 20 ++++-- stream_test.go | 114 ++++++++++++++++++--------------- xmlWorksheet.go | 5 -- 14 files changed, 397 insertions(+), 433 deletions(-) diff --git a/cell.go b/cell.go index d07b593e..84fce4c7 100644 --- a/cell.go +++ b/cell.go @@ -392,27 +392,27 @@ 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 } diff --git a/col.go b/col.go index bcf3b7b5..72a079ab 100644 --- a/col.go +++ b/col.go @@ -6,20 +6,19 @@ const Excel2006MaxRowCount = 1048576 const Excel2006MaxRowIndex = Excel2006MaxRowCount - 1 type Col struct { - Min int - Max int - Hidden bool - Width float64 - Collapsed bool - OutlineLevel uint8 - BestFit bool - CustomWidth bool - Phonetic bool - numFmt string - parsedNumFmt *parsedNumberFormat - style *Style - defaultCellType *CellType - outXfID int + 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 @@ -67,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 @@ -84,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 *xlsxDataValidation, start, end int) { -// if end < 0 { -// end = Excel2006MaxRowIndex -// } - -// dd.minRow = start -// dd.maxRow = end - -// tmpDD := make([]*xlsxDataValidation, 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(xlsxDataValidation) -// *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 *xlsxDataValidation, 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 @@ -155,20 +100,18 @@ func (c *Col) SetOutlineLevel(outlineLevel uint8) { // 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, - DataValidation: append([]*xlsxDataValidation{}, c.DataValidation...), - defaultCellType: c.defaultCellType, + 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, } } diff --git a/col_test.go b/col_test.go index b94badce..b5b16124 100644 --- a/col_test.go +++ b/col_test.go @@ -55,21 +55,16 @@ func TestCol(t *testing.T) { c.Run("copyToRange", func(c *qt.C) { nf := &parsedNumberFormat{} s := &Style{} - cdv1 := &xlsxDataValidation{} - cdv2 := &xlsxDataValidation{} - ct := CellTypeBool.Ptr() c1 := &Col{ - Min: 1, - Max: 11, - Hidden: true, - Width: 300.4, - Collapsed: true, - OutlineLevel: 2, - numFmt: "-0.00", - parsedNumFmt: nf, - style: s, - DataValidation: []*xlsxDataValidation{cdv1, cdv2}, - defaultCellType: ct, + 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) @@ -82,10 +77,6 @@ func TestCol(t *testing.T) { c.Assert(c2.numFmt, qt.Equals, c1.numFmt) c.Assert(c2.parsedNumFmt, qt.Equals, c1.parsedNumFmt) c.Assert(c2.style, qt.Equals, c1.style) - c.Assert(c2.DataValidation, qt.HasLen, 2) - c.Assert(c2.DataValidation[0], qt.Equals, c1.DataValidation[0]) - c.Assert(c2.DataValidation[1], qt.Equals, c1.DataValidation[1]) - c.Assert(c2.defaultCellType, qt.Equals, c1.defaultCellType) }) } diff --git a/data_validation.go b/data_validation.go index bfcedd63..4f782e25 100644 --- a/data_validation.go +++ b/data_validation.go @@ -61,10 +61,20 @@ const ( DataValidationOperatorNotEqual ) -// NewXlsxCellDataValidation return data validation struct -func NewXlsxCellDataValidation(allowBlank bool) *xlsxDataValidation { +// 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, } } diff --git a/data_validation_test.go b/data_validation_test.go index 05e01df3..ed6ec9c9 100644 --- a/data_validation_test.go +++ b/data_validation_test.go @@ -28,207 +28,205 @@ func TestDataValidation(t *testing.T) { cell = row.AddCell() cell.Value = "a1" - dd := NewXlsxCellDataValidation(true) + dd := NewDataValidation(0, 0, 0, 0, true) err = dd.SetDropList([]string{"a1", "a2", "a3"}) 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"}) c.Assert(err, qt.IsNil) title = "col c" dd.SetInput(&title, &msg) - sheet.SetDataValidation(2, 2, dd, 0, 0) + sheet.AddDataValidation(dd) - dd = NewXlsxCellDataValidation(true) + dd = NewDataValidation(3, 3, 3, 7, true) err = dd.SetDropList([]string{"d", "d1", "d2"}) c.Assert(err, qt.IsNil) title = "col d range" dd.SetInput(&title, &msg) - sheet.SetDataValidation(3, 3, dd, 3, 7) + sheet.AddDataValidation(dd) - dd = NewXlsxCellDataValidation(true) + dd = NewDataValidation(4, 1, 4, Excel2006MaxRowIndex, true) err = dd.SetDropList([]string{"e1", "e2", "e3"}) c.Assert(err, qt.IsNil) title = "col e start 3" dd.SetInput(&title, &msg) - sheet.SetDataValidationWithStart(4, 4, 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) c.Assert(err, qt.IsNil) - sheet.Cell(rowIndex, index).SetDataValidation(dd) + sheet.AddDataValidation(dd) index++ - dd = NewXlsxCellDataValidation(true) + dd = NewDataValidation(rowIndex, index, rowIndex, index, true) err = dd.SetRange(10, 1, DataValidationTypeTextLeng, DataValidationOperatorEqual) c.Assert(err, qt.IsNil) - sheet.Cell(rowIndex, index).SetDataValidation(dd) + sheet.AddDataValidation(dd) index++ - dd = NewXlsxCellDataValidation(true) + dd = NewDataValidation(rowIndex, index, rowIndex, index, true) err = dd.SetRange(10, 1, DataValidationTypeTextLeng, DataValidationOperatorGreaterThanOrEqual) c.Assert(err, qt.IsNil) - sheet.Cell(rowIndex, index).SetDataValidation(dd) + sheet.AddDataValidation(dd) index++ - dd = NewXlsxCellDataValidation(true) + dd = NewDataValidation(rowIndex, index, rowIndex, index, true) err = dd.SetRange(10, 1, DataValidationTypeTextLeng, DataValidationOperatorGreaterThan) c.Assert(err, qt.IsNil) - sheet.Cell(rowIndex, index).SetDataValidation(dd) + sheet.AddDataValidation(dd) index++ - dd = NewXlsxCellDataValidation(true) + dd = NewDataValidation(rowIndex, index, rowIndex, index, true) err = dd.SetRange(10, 1, DataValidationTypeTextLeng, DataValidationOperatorLessThan) c.Assert(err, qt.IsNil) - sheet.Cell(rowIndex, index).SetDataValidation(dd) + sheet.AddDataValidation(dd) index++ - dd = NewXlsxCellDataValidation(true) + dd = NewDataValidation(rowIndex, index, rowIndex, index, true) err = dd.SetRange(10, 1, DataValidationTypeTextLeng, DataValidationOperatorLessThanOrEqual) c.Assert(err, qt.IsNil) - sheet.Cell(rowIndex, index).SetDataValidation(dd) + sheet.AddDataValidation(dd) index++ - dd = NewXlsxCellDataValidation(true) + dd = NewDataValidation(rowIndex, index, rowIndex, index, true) err = dd.SetRange(10, 1, DataValidationTypeTextLeng, DataValidationOperatorNotEqual) c.Assert(err, qt.IsNil) - sheet.Cell(rowIndex, index).SetDataValidation(dd) + sheet.AddDataValidation(dd) index++ - dd = NewXlsxCellDataValidation(true) + dd = NewDataValidation(rowIndex, index, rowIndex, index, true) err = dd.SetRange(10, 1, DataValidationTypeTextLeng, DataValidationOperatorNotBetween) c.Assert(err, qt.IsNil) - sheet.Cell(rowIndex, index).SetDataValidation(dd) + sheet.AddDataValidation(dd) index++ rowIndex++ index = 5 - dd = NewXlsxCellDataValidation(true) + dd = NewDataValidation(rowIndex, index, rowIndex, index, true) err = dd.SetRange(4, 15, DataValidationTypeWhole, DataValidationOperatorBetween) c.Assert(err, qt.IsNil) - sheet.Cell(rowIndex, index).SetDataValidation(dd) + sheet.AddDataValidation(dd) index++ - dd = NewXlsxCellDataValidation(true) + dd = NewDataValidation(rowIndex, index, rowIndex, index, true) err = dd.SetRange(10, 1, DataValidationTypeWhole, DataValidationOperatorEqual) c.Assert(err, qt.IsNil) - sheet.Cell(rowIndex, index).SetDataValidation(dd) + sheet.AddDataValidation(dd) index++ - dd = NewXlsxCellDataValidation(true) + dd = NewDataValidation(rowIndex, index, rowIndex, index, true) err = dd.SetRange(10, 1, DataValidationTypeWhole, DataValidationOperatorGreaterThanOrEqual) c.Assert(err, qt.IsNil) - sheet.Cell(rowIndex, index).SetDataValidation(dd) + sheet.AddDataValidation(dd) index++ - dd = NewXlsxCellDataValidation(true) + dd = NewDataValidation(rowIndex, index, rowIndex, index, true) err = dd.SetRange(10, 1, DataValidationTypeWhole, DataValidationOperatorGreaterThan) c.Assert(err, qt.IsNil) - sheet.Cell(rowIndex, index).SetDataValidation(dd) + sheet.AddDataValidation(dd) index++ - dd = NewXlsxCellDataValidation(true) + dd = NewDataValidation(rowIndex, index, rowIndex, index, true) err = dd.SetRange(10, 1, DataValidationTypeWhole, DataValidationOperatorLessThan) c.Assert(err, qt.IsNil) - sheet.Cell(rowIndex, index).SetDataValidation(dd) + sheet.AddDataValidation(dd) index++ - dd = NewXlsxCellDataValidation(true) + dd = NewDataValidation(rowIndex, index, rowIndex, index, true) err = dd.SetRange(10, 1, DataValidationTypeWhole, DataValidationOperatorLessThanOrEqual) c.Assert(err, qt.IsNil) - sheet.Cell(rowIndex, index).SetDataValidation(dd) + sheet.AddDataValidation(dd) index++ - dd = NewXlsxCellDataValidation(true) + dd = NewDataValidation(rowIndex, index, rowIndex, index, true) err = dd.SetRange(10, 1, DataValidationTypeWhole, DataValidationOperatorNotEqual) c.Assert(err, qt.IsNil) - sheet.Cell(rowIndex, index).SetDataValidation(dd) + 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"}) c.Assert(err, qt.IsNil) - dd1 := NewXlsxCellDataValidation(true) + dd1 := NewDataValidation(12, 3, 12, 4, true) err = dd1.SetDropList([]string{"11", "22", "44"}) c.Assert(err, qt.IsNil) - dd2 := NewXlsxCellDataValidation(true) + dd2 := NewDataValidation(12, 5, 12, 7, true) err = dd2.SetDropList([]string{"111", "222", "444"}) c.Assert(err, qt.IsNil) - sheet.SetDataValidation(12, 12, dd, 2, 10) - sheet.SetDataValidation(12, 12, dd1, 3, 4) - sheet.SetDataValidation(12, 12, dd2, 5, 7) + 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"}) c.Assert(err, qt.IsNil) - dd1 = NewXlsxCellDataValidation(true) + dd1 = NewDataValidation(13, 1, 13, 2, true) err = dd1.SetDropList([]string{"11", "22", "44"}) c.Assert(err, qt.IsNil) - sheet.SetDataValidation(13, 13, dd, 2, 10) - sheet.SetDataValidation(13, 13, dd1, 1, 2) + sheet.AddDataValidation(dd) + sheet.AddDataValidation(dd1) - dd = NewXlsxCellDataValidation(true) + dd = NewDataValidation(14, 2, 14, 10, true) err = dd.SetDropList([]string{"1", "2", "4"}) c.Assert(err, qt.IsNil) - dd1 = NewXlsxCellDataValidation(true) + dd1 = NewDataValidation(14, 1, 14, 5, true) err = dd1.SetDropList([]string{"11", "22", "44"}) c.Assert(err, qt.IsNil) - sheet.SetDataValidation(14, 14, dd, 2, 10) - sheet.SetDataValidation(14, 14, dd1, 1, 5) + 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"}) c.Assert(err, qt.IsNil) - sheet.SetDataValidation(15, 15, dd, 2, 10) - sheet.SetDataValidation(15, 15, dd1, 1, 10) + sheet.AddDataValidation(dd) + sheet.AddDataValidation(dd1) - dd = NewXlsxCellDataValidation(true) + dd = NewDataValidation(16, 10, 16, 20, true) err = dd.SetDropList([]string{"1", "2", "4"}) c.Assert(err, qt.IsNil) - dd1 = NewXlsxCellDataValidation(true) + dd1 = NewDataValidation(16, 2, 16, 4, true) err = dd1.SetDropList([]string{"11", "22", "44"}) c.Assert(err, qt.IsNil) - dd2 = NewXlsxCellDataValidation(true) + dd2 = NewDataValidation(16, 12, 16, 30, true) err = dd2.SetDropList([]string{"111", "222", "444"}) c.Assert(err, qt.IsNil) - sheet.SetDataValidation(16, 16, dd, 10, 20) - sheet.SetDataValidation(16, 16, dd1, 2, 4) - sheet.SetDataValidation(16, 16, dd2, 21, 30) + 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"}) c.Assert(err, qt.IsNil) title = "col d range" dd.SetInput(&title, &msg) - sheet.SetDataValidation(3, 3, dd, 3, Excel2006MaxRowIndex) + sheet.AddDataValidation(dd) - dd = NewXlsxCellDataValidation(true) + dd = NewDataValidation(3, 4, 3, Excel2006MaxRowIndex, true) err = dd.SetDropList([]string{"d", "d1", "d2"}) c.Assert(err, qt.IsNil) title = "col d range" dd.SetInput(&title, &msg) - sheet.SetDataValidation(3, 3, dd, 4, -1) - maxRow := sheet.Col(3).DataValidation[len(sheet.Col(3).DataValidation)-1].maxRow - c.Assert(maxRow, qt.Equals, Excel2006MaxRowIndex) + sheet.AddDataValidation(dd) dest := &bytes.Buffer{} err = file.Write(dest) @@ -244,7 +242,7 @@ func TestDataValidation(t *testing.T) { 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) + dd := NewDataValidation(0, 0, 0, 0, true) c.Assert(dd.ShowErrorMessage, qt.Equals, false) c.Assert(dd.ShowInputMessage, qt.Equals, false) diff --git a/lib.go b/lib.go index 1d6f6bb7..4302ac5e 100644 --- a/lib.go +++ b/lib.go @@ -719,44 +719,45 @@ 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(xlsxDataValidation) - *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(xlsxDataValidation) - *newDD = *dd - newDD.Sqref = "" - sheet.Col(i).SetDataValidation(dd, minRow, maxRow) - } - - } - } else { - newDD := new(xlsxDataValidation) - *newDD = *dd - newDD.Sqref = "" - sheet.Cell(minRow, minCol).SetDataValidation(dd) - - } - } + sheet.AddDataValidation(dd) + // 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(xlsxDataValidation) + // *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(xlsxDataValidation) + // *newDD = *dd + // newDD.Sqref = "" + // sheet.Col(i).SetDataValidation(dd, minRow, maxRow) + // } + + // } + // } else { + // newDD := new(xlsxDataValidation) + // *newDD = *dd + // newDD.Sqref = "" + // sheet.Cell(minRow, minCol).SetDataValidation(dd) + + // } + // } } diff --git a/sheet.go b/sheet.go index c42569e7..135f170c 100644 --- a/sheet.go +++ b/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 *ColStore - 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) { @@ -196,20 +202,6 @@ func (s *Sheet) SetColWidth(min, max int, width float64) { }) } -// Set the data validation for a range of columns. -func (s *Sheet) SetDataValidation(minCol, maxCol int, dd *xlsxDataValidation, minRow, maxRow int) { - s.setCol(minCol, maxCol, func(col *Col) { - col.SetDataValidation(dd, minRow, maxRow) - }) -} - -// Set the data validation for a range of columns. -func (s *Sheet) SetDataValidationWithStart(minCol, maxCol int, dd *xlsxDataValidation, start int) { - s.setCol(minCol, maxCol, func(col *Col) { - col.SetDataValidation(dd, start, -1) - }) -} - // 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) { @@ -225,20 +217,6 @@ func (s *Sheet) SetType(minCol, maxCol int, cellType CellType) { } -// Set the cell metadata for a range of columns. -func (s *Sheet) SetCellMetadata(minCol, maxCol int, cm CellMetadata) { - s.setCol(minCol, maxCol, func(col *Col) { - col.SetCellMetadata(cm) - }) -} - -// Set the stream style for a range of columns. -func (s *Sheet) SetStreamStyle(minCol, maxCol int, ss StreamStyle) { - s.setCol(minCol, maxCol, func(col *Col) { - col.SetStreamStyle(ss) - }) -} - // When merging cells, the cell may be the 'original' or the 'covered'. // First, figure out which cells are merge starting points. Then create // the necessary cells underlying the merge area. @@ -341,24 +319,20 @@ func (s *Sheet) makeCols(worksheet *xlsxWorksheet, styles *xlsxStyleSheet) (maxL if col.OutlineLevel > maxLevelCol { maxLevelCol = col.OutlineLevel } - if nil != col.DataValidation { - if nil == worksheet.DataValidations { - worksheet.DataValidations = &xlsxDataValidations{} - } - 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) - - } - worksheet.DataValidations.Count = len(worksheet.DataValidations.DataValidation) - } + // if nil != col.DataValidation { + // if nil == worksheet.DataValidations { + // worksheet.DataValidations = &xlsxDataValidations{} + // } + // 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) + // } }) + return maxLevelCol } @@ -494,6 +468,18 @@ func (s *Sheet) makeRows(worksheet *xlsxWorksheet, styles *xlsxStyleSheet, refTa } +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() @@ -506,6 +492,7 @@ func (s *Sheet) makeXLSXSheet(refTable *RefTable, styles *xlsxStyleSheet) *xlsxW s.makeSheetView(worksheet) s.makeSheetFormatPr(worksheet) maxLevelCol := s.makeCols(worksheet, styles) + s.makeDataValidations(worksheet) s.makeRows(worksheet, styles, refTable, maxLevelCol) return worksheet diff --git a/sheet_test.go b/sheet_test.go index 7f2528de..c688327d 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -332,17 +332,13 @@ func TestSetDataValidation(t *testing.T) { file := NewFile() sheet, _ := file.AddSheet("Sheet1") - dd := NewXlsxCellDataValidation(true) + dd := NewDataValidation(0, 0, 10, 0, true) err := dd.SetDropList([]string{"a1", "a2", "a3"}) c.Assert(err, qt.IsNil) - sheet.SetDataValidation(0, 10, dd, 0, 0) - col := sheet.Cols.FindColByIndex(0) - c.Assert(col, notNil) - c.Assert(col.Min, qt.Equals, 0) - c.Assert(col.Max, qt.Equals, 10) - c.Assert(col.DataValidation, qt.HasLen, 1) - c.Assert(col.DataValidation[0], qt.Equals, dd) + sheet.AddDataValidation(dd) + c.Assert(sheet.DataValidations, qt.HasLen, 1) + c.Assert(sheet.DataValidations[0], qt.Equals, dd) } func (s *SheetSuite) TestSetRowHeightCM(c *C) { diff --git a/stream_file.go b/stream_file.go index 27b1c6d0..115c1e61 100644 --- a/stream_file.go +++ b/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,14 +59,14 @@ 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, cellCount int) error { +func (sf *StreamFile) WriteWithColumnDefaultMetadata(cells []string) error { if sf.err != nil { return sf.err } - err := sf.writeWithColumnDefaultMetadata(cells, cellCount) + err := sf.writeWithColumnDefaultMetadata(cells) if err != nil { sf.err = err return err @@ -122,8 +125,12 @@ 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++ @@ -166,13 +173,14 @@ func (sf *StreamFile) write(cells []string) error { return sf.zipWriter.Flush() } -func (sf *StreamFile) writeWithColumnDefaultMetadata(cells []string, cellCount int) error { +func (sf *StreamFile) writeWithColumnDefaultMetadata(cells []string) error { if sf.currentSheet == nil { return NoCurrentSheetError } - currentSheet := sf.xlsxFile.Sheets[sf.currentSheet.index-1] + sheetIndex := sf.currentSheet.index - 1 + currentSheet := sf.xlsxFile.Sheets[sheetIndex] var streamCells []StreamCell @@ -180,12 +188,17 @@ func (sf *StreamFile) writeWithColumnDefaultMetadata(cells []string, cellCount i panic("trying to use uninitialised ColStore") } - if len(cells) != cellCount { - return WrongNumberOfRowsError + 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 { - col := currentSheet.Col(ci) // 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 @@ -194,14 +207,17 @@ func (sf *StreamFile) writeWithColumnDefaultMetadata(cells []string, cellCount i // parse into the default cell type and if parsing fails fall back // to some sensible default cellType := CellTypeInline - if col != nil { - 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[col.Min-1], CellTypeString) - if defaultType != nil && *defaultType == cellType { - style = col.GetStreamStyle() + 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( diff --git a/stream_file_builder.go b/stream_file_builder.go index cee119f6..980f3516 100644 --- a/stream_file_builder.go +++ b/stream_file_builder.go @@ -39,18 +39,24 @@ import ( "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 ( @@ -69,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), } } @@ -91,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. @@ -105,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 @@ -134,43 +136,39 @@ func (sb *StreamFileBuilder) AddSheet(name string, headers []string, cellTypes [ 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") - } + sheetIndex := len(sb.xlsxFile.Sheets) - 1 - for i, cellMetadata := range columnsDefaultCellMetadata { + 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.SetCellMetadata(i+1, i+1, *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) } @@ -180,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 } @@ -216,22 +216,24 @@ func (sb *StreamFileBuilder) AddSheetS(name string, columnStyles []StreamStyle) 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 { colNum := i + 1 - sheet.SetStreamStyle(colNum, colNum, colStyle) + 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 *xlsxDataValidation) { +// 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 @@ -255,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.default ColumnCellMetadataAdded 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 diff --git a/stream_style.go b/stream_style.go index 88db52de..74171dd9 100644 --- a/stream_style.go +++ b/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. diff --git a/stream_style_test.go b/stream_style_test.go index 7341a3ee..14a4c829 100644 --- a/stream_style_test.go +++ b/stream_style_test.go @@ -438,11 +438,13 @@ func TestStreamStyleDates(t *testing.T) { 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)}, }, } @@ -473,7 +475,7 @@ func TestStreamStyleDates(t *testing.T) { } 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 @@ -482,11 +484,21 @@ func TestStreamStyleDates(t *testing.T) { 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 { diff --git a/stream_test.go b/stream_test.go index a638673a..81f89150 100644 --- a/stream_test.go +++ b/stream_test.go @@ -235,9 +235,9 @@ func TestXlsxStreamWrite(t *testing.T) { }, } for i, testCase := range testCases { - if testCase.testName != "One Sheet" { - continue - } + // if testCase.testName != "One Sheet" { + // continue + // } t.Run(testCase.testName, func(t *testing.T) { var filePath string var buffer bytes.Buffer @@ -286,7 +286,7 @@ func TestXlsxStreamWriteWithDefaultCellType(t *testing.T) { sheetNames []string workbookData [][][]string expectedWorkbookData [][][]string - headerTypes [][]*CellMetadata + headerTypes [][]*StreamingCellMetadata expectedError error }{ { @@ -308,8 +308,8 @@ func TestXlsxStreamWriteWithDefaultCellType(t *testing.T) { {"123", "Taco", "string", "0000000123"}, }, }, - headerTypes: [][]*CellMetadata{ - {DefaultStringCellMetadata.Ptr(), DefaultStringCellMetadata.Ptr(), DefaultDecimalCellMetadata.Ptr(), DefaultStringCellMetadata.Ptr()}, + headerTypes: [][]*StreamingCellMetadata{ + {DefaultStringStreamingCellMetadata.Ptr(), DefaultStringStreamingCellMetadata.Ptr(), DefaultDecimalStreamingCellMetadata.Ptr(), DefaultStringStreamingCellMetadata.Ptr()}, }, }, { @@ -329,8 +329,8 @@ func TestXlsxStreamWriteWithDefaultCellType(t *testing.T) { {"1234.00"}, }, }, - headerTypes: [][]*CellMetadata{ - {DefaultDecimalCellMetadata.Ptr()}, + headerTypes: [][]*StreamingCellMetadata{ + {DefaultDecimalStreamingCellMetadata.Ptr()}, }, }, { @@ -370,9 +370,9 @@ func TestXlsxStreamWriteWithDefaultCellType(t *testing.T) { {"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}, }, }, @@ -453,14 +453,25 @@ func TestXlsxStreamWriteWithDefaultCellType(t *testing.T) { {{}}, {{}}, }, - 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", @@ -474,8 +485,16 @@ func TestXlsxStreamWriteWithDefaultCellType(t *testing.T) { }, {{}}, }, - 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}, }, }, @@ -513,8 +532,8 @@ func TestXlsxStreamWriteWithDefaultCellType(t *testing.T) { {"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()}, }, }, { @@ -537,15 +556,15 @@ func TestXlsxStreamWriteWithDefaultCellType(t *testing.T) { {"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 != "One Sheet, too few columns in row 1" { - continue - } + // 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 @@ -579,6 +598,9 @@ func TestXlsxStreamWriteWithDefaultCellType(t *testing.T) { 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) @@ -594,7 +616,7 @@ func TestXlsxStreamWriteWithDefaultCellType(t *testing.T) { // 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 *testing.T, workbookCellTypes [][][]CellType, headerMetadata [][]*CellMetadata, workbookData [][][]string) { +func verifyCellTypesInColumnMatchHeaderType(t *testing.T, workbookCellTypes [][][]CellType, headerMetadata [][]*StreamingCellMetadata, workbookData [][][]string) { numSheets := len(workbookCellTypes) numHeaders := len(headerMetadata) @@ -605,7 +627,7 @@ func verifyCellTypesInColumnMatchHeaderType(t *testing.T, workbookCellTypes [][] 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) @@ -694,15 +716,12 @@ func writeStreamFile(filePath string, fileBuffer io.Writer, sheetNames []string, } else { file = NewStreamFileBuilder(fileBuffer) } - var headerLen []int for i, sheetName := range sheetNames { - header := workbookData[i][0] - headerLen = append(headerLen, len(header)) 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 } @@ -718,11 +737,7 @@ func writeStreamFile(filePath string, fileBuffer io.Writer, sheetNames []string, return err } } - streamFile.currentSheet.columnCount = headerLen[i] - for i, row := range sheetData { - if i == 0 { - continue - } + for _, row := range sheetData { err = streamFile.Write(row) if err != nil { return err @@ -737,7 +752,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 { @@ -748,15 +763,13 @@ func writeStreamFileWithDefaultMetadata(filePath string, fileBuffer io.Writer, s } else { file = NewStreamFileBuilder(fileBuffer) } - var headerLen []int + for i, sheetName := range sheetNames { - header := workbookData[i][0] - headerLen = append(headerLen, len(header)) - 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 } @@ -772,12 +785,9 @@ func writeStreamFileWithDefaultMetadata(filePath string, fileBuffer io.Writer, s return err } } - cellCount := headerLen[i] - for i, row := range sheetData { - if i == 0 { - continue - } - err = streamFile.WriteWithColumnDefaultMetadata(row, cellCount) + + for _, row := range sheetData { + err = streamFile.WriteWithColumnDefaultMetadata(row) if err != nil { return err } @@ -835,11 +845,11 @@ func readXLSXFile(t *testing.T, filePath string, fileBuffer io.ReaderAt, size in 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) } @@ -848,7 +858,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) } @@ -857,11 +867,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) } @@ -885,11 +895,11 @@ func TestCloseWithNothingWrittenToSheets(t *testing.T) { {{"Header1", "Header2"}}, {{"Header3", "Header4"}}, } - err := file.AddSheet(sheetNames[0], workbookData[0][0], nil) + 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) } diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 6f1c6674..30774810 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -274,11 +274,6 @@ type xlsxDataValidation 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 From 8af32a69f77323e743a4039dc2994ad57841bd2a Mon Sep 17 00:00:00 2001 From: "Geoffrey J. Teale" Date: Thu, 10 Oct 2019 13:41:15 +0200 Subject: [PATCH 10/13] Remove commented out code --- lib.go | 39 --------------------------------------- sheet.go | 12 ------------ stream_test.go | 3 --- 3 files changed, 54 deletions(-) diff --git a/lib.go b/lib.go index 4302ac5e..5613a9eb 100644 --- a/lib.go +++ b/lib.go @@ -720,45 +720,6 @@ func readSheetFromFile(sc chan *indexedSheet, index int, rsheet xlsxSheet, fi *F if nil != worksheet.DataValidations { for _, dd := range worksheet.DataValidations.DataValidation { sheet.AddDataValidation(dd) - // 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(xlsxDataValidation) - // *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(xlsxDataValidation) - // *newDD = *dd - // newDD.Sqref = "" - // sheet.Col(i).SetDataValidation(dd, minRow, maxRow) - // } - - // } - // } else { - // newDD := new(xlsxDataValidation) - // *newDD = *dd - // newDD.Sqref = "" - // sheet.Cell(minRow, minCol).SetDataValidation(dd) - - // } - // } - } } diff --git a/sheet.go b/sheet.go index 135f170c..10abdde0 100644 --- a/sheet.go +++ b/sheet.go @@ -319,18 +319,6 @@ func (s *Sheet) makeCols(worksheet *xlsxWorksheet, styles *xlsxStyleSheet) (maxL if col.OutlineLevel > maxLevelCol { maxLevelCol = col.OutlineLevel } - // if nil != col.DataValidation { - // if nil == worksheet.DataValidations { - // worksheet.DataValidations = &xlsxDataValidations{} - // } - // 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) - // } - }) return maxLevelCol diff --git a/stream_test.go b/stream_test.go index 81f89150..83e08bb4 100644 --- a/stream_test.go +++ b/stream_test.go @@ -235,9 +235,6 @@ func TestXlsxStreamWrite(t *testing.T) { }, } for i, testCase := range testCases { - // if testCase.testName != "One Sheet" { - // continue - // } t.Run(testCase.testName, func(t *testing.T) { var filePath string var buffer bytes.Buffer From 9348bddafb98c3b8cec987f2547e86e68a750b33 Mon Sep 17 00:00:00 2001 From: "Geoffrey J. Teale" Date: Thu, 10 Oct 2019 13:41:23 +0200 Subject: [PATCH 11/13] Fix bounds errors --- xmlStyle.go | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/xmlStyle.go b/xmlStyle.go index 9fa7af5a..8fc0ab3f 100644 --- a/xmlStyle.go +++ b/xmlStyle.go @@ -151,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 { @@ -232,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 From d8bce4b90c2f5b55f0fdd7e1fe0fc590105516ce Mon Sep 17 00:00:00 2001 From: "Geoffrey J. Teale" Date: Thu, 10 Oct 2019 13:45:53 +0200 Subject: [PATCH 12/13] Knowingly change the expectations of the stream builder --- stream_test.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/stream_test.go b/stream_test.go index 83e08bb4..6f7a5519 100644 --- a/stream_test.go +++ b/stream_test.go @@ -888,10 +888,7 @@ func TestCloseWithNothingWrittenToSheets(t *testing.T) { file := NewStreamFileBuilder(buffer) sheetNames := []string{"Sheet1", "Sheet2"} - workbookData := [][][]string{ - {{"Header1", "Header2"}}, - {{"Header3", "Header4"}}, - } + expectedWorkbookData := [][][]string{{}, {}} err := file.AddSheet(sheetNames[0], nil) if err != nil { t.Fatal(err) @@ -917,7 +914,9 @@ func TestCloseWithNothingWrittenToSheets(t *testing.T) { 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") } } From d9a28ac759162df8a25607f1c294c51be52ec0fe Mon Sep 17 00:00:00 2001 From: "Geoffrey J. Teale" Date: Thu, 10 Oct 2019 13:47:44 +0200 Subject: [PATCH 13/13] gofmt --- col_test.go | 68 +++++++++++++++++++++--------------------- date.go | 16 +++++----- stream_file_builder.go | 4 +-- stream_style_test.go | 18 +++++------ write.go | 2 +- 5 files changed, 54 insertions(+), 54 deletions(-) diff --git a/col_test.go b/col_test.go index b5b16124..e68f8a68 100644 --- a/col_test.go +++ b/col_test.go @@ -106,7 +106,7 @@ func TestMakeWay(t *testing.T) { // Col1: |--| // Col2: |--| - assertWayMade([]*Col{&Col{Min: 1, Max: 2}, &Col{Min: 3, Max: 4}}, + assertWayMade([]*Col{{Min: 1, Max: 2}, {Min: 3, Max: 4}}, func(cs *ColStore) { c.Assert(cs.Len, qt.Equals, 2) root := cs.Root @@ -123,7 +123,7 @@ func TestMakeWay(t *testing.T) { // Col1: |--| // Col2: |--| - assertWayMade([]*Col{&Col{Min: 3, Max: 4}, &Col{Min: 1, Max: 2}}, + assertWayMade([]*Col{{Min: 3, Max: 4}, {Min: 1, Max: 2}}, func(cs *ColStore) { root := cs.Root c.Assert(cs.Len, qt.Equals, 2) @@ -140,7 +140,7 @@ func TestMakeWay(t *testing.T) { // Col1: |--x| // Col2: |--| - assertWayMade([]*Col{&Col{Min: 1, Max: 3}, &Col{Min: 3, Max: 4}}, + assertWayMade([]*Col{{Min: 1, Max: 3}, {Min: 3, Max: 4}}, func(cs *ColStore) { root := cs.Root c.Assert(cs.Len, qt.Equals, 2) @@ -157,7 +157,7 @@ func TestMakeWay(t *testing.T) { // Col1: |x-| // Col2: |--| - assertWayMade([]*Col{&Col{Min: 2, Max: 3}, &Col{Min: 1, Max: 2}}, + assertWayMade([]*Col{{Min: 2, Max: 3}, {Min: 1, Max: 2}}, func(cs *ColStore) { root := cs.Root c.Assert(cs.Len, qt.Equals, 2) @@ -174,7 +174,7 @@ func TestMakeWay(t *testing.T) { // Col1: |---xx---| // Col2: |--| - assertWayMade([]*Col{&Col{Min: 1, Max: 8}, &Col{Min: 4, Max: 5}}, + assertWayMade([]*Col{{Min: 1, Max: 8}, {Min: 4, Max: 5}}, func(cs *ColStore) { root := cs.Root c.Assert(cs.Len, qt.Equals, 3) @@ -196,7 +196,7 @@ func TestMakeWay(t *testing.T) { // Col1: |xx| // Col2: |--| - assertWayMade([]*Col{&Col{Min: 1, Max: 2, Width: 40.1}, &Col{Min: 1, Max: 2, Width: 10.0}}, + 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) @@ -211,7 +211,7 @@ func TestMakeWay(t *testing.T) { // Col1: |xx| // Col2: |----| - assertWayMade([]*Col{&Col{Min: 2, Max: 3, Width: 40.1}, &Col{Min: 1, Max: 4, Width: 10.0}}, + 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) @@ -226,7 +226,7 @@ func TestMakeWay(t *testing.T) { // Col1: |--| // Col2: |--| // Col3: |--| - assertWayMade([]*Col{&Col{Min: 1, Max: 2}, &Col{Min: 3, Max: 4}, &Col{Min: 5, Max: 6}}, + 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) @@ -249,7 +249,7 @@ func TestMakeWay(t *testing.T) { // Col1: |--| // Col2: |--| // Col3: |--| - assertWayMade([]*Col{&Col{Min: 5, Max: 6}, &Col{Min: 3, Max: 4}, &Col{Min: 1, Max: 2}}, + 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) @@ -272,7 +272,7 @@ func TestMakeWay(t *testing.T) { // Col1: |--| // Col2: |--| // Col3: |--| - assertWayMade([]*Col{&Col{Min: 1, Max: 2}, &Col{Min: 10, Max: 11}, &Col{Min: 5, Max: 6}}, + 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) @@ -296,7 +296,7 @@ func TestMakeWay(t *testing.T) { // Col2: |x-| // Col3: |-------| assertWayMade([]*Col{ - &Col{Min: 1, Max: 2}, &Col{Min: 8, Max: 9}, &Col{Min: 2, Max: 8}}, + {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) @@ -320,7 +320,7 @@ func TestMakeWay(t *testing.T) { // Col2: |--| // Col3: |-----| assertWayMade([]*Col{ - &Col{Min: 1, Max: 2}, &Col{Min: 8, Max: 9}, &Col{Min: 2, Max: 7}}, + {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) @@ -344,7 +344,7 @@ func TestMakeWay(t *testing.T) { // Col2: |x-| // Col3: |-----| assertWayMade([]*Col{ - &Col{Min: 1, Max: 2}, &Col{Min: 8, Max: 9}, &Col{Min: 3, Max: 8}}, + {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) @@ -370,10 +370,10 @@ func TestMakeWay(t *testing.T) { // Col4: |--| assertWayMade( []*Col{ - &Col{Min: 1, Max: 2}, - &Col{Min: 3, Max: 4, Width: 1.0}, - &Col{Min: 5, Max: 6}, - &Col{Min: 3, Max: 4, Width: 2.0}, + {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 @@ -401,10 +401,10 @@ func TestMakeWay(t *testing.T) { // Col4: |----| assertWayMade( []*Col{ - &Col{Min: 1, Max: 2, Width: 1.0}, - &Col{Min: 3, Max: 4, Width: 2.0}, - &Col{Min: 5, Max: 6, Width: 3.0}, - &Col{Min: 2, Max: 5, Width: 4.0}, + {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 @@ -545,35 +545,35 @@ func (css *ColStoreSuite) TestGetOrMakeColsForRange(c *C) { } // make everything - assertCols(1, 11, nil, []*Col{&Col{Min: 1, Max: 11}}) + assertCols(1, 11, nil, []*Col{{Min: 1, Max: 11}}) // get everything, one col - assertCols(1, 11, []*Col{&Col{Min: 1, Max: 11}}, []*Col{&Col{Min: 1, Max: 11}}) + assertCols(1, 11, []*Col{{Min: 1, Max: 11}}, []*Col{{Min: 1, Max: 11}}) // get everything, many cols assertCols(1, 11, []*Col{ - &Col{Min: 1, Max: 4}, - &Col{Min: 5, Max: 8}, - &Col{Min: 9, Max: 11}, + {Min: 1, Max: 4}, + {Min: 5, Max: 8}, + {Min: 9, Max: 11}, }, []*Col{ - &Col{Min: 1, Max: 4}, - &Col{Min: 5, Max: 8}, - &Col{Min: 9, Max: 11}, + {Min: 1, Max: 4}, + {Min: 5, Max: 8}, + {Min: 9, Max: 11}, }, ) // make missing col assertCols(1, 11, []*Col{ - &Col{Min: 1, Max: 4}, - &Col{Min: 9, Max: 11}, + {Min: 1, Max: 4}, + {Min: 9, Max: 11}, }, []*Col{ - &Col{Min: 1, Max: 4}, - &Col{Min: 5, Max: 8}, - &Col{Min: 9, Max: 11}, + {Min: 1, Max: 4}, + {Min: 5, Max: 8}, + {Min: 9, Max: 11}, }, ) diff --git a/date.go b/date.go index 234912ab..85fcd952 100644 --- a/date.go +++ b/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 { diff --git a/stream_file_builder.go b/stream_file_builder.go index 980f3516..d48114de 100644 --- a/stream_file_builder.go +++ b/stream_file_builder.go @@ -299,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 } @@ -390,7 +390,7 @@ func getSheetIndex(sf *StreamFile, path string) (int, error) { func removeDimensionTag(data string) string { start := strings.Index(data, "") + 12 - return data[0:start] + data[end:len(data)] + return data[0:start] + data[end:] } // splitSheetIntoPrefixAndSuffix will split the provided XML sheet into a prefix and a suffix so that diff --git a/stream_style_test.go b/stream_style_test.go index 14a4c829..db04ebd9 100644 --- a/stream_style_test.go +++ b/stream_style_test.go @@ -301,9 +301,9 @@ func TestXlsxStreamWriteWithStyle(t *testing.T) { } expectedWorkbookDataStrings := [][][]string{} - for j, _ := range testCase.workbookData { + for j := range testCase.workbookData { expectedWorkbookDataStrings = append(expectedWorkbookDataStrings, [][]string{}) - for k, _ := range testCase.workbookData[j] { + for k := range testCase.workbookData[j] { if len(testCase.workbookData[j][k]) == 0 { expectedWorkbookDataStrings[j] = append(expectedWorkbookDataStrings[j], nil) } else { @@ -467,7 +467,7 @@ func TestStreamStyleDates(t *testing.T) { } expectedWorkbookDataStrings := [][][]string{} - for j, _ := range workbookData { + for j := range workbookData { expectedWorkbookDataStrings = append(expectedWorkbookDataStrings, [][]string{}) for range workbookData[j] { expectedWorkbookDataStrings[j] = append(expectedWorkbookDataStrings[j], []string{}) @@ -552,9 +552,9 @@ func TestMakeNewStylesAndUseIt(t *testing.T) { } 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) @@ -607,9 +607,9 @@ func TestStreamNewTypes(t *testing.T) { } 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" { @@ -794,8 +794,8 @@ func TestStreamNoStylesWriteSError(t *testing.T) { } 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 { diff --git a/write.go b/write.go index cdfd7385..3b590fd4 100644 --- a/write.go +++ b/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)