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
formula
Veniamin Albaev 6 years ago committed by xuri
parent 092f16c744
commit dc01264562

1
.gitignore vendored

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

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

@ -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) {
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))
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)
f.SetCellStr(sheet, axis, fmt.Sprintf("%v", 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))
}
// 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 coordinates and cell value.
// 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
}
}
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
}
}
}
}
return ""
}
cellData, col, _ := f.prepareCell(xlsx, sheet, axis)
// 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
// 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",
}
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
}
if c.F.T == STCellFormulaTypeShared {
return getSharedForumula(x, c.F.Si), true
}
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
}
}
}
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},
}
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{}
// Check for correct cell name
if _, _, err := SplitCellName(axis); err != nil {
panic(err) // Fail fast to avoid possible future side effects
}
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
var linkData xlsxHyperlink
switch linkType {
case "External":
linkData = xlsxHyperlink{
Ref: axis,
}
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)
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())
}
}
// 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
}
return style
prepareSheetXML(xlsx, col, row)
return &xlsx.SheetData.Row[row-1].C[col-1], col, row
}
// 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) {
// 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)
if len(value) > 32767 {
value = value[0:32767]
}
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 ""
}
// 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",
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
}
}
}
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 ""
}
// 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
// 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
}
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
styleSheet := f.stylesReader()
ok := builtInNumFmtFunc[styleSheet.CellXfs.Xf[s].NumFmtID]
if ok != nil {
return ok(styleSheet.CellXfs.Xf[s].NumFmtID, v)
}
return v
}
// 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
// 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
}
// 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
}
return style
}
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++
// 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 ""
}

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

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

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

166
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

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

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

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

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

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

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

@ -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.
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.
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.
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")
// 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.
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)
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")
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.
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",
},
}
}}
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.
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.
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) {
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) {
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", "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", 2, 250)
assert.Panics(t, func() {
xlsx.SetRowOutlineLevel("Sheet1", 0, 1)
xlsx.GetRowOutlineLevel("Sheet1", 2)
})
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)
}
}
}

228
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,48 +64,32 @@ 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 ""
}
var ans string
i := value + 1
for i > 0 {
ans = string((i-1)%26+65) + ans
i = (i - 1) / 26
// 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')
}
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 {
// 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
@ -113,67 +97,155 @@ func letterOnlyMapF(rune rune) rune {
return rune - 32
}
return -1
}, col)
if len(col) == 0 || len(col) != len(normCol) {
return "", newInvalidColumnNameError(col)
}
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
}
// boolPtr returns a pointer to a bool with the given value.
func boolPtr(b bool) *bool { return &b }
// 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
}
// defaultTrue returns true if b is nil, or the pointed value.
func defaultTrue(b *bool) bool {
if b == nil {
return true
// 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)
}
return *b
var col string
for num > 0 {
col = string((num-1)%26+65) + col
num = (num - 1) / 26
}
return col, nil
}
// axisLowerOrEqualThan returns true if axis1 <= axis2 axis1/axis2 can be
// either a column or a row axis, e.g. "A", "AAE", "42", "1", etc.
// CellNameToCoordinates converts alpha-numeric cell name
// to [X, Y] coordinates or retrusn an error.
//
// For instance, the following comparisons are all true:
// Example:
// CellCoordinates("A1") // returns 1, 1, nil
// CellCoordinates("Z3") // returns 26, 3, nil
//
// "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
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
}
// getCellColRow returns the two parts of a cell identifier (its col and row)
// as strings
// MustCellNameToCoordinates converts alpha-numeric cell name
// to [X, Y] coordinates or panics.
//
// For instance:
// Example:
// MustCellNameToCoordinates("A1") // returns 1, 1
// MustCellNameToCoordinates("Z3") // returns 26, 3
//
// "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:]
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
}
return cell, ""
// 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.
func boolPtr(b bool) *bool { return &b }
// defaultTrue returns true if b is nil, or the pointed value.
func defaultTrue(b *bool) bool {
if b == nil {
return true
}
return *b
}
// parseFormatSet provides a method to convert format string to []byte and
@ -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

@ -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},
}
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 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 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)
}
}
}
for i, trueExpectedInput := range trueExpectedInputList {
t.Run(fmt.Sprintf("TestData%d", i), func(t *testing.T) {
assert.True(t, axisLowerOrEqualThan(trueExpectedInput[0], trueExpectedInput[1]))
})
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 TestAxisLowerOrEqualThanIsFalse(t *testing.T) {
falseExpectedInputList := [][2]string{
{"B", "A"},
{"AA", "A"},
{"AA", "B"},
{"ABCD", "AB"},
{"2", "1"},
{"11", "2"},
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 i, falseExpectedInput := range falseExpectedInputList {
t.Run(fmt.Sprintf("TestData%d", i), func(t *testing.T) {
assert.False(t, axisLowerOrEqualThan(falseExpectedInput[0], falseExpectedInput[1]))
})
for _, col := range invalidColumns {
test(col.Name, 1)
for _, row := range invalidIndexes {
test("A", row)
test(col.Name, row)
}
}
}
func TestGetCellColRow(t *testing.T) {
cellExpectedColRowList := [][3]string{
{"C220", "C", "220"},
{"aaef42", "aaef", "42"},
{"bonjour", "bonjour", ""},
{"59", "", "59"},
{"", "", ""},
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)
}
}
}
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 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)
}
}
}

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

@ -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
}
cells := 0
completeRow(xlsx, row, cells)
rowIdx := row - 1
if visible {
xlsx.SheetData.Row[rowIdx].Hidden = false
return
panic(newInvalidRowNumberError(row)) // Fail fats to avoid possible future side effects!
}
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]
colCount := len(rowData.C)
if colCount == 0 {
continue
}
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)})
}
}
// 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
}
}
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()
rowData.C = newlist
for colIdx := range oldList {
colData := &oldList[colIdx]
colNum, _ := MustCellNameToCoordinates(colData.R)
xlsx.SheetData.Row[rowIdx].C[colNum-1] = *colData
}
}
}

@ -3,45 +3,39 @@ 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])) {
if !assert.Equal(t, collectedRows, returnedRows) {
t.FailNow()
}
}
r := Rows{}
r.Columns()
@ -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
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")
assert.Panics(t, func() {
xlsx.RemoveRow(sheet1, -1)
if !assert.Len(t, r.SheetData.Row, rowCount) {
t.FailNow()
}
})
assert.Panics(t, func() {
xlsx.RemoveRow(sheet1, 0)
if !assert.Len(t, r.SheetData.Row, rowCount) {
t.FailNow()
}
})
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
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")
assert.Panics(t, func() {
xlsx.InsertRow(sheet1, -1)
if !assert.Len(t, r.SheetData.Row, rowCount) {
t.FailNow()
}
})
assert.Panics(t, func() {
xlsx.InsertRow(sheet1, 0)
if !assert.Len(t, r.SheetData.Row, rowCount) {
t.FailNow()
}
})
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()
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}
testRowPairs := []struct {
row1 int
row2 int
}{
{-1, -1},
{-1, 0},
{-1, 1},
{0, -1},
{0, 0},
{0, 1},
{1, -1},
{1, 1},
{1, 0},
}
invalidIndexes := []int{-100, -2, -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)
}
assert.Panics(t, func() {
xlsx.DuplicateRow(sheet, row)
})
for col, val := range cells {
if !assert.Equal(t, val, xlsx.GetCellValue(sheet, col)) {
@ -479,14 +480,18 @@ func TestDuplicateRowInvalidRownum(t *testing.T) {
})
}
for i, pair := range testRowPairs {
name := fmt.Sprintf("TestRowPair_%d", i+1)
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)
}
xlsx.DuplicateRowTo(sheet, pair.row1, pair.row2)
assert.Panics(t, func() {
xlsx.DuplicateRowTo(sheet, row1, row2)
})
for col, val := range cells {
if !assert.Equal(t, val, xlsx.GetCellValue(sheet, col)) {
@ -496,6 +501,7 @@ func TestDuplicateRowInvalidRownum(t *testing.T) {
assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, name)))
})
}
}
}
func trimSliceSpace(s []string) []string {

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

@ -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 :\/?*[]
@ -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})
}
}
}
}

@ -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
panic(err)
}
vyAxis := vrow - 1
vxAxis := TitleToNumber(vcol)
// Correct the coordinate area, such correct C1:B3 to B1:C3.
if vxAxis < hxAxis {
vxAxis, hxAxis = hxAxis, vxAxis
// Normalize the coordinate area, such correct C1:B3 to B1:C3.
if vcol < hcol {
vcol, hcol = hcol, vcol
}
if vyAxis < hyAxis {
vyAxis, hyAxis = hyAxis, vyAxis
if vrow < hrow {
vrow, hrow = hrow, vrow
}
xlsx := f.workSheetReader(sheet)
hcolIdx := hcol - 1
hrowIdx := hrow - 1
vcolIdx := vcol - 1
vrowIdx := vrow - 1
completeRow(xlsx, vyAxis+1, vxAxis+1)
completeCol(xlsx, vyAxis+1, vxAxis+1)
xlsx := f.workSheetReader(sheet)
prepareSheetXML(xlsx, vcol, vrow)
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
}
}

@ -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 {
formatSet, _ := parseAutoFilterSet(format)
hcol, hrow := MustCellNameToCoordinates(hcell)
vcol, vrow := MustCellNameToCoordinates(vcell)
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 vcol < hcol {
vcol, hcol = hcol, vcol
}
if vyAxis < hyAxis {
vyAxis, hyAxis = hyAxis, vyAxis
if vrow < hrow {
vrow, hrow = hrow, vrow
}
ref := ToAlphaString(hxAxis) + strconv.Itoa(hyAxis+1) + ":" + ToAlphaString(vxAxis) + strconv.Itoa(vyAxis+1)
refRange := vxAxis - hxAxis
return f.autoFilter(sheet, ref, refRange, hxAxis, formatSet)
formatSet, _ := parseAutoFilterSet(format)
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 {

Loading…
Cancel
Save