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 {