diff --git a/README.md b/README.md index 14a0e97b..b761eb66 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ # Excelize -[![Build Status](https://travis-ci.org/xuri/excelize.svg?branch=master)](https://travis-ci.org/xuri/excelize) -[![Code Coverage](https://codecov.io/gh/xuri/excelize/branch/master/graph/badge.svg)](https://codecov.io/gh/xuri/excelize) +[![Build Status](https://travis-ci.org/360EntSecGroup-Skylar/excelize.svg?branch=master)](https://travis-ci.org/360EntSecGroup-Skylar/excelize) +[![Code Coverage](https://codecov.io/gh/360EntSecGroup-Skylar/excelize/branch/master/graph/badge.svg)](https://codecov.io/gh/360EntSecGroup-Skylar/excelize) [![Go Report Card](https://goreportcard.com/badge/github.com/xuri/excelize)](https://goreportcard.com/report/github.com/xuri/excelize) [![GoDoc](https://godoc.org/github.com/xuri/excelize?status.svg)](https://godoc.org/github.com/xuri/excelize) [![Licenses](https://img.shields.io/badge/license-bsd-orange.svg)](https://opensource.org/licenses/BSD-3-Clause) diff --git a/cell.go b/cell.go index 34595bcf..f81f36c5 100644 --- a/cell.go +++ b/cell.go @@ -1,8 +1,11 @@ package excelize import ( + "encoding/xml" + "fmt" "strconv" "strings" + "time" ) // mergeCellsParser provides function to check merged cells in worksheet by @@ -19,6 +22,53 @@ func (f *File) mergeCellsParser(xlsx *xlsxWorksheet, axis string) string { return axis } +// SetCellValue provides function to set value of a cell. The following shows +// the supported data types: +// +// int +// int8 +// int16 +// int32 +// int64 +// float32 +// float64 +// string +// []byte +// time.Time +// nil +// +// Note that default date format is m/d/yy h:mm of time.Time type value. You can +// set numbers format by SetCellStyle() method. +func (f *File) SetCellValue(sheet, axis string, value interface{}) { + switch t := value.(type) { + case 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 float32: + f.SetCellDefault(sheet, axis, strconv.FormatFloat(float64(value.(float32)), 'f', -1, 32)) + case float64: + f.SetCellDefault(sheet, axis, strconv.FormatFloat(float64(value.(float64)), 'f', -1, 64)) + case string: + f.SetCellStr(sheet, axis, t) + case []byte: + f.SetCellStr(sheet, axis, string(t)) + case time.Time: + f.SetCellDefault(sheet, axis, strconv.FormatFloat(float64(timeToExcelTime(timeToUTCTime(value.(time.Time)))), 'f', -1, 32)) + f.setDefaultTimeStyle(sheet, axis) + case nil: + f.SetCellStr(sheet, axis, "") + default: + f.SetCellStr(sheet, axis, fmt.Sprintf("%v", value)) + } +} + // GetCellValue provides function to get formatted value from cell by given // sheet index 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 @@ -77,6 +127,25 @@ func (f *File) formattedValue(s int, v string) string { return v } +// GetCellStyle provides function to get cell style index by given worksheet +// name and cell coordinates. +func (f *File) GetCellStyle(sheet, axis string) int { + xlsx := f.workSheetReader(sheet) + axis = f.mergeCellsParser(xlsx, axis) + col := string(strings.Map(letterOnlyMapF, axis)) + row, _ := strconv.Atoi(strings.Map(intOnlyMapF, axis)) + xAxis := row - 1 + yAxis := TitleToNumber(col) + + rows := xAxis + 1 + cell := yAxis + 1 + + completeRow(xlsx, rows, cell) + completeCol(xlsx, rows, cell) + + return f.prepareCellStyle(xlsx, cell, xlsx.SheetData.Row[xAxis].C[yAxis].S) +} + // GetCellFormula provides function to get formula from cell by given sheet // index and axis in XLSX file. func (f *File) GetCellFormula(sheet, axis string) string { @@ -215,6 +284,94 @@ func (f *File) MergeCell(sheet, hcell, vcell string) { } } +// SetCellInt provides 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, _ := strconv.Atoi(strings.Map(intOnlyMapF, axis)) + xAxis := row - 1 + yAxis := TitleToNumber(col) + + rows := xAxis + 1 + cell := yAxis + 1 + + completeRow(xlsx, rows, cell) + completeCol(xlsx, rows, cell) + + xlsx.SheetData.Row[xAxis].C[yAxis].S = f.prepareCellStyle(xlsx, cell, xlsx.SheetData.Row[xAxis].C[yAxis].S) + xlsx.SheetData.Row[xAxis].C[yAxis].T = "" + xlsx.SheetData.Row[xAxis].C[yAxis].V = strconv.Itoa(value) +} + +// prepareCellStyle provides function to prepare style index of cell in +// worksheet by given column index and style index. +func (f *File) prepareCellStyle(xlsx *xlsxWorksheet, col, style int) int { + if xlsx.Cols != nil && style == 0 { + for _, v := range xlsx.Cols.Col { + if v.Min <= col && col <= v.Max { + style = v.Style + } + } + } + return style +} + +// SetCellStr provides function to set string type value of a cell. Total number +// of characters that a cell can contain 32767 characters. +func (f *File) SetCellStr(sheet, axis, value string) { + xlsx := f.workSheetReader(sheet) + axis = f.mergeCellsParser(xlsx, axis) + if len(value) > 32767 { + value = value[0:32767] + } + col := string(strings.Map(letterOnlyMapF, axis)) + row, _ := strconv.Atoi(strings.Map(intOnlyMapF, axis)) + xAxis := row - 1 + yAxis := TitleToNumber(col) + + rows := xAxis + 1 + cell := yAxis + 1 + + completeRow(xlsx, rows, cell) + completeCol(xlsx, rows, cell) + + // Leading space(s) character detection. + if len(value) > 0 { + if value[0] == 32 { + xlsx.SheetData.Row[xAxis].C[yAxis].XMLSpace = xml.Attr{ + Name: xml.Name{Space: NameSpaceXML, Local: "space"}, + Value: "preserve", + } + } + } + xlsx.SheetData.Row[xAxis].C[yAxis].S = f.prepareCellStyle(xlsx, cell, xlsx.SheetData.Row[xAxis].C[yAxis].S) + xlsx.SheetData.Row[xAxis].C[yAxis].T = "str" + xlsx.SheetData.Row[xAxis].C[yAxis].V = value +} + +// SetCellDefault provides 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, _ := strconv.Atoi(strings.Map(intOnlyMapF, axis)) + 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 +} + // checkCellInArea provides function to determine if a given coordinate is // within an area. func checkCellInArea(cell, area string) bool { diff --git a/col.go b/col.go index 7934ef38..00d71357 100644 --- a/col.go +++ b/col.go @@ -1,7 +1,9 @@ package excelize import ( + "bytes" "math" + "strconv" "strings" ) @@ -243,6 +245,61 @@ func (f *File) GetColWidth(sheet, column string) float64 { return defaultColWidthPixels } +// InsertCol provides function to insert a new column before given column index. +// For example, create a new column before column C in Sheet1: +// +// xlsx.InsertCol("Sheet1", "C") +// +func (f *File) InsertCol(sheet, column string) { + col := TitleToNumber(strings.ToUpper(column)) + f.adjustHelper(sheet, col, -1, 1) +} + +// RemoveCol provides function to remove single column by given worksheet index +// and column index. For example, remove column C in Sheet1: +// +// xlsx.RemoveCol("Sheet1", "C") +// +func (f *File) RemoveCol(sheet, column string) { + xlsx := f.workSheetReader(sheet) + for i, r := range xlsx.SheetData.Row { + for k, v := range r.C { + axis := v.R + col := string(strings.Map(letterOnlyMapF, axis)) + if col == column { + xlsx.SheetData.Row[i].C = append(xlsx.SheetData.Row[i].C[:k], xlsx.SheetData.Row[i].C[k+1:]...) + } + } + } + col := TitleToNumber(strings.ToUpper(column)) + f.adjustHelper(sheet, col, -1, -1) +} + +// Completion column element tags of XML in a sheet. +func completeCol(xlsx *xlsxWorksheet, row, cell int) { + if len(xlsx.SheetData.Row) < cell { + for i := len(xlsx.SheetData.Row); i < cell; i++ { + xlsx.SheetData.Row = append(xlsx.SheetData.Row, xlsxRow{ + R: i + 1, + }) + } + } + buffer := bytes.Buffer{} + for k, v := range xlsx.SheetData.Row { + if len(v.C) < cell { + start := len(v.C) + for iii := start; iii < cell; iii++ { + buffer.WriteString(ToAlphaString(iii)) + buffer.WriteString(strconv.Itoa(k + 1)) + xlsx.SheetData.Row[k].C = append(xlsx.SheetData.Row[k].C, xlsxC{ + R: buffer.String(), + }) + buffer.Reset() + } + } + } +} + // convertColWidthToPixels provieds function to convert the width of a cell from // user's units to pixels. Excel rounds the column width to the nearest pixel. // If the width hasn't been set by the user we use the default value. If the @@ -261,15 +318,3 @@ func convertColWidthToPixels(width float64) float64 { pixels = (width*maxDigitWidth + 0.5) + padding return math.Ceil(pixels) } - -// convertRowHeightToPixels provides function to convert the height of a cell -// from user's units to pixels. If the height hasn't been set by the user we use -// the default value. If the row is hidden it has a value of zero. -func convertRowHeightToPixels(height float64) float64 { - var pixels float64 - if height == 0 { - return pixels - } - pixels = math.Ceil(4.0 / 3.0 * height) - return pixels -} diff --git a/excelize.go b/excelize.go index 7314ead9..681759e8 100644 --- a/excelize.go +++ b/excelize.go @@ -4,13 +4,11 @@ import ( "archive/zip" "bytes" "encoding/xml" - "fmt" "io" "io/ioutil" "os" "strconv" "strings" - "time" ) // File define a populated XLSX file struct. @@ -67,72 +65,6 @@ func OpenReader(r io.Reader) (*File, error) { }, nil } -// SetCellValue provides function to set value of a cell. The following shows -// the supported data types: -// -// int -// int8 -// int16 -// int32 -// int64 -// float32 -// float64 -// string -// []byte -// time.Time -// nil -// -// Note that default date format is m/d/yy h:mm of time.Time type value. You can -// set numbers format by SetCellStyle() method. -func (f *File) SetCellValue(sheet, axis string, value interface{}) { - switch t := value.(type) { - case 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 float32: - f.SetCellDefault(sheet, axis, strconv.FormatFloat(float64(value.(float32)), 'f', -1, 32)) - case float64: - f.SetCellDefault(sheet, axis, strconv.FormatFloat(float64(value.(float64)), 'f', -1, 64)) - case string: - f.SetCellStr(sheet, axis, t) - case []byte: - f.SetCellStr(sheet, axis, string(t)) - case time.Time: - f.SetCellDefault(sheet, axis, strconv.FormatFloat(float64(timeToExcelTime(timeToUTCTime(value.(time.Time)))), 'f', -1, 32)) - f.setDefaultTimeStyle(sheet, axis) - case nil: - f.SetCellStr(sheet, axis, "") - default: - f.SetCellStr(sheet, axis, fmt.Sprintf("%v", value)) - } -} - -// GetCellStyle provides function to get cell style index by given worksheet -// name and cell coordinates. -func (f *File) GetCellStyle(sheet, axis string) int { - xlsx := f.workSheetReader(sheet) - axis = f.mergeCellsParser(xlsx, axis) - col := string(strings.Map(letterOnlyMapF, axis)) - row, _ := strconv.Atoi(strings.Map(intOnlyMapF, axis)) - 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) -} - // setDefaultTimeStyle provides function to set default numbers format for // time.Time type cell value by given worksheet name and cell coordinates. func (f *File) setDefaultTimeStyle(sheet, axis string) { @@ -163,151 +95,6 @@ func (f *File) workSheetReader(sheet string) *xlsxWorksheet { return f.Sheet[name] } -// SetCellInt provides 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, _ := strconv.Atoi(strings.Map(intOnlyMapF, axis)) - xAxis := row - 1 - yAxis := TitleToNumber(col) - - rows := xAxis + 1 - cell := yAxis + 1 - - completeRow(xlsx, rows, cell) - completeCol(xlsx, rows, cell) - - xlsx.SheetData.Row[xAxis].C[yAxis].S = f.prepareCellStyle(xlsx, cell, xlsx.SheetData.Row[xAxis].C[yAxis].S) - xlsx.SheetData.Row[xAxis].C[yAxis].T = "" - xlsx.SheetData.Row[xAxis].C[yAxis].V = strconv.Itoa(value) -} - -// prepareCellStyle provides function to prepare style index of cell in -// worksheet by given column index and style index. -func (f *File) prepareCellStyle(xlsx *xlsxWorksheet, col, style int) int { - if xlsx.Cols != nil && style == 0 { - for _, v := range xlsx.Cols.Col { - if v.Min <= col && col <= v.Max { - style = v.Style - } - } - } - return style -} - -// SetCellStr provides function to set string type value of a cell. Total number -// of characters that a cell can contain 32767 characters. -func (f *File) SetCellStr(sheet, axis, value string) { - xlsx := f.workSheetReader(sheet) - axis = f.mergeCellsParser(xlsx, axis) - if len(value) > 32767 { - value = value[0:32767] - } - col := string(strings.Map(letterOnlyMapF, axis)) - row, _ := strconv.Atoi(strings.Map(intOnlyMapF, axis)) - xAxis := row - 1 - yAxis := TitleToNumber(col) - - rows := xAxis + 1 - cell := yAxis + 1 - - completeRow(xlsx, rows, cell) - completeCol(xlsx, rows, cell) - - // Leading space(s) character detection. - if len(value) > 0 { - if value[0] == 32 { - xlsx.SheetData.Row[xAxis].C[yAxis].XMLSpace = xml.Attr{ - Name: xml.Name{Space: NameSpaceXML, Local: "space"}, - Value: "preserve", - } - } - } - xlsx.SheetData.Row[xAxis].C[yAxis].S = f.prepareCellStyle(xlsx, cell, xlsx.SheetData.Row[xAxis].C[yAxis].S) - xlsx.SheetData.Row[xAxis].C[yAxis].T = "str" - xlsx.SheetData.Row[xAxis].C[yAxis].V = value -} - -// SetCellDefault provides 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, _ := strconv.Atoi(strings.Map(intOnlyMapF, axis)) - 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 -} - -// Completion column element tags of XML in a sheet. -func completeCol(xlsx *xlsxWorksheet, row, cell int) { - if len(xlsx.SheetData.Row) < cell { - for i := len(xlsx.SheetData.Row); i < cell; i++ { - xlsx.SheetData.Row = append(xlsx.SheetData.Row, xlsxRow{ - R: i + 1, - }) - } - } - buffer := bytes.Buffer{} - for k, v := range xlsx.SheetData.Row { - if len(v.C) < cell { - start := len(v.C) - for iii := start; iii < cell; iii++ { - buffer.WriteString(ToAlphaString(iii)) - buffer.WriteString(strconv.Itoa(k + 1)) - xlsx.SheetData.Row[k].C = append(xlsx.SheetData.Row[k].C, xlsxC{ - R: buffer.String(), - }) - buffer.Reset() - } - } - } -} - -// completeRow provides 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() - } - } - } -} - // checkSheet provides function to fill each row element and make that is // continuous in a worksheet of XML. func checkSheet(xlsx *xlsxWorksheet) { @@ -346,61 +133,6 @@ func replaceWorkSheetsRelationshipsNameSpace(workbookMarshal string) string { return workbookMarshal } -// checkRow provides function to check and fill each column element for all rows -// and make that is continuous in a worksheet of XML. For example: -// -// -// -// -// -// -// -// -// in this case, we should to change it to -// -// -// -// -// -// -// -// -// -// -// -// 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, v := range xlsx.SheetData.Row { - lenCol := len(v.C) - if lenCol < 1 { - continue - } - endR := string(strings.Map(letterOnlyMapF, v.C[lenCol-1].R)) - endRow, _ := strconv.Atoi(strings.Map(intOnlyMapF, v.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] - 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 - } - } - } -} - // UpdateLinkedValue fix linked values within a spreadsheet are not updating in // Office Excel 2007 and 2010. This function will be remove value tag when met a // cell have a linked value. Reference @@ -439,3 +171,224 @@ func (f *File) UpdateLinkedValue() { } } } + +// adjustHelper provides function to adjust rows and columns dimensions, +// hyperlinks, merged cells and auto filter when inserting or deleting rows or +// columns. +// +// sheet: Worksheet index 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: 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 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 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 { + continue + } + xlsx.SheetData.Row[i].R += offset + for k, v := range xlsx.SheetData.Row[i].C { + axis := v.R + col := string(strings.Map(letterOnlyMapF, axis)) + row, _ := strconv.Atoi(strings.Map(intOnlyMapF, axis)) + xAxis := row + offset + xlsx.SheetData.Row[i].C[k].R = col + strconv.Itoa(xAxis) + } + } +} + +// adjustHyperlinks provides function to update hyperlinks when inserting or +// deleting rows or columns. +func (f *File) adjustHyperlinks(sheet string, column, rowIndex, offset int) { + xlsx := f.workSheetReader(sheet) + + // order is important + if xlsx.Hyperlinks != nil && offset < 0 { + for i, v := range xlsx.Hyperlinks.Hyperlink { + axis := v.Ref + col := string(strings.Map(letterOnlyMapF, axis)) + row, _ := strconv.Atoi(strings.Map(intOnlyMapF, axis)) + yAxis := TitleToNumber(col) + if row == rowIndex || yAxis == column { + f.deleteSheetRelationships(sheet, v.RID) + if len(xlsx.Hyperlinks.Hyperlink) > 1 { + xlsx.Hyperlinks.Hyperlink = append(xlsx.Hyperlinks.Hyperlink[:i], xlsx.Hyperlinks.Hyperlink[i+1:]...) + } else { + xlsx.Hyperlinks = nil + } + } + } + } + + if xlsx.Hyperlinks != nil { + for i, v := range xlsx.Hyperlinks.Hyperlink { + axis := v.Ref + col := string(strings.Map(letterOnlyMapF, axis)) + row, _ := strconv.Atoi(strings.Map(intOnlyMapF, axis)) + xAxis := row + offset + yAxis := TitleToNumber(col) + if rowIndex != -1 && row >= rowIndex { + xlsx.Hyperlinks.Hyperlink[i].Ref = col + strconv.Itoa(xAxis) + } + if column != -1 && yAxis >= column { + xlsx.Hyperlinks.Hyperlink[i].Ref = ToAlphaString(yAxis+offset) + strconv.Itoa(row) + } + } + } +} + +// adjustMergeCellsHelper provides 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 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 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 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 + } + } + } + } +} diff --git a/excelize_test.go b/excelize_test.go index d6e6d052..23630f35 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -750,7 +750,88 @@ func TestAddChart(t *testing.T) { xlsx.AddChart("SHEET2", "P1", `{"type":"radar","series":[{"name":"=Sheet1!$A$30","categories":"=Sheet1!$B$29:$D$29","values":"=Sheet1!$B$30:$D$30"},{"name":"=Sheet1!$A$31","categories":"=Sheet1!$B$29:$D$29","values":"=Sheet1!$B$31:$D$31"},{"name":"=Sheet1!$A$32","categories":"=Sheet1!$B$29:$D$29","values":"=Sheet1!$B$32:$D$32"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"top_right","show_legend_key":false},"title":{"name":"Fruit Radar Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"span"}`) xlsx.AddChart("SHEET2", "X1", `{"type":"scatter","series":[{"name":"=Sheet1!$A$30","categories":"=Sheet1!$B$29:$D$29","values":"=Sheet1!$B$30:$D$30"},{"name":"=Sheet1!$A$31","categories":"=Sheet1!$B$29:$D$29","values":"=Sheet1!$B$31:$D$31"},{"name":"=Sheet1!$A$32","categories":"=Sheet1!$B$29:$D$29","values":"=Sheet1!$B$32:$D$32"}],"format":{"x_scale":1.0,"y_scale":1.0,"x_offset":15,"y_offset":10,"print_obj":true,"lock_aspect_ratio":false,"locked":false},"legend":{"position":"bottom","show_legend_key":false},"title":{"name":"Fruit Scatter Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"show_blanks_as":"zero"}`) // Save xlsx file by the given path. - err = xlsx.SaveAs("./test/Workbook_6.xlsx") + err = xlsx.SaveAs("./test/Workbook_addchart.xlsx") + if err != nil { + t.Log(err) + } +} + +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/xuri/excelize") + xlsx.MergeCell("sheet1", "A1", "C3") + err := xlsx.AutoFilter("Sheet1", "A2", "B2", `{"column":"B","expression":"x != blanks"}`) + t.Log(err) + xlsx.InsertCol("Sheet1", "A") + err = xlsx.SaveAs("./test/Workbook_insertcol.xlsx") + if err != nil { + t.Log(err) + } +} + +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/xuri/excelize") + xlsx.SetCellHyperLink("Sheet1", "C5", "https://github.com") + xlsx.MergeCell("sheet1", "A1", "B1") + xlsx.MergeCell("sheet1", "A2", "B2") + xlsx.RemoveCol("Sheet1", "A") + xlsx.RemoveCol("Sheet1", "A") + err := xlsx.SaveAs("./test/Workbook_removecol.xlsx") + if err != nil { + t.Log(err) + } +} + +func TestInsertRow(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/xuri/excelize") + xlsx.InsertRow("Sheet1", -1) + xlsx.InsertRow("Sheet1", 4) + err := xlsx.SaveAs("./test/Workbook_insertrow.xlsx") + if err != nil { + t.Log(err) + } +} + +func TestRemoveRow(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/xuri/excelize") + xlsx.RemoveRow("Sheet1", -1) + xlsx.RemoveRow("Sheet1", 4) + xlsx.MergeCell("sheet1", "B3", "B5") + xlsx.RemoveRow("Sheet1", 2) + xlsx.RemoveRow("Sheet1", 4) + err := xlsx.AutoFilter("Sheet1", "A2", "A2", `{"column":"A","expression":"x != blanks"}`) + t.Log(err) + xlsx.RemoveRow("Sheet1", 0) + xlsx.RemoveRow("Sheet1", 1) + xlsx.RemoveRow("Sheet1", 0) + err = xlsx.SaveAs("./test/Workbook_removerow.xlsx") if err != nil { t.Log(err) } diff --git a/picture.go b/picture.go index 3ae309dc..f2216162 100644 --- a/picture.go +++ b/picture.go @@ -127,6 +127,23 @@ func (f *File) addSheetRelationships(sheet, relType, target, targetMode string) return rID } +// deleteSheetRelationships provides function to delete relationships in +// xl/worksheets/_rels/sheet%d.xml.rels by given sheet name and relationship +// index. +func (f *File) deleteSheetRelationships(sheet, rID string) { + var rels = "xl/worksheets/_rels/" + strings.ToLower(sheet) + ".xml.rels" + var sheetRels xlsxWorkbookRels + xml.Unmarshal([]byte(f.readXML(rels)), &sheetRels) + for k, v := range sheetRels.Relationships { + if v.ID != rID { + continue + } + sheetRels.Relationships = append(sheetRels.Relationships[:k], sheetRels.Relationships[k+1:]...) + } + output, _ := xml.Marshal(sheetRels) + f.saveFileList(rels, string(output)) +} + // addSheetLegacyDrawing provides function to add legacy drawing element to // xl/worksheets/sheet%d.xml by given sheet name and relationship index. func (f *File) addSheetLegacyDrawing(sheet string, rID int) { diff --git a/rows.go b/rows.go index 227a7d5c..c45b49fa 100644 --- a/rows.go +++ b/rows.go @@ -1,7 +1,9 @@ package excelize import ( + "bytes" "encoding/xml" + "math" "strconv" "strings" ) @@ -209,3 +211,136 @@ func (f *File) GetRowVisible(sheet string, rowIndex int) bool { completeRow(xlsx, rows, cells) return !xlsx.SheetData.Row[rowIndex].Hidden } + +// RemoveRow provides function to remove single row by given worksheet index and +// row index. For example, remove row 3 in Sheet1: +// +// xlsx.RemoveRow("Sheet1", 2) +// +func (f *File) RemoveRow(sheet string, row int) { + if row < 0 { + return + } + xlsx := f.workSheetReader(sheet) + row++ + for i, r := range xlsx.SheetData.Row { + if r.R != row { + continue + } + xlsx.SheetData.Row = append(xlsx.SheetData.Row[:i], xlsx.SheetData.Row[i+1:]...) + f.adjustHelper(sheet, -1, row, -1) + return + } +} + +// InsertRow provides function to insert a new row before given row index. For +// example, create a new row before row 3 in Sheet1: +// +// xlsx.InsertRow("Sheet1", 2) +// +func (f *File) InsertRow(sheet string, row int) { + if row < 0 { + return + } + row++ + f.adjustHelper(sheet, -1, row, 1) +} + +// checkRow provides function to check and fill each column element for all rows +// and make that is continuous in a worksheet of XML. For example: +// +// +// +// +// +// +// +// +// in this case, we should to change it to +// +// +// +// +// +// +// +// +// +// +// +// 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, v := range xlsx.SheetData.Row { + lenCol := len(v.C) + if lenCol < 1 { + continue + } + endR := string(strings.Map(letterOnlyMapF, v.C[lenCol-1].R)) + endRow, _ := strconv.Atoi(strings.Map(intOnlyMapF, v.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] + 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 + } + } + } +} + +// completeRow provides 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() + } + } + } +} + +// convertRowHeightToPixels provides function to convert the height of a cell +// from user's units to pixels. If the height hasn't been set by the user we use +// the default value. If the row is hidden it has a value of zero. +func convertRowHeightToPixels(height float64) float64 { + var pixels float64 + if height == 0 { + return pixels + } + pixels = math.Ceil(4.0 / 3.0 * height) + return pixels +}