Browse Source

Huge refactorig for consistent col/row numbering (#356)

* Huge refactorig for consistent col/row numbering

Started from simply changing ToALphaString()/TitleToNumber() logic and related fixes.
But have to go deeper, do fixes, after do related fixes and again and again.

Major improvements:

1. Tests made stronger again (But still be weak).
2. "Empty" returns for incorrect input replaces with panic.
3. Check for correct col/row/cell naming & addressing by default.
4. Removed huge amount of duplicated code.
5. Removed ToALphaString(), TitleToNumber() and it helpers functions at all,
and replaced with SplitCellName(), JoinCellName(), ColumnNameToNumber(), ColumnNumberToName(), CellNameToCoordinates(), CoordinatesToCellName().
6. Minor fixes for internal variable naming for code readability (ex. col, row for input params, colIdx, rowIdx for slice indexes etc).

* Formatting fixes
tags/v2.0.0
Veniamin Albaev xuri 8 months ago
parent
commit
dc01264562
23 changed files with 1647 additions and 1334 deletions
  1. +1
    -0
      .gitignore
  2. +229
    -0
      adjust.go
  3. +302
    -404
      cell.go
  4. +6
    -3
      cell_test.go
  5. +6
    -6
      chart.go
  6. +38
    -10
      chart_test.go
  7. +78
    -88
      col.go
  8. +2
    -3
      comment.go
  9. +41
    -19
      date.go
  10. +36
    -7
      date_test.go
  11. +17
    -0
      errors.go
  12. +21
    -0
      errors_test.go
  13. +5
    -224
      excelize.go
  14. +191
    -156
      excelize_test.go
  15. +154
    -80
      lib.go
  16. +191
    -37
      lib_test.go
  17. +19
    -16
      picture.go
  18. +91
    -106
      rows.go
  19. +90
    -84
      rows_test.go
  20. +29
    -7
      shape.go
  21. +36
    -12
      sheet.go
  22. +26
    -24
      styles.go
  23. +38
    -48
      table.go

+ 1
- 0
.gitignore View File

@@ -1 +1,2 @@
~$*.xlsx
test/Test*.xlsx

+ 229
- 0
adjust.go View File

@@ -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
}
}

+ 302
- 404
cell.go View File

@@ -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,256 +69,169 @@ 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) {
case float32:
f.SetCellDefault(sheet, axis, strconv.FormatFloat(float64(value.(float32)), 'f', -1, 32))
case float64:
f.SetCellDefault(sheet, axis, strconv.FormatFloat(float64(value.(float64)), 'f', -1, 64))
case string:
f.SetCellStr(sheet, axis, t)
case []byte:
f.SetCellStr(sheet, axis, string(t))
case time.Duration:
f.SetCellDefault(sheet, axis, strconv.FormatFloat(float64(value.(time.Duration).Seconds()/86400), '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)
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) {
switch v := value.(type) {
case int:
f.SetCellInt(sheet, axis, value.(int))
f.SetCellInt(sheet, axis, v)
case int8:
f.SetCellInt(sheet, axis, int(value.(int8)))
f.SetCellInt(sheet, axis, int(v))
case int16:
f.SetCellInt(sheet, axis, int(value.(int16)))
f.SetCellInt(sheet, axis, int(v))
case int32:
f.SetCellInt(sheet, axis, int(value.(int32)))
f.SetCellInt(sheet, axis, int(v))
case int64:
f.SetCellInt(sheet, axis, int(value.(int64)))
f.SetCellInt(sheet, axis, int(v))
case uint:
f.SetCellInt(sheet, axis, int(value.(uint)))
f.SetCellInt(sheet, axis, int(v))
case uint8:
f.SetCellInt(sheet, axis, int(value.(uint8)))
f.SetCellInt(sheet, axis, int(v))
case uint16:
f.SetCellInt(sheet, axis, int(value.(uint16)))
f.SetCellInt(sheet, axis, int(v))
case uint32:
f.SetCellInt(sheet, axis, int(value.(uint32)))
f.SetCellInt(sheet, axis, int(v))
case uint64:
f.SetCellInt(sheet, axis, int(value.(uint64)))
f.SetCellInt(sheet, axis, int(v))
case float32:
f.SetCellDefault(sheet, axis, strconv.FormatFloat(float64(v), 'f', -1, 32))
case float64:
f.SetCellDefault(sheet, axis, strconv.FormatFloat(v, 'f', -1, 64))
case string:
f.SetCellStr(sheet, axis, v)
case []byte:
f.SetCellStr(sheet, axis, string(v))
case time.Duration:
f.SetCellDefault(sheet, axis, strconv.FormatFloat(v.Seconds()/86400.0, 'f', -1, 32))
f.setDefaultTimeStyle(sheet, axis, 21)
case time.Time:
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, "")
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) SetCellBool(sheet, axis string, value bool) {
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)
cellData, col, _ := f.prepareCell(xlsx, sheet, axis)
cellData.S = f.prepareCellStyle(xlsx, col, cellData.S)
cellData.T = ""
cellData.V = strconv.Itoa(value)
}

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"
// 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)
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
}
}
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 {
val, _ := xlsx.SheetData.Row[k].C[i].getValueFrom(f, f.sharedStringsReader())
return val
}
}
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",
}
}
return ""
}

// formattedValue provides a function to returns a value after formatted. 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) formattedValue(s int, v string) string {
if s == 0 {
return v
}
styleSheet := f.stylesReader()
ok := builtInNumFmtFunc[styleSheet.CellXfs.Xf[s].NumFmtID]
if ok != nil {
return ok(styleSheet.CellXfs.Xf[s].NumFmtID, v)
}
return v
cellData.S = f.prepareCellStyle(xlsx, col, cellData.S)
cellData.T = "str"
cellData.V = value
}

// GetCellStyle provides a function to get cell style index by given worksheet
// name and cell coordinates.
func (f *File) GetCellStyle(sheet, axis string) int {
// 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 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)
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 {
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
}
}
return f.getCellStringFunc(sheet, axis, func(x *xlsxWorksheet, c *xlsxC) (string, bool) {
if c.F == nil {
return "", false
}
}
return ""
}

// getSharedForumula find a cell contains the same formula as another cell,
// the "shared" value can be used for the t attribute and the si attribute can
// be used to refer to the cell containing the formula. Two formulas are
// considered to be the same when their respective representations in
// R1C1-reference notation, are the same.
//
// 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
}
if c.F.T == STCellFormulaTypeShared {
return getSharedForumula(x, c.F.Si), true
}
}
return ""
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)
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)
cellData, _, _ := f.prepareCell(xlsx, sheet, axis)

if formula == "" {
xlsx.SheetData.Row[xAxis].C[yAxis].F = nil
cellData.F = nil
f.deleteCalcChain(axis)
return
}
if xlsx.SheetData.Row[xAxis].C[yAxis].F != nil {
xlsx.SheetData.Row[xAxis].C[yAxis].F.Content = formula

if cellData.F != nil {
cellData.F.Content = formula
} else {
f := xlsxF{
Content: formula,
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
}
}
xlsx.SheetData.Row[xAxis].C[yAxis].F = &f
}
return false, ""
}

// SetCellHyperLink provides a function to set cell hyperlink by given
@@ -335,53 +249,36 @@ func (f *File) SetCellFormula(sheet, axis, formula string) {
// 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},
// Check for correct cell name
if _, _, err := SplitCellName(axis); err != nil {
panic(err) // Fail fast to avoid possible future side effects
}
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)
}

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)
}
return link, target
xlsx.Hyperlinks.Hyperlink = append(xlsx.Hyperlinks.Hyperlink, linkData)
}

// MergeCell provides a function to merge cells by given coordinate area and
@@ -392,213 +289,214 @@ func (f *File) GetCellHyperLink(sheet, axis string) (bool, string) {
// 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
hcol, hrow, err := CellNameToCoordinates(hcell)
if err != nil {
panic(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)
vcol, vrow, err := CellNameToCoordinates(vcell)
if err != nil {
panic(err)
}

vcol := string(strings.Map(letterOnlyMapF, vcell))
vrow, _ := strconv.Atoi(strings.Map(intOnlyMapF, vcell))
vyAxis := vrow - 1
vxAxis := TitleToNumber(vcol)
if hcol == vcol && hrow == vrow {
return
}

if vxAxis < hxAxis {
hcell, vcell = vcell, hcell
vxAxis, hxAxis = hxAxis, vxAxis
if vcol < hcol {
hcol, vcol = vcol, hcol
}

if vyAxis < hyAxis {
hcell, vcell = vcell, hcell
vyAxis, hyAxis = hyAxis, vyAxis
if vrow < hrow {
hrow, vrow = vrow, hrow
}

hcell, _ = CoordinatesToCellName(hcol, hrow)
vcell, _ = CoordinatesToCellName(vcol, vrow)

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)
ref := hcell + ":" + vcell
cells := make([]*xlsxMergeCell, 0, len(xlsx.MergeCells.Cells))
// 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:]...)
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)
}
}
xlsx.MergeCells.Cells = append(xlsx.MergeCells.Cells, &mergeCell)
cells = append(xlsx.MergeCells.Cells, &xlsxMergeCell{Ref: ref})
xlsx.MergeCells.Cells = cells
} 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
xlsx.MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: hcell + ":" + vcell}}}
}
}

// 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))
// 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 {
return
panic(err) // Fail fast to avoid future side effects!
}
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)
}
// 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()

// 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
}
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())
}
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))
// 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 {
return
panic(err) // Fail fast and prevent future side effects
}
xAxis := row - 1
yAxis := TitleToNumber(col)

rows := xAxis + 1
cell := yAxis + 1

completeRow(xlsx, rows, cell)
completeCol(xlsx, rows, cell)
prepareSheetXML(xlsx, col, row)

// 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
return &xlsx.SheetData.Row[row-1].C[col-1], col, row
}

// 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) {
// 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)
col := string(strings.Map(letterOnlyMapF, axis))
row, err := strconv.Atoi(strings.Map(intOnlyMapF, axis))

_, row, err := CellNameToCoordinates(axis)
if err != nil {
return
panic(err) // Fail fast to avoid future side effects!
}
xAxis := row - 1
yAxis := TitleToNumber(col)

rows := xAxis + 1
cell := yAxis + 1
lastRowNum := 0
if l := len(xlsx.SheetData.Row); l > 0 {
lastRowNum = xlsx.SheetData.Row[l-1].R
}

completeRow(xlsx, rows, cell)
completeCol(xlsx, rows, cell)
// keep in mind: row starts from 1
if row > lastRowNum {
return ""
}

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
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
}
}
}
return ""
}

// 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
// formattedValue provides a function to returns a value after formatted. 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) formattedValue(s int, v string) string {
if s == 0 {
return v
}
v = v.Elem()
if v.Kind() != reflect.Slice {
return
styleSheet := f.stylesReader()
ok := builtInNumFmtFunc[styleSheet.CellXfs.Xf[s].NumFmtID]
if ok != nil {
return ok(styleSheet.CellXfs.Xf[s].NumFmtID, v)
}
return v
}

xAxis := row - 1
yAxis := TitleToNumber(col)

rows := xAxis + 1
cell := yAxis + 1

completeRow(xlsx, rows, cell)
completeCol(xlsx, rows, cell)
// 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 style
}

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++
// 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 {
cell = strings.ToUpper(cell)
area = strings.ToUpper(area)
col, row, err := CellNameToCoordinates(cell)
if err != nil {
panic(err)
}

ref := strings.Split(area, ":")
if len(ref) < 2 {
rng := strings.Split(area, ":")
if len(rng) != 2 {
return false
}

from := ref[0]
to := ref[1]
firstCol, firtsRow, _ := CellNameToCoordinates(rng[0])
lastCol, lastRow, _ := CellNameToCoordinates(rng[1])

col, row := getCellColRow(cell)
fromCol, fromRow := getCellColRow(from)
toCol, toRow := getCellColRow(to)
return col >= firstCol && col <= lastCol && row >= firtsRow && row <= lastRow
}

return axisLowerOrEqualThan(fromCol, col) && axisLowerOrEqualThan(col, toCol) && axisLowerOrEqualThan(fromRow, row) && axisLowerOrEqualThan(row, toRow)
// getSharedForumula find a cell contains the same formula as another cell,
// the "shared" value can be used for the t attribute and the si attribute can
// be used to refer to the cell containing the formula. Two formulas are
// considered to be the same when their respective representations in
// R1C1-reference notation, are the same.
//
// 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 _, 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 ""
}

+ 6
- 3
cell_test.go View File

@@ -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")
})
}

+ 6
- 6
chart.go View File

@@ -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


+ 38
- 10
chart_test.go View File

@@ -9,19 +9,46 @@ import (
)

func TestChartSize(t *testing.T) {
xlsx := NewFile()
sheet1 := xlsx.GetSheetName(1)

var buffer bytes.Buffer
categories := map[string]string{
"A2": "Small",
"A3": "Normal",
"A4": "Large",
"B1": "Apple",
"C1": "Orange",
"D1": "Pear",
}
for cell, v := range categories {
xlsx.SetCellValue(sheet1, cell, v)
}

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)
values := map[string]int{
"B2": 2,
"C2": 3,
"D2": 3,
"B3": 5,
"C3": 2,
"D3": 4,
"B4": 6,
"C4": 7,
"D4": 8,
}
for k, v := range values {
xlsx.SetCellValue("Sheet1", k, v)
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"}}`)

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("<decodeTwoCellAnchor>"+workdir.TwoCellAnchor[0].Content+"</decodeTwoCellAnchor>"), &anchor)
err = xml.Unmarshal([]byte("<decodeTwoCellAnchor>"+
workdir.TwoCellAnchor[0].Content+"</decodeTwoCellAnchor>"), &anchor)
if !assert.NoError(t, err) {
t.FailNow()
}


+ 78
- 88
col.go View File

@@ -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


+ 2
- 3
comment.go View File

@@ -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{


+ 41
- 19
date.go View File

@@ -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")
}

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
}
// 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
return result
}

// shiftJulianToNoon provides a function to process julian date to noon.


+ 36
- 7
date_test.go View File

@@ -13,17 +13,40 @@ type dateTest struct {
GoValue time.Time
}

var trueExpectedDateList = []dateTest{
{0.0000000000000000, time.Date(1899, time.December, 30, 0, 0, 0, 0, time.UTC)},
{25569.000000000000, time.Unix(0, 0).UTC()},

// 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) {
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)},
for i, test := range trueExpectedDateList {
t.Run(fmt.Sprintf("TestData%d", i+1), func(t *testing.T) {
assert.Equalf(t, test.ExcelValue, timeToExcelTime(test.GoValue),
"Time: %s", test.GoValue.String())
})
}
}

for i, test := range trueExpectedInputList {
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.Equal(t, test.ExcelValue, timeToExcelTime(test.GoValue))
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)
}

+ 17
- 0
errors.go View File

@@ -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)
}

+ 21
- 0
errors_test.go View File

@@ -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 \"\"")
}

+ 5
- 224
excelize.go View File

@@ -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) {
// GetMergeCells provides a function to get all merged cells from a worksheet currently.
func (f *File) GetMergeCells(sheet string) []MergeCell {
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
}
}
}
}
var mergeCells []MergeCell

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]
mergeCells = make([]MergeCell, 0, len(xlsx.MergeCells.Cells))

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)
if xlsx.MergeCells != nil {
for i := 0; i < len(xlsx.MergeCells.Cells); i++ {
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)})


+ 191
- 156
excelize_test.go View File

@@ -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")