Browse Source

'WriteWithDefaultCellType' writes all cells in a column as the default cell type of that column

Alex Milkov 6 years ago
parent
commit
bc7ea55fcd
5 changed files with 425 additions and 9 deletions
  1. 13 0
      cell.go
  2. 37 0
      cell_test.go
  3. 2 0
      col.go
  4. 44 2
      stream_file.go
  5. 329 7
      stream_test.go

+ 13 - 0
cell.go

@@ -39,6 +39,19 @@ func (ct CellType) Ptr() *CellType {
 	return &ct
 	return &ct
 }
 }
 
 
+func (ct *CellType) fallbackTo(cellData string, fallback CellType) CellType {
+	if ct != nil {
+		switch *ct {
+		case CellTypeNumeric:
+			if _, err := strconv.ParseFloat(cellData, 64); err == nil {
+				return *ct
+			}
+		default:
+		}
+	}
+	return fallback
+}
+
 // Cell is a high level structure intended to provide user access to
 // Cell is a high level structure intended to provide user access to
 // the contents of Cell within an xlsx.Row.
 // the contents of Cell within an xlsx.Row.
 type Cell struct {
 type Cell struct {

+ 37 - 0
cell_test.go

@@ -806,3 +806,40 @@ func (s *CellSuite) TestIs12HourtTime(c *C) {
 	c.Assert(is12HourTime("A/P"), Equals, true)
 	c.Assert(is12HourTime("A/P"), Equals, true)
 	c.Assert(is12HourTime("x"), Equals, false)
 	c.Assert(is12HourTime("x"), Equals, false)
 }
 }
+
+func (s *CellSuite) TestFallbackTo(c *C) {
+	testCases := []struct {
+		cellType       *CellType
+		cellData       string
+		fallback       CellType
+		expectedReturn CellType
+	}{
+		{
+			cellType:       CellTypeNumeric.Ptr(),
+			cellData:       `string`,
+			fallback:       CellTypeString,
+			expectedReturn: CellTypeString,
+		},
+		{
+			cellType:       nil,
+			cellData:       `string`,
+			fallback:       CellTypeNumeric,
+			expectedReturn: CellTypeNumeric,
+		},
+		{
+			cellType:       CellTypeNumeric.Ptr(),
+			cellData:       `300.24`,
+			fallback:       CellTypeString,
+			expectedReturn: CellTypeNumeric,
+		},
+		{
+			cellType:       CellTypeNumeric.Ptr(),
+			cellData:       `300`,
+			fallback:       CellTypeString,
+			expectedReturn: CellTypeNumeric,
+		},
+	}
+	for _, testCase := range testCases {
+		c.Assert(testCase.cellType.fallbackTo(testCase.cellData, testCase.fallback), Equals, testCase.expectedReturn)
+	}
+}

+ 2 - 0
col.go

@@ -16,11 +16,13 @@ type Col struct {
 	parsedNumFmt   *parsedNumberFormat
 	parsedNumFmt   *parsedNumberFormat
 	style          *Style
 	style          *Style
 	DataValidation []*xlsxCellDataValidation
 	DataValidation []*xlsxCellDataValidation
+	defaultCellType *CellType
 }
 }
 
 
 // SetType will set the format string of a column based on the type that you want to set it to.
 // SetType will set the format string of a column based on the type that you want to set it to.
 // This function does not really make a lot of sense.
 // This function does not really make a lot of sense.
 func (c *Col) SetType(cellType CellType) {
 func (c *Col) SetType(cellType CellType) {
+	c.defaultCellType = &cellType
 	switch cellType {
 	switch cellType {
 	case CellTypeString:
 	case CellTypeString:
 		c.numFmt = builtInNumFmt[builtInNumFmtIndex_STRING]
 		c.numFmt = builtInNumFmt[builtInNumFmtIndex_STRING]

+ 44 - 2
stream_file.go

@@ -53,6 +53,24 @@ func (sf *StreamFile) Write(cells []string) error {
 	return sf.zipWriter.Flush()
 	return sf.zipWriter.Flush()
 }
 }
 
 
+// WriteWithDefaultCellType will write a row of cells to the current sheet. Every call to WriteWithDefaultCellType
+// on the same sheet must contain the same number of cells as the header provided when the sheet was created or
+// an error will be returned. This function will always trigger a flush on success. Each cell will have the
+// default cell type of the column that it belongs to. However, if the cell data string cannot be
+// parsed into said cell type, we fall back on encoding the cell as a string
+// In addition, note that cells WILL NOT be styled
+func (sf *StreamFile) WriteWithDefaultCellType(cells []string) error {
+	if sf.err != nil {
+		return sf.err
+	}
+	err := sf.writeWithColumnDefaultCellType(cells)
+	if err != nil {
+		sf.err = err
+		return err
+	}
+	return sf.zipWriter.Flush()
+}
+
 // WriteS will write a row of cells to the current sheet. Every call to WriteS on the same sheet must
 // 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
 // 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
 // will be returned. This function will always trigger a flush on success. WriteS supports all data types
@@ -147,6 +165,30 @@ func (sf *StreamFile) write(cells []string) error {
 	return sf.zipWriter.Flush()
 	return sf.zipWriter.Flush()
 }
 }
 
 
+func (sf *StreamFile) writeWithColumnDefaultCellType(cells []string) error {
+
+	if sf.currentSheet == nil {
+		return NoCurrentSheetError
+	}
+	if len(cells) != sf.currentSheet.columnCount {
+		return WrongNumberOfRowsError
+	}
+
+	currentSheet := sf.xlsxFile.Sheets[sf.currentSheet.index-1]
+
+	var streamCells []StreamCell
+	for colIndex, col := range currentSheet.Cols {
+		streamCells = append(
+			streamCells,
+			NewStreamCell(
+				cells[colIndex],
+				StreamStyle{},
+				col.defaultCellType.fallbackTo(cells[colIndex], CellTypeString),
+			))
+	}
+	return sf.writeS(streamCells)
+}
+
 func (sf *StreamFile) writeS(cells []StreamCell) error {
 func (sf *StreamFile) writeS(cells []StreamCell) error {
 	if sf.currentSheet == nil {
 	if sf.currentSheet == nil {
 		return NoCurrentSheetError
 		return NoCurrentSheetError
@@ -219,7 +261,7 @@ func makeXlsxCell(cellType CellType, cellCoordinate string, cellStyleId int, cel
 		return xlsxC{XMLName: xml.Name{Local: "c"}, R: cellCoordinate, S: cellStyleId, T: "b", V: cellData}, nil
 		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
 	// Dates are better represented using CellTyleNumeric and the date formatting
 	//case CellTypeDate:
 	//case CellTypeDate:
-		//return xlsxC{XMLName: xml.Name{Local: "c"}, R: cellCoordinate, S: cellStyleId, T: "d", V: cellData}, nil
+	//return xlsxC{XMLName: xml.Name{Local: "c"}, R: cellCoordinate, S: cellStyleId, T: "d", V: cellData}, nil
 	case CellTypeError:
 	case CellTypeError:
 		return xlsxC{XMLName: xml.Name{Local: "c"}, R: cellCoordinate, S: cellStyleId, T: "e", V: cellData}, nil
 		return xlsxC{XMLName: xml.Name{Local: "c"}, R: cellCoordinate, S: cellStyleId, T: "e", V: cellData}, nil
 	case CellTypeInline:
 	case CellTypeInline:
@@ -231,7 +273,7 @@ func makeXlsxCell(cellType CellType, cellCoordinate string, cellStyleId int, cel
 		return xlsxC{XMLName: xml.Name{Local: "c"}, R: cellCoordinate, S: cellStyleId, T: "inlineStr", Is: &xlsxSI{T: cellData}}, nil
 		return xlsxC{XMLName: xml.Name{Local: "c"}, R: cellCoordinate, S: cellStyleId, T: "inlineStr", Is: &xlsxSI{T: cellData}}, nil
 	// TODO currently not supported
 	// TODO currently not supported
 	// case CellTypeStringFormula:
 	// case CellTypeStringFormula:
-		// return xlsxC{}, UnsupportedCellTypeError
+	// return xlsxC{}, UnsupportedCellTypeError
 	default:
 	default:
 		return xlsxC{}, UnsupportedCellTypeError
 		return xlsxC{}, UnsupportedCellTypeError
 	}
 	}

+ 329 - 7
stream_test.go

@@ -239,7 +239,7 @@ func (s *StreamSuite) TestXlsxStreamWrite(t *C) {
 		if TestsShouldMakeRealFiles {
 		if TestsShouldMakeRealFiles {
 			filePath = fmt.Sprintf("Workbook%d.xlsx", i)
 			filePath = fmt.Sprintf("Workbook%d.xlsx", i)
 		}
 		}
-		err := writeStreamFile(filePath, &buffer, testCase.sheetNames, testCase.workbookData, testCase.headerTypes, TestsShouldMakeRealFiles)
+		err := writeStreamFile(filePath, &buffer, testCase.sheetNames, testCase.workbookData, testCase.headerTypes, TestsShouldMakeRealFiles, false)
 		if err != testCase.expectedError && err.Error() != testCase.expectedError.Error() {
 		if err != testCase.expectedError && err.Error() != testCase.expectedError.Error() {
 			t.Fatalf("Error differs from expected error. Error: %v, Expected Error: %v ", err, testCase.expectedError)
 			t.Fatalf("Error differs from expected error. Error: %v, Expected Error: %v ", err, testCase.expectedError)
 		}
 		}
@@ -253,7 +253,7 @@ func (s *StreamSuite) TestXlsxStreamWrite(t *C) {
 			bufReader = bytes.NewReader(buffer.Bytes())
 			bufReader = bytes.NewReader(buffer.Bytes())
 			size = bufReader.Size()
 			size = bufReader.Size()
 		}
 		}
-		actualSheetNames, actualWorkbookData := readXLSXFile(t, filePath, bufReader, size, TestsShouldMakeRealFiles)
+		actualSheetNames, actualWorkbookData, _ := readXLSXFile(t, filePath, bufReader, size, TestsShouldMakeRealFiles)
 		// check if data was able to be read correctly
 		// check if data was able to be read correctly
 		if !reflect.DeepEqual(actualSheetNames, testCase.sheetNames) {
 		if !reflect.DeepEqual(actualSheetNames, testCase.sheetNames) {
 			t.Fatal("Expected sheet names to be equal")
 			t.Fatal("Expected sheet names to be equal")
@@ -264,6 +264,318 @@ func (s *StreamSuite) TestXlsxStreamWrite(t *C) {
 	}
 	}
 }
 }
 
 
+func (s *StreamSuite) TestXlsxStreamWriteWithDefaultCellType(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  [][][]string
+		headerTypes   [][]*CellType
+		expectedError error
+	}{
+		{
+			testName: "One Sheet",
+			sheetNames: []string{
+				"Sheet1",
+			},
+			workbookData: [][][]string{
+				{
+					{"Token", "Name", "Price", "SKU"},
+					{"123", "Taco", "300", "0000000123"},
+				},
+			},
+			headerTypes: [][]*CellType{
+				{nil, CellTypeString.Ptr(), CellTypeNumeric.Ptr(), CellTypeString.Ptr()},
+			},
+		},
+		{
+			testName: "One Column",
+			sheetNames: []string{
+				"Sheet1",
+			},
+			workbookData: [][][]string{
+				{
+					{"Token"},
+					{"123"},
+				},
+			},
+			headerTypes: [][]*CellType{
+				{CellTypeNumeric.Ptr()},
+			},
+		},
+		{
+			testName: "Several Sheets, with different numbers of columns and rows",
+			sheetNames: []string{
+				"Sheet 1", "Sheet 2", "Sheet3",
+			},
+			workbookData: [][][]string{
+				{
+					{"Token", "Name", "Price", "SKU"},
+					{"123", "Taco", "variable", "0000000123"},
+				},
+				{
+					{"Token", "Name", "Price", "SKU", "Stock"},
+					{"456", "Salsa", "200", "0346", "1"},
+					{"789", "Burritos", "400", "754", "3"},
+				},
+				{
+					{"Token", "Name", "Price"},
+					{"9853", "Guacamole", "500"},
+					{"2357", "Margarita", "700"},
+				},
+			},
+			headerTypes: [][]*CellType{
+				{CellTypeNumeric.Ptr(), CellTypeString.Ptr(), CellTypeNumeric.Ptr(), CellTypeString.Ptr()},
+				{nil, CellTypeString.Ptr(), nil, CellTypeString.Ptr(), nil},
+				{nil, nil, nil},
+			},
+		},
+		{
+			testName: "Two Sheets with same the name",
+			sheetNames: []string{
+				"Sheet 1", "Sheet 1",
+			},
+			workbookData: [][][]string{
+				{
+					{"Token", "Name", "Price", "SKU"},
+					{"123", "Taco", "300", "0000000123"},
+				},
+				{
+					{"Token", "Name", "Price", "SKU", "Stock"},
+					{"456", "Salsa", "200", "0346", "1"},
+					{"789", "Burritos", "400", "754", "3"},
+				},
+			},
+			expectedError: fmt.Errorf("duplicate sheet name '%s'.", "Sheet 1"),
+		},
+		{
+			testName: "One Sheet Registered, tries to write to two",
+			sheetNames: []string{
+				"Sheet 1",
+			},
+			workbookData: [][][]string{
+				{
+					{"Token", "Name", "Price", "SKU"},
+					{"123", "Taco", "300", "0000000123"},
+				},
+				{
+					{"Token", "Name", "Price", "SKU"},
+					{"456", "Salsa", "200", "0346"},
+				},
+			},
+			expectedError: AlreadyOnLastSheetError,
+		},
+		{
+			testName: "One Sheet, too many columns in row 1",
+			sheetNames: []string{
+				"Sheet 1",
+			},
+			workbookData: [][][]string{
+				{
+					{"Token", "Name", "Price", "SKU"},
+					{"123", "Taco", "300", "0000000123", "asdf"},
+				},
+			},
+			expectedError: WrongNumberOfRowsError,
+		},
+		{
+			testName: "One Sheet, too few columns in row 1",
+			sheetNames: []string{
+				"Sheet 1",
+			},
+			workbookData: [][][]string{
+				{
+					{"Token", "Name", "Price", "SKU"},
+					{"123", "Taco", "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: [][][]string{
+				{
+					{"Token", "Name", "Price", "SKU"},
+					{"123", "Taco", "300", "0000000123"},
+				},
+				{{}},
+				{{"Id", "Unit Cost"}},
+				{{}},
+				{{}},
+				{{}},
+			},
+			headerTypes: [][]*CellType{
+				{CellTypeNumeric.Ptr(), CellTypeString.Ptr(), CellTypeNumeric.Ptr(), CellTypeString.Ptr()},
+				{nil},
+				{nil, nil},
+				{nil},
+				{nil},
+				{nil},
+			},
+		},
+		{
+			testName: "Two Sheets, only writes to one, should not error and should still create a valid file",
+			sheetNames: []string{
+				"Sheet 1", "Sheet 2",
+			},
+			workbookData: [][][]string{
+				{
+					{"Token", "Name", "Price", "SKU"},
+					{"123", "Taco", "300", "0000000123"},
+				},
+				{{}},
+			},
+			headerTypes: [][]*CellType{
+				{CellTypeDate.Ptr(), CellTypeDate.Ptr(), CellTypeDate.Ptr(), CellTypeString.Ptr()},
+				{nil},
+			},
+		},
+		{
+			testName: "Larger Sheet",
+			sheetNames: []string{
+				"Sheet 1",
+			},
+			workbookData: [][][]string{
+				{
+					{"Token", "Name", "Price", "SKU", "Token", "Name", "Price", "SKU", "Token", "Name", "Price", "SKU", "Token", "Name", "Price", "SKU", "Token", "Name", "Price", "SKU", "Token", "Name", "Price", "SKU"},
+					{"123", "Taco", "300", "0000000123", "123", "Taco", "300", "0000000123", "123", "Taco", "300", "0000000123", "123", "Taco", "300", "0000000123", "123", "Taco", "300", "0000000123", "123", "Taco", "300", "0000000123"},
+					{"456", "Salsa", "200", "0346", "456", "Salsa", "200", "0346", "456", "Salsa", "200", "0346", "456", "Salsa", "200", "0346", "456", "Salsa", "200", "0346", "456", "Salsa", "200", "0346"},
+					{"789", "Burritos", "400", "754", "789", "Burritos", "400", "754", "789", "Burritos", "400", "754", "789", "Burritos", "400", "754", "789", "Burritos", "400", "754", "789", "Burritos", "400", "754"},
+					{"123", "Taco", "300", "0000000123", "123", "Taco", "300", "0000000123", "123", "Taco", "300", "0000000123", "123", "Taco", "300", "0000000123", "123", "Taco", "300", "0000000123", "123", "Taco", "300", "0000000123"},
+					{"456", "Salsa", "200", "0346", "456", "Salsa", "200", "0346", "456", "Salsa", "200", "0346", "456", "Salsa", "200", "0346", "456", "Salsa", "200", "0346", "456", "Salsa", "200", "0346"},
+					{"789", "Burritos", "400", "754", "789", "Burritos", "400", "754", "789", "Burritos", "400", "754", "789", "Burritos", "400", "754", "789", "Burritos", "400", "754", "789", "Burritos", "400", "754"},
+					{"123", "Taco", "300", "0000000123", "123", "Taco", "300", "0000000123", "123", "Taco", "300", "0000000123", "123", "Taco", "300", "0000000123", "123", "Taco", "300", "0000000123", "123", "Taco", "300", "0000000123"},
+					{"456", "Salsa", "200", "0346", "456", "Salsa", "200", "0346", "456", "Salsa", "200", "0346", "456", "Salsa", "200", "0346", "456", "Salsa", "200", "0346", "456", "Salsa", "200", "0346"},
+					{"789", "Burritos", "400", "754", "789", "Burritos", "400", "754", "789", "Burritos", "400", "754", "789", "Burritos", "400", "754", "789", "Burritos", "400", "754", "789", "Burritos", "400", "754"},
+					{"123", "Taco", "300", "0000000123", "123", "Taco", "300", "0000000123", "123", "Taco", "300", "0000000123", "123", "Taco", "300", "0000000123", "123", "Taco", "300", "0000000123", "123", "Taco", "300", "0000000123"},
+					{"456", "Salsa", "200", "0346", "456", "Salsa", "200", "0346", "456", "Salsa", "200", "0346", "456", "Salsa", "200", "0346", "456", "Salsa", "200", "0346", "456", "Salsa", "200", "0346"},
+					{"789", "Burritos", "400", "754", "789", "Burritos", "400", "754", "789", "Burritos", "400", "754", "789", "Burritos", "400", "754", "789", "Burritos", "400", "754", "789", "Burritos", "400", "754"},
+					{"123", "Taco", "300", "0000000123", "123", "Taco", "300", "0000000123", "123", "Taco", "300", "0000000123", "123", "Taco", "300", "0000000123", "123", "Taco", "300", "0000000123", "123", "Taco", "300", "0000000123"},
+					{"456", "Salsa", "200", "0346", "456", "Salsa", "200", "0346", "456", "Salsa", "200", "0346", "456", "Salsa", "200", "0346", "456", "Salsa", "200", "0346", "456", "Salsa", "200", "0346"},
+					{"789", "Burritos", "400", "754", "789", "Burritos", "400", "754", "789", "Burritos", "400", "754", "789", "Burritos", "400", "754", "789", "Burritos", "400", "754", "789", "Burritos", "400", "754"},
+					{"123", "Taco", "300", "0000000123", "123", "Taco", "300", "0000000123", "123", "Taco", "300", "0000000123", "123", "Taco", "300", "0000000123", "123", "Taco", "300", "0000000123", "123", "Taco", "300", "0000000123"},
+					{"456", "Salsa", "200", "0346", "456", "Salsa", "200", "0346", "456", "Salsa", "200", "0346", "456", "Salsa", "200", "0346", "456", "Salsa", "200", "0346", "456", "Salsa", "200", "0346"},
+					{"789", "Burritos", "400", "754", "789", "Burritos", "400", "754", "789", "Burritos", "400", "754", "789", "Burritos", "400", "754", "789", "Burritos", "400", "754", "789", "Burritos", "400", "754"},
+					{"123", "Taco", "300", "0000000123", "123", "Taco", "300", "0000000123", "123", "Taco", "300", "0000000123", "123", "Taco", "300", "0000000123", "123", "Taco", "300", "0000000123", "123", "Taco", "300", "0000000123"},
+					{"456", "Salsa", "200", "0346", "456", "Salsa", "200", "0346", "456", "Salsa", "200", "0346", "456", "Salsa", "200", "0346", "456", "Salsa", "200", "0346", "456", "Salsa", "200", "0346"},
+					{"789", "Burritos", "400", "754", "789", "Burritos", "400", "754", "789", "Burritos", "400", "754", "789", "Burritos", "400", "754", "789", "Burritos", "400", "754", "789", "Burritos", "400", "754"},
+					{"123", "Taco", "300", "0000000123", "123", "Taco", "300", "0000000123", "123", "Taco", "300", "0000000123", "123", "Taco", "300", "0000000123", "123", "Taco", "300", "0000000123", "123", "Taco", "300", "0000000123"},
+					{"456", "Salsa", "200", "0346", "456", "Salsa", "200", "0346", "456", "Salsa", "200", "0346", "456", "Salsa", "200", "0346", "456", "Salsa", "200", "0346", "456", "Salsa", "200", "0346"},
+					{"789", "Burritos", "400", "754", "789", "Burritos", "400", "754", "789", "Burritos", "400", "754", "789", "Burritos", "400", "754", "789", "Burritos", "400", "754", "789", "Burritos", "400", "754"},
+				},
+			},
+			headerTypes: [][]*CellType{
+				{CellTypeNumeric.Ptr(), CellTypeDate.Ptr(), CellTypeDate.Ptr(), CellTypeString.Ptr(), CellTypeDate.Ptr(), CellTypeDate.Ptr(), CellTypeDate.Ptr(), CellTypeDate.Ptr(), CellTypeDate.Ptr(), CellTypeDate.Ptr(), CellTypeDate.Ptr(), CellTypeDate.Ptr(), CellTypeDate.Ptr(), CellTypeDate.Ptr(), CellTypeDate.Ptr(), CellTypeDate.Ptr(), CellTypeDate.Ptr(), CellTypeDate.Ptr(), CellTypeDate.Ptr(), CellTypeDate.Ptr(), CellTypeDate.Ptr(), CellTypeDate.Ptr(), CellTypeDate.Ptr(), CellTypeDate.Ptr()},
+			},
+		},
+		{
+			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: [][][]string{
+				{
+					// String courtesy of https://github.com/minimaxir/big-list-of-naughty-strings/
+					// Header row contains the tags that I am filtering on
+					{"Token", endSheetDataTag, "Price", fmt.Sprintf(dimensionTag, "A1:D1")},
+					// Japanese and emojis
+					{"123", "パーティーへ行かないか", "300", "🍕🐵 🙈 🙉 🙊"},
+					// XML encoder/parser test strings
+					{"123", `<?xml version="1.0" encoding="ISO-8859-1"?>`, "300", `<?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
+					{"123", `˙ɐ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˙Ɩ$-`, "300", `ﷺ`},
+					{"123", "Taco", "300", "0000000123"},
+				},
+			},
+			headerTypes: [][]*CellType{
+				{CellTypeDate.Ptr(), CellTypeDate.Ptr(), CellTypeDate.Ptr(), CellTypeString.Ptr(), CellTypeString.Ptr()},
+			},
+		},
+	}
+	for i, testCase := range testCases {
+
+		var filePath string
+		var buffer bytes.Buffer
+		if TestsShouldMakeRealFiles {
+			filePath = fmt.Sprintf("WorkbookTyped%d.xlsx", i)
+		}
+		err := writeStreamFile(filePath, &buffer, testCase.sheetNames, testCase.workbookData, testCase.headerTypes, TestsShouldMakeRealFiles, true)
+		if err != testCase.expectedError && err.Error() != testCase.expectedError.Error() {
+			t.Fatalf("Error differs from expected error. Error: %v, Expected Error: %v ", err, testCase.expectedError)
+		}
+		if testCase.expectedError != nil {
+			return
+		}
+		// read the file back with the xlsx package
+		var bufReader *bytes.Reader
+		var size int64
+		if !TestsShouldMakeRealFiles {
+			bufReader = bytes.NewReader(buffer.Bytes())
+			size = bufReader.Size()
+		}
+		actualSheetNames, actualWorkbookData, workbookCellTypes := readXLSXFile(t, filePath, bufReader, size, TestsShouldMakeRealFiles)
+		verifyCellTypesInColumnMatchHeaderType(t, workbookCellTypes, testCase.headerTypes, testCase.workbookData)
+		// check if data was able to be read correctly
+		if !reflect.DeepEqual(actualSheetNames, testCase.sheetNames) {
+			t.Fatal("Expected sheet names to be equal")
+		}
+		if !reflect.DeepEqual(actualWorkbookData, testCase.workbookData) {
+			t.Fatal("Expected workbook data to be equal")
+		}
+		/*if len(testCase.headerTypes) != 0 {
+			testCaseLogic(true)
+		}*/
+	}
+}
+
+// Ensures that the cell type of all cells in each column across all sheets matches the provided header types
+// in each corresponding sheet
+func verifyCellTypesInColumnMatchHeaderType(t *C, workbookCellTypes [][][]CellType, headerTypes [][]*CellType, workbookData [][][]string) {
+
+	numSheets := len(workbookCellTypes)
+	numHeaders := len(headerTypes)
+	if numSheets != numHeaders {
+		t.Fatalf("Number of sheets in workbook: %d not equal to number of sheet headers: %d", numSheets, numHeaders)
+	}
+
+	for sheetI, headers := range headerTypes {
+		var sanitizedHeaders []CellType
+		for _, headerType := range headers {
+			// if `Col.defaultCellType` is `nil` in `StreamFile.writeWithColumnDefaultCellType` we give it a
+			// `CellTypeString` by default but then later `StreamFile.makeXlsxCell` actually encodes `CellTypeString` as
+			// `CellTypeInline` before marshalling
+			if headerType == (*CellType)(nil) || *headerType == CellTypeString {
+				sanitizedHeaders = append(sanitizedHeaders, CellTypeInline)
+			} else {
+				sanitizedHeaders = append(sanitizedHeaders, *headerType)
+			}
+		}
+
+		sheet := workbookCellTypes[sheetI]
+		// Skip header row
+		for rowI, row := range sheet[1:] {
+			if len(row) != len(headers) {
+				t.Fatalf("Number of cells in row: %d not equal number of headers; %d", len(row), len(headers))
+			}
+			for colI, cellType := range row {
+				headerTypeForCol := sanitizedHeaders[colI]
+				if cellType != headerTypeForCol.fallbackTo(workbookData[sheetI][rowI+1][colI], CellTypeInline) {
+					t.Fatalf("Cell type %d in row: %d and col: %d does not match header type: %d for this col in sheet: %d",
+						cellType, rowI, colI, headerTypeForCol, sheetI)
+				}
+			}
+		}
+	}
+
+}
+
 // The purpose of TestXlsxStyleBehavior is to ensure that initMaxStyleId has the correct starting value
 // The purpose of TestXlsxStyleBehavior is to ensure that initMaxStyleId has the correct starting value
 // and that the logic in AddSheet() that predicts Style IDs is correct.
 // and that the logic in AddSheet() that predicts Style IDs is correct.
 func (s *StreamSuite) TestXlsxStyleBehavior(t *C) {
 func (s *StreamSuite) TestXlsxStyleBehavior(t *C) {
@@ -317,7 +629,7 @@ func (s *StreamSuite) TestXlsxStyleBehavior(t *C) {
 }
 }
 
 
 // writeStreamFile will write the file using this stream package
 // writeStreamFile will write the file using this stream package
-func writeStreamFile(filePath string, fileBuffer io.Writer, sheetNames []string, workbookData [][][]string, headerTypes [][]*CellType, shouldMakeRealFiles bool) error {
+func writeStreamFile(filePath string, fileBuffer io.Writer, sheetNames []string, workbookData [][][]string, headerTypes [][]*CellType, shouldMakeRealFiles bool, useHeaderTypeAsCellType bool) error {
 	var file *StreamFileBuilder
 	var file *StreamFileBuilder
 	var err error
 	var err error
 	if shouldMakeRealFiles {
 	if shouldMakeRealFiles {
@@ -354,7 +666,11 @@ func writeStreamFile(filePath string, fileBuffer io.Writer, sheetNames []string,
 			if i == 0 {
 			if i == 0 {
 				continue
 				continue
 			}
 			}
-			err = streamFile.Write(row)
+			if useHeaderTypeAsCellType {
+				err = streamFile.WriteWithDefaultCellType(row)
+			} else {
+				err = streamFile.Write(row)
+			}
 			if err != nil {
 			if err != nil {
 				return err
 				return err
 			}
 			}
@@ -368,7 +684,7 @@ func writeStreamFile(filePath string, fileBuffer io.Writer, sheetNames []string,
 }
 }
 
 
 // readXLSXFile will read the file using the xlsx package.
 // readXLSXFile will read the file using the xlsx package.
-func readXLSXFile(t *C, filePath string, fileBuffer io.ReaderAt, size int64, shouldMakeRealFiles bool) ([]string, [][][]string) {
+func readXLSXFile(t *C, filePath string, fileBuffer io.ReaderAt, size int64, shouldMakeRealFiles bool) ([]string, [][][]string, [][][]CellType) {
 	var readFile *File
 	var readFile *File
 	var err error
 	var err error
 	if shouldMakeRealFiles {
 	if shouldMakeRealFiles {
@@ -383,24 +699,30 @@ func readXLSXFile(t *C, filePath string, fileBuffer io.ReaderAt, size int64, sho
 		}
 		}
 	}
 	}
 	var actualWorkbookData [][][]string
 	var actualWorkbookData [][][]string
+	var workbookCellTypes [][][]CellType
 	var sheetNames []string
 	var sheetNames []string
 	for _, sheet := range readFile.Sheets {
 	for _, sheet := range readFile.Sheets {
 		sheetData := [][]string{}
 		sheetData := [][]string{}
+		sheetCellTypes := [][]CellType{}
 		for _, row := range sheet.Rows {
 		for _, row := range sheet.Rows {
 			data := []string{}
 			data := []string{}
+			cellTypes := []CellType{}
 			for _, cell := range row.Cells {
 			for _, cell := range row.Cells {
 				str, err := cell.FormattedValue()
 				str, err := cell.FormattedValue()
 				if err != nil {
 				if err != nil {
 					t.Fatal(err)
 					t.Fatal(err)
 				}
 				}
 				data = append(data, str)
 				data = append(data, str)
+				cellTypes = append(cellTypes, cell.Type())
 			}
 			}
 			sheetData = append(sheetData, data)
 			sheetData = append(sheetData, data)
+			sheetCellTypes = append(sheetCellTypes, cellTypes)
 		}
 		}
 		sheetNames = append(sheetNames, sheet.Name)
 		sheetNames = append(sheetNames, sheet.Name)
 		actualWorkbookData = append(actualWorkbookData, sheetData)
 		actualWorkbookData = append(actualWorkbookData, sheetData)
+		workbookCellTypes = append(workbookCellTypes, sheetCellTypes)
 	}
 	}
-	return sheetNames, actualWorkbookData
+	return sheetNames, actualWorkbookData, workbookCellTypes
 }
 }
 
 
 func (s *StreamSuite) TestAddSheetErrorsAfterBuild(t *C) {
 func (s *StreamSuite) TestAddSheetErrorsAfterBuild(t *C) {
@@ -476,7 +798,7 @@ func (s *StreamSuite) TestCloseWithNothingWrittenToSheets(t *C) {
 	bufReader := bytes.NewReader(buffer.Bytes())
 	bufReader := bytes.NewReader(buffer.Bytes())
 	size := bufReader.Size()
 	size := bufReader.Size()
 
 
-	actualSheetNames, actualWorkbookData := readXLSXFile(t, "", bufReader, size, false)
+	actualSheetNames, actualWorkbookData, _ := readXLSXFile(t, "", bufReader, size, false)
 	// check if data was able to be read correctly
 	// check if data was able to be read correctly
 	if !reflect.DeepEqual(actualSheetNames, sheetNames) {
 	if !reflect.DeepEqual(actualSheetNames, sheetNames) {
 		t.Fatal("Expected sheet names to be equal")
 		t.Fatal("Expected sheet names to be equal")