diff --git a/.gitignore b/.gitignore index ef972f60..29e0f2b7 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ +~$*.xlsx test/Test*.xlsx diff --git a/adjust.go b/adjust.go new file mode 100644 index 00000000..ee2065f4 --- /dev/null +++ b/adjust.go @@ -0,0 +1,229 @@ +package excelize + +import ( + "strings" +) + +type adjustDirection bool + +const ( + columns adjustDirection = false + rows adjustDirection = true +) + +// adjustHelper provides a function to adjust rows and columns dimensions, +// hyperlinks, merged cells and auto filter when inserting or deleting rows or +// columns. +// +// sheet: Worksheet name that we're editing +// column: Index number of the column we're inserting/deleting before +// row: Index number of the row we're inserting/deleting before +// offset: Number of rows/column to insert/delete negative values indicate deletion +// +// TODO: adjustCalcChain, adjustPageBreaks, adjustComments, +// adjustDataValidations, adjustProtectedCells +// +func (f *File) adjustHelper(sheet string, dir adjustDirection, num, offset int) { + xlsx := f.workSheetReader(sheet) + + if dir == rows { + f.adjustRowDimensions(xlsx, num, offset) + } else { + f.adjustColDimensions(xlsx, num, offset) + } + f.adjustHyperlinks(xlsx, sheet, dir, num, offset) + f.adjustMergeCells(xlsx, dir, num, offset) + f.adjustAutoFilter(xlsx, dir, num, offset) + + checkSheet(xlsx) + checkRow(xlsx) +} + +// adjustColDimensions provides a function to update column dimensions when +// inserting or deleting rows or columns. +func (f *File) adjustColDimensions(xlsx *xlsxWorksheet, col, offset int) { + for rowIdx := range xlsx.SheetData.Row { + for colIdx, v := range xlsx.SheetData.Row[rowIdx].C { + cellCol, cellRow, _ := CellNameToCoordinates(v.R) + if col <= cellCol { + if newCol := cellCol + offset; newCol > 0 { + xlsx.SheetData.Row[rowIdx].C[colIdx].R, _ = CoordinatesToCellName(newCol, cellRow) + } + } + } + } +} + +// adjustRowDimensions provides a function to update row dimensions when +// inserting or deleting rows or columns. +func (f *File) adjustRowDimensions(xlsx *xlsxWorksheet, row, offset int) { + for i, r := range xlsx.SheetData.Row { + if newRow := r.R + offset; r.R >= row && newRow > 0 { + f.ajustSingleRowDimensions(&xlsx.SheetData.Row[i], newRow) + } + } +} + +// ajustSingleRowDimensions provides a function to ajust single row dimensions. +func (f *File) ajustSingleRowDimensions(r *xlsxRow, num int) { + r.R = num + for i, col := range r.C { + colName, _, _ := SplitCellName(col.R) + r.C[i].R, _ = JoinCellName(colName, num) + } +} + +// adjustHyperlinks provides a function to update hyperlinks when inserting or +// deleting rows or columns. +func (f *File) adjustHyperlinks(xlsx *xlsxWorksheet, sheet string, dir adjustDirection, num, offset int) { + // short path + if xlsx.Hyperlinks == nil || len(xlsx.Hyperlinks.Hyperlink) == 0 { + return + } + + // order is important + if offset < 0 { + for rowIdx, linkData := range xlsx.Hyperlinks.Hyperlink { + colNum, rowNum, _ := CellNameToCoordinates(linkData.Ref) + + if (dir == rows && num == rowNum) || (dir == columns && num == colNum) { + f.deleteSheetRelationships(sheet, linkData.RID) + if len(xlsx.Hyperlinks.Hyperlink) > 1 { + xlsx.Hyperlinks.Hyperlink = append(xlsx.Hyperlinks.Hyperlink[:rowIdx], + xlsx.Hyperlinks.Hyperlink[rowIdx+1:]...) + } else { + xlsx.Hyperlinks = nil + } + } + } + } + + if xlsx.Hyperlinks == nil { + return + } + + for i := range xlsx.Hyperlinks.Hyperlink { + link := &xlsx.Hyperlinks.Hyperlink[i] // get reference + colNum, rowNum, _ := CellNameToCoordinates(link.Ref) + + if dir == rows { + if rowNum >= num { + link.Ref, _ = CoordinatesToCellName(colNum, rowNum+offset) + } + } else { + if colNum >= num { + link.Ref, _ = CoordinatesToCellName(colNum+offset, rowNum) + } + } + } +} + +// adjustAutoFilter provides a function to update the auto filter when +// inserting or deleting rows or columns. +func (f *File) adjustAutoFilter(xlsx *xlsxWorksheet, dir adjustDirection, num, offset int) { + if xlsx.AutoFilter == nil { + return + } + + rng := strings.Split(xlsx.AutoFilter.Ref, ":") + firstCell := rng[0] + lastCell := rng[1] + + firstCol, firstRow, err := CellNameToCoordinates(firstCell) + if err != nil { + panic(err) + } + + lastCol, lastRow, err := CellNameToCoordinates(lastCell) + if err != nil { + panic(err) + } + + if (dir == rows && firstRow == num && offset < 0) || (dir == columns && firstCol == num && lastCol == num) { + xlsx.AutoFilter = nil + for rowIdx := range xlsx.SheetData.Row { + rowData := &xlsx.SheetData.Row[rowIdx] + if rowData.R > firstRow && rowData.R <= lastRow { + rowData.Hidden = false + } + } + return + } + + if dir == rows { + if firstRow >= num { + firstCell, _ = CoordinatesToCellName(firstCol, firstRow+offset) + } + if lastRow >= num { + lastCell, _ = CoordinatesToCellName(lastCol, lastRow+offset) + } + } else { + if lastCol >= num { + lastCell, _ = CoordinatesToCellName(lastCol+offset, lastRow) + } + } + + xlsx.AutoFilter.Ref = firstCell + ":" + lastCell +} + +// adjustMergeCells provides a function to update merged cells when inserting +// or deleting rows or columns. +func (f *File) adjustMergeCells(xlsx *xlsxWorksheet, dir adjustDirection, num, offset int) { + if xlsx.MergeCells == nil { + return + } + + for i, areaData := range xlsx.MergeCells.Cells { + rng := strings.Split(areaData.Ref, ":") + firstCell := rng[0] + lastCell := rng[1] + + firstCol, firstRow, err := CellNameToCoordinates(firstCell) + if err != nil { + panic(err) + } + + lastCol, lastRow, err := CellNameToCoordinates(lastCell) + if err != nil { + panic(err) + } + + adjust := func(v int) int { + if v >= num { + v += offset + if v < 1 { + return 1 + } + return v + } + return v + } + + if dir == rows { + firstRow = adjust(firstRow) + lastRow = adjust(lastRow) + } else { + firstCol = adjust(firstCol) + lastCol = adjust(lastCol) + } + + if firstCol == lastCol && firstRow == lastRow { + if len(xlsx.MergeCells.Cells) > 1 { + xlsx.MergeCells.Cells = append(xlsx.MergeCells.Cells[:i], xlsx.MergeCells.Cells[i+1:]...) + xlsx.MergeCells.Count = len(xlsx.MergeCells.Cells) + } else { + xlsx.MergeCells = nil + } + } + + if firstCell, err = CoordinatesToCellName(firstCol, firstRow); err != nil { + panic(err) + } + + if lastCell, err = CoordinatesToCellName(lastCol, lastRow); err != nil { + panic(err) + } + + areaData.Ref = firstCell + ":" + lastCell + } +} diff --git a/cell.go b/cell.go index 3cf880a9..126b1ff5 100644 --- a/cell.go +++ b/cell.go @@ -11,6 +11,7 @@ package excelize import ( "encoding/xml" + "errors" "fmt" "reflect" "strconv" @@ -29,18 +30,18 @@ const ( STCellFormulaTypeShared = "shared" ) -// mergeCellsParser provides a function to check merged cells in worksheet by -// given axis. -func (f *File) mergeCellsParser(xlsx *xlsxWorksheet, axis string) string { - axis = strings.ToUpper(axis) - if xlsx.MergeCells != nil { - for i := 0; i < len(xlsx.MergeCells.Cells); i++ { - if checkCellInArea(axis, xlsx.MergeCells.Cells[i].Ref) { - axis = strings.Split(xlsx.MergeCells.Cells[i].Ref, ":")[0] - } +// GetCellValue provides a function to get formatted value from cell by given +// worksheet name and axis in XLSX file. If it is possible to apply a format +// to the cell value, it will do so, if not then an error will be returned, +// along with the raw value of the cell. +func (f *File) GetCellValue(sheet, axis string) string { + return f.getCellStringFunc(sheet, axis, func(x *xlsxWorksheet, c *xlsxC) (string, bool) { + val, err := c.getValueFrom(f, f.sharedStringsReader()) + if err != nil { + panic(err) // Fail fast to avoid future side effects! } - } - return axis + return val, true + }) } // SetCellValue provides a function to set value of a cell. The following @@ -68,115 +69,352 @@ func (f *File) mergeCellsParser(xlsx *xlsxWorksheet, axis string) string { // Note that default date format is m/d/yy h:mm of time.Time type value. You can // set numbers format by SetCellStyle() method. func (f *File) SetCellValue(sheet, axis string, value interface{}) { - switch t := value.(type) { + switch v := value.(type) { + case int: + f.SetCellInt(sheet, axis, v) + case int8: + f.SetCellInt(sheet, axis, int(v)) + case int16: + f.SetCellInt(sheet, axis, int(v)) + case int32: + f.SetCellInt(sheet, axis, int(v)) + case int64: + f.SetCellInt(sheet, axis, int(v)) + case uint: + f.SetCellInt(sheet, axis, int(v)) + case uint8: + f.SetCellInt(sheet, axis, int(v)) + case uint16: + f.SetCellInt(sheet, axis, int(v)) + case uint32: + f.SetCellInt(sheet, axis, int(v)) + case uint64: + f.SetCellInt(sheet, axis, int(v)) case float32: - f.SetCellDefault(sheet, axis, strconv.FormatFloat(float64(value.(float32)), 'f', -1, 32)) + f.SetCellDefault(sheet, axis, strconv.FormatFloat(float64(v), 'f', -1, 32)) case float64: - f.SetCellDefault(sheet, axis, strconv.FormatFloat(float64(value.(float64)), 'f', -1, 64)) + f.SetCellDefault(sheet, axis, strconv.FormatFloat(v, 'f', -1, 64)) case string: - f.SetCellStr(sheet, axis, t) + f.SetCellStr(sheet, axis, v) case []byte: - f.SetCellStr(sheet, axis, string(t)) + f.SetCellStr(sheet, axis, string(v)) case time.Duration: - f.SetCellDefault(sheet, axis, strconv.FormatFloat(float64(value.(time.Duration).Seconds()/86400), 'f', -1, 32)) + f.SetCellDefault(sheet, axis, strconv.FormatFloat(v.Seconds()/86400.0, 'f', -1, 32)) f.setDefaultTimeStyle(sheet, axis, 21) case time.Time: - f.SetCellDefault(sheet, axis, strconv.FormatFloat(float64(timeToExcelTime(timeToUTCTime(value.(time.Time)))), 'f', -1, 64)) - f.setDefaultTimeStyle(sheet, axis, 22) + vv := timeToExcelTime(v) + if vv > 0 { + f.SetCellDefault(sheet, axis, strconv.FormatFloat(timeToExcelTime(v), 'f', -1, 64)) + f.setDefaultTimeStyle(sheet, axis, 22) + } else { + f.SetCellStr(sheet, axis, v.Format(time.RFC3339Nano)) + } + case bool: + f.SetCellBool(sheet, axis, v) case nil: f.SetCellStr(sheet, axis, "") - case bool: - f.SetCellBool(sheet, axis, bool(value.(bool))) - default: - f.setCellIntValue(sheet, axis, value) - } -} - -// setCellIntValue provides a function to set int value of a cell. -func (f *File) setCellIntValue(sheet, axis string, value interface{}) { - switch value.(type) { - case int: - f.SetCellInt(sheet, axis, value.(int)) - case int8: - f.SetCellInt(sheet, axis, int(value.(int8))) - case int16: - f.SetCellInt(sheet, axis, int(value.(int16))) - case int32: - f.SetCellInt(sheet, axis, int(value.(int32))) - case int64: - f.SetCellInt(sheet, axis, int(value.(int64))) - case uint: - f.SetCellInt(sheet, axis, int(value.(uint))) - case uint8: - f.SetCellInt(sheet, axis, int(value.(uint8))) - case uint16: - f.SetCellInt(sheet, axis, int(value.(uint16))) - case uint32: - f.SetCellInt(sheet, axis, int(value.(uint32))) - case uint64: - f.SetCellInt(sheet, axis, int(value.(uint64))) default: f.SetCellStr(sheet, axis, fmt.Sprintf("%v", value)) } } -// SetCellBool provides a function to set bool type value of a cell by given +// SetCellInt provides a function to set int type value of a cell by given // worksheet name, cell coordinates and cell value. +func (f *File) SetCellInt(sheet, axis string, value int) { + xlsx := f.workSheetReader(sheet) + cellData, col, _ := f.prepareCell(xlsx, sheet, axis) + cellData.S = f.prepareCellStyle(xlsx, col, cellData.S) + cellData.T = "" + cellData.V = strconv.Itoa(value) +} + +// SetCellBool provides a function to set bool type value of a cell by given +// worksheet name, cell name and cell value. func (f *File) SetCellBool(sheet, axis string, value bool) { xlsx := f.workSheetReader(sheet) - axis = f.mergeCellsParser(xlsx, axis) - col := string(strings.Map(letterOnlyMapF, axis)) - row, err := strconv.Atoi(strings.Map(intOnlyMapF, axis)) - if err != nil { - return - } - xAxis := row - 1 - yAxis := TitleToNumber(col) - - rows := xAxis + 1 - cell := yAxis + 1 - - completeRow(xlsx, rows, cell) - completeCol(xlsx, rows, cell) - - xlsx.SheetData.Row[xAxis].C[yAxis].S = f.prepareCellStyle(xlsx, cell, xlsx.SheetData.Row[xAxis].C[yAxis].S) - xlsx.SheetData.Row[xAxis].C[yAxis].T = "b" + cellData, col, _ := f.prepareCell(xlsx, sheet, axis) + cellData.S = f.prepareCellStyle(xlsx, col, cellData.S) + cellData.T = "b" if value { - xlsx.SheetData.Row[xAxis].C[yAxis].V = "1" + cellData.V = "1" } else { - xlsx.SheetData.Row[xAxis].C[yAxis].V = "0" + cellData.V = "0" } } -// GetCellValue provides a function to get formatted value from cell by given -// worksheet name and axis in XLSX file. If it is possible to apply a format -// to the cell value, it will do so, if not then an error will be returned, -// along with the raw value of the cell. -func (f *File) GetCellValue(sheet, axis string) string { +// SetCellStr provides a function to set string type value of a cell. Total +// number of characters that a cell can contain 32767 characters. +func (f *File) SetCellStr(sheet, axis, value string) { xlsx := f.workSheetReader(sheet) - axis = f.mergeCellsParser(xlsx, axis) - row, err := strconv.Atoi(strings.Map(intOnlyMapF, axis)) - if err != nil { - return "" - } - xAxis := row - 1 - rows := len(xlsx.SheetData.Row) - if rows > 1 { - lastRow := xlsx.SheetData.Row[rows-1].R - if lastRow >= rows { - rows = lastRow + cellData, col, _ := f.prepareCell(xlsx, sheet, axis) + + // Leading space(s) character detection. + if len(value) > 0 && value[0] == 32 { + cellData.XMLSpace = xml.Attr{ + Name: xml.Name{Space: NameSpaceXML, Local: "space"}, + Value: "preserve", } } - if rows < xAxis { + + cellData.S = f.prepareCellStyle(xlsx, col, cellData.S) + cellData.T = "str" + cellData.V = value +} + +// SetCellDefault provides a function to set string type value of a cell as +// default format without escaping the cell. +func (f *File) SetCellDefault(sheet, axis, value string) { + xlsx := f.workSheetReader(sheet) + cellData, col, _ := f.prepareCell(xlsx, sheet, axis) + cellData.S = f.prepareCellStyle(xlsx, col, cellData.S) + cellData.T = "" + cellData.V = value +} + +// GetCellFormula provides a function to get formula from cell by given +// worksheet name and axis in XLSX file. +func (f *File) GetCellFormula(sheet, axis string) string { + return f.getCellStringFunc(sheet, axis, func(x *xlsxWorksheet, c *xlsxC) (string, bool) { + if c.F == nil { + return "", false + } + if c.F.T == STCellFormulaTypeShared { + return getSharedForumula(x, c.F.Si), true + } + return c.F.Content, true + }) +} + +// SetCellFormula provides a function to set cell formula by given string and +// worksheet name. +func (f *File) SetCellFormula(sheet, axis, formula string) { + xlsx := f.workSheetReader(sheet) + cellData, _, _ := f.prepareCell(xlsx, sheet, axis) + + if formula == "" { + cellData.F = nil + f.deleteCalcChain(axis) + return + } + + if cellData.F != nil { + cellData.F.Content = formula + } else { + cellData.F = &xlsxF{Content: formula} + } +} + +// GetCellHyperLink provides a function to get cell hyperlink by given +// worksheet name and axis. Boolean type value link will be ture if the cell +// has a hyperlink and the target is the address of the hyperlink. Otherwise, +// the value of link will be false and the value of the target will be a blank +// string. For example get hyperlink of Sheet1!H6: +// +// link, target := xlsx.GetCellHyperLink("Sheet1", "H6") +// +func (f *File) GetCellHyperLink(sheet, axis string) (bool, string) { + // Check for correct cell name + if _, _, err := SplitCellName(axis); err != nil { + panic(err) // Fail fast to avoid possible future side effects + } + + xlsx := f.workSheetReader(sheet) + axis = f.mergeCellsParser(xlsx, axis) + + if xlsx.Hyperlinks != nil { + for _, link := range xlsx.Hyperlinks.Hyperlink { + if link.Ref == axis { + if link.RID != "" { + return true, f.getSheetRelationshipsTargetByID(sheet, link.RID) + } + return true, link.Location + } + } + } + return false, "" +} + +// SetCellHyperLink provides a function to set cell hyperlink by given +// worksheet name and link URL address. LinkType defines two types of +// hyperlink "External" for web site or "Location" for moving to one of cell +// in this workbook. The below is example for external link. +// +// xlsx.SetCellHyperLink("Sheet1", "A3", "https://github.com/360EntSecGroup-Skylar/excelize", "External") +// // Set underline and font color style for the cell. +// style, _ := xlsx.NewStyle(`{"font":{"color":"#1265BE","underline":"single"}}`) +// xlsx.SetCellStyle("Sheet1", "A3", "A3", style) +// +// A this is another example for "Location": +// +// xlsx.SetCellHyperLink("Sheet1", "A3", "Sheet1!A40", "Location") +// +func (f *File) SetCellHyperLink(sheet, axis, link, linkType string) { + // Check for correct cell name + if _, _, err := SplitCellName(axis); err != nil { + panic(err) // Fail fast to avoid possible future side effects + } + + xlsx := f.workSheetReader(sheet) + axis = f.mergeCellsParser(xlsx, axis) + + var linkData xlsxHyperlink + + switch linkType { + case "External": + linkData = xlsxHyperlink{ + Ref: axis, + } + rID := f.addSheetRelationships(sheet, SourceRelationshipHyperLink, link, linkType) + linkData.RID = "rId" + strconv.Itoa(rID) + case "Location": + linkData = xlsxHyperlink{ + Ref: axis, + Location: link, + } + default: + panic(fmt.Errorf("invalid link type %q", linkType)) + } + + if xlsx.Hyperlinks == nil { + xlsx.Hyperlinks = new(xlsxHyperlinks) + } + xlsx.Hyperlinks.Hyperlink = append(xlsx.Hyperlinks.Hyperlink, linkData) +} + +// MergeCell provides a function to merge cells by given coordinate area and +// sheet name. For example create a merged cell of D3:E9 on Sheet1: +// +// xlsx.MergeCell("Sheet1", "D3", "E9") +// +// If you create a merged cell that overlaps with another existing merged cell, +// those merged cells that already exist will be removed. +func (f *File) MergeCell(sheet, hcell, vcell string) { + hcol, hrow, err := CellNameToCoordinates(hcell) + if err != nil { + panic(err) + } + + vcol, vrow, err := CellNameToCoordinates(vcell) + if err != nil { + panic(err) + } + + if hcol == vcol && hrow == vrow { + return + } + + if vcol < hcol { + hcol, vcol = vcol, hcol + } + + if vrow < hrow { + hrow, vrow = vrow, hrow + } + + hcell, _ = CoordinatesToCellName(hcol, hrow) + vcell, _ = CoordinatesToCellName(vcol, vrow) + + xlsx := f.workSheetReader(sheet) + if xlsx.MergeCells != nil { + ref := hcell + ":" + vcell + cells := make([]*xlsxMergeCell, 0, len(xlsx.MergeCells.Cells)) + + // Delete the merged cells of the overlapping area. + for _, cellData := range xlsx.MergeCells.Cells { + cc := strings.Split(cellData.Ref, ":") + if len(cc) != 2 { + panic(fmt.Errorf("invalid area %q", cellData.Ref)) + } + + if !checkCellInArea(hcell, cellData.Ref) && !checkCellInArea(vcell, cellData.Ref) && + !checkCellInArea(cc[0], ref) && !checkCellInArea(cc[1], ref) { + cells = append(cells, cellData) + } + } + cells = append(xlsx.MergeCells.Cells, &xlsxMergeCell{Ref: ref}) + xlsx.MergeCells.Cells = cells + } else { + xlsx.MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: hcell + ":" + vcell}}} + } +} + +// SetSheetRow writes an array to row by given worksheet name, starting +// coordinate and a pointer to array type 'slice'. For example, writes an +// array to row 6 start with the cell B6 on Sheet1: +// +// xlsx.SetSheetRow("Sheet1", "B6", &[]interface{}{"1", nil, 2}) +// +func (f *File) SetSheetRow(sheet, axis string, slice interface{}) { + col, row, err := CellNameToCoordinates(axis) + if err != nil { + panic(err) // Fail fast to avoid future side effects! + } + + // Make sure 'slice' is a Ptr to Slice + v := reflect.ValueOf(slice) + if v.Kind() != reflect.Ptr || v.Elem().Kind() != reflect.Slice { + panic(errors.New("pointer to slice expected")) // Fail fast to avoid future side effects! + } + v = v.Elem() + + for i := 0; i < v.Len(); i++ { + cell, err := CoordinatesToCellName(col+i, row) + // Error should never happens here. But keep ckecking to early detect regresions + // if it will be introduced in furure + if err != nil { + panic(err) // Fail fast to avoid future side effects! + } + f.SetCellValue(sheet, cell, v.Index(i).Interface()) + } +} + +// getCellInfo does common preparation for all SetCell* methods. +func (f *File) prepareCell(xlsx *xlsxWorksheet, sheet, cell string) (*xlsxC, int, int) { + cell = f.mergeCellsParser(xlsx, cell) + + col, row, err := CellNameToCoordinates(cell) + if err != nil { + panic(err) // Fail fast and prevent future side effects + } + + prepareSheetXML(xlsx, col, row) + + return &xlsx.SheetData.Row[row-1].C[col-1], col, row +} + +// getCellStringFunc does common value extraction workflow for all GetCell* methods. +// Passed function implements specific part of required logic. +func (f *File) getCellStringFunc(sheet, axis string, fn func(x *xlsxWorksheet, c *xlsxC) (string, bool)) string { + xlsx := f.workSheetReader(sheet) + axis = f.mergeCellsParser(xlsx, axis) + + _, row, err := CellNameToCoordinates(axis) + if err != nil { + panic(err) // Fail fast to avoid future side effects! + } + + lastRowNum := 0 + if l := len(xlsx.SheetData.Row); l > 0 { + lastRowNum = xlsx.SheetData.Row[l-1].R + } + + // keep in mind: row starts from 1 + if row > lastRowNum { return "" } - for k := range xlsx.SheetData.Row { - if xlsx.SheetData.Row[k].R == row { - for i := range xlsx.SheetData.Row[k].C { - if axis == xlsx.SheetData.Row[k].C[i].R { - val, _ := xlsx.SheetData.Row[k].C[i].getValueFrom(f, f.sharedStringsReader()) - return val - } + + for rowIdx := range xlsx.SheetData.Row { + rowData := &xlsx.SheetData.Row[rowIdx] + if rowData.R != row { + continue + } + for colIdx := range rowData.C { + colData := &rowData.C[colIdx] + if axis != colData.R { + continue + } + if val, ok := fn(xlsx, colData); ok { + return val } } } @@ -198,64 +436,50 @@ func (f *File) formattedValue(s int, v string) string { return v } -// GetCellStyle provides a function to get cell style index by given worksheet -// name and cell coordinates. -func (f *File) GetCellStyle(sheet, axis string) int { - xlsx := f.workSheetReader(sheet) - axis = f.mergeCellsParser(xlsx, axis) - col := string(strings.Map(letterOnlyMapF, axis)) - row, err := strconv.Atoi(strings.Map(intOnlyMapF, axis)) - if err != nil { - return 0 - } - xAxis := row - 1 - yAxis := TitleToNumber(col) - - rows := xAxis + 1 - cell := yAxis + 1 - - completeRow(xlsx, rows, cell) - completeCol(xlsx, rows, cell) - - return f.prepareCellStyle(xlsx, cell, xlsx.SheetData.Row[xAxis].C[yAxis].S) -} - -// GetCellFormula provides a function to get formula from cell by given -// worksheet name and axis in XLSX file. -func (f *File) GetCellFormula(sheet, axis string) string { - xlsx := f.workSheetReader(sheet) - axis = f.mergeCellsParser(xlsx, axis) - row, err := strconv.Atoi(strings.Map(intOnlyMapF, axis)) - if err != nil { - return "" - } - xAxis := row - 1 - rows := len(xlsx.SheetData.Row) - if rows > 1 { - lastRow := xlsx.SheetData.Row[rows-1].R - if lastRow >= rows { - rows = lastRow - } - } - if rows < xAxis { - return "" - } - for k := range xlsx.SheetData.Row { - if xlsx.SheetData.Row[k].R == row { - for i := range xlsx.SheetData.Row[k].C { - if axis == xlsx.SheetData.Row[k].C[i].R { - if xlsx.SheetData.Row[k].C[i].F == nil { - continue - } - if xlsx.SheetData.Row[k].C[i].F.T == STCellFormulaTypeShared { - return getSharedForumula(xlsx, xlsx.SheetData.Row[k].C[i].F.Si) - } - return xlsx.SheetData.Row[k].C[i].F.Content - } +// prepareCellStyle provides a function to prepare style index of cell in +// worksheet by given column index and style index. +func (f *File) prepareCellStyle(xlsx *xlsxWorksheet, col, style int) int { + if xlsx.Cols != nil && style == 0 { + for _, c := range xlsx.Cols.Col { + if c.Min <= col && col <= c.Max { + style = c.Style } } } - return "" + return style +} + +// mergeCellsParser provides a function to check merged cells in worksheet by +// given axis. +func (f *File) mergeCellsParser(xlsx *xlsxWorksheet, axis string) string { + axis = strings.ToUpper(axis) + if xlsx.MergeCells != nil { + for i := 0; i < len(xlsx.MergeCells.Cells); i++ { + if checkCellInArea(axis, xlsx.MergeCells.Cells[i].Ref) { + axis = strings.Split(xlsx.MergeCells.Cells[i].Ref, ":")[0] + } + } + } + return axis +} + +// checkCellInArea provides a function to determine if a given coordinate is +// within an area. +func checkCellInArea(cell, area string) bool { + col, row, err := CellNameToCoordinates(cell) + if err != nil { + panic(err) + } + + rng := strings.Split(area, ":") + if len(rng) != 2 { + return false + } + + firstCol, firtsRow, _ := CellNameToCoordinates(rng[0]) + lastCol, lastRow, _ := CellNameToCoordinates(rng[1]) + + return col >= firstCol && col <= lastCol && row >= firtsRow && row <= lastRow } // getSharedForumula find a cell contains the same formula as another cell, @@ -267,338 +491,12 @@ func (f *File) GetCellFormula(sheet, axis string) string { // Note that this function not validate ref tag to check the cell if or not in // allow area, and always return origin shared formula. func getSharedForumula(xlsx *xlsxWorksheet, si string) string { - for k := range xlsx.SheetData.Row { - for i := range xlsx.SheetData.Row[k].C { - if xlsx.SheetData.Row[k].C[i].F == nil { - continue - } - if xlsx.SheetData.Row[k].C[i].F.T != STCellFormulaTypeShared { - continue - } - if xlsx.SheetData.Row[k].C[i].F.Si != si { - continue - } - if xlsx.SheetData.Row[k].C[i].F.Ref != "" { - return xlsx.SheetData.Row[k].C[i].F.Content + for _, r := range xlsx.SheetData.Row { + for _, c := range r.C { + if c.F != nil && c.F.Ref != "" && c.F.T == STCellFormulaTypeShared && c.F.Si == si { + return c.F.Content } } } return "" } - -// SetCellFormula provides a function to set cell formula by given string and -// worksheet name. -func (f *File) SetCellFormula(sheet, axis, formula string) { - xlsx := f.workSheetReader(sheet) - axis = f.mergeCellsParser(xlsx, axis) - col := string(strings.Map(letterOnlyMapF, axis)) - row, err := strconv.Atoi(strings.Map(intOnlyMapF, axis)) - if err != nil { - return - } - xAxis := row - 1 - yAxis := TitleToNumber(col) - - rows := xAxis + 1 - cell := yAxis + 1 - - completeRow(xlsx, rows, cell) - completeCol(xlsx, rows, cell) - - if formula == "" { - xlsx.SheetData.Row[xAxis].C[yAxis].F = nil - f.deleteCalcChain(axis) - return - } - if xlsx.SheetData.Row[xAxis].C[yAxis].F != nil { - xlsx.SheetData.Row[xAxis].C[yAxis].F.Content = formula - } else { - f := xlsxF{ - Content: formula, - } - xlsx.SheetData.Row[xAxis].C[yAxis].F = &f - } -} - -// SetCellHyperLink provides a function to set cell hyperlink by given -// worksheet name and link URL address. LinkType defines two types of -// hyperlink "External" for web site or "Location" for moving to one of cell -// in this workbook. The below is example for external link. -// -// xlsx.SetCellHyperLink("Sheet1", "A3", "https://github.com/360EntSecGroup-Skylar/excelize", "External") -// // Set underline and font color style for the cell. -// style, _ := xlsx.NewStyle(`{"font":{"color":"#1265BE","underline":"single"}}`) -// xlsx.SetCellStyle("Sheet1", "A3", "A3", style) -// -// A this is another example for "Location": -// -// xlsx.SetCellHyperLink("Sheet1", "A3", "Sheet1!A40", "Location") -// -func (f *File) SetCellHyperLink(sheet, axis, link, linkType string) { - xlsx := f.workSheetReader(sheet) - axis = f.mergeCellsParser(xlsx, axis) - linkTypes := map[string]xlsxHyperlink{ - "External": {}, - "Location": {Location: link}, - } - hyperlink, ok := linkTypes[linkType] - if !ok || axis == "" { - return - } - hyperlink.Ref = axis - if linkType == "External" { - rID := f.addSheetRelationships(sheet, SourceRelationshipHyperLink, link, linkType) - hyperlink.RID = "rId" + strconv.Itoa(rID) - } - if xlsx.Hyperlinks == nil { - xlsx.Hyperlinks = &xlsxHyperlinks{} - } - xlsx.Hyperlinks.Hyperlink = append(xlsx.Hyperlinks.Hyperlink, hyperlink) -} - -// GetCellHyperLink provides a function to get cell hyperlink by given -// worksheet name and axis. Boolean type value link will be ture if the cell -// has a hyperlink and the target is the address of the hyperlink. Otherwise, -// the value of link will be false and the value of the target will be a blank -// string. For example get hyperlink of Sheet1!H6: -// -// link, target := xlsx.GetCellHyperLink("Sheet1", "H6") -// -func (f *File) GetCellHyperLink(sheet, axis string) (bool, string) { - var link bool - var target string - xlsx := f.workSheetReader(sheet) - axis = f.mergeCellsParser(xlsx, axis) - if xlsx.Hyperlinks == nil || axis == "" { - return link, target - } - for h := range xlsx.Hyperlinks.Hyperlink { - if xlsx.Hyperlinks.Hyperlink[h].Ref == axis { - link = true - target = xlsx.Hyperlinks.Hyperlink[h].Location - if xlsx.Hyperlinks.Hyperlink[h].RID != "" { - target = f.getSheetRelationshipsTargetByID(sheet, xlsx.Hyperlinks.Hyperlink[h].RID) - } - } - } - return link, target -} - -// MergeCell provides a function to merge cells by given coordinate area and -// sheet name. For example create a merged cell of D3:E9 on Sheet1: -// -// xlsx.MergeCell("Sheet1", "D3", "E9") -// -// If you create a merged cell that overlaps with another existing merged cell, -// those merged cells that already exist will be removed. -func (f *File) MergeCell(sheet, hcell, vcell string) { - if hcell == vcell { - return - } - - hcell = strings.ToUpper(hcell) - vcell = strings.ToUpper(vcell) - - // Coordinate conversion, convert C1:B3 to 2,0,1,2. - hcol := string(strings.Map(letterOnlyMapF, hcell)) - hrow, _ := strconv.Atoi(strings.Map(intOnlyMapF, hcell)) - hyAxis := hrow - 1 - hxAxis := TitleToNumber(hcol) - - vcol := string(strings.Map(letterOnlyMapF, vcell)) - vrow, _ := strconv.Atoi(strings.Map(intOnlyMapF, vcell)) - vyAxis := vrow - 1 - vxAxis := TitleToNumber(vcol) - - if vxAxis < hxAxis { - hcell, vcell = vcell, hcell - vxAxis, hxAxis = hxAxis, vxAxis - } - - if vyAxis < hyAxis { - hcell, vcell = vcell, hcell - vyAxis, hyAxis = hyAxis, vyAxis - } - - xlsx := f.workSheetReader(sheet) - if xlsx.MergeCells != nil { - mergeCell := xlsxMergeCell{} - // Correct the coordinate area, such correct C1:B3 to B1:C3. - mergeCell.Ref = ToAlphaString(hxAxis) + strconv.Itoa(hyAxis+1) + ":" + ToAlphaString(vxAxis) + strconv.Itoa(vyAxis+1) - // Delete the merged cells of the overlapping area. - for i := 0; i < len(xlsx.MergeCells.Cells); i++ { - if checkCellInArea(hcell, xlsx.MergeCells.Cells[i].Ref) || checkCellInArea(strings.Split(xlsx.MergeCells.Cells[i].Ref, ":")[0], mergeCell.Ref) { - xlsx.MergeCells.Cells = append(xlsx.MergeCells.Cells[:i], xlsx.MergeCells.Cells[i+1:]...) - } else if checkCellInArea(vcell, xlsx.MergeCells.Cells[i].Ref) || checkCellInArea(strings.Split(xlsx.MergeCells.Cells[i].Ref, ":")[1], mergeCell.Ref) { - xlsx.MergeCells.Cells = append(xlsx.MergeCells.Cells[:i], xlsx.MergeCells.Cells[i+1:]...) - } - } - xlsx.MergeCells.Cells = append(xlsx.MergeCells.Cells, &mergeCell) - } else { - mergeCell := xlsxMergeCell{} - // Correct the coordinate area, such correct C1:B3 to B1:C3. - mergeCell.Ref = ToAlphaString(hxAxis) + strconv.Itoa(hyAxis+1) + ":" + ToAlphaString(vxAxis) + strconv.Itoa(vyAxis+1) - mergeCells := xlsxMergeCells{} - mergeCells.Cells = append(mergeCells.Cells, &mergeCell) - xlsx.MergeCells = &mergeCells - } -} - -// SetCellInt provides a function to set int type value of a cell by given -// worksheet name, cell coordinates and cell value. -func (f *File) SetCellInt(sheet, axis string, value int) { - xlsx := f.workSheetReader(sheet) - axis = f.mergeCellsParser(xlsx, axis) - col := string(strings.Map(letterOnlyMapF, axis)) - row, err := strconv.Atoi(strings.Map(intOnlyMapF, axis)) - if err != nil { - return - } - xAxis := row - 1 - yAxis := TitleToNumber(col) - - rows := xAxis + 1 - cell := yAxis + 1 - - completeRow(xlsx, rows, cell) - completeCol(xlsx, rows, cell) - - xlsx.SheetData.Row[xAxis].C[yAxis].S = f.prepareCellStyle(xlsx, cell, xlsx.SheetData.Row[xAxis].C[yAxis].S) - xlsx.SheetData.Row[xAxis].C[yAxis].T = "" - xlsx.SheetData.Row[xAxis].C[yAxis].V = strconv.Itoa(value) -} - -// prepareCellStyle provides a function to prepare style index of cell in -// worksheet by given column index and style index. -func (f *File) prepareCellStyle(xlsx *xlsxWorksheet, col, style int) int { - if xlsx.Cols != nil && style == 0 { - for _, v := range xlsx.Cols.Col { - if v.Min <= col && col <= v.Max { - style = v.Style - } - } - } - return style -} - -// SetCellStr provides a function to set string type value of a cell. Total -// number of characters that a cell can contain 32767 characters. -func (f *File) SetCellStr(sheet, axis, value string) { - xlsx := f.workSheetReader(sheet) - axis = f.mergeCellsParser(xlsx, axis) - if len(value) > 32767 { - value = value[0:32767] - } - col := string(strings.Map(letterOnlyMapF, axis)) - row, err := strconv.Atoi(strings.Map(intOnlyMapF, axis)) - if err != nil { - return - } - xAxis := row - 1 - yAxis := TitleToNumber(col) - - rows := xAxis + 1 - cell := yAxis + 1 - - completeRow(xlsx, rows, cell) - completeCol(xlsx, rows, cell) - - // Leading space(s) character detection. - if len(value) > 0 { - if value[0] == 32 { - xlsx.SheetData.Row[xAxis].C[yAxis].XMLSpace = xml.Attr{ - Name: xml.Name{Space: NameSpaceXML, Local: "space"}, - Value: "preserve", - } - } - } - xlsx.SheetData.Row[xAxis].C[yAxis].S = f.prepareCellStyle(xlsx, cell, xlsx.SheetData.Row[xAxis].C[yAxis].S) - xlsx.SheetData.Row[xAxis].C[yAxis].T = "str" - xlsx.SheetData.Row[xAxis].C[yAxis].V = value -} - -// SetCellDefault provides a function to set string type value of a cell as -// default format without escaping the cell. -func (f *File) SetCellDefault(sheet, axis, value string) { - xlsx := f.workSheetReader(sheet) - axis = f.mergeCellsParser(xlsx, axis) - col := string(strings.Map(letterOnlyMapF, axis)) - row, err := strconv.Atoi(strings.Map(intOnlyMapF, axis)) - if err != nil { - return - } - xAxis := row - 1 - yAxis := TitleToNumber(col) - - rows := xAxis + 1 - cell := yAxis + 1 - - completeRow(xlsx, rows, cell) - completeCol(xlsx, rows, cell) - - xlsx.SheetData.Row[xAxis].C[yAxis].S = f.prepareCellStyle(xlsx, cell, xlsx.SheetData.Row[xAxis].C[yAxis].S) - xlsx.SheetData.Row[xAxis].C[yAxis].T = "" - xlsx.SheetData.Row[xAxis].C[yAxis].V = value -} - -// SetSheetRow writes an array to row by given worksheet name, starting -// coordinate and a pointer to array type 'slice'. For example, writes an -// array to row 6 start with the cell B6 on Sheet1: -// -// xlsx.SetSheetRow("Sheet1", "B6", &[]interface{}{"1", nil, 2}) -// -func (f *File) SetSheetRow(sheet, axis string, slice interface{}) { - xlsx := f.workSheetReader(sheet) - axis = f.mergeCellsParser(xlsx, axis) - col := string(strings.Map(letterOnlyMapF, axis)) - row, err := strconv.Atoi(strings.Map(intOnlyMapF, axis)) - if err != nil { - return - } - // Make sure 'slice' is a Ptr to Slice - v := reflect.ValueOf(slice) - if v.Kind() != reflect.Ptr { - return - } - v = v.Elem() - if v.Kind() != reflect.Slice { - return - } - - xAxis := row - 1 - yAxis := TitleToNumber(col) - - rows := xAxis + 1 - cell := yAxis + 1 - - completeRow(xlsx, rows, cell) - completeCol(xlsx, rows, cell) - - idx := 0 - for i := cell - 1; i < v.Len()+cell-1; i++ { - c := ToAlphaString(i) + strconv.Itoa(row) - f.SetCellValue(sheet, c, v.Index(idx).Interface()) - idx++ - } -} - -// checkCellInArea provides a function to determine if a given coordinate is -// within an area. -func checkCellInArea(cell, area string) bool { - cell = strings.ToUpper(cell) - area = strings.ToUpper(area) - - ref := strings.Split(area, ":") - if len(ref) < 2 { - return false - } - - from := ref[0] - to := ref[1] - - col, row := getCellColRow(cell) - fromCol, fromRow := getCellColRow(from) - toCol, toRow := getCellColRow(to) - - return axisLowerOrEqualThan(fromCol, col) && axisLowerOrEqualThan(col, toCol) && axisLowerOrEqualThan(fromRow, row) && axisLowerOrEqualThan(row, toRow) -} diff --git a/cell_test.go b/cell_test.go index cb3d80e1..d388c7f2 100644 --- a/cell_test.go +++ b/cell_test.go @@ -9,7 +9,6 @@ import ( func TestCheckCellInArea(t *testing.T) { expectedTrueCellInAreaList := [][2]string{ {"c2", "A1:AAZ32"}, - {"AA0", "Z0:AB1"}, {"B9", "A1:B9"}, {"C2", "C2:C2"}, } @@ -18,7 +17,7 @@ func TestCheckCellInArea(t *testing.T) { cell := expectedTrueCellInArea[0] area := expectedTrueCellInArea[1] - assert.True(t, checkCellInArea(cell, area), + assert.Truef(t, checkCellInArea(cell, area), "Expected cell %v to be in area %v, got false\n", cell, area) } @@ -32,7 +31,11 @@ func TestCheckCellInArea(t *testing.T) { cell := expectedFalseCellInArea[0] area := expectedFalseCellInArea[1] - assert.False(t, checkCellInArea(cell, area), + assert.Falsef(t, checkCellInArea(cell, area), "Expected cell %v not to be inside of area %v, but got true\n", cell, area) } + + assert.Panics(t, func() { + checkCellInArea("AA0", "Z0:AB1") + }) } diff --git a/chart.go b/chart.go index f11fd55a..c31995e1 100644 --- a/chart.go +++ b/chart.go @@ -1240,14 +1240,14 @@ func (f *File) drawingParser(path string) (*xlsxWsDr, int) { // addDrawingChart provides a function to add chart graphic frame by given // sheet, drawingXML, cell, width, height, relationship index and format sets. func (f *File) addDrawingChart(sheet, drawingXML, cell string, width, height, rID int, formatSet *formatPicture) { - cell = strings.ToUpper(cell) - fromCol := string(strings.Map(letterOnlyMapF, cell)) - fromRow, _ := strconv.Atoi(strings.Map(intOnlyMapF, cell)) - row := fromRow - 1 - col := TitleToNumber(fromCol) + col, row := MustCellNameToCoordinates(cell) + colIdx := col - 1 + rowIdx := row - 1 + width = int(float64(width) * formatSet.XScale) height = int(float64(height) * formatSet.YScale) - colStart, rowStart, _, _, colEnd, rowEnd, x2, y2 := f.positionObjectPixels(sheet, col, row, formatSet.OffsetX, formatSet.OffsetY, width, height) + colStart, rowStart, _, _, colEnd, rowEnd, x2, y2 := + f.positionObjectPixels(sheet, colIdx, rowIdx, formatSet.OffsetX, formatSet.OffsetY, width, height) content, cNvPrID := f.drawingParser(drawingXML) twoCellAnchor := xdrCellAnchor{} twoCellAnchor.EditAs = formatSet.Positioning diff --git a/chart_test.go b/chart_test.go index f3d7bdf2..1dfc4686 100644 --- a/chart_test.go +++ b/chart_test.go @@ -9,19 +9,46 @@ import ( ) func TestChartSize(t *testing.T) { - - var buffer bytes.Buffer - - categories := map[string]string{"A2": "Small", "A3": "Normal", "A4": "Large", "B1": "Apple", "C1": "Orange", "D1": "Pear"} - values := map[string]int{"B2": 2, "C2": 3, "D2": 3, "B3": 5, "C3": 2, "D3": 4, "B4": 6, "C4": 7, "D4": 8} xlsx := NewFile() - for k, v := range categories { - xlsx.SetCellValue("Sheet1", k, v) + sheet1 := xlsx.GetSheetName(1) + + categories := map[string]string{ + "A2": "Small", + "A3": "Normal", + "A4": "Large", + "B1": "Apple", + "C1": "Orange", + "D1": "Pear", } - for k, v := range values { - xlsx.SetCellValue("Sheet1", k, v) + for cell, v := range categories { + xlsx.SetCellValue(sheet1, cell, v) } - xlsx.AddChart("Sheet1", "E4", `{"type":"col3DClustered","dimension":{"width":640, "height":480},"series":[{"name":"Sheet1!$A$2","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$2:$D$2"},{"name":"Sheet1!$A$3","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$3:$D$3"},{"name":"Sheet1!$A$4","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$4:$D$4"}],"title":{"name":"Fruit 3D Clustered Column Chart"}}`) + + values := map[string]int{ + "B2": 2, + "C2": 3, + "D2": 3, + "B3": 5, + "C3": 2, + "D3": 4, + "B4": 6, + "C4": 7, + "D4": 8, + } + for cell, v := range values { + xlsx.SetCellValue(sheet1, cell, v) + } + + xlsx.AddChart("Sheet1", "E4", `{"type":"col3DClustered","dimension":{"width":640, "height":480},`+ + `"series":[{"name":"Sheet1!$A$2","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$2:$D$2"},`+ + `{"name":"Sheet1!$A$3","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$3:$D$3"},`+ + `{"name":"Sheet1!$A$4","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$4:$D$4"}],`+ + `"title":{"name":"Fruit 3D Clustered Column Chart"}}`) + + var ( + buffer bytes.Buffer + ) + // Save xlsx file by the given path. err := xlsx.Write(&buffer) if !assert.NoError(t, err) { @@ -51,7 +78,8 @@ func TestChartSize(t *testing.T) { t.FailNow() } - err = xml.Unmarshal([]byte(""+workdir.TwoCellAnchor[0].Content+""), &anchor) + err = xml.Unmarshal([]byte(""+ + workdir.TwoCellAnchor[0].Content+""), &anchor) if !assert.NoError(t, err) { t.FailNow() } diff --git a/col.go b/col.go index 1130c3a5..131af1e0 100644 --- a/col.go +++ b/col.go @@ -10,10 +10,7 @@ package excelize import ( - "bytes" "math" - "strconv" - "strings" ) // Define the default cell size and EMU unit of measurement. @@ -29,16 +26,19 @@ const ( // // xlsx.GetColVisible("Sheet1", "D") // -func (f *File) GetColVisible(sheet, column string) bool { +func (f *File) GetColVisible(sheet, col string) bool { + colNum := MustColumnNameToNumber(col) + xlsx := f.workSheetReader(sheet) - col := TitleToNumber(strings.ToUpper(column)) + 1 - visible := true if xlsx.Cols == nil { - return visible + return true } + + visible := true for c := range xlsx.Cols.Col { - if xlsx.Cols.Col[c].Min <= col && col <= xlsx.Cols.Col[c].Max { - visible = !xlsx.Cols.Col[c].Hidden + colData := &xlsx.Cols.Col[c] + if colData.Min <= colNum && colNum <= colData.Max { + visible = !colData.Hidden } } return visible @@ -49,31 +49,31 @@ func (f *File) GetColVisible(sheet, column string) bool { // // xlsx.SetColVisible("Sheet1", "D", false) // -func (f *File) SetColVisible(sheet, column string, visible bool) { - xlsx := f.workSheetReader(sheet) - c := TitleToNumber(strings.ToUpper(column)) + 1 - col := xlsxCol{ - Min: c, - Max: c, +func (f *File) SetColVisible(sheet, col string, visible bool) { + colNum := MustColumnNameToNumber(col) + colData := xlsxCol{ + Min: colNum, + Max: colNum, Hidden: !visible, CustomWidth: true, } + xlsx := f.workSheetReader(sheet) if xlsx.Cols == nil { cols := xlsxCols{} - cols.Col = append(cols.Col, col) + cols.Col = append(cols.Col, colData) xlsx.Cols = &cols return } for v := range xlsx.Cols.Col { - if xlsx.Cols.Col[v].Min <= c && c <= xlsx.Cols.Col[v].Max { - col = xlsx.Cols.Col[v] + if xlsx.Cols.Col[v].Min <= colNum && colNum <= xlsx.Cols.Col[v].Max { + colData = xlsx.Cols.Col[v] } } - col.Min = c - col.Max = c - col.Hidden = !visible - col.CustomWidth = true - xlsx.Cols.Col = append(xlsx.Cols.Col, col) + colData.Min = colNum + colData.Max = colNum + colData.Hidden = !visible + colData.CustomWidth = true + xlsx.Cols.Col = append(xlsx.Cols.Col, colData) } // GetColOutlineLevel provides a function to get outline level of a single @@ -82,16 +82,17 @@ func (f *File) SetColVisible(sheet, column string, visible bool) { // // xlsx.GetColOutlineLevel("Sheet1", "D") // -func (f *File) GetColOutlineLevel(sheet, column string) uint8 { +func (f *File) GetColOutlineLevel(sheet, col string) uint8 { + colNum := MustColumnNameToNumber(col) xlsx := f.workSheetReader(sheet) - col := TitleToNumber(strings.ToUpper(column)) + 1 level := uint8(0) if xlsx.Cols == nil { return level } for c := range xlsx.Cols.Col { - if xlsx.Cols.Col[c].Min <= col && col <= xlsx.Cols.Col[c].Max { - level = xlsx.Cols.Col[c].OutlineLevel + colData := &xlsx.Cols.Col[c] + if colData.Min <= colNum && colNum <= colData.Max { + level = colData.OutlineLevel } } return level @@ -103,31 +104,31 @@ func (f *File) GetColOutlineLevel(sheet, column string) uint8 { // // xlsx.SetColOutlineLevel("Sheet1", "D", 2) // -func (f *File) SetColOutlineLevel(sheet, column string, level uint8) { - xlsx := f.workSheetReader(sheet) - c := TitleToNumber(strings.ToUpper(column)) + 1 - col := xlsxCol{ - Min: c, - Max: c, +func (f *File) SetColOutlineLevel(sheet, col string, level uint8) { + colNum := MustColumnNameToNumber(col) + colData := xlsxCol{ + Min: colNum, + Max: colNum, OutlineLevel: level, CustomWidth: true, } + xlsx := f.workSheetReader(sheet) if xlsx.Cols == nil { cols := xlsxCols{} - cols.Col = append(cols.Col, col) + cols.Col = append(cols.Col, colData) xlsx.Cols = &cols return } for v := range xlsx.Cols.Col { - if xlsx.Cols.Col[v].Min <= c && c <= xlsx.Cols.Col[v].Max { - col = xlsx.Cols.Col[v] + if xlsx.Cols.Col[v].Min <= colNum && colNum <= xlsx.Cols.Col[v].Max { + colData = xlsx.Cols.Col[v] } } - col.Min = c - col.Max = c - col.OutlineLevel = level - col.CustomWidth = true - xlsx.Cols.Col = append(xlsx.Cols.Col, col) + colData.Min = colNum + colData.Max = colNum + colData.OutlineLevel = level + colData.CustomWidth = true + xlsx.Cols.Col = append(xlsx.Cols.Col, colData) } // SetColWidth provides a function to set the width of a single column or @@ -141,11 +142,12 @@ func (f *File) SetColOutlineLevel(sheet, column string, level uint8) { // } // func (f *File) SetColWidth(sheet, startcol, endcol string, width float64) { - min := TitleToNumber(strings.ToUpper(startcol)) + 1 - max := TitleToNumber(strings.ToUpper(endcol)) + 1 + min := MustColumnNameToNumber(startcol) + max := MustColumnNameToNumber(endcol) if min > max { min, max = max, min } + xlsx := f.workSheetReader(sheet) col := xlsxCol{ Min: min, @@ -214,38 +216,38 @@ func (f *File) SetColWidth(sheet, startcol, endcol string, width float64) { // xAbs # Absolute distance to left side of object. // yAbs # Absolute distance to top side of object. // -func (f *File) positionObjectPixels(sheet string, colStart, rowStart, x1, y1, width, height int) (int, int, int, int, int, int, int, int) { +func (f *File) positionObjectPixels(sheet string, col, row, x1, y1, width, height int) (int, int, int, int, int, int, int, int) { xAbs := 0 yAbs := 0 // Calculate the absolute x offset of the top-left vertex. - for colID := 1; colID <= colStart; colID++ { + for colID := 1; colID <= col; colID++ { xAbs += f.getColWidth(sheet, colID) } xAbs += x1 // Calculate the absolute y offset of the top-left vertex. // Store the column change to allow optimisations. - for rowID := 1; rowID <= rowStart; rowID++ { + for rowID := 1; rowID <= row; rowID++ { yAbs += f.getRowHeight(sheet, rowID) } yAbs += y1 // Adjust start column for offsets that are greater than the col width. - for x1 >= f.getColWidth(sheet, colStart) { - x1 -= f.getColWidth(sheet, colStart) - colStart++ + for x1 >= f.getColWidth(sheet, col) { + x1 -= f.getColWidth(sheet, col) + col++ } // Adjust start row for offsets that are greater than the row height. - for y1 >= f.getRowHeight(sheet, rowStart) { - y1 -= f.getRowHeight(sheet, rowStart) - rowStart++ + for y1 >= f.getRowHeight(sheet, row) { + y1 -= f.getRowHeight(sheet, row) + row++ } // Initialise end cell to the same as the start cell. - colEnd := colStart - rowEnd := rowStart + colEnd := col + rowEnd := row width += x1 height += y1 @@ -265,7 +267,7 @@ func (f *File) positionObjectPixels(sheet string, colStart, rowStart, x1, y1, wi // The end vertices are whatever is left from the width and height. x2 := width y2 := height - return colStart, rowStart, xAbs, yAbs, colEnd, rowEnd, x2, y2 + return col, row, xAbs, yAbs, colEnd, rowEnd, x2, y2 } // getColWidth provides a function to get column width in pixels by given @@ -289,13 +291,13 @@ func (f *File) getColWidth(sheet string, col int) int { // GetColWidth provides a function to get column width by given worksheet name // and column index. -func (f *File) GetColWidth(sheet, column string) float64 { - col := TitleToNumber(strings.ToUpper(column)) + 1 +func (f *File) GetColWidth(sheet, col string) float64 { + colNum := MustColumnNameToNumber(col) xlsx := f.workSheetReader(sheet) if xlsx.Cols != nil { var width float64 for _, v := range xlsx.Cols.Col { - if v.Min <= col && col <= v.Max { + if v.Min <= colNum && colNum <= v.Max { width = v.Width } } @@ -312,9 +314,12 @@ func (f *File) GetColWidth(sheet, column string) float64 { // // xlsx.InsertCol("Sheet1", "C") // -func (f *File) InsertCol(sheet, column string) { - col := TitleToNumber(strings.ToUpper(column)) - f.adjustHelper(sheet, col, -1, 1) +func (f *File) InsertCol(sheet, col string) { + num, err := ColumnNameToNumber(col) + if err != nil { + panic(err) + } + f.adjustHelper(sheet, columns, num, 1) } // RemoveCol provides a function to remove single column by given worksheet @@ -326,38 +331,23 @@ func (f *File) InsertCol(sheet, column string) { // as formulas, charts, and so on. If there is any referenced value of the // worksheet, it will cause a file error when you open it. The excelize only // partially updates these references currently. -func (f *File) RemoveCol(sheet, column string) { - xlsx := f.workSheetReader(sheet) - for r := range xlsx.SheetData.Row { - for k, v := range xlsx.SheetData.Row[r].C { - axis := v.R - col := string(strings.Map(letterOnlyMapF, axis)) - if col == column { - xlsx.SheetData.Row[r].C = append(xlsx.SheetData.Row[r].C[:k], xlsx.SheetData.Row[r].C[k+1:]...) - } - } +func (f *File) RemoveCol(sheet, col string) { + num, err := ColumnNameToNumber(col) + if err != nil { + panic(err) // Fail fast to avoid possible future side effects! } - col := TitleToNumber(strings.ToUpper(column)) - f.adjustHelper(sheet, col, -1, -1) -} -// completeCol provieds function to completion column element tags of XML in a -// sheet. -func completeCol(xlsx *xlsxWorksheet, row, cell int) { - buffer := bytes.Buffer{} - for r := range xlsx.SheetData.Row { - if len(xlsx.SheetData.Row[r].C) < cell { - start := len(xlsx.SheetData.Row[r].C) - for iii := start; iii < cell; iii++ { - buffer.WriteString(ToAlphaString(iii)) - buffer.WriteString(strconv.Itoa(r + 1)) - xlsx.SheetData.Row[r].C = append(xlsx.SheetData.Row[r].C, xlsxC{ - R: buffer.String(), - }) - buffer.Reset() + xlsx := f.workSheetReader(sheet) + for rowIdx := range xlsx.SheetData.Row { + rowData := xlsx.SheetData.Row[rowIdx] + for colIdx, cellData := range rowData.C { + colName, _, _ := SplitCellName(cellData.R) + if colName == col { + rowData.C = append(rowData.C[:colIdx], rowData.C[colIdx+1:]...) } } } + f.adjustHelper(sheet, columns, num, -1) } // convertColWidthToPixels provieds function to convert the width of a cell diff --git a/comment.go b/comment.go index 7fb1739a..a94194dd 100644 --- a/comment.go +++ b/comment.go @@ -115,10 +115,9 @@ func (f *File) AddComment(sheet, cell, format string) error { // addDrawingVML provides a function to create comment as // xl/drawings/vmlDrawing%d.vml by given commit ID and cell. func (f *File) addDrawingVML(commentID int, drawingVML, cell string, lineCount, colCount int) { - col := string(strings.Map(letterOnlyMapF, cell)) - row, _ := strconv.Atoi(strings.Map(intOnlyMapF, cell)) + col, row := MustCellNameToCoordinates(cell) + yAxis := col - 1 xAxis := row - 1 - yAxis := TitleToNumber(col) vml := f.VMLDrawing[drawingVML] if vml == nil { vml = &vmlDrawing{ diff --git a/date.go b/date.go index 7dc5ef81..e550feb2 100644 --- a/date.go +++ b/date.go @@ -14,31 +14,53 @@ import ( "time" ) -// timeLocationUTC defined the UTC time location. -var timeLocationUTC, _ = time.LoadLocation("UTC") +const ( + dayNanoseconds = 24 * time.Hour + maxDuration = 290 * 364 * dayNanoseconds +) -// timeToUTCTime provides a function to convert time to UTC time. -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) -} +var ( + excelMinTime1900 = time.Date(1899, time.December, 31, 0, 0, 0, 0, time.UTC) + excelBuggyPeriodStart = time.Date(1900, time.March, 1, 0, 0, 0, 0, time.UTC).Add(-time.Nanosecond) +) // timeToExcelTime provides a function to convert time to Excel time. func timeToExcelTime(t time.Time) float64 { // TODO in future this should probably also handle date1904 and like TimeFromExcelTime - var excelTime float64 - var deltaDays int64 - excelTime = 0 - deltaDays = 290 * 364 - // check if UnixNano would be out of int64 range - for t.Unix() > deltaDays*24*60*60 { - // reduce by aprox. 290 years, which is max for int64 nanoseconds - delta := time.Duration(deltaDays) * 24 * time.Hour - excelTime = excelTime + float64(deltaDays) - t = t.Add(-delta) + + // Force user to explicit convet passed value to UTC time. + // Because for example 1900-01-01 00:00:00 +0300 MSK converts to 1900-01-01 00:00:00 +0230 LMT + // probably due to daylight saving. + if t.Location() != time.UTC { + panic("only UTC time expected") } - // finally add remainder of UnixNano to keep nano precision - // and 25569 which is days between 1900 and 1970 - return excelTime + float64(t.UnixNano())/8.64e13 + 25569.0 + + if t.Before(excelMinTime1900) { + return 0.0 + } + + tt := t + diff := t.Sub(excelMinTime1900) + result := float64(0) + + for diff >= maxDuration { + result += float64(maxDuration / dayNanoseconds) + tt = tt.Add(-maxDuration) + diff = tt.Sub(excelMinTime1900) + } + + rem := diff % dayNanoseconds + result += float64(diff-rem)/float64(dayNanoseconds) + float64(rem)/float64(dayNanoseconds) + + // Excel dates after 28th February 1900 are actually one day out. + // Excel behaves as though the date 29th February 1900 existed, which it didn't. + // Microsoft intentionally included this bug in Excel so that it would remain compatible with the spreadsheet + // program that had the majority market share at the time; Lotus 1-2-3. + // https://www.myonlinetraininghub.com/excel-date-and-time + if t.After(excelBuggyPeriodStart) { + result += 1.0 + } + return result } // shiftJulianToNoon provides a function to process julian date to noon. diff --git a/date_test.go b/date_test.go index a1bfbf48..709fb001 100644 --- a/date_test.go +++ b/date_test.go @@ -13,17 +13,40 @@ type dateTest struct { GoValue time.Time } -func TestTimeToExcelTime(t *testing.T) { - trueExpectedInputList := []dateTest{ - {0.0, time.Date(1899, 12, 30, 0, 0, 0, 0, time.UTC)}, - {25569.0, time.Unix(0, 0)}, - {43269.0, time.Date(2018, 6, 18, 0, 0, 0, 0, time.UTC)}, - {401769.0, time.Date(3000, 1, 1, 0, 0, 0, 0, time.UTC)}, - } +var trueExpectedDateList = []dateTest{ + {0.0000000000000000, time.Date(1899, time.December, 30, 0, 0, 0, 0, time.UTC)}, + {25569.000000000000, time.Unix(0, 0).UTC()}, - for i, test := range trueExpectedInputList { + // Expected values extracted from real xlsx file + {1.0000000000000000, time.Date(1900, time.January, 1, 0, 0, 0, 0, time.UTC)}, + {1.0000115740740740, time.Date(1900, time.January, 1, 0, 0, 1, 0, time.UTC)}, + {1.0006944444444446, time.Date(1900, time.January, 1, 0, 1, 0, 0, time.UTC)}, + {1.0416666666666667, time.Date(1900, time.January, 1, 1, 0, 0, 0, time.UTC)}, + {2.0000000000000000, time.Date(1900, time.January, 2, 0, 0, 0, 0, time.UTC)}, + {43269.000000000000, time.Date(2018, time.June, 18, 0, 0, 0, 0, time.UTC)}, + {43542.611111111109, time.Date(2019, time.March, 18, 14, 40, 0, 0, time.UTC)}, + {401769.00000000000, time.Date(3000, time.January, 1, 0, 0, 0, 0, time.UTC)}, +} + +func TestTimeToExcelTime(t *testing.T) { + for i, test := range trueExpectedDateList { t.Run(fmt.Sprintf("TestData%d", i+1), func(t *testing.T) { - assert.Equal(t, test.ExcelValue, timeToExcelTime(test.GoValue)) + assert.Equalf(t, test.ExcelValue, timeToExcelTime(test.GoValue), + "Time: %s", test.GoValue.String()) + }) + } +} + +func TestTimeToExcelTime_Timezone(t *testing.T) { + msk, err := time.LoadLocation("Europe/Moscow") + if !assert.NoError(t, err) { + t.FailNow() + } + for i, test := range trueExpectedDateList { + t.Run(fmt.Sprintf("TestData%d", i+1), func(t *testing.T) { + assert.Panics(t, func() { + timeToExcelTime(test.GoValue.In(msk)) + }, "Time: %s", test.GoValue.String()) }) } } @@ -43,3 +66,9 @@ func TestTimeFromExcelTime(t *testing.T) { }) } } + +func TestTimeFromExcelTime_1904(t *testing.T) { + shiftJulianToNoon(1, -0.6) + timeFromExcelTime(61, true) + timeFromExcelTime(62, true) +} diff --git a/errors.go b/errors.go new file mode 100644 index 00000000..4dec8159 --- /dev/null +++ b/errors.go @@ -0,0 +1,17 @@ +package excelize + +import ( + "fmt" +) + +func newInvalidColumnNameError(col string) error { + return fmt.Errorf("invalid column name %q", col) +} + +func newInvalidRowNumberError(row int) error { + return fmt.Errorf("invalid row number %d", row) +} + +func newInvalidCellNameError(cell string) error { + return fmt.Errorf("invalid cell name %q", cell) +} diff --git a/errors_test.go b/errors_test.go new file mode 100644 index 00000000..89d241c7 --- /dev/null +++ b/errors_test.go @@ -0,0 +1,21 @@ +package excelize + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewInvalidColNameError(t *testing.T) { + assert.EqualError(t, newInvalidColumnNameError("A"), "invalid column name \"A\"") + assert.EqualError(t, newInvalidColumnNameError(""), "invalid column name \"\"") +} + +func TestNewInvalidRowNumberError(t *testing.T) { + assert.EqualError(t, newInvalidRowNumberError(0), "invalid row number 0") +} + +func TestNewInvalidCellNameError(t *testing.T) { + assert.EqualError(t, newInvalidCellNameError("A"), "invalid cell name \"A\"") + assert.EqualError(t, newInvalidCellNameError(""), "invalid cell name \"\"") +} diff --git a/excelize.go b/excelize.go index feb41cba..7a504604 100644 --- a/excelize.go +++ b/excelize.go @@ -206,235 +206,16 @@ func (f *File) UpdateLinkedValue() { } } -// adjustHelper provides a function to adjust rows and columns dimensions, -// hyperlinks, merged cells and auto filter when inserting or deleting rows or -// columns. -// -// sheet: Worksheet name that we're editing -// column: Index number of the column we're inserting/deleting before -// row: Index number of the row we're inserting/deleting before -// offset: Number of rows/column to insert/delete negative values indicate deletion -// -// TODO: adjustCalcChain, adjustPageBreaks, adjustComments, -// adjustDataValidations, adjustProtectedCells -// -func (f *File) adjustHelper(sheet string, column, row, offset int) { - xlsx := f.workSheetReader(sheet) - f.adjustRowDimensions(xlsx, row, offset) - f.adjustColDimensions(xlsx, column, offset) - f.adjustHyperlinks(sheet, column, row, offset) - f.adjustMergeCells(xlsx, column, row, offset) - f.adjustAutoFilter(xlsx, column, row, offset) - checkSheet(xlsx) - checkRow(xlsx) -} - -// adjustColDimensions provides a function to update column dimensions when -// inserting or deleting rows or columns. -func (f *File) adjustColDimensions(xlsx *xlsxWorksheet, column, offset int) { - for i, r := range xlsx.SheetData.Row { - for k, v := range r.C { - axis := v.R - col := string(strings.Map(letterOnlyMapF, axis)) - row, _ := strconv.Atoi(strings.Map(intOnlyMapF, axis)) - yAxis := TitleToNumber(col) - if yAxis >= column && column != -1 { - xlsx.SheetData.Row[i].C[k].R = ToAlphaString(yAxis+offset) + strconv.Itoa(row) - } - } - } -} - -// adjustRowDimensions provides a function to update row dimensions when -// inserting or deleting rows or columns. -func (f *File) adjustRowDimensions(xlsx *xlsxWorksheet, rowIndex, offset int) { - if rowIndex == -1 { - return - } - for i, r := range xlsx.SheetData.Row { - if r.R >= rowIndex { - f.ajustSingleRowDimensions(&xlsx.SheetData.Row[i], r.R+offset) - } - } -} - -// ajustSingleRowDimensions provides a function to ajust single row dimensions. -func (f *File) ajustSingleRowDimensions(r *xlsxRow, row int) { - r.R = row - for i, col := range r.C { - r.C[i].R = string(strings.Map(letterOnlyMapF, col.R)) + strconv.Itoa(r.R) - } -} - -// adjustHyperlinks provides a function to update hyperlinks when inserting or -// deleting rows or columns. -func (f *File) adjustHyperlinks(sheet string, column, rowIndex, offset int) { - xlsx := f.workSheetReader(sheet) - - // order is important - if xlsx.Hyperlinks != nil && offset < 0 { - for i, v := range xlsx.Hyperlinks.Hyperlink { - axis := v.Ref - col := string(strings.Map(letterOnlyMapF, axis)) - row, _ := strconv.Atoi(strings.Map(intOnlyMapF, axis)) - yAxis := TitleToNumber(col) - if row == rowIndex || yAxis == column { - f.deleteSheetRelationships(sheet, v.RID) - if len(xlsx.Hyperlinks.Hyperlink) > 1 { - xlsx.Hyperlinks.Hyperlink = append(xlsx.Hyperlinks.Hyperlink[:i], xlsx.Hyperlinks.Hyperlink[i+1:]...) - } else { - xlsx.Hyperlinks = nil - } - } - } - } - - if xlsx.Hyperlinks != nil { - for i, v := range xlsx.Hyperlinks.Hyperlink { - axis := v.Ref - col := string(strings.Map(letterOnlyMapF, axis)) - row, _ := strconv.Atoi(strings.Map(intOnlyMapF, axis)) - xAxis := row + offset - yAxis := TitleToNumber(col) - if rowIndex != -1 && row >= rowIndex { - xlsx.Hyperlinks.Hyperlink[i].Ref = col + strconv.Itoa(xAxis) - } - if column != -1 && yAxis >= column { - xlsx.Hyperlinks.Hyperlink[i].Ref = ToAlphaString(yAxis+offset) + strconv.Itoa(row) - } - } - } -} - -// adjustMergeCellsHelper provides a function to update merged cells when -// inserting or deleting rows or columns. -func (f *File) adjustMergeCellsHelper(xlsx *xlsxWorksheet, column, rowIndex, offset int) { - if xlsx.MergeCells != nil { - for k, v := range xlsx.MergeCells.Cells { - beg := strings.Split(v.Ref, ":")[0] - end := strings.Split(v.Ref, ":")[1] - - begcol := string(strings.Map(letterOnlyMapF, beg)) - begrow, _ := strconv.Atoi(strings.Map(intOnlyMapF, beg)) - begxAxis := begrow + offset - begyAxis := TitleToNumber(begcol) - - endcol := string(strings.Map(letterOnlyMapF, end)) - endrow, _ := strconv.Atoi(strings.Map(intOnlyMapF, end)) - endxAxis := endrow + offset - endyAxis := TitleToNumber(endcol) - - if rowIndex != -1 { - if begrow > 1 && begrow >= rowIndex { - beg = begcol + strconv.Itoa(begxAxis) - } - if endrow > 1 && endrow >= rowIndex { - end = endcol + strconv.Itoa(endxAxis) - } - } - - if column != -1 { - if begyAxis >= column { - beg = ToAlphaString(begyAxis+offset) + strconv.Itoa(endrow) - } - if endyAxis >= column { - end = ToAlphaString(endyAxis+offset) + strconv.Itoa(endrow) - } - } - - xlsx.MergeCells.Cells[k].Ref = beg + ":" + end - } - } -} - -// adjustMergeCells provides a function to update merged cells when inserting -// or deleting rows or columns. -func (f *File) adjustMergeCells(xlsx *xlsxWorksheet, column, rowIndex, offset int) { - f.adjustMergeCellsHelper(xlsx, column, rowIndex, offset) - - if xlsx.MergeCells != nil && offset < 0 { - for k, v := range xlsx.MergeCells.Cells { - beg := strings.Split(v.Ref, ":")[0] - end := strings.Split(v.Ref, ":")[1] - if beg == end { - xlsx.MergeCells.Count += offset - if len(xlsx.MergeCells.Cells) > 1 { - xlsx.MergeCells.Cells = append(xlsx.MergeCells.Cells[:k], xlsx.MergeCells.Cells[k+1:]...) - } else { - xlsx.MergeCells = nil - } - } - } - } -} - -// adjustAutoFilter provides a function to update the auto filter when -// inserting or deleting rows or columns. -func (f *File) adjustAutoFilter(xlsx *xlsxWorksheet, column, rowIndex, offset int) { - f.adjustAutoFilterHelper(xlsx, column, rowIndex, offset) - - if xlsx.AutoFilter != nil { - beg := strings.Split(xlsx.AutoFilter.Ref, ":")[0] - end := strings.Split(xlsx.AutoFilter.Ref, ":")[1] - - begcol := string(strings.Map(letterOnlyMapF, beg)) - begrow, _ := strconv.Atoi(strings.Map(intOnlyMapF, beg)) - begxAxis := begrow + offset - - endcol := string(strings.Map(letterOnlyMapF, end)) - endrow, _ := strconv.Atoi(strings.Map(intOnlyMapF, end)) - endxAxis := endrow + offset - endyAxis := TitleToNumber(endcol) - - if rowIndex != -1 { - if begrow >= rowIndex { - beg = begcol + strconv.Itoa(begxAxis) - } - if endrow >= rowIndex { - end = endcol + strconv.Itoa(endxAxis) - } - } - - if column != -1 && endyAxis >= column { - end = ToAlphaString(endyAxis+offset) + strconv.Itoa(endrow) - } - xlsx.AutoFilter.Ref = beg + ":" + end - } -} - -// adjustAutoFilterHelper provides a function to update the auto filter when -// inserting or deleting rows or columns. -func (f *File) adjustAutoFilterHelper(xlsx *xlsxWorksheet, column, rowIndex, offset int) { - if xlsx.AutoFilter != nil { - beg := strings.Split(xlsx.AutoFilter.Ref, ":")[0] - end := strings.Split(xlsx.AutoFilter.Ref, ":")[1] - - begcol := string(strings.Map(letterOnlyMapF, beg)) - begrow, _ := strconv.Atoi(strings.Map(intOnlyMapF, beg)) - begyAxis := TitleToNumber(begcol) - - endcol := string(strings.Map(letterOnlyMapF, end)) - endyAxis := TitleToNumber(endcol) - endrow, _ := strconv.Atoi(strings.Map(intOnlyMapF, end)) - - if (begrow == rowIndex && offset < 0) || (column == begyAxis && column == endyAxis) { - xlsx.AutoFilter = nil - for i, r := range xlsx.SheetData.Row { - if begrow < r.R && r.R <= endrow { - xlsx.SheetData.Row[i].Hidden = false - } - } - } - } -} - // GetMergeCells provides a function to get all merged cells from a worksheet currently. func (f *File) GetMergeCells(sheet string) []MergeCell { - mergeCells := []MergeCell{} - xlsx := f.workSheetReader(sheet) + + var mergeCells []MergeCell + if xlsx.MergeCells != nil { - for i := 0; i < len(xlsx.MergeCells.Cells); i++ { + mergeCells = make([]MergeCell, 0, len(xlsx.MergeCells.Cells)) + + for i := range xlsx.MergeCells.Cells { ref := xlsx.MergeCells.Cells[i].Ref axis := strings.Split(ref, ":")[0] mergeCells = append(mergeCells, []string{ref, f.GetCellValue(sheet, axis)}) diff --git a/excelize_test.go b/excelize_test.go index 47b9561c..694f505b 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -35,13 +35,22 @@ func TestOpenFile(t *testing.T) { t.Log("\r\n") } xlsx.UpdateLinkedValue() + xlsx.SetCellDefault("Sheet2", "A1", strconv.FormatFloat(float64(100.1588), 'f', -1, 32)) xlsx.SetCellDefault("Sheet2", "A1", strconv.FormatFloat(float64(-100.1588), 'f', -1, 64)) + // Test set cell value with illegal row number. - xlsx.SetCellDefault("Sheet2", "A", strconv.FormatFloat(float64(-100.1588), 'f', -1, 64)) + assert.Panics(t, func() { + xlsx.SetCellDefault("Sheet2", "A", strconv.FormatFloat(float64(-100.1588), 'f', -1, 64)) + }) + xlsx.SetCellInt("Sheet2", "A1", 100) + // Test set cell integer value with illegal row number. - xlsx.SetCellInt("Sheet2", "A", 100) + assert.Panics(t, func() { + xlsx.SetCellInt("Sheet2", "A", 100) + }) + xlsx.SetCellStr("Sheet2", "C11", "Knowns") // Test max characters in a cell. xlsx.SetCellStr("Sheet2", "D11", strings.Repeat("c", 32769)) @@ -51,23 +60,38 @@ func TestOpenFile(t *testing.T) { xlsx.SetCellInt("Sheet3", "A23", 10) xlsx.SetCellStr("Sheet3", "b230", "10") xlsx.SetCellStr("Sheet10", "b230", "10") + // Test set cell string value with illegal row number. - xlsx.SetCellStr("Sheet10", "A", "10") + assert.Panics(t, func() { + xlsx.SetCellStr("Sheet10", "A", "10") + }) + xlsx.SetActiveSheet(2) // Test get cell formula with given rows number. xlsx.GetCellFormula("Sheet1", "B19") // Test get cell formula with illegal worksheet name. xlsx.GetCellFormula("Sheet2", "B20") - // Test get cell formula with illegal rows number. xlsx.GetCellFormula("Sheet1", "B20") - xlsx.GetCellFormula("Sheet1", "B") + + // Test get cell formula with illegal rows number. + assert.Panics(t, func() { + xlsx.GetCellFormula("Sheet1", "B") + }) + // Test get shared cell formula xlsx.GetCellFormula("Sheet2", "H11") xlsx.GetCellFormula("Sheet2", "I11") getSharedForumula(&xlsxWorksheet{}, "") + // Test read cell value with given illegal rows number. - xlsx.GetCellValue("Sheet2", "a-1") - xlsx.GetCellValue("Sheet2", "A") + assert.Panics(t, func() { + xlsx.GetCellValue("Sheet2", "a-1") + }) + + assert.Panics(t, func() { + xlsx.GetCellValue("Sheet2", "A") + }) + // Test read cell value with given lowercase column number. xlsx.GetCellValue("Sheet2", "a5") xlsx.GetCellValue("Sheet2", "C11") @@ -92,10 +116,7 @@ func TestOpenFile(t *testing.T) { xlsx.SetCellValue("Sheet2", "F15", uint64(1<<32-1)) xlsx.SetCellValue("Sheet2", "F16", true) xlsx.SetCellValue("Sheet2", "F17", complex64(5+10i)) - t.Log(letterOnlyMapF('x')) - shiftJulianToNoon(1, -0.6) - timeFromExcelTime(61, true) - timeFromExcelTime(62, true) + // Test boolean write booltest := []struct { value bool @@ -108,8 +129,14 @@ func TestOpenFile(t *testing.T) { xlsx.SetCellValue("Sheet2", "F16", test.value) assert.Equal(t, test.expected, xlsx.GetCellValue("Sheet2", "F16")) } + xlsx.SetCellValue("Sheet2", "G2", nil) - xlsx.SetCellValue("Sheet2", "G4", time.Now()) + + assert.Panics(t, func() { + xlsx.SetCellValue("Sheet2", "G4", time.Now()) + }) + + xlsx.SetCellValue("Sheet2", "G4", time.Now().UTC()) // 02:46:40 xlsx.SetCellValue("Sheet2", "G5", time.Duration(1e13)) // Test completion column. @@ -298,8 +325,15 @@ func TestSetCellHyperLink(t *testing.T) { xlsx.SetCellHyperLink("Sheet2", "C1", "https://github.com/360EntSecGroup-Skylar/excelize", "External") // Test add Location hyperlink in a work sheet. xlsx.SetCellHyperLink("Sheet2", "D6", "Sheet1!D8", "Location") - xlsx.SetCellHyperLink("Sheet2", "C3", "Sheet1!D8", "") - xlsx.SetCellHyperLink("Sheet2", "", "Sheet1!D60", "Location") + + assert.Panics(t, func() { + xlsx.SetCellHyperLink("Sheet2", "C3", "Sheet1!D8", "") + }) + + assert.Panics(t, func() { + xlsx.SetCellHyperLink("Sheet2", "", "Sheet1!D60", "Location") + }) + assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestSetCellHyperLink.xlsx"))) } @@ -309,9 +343,11 @@ func TestGetCellHyperLink(t *testing.T) { t.FailNow() } - link, target := xlsx.GetCellHyperLink("Sheet1", "") - t.Log(link, target) - link, target = xlsx.GetCellHyperLink("Sheet1", "A22") + assert.Panics(t, func() { + xlsx.GetCellHyperLink("Sheet1", "") + }) + + link, target := xlsx.GetCellHyperLink("Sheet1", "A22") t.Log(link, target) link, target = xlsx.GetCellHyperLink("Sheet2", "D6") t.Log(link, target) @@ -327,8 +363,12 @@ func TestSetCellFormula(t *testing.T) { xlsx.SetCellFormula("Sheet1", "B19", "SUM(Sheet2!D2,Sheet2!D11)") xlsx.SetCellFormula("Sheet1", "C19", "SUM(Sheet2!D2,Sheet2!D9)") + // Test set cell formula with illegal rows number. - xlsx.SetCellFormula("Sheet1", "C", "SUM(Sheet2!D2,Sheet2!D9)") + assert.Panics(t, func() { + xlsx.SetCellFormula("Sheet1", "C", "SUM(Sheet2!D2,Sheet2!D9)") + }) + assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestSetCellFormula1.xlsx"))) xlsx, err = OpenFile(filepath.Join("test", "CalcChain.xlsx")) @@ -408,51 +448,39 @@ func TestGetMergeCells(t *testing.T) { value string start string end string - }{ - { - value: "A1", - start: "A1", - end: "B1", - }, - { - value: "A2", - start: "A2", - end: "A3", - }, - { - value: "A4", - start: "A4", - end: "B5", - }, - { - value: "A7", - start: "A7", - end: "C10", - }, - } + }{{ + value: "A1", + start: "A1", + end: "B1", + }, { + value: "A2", + start: "A2", + end: "A3", + }, { + value: "A4", + start: "A4", + end: "B5", + }, { + value: "A7", + start: "A7", + end: "C10", + }} xlsx, err := OpenFile(filepath.Join("test", "MergeCell.xlsx")) if !assert.NoError(t, err) { t.FailNow() } + sheet1 := xlsx.GetSheetName(1) - mergeCells := xlsx.GetMergeCells("Sheet1") - if len(mergeCells) != len(wants) { - t.Fatalf("Expected count of merge cells %d, but got %d\n", len(wants), len(mergeCells)) + mergeCells := xlsx.GetMergeCells(sheet1) + if !assert.Len(t, mergeCells, len(wants)) { + t.FailNow() } for i, m := range mergeCells { - if wants[i].value != m.GetCellValue() { - t.Fatalf("Expected merged cell value %s, but got %s\n", wants[i].value, m.GetCellValue()) - } - - if wants[i].start != m.GetStartAxis() { - t.Fatalf("Expected merged cell value %s, but got %s\n", wants[i].start, m.GetStartAxis()) - } - - if wants[i].end != m.GetEndAxis() { - t.Fatalf("Expected merged cell value %s, but got %s\n", wants[i].end, m.GetEndAxis()) - } + assert.Equal(t, wants[i].value, m.GetCellValue()) + assert.Equal(t, wants[i].start, m.GetStartAxis()) + assert.Equal(t, wants[i].end, m.GetEndAxis()) } } @@ -469,11 +497,20 @@ func TestSetCellStyleAlignment(t *testing.T) { } xlsx.SetCellStyle("Sheet1", "A22", "A22", style) + // Test set cell style with given illegal rows number. - xlsx.SetCellStyle("Sheet1", "A", "A22", style) - xlsx.SetCellStyle("Sheet1", "A22", "A", style) + assert.Panics(t, func() { + xlsx.SetCellStyle("Sheet1", "A", "A22", style) + }) + + assert.Panics(t, func() { + xlsx.SetCellStyle("Sheet1", "A22", "A", style) + }) + // Test get cell style with given illegal rows number. - xlsx.GetCellStyle("Sheet1", "A") + assert.Panics(t, func() { + xlsx.GetCellStyle("Sheet1", "A") + }) assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestSetCellStyleAlignment.xlsx"))) } @@ -782,46 +819,48 @@ func TestGetPicture(t *testing.T) { } file, raw := xlsx.GetPicture("Sheet1", "F21") - if file == "" { - err = ioutil.WriteFile(file, raw, 0644) - if !assert.NoError(t, err) { - t.FailNow() - } + if !assert.NotEmpty(t, file) || !assert.NotEmpty(t, raw) || + !assert.NoError(t, ioutil.WriteFile(file, raw, 0644)) { + + t.FailNow() } // Try to get picture from a worksheet that doesn't contain any images. file, raw = xlsx.GetPicture("Sheet3", "I9") - if file != "" { - err = ioutil.WriteFile(file, raw, 0644) - if !assert.NoError(t, err) { - t.FailNow() - } - } + assert.Empty(t, file) + assert.Empty(t, raw) + // Try to get picture from a cell that doesn't contain an image. file, raw = xlsx.GetPicture("Sheet2", "A2") - t.Log(file, len(raw)) + assert.Empty(t, file) + assert.Empty(t, raw) + xlsx.getDrawingRelationships("xl/worksheets/_rels/sheet1.xml.rels", "rId8") xlsx.getDrawingRelationships("", "") xlsx.getSheetRelationshipsTargetByID("", "") xlsx.deleteSheetRelationships("", "") // Try to get picture from a local storage file. - assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestGetPicture.xlsx"))) + if !assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestGetPicture.xlsx"))) { + t.FailNow() + } + xlsx, err = OpenFile(filepath.Join("test", "TestGetPicture.xlsx")) if !assert.NoError(t, err) { t.FailNow() } + file, raw = xlsx.GetPicture("Sheet1", "F21") - if file == "" { - err = ioutil.WriteFile(file, raw, 0644) - if !assert.NoError(t, err) { - t.FailNow() - } + if !assert.NotEmpty(t, file) || !assert.NotEmpty(t, raw) || + !assert.NoError(t, ioutil.WriteFile(file, raw, 0644)) { + + t.FailNow() } // Try to get picture from a local storage file that doesn't contain an image. file, raw = xlsx.GetPicture("Sheet1", "F22") - t.Log(file, len(raw)) + assert.Empty(t, file) + assert.Empty(t, raw) } func TestSheetVisibility(t *testing.T) { @@ -838,21 +877,6 @@ func TestSheetVisibility(t *testing.T) { assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestSheetVisibility.xlsx"))) } -func TestRowVisibility(t *testing.T) { - xlsx, err := prepareTestBook1() - if !assert.NoError(t, err) { - t.FailNow() - } - - xlsx.SetRowVisible("Sheet3", 2, false) - xlsx.SetRowVisible("Sheet3", 2, true) - xlsx.SetRowVisible("Sheet3", 0, true) - xlsx.GetRowVisible("Sheet3", 2) - xlsx.GetRowVisible("Sheet3", 0) - - assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestRowVisibility.xlsx"))) -} - func TestColumnVisibility(t *testing.T) { t.Run("TestBook1", func(t *testing.T) { xlsx, err := prepareTestBook1() @@ -1065,38 +1089,37 @@ func TestAddChart(t *testing.T) { func TestInsertCol(t *testing.T) { xlsx := NewFile() - for j := 1; j <= 10; j++ { - for i := 0; i <= 10; i++ { - axis := ToAlphaString(i) + strconv.Itoa(j) - xlsx.SetCellStr("Sheet1", axis, axis) - } - } - xlsx.SetCellHyperLink("Sheet1", "A5", "https://github.com/360EntSecGroup-Skylar/excelize", "External") - xlsx.MergeCell("Sheet1", "A1", "C3") - err := xlsx.AutoFilter("Sheet1", "A2", "B2", `{"column":"B","expression":"x != blanks"}`) + sheet1 := xlsx.GetSheetName(1) + + fillCells(xlsx, sheet1, 10, 10) + + xlsx.SetCellHyperLink(sheet1, "A5", "https://github.com/360EntSecGroup-Skylar/excelize", "External") + xlsx.MergeCell(sheet1, "A1", "C3") + + err := xlsx.AutoFilter(sheet1, "A2", "B2", `{"column":"B","expression":"x != blanks"}`) if !assert.NoError(t, err) { t.FailNow() } - xlsx.InsertCol("Sheet1", "A") + xlsx.InsertCol(sheet1, "A") assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestInsertCol.xlsx"))) } func TestRemoveCol(t *testing.T) { xlsx := NewFile() - for j := 1; j <= 10; j++ { - for i := 0; i <= 10; i++ { - axis := ToAlphaString(i) + strconv.Itoa(j) - xlsx.SetCellStr("Sheet1", axis, axis) - } - } - xlsx.SetCellHyperLink("Sheet1", "A5", "https://github.com/360EntSecGroup-Skylar/excelize", "External") - xlsx.SetCellHyperLink("Sheet1", "C5", "https://github.com", "External") - xlsx.MergeCell("Sheet1", "A1", "B1") - xlsx.MergeCell("Sheet1", "A2", "B2") - xlsx.RemoveCol("Sheet1", "A") - xlsx.RemoveCol("Sheet1", "A") + sheet1 := xlsx.GetSheetName(1) + + fillCells(xlsx, sheet1, 10, 15) + + xlsx.SetCellHyperLink(sheet1, "A5", "https://github.com/360EntSecGroup-Skylar/excelize", "External") + xlsx.SetCellHyperLink(sheet1, "C5", "https://github.com", "External") + + xlsx.MergeCell(sheet1, "A1", "B1") + xlsx.MergeCell(sheet1, "A2", "B2") + + xlsx.RemoveCol(sheet1, "A") + xlsx.RemoveCol(sheet1, "A") assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestRemoveCol.xlsx"))) } @@ -1117,11 +1140,10 @@ func TestSetPane(t *testing.T) { func TestConditionalFormat(t *testing.T) { xlsx := NewFile() - for j := 1; j <= 10; j++ { - for i := 0; i <= 15; i++ { - xlsx.SetCellInt("Sheet1", ToAlphaString(i)+strconv.Itoa(j), j) - } - } + sheet1 := xlsx.GetSheetName(1) + + fillCells(xlsx, sheet1, 10, 15) + var format1, format2, format3 int var err error // Rose format for bad conditional. @@ -1143,31 +1165,31 @@ func TestConditionalFormat(t *testing.T) { } // Color scales: 2 color. - xlsx.SetConditionalFormat("Sheet1", "A1:A10", `[{"type":"2_color_scale","criteria":"=","min_type":"min","max_type":"max","min_color":"#F8696B","max_color":"#63BE7B"}]`) + xlsx.SetConditionalFormat(sheet1, "A1:A10", `[{"type":"2_color_scale","criteria":"=","min_type":"min","max_type":"max","min_color":"#F8696B","max_color":"#63BE7B"}]`) // Color scales: 3 color. - xlsx.SetConditionalFormat("Sheet1", "B1:B10", `[{"type":"3_color_scale","criteria":"=","min_type":"min","mid_type":"percentile","max_type":"max","min_color":"#F8696B","mid_color":"#FFEB84","max_color":"#63BE7B"}]`) + xlsx.SetConditionalFormat(sheet1, "B1:B10", `[{"type":"3_color_scale","criteria":"=","min_type":"min","mid_type":"percentile","max_type":"max","min_color":"#F8696B","mid_color":"#FFEB84","max_color":"#63BE7B"}]`) // Hightlight cells rules: between... - xlsx.SetConditionalFormat("Sheet1", "C1:C10", fmt.Sprintf(`[{"type":"cell","criteria":"between","format":%d,"minimum":"6","maximum":"8"}]`, format1)) + xlsx.SetConditionalFormat(sheet1, "C1:C10", fmt.Sprintf(`[{"type":"cell","criteria":"between","format":%d,"minimum":"6","maximum":"8"}]`, format1)) // Hightlight cells rules: Greater Than... - xlsx.SetConditionalFormat("Sheet1", "D1:D10", fmt.Sprintf(`[{"type":"cell","criteria":">","format":%d,"value":"6"}]`, format3)) + xlsx.SetConditionalFormat(sheet1, "D1:D10", fmt.Sprintf(`[{"type":"cell","criteria":">","format":%d,"value":"6"}]`, format3)) // Hightlight cells rules: Equal To... - xlsx.SetConditionalFormat("Sheet1", "E1:E10", fmt.Sprintf(`[{"type":"top","criteria":"=","format":%d}]`, format3)) + xlsx.SetConditionalFormat(sheet1, "E1:E10", fmt.Sprintf(`[{"type":"top","criteria":"=","format":%d}]`, format3)) // Hightlight cells rules: Not Equal To... - xlsx.SetConditionalFormat("Sheet1", "F1:F10", fmt.Sprintf(`[{"type":"unique","criteria":"=","format":%d}]`, format2)) + xlsx.SetConditionalFormat(sheet1, "F1:F10", fmt.Sprintf(`[{"type":"unique","criteria":"=","format":%d}]`, format2)) // Hightlight cells rules: Duplicate Values... - xlsx.SetConditionalFormat("Sheet1", "G1:G10", fmt.Sprintf(`[{"type":"duplicate","criteria":"=","format":%d}]`, format2)) + xlsx.SetConditionalFormat(sheet1, "G1:G10", fmt.Sprintf(`[{"type":"duplicate","criteria":"=","format":%d}]`, format2)) // Top/Bottom rules: Top 10%. - xlsx.SetConditionalFormat("Sheet1", "H1:H10", fmt.Sprintf(`[{"type":"top","criteria":"=","format":%d,"value":"6","percent":true}]`, format1)) + xlsx.SetConditionalFormat(sheet1, "H1:H10", fmt.Sprintf(`[{"type":"top","criteria":"=","format":%d,"value":"6","percent":true}]`, format1)) // Top/Bottom rules: Above Average... - xlsx.SetConditionalFormat("Sheet1", "I1:I10", fmt.Sprintf(`[{"type":"average","criteria":"=","format":%d, "above_average": true}]`, format3)) + xlsx.SetConditionalFormat(sheet1, "I1:I10", fmt.Sprintf(`[{"type":"average","criteria":"=","format":%d, "above_average": true}]`, format3)) // Top/Bottom rules: Below Average... - xlsx.SetConditionalFormat("Sheet1", "J1:J10", fmt.Sprintf(`[{"type":"average","criteria":"=","format":%d, "above_average": false}]`, format1)) + xlsx.SetConditionalFormat(sheet1, "J1:J10", fmt.Sprintf(`[{"type":"average","criteria":"=","format":%d, "above_average": false}]`, format1)) // Data Bars: Gradient Fill. - xlsx.SetConditionalFormat("Sheet1", "K1:K10", `[{"type":"data_bar", "criteria":"=", "min_type":"min","max_type":"max","bar_color":"#638EC6"}]`) + xlsx.SetConditionalFormat(sheet1, "K1:K10", `[{"type":"data_bar", "criteria":"=", "min_type":"min","max_type":"max","bar_color":"#638EC6"}]`) // Use a formula to determine which cells to format. - xlsx.SetConditionalFormat("Sheet1", "L1:L10", fmt.Sprintf(`[{"type":"formula", "criteria":"L2<3", "format":%d}]`, format1)) + xlsx.SetConditionalFormat(sheet1, "L1:L10", fmt.Sprintf(`[{"type":"formula", "criteria":"L2<3", "format":%d}]`, format1)) // Test set invalid format set in conditional format - xlsx.SetConditionalFormat("Sheet1", "L1:L10", "") + xlsx.SetConditionalFormat(sheet1, "L1:L10", "") err = xlsx.SaveAs(filepath.Join("test", "TestConditionalFormat.xlsx")) if !assert.NoError(t, err) { @@ -1175,9 +1197,9 @@ func TestConditionalFormat(t *testing.T) { } // Set conditional format with illegal valid type. - xlsx.SetConditionalFormat("Sheet1", "K1:K10", `[{"type":"", "criteria":"=", "min_type":"min","max_type":"max","bar_color":"#638EC6"}]`) + xlsx.SetConditionalFormat(sheet1, "K1:K10", `[{"type":"", "criteria":"=", "min_type":"min","max_type":"max","bar_color":"#638EC6"}]`) // Set conditional format with illegal criteria type. - xlsx.SetConditionalFormat("Sheet1", "K1:K10", `[{"type":"data_bar", "criteria":"", "min_type":"min","max_type":"max","bar_color":"#638EC6"}]`) + xlsx.SetConditionalFormat(sheet1, "K1:K10", `[{"type":"data_bar", "criteria":"", "min_type":"min","max_type":"max","bar_color":"#638EC6"}]`) // Set conditional format with file without dxfs element shold not return error. xlsx, err = OpenFile(filepath.Join("test", "Book1.xlsx")) @@ -1193,11 +1215,9 @@ func TestConditionalFormat(t *testing.T) { func TestConditionalFormatError(t *testing.T) { xlsx := NewFile() - for j := 1; j <= 10; j++ { - for i := 0; i <= 15; i++ { - xlsx.SetCellInt("Sheet1", ToAlphaString(i)+strconv.Itoa(j), j) - } - } + sheet1 := xlsx.GetSheetName(1) + + fillCells(xlsx, sheet1, 10, 15) // Set conditional format with illegal JSON string should return error _, err := xlsx.NewConditionalStyle("") @@ -1206,15 +1226,6 @@ func TestConditionalFormatError(t *testing.T) { } } -func TestTitleToNumber(t *testing.T) { - assert.Equal(t, 0, TitleToNumber("A")) - assert.Equal(t, 25, TitleToNumber("Z")) - assert.Equal(t, 26, TitleToNumber("AA")) - assert.Equal(t, 36, TitleToNumber("AK")) - assert.Equal(t, 36, TitleToNumber("ak")) - assert.Equal(t, 51, TitleToNumber("AZ")) -} - func TestSharedStrings(t *testing.T) { xlsx, err := OpenFile(filepath.Join("test", "SharedStrings.xlsx")) if !assert.NoError(t, err) { @@ -1229,10 +1240,19 @@ func TestSetSheetRow(t *testing.T) { t.FailNow() } - xlsx.SetSheetRow("Sheet1", "B27", &[]interface{}{"cell", nil, int32(42), float64(42), time.Now()}) - xlsx.SetSheetRow("Sheet1", "", &[]interface{}{"cell", nil, 2}) - xlsx.SetSheetRow("Sheet1", "B27", []interface{}{}) - xlsx.SetSheetRow("Sheet1", "B27", &xlsx) + xlsx.SetSheetRow("Sheet1", "B27", &[]interface{}{"cell", nil, int32(42), float64(42), time.Now().UTC()}) + + assert.Panics(t, func() { + xlsx.SetSheetRow("Sheet1", "", &[]interface{}{"cell", nil, 2}) + }) + + assert.Panics(t, func() { + xlsx.SetSheetRow("Sheet1", "B27", []interface{}{}) + }) + + assert.Panics(t, func() { + xlsx.SetSheetRow("Sheet1", "B27", &xlsx) + }) assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestSetSheetRow.xlsx"))) } @@ -1245,10 +1265,17 @@ func TestOutlineLevel(t *testing.T) { xlsx.GetColOutlineLevel("Shee2", "A") xlsx.SetColWidth("Sheet2", "A", "D", 13) xlsx.SetColOutlineLevel("Sheet2", "B", 2) - xlsx.SetRowOutlineLevel("Sheet1", 2, 1) - xlsx.SetRowOutlineLevel("Sheet1", 0, 1) - xlsx.GetRowOutlineLevel("Sheet1", 2) - xlsx.GetRowOutlineLevel("Sheet1", 0) + xlsx.SetRowOutlineLevel("Sheet1", 2, 250) + + assert.Panics(t, func() { + xlsx.SetRowOutlineLevel("Sheet1", 0, 1) + }) + + assert.Equal(t, uint8(250), xlsx.GetRowOutlineLevel("Sheet1", 2)) + + assert.Panics(t, func() { + xlsx.GetRowOutlineLevel("Sheet1", 0) + }) err := xlsx.SaveAs(filepath.Join("test", "TestOutlineLevel.xlsx")) if !assert.NoError(t, err) { t.FailNow() @@ -1258,7 +1285,6 @@ func TestOutlineLevel(t *testing.T) { if !assert.NoError(t, err) { t.FailNow() } - xlsx.SetColOutlineLevel("Sheet2", "B", 2) } @@ -1388,3 +1414,12 @@ func prepareTestBook4() (*File, error) { return xlsx, nil } + +func fillCells(xlsx *File, sheet string, colCount, rowCount int) { + for col := 1; col <= colCount; col++ { + for row := 1; row <= rowCount; row++ { + cell := MustCoordinatesToCellName(col, row) + xlsx.SetCellStr(sheet, cell, cell) + } + } +} diff --git a/lib.go b/lib.go index 30a20e03..c33c934b 100644 --- a/lib.go +++ b/lib.go @@ -12,11 +12,11 @@ package excelize import ( "archive/zip" "bytes" + "fmt" "io" "log" "strconv" "strings" - "unicode" ) // ReadZipReader can be used to read an XLSX in memory without touching the @@ -64,64 +64,177 @@ func readFile(file *zip.File) []byte { return buff.Bytes() } -// ToAlphaString provides a function to convert integer to Excel sheet column -// title. For example convert 36 to column title AK: +// SplitCellName splits cell name to column name and row number. // -// excelize.ToAlphaString(36) +// Example: // -func ToAlphaString(value int) string { - if value < 0 { - return "" +// excelize.SplitCellName("AK74") // return "AK", 74, nil +// +func SplitCellName(cell string) (string, int, error) { + alpha := func(r rune) bool { + return ('A' <= r && r <= 'Z') || ('a' <= r && r <= 'z') } - var ans string - i := value + 1 - for i > 0 { - ans = string((i-1)%26+65) + ans - i = (i - 1) / 26 - } - return ans -} -// TitleToNumber provides a function to convert Excel sheet column title to -// int (this function doesn't do value check currently). For example convert -// AK and ak to column title 36: -// -// excelize.TitleToNumber("AK") -// excelize.TitleToNumber("ak") -// -func TitleToNumber(s string) int { - weight := 1 - sum := 0 - for i := len(s) - 1; i >= 0; i-- { - ch := s[i] - if ch >= 'a' && ch <= 'z' { - ch -= 32 + if strings.IndexFunc(cell, alpha) == 0 { + i := strings.LastIndexFunc(cell, alpha) + if i >= 0 && i < len(cell)-1 { + col, rowstr := cell[:i+1], cell[i+1:] + if row, err := strconv.Atoi(rowstr); err == nil && row > 0 { + return col, row, nil + } } - sum += int(ch-'A'+1) * weight - weight *= 26 } - return sum - 1 + return "", -1, newInvalidCellNameError(cell) } -// letterOnlyMapF is used in conjunction with strings.Map to return only the -// characters A-Z and a-z in a string. -func letterOnlyMapF(rune rune) rune { - switch { - case 'A' <= rune && rune <= 'Z': - return rune - case 'a' <= rune && rune <= 'z': - return rune - 32 +// JoinCellName joins cell name from column name and row number +func JoinCellName(col string, row int) (string, error) { + normCol := strings.Map(func(rune rune) rune { + switch { + case 'A' <= rune && rune <= 'Z': + return rune + case 'a' <= rune && rune <= 'z': + return rune - 32 + } + return -1 + }, col) + if len(col) == 0 || len(col) != len(normCol) { + return "", newInvalidColumnNameError(col) } - return -1 + if row < 1 { + return "", newInvalidRowNumberError(row) + } + return fmt.Sprintf("%s%d", normCol, row), nil } -// intOnlyMapF is used in conjunction with strings.Map to return only the -// numeric portions of a string. -func intOnlyMapF(rune rune) rune { - if rune >= 48 && rune < 58 { - return rune +// ColumnNameToNumber provides a function to convert Excel sheet +// column name to int. Column name case insencitive +// Function returns error if column name incorrect. +// +// Example: +// +// excelize.ColumnNameToNumber("AK") // returns 37, nil +// +func ColumnNameToNumber(name string) (int, error) { + if len(name) == 0 { + return -1, newInvalidColumnNameError(name) } - return -1 + col := 0 + multi := 1 + for i := len(name) - 1; i >= 0; i-- { + r := name[i] + if r >= 'A' && r <= 'Z' { + col += int(r-'A'+1) * multi + } else if r >= 'a' && r <= 'z' { + col += int(r-'a'+1) * multi + } else { + return -1, newInvalidColumnNameError(name) + } + multi *= 26 + } + return col, nil +} + +// MustColumnNameToNumber provides a function to convert Excel sheet column +// name to int. Column name case insencitive. +// Function returns error if column name incorrect. +// +// Example: +// +// excelize.MustColumnNameToNumber("AK") // returns 37 +// +func MustColumnNameToNumber(name string) int { + n, err := ColumnNameToNumber(name) + if err != nil { + panic(err) + } + return n +} + +// ColumnNumberToName provides a function to convert integer +// to Excel sheet column title. +// +// Example: +// +// excelize.ToAlphaString(37) // returns "AK", nil +// +func ColumnNumberToName(num int) (string, error) { + if num < 1 { + return "", fmt.Errorf("incorrect column number %d", num) + } + var col string + for num > 0 { + col = string((num-1)%26+65) + col + num = (num - 1) / 26 + } + return col, nil +} + +// CellNameToCoordinates converts alpha-numeric cell name +// to [X, Y] coordinates or retrusn an error. +// +// Example: +// CellCoordinates("A1") // returns 1, 1, nil +// CellCoordinates("Z3") // returns 26, 3, nil +// +func CellNameToCoordinates(cell string) (int, int, error) { + const msg = "cannot convert cell %q to coordinates: %v" + + colname, row, err := SplitCellName(cell) + if err != nil { + return -1, -1, fmt.Errorf(msg, cell, err) + } + + col, err := ColumnNameToNumber(colname) + if err != nil { + return -1, -1, fmt.Errorf(msg, cell, err) + } + + return col, row, nil +} + +// MustCellNameToCoordinates converts alpha-numeric cell name +// to [X, Y] coordinates or panics. +// +// Example: +// MustCellNameToCoordinates("A1") // returns 1, 1 +// MustCellNameToCoordinates("Z3") // returns 26, 3 +// +func MustCellNameToCoordinates(cell string) (int, int) { + c, r, err := CellNameToCoordinates(cell) + if err != nil { + panic(err) + } + return c, r +} + +// CoordinatesToCellName converts [X, Y] coordinates to alpha-numeric cell name or returns an error. +// +// Example: +// CoordinatesToCellName(1, 1) // returns "A1", nil +// +func CoordinatesToCellName(col, row int) (string, error) { + if col < 1 || row < 1 { + return "", fmt.Errorf("invalid cell coordinates [%d, %d]", col, row) + } + colname, err := ColumnNumberToName(col) + if err != nil { + return "", fmt.Errorf("invalid cell coordinates [%d, %d]: %v", col, row, err) + } + return fmt.Sprintf("%s%d", colname, row), nil +} + +// MustCoordinatesToCellName converts [X, Y] coordinates to alpha-numeric cell name or panics. +// +// Example: +// MustCoordinatesToCellName(1, 1) // returns "A1" +// +func MustCoordinatesToCellName(col, row int) string { + n, err := CoordinatesToCellName(col, row) + if err != nil { + panic(err) + } + return n } // boolPtr returns a pointer to a bool with the given value. @@ -135,47 +248,6 @@ func defaultTrue(b *bool) bool { return *b } -// axisLowerOrEqualThan returns true if axis1 <= axis2 axis1/axis2 can be -// either a column or a row axis, e.g. "A", "AAE", "42", "1", etc. -// -// For instance, the following comparisons are all true: -// -// "A" <= "B" -// "A" <= "AA" -// "B" <= "AA" -// "BC" <= "ABCD" (in a XLSX sheet, the BC col comes before the ABCD col) -// "1" <= "2" -// "2" <= "11" (in a XLSX sheet, the row 2 comes before the row 11) -// and so on -func axisLowerOrEqualThan(axis1, axis2 string) bool { - if len(axis1) < len(axis2) { - return true - } else if len(axis1) > len(axis2) { - return false - } else { - return axis1 <= axis2 - } -} - -// getCellColRow returns the two parts of a cell identifier (its col and row) -// as strings -// -// For instance: -// -// "C220" => "C", "220" -// "aaef42" => "aaef", "42" -// "" => "", "" -func getCellColRow(cell string) (col, row string) { - for index, rune := range cell { - if unicode.IsDigit(rune) { - return cell[:index], cell[index:] - } - - } - - return cell, "" -} - // parseFormatSet provides a method to convert format string to []byte and // handle empty string. func parseFormatSet(formatSet string) []byte { @@ -208,7 +280,9 @@ func namespaceStrictToTransitional(content []byte) []byte { // is great, numerous passwords will match the same hash. Here is the // algorithm to create the hash value: // -// take the ASCII values of all characters shift left the first character 1 bit, the second 2 bits and so on (use only the lower 15 bits and rotate all higher bits, the highest bit of the 16-bit value is always 0 [signed short]) +// take the ASCII values of all characters shift left the first character 1 bit, +// the second 2 bits and so on (use only the lower 15 bits and rotate all higher bits, +// the highest bit of the 16-bit value is always 0 [signed short]) // XOR all these values // XOR the count of characters // XOR the constant 0xCE4B diff --git a/lib_test.go b/lib_test.go index ef0d8f5b..4c19f734 100644 --- a/lib_test.go +++ b/lib_test.go @@ -2,59 +2,213 @@ package excelize import ( "fmt" + "strconv" + "strings" "testing" "github.com/stretchr/testify/assert" ) -func TestAxisLowerOrEqualThanIsTrue(t *testing.T) { - trueExpectedInputList := [][2]string{ - {"A", "B"}, - {"A", "AA"}, - {"B", "AA"}, - {"BC", "ABCD"}, - {"1", "2"}, - {"2", "11"}, - } +var validColumns = []struct { + Name string + Num int +}{ + {Name: "A", Num: 1}, + {Name: "Z", Num: 26}, + {Name: "AA", Num: 26 + 1}, + {Name: "AK", Num: 26 + 11}, + {Name: "ak", Num: 26 + 11}, + {Name: "Ak", Num: 26 + 11}, + {Name: "aK", Num: 26 + 11}, + {Name: "AZ", Num: 26 + 26}, + {Name: "ZZ", Num: 26 + 26*26}, + {Name: "AAA", Num: 26 + 26*26 + 1}, + {Name: "ZZZ", Num: 26 + 26*26 + 26*26*26}, +} - for i, trueExpectedInput := range trueExpectedInputList { - t.Run(fmt.Sprintf("TestData%d", i), func(t *testing.T) { - assert.True(t, axisLowerOrEqualThan(trueExpectedInput[0], trueExpectedInput[1])) - }) +var invalidColumns = []struct { + Name string + Num int +}{ + {Name: "", Num: -1}, + {Name: " ", Num: -1}, + {Name: "_", Num: -1}, + {Name: "__", Num: -1}, + {Name: "-1", Num: -1}, + {Name: "0", Num: -1}, + {Name: " A", Num: -1}, + {Name: "A ", Num: -1}, + {Name: "A1", Num: -1}, + {Name: "1A", Num: -1}, + {Name: " a", Num: -1}, + {Name: "a ", Num: -1}, + {Name: "a1", Num: -1}, + {Name: "1a", Num: -1}, + {Name: " _", Num: -1}, + {Name: "_ ", Num: -1}, + {Name: "_1", Num: -1}, + {Name: "1_", Num: -1}, +} + +var invalidCells = []string{"", "A", "AA", " A", "A ", "1A", "A1A", "A1 ", " A1", "1A1", "a-1", "A-1"} + +var invalidIndexes = []int{-100, -2, -1, 0} + +func TestColumnNameToNumber_OK(t *testing.T) { + const msg = "Column %q" + for _, col := range validColumns { + out, err := ColumnNameToNumber(col.Name) + if assert.NoErrorf(t, err, msg, col.Name) { + assert.Equalf(t, col.Num, out, msg, col.Name) + } } } -func TestAxisLowerOrEqualThanIsFalse(t *testing.T) { - falseExpectedInputList := [][2]string{ - {"B", "A"}, - {"AA", "A"}, - {"AA", "B"}, - {"ABCD", "AB"}, - {"2", "1"}, - {"11", "2"}, - } - - for i, falseExpectedInput := range falseExpectedInputList { - t.Run(fmt.Sprintf("TestData%d", i), func(t *testing.T) { - assert.False(t, axisLowerOrEqualThan(falseExpectedInput[0], falseExpectedInput[1])) - }) +func TestColumnNameToNumber_Error(t *testing.T) { + const msg = "Column %q" + for _, col := range invalidColumns { + out, err := ColumnNameToNumber(col.Name) + if assert.Errorf(t, err, msg, col.Name) { + assert.Equalf(t, col.Num, out, msg, col.Name) + } + assert.Panicsf(t, func() { + MustColumnNameToNumber(col.Name) + }, msg, col.Name) } } -func TestGetCellColRow(t *testing.T) { - cellExpectedColRowList := [][3]string{ - {"C220", "C", "220"}, - {"aaef42", "aaef", "42"}, - {"bonjour", "bonjour", ""}, - {"59", "", "59"}, - {"", "", ""}, - } - - for i, test := range cellExpectedColRowList { - t.Run(fmt.Sprintf("TestData%d", i), func(t *testing.T) { - col, row := getCellColRow(test[0]) - assert.Equal(t, test[1], col, "Unexpected col") - assert.Equal(t, test[2], row, "Unexpected row") - }) +func TestColumnNumberToName_OK(t *testing.T) { + const msg = "Column %q" + for _, col := range validColumns { + out, err := ColumnNumberToName(col.Num) + if assert.NoErrorf(t, err, msg, col.Name) { + assert.Equalf(t, strings.ToUpper(col.Name), out, msg, col.Name) + } + } +} + +func TestColumnNumberToName_Error(t *testing.T) { + out, err := ColumnNumberToName(-1) + if assert.Error(t, err) { + assert.Equal(t, "", out) + } + + out, err = ColumnNumberToName(0) + if assert.Error(t, err) { + assert.Equal(t, "", out) + } +} + +func TestSplitCellName_OK(t *testing.T) { + const msg = "Cell \"%s%d\"" + for i, col := range validColumns { + row := i + 1 + c, r, err := SplitCellName(col.Name + strconv.Itoa(row)) + if assert.NoErrorf(t, err, msg, col.Name, row) { + assert.Equalf(t, col.Name, c, msg, col.Name, row) + assert.Equalf(t, row, r, msg, col.Name, row) + } + } +} + +func TestSplitCellName_Error(t *testing.T) { + const msg = "Cell %q" + for _, cell := range invalidCells { + c, r, err := SplitCellName(cell) + if assert.Errorf(t, err, msg, cell) { + assert.Equalf(t, "", c, msg, cell) + assert.Equalf(t, -1, r, msg, cell) + } + } +} + +func TestJoinCellName_OK(t *testing.T) { + const msg = "Cell \"%s%d\"" + + for i, col := range validColumns { + row := i + 1 + cell, err := JoinCellName(col.Name, row) + if assert.NoErrorf(t, err, msg, col.Name, row) { + assert.Equalf(t, strings.ToUpper(fmt.Sprintf("%s%d", col.Name, row)), cell, msg, row) + } + } +} + +func TestJoinCellName_Error(t *testing.T) { + const msg = "Cell \"%s%d\"" + + test := func(col string, row int) { + cell, err := JoinCellName(col, row) + if assert.Errorf(t, err, msg, col, row) { + assert.Equalf(t, "", cell, msg, col, row) + } + } + + for _, col := range invalidColumns { + test(col.Name, 1) + for _, row := range invalidIndexes { + test("A", row) + test(col.Name, row) + } + } + +} + +func TestCellNameToCoordinates_OK(t *testing.T) { + const msg = "Cell \"%s%d\"" + for i, col := range validColumns { + row := i + 1 + c, r, err := CellNameToCoordinates(col.Name + strconv.Itoa(row)) + if assert.NoErrorf(t, err, msg, col.Name, row) { + assert.Equalf(t, col.Num, c, msg, col.Name, row) + assert.Equalf(t, i+1, r, msg, col.Name, row) + } + } +} + +func TestCellNameToCoordinates_Error(t *testing.T) { + const msg = "Cell %q" + for _, cell := range invalidCells { + c, r, err := CellNameToCoordinates(cell) + if assert.Errorf(t, err, msg, cell) { + assert.Equalf(t, -1, c, msg, cell) + assert.Equalf(t, -1, r, msg, cell) + } + assert.Panicsf(t, func() { + MustCellNameToCoordinates(cell) + }, msg, cell) + } +} + +func TestCoordinatesToCellName_OK(t *testing.T) { + const msg = "Coordinates [%d, %d]" + for i, col := range validColumns { + row := i + 1 + cell, err := CoordinatesToCellName(col.Num, row) + if assert.NoErrorf(t, err, msg, col.Num, row) { + assert.Equalf(t, strings.ToUpper(col.Name+strconv.Itoa(row)), cell, msg, col.Num, row) + } + } +} + +func TestCoordinatesToCellName_Error(t *testing.T) { + const msg = "Coordinates [%d, %d]" + + test := func(col, row int) { + cell, err := CoordinatesToCellName(col, row) + if assert.Errorf(t, err, msg, col, row) { + assert.Equalf(t, "", cell, msg, col, row) + } + assert.Panicsf(t, func() { + MustCoordinatesToCellName(col, row) + }, msg, col, row) + } + + for _, col := range invalidIndexes { + test(col, 1) + for _, row := range invalidIndexes { + test(1, row) + test(col, row) + } } } diff --git a/picture.go b/picture.go index 131b15ce..f3463aaf 100644 --- a/picture.go +++ b/picture.go @@ -143,7 +143,7 @@ func (f *File) AddPictureFromBytes(sheet, cell, format, name, extension string, if err != nil { return err } - image, _, err := image.DecodeConfig(bytes.NewReader(file)) + img, _, err := image.DecodeConfig(bytes.NewReader(file)) if err != nil { return err } @@ -162,7 +162,7 @@ func (f *File) AddPictureFromBytes(sheet, cell, format, name, extension string, } drawingHyperlinkRID = f.addDrawingRelationships(drawingID, SourceRelationshipHyperLink, formatSet.Hyperlink, hyperlinkType) } - f.addDrawingPicture(sheet, drawingXML, cell, name, image.Width, image.Height, drawingRID, drawingHyperlinkRID, formatSet) + f.addDrawingPicture(sheet, drawingXML, cell, name, img.Width, img.Height, drawingRID, drawingHyperlinkRID, formatSet) f.addMedia(file, ext) f.addContentTypePart(drawingID, "drawings") return err @@ -263,14 +263,11 @@ func (f *File) countDrawings() int { // drawingXML, cell, file name, width, height relationship index and format // sets. func (f *File) addDrawingPicture(sheet, drawingXML, cell, file string, width, height, rID, hyperlinkRID int, formatSet *formatPicture) { - cell = strings.ToUpper(cell) - fromCol := string(strings.Map(letterOnlyMapF, cell)) - fromRow, _ := strconv.Atoi(strings.Map(intOnlyMapF, cell)) - row := fromRow - 1 - col := TitleToNumber(fromCol) + col, row := MustCellNameToCoordinates(cell) width = int(float64(width) * formatSet.XScale) height = int(float64(height) * formatSet.YScale) - colStart, rowStart, _, _, colEnd, rowEnd, x2, y2 := f.positionObjectPixels(sheet, col, row, formatSet.OffsetX, formatSet.OffsetY, width, height) + colStart, rowStart, _, _, colEnd, rowEnd, x2, y2 := + f.positionObjectPixels(sheet, col, row, formatSet.OffsetX, formatSet.OffsetY, width, height) content, cNvPrID := f.drawingParser(drawingXML) twoCellAnchor := xdrCellAnchor{} twoCellAnchor.EditAs = formatSet.Positioning @@ -471,30 +468,36 @@ func (f *File) getSheetRelationshipsTargetByID(sheet, rID string) string { // } // func (f *File) GetPicture(sheet, cell string) (string, []byte) { + col, row := MustCellNameToCoordinates(cell) + xlsx := f.workSheetReader(sheet) if xlsx.Drawing == nil { return "", []byte{} } + target := f.getSheetRelationshipsTargetByID(sheet, xlsx.Drawing.RID) drawingXML := strings.Replace(target, "..", "xl", -1) - cell = strings.ToUpper(cell) - fromCol := string(strings.Map(letterOnlyMapF, cell)) - fromRow, _ := strconv.Atoi(strings.Map(intOnlyMapF, cell)) - row := fromRow - 1 - col := TitleToNumber(fromCol) - drawingRelationships := strings.Replace(strings.Replace(target, "../drawings", "xl/drawings/_rels", -1), ".xml", ".xml.rels", -1) + + drawingRelationships := strings.Replace( + strings.Replace(target, "../drawings", "xl/drawings/_rels", -1), ".xml", ".xml.rels", -1) + wsDr, _ := f.drawingParser(drawingXML) + for _, anchor := range wsDr.TwoCellAnchor { if anchor.From != nil && anchor.Pic != nil { if anchor.From.Col == col && anchor.From.Row == row { - xlsxWorkbookRelation := f.getDrawingRelationships(drawingRelationships, anchor.Pic.BlipFill.Blip.Embed) + xlsxWorkbookRelation := f.getDrawingRelationships(drawingRelationships, + anchor.Pic.BlipFill.Blip.Embed) _, ok := supportImageTypes[filepath.Ext(xlsxWorkbookRelation.Target)] if ok { - return filepath.Base(xlsxWorkbookRelation.Target), []byte(f.XLSX[strings.Replace(xlsxWorkbookRelation.Target, "..", "xl", -1)]) + return filepath.Base(xlsxWorkbookRelation.Target), + []byte(f.XLSX[strings.Replace(xlsxWorkbookRelation.Target, + "..", "xl", -1)]) } } } } + _, ok := f.XLSX[drawingXML] if !ok { return "", nil diff --git a/rows.go b/rows.go index 9575876b..8b4f8eca 100644 --- a/rows.go +++ b/rows.go @@ -16,7 +16,6 @@ import ( "io" "math" "strconv" - "strings" ) // GetRows return all the rows in a sheet by given worksheet name (case @@ -30,24 +29,30 @@ import ( // } // func (f *File) GetRows(sheet string) [][]string { - xlsx := f.workSheetReader(sheet) name, ok := f.sheetMap[trimSheetName(sheet)] if !ok { - return [][]string{} + return nil } + + xlsx := f.workSheetReader(sheet) if xlsx != nil { output, _ := xml.Marshal(f.Sheet[name]) f.saveFileList(name, replaceWorkSheetsRelationshipsNameSpaceBytes(output)) } + xml.NewDecoder(bytes.NewReader(f.readXML(name))) d := f.sharedStringsReader() - var inElement string - var r xlsxRow - tr, tc := f.getTotalRowsCols(name) - rows := make([][]string, tr) + var ( + inElement string + rowData xlsxRow + ) + + rowCount, colCount := f.getTotalRowsCols(name) + rows := make([][]string, rowCount) for i := range rows { - rows[i] = make([]string, tc+1) + rows[i] = make([]string, colCount+1) } + var row int decoder := xml.NewDecoder(bytes.NewReader(f.readXML(name))) for { @@ -59,15 +64,15 @@ func (f *File) GetRows(sheet string) [][]string { case xml.StartElement: inElement = startElement.Name.Local if inElement == "row" { - r = xlsxRow{} - _ = decoder.DecodeElement(&r, &startElement) - cr := r.R - 1 - for _, colCell := range r.C { - c := TitleToNumber(strings.Map(letterOnlyMapF, colCell.R)) + rowData = xlsxRow{} + _ = decoder.DecodeElement(&rowData, &startElement) + cr := rowData.R - 1 + for _, colCell := range rowData.C { + col, _ := MustCellNameToCoordinates(colCell.R) val, _ := colCell.getValueFrom(f, d) - rows[cr][c] = val + rows[cr][col-1] = val if val != "" { - row = r.R + row = rowData.R } } } @@ -120,13 +125,13 @@ func (rows *Rows) Columns() []string { r := xlsxRow{} _ = rows.decoder.DecodeElement(&r, &startElement) d := rows.f.sharedStringsReader() - row := make([]string, len(r.C)) + columns := make([]string, len(r.C)) for _, colCell := range r.C { - c := TitleToNumber(strings.Map(letterOnlyMapF, colCell.R)) + col, _ := MustCellNameToCoordinates(colCell.R) val, _ := colCell.getValueFrom(rows.f, d) - row[c] = val + columns[col-1] = val } - return row + return columns } // ErrSheetNotExist defines an error of sheet is not exist @@ -184,7 +189,7 @@ func (f *File) getTotalRowsCols(name string) (int, int) { _ = decoder.DecodeElement(&r, &startElement) tr = r.R for _, colCell := range r.C { - col := TitleToNumber(strings.Map(letterOnlyMapF, colCell.R)) + col, _ := MustCellNameToCoordinates(colCell.R) if col > tc { tc = col } @@ -202,13 +207,15 @@ func (f *File) getTotalRowsCols(name string) (int, int) { // xlsx.SetRowHeight("Sheet1", 1, 50) // func (f *File) SetRowHeight(sheet string, row int, height float64) { - xlsx := f.workSheetReader(sheet) if row < 1 { - return + panic(newInvalidRowNumberError(row)) // Fail fats to avoid possible future side effects! } - cells := 0 + + xlsx := f.workSheetReader(sheet) + + prepareSheetXML(xlsx, 0, row) + rowIdx := row - 1 - completeRow(xlsx, row, cells) xlsx.SheetData.Row[rowIdx].Ht = height xlsx.SheetData.Row[rowIdx].CustomHeight = true } @@ -232,8 +239,12 @@ func (f *File) getRowHeight(sheet string, row int) int { // xlsx.GetRowHeight("Sheet1", 1) // func (f *File) GetRowHeight(sheet string, row int) float64 { + if row < 1 { + panic(newInvalidRowNumberError(row)) // Fail fats to avoid possible future side effects! + } + xlsx := f.workSheetReader(sheet) - if row < 1 || row > len(xlsx.SheetData.Row) { + if row > len(xlsx.SheetData.Row) { return defaultRowHeightPixels // it will be better to use 0, but we take care with BC } for _, v := range xlsx.SheetData.Row { @@ -291,18 +302,13 @@ func (xlsx *xlsxC) getValueFrom(f *File, d *xlsxSST) (string, error) { // xlsx.SetRowVisible("Sheet1", 2, false) // func (f *File) SetRowVisible(sheet string, row int, visible bool) { - xlsx := f.workSheetReader(sheet) if row < 1 { - return + panic(newInvalidRowNumberError(row)) // Fail fats to avoid possible future side effects! } - cells := 0 - completeRow(xlsx, row, cells) - rowIdx := row - 1 - if visible { - xlsx.SheetData.Row[rowIdx].Hidden = false - return - } - xlsx.SheetData.Row[rowIdx].Hidden = true + + xlsx := f.workSheetReader(sheet) + prepareSheetXML(xlsx, 0, row) + xlsx.SheetData.Row[row-1].Hidden = !visible } // GetRowVisible provides a function to get visible of a single row by given @@ -312,14 +318,15 @@ func (f *File) SetRowVisible(sheet string, row int, visible bool) { // xlsx.GetRowVisible("Sheet1", 2) // func (f *File) GetRowVisible(sheet string, row int) bool { + if row < 1 { + panic(newInvalidRowNumberError(row)) // Fail fats to avoid possible future side effects! + } + xlsx := f.workSheetReader(sheet) - if row < 1 || row > len(xlsx.SheetData.Row) { + if row > len(xlsx.SheetData.Row) { return false } - rowIndex := row - 1 - cells := 0 - completeRow(xlsx, row, cells) - return !xlsx.SheetData.Row[rowIndex].Hidden + return !xlsx.SheetData.Row[row-1].Hidden } // SetRowOutlineLevel provides a function to set outline level number of a @@ -329,12 +336,11 @@ func (f *File) GetRowVisible(sheet string, row int) bool { // xlsx.SetRowOutlineLevel("Sheet1", 2, 1) // func (f *File) SetRowOutlineLevel(sheet string, row int, level uint8) { - xlsx := f.workSheetReader(sheet) if row < 1 { - return + panic(newInvalidRowNumberError(row)) // Fail fats to avoid possible future side effects! } - cells := 0 - completeRow(xlsx, row, cells) + xlsx := f.workSheetReader(sheet) + prepareSheetXML(xlsx, 0, row) xlsx.SheetData.Row[row-1].OutlineLevel = level } @@ -345,8 +351,11 @@ func (f *File) SetRowOutlineLevel(sheet string, row int, level uint8) { // xlsx.GetRowOutlineLevel("Sheet1", 2) // func (f *File) GetRowOutlineLevel(sheet string, row int) uint8 { + if row < 1 { + panic(newInvalidRowNumberError(row)) // Fail fats to avoid possible future side effects! + } xlsx := f.workSheetReader(sheet) - if row < 1 || row > len(xlsx.SheetData.Row) { + if row > len(xlsx.SheetData.Row) { return 0 } return xlsx.SheetData.Row[row-1].OutlineLevel @@ -362,14 +371,18 @@ func (f *File) GetRowOutlineLevel(sheet string, row int) uint8 { // worksheet, it will cause a file error when you open it. The excelize only // partially updates these references currently. func (f *File) RemoveRow(sheet string, row int) { + if row < 1 { + panic(newInvalidRowNumberError(row)) // Fail fats to avoid possible future side effects! + } + xlsx := f.workSheetReader(sheet) - if row < 1 || row > len(xlsx.SheetData.Row) { + if row > len(xlsx.SheetData.Row) { return } for i, r := range xlsx.SheetData.Row { if r.R == row { xlsx.SheetData.Row = append(xlsx.SheetData.Row[:i], xlsx.SheetData.Row[i+1:]...) - f.adjustHelper(sheet, -1, row, -1) + f.adjustHelper(sheet, rows, row, -1) return } } @@ -383,9 +396,9 @@ func (f *File) RemoveRow(sheet string, row int) { // func (f *File) InsertRow(sheet string, row int) { if row < 1 { - return + panic(newInvalidRowNumberError(row)) // Fail fats to avoid possible future side effects! } - f.adjustHelper(sheet, -1, row, 1) + f.adjustHelper(sheet, rows, row, 1) } // DuplicateRow inserts a copy of specified row (by it Excel row number) below @@ -410,9 +423,12 @@ func (f *File) DuplicateRow(sheet string, row int) { // worksheet, it will cause a file error when you open it. The excelize only // partially updates these references currently. func (f *File) DuplicateRowTo(sheet string, row, row2 int) { - xlsx := f.workSheetReader(sheet) + if row < 1 { + panic(newInvalidRowNumberError(row)) // Fail fats to avoid possible future side effects! + } - if row < 1 || row > len(xlsx.SheetData.Row) || row2 < 1 || row == row2 { + xlsx := f.workSheetReader(sheet) + if row > len(xlsx.SheetData.Row) || row2 < 1 || row == row2 { return } @@ -430,7 +446,7 @@ func (f *File) DuplicateRowTo(sheet string, row, row2 int) { return } - f.adjustHelper(sheet, -1, row2, 1) + f.adjustHelper(sheet, rows, row2, 1) idx2 := -1 for i, r := range xlsx.SheetData.Row { @@ -478,62 +494,31 @@ func (f *File) DuplicateRowTo(sheet string, row, row2 int) { // Noteice: this method could be very slow for large spreadsheets (more than // 3000 rows one sheet). func checkRow(xlsx *xlsxWorksheet) { - buffer := bytes.Buffer{} - for k := range xlsx.SheetData.Row { - lenCol := len(xlsx.SheetData.Row[k].C) - if lenCol > 0 { - endR := string(strings.Map(letterOnlyMapF, xlsx.SheetData.Row[k].C[lenCol-1].R)) - endRow, _ := strconv.Atoi(strings.Map(intOnlyMapF, xlsx.SheetData.Row[k].C[lenCol-1].R)) - endCol := TitleToNumber(endR) + 1 - if lenCol < endCol { - oldRow := xlsx.SheetData.Row[k].C - xlsx.SheetData.Row[k].C = xlsx.SheetData.Row[k].C[:0] - var tmp []xlsxC - for i := 0; i < endCol; i++ { - buffer.WriteString(ToAlphaString(i)) - buffer.WriteString(strconv.Itoa(endRow)) - tmp = append(tmp, xlsxC{ - R: buffer.String(), - }) - buffer.Reset() - } - xlsx.SheetData.Row[k].C = tmp - for _, y := range oldRow { - colAxis := TitleToNumber(string(strings.Map(letterOnlyMapF, y.R))) - xlsx.SheetData.Row[k].C[colAxis] = y - } - } - } - } -} + for rowIdx := range xlsx.SheetData.Row { + rowData := &xlsx.SheetData.Row[rowIdx] -// completeRow provides a function to check and fill each column element for a -// single row and make that is continuous in a worksheet of XML by given row -// index and axis. -func completeRow(xlsx *xlsxWorksheet, row, cell int) { - currentRows := len(xlsx.SheetData.Row) - if currentRows > 1 { - lastRow := xlsx.SheetData.Row[currentRows-1].R - if lastRow >= row { - row = lastRow + colCount := len(rowData.C) + if colCount == 0 { + continue } - } - for i := currentRows; i < row; i++ { - xlsx.SheetData.Row = append(xlsx.SheetData.Row, xlsxRow{ - R: i + 1, - }) - } - buffer := bytes.Buffer{} - for ii := currentRows; ii < row; ii++ { - start := len(xlsx.SheetData.Row[ii].C) - if start == 0 { - for iii := start; iii < cell; iii++ { - buffer.WriteString(ToAlphaString(iii)) - buffer.WriteString(strconv.Itoa(ii + 1)) - xlsx.SheetData.Row[ii].C = append(xlsx.SheetData.Row[ii].C, xlsxC{ - R: buffer.String(), - }) - buffer.Reset() + lastCol, _ := MustCellNameToCoordinates(rowData.C[colCount-1].R) + + if colCount < lastCol { + oldList := rowData.C + newlist := make([]xlsxC, 0, lastCol) + + rowData.C = xlsx.SheetData.Row[rowIdx].C[:0] + + for colIdx := 0; colIdx < lastCol; colIdx++ { + newlist = append(newlist, xlsxC{R: MustCoordinatesToCellName(colIdx+1, rowIdx+1)}) + } + + rowData.C = newlist + + for colIdx := range oldList { + colData := &oldList[colIdx] + colNum, _ := MustCellNameToCoordinates(colData.R) + xlsx.SheetData.Row[rowIdx].C[colNum-1] = *colData } } } diff --git a/rows_test.go b/rows_test.go index b83d3776..50e26dd2 100644 --- a/rows_test.go +++ b/rows_test.go @@ -3,44 +3,38 @@ package excelize import ( "fmt" "path/filepath" - "strconv" "testing" "github.com/stretchr/testify/assert" ) func TestRows(t *testing.T) { + const sheet2 = "Sheet2" + xlsx, err := OpenFile(filepath.Join("test", "Book1.xlsx")) if !assert.NoError(t, err) { t.FailNow() } - rows, err := xlsx.Rows("Sheet2") + rows, err := xlsx.Rows(sheet2) if !assert.NoError(t, err) { t.FailNow() } - rowStrs := make([][]string, 0) - var i = 0 + collectedRows := make([][]string, 0) for rows.Next() { - i++ - columns := rows.Columns() - rowStrs = append(rowStrs, columns) + collectedRows = append(collectedRows, trimSliceSpace(rows.Columns())) } - if !assert.NoError(t, rows.Error()) { t.FailNow() } - dstRows := xlsx.GetRows("Sheet2") - if !assert.Equal(t, len(rowStrs), len(dstRows)) { - t.FailNow() + returnedRows := xlsx.GetRows(sheet2) + for i := range returnedRows { + returnedRows[i] = trimSliceSpace(returnedRows[i]) } - - for i := 0; i < len(rowStrs); i++ { - if !assert.Equal(t, trimSliceSpace(dstRows[i]), trimSliceSpace(rowStrs[i])) { - t.FailNow() - } + if !assert.Equal(t, collectedRows, returnedRows) { + t.FailNow() } r := Rows{} @@ -60,8 +54,13 @@ func TestRowHeight(t *testing.T) { xlsx := NewFile() sheet1 := xlsx.GetSheetName(1) - xlsx.SetRowHeight(sheet1, 0, defaultRowHeightPixels+1.0) // should no effect - assert.Equal(t, defaultRowHeightPixels, xlsx.GetRowHeight("Sheet1", 0)) + assert.Panics(t, func() { + xlsx.SetRowHeight(sheet1, 0, defaultRowHeightPixels+1.0) + }) + + assert.Panics(t, func() { + xlsx.GetRowHeight("Sheet1", 0) + }) xlsx.SetRowHeight(sheet1, 1, 111.0) assert.Equal(t, 111.0, xlsx.GetRowHeight(sheet1, 1)) @@ -77,32 +76,47 @@ func TestRowHeight(t *testing.T) { convertColWidthToPixels(0) } +func TestRowVisibility(t *testing.T) { + xlsx, err := prepareTestBook1() + if !assert.NoError(t, err) { + t.FailNow() + } + + xlsx.SetRowVisible("Sheet3", 2, false) + xlsx.SetRowVisible("Sheet3", 2, true) + xlsx.GetRowVisible("Sheet3", 2) + + assert.Panics(t, func() { + xlsx.SetRowVisible("Sheet3", 0, true) + }) + + assert.Panics(t, func() { + xlsx.GetRowVisible("Sheet3", 0) + }) + + assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestRowVisibility.xlsx"))) +} + func TestRemoveRow(t *testing.T) { xlsx := NewFile() sheet1 := xlsx.GetSheetName(1) r := xlsx.workSheetReader(sheet1) const ( - cellCount = 10 - rowCount = 10 + colCount = 10 + rowCount = 10 ) - for j := 1; j <= cellCount; j++ { - for i := 1; i <= rowCount; i++ { - axis := ToAlphaString(i) + strconv.Itoa(j) - xlsx.SetCellStr(sheet1, axis, axis) - } - } + fillCells(xlsx, sheet1, colCount, rowCount) + xlsx.SetCellHyperLink(sheet1, "A5", "https://github.com/360EntSecGroup-Skylar/excelize", "External") - xlsx.RemoveRow(sheet1, -1) - if !assert.Len(t, r.SheetData.Row, rowCount) { - t.FailNow() - } + assert.Panics(t, func() { + xlsx.RemoveRow(sheet1, -1) + }) - xlsx.RemoveRow(sheet1, 0) - if !assert.Len(t, r.SheetData.Row, rowCount) { - t.FailNow() - } + assert.Panics(t, func() { + xlsx.RemoveRow(sheet1, 0) + }) xlsx.RemoveRow(sheet1, 4) if !assert.Len(t, r.SheetData.Row, rowCount-1) { @@ -150,26 +164,20 @@ func TestInsertRow(t *testing.T) { r := xlsx.workSheetReader(sheet1) const ( - cellCount = 10 - rowCount = 10 + colCount = 10 + rowCount = 10 ) - for j := 1; j <= cellCount; j++ { - for i := 1; i < rowCount; i++ { - axis := ToAlphaString(i) + strconv.Itoa(j) - xlsx.SetCellStr(sheet1, axis, axis) - } - } + fillCells(xlsx, sheet1, colCount, rowCount) + xlsx.SetCellHyperLink(sheet1, "A5", "https://github.com/360EntSecGroup-Skylar/excelize", "External") - xlsx.InsertRow(sheet1, -1) - if !assert.Len(t, r.SheetData.Row, rowCount) { - t.FailNow() - } + assert.Panics(t, func() { + xlsx.InsertRow(sheet1, -1) + }) - xlsx.InsertRow(sheet1, 0) - if !assert.Len(t, r.SheetData.Row, rowCount) { - t.FailNow() - } + assert.Panics(t, func() { + xlsx.InsertRow(sheet1, 0) + }) xlsx.InsertRow(sheet1, 1) if !assert.Len(t, r.SheetData.Row, rowCount+1) { @@ -304,19 +312,24 @@ func TestDuplicateRow(t *testing.T) { t.Run("ZeroWithNoRows", func(t *testing.T) { xlsx := NewFile() - xlsx.DuplicateRow(sheet, 0) + assert.Panics(t, func() { + xlsx.DuplicateRow(sheet, 0) + }) if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.ZeroWithNoRows"))) { t.FailNow() } + assert.Equal(t, "", xlsx.GetCellValue(sheet, "A1")) assert.Equal(t, "", xlsx.GetCellValue(sheet, "B1")) assert.Equal(t, "", xlsx.GetCellValue(sheet, "A2")) assert.Equal(t, "", xlsx.GetCellValue(sheet, "B2")) + expect := map[string]string{ "A1": "", "B1": "", "A2": "", "B2": "", } + for cell, val := range expect { if !assert.Equal(t, val, xlsx.GetCellValue(sheet, cell), cell) { t.FailNow() @@ -444,31 +457,19 @@ func TestDuplicateRowInvalidRownum(t *testing.T) { "B3": "B3 Value", } - testRows := []int{-2, -1} + invalidIndexes := []int{-100, -2, -1, 0} - testRowPairs := []struct { - row1 int - row2 int - }{ - {-1, -1}, - {-1, 0}, - {-1, 1}, - {0, -1}, - {0, 0}, - {0, 1}, - {1, -1}, - {1, 1}, - {1, 0}, - } - - for i, row := range testRows { - name := fmt.Sprintf("TestRow_%d", i+1) + for _, row := range invalidIndexes { + name := fmt.Sprintf("%d", row) t.Run(name, func(t *testing.T) { xlsx := NewFile() for col, val := range cells { xlsx.SetCellStr(sheet, col, val) } - xlsx.DuplicateRow(sheet, row) + + assert.Panics(t, func() { + xlsx.DuplicateRow(sheet, row) + }) for col, val := range cells { if !assert.Equal(t, val, xlsx.GetCellValue(sheet, col)) { @@ -479,22 +480,27 @@ func TestDuplicateRowInvalidRownum(t *testing.T) { }) } - for i, pair := range testRowPairs { - name := fmt.Sprintf("TestRowPair_%d", i+1) - t.Run(name, func(t *testing.T) { - xlsx := NewFile() - for col, val := range cells { - xlsx.SetCellStr(sheet, col, val) - } - xlsx.DuplicateRowTo(sheet, pair.row1, pair.row2) - - for col, val := range cells { - if !assert.Equal(t, val, xlsx.GetCellValue(sheet, col)) { - t.FailNow() + for _, row1 := range invalidIndexes { + for _, row2 := range invalidIndexes { + name := fmt.Sprintf("[%d,%d]", row1, row2) + t.Run(name, func(t *testing.T) { + xlsx := NewFile() + for col, val := range cells { + xlsx.SetCellStr(sheet, col, val) } - } - assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, name))) - }) + + assert.Panics(t, func() { + xlsx.DuplicateRowTo(sheet, row1, row2) + }) + + for col, val := range cells { + if !assert.Equal(t, val, xlsx.GetCellValue(sheet, col)) { + t.FailNow() + } + } + assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, name))) + }) + } } } diff --git a/shape.go b/shape.go index 3cf09d8a..c58038c7 100644 --- a/shape.go +++ b/shape.go @@ -283,15 +283,37 @@ func (f *File) AddShape(sheet, cell, format string) error { // addDrawingShape provides a function to add preset geometry by given sheet, // drawingXMLand format sets. func (f *File) addDrawingShape(sheet, drawingXML, cell string, formatSet *formatShape) { - textUnderlineType := map[string]bool{"none": true, "words": true, "sng": true, "dbl": true, "heavy": true, "dotted": true, "dottedHeavy": true, "dash": true, "dashHeavy": true, "dashLong": true, "dashLongHeavy": true, "dotDash": true, "dotDashHeavy": true, "dotDotDash": true, "dotDotDashHeavy": true, "wavy": true, "wavyHeavy": true, "wavyDbl": true} - cell = strings.ToUpper(cell) - fromCol := string(strings.Map(letterOnlyMapF, cell)) - fromRow, _ := strconv.Atoi(strings.Map(intOnlyMapF, cell)) - row := fromRow - 1 - col := TitleToNumber(fromCol) + fromCol, fromRow := MustCellNameToCoordinates(cell) + colIdx := fromCol - 1 + rowIdx := fromRow - 1 + + textUnderlineType := map[string]bool{ + "none": true, + "words": true, + "sng": true, + "dbl": true, + "heavy": true, + "dotted": true, + "dottedHeavy": true, + "dash": true, + "dashHeavy": true, + "dashLong": true, + "dashLongHeavy": true, + "dotDash": true, + "dotDashHeavy": true, + "dotDotDash": true, + "dotDotDashHeavy": true, + "wavy": true, + "wavyHeavy": true, + "wavyDbl": true, + } + width := int(float64(formatSet.Width) * formatSet.Format.XScale) height := int(float64(formatSet.Height) * formatSet.Format.YScale) - colStart, rowStart, _, _, colEnd, rowEnd, x2, y2 := f.positionObjectPixels(sheet, col, row, formatSet.Format.OffsetX, formatSet.Format.OffsetY, width, height) + + colStart, rowStart, _, _, colEnd, rowEnd, x2, y2 := + f.positionObjectPixels(sheet, colIdx, rowIdx, formatSet.Format.OffsetX, formatSet.Format.OffsetY, + width, height) content, cNvPrID := f.drawingParser(drawingXML) twoCellAnchor := xdrCellAnchor{} twoCellAnchor.EditAs = formatSet.Format.Positioning diff --git a/sheet.go b/sheet.go index 26d4b4cb..ee962773 100644 --- a/sheet.go +++ b/sheet.go @@ -14,7 +14,6 @@ import ( "encoding/json" "encoding/xml" "errors" - "fmt" "io/ioutil" "os" "path" @@ -100,16 +99,16 @@ func (f *File) workBookWriter() { // workSheetWriter provides a function to save xl/worksheets/sheet%d.xml after // serialize structure. func (f *File) workSheetWriter() { - for path, sheet := range f.Sheet { + for p, sheet := range f.Sheet { if sheet != nil { for k, v := range sheet.SheetData.Row { - f.Sheet[path].SheetData.Row[k].C = trimCell(v.C) + f.Sheet[p].SheetData.Row[k].C = trimCell(v.C) } output, _ := xml.Marshal(sheet) - f.saveFileList(path, replaceWorkSheetsRelationshipsNameSpaceBytes(output)) - ok := f.checked[path] + f.saveFileList(p, replaceWorkSheetsRelationshipsNameSpaceBytes(output)) + ok := f.checked[p] if ok { - f.checked[path] = false + f.checked[p] = false } } } @@ -679,7 +678,9 @@ func (f *File) SearchSheet(sheet, value string, reg ...bool) []string { regSearch = r } xlsx := f.workSheetReader(sheet) - result := []string{} + var ( + result []string + ) name, ok := f.sheetMap[trimSheetName(sheet)] if !ok { return result @@ -716,7 +717,9 @@ func (f *File) SearchSheet(sheet, value string, reg ...bool) []string { continue } } - result = append(result, fmt.Sprintf("%s%d", strings.Map(letterOnlyMapF, colCell.R), r.R)) + + cellCol, _ := MustCellNameToCoordinates(colCell.R) + result = append(result, MustCoordinatesToCellName(cellCol, r.R)) } } default: @@ -775,7 +778,7 @@ func (f *File) UnprotectSheet(sheet string) { // trimSheetName provides a function to trim invaild characters by given worksheet // name. func trimSheetName(name string) string { - r := []rune{} + var r []rune for _, v := range name { switch v { case 58, 92, 47, 63, 42, 91, 93: // replace :\/?*[] @@ -852,7 +855,7 @@ func (p *PageLayoutPaperSize) getPageLayout(ps *xlsxPageSetUp) { // // Available options: // PageLayoutOrientation(string) -// PageLayoutPaperSize(int) +// PageLayoutPaperSize(int) // // The following shows the paper size sorted by excelize index number: // @@ -1021,10 +1024,31 @@ func (f *File) workSheetRelsReader(path string) *xlsxWorkbookRels { // workSheetRelsWriter provides a function to save // xl/worksheets/_rels/sheet%d.xml.rels after serialize structure. func (f *File) workSheetRelsWriter() { - for path, r := range f.WorkSheetRels { + for p, r := range f.WorkSheetRels { if r != nil { v, _ := xml.Marshal(r) - f.saveFileList(path, v) + f.saveFileList(p, v) + } + } +} + +// fillSheetData fill missing row and cell XML data to made it continous from first cell [1, 1] to last cell [col, row] +func prepareSheetXML(xlsx *xlsxWorksheet, col int, row int) { + rowCount := len(xlsx.SheetData.Row) + if rowCount < row { + // append missing rows + for rowIdx := rowCount; rowIdx < row; rowIdx++ { + xlsx.SheetData.Row = append(xlsx.SheetData.Row, xlsxRow{R: rowIdx + 1}) + } + } + for rowIdx := range xlsx.SheetData.Row { + rowData := &xlsx.SheetData.Row[rowIdx] // take reference + cellCount := len(rowData.C) + if cellCount < col { + for colIdx := cellCount; colIdx < col; colIdx++ { + cellName, _ := CoordinatesToCellName(colIdx+1, rowIdx+1) + rowData.C = append(rowData.C, xlsxC{R: cellName}) + } } } } diff --git a/styles.go b/styles.go index 7ffc8ff1..50b30b86 100644 --- a/styles.go +++ b/styles.go @@ -2263,6 +2263,14 @@ func setCellXfs(style *xlsxStyleSheet, fontID, numFmtID, fillID, borderID int, a return style.CellXfs.Count - 1 } +// GetCellStyle provides a function to get cell style index by given worksheet +// name and cell coordinates. +func (f *File) GetCellStyle(sheet, axis string) int { + xlsx := f.workSheetReader(sheet) + cellData, col, _ := f.prepareCell(xlsx, sheet, axis) + return f.prepareCellStyle(xlsx, col, cellData.S) +} + // SetCellStyle provides a function to add style attribute for cells by given // worksheet name, coordinate area and style ID. Note that diagonalDown and // diagonalUp type border should be use same color in the same coordinate @@ -2329,42 +2337,36 @@ func setCellXfs(style *xlsxStyleSheet, fontID, numFmtID, fillID, borderID int, a // xlsx.SetCellStyle("Sheet1", "H9", "H9", style) // func (f *File) SetCellStyle(sheet, hcell, vcell string, styleID int) { - hcell = strings.ToUpper(hcell) - vcell = strings.ToUpper(vcell) - - // Coordinate conversion, convert C1:B3 to 2,0,1,2. - hcol := string(strings.Map(letterOnlyMapF, hcell)) - hrow, err := strconv.Atoi(strings.Map(intOnlyMapF, hcell)) + hcol, hrow, err := CellNameToCoordinates(hcell) if err != nil { - return + panic(err) } - hyAxis := hrow - 1 - hxAxis := TitleToNumber(hcol) - vcol := string(strings.Map(letterOnlyMapF, vcell)) - vrow, err := strconv.Atoi(strings.Map(intOnlyMapF, vcell)) + vcol, vrow, err := CellNameToCoordinates(vcell) if err != nil { - return - } - vyAxis := vrow - 1 - vxAxis := TitleToNumber(vcol) - - // Correct the coordinate area, such correct C1:B3 to B1:C3. - if vxAxis < hxAxis { - vxAxis, hxAxis = hxAxis, vxAxis + panic(err) } - if vyAxis < hyAxis { - vyAxis, hyAxis = hyAxis, vyAxis + // Normalize the coordinate area, such correct C1:B3 to B1:C3. + if vcol < hcol { + vcol, hcol = hcol, vcol } + if vrow < hrow { + vrow, hrow = hrow, vrow + } + + hcolIdx := hcol - 1 + hrowIdx := hrow - 1 + + vcolIdx := vcol - 1 + vrowIdx := vrow - 1 + xlsx := f.workSheetReader(sheet) + prepareSheetXML(xlsx, vcol, vrow) - completeRow(xlsx, vyAxis+1, vxAxis+1) - completeCol(xlsx, vyAxis+1, vxAxis+1) - - for r := hyAxis; r <= vyAxis; r++ { - for k := hxAxis; k <= vxAxis; k++ { + for r := hrowIdx; r <= vrowIdx; r++ { + for k := hcolIdx; k <= vcolIdx; k++ { xlsx.SheetData.Row[r].C[k].S = styleID } } diff --git a/table.go b/table.go index 7c7e0619..e33264bb 100644 --- a/table.go +++ b/table.go @@ -55,31 +55,25 @@ func (f *File) AddTable(sheet, hcell, vcell, format string) error { if err != nil { return err } - hcell = strings.ToUpper(hcell) - vcell = strings.ToUpper(vcell) // Coordinate conversion, convert C1:B3 to 2,0,1,2. - hcol := string(strings.Map(letterOnlyMapF, hcell)) - hrow, _ := strconv.Atoi(strings.Map(intOnlyMapF, hcell)) - hyAxis := hrow - 1 - hxAxis := TitleToNumber(hcol) + hcol, hrow := MustCellNameToCoordinates(hcell) + vcol, vrow := MustCellNameToCoordinates(vcell) - vcol := string(strings.Map(letterOnlyMapF, vcell)) - vrow, _ := strconv.Atoi(strings.Map(intOnlyMapF, vcell)) - vyAxis := vrow - 1 - vxAxis := TitleToNumber(vcol) - if vxAxis < hxAxis { - vxAxis, hxAxis = hxAxis, vxAxis + if vcol < hcol { + vcol, hcol = hcol, vcol } - if vyAxis < hyAxis { - vyAxis, hyAxis = hyAxis, vyAxis + + if vrow < hrow { + vrow, hrow = hrow, vrow } + tableID := f.countTables() + 1 sheetRelationshipsTableXML := "../tables/table" + strconv.Itoa(tableID) + ".xml" tableXML := strings.Replace(sheetRelationshipsTableXML, "..", "xl", -1) // Add first table for given sheet. rID := f.addSheetRelationships(sheet, SourceRelationshipTable, sheetRelationshipsTableXML, "") f.addSheetTable(sheet, rID) - f.addTable(sheet, tableXML, hxAxis, hyAxis, vxAxis, vyAxis, tableID, formatSet) + f.addTable(sheet, tableXML, hcol, hrow, vcol, vrow, tableID, formatSet) f.addContentTypePart(tableID, "table") return err } @@ -112,18 +106,23 @@ func (f *File) addSheetTable(sheet string, rID int) { // addTable provides a function to add table by given worksheet name, // coordinate area and format set. -func (f *File) addTable(sheet, tableXML string, hxAxis, hyAxis, vxAxis, vyAxis, i int, formatSet *formatTable) { +func (f *File) addTable(sheet, tableXML string, hcol, hrow, vcol, vrow, i int, formatSet *formatTable) { // Correct the minimum number of rows, the table at least two lines. - if hyAxis == vyAxis { - vyAxis++ + if hrow == vrow { + vrow++ } + // Correct table reference coordinate area, such correct C1:B3 to B1:C3. - ref := ToAlphaString(hxAxis) + strconv.Itoa(hyAxis+1) + ":" + ToAlphaString(vxAxis) + strconv.Itoa(vyAxis+1) - tableColumn := []*xlsxTableColumn{} + ref := MustCoordinatesToCellName(hcol, hrow) + ":" + MustCoordinatesToCellName(vcol, vrow) + + var ( + tableColumn []*xlsxTableColumn + ) + idx := 0 - for i := hxAxis; i <= vxAxis; i++ { + for i := hcol; i <= vcol; i++ { idx++ - cell := ToAlphaString(i) + strconv.Itoa(hyAxis+1) + cell := MustCoordinatesToCellName(i, hrow) name := f.GetCellValue(sheet, cell) if _, err := strconv.Atoi(name); err == nil { f.SetCellStr(sheet, cell, name) @@ -245,37 +244,26 @@ func parseAutoFilterSet(formatSet string) (*formatAutoFilter, error) { // Price < 2000 // func (f *File) AutoFilter(sheet, hcell, vcell, format string) error { + hcol, hrow := MustCellNameToCoordinates(hcell) + vcol, vrow := MustCellNameToCoordinates(vcell) + + if vcol < hcol { + vcol, hcol = hcol, vcol + } + + if vrow < hrow { + vrow, hrow = hrow, vrow + } + formatSet, _ := parseAutoFilterSet(format) - - hcell = strings.ToUpper(hcell) - vcell = strings.ToUpper(vcell) - - // Coordinate conversion, convert C1:B3 to 2,0,1,2. - hcol := string(strings.Map(letterOnlyMapF, hcell)) - hrow, _ := strconv.Atoi(strings.Map(intOnlyMapF, hcell)) - hyAxis := hrow - 1 - hxAxis := TitleToNumber(hcol) - - vcol := string(strings.Map(letterOnlyMapF, vcell)) - vrow, _ := strconv.Atoi(strings.Map(intOnlyMapF, vcell)) - vyAxis := vrow - 1 - vxAxis := TitleToNumber(vcol) - - if vxAxis < hxAxis { - vxAxis, hxAxis = hxAxis, vxAxis - } - - if vyAxis < hyAxis { - vyAxis, hyAxis = hyAxis, vyAxis - } - ref := ToAlphaString(hxAxis) + strconv.Itoa(hyAxis+1) + ":" + ToAlphaString(vxAxis) + strconv.Itoa(vyAxis+1) - refRange := vxAxis - hxAxis - return f.autoFilter(sheet, ref, refRange, hxAxis, formatSet) + ref := MustCoordinatesToCellName(hcol, hrow) + ":" + MustCoordinatesToCellName(vcol, vrow) + refRange := vcol - hcol + return f.autoFilter(sheet, ref, refRange, hcol, formatSet) } // autoFilter provides a function to extract the tokens from the filter // expression. The tokens are mainly non-whitespace groups. -func (f *File) autoFilter(sheet, ref string, refRange, hxAxis int, formatSet *formatAutoFilter) error { +func (f *File) autoFilter(sheet, ref string, refRange, col int, formatSet *formatAutoFilter) error { xlsx := f.workSheetReader(sheet) if xlsx.SheetPr != nil { xlsx.SheetPr.FilterMode = true @@ -288,11 +276,13 @@ func (f *File) autoFilter(sheet, ref string, refRange, hxAxis int, formatSet *fo if formatSet.Column == "" || formatSet.Expression == "" { return nil } - col := TitleToNumber(formatSet.Column) - offset := col - hxAxis + + fsCol := MustColumnNameToNumber(formatSet.Column) + offset := fsCol - col if offset < 0 || offset > refRange { return fmt.Errorf("incorrect index of column '%s'", formatSet.Column) } + filter.FilterColumn = &xlsxFilterColumn{ ColID: offset, } @@ -315,7 +305,7 @@ func (f *File) autoFilter(sheet, ref string, refRange, hxAxis int, formatSet *fo func (f *File) writeAutoFilter(filter *xlsxAutoFilter, exp []int, tokens []string) { if len(exp) == 1 && exp[0] == 2 { // Single equality. - filters := []*xlsxFilter{} + var filters []*xlsxFilter filters = append(filters, &xlsxFilter{Val: tokens[0]}) filter.FilterColumn.Filters = &xlsxFilters{Filter: filters} } else if len(exp) == 3 && exp[0] == 2 && exp[1] == 1 && exp[2] == 2 {