diff --git a/README.md b/README.md index 821bbd7..b3106df 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,7 @@ ## Introduction -Excelize is a library written in pure Go providing a set of functions that allow you to write to and read from XLSX files. Supports reading and writing XLSX file generated by Microsoft Excel™ 2007 and later. -Supports saving a file without losing original charts of XLSX. This library needs Go version 1.10 or later. The full API docs can be seen using go's built-in documentation tool, or online at [go.dev](https://pkg.go.dev/github.com/360EntSecGroup-Skylar/excelize/v2?tab=doc) and [docs reference](https://xuri.me/excelize/). +Excelize is a library written in pure Go providing a set of functions that allow you to write to and read from XLSX / XLSM / XLTM files. Supports reading and writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. Supports complex components by high compatibility, and provided streaming API for generating or reading data from a worksheet with huge amounts of data. This library needs Go version 1.10 or later. The full API docs can be seen using go's built-in documentation tool, or online at [go.dev](https://pkg.go.dev/github.com/360EntSecGroup-Skylar/excelize/v2?tab=doc) and [docs reference](https://xuri.me/excelize/). ## Basic Usage @@ -24,6 +23,12 @@ Supports saving a file without losing original charts of XLSX. This library need go get github.com/360EntSecGroup-Skylar/excelize ``` +- If your package management with [Go Modules](https://blog.golang.org/using-go-modules), please install with following command. + +```bash +go get github.com/360EntSecGroup-Skylar/excelize/v2 +``` + ### Create XLSX file Here is a minimal example usage that will create XLSX file. diff --git a/README_zh.md b/README_zh.md index 18db28f..deba22a 100644 --- a/README_zh.md +++ b/README_zh.md @@ -13,7 +13,7 @@ ## 简介 -Excelize 是 Go 语言编写的用于操作 Office Excel 文档类库,基于 ECMA-376 Office OpenXML 标准。可以使用它来读取、写入由 Microsoft Excel™ 2007 及以上版本创建的 XLSX 文档。相比较其他的开源类库,Excelize 支持写入原本带有图片(表)、透视表和切片器等复杂样式的文档,还支持向 Excel 文档中插入图片与图表,并且在保存后不会丢失文档原有样式,可以应用于各类报表系统中。使用本类库要求使用的 Go 语言为 1.10 或更高版本,完整的 API 使用文档请访问 [go.dev](https://pkg.go.dev/github.com/360EntSecGroup-Skylar/excelize/v2?tab=doc) 或查看 [参考文档](https://xuri.me/excelize/)。 +Excelize 是 Go 语言编写的用于操作 Office Excel 文档基础库,基于 ECMA-376,ISO/IEC 29500 国际标准。可以使用它来读取、写入由 Microsoft Excel™ 2007 及以上版本创建的电子表格文档。支持 XLSX / XLSM / XLTM 等多种文档格式,高度兼容带有样式、图片(表)、透视表、切片器等复杂组件的文档,并提供流式读写 API,用于处理包含大规模数据的工作簿。可应用于各类报表平台、云计算、边缘计算等系统。使用本类库要求使用的 Go 语言为 1.10 或更高版本,完整的 API 使用文档请访问 [go.dev](https://pkg.go.dev/github.com/360EntSecGroup-Skylar/excelize/v2?tab=doc) 或查看 [参考文档](https://xuri.me/excelize/)。 ## 快速上手 @@ -23,6 +23,12 @@ Excelize 是 Go 语言编写的用于操作 Office Excel 文档类库,基于 E go get github.com/360EntSecGroup-Skylar/excelize ``` +- 如果您使用 [Go Modules](https://blog.golang.org/using-go-modules) 管理软件包,请使用下面的命令来安装最新版本。 + +```bash +go get github.com/360EntSecGroup-Skylar/excelize/v2 +``` + ### 创建 Excel 文档 下面是一个创建 Excel 文档的简单例子: diff --git a/cell.go b/cell.go index 8e7ede1..a69f4d9 100644 --- a/cell.go +++ b/cell.go @@ -337,7 +337,7 @@ func (f *File) SetCellFormula(sheet, axis, formula string, opts ...FormulaOpts) } if formula == "" { cellData.F = nil - f.deleteCalcChain(f.GetSheetIndex(sheet), axis) + f.deleteCalcChain(f.getSheetID(sheet), axis) return err } diff --git a/chart.go b/chart.go index 2c802ee..8fa0b5d 100644 --- a/chart.go +++ b/chart.go @@ -762,7 +762,7 @@ func (f *File) AddChart(sheet, cell, format string, combo ...string) error { // a chart. func (f *File) AddChartSheet(sheet, format string, combo ...string) error { // Check if the worksheet already exists - if f.GetSheetIndex(sheet) != 0 { + if f.GetSheetIndex(sheet) != -1 { return errors.New("the same name worksheet already exists") } formatSet, comboCharts, err := f.getFormatChart(format, combo) diff --git a/chart_test.go b/chart_test.go index 3b419f0..b35cb98 100644 --- a/chart_test.go +++ b/chart_test.go @@ -12,7 +12,7 @@ import ( func TestChartSize(t *testing.T) { xlsx := NewFile() - sheet1 := xlsx.GetSheetName(1) + sheet1 := xlsx.GetSheetName(0) categories := map[string]string{ "A2": "Small", @@ -220,14 +220,14 @@ func TestAddChartSheet(t *testing.T) { } assert.NoError(t, f.AddChartSheet("Chart1", `{"type":"col3DClustered","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"}}`)) // Test set the chartsheet as active sheet - var sheetID int - for idx, sheetName := range f.GetSheetMap() { + var sheetIdx int + for idx, sheetName := range f.GetSheetList() { if sheetName != "Chart1" { continue } - sheetID = idx + sheetIdx = idx } - f.SetActiveSheet(sheetID) + f.SetActiveSheet(sheetIdx) // Test cell value on chartsheet assert.EqualError(t, f.SetCellValue("Chart1", "A1", true), "sheet Chart1 is chart sheet") diff --git a/col_test.go b/col_test.go index 050c998..fcb1619 100644 --- a/col_test.go +++ b/col_test.go @@ -170,7 +170,7 @@ func TestColWidth(t *testing.T) { func TestInsertCol(t *testing.T) { f := NewFile() - sheet1 := f.GetSheetName(1) + sheet1 := f.GetSheetName(0) fillCells(f, sheet1, 10, 10) @@ -188,7 +188,7 @@ func TestInsertCol(t *testing.T) { func TestRemoveCol(t *testing.T) { f := NewFile() - sheet1 := f.GetSheetName(1) + sheet1 := f.GetSheetName(0) fillCells(f, sheet1, 10, 15) diff --git a/excelize.go b/excelize.go index ae011d9..73bc1b5 100644 --- a/excelize.go +++ b/excelize.go @@ -268,7 +268,7 @@ func (f *File) UpdateLinkedValue() error { wb := f.workbookReader() // recalculate formulas wb.CalcPr = nil - for _, name := range f.GetSheetMap() { + for _, name := range f.GetSheetList() { xlsx, err := f.workSheetReader(name) if err != nil { return err diff --git a/excelize_test.go b/excelize_test.go index e8d3a30..8ee8051 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -239,7 +239,7 @@ func TestBrokenFile(t *testing.T) { // Test set active sheet without BookViews and Sheets maps in xl/workbook.xml. f3, err := OpenFile(filepath.Join("test", "BadWorkbook.xlsx")) f3.GetActiveSheetIndex() - f3.SetActiveSheet(2) + f3.SetActiveSheet(1) assert.NoError(t, err) }) @@ -908,7 +908,7 @@ func TestCopySheet(t *testing.T) { } idx := f.NewSheet("CopySheet") - assert.NoError(t, f.CopySheet(1, idx)) + assert.NoError(t, f.CopySheet(0, idx)) assert.NoError(t, f.SetCellValue("CopySheet", "F1", "Hello")) val, err := f.GetCellValue("Sheet1", "F1") @@ -924,8 +924,8 @@ func TestCopySheetError(t *testing.T) { t.FailNow() } - assert.EqualError(t, f.copySheet(0, -1), "sheet is not exist") - if !assert.EqualError(t, f.CopySheet(0, -1), "invalid worksheet index") { + assert.EqualError(t, f.copySheet(-1, -2), "sheet is not exist") + if !assert.EqualError(t, f.CopySheet(-1, -2), "invalid worksheet index") { t.FailNow() } @@ -957,7 +957,7 @@ func TestSetSheetVisible(t *testing.T) { func TestGetActiveSheetIndex(t *testing.T) { f := NewFile() f.WorkBook.BookViews = nil - assert.Equal(t, 1, f.GetActiveSheetIndex()) + assert.Equal(t, 0, f.GetActiveSheetIndex()) } func TestRelsWriter(t *testing.T) { @@ -974,7 +974,7 @@ func TestGetSheetView(t *testing.T) { func TestConditionalFormat(t *testing.T) { f := NewFile() - sheet1 := f.GetSheetName(1) + sheet1 := f.GetSheetName(0) fillCells(f, sheet1, 10, 15) @@ -1060,7 +1060,7 @@ func TestConditionalFormat(t *testing.T) { func TestConditionalFormatError(t *testing.T) { f := NewFile() - sheet1 := f.GetSheetName(1) + sheet1 := f.GetSheetName(0) fillCells(f, sheet1, 10, 15) diff --git a/merge_test.go b/merge_test.go index e880d05..afe75aa 100644 --- a/merge_test.go +++ b/merge_test.go @@ -108,7 +108,7 @@ func TestGetMergeCells(t *testing.T) { if !assert.NoError(t, err) { t.FailNow() } - sheet1 := f.GetSheetName(1) + sheet1 := f.GetSheetName(0) mergeCells, err := f.GetMergeCells(sheet1) if !assert.Len(t, mergeCells, len(wants)) { @@ -132,7 +132,7 @@ func TestUnmergeCell(t *testing.T) { if !assert.NoError(t, err) { t.FailNow() } - sheet1 := f.GetSheetName(1) + sheet1 := f.GetSheetName(0) xlsx, err := f.workSheetReader(sheet1) assert.NoError(t, err) diff --git a/rows_test.go b/rows_test.go index a53b0a9..e5f0524 100644 --- a/rows_test.go +++ b/rows_test.go @@ -92,7 +92,7 @@ func TestRowsError(t *testing.T) { func TestRowHeight(t *testing.T) { xlsx := NewFile() - sheet1 := xlsx.GetSheetName(1) + sheet1 := xlsx.GetSheetName(0) assert.EqualError(t, xlsx.SetRowHeight(sheet1, 0, defaultRowHeightPixels+1.0), "invalid row number 0") @@ -199,7 +199,7 @@ func TestRowVisibility(t *testing.T) { func TestRemoveRow(t *testing.T) { f := NewFile() - sheet1 := f.GetSheetName(1) + sheet1 := f.GetSheetName(0) r, err := f.workSheetReader(sheet1) assert.NoError(t, err) const ( @@ -260,7 +260,7 @@ func TestRemoveRow(t *testing.T) { func TestInsertRow(t *testing.T) { xlsx := NewFile() - sheet1 := xlsx.GetSheetName(1) + sheet1 := xlsx.GetSheetName(0) r, err := xlsx.workSheetReader(sheet1) assert.NoError(t, err) const ( @@ -292,7 +292,7 @@ func TestInsertRow(t *testing.T) { // It is important for insert workflow to be constant to avoid side effect with functions related to internal structure. func TestInsertRowInEmptyFile(t *testing.T) { xlsx := NewFile() - sheet1 := xlsx.GetSheetName(1) + sheet1 := xlsx.GetSheetName(0) r, err := xlsx.workSheetReader(sheet1) assert.NoError(t, err) assert.NoError(t, xlsx.InsertRow(sheet1, 1)) diff --git a/sheet.go b/sheet.go index a3276c2..50081e8 100644 --- a/sheet.go +++ b/sheet.go @@ -34,7 +34,7 @@ import ( // the number of sheets in the workbook (file) after appending the new sheet. func (f *File) NewSheet(name string) int { // Check if the worksheet already exists - if f.GetSheetIndex(name) != 0 { + if f.GetSheetIndex(name) != -1 { return f.SheetCount } f.DeleteSheet(name) @@ -57,7 +57,7 @@ func (f *File) NewSheet(name string) int { rID := f.addRels("xl/_rels/workbook.xml.rels", SourceRelationshipWorkSheet, fmt.Sprintf("worksheets/sheet%d.xml", sheetID), "") // Update xl/workbook.xml f.setWorkbook(name, sheetID, rID) - return sheetID + return f.GetSheetIndex(name) } // contentTypesReader provides a function to get the pointer to the @@ -213,15 +213,15 @@ func replaceRelationshipsBytes(content []byte) []byte { // SetActiveSheet provides function to set default active worksheet of XLSX by // given index. Note that active index is different from the index returned by -// function GetSheetMap(). It should be greater than 0 and less than total -// worksheet numbers. +// function GetSheetMap(). It should be greater or equal to 0 and less than +// total worksheet numbers. func (f *File) SetActiveSheet(index int) { - if index < 1 { - index = 1 + if index < 0 { + index = 0 } wb := f.workbookReader() - for activeTab, sheet := range wb.Sheets.Sheet { - if sheet.SheetID == index { + for activeTab := range wb.Sheets.Sheet { + if activeTab == index { if wb.BookViews == nil { wb.BookViews = &xlsxBookViews{} } @@ -234,7 +234,7 @@ func (f *File) SetActiveSheet(index int) { } } } - for idx, name := range f.GetSheetMap() { + for idx, name := range f.GetSheetList() { xlsx, err := f.workSheetReader(name) if err != nil { // Chartsheet @@ -262,7 +262,22 @@ func (f *File) SetActiveSheet(index int) { // GetActiveSheetIndex provides a function to get active sheet index of the // XLSX. If not found the active sheet will be return integer 0. -func (f *File) GetActiveSheetIndex() int { +func (f *File) GetActiveSheetIndex() (index int) { + var sheetID = f.getActiveSheetID() + wb := f.workbookReader() + if wb != nil { + for idx, sheet := range wb.Sheets.Sheet { + if sheet.SheetID == sheetID { + index = idx + } + } + } + return +} + +// getActiveSheetID provides a function to get active sheet index of the +// XLSX. If not found the active sheet will be return integer 0. +func (f *File) getActiveSheetID() int { wb := f.workbookReader() if wb != nil { if wb.BookViews != nil && len(wb.BookViews.WorkBookView) > 0 { @@ -296,39 +311,62 @@ func (f *File) SetSheetName(oldName, newName string) { } } -// GetSheetName provides a function to get worksheet name of XLSX by given -// worksheet index. If given sheet index is invalid, will return an empty +// getSheetNameByID provides a function to get worksheet name of XLSX by given +// worksheet ID. If given sheet ID is invalid, will return an empty // string. -func (f *File) GetSheetName(index int) string { +func (f *File) getSheetNameByID(ID int) string { wb := f.workbookReader() - if wb == nil || index < 1 { + if wb == nil || ID < 1 { return "" } for _, sheet := range wb.Sheets.Sheet { - if index == sheet.SheetID { + if ID == sheet.SheetID { return sheet.Name } } return "" } +// GetSheetName provides a function to get worksheet name of XLSX by given +// worksheet index. If given sheet index is invalid, will return an empty +// string. +func (f *File) GetSheetName(index int) (name string) { + for idx, sheet := range f.GetSheetList() { + if idx == index { + name = sheet + } + } + return +} + +// getSheetID provides a function to get worksheet ID of XLSX by given +// sheet name. If given worksheet name is invalid, will return an integer type +// value -1. +func (f *File) getSheetID(name string) int { + var ID = -1 + for sheetID, sheet := range f.GetSheetMap() { + if sheet == trimSheetName(name) { + ID = sheetID + } + } + return ID +} + // GetSheetIndex provides a function to get worksheet index of XLSX by given // sheet name. If given worksheet name is invalid, will return an integer type -// value 0. +// value -1. func (f *File) GetSheetIndex(name string) int { - wb := f.workbookReader() - if wb != nil { - for _, sheet := range wb.Sheets.Sheet { - if sheet.Name == trimSheetName(name) { - return sheet.SheetID - } + var idx = -1 + for index, sheet := range f.GetSheetList() { + if sheet == trimSheetName(name) { + idx = index } } - return 0 + return idx } // GetSheetMap provides a function to get worksheet and chartsheet name and -// index map of XLSX. For example: +// ID map of XLSX. For example: // // f, err := excelize.OpenFile("Book1.xlsx") // if err != nil { @@ -349,8 +387,20 @@ func (f *File) GetSheetMap() map[int]string { return sheetMap } -// getSheetMap provides a function to get worksheet and chartsheet name and -// XML file path map of XLSX. +// GetSheetList provides a function to get worksheet and chartsheet name list +// of workbook. +func (f *File) GetSheetList() (list []string) { + wb := f.workbookReader() + if wb != nil { + for _, sheet := range wb.Sheets.Sheet { + list = append(list, sheet.Name) + } + } + return +} + +// getSheetMap provides a function to get worksheet name and XML file path map +// of XLSX. func (f *File) getSheetMap() map[string]string { content := f.workbookReader() rels := f.relsReader("xl/_rels/workbook.xml.rels") @@ -397,7 +447,7 @@ func (f *File) SetSheetBackground(sheet, picture string) error { // value of the deleted worksheet, it will cause a file error when you open it. // This function will be invalid when only the one worksheet is left. func (f *File) DeleteSheet(name string) { - if f.SheetCount == 1 || f.GetSheetIndex(name) == 0 { + if f.SheetCount == 1 || f.GetSheetIndex(name) == -1 { return } sheetName := trimSheetName(name) @@ -474,7 +524,7 @@ func (f *File) deleteSheetFromContentTypes(target string) { // return err // func (f *File) CopySheet(from, to int) error { - if from < 1 || to < 1 || from == to || f.GetSheetName(from) == "" || f.GetSheetName(to) == "" { + if from < 0 || to < 0 || from == to || f.GetSheetName(from) == "" || f.GetSheetName(to) == "" { return errors.New("invalid worksheet index") } return f.copySheet(from, to) @@ -483,12 +533,14 @@ func (f *File) CopySheet(from, to int) error { // copySheet provides a function to duplicate a worksheet by gave source and // target worksheet name. func (f *File) copySheet(from, to int) error { - sheet, err := f.workSheetReader(f.GetSheetName(from)) + fromSheet := f.GetSheetName(from) + sheet, err := f.workSheetReader(fromSheet) if err != nil { return err } worksheet := deepcopy.Copy(sheet).(*xlsxWorksheet) - path := "xl/worksheets/sheet" + strconv.Itoa(to) + ".xml" + toSheetID := strconv.Itoa(f.getSheetID(f.GetSheetName(to))) + path := "xl/worksheets/sheet" + toSheetID + ".xml" if len(worksheet.SheetViews.SheetView) > 0 { worksheet.SheetViews.SheetView[0].TabSelected = false } @@ -496,8 +548,8 @@ func (f *File) copySheet(from, to int) error { worksheet.TableParts = nil worksheet.PageSetUp = nil f.Sheet[path] = worksheet - toRels := "xl/worksheets/_rels/sheet" + strconv.Itoa(to) + ".xml.rels" - fromRels := "xl/worksheets/_rels/sheet" + strconv.Itoa(from) + ".xml.rels" + toRels := "xl/worksheets/_rels/sheet" + toSheetID + ".xml.rels" + fromRels := "xl/worksheets/_rels/sheet" + strconv.Itoa(f.getSheetID(fromSheet)) + ".xml.rels" _, ok := f.XLSX[fromRels] if ok { f.XLSX[toRels] = f.XLSX[fromRels] @@ -1303,7 +1355,7 @@ func (f *File) SetDefinedName(definedName *DefinedName) error { Data: definedName.RefersTo, } if definedName.Scope != "" { - if sheetID := f.GetSheetIndex(definedName.Scope); sheetID != 0 { + if sheetID := f.getSheetID(definedName.Scope); sheetID != 0 { sheetID-- d.LocalSheetID = &sheetID } @@ -1312,7 +1364,7 @@ func (f *File) SetDefinedName(definedName *DefinedName) error { for _, dn := range wb.DefinedNames.DefinedName { var scope string if dn.LocalSheetID != nil { - scope = f.GetSheetName(*dn.LocalSheetID + 1) + scope = f.getSheetNameByID(*dn.LocalSheetID + 1) } if scope == definedName.Scope && dn.Name == definedName.Name { return errors.New("the same name already exists on the scope") @@ -1342,7 +1394,7 @@ func (f *File) DeleteDefinedName(definedName *DefinedName) error { for idx, dn := range wb.DefinedNames.DefinedName { var scope string if dn.LocalSheetID != nil { - scope = f.GetSheetName(*dn.LocalSheetID + 1) + scope = f.getSheetNameByID(*dn.LocalSheetID + 1) } if scope == definedName.Scope && dn.Name == definedName.Name { wb.DefinedNames.DefinedName = append(wb.DefinedNames.DefinedName[:idx], wb.DefinedNames.DefinedName[idx+1:]...) @@ -1367,7 +1419,7 @@ func (f *File) GetDefinedName() []DefinedName { Scope: "Workbook", } if dn.LocalSheetID != nil { - definedName.Scope = f.GetSheetName(*dn.LocalSheetID + 1) + definedName.Scope = f.getSheetNameByID(*dn.LocalSheetID + 1) } definedNames = append(definedNames, definedName) } @@ -1381,7 +1433,7 @@ func (f *File) GroupSheets(sheets []string) error { // check an active worksheet in group worksheets var inActiveSheet bool activeSheet := f.GetActiveSheetIndex() - sheetMap := f.GetSheetMap() + sheetMap := f.GetSheetList() for idx, sheetName := range sheetMap { for _, s := range sheets { if s == sheetName && idx == activeSheet { @@ -1416,16 +1468,15 @@ func (f *File) GroupSheets(sheets []string) error { // UngroupSheets provides a function to ungroup worksheets. func (f *File) UngroupSheets() error { activeSheet := f.GetActiveSheetIndex() - sheetMap := f.GetSheetMap() - for sheetID, sheet := range sheetMap { - if activeSheet == sheetID { + for index, sheet := range f.GetSheetList() { + if activeSheet == index { continue } - xlsx, _ := f.workSheetReader(sheet) - sheetViews := xlsx.SheetViews.SheetView + ws, _ := f.workSheetReader(sheet) + sheetViews := ws.SheetViews.SheetView if len(sheetViews) > 0 { for idx := range sheetViews { - xlsx.SheetViews.SheetView[idx].TabSelected = false + ws.SheetViews.SheetView[idx].TabSelected = false } } } diff --git a/sheet_test.go b/sheet_test.go index 38d86e6..0014220 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -303,10 +303,10 @@ func TestRemovePageBreak(t *testing.T) { func TestGetSheetName(t *testing.T) { f, _ := excelize.OpenFile(filepath.Join("test", "Book1.xlsx")) - assert.Equal(t, "Sheet1", f.GetSheetName(1)) - assert.Equal(t, "Sheet2", f.GetSheetName(2)) - assert.Equal(t, "", f.GetSheetName(0)) - assert.Equal(t, "", f.GetSheetName(3)) + assert.Equal(t, "Sheet1", f.GetSheetName(0)) + assert.Equal(t, "Sheet2", f.GetSheetName(1)) + assert.Equal(t, "", f.GetSheetName(-1)) + assert.Equal(t, "", f.GetSheetName(2)) } func TestGetSheetMap(t *testing.T) { diff --git a/stream.go b/stream.go index 1af0b9f..838751d 100644 --- a/stream.go +++ b/stream.go @@ -69,7 +69,7 @@ type StreamWriter struct { // } // func (f *File) NewStreamWriter(sheet string) (*StreamWriter, error) { - sheetID := f.GetSheetIndex(sheet) + sheetID := f.getSheetID(sheet) if sheetID == 0 { return nil, fmt.Errorf("sheet %s is not exist", sheet) }