Просмотр исходного кода

Merge branch 'copy_of_original' of https://github.com/elimity-com/xlsx into copy_of_original

DamianSzkuat 6 лет назад
Родитель
Сommit
7e2b91f720
12 измененных файлов с 1085 добавлено и 7 удалено
  1. 1 0
      .gitignore
  2. 1 0
      col.go
  3. 14 0
      row.go
  4. 7 2
      sheet.go
  5. 51 0
      stream_cell.go
  6. 160 3
      stream_file.go
  7. 120 2
      stream_file_builder.go
  8. 120 0
      stream_style.go
  9. 571 0
      stream_style_test.go
  10. 23 0
      style.go
  11. 15 0
      write.go
  12. 2 0
      xmlWorksheet.go

+ 1 - 0
.gitignore

@@ -1,4 +1,5 @@
 .vscode
+.idea
 .DS_Store
 xlsx.test
 *.swp

+ 1 - 0
col.go

@@ -8,6 +8,7 @@ type Col struct {
 	Max          int
 	Hidden       bool
 	Width        float64
+	BestFit		 bool
 	Collapsed    bool
 	OutlineLevel uint8
 	numFmt       string

+ 14 - 0
row.go

@@ -25,3 +25,17 @@ func (r *Row) AddCell() *Cell {
 	r.Sheet.maybeAddCol(len(r.Cells))
 	return cell
 }
+
+// AddStreamCell takes as input a StreamCell, creates a new Cell from it,
+// and appends the new cell to the row.
+func (r *Row) AddStreamCell(streamCell StreamCell) {
+	cell := NewCell(r)
+	cell.Value = streamCell.cellData
+	cell.style = streamCell.cellStyle.style
+	cell.NumFmt = builtInNumFmt[streamCell.cellStyle.xNumFmtId]
+	cell.cellType = streamCell.cellType
+	r.Cells = append(r.Cells, cell)
+	r.Sheet.maybeAddCol(len(r.Cells))
+}
+
+

+ 7 - 2
sheet.go

@@ -136,7 +136,6 @@ func (s *Sheet) SetColWidth(startcol, endcol int, width float64) error {
 	for ; startcol < end; startcol++ {
 		s.Cols[startcol].Width = width
 	}
-
 	return nil
 }
 
@@ -256,10 +255,15 @@ func (s *Sheet) makeXLSXSheet(refTable *RefTable, styles *xlsxStyleSheet) *xlsxW
 		colsXfIdList[c] = XfId
 
 		var customWidth bool
+		// TODO test
+		var bestFit bool
 		if col.Width == 0 {
 			col.Width = ColWidth
 			customWidth = false
-
+			// TODO test
+			if col.BestFit{
+				bestFit = true
+			}
 		} else {
 			customWidth = true
 		}
@@ -268,6 +272,7 @@ func (s *Sheet) makeXLSXSheet(refTable *RefTable, styles *xlsxStyleSheet) *xlsxW
 				Max:          col.Max,
 				Hidden:       col.Hidden,
 				Width:        col.Width,
+				BestFit:	  bestFit,
 				CustomWidth:  customWidth,
 				Collapsed:    col.Collapsed,
 				OutlineLevel: col.OutlineLevel,

+ 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 StreamCell
+func NewStreamCell(cellData string, cellStyle StreamStyle, cellType CellType) StreamCell{
+	return StreamCell{
+		cellData:  cellData,
+		cellStyle: cellStyle,
+		cellType:  cellType,
+	}
+}
+
+// MakeStringStreamCell creates a new cell that holds string data, is of type string and uses general formatting
+func MakeStringStreamCell(cellData string) StreamCell{
+	return NewStreamCell(cellData, Strings, CellTypeString)
+}
+
+// MakeStyledStringStreamCell creates a new cell that holds a string and is styled according to the given style
+func MakeStyledStringStreamCell(cellData string, cellStyle StreamStyle) StreamCell {
+	return NewStreamCell(cellData, cellStyle, CellTypeString)
+}
+
+// MakeIntegerStreamCell creates a new cell that holds an integer value (represented as string),
+// is formatted as a standard integer and is of type numeric.
+func MakeIntegerStreamCell(cellData int) StreamCell {
+	return NewStreamCell(strconv.Itoa(cellData), Integers, CellTypeNumeric)
+}
+
+// MakeStyledIntegerStreamCell creates a new cell that holds an integer value (represented as string)
+// and is styled according to the given style.
+func MakeStyledIntegerStreamCell(cellData int, cellStyle StreamStyle) StreamCell {
+	return NewStreamCell(strconv.Itoa(cellData), cellStyle, CellTypeNumeric)
+}
+
+// MakeDateStreamCell creates a new cell that holds a date value and is formatted as dd-mm-yyyy and
+// and is of type numeric
+func MakeDateStreamCell(t time.Time) StreamCell {
+	excelTime := TimeToExcelTime(t, false)
+	return NewStreamCell(strconv.Itoa(int(excelTime)), Dates, CellTypeNumeric)
+}

+ 160 - 3
stream_file.go

@@ -15,6 +15,7 @@ type StreamFile struct {
 	zipWriter      *zip.Writer
 	currentSheet   *streamSheet
 	styleIds       [][]int
+	styleIdMap	   map[StreamStyle]int
 	err            error
 }
 
@@ -31,9 +32,10 @@ type streamSheet struct {
 }
 
 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
@@ -51,6 +53,22 @@ func (sf *StreamFile) Write(cells []string) error {
 	return sf.zipWriter.Flush()
 }
 
+// WriteWithStyle will write a row of cells to the current sheet. Every call to WriteWithStyle 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. WriteWithStyle supports all data types and styles that
+// are supported by StreamCell.
+func (sf *StreamFile) WriteWithStyle(cells []StreamCell) error {
+	if sf.err != nil {
+		return sf.err
+	}
+	err := sf.writeWithStyle(cells)
+	if err != nil {
+		sf.err = err
+		return err
+	}
+	return sf.zipWriter.Flush()
+}
+
 func (sf *StreamFile) WriteAll(records [][]string) error {
 	if sf.err != nil {
 		return sf.err
@@ -65,6 +83,23 @@ func (sf *StreamFile) WriteAll(records [][]string) error {
 	return sf.zipWriter.Flush()
 }
 
+// WriteAllWithStyle will write all the rows provided in records. All rows must have the same number of cells as
+// the headers. This function will always trigger a flush on success. WriteWithStyle supports all data types and
+// styles that are supported by StreamCell.
+func (sf *StreamFile) WriteAllWithStyle(records [][]StreamCell) error{
+	if sf.err != nil {
+		return sf.err
+	}
+	for _, row := range records {
+		err := sf.writeWithStyle(row)
+		if err != nil {
+			sf.err = err
+			return err
+		}
+	}
+	return sf.zipWriter.Flush()
+}
+
 func (sf *StreamFile) write(cells []string) error {
 	if sf.currentSheet == nil {
 		return NoCurrentSheetError
@@ -112,6 +147,128 @@ func (sf *StreamFile) write(cells []string) error {
 	return sf.zipWriter.Flush()
 }
 
+func (sf *StreamFile) writeWithStyle(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 {
+		// Get the cell reference (location)
+		cellCoordinate := GetCellIDStringFromCoords(colIndex, sf.currentSheet.rowCount-1)
+
+		// Get the cell type string
+		cellType, err := getCellTypeAsString(cell.cellType)
+		if err != nil {
+			return err
+		}
+
+		// Build the XML cell opening
+		cellOpen := `<c r="` + cellCoordinate + `" t="` + cellType + `"`
+		// Add in the style id of the stream cell.
+		if idx, ok := sf.styleIdMap[cell.cellStyle]; ok {
+			cellOpen += ` s="` + strconv.Itoa(idx) + `"`
+		} else {
+			return errors.New("trying to make use of a style that has not been added")
+		}
+		cellOpen += `>`
+
+		// The XML cell contents
+		cellContentsOpen, cellContentsClose, err := getCellContentOpenAncCloseTags(cell.cellType)
+		if err != nil {
+			return err
+		}
+
+		// The XMl cell ending
+		cellClose := `</c>`
+
+		// Write the cell opening
+		if err := sf.currentSheet.write(cellOpen); err != nil {
+			return err
+		}
+
+		// Write the cell contents opening
+		if err := sf.currentSheet.write(cellContentsOpen); err != nil {
+			return err
+		}
+
+		// Write cell contents
+		if err:= xml.EscapeText(sf.currentSheet.writer, []byte(cell.cellData)); err != nil {
+			return err
+		}
+
+		// Write cell contents ending
+		if err := sf.currentSheet.write(cellContentsClose); err != nil {
+			return err
+		}
+
+		// Write the cell ending
+		if err := sf.currentSheet.write(cellClose); err != nil {
+			return err
+		}
+	}
+	// Write the row ending
+	if err := sf.currentSheet.write(`</row>`); err != nil {
+		return err
+	}
+	return sf.zipWriter.Flush()
+}
+
+func getCellTypeAsString(cellType CellType) (string, 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 "b", nil
+	case CellTypeDate:
+		return "d", nil
+	case CellTypeError:
+		return "e", nil
+	case CellTypeInline:
+		return "inlineStr", nil
+	case CellTypeNumeric:
+		return "n", nil
+	case CellTypeString:
+		// TODO Currently shared strings are types as inline strings
+		return "inlineStr", nil
+		// return "s", nil
+	case CellTypeStringFormula:
+		return "str", nil
+	default:
+		return "", UnsupportedCellTypeError
+	}
+}
+
+func getCellContentOpenAncCloseTags(cellType CellType) (string, string, error) {
+	switch cellType{
+	case CellTypeString:
+		// TODO Currently shared strings are types as inline strings
+		return `<is><t>`, `</t></is>`, nil
+	case CellTypeInline:
+		return `<is><t>`, `</t></is>`, nil
+	case CellTypeStringFormula:
+		// Formulas are currently not supported
+		return ``, ``, UnsupportedCellTypeError
+	default:
+		return `<v>`, `</v>`, nil
+	}
+}
+
 // Error reports any error that has occurred during a previous Write or Flush.
 func (sf *StreamFile) Error() error {
 	return sf.err

+ 120 - 2
stream_file_builder.go

@@ -34,11 +34,15 @@ import (
 
 type StreamFileBuilder struct {
 	built              bool
+	firstSheetAdded    bool
+	customStylesAdded  bool
 	xlsxFile           *File
 	zipWriter          *zip.Writer
 	cellTypeToStyleIds map[CellType]int
 	maxStyleId         int
 	styleIds           [][]int
+	// streamStyles	   map[StreamStyle]struct{}
+	styleIdMap		   map[StreamStyle]int
 }
 
 const (
@@ -61,6 +65,8 @@ func NewStreamFileBuilder(writer io.Writer) *StreamFileBuilder {
 		xlsxFile:           NewFile(),
 		cellTypeToStyleIds: make(map[CellType]int),
 		maxStyleId:         initMaxStyleId,
+		// streamStyles: 		make(map[StreamStyle]struct{}),
+		styleIdMap:			make(map[StreamStyle]int),
 	}
 }
 
@@ -119,6 +125,49 @@ func (sb *StreamFileBuilder) AddSheet(name string, headers []string, cellTypes [
 	return nil
 }
 
+// TODO update comments
+// AddSheetWithStyle 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. Additionally AddSheetWithStyle allows to add Style information to the headers.
+func (sb *StreamFileBuilder) AddSheetWithStyle(name string, cells []StreamCell) 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 in the headers have been created
+	for _,cell := range cells{
+		if _, ok := sb.styleIdMap[cell.cellStyle]; !ok {
+			return errors.New("trying to make use of a style that has not been added")
+		}
+	}
+	// TODO Is needed for stream file to work but is not needed for streaming with styles
+	sb.styleIds = append(sb.styleIds, []int{})
+
+	// Set the values of the first row and the the number of columns
+	row := sheet.AddRow()
+	if count := row.WriteCellSlice(cells, -1); count != len(cells) {
+		// 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")
+	}
+
+	// Set default column types based on the cel types in the first row
+	for i, cell := range cells {
+		sheet.Cols[i].SetType(cell.cellType)
+		// TODO test
+		sheet.Cols[i].BestFit = true
+	}
+	return nil
+}
+
 // Build begins streaming the XLSX file to the io, by writing all the XLSX metadata. It creates a StreamFile struct
 // that can be used to write the rows to the sheets.
 func (sb *StreamFileBuilder) Build() (*StreamFile, error) {
@@ -126,16 +175,34 @@ func (sb *StreamFileBuilder) Build() (*StreamFile, error) {
 		return nil, BuiltStreamFileBuilderError
 	}
 	sb.built = true
+
+	// Marshall Parts resets the style sheet, so to keep style information that has been added by the user
+	// we have to marshal it beforehand and add it again after the entire file has been marshaled
+	var xmlStylesSheetString string
+	var err error
+	if sb.customStylesAdded{
+		xmlStylesSheetString, err = sb.marshalStyles()
+		if err != nil {
+			return nil, err
+		}
+	}
+
 	parts, err := sb.xlsxFile.MarshallParts()
 	if err != nil {
 		return nil, err
 	}
+
+	if sb.customStylesAdded{
+		parts["xl/styles.xml"] = xmlStylesSheetString
+	}
+
 	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,
 	}
 	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
@@ -162,6 +229,57 @@ func (sb *StreamFileBuilder) Build() (*StreamFile, error) {
 	return es, nil
 }
 
+func (sb *StreamFileBuilder) marshalStyles() (string, error) {
+	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 AddSheetWithStyle has been called, and if it is
+// called after AddSheetWithStyle it 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")
+//	}
+//	sb.streamStyles[streamStyle] = struct{}{}
+//	return nil
+//}
+
+// AddStreamStyle adds a new style to the style sheet.
+// Only Styles that have been added through either this function or AddStreamStyleList will be usable.
+// This function cannot be used after AddSheetWithStyle has been called, and if it is
+// called after AddSheetWithStyle it will return an error.
+func (sb *StreamFileBuilder) AddStreamStyle(streamStyle StreamStyle) error {
+	if sb.firstSheetAdded {
+		return errors.New("the style file has been built, cannot add new styles anymore")
+	}
+	if sb.xlsxFile.styles == nil {
+		sb.xlsxFile.styles = newXlsxStyleSheet(sb.xlsxFile.theme)
+	}
+	XfId := handleStyleForXLSX(streamStyle.style, streamStyle.xNumFmtId, sb.xlsxFile.styles)
+	sb.styleIdMap[streamStyle] = XfId
+	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 AddSheetWithStyle has been called, and if it is
+// called after AddSheetWithStyle 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
 // XML file so that these can be written at the right time.
 func (sb *StreamFileBuilder) processEmptySheetXML(sf *StreamFile, path, data string) error {
@@ -196,11 +314,11 @@ func getSheetIndex(sf *StreamFile, path string) (int, error) {
 	indexString := path[len(sheetFilePathPrefix) : len(path)-len(sheetFilePathSuffix)]
 	sheetXLSXIndex, err := strconv.Atoi(indexString)
 	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 ||
 		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
 	return sheetArrayIndex, nil

+ 120 - 0
stream_style.go

@@ -0,0 +1,120 @@
+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 Strings StreamStyle
+var BoldStrings StreamStyle
+var ItalicStrings StreamStyle
+var UnderlinedStrings StreamStyle
+
+var Integers StreamStyle
+var BoldIntegers StreamStyle
+var ItalicIntegers StreamStyle
+var UnderlinedIntegers StreamStyle
+
+var Dates StreamStyle
+
+var Decimals StreamStyle
+
+var Bold *Font
+var Italic *Font
+var Underlined *Font
+
+var GreenCell *Fill
+var RedCell *Fill
+var WhiteCel *Fill
+
+func init(){
+	// Init Fonts
+	Bold = DefaultFont()
+	Bold.Bold = true
+
+	Italic = DefaultFont()
+	Italic.Italic = true
+
+	Underlined = DefaultFont()
+	Underlined.Underline = true
+
+	// Init Fills
+	GreenCell = NewFill(Solid_Cell_Fill, RGB_Light_Green, RGB_White)
+	RedCell = NewFill(Solid_Cell_Fill, RGB_Light_Red, RGB_White)
+	WhiteCel = DefaultFill()
+
+	// Init default string styles
+	Strings = MakeStringStyle(DefaultFont(), DefaultFill(), DefaultAlignment(), DefaultBorder())
+	BoldStrings = MakeStringStyle(Bold, DefaultFill(), DefaultAlignment(), DefaultBorder())
+	ItalicStrings = MakeStringStyle(Italic, DefaultFill(), DefaultAlignment(), DefaultBorder())
+	UnderlinedStrings = MakeStringStyle(Underlined, DefaultFill(), DefaultAlignment(), DefaultBorder())
+
+	// Init default Integer styles
+	Integers = MakeIntegerStyle(DefaultFont(), DefaultFill(), DefaultAlignment(), DefaultBorder())
+	BoldIntegers = MakeIntegerStyle(Bold, DefaultFill(), DefaultAlignment(), DefaultBorder())
+	ItalicIntegers = MakeIntegerStyle(Italic, DefaultFill(), DefaultAlignment(), DefaultBorder())
+	UnderlinedIntegers = MakeIntegerStyle(Underlined, DefaultFill(), DefaultAlignment(), DefaultBorder())
+
+	Dates = MakeDateStyle(DefaultFont(), DefaultFill(), DefaultAlignment(), DefaultBorder())
+
+	Decimals = MakeDecimalStyle(DefaultFont(), DefaultFill(), DefaultAlignment(), DefaultBorder())
+}
+
+// MakeStyle creates a new StreamStyle and add it to the styles that will be streamed
+// This function returns a reference to the created StreamStyle
+func MakeStyle(formatStyleId 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: 	formatStyleId,
+		style: 		newStyle,
+	}
+
+	// DefaultStyles = append(DefaultStyles, newStreamStyle)
+	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)
+}

+ 571 - 0
stream_style_test.go

@@ -0,0 +1,571 @@
+package xlsx
+
+import (
+	"bytes"
+	"fmt"
+	. "gopkg.in/check.v1"
+	"io"
+	"reflect"
+	"time"
+)
+
+const (
+	StyleStreamTestsShouldMakeRealFiles = true
+)
+
+type StreamStyleSuite struct{}
+
+var _ = Suite(&StreamStyleSuite{})
+
+func (s *StreamSuite) 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 *StreamSuite) 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{
+				{
+					{MakeStringStreamCell("1"), MakeStringStreamCell("25"),
+						MakeStyledStringStreamCell("A", BoldStrings), MakeStringStreamCell("B")},
+					{MakeIntegerStreamCell(1234), MakeStyledIntegerStreamCell(98, BoldIntegers),
+						MakeStyledIntegerStreamCell(34, ItalicIntegers), MakeStyledIntegerStreamCell(26, UnderlinedIntegers)},
+				},
+			},
+		},
+		{
+			testName: "One Sheet",
+			sheetNames: []string{
+				"Sheet1",
+			},
+			workbookData: [][][]StreamCell{
+				{
+					{MakeStringStreamCell("Token"), MakeStringStreamCell("Name"),
+						MakeStringStreamCell("Price"), MakeStringStreamCell("SKU")},
+					{MakeIntegerStreamCell(123), MakeStringStreamCell("Taco"),
+						MakeIntegerStreamCell(300), MakeIntegerStreamCell(123)},
+				},
+			},
+		},
+		{
+			testName: "One Column",
+			sheetNames: []string{
+				"Sheet1",
+			},
+			workbookData: [][][]StreamCell{
+				{
+					{MakeStringStreamCell("Token")},
+					{MakeIntegerStreamCell(123)},
+				},
+			},
+		},
+		{
+			testName: "Several Sheets, with different numbers of columns and rows",
+			sheetNames: []string{
+				"Sheet 1", "Sheet 2", "Sheet3",
+			},
+			workbookData: [][][]StreamCell{
+				{
+					{MakeStringStreamCell("Token"), MakeStringStreamCell("Name"),
+						MakeStringStreamCell("Price"), MakeStringStreamCell("SKU")},
+
+					{MakeIntegerStreamCell(123), MakeStringStreamCell("Taco"),
+						MakeIntegerStreamCell(300), MakeIntegerStreamCell(123)},
+				},
+				{
+					{MakeStringStreamCell("Token"), MakeStringStreamCell("Name"),
+						MakeStringStreamCell("Price"), MakeStringStreamCell("SKU"),
+						MakeStringStreamCell("Stock")},
+
+					{MakeIntegerStreamCell(456), MakeStringStreamCell("Salsa"),
+						MakeIntegerStreamCell(200), MakeIntegerStreamCell(346),
+						MakeIntegerStreamCell(1)},
+
+					{MakeIntegerStreamCell(789), MakeStringStreamCell("Burritos"),
+						MakeIntegerStreamCell(400), MakeIntegerStreamCell(754),
+						MakeIntegerStreamCell(3)},
+				},
+				{
+					{MakeStringStreamCell("Token"), MakeStringStreamCell("Name"),
+						MakeStringStreamCell("Price")},
+
+					{MakeIntegerStreamCell(9853), MakeStringStreamCell("Guacamole"),
+						MakeIntegerStreamCell(500)},
+
+					{MakeIntegerStreamCell(2357), MakeStringStreamCell("Margarita"),
+						MakeIntegerStreamCell(700)},
+				},
+			},
+		},
+		{
+			testName: "Two Sheets with same the name",
+			sheetNames: []string{
+				"Sheet 1", "Sheet 1",
+			},
+			workbookData: [][][]StreamCell{
+				{
+					{MakeStringStreamCell("Token"), MakeStringStreamCell("Name"),
+						MakeStringStreamCell("Price"), MakeStringStreamCell("SKU")},
+
+					{MakeIntegerStreamCell(123), MakeStringStreamCell("Taco"),
+						MakeIntegerStreamCell(300), MakeIntegerStreamCell(123)},
+				},
+				{
+					{MakeStringStreamCell("Token"), MakeStringStreamCell("Name"),
+						MakeStringStreamCell("Price"), MakeStringStreamCell("SKU"),
+						MakeStringStreamCell("Stock")},
+
+					{MakeIntegerStreamCell(456), MakeStringStreamCell("Salsa"),
+						MakeIntegerStreamCell(200), MakeIntegerStreamCell(346),
+						MakeIntegerStreamCell(1)},
+
+					{MakeIntegerStreamCell(789), MakeStringStreamCell("Burritos"),
+						MakeIntegerStreamCell(400), MakeIntegerStreamCell(754),
+						MakeIntegerStreamCell(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{
+				{
+					{MakeStringStreamCell("Token"), MakeStringStreamCell("Name"),
+						MakeStringStreamCell("Price"), MakeStringStreamCell("SKU")},
+
+					{MakeIntegerStreamCell(123), MakeStringStreamCell("Taco"),
+						MakeIntegerStreamCell(300), MakeIntegerStreamCell(123)},
+				},
+				{
+					{MakeStringStreamCell("Token"), MakeStringStreamCell("Name"),
+						MakeStringStreamCell("Price"), MakeStringStreamCell("SKU")},
+
+					{MakeIntegerStreamCell(456), MakeStringStreamCell("Salsa"),
+						MakeIntegerStreamCell(200), MakeIntegerStreamCell(346)},
+				},
+			},
+			expectedError: AlreadyOnLastSheetError,
+		},
+		{
+			testName: "One Sheet, too many columns in row 1",
+			sheetNames: []string{
+				"Sheet 1",
+			},
+			workbookData: [][][]StreamCell{
+				{
+					{MakeStringStreamCell("Token"), MakeStringStreamCell("Name"),
+						MakeStringStreamCell("Price"), MakeStringStreamCell("SKU")},
+
+					{MakeIntegerStreamCell(123), MakeStringStreamCell("Taco"),
+						MakeIntegerStreamCell(300), MakeIntegerStreamCell(123),
+						MakeStringStreamCell("asdf")},
+				},
+			},
+			expectedError: WrongNumberOfRowsError,
+		},
+		{
+			testName: "One Sheet, too few columns in row 1",
+			sheetNames: []string{
+				"Sheet 1",
+			},
+			workbookData: [][][]StreamCell{
+				{
+					{MakeStringStreamCell("Token"), MakeStringStreamCell("Name"),
+						MakeStringStreamCell("Price"), MakeStringStreamCell("SKU")},
+
+					{MakeIntegerStreamCell(123), MakeStringStreamCell("Taco"),
+						MakeIntegerStreamCell(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{
+				{
+					{MakeStringStreamCell("Token"), MakeStringStreamCell("Name"),
+						MakeStringStreamCell("Price"), MakeStringStreamCell("SKU")},
+
+					{MakeIntegerStreamCell(123), MakeStringStreamCell("Taco"),
+						MakeIntegerStreamCell(300), MakeIntegerStreamCell(123)},
+				},
+				{{}},
+				{{MakeStringStreamCell("Id"), MakeStringStreamCell("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{
+				{
+					{MakeStringStreamCell("Token"), MakeStringStreamCell("Name"),
+						MakeStringStreamCell("Price"), MakeStringStreamCell("SKU")},
+
+					{MakeIntegerStreamCell(123), MakeStringStreamCell("Taco"),
+						MakeIntegerStreamCell(300), MakeIntegerStreamCell(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
+					{MakeStringStreamCell("Token"), MakeStringStreamCell(endSheetDataTag),
+						MakeStringStreamCell("Price"), MakeStringStreamCell(fmt.Sprintf(dimensionTag, "A1:D1"))},
+					// Japanese and emojis
+					{MakeIntegerStreamCell(123), MakeStringStreamCell("パーティーへ行かないか"),
+						MakeIntegerStreamCell(300), MakeStringStreamCell("🍕🐵 🙈 🙉 🙊")},
+					// XML encoder/parser test strings
+					{MakeIntegerStreamCell(123), MakeStringStreamCell(`<?xml version="1.0" encoding="ISO-8859-1"?>`),
+						MakeIntegerStreamCell(300), MakeStringStreamCell(`<?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
+					{MakeIntegerStreamCell(123), MakeStringStreamCell(`˙ɐ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˙Ɩ$-`), MakeIntegerStreamCell(300), MakeStringStreamCell(`ﷺ`)} ,
+					{MakeIntegerStreamCell(123), MakeStringStreamCell("Taco"),
+						MakeIntegerStreamCell(300), MakeIntegerStreamCell(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
+		}
+		// 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 := readXLSXFile(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]{
+				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")
+		}
+	}
+}
+
+// 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{Strings,BoldStrings,ItalicIntegers,UnderlinedStrings,
+						Integers, BoldIntegers, ItalicIntegers, UnderlinedIntegers,
+						Dates}
+	allStylesToBeAdded := append(defaultStyles, customStyles...)
+	err = file.AddStreamStyleList(allStylesToBeAdded)
+	if err != nil {
+		return err
+	}
+
+	for i, sheetName := range sheetNames {
+		header := workbookData[i][0]
+		err := file.AddSheetWithStyle(sheetName, header)
+		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
+			}
+		}
+		for i, row := range sheetData {
+			if i == 0 {
+				continue
+			}
+			err = streamFile.WriteWithStyle(row)
+			if err != nil {
+				return err
+			}
+		}
+	}
+	err = streamFile.Close()
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+func (s *StreamSuite) TestDates(t *C) {
+	var filePath string
+	var buffer bytes.Buffer
+	if StyleStreamTestsShouldMakeRealFiles {
+		filePath = fmt.Sprintf("Workbook_Date_test.xlsx")
+	}
+
+	sheetNames := []string{"Sheet1"}
+	workbookData := [][][]StreamCell{
+		{
+			{MakeStringStreamCell("Date:")},
+			{MakeDateStreamCell(time.Now())},
+		},
+	}
+
+	err := writeStreamFileWithStyle(filePath, &buffer, sheetNames, workbookData, StyleStreamTestsShouldMakeRealFiles, []StreamStyle{})
+	if err != nil {
+		t.Fatal("Error during writing")
+	}
+}
+
+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_Dard_Green
+	courier20 := NewFont(12, Courier)
+	courier20.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(0, timesNewRoman12, greenFill, DefaultAlignment(), DefaultBorder())
+	redStyle := MakeStyle(0, courier20, redFill, DefaultAlignment(), DefaultBorder())
+
+	sheetNames := []string{"Sheet1"}
+	workbookData := [][][]StreamCell{
+		{
+			{MakeStringStreamCell("Header1"), MakeStringStreamCell("Header2")},
+			{MakeStyledStringStreamCell("Good", greenStyle), MakeStyledStringStreamCell("Bad", redStyle)},
+		},
+	}
+
+	err := writeStreamFileWithStyle(filePath, &buffer, sheetNames, workbookData, TestsShouldMakeRealFiles, []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 !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, 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")
+	}
+}
+
+func (s *StreamSuite) TestCloseWithNothingWrittenToSheetsWithStyle(t *C) {
+	buffer := bytes.NewBuffer(nil)
+	file := NewStreamFileBuilder(buffer)
+
+	sheetNames := []string{"Sheet1", "Sheet2"}
+	workbookData := [][][]StreamCell{
+		{{MakeStringStreamCell("Header1"), MakeStringStreamCell("Header2")}},
+		{{MakeStringStreamCell("Header3"), MakeStringStreamCell("Header4")}},
+	}
+
+	defaultStyles := []StreamStyle{Strings,BoldStrings,ItalicIntegers,UnderlinedStrings,
+		Integers, BoldIntegers, ItalicIntegers, UnderlinedIntegers,
+		Dates}
+	err := file.AddStreamStyleList(defaultStyles)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	err = file.AddSheetWithStyle(sheetNames[0], workbookData[0][0])
+	if err != nil {
+		t.Fatal(err)
+	}
+	err = file.AddSheetWithStyle(sheetNames[1], workbookData[1][0])
+	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 := readXLSXFile(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 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")
+	}
+}
+
+func (s *StreamSuite) TestBuildErrorsAfterBuildWithStyle(t *C) {
+	file := NewStreamFileBuilder(bytes.NewBuffer(nil))
+
+	defaultStyles := []StreamStyle{Strings,BoldStrings,ItalicIntegers,UnderlinedStrings,
+		Integers, BoldIntegers, ItalicIntegers, UnderlinedIntegers,
+		Dates}
+	err := file.AddStreamStyleList(defaultStyles)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	err = file.AddSheetWithStyle("Sheet1", []StreamCell{MakeStringStreamCell("Header")})
+	if err != nil {
+		t.Fatal(err)
+	}
+	err = file.AddSheetWithStyle("Sheet2", []StreamCell{MakeStringStreamCell("Header")})
+	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 *StreamSuite) TestAddSheetWithStyleErrorsAfterBuild(t *C) {
+	file := NewStreamFileBuilder(bytes.NewBuffer(nil))
+
+	defaultStyles := []StreamStyle{Strings,BoldStrings,ItalicIntegers,UnderlinedStrings,
+		Integers, BoldIntegers, ItalicIntegers, UnderlinedIntegers,
+		Dates}
+	err := file.AddStreamStyleList(defaultStyles)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	err = file.AddSheetWithStyle("Sheet1", []StreamCell{MakeStringStreamCell("Header")})
+	if err != nil {
+		t.Fatal(err)
+	}
+	err = file.AddSheetWithStyle("Sheet2", []StreamCell{MakeStringStreamCell("Header2")})
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	_, err = file.Build()
+	if err != nil {
+		t.Fatal(err)
+	}
+	err = file.AddSheetWithStyle("Sheet3", []StreamCell{MakeStringStreamCell("Header3")})
+	if err != BuiltStreamFileBuilderError {
+		t.Fatal(err)
+	}
+}
+
+
+
+
+
+

+ 23 - 0
style.go

@@ -2,6 +2,29 @@ package xlsx
 
 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_Dard_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
 // the contents of Style within an XLSX file.
 type Style struct {

+ 15 - 0
write.go

@@ -63,6 +63,21 @@ func (r *Row) WriteSlice(e interface{}, cols int) int {
 	return i
 }
 
+// WriteCellSlice an array of Cell's to row r. Accepts an array of StreamCells
+// and writes the number of columns to write, 'cols'. If 'cols' is < 0,
+// the entire array will be written if possible. Returns the number of columns written.
+func (r *Row) WriteCellSlice(streamCells []StreamCell, cols int) int {
+	if cols == 0 {
+		return cols
+	}
+	i := 0
+	for _, streamCell := range streamCells {
+		r.AddStreamCell(streamCell)
+		i++
+	}
+	return i
+}
+
 // Writes a struct to row r. Accepts a pointer to struct type 'e',
 // and the number of columns to write, `cols`. If 'cols' is < 0,
 // the entire struct will be written if possible. Returns -1 if the 'e'

+ 2 - 0
xmlWorksheet.go

@@ -203,6 +203,8 @@ type xlsxCol struct {
 	Style        int     `xml:"style,attr"`
 	Width        float64 `xml:"width,attr"`
 	CustomWidth  bool    `xml:"customWidth,attr,omitempty"`
+	// TODO test
+	BestFit 	 bool    `xml:"bestFit,attr,omitempty"`
 	OutlineLevel uint8   `xml:"outlineLevel,attr,omitempty"`
 }