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

CellType support

Adding cell type support for streaming
DamianSzkuat 6 лет назад
Родитель
Сommit
edc622ac23
3 измененных файлов с 205 добавлено и 55 удалено
  1. 167 18
      stream_file.go
  2. 1 1
      stream_file_builder.go
  3. 37 36
      stream_test.go

+ 167 - 18
stream_file.go

@@ -34,16 +34,20 @@ 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")
+	WrongNumberOfCellTypesError = errors.New("the numbers of cells and cell types do not match")
+	UnsupportedCellTypeError = errors.New("the given cell type is not supported")
+	UnsupportedDataTypeError = errors.New("the given data 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
 // 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. Currently the only supported data type is string data.
-func (sf *StreamFile) Write(cells []string) error {
+// TODO update comment
+func (sf *StreamFile) Write(cells []interface{}, cellTypes []*CellType) error {
 	if sf.err != nil {
 		return sf.err
 	}
-	err := sf.write(cells)
+	err := sf.write(cells, cellTypes)
 	if err != nil {
 		sf.err = err
 		return err
@@ -51,12 +55,13 @@ func (sf *StreamFile) Write(cells []string) error {
 	return sf.zipWriter.Flush()
 }
 
-func (sf *StreamFile) WriteAll(records [][]string) error {
+//TODO Add comment
+func (sf *StreamFile) WriteAll(records [][]interface{}, cellTypes []*CellType) error {
 	if sf.err != nil {
 		return sf.err
 	}
 	for _, row := range records {
-		err := sf.write(row)
+		err := sf.write(row, cellTypes)
 		if err != nil {
 			sf.err = err
 			return err
@@ -65,43 +70,73 @@ func (sf *StreamFile) WriteAll(records [][]string) error {
 	return sf.zipWriter.Flush()
 }
 
-func (sf *StreamFile) write(cells []string) error {
+// TODO Add comment
+func (sf *StreamFile) write(cells []interface{}, cellTypes []*CellType) error {
 	if sf.currentSheet == nil {
 		return NoCurrentSheetError
 	}
 	if len(cells) != sf.currentSheet.columnCount {
 		return WrongNumberOfRowsError
 	}
+	if len(cells) != len(cellTypes) {
+		return WrongNumberOfCellTypesError
+	}
 	sf.currentSheet.rowCount++
+
+	// This is the XML 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, cellData := range cells {
-		// 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.
+		// Get the cell reference (location)
 		cellCoordinate := GetCellIDStringFromCoords(colIndex, sf.currentSheet.rowCount-1)
-		cellType := "inlineStr"
+
+		// Get the cell type string
+		cellType, err := GetCellTypeAsString(cellTypes[colIndex])
+		if err != nil {
+			return  err
+		}
+
+		// Build the XML cell opening
 		cellOpen := `<c r="` + cellCoordinate + `" t="` + cellType + `"`
 		// Add in the style id if the cell isn't using the default style
 		if colIndex < len(sf.currentSheet.styleIds) && sf.currentSheet.styleIds[colIndex] != 0 {
 			cellOpen += ` s="` + strconv.Itoa(sf.currentSheet.styleIds[colIndex]) + `"`
 		}
-		cellOpen += `><is><t>`
-		cellClose := `</t></is></c>`
+		cellOpen += `>`
+
+		// The XML cell contents
+		cellContentsOpen, cellContentsClose, err := GetCellContentOpenAncCloseTags(cellTypes[colIndex])
+		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
 		}
-		if err := xml.EscapeText(sf.currentSheet.writer, []byte(cellData)); err != nil {
+
+		// Write the cell contents opening
+		if err := sf.currentSheet.write(cellContentsOpen); err != nil {
+			return err
+		}
+
+		// Write cell contents
+		if err := sf.WriteCellContents(cellData, cellTypes[colIndex]); 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
 		}
@@ -112,6 +147,120 @@ func (sf *StreamFile) write(cells []string) error {
 	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.
+	if cellType == nil {
+		// TODO should default be inline string?
+		return "inlineStr", nil
+	}
+	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 inline strings are typed as shared strings
+		// TODO remove once the tests have been changed
+		return "inlineStr", nil
+		// return "s", nil
+	case CellTypeStringFormula:
+		return "str", nil
+	default:
+		return "", UnsupportedCellTypeError
+	}
+}
+
+func GetCellContentOpenAncCloseTags(cellType *CellType) (string, string, error) {
+	if cellType == nil {
+		// TODO should default be inline string?
+		return `<is><t>`, `</t></is>`, nil
+	}
+	// TODO Currently inline strings are types as shared strings
+	// TODO remove once the tests have been changed
+	if *cellType == CellTypeString {
+		return `<is><t>`, `</t></is>`, nil
+	}
+	switch *cellType{
+	case CellTypeInline:
+		return `<is><t>`, `</t></is>`, nil
+	case CellTypeStringFormula:
+		// Formulas are currently not supported
+		return ``, ``, UnsupportedCellTypeError
+	default:
+		return `<v>`, `</v>`, nil
+	}
+}
+
+// TODO make sure to test shared strings
+func (sf *StreamFile) WriteCellContents(cellContents interface{}, cellType *CellType) error {
+	if cellType == nil {
+		// TODO should default be inline string?
+		cellStringData := cellContents.(string)
+		return xml.EscapeText(sf.currentSheet.writer, []byte(cellStringData))
+	}
+	// TODO currently shared strings are assigned the ContentTypeString in tests instead of ContentTypeInline
+	// TODO Remove once test have been changed.
+	if *cellType == CellTypeString {
+		cellStringData := cellContents.(string)
+		return xml.EscapeText(sf.currentSheet.writer, []byte(cellStringData))
+	}
+	XMLEncoder := xml.NewEncoder(sf.currentSheet.writer)
+	switch cellType {
+	case CellTypeInline.Ptr():
+		cellStringData := cellContents.(string)
+		return xml.EscapeText(sf.currentSheet.writer, []byte(cellStringData))
+	case CellTypeStringFormula.Ptr():
+		// Formulas are currently not supported
+		return UnsupportedCellTypeError
+	default:
+		switch cellContents.(type) {
+		case bool:
+			return XMLEncoder.Encode(cellContents.(bool))
+		case int:
+			return XMLEncoder.Encode(cellContents.(int))
+		case int8:
+			return XMLEncoder.Encode(cellContents.(int8))
+		case int16:
+			return XMLEncoder.Encode(cellContents.(int16))
+		case int32:
+			return XMLEncoder.Encode(cellContents.(int32))
+		case int64:
+			return XMLEncoder.Encode(cellContents.(int64))
+		case uint:
+			return XMLEncoder.Encode(cellContents.(uint))
+		case uint8:
+			return XMLEncoder.Encode(cellContents.(uint8))
+		case uint16:
+			return XMLEncoder.Encode(cellContents.(uint16))
+		case uint32:
+			return XMLEncoder.Encode(cellContents.(uint32))
+		case uint64:
+			return XMLEncoder.Encode(cellContents.(uint64))
+		case float32:
+			return XMLEncoder.Encode(cellContents.(float32))
+		case float64:
+			return XMLEncoder.Encode(cellContents.(float64))
+		default:
+			return UnsupportedDataTypeError
+		}
+	}
+}
+
 // Error reports any error that has occurred during a previous Write or Flush.
 func (sf *StreamFile) Error() error {
 	return sf.err

+ 1 - 1
stream_file_builder.go

@@ -77,7 +77,7 @@ func NewStreamFileBuilderForPath(path string) (*StreamFileBuilder, error) {
 // AddSheet will add sheets with the given name with the provided headers. The headers cannot be edited later, and all
 // rows written to the sheet must contain the same number of cells as the header. Sheet names must be unique, or an
 // error will be thrown.
-func (sb *StreamFileBuilder) AddSheet(name string, headers []string, cellTypes []*CellType) error {
+func (sb *StreamFileBuilder) AddSheet(name string, headers []interface{}, cellTypes []*CellType) error {
 	if sb.built {
 		return BuiltStreamFileBuilderError
 	}

+ 37 - 36
stream_test.go

@@ -11,7 +11,7 @@ import (
 )
 
 const (
-	TestsShouldMakeRealFiles = false
+	TestsShouldMakeRealFiles = true
 )
 
 type StreamSuite struct{}
@@ -32,31 +32,31 @@ func (s *StreamSuite) TestXlsxStreamWrite(t *C) {
 	testCases := []struct {
 		testName      string
 		sheetNames    []string
-		workbookData  [][][]string
+		workbookData  [][][]interface{}
 		headerTypes   [][]*CellType
 		expectedError error
 	}{
-		{
-			testName: "Date Row",
-			sheetNames: []string{
-				"Sheet1",
-			},
-			workbookData: [][][]string{
-				{
-					{"1", "25"},
-					{"123", "098"},
-				},
-			},
-			headerTypes: [][]*CellType{
-				{CellTypeDate.Ptr(), CellTypeDate.Ptr()},
-			},
-		},
+		//{
+		//	testName: "Number Row",
+		//	sheetNames: []string{
+		//		"Sheet1",
+		//	},
+		//	workbookData: [][][]interface{}{
+		//		{
+		//			{"1", "25"},
+		//			{123, 98},
+		//		},
+		//	},
+		//	headerTypes: [][]*CellType{
+		//		{CellTypeNumeric.Ptr(), CellTypeNumeric.Ptr()},
+		//	},
+		//},
 		{
 			testName: "One Sheet",
 			sheetNames: []string{
 				"Sheet1",
 			},
-			workbookData: [][][]string{
+			workbookData: [][][]interface{}{
 				{
 					{"Token", "Name", "Price", "SKU"},
 					{"123", "Taco", "300", "0000000123"},
@@ -71,7 +71,7 @@ func (s *StreamSuite) TestXlsxStreamWrite(t *C) {
 			sheetNames: []string{
 				"Sheet1",
 			},
-			workbookData: [][][]string{
+			workbookData: [][][]interface{}{
 				{
 					{"Token"},
 					{"123"},
@@ -83,7 +83,7 @@ func (s *StreamSuite) TestXlsxStreamWrite(t *C) {
 			sheetNames: []string{
 				"Sheet 1", "Sheet 2", "Sheet3",
 			},
-			workbookData: [][][]string{
+			workbookData: [][][]interface{}{
 				{
 					{"Token", "Name", "Price", "SKU"},
 					{"123", "Taco", "300", "0000000123"},
@@ -105,7 +105,7 @@ func (s *StreamSuite) TestXlsxStreamWrite(t *C) {
 			sheetNames: []string{
 				"Sheet 1", "Sheet 1",
 			},
-			workbookData: [][][]string{
+			workbookData: [][][]interface{}{
 				{
 					{"Token", "Name", "Price", "SKU"},
 					{"123", "Taco", "300", "0000000123"},
@@ -123,7 +123,7 @@ func (s *StreamSuite) TestXlsxStreamWrite(t *C) {
 			sheetNames: []string{
 				"Sheet 1",
 			},
-			workbookData: [][][]string{
+			workbookData: [][][]interface{}{
 				{
 					{"Token", "Name", "Price", "SKU"},
 					{"123", "Taco", "300", "0000000123"},
@@ -140,7 +140,7 @@ func (s *StreamSuite) TestXlsxStreamWrite(t *C) {
 			sheetNames: []string{
 				"Sheet 1",
 			},
-			workbookData: [][][]string{
+			workbookData: [][][]interface{}{
 				{
 					{"Token", "Name", "Price", "SKU"},
 					{"123", "Taco", "300", "0000000123", "asdf"},
@@ -153,7 +153,7 @@ func (s *StreamSuite) TestXlsxStreamWrite(t *C) {
 			sheetNames: []string{
 				"Sheet 1",
 			},
-			workbookData: [][][]string{
+			workbookData: [][][]interface{}{
 				{
 					{"Token", "Name", "Price", "SKU"},
 					{"123", "Taco", "300"},
@@ -166,7 +166,7 @@ func (s *StreamSuite) TestXlsxStreamWrite(t *C) {
 			sheetNames: []string{
 				"Sheet 1", "Sheet 2", "Sheet 3", "Sheet 4", "Sheet 5", "Sheet 6",
 			},
-			workbookData: [][][]string{
+			workbookData: [][][]interface{}{
 				{
 					{"Token", "Name", "Price", "SKU"},
 					{"123", "Taco", "300", "0000000123"},
@@ -183,7 +183,7 @@ func (s *StreamSuite) TestXlsxStreamWrite(t *C) {
 			sheetNames: []string{
 				"Sheet 1", "Sheet 2",
 			},
-			workbookData: [][][]string{
+			workbookData: [][][]interface{}{
 				{
 					{"Token", "Name", "Price", "SKU"},
 					{"123", "Taco", "300", "0000000123"},
@@ -196,7 +196,7 @@ func (s *StreamSuite) TestXlsxStreamWrite(t *C) {
 			sheetNames: []string{
 				"Sheet 1",
 			},
-			workbookData: [][][]string{
+			workbookData: [][][]interface{}{
 				{
 					{"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"},
@@ -231,7 +231,7 @@ func (s *StreamSuite) TestXlsxStreamWrite(t *C) {
 			sheetNames: []string{
 				"Sheet1",
 			},
-			workbookData: [][][]string{
+			workbookData: [][][]interface{}{
 				{
 					// String courtesy of https://github.com/minimaxir/big-list-of-naughty-strings/
 					// Header row contains the tags that I am filtering on
@@ -332,7 +332,7 @@ func (s *StreamSuite) TestXlsxStyleBehavior(t *C) {
 }
 
 // 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 [][][]interface{}, headerTypes [][]*CellType, shouldMakeRealFiles bool) error {
 	var file *StreamFileBuilder
 	var err error
 	if shouldMakeRealFiles {
@@ -359,6 +359,7 @@ func writeStreamFile(filePath string, fileBuffer io.Writer, sheetNames []string,
 		return err
 	}
 	for i, sheetData := range workbookData {
+		currentHeaderTypes := headerTypes[i]
 		if i != 0 {
 			err = streamFile.NextSheet()
 			if err != nil {
@@ -369,7 +370,7 @@ func writeStreamFile(filePath string, fileBuffer io.Writer, sheetNames []string,
 			if i == 0 {
 				continue
 			}
-			err = streamFile.Write(row)
+			err = streamFile.Write(row, currentHeaderTypes)
 			if err != nil {
 				return err
 			}
@@ -421,11 +422,11 @@ func readXLSXFile(t *C, filePath string, fileBuffer io.ReaderAt, size int64, sho
 func (s *StreamSuite) TestAddSheetErrorsAfterBuild(t *C) {
 	file := NewStreamFileBuilder(bytes.NewBuffer(nil))
 
-	err := file.AddSheet("Sheet1", []string{"Header"}, nil)
+	err := file.AddSheet("Sheet1", []interface{}{"Header"}, nil)
 	if err != nil {
 		t.Fatal(err)
 	}
-	err = file.AddSheet("Sheet2", []string{"Header2"}, nil)
+	err = file.AddSheet("Sheet2", []interface{}{"Header2"}, nil)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -434,7 +435,7 @@ func (s *StreamSuite) TestAddSheetErrorsAfterBuild(t *C) {
 	if err != nil {
 		t.Fatal(err)
 	}
-	err = file.AddSheet("Sheet3", []string{"Header3"}, nil)
+	err = file.AddSheet("Sheet3", []interface{}{"Header3"}, nil)
 	if err != BuiltStreamFileBuilderError {
 		t.Fatal(err)
 	}
@@ -443,11 +444,11 @@ func (s *StreamSuite) TestAddSheetErrorsAfterBuild(t *C) {
 func (s *StreamSuite) TestBuildErrorsAfterBuild(t *C) {
 	file := NewStreamFileBuilder(bytes.NewBuffer(nil))
 
-	err := file.AddSheet("Sheet1", []string{"Header"}, nil)
+	err := file.AddSheet("Sheet1", []interface{}{"Header"}, nil)
 	if err != nil {
 		t.Fatal(err)
 	}
-	err = file.AddSheet("Sheet2", []string{"Header2"}, nil)
+	err = file.AddSheet("Sheet2", []interface{}{"Header2"}, nil)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -467,7 +468,7 @@ func (s *StreamSuite) TestCloseWithNothingWrittenToSheets(t *C) {
 	file := NewStreamFileBuilder(buffer)
 
 	sheetNames := []string{"Sheet1", "Sheet2"}
-	workbookData := [][][]string{
+	workbookData := [][][]interface{}{
 		{{"Header1", "Header2"}},
 		{{"Header3", "Header4"}},
 	}