Browse Source

Merge pull request #442 from elimity-com/master

Support streaming with custom styles and new cell types
Ryan Hollis 6 years ago
parent
commit
85d01a0647
12 changed files with 1272 additions and 32 deletions
  1. 2 1
      .gitignore
  2. 6 0
      col.go
  3. 1 1
      row.go
  4. 0 2
      sheet.go
  5. 51 0
      stream_cell.go
  6. 129 4
      stream_file.go
  7. 123 13
      stream_file_builder.go
  8. 124 0
      stream_style.go
  9. 801 0
      stream_style_test.go
  10. 23 0
      style.go
  11. 4 4
      xmlStyle.go
  12. 8 7
      xmlWorksheet.go

+ 2 - 1
.gitignore

@@ -1,5 +1,6 @@
 .vscode
 .vscode
+.idea
 .DS_Store
 .DS_Store
 xlsx.test
 xlsx.test
 *.swp
 *.swp
-coverage.txt
+coverage.txt

+ 6 - 0
col.go

@@ -97,3 +97,9 @@ func (c *Col) SetDataValidation(dd *xlsxCellDataValidation, start, end int) {
 func (c *Col) SetDataValidationWithStart(dd *xlsxCellDataValidation, start int) {
 func (c *Col) SetDataValidationWithStart(dd *xlsxCellDataValidation, start int) {
 	c.SetDataValidation(dd, start, -1)
 	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
+	c.numFmt = builtInNumFmt[style.xNumFmtId]
+}

+ 1 - 1
row.go

@@ -24,4 +24,4 @@ func (r *Row) AddCell() *Cell {
 	r.Cells = append(r.Cells, cell)
 	r.Cells = append(r.Cells, cell)
 	r.Sheet.maybeAddCol(len(r.Cells))
 	r.Sheet.maybeAddCol(len(r.Cells))
 	return cell
 	return cell
-}
+}

+ 0 - 2
sheet.go

@@ -163,7 +163,6 @@ func (s *Sheet) SetColWidth(startcol, endcol int, width float64) error {
 	for ; startcol < end; startcol++ {
 	for ; startcol < end; startcol++ {
 		s.Cols[startcol].Width = width
 		s.Cols[startcol].Width = width
 	}
 	}
-
 	return nil
 	return nil
 }
 }
 
 
@@ -256,7 +255,6 @@ func (s *Sheet) makeXLSXSheet(refTable *RefTable, styles *xlsxStyleSheet) *xlsxW
 		if col.Width == 0 {
 		if col.Width == 0 {
 			col.Width = ColWidth
 			col.Width = ColWidth
 			customWidth = false
 			customWidth = false
-
 		} else {
 		} else {
 			customWidth = true
 			customWidth = true
 		}
 		}

+ 51 - 0
stream_cell.go

@@ -0,0 +1,51 @@
+package xlsx
+
+import (
+	"strconv"
+	"time"
+)
+
+// StreamCell holds the data, style and type of cell for streaming.
+type StreamCell struct {
+	cellData  string
+	cellStyle StreamStyle
+	cellType  CellType
+}
+
+// NewStreamCell creates a new cell containing the given data with the given style and type.
+func NewStreamCell(cellData string, cellStyle StreamStyle, cellType CellType) StreamCell {
+	return StreamCell{
+		cellData:  cellData,
+		cellStyle: cellStyle,
+		cellType:  cellType,
+	}
+}
+
+// NewStringStreamCell creates a new cell that holds string data, is of type string and uses general formatting.
+func NewStringStreamCell(cellData string) StreamCell {
+	return NewStreamCell(cellData, StreamStyleDefaultString, CellTypeString)
+}
+
+// NewStyledStringStreamCell creates a new cell that holds a string and is styled according to the given style.
+func NewStyledStringStreamCell(cellData string, cellStyle StreamStyle) StreamCell {
+	return NewStreamCell(cellData, cellStyle, CellTypeString)
+}
+
+// NewIntegerStreamCell creates a new cell that holds an integer value (represented as string),
+// is formatted as a standard integer and is of type numeric.
+func NewIntegerStreamCell(cellData int) StreamCell {
+	return NewStreamCell(strconv.Itoa(cellData), StreamStyleDefaultInteger, CellTypeNumeric)
+}
+
+// NewStyledIntegerStreamCell creates a new cell that holds an integer value (represented as string)
+// and is styled according to the given style.
+func NewStyledIntegerStreamCell(cellData int, cellStyle StreamStyle) StreamCell {
+	return NewStreamCell(strconv.Itoa(cellData), cellStyle, CellTypeNumeric)
+}
+
+// NewDateStreamCell creates a new cell that holds a date value and is formatted as dd-mm-yyyy
+// and is of type numeric.
+func NewDateStreamCell(t time.Time) StreamCell {
+	excelTime := TimeToExcelTime(t, false)
+	return NewStreamCell(strconv.Itoa(int(excelTime)), StreamStyleDefaultDate, CellTypeNumeric)
+}

+ 129 - 4
stream_file.go

@@ -15,6 +15,7 @@ type StreamFile struct {
 	zipWriter      *zip.Writer
 	zipWriter      *zip.Writer
 	currentSheet   *streamSheet
 	currentSheet   *streamSheet
 	styleIds       [][]int
 	styleIds       [][]int
+	styleIdMap     map[StreamStyle]int
 	err            error
 	err            error
 }
 }
 
 
@@ -31,9 +32,10 @@ type streamSheet struct {
 }
 }
 
 
 var (
 var (
-	NoCurrentSheetError     = errors.New("no Current Sheet")
-	WrongNumberOfRowsError  = errors.New("invalid number of cells passed to Write. All calls to Write on the same sheet must have the same number of cells")
-	AlreadyOnLastSheetError = errors.New("NextSheet() called, but already on last sheet")
+	NoCurrentSheetError      = errors.New("no Current Sheet")
+	WrongNumberOfRowsError   = errors.New("invalid number of cells passed to Write. All calls to Write on the same sheet must have the same number of cells")
+	AlreadyOnLastSheetError  = errors.New("NextSheet() called, but already on last sheet")
+	UnsupportedCellTypeError = errors.New("the given cell type is not supported")
 )
 )
 
 
 // Write will write a row of cells to the current sheet. Every call to Write on the same sheet must contain the
 // Write will write a row of cells to the current sheet. Every call to Write on the same sheet must contain the
@@ -51,6 +53,22 @@ func (sf *StreamFile) Write(cells []string) error {
 	return sf.zipWriter.Flush()
 	return sf.zipWriter.Flush()
 }
 }
 
 
+// WriteS will write a row of cells to the current sheet. Every call to WriteS on the same sheet must
+// contain the same number of cells as the number of columns provided when the sheet was created or an error
+// will be returned. This function will always trigger a flush on success. WriteS supports all data types
+// and styles that are supported by StreamCell.
+func (sf *StreamFile) WriteS(cells []StreamCell) error {
+	if sf.err != nil {
+		return sf.err
+	}
+	err := sf.writeS(cells)
+	if err != nil {
+		sf.err = err
+		return err
+	}
+	return sf.zipWriter.Flush()
+}
+
 func (sf *StreamFile) WriteAll(records [][]string) error {
 func (sf *StreamFile) WriteAll(records [][]string) error {
 	if sf.err != nil {
 	if sf.err != nil {
 		return sf.err
 		return sf.err
@@ -65,6 +83,23 @@ func (sf *StreamFile) WriteAll(records [][]string) error {
 	return sf.zipWriter.Flush()
 	return sf.zipWriter.Flush()
 }
 }
 
 
+// WriteAllS will write all the rows provided in records. All rows must have the same number of cells as
+// the number of columns given when creating the sheet. This function will always trigger a flush on success.
+// WriteAllS supports all data types and styles that are supported by StreamCell.
+func (sf *StreamFile) WriteAllS(records [][]StreamCell) error {
+	if sf.err != nil {
+		return sf.err
+	}
+	for _, row := range records {
+		err := sf.writeS(row)
+		if err != nil {
+			sf.err = err
+			return err
+		}
+	}
+	return sf.zipWriter.Flush()
+}
+
 func (sf *StreamFile) write(cells []string) error {
 func (sf *StreamFile) write(cells []string) error {
 	if sf.currentSheet == nil {
 	if sf.currentSheet == nil {
 		return NoCurrentSheetError
 		return NoCurrentSheetError
@@ -112,6 +147,96 @@ func (sf *StreamFile) write(cells []string) error {
 	return sf.zipWriter.Flush()
 	return sf.zipWriter.Flush()
 }
 }
 
 
+func (sf *StreamFile) writeS(cells []StreamCell) error {
+	if sf.currentSheet == nil {
+		return NoCurrentSheetError
+	}
+	if len(cells) != sf.currentSheet.columnCount {
+		return WrongNumberOfRowsError
+	}
+
+	sf.currentSheet.rowCount++
+	// Write the row opening
+	if err := sf.currentSheet.write(`<row r="` + strconv.Itoa(sf.currentSheet.rowCount) + `">`); err != nil {
+		return err
+	}
+
+	// Add cells one by one
+	for colIndex, cell := range cells {
+
+		xlsxCell, err := sf.getXlsxCell(cell, colIndex)
+		if err != nil {
+			return err
+		}
+
+		marshaledCell, err := xml.Marshal(xlsxCell)
+		if err != nil {
+			return nil
+		}
+
+		// Write the cell
+		if _, err := sf.currentSheet.writer.Write(marshaledCell); err != nil {
+			return err
+		}
+
+	}
+	// Write the row ending
+	if err := sf.currentSheet.write(`</row>`); err != nil {
+		return err
+	}
+	return sf.zipWriter.Flush()
+}
+
+func (sf *StreamFile) getXlsxCell(cell StreamCell, colIndex int) (xlsxC, error) {
+	// Get the cell reference (location)
+	cellCoordinate := GetCellIDStringFromCoords(colIndex, sf.currentSheet.rowCount-1)
+
+	var cellStyleId int
+
+	if cell.cellStyle != (StreamStyle{}) {
+		if idx, ok := sf.styleIdMap[cell.cellStyle]; ok {
+			cellStyleId = idx
+		} else {
+			return xlsxC{}, errors.New("trying to make use of a style that has not been added")
+		}
+	}
+
+	return makeXlsxCell(cell.cellType, cellCoordinate, cellStyleId, cell.cellData)
+}
+
+func makeXlsxCell(cellType CellType, cellCoordinate string, cellStyleId int, cellData string) (xlsxC, error) {
+	// documentation for the c.t (cell.Type) attribute:
+	// b (Boolean): Cell containing a boolean.
+	// d (Date): Cell contains a date in the ISO 8601 format.
+	// e (Error): Cell containing an error.
+	// inlineStr (Inline String): Cell containing an (inline) rich string, i.e., one not in the shared string table.
+	// If this cell type is used, then the cell value is in the is element rather than the v element in the cell (c element).
+	// n (Number): Cell containing a number.
+	// s (Shared String): Cell containing a shared string.
+	// str (String): Cell containing a formula string.
+	switch cellType {
+	case CellTypeBool:
+		return xlsxC{XMLName: xml.Name{Local: "c"}, R: cellCoordinate, S: cellStyleId, T: "b", V: cellData}, nil
+	// Dates are better represented using CellTyleNumeric and the date formatting
+	//case CellTypeDate:
+		//return xlsxC{XMLName: xml.Name{Local: "c"}, R: cellCoordinate, S: cellStyleId, T: "d", V: cellData}, nil
+	case CellTypeError:
+		return xlsxC{XMLName: xml.Name{Local: "c"}, R: cellCoordinate, S: cellStyleId, T: "e", V: cellData}, nil
+	case CellTypeInline:
+		return xlsxC{XMLName: xml.Name{Local: "c"}, R: cellCoordinate, S: cellStyleId, T: "inlineStr", Is: &xlsxSI{T: cellData}}, nil
+	case CellTypeNumeric:
+		return xlsxC{XMLName: xml.Name{Local: "c"}, R: cellCoordinate, S: cellStyleId, T: "n", V: cellData}, nil
+	case CellTypeString:
+		// TODO Currently shared strings are types as inline strings
+		return xlsxC{XMLName: xml.Name{Local: "c"}, R: cellCoordinate, S: cellStyleId, T: "inlineStr", Is: &xlsxSI{T: cellData}}, nil
+	// TODO currently not supported
+	// case CellTypeStringFormula:
+		// return xlsxC{}, UnsupportedCellTypeError
+	default:
+		return xlsxC{}, UnsupportedCellTypeError
+	}
+}
+
 // Error reports any error that has occurred during a previous Write or Flush.
 // Error reports any error that has occurred during a previous Write or Flush.
 func (sf *StreamFile) Error() error {
 func (sf *StreamFile) Error() error {
 	return sf.err
 	return sf.err
@@ -147,7 +272,7 @@ func (sf *StreamFile) NextSheet() error {
 		index:       sheetIndex,
 		index:       sheetIndex,
 		columnCount: len(sf.xlsxFile.Sheets[sheetIndex-1].Cols),
 		columnCount: len(sf.xlsxFile.Sheets[sheetIndex-1].Cols),
 		styleIds:    sf.styleIds[sheetIndex-1],
 		styleIds:    sf.styleIds[sheetIndex-1],
-		rowCount:    1,
+		rowCount:    len(sf.xlsxFile.Sheets[sheetIndex-1].Rows),
 	}
 	}
 	sheetPath := sheetFilePathPrefix + strconv.Itoa(sf.currentSheet.index) + sheetFilePathSuffix
 	sheetPath := sheetFilePathPrefix + strconv.Itoa(sf.currentSheet.index) + sheetFilePathSuffix
 	fileWriter, err := sf.zipWriter.Create(sheetPath)
 	fileWriter, err := sf.zipWriter.Create(sheetPath)

+ 123 - 13
stream_file_builder.go

@@ -11,14 +11,22 @@
 // 5. Call NextSheet() to proceed to the next sheet. Once NextSheet() is called, the previous sheet can not be edited.
 // 5. Call NextSheet() to proceed to the next sheet. Once NextSheet() is called, the previous sheet can not be edited.
 // 6. Call Close() to finish.
 // 6. Call Close() to finish.
 
 
+// Directions for using custom styles and different data types:
+// 1. Create a StreamFileBuilder with NewStreamFileBuilder() or NewStreamFileBuilderForPath().
+// 2. Use MakeStyle() to create the styles you want yo use in your document. Keep a list of these styles.
+// 3. Add all the styles you created by using AddStreamStyle() or AddStreamStyleList().
+// 4. Add the sheets and their column styles of data by calling AddSheetS().
+// 5. Call Build() to get a StreamFile. Once built, all functions on the builder will return an error.
+// 6. Write to the StreamFile with WriteS(). Writes begin on the first sheet. New rows are always written and flushed
+// to the io. All rows written to the same sheet must have the same number of cells as the number of column styles
+// provided when adding the sheet with AddSheetS() or an error will be returned.
+// 5. Call NextSheet() to proceed to the next sheet. Once NextSheet() is called, the previous sheet can not be edited.
+// 6. Call Close() to finish.
+
 // Future work suggestions:
 // Future work suggestions:
-// Currently the only supported cell type is string, since the main reason this library was written was to prevent
-// strings from being interpreted as numbers. It would be nice to have support for numbers and money so that the exported
-// files could better take advantage of XLSX's features.
-// All text is written with the same text style. Support for additional text styles could be added to highlight certain
-// data in the file.
 // The current default style uses fonts that are not on Macs by default so opening the XLSX files in Numbers causes a
 // The current default style uses fonts that are not on Macs by default so opening the XLSX files in Numbers causes a
 // pop up that says there are missing fonts. The font could be changed to something that is usually found on Mac and PC.
 // pop up that says there are missing fonts. The font could be changed to something that is usually found on Mac and PC.
+// Extend support for Formulas and Shared Strings.
 
 
 package xlsx
 package xlsx
 
 
@@ -34,11 +42,15 @@ import (
 
 
 type StreamFileBuilder struct {
 type StreamFileBuilder struct {
 	built              bool
 	built              bool
+	firstSheetAdded    bool
+	customStylesAdded  bool
 	xlsxFile           *File
 	xlsxFile           *File
 	zipWriter          *zip.Writer
 	zipWriter          *zip.Writer
 	cellTypeToStyleIds map[CellType]int
 	cellTypeToStyleIds map[CellType]int
 	maxStyleId         int
 	maxStyleId         int
 	styleIds           [][]int
 	styleIds           [][]int
+	customStreamStyles map[StreamStyle]struct{}
+	styleIdMap         map[StreamStyle]int
 }
 }
 
 
 const (
 const (
@@ -61,6 +73,8 @@ func NewStreamFileBuilder(writer io.Writer) *StreamFileBuilder {
 		xlsxFile:           NewFile(),
 		xlsxFile:           NewFile(),
 		cellTypeToStyleIds: make(map[CellType]int),
 		cellTypeToStyleIds: make(map[CellType]int),
 		maxStyleId:         initMaxStyleId,
 		maxStyleId:         initMaxStyleId,
+		customStreamStyles: make(map[StreamStyle]struct{}),
+		styleIdMap:         make(map[StreamStyle]int),
 	}
 	}
 }
 }
 
 
@@ -119,6 +133,46 @@ func (sb *StreamFileBuilder) AddSheet(name string, headers []string, cellTypes [
 	return nil
 	return nil
 }
 }
 
 
+// AddSheetS will add a sheet with the given name and column styles. The number of column styles given
+// is the number of columns that will be created, and thus the number of cells each row has to have.
+// columnStyles[0] becomes the style of the first column, columnStyles[1] the style of the second column etc.
+// All the styles in columnStyles have to have been added or an error will be returned.
+// Sheet names must be unique, or an error will be returned.
+func (sb *StreamFileBuilder) AddSheetS(name string, columnStyles []StreamStyle) error {
+	if sb.built {
+		return BuiltStreamFileBuilderError
+	}
+	sheet, 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
+	}
+	// To make sure no new styles can be added after adding a sheet
+	sb.firstSheetAdded = true
+
+	// Check if all styles that will be used for columns have been created
+	for _, colStyle := range columnStyles {
+		if _, ok := sb.customStreamStyles[colStyle]; !ok {
+			return errors.New("trying to make use of a style that has not been added")
+		}
+	}
+
+	// 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))
+
+	// 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
+	}
+	return nil
+}
+
 // AddValidation will add a validation to a specific column.
 // 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 *xlsxCellDataValidation) {
 	sheet := sb.xlsxFile.Sheets[sheetIndex]
 	sheet := sb.xlsxFile.Sheets[sheetIndex]
@@ -133,22 +187,32 @@ func (sb *StreamFileBuilder) Build() (*StreamFile, error) {
 		return nil, BuiltStreamFileBuilderError
 		return nil, BuiltStreamFileBuilderError
 	}
 	}
 	sb.built = true
 	sb.built = true
+
 	parts, err := sb.xlsxFile.MarshallParts()
 	parts, err := sb.xlsxFile.MarshallParts()
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
+
+	if sb.customStylesAdded {
+		parts["xl/styles.xml"], err = sb.marshalStyles()
+		if err != nil {
+			return nil, err
+		}
+	}
+
 	es := &StreamFile{
 	es := &StreamFile{
 		zipWriter:      sb.zipWriter,
 		zipWriter:      sb.zipWriter,
 		xlsxFile:       sb.xlsxFile,
 		xlsxFile:       sb.xlsxFile,
 		sheetXmlPrefix: make([]string, len(sb.xlsxFile.Sheets)),
 		sheetXmlPrefix: make([]string, len(sb.xlsxFile.Sheets)),
 		sheetXmlSuffix: make([]string, len(sb.xlsxFile.Sheets)),
 		sheetXmlSuffix: make([]string, len(sb.xlsxFile.Sheets)),
 		styleIds:       sb.styleIds,
 		styleIds:       sb.styleIds,
+		styleIdMap:     sb.styleIdMap,
 	}
 	}
 	for path, data := range parts {
 	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
 		// 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.
 		// point the sheets are still empty. The sheet files will be written later as their rows come in.
 		if strings.HasPrefix(path, sheetFilePathPrefix) {
 		if strings.HasPrefix(path, sheetFilePathPrefix) {
-			if err := sb.processEmptySheetXML(es, path, data); err != nil {
+			if err := sb.processEmptySheetXML(es, path, data, !sb.customStylesAdded); err != nil {
 				return nil, err
 				return nil, err
 			}
 			}
 			continue
 			continue
@@ -169,9 +233,53 @@ func (sb *StreamFileBuilder) Build() (*StreamFile, error) {
 	return es, nil
 	return es, nil
 }
 }
 
 
+func (sb *StreamFileBuilder) marshalStyles() (string, error) {
+
+	for streamStyle, _ := range sb.customStreamStyles {
+		XfId := handleStyleForXLSX(streamStyle.style, streamStyle.xNumFmtId, sb.xlsxFile.styles)
+		sb.styleIdMap[streamStyle] = XfId
+	}
+
+	styleSheetXMLString, err := sb.xlsxFile.styles.Marshal()
+	if err != nil {
+		return "", err
+	}
+	return styleSheetXMLString, nil
+}
+
+// AddStreamStyle adds a new style to the style sheet.
+// Only Styles that have been added through this function will be usable.
+// This function cannot be used after AddSheetS or Build has been called, and if it is
+// called after AddSheetS or Buildit will return an error.
+func (sb *StreamFileBuilder) AddStreamStyle(streamStyle StreamStyle) error {
+	if sb.firstSheetAdded {
+		return errors.New("at least one sheet has been added, cannot add new styles anymore")
+	}
+	if sb.built {
+		return errors.New("file has been build, cannot add new styles anymore")
+	}
+	sb.customStreamStyles[streamStyle] = struct{}{}
+	sb.customStylesAdded = true
+	return nil
+}
+
+// AddStreamStyleList adds a list of new styles to the style sheet.
+// Only Styles that have been added through either this function or AddStreamStyle will be usable.
+// This function cannot be used after AddSheetS and Build has been called, and if it is
+// called after AddSheetS and Build it will return an error.
+func (sb *StreamFileBuilder) AddStreamStyleList(streamStyles []StreamStyle) error {
+	for _, streamStyle := range streamStyles {
+		err := sb.AddStreamStyle(streamStyle)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
 // processEmptySheetXML will take in the path and XML data of an empty sheet, and will save the beginning and end of the
 // processEmptySheetXML will take in the path and XML data of an empty sheet, and will save the beginning and end of the
 // XML file so that these can be written at the right time.
 // XML file so that these can be written at the right time.
-func (sb *StreamFileBuilder) processEmptySheetXML(sf *StreamFile, path, data string) error {
+func (sb *StreamFileBuilder) processEmptySheetXML(sf *StreamFile, path, data string, removeDimensionTagFlag bool) error {
 	// Get the sheet index from the path
 	// Get the sheet index from the path
 	sheetIndex, err := getSheetIndex(sf, path)
 	sheetIndex, err := getSheetIndex(sf, path)
 	if err != nil {
 	if err != nil {
@@ -180,9 +288,11 @@ 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.
 	// 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.
 	// It is valid to for a sheet to be missing a Dimension tag, but it is not valid for it to be wrong.
-	data, err = removeDimensionTag(data, sf.xlsxFile.Sheets[sheetIndex])
-	if err != nil {
-		return err
+	if removeDimensionTagFlag {
+		data, err = removeDimensionTag(data, sf.xlsxFile.Sheets[sheetIndex])
+		if err != nil {
+			return err
+		}
 	}
 	}
 
 
 	// Split the sheet at the end of its SheetData tag so that more rows can be added inside.
 	// Split the sheet at the end of its SheetData tag so that more rows can be added inside.
@@ -203,11 +313,11 @@ func getSheetIndex(sf *StreamFile, path string) (int, error) {
 	indexString := path[len(sheetFilePathPrefix) : len(path)-len(sheetFilePathSuffix)]
 	indexString := path[len(sheetFilePathPrefix) : len(path)-len(sheetFilePathSuffix)]
 	sheetXLSXIndex, err := strconv.Atoi(indexString)
 	sheetXLSXIndex, err := strconv.Atoi(indexString)
 	if err != nil {
 	if err != nil {
-		return -1, errors.New("Unexpected sheet file name from xlsx package")
+		return -1, errors.New("unexpected sheet file name from xlsx package")
 	}
 	}
 	if sheetXLSXIndex < 1 || len(sf.sheetXmlPrefix) < sheetXLSXIndex ||
 	if sheetXLSXIndex < 1 || len(sf.sheetXmlPrefix) < sheetXLSXIndex ||
 		len(sf.sheetXmlSuffix) < sheetXLSXIndex || len(sf.xlsxFile.Sheets) < sheetXLSXIndex {
 		len(sf.sheetXmlSuffix) < sheetXLSXIndex || len(sf.xlsxFile.Sheets) < sheetXLSXIndex {
-		return -1, errors.New("Unexpected sheet index")
+		return -1, errors.New("unexpected sheet index")
 	}
 	}
 	sheetArrayIndex := sheetXLSXIndex - 1
 	sheetArrayIndex := sheetXLSXIndex - 1
 	return sheetArrayIndex, nil
 	return sheetArrayIndex, nil
@@ -216,7 +326,7 @@ func getSheetIndex(sf *StreamFile, path string) (int, error) {
 // removeDimensionTag will return the passed in XLSX Spreadsheet XML with the dimension tag removed.
 // removeDimensionTag will return the passed in XLSX Spreadsheet XML with the dimension tag removed.
 // data is the XML data for the sheet
 // data is the XML data for the sheet
 // sheet is the Sheet struct that the XML was created from.
 // sheet is the Sheet struct that the XML was created from.
-// Can return an error if the XML's dimension tag does not match was is expected based on the provided Sheet
+// 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) {
 func removeDimensionTag(data string, sheet *Sheet) (string, error) {
 	x := len(sheet.Cols) - 1
 	x := len(sheet.Cols) - 1
 	y := len(sheet.Rows) - 1
 	y := len(sheet.Rows) - 1

+ 124 - 0
stream_style.go

@@ -0,0 +1,124 @@
+package xlsx
+
+// StreamStyle has style and formatting information.
+// Used to store a style for streaming
+type StreamStyle struct {
+	xNumFmtId int
+	style     *Style
+}
+
+const (
+	GeneralFormat              = 0
+	IntegerFormat              = 1
+	DecimalFormat              = 2
+	DateFormat_dd_mm_yy        = 14
+	DateTimeFormat_d_m_yy_h_mm = 22
+)
+
+var (
+	StreamStyleFromColumn StreamStyle
+
+	StreamStyleDefaultString    StreamStyle
+	StreamStyleBoldString       StreamStyle
+	StreamStyleItalicString     StreamStyle
+	StreamStyleUnderlinedString StreamStyle
+
+	StreamStyleDefaultInteger    StreamStyle
+	StreamStyleBoldInteger       StreamStyle
+	StreamStyleItalicInteger     StreamStyle
+	StreamStyleUnderlinedInteger StreamStyle
+
+	StreamStyleDefaultDate StreamStyle
+
+	StreamStyleDefaultDecimal StreamStyle
+)
+var (
+	FontBold       *Font
+	FontItalic     *Font
+	FontUnderlined *Font
+)
+var (
+	FillGreen *Fill
+	FillRed   *Fill
+	FillWhite *Fill
+)
+
+func init() {
+	// Init Fonts
+	FontBold = DefaultFont()
+	FontBold.Bold = true
+
+	FontItalic = DefaultFont()
+	FontItalic.Italic = true
+
+	FontUnderlined = DefaultFont()
+	FontUnderlined.Underline = true
+
+	// Init Fills
+	FillGreen = NewFill(Solid_Cell_Fill, RGB_Light_Green, RGB_White)
+	FillRed = NewFill(Solid_Cell_Fill, RGB_Light_Red, RGB_White)
+	FillWhite = DefaultFill()
+
+	// Init default string styles
+	StreamStyleDefaultString = MakeStringStyle(DefaultFont(), DefaultFill(), DefaultAlignment(), DefaultBorder())
+	StreamStyleBoldString = MakeStringStyle(FontBold, DefaultFill(), DefaultAlignment(), DefaultBorder())
+	StreamStyleItalicString = MakeStringStyle(FontItalic, DefaultFill(), DefaultAlignment(), DefaultBorder())
+	StreamStyleUnderlinedString = MakeStringStyle(FontUnderlined, DefaultFill(), DefaultAlignment(), DefaultBorder())
+
+	// Init default Integer styles
+	StreamStyleDefaultInteger = MakeIntegerStyle(DefaultFont(), DefaultFill(), DefaultAlignment(), DefaultBorder())
+	StreamStyleBoldInteger = MakeIntegerStyle(FontBold, DefaultFill(), DefaultAlignment(), DefaultBorder())
+	StreamStyleItalicInteger = MakeIntegerStyle(FontItalic, DefaultFill(), DefaultAlignment(), DefaultBorder())
+	StreamStyleUnderlinedInteger = MakeIntegerStyle(FontUnderlined, DefaultFill(), DefaultAlignment(), DefaultBorder())
+
+	StreamStyleDefaultDate = MakeDateStyle(DefaultFont(), DefaultFill(), DefaultAlignment(), DefaultBorder())
+
+	StreamStyleDefaultDecimal = MakeDecimalStyle(DefaultFont(), DefaultFill(), DefaultAlignment(), DefaultBorder())
+}
+
+// MakeStyle creates a new StreamStyle and add it to the styles that will be streamed.
+func MakeStyle(numFormatId int, font *Font, fill *Fill, alignment *Alignment, border *Border) StreamStyle {
+	newStyle := NewStyle()
+
+	newStyle.Font = *font
+	newStyle.Fill = *fill
+	newStyle.Alignment = *alignment
+	newStyle.Border = *border
+
+	newStyle.ApplyFont = true
+	newStyle.ApplyFill = true
+	newStyle.ApplyAlignment = true
+	newStyle.ApplyBorder = true
+
+	newStreamStyle := StreamStyle{
+		xNumFmtId: numFormatId,
+		style:     newStyle,
+	}
+
+	return newStreamStyle
+}
+
+// MakeStringStyle creates a new style that can be used on cells with string data.
+// If used on other data the formatting might be wrong.
+func MakeStringStyle(font *Font, fill *Fill, alignment *Alignment, border *Border) StreamStyle {
+	return MakeStyle(GeneralFormat, font, fill, alignment, border)
+}
+
+// MakeIntegerStyle creates a new style that can be used on cells with integer data.
+// If used on other data the formatting might be wrong.
+func MakeIntegerStyle(font *Font, fill *Fill, alignment *Alignment, border *Border) StreamStyle {
+	return MakeStyle(IntegerFormat, font, fill, alignment, border)
+}
+
+// MakeDecimalStyle creates a new style that can be used on cells with decimal numeric data.
+// If used on other data the formatting might be wrong.
+func MakeDecimalStyle(font *Font, fill *Fill, alignment *Alignment, border *Border) StreamStyle {
+	return MakeStyle(DecimalFormat, font, fill, alignment, border)
+}
+
+// MakeDateStyle creates a new style that can be used on cells with Date data.
+// The formatting used is: dd_mm_yy
+// If used on other data the formatting might be wrong.
+func MakeDateStyle(font *Font, fill *Fill, alignment *Alignment, border *Border) StreamStyle {
+	return MakeStyle(DateFormat_dd_mm_yy, font, fill, alignment, border)
+}

+ 801 - 0
stream_style_test.go

@@ -0,0 +1,801 @@
+package xlsx
+
+import (
+	"bytes"
+	"errors"
+	"fmt"
+	. "gopkg.in/check.v1"
+	"io"
+	"reflect"
+	"strconv"
+	"time"
+)
+
+const (
+	StyleStreamTestsShouldMakeRealFiles = false
+)
+
+type StreamStyleSuite struct{}
+
+var _ = Suite(&StreamStyleSuite{})
+
+func (s *StreamStyleSuite) TestStreamTestsShouldMakeRealFilesShouldBeFalse(t *C) {
+	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) {
+	// 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:
+	// https://www.microsoft.com/en-us/download/details.aspx?id=30425
+	testCases := []struct {
+		testName      string
+		sheetNames    []string
+		workbookData  [][][]StreamCell
+		expectedError error
+	}{
+		{
+			testName: "Style Test",
+			sheetNames: []string{
+				"Sheet1",
+			},
+			workbookData: [][][]StreamCell{
+				{
+					{NewStyledStringStreamCell("1", StreamStyleUnderlinedString), NewStyledStringStreamCell("25", StreamStyleItalicString),
+						NewStyledStringStreamCell("A", StreamStyleBoldString), NewStringStreamCell("B")},
+					{NewIntegerStreamCell(1234), NewStyledIntegerStreamCell(98, StreamStyleBoldInteger),
+						NewStyledIntegerStreamCell(34, StreamStyleItalicInteger), NewStyledIntegerStreamCell(26, StreamStyleUnderlinedInteger)},
+				},
+			},
+		},
+		{
+			testName: "One Sheet",
+			sheetNames: []string{
+				"Sheet1",
+			},
+			workbookData: [][][]StreamCell{
+				{
+					{NewStringStreamCell("Token"), NewStringStreamCell("Name"),
+						NewStringStreamCell("Price"), NewStringStreamCell("SKU")},
+					{NewIntegerStreamCell(123), NewStringStreamCell("Taco"),
+						NewIntegerStreamCell(300), NewIntegerStreamCell(123)},
+				},
+			},
+		},
+		{
+			testName: "One Column",
+			sheetNames: []string{
+				"Sheet1",
+			},
+			workbookData: [][][]StreamCell{
+				{
+					{NewStringStreamCell("Token")},
+					{NewIntegerStreamCell(123)},
+				},
+			},
+		},
+		{
+			testName: "Several Sheets, with different numbers of columns and rows",
+			sheetNames: []string{
+				"Sheet 1", "Sheet 2", "Sheet3",
+			},
+			workbookData: [][][]StreamCell{
+				{
+					{NewStringStreamCell("Token"), NewStringStreamCell("Name"),
+						NewStringStreamCell("Price"), NewStringStreamCell("SKU")},
+
+					{NewIntegerStreamCell(123), NewStringStreamCell("Taco"),
+						NewIntegerStreamCell(300), NewIntegerStreamCell(123)},
+				},
+				{
+					{NewStringStreamCell("Token"), NewStringStreamCell("Name"),
+						NewStringStreamCell("Price"), NewStringStreamCell("SKU"),
+						NewStringStreamCell("Stock")},
+
+					{NewIntegerStreamCell(456), NewStringStreamCell("Salsa"),
+						NewIntegerStreamCell(200), NewIntegerStreamCell(346),
+						NewIntegerStreamCell(1)},
+
+					{NewIntegerStreamCell(789), NewStringStreamCell("Burritos"),
+						NewIntegerStreamCell(400), NewIntegerStreamCell(754),
+						NewIntegerStreamCell(3)},
+				},
+				{
+					{NewStringStreamCell("Token"), NewStringStreamCell("Name"),
+						NewStringStreamCell("Price")},
+
+					{NewIntegerStreamCell(9853), NewStringStreamCell("Guacamole"),
+						NewIntegerStreamCell(500)},
+
+					{NewIntegerStreamCell(2357), NewStringStreamCell("Margarita"),
+						NewIntegerStreamCell(700)},
+				},
+			},
+		},
+		{
+			testName: "Two Sheets with same the name",
+			sheetNames: []string{
+				"Sheet 1", "Sheet 1",
+			},
+			workbookData: [][][]StreamCell{
+				{
+					{NewStringStreamCell("Token"), NewStringStreamCell("Name"),
+						NewStringStreamCell("Price"), NewStringStreamCell("SKU")},
+
+					{NewIntegerStreamCell(123), NewStringStreamCell("Taco"),
+						NewIntegerStreamCell(300), NewIntegerStreamCell(123)},
+				},
+				{
+					{NewStringStreamCell("Token"), NewStringStreamCell("Name"),
+						NewStringStreamCell("Price"), NewStringStreamCell("SKU"),
+						NewStringStreamCell("Stock")},
+
+					{NewIntegerStreamCell(456), NewStringStreamCell("Salsa"),
+						NewIntegerStreamCell(200), NewIntegerStreamCell(346),
+						NewIntegerStreamCell(1)},
+
+					{NewIntegerStreamCell(789), NewStringStreamCell("Burritos"),
+						NewIntegerStreamCell(400), NewIntegerStreamCell(754),
+						NewIntegerStreamCell(3)},
+				},
+			},
+			expectedError: fmt.Errorf("duplicate sheet name '%s'.", "Sheet 1"),
+		},
+		{
+			testName: "One Sheet Registered, tries to write to two",
+			sheetNames: []string{
+				"Sheet 1",
+			},
+			workbookData: [][][]StreamCell{
+				{
+					{NewStringStreamCell("Token"), NewStringStreamCell("Name"),
+						NewStringStreamCell("Price"), NewStringStreamCell("SKU")},
+
+					{NewIntegerStreamCell(123), NewStringStreamCell("Taco"),
+						NewIntegerStreamCell(300), NewIntegerStreamCell(123)},
+				},
+				{
+					{NewStringStreamCell("Token"), NewStringStreamCell("Name"),
+						NewStringStreamCell("Price"), NewStringStreamCell("SKU")},
+
+					{NewIntegerStreamCell(456), NewStringStreamCell("Salsa"),
+						NewIntegerStreamCell(200), NewIntegerStreamCell(346)},
+				},
+			},
+			expectedError: AlreadyOnLastSheetError,
+		},
+		{
+			testName: "One Sheet, too many columns in row 1",
+			sheetNames: []string{
+				"Sheet 1",
+			},
+			workbookData: [][][]StreamCell{
+				{
+					{NewStringStreamCell("Token"), NewStringStreamCell("Name"),
+						NewStringStreamCell("Price"), NewStringStreamCell("SKU")},
+
+					{NewIntegerStreamCell(123), NewStringStreamCell("Taco"),
+						NewIntegerStreamCell(300), NewIntegerStreamCell(123),
+						NewStringStreamCell("asdf")},
+				},
+			},
+			expectedError: WrongNumberOfRowsError,
+		},
+		{
+			testName: "One Sheet, too few columns in row 1",
+			sheetNames: []string{
+				"Sheet 1",
+			},
+			workbookData: [][][]StreamCell{
+				{
+					{NewStringStreamCell("Token"), NewStringStreamCell("Name"),
+						NewStringStreamCell("Price"), NewStringStreamCell("SKU")},
+
+					{NewIntegerStreamCell(123), NewStringStreamCell("Taco"),
+						NewIntegerStreamCell(300)},
+				},
+			},
+			expectedError: WrongNumberOfRowsError,
+		},
+		{
+			testName: "Lots of Sheets, only writes rows to one, only writes headers to one, should not error and should still create a valid file",
+			sheetNames: []string{
+				"Sheet 1", "Sheet 2", "Sheet 3", "Sheet 4", "Sheet 5", "Sheet 6",
+			},
+			workbookData: [][][]StreamCell{
+				{
+					{NewStringStreamCell("Token"), NewStringStreamCell("Name"),
+						NewStringStreamCell("Price"), NewStringStreamCell("SKU")},
+
+					{NewIntegerStreamCell(123), NewStringStreamCell("Taco"),
+						NewIntegerStreamCell(300), NewIntegerStreamCell(123)},
+				},
+				{{}},
+				{{NewStringStreamCell("Id"), NewStringStreamCell("Unit Cost")}},
+				{{}},
+				{{}},
+				{{}},
+			},
+		},
+		{
+			testName: "Two Sheets, only writes to one, should not error and should still create a valid file",
+			sheetNames: []string{
+				"Sheet 1", "Sheet 2",
+			},
+			workbookData: [][][]StreamCell{
+				{
+					{NewStringStreamCell("Token"), NewStringStreamCell("Name"),
+						NewStringStreamCell("Price"), NewStringStreamCell("SKU")},
+
+					{NewIntegerStreamCell(123), NewStringStreamCell("Taco"),
+						NewIntegerStreamCell(300), NewIntegerStreamCell(123)},
+				},
+				{{}},
+			},
+		},
+		{
+			testName: "UTF-8 Characters. This XLSX File loads correctly with Excel, Numbers, and Google Docs. It also passes Microsoft's Office File Format Validator.",
+			sheetNames: []string{
+				"Sheet1",
+			},
+			workbookData: [][][]StreamCell{
+				{
+					// String courtesy of https://github.com/minimaxir/big-list-of-naughty-strings/
+					// Header row contains the tags that I am filtering on
+					{NewStringStreamCell("Token"), NewStringStreamCell(endSheetDataTag),
+						NewStringStreamCell("Price"), NewStringStreamCell(fmt.Sprintf(dimensionTag, "A1:D1"))},
+					// Japanese and emojis
+					{NewIntegerStreamCell(123), NewStringStreamCell("パーティーへ行かないか"),
+						NewIntegerStreamCell(300), NewStringStreamCell("🍕🐵 🙈 🙉 🙊")},
+					// XML encoder/parser test strings
+					{NewIntegerStreamCell(123), NewStringStreamCell(`<?xml version="1.0" encoding="ISO-8859-1"?>`),
+						NewIntegerStreamCell(300), NewStringStreamCell(`<?xml version="1.0" encoding="ISO-8859-1"?><!DOCTYPE foo [ <!ELEMENT foo ANY ><!ENTITY xxe SYSTEM "file:///etc/passwd" >]><foo>&xxe;</foo>`)},
+					// Upside down text and Right to Left Arabic text
+					{NewIntegerStreamCell(123), NewStringStreamCell(`˙ɐnbᴉlɐ ɐuƃɐɯ ǝɹolop ʇǝ ǝɹoqɐl ʇn ʇunpᴉpᴉɔuᴉ ɹodɯǝʇ poɯsnᴉǝ op pǝs 'ʇᴉlǝ ƃuᴉɔsᴉdᴉpɐ ɹnʇǝʇɔǝsuoɔ 'ʇǝɯɐ ʇᴉs ɹolop ɯnsdᴉ ɯǝɹo˥
+					00˙Ɩ$-`), NewIntegerStreamCell(300), NewStringStreamCell(`ﷺ`)},
+					{NewIntegerStreamCell(123), NewStringStreamCell("Taco"),
+						NewIntegerStreamCell(300), NewIntegerStreamCell(123)},
+				},
+			},
+		},
+	}
+
+	for i, testCase := range testCases {
+		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")
+		}
+
+		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 err := checkForCorrectCellStyles(actualWorkbookCells, testCase.workbookData); err != nil {
+			t.Fatal("Expected styles to be equal")
+		}
+	}
+}
+
+// writeStreamFile will write the file using this stream package
+func writeStreamFileWithStyle(filePath string, fileBuffer io.Writer, sheetNames []string, workbookData [][][]StreamCell,
+	shouldMakeRealFiles bool, customStyles []StreamStyle) error {
+	var file *StreamFileBuilder
+	var err error
+	if shouldMakeRealFiles {
+		file, err = NewStreamFileBuilderForPath(filePath)
+		if err != nil {
+			return err
+		}
+	} else {
+		file = NewStreamFileBuilder(fileBuffer)
+	}
+
+	defaultStyles := []StreamStyle{StreamStyleDefaultString, StreamStyleBoldString, StreamStyleItalicString, StreamStyleUnderlinedString,
+		StreamStyleDefaultInteger, StreamStyleBoldInteger, StreamStyleItalicInteger, StreamStyleUnderlinedInteger,
+		StreamStyleDefaultDate}
+	allStylesToBeAdded := append(defaultStyles, customStyles...)
+	err = file.AddStreamStyleList(allStylesToBeAdded)
+	if err != nil {
+		return err
+	}
+
+	for i, sheetName := range sheetNames {
+		var colStyles []StreamStyle
+		for range workbookData[i][0] {
+			colStyles = append(colStyles, StreamStyleDefaultString)
+		}
+
+		err := file.AddSheetS(sheetName, colStyles)
+		if err != nil {
+			return err
+		}
+	}
+	streamFile, err := file.Build()
+	if err != nil {
+		return err
+	}
+	for i, sheetData := range workbookData {
+
+		if i != 0 {
+			err = streamFile.NextSheet()
+			if err != nil {
+				return err
+			}
+		}
+		if i%2 == 0 {
+			err = streamFile.WriteAllS(sheetData)
+		} else {
+			for _, row := range sheetData {
+				err = streamFile.WriteS(row)
+				if err != nil {
+					return err
+				}
+			}
+		}
+	}
+	err = streamFile.Close()
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+// 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) {
+	var readFile *File
+	var err error
+	if shouldMakeRealFiles {
+		readFile, err = OpenFile(filePath)
+		if err != nil {
+			t.Fatal(err)
+		}
+	} else {
+		readFile, err = OpenReaderAt(fileBuffer, size)
+		if err != nil {
+			t.Fatal(err)
+		}
+	}
+	var actualWorkbookData [][][]string
+	var sheetNames []string
+	var actualWorkBookCells [][][]Cell
+	for i, sheet := range readFile.Sheets {
+		actualWorkBookCells = append(actualWorkBookCells, [][]Cell{})
+		var sheetData [][]string
+		for j, row := range sheet.Rows {
+			actualWorkBookCells[i] = append(actualWorkBookCells[i], []Cell{})
+			var data []string
+			for _, cell := range row.Cells {
+				actualWorkBookCells[i][j] = append(actualWorkBookCells[i][j], *cell)
+				str, err := cell.FormattedValue()
+				if err != nil {
+					t.Fatal(err)
+				}
+				data = append(data, str)
+			}
+			sheetData = append(sheetData, data)
+		}
+		sheetNames = append(sheetNames, sheet.Name)
+		actualWorkbookData = append(actualWorkbookData, sheetData)
+	}
+	return sheetNames, actualWorkbookData, actualWorkBookCells
+}
+
+func (s *StreamStyleSuite) TestDates(t *C) {
+	var filePath string
+	var buffer bytes.Buffer
+	if StyleStreamTestsShouldMakeRealFiles {
+		filePath = fmt.Sprintf("Workbook_Date_test.xlsx")
+	}
+
+	sheetNames := []string{"Sheet1"}
+	workbookData := [][][]StreamCell{
+		{
+			{NewStringStreamCell("Date:")},
+			{NewDateStreamCell(time.Now())},
+		},
+	}
+
+	err := writeStreamFileWithStyle(filePath, &buffer, sheetNames, workbookData, StyleStreamTestsShouldMakeRealFiles, []StreamStyle{})
+	if err != nil {
+		t.Fatal("Error during writing")
+	}
+
+	// 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, sheetNames) {
+		t.Fatal("Expected sheet names to be equal")
+	}
+
+	expectedWorkbookDataStrings := [][][]string{}
+	for j, _ := range workbookData {
+		expectedWorkbookDataStrings = append(expectedWorkbookDataStrings, [][]string{})
+		for range workbookData[j] {
+			expectedWorkbookDataStrings[j] = append(expectedWorkbookDataStrings[j], []string{})
+		}
+	}
+
+	expectedWorkbookDataStrings[0][0] = append(expectedWorkbookDataStrings[0][0], workbookData[0][0][0].cellData)
+	year, month, day := time.Now().Date()
+	monthString := strconv.Itoa(int(month))
+	if int(month) < 10 {
+		monthString = "0" + monthString
+	}
+	dayString := strconv.Itoa(day)
+	if day < 10 {
+		dayString = "0" + dayString
+	}
+	expectedWorkbookDataStrings[0][1] = append(expectedWorkbookDataStrings[0][1],
+		monthString+"-"+dayString+"-"+strconv.Itoa(year-2000))
+
+	if !reflect.DeepEqual(actualWorkbookData, expectedWorkbookDataStrings) {
+		t.Fatal("Expected workbook data to be equal")
+	}
+
+	if err := checkForCorrectCellStyles(actualWorkbookCells, workbookData); err != nil {
+		t.Fatal("Expected styles to be equal")
+	}
+}
+
+func (s *StreamSuite) TestMakeNewStylesAndUseIt(t *C) {
+	var filePath string
+	var buffer bytes.Buffer
+	if StyleStreamTestsShouldMakeRealFiles {
+		filePath = fmt.Sprintf("Workbook_newStyle.xlsx")
+	}
+
+	timesNewRoman12 := NewFont(12, TimesNewRoman)
+	timesNewRoman12.Color = RGB_Dark_Green
+	courier12 := NewFont(12, Courier)
+	courier12.Color = RGB_Dark_Red
+
+	greenFill := NewFill(Solid_Cell_Fill, RGB_Light_Green, RGB_White)
+	redFill := NewFill(Solid_Cell_Fill, RGB_Light_Red, RGB_White)
+
+	greenStyle := MakeStyle(GeneralFormat, timesNewRoman12, greenFill, DefaultAlignment(), DefaultBorder())
+	redStyle := MakeStyle(GeneralFormat, courier12, redFill, DefaultAlignment(), DefaultBorder())
+
+	sheetNames := []string{"Sheet1"}
+	workbookData := [][][]StreamCell{
+		{
+			{NewStringStreamCell("TRUE"), NewStringStreamCell("False")},
+			{NewStyledStringStreamCell("Good", greenStyle), NewStyledStringStreamCell("Bad", redStyle)},
+		},
+	}
+
+	err := writeStreamFileWithStyle(filePath, &buffer, sheetNames, workbookData, StyleStreamTestsShouldMakeRealFiles, []StreamStyle{greenStyle, redStyle})
+
+	if err != nil {
+		t.Fatal("Error during writing")
+	}
+
+	// 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, sheetNames) {
+		t.Fatal("Expected sheet names to be equal")
+	}
+
+	expectedWorkbookDataStrings := [][][]string{}
+	for j, _ := range workbookData {
+		expectedWorkbookDataStrings = append(expectedWorkbookDataStrings, [][]string{})
+		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)
+			}
+		}
+
+	}
+	if !reflect.DeepEqual(actualWorkbookData, expectedWorkbookDataStrings) {
+		t.Fatal("Expected workbook data to be equal")
+	}
+
+	if err := checkForCorrectCellStyles(actualWorkbookCells, workbookData); err != nil {
+		t.Fatal("Expected styles to be equal")
+	}
+}
+
+func (s *StreamSuite) TestNewTypes(t *C) {
+	var filePath string
+	var buffer bytes.Buffer
+	if StyleStreamTestsShouldMakeRealFiles {
+		filePath = fmt.Sprintf("Workbook_newStyle.xlsx")
+	}
+
+	sheetNames := []string{"Sheet1"}
+	workbookData := [][][]StreamCell{
+		{
+			{NewStreamCell("1", StreamStyleDefaultString, CellTypeBool),
+				NewStreamCell("InLine", StreamStyleBoldString, CellTypeInline),
+				NewStreamCell("Error", StreamStyleDefaultString, CellTypeError)},
+		},
+	}
+
+	err := writeStreamFileWithStyle(filePath, &buffer, sheetNames, workbookData, StyleStreamTestsShouldMakeRealFiles, []StreamStyle{})
+
+	if err != nil {
+		t.Fatal("Error during writing")
+	}
+
+	// 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, sheetNames) {
+		t.Fatal("Expected sheet names to be equal")
+	}
+
+	expectedWorkbookDataStrings := [][][]string{}
+	for j, _ := range workbookData {
+		expectedWorkbookDataStrings = append(expectedWorkbookDataStrings, [][]string{})
+		for k, _ := range workbookData[j] {
+			expectedWorkbookDataStrings[j] = append(expectedWorkbookDataStrings[j], []string{})
+			for _, cell := range workbookData[j][k] {
+				if cell.cellData == "1" {
+					expectedWorkbookDataStrings[j][k] = append(expectedWorkbookDataStrings[j][k], "TRUE")
+				} else {
+					expectedWorkbookDataStrings[j][k] = append(expectedWorkbookDataStrings[j][k], cell.cellData)
+				}
+			}
+		}
+
+	}
+	if !reflect.DeepEqual(actualWorkbookData, expectedWorkbookDataStrings) {
+		t.Fatal("Expected workbook data to be equal")
+	}
+
+	if err := checkForCorrectCellStyles(actualWorkbookCells, workbookData); err != nil {
+		t.Fatal("Expected styles to be equal")
+	}
+}
+
+func (s *StreamStyleSuite) TestCloseWithNothingWrittenToSheetsWithStyle(t *C) {
+	buffer := bytes.NewBuffer(nil)
+	file := NewStreamFileBuilder(buffer)
+
+	sheetNames := []string{"Sheet1", "Sheet2"}
+	workbookData := [][][]StreamCell{
+		{{NewStringStreamCell("Header1"), NewStringStreamCell("Header2")}},
+		{{NewStringStreamCell("Header3"), NewStringStreamCell("Header4")}},
+	}
+
+	defaultStyles := []StreamStyle{StreamStyleDefaultString, StreamStyleBoldString, StreamStyleItalicInteger, StreamStyleUnderlinedString,
+		StreamStyleDefaultInteger, StreamStyleBoldInteger, StreamStyleItalicInteger, StreamStyleUnderlinedInteger,
+		StreamStyleDefaultDate}
+	err := file.AddStreamStyleList(defaultStyles)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	colStyles0 := []StreamStyle{}
+	for range workbookData[0][0] {
+		colStyles0 = append(colStyles0, StreamStyleDefaultString)
+	}
+
+	colStyles1 := []StreamStyle{}
+	for range workbookData[1][0] {
+		colStyles1 = append(colStyles1, StreamStyleDefaultString)
+	}
+
+	err = file.AddSheetS(sheetNames[0], colStyles0)
+	if err != nil {
+		t.Fatal(err)
+	}
+	err = file.AddSheetS(sheetNames[1], colStyles1)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	stream, err := file.Build()
+	if err != nil {
+		t.Fatal(err)
+	}
+	err = stream.Close()
+	if err != nil {
+		t.Fatal(err)
+	}
+	bufReader := bytes.NewReader(buffer.Bytes())
+	size := bufReader.Size()
+
+	actualSheetNames, actualWorkbookData, _ := readXLSXFileS(t, "", bufReader, size, false)
+	// check if data was able to be read correctly
+	if !reflect.DeepEqual(actualSheetNames, sheetNames) {
+		t.Fatal("Expected sheet names to be equal")
+	}
+	expectedWorkbookDataStrings := [][][]string{}
+	for range workbookData {
+		expectedWorkbookDataStrings = append(expectedWorkbookDataStrings, nil)
+	}
+	if !reflect.DeepEqual(actualWorkbookData, expectedWorkbookDataStrings) {
+		t.Fatal("Expected workbook data to be equal")
+	}
+}
+
+func (s *StreamStyleSuite) TestBuildErrorsAfterBuildWithStyle(t *C) {
+	file := NewStreamFileBuilder(bytes.NewBuffer(nil))
+
+	defaultStyles := []StreamStyle{StreamStyleDefaultString, StreamStyleBoldString, StreamStyleItalicInteger, StreamStyleUnderlinedString,
+		StreamStyleDefaultInteger, StreamStyleBoldInteger, StreamStyleItalicInteger, StreamStyleUnderlinedInteger,
+		StreamStyleDefaultDate}
+	err := file.AddStreamStyleList(defaultStyles)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	err = file.AddSheetS("Sheet1", []StreamStyle{StreamStyleDefaultString})
+	if err != nil {
+		t.Fatal(err)
+	}
+	err = file.AddSheetS("Sheet2", []StreamStyle{StreamStyleDefaultString})
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	_, err = file.Build()
+	if err != nil {
+		t.Fatal(err)
+	}
+	_, err = file.Build()
+	if err != BuiltStreamFileBuilderError {
+		t.Fatal(err)
+	}
+}
+
+func (s *StreamStyleSuite) TestAddSheetSWithErrorsAfterBuild(t *C) {
+	file := NewStreamFileBuilder(bytes.NewBuffer(nil))
+
+	defaultStyles := []StreamStyle{StreamStyleDefaultString, StreamStyleBoldString, StreamStyleItalicInteger, StreamStyleUnderlinedString,
+		StreamStyleDefaultInteger, StreamStyleBoldInteger, StreamStyleItalicInteger, StreamStyleUnderlinedInteger,
+		StreamStyleDefaultDate}
+	err := file.AddStreamStyleList(defaultStyles)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	err = file.AddSheetS("Sheet1", []StreamStyle{StreamStyleDefaultString})
+	if err != nil {
+		t.Fatal(err)
+	}
+	err = file.AddSheetS("Sheet2", []StreamStyle{StreamStyleDefaultString})
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	_, err = file.Build()
+	if err != nil {
+		t.Fatal(err)
+	}
+	err = file.AddSheetS("Sheet3", []StreamStyle{StreamStyleDefaultString})
+	if err != BuiltStreamFileBuilderError {
+		t.Fatal(err)
+	}
+}
+
+func (s *StreamStyleSuite) TestNoStylesAddSheetSError(t *C) {
+	buffer := bytes.NewBuffer(nil)
+	file := NewStreamFileBuilder(buffer)
+
+	sheetNames := []string{"Sheet1", "Sheet2"}
+	workbookData := [][][]StreamCell{
+		{{NewStringStreamCell("Header1"), NewStringStreamCell("Header2")}},
+		{{NewStyledStringStreamCell("Header3", StreamStyleBoldString), NewStringStreamCell("Header4")}},
+	}
+
+	colStyles0 := []StreamStyle{}
+	for range workbookData[0][0] {
+		colStyles0 = append(colStyles0, StreamStyleDefaultString)
+	}
+
+	err := file.AddSheetS(sheetNames[0], colStyles0)
+	if err.Error() != "trying to make use of a style that has not been added" {
+		t.Fatal("Error differs from expected error.")
+	}
+}
+
+func (s *StreamStyleSuite) TestNoStylesWriteSError(t *C) {
+	buffer := bytes.NewBuffer(nil)
+	var filePath string
+
+	greenStyle := MakeStyle(GeneralFormat, DefaultFont(), FillGreen, DefaultAlignment(), DefaultBorder())
+
+	sheetNames := []string{"Sheet1", "Sheet2"}
+	workbookData := [][][]StreamCell{
+		{{NewStringStreamCell("Header1"), NewStringStreamCell("Header2")}},
+		{{NewStyledStringStreamCell("Header3", greenStyle), NewStringStreamCell("Header4")}},
+	}
+
+	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")
+	}
+
+
+}
+
+func checkForCorrectCellStyles(actualCells [][][]Cell, expectedCells [][][]StreamCell) error {
+	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 {
+					return err
+				}
+			}
+		}
+	}
+	return nil
+}
+
+func compareCellStyles(cellA Cell, cellB StreamCell) error {
+	fontA := cellA.style.Font
+	fontB := cellB.cellStyle.style.Font
+
+	if fontA != fontB {
+		return errors.New("actual and expected font do not match")
+	}
+
+	numFmtA := cellA.NumFmt
+	numFmtB := builtInNumFmt[cellB.cellStyle.xNumFmtId]
+	if numFmtA != numFmtB {
+		return errors.New("actual and expected NumFmt do not match")
+	}
+
+	return nil
+}

+ 23 - 0
style.go

@@ -2,6 +2,29 @@ package xlsx
 
 
 import "strconv"
 import "strconv"
 
 
+// Several popular font names that can be used to create fonts
+const (
+	Helvetica     = "Helvetica"
+	Baskerville   = "Baskerville Old Face"
+	TimesNewRoman = "Times New Roman"
+	Bodoni        = "Bodoni MT"
+	GillSans      = "Gill Sans MT"
+	Courier       = "Courier"
+)
+
+const (
+	RGB_Light_Green = "FFC6EFCE"
+	RGB_Dark_Green  = "FF006100"
+	RGB_Light_Red   = "FFFFC7CE"
+	RGB_Dark_Red    = "FF9C0006"
+	RGB_White       = "00000000"
+	RGB_Black       = "FFFFFFFF"
+)
+
+const (
+	Solid_Cell_Fill = "solid"
+)
+
 // Style is a high level structure intended to provide user access to
 // Style is a high level structure intended to provide user access to
 // the contents of Style within an XLSX file.
 // the contents of Style within an XLSX file.
 type Style struct {
 type Style struct {

+ 4 - 4
xmlStyle.go

@@ -103,7 +103,7 @@ type xlsxStyleSheet struct {
 
 
 	theme *theme
 	theme *theme
 
 
-	sync.RWMutex      // protects the following
+	sync.RWMutex // protects the following
 	styleCache        map[int]*Style
 	styleCache        map[int]*Style
 	numFmtRefTable    map[int]xlsxNumFmt
 	numFmtRefTable    map[int]xlsxNumFmt
 	parsedNumFmtTable map[string]*parsedNumberFormat
 	parsedNumFmtTable map[string]*parsedNumberFormat
@@ -211,9 +211,9 @@ func (styles *xlsxStyleSheet) getStyle(styleIndex int) *Style {
 			style.Alignment.Vertical = xf.Alignment.Vertical
 			style.Alignment.Vertical = xf.Alignment.Vertical
 		}
 		}
 		style.Alignment.WrapText = xf.Alignment.WrapText
 		style.Alignment.WrapText = xf.Alignment.WrapText
-        	style.Alignment.TextRotation = xf.Alignment.TextRotation
-		
-        	styles.Lock()
+		style.Alignment.TextRotation = xf.Alignment.TextRotation
+
+		styles.Lock()
 		styles.styleCache[styleIndex] = style
 		styles.styleCache[styleIndex] = style
 		styles.Unlock()
 		styles.Unlock()
 	}
 	}

+ 8 - 7
xmlWorksheet.go

@@ -302,7 +302,7 @@ type xlsxMergeCell struct {
 }
 }
 
 
 type xlsxMergeCells struct {
 type xlsxMergeCells struct {
-	XMLName xml.Name        //`xml:"mergeCells,omitempty"`
+	XMLName xml.Name //`xml:"mergeCells,omitempty"`
 	Count   int             `xml:"count,attr,omitempty"`
 	Count   int             `xml:"count,attr,omitempty"`
 	Cells   []xlsxMergeCell `xml:"mergeCell,omitempty"`
 	Cells   []xlsxMergeCell `xml:"mergeCell,omitempty"`
 }
 }
@@ -335,12 +335,13 @@ func (mc *xlsxMergeCells) getExtent(cellRef string) (int, int, error) {
 // currently I have not checked it for completeness - it does as much
 // currently I have not checked it for completeness - it does as much
 // as I need.
 // as I need.
 type xlsxC struct {
 type xlsxC struct {
-	R  string  `xml:"r,attr"`           // Cell ID, e.g. A1
-	S  int     `xml:"s,attr,omitempty"` // Style reference.
-	T  string  `xml:"t,attr,omitempty"` // Type.
-	F  *xlsxF  `xml:"f,omitempty"`      // Formula
-	V  string  `xml:"v,omitempty"`      // Value
-	Is *xlsxSI `xml:"is,omitempty"`     // Inline String.
+	XMLName xml.Name
+	R       string  `xml:"r,attr"`           // Cell ID, e.g. A1
+	S       int     `xml:"s,attr,omitempty"` // Style reference.
+	T       string  `xml:"t,attr,omitempty"` // Type.
+	F       *xlsxF  `xml:"f,omitempty"`      // Formula
+	V       string  `xml:"v,omitempty"`      // Value
+	Is      *xlsxSI `xml:"is,omitempty"`     // Inline String.
 }
 }
 
 
 // xlsxF directly maps the f element in the namespace
 // xlsxF directly maps the f element in the namespace