Przeglądaj źródła

Merge branch 'master' into master

Geoffrey J. Teale 8 lat temu
rodzic
commit
6b2dc5715a
11 zmienionych plików z 144 dodań i 31 usunięć
  1. 1 0
      .gitignore
  2. 6 2
      cell.go
  3. 25 1
      file.go
  4. 29 0
      file_test.go
  5. 21 16
      lib.go
  6. 4 4
      lib_test.go
  7. 15 3
      sheet.go
  8. 32 2
      sheet_test.go
  9. BIN
      testdocs/testFileToSlice.xlsx
  10. 8 3
      xmlWorksheet.go
  11. 3 0
      xmlWorksheet_test.go

+ 1 - 0
.gitignore

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

+ 6 - 2
cell.go

@@ -114,8 +114,12 @@ func (c *Cell) SetFloatWithFormat(n float64, format string) {
 
 var timeLocationUTC, _ = time.LoadLocation("UTC")
 
-func timeToExcelTime(t time.Time) float64 {
-	return float64(t.Unix())/86400.0 + 25569.0
+func TimeToUTCTime(t time.Time) time.Time {
+	return time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), timeLocationUTC)
+}
+
+func TimeToExcelTime(t time.Time) float64 {
+	return float64(t.UnixNano())/8.64e13 + 25569.0
 }
 
 // DateTimeOptions are additional options for exporting times

+ 25 - 1
file.go

@@ -9,6 +9,7 @@ import (
 	"os"
 	"strconv"
 	"strings"
+	"errors"
 )
 
 // File is a high level structure providing a slice of Sheet structs
@@ -132,6 +133,19 @@ func (f *File) AddSheet(sheetName string) (*Sheet, error) {
 	return sheet, nil
 }
 
+// Appends an existing Sheet, with the provided name, to a File
+func (f *File) AppendSheet(sheet Sheet, sheetName string) (*Sheet, error) {
+	if _, exists := f.Sheet[sheetName]; exists {
+		return nil, fmt.Errorf("duplicate sheet name '%s'.", sheetName)
+	}
+	sheet.Name = sheetName
+	sheet.File = f
+	sheet.Selected = len(f.Sheets) == 0
+	f.Sheet[sheetName] = &sheet
+	f.Sheets = append(f.Sheets, &sheet)
+	return &sheet, nil
+}
+
 func (f *File) makeWorkbook() xlsxWorkbook {
 	return xlsxWorkbook{
 		FileVersion: xlsxFileVersion{AppName: "Go XLSX"},
@@ -205,6 +219,10 @@ func (f *File) MarshallParts() (map[string]string, error) {
 		f.styles = newXlsxStyleSheet(f.theme)
 	}
 	f.styles.reset()
+	if len(f.Sheets)==0 {
+		err:= errors.New("Workbook must contains atleast one worksheet")
+		return nil, err
+	}
 	for _, sheet := range f.Sheets {
 		xSheet := sheet.makeXLSXSheet(refTable, f.styles)
 		rId := fmt.Sprintf("rId%d", sheetIndex)
@@ -296,7 +314,13 @@ func (file *File) ToSlice() (output [][][]string, err error) {
 			for _, cell := range row.Cells {
 				str, err := cell.String()
 				if err != nil {
-					return output, err
+					// Recover from strconv.NumError if the value is an empty string,
+					// and insert an empty string in the output.
+					if numErr, ok := err.(*strconv.NumError); ok && numErr.Num == "" {
+						str = ""
+					} else {
+						return output, err
+					}
 				}
 				r = append(r, str)
 			}

+ 29 - 0
file_test.go

@@ -252,6 +252,29 @@ func (l *FileSuite) TestAddSheetWithDuplicateName(c *C) {
 	c.Assert(err, ErrorMatches, "duplicate sheet name 'MySheet'.")
 }
 
+// Test that we can append a sheet to a File
+func (l *FileSuite) TestAppendSheet(c *C) {
+	var f *File
+
+	f = NewFile()
+	s := Sheet{}
+	sheet, err := f.AppendSheet(s, "MySheet")
+	c.Assert(err, IsNil)
+	c.Assert(sheet, NotNil)
+	c.Assert(len(f.Sheets), Equals, 1)
+	c.Assert(f.Sheet["MySheet"], Equals, sheet)
+}
+
+// Test that AppendSheet returns an error if you try to add two sheets with the same name
+func (l *FileSuite) TestAppendSheetWithDuplicateName(c *C) {
+	f := NewFile()
+	s := Sheet{}
+	_, err := f.AppendSheet(s, "MySheet")
+	c.Assert(err, IsNil)
+	_, err = f.AppendSheet(s, "MySheet")
+	c.Assert(err, ErrorMatches, "duplicate sheet name 'MySheet'.")
+}
+
 // Test that we can get the Nth sheet
 func (l *FileSuite) TestNthSheet(c *C) {
 	var f *File
@@ -740,6 +763,12 @@ func (s *SliceReaderSuite) TestFileToSlice(c *C) {
 	fileToSliceCheckOutput(c, output)
 }
 
+func (s *SliceReaderSuite) TestFileToSliceMissingCol(c *C) {
+	// Test xlsx file with the A column removed
+	_, err := FileToSlice("./testdocs/testFileToSlice.xlsx")
+	c.Assert(err, IsNil)
+}
+
 func (s *SliceReaderSuite) TestFileObjToSlice(c *C) {
 	f, err := OpenFile("./testdocs/testfile.xlsx")
 	output, err := f.ToSlice()

+ 21 - 16
lib.go

@@ -162,10 +162,10 @@ func intOnlyMapF(rune rune) rune {
 	return -1
 }
 
-// getCoordsFromCellIDString returns the zero based cartesian
+// GetCoordsFromCellIDString returns the zero based cartesian
 // coordinates from a cell name in Excel format, e.g. the cellIDString
 // "A1" returns 0, 0 and the "B3" return 1, 2.
-func getCoordsFromCellIDString(cellIDString string) (x, y int, error error) {
+func GetCoordsFromCellIDString(cellIDString string) (x, y int, error error) {
 	var letterPart string = strings.Map(letterOnlyMapF, cellIDString)
 	y, error = strconv.Atoi(strings.Map(intOnlyMapF, cellIDString))
 	if error != nil {
@@ -176,9 +176,9 @@ func getCoordsFromCellIDString(cellIDString string) (x, y int, error error) {
 	return x, y, error
 }
 
-// getCellIDStringFromCoords returns the Excel format cell name that
+// GetCellIDStringFromCoords returns the Excel format cell name that
 // represents a pair of zero based cartesian coordinates.
-func getCellIDStringFromCoords(x, y int) string {
+func GetCellIDStringFromCoords(x, y int) string {
 	letterPart := numericToLetters(x)
 	numericPart := y + 1
 	return fmt.Sprintf("%s%d", letterPart, numericPart)
@@ -191,7 +191,7 @@ func getCellIDStringFromCoords(x, y int) string {
 func getMaxMinFromDimensionRef(ref string) (minx, miny, maxx, maxy int, err error) {
 	var parts []string
 	parts = strings.Split(ref, ":")
-	minx, miny, err = getCoordsFromCellIDString(parts[0])
+	minx, miny, err = GetCoordsFromCellIDString(parts[0])
 	if err != nil {
 		return -1, -1, -1, -1, err
 	}
@@ -199,7 +199,7 @@ func getMaxMinFromDimensionRef(ref string) (minx, miny, maxx, maxy int, err erro
 		maxx, maxy = minx, miny
 		return
 	}
-	maxx, maxy, err = getCoordsFromCellIDString(parts[1])
+	maxx, maxy, err = GetCoordsFromCellIDString(parts[1])
 	if err != nil {
 		return -1, -1, -1, -1, err
 	}
@@ -220,7 +220,7 @@ func calculateMaxMinFromWorksheet(worksheet *xlsxWorksheet) (minx, miny, maxx, m
 	maxx = 0
 	for _, row := range worksheet.SheetData.Row {
 		for _, cell := range row.C {
-			x, y, err = getCoordsFromCellIDString(cell.R)
+			x, y, err = GetCoordsFromCellIDString(cell.R)
 			if err != nil {
 				return -1, -1, -1, -1, err
 			}
@@ -285,7 +285,7 @@ func makeRowFromRaw(rawrow xlsxRow, sheet *Sheet) *Row {
 
 	for _, rawcell := range rawrow.C {
 		if rawcell.R != "" {
-			x, _, error := getCoordsFromCellIDString(rawcell.R)
+			x, _, error := GetCoordsFromCellIDString(rawcell.R)
 			if error != nil {
 				panic(fmt.Sprintf("Invalid Cell Coord, %s\n", rawcell.R))
 			}
@@ -329,7 +329,7 @@ func formulaForCell(rawcell xlsxC, sharedFormulas map[int]sharedFormula) string
 		return ""
 	}
 	if f.T == "shared" {
-		x, y, err := getCoordsFromCellIDString(rawcell.R)
+		x, y, err := GetCoordsFromCellIDString(rawcell.R)
 		if err != nil {
 			res = f.Content
 		} else {
@@ -392,7 +392,7 @@ func formulaForCell(rawcell xlsxC, sharedFormulas map[int]sharedFormula) string
 // shiftCell returns the cell shifted according to dx and dy taking into consideration of absolute
 // references with dollar sign ($)
 func shiftCell(cellID string, dx, dy int) string {
-	fx, fy, _ := getCoordsFromCellIDString(cellID)
+	fx, fy, _ := GetCoordsFromCellIDString(cellID)
 
 	// Is fixed column?
 	fixedCol := strings.Index(cellID, "$") == 0
@@ -411,7 +411,7 @@ func shiftCell(cellID string, dx, dy int) string {
 	}
 
 	// New shifted cell
-	shiftedCellID := getCellIDStringFromCoords(fx, fy)
+	shiftedCellID := GetCellIDStringFromCoords(fx, fy)
 
 	if !fixedCol && !fixedRow {
 		return shiftedCellID
@@ -575,7 +575,7 @@ func readRowsFromSheet(Worksheet *xlsxWorksheet, file *File, sheet *Sheet) ([]*R
 			if err != nil {
 				panic(err.Error())
 			}
-			x, _, _ := getCoordsFromCellIDString(rawcell.R)
+			x, _, _ := GetCoordsFromCellIDString(rawcell.R)
 
 			// K1000000: Prevent panic when the range specified in the spreadsheet
 			//           view exceeds the actual number of columns in the dataset.
@@ -646,13 +646,15 @@ func readSheetViews(xSheetViews xlsxSheetViews) []SheetView {
 // into a Sheet struct.  This work can be done in parallel and so
 // readSheetsFromZipFile will spawn an instance of this function per
 // sheet and get the results back on the provided channel.
-func readSheetFromFile(sc chan *indexedSheet, index int, rsheet xlsxSheet, fi *File, sheetXMLMap map[string]string) {
+func readSheetFromFile(sc chan *indexedSheet, index int, rsheet xlsxSheet, fi *File, sheetXMLMap map[string]string) (errRes error) {
 	result := &indexedSheet{Index: index, Sheet: nil, Error: nil}
 	defer func() {
 		if e := recover(); e != nil {
+
 			switch e.(type) {
 			case error:
 				result.Error = e.(error)
+				errRes = e.(error)
 			default:
 				result.Error = errors.New("unexpected error")
 			}
@@ -665,7 +667,7 @@ func readSheetFromFile(sc chan *indexedSheet, index int, rsheet xlsxSheet, fi *F
 	if error != nil {
 		result.Error = error
 		sc <- result
-		return
+		return error
 	}
 	sheet := new(Sheet)
 	sheet.File = fi
@@ -680,6 +682,7 @@ func readSheetFromFile(sc chan *indexedSheet, index int, rsheet xlsxSheet, fi *F
 
 	result.Sheet = sheet
 	sc <- result
+	return nil
 }
 
 // readSheetsFromZipFile is an internal helper function that loops
@@ -719,12 +722,14 @@ func readSheetsFromZipFile(f *zip.File, file *File, sheetXMLMap map[string]strin
 	sheetsByName := make(map[string]*Sheet, sheetCount)
 	sheets := make([]*Sheet, sheetCount)
 	sheetChan := make(chan *indexedSheet, sheetCount)
-	defer close(sheetChan)
 
 	go func() {
+		defer close(sheetChan)
 		err = nil
 		for i, rawsheet := range workbookSheets {
-			readSheetFromFile(sheetChan, i, rawsheet, file, sheetXMLMap)
+			if err := readSheetFromFile(sheetChan, i, rawsheet, file, sheetXMLMap); err != nil {
+				return
+			}
 		}
 	}()
 

+ 4 - 4
lib_test.go

@@ -160,15 +160,15 @@ func (l *LibSuite) TestGetCoordsFromCellIDString(c *C) {
 	var cellIDString string = "A3"
 	var x, y int
 	var err error
-	x, y, err = getCoordsFromCellIDString(cellIDString)
+	x, y, err = GetCoordsFromCellIDString(cellIDString)
 	c.Assert(err, IsNil)
 	c.Assert(x, Equals, 0)
 	c.Assert(y, Equals, 2)
 }
 
 func (l *LibSuite) TestGetCellIDStringFromCoords(c *C) {
-	c.Assert(getCellIDStringFromCoords(0, 0), Equals, "A1")
-	c.Assert(getCellIDStringFromCoords(2, 2), Equals, "C3")
+	c.Assert(GetCellIDStringFromCoords(0, 0), Equals, "A1")
+	c.Assert(GetCellIDStringFromCoords(2, 2), Equals, "C3")
 }
 
 func (l *LibSuite) TestGetMaxMinFromDimensionRef(c *C) {
@@ -1227,7 +1227,7 @@ func (l *LibSuite) TestSharedFormulasWithAbsoluteReferences(c *C) {
 	anchorCell := "C4"
 
 	sharedFormulas := map[int]sharedFormula{}
-	x, y, _ := getCoordsFromCellIDString(anchorCell)
+	x, y, _ := GetCoordsFromCellIDString(anchorCell)
 	for i, formula := range formulas {
 		res := formula
 		sharedFormulas[i] = sharedFormula{x, y, res}

+ 15 - 3
sheet.go

@@ -18,6 +18,7 @@ type Sheet struct {
 	Selected    bool
 	SheetViews  []SheetView
 	SheetFormat SheetFormat
+	AutoFilter  *AutoFilter
 }
 
 type SheetView struct {
@@ -39,6 +40,11 @@ type SheetFormat struct {
 	OutlineLevelRow  uint8
 }
 
+type AutoFilter struct {
+	TopLeftCell     string
+	BottomRightCell string
+}
+
 // Add a new Row to a Sheet
 func (s *Sheet) AddRow() *Row {
 	row := &Row{Sheet: s}
@@ -147,7 +153,7 @@ func (s *Sheet) handleMerged() {
 		mainstyle.Border.Right = "none"
 		mainstyle.Border.Bottom = "none"
 
-		maincol, mainrow, _ := getCoordsFromCellIDString(key)
+		maincol, mainrow, _ := GetCoordsFromCellIDString(key)
 		for rownum := 0; rownum <= cell.VMerge; rownum++ {
 			for colnum := 0; colnum <= cell.HMerge; colnum++ {
 				tmpcell := s.Cell(mainrow+rownum, maincol+colnum)
@@ -227,11 +233,13 @@ func (s *Sheet) makeXLSXSheet(refTable *RefTable, styles *xlsxStyleSheet) *xlsxW
 		}
 		colsXfIdList[c] = XfId
 
-		var customWidth int
+		var customWidth bool
 		if col.Width == 0 {
 			col.Width = ColWidth
+			customWidth = false
+			
 		} else {
-			customWidth = 1
+			customWidth = true
 		}
 		worksheet.Cols.Col = append(worksheet.Cols.Col,
 			xlsxCol{Min: col.Min,
@@ -342,6 +350,10 @@ func (s *Sheet) makeXLSXSheet(refTable *RefTable, styles *xlsxStyleSheet) *xlsxW
 		worksheet.MergeCells.Count = len(worksheet.MergeCells.Cells)
 	}
 
+	if s.AutoFilter != nil {
+		worksheet.AutoFilter = &xlsxAutoFilter{Ref: fmt.Sprintf("%v:%v", s.AutoFilter.TopLeftCell, s.AutoFilter.BottomRightCell)}
+	}
+
 	worksheet.SheetData = xSheet
 	dimension := xlsxDimension{}
 	dimension.Ref = fmt.Sprintf("A1:%s%d",

+ 32 - 2
sheet_test.go

@@ -162,7 +162,7 @@ func (s *SheetSuite) TestMakeXLSXSheetDefaultsCustomColWidth(c *C) {
 	refTable := NewSharedStringRefTable()
 	styles := newXlsxStyleSheet(nil)
 	worksheet := sheet.makeXLSXSheet(refTable, styles)
-	c.Assert(worksheet.Cols.Col[0].CustomWidth, Equals, 0)
+	c.Assert(worksheet.Cols.Col[0].CustomWidth, Equals, false)
 }
 
 // If the column width is customised, the xslxCol.CustomWidth field is set to 1.
@@ -178,7 +178,7 @@ func (s *SheetSuite) TestMakeXLSXSheetSetsCustomColWidth(c *C) {
 	refTable := NewSharedStringRefTable()
 	styles := newXlsxStyleSheet(nil)
 	worksheet := sheet.makeXLSXSheet(refTable, styles)
-	c.Assert(worksheet.Cols.Col[1].CustomWidth, Equals, 1)
+	c.Assert(worksheet.Cols.Col[1].CustomWidth, Equals, true)
 }
 
 func (s *SheetSuite) TestMarshalSheet(c *C) {
@@ -392,3 +392,33 @@ func (s *SheetSuite) TestOutlineLevels(c *C) {
 	c.Assert(worksheet.SheetData.Row[1].OutlineLevel, Equals, uint8(2))
 	c.Assert(worksheet.SheetData.Row[2].OutlineLevel, Equals, uint8(0))
 }
+
+func (s *SheetSuite) TestAutoFilter(c *C) {
+	file := NewFile()
+	sheet, _ := file.AddSheet("Sheet1")
+
+	r1 := sheet.AddRow()
+	r1.AddCell()
+	r1.AddCell()
+	r1.AddCell()
+
+	r2 := sheet.AddRow()
+	r2.AddCell()
+	r2.AddCell()
+	r2.AddCell()
+
+	r3 := sheet.AddRow()
+	r3.AddCell()
+	r3.AddCell()
+	r3.AddCell()
+
+	// Define a filter area
+	sheet.AutoFilter = &AutoFilter{TopLeftCell: "B2", BottomRightCell: "C3"}
+
+	refTable := NewSharedStringRefTable()
+	styles := newXlsxStyleSheet(nil)
+	worksheet := sheet.makeXLSXSheet(refTable, styles)
+
+	c.Assert(worksheet.AutoFilter, NotNil)
+	c.Assert(worksheet.AutoFilter.Ref, Equals, "B2:C3")
+}

BIN
testdocs/testFileToSlice.xlsx


+ 8 - 3
xmlWorksheet.go

@@ -17,6 +17,7 @@ type xlsxWorksheet struct {
 	SheetFormatPr xlsxSheetFormatPr `xml:"sheetFormatPr"`
 	Cols          *xlsxCols         `xml:"cols,omitempty"`
 	SheetData     xlsxSheetData     `xml:"sheetData"`
+	AutoFilter    *xlsxAutoFilter   `xml:"autoFilter,omitempty"`
 	MergeCells    *xlsxMergeCells   `xml:"mergeCells,omitempty"`
 	PrintOptions  xlsxPrintOptions  `xml:"printOptions"`
 	PageMargins   xlsxPageMargins   `xml:"pageMargins"`
@@ -201,7 +202,7 @@ type xlsxCol struct {
 	Min          int     `xml:"min,attr"`
 	Style        int     `xml:"style,attr"`
 	Width        float64 `xml:"width,attr"`
-	CustomWidth  int     `xml:"customWidth,attr,omitempty"`
+	CustomWidth  bool    `xml:"customWidth,attr,omitempty"`
 	OutlineLevel uint8   `xml:"outlineLevel,attr,omitempty"`
 }
 
@@ -236,6 +237,10 @@ type xlsxRow struct {
 	OutlineLevel uint8   `xml:"outlineLevel,attr,omitempty"`
 }
 
+type xlsxAutoFilter struct {
+	Ref string `xml:"ref,attr"`
+}
+
 type xlsxMergeCell struct {
 	Ref string `xml:"ref,attr"` // ref: horiz "A1:C1", vert "B3:B6", both  "D3:G4"
 }
@@ -255,11 +260,11 @@ func (mc *xlsxMergeCells) getExtent(cellRef string) (int, int, error) {
 	for _, cell := range mc.Cells {
 		if strings.HasPrefix(cell.Ref, cellRef+":") {
 			parts := strings.Split(cell.Ref, ":")
-			startx, starty, err := getCoordsFromCellIDString(parts[0])
+			startx, starty, err := GetCoordsFromCellIDString(parts[0])
 			if err != nil {
 				return -1, -1, err
 			}
-			endx, endy, err := getCoordsFromCellIDString(parts[1])
+			endx, endy, err := GetCoordsFromCellIDString(parts[1])
 			if err != nil {
 				return -2, -2, err
 			}

+ 3 - 0
xmlWorksheet_test.go

@@ -93,6 +93,7 @@ func (w *WorksheetSuite) TestUnmarshallWorksheet(c *C) {
               </c>
             </row>
           </sheetData>
+          <autoFilter ref="A1:Z4" />
           <printOptions headings="false"
                         gridLines="false"
                         gridLinesSet="true"
@@ -141,6 +142,8 @@ func (w *WorksheetSuite) TestUnmarshallWorksheet(c *C) {
 	c.Assert(cell.R, Equals, "A1")
 	c.Assert(cell.T, Equals, "s")
 	c.Assert(cell.V, Equals, "0")
+	c.Assert(worksheet.AutoFilter, NotNil)
+	c.Assert(worksheet.AutoFilter.Ref, Equals, "A1:Z4")
 }
 
 // MergeCells information is correctly read from the worksheet.