From 01a418bda8502890e89ab20a2b41220372877bae Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 27 Apr 2019 23:40:57 +0800 Subject: Resolve #392, compatible with strict relations name space inspection --- sheet.go | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/sheet.go b/sheet.go index 32d12d1..5e9fcae 100644 --- a/sheet.go +++ b/sheet.go @@ -92,7 +92,7 @@ func (f *File) workbookReader() *xlsxWorkbook { func (f *File) workBookWriter() { if f.WorkBook != nil { output, _ := xml.Marshal(f.WorkBook) - f.saveFileList("xl/workbook.xml", replaceRelationshipsNameSpaceBytes(output)) + f.saveFileList("xl/workbook.xml", replaceRelationshipsBytes(replaceRelationshipsNameSpaceBytes(output))) } } @@ -105,7 +105,7 @@ func (f *File) workSheetWriter() { f.Sheet[p].SheetData.Row[k].C = trimCell(v.C) } output, _ := xml.Marshal(sheet) - f.saveFileList(p, replaceWorkSheetsRelationshipsNameSpaceBytes(output)) + f.saveFileList(p, replaceRelationshipsBytes(replaceWorkSheetsRelationshipsNameSpaceBytes(output))) ok := f.checked[p] if ok { f.checked[p] = false @@ -211,6 +211,15 @@ func (f *File) setAppXML() { f.saveFileList("docProps/app.xml", []byte(templateDocpropsApp)) } +// replaceRelationshipsBytes; Some tools that read XLSX files have very strict +// requirements about the structure of the input XML. This function is a +// horrible hack to fix that after the XML marshalling is completed. +func replaceRelationshipsBytes(content []byte) []byte { + oldXmlns := []byte(`xmlns:relationships="http://schemas.openxmlformats.org/officeDocument/2006/relationships" relationships`) + newXmlns := []byte("r") + return bytes.Replace(content, oldXmlns, newXmlns, -1) +} + // replaceRelationshipsNameSpaceBytes; Some tools that read XLSX files have // very strict requirements about the structure of the input XML. In // particular both Numbers on the Mac and SAS dislike inline XML namespace -- cgit v1.2.1 From b1f632d4084130628f10906ff6a7bb55022e4c08 Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 30 Apr 2019 18:39:27 +0800 Subject: Resolve #393, upgrade Go module to v2 --- README.md | 10 +++++----- README_zh.md | 10 +++++----- cellmerged.go | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ cellmerged_test.go | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ chart.go | 2 +- excelize.go | 47 ----------------------------------------------- excelize_test.go | 46 ---------------------------------------------- go.mod | 5 +++-- go.sum | 3 +-- picture.go | 4 ++-- sheet_test.go | 2 +- sheetpr_test.go | 30 +++++++++++++++--------------- sheetview_test.go | 44 ++++++++++++++++++++++---------------------- 13 files changed, 157 insertions(+), 148 deletions(-) create mode 100644 cellmerged.go create mode 100644 cellmerged_test.go diff --git a/README.md b/README.md index eae0072..91155e3 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Supports saving a file without losing original charts of XLSX. This library need ### Installation ```bash -go get github.com/360EntSecGroup-Skylar/excelize +go get github.com/360EntSecGroup-Skylar/excelize/v2 ``` ### Create XLSX file @@ -34,7 +34,7 @@ package main import ( "fmt" - "github.com/360EntSecGroup-Skylar/excelize" + "github.com/360EntSecGroup-Skylar/excelize/v2" ) func main() { @@ -64,7 +64,7 @@ package main import ( "fmt" - "github.com/360EntSecGroup-Skylar/excelize" + "github.com/360EntSecGroup-Skylar/excelize/v2" ) func main() { @@ -103,7 +103,7 @@ package main import ( "fmt" - "github.com/360EntSecGroup-Skylar/excelize" + "github.com/360EntSecGroup-Skylar/excelize/v2" ) func main() { @@ -140,7 +140,7 @@ import ( _ "image/jpeg" _ "image/png" - "github.com/360EntSecGroup-Skylar/excelize" + "github.com/360EntSecGroup-Skylar/excelize/v2" ) func main() { diff --git a/README_zh.md b/README_zh.md index dfed749..044d930 100644 --- a/README_zh.md +++ b/README_zh.md @@ -20,7 +20,7 @@ Excelize 是 Go 语言编写的用于操作 Office Excel 文档类库,基于 E ### 安装 ```bash -go get github.com/360EntSecGroup-Skylar/excelize +go get github.com/360EntSecGroup-Skylar/excelize/v2 ``` ### 创建 Excel 文档 @@ -33,7 +33,7 @@ package main import ( "fmt" - "github.com/360EntSecGroup-Skylar/excelize" + "github.com/360EntSecGroup-Skylar/excelize/v2" ) func main() { @@ -63,7 +63,7 @@ package main import ( "fmt" - "github.com/360EntSecGroup-Skylar/excelize" + "github.com/360EntSecGroup-Skylar/excelize/v2" ) func main() { @@ -102,7 +102,7 @@ package main import ( "fmt" - "github.com/360EntSecGroup-Skylar/excelize" + "github.com/360EntSecGroup-Skylar/excelize/v2" ) func main() { @@ -140,7 +140,7 @@ import ( _ "image/jpeg" _ "image/png" - "github.com/360EntSecGroup-Skylar/excelize" + "github.com/360EntSecGroup-Skylar/excelize/v2" ) func main() { diff --git a/cellmerged.go b/cellmerged.go new file mode 100644 index 0000000..5392463 --- /dev/null +++ b/cellmerged.go @@ -0,0 +1,48 @@ +package excelize + +import "strings" + +// GetMergeCells provides a function to get all merged cells from a worksheet currently. +func (f *File) GetMergeCells(sheet string) ([]MergeCell, error) { + var mergeCells []MergeCell + xlsx, err := f.workSheetReader(sheet) + if err != nil { + return mergeCells, err + } + if xlsx.MergeCells != nil { + mergeCells = make([]MergeCell, 0, len(xlsx.MergeCells.Cells)) + + for i := range xlsx.MergeCells.Cells { + ref := xlsx.MergeCells.Cells[i].Ref + axis := strings.Split(ref, ":")[0] + val, _ := f.GetCellValue(sheet, axis) + mergeCells = append(mergeCells, []string{ref, val}) + } + } + + return mergeCells, err +} + +// MergeCell define a merged cell data. +// It consists of the following structure. +// example: []string{"D4:E10", "cell value"} +type MergeCell []string + +// GetCellValue returns merged cell value. +func (m *MergeCell) GetCellValue() string { + return (*m)[1] +} + +// GetStartAxis returns the merge start axis. +// example: "C2" +func (m *MergeCell) GetStartAxis() string { + axis := strings.Split((*m)[0], ":") + return axis[0] +} + +// GetEndAxis returns the merge end axis. +// example: "D4" +func (m *MergeCell) GetEndAxis() string { + axis := strings.Split((*m)[0], ":") + return axis[1] +} \ No newline at end of file diff --git a/cellmerged_test.go b/cellmerged_test.go new file mode 100644 index 0000000..d53acc2 --- /dev/null +++ b/cellmerged_test.go @@ -0,0 +1,54 @@ +package excelize + +import ( + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGetMergeCells(t *testing.T) { + wants := []struct { + 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", + }} + + f, err := OpenFile(filepath.Join("test", "MergeCell.xlsx")) + if !assert.NoError(t, err) { + t.FailNow() + } + sheet1 := f.GetSheetName(1) + + mergeCells, err := f.GetMergeCells(sheet1) + if !assert.Len(t, mergeCells, len(wants)) { + t.FailNow() + } + assert.NoError(t, err) + + for i, m := range mergeCells { + assert.Equal(t, wants[i].value, m.GetCellValue()) + assert.Equal(t, wants[i].start, m.GetStartAxis()) + assert.Equal(t, wants[i].end, m.GetEndAxis()) + } + + // Test get merged cells on not exists worksheet. + _, err = f.GetMergeCells("SheetN") + assert.EqualError(t, err, "sheet SheetN is not exist") +} diff --git a/chart.go b/chart.go index d669a47..88f48b2 100644 --- a/chart.go +++ b/chart.go @@ -308,7 +308,7 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // import ( // "fmt" // -// "github.com/360EntSecGroup-Skylar/excelize" +// "github.com/360EntSecGroup-Skylar/excelize/v2" // ) // // func main() { diff --git a/excelize.go b/excelize.go index 41fba37..6fb98c4 100644 --- a/excelize.go +++ b/excelize.go @@ -19,7 +19,6 @@ import ( "io/ioutil" "os" "strconv" - "strings" ) // File define a populated XLSX file struct. @@ -215,49 +214,3 @@ func (f *File) UpdateLinkedValue() error { } return nil } - -// GetMergeCells provides a function to get all merged cells from a worksheet -// currently. -func (f *File) GetMergeCells(sheet string) ([]MergeCell, error) { - var mergeCells []MergeCell - xlsx, err := f.workSheetReader(sheet) - if err != nil { - return mergeCells, err - } - if xlsx.MergeCells != nil { - mergeCells = make([]MergeCell, 0, len(xlsx.MergeCells.Cells)) - - for i := range xlsx.MergeCells.Cells { - ref := xlsx.MergeCells.Cells[i].Ref - axis := strings.Split(ref, ":")[0] - val, _ := f.GetCellValue(sheet, axis) - mergeCells = append(mergeCells, []string{ref, val}) - } - } - - return mergeCells, err -} - -// MergeCell define a merged cell data. -// It consists of the following structure. -// example: []string{"D4:E10", "cell value"} -type MergeCell []string - -// GetCellValue returns merged cell value. -func (m *MergeCell) GetCellValue() string { - return (*m)[1] -} - -// GetStartAxis returns the merge start axis. -// example: "C2" -func (m *MergeCell) GetStartAxis() string { - axis := strings.Split((*m)[0], ":") - return axis[0] -} - -// GetEndAxis returns the merge end axis. -// example: "D4" -func (m *MergeCell) GetEndAxis() string { - axis := strings.Split((*m)[0], ":") - return axis[1] -} diff --git a/excelize_test.go b/excelize_test.go index 87fd806..c76aa92 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -407,52 +407,6 @@ func TestMergeCell(t *testing.T) { assert.NoError(t, f.SaveAs(filepath.Join("test", "TestMergeCell.xlsx"))) } -func TestGetMergeCells(t *testing.T) { - wants := []struct { - 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", - }} - - f, err := OpenFile(filepath.Join("test", "MergeCell.xlsx")) - if !assert.NoError(t, err) { - t.FailNow() - } - sheet1 := f.GetSheetName(1) - - mergeCells, err := f.GetMergeCells(sheet1) - if !assert.Len(t, mergeCells, len(wants)) { - t.FailNow() - } - assert.NoError(t, err) - - for i, m := range mergeCells { - assert.Equal(t, wants[i].value, m.GetCellValue()) - assert.Equal(t, wants[i].start, m.GetStartAxis()) - assert.Equal(t, wants[i].end, m.GetEndAxis()) - } - - // Test get merged cells on not exists worksheet. - _, err = f.GetMergeCells("SheetN") - assert.EqualError(t, err, "sheet SheetN is not exist") -} - func TestSetCellStyleAlignment(t *testing.T) { f, err := prepareTestBook1() if !assert.NoError(t, err) { diff --git a/go.mod b/go.mod index b96dbe2..9f36b59 100644 --- a/go.mod +++ b/go.mod @@ -1,7 +1,8 @@ -module github.com/360EntSecGroup-Skylar/excelize +module github.com/360EntSecGroup-Skylar/excelize/v2 + +go 1.12 require ( - github.com/davecgh/go-spew v1.1.1 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 github.com/stretchr/testify v1.3.0 ) diff --git a/go.sum b/go.sum index 106a417..890277c 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,5 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= diff --git a/picture.go b/picture.go index 3cfcbf5..01c2ae2 100644 --- a/picture.go +++ b/picture.go @@ -51,7 +51,7 @@ func parseFormatPictureSet(formatSet string) (*formatPicture, error) { // _ "image/jpeg" // _ "image/png" // -// "github.com/360EntSecGroup-Skylar/excelize" +// "github.com/360EntSecGroup-Skylar/excelize/v2" // ) // // func main() { @@ -111,7 +111,7 @@ func (f *File) AddPicture(sheet, cell, picture, format string) error { // _ "image/jpeg" // "io/ioutil" // -// "github.com/360EntSecGroup-Skylar/excelize" +// "github.com/360EntSecGroup-Skylar/excelize/v2" // ) // // func main() { diff --git a/sheet_test.go b/sheet_test.go index 7db982a..f0a1963 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -4,7 +4,7 @@ import ( "fmt" "testing" - "github.com/360EntSecGroup-Skylar/excelize" + "github.com/360EntSecGroup-Skylar/excelize/v2" "github.com/mohae/deepcopy" "github.com/stretchr/testify/assert" ) diff --git a/sheetpr_test.go b/sheetpr_test.go index 48d330e..97a314c 100644 --- a/sheetpr_test.go +++ b/sheetpr_test.go @@ -7,7 +7,7 @@ import ( "github.com/mohae/deepcopy" "github.com/stretchr/testify/assert" - "github.com/360EntSecGroup-Skylar/excelize" + "github.com/360EntSecGroup-Skylar/excelize/v2" ) var _ = []excelize.SheetPrOption{ @@ -29,10 +29,10 @@ var _ = []excelize.SheetPrOptionPtr{ } func ExampleFile_SetSheetPrOptions() { - xl := excelize.NewFile() + f := excelize.NewFile() const sheet = "Sheet1" - if err := xl.SetSheetPrOptions(sheet, + if err := f.SetSheetPrOptions(sheet, excelize.CodeName("code"), excelize.EnableFormatConditionsCalculation(false), excelize.Published(false), @@ -46,7 +46,7 @@ func ExampleFile_SetSheetPrOptions() { } func ExampleFile_GetSheetPrOptions() { - xl := excelize.NewFile() + f := excelize.NewFile() const sheet = "Sheet1" var ( @@ -58,7 +58,7 @@ func ExampleFile_GetSheetPrOptions() { outlineSummaryBelow excelize.OutlineSummaryBelow ) - if err := xl.GetSheetPrOptions(sheet, + if err := f.GetSheetPrOptions(sheet, &codeName, &enableFormatConditionsCalculation, &published, @@ -110,26 +110,26 @@ func TestSheetPrOptions(t *testing.T) { val1 := deepcopy.Copy(def).(excelize.SheetPrOptionPtr) val2 := deepcopy.Copy(def).(excelize.SheetPrOptionPtr) - xl := excelize.NewFile() + f := excelize.NewFile() // Get the default value - assert.NoError(t, xl.GetSheetPrOptions(sheet, def), opt) + assert.NoError(t, f.GetSheetPrOptions(sheet, def), opt) // Get again and check - assert.NoError(t, xl.GetSheetPrOptions(sheet, val1), opt) + assert.NoError(t, f.GetSheetPrOptions(sheet, val1), opt) if !assert.Equal(t, val1, def, opt) { t.FailNow() } // Set the same value - assert.NoError(t, xl.SetSheetPrOptions(sheet, val1), opt) + assert.NoError(t, f.SetSheetPrOptions(sheet, val1), opt) // Get again and check - assert.NoError(t, xl.GetSheetPrOptions(sheet, val1), opt) + assert.NoError(t, f.GetSheetPrOptions(sheet, val1), opt) if !assert.Equal(t, val1, def, "%T: value should not have changed", opt) { t.FailNow() } // Set a different value - assert.NoError(t, xl.SetSheetPrOptions(sheet, test.nonDefault), opt) - assert.NoError(t, xl.GetSheetPrOptions(sheet, val1), opt) + assert.NoError(t, f.SetSheetPrOptions(sheet, test.nonDefault), opt) + assert.NoError(t, f.GetSheetPrOptions(sheet, val1), opt) // Get again and compare - assert.NoError(t, xl.GetSheetPrOptions(sheet, val2), opt) + assert.NoError(t, f.GetSheetPrOptions(sheet, val2), opt) if !assert.Equal(t, val1, val2, "%T: value should not have changed", opt) { t.FailNow() } @@ -138,8 +138,8 @@ func TestSheetPrOptions(t *testing.T) { t.FailNow() } // Restore the default value - assert.NoError(t, xl.SetSheetPrOptions(sheet, def), opt) - assert.NoError(t, xl.GetSheetPrOptions(sheet, val1), opt) + assert.NoError(t, f.SetSheetPrOptions(sheet, def), opt) + assert.NoError(t, f.GetSheetPrOptions(sheet, val1), opt) if !assert.Equal(t, def, val1) { t.FailNow() } diff --git a/sheetview_test.go b/sheetview_test.go index b565a12..2e697b8 100644 --- a/sheetview_test.go +++ b/sheetview_test.go @@ -6,7 +6,7 @@ import ( "github.com/stretchr/testify/assert" - "github.com/360EntSecGroup-Skylar/excelize" + "github.com/360EntSecGroup-Skylar/excelize/v2" ) var _ = []excelize.SheetViewOption{ @@ -35,10 +35,10 @@ var _ = []excelize.SheetViewOptionPtr{ } func ExampleFile_SetSheetViewOptions() { - xl := excelize.NewFile() + f := excelize.NewFile() const sheet = "Sheet1" - if err := xl.SetSheetViewOptions(sheet, 0, + if err := f.SetSheetViewOptions(sheet, 0, excelize.DefaultGridColor(false), excelize.RightToLeft(false), excelize.ShowFormulas(true), @@ -54,22 +54,22 @@ func ExampleFile_SetSheetViewOptions() { fmt.Println("Default:") fmt.Println("- zoomScale: 80") - if err := xl.SetSheetViewOptions(sheet, 0, excelize.ZoomScale(500)); err != nil { + if err := f.SetSheetViewOptions(sheet, 0, excelize.ZoomScale(500)); err != nil { panic(err) } - if err := xl.GetSheetViewOptions(sheet, 0, &zoomScale); err != nil { + if err := f.GetSheetViewOptions(sheet, 0, &zoomScale); err != nil { panic(err) } fmt.Println("Used out of range value:") fmt.Println("- zoomScale:", zoomScale) - if err := xl.SetSheetViewOptions(sheet, 0, excelize.ZoomScale(123)); err != nil { + if err := f.SetSheetViewOptions(sheet, 0, excelize.ZoomScale(123)); err != nil { panic(err) } - if err := xl.GetSheetViewOptions(sheet, 0, &zoomScale); err != nil { + if err := f.GetSheetViewOptions(sheet, 0, &zoomScale); err != nil { panic(err) } @@ -87,7 +87,7 @@ func ExampleFile_SetSheetViewOptions() { } func ExampleFile_GetSheetViewOptions() { - xl := excelize.NewFile() + f := excelize.NewFile() const sheet = "Sheet1" var ( @@ -100,7 +100,7 @@ func ExampleFile_GetSheetViewOptions() { topLeftCell excelize.TopLeftCell ) - if err := xl.GetSheetViewOptions(sheet, 0, + if err := f.GetSheetViewOptions(sheet, 0, &defaultGridColor, &rightToLeft, &showFormulas, @@ -121,19 +121,19 @@ func ExampleFile_GetSheetViewOptions() { fmt.Println("- zoomScale:", zoomScale) fmt.Println("- topLeftCell:", `"`+topLeftCell+`"`) - if err := xl.SetSheetViewOptions(sheet, 0, excelize.TopLeftCell("B2")); err != nil { + if err := f.SetSheetViewOptions(sheet, 0, excelize.TopLeftCell("B2")); err != nil { panic(err) } - if err := xl.GetSheetViewOptions(sheet, 0, &topLeftCell); err != nil { + if err := f.GetSheetViewOptions(sheet, 0, &topLeftCell); err != nil { panic(err) } - if err := xl.SetSheetViewOptions(sheet, 0, excelize.ShowGridLines(false)); err != nil { + if err := f.SetSheetViewOptions(sheet, 0, excelize.ShowGridLines(false)); err != nil { panic(err) } - if err := xl.GetSheetViewOptions(sheet, 0, &showGridLines); err != nil { + if err := f.GetSheetViewOptions(sheet, 0, &showGridLines); err != nil { panic(err) } @@ -156,15 +156,15 @@ func ExampleFile_GetSheetViewOptions() { } func TestSheetViewOptionsErrors(t *testing.T) { - xl := excelize.NewFile() + f := excelize.NewFile() const sheet = "Sheet1" - assert.NoError(t, xl.GetSheetViewOptions(sheet, 0)) - assert.NoError(t, xl.GetSheetViewOptions(sheet, -1)) - assert.Error(t, xl.GetSheetViewOptions(sheet, 1)) - assert.Error(t, xl.GetSheetViewOptions(sheet, -2)) - assert.NoError(t, xl.SetSheetViewOptions(sheet, 0)) - assert.NoError(t, xl.SetSheetViewOptions(sheet, -1)) - assert.Error(t, xl.SetSheetViewOptions(sheet, 1)) - assert.Error(t, xl.SetSheetViewOptions(sheet, -2)) + assert.NoError(t, f.GetSheetViewOptions(sheet, 0)) + assert.NoError(t, f.GetSheetViewOptions(sheet, -1)) + assert.Error(t, f.GetSheetViewOptions(sheet, 1)) + assert.Error(t, f.GetSheetViewOptions(sheet, -2)) + assert.NoError(t, f.SetSheetViewOptions(sheet, 0)) + assert.NoError(t, f.SetSheetViewOptions(sheet, -1)) + assert.Error(t, f.SetSheetViewOptions(sheet, 1)) + assert.Error(t, f.SetSheetViewOptions(sheet, -2)) } -- cgit v1.2.1 From 63e97ffc9aae35780cdbd69ad966fb101fc5217f Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 3 May 2019 02:11:53 +0800 Subject: Remove Go 1.8 test in TravisCI --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 6c061a8..9f892c5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,6 @@ install: - go get -d -t -v ./... && go build -v ./... go: - - 1.8.x - 1.9.x - 1.10.x - 1.11.x -- cgit v1.2.1 From 72701e89c7145f9d08a79c93040e232b2875c855 Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 4 May 2019 00:10:11 +0800 Subject: Fix structs fields definition errors and keep double quotes in data validation formula --- datavalidation.go | 8 +++---- xmlWorksheet.go | 68 +++++++++++++++++++++++++++++++++++-------------------- 2 files changed, 47 insertions(+), 29 deletions(-) diff --git a/datavalidation.go b/datavalidation.go index 8fb9623..56b96fd 100644 --- a/datavalidation.go +++ b/datavalidation.go @@ -112,7 +112,7 @@ func (dd *DataValidation) SetDropList(keys []string) error { if dataValidationFormulaStrLen < len(formula) { return fmt.Errorf(dataValidationFormulaStrLenErr) } - dd.Formula1 = formula + dd.Formula1 = fmt.Sprintf("%s", formula) dd.Type = convDataValidationType(typeList) return nil } @@ -121,12 +121,12 @@ func (dd *DataValidation) SetDropList(keys []string) error { func (dd *DataValidation) SetRange(f1, f2 int, t DataValidationType, o DataValidationOperator) error { formula1 := fmt.Sprintf("%d", f1) formula2 := fmt.Sprintf("%d", f2) - if dataValidationFormulaStrLen < len(dd.Formula1) || dataValidationFormulaStrLen < len(dd.Formula2) { + if dataValidationFormulaStrLen+21 < len(dd.Formula1) || dataValidationFormulaStrLen+21 < len(dd.Formula2) { return fmt.Errorf(dataValidationFormulaStrLenErr) } - dd.Formula1 = formula1 - dd.Formula2 = formula2 + dd.Formula1 = fmt.Sprintf("%s", formula1) + dd.Formula2 = fmt.Sprintf("%s", formula2) dd.Type = convDataValidationType(t) dd.Operator = convDataValidationOperatior(o) return nil diff --git a/xmlWorksheet.go b/xmlWorksheet.go index f3323cb..f2eb47a 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -53,23 +53,27 @@ type xlsxDrawing struct { // footers on the first page can differ from those on odd- and even-numbered // pages. In the latter case, the first page is not considered an odd page. type xlsxHeaderFooter struct { - DifferentFirst bool `xml:"differentFirst,attr,omitempty"` - DifferentOddEven bool `xml:"differentOddEven,attr,omitempty"` - OddHeader []*xlsxOddHeader `xml:"oddHeader"` - OddFooter []*xlsxOddFooter `xml:"oddFooter"` -} - -// xlsxOddHeader directly maps the oddHeader element in the namespace -// http://schemas.openxmlformats.org/spreadsheetml/2006/main - currently I have -// not checked it for completeness - it does as much as I need. -type xlsxOddHeader struct { - Content string `xml:",chardata"` -} - -// xlsxOddFooter directly maps the oddFooter element in the namespace -// http://schemas.openxmlformats.org/spreadsheetml/2006/main - currently I have -// not checked it for completeness - it does as much as I need. -type xlsxOddFooter struct { + AlignWithMargins bool `xml:"alignWithMargins,attr,omitempty"` + DifferentFirst bool `xml:"differentFirst,attr,omitempty"` + DifferentOddEven bool `xml:"differentOddEven,attr,omitempty"` + ScaleWithDoc bool `xml:"scaleWithDoc,attr,omitempty"` + OddHeader string `xml:"oddHeader,omitempty"` + OddFooter string `xml:"oddFooter,omitempty"` + EvenHeader string `xml:"evenHeader,omitempty"` + EvenFooter string `xml:"evenFooter,omitempty"` + FirstFooter string `xml:"firstFooter,omitempty"` + FirstHeader string `xml:"firstHeader,omitempty"` + DrawingHF *xlsxDrawingHF `xml:"drawingHF"` +} + +// xlsxDrawingHF (Drawing Reference in Header Footer) specifies the usage of +// drawing objects to be rendered in the headers and footers of the sheet. It +// specifies an explicit relationship to the part containing the DrawingML +// shapes used in the headers and footers. It also indicates where in the +// headers and footers each shape belongs. One drawing object can appear in +// each of the left section, center section and right section of a header and +// a footer. +type xlsxDrawingHF struct { Content string `xml:",chardata"` } @@ -324,16 +328,16 @@ type DataValidation struct { Error *string `xml:"error,attr"` ErrorStyle *string `xml:"errorStyle,attr"` ErrorTitle *string `xml:"errorTitle,attr"` - Operator string `xml:"operator,attr"` + Operator string `xml:"operator,attr,omitempty"` Prompt *string `xml:"prompt,attr"` - PromptTitle *string `xml:"promptTitle"` - ShowDropDown bool `xml:"showDropDown,attr"` - ShowErrorMessage bool `xml:"showErrorMessage,attr"` - ShowInputMessage bool `xml:"showInputMessage,attr"` + PromptTitle *string `xml:"promptTitle,attr"` + ShowDropDown bool `xml:"showDropDown,attr,omitempty"` + ShowErrorMessage bool `xml:"showErrorMessage,attr,omitempty"` + ShowInputMessage bool `xml:"showInputMessage,attr,omitempty"` Sqref string `xml:"sqref,attr"` Type string `xml:"type,attr"` - Formula1 string `xml:"formula1"` - Formula2 string `xml:"formula2"` + Formula1 string `xml:",innerxml"` + Formula2 string `xml:",innerxml"` } // xlsxC directly maps the c element in the namespace @@ -482,7 +486,7 @@ type xlsxIconSet struct { type xlsxCfvo struct { Gte bool `xml:"gte,attr,omitempty"` Type string `xml:"type,attr,omitempty"` - Val string `xml:"val,attr"` + Val string `xml:"val,attr,omitempty"` ExtLst *xlsxExtLst `xml:"extLst"` } @@ -627,3 +631,17 @@ type FormatSheetProtection struct { SelectUnlockedCells bool Sort bool } + +// FormatHeaderFooter directly maps the settings of header and footer. +type FormatHeaderFooter struct { + AlignWithMargins bool + DifferentFirst bool + DifferentOddEven bool + ScaleWithDoc bool + OddHeader string + OddFooter string + EvenHeader string + EvenFooter string + FirstFooter string + FirstHeader string +} -- cgit v1.2.1 From 69b38ddcd60f7cf4c158c706ddbbeb89a8ff2108 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 5 May 2019 16:25:57 +0800 Subject: Resolve #394, init set header and footer support --- datavalidation_test.go | 30 +++++----- excelize_test.go | 8 +-- sheet.go | 155 ++++++++++++++++++++++++++++++++++++++++++++++++- sheet_test.go | 57 +++++++++++++----- 4 files changed, 213 insertions(+), 37 deletions(-) diff --git a/datavalidation_test.go b/datavalidation_test.go index afb659c..0fee092 100644 --- a/datavalidation_test.go +++ b/datavalidation_test.go @@ -19,7 +19,7 @@ import ( func TestDataValidation(t *testing.T) { resultFile := filepath.Join("test", "TestDataValidation.xlsx") - xlsx := NewFile() + f := NewFile() dvRange := NewDataValidation(true) dvRange.Sqref = "A1:B2" @@ -27,8 +27,8 @@ func TestDataValidation(t *testing.T) { dvRange.SetError(DataValidationErrorStyleStop, "error title", "error body") dvRange.SetError(DataValidationErrorStyleWarning, "error title", "error body") dvRange.SetError(DataValidationErrorStyleInformation, "error title", "error body") - xlsx.AddDataValidation("Sheet1", dvRange) - if !assert.NoError(t, xlsx.SaveAs(resultFile)) { + f.AddDataValidation("Sheet1", dvRange) + if !assert.NoError(t, f.SaveAs(resultFile)) { t.FailNow() } @@ -36,16 +36,16 @@ func TestDataValidation(t *testing.T) { dvRange.Sqref = "A3:B4" dvRange.SetRange(10, 20, DataValidationTypeWhole, DataValidationOperatorGreaterThan) dvRange.SetInput("input title", "input body") - xlsx.AddDataValidation("Sheet1", dvRange) - if !assert.NoError(t, xlsx.SaveAs(resultFile)) { + f.AddDataValidation("Sheet1", dvRange) + if !assert.NoError(t, f.SaveAs(resultFile)) { t.FailNow() } dvRange = NewDataValidation(true) dvRange.Sqref = "A5:B6" dvRange.SetDropList([]string{"1", "2", "3"}) - xlsx.AddDataValidation("Sheet1", dvRange) - if !assert.NoError(t, xlsx.SaveAs(resultFile)) { + f.AddDataValidation("Sheet1", dvRange) + if !assert.NoError(t, f.SaveAs(resultFile)) { t.FailNow() } } @@ -53,10 +53,10 @@ func TestDataValidation(t *testing.T) { func TestDataValidationError(t *testing.T) { resultFile := filepath.Join("test", "TestDataValidationError.xlsx") - xlsx := NewFile() - xlsx.SetCellStr("Sheet1", "E1", "E1") - xlsx.SetCellStr("Sheet1", "E2", "E2") - xlsx.SetCellStr("Sheet1", "E3", "E3") + f := NewFile() + f.SetCellStr("Sheet1", "E1", "E1") + f.SetCellStr("Sheet1", "E2", "E2") + f.SetCellStr("Sheet1", "E3", "E3") dvRange := NewDataValidation(true) dvRange.SetSqref("A7:B8") @@ -66,8 +66,8 @@ func TestDataValidationError(t *testing.T) { err := dvRange.SetSqrefDropList("$E$1:$E$3", false) assert.EqualError(t, err, "cross-sheet sqref cell are not supported") - xlsx.AddDataValidation("Sheet1", dvRange) - if !assert.NoError(t, xlsx.SaveAs(resultFile)) { + f.AddDataValidation("Sheet1", dvRange) + if !assert.NoError(t, f.SaveAs(resultFile)) { t.FailNow() } @@ -81,8 +81,8 @@ func TestDataValidationError(t *testing.T) { dvRange.SetRange(10, 20, DataValidationTypeWhole, DataValidationOperatorGreaterThan) dvRange.SetSqref("A9:B10") - xlsx.AddDataValidation("Sheet1", dvRange) - if !assert.NoError(t, xlsx.SaveAs(resultFile)) { + f.AddDataValidation("Sheet1", dvRange) + if !assert.NoError(t, f.SaveAs(resultFile)) { t.FailNow() } } diff --git a/excelize_test.go b/excelize_test.go index c76aa92..3509cb8 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -789,9 +789,9 @@ func TestCopySheet(t *testing.T) { } idx := f.NewSheet("CopySheet") - assert.EqualError(t, f.CopySheet(1, idx), "sheet sheet1 is not exist") + assert.NoError(t, f.CopySheet(1, idx)) - f.SetCellValue("Sheet4", "F1", "Hello") + f.SetCellValue("CopySheet", "F1", "Hello") val, err := f.GetCellValue("Sheet1", "F1") assert.NoError(t, err) assert.NotEqual(t, "Hello", val) @@ -805,8 +805,8 @@ func TestCopySheetError(t *testing.T) { t.FailNow() } - err = f.CopySheet(0, -1) - if !assert.EqualError(t, err, "invalid worksheet index") { + assert.EqualError(t, f.copySheet(0, -1), "sheet is not exist") + if !assert.EqualError(t, f.CopySheet(0, -1), "invalid worksheet index") { t.FailNow() } diff --git a/sheet.go b/sheet.go index 5e9fcae..b22592d 100644 --- a/sheet.go +++ b/sheet.go @@ -14,9 +14,11 @@ import ( "encoding/json" "encoding/xml" "errors" + "fmt" "io/ioutil" "os" "path" + "reflect" "regexp" "strconv" "strings" @@ -466,7 +468,7 @@ 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("sheet" + strconv.Itoa(from)) + sheet, err := f.workSheetReader(f.GetSheetName(from)) if err != nil { return err } @@ -761,6 +763,155 @@ func (f *File) SearchSheet(sheet, value string, reg ...bool) ([]string, error) { return result, nil } +// SetHeaderFooter provides a function to set headers and footers by given +// worksheet name and the control characters. +// +// Headers and footers are specified using the following settings fields: +// +// Fields | Description +// ------------------+----------------------------------------------------------- +// AlignWithMargins | Align header footer margins with page margins +// DifferentFirst | Different first-page header and footer indicator +// DifferentOddEven | Different odd and even page headers and footers indicator +// ScaleWithDoc | Scale header and footer with document scaling +// OddFooter | Odd Page Footer +// OddHeader | Odd Header +// EvenFooter | Even Page Footer +// EvenHeader | Even Page Header +// FirstFooter | First Page Footer +// FirstHeader | First Page Header +// +// The following formatting codes can be used in 6 string type fields: +// OddHeader, OddFooter, EvenHeader, EvenFooter, FirstFooter, FirstHeader +// +// Formatting Code | Description +// ------------------------+------------------------------------------------------------------------- +// && | The character "&" +// | +// &font-size | Size of the text font, where font-size is a decimal font size in points +// | +// &"font name,font type" | A text font-name string, font name, and a text font-type string, +// | font type +// | +// &"-,Regular" | Regular text format. Toggles bold and italic modes to off +// | +// &A | Current worksheet's tab name +// | +// &B or &"-,Bold" | Bold text format, from off to on, or vice versa. The default mode is off +// | +// &D | Current date +// | +// &C | Center section +// | +// &E | Double-underline text format +// | +// &F | Current workbook's file name +// | +// &G | Drawing object as background +// | +// &H | Shadow text format +// | +// &I or &"-,Italic" | Italic text format +// | +// &K | Text font color +// | +// | An RGB Color is specified as RRGGBB +// | +// | A Theme Color is specified as TTSNNN where TT is the theme color Id, +// | S is either "+" or "-" of the tint/shade value, and NNN is the +// | tint/shade value +// | +// &L | Left section +// | +// &N | Total number of pages +// | +// &O | Outline text format +// | +// &P[[+|-]n] | Without the optional suffix, the current page number in decimal +// | +// &R | Right section +// | +// &S | Strikethrough text format +// | +// &T | Current time +// | +// &U | Single-underline text format. If double-underline mode is on, the next +// | occurrence in a section specifier toggles double-underline mode to off; +// | otherwise, it toggles single-underline mode, from off to on, or vice +// | versa. The default mode is off +// | +// &X | Superscript text format +// | +// &Y | Subscript text format +// | +// &Z | Current workbook's file path +// +// For example: +// +// err := f.SetHeaderFooter("Sheet1", &excelize.FormatHeaderFooter{ +// DifferentFirst: true, +// DifferentOddEven: true, +// OddHeader: "&R&P", +// OddFooter: "&C&F", +// EvenHeader: "&L&P", +// EvenFooter: "&L&D&R&T", +// FirstHeader: `&CCenter &"-,Bold"Bold&"-,Regular"HeaderU+000A&D`, +// }) +// +// This example shows: +// +// - The first page has its own header and footer +// +// - Odd and even-numbered pages have different headers and footers +// +// - Current page number in the right section of odd-page headers +// +// - Current workbook's file name in the center section of odd-page footers +// +// - Current page number in the left section of even-page headers +// +// - Current date in the left section and the current time in the right section +// of even-page footers +// +// - The text "Center Bold Header" on the first line of the center section of +// the first page, and the date on the second line of the center section of +// that same page +// +// - No footer on the first page +// +func (f *File) SetHeaderFooter(sheet string, settings *FormatHeaderFooter) error { + xlsx, err := f.workSheetReader(sheet) + if err != nil { + return err + } + if settings == nil { + xlsx.HeaderFooter = nil + return err + } + + v := reflect.ValueOf(*settings) + // Check 6 string type fields: OddHeader, OddFooter, EvenHeader, EvenFooter, + // FirstFooter, FirstHeader + for i := 4; i < v.NumField()-1; i++ { + if v.Field(i).Len() >= 255 { + return fmt.Errorf("field %s must be less than 255 characters", v.Type().Field(i).Name) + } + } + xlsx.HeaderFooter = &xlsxHeaderFooter{ + AlignWithMargins: settings.AlignWithMargins, + DifferentFirst: settings.DifferentFirst, + DifferentOddEven: settings.DifferentOddEven, + ScaleWithDoc: settings.ScaleWithDoc, + OddHeader: settings.OddHeader, + OddFooter: settings.OddFooter, + EvenHeader: settings.EvenHeader, + EvenFooter: settings.EvenFooter, + FirstFooter: settings.FirstFooter, + FirstHeader: settings.FirstHeader, + } + return err +} + // ProtectSheet provides a function to prevent other users from accidentally // or deliberately changing, moving, or deleting data in a worksheet. For // example, protect Sheet1 with protection settings: @@ -898,7 +1049,7 @@ func (p *PageLayoutPaperSize) getPageLayout(ps *xlsxPageSetUp) { // // Available options: // PageLayoutOrientation(string) -// PageLayoutPaperSize(int) +// PageLayoutPaperSize(int) // // The following shows the paper size sorted by excelize index number: // diff --git a/sheet_test.go b/sheet_test.go index f0a1963..beee10b 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -2,6 +2,8 @@ package excelize_test import ( "fmt" + "path/filepath" + "strings" "testing" "github.com/360EntSecGroup-Skylar/excelize/v2" @@ -10,15 +12,15 @@ import ( ) func ExampleFile_SetPageLayout() { - xl := excelize.NewFile() + f := excelize.NewFile() - if err := xl.SetPageLayout( + if err := f.SetPageLayout( "Sheet1", excelize.PageLayoutOrientation(excelize.OrientationLandscape), ); err != nil { panic(err) } - if err := xl.SetPageLayout( + if err := f.SetPageLayout( "Sheet1", excelize.PageLayoutPaperSize(10), ); err != nil { @@ -28,15 +30,15 @@ func ExampleFile_SetPageLayout() { } func ExampleFile_GetPageLayout() { - xl := excelize.NewFile() + f := excelize.NewFile() var ( orientation excelize.PageLayoutOrientation paperSize excelize.PageLayoutPaperSize ) - if err := xl.GetPageLayout("Sheet1", &orientation); err != nil { + if err := f.GetPageLayout("Sheet1", &orientation); err != nil { panic(err) } - if err := xl.GetPageLayout("Sheet1", &paperSize); err != nil { + if err := f.GetPageLayout("Sheet1", &paperSize); err != nil { panic(err) } fmt.Println("Defaults:") @@ -69,26 +71,26 @@ func TestPageLayoutOption(t *testing.T) { val1 := deepcopy.Copy(def).(excelize.PageLayoutOptionPtr) val2 := deepcopy.Copy(def).(excelize.PageLayoutOptionPtr) - xl := excelize.NewFile() + f := excelize.NewFile() // Get the default value - assert.NoError(t, xl.GetPageLayout(sheet, def), opt) + assert.NoError(t, f.GetPageLayout(sheet, def), opt) // Get again and check - assert.NoError(t, xl.GetPageLayout(sheet, val1), opt) + assert.NoError(t, f.GetPageLayout(sheet, val1), opt) if !assert.Equal(t, val1, def, opt) { t.FailNow() } // Set the same value - assert.NoError(t, xl.SetPageLayout(sheet, val1), opt) + assert.NoError(t, f.SetPageLayout(sheet, val1), opt) // Get again and check - assert.NoError(t, xl.GetPageLayout(sheet, val1), opt) + assert.NoError(t, f.GetPageLayout(sheet, val1), opt) if !assert.Equal(t, val1, def, "%T: value should not have changed", opt) { t.FailNow() } // Set a different value - assert.NoError(t, xl.SetPageLayout(sheet, test.nonDefault), opt) - assert.NoError(t, xl.GetPageLayout(sheet, val1), opt) + assert.NoError(t, f.SetPageLayout(sheet, test.nonDefault), opt) + assert.NoError(t, f.GetPageLayout(sheet, val1), opt) // Get again and compare - assert.NoError(t, xl.GetPageLayout(sheet, val2), opt) + assert.NoError(t, f.GetPageLayout(sheet, val2), opt) if !assert.Equal(t, val1, val2, "%T: value should not have changed", opt) { t.FailNow() } @@ -97,8 +99,8 @@ func TestPageLayoutOption(t *testing.T) { t.FailNow() } // Restore the default value - assert.NoError(t, xl.SetPageLayout(sheet, def), opt) - assert.NoError(t, xl.GetPageLayout(sheet, val1), opt) + assert.NoError(t, f.SetPageLayout(sheet, def), opt) + assert.NoError(t, f.GetPageLayout(sheet, val1), opt) if !assert.Equal(t, def, val1) { t.FailNow() } @@ -117,3 +119,26 @@ func TestGetPageLayout(t *testing.T) { // Test get page layout on not exists worksheet. assert.EqualError(t, f.GetPageLayout("SheetN"), "sheet SheetN is not exist") } + +func TestSetHeaderFooter(t *testing.T) { + f := excelize.NewFile() + f.SetCellStr("Sheet1", "A1", "Test SetHeaderFooter") + // Test set header and footer on not exists worksheet. + assert.EqualError(t, f.SetHeaderFooter("SheetN", nil), "sheet SheetN is not exist") + // Test set header and footer with illegal setting. + assert.EqualError(t, f.SetHeaderFooter("Sheet1", &excelize.FormatHeaderFooter{ + OddHeader: strings.Repeat("c", 256), + }), "field OddHeader must be less than 255 characters") + + assert.NoError(t, f.SetHeaderFooter("Sheet1", nil)) + assert.NoError(t, f.SetHeaderFooter("Sheet1", &excelize.FormatHeaderFooter{ + DifferentFirst: true, + DifferentOddEven: true, + OddHeader: "&R&P", + OddFooter: "&C&F", + EvenHeader: "&L&P", + EvenFooter: "&L&D&R&T", + FirstHeader: `&CCenter &"-,Bold"Bold&"-,Regular"HeaderU+000A&D`, + })) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetHeaderFooter.xlsx"))) +} -- cgit v1.2.1 From 25763ba3e1af39bf2fd00bfa6aabcb054ca78327 Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 11 May 2019 09:46:20 +0800 Subject: fixed #373, comments duplicate caused by inner counting errors --- comment.go | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/comment.go b/comment.go index 79f6fec..3cf0c1d 100644 --- a/comment.go +++ b/comment.go @@ -277,13 +277,21 @@ func (f *File) addComment(commentsXML, cell string, formatSet *formatComment) { // countComments provides a function to get comments files count storage in // the folder xl. func (f *File) countComments() int { - count := 0 + c1, c2 := 0, 0 for k := range f.XLSX { if strings.Contains(k, "xl/comments") { - count++ + c1++ } } - return count + for rel := range f.Comments { + if strings.Contains(rel, "xl/comments") { + c2++ + } + } + if c1 < c2 { + return c2 + } + return c1 } // decodeVMLDrawingReader provides a function to get the pointer to the -- cgit v1.2.1 From 7e77e14814658486267e3f237f484fa8e63a0cd4 Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 16 May 2019 13:36:50 +0800 Subject: Resolve #397, support set style by columns --- col.go | 62 +++++++++++++++++++- col_test.go | 176 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ excelize_test.go | 157 ++----------------------------------------------- 3 files changed, 242 insertions(+), 153 deletions(-) create mode 100644 col_test.go diff --git a/col.go b/col.go index 6b73fdc..db3a901 100644 --- a/col.go +++ b/col.go @@ -9,7 +9,10 @@ package excelize -import "math" +import ( + "math" + "strings" +) // Define the default cell size and EMU unit of measurement. const ( @@ -155,6 +158,63 @@ func (f *File) SetColOutlineLevel(sheet, col string, level uint8) error { return err } +// SetColStyle provides a function to set style of columns by given worksheet +// name, columns range and style ID. +// +// For example set style of column H on Sheet1: +// +// err = f.SetColStyle("Sheet1", "H", style) +// +// Set style of columns C:F on Sheet1: +// +// err = f.SetColStyle("Sheet1", "C:F", style) +// +func (f *File) SetColStyle(sheet, columns string, styleID int) error { + xlsx, err := f.workSheetReader(sheet) + if err != nil { + return err + } + var c1, c2 string + var min, max int + cols := strings.Split(columns, ":") + c1 = cols[0] + min, err = ColumnNameToNumber(c1) + if err != nil { + return err + } + if len(cols) == 2 { + c2 = cols[1] + max, err = ColumnNameToNumber(c2) + if err != nil { + return err + } + } else { + max = min + } + if max < min { + min, max = max, min + } + if xlsx.Cols == nil { + xlsx.Cols = &xlsxCols{} + } + var find bool + for idx, col := range xlsx.Cols.Col { + if col.Min == min && col.Max == max { + xlsx.Cols.Col[idx].Style = styleID + find = true + } + } + if !find { + xlsx.Cols.Col = append(xlsx.Cols.Col, xlsxCol{ + Min: min, + Max: max, + Width: 9, + Style: styleID, + }) + } + return nil +} + // SetColWidth provides a function to set the width of a single column or // multiple columns. For example: // diff --git a/col_test.go b/col_test.go new file mode 100644 index 0000000..e3164d4 --- /dev/null +++ b/col_test.go @@ -0,0 +1,176 @@ +package excelize + +import ( + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestColumnVisibility(t *testing.T) { + t.Run("TestBook1", func(t *testing.T) { + f, err := prepareTestBook1() + if !assert.NoError(t, err) { + t.FailNow() + } + + assert.NoError(t, f.SetColVisible("Sheet1", "F", false)) + assert.NoError(t, f.SetColVisible("Sheet1", "F", true)) + visible, err := f.GetColVisible("Sheet1", "F") + assert.Equal(t, true, visible) + assert.NoError(t, err) + + // Test get column visiable on not exists worksheet. + _, err = f.GetColVisible("SheetN", "F") + assert.EqualError(t, err, "sheet SheetN is not exist") + + // Test get column visiable with illegal cell coordinates. + _, err = f.GetColVisible("Sheet1", "*") + assert.EqualError(t, err, `invalid column name "*"`) + assert.EqualError(t, f.SetColVisible("Sheet1", "*", false), `invalid column name "*"`) + + f.NewSheet("Sheet3") + assert.NoError(t, f.SetColVisible("Sheet3", "E", false)) + + assert.EqualError(t, f.SetColVisible("SheetN", "E", false), "sheet SheetN is not exist") + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestColumnVisibility.xlsx"))) + }) + + t.Run("TestBook3", func(t *testing.T) { + f, err := prepareTestBook3() + if !assert.NoError(t, err) { + t.FailNow() + } + f.GetColVisible("Sheet1", "B") + }) +} + +func TestOutlineLevel(t *testing.T) { + f := NewFile() + f.GetColOutlineLevel("Sheet1", "D") + f.NewSheet("Sheet2") + f.SetColOutlineLevel("Sheet1", "D", 4) + f.GetColOutlineLevel("Sheet1", "D") + f.GetColOutlineLevel("Shee2", "A") + f.SetColWidth("Sheet2", "A", "D", 13) + f.SetColOutlineLevel("Sheet2", "B", 2) + f.SetRowOutlineLevel("Sheet1", 2, 250) + + // Test set and get column outline level with illegal cell coordinates. + assert.EqualError(t, f.SetColOutlineLevel("Sheet1", "*", 1), `invalid column name "*"`) + _, err := f.GetColOutlineLevel("Sheet1", "*") + assert.EqualError(t, err, `invalid column name "*"`) + + // Test set column outline level on not exists worksheet. + assert.EqualError(t, f.SetColOutlineLevel("SheetN", "E", 2), "sheet SheetN is not exist") + + assert.EqualError(t, f.SetRowOutlineLevel("Sheet1", 0, 1), "invalid row number 0") + level, err := f.GetRowOutlineLevel("Sheet1", 2) + assert.NoError(t, err) + assert.Equal(t, uint8(250), level) + + _, err = f.GetRowOutlineLevel("Sheet1", 0) + assert.EqualError(t, err, `invalid row number 0`) + + level, err = f.GetRowOutlineLevel("Sheet1", 10) + assert.NoError(t, err) + assert.Equal(t, uint8(0), level) + + err = f.SaveAs(filepath.Join("test", "TestOutlineLevel.xlsx")) + if !assert.NoError(t, err) { + t.FailNow() + } + + f, err = OpenFile(filepath.Join("test", "Book1.xlsx")) + if !assert.NoError(t, err) { + t.FailNow() + } + f.SetColOutlineLevel("Sheet2", "B", 2) +} + +func TestSetColStyle(t *testing.T) { + f := NewFile() + style, err := f.NewStyle(`{"fill":{"type":"pattern","color":["#94d3a2"],"pattern":1}}`) + assert.NoError(t, err) + // Test set column style on not exists worksheet. + assert.EqualError(t, f.SetColStyle("SheetN", "E", style), "sheet SheetN is not exist") + // Test set column style with illegal cell coordinates. + assert.EqualError(t, f.SetColStyle("Sheet1", "*", style), `invalid column name "*"`) + assert.EqualError(t, f.SetColStyle("Sheet1", "A:*", style), `invalid column name "*"`) + + assert.NoError(t, f.SetColStyle("Sheet1", "B", style)) + // Test set column style with already exists column with style. + assert.NoError(t, f.SetColStyle("Sheet1", "B", style)) + assert.NoError(t, f.SetColStyle("Sheet1", "D:C", style)) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetColStyle.xlsx"))) +} + +func TestColWidth(t *testing.T) { + f := NewFile() + f.SetColWidth("Sheet1", "B", "A", 12) + f.SetColWidth("Sheet1", "A", "B", 12) + f.GetColWidth("Sheet1", "A") + f.GetColWidth("Sheet1", "C") + + // Test set and get column width with illegal cell coordinates. + _, err := f.GetColWidth("Sheet1", "*") + assert.EqualError(t, err, `invalid column name "*"`) + assert.EqualError(t, f.SetColWidth("Sheet1", "*", "B", 1), `invalid column name "*"`) + assert.EqualError(t, f.SetColWidth("Sheet1", "A", "*", 1), `invalid column name "*"`) + + // Test set column width on not exists worksheet. + assert.EqualError(t, f.SetColWidth("SheetN", "B", "A", 12), "sheet SheetN is not exist") + + // Test get column width on not exists worksheet. + _, err = f.GetColWidth("SheetN", "A") + assert.EqualError(t, err, "sheet SheetN is not exist") + + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestColWidth.xlsx"))) + convertRowHeightToPixels(0) +} + +func TestInsertCol(t *testing.T) { + f := NewFile() + sheet1 := f.GetSheetName(1) + + fillCells(f, sheet1, 10, 10) + + f.SetCellHyperLink(sheet1, "A5", "https://github.com/360EntSecGroup-Skylar/excelize", "External") + f.MergeCell(sheet1, "A1", "C3") + + err := f.AutoFilter(sheet1, "A2", "B2", `{"column":"B","expression":"x != blanks"}`) + if !assert.NoError(t, err) { + t.FailNow() + } + + assert.NoError(t, f.InsertCol(sheet1, "A")) + + // Test insert column with illegal cell coordinates. + assert.EqualError(t, f.InsertCol("Sheet1", "*"), `invalid column name "*"`) + + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestInsertCol.xlsx"))) +} + +func TestRemoveCol(t *testing.T) { + f := NewFile() + sheet1 := f.GetSheetName(1) + + fillCells(f, sheet1, 10, 15) + + f.SetCellHyperLink(sheet1, "A5", "https://github.com/360EntSecGroup-Skylar/excelize", "External") + f.SetCellHyperLink(sheet1, "C5", "https://github.com", "External") + + f.MergeCell(sheet1, "A1", "B1") + f.MergeCell(sheet1, "A2", "B2") + + assert.NoError(t, f.RemoveCol(sheet1, "A")) + assert.NoError(t, f.RemoveCol(sheet1, "A")) + + // Test remove column with illegal cell coordinates. + assert.EqualError(t, f.RemoveCol("Sheet1", "*"), `invalid column name "*"`) + + // Test remove column on not exists worksheet. + assert.EqualError(t, f.RemoveCol("SheetN", "B"), "sheet SheetN is not exist") + + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestRemoveCol.xlsx"))) +} diff --git a/excelize_test.go b/excelize_test.go index 3509cb8..85df09b 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -244,30 +244,6 @@ func TestNewFile(t *testing.T) { assert.NoError(t, f.SaveAs(filepath.Join("test", "TestNewFile.xlsx"))) } -func TestColWidth(t *testing.T) { - xlsx := NewFile() - xlsx.SetColWidth("Sheet1", "B", "A", 12) - xlsx.SetColWidth("Sheet1", "A", "B", 12) - xlsx.GetColWidth("Sheet1", "A") - xlsx.GetColWidth("Sheet1", "C") - - // Test set and get column width with illegal cell coordinates. - _, err := xlsx.GetColWidth("Sheet1", "*") - assert.EqualError(t, err, `invalid column name "*"`) - assert.EqualError(t, xlsx.SetColWidth("Sheet1", "*", "B", 1), `invalid column name "*"`) - assert.EqualError(t, xlsx.SetColWidth("Sheet1", "A", "*", 1), `invalid column name "*"`) - - // Test get column width on not exists worksheet. - _, err = xlsx.GetColWidth("SheetN", "A") - assert.EqualError(t, err, "sheet SheetN is not exist") - - err = xlsx.SaveAs(filepath.Join("test", "TestColWidth.xlsx")) - if err != nil { - t.Error(err) - } - convertRowHeightToPixels(0) -} - func TestAddDrawingVML(t *testing.T) { // Test addDrawingVML with illegal cell coordinates. f := NewFile() @@ -744,44 +720,6 @@ func TestSheetVisibility(t *testing.T) { assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSheetVisibility.xlsx"))) } -func TestColumnVisibility(t *testing.T) { - t.Run("TestBook1", func(t *testing.T) { - f, err := prepareTestBook1() - if !assert.NoError(t, err) { - t.FailNow() - } - - assert.NoError(t, f.SetColVisible("Sheet1", "F", false)) - assert.NoError(t, f.SetColVisible("Sheet1", "F", true)) - visible, err := f.GetColVisible("Sheet1", "F") - assert.Equal(t, true, visible) - assert.NoError(t, err) - - // Test get column visiable on not exists worksheet. - _, err = f.GetColVisible("SheetN", "F") - assert.EqualError(t, err, "sheet SheetN is not exist") - - // Test get column visiable with illegal cell coordinates. - _, err = f.GetColVisible("Sheet1", "*") - assert.EqualError(t, err, `invalid column name "*"`) - assert.EqualError(t, f.SetColVisible("Sheet1", "*", false), `invalid column name "*"`) - - f.NewSheet("Sheet3") - assert.NoError(t, f.SetColVisible("Sheet3", "E", false)) - - assert.EqualError(t, f.SetColVisible("SheetN", "E", false), "sheet SheetN is not exist") - assert.NoError(t, f.SaveAs(filepath.Join("test", "TestColumnVisibility.xlsx"))) - }) - - t.Run("TestBook3", func(t *testing.T) { - f, err := prepareTestBook3() - if !assert.NoError(t, err) { - t.FailNow() - } - f.GetColVisible("Sheet1", "B") - }) -} - func TestCopySheet(t *testing.T) { f, err := prepareTestBook1() if !assert.NoError(t, err) { @@ -870,8 +808,11 @@ func TestAddComments(t *testing.T) { } s := strings.Repeat("c", 32768) - f.AddComment("Sheet1", "A30", `{"author":"`+s+`","text":"`+s+`"}`) - f.AddComment("Sheet2", "B7", `{"author":"Excelize: ","text":"This is a comment."}`) + assert.NoError(t, f.AddComment("Sheet1", "A30", `{"author":"`+s+`","text":"`+s+`"}`)) + assert.NoError(t, f.AddComment("Sheet2", "B7", `{"author":"Excelize: ","text":"This is a comment."}`)) + + // Test add comment on not exists worksheet. + assert.EqualError(t, f.AddComment("SheetN", "B7", `{"author":"Excelize: ","text":"This is a comment."}`), "sheet SheetN is not exist") if assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddComments.xlsx"))) { assert.Len(t, f.GetComments(), 2) @@ -990,52 +931,6 @@ func TestAddChart(t *testing.T) { assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddChart.xlsx"))) } -func TestInsertCol(t *testing.T) { - f := NewFile() - sheet1 := f.GetSheetName(1) - - fillCells(f, sheet1, 10, 10) - - f.SetCellHyperLink(sheet1, "A5", "https://github.com/360EntSecGroup-Skylar/excelize", "External") - f.MergeCell(sheet1, "A1", "C3") - - err := f.AutoFilter(sheet1, "A2", "B2", `{"column":"B","expression":"x != blanks"}`) - if !assert.NoError(t, err) { - t.FailNow() - } - - assert.NoError(t, f.InsertCol(sheet1, "A")) - - // Test insert column with illegal cell coordinates. - assert.EqualError(t, f.InsertCol("Sheet1", "*"), `invalid column name "*"`) - - assert.NoError(t, f.SaveAs(filepath.Join("test", "TestInsertCol.xlsx"))) -} - -func TestRemoveCol(t *testing.T) { - f := NewFile() - sheet1 := f.GetSheetName(1) - - fillCells(f, sheet1, 10, 15) - - f.SetCellHyperLink(sheet1, "A5", "https://github.com/360EntSecGroup-Skylar/excelize", "External") - f.SetCellHyperLink(sheet1, "C5", "https://github.com", "External") - - f.MergeCell(sheet1, "A1", "B1") - f.MergeCell(sheet1, "A2", "B2") - - assert.NoError(t, f.RemoveCol(sheet1, "A")) - assert.NoError(t, f.RemoveCol(sheet1, "A")) - - // Test remove column with illegal cell coordinates. - assert.EqualError(t, f.RemoveCol("Sheet1", "*"), `invalid column name "*"`) - - // Test remove column on not exists worksheet. - assert.EqualError(t, f.RemoveCol("SheetN", "B"), "sheet SheetN is not exist") - - assert.NoError(t, f.SaveAs(filepath.Join("test", "TestRemoveCol.xlsx"))) -} - func TestSetPane(t *testing.T) { f := NewFile() f.SetPanes("Sheet1", `{"freeze":false,"split":false}`) @@ -1162,48 +1057,6 @@ func TestSetSheetRow(t *testing.T) { assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetSheetRow.xlsx"))) } -func TestOutlineLevel(t *testing.T) { - f := NewFile() - f.NewSheet("Sheet2") - f.SetColOutlineLevel("Sheet1", "D", 4) - f.GetColOutlineLevel("Sheet1", "D") - f.GetColOutlineLevel("Shee2", "A") - f.SetColWidth("Sheet2", "A", "D", 13) - f.SetColOutlineLevel("Sheet2", "B", 2) - f.SetRowOutlineLevel("Sheet1", 2, 250) - - // Test set and get column outline level with illegal cell coordinates. - assert.EqualError(t, f.SetColOutlineLevel("Sheet1", "*", 1), `invalid column name "*"`) - _, err := f.GetColOutlineLevel("Sheet1", "*") - assert.EqualError(t, err, `invalid column name "*"`) - - // Test set column outline level on not exists worksheet. - assert.EqualError(t, f.SetColOutlineLevel("SheetN", "E", 2), "sheet SheetN is not exist") - - assert.EqualError(t, f.SetRowOutlineLevel("Sheet1", 0, 1), "invalid row number 0") - level, err := f.GetRowOutlineLevel("Sheet1", 2) - assert.NoError(t, err) - assert.Equal(t, uint8(250), level) - - _, err = f.GetRowOutlineLevel("Sheet1", 0) - assert.EqualError(t, err, `invalid row number 0`) - - level, err = f.GetRowOutlineLevel("Sheet1", 10) - assert.NoError(t, err) - assert.Equal(t, uint8(0), level) - - err = f.SaveAs(filepath.Join("test", "TestOutlineLevel.xlsx")) - if !assert.NoError(t, err) { - t.FailNow() - } - - f, err = OpenFile(filepath.Join("test", "Book1.xlsx")) - if !assert.NoError(t, err) { - t.FailNow() - } - f.SetColOutlineLevel("Sheet2", "B", 2) -} - func TestThemeColor(t *testing.T) { t.Log(ThemeColor("000000", -0.1)) t.Log(ThemeColor("000000", 0)) -- cgit v1.2.1 From f91f548614a7182ce66d55d10ed311e9b7e08a2a Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 17 May 2019 22:58:12 +0800 Subject: Resolve #404, get sheet map by target rels. --- sheet.go | 16 +++++++++++----- styles.go | 9 +++------ xmlStyles.go | 13 ++++--------- xmlWorkbook.go | 5 ++--- 4 files changed, 20 insertions(+), 23 deletions(-) diff --git a/sheet.go b/sheet.go index b22592d..e873118 100644 --- a/sheet.go +++ b/sheet.go @@ -369,12 +369,18 @@ func (f *File) GetSheetMap() map[int]string { return sheetMap } -// getSheetMap provides a function to get worksheet name and XML file path map of -// XLSX. +// getSheetMap provides a function to get worksheet name and XML file path map +// of XLSX. func (f *File) getSheetMap() map[string]string { - maps := make(map[string]string) - for idx, name := range f.GetSheetMap() { - maps[name] = "xl/worksheets/sheet" + strconv.Itoa(idx) + ".xml" + content := f.workbookReader() + rels := f.workbookRelsReader() + maps := map[string]string{} + for _, v := range content.Sheets.Sheet { + for _, rel := range rels.Relationships { + if rel.ID == v.ID { + maps[v.Name] = fmt.Sprintf("xl/%s", rel.Target) + } + } } return maps } diff --git a/styles.go b/styles.go index d6d267d..fc8a290 100644 --- a/styles.go +++ b/styles.go @@ -1895,11 +1895,8 @@ func (f *File) NewStyle(style string) (int, error) { numFmtID := setNumFmt(s, fs) if fs.Font != nil { - font, _ := xml.Marshal(setFont(fs)) s.Fonts.Count++ - s.Fonts.Font = append(s.Fonts.Font, &xlsxFont{ - Font: string(font[6 : len(font)-7]), - }) + s.Fonts.Font = append(s.Fonts.Font, setFont(fs)) fontID = s.Fonts.Count - 1 } @@ -1950,7 +1947,7 @@ func (f *File) NewConditionalStyle(style string) (int, error) { // setFont provides a function to add font style by given cell format // settings. -func setFont(formatStyle *formatStyle) *font { +func setFont(formatStyle *formatStyle) *xlsxFont { fontUnderlineType := map[string]string{"single": "single", "double": "double"} if formatStyle.Font.Size < 1 { formatStyle.Font.Size = 11 @@ -1958,7 +1955,7 @@ func setFont(formatStyle *formatStyle) *font { if formatStyle.Font.Color == "" { formatStyle.Font.Color = "#000000" } - f := font{ + f := xlsxFont{ B: formatStyle.Font.Bold, I: formatStyle.Font.Italic, Sz: &attrValInt{Val: formatStyle.Font.Size}, diff --git a/xmlStyles.go b/xmlStyles.go index fc53f77..5c198e7 100644 --- a/xmlStyles.go +++ b/xmlStyles.go @@ -82,8 +82,9 @@ type xlsxFonts struct { Font []*xlsxFont `xml:"font"` } -// font directly maps the font element. -type font struct { +// xlsxFont directly maps the font element. This element defines the +// properties for one of the fonts used in this workbook. +type xlsxFont struct { Name *attrValString `xml:"name"` Charset *attrValInt `xml:"charset"` Family *attrValInt `xml:"family"` @@ -100,12 +101,6 @@ type font struct { Scheme *attrValString `xml:"scheme"` } -// xlsxFont directly maps the font element. This element defines the properties -// for one of the fonts used in this workbook. -type xlsxFont struct { - Font string `xml:",innerxml"` -} - // xlsxFills directly maps the fills element. This element defines the cell // fills portion of the Styles part, consisting of a sequence of fill records. A // cell fill consists of a background color, foreground color, and pattern to be @@ -262,7 +257,7 @@ type xlsxDxf struct { // dxf directly maps the dxf element. type dxf struct { - Font *font `xml:"font"` + Font *xlsxFont `xml:"font"` NumFmt *xlsxNumFmt `xml:"numFmt"` Fill *xlsxFill `xml:"fill"` Alignment *xlsxAlignment `xml:"alignment"` diff --git a/xmlWorkbook.go b/xmlWorkbook.go index ad66f42..90a1427 100644 --- a/xmlWorkbook.go +++ b/xmlWorkbook.go @@ -146,9 +146,8 @@ type xlsxSheets struct { Sheet []xlsxSheet `xml:"sheet"` } -// xlsxSheet directly maps the sheet element from the namespace -// http://schemas.openxmlformats.org/spreadsheetml/2006/main - currently I have -// not checked it for completeness - it does as much as I need. +// xlsxSheet defines a sheet in this workbook. Sheet data is stored in a +// separate part. type xlsxSheet struct { Name string `xml:"name,attr,omitempty"` SheetID int `xml:"sheetId,attr,omitempty"` -- cgit v1.2.1 From b1c9884f6d186bd1bfb4fc1d34061856345b8530 Mon Sep 17 00:00:00 2001 From: Harris Date: Thu, 25 Apr 2019 11:24:25 -0500 Subject: Add the ability to change the default font Closes #390 --- comment.go | 5 +++-- shape.go | 2 +- styles.go | 40 +++++++++++++++++++++++++++++++--------- styles_test.go | 30 +++++++++++++++++++++++++++++- templates.go | 2 +- 5 files changed, 65 insertions(+), 14 deletions(-) diff --git a/comment.go b/comment.go index 3cf0c1d..af70820 100644 --- a/comment.go +++ b/comment.go @@ -239,6 +239,7 @@ func (f *File) addComment(commentsXML, cell string, formatSet *formatComment) { }, } } + defaultFont := f.GetDefaultFont() cmt := xlsxComment{ Ref: cell, AuthorID: 0, @@ -251,7 +252,7 @@ func (f *File) addComment(commentsXML, cell string, formatSet *formatComment) { Color: &xlsxColor{ Indexed: 81, }, - RFont: &attrValString{Val: "Calibri"}, + RFont: &attrValString{Val: defaultFont}, Family: &attrValInt{Val: 2}, }, T: a, @@ -262,7 +263,7 @@ func (f *File) addComment(commentsXML, cell string, formatSet *formatComment) { Color: &xlsxColor{ Indexed: 81, }, - RFont: &attrValString{Val: "Calibri"}, + RFont: &attrValString{Val: defaultFont}, Family: &attrValInt{Val: 2}, }, T: t, diff --git a/shape.go b/shape.go index c90963c..7dc7021 100644 --- a/shape.go +++ b/shape.go @@ -381,7 +381,7 @@ func (f *File) addDrawingShape(sheet, drawingXML, cell string, formatSet *format Bold: false, Italic: false, Underline: "none", - Family: "Calibri", + Family: f.GetDefaultFont(), Size: 11, Color: "#000000", }, diff --git a/styles.go b/styles.go index fc8a290..e0e6f78 100644 --- a/styles.go +++ b/styles.go @@ -1896,7 +1896,7 @@ func (f *File) NewStyle(style string) (int, error) { if fs.Font != nil { s.Fonts.Count++ - s.Fonts.Font = append(s.Fonts.Font, setFont(fs)) + s.Fonts.Font = append(s.Fonts.Font, f.setFont(fs)) fontID = s.Fonts.Count - 1 } @@ -1932,7 +1932,7 @@ func (f *File) NewConditionalStyle(style string) (int, error) { Border: setBorders(fs), } if fs.Font != nil { - dxf.Font = setFont(fs) + dxf.Font = f.setFont(fs) } dxfStr, _ := xml.Marshal(dxf) if s.Dxfs == nil { @@ -1945,9 +1945,32 @@ func (f *File) NewConditionalStyle(style string) (int, error) { return s.Dxfs.Count - 1, nil } +// GetDefaultFont provides the default font name currently set in the workbook +// Documents generated by excelize start with Calibri +func (f *File) GetDefaultFont() string { + font := f.readDefaultFont() + return font.Name.Val +} + +// SetDefaultFont changes the default font in the workbook +func (f *File) SetDefaultFont(fontName string) { + font := f.readDefaultFont() + font.Name.Val = fontName + s := f.stylesReader() + s.Fonts.Font[0] = font + custom := true + s.CellStyles.CellStyle[0].CustomBuiltIn = &custom +} + +// readDefaultFont provides an unmarshalled font value +func (f *File) readDefaultFont() *xlsxFont { + s := f.stylesReader() + return s.Fonts.Font[0] +} + // setFont provides a function to add font style by given cell format // settings. -func setFont(formatStyle *formatStyle) *xlsxFont { +func (f *File) setFont(formatStyle *formatStyle) *xlsxFont { fontUnderlineType := map[string]string{"single": "single", "double": "double"} if formatStyle.Font.Size < 1 { formatStyle.Font.Size = 11 @@ -1955,7 +1978,7 @@ func setFont(formatStyle *formatStyle) *xlsxFont { if formatStyle.Font.Color == "" { formatStyle.Font.Color = "#000000" } - f := xlsxFont{ + fnt := xlsxFont{ B: formatStyle.Font.Bold, I: formatStyle.Font.Italic, Sz: &attrValInt{Val: formatStyle.Font.Size}, @@ -1963,15 +1986,14 @@ func setFont(formatStyle *formatStyle) *xlsxFont { Name: &attrValString{Val: formatStyle.Font.Family}, Family: &attrValInt{Val: 2}, } - if f.Name.Val == "" { - f.Name.Val = "Calibri" - f.Scheme = &attrValString{Val: "minor"} + if fnt.Name.Val == "" { + fnt.Name.Val = f.GetDefaultFont() } val, ok := fontUnderlineType[formatStyle.Font.Underline] if ok { - f.U = &attrValString{Val: val} + fnt.U = &attrValString{Val: val} } - return &f + return &fnt } // setNumFmt provides a function to check if number format code in the range diff --git a/styles_test.go b/styles_test.go index 54321bb..decfbb9 100644 --- a/styles_test.go +++ b/styles_test.go @@ -25,7 +25,7 @@ func TestStyleFill(t *testing.T) { xl := NewFile() styleID, err := xl.NewStyle(testCase.format) if err != nil { - t.Fatalf("%v", err) + t.Fatal(err) } styles := xl.stylesReader() @@ -165,3 +165,31 @@ func TestSetConditionalFormat(t *testing.T) { assert.EqualValues(t, testCase.rules, cf[0].CfRule, testCase.label) } } + +func TestNewStyle(t *testing.T) { + f := NewFile() + styleID, err := f.NewStyle(`{"font":{"bold":true,"italic":true,"family":"Berlin Sans FB Demi","size":36,"color":"#777777"}}`) + if err != nil { + t.Fatal(err) + } + styles := f.stylesReader() + fontID := styles.CellXfs.Xf[styleID].FontID + font := styles.Fonts.Font[fontID] + assert.Contains(t, font.Name.Val, "Berlin Sans FB Demi", "Stored font should contain font name") + assert.Equal(t, 2, styles.CellXfs.Count, "Should have 2 styles") +} + +func TestGetDefaultFont(t *testing.T) { + f := NewFile() + s := f.GetDefaultFont() + assert.Equal(t, s, "Calibri", "Default font should be Calibri") +} + +func TestSetDefaultFont(t *testing.T) { + f := NewFile() + f.SetDefaultFont("Ariel") + styles := f.stylesReader() + s := f.GetDefaultFont() + assert.Equal(t, s, "Ariel", "Default font should change to Ariel") + assert.Equal(t, *styles.CellStyles.CellStyle[0].CustomBuiltIn, true) +} diff --git a/templates.go b/templates.go index 17fc8d4..923cebd 100644 --- a/templates.go +++ b/templates.go @@ -27,7 +27,7 @@ const templateContentTypes = `` -const templateStyles = `` +const templateStyles = `` const templateSheet = `` -- cgit v1.2.1 From d038ca2e9c755abb6458606a90129559f14dc33b Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 1 Jun 2019 15:37:47 +0800 Subject: Fix #413, make pivot cache ID not omit empty --- xmlWorkbook.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xmlWorkbook.go b/xmlWorkbook.go index 90a1427..5384977 100644 --- a/xmlWorkbook.go +++ b/xmlWorkbook.go @@ -175,7 +175,7 @@ type xlsxPivotCaches struct { // xlsxPivotCache directly maps the pivotCache element. type xlsxPivotCache struct { - CacheID int `xml:"cacheId,attr,omitempty"` + CacheID int `xml:"cacheId,attr"` RID string `xml:"http://schemas.openxmlformats.org/officeDocument/2006/relationships id,attr,omitempty"` } -- cgit v1.2.1 From db99373b25a3f5c986d8f7d26ec983853574a17d Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 4 Jun 2019 23:30:55 +0800 Subject: Resolve #415, init set and get doc properties support --- docProps.go | 140 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ xmlCore.go | 89 +++++++++++++++++++++++++++++++++++++ xmlDrawing.go | 50 +++++++++++---------- 3 files changed, 256 insertions(+), 23 deletions(-) create mode 100755 docProps.go create mode 100755 xmlCore.go mode change 100644 => 100755 xmlDrawing.go diff --git a/docProps.go b/docProps.go new file mode 100755 index 0000000..0f44ac4 --- /dev/null +++ b/docProps.go @@ -0,0 +1,140 @@ +// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// this source code is governed by a BSD-style license that can be found in +// the LICENSE file. +// +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. + +package excelize + +import ( + "encoding/xml" + "reflect" +) + +// SetDocProps provides a function to set document core properties. The +// properties that can be set are: +// +// Property | Description +// ----------------+----------------------------------------------------------------------------- +// Title | The name given to the resource. +// | +// Subject | The topic of the content of the resource. +// | +// Creator | An entity primarily responsible for making the content of the resource. +// | +// Keywords | A delimited set of keywords to support searching and indexing. This is +// | typically a list of terms that are not available elsewhere in the properties. +// | +// Description | An explanation of the content of the resource. +// | +// LastModifiedBy | The user who performed the last modification. The identification is +// | environment-specific. +// | +// Language | The language of the intellectual content of the resource. +// | +// Identifier | An unambiguous reference to the resource within a given context. +// | +// Revision | The topic of the content of the resource. +// | +// ContentStatus | The status of the content. For example: Values might include "Draft", +// | "Reviewed", and "Final" +// | +// Category | A categorization of the content of this package. +// | +// Version | The version number. This value is set by the user or by the application. +// +// For example: +// +// err := f.SetDocProps(&excelize.DocProperties{ +// Category: "category", +// ContentStatus: "Draft", +// Created: "2019-06-04T22:00:10Z", +// Creator: "Go Excelize", +// Description: "This file created by Go Excelize", +// Identifier: "xlsx", +// Keywords: "Spreadsheet", +// LastModifiedBy: "Go Author", +// Modified: "2019-06-04T22:00:10Z", +// Revision: "0", +// Subject: "Test Subject", +// Title: "Test Title", +// Language: "en-US", +// Version: "1.0.0", +// }) +// +func (f *File) SetDocProps(docProperties *DocProperties) error { + core := decodeCoreProperties{} + err := xml.Unmarshal(namespaceStrictToTransitional(f.readXML("docProps/core.xml")), &core) + if err != nil { + return err + } + newProps := xlsxCoreProperties{ + Dc: NameSpaceDublinCore, + Dcterms: NameSpaceDublinCoreTerms, + Dcmitype: NameSpaceDublinCoreMetadataIntiative, + XSI: NameSpaceXMLSchemaInstance, + Title: core.Title, + Subject: core.Subject, + Creator: core.Creator, + Keywords: core.Keywords, + Description: core.Description, + LastModifiedBy: core.LastModifiedBy, + Language: core.Language, + Identifier: core.Identifier, + Revision: core.Revision, + ContentStatus: core.ContentStatus, + Category: core.Category, + Version: core.Version, + } + newProps.Created.Text = core.Created.Text + newProps.Created.Type = core.Created.Type + newProps.Modified.Text = core.Modified.Text + newProps.Modified.Type = core.Modified.Type + + fields := []string{"Category", "ContentStatus", "Creator", "Description", "Identifier", "Keywords", "LastModifiedBy", "Revision", "Subject", "Title", "Language", "Version"} + immutable := reflect.ValueOf(*docProperties) + mutable := reflect.ValueOf(&newProps).Elem() + for _, field := range fields { + val := immutable.FieldByName(field).String() + if val != "" { + mutable.FieldByName(field).SetString(val) + } + } + if docProperties.Created != "" { + newProps.Created.Text = docProperties.Created + } + if docProperties.Modified != "" { + newProps.Modified.Text = docProperties.Modified + } + output, err := xml.Marshal(&newProps) + f.saveFileList("docProps/core.xml", output) + return err +} + +// GetDocProps provides a function to get document core properties. +func (f *File) GetDocProps() (*DocProperties, error) { + core := decodeCoreProperties{} + err := xml.Unmarshal(namespaceStrictToTransitional(f.readXML("docProps/core.xml")), &core) + if err != nil { + return nil, err + } + return &DocProperties{ + Category: core.Category, + ContentStatus: core.ContentStatus, + Created: core.Created.Text, + Creator: core.Creator, + Description: core.Description, + Identifier: core.Identifier, + Keywords: core.Keywords, + LastModifiedBy: core.LastModifiedBy, + Modified: core.Modified.Text, + Revision: core.Revision, + Subject: core.Subject, + Title: core.Title, + Language: core.Language, + Version: core.Version, + }, nil +} diff --git a/xmlCore.go b/xmlCore.go new file mode 100755 index 0000000..357f688 --- /dev/null +++ b/xmlCore.go @@ -0,0 +1,89 @@ +// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// this source code is governed by a BSD-style license that can be found in +// the LICENSE file. +// +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. + +package excelize + +import "encoding/xml" + +// DocProperties directly maps the document core properties. +type DocProperties struct { + Category string + ContentStatus string + Created string + Creator string + Description string + Identifier string + Keywords string + LastModifiedBy string + Modified string + Revision string + Subject string + Title string + Language string + Version string +} + +// decodeCoreProperties directly maps the root element for a part of this +// content type shall coreProperties. In order to solve the problem that the +// label structure is changed after serialization and deserialization, two +// different structures are defined. decodeCoreProperties just for +// deserialization. +type decodeCoreProperties struct { + XMLName xml.Name `xml:"http://schemas.openxmlformats.org/package/2006/metadata/core-properties coreProperties"` + Title string `xml:"http://purl.org/dc/elements/1.1/ title,omitempty"` + Subject string `xml:"http://purl.org/dc/elements/1.1/ subject,omitempty"` + Creator string `xml:"http://purl.org/dc/elements/1.1/ creator"` + Keywords string `xml:"keywords,omitempty"` + Description string `xml:"http://purl.org/dc/elements/1.1/ description,omitempty"` + LastModifiedBy string `xml:"lastModifiedBy"` + Language string `xml:"http://purl.org/dc/elements/1.1/ language,omitempty"` + Identifier string `xml:"http://purl.org/dc/elements/1.1/ identifier,omitempty"` + Revision string `xml:"revision,omitempty"` + Created struct { + Text string `xml:",chardata"` + Type string `xml:"http://www.w3.org/2001/XMLSchema-instance type,attr"` + } `xml:"http://purl.org/dc/terms/ created"` + Modified struct { + Text string `xml:",chardata"` + Type string `xml:"http://www.w3.org/2001/XMLSchema-instance type,attr"` + } `xml:"http://purl.org/dc/terms/ modified"` + ContentStatus string `xml:"contentStatus,omitempty"` + Category string `xml:"category,omitempty"` + Version string `xml:"version,omitempty"` +} + +// xlsxCoreProperties directly maps the root element for a part of this +// content type shall coreProperties. +type xlsxCoreProperties struct { + XMLName xml.Name `xml:"http://schemas.openxmlformats.org/package/2006/metadata/core-properties coreProperties"` + Dc string `xml:"xmlns:dc,attr"` + Dcterms string `xml:"xmlns:dcterms,attr"` + Dcmitype string `xml:"xmlns:dcmitype,attr"` + XSI string `xml:"xmlns:xsi,attr"` + Title string `xml:"dc:title,omitempty"` + Subject string `xml:"dc:subject,omitempty"` + Creator string `xml:"dc:creator"` + Keywords string `xml:"keywords,omitempty"` + Description string `xml:"dc:description,omitempty"` + LastModifiedBy string `xml:"lastModifiedBy"` + Language string `xml:"dc:language,omitempty"` + Identifier string `xml:"dc:identifier,omitempty"` + Revision string `xml:"revision,omitempty"` + Created struct { + Text string `xml:",chardata"` + Type string `xml:"xsi:type,attr"` + } `xml:"dcterms:created"` + Modified struct { + Text string `xml:",chardata"` + Type string `xml:"xsi:type,attr"` + } `xml:"dcterms:modified"` + ContentStatus string `xml:"contentStatus,omitempty"` + Category string `xml:"category,omitempty"` + Version string `xml:"version,omitempty"` +} diff --git a/xmlDrawing.go b/xmlDrawing.go old mode 100644 new mode 100755 index 89496c4..13e164e --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -13,29 +13,33 @@ import "encoding/xml" // Source relationship and namespace. const ( - SourceRelationship = "http://schemas.openxmlformats.org/officeDocument/2006/relationships" - SourceRelationshipChart = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart" - SourceRelationshipComments = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments" - SourceRelationshipImage = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" - SourceRelationshipTable = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/table" - SourceRelationshipDrawingML = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing" - SourceRelationshipDrawingVML = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing" - SourceRelationshipHyperLink = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink" - SourceRelationshipWorkSheet = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" - SourceRelationshipChart201506 = "http://schemas.microsoft.com/office/drawing/2015/06/chart" - SourceRelationshipChart20070802 = "http://schemas.microsoft.com/office/drawing/2007/8/2/chart" - SourceRelationshipChart2014 = "http://schemas.microsoft.com/office/drawing/2014/chart" - SourceRelationshipCompatibility = "http://schemas.openxmlformats.org/markup-compatibility/2006" - NameSpaceDrawingML = "http://schemas.openxmlformats.org/drawingml/2006/main" - NameSpaceDrawingMLChart = "http://schemas.openxmlformats.org/drawingml/2006/chart" - NameSpaceDrawingMLSpreadSheet = "http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing" - NameSpaceSpreadSheet = "http://schemas.openxmlformats.org/spreadsheetml/2006/main" - NameSpaceXML = "http://www.w3.org/XML/1998/namespace" - StrictSourceRelationship = "http://purl.oclc.org/ooxml/officeDocument/relationships" - StrictSourceRelationshipChart = "http://purl.oclc.org/ooxml/officeDocument/relationships/chart" - StrictSourceRelationshipComments = "http://purl.oclc.org/ooxml/officeDocument/relationships/comments" - StrictSourceRelationshipImage = "http://purl.oclc.org/ooxml/officeDocument/relationships/image" - StrictNameSpaceSpreadSheet = "http://purl.oclc.org/ooxml/spreadsheetml/main" + SourceRelationship = "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + SourceRelationshipChart = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart" + SourceRelationshipComments = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments" + SourceRelationshipImage = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" + SourceRelationshipTable = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/table" + SourceRelationshipDrawingML = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing" + SourceRelationshipDrawingVML = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing" + SourceRelationshipHyperLink = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink" + SourceRelationshipWorkSheet = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" + SourceRelationshipChart201506 = "http://schemas.microsoft.com/office/drawing/2015/06/chart" + SourceRelationshipChart20070802 = "http://schemas.microsoft.com/office/drawing/2007/8/2/chart" + SourceRelationshipChart2014 = "http://schemas.microsoft.com/office/drawing/2014/chart" + SourceRelationshipCompatibility = "http://schemas.openxmlformats.org/markup-compatibility/2006" + NameSpaceDrawingML = "http://schemas.openxmlformats.org/drawingml/2006/main" + NameSpaceDrawingMLChart = "http://schemas.openxmlformats.org/drawingml/2006/chart" + NameSpaceDrawingMLSpreadSheet = "http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing" + NameSpaceSpreadSheet = "http://schemas.openxmlformats.org/spreadsheetml/2006/main" + NameSpaceXML = "http://www.w3.org/XML/1998/namespace" + NameSpaceXMLSchemaInstance = "http://www.w3.org/2001/XMLSchema-instance" + StrictSourceRelationship = "http://purl.oclc.org/ooxml/officeDocument/relationships" + StrictSourceRelationshipChart = "http://purl.oclc.org/ooxml/officeDocument/relationships/chart" + StrictSourceRelationshipComments = "http://purl.oclc.org/ooxml/officeDocument/relationships/comments" + StrictSourceRelationshipImage = "http://purl.oclc.org/ooxml/officeDocument/relationships/image" + StrictNameSpaceSpreadSheet = "http://purl.oclc.org/ooxml/spreadsheetml/main" + NameSpaceDublinCore = "http://purl.org/dc/elements/1.1/" + NameSpaceDublinCoreTerms = "http://purl.org/dc/terms/" + NameSpaceDublinCoreMetadataIntiative = "http://purl.org/dc/dcmitype/" ) var supportImageTypes = map[string]string{".gif": ".gif", ".jpg": ".jpeg", ".jpeg": ".jpeg", ".png": ".png"} -- cgit v1.2.1 From cff16fa8118291fd885f3f3f75fa07e28bba5eec Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 5 Jun 2019 22:06:15 +0800 Subject: - Supplemental worksheet struct fields and field order adjustment - Testing case for set and get doc properties - Update charts struct XML tags --- cellmerged.go | 14 ++- docProps.go | 0 docProps_test.go | 56 ++++++++++ excelize_test.go | 2 +- xmlChart.go | 328 +++++++++++++++++++++++++++---------------------------- xmlCore.go | 0 xmlDrawing.go | 0 xmlWorksheet.go | 86 ++++++++++++--- 8 files changed, 306 insertions(+), 180 deletions(-) mode change 100755 => 100644 docProps.go create mode 100644 docProps_test.go mode change 100755 => 100644 xmlCore.go mode change 100755 => 100644 xmlDrawing.go diff --git a/cellmerged.go b/cellmerged.go index 5392463..a78b244 100644 --- a/cellmerged.go +++ b/cellmerged.go @@ -1,8 +1,18 @@ +// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// this source code is governed by a BSD-style license that can be found in +// the LICENSE file. +// +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. + package excelize import "strings" -// GetMergeCells provides a function to get all merged cells from a worksheet currently. +// GetMergeCells provides a function to get all merged cells from a worksheet +// currently. func (f *File) GetMergeCells(sheet string) ([]MergeCell, error) { var mergeCells []MergeCell xlsx, err := f.workSheetReader(sheet) @@ -45,4 +55,4 @@ func (m *MergeCell) GetStartAxis() string { func (m *MergeCell) GetEndAxis() string { axis := strings.Split((*m)[0], ":") return axis[1] -} \ No newline at end of file +} diff --git a/docProps.go b/docProps.go old mode 100755 new mode 100644 diff --git a/docProps_test.go b/docProps_test.go new file mode 100644 index 0000000..1f52beb --- /dev/null +++ b/docProps_test.go @@ -0,0 +1,56 @@ +// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// this source code is governed by a BSD-style license that can be found in +// the LICENSE file. +// +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. + +package excelize + +import ( + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSetDocProps(t *testing.T) { + f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) + if !assert.NoError(t, err) { + t.FailNow() + } + assert.NoError(t, f.SetDocProps(&DocProperties{ + Category: "category", + ContentStatus: "Draft", + Created: "2019-06-04T22:00:10Z", + Creator: "Go Excelize", + Description: "This file created by Go Excelize", + Identifier: "xlsx", + Keywords: "Spreadsheet", + LastModifiedBy: "Go Author", + Modified: "2019-06-04T22:00:10Z", + Revision: "0", + Subject: "Test Subject", + Title: "Test Title", + Language: "en-US", + Version: "1.0.0", + })) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetDocProps.xlsx"))) + f.XLSX["docProps/core.xml"] = nil + assert.EqualError(t, f.SetDocProps(&DocProperties{}), "EOF") +} + +func TestGetDocProps(t *testing.T) { + f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) + if !assert.NoError(t, err) { + t.FailNow() + } + props, err := f.GetDocProps() + assert.NoError(t, err) + assert.Equal(t, props.Creator, "Microsoft Office User") + f.XLSX["docProps/core.xml"] = nil + _, err = f.GetDocProps() + assert.EqualError(t, err, "EOF") +} diff --git a/excelize_test.go b/excelize_test.go index 85df09b..1a3dde6 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -192,7 +192,7 @@ func TestBrokenFile(t *testing.T) { t.Run("SaveAsEmptyStruct", func(t *testing.T) { // Test write file with broken file struct with given path. - assert.NoError(t, f.SaveAs(filepath.Join("test", "TestBrokenFile.SaveAsEmptyStruct.xlsx"))) + assert.NoError(t, f.SaveAs(filepath.Join("test", "BrokenFile.SaveAsEmptyStruct.xlsx"))) }) t.Run("OpenBadWorkbook", func(t *testing.T) { diff --git a/xmlChart.go b/xmlChart.go index 163812d..972ead3 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -11,66 +11,66 @@ package excelize import "encoding/xml" -// xlsxChartSpace directly maps the c:chartSpace element. The chart namespace in +// xlsxChartSpace directly maps the chartSpace element. The chart namespace in // DrawingML is for representing visualizations of numeric data with column // charts, pie charts, scatter charts, or other types of charts. type xlsxChartSpace struct { - XMLName xml.Name `xml:"c:chartSpace"` + XMLName xml.Name `xml:"http://schemas.openxmlformats.org/drawingml/2006/chart chartSpace"` XMLNSc string `xml:"xmlns:c,attr"` XMLNSa string `xml:"xmlns:a,attr"` XMLNSr string `xml:"xmlns:r,attr"` XMLNSc16r2 string `xml:"xmlns:c16r2,attr"` - Date1904 *attrValBool `xml:"c:date1904"` - Lang *attrValString `xml:"c:lang"` - RoundedCorners *attrValBool `xml:"c:roundedCorners"` - Chart cChart `xml:"c:chart"` - SpPr *cSpPr `xml:"c:spPr"` - TxPr *cTxPr `xml:"c:txPr"` - PrintSettings *cPrintSettings `xml:"c:printSettings"` -} - -// cThicknessSpPr directly maps the element that specifies the thickness of the -// walls or floor as a percentage of the largest dimension of the plot volume -// and SpPr element. + Date1904 *attrValBool `xml:"date1904"` + Lang *attrValString `xml:"lang"` + RoundedCorners *attrValBool `xml:"roundedCorners"` + Chart cChart `xml:"chart"` + SpPr *cSpPr `xml:"spPr"` + TxPr *cTxPr `xml:"txPr"` + PrintSettings *cPrintSettings `xml:"printSettings"` +} + +// cThicknessSpPr directly maps the element that specifies the thickness of +// the walls or floor as a percentage of the largest dimension of the plot +// volume and SpPr element. type cThicknessSpPr struct { - Thickness *attrValInt `xml:"c:thickness"` - SpPr *cSpPr `xml:"c:spPr"` + Thickness *attrValInt `xml:"thickness"` + SpPr *cSpPr `xml:"spPr"` } -// cChart (Chart) directly maps the c:chart element. This element specifies a +// cChart (Chart) directly maps the chart element. This element specifies a // title. type cChart struct { - Title *cTitle `xml:"c:title"` - AutoTitleDeleted *cAutoTitleDeleted `xml:"c:autoTitleDeleted"` - View3D *cView3D `xml:"c:view3D"` - Floor *cThicknessSpPr `xml:"c:floor"` - SideWall *cThicknessSpPr `xml:"c:sideWall"` - BackWall *cThicknessSpPr `xml:"c:backWall"` - PlotArea *cPlotArea `xml:"c:plotArea"` - Legend *cLegend `xml:"c:legend"` - PlotVisOnly *attrValBool `xml:"c:plotVisOnly"` - DispBlanksAs *attrValString `xml:"c:dispBlanksAs"` - ShowDLblsOverMax *attrValBool `xml:"c:showDLblsOverMax"` -} - -// cTitle (Title) directly maps the c:title element. This element specifies a + Title *cTitle `xml:"title"` + AutoTitleDeleted *cAutoTitleDeleted `xml:"autoTitleDeleted"` + View3D *cView3D `xml:"view3D"` + Floor *cThicknessSpPr `xml:"floor"` + SideWall *cThicknessSpPr `xml:"sideWall"` + BackWall *cThicknessSpPr `xml:"backWall"` + PlotArea *cPlotArea `xml:"plotArea"` + Legend *cLegend `xml:"legend"` + PlotVisOnly *attrValBool `xml:"plotVisOnly"` + DispBlanksAs *attrValString `xml:"dispBlanksAs"` + ShowDLblsOverMax *attrValBool `xml:"showDLblsOverMax"` +} + +// cTitle (Title) directly maps the title element. This element specifies a // title. type cTitle struct { - Tx cTx `xml:"c:tx,omitempty"` - Layout string `xml:"c:layout,omitempty"` - Overlay attrValBool `xml:"c:overlay,omitempty"` - SpPr cSpPr `xml:"c:spPr,omitempty"` - TxPr cTxPr `xml:"c:txPr,omitempty"` + Tx cTx `xml:"tx,omitempty"` + Layout string `xml:"layout,omitempty"` + Overlay attrValBool `xml:"overlay,omitempty"` + SpPr cSpPr `xml:"spPr,omitempty"` + TxPr cTxPr `xml:"txPr,omitempty"` } -// cTx (Chart Text) directly maps the c:tx element. This element specifies text +// cTx (Chart Text) directly maps the tx element. This element specifies text // to use on a chart, including rich text formatting. type cTx struct { - StrRef *cStrRef `xml:"c:strRef"` - Rich *cRich `xml:"c:rich,omitempty"` + StrRef *cStrRef `xml:"strRef"` + Rich *cRich `xml:"rich,omitempty"` } -// cRich (Rich Text) directly maps the c:rich element. This element contains a +// cRich (Rich Text) directly maps the rich element. This element contains a // string with rich text formatting. type cRich struct { BodyPr aBodyPr `xml:"a:bodyPr,omitempty"` @@ -186,7 +186,7 @@ type aR struct { T string `xml:"a:t,omitempty"` } -// aRPr (Run Properties) directly maps the c:rPr element. This element +// aRPr (Run Properties) directly maps the rPr element. This element // specifies a set of run properties which shall be applied to the contents of // the parent run after all style formatting has been applied to the text. These // properties are defined as direct formatting, since they are directly applied @@ -217,7 +217,7 @@ type aRPr struct { Cs *aCs `xml:"a:cs"` } -// cSpPr (Shape Properties) directly maps the c:spPr element. This element +// cSpPr (Shape Properties) directly maps the spPr element. This element // specifies the visual shape properties that can be applied to a shape. These // properties include the shape fill, outline, geometry, effects, and 3D // orientation. @@ -259,7 +259,7 @@ type aLn struct { SolidFill *aSolidFill `xml:"a:solidFill"` } -// cTxPr (Text Properties) directly maps the c:txPr element. This element +// cTxPr (Text Properties) directly maps the txPr element. This element // specifies text formatting. The lstStyle element is not supported. type cTxPr struct { BodyPr aBodyPr `xml:"a:bodyPr,omitempty"` @@ -282,207 +282,207 @@ type aEndParaRPr struct { } // cAutoTitleDeleted (Auto Title Is Deleted) directly maps the -// c:autoTitleDeleted element. This element specifies the title shall not be +// autoTitleDeleted element. This element specifies the title shall not be // shown for this chart. type cAutoTitleDeleted struct { Val bool `xml:"val,attr"` } -// cView3D (View In 3D) directly maps the c:view3D element. This element +// cView3D (View In 3D) directly maps the view3D element. This element // specifies the 3-D view of the chart. type cView3D struct { - RotX *attrValInt `xml:"c:rotX"` - RotY *attrValInt `xml:"c:rotY"` - DepthPercent *attrValInt `xml:"c:depthPercent"` - RAngAx *attrValInt `xml:"c:rAngAx"` + RotX *attrValInt `xml:"rotX"` + RotY *attrValInt `xml:"rotY"` + DepthPercent *attrValInt `xml:"depthPercent"` + RAngAx *attrValInt `xml:"rAngAx"` } -// cPlotArea directly maps the c:plotArea element. This element specifies the +// cPlotArea directly maps the plotArea element. This element specifies the // plot area of the chart. type cPlotArea struct { - Layout *string `xml:"c:layout"` - AreaChart *cCharts `xml:"c:areaChart"` - Area3DChart *cCharts `xml:"c:area3DChart"` - BarChart *cCharts `xml:"c:barChart"` - Bar3DChart *cCharts `xml:"c:bar3DChart"` - DoughnutChart *cCharts `xml:"c:doughnutChart"` - LineChart *cCharts `xml:"c:lineChart"` - PieChart *cCharts `xml:"c:pieChart"` - Pie3DChart *cCharts `xml:"c:pie3DChart"` - RadarChart *cCharts `xml:"c:radarChart"` - ScatterChart *cCharts `xml:"c:scatterChart"` - CatAx []*cAxs `xml:"c:catAx"` - ValAx []*cAxs `xml:"c:valAx"` - SpPr *cSpPr `xml:"c:spPr"` + Layout *string `xml:"layout"` + AreaChart *cCharts `xml:"areaChart"` + Area3DChart *cCharts `xml:"area3DChart"` + BarChart *cCharts `xml:"barChart"` + Bar3DChart *cCharts `xml:"bar3DChart"` + DoughnutChart *cCharts `xml:"doughnutChart"` + LineChart *cCharts `xml:"lineChart"` + PieChart *cCharts `xml:"pieChart"` + Pie3DChart *cCharts `xml:"pie3DChart"` + RadarChart *cCharts `xml:"radarChart"` + ScatterChart *cCharts `xml:"scatterChart"` + CatAx []*cAxs `xml:"catAx"` + ValAx []*cAxs `xml:"valAx"` + SpPr *cSpPr `xml:"spPr"` } // cCharts specifies the common element of the chart. type cCharts struct { - BarDir *attrValString `xml:"c:barDir"` - Grouping *attrValString `xml:"c:grouping"` - RadarStyle *attrValString `xml:"c:radarStyle"` - ScatterStyle *attrValString `xml:"c:scatterStyle"` - VaryColors *attrValBool `xml:"c:varyColors"` - Ser *[]cSer `xml:"c:ser"` - DLbls *cDLbls `xml:"c:dLbls"` - HoleSize *attrValInt `xml:"c:holeSize"` - Smooth *attrValBool `xml:"c:smooth"` - Overlap *attrValInt `xml:"c:overlap"` - AxID []*attrValInt `xml:"c:axId"` -} - -// cAxs directly maps the c:catAx and c:valAx element. + BarDir *attrValString `xml:"barDir"` + Grouping *attrValString `xml:"grouping"` + RadarStyle *attrValString `xml:"radarStyle"` + ScatterStyle *attrValString `xml:"scatterStyle"` + VaryColors *attrValBool `xml:"varyColors"` + Ser *[]cSer `xml:"ser"` + DLbls *cDLbls `xml:"dLbls"` + HoleSize *attrValInt `xml:"holeSize"` + Smooth *attrValBool `xml:"smooth"` + Overlap *attrValInt `xml:"overlap"` + AxID []*attrValInt `xml:"axId"` +} + +// cAxs directly maps the catAx and valAx element. type cAxs struct { - AxID *attrValInt `xml:"c:axId"` - Scaling *cScaling `xml:"c:scaling"` - Delete *attrValBool `xml:"c:delete"` - AxPos *attrValString `xml:"c:axPos"` - NumFmt *cNumFmt `xml:"c:numFmt"` - MajorTickMark *attrValString `xml:"c:majorTickMark"` - MinorTickMark *attrValString `xml:"c:minorTickMark"` - TickLblPos *attrValString `xml:"c:tickLblPos"` - SpPr *cSpPr `xml:"c:spPr"` - TxPr *cTxPr `xml:"c:txPr"` - CrossAx *attrValInt `xml:"c:crossAx"` - Crosses *attrValString `xml:"c:crosses"` - CrossBetween *attrValString `xml:"c:crossBetween"` - Auto *attrValBool `xml:"c:auto"` - LblAlgn *attrValString `xml:"c:lblAlgn"` - LblOffset *attrValInt `xml:"c:lblOffset"` - NoMultiLvlLbl *attrValBool `xml:"c:noMultiLvlLbl"` -} - -// cScaling directly maps the c:scaling element. This element contains + AxID *attrValInt `xml:"axId"` + Scaling *cScaling `xml:"scaling"` + Delete *attrValBool `xml:"delete"` + AxPos *attrValString `xml:"axPos"` + NumFmt *cNumFmt `xml:"numFmt"` + MajorTickMark *attrValString `xml:"majorTickMark"` + MinorTickMark *attrValString `xml:"minorTickMark"` + TickLblPos *attrValString `xml:"tickLblPos"` + SpPr *cSpPr `xml:"spPr"` + TxPr *cTxPr `xml:"txPr"` + CrossAx *attrValInt `xml:"crossAx"` + Crosses *attrValString `xml:"crosses"` + CrossBetween *attrValString `xml:"crossBetween"` + Auto *attrValBool `xml:"auto"` + LblAlgn *attrValString `xml:"lblAlgn"` + LblOffset *attrValInt `xml:"lblOffset"` + NoMultiLvlLbl *attrValBool `xml:"noMultiLvlLbl"` +} + +// cScaling directly maps the scaling element. This element contains // additional axis settings. type cScaling struct { - Orientation *attrValString `xml:"c:orientation"` - Max *attrValFloat `xml:"c:max"` - Min *attrValFloat `xml:"c:min"` + Orientation *attrValString `xml:"orientation"` + Max *attrValFloat `xml:"max"` + Min *attrValFloat `xml:"min"` } -// cNumFmt (Numbering Format) directly maps the c:numFmt element. This element +// cNumFmt (Numbering Format) directly maps the numFmt element. This element // specifies number formatting for the parent element. type cNumFmt struct { FormatCode string `xml:"formatCode,attr"` SourceLinked bool `xml:"sourceLinked,attr"` } -// cSer directly maps the c:ser element. This element specifies a series on a +// cSer directly maps the ser element. This element specifies a series on a // chart. type cSer struct { - IDx *attrValInt `xml:"c:idx"` - Order *attrValInt `xml:"c:order"` - Tx *cTx `xml:"c:tx"` - SpPr *cSpPr `xml:"c:spPr"` - DPt []*cDPt `xml:"c:dPt"` - DLbls *cDLbls `xml:"c:dLbls"` - Marker *cMarker `xml:"c:marker"` - InvertIfNegative *attrValBool `xml:"c:invertIfNegative"` - Cat *cCat `xml:"c:cat"` - Val *cVal `xml:"c:val"` - XVal *cCat `xml:"c:xVal"` - YVal *cVal `xml:"c:yVal"` - Smooth *attrValBool `xml:"c:smooth"` -} - -// cMarker (Marker) directly maps the c:marker element. This element specifies a + IDx *attrValInt `xml:"idx"` + Order *attrValInt `xml:"order"` + Tx *cTx `xml:"tx"` + SpPr *cSpPr `xml:"spPr"` + DPt []*cDPt `xml:"dPt"` + DLbls *cDLbls `xml:"dLbls"` + Marker *cMarker `xml:"marker"` + InvertIfNegative *attrValBool `xml:"invertIfNegative"` + Cat *cCat `xml:"cat"` + Val *cVal `xml:"val"` + XVal *cCat `xml:"xVal"` + YVal *cVal `xml:"yVal"` + Smooth *attrValBool `xml:"smooth"` +} + +// cMarker (Marker) directly maps the marker element. This element specifies a // data marker. type cMarker struct { - Symbol *attrValString `xml:"c:symbol"` - Size *attrValInt `xml:"c:size"` - SpPr *cSpPr `xml:"c:spPr"` + Symbol *attrValString `xml:"symbol"` + Size *attrValInt `xml:"size"` + SpPr *cSpPr `xml:"spPr"` } -// cDPt (Data Point) directly maps the c:dPt element. This element specifies a +// cDPt (Data Point) directly maps the dPt element. This element specifies a // single data point. type cDPt struct { - IDx *attrValInt `xml:"c:idx"` - Bubble3D *attrValBool `xml:"c:bubble3D"` - SpPr *cSpPr `xml:"c:spPr"` + IDx *attrValInt `xml:"idx"` + Bubble3D *attrValBool `xml:"bubble3D"` + SpPr *cSpPr `xml:"spPr"` } -// cCat (Category Axis Data) directly maps the c:cat element. This element +// cCat (Category Axis Data) directly maps the cat element. This element // specifies the data used for the category axis. type cCat struct { - StrRef *cStrRef `xml:"c:strRef"` + StrRef *cStrRef `xml:"strRef"` } -// cStrRef (String Reference) directly maps the c:strRef element. This element +// cStrRef (String Reference) directly maps the strRef element. This element // specifies a reference to data for a single data label or title with a cache // of the last values used. type cStrRef struct { - F string `xml:"c:f"` - StrCache *cStrCache `xml:"c:strCache"` + F string `xml:"f"` + StrCache *cStrCache `xml:"strCache"` } -// cStrCache (String Cache) directly maps the c:strCache element. This element +// cStrCache (String Cache) directly maps the strCache element. This element // specifies the last string data used for a chart. type cStrCache struct { - Pt []*cPt `xml:"c:pt"` - PtCount *attrValInt `xml:"c:ptCount"` + Pt []*cPt `xml:"pt"` + PtCount *attrValInt `xml:"ptCount"` } -// cPt directly maps the c:pt element. This element specifies data for a +// cPt directly maps the pt element. This element specifies data for a // particular data point. type cPt struct { IDx int `xml:"idx,attr"` - V *string `xml:"c:v"` + V *string `xml:"v"` } -// cVal directly maps the c:val element. This element specifies the data values +// cVal directly maps the val element. This element specifies the data values // which shall be used to define the location of data markers on a chart. type cVal struct { - NumRef *cNumRef `xml:"c:numRef"` + NumRef *cNumRef `xml:"numRef"` } -// cNumRef directly maps the c:numRef element. This element specifies a +// cNumRef directly maps the numRef element. This element specifies a // reference to numeric data with a cache of the last values used. type cNumRef struct { - F string `xml:"c:f"` - NumCache *cNumCache `xml:"c:numCache"` + F string `xml:"f"` + NumCache *cNumCache `xml:"numCache"` } -// cNumCache directly maps the c:numCache element. This element specifies the +// cNumCache directly maps the numCache element. This element specifies the // last data shown on the chart for a series. type cNumCache struct { - FormatCode string `xml:"c:formatCode"` - Pt []*cPt `xml:"c:pt"` - PtCount *attrValInt `xml:"c:ptCount"` + FormatCode string `xml:"formatCode"` + Pt []*cPt `xml:"pt"` + PtCount *attrValInt `xml:"ptCount"` } -// cDLbls (Data Lables) directly maps the c:dLbls element. This element serves +// cDLbls (Data Lables) directly maps the dLbls element. This element serves // as a root element that specifies the settings for the data labels for an // entire series or the entire chart. It contains child elements that specify // the specific formatting and positioning settings. type cDLbls struct { - ShowLegendKey *attrValBool `xml:"c:showLegendKey"` - ShowVal *attrValBool `xml:"c:showVal"` - ShowCatName *attrValBool `xml:"c:showCatName"` - ShowSerName *attrValBool `xml:"c:showSerName"` - ShowPercent *attrValBool `xml:"c:showPercent"` - ShowBubbleSize *attrValBool `xml:"c:showBubbleSize"` - ShowLeaderLines *attrValBool `xml:"c:showLeaderLines"` + ShowLegendKey *attrValBool `xml:"showLegendKey"` + ShowVal *attrValBool `xml:"showVal"` + ShowCatName *attrValBool `xml:"showCatName"` + ShowSerName *attrValBool `xml:"showSerName"` + ShowPercent *attrValBool `xml:"showPercent"` + ShowBubbleSize *attrValBool `xml:"showBubbleSize"` + ShowLeaderLines *attrValBool `xml:"showLeaderLines"` } -// cLegend (Legend) directly maps the c:legend element. This element specifies +// cLegend (Legend) directly maps the legend element. This element specifies // the legend. type cLegend struct { - Layout *string `xml:"c:layout"` - LegendPos *attrValString `xml:"c:legendPos"` - Overlay *attrValBool `xml:"c:overlay"` - SpPr *cSpPr `xml:"c:spPr"` - TxPr *cTxPr `xml:"c:txPr"` + Layout *string `xml:"layout"` + LegendPos *attrValString `xml:"legendPos"` + Overlay *attrValBool `xml:"overlay"` + SpPr *cSpPr `xml:"spPr"` + TxPr *cTxPr `xml:"txPr"` } -// cPrintSettings directly maps the c:printSettings element. This element +// cPrintSettings directly maps the printSettings element. This element // specifies the print settings for the chart. type cPrintSettings struct { - HeaderFooter *string `xml:"c:headerFooter"` - PageMargins *cPageMargins `xml:"c:pageMargins"` - PageSetup *string `xml:"c:pageSetup"` + HeaderFooter *string `xml:"headerFooter"` + PageMargins *cPageMargins `xml:"pageMargins"` + PageSetup *string `xml:"pageSetup"` } -// cPageMargins directly maps the c:pageMargins element. This element specifies +// cPageMargins directly maps the pageMargins element. This element specifies // the page margins for a chart. type cPageMargins struct { B float64 `xml:"b,attr"` diff --git a/xmlCore.go b/xmlCore.go old mode 100755 new mode 100644 diff --git a/xmlDrawing.go b/xmlDrawing.go old mode 100755 new mode 100644 diff --git a/xmlWorksheet.go b/xmlWorksheet.go index f2eb47a..4d19cde 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -24,6 +24,7 @@ type xlsxWorksheet struct { SheetData xlsxSheetData `xml:"sheetData"` SheetProtection *xlsxSheetProtection `xml:"sheetProtection"` AutoFilter *xlsxAutoFilter `xml:"autoFilter"` + CustomSheetViews *xlsxCustomSheetViews `xml:"customSheetViews"` MergeCells *xlsxMergeCells `xml:"mergeCells"` PhoneticPr *xlsxPhoneticPr `xml:"phoneticPr"` ConditionalFormatting []*xlsxConditionalFormatting `xml:"conditionalFormatting"` @@ -33,6 +34,8 @@ type xlsxWorksheet struct { PageMargins *xlsxPageMargins `xml:"pageMargins"` PageSetUp *xlsxPageSetUp `xml:"pageSetup"` HeaderFooter *xlsxHeaderFooter `xml:"headerFooter"` + RowBreaks *xlsxBreaks `xml:"rowBreaks"` + ColBreaks *xlsxBreaks `xml:"colBreaks"` Drawing *xlsxDrawing `xml:"drawing"` LegacyDrawing *xlsxLegacyDrawing `xml:"legacyDrawing"` Picture *xlsxPicture `xml:"picture"` @@ -299,6 +302,63 @@ type xlsxRow struct { C []xlsxC `xml:"c"` } +// xlsxCustomSheetViews directly maps the customSheetViews element. This is a +// collection of custom sheet views. +type xlsxCustomSheetViews struct { + CustomSheetView []*xlsxCustomSheetView `xml:"customSheetView"` +} + +// xlsxBrk directly maps the row or column break to use when paginating a +// worksheet. +type xlsxBrk struct { + ID int `xml:"id,attr,omitempty"` + Min int `xml:"min,attr,omitempty"` + Max int `xml:"max,attr,omitempty"` + Man bool `xml:"man,attr,omitempty"` + Pt bool `xml:"pt,attr,omitempty"` +} + +// xlsxBreaks directly maps a collection of the row or column breaks. +type xlsxBreaks struct { + Brk *xlsxBrk `xml:"brk"` + Count int `xml:"count,attr,omitempty"` + ManualBreakCount int `xml:"manualBreakCount,attr,omitempty"` +} + +// xlsxCustomSheetView directly maps the customSheetView element. +type xlsxCustomSheetView struct { + Pane *xlsxPane `xml:"pane"` + Selection *xlsxSelection `xml:"selection"` + RowBreaks *xlsxBreaks `xml:"rowBreaks"` + ColBreaks *xlsxBreaks `xml:"colBreaks"` + PageMargins *xlsxPageMargins `xml:"pageMargins"` + PrintOptions *xlsxPrintOptions `xml:"printOptions"` + PageSetup *xlsxPageSetUp `xml:"pageSetup"` + HeaderFooter *xlsxHeaderFooter `xml:"headerFooter"` + AutoFilter *xlsxAutoFilter `xml:"autoFilter"` + ExtLst *xlsxExt `xml:"extLst"` + GUID string `xml:"guid,attr"` + Scale int `xml:"scale,attr,omitempty"` + ColorID int `xml:"colorId,attr,omitempty"` + ShowPageBreaks bool `xml:"showPageBreaks,attr,omitempty"` + ShowFormulas bool `xml:"showFormulas,attr,omitempty"` + ShowGridLines bool `xml:"showGridLines,attr,omitempty"` + ShowRowCol bool `xml:"showRowCol,attr,omitempty"` + OutlineSymbols bool `xml:"outlineSymbols,attr,omitempty"` + ZeroValues bool `xml:"zeroValues,attr,omitempty"` + FitToPage bool `xml:"fitToPage,attr,omitempty"` + PrintArea bool `xml:"printArea,attr,omitempty"` + Filter bool `xml:"filter,attr,omitempty"` + ShowAutoFilter bool `xml:"showAutoFilter,attr,omitempty"` + HiddenRows bool `xml:"hiddenRows,attr,omitempty"` + HiddenColumns bool `xml:"hiddenColumns,attr,omitempty"` + State string `xml:"state,attr,omitempty"` + FilterUnique bool `xml:"filterUnique,attr,omitempty"` + View string `xml:"view,attr,omitempty"` + ShowRuler bool `xml:"showRuler,attr,omitempty"` + TopLeftCell string `xml:"topLeftCell,attr,omitempty"` +} + // xlsxMergeCell directly maps the mergeCell element. A single merged cell. type xlsxMergeCell struct { Ref string `xml:"ref,attr,omitempty"` @@ -389,26 +449,26 @@ type xlsxF struct { // enforce when the sheet is protected. type xlsxSheetProtection struct { AlgorithmName string `xml:"algorithmName,attr,omitempty"` - AutoFilter bool `xml:"autoFilter,attr,omitempty"` - DeleteColumns bool `xml:"deleteColumns,attr,omitempty"` - DeleteRows bool `xml:"deleteRows,attr,omitempty"` + Password string `xml:"password,attr,omitempty"` + HashValue string `xml:"hashValue,attr,omitempty"` + SaltValue string `xml:"saltValue,attr,omitempty"` + SpinCount int `xml:"spinCount,attr,omitempty"` + Sheet bool `xml:"sheet,attr,omitempty"` + Objects bool `xml:"objects,attr,omitempty"` + Scenarios bool `xml:"scenarios,attr,omitempty"` FormatCells bool `xml:"formatCells,attr,omitempty"` FormatColumns bool `xml:"formatColumns,attr,omitempty"` FormatRows bool `xml:"formatRows,attr,omitempty"` - HashValue string `xml:"hashValue,attr,omitempty"` InsertColumns bool `xml:"insertColumns,attr,omitempty"` - InsertHyperlinks bool `xml:"insertHyperlinks,attr,omitempty"` InsertRows bool `xml:"insertRows,attr,omitempty"` - Objects bool `xml:"objects,attr,omitempty"` - Password string `xml:"password,attr,omitempty"` - PivotTables bool `xml:"pivotTables,attr,omitempty"` - SaltValue string `xml:"saltValue,attr,omitempty"` - Scenarios bool `xml:"scenarios,attr,omitempty"` + InsertHyperlinks bool `xml:"insertHyperlinks,attr,omitempty"` + DeleteColumns bool `xml:"deleteColumns,attr,omitempty"` + DeleteRows bool `xml:"deleteRows,attr,omitempty"` SelectLockedCells bool `xml:"selectLockedCells,attr,omitempty"` - SelectUnlockedCells bool `xml:"selectUnlockedCells,attr,omitempty"` - Sheet bool `xml:"sheet,attr,omitempty"` Sort bool `xml:"sort,attr,omitempty"` - SpinCount int `xml:"spinCount,attr,omitempty"` + AutoFilter bool `xml:"autoFilter,attr,omitempty"` + PivotTables bool `xml:"pivotTables,attr,omitempty"` + SelectUnlockedCells bool `xml:"selectUnlockedCells,attr,omitempty"` } // xlsxPhoneticPr (Phonetic Properties) represents a collection of phonetic -- cgit v1.2.1 From 3997dee1f58c81444733e1756da6138d4ce445f1 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 7 Jun 2019 09:16:55 +0800 Subject: Fix #411, change font size to float type --- styles.go | 2 +- xmlChart.go | 2 +- xmlStyles.go | 14 +++++++------- 3 files changed, 9 insertions(+), 9 deletions(-) mode change 100644 => 100755 styles.go mode change 100644 => 100755 xmlChart.go mode change 100644 => 100755 xmlStyles.go diff --git a/styles.go b/styles.go old mode 100644 new mode 100755 index e0e6f78..1cae186 --- a/styles.go +++ b/styles.go @@ -1981,7 +1981,7 @@ func (f *File) setFont(formatStyle *formatStyle) *xlsxFont { fnt := xlsxFont{ B: formatStyle.Font.Bold, I: formatStyle.Font.Italic, - Sz: &attrValInt{Val: formatStyle.Font.Size}, + Sz: &attrValFloat{Val: formatStyle.Font.Size}, Color: &xlsxColor{RGB: getPaletteColor(formatStyle.Font.Color)}, Name: &attrValString{Val: formatStyle.Font.Family}, Family: &attrValInt{Val: 2}, diff --git a/xmlChart.go b/xmlChart.go old mode 100644 new mode 100755 index 972ead3..d23364c --- a/xmlChart.go +++ b/xmlChart.go @@ -209,7 +209,7 @@ type aRPr struct { SmtID uint64 `xml:"smtId,attr,omitempty"` Spc int `xml:"spc,attr"` Strike string `xml:"strike,attr,omitempty"` - Sz int `xml:"sz,attr,omitempty"` + Sz float64 `xml:"sz,attr,omitempty"` U string `xml:"u,attr,omitempty"` SolidFill *aSolidFill `xml:"a:solidFill"` Latin *aLatin `xml:"a:latin"` diff --git a/xmlStyles.go b/xmlStyles.go old mode 100644 new mode 100755 index 5c198e7..46587da --- a/xmlStyles.go +++ b/xmlStyles.go @@ -96,7 +96,7 @@ type xlsxFont struct { Condense bool `xml:"condense,omitempty"` Extend bool `xml:"extend,omitempty"` Color *xlsxColor `xml:"color"` - Sz *attrValInt `xml:"sz"` + Sz *attrValFloat `xml:"sz"` U *attrValString `xml:"u"` Scheme *attrValString `xml:"scheme"` } @@ -315,12 +315,12 @@ type xlsxStyleColors struct { // formatFont directly maps the styles settings of the fonts. type formatFont struct { - Bold bool `json:"bold"` - Italic bool `json:"italic"` - Underline string `json:"underline"` - Family string `json:"family"` - Size int `json:"size"` - Color string `json:"color"` + Bold bool `json:"bold"` + Italic bool `json:"italic"` + Underline string `json:"underline"` + Family string `json:"family"` + Size float64 `json:"size"` + Color string `json:"color"` } // formatStyle directly maps the styles settings of the cells. -- cgit v1.2.1 From 421f945f51f254054991127758db0520cf0f5456 Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 8 Jun 2019 00:00:55 +0800 Subject: Fixed #418, #420, #421, init adjust calculation chain support Update testing case --- adjust.go | 32 +++++++++++++++++++++++++++++--- adjust_test.go | 16 ++++++++++++++++ rows.go | 4 ++++ sheet.go | 2 +- styles.go | 4 ++-- xmlStyles.go | 14 +++++++------- 6 files changed, 59 insertions(+), 13 deletions(-) diff --git a/adjust.go b/adjust.go index 51db57e..d88990d 100644 --- a/adjust.go +++ b/adjust.go @@ -27,8 +27,7 @@ const ( // 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 +// TODO: adjustPageBreaks, adjustComments, adjustDataValidations, adjustProtectedCells // func (f *File) adjustHelper(sheet string, dir adjustDirection, num, offset int) error { xlsx, err := f.workSheetReader(sheet) @@ -47,7 +46,9 @@ func (f *File) adjustHelper(sheet string, dir adjustDirection, num, offset int) if err = f.adjustAutoFilter(xlsx, dir, num, offset); err != nil { return err } - + if err = f.adjustCalcChain(dir, num, offset); err != nil { + return err + } checkSheet(xlsx) checkRow(xlsx) return nil @@ -243,3 +244,28 @@ func (f *File) adjustMergeCells(xlsx *xlsxWorksheet, dir adjustDirection, num, o } return nil } + +// adjustCalcChain provides a function to update the calculation chain when +// inserting or deleting rows or columns. +func (f *File) adjustCalcChain(dir adjustDirection, num, offset int) error { + if f.CalcChain == nil { + return nil + } + for index, c := range f.CalcChain.C { + colNum, rowNum, err := CellNameToCoordinates(c.R) + if err != nil { + return err + } + if dir == rows && num <= rowNum { + if newRow := rowNum + offset; newRow > 0 { + f.CalcChain.C[index].R, _ = CoordinatesToCellName(colNum, newRow) + } + } + if dir == columns && num <= colNum { + if newCol := colNum + offset; newCol > 0 { + f.CalcChain.C[index].R, _ = CoordinatesToCellName(newCol, rowNum) + } + } + } + return nil +} diff --git a/adjust_test.go b/adjust_test.go index 7b708ab..28432bc 100644 --- a/adjust_test.go +++ b/adjust_test.go @@ -67,3 +67,19 @@ func TestAdjustHelper(t *testing.T) { // testing adjustHelper on not exists worksheet. assert.EqualError(t, f.adjustHelper("SheetN", rows, 0, 0), "sheet SheetN is not exist") } + +func TestAdjustCalcChain(t *testing.T) { + f := NewFile() + f.CalcChain = &xlsxCalcChain{ + C: []xlsxCalcChainC{ + {R: "B2"}, + }, + } + assert.NoError(t, f.InsertCol("Sheet1", "A")) + assert.NoError(t, f.InsertRow("Sheet1", 1)) + + f.CalcChain.C[0].R = "invalid coordinates" + assert.EqualError(t, f.InsertCol("Sheet1", "A"), `cannot convert cell "invalid coordinates" to coordinates: invalid cell name "invalid coordinates"`) + f.CalcChain = nil + assert.NoError(t, f.InsertCol("Sheet1", "A")) +} diff --git a/rows.go b/rows.go index b228fc2..249ca2f 100644 --- a/rows.go +++ b/rows.go @@ -439,6 +439,10 @@ func (f *File) RemoveRow(sheet string, row int) error { // // err := f.InsertRow("Sheet1", 3) // +// Use this method with caution, which will affect changes in references such +// 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) InsertRow(sheet string, row int) error { if row < 1 { return newInvalidRowNumberError(row) diff --git a/sheet.go b/sheet.go index e873118..d3099fb 100644 --- a/sheet.go +++ b/sheet.go @@ -527,7 +527,7 @@ func (f *File) SetSheetVisible(name string, visible bool) error { } } for k, v := range content.Sheets.Sheet { - xlsx, err := f.workSheetReader(f.GetSheetMap()[k]) + xlsx, err := f.workSheetReader(v.Name) if err != nil { return err } diff --git a/styles.go b/styles.go index 1cae186..5c4f66e 100755 --- a/styles.go +++ b/styles.go @@ -1979,8 +1979,8 @@ func (f *File) setFont(formatStyle *formatStyle) *xlsxFont { formatStyle.Font.Color = "#000000" } fnt := xlsxFont{ - B: formatStyle.Font.Bold, - I: formatStyle.Font.Italic, + B: &formatStyle.Font.Bold, + I: &formatStyle.Font.Italic, Sz: &attrValFloat{Val: formatStyle.Font.Size}, Color: &xlsxColor{RGB: getPaletteColor(formatStyle.Font.Color)}, Name: &attrValString{Val: formatStyle.Font.Family}, diff --git a/xmlStyles.go b/xmlStyles.go index 46587da..49abe3c 100755 --- a/xmlStyles.go +++ b/xmlStyles.go @@ -88,13 +88,13 @@ type xlsxFont struct { Name *attrValString `xml:"name"` Charset *attrValInt `xml:"charset"` Family *attrValInt `xml:"family"` - B bool `xml:"b,omitempty"` - I bool `xml:"i,omitempty"` - Strike bool `xml:"strike,omitempty"` - Outline bool `xml:"outline,omitempty"` - Shadow bool `xml:"shadow,omitempty"` - Condense bool `xml:"condense,omitempty"` - Extend bool `xml:"extend,omitempty"` + B *bool `xml:"b,omitempty"` + I *bool `xml:"i,omitempty"` + Strike *bool `xml:"strike,omitempty"` + Outline *bool `xml:"outline,omitempty"` + Shadow *bool `xml:"shadow,omitempty"` + Condense *bool `xml:"condense,omitempty"` + Extend *bool `xml:"extend,omitempty"` Color *xlsxColor `xml:"color"` Sz *attrValFloat `xml:"sz"` U *attrValString `xml:"u"` -- cgit v1.2.1 From 46a3632ee0f441c8990a7205445dfdb00823a6ad Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 9 Jun 2019 09:53:02 +0800 Subject: Fix #422, avoid accent theme color index overflow --- chart.go | 14 +++++++++----- chart_test.go | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ excelize_test.go | 49 ------------------------------------------------- 3 files changed, 59 insertions(+), 54 deletions(-) diff --git a/chart.go b/chart.go index 88f48b2..73c4728 100644 --- a/chart.go +++ b/chart.go @@ -946,11 +946,13 @@ func (f *File) drawChartSeriesSpPr(i int, formatSet *formatChart) *cSpPr { Ln: &aLn{ W: 25400, Cap: "rnd", // rnd, sq, flat - SolidFill: &aSolidFill{ - SchemeClr: &aSchemeClr{Val: "accent" + strconv.Itoa(i+1)}, - }, }, } + if i < 6 { + spPrLine.Ln.SolidFill = &aSolidFill{ + SchemeClr: &aSchemeClr{Val: "accent" + strconv.Itoa(i+1)}, + } + } chartSeriesSpPr := map[string]*cSpPr{Area: nil, AreaStacked: nil, AreaPercentStacked: nil, Area3D: nil, Area3DStacked: nil, Area3DPercentStacked: nil, Bar: nil, BarStacked: nil, BarPercentStacked: nil, Bar3DClustered: nil, Bar3DStacked: nil, Bar3DPercentStacked: nil, Col: nil, ColStacked: nil, ColPercentStacked: nil, Col3DClustered: nil, Col3D: nil, Col3DStacked: nil, Col3DPercentStacked: nil, Doughnut: nil, Line: spPrLine, Pie: nil, Pie3D: nil, Radar: nil, Scatter: spPrScatter} return chartSeriesSpPr[formatSet.Type] } @@ -1014,7 +1016,9 @@ func (f *File) drawChartSeriesMarker(i int, formatSet *formatChart) *cMarker { marker := &cMarker{ Symbol: &attrValString{Val: "circle"}, Size: &attrValInt{Val: 5}, - SpPr: &cSpPr{ + } + if i < 6 { + marker.SpPr = &cSpPr{ SolidFill: &aSolidFill{ SchemeClr: &aSchemeClr{ Val: "accent" + strconv.Itoa(i+1), @@ -1028,7 +1032,7 @@ func (f *File) drawChartSeriesMarker(i int, formatSet *formatChart) *cMarker { }, }, }, - }, + } } chartSeriesMarker := map[string]*cMarker{Area: nil, AreaStacked: nil, AreaPercentStacked: nil, Area3D: nil, Area3DStacked: nil, Area3DPercentStacked: nil, Bar: nil, BarStacked: nil, BarPercentStacked: nil, Bar3DClustered: nil, Bar3DStacked: nil, Bar3DPercentStacked: nil, Col: nil, ColStacked: nil, ColPercentStacked: nil, Col3DClustered: nil, Col3D: nil, Col3DStacked: nil, Col3DPercentStacked: nil, Doughnut: nil, Line: nil, Pie: nil, Pie3D: nil, Radar: nil, Scatter: marker} return chartSeriesMarker[formatSet.Type] diff --git a/chart_test.go b/chart_test.go index 98baedd..b1cd3b0 100644 --- a/chart_test.go +++ b/chart_test.go @@ -3,6 +3,7 @@ package excelize import ( "bytes" "encoding/xml" + "path/filepath" "testing" "github.com/stretchr/testify/assert" @@ -101,3 +102,52 @@ func TestAddDrawingChart(t *testing.T) { f := NewFile() assert.EqualError(t, f.addDrawingChart("SheetN", "", "", 0, 0, 0, nil), `cannot convert cell "" to coordinates: invalid cell name ""`) } + +func TestAddChart(t *testing.T) { + f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) + if !assert.NoError(t, err) { + t.FailNow() + } + + categories := map[string]string{"A30": "SS", "A31": "S", "A32": "M", "A33": "L", "A34": "LL", "A35": "XL", "A36": "XXL", "A37": "XXXL", "B29": "Apple", "C29": "Orange", "D29": "Pear"} + values := map[string]int{"B30": 1, "C30": 1, "D30": 1, "B31": 2, "C31": 2, "D31": 2, "B32": 3, "C32": 3, "D32": 3, "B33": 4, "C33": 4, "D33": 4, "B34": 5, "C34": 5, "D34": 5, "B35": 6, "C35": 6, "D35": 6, "B36": 7, "C36": 7, "D36": 7, "B37": 8, "C37": 8, "D37": 8} + for k, v := range categories { + assert.NoError(t, f.SetCellValue("Sheet1", k, v)) + } + for k, v := range values { + assert.NoError(t, f.SetCellValue("Sheet1", k, v)) + } + assert.EqualError(t, f.AddChart("Sheet1", "P1", ""), "unexpected end of JSON input") + + // Test add chart on not exists worksheet. + assert.EqualError(t, f.AddChart("SheetN", "P1", "{}"), "sheet SheetN is not exist") + + assert.NoError(t, f.AddChart("Sheet1", "P1", `{"type":"col","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"Fruit 2D Column 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"}`)) + assert.NoError(t, f.AddChart("Sheet1", "X1", `{"type":"colStacked","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"Fruit 2D Stacked Column 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"}`)) + assert.NoError(t, f.AddChart("Sheet1", "P16", `{"type":"colPercentStacked","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"Fruit 100% Stacked Column 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"}`)) + assert.NoError(t, f.AddChart("Sheet1", "X16", `{"type":"col3DClustered","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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 3D Clustered Column 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"}`)) + assert.NoError(t, f.AddChart("Sheet1", "P30", `{"type":"col3DStacked","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"Fruit 3D 100% Stacked Bar 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"}`)) + assert.NoError(t, f.AddChart("Sheet1", "X30", `{"type":"col3DPercentStacked","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"Fruit 3D 100% Stacked Column 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"}`)) + assert.NoError(t, f.AddChart("Sheet1", "P45", `{"type":"col3D","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"Fruit 3D Column 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"}`)) + assert.NoError(t, f.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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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"}`)) + assert.NoError(t, f.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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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"}`)) + assert.NoError(t, f.AddChart("Sheet2", "P16", `{"type":"doughnut","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$A$30:$D$37","values":"Sheet1!$B$30:$B$37"}],"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":"right","show_legend_key":false},"title":{"name":"Fruit Doughnut Chart"},"plotarea":{"show_bubble_size":false,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":false,"show_val":false},"show_blanks_as":"zero"}`)) + assert.NoError(t, f.AddChart("Sheet2", "X16", `{"type":"line","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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","show_legend_key":false},"title":{"name":"Fruit Line 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"}`)) + assert.NoError(t, f.AddChart("Sheet2", "P32", `{"type":"pie3D","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$A$30:$D$37","values":"Sheet1!$B$30:$B$37"}],"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 3D Pie Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":false,"show_val":false},"show_blanks_as":"zero"}`)) + assert.NoError(t, f.AddChart("Sheet2", "X32", `{"type":"pie","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$A$30:$D$37","values":"Sheet1!$B$30:$B$37"}],"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 Pie Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":false,"show_val":false},"show_blanks_as":"gap"}`)) + assert.NoError(t, f.AddChart("Sheet2", "P48", `{"type":"bar","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"Fruit 2D Clustered Bar 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"}`)) + assert.NoError(t, f.AddChart("Sheet2", "X48", `{"type":"barStacked","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"Fruit 2D Stacked Bar 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"}`)) + assert.NoError(t, f.AddChart("Sheet2", "P64", `{"type":"barPercentStacked","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"Fruit 2D Stacked 100% Bar 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"}`)) + assert.NoError(t, f.AddChart("Sheet2", "X64", `{"type":"bar3DClustered","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"Fruit 3D Clustered Bar 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"}`)) + assert.NoError(t, f.AddChart("Sheet2", "P80", `{"type":"bar3DStacked","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"Fruit 3D Stacked Bar 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","y_axis":{"maximum":7.5,"minimum":0.5}}`)) + assert.NoError(t, f.AddChart("Sheet2", "X80", `{"type":"bar3DPercentStacked","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"Fruit 3D 100% Stacked Bar 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","x_axis":{"reverse_order":true,"maximum":0,"minimum":0},"y_axis":{"reverse_order":true,"maximum":0,"minimum":0}}`)) + // area series charts + assert.NoError(t, f.AddChart("Sheet2", "AF1", `{"type":"area","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"Fruit 2D Area 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"}`)) + assert.NoError(t, f.AddChart("Sheet2", "AN1", `{"type":"areaStacked","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"Fruit 2D Stacked Area 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"}`)) + assert.NoError(t, f.AddChart("Sheet2", "AF16", `{"type":"areaPercentStacked","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"Fruit 2D 100% Stacked Area 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"}`)) + assert.NoError(t, f.AddChart("Sheet2", "AN16", `{"type":"area3D","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"Fruit 3D Area 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"}`)) + assert.NoError(t, f.AddChart("Sheet2", "AF32", `{"type":"area3DStacked","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"Fruit 3D Stacked Area 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"}`)) + assert.NoError(t, f.AddChart("Sheet2", "AN32", `{"type":"area3DPercentStacked","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"Fruit 3D 100% Stacked Area 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"}`)) + + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddChart.xlsx"))) +} diff --git a/excelize_test.go b/excelize_test.go index 1a3dde6..e4b2548 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -882,55 +882,6 @@ func TestAutoFilterError(t *testing.T) { } } -func TestAddChart(t *testing.T) { - f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) - if !assert.NoError(t, err) { - t.FailNow() - } - - categories := map[string]string{"A30": "Small", "A31": "Normal", "A32": "Large", "B29": "Apple", "C29": "Orange", "D29": "Pear"} - values := map[string]int{"B30": 2, "C30": 3, "D30": 3, "B31": 5, "C31": 2, "D31": 4, "B32": 6, "C32": 7, "D32": 8} - for k, v := range categories { - assert.NoError(t, f.SetCellValue("Sheet1", k, v)) - } - for k, v := range values { - assert.NoError(t, f.SetCellValue("Sheet1", k, v)) - } - assert.EqualError(t, f.AddChart("Sheet1", "P1", ""), "unexpected end of JSON input") - - // Test add chart on not exists worksheet. - assert.EqualError(t, f.AddChart("SheetN", "P1", "{}"), "sheet SheetN is not exist") - - assert.NoError(t, f.AddChart("Sheet1", "P1", `{"type":"col","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":"left","show_legend_key":false},"title":{"name":"Fruit 2D Column 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"}`)) - assert.NoError(t, f.AddChart("Sheet1", "X1", `{"type":"colStacked","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":"left","show_legend_key":false},"title":{"name":"Fruit 2D Stacked Column 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"}`)) - assert.NoError(t, f.AddChart("Sheet1", "P16", `{"type":"colPercentStacked","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":"left","show_legend_key":false},"title":{"name":"Fruit 100% Stacked Column 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"}`)) - assert.NoError(t, f.AddChart("Sheet1", "X16", `{"type":"col3DClustered","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 3D Clustered Column 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"}`)) - assert.NoError(t, f.AddChart("Sheet1", "P30", `{"type":"col3DStacked","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":"left","show_legend_key":false},"title":{"name":"Fruit 3D 100% Stacked Bar 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"}`)) - assert.NoError(t, f.AddChart("Sheet1", "X30", `{"type":"col3DPercentStacked","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":"left","show_legend_key":false},"title":{"name":"Fruit 3D 100% Stacked Column 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"}`)) - assert.NoError(t, f.AddChart("Sheet1", "P45", `{"type":"col3D","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":"left","show_legend_key":false},"title":{"name":"Fruit 3D Column 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"}`)) - assert.NoError(t, f.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"}`)) - assert.NoError(t, f.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"}`)) - assert.NoError(t, f.AddChart("Sheet2", "P16", `{"type":"doughnut","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"}],"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":"right","show_legend_key":false},"title":{"name":"Fruit Doughnut Chart"},"plotarea":{"show_bubble_size":false,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":false,"show_val":false},"show_blanks_as":"zero"}`)) - assert.NoError(t, f.AddChart("Sheet2", "X16", `{"type":"line","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","show_legend_key":false},"title":{"name":"Fruit Line 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"}`)) - assert.NoError(t, f.AddChart("Sheet2", "P32", `{"type":"pie3D","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"}],"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 3D Pie Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":false,"show_val":false},"show_blanks_as":"zero"}`)) - assert.NoError(t, f.AddChart("Sheet2", "X32", `{"type":"pie","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"}],"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 Pie Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":false,"show_val":false},"show_blanks_as":"gap"}`)) - assert.NoError(t, f.AddChart("Sheet2", "P48", `{"type":"bar","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":"left","show_legend_key":false},"title":{"name":"Fruit 2D Clustered Bar 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"}`)) - assert.NoError(t, f.AddChart("Sheet2", "X48", `{"type":"barStacked","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":"left","show_legend_key":false},"title":{"name":"Fruit 2D Stacked Bar 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"}`)) - assert.NoError(t, f.AddChart("Sheet2", "P64", `{"type":"barPercentStacked","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":"left","show_legend_key":false},"title":{"name":"Fruit 2D Stacked 100% Bar 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"}`)) - assert.NoError(t, f.AddChart("Sheet2", "X64", `{"type":"bar3DClustered","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":"left","show_legend_key":false},"title":{"name":"Fruit 3D Clustered Bar 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"}`)) - assert.NoError(t, f.AddChart("Sheet2", "P80", `{"type":"bar3DStacked","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":"left","show_legend_key":false},"title":{"name":"Fruit 3D Stacked Bar 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","y_axis":{"maximum":7.5,"minimum":0.5}}`)) - assert.NoError(t, f.AddChart("Sheet2", "X80", `{"type":"bar3DPercentStacked","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":"left","show_legend_key":false},"title":{"name":"Fruit 3D 100% Stacked Bar 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","x_axis":{"reverse_order":true,"maximum":0,"minimum":0},"y_axis":{"reverse_order":true,"maximum":0,"minimum":0}}`)) - // area series charts - assert.NoError(t, f.AddChart("Sheet2", "AF1", `{"type":"area","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":"left","show_legend_key":false},"title":{"name":"Fruit 2D Area 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"}`)) - assert.NoError(t, f.AddChart("Sheet2", "AN1", `{"type":"areaStacked","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":"left","show_legend_key":false},"title":{"name":"Fruit 2D Stacked Area 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"}`)) - assert.NoError(t, f.AddChart("Sheet2", "AF16", `{"type":"areaPercentStacked","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":"left","show_legend_key":false},"title":{"name":"Fruit 2D 100% Stacked Area 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"}`)) - assert.NoError(t, f.AddChart("Sheet2", "AN16", `{"type":"area3D","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":"left","show_legend_key":false},"title":{"name":"Fruit 3D Area 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"}`)) - assert.NoError(t, f.AddChart("Sheet2", "AF32", `{"type":"area3DStacked","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":"left","show_legend_key":false},"title":{"name":"Fruit 3D Stacked Area 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"}`)) - assert.NoError(t, f.AddChart("Sheet2", "AN32", `{"type":"area3DPercentStacked","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":"left","show_legend_key":false},"title":{"name":"Fruit 3D 100% Stacked Area 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"}`)) - - assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddChart.xlsx"))) -} - func TestSetPane(t *testing.T) { f := NewFile() f.SetPanes("Sheet1", `{"freeze":false,"split":false}`) -- cgit v1.2.1 From 821632cf89422b9955160a3af7f28f05a12f70f8 Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 12 Jun 2019 08:10:33 +0800 Subject: Fix #424, refactor merged cells adjuster --- adjust.go | 161 ++++++++++++++++++++++++++++++++-------------------- adjust_test.go | 31 ++++++++++ cell.go | 22 +++---- rows.go | 2 +- styles.go | 0 table.go | 17 ++---- xmlChart.go | 0 xmlDecodeDrawing.go | 111 ++++++++++++++++++++++++------------ xmlStyles.go | 0 9 files changed, 221 insertions(+), 123 deletions(-) mode change 100755 => 100644 styles.go mode change 100755 => 100644 xmlChart.go mode change 100755 => 100644 xmlStyles.go diff --git a/adjust.go b/adjust.go index d88990d..56d812f 100644 --- a/adjust.go +++ b/adjust.go @@ -9,7 +9,10 @@ package excelize -import "strings" +import ( + "errors" + "strings" +) type adjustDirection bool @@ -140,46 +143,85 @@ func (f *File) adjustAutoFilter(xlsx *xlsxWorksheet, dir adjustDirection, num, o return nil } - rng := strings.Split(xlsx.AutoFilter.Ref, ":") - firstCell := rng[0] - lastCell := rng[1] - - firstCol, firstRow, err := CellNameToCoordinates(firstCell) + coordinates, err := f.areaRefToCoordinates(xlsx.AutoFilter.Ref) if err != nil { return err } + x1, y1, x2, y2 := coordinates[0], coordinates[1], coordinates[2], coordinates[3] - lastCol, lastRow, err := CellNameToCoordinates(lastCell) - if err != nil { - return err - } - - if (dir == rows && firstRow == num && offset < 0) || (dir == columns && firstCol == num && lastCol == num) { + if (dir == rows && y1 == num && offset < 0) || (dir == columns && x1 == num && x2 == num) { xlsx.AutoFilter = nil for rowIdx := range xlsx.SheetData.Row { rowData := &xlsx.SheetData.Row[rowIdx] - if rowData.R > firstRow && rowData.R <= lastRow { + if rowData.R > y1 && rowData.R <= y2 { rowData.Hidden = false } } return nil } + coordinates = f.adjustAutoFilterHelper(dir, coordinates, num, offset) + x1, y1, x2, y2 = coordinates[0], coordinates[1], coordinates[2], coordinates[3] + + if xlsx.AutoFilter.Ref, err = f.coordinatesToAreaRef([]int{x1, y1, x2, y2}); err != nil { + return err + } + return nil +} + +// adjustAutoFilterHelper provides a function for adjusting auto filter to +// compare and calculate cell axis by the given adjust direction, operation +// axis and offset. +func (f *File) adjustAutoFilterHelper(dir adjustDirection, coordinates []int, num, offset int) []int { if dir == rows { - if firstRow >= num { - firstCell, _ = CoordinatesToCellName(firstCol, firstRow+offset) + if coordinates[1] >= num { + coordinates[1] += offset } - if lastRow >= num { - lastCell, _ = CoordinatesToCellName(lastCol, lastRow+offset) + if coordinates[3] >= num { + coordinates[3] += offset } } else { - if lastCol >= num { - lastCell, _ = CoordinatesToCellName(lastCol+offset, lastRow) + if coordinates[2] >= num { + coordinates[2] += offset } } + return coordinates +} - xlsx.AutoFilter.Ref = firstCell + ":" + lastCell - return nil +// areaRefToCoordinates provides a function to convert area reference to a +// pair of coordinates. +func (f *File) areaRefToCoordinates(ref string) ([]int, error) { + coordinates := make([]int, 4) + rng := strings.Split(ref, ":") + firstCell := rng[0] + lastCell := rng[1] + var err error + coordinates[0], coordinates[1], err = CellNameToCoordinates(firstCell) + if err != nil { + return coordinates, err + } + coordinates[2], coordinates[3], err = CellNameToCoordinates(lastCell) + if err != nil { + return coordinates, err + } + return coordinates, err +} + +// coordinatesToAreaRef provides a function to convert a pair of coordinates +// to area reference. +func (f *File) coordinatesToAreaRef(coordinates []int) (string, error) { + if len(coordinates) != 4 { + return "", errors.New("coordinates length must be 4") + } + firstCell, err := CoordinatesToCellName(coordinates[0], coordinates[1]) + if err != nil { + return "", err + } + lastCell, err := CoordinatesToCellName(coordinates[2], coordinates[3]) + if err != nil { + return "", err + } + return firstCell + ":" + lastCell, err } // adjustMergeCells provides a function to update merged cells when inserting @@ -190,59 +232,56 @@ func (f *File) adjustMergeCells(xlsx *xlsxWorksheet, dir adjustDirection, num, o } for i, areaData := range xlsx.MergeCells.Cells { - rng := strings.Split(areaData.Ref, ":") - firstCell := rng[0] - lastCell := rng[1] - - firstCol, firstRow, err := CellNameToCoordinates(firstCell) + coordinates, err := f.areaRefToCoordinates(areaData.Ref) if err != nil { return err } - - lastCol, lastRow, err := CellNameToCoordinates(lastCell) - if err != nil { - return err - } - - adjust := func(v int) int { - if v >= num { - v += offset - if v < 1 { - return 1 - } - return v - } - return v - } - + x1, y1, x2, y2 := coordinates[0], coordinates[1], coordinates[2], coordinates[3] if dir == rows { - firstRow = adjust(firstRow) - lastRow = adjust(lastRow) + if y1 == num && y2 == num && offset < 0 { + f.deleteMergeCell(xlsx, i) + } + y1 = f.adjustMergeCellsHelper(y1, num, offset) + y2 = f.adjustMergeCellsHelper(y2, num, offset) } 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 x1 == num && x2 == num && offset < 0 { + f.deleteMergeCell(xlsx, i) } + x1 = f.adjustMergeCellsHelper(x1, num, offset) + x2 = f.adjustMergeCellsHelper(x2, num, offset) } - - if firstCell, err = CoordinatesToCellName(firstCol, firstRow); err != nil { + if x1 == x2 && y1 == y2 { + f.deleteMergeCell(xlsx, i) + } + if areaData.Ref, err = f.coordinatesToAreaRef([]int{x1, y1, x2, y2}); err != nil { return err } + } + return nil +} - if lastCell, err = CoordinatesToCellName(lastCol, lastRow); err != nil { - return err +// adjustMergeCellsHelper provides a function for adjusting merge cells to +// compare and calculate cell axis by the given pivot, operation axis and +// offset. +func (f *File) adjustMergeCellsHelper(pivot, num, offset int) int { + if pivot >= num { + pivot += offset + if pivot < 1 { + return 1 } + return pivot + } + return pivot +} - areaData.Ref = firstCell + ":" + lastCell +// deleteMergeCell provides a function to delete merged cell by given index. +func (f *File) deleteMergeCell(sheet *xlsxWorksheet, idx int) { + if len(sheet.MergeCells.Cells) > 1 { + sheet.MergeCells.Cells = append(sheet.MergeCells.Cells[:idx], sheet.MergeCells.Cells[idx+1:]...) + sheet.MergeCells.Count = len(sheet.MergeCells.Cells) + } else { + sheet.MergeCells = nil } - return nil } // adjustCalcChain provides a function to update the calculation chain when diff --git a/adjust_test.go b/adjust_test.go index 28432bc..364a8b8 100644 --- a/adjust_test.go +++ b/adjust_test.go @@ -27,6 +27,24 @@ func TestAdjustMergeCells(t *testing.T) { }, }, }, rows, 0, 0), `cannot convert cell "B" to coordinates: invalid cell name "B"`) + assert.NoError(t, f.adjustMergeCells(&xlsxWorksheet{ + MergeCells: &xlsxMergeCells{ + Cells: []*xlsxMergeCell{ + { + Ref: "A1:B1", + }, + }, + }, + }, rows, 1, -1)) + assert.NoError(t, f.adjustMergeCells(&xlsxWorksheet{ + MergeCells: &xlsxMergeCells{ + Cells: []*xlsxMergeCell{ + { + Ref: "A1:A2", + }, + }, + }, + }, columns, 1, -1)) } func TestAdjustAutoFilter(t *testing.T) { @@ -83,3 +101,16 @@ func TestAdjustCalcChain(t *testing.T) { f.CalcChain = nil assert.NoError(t, f.InsertCol("Sheet1", "A")) } + +func TestCoordinatesToAreaRef(t *testing.T) { + f := NewFile() + ref, err := f.coordinatesToAreaRef([]int{}) + assert.EqualError(t, err, "coordinates length must be 4") + ref, err = f.coordinatesToAreaRef([]int{1, -1, 1, 1}) + assert.EqualError(t, err, "invalid cell coordinates [1, -1]") + ref, err = f.coordinatesToAreaRef([]int{1, 1, 1, -1}) + assert.EqualError(t, err, "invalid cell coordinates [1, -1]") + ref, err = f.coordinatesToAreaRef([]int{1, 1, 1, 1}) + assert.NoError(t, err) + assert.EqualValues(t, ref, "A1:A1") +} diff --git a/cell.go b/cell.go index bd4d93b..6743e2a 100644 --- a/cell.go +++ b/cell.go @@ -401,31 +401,27 @@ func (f *File) SetCellHyperLink(sheet, axis, link, linkType string) error { // 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) error { - hcol, hrow, err := CellNameToCoordinates(hcell) + coordinates, err := f.areaRefToCoordinates(hcell + ":" + vcell) if err != nil { return err } + x1, y1, x2, y2 := coordinates[0], coordinates[1], coordinates[2], coordinates[3] - vcol, vrow, err := CellNameToCoordinates(vcell) - if err != nil { - return err - } - - if hcol == vcol && hrow == vrow { + if x1 == x2 && y1 == y2 { return err } // Correct the coordinate area, such correct C1:B3 to B1:C3. - if vcol < hcol { - hcol, vcol = vcol, hcol + if x2 < x1 { + x1, x2 = x2, x1 } - if vrow < hrow { - hrow, vrow = vrow, hrow + if y2 < y1 { + y1, y2 = y2, y1 } - hcell, _ = CoordinatesToCellName(hcol, hrow) - vcell, _ = CoordinatesToCellName(vcol, vrow) + hcell, _ = CoordinatesToCellName(x1, y1) + vcell, _ = CoordinatesToCellName(x2, y2) xlsx, err := f.workSheetReader(sheet) if err != nil { diff --git a/rows.go b/rows.go index 249ca2f..064fefe 100644 --- a/rows.go +++ b/rows.go @@ -421,7 +421,7 @@ func (f *File) RemoveRow(sheet string, row int) error { return err } if row > len(xlsx.SheetData.Row) { - return nil + return f.adjustHelper(sheet, rows, row, -1) } for rowIdx := range xlsx.SheetData.Row { if xlsx.SheetData.Row[rowIdx].R == row { diff --git a/styles.go b/styles.go old mode 100755 new mode 100644 diff --git a/table.go b/table.go index f3819d3..3d8d402 100644 --- a/table.go +++ b/table.go @@ -115,29 +115,24 @@ 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, hcol, hrow, vcol, vrow, i int, formatSet *formatTable) error { +func (f *File) addTable(sheet, tableXML string, x1, y1, x2, y2, i int, formatSet *formatTable) error { // Correct the minimum number of rows, the table at least two lines. - if hrow == vrow { - vrow++ + if y1 == y2 { + y2++ } // Correct table reference coordinate area, such correct C1:B3 to B1:C3. - hcell, err := CoordinatesToCellName(hcol, hrow) + ref, err := f.coordinatesToAreaRef([]int{x1, y1, x2, y2}) if err != nil { return err } - vcell, err := CoordinatesToCellName(vcol, vrow) - if err != nil { - return err - } - ref := hcell + ":" + vcell var tableColumn []*xlsxTableColumn idx := 0 - for i := hcol; i <= vcol; i++ { + for i := x1; i <= x2; i++ { idx++ - cell, err := CoordinatesToCellName(i, hrow) + cell, err := CoordinatesToCellName(i, y1) if err != nil { return err } diff --git a/xmlChart.go b/xmlChart.go old mode 100755 new mode 100644 diff --git a/xmlDecodeDrawing.go b/xmlDecodeDrawing.go index eead575..6cb224a 100644 --- a/xmlDecodeDrawing.go +++ b/xmlDecodeDrawing.go @@ -11,19 +11,55 @@ package excelize import "encoding/xml" -// decodeCellAnchor directly maps the oneCellAnchor (One Cell Anchor Shape Size) -// and twoCellAnchor (Two Cell Anchor Shape Size). This element specifies a two -// cell anchor placeholder for a group, a shape, or a drawing element. It moves -// with cells and its extents are in EMU units. +// decodeCellAnchor directly maps the oneCellAnchor (One Cell Anchor Shape +// Size) and twoCellAnchor (Two Cell Anchor Shape Size). This element +// specifies a two cell anchor placeholder for a group, a shape, or a drawing +// element. It moves with cells and its extents are in EMU units. type decodeCellAnchor struct { - EditAs string `xml:"editAs,attr,omitempty"` - Content string `xml:",innerxml"` + EditAs string `xml:"editAs,attr,omitempty"` + From *decodeFrom `xml:"from"` + To *decodeTo `xml:"to"` + Sp *decodeSp `xml:"sp"` + ClientData *decodeClientData `xml:"clientData"` + Content string `xml:",innerxml"` +} + +// xdrSp (Shape) directly maps the sp element. This element specifies the +// existence of a single shape. A shape can either be a preset or a custom +// geometry, defined using the SpreadsheetDrawingML framework. In addition to +// a geometry each shape can have both visual and non-visual properties +// attached. Text and corresponding styling information can also be attached +// to a shape. This shape is specified along with all other shapes within +// either the shape tree or group shape elements. +type decodeSp struct { + NvSpPr *decodeNvSpPr `xml:"nvSpPr"` + SpPr *decodeSpPr `xml:"spPr"` +} + +// decodeSp (Non-Visual Properties for a Shape) directly maps the nvSpPr +// element. This element specifies all non-visual properties for a shape. This +// element is a container for the non-visual identification properties, shape +// properties and application properties that are to be associated with a +// shape. This allows for additional information that does not affect the +// appearance of the shape to be stored. +type decodeNvSpPr struct { + CNvPr *decodeCNvPr `xml:"cNvPr"` + ExtLst *decodeExt `xml:"extLst"` + CNvSpPr *decodeCNvSpPr `xml:"cNvSpPr"` +} + +// decodeCNvSpPr (Connection Non-Visual Shape Properties) directly maps the +// cNvSpPr element. This element specifies the set of non-visual properties +// for a connection shape. These properties specify all data about the +// connection shape which do not affect its display within a spreadsheet. +type decodeCNvSpPr struct { + TxBox bool `xml:"txBox,attr"` } // decodeWsDr directly maps the root element for a part of this content type -// shall wsDr. In order to solve the problem that the label structure is changed -// after serialization and deserialization, two different structures are -// defined. decodeWsDr just for deserialization. +// shall wsDr. In order to solve the problem that the label structure is +// changed after serialization and deserialization, two different structures +// are defined. decodeWsDr just for deserialization. type decodeWsDr struct { A string `xml:"xmlns a,attr"` Xdr string `xml:"xmlns xdr,attr"` @@ -34,9 +70,9 @@ type decodeWsDr struct { } // decodeTwoCellAnchor directly maps the oneCellAnchor (One Cell Anchor Shape -// Size) and twoCellAnchor (Two Cell Anchor Shape Size). This element specifies -// a two cell anchor placeholder for a group, a shape, or a drawing element. It -// moves with cells and its extents are in EMU units. +// Size) and twoCellAnchor (Two Cell Anchor Shape Size). This element +// specifies a two cell anchor placeholder for a group, a shape, or a drawing +// element. It moves with cells and its extents are in EMU units. type decodeTwoCellAnchor struct { From *decodeFrom `xml:"from"` To *decodeTo `xml:"to"` @@ -46,7 +82,8 @@ type decodeTwoCellAnchor struct { // decodeCNvPr directly maps the cNvPr (Non-Visual Drawing Properties). This // element specifies non-visual canvas properties. This allows for additional -// information that does not affect the appearance of the picture to be stored. +// information that does not affect the appearance of the picture to be +// stored. type decodeCNvPr struct { ID int `xml:"id,attr"` Name string `xml:"name,attr"` @@ -55,8 +92,8 @@ type decodeCNvPr struct { } // decodePicLocks directly maps the picLocks (Picture Locks). This element -// specifies all locking properties for a graphic frame. These properties inform -// the generating application about specific properties that have been +// specifies all locking properties for a graphic frame. These properties +// inform the generating application about specific properties that have been // previously locked and thus should not be changed. type decodePicLocks struct { NoAdjustHandles bool `xml:"noAdjustHandles,attr,omitempty"` @@ -82,9 +119,9 @@ type decodeBlip struct { R string `xml:"r,attr"` } -// decodeStretch directly maps the stretch element. This element specifies that -// a BLIP should be stretched to fill the target rectangle. The other option is -// a tile where a BLIP is tiled to fill the available area. +// decodeStretch directly maps the stretch element. This element specifies +// that a BLIP should be stretched to fill the target rectangle. The other +// option is a tile where a BLIP is tiled to fill the available area. type decodeStretch struct { FillRect string `xml:"fillRect"` } @@ -128,12 +165,12 @@ type decodeCNvPicPr struct { PicLocks decodePicLocks `xml:"picLocks"` } -// directly maps the nvPicPr (Non-Visual Properties for a Picture). This element -// specifies all non-visual properties for a picture. This element is a -// container for the non-visual identification properties, shape properties and -// application properties that are to be associated with a picture. This allows -// for additional information that does not affect the appearance of the picture -// to be stored. +// directly maps the nvPicPr (Non-Visual Properties for a Picture). This +// element specifies all non-visual properties for a picture. This element is +// a container for the non-visual identification properties, shape properties +// and application properties that are to be associated with a picture. This +// allows for additional information that does not affect the appearance of +// the picture to be stored. type decodeNvPicPr struct { CNvPr decodeCNvPr `xml:"cNvPr"` CNvPicPr decodeCNvPicPr `xml:"cNvPicPr"` @@ -148,20 +185,20 @@ type decodeBlipFill struct { Stretch decodeStretch `xml:"stretch"` } -// decodeSpPr directly maps the spPr (Shape Properties). This element specifies -// the visual shape properties that can be applied to a picture. These are the -// same properties that are allowed to describe the visual properties of a shape -// but are used here to describe the visual appearance of a picture within a -// document. +// decodeSpPr directly maps the spPr (Shape Properties). This element +// specifies the visual shape properties that can be applied to a picture. +// These are the same properties that are allowed to describe the visual +// properties of a shape but are used here to describe the visual appearance +// of a picture within a document. type decodeSpPr struct { - Xfrm decodeXfrm `xml:"a:xfrm"` - PrstGeom decodePrstGeom `xml:"a:prstGeom"` + Xfrm decodeXfrm `xml:"xfrm"` + PrstGeom decodePrstGeom `xml:"prstGeom"` } -// decodePic elements encompass the definition of pictures within the DrawingML -// framework. While pictures are in many ways very similar to shapes they have -// specific properties that are unique in order to optimize for picture- -// specific scenarios. +// decodePic elements encompass the definition of pictures within the +// DrawingML framework. While pictures are in many ways very similar to shapes +// they have specific properties that are unique in order to optimize for +// picture- specific scenarios. type decodePic struct { NvPicPr decodeNvPicPr `xml:"nvPicPr"` BlipFill decodeBlipFill `xml:"blipFill"` @@ -184,8 +221,8 @@ type decodeTo struct { RowOff int `xml:"rowOff"` } -// decodeClientData directly maps the clientData element. An empty element which -// specifies (via attributes) certain properties related to printing and +// decodeClientData directly maps the clientData element. An empty element +// which specifies (via attributes) certain properties related to printing and // selection of the drawing object. The fLocksWithSheet attribute (either true // or false) determines whether to disable selection when the sheet is // protected, and fPrintsWithSheet attribute (either true or false) determines diff --git a/xmlStyles.go b/xmlStyles.go old mode 100755 new mode 100644 -- cgit v1.2.1 From e124f6000a2ea731b96a07d6bf2901781e272d90 Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 13 Jun 2019 08:25:35 +0800 Subject: Fix #425, handle empty font style format --- styles.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/styles.go b/styles.go index 5c4f66e..1c01421 100644 --- a/styles.go +++ b/styles.go @@ -1979,13 +1979,17 @@ func (f *File) setFont(formatStyle *formatStyle) *xlsxFont { formatStyle.Font.Color = "#000000" } fnt := xlsxFont{ - B: &formatStyle.Font.Bold, - I: &formatStyle.Font.Italic, Sz: &attrValFloat{Val: formatStyle.Font.Size}, Color: &xlsxColor{RGB: getPaletteColor(formatStyle.Font.Color)}, Name: &attrValString{Val: formatStyle.Font.Family}, Family: &attrValInt{Val: 2}, } + if formatStyle.Font.Bold { + fnt.B = &formatStyle.Font.Bold + } + if formatStyle.Font.Italic { + fnt.I = &formatStyle.Font.Italic + } if fnt.Name.Val == "" { fnt.Name.Val = f.GetDefaultFont() } -- cgit v1.2.1 From dc0869fde3a717009eb4aeff6a26387f0495b655 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 14 Jun 2019 00:05:10 +0800 Subject: support to create cone, pyramid and cylinder series chart for column and bar types --- chart.go | 930 +++++++++++++++++++++++++++++++++++++++++----------------- chart_test.go | 75 +++-- xmlChart.go | 1 + 3 files changed, 706 insertions(+), 300 deletions(-) diff --git a/chart.go b/chart.go index 73c4728..8ecd8c7 100644 --- a/chart.go +++ b/chart.go @@ -12,148 +12,254 @@ package excelize import ( "encoding/json" "encoding/xml" + "errors" "strconv" "strings" ) // This section defines the currently supported chart types. const ( - Area = "area" - AreaStacked = "areaStacked" - AreaPercentStacked = "areaPercentStacked" - Area3D = "area3D" - Area3DStacked = "area3DStacked" - Area3DPercentStacked = "area3DPercentStacked" - Bar = "bar" - BarStacked = "barStacked" - BarPercentStacked = "barPercentStacked" - Bar3DClustered = "bar3DClustered" - Bar3DStacked = "bar3DStacked" - Bar3DPercentStacked = "bar3DPercentStacked" - Col = "col" - ColStacked = "colStacked" - ColPercentStacked = "colPercentStacked" - Col3DClustered = "col3DClustered" - Col3D = "col3D" - Col3DStacked = "col3DStacked" - Col3DPercentStacked = "col3DPercentStacked" - Doughnut = "doughnut" - Line = "line" - Pie = "pie" - Pie3D = "pie3D" - Radar = "radar" - Scatter = "scatter" + Area = "area" + AreaStacked = "areaStacked" + AreaPercentStacked = "areaPercentStacked" + Area3D = "area3D" + Area3DStacked = "area3DStacked" + Area3DPercentStacked = "area3DPercentStacked" + Bar = "bar" + BarStacked = "barStacked" + BarPercentStacked = "barPercentStacked" + Bar3DClustered = "bar3DClustered" + Bar3DStacked = "bar3DStacked" + Bar3DPercentStacked = "bar3DPercentStacked" + Bar3DConeClustered = "bar3DConeClustered" + Bar3DConeStacked = "bar3DConeStacked" + Bar3DConePercentStacked = "bar3DConePercentStacked" + Bar3DPyramidClustered = "bar3DPyramidClustered" + Bar3DPyramidStacked = "bar3DPyramidStacked" + Bar3DPyramidPercentStacked = "bar3DPyramidPercentStacked" + Bar3DCylinderClustered = "bar3DCylinderClustered" + Bar3DCylinderStacked = "bar3DCylinderStacked" + Bar3DCylinderPercentStacked = "bar3DCylinderPercentStacked" + Col = "col" + ColStacked = "colStacked" + ColPercentStacked = "colPercentStacked" + Col3D = "col3D" + Col3DClustered = "col3DClustered" + Col3DStacked = "col3DStacked" + Col3DPercentStacked = "col3DPercentStacked" + Col3DCone = "col3DCone" + Col3DConeClustered = "col3DConeClustered" + Col3DConeStacked = "col3DConeStacked" + Col3DConePercentStacked = "col3DConePercentStacked" + Col3DPyramid = "col3DPyramid" + Col3DPyramidClustered = "col3DPyramidClustered" + Col3DPyramidStacked = "col3DPyramidStacked" + Col3DPyramidPercentStacked = "col3DPyramidPercentStacked" + Col3DCylinder = "col3DCylinder" + Col3DCylinderClustered = "col3DCylinderClustered" + Col3DCylinderStacked = "col3DCylinderStacked" + Col3DCylinderPercentStacked = "col3DCylinderPercentStacked" + Doughnut = "doughnut" + Line = "line" + Pie = "pie" + Pie3D = "pie3D" + Radar = "radar" + Scatter = "scatter" ) // This section defines the default value of chart properties. var ( chartView3DRotX = map[string]int{ - Area: 0, - AreaStacked: 0, - AreaPercentStacked: 0, - Area3D: 15, - Area3DStacked: 15, - Area3DPercentStacked: 15, - Bar: 0, - BarStacked: 0, - BarPercentStacked: 0, - Bar3DClustered: 15, - Bar3DStacked: 15, - Bar3DPercentStacked: 15, - Col: 0, - ColStacked: 0, - ColPercentStacked: 0, - Col3DClustered: 15, - Col3D: 15, - Col3DStacked: 15, - Col3DPercentStacked: 15, - Doughnut: 0, - Line: 0, - Pie: 0, - Pie3D: 30, - Radar: 0, - Scatter: 0, + Area: 0, + AreaStacked: 0, + AreaPercentStacked: 0, + Area3D: 15, + Area3DStacked: 15, + Area3DPercentStacked: 15, + Bar: 0, + BarStacked: 0, + BarPercentStacked: 0, + Bar3DClustered: 15, + Bar3DStacked: 15, + Bar3DPercentStacked: 15, + Bar3DConeClustered: 15, + Bar3DConeStacked: 15, + Bar3DConePercentStacked: 15, + Bar3DPyramidClustered: 15, + Bar3DPyramidStacked: 15, + Bar3DPyramidPercentStacked: 15, + Bar3DCylinderClustered: 15, + Bar3DCylinderStacked: 15, + Bar3DCylinderPercentStacked: 15, + Col: 0, + ColStacked: 0, + ColPercentStacked: 0, + Col3D: 15, + Col3DClustered: 15, + Col3DStacked: 15, + Col3DPercentStacked: 15, + Col3DCone: 15, + Col3DConeClustered: 15, + Col3DConeStacked: 15, + Col3DConePercentStacked: 15, + Col3DPyramid: 15, + Col3DPyramidClustered: 15, + Col3DPyramidStacked: 15, + Col3DPyramidPercentStacked: 15, + Col3DCylinder: 15, + Col3DCylinderClustered: 15, + Col3DCylinderStacked: 15, + Col3DCylinderPercentStacked: 15, + Doughnut: 0, + Line: 0, + Pie: 0, + Pie3D: 30, + Radar: 0, + Scatter: 0, } chartView3DRotY = map[string]int{ - Area: 0, - AreaStacked: 0, - AreaPercentStacked: 0, - Area3D: 20, - Area3DStacked: 20, - Area3DPercentStacked: 20, - Bar: 0, - BarStacked: 0, - BarPercentStacked: 0, - Bar3DClustered: 20, - Bar3DStacked: 20, - Bar3DPercentStacked: 20, - Col: 0, - ColStacked: 0, - ColPercentStacked: 0, - Col3DClustered: 20, - Col3D: 20, - Col3DStacked: 20, - Col3DPercentStacked: 20, - Doughnut: 0, - Line: 0, - Pie: 0, - Pie3D: 0, - Radar: 0, - Scatter: 0, + Area: 0, + AreaStacked: 0, + AreaPercentStacked: 0, + Area3D: 20, + Area3DStacked: 20, + Area3DPercentStacked: 20, + Bar: 0, + BarStacked: 0, + BarPercentStacked: 0, + Bar3DClustered: 20, + Bar3DStacked: 20, + Bar3DPercentStacked: 20, + Bar3DConeClustered: 20, + Bar3DConeStacked: 20, + Bar3DConePercentStacked: 20, + Bar3DPyramidClustered: 20, + Bar3DPyramidStacked: 20, + Bar3DPyramidPercentStacked: 20, + Bar3DCylinderClustered: 20, + Bar3DCylinderStacked: 20, + Bar3DCylinderPercentStacked: 20, + Col: 0, + ColStacked: 0, + ColPercentStacked: 0, + Col3D: 20, + Col3DClustered: 20, + Col3DStacked: 20, + Col3DPercentStacked: 20, + Col3DCone: 20, + Col3DConeClustered: 20, + Col3DConeStacked: 20, + Col3DConePercentStacked: 20, + Col3DPyramid: 20, + Col3DPyramidClustered: 20, + Col3DPyramidStacked: 20, + Col3DPyramidPercentStacked: 20, + Col3DCylinder: 20, + Col3DCylinderClustered: 20, + Col3DCylinderStacked: 20, + Col3DCylinderPercentStacked: 20, + Doughnut: 0, + Line: 0, + Pie: 0, + Pie3D: 0, + Radar: 0, + Scatter: 0, } chartView3DDepthPercent = map[string]int{ - Area: 100, - AreaStacked: 100, - AreaPercentStacked: 100, - Area3D: 100, - Area3DStacked: 100, - Area3DPercentStacked: 100, - Bar: 100, - BarStacked: 100, - BarPercentStacked: 100, - Bar3DClustered: 100, - Bar3DStacked: 100, - Bar3DPercentStacked: 100, - Col: 100, - ColStacked: 100, - ColPercentStacked: 100, - Col3DClustered: 100, - Col3D: 100, - Col3DStacked: 100, - Col3DPercentStacked: 100, - Doughnut: 100, - Line: 100, - Pie: 100, - Pie3D: 100, - Radar: 100, - Scatter: 100, + Area: 100, + AreaStacked: 100, + AreaPercentStacked: 100, + Area3D: 100, + Area3DStacked: 100, + Area3DPercentStacked: 100, + Bar: 100, + BarStacked: 100, + BarPercentStacked: 100, + Bar3DClustered: 100, + Bar3DStacked: 100, + Bar3DPercentStacked: 100, + Bar3DConeClustered: 100, + Bar3DConeStacked: 100, + Bar3DConePercentStacked: 100, + Bar3DPyramidClustered: 100, + Bar3DPyramidStacked: 100, + Bar3DPyramidPercentStacked: 100, + Bar3DCylinderClustered: 100, + Bar3DCylinderStacked: 100, + Bar3DCylinderPercentStacked: 100, + Col: 100, + ColStacked: 100, + ColPercentStacked: 100, + Col3D: 100, + Col3DClustered: 100, + Col3DStacked: 100, + Col3DPercentStacked: 100, + Col3DCone: 100, + Col3DConeClustered: 100, + Col3DConeStacked: 100, + Col3DConePercentStacked: 100, + Col3DPyramid: 100, + Col3DPyramidClustered: 100, + Col3DPyramidStacked: 100, + Col3DPyramidPercentStacked: 100, + Col3DCylinder: 100, + Col3DCylinderClustered: 100, + Col3DCylinderStacked: 100, + Col3DCylinderPercentStacked: 100, + Doughnut: 100, + Line: 100, + Pie: 100, + Pie3D: 100, + Radar: 100, + Scatter: 100, } chartView3DRAngAx = map[string]int{ - Area: 0, - AreaStacked: 0, - AreaPercentStacked: 0, - Area3D: 1, - Area3DStacked: 1, - Area3DPercentStacked: 1, - Bar: 0, - BarStacked: 0, - BarPercentStacked: 0, - Bar3DClustered: 1, - Bar3DStacked: 1, - Bar3DPercentStacked: 1, - Col: 0, - ColStacked: 0, - ColPercentStacked: 0, - Col3DClustered: 1, - Col3D: 1, - Col3DStacked: 1, - Col3DPercentStacked: 1, - Doughnut: 0, - Line: 0, - Pie: 0, - Pie3D: 0, - Radar: 0, - Scatter: 0, + Area: 0, + AreaStacked: 0, + AreaPercentStacked: 0, + Area3D: 1, + Area3DStacked: 1, + Area3DPercentStacked: 1, + Bar: 0, + BarStacked: 0, + BarPercentStacked: 0, + Bar3DClustered: 1, + Bar3DStacked: 1, + Bar3DPercentStacked: 1, + Bar3DConeClustered: 1, + Bar3DConeStacked: 1, + Bar3DConePercentStacked: 1, + Bar3DPyramidClustered: 1, + Bar3DPyramidStacked: 1, + Bar3DPyramidPercentStacked: 1, + Bar3DCylinderClustered: 1, + Bar3DCylinderStacked: 1, + Bar3DCylinderPercentStacked: 1, + Col: 0, + ColStacked: 0, + ColPercentStacked: 0, + Col3D: 1, + Col3DClustered: 1, + Col3DStacked: 1, + Col3DPercentStacked: 1, + Col3DCone: 1, + Col3DConeClustered: 1, + Col3DConeStacked: 1, + Col3DConePercentStacked: 1, + Col3DPyramid: 1, + Col3DPyramidClustered: 1, + Col3DPyramidStacked: 1, + Col3DPyramidPercentStacked: 1, + Col3DCylinder: 1, + Col3DCylinderClustered: 1, + Col3DCylinderStacked: 1, + Col3DCylinderPercentStacked: 1, + Doughnut: 0, + Line: 0, + Pie: 0, + Pie3D: 0, + Radar: 0, + Scatter: 0, } chartLegendPosition = map[string]string{ "bottom": "b", @@ -163,96 +269,180 @@ var ( "top_right": "tr", } chartValAxNumFmtFormatCode = map[string]string{ - Area: "General", - AreaStacked: "General", - AreaPercentStacked: "0%", - Area3D: "General", - Area3DStacked: "General", - Area3DPercentStacked: "0%", - Bar: "General", - BarStacked: "General", - BarPercentStacked: "0%", - Bar3DClustered: "General", - Bar3DStacked: "General", - Bar3DPercentStacked: "0%", - Col: "General", - ColStacked: "General", - ColPercentStacked: "0%", - Col3DClustered: "General", - Col3D: "General", - Col3DStacked: "General", - Col3DPercentStacked: "0%", - Doughnut: "General", - Line: "General", - Pie: "General", - Pie3D: "General", - Radar: "General", - Scatter: "General", + Area: "General", + AreaStacked: "General", + AreaPercentStacked: "0%", + Area3D: "General", + Area3DStacked: "General", + Area3DPercentStacked: "0%", + Bar: "General", + BarStacked: "General", + BarPercentStacked: "0%", + Bar3DClustered: "General", + Bar3DStacked: "General", + Bar3DPercentStacked: "0%", + Bar3DConeClustered: "General", + Bar3DConeStacked: "General", + Bar3DConePercentStacked: "0%", + Bar3DPyramidClustered: "General", + Bar3DPyramidStacked: "General", + Bar3DPyramidPercentStacked: "0%", + Bar3DCylinderClustered: "General", + Bar3DCylinderStacked: "General", + Bar3DCylinderPercentStacked: "0%", + Col: "General", + ColStacked: "General", + ColPercentStacked: "0%", + Col3D: "General", + Col3DClustered: "General", + Col3DStacked: "General", + Col3DPercentStacked: "0%", + Col3DCone: "General", + Col3DConeClustered: "General", + Col3DConeStacked: "General", + Col3DConePercentStacked: "0%", + Col3DPyramid: "General", + Col3DPyramidClustered: "General", + Col3DPyramidStacked: "General", + Col3DPyramidPercentStacked: "0%", + Col3DCylinder: "General", + Col3DCylinderClustered: "General", + Col3DCylinderStacked: "General", + Col3DCylinderPercentStacked: "0%", + Doughnut: "General", + Line: "General", + Pie: "General", + Pie3D: "General", + Radar: "General", + Scatter: "General", } chartValAxCrossBetween = map[string]string{ - Area: "midCat", - AreaStacked: "midCat", - AreaPercentStacked: "midCat", - Area3D: "midCat", - Area3DStacked: "midCat", - Area3DPercentStacked: "midCat", - Bar: "between", - BarStacked: "between", - BarPercentStacked: "between", - Bar3DClustered: "between", - Bar3DStacked: "between", - Bar3DPercentStacked: "between", - Col: "between", - ColStacked: "between", - ColPercentStacked: "between", - Col3DClustered: "between", - Col3D: "between", - Col3DStacked: "between", - Col3DPercentStacked: "between", - Doughnut: "between", - Line: "between", - Pie: "between", - Pie3D: "between", - Radar: "between", - Scatter: "between", + Area: "midCat", + AreaStacked: "midCat", + AreaPercentStacked: "midCat", + Area3D: "midCat", + Area3DStacked: "midCat", + Area3DPercentStacked: "midCat", + Bar: "between", + BarStacked: "between", + BarPercentStacked: "between", + Bar3DClustered: "between", + Bar3DStacked: "between", + Bar3DPercentStacked: "between", + Bar3DConeClustered: "between", + Bar3DConeStacked: "between", + Bar3DConePercentStacked: "between", + Bar3DPyramidClustered: "between", + Bar3DPyramidStacked: "between", + Bar3DPyramidPercentStacked: "between", + Bar3DCylinderClustered: "between", + Bar3DCylinderStacked: "between", + Bar3DCylinderPercentStacked: "between", + Col: "between", + ColStacked: "between", + ColPercentStacked: "between", + Col3D: "between", + Col3DClustered: "between", + Col3DStacked: "between", + Col3DPercentStacked: "between", + Col3DCone: "between", + Col3DConeClustered: "between", + Col3DConeStacked: "between", + Col3DConePercentStacked: "between", + Col3DPyramid: "between", + Col3DPyramidClustered: "between", + Col3DPyramidStacked: "between", + Col3DPyramidPercentStacked: "between", + Col3DCylinder: "between", + Col3DCylinderClustered: "between", + Col3DCylinderStacked: "between", + Col3DCylinderPercentStacked: "between", + Doughnut: "between", + Line: "between", + Pie: "between", + Pie3D: "between", + Radar: "between", + Scatter: "between", } plotAreaChartGrouping = map[string]string{ - Area: "standard", - AreaStacked: "stacked", - AreaPercentStacked: "percentStacked", - Area3D: "standard", - Area3DStacked: "stacked", - Area3DPercentStacked: "percentStacked", - Bar: "clustered", - BarStacked: "stacked", - BarPercentStacked: "percentStacked", - Bar3DClustered: "clustered", - Bar3DStacked: "stacked", - Bar3DPercentStacked: "percentStacked", - Col: "clustered", - ColStacked: "stacked", - ColPercentStacked: "percentStacked", - Col3DClustered: "clustered", - Col3D: "standard", - Col3DStacked: "stacked", - Col3DPercentStacked: "percentStacked", - Line: "standard", + Area: "standard", + AreaStacked: "stacked", + AreaPercentStacked: "percentStacked", + Area3D: "standard", + Area3DStacked: "stacked", + Area3DPercentStacked: "percentStacked", + Bar: "clustered", + BarStacked: "stacked", + BarPercentStacked: "percentStacked", + Bar3DClustered: "clustered", + Bar3DStacked: "stacked", + Bar3DPercentStacked: "percentStacked", + Bar3DConeClustered: "clustered", + Bar3DConeStacked: "stacked", + Bar3DConePercentStacked: "percentStacked", + Bar3DPyramidClustered: "clustered", + Bar3DPyramidStacked: "stacked", + Bar3DPyramidPercentStacked: "percentStacked", + Bar3DCylinderClustered: "clustered", + Bar3DCylinderStacked: "stacked", + Bar3DCylinderPercentStacked: "percentStacked", + Col: "clustered", + ColStacked: "stacked", + ColPercentStacked: "percentStacked", + Col3D: "standard", + Col3DClustered: "clustered", + Col3DStacked: "stacked", + Col3DPercentStacked: "percentStacked", + Col3DCone: "standard", + Col3DConeClustered: "clustered", + Col3DConeStacked: "stacked", + Col3DConePercentStacked: "percentStacked", + Col3DPyramid: "standard", + Col3DPyramidClustered: "clustered", + Col3DPyramidStacked: "stacked", + Col3DPyramidPercentStacked: "percentStacked", + Col3DCylinder: "standard", + Col3DCylinderClustered: "clustered", + Col3DCylinderStacked: "stacked", + Col3DCylinderPercentStacked: "percentStacked", + Line: "standard", } plotAreaChartBarDir = map[string]string{ - Bar: "bar", - BarStacked: "bar", - BarPercentStacked: "bar", - Bar3DClustered: "bar", - Bar3DStacked: "bar", - Bar3DPercentStacked: "bar", - Col: "col", - ColStacked: "col", - ColPercentStacked: "col", - Col3DClustered: "col", - Col3D: "col", - Col3DStacked: "col", - Col3DPercentStacked: "col", - Line: "standard", + Bar: "bar", + BarStacked: "bar", + BarPercentStacked: "bar", + Bar3DClustered: "bar", + Bar3DStacked: "bar", + Bar3DPercentStacked: "bar", + Bar3DConeClustered: "bar", + Bar3DConeStacked: "bar", + Bar3DConePercentStacked: "bar", + Bar3DPyramidClustered: "bar", + Bar3DPyramidStacked: "bar", + Bar3DPyramidPercentStacked: "bar", + Bar3DCylinderClustered: "bar", + Bar3DCylinderStacked: "bar", + Bar3DCylinderPercentStacked: "bar", + Col: "col", + ColStacked: "col", + ColPercentStacked: "col", + Col3D: "col", + Col3DClustered: "col", + Col3DStacked: "col", + Col3DPercentStacked: "col", + Col3DCone: "col", + Col3DConeStacked: "col", + Col3DConeClustered: "col", + Col3DConePercentStacked: "col", + Col3DPyramid: "col", + Col3DPyramidClustered: "col", + Col3DPyramidStacked: "col", + Col3DPyramidPercentStacked: "col", + Col3DCylinder: "col", + Col3DCylinderClustered: "col", + Col3DCylinderStacked: "col", + Col3DCylinderPercentStacked: "col", + Line: "standard", } orientation = map[bool]string{ true: "maxMin", @@ -335,33 +525,54 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // // The following shows the type of chart supported by excelize: // -// Type | Chart -// ----------------------+------------------------------ -// area | 2D area chart -// areaStacked | 2D stacked area chart -// areaPercentStacked | 2D 100% stacked area chart -// area3D | 3D area chart -// area3DStacked | 3D stacked area chart -// area3DPercentStacked | 3D 100% stacked area chart -// bar | 2D clustered bar chart -// barStacked | 2D stacked bar chart -// barPercentStacked | 2D 100% stacked bar chart -// bar3DClustered | 3D clustered bar chart -// bar3DStacked | 3D stacked bar chart -// bar3DPercentStacked | 3D 100% stacked bar chart -// col | 2D clustered column chart -// colStacked | 2D stacked column chart -// colPercentStacked | 2D 100% stacked column chart -// col3DClustered | 3D clustered column chart -// col3D | 3D column chart -// col3DStacked | 3D stacked column chart -// col3DPercentStacked | 3D 100% stacked column chart -// doughnut | doughnut chart -// line | line chart -// pie | pie chart -// pie3D | 3D pie chart -// radar | radar chart -// scatter | scatter chart +// Type | Chart +// -----------------------------+------------------------------ +// area | 2D area chart +// areaStacked | 2D stacked area chart +// areaPercentStacked | 2D 100% stacked area chart +// area3D | 3D area chart +// area3DStacked | 3D stacked area chart +// area3DPercentStacked | 3D 100% stacked area chart +// bar | 2D clustered bar chart +// barStacked | 2D stacked bar chart +// barPercentStacked | 2D 100% stacked bar chart +// bar3DClustered | 3D clustered bar chart +// bar3DStacked | 3D stacked bar chart +// bar3DPercentStacked | 3D 100% stacked bar chart +// bar3DConeClustered | 3D cone clustered bar chart +// bar3DConeStacked | 3D cone stacked bar chart +// bar3DConePercentStacked | 3D cone percent bar chart +// bar3DPyramidClustered | 3D pyramid clustered bar chart +// bar3DPyramidStacked | 3D pyramid stacked bar chart +// bar3DPyramidPercentStacked | 3D pyramid percent stacked bar chart +// bar3DCylinderClustered | 3D cylinder clustered bar chart +// bar3DCylinderStacked | 3D cylinder stacked bar chart +// bar3DCylinderPercentStacked | 3D cylinder percent stacked bar chart +// col | 2D clustered column chart +// colStacked | 2D stacked column chart +// colPercentStacked | 2D 100% stacked column chart +// col3DClustered | 3D clustered column chart +// col3D | 3D column chart +// col3DStacked | 3D stacked column chart +// col3DPercentStacked | 3D 100% stacked column chart +// col3DCone | 3D cone column chart +// col3DConeClustered | 3D cone clustered column chart +// col3DConeStacked | 3D cone stacked column chart +// col3DConePercentStacked | 3D cone percent stacked column chart +// col3DPyramid | 3D pyramid column chart +// col3DPyramidClustered | 3D pyramid clustered column chart +// col3DPyramidStacked | 3D pyramid stacked column chart +// col3DPyramidPercentStacked | 3D pyramid percent stacked column chart +// col3DCylinder | 3D cylinder column chart +// col3DCylinderClustered | 3D cylinder clustered column chart +// col3DCylinderStacked | 3D cylinder stacked column chart +// col3DCylinderPercentStacked | 3D cylinder percent stacked column chart +// doughnut | doughnut chart +// line | line chart +// pie | pie chart +// pie3D | 3D pie chart +// radar | radar chart +// scatter | scatter chart // // In Excel a chart series is a collection of information that defines which data is plotted such as values, axis labels and formatting. // @@ -457,6 +668,9 @@ func (f *File) AddChart(sheet, cell, format string) error { if err != nil { return err } + if _, ok := chartValAxNumFmtFormatCode[formatSet.Type]; !ok { + return errors.New("unsupported chart type " + formatSet.Type) + } // Add first picture for given sheet, create xl/drawings/ and xl/drawings/_rels/ folder. drawingID := f.countDrawings() + 1 chartID := f.countCharts() + 1 @@ -631,31 +845,52 @@ func (f *File) addChart(formatSet *formatChart) { }, } plotAreaFunc := map[string]func(*formatChart) *cPlotArea{ - Area: f.drawBaseChart, - AreaStacked: f.drawBaseChart, - AreaPercentStacked: f.drawBaseChart, - Area3D: f.drawBaseChart, - Area3DStacked: f.drawBaseChart, - Area3DPercentStacked: f.drawBaseChart, - Bar: f.drawBaseChart, - BarStacked: f.drawBaseChart, - BarPercentStacked: f.drawBaseChart, - Bar3DClustered: f.drawBaseChart, - Bar3DStacked: f.drawBaseChart, - Bar3DPercentStacked: f.drawBaseChart, - Col: f.drawBaseChart, - ColStacked: f.drawBaseChart, - ColPercentStacked: f.drawBaseChart, - Col3DClustered: f.drawBaseChart, - Col3D: f.drawBaseChart, - Col3DStacked: f.drawBaseChart, - Col3DPercentStacked: f.drawBaseChart, - Doughnut: f.drawDoughnutChart, - Line: f.drawLineChart, - Pie3D: f.drawPie3DChart, - Pie: f.drawPieChart, - Radar: f.drawRadarChart, - Scatter: f.drawScatterChart, + Area: f.drawBaseChart, + AreaStacked: f.drawBaseChart, + AreaPercentStacked: f.drawBaseChart, + Area3D: f.drawBaseChart, + Area3DStacked: f.drawBaseChart, + Area3DPercentStacked: f.drawBaseChart, + Bar: f.drawBaseChart, + BarStacked: f.drawBaseChart, + BarPercentStacked: f.drawBaseChart, + Bar3DClustered: f.drawBaseChart, + Bar3DStacked: f.drawBaseChart, + Bar3DPercentStacked: f.drawBaseChart, + Bar3DConeClustered: f.drawBaseChart, + Bar3DConeStacked: f.drawBaseChart, + Bar3DConePercentStacked: f.drawBaseChart, + Bar3DPyramidClustered: f.drawBaseChart, + Bar3DPyramidStacked: f.drawBaseChart, + Bar3DPyramidPercentStacked: f.drawBaseChart, + Bar3DCylinderClustered: f.drawBaseChart, + Bar3DCylinderStacked: f.drawBaseChart, + Bar3DCylinderPercentStacked: f.drawBaseChart, + Col: f.drawBaseChart, + ColStacked: f.drawBaseChart, + ColPercentStacked: f.drawBaseChart, + Col3D: f.drawBaseChart, + Col3DClustered: f.drawBaseChart, + Col3DStacked: f.drawBaseChart, + Col3DPercentStacked: f.drawBaseChart, + Col3DCone: f.drawBaseChart, + Col3DConeClustered: f.drawBaseChart, + Col3DConeStacked: f.drawBaseChart, + Col3DConePercentStacked: f.drawBaseChart, + Col3DPyramid: f.drawBaseChart, + Col3DPyramidClustered: f.drawBaseChart, + Col3DPyramidStacked: f.drawBaseChart, + Col3DPyramidPercentStacked: f.drawBaseChart, + Col3DCylinder: f.drawBaseChart, + Col3DCylinderClustered: f.drawBaseChart, + Col3DCylinderStacked: f.drawBaseChart, + Col3DCylinderPercentStacked: f.drawBaseChart, + Doughnut: f.drawDoughnutChart, + Line: f.drawLineChart, + Pie3D: f.drawPie3DChart, + Pie: f.drawPieChart, + Radar: f.drawRadarChart, + Scatter: f.drawScatterChart, } xlsxChartSpace.Chart.PlotArea = plotAreaFunc[formatSet.Type](formatSet) @@ -678,6 +913,7 @@ func (f *File) drawBaseChart(formatSet *formatChart) *cPlotArea { Val: true, }, Ser: f.drawChartSeries(formatSet), + Shape: f.drawChartShape(formatSet), DLbls: f.drawChartDLbls(formatSet), AxID: []*attrValInt{ {Val: 754001152}, @@ -756,6 +992,51 @@ func (f *File) drawBaseChart(formatSet *formatChart) *cPlotArea { CatAx: catAx, ValAx: valAx, }, + "bar3DConeClustered": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "bar3DConeStacked": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "bar3DConePercentStacked": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "bar3DPyramidClustered": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "bar3DPyramidStacked": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "bar3DPyramidPercentStacked": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "bar3DCylinderClustered": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "bar3DCylinderStacked": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "bar3DCylinderPercentStacked": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, "col": { BarChart: &c, CatAx: catAx, @@ -771,12 +1052,12 @@ func (f *File) drawBaseChart(formatSet *formatChart) *cPlotArea { CatAx: catAx, ValAx: valAx, }, - "col3DClustered": { + "col3D": { Bar3DChart: &c, CatAx: catAx, ValAx: valAx, }, - "col3D": { + "col3DClustered": { Bar3DChart: &c, CatAx: catAx, ValAx: valAx, @@ -791,6 +1072,66 @@ func (f *File) drawBaseChart(formatSet *formatChart) *cPlotArea { CatAx: catAx, ValAx: valAx, }, + "col3DCone": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "col3DConeClustered": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "col3DConeStacked": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "col3DConePercentStacked": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "col3DPyramid": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "col3DPyramidClustered": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "col3DPyramidStacked": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "col3DPyramidPercentStacked": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "col3DCylinder": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "col3DCylinderClustered": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "col3DCylinderStacked": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "col3DCylinderPercentStacked": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, } return charts[formatSet.Type] } @@ -907,6 +1248,38 @@ func (f *File) drawScatterChart(formatSet *formatChart) *cPlotArea { } } +func (f *File) drawChartShape(formatSet *formatChart) *attrValString { + shapes := map[string]string{ + Bar3DConeClustered: "cone", + Bar3DConeStacked: "cone", + Bar3DConePercentStacked: "cone", + Bar3DPyramidClustered: "pyramid", + Bar3DPyramidStacked: "pyramid", + Bar3DPyramidPercentStacked: "pyramid", + Bar3DCylinderClustered: "cylinder", + Bar3DCylinderStacked: "cylinder", + Bar3DCylinderPercentStacked: "cylinder", + Col3DCone: "cone", + Col3DConeClustered: "cone", + Col3DConeStacked: "cone", + Col3DConePercentStacked: "cone", + Col3DPyramid: "pyramid", + Col3DPyramidClustered: "pyramid", + Col3DPyramidStacked: "pyramid", + Col3DPyramidPercentStacked: "pyramid", + Col3DCylinder: "cylinder", + Col3DCylinderClustered: "cylinder", + Col3DCylinderStacked: "cylinder", + Col3DCylinderPercentStacked: "cylinder", + } + if shape, ok := shapes[formatSet.Type]; ok { + return &attrValString{ + Val: shape, + } + } + return nil +} + // drawChartSeries provides a function to draw the c:ser element by given // format sets. func (f *File) drawChartSeries(formatSet *formatChart) *[]cSer { @@ -953,7 +1326,7 @@ func (f *File) drawChartSeriesSpPr(i int, formatSet *formatChart) *cSpPr { SchemeClr: &aSchemeClr{Val: "accent" + strconv.Itoa(i+1)}, } } - chartSeriesSpPr := map[string]*cSpPr{Area: nil, AreaStacked: nil, AreaPercentStacked: nil, Area3D: nil, Area3DStacked: nil, Area3DPercentStacked: nil, Bar: nil, BarStacked: nil, BarPercentStacked: nil, Bar3DClustered: nil, Bar3DStacked: nil, Bar3DPercentStacked: nil, Col: nil, ColStacked: nil, ColPercentStacked: nil, Col3DClustered: nil, Col3D: nil, Col3DStacked: nil, Col3DPercentStacked: nil, Doughnut: nil, Line: spPrLine, Pie: nil, Pie3D: nil, Radar: nil, Scatter: spPrScatter} + chartSeriesSpPr := map[string]*cSpPr{Line: spPrLine, Scatter: spPrScatter} return chartSeriesSpPr[formatSet.Type] } @@ -982,7 +1355,7 @@ func (f *File) drawChartSeriesDPt(i int, formatSet *formatChart) []*cDPt { }, }, }} - chartSeriesDPt := map[string][]*cDPt{Area: nil, AreaStacked: nil, AreaPercentStacked: nil, Area3D: nil, Area3DStacked: nil, Area3DPercentStacked: nil, Bar: nil, BarStacked: nil, BarPercentStacked: nil, Bar3DClustered: nil, Bar3DStacked: nil, Bar3DPercentStacked: nil, Col: nil, ColStacked: nil, ColPercentStacked: nil, Col3DClustered: nil, Col3D: nil, Col3DStacked: nil, Col3DPercentStacked: nil, Doughnut: nil, Line: nil, Pie: dpt, Pie3D: dpt, Radar: nil, Scatter: nil} + chartSeriesDPt := map[string][]*cDPt{Pie: dpt, Pie3D: dpt} return chartSeriesDPt[formatSet.Type] } @@ -994,8 +1367,11 @@ func (f *File) drawChartSeriesCat(v formatChartSeries, formatSet *formatChart) * F: v.Categories, }, } - chartSeriesCat := map[string]*cCat{Area: cat, AreaStacked: cat, AreaPercentStacked: cat, Area3D: cat, Area3DStacked: cat, Area3DPercentStacked: cat, Bar: cat, BarStacked: cat, BarPercentStacked: cat, Bar3DClustered: cat, Bar3DStacked: cat, Bar3DPercentStacked: cat, Col: cat, ColStacked: cat, ColPercentStacked: cat, Col3DClustered: cat, Col3D: cat, Col3DStacked: cat, Col3DPercentStacked: cat, Doughnut: cat, Line: cat, Pie: cat, Pie3D: cat, Radar: cat, Scatter: nil} - return chartSeriesCat[formatSet.Type] + chartSeriesCat := map[string]*cCat{Scatter: nil} + if _, ok := chartSeriesCat[formatSet.Type]; ok { + return nil + } + return cat } // drawChartSeriesVal provides a function to draw the c:val element by given @@ -1006,8 +1382,11 @@ func (f *File) drawChartSeriesVal(v formatChartSeries, formatSet *formatChart) * F: v.Values, }, } - chartSeriesVal := map[string]*cVal{Area: val, AreaStacked: val, AreaPercentStacked: val, Area3D: val, Area3DStacked: val, Area3DPercentStacked: val, Bar: val, BarStacked: val, BarPercentStacked: val, Bar3DClustered: val, Bar3DStacked: val, Bar3DPercentStacked: val, Col: val, ColStacked: val, ColPercentStacked: val, Col3DClustered: val, Col3D: val, Col3DStacked: val, Col3DPercentStacked: val, Doughnut: val, Line: val, Pie: val, Pie3D: val, Radar: val, Scatter: nil} - return chartSeriesVal[formatSet.Type] + chartSeriesVal := map[string]*cVal{Scatter: nil} + if _, ok := chartSeriesVal[formatSet.Type]; ok { + return nil + } + return val } // drawChartSeriesMarker provides a function to draw the c:marker element by @@ -1034,7 +1413,7 @@ func (f *File) drawChartSeriesMarker(i int, formatSet *formatChart) *cMarker { }, } } - chartSeriesMarker := map[string]*cMarker{Area: nil, AreaStacked: nil, AreaPercentStacked: nil, Area3D: nil, Area3DStacked: nil, Area3DPercentStacked: nil, Bar: nil, BarStacked: nil, BarPercentStacked: nil, Bar3DClustered: nil, Bar3DStacked: nil, Bar3DPercentStacked: nil, Col: nil, ColStacked: nil, ColPercentStacked: nil, Col3DClustered: nil, Col3D: nil, Col3DStacked: nil, Col3DPercentStacked: nil, Doughnut: nil, Line: nil, Pie: nil, Pie3D: nil, Radar: nil, Scatter: marker} + chartSeriesMarker := map[string]*cMarker{Scatter: marker} return chartSeriesMarker[formatSet.Type] } @@ -1046,7 +1425,7 @@ func (f *File) drawChartSeriesXVal(v formatChartSeries, formatSet *formatChart) F: v.Categories, }, } - chartSeriesXVal := map[string]*cCat{Area: nil, AreaStacked: nil, AreaPercentStacked: nil, Area3D: nil, Area3DStacked: nil, Area3DPercentStacked: nil, Bar: nil, BarStacked: nil, BarPercentStacked: nil, Bar3DClustered: nil, Bar3DStacked: nil, Bar3DPercentStacked: nil, Col: nil, ColStacked: nil, ColPercentStacked: nil, Col3DClustered: nil, Col3D: nil, Col3DStacked: nil, Col3DPercentStacked: nil, Doughnut: nil, Line: nil, Pie: nil, Pie3D: nil, Radar: nil, Scatter: cat} + chartSeriesXVal := map[string]*cCat{Scatter: cat} return chartSeriesXVal[formatSet.Type] } @@ -1058,7 +1437,7 @@ func (f *File) drawChartSeriesYVal(v formatChartSeries, formatSet *formatChart) F: v.Values, }, } - chartSeriesYVal := map[string]*cVal{Area: nil, AreaStacked: nil, AreaPercentStacked: nil, Area3D: nil, Area3DStacked: nil, Area3DPercentStacked: nil, Bar: nil, BarStacked: nil, BarPercentStacked: nil, Bar3DClustered: nil, Bar3DStacked: nil, Bar3DPercentStacked: nil, Col: nil, ColStacked: nil, ColPercentStacked: nil, Col3DClustered: nil, Col3D: nil, Col3DStacked: nil, Col3DPercentStacked: nil, Doughnut: nil, Line: nil, Pie: nil, Pie3D: nil, Radar: nil, Scatter: val} + chartSeriesYVal := map[string]*cVal{Scatter: val} return chartSeriesYVal[formatSet.Type] } @@ -1080,8 +1459,11 @@ func (f *File) drawChartDLbls(formatSet *formatChart) *cDLbls { // given format sets. func (f *File) drawChartSeriesDLbls(formatSet *formatChart) *cDLbls { dLbls := f.drawChartDLbls(formatSet) - chartSeriesDLbls := map[string]*cDLbls{Area: dLbls, AreaStacked: dLbls, AreaPercentStacked: dLbls, Area3D: dLbls, Area3DStacked: dLbls, Area3DPercentStacked: dLbls, Bar: dLbls, BarStacked: dLbls, BarPercentStacked: dLbls, Bar3DClustered: dLbls, Bar3DStacked: dLbls, Bar3DPercentStacked: dLbls, Col: dLbls, ColStacked: dLbls, ColPercentStacked: dLbls, Col3DClustered: dLbls, Col3D: dLbls, Col3DStacked: dLbls, Col3DPercentStacked: dLbls, Doughnut: dLbls, Line: dLbls, Pie: dLbls, Pie3D: dLbls, Radar: dLbls, Scatter: nil} - return chartSeriesDLbls[formatSet.Type] + chartSeriesDLbls := map[string]*cDLbls{Scatter: nil} + if _, ok := chartSeriesDLbls[formatSet.Type]; ok { + return nil + } + return dLbls } // drawPlotAreaCatAx provides a function to draw the c:catAx element. diff --git a/chart_test.go b/chart_test.go index b1cd3b0..e69767a 100644 --- a/chart_test.go +++ b/chart_test.go @@ -44,7 +44,7 @@ func TestChartSize(t *testing.T) { `"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"}}`) + `"title":{"name":"3D Clustered Column Chart"}}`) var ( buffer bytes.Buffer @@ -122,32 +122,55 @@ func TestAddChart(t *testing.T) { // Test add chart on not exists worksheet. assert.EqualError(t, f.AddChart("SheetN", "P1", "{}"), "sheet SheetN is not exist") - assert.NoError(t, f.AddChart("Sheet1", "P1", `{"type":"col","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"Fruit 2D Column 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"}`)) - assert.NoError(t, f.AddChart("Sheet1", "X1", `{"type":"colStacked","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"Fruit 2D Stacked Column 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"}`)) - assert.NoError(t, f.AddChart("Sheet1", "P16", `{"type":"colPercentStacked","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"Fruit 100% Stacked Column 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"}`)) - assert.NoError(t, f.AddChart("Sheet1", "X16", `{"type":"col3DClustered","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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 3D Clustered Column 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"}`)) - assert.NoError(t, f.AddChart("Sheet1", "P30", `{"type":"col3DStacked","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"Fruit 3D 100% Stacked Bar 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"}`)) - assert.NoError(t, f.AddChart("Sheet1", "X30", `{"type":"col3DPercentStacked","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"Fruit 3D 100% Stacked Column 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"}`)) - assert.NoError(t, f.AddChart("Sheet1", "P45", `{"type":"col3D","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"Fruit 3D Column 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"}`)) - assert.NoError(t, f.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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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"}`)) - assert.NoError(t, f.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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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"}`)) - assert.NoError(t, f.AddChart("Sheet2", "P16", `{"type":"doughnut","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$A$30:$D$37","values":"Sheet1!$B$30:$B$37"}],"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":"right","show_legend_key":false},"title":{"name":"Fruit Doughnut Chart"},"plotarea":{"show_bubble_size":false,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":false,"show_val":false},"show_blanks_as":"zero"}`)) - assert.NoError(t, f.AddChart("Sheet2", "X16", `{"type":"line","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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","show_legend_key":false},"title":{"name":"Fruit Line 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"}`)) - assert.NoError(t, f.AddChart("Sheet2", "P32", `{"type":"pie3D","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$A$30:$D$37","values":"Sheet1!$B$30:$B$37"}],"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 3D Pie Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":false,"show_val":false},"show_blanks_as":"zero"}`)) - assert.NoError(t, f.AddChart("Sheet2", "X32", `{"type":"pie","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$A$30:$D$37","values":"Sheet1!$B$30:$B$37"}],"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 Pie Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":false,"show_val":false},"show_blanks_as":"gap"}`)) - assert.NoError(t, f.AddChart("Sheet2", "P48", `{"type":"bar","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"Fruit 2D Clustered Bar 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"}`)) - assert.NoError(t, f.AddChart("Sheet2", "X48", `{"type":"barStacked","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"Fruit 2D Stacked Bar 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"}`)) - assert.NoError(t, f.AddChart("Sheet2", "P64", `{"type":"barPercentStacked","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"Fruit 2D Stacked 100% Bar 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"}`)) - assert.NoError(t, f.AddChart("Sheet2", "X64", `{"type":"bar3DClustered","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"Fruit 3D Clustered Bar 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"}`)) - assert.NoError(t, f.AddChart("Sheet2", "P80", `{"type":"bar3DStacked","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"Fruit 3D Stacked Bar 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","y_axis":{"maximum":7.5,"minimum":0.5}}`)) - assert.NoError(t, f.AddChart("Sheet2", "X80", `{"type":"bar3DPercentStacked","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"Fruit 3D 100% Stacked Bar 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","x_axis":{"reverse_order":true,"maximum":0,"minimum":0},"y_axis":{"reverse_order":true,"maximum":0,"minimum":0}}`)) + assert.NoError(t, f.AddChart("Sheet1", "P1", `{"type":"col","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"2D Column 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"}`)) + assert.NoError(t, f.AddChart("Sheet1", "X1", `{"type":"colStacked","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"2D Stacked Column 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"}`)) + assert.NoError(t, f.AddChart("Sheet1", "P16", `{"type":"colPercentStacked","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"100% Stacked Column 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"}`)) + assert.NoError(t, f.AddChart("Sheet1", "X16", `{"type":"col3DClustered","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"3D Clustered Column 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"}`)) + assert.NoError(t, f.AddChart("Sheet1", "P30", `{"type":"col3DStacked","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"3D 100% Stacked Bar 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"}`)) + assert.NoError(t, f.AddChart("Sheet1", "X30", `{"type":"col3DPercentStacked","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"3D 100% Stacked Column 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"}`)) + assert.NoError(t, f.AddChart("Sheet1", "AF1", `{"type":"col3DConeStacked","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"3D Column Cone Stacked 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"}`)) + assert.NoError(t, f.AddChart("Sheet1", "AF16", `{"type":"col3DConeClustered","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"3D Column Cone Clustered 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"}`)) + assert.NoError(t, f.AddChart("Sheet1", "AF30", `{"type":"col3DConePercentStacked","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"3D Column Cone Percent Stacked 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"}`)) + assert.NoError(t, f.AddChart("Sheet1", "AF45", `{"type":"col3DCone","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"3D Column Cone 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"}`)) + assert.NoError(t, f.AddChart("Sheet1", "AN1", `{"type":"col3DPyramidStacked","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"3D Column Pyramid Percent Stacked 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"}`)) + assert.NoError(t, f.AddChart("Sheet1", "AN16", `{"type":"col3DPyramidClustered","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"3D Column Pyramid Clustered 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"}`)) + assert.NoError(t, f.AddChart("Sheet1", "AN30", `{"type":"col3DPyramidPercentStacked","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"3D Column Pyramid Percent Stacked 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"}`)) + assert.NoError(t, f.AddChart("Sheet1", "AN45", `{"type":"col3DPyramid","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"3D Column Pyramid 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"}`)) + assert.NoError(t, f.AddChart("Sheet1", "AV1", `{"type":"col3DCylinderStacked","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"3D Column Cylinder Stacked 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"}`)) + assert.NoError(t, f.AddChart("Sheet1", "AV16", `{"type":"col3DCylinderClustered","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"3D Column Cylinder Clustered 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"}`)) + assert.NoError(t, f.AddChart("Sheet1", "AV30", `{"type":"col3DCylinderPercentStacked","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"3D Column Cylinder Percent Stacked 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"}`)) + assert.NoError(t, f.AddChart("Sheet1", "AV45", `{"type":"col3DCylinder","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"3D Column Cylinder 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"}`)) + assert.NoError(t, f.AddChart("Sheet1", "P45", `{"type":"col3D","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"3D Column 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"}`)) + assert.NoError(t, f.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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"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"}`)) + assert.NoError(t, f.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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"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"}`)) + assert.NoError(t, f.AddChart("Sheet2", "P16", `{"type":"doughnut","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$A$30:$D$37","values":"Sheet1!$B$30:$B$37"}],"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":"right","show_legend_key":false},"title":{"name":"Doughnut Chart"},"plotarea":{"show_bubble_size":false,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":false,"show_val":false},"show_blanks_as":"zero"}`)) + assert.NoError(t, f.AddChart("Sheet2", "X16", `{"type":"line","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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","show_legend_key":false},"title":{"name":"Line 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"}`)) + assert.NoError(t, f.AddChart("Sheet2", "P32", `{"type":"pie3D","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$A$30:$D$37","values":"Sheet1!$B$30:$B$37"}],"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":"3D Pie Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":false,"show_val":false},"show_blanks_as":"zero"}`)) + assert.NoError(t, f.AddChart("Sheet2", "X32", `{"type":"pie","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$A$30:$D$37","values":"Sheet1!$B$30:$B$37"}],"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":"Pie Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":false,"show_val":false},"show_blanks_as":"gap"}`)) + assert.NoError(t, f.AddChart("Sheet2", "P48", `{"type":"bar","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"2D Clustered Bar 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"}`)) + assert.NoError(t, f.AddChart("Sheet2", "X48", `{"type":"barStacked","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"2D Stacked Bar 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"}`)) + assert.NoError(t, f.AddChart("Sheet2", "P64", `{"type":"barPercentStacked","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"2D Stacked 100% Bar 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"}`)) + assert.NoError(t, f.AddChart("Sheet2", "X64", `{"type":"bar3DClustered","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"3D Clustered Bar 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"}`)) + assert.NoError(t, f.AddChart("Sheet2", "P80", `{"type":"bar3DStacked","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"3D Stacked Bar 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","y_axis":{"maximum":7.5,"minimum":0.5}}`)) + assert.NoError(t, f.AddChart("Sheet2", "X80", `{"type":"bar3DPercentStacked","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"3D 100% Stacked Bar 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","x_axis":{"reverse_order":true,"maximum":0,"minimum":0},"y_axis":{"reverse_order":true,"maximum":0,"minimum":0}}`)) // area series charts - assert.NoError(t, f.AddChart("Sheet2", "AF1", `{"type":"area","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"Fruit 2D Area 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"}`)) - assert.NoError(t, f.AddChart("Sheet2", "AN1", `{"type":"areaStacked","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"Fruit 2D Stacked Area 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"}`)) - assert.NoError(t, f.AddChart("Sheet2", "AF16", `{"type":"areaPercentStacked","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"Fruit 2D 100% Stacked Area 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"}`)) - assert.NoError(t, f.AddChart("Sheet2", "AN16", `{"type":"area3D","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"Fruit 3D Area 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"}`)) - assert.NoError(t, f.AddChart("Sheet2", "AF32", `{"type":"area3DStacked","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"Fruit 3D Stacked Area 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"}`)) - assert.NoError(t, f.AddChart("Sheet2", "AN32", `{"type":"area3DPercentStacked","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"Fruit 3D 100% Stacked Area 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"}`)) + assert.NoError(t, f.AddChart("Sheet2", "AF1", `{"type":"area","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"2D Area 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"}`)) + assert.NoError(t, f.AddChart("Sheet2", "AN1", `{"type":"areaStacked","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"2D Stacked Area 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"}`)) + assert.NoError(t, f.AddChart("Sheet2", "AF16", `{"type":"areaPercentStacked","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"2D 100% Stacked Area 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"}`)) + assert.NoError(t, f.AddChart("Sheet2", "AN16", `{"type":"area3D","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"3D Area 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"}`)) + assert.NoError(t, f.AddChart("Sheet2", "AF32", `{"type":"area3DStacked","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"3D Stacked Area 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"}`)) + assert.NoError(t, f.AddChart("Sheet2", "AN32", `{"type":"area3DPercentStacked","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"3D 100% Stacked Area 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"}`)) + + assert.NoError(t, f.AddChart("Sheet2", "AF48", `{"type":"bar3DCylinderStacked","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"3D Bar Cylinder Stacked 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"}`)) + assert.NoError(t, f.AddChart("Sheet2", "AF64", `{"type":"bar3DCylinderClustered","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"3D Bar Cylinder Clustered 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"}`)) + assert.NoError(t, f.AddChart("Sheet2", "AF80", `{"type":"bar3DCylinderPercentStacked","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"3D Bar Cylinder Percent Stacked 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"}`)) + + assert.NoError(t, f.AddChart("Sheet2", "AN48", `{"type":"bar3DConeStacked","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"3D Bar Cone Stacked 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"}`)) + assert.NoError(t, f.AddChart("Sheet2", "AN64", `{"type":"bar3DConeClustered","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"3D Bar Cone Clustered 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"}`)) + assert.NoError(t, f.AddChart("Sheet2", "AN80", `{"type":"bar3DConePercentStacked","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"3D Bar Cone Percent Stacked 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"}`)) + assert.NoError(t, f.AddChart("Sheet2", "AV48", `{"type":"bar3DPyramidStacked","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"3D Bar Pyramid Stacked 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"}`)) + assert.NoError(t, f.AddChart("Sheet2", "AV64", `{"type":"bar3DPyramidClustered","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"3D Bar Pyramid Clustered 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"}`)) + assert.NoError(t, f.AddChart("Sheet2", "AV80", `{"type":"bar3DPyramidPercentStacked","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"3D Bar Pyramid Percent Stacked 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"}`)) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddChart.xlsx"))) } diff --git a/xmlChart.go b/xmlChart.go index d23364c..15c8812 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -324,6 +324,7 @@ type cCharts struct { ScatterStyle *attrValString `xml:"scatterStyle"` VaryColors *attrValBool `xml:"varyColors"` Ser *[]cSer `xml:"ser"` + Shape *attrValString `xml:"shape"` DLbls *cDLbls `xml:"dLbls"` HoleSize *attrValInt `xml:"holeSize"` Smooth *attrValBool `xml:"smooth"` -- cgit v1.2.1 From 5cf1c05ed48ad92b6c58d3dfe7d3598526b77b01 Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 15 Jun 2019 20:55:56 +0800 Subject: Add surface 3D, wireframe Surface 3D, contour, and wireframe contour chart support --- chart.go | 139 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--- chart_test.go | 10 +++-- xmlChart.go | 74 ++++++++++++++++++------------- 3 files changed, 183 insertions(+), 40 deletions(-) diff --git a/chart.go b/chart.go index 8ecd8c7..c0060c9 100644 --- a/chart.go +++ b/chart.go @@ -65,6 +65,10 @@ const ( Pie3D = "pie3D" Radar = "radar" Scatter = "scatter" + Surface3D = "surface3D" + WireframeSurface3D = "wireframeSurface3D" + Contour = "contour" + WireframeContour = "wireframeContour" ) // This section defines the default value of chart properties. @@ -116,6 +120,10 @@ var ( Pie3D: 30, Radar: 0, Scatter: 0, + Surface3D: 15, + WireframeSurface3D: 15, + Contour: 90, + WireframeContour: 90, } chartView3DRotY = map[string]int{ Area: 0, @@ -164,6 +172,10 @@ var ( Pie3D: 0, Radar: 0, Scatter: 0, + Surface3D: 20, + WireframeSurface3D: 20, + Contour: 0, + WireframeContour: 0, } chartView3DDepthPercent = map[string]int{ Area: 100, @@ -212,6 +224,14 @@ var ( Pie3D: 100, Radar: 100, Scatter: 100, + Surface3D: 100, + WireframeSurface3D: 100, + Contour: 100, + WireframeContour: 100, + } + chartView3DPerspective = map[string]int{ + Contour: 0, + WireframeContour: 0, } chartView3DRAngAx = map[string]int{ Area: 0, @@ -260,6 +280,9 @@ var ( Pie3D: 0, Radar: 0, Scatter: 0, + Surface3D: 0, + WireframeSurface3D: 0, + Contour: 0, } chartLegendPosition = map[string]string{ "bottom": "b", @@ -315,6 +338,10 @@ var ( Pie3D: "General", Radar: "General", Scatter: "General", + Surface3D: "General", + WireframeSurface3D: "General", + Contour: "General", + WireframeContour: "General", } chartValAxCrossBetween = map[string]string{ Area: "midCat", @@ -363,6 +390,10 @@ var ( Pie3D: "between", Radar: "between", Scatter: "between", + Surface3D: "midCat", + WireframeSurface3D: "midCat", + Contour: "midCat", + WireframeContour: "midCat", } plotAreaChartGrouping = map[string]string{ Area: "standard", @@ -456,6 +487,10 @@ var ( true: "r", false: "l", } + valTickLblPos = map[string]string{ + Contour: "none", + WireframeContour: "none", + } ) // parseFormatChartSet provides a function to parse the format settings of the @@ -573,6 +608,10 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // pie3D | 3D pie chart // radar | radar chart // scatter | scatter chart +// surface3D | 3D surface chart +// wireframeSurface3D | 3D wireframe surface chart +// contour | contour chart +// wireframeContour | wireframe contour // // In Excel a chart series is a collection of information that defines which data is plotted such as values, axis labels and formatting. // @@ -791,6 +830,7 @@ func (f *File) addChart(formatSet *formatChart) { RotX: &attrValInt{Val: chartView3DRotX[formatSet.Type]}, RotY: &attrValInt{Val: chartView3DRotY[formatSet.Type]}, DepthPercent: &attrValInt{Val: chartView3DDepthPercent[formatSet.Type]}, + Perspective: &attrValInt{Val: chartView3DPerspective[formatSet.Type]}, RAngAx: &attrValInt{Val: chartView3DRAngAx[formatSet.Type]}, }, Floor: &cThicknessSpPr{ @@ -891,6 +931,10 @@ func (f *File) addChart(formatSet *formatChart) { Pie: f.drawPieChart, Radar: f.drawRadarChart, Scatter: f.drawScatterChart, + Surface3D: f.drawSurface3DChart, + WireframeSurface3D: f.drawSurface3DChart, + Contour: f.drawSurfaceChart, + WireframeContour: f.drawSurfaceChart, } xlsxChartSpace.Chart.PlotArea = plotAreaFunc[formatSet.Type](formatSet) @@ -1248,6 +1292,52 @@ func (f *File) drawScatterChart(formatSet *formatChart) *cPlotArea { } } +// drawSurface3DChart provides a function to draw the c:surface3DChart element by +// given format sets. +func (f *File) drawSurface3DChart(formatSet *formatChart) *cPlotArea { + plotArea := &cPlotArea{ + Surface3DChart: &cCharts{ + Ser: f.drawChartSeries(formatSet), + AxID: []*attrValInt{ + {Val: 754001152}, + {Val: 753999904}, + {Val: 832256642}, + }, + }, + CatAx: f.drawPlotAreaCatAx(formatSet), + ValAx: f.drawPlotAreaValAx(formatSet), + SerAx: f.drawPlotAreaSerAx(formatSet), + } + if formatSet.Type == WireframeSurface3D { + plotArea.Surface3DChart.Wireframe = &attrValBool{Val: true} + } + return plotArea +} + +// drawSurfaceChart provides a function to draw the c:surfaceChart element by +// given format sets. +func (f *File) drawSurfaceChart(formatSet *formatChart) *cPlotArea { + plotArea := &cPlotArea{ + SurfaceChart: &cCharts{ + Ser: f.drawChartSeries(formatSet), + AxID: []*attrValInt{ + {Val: 754001152}, + {Val: 753999904}, + {Val: 832256642}, + }, + }, + CatAx: f.drawPlotAreaCatAx(formatSet), + ValAx: f.drawPlotAreaValAx(formatSet), + SerAx: f.drawPlotAreaSerAx(formatSet), + } + if formatSet.Type == WireframeContour { + plotArea.SurfaceChart.Wireframe = &attrValBool{Val: true} + } + return plotArea +} + +// drawChartShape provides a function to draw the c:shape element by given +// format sets. func (f *File) drawChartShape(formatSet *formatChart) *attrValString { shapes := map[string]string{ Bar3DConeClustered: "cone", @@ -1273,9 +1363,7 @@ func (f *File) drawChartShape(formatSet *formatChart) *attrValString { Col3DCylinderPercentStacked: "cylinder", } if shape, ok := shapes[formatSet.Type]; ok { - return &attrValString{ - Val: shape, - } + return &attrValString{Val: shape} } return nil } @@ -1459,7 +1547,7 @@ func (f *File) drawChartDLbls(formatSet *formatChart) *cDLbls { // given format sets. func (f *File) drawChartSeriesDLbls(formatSet *formatChart) *cDLbls { dLbls := f.drawChartDLbls(formatSet) - chartSeriesDLbls := map[string]*cDLbls{Scatter: nil} + chartSeriesDLbls := map[string]*cDLbls{Scatter: nil, Surface3D: nil, WireframeSurface3D: nil, Contour: nil, WireframeContour: nil} if _, ok := chartSeriesDLbls[formatSet.Type]; ok { return nil } @@ -1476,7 +1564,7 @@ func (f *File) drawPlotAreaCatAx(formatSet *formatChart) []*cAxs { if formatSet.XAxis.Maximum == 0 { max = nil } - return []*cAxs{ + axs := []*cAxs{ { AxID: &attrValInt{Val: 754001152}, Scaling: &cScaling{ @@ -1503,6 +1591,10 @@ func (f *File) drawPlotAreaCatAx(formatSet *formatChart) []*cAxs { NoMultiLvlLbl: &attrValBool{Val: false}, }, } + if formatSet.XAxis.MajorGridlines { + axs[0].MajorGridlines = &cChartLines{SpPr: f.drawPlotAreaSpPr()} + } + return axs } // drawPlotAreaValAx provides a function to draw the c:valAx element. @@ -1515,7 +1607,7 @@ func (f *File) drawPlotAreaValAx(formatSet *formatChart) []*cAxs { if formatSet.YAxis.Maximum == 0 { max = nil } - return []*cAxs{ + axs := []*cAxs{ { AxID: &attrValInt{Val: 753999904}, Scaling: &cScaling{ @@ -1539,6 +1631,41 @@ func (f *File) drawPlotAreaValAx(formatSet *formatChart) []*cAxs { CrossBetween: &attrValString{Val: chartValAxCrossBetween[formatSet.Type]}, }, } + if formatSet.YAxis.MajorGridlines { + axs[0].MajorGridlines = &cChartLines{SpPr: f.drawPlotAreaSpPr()} + } + if pos, ok := valTickLblPos[formatSet.Type]; ok { + axs[0].TickLblPos.Val = pos + } + return axs +} + +// drawPlotAreaSerAx provides a function to draw the c:serAx element. +func (f *File) drawPlotAreaSerAx(formatSet *formatChart) []*cAxs { + min := &attrValFloat{Val: formatSet.YAxis.Minimum} + max := &attrValFloat{Val: formatSet.YAxis.Maximum} + if formatSet.YAxis.Minimum == 0 { + min = nil + } + if formatSet.YAxis.Maximum == 0 { + max = nil + } + return []*cAxs{ + { + AxID: &attrValInt{Val: 832256642}, + Scaling: &cScaling{ + Orientation: &attrValString{Val: orientation[formatSet.YAxis.ReverseOrder]}, + Max: max, + Min: min, + }, + Delete: &attrValBool{Val: false}, + AxPos: &attrValString{Val: catAxPos[formatSet.XAxis.ReverseOrder]}, + TickLblPos: &attrValString{Val: "nextTo"}, + SpPr: f.drawPlotAreaSpPr(), + TxPr: f.drawPlotAreaTxPr(), + CrossAx: &attrValInt{Val: 753999904}, + }, + } } // drawPlotAreaSpPr provides a function to draw the c:spPr element. diff --git a/chart_test.go b/chart_test.go index e69767a..326354a 100644 --- a/chart_test.go +++ b/chart_test.go @@ -160,17 +160,21 @@ func TestAddChart(t *testing.T) { assert.NoError(t, f.AddChart("Sheet2", "AN16", `{"type":"area3D","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"3D Area 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"}`)) assert.NoError(t, f.AddChart("Sheet2", "AF32", `{"type":"area3DStacked","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"3D Stacked Area 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"}`)) assert.NoError(t, f.AddChart("Sheet2", "AN32", `{"type":"area3DPercentStacked","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"3D 100% Stacked Area 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"}`)) - + // cylinder series chart assert.NoError(t, f.AddChart("Sheet2", "AF48", `{"type":"bar3DCylinderStacked","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"3D Bar Cylinder Stacked 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"}`)) assert.NoError(t, f.AddChart("Sheet2", "AF64", `{"type":"bar3DCylinderClustered","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"3D Bar Cylinder Clustered 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"}`)) assert.NoError(t, f.AddChart("Sheet2", "AF80", `{"type":"bar3DCylinderPercentStacked","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"3D Bar Cylinder Percent Stacked 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"}`)) - + // cone series chart assert.NoError(t, f.AddChart("Sheet2", "AN48", `{"type":"bar3DConeStacked","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"3D Bar Cone Stacked 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"}`)) assert.NoError(t, f.AddChart("Sheet2", "AN64", `{"type":"bar3DConeClustered","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"3D Bar Cone Clustered 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"}`)) assert.NoError(t, f.AddChart("Sheet2", "AN80", `{"type":"bar3DConePercentStacked","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"3D Bar Cone Percent Stacked 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"}`)) assert.NoError(t, f.AddChart("Sheet2", "AV48", `{"type":"bar3DPyramidStacked","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"3D Bar Pyramid Stacked 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"}`)) assert.NoError(t, f.AddChart("Sheet2", "AV64", `{"type":"bar3DPyramidClustered","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"3D Bar Pyramid Clustered 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"}`)) assert.NoError(t, f.AddChart("Sheet2", "AV80", `{"type":"bar3DPyramidPercentStacked","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"3D Bar Pyramid Percent Stacked 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"}`)) - + // surface series chart + assert.NoError(t, f.AddChart("Sheet2", "AV1", `{"type":"surface3D","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"3D Surface 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","y_axis":{"major_grid_lines":true}}`)) + assert.NoError(t, f.AddChart("Sheet2", "AV16", `{"type":"wireframeSurface3D","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"3D Wireframe Surface 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","y_axis":{"major_grid_lines":true}}`)) + assert.NoError(t, f.AddChart("Sheet2", "AV30", `{"type":"contour","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"Contour 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"}`)) + assert.NoError(t, f.AddChart("Sheet2", "BD1", `{"type":"wireframeContour","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"Wireframe Contour 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"}`)) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddChart.xlsx"))) } diff --git a/xmlChart.go b/xmlChart.go index 15c8812..ff28bd3 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -294,26 +294,30 @@ type cView3D struct { RotX *attrValInt `xml:"rotX"` RotY *attrValInt `xml:"rotY"` DepthPercent *attrValInt `xml:"depthPercent"` + Perspective *attrValInt `xml:"perspective"` RAngAx *attrValInt `xml:"rAngAx"` } // cPlotArea directly maps the plotArea element. This element specifies the // plot area of the chart. type cPlotArea struct { - Layout *string `xml:"layout"` - AreaChart *cCharts `xml:"areaChart"` - Area3DChart *cCharts `xml:"area3DChart"` - BarChart *cCharts `xml:"barChart"` - Bar3DChart *cCharts `xml:"bar3DChart"` - DoughnutChart *cCharts `xml:"doughnutChart"` - LineChart *cCharts `xml:"lineChart"` - PieChart *cCharts `xml:"pieChart"` - Pie3DChart *cCharts `xml:"pie3DChart"` - RadarChart *cCharts `xml:"radarChart"` - ScatterChart *cCharts `xml:"scatterChart"` - CatAx []*cAxs `xml:"catAx"` - ValAx []*cAxs `xml:"valAx"` - SpPr *cSpPr `xml:"spPr"` + Layout *string `xml:"layout"` + AreaChart *cCharts `xml:"areaChart"` + Area3DChart *cCharts `xml:"area3DChart"` + BarChart *cCharts `xml:"barChart"` + Bar3DChart *cCharts `xml:"bar3DChart"` + DoughnutChart *cCharts `xml:"doughnutChart"` + LineChart *cCharts `xml:"lineChart"` + PieChart *cCharts `xml:"pieChart"` + Pie3DChart *cCharts `xml:"pie3DChart"` + RadarChart *cCharts `xml:"radarChart"` + ScatterChart *cCharts `xml:"scatterChart"` + Surface3DChart *cCharts `xml:"surface3DChart"` + SurfaceChart *cCharts `xml:"surfaceChart"` + CatAx []*cAxs `xml:"catAx"` + ValAx []*cAxs `xml:"valAx"` + SerAx []*cAxs `xml:"serAx"` + SpPr *cSpPr `xml:"spPr"` } // cCharts specifies the common element of the chart. @@ -323,6 +327,7 @@ type cCharts struct { RadarStyle *attrValString `xml:"radarStyle"` ScatterStyle *attrValString `xml:"scatterStyle"` VaryColors *attrValBool `xml:"varyColors"` + Wireframe *attrValBool `xml:"wireframe"` Ser *[]cSer `xml:"ser"` Shape *attrValString `xml:"shape"` DLbls *cDLbls `xml:"dLbls"` @@ -334,23 +339,29 @@ type cCharts struct { // cAxs directly maps the catAx and valAx element. type cAxs struct { - AxID *attrValInt `xml:"axId"` - Scaling *cScaling `xml:"scaling"` - Delete *attrValBool `xml:"delete"` - AxPos *attrValString `xml:"axPos"` - NumFmt *cNumFmt `xml:"numFmt"` - MajorTickMark *attrValString `xml:"majorTickMark"` - MinorTickMark *attrValString `xml:"minorTickMark"` - TickLblPos *attrValString `xml:"tickLblPos"` - SpPr *cSpPr `xml:"spPr"` - TxPr *cTxPr `xml:"txPr"` - CrossAx *attrValInt `xml:"crossAx"` - Crosses *attrValString `xml:"crosses"` - CrossBetween *attrValString `xml:"crossBetween"` - Auto *attrValBool `xml:"auto"` - LblAlgn *attrValString `xml:"lblAlgn"` - LblOffset *attrValInt `xml:"lblOffset"` - NoMultiLvlLbl *attrValBool `xml:"noMultiLvlLbl"` + AxID *attrValInt `xml:"axId"` + Scaling *cScaling `xml:"scaling"` + Delete *attrValBool `xml:"delete"` + AxPos *attrValString `xml:"axPos"` + MajorGridlines *cChartLines `xml:"majorGridlines"` + NumFmt *cNumFmt `xml:"numFmt"` + MajorTickMark *attrValString `xml:"majorTickMark"` + MinorTickMark *attrValString `xml:"minorTickMark"` + TickLblPos *attrValString `xml:"tickLblPos"` + SpPr *cSpPr `xml:"spPr"` + TxPr *cTxPr `xml:"txPr"` + CrossAx *attrValInt `xml:"crossAx"` + Crosses *attrValString `xml:"crosses"` + CrossBetween *attrValString `xml:"crossBetween"` + Auto *attrValBool `xml:"auto"` + LblAlgn *attrValString `xml:"lblAlgn"` + LblOffset *attrValInt `xml:"lblOffset"` + NoMultiLvlLbl *attrValBool `xml:"noMultiLvlLbl"` +} + +// cChartLines directly maps the chart lines content model. +type cChartLines struct { + SpPr *cSpPr `xml:"spPr"` } // cScaling directly maps the scaling element. This element contains @@ -497,6 +508,7 @@ type cPageMargins struct { // formatChartAxis directly maps the format settings of the chart axis. type formatChartAxis struct { Crossing string `json:"crossing"` + MajorGridlines bool `json:"major_grid_lines"` MajorTickMark string `json:"major_tick_mark"` MinorTickMark string `json:"minor_tick_mark"` MinorUnitType string `json:"minor_unit_type"` -- cgit v1.2.1 From a335be7e4e6824e65f3d8a34b7b45ffa8d78fe4b Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 18 Jun 2019 23:07:44 +0800 Subject: New functions: SetDefinedName and GetDefinedName added --- excelize_test.go | 21 ++++----------- sheet.go | 80 +++++++++++++++++++++++++++++++++++++++++++++++++++++--- sheet_test.go | 45 +++++++++++++++++++++++++++++++ xmlWorkbook.go | 9 +++++++ 4 files changed, 135 insertions(+), 20 deletions(-) diff --git a/excelize_test.go b/excelize_test.go index e4b2548..c7c3aec 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -182,6 +182,11 @@ func TestSaveAsWrongPath(t *testing.T) { } } +func TestOpenReader(t *testing.T) { + _, err := OpenReader(strings.NewReader("")) + assert.EqualError(t, err, "zip: not a valid zip file") +} + func TestBrokenFile(t *testing.T) { // Test write file with broken file struct. f := File{} @@ -1033,22 +1038,6 @@ func TestHSL(t *testing.T) { t.Log(RGBToHSL(250, 50, 100)) } -func TestSearchSheet(t *testing.T) { - f, err := OpenFile(filepath.Join("test", "SharedStrings.xlsx")) - if !assert.NoError(t, err) { - t.FailNow() - } - - // Test search in a not exists worksheet. - t.Log(f.SearchSheet("Sheet4", "")) - // Test search a not exists value. - t.Log(f.SearchSheet("Sheet1", "X")) - t.Log(f.SearchSheet("Sheet1", "A")) - // Test search the coordinates where the numerical value in the range of - // "0-9" of Sheet1 is described by regular expression: - t.Log(f.SearchSheet("Sheet1", "[0-9]", true)) -} - func TestProtectSheet(t *testing.T) { f := NewFile() f.ProtectSheet("Sheet1", nil) diff --git a/sheet.go b/sheet.go index d3099fb..c0eba56 100644 --- a/sheet.go +++ b/sheet.go @@ -704,18 +704,14 @@ func (f *File) SearchSheet(sheet, value string, reg ...bool) ([]string, error) { var ( regSearch bool result []string - inElement string - r xlsxRow ) for _, r := range reg { regSearch = r } - xlsx, err := f.workSheetReader(sheet) if err != nil { return result, err } - name, ok := f.sheetMap[trimSheetName(sheet)] if !ok { return result, nil @@ -724,6 +720,17 @@ func (f *File) SearchSheet(sheet, value string, reg ...bool) ([]string, error) { output, _ := xml.Marshal(f.Sheet[name]) f.saveFileList(name, replaceWorkSheetsRelationshipsNameSpaceBytes(output)) } + return f.searchSheet(name, value, regSearch) +} + +// searchSheet provides a function to get coordinates by given worksheet name, +// cell value, and regular expression. +func (f *File) searchSheet(name, value string, regSearch bool) ([]string, error) { + var ( + inElement string + result []string + r xlsxRow + ) xml.NewDecoder(bytes.NewReader(f.readXML(name))) d := f.sharedStringsReader() @@ -1213,6 +1220,71 @@ func (f *File) GetPageLayout(sheet string, opts ...PageLayoutOptionPtr) error { return err } +// SetDefinedName provides a function to set the defined names of the workbook +// or worksheet. If not specified scopr, the default scope is workbook. +// For example: +// +// f.SetDefinedName(&excelize.DefinedName{ +// Name: "Amount", +// RefersTo: "Sheet1!$A$2:$D$5", +// Comment: "defined name comment", +// Scope: "Sheet2", +// }) +// +func (f *File) SetDefinedName(definedName *DefinedName) error { + wb := f.workbookReader() + d := xlsxDefinedName{ + Name: definedName.Name, + Comment: definedName.Comment, + Data: definedName.RefersTo, + } + if definedName.Scope != "" { + if sheetID := f.GetSheetIndex(definedName.Scope); sheetID != 0 { + sheetID-- + d.LocalSheetID = &sheetID + } + } + if wb.DefinedNames != nil { + for _, dn := range wb.DefinedNames.DefinedName { + var scope string + if dn.LocalSheetID != nil { + scope = f.GetSheetName(*dn.LocalSheetID + 1) + } + if scope == definedName.Scope && dn.Name == definedName.Name { + return errors.New("the same name already exists on scope") + } + } + wb.DefinedNames.DefinedName = append(wb.DefinedNames.DefinedName, d) + return nil + } + wb.DefinedNames = &xlsxDefinedNames{ + DefinedName: []xlsxDefinedName{d}, + } + return nil +} + +// GetDefinedName provides a function to get the defined names of the workbook +// or worksheet. +func (f *File) GetDefinedName() []DefinedName { + var definedNames []DefinedName + wb := f.workbookReader() + if wb.DefinedNames != nil { + for _, dn := range wb.DefinedNames.DefinedName { + definedName := DefinedName{ + Name: dn.Name, + Comment: dn.Comment, + RefersTo: dn.Data, + Scope: "Workbook", + } + if dn.LocalSheetID != nil { + definedName.Scope = f.GetSheetName(*dn.LocalSheetID + 1) + } + definedNames = append(definedNames, definedName) + } + } + return definedNames +} + // workSheetRelsReader provides a function to get the pointer to the structure // after deserialization of xl/worksheets/_rels/sheet%d.xml.rels. func (f *File) workSheetRelsReader(path string) *xlsxWorkbookRels { diff --git a/sheet_test.go b/sheet_test.go index beee10b..a7fd9e9 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -108,6 +108,29 @@ func TestPageLayoutOption(t *testing.T) { } } +func TestSearchSheet(t *testing.T) { + f, err := excelize.OpenFile(filepath.Join("test", "SharedStrings.xlsx")) + if !assert.NoError(t, err) { + t.FailNow() + } + // Test search in a not exists worksheet. + _, err = f.SearchSheet("Sheet4", "") + assert.EqualError(t, err, "sheet Sheet4 is not exist") + var expected []string + // Test search a not exists value. + result, err := f.SearchSheet("Sheet1", "X") + assert.NoError(t, err) + assert.EqualValues(t, expected, result) + result, err = f.SearchSheet("Sheet1", "A") + assert.NoError(t, err) + assert.EqualValues(t, []string{"A1"}, result) + // Test search the coordinates where the numerical value in the range of + // "0-9" of Sheet1 is described by regular expression: + result, err = f.SearchSheet("Sheet1", "[0-9]", true) + assert.NoError(t, err) + assert.EqualValues(t, expected, result) +} + func TestSetPageLayout(t *testing.T) { f := excelize.NewFile() // Test set page layout on not exists worksheet. @@ -142,3 +165,25 @@ func TestSetHeaderFooter(t *testing.T) { })) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetHeaderFooter.xlsx"))) } + +func TestDefinedName(t *testing.T) { + f := excelize.NewFile() + assert.NoError(t, f.SetDefinedName(&excelize.DefinedName{ + Name: "Amount", + RefersTo: "Sheet1!$A$2:$D$5", + Comment: "defined name comment", + Scope: "Sheet1", + })) + assert.NoError(t, f.SetDefinedName(&excelize.DefinedName{ + Name: "Amount", + RefersTo: "Sheet1!$A$2:$D$5", + Comment: "defined name comment", + })) + assert.EqualError(t, f.SetDefinedName(&excelize.DefinedName{ + Name: "Amount", + RefersTo: "Sheet1!$A$2:$D$5", + Comment: "defined name comment", + }), "the same name already exists on scope") + assert.Exactly(t, "Sheet1!$A$2:$D$5", f.GetDefinedName()[1].RefersTo) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestDefinedName.xlsx"))) +} diff --git a/xmlWorkbook.go b/xmlWorkbook.go index 5384977..0118685 100644 --- a/xmlWorkbook.go +++ b/xmlWorkbook.go @@ -288,3 +288,12 @@ type xlsxCustomWorkbookView struct { XWindow *int `xml:"xWindow,attr"` YWindow *int `xml:"yWindow,attr"` } + +// DefinedName directly maps the name for a cell or cell range on a +// worksheet. +type DefinedName struct { + Name string + Comment string + RefersTo string + Scope string +} -- cgit v1.2.1 From e77c462d3f1c29b009186d42832e6d5f42ac069f Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 19 Jun 2019 00:01:18 +0800 Subject: Support to create bubble and 3D bubble chart --- chart.go | 79 +++++++++++++++++++++++++++++++++++++++++++++++------------ chart_test.go | 7 +++++- xmlChart.go | 4 +++ 3 files changed, 74 insertions(+), 16 deletions(-) diff --git a/chart.go b/chart.go index c0060c9..b9439ca 100644 --- a/chart.go +++ b/chart.go @@ -69,6 +69,8 @@ const ( WireframeSurface3D = "wireframeSurface3D" Contour = "contour" WireframeContour = "wireframeContour" + Bubble = "bubble" + Bubble3D = "bubble3D" ) // This section defines the default value of chart properties. @@ -228,6 +230,8 @@ var ( WireframeSurface3D: 100, Contour: 100, WireframeContour: 100, + Bubble: 100, + Bubble3D: 100, } chartView3DPerspective = map[string]int{ Contour: 0, @@ -283,6 +287,8 @@ var ( Surface3D: 0, WireframeSurface3D: 0, Contour: 0, + Bubble: 0, + Bubble3D: 0, } chartLegendPosition = map[string]string{ "bottom": "b", @@ -342,6 +348,8 @@ var ( WireframeSurface3D: "General", Contour: "General", WireframeContour: "General", + Bubble: "General", + Bubble3D: "General", } chartValAxCrossBetween = map[string]string{ Area: "midCat", @@ -394,6 +402,8 @@ var ( WireframeSurface3D: "midCat", Contour: "midCat", WireframeContour: "midCat", + Bubble: "midCat", + Bubble3D: "midCat", } plotAreaChartGrouping = map[string]string{ Area: "standard", @@ -611,7 +621,9 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // surface3D | 3D surface chart // wireframeSurface3D | 3D wireframe surface chart // contour | contour chart -// wireframeContour | wireframe contour +// wireframeContour | wireframe contour chart +// bubble | bubble chart +// bubble3D | 3D bubble chart // // In Excel a chart series is a collection of information that defines which data is plotted such as values, axis labels and formatting. // @@ -935,6 +947,8 @@ func (f *File) addChart(formatSet *formatChart) { WireframeSurface3D: f.drawSurface3DChart, Contour: f.drawSurfaceChart, WireframeContour: f.drawSurfaceChart, + Bubble: f.drawBaseChart, + Bubble3D: f.drawBaseChart, } xlsxChartSpace.Chart.PlotArea = plotAreaFunc[formatSet.Type](formatSet) @@ -965,12 +979,13 @@ func (f *File) drawBaseChart(formatSet *formatChart) *cPlotArea { }, } var ok bool - c.BarDir.Val, ok = plotAreaChartBarDir[formatSet.Type] - if !ok { + if c.BarDir.Val, ok = plotAreaChartBarDir[formatSet.Type]; !ok { c.BarDir = nil } - c.Grouping.Val = plotAreaChartGrouping[formatSet.Type] - if formatSet.Type == "colStacked" || formatSet.Type == "barStacked" || formatSet.Type == "barPercentStacked" || formatSet.Type == "colPercentStacked" || formatSet.Type == "areaPercentStacked" { + if c.Grouping.Val, ok = plotAreaChartGrouping[formatSet.Type]; !ok { + c.Grouping = nil + } + if strings.HasSuffix(formatSet.Type, "Stacked") { c.Overlap = &attrValInt{Val: 100} } catAx := f.drawPlotAreaCatAx(formatSet) @@ -1176,6 +1191,16 @@ func (f *File) drawBaseChart(formatSet *formatChart) *cPlotArea { CatAx: catAx, ValAx: valAx, }, + "bubble": { + BubbleChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "bubble3D": { + BubbleChart: &c, + CatAx: catAx, + ValAx: valAx, + }, } return charts[formatSet.Type] } @@ -1381,14 +1406,16 @@ func (f *File) drawChartSeries(formatSet *formatChart) *[]cSer { F: formatSet.Series[k].Name, }, }, - SpPr: f.drawChartSeriesSpPr(k, formatSet), - Marker: f.drawChartSeriesMarker(k, formatSet), - DPt: f.drawChartSeriesDPt(k, formatSet), - DLbls: f.drawChartSeriesDLbls(formatSet), - Cat: f.drawChartSeriesCat(formatSet.Series[k], formatSet), - Val: f.drawChartSeriesVal(formatSet.Series[k], formatSet), - XVal: f.drawChartSeriesXVal(formatSet.Series[k], formatSet), - YVal: f.drawChartSeriesYVal(formatSet.Series[k], formatSet), + SpPr: f.drawChartSeriesSpPr(k, formatSet), + Marker: f.drawChartSeriesMarker(k, formatSet), + DPt: f.drawChartSeriesDPt(k, formatSet), + DLbls: f.drawChartSeriesDLbls(formatSet), + Cat: f.drawChartSeriesCat(formatSet.Series[k], formatSet), + Val: f.drawChartSeriesVal(formatSet.Series[k], formatSet), + XVal: f.drawChartSeriesXVal(formatSet.Series[k], formatSet), + YVal: f.drawChartSeriesYVal(formatSet.Series[k], formatSet), + BubbleSize: f.drawCharSeriesBubbleSize(formatSet.Series[k], formatSet), + Bubble3D: f.drawCharSeriesBubble3D(formatSet), }) } return &ser @@ -1525,10 +1552,32 @@ func (f *File) drawChartSeriesYVal(v formatChartSeries, formatSet *formatChart) F: v.Values, }, } - chartSeriesYVal := map[string]*cVal{Scatter: val} + chartSeriesYVal := map[string]*cVal{Scatter: val, Bubble: val, Bubble3D: val} return chartSeriesYVal[formatSet.Type] } +// drawCharSeriesBubbleSize provides a function to draw the c:bubbleSize +// element by given chart series and format sets. +func (f *File) drawCharSeriesBubbleSize(v formatChartSeries, formatSet *formatChart) *cVal { + if _, ok := map[string]bool{Bubble: true, Bubble3D: true}[formatSet.Type]; !ok { + return nil + } + return &cVal{ + NumRef: &cNumRef{ + F: v.Values, + }, + } +} + +// drawCharSeriesBubble3D provides a function to draw the c:bubble3D element +// by given format sets. +func (f *File) drawCharSeriesBubble3D(formatSet *formatChart) *attrValBool { + if _, ok := map[string]bool{Bubble3D: true}[formatSet.Type]; !ok { + return nil + } + return &attrValBool{Val: true} +} + // drawChartDLbls provides a function to draw the c:dLbls element by given // format sets. func (f *File) drawChartDLbls(formatSet *formatChart) *cDLbls { @@ -1547,7 +1596,7 @@ func (f *File) drawChartDLbls(formatSet *formatChart) *cDLbls { // given format sets. func (f *File) drawChartSeriesDLbls(formatSet *formatChart) *cDLbls { dLbls := f.drawChartDLbls(formatSet) - chartSeriesDLbls := map[string]*cDLbls{Scatter: nil, Surface3D: nil, WireframeSurface3D: nil, Contour: nil, WireframeContour: nil} + chartSeriesDLbls := map[string]*cDLbls{Scatter: nil, Surface3D: nil, WireframeSurface3D: nil, Contour: nil, WireframeContour: nil, Bubble: nil, Bubble3D: nil} if _, ok := chartSeriesDLbls[formatSet.Type]; ok { return nil } diff --git a/chart_test.go b/chart_test.go index 326354a..c0bae33 100644 --- a/chart_test.go +++ b/chart_test.go @@ -174,7 +174,12 @@ func TestAddChart(t *testing.T) { // surface series chart assert.NoError(t, f.AddChart("Sheet2", "AV1", `{"type":"surface3D","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"3D Surface 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","y_axis":{"major_grid_lines":true}}`)) assert.NoError(t, f.AddChart("Sheet2", "AV16", `{"type":"wireframeSurface3D","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"3D Wireframe Surface 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","y_axis":{"major_grid_lines":true}}`)) - assert.NoError(t, f.AddChart("Sheet2", "AV30", `{"type":"contour","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"Contour 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"}`)) + assert.NoError(t, f.AddChart("Sheet2", "AV32", `{"type":"contour","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"Contour 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"}`)) assert.NoError(t, f.AddChart("Sheet2", "BD1", `{"type":"wireframeContour","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"Wireframe Contour 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"}`)) + // bubble chart + assert.NoError(t, f.AddChart("Sheet2", "BD16", `{"type":"bubble","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"Bubble 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"}`)) + assert.NoError(t, f.AddChart("Sheet2", "BD32", `{"type":"bubble3D","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"Bubble 3D 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","x_axis":{"major_grid_lines":true},"y_axis":{"major_grid_lines":true}}`)) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddChart.xlsx"))) + + assert.EqualError(t, f.AddChart("Sheet2", "BD32", `{"type":"unknown","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"Bubble 3D 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"}`), "unsupported chart type unknown") } diff --git a/xmlChart.go b/xmlChart.go index ff28bd3..8a3a680 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -306,6 +306,7 @@ type cPlotArea struct { Area3DChart *cCharts `xml:"area3DChart"` BarChart *cCharts `xml:"barChart"` Bar3DChart *cCharts `xml:"bar3DChart"` + BubbleChart *cCharts `xml:"bubbleChart"` DoughnutChart *cCharts `xml:"doughnutChart"` LineChart *cCharts `xml:"lineChart"` PieChart *cCharts `xml:"pieChart"` @@ -323,6 +324,7 @@ type cPlotArea struct { // cCharts specifies the common element of the chart. type cCharts struct { BarDir *attrValString `xml:"barDir"` + BubbleScale *attrValFloat `xml:"bubbleScale"` Grouping *attrValString `xml:"grouping"` RadarStyle *attrValString `xml:"radarStyle"` ScatterStyle *attrValString `xml:"scatterStyle"` @@ -395,6 +397,8 @@ type cSer struct { XVal *cCat `xml:"xVal"` YVal *cVal `xml:"yVal"` Smooth *attrValBool `xml:"smooth"` + BubbleSize *cVal `xml:"bubbleSize"` + Bubble3D *attrValBool `xml:"bubble3D"` } // cMarker (Marker) directly maps the marker element. This element specifies a -- cgit v1.2.1 From 9f8623047d2fc38e12c3b214475710d25ec88c55 Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 20 Jun 2019 00:00:40 +0800 Subject: Optimize code, fix golint issues --- adjust_test.go | 8 ++++---- picture.go | 29 ++++++++++++++++------------- rows.go | 4 ++-- 3 files changed, 22 insertions(+), 19 deletions(-) diff --git a/adjust_test.go b/adjust_test.go index 364a8b8..a0de844 100644 --- a/adjust_test.go +++ b/adjust_test.go @@ -104,13 +104,13 @@ func TestAdjustCalcChain(t *testing.T) { func TestCoordinatesToAreaRef(t *testing.T) { f := NewFile() - ref, err := f.coordinatesToAreaRef([]int{}) + _, err := f.coordinatesToAreaRef([]int{}) assert.EqualError(t, err, "coordinates length must be 4") - ref, err = f.coordinatesToAreaRef([]int{1, -1, 1, 1}) + _, err = f.coordinatesToAreaRef([]int{1, -1, 1, 1}) assert.EqualError(t, err, "invalid cell coordinates [1, -1]") - ref, err = f.coordinatesToAreaRef([]int{1, 1, 1, -1}) + _, err = f.coordinatesToAreaRef([]int{1, 1, 1, -1}) assert.EqualError(t, err, "invalid cell coordinates [1, -1]") - ref, err = f.coordinatesToAreaRef([]int{1, 1, 1, 1}) + ref, err := f.coordinatesToAreaRef([]int{1, 1, 1, 1}) assert.NoError(t, err) assert.EqualValues(t, ref, "A1:A1") } diff --git a/picture.go b/picture.go index 01c2ae2..7804bce 100644 --- a/picture.go +++ b/picture.go @@ -499,26 +499,33 @@ func (f *File) getSheetRelationshipsTargetByID(sheet, rID string) string { func (f *File) GetPicture(sheet, cell string) (string, []byte, error) { col, row, err := CellNameToCoordinates(cell) if err != nil { - return "", []byte{}, err + return "", nil, err } col-- row-- xlsx, err := f.workSheetReader(sheet) if err != nil { - return "", []byte{}, err + return "", nil, err } if xlsx.Drawing == nil { - return "", []byte{}, err + return "", nil, err } - target := f.getSheetRelationshipsTargetByID(sheet, xlsx.Drawing.RID) drawingXML := strings.Replace(target, "..", "xl", -1) - + _, ok := f.XLSX[drawingXML] + if !ok { + return "", nil, err + } drawingRelationships := strings.Replace( strings.Replace(target, "../drawings", "xl/drawings/_rels", -1), ".xml", ".xml.rels", -1) - wsDr, _ := f.drawingParser(drawingXML) + return f.getPicture(row, col, drawingXML, drawingRelationships) +} +// getPicture provides a function to get picture base name and raw content +// embed in XLSX by given coordinates and drawing relationships. +func (f *File) getPicture(row, col int, drawingXML, drawingRelationships string) (string, []byte, error) { + 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 { @@ -528,16 +535,12 @@ func (f *File) GetPicture(sheet, cell string) (string, []byte, error) { if ok { return filepath.Base(xlsxWorkbookRelation.Target), []byte(f.XLSX[strings.Replace(xlsxWorkbookRelation.Target, - "..", "xl", -1)]), err + "..", "xl", -1)]), nil } } } } - _, ok := f.XLSX[drawingXML] - if !ok { - return "", nil, err - } decodeWsDr := decodeWsDr{} _ = xml.Unmarshal(namespaceStrictToTransitional(f.readXML(drawingXML)), &decodeWsDr) for _, anchor := range decodeWsDr.TwoCellAnchor { @@ -548,12 +551,12 @@ func (f *File) GetPicture(sheet, cell string) (string, []byte, error) { xlsxWorkbookRelation := f.getDrawingRelationships(drawingRelationships, decodeTwoCellAnchor.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)]), err + return filepath.Base(xlsxWorkbookRelation.Target), []byte(f.XLSX[strings.Replace(xlsxWorkbookRelation.Target, "..", "xl", -1)]), nil } } } } - return "", []byte{}, err + return "", nil, nil } // getDrawingRelationships provides a function to get drawing relationships diff --git a/rows.go b/rows.go index 064fefe..3079d5a 100644 --- a/rows.go +++ b/rows.go @@ -21,7 +21,7 @@ import ( // GetRows return all the rows in a sheet by given worksheet name (case // sensitive). For example: // -// rows, err := f.GetRows("Sheet1") +// rows, err := f.GetRows("Sheet1") // for _, row := range rows { // for _, colCell := range row { // fmt.Print(colCell, "\t") @@ -160,7 +160,7 @@ func (err ErrSheetNotExist) Error() string { // // rows, err := f.Rows("Sheet1") // for rows.Next() { -// row, err := rows.Columns() +// row, err := rows.Columns() // for _, colCell := range row { // fmt.Print(colCell, "\t") // } -- cgit v1.2.1 From 54def7eaad9ee0469ca495b3661798919239384a Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 27 Jun 2019 21:58:14 +0800 Subject: Add TIF, TIFF format images and more detailed error information when open the encrypted file --- excelize.go | 12 +++++++++++ excelize_test.go | 9 +++++++++ picture.go | 2 +- picture_test.go | 25 +++++++++++++++-------- test/images/excel.tif | Bin 0 -> 27052 bytes xmlApp.go | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++ xmlDrawing.go | 2 +- 7 files changed, 95 insertions(+), 10 deletions(-) create mode 100644 test/images/excel.tif create mode 100644 xmlApp.go diff --git a/excelize.go b/excelize.go index 6fb98c4..f636a84 100644 --- a/excelize.go +++ b/excelize.go @@ -14,6 +14,7 @@ import ( "archive/zip" "bytes" "encoding/xml" + "errors" "fmt" "io" "io/ioutil" @@ -69,6 +70,17 @@ func OpenReader(r io.Reader) (*File, error) { zr, err := zip.NewReader(bytes.NewReader(b), int64(len(b))) if err != nil { + identifier := []byte{ + // checking protect workbook by [MS-OFFCRYPTO] - v20181211 3.1 FeatureIdentifier + 0x3c, 0x00, 0x00, 0x00, 0x4d, 0x00, 0x69, 0x00, 0x63, 0x00, 0x72, 0x00, 0x6f, 0x00, 0x73, 0x00, + 0x6f, 0x00, 0x66, 0x00, 0x74, 0x00, 0x2e, 0x00, 0x43, 0x00, 0x6f, 0x00, 0x6e, 0x00, 0x74, 0x00, + 0x61, 0x00, 0x69, 0x00, 0x6e, 0x00, 0x65, 0x00, 0x72, 0x00, 0x2e, 0x00, 0x44, 0x00, 0x61, 0x00, + 0x74, 0x00, 0x61, 0x00, 0x53, 0x00, 0x70, 0x00, 0x61, 0x00, 0x63, 0x00, 0x65, 0x00, 0x73, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + } + if bytes.Contains(b, identifier) { + return nil, errors.New("not support encrypted file currently") + } return nil, err } diff --git a/excelize_test.go b/excelize_test.go index c7c3aec..c4a06a5 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -1,6 +1,7 @@ package excelize import ( + "bytes" "fmt" "image/color" _ "image/gif" @@ -185,6 +186,14 @@ func TestSaveAsWrongPath(t *testing.T) { func TestOpenReader(t *testing.T) { _, err := OpenReader(strings.NewReader("")) assert.EqualError(t, err, "zip: not a valid zip file") + _, err = OpenReader(bytes.NewReader([]byte{ + 0x3c, 0x00, 0x00, 0x00, 0x4d, 0x00, 0x69, 0x00, 0x63, 0x00, 0x72, 0x00, 0x6f, 0x00, 0x73, 0x00, + 0x6f, 0x00, 0x66, 0x00, 0x74, 0x00, 0x2e, 0x00, 0x43, 0x00, 0x6f, 0x00, 0x6e, 0x00, 0x74, 0x00, + 0x61, 0x00, 0x69, 0x00, 0x6e, 0x00, 0x65, 0x00, 0x72, 0x00, 0x2e, 0x00, 0x44, 0x00, 0x61, 0x00, + 0x74, 0x00, 0x61, 0x00, 0x53, 0x00, 0x70, 0x00, 0x61, 0x00, 0x63, 0x00, 0x65, 0x00, 0x73, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + })) + assert.EqualError(t, err, "not support encrypted file currently") } func TestBrokenFile(t *testing.T) { diff --git a/picture.go b/picture.go index 7804bce..812eb5c 100644 --- a/picture.go +++ b/picture.go @@ -385,7 +385,7 @@ func (f *File) addMedia(file []byte, ext string) string { // setContentTypePartImageExtensions provides a function to set the content // type for relationship parts and the Main Document part. func (f *File) setContentTypePartImageExtensions() { - var imageTypes = map[string]bool{"jpeg": false, "png": false, "gif": false} + var imageTypes = map[string]bool{"jpeg": false, "png": false, "gif": false, "tiff": false} content := f.contentTypesReader() for _, v := range content.Defaults { _, ok := imageTypes[v.Extension] diff --git a/picture_test.go b/picture_test.go index 890092e..9a2edda 100644 --- a/picture_test.go +++ b/picture_test.go @@ -1,8 +1,13 @@ package excelize import ( - "fmt" + _ "image/gif" + _ "image/jpeg" _ "image/png" + + _ "golang.org/x/image/tiff" + + "fmt" "io/ioutil" "os" "path/filepath" @@ -25,37 +30,41 @@ func BenchmarkAddPictureFromBytes(b *testing.B) { } func TestAddPicture(t *testing.T) { - xlsx, err := OpenFile(filepath.Join("test", "Book1.xlsx")) + f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) if !assert.NoError(t, err) { t.FailNow() } // Test add picture to worksheet with offset and location hyperlink. - err = xlsx.AddPicture("Sheet2", "I9", filepath.Join("test", "images", "excel.jpg"), + err = f.AddPicture("Sheet2", "I9", filepath.Join("test", "images", "excel.jpg"), `{"x_offset": 140, "y_offset": 120, "hyperlink": "#Sheet2!D8", "hyperlink_type": "Location"}`) if !assert.NoError(t, err) { t.FailNow() } // Test add picture to worksheet with offset, external hyperlink and positioning. - err = xlsx.AddPicture("Sheet1", "F21", filepath.Join("test", "images", "excel.jpg"), + err = f.AddPicture("Sheet1", "F21", filepath.Join("test", "images", "excel.jpg"), `{"x_offset": 10, "y_offset": 10, "hyperlink": "https://github.com/360EntSecGroup-Skylar/excelize", "hyperlink_type": "External", "positioning": "oneCell"}`) if !assert.NoError(t, err) { t.FailNow() } - file, err := ioutil.ReadFile(filepath.Join("test", "images", "excel.jpg")) + file, err := ioutil.ReadFile(filepath.Join("test", "images", "excel.png")) if !assert.NoError(t, err) { t.FailNow() } // Test add picture to worksheet from bytes. - assert.NoError(t, xlsx.AddPictureFromBytes("Sheet1", "Q1", "", "Excel Logo", ".jpg", file)) + assert.NoError(t, f.AddPictureFromBytes("Sheet1", "Q1", "", "Excel Logo", ".png", file)) // Test add picture to worksheet from bytes with illegal cell coordinates. - assert.EqualError(t, xlsx.AddPictureFromBytes("Sheet1", "A", "", "Excel Logo", ".jpg", file), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + assert.EqualError(t, f.AddPictureFromBytes("Sheet1", "A", "", "Excel Logo", ".png", file), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + + assert.NoError(t, f.AddPicture("Sheet1", "Q8", filepath.Join("test", "images", "excel.gif"), "")) + assert.NoError(t, f.AddPicture("Sheet1", "Q15", filepath.Join("test", "images", "excel.jpg"), "")) + assert.NoError(t, f.AddPicture("Sheet1", "Q22", filepath.Join("test", "images", "excel.tif"), "")) // Test write file to given path. - assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestAddPicture.xlsx"))) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddPicture.xlsx"))) } func TestAddPictureErrors(t *testing.T) { diff --git a/test/images/excel.tif b/test/images/excel.tif new file mode 100644 index 0000000..4ce5eff Binary files /dev/null and b/test/images/excel.tif differ diff --git a/xmlApp.go b/xmlApp.go new file mode 100644 index 0000000..ad414fa --- /dev/null +++ b/xmlApp.go @@ -0,0 +1,55 @@ +// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// this source code is governed by a BSD-style license that can be found in +// the LICENSE file. +// +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. + +package excelize + +import "encoding/xml" + +type xlsxProperties struct { + XMLName xml.Name `xml:"http://schemas.openxmlformats.org/officeDocument/2006/extended-properties Properties"` + Template string + Manager string + Company string + Pages int + Words int + Characters int + PresentationFormat string + Lines int + Paragraphs int + Slides int + Notes int + TotalTime int + HiddenSlides int + MMClips int + ScaleCrop bool + HeadingPairs *xlsxVectorVariant + TitlesOfParts *xlsxVectorLpstr + LinksUpToDate bool + CharactersWithSpaces int + SharedDoc bool + HyperlinkBase string + HLinks *xlsxVectorVariant + HyperlinksChanged bool + DigSig *xlsxDigSig + Application string + AppVersion string + DocSecurity int +} + +type xlsxVectorVariant struct { + Content string `xml:",innerxml"` +} + +type xlsxVectorLpstr struct { + Content string `xml:",innerxml"` +} + +type xlsxDigSig struct { + Content string `xml:",innerxml"` +} diff --git a/xmlDrawing.go b/xmlDrawing.go index 13e164e..bb468bc 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -42,7 +42,7 @@ const ( NameSpaceDublinCoreMetadataIntiative = "http://purl.org/dc/dcmitype/" ) -var supportImageTypes = map[string]string{".gif": ".gif", ".jpg": ".jpeg", ".jpeg": ".jpeg", ".png": ".png"} +var supportImageTypes = map[string]string{".gif": ".gif", ".jpg": ".jpeg", ".jpeg": ".jpeg", ".png": ".png", ".tif": ".tiff", ".tiff": ".tiff"} // xlsxCNvPr directly maps the cNvPr (Non-Visual Drawing Properties). This // element specifies non-visual canvas properties. This allows for additional -- cgit v1.2.1 From dc8210d4a7d18f6425f6f18dc383b26778883715 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 30 Jun 2019 19:50:47 +0800 Subject: Update GoDoc and typo fixed --- docProps.go | 4 ++-- sheet.go | 2 +- styles.go | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docProps.go b/docProps.go index 0f44ac4..ff19fda 100644 --- a/docProps.go +++ b/docProps.go @@ -31,7 +31,7 @@ import ( // Description | An explanation of the content of the resource. // | // LastModifiedBy | The user who performed the last modification. The identification is -// | environment-specific. +// | environment-specific. // | // Language | The language of the intellectual content of the resource. // | @@ -40,7 +40,7 @@ import ( // Revision | The topic of the content of the resource. // | // ContentStatus | The status of the content. For example: Values might include "Draft", -// | "Reviewed", and "Final" +// | "Reviewed" and "Final" // | // Category | A categorization of the content of this package. // | diff --git a/sheet.go b/sheet.go index c0eba56..a43ca6b 100644 --- a/sheet.go +++ b/sheet.go @@ -1221,7 +1221,7 @@ func (f *File) GetPageLayout(sheet string, opts ...PageLayoutOptionPtr) error { } // SetDefinedName provides a function to set the defined names of the workbook -// or worksheet. If not specified scopr, the default scope is workbook. +// or worksheet. If not specified scope, the default scope is workbook. // For example: // // f.SetDefinedName(&excelize.DefinedName{ diff --git a/styles.go b/styles.go index 1c01421..b246e30 100644 --- a/styles.go +++ b/styles.go @@ -1946,13 +1946,13 @@ func (f *File) NewConditionalStyle(style string) (int, error) { } // GetDefaultFont provides the default font name currently set in the workbook -// Documents generated by excelize start with Calibri +// Documents generated by excelize start with Calibri. func (f *File) GetDefaultFont() string { font := f.readDefaultFont() return font.Name.Val } -// SetDefaultFont changes the default font in the workbook +// SetDefaultFont changes the default font in the workbook. func (f *File) SetDefaultFont(fontName string) { font := f.readDefaultFont() font.Name.Val = fontName @@ -1962,7 +1962,7 @@ func (f *File) SetDefaultFont(fontName string) { s.CellStyles.CellStyle[0].CustomBuiltIn = &custom } -// readDefaultFont provides an unmarshalled font value +// readDefaultFont provides an unmarshalled font value. func (f *File) readDefaultFont() *xlsxFont { s := f.stylesReader() return s.Fonts.Font[0] -- cgit v1.2.1 From 8b2d4cb697420e5daeb85e5d9593c563bd77db53 Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 3 Jul 2019 00:50:10 +0800 Subject: New feature: group and ungroup sheets support New functions `GroupSheets` and `UngroupSheets` added Refactor sheet index calculation --- sheet.go | 155 ++++++++++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 109 insertions(+), 46 deletions(-) diff --git a/sheet.go b/sheet.go index a43ca6b..d579c6a 100644 --- a/sheet.go +++ b/sheet.go @@ -275,13 +275,20 @@ 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 { - for idx, name := range f.GetSheetMap() { - xlsx, _ := f.workSheetReader(name) - for _, sheetView := range xlsx.SheetViews.SheetView { - if sheetView.TabSelected { - return idx + wb := f.workbookReader() + if wb != nil { + view := wb.BookViews.WorkBookView + sheets := wb.Sheets.Sheet + var activeTab int + if len(view) > 0 { + activeTab = view[0].ActiveTab + if len(sheets) > activeTab && sheets[activeTab].SheetID != 0 { + return sheets[activeTab].SheetID } } + if len(wb.Sheets.Sheet) == 1 { + return wb.Sheets.Sheet[0].SheetID + } } return 0 } @@ -308,34 +315,26 @@ func (f *File) SetSheetName(oldName, newName string) { // worksheet index. If given sheet index is invalid, will return an empty // string. func (f *File) GetSheetName(index int) string { - content := f.workbookReader() - rels := f.workbookRelsReader() - for _, rel := range rels.Relationships { - rID, _ := strconv.Atoi(strings.TrimSuffix(strings.TrimPrefix(rel.Target, "worksheets/sheet"), ".xml")) - if rID == index { - for _, v := range content.Sheets.Sheet { - if v.ID == rel.ID { - return v.Name - } + wb := f.workbookReader() + if wb != nil { + for _, sheet := range wb.Sheets.Sheet { + if sheet.SheetID == index { + return sheet.Name } } } return "" } -// 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. +// 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. func (f *File) GetSheetIndex(name string) int { - content := f.workbookReader() - rels := f.workbookRelsReader() - for _, v := range content.Sheets.Sheet { - if v.Name == name { - for _, rel := range rels.Relationships { - if v.ID == rel.ID { - rID, _ := strconv.Atoi(strings.TrimSuffix(strings.TrimPrefix(rel.Target, "worksheets/sheet"), ".xml")) - return rID - } + wb := f.workbookReader() + if wb != nil { + for _, sheet := range wb.Sheets.Sheet { + if sheet.Name == trimSheetName(name) { + return sheet.SheetID } } } @@ -354,16 +353,11 @@ func (f *File) GetSheetIndex(name string) int { // } // func (f *File) GetSheetMap() map[int]string { - content := f.workbookReader() - rels := f.workbookRelsReader() + wb := f.workbookReader() sheetMap := map[int]string{} - for _, v := range content.Sheets.Sheet { - for _, rel := range rels.Relationships { - relStr := strings.SplitN(rel.Target, "worksheets/sheet", 2) - if rel.ID == v.ID && len(relStr) == 2 { - rID, _ := strconv.Atoi(strings.TrimSuffix(relStr[1], ".xml")) - sheetMap[rID] = v.Name - } + if wb != nil { + for _, sheet := range wb.Sheets.Sheet { + sheetMap[sheet.SheetID] = sheet.Name } } return sheetMap @@ -411,19 +405,31 @@ 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) { - content := f.workbookReader() - for k, v := range content.Sheets.Sheet { - if v.Name == trimSheetName(name) && len(content.Sheets.Sheet) > 1 { - content.Sheets.Sheet = append(content.Sheets.Sheet[:k], content.Sheets.Sheet[k+1:]...) - sheet := "xl/worksheets/sheet" + strconv.Itoa(v.SheetID) + ".xml" - rels := "xl/worksheets/_rels/sheet" + strconv.Itoa(v.SheetID) + ".xml.rels" - target := f.deleteSheetFromWorkbookRels(v.ID) + if f.SheetCount == 1 || f.GetSheetIndex(name) == 0 { + return + } + sheetName := trimSheetName(name) + wb := f.workbookReader() + wbRels := f.workbookRelsReader() + for idx, sheet := range wb.Sheets.Sheet { + if sheet.Name == sheetName { + wb.Sheets.Sheet = append(wb.Sheets.Sheet[:idx], wb.Sheets.Sheet[idx+1:]...) + var sheetXML, rels string + if wbRels != nil { + for _, rel := range wbRels.Relationships { + if rel.ID == sheet.ID { + sheetXML = fmt.Sprintf("xl/%s", rel.Target) + rels = strings.Replace(fmt.Sprintf("xl/%s.rels", rel.Target), "xl/worksheets/", "xl/worksheets/_rels/", -1) + } + } + } + target := f.deleteSheetFromWorkbookRels(sheet.ID) f.deleteSheetFromContentTypes(target) - f.deleteCalcChain(v.SheetID, "") // Delete CalcChain - delete(f.sheetMap, name) - delete(f.XLSX, sheet) + f.deleteCalcChain(sheet.SheetID, "") // Delete CalcChain + delete(f.sheetMap, sheetName) + delete(f.XLSX, sheetXML) delete(f.XLSX, rels) - delete(f.Sheet, sheet) + delete(f.Sheet, sheetXML) f.SheetCount-- } } @@ -1285,6 +1291,63 @@ func (f *File) GetDefinedName() []DefinedName { return definedNames } +// GroupSheets provides a function to group worksheets by given worksheets +// name. Group worksheets must contain an active worksheet. +func (f *File) GroupSheets(sheets []string) error { + // check an active worksheet in group worksheets + var inActiveSheet bool + activeSheet := f.GetActiveSheetIndex() + sheetMap := f.GetSheetMap() + for idx, sheetName := range sheetMap { + for _, s := range sheets { + if s == sheetName && idx == activeSheet { + inActiveSheet = true + } + } + } + if !inActiveSheet { + return errors.New("group worksheet must contain an active worksheet") + } + // check worksheet exists + ws := []*xlsxWorksheet{} + for _, sheet := range sheets { + xlsx, err := f.workSheetReader(sheet) + if err != nil { + return err + } + ws = append(ws, xlsx) + } + for _, s := range ws { + sheetViews := s.SheetViews.SheetView + if len(sheetViews) > 0 { + for idx := range sheetViews { + s.SheetViews.SheetView[idx].TabSelected = true + } + continue + } + } + return nil +} + +// 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 { + continue + } + xlsx, _ := f.workSheetReader(sheet) + sheetViews := xlsx.SheetViews.SheetView + if len(sheetViews) > 0 { + for idx := range sheetViews { + xlsx.SheetViews.SheetView[idx].TabSelected = false + } + } + } + return nil +} + // workSheetRelsReader provides a function to get the pointer to the structure // after deserialization of xl/worksheets/_rels/sheet%d.xml.rels. func (f *File) workSheetRelsReader(path string) *xlsxWorkbookRels { -- cgit v1.2.1 From e780aa27c8037fadbc4a7c08466b52d0f4ac268a Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 4 Jul 2019 16:15:20 +0800 Subject: Add unit test for GroupSheets and UngroupSheets --- sheet_test.go | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/sheet_test.go b/sheet_test.go index a7fd9e9..3a7f579 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -187,3 +187,24 @@ func TestDefinedName(t *testing.T) { assert.Exactly(t, "Sheet1!$A$2:$D$5", f.GetDefinedName()[1].RefersTo) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestDefinedName.xlsx"))) } + +func TestGroupSheets(t *testing.T) { + f := excelize.NewFile() + sheets := []string{"Sheet2", "Sheet3"} + for _, sheet := range sheets { + f.NewSheet(sheet) + } + assert.EqualError(t, f.GroupSheets([]string{"Sheet1", "SheetN"}), "sheet SheetN is not exist") + assert.EqualError(t, f.GroupSheets([]string{"Sheet2", "Sheet3"}), "group worksheet must contain an active worksheet") + assert.NoError(t, f.GroupSheets([]string{"Sheet1", "Sheet2"})) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestGroupSheets.xlsx"))) +} + +func TestUngroupSheets(t *testing.T) { + f := excelize.NewFile() + sheets := []string{"Sheet2", "Sheet3", "Sheet4", "Sheet5"} + for _, sheet := range sheets { + f.NewSheet(sheet) + } + assert.NoError(t, f.UngroupSheets()) +} -- cgit v1.2.1 From 4897276c68474c5a3e16ac4e07fae55738c66eca Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 5 Jul 2019 23:15:39 +0800 Subject: Make fitToHeight tag omit empty --- xmlWorksheet.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 4d19cde..b94c521 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -90,7 +90,7 @@ type xlsxPageSetUp struct { Draft bool `xml:"draft,attr,omitempty"` Errors string `xml:"errors,attr,omitempty"` FirstPageNumber int `xml:"firstPageNumber,attr,omitempty"` - FitToHeight *int `xml:"fitToHeight,attr"` + FitToHeight int `xml:"fitToHeight,attr,omitempty"` FitToWidth int `xml:"fitToWidth,attr,omitempty"` HorizontalDPI float32 `xml:"horizontalDpi,attr,omitempty"` RID string `xml:"http://schemas.openxmlformats.org/officeDocument/2006/relationships id,attr,omitempty"` -- cgit v1.2.1 From e14d2febc880f5dc0a8352f9f57af5ac3a9d37f5 Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 6 Jul 2019 15:11:51 +0800 Subject: Resolve #432, supplement the function of SetPageLayout SetPageLayout support to set fit to width and height --- sheet.go | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/sheet.go b/sheet.go index d579c6a..1c19e86 100644 --- a/sheet.go +++ b/sheet.go @@ -1025,6 +1025,10 @@ type ( PageLayoutOrientation string // PageLayoutPaperSize defines the paper size of the worksheet PageLayoutPaperSize int + // FitToHeight specified number of vertical pages to fit on + FitToHeight int + // FitToWidth specified number of horizontal pages to fit on + FitToWidth int ) const ( @@ -1064,6 +1068,38 @@ func (p *PageLayoutPaperSize) getPageLayout(ps *xlsxPageSetUp) { *p = PageLayoutPaperSize(ps.PaperSize) } +// setPageLayout provides a method to set the fit to height for the worksheet. +func (p FitToHeight) setPageLayout(ps *xlsxPageSetUp) { + if int(p) > 0 { + ps.FitToHeight = int(p) + } +} + +// getPageLayout provides a method to get the fit to height for the worksheet. +func (p *FitToHeight) getPageLayout(ps *xlsxPageSetUp) { + if ps == nil || ps.FitToHeight == 0 { + *p = 1 + return + } + *p = FitToHeight(ps.FitToHeight) +} + +// setPageLayout provides a method to set the fit to width for the worksheet. +func (p FitToWidth) setPageLayout(ps *xlsxPageSetUp) { + if int(p) > 0 { + ps.FitToWidth = int(p) + } +} + +// getPageLayout provides a method to get the fit to width for the worksheet. +func (p *FitToWidth) getPageLayout(ps *xlsxPageSetUp) { + if ps == nil || ps.FitToWidth == 0 { + *p = 1 + return + } + *p = FitToWidth(ps.FitToWidth) +} + // SetPageLayout provides a function to sets worksheet page layout. // // Available options: @@ -1213,6 +1249,8 @@ func (f *File) SetPageLayout(sheet string, opts ...PageLayoutOption) error { // Available options: // PageLayoutOrientation(string) // PageLayoutPaperSize(int) +// FitToHeight(int) +// FitToWidth(int) func (f *File) GetPageLayout(sheet string, opts ...PageLayoutOptionPtr) error { s, err := f.workSheetReader(sheet) if err != nil { -- cgit v1.2.1 From 14d490c83d9744e635c88bca9018994dfe8981af Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 7 Jul 2019 00:17:15 +0800 Subject: Add unit test for SetPageLayout --- sheet_test.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/sheet_test.go b/sheet_test.go index 3a7f579..ef795ad 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -23,6 +23,8 @@ func ExampleFile_SetPageLayout() { if err := f.SetPageLayout( "Sheet1", excelize.PageLayoutPaperSize(10), + excelize.FitToHeight(2), + excelize.FitToWidth(2), ); err != nil { panic(err) } @@ -34,6 +36,8 @@ func ExampleFile_GetPageLayout() { var ( orientation excelize.PageLayoutOrientation paperSize excelize.PageLayoutPaperSize + fitToHeight excelize.FitToHeight + fitToWidth excelize.FitToWidth ) if err := f.GetPageLayout("Sheet1", &orientation); err != nil { panic(err) @@ -41,13 +45,24 @@ func ExampleFile_GetPageLayout() { if err := f.GetPageLayout("Sheet1", &paperSize); err != nil { panic(err) } + if err := f.GetPageLayout("Sheet1", &fitToHeight); err != nil { + panic(err) + } + + if err := f.GetPageLayout("Sheet1", &fitToWidth); err != nil { + panic(err) + } fmt.Println("Defaults:") fmt.Printf("- orientation: %q\n", orientation) fmt.Printf("- paper size: %d\n", paperSize) + fmt.Printf("- fit to height: %d\n", fitToHeight) + fmt.Printf("- fit to width: %d\n", fitToWidth) // Output: // Defaults: // - orientation: "portrait" // - paper size: 1 + // - fit to height: 1 + // - fit to width: 1 } func TestPageLayoutOption(t *testing.T) { @@ -59,6 +74,8 @@ func TestPageLayoutOption(t *testing.T) { }{ {new(excelize.PageLayoutOrientation), excelize.PageLayoutOrientation(excelize.OrientationLandscape)}, {new(excelize.PageLayoutPaperSize), excelize.PageLayoutPaperSize(10)}, + {new(excelize.FitToHeight), excelize.FitToHeight(2)}, + {new(excelize.FitToWidth), excelize.FitToWidth(2)}, } for i, test := range testData { -- cgit v1.2.1 From 821b90d1f0e53518c84dce732e265348c46e8f12 Mon Sep 17 00:00:00 2001 From: Farmerx Date: Mon, 8 Jul 2019 15:18:36 +0800 Subject: # add: add Remarks for CalcChain c Attributes --- xmlCalcChain.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/xmlCalcChain.go b/xmlCalcChain.go index 9c916bf..f34aab1 100644 --- a/xmlCalcChain.go +++ b/xmlCalcChain.go @@ -18,6 +18,19 @@ type xlsxCalcChain struct { } // xlsxCalcChainC directly maps the c element. +// | Attributes | Description | +// ----------------------------------------------------------------------------------- +// | a (Array) | A Boolean flag indicating whether the cell's formula is an array formula. True if this cell's formula is an array formula, false otherwise. If there is a conflict between this attribute and the t attribute of the f element (§18.3.1.40), the t attribute takes precedence.、 The possible values for this attribute are defined by the W3C XML Schema boolean datatype. +// ------------------------------------------------------------------------------------ +// | i (Sheet Id) | A sheet Id of a sheet the cell belongs to. If this is omitted, it is assumed to be the same as the i value of the previous cell.The possible values for this attribute are defined by the W3C XML Schema int datatype. +// --------------------------------------------------- +// | l (New Dependency Level) | A Boolean flag indicating that the cell's formula starts a new dependency level. True if the formula starts a new dependency level, false otherwise.Starting a new dependency level means that all concurrent calculations, and child calculations, shall be completed - and the cells have new values - before the calc chain can continue. In other words, this dependency level might depend on levels that came before it, and any later dependency levels might depend on this level; but not later dependency levels can have any calculations started until this dependency level completes.The possible values for this attribute are defined by the W3C XML Schema boolean datatype. +// ------------------------------------------------- +// | r (Cell Reference) | An A-1 style reference to a cell.The possible values for this attribute are defined by the ST_CellRef simple type (§18.18.7). +// -------------------------------------------------- +// | s (Child Chain) | A Boolean flag indicating whether the cell's formula is on a child chain. True if this cell is part of a child chain, false otherwise. If this is omitted, it is assumed to be the same as the s value of the previous cell .A child chain is a list of calculations that occur which depend on the parent to the chain. There shall not be cross dependencies between child chains. Child chains are not the same as dependency levels - a child chain and its parent are all on the same dependency level. Child chains are series of calculations that can be independently farmed out to other threads or processors.The possible values for this attribute are defined by the W3C XML Schema boolean datatype. +// --------------------------------------------------- +// | t (New Thread) | A Boolean flag indicating whether the cell's formula starts a new thread. True if the cell's formula starts a new thread, false otherwise.The possible values for this attribute are defined by the W3C XML Schema boolean datatype. type xlsxCalcChainC struct { R string `xml:"r,attr"` I int `xml:"i,attr"` @@ -26,3 +39,4 @@ type xlsxCalcChainC struct { T bool `xml:"t,attr,omitempty"` A bool `xml:"a,attr,omitempty"` } + -- cgit v1.2.1 From 6962061200e2198acc39392c35344d1826268f34 Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 8 Jul 2019 20:17:47 +0800 Subject: add comments for SheetView parameters --- sheetview.go | 36 +++++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/sheetview.go b/sheetview.go index 8ffc9bc..91260fe 100644 --- a/sheetview.go +++ b/sheetview.go @@ -23,22 +23,36 @@ type SheetViewOptionPtr interface { } type ( - // DefaultGridColor is a SheetViewOption. + // DefaultGridColor specified a flag indicating that the consuming + // application should use the default grid lines color (system dependent). + // Overrides any color specified in colorId. DefaultGridColor bool - // RightToLeft is a SheetViewOption. + // RightToLeft specified a flag indicating whether the sheet is in 'right to + // left' display mode. When in this mode, Column A is on the far right, + // Column B ;is one column left of Column A, and so on. Also, information in + // cells is displayed in the Right to Left format. RightToLeft bool - // ShowFormulas is a SheetViewOption. + // ShowFormulas specified a flag indicating whether this sheet should display + // formulas. ShowFormulas bool - // ShowGridLines is a SheetViewOption. + // ShowGridLines specified a flag indicating whether this sheet should + // display gridlines. ShowGridLines bool - // ShowRowColHeaders is a SheetViewOption. + // ShowRowColHeaders specified a flag indicating whether the sheet should + // display row and column headings. ShowRowColHeaders bool - // ZoomScale is a SheetViewOption. + // ZoomScale specified a window zoom magnification for current view + // representing percent values. This attribute is restricted to values + // ranging from 10 to 400. Horizontal & Vertical scale together. ZoomScale float64 - // TopLeftCell is a SheetViewOption. + // TopLeftCell specified a location of the top left visible cell Location of + // the top left visible cell in the bottom right pane (when in Left-to-Right + // mode). TopLeftCell string /* TODO - // ShowWhiteSpace is a SheetViewOption. + // ShowWhiteSpace specified flag indicating whether page layout view shall + // display margins. False means do not display left, right, top (header), and + // bottom (footer) margins (even when there is data in the header or footer). ShowWhiteSpace bool // ShowZeros is a SheetViewOption. ShowZeros bool @@ -98,7 +112,7 @@ func (o *ShowRowColHeaders) getSheetViewOption(view *xlsxSheetView) { } func (o ZoomScale) setSheetViewOption(view *xlsxSheetView) { - //This attribute is restricted to values ranging from 10 to 400. + // This attribute is restricted to values ranging from 10 to 400. if float64(o) >= 10 && float64(o) <= 400 { view.ZoomScale = float64(o) } @@ -135,6 +149,8 @@ func (f *File) getSheetView(sheetName string, viewIndex int) (*xlsxSheetView, er // ShowFormulas(bool) // ShowGridLines(bool) // ShowRowColHeaders(bool) +// ZoomScale(float64) +// TopLeftCell(string) // Example: // err = f.SetSheetViewOptions("Sheet1", -1, ShowGridLines(false)) func (f *File) SetSheetViewOptions(name string, viewIndex int, opts ...SheetViewOption) error { @@ -158,6 +174,8 @@ func (f *File) SetSheetViewOptions(name string, viewIndex int, opts ...SheetView // ShowFormulas(bool) // ShowGridLines(bool) // ShowRowColHeaders(bool) +// ZoomScale(float64) +// TopLeftCell(string) // Example: // var showGridLines excelize.ShowGridLines // err = f.GetSheetViewOptions("Sheet1", -1, &showGridLines) -- cgit v1.2.1 From 363a05734591c4d1056958bec758a3819bed90d8 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 14 Jul 2019 13:13:10 +0800 Subject: Structure update #434 Add a missing element of the comment text elements --- xmlComments.go | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/xmlComments.go b/xmlComments.go index 5ffbecf..47d8f51 100644 --- a/xmlComments.go +++ b/xmlComments.go @@ -54,7 +54,20 @@ type xlsxComment struct { // spreadsheet application implementation detail. A recommended guideline is // 32767 chars. type xlsxText struct { - R []xlsxR `xml:"r"` + T *string `xml:"t"` + R []xlsxR `xml:"r"` + RPh *xlsxPhoneticRun `xml:"rPh"` + PhoneticPr *xlsxPhoneticPr `xml:"phoneticPr"` +} + +// xlsxPhoneticRun element represents a run of text which displays a phonetic +// hint for this String Item (si). Phonetic hints are used to give information +// about the pronunciation of an East Asian language. The hints are displayed +// as text within the spreadsheet cells across the top portion of the cell. +type xlsxPhoneticRun struct { + Sb uint32 `xml:"sb,attr"` + Eb uint32 `xml:"eb,attr"` + T string `xml:"t,attr"` } // formatComment directly maps the format settings of the comment. -- cgit v1.2.1 From 74c61126581736e2214e440718fba9843d91ab94 Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 15 Jul 2019 09:13:55 +0800 Subject: Fix #434, add missing comments --- comment.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/comment.go b/comment.go index af70820..bc6fa27 100644 --- a/comment.go +++ b/comment.go @@ -42,6 +42,9 @@ func (f *File) GetComments() (comments map[string][]Comment) { } sheetComment.Ref = comment.Ref sheetComment.AuthorID = comment.AuthorID + if comment.Text.T != nil { + sheetComment.Text += *comment.Text.T + } for _, text := range comment.Text.R { sheetComment.Text += text.T } -- cgit v1.2.1 From 4f469530de9877f94001da1056de7d320b6dae35 Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 17 Jul 2019 19:25:41 +0800 Subject: Update docs --- xmlCalcChain.go | 69 +++++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 55 insertions(+), 14 deletions(-) diff --git a/xmlCalcChain.go b/xmlCalcChain.go index f34aab1..05a176d 100644 --- a/xmlCalcChain.go +++ b/xmlCalcChain.go @@ -18,19 +18,61 @@ type xlsxCalcChain struct { } // xlsxCalcChainC directly maps the c element. -// | Attributes | Description | -// ----------------------------------------------------------------------------------- -// | a (Array) | A Boolean flag indicating whether the cell's formula is an array formula. True if this cell's formula is an array formula, false otherwise. If there is a conflict between this attribute and the t attribute of the f element (§18.3.1.40), the t attribute takes precedence.、 The possible values for this attribute are defined by the W3C XML Schema boolean datatype. -// ------------------------------------------------------------------------------------ -// | i (Sheet Id) | A sheet Id of a sheet the cell belongs to. If this is omitted, it is assumed to be the same as the i value of the previous cell.The possible values for this attribute are defined by the W3C XML Schema int datatype. -// --------------------------------------------------- -// | l (New Dependency Level) | A Boolean flag indicating that the cell's formula starts a new dependency level. True if the formula starts a new dependency level, false otherwise.Starting a new dependency level means that all concurrent calculations, and child calculations, shall be completed - and the cells have new values - before the calc chain can continue. In other words, this dependency level might depend on levels that came before it, and any later dependency levels might depend on this level; but not later dependency levels can have any calculations started until this dependency level completes.The possible values for this attribute are defined by the W3C XML Schema boolean datatype. -// ------------------------------------------------- -// | r (Cell Reference) | An A-1 style reference to a cell.The possible values for this attribute are defined by the ST_CellRef simple type (§18.18.7). -// -------------------------------------------------- -// | s (Child Chain) | A Boolean flag indicating whether the cell's formula is on a child chain. True if this cell is part of a child chain, false otherwise. If this is omitted, it is assumed to be the same as the s value of the previous cell .A child chain is a list of calculations that occur which depend on the parent to the chain. There shall not be cross dependencies between child chains. Child chains are not the same as dependency levels - a child chain and its parent are all on the same dependency level. Child chains are series of calculations that can be independently farmed out to other threads or processors.The possible values for this attribute are defined by the W3C XML Schema boolean datatype. -// --------------------------------------------------- -// | t (New Thread) | A Boolean flag indicating whether the cell's formula starts a new thread. True if the cell's formula starts a new thread, false otherwise.The possible values for this attribute are defined by the W3C XML Schema boolean datatype. +// +// Attributes | Attributes +// --------------------------+---------------------------------------------------------- +// a (Array) | A Boolean flag indicating whether the cell's formula +// | is an array formula. True if this cell's formula is +// | an array formula, false otherwise. If there is a +// | conflict between this attribute and the t attribute +// | of the f element (§18.3.1.40), the t attribute takes +// | precedence. The possible values for this attribute +// | are defined by the W3C XML Schema boolean datatype. +// | +// i (Sheet Id) | A sheet Id of a sheet the cell belongs to. If this is +// | omitted, it is assumed to be the same as the i value +// | of the previous cell.The possible values for this +// | attribute are defined by the W3C XML Schema int datatype. +// | +// l (New Dependency Level) | A Boolean flag indicating that the cell's formula +// | starts a new dependency level. True if the formula +// | starts a new dependency level, false otherwise. +// | Starting a new dependency level means that all +// | concurrent calculations, and child calculations, shall +// | be completed - and the cells have new values - before +// | the calc chain can continue. In other words, this +// | dependency level might depend on levels that came before +// | it, and any later dependency levels might depend on +// | this level; but not later dependency levels can have +// | any calculations started until this dependency level +// | completes.The possible values for this attribute are +// | defined by the W3C XML Schema boolean datatype. +// | +// r (Cell Reference) | An A-1 style reference to a cell.The possible values +// | for this attribute are defined by the ST_CellRef +// | simple type (§18.18.7). +// | +// s (Child Chain) | A Boolean flag indicating whether the cell's formula +// | is on a child chain. True if this cell is part of a +// | child chain, false otherwise. If this is omitted, it +// | is assumed to be the same as the s value of the +// | previous cell .A child chain is a list of calculations +// | that occur which depend on the parent to the chain. +// | There shall not be cross dependencies between child +// | chains. Child chains are not the same as dependency +// | levels - a child chain and its parent are all on the +// | same dependency level. Child chains are series of +// | calculations that can be independently farmed out to +// | other threads or processors.The possible values for +// | this attribute are defined by the W3C XML Schema +// | boolean datatype. +// | +// t (New Thread) | A Boolean flag indicating whether the cell's formula +// | starts a new thread. True if the cell's formula starts +// | a new thread, false otherwise.The possible values for +// | this attribute are defined by the W3C XML Schema +// | boolean datatype. +// type xlsxCalcChainC struct { R string `xml:"r,attr"` I int `xml:"i,attr"` @@ -39,4 +81,3 @@ type xlsxCalcChainC struct { T bool `xml:"t,attr,omitempty"` A bool `xml:"a,attr,omitempty"` } - -- cgit v1.2.1 From 855c3605f6fce4916cdde1dadba2dd73d9f4b744 Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 20 Jul 2019 19:24:57 +0800 Subject: Fix #437, recalculate offset for merged cells adjuster --- adjust.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/adjust.go b/adjust.go index 56d812f..f26f132 100644 --- a/adjust.go +++ b/adjust.go @@ -231,7 +231,8 @@ func (f *File) adjustMergeCells(xlsx *xlsxWorksheet, dir adjustDirection, num, o return nil } - for i, areaData := range xlsx.MergeCells.Cells { + for i := 0; i < len(xlsx.MergeCells.Cells); i++ { + areaData := xlsx.MergeCells.Cells[i] coordinates, err := f.areaRefToCoordinates(areaData.Ref) if err != nil { return err @@ -240,18 +241,21 @@ func (f *File) adjustMergeCells(xlsx *xlsxWorksheet, dir adjustDirection, num, o if dir == rows { if y1 == num && y2 == num && offset < 0 { f.deleteMergeCell(xlsx, i) + i-- } y1 = f.adjustMergeCellsHelper(y1, num, offset) y2 = f.adjustMergeCellsHelper(y2, num, offset) } else { if x1 == num && x2 == num && offset < 0 { f.deleteMergeCell(xlsx, i) + i-- } x1 = f.adjustMergeCellsHelper(x1, num, offset) x2 = f.adjustMergeCellsHelper(x2, num, offset) } if x1 == x2 && y1 == y2 { f.deleteMergeCell(xlsx, i) + i-- } if areaData.Ref, err = f.coordinatesToAreaRef([]int{x1, y1, x2, y2}); err != nil { return err @@ -276,7 +280,7 @@ func (f *File) adjustMergeCellsHelper(pivot, num, offset int) int { // deleteMergeCell provides a function to delete merged cell by given index. func (f *File) deleteMergeCell(sheet *xlsxWorksheet, idx int) { - if len(sheet.MergeCells.Cells) > 1 { + if len(sheet.MergeCells.Cells) > idx { sheet.MergeCells.Cells = append(sheet.MergeCells.Cells[:idx], sheet.MergeCells.Cells[idx+1:]...) sheet.MergeCells.Count = len(sheet.MergeCells.Cells) } else { -- cgit v1.2.1 From 35e485756f1d3f5eb1e5f78a5cee06b5ed902645 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 21 Jul 2019 12:56:36 +0800 Subject: Resolve #217, new function add VBA project supported. --- excelize.go | 77 ++++++++++++++++++++++++++++++++++++++++++++++++++++ excelize_test.go | 11 ++++++++ test/vbaProject.bin | Bin 0 -> 16896 bytes xmlDrawing.go | 1 + 4 files changed, 89 insertions(+) create mode 100755 test/vbaProject.bin diff --git a/excelize.go b/excelize.go index f636a84..c7eff10 100644 --- a/excelize.go +++ b/excelize.go @@ -19,7 +19,9 @@ import ( "io" "io/ioutil" "os" + "path" "strconv" + "strings" ) // File define a populated XLSX file struct. @@ -226,3 +228,78 @@ func (f *File) UpdateLinkedValue() error { } return nil } + +// AddVBAProject provides the method to add vbaProject.bin file which contains +// functions and/or macros. The file extension should be .xlsm. For example: +// +// err := f.SetSheetPrOptions("Sheet1", excelize.CodeName("Sheet1")) +// if err != nil { +// fmt.Println(err) +// } +// err = f.AddVBAProject("vbaProject.bin") +// if err != nil { +// fmt.Println(err) +// } +// err = f.SaveAs("macros.xlsm") +// if err != nil { +// fmt.Println(err) +// } +// +func (f *File) AddVBAProject(bin string) error { + var err error + // Check vbaProject.bin exists first. + if _, err = os.Stat(bin); os.IsNotExist(err) { + return err + } + if path.Ext(bin) != ".bin" { + return errors.New("unsupported VBA project extension") + } + f.setContentTypePartVBAProjectExtensions() + wb := f.workbookRelsReader() + var rID int + var ok bool + for _, rel := range wb.Relationships { + if rel.Target == "vbaProject.bin" && rel.Type == SourceRelationshipVBAProject { + ok = true + continue + } + t, _ := strconv.Atoi(strings.TrimPrefix(rel.ID, "rId")) + if t > rID { + rID = t + } + } + rID++ + if !ok { + wb.Relationships = append(wb.Relationships, xlsxWorkbookRelation{ + ID: "rId" + strconv.Itoa(rID), + Target: "vbaProject.bin", + Type: SourceRelationshipVBAProject, + }) + } + file, _ := ioutil.ReadFile(bin) + f.XLSX["xl/vbaProject.bin"] = file + return err +} + +// setContentTypePartVBAProjectExtensions provides a function to set the +// content type for relationship parts and the main document part. +func (f *File) setContentTypePartVBAProjectExtensions() { + var ok bool + content := f.contentTypesReader() + for _, v := range content.Defaults { + if v.Extension == "bin" { + ok = true + } + } + for idx, o := range content.Overrides { + if o.PartName == "/xl/workbook.xml" { + content.Overrides[idx].ContentType = "application/vnd.ms-excel.sheet.macroEnabled.main+xml" + } + } + if !ok { + content.Defaults = append(content.Defaults, xlsxDefault{ + Extension: "bin", + ContentType: "application/vnd.ms-office.vbaProject", + }) + } +} diff --git a/excelize_test.go b/excelize_test.go index c4a06a5..79010b1 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -1078,6 +1078,17 @@ func TestSetDefaultTimeStyle(t *testing.T) { assert.EqualError(t, f.setDefaultTimeStyle("SheetN", "", 0), "sheet SheetN is not exist") } +func TestAddVBAProject(t *testing.T) { + f := NewFile() + assert.NoError(t, f.SetSheetPrOptions("Sheet1", CodeName("Sheet1"))) + assert.EqualError(t, f.AddVBAProject("macros.bin"), "stat macros.bin: no such file or directory") + assert.EqualError(t, f.AddVBAProject(filepath.Join("test", "Book1.xlsx")), "unsupported VBA project extension") + assert.NoError(t, f.AddVBAProject(filepath.Join("test", "vbaProject.bin"))) + // Test add VBA project twice. + assert.NoError(t, f.AddVBAProject(filepath.Join("test", "vbaProject.bin"))) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddVBAProject.xlsm"))) +} + func prepareTestBook1() (*File, error) { f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) if err != nil { diff --git a/test/vbaProject.bin b/test/vbaProject.bin new file mode 100755 index 0000000..fc15dca Binary files /dev/null and b/test/vbaProject.bin differ diff --git a/xmlDrawing.go b/xmlDrawing.go index bb468bc..1201cc8 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -22,6 +22,7 @@ const ( SourceRelationshipDrawingVML = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing" SourceRelationshipHyperLink = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink" SourceRelationshipWorkSheet = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" + SourceRelationshipVBAProject = "http://schemas.microsoft.com/office/2006/relationships/vbaProject" SourceRelationshipChart201506 = "http://schemas.microsoft.com/office/drawing/2015/06/chart" SourceRelationshipChart20070802 = "http://schemas.microsoft.com/office/drawing/2007/8/2/chart" SourceRelationshipChart2014 = "http://schemas.microsoft.com/office/drawing/2014/chart" -- cgit v1.2.1 From c1357ee58fb7478645fa22adc7a5905e86c1f7c1 Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 22 Jul 2019 22:36:44 +0800 Subject: Fix invalid formula in data validation drop list --- datavalidation.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/datavalidation.go b/datavalidation.go index 56b96fd..209204a 100644 --- a/datavalidation.go +++ b/datavalidation.go @@ -141,12 +141,12 @@ func (dd *DataValidation) SetRange(f1, f2 int, t DataValidationType, o DataValid // // dvRange := excelize.NewDataValidation(true) // dvRange.Sqref = "A7:B8" -// dvRange.SetSqrefDropList("E1:E3", true) +// dvRange.SetSqrefDropList("$E$1:$E$3", true) // f.AddDataValidation("Sheet1", dvRange) // func (dd *DataValidation) SetSqrefDropList(sqref string, isCurrentSheet bool) error { if isCurrentSheet { - dd.Formula1 = sqref + dd.Formula1 = fmt.Sprintf("%s", sqref) dd.Type = convDataValidationType(typeList) return nil } -- cgit v1.2.1 From 53e653f28ef38e8b2175fdb88de72156eab14ee4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=91=A3=E5=87=AF?= <13122321216@163.com> Date: Thu, 25 Jul 2019 20:27:03 +0800 Subject: Fix #443 --- sheet.go | 1 + 1 file changed, 1 insertion(+) diff --git a/sheet.go b/sheet.go index 1c19e86..9c288b0 100644 --- a/sheet.go +++ b/sheet.go @@ -110,6 +110,7 @@ func (f *File) workSheetWriter() { f.saveFileList(p, replaceRelationshipsBytes(replaceWorkSheetsRelationshipsNameSpaceBytes(output))) ok := f.checked[p] if ok { + delete(f.Sheet, p) f.checked[p] = false } } -- cgit v1.2.1 From 0c9e5137e35b32e6046d25604edcb9a33f8353a2 Mon Sep 17 00:00:00 2001 From: Sustainedhhh <15829307082_pp@sina.cn> Date: Thu, 25 Jul 2019 20:31:21 +0800 Subject: Fix #442 --- sheet.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/sheet.go b/sheet.go index 1c19e86..32fc351 100644 --- a/sheet.go +++ b/sheet.go @@ -372,7 +372,12 @@ func (f *File) getSheetMap() map[string]string { for _, v := range content.Sheets.Sheet { for _, rel := range rels.Relationships { if rel.ID == v.ID { - maps[v.Name] = fmt.Sprintf("xl/%s", rel.Target) + // Construct a target XML as xl/worksheets/sheet%d by split path, compatible with different types of relative paths in workbook.xml.rels, for example: worksheets/sheet%d.xml and /xl/worksheets/sheet%d.xml + pathInfo := strings.Split(rel.Target, "/") + pathInfoLen := len(pathInfo) + if pathInfoLen > 0 { + maps[v.Name] = fmt.Sprintf("xl/worksheets/%s", pathInfo[pathInfoLen-1]) + } } } } -- cgit v1.2.1 From 9279c86d85ab0077f3696b8ec4cfb49ad8222530 Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 31 Jul 2019 23:53:49 +0800 Subject: Add extensions URI of spreadsheetML --- xmlDrawing.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/xmlDrawing.go b/xmlDrawing.go index 1201cc8..2f75eef 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -31,6 +31,8 @@ const ( NameSpaceDrawingMLChart = "http://schemas.openxmlformats.org/drawingml/2006/chart" NameSpaceDrawingMLSpreadSheet = "http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing" NameSpaceSpreadSheet = "http://schemas.openxmlformats.org/spreadsheetml/2006/main" + NameSpaceSpreadSheetX14 = "http://schemas.microsoft.com/office/spreadsheetml/2009/9/main" + NameSpaceSpreadSheetExcel2006Main = "http://schemas.microsoft.com/office/excel/2006/main" NameSpaceXML = "http://www.w3.org/XML/1998/namespace" NameSpaceXMLSchemaInstance = "http://www.w3.org/2001/XMLSchema-instance" StrictSourceRelationship = "http://purl.oclc.org/ooxml/officeDocument/relationships" @@ -41,6 +43,19 @@ const ( NameSpaceDublinCore = "http://purl.org/dc/elements/1.1/" NameSpaceDublinCoreTerms = "http://purl.org/dc/terms/" NameSpaceDublinCoreMetadataIntiative = "http://purl.org/dc/dcmitype/" + // The extLst child element ([ISO/IEC29500-1:2016] section 18.2.10) of the + // worksheet element ([ISO/IEC29500-1:2016] section 18.3.1.99) is extended by + // the addition of new child ext elements ([ISO/IEC29500-1:2016] section + // 18.2.7) + ExtURIConditionalFormattings = "{78C0D931-6437-407D-A8EE-F0AAD7539E65}" + ExtURIDataValidations = "{CCE6A557-97BC-4B89-ADB6-D9C93CAAB3DF}" + ExtURISparklineGroups = "{05C60535-1F16-4fd2-B633-F4F36F0B64E0}" + ExtURISlicerList = "{A8765BA9-456A-4DAB-B4F3-ACF838C121DE}" + ExtURIProtectedRanges = "{FC87AEE6-9EDD-4A0A-B7FB-166176984837}" + ExtURIIgnoredErrors = "{01252117-D84E-4E92-8308-4BE1C098FCBB}" + ExtURIWebExtensions = "{F7C9EE02-42E1-4005-9D12-6889AFFD525C}" + ExtURITimelineRefs = "{7E03D99C-DC04-49d9-9315-930204A7B6E9}" + ExtURIDrawingBlip = "{28A0092B-C50C-407E-A947-70E740481C1C}" ) var supportImageTypes = map[string]string{".gif": ".gif", ".jpg": ".jpeg", ".jpeg": ".jpeg", ".png": ".png", ".tif": ".tiff", ".tiff": ".tiff"} -- cgit v1.2.1 From 1092009541430c711676efb95b876598f59bb53c Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 3 Aug 2019 23:10:01 +0800 Subject: Fixed doc corruption when deleting all merged cells --- adjust.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/adjust.go b/adjust.go index f26f132..ccc5ce9 100644 --- a/adjust.go +++ b/adjust.go @@ -54,6 +54,11 @@ func (f *File) adjustHelper(sheet string, dir adjustDirection, num, offset int) } checkSheet(xlsx) checkRow(xlsx) + + if xlsx.MergeCells != nil && len(xlsx.MergeCells.Cells) == 0 { + xlsx.MergeCells = nil + } + return nil } @@ -283,8 +288,6 @@ func (f *File) deleteMergeCell(sheet *xlsxWorksheet, idx int) { if len(sheet.MergeCells.Cells) > idx { sheet.MergeCells.Cells = append(sheet.MergeCells.Cells[:idx], sheet.MergeCells.Cells[idx+1:]...) sheet.MergeCells.Count = len(sheet.MergeCells.Cells) - } else { - sheet.MergeCells = nil } } -- cgit v1.2.1 From cbe919fdf6c00733513494680b89171b8b1b41a1 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 4 Aug 2019 20:24:59 +0800 Subject: New feature: sparkline supported --- excelize.go | 12 +- sheet.go | 2 +- sparkline.go | 509 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ sparkline_test.go | 297 +++++++++++++++++++++++++++++++ styles.go | 2 +- xmlDrawing.go | 7 +- xmlWorksheet.go | 108 +++++++++++- 7 files changed, 932 insertions(+), 5 deletions(-) create mode 100644 sparkline.go create mode 100644 sparkline_test.go diff --git a/excelize.go b/excelize.go index c7eff10..6d014a0 100644 --- a/excelize.go +++ b/excelize.go @@ -181,11 +181,21 @@ func checkSheet(xlsx *xlsxWorksheet) { // Office Excel 2007. func replaceWorkSheetsRelationshipsNameSpaceBytes(workbookMarshal []byte) []byte { var oldXmlns = []byte(``) - var newXmlns = []byte(``) + var newXmlns = []byte(``) workbookMarshal = bytes.Replace(workbookMarshal, oldXmlns, newXmlns, -1) return workbookMarshal } +// replaceStyleRelationshipsNameSpaceBytes provides a function to replace +// xl/styles.xml XML tags to self-closing for compatible Microsoft Office +// Excel 2007. +func replaceStyleRelationshipsNameSpaceBytes(contentMarshal []byte) []byte { + var oldXmlns = []byte(``) + var newXmlns = []byte(``) + contentMarshal = bytes.Replace(contentMarshal, oldXmlns, newXmlns, -1) + return contentMarshal +} + // 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 diff --git a/sheet.go b/sheet.go index 347f255..e02782a 100644 --- a/sheet.go +++ b/sheet.go @@ -232,7 +232,7 @@ func replaceRelationshipsBytes(content []byte) []byte { // a horrible hack to fix that after the XML marshalling is completed. func replaceRelationshipsNameSpaceBytes(workbookMarshal []byte) []byte { oldXmlns := []byte(``) - newXmlns := []byte(``) + newXmlns := []byte(``) return bytes.Replace(workbookMarshal, oldXmlns, newXmlns, -1) } diff --git a/sparkline.go b/sparkline.go new file mode 100644 index 0000000..73e125e --- /dev/null +++ b/sparkline.go @@ -0,0 +1,509 @@ +// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// this source code is governed by a BSD-style license that can be found in +// the LICENSE file. +// +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. + +package excelize + +import ( + "encoding/xml" + "errors" + "strings" +) + +// addSparklineGroupByStyle provides a function to create x14:sparklineGroups +// element by given sparkline style ID. +func (f *File) addSparklineGroupByStyle(ID int) *xlsxX14SparklineGroup { + groups := []*xlsxX14SparklineGroup{ + { + ColorSeries: &xlsxTabColor{Theme: 4, Tint: -0.499984740745262}, + ColorNegative: &xlsxTabColor{Theme: 5}, + ColorMarkers: &xlsxTabColor{Theme: 4, Tint: -0.499984740745262}, + ColorFirst: &xlsxTabColor{Theme: 4, Tint: 0.39997558519241921}, + ColorLast: &xlsxTabColor{Theme: 4, Tint: 0.39997558519241921}, + ColorHigh: &xlsxTabColor{Theme: 4}, + ColorLow: &xlsxTabColor{Theme: 4}, + }, // 0 + { + ColorSeries: &xlsxTabColor{Theme: 4, Tint: -0.499984740745262}, + ColorNegative: &xlsxTabColor{Theme: 5}, + ColorMarkers: &xlsxTabColor{Theme: 4, Tint: -0.499984740745262}, + ColorFirst: &xlsxTabColor{Theme: 4, Tint: 0.39997558519241921}, + ColorLast: &xlsxTabColor{Theme: 4, Tint: 0.39997558519241921}, + ColorHigh: &xlsxTabColor{Theme: 4}, + ColorLow: &xlsxTabColor{Theme: 4}, + }, // 1 + { + ColorSeries: &xlsxTabColor{Theme: 5, Tint: -0.499984740745262}, + ColorNegative: &xlsxTabColor{Theme: 6}, + ColorMarkers: &xlsxTabColor{Theme: 5, Tint: -0.499984740745262}, + ColorFirst: &xlsxTabColor{Theme: 5, Tint: 0.39997558519241921}, + ColorLast: &xlsxTabColor{Theme: 5, Tint: 0.39997558519241921}, + ColorHigh: &xlsxTabColor{Theme: 5}, + ColorLow: &xlsxTabColor{Theme: 5}, + }, // 2 + { + ColorSeries: &xlsxTabColor{Theme: 6, Tint: -0.499984740745262}, + ColorNegative: &xlsxTabColor{Theme: 7}, + ColorMarkers: &xlsxTabColor{Theme: 6, Tint: -0.499984740745262}, + ColorFirst: &xlsxTabColor{Theme: 6, Tint: 0.39997558519241921}, + ColorLast: &xlsxTabColor{Theme: 6, Tint: 0.39997558519241921}, + ColorHigh: &xlsxTabColor{Theme: 6}, + ColorLow: &xlsxTabColor{Theme: 6}, + }, // 3 + { + ColorSeries: &xlsxTabColor{Theme: 7, Tint: -0.499984740745262}, + ColorNegative: &xlsxTabColor{Theme: 8}, + ColorMarkers: &xlsxTabColor{Theme: 7, Tint: -0.499984740745262}, + ColorFirst: &xlsxTabColor{Theme: 7, Tint: 0.39997558519241921}, + ColorLast: &xlsxTabColor{Theme: 7, Tint: 0.39997558519241921}, + ColorHigh: &xlsxTabColor{Theme: 7}, + ColorLow: &xlsxTabColor{Theme: 7}, + }, // 4 + { + ColorSeries: &xlsxTabColor{Theme: 8, Tint: -0.499984740745262}, + ColorNegative: &xlsxTabColor{Theme: 9}, + ColorMarkers: &xlsxTabColor{Theme: 8, Tint: -0.499984740745262}, + ColorFirst: &xlsxTabColor{Theme: 8, Tint: 0.39997558519241921}, + ColorLast: &xlsxTabColor{Theme: 8, Tint: 0.39997558519241921}, + ColorHigh: &xlsxTabColor{Theme: 8}, + ColorLow: &xlsxTabColor{Theme: 8}, + }, // 5 + { + ColorSeries: &xlsxTabColor{Theme: 9, Tint: -0.499984740745262}, + ColorNegative: &xlsxTabColor{Theme: 4}, + ColorMarkers: &xlsxTabColor{Theme: 9, Tint: -0.499984740745262}, + ColorFirst: &xlsxTabColor{Theme: 9, Tint: 0.39997558519241921}, + ColorLast: &xlsxTabColor{Theme: 9, Tint: 0.39997558519241921}, + ColorHigh: &xlsxTabColor{Theme: 9}, + ColorLow: &xlsxTabColor{Theme: 9}, + }, // 6 + { + ColorSeries: &xlsxTabColor{Theme: 4, Tint: -0.249977111117893}, + ColorNegative: &xlsxTabColor{Theme: 5}, + ColorMarkers: &xlsxTabColor{Theme: 5, Tint: -0.249977111117893}, + ColorFirst: &xlsxTabColor{Theme: 5, Tint: -0.249977111117893}, + ColorLast: &xlsxTabColor{Theme: 5, Tint: -0.249977111117893}, + ColorHigh: &xlsxTabColor{Theme: 5}, + ColorLow: &xlsxTabColor{Theme: 5}, + }, // 7 + { + ColorSeries: &xlsxTabColor{Theme: 5, Tint: -0.249977111117893}, + ColorNegative: &xlsxTabColor{Theme: 6}, + ColorMarkers: &xlsxTabColor{Theme: 6, Tint: -0.249977111117893}, + ColorFirst: &xlsxTabColor{Theme: 6, Tint: -0.249977111117893}, + ColorLast: &xlsxTabColor{Theme: 6, Tint: -0.249977111117893}, + ColorHigh: &xlsxTabColor{Theme: 6, Tint: -0.249977111117893}, + ColorLow: &xlsxTabColor{Theme: 6, Tint: -0.249977111117893}, + }, // 8 + { + ColorSeries: &xlsxTabColor{Theme: 6, Tint: -0.249977111117893}, + ColorNegative: &xlsxTabColor{Theme: 7}, + ColorMarkers: &xlsxTabColor{Theme: 7, Tint: -0.249977111117893}, + ColorFirst: &xlsxTabColor{Theme: 7, Tint: -0.249977111117893}, + ColorLast: &xlsxTabColor{Theme: 7, Tint: -0.249977111117893}, + ColorHigh: &xlsxTabColor{Theme: 7, Tint: -0.249977111117893}, + ColorLow: &xlsxTabColor{Theme: 7, Tint: -0.249977111117893}, + }, // 9 + { + ColorSeries: &xlsxTabColor{Theme: 7, Tint: -0.249977111117893}, + ColorNegative: &xlsxTabColor{Theme: 8}, + ColorMarkers: &xlsxTabColor{Theme: 8, Tint: -0.249977111117893}, + ColorFirst: &xlsxTabColor{Theme: 8, Tint: -0.249977111117893}, + ColorLast: &xlsxTabColor{Theme: 8, Tint: -0.249977111117893}, + ColorHigh: &xlsxTabColor{Theme: 8, Tint: -0.249977111117893}, + ColorLow: &xlsxTabColor{Theme: 8, Tint: -0.249977111117893}, + }, // 10 + { + ColorSeries: &xlsxTabColor{Theme: 8, Tint: -0.249977111117893}, + ColorNegative: &xlsxTabColor{Theme: 9}, + ColorMarkers: &xlsxTabColor{Theme: 9, Tint: -0.249977111117893}, + ColorFirst: &xlsxTabColor{Theme: 9, Tint: -0.249977111117893}, + ColorLast: &xlsxTabColor{Theme: 9, Tint: -0.249977111117893}, + ColorHigh: &xlsxTabColor{Theme: 9, Tint: -0.249977111117893}, + ColorLow: &xlsxTabColor{Theme: 9, Tint: -0.249977111117893}, + }, // 11 + { + ColorSeries: &xlsxTabColor{Theme: 9, Tint: -0.249977111117893}, + ColorNegative: &xlsxTabColor{Theme: 4}, + ColorMarkers: &xlsxTabColor{Theme: 4, Tint: -0.249977111117893}, + ColorFirst: &xlsxTabColor{Theme: 4, Tint: -0.249977111117893}, + ColorLast: &xlsxTabColor{Theme: 4, Tint: -0.249977111117893}, + ColorHigh: &xlsxTabColor{Theme: 4, Tint: -0.249977111117893}, + ColorLow: &xlsxTabColor{Theme: 4, Tint: -0.249977111117893}, + }, // 12 + { + ColorSeries: &xlsxTabColor{Theme: 4}, + ColorNegative: &xlsxTabColor{Theme: 5}, + ColorMarkers: &xlsxTabColor{Theme: 4, Tint: -0.249977111117893}, + ColorFirst: &xlsxTabColor{Theme: 4, Tint: -0.249977111117893}, + ColorLast: &xlsxTabColor{Theme: 4, Tint: -0.249977111117893}, + ColorHigh: &xlsxTabColor{Theme: 4, Tint: -0.249977111117893}, + ColorLow: &xlsxTabColor{Theme: 4, Tint: -0.249977111117893}, + }, // 13 + { + ColorSeries: &xlsxTabColor{Theme: 5}, + ColorNegative: &xlsxTabColor{Theme: 6}, + ColorMarkers: &xlsxTabColor{Theme: 5, Tint: -0.249977111117893}, + ColorFirst: &xlsxTabColor{Theme: 5, Tint: -0.249977111117893}, + ColorLast: &xlsxTabColor{Theme: 5, Tint: -0.249977111117893}, + ColorHigh: &xlsxTabColor{Theme: 5, Tint: -0.249977111117893}, + ColorLow: &xlsxTabColor{Theme: 5, Tint: -0.249977111117893}, + }, // 14 + { + ColorSeries: &xlsxTabColor{Theme: 6}, + ColorNegative: &xlsxTabColor{Theme: 7}, + ColorMarkers: &xlsxTabColor{Theme: 6, Tint: -0.249977111117893}, + ColorFirst: &xlsxTabColor{Theme: 6, Tint: -0.249977111117893}, + ColorLast: &xlsxTabColor{Theme: 6, Tint: -0.249977111117893}, + ColorHigh: &xlsxTabColor{Theme: 6, Tint: -0.249977111117893}, + ColorLow: &xlsxTabColor{Theme: 6, Tint: -0.249977111117893}, + }, // 15 + { + ColorSeries: &xlsxTabColor{Theme: 7}, + ColorNegative: &xlsxTabColor{Theme: 8}, + ColorMarkers: &xlsxTabColor{Theme: 7, Tint: -0.249977111117893}, + ColorFirst: &xlsxTabColor{Theme: 7, Tint: -0.249977111117893}, + ColorLast: &xlsxTabColor{Theme: 7, Tint: -0.249977111117893}, + ColorHigh: &xlsxTabColor{Theme: 7, Tint: -0.249977111117893}, + ColorLow: &xlsxTabColor{Theme: 7, Tint: -0.249977111117893}, + }, // 16 + { + ColorSeries: &xlsxTabColor{Theme: 8}, + ColorNegative: &xlsxTabColor{Theme: 9}, + ColorMarkers: &xlsxTabColor{Theme: 8, Tint: -0.249977111117893}, + ColorFirst: &xlsxTabColor{Theme: 8, Tint: -0.249977111117893}, + ColorLast: &xlsxTabColor{Theme: 8, Tint: -0.249977111117893}, + ColorHigh: &xlsxTabColor{Theme: 8, Tint: -0.249977111117893}, + ColorLow: &xlsxTabColor{Theme: 8, Tint: -0.249977111117893}, + }, // 17 + { + ColorSeries: &xlsxTabColor{Theme: 9}, + ColorNegative: &xlsxTabColor{Theme: 4}, + ColorMarkers: &xlsxTabColor{Theme: 9, Tint: -0.249977111117893}, + ColorFirst: &xlsxTabColor{Theme: 9, Tint: -0.249977111117893}, + ColorLast: &xlsxTabColor{Theme: 9, Tint: -0.249977111117893}, + ColorHigh: &xlsxTabColor{Theme: 9, Tint: -0.249977111117893}, + ColorLow: &xlsxTabColor{Theme: 9, Tint: -0.249977111117893}, + }, // 18 + { + ColorSeries: &xlsxTabColor{Theme: 4, Tint: 0.39997558519241921}, + ColorNegative: &xlsxTabColor{Theme: 0, Tint: -0.499984740745262}, + ColorMarkers: &xlsxTabColor{Theme: 4, Tint: 0.79998168889431442}, + ColorFirst: &xlsxTabColor{Theme: 4, Tint: -0.249977111117893}, + ColorLast: &xlsxTabColor{Theme: 4, Tint: -0.249977111117893}, + ColorHigh: &xlsxTabColor{Theme: 4, Tint: -0.499984740745262}, + ColorLow: &xlsxTabColor{Theme: 4, Tint: -0.499984740745262}, + }, // 19 + { + ColorSeries: &xlsxTabColor{Theme: 5, Tint: 0.39997558519241921}, + ColorNegative: &xlsxTabColor{Theme: 0, Tint: -0.499984740745262}, + ColorMarkers: &xlsxTabColor{Theme: 5, Tint: 0.79998168889431442}, + ColorFirst: &xlsxTabColor{Theme: 5, Tint: -0.249977111117893}, + ColorLast: &xlsxTabColor{Theme: 5, Tint: -0.249977111117893}, + ColorHigh: &xlsxTabColor{Theme: 5, Tint: -0.499984740745262}, + ColorLow: &xlsxTabColor{Theme: 5, Tint: -0.499984740745262}, + }, // 20 + { + ColorSeries: &xlsxTabColor{Theme: 6, Tint: 0.39997558519241921}, + ColorNegative: &xlsxTabColor{Theme: 0, Tint: -0.499984740745262}, + ColorMarkers: &xlsxTabColor{Theme: 6, Tint: 0.79998168889431442}, + ColorFirst: &xlsxTabColor{Theme: 6, Tint: -0.249977111117893}, + ColorLast: &xlsxTabColor{Theme: 6, Tint: -0.249977111117893}, + ColorHigh: &xlsxTabColor{Theme: 6, Tint: -0.499984740745262}, + ColorLow: &xlsxTabColor{Theme: 6, Tint: -0.499984740745262}, + }, // 21 + { + ColorSeries: &xlsxTabColor{Theme: 7, Tint: 0.39997558519241921}, + ColorNegative: &xlsxTabColor{Theme: 0, Tint: -0.499984740745262}, + ColorMarkers: &xlsxTabColor{Theme: 7, Tint: 0.79998168889431442}, + ColorFirst: &xlsxTabColor{Theme: 7, Tint: -0.249977111117893}, + ColorLast: &xlsxTabColor{Theme: 7, Tint: -0.249977111117893}, + ColorHigh: &xlsxTabColor{Theme: 7, Tint: -0.499984740745262}, + ColorLow: &xlsxTabColor{Theme: 7, Tint: -0.499984740745262}, + }, // 22 + { + ColorSeries: &xlsxTabColor{Theme: 8, Tint: 0.39997558519241921}, + ColorNegative: &xlsxTabColor{Theme: 0, Tint: -0.499984740745262}, + ColorMarkers: &xlsxTabColor{Theme: 8, Tint: 0.79998168889431442}, + ColorFirst: &xlsxTabColor{Theme: 8, Tint: -0.249977111117893}, + ColorLast: &xlsxTabColor{Theme: 8, Tint: -0.249977111117893}, + ColorHigh: &xlsxTabColor{Theme: 8, Tint: -0.499984740745262}, + ColorLow: &xlsxTabColor{Theme: 8, Tint: -0.499984740745262}, + }, // 23 + { + ColorSeries: &xlsxTabColor{Theme: 9, Tint: 0.39997558519241921}, + ColorNegative: &xlsxTabColor{Theme: 0, Tint: -0.499984740745262}, + ColorMarkers: &xlsxTabColor{Theme: 9, Tint: 0.79998168889431442}, + ColorFirst: &xlsxTabColor{Theme: 9, Tint: -0.249977111117893}, + ColorLast: &xlsxTabColor{Theme: 9, Tint: -0.249977111117893}, + ColorHigh: &xlsxTabColor{Theme: 9, Tint: -0.499984740745262}, + ColorLow: &xlsxTabColor{Theme: 9, Tint: -0.499984740745262}, + }, // 24 + { + ColorSeries: &xlsxTabColor{Theme: 1, Tint: 0.499984740745262}, + ColorNegative: &xlsxTabColor{Theme: 1, Tint: 0.249977111117893}, + ColorMarkers: &xlsxTabColor{Theme: 1, Tint: 0.249977111117893}, + ColorFirst: &xlsxTabColor{Theme: 1, Tint: 0.249977111117893}, + ColorLast: &xlsxTabColor{Theme: 1, Tint: 0.249977111117893}, + ColorHigh: &xlsxTabColor{Theme: 1, Tint: 0.249977111117893}, + ColorLow: &xlsxTabColor{Theme: 1, Tint: 0.249977111117893}, + }, // 25 + { + ColorSeries: &xlsxTabColor{Theme: 1, Tint: 0.34998626667073579}, + ColorNegative: &xlsxTabColor{Theme: 0, Tint: 0.249977111117893}, + ColorMarkers: &xlsxTabColor{Theme: 0, Tint: 0.249977111117893}, + ColorFirst: &xlsxTabColor{Theme: 0, Tint: 0.249977111117893}, + ColorLast: &xlsxTabColor{Theme: 0, Tint: 0.249977111117893}, + ColorHigh: &xlsxTabColor{Theme: 0, Tint: 0.249977111117893}, + ColorLow: &xlsxTabColor{Theme: 0, Tint: 0.249977111117893}, + }, // 26 + { + ColorSeries: &xlsxTabColor{RGB: "FF323232"}, + ColorNegative: &xlsxTabColor{RGB: "FFD00000"}, + ColorMarkers: &xlsxTabColor{RGB: "FFD00000"}, + ColorFirst: &xlsxTabColor{RGB: "FFD00000"}, + ColorLast: &xlsxTabColor{RGB: "FFD00000"}, + ColorHigh: &xlsxTabColor{RGB: "FFD00000"}, + ColorLow: &xlsxTabColor{RGB: "FFD00000"}, + }, // 27 + { + ColorSeries: &xlsxTabColor{RGB: "FF000000"}, + ColorNegative: &xlsxTabColor{RGB: "FF0070C0"}, + ColorMarkers: &xlsxTabColor{RGB: "FF0070C0"}, + ColorFirst: &xlsxTabColor{RGB: "FF0070C0"}, + ColorLast: &xlsxTabColor{RGB: "FF0070C0"}, + ColorHigh: &xlsxTabColor{RGB: "FF0070C0"}, + ColorLow: &xlsxTabColor{RGB: "FF0070C0"}, + }, // 28 + { + ColorSeries: &xlsxTabColor{RGB: "FF376092"}, + ColorNegative: &xlsxTabColor{RGB: "FFD00000"}, + ColorMarkers: &xlsxTabColor{RGB: "FFD00000"}, + ColorFirst: &xlsxTabColor{RGB: "FFD00000"}, + ColorLast: &xlsxTabColor{RGB: "FFD00000"}, + ColorHigh: &xlsxTabColor{RGB: "FFD00000"}, + ColorLow: &xlsxTabColor{RGB: "FFD00000"}, + }, // 29 + { + ColorSeries: &xlsxTabColor{RGB: "FF0070C0"}, + ColorNegative: &xlsxTabColor{RGB: "FF000000"}, + ColorMarkers: &xlsxTabColor{RGB: "FF000000"}, + ColorFirst: &xlsxTabColor{RGB: "FF000000"}, + ColorLast: &xlsxTabColor{RGB: "FF000000"}, + ColorHigh: &xlsxTabColor{RGB: "FF000000"}, + ColorLow: &xlsxTabColor{RGB: "FF000000"}, + }, // 30 + { + ColorSeries: &xlsxTabColor{RGB: "FF5F5F5F"}, + ColorNegative: &xlsxTabColor{RGB: "FFFFB620"}, + ColorMarkers: &xlsxTabColor{RGB: "FFD70077"}, + ColorFirst: &xlsxTabColor{RGB: "FF5687C2"}, + ColorLast: &xlsxTabColor{RGB: "FF359CEB"}, + ColorHigh: &xlsxTabColor{RGB: "FF56BE79"}, + ColorLow: &xlsxTabColor{RGB: "FFFF5055"}, + }, // 31 + { + ColorSeries: &xlsxTabColor{RGB: "FF5687C2"}, + ColorNegative: &xlsxTabColor{RGB: "FFFFB620"}, + ColorMarkers: &xlsxTabColor{RGB: "FFD70077"}, + ColorFirst: &xlsxTabColor{RGB: "FF777777"}, + ColorLast: &xlsxTabColor{RGB: "FF359CEB"}, + ColorHigh: &xlsxTabColor{RGB: "FF56BE79"}, + ColorLow: &xlsxTabColor{RGB: "FFFF5055"}, + }, // 32 + { + ColorSeries: &xlsxTabColor{RGB: "FFC6EFCE"}, + ColorNegative: &xlsxTabColor{RGB: "FFFFC7CE"}, + ColorMarkers: &xlsxTabColor{RGB: "FF8CADD6"}, + ColorFirst: &xlsxTabColor{RGB: "FFFFDC47"}, + ColorLast: &xlsxTabColor{RGB: "FFFFEB9C"}, + ColorHigh: &xlsxTabColor{RGB: "FF60D276"}, + ColorLow: &xlsxTabColor{RGB: "FFFF5367"}, + }, // 33 + { + ColorSeries: &xlsxTabColor{RGB: "FF00B050"}, + ColorNegative: &xlsxTabColor{RGB: "FFFF0000"}, + ColorMarkers: &xlsxTabColor{RGB: "FF0070C0"}, + ColorFirst: &xlsxTabColor{RGB: "FFFFC000"}, + ColorLast: &xlsxTabColor{RGB: "FFFFC000"}, + ColorHigh: &xlsxTabColor{RGB: "FF00B050"}, + ColorLow: &xlsxTabColor{RGB: "FFFF0000"}, + }, // 34 + { + ColorSeries: &xlsxTabColor{Theme: 3}, + ColorNegative: &xlsxTabColor{Theme: 9}, + ColorMarkers: &xlsxTabColor{Theme: 8}, + ColorFirst: &xlsxTabColor{Theme: 4}, + ColorLast: &xlsxTabColor{Theme: 5}, + ColorHigh: &xlsxTabColor{Theme: 6}, + ColorLow: &xlsxTabColor{Theme: 7}, + }, // 35 + { + ColorSeries: &xlsxTabColor{Theme: 1}, + ColorNegative: &xlsxTabColor{Theme: 9}, + ColorMarkers: &xlsxTabColor{Theme: 8}, + ColorFirst: &xlsxTabColor{Theme: 4}, + ColorLast: &xlsxTabColor{Theme: 5}, + ColorHigh: &xlsxTabColor{Theme: 6}, + ColorLow: &xlsxTabColor{Theme: 7}, + }, // 36 + } + return groups[ID] +} + +// AddSparkline provides a function to add sparklines to the worksheet by +// given formatting options. Sparklines are small charts that fit in a single +// cell and are used to show trends in data. Sparklines are a feature of Excel +// 2010 and later only. You can write them to an XLSX file that can be read by +// Excel 2007 but they won't be displayed. For example, add a grouped +// sparkline. Changes are applied to all three: +// +// err := f.AddSparkline("Sheet1", &excelize.SparklineOption{ +// Location: []string{"A1", "A2", "A3"}, +// Range: []string{"Sheet2!A1:J1", "Sheet2!A2:J2", "Sheet2!A3:J3"}, +// Markers: true, +// }) +// +// The following shows the formatting options of sparkline supported by excelize: +// +// Parameter | Description +// -----------+-------------------------------------------- +// Location | Required, must have the same number with 'Range' parameter +// Range |Required, must have the same number with 'Location' parameter +// Type | Enumeration value: line, column, win_loss +// Style | Value range: 0 - 35 +// Hight | Toggle sparkine high points +// Low | Toggle sparkine low points +// First | Toggle sparkine first points +// Last | Toggle sparkine last points +// Negative | Toggle sparkine negative points +// Markers | Toggle sparkine markers +// ColorAxis | An RGB Color is specified as RRGGBB +// Axis | Show sparkline axis +// +func (f *File) AddSparkline(sheet string, opt *SparklineOption) error { + // parameter validation + ws, err := f.parseFormatAddSparklineSet(sheet, opt) + if err != nil { + return err + } + // Handle the sparkline type + sparkType := "line" + sparkTypes := map[string]string{"line": "line", "column": "column", "win_loss": "stacked"} + if opt.Type != "" { + specifiedSparkTypes, ok := sparkTypes[opt.Type] + if !ok { + return errors.New("parameter 'Type' must be 'line', 'column' or 'win_loss'") + } + sparkType = specifiedSparkTypes + } + group := f.addSparklineGroupByStyle(opt.Style) + group.Type = sparkType + group.ColorAxis = &xlsxColor{RGB: "FF000000"} + group.DisplayEmptyCellsAs = "gap" + group.High = opt.High + group.Low = opt.Low + group.First = opt.First + group.Last = opt.Last + group.Negative = opt.Negative + group.DisplayXAxis = opt.Axis + group.Markers = opt.Markers + if opt.SeriesColor != "" { + group.ColorSeries = &xlsxTabColor{ + RGB: getPaletteColor(opt.SeriesColor), + } + } + if opt.Reverse { + group.RightToLeft = opt.Reverse + } + f.addSparkline(opt, group) + if ws.ExtLst.Ext != "" { // append mode ext + decodeExtLst := decodeWorksheetExt{} + err = xml.Unmarshal([]byte(""+ws.ExtLst.Ext+""), &decodeExtLst) + if err != nil { + return err + } + for idx, ext := range decodeExtLst.Ext { + // hack: add back missing namespace + decodeExtLst.Ext[idx].XMLNSX14 = decodeExtLst.Ext[idx].X14 + decodeExtLst.Ext[idx].XMLNSX15 = decodeExtLst.Ext[idx].X15 + decodeExtLst.Ext[idx].XMLNSX14 = "" + decodeExtLst.Ext[idx].XMLNSX15 = "" + if ext.URI == ExtURISparklineGroups { + decodeSparklineGroups := decodeX14SparklineGroups{} + _ = xml.Unmarshal([]byte(ext.Content), &decodeSparklineGroups) + sparklineGroupBytes, _ := xml.Marshal(group) + groups := xlsxX14SparklineGroups{ + XMLNSXM: NameSpaceSpreadSheetExcel2006Main, + Content: decodeSparklineGroups.Content + string(sparklineGroupBytes), + } + sparklineGroupsBytes, _ := xml.Marshal(groups) + decodeExtLst.Ext[idx].Content = string(sparklineGroupsBytes) + } + } + extLstBytes, _ := xml.Marshal(decodeExtLst) + extLst := string(extLstBytes) + ws.ExtLst = &xlsxExtLst{ + Ext: strings.TrimSuffix(strings.TrimPrefix(extLst, ""), ""), + } + } else { + groups := xlsxX14SparklineGroups{ + XMLNSXM: NameSpaceSpreadSheetExcel2006Main, + SparklineGroups: []*xlsxX14SparklineGroup{group}, + } + sparklineGroupsBytes, _ := xml.Marshal(groups) + extLst := xlsxWorksheetExt{ + XMLNSX14: NameSpaceSpreadSheetX14, + URI: ExtURISparklineGroups, + Content: string(sparklineGroupsBytes), + } + extBytes, _ := xml.Marshal(extLst) + ws.ExtLst.Ext = string(extBytes) + } + return nil +} + +// parseFormatAddSparklineSet provides a function to validate sparkline +// properties. +func (f *File) parseFormatAddSparklineSet(sheet string, opt *SparklineOption) (*xlsxWorksheet, error) { + ws, err := f.workSheetReader(sheet) + if err != nil { + return ws, err + } + if opt == nil { + return ws, errors.New("parameter is required") + } + if len(opt.Location) < 1 { + return ws, errors.New("parameter 'Location' is required") + } + if len(opt.Range) < 1 { + return ws, errors.New("parameter 'Range' is required") + } + // The ranges and locations must match.\ + if len(opt.Location) != len(opt.Range) { + return ws, errors.New(`must have the same number of 'Location' and 'Range' parameters`) + } + if opt.Style < 0 || opt.Style > 35 { + return ws, errors.New("parameter 'Style' must betweent 0-35") + } + if ws.ExtLst == nil { + ws.ExtLst = &xlsxExtLst{} + } + return ws, err +} + +// addSparkline provides a function to create a sparkline in a sparkline group +// by given properties. +func (f *File) addSparkline(opt *SparklineOption, group *xlsxX14SparklineGroup) { + for idx, location := range opt.Location { + group.Sparklines.Sparkline = append(group.Sparklines.Sparkline, &xlsxX14Sparkline{ + F: opt.Range[idx], + Sqref: location, + }) + } +} diff --git a/sparkline_test.go b/sparkline_test.go new file mode 100644 index 0000000..d52929b --- /dev/null +++ b/sparkline_test.go @@ -0,0 +1,297 @@ +package excelize + +import ( + "fmt" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestAddSparkline(t *testing.T) { + f := prepareSparklineDataset() + + // Set the columns widths to make the output clearer + style, err := f.NewStyle(`{"font":{"bold":true}}`) + assert.NoError(t, err) + assert.NoError(t, f.SetCellStyle("Sheet1", "A1", "B1", style)) + assert.NoError(t, f.SetSheetViewOptions("Sheet1", 0, ZoomScale(150))) + + f.SetColWidth("Sheet1", "A", "A", 14) + f.SetColWidth("Sheet1", "B", "B", 50) + // Headings + f.SetCellValue("Sheet1", "A1", "Sparkline") + f.SetCellValue("Sheet1", "B1", "Description") + + f.SetCellValue("Sheet1", "B2", `A default "line" sparkline.`) + assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ + Location: []string{"A2"}, + Range: []string{"Sheet3!A1:J1"}, + })) + + f.SetCellValue("Sheet1", "B3", `A default "column" sparkline.`) + assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ + Location: []string{"A3"}, + Range: []string{"Sheet3!A2:J2"}, + Type: "column", + })) + + f.SetCellValue("Sheet1", "B4", `A default "win/loss" sparkline.`) + assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ + Location: []string{"A4"}, + Range: []string{"Sheet3!A3:J3"}, + Type: "win_loss", + })) + + f.SetCellValue("Sheet1", "B6", "Line with markers.") + assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ + Location: []string{"A6"}, + Range: []string{"Sheet3!A1:J1"}, + Markers: true, + })) + + f.SetCellValue("Sheet1", "B7", "Line with high and low points.") + assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ + Location: []string{"A7"}, + Range: []string{"Sheet3!A1:J1"}, + High: true, + Low: true, + })) + + f.SetCellValue("Sheet1", "B8", "Line with first and last point markers.") + assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ + Location: []string{"A8"}, + Range: []string{"Sheet3!A1:J1"}, + First: true, + Last: true, + })) + + f.SetCellValue("Sheet1", "B9", "Line with negative point markers.") + assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ + Location: []string{"A9"}, + Range: []string{"Sheet3!A1:J1"}, + Negative: true, + })) + + f.SetCellValue("Sheet1", "B10", "Line with axis.") + assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ + Location: []string{"A10"}, + Range: []string{"Sheet3!A1:J1"}, + Axis: true, + })) + + f.SetCellValue("Sheet1", "B12", "Column with default style (1).") + assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ + Location: []string{"A12"}, + Range: []string{"Sheet3!A2:J2"}, + Type: "column", + })) + + f.SetCellValue("Sheet1", "B13", "Column with style 2.") + assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ + Location: []string{"A13"}, + Range: []string{"Sheet3!A2:J2"}, + Type: "column", + Style: 2, + })) + + f.SetCellValue("Sheet1", "B14", "Column with style 3.") + assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ + Location: []string{"A14"}, + Range: []string{"Sheet3!A2:J2"}, + Type: "column", + Style: 3, + })) + + f.SetCellValue("Sheet1", "B15", "Column with style 4.") + assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ + Location: []string{"A15"}, + Range: []string{"Sheet3!A2:J2"}, + Type: "column", + Style: 4, + })) + + f.SetCellValue("Sheet1", "B16", "Column with style 5.") + assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ + Location: []string{"A16"}, + Range: []string{"Sheet3!A2:J2"}, + Type: "column", + Style: 5, + })) + + f.SetCellValue("Sheet1", "B17", "Column with style 6.") + assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ + Location: []string{"A17"}, + Range: []string{"Sheet3!A2:J2"}, + Type: "column", + Style: 6, + })) + + f.SetCellValue("Sheet1", "B18", "Column with a user defined color.") + assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ + Location: []string{"A18"}, + Range: []string{"Sheet3!A2:J2"}, + Type: "column", + SeriesColor: "#E965E0", + })) + + f.SetCellValue("Sheet1", "B20", "A win/loss sparkline.") + assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ + Location: []string{"A20"}, + Range: []string{"Sheet3!A3:J3"}, + Type: "win_loss", + })) + + f.SetCellValue("Sheet1", "B21", "A win/loss sparkline with negative points highlighted.") + assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ + Location: []string{"A21"}, + Range: []string{"Sheet3!A3:J3"}, + Type: "win_loss", + Negative: true, + })) + + f.SetCellValue("Sheet1", "B23", "A left to right column (the default).") + assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ + Location: []string{"A23"}, + Range: []string{"Sheet3!A4:J4"}, + Type: "column", + Style: 20, + })) + + f.SetCellValue("Sheet1", "B24", "A right to left column.") + assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ + Location: []string{"A24"}, + Range: []string{"Sheet3!A4:J4"}, + Type: "column", + Style: 20, + Reverse: true, + })) + + f.SetCellValue("Sheet1", "B25", "Sparkline and text in one cell.") + assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ + Location: []string{"A25"}, + Range: []string{"Sheet3!A4:J4"}, + Type: "column", + Style: 20, + })) + f.SetCellValue("Sheet1", "A25", "Growth") + + f.SetCellValue("Sheet1", "B27", "A grouped sparkline. Changes are applied to all three.") + assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ + Location: []string{"A27", "A28", "A29"}, + Range: []string{"Sheet3!A5:J5", "Sheet3!A6:J6", "Sheet3!A7:J7"}, + Markers: true, + })) + + // Sheet2 sections + assert.NoError(t, f.AddSparkline("Sheet2", &SparklineOption{ + Location: []string{"F3"}, + Range: []string{"Sheet2!A3:E3"}, + Type: "win_loss", + Negative: true, + })) + + assert.NoError(t, f.AddSparkline("Sheet2", &SparklineOption{ + Location: []string{"F1"}, + Range: []string{"Sheet2!A1:E1"}, + Markers: true, + })) + + assert.NoError(t, f.AddSparkline("Sheet2", &SparklineOption{ + Location: []string{"F2"}, + Range: []string{"Sheet2!A2:E2"}, + Type: "column", + Style: 12, + })) + + assert.NoError(t, f.AddSparkline("Sheet2", &SparklineOption{ + Location: []string{"F3"}, + Range: []string{"Sheet2!A3:E3"}, + Type: "win_loss", + Negative: true, + })) + + // Save xlsx file by the given path. + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddSparkline.xlsx"))) + + // Test error exceptions + assert.EqualError(t, f.AddSparkline("SheetN", &SparklineOption{ + Location: []string{"F3"}, + Range: []string{"Sheet2!A3:E3"}, + }), "sheet SheetN is not exist") + + assert.EqualError(t, f.AddSparkline("Sheet1", nil), "parameter is required") + + assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOption{ + Range: []string{"Sheet2!A3:E3"}, + }), `parameter 'Location' is required`) + + assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOption{ + Location: []string{"F3"}, + }), `parameter 'Range' is required`) + + assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOption{ + Location: []string{"F2", "F3"}, + Range: []string{"Sheet2!A3:E3"}, + }), `must have the same number of 'Location' and 'Range' parameters`) + + assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOption{ + Location: []string{"F3"}, + Range: []string{"Sheet2!A3:E3"}, + Type: "unknown_type", + }), `parameter 'Type' must be 'line', 'column' or 'win_loss'`) + + assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOption{ + Location: []string{"F3"}, + Range: []string{"Sheet2!A3:E3"}, + Style: -1, + }), `parameter 'Style' must betweent 0-35`) + + assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOption{ + Location: []string{"F3"}, + Range: []string{"Sheet2!A3:E3"}, + Style: -1, + }), `parameter 'Style' must betweent 0-35`) + + f.Sheet["xl/worksheets/sheet1.xml"].ExtLst.Ext = ` + + + + + + + + ` + assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOption{ + Location: []string{"A2"}, + Range: []string{"Sheet3!A1:J1"}, + }), "XML syntax error on line 6: element closed by ") +} + +func prepareSparklineDataset() *File { + f := NewFile() + sheet2 := [][]int{ + {-2, 2, 3, -1, 0}, + {30, 20, 33, 20, 15}, + {1, -1, -1, 1, -1}, + } + sheet3 := [][]int{ + {-2, 2, 3, -1, 0, -2, 3, 2, 1, 0}, + {30, 20, 33, 20, 15, 5, 5, 15, 10, 15}, + {1, 1, -1, -1, 1, -1, 1, 1, 1, -1}, + {5, 6, 7, 10, 15, 20, 30, 50, 70, 100}, + {-2, 2, 3, -1, 0, -2, 3, 2, 1, 0}, + {3, -1, 0, -2, 3, 2, 1, 0, 2, 1}, + {0, -2, 3, 2, 1, 0, 1, 2, 3, 1}, + } + f.NewSheet("Sheet2") + f.NewSheet("Sheet3") + for row, data := range sheet2 { + f.SetSheetRow("Sheet2", fmt.Sprintf("A%d", row+1), &data) + } + for row, data := range sheet3 { + f.SetSheetRow("Sheet3", fmt.Sprintf("A%d", row+1), &data) + } + return f +} diff --git a/styles.go b/styles.go index b246e30..04a5c33 100644 --- a/styles.go +++ b/styles.go @@ -1010,7 +1010,7 @@ func (f *File) stylesReader() *xlsxStyleSheet { func (f *File) styleSheetWriter() { if f.Styles != nil { output, _ := xml.Marshal(f.Styles) - f.saveFileList("xl/styles.xml", replaceWorkSheetsRelationshipsNameSpaceBytes(output)) + f.saveFileList("xl/styles.xml", replaceStyleRelationshipsNameSpaceBytes(output)) } } diff --git a/xmlDrawing.go b/xmlDrawing.go index 2f75eef..20cb83d 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -32,7 +32,9 @@ const ( NameSpaceDrawingMLSpreadSheet = "http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing" NameSpaceSpreadSheet = "http://schemas.openxmlformats.org/spreadsheetml/2006/main" NameSpaceSpreadSheetX14 = "http://schemas.microsoft.com/office/spreadsheetml/2009/9/main" + NameSpaceSpreadSheetX15 = "http://schemas.microsoft.com/office/spreadsheetml/2010/11/main" NameSpaceSpreadSheetExcel2006Main = "http://schemas.microsoft.com/office/excel/2006/main" + NameSpaceMacExcel2008Main = "http://schemas.microsoft.com/office/mac/excel/2008/main" NameSpaceXML = "http://www.w3.org/XML/1998/namespace" NameSpaceXMLSchemaInstance = "http://www.w3.org/2001/XMLSchema-instance" StrictSourceRelationship = "http://purl.oclc.org/ooxml/officeDocument/relationships" @@ -50,12 +52,15 @@ const ( ExtURIConditionalFormattings = "{78C0D931-6437-407D-A8EE-F0AAD7539E65}" ExtURIDataValidations = "{CCE6A557-97BC-4B89-ADB6-D9C93CAAB3DF}" ExtURISparklineGroups = "{05C60535-1F16-4fd2-B633-F4F36F0B64E0}" - ExtURISlicerList = "{A8765BA9-456A-4DAB-B4F3-ACF838C121DE}" + ExtURISlicerListX14 = "{A8765BA9-456A-4DAB-B4F3-ACF838C121DE}" + ExtURISlicerCachesListX14 = "{BBE1A952-AA13-448e-AADC-164F8A28A991}" + ExtURISlicerListX15 = "{3A4CF648-6AED-40f4-86FF-DC5316D8AED3}" ExtURIProtectedRanges = "{FC87AEE6-9EDD-4A0A-B7FB-166176984837}" ExtURIIgnoredErrors = "{01252117-D84E-4E92-8308-4BE1C098FCBB}" ExtURIWebExtensions = "{F7C9EE02-42E1-4005-9D12-6889AFFD525C}" ExtURITimelineRefs = "{7E03D99C-DC04-49d9-9315-930204A7B6E9}" ExtURIDrawingBlip = "{28A0092B-C50C-407E-A947-70E740481C1C}" + ExtURIMacExcelMX = "{64002731-A6B0-56B0-2670-7721B7C09600}" ) var supportImageTypes = map[string]string{".gif": ".gif", ".jpg": ".jpeg", ".jpeg": ".jpeg", ".png": ".png", ".tif": ".tiff", ".tiff": ".tiff"} diff --git a/xmlWorksheet.go b/xmlWorksheet.go index b94c521..7168df6 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -238,6 +238,7 @@ type xlsxPageSetUpPr struct { // xlsxTabColor directly maps the tabColor element in the namespace currently I // have not checked it for completeness - it does as much as I need. type xlsxTabColor struct { + RGB string `xml:"rgb,attr,omitempty"` Theme int `xml:"theme,attr,omitempty"` Tint float64 `xml:"tint,attr,omitempty"` } @@ -336,7 +337,7 @@ type xlsxCustomSheetView struct { PageSetup *xlsxPageSetUp `xml:"pageSetup"` HeaderFooter *xlsxHeaderFooter `xml:"headerFooter"` AutoFilter *xlsxAutoFilter `xml:"autoFilter"` - ExtLst *xlsxExt `xml:"extLst"` + ExtLst *xlsxExtLst `xml:"extLst"` GUID string `xml:"guid,attr"` Scale int `xml:"scale,attr,omitempty"` ColorID int `xml:"colorId,attr,omitempty"` @@ -632,6 +633,111 @@ type xlsxLegacyDrawing struct { RID string `xml:"http://schemas.openxmlformats.org/officeDocument/2006/relationships id,attr,omitempty"` } +// xlsxWorksheetExt directly maps the ext element in the worksheet. +type xlsxWorksheetExt struct { + XMLName xml.Name `xml:"ext"` + XMLNSX14 string `xml:"xmlns:x14,attr,omitempty"` + XMLNSX15 string `xml:"xmlns:x15,attr,omitempty"` + X14 string `xml:"x14,attr,omitempty"` + X15 string `xml:"x15,attr,omitempty"` + URI string `xml:"uri,attr"` + Content string `xml:",innerxml"` +} + +// decodeWorksheetExt directly maps the ext element. +type decodeWorksheetExt struct { + XMLName xml.Name `xml:"extLst"` + Ext []*xlsxWorksheetExt `xml:"ext"` +} + +// decodeX14SparklineGroups directly maps the sparklineGroups element. +type decodeX14SparklineGroups struct { + XMLName xml.Name `xml:"sparklineGroups"` + XMLNSXM string `xml:"xmlns:xm,attr"` + Content string `xml:",innerxml"` +} + +// xlsxX14SparklineGroups directly maps the sparklineGroups element. +type xlsxX14SparklineGroups struct { + XMLName xml.Name `xml:"x14:sparklineGroups"` + XMLNSXM string `xml:"xmlns:xm,attr"` + SparklineGroups []*xlsxX14SparklineGroup `xml:"x14:sparklineGroup"` + Content string `xml:",innerxml"` +} + +// xlsxX14SparklineGroup directly maps the sparklineGroup element. +type xlsxX14SparklineGroup struct { + XMLName xml.Name `xml:"x14:sparklineGroup"` + ManualMax int `xml:"manualMax,attr,omitempty"` + ManualMin int `xml:"manualMin,attr,omitempty"` + LineWeight float64 `xml:"lineWeight,attr,omitempty"` + Type string `xml:"type,attr,omitempty"` + DateAxis bool `xml:"dateAxis,attr,omitempty"` + DisplayEmptyCellsAs string `xml:"displayEmptyCellsAs,attr,omitempty"` + Markers bool `xml:"markers,attr,omitempty"` + High bool `xml:"high,attr,omitempty"` + Low bool `xml:"low,attr,omitempty"` + First bool `xml:"first,attr,omitempty"` + Last bool `xml:"last,attr,omitempty"` + Negative bool `xml:"negative,attr,omitempty"` + DisplayXAxis bool `xml:"displayXAxis,attr,omitempty"` + DisplayHidden bool `xml:"displayHidden,attr,omitempty"` + MinAxisType string `xml:"minAxisType,attr,omitempty"` + MaxAxisType string `xml:"maxAxisType,attr,omitempty"` + RightToLeft bool `xml:"rightToLeft,attr,omitempty"` + ColorSeries *xlsxTabColor `xml:"x14:colorSeries"` + ColorNegative *xlsxTabColor `xml:"x14:colorNegative"` + ColorAxis *xlsxColor `xml:"x14:colorAxis"` + ColorMarkers *xlsxTabColor `xml:"x14:colorMarkers"` + ColorFirst *xlsxTabColor `xml:"x14:colorFirst"` + ColorLast *xlsxTabColor `xml:"x14:colorLast"` + ColorHigh *xlsxTabColor `xml:"x14:colorHigh"` + ColorLow *xlsxTabColor `xml:"x14:colorLow"` + Sparklines xlsxX14Sparklines `xml:"x14:sparklines"` +} + +// xlsxX14Sparklines directly maps the sparklines element. +type xlsxX14Sparklines struct { + Sparkline []*xlsxX14Sparkline `xml:"x14:sparkline"` +} + +// xlsxX14Sparkline directly maps the sparkline element. +type xlsxX14Sparkline struct { + F string `xml:"xm:f"` + Sqref string `xml:"xm:sqref"` +} + +// SparklineOption directly maps the settings of the sparkline. +type SparklineOption struct { + Location []string + Range []string + Max int + CustMax int + Min int + CustMin int + Type string + Weight float64 + DateAxis bool + Markers bool + High bool + Low bool + First bool + Last bool + Negative bool + Axis bool + Hidden bool + Reverse bool + Style int + SeriesColor string + NegativeColor string + MarkersColor string + FirstColor string + LastColor string + HightColor string + LowColor string + EmptyCells string +} + // formatPanes directly maps the settings of the panes. type formatPanes struct { Freeze bool `json:"freeze"` -- cgit v1.2.1 From ac91ca0ded4111ed9f22578d4a0570a9084c97b0 Mon Sep 17 00:00:00 2001 From: Harris Date: Sun, 4 Aug 2019 17:23:42 -0500 Subject: Only parse xml once when reading We were parsing the whole sheet twice since the sheet reader already reads in all the rows. getTotalRowsCols function is unused after these changes so it has been deleted as well. Closes #439 --- rows.go | 142 +++++++++-------------------------------------------------- rows_test.go | 18 ++++++-- 2 files changed, 36 insertions(+), 124 deletions(-) diff --git a/rows.go b/rows.go index 3079d5a..cb0e31f 100644 --- a/rows.go +++ b/rows.go @@ -10,10 +10,8 @@ package excelize import ( - "bytes" "encoding/xml" "fmt" - "io" "math" "strconv" ) @@ -30,95 +28,35 @@ import ( // } // func (f *File) GetRows(sheet string) ([][]string, error) { - name, ok := f.sheetMap[trimSheetName(sheet)] - if !ok { - return nil, nil - } - - xlsx, err := f.workSheetReader(sheet) + rows, err := f.Rows(sheet) if err != nil { return nil, err } - 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 - rowData xlsxRow - ) - - rowCount, colCount, err := f.getTotalRowsCols(name) - if err != nil { - return nil, nil - } - rows := make([][]string, rowCount) - for i := range rows { - rows[i] = make([]string, colCount) - } - - var row int - decoder := xml.NewDecoder(bytes.NewReader(f.readXML(name))) - for { - token, _ := decoder.Token() - if token == nil { + results := make([][]string, 0, 64) + for rows.Next() { + if rows.Error() != nil { break } - switch startElement := token.(type) { - case xml.StartElement: - inElement = startElement.Name.Local - if inElement == "row" { - rowData = xlsxRow{} - _ = decoder.DecodeElement(&rowData, &startElement) - cr := rowData.R - 1 - for _, colCell := range rowData.C { - col, _, err := CellNameToCoordinates(colCell.R) - if err != nil { - return nil, err - } - val, _ := colCell.getValueFrom(f, d) - rows[cr][col-1] = val - if val != "" { - row = rowData.R - } - } - } - default: + row, err := rows.Columns() + if err != nil { + break } + results = append(results, row) } - return rows[:row], nil + return results, nil } // Rows defines an iterator to a sheet type Rows struct { - decoder *xml.Decoder - token xml.Token - err error - f *File + err error + f *File + rows []xlsxRow + curRow int } // Next will return true if find the next row element. func (rows *Rows) Next() bool { - for { - rows.token, rows.err = rows.decoder.Token() - if rows.err == io.EOF { - rows.err = nil - } - if rows.token == nil { - return false - } - - switch startElement := rows.token.(type) { - case xml.StartElement: - inElement := startElement.Name.Local - if inElement == "row" { - return true - } - } - } + return rows.curRow < len(rows.rows) } // Error will return the error when the find next row element @@ -128,15 +66,12 @@ func (rows *Rows) Error() error { // Columns return the current row's column values func (rows *Rows) Columns() ([]string, error) { - if rows.token == nil { - return []string{}, nil - } - startElement := rows.token.(xml.StartElement) - r := xlsxRow{} - _ = rows.decoder.DecodeElement(&r, &startElement) + curRow := rows.rows[rows.curRow] + rows.curRow++ + + columns := make([]string, len(curRow.C)) d := rows.f.sharedStringsReader() - columns := make([]string, len(r.C)) - for _, colCell := range r.C { + for _, colCell := range curRow.C { col, _, err := CellNameToCoordinates(colCell.R) if err != nil { return columns, err @@ -181,46 +116,11 @@ func (f *File) Rows(sheet string) (*Rows, error) { f.saveFileList(name, replaceWorkSheetsRelationshipsNameSpaceBytes(output)) } return &Rows{ - f: f, - decoder: xml.NewDecoder(bytes.NewReader(f.readXML(name))), + f: f, + rows: xlsx.SheetData.Row, }, nil } -// getTotalRowsCols provides a function to get total columns and rows in a -// worksheet. -func (f *File) getTotalRowsCols(name string) (int, int, error) { - decoder := xml.NewDecoder(bytes.NewReader(f.readXML(name))) - var inElement string - var r xlsxRow - var tr, tc int - for { - token, _ := decoder.Token() - if token == nil { - break - } - switch startElement := token.(type) { - case xml.StartElement: - inElement = startElement.Name.Local - if inElement == "row" { - r = xlsxRow{} - _ = decoder.DecodeElement(&r, &startElement) - tr = r.R - for _, colCell := range r.C { - col, _, err := CellNameToCoordinates(colCell.R) - if err != nil { - return tr, tc, err - } - if col > tc { - tc = col - } - } - } - default: - } - } - return tr, tc, nil -} - // SetRowHeight provides a function to set the height of a single row. For // example, set the height of the first row in Sheet1: // diff --git a/rows_test.go b/rows_test.go index f7d49b4..d52c635 100644 --- a/rows_test.go +++ b/rows_test.go @@ -39,9 +39,6 @@ func TestRows(t *testing.T) { if !assert.Equal(t, collectedRows, returnedRows) { t.FailNow() } - - r := Rows{} - r.Columns() } func TestRowsError(t *testing.T) { @@ -672,6 +669,21 @@ func TestDuplicateRowInvalidRownum(t *testing.T) { } } +func BenchmarkRows(b *testing.B) { + for i := 0; i < b.N; i++ { + f, _ := OpenFile(filepath.Join("test", "Book1.xlsx")) + rows, _ := f.Rows("Sheet2") + for rows.Next() { + row, _ := rows.Columns() + for i := range row { + if i >= 0 { + continue + } + } + } + } +} + func trimSliceSpace(s []string) []string { for { if len(s) > 0 && s[len(s)-1] == "" { -- cgit v1.2.1 From 58a79b41720974009aacf57209a7d5afdd75a7fd Mon Sep 17 00:00:00 2001 From: zhaov <779137069@qq.com> Date: Tue, 6 Aug 2019 09:50:45 +0800 Subject: Update comments --- sheetview.go | 67 +++++++++++++++++++++++++++++++++++------------------------- 1 file changed, 39 insertions(+), 28 deletions(-) diff --git a/sheetview.go b/sheetview.go index 91260fe..9712d85 100644 --- a/sheetview.go +++ b/sheetview.go @@ -11,48 +11,51 @@ package excelize import "fmt" -// SheetViewOption is an option of a view of a worksheet. See SetSheetViewOptions(). +// SheetViewOption is an option of a view of a worksheet. See +// SetSheetViewOptions(). type SheetViewOption interface { setSheetViewOption(view *xlsxSheetView) } -// SheetViewOptionPtr is a writable SheetViewOption. See GetSheetViewOptions(). +// SheetViewOptionPtr is a writable SheetViewOption. See +// GetSheetViewOptions(). type SheetViewOptionPtr interface { SheetViewOption getSheetViewOption(view *xlsxSheetView) } type ( - // DefaultGridColor specified a flag indicating that the consuming - // application should use the default grid lines color (system dependent). - // Overrides any color specified in colorId. + // DefaultGridColor is a SheetViewOption. It specifies a flag indicating that + // the consuming application should use the default grid lines color (system + // dependent). Overrides any color specified in colorId. DefaultGridColor bool - // RightToLeft specified a flag indicating whether the sheet is in 'right to - // left' display mode. When in this mode, Column A is on the far right, - // Column B ;is one column left of Column A, and so on. Also, information in - // cells is displayed in the Right to Left format. + // RightToLeft is a SheetViewOption. It specifies a flag indicating whether + // the sheet is in 'right to left' display mode. When in this mode, Column A + // is on the far right, Column B ;is one column left of Column A, and so on. + // Also, information in cells is displayed in the Right to Left format. RightToLeft bool - // ShowFormulas specified a flag indicating whether this sheet should display - // formulas. + // ShowFormulas is a SheetViewOption. It specifies a flag indicating whether + // this sheet should display formulas. ShowFormulas bool - // ShowGridLines specified a flag indicating whether this sheet should - // display gridlines. + // ShowGridLines is a SheetViewOption. It specifies a flag indicating whether + // this sheet should display gridlines. ShowGridLines bool - // ShowRowColHeaders specified a flag indicating whether the sheet should - // display row and column headings. + // ShowRowColHeaders is a SheetViewOption. It specifies a flag indicating + // whether the sheet should display row and column headings. ShowRowColHeaders bool - // ZoomScale specified a window zoom magnification for current view - // representing percent values. This attribute is restricted to values - // ranging from 10 to 400. Horizontal & Vertical scale together. + // ZoomScale is a SheetViewOption. It specifies a window zoom magnification + // for current view representing percent values. This attribute is restricted + // to values ranging from 10 to 400. Horizontal & Vertical scale together. ZoomScale float64 - // TopLeftCell specified a location of the top left visible cell Location of - // the top left visible cell in the bottom right pane (when in Left-to-Right - // mode). + // TopLeftCell is a SheetViewOption. It specifies a location of the top left + // visible cell Location of the top left visible cell in the bottom right + // pane (when in Left-to-Right mode). TopLeftCell string /* TODO - // ShowWhiteSpace specified flag indicating whether page layout view shall - // display margins. False means do not display left, right, top (header), and - // bottom (footer) margins (even when there is data in the header or footer). + // ShowWhiteSpace is a SheetViewOption. It specifies a flag indicating + // whether page layout view shall display margins. False means do not display + // left, right, top (header), and bottom (footer) margins (even when there is + // data in the header or footer). ShowWhiteSpace bool // ShowZeros is a SheetViewOption. ShowZeros bool @@ -140,10 +143,11 @@ func (f *File) getSheetView(sheetName string, viewIndex int) (*xlsxSheetView, er return &(xlsx.SheetViews.SheetView[viewIndex]), err } -// SetSheetViewOptions sets sheet view options. -// The viewIndex may be negative and if so is counted backward (-1 is the last view). +// SetSheetViewOptions sets sheet view options. The viewIndex may be negative +// and if so is counted backward (-1 is the last view). // // Available options: +// // DefaultGridColor(bool) // RightToLeft(bool) // ShowFormulas(bool) @@ -151,8 +155,11 @@ func (f *File) getSheetView(sheetName string, viewIndex int) (*xlsxSheetView, er // ShowRowColHeaders(bool) // ZoomScale(float64) // TopLeftCell(string) +// // Example: +// // err = f.SetSheetViewOptions("Sheet1", -1, ShowGridLines(false)) +// func (f *File) SetSheetViewOptions(name string, viewIndex int, opts ...SheetViewOption) error { view, err := f.getSheetView(name, viewIndex) if err != nil { @@ -165,10 +172,11 @@ func (f *File) SetSheetViewOptions(name string, viewIndex int, opts ...SheetView return nil } -// GetSheetViewOptions gets the value of sheet view options. -// The viewIndex may be negative and if so is counted backward (-1 is the last view). +// GetSheetViewOptions gets the value of sheet view options. The viewIndex may +// be negative and if so is counted backward (-1 is the last view). // // Available options: +// // DefaultGridColor(bool) // RightToLeft(bool) // ShowFormulas(bool) @@ -176,9 +184,12 @@ func (f *File) SetSheetViewOptions(name string, viewIndex int, opts ...SheetView // ShowRowColHeaders(bool) // ZoomScale(float64) // TopLeftCell(string) +// // Example: +// // var showGridLines excelize.ShowGridLines // err = f.GetSheetViewOptions("Sheet1", -1, &showGridLines) +// func (f *File) GetSheetViewOptions(name string, viewIndex int, opts ...SheetViewOptionPtr) error { view, err := f.getSheetView(name, viewIndex) if err != nil { -- cgit v1.2.1 From 497ad8f4be669525a85e49b44dacacc16cf450fd Mon Sep 17 00:00:00 2001 From: WuXu1995 <49405879+WuXu1995@users.noreply.github.com> Date: Tue, 6 Aug 2019 10:00:10 +0800 Subject: Bugfix #454 --- xmlWorksheet.go | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 7168df6..9727866 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -454,22 +454,22 @@ type xlsxSheetProtection struct { HashValue string `xml:"hashValue,attr,omitempty"` SaltValue string `xml:"saltValue,attr,omitempty"` SpinCount int `xml:"spinCount,attr,omitempty"` - Sheet bool `xml:"sheet,attr,omitempty"` - Objects bool `xml:"objects,attr,omitempty"` - Scenarios bool `xml:"scenarios,attr,omitempty"` - FormatCells bool `xml:"formatCells,attr,omitempty"` - FormatColumns bool `xml:"formatColumns,attr,omitempty"` - FormatRows bool `xml:"formatRows,attr,omitempty"` - InsertColumns bool `xml:"insertColumns,attr,omitempty"` - InsertRows bool `xml:"insertRows,attr,omitempty"` - InsertHyperlinks bool `xml:"insertHyperlinks,attr,omitempty"` - DeleteColumns bool `xml:"deleteColumns,attr,omitempty"` - DeleteRows bool `xml:"deleteRows,attr,omitempty"` - SelectLockedCells bool `xml:"selectLockedCells,attr,omitempty"` - Sort bool `xml:"sort,attr,omitempty"` - AutoFilter bool `xml:"autoFilter,attr,omitempty"` - PivotTables bool `xml:"pivotTables,attr,omitempty"` - SelectUnlockedCells bool `xml:"selectUnlockedCells,attr,omitempty"` + Sheet bool `xml:"sheet,attr"` + Objects bool `xml:"objects,attr"` + Scenarios bool `xml:"scenarios,attr"` + FormatCells bool `xml:"formatCells,attr"` + FormatColumns bool `xml:"formatColumns,attr"` + FormatRows bool `xml:"formatRows,attr"` + InsertColumns bool `xml:"insertColumns,attr"` + InsertRows bool `xml:"insertRows,attr"` + InsertHyperlinks bool `xml:"insertHyperlinks,attr"` + DeleteColumns bool `xml:"deleteColumns,attr"` + DeleteRows bool `xml:"deleteRows,attr"` + SelectLockedCells bool `xml:"selectLockedCells,attr"` + Sort bool `xml:"sort,attr"` + AutoFilter bool `xml:"autoFilter,attr"` + PivotTables bool `xml:"pivotTables,attr"` + SelectUnlockedCells bool `xml:"selectUnlockedCells,attr"` } // xlsxPhoneticPr (Phonetic Properties) represents a collection of phonetic -- cgit v1.2.1 From e07581e980444b64bc15fce328ff07736ac9dbf6 Mon Sep 17 00:00:00 2001 From: Harris Date: Tue, 6 Aug 2019 16:43:56 -0500 Subject: Further improve read performance Instead of re-encoding the full sheet to change the namespaces in the encoded bytes, read the sheet again and do the byte replacements there. In this case, file access ends up being more performant than marshaling the sheet back to XML. In the SharedStrings test, ensure the strings are actually read. Fix #439 --- excelize_test.go | 6 +++++- rows.go | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/excelize_test.go b/excelize_test.go index 79010b1..4169983 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -1003,7 +1003,11 @@ func TestSharedStrings(t *testing.T) { if !assert.NoError(t, err) { t.FailNow() } - f.GetRows("Sheet1") + rows, err := f.GetRows("Sheet1") + if !assert.NoError(t, err) { + t.FailNow() + } + assert.Equal(t, "A", rows[0][0]) } func TestSetSheetRow(t *testing.T) { diff --git a/rows.go b/rows.go index cb0e31f..220c233 100644 --- a/rows.go +++ b/rows.go @@ -112,8 +112,8 @@ func (f *File) Rows(sheet string) (*Rows, error) { return nil, ErrSheetNotExist{sheet} } if xlsx != nil { - output, _ := xml.Marshal(f.Sheet[name]) - f.saveFileList(name, replaceWorkSheetsRelationshipsNameSpaceBytes(output)) + data := f.readXML(name) + f.saveFileList(name, replaceWorkSheetsRelationshipsNameSpaceBytes(namespaceStrictToTransitional(data))) } return &Rows{ f: f, -- cgit v1.2.1 From faaaa52cb862499454a7f893b92e8430d00172a5 Mon Sep 17 00:00:00 2001 From: Harris Date: Wed, 7 Aug 2019 08:53:37 -0500 Subject: Get sheet names based on index SheetID only seems to indicate the file name for the sheet. Check the sheets list based on index instead. Reordering sheets in Excel changes the order they appear in that list. Fixes #457 --- sheet.go | 15 ++++++--------- sheet_test.go | 21 +++++++++++++++++++++ 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/sheet.go b/sheet.go index e02782a..935deac 100644 --- a/sheet.go +++ b/sheet.go @@ -317,14 +317,11 @@ func (f *File) SetSheetName(oldName, newName string) { // string. func (f *File) GetSheetName(index int) string { wb := f.workbookReader() - if wb != nil { - for _, sheet := range wb.Sheets.Sheet { - if sheet.SheetID == index { - return sheet.Name - } - } + realIdx := index - 1 // sheets are 1 based index, but we're checking against an array + if wb == nil || realIdx < 0 || realIdx >= len(wb.Sheets.Sheet) { + return "" } - return "" + return wb.Sheets.Sheet[realIdx].Name } // GetSheetIndex provides a function to get worksheet index of XLSX by given @@ -357,8 +354,8 @@ func (f *File) GetSheetMap() map[int]string { wb := f.workbookReader() sheetMap := map[int]string{} if wb != nil { - for _, sheet := range wb.Sheets.Sheet { - sheetMap[sheet.SheetID] = sheet.Name + for i, sheet := range wb.Sheets.Sheet { + sheetMap[i+1] = sheet.Name } } return sheetMap diff --git a/sheet_test.go b/sheet_test.go index ef795ad..3baa084 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -225,3 +225,24 @@ func TestUngroupSheets(t *testing.T) { } assert.NoError(t, f.UngroupSheets()) } + +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)) +} + +func TestGetSheetMap(t *testing.T) { + expectedMap := map[int]string{ + 1: "Sheet1", + 2: "Sheet2", + } + f, _ := excelize.OpenFile(filepath.Join("test", "Book1.xlsx")) + sheetMap := f.GetSheetMap() + for idx, name := range sheetMap { + assert.Equal(t, expectedMap[idx], name) + } + assert.Equal(t, len(sheetMap), 2) +} -- cgit v1.2.1 From 51079288923076d00a8b36ecec07980a158d742c Mon Sep 17 00:00:00 2001 From: zaddok Date: Fri, 9 Aug 2019 09:47:06 +1000 Subject: Fix potential memory leak Fix potential memory leak where zw is not Close() when an error occurs. --- file.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/file.go b/file.go index a9e7eec..a4aa11d 100644 --- a/file.go +++ b/file.go @@ -109,10 +109,12 @@ func (f *File) WriteToBuffer() (*bytes.Buffer, error) { for path, content := range f.XLSX { fi, err := zw.Create(path) if err != nil { + zw.Close() return buf, err } _, err = fi.Write(content) if err != nil { + zw.Close() return buf, err } } -- cgit v1.2.1 From acd76425c2ee55c45a51cf7f71c8a6187a09f507 Mon Sep 17 00:00:00 2001 From: Harris Date: Wed, 7 Aug 2019 16:26:13 -0500 Subject: Handle multi row inline strings The inline string struct is actually the same as the shared strings struct, reuse it. Note that Go version 1.10 is required. Fixes #462 --- README.md | 2 +- excelize_test.go | 5 +++++ rows.go | 11 ++--------- test/SharedStrings.xlsx | Bin 6462 -> 9419 bytes xmlSharedStrings.go | 16 +++++++++++++++- xmlWorksheet.go | 14 ++++---------- 6 files changed, 27 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 91155e3..4bb7d66 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,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.8 or later. The full API docs can be seen using go's built-in documentation tool, or online at [godoc.org](https://godoc.org/github.com/360EntSecGroup-Skylar/excelize) and [docs reference](https://xuri.me/excelize/). +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 [godoc.org](https://godoc.org/github.com/360EntSecGroup-Skylar/excelize) and [docs reference](https://xuri.me/excelize/). ## Basic Usage diff --git a/excelize_test.go b/excelize_test.go index 4169983..d61dd7b 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -1008,6 +1008,11 @@ func TestSharedStrings(t *testing.T) { t.FailNow() } assert.Equal(t, "A", rows[0][0]) + rows, err = f.GetRows("Sheet2") + if !assert.NoError(t, err) { + t.FailNow() + } + assert.Equal(t, "Test Weight (Kgs)", rows[0][0]) } func TestSetSheetRow(t *testing.T) { diff --git a/rows.go b/rows.go index 220c233..c17179f 100644 --- a/rows.go +++ b/rows.go @@ -206,18 +206,11 @@ func (xlsx *xlsxC) getValueFrom(f *File, d *xlsxSST) (string, error) { case "s": xlsxSI := 0 xlsxSI, _ = strconv.Atoi(xlsx.V) - if len(d.SI[xlsxSI].R) > 0 { - value := "" - for _, v := range d.SI[xlsxSI].R { - value += v.T - } - return value, nil - } - return f.formattedValue(xlsx.S, d.SI[xlsxSI].T), nil + return f.formattedValue(xlsx.S, d.SI[xlsxSI].String()), nil case "str": return f.formattedValue(xlsx.S, xlsx.V), nil case "inlineStr": - return f.formattedValue(xlsx.S, xlsx.IS.T), nil + return f.formattedValue(xlsx.S, xlsx.IS.String()), nil default: return f.formattedValue(xlsx.S, xlsx.V), nil } diff --git a/test/SharedStrings.xlsx b/test/SharedStrings.xlsx index 7b722d9..d6004c0 100644 Binary files a/test/SharedStrings.xlsx and b/test/SharedStrings.xlsx differ diff --git a/xmlSharedStrings.go b/xmlSharedStrings.go index 3fcf3d5..48d4464 100644 --- a/xmlSharedStrings.go +++ b/xmlSharedStrings.go @@ -9,7 +9,10 @@ package excelize -import "encoding/xml" +import ( + "encoding/xml" + "strings" +) // xlsxSST directly maps the sst element from the namespace // http://schemas.openxmlformats.org/spreadsheetml/2006/main. String values may @@ -33,6 +36,17 @@ type xlsxSI struct { R []xlsxR `xml:"r"` } +func (x xlsxSI) String() string { + if len(x.R) > 0 { + var rows strings.Builder + for _, s := range x.R { + rows.WriteString(s.T) + } + return rows.String() + } + return x.T +} + // xlsxR directly maps the r element from the namespace // http://schemas.openxmlformats.org/spreadsheetml/2006/main - currently I have // not checked this for completeness - it does as much as I need. diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 9727866..a5db776 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -9,7 +9,9 @@ package excelize -import "encoding/xml" +import ( + "encoding/xml" +) // xlsxWorksheet directly maps the worksheet element in the namespace // http://schemas.openxmlformats.org/spreadsheetml/2006/main - currently I have @@ -424,18 +426,10 @@ type xlsxC struct { T string `xml:"t,attr,omitempty"` // Type. F *xlsxF `xml:"f,omitempty"` // Formula V string `xml:"v,omitempty"` // Value - IS *xlsxIS `xml:"is"` + IS *xlsxSI `xml:"is"` XMLSpace xml.Attr `xml:"space,attr,omitempty"` } -// xlsxIS directly maps the t element. Cell containing an (inline) rich -// string, i.e., one not in the shared string table. If this cell type is -// used, then the cell value is in the is element rather than the v element in -// the cell (c element). -type xlsxIS struct { - T string `xml:"t"` -} - // xlsxF directly maps the f element in the namespace // http://schemas.openxmlformats.org/spreadsheetml/2006/main - currently I have // not checked it for completeness - it does as much as I need. -- cgit v1.2.1 From 9c70d0ac868f66badf2663cc7b4b3c46d5411131 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 11 Aug 2019 00:36:14 +0800 Subject: Documentation updated, Go 1.10+ required --- .travis.yml | 1 - adjust.go | 2 +- calcchain.go | 2 +- cell.go | 2 +- cellmerged.go | 2 +- chart.go | 2 +- col.go | 2 +- comment.go | 2 +- datavalidation.go | 2 +- datavalidation_test.go | 2 +- date.go | 2 +- docProps.go | 2 +- docProps_test.go | 2 +- errors.go | 2 +- excelize.go | 2 +- file.go | 2 +- lib.go | 2 +- picture.go | 2 +- rows.go | 2 +- shape.go | 2 +- sheet.go | 4 +--- sheetpr.go | 2 +- sheetview.go | 2 +- sparkline.go | 2 +- styles.go | 2 +- table.go | 2 +- templates.go | 2 +- test/SharedStrings.xlsx | Bin 9419 -> 7386 bytes vmlDrawing.go | 2 +- xmlApp.go | 8 +++++++- xmlCalcChain.go | 2 +- xmlChart.go | 2 +- xmlComments.go | 2 +- xmlContentTypes.go | 2 +- xmlCore.go | 2 +- xmlDecodeDrawing.go | 2 +- xmlDrawing.go | 2 +- xmlSharedStrings.go | 2 +- xmlStyles.go | 2 +- xmlTable.go | 2 +- xmlTheme.go | 2 +- xmlWorkbook.go | 2 +- xmlWorksheet.go | 6 ++---- 43 files changed, 48 insertions(+), 47 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9f892c5..faf9916 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,6 @@ install: - go get -d -t -v ./... && go build -v ./... go: - - 1.9.x - 1.10.x - 1.11.x - 1.12.x diff --git a/adjust.go b/adjust.go index ccc5ce9..186112d 100644 --- a/adjust.go +++ b/adjust.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize diff --git a/calcchain.go b/calcchain.go index ce679e5..b4cadef 100644 --- a/calcchain.go +++ b/calcchain.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize diff --git a/cell.go b/cell.go index 6743e2a..f61e268 100644 --- a/cell.go +++ b/cell.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize diff --git a/cellmerged.go b/cellmerged.go index a78b244..c1df9b3 100644 --- a/cellmerged.go +++ b/cellmerged.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize diff --git a/chart.go b/chart.go index b9439ca..6a106f0 100644 --- a/chart.go +++ b/chart.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize diff --git a/col.go b/col.go index db3a901..ffa0ca6 100644 --- a/col.go +++ b/col.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize diff --git a/comment.go b/comment.go index bc6fa27..97e0e9b 100644 --- a/comment.go +++ b/comment.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize diff --git a/datavalidation.go b/datavalidation.go index 209204a..2499035 100644 --- a/datavalidation.go +++ b/datavalidation.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize diff --git a/datavalidation_test.go b/datavalidation_test.go index 0fee092..211830d 100644 --- a/datavalidation_test.go +++ b/datavalidation_test.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize diff --git a/date.go b/date.go index b49a695..8f63702 100644 --- a/date.go +++ b/date.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize diff --git a/docProps.go b/docProps.go index ff19fda..166512f 100644 --- a/docProps.go +++ b/docProps.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize diff --git a/docProps_test.go b/docProps_test.go index 1f52beb..671d998 100644 --- a/docProps_test.go +++ b/docProps_test.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize diff --git a/errors.go b/errors.go index 3404c7e..8520a01 100644 --- a/errors.go +++ b/errors.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize diff --git a/excelize.go b/excelize.go index 6d014a0..b734e57 100644 --- a/excelize.go +++ b/excelize.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. // // See https://xuri.me/excelize for more information about this package. package excelize diff --git a/file.go b/file.go index a4aa11d..46f1f62 100644 --- a/file.go +++ b/file.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize diff --git a/lib.go b/lib.go index b99b175..4dea16a 100644 --- a/lib.go +++ b/lib.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize diff --git a/picture.go b/picture.go index 812eb5c..62d48dc 100644 --- a/picture.go +++ b/picture.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize diff --git a/rows.go b/rows.go index c17179f..6281e62 100644 --- a/rows.go +++ b/rows.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize diff --git a/shape.go b/shape.go index 7dc7021..e3ed968 100644 --- a/shape.go +++ b/shape.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize diff --git a/sheet.go b/sheet.go index 935deac..ed6d888 100644 --- a/sheet.go +++ b/sheet.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize @@ -740,9 +740,7 @@ func (f *File) searchSheet(name, value string, regSearch bool) ([]string, error) result []string r xlsxRow ) - xml.NewDecoder(bytes.NewReader(f.readXML(name))) d := f.sharedStringsReader() - decoder := xml.NewDecoder(bytes.NewReader(f.readXML(name))) for { token, _ := decoder.Token() diff --git a/sheetpr.go b/sheetpr.go index 66761f3..a273ac1 100644 --- a/sheetpr.go +++ b/sheetpr.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize diff --git a/sheetview.go b/sheetview.go index 9712d85..09f5789 100644 --- a/sheetview.go +++ b/sheetview.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize diff --git a/sparkline.go b/sparkline.go index 73e125e..18eae6c 100644 --- a/sparkline.go +++ b/sparkline.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize diff --git a/styles.go b/styles.go index 04a5c33..c19cee0 100644 --- a/styles.go +++ b/styles.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize diff --git a/table.go b/table.go index 3d8d402..45a1622 100644 --- a/table.go +++ b/table.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize diff --git a/templates.go b/templates.go index 923cebd..0d3a0c5 100644 --- a/templates.go +++ b/templates.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. // // This file contains default templates for XML files we don't yet populated // based on content. diff --git a/test/SharedStrings.xlsx b/test/SharedStrings.xlsx index d6004c0..bcea2c8 100644 Binary files a/test/SharedStrings.xlsx and b/test/SharedStrings.xlsx differ diff --git a/vmlDrawing.go b/vmlDrawing.go index 8b1d00f..24b615f 100644 --- a/vmlDrawing.go +++ b/vmlDrawing.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize diff --git a/xmlApp.go b/xmlApp.go index ad414fa..48450e3 100644 --- a/xmlApp.go +++ b/xmlApp.go @@ -5,12 +5,15 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize import "encoding/xml" +// xlsxProperties specifies to an OOXML document properties such as the +// template used, the number of pages and words, and the application name and +// version. type xlsxProperties struct { XMLName xml.Name `xml:"http://schemas.openxmlformats.org/officeDocument/2006/extended-properties Properties"` Template string @@ -42,6 +45,8 @@ type xlsxProperties struct { DocSecurity int } +// xlsxVectorVariant specifies the set of hyperlinks that were in this +// document when last saved. type xlsxVectorVariant struct { Content string `xml:",innerxml"` } @@ -50,6 +55,7 @@ type xlsxVectorLpstr struct { Content string `xml:",innerxml"` } +// xlsxDigSig contains the signature of a digitally signed document. type xlsxDigSig struct { Content string `xml:",innerxml"` } diff --git a/xmlCalcChain.go b/xmlCalcChain.go index 05a176d..343f15f 100644 --- a/xmlCalcChain.go +++ b/xmlCalcChain.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize diff --git a/xmlChart.go b/xmlChart.go index 8a3a680..bb4b4bc 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize diff --git a/xmlComments.go b/xmlComments.go index 47d8f51..f13d002 100644 --- a/xmlComments.go +++ b/xmlComments.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize diff --git a/xmlContentTypes.go b/xmlContentTypes.go index e99b0b3..fa4d347 100644 --- a/xmlContentTypes.go +++ b/xmlContentTypes.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize diff --git a/xmlCore.go b/xmlCore.go index 357f688..96482fc 100644 --- a/xmlCore.go +++ b/xmlCore.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize diff --git a/xmlDecodeDrawing.go b/xmlDecodeDrawing.go index 6cb224a..e11bb00 100644 --- a/xmlDecodeDrawing.go +++ b/xmlDecodeDrawing.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize diff --git a/xmlDrawing.go b/xmlDrawing.go index 20cb83d..ade6261 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize diff --git a/xmlSharedStrings.go b/xmlSharedStrings.go index 48d4464..7983741 100644 --- a/xmlSharedStrings.go +++ b/xmlSharedStrings.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize diff --git a/xmlStyles.go b/xmlStyles.go index 49abe3c..5823bc9 100644 --- a/xmlStyles.go +++ b/xmlStyles.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize diff --git a/xmlTable.go b/xmlTable.go index 6d27dc9..ca4ce03 100644 --- a/xmlTable.go +++ b/xmlTable.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize diff --git a/xmlTheme.go b/xmlTheme.go index 01d0054..f764c20 100644 --- a/xmlTheme.go +++ b/xmlTheme.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize diff --git a/xmlWorkbook.go b/xmlWorkbook.go index 0118685..8150e29 100644 --- a/xmlWorkbook.go +++ b/xmlWorkbook.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize diff --git a/xmlWorksheet.go b/xmlWorksheet.go index a5db776..09dec5e 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -5,13 +5,11 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize -import ( - "encoding/xml" -) +import "encoding/xml" // xlsxWorksheet directly maps the worksheet element in the namespace // http://schemas.openxmlformats.org/spreadsheetml/2006/main - currently I have -- cgit v1.2.1 From ac395a60ed2ac643403678991ff4745231ff48c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20Mengu=C3=A9?= Date: Tue, 13 Aug 2019 15:39:12 +0200 Subject: SetCellValue: use fmt.Sprint(v) instead of fmt.Sprintf("%v", v) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Because that does the same thing, but without having to parse a format string. Signed-off-by: Olivier Mengué --- cell.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cell.go b/cell.go index f61e268..9d478a5 100644 --- a/cell.go +++ b/cell.go @@ -94,7 +94,7 @@ func (f *File) SetCellValue(sheet, axis string, value interface{}) error { case nil: err = f.SetCellStr(sheet, axis, "") default: - err = f.SetCellStr(sheet, axis, fmt.Sprintf("%v", value)) + err = f.SetCellStr(sheet, axis, fmt.Sprint(value)) } return err } -- cgit v1.2.1 From 64809db2c9ee30779e4839e9d60a315479092ce6 Mon Sep 17 00:00:00 2001 From: mqy Date: Mon, 19 Aug 2019 15:53:56 +0800 Subject: add missing error check in SetSheetRow() --- cell.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/cell.go b/cell.go index 9d478a5..e897379 100644 --- a/cell.go +++ b/cell.go @@ -471,12 +471,14 @@ func (f *File) SetSheetRow(sheet, axis string, slice interface{}) error { 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 + // Error should never happens here. But keep checking to early detect regresions + // if it will be introduced in future. if err != nil { return err } - f.SetCellValue(sheet, cell, v.Index(i).Interface()) + if err := f.SetCellValue(sheet, cell, v.Index(i).Interface()); err != nil { + return err + } } return err } -- cgit v1.2.1 From 407fb55c20a2524c4eccad9361120dee2a2719cd Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 21 Aug 2019 23:03:34 +0800 Subject: Update the Godoc --- README_zh.md | 2 +- excelize_test.go | 4 ++-- shape.go | 2 +- sparkline.go | 14 +++++++------- styles.go | 2 +- styles_test.go | 4 ++-- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/README_zh.md b/README_zh.md index 044d930..d57f301 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.8 或更高版本,完整的 API 使用文档请访问 [godoc.org](https://godoc.org/github.com/360EntSecGroup-Skylar/excelize) 或查看 [参考文档](https://xuri.me/excelize/)。 +Excelize 是 Go 语言编写的用于操作 Office Excel 文档类库,基于 ECMA-376 Office OpenXML 标准。可以使用它来读取、写入由 Microsoft Excel™ 2007 及以上版本创建的 XLSX 文档。相比较其他的开源类库,Excelize 支持写入原本带有图片(表)、透视表和切片器等复杂样式的文档,还支持向 Excel 文档中插入图片与图表,并且在保存后不会丢失文档原有样式,可以应用于各类报表系统中。使用本类库要求使用的 Go 语言为 1.10 或更高版本,完整的 API 使用文档请访问 [godoc.org](https://godoc.org/github.com/360EntSecGroup-Skylar/excelize) 或查看 [参考文档](https://xuri.me/excelize/)。 ## 快速上手 diff --git a/excelize_test.go b/excelize_test.go index d61dd7b..a5d7671 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -640,7 +640,7 @@ func TestSetCellStyleFont(t *testing.T) { } var style int - style, err = f.NewStyle(`{"font":{"bold":true,"italic":true,"family":"Berlin Sans FB Demi","size":36,"color":"#777777","underline":"single"}}`) + style, err = f.NewStyle(`{"font":{"bold":true,"italic":true,"family":"Times New Roman","size":36,"color":"#777777","underline":"single"}}`) if !assert.NoError(t, err) { t.FailNow() } @@ -809,7 +809,7 @@ func TestAddShape(t *testing.T) { f.AddShape("Sheet1", "A30", `{"type":"rect","paragraph":[{"text":"Rectangle","font":{"color":"CD5C5C"}},{"text":"Shape","font":{"bold":true,"color":"2980B9"}}]}`) f.AddShape("Sheet1", "B30", `{"type":"rect","paragraph":[{"text":"Rectangle"},{}]}`) f.AddShape("Sheet1", "C30", `{"type":"rect","paragraph":[]}`) - f.AddShape("Sheet3", "H1", `{"type":"ellipseRibbon", "color":{"line":"#4286f4","fill":"#8eb9ff"}, "paragraph":[{"font":{"bold":true,"italic":true,"family":"Berlin Sans FB Demi","size":36,"color":"#777777","underline":"single"}}], "height": 90}`) + f.AddShape("Sheet3", "H1", `{"type":"ellipseRibbon", "color":{"line":"#4286f4","fill":"#8eb9ff"}, "paragraph":[{"font":{"bold":true,"italic":true,"family":"Times New Roman","size":36,"color":"#777777","underline":"single"}}], "height": 90}`) f.AddShape("Sheet3", "H1", "") assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddShape.xlsx"))) diff --git a/shape.go b/shape.go index e3ed968..8d95849 100644 --- a/shape.go +++ b/shape.go @@ -40,7 +40,7 @@ func parseFormatShapeSet(formatSet string) (*formatShape, error) { // print settings) and properties set. For example, add text box (rect shape) // in Sheet1: // -// err := f.AddShape("Sheet1", "G6", `{"type":"rect","color":{"line":"#4286F4","fill":"#8eb9ff"},"paragraph":[{"text":"Rectangle Shape","font":{"bold":true,"italic":true,"family":"Berlin Sans FB Demi","size":36,"color":"#777777","underline":"sng"}}],"width":180,"height": 90}`) +// err := f.AddShape("Sheet1", "G6", `{"type":"rect","color":{"line":"#4286F4","fill":"#8eb9ff"},"paragraph":[{"text":"Rectangle Shape","font":{"bold":true,"italic":true,"family":"Times New Roman","size":36,"color":"#777777","underline":"sng"}}],"width":180,"height": 90}`) // // The following shows the type of shape supported by excelize: // diff --git a/sparkline.go b/sparkline.go index 18eae6c..314ea83 100644 --- a/sparkline.go +++ b/sparkline.go @@ -374,15 +374,15 @@ func (f *File) addSparklineGroupByStyle(ID int) *xlsxX14SparklineGroup { // Parameter | Description // -----------+-------------------------------------------- // Location | Required, must have the same number with 'Range' parameter -// Range |Required, must have the same number with 'Location' parameter +// Range | Required, must have the same number with 'Location' parameter // Type | Enumeration value: line, column, win_loss // Style | Value range: 0 - 35 -// Hight | Toggle sparkine high points -// Low | Toggle sparkine low points -// First | Toggle sparkine first points -// Last | Toggle sparkine last points -// Negative | Toggle sparkine negative points -// Markers | Toggle sparkine markers +// Hight | Toggle sparkline high points +// Low | Toggle sparkline low points +// First | Toggle sparkline first points +// Last | Toggle sparkline last points +// Negative | Toggle sparkline negative points +// Markers | Toggle sparkline markers // ColorAxis | An RGB Color is specified as RRGGBB // Axis | Show sparkline axis // diff --git a/styles.go b/styles.go index c19cee0..16f8030 100644 --- a/styles.go +++ b/styles.go @@ -2351,7 +2351,7 @@ func (f *File) GetCellStyle(sheet, axis string) (int, error) { // // Set font style for cell H9 on Sheet1: // -// style, err := f.NewStyle(`{"font":{"bold":true,"italic":true,"family":"Berlin Sans FB Demi","size":36,"color":"#777777"}}`) +// style, err := f.NewStyle(`{"font":{"bold":true,"italic":true,"family":"Times New Roman","size":36,"color":"#777777"}}`) // if err != nil { // fmt.Println(err) // } diff --git a/styles_test.go b/styles_test.go index decfbb9..36a78ed 100644 --- a/styles_test.go +++ b/styles_test.go @@ -168,14 +168,14 @@ func TestSetConditionalFormat(t *testing.T) { func TestNewStyle(t *testing.T) { f := NewFile() - styleID, err := f.NewStyle(`{"font":{"bold":true,"italic":true,"family":"Berlin Sans FB Demi","size":36,"color":"#777777"}}`) + styleID, err := f.NewStyle(`{"font":{"bold":true,"italic":true,"family":"Times New Roman","size":36,"color":"#777777"}}`) if err != nil { t.Fatal(err) } styles := f.stylesReader() fontID := styles.CellXfs.Xf[styleID].FontID font := styles.Fonts.Font[fontID] - assert.Contains(t, font.Name.Val, "Berlin Sans FB Demi", "Stored font should contain font name") + assert.Contains(t, font.Name.Val, "Times New Roman", "Stored font should contain font name") assert.Equal(t, 2, styles.CellXfs.Count, "Should have 2 styles") } -- cgit v1.2.1 From 875dd22bd013ef3873711c8e82f3d4d5e1675ebc Mon Sep 17 00:00:00 2001 From: Matthew McFarling Date: Wed, 28 Aug 2019 17:05:27 -0400 Subject: Updating Readme Removing the /v2 on the package url as it does not work with the ```go get``` command. --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 4bb7d66..7f9cf70 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Supports saving a file without losing original charts of XLSX. This library need ### Installation ```bash -go get github.com/360EntSecGroup-Skylar/excelize/v2 +go get github.com/360EntSecGroup-Skylar/excelize ``` ### Create XLSX file @@ -34,7 +34,7 @@ package main import ( "fmt" - "github.com/360EntSecGroup-Skylar/excelize/v2" + "github.com/360EntSecGroup-Skylar/excelize" ) func main() { @@ -64,7 +64,7 @@ package main import ( "fmt" - "github.com/360EntSecGroup-Skylar/excelize/v2" + "github.com/360EntSecGroup-Skylar/excelize" ) func main() { @@ -103,7 +103,7 @@ package main import ( "fmt" - "github.com/360EntSecGroup-Skylar/excelize/v2" + "github.com/360EntSecGroup-Skylar/excelize" ) func main() { @@ -140,7 +140,7 @@ import ( _ "image/jpeg" _ "image/png" - "github.com/360EntSecGroup-Skylar/excelize/v2" + "github.com/360EntSecGroup-Skylar/excelize" ) func main() { -- cgit v1.2.1 From 1fc4bc52fb8a0160eb624e7a24619d8c0e47e540 Mon Sep 17 00:00:00 2001 From: Vsevolod Balashov Date: Sun, 1 Sep 2019 07:30:14 +0300 Subject: Fix #386 regression test added (#440) * #386 regression test added * closes #386 string to bigint on GOARCH=386 --- cell_test.go | 13 +++++++++++++ styles.go | 2 +- test/OverflowNumericCell.xlsx | Bin 0 -> 11445 bytes 3 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 test/OverflowNumericCell.xlsx diff --git a/cell_test.go b/cell_test.go index d4a5b02..09627c2 100644 --- a/cell_test.go +++ b/cell_test.go @@ -2,6 +2,7 @@ package excelize import ( "fmt" + "path/filepath" "testing" "github.com/stretchr/testify/assert" @@ -94,3 +95,15 @@ func BenchmarkSetCellValue(b *testing.B) { } } } + +func TestOverflowNumericCell(t *testing.T) { + f, err := OpenFile(filepath.Join("test", "OverflowNumericCell.xlsx")) + if !assert.NoError(t, err) { + t.FailNow() + } + // source of xlsx file is Russia, don`t touch it, elsewhere bug not reproduced + val, err := f.GetCellValue("Лист1", "A1") + assert.NoError(t, err) + // GOARCH=amd64 - all ok; GOARCH=386 - actual : "-2147483648" + assert.Equal(t, "8595602512225", val, "A1 should be 8595602512225") +} diff --git a/styles.go b/styles.go index 16f8030..4d6071a 100644 --- a/styles.go +++ b/styles.go @@ -852,7 +852,7 @@ func formatToInt(i int, v string) string { if err != nil { return v } - return fmt.Sprintf("%d", int(f)) + return fmt.Sprintf("%d", int64(f)) } // formatToFloat provides a function to convert original string to float diff --git a/test/OverflowNumericCell.xlsx b/test/OverflowNumericCell.xlsx new file mode 100644 index 0000000..1844888 Binary files /dev/null and b/test/OverflowNumericCell.xlsx differ -- cgit v1.2.1 From 0acb3ef9685e80d51dfda5ab9a9db870af7e1614 Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 2 Sep 2019 21:52:55 +0800 Subject: Testing files updated --- README_zh.md | 10 +++++----- cell_test.go | 5 ++--- chart.go | 2 +- picture.go | 4 ++-- sheet_test.go | 3 ++- sheetpr_test.go | 2 +- sheetview_test.go | 2 +- test/BadWorkbook.xlsx | Bin 6019 -> 4955 bytes test/CalcChain.xlsx | Bin 5959 -> 5838 bytes test/MergeCell.xlsx | Bin 8583 -> 6343 bytes test/OverflowNumericCell.xlsx | Bin 11445 -> 5094 bytes 11 files changed, 14 insertions(+), 14 deletions(-) diff --git a/README_zh.md b/README_zh.md index d57f301..6c2b190 100644 --- a/README_zh.md +++ b/README_zh.md @@ -20,7 +20,7 @@ Excelize 是 Go 语言编写的用于操作 Office Excel 文档类库,基于 E ### 安装 ```bash -go get github.com/360EntSecGroup-Skylar/excelize/v2 +go get github.com/360EntSecGroup-Skylar/excelize ``` ### 创建 Excel 文档 @@ -33,7 +33,7 @@ package main import ( "fmt" - "github.com/360EntSecGroup-Skylar/excelize/v2" + "github.com/360EntSecGroup-Skylar/excelize" ) func main() { @@ -63,7 +63,7 @@ package main import ( "fmt" - "github.com/360EntSecGroup-Skylar/excelize/v2" + "github.com/360EntSecGroup-Skylar/excelize" ) func main() { @@ -102,7 +102,7 @@ package main import ( "fmt" - "github.com/360EntSecGroup-Skylar/excelize/v2" + "github.com/360EntSecGroup-Skylar/excelize" ) func main() { @@ -140,7 +140,7 @@ import ( _ "image/jpeg" _ "image/png" - "github.com/360EntSecGroup-Skylar/excelize/v2" + "github.com/360EntSecGroup-Skylar/excelize" ) func main() { diff --git a/cell_test.go b/cell_test.go index 09627c2..653aaab 100644 --- a/cell_test.go +++ b/cell_test.go @@ -101,9 +101,8 @@ func TestOverflowNumericCell(t *testing.T) { if !assert.NoError(t, err) { t.FailNow() } - // source of xlsx file is Russia, don`t touch it, elsewhere bug not reproduced - val, err := f.GetCellValue("Лист1", "A1") + val, err := f.GetCellValue("Sheet1", "A1") assert.NoError(t, err) - // GOARCH=amd64 - all ok; GOARCH=386 - actual : "-2147483648" + // GOARCH=amd64 - all ok; GOARCH=386 - actual: "-2147483648" assert.Equal(t, "8595602512225", val, "A1 should be 8595602512225") } diff --git a/chart.go b/chart.go index 6a106f0..e1eb81f 100644 --- a/chart.go +++ b/chart.go @@ -543,7 +543,7 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // import ( // "fmt" // -// "github.com/360EntSecGroup-Skylar/excelize/v2" +// "github.com/360EntSecGroup-Skylar/excelize" // ) // // func main() { diff --git a/picture.go b/picture.go index 62d48dc..a5904ff 100644 --- a/picture.go +++ b/picture.go @@ -51,7 +51,7 @@ func parseFormatPictureSet(formatSet string) (*formatPicture, error) { // _ "image/jpeg" // _ "image/png" // -// "github.com/360EntSecGroup-Skylar/excelize/v2" +// "github.com/360EntSecGroup-Skylar/excelize" // ) // // func main() { @@ -111,7 +111,7 @@ func (f *File) AddPicture(sheet, cell, picture, format string) error { // _ "image/jpeg" // "io/ioutil" // -// "github.com/360EntSecGroup-Skylar/excelize/v2" +// "github.com/360EntSecGroup-Skylar/excelize" // ) // // func main() { diff --git a/sheet_test.go b/sheet_test.go index 3baa084..145e302 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -6,7 +6,8 @@ import ( "strings" "testing" - "github.com/360EntSecGroup-Skylar/excelize/v2" + "github.com/360EntSecGroup-Skylar/excelize" + "github.com/mohae/deepcopy" "github.com/stretchr/testify/assert" ) diff --git a/sheetpr_test.go b/sheetpr_test.go index 97a314c..b9f9e3b 100644 --- a/sheetpr_test.go +++ b/sheetpr_test.go @@ -7,7 +7,7 @@ import ( "github.com/mohae/deepcopy" "github.com/stretchr/testify/assert" - "github.com/360EntSecGroup-Skylar/excelize/v2" + "github.com/360EntSecGroup-Skylar/excelize" ) var _ = []excelize.SheetPrOption{ diff --git a/sheetview_test.go b/sheetview_test.go index 2e697b8..07f59ed 100644 --- a/sheetview_test.go +++ b/sheetview_test.go @@ -6,7 +6,7 @@ import ( "github.com/stretchr/testify/assert" - "github.com/360EntSecGroup-Skylar/excelize/v2" + "github.com/360EntSecGroup-Skylar/excelize" ) var _ = []excelize.SheetViewOption{ diff --git a/test/BadWorkbook.xlsx b/test/BadWorkbook.xlsx index f917a20..a1901b0 100644 Binary files a/test/BadWorkbook.xlsx and b/test/BadWorkbook.xlsx differ diff --git a/test/CalcChain.xlsx b/test/CalcChain.xlsx index 8558f82..fa57710 100644 Binary files a/test/CalcChain.xlsx and b/test/CalcChain.xlsx differ diff --git a/test/MergeCell.xlsx b/test/MergeCell.xlsx index d4dad18..3539e4b 100644 Binary files a/test/MergeCell.xlsx and b/test/MergeCell.xlsx differ diff --git a/test/OverflowNumericCell.xlsx b/test/OverflowNumericCell.xlsx index 1844888..9da5091 100644 Binary files a/test/OverflowNumericCell.xlsx and b/test/OverflowNumericCell.xlsx differ -- cgit v1.2.1 From b30c642e2bf2a328cf087e03399b783e02e1e647 Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 5 Sep 2019 23:42:40 +0800 Subject: Prepare pivot table support, add pivot table definition struct --- xmlPivotTable.go | 289 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 289 insertions(+) create mode 100644 xmlPivotTable.go diff --git a/xmlPivotTable.go b/xmlPivotTable.go new file mode 100644 index 0000000..16c469f --- /dev/null +++ b/xmlPivotTable.go @@ -0,0 +1,289 @@ +// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// this source code is governed by a BSD-style license that can be found in +// the LICENSE file. +// +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.10 or later. + +package excelize + +import "encoding/xml" + +// xlsxPivotTableDefinition represents the PivotTable root element for +// non-null PivotTables. There exists one pivotTableDefinition for each +// PivotTableDefinition part +type xlsxPivotTableDefinition struct { + XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main pivotTableDefinition"` + Name string `xml:"name,attr"` + CacheID int `xml:"cacheId,attr"` + DataOnRows bool `xml:"dataOnRows,attr"` + DataPosition int `xml:"dataPosition,attr"` + DataCaption string `xml:"dataCaption,attr"` + GrandTotalCaption string `xml:"grandTotalCaption,attr"` + ErrorCaption string `xml:"errorCaption,attr"` + ShowError bool `xml:"showError,attr"` + MissingCaption string `xml:"missingCaption,attr"` + ShowMissing bool `xml:"showMissing,attr"` + PageStyle string `xml:"pageStyle,attr"` + PivotTableStyle string `xml:"pivotTableStyle,attr"` + VacatedStyle string `xml:"vacatedStyle,attr"` + Tag string `xml:"tag,attr"` + UpdatedVersion int `xml:"updatedVersion,attr"` + MinRefreshableVersion int `xml:"minRefreshableVersion,attr"` + AsteriskTotals bool `xml:"asteriskTotals,attr"` + ShowItems bool `xml:"showItems,attr"` + EditData bool `xml:"editData,attr"` + DisableFieldList bool `xml:"disableFieldList,attr"` + ShowCalcMbrs bool `xml:"showCalcMbrs,attr"` + VisualTotals bool `xml:"visualTotals,attr"` + ShowMultipleLabel bool `xml:"showMultipleLabel,attr"` + ShowDataDropDown bool `xml:"showDataDropDown,attr"` + ShowDrill bool `xml:"showDrill,attr"` + PrintDrill bool `xml:"printDrill,attr"` + ShowMemberPropertyTips bool `xml:"showMemberPropertyTips,attr"` + ShowDataTips bool `xml:"showDataTips,attr"` + EnableWizard bool `xml:"enableWizard,attr"` + EnableDrill bool `xml:"enableDrill,attr"` + EnableFieldProperties bool `xml:"enableFieldProperties,attr"` + PreserveFormatting bool `xml:"preserveFormatting,attr"` + UseAutoFormatting bool `xml:"useAutoFormatting,attr"` + PageWrap int `xml:"pageWrap,attr"` + PageOverThenDown bool `xml:"pageOverThenDown,attr"` + SubtotalHiddenItems bool `xml:"subtotalHiddenItems,attr"` + RowGrandTotals bool `xml:"rowGrandTotals,attr"` + ColGrandTotals bool `xml:"colGrandTotals,attr"` + FieldPrintTitles bool `xml:"fieldPrintTitles,attr"` + ItemPrintTitles bool `xml:"itemPrintTitles,attr"` + MergeItem bool `xml:"mergeItem,attr"` + ShowDropZones bool `xml:"showDropZones,attr"` + CreatedVersion int `xml:"createdVersion,attr"` + Indent int `xml:"indent,attr"` + ShowEmptyRow bool `xml:"showEmptyRow,attr"` + ShowEmptyCol bool `xml:"showEmptyCol,attr"` + ShowHeaders bool `xml:"showHeaders,attr"` + Compact bool `xml:"compact,attr"` + Outline bool `xml:"outline,attr"` + OutlineData bool `xml:"outlineData,attr"` + CompactData bool `xml:"compactData,attr"` + Published bool `xml:"published,attr"` + GridDropZones bool `xml:"gridDropZones,attr"` + Immersive bool `xml:"immersive,attr"` + MultipleFieldFilters bool `xml:"multipleFieldFilters,attr"` + ChartFormat int `xml:"chartFormat,attr"` + RowHeaderCaption string `xml:"rowHeaderCaption,attr"` + ColHeaderCaption string `xml:"colHeaderCaption,attr"` + FieldListSortAscending bool `xml:"fieldListSortAscending,attr"` + MdxSubqueries bool `xml:"mdxSubqueries,attr"` + CustomListSort bool `xml:"customListSort,attr"` + Location *xlsxLocation `xml:"location"` + PivotFields *xlsxPivotFields `xml:"pivotFields"` + RowFields *xlsxRowFields `xml:"rowFields"` + RowItems *xlsxRowItems `xml:"rowItems"` + ColFields *xlsxColFields `xml:"colFields"` + ColItems *xlsxColItems `xml:"colItems"` + PageFields *xlsxPageFields `xml:"pageFields"` + DataFields *xlsxDataFields `xml:"dataFields"` + ConditionalFormats *xlsxConditionalFormats `xml:"conditionalFormats"` + PivotTableStyleInfo *xlsxPivotTableStyleInfo `xml:"pivotTableStyleInfo"` +} + +// xlsxLocation represents location information for the PivotTable. +type xlsxLocation struct { + Ref string `xml:"ref,attr"` + FirstHeaderRow int `xml:"firstHeaderRow,attr"` + FirstDataRow int `xml:"firstDataRow,attr"` + FirstDataCol int `xml:"firstDataCol,attr"` + RowPageCount int `xml:"rowPageCount,attr"` + ColPageCount int `xml:"colPageCount,attr"` +} + +// xlsxPivotFields represents the collection of fields that appear on the +// PivotTable. +type xlsxPivotFields struct { + Count int `xml:"count,attr"` + PivotField []*xlsxPivotField `xml:"pivotField"` +} + +// xlsxPivotField represents a single field in the PivotTable. This element +// contains information about the field, including the collection of items in +// the field. +type xlsxPivotField struct { + Name string `xml:"name,attr"` + Axis string `xml:"axis,attr,omitempty"` + DataField bool `xml:"dataField,attr"` + SubtotalCaption string `xml:"subtotalCaption,attr"` + ShowDropDowns bool `xml:"showDropDowns,attr"` + HiddenLevel bool `xml:"hiddenLevel,attr"` + UniqueMemberProperty string `xml:"uniqueMemberProperty,attr"` + Compact bool `xml:"compact,attr"` + AllDrilled bool `xml:"allDrilled,attr"` + NumFmtId string `xml:"numFmtId,attr,omitempty"` + Outline bool `xml:"outline,attr"` + SubtotalTop bool `xml:"subtotalTop,attr"` + DragToRow bool `xml:"dragToRow,attr"` + DragToCol bool `xml:"dragToCol,attr"` + MultipleItemSelectionAllowed bool `xml:"multipleItemSelectionAllowed,attr"` + DragToPage bool `xml:"dragToPage,attr"` + DragToData bool `xml:"dragToData,attr"` + DragOff bool `xml:"dragOff,attr"` + ShowAll bool `xml:"showAll,attr"` + InsertBlankRow bool `xml:"insertBlankRow,attr"` + ServerField bool `xml:"serverField,attr"` + InsertPageBreak bool `xml:"insertPageBreak,attr"` + AutoShow bool `xml:"autoShow,attr"` + TopAutoShow bool `xml:"topAutoShow,attr"` + HideNewItems bool `xml:"hideNewItems,attr"` + MeasureFilter bool `xml:"measureFilter,attr"` + IncludeNewItemsInFilter bool `xml:"includeNewItemsInFilter,attr"` + ItemPageCount int `xml:"itemPageCount,attr"` + SortType string `xml:"sortType,attr"` + DataSourceSort bool `xml:"dataSourceSort,attr,omitempty"` + NonAutoSortDefault bool `xml:"nonAutoSortDefault,attr"` + RankBy int `xml:"rankBy,attr,omitempty"` + DefaultSubtotal bool `xml:"defaultSubtotal,attr"` + SumSubtotal bool `xml:"sumSubtotal,attr"` + CountASubtotal bool `xml:"countASubtotal,attr"` + AvgSubtotal bool `xml:"avgSubtotal,attr"` + MaxSubtotal bool `xml:"maxSubtotal,attr"` + MinSubtotal bool `xml:"minSubtotal,attr"` + ProductSubtotal bool `xml:"productSubtotal,attr"` + CountSubtotal bool `xml:"countSubtotal,attr"` + StdDevSubtotal bool `xml:"stdDevSubtotal,attr"` + StdDevPSubtotal bool `xml:"stdDevPSubtotal,attr"` + VarSubtotal bool `xml:"varSubtotal,attr"` + VarPSubtotal bool `xml:"varPSubtotal,attr"` + ShowPropCell bool `xml:"showPropCell,attr,omitempty"` + ShowPropTip bool `xml:"showPropTip,attr,omitempty"` + ShowPropAsCaption bool `xml:"showPropAsCaption,attr,omitempty"` + DefaultAttributeDrillState bool `xml:"defaultAttributeDrillState,attr,omitempty"` + Items *xlsxItems `xml:"items"` + AutoSortScope *xlsxAutoSortScope `xml:"autoSortScope"` + ExtLst *xlsxExtLst `xml:"extLst"` +} + +// xlsxItems represents the collection of items in a PivotTable field. The +// items in the collection are ordered by index. Items represent the unique +// entries from the field in the source data. +type xlsxItems struct { + Count int `xml:"count,attr"` + Item []*xlsxItem `xml:"item"` +} + +// xlsxItem represents a single item in PivotTable field. +type xlsxItem struct { + N string `xml:"n,attr"` + T string `xml:"t,attr"` + H bool `xml:"h,attr"` + S bool `xml:"s,attr"` + SD bool `xml:"sd,attr"` + F bool `xml:"f,attr"` + M bool `xml:"m,attr"` + C bool `xml:"c,attr"` + X int `xml:"x,attr,omitempty"` + D bool `xml:"d,attr"` + E bool `xml:"e,attr"` +} + +// xlsxAutoSortScope represents the sorting scope for the PivotTable. +type xlsxAutoSortScope struct { +} + +// xlsxRowFields represents the collection of row fields for the PivotTable. +type xlsxRowFields struct { + Count int `xml:"count,attr"` + Fields []*xlsxField `xml:"fields"` +} + +// xlsxField represents a generic field that can appear either on the column +// or the row region of the PivotTable. There areas many elements as there +// are item values in any particular column or row. +type xlsxField struct { + X int `xml:"x,attr"` +} + +// xlsxRowItems represents the collection of items in row axis of the +// PivotTable. +type xlsxRowItems struct { + Count int `xml:"count,attr"` + I []*xlsxI `xml:"i"` +} + +// xlsxI represents the collection of items in the row region of the +// PivotTable. +type xlsxI struct { + X []*xlsxX `xml:"x"` +} + +// xlsxX represents an array of indexes to cached shared item values. +type xlsxX struct { + XMLName xml.Name `xml:"x"` +} + +// xlsxColFields represents the collection of fields that are on the column +// axis of the PivotTable. +type xlsxColFields struct { + Count int `xml:"count,attr"` + Fields []*xlsxField `xml:"fields"` +} + +// xlsxColItems represents the collection of column items of the PivotTable. +type xlsxColItems struct { + Count int `xml:"count,attr"` + I []*xlsxI `xml:"i"` +} + +// xlsxPageFields represents the collection of items in the page or report +// filter region of the PivotTable. +type xlsxPageFields struct { + Count int `xml:"count,attr"` + PageField []*xlsxPageField `xml:"pageField"` +} + +// xlsxPageField represents a field on the page or report filter of the +// PivotTable. +type xlsxPageField struct { + Fld int `xml:"fld,attr"` + Item int `xml:"item,attr,omitempty"` + Hier int `xml:"hier,attr"` + Name string `xml:"name,attr"` + Cap string `xml:"cap,attr"` + ExtLst *xlsxExtLst `xml:"extLst"` +} + +// xlsxDataFields represents the collection of items in the data region of the +// PivotTable. +type xlsxDataFields struct { + Count int `xml:"count,attr"` + DataField *xlsxDataField `xml:"dataField"` +} + +// xlsxDataField represents a field from a source list, table, or database +// that contains data that is summarized in a PivotTable. +type xlsxDataField struct { + Name string `xml:"name,attr,omitempty"` + Fld int `xml:"fld,attr"` + Subtotal string `xml:"subtotal,attr"` + ShowDataAs string `xml:"showDataAs,attr"` + BaseField int `xml:"baseField,attr"` + BaseItem int64 `xml:"baseItem,attr"` + NumFmtId string `xml:"numFmtId,attr,omitempty"` + ExtLst *xlsxExtLst `xml:"extLst"` +} + +// xlsxConditionalFormats represents the collection of conditional formats +// applied to a PivotTable. +type xlsxConditionalFormats struct { +} + +// xlsxPivotTableStyleInfo represent information on style applied to the +// PivotTable. +type xlsxPivotTableStyleInfo struct { + Name string `xml:"name,attr"` + ShowRowHeaders bool `xml:"showRowHeaders,attr"` + ShowColHeaders bool `xml:"showColHeaders,attr"` + ShowRowStripes bool `xml:"showRowStripes,attr"` + ShowColStripes bool `xml:"showColStripes,attr"` + ShowLastColumn bool `xml:"showLastColumn,attr,omitempty"` +} -- cgit v1.2.1 From acbabcf8fcf2e27dc466ca17185eb6a9e7397356 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 8 Sep 2019 21:57:55 +0800 Subject: Add pivot table cache definition struct --- xmlPivotCache.go | 196 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 196 insertions(+) create mode 100644 xmlPivotCache.go diff --git a/xmlPivotCache.go b/xmlPivotCache.go new file mode 100644 index 0000000..9e07931 --- /dev/null +++ b/xmlPivotCache.go @@ -0,0 +1,196 @@ +package excelize + +import "encoding/xml" + +// pivotCacheDefinition represents the pivotCacheDefinition part. This part +// defines each field in the source data, including the name, the string +// resources of the instance data (for shared items), and information about +// the type of data that appears in the field. +type xmlPivotCacheDefinition struct { + XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main pivotCacheDefinition"` + RID string `xml:"http://schemas.openxmlformats.org/officeDocument/2006/relationships id,attr,omitempty"` + Invalid bool `xml:"invalid,attr,omitempty"` + SaveData bool `xml:"saveData,attr,omitempty"` + RefreshOnLoad bool `xml:"refreshOnLoad,attr,omitempty"` + OptimizeMemory bool `xml:"optimizeMemory,attr,omitempty"` + EnableRefresh bool `xml:"enableRefresh,attr,omitempty"` + RefreshedBy string `xml:"refreshedBy,attr,omitempty"` + RefreshedDate float64 `xml:"refreshedDate,attr,omitempty"` + RefreshedDateIso float64 `xml:"refreshedDateIso,attr,omitempty"` + BackgroundQuery bool `xml:"backgroundQuery,attr"` + MissingItemsLimit int `xml:"missingItemsLimit,attr,omitempty"` + CreatedVersion int `xml:"createdVersion,attr,omitempty"` + RefreshedVersion int `xml:"refreshedVersion,attr,omitempty"` + MinRefreshableVersion int `xml:"minRefreshableVersion,attr,omitempty"` + RecordCount int `xml:"recordCount,attr,omitempty"` + UpgradeOnRefresh bool `xml:"upgradeOnRefresh,attr,omitempty"` + TupleCacheAttr bool `xml:"tupleCache,attr,omitempty"` + SupportSubquery bool `xml:"supportSubquery,attr,omitempty"` + SupportAdvancedDrill bool `xml:"supportAdvancedDrill,attr,omitempty"` + CacheSource *xlsxCacheSource `xml:"cacheSource"` + CacheFields *xlsxCacheFields `xml:"cacheFields"` + CacheHierarchies *xlsxCacheHierarchies `xml:"cacheHierarchies"` + Kpis *xlsxKpis `xml:"kpis"` + TupleCache *xlsxTupleCache `xml:"tupleCache"` + CalculatedItems *xlsxCalculatedItems `xml:"calculatedItems"` + CalculatedMembers *xlsxCalculatedMembers `xml:"calculatedMembers"` + Dimensions *xlsxDimensions `xml:"dimensions"` + MeasureGroups *xlsxMeasureGroups `xml:"measureGroups"` + Maps *xlsxMaps `xml:"maps"` + ExtLst *xlsxExtLst `xml:"extLst"` +} + +// xlsxCacheSource represents the description of data source whose data is +// stored in the pivot cache. The data source refers to the underlying rows or +// database records that provide the data for a PivotTable. You can create a +// PivotTable report from a SpreadsheetML table, an external database +// (including OLAP cubes), multiple SpreadsheetML worksheets, or another +// PivotTable. +type xlsxCacheSource struct { +} + +// xlsxCacheFields represents the collection of field definitions in the +// source data. +type xlsxCacheFields struct { + Count int `xml:"count,attr"` + CacheField []*xlsxCacheField `xml:"cacheField"` +} + +// xlsxCacheField represent a single field in the PivotCache. This definition +// contains information about the field, such as its source, data type, and +// location within a level or hierarchy. The sharedItems element stores +// additional information about the data in this field. If there are no shared +// items, then values are stored directly in the pivotCacheRecords part. +type xlsxCacheField struct { + Name string `xml:"name,attr"` + Caption string `xml:"caption,attr,omitempty"` + PropertyName string `xml:"propertyName,attr,omitempty"` + ServerField bool `xml:"serverField,attr,omitempty"` + UniqueList bool `xml:"uniqueList,attr,omitempty"` + NumFmtId string `xml:"numFmtId,attr,omitempty"` + Formula string `xml:"formula,attr,omitempty"` + SQLType int `xml:"sqlType,attr,omitempty"` + Hierarchy int `xml:"hierarchy,attr,omitempty"` + Level int `xml:"level,attr,omitempty"` + DatabaseField bool `xml:"databaseField,attr"` + MappingCount int `xml:"mappingCount,attr,omitempty"` + MemberPropertyField bool `xml:"memberPropertyField,attr,omitempty"` + SharedItems *xlsxSharedItems `xml:"sharedItems"` + FieldGroup *xlsxFieldGroup `xml:"fieldGroup"` + MpMap *xlsxX `xml:"map"` + ExtLst *xlsxExtLst `xml:"exrLst"` +} + +// xlsxSharedItems represents the collection of unique items for a field in +// the PivotCacheDefinition. The sharedItems complex type stores data type and +// formatting information about the data in a field. Items in the +// PivotCacheDefinition can be shared in order to reduce the redundancy of +// those values that are referenced in multiple places across all the +// PivotTable parts. +type xlsxSharedItems struct { + ContainsSemiMixedTypes bool `xml:"containsSemiMixedTypes,attr,omitempty"` + ContainsNonDate bool `xml:"containsNonDate,attr,omitempty"` + ContainsDate bool `xml:"containsDate,attr,omitempty"` + ContainsString bool `xml:"containsString,attr,omitempty"` + ContainsBlank bool `xml:"containsBlank,attr,omitempty"` + ContainsMixedTypes bool `xml:"containsMixedTypes,attr,omitempty"` + ContainsNumber bool `xml:"containsNumber,attr,omitempty"` + ContainsInteger bool `xml:"containsInteger,attr,omitempty"` + MinValue float64 `xml:"minValue,attr,omitempty"` + MaxValue float64 `xml:"maxValue,attr,omitempty"` + MinDate string `xml:"minDate,attr,omitempty"` + MaxDate string `xml:"maxDate,attr,omitempty"` + Count int `xml:"count,attr,omitempty"` + LongText bool `xml:"longText,attr,omitempty"` + M *xlsxMissing `xml:"m"` + N *xlsxNumber `xml:"n"` + B *xlsxBoolean `xml:"b"` + E *xlsxError `xml:"e"` + S *xlsxString `xml:"s"` + D *xlsxDateTime `xml:"d"` +} + +// xlsxMissing represents a value that was not specified. +type xlsxMissing struct { +} + +// xlsxNumber represents a numeric value in the PivotTable. +type xlsxNumber struct { + V float64 `xml:"v,attr"` + U bool `xml:"u,attr,omitempty"` + F bool `xml:"f,attr,omitempty"` + C string `xml:"c,attr,omitempty"` + Cp int `xml:"cp,attr,omitempty"` + In int `xml:"in,attr,omitempty"` + Bc string `xml:"bc,attr,omitempty"` + Fc string `xml:"fc,attr,omitempty"` + I bool `xml:"i,attr,omitempty"` + Un bool `xml:"un,attr,omitempty"` + St bool `xml:"st,attr,omitempty"` + B bool `xml:"b,attr,omitempty"` + Tpls *xlsxTuples `xml:"tpls"` + X *attrValInt `xml:"x"` +} + +// xlsxTuples represents members for the OLAP sheet data entry, also known as +// a tuple. +type xlsxTuples struct { +} + +// xlsxBoolean represents a boolean value for an item in the PivotTable. +type xlsxBoolean struct { +} + +// xlsxError represents an error value. The use of this item indicates that an +// error value is present in the PivotTable source. The error is recorded in +// the value attribute. +type xlsxError struct { +} + +// xlsxString represents a character value in a PivotTable. +type xlsxString struct { +} + +// xlsxDateTime represents a date-time value in the PivotTable. +type xlsxDateTime struct { +} + +// xlsxFieldGroup represents the collection of properties for a field group. +type xlsxFieldGroup struct { +} + +// xlsxCacheHierarchies represents the collection of OLAP hierarchies in the +// PivotCache. +type xlsxCacheHierarchies struct { +} + +// xlsxKpis represents the collection of Key Performance Indicators (KPIs) +// defined on the OLAP server and stored in the PivotCache. +type xlsxKpis struct { +} + +// xlsxTupleCache represents the cache of OLAP sheet data members, or tuples. +type xlsxTupleCache struct { +} + +// xlsxCalculatedItems represents the collection of calculated items. +type xlsxCalculatedItems struct { +} + +// xlsxCalculatedMembers represents the collection of calculated members in an +// OLAP PivotTable. +type xlsxCalculatedMembers struct { +} + +// xlsxDimensions represents the collection of PivotTable OLAP dimensions. +type xlsxDimensions struct { +} + +// xlsxMeasureGroups represents the collection of PivotTable OLAP measure +// groups. +type xlsxMeasureGroups struct { +} + +// xlsxMaps represents the PivotTable OLAP measure group - Dimension maps. +type xlsxMaps struct { +} -- cgit v1.2.1 From 3c327413d963f6ffa934a72beb483d1fedd25660 Mon Sep 17 00:00:00 2001 From: Ben Wells Date: Fri, 13 Sep 2019 11:38:44 +0100 Subject: Fix dependency on github.com/360EntSecGroup-Skylar/excelize v1 --- go.mod | 2 ++ go.sum | 5 +++++ sheet_test.go | 2 +- sheetpr_test.go | 2 +- sheetview_test.go | 2 +- 5 files changed, 10 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 9f36b59..892f306 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,8 @@ module github.com/360EntSecGroup-Skylar/excelize/v2 go 1.12 require ( + github.com/davecgh/go-spew v1.1.1 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 github.com/stretchr/testify v1.3.0 + golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a ) diff --git a/go.sum b/go.sum index 890277c..2d29d33 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -7,3 +9,6 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a h1:gHevYm0pO4QUbwy8Dmdr01R5r1BuKtfYqRqF0h/Cbh0= +golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/sheet_test.go b/sheet_test.go index 145e302..5179793 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -6,7 +6,7 @@ import ( "strings" "testing" - "github.com/360EntSecGroup-Skylar/excelize" + "github.com/360EntSecGroup-Skylar/excelize/v2" "github.com/mohae/deepcopy" "github.com/stretchr/testify/assert" diff --git a/sheetpr_test.go b/sheetpr_test.go index b9f9e3b..97a314c 100644 --- a/sheetpr_test.go +++ b/sheetpr_test.go @@ -7,7 +7,7 @@ import ( "github.com/mohae/deepcopy" "github.com/stretchr/testify/assert" - "github.com/360EntSecGroup-Skylar/excelize" + "github.com/360EntSecGroup-Skylar/excelize/v2" ) var _ = []excelize.SheetPrOption{ diff --git a/sheetview_test.go b/sheetview_test.go index 07f59ed..2e697b8 100644 --- a/sheetview_test.go +++ b/sheetview_test.go @@ -6,7 +6,7 @@ import ( "github.com/stretchr/testify/assert" - "github.com/360EntSecGroup-Skylar/excelize" + "github.com/360EntSecGroup-Skylar/excelize/v2" ) var _ = []excelize.SheetViewOption{ -- cgit v1.2.1 From 8922f659788187afa6d0a5d3248e999c2c1bb846 Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 16 Sep 2019 01:17:35 +0800 Subject: Combine functions: workBookRelsWriter, drawingRelsWriter into relsWriter; drawingRelsReader, workbookRelsReader, workSheetRelsReader into relsReader; addDrawingRelationships, addSheetRelationships into addRels --- README.md | 2 +- README_zh.md | 2 +- cell.go | 4 +- chart.go | 7 ++- comment.go | 8 +-- excelize.go | 33 +++++++++--- excelize.png | Bin 27196 -> 0 bytes excelize.svg | 1 + file.go | 9 ++-- picture.go | 136 +++++++++++-------------------------------------- shape.go | 4 +- sheet.go | 85 ++++++++----------------------- table.go | 4 +- xmlDrawing.go | 2 + xmlPivotCache.go | 4 +- xmlPivotTable.go | 150 +++++++++++++++++++++++++++++-------------------------- xmlWorkbook.go | 12 ++--- 17 files changed, 190 insertions(+), 273 deletions(-) delete mode 100644 excelize.png create mode 100644 excelize.svg diff --git a/README.md b/README.md index 7f9cf70..998a4c1 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -

Excelize logo

+

Excelize logo

Build Status diff --git a/README_zh.md b/README_zh.md index 6c2b190..d4cac66 100644 --- a/README_zh.md +++ b/README_zh.md @@ -1,4 +1,4 @@ -

Excelize logo

+

Excelize logo

Build Status diff --git a/cell.go b/cell.go index e897379..1da46aa 100644 --- a/cell.go +++ b/cell.go @@ -378,7 +378,9 @@ func (f *File) SetCellHyperLink(sheet, axis, link, linkType string) error { linkData = xlsxHyperlink{ Ref: axis, } - rID := f.addSheetRelationships(sheet, SourceRelationshipHyperLink, link, linkType) + sheetPath, _ := f.sheetMap[trimSheetName(sheet)] + sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetPath, "xl/worksheets/") + ".rels" + rID := f.addRels(sheetRels, SourceRelationshipHyperLink, link, linkType) linkData.RID = "rId" + strconv.Itoa(rID) case "Location": linkData = xlsxHyperlink{ diff --git a/chart.go b/chart.go index e1eb81f..db2df1e 100644 --- a/chart.go +++ b/chart.go @@ -727,7 +727,8 @@ func (f *File) AddChart(sheet, cell, format string) error { chartID := f.countCharts() + 1 drawingXML := "xl/drawings/drawing" + strconv.Itoa(drawingID) + ".xml" drawingID, drawingXML = f.prepareDrawing(xlsx, drawingID, sheet, drawingXML) - drawingRID := f.addDrawingRelationships(drawingID, SourceRelationshipChart, "../charts/chart"+strconv.Itoa(chartID)+".xml", "") + drawingRels := "xl/drawings/_rels/drawing" + strconv.Itoa(drawingID) + ".xml.rels" + drawingRID := f.addRels(drawingRels, SourceRelationshipChart, "../charts/chart"+strconv.Itoa(chartID)+".xml", "") err = f.addDrawingChart(sheet, drawingXML, cell, formatSet.Dimension.Width, formatSet.Dimension.Height, drawingRID, &formatSet.Format) if err != nil { return err @@ -761,7 +762,9 @@ func (f *File) prepareDrawing(xlsx *xlsxWorksheet, drawingID int, sheet, drawing drawingXML = strings.Replace(sheetRelationshipsDrawingXML, "..", "xl", -1) } else { // Add first picture for given sheet. - rID := f.addSheetRelationships(sheet, SourceRelationshipDrawingML, sheetRelationshipsDrawingXML, "") + sheetPath, _ := f.sheetMap[trimSheetName(sheet)] + sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetPath, "xl/worksheets/") + ".rels" + rID := f.addRels(sheetRels, SourceRelationshipDrawingML, sheetRelationshipsDrawingXML, "") f.addSheetDrawing(sheet, rID) } return drawingID, drawingXML diff --git a/comment.go b/comment.go index 97e0e9b..7f3b10d 100644 --- a/comment.go +++ b/comment.go @@ -60,7 +60,7 @@ func (f *File) GetComments() (comments map[string][]Comment) { // given worksheet index. func (f *File) getSheetComments(sheetID int) string { var rels = "xl/worksheets/_rels/sheet" + strconv.Itoa(sheetID) + ".xml.rels" - if sheetRels := f.workSheetRelsReader(rels); sheetRels != nil { + if sheetRels := f.relsReader(rels); sheetRels != nil { for _, v := range sheetRels.Relationships { if v.Type == SourceRelationshipComments { return v.Target @@ -98,8 +98,10 @@ func (f *File) AddComment(sheet, cell, format string) error { drawingVML = strings.Replace(sheetRelationshipsDrawingVML, "..", "xl", -1) } else { // Add first comment for given sheet. - rID := f.addSheetRelationships(sheet, SourceRelationshipDrawingVML, sheetRelationshipsDrawingVML, "") - f.addSheetRelationships(sheet, SourceRelationshipComments, sheetRelationshipsComments, "") + sheetPath, _ := f.sheetMap[trimSheetName(sheet)] + sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetPath, "xl/worksheets/") + ".rels" + rID := f.addRels(sheetRels, SourceRelationshipDrawingVML, sheetRelationshipsDrawingVML, "") + f.addRels(sheetRels, SourceRelationshipComments, sheetRelationshipsComments, "") f.addSheetLegacyDrawing(sheet, rID) } commentsXML := "xl/comments" + strconv.Itoa(commentID) + ".xml" diff --git a/excelize.go b/excelize.go index b734e57..4d46b94 100644 --- a/excelize.go +++ b/excelize.go @@ -31,7 +31,6 @@ type File struct { CalcChain *xlsxCalcChain Comments map[string]*xlsxComments ContentTypes *xlsxTypes - DrawingRels map[string]*xlsxWorkbookRels Drawings map[string]*xlsxWsDr Path string SharedStrings *xlsxSST @@ -42,8 +41,7 @@ type File struct { DecodeVMLDrawing map[string]*decodeVmlDrawing VMLDrawing map[string]*vmlDrawing WorkBook *xlsxWorkbook - WorkBookRels *xlsxWorkbookRels - WorkSheetRels map[string]*xlsxWorkbookRels + Relationships map[string]*xlsxRelationships XLSX map[string][]byte } @@ -93,13 +91,12 @@ func OpenReader(r io.Reader) (*File, error) { f := &File{ checked: make(map[string]bool), Comments: make(map[string]*xlsxComments), - DrawingRels: make(map[string]*xlsxWorkbookRels), Drawings: make(map[string]*xlsxWsDr), Sheet: make(map[string]*xlsxWorksheet), SheetCount: sheetCount, DecodeVMLDrawing: make(map[string]*decodeVmlDrawing), VMLDrawing: make(map[string]*vmlDrawing), - WorkSheetRels: make(map[string]*xlsxWorkbookRels), + Relationships: make(map[string]*xlsxRelationships), XLSX: file, } f.CalcChain = f.calcChainReader() @@ -176,6 +173,28 @@ func checkSheet(xlsx *xlsxWorksheet) { xlsx.SheetData = sheetData } +// addRels provides a function to add relationships by given XML path, +// relationship type, target and target mode. +func (f *File) addRels(relPath, relType, target, targetMode string) int { + rels := f.relsReader(relPath) + rID := 0 + if rels == nil { + rels = &xlsxRelationships{} + } + rID = len(rels.Relationships) + 1 + var ID bytes.Buffer + ID.WriteString("rId") + ID.WriteString(strconv.Itoa(rID)) + rels.Relationships = append(rels.Relationships, xlsxRelationship{ + ID: ID.String(), + Type: relType, + Target: target, + TargetMode: targetMode, + }) + f.Relationships[relPath] = rels + return rID +} + // replaceWorkSheetsRelationshipsNameSpaceBytes provides a function to replace // xl/worksheets/sheet%d.xml XML tags to self-closing for compatible Microsoft // Office Excel 2007. @@ -265,7 +284,7 @@ func (f *File) AddVBAProject(bin string) error { return errors.New("unsupported VBA project extension") } f.setContentTypePartVBAProjectExtensions() - wb := f.workbookRelsReader() + wb := f.relsReader("xl/_rels/workbook.xml.rels") var rID int var ok bool for _, rel := range wb.Relationships { @@ -280,7 +299,7 @@ func (f *File) AddVBAProject(bin string) error { } rID++ if !ok { - wb.Relationships = append(wb.Relationships, xlsxWorkbookRelation{ + wb.Relationships = append(wb.Relationships, xlsxRelationship{ ID: "rId" + strconv.Itoa(rID), Target: "vbaProject.bin", Type: SourceRelationshipVBAProject, diff --git a/excelize.png b/excelize.png deleted file mode 100644 index 8ba520e..0000000 Binary files a/excelize.png and /dev/null differ diff --git a/excelize.svg b/excelize.svg new file mode 100644 index 0000000..afa8828 --- /dev/null +++ b/excelize.svg @@ -0,0 +1 @@ +Excelize logo \ No newline at end of file diff --git a/file.go b/file.go index 46f1f62..2e0d27b 100644 --- a/file.go +++ b/file.go @@ -42,14 +42,13 @@ func NewFile() *File { f.CalcChain = f.calcChainReader() f.Comments = make(map[string]*xlsxComments) f.ContentTypes = f.contentTypesReader() - f.DrawingRels = make(map[string]*xlsxWorkbookRels) f.Drawings = make(map[string]*xlsxWsDr) f.Styles = f.stylesReader() f.DecodeVMLDrawing = make(map[string]*decodeVmlDrawing) f.VMLDrawing = make(map[string]*vmlDrawing) f.WorkBook = f.workbookReader() - f.WorkBookRels = f.workbookRelsReader() - f.WorkSheetRels = make(map[string]*xlsxWorkbookRels) + f.Relationships = make(map[string]*xlsxRelationships) + f.Relationships["xl/_rels/workbook.xml.rels"] = f.relsReader("xl/_rels/workbook.xml.rels") f.Sheet["xl/worksheets/sheet1.xml"], _ = f.workSheetReader("Sheet1") f.sheetMap["Sheet1"] = "xl/worksheets/sheet1.xml" f.Theme = f.themeReader() @@ -97,13 +96,11 @@ func (f *File) WriteToBuffer() (*bytes.Buffer, error) { f.calcChainWriter() f.commentsWriter() f.contentTypesWriter() - f.drawingRelsWriter() f.drawingsWriter() f.vmlDrawingWriter() f.workBookWriter() - f.workBookRelsWriter() f.workSheetWriter() - f.workSheetRelsWriter() + f.relsWriter() f.styleSheetWriter() for path, content := range f.XLSX { diff --git a/picture.go b/picture.go index a5904ff..518463a 100644 --- a/picture.go +++ b/picture.go @@ -155,14 +155,15 @@ func (f *File) AddPictureFromBytes(sheet, cell, format, name, extension string, drawingID := f.countDrawings() + 1 drawingXML := "xl/drawings/drawing" + strconv.Itoa(drawingID) + ".xml" drawingID, drawingXML = f.prepareDrawing(xlsx, drawingID, sheet, drawingXML) + drawingRels := "xl/drawings/_rels/drawing" + strconv.Itoa(drawingID) + ".xml.rels" mediaStr := ".." + strings.TrimPrefix(f.addMedia(file, ext), "xl") - drawingRID := f.addDrawingRelationships(drawingID, SourceRelationshipImage, mediaStr, hyperlinkType) + drawingRID := f.addRels(drawingRels, SourceRelationshipImage, mediaStr, hyperlinkType) // Add picture with hyperlink. if formatSet.Hyperlink != "" && formatSet.HyperlinkType != "" { if formatSet.HyperlinkType == "External" { hyperlinkType = formatSet.HyperlinkType } - drawingHyperlinkRID = f.addDrawingRelationships(drawingID, SourceRelationshipHyperLink, formatSet.Hyperlink, hyperlinkType) + drawingHyperlinkRID = f.addRels(drawingRels, SourceRelationshipHyperLink, formatSet.Hyperlink, hyperlinkType) } err = f.addDrawingPicture(sheet, drawingXML, cell, name, img.Width, img.Height, drawingRID, drawingHyperlinkRID, formatSet) if err != nil { @@ -172,37 +173,6 @@ func (f *File) AddPictureFromBytes(sheet, cell, format, name, extension string, return err } -// addSheetRelationships provides a function to add -// xl/worksheets/_rels/sheet%d.xml.rels by given worksheet name, relationship -// type and target. -func (f *File) addSheetRelationships(sheet, relType, target, targetMode string) int { - name, ok := f.sheetMap[trimSheetName(sheet)] - if !ok { - name = strings.ToLower(sheet) + ".xml" - } - var rels = "xl/worksheets/_rels/" + strings.TrimPrefix(name, "xl/worksheets/") + ".rels" - sheetRels := f.workSheetRelsReader(rels) - if sheetRels == nil { - sheetRels = &xlsxWorkbookRels{} - } - var rID = 1 - var ID bytes.Buffer - ID.WriteString("rId") - ID.WriteString(strconv.Itoa(rID)) - ID.Reset() - rID = len(sheetRels.Relationships) + 1 - ID.WriteString("rId") - ID.WriteString(strconv.Itoa(rID)) - sheetRels.Relationships = append(sheetRels.Relationships, xlsxWorkbookRelation{ - ID: ID.String(), - Type: relType, - Target: target, - TargetMode: targetMode, - }) - f.WorkSheetRels[rels] = sheetRels - return rID -} - // deleteSheetRelationships provides a function to delete relationships in // xl/worksheets/_rels/sheet%d.xml.rels by given worksheet name and // relationship index. @@ -212,16 +182,16 @@ func (f *File) deleteSheetRelationships(sheet, rID string) { name = strings.ToLower(sheet) + ".xml" } var rels = "xl/worksheets/_rels/" + strings.TrimPrefix(name, "xl/worksheets/") + ".rels" - sheetRels := f.workSheetRelsReader(rels) + sheetRels := f.relsReader(rels) if sheetRels == nil { - sheetRels = &xlsxWorkbookRels{} + sheetRels = &xlsxRelationships{} } for k, v := range sheetRels.Relationships { if v.ID == rID { sheetRels.Relationships = append(sheetRels.Relationships[:k], sheetRels.Relationships[k+1:]...) } } - f.WorkSheetRels[rels] = sheetRels + f.Relationships[rels] = sheetRels } // addSheetLegacyDrawing provides a function to add legacy drawing element to @@ -325,33 +295,6 @@ func (f *File) addDrawingPicture(sheet, drawingXML, cell, file string, width, he return err } -// addDrawingRelationships provides a function to add image part relationships -// in the file xl/drawings/_rels/drawing%d.xml.rels by given drawing index, -// relationship type and target. -func (f *File) addDrawingRelationships(index int, relType, target, targetMode string) int { - var rels = "xl/drawings/_rels/drawing" + strconv.Itoa(index) + ".xml.rels" - var rID = 1 - var ID bytes.Buffer - ID.WriteString("rId") - ID.WriteString(strconv.Itoa(rID)) - drawingRels := f.drawingRelsReader(rels) - if drawingRels == nil { - drawingRels = &xlsxWorkbookRels{} - } - ID.Reset() - rID = len(drawingRels.Relationships) + 1 - ID.WriteString("rId") - ID.WriteString(strconv.Itoa(rID)) - drawingRels.Relationships = append(drawingRels.Relationships, xlsxWorkbookRelation{ - ID: ID.String(), - Type: relType, - Target: target, - TargetMode: targetMode, - }) - f.DrawingRels[rels] = drawingRels - return rID -} - // countMedia provides a function to get media files count storage in the // folder xl/media/image. func (f *File) countMedia() int { @@ -429,16 +372,20 @@ func (f *File) addContentTypePart(index int, contentType string) { "drawings": f.setContentTypePartImageExtensions, } partNames := map[string]string{ - "chart": "/xl/charts/chart" + strconv.Itoa(index) + ".xml", - "comments": "/xl/comments" + strconv.Itoa(index) + ".xml", - "drawings": "/xl/drawings/drawing" + strconv.Itoa(index) + ".xml", - "table": "/xl/tables/table" + strconv.Itoa(index) + ".xml", + "chart": "/xl/charts/chart" + strconv.Itoa(index) + ".xml", + "comments": "/xl/comments" + strconv.Itoa(index) + ".xml", + "drawings": "/xl/drawings/drawing" + strconv.Itoa(index) + ".xml", + "table": "/xl/tables/table" + strconv.Itoa(index) + ".xml", + "pivotTable": "/xl/pivotTables/pivotTable" + strconv.Itoa(index) + ".xml", + "pivotCache": "/xl/pivotCache/pivotCacheDefinition" + strconv.Itoa(index) + ".xml", } contentTypes := map[string]string{ - "chart": "application/vnd.openxmlformats-officedocument.drawingml.chart+xml", - "comments": "application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml", - "drawings": "application/vnd.openxmlformats-officedocument.drawing+xml", - "table": "application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml", + "chart": "application/vnd.openxmlformats-officedocument.drawingml.chart+xml", + "comments": "application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml", + "drawings": "application/vnd.openxmlformats-officedocument.drawing+xml", + "table": "application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml", + "pivotTable": "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTable+xml", + "pivotCache": "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheDefinition+xml", } s, ok := setContentType[contentType] if ok { @@ -465,9 +412,9 @@ func (f *File) getSheetRelationshipsTargetByID(sheet, rID string) string { name = strings.ToLower(sheet) + ".xml" } var rels = "xl/worksheets/_rels/" + strings.TrimPrefix(name, "xl/worksheets/") + ".rels" - sheetRels := f.workSheetRelsReader(rels) + sheetRels := f.relsReader(rels) if sheetRels == nil { - sheetRels = &xlsxWorkbookRels{} + sheetRels = &xlsxRelationships{} } for _, v := range sheetRels.Relationships { if v.ID == rID { @@ -529,12 +476,12 @@ func (f *File) getPicture(row, col int, drawingXML, drawingRelationships string) 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, + xlsxRelationship := f.getDrawingRelationships(drawingRelationships, anchor.Pic.BlipFill.Blip.Embed) - _, ok := supportImageTypes[filepath.Ext(xlsxWorkbookRelation.Target)] + _, ok := supportImageTypes[filepath.Ext(xlsxRelationship.Target)] if ok { - return filepath.Base(xlsxWorkbookRelation.Target), - []byte(f.XLSX[strings.Replace(xlsxWorkbookRelation.Target, + return filepath.Base(xlsxRelationship.Target), + []byte(f.XLSX[strings.Replace(xlsxRelationship.Target, "..", "xl", -1)]), nil } } @@ -548,10 +495,10 @@ func (f *File) getPicture(row, col int, drawingXML, drawingRelationships string) _ = xml.Unmarshal([]byte(""+anchor.Content+""), &decodeTwoCellAnchor) if decodeTwoCellAnchor.From != nil && decodeTwoCellAnchor.Pic != nil { if decodeTwoCellAnchor.From.Col == col && decodeTwoCellAnchor.From.Row == row { - xlsxWorkbookRelation := f.getDrawingRelationships(drawingRelationships, decodeTwoCellAnchor.Pic.BlipFill.Blip.Embed) - _, ok := supportImageTypes[filepath.Ext(xlsxWorkbookRelation.Target)] + xlsxRelationship := f.getDrawingRelationships(drawingRelationships, decodeTwoCellAnchor.Pic.BlipFill.Blip.Embed) + _, ok := supportImageTypes[filepath.Ext(xlsxRelationship.Target)] if ok { - return filepath.Base(xlsxWorkbookRelation.Target), []byte(f.XLSX[strings.Replace(xlsxWorkbookRelation.Target, "..", "xl", -1)]), nil + return filepath.Base(xlsxRelationship.Target), []byte(f.XLSX[strings.Replace(xlsxRelationship.Target, "..", "xl", -1)]), nil } } } @@ -562,8 +509,8 @@ func (f *File) getPicture(row, col int, drawingXML, drawingRelationships string) // getDrawingRelationships provides a function to get drawing relationships // from xl/drawings/_rels/drawing%s.xml.rels by given file name and // relationship ID. -func (f *File) getDrawingRelationships(rels, rID string) *xlsxWorkbookRelation { - if drawingRels := f.drawingRelsReader(rels); drawingRels != nil { +func (f *File) getDrawingRelationships(rels, rID string) *xlsxRelationship { + if drawingRels := f.relsReader(rels); drawingRels != nil { for _, v := range drawingRels.Relationships { if v.ID == rID { return &v @@ -573,31 +520,6 @@ func (f *File) getDrawingRelationships(rels, rID string) *xlsxWorkbookRelation { return nil } -// drawingRelsReader provides a function to get the pointer to the structure -// after deserialization of xl/drawings/_rels/drawing%d.xml.rels. -func (f *File) drawingRelsReader(rel string) *xlsxWorkbookRels { - if f.DrawingRels[rel] == nil { - _, ok := f.XLSX[rel] - if ok { - d := xlsxWorkbookRels{} - _ = xml.Unmarshal(namespaceStrictToTransitional(f.readXML(rel)), &d) - f.DrawingRels[rel] = &d - } - } - return f.DrawingRels[rel] -} - -// drawingRelsWriter provides a function to save -// xl/drawings/_rels/drawing%d.xml.rels after serialize structure. -func (f *File) drawingRelsWriter() { - for path, d := range f.DrawingRels { - if d != nil { - v, _ := xml.Marshal(d) - f.saveFileList(path, v) - } - } -} - // drawingsWriter provides a function to save xl/drawings/drawing%d.xml after // serialize structure. func (f *File) drawingsWriter() { diff --git a/shape.go b/shape.go index 8d95849..e6a2ff3 100644 --- a/shape.go +++ b/shape.go @@ -275,7 +275,9 @@ func (f *File) AddShape(sheet, cell, format string) error { drawingXML = strings.Replace(sheetRelationshipsDrawingXML, "..", "xl", -1) } else { // Add first shape for given sheet. - rID := f.addSheetRelationships(sheet, SourceRelationshipDrawingML, sheetRelationshipsDrawingXML, "") + name, _ := f.sheetMap[trimSheetName(sheet)] + sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(name, "xl/worksheets/") + ".rels" + rID := f.addRels(sheetRels, SourceRelationshipDrawingML, sheetRelationshipsDrawingXML, "") f.addSheetDrawing(sheet, rID) } err = f.addDrawingShape(sheet, drawingXML, cell, formatSet) diff --git a/sheet.go b/sheet.go index ed6d888..951baf9 100644 --- a/sheet.go +++ b/sheet.go @@ -52,7 +52,7 @@ func (f *File) NewSheet(name string) int { // Create new sheet /xl/worksheets/sheet%d.xml f.setSheet(sheetID, name) // Update xl/_rels/workbook.xml.rels - rID := f.addXlsxWorkbookRels(sheetID) + 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 @@ -163,50 +163,18 @@ func (f *File) setWorkbook(name string, sheetID, rid int) { }) } -// workbookRelsReader provides a function to read and unmarshal workbook -// relationships of XLSX file. -func (f *File) workbookRelsReader() *xlsxWorkbookRels { - if f.WorkBookRels == nil { - var content xlsxWorkbookRels - _ = xml.Unmarshal(namespaceStrictToTransitional(f.readXML("xl/_rels/workbook.xml.rels")), &content) - f.WorkBookRels = &content - } - return f.WorkBookRels -} - -// workBookRelsWriter provides a function to save xl/_rels/workbook.xml.rels after +// relsWriter provides a function to save relationships after // serialize structure. -func (f *File) workBookRelsWriter() { - if f.WorkBookRels != nil { - output, _ := xml.Marshal(f.WorkBookRels) - f.saveFileList("xl/_rels/workbook.xml.rels", output) - } -} - -// addXlsxWorkbookRels update workbook relationships property of XLSX. -func (f *File) addXlsxWorkbookRels(sheet int) int { - content := f.workbookRelsReader() - rID := 0 - for _, v := range content.Relationships { - t, _ := strconv.Atoi(strings.TrimPrefix(v.ID, "rId")) - if t > rID { - rID = t +func (f *File) relsWriter() { + for path, rel := range f.Relationships { + if rel != nil { + output, _ := xml.Marshal(rel) + if strings.HasPrefix(path, "xl/worksheets/sheet/rels/sheet") { + output = replaceWorkSheetsRelationshipsNameSpaceBytes(output) + } + f.saveFileList(path, replaceRelationshipsBytes(output)) } } - rID++ - ID := bytes.Buffer{} - ID.WriteString("rId") - ID.WriteString(strconv.Itoa(rID)) - target := bytes.Buffer{} - target.WriteString("worksheets/sheet") - target.WriteString(strconv.Itoa(sheet)) - target.WriteString(".xml") - content.Relationships = append(content.Relationships, xlsxWorkbookRelation{ - ID: ID.String(), - Target: target.String(), - Type: SourceRelationshipWorkSheet, - }) - return rID } // setAppXML update docProps/app.xml file of XML. @@ -365,7 +333,7 @@ func (f *File) GetSheetMap() map[int]string { // of XLSX. func (f *File) getSheetMap() map[string]string { content := f.workbookReader() - rels := f.workbookRelsReader() + rels := f.relsReader("xl/_rels/workbook.xml.rels") maps := map[string]string{} for _, v := range content.Sheets.Sheet { for _, rel := range rels.Relationships { @@ -396,7 +364,9 @@ func (f *File) SetSheetBackground(sheet, picture string) error { } file, _ := ioutil.ReadFile(picture) name := f.addMedia(file, ext) - rID := f.addSheetRelationships(sheet, SourceRelationshipImage, strings.Replace(name, "xl", "..", 1), "") + sheetPath, _ := f.sheetMap[trimSheetName(sheet)] + sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetPath, "xl/worksheets/") + ".rels" + rID := f.addRels(sheetRels, SourceRelationshipImage, strings.Replace(name, "xl", "..", 1), "") f.addSheetPicture(sheet, rID) f.setContentTypePartImageExtensions() return err @@ -413,7 +383,7 @@ func (f *File) DeleteSheet(name string) { } sheetName := trimSheetName(name) wb := f.workbookReader() - wbRels := f.workbookRelsReader() + wbRels := f.relsReader("xl/_rels/workbook.xml.rels") for idx, sheet := range wb.Sheets.Sheet { if sheet.Name == sheetName { wb.Sheets.Sheet = append(wb.Sheets.Sheet[:idx], wb.Sheets.Sheet[idx+1:]...) @@ -443,7 +413,7 @@ func (f *File) DeleteSheet(name string) { // relationships by given relationships ID in the file // xl/_rels/workbook.xml.rels. func (f *File) deleteSheetFromWorkbookRels(rID string) string { - content := f.workbookRelsReader() + content := f.relsReader("xl/_rels/workbook.xml.rels") for k, v := range content.Relationships { if v.ID == rID { content.Relationships = append(content.Relationships[:k], content.Relationships[k+1:]...) @@ -1387,29 +1357,18 @@ func (f *File) UngroupSheets() error { return nil } -// workSheetRelsReader provides a function to get the pointer to the structure +// relsReader provides a function to get the pointer to the structure // after deserialization of xl/worksheets/_rels/sheet%d.xml.rels. -func (f *File) workSheetRelsReader(path string) *xlsxWorkbookRels { - if f.WorkSheetRels[path] == nil { +func (f *File) relsReader(path string) *xlsxRelationships { + if f.Relationships[path] == nil { _, ok := f.XLSX[path] if ok { - c := xlsxWorkbookRels{} + c := xlsxRelationships{} _ = xml.Unmarshal(namespaceStrictToTransitional(f.readXML(path)), &c) - f.WorkSheetRels[path] = &c - } - } - return f.WorkSheetRels[path] -} - -// workSheetRelsWriter provides a function to save -// xl/worksheets/_rels/sheet%d.xml.rels after serialize structure. -func (f *File) workSheetRelsWriter() { - for p, r := range f.WorkSheetRels { - if r != nil { - v, _ := xml.Marshal(r) - f.saveFileList(p, v) + f.Relationships[path] = &c } } + return f.Relationships[path] } // fillSheetData ensures there are enough rows, and columns in the chosen diff --git a/table.go b/table.go index 45a1622..d26f8fd 100644 --- a/table.go +++ b/table.go @@ -77,7 +77,9 @@ func (f *File) AddTable(sheet, hcell, vcell, format string) error { 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, "") + sheetPath, _ := f.sheetMap[trimSheetName(sheet)] + sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetPath, "xl/worksheets/") + ".rels" + rID := f.addRels(sheetRels, SourceRelationshipTable, sheetRelationshipsTableXML, "") f.addSheetTable(sheet, rID) err = f.addTable(sheet, tableXML, hcol, hrow, vcol, vrow, tableID, formatSet) if err != nil { diff --git a/xmlDrawing.go b/xmlDrawing.go index ade6261..4338c5e 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -22,6 +22,8 @@ const ( SourceRelationshipDrawingVML = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing" SourceRelationshipHyperLink = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink" SourceRelationshipWorkSheet = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" + SourceRelationshipPivotTable = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotTable" + SourceRelationshipPivotCache = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheDefinition" SourceRelationshipVBAProject = "http://schemas.microsoft.com/office/2006/relationships/vbaProject" SourceRelationshipChart201506 = "http://schemas.microsoft.com/office/drawing/2015/06/chart" SourceRelationshipChart20070802 = "http://schemas.microsoft.com/office/drawing/2007/8/2/chart" diff --git a/xmlPivotCache.go b/xmlPivotCache.go index 9e07931..0c00832 100644 --- a/xmlPivotCache.go +++ b/xmlPivotCache.go @@ -2,11 +2,11 @@ package excelize import "encoding/xml" -// pivotCacheDefinition represents the pivotCacheDefinition part. This part +// xlsxPivotCacheDefinition represents the pivotCacheDefinition part. This part // defines each field in the source data, including the name, the string // resources of the instance data (for shared items), and information about // the type of data that appears in the field. -type xmlPivotCacheDefinition struct { +type xlsxPivotCacheDefinition struct { XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main pivotCacheDefinition"` RID string `xml:"http://schemas.openxmlformats.org/officeDocument/2006/relationships id,attr,omitempty"` Invalid bool `xml:"invalid,attr,omitempty"` diff --git a/xmlPivotTable.go b/xmlPivotTable.go index 16c469f..6f2a8e7 100644 --- a/xmlPivotTable.go +++ b/xmlPivotTable.go @@ -15,78 +15,84 @@ import "encoding/xml" // non-null PivotTables. There exists one pivotTableDefinition for each // PivotTableDefinition part type xlsxPivotTableDefinition struct { - XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main pivotTableDefinition"` - Name string `xml:"name,attr"` - CacheID int `xml:"cacheId,attr"` - DataOnRows bool `xml:"dataOnRows,attr"` - DataPosition int `xml:"dataPosition,attr"` - DataCaption string `xml:"dataCaption,attr"` - GrandTotalCaption string `xml:"grandTotalCaption,attr"` - ErrorCaption string `xml:"errorCaption,attr"` - ShowError bool `xml:"showError,attr"` - MissingCaption string `xml:"missingCaption,attr"` - ShowMissing bool `xml:"showMissing,attr"` - PageStyle string `xml:"pageStyle,attr"` - PivotTableStyle string `xml:"pivotTableStyle,attr"` - VacatedStyle string `xml:"vacatedStyle,attr"` - Tag string `xml:"tag,attr"` - UpdatedVersion int `xml:"updatedVersion,attr"` - MinRefreshableVersion int `xml:"minRefreshableVersion,attr"` - AsteriskTotals bool `xml:"asteriskTotals,attr"` - ShowItems bool `xml:"showItems,attr"` - EditData bool `xml:"editData,attr"` - DisableFieldList bool `xml:"disableFieldList,attr"` - ShowCalcMbrs bool `xml:"showCalcMbrs,attr"` - VisualTotals bool `xml:"visualTotals,attr"` - ShowMultipleLabel bool `xml:"showMultipleLabel,attr"` - ShowDataDropDown bool `xml:"showDataDropDown,attr"` - ShowDrill bool `xml:"showDrill,attr"` - PrintDrill bool `xml:"printDrill,attr"` - ShowMemberPropertyTips bool `xml:"showMemberPropertyTips,attr"` - ShowDataTips bool `xml:"showDataTips,attr"` - EnableWizard bool `xml:"enableWizard,attr"` - EnableDrill bool `xml:"enableDrill,attr"` - EnableFieldProperties bool `xml:"enableFieldProperties,attr"` - PreserveFormatting bool `xml:"preserveFormatting,attr"` - UseAutoFormatting bool `xml:"useAutoFormatting,attr"` - PageWrap int `xml:"pageWrap,attr"` - PageOverThenDown bool `xml:"pageOverThenDown,attr"` - SubtotalHiddenItems bool `xml:"subtotalHiddenItems,attr"` - RowGrandTotals bool `xml:"rowGrandTotals,attr"` - ColGrandTotals bool `xml:"colGrandTotals,attr"` - FieldPrintTitles bool `xml:"fieldPrintTitles,attr"` - ItemPrintTitles bool `xml:"itemPrintTitles,attr"` - MergeItem bool `xml:"mergeItem,attr"` - ShowDropZones bool `xml:"showDropZones,attr"` - CreatedVersion int `xml:"createdVersion,attr"` - Indent int `xml:"indent,attr"` - ShowEmptyRow bool `xml:"showEmptyRow,attr"` - ShowEmptyCol bool `xml:"showEmptyCol,attr"` - ShowHeaders bool `xml:"showHeaders,attr"` - Compact bool `xml:"compact,attr"` - Outline bool `xml:"outline,attr"` - OutlineData bool `xml:"outlineData,attr"` - CompactData bool `xml:"compactData,attr"` - Published bool `xml:"published,attr"` - GridDropZones bool `xml:"gridDropZones,attr"` - Immersive bool `xml:"immersive,attr"` - MultipleFieldFilters bool `xml:"multipleFieldFilters,attr"` - ChartFormat int `xml:"chartFormat,attr"` - RowHeaderCaption string `xml:"rowHeaderCaption,attr"` - ColHeaderCaption string `xml:"colHeaderCaption,attr"` - FieldListSortAscending bool `xml:"fieldListSortAscending,attr"` - MdxSubqueries bool `xml:"mdxSubqueries,attr"` - CustomListSort bool `xml:"customListSort,attr"` - Location *xlsxLocation `xml:"location"` - PivotFields *xlsxPivotFields `xml:"pivotFields"` - RowFields *xlsxRowFields `xml:"rowFields"` - RowItems *xlsxRowItems `xml:"rowItems"` - ColFields *xlsxColFields `xml:"colFields"` - ColItems *xlsxColItems `xml:"colItems"` - PageFields *xlsxPageFields `xml:"pageFields"` - DataFields *xlsxDataFields `xml:"dataFields"` - ConditionalFormats *xlsxConditionalFormats `xml:"conditionalFormats"` - PivotTableStyleInfo *xlsxPivotTableStyleInfo `xml:"pivotTableStyleInfo"` + XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main pivotTableDefinition"` + Name string `xml:"name,attr"` + CacheID int `xml:"cacheId,attr"` + ApplyNumberFormats bool `xml:"applyNumberFormats,attr,omitempty"` + ApplyBorderFormats bool `xml:"applyBorderFormats,attr,omitempty"` + ApplyFontFormats bool `xml:"applyFontFormats,attr,omitempty"` + ApplyPatternFormats bool `xml:"applyPatternFormats,attr,omitempty"` + ApplyAlignmentFormats bool `xml:"applyAlignmentFormats,attr,omitempty"` + ApplyWidthHeightFormats bool `xml:"applyWidthHeightFormats,attr,omitempty"` + DataOnRows bool `xml:"dataOnRows,attr,omitempty"` + DataPosition int `xml:"dataPosition,attr,omitempty"` + DataCaption string `xml:"dataCaption,attr"` + GrandTotalCaption string `xml:"grandTotalCaption,attr,omitempty"` + ErrorCaption string `xml:"errorCaption,attr,omitempty"` + ShowError bool `xml:"showError,attr,omitempty"` + MissingCaption string `xml:"missingCaption,attr,omitempty"` + ShowMissing bool `xml:"showMissing,attr,omitempty"` + PageStyle string `xml:"pageStyle,attr,omitempty"` + PivotTableStyle string `xml:"pivotTableStyle,attr,omitempty"` + VacatedStyle string `xml:"vacatedStyle,attr,omitempty"` + Tag string `xml:"tag,attr,omitempty"` + UpdatedVersion int `xml:"updatedVersion,attr"` + MinRefreshableVersion int `xml:"minRefreshableVersion,attr"` + AsteriskTotals bool `xml:"asteriskTotals,attr,omitempty"` + ShowItems bool `xml:"showItems,attr,omitempty"` + EditData bool `xml:"editData,attr,omitempty"` + DisableFieldList bool `xml:"disableFieldList,attr,omitempty"` + ShowCalcMbrs bool `xml:"showCalcMbrs,attr,omitempty"` + VisualTotals bool `xml:"visualTotals,attr,omitempty"` + ShowMultipleLabel bool `xml:"showMultipleLabel,attr,omitempty"` + ShowDataDropDown bool `xml:"showDataDropDown,attr,omitempty"` + ShowDrill bool `xml:"showDrill,attr,omitempty"` + PrintDrill bool `xml:"printDrill,attr,omitempty"` + ShowMemberPropertyTips bool `xml:"showMemberPropertyTips,attr,omitempty"` + ShowDataTips bool `xml:"showDataTips,attr,omitempty"` + EnableWizard bool `xml:"enableWizard,attr,omitempty"` + EnableDrill bool `xml:"enableDrill,attr,omitempty"` + EnableFieldProperties bool `xml:"enableFieldProperties,attr,omitempty"` + PreserveFormatting bool `xml:"preserveFormatting,attr,omitempty"` + UseAutoFormatting bool `xml:"useAutoFormatting,attr"` + PageWrap int `xml:"pageWrap,attr,omitempty"` + PageOverThenDown bool `xml:"pageOverThenDown,attr,omitempty"` + SubtotalHiddenItems bool `xml:"subtotalHiddenItems,attr,omitempty"` + RowGrandTotals bool `xml:"rowGrandTotals,attr,omitempty"` + ColGrandTotals bool `xml:"colGrandTotals,attr,omitempty"` + FieldPrintTitles bool `xml:"fieldPrintTitles,attr,omitempty"` + ItemPrintTitles bool `xml:"itemPrintTitles,attr"` + MergeItem bool `xml:"mergeItem,attr,omitempty"` + ShowDropZones bool `xml:"showDropZones,attr,omitempty"` + CreatedVersion int `xml:"createdVersion,attr"` + Indent int `xml:"indent,attr,omitempty"` + ShowEmptyRow bool `xml:"showEmptyRow,attr,omitempty"` + ShowEmptyCol bool `xml:"showEmptyCol,attr,omitempty"` + ShowHeaders bool `xml:"showHeaders,attr,omitempty"` + Compact bool `xml:"compact,attr,omitempty"` + Outline bool `xml:"outline,attr,omitempty"` + OutlineData bool `xml:"outlineData,attr,omitempty"` + CompactData bool `xml:"compactData,attr,omitempty"` + Published bool `xml:"published,attr,omitempty"` + GridDropZones bool `xml:"gridDropZones,attr"` + Immersive bool `xml:"immersive,attr,omitempty"` + MultipleFieldFilters bool `xml:"multipleFieldFilters,attr,omitempty"` + ChartFormat int `xml:"chartFormat,attr,omitempty"` + RowHeaderCaption string `xml:"rowHeaderCaption,attr,omitempty"` + ColHeaderCaption string `xml:"colHeaderCaption,attr,omitempty"` + FieldListSortAscending bool `xml:"fieldListSortAscending,attr,omitempty"` + MdxSubqueries bool `xml:"mdxSubqueries,attr,omitempty"` + CustomListSort bool `xml:"customListSort,attr,omitempty"` + Location *xlsxLocation `xml:"location"` + PivotFields *xlsxPivotFields `xml:"pivotFields"` + RowFields *xlsxRowFields `xml:"rowFields"` + RowItems *xlsxRowItems `xml:"rowItems"` + ColFields *xlsxColFields `xml:"colFields"` + ColItems *xlsxColItems `xml:"colItems"` + PageFields *xlsxPageFields `xml:"pageFields"` + DataFields *xlsxDataFields `xml:"dataFields"` + ConditionalFormats *xlsxConditionalFormats `xml:"conditionalFormats"` + PivotTableStyleInfo *xlsxPivotTableStyleInfo `xml:"pivotTableStyleInfo"` } // xlsxLocation represents location information for the PivotTable. diff --git a/xmlWorkbook.go b/xmlWorkbook.go index 8150e29..765563b 100644 --- a/xmlWorkbook.go +++ b/xmlWorkbook.go @@ -11,14 +11,14 @@ package excelize import "encoding/xml" -// xmlxWorkbookRels contains xmlxWorkbookRelations which maps sheet id and sheet XML. -type xlsxWorkbookRels struct { - XMLName xml.Name `xml:"http://schemas.openxmlformats.org/package/2006/relationships Relationships"` - Relationships []xlsxWorkbookRelation `xml:"Relationship"` +// xlsxRelationships describe references from parts to other internal resources in the package or to external resources. +type xlsxRelationships struct { + XMLName xml.Name `xml:"http://schemas.openxmlformats.org/package/2006/relationships Relationships"` + Relationships []xlsxRelationship `xml:"Relationship"` } -// xmlxWorkbookRelation maps sheet id and xl/worksheets/_rels/sheet%d.xml.rels -type xlsxWorkbookRelation struct { +// xlsxRelationship contains relations which maps id and XML. +type xlsxRelationship struct { ID string `xml:"Id,attr"` Target string `xml:",attr"` Type string `xml:",attr"` -- cgit v1.2.1 From eef232f09ecd41b1f8fc199906ce0be64865802e Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 18 Sep 2019 00:47:31 +0800 Subject: Fix #483, adjust the order of fields in the structure --- xmlWorksheet.go | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 09dec5e..7e8cfde 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -209,16 +209,18 @@ type xlsxPane struct { // properties. type xlsxSheetPr struct { XMLName xml.Name `xml:"sheetPr"` - CodeName string `xml:"codeName,attr,omitempty"` - EnableFormatConditionsCalculation *bool `xml:"enableFormatConditionsCalculation,attr"` - FilterMode bool `xml:"filterMode,attr,omitempty"` - Published *bool `xml:"published,attr"` SyncHorizontal bool `xml:"syncHorizontal,attr,omitempty"` SyncVertical bool `xml:"syncVertical,attr,omitempty"` + SyncRef string `xml:"syncRef,attr,omitempty"` + TransitionEvaluation bool `xml:"transitionEvaluation,attr,omitempty"` + Published *bool `xml:"published,attr"` + CodeName string `xml:"codeName,attr,omitempty"` + FilterMode bool `xml:"filterMode,attr,omitempty"` + EnableFormatConditionsCalculation *bool `xml:"enableFormatConditionsCalculation,attr"` TransitionEntry bool `xml:"transitionEntry,attr,omitempty"` TabColor *xlsxTabColor `xml:"tabColor,omitempty"` - PageSetUpPr *xlsxPageSetUpPr `xml:"pageSetUpPr,omitempty"` OutlinePr *xlsxOutlinePr `xml:"outlinePr,omitempty"` + PageSetUpPr *xlsxPageSetUpPr `xml:"pageSetUpPr,omitempty"` } // xlsxOutlinePr maps to the outlinePr element -- cgit v1.2.1 From 3c636da46029b1c578871dfab3e1692e989af9f7 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 20 Sep 2019 00:20:30 +0800 Subject: Resolve #40, init pivot table support --- pivotTable.go | 434 +++++++++++++++++++++++++++++++++++++++++++++++++++++ pivotTable_test.go | 164 ++++++++++++++++++++ xmlPivotCache.go | 34 ++++- xmlPivotTable.go | 141 +++++++++-------- 4 files changed, 696 insertions(+), 77 deletions(-) create mode 100644 pivotTable.go create mode 100644 pivotTable_test.go diff --git a/pivotTable.go b/pivotTable.go new file mode 100644 index 0000000..881d774 --- /dev/null +++ b/pivotTable.go @@ -0,0 +1,434 @@ +// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// this source code is governed by a BSD-style license that can be found in +// the LICENSE file. +// +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.10 or later. + +package excelize + +import ( + "encoding/xml" + "errors" + "fmt" + "strconv" + "strings" +) + +// PivotTableOption directly maps the format settings of the pivot table. +type PivotTableOption struct { + DataRange string + PivotTableRange string + Rows []string + Columns []string + Data []string + Page []string +} + +// AddPivotTable provides the method to add pivot table by given pivot table +// options. For example, create a pivot table on the Sheet1!$G$2:$M$34 area +// with the region Sheet1!$A$1:$E$31 as the data source, summarize by sum for +// sales: +// +// package main +// +// import ( +// "fmt" +// "math/rand" +// +// "github.com/360EntSecGroup-Skylar/excelize" +// ) +// +// func main() { +// f := excelize.NewFile() +// // Create some data in a sheet +// month := []string{"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"} +// year := []int{2017, 2018, 2019} +// types := []string{"Meat", "Dairy", "Beverages", "Produce"} +// region := []string{"East", "West", "North", "South"} +// f.SetSheetRow("Sheet1", "A1", &[]string{"Month", "Year", "Type", "Sales", "Region"}) +// for i := 0; i < 30; i++ { +// f.SetCellValue("Sheet1", fmt.Sprintf("A%d", i+2), month[rand.Intn(12)]) +// f.SetCellValue("Sheet1", fmt.Sprintf("B%d", i+2), year[rand.Intn(3)]) +// f.SetCellValue("Sheet1", fmt.Sprintf("C%d", i+2), types[rand.Intn(4)]) +// f.SetCellValue("Sheet1", fmt.Sprintf("D%d", i+2), rand.Intn(5000)) +// f.SetCellValue("Sheet1", fmt.Sprintf("E%d", i+2), region[rand.Intn(4)]) +// } +// err := f.AddPivotTable(&excelize.PivotTableOption{ +// DataRange: "Sheet1!$A$1:$E$31", +// PivotTableRange: "Sheet1!$G$2:$M$34", +// Rows: []string{"Month", "Year"}, +// Columns: []string{"Type"}, +// Data: []string{"Sales"}, +// }) +// if err != nil { +// fmt.Println(err) +// } +// err = f.SaveAs("Book1.xlsx") +// if err != nil { +// fmt.Println(err) +// } +// } +// +func (f *File) AddPivotTable(opt *PivotTableOption) error { + // parameter validation + dataSheet, pivotTableSheetPath, err := f.parseFormatPivotTableSet(opt) + if err != nil { + return err + } + + pivotTableID := f.countPivotTables() + 1 + pivotCacheID := f.countPivotCache() + 1 + + sheetRelationshipsPivotTableXML := "../pivotTables/pivotTable" + strconv.Itoa(pivotTableID) + ".xml" + pivotTableXML := strings.Replace(sheetRelationshipsPivotTableXML, "..", "xl", -1) + pivotCacheXML := "xl/pivotCache/pivotCacheDefinition" + strconv.Itoa(pivotCacheID) + ".xml" + err = f.addPivotCache(pivotCacheID, pivotCacheXML, opt, dataSheet) + if err != nil { + return err + } + + // workbook pivot cache + workBookPivotCacheRID := f.addRels("xl/_rels/workbook.xml.rels", SourceRelationshipPivotCache, fmt.Sprintf("pivotCache/pivotCacheDefinition%d.xml", pivotCacheID), "") + cacheID := f.addWorkbookPivotCache(workBookPivotCacheRID) + + pivotCacheRels := "xl/pivotTables/_rels/pivotTable" + strconv.Itoa(pivotTableID) + ".xml.rels" + // rId not used + _ = f.addRels(pivotCacheRels, SourceRelationshipPivotCache, fmt.Sprintf("../pivotCache/pivotCacheDefinition%d.xml", pivotCacheID), "") + err = f.addPivotTable(cacheID, pivotTableID, pivotTableXML, opt) + if err != nil { + return err + } + pivotTableSheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(pivotTableSheetPath, "xl/worksheets/") + ".rels" + f.addRels(pivotTableSheetRels, SourceRelationshipPivotTable, sheetRelationshipsPivotTableXML, "") + f.addContentTypePart(pivotTableID, "pivotTable") + f.addContentTypePart(pivotCacheID, "pivotCache") + + return nil +} + +// parseFormatPivotTableSet provides a function to validate pivot table +// properties. +func (f *File) parseFormatPivotTableSet(opt *PivotTableOption) (*xlsxWorksheet, string, error) { + if opt == nil { + return nil, "", errors.New("parameter is required") + } + dataSheetName, _, err := f.adjustRange(opt.DataRange) + if err != nil { + return nil, "", fmt.Errorf("parameter 'DataRange' parsing error: %s", err.Error()) + } + pivotTableSheetName, _, err := f.adjustRange(opt.PivotTableRange) + if err != nil { + return nil, "", fmt.Errorf("parameter 'PivotTableRange' parsing error: %s", err.Error()) + } + dataSheet, err := f.workSheetReader(dataSheetName) + if err != nil { + return dataSheet, "", err + } + pivotTableSheetPath, ok := f.sheetMap[trimSheetName(pivotTableSheetName)] + if !ok { + return dataSheet, pivotTableSheetPath, fmt.Errorf("sheet %s is not exist", pivotTableSheetName) + } + return dataSheet, pivotTableSheetPath, err +} + +// adjustRange adjust range, for example: adjust Sheet1!$E$31:$A$1 to Sheet1!$A$1:$E$31 +func (f *File) adjustRange(rangeStr string) (string, []int, error) { + if len(rangeStr) < 1 { + return "", []int{}, errors.New("parameter is required") + } + rng := strings.Split(rangeStr, "!") + if len(rng) != 2 { + return "", []int{}, errors.New("parameter is invalid") + } + trimRng := strings.Replace(rng[1], "$", "", -1) + coordinates, err := f.areaRefToCoordinates(trimRng) + if err != nil { + return rng[0], []int{}, err + } + x1, y1, x2, y2 := coordinates[0], coordinates[1], coordinates[2], coordinates[3] + if x1 == x2 && y1 == y2 { + return rng[0], []int{}, errors.New("parameter is invalid") + } + + // Correct the coordinate area, such correct C1:B3 to B1:C3. + if x2 < x1 { + x1, x2 = x2, x1 + } + + if y2 < y1 { + y1, y2 = y2, y1 + } + return rng[0], []int{x1, y1, x2, y2}, nil +} + +func (f *File) getPivotFieldsOrder(dataRange string) ([]string, error) { + order := []string{} + // data range has been checked + dataSheet, coordinates, err := f.adjustRange(dataRange) + if err != nil { + return order, fmt.Errorf("parameter 'DataRange' parsing error: %s", err.Error()) + } + for col := coordinates[0]; col <= coordinates[2]; col++ { + coordinate, _ := CoordinatesToCellName(col, coordinates[1]) + name, err := f.GetCellValue(dataSheet, coordinate) + if err != nil { + return order, err + } + order = append(order, name) + } + return order, nil +} + +// addPivotCache provides a function to create a pivot cache by given properties. +func (f *File) addPivotCache(pivotCacheID int, pivotCacheXML string, opt *PivotTableOption, ws *xlsxWorksheet) error { + // validate data range + dataSheet, coordinates, err := f.adjustRange(opt.DataRange) + if err != nil { + return fmt.Errorf("parameter 'DataRange' parsing error: %s", err.Error()) + } + order, err := f.getPivotFieldsOrder(opt.DataRange) + if err != nil { + return err + } + hcell, _ := CoordinatesToCellName(coordinates[0], coordinates[1]) + vcell, _ := CoordinatesToCellName(coordinates[2], coordinates[3]) + pc := xlsxPivotCacheDefinition{ + SaveData: false, + RefreshOnLoad: true, + CacheSource: &xlsxCacheSource{ + Type: "worksheet", + WorksheetSource: &xlsxWorksheetSource{ + Ref: hcell + ":" + vcell, + Sheet: dataSheet, + }, + }, + CacheFields: &xlsxCacheFields{}, + } + for _, name := range order { + pc.CacheFields.CacheField = append(pc.CacheFields.CacheField, &xlsxCacheField{ + Name: name, + SharedItems: &xlsxSharedItems{ + Count: 0, + }, + }) + } + pc.CacheFields.Count = len(pc.CacheFields.CacheField) + pivotCache, err := xml.Marshal(pc) + f.saveFileList(pivotCacheXML, pivotCache) + return err +} + +// addPivotTable provides a function to create a pivot table by given pivot +// table ID and properties. +func (f *File) addPivotTable(cacheID, pivotTableID int, pivotTableXML string, opt *PivotTableOption) error { + // validate pivot table range + _, coordinates, err := f.adjustRange(opt.PivotTableRange) + if err != nil { + return fmt.Errorf("parameter 'PivotTableRange' parsing error: %s", err.Error()) + } + + hcell, _ := CoordinatesToCellName(coordinates[0], coordinates[1]) + vcell, _ := CoordinatesToCellName(coordinates[2], coordinates[3]) + + pt := xlsxPivotTableDefinition{ + Name: fmt.Sprintf("Pivot Table%d", pivotTableID), + CacheID: cacheID, + DataCaption: "Values", + Location: &xlsxLocation{ + Ref: hcell + ":" + vcell, + FirstDataCol: 1, + FirstDataRow: 1, + FirstHeaderRow: 1, + }, + PivotFields: &xlsxPivotFields{}, + RowFields: &xlsxRowFields{}, + RowItems: &xlsxRowItems{ + Count: 1, + I: []*xlsxI{ + { + []*xlsxX{{}, {}}, + }, + }, + }, + ColFields: &xlsxColFields{}, + DataFields: &xlsxDataFields{}, + PivotTableStyleInfo: &xlsxPivotTableStyleInfo{ + Name: "PivotStyleLight16", + ShowRowHeaders: true, + ShowColHeaders: true, + ShowLastColumn: true, + }, + } + + // pivot fields + err = f.addPivotFields(&pt, opt) + if err != nil { + return err + } + + // count pivot fields + pt.PivotFields.Count = len(pt.PivotFields.PivotField) + + // row fields + rowFieldsIndex, err := f.getPivotFieldsIndex(opt.Rows, opt) + if err != nil { + return err + } + for _, filedIdx := range rowFieldsIndex { + pt.RowFields.Field = append(pt.RowFields.Field, &xlsxField{ + X: filedIdx, + }) + } + + // count row fields + pt.RowFields.Count = len(pt.RowFields.Field) + + // col fields + colFieldsIndex, err := f.getPivotFieldsIndex(opt.Columns, opt) + if err != nil { + return err + } + for _, filedIdx := range colFieldsIndex { + pt.ColFields.Field = append(pt.ColFields.Field, &xlsxField{ + X: filedIdx, + }) + } + + // count col fields + pt.ColFields.Count = len(pt.ColFields.Field) + + // data fields + dataFieldsIndex, err := f.getPivotFieldsIndex(opt.Data, opt) + if err != nil { + return err + } + for _, dataField := range dataFieldsIndex { + pt.DataFields.DataField = append(pt.DataFields.DataField, &xlsxDataField{ + Fld: dataField, + }) + } + + // count data fields + pt.DataFields.Count = len(pt.DataFields.DataField) + + pivotTable, err := xml.Marshal(pt) + f.saveFileList(pivotTableXML, pivotTable) + return err +} + +// inStrSlice provides a method to check if an element is present in an array, +// and return the index of its location, otherwise return -1. +func inStrSlice(a []string, x string) int { + for idx, n := range a { + if x == n { + return idx + } + } + return -1 +} + +// addPivotFields create pivot fields based on the column order of the first +// row in the data region by given pivot table definition and option. +func (f *File) addPivotFields(pt *xlsxPivotTableDefinition, opt *PivotTableOption) error { + order, err := f.getPivotFieldsOrder(opt.DataRange) + if err != nil { + return err + } + for _, name := range order { + if inStrSlice(opt.Rows, name) != -1 { + pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, &xlsxPivotField{ + Axis: "axisRow", + Items: &xlsxItems{ + Count: 1, + Item: []*xlsxItem{ + {T: "default"}, + }, + }, + }) + continue + } + if inStrSlice(opt.Columns, name) != -1 { + pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, &xlsxPivotField{ + Axis: "axisCol", + Items: &xlsxItems{ + Count: 1, + Item: []*xlsxItem{ + {T: "default"}, + }, + }, + }) + continue + } + if inStrSlice(opt.Data, name) != -1 { + pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, &xlsxPivotField{ + DataField: true, + }) + continue + } + pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, &xlsxPivotField{}) + } + return err +} + +// countPivotTables provides a function to get drawing files count storage in +// the folder xl/pivotTables. +func (f *File) countPivotTables() int { + count := 0 + for k := range f.XLSX { + if strings.Contains(k, "xl/pivotTables/pivotTable") { + count++ + } + } + return count +} + +// countPivotCache provides a function to get drawing files count storage in +// the folder xl/pivotCache. +func (f *File) countPivotCache() int { + count := 0 + for k := range f.XLSX { + if strings.Contains(k, "xl/pivotCache/pivotCacheDefinition") { + count++ + } + } + return count +} + +// getPivotFieldsIndex convert the column of the first row in the data region +// to a sequential index by given fields and pivot option. +func (f *File) getPivotFieldsIndex(fields []string, opt *PivotTableOption) ([]int, error) { + pivotFieldsIndex := []int{} + orders, err := f.getPivotFieldsOrder(opt.DataRange) + if err != nil { + return pivotFieldsIndex, err + } + for _, field := range fields { + if pos := inStrSlice(orders, field); pos != -1 { + pivotFieldsIndex = append(pivotFieldsIndex, pos) + } + } + return pivotFieldsIndex, nil +} + +// addWorkbookPivotCache add the association ID of the pivot cache in xl/workbook.xml. +func (f *File) addWorkbookPivotCache(RID int) int { + wb := f.workbookReader() + if wb.PivotCaches == nil { + wb.PivotCaches = &xlsxPivotCaches{} + } + cacheID := 1 + for _, pivotCache := range wb.PivotCaches.PivotCache { + if pivotCache.CacheID > cacheID { + cacheID = pivotCache.CacheID + } + } + cacheID++ + wb.PivotCaches.PivotCache = append(wb.PivotCaches.PivotCache, xlsxPivotCache{ + CacheID: cacheID, + RID: fmt.Sprintf("rId%d", RID), + }) + return cacheID +} diff --git a/pivotTable_test.go b/pivotTable_test.go new file mode 100644 index 0000000..27e5914 --- /dev/null +++ b/pivotTable_test.go @@ -0,0 +1,164 @@ +package excelize + +import ( + "fmt" + "math/rand" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestAddPivotTable(t *testing.T) { + f := NewFile() + // Create some data in a sheet + month := []string{"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"} + year := []int{2017, 2018, 2019} + types := []string{"Meat", "Dairy", "Beverages", "Produce"} + region := []string{"East", "West", "North", "South"} + f.SetSheetRow("Sheet1", "A1", &[]string{"Month", "Year", "Type", "Sales", "Region"}) + for i := 0; i < 30; i++ { + f.SetCellValue("Sheet1", fmt.Sprintf("A%d", i+2), month[rand.Intn(12)]) + f.SetCellValue("Sheet1", fmt.Sprintf("B%d", i+2), year[rand.Intn(3)]) + f.SetCellValue("Sheet1", fmt.Sprintf("C%d", i+2), types[rand.Intn(4)]) + f.SetCellValue("Sheet1", fmt.Sprintf("D%d", i+2), rand.Intn(5000)) + f.SetCellValue("Sheet1", fmt.Sprintf("E%d", i+2), region[rand.Intn(4)]) + } + assert.NoError(t, f.AddPivotTable(&PivotTableOption{ + DataRange: "Sheet1!$A$1:$E$31", + PivotTableRange: "Sheet1!$G$2:$M$34", + Rows: []string{"Month", "Year"}, + Columns: []string{"Type"}, + Data: []string{"Sales"}, + })) + // Use different order of coordinate tests + assert.NoError(t, f.AddPivotTable(&PivotTableOption{ + DataRange: "Sheet1!$A$1:$E$31", + PivotTableRange: "Sheet1!$U$34:$O$2", + Rows: []string{"Month", "Year"}, + Columns: []string{"Type"}, + Data: []string{"Sales"}, + })) + + assert.NoError(t, f.AddPivotTable(&PivotTableOption{ + DataRange: "Sheet1!$A$1:$E$31", + PivotTableRange: "Sheet1!$W$2:$AC$34", + Rows: []string{"Month", "Year"}, + Columns: []string{"Region"}, + Data: []string{"Sales"}, + })) + assert.NoError(t, f.AddPivotTable(&PivotTableOption{ + DataRange: "Sheet1!$A$1:$E$31", + PivotTableRange: "Sheet1!$G$37:$W$50", + Rows: []string{"Month"}, + Columns: []string{"Region", "Year"}, + Data: []string{"Sales"}, + })) + f.NewSheet("Sheet2") + assert.NoError(t, f.AddPivotTable(&PivotTableOption{ + DataRange: "Sheet1!$A$1:$E$31", + PivotTableRange: "Sheet2!$A$1:$AR$15", + Rows: []string{"Month"}, + Columns: []string{"Region", "Type", "Year"}, + Data: []string{"Sales"}, + })) + assert.NoError(t, f.AddPivotTable(&PivotTableOption{ + DataRange: "Sheet1!$A$1:$E$31", + PivotTableRange: "Sheet2!$A$18:$AR$54", + Rows: []string{"Month", "Type"}, + Columns: []string{"Region", "Year"}, + Data: []string{"Sales"}, + })) + + // Test empty pivot table options + assert.EqualError(t, f.AddPivotTable(nil), "parameter is required") + // Test invalid data range + assert.EqualError(t, f.AddPivotTable(&PivotTableOption{ + DataRange: "Sheet1!$A$1:$A$1", + PivotTableRange: "Sheet1!$U$34:$O$2", + Rows: []string{"Month", "Year"}, + Columns: []string{"Type"}, + Data: []string{"Sales"}, + }), `parameter 'DataRange' parsing error: parameter is invalid`) + // Test the data range of the worksheet that is not declared + assert.EqualError(t, f.AddPivotTable(&PivotTableOption{ + DataRange: "$A$1:$E$31", + PivotTableRange: "Sheet1!$U$34:$O$2", + Rows: []string{"Month", "Year"}, + Columns: []string{"Type"}, + Data: []string{"Sales"}, + }), `parameter 'DataRange' parsing error: parameter is invalid`) + // Test the worksheet declared in the data range does not exist + assert.EqualError(t, f.AddPivotTable(&PivotTableOption{ + DataRange: "SheetN!$A$1:$E$31", + PivotTableRange: "Sheet1!$U$34:$O$2", + Rows: []string{"Month", "Year"}, + Columns: []string{"Type"}, + Data: []string{"Sales"}, + }), "sheet SheetN is not exist") + // Test the pivot table range of the worksheet that is not declared + assert.EqualError(t, f.AddPivotTable(&PivotTableOption{ + DataRange: "Sheet1!$A$1:$E$31", + PivotTableRange: "$U$34:$O$2", + Rows: []string{"Month", "Year"}, + Columns: []string{"Type"}, + Data: []string{"Sales"}, + }), `parameter 'PivotTableRange' parsing error: parameter is invalid`) + // Test the worksheet declared in the pivot table range does not exist + assert.EqualError(t, f.AddPivotTable(&PivotTableOption{ + DataRange: "Sheet1!$A$1:$E$31", + PivotTableRange: "SheetN!$U$34:$O$2", + Rows: []string{"Month", "Year"}, + Columns: []string{"Type"}, + Data: []string{"Sales"}, + }), "sheet SheetN is not exist") + // Test not exists worksheet in data range + assert.EqualError(t, f.AddPivotTable(&PivotTableOption{ + DataRange: "SheetN!$A$1:$E$31", + PivotTableRange: "Sheet1!$U$34:$O$2", + Rows: []string{"Month", "Year"}, + Columns: []string{"Type"}, + Data: []string{"Sales"}, + }), "sheet SheetN is not exist") + // Test invalid row number in data range + assert.EqualError(t, f.AddPivotTable(&PivotTableOption{ + DataRange: "Sheet1!$A$0:$E$31", + PivotTableRange: "Sheet1!$U$34:$O$2", + Rows: []string{"Month", "Year"}, + Columns: []string{"Type"}, + Data: []string{"Sales"}, + }), `parameter 'DataRange' parsing error: cannot convert cell "A0" to coordinates: invalid cell name "A0"`) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddPivotTable1.xlsx"))) + + // Test adjust range with invalid range + _, _, err := f.adjustRange("") + assert.EqualError(t, err, "parameter is required") + // Test get pivot fields order with empty data range + _, err = f.getPivotFieldsOrder("") + assert.EqualError(t, err, `parameter 'DataRange' parsing error: parameter is required`) + // Test add pivot cache with empty data range + assert.EqualError(t, f.addPivotCache(0, "", &PivotTableOption{}, nil), "parameter 'DataRange' parsing error: parameter is required") + // Test add pivot cache with invalid data range + assert.EqualError(t, f.addPivotCache(0, "", &PivotTableOption{ + DataRange: "$A$1:$E$31", + PivotTableRange: "Sheet1!$U$34:$O$2", + Rows: []string{"Month", "Year"}, + Columns: []string{"Type"}, + Data: []string{"Sales"}, + }, nil), "parameter 'DataRange' parsing error: parameter is invalid") + // Test add pivot table with empty options + assert.EqualError(t, f.addPivotTable(0, 0, "", &PivotTableOption{}), "parameter 'PivotTableRange' parsing error: parameter is required") + // Test add pivot table with invalid data range + assert.EqualError(t, f.addPivotTable(0, 0, "", &PivotTableOption{}), "parameter 'PivotTableRange' parsing error: parameter is required") + // Test add pivot fields with empty data range + assert.EqualError(t, f.addPivotFields(nil, &PivotTableOption{ + DataRange: "$A$1:$E$31", + PivotTableRange: "Sheet1!$U$34:$O$2", + Rows: []string{"Month", "Year"}, + Columns: []string{"Type"}, + Data: []string{"Sales"}, + }), `parameter 'DataRange' parsing error: parameter is invalid`) + // Test get pivot fields index with empty data range + _, err = f.getPivotFieldsIndex([]string{}, &PivotTableOption{}) + assert.EqualError(t, err, `parameter 'DataRange' parsing error: parameter is required`) +} diff --git a/xmlPivotCache.go b/xmlPivotCache.go index 0c00832..a4b0711 100644 --- a/xmlPivotCache.go +++ b/xmlPivotCache.go @@ -10,7 +10,7 @@ type xlsxPivotCacheDefinition struct { XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main pivotCacheDefinition"` RID string `xml:"http://schemas.openxmlformats.org/officeDocument/2006/relationships id,attr,omitempty"` Invalid bool `xml:"invalid,attr,omitempty"` - SaveData bool `xml:"saveData,attr,omitempty"` + SaveData bool `xml:"saveData,attr"` RefreshOnLoad bool `xml:"refreshOnLoad,attr,omitempty"` OptimizeMemory bool `xml:"optimizeMemory,attr,omitempty"` EnableRefresh bool `xml:"enableRefresh,attr,omitempty"` @@ -47,6 +47,28 @@ type xlsxPivotCacheDefinition struct { // (including OLAP cubes), multiple SpreadsheetML worksheets, or another // PivotTable. type xlsxCacheSource struct { + Type string `xml:"type,attr"` + ConnectionId int `xml:"connectionId,attr,omitempty"` + WorksheetSource *xlsxWorksheetSource `xml:"worksheetSource"` + Consolidation *xlsxConsolidation `xml:"consolidation"` + ExtLst *xlsxExtLst `xml:"extLst"` +} + +// xlsxWorksheetSource represents the location of the source of the data that +// is stored in the cache. +type xlsxWorksheetSource struct { + RID string `xml:"http://schemas.openxmlformats.org/officeDocument/2006/relationships id,attr,omitempty"` + Ref string `xml:"ref,attr,omitempty"` + Name string `xml:"name,attr,omitempty"` + Sheet string `xml:"sheet,attr,omitempty"` +} + +// xlsxConsolidation represents the description of the PivotCache source using +// multiple consolidation ranges. This element is used when the source of the +// PivotTable is a collection of ranges in the workbook. The ranges are +// specified in the rangeSets collection. The logic for how the application +// consolidates the data in the ranges is application- defined. +type xlsxConsolidation struct { } // xlsxCacheFields represents the collection of field definitions in the @@ -67,18 +89,18 @@ type xlsxCacheField struct { PropertyName string `xml:"propertyName,attr,omitempty"` ServerField bool `xml:"serverField,attr,omitempty"` UniqueList bool `xml:"uniqueList,attr,omitempty"` - NumFmtId string `xml:"numFmtId,attr,omitempty"` + NumFmtId int `xml:"numFmtId,attr"` Formula string `xml:"formula,attr,omitempty"` SQLType int `xml:"sqlType,attr,omitempty"` Hierarchy int `xml:"hierarchy,attr,omitempty"` Level int `xml:"level,attr,omitempty"` - DatabaseField bool `xml:"databaseField,attr"` + DatabaseField bool `xml:"databaseField,attr,omitempty"` MappingCount int `xml:"mappingCount,attr,omitempty"` MemberPropertyField bool `xml:"memberPropertyField,attr,omitempty"` SharedItems *xlsxSharedItems `xml:"sharedItems"` FieldGroup *xlsxFieldGroup `xml:"fieldGroup"` - MpMap *xlsxX `xml:"map"` - ExtLst *xlsxExtLst `xml:"exrLst"` + MpMap *xlsxX `xml:"mpMap"` + ExtLst *xlsxExtLst `xml:"extLst"` } // xlsxSharedItems represents the collection of unique items for a field in @@ -100,7 +122,7 @@ type xlsxSharedItems struct { MaxValue float64 `xml:"maxValue,attr,omitempty"` MinDate string `xml:"minDate,attr,omitempty"` MaxDate string `xml:"maxDate,attr,omitempty"` - Count int `xml:"count,attr,omitempty"` + Count int `xml:"count,attr"` LongText bool `xml:"longText,attr,omitempty"` M *xlsxMissing `xml:"m"` N *xlsxNumber `xml:"n"` diff --git a/xmlPivotTable.go b/xmlPivotTable.go index 6f2a8e7..3738ed8 100644 --- a/xmlPivotTable.go +++ b/xmlPivotTable.go @@ -36,8 +36,8 @@ type xlsxPivotTableDefinition struct { PivotTableStyle string `xml:"pivotTableStyle,attr,omitempty"` VacatedStyle string `xml:"vacatedStyle,attr,omitempty"` Tag string `xml:"tag,attr,omitempty"` - UpdatedVersion int `xml:"updatedVersion,attr"` - MinRefreshableVersion int `xml:"minRefreshableVersion,attr"` + UpdatedVersion int `xml:"updatedVersion,attr,omitempty"` + MinRefreshableVersion int `xml:"minRefreshableVersion,attr,omitempty"` AsteriskTotals bool `xml:"asteriskTotals,attr,omitempty"` ShowItems bool `xml:"showItems,attr,omitempty"` EditData bool `xml:"editData,attr,omitempty"` @@ -54,27 +54,27 @@ type xlsxPivotTableDefinition struct { EnableDrill bool `xml:"enableDrill,attr,omitempty"` EnableFieldProperties bool `xml:"enableFieldProperties,attr,omitempty"` PreserveFormatting bool `xml:"preserveFormatting,attr,omitempty"` - UseAutoFormatting bool `xml:"useAutoFormatting,attr"` + UseAutoFormatting bool `xml:"useAutoFormatting,attr,omitempty"` PageWrap int `xml:"pageWrap,attr,omitempty"` PageOverThenDown bool `xml:"pageOverThenDown,attr,omitempty"` SubtotalHiddenItems bool `xml:"subtotalHiddenItems,attr,omitempty"` RowGrandTotals bool `xml:"rowGrandTotals,attr,omitempty"` ColGrandTotals bool `xml:"colGrandTotals,attr,omitempty"` FieldPrintTitles bool `xml:"fieldPrintTitles,attr,omitempty"` - ItemPrintTitles bool `xml:"itemPrintTitles,attr"` + ItemPrintTitles bool `xml:"itemPrintTitles,attr,omitempty"` MergeItem bool `xml:"mergeItem,attr,omitempty"` ShowDropZones bool `xml:"showDropZones,attr,omitempty"` - CreatedVersion int `xml:"createdVersion,attr"` + CreatedVersion int `xml:"createdVersion,attr,omitempty"` Indent int `xml:"indent,attr,omitempty"` ShowEmptyRow bool `xml:"showEmptyRow,attr,omitempty"` ShowEmptyCol bool `xml:"showEmptyCol,attr,omitempty"` ShowHeaders bool `xml:"showHeaders,attr,omitempty"` - Compact bool `xml:"compact,attr,omitempty"` - Outline bool `xml:"outline,attr,omitempty"` + Compact bool `xml:"compact,attr"` + Outline bool `xml:"outline,attr"` OutlineData bool `xml:"outlineData,attr,omitempty"` CompactData bool `xml:"compactData,attr,omitempty"` Published bool `xml:"published,attr,omitempty"` - GridDropZones bool `xml:"gridDropZones,attr"` + GridDropZones bool `xml:"gridDropZones,attr,omitempty"` Immersive bool `xml:"immersive,attr,omitempty"` MultipleFieldFilters bool `xml:"multipleFieldFilters,attr,omitempty"` ChartFormat int `xml:"chartFormat,attr,omitempty"` @@ -101,8 +101,8 @@ type xlsxLocation struct { FirstHeaderRow int `xml:"firstHeaderRow,attr"` FirstDataRow int `xml:"firstDataRow,attr"` FirstDataCol int `xml:"firstDataCol,attr"` - RowPageCount int `xml:"rowPageCount,attr"` - ColPageCount int `xml:"colPageCount,attr"` + RowPageCount int `xml:"rowPageCount,attr,omitempty"` + ColPageCount int `xml:"colPageCount,attr,omitempty"` } // xlsxPivotFields represents the collection of fields that appear on the @@ -116,50 +116,50 @@ type xlsxPivotFields struct { // contains information about the field, including the collection of items in // the field. type xlsxPivotField struct { - Name string `xml:"name,attr"` + Name string `xml:"name,attr,omitempty"` Axis string `xml:"axis,attr,omitempty"` - DataField bool `xml:"dataField,attr"` - SubtotalCaption string `xml:"subtotalCaption,attr"` - ShowDropDowns bool `xml:"showDropDowns,attr"` - HiddenLevel bool `xml:"hiddenLevel,attr"` - UniqueMemberProperty string `xml:"uniqueMemberProperty,attr"` + DataField bool `xml:"dataField,attr,omitempty"` + SubtotalCaption string `xml:"subtotalCaption,attr,omitempty"` + ShowDropDowns bool `xml:"showDropDowns,attr,omitempty"` + HiddenLevel bool `xml:"hiddenLevel,attr,omitempty"` + UniqueMemberProperty string `xml:"uniqueMemberProperty,attr,omitempty"` Compact bool `xml:"compact,attr"` - AllDrilled bool `xml:"allDrilled,attr"` + AllDrilled bool `xml:"allDrilled,attr,omitempty"` NumFmtId string `xml:"numFmtId,attr,omitempty"` Outline bool `xml:"outline,attr"` - SubtotalTop bool `xml:"subtotalTop,attr"` - DragToRow bool `xml:"dragToRow,attr"` - DragToCol bool `xml:"dragToCol,attr"` - MultipleItemSelectionAllowed bool `xml:"multipleItemSelectionAllowed,attr"` - DragToPage bool `xml:"dragToPage,attr"` - DragToData bool `xml:"dragToData,attr"` - DragOff bool `xml:"dragOff,attr"` + SubtotalTop bool `xml:"subtotalTop,attr,omitempty"` + DragToRow bool `xml:"dragToRow,attr,omitempty"` + DragToCol bool `xml:"dragToCol,attr,omitempty"` + MultipleItemSelectionAllowed bool `xml:"multipleItemSelectionAllowed,attr,omitempty"` + DragToPage bool `xml:"dragToPage,attr,omitempty"` + DragToData bool `xml:"dragToData,attr,omitempty"` + DragOff bool `xml:"dragOff,attr,omitempty"` ShowAll bool `xml:"showAll,attr"` - InsertBlankRow bool `xml:"insertBlankRow,attr"` - ServerField bool `xml:"serverField,attr"` - InsertPageBreak bool `xml:"insertPageBreak,attr"` - AutoShow bool `xml:"autoShow,attr"` - TopAutoShow bool `xml:"topAutoShow,attr"` - HideNewItems bool `xml:"hideNewItems,attr"` - MeasureFilter bool `xml:"measureFilter,attr"` - IncludeNewItemsInFilter bool `xml:"includeNewItemsInFilter,attr"` - ItemPageCount int `xml:"itemPageCount,attr"` - SortType string `xml:"sortType,attr"` + InsertBlankRow bool `xml:"insertBlankRow,attr,omitempty"` + ServerField bool `xml:"serverField,attr,omitempty"` + InsertPageBreak bool `xml:"insertPageBreak,attr,omitempty"` + AutoShow bool `xml:"autoShow,attr,omitempty"` + TopAutoShow bool `xml:"topAutoShow,attr,omitempty"` + HideNewItems bool `xml:"hideNewItems,attr,omitempty"` + MeasureFilter bool `xml:"measureFilter,attr,omitempty"` + IncludeNewItemsInFilter bool `xml:"includeNewItemsInFilter,attr,omitempty"` + ItemPageCount int `xml:"itemPageCount,attr,omitempty"` + SortType string `xml:"sortType,attr,omitempty"` DataSourceSort bool `xml:"dataSourceSort,attr,omitempty"` - NonAutoSortDefault bool `xml:"nonAutoSortDefault,attr"` + NonAutoSortDefault bool `xml:"nonAutoSortDefault,attr,omitempty"` RankBy int `xml:"rankBy,attr,omitempty"` - DefaultSubtotal bool `xml:"defaultSubtotal,attr"` - SumSubtotal bool `xml:"sumSubtotal,attr"` - CountASubtotal bool `xml:"countASubtotal,attr"` - AvgSubtotal bool `xml:"avgSubtotal,attr"` - MaxSubtotal bool `xml:"maxSubtotal,attr"` - MinSubtotal bool `xml:"minSubtotal,attr"` - ProductSubtotal bool `xml:"productSubtotal,attr"` - CountSubtotal bool `xml:"countSubtotal,attr"` - StdDevSubtotal bool `xml:"stdDevSubtotal,attr"` - StdDevPSubtotal bool `xml:"stdDevPSubtotal,attr"` - VarSubtotal bool `xml:"varSubtotal,attr"` - VarPSubtotal bool `xml:"varPSubtotal,attr"` + DefaultSubtotal bool `xml:"defaultSubtotal,attr,omitempty"` + SumSubtotal bool `xml:"sumSubtotal,attr,omitempty"` + CountASubtotal bool `xml:"countASubtotal,attr,omitempty"` + AvgSubtotal bool `xml:"avgSubtotal,attr,omitempty"` + MaxSubtotal bool `xml:"maxSubtotal,attr,omitempty"` + MinSubtotal bool `xml:"minSubtotal,attr,omitempty"` + ProductSubtotal bool `xml:"productSubtotal,attr,omitempty"` + CountSubtotal bool `xml:"countSubtotal,attr,omitempty"` + StdDevSubtotal bool `xml:"stdDevSubtotal,attr,omitempty"` + StdDevPSubtotal bool `xml:"stdDevPSubtotal,attr,omitempty"` + VarSubtotal bool `xml:"varSubtotal,attr,omitempty"` + VarPSubtotal bool `xml:"varPSubtotal,attr,omitempty"` ShowPropCell bool `xml:"showPropCell,attr,omitempty"` ShowPropTip bool `xml:"showPropTip,attr,omitempty"` ShowPropAsCaption bool `xml:"showPropAsCaption,attr,omitempty"` @@ -179,17 +179,17 @@ type xlsxItems struct { // xlsxItem represents a single item in PivotTable field. type xlsxItem struct { - N string `xml:"n,attr"` - T string `xml:"t,attr"` - H bool `xml:"h,attr"` - S bool `xml:"s,attr"` - SD bool `xml:"sd,attr"` - F bool `xml:"f,attr"` - M bool `xml:"m,attr"` - C bool `xml:"c,attr"` - X int `xml:"x,attr,omitempty"` - D bool `xml:"d,attr"` - E bool `xml:"e,attr"` + N string `xml:"n,attr,omitempty"` + T string `xml:"t,attr,omitempty"` + H bool `xml:"h,attr,omitempty"` + S bool `xml:"s,attr,omitempty"` + SD bool `xml:"sd,attr,omitempty"` + F bool `xml:"f,attr,omitempty"` + M bool `xml:"m,attr,omitempty"` + C bool `xml:"c,attr,omitempty"` + X int `xml:"x,attr,omitempty,omitempty"` + D bool `xml:"d,attr,omitempty"` + E bool `xml:"e,attr,omitempty"` } // xlsxAutoSortScope represents the sorting scope for the PivotTable. @@ -198,8 +198,8 @@ type xlsxAutoSortScope struct { // xlsxRowFields represents the collection of row fields for the PivotTable. type xlsxRowFields struct { - Count int `xml:"count,attr"` - Fields []*xlsxField `xml:"fields"` + Count int `xml:"count,attr"` + Field []*xlsxField `xml:"field"` } // xlsxField represents a generic field that can appear either on the column @@ -224,14 +224,13 @@ type xlsxI struct { // xlsxX represents an array of indexes to cached shared item values. type xlsxX struct { - XMLName xml.Name `xml:"x"` } // xlsxColFields represents the collection of fields that are on the column // axis of the PivotTable. type xlsxColFields struct { - Count int `xml:"count,attr"` - Fields []*xlsxField `xml:"fields"` + Count int `xml:"count,attr"` + Field []*xlsxField `xml:"field"` } // xlsxColItems represents the collection of column items of the PivotTable. @@ -261,8 +260,8 @@ type xlsxPageField struct { // xlsxDataFields represents the collection of items in the data region of the // PivotTable. type xlsxDataFields struct { - Count int `xml:"count,attr"` - DataField *xlsxDataField `xml:"dataField"` + Count int `xml:"count,attr"` + DataField []*xlsxDataField `xml:"dataField"` } // xlsxDataField represents a field from a source list, table, or database @@ -270,10 +269,10 @@ type xlsxDataFields struct { type xlsxDataField struct { Name string `xml:"name,attr,omitempty"` Fld int `xml:"fld,attr"` - Subtotal string `xml:"subtotal,attr"` - ShowDataAs string `xml:"showDataAs,attr"` - BaseField int `xml:"baseField,attr"` - BaseItem int64 `xml:"baseItem,attr"` + Subtotal string `xml:"subtotal,attr,omitempty"` + ShowDataAs string `xml:"showDataAs,attr,omitempty"` + BaseField int `xml:"baseField,attr,omitempty"` + BaseItem int64 `xml:"baseItem,attr,omitempty"` NumFmtId string `xml:"numFmtId,attr,omitempty"` ExtLst *xlsxExtLst `xml:"extLst"` } @@ -289,7 +288,7 @@ type xlsxPivotTableStyleInfo struct { Name string `xml:"name,attr"` ShowRowHeaders bool `xml:"showRowHeaders,attr"` ShowColHeaders bool `xml:"showColHeaders,attr"` - ShowRowStripes bool `xml:"showRowStripes,attr"` - ShowColStripes bool `xml:"showColStripes,attr"` + ShowRowStripes bool `xml:"showRowStripes,attr,omitempty"` + ShowColStripes bool `xml:"showColStripes,attr,omitempty"` ShowLastColumn bool `xml:"showLastColumn,attr,omitempty"` } -- cgit v1.2.1 From 3280e1b68664e12143cbd2b3a408f9f494a72897 Mon Sep 17 00:00:00 2001 From: Christian Fiedler Date: Sun, 22 Sep 2019 14:52:01 +0200 Subject: Allow access to more formula attributes in SetCellFormula (#484) * Allow access to more formula attributes in SetCellFormula Make SetCellFormula variadic to not break API. The new arguments are option arguments in which the type of the formula and the ref attribute may be set. These need to be set for an array formula to work. * Add TestWriteArrayFormula to test optional parameters of SetCellFormula TestWriteArrayFormula writes a document to the test directory that contains array formulas that are used to calculate standard deviations. The file also contains values calculated by the Go testcase, so the results can be verified. It should be tested, if the array formula works (i.e. shows a number, not an error) and that the values calculated by the formula and those calculated by Go are the same. --- cell.go | 19 +++++++++- excelize_test.go | 107 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 125 insertions(+), 1 deletion(-) diff --git a/cell.go b/cell.go index 1da46aa..f9868de 100644 --- a/cell.go +++ b/cell.go @@ -273,9 +273,15 @@ func (f *File) GetCellFormula(sheet, axis string) (string, error) { }) } +// FormulaOpts can be passed to SetCellFormula to use other formula types. +type FormulaOpts struct { + Type *string // Formula type + Ref *string // Shared formula ref +} + // SetCellFormula provides a function to set cell formula by given string and // worksheet name. -func (f *File) SetCellFormula(sheet, axis, formula string) error { +func (f *File) SetCellFormula(sheet, axis, formula string, opts ...FormulaOpts) error { xlsx, err := f.workSheetReader(sheet) if err != nil { return err @@ -295,6 +301,17 @@ func (f *File) SetCellFormula(sheet, axis, formula string) error { } else { cellData.F = &xlsxF{Content: formula} } + + for _, o := range opts { + if o.Type != nil { + cellData.F.T = *o.Type + } + + if o.Ref != nil { + cellData.F.Ref = *o.Ref + } + } + return err } diff --git a/excelize_test.go b/excelize_test.go index a5d7671..daf9e7d 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -8,6 +8,7 @@ import ( _ "image/jpeg" _ "image/png" "io/ioutil" + "math" "os" "path/filepath" "strconv" @@ -397,6 +398,112 @@ func TestMergeCell(t *testing.T) { assert.NoError(t, f.SaveAs(filepath.Join("test", "TestMergeCell.xlsx"))) } +// TestWriteArrayFormula tests the extended options of SetCellFormula by writing an array function +// to a workbook. In the resulting file, the lines 2 and 3 as well as 4 and 5 should have matching +// contents. +func TestWriteArrayFormula(t *testing.T) { + cell := func(col, row int) string { + c, err := CoordinatesToCellName(col, row) + if err != nil { + t.Fatal(err) + } + + return c + } + + f := NewFile() + + sample := []string{"Sample 1", "Sample 2", "Sample 3"} + values := []int{1855, 1709, 1462, 1115, 1524, 625, 773, 126, 1027, 1696, 1078, 1917, 1109, 1753, 1884, 659, 994, 1911, 1925, 899, 196, 244, 1488, 1056, 1986, 66, 784, 725, 767, 1722, 1541, 1026, 1455, 264, 1538, 877, 1581, 1098, 383, 762, 237, 493, 29, 1923, 474, 430, 585, 688, 308, 200, 1259, 622, 798, 1048, 996, 601, 582, 332, 377, 805, 250, 1860, 1360, 840, 911, 1346, 1651, 1651, 665, 584, 1057, 1145, 925, 1752, 202, 149, 1917, 1398, 1894, 818, 714, 624, 1085, 1566, 635, 78, 313, 1686, 1820, 494, 614, 1913, 271, 1016, 338, 1301, 489, 1733, 1483, 1141} + assoc := []int{2, 0, 0, 0, 0, 1, 1, 0, 0, 1, 2, 2, 2, 1, 1, 1, 1, 0, 0, 0, 1, 0, 2, 0, 2, 1, 2, 2, 2, 1, 0, 1, 0, 1, 1, 2, 0, 2, 1, 0, 2, 1, 0, 1, 0, 0, 2, 0, 2, 2, 1, 2, 2, 1, 2, 2, 1, 2, 1, 2, 2, 1, 1, 1, 0, 1, 0, 2, 0, 0, 1, 2, 1, 0, 1, 0, 0, 2, 1, 1, 2, 0, 2, 1, 0, 2, 2, 2, 1, 0, 0, 1, 1, 1, 2, 0, 2, 0, 1, 1} + if len(values) != len(assoc) { + t.Fatal("values and assoc must be of same length") + } + + // Average calculates the average of the n-th sample (0 <= n < len(sample)). + average := func(n int) int { + sum := 0 + count := 0 + for i := 0; i != len(values); i++ { + if assoc[i] == n { + sum += values[i] + count++ + } + } + + return int(math.Round(float64(sum) / float64(count))) + } + + // Stdev calculates the standard deviation of the n-th sample (0 <= n < len(sample)). + stdev := func(n int) int { + avg := average(n) + + sum := 0 + count := 0 + for i := 0; i != len(values); i++ { + if assoc[i] == n { + sum += (values[i] - avg) * (values[i] - avg) + count++ + } + } + + return int(math.Round(math.Sqrt(float64(sum) / float64(count)))) + } + + // Line 2 contains the results of AVERAGEIF + f.SetCellStr("Sheet1", "A2", "Average") + + // Line 3 contains the average that was calculated in Go + f.SetCellStr("Sheet1", "A3", "Average (calculated)") + + // Line 4 contains the results of the array function that calculates the standard deviation + f.SetCellStr("Sheet1", "A4", "Std. deviation") + + // Line 5 contains the standard deviations calculated in Go + f.SetCellStr("Sheet1", "A5", "Std. deviation (calculated)") + + f.SetCellStr("Sheet1", "B1", sample[0]) + f.SetCellStr("Sheet1", "C1", sample[1]) + f.SetCellStr("Sheet1", "D1", sample[2]) + + firstResLine := 8 + f.SetCellStr("Sheet1", cell(1, firstResLine-1), "Result Values") + f.SetCellStr("Sheet1", cell(2, firstResLine-1), "Sample") + + for i := 0; i != len(values); i++ { + valCell := cell(1, i+firstResLine) + assocCell := cell(2, i+firstResLine) + + f.SetCellInt("Sheet1", valCell, values[i]) + f.SetCellStr("Sheet1", assocCell, sample[assoc[i]]) + } + + valRange := fmt.Sprintf("$A$%d:$A$%d", firstResLine, len(values)+firstResLine-1) + assocRange := fmt.Sprintf("$B$%d:$B$%d", firstResLine, len(values)+firstResLine-1) + + for i := 0; i != len(sample); i++ { + nameCell := cell(i+2, 1) + avgCell := cell(i+2, 2) + calcAvgCell := cell(i+2, 3) + stdevCell := cell(i+2, 4) + calcStdevCell := cell(i+2, 5) + + f.SetCellInt("Sheet1", calcAvgCell, average(i)) + f.SetCellInt("Sheet1", calcStdevCell, stdev(i)) + + // Average can be done with AVERAGEIF + f.SetCellFormula("Sheet1", avgCell, fmt.Sprintf("ROUND(AVERAGEIF(%s,%s,%s),0)", assocRange, nameCell, valRange)) + + ref := stdevCell + ":" + stdevCell + t := STCellFormulaTypeArray + // Use an array formula for standard deviation + f.SetCellFormula("Sheet1", stdevCell, fmt.Sprintf("ROUND(STDEVP(IF(%s=%s,%s)),0)", assocRange, nameCell, valRange), + FormulaOpts{}, FormulaOpts{Type: &t}, FormulaOpts{Ref: &ref}) + } + + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestWriteArrayFormula.xlsx"))) +} + func TestSetCellStyleAlignment(t *testing.T) { f, err := prepareTestBook1() if !assert.NoError(t, err) { -- cgit v1.2.1 From 75d66a03f33f25c29167c5f75ee8a4cc58598420 Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 23 Sep 2019 21:50:03 +0800 Subject: Fix #482, font strike style support --- excelize_test.go | 2 +- styles.go | 4 ++++ xmlStyles.go | 1 + 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/excelize_test.go b/excelize_test.go index daf9e7d..7b6b674 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -775,7 +775,7 @@ func TestSetCellStyleFont(t *testing.T) { assert.NoError(t, f.SetCellStyle("Sheet2", "A4", "A4", style)) - style, err = f.NewStyle(`{"font":{"color":"#777777"}}`) + style, err = f.NewStyle(`{"font":{"color":"#777777","strike":true}}`) if !assert.NoError(t, err) { t.FailNow() } diff --git a/styles.go b/styles.go index 4d6071a..3244be2 100644 --- a/styles.go +++ b/styles.go @@ -1993,6 +1993,10 @@ func (f *File) setFont(formatStyle *formatStyle) *xlsxFont { if fnt.Name.Val == "" { fnt.Name.Val = f.GetDefaultFont() } + if formatStyle.Font.Strike { + strike := true + fnt.Strike = &strike + } val, ok := fontUnderlineType[formatStyle.Font.Underline] if ok { fnt.U = &attrValString{Val: val} diff --git a/xmlStyles.go b/xmlStyles.go index 5823bc9..7e02d6e 100644 --- a/xmlStyles.go +++ b/xmlStyles.go @@ -320,6 +320,7 @@ type formatFont struct { Underline string `json:"underline"` Family string `json:"family"` Size float64 `json:"size"` + Strike bool `json:"strike"` Color string `json:"color"` } -- cgit v1.2.1 From a34d3b8c86d67d3ad0bc0dbedb69d3b4ebbc210f Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 24 Sep 2019 21:53:19 +0800 Subject: Compatibility improvement --- chart.go | 2 +- col.go | 10 +++++++--- col_test.go | 35 ++++++++++++----------------------- picture.go | 2 +- rows.go | 8 ++++++-- sparkline.go | 10 ++-------- test/Book1.xlsx | Bin 20899 -> 20750 bytes xmlChart.go | 3 ++- xmlStyles.go | 10 +++++----- xmlWorksheet.go | 10 +++------- 10 files changed, 39 insertions(+), 51 deletions(-) diff --git a/chart.go b/chart.go index db2df1e..7db7eee 100644 --- a/chart.go +++ b/chart.go @@ -1845,7 +1845,7 @@ func (f *File) addDrawingChart(sheet, drawingXML, cell string, width, height, rI graphicFrame := xlsxGraphicFrame{ NvGraphicFramePr: xlsxNvGraphicFramePr{ CNvPr: &xlsxCNvPr{ - ID: f.countCharts() + f.countMedia() + 1, + ID: len(content.OneCellAnchor) + len(content.TwoCellAnchor) + 2, Name: "Chart " + strconv.Itoa(cNvPrID), }, }, diff --git a/col.go b/col.go index ffa0ca6..be08c29 100644 --- a/col.go +++ b/col.go @@ -10,6 +10,7 @@ package excelize import ( + "errors" "math" "strings" ) @@ -112,19 +113,22 @@ func (f *File) GetColOutlineLevel(sheet, col string) (uint8, error) { for c := range xlsx.Cols.Col { colData := &xlsx.Cols.Col[c] if colData.Min <= colNum && colNum <= colData.Max { - level = colData.OutlineLevel + level = colData.OutlineLevel + 1 } } return level, err } // SetColOutlineLevel provides a function to set outline level of a single -// column by given worksheet name and column name. For example, set outline -// level of column D in Sheet1 to 2: +// column by given worksheet name and column name. The value of parameter +// 'level' is 1-7. For example, set outline level of column D in Sheet1 to 2: // // err := f.SetColOutlineLevel("Sheet1", "D", 2) // func (f *File) SetColOutlineLevel(sheet, col string, level uint8) error { + if level > 7 || level < 1 { + return errors.New("invalid outline level") + } colNum, err := ColumnNameToNumber(col) if err != nil { return err diff --git a/col_test.go b/col_test.go index e3164d4..a696caa 100644 --- a/col_test.go +++ b/col_test.go @@ -10,9 +10,7 @@ import ( func TestColumnVisibility(t *testing.T) { t.Run("TestBook1", func(t *testing.T) { f, err := prepareTestBook1() - if !assert.NoError(t, err) { - t.FailNow() - } + assert.NoError(t, err) assert.NoError(t, f.SetColVisible("Sheet1", "F", false)) assert.NoError(t, f.SetColVisible("Sheet1", "F", true)) @@ -38,9 +36,7 @@ func TestColumnVisibility(t *testing.T) { t.Run("TestBook3", func(t *testing.T) { f, err := prepareTestBook3() - if !assert.NoError(t, err) { - t.FailNow() - } + assert.NoError(t, err) f.GetColVisible("Sheet1", "B") }) } @@ -49,12 +45,14 @@ func TestOutlineLevel(t *testing.T) { f := NewFile() f.GetColOutlineLevel("Sheet1", "D") f.NewSheet("Sheet2") - f.SetColOutlineLevel("Sheet1", "D", 4) + assert.NoError(t, f.SetColOutlineLevel("Sheet1", "D", 4)) f.GetColOutlineLevel("Sheet1", "D") f.GetColOutlineLevel("Shee2", "A") - f.SetColWidth("Sheet2", "A", "D", 13) - f.SetColOutlineLevel("Sheet2", "B", 2) - f.SetRowOutlineLevel("Sheet1", 2, 250) + assert.NoError(t, f.SetColWidth("Sheet2", "A", "D", 13)) + assert.NoError(t, f.SetColOutlineLevel("Sheet2", "B", 2)) + assert.NoError(t, f.SetRowOutlineLevel("Sheet1", 2, 7)) + assert.EqualError(t, f.SetColOutlineLevel("Sheet1", "D", 8), "invalid outline level") + assert.EqualError(t, f.SetRowOutlineLevel("Sheet1", 2, 8), "invalid outline level") // Test set and get column outline level with illegal cell coordinates. assert.EqualError(t, f.SetColOutlineLevel("Sheet1", "*", 1), `invalid column name "*"`) @@ -67,7 +65,7 @@ func TestOutlineLevel(t *testing.T) { assert.EqualError(t, f.SetRowOutlineLevel("Sheet1", 0, 1), "invalid row number 0") level, err := f.GetRowOutlineLevel("Sheet1", 2) assert.NoError(t, err) - assert.Equal(t, uint8(250), level) + assert.Equal(t, uint8(7), level) _, err = f.GetRowOutlineLevel("Sheet1", 0) assert.EqualError(t, err, `invalid row number 0`) @@ -76,15 +74,10 @@ func TestOutlineLevel(t *testing.T) { assert.NoError(t, err) assert.Equal(t, uint8(0), level) - err = f.SaveAs(filepath.Join("test", "TestOutlineLevel.xlsx")) - if !assert.NoError(t, err) { - t.FailNow() - } + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestOutlineLevel.xlsx"))) f, err = OpenFile(filepath.Join("test", "Book1.xlsx")) - if !assert.NoError(t, err) { - t.FailNow() - } + assert.NoError(t, err) f.SetColOutlineLevel("Sheet2", "B", 2) } @@ -138,11 +131,7 @@ func TestInsertCol(t *testing.T) { f.SetCellHyperLink(sheet1, "A5", "https://github.com/360EntSecGroup-Skylar/excelize", "External") f.MergeCell(sheet1, "A1", "C3") - err := f.AutoFilter(sheet1, "A2", "B2", `{"column":"B","expression":"x != blanks"}`) - if !assert.NoError(t, err) { - t.FailNow() - } - + assert.NoError(t, f.AutoFilter(sheet1, "A2", "B2", `{"column":"B","expression":"x != blanks"}`)) assert.NoError(t, f.InsertCol(sheet1, "A")) // Test insert column with illegal cell coordinates. diff --git a/picture.go b/picture.go index 518463a..4470fa1 100644 --- a/picture.go +++ b/picture.go @@ -272,7 +272,7 @@ func (f *File) addDrawingPicture(sheet, drawingXML, cell, file string, width, he twoCellAnchor.To = &to pic := xlsxPic{} pic.NvPicPr.CNvPicPr.PicLocks.NoChangeAspect = formatSet.NoChangeAspect - pic.NvPicPr.CNvPr.ID = f.countCharts() + f.countMedia() + 1 + pic.NvPicPr.CNvPr.ID = len(content.OneCellAnchor) + len(content.TwoCellAnchor) + 2 pic.NvPicPr.CNvPr.Descr = file pic.NvPicPr.CNvPr.Name = "Picture " + strconv.Itoa(cNvPrID) if hyperlinkRID != 0 { diff --git a/rows.go b/rows.go index 6281e62..3796441 100644 --- a/rows.go +++ b/rows.go @@ -11,6 +11,7 @@ package excelize import ( "encoding/xml" + "errors" "fmt" "math" "strconv" @@ -257,8 +258,8 @@ func (f *File) GetRowVisible(sheet string, row int) (bool, error) { } // SetRowOutlineLevel provides a function to set outline level number of a -// single row by given worksheet name and Excel row number. For example, -// outline row 2 in Sheet1 to level 1: +// single row by given worksheet name and Excel row number. The value of +// parameter 'level' is 1-7. For example, outline row 2 in Sheet1 to level 1: // // err := f.SetRowOutlineLevel("Sheet1", 2, 1) // @@ -266,6 +267,9 @@ func (f *File) SetRowOutlineLevel(sheet string, row int, level uint8) error { if row < 1 { return newInvalidRowNumberError(row) } + if level > 7 || level < 1 { + return errors.New("invalid outline level") + } xlsx, err := f.workSheetReader(sheet) if err != nil { return err diff --git a/sparkline.go b/sparkline.go index 314ea83..b09dbf4 100644 --- a/sparkline.go +++ b/sparkline.go @@ -429,11 +429,6 @@ func (f *File) AddSparkline(sheet string, opt *SparklineOption) error { return err } for idx, ext := range decodeExtLst.Ext { - // hack: add back missing namespace - decodeExtLst.Ext[idx].XMLNSX14 = decodeExtLst.Ext[idx].X14 - decodeExtLst.Ext[idx].XMLNSX15 = decodeExtLst.Ext[idx].X15 - decodeExtLst.Ext[idx].XMLNSX14 = "" - decodeExtLst.Ext[idx].XMLNSX15 = "" if ext.URI == ExtURISparklineGroups { decodeSparklineGroups := decodeX14SparklineGroups{} _ = xml.Unmarshal([]byte(ext.Content), &decodeSparklineGroups) @@ -458,9 +453,8 @@ func (f *File) AddSparkline(sheet string, opt *SparklineOption) error { } sparklineGroupsBytes, _ := xml.Marshal(groups) extLst := xlsxWorksheetExt{ - XMLNSX14: NameSpaceSpreadSheetX14, - URI: ExtURISparklineGroups, - Content: string(sparklineGroupsBytes), + URI: ExtURISparklineGroups, + Content: string(sparklineGroupsBytes), } extBytes, _ := xml.Marshal(extLst) ws.ExtLst.Ext = string(extBytes) diff --git a/test/Book1.xlsx b/test/Book1.xlsx index 78431dc..d5a0591 100644 Binary files a/test/Book1.xlsx and b/test/Book1.xlsx differ diff --git a/xmlChart.go b/xmlChart.go index bb4b4bc..19e86e2 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -293,9 +293,10 @@ type cAutoTitleDeleted struct { type cView3D struct { RotX *attrValInt `xml:"rotX"` RotY *attrValInt `xml:"rotY"` + RAngAx *attrValInt `xml:"rAngAx"` DepthPercent *attrValInt `xml:"depthPercent"` Perspective *attrValInt `xml:"perspective"` - RAngAx *attrValInt `xml:"rAngAx"` + ExtLst *xlsxExtLst `xml:"extLst"` } // cPlotArea directly maps the plotArea element. This element specifies the diff --git a/xmlStyles.go b/xmlStyles.go index 7e02d6e..16a89ab 100644 --- a/xmlStyles.go +++ b/xmlStyles.go @@ -85,9 +85,6 @@ type xlsxFonts struct { // xlsxFont directly maps the font element. This element defines the // properties for one of the fonts used in this workbook. type xlsxFont struct { - Name *attrValString `xml:"name"` - Charset *attrValInt `xml:"charset"` - Family *attrValInt `xml:"family"` B *bool `xml:"b,omitempty"` I *bool `xml:"i,omitempty"` Strike *bool `xml:"strike,omitempty"` @@ -95,9 +92,12 @@ type xlsxFont struct { Shadow *bool `xml:"shadow,omitempty"` Condense *bool `xml:"condense,omitempty"` Extend *bool `xml:"extend,omitempty"` - Color *xlsxColor `xml:"color"` - Sz *attrValFloat `xml:"sz"` U *attrValString `xml:"u"` + Sz *attrValFloat `xml:"sz"` + Color *xlsxColor `xml:"color"` + Name *attrValString `xml:"name"` + Family *attrValInt `xml:"family"` + Charset *attrValInt `xml:"charset"` Scheme *attrValString `xml:"scheme"` } diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 7e8cfde..fa07974 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -629,13 +629,9 @@ type xlsxLegacyDrawing struct { // xlsxWorksheetExt directly maps the ext element in the worksheet. type xlsxWorksheetExt struct { - XMLName xml.Name `xml:"ext"` - XMLNSX14 string `xml:"xmlns:x14,attr,omitempty"` - XMLNSX15 string `xml:"xmlns:x15,attr,omitempty"` - X14 string `xml:"x14,attr,omitempty"` - X15 string `xml:"x15,attr,omitempty"` - URI string `xml:"uri,attr"` - Content string `xml:",innerxml"` + XMLName xml.Name `xml:"ext"` + URI string `xml:"uri,attr"` + Content string `xml:",innerxml"` } // decodeWorksheetExt directly maps the ext element. -- cgit v1.2.1 From 475fbf3856dd83c4813874570ae5ae2cb48ed421 Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 25 Sep 2019 11:12:16 +0800 Subject: Create SECURITY.md --- SECURITY.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..9d032de --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,9 @@ +# Security Policy + +## Supported Versions + +We will dive into any security-related issue as long as your Excelize version is still supported by us. When reporting an issue, include as much information as possible, but no need to fill fancy forms or answer tedious questions. Just tell us what you found, how to reproduce it, and any concerns you have about it. We will respond as soon as possible and follow up with any missing information. + +## Reporting a Vulnerability + +Please e-mail us directly at `xuri.me@gmail.com` or use the security issue template on GitHub. In general, public disclosure is made after the issue has been fully identified and a patch is ready to be released. A security issue gets the highest priority assigned and a reply regarding the vulnerability is given within a typical 24 hours. Thank you! -- cgit v1.2.1 From eb520ae27757d4bca276fa2894bf461d75df9b37 Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 26 Sep 2019 22:28:14 +0800 Subject: Improve compatibility for charts --- chart.go | 83 ++++++++++++----------------------------------------------- chart_test.go | 2 +- col.go | 2 +- picture.go | 2 +- shape.go | 13 ++++++---- xmlChart.go | 2 +- 6 files changed, 29 insertions(+), 75 deletions(-) diff --git a/chart.go b/chart.go index 7db7eee..7d40405 100644 --- a/chart.go +++ b/chart.go @@ -179,59 +179,11 @@ var ( Contour: 0, WireframeContour: 0, } - chartView3DDepthPercent = map[string]int{ - Area: 100, - AreaStacked: 100, - AreaPercentStacked: 100, - Area3D: 100, - Area3DStacked: 100, - Area3DPercentStacked: 100, - Bar: 100, - BarStacked: 100, - BarPercentStacked: 100, - Bar3DClustered: 100, - Bar3DStacked: 100, - Bar3DPercentStacked: 100, - Bar3DConeClustered: 100, - Bar3DConeStacked: 100, - Bar3DConePercentStacked: 100, - Bar3DPyramidClustered: 100, - Bar3DPyramidStacked: 100, - Bar3DPyramidPercentStacked: 100, - Bar3DCylinderClustered: 100, - Bar3DCylinderStacked: 100, - Bar3DCylinderPercentStacked: 100, - Col: 100, - ColStacked: 100, - ColPercentStacked: 100, - Col3D: 100, - Col3DClustered: 100, - Col3DStacked: 100, - Col3DPercentStacked: 100, - Col3DCone: 100, - Col3DConeClustered: 100, - Col3DConeStacked: 100, - Col3DConePercentStacked: 100, - Col3DPyramid: 100, - Col3DPyramidClustered: 100, - Col3DPyramidStacked: 100, - Col3DPyramidPercentStacked: 100, - Col3DCylinder: 100, - Col3DCylinderClustered: 100, - Col3DCylinderStacked: 100, - Col3DCylinderPercentStacked: 100, - Doughnut: 100, - Line: 100, - Pie: 100, - Pie3D: 100, - Radar: 100, - Scatter: 100, - Surface3D: 100, - WireframeSurface3D: 100, - Contour: 100, - WireframeContour: 100, - Bubble: 100, - Bubble3D: 100, + plotAreaChartOverlap = map[string]int{ + BarStacked: 100, + BarPercentStacked: 100, + ColStacked: 100, + ColPercentStacked: 100, } chartView3DPerspective = map[string]int{ Contour: 0, @@ -842,11 +794,10 @@ func (f *File) addChart(formatSet *formatChart) { }, }, View3D: &cView3D{ - RotX: &attrValInt{Val: chartView3DRotX[formatSet.Type]}, - RotY: &attrValInt{Val: chartView3DRotY[formatSet.Type]}, - DepthPercent: &attrValInt{Val: chartView3DDepthPercent[formatSet.Type]}, - Perspective: &attrValInt{Val: chartView3DPerspective[formatSet.Type]}, - RAngAx: &attrValInt{Val: chartView3DRAngAx[formatSet.Type]}, + RotX: &attrValInt{Val: chartView3DRotX[formatSet.Type]}, + RotY: &attrValInt{Val: chartView3DRotY[formatSet.Type]}, + Perspective: &attrValInt{Val: chartView3DPerspective[formatSet.Type]}, + RAngAx: &attrValInt{Val: chartView3DRAngAx[formatSet.Type]}, }, Floor: &cThicknessSpPr{ Thickness: &attrValInt{Val: 0}, @@ -980,6 +931,7 @@ func (f *File) drawBaseChart(formatSet *formatChart) *cPlotArea { {Val: 754001152}, {Val: 753999904}, }, + Overlap: &attrValInt{Val: 100}, } var ok bool if c.BarDir.Val, ok = plotAreaChartBarDir[formatSet.Type]; !ok { @@ -988,8 +940,8 @@ func (f *File) drawBaseChart(formatSet *formatChart) *cPlotArea { if c.Grouping.Val, ok = plotAreaChartGrouping[formatSet.Type]; !ok { c.Grouping = nil } - if strings.HasSuffix(formatSet.Type, "Stacked") { - c.Overlap = &attrValInt{Val: 100} + if c.Overlap.Val, ok = plotAreaChartOverlap[formatSet.Type]; !ok { + c.Overlap = nil } catAx := f.drawPlotAreaCatAx(formatSet) valAx := f.drawPlotAreaValAx(formatSet) @@ -1485,7 +1437,7 @@ func (f *File) drawChartSeriesCat(v formatChartSeries, formatSet *formatChart) * F: v.Categories, }, } - chartSeriesCat := map[string]*cCat{Scatter: nil} + chartSeriesCat := map[string]*cCat{Scatter: nil, Bubble: nil, Bubble3D: nil} if _, ok := chartSeriesCat[formatSet.Type]; ok { return nil } @@ -1500,7 +1452,7 @@ func (f *File) drawChartSeriesVal(v formatChartSeries, formatSet *formatChart) * F: v.Values, }, } - chartSeriesVal := map[string]*cVal{Scatter: nil} + chartSeriesVal := map[string]*cVal{Scatter: nil, Bubble: nil, Bubble3D: nil} if _, ok := chartSeriesVal[formatSet.Type]; ok { return nil } @@ -1783,7 +1735,6 @@ func (f *File) drawPlotAreaTxPr() *cTxPr { // deserialization, two different structures: decodeWsDr and encodeWsDr are // defined. func (f *File) drawingParser(path string) (*xlsxWsDr, int) { - cNvPrID := 1 if f.Drawings[path] == nil { content := xlsxWsDr{} content.A = NameSpaceDrawingML @@ -1793,7 +1744,6 @@ func (f *File) drawingParser(path string) (*xlsxWsDr, int) { decodeWsDr := decodeWsDr{} _ = xml.Unmarshal(namespaceStrictToTransitional(f.readXML(path)), &decodeWsDr) content.R = decodeWsDr.R - cNvPrID = len(decodeWsDr.OneCellAnchor) + len(decodeWsDr.TwoCellAnchor) + 1 for _, v := range decodeWsDr.OneCellAnchor { content.OneCellAnchor = append(content.OneCellAnchor, &xdrCellAnchor{ EditAs: v.EditAs, @@ -1809,7 +1759,8 @@ func (f *File) drawingParser(path string) (*xlsxWsDr, int) { } f.Drawings[path] = &content } - return f.Drawings[path], cNvPrID + wsDr := f.Drawings[path] + return wsDr, len(wsDr.OneCellAnchor) + len(wsDr.TwoCellAnchor) + 2 } // addDrawingChart provides a function to add chart graphic frame by given @@ -1845,7 +1796,7 @@ func (f *File) addDrawingChart(sheet, drawingXML, cell string, width, height, rI graphicFrame := xlsxGraphicFrame{ NvGraphicFramePr: xlsxNvGraphicFramePr{ CNvPr: &xlsxCNvPr{ - ID: len(content.OneCellAnchor) + len(content.TwoCellAnchor) + 2, + ID: cNvPrID, Name: "Chart " + strconv.Itoa(cNvPrID), }, }, diff --git a/chart_test.go b/chart_test.go index c0bae33..932e873 100644 --- a/chart_test.go +++ b/chart_test.go @@ -126,7 +126,7 @@ func TestAddChart(t *testing.T) { assert.NoError(t, f.AddChart("Sheet1", "X1", `{"type":"colStacked","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"2D Stacked Column 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"}`)) assert.NoError(t, f.AddChart("Sheet1", "P16", `{"type":"colPercentStacked","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"100% Stacked Column 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"}`)) assert.NoError(t, f.AddChart("Sheet1", "X16", `{"type":"col3DClustered","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"3D Clustered Column 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"}`)) - assert.NoError(t, f.AddChart("Sheet1", "P30", `{"type":"col3DStacked","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"3D 100% Stacked Bar 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"}`)) + assert.NoError(t, f.AddChart("Sheet1", "P30", `{"type":"col3DStacked","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"3D Stacked Column 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"}`)) assert.NoError(t, f.AddChart("Sheet1", "X30", `{"type":"col3DPercentStacked","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"3D 100% Stacked Column 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"}`)) assert.NoError(t, f.AddChart("Sheet1", "AF1", `{"type":"col3DConeStacked","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"3D Column Cone Stacked 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"}`)) assert.NoError(t, f.AddChart("Sheet1", "AF16", `{"type":"col3DConeClustered","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"3D Column Cone Clustered 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"}`)) diff --git a/col.go b/col.go index be08c29..5d4e764 100644 --- a/col.go +++ b/col.go @@ -113,7 +113,7 @@ func (f *File) GetColOutlineLevel(sheet, col string) (uint8, error) { for c := range xlsx.Cols.Col { colData := &xlsx.Cols.Col[c] if colData.Min <= colNum && colNum <= colData.Max { - level = colData.OutlineLevel + 1 + level = colData.OutlineLevel } } return level, err diff --git a/picture.go b/picture.go index 4470fa1..ff40863 100644 --- a/picture.go +++ b/picture.go @@ -272,7 +272,7 @@ func (f *File) addDrawingPicture(sheet, drawingXML, cell, file string, width, he twoCellAnchor.To = &to pic := xlsxPic{} pic.NvPicPr.CNvPicPr.PicLocks.NoChangeAspect = formatSet.NoChangeAspect - pic.NvPicPr.CNvPr.ID = len(content.OneCellAnchor) + len(content.TwoCellAnchor) + 2 + pic.NvPicPr.CNvPr.ID = cNvPrID pic.NvPicPr.CNvPr.Descr = file pic.NvPicPr.CNvPr.Name = "Picture " + strconv.Itoa(cNvPrID) if hyperlinkRID != 0 { diff --git a/shape.go b/shape.go index e6a2ff3..f284e43 100644 --- a/shape.go +++ b/shape.go @@ -411,11 +411,6 @@ func (f *File) addDrawingShape(sheet, drawingXML, cell string, formatSet *format U: u, Sz: p.Font.Size * 100, Latin: &aLatin{Typeface: p.Font.Family}, - SolidFill: &aSolidFill{ - SrgbClr: &attrValString{ - Val: strings.Replace(strings.ToUpper(p.Font.Color), "#", "", -1), - }, - }, }, T: text, }, @@ -423,6 +418,14 @@ func (f *File) addDrawingShape(sheet, drawingXML, cell string, formatSet *format Lang: "en-US", }, } + srgbClr := strings.Replace(strings.ToUpper(p.Font.Color), "#", "", -1) + if len(srgbClr) == 6 { + paragraph.R.RPr.SolidFill = &aSolidFill{ + SrgbClr: &attrValString{ + Val: srgbClr, + }, + } + } shape.TxBody.P = append(shape.TxBody.P, paragraph) } twoCellAnchor.Sp = &shape diff --git a/xmlChart.go b/xmlChart.go index 19e86e2..69e119a 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -332,8 +332,8 @@ type cCharts struct { VaryColors *attrValBool `xml:"varyColors"` Wireframe *attrValBool `xml:"wireframe"` Ser *[]cSer `xml:"ser"` - Shape *attrValString `xml:"shape"` DLbls *cDLbls `xml:"dLbls"` + Shape *attrValString `xml:"shape"` HoleSize *attrValInt `xml:"holeSize"` Smooth *attrValBool `xml:"smooth"` Overlap *attrValInt `xml:"overlap"` -- cgit v1.2.1 From babfeb6b57ad3e63f68f5e031869efc54c9cfe0b Mon Sep 17 00:00:00 2001 From: jaby Date: Mon, 30 Sep 2019 14:37:52 +0200 Subject: Add missing ShowZeros SheetViewOption implementation --- sheetview.go | 16 ++++++++++++++-- sheetview_test.go | 14 ++++++++++++++ xmlWorksheet.go | 2 +- 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/sheetview.go b/sheetview.go index 09f5789..8a5091f 100644 --- a/sheetview.go +++ b/sheetview.go @@ -51,14 +51,18 @@ type ( // visible cell Location of the top left visible cell in the bottom right // pane (when in Left-to-Right mode). TopLeftCell string + // ShowZeros is a SheetViewOption. It specifies a flag indicating + // whether to "show a zero in cells that have zero value". + // When using a formula to reference another cell which is empty, the referenced value becomes 0 + // when the flag is true. (Default setting is true.) + ShowZeros bool + /* TODO // ShowWhiteSpace is a SheetViewOption. It specifies a flag indicating // whether page layout view shall display margins. False means do not display // left, right, top (header), and bottom (footer) margins (even when there is // data in the header or footer). ShowWhiteSpace bool - // ShowZeros is a SheetViewOption. - ShowZeros bool // WindowProtection is a SheetViewOption. WindowProtection bool */ @@ -106,6 +110,14 @@ func (o *ShowGridLines) getSheetViewOption(view *xlsxSheetView) { *o = ShowGridLines(defaultTrue(view.ShowGridLines)) // Excel default: true } +func (o ShowZeros) setSheetViewOption(view *xlsxSheetView) { + view.ShowZeros = boolPtr(bool(o)) +} + +func (o *ShowZeros) getSheetViewOption(view *xlsxSheetView) { + *o = ShowZeros(defaultTrue(view.ShowZeros)) // Excel default: true +} + func (o ShowRowColHeaders) setSheetViewOption(view *xlsxSheetView) { view.ShowRowColHeaders = boolPtr(bool(o)) } diff --git a/sheetview_test.go b/sheetview_test.go index 2e697b8..e45b8ce 100644 --- a/sheetview_test.go +++ b/sheetview_test.go @@ -95,6 +95,7 @@ func ExampleFile_GetSheetViewOptions() { rightToLeft excelize.RightToLeft showFormulas excelize.ShowFormulas showGridLines excelize.ShowGridLines + showZeros excelize.ShowZeros showRowColHeaders excelize.ShowRowColHeaders zoomScale excelize.ZoomScale topLeftCell excelize.TopLeftCell @@ -105,6 +106,7 @@ func ExampleFile_GetSheetViewOptions() { &rightToLeft, &showFormulas, &showGridLines, + &showZeros, &showRowColHeaders, &zoomScale, &topLeftCell, @@ -117,6 +119,7 @@ func ExampleFile_GetSheetViewOptions() { fmt.Println("- rightToLeft:", rightToLeft) fmt.Println("- showFormulas:", showFormulas) fmt.Println("- showGridLines:", showGridLines) + fmt.Println("- showZeros:", showZeros) fmt.Println("- showRowColHeaders:", showRowColHeaders) fmt.Println("- zoomScale:", zoomScale) fmt.Println("- topLeftCell:", `"`+topLeftCell+`"`) @@ -137,8 +140,17 @@ func ExampleFile_GetSheetViewOptions() { panic(err) } + if err := f.SetSheetViewOptions(sheet, 0, excelize.ShowZeros(false)); err != nil { + panic(err) + } + + if err := f.GetSheetViewOptions(sheet, 0, &showZeros); err != nil { + panic(err) + } + fmt.Println("After change:") fmt.Println("- showGridLines:", showGridLines) + fmt.Println("- showZeros:", showZeros) fmt.Println("- topLeftCell:", topLeftCell) // Output: @@ -147,11 +159,13 @@ func ExampleFile_GetSheetViewOptions() { // - rightToLeft: false // - showFormulas: false // - showGridLines: true + // - showZeros: true // - showRowColHeaders: true // - zoomScale: 0 // - topLeftCell: "" // After change: // - showGridLines: false + // - showZeros: false // - topLeftCell: B2 } diff --git a/xmlWorksheet.go b/xmlWorksheet.go index fa07974..c78d3ef 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -167,7 +167,7 @@ type xlsxSheetView struct { ShowFormulas bool `xml:"showFormulas,attr,omitempty"` ShowGridLines *bool `xml:"showGridLines,attr"` ShowRowColHeaders *bool `xml:"showRowColHeaders,attr"` - ShowZeros bool `xml:"showZeros,attr,omitempty"` + ShowZeros *bool `xml:"showZeros,attr,omitempty"` RightToLeft bool `xml:"rightToLeft,attr,omitempty"` TabSelected bool `xml:"tabSelected,attr,omitempty"` ShowWhiteSpace *bool `xml:"showWhiteSpace,attr"` -- cgit v1.2.1 From 810139f5fc46b1002c0998379b18af3d2feffbb7 Mon Sep 17 00:00:00 2001 From: heiy <287789299@qq.com> Date: Thu, 10 Oct 2019 20:04:33 +0800 Subject: solve ending space missing --- cell.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cell.go b/cell.go index f9868de..ab42b72 100644 --- a/cell.go +++ b/cell.go @@ -229,7 +229,7 @@ func (f *File) SetCellStr(sheet, axis, value string) error { value = value[0:32767] } // Leading space(s) character detection. - if len(value) > 0 && value[0] == 32 { + if len(value) > 0 && (value[0] == 32 || value[len(value)-1] == 32) { cellData.XMLSpace = xml.Attr{ Name: xml.Name{Space: NameSpaceXML, Local: "space"}, Value: "preserve", -- cgit v1.2.1 From 2d21b5b50f30ae9868b2f8b1f7299ceefcf87fd2 Mon Sep 17 00:00:00 2001 From: streboryaj Date: Tue, 15 Oct 2019 09:26:08 -0500 Subject: Added accessors for Getting/Setting Page Margins (#497) * Added accessors for Getting/Setting Page Margins * Added test cases --- sheet.go | 159 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ sheet_test.go | 62 ++++++++++++++++++++++ xmlWorksheet.go | 10 ++++ 3 files changed, 231 insertions(+) diff --git a/sheet.go b/sheet.go index 951baf9..ce3e645 100644 --- a/sheet.go +++ b/sheet.go @@ -1401,3 +1401,162 @@ func makeContiguousColumns(xlsx *xlsxWorksheet, fromRow, toRow, colCount int) { fillColumns(rowData, colCount, fromRow) } } + +type ( + PageMarginBottom float64 + PageMarginFooter float64 + PageMarginHeader float64 + PageMarginLeft float64 + PageMarginRight float64 + PageMarginTop float64 +) + +// setPageMargins provides a method to set the bottom margin for the worksheet. +func (p PageMarginBottom) setPageMargins(ps *xlsxPageMargins) { + ps.Bottom = float64(p) +} + +// setPageMargins provides a method to get the bottom margin for the worksheet. +func (o *PageMarginBottom) getPageMargins(ps *xlsxPageMargins) { + // Excel default: portrait + if ps == nil || ps.Bottom == 0 { + *o = 0.75 + return + } + *o = PageMarginBottom(ps.Bottom) +} + +// setPageMargins provides a method to set the Footer margin for the worksheet. +func (p PageMarginFooter) setPageMargins(ps *xlsxPageMargins) { + ps.Footer = float64(p) +} + +// setPageMargins provides a method to get the Footer margin for the worksheet. +func (o *PageMarginFooter) getPageMargins(ps *xlsxPageMargins) { + // Excel default: portrait + if ps == nil || ps.Footer == 0 { + *o = 0.3 + return + } + *o = PageMarginFooter(ps.Footer) +} + +// setPageMargins provides a method to set the Header margin for the worksheet. +func (p PageMarginHeader) setPageMargins(ps *xlsxPageMargins) { + ps.Header = float64(p) +} + +// setPageMargins provides a method to get the Header margin for the worksheet. +func (o *PageMarginHeader) getPageMargins(ps *xlsxPageMargins) { + // Excel default: portrait + if ps == nil || ps.Header == 0 { + *o = 0.3 + return + } + *o = PageMarginHeader(ps.Header) +} + +// setPageMargins provides a method to set the left margin for the worksheet. +func (p PageMarginLeft) setPageMargins(ps *xlsxPageMargins) { + ps.Left = float64(p) +} + +// setPageMargins provides a method to get the left margin for the worksheet. +func (o *PageMarginLeft) getPageMargins(ps *xlsxPageMargins) { + // Excel default: portrait + if ps == nil || ps.Left == 0 { + *o = 0.7 + return + } + *o = PageMarginLeft(ps.Left) +} + +// setPageMargins provides a method to set the right margin for the worksheet. +func (p PageMarginRight) setPageMargins(ps *xlsxPageMargins) { + ps.Right = float64(p) +} + +// setPageMargins provides a method to get the right margin for the worksheet. +func (o *PageMarginRight) getPageMargins(ps *xlsxPageMargins) { + // Excel default: portrait + if ps == nil || ps.Right == 0 { + *o = 0.7 + return + } + *o = PageMarginRight(ps.Right) +} + +// setPageMargins provides a method to set the top margin for the worksheet. +func (p PageMarginTop) setPageMargins(ps *xlsxPageMargins) { + ps.Top = float64(p) +} + +// setPageMargins provides a method to get the top margin for the worksheet. +func (o *PageMarginTop) getPageMargins(ps *xlsxPageMargins) { + // Excel default: portrait + if ps == nil || ps.Top == 0 { + *o = 0.75 + return + } + *o = PageMarginTop(ps.Top) +} + +// PageMarginsOptions is an option of a page margin of a worksheet. See +// SetPageMargins(). +type PageMarginsOptions interface { + setPageMargins(layout *xlsxPageMargins) +} + +// PageMarginsOptionsPtr is a writable PageMarginsOptions. See GetPageMargins(). +type PageMarginsOptionsPtr interface { + PageMarginsOptions + getPageMargins(layout *xlsxPageMargins) +} + +// SetPageMargins provides a function to set worksheet page lmargins. +// +// Available options: +// PageMarginBotom(float64) +// PageMarginFooter(float64) +// PageMarginHeader(float64) +// PageMarginLeft(float64) +// PageMarginRightfloat64) +// PageMarginTop(float64) +func (f *File) SetPageMargins(sheet string, opts ...PageMarginsOptions) error { + s, err := f.workSheetReader(sheet) + if err != nil { + return err + } + ps := s.PageMargins + if ps == nil { + ps = new(xlsxPageMargins) + s.PageMargins = ps + } + + for _, opt := range opts { + opt.setPageMargins(ps) + } + return err +} + +// GetPageMargins provides a function to get worksheet page margins. +// +// Available options: +// PageMarginBotom(float64) +// PageMarginFooter(float64) +// PageMarginHeader(float64) +// PageMarginLeft(float64) +// PageMarginRightfloat64) +// PageMarginTop(float64) +func (f *File) GetPageMargins(sheet string, opts ...PageMarginsOptionsPtr) error { + s, err := f.workSheetReader(sheet) + if err != nil { + return err + } + ps := s.PageMargins + + for _, opt := range opts { + opt.getPageMargins(ps) + } + return err +} diff --git a/sheet_test.go b/sheet_test.go index 5179793..6bfa7dc 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -247,3 +247,65 @@ func TestGetSheetMap(t *testing.T) { } assert.Equal(t, len(sheetMap), 2) } + +func TestPageMarginsOption(t *testing.T) { + const sheet = "Sheet1" + + testData := []struct { + container excelize.PageMarginsOptionsPtr + nonDefault excelize.PageMarginsOptions + }{ + {new(excelize.PageMarginTop), excelize.PageMarginTop(1.0)}, + {new(excelize.PageMarginBottom), excelize.PageMarginBottom(1.0)}, + {new(excelize.PageMarginLeft), excelize.PageMarginLeft(1.0)}, + {new(excelize.PageMarginRight), excelize.PageMarginRight(1.0)}, + {new(excelize.PageMarginHeader), excelize.PageMarginHeader(1.0)}, + {new(excelize.PageMarginFooter), excelize.PageMarginFooter(1.0)}, + } + + for i, test := range testData { + t.Run(fmt.Sprintf("TestData%d", i), func(t *testing.T) { + + opt := test.nonDefault + t.Logf("option %T", opt) + + def := deepcopy.Copy(test.container).(excelize.PageMarginsOptionsPtr) + val1 := deepcopy.Copy(def).(excelize.PageMarginsOptionsPtr) + val2 := deepcopy.Copy(def).(excelize.PageMarginsOptionsPtr) + + f := excelize.NewFile() + // Get the default value + assert.NoError(t, f.GetPageMargins(sheet, def), opt) + // Get again and check + assert.NoError(t, f.GetPageMargins(sheet, val1), opt) + if !assert.Equal(t, val1, def, opt) { + t.FailNow() + } + // Set the same value + assert.NoError(t, f.SetPageMargins(sheet, val1), opt) + // Get again and check + assert.NoError(t, f.GetPageMargins(sheet, val1), opt) + if !assert.Equal(t, val1, def, "%T: value should not have changed", opt) { + t.FailNow() + } + // Set a different value + assert.NoError(t, f.SetPageMargins(sheet, test.nonDefault), opt) + assert.NoError(t, f.GetPageMargins(sheet, val1), opt) + // Get again and compare + assert.NoError(t, f.GetPageMargins(sheet, val2), opt) + if !assert.Equal(t, val1, val2, "%T: value should not have changed", opt) { + t.FailNow() + } + // Value should not be the same as the default + if !assert.NotEqual(t, def, val1, "%T: value should have changed from default", opt) { + t.FailNow() + } + // Restore the default value + assert.NoError(t, f.SetPageMargins(sheet, def), opt) + assert.NoError(t, f.GetPageMargins(sheet, val1), opt) + if !assert.Equal(t, def, val1) { + t.FailNow() + } + }) + } +} diff --git a/xmlWorksheet.go b/xmlWorksheet.go index c78d3ef..96ca235 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -801,3 +801,13 @@ type FormatHeaderFooter struct { FirstFooter string FirstHeader string } + +// FormatPageMargins directly maps the settings of page margins +type FormatPageMargins struct { + Bottom string + Footer string + Header string + Left string + Right string + Top string +} -- cgit v1.2.1 From 2e791fa433def282ee2e7a5049a46fc4a76796cf Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 16 Oct 2019 01:03:29 +0800 Subject: Optimize code of Getting/Setting Page Margins --- cell.go | 2 +- rows_test.go | 5 ++ sheet.go | 159 ---------------------------------------------------- sheet_test.go | 62 --------------------- sheetpr.go | 166 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ sheetpr_test.go | 161 +++++++++++++++++++++++++++++++++++++++++++++++++++++ templates.go | 2 +- xmlPivotCache.go | 4 +- xmlPivotTable.go | 4 +- 9 files changed, 338 insertions(+), 227 deletions(-) diff --git a/cell.go b/cell.go index ab42b72..a25f2e4 100644 --- a/cell.go +++ b/cell.go @@ -228,7 +228,7 @@ func (f *File) SetCellStr(sheet, axis, value string) error { if len(value) > 32767 { value = value[0:32767] } - // Leading space(s) character detection. + // Leading and ending space(s) character detection. if len(value) > 0 && (value[0] == 32 || value[len(value)-1] == 32) { cellData.XMLSpace = xml.Attr{ Name: xml.Name{Space: NameSpaceXML, Local: "space"}, diff --git a/rows_test.go b/rows_test.go index d52c635..a99a594 100644 --- a/rows_test.go +++ b/rows_test.go @@ -669,6 +669,11 @@ func TestDuplicateRowInvalidRownum(t *testing.T) { } } +func TestErrSheetNotExistError(t *testing.T) { + err := ErrSheetNotExist{SheetName: "Sheet1"} + assert.EqualValues(t, err.Error(), "Sheet Sheet1 is not exist") +} + func BenchmarkRows(b *testing.B) { for i := 0; i < b.N; i++ { f, _ := OpenFile(filepath.Join("test", "Book1.xlsx")) diff --git a/sheet.go b/sheet.go index ce3e645..951baf9 100644 --- a/sheet.go +++ b/sheet.go @@ -1401,162 +1401,3 @@ func makeContiguousColumns(xlsx *xlsxWorksheet, fromRow, toRow, colCount int) { fillColumns(rowData, colCount, fromRow) } } - -type ( - PageMarginBottom float64 - PageMarginFooter float64 - PageMarginHeader float64 - PageMarginLeft float64 - PageMarginRight float64 - PageMarginTop float64 -) - -// setPageMargins provides a method to set the bottom margin for the worksheet. -func (p PageMarginBottom) setPageMargins(ps *xlsxPageMargins) { - ps.Bottom = float64(p) -} - -// setPageMargins provides a method to get the bottom margin for the worksheet. -func (o *PageMarginBottom) getPageMargins(ps *xlsxPageMargins) { - // Excel default: portrait - if ps == nil || ps.Bottom == 0 { - *o = 0.75 - return - } - *o = PageMarginBottom(ps.Bottom) -} - -// setPageMargins provides a method to set the Footer margin for the worksheet. -func (p PageMarginFooter) setPageMargins(ps *xlsxPageMargins) { - ps.Footer = float64(p) -} - -// setPageMargins provides a method to get the Footer margin for the worksheet. -func (o *PageMarginFooter) getPageMargins(ps *xlsxPageMargins) { - // Excel default: portrait - if ps == nil || ps.Footer == 0 { - *o = 0.3 - return - } - *o = PageMarginFooter(ps.Footer) -} - -// setPageMargins provides a method to set the Header margin for the worksheet. -func (p PageMarginHeader) setPageMargins(ps *xlsxPageMargins) { - ps.Header = float64(p) -} - -// setPageMargins provides a method to get the Header margin for the worksheet. -func (o *PageMarginHeader) getPageMargins(ps *xlsxPageMargins) { - // Excel default: portrait - if ps == nil || ps.Header == 0 { - *o = 0.3 - return - } - *o = PageMarginHeader(ps.Header) -} - -// setPageMargins provides a method to set the left margin for the worksheet. -func (p PageMarginLeft) setPageMargins(ps *xlsxPageMargins) { - ps.Left = float64(p) -} - -// setPageMargins provides a method to get the left margin for the worksheet. -func (o *PageMarginLeft) getPageMargins(ps *xlsxPageMargins) { - // Excel default: portrait - if ps == nil || ps.Left == 0 { - *o = 0.7 - return - } - *o = PageMarginLeft(ps.Left) -} - -// setPageMargins provides a method to set the right margin for the worksheet. -func (p PageMarginRight) setPageMargins(ps *xlsxPageMargins) { - ps.Right = float64(p) -} - -// setPageMargins provides a method to get the right margin for the worksheet. -func (o *PageMarginRight) getPageMargins(ps *xlsxPageMargins) { - // Excel default: portrait - if ps == nil || ps.Right == 0 { - *o = 0.7 - return - } - *o = PageMarginRight(ps.Right) -} - -// setPageMargins provides a method to set the top margin for the worksheet. -func (p PageMarginTop) setPageMargins(ps *xlsxPageMargins) { - ps.Top = float64(p) -} - -// setPageMargins provides a method to get the top margin for the worksheet. -func (o *PageMarginTop) getPageMargins(ps *xlsxPageMargins) { - // Excel default: portrait - if ps == nil || ps.Top == 0 { - *o = 0.75 - return - } - *o = PageMarginTop(ps.Top) -} - -// PageMarginsOptions is an option of a page margin of a worksheet. See -// SetPageMargins(). -type PageMarginsOptions interface { - setPageMargins(layout *xlsxPageMargins) -} - -// PageMarginsOptionsPtr is a writable PageMarginsOptions. See GetPageMargins(). -type PageMarginsOptionsPtr interface { - PageMarginsOptions - getPageMargins(layout *xlsxPageMargins) -} - -// SetPageMargins provides a function to set worksheet page lmargins. -// -// Available options: -// PageMarginBotom(float64) -// PageMarginFooter(float64) -// PageMarginHeader(float64) -// PageMarginLeft(float64) -// PageMarginRightfloat64) -// PageMarginTop(float64) -func (f *File) SetPageMargins(sheet string, opts ...PageMarginsOptions) error { - s, err := f.workSheetReader(sheet) - if err != nil { - return err - } - ps := s.PageMargins - if ps == nil { - ps = new(xlsxPageMargins) - s.PageMargins = ps - } - - for _, opt := range opts { - opt.setPageMargins(ps) - } - return err -} - -// GetPageMargins provides a function to get worksheet page margins. -// -// Available options: -// PageMarginBotom(float64) -// PageMarginFooter(float64) -// PageMarginHeader(float64) -// PageMarginLeft(float64) -// PageMarginRightfloat64) -// PageMarginTop(float64) -func (f *File) GetPageMargins(sheet string, opts ...PageMarginsOptionsPtr) error { - s, err := f.workSheetReader(sheet) - if err != nil { - return err - } - ps := s.PageMargins - - for _, opt := range opts { - opt.getPageMargins(ps) - } - return err -} diff --git a/sheet_test.go b/sheet_test.go index 6bfa7dc..5179793 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -247,65 +247,3 @@ func TestGetSheetMap(t *testing.T) { } assert.Equal(t, len(sheetMap), 2) } - -func TestPageMarginsOption(t *testing.T) { - const sheet = "Sheet1" - - testData := []struct { - container excelize.PageMarginsOptionsPtr - nonDefault excelize.PageMarginsOptions - }{ - {new(excelize.PageMarginTop), excelize.PageMarginTop(1.0)}, - {new(excelize.PageMarginBottom), excelize.PageMarginBottom(1.0)}, - {new(excelize.PageMarginLeft), excelize.PageMarginLeft(1.0)}, - {new(excelize.PageMarginRight), excelize.PageMarginRight(1.0)}, - {new(excelize.PageMarginHeader), excelize.PageMarginHeader(1.0)}, - {new(excelize.PageMarginFooter), excelize.PageMarginFooter(1.0)}, - } - - for i, test := range testData { - t.Run(fmt.Sprintf("TestData%d", i), func(t *testing.T) { - - opt := test.nonDefault - t.Logf("option %T", opt) - - def := deepcopy.Copy(test.container).(excelize.PageMarginsOptionsPtr) - val1 := deepcopy.Copy(def).(excelize.PageMarginsOptionsPtr) - val2 := deepcopy.Copy(def).(excelize.PageMarginsOptionsPtr) - - f := excelize.NewFile() - // Get the default value - assert.NoError(t, f.GetPageMargins(sheet, def), opt) - // Get again and check - assert.NoError(t, f.GetPageMargins(sheet, val1), opt) - if !assert.Equal(t, val1, def, opt) { - t.FailNow() - } - // Set the same value - assert.NoError(t, f.SetPageMargins(sheet, val1), opt) - // Get again and check - assert.NoError(t, f.GetPageMargins(sheet, val1), opt) - if !assert.Equal(t, val1, def, "%T: value should not have changed", opt) { - t.FailNow() - } - // Set a different value - assert.NoError(t, f.SetPageMargins(sheet, test.nonDefault), opt) - assert.NoError(t, f.GetPageMargins(sheet, val1), opt) - // Get again and compare - assert.NoError(t, f.GetPageMargins(sheet, val2), opt) - if !assert.Equal(t, val1, val2, "%T: value should not have changed", opt) { - t.FailNow() - } - // Value should not be the same as the default - if !assert.NotEqual(t, def, val1, "%T: value should have changed from default", opt) { - t.FailNow() - } - // Restore the default value - assert.NoError(t, f.SetPageMargins(sheet, def), opt) - assert.NoError(t, f.GetPageMargins(sheet, val1), opt) - if !assert.Equal(t, def, val1) { - t.FailNow() - } - }) - } -} diff --git a/sheetpr.go b/sheetpr.go index a273ac1..086bd3a 100644 --- a/sheetpr.go +++ b/sheetpr.go @@ -191,3 +191,169 @@ func (f *File) GetSheetPrOptions(name string, opts ...SheetPrOptionPtr) error { } return err } + +type ( + // PageMarginBottom specifies the bottom margin for the page. + PageMarginBottom float64 + // PageMarginFooter specifies the footer margin for the page. + PageMarginFooter float64 + // PageMarginHeader specifies the header margin for the page. + PageMarginHeader float64 + // PageMarginLeft specifies the left margin for the page. + PageMarginLeft float64 + // PageMarginRight specifies the right margin for the page. + PageMarginRight float64 + // PageMarginTop specifies the top margin for the page. + PageMarginTop float64 +) + +// setPageMargins provides a method to set the bottom margin for the worksheet. +func (p PageMarginBottom) setPageMargins(pm *xlsxPageMargins) { + pm.Bottom = float64(p) +} + +// setPageMargins provides a method to get the bottom margin for the worksheet. +func (p *PageMarginBottom) getPageMargins(pm *xlsxPageMargins) { + // Excel default: 0.75 + if pm == nil || pm.Bottom == 0 { + *p = 0.75 + return + } + *p = PageMarginBottom(pm.Bottom) +} + +// setPageMargins provides a method to set the footer margin for the worksheet. +func (p PageMarginFooter) setPageMargins(pm *xlsxPageMargins) { + pm.Footer = float64(p) +} + +// setPageMargins provides a method to get the footer margin for the worksheet. +func (p *PageMarginFooter) getPageMargins(pm *xlsxPageMargins) { + // Excel default: 0.3 + if pm == nil || pm.Footer == 0 { + *p = 0.3 + return + } + *p = PageMarginFooter(pm.Footer) +} + +// setPageMargins provides a method to set the header margin for the worksheet. +func (p PageMarginHeader) setPageMargins(pm *xlsxPageMargins) { + pm.Header = float64(p) +} + +// setPageMargins provides a method to get the header margin for the worksheet. +func (p *PageMarginHeader) getPageMargins(pm *xlsxPageMargins) { + // Excel default: 0.3 + if pm == nil || pm.Header == 0 { + *p = 0.3 + return + } + *p = PageMarginHeader(pm.Header) +} + +// setPageMargins provides a method to set the left margin for the worksheet. +func (p PageMarginLeft) setPageMargins(pm *xlsxPageMargins) { + pm.Left = float64(p) +} + +// setPageMargins provides a method to get the left margin for the worksheet. +func (p *PageMarginLeft) getPageMargins(pm *xlsxPageMargins) { + // Excel default: 0.7 + if pm == nil || pm.Left == 0 { + *p = 0.7 + return + } + *p = PageMarginLeft(pm.Left) +} + +// setPageMargins provides a method to set the right margin for the worksheet. +func (p PageMarginRight) setPageMargins(pm *xlsxPageMargins) { + pm.Right = float64(p) +} + +// setPageMargins provides a method to get the right margin for the worksheet. +func (p *PageMarginRight) getPageMargins(pm *xlsxPageMargins) { + // Excel default: 0.7 + if pm == nil || pm.Right == 0 { + *p = 0.7 + return + } + *p = PageMarginRight(pm.Right) +} + +// setPageMargins provides a method to set the top margin for the worksheet. +func (p PageMarginTop) setPageMargins(pm *xlsxPageMargins) { + pm.Top = float64(p) +} + +// setPageMargins provides a method to get the top margin for the worksheet. +func (p *PageMarginTop) getPageMargins(pm *xlsxPageMargins) { + // Excel default: 0.75 + if pm == nil || pm.Top == 0 { + *p = 0.75 + return + } + *p = PageMarginTop(pm.Top) +} + +// PageMarginsOptions is an option of a page margin of a worksheet. See +// SetPageMargins(). +type PageMarginsOptions interface { + setPageMargins(layout *xlsxPageMargins) +} + +// PageMarginsOptionsPtr is a writable PageMarginsOptions. See +// GetPageMargins(). +type PageMarginsOptionsPtr interface { + PageMarginsOptions + getPageMargins(layout *xlsxPageMargins) +} + +// SetPageMargins provides a function to set worksheet page margins. +// +// Available options: +// PageMarginBotom(float64) +// PageMarginFooter(float64) +// PageMarginHeader(float64) +// PageMarginLeft(float64) +// PageMarginRight(float64) +// PageMarginTop(float64) +func (f *File) SetPageMargins(sheet string, opts ...PageMarginsOptions) error { + s, err := f.workSheetReader(sheet) + if err != nil { + return err + } + pm := s.PageMargins + if pm == nil { + pm = new(xlsxPageMargins) + s.PageMargins = pm + } + + for _, opt := range opts { + opt.setPageMargins(pm) + } + return err +} + +// GetPageMargins provides a function to get worksheet page margins. +// +// Available options: +// PageMarginBotom(float64) +// PageMarginFooter(float64) +// PageMarginHeader(float64) +// PageMarginLeft(float64) +// PageMarginRight(float64) +// PageMarginTop(float64) +func (f *File) GetPageMargins(sheet string, opts ...PageMarginsOptionsPtr) error { + s, err := f.workSheetReader(sheet) + if err != nil { + return err + } + pm := s.PageMargins + + for _, opt := range opts { + opt.getPageMargins(pm) + } + return err +} diff --git a/sheetpr_test.go b/sheetpr_test.go index 97a314c..d1ae2f1 100644 --- a/sheetpr_test.go +++ b/sheetpr_test.go @@ -146,3 +146,164 @@ func TestSheetPrOptions(t *testing.T) { }) } } + +func TestSetSheetrOptions(t *testing.T) { + f := excelize.NewFile() + // Test SetSheetrOptions on not exists worksheet. + assert.EqualError(t, f.SetSheetPrOptions("SheetN"), "sheet SheetN is not exist") +} + +func TestGetSheetPrOptions(t *testing.T) { + f := excelize.NewFile() + // Test GetSheetPrOptions on not exists worksheet. + assert.EqualError(t, f.GetSheetPrOptions("SheetN"), "sheet SheetN is not exist") +} + +var _ = []excelize.PageMarginsOptions{ + excelize.PageMarginBottom(1.0), + excelize.PageMarginFooter(1.0), + excelize.PageMarginHeader(1.0), + excelize.PageMarginLeft(1.0), + excelize.PageMarginRight(1.0), + excelize.PageMarginTop(1.0), +} + +var _ = []excelize.PageMarginsOptionsPtr{ + (*excelize.PageMarginBottom)(nil), + (*excelize.PageMarginFooter)(nil), + (*excelize.PageMarginHeader)(nil), + (*excelize.PageMarginLeft)(nil), + (*excelize.PageMarginRight)(nil), + (*excelize.PageMarginTop)(nil), +} + +func ExampleFile_SetPageMargins() { + f := excelize.NewFile() + const sheet = "Sheet1" + + if err := f.SetPageMargins(sheet, + excelize.PageMarginBottom(1.0), + excelize.PageMarginFooter(1.0), + excelize.PageMarginHeader(1.0), + excelize.PageMarginLeft(1.0), + excelize.PageMarginRight(1.0), + excelize.PageMarginTop(1.0), + ); err != nil { + panic(err) + } + // Output: +} + +func ExampleFile_GetPageMargins() { + f := excelize.NewFile() + const sheet = "Sheet1" + + var ( + marginBottom excelize.PageMarginBottom + marginFooter excelize.PageMarginFooter + marginHeader excelize.PageMarginHeader + marginLeft excelize.PageMarginLeft + marginRight excelize.PageMarginRight + marginTop excelize.PageMarginTop + ) + + if err := f.GetPageMargins(sheet, + &marginBottom, + &marginFooter, + &marginHeader, + &marginLeft, + &marginRight, + &marginTop, + ); err != nil { + panic(err) + } + fmt.Println("Defaults:") + fmt.Println("- marginBottom:", marginBottom) + fmt.Println("- marginFooter:", marginFooter) + fmt.Println("- marginHeader:", marginHeader) + fmt.Println("- marginLeft:", marginLeft) + fmt.Println("- marginRight:", marginRight) + fmt.Println("- marginTop:", marginTop) + // Output: + // Defaults: + // - marginBottom: 0.75 + // - marginFooter: 0.3 + // - marginHeader: 0.3 + // - marginLeft: 0.7 + // - marginRight: 0.7 + // - marginTop: 0.75 +} + +func TestPageMarginsOption(t *testing.T) { + const sheet = "Sheet1" + + testData := []struct { + container excelize.PageMarginsOptionsPtr + nonDefault excelize.PageMarginsOptions + }{ + {new(excelize.PageMarginTop), excelize.PageMarginTop(1.0)}, + {new(excelize.PageMarginBottom), excelize.PageMarginBottom(1.0)}, + {new(excelize.PageMarginLeft), excelize.PageMarginLeft(1.0)}, + {new(excelize.PageMarginRight), excelize.PageMarginRight(1.0)}, + {new(excelize.PageMarginHeader), excelize.PageMarginHeader(1.0)}, + {new(excelize.PageMarginFooter), excelize.PageMarginFooter(1.0)}, + } + + for i, test := range testData { + t.Run(fmt.Sprintf("TestData%d", i), func(t *testing.T) { + + opt := test.nonDefault + t.Logf("option %T", opt) + + def := deepcopy.Copy(test.container).(excelize.PageMarginsOptionsPtr) + val1 := deepcopy.Copy(def).(excelize.PageMarginsOptionsPtr) + val2 := deepcopy.Copy(def).(excelize.PageMarginsOptionsPtr) + + f := excelize.NewFile() + // Get the default value + assert.NoError(t, f.GetPageMargins(sheet, def), opt) + // Get again and check + assert.NoError(t, f.GetPageMargins(sheet, val1), opt) + if !assert.Equal(t, val1, def, opt) { + t.FailNow() + } + // Set the same value + assert.NoError(t, f.SetPageMargins(sheet, val1), opt) + // Get again and check + assert.NoError(t, f.GetPageMargins(sheet, val1), opt) + if !assert.Equal(t, val1, def, "%T: value should not have changed", opt) { + t.FailNow() + } + // Set a different value + assert.NoError(t, f.SetPageMargins(sheet, test.nonDefault), opt) + assert.NoError(t, f.GetPageMargins(sheet, val1), opt) + // Get again and compare + assert.NoError(t, f.GetPageMargins(sheet, val2), opt) + if !assert.Equal(t, val1, val2, "%T: value should not have changed", opt) { + t.FailNow() + } + // Value should not be the same as the default + if !assert.NotEqual(t, def, val1, "%T: value should have changed from default", opt) { + t.FailNow() + } + // Restore the default value + assert.NoError(t, f.SetPageMargins(sheet, def), opt) + assert.NoError(t, f.GetPageMargins(sheet, val1), opt) + if !assert.Equal(t, def, val1) { + t.FailNow() + } + }) + } +} + +func TestSetPageMargins(t *testing.T) { + f := excelize.NewFile() + // Test set page margins on not exists worksheet. + assert.EqualError(t, f.SetPageMargins("SheetN"), "sheet SheetN is not exist") +} + +func TestGetPageMargins(t *testing.T) { + f := excelize.NewFile() + // Test get page margins on not exists worksheet. + assert.EqualError(t, f.GetPageMargins("SheetN"), "sheet SheetN is not exist") +} diff --git a/templates.go b/templates.go index 0d3a0c5..b570910 100644 --- a/templates.go +++ b/templates.go @@ -29,7 +29,7 @@ const templateWorkbook = `` -const templateSheet = `` +const templateSheet = `` const templateWorkbookRels = `` diff --git a/xmlPivotCache.go b/xmlPivotCache.go index a4b0711..45b48de 100644 --- a/xmlPivotCache.go +++ b/xmlPivotCache.go @@ -48,7 +48,7 @@ type xlsxPivotCacheDefinition struct { // PivotTable. type xlsxCacheSource struct { Type string `xml:"type,attr"` - ConnectionId int `xml:"connectionId,attr,omitempty"` + ConnectionID int `xml:"connectionId,attr,omitempty"` WorksheetSource *xlsxWorksheetSource `xml:"worksheetSource"` Consolidation *xlsxConsolidation `xml:"consolidation"` ExtLst *xlsxExtLst `xml:"extLst"` @@ -89,7 +89,7 @@ type xlsxCacheField struct { PropertyName string `xml:"propertyName,attr,omitempty"` ServerField bool `xml:"serverField,attr,omitempty"` UniqueList bool `xml:"uniqueList,attr,omitempty"` - NumFmtId int `xml:"numFmtId,attr"` + NumFmtID int `xml:"numFmtId,attr"` Formula string `xml:"formula,attr,omitempty"` SQLType int `xml:"sqlType,attr,omitempty"` Hierarchy int `xml:"hierarchy,attr,omitempty"` diff --git a/xmlPivotTable.go b/xmlPivotTable.go index 3738ed8..0549c5e 100644 --- a/xmlPivotTable.go +++ b/xmlPivotTable.go @@ -125,7 +125,7 @@ type xlsxPivotField struct { UniqueMemberProperty string `xml:"uniqueMemberProperty,attr,omitempty"` Compact bool `xml:"compact,attr"` AllDrilled bool `xml:"allDrilled,attr,omitempty"` - NumFmtId string `xml:"numFmtId,attr,omitempty"` + NumFmtID string `xml:"numFmtId,attr,omitempty"` Outline bool `xml:"outline,attr"` SubtotalTop bool `xml:"subtotalTop,attr,omitempty"` DragToRow bool `xml:"dragToRow,attr,omitempty"` @@ -273,7 +273,7 @@ type xlsxDataField struct { ShowDataAs string `xml:"showDataAs,attr,omitempty"` BaseField int `xml:"baseField,attr,omitempty"` BaseItem int64 `xml:"baseItem,attr,omitempty"` - NumFmtId string `xml:"numFmtId,attr,omitempty"` + NumFmtID string `xml:"numFmtId,attr,omitempty"` ExtLst *xlsxExtLst `xml:"extLst"` } -- cgit v1.2.1 From 866fda230028a3a9e6ff1c5234e432ad850d3c6b Mon Sep 17 00:00:00 2001 From: ducquangkstn Date: Fri, 18 Oct 2019 13:57:35 +0700 Subject: fix #503 rows next issue --- rows.go | 6 +++--- rows_test.go | 20 ++++++++++++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/rows.go b/rows.go index 3796441..c8ad2b1 100644 --- a/rows.go +++ b/rows.go @@ -57,7 +57,8 @@ type Rows struct { // Next will return true if find the next row element. func (rows *Rows) Next() bool { - return rows.curRow < len(rows.rows) + rows.curRow++ + return rows.curRow <= len(rows.rows) } // Error will return the error when the find next row element @@ -67,8 +68,7 @@ func (rows *Rows) Error() error { // Columns return the current row's column values func (rows *Rows) Columns() ([]string, error) { - curRow := rows.rows[rows.curRow] - rows.curRow++ + curRow := rows.rows[rows.curRow-1] columns := make([]string, len(curRow.C)) d := rows.f.sharedStringsReader() diff --git a/rows_test.go b/rows_test.go index a99a594..ba81f9f 100644 --- a/rows_test.go +++ b/rows_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestRows(t *testing.T) { @@ -41,6 +42,25 @@ func TestRows(t *testing.T) { } } +// test bug https://github.com/360EntSecGroup-Skylar/excelize/issues/502 +func TestRowsIterator(t *testing.T) { + const ( + sheet2 = "Sheet2" + expectedNumRow = 11 + ) + xlsx, err := OpenFile(filepath.Join("test", "Book1.xlsx")) + require.NoError(t, err) + + rows, err := xlsx.Rows(sheet2) + require.NoError(t, err) + var rowCount int + for rows.Next() { + rowCount++ + require.True(t, rowCount <= expectedNumRow, "rowCount is greater than expected") + } + assert.Equal(t, expectedNumRow, rowCount) +} + func TestRowsError(t *testing.T) { xlsx, err := OpenFile(filepath.Join("test", "Book1.xlsx")) if !assert.NoError(t, err) { -- cgit v1.2.1 From 7716968abc1d330492e311504af8951c34fb7520 Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 21 Oct 2019 00:04:18 +0800 Subject: Fix #505, support set line width of the line chart --- chart.go | 17 +++++++++++++++-- chart_test.go | 2 +- xmlChart.go | 5 +++-- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/chart.go b/chart.go index 7d40405..289903e 100644 --- a/chart.go +++ b/chart.go @@ -584,6 +584,7 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // name // categories // values +// line // // name: Set the name for the series. The name is displayed in the chart legend and in the formula bar. The name property is optional and if it isn't supplied it will default to Series 1..n. The name can also be a formula such as Sheet1!$A$1 // @@ -591,6 +592,8 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // // values: This is the most important property of a series and is the only mandatory option for every chart object. This option links the chart with the worksheet data that it displays. // +// line: This sets the line format of the line chart. The line property is optional and if it isn't supplied it will default style. The options that can be set is width. The range of width is 0.25pt - 999pt. If the value of width is outside the range, the default width of the line is 2pt. +// // Set properties of the chart legend. The options that can be set are: // // position @@ -1387,7 +1390,7 @@ func (f *File) drawChartSeriesSpPr(i int, formatSet *formatChart) *cSpPr { } spPrLine := &cSpPr{ Ln: &aLn{ - W: 25400, + W: f.ptToEMUs(formatSet.Series[i].Line.Width), Cap: "rnd", // rnd, sq, flat }, } @@ -1438,7 +1441,7 @@ func (f *File) drawChartSeriesCat(v formatChartSeries, formatSet *formatChart) * }, } chartSeriesCat := map[string]*cCat{Scatter: nil, Bubble: nil, Bubble3D: nil} - if _, ok := chartSeriesCat[formatSet.Type]; ok { + if _, ok := chartSeriesCat[formatSet.Type]; ok || v.Categories == "" { return nil } return cat @@ -1821,3 +1824,13 @@ func (f *File) addDrawingChart(sheet, drawingXML, cell string, width, height, rI f.Drawings[drawingXML] = content return err } + +// ptToEMUs provides a function to convert pt to EMUs, 1 pt = 12700 EMUs. The +// range of pt is 0.25pt - 999pt. If the value of pt is outside the range, the +// default EMUs will be returned. +func (f *File) ptToEMUs(pt float64) int { + if 0.25 > pt || pt > 999 { + return 25400 + } + return int(12700 * pt) +} diff --git a/chart_test.go b/chart_test.go index 932e873..20df373 100644 --- a/chart_test.go +++ b/chart_test.go @@ -144,7 +144,7 @@ func TestAddChart(t *testing.T) { assert.NoError(t, f.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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"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"}`)) assert.NoError(t, f.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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"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"}`)) assert.NoError(t, f.AddChart("Sheet2", "P16", `{"type":"doughnut","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$A$30:$D$37","values":"Sheet1!$B$30:$B$37"}],"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":"right","show_legend_key":false},"title":{"name":"Doughnut Chart"},"plotarea":{"show_bubble_size":false,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":false,"show_val":false},"show_blanks_as":"zero"}`)) - assert.NoError(t, f.AddChart("Sheet2", "X16", `{"type":"line","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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","show_legend_key":false},"title":{"name":"Line 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"}`)) + assert.NoError(t, f.AddChart("Sheet2", "X16", `{"type":"line","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37","line":{"width":0.25}}],"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","show_legend_key":false},"title":{"name":"Line 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"}`)) assert.NoError(t, f.AddChart("Sheet2", "P32", `{"type":"pie3D","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$A$30:$D$37","values":"Sheet1!$B$30:$B$37"}],"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":"3D Pie Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":false,"show_val":false},"show_blanks_as":"zero"}`)) assert.NoError(t, f.AddChart("Sheet2", "X32", `{"type":"pie","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$A$30:$D$37","values":"Sheet1!$B$30:$B$37"}],"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":"Pie Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":false,"show_val":false},"show_blanks_as":"gap"}`)) assert.NoError(t, f.AddChart("Sheet2", "P48", `{"type":"bar","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"2D Clustered Bar 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"}`)) diff --git a/xmlChart.go b/xmlChart.go index 69e119a..50d0b3e 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -606,8 +606,9 @@ type formatChartSeries struct { Categories string `json:"categories"` Values string `json:"values"` Line struct { - None bool `json:"none"` - Color string `json:"color"` + None bool `json:"none"` + Color string `json:"color"` + Width float64 `json:"width"` } `json:"line"` Marker struct { Type string `json:"type"` -- cgit v1.2.1 From e7581ebf3e14f096b6e2d56ed34d381b4af6d310 Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 23 Oct 2019 10:08:29 +0800 Subject: Fix corrupted Excel file issue #413 --- sheet.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/sheet.go b/sheet.go index 951baf9..9e8d504 100644 --- a/sheet.go +++ b/sheet.go @@ -406,6 +406,11 @@ func (f *File) DeleteSheet(name string) { f.SheetCount-- } } + for idx, bookView := range wb.BookViews.WorkBookView { + if bookView.ActiveTab >= f.SheetCount { + wb.BookViews.WorkBookView[idx].ActiveTab-- + } + } f.SetActiveSheet(len(f.GetSheetMap())) } -- cgit v1.2.1 From 9fe267ffcfa06545223160cdb8c35cd91163730e Mon Sep 17 00:00:00 2001 From: Michael Date: Thu, 24 Oct 2019 09:14:33 -0500 Subject: Pre-allocate some memory when reading files (#510) --- excelize_test.go | 6 ++++++ lib.go | 11 +++++------ rows_test.go | 2 +- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/excelize_test.go b/excelize_test.go index 7b6b674..8d7e7f7 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -1278,3 +1278,9 @@ func fillCells(f *File, sheet string, colCount, rowCount int) { } } } + +func BenchmarkOpenFile(b *testing.B) { + for i := 0; i < b.N; i++ { + OpenFile(filepath.Join("test", "Book1.xlsx")) + } +} diff --git a/lib.go b/lib.go index 4dea16a..edac98a 100644 --- a/lib.go +++ b/lib.go @@ -22,14 +22,12 @@ import ( // ReadZipReader can be used to read an XLSX in memory without touching the // filesystem. func ReadZipReader(r *zip.Reader) (map[string][]byte, int, error) { - fileList := make(map[string][]byte) + fileList := make(map[string][]byte, len(r.File)) worksheets := 0 for _, v := range r.File { fileList[v.Name] = readFile(v) - if len(v.Name) > 18 { - if v.Name[0:19] == "xl/worksheets/sheet" { - worksheets++ - } + if strings.HasPrefix(v.Name, "xl/worksheets/sheet") { + worksheets++ } } return fileList, worksheets, nil @@ -58,7 +56,8 @@ func readFile(file *zip.File) []byte { if err != nil { log.Fatal(err) } - buff := bytes.NewBuffer(nil) + dat := make([]byte, 0, file.FileInfo().Size()) + buff := bytes.NewBuffer(dat) _, _ = io.Copy(buff, rc) rc.Close() return buff.Bytes() diff --git a/rows_test.go b/rows_test.go index ba81f9f..f0fbe03 100644 --- a/rows_test.go +++ b/rows_test.go @@ -695,8 +695,8 @@ func TestErrSheetNotExistError(t *testing.T) { } func BenchmarkRows(b *testing.B) { + f, _ := OpenFile(filepath.Join("test", "Book1.xlsx")) for i := 0; i < b.N; i++ { - f, _ := OpenFile(filepath.Join("test", "Book1.xlsx")) rows, _ := f.Rows("Sheet2") for rows.Next() { row, _ := rows.Columns() -- cgit v1.2.1 From 87390cdd99b3afbe07daeef9abe96f57d03cb352 Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 24 Oct 2019 23:18:02 +0800 Subject: Resolve #511, allow empty columns in the pivot table --- calcchain.go | 2 +- pivotTable.go | 41 ++++++++++++++++++++++++++++++----------- pivotTable_test.go | 6 ++++++ rows_test.go | 2 +- sheet_test.go | 9 +++++++++ xmlDrawing.go | 8 ++++---- 6 files changed, 51 insertions(+), 17 deletions(-) diff --git a/calcchain.go b/calcchain.go index b4cadef..7cc175c 100644 --- a/calcchain.go +++ b/calcchain.go @@ -56,7 +56,7 @@ type xlsxCalcChainCollection []xlsxCalcChainC // Filter provides a function to filter calculation chain. func (c xlsxCalcChainCollection) Filter(fn func(v xlsxCalcChainC) bool) []xlsxCalcChainC { - results := make([]xlsxCalcChainC, 0) + var results []xlsxCalcChainC for _, v := range c { if fn(v) { results = append(results, v) diff --git a/pivotTable.go b/pivotTable.go index 881d774..6045e41 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -253,7 +253,10 @@ func (f *File) addPivotTable(cacheID, pivotTableID int, pivotTableXML string, op }, }, }, - ColFields: &xlsxColFields{}, + ColItems: &xlsxColItems{ + Count: 1, + I: []*xlsxI{{}}, + }, DataFields: &xlsxDataFields{}, PivotTableStyleInfo: &xlsxPivotTableStyleInfo{ Name: "PivotStyleLight16", @@ -286,19 +289,10 @@ func (f *File) addPivotTable(cacheID, pivotTableID int, pivotTableXML string, op // count row fields pt.RowFields.Count = len(pt.RowFields.Field) - // col fields - colFieldsIndex, err := f.getPivotFieldsIndex(opt.Columns, opt) + err = f.addPivotColFields(&pt, opt) if err != nil { return err } - for _, filedIdx := range colFieldsIndex { - pt.ColFields.Field = append(pt.ColFields.Field, &xlsxField{ - X: filedIdx, - }) - } - - // count col fields - pt.ColFields.Count = len(pt.ColFields.Field) // data fields dataFieldsIndex, err := f.getPivotFieldsIndex(opt.Data, opt) @@ -330,6 +324,31 @@ func inStrSlice(a []string, x string) int { return -1 } +// addPivotColFields create pivot column fields by given pivot table +// definition and option. +func (f *File) addPivotColFields(pt *xlsxPivotTableDefinition, opt *PivotTableOption) error { + if len(opt.Columns) == 0 { + return nil + } + + pt.ColFields = &xlsxColFields{} + + // col fields + colFieldsIndex, err := f.getPivotFieldsIndex(opt.Columns, opt) + if err != nil { + return err + } + for _, filedIdx := range colFieldsIndex { + pt.ColFields.Field = append(pt.ColFields.Field, &xlsxField{ + X: filedIdx, + }) + } + + // count col fields + pt.ColFields.Count = len(pt.ColFields.Field) + return err +} + // addPivotFields create pivot fields based on the column order of the first // row in the data region by given pivot table definition and option. func (f *File) addPivotFields(pt *xlsxPivotTableDefinition, opt *PivotTableOption) error { diff --git a/pivotTable_test.go b/pivotTable_test.go index 27e5914..9bf08e8 100644 --- a/pivotTable_test.go +++ b/pivotTable_test.go @@ -54,6 +54,12 @@ func TestAddPivotTable(t *testing.T) { Columns: []string{"Region", "Year"}, Data: []string{"Sales"}, })) + assert.NoError(t, f.AddPivotTable(&PivotTableOption{ + DataRange: "Sheet1!$A$1:$E$31", + PivotTableRange: "Sheet1!$AE$2:$AG$33", + Rows: []string{"Month", "Year"}, + Data: []string{"Sales"}, + })) f.NewSheet("Sheet2") assert.NoError(t, f.AddPivotTable(&PivotTableOption{ DataRange: "Sheet1!$A$1:$E$31", diff --git a/rows_test.go b/rows_test.go index f0fbe03..a443e89 100644 --- a/rows_test.go +++ b/rows_test.go @@ -22,7 +22,7 @@ func TestRows(t *testing.T) { t.FailNow() } - collectedRows := make([][]string, 0) + var collectedRows [][]string for rows.Next() { columns, err := rows.Columns() assert.NoError(t, err) diff --git a/sheet_test.go b/sheet_test.go index 5179793..ea345a3 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -66,6 +66,15 @@ func ExampleFile_GetPageLayout() { // - fit to width: 1 } +func TestNewSheet(t *testing.T) { + f := excelize.NewFile() + sheetID := f.NewSheet("Sheet2") + f.SetActiveSheet(sheetID) + // delete original sheet + f.DeleteSheet(f.GetSheetName(f.GetSheetIndex("Sheet1"))) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestNewSheet.xlsx"))) +} + func TestPageLayoutOption(t *testing.T) { const sheet = "Sheet1" diff --git a/xmlDrawing.go b/xmlDrawing.go index 4338c5e..1c24f08 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -47,10 +47,10 @@ const ( NameSpaceDublinCore = "http://purl.org/dc/elements/1.1/" NameSpaceDublinCoreTerms = "http://purl.org/dc/terms/" NameSpaceDublinCoreMetadataIntiative = "http://purl.org/dc/dcmitype/" - // The extLst child element ([ISO/IEC29500-1:2016] section 18.2.10) of the - // worksheet element ([ISO/IEC29500-1:2016] section 18.3.1.99) is extended by - // the addition of new child ext elements ([ISO/IEC29500-1:2016] section - // 18.2.7) + // ExtURIConditionalFormattings is the extLst child element + // ([ISO/IEC29500-1:2016] section 18.2.10) of the worksheet element + // ([ISO/IEC29500-1:2016] section 18.3.1.99) is extended by the addition of + // new child ext elements ([ISO/IEC29500-1:2016] section 18.2.7) ExtURIConditionalFormattings = "{78C0D931-6437-407D-A8EE-F0AAD7539E65}" ExtURIDataValidations = "{CCE6A557-97BC-4B89-ADB6-D9C93CAAB3DF}" ExtURISparklineGroups = "{05C60535-1F16-4fd2-B633-F4F36F0B64E0}" -- cgit v1.2.1 From 5e418ebd665f38d1211b27d7157ec7e5868451bc Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 26 Oct 2019 20:55:24 +0800 Subject: Resolve #507, add the new function `DeleteDefinedName` --- cell_test.go | 45 +++++++++++++++++++++++++++++++++++++++++++++ col_test.go | 7 ++++++- excelize_test.go | 43 +++++++++++-------------------------------- rows_test.go | 26 ++++++++++++++++---------- sheet.go | 28 +++++++++++++++++++++++++++- sheet_test.go | 10 +++++++++- 6 files changed, 114 insertions(+), 45 deletions(-) diff --git a/cell_test.go b/cell_test.go index 653aaab..da0c1f1 100644 --- a/cell_test.go +++ b/cell_test.go @@ -4,6 +4,7 @@ import ( "fmt" "path/filepath" "testing" + "time" "github.com/stretchr/testify/assert" ) @@ -73,6 +74,50 @@ func TestSetCellFloat(t *testing.T) { assert.NoError(t, err) assert.Equal(t, "123.42", val, "A1 should be 123.42") }) + f := NewFile() + assert.EqualError(t, f.SetCellFloat(sheet, "A", 123.42, -1, 64), `cannot convert cell "A" to coordinates: invalid cell name "A"`) +} + +func TestSetCellValue(t *testing.T) { + f := NewFile() + assert.EqualError(t, f.SetCellValue("Sheet1", "A", time.Now().UTC()), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + assert.EqualError(t, f.SetCellValue("Sheet1", "A", time.Duration(1e13)), `cannot convert cell "A" to coordinates: invalid cell name "A"`) +} + +func TestSetCellBool(t *testing.T) { + f := NewFile() + assert.EqualError(t, f.SetCellBool("Sheet1", "A", true), `cannot convert cell "A" to coordinates: invalid cell name "A"`) +} + +func TestGetCellFormula(t *testing.T) { + f := NewFile() + f.GetCellFormula("Sheet", "A1") +} + +func TestMergeCell(t *testing.T) { + f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) + if !assert.NoError(t, err) { + t.FailNow() + } + assert.EqualError(t, f.MergeCell("Sheet1", "A", "B"), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + f.MergeCell("Sheet1", "D9", "D9") + f.MergeCell("Sheet1", "D9", "E9") + f.MergeCell("Sheet1", "H14", "G13") + f.MergeCell("Sheet1", "C9", "D8") + f.MergeCell("Sheet1", "F11", "G13") + f.MergeCell("Sheet1", "H7", "B15") + f.MergeCell("Sheet1", "D11", "F13") + f.MergeCell("Sheet1", "G10", "K12") + f.SetCellValue("Sheet1", "G11", "set value in merged cell") + f.SetCellInt("Sheet1", "H11", 100) + f.SetCellValue("Sheet1", "I11", float64(0.5)) + f.SetCellHyperLink("Sheet1", "J11", "https://github.com/360EntSecGroup-Skylar/excelize", "External") + f.SetCellFormula("Sheet1", "G12", "SUM(Sheet1!B19,Sheet1!C19)") + f.GetCellValue("Sheet1", "H11") + f.GetCellValue("Sheet2", "A6") // Merged cell ref is single coordinate. + f.GetCellFormula("Sheet1", "G12") + + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestMergeCell.xlsx"))) } func ExampleFile_SetCellFloat() { diff --git a/col_test.go b/col_test.go index a696caa..edbdae7 100644 --- a/col_test.go +++ b/col_test.go @@ -53,10 +53,15 @@ func TestOutlineLevel(t *testing.T) { assert.NoError(t, f.SetRowOutlineLevel("Sheet1", 2, 7)) assert.EqualError(t, f.SetColOutlineLevel("Sheet1", "D", 8), "invalid outline level") assert.EqualError(t, f.SetRowOutlineLevel("Sheet1", 2, 8), "invalid outline level") + // Test set row outline level on not exists worksheet. + assert.EqualError(t, f.SetRowOutlineLevel("SheetN", 1, 4), "sheet SheetN is not exist") + // Test get row outline level on not exists worksheet. + _, err := f.GetRowOutlineLevel("SheetN", 1) + assert.EqualError(t, err, "sheet SheetN is not exist") // Test set and get column outline level with illegal cell coordinates. assert.EqualError(t, f.SetColOutlineLevel("Sheet1", "*", 1), `invalid column name "*"`) - _, err := f.GetColOutlineLevel("Sheet1", "*") + _, err = f.GetColOutlineLevel("Sheet1", "*") assert.EqualError(t, err, `invalid column name "*"`) // Test set column outline level on not exists worksheet. diff --git a/excelize_test.go b/excelize_test.go index 8d7e7f7..38a35b0 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -372,32 +372,6 @@ func TestSetSheetBackgroundErrors(t *testing.T) { assert.EqualError(t, err, "unsupported image extension") } -func TestMergeCell(t *testing.T) { - f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) - if !assert.NoError(t, err) { - t.FailNow() - } - - f.MergeCell("Sheet1", "D9", "D9") - f.MergeCell("Sheet1", "D9", "E9") - f.MergeCell("Sheet1", "H14", "G13") - f.MergeCell("Sheet1", "C9", "D8") - f.MergeCell("Sheet1", "F11", "G13") - f.MergeCell("Sheet1", "H7", "B15") - f.MergeCell("Sheet1", "D11", "F13") - f.MergeCell("Sheet1", "G10", "K12") - f.SetCellValue("Sheet1", "G11", "set value in merged cell") - f.SetCellInt("Sheet1", "H11", 100) - f.SetCellValue("Sheet1", "I11", float64(0.5)) - f.SetCellHyperLink("Sheet1", "J11", "https://github.com/360EntSecGroup-Skylar/excelize", "External") - f.SetCellFormula("Sheet1", "G12", "SUM(Sheet1!B19,Sheet1!C19)") - f.GetCellValue("Sheet1", "H11") - f.GetCellValue("Sheet2", "A6") // Merged cell ref is single coordinate. - f.GetCellFormula("Sheet1", "G12") - - assert.NoError(t, f.SaveAs(filepath.Join("test", "TestMergeCell.xlsx"))) -} - // TestWriteArrayFormula tests the extended options of SetCellFormula by writing an array function // to a workbook. In the resulting file, the lines 2 and 3 as well as 4 and 5 should have matching // contents. @@ -913,13 +887,18 @@ func TestAddShape(t *testing.T) { t.FailNow() } - f.AddShape("Sheet1", "A30", `{"type":"rect","paragraph":[{"text":"Rectangle","font":{"color":"CD5C5C"}},{"text":"Shape","font":{"bold":true,"color":"2980B9"}}]}`) - f.AddShape("Sheet1", "B30", `{"type":"rect","paragraph":[{"text":"Rectangle"},{}]}`) - f.AddShape("Sheet1", "C30", `{"type":"rect","paragraph":[]}`) - f.AddShape("Sheet3", "H1", `{"type":"ellipseRibbon", "color":{"line":"#4286f4","fill":"#8eb9ff"}, "paragraph":[{"font":{"bold":true,"italic":true,"family":"Times New Roman","size":36,"color":"#777777","underline":"single"}}], "height": 90}`) - f.AddShape("Sheet3", "H1", "") + assert.NoError(t, f.AddShape("Sheet1", "A30", `{"type":"rect","paragraph":[{"text":"Rectangle","font":{"color":"CD5C5C"}},{"text":"Shape","font":{"bold":true,"color":"2980B9"}}]}`)) + assert.NoError(t, f.AddShape("Sheet1", "B30", `{"type":"rect","paragraph":[{"text":"Rectangle"},{}]}`)) + assert.NoError(t, f.AddShape("Sheet1", "C30", `{"type":"rect","paragraph":[]}`)) + assert.EqualError(t, f.AddShape("Sheet3", "H1", `{"type":"ellipseRibbon", "color":{"line":"#4286f4","fill":"#8eb9ff"}, "paragraph":[{"font":{"bold":true,"italic":true,"family":"Times New Roman","size":36,"color":"#777777","underline":"single"}}], "height": 90}`), "sheet Sheet3 is not exist") + assert.EqualError(t, f.AddShape("Sheet3", "H1", ""), "unexpected end of JSON input") + assert.EqualError(t, f.AddShape("Sheet1", "A", `{"type":"rect","paragraph":[{"text":"Rectangle","font":{"color":"CD5C5C"}},{"text":"Shape","font":{"bold":true,"color":"2980B9"}}]}`), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddShape1.xlsx"))) - assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddShape.xlsx"))) + // Test add first shape for given sheet. + f = NewFile() + assert.NoError(t, f.AddShape("Sheet1", "A1", `{"type":"ellipseRibbon", "color":{"line":"#4286f4","fill":"#8eb9ff"}, "paragraph":[{"font":{"bold":true,"italic":true,"family":"Times New Roman","size":36,"color":"#777777","underline":"single"}}], "height": 90}`)) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddShape2.xlsx"))) } func TestAddComments(t *testing.T) { diff --git a/rows_test.go b/rows_test.go index a443e89..ff70118 100644 --- a/rows_test.go +++ b/rows_test.go @@ -42,7 +42,6 @@ func TestRows(t *testing.T) { } } -// test bug https://github.com/360EntSecGroup-Skylar/excelize/issues/502 func TestRowsIterator(t *testing.T) { const ( sheet2 = "Sheet2" @@ -59,6 +58,10 @@ func TestRowsIterator(t *testing.T) { require.True(t, rowCount <= expectedNumRow, "rowCount is greater than expected") } assert.Equal(t, expectedNumRow, rowCount) + + rows = &Rows{f: xlsx, rows: []xlsxRow{{C: []xlsxC{{R: "A"}}}}, curRow: 1} + _, err = rows.Columns() + assert.EqualError(t, err, `cannot convert cell "A" to coordinates: invalid cell name "A"`) } func TestRowsError(t *testing.T) { @@ -113,22 +116,25 @@ func TestRowHeight(t *testing.T) { } func TestRowVisibility(t *testing.T) { - xlsx, err := prepareTestBook1() + f, err := prepareTestBook1() if !assert.NoError(t, err) { t.FailNow() } - xlsx.NewSheet("Sheet3") - assert.NoError(t, xlsx.SetRowVisible("Sheet3", 2, false)) - assert.NoError(t, xlsx.SetRowVisible("Sheet3", 2, true)) - xlsx.GetRowVisible("Sheet3", 2) - xlsx.GetRowVisible("Sheet3", 25) - assert.EqualError(t, xlsx.SetRowVisible("Sheet3", 0, true), "invalid row number 0") + f.NewSheet("Sheet3") + assert.NoError(t, f.SetRowVisible("Sheet3", 2, false)) + assert.NoError(t, f.SetRowVisible("Sheet3", 2, true)) + f.GetRowVisible("Sheet3", 2) + f.GetRowVisible("Sheet3", 25) + assert.EqualError(t, f.SetRowVisible("Sheet3", 0, true), "invalid row number 0") + assert.EqualError(t, f.SetRowVisible("SheetN", 2, false), "sheet SheetN is not exist") - visible, err := xlsx.GetRowVisible("Sheet3", 0) + visible, err := f.GetRowVisible("Sheet3", 0) assert.Equal(t, false, visible) assert.EqualError(t, err, "invalid row number 0") + _, err = f.GetRowVisible("SheetN", 1) + assert.EqualError(t, err, "sheet SheetN is not exist") - assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestRowVisibility.xlsx"))) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestRowVisibility.xlsx"))) } func TestRemoveRow(t *testing.T) { diff --git a/sheet.go b/sheet.go index 9e8d504..335c4fc 100644 --- a/sheet.go +++ b/sheet.go @@ -1271,7 +1271,7 @@ func (f *File) SetDefinedName(definedName *DefinedName) error { scope = f.GetSheetName(*dn.LocalSheetID + 1) } if scope == definedName.Scope && dn.Name == definedName.Name { - return errors.New("the same name already exists on scope") + return errors.New("the same name already exists on the scope") } } wb.DefinedNames.DefinedName = append(wb.DefinedNames.DefinedName, d) @@ -1283,6 +1283,32 @@ func (f *File) SetDefinedName(definedName *DefinedName) error { return nil } +// DeleteDefinedName provides a function to delete the defined names of the +// workbook or worksheet. If not specified scope, the default scope is +// workbook. For example: +// +// f.DeleteDefinedName(&excelize.DefinedName{ +// Name: "Amount", +// Scope: "Sheet2", +// }) +// +func (f *File) DeleteDefinedName(definedName *DefinedName) error { + wb := f.workbookReader() + if wb.DefinedNames != nil { + for idx, dn := range wb.DefinedNames.DefinedName { + var scope string + if dn.LocalSheetID != nil { + scope = f.GetSheetName(*dn.LocalSheetID + 1) + } + if scope == definedName.Scope && dn.Name == definedName.Name { + wb.DefinedNames.DefinedName = append(wb.DefinedNames.DefinedName[:idx], wb.DefinedNames.DefinedName[idx+1:]...) + return nil + } + } + } + return errors.New("no defined name on the scope") +} + // GetDefinedName provides a function to get the defined names of the workbook // or worksheet. func (f *File) GetDefinedName() []DefinedName { diff --git a/sheet_test.go b/sheet_test.go index ea345a3..b9e4abf 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -210,8 +210,16 @@ func TestDefinedName(t *testing.T) { Name: "Amount", RefersTo: "Sheet1!$A$2:$D$5", Comment: "defined name comment", - }), "the same name already exists on scope") + }), "the same name already exists on the scope") + assert.EqualError(t, f.DeleteDefinedName(&excelize.DefinedName{ + Name: "No Exist Defined Name", + }), "no defined name on the scope") assert.Exactly(t, "Sheet1!$A$2:$D$5", f.GetDefinedName()[1].RefersTo) + assert.NoError(t, f.DeleteDefinedName(&excelize.DefinedName{ + Name: "Amount", + })) + assert.Exactly(t, "Sheet1!$A$2:$D$5", f.GetDefinedName()[0].RefersTo) + assert.Exactly(t, 1, len(f.GetDefinedName())) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestDefinedName.xlsx"))) } -- cgit v1.2.1 From 6abf8bf9723512086f009ca574bde1d6682fc83d Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 27 Oct 2019 14:16:02 +0800 Subject: Resolve #501, support set minor grid lines for the chart --- chart.go | 12 ++++++++++++ chart_test.go | 2 +- xmlChart.go | 2 ++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/chart.go b/chart.go index 289903e..bbd71d6 100644 --- a/chart.go +++ b/chart.go @@ -652,10 +652,16 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // // Set the primary horizontal and vertical axis options by x_axis and y_axis. The properties that can be set are: // +// major_grid_lines +// minor_grid_lines // reverse_order // maximum // minimum // +// major_grid_lines: Specifies major gridlines. +// +// minor_grid_lines: Specifies minor gridlines. +// // reverse_order: Specifies that the categories or values on reverse order (orientation of the chart). The reverse_order property is optional. The default value is false. // // maximum: Specifies that the fixed maximum, 0 is auto. The maximum property is optional. The default value is auto. @@ -1601,6 +1607,9 @@ func (f *File) drawPlotAreaCatAx(formatSet *formatChart) []*cAxs { if formatSet.XAxis.MajorGridlines { axs[0].MajorGridlines = &cChartLines{SpPr: f.drawPlotAreaSpPr()} } + if formatSet.XAxis.MinorGridlines { + axs[0].MinorGridlines = &cChartLines{SpPr: f.drawPlotAreaSpPr()} + } return axs } @@ -1641,6 +1650,9 @@ func (f *File) drawPlotAreaValAx(formatSet *formatChart) []*cAxs { if formatSet.YAxis.MajorGridlines { axs[0].MajorGridlines = &cChartLines{SpPr: f.drawPlotAreaSpPr()} } + if formatSet.YAxis.MinorGridlines { + axs[0].MinorGridlines = &cChartLines{SpPr: f.drawPlotAreaSpPr()} + } if pos, ok := valTickLblPos[formatSet.Type]; ok { axs[0].TickLblPos.Val = pos } diff --git a/chart_test.go b/chart_test.go index 20df373..bc5c30a 100644 --- a/chart_test.go +++ b/chart_test.go @@ -144,7 +144,7 @@ func TestAddChart(t *testing.T) { assert.NoError(t, f.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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"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"}`)) assert.NoError(t, f.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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"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"}`)) assert.NoError(t, f.AddChart("Sheet2", "P16", `{"type":"doughnut","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$A$30:$D$37","values":"Sheet1!$B$30:$B$37"}],"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":"right","show_legend_key":false},"title":{"name":"Doughnut Chart"},"plotarea":{"show_bubble_size":false,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":false,"show_val":false},"show_blanks_as":"zero"}`)) - assert.NoError(t, f.AddChart("Sheet2", "X16", `{"type":"line","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37","line":{"width":0.25}}],"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","show_legend_key":false},"title":{"name":"Line 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"}`)) + assert.NoError(t, f.AddChart("Sheet2", "X16", `{"type":"line","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37","line":{"width":0.25}}],"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","show_legend_key":false},"title":{"name":"Line 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","x_axis":{"major_grid_lines":true,"minor_grid_lines":true},"y_axis":{"major_grid_lines":true,"minor_grid_lines":true}}`)) assert.NoError(t, f.AddChart("Sheet2", "P32", `{"type":"pie3D","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$A$30:$D$37","values":"Sheet1!$B$30:$B$37"}],"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":"3D Pie Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":false,"show_val":false},"show_blanks_as":"zero"}`)) assert.NoError(t, f.AddChart("Sheet2", "X32", `{"type":"pie","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$A$30:$D$37","values":"Sheet1!$B$30:$B$37"}],"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":"Pie Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":false,"show_val":false},"show_blanks_as":"gap"}`)) assert.NoError(t, f.AddChart("Sheet2", "P48", `{"type":"bar","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"2D Clustered Bar 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"}`)) diff --git a/xmlChart.go b/xmlChart.go index 50d0b3e..a02da2a 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -347,6 +347,7 @@ type cAxs struct { Delete *attrValBool `xml:"delete"` AxPos *attrValString `xml:"axPos"` MajorGridlines *cChartLines `xml:"majorGridlines"` + MinorGridlines *cChartLines `xml:"minorGridlines"` NumFmt *cNumFmt `xml:"numFmt"` MajorTickMark *attrValString `xml:"majorTickMark"` MinorTickMark *attrValString `xml:"minorTickMark"` @@ -514,6 +515,7 @@ type cPageMargins struct { type formatChartAxis struct { Crossing string `json:"crossing"` MajorGridlines bool `json:"major_grid_lines"` + MinorGridlines bool `json:"minor_grid_lines"` MajorTickMark string `json:"major_tick_mark"` MinorTickMark string `json:"minor_tick_mark"` MinorUnitType string `json:"minor_unit_type"` -- cgit v1.2.1 From bf9a8355494eac18812f3caf6d469962824f627f Mon Sep 17 00:00:00 2001 From: Harris Date: Mon, 28 Oct 2019 10:34:21 -0500 Subject: Reduce allocations when writing Fix #494 If a row is full, don't bother allocating a new one, just return it. Use the last populated row as a hint for the size of new rows. Simplify checkSheet to remove row map --- excelize.go | 18 +++++------------- file_test.go | 27 +++++++++++++++++++++++++++ sheet.go | 18 +++++++++++++++--- xmlWorksheet.go | 4 ++++ 4 files changed, 51 insertions(+), 16 deletions(-) create mode 100644 file_test.go diff --git a/excelize.go b/excelize.go index 4d46b94..ba6445f 100644 --- a/excelize.go +++ b/excelize.go @@ -155,20 +155,12 @@ func checkSheet(xlsx *xlsxWorksheet) { row = lastRow } } - sheetData := xlsxSheetData{} - existsRows := map[int]int{} - for k := range xlsx.SheetData.Row { - existsRows[xlsx.SheetData.Row[k].R] = k + sheetData := xlsxSheetData{Row: make([]xlsxRow, row)} + for _, r := range xlsx.SheetData.Row { + sheetData.Row[r.R-1] = r } - for i := 0; i < row; i++ { - _, ok := existsRows[i+1] - if ok { - sheetData.Row = append(sheetData.Row, xlsx.SheetData.Row[existsRows[i+1]]) - } else { - sheetData.Row = append(sheetData.Row, xlsxRow{ - R: i + 1, - }) - } + for i := 1; i <= row; i++ { + sheetData.Row[i-1].R = i } xlsx.SheetData = sheetData } diff --git a/file_test.go b/file_test.go new file mode 100644 index 0000000..6c30f4a --- /dev/null +++ b/file_test.go @@ -0,0 +1,27 @@ +package excelize + +import ( + "testing" +) + +func BenchmarkWrite(b *testing.B) { + const s = "This is test data" + for i := 0; i < b.N; i++ { + f := NewFile() + for row := 1; row <= 10000; row++ { + for col := 1; col <= 20; col++ { + val, err := CoordinatesToCellName(col, row) + if err != nil { + panic(err) + } + f.SetCellDefault("Sheet1", val, s) + } + } + // Save xlsx file by the given path. + err := f.SaveAs("./test.xlsx") + if err != nil { + panic(err) + } + } + +} diff --git a/sheet.go b/sheet.go index 335c4fc..43c7cc0 100644 --- a/sheet.go +++ b/sheet.go @@ -117,12 +117,19 @@ func (f *File) workSheetWriter() { } } -// trimCell provides a function to trim blank cells which created by completeCol. +// trimCell provides a function to trim blank cells which created by fillColumns. func trimCell(column []xlsxC) []xlsxC { + rowFull := true + for i := range column { + rowFull = column[i].hasValue() && rowFull + } + if rowFull { + return column + } col := make([]xlsxC, len(column)) i := 0 for _, c := range column { - if c.S != 0 || c.V != "" || c.F != nil || c.T != "" { + if c.hasValue() { col[i] = c i++ } @@ -1404,12 +1411,17 @@ func (f *File) relsReader(path string) *xlsxRelationships { // fillSheetData ensures there are enough rows, and columns in the chosen // row to accept data. Missing rows are backfilled and given their row number +// Uses the last populated row as a hint for the size of the next row to add func prepareSheetXML(xlsx *xlsxWorksheet, col int, row int) { rowCount := len(xlsx.SheetData.Row) + sizeHint := 0 + if rowCount > 0 { + sizeHint = len(xlsx.SheetData.Row[rowCount-1].C) + } if rowCount < row { // append missing rows for rowIdx := rowCount; rowIdx < row; rowIdx++ { - xlsx.SheetData.Row = append(xlsx.SheetData.Row, xlsxRow{R: rowIdx + 1}) + xlsx.SheetData.Row = append(xlsx.SheetData.Row, xlsxRow{R: rowIdx + 1, C: make([]xlsxC, 0, sizeHint)}) } } rowData := &xlsx.SheetData.Row[row-1] diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 96ca235..8408cfa 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -430,6 +430,10 @@ type xlsxC struct { XMLSpace xml.Attr `xml:"space,attr,omitempty"` } +func (c *xlsxC) hasValue() bool { + return c.S != 0 || c.V != "" || c.F != nil || c.T != "" +} + // xlsxF directly maps the f element in the namespace // http://schemas.openxmlformats.org/spreadsheetml/2006/main - currently I have // not checked it for completeness - it does as much as I need. -- cgit v1.2.1 From 7965e1231b736f8507f93f6383b76332eb15ff5f Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 23 Nov 2019 04:13:59 +0800 Subject: Resolve #146, make the GetRow function read data as streaming. Ref: #382, #515 --- rows.go | 122 ++++++++++++++++++++++++++++++++++++++++++++++------------- rows_test.go | 23 ++++++++--- sheet.go | 63 ++++++++++++++++-------------- 3 files changed, 146 insertions(+), 62 deletions(-) diff --git a/rows.go b/rows.go index c8ad2b1..69a9846 100644 --- a/rows.go +++ b/rows.go @@ -10,6 +10,7 @@ package excelize import ( + "bytes" "encoding/xml" "errors" "fmt" @@ -49,16 +50,19 @@ func (f *File) GetRows(sheet string) ([][]string, error) { // Rows defines an iterator to a sheet type Rows struct { - err error - f *File - rows []xlsxRow - curRow int + err error + f *File + rows []xlsxRow + sheet string + curRow int + totalRow int + decoder *xml.Decoder } // Next will return true if find the next row element. func (rows *Rows) Next() bool { rows.curRow++ - return rows.curRow <= len(rows.rows) + return rows.curRow <= rows.totalRow } // Error will return the error when the find next row element @@ -68,19 +72,57 @@ func (rows *Rows) Error() error { // Columns return the current row's column values func (rows *Rows) Columns() ([]string, error) { - curRow := rows.rows[rows.curRow-1] - - columns := make([]string, len(curRow.C)) + var ( + err error + inElement string + row, cellCol int + columns []string + ) d := rows.f.sharedStringsReader() - for _, colCell := range curRow.C { - col, _, err := CellNameToCoordinates(colCell.R) - if err != nil { - return columns, err + for { + token, _ := rows.decoder.Token() + if token == nil { + break + } + switch startElement := token.(type) { + case xml.StartElement: + inElement = startElement.Name.Local + if inElement == "row" { + for _, attr := range startElement.Attr { + if attr.Name.Local == "r" { + row, err = strconv.Atoi(attr.Value) + if err != nil { + return columns, err + } + if row > rows.curRow { + return columns, err + } + } + } + } + if inElement == "c" { + colCell := xlsxC{} + _ = rows.decoder.DecodeElement(&colCell, &startElement) + cellCol, _, err = CellNameToCoordinates(colCell.R) + if err != nil { + return columns, err + } + blank := cellCol - len(columns) + for i := 1; i < blank; i++ { + columns = append(columns, "") + } + val, _ := colCell.getValueFrom(rows.f, d) + columns = append(columns, val) + } + case xml.EndElement: + inElement = startElement.Name.Local + if inElement == "row" { + return columns, err + } + default: } - val, _ := colCell.getValueFrom(rows.f, d) - columns[col-1] = val } - return columns, nil + return columns, err } // ErrSheetNotExist defines an error of sheet is not exist @@ -89,7 +131,7 @@ type ErrSheetNotExist struct { } func (err ErrSheetNotExist) Error() string { - return fmt.Sprintf("Sheet %s is not exist", string(err.SheetName)) + return fmt.Sprintf("sheet %s is not exist", string(err.SheetName)) } // Rows return a rows iterator. For example: @@ -104,22 +146,48 @@ func (err ErrSheetNotExist) Error() string { // } // func (f *File) Rows(sheet string) (*Rows, error) { - xlsx, err := f.workSheetReader(sheet) - if err != nil { - return nil, err - } name, ok := f.sheetMap[trimSheetName(sheet)] if !ok { return nil, ErrSheetNotExist{sheet} } - if xlsx != nil { - data := f.readXML(name) - f.saveFileList(name, replaceWorkSheetsRelationshipsNameSpaceBytes(namespaceStrictToTransitional(data))) + if f.Sheet[name] != nil { + // flush data + output, _ := xml.Marshal(f.Sheet[name]) + f.saveFileList(name, replaceWorkSheetsRelationshipsNameSpaceBytes(output)) + } + var ( + err error + inElement string + row int + rows Rows + ) + decoder := xml.NewDecoder(bytes.NewReader(f.readXML(name))) + for { + token, _ := decoder.Token() + if token == nil { + break + } + switch startElement := token.(type) { + case xml.StartElement: + inElement = startElement.Name.Local + if inElement == "row" { + for _, attr := range startElement.Attr { + if attr.Name.Local == "r" { + row, err = strconv.Atoi(attr.Value) + if err != nil { + return &rows, err + } + } + } + rows.totalRow = row + } + default: + } } - return &Rows{ - f: f, - rows: xlsx.SheetData.Row, - }, nil + rows.f = f + rows.sheet = name + rows.decoder = xml.NewDecoder(bytes.NewReader(f.readXML(name))) + return &rows, nil } // SetRowHeight provides a function to set the height of a single row. For diff --git a/rows_test.go b/rows_test.go index ff70118..47c9d96 100644 --- a/rows_test.go +++ b/rows_test.go @@ -47,10 +47,10 @@ func TestRowsIterator(t *testing.T) { sheet2 = "Sheet2" expectedNumRow = 11 ) - xlsx, err := OpenFile(filepath.Join("test", "Book1.xlsx")) + f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) require.NoError(t, err) - rows, err := xlsx.Rows(sheet2) + rows, err := f.Rows(sheet2) require.NoError(t, err) var rowCount int for rows.Next() { @@ -59,9 +59,20 @@ func TestRowsIterator(t *testing.T) { } assert.Equal(t, expectedNumRow, rowCount) - rows = &Rows{f: xlsx, rows: []xlsxRow{{C: []xlsxC{{R: "A"}}}}, curRow: 1} - _, err = rows.Columns() - assert.EqualError(t, err, `cannot convert cell "A" to coordinates: invalid cell name "A"`) + // Valued cell sparse distribution test + f = NewFile() + cells := []string{"C1", "E1", "A3", "B3", "C3", "D3", "E3"} + for _, cell := range cells { + f.SetCellValue("Sheet1", cell, 1) + } + rows, err = f.Rows("Sheet1") + require.NoError(t, err) + rowCount = 0 + for rows.Next() { + rowCount++ + require.True(t, rowCount <= 3, "rowCount is greater than expected") + } + assert.Equal(t, 3, rowCount) } func TestRowsError(t *testing.T) { @@ -697,7 +708,7 @@ func TestDuplicateRowInvalidRownum(t *testing.T) { func TestErrSheetNotExistError(t *testing.T) { err := ErrSheetNotExist{SheetName: "Sheet1"} - assert.EqualValues(t, err.Error(), "Sheet Sheet1 is not exist") + assert.EqualValues(t, err.Error(), "sheet Sheet1 is not exist") } func BenchmarkRows(b *testing.B) { diff --git a/sheet.go b/sheet.go index 43c7cc0..c2e6bf6 100644 --- a/sheet.go +++ b/sheet.go @@ -699,15 +699,12 @@ func (f *File) SearchSheet(sheet, value string, reg ...bool) ([]string, error) { for _, r := range reg { regSearch = r } - xlsx, err := f.workSheetReader(sheet) - if err != nil { - return result, err - } name, ok := f.sheetMap[trimSheetName(sheet)] if !ok { - return result, nil + return result, ErrSheetNotExist{sheet} } - if xlsx != nil { + if f.Sheet[name] != nil { + // flush data output, _ := xml.Marshal(f.Sheet[name]) f.saveFileList(name, replaceWorkSheetsRelationshipsNameSpaceBytes(output)) } @@ -718,9 +715,10 @@ func (f *File) SearchSheet(sheet, value string, reg ...bool) ([]string, error) { // cell value, and regular expression. func (f *File) searchSheet(name, value string, regSearch bool) ([]string, error) { var ( - inElement string - result []string - r xlsxRow + err error + cellName, inElement string + result []string + cellCol, row int ) d := f.sharedStringsReader() decoder := xml.NewDecoder(bytes.NewReader(f.readXML(name))) @@ -733,31 +731,38 @@ func (f *File) searchSheet(name, value string, regSearch bool) ([]string, error) case xml.StartElement: inElement = startElement.Name.Local if inElement == "row" { - r = xlsxRow{} - _ = decoder.DecodeElement(&r, &startElement) - for _, colCell := range r.C { - val, _ := colCell.getValueFrom(f, d) - if regSearch { - regex := regexp.MustCompile(value) - if !regex.MatchString(val) { - continue - } - } else { - if val != value { - continue + for _, attr := range startElement.Attr { + if attr.Name.Local == "r" { + row, err = strconv.Atoi(attr.Value) + if err != nil { + return result, err } } - - cellCol, _, err := CellNameToCoordinates(colCell.R) - if err != nil { - return result, err + } + } + if inElement == "c" { + colCell := xlsxC{} + _ = decoder.DecodeElement(&colCell, &startElement) + val, _ := colCell.getValueFrom(f, d) + if regSearch { + regex := regexp.MustCompile(value) + if !regex.MatchString(val) { + continue } - cellName, err := CoordinatesToCellName(cellCol, r.R) - if err != nil { - return result, err + } else { + if val != value { + continue } - result = append(result, cellName) } + cellCol, _, err = CellNameToCoordinates(colCell.R) + if err != nil { + return result, err + } + cellName, err = CoordinatesToCellName(cellCol, row) + if err != nil { + return result, err + } + result = append(result, cellName) } default: } -- cgit v1.2.1 From 8d6e431dcd8d96dc51f74308e49b5d4a5b2b9d2e Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 28 Nov 2019 21:53:50 +0800 Subject: Resolve #521, fix missing elements when parsing --- excelize.go | 2 +- sheet.go | 2 +- xmlWorksheet.go | 18 ++++++++++++++++++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/excelize.go b/excelize.go index ba6445f..c59ec8c 100644 --- a/excelize.go +++ b/excelize.go @@ -192,7 +192,7 @@ func (f *File) addRels(relPath, relType, target, targetMode string) int { // Office Excel 2007. func replaceWorkSheetsRelationshipsNameSpaceBytes(workbookMarshal []byte) []byte { var oldXmlns = []byte(``) - var newXmlns = []byte(``) + var newXmlns = []byte(``) workbookMarshal = bytes.Replace(workbookMarshal, oldXmlns, newXmlns, -1) return workbookMarshal } diff --git a/sheet.go b/sheet.go index c2e6bf6..566e6e7 100644 --- a/sheet.go +++ b/sheet.go @@ -207,7 +207,7 @@ func replaceRelationshipsBytes(content []byte) []byte { // a horrible hack to fix that after the XML marshalling is completed. func replaceRelationshipsNameSpaceBytes(workbookMarshal []byte) []byte { oldXmlns := []byte(``) - newXmlns := []byte(``) + newXmlns := []byte(``) return bytes.Replace(workbookMarshal, oldXmlns, newXmlns, -1) } diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 8408cfa..cb854cd 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -22,8 +22,13 @@ type xlsxWorksheet struct { SheetFormatPr *xlsxSheetFormatPr `xml:"sheetFormatPr"` Cols *xlsxCols `xml:"cols,omitempty"` SheetData xlsxSheetData `xml:"sheetData"` + SheetCalcPr *xlsxInnerXML `xml:"sheetCalcPr"` SheetProtection *xlsxSheetProtection `xml:"sheetProtection"` + ProtectedRanges *xlsxInnerXML `xml:"protectedRanges"` + Scenarios *xlsxInnerXML `xml:"scenarios"` AutoFilter *xlsxAutoFilter `xml:"autoFilter"` + SortState *xlsxInnerXML `xml:"sortState"` + DataConsolidate *xlsxInnerXML `xml:"dataConsolidate"` CustomSheetViews *xlsxCustomSheetViews `xml:"customSheetViews"` MergeCells *xlsxMergeCells `xml:"mergeCells"` PhoneticPr *xlsxPhoneticPr `xml:"phoneticPr"` @@ -36,9 +41,18 @@ type xlsxWorksheet struct { HeaderFooter *xlsxHeaderFooter `xml:"headerFooter"` RowBreaks *xlsxBreaks `xml:"rowBreaks"` ColBreaks *xlsxBreaks `xml:"colBreaks"` + CustomProperties *xlsxInnerXML `xml:"customProperties"` + CellWatches *xlsxInnerXML `xml:"cellWatches"` + IgnoredErrors *xlsxInnerXML `xml:"ignoredErrors"` + SmartTags *xlsxInnerXML `xml:"smartTags"` Drawing *xlsxDrawing `xml:"drawing"` LegacyDrawing *xlsxLegacyDrawing `xml:"legacyDrawing"` + LegacyDrawingHF *xlsxInnerXML `xml:"legacyDrawingHF"` + DrawingHF *xlsxDrawingHF `xml:"drawingHF"` Picture *xlsxPicture `xml:"picture"` + OleObjects *xlsxInnerXML `xml:"oleObjects"` + Controls *xlsxInnerXML `xml:"controls"` + WebPublishItems *xlsxInnerXML `xml:"webPublishItems"` TableParts *xlsxTableParts `xml:"tableParts"` ExtLst *xlsxExtLst `xml:"extLst"` } @@ -631,6 +645,10 @@ type xlsxLegacyDrawing struct { RID string `xml:"http://schemas.openxmlformats.org/officeDocument/2006/relationships id,attr,omitempty"` } +type xlsxInnerXML struct { + Content string `xml:",innerxml"` +} + // xlsxWorksheetExt directly maps the ext element in the worksheet. type xlsxWorksheetExt struct { XMLName xml.Name `xml:"ext"` -- cgit v1.2.1 From 402ad2f62b04d44f1ab866b32b9e7314a713e5f0 Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 30 Nov 2019 00:06:36 +0800 Subject: Update XML namespace --- excelize.go | 4 ++-- sheet.go | 2 +- templates.go | 2 ++ 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/excelize.go b/excelize.go index c59ec8c..cbe7231 100644 --- a/excelize.go +++ b/excelize.go @@ -192,7 +192,7 @@ func (f *File) addRels(relPath, relType, target, targetMode string) int { // Office Excel 2007. func replaceWorkSheetsRelationshipsNameSpaceBytes(workbookMarshal []byte) []byte { var oldXmlns = []byte(``) - var newXmlns = []byte(``) + var newXmlns = []byte(``) - var newXmlns = []byte(``) + var newXmlns = []byte(``) - newXmlns := []byte(``) + newXmlns := []byte(`` const templateTheme = `` + +const templateNamespaceIDMap = ` xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:ap="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties" xmlns:op="http://schemas.openxmlformats.org/officeDocument/2006/custom-properties" xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:ac="http://schemas.openxmlformats.org/officeDocument/2006/characteristics" xmlns:b="http://schemas.openxmlformats.org/officeDocument/2006/bibliography" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:cdr="http://schemas.openxmlformats.org/drawingml/2006/chartDrawing" xmlns:comp="http://schemas.openxmlformats.org/drawingml/2006/compatibility" xmlns:dgm="http://schemas.openxmlformats.org/drawingml/2006/diagram" xmlns:lc="http://schemas.openxmlformats.org/drawingml/2006/lockedCanvas" xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture" xmlns:xdr="http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:ds="http://schemas.openxmlformats.org/officeDocument/2006/customXml" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" xmlns:x="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:sl="http://schemas.openxmlformats.org/schemaLibrary/2006/main" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:xne="http://schemas.microsoft.com/office/excel/2006/main" xmlns:wne="http://schemas.microsoft.com/office/word/2006/wordml" xmlns:mso="http://schemas.microsoft.com/office/2006/01/customui" xmlns:ax="http://schemas.microsoft.com/office/2006/activeX" xmlns:cppr="http://schemas.microsoft.com/office/2006/coverPageProps" xmlns:cdip="http://schemas.microsoft.com/office/2006/customDocumentInformationPanel" xmlns:ct="http://schemas.microsoft.com/office/2006/metadata/contentType" xmlns:ntns="http://schemas.microsoft.com/office/2006/metadata/customXsn" xmlns:lp="http://schemas.microsoft.com/office/2006/metadata/longProperties" xmlns:ma="http://schemas.microsoft.com/office/2006/metadata/properties/metaAttributes" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:inkml="http://www.w3.org/2003/InkML" xmlns:emma="http://www.w3.org/2003/04/emma" xmlns:msink="http://schemas.microsoft.com/ink/2010/main" mc:Ignorable="c14 cdr14 a14 p14 pic14 wp14 w14 x14 xdr14 x14ac mso14 dgm14 wpc wpg wps sle com14 c15 cs we a15 p15 w15 wetp x15 x12ac thm15 x15ac wp15 pRoam tsle p16 a16 cx c16ac c16 xr xr2 xr3 xr4 xr5 xr6 xr7 xr8 xr9 xr10 x16 x16r2 w16se mo mx mv o v" xmlns:c14="http://schemas.microsoft.com/office/drawing/2007/8/2/chart" xmlns:cdr14="http://schemas.microsoft.com/office/drawing/2010/chartDrawing" xmlns:a14="http://schemas.microsoft.com/office/drawing/2010/main" xmlns:p14="http://schemas.microsoft.com/office/powerpoint/2010/main" xmlns:pic14="http://schemas.microsoft.com/office/drawing/2010/picture" xmlns:wp14="http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing" xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml" xmlns:x14="http://schemas.microsoft.com/office/spreadsheetml/2009/9/main" xmlns:xdr14="http://schemas.microsoft.com/office/excel/2010/spreadsheetDrawing" xmlns:x14ac="http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac" xmlns:dsp="http://schemas.microsoft.com/office/drawing/2008/diagram" xmlns:mso14="http://schemas.microsoft.com/office/2009/07/customui" xmlns:dgm14="http://schemas.microsoft.com/office/drawing/2010/diagram" xmlns:wpc="http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas" xmlns:wpg="http://schemas.microsoft.com/office/word/2010/wordprocessingGroup" xmlns:wps="http://schemas.microsoft.com/office/word/2010/wordprocessingShape" xmlns:sle="http://schemas.microsoft.com/office/drawing/2010/slicer" xmlns:com14="http://schemas.microsoft.com/office/drawing/2010/compatibility" xmlns:c15="http://schemas.microsoft.com/office/drawing/2012/chart" xmlns:cs="http://schemas.microsoft.com/office/drawing/2012/chartStyle" xmlns:we="http://schemas.microsoft.com/office/webextensions/webextension/2010/11" xmlns:a15="http://schemas.microsoft.com/office/drawing/2012/main" xmlns:p15="http://schemas.microsoft.com/office/powerpoint/2012/main" xmlns:w15="http://schemas.microsoft.com/office/word/2012/wordml" xmlns:wetp="http://schemas.microsoft.com/office/webextensions/taskpanes/2010/11" xmlns:x15="http://schemas.microsoft.com/office/spreadsheetml/2010/11/main" xmlns:x12ac="http://schemas.microsoft.com/office/spreadsheetml/2011/1/ac" xmlns:thm15="http://schemas.microsoft.com/office/thememl/2012/main" xmlns:x15ac="http://schemas.microsoft.com/office/spreadsheetml/2010/11/ac" xmlns:wp15="http://schemas.microsoft.com/office/word/2012/wordprocessingDrawing" xmlns:pRoam="http://schemas.microsoft.com/office/powerpoint/2012/roamingSettings" xmlns:tsle="http://schemas.microsoft.com/office/drawing/2012/timeslicer" xmlns:p16="http://schemas.microsoft.com/office/powerpoint/2015/main" xmlns:a16="http://schemas.microsoft.com/office/drawing/2014/main" xmlns:cx="http://schemas.microsoft.com/office/drawing/2014/chartex" xmlns:c16ac="http://schemas.microsoft.com/office/drawing/2014/chart/ac" xmlns:c16="http://schemas.microsoft.com/office/drawing/2014/chart" xmlns:xr="http://schemas.microsoft.com/office/spreadsheetml/2014/revision" xmlns:xr2="http://schemas.microsoft.com/office/spreadsheetml/2015/revision2" xmlns:xr3="http://schemas.microsoft.com/office/spreadsheetml/2016/revision3" xmlns:xr4="http://schemas.microsoft.com/office/spreadsheetml/2016/revision4" xmlns:xr5="http://schemas.microsoft.com/office/spreadsheetml/2016/revision5" xmlns:xr6="http://schemas.microsoft.com/office/spreadsheetml/2016/revision6" xmlns:xr7="http://schemas.microsoft.com/office/spreadsheetml/2016/revision7" xmlns:xr8="http://schemas.microsoft.com/office/spreadsheetml/2016/revision8" xmlns:xr9="http://schemas.microsoft.com/office/spreadsheetml/2016/revision9" xmlns:xr10="http://schemas.microsoft.com/office/spreadsheetml/2016/revision10" xmlns:xr11="http://schemas.microsoft.com/office/spreadsheetml/2016/revision11" xmlns:xr12="http://schemas.microsoft.com/office/spreadsheetml/2016/revision12" xmlns:xr13="http://schemas.microsoft.com/office/spreadsheetml/2016/revision13" xmlns:xr14="http://schemas.microsoft.com/office/spreadsheetml/2016/revision14" xmlns:xr15="http://schemas.microsoft.com/office/spreadsheetml/2016/revision15" xmlns:xr16="http://schemas.microsoft.com/office/spreadsheetml/2017/revision16" xmlns:xr17="http://schemas.microsoft.com/office/spreadsheetml/2017/revision17" xmlns:xr18="http://schemas.microsoft.com/office/spreadsheetml/2017/revision18" xmlns:xr19="http://schemas.microsoft.com/office/spreadsheetml/2017/revision19" xmlns:xr20="http://schemas.microsoft.com/office/spreadsheetml/2017/revision20" xmlns:xr21="http://schemas.microsoft.com/office/spreadsheetml/2018/revision21" xmlns:xr22="http://schemas.microsoft.com/office/spreadsheetml/2018/revision22" xmlns:x16="http://schemas.microsoft.com/office/spreadsheetml/2014/11/main" xmlns:x16r2="http://schemas.microsoft.com/office/spreadsheetml/2015/02/main" xmlns:w16se="http://schemas.microsoft.com/office/word/2015/wordml/symex" xmlns:mo="http://schemas.microsoft.com/office/mac/office/2008/main" xmlns:mx="http://schemas.microsoft.com/office/mac/excel/2008/main" xmlns:mv="urn:schemas-microsoft-com:mac:vml" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:v="urn:schemas-microsoft-com:vml" xr:uid="{00000000-0001-0000-0000-000000000000}">` -- cgit v1.2.1 From 842b942c71df8a7bcc6e8f32232851679bd6a090 Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 2 Dec 2019 22:39:32 +0800 Subject: Compatible with up to 64 namespaces of Kingsoft WPS --- templates.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates.go b/templates.go index 7af5504..5b79b0c 100644 --- a/templates.go +++ b/templates.go @@ -39,4 +39,4 @@ const templateRels = `` -const templateNamespaceIDMap = ` xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:ap="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties" xmlns:op="http://schemas.openxmlformats.org/officeDocument/2006/custom-properties" xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:ac="http://schemas.openxmlformats.org/officeDocument/2006/characteristics" xmlns:b="http://schemas.openxmlformats.org/officeDocument/2006/bibliography" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:cdr="http://schemas.openxmlformats.org/drawingml/2006/chartDrawing" xmlns:comp="http://schemas.openxmlformats.org/drawingml/2006/compatibility" xmlns:dgm="http://schemas.openxmlformats.org/drawingml/2006/diagram" xmlns:lc="http://schemas.openxmlformats.org/drawingml/2006/lockedCanvas" xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture" xmlns:xdr="http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:ds="http://schemas.openxmlformats.org/officeDocument/2006/customXml" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" xmlns:x="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:sl="http://schemas.openxmlformats.org/schemaLibrary/2006/main" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:xne="http://schemas.microsoft.com/office/excel/2006/main" xmlns:wne="http://schemas.microsoft.com/office/word/2006/wordml" xmlns:mso="http://schemas.microsoft.com/office/2006/01/customui" xmlns:ax="http://schemas.microsoft.com/office/2006/activeX" xmlns:cppr="http://schemas.microsoft.com/office/2006/coverPageProps" xmlns:cdip="http://schemas.microsoft.com/office/2006/customDocumentInformationPanel" xmlns:ct="http://schemas.microsoft.com/office/2006/metadata/contentType" xmlns:ntns="http://schemas.microsoft.com/office/2006/metadata/customXsn" xmlns:lp="http://schemas.microsoft.com/office/2006/metadata/longProperties" xmlns:ma="http://schemas.microsoft.com/office/2006/metadata/properties/metaAttributes" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:inkml="http://www.w3.org/2003/InkML" xmlns:emma="http://www.w3.org/2003/04/emma" xmlns:msink="http://schemas.microsoft.com/ink/2010/main" mc:Ignorable="c14 cdr14 a14 p14 pic14 wp14 w14 x14 xdr14 x14ac mso14 dgm14 wpc wpg wps sle com14 c15 cs we a15 p15 w15 wetp x15 x12ac thm15 x15ac wp15 pRoam tsle p16 a16 cx c16ac c16 xr xr2 xr3 xr4 xr5 xr6 xr7 xr8 xr9 xr10 x16 x16r2 w16se mo mx mv o v" xmlns:c14="http://schemas.microsoft.com/office/drawing/2007/8/2/chart" xmlns:cdr14="http://schemas.microsoft.com/office/drawing/2010/chartDrawing" xmlns:a14="http://schemas.microsoft.com/office/drawing/2010/main" xmlns:p14="http://schemas.microsoft.com/office/powerpoint/2010/main" xmlns:pic14="http://schemas.microsoft.com/office/drawing/2010/picture" xmlns:wp14="http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing" xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml" xmlns:x14="http://schemas.microsoft.com/office/spreadsheetml/2009/9/main" xmlns:xdr14="http://schemas.microsoft.com/office/excel/2010/spreadsheetDrawing" xmlns:x14ac="http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac" xmlns:dsp="http://schemas.microsoft.com/office/drawing/2008/diagram" xmlns:mso14="http://schemas.microsoft.com/office/2009/07/customui" xmlns:dgm14="http://schemas.microsoft.com/office/drawing/2010/diagram" xmlns:wpc="http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas" xmlns:wpg="http://schemas.microsoft.com/office/word/2010/wordprocessingGroup" xmlns:wps="http://schemas.microsoft.com/office/word/2010/wordprocessingShape" xmlns:sle="http://schemas.microsoft.com/office/drawing/2010/slicer" xmlns:com14="http://schemas.microsoft.com/office/drawing/2010/compatibility" xmlns:c15="http://schemas.microsoft.com/office/drawing/2012/chart" xmlns:cs="http://schemas.microsoft.com/office/drawing/2012/chartStyle" xmlns:we="http://schemas.microsoft.com/office/webextensions/webextension/2010/11" xmlns:a15="http://schemas.microsoft.com/office/drawing/2012/main" xmlns:p15="http://schemas.microsoft.com/office/powerpoint/2012/main" xmlns:w15="http://schemas.microsoft.com/office/word/2012/wordml" xmlns:wetp="http://schemas.microsoft.com/office/webextensions/taskpanes/2010/11" xmlns:x15="http://schemas.microsoft.com/office/spreadsheetml/2010/11/main" xmlns:x12ac="http://schemas.microsoft.com/office/spreadsheetml/2011/1/ac" xmlns:thm15="http://schemas.microsoft.com/office/thememl/2012/main" xmlns:x15ac="http://schemas.microsoft.com/office/spreadsheetml/2010/11/ac" xmlns:wp15="http://schemas.microsoft.com/office/word/2012/wordprocessingDrawing" xmlns:pRoam="http://schemas.microsoft.com/office/powerpoint/2012/roamingSettings" xmlns:tsle="http://schemas.microsoft.com/office/drawing/2012/timeslicer" xmlns:p16="http://schemas.microsoft.com/office/powerpoint/2015/main" xmlns:a16="http://schemas.microsoft.com/office/drawing/2014/main" xmlns:cx="http://schemas.microsoft.com/office/drawing/2014/chartex" xmlns:c16ac="http://schemas.microsoft.com/office/drawing/2014/chart/ac" xmlns:c16="http://schemas.microsoft.com/office/drawing/2014/chart" xmlns:xr="http://schemas.microsoft.com/office/spreadsheetml/2014/revision" xmlns:xr2="http://schemas.microsoft.com/office/spreadsheetml/2015/revision2" xmlns:xr3="http://schemas.microsoft.com/office/spreadsheetml/2016/revision3" xmlns:xr4="http://schemas.microsoft.com/office/spreadsheetml/2016/revision4" xmlns:xr5="http://schemas.microsoft.com/office/spreadsheetml/2016/revision5" xmlns:xr6="http://schemas.microsoft.com/office/spreadsheetml/2016/revision6" xmlns:xr7="http://schemas.microsoft.com/office/spreadsheetml/2016/revision7" xmlns:xr8="http://schemas.microsoft.com/office/spreadsheetml/2016/revision8" xmlns:xr9="http://schemas.microsoft.com/office/spreadsheetml/2016/revision9" xmlns:xr10="http://schemas.microsoft.com/office/spreadsheetml/2016/revision10" xmlns:xr11="http://schemas.microsoft.com/office/spreadsheetml/2016/revision11" xmlns:xr12="http://schemas.microsoft.com/office/spreadsheetml/2016/revision12" xmlns:xr13="http://schemas.microsoft.com/office/spreadsheetml/2016/revision13" xmlns:xr14="http://schemas.microsoft.com/office/spreadsheetml/2016/revision14" xmlns:xr15="http://schemas.microsoft.com/office/spreadsheetml/2016/revision15" xmlns:xr16="http://schemas.microsoft.com/office/spreadsheetml/2017/revision16" xmlns:xr17="http://schemas.microsoft.com/office/spreadsheetml/2017/revision17" xmlns:xr18="http://schemas.microsoft.com/office/spreadsheetml/2017/revision18" xmlns:xr19="http://schemas.microsoft.com/office/spreadsheetml/2017/revision19" xmlns:xr20="http://schemas.microsoft.com/office/spreadsheetml/2017/revision20" xmlns:xr21="http://schemas.microsoft.com/office/spreadsheetml/2018/revision21" xmlns:xr22="http://schemas.microsoft.com/office/spreadsheetml/2018/revision22" xmlns:x16="http://schemas.microsoft.com/office/spreadsheetml/2014/11/main" xmlns:x16r2="http://schemas.microsoft.com/office/spreadsheetml/2015/02/main" xmlns:w16se="http://schemas.microsoft.com/office/word/2015/wordml/symex" xmlns:mo="http://schemas.microsoft.com/office/mac/office/2008/main" xmlns:mx="http://schemas.microsoft.com/office/mac/excel/2008/main" xmlns:mv="urn:schemas-microsoft-com:mac:vml" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:v="urn:schemas-microsoft-com:vml" xr:uid="{00000000-0001-0000-0000-000000000000}">` +const templateNamespaceIDMap = ` xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:ap="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties" xmlns:op="http://schemas.openxmlformats.org/officeDocument/2006/custom-properties" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:cdr="http://schemas.openxmlformats.org/drawingml/2006/chartDrawing" xmlns:comp="http://schemas.openxmlformats.org/drawingml/2006/compatibility" xmlns:dgm="http://schemas.openxmlformats.org/drawingml/2006/diagram" xmlns:lc="http://schemas.openxmlformats.org/drawingml/2006/lockedCanvas" xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture" xmlns:xdr="http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:ds="http://schemas.openxmlformats.org/officeDocument/2006/customXml" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" xmlns:x="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:sl="http://schemas.openxmlformats.org/schemaLibrary/2006/main" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:xne="http://schemas.microsoft.com/office/excel/2006/main" xmlns:mso="http://schemas.microsoft.com/office/2006/01/customui" xmlns:ax="http://schemas.microsoft.com/office/2006/activeX" xmlns:cppr="http://schemas.microsoft.com/office/2006/coverPageProps" xmlns:cdip="http://schemas.microsoft.com/office/2006/customDocumentInformationPanel" xmlns:ct="http://schemas.microsoft.com/office/2006/metadata/contentType" xmlns:ntns="http://schemas.microsoft.com/office/2006/metadata/customXsn" xmlns:lp="http://schemas.microsoft.com/office/2006/metadata/longProperties" xmlns:ma="http://schemas.microsoft.com/office/2006/metadata/properties/metaAttributes" xmlns:msink="http://schemas.microsoft.com/ink/2010/main" xmlns:c14="http://schemas.microsoft.com/office/drawing/2007/8/2/chart" xmlns:cdr14="http://schemas.microsoft.com/office/drawing/2010/chartDrawing" xmlns:a14="http://schemas.microsoft.com/office/drawing/2010/main" xmlns:pic14="http://schemas.microsoft.com/office/drawing/2010/picture" xmlns:x14="http://schemas.microsoft.com/office/spreadsheetml/2009/9/main" xmlns:xdr14="http://schemas.microsoft.com/office/excel/2010/spreadsheetDrawing" xmlns:x14ac="http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac" xmlns:dsp="http://schemas.microsoft.com/office/drawing/2008/diagram" xmlns:mso14="http://schemas.microsoft.com/office/2009/07/customui" xmlns:dgm14="http://schemas.microsoft.com/office/drawing/2010/diagram" xmlns:x15="http://schemas.microsoft.com/office/spreadsheetml/2010/11/main" xmlns:x12ac="http://schemas.microsoft.com/office/spreadsheetml/2011/1/ac" xmlns:x15ac="http://schemas.microsoft.com/office/spreadsheetml/2010/11/ac" xmlns:xr="http://schemas.microsoft.com/office/spreadsheetml/2014/revision" xmlns:xr2="http://schemas.microsoft.com/office/spreadsheetml/2015/revision2" xmlns:xr3="http://schemas.microsoft.com/office/spreadsheetml/2016/revision3" xmlns:xr4="http://schemas.microsoft.com/office/spreadsheetml/2016/revision4" xmlns:xr5="http://schemas.microsoft.com/office/spreadsheetml/2016/revision5" xmlns:xr6="http://schemas.microsoft.com/office/spreadsheetml/2016/revision6" xmlns:xr7="http://schemas.microsoft.com/office/spreadsheetml/2016/revision7" xmlns:xr8="http://schemas.microsoft.com/office/spreadsheetml/2016/revision8" xmlns:xr9="http://schemas.microsoft.com/office/spreadsheetml/2016/revision9" xmlns:xr10="http://schemas.microsoft.com/office/spreadsheetml/2016/revision10" xmlns:xr11="http://schemas.microsoft.com/office/spreadsheetml/2016/revision11" xmlns:xr12="http://schemas.microsoft.com/office/spreadsheetml/2016/revision12" xmlns:xr13="http://schemas.microsoft.com/office/spreadsheetml/2016/revision13" xmlns:xr14="http://schemas.microsoft.com/office/spreadsheetml/2016/revision14" xmlns:xr15="http://schemas.microsoft.com/office/spreadsheetml/2016/revision15" xmlns:x16="http://schemas.microsoft.com/office/spreadsheetml/2014/11/main" xmlns:x16r2="http://schemas.microsoft.com/office/spreadsheetml/2015/02/main" mc:Ignorable="c14 cdr14 a14 pic14 x14 xdr14 x14ac dsp mso14 dgm14 x15 x12ac x15ac xr xr2 xr3 xr4 xr5 xr6 xr7 xr8 xr9 xr10 xr11 xr12 xr13 xr14 xr15 x15 x16 x16r2 mo mx mv o v" xmlns:mo="http://schemas.microsoft.com/office/mac/office/2008/main" xmlns:mx="http://schemas.microsoft.com/office/mac/excel/2008/main" xmlns:mv="urn:schemas-microsoft-com:mac:vml" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:v="urn:schemas-microsoft-com:vml" xr:uid="{00000000-0001-0000-0000-000000000000}">` -- cgit v1.2.1 From 08d1a86c3a1bffdf431dba6a3d5a3b369ef740a7 Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 10 Dec 2019 00:16:17 +0800 Subject: Fix #523, add stream writer for generate new worksheet with huge amounts of data --- stream.go | 219 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ stream_test.go | 66 +++++++++++++++++ xmlTable.go | 1 + xmlWorksheet.go | 180 +++++++++++++++++++++++++--------------------- 4 files changed, 386 insertions(+), 80 deletions(-) create mode 100644 stream.go create mode 100644 stream_test.go diff --git a/stream.go b/stream.go new file mode 100644 index 0000000..0d91ddd --- /dev/null +++ b/stream.go @@ -0,0 +1,219 @@ +// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// this source code is governed by a BSD-style license that can be found in +// the LICENSE file. +// +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.10 or later. + +package excelize + +import ( + "bytes" + "encoding/xml" + "errors" + "fmt" + "io/ioutil" + "os" + "reflect" +) + +// StreamWriter defined the type of stream writer. +type StreamWriter struct { + tmpFile *os.File + File *File + Sheet string + SheetID int + SheetData bytes.Buffer +} + +// NewStreamWriter return stream writer struct by given worksheet name for +// generate new worksheet with large amounts of data. Note that after set +// rows, you must call the 'Flush' method to end the streaming writing +// process and ensure that the order of line numbers is ascending. For +// example, set data for worksheet of size 102400 rows x 50 columns with +// numbers: +// +// file := excelize.NewFile() +// streamWriter, err := file.NewStreamWriter("Sheet1") +// if err != nil { +// panic(err) +// } +// for rowID := 1; rowID <= 102400; rowID++ { +// row := make([]interface{}, 50) +// for colID := 0; colID < 50; colID++ { +// row[colID] = rand.Intn(640000) +// } +// cell, _ := excelize.CoordinatesToCellName(1, rowID) +// if err := streamWriter.SetRow(cell, &row); err != nil { +// panic(err) +// } +// } +// if err := streamWriter.Flush(); err != nil { +// panic(err) +// } +// if err := file.SaveAs("Book1.xlsx"); err != nil { +// panic(err) +// } +// +func (f *File) NewStreamWriter(sheet string) (*StreamWriter, error) { + sheetID := f.GetSheetIndex(sheet) + if sheetID == 0 { + return nil, fmt.Errorf("sheet %s is not exist", sheet) + } + rsw := &StreamWriter{ + File: f, + Sheet: sheet, + SheetID: sheetID, + } + rsw.SheetData.WriteString("") + return rsw, nil +} + +// SetRow writes an array to streaming row by given worksheet name, starting +// coordinate and a pointer to array type 'slice'. Note that, cell settings +// with styles are not supported currently and after set rows, you must call the +// 'Flush' method to end the streaming writing process. The following +// shows the supported data types: +// +// int +// string +// +func (sw *StreamWriter) SetRow(axis string, slice interface{}) error { + col, row, err := CellNameToCoordinates(axis) + if err != nil { + return err + } + // Make sure 'slice' is a Ptr to Slice + v := reflect.ValueOf(slice) + if v.Kind() != reflect.Ptr || v.Elem().Kind() != reflect.Slice { + return errors.New("pointer to slice expected") + } + v = v.Elem() + sw.SheetData.WriteString(fmt.Sprintf(``, row)) + for i := 0; i < v.Len(); i++ { + axis, err := CoordinatesToCellName(col+i, row) + if err != nil { + return err + } + switch val := v.Index(i).Interface().(type) { + case int: + sw.SheetData.WriteString(fmt.Sprintf(`%d`, axis, val)) + case string: + sw.SheetData.WriteString(sw.setCellStr(axis, val)) + default: + sw.SheetData.WriteString(sw.setCellStr(axis, fmt.Sprint(val))) + } + } + sw.SheetData.WriteString(``) + // Try to use local storage + chunk := 1 << 24 + if sw.SheetData.Len() >= chunk { + if sw.tmpFile == nil { + err := sw.createTmp() + if err != nil { + // can not use local storage + return nil + } + } + // use local storage + _, err := sw.tmpFile.Write(sw.SheetData.Bytes()) + if err != nil { + return nil + } + sw.SheetData.Reset() + } + return err +} + +// Flush ending the streaming writing process. +func (sw *StreamWriter) Flush() error { + sw.SheetData.WriteString(``) + + ws, err := sw.File.workSheetReader(sw.Sheet) + if err != nil { + return err + } + sheetXML := fmt.Sprintf("xl/worksheets/sheet%d.xml", sw.SheetID) + delete(sw.File.Sheet, sheetXML) + delete(sw.File.checked, sheetXML) + var sheetDataByte []byte + if sw.tmpFile != nil { + // close the local storage file + if err = sw.tmpFile.Close(); err != nil { + return err + } + + file, err := os.Open(sw.tmpFile.Name()) + if err != nil { + return err + } + + sheetDataByte, err = ioutil.ReadAll(file) + if err != nil { + return err + } + + if err := file.Close(); err != nil { + return err + } + + err = os.Remove(sw.tmpFile.Name()) + if err != nil { + return err + } + } + + sheetDataByte = append(sheetDataByte, sw.SheetData.Bytes()...) + replaceMap := map[string][]byte{ + "XMLName": []byte{}, + "SheetData": sheetDataByte, + } + sw.SheetData.Reset() + sw.File.XLSX[fmt.Sprintf("xl/worksheets/sheet%d.xml", sw.SheetID)] = + StreamMarshalSheet(ws, replaceMap) + return err +} + +// createTmp creates a temporary file in the operating system default +// temporary directory. +func (sw *StreamWriter) createTmp() (err error) { + sw.tmpFile, err = ioutil.TempFile(os.TempDir(), "excelize-") + return err +} + +// StreamMarshalSheet provides method to serialization worksheets by field as +// streaming. +func StreamMarshalSheet(ws *xlsxWorksheet, replaceMap map[string][]byte) []byte { + s := reflect.ValueOf(ws).Elem() + typeOfT := s.Type() + var marshalResult []byte + marshalResult = append(marshalResult, []byte(XMLHeader+``)...) + return marshalResult +} + +// setCellStr provides a function to set string type value of a cell as +// streaming. Total number of characters that a cell can contain 32767 +// characters. +func (sw *StreamWriter) setCellStr(axis, value string) string { + if len(value) > 32767 { + value = value[0:32767] + } + // Leading and ending space(s) character detection. + if len(value) > 0 && (value[0] == 32 || value[len(value)-1] == 32) { + return fmt.Sprintf(`%s`, axis, value) + } + return fmt.Sprintf(`%s`, axis, value) +} diff --git a/stream_test.go b/stream_test.go new file mode 100644 index 0000000..97c55a7 --- /dev/null +++ b/stream_test.go @@ -0,0 +1,66 @@ +package excelize + +import ( + "math/rand" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestStreamWriter(t *testing.T) { + file := NewFile() + streamWriter, err := file.NewStreamWriter("Sheet1") + assert.NoError(t, err) + + // Test max characters in a cell. + row := make([]interface{}, 1) + row[0] = strings.Repeat("c", 32769) + assert.NoError(t, streamWriter.SetRow("A1", &row)) + + // Test leading and ending space(s) character characters in a cell. + row = make([]interface{}, 1) + row[0] = " characters" + assert.NoError(t, streamWriter.SetRow("A2", &row)) + + row = make([]interface{}, 1) + row[0] = []byte("Word") + assert.NoError(t, streamWriter.SetRow("A3", &row)) + + for rowID := 10; rowID <= 51200; rowID++ { + row := make([]interface{}, 50) + for colID := 0; colID < 50; colID++ { + row[colID] = rand.Intn(640000) + } + cell, _ := CoordinatesToCellName(1, rowID) + assert.NoError(t, streamWriter.SetRow(cell, &row)) + } + + err = streamWriter.Flush() + assert.NoError(t, err) + // Save xlsx file by the given path. + assert.NoError(t, file.SaveAs(filepath.Join("test", "TestStreamWriter.xlsx"))) + + // Test error exceptions + streamWriter, err = file.NewStreamWriter("SheetN") + assert.EqualError(t, err, "sheet SheetN is not exist") +} + +func TestFlush(t *testing.T) { + // Test error exceptions + file := NewFile() + streamWriter, err := file.NewStreamWriter("Sheet1") + assert.NoError(t, err) + streamWriter.Sheet = "SheetN" + assert.EqualError(t, streamWriter.Flush(), "sheet SheetN is not exist") +} + +func TestSetRow(t *testing.T) { + // Test error exceptions + file := NewFile() + streamWriter, err := file.NewStreamWriter("Sheet1") + assert.NoError(t, err) + assert.EqualError(t, streamWriter.SetRow("A", &[]interface{}{}), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + assert.EqualError(t, streamWriter.SetRow("A1", []interface{}{}), `pointer to slice expected`) +} diff --git a/xmlTable.go b/xmlTable.go index ca4ce03..017bda1 100644 --- a/xmlTable.go +++ b/xmlTable.go @@ -44,6 +44,7 @@ type xlsxTable struct { // applied column by column to a table of data in the worksheet. This collection // expresses AutoFilter settings. type xlsxAutoFilter struct { + XMLName xml.Name `xml:"autoFilter"` Ref string `xml:"ref,attr"` FilterColumn *xlsxFilterColumn `xml:"filterColumn"` } diff --git a/xmlWorksheet.go b/xmlWorksheet.go index cb854cd..a071e4d 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -59,7 +59,8 @@ type xlsxWorksheet struct { // xlsxDrawing change r:id to rid in the namespace. type xlsxDrawing struct { - RID string `xml:"http://schemas.openxmlformats.org/officeDocument/2006/relationships id,attr,omitempty"` + XMLName xml.Name `xml:"drawing"` + RID string `xml:"http://schemas.openxmlformats.org/officeDocument/2006/relationships id,attr,omitempty"` } // xlsxHeaderFooter directly maps the headerFooter element in the namespace @@ -70,6 +71,7 @@ type xlsxDrawing struct { // footers on the first page can differ from those on odd- and even-numbered // pages. In the latter case, the first page is not considered an odd page. type xlsxHeaderFooter struct { + XMLName xml.Name `xml:"headerFooter"` AlignWithMargins bool `xml:"alignWithMargins,attr,omitempty"` DifferentFirst bool `xml:"differentFirst,attr,omitempty"` DifferentOddEven bool `xml:"differentOddEven,attr,omitempty"` @@ -91,32 +93,33 @@ type xlsxHeaderFooter struct { // each of the left section, center section and right section of a header and // a footer. type xlsxDrawingHF struct { - Content string `xml:",chardata"` + Content string `xml:",innerxml"` } // xlsxPageSetUp directly maps the pageSetup element in the namespace // http://schemas.openxmlformats.org/spreadsheetml/2006/main - Page setup // settings for the worksheet. type xlsxPageSetUp struct { - BlackAndWhite bool `xml:"blackAndWhite,attr,omitempty"` - CellComments string `xml:"cellComments,attr,omitempty"` - Copies int `xml:"copies,attr,omitempty"` - Draft bool `xml:"draft,attr,omitempty"` - Errors string `xml:"errors,attr,omitempty"` - FirstPageNumber int `xml:"firstPageNumber,attr,omitempty"` - FitToHeight int `xml:"fitToHeight,attr,omitempty"` - FitToWidth int `xml:"fitToWidth,attr,omitempty"` - HorizontalDPI float32 `xml:"horizontalDpi,attr,omitempty"` - RID string `xml:"http://schemas.openxmlformats.org/officeDocument/2006/relationships id,attr,omitempty"` - Orientation string `xml:"orientation,attr,omitempty"` - PageOrder string `xml:"pageOrder,attr,omitempty"` - PaperHeight string `xml:"paperHeight,attr,omitempty"` - PaperSize int `xml:"paperSize,attr,omitempty"` - PaperWidth string `xml:"paperWidth,attr,omitempty"` - Scale int `xml:"scale,attr,omitempty"` - UseFirstPageNumber bool `xml:"useFirstPageNumber,attr,omitempty"` - UsePrinterDefaults bool `xml:"usePrinterDefaults,attr,omitempty"` - VerticalDPI float32 `xml:"verticalDpi,attr,omitempty"` + XMLName xml.Name `xml:"pageSetup"` + BlackAndWhite bool `xml:"blackAndWhite,attr,omitempty"` + CellComments string `xml:"cellComments,attr,omitempty"` + Copies int `xml:"copies,attr,omitempty"` + Draft bool `xml:"draft,attr,omitempty"` + Errors string `xml:"errors,attr,omitempty"` + FirstPageNumber int `xml:"firstPageNumber,attr,omitempty"` + FitToHeight int `xml:"fitToHeight,attr,omitempty"` + FitToWidth int `xml:"fitToWidth,attr,omitempty"` + HorizontalDPI float32 `xml:"horizontalDpi,attr,omitempty"` + RID string `xml:"http://schemas.openxmlformats.org/officeDocument/2006/relationships id,attr,omitempty"` + Orientation string `xml:"orientation,attr,omitempty"` + PageOrder string `xml:"pageOrder,attr,omitempty"` + PaperHeight string `xml:"paperHeight,attr,omitempty"` + PaperSize int `xml:"paperSize,attr,omitempty"` + PaperWidth string `xml:"paperWidth,attr,omitempty"` + Scale int `xml:"scale,attr,omitempty"` + UseFirstPageNumber bool `xml:"useFirstPageNumber,attr,omitempty"` + UsePrinterDefaults bool `xml:"usePrinterDefaults,attr,omitempty"` + VerticalDPI float32 `xml:"verticalDpi,attr,omitempty"` } // xlsxPrintOptions directly maps the printOptions element in the namespace @@ -124,44 +127,48 @@ type xlsxPageSetUp struct { // the sheet. Printer-specific settings are stored separately in the Printer // Settings part. type xlsxPrintOptions struct { - GridLines bool `xml:"gridLines,attr,omitempty"` - GridLinesSet bool `xml:"gridLinesSet,attr,omitempty"` - Headings bool `xml:"headings,attr,omitempty"` - HorizontalCentered bool `xml:"horizontalCentered,attr,omitempty"` - VerticalCentered bool `xml:"verticalCentered,attr,omitempty"` + XMLName xml.Name `xml:"printOptions"` + GridLines bool `xml:"gridLines,attr,omitempty"` + GridLinesSet bool `xml:"gridLinesSet,attr,omitempty"` + Headings bool `xml:"headings,attr,omitempty"` + HorizontalCentered bool `xml:"horizontalCentered,attr,omitempty"` + VerticalCentered bool `xml:"verticalCentered,attr,omitempty"` } // xlsxPageMargins directly maps the pageMargins element in the namespace // http://schemas.openxmlformats.org/spreadsheetml/2006/main - Page margins for // a sheet or a custom sheet view. type xlsxPageMargins struct { - Bottom float64 `xml:"bottom,attr"` - Footer float64 `xml:"footer,attr"` - Header float64 `xml:"header,attr"` - Left float64 `xml:"left,attr"` - Right float64 `xml:"right,attr"` - Top float64 `xml:"top,attr"` + XMLName xml.Name `xml:"pageMargins"` + Bottom float64 `xml:"bottom,attr"` + Footer float64 `xml:"footer,attr"` + Header float64 `xml:"header,attr"` + Left float64 `xml:"left,attr"` + Right float64 `xml:"right,attr"` + Top float64 `xml:"top,attr"` } // xlsxSheetFormatPr directly maps the sheetFormatPr element in the namespace // http://schemas.openxmlformats.org/spreadsheetml/2006/main. This element // specifies the sheet formatting properties. type xlsxSheetFormatPr struct { - BaseColWidth uint8 `xml:"baseColWidth,attr,omitempty"` - DefaultColWidth float64 `xml:"defaultColWidth,attr,omitempty"` - DefaultRowHeight float64 `xml:"defaultRowHeight,attr"` - CustomHeight bool `xml:"customHeight,attr,omitempty"` - ZeroHeight bool `xml:"zeroHeight,attr,omitempty"` - ThickTop bool `xml:"thickTop,attr,omitempty"` - ThickBottom bool `xml:"thickBottom,attr,omitempty"` - OutlineLevelRow uint8 `xml:"outlineLevelRow,attr,omitempty"` - OutlineLevelCol uint8 `xml:"outlineLevelCol,attr,omitempty"` + XMLName xml.Name `xml:"sheetFormatPr"` + BaseColWidth uint8 `xml:"baseColWidth,attr,omitempty"` + DefaultColWidth float64 `xml:"defaultColWidth,attr,omitempty"` + DefaultRowHeight float64 `xml:"defaultRowHeight,attr"` + CustomHeight bool `xml:"customHeight,attr,omitempty"` + ZeroHeight bool `xml:"zeroHeight,attr,omitempty"` + ThickTop bool `xml:"thickTop,attr,omitempty"` + ThickBottom bool `xml:"thickBottom,attr,omitempty"` + OutlineLevelRow uint8 `xml:"outlineLevelRow,attr,omitempty"` + OutlineLevelCol uint8 `xml:"outlineLevelCol,attr,omitempty"` } // xlsxSheetViews directly maps the sheetViews element in the namespace // http://schemas.openxmlformats.org/spreadsheetml/2006/main - Worksheet views // collection. type xlsxSheetViews struct { + XMLName xml.Name `xml:"sheetViews"` SheetView []xlsxSheetView `xml:"sheetView"` } @@ -263,7 +270,8 @@ type xlsxTabColor struct { // http://schemas.openxmlformats.org/spreadsheetml/2006/main - currently I have // not checked it for completeness - it does as much as I need. type xlsxCols struct { - Col []xlsxCol `xml:"col"` + XMLName xml.Name `xml:"cols"` + Col []xlsxCol `xml:"col"` } // xlsxCol directly maps the col (Column Width & Formatting). Defines column @@ -289,7 +297,8 @@ type xlsxCol struct { // When an entire column is formatted, only the first cell in that column is // considered used. type xlsxDimension struct { - Ref string `xml:"ref,attr"` + XMLName xml.Name `xml:"dimension"` + Ref string `xml:"ref,attr"` } // xlsxSheetData directly maps the sheetData element in the namespace @@ -322,6 +331,7 @@ type xlsxRow struct { // xlsxCustomSheetViews directly maps the customSheetViews element. This is a // collection of custom sheet views. type xlsxCustomSheetViews struct { + XMLName xml.Name `xml:"customSheetViews"` CustomSheetView []*xlsxCustomSheetView `xml:"customSheetView"` } @@ -384,13 +394,15 @@ type xlsxMergeCell struct { // xlsxMergeCells directly maps the mergeCells element. This collection // expresses all the merged cells in the sheet. type xlsxMergeCells struct { - Count int `xml:"count,attr,omitempty"` - Cells []*xlsxMergeCell `xml:"mergeCell,omitempty"` + XMLName xml.Name `xml:"mergeCells"` + Count int `xml:"count,attr,omitempty"` + Cells []*xlsxMergeCell `xml:"mergeCell,omitempty"` } // xlsxDataValidations expresses all data validation information for cells in a // sheet which have data validation features applied. type xlsxDataValidations struct { + XMLName xml.Name `xml:"dataValidations"` Count int `xml:"count,attr,omitempty"` DisablePrompts bool `xml:"disablePrompts,attr,omitempty"` XWindow int `xml:"xWindow,attr,omitempty"` @@ -434,14 +446,15 @@ type DataValidation struct { // str (String) | Cell containing a formula string. // type xlsxC struct { - R string `xml:"r,attr"` // Cell ID, e.g. A1 - S int `xml:"s,attr,omitempty"` // Style reference. - // Str string `xml:"str,attr,omitempty"` // Style reference. - T string `xml:"t,attr,omitempty"` // Type. - F *xlsxF `xml:"f,omitempty"` // Formula - V string `xml:"v,omitempty"` // Value - IS *xlsxSI `xml:"is"` + XMLName xml.Name `xml:"c"` XMLSpace xml.Attr `xml:"space,attr,omitempty"` + R string `xml:"r,attr"` // Cell ID, e.g. A1 + S int `xml:"s,attr,omitempty"` // Style reference. + // Str string `xml:"str,attr,omitempty"` // Style reference. + T string `xml:"t,attr,omitempty"` // Type. + F *xlsxF `xml:"f,omitempty"` // Formula + V string `xml:"v,omitempty"` // Value + IS *xlsxSI `xml:"is"` } func (c *xlsxC) hasValue() bool { @@ -461,27 +474,28 @@ type xlsxF struct { // xlsxSheetProtection collection expresses the sheet protection options to // enforce when the sheet is protected. type xlsxSheetProtection struct { - AlgorithmName string `xml:"algorithmName,attr,omitempty"` - Password string `xml:"password,attr,omitempty"` - HashValue string `xml:"hashValue,attr,omitempty"` - SaltValue string `xml:"saltValue,attr,omitempty"` - SpinCount int `xml:"spinCount,attr,omitempty"` - Sheet bool `xml:"sheet,attr"` - Objects bool `xml:"objects,attr"` - Scenarios bool `xml:"scenarios,attr"` - FormatCells bool `xml:"formatCells,attr"` - FormatColumns bool `xml:"formatColumns,attr"` - FormatRows bool `xml:"formatRows,attr"` - InsertColumns bool `xml:"insertColumns,attr"` - InsertRows bool `xml:"insertRows,attr"` - InsertHyperlinks bool `xml:"insertHyperlinks,attr"` - DeleteColumns bool `xml:"deleteColumns,attr"` - DeleteRows bool `xml:"deleteRows,attr"` - SelectLockedCells bool `xml:"selectLockedCells,attr"` - Sort bool `xml:"sort,attr"` - AutoFilter bool `xml:"autoFilter,attr"` - PivotTables bool `xml:"pivotTables,attr"` - SelectUnlockedCells bool `xml:"selectUnlockedCells,attr"` + XMLName xml.Name `xml:"sheetProtection"` + AlgorithmName string `xml:"algorithmName,attr,omitempty"` + Password string `xml:"password,attr,omitempty"` + HashValue string `xml:"hashValue,attr,omitempty"` + SaltValue string `xml:"saltValue,attr,omitempty"` + SpinCount int `xml:"spinCount,attr,omitempty"` + Sheet bool `xml:"sheet,attr"` + Objects bool `xml:"objects,attr"` + Scenarios bool `xml:"scenarios,attr"` + FormatCells bool `xml:"formatCells,attr"` + FormatColumns bool `xml:"formatColumns,attr"` + FormatRows bool `xml:"formatRows,attr"` + InsertColumns bool `xml:"insertColumns,attr"` + InsertRows bool `xml:"insertRows,attr"` + InsertHyperlinks bool `xml:"insertHyperlinks,attr"` + DeleteColumns bool `xml:"deleteColumns,attr"` + DeleteRows bool `xml:"deleteRows,attr"` + SelectLockedCells bool `xml:"selectLockedCells,attr"` + Sort bool `xml:"sort,attr"` + AutoFilter bool `xml:"autoFilter,attr"` + PivotTables bool `xml:"pivotTables,attr"` + SelectUnlockedCells bool `xml:"selectUnlockedCells,attr"` } // xlsxPhoneticPr (Phonetic Properties) represents a collection of phonetic @@ -492,9 +506,10 @@ type xlsxSheetProtection struct { // every phonetic hint is expressed as a phonetic run (rPh), and these // properties specify how to display that phonetic run. type xlsxPhoneticPr struct { - Alignment string `xml:"alignment,attr,omitempty"` - FontID *int `xml:"fontId,attr"` - Type string `xml:"type,attr,omitempty"` + XMLName xml.Name `xml:"phoneticPr"` + Alignment string `xml:"alignment,attr,omitempty"` + FontID *int `xml:"fontId,attr"` + Type string `xml:"type,attr,omitempty"` } // A Conditional Format is a format, such as cell shading or font color, that a @@ -502,8 +517,9 @@ type xlsxPhoneticPr struct { // condition is true. This collection expresses conditional formatting rules // applied to a particular cell or range. type xlsxConditionalFormatting struct { - SQRef string `xml:"sqref,attr,omitempty"` - CfRule []*xlsxCfRule `xml:"cfRule"` + XMLName xml.Name `xml:"conditionalFormatting"` + SQRef string `xml:"sqref,attr,omitempty"` + CfRule []*xlsxCfRule `xml:"cfRule"` } // xlsxCfRule (Conditional Formatting Rule) represents a description of a @@ -568,6 +584,7 @@ type xlsxCfvo struct { // be stored in a package as a relationship. Hyperlinks shall be identified by // containing a target which specifies the destination of the given hyperlink. type xlsxHyperlinks struct { + XMLName xml.Name `xml:"hyperlinks"` Hyperlink []xlsxHyperlink `xml:"hyperlink"` } @@ -612,6 +629,7 @@ type xlsxHyperlink struct { // // type xlsxTableParts struct { + XMLName xml.Name `xml:"tableParts"` Count int `xml:"count,attr,omitempty"` TableParts []*xlsxTablePart `xml:"tablePart"` } @@ -629,7 +647,8 @@ type xlsxTablePart struct { // // type xlsxPicture struct { - RID string `xml:"http://schemas.openxmlformats.org/officeDocument/2006/relationships id,attr,omitempty"` + XMLName xml.Name `xml:"picture"` + RID string `xml:"http://schemas.openxmlformats.org/officeDocument/2006/relationships id,attr,omitempty"` } // xlsxLegacyDrawing directly maps the legacyDrawing element in the namespace @@ -642,7 +661,8 @@ type xlsxPicture struct { // can also be used to explain assumptions made in a formula or to call out // something special about the cell. type xlsxLegacyDrawing struct { - RID string `xml:"http://schemas.openxmlformats.org/officeDocument/2006/relationships id,attr,omitempty"` + XMLName xml.Name `xml:"legacyDrawing"` + RID string `xml:"http://schemas.openxmlformats.org/officeDocument/2006/relationships id,attr,omitempty"` } type xlsxInnerXML struct { -- cgit v1.2.1 From 5d8365ca17240f5b144d437a7b47052f22c4f3c6 Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 11 Dec 2019 00:02:33 +0800 Subject: Fix #529, handle empty inline rich text --- rows.go | 5 ++++- rows_test.go | 9 +++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/rows.go b/rows.go index 69a9846..ff4aa0f 100644 --- a/rows.go +++ b/rows.go @@ -279,7 +279,10 @@ func (xlsx *xlsxC) getValueFrom(f *File, d *xlsxSST) (string, error) { case "str": return f.formattedValue(xlsx.S, xlsx.V), nil case "inlineStr": - return f.formattedValue(xlsx.S, xlsx.IS.String()), nil + if xlsx.IS != nil { + return f.formattedValue(xlsx.S, xlsx.IS.String()), nil + } + return f.formattedValue(xlsx.S, xlsx.V), nil default: return f.formattedValue(xlsx.S, xlsx.V), nil } diff --git a/rows_test.go b/rows_test.go index 47c9d96..6b50c75 100644 --- a/rows_test.go +++ b/rows_test.go @@ -706,6 +706,15 @@ func TestDuplicateRowInvalidRownum(t *testing.T) { } } +func TestGetValueFrom(t *testing.T) { + c := &xlsxC{T: "inlineStr"} + f := NewFile() + d := &xlsxSST{} + val, err := c.getValueFrom(f, d) + assert.NoError(t, err) + assert.Equal(t, "", val) +} + func TestErrSheetNotExistError(t *testing.T) { err := ErrSheetNotExist{SheetName: "Sheet1"} assert.EqualValues(t, err.Error(), "sheet Sheet1 is not exist") -- cgit v1.2.1 From 4c433c57e65734094f959d25b50f138a6ca88020 Mon Sep 17 00:00:00 2001 From: Xudong Zhang Date: Fri, 13 Dec 2019 21:43:59 +0800 Subject: Resolve #527, unmerge an area (#528) --- cellmerged.go | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ cellmerged_test.go | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+) diff --git a/cellmerged.go b/cellmerged.go index c1df9b3..4a5d11f 100644 --- a/cellmerged.go +++ b/cellmerged.go @@ -33,6 +33,56 @@ func (f *File) GetMergeCells(sheet string) ([]MergeCell, error) { return mergeCells, err } +// UnmergeCell provides a function to unmerge a given coordinate area. +// For example unmerge area D3:E9 on Sheet1: +// +// err := f.UnmergeCell("Sheet1", "D3", "E9") +// +// Attention: overlapped areas will also be unmerged. +func (f *File) UnmergeCell(sheet string, hcell, vcell string) error { + xlsx, err := f.workSheetReader(sheet) + if err != nil { + return err + } + coordinates, err := f.areaRefToCoordinates(hcell + ":" + vcell) + if err != nil { + return err + } + x1, y1, x2, y2 := coordinates[0], coordinates[1], coordinates[2], coordinates[3] + + if x2 < x1 { + x1, x2 = x2, x1 + } + if y2 < y1 { + y1, y2 = y2, y1 + } + hcell, _ = CoordinatesToCellName(x1, y1) + vcell, _ = CoordinatesToCellName(x2, y2) + + // return nil since no MergeCells in the sheet + if xlsx.MergeCells == nil { + return nil + } + + ref := hcell + ":" + vcell + i := 0 + for _, cellData := range xlsx.MergeCells.Cells { + cc := strings.Split(cellData.Ref, ":") + c1, _ := checkCellInArea(hcell, cellData.Ref) + c2, _ := checkCellInArea(vcell, cellData.Ref) + c3, _ := checkCellInArea(cc[0], ref) + c4, _ := checkCellInArea(cc[1], ref) + // skip the overlapped mergecell + if c1 || c2 || c3 || c4 { + continue + } + xlsx.MergeCells.Cells[i] = cellData + i++ + } + xlsx.MergeCells.Cells = xlsx.MergeCells.Cells[:i] + return nil +} + // MergeCell define a merged cell data. // It consists of the following structure. // example: []string{"D4:E10", "cell value"} diff --git a/cellmerged_test.go b/cellmerged_test.go index d53acc2..0c5ac76 100644 --- a/cellmerged_test.go +++ b/cellmerged_test.go @@ -52,3 +52,39 @@ func TestGetMergeCells(t *testing.T) { _, err = f.GetMergeCells("SheetN") assert.EqualError(t, err, "sheet SheetN is not exist") } + +func TestUnmergeCell(t *testing.T) { + f, err := OpenFile(filepath.Join("test", "MergeCell.xlsx")) + if !assert.NoError(t, err) { + t.FailNow() + } + sheet1 := f.GetSheetName(1) + + xlsx, err := f.workSheetReader(sheet1) + assert.NoError(t, err) + + mergeCellNum := len(xlsx.MergeCells.Cells) + + assert.EqualError(t, f.UnmergeCell("Sheet1", "A", "A"), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + + // unmerge the mergecell that contains A1 + err = f.UnmergeCell(sheet1, "A1", "A1") + assert.NoError(t, err) + + if len(xlsx.MergeCells.Cells) != mergeCellNum-1 { + t.FailNow() + } + + // unmerge area A7:D3(A3:D7) + // this will unmerge all since this area overlaps with all others + err = f.UnmergeCell(sheet1, "D7", "A3") + assert.NoError(t, err) + + if len(xlsx.MergeCells.Cells) != 0 { + t.FailNow() + } + + // Test unmerged area on not exists worksheet. + err = f.UnmergeCell("SheetN", "A1", "A1") + assert.EqualError(t, err, "sheet SheetN is not exist") +} -- cgit v1.2.1 From da0d2ffbb6ebdfb7b1e5cf501a1986421311017b Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 14 Dec 2019 19:57:37 +0800 Subject: Fix #533, add support overlapped mergecells --- adjust.go | 3 - cell.go | 86 ++++++++--------------------- cell_test.go | 37 +++---------- cellmerged.go | 159 +++++++++++++++++++++++++++++++++++++++++++---------- cellmerged_test.go | 99 ++++++++++++++++++++++++++++----- excelize_test.go | 23 ++++++++ xmlWorksheet.go | 4 +- 7 files changed, 273 insertions(+), 138 deletions(-) diff --git a/adjust.go b/adjust.go index 186112d..bb583f1 100644 --- a/adjust.go +++ b/adjust.go @@ -206,9 +206,6 @@ func (f *File) areaRefToCoordinates(ref string) ([]int, error) { return coordinates, err } coordinates[2], coordinates[3], err = CellNameToCoordinates(lastCell) - if err != nil { - return coordinates, err - } return coordinates, err } diff --git a/cell.go b/cell.go index a25f2e4..ad4bcdb 100644 --- a/cell.go +++ b/cell.go @@ -412,63 +412,6 @@ func (f *File) SetCellHyperLink(sheet, axis, link, linkType string) error { return nil } -// MergeCell provides a function to merge cells by given coordinate area and -// sheet name. For example create a merged cell of D3:E9 on Sheet1: -// -// err := f.MergeCell("Sheet1", "D3", "E9") -// -// If you create a merged cell that overlaps with another existing merged cell, -// those merged cells that already exist will be removed. -func (f *File) MergeCell(sheet, hcell, vcell string) error { - coordinates, err := f.areaRefToCoordinates(hcell + ":" + vcell) - if err != nil { - return err - } - x1, y1, x2, y2 := coordinates[0], coordinates[1], coordinates[2], coordinates[3] - - if x1 == x2 && y1 == y2 { - return err - } - - // Correct the coordinate area, such correct C1:B3 to B1:C3. - if x2 < x1 { - x1, x2 = x2, x1 - } - - if y2 < y1 { - y1, y2 = y2, y1 - } - - hcell, _ = CoordinatesToCellName(x1, y1) - vcell, _ = CoordinatesToCellName(x2, y2) - - xlsx, err := f.workSheetReader(sheet) - if err != nil { - return err - } - if xlsx.MergeCells != nil { - ref := hcell + ":" + vcell - // Delete the merged cells of the overlapping area. - for _, cellData := range xlsx.MergeCells.Cells { - cc := strings.Split(cellData.Ref, ":") - if len(cc) != 2 { - return fmt.Errorf("invalid area %q", cellData.Ref) - } - c1, _ := checkCellInArea(hcell, cellData.Ref) - c2, _ := checkCellInArea(vcell, cellData.Ref) - c3, _ := checkCellInArea(cc[0], ref) - c4, _ := checkCellInArea(cc[1], ref) - if !(!c1 && !c2 && !c3 && !c4) { - return nil - } - } - xlsx.MergeCells.Cells = append(xlsx.MergeCells.Cells, &xlsxMergeCell{Ref: ref}) - } else { - xlsx.MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: hcell + ":" + vcell}}} - } - return err -} - // 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: @@ -601,7 +544,7 @@ func (f *File) mergeCellsParser(xlsx *xlsxWorksheet, axis string) (string, error axis = strings.ToUpper(axis) if xlsx.MergeCells != nil { for i := 0; i < len(xlsx.MergeCells.Cells); i++ { - ok, err := checkCellInArea(axis, xlsx.MergeCells.Cells[i].Ref) + ok, err := f.checkCellInArea(axis, xlsx.MergeCells.Cells[i].Ref) if err != nil { return axis, err } @@ -615,7 +558,7 @@ func (f *File) mergeCellsParser(xlsx *xlsxWorksheet, axis string) (string, error // checkCellInArea provides a function to determine if a given coordinate is // within an area. -func checkCellInArea(cell, area string) (bool, error) { +func (f *File) checkCellInArea(cell, area string) (bool, error) { col, row, err := CellNameToCoordinates(cell) if err != nil { return false, err @@ -625,11 +568,30 @@ func checkCellInArea(cell, area string) (bool, error) { if len(rng) != 2 { return false, err } + coordinates, err := f.areaRefToCoordinates(area) + if err != nil { + return false, err + } + + return cellInRef([]int{col, row}, coordinates), err +} - firstCol, firstRow, _ := CellNameToCoordinates(rng[0]) - lastCol, lastRow, _ := CellNameToCoordinates(rng[1]) +// cellInRef provides a function to determine if a given range is within an +// range. +func cellInRef(cell, ref []int) bool { + return cell[0] >= ref[0] && cell[0] <= ref[2] && cell[1] >= ref[1] && cell[1] <= ref[3] +} - return col >= firstCol && col <= lastCol && row >= firstRow && row <= lastRow, err +// isOverlap find if the given two rectangles overlap or not. +func isOverlap(rect1, rect2 []int) bool { + return cellInRef([]int{rect1[0], rect1[1]}, rect2) || + cellInRef([]int{rect1[2], rect1[1]}, rect2) || + cellInRef([]int{rect1[0], rect1[3]}, rect2) || + cellInRef([]int{rect1[2], rect1[3]}, rect2) || + cellInRef([]int{rect2[0], rect2[1]}, rect1) || + cellInRef([]int{rect2[2], rect2[1]}, rect1) || + cellInRef([]int{rect2[0], rect2[3]}, rect1) || + cellInRef([]int{rect2[2], rect2[3]}, rect1) } // getSharedForumula find a cell contains the same formula as another cell, diff --git a/cell_test.go b/cell_test.go index da0c1f1..b030622 100644 --- a/cell_test.go +++ b/cell_test.go @@ -10,6 +10,7 @@ import ( ) func TestCheckCellInArea(t *testing.T) { + f := NewFile() expectedTrueCellInAreaList := [][2]string{ {"c2", "A1:AAZ32"}, {"B9", "A1:B9"}, @@ -19,7 +20,7 @@ func TestCheckCellInArea(t *testing.T) { for _, expectedTrueCellInArea := range expectedTrueCellInAreaList { cell := expectedTrueCellInArea[0] area := expectedTrueCellInArea[1] - ok, err := checkCellInArea(cell, area) + ok, err := f.checkCellInArea(cell, area) assert.NoError(t, err) assert.Truef(t, ok, "Expected cell %v to be in area %v, got false\n", cell, area) @@ -34,13 +35,17 @@ func TestCheckCellInArea(t *testing.T) { for _, expectedFalseCellInArea := range expectedFalseCellInAreaList { cell := expectedFalseCellInArea[0] area := expectedFalseCellInArea[1] - ok, err := checkCellInArea(cell, area) + ok, err := f.checkCellInArea(cell, area) assert.NoError(t, err) assert.Falsef(t, ok, "Expected cell %v not to be inside of area %v, but got true\n", cell, area) } - ok, err := checkCellInArea("AA0", "Z0:AB1") + ok, err := f.checkCellInArea("A1", "A:B") + assert.EqualError(t, err, `cannot convert cell "A" to coordinates: invalid cell name "A"`) + assert.False(t, ok) + + ok, err = f.checkCellInArea("AA0", "Z0:AB1") assert.EqualError(t, err, `cannot convert cell "AA0" to coordinates: invalid cell name "AA0"`) assert.False(t, ok) } @@ -94,32 +99,6 @@ func TestGetCellFormula(t *testing.T) { f.GetCellFormula("Sheet", "A1") } -func TestMergeCell(t *testing.T) { - f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) - if !assert.NoError(t, err) { - t.FailNow() - } - assert.EqualError(t, f.MergeCell("Sheet1", "A", "B"), `cannot convert cell "A" to coordinates: invalid cell name "A"`) - f.MergeCell("Sheet1", "D9", "D9") - f.MergeCell("Sheet1", "D9", "E9") - f.MergeCell("Sheet1", "H14", "G13") - f.MergeCell("Sheet1", "C9", "D8") - f.MergeCell("Sheet1", "F11", "G13") - f.MergeCell("Sheet1", "H7", "B15") - f.MergeCell("Sheet1", "D11", "F13") - f.MergeCell("Sheet1", "G10", "K12") - f.SetCellValue("Sheet1", "G11", "set value in merged cell") - f.SetCellInt("Sheet1", "H11", 100) - f.SetCellValue("Sheet1", "I11", float64(0.5)) - f.SetCellHyperLink("Sheet1", "J11", "https://github.com/360EntSecGroup-Skylar/excelize", "External") - f.SetCellFormula("Sheet1", "G12", "SUM(Sheet1!B19,Sheet1!C19)") - f.GetCellValue("Sheet1", "H11") - f.GetCellValue("Sheet2", "A6") // Merged cell ref is single coordinate. - f.GetCellFormula("Sheet1", "G12") - - assert.NoError(t, f.SaveAs(filepath.Join("test", "TestMergeCell.xlsx"))) -} - func ExampleFile_SetCellFloat() { f := NewFile() var x = 3.14159265 diff --git a/cellmerged.go b/cellmerged.go index 4a5d11f..968a28a 100644 --- a/cellmerged.go +++ b/cellmerged.go @@ -9,28 +9,102 @@ package excelize -import "strings" +import ( + "fmt" + "strings" +) + +// MergeCell provides a function to merge cells by given coordinate area and +// sheet name. For example create a merged cell of D3:E9 on Sheet1: +// +// err := f.MergeCell("Sheet1", "D3", "E9") +// +// If you create a merged cell that overlaps with another existing merged cell, +// those merged cells that already exist will be removed. +// +// B1(x1,y1) D1(x2,y1) +// +--------------------------------+ +// | | +// | | +// A4(x3,y3) | C4(x4,y3) | +// +-----------------------------+ | +// | | | | +// | | | | +// | |B5(x1,y2) | D5(x2,y2)| +// | +--------------------------------+ +// | | +// | | +// |A8(x3,y4) C8(x4,y4)| +// +-----------------------------+ +// +func (f *File) MergeCell(sheet, hcell, vcell string) error { + rect1, err := f.areaRefToCoordinates(hcell + ":" + vcell) + if err != nil { + return err + } + // Correct the coordinate area, such correct C1:B3 to B1:C3. + if rect1[2] < rect1[0] { + rect1[0], rect1[2] = rect1[2], rect1[0] + } + + if rect1[3] < rect1[1] { + rect1[1], rect1[3] = rect1[3], rect1[1] + } + + hcell, _ = CoordinatesToCellName(rect1[0], rect1[1]) + vcell, _ = CoordinatesToCellName(rect1[2], rect1[3]) -// GetMergeCells provides a function to get all merged cells from a worksheet -// currently. -func (f *File) GetMergeCells(sheet string) ([]MergeCell, error) { - var mergeCells []MergeCell xlsx, err := f.workSheetReader(sheet) if err != nil { - return mergeCells, err + return err } + ref := hcell + ":" + vcell if xlsx.MergeCells != nil { - mergeCells = make([]MergeCell, 0, len(xlsx.MergeCells.Cells)) + for i := 0; i < len(xlsx.MergeCells.Cells); i++ { + cellData := xlsx.MergeCells.Cells[i] + if cellData == nil { + continue + } + cc := strings.Split(cellData.Ref, ":") + if len(cc) != 2 { + return fmt.Errorf("invalid area %q", cellData.Ref) + } - for i := range xlsx.MergeCells.Cells { - ref := xlsx.MergeCells.Cells[i].Ref - axis := strings.Split(ref, ":")[0] - val, _ := f.GetCellValue(sheet, axis) - mergeCells = append(mergeCells, []string{ref, val}) + rect2, err := f.areaRefToCoordinates(cellData.Ref) + if err != nil { + return err + } + + // Delete the merged cells of the overlapping area. + if isOverlap(rect1, rect2) { + xlsx.MergeCells.Cells = append(xlsx.MergeCells.Cells[:i], xlsx.MergeCells.Cells[i+1:]...) + i-- + + if rect1[0] > rect2[0] { + rect1[0], rect2[0] = rect2[0], rect1[0] + } + + if rect1[2] < rect2[2] { + rect1[2], rect2[2] = rect2[2], rect1[2] + } + + if rect1[1] > rect2[1] { + rect1[1], rect2[1] = rect2[1], rect1[1] + } + + if rect1[3] < rect2[3] { + rect1[3], rect2[3] = rect2[3], rect1[3] + } + hcell, _ = CoordinatesToCellName(rect1[0], rect1[1]) + vcell, _ = CoordinatesToCellName(rect1[2], rect1[3]) + ref = hcell + ":" + vcell + } } + xlsx.MergeCells.Cells = append(xlsx.MergeCells.Cells, &xlsxMergeCell{Ref: ref}) + } else { + xlsx.MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: ref}}} } - - return mergeCells, err + return err } // UnmergeCell provides a function to unmerge a given coordinate area. @@ -44,36 +118,41 @@ func (f *File) UnmergeCell(sheet string, hcell, vcell string) error { if err != nil { return err } - coordinates, err := f.areaRefToCoordinates(hcell + ":" + vcell) + rect1, err := f.areaRefToCoordinates(hcell + ":" + vcell) if err != nil { return err } - x1, y1, x2, y2 := coordinates[0], coordinates[1], coordinates[2], coordinates[3] - if x2 < x1 { - x1, x2 = x2, x1 + if rect1[2] < rect1[0] { + rect1[0], rect1[2] = rect1[2], rect1[0] } - if y2 < y1 { - y1, y2 = y2, y1 + if rect1[3] < rect1[1] { + rect1[1], rect1[3] = rect1[3], rect1[1] } - hcell, _ = CoordinatesToCellName(x1, y1) - vcell, _ = CoordinatesToCellName(x2, y2) + hcell, _ = CoordinatesToCellName(rect1[0], rect1[1]) + vcell, _ = CoordinatesToCellName(rect1[2], rect1[3]) // return nil since no MergeCells in the sheet if xlsx.MergeCells == nil { return nil } - ref := hcell + ":" + vcell i := 0 for _, cellData := range xlsx.MergeCells.Cells { + if cellData == nil { + continue + } cc := strings.Split(cellData.Ref, ":") - c1, _ := checkCellInArea(hcell, cellData.Ref) - c2, _ := checkCellInArea(vcell, cellData.Ref) - c3, _ := checkCellInArea(cc[0], ref) - c4, _ := checkCellInArea(cc[1], ref) - // skip the overlapped mergecell - if c1 || c2 || c3 || c4 { + if len(cc) != 2 { + return fmt.Errorf("invalid area %q", cellData.Ref) + } + + rect2, err := f.areaRefToCoordinates(cellData.Ref) + if err != nil { + return err + } + + if isOverlap(rect1, rect2) { continue } xlsx.MergeCells.Cells[i] = cellData @@ -83,6 +162,28 @@ func (f *File) UnmergeCell(sheet string, hcell, vcell string) error { return nil } +// GetMergeCells provides a function to get all merged cells from a worksheet +// currently. +func (f *File) GetMergeCells(sheet string) ([]MergeCell, error) { + var mergeCells []MergeCell + xlsx, err := f.workSheetReader(sheet) + if err != nil { + return mergeCells, err + } + if xlsx.MergeCells != nil { + mergeCells = make([]MergeCell, 0, len(xlsx.MergeCells.Cells)) + + for i := range xlsx.MergeCells.Cells { + ref := xlsx.MergeCells.Cells[i].Ref + axis := strings.Split(ref, ":")[0] + val, _ := f.GetCellValue(sheet, axis) + mergeCells = append(mergeCells, []string{ref, val}) + } + } + + return mergeCells, err +} + // MergeCell define a merged cell data. // It consists of the following structure. // example: []string{"D4:E10", "cell value"} diff --git a/cellmerged_test.go b/cellmerged_test.go index 0c5ac76..1da0eb3 100644 --- a/cellmerged_test.go +++ b/cellmerged_test.go @@ -7,6 +7,74 @@ import ( "github.com/stretchr/testify/assert" ) +func TestMergeCell(t *testing.T) { + f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) + if !assert.NoError(t, err) { + t.FailNow() + } + assert.EqualError(t, f.MergeCell("Sheet1", "A", "B"), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + assert.NoError(t, f.MergeCell("Sheet1", "D9", "D9")) + assert.NoError(t, f.MergeCell("Sheet1", "D9", "E9")) + assert.NoError(t, f.MergeCell("Sheet1", "H14", "G13")) + assert.NoError(t, f.MergeCell("Sheet1", "C9", "D8")) + assert.NoError(t, f.MergeCell("Sheet1", "F11", "G13")) + assert.NoError(t, f.MergeCell("Sheet1", "H7", "B15")) + assert.NoError(t, f.MergeCell("Sheet1", "D11", "F13")) + assert.NoError(t, f.MergeCell("Sheet1", "G10", "K12")) + f.SetCellValue("Sheet1", "G11", "set value in merged cell") + f.SetCellInt("Sheet1", "H11", 100) + f.SetCellValue("Sheet1", "I11", float64(0.5)) + f.SetCellHyperLink("Sheet1", "J11", "https://github.com/360EntSecGroup-Skylar/excelize", "External") + f.SetCellFormula("Sheet1", "G12", "SUM(Sheet1!B19,Sheet1!C19)") + f.GetCellValue("Sheet1", "H11") + f.GetCellValue("Sheet2", "A6") // Merged cell ref is single coordinate. + f.GetCellFormula("Sheet1", "G12") + + f.NewSheet("Sheet3") + assert.NoError(t, f.MergeCell("Sheet3", "D11", "F13")) + assert.NoError(t, f.MergeCell("Sheet3", "G10", "K12")) + + assert.NoError(t, f.MergeCell("Sheet3", "B1", "D5")) // B1:D5 + assert.NoError(t, f.MergeCell("Sheet3", "E1", "F5")) // E1:F5 + + assert.NoError(t, f.MergeCell("Sheet3", "H2", "I5")) + assert.NoError(t, f.MergeCell("Sheet3", "I4", "J6")) // H2:J6 + + assert.NoError(t, f.MergeCell("Sheet3", "M2", "N5")) + assert.NoError(t, f.MergeCell("Sheet3", "L4", "M6")) // L2:N6 + + assert.NoError(t, f.MergeCell("Sheet3", "P4", "Q7")) + assert.NoError(t, f.MergeCell("Sheet3", "O2", "P5")) // O2:Q7 + + assert.NoError(t, f.MergeCell("Sheet3", "A9", "B12")) + assert.NoError(t, f.MergeCell("Sheet3", "B7", "C9")) // A7:C12 + + assert.NoError(t, f.MergeCell("Sheet3", "E9", "F10")) + assert.NoError(t, f.MergeCell("Sheet3", "D8", "G12")) + + assert.NoError(t, f.MergeCell("Sheet3", "I8", "I12")) + assert.NoError(t, f.MergeCell("Sheet3", "I10", "K10")) + + assert.NoError(t, f.MergeCell("Sheet3", "M8", "Q13")) + assert.NoError(t, f.MergeCell("Sheet3", "N10", "O11")) + + // Test get merged cells on not exists worksheet. + assert.EqualError(t, f.MergeCell("SheetN", "N10", "O11"), "sheet SheetN is not exist") + + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestMergeCell.xlsx"))) + + f = NewFile() + assert.NoError(t, f.MergeCell("Sheet1", "A2", "B3")) + f.Sheet["xl/worksheets/sheet1.xml"].MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{nil, nil}} + assert.NoError(t, f.MergeCell("Sheet1", "A2", "B3")) + + f.Sheet["xl/worksheets/sheet1.xml"].MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A1"}}} + assert.EqualError(t, f.MergeCell("Sheet1", "A2", "B3"), `invalid area "A1"`) + + f.Sheet["xl/worksheets/sheet1.xml"].MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A:A"}}} + assert.EqualError(t, f.MergeCell("Sheet1", "A2", "B3"), `cannot convert cell "A" to coordinates: invalid cell name "A"`) +} + func TestGetMergeCells(t *testing.T) { wants := []struct { value string @@ -68,23 +136,28 @@ func TestUnmergeCell(t *testing.T) { assert.EqualError(t, f.UnmergeCell("Sheet1", "A", "A"), `cannot convert cell "A" to coordinates: invalid cell name "A"`) // unmerge the mergecell that contains A1 - err = f.UnmergeCell(sheet1, "A1", "A1") - assert.NoError(t, err) - + assert.NoError(t, f.UnmergeCell(sheet1, "A1", "A1")) if len(xlsx.MergeCells.Cells) != mergeCellNum-1 { t.FailNow() } - // unmerge area A7:D3(A3:D7) - // this will unmerge all since this area overlaps with all others - err = f.UnmergeCell(sheet1, "D7", "A3") - assert.NoError(t, err) - - if len(xlsx.MergeCells.Cells) != 0 { - t.FailNow() - } + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestUnmergeCell.xlsx"))) + f = NewFile() + assert.NoError(t, f.MergeCell("Sheet1", "A2", "B3")) // Test unmerged area on not exists worksheet. - err = f.UnmergeCell("SheetN", "A1", "A1") - assert.EqualError(t, err, "sheet SheetN is not exist") + assert.EqualError(t, f.UnmergeCell("SheetN", "A1", "A1"), "sheet SheetN is not exist") + + f.Sheet["xl/worksheets/sheet1.xml"].MergeCells = nil + assert.NoError(t, f.UnmergeCell("Sheet1", "H7", "B15")) + + f.Sheet["xl/worksheets/sheet1.xml"].MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{nil, nil}} + assert.NoError(t, f.UnmergeCell("Sheet1", "H15", "B7")) + + f.Sheet["xl/worksheets/sheet1.xml"].MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A1"}}} + assert.EqualError(t, f.UnmergeCell("Sheet1", "A2", "B3"), `invalid area "A1"`) + + f.Sheet["xl/worksheets/sheet1.xml"].MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A:A"}}} + assert.EqualError(t, f.UnmergeCell("Sheet1", "A2", "B3"), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + } diff --git a/excelize_test.go b/excelize_test.go index 38a35b0..95d63fd 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -290,6 +290,12 @@ func TestSetCellHyperLink(t *testing.T) { assert.NoError(t, file.SetCellHyperLink("Sheet1", cell, "https://github.com/360EntSecGroup-Skylar/excelize", "External")) } assert.EqualError(t, file.SetCellHyperLink("Sheet1", "A65531", "https://github.com/360EntSecGroup-Skylar/excelize", "External"), "over maximum limit hyperlinks in a worksheet") + + f = NewFile() + f.workSheetReader("Sheet1") + f.Sheet["xl/worksheets/sheet1.xml"].MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A:A"}}} + err = f.SetCellHyperLink("Sheet1", "A1", "https://github.com/360EntSecGroup-Skylar/excelize", "External") + assert.EqualError(t, err, `cannot convert cell "A" to coordinates: invalid cell name "A"`) } func TestGetCellHyperLink(t *testing.T) { @@ -310,6 +316,23 @@ func TestGetCellHyperLink(t *testing.T) { link, target, err = f.GetCellHyperLink("Sheet3", "H3") assert.EqualError(t, err, "sheet Sheet3 is not exist") t.Log(link, target) + + f = NewFile() + f.workSheetReader("Sheet1") + f.Sheet["xl/worksheets/sheet1.xml"].Hyperlinks = &xlsxHyperlinks{ + Hyperlink: []xlsxHyperlink{{Ref: "A1"}}, + } + link, target, err = f.GetCellHyperLink("Sheet1", "A1") + assert.NoError(t, err) + assert.Equal(t, link, true) + assert.Equal(t, target, "") + + f.Sheet["xl/worksheets/sheet1.xml"].MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A:A"}}} + link, target, err = f.GetCellHyperLink("Sheet1", "A1") + assert.EqualError(t, err, `cannot convert cell "A" to coordinates: invalid cell name "A"`) + assert.Equal(t, link, false) + assert.Equal(t, target, "") + } func TestSetCellFormula(t *testing.T) { diff --git a/xmlWorksheet.go b/xmlWorksheet.go index a071e4d..9a478e1 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -109,7 +109,7 @@ type xlsxPageSetUp struct { FirstPageNumber int `xml:"firstPageNumber,attr,omitempty"` FitToHeight int `xml:"fitToHeight,attr,omitempty"` FitToWidth int `xml:"fitToWidth,attr,omitempty"` - HorizontalDPI float32 `xml:"horizontalDpi,attr,omitempty"` + HorizontalDPI int `xml:"horizontalDpi,attr,omitempty"` RID string `xml:"http://schemas.openxmlformats.org/officeDocument/2006/relationships id,attr,omitempty"` Orientation string `xml:"orientation,attr,omitempty"` PageOrder string `xml:"pageOrder,attr,omitempty"` @@ -119,7 +119,7 @@ type xlsxPageSetUp struct { Scale int `xml:"scale,attr,omitempty"` UseFirstPageNumber bool `xml:"useFirstPageNumber,attr,omitempty"` UsePrinterDefaults bool `xml:"usePrinterDefaults,attr,omitempty"` - VerticalDPI float32 `xml:"verticalDpi,attr,omitempty"` + VerticalDPI int `xml:"verticalDpi,attr,omitempty"` } // xlsxPrintOptions directly maps the printOptions element in the namespace -- cgit v1.2.1 From a526e90404913f5d649d29a7aeee29f5ac9ff590 Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 16 Dec 2019 08:32:04 +0800 Subject: Fix #426, handle empty workbook view --- sheet.go | 35 +++++++++++++++++++---------------- stream.go | 3 +-- xmlWorkbook.go | 2 +- xmlWorksheet.go | 8 ++++---- 4 files changed, 25 insertions(+), 23 deletions(-) diff --git a/sheet.go b/sheet.go index 1ce85b4..e261935 100644 --- a/sheet.go +++ b/sheet.go @@ -149,11 +149,12 @@ func (f *File) setContentTypes(index int) { // setSheet provides a function to update sheet property by given index. func (f *File) setSheet(index int, name string) { - var xlsx xlsxWorksheet - xlsx.Dimension.Ref = "A1" - xlsx.SheetViews.SheetView = append(xlsx.SheetViews.SheetView, xlsxSheetView{ - WorkbookViewID: 0, - }) + xlsx := xlsxWorksheet{ + Dimension: &xlsxDimension{Ref: "A1"}, + SheetViews: xlsxSheetViews{ + SheetView: []xlsxSheetView{{WorkbookViewID: 0}}, + }, + } path := "xl/worksheets/sheet" + strconv.Itoa(index) + ".xml" f.sheetMap[trimSheetName(name)] = path f.Sheet[path] = &xlsx @@ -222,6 +223,9 @@ func (f *File) SetActiveSheet(index int) { wb := f.workbookReader() for activeTab, sheet := range wb.Sheets.Sheet { if sheet.SheetID == index { + if wb.BookViews == nil { + wb.BookViews = &xlsxBookViews{} + } if len(wb.BookViews.WorkBookView) > 0 { wb.BookViews.WorkBookView[0].ActiveTab = activeTab } else { @@ -253,16 +257,13 @@ func (f *File) SetActiveSheet(index int) { func (f *File) GetActiveSheetIndex() int { wb := f.workbookReader() if wb != nil { - view := wb.BookViews.WorkBookView - sheets := wb.Sheets.Sheet - var activeTab int - if len(view) > 0 { - activeTab = view[0].ActiveTab - if len(sheets) > activeTab && sheets[activeTab].SheetID != 0 { - return sheets[activeTab].SheetID + if wb.BookViews != nil && len(wb.BookViews.WorkBookView) > 0 { + activeTab := wb.BookViews.WorkBookView[0].ActiveTab + if len(wb.Sheets.Sheet) > activeTab && wb.Sheets.Sheet[activeTab].SheetID != 0 { + return wb.Sheets.Sheet[activeTab].SheetID } } - if len(wb.Sheets.Sheet) == 1 { + if len(wb.Sheets.Sheet) >= 1 { return wb.Sheets.Sheet[0].SheetID } } @@ -413,9 +414,11 @@ func (f *File) DeleteSheet(name string) { f.SheetCount-- } } - for idx, bookView := range wb.BookViews.WorkBookView { - if bookView.ActiveTab >= f.SheetCount { - wb.BookViews.WorkBookView[idx].ActiveTab-- + if wb.BookViews != nil { + for idx, bookView := range wb.BookViews.WorkBookView { + if bookView.ActiveTab >= f.SheetCount { + wb.BookViews.WorkBookView[idx].ActiveTab-- + } } } f.SetActiveSheet(len(f.GetSheetMap())) diff --git a/stream.go b/stream.go index 0d91ddd..5e74e8e 100644 --- a/stream.go +++ b/stream.go @@ -191,13 +191,12 @@ func StreamMarshalSheet(ws *xlsxWorksheet, replaceMap map[string][]byte) []byte var marshalResult []byte marshalResult = append(marshalResult, []byte(XMLHeader+``)...) diff --git a/xmlWorkbook.go b/xmlWorkbook.go index 765563b..e9ded6c 100644 --- a/xmlWorkbook.go +++ b/xmlWorkbook.go @@ -33,7 +33,7 @@ type xlsxWorkbook struct { FileVersion *xlsxFileVersion `xml:"fileVersion"` WorkbookPr *xlsxWorkbookPr `xml:"workbookPr"` WorkbookProtection *xlsxWorkbookProtection `xml:"workbookProtection"` - BookViews xlsxBookViews `xml:"bookViews"` + BookViews *xlsxBookViews `xml:"bookViews"` Sheets xlsxSheets `xml:"sheets"` ExternalReferences *xlsxExternalReferences `xml:"externalReferences"` DefinedNames *xlsxDefinedNames `xml:"definedNames"` diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 9a478e1..b785eac 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -17,10 +17,10 @@ import "encoding/xml" type xlsxWorksheet struct { XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main worksheet"` SheetPr *xlsxSheetPr `xml:"sheetPr"` - Dimension xlsxDimension `xml:"dimension"` - SheetViews xlsxSheetViews `xml:"sheetViews,omitempty"` + Dimension *xlsxDimension `xml:"dimension"` + SheetViews xlsxSheetViews `xml:"sheetViews"` SheetFormatPr *xlsxSheetFormatPr `xml:"sheetFormatPr"` - Cols *xlsxCols `xml:"cols,omitempty"` + Cols *xlsxCols `xml:"cols"` SheetData xlsxSheetData `xml:"sheetData"` SheetCalcPr *xlsxInnerXML `xml:"sheetCalcPr"` SheetProtection *xlsxSheetProtection `xml:"sheetProtection"` @@ -33,7 +33,7 @@ type xlsxWorksheet struct { MergeCells *xlsxMergeCells `xml:"mergeCells"` PhoneticPr *xlsxPhoneticPr `xml:"phoneticPr"` ConditionalFormatting []*xlsxConditionalFormatting `xml:"conditionalFormatting"` - DataValidations *xlsxDataValidations `xml:"dataValidations,omitempty"` + DataValidations *xlsxDataValidations `xml:"dataValidations"` Hyperlinks *xlsxHyperlinks `xml:"hyperlinks"` PrintOptions *xlsxPrintOptions `xml:"printOptions"` PageMargins *xlsxPageMargins `xml:"pageMargins"` -- cgit v1.2.1 From b1b3c0d15158abc71267da5893de020f047c3872 Mon Sep 17 00:00:00 2001 From: Alex Geer Date: Thu, 19 Dec 2019 19:30:48 +0300 Subject: =?UTF-8?q?Fix=20#539=20Fixed=20error=20opening=20excel=20file=20c?= =?UTF-8?q?reated=20in=20encoding=20d=E2=80=A6=20(#540)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fixed issue #539 Fixed error opening excel file created in encoding different from UTF-8, added logging of possible errors when decoding XML if the function does not provide exit with an error * Added test for CharsetReader * Fixed #discussion_r359397878 Discussion: https://github.com/360EntSecGroup-Skylar/excelize/pull/540#discussion_r359397878 * Fixed go fmt * go mod tidy and removed unused imports * The code has been refactored --- .gitignore | 3 +- calcchain.go | 18 +++++++++--- chart.go | 16 +++++++++-- comment.go | 23 +++++++++++---- docProps.go | 66 ++++++++++++++++++++++++++---------------- docProps_test.go | 4 +-- excelize.go | 81 ++++++++++++++++++++++++++++++++++++---------------- file.go | 8 ++---- go.mod | 2 ++ go.sum | 8 ++++++ picture.go | 62 ++++++++++++++++++++++++++-------------- rows.go | 12 ++++++-- sheet.go | 78 ++++++++++++++++++++++++++++++++++---------------- sparkline.go | 87 ++++++++++++++++++++++++++++++++++++++------------------ styles.go | 26 +++++++++++++---- 15 files changed, 342 insertions(+), 152 deletions(-) diff --git a/.gitignore b/.gitignore index bafda04..a3fcff2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ ~$*.xlsx test/Test*.xlsx *.out -*.test \ No newline at end of file +*.test +.idea diff --git a/calcchain.go b/calcchain.go index b4cadef..413f470 100644 --- a/calcchain.go +++ b/calcchain.go @@ -9,16 +9,26 @@ package excelize -import "encoding/xml" +import ( + "bytes" + "encoding/xml" + "io" + "log" +) // calcChainReader provides a function to get the pointer to the structure // after deserialization of xl/calcChain.xml. func (f *File) calcChainReader() *xlsxCalcChain { + var err error + if f.CalcChain == nil { - var c xlsxCalcChain - _ = xml.Unmarshal(namespaceStrictToTransitional(f.readXML("xl/calcChain.xml")), &c) - f.CalcChain = &c + f.CalcChain = new(xlsxCalcChain) + if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML("xl/calcChain.xml")))). + Decode(f.CalcChain); err != nil && err != io.EOF { + log.Printf("xml decode error: %s", err) + } } + return f.CalcChain } diff --git a/chart.go b/chart.go index 7d40405..aaa7cd6 100644 --- a/chart.go +++ b/chart.go @@ -10,9 +10,12 @@ package excelize import ( + "bytes" "encoding/json" "encoding/xml" "errors" + "io" + "log" "strconv" "strings" ) @@ -1735,14 +1738,21 @@ func (f *File) drawPlotAreaTxPr() *cTxPr { // deserialization, two different structures: decodeWsDr and encodeWsDr are // defined. func (f *File) drawingParser(path string) (*xlsxWsDr, int) { + var ( + err error + ok bool + ) + if f.Drawings[path] == nil { content := xlsxWsDr{} content.A = NameSpaceDrawingML content.Xdr = NameSpaceDrawingMLSpreadSheet - _, ok := f.XLSX[path] - if ok { // Append Model + if _, ok = f.XLSX[path]; ok { // Append Model decodeWsDr := decodeWsDr{} - _ = xml.Unmarshal(namespaceStrictToTransitional(f.readXML(path)), &decodeWsDr) + if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(path)))). + Decode(&decodeWsDr); err != nil && err != io.EOF { + log.Printf("xml decode error: %s", err) + } content.R = decodeWsDr.R for _, v := range decodeWsDr.OneCellAnchor { content.OneCellAnchor = append(content.OneCellAnchor, &xdrCellAnchor{ diff --git a/comment.go b/comment.go index 7f3b10d..99630c9 100644 --- a/comment.go +++ b/comment.go @@ -10,9 +10,12 @@ package excelize import ( + "bytes" "encoding/json" "encoding/xml" "fmt" + "io" + "log" "strconv" "strings" ) @@ -303,12 +306,16 @@ func (f *File) countComments() int { // decodeVMLDrawingReader provides a function to get the pointer to the // structure after deserialization of xl/drawings/vmlDrawing%d.xml. func (f *File) decodeVMLDrawingReader(path string) *decodeVmlDrawing { + var err error + if f.DecodeVMLDrawing[path] == nil { c, ok := f.XLSX[path] if ok { - d := decodeVmlDrawing{} - _ = xml.Unmarshal(namespaceStrictToTransitional(c), &d) - f.DecodeVMLDrawing[path] = &d + f.DecodeVMLDrawing[path] = new(decodeVmlDrawing) + if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(c))). + Decode(f.DecodeVMLDrawing[path]); err != nil && err != io.EOF { + log.Printf("xml decode error: %s", err) + } } } return f.DecodeVMLDrawing[path] @@ -328,12 +335,16 @@ func (f *File) vmlDrawingWriter() { // commentsReader provides a function to get the pointer to the structure // after deserialization of xl/comments%d.xml. func (f *File) commentsReader(path string) *xlsxComments { + var err error + if f.Comments[path] == nil { content, ok := f.XLSX[path] if ok { - c := xlsxComments{} - _ = xml.Unmarshal(namespaceStrictToTransitional(content), &c) - f.Comments[path] = &c + f.Comments[path] = new(xlsxComments) + if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(content))). + Decode(f.Comments[path]); err != nil && err != io.EOF { + log.Printf("xml decode error: %s", err) + } } } return f.Comments[path] diff --git a/docProps.go b/docProps.go index 166512f..884eb63 100644 --- a/docProps.go +++ b/docProps.go @@ -10,7 +10,10 @@ package excelize import ( + "bytes" "encoding/xml" + "fmt" + "io" "reflect" ) @@ -65,13 +68,23 @@ import ( // Version: "1.0.0", // }) // -func (f *File) SetDocProps(docProperties *DocProperties) error { - core := decodeCoreProperties{} - err := xml.Unmarshal(namespaceStrictToTransitional(f.readXML("docProps/core.xml")), &core) - if err != nil { - return err +func (f *File) SetDocProps(docProperties *DocProperties) (err error) { + var ( + core *decodeCoreProperties + newProps *xlsxCoreProperties + fields []string + output []byte + immutable, mutable reflect.Value + field, val string + ) + + core = new(decodeCoreProperties) + if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML("docProps/core.xml")))). + Decode(core); err != nil && err != io.EOF { + err = fmt.Errorf("xml decode error: %s", err) + return } - newProps := xlsxCoreProperties{ + newProps, err = &xlsxCoreProperties{ Dc: NameSpaceDublinCore, Dcterms: NameSpaceDublinCoreTerms, Dcmitype: NameSpaceDublinCoreMetadataIntiative, @@ -88,18 +101,16 @@ func (f *File) SetDocProps(docProperties *DocProperties) error { ContentStatus: core.ContentStatus, Category: core.Category, Version: core.Version, + }, nil + newProps.Created.Text, newProps.Created.Type, newProps.Modified.Text, newProps.Modified.Type = + core.Created.Text, core.Created.Type, core.Modified.Text, core.Modified.Type + fields = []string{ + "Category", "ContentStatus", "Creator", "Description", "Identifier", "Keywords", + "LastModifiedBy", "Revision", "Subject", "Title", "Language", "Version", } - newProps.Created.Text = core.Created.Text - newProps.Created.Type = core.Created.Type - newProps.Modified.Text = core.Modified.Text - newProps.Modified.Type = core.Modified.Type - - fields := []string{"Category", "ContentStatus", "Creator", "Description", "Identifier", "Keywords", "LastModifiedBy", "Revision", "Subject", "Title", "Language", "Version"} - immutable := reflect.ValueOf(*docProperties) - mutable := reflect.ValueOf(&newProps).Elem() - for _, field := range fields { - val := immutable.FieldByName(field).String() - if val != "" { + immutable, mutable = reflect.ValueOf(*docProperties), reflect.ValueOf(newProps).Elem() + for _, field = range fields { + if val = immutable.FieldByName(field).String(); val != "" { mutable.FieldByName(field).SetString(val) } } @@ -109,19 +120,22 @@ func (f *File) SetDocProps(docProperties *DocProperties) error { if docProperties.Modified != "" { newProps.Modified.Text = docProperties.Modified } - output, err := xml.Marshal(&newProps) + output, err = xml.Marshal(newProps) f.saveFileList("docProps/core.xml", output) - return err + + return } // GetDocProps provides a function to get document core properties. -func (f *File) GetDocProps() (*DocProperties, error) { - core := decodeCoreProperties{} - err := xml.Unmarshal(namespaceStrictToTransitional(f.readXML("docProps/core.xml")), &core) - if err != nil { - return nil, err +func (f *File) GetDocProps() (ret *DocProperties, err error) { + var core = new(decodeCoreProperties) + + if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML("docProps/core.xml")))). + Decode(core); err != nil && err != io.EOF { + err = fmt.Errorf("xml decode error: %s", err) + return } - return &DocProperties{ + ret, err = &DocProperties{ Category: core.Category, ContentStatus: core.ContentStatus, Created: core.Created.Text, @@ -137,4 +151,6 @@ func (f *File) GetDocProps() (*DocProperties, error) { Language: core.Language, Version: core.Version, }, nil + + return } diff --git a/docProps_test.go b/docProps_test.go index 671d998..df3122b 100644 --- a/docProps_test.go +++ b/docProps_test.go @@ -39,7 +39,7 @@ func TestSetDocProps(t *testing.T) { })) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetDocProps.xlsx"))) f.XLSX["docProps/core.xml"] = nil - assert.EqualError(t, f.SetDocProps(&DocProperties{}), "EOF") + assert.NoError(t, f.SetDocProps(&DocProperties{})) } func TestGetDocProps(t *testing.T) { @@ -52,5 +52,5 @@ func TestGetDocProps(t *testing.T) { assert.Equal(t, props.Creator, "Microsoft Office User") f.XLSX["docProps/core.xml"] = nil _, err = f.GetDocProps() - assert.EqualError(t, err, "EOF") + assert.NoError(t, err) } diff --git a/excelize.go b/excelize.go index 4d46b94..fe227b9 100644 --- a/excelize.go +++ b/excelize.go @@ -22,6 +22,8 @@ import ( "path" "strconv" "strings" + + "golang.org/x/net/html/charset" ) // File define a populated XLSX file struct. @@ -43,8 +45,11 @@ type File struct { WorkBook *xlsxWorkbook Relationships map[string]*xlsxRelationships XLSX map[string][]byte + CharsetReader charsetTranscoderFn } +type charsetTranscoderFn func(charset string, input io.Reader) (rdr io.Reader, err error) + // OpenFile take the name of an XLSX file and returns a populated XLSX file // struct for it. func OpenFile(filename string) (*File, error) { @@ -61,6 +66,21 @@ func OpenFile(filename string) (*File, error) { return f, nil } +// object builder +func newFile() *File { + return &File{ + checked: make(map[string]bool), + sheetMap: make(map[string]string), + Comments: make(map[string]*xlsxComments), + Drawings: make(map[string]*xlsxWsDr), + Sheet: make(map[string]*xlsxWorksheet), + DecodeVMLDrawing: make(map[string]*decodeVmlDrawing), + VMLDrawing: make(map[string]*vmlDrawing), + Relationships: make(map[string]*xlsxRelationships), + CharsetReader: charset.NewReaderLabel, + } +} + // OpenReader take an io.Reader and return a populated XLSX file. func OpenReader(r io.Reader) (*File, error) { b, err := ioutil.ReadAll(r) @@ -88,17 +108,8 @@ func OpenReader(r io.Reader) (*File, error) { if err != nil { return nil, err } - f := &File{ - checked: make(map[string]bool), - Comments: make(map[string]*xlsxComments), - Drawings: make(map[string]*xlsxWsDr), - Sheet: make(map[string]*xlsxWorksheet), - SheetCount: sheetCount, - DecodeVMLDrawing: make(map[string]*decodeVmlDrawing), - VMLDrawing: make(map[string]*vmlDrawing), - Relationships: make(map[string]*xlsxRelationships), - XLSX: file, - } + f := newFile() + f.SheetCount, f.XLSX = sheetCount, file f.CalcChain = f.calcChainReader() f.sheetMap = f.getSheetMap() f.Styles = f.stylesReader() @@ -106,6 +117,16 @@ func OpenReader(r io.Reader) (*File, error) { return f, nil } +// CharsetTranscoder Set user defined codepage transcoder function for open XLSX from non UTF-8 encoding +func (f *File) CharsetTranscoder(fn charsetTranscoderFn) *File { f.CharsetReader = fn; return f } + +// Creates new XML decoder with charset reader +func (f *File) xmlNewDecoder(rdr io.Reader) (ret *xml.Decoder) { + ret = xml.NewDecoder(rdr) + ret.CharsetReader = f.CharsetReader + return +} + // setDefaultTimeStyle provides a function to set default numbers format for // time.Time type cell value by given worksheet name, cell coordinates and // number format code. @@ -123,26 +144,38 @@ func (f *File) setDefaultTimeStyle(sheet, axis string, format int) error { // workSheetReader provides a function to get the pointer to the structure // after deserialization by given worksheet name. -func (f *File) workSheetReader(sheet string) (*xlsxWorksheet, error) { - name, ok := f.sheetMap[trimSheetName(sheet)] - if !ok { - return nil, fmt.Errorf("sheet %s is not exist", sheet) +func (f *File) workSheetReader(sheet string) (xlsx *xlsxWorksheet, err error) { + var ( + name string + ok bool + ) + + if name, ok = f.sheetMap[trimSheetName(sheet)]; !ok { + err = fmt.Errorf("sheet %s is not exist", sheet) + return } - if f.Sheet[name] == nil { - var xlsx xlsxWorksheet - _ = xml.Unmarshal(namespaceStrictToTransitional(f.readXML(name)), &xlsx) + if xlsx = f.Sheet[name]; f.Sheet[name] == nil { + xlsx = new(xlsxWorksheet) + if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(name)))). + Decode(xlsx); err != nil && err != io.EOF { + err = fmt.Errorf("xml decode error: %s", err) + return + } + err = nil if f.checked == nil { f.checked = make(map[string]bool) } - ok := f.checked[name] - if !ok { - checkSheet(&xlsx) - checkRow(&xlsx) + if ok = f.checked[name]; !ok { + checkSheet(xlsx) + if err = checkRow(xlsx); err != nil { + return + } f.checked[name] = true } - f.Sheet[name] = &xlsx + f.Sheet[name] = xlsx } - return f.Sheet[name], nil + + return } // checkSheet provides a function to fill each row element and make that is diff --git a/file.go b/file.go index 2e0d27b..d8f10fa 100644 --- a/file.go +++ b/file.go @@ -33,12 +33,8 @@ func NewFile() *File { file["xl/styles.xml"] = []byte(XMLHeader + templateStyles) file["xl/workbook.xml"] = []byte(XMLHeader + templateWorkbook) file["[Content_Types].xml"] = []byte(XMLHeader + templateContentTypes) - f := &File{ - sheetMap: make(map[string]string), - Sheet: make(map[string]*xlsxWorksheet), - SheetCount: 1, - XLSX: file, - } + f := newFile() + f.SheetCount, f.XLSX = 1, file f.CalcChain = f.calcChainReader() f.Comments = make(map[string]*xlsxComments) f.ContentTypes = f.contentTypesReader() diff --git a/go.mod b/go.mod index 892f306..420c64e 100644 --- a/go.mod +++ b/go.mod @@ -7,4 +7,6 @@ require ( github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 github.com/stretchr/testify v1.3.0 golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a + golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 + golang.org/x/text v0.3.2 // indirect ) diff --git a/go.sum b/go.sum index 2d29d33..54492ac 100644 --- a/go.sum +++ b/go.sum @@ -9,6 +9,14 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a h1:gHevYm0pO4QUbwy8Dmdr01R5r1BuKtfYqRqF0h/Cbh0= golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/picture.go b/picture.go index ff40863..09c1955 100644 --- a/picture.go +++ b/picture.go @@ -14,7 +14,9 @@ import ( "encoding/json" "encoding/xml" "errors" + "fmt" "image" + "io" "io/ioutil" "os" "path" @@ -471,39 +473,55 @@ func (f *File) GetPicture(sheet, cell string) (string, []byte, error) { // getPicture provides a function to get picture base name and raw content // embed in XLSX by given coordinates and drawing relationships. -func (f *File) getPicture(row, col int, drawingXML, drawingRelationships string) (string, []byte, error) { - wsDr, _ := f.drawingParser(drawingXML) - for _, anchor := range wsDr.TwoCellAnchor { +func (f *File) getPicture(row, col int, drawingXML, drawingRelationships string) (ret string, buf []byte, err error) { + var ( + wsDr *xlsxWsDr + ok bool + anchor *xdrCellAnchor + deWsDr *decodeWsDr + xxRelationship *xlsxRelationship + deTwoCellAnchor *decodeTwoCellAnchor + ) + + 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 { - xlsxRelationship := f.getDrawingRelationships(drawingRelationships, + xxRelationship = f.getDrawingRelationships(drawingRelationships, anchor.Pic.BlipFill.Blip.Embed) - _, ok := supportImageTypes[filepath.Ext(xlsxRelationship.Target)] - if ok { - return filepath.Base(xlsxRelationship.Target), - []byte(f.XLSX[strings.Replace(xlsxRelationship.Target, - "..", "xl", -1)]), nil + if _, ok = supportImageTypes[filepath.Ext(xxRelationship.Target)]; ok { + ret, buf = filepath.Base(xxRelationship.Target), []byte(f.XLSX[strings.Replace(xxRelationship.Target, "..", "xl", -1)]) + return } } } } - - decodeWsDr := decodeWsDr{} - _ = xml.Unmarshal(namespaceStrictToTransitional(f.readXML(drawingXML)), &decodeWsDr) - for _, anchor := range decodeWsDr.TwoCellAnchor { - decodeTwoCellAnchor := decodeTwoCellAnchor{} - _ = xml.Unmarshal([]byte(""+anchor.Content+""), &decodeTwoCellAnchor) - if decodeTwoCellAnchor.From != nil && decodeTwoCellAnchor.Pic != nil { - if decodeTwoCellAnchor.From.Col == col && decodeTwoCellAnchor.From.Row == row { - xlsxRelationship := f.getDrawingRelationships(drawingRelationships, decodeTwoCellAnchor.Pic.BlipFill.Blip.Embed) - _, ok := supportImageTypes[filepath.Ext(xlsxRelationship.Target)] - if ok { - return filepath.Base(xlsxRelationship.Target), []byte(f.XLSX[strings.Replace(xlsxRelationship.Target, "..", "xl", -1)]), nil + deWsDr = new(decodeWsDr) + if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(drawingXML)))). + Decode(deWsDr); err != nil && err != io.EOF { + err = fmt.Errorf("xml decode error: %s", err) + return + } + err = nil + for _, anchor := range deWsDr.TwoCellAnchor { + deTwoCellAnchor = new(decodeTwoCellAnchor) + if err = f.xmlNewDecoder(bytes.NewReader([]byte("" + anchor.Content + ""))). + Decode(deTwoCellAnchor); err != nil && err != io.EOF { + err = fmt.Errorf("xml decode error: %s", err) + return + } + if err = nil; deTwoCellAnchor.From != nil && deTwoCellAnchor.Pic != nil { + if deTwoCellAnchor.From.Col == col && deTwoCellAnchor.From.Row == row { + xxRelationship = f.getDrawingRelationships(drawingRelationships, deTwoCellAnchor.Pic.BlipFill.Blip.Embed) + if _, ok = supportImageTypes[filepath.Ext(xxRelationship.Target)]; ok { + ret, buf = filepath.Base(xxRelationship.Target), []byte(f.XLSX[strings.Replace(xxRelationship.Target, "..", "xl", -1)]) + return } } } } - return "", nil, nil + + return } // getDrawingRelationships provides a function to get drawing relationships diff --git a/rows.go b/rows.go index 3796441..e12e349 100644 --- a/rows.go +++ b/rows.go @@ -10,9 +10,11 @@ package excelize import ( - "encoding/xml" + "bytes" "errors" "fmt" + "io" + "log" "math" "strconv" ) @@ -187,15 +189,21 @@ func (f *File) GetRowHeight(sheet string, row int) (float64, error) { // sharedStringsReader provides a function to get the pointer to the structure // after deserialization of xl/sharedStrings.xml. func (f *File) sharedStringsReader() *xlsxSST { + var err error + if f.SharedStrings == nil { var sharedStrings xlsxSST ss := f.readXML("xl/sharedStrings.xml") if len(ss) == 0 { ss = f.readXML("xl/SharedStrings.xml") } - _ = xml.Unmarshal(namespaceStrictToTransitional(ss), &sharedStrings) + if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(ss))). + Decode(&sharedStrings); err != nil && err != io.EOF { + log.Printf("xml decode error: %s", err) + } f.SharedStrings = &sharedStrings } + return f.SharedStrings } diff --git a/sheet.go b/sheet.go index 951baf9..42fd6b3 100644 --- a/sheet.go +++ b/sheet.go @@ -15,7 +15,9 @@ import ( "encoding/xml" "errors" "fmt" + "io" "io/ioutil" + "log" "os" "path" "reflect" @@ -61,11 +63,16 @@ func (f *File) NewSheet(name string) int { // contentTypesReader provides a function to get the pointer to the // [Content_Types].xml structure after deserialization. func (f *File) contentTypesReader() *xlsxTypes { + var err error + if f.ContentTypes == nil { - var content xlsxTypes - _ = xml.Unmarshal(namespaceStrictToTransitional(f.readXML("[Content_Types].xml")), &content) - f.ContentTypes = &content + f.ContentTypes = new(xlsxTypes) + if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML("[Content_Types].xml")))). + Decode(f.ContentTypes); err != nil && err != io.EOF { + log.Printf("xml decode error: %s", err) + } } + return f.ContentTypes } @@ -81,11 +88,16 @@ func (f *File) contentTypesWriter() { // workbookReader provides a function to get the pointer to the xl/workbook.xml // structure after deserialization. func (f *File) workbookReader() *xlsxWorkbook { + var err error + if f.WorkBook == nil { - var content xlsxWorkbook - _ = xml.Unmarshal(namespaceStrictToTransitional(f.readXML("xl/workbook.xml")), &content) - f.WorkBook = &content + f.WorkBook = new(xlsxWorkbook) + if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML("xl/workbook.xml")))). + Decode(f.WorkBook); err != nil && err != io.EOF { + log.Printf("xml decode error: %s", err) + } } + return f.WorkBook } @@ -679,42 +691,51 @@ func (f *File) GetSheetVisible(name string) bool { // // result, err := f.SearchSheet("Sheet1", "[0-9]", true) // -func (f *File) SearchSheet(sheet, value string, reg ...bool) ([]string, error) { +func (f *File) SearchSheet(sheet, value string, reg ...bool) (result []string, err error) { var ( - regSearch bool - result []string + xlsx *xlsxWorksheet + regSearch, r, ok bool + name string + output []byte ) - for _, r := range reg { + + for _, r = range reg { regSearch = r } - xlsx, err := f.workSheetReader(sheet) - if err != nil { - return result, err + if xlsx, err = f.workSheetReader(sheet); err != nil { + return } - name, ok := f.sheetMap[trimSheetName(sheet)] - if !ok { - return result, nil + if name, ok = f.sheetMap[trimSheetName(sheet)]; !ok { + return } if xlsx != nil { - output, _ := xml.Marshal(f.Sheet[name]) + if output, err = xml.Marshal(f.Sheet[name]); err != nil { + return + } f.saveFileList(name, replaceWorkSheetsRelationshipsNameSpaceBytes(output)) } + return f.searchSheet(name, value, regSearch) } // searchSheet provides a function to get coordinates by given worksheet name, // cell value, and regular expression. -func (f *File) searchSheet(name, value string, regSearch bool) ([]string, error) { +func (f *File) searchSheet(name, value string, regSearch bool) (result []string, err error) { var ( + d *xlsxSST + decoder *xml.Decoder inElement string - result []string r xlsxRow + token xml.Token ) - d := f.sharedStringsReader() - decoder := xml.NewDecoder(bytes.NewReader(f.readXML(name))) + + d = f.sharedStringsReader() + decoder = f.xmlNewDecoder(bytes.NewReader(f.readXML(name))) for { - token, _ := decoder.Token() - if token == nil { + if token, err = decoder.Token(); err != nil || token == nil { + if err == io.EOF { + err = nil + } break } switch startElement := token.(type) { @@ -750,7 +771,8 @@ func (f *File) searchSheet(name, value string, regSearch bool) ([]string, error) default: } } - return result, nil + + return } // SetHeaderFooter provides a function to set headers and footers by given @@ -1360,14 +1382,20 @@ func (f *File) UngroupSheets() error { // relsReader provides a function to get the pointer to the structure // after deserialization of xl/worksheets/_rels/sheet%d.xml.rels. func (f *File) relsReader(path string) *xlsxRelationships { + var err error + if f.Relationships[path] == nil { _, ok := f.XLSX[path] if ok { c := xlsxRelationships{} - _ = xml.Unmarshal(namespaceStrictToTransitional(f.readXML(path)), &c) + if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(path)))). + Decode(&c); err != nil && err != io.EOF { + log.Printf("xml decode error: %s", err) + } f.Relationships[path] = &c } } + return f.Relationships[path] } diff --git a/sparkline.go b/sparkline.go index b09dbf4..9ad5087 100644 --- a/sparkline.go +++ b/sparkline.go @@ -10,8 +10,10 @@ package excelize import ( + "bytes" "encoding/xml" "errors" + "io" "strings" ) @@ -386,23 +388,40 @@ func (f *File) addSparklineGroupByStyle(ID int) *xlsxX14SparklineGroup { // ColorAxis | An RGB Color is specified as RRGGBB // Axis | Show sparkline axis // -func (f *File) AddSparkline(sheet string, opt *SparklineOption) error { +func (f *File) AddSparkline(sheet string, opt *SparklineOption) (err error) { + var ( + ws *xlsxWorksheet + sparkType string + sparkTypes map[string]string + specifiedSparkTypes string + ok bool + group *xlsxX14SparklineGroup + groups *xlsxX14SparklineGroups + decodeExtLst *decodeWorksheetExt + idx int + ext *xlsxWorksheetExt + decodeSparklineGroups *decodeX14SparklineGroups + sparklineGroupBytes []byte + sparklineGroupsBytes []byte + extLst string + extLstBytes, extBytes []byte + ) + // parameter validation - ws, err := f.parseFormatAddSparklineSet(sheet, opt) - if err != nil { - return err + if ws, err = f.parseFormatAddSparklineSet(sheet, opt); err != nil { + return } // Handle the sparkline type - sparkType := "line" - sparkTypes := map[string]string{"line": "line", "column": "column", "win_loss": "stacked"} + sparkType = "line" + sparkTypes = map[string]string{"line": "line", "column": "column", "win_loss": "stacked"} if opt.Type != "" { - specifiedSparkTypes, ok := sparkTypes[opt.Type] - if !ok { - return errors.New("parameter 'Type' must be 'line', 'column' or 'win_loss'") + if specifiedSparkTypes, ok = sparkTypes[opt.Type]; !ok { + err = errors.New("parameter 'Type' must be 'line', 'column' or 'win_loss'") + return } sparkType = specifiedSparkTypes } - group := f.addSparklineGroupByStyle(opt.Style) + group = f.addSparklineGroupByStyle(opt.Style) group.Type = sparkType group.ColorAxis = &xlsxColor{RGB: "FF000000"} group.DisplayEmptyCellsAs = "gap" @@ -423,43 +442,57 @@ func (f *File) AddSparkline(sheet string, opt *SparklineOption) error { } f.addSparkline(opt, group) if ws.ExtLst.Ext != "" { // append mode ext - decodeExtLst := decodeWorksheetExt{} - err = xml.Unmarshal([]byte(""+ws.ExtLst.Ext+""), &decodeExtLst) - if err != nil { - return err + decodeExtLst = new(decodeWorksheetExt) + if err = f.xmlNewDecoder(bytes.NewReader([]byte("" + ws.ExtLst.Ext + ""))). + Decode(decodeExtLst); err != nil && err != io.EOF { + return } - for idx, ext := range decodeExtLst.Ext { + for idx, ext = range decodeExtLst.Ext { if ext.URI == ExtURISparklineGroups { - decodeSparklineGroups := decodeX14SparklineGroups{} - _ = xml.Unmarshal([]byte(ext.Content), &decodeSparklineGroups) - sparklineGroupBytes, _ := xml.Marshal(group) - groups := xlsxX14SparklineGroups{ + decodeSparklineGroups = new(decodeX14SparklineGroups) + if err = f.xmlNewDecoder(bytes.NewReader([]byte(ext.Content))). + Decode(decodeSparklineGroups); err != nil && err != io.EOF { + return + } + if sparklineGroupBytes, err = xml.Marshal(group); err != nil { + return + } + groups = &xlsxX14SparklineGroups{ XMLNSXM: NameSpaceSpreadSheetExcel2006Main, Content: decodeSparklineGroups.Content + string(sparklineGroupBytes), } - sparklineGroupsBytes, _ := xml.Marshal(groups) + if sparklineGroupsBytes, err = xml.Marshal(groups); err != nil { + return + } decodeExtLst.Ext[idx].Content = string(sparklineGroupsBytes) } } - extLstBytes, _ := xml.Marshal(decodeExtLst) - extLst := string(extLstBytes) + if extLstBytes, err = xml.Marshal(decodeExtLst); err != nil { + return + } + extLst = string(extLstBytes) ws.ExtLst = &xlsxExtLst{ Ext: strings.TrimSuffix(strings.TrimPrefix(extLst, ""), ""), } } else { - groups := xlsxX14SparklineGroups{ + groups = &xlsxX14SparklineGroups{ XMLNSXM: NameSpaceSpreadSheetExcel2006Main, SparklineGroups: []*xlsxX14SparklineGroup{group}, } - sparklineGroupsBytes, _ := xml.Marshal(groups) - extLst := xlsxWorksheetExt{ + if sparklineGroupsBytes, err = xml.Marshal(groups); err != nil { + return + } + ext = &xlsxWorksheetExt{ URI: ExtURISparklineGroups, Content: string(sparklineGroupsBytes), } - extBytes, _ := xml.Marshal(extLst) + if extBytes, err = xml.Marshal(ext); err != nil { + return + } ws.ExtLst.Ext = string(extBytes) } - return nil + + return } // parseFormatAddSparklineSet provides a function to validate sparkline diff --git a/styles.go b/styles.go index 3244be2..fa0507e 100644 --- a/styles.go +++ b/styles.go @@ -10,9 +10,12 @@ package excelize import ( + "bytes" "encoding/json" "encoding/xml" "fmt" + "io" + "log" "math" "strconv" "strings" @@ -997,11 +1000,16 @@ func is12HourTime(format string) bool { // stylesReader provides a function to get the pointer to the structure after // deserialization of xl/styles.xml. func (f *File) stylesReader() *xlsxStyleSheet { + var err error + if f.Styles == nil { - var styleSheet xlsxStyleSheet - _ = xml.Unmarshal(namespaceStrictToTransitional(f.readXML("xl/styles.xml")), &styleSheet) - f.Styles = &styleSheet + f.Styles = new(xlsxStyleSheet) + if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML("xl/styles.xml")))). + Decode(f.Styles); err != nil && err != io.EOF { + log.Printf("xml decode error: %s", err) + } } + return f.Styles } @@ -2803,8 +2811,16 @@ func getPaletteColor(color string) string { // themeReader provides a function to get the pointer to the xl/theme/theme1.xml // structure after deserialization. func (f *File) themeReader() *xlsxTheme { - var theme xlsxTheme - _ = xml.Unmarshal(namespaceStrictToTransitional(f.readXML("xl/theme/theme1.xml")), &theme) + var ( + err error + theme xlsxTheme + ) + + if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML("xl/theme/theme1.xml")))). + Decode(&theme); err != nil && err != io.EOF { + log.Printf("xml decoder error: %s", err) + } + return &theme } -- cgit v1.2.1 From 7358dca436f6ca5948a3f2865b14e828863d86a9 Mon Sep 17 00:00:00 2001 From: match-meng <54879059+match-meng@users.noreply.github.com> Date: Fri, 20 Dec 2019 22:22:56 +0800 Subject: Update comments for the xmlNewDecoder (#542) --- excelize.go | 7 ++++--- rows.go | 5 ++++- sheet.go | 2 +- xmlWorksheet.go | 2 +- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/excelize.go b/excelize.go index 8e386a8..a2e20ff 100644 --- a/excelize.go +++ b/excelize.go @@ -66,7 +66,7 @@ func OpenFile(filename string) (*File, error) { return f, nil } -// object builder +// newFile is object builder func newFile() *File { return &File{ checked: make(map[string]bool), @@ -117,10 +117,11 @@ func OpenReader(r io.Reader) (*File, error) { return f, nil } -// CharsetTranscoder Set user defined codepage transcoder function for open XLSX from non UTF-8 encoding +// CharsetTranscoder Set user defined codepage transcoder function for open +// XLSX from non UTF-8 encoding. func (f *File) CharsetTranscoder(fn charsetTranscoderFn) *File { f.CharsetReader = fn; return f } -// Creates new XML decoder with charset reader +// Creates new XML decoder with charset reader. func (f *File) xmlNewDecoder(rdr io.Reader) (ret *xml.Decoder) { ret = xml.NewDecoder(rdr) ret.CharsetReader = f.CharsetReader diff --git a/rows.go b/rows.go index fc7b55a..687828c 100644 --- a/rows.go +++ b/rows.go @@ -283,7 +283,10 @@ func (xlsx *xlsxC) getValueFrom(f *File, d *xlsxSST) (string, error) { case "s": xlsxSI := 0 xlsxSI, _ = strconv.Atoi(xlsx.V) - return f.formattedValue(xlsx.S, d.SI[xlsxSI].String()), nil + if len(d.SI) > xlsxSI { + return f.formattedValue(xlsx.S, d.SI[xlsxSI].String()), nil + } + return f.formattedValue(xlsx.S, xlsx.V), nil case "str": return f.formattedValue(xlsx.S, xlsx.V), nil case "inlineStr": diff --git a/sheet.go b/sheet.go index 3b22a0e..6ef7c6e 100644 --- a/sheet.go +++ b/sheet.go @@ -163,7 +163,7 @@ func (f *File) setContentTypes(index int) { func (f *File) setSheet(index int, name string) { xlsx := xlsxWorksheet{ Dimension: &xlsxDimension{Ref: "A1"}, - SheetViews: xlsxSheetViews{ + SheetViews: &xlsxSheetViews{ SheetView: []xlsxSheetView{{WorkbookViewID: 0}}, }, } diff --git a/xmlWorksheet.go b/xmlWorksheet.go index b785eac..71ff4cc 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -18,7 +18,7 @@ type xlsxWorksheet struct { XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main worksheet"` SheetPr *xlsxSheetPr `xml:"sheetPr"` Dimension *xlsxDimension `xml:"dimension"` - SheetViews xlsxSheetViews `xml:"sheetViews"` + SheetViews *xlsxSheetViews `xml:"sheetViews"` SheetFormatPr *xlsxSheetFormatPr `xml:"sheetFormatPr"` Cols *xlsxCols `xml:"cols"` SheetData xlsxSheetData `xml:"sheetData"` -- cgit v1.2.1 From ae2865d9237cfd27d7bc4fbef3870b3361597be8 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 22 Dec 2019 00:02:09 +0800 Subject: Improve code coverage unit tests --- calcchain_test.go | 19 +++++++ cell_test.go | 9 +++- comment_test.go | 56 +++++++++++++++++++++ datavalidation_test.go | 9 ++++ docProps_test.go | 13 +++++ excelize_test.go | 133 ++++++++++++++++++++++++++++++++++++++----------- picture.go | 39 ++++++++++----- picture_test.go | 42 +++++++++------- rows_test.go | 78 +++++++++++++++++++++-------- sheet.go | 28 +++++++---- sheet_test.go | 20 ++++++++ sparkline.go | 103 ++++++++++++++++++++------------------ sparkline_test.go | 9 ++++ stream_test.go | 18 ++++++- xmlWorkbook.go | 2 +- xmlWorksheet.go | 2 +- 16 files changed, 438 insertions(+), 142 deletions(-) create mode 100644 calcchain_test.go create mode 100644 comment_test.go diff --git a/calcchain_test.go b/calcchain_test.go new file mode 100644 index 0000000..842dde1 --- /dev/null +++ b/calcchain_test.go @@ -0,0 +1,19 @@ +package excelize + +import "testing" + +func TestCalcChainReader(t *testing.T) { + f := NewFile() + f.CalcChain = nil + f.XLSX["xl/calcChain.xml"] = MacintoshCyrillicCharset + f.calcChainReader() +} + +func TestDeleteCalcChain(t *testing.T) { + f := NewFile() + f.CalcChain = &xlsxCalcChain{C: []xlsxCalcChainC{}} + f.ContentTypes.Overrides = append(f.ContentTypes.Overrides, xlsxOverride{ + PartName: "/xl/calcChain.xml", + }) + f.deleteCalcChain(1, "A1") +} diff --git a/cell_test.go b/cell_test.go index b030622..7d3339f 100644 --- a/cell_test.go +++ b/cell_test.go @@ -95,8 +95,15 @@ func TestSetCellBool(t *testing.T) { } func TestGetCellFormula(t *testing.T) { + // Test get cell formula on not exist worksheet. f := NewFile() - f.GetCellFormula("Sheet", "A1") + _, err := f.GetCellFormula("SheetN", "A1") + assert.EqualError(t, err, "sheet SheetN is not exist") + + // Test get cell formula on no formula cell. + f.SetCellValue("Sheet1", "A1", true) + _, err = f.GetCellFormula("Sheet1", "A1") + assert.NoError(t, err) } func ExampleFile_SetCellFloat() { diff --git a/comment_test.go b/comment_test.go new file mode 100644 index 0000000..dd07951 --- /dev/null +++ b/comment_test.go @@ -0,0 +1,56 @@ +// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// this source code is governed by a BSD-style license that can be found in +// the LICENSE file. +// +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.10 or later. + +package excelize + +import ( + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestAddComments(t *testing.T) { + f, err := prepareTestBook1() + if !assert.NoError(t, err) { + t.FailNow() + } + + s := strings.Repeat("c", 32768) + assert.NoError(t, f.AddComment("Sheet1", "A30", `{"author":"`+s+`","text":"`+s+`"}`)) + assert.NoError(t, f.AddComment("Sheet2", "B7", `{"author":"Excelize: ","text":"This is a comment."}`)) + + // Test add comment on not exists worksheet. + assert.EqualError(t, f.AddComment("SheetN", "B7", `{"author":"Excelize: ","text":"This is a comment."}`), "sheet SheetN is not exist") + + if assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddComments.xlsx"))) { + assert.Len(t, f.GetComments(), 2) + } +} + +func TestDecodeVMLDrawingReader(t *testing.T) { + f := NewFile() + path := "xl/drawings/vmlDrawing1.xml" + f.XLSX[path] = MacintoshCyrillicCharset + f.decodeVMLDrawingReader(path) +} + +func TestCommentsReader(t *testing.T) { + f := NewFile() + path := "xl/comments1.xml" + f.XLSX[path] = MacintoshCyrillicCharset + f.commentsReader(path) +} + +func TestCountComments(t *testing.T) { + f := NewFile() + f.Comments["xl/comments1.xml"] = nil + assert.Equal(t, f.countComments(), 1) +} diff --git a/datavalidation_test.go b/datavalidation_test.go index 211830d..763bad1 100644 --- a/datavalidation_test.go +++ b/datavalidation_test.go @@ -11,6 +11,7 @@ package excelize import ( "path/filepath" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -85,4 +86,12 @@ func TestDataValidationError(t *testing.T) { if !assert.NoError(t, f.SaveAs(resultFile)) { t.FailNow() } + + // Test width invalid data validation formula. + dvRange.Formula1 = strings.Repeat("s", dataValidationFormulaStrLen+22) + assert.EqualError(t, dvRange.SetRange(10, 20, DataValidationTypeWhole, DataValidationOperatorGreaterThan), "data validation must be 0-255 characters") + + // Test add data validation on no exists worksheet. + f = NewFile() + assert.EqualError(t, f.AddDataValidation("SheetN", nil), "sheet SheetN is not exist") } diff --git a/docProps_test.go b/docProps_test.go index df3122b..30c3149 100644 --- a/docProps_test.go +++ b/docProps_test.go @@ -16,6 +16,8 @@ import ( "github.com/stretchr/testify/assert" ) +var MacintoshCyrillicCharset = []byte{0x8F, 0xF0, 0xE8, 0xE2, 0xE5, 0xF2, 0x20, 0xEC, 0xE8, 0xF0} + func TestSetDocProps(t *testing.T) { f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) if !assert.NoError(t, err) { @@ -40,6 +42,11 @@ func TestSetDocProps(t *testing.T) { assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetDocProps.xlsx"))) f.XLSX["docProps/core.xml"] = nil assert.NoError(t, f.SetDocProps(&DocProperties{})) + + // Test unsupport charset + f = NewFile() + f.XLSX["docProps/core.xml"] = MacintoshCyrillicCharset + assert.EqualError(t, f.SetDocProps(&DocProperties{}), "xml decode error: XML syntax error on line 1: invalid UTF-8") } func TestGetDocProps(t *testing.T) { @@ -53,4 +60,10 @@ func TestGetDocProps(t *testing.T) { f.XLSX["docProps/core.xml"] = nil _, err = f.GetDocProps() assert.NoError(t, err) + + // Test unsupport charset + f = NewFile() + f.XLSX["docProps/core.xml"] = MacintoshCyrillicCharset + _, err = f.GetDocProps() + assert.EqualError(t, err, "xml decode error: XML syntax error on line 1: invalid UTF-8") } diff --git a/excelize_test.go b/excelize_test.go index 95d63fd..6929a4f 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -2,6 +2,8 @@ package excelize import ( "bytes" + "compress/gzip" + "encoding/xml" "fmt" "image/color" _ "image/gif" @@ -184,6 +186,11 @@ func TestSaveAsWrongPath(t *testing.T) { } } +func TestCharsetTranscoder(t *testing.T) { + f := NewFile() + f.CharsetTranscoder(*new(charsetTranscoderFn)) +} + func TestOpenReader(t *testing.T) { _, err := OpenReader(strings.NewReader("")) assert.EqualError(t, err, "zip: not a valid zip file") @@ -195,6 +202,18 @@ func TestOpenReader(t *testing.T) { 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, })) assert.EqualError(t, err, "not support encrypted file currently") + + // Test unexpected EOF. + var b bytes.Buffer + w := gzip.NewWriter(&b) + defer w.Close() + w.Flush() + + r, _ := gzip.NewReader(&b) + defer r.Close() + + _, err = OpenReader(r) + assert.EqualError(t, err, "unexpected EOF") } func TestBrokenFile(t *testing.T) { @@ -924,24 +943,6 @@ func TestAddShape(t *testing.T) { assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddShape2.xlsx"))) } -func TestAddComments(t *testing.T) { - f, err := prepareTestBook1() - if !assert.NoError(t, err) { - t.FailNow() - } - - s := strings.Repeat("c", 32768) - assert.NoError(t, f.AddComment("Sheet1", "A30", `{"author":"`+s+`","text":"`+s+`"}`)) - assert.NoError(t, f.AddComment("Sheet2", "B7", `{"author":"Excelize: ","text":"This is a comment."}`)) - - // Test add comment on not exists worksheet. - assert.EqualError(t, f.AddComment("SheetN", "B7", `{"author":"Excelize: ","text":"This is a comment."}`), "sheet SheetN is not exist") - - if assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddComments.xlsx"))) { - assert.Len(t, f.GetComments(), 2) - } -} - func TestGetSheetComments(t *testing.T) { f := NewFile() assert.Equal(t, "", f.getSheetComments(0)) @@ -1005,18 +1006,37 @@ func TestAutoFilterError(t *testing.T) { } } -func TestSetPane(t *testing.T) { +func TestSetActiveSheet(t *testing.T) { + f := NewFile() + f.WorkBook.BookViews = nil + f.SetActiveSheet(1) + f.WorkBook.BookViews = &xlsxBookViews{WorkBookView: []xlsxWorkBookView{}} + f.Sheet["xl/worksheets/sheet1.xml"].SheetViews = &xlsxSheetViews{SheetView: []xlsxSheetView{}} + f.SetActiveSheet(1) +} + +func TestSetSheetVisible(t *testing.T) { + f := NewFile() + f.WorkBook.Sheets.Sheet[0].Name = "SheetN" + assert.EqualError(t, f.SetSheetVisible("Sheet1", false), "sheet SheetN is not exist") +} + +func TestGetActiveSheetIndex(t *testing.T) { + f := NewFile() + f.WorkBook.BookViews = nil + assert.Equal(t, 1, f.GetActiveSheetIndex()) +} + +func TestRelsWriter(t *testing.T) { + f := NewFile() + f.Relationships["xl/worksheets/sheet/rels/sheet1.xml.rel"] = &xlsxRelationships{} + f.relsWriter() +} + +func TestGetSheetView(t *testing.T) { f := NewFile() - f.SetPanes("Sheet1", `{"freeze":false,"split":false}`) - f.NewSheet("Panes 2") - f.SetPanes("Panes 2", `{"freeze":true,"split":false,"x_split":1,"y_split":0,"top_left_cell":"B1","active_pane":"topRight","panes":[{"sqref":"K16","active_cell":"K16","pane":"topRight"}]}`) - f.NewSheet("Panes 3") - f.SetPanes("Panes 3", `{"freeze":false,"split":true,"x_split":3270,"y_split":1800,"top_left_cell":"N57","active_pane":"bottomLeft","panes":[{"sqref":"I36","active_cell":"I36"},{"sqref":"G33","active_cell":"G33","pane":"topRight"},{"sqref":"J60","active_cell":"J60","pane":"bottomLeft"},{"sqref":"O60","active_cell":"O60","pane":"bottomRight"}]}`) - f.NewSheet("Panes 4") - f.SetPanes("Panes 4", `{"freeze":true,"split":false,"x_split":0,"y_split":9,"top_left_cell":"A34","active_pane":"bottomLeft","panes":[{"sqref":"A11:XFD11","active_cell":"A11","pane":"bottomLeft"}]}`) - f.SetPanes("Panes 4", "") - - assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetPane.xlsx"))) + _, err := f.getSheetView("SheetN", 0) + assert.EqualError(t, err, "sheet SheetN is not exist") } func TestConditionalFormat(t *testing.T) { @@ -1207,6 +1227,61 @@ func TestAddVBAProject(t *testing.T) { assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddVBAProject.xlsm"))) } +func TestContentTypesReader(t *testing.T) { + // Test unsupport charset. + f := NewFile() + f.ContentTypes = nil + f.XLSX["[Content_Types].xml"] = MacintoshCyrillicCharset + f.contentTypesReader() +} + +func TestWorkbookReader(t *testing.T) { + // Test unsupport charset. + f := NewFile() + f.WorkBook = nil + f.XLSX["xl/workbook.xml"] = MacintoshCyrillicCharset + f.workbookReader() +} + +func TestWorkSheetReader(t *testing.T) { + // Test unsupport charset. + f := NewFile() + delete(f.Sheet, "xl/worksheets/sheet1.xml") + f.XLSX["xl/worksheets/sheet1.xml"] = MacintoshCyrillicCharset + _, err := f.workSheetReader("Sheet1") + assert.EqualError(t, err, "xml decode error: XML syntax error on line 1: invalid UTF-8") + + // Test on no checked worksheet. + f = NewFile() + delete(f.Sheet, "xl/worksheets/sheet1.xml") + f.XLSX["xl/worksheets/sheet1.xml"] = []byte(``) + f.checked = nil + _, err = f.workSheetReader("Sheet1") + assert.NoError(t, err) +} + +func TestRelsReader(t *testing.T) { + // Test unsupport charset. + f := NewFile() + rels := "xl/_rels/workbook.xml.rels" + f.Relationships[rels] = nil + f.XLSX[rels] = MacintoshCyrillicCharset + f.relsReader(rels) +} + +func TestDeleteSheetFromWorkbookRels(t *testing.T) { + f := NewFile() + rels := "xl/_rels/workbook.xml.rels" + f.Relationships[rels] = nil + assert.Equal(t, f.deleteSheetFromWorkbookRels("rID"), "") +} + +func TestAttrValToInt(t *testing.T) { + _, err := attrValToInt("r", []xml.Attr{ + {Name: xml.Name{Local: "r"}, Value: "s"}}) + assert.EqualError(t, err, `strconv.Atoi: parsing "s": invalid syntax`) +} + func prepareTestBook1() (*File, error) { f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) if err != nil { diff --git a/picture.go b/picture.go index 2420350..01df849 100644 --- a/picture.go +++ b/picture.go @@ -477,24 +477,14 @@ func (f *File) getPicture(row, col int, drawingXML, drawingRelationships string) var ( wsDr *xlsxWsDr ok bool - anchor *xdrCellAnchor deWsDr *decodeWsDr drawRel *xlsxRelationship deTwoCellAnchor *decodeTwoCellAnchor ) 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 { - drawRel = f.getDrawingRelationships(drawingRelationships, - anchor.Pic.BlipFill.Blip.Embed) - if _, ok = supportImageTypes[filepath.Ext(drawRel.Target)]; ok { - ret, buf = filepath.Base(drawRel.Target), []byte(f.XLSX[strings.Replace(drawRel.Target, "..", "xl", -1)]) - return - } - } - } + if ret, buf = f.getPictureFromWsDr(row, col, drawingRelationships, wsDr); len(buf) > 0 { + return } deWsDr = new(decodeWsDr) if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(drawingXML)))). @@ -514,13 +504,36 @@ func (f *File) getPicture(row, col int, drawingXML, drawingRelationships string) if deTwoCellAnchor.From.Col == col && deTwoCellAnchor.From.Row == row { drawRel = f.getDrawingRelationships(drawingRelationships, deTwoCellAnchor.Pic.BlipFill.Blip.Embed) if _, ok = supportImageTypes[filepath.Ext(drawRel.Target)]; ok { - ret, buf = filepath.Base(drawRel.Target), []byte(f.XLSX[strings.Replace(drawRel.Target, "..", "xl", -1)]) + ret, buf = filepath.Base(drawRel.Target), f.XLSX[strings.Replace(drawRel.Target, "..", "xl", -1)] return } } } } + return +} +// getPictureFromWsDr provides a function to get picture base name and raw +// content in worksheet drawing by given coordinates and drawing +// relationships. +func (f *File) getPictureFromWsDr(row, col int, drawingRelationships string, wsDr *xlsxWsDr) (ret string, buf []byte) { + var ( + ok bool + anchor *xdrCellAnchor + drawRel *xlsxRelationship + ) + for _, anchor = range wsDr.TwoCellAnchor { + if anchor.From != nil && anchor.Pic != nil { + if anchor.From.Col == col && anchor.From.Row == row { + drawRel = f.getDrawingRelationships(drawingRelationships, + anchor.Pic.BlipFill.Blip.Embed) + if _, ok = supportImageTypes[filepath.Ext(drawRel.Target)]; ok { + ret, buf = filepath.Base(drawRel.Target), f.XLSX[strings.Replace(drawRel.Target, "..", "xl", -1)] + return + } + } + } + } return } diff --git a/picture_test.go b/picture_test.go index 9a2edda..6af3904 100644 --- a/picture_test.go +++ b/picture_test.go @@ -92,12 +92,12 @@ func TestAddPictureErrors(t *testing.T) { } func TestGetPicture(t *testing.T) { - xlsx, err := prepareTestBook1() + f, err := prepareTestBook1() if !assert.NoError(t, err) { t.FailNow() } - file, raw, err := xlsx.GetPicture("Sheet1", "F21") + file, raw, err := f.GetPicture("Sheet1", "F21") assert.NoError(t, err) if !assert.NotEmpty(t, filepath.Join("test", file)) || !assert.NotEmpty(t, raw) || !assert.NoError(t, ioutil.WriteFile(filepath.Join("test", file), raw, 0644)) { @@ -106,37 +106,37 @@ func TestGetPicture(t *testing.T) { } // Try to get picture from a worksheet with illegal cell coordinates. - _, _, err = xlsx.GetPicture("Sheet1", "A") + _, _, err = f.GetPicture("Sheet1", "A") assert.EqualError(t, err, `cannot convert cell "A" to coordinates: invalid cell name "A"`) // Try to get picture from a worksheet that doesn't contain any images. - file, raw, err = xlsx.GetPicture("Sheet3", "I9") + file, raw, err = f.GetPicture("Sheet3", "I9") assert.EqualError(t, err, "sheet Sheet3 is not exist") assert.Empty(t, file) assert.Empty(t, raw) // Try to get picture from a cell that doesn't contain an image. - file, raw, err = xlsx.GetPicture("Sheet2", "A2") + file, raw, err = f.GetPicture("Sheet2", "A2") assert.NoError(t, err) assert.Empty(t, file) assert.Empty(t, raw) - xlsx.getDrawingRelationships("xl/worksheets/_rels/sheet1.xml.rels", "rId8") - xlsx.getDrawingRelationships("", "") - xlsx.getSheetRelationshipsTargetByID("", "") - xlsx.deleteSheetRelationships("", "") + f.getDrawingRelationships("xl/worksheets/_rels/sheet1.xml.rels", "rId8") + f.getDrawingRelationships("", "") + f.getSheetRelationshipsTargetByID("", "") + f.deleteSheetRelationships("", "") // Try to get picture from a local storage file. - if !assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestGetPicture.xlsx"))) { + if !assert.NoError(t, f.SaveAs(filepath.Join("test", "TestGetPicture.xlsx"))) { t.FailNow() } - xlsx, err = OpenFile(filepath.Join("test", "TestGetPicture.xlsx")) + f, err = OpenFile(filepath.Join("test", "TestGetPicture.xlsx")) if !assert.NoError(t, err) { t.FailNow() } - file, raw, err = xlsx.GetPicture("Sheet1", "F21") + file, raw, err = f.GetPicture("Sheet1", "F21") assert.NoError(t, err) if !assert.NotEmpty(t, filepath.Join("test", file)) || !assert.NotEmpty(t, raw) || !assert.NoError(t, ioutil.WriteFile(filepath.Join("test", file), raw, 0644)) { @@ -145,7 +145,14 @@ func TestGetPicture(t *testing.T) { } // Try to get picture from a local storage file that doesn't contain an image. - file, raw, err = xlsx.GetPicture("Sheet1", "F22") + file, raw, err = f.GetPicture("Sheet1", "F22") + assert.NoError(t, err) + assert.Empty(t, file) + assert.Empty(t, raw) + + // Test get picture from none drawing worksheet. + f = NewFile() + file, raw, err = f.GetPicture("Sheet1", "F22") assert.NoError(t, err) assert.Empty(t, file) assert.Empty(t, raw) @@ -160,11 +167,9 @@ func TestAddDrawingPicture(t *testing.T) { func TestAddPictureFromBytes(t *testing.T) { f := NewFile() imgFile, err := ioutil.ReadFile("logo.png") - if err != nil { - t.Error("Unable to load logo for test") - } - f.AddPictureFromBytes("Sheet1", fmt.Sprint("A", 1), "", "logo", ".png", imgFile) - f.AddPictureFromBytes("Sheet1", fmt.Sprint("A", 50), "", "logo", ".png", imgFile) + assert.NoError(t, err, "Unable to load logo for test") + assert.NoError(t, f.AddPictureFromBytes("Sheet1", fmt.Sprint("A", 1), "", "logo", ".png", imgFile)) + assert.NoError(t, f.AddPictureFromBytes("Sheet1", fmt.Sprint("A", 50), "", "logo", ".png", imgFile)) imageCount := 0 for fileName := range f.XLSX { if strings.Contains(fileName, "media/image") { @@ -172,4 +177,5 @@ func TestAddPictureFromBytes(t *testing.T) { } } assert.Equal(t, 1, imageCount, "Duplicate image should only be stored once.") + assert.EqualError(t, f.AddPictureFromBytes("SheetN", fmt.Sprint("A", 1), "", "logo", ".png", imgFile), "sheet SheetN is not exist") } diff --git a/rows_test.go b/rows_test.go index 6b50c75..6494242 100644 --- a/rows_test.go +++ b/rows_test.go @@ -1,6 +1,7 @@ package excelize import ( + "bytes" "fmt" "path/filepath" "testing" @@ -12,12 +13,12 @@ import ( func TestRows(t *testing.T) { const sheet2 = "Sheet2" - xlsx, err := OpenFile(filepath.Join("test", "Book1.xlsx")) + f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) if !assert.NoError(t, err) { t.FailNow() } - rows, err := xlsx.Rows(sheet2) + rows, err := f.Rows(sheet2) if !assert.NoError(t, err) { t.FailNow() } @@ -32,7 +33,7 @@ func TestRows(t *testing.T) { t.FailNow() } - returnedRows, err := xlsx.GetRows(sheet2) + returnedRows, err := f.GetRows(sheet2) assert.NoError(t, err) for i := range returnedRows { returnedRows[i] = trimSliceSpace(returnedRows[i]) @@ -40,6 +41,11 @@ func TestRows(t *testing.T) { if !assert.Equal(t, collectedRows, returnedRows) { t.FailNow() } + + f = NewFile() + f.XLSX["xl/worksheets/sheet1.xml"] = []byte(`1B`) + _, err = f.Rows("Sheet1") + assert.EqualError(t, err, `strconv.Atoi: parsing "A": invalid syntax`) } func TestRowsIterator(t *testing.T) { @@ -126,6 +132,35 @@ func TestRowHeight(t *testing.T) { convertColWidthToPixels(0) } +func TestColumns(t *testing.T) { + f := NewFile() + rows, err := f.Rows("Sheet1") + assert.NoError(t, err) + rows.decoder = f.xmlNewDecoder(bytes.NewReader([]byte(`1B`))) + _, err = rows.Columns() + assert.EqualError(t, err, `strconv.Atoi: parsing "A": invalid syntax`) + + rows.decoder = f.xmlNewDecoder(bytes.NewReader([]byte(`1B`))) + _, err = rows.Columns() + assert.NoError(t, err) + + rows.curRow = 3 + rows.decoder = f.xmlNewDecoder(bytes.NewReader([]byte(`1`))) + _, err = rows.Columns() + assert.EqualError(t, err, `cannot convert cell "A" to coordinates: invalid cell name "A"`) + + // Test token is nil + rows.decoder = f.xmlNewDecoder(bytes.NewReader(nil)) + _, err = rows.Columns() + assert.NoError(t, err) +} + +func TestSharedStringsReader(t *testing.T) { + f := NewFile() + f.XLSX["xl/sharedStrings.xml"] = MacintoshCyrillicCharset + f.sharedStringsReader() +} + func TestRowVisibility(t *testing.T) { f, err := prepareTestBook1() if !assert.NoError(t, err) { @@ -149,61 +184,64 @@ func TestRowVisibility(t *testing.T) { } func TestRemoveRow(t *testing.T) { - xlsx := NewFile() - sheet1 := xlsx.GetSheetName(1) - r, err := xlsx.workSheetReader(sheet1) + f := NewFile() + sheet1 := f.GetSheetName(1) + r, err := f.workSheetReader(sheet1) assert.NoError(t, err) const ( colCount = 10 rowCount = 10 ) - fillCells(xlsx, sheet1, colCount, rowCount) + fillCells(f, sheet1, colCount, rowCount) - xlsx.SetCellHyperLink(sheet1, "A5", "https://github.com/360EntSecGroup-Skylar/excelize", "External") + f.SetCellHyperLink(sheet1, "A5", "https://github.com/360EntSecGroup-Skylar/excelize", "External") - assert.EqualError(t, xlsx.RemoveRow(sheet1, -1), "invalid row number -1") + assert.EqualError(t, f.RemoveRow(sheet1, -1), "invalid row number -1") - assert.EqualError(t, xlsx.RemoveRow(sheet1, 0), "invalid row number 0") + assert.EqualError(t, f.RemoveRow(sheet1, 0), "invalid row number 0") - assert.NoError(t, xlsx.RemoveRow(sheet1, 4)) + assert.NoError(t, f.RemoveRow(sheet1, 4)) if !assert.Len(t, r.SheetData.Row, rowCount-1) { t.FailNow() } - xlsx.MergeCell(sheet1, "B3", "B5") + f.MergeCell(sheet1, "B3", "B5") - assert.NoError(t, xlsx.RemoveRow(sheet1, 2)) + assert.NoError(t, f.RemoveRow(sheet1, 2)) if !assert.Len(t, r.SheetData.Row, rowCount-2) { t.FailNow() } - assert.NoError(t, xlsx.RemoveRow(sheet1, 4)) + assert.NoError(t, f.RemoveRow(sheet1, 4)) if !assert.Len(t, r.SheetData.Row, rowCount-3) { t.FailNow() } - err = xlsx.AutoFilter(sheet1, "A2", "A2", `{"column":"A","expression":"x != blanks"}`) + err = f.AutoFilter(sheet1, "A2", "A2", `{"column":"A","expression":"x != blanks"}`) if !assert.NoError(t, err) { t.FailNow() } - assert.NoError(t, xlsx.RemoveRow(sheet1, 1)) + assert.NoError(t, f.RemoveRow(sheet1, 1)) if !assert.Len(t, r.SheetData.Row, rowCount-4) { t.FailNow() } - assert.NoError(t, xlsx.RemoveRow(sheet1, 2)) + assert.NoError(t, f.RemoveRow(sheet1, 2)) if !assert.Len(t, r.SheetData.Row, rowCount-5) { t.FailNow() } - assert.NoError(t, xlsx.RemoveRow(sheet1, 1)) + assert.NoError(t, f.RemoveRow(sheet1, 1)) if !assert.Len(t, r.SheetData.Row, rowCount-6) { t.FailNow() } - assert.NoError(t, xlsx.RemoveRow(sheet1, 10)) - assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestRemoveRow.xlsx"))) + assert.NoError(t, f.RemoveRow(sheet1, 10)) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestRemoveRow.xlsx"))) + + // Test remove row on not exist worksheet + assert.EqualError(t, f.RemoveRow("SheetN", 1), `sheet SheetN is not exist`) } func TestInsertRow(t *testing.T) { diff --git a/sheet.go b/sheet.go index 6ef7c6e..7412fce 100644 --- a/sheet.go +++ b/sheet.go @@ -505,7 +505,7 @@ func (f *File) copySheet(from, to int) error { // SetSheetVisible provides a function to set worksheet visible by given worksheet // name. A workbook must contain at least one visible worksheet. If the given // worksheet has been activated, this setting will be invalidated. Sheet state -// values as defined by http://msdn.microsoft.com/en-us/library/office/documentformat.openxml.spreadsheet.sheetstatevalues.aspx +// values as defined by https://docs.microsoft.com/en-us/dotnet/api/documentformat.openxml.spreadsheet.sheetstatevalues // // visible // hidden @@ -738,7 +738,8 @@ func (f *File) searchSheet(name, value string, regSearch bool) (result []string, d = f.sharedStringsReader() decoder := f.xmlNewDecoder(bytes.NewReader(f.readXML(name))) for { - token, err := decoder.Token() + var token xml.Token + token, err = decoder.Token() if err != nil || token == nil { if err == io.EOF { err = nil @@ -749,13 +750,9 @@ func (f *File) searchSheet(name, value string, regSearch bool) (result []string, case xml.StartElement: inElement = startElement.Name.Local if inElement == "row" { - for _, attr := range startElement.Attr { - if attr.Name.Local == "r" { - row, err = strconv.Atoi(attr.Value) - if err != nil { - return result, err - } - } + row, err = attrValToInt("r", startElement.Attr) + if err != nil { + return } } if inElement == "c" { @@ -785,7 +782,20 @@ func (f *File) searchSheet(name, value string, regSearch bool) (result []string, default: } } + return +} +// attrValToInt provides a function to convert the local names to an integer +// by given XML attributes and specified names. +func attrValToInt(name string, attrs []xml.Attr) (val int, err error) { + for _, attr := range attrs { + if attr.Name.Local == name { + val, err = strconv.Atoi(attr.Value) + if err != nil { + return + } + } + } return } diff --git a/sheet_test.go b/sheet_test.go index b9e4abf..aada60a 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -75,6 +75,20 @@ func TestNewSheet(t *testing.T) { assert.NoError(t, f.SaveAs(filepath.Join("test", "TestNewSheet.xlsx"))) } +func TestSetPane(t *testing.T) { + f := excelize.NewFile() + assert.NoError(t, f.SetPanes("Sheet1", `{"freeze":false,"split":false}`)) + f.NewSheet("Panes 2") + assert.NoError(t, f.SetPanes("Panes 2", `{"freeze":true,"split":false,"x_split":1,"y_split":0,"top_left_cell":"B1","active_pane":"topRight","panes":[{"sqref":"K16","active_cell":"K16","pane":"topRight"}]}`)) + f.NewSheet("Panes 3") + assert.NoError(t, f.SetPanes("Panes 3", `{"freeze":false,"split":true,"x_split":3270,"y_split":1800,"top_left_cell":"N57","active_pane":"bottomLeft","panes":[{"sqref":"I36","active_cell":"I36"},{"sqref":"G33","active_cell":"G33","pane":"topRight"},{"sqref":"J60","active_cell":"J60","pane":"bottomLeft"},{"sqref":"O60","active_cell":"O60","pane":"bottomRight"}]}`)) + f.NewSheet("Panes 4") + assert.NoError(t, f.SetPanes("Panes 4", `{"freeze":true,"split":false,"x_split":0,"y_split":9,"top_left_cell":"A34","active_pane":"bottomLeft","panes":[{"sqref":"A11:XFD11","active_cell":"A11","pane":"bottomLeft"}]}`)) + assert.NoError(t, f.SetPanes("Panes 4", "")) + assert.EqualError(t, f.SetPanes("SheetN", ""), "sheet SheetN is not exist") + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetPane.xlsx"))) +} + func TestPageLayoutOption(t *testing.T) { const sheet = "Sheet1" @@ -156,6 +170,12 @@ func TestSearchSheet(t *testing.T) { result, err = f.SearchSheet("Sheet1", "[0-9]", true) assert.NoError(t, err) assert.EqualValues(t, expected, result) + + // Test search worksheet data after set cell value + f = excelize.NewFile() + assert.NoError(t, f.SetCellValue("Sheet1", "A1", true)) + _, err = f.SearchSheet("Sheet1", "") + assert.NoError(t, err) } func TestSetPageLayout(t *testing.T) { diff --git a/sparkline.go b/sparkline.go index 9ad5087..47c8d5a 100644 --- a/sparkline.go +++ b/sparkline.go @@ -390,21 +390,14 @@ func (f *File) addSparklineGroupByStyle(ID int) *xlsxX14SparklineGroup { // func (f *File) AddSparkline(sheet string, opt *SparklineOption) (err error) { var ( - ws *xlsxWorksheet - sparkType string - sparkTypes map[string]string - specifiedSparkTypes string - ok bool - group *xlsxX14SparklineGroup - groups *xlsxX14SparklineGroups - decodeExtLst *decodeWorksheetExt - idx int - ext *xlsxWorksheetExt - decodeSparklineGroups *decodeX14SparklineGroups - sparklineGroupBytes []byte - sparklineGroupsBytes []byte - extLst string - extLstBytes, extBytes []byte + ws *xlsxWorksheet + sparkType string + sparkTypes map[string]string + specifiedSparkTypes string + ok bool + group *xlsxX14SparklineGroup + groups *xlsxX14SparklineGroups + sparklineGroupsBytes, extBytes []byte ) // parameter validation @@ -442,38 +435,9 @@ func (f *File) AddSparkline(sheet string, opt *SparklineOption) (err error) { } f.addSparkline(opt, group) if ws.ExtLst.Ext != "" { // append mode ext - decodeExtLst = new(decodeWorksheetExt) - if err = f.xmlNewDecoder(bytes.NewReader([]byte("" + ws.ExtLst.Ext + ""))). - Decode(decodeExtLst); err != nil && err != io.EOF { + if err = f.appendSparkline(ws, group, groups); err != nil { return } - for idx, ext = range decodeExtLst.Ext { - if ext.URI == ExtURISparklineGroups { - decodeSparklineGroups = new(decodeX14SparklineGroups) - if err = f.xmlNewDecoder(bytes.NewReader([]byte(ext.Content))). - Decode(decodeSparklineGroups); err != nil && err != io.EOF { - return - } - if sparklineGroupBytes, err = xml.Marshal(group); err != nil { - return - } - groups = &xlsxX14SparklineGroups{ - XMLNSXM: NameSpaceSpreadSheetExcel2006Main, - Content: decodeSparklineGroups.Content + string(sparklineGroupBytes), - } - if sparklineGroupsBytes, err = xml.Marshal(groups); err != nil { - return - } - decodeExtLst.Ext[idx].Content = string(sparklineGroupsBytes) - } - } - if extLstBytes, err = xml.Marshal(decodeExtLst); err != nil { - return - } - extLst = string(extLstBytes) - ws.ExtLst = &xlsxExtLst{ - Ext: strings.TrimSuffix(strings.TrimPrefix(extLst, ""), ""), - } } else { groups = &xlsxX14SparklineGroups{ XMLNSXM: NameSpaceSpreadSheetExcel2006Main, @@ -482,11 +446,10 @@ func (f *File) AddSparkline(sheet string, opt *SparklineOption) (err error) { if sparklineGroupsBytes, err = xml.Marshal(groups); err != nil { return } - ext = &xlsxWorksheetExt{ + if extBytes, err = xml.Marshal(&xlsxWorksheetExt{ URI: ExtURISparklineGroups, Content: string(sparklineGroupsBytes), - } - if extBytes, err = xml.Marshal(ext); err != nil { + }); err != nil { return } ws.ExtLst.Ext = string(extBytes) @@ -534,3 +497,47 @@ func (f *File) addSparkline(opt *SparklineOption, group *xlsxX14SparklineGroup) }) } } + +// appendSparkline provides a function to append sparkline to sparkline +// groups. +func (f *File) appendSparkline(ws *xlsxWorksheet, group *xlsxX14SparklineGroup, groups *xlsxX14SparklineGroups) (err error) { + var ( + idx int + decodeExtLst *decodeWorksheetExt + decodeSparklineGroups *decodeX14SparklineGroups + ext *xlsxWorksheetExt + sparklineGroupsBytes, sparklineGroupBytes, extLstBytes []byte + ) + decodeExtLst = new(decodeWorksheetExt) + if err = f.xmlNewDecoder(bytes.NewReader([]byte("" + ws.ExtLst.Ext + ""))). + Decode(decodeExtLst); err != nil && err != io.EOF { + return + } + for idx, ext = range decodeExtLst.Ext { + if ext.URI == ExtURISparklineGroups { + decodeSparklineGroups = new(decodeX14SparklineGroups) + if err = f.xmlNewDecoder(bytes.NewReader([]byte(ext.Content))). + Decode(decodeSparklineGroups); err != nil && err != io.EOF { + return + } + if sparklineGroupBytes, err = xml.Marshal(group); err != nil { + return + } + groups = &xlsxX14SparklineGroups{ + XMLNSXM: NameSpaceSpreadSheetExcel2006Main, + Content: decodeSparklineGroups.Content + string(sparklineGroupBytes), + } + if sparklineGroupsBytes, err = xml.Marshal(groups); err != nil { + return + } + decodeExtLst.Ext[idx].Content = string(sparklineGroupsBytes) + } + } + if extLstBytes, err = xml.Marshal(decodeExtLst); err != nil { + return + } + ws.ExtLst = &xlsxExtLst{ + Ext: strings.TrimSuffix(strings.TrimPrefix(string(extLstBytes), ""), ""), + } + return +} diff --git a/sparkline_test.go b/sparkline_test.go index d52929b..a5cb216 100644 --- a/sparkline_test.go +++ b/sparkline_test.go @@ -269,6 +269,15 @@ func TestAddSparkline(t *testing.T) { }), "XML syntax error on line 6: element closed by ") } +func TestAppendSparkline(t *testing.T) { + // Test unsupport charset. + f := NewFile() + ws, err := f.workSheetReader("Sheet1") + assert.NoError(t, err) + ws.ExtLst = &xlsxExtLst{Ext: string(MacintoshCyrillicCharset)} + assert.EqualError(t, f.appendSparkline(ws, &xlsxX14SparklineGroup{}, &xlsxX14SparklineGroups{}), "XML syntax error on line 1: invalid UTF-8") +} + func prepareSparklineDataset() *File { f := NewFile() sheet2 := [][]int{ diff --git a/stream_test.go b/stream_test.go index 97c55a7..8371a4e 100644 --- a/stream_test.go +++ b/stream_test.go @@ -37,8 +37,7 @@ func TestStreamWriter(t *testing.T) { assert.NoError(t, streamWriter.SetRow(cell, &row)) } - err = streamWriter.Flush() - assert.NoError(t, err) + assert.NoError(t, streamWriter.Flush()) // Save xlsx file by the given path. assert.NoError(t, file.SaveAs(filepath.Join("test", "TestStreamWriter.xlsx"))) @@ -54,6 +53,21 @@ func TestFlush(t *testing.T) { assert.NoError(t, err) streamWriter.Sheet = "SheetN" assert.EqualError(t, streamWriter.Flush(), "sheet SheetN is not exist") + + // Test close temporary file error + file = NewFile() + streamWriter, err = file.NewStreamWriter("Sheet1") + assert.NoError(t, err) + for rowID := 10; rowID <= 51200; rowID++ { + row := make([]interface{}, 50) + for colID := 0; colID < 50; colID++ { + row[colID] = rand.Intn(640000) + } + cell, _ := CoordinatesToCellName(1, rowID) + assert.NoError(t, streamWriter.SetRow(cell, &row)) + } + assert.NoError(t, streamWriter.tmpFile.Close()) + assert.Error(t, streamWriter.Flush()) } func TestSetRow(t *testing.T) { diff --git a/xmlWorkbook.go b/xmlWorkbook.go index e9ded6c..65606b0 100644 --- a/xmlWorkbook.go +++ b/xmlWorkbook.go @@ -203,7 +203,7 @@ type xlsxDefinedNames struct { // http://schemas.openxmlformats.org/spreadsheetml/2006/main This element // defines a defined name within this workbook. A defined name is descriptive // text that is used to represents a cell, range of cells, formula, or constant -// value. For a descriptions of the attributes see https://msdn.microsoft.com/en-us/library/office/documentformat.openxml.spreadsheet.definedname.aspx +// value. For a descriptions of the attributes see https://docs.microsoft.com/en-us/dotnet/api/documentformat.openxml.spreadsheet.definedname type xlsxDefinedName struct { Comment string `xml:"comment,attr,omitempty"` CustomMenu string `xml:"customMenu,attr,omitempty"` diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 71ff4cc..8f39adf 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -182,7 +182,7 @@ type xlsxSheetViews struct { // last sheetView definition is loaded, and the others are discarded. When // multiple windows are viewing the same sheet, multiple sheetView elements // (with corresponding workbookView entries) are saved. -// See https://msdn.microsoft.com/en-us/library/office/documentformat.openxml.spreadsheet.sheetview.aspx +// See https://docs.microsoft.com/en-us/dotnet/api/documentformat.openxml.spreadsheet.sheetview type xlsxSheetView struct { WindowProtection bool `xml:"windowProtection,attr,omitempty"` ShowFormulas bool `xml:"showFormulas,attr,omitempty"` -- cgit v1.2.1 From 4e4a5b9b3e052d1694442515492792fb1aa74c5a Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 23 Dec 2019 00:07:40 +0800 Subject: Improve compatibility, fix workbook's rels ID calc error --- adjust.go | 2 +- cell.go | 2 +- cellmerged.go | 2 - chart.go | 219 +++++++++++++++++++++++++++---------------------------- comment.go | 15 ++-- excelize.go | 10 ++- excelize_test.go | 36 +++++---- lib.go | 9 +++ shape.go | 9 +-- sheet.go | 22 ++++-- stream_test.go | 2 +- styles.go | 16 ++-- styles_test.go | 2 +- table.go | 3 +- xmlChart.go | 8 +- xmlPivotTable.go | 2 +- xmlWorksheet.go | 26 ++++++- 17 files changed, 213 insertions(+), 172 deletions(-) diff --git a/adjust.go b/adjust.go index bb583f1..c15d4b4 100644 --- a/adjust.go +++ b/adjust.go @@ -53,7 +53,7 @@ func (f *File) adjustHelper(sheet string, dir adjustDirection, num, offset int) return err } checkSheet(xlsx) - checkRow(xlsx) + _ = checkRow(xlsx) if xlsx.MergeCells != nil && len(xlsx.MergeCells.Cells) == 0 { xlsx.MergeCells = nil diff --git a/cell.go b/cell.go index ad4bcdb..e59a659 100644 --- a/cell.go +++ b/cell.go @@ -395,7 +395,7 @@ func (f *File) SetCellHyperLink(sheet, axis, link, linkType string) error { linkData = xlsxHyperlink{ Ref: axis, } - sheetPath, _ := f.sheetMap[trimSheetName(sheet)] + sheetPath := f.sheetMap[trimSheetName(sheet)] sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetPath, "xl/worksheets/") + ".rels" rID := f.addRels(sheetRels, SourceRelationshipHyperLink, link, linkType) linkData.RID = "rId" + strconv.Itoa(rID) diff --git a/cellmerged.go b/cellmerged.go index 968a28a..5bea0bc 100644 --- a/cellmerged.go +++ b/cellmerged.go @@ -129,8 +129,6 @@ func (f *File) UnmergeCell(sheet string, hcell, vcell string) error { if rect1[3] < rect1[1] { rect1[1], rect1[3] = rect1[3], rect1[1] } - hcell, _ = CoordinatesToCellName(rect1[0], rect1[1]) - vcell, _ = CoordinatesToCellName(rect1[2], rect1[3]) // return nil since no MergeCells in the sheet if xlsx.MergeCells == nil { diff --git a/chart.go b/chart.go index bf8155a..5a42c5b 100644 --- a/chart.go +++ b/chart.go @@ -726,8 +726,7 @@ func (f *File) prepareDrawing(xlsx *xlsxWorksheet, drawingID int, sheet, drawing drawingXML = strings.Replace(sheetRelationshipsDrawingXML, "..", "xl", -1) } else { // Add first picture for given sheet. - sheetPath, _ := f.sheetMap[trimSheetName(sheet)] - sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetPath, "xl/worksheets/") + ".rels" + sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(f.sheetMap[trimSheetName(sheet)], "xl/worksheets/") + ".rels" rID := f.addRels(sheetRels, SourceRelationshipDrawingML, sheetRelationshipsDrawingXML, "") f.addSheetDrawing(sheet, rID) } @@ -743,9 +742,9 @@ func (f *File) addChart(formatSet *formatChart) { XMLNSa: NameSpaceDrawingML, XMLNSr: SourceRelationship, XMLNSc16r2: SourceRelationshipChart201506, - Date1904: &attrValBool{Val: false}, - Lang: &attrValString{Val: "en-US"}, - RoundedCorners: &attrValBool{Val: false}, + Date1904: &attrValBool{Val: boolPtr(false)}, + Lang: &attrValString{Val: stringPtr("en-US")}, + RoundedCorners: &attrValBool{Val: boolPtr(false)}, Chart: cChart{ Title: &cTitle{ Tx: cTx{ @@ -761,10 +760,10 @@ func (f *File) addChart(formatSet *formatChart) { SchemeClr: &aSchemeClr{ Val: "tx1", LumMod: &attrValInt{ - Val: 65000, + Val: intPtr(65000), }, LumOff: &attrValInt{ - Val: 35000, + Val: intPtr(35000), }, }, }, @@ -806,29 +805,29 @@ func (f *File) addChart(formatSet *formatChart) { }, }, View3D: &cView3D{ - RotX: &attrValInt{Val: chartView3DRotX[formatSet.Type]}, - RotY: &attrValInt{Val: chartView3DRotY[formatSet.Type]}, - Perspective: &attrValInt{Val: chartView3DPerspective[formatSet.Type]}, - RAngAx: &attrValInt{Val: chartView3DRAngAx[formatSet.Type]}, + RotX: &attrValInt{Val: intPtr(chartView3DRotX[formatSet.Type])}, + RotY: &attrValInt{Val: intPtr(chartView3DRotY[formatSet.Type])}, + Perspective: &attrValInt{Val: intPtr(chartView3DPerspective[formatSet.Type])}, + RAngAx: &attrValInt{Val: intPtr(chartView3DRAngAx[formatSet.Type])}, }, Floor: &cThicknessSpPr{ - Thickness: &attrValInt{Val: 0}, + Thickness: &attrValInt{Val: intPtr(0)}, }, SideWall: &cThicknessSpPr{ - Thickness: &attrValInt{Val: 0}, + Thickness: &attrValInt{Val: intPtr(0)}, }, BackWall: &cThicknessSpPr{ - Thickness: &attrValInt{Val: 0}, + Thickness: &attrValInt{Val: intPtr(0)}, }, PlotArea: &cPlotArea{}, Legend: &cLegend{ - LegendPos: &attrValString{Val: chartLegendPosition[formatSet.Legend.Position]}, - Overlay: &attrValBool{Val: false}, + LegendPos: &attrValString{Val: stringPtr(chartLegendPosition[formatSet.Legend.Position])}, + Overlay: &attrValBool{Val: boolPtr(false)}, }, - PlotVisOnly: &attrValBool{Val: false}, - DispBlanksAs: &attrValString{Val: formatSet.ShowBlanksAs}, - ShowDLblsOverMax: &attrValBool{Val: false}, + PlotVisOnly: &attrValBool{Val: boolPtr(false)}, + DispBlanksAs: &attrValString{Val: stringPtr(formatSet.ShowBlanksAs)}, + ShowDLblsOverMax: &attrValBool{Val: boolPtr(false)}, }, SpPr: &cSpPr{ SolidFill: &aSolidFill{ @@ -842,10 +841,10 @@ func (f *File) addChart(formatSet *formatChart) { SolidFill: &aSolidFill{ SchemeClr: &aSchemeClr{Val: "tx1", LumMod: &attrValInt{ - Val: 15000, + Val: intPtr(15000), }, LumOff: &attrValInt{ - Val: 85000, + Val: intPtr(85000), }, }, }, @@ -928,31 +927,31 @@ func (f *File) addChart(formatSet *formatChart) { func (f *File) drawBaseChart(formatSet *formatChart) *cPlotArea { c := cCharts{ BarDir: &attrValString{ - Val: "col", + Val: stringPtr("col"), }, Grouping: &attrValString{ - Val: "clustered", + Val: stringPtr("clustered"), }, VaryColors: &attrValBool{ - Val: true, + Val: boolPtr(true), }, Ser: f.drawChartSeries(formatSet), Shape: f.drawChartShape(formatSet), DLbls: f.drawChartDLbls(formatSet), AxID: []*attrValInt{ - {Val: 754001152}, - {Val: 753999904}, + {Val: intPtr(754001152)}, + {Val: intPtr(753999904)}, }, - Overlap: &attrValInt{Val: 100}, + Overlap: &attrValInt{Val: intPtr(100)}, } var ok bool - if c.BarDir.Val, ok = plotAreaChartBarDir[formatSet.Type]; !ok { + if *c.BarDir.Val, ok = plotAreaChartBarDir[formatSet.Type]; !ok { c.BarDir = nil } - if c.Grouping.Val, ok = plotAreaChartGrouping[formatSet.Type]; !ok { + if *c.Grouping.Val, ok = plotAreaChartGrouping[formatSet.Type]; !ok { c.Grouping = nil } - if c.Overlap.Val, ok = plotAreaChartOverlap[formatSet.Type]; !ok { + if *c.Overlap.Val, ok = plotAreaChartOverlap[formatSet.Type]; !ok { c.Overlap = nil } catAx := f.drawPlotAreaCatAx(formatSet) @@ -1178,10 +1177,10 @@ func (f *File) drawDoughnutChart(formatSet *formatChart) *cPlotArea { return &cPlotArea{ DoughnutChart: &cCharts{ VaryColors: &attrValBool{ - Val: true, + Val: boolPtr(true), }, Ser: f.drawChartSeries(formatSet), - HoleSize: &attrValInt{Val: 75}, + HoleSize: &attrValInt{Val: intPtr(75)}, }, } } @@ -1192,19 +1191,19 @@ func (f *File) drawLineChart(formatSet *formatChart) *cPlotArea { return &cPlotArea{ LineChart: &cCharts{ Grouping: &attrValString{ - Val: plotAreaChartGrouping[formatSet.Type], + Val: stringPtr(plotAreaChartGrouping[formatSet.Type]), }, VaryColors: &attrValBool{ - Val: false, + Val: boolPtr(false), }, Ser: f.drawChartSeries(formatSet), DLbls: f.drawChartDLbls(formatSet), Smooth: &attrValBool{ - Val: false, + Val: boolPtr(false), }, AxID: []*attrValInt{ - {Val: 754001152}, - {Val: 753999904}, + {Val: intPtr(754001152)}, + {Val: intPtr(753999904)}, }, }, CatAx: f.drawPlotAreaCatAx(formatSet), @@ -1218,7 +1217,7 @@ func (f *File) drawPieChart(formatSet *formatChart) *cPlotArea { return &cPlotArea{ PieChart: &cCharts{ VaryColors: &attrValBool{ - Val: true, + Val: boolPtr(true), }, Ser: f.drawChartSeries(formatSet), }, @@ -1231,7 +1230,7 @@ func (f *File) drawPie3DChart(formatSet *formatChart) *cPlotArea { return &cPlotArea{ Pie3DChart: &cCharts{ VaryColors: &attrValBool{ - Val: true, + Val: boolPtr(true), }, Ser: f.drawChartSeries(formatSet), }, @@ -1244,16 +1243,16 @@ func (f *File) drawRadarChart(formatSet *formatChart) *cPlotArea { return &cPlotArea{ RadarChart: &cCharts{ RadarStyle: &attrValString{ - Val: "marker", + Val: stringPtr("marker"), }, VaryColors: &attrValBool{ - Val: false, + Val: boolPtr(false), }, Ser: f.drawChartSeries(formatSet), DLbls: f.drawChartDLbls(formatSet), AxID: []*attrValInt{ - {Val: 754001152}, - {Val: 753999904}, + {Val: intPtr(754001152)}, + {Val: intPtr(753999904)}, }, }, CatAx: f.drawPlotAreaCatAx(formatSet), @@ -1267,16 +1266,16 @@ func (f *File) drawScatterChart(formatSet *formatChart) *cPlotArea { return &cPlotArea{ ScatterChart: &cCharts{ ScatterStyle: &attrValString{ - Val: "smoothMarker", // line,lineMarker,marker,none,smooth,smoothMarker + Val: stringPtr("smoothMarker"), // line,lineMarker,marker,none,smooth,smoothMarker }, VaryColors: &attrValBool{ - Val: false, + Val: boolPtr(false), }, Ser: f.drawChartSeries(formatSet), DLbls: f.drawChartDLbls(formatSet), AxID: []*attrValInt{ - {Val: 754001152}, - {Val: 753999904}, + {Val: intPtr(754001152)}, + {Val: intPtr(753999904)}, }, }, CatAx: f.drawPlotAreaCatAx(formatSet), @@ -1291,9 +1290,9 @@ func (f *File) drawSurface3DChart(formatSet *formatChart) *cPlotArea { Surface3DChart: &cCharts{ Ser: f.drawChartSeries(formatSet), AxID: []*attrValInt{ - {Val: 754001152}, - {Val: 753999904}, - {Val: 832256642}, + {Val: intPtr(754001152)}, + {Val: intPtr(753999904)}, + {Val: intPtr(832256642)}, }, }, CatAx: f.drawPlotAreaCatAx(formatSet), @@ -1301,7 +1300,7 @@ func (f *File) drawSurface3DChart(formatSet *formatChart) *cPlotArea { SerAx: f.drawPlotAreaSerAx(formatSet), } if formatSet.Type == WireframeSurface3D { - plotArea.Surface3DChart.Wireframe = &attrValBool{Val: true} + plotArea.Surface3DChart.Wireframe = &attrValBool{Val: boolPtr(true)} } return plotArea } @@ -1313,9 +1312,9 @@ func (f *File) drawSurfaceChart(formatSet *formatChart) *cPlotArea { SurfaceChart: &cCharts{ Ser: f.drawChartSeries(formatSet), AxID: []*attrValInt{ - {Val: 754001152}, - {Val: 753999904}, - {Val: 832256642}, + {Val: intPtr(754001152)}, + {Val: intPtr(753999904)}, + {Val: intPtr(832256642)}, }, }, CatAx: f.drawPlotAreaCatAx(formatSet), @@ -1323,7 +1322,7 @@ func (f *File) drawSurfaceChart(formatSet *formatChart) *cPlotArea { SerAx: f.drawPlotAreaSerAx(formatSet), } if formatSet.Type == WireframeContour { - plotArea.SurfaceChart.Wireframe = &attrValBool{Val: true} + plotArea.SurfaceChart.Wireframe = &attrValBool{Val: boolPtr(true)} } return plotArea } @@ -1355,7 +1354,7 @@ func (f *File) drawChartShape(formatSet *formatChart) *attrValString { Col3DCylinderPercentStacked: "cylinder", } if shape, ok := shapes[formatSet.Type]; ok { - return &attrValString{Val: shape} + return &attrValString{Val: stringPtr(shape)} } return nil } @@ -1366,8 +1365,8 @@ func (f *File) drawChartSeries(formatSet *formatChart) *[]cSer { ser := []cSer{} for k := range formatSet.Series { ser = append(ser, cSer{ - IDx: &attrValInt{Val: k}, - Order: &attrValInt{Val: k}, + IDx: &attrValInt{Val: intPtr(k)}, + Order: &attrValInt{Val: intPtr(k)}, Tx: &cTx{ StrRef: &cStrRef{ F: formatSet.Series[k].Name, @@ -1416,8 +1415,8 @@ func (f *File) drawChartSeriesSpPr(i int, formatSet *formatChart) *cSpPr { // data index and format sets. func (f *File) drawChartSeriesDPt(i int, formatSet *formatChart) []*cDPt { dpt := []*cDPt{{ - IDx: &attrValInt{Val: i}, - Bubble3D: &attrValBool{Val: false}, + IDx: &attrValInt{Val: intPtr(i)}, + Bubble3D: &attrValBool{Val: boolPtr(false)}, SpPr: &cSpPr{ SolidFill: &aSolidFill{ SchemeClr: &aSchemeClr{Val: "accent" + strconv.Itoa(i+1)}, @@ -1475,8 +1474,8 @@ func (f *File) drawChartSeriesVal(v formatChartSeries, formatSet *formatChart) * // given data index and format sets. func (f *File) drawChartSeriesMarker(i int, formatSet *formatChart) *cMarker { marker := &cMarker{ - Symbol: &attrValString{Val: "circle"}, - Size: &attrValInt{Val: 5}, + Symbol: &attrValString{Val: stringPtr("circle")}, + Size: &attrValInt{Val: intPtr(5)}, } if i < 6 { marker.SpPr = &cSpPr{ @@ -1542,20 +1541,20 @@ func (f *File) drawCharSeriesBubble3D(formatSet *formatChart) *attrValBool { if _, ok := map[string]bool{Bubble3D: true}[formatSet.Type]; !ok { return nil } - return &attrValBool{Val: true} + return &attrValBool{Val: boolPtr(true)} } // drawChartDLbls provides a function to draw the c:dLbls element by given // format sets. func (f *File) drawChartDLbls(formatSet *formatChart) *cDLbls { return &cDLbls{ - ShowLegendKey: &attrValBool{Val: formatSet.Legend.ShowLegendKey}, - ShowVal: &attrValBool{Val: formatSet.Plotarea.ShowVal}, - ShowCatName: &attrValBool{Val: formatSet.Plotarea.ShowCatName}, - ShowSerName: &attrValBool{Val: formatSet.Plotarea.ShowSerName}, - ShowBubbleSize: &attrValBool{Val: formatSet.Plotarea.ShowBubbleSize}, - ShowPercent: &attrValBool{Val: formatSet.Plotarea.ShowPercent}, - ShowLeaderLines: &attrValBool{Val: formatSet.Plotarea.ShowLeaderLines}, + ShowLegendKey: &attrValBool{Val: boolPtr(formatSet.Legend.ShowLegendKey)}, + ShowVal: &attrValBool{Val: boolPtr(formatSet.Plotarea.ShowVal)}, + ShowCatName: &attrValBool{Val: boolPtr(formatSet.Plotarea.ShowCatName)}, + ShowSerName: &attrValBool{Val: boolPtr(formatSet.Plotarea.ShowSerName)}, + ShowBubbleSize: &attrValBool{Val: boolPtr(formatSet.Plotarea.ShowBubbleSize)}, + ShowPercent: &attrValBool{Val: boolPtr(formatSet.Plotarea.ShowPercent)}, + ShowLeaderLines: &attrValBool{Val: boolPtr(formatSet.Plotarea.ShowLeaderLines)}, } } @@ -1572,8 +1571,8 @@ func (f *File) drawChartSeriesDLbls(formatSet *formatChart) *cDLbls { // drawPlotAreaCatAx provides a function to draw the c:catAx element. func (f *File) drawPlotAreaCatAx(formatSet *formatChart) []*cAxs { - min := &attrValFloat{Val: formatSet.XAxis.Minimum} - max := &attrValFloat{Val: formatSet.XAxis.Maximum} + min := &attrValFloat{Val: float64Ptr(formatSet.XAxis.Minimum)} + max := &attrValFloat{Val: float64Ptr(formatSet.XAxis.Maximum)} if formatSet.XAxis.Minimum == 0 { min = nil } @@ -1582,29 +1581,29 @@ func (f *File) drawPlotAreaCatAx(formatSet *formatChart) []*cAxs { } axs := []*cAxs{ { - AxID: &attrValInt{Val: 754001152}, + AxID: &attrValInt{Val: intPtr(754001152)}, Scaling: &cScaling{ - Orientation: &attrValString{Val: orientation[formatSet.XAxis.ReverseOrder]}, + Orientation: &attrValString{Val: stringPtr(orientation[formatSet.XAxis.ReverseOrder])}, Max: max, Min: min, }, - Delete: &attrValBool{Val: false}, - AxPos: &attrValString{Val: catAxPos[formatSet.XAxis.ReverseOrder]}, + Delete: &attrValBool{Val: boolPtr(false)}, + AxPos: &attrValString{Val: stringPtr(catAxPos[formatSet.XAxis.ReverseOrder])}, NumFmt: &cNumFmt{ FormatCode: "General", SourceLinked: true, }, - MajorTickMark: &attrValString{Val: "none"}, - MinorTickMark: &attrValString{Val: "none"}, - TickLblPos: &attrValString{Val: "nextTo"}, + MajorTickMark: &attrValString{Val: stringPtr("none")}, + MinorTickMark: &attrValString{Val: stringPtr("none")}, + TickLblPos: &attrValString{Val: stringPtr("nextTo")}, SpPr: f.drawPlotAreaSpPr(), TxPr: f.drawPlotAreaTxPr(), - CrossAx: &attrValInt{Val: 753999904}, - Crosses: &attrValString{Val: "autoZero"}, - Auto: &attrValBool{Val: true}, - LblAlgn: &attrValString{Val: "ctr"}, - LblOffset: &attrValInt{Val: 100}, - NoMultiLvlLbl: &attrValBool{Val: false}, + CrossAx: &attrValInt{Val: intPtr(753999904)}, + Crosses: &attrValString{Val: stringPtr("autoZero")}, + Auto: &attrValBool{Val: boolPtr(true)}, + LblAlgn: &attrValString{Val: stringPtr("ctr")}, + LblOffset: &attrValInt{Val: intPtr(100)}, + NoMultiLvlLbl: &attrValBool{Val: boolPtr(false)}, }, } if formatSet.XAxis.MajorGridlines { @@ -1618,8 +1617,8 @@ func (f *File) drawPlotAreaCatAx(formatSet *formatChart) []*cAxs { // drawPlotAreaValAx provides a function to draw the c:valAx element. func (f *File) drawPlotAreaValAx(formatSet *formatChart) []*cAxs { - min := &attrValFloat{Val: formatSet.YAxis.Minimum} - max := &attrValFloat{Val: formatSet.YAxis.Maximum} + min := &attrValFloat{Val: float64Ptr(formatSet.YAxis.Minimum)} + max := &attrValFloat{Val: float64Ptr(formatSet.YAxis.Maximum)} if formatSet.YAxis.Minimum == 0 { min = nil } @@ -1628,26 +1627,26 @@ func (f *File) drawPlotAreaValAx(formatSet *formatChart) []*cAxs { } axs := []*cAxs{ { - AxID: &attrValInt{Val: 753999904}, + AxID: &attrValInt{Val: intPtr(753999904)}, Scaling: &cScaling{ - Orientation: &attrValString{Val: orientation[formatSet.YAxis.ReverseOrder]}, + Orientation: &attrValString{Val: stringPtr(orientation[formatSet.YAxis.ReverseOrder])}, Max: max, Min: min, }, - Delete: &attrValBool{Val: false}, - AxPos: &attrValString{Val: valAxPos[formatSet.YAxis.ReverseOrder]}, + Delete: &attrValBool{Val: boolPtr(false)}, + AxPos: &attrValString{Val: stringPtr(valAxPos[formatSet.YAxis.ReverseOrder])}, NumFmt: &cNumFmt{ FormatCode: chartValAxNumFmtFormatCode[formatSet.Type], SourceLinked: true, }, - MajorTickMark: &attrValString{Val: "none"}, - MinorTickMark: &attrValString{Val: "none"}, - TickLblPos: &attrValString{Val: "nextTo"}, + MajorTickMark: &attrValString{Val: stringPtr("none")}, + MinorTickMark: &attrValString{Val: stringPtr("none")}, + TickLblPos: &attrValString{Val: stringPtr("nextTo")}, SpPr: f.drawPlotAreaSpPr(), TxPr: f.drawPlotAreaTxPr(), - CrossAx: &attrValInt{Val: 754001152}, - Crosses: &attrValString{Val: "autoZero"}, - CrossBetween: &attrValString{Val: chartValAxCrossBetween[formatSet.Type]}, + CrossAx: &attrValInt{Val: intPtr(754001152)}, + Crosses: &attrValString{Val: stringPtr("autoZero")}, + CrossBetween: &attrValString{Val: stringPtr(chartValAxCrossBetween[formatSet.Type])}, }, } if formatSet.YAxis.MajorGridlines { @@ -1657,15 +1656,15 @@ func (f *File) drawPlotAreaValAx(formatSet *formatChart) []*cAxs { axs[0].MinorGridlines = &cChartLines{SpPr: f.drawPlotAreaSpPr()} } if pos, ok := valTickLblPos[formatSet.Type]; ok { - axs[0].TickLblPos.Val = pos + axs[0].TickLblPos.Val = stringPtr(pos) } return axs } // drawPlotAreaSerAx provides a function to draw the c:serAx element. func (f *File) drawPlotAreaSerAx(formatSet *formatChart) []*cAxs { - min := &attrValFloat{Val: formatSet.YAxis.Minimum} - max := &attrValFloat{Val: formatSet.YAxis.Maximum} + min := &attrValFloat{Val: float64Ptr(formatSet.YAxis.Minimum)} + max := &attrValFloat{Val: float64Ptr(formatSet.YAxis.Maximum)} if formatSet.YAxis.Minimum == 0 { min = nil } @@ -1674,18 +1673,18 @@ func (f *File) drawPlotAreaSerAx(formatSet *formatChart) []*cAxs { } return []*cAxs{ { - AxID: &attrValInt{Val: 832256642}, + AxID: &attrValInt{Val: intPtr(832256642)}, Scaling: &cScaling{ - Orientation: &attrValString{Val: orientation[formatSet.YAxis.ReverseOrder]}, + Orientation: &attrValString{Val: stringPtr(orientation[formatSet.YAxis.ReverseOrder])}, Max: max, Min: min, }, - Delete: &attrValBool{Val: false}, - AxPos: &attrValString{Val: catAxPos[formatSet.XAxis.ReverseOrder]}, - TickLblPos: &attrValString{Val: "nextTo"}, + Delete: &attrValBool{Val: boolPtr(false)}, + AxPos: &attrValString{Val: stringPtr(catAxPos[formatSet.XAxis.ReverseOrder])}, + TickLblPos: &attrValString{Val: stringPtr("nextTo")}, SpPr: f.drawPlotAreaSpPr(), TxPr: f.drawPlotAreaTxPr(), - CrossAx: &attrValInt{Val: 753999904}, + CrossAx: &attrValInt{Val: intPtr(753999904)}, }, } } @@ -1701,8 +1700,8 @@ func (f *File) drawPlotAreaSpPr() *cSpPr { SolidFill: &aSolidFill{ SchemeClr: &aSchemeClr{ Val: "tx1", - LumMod: &attrValInt{Val: 15000}, - LumOff: &attrValInt{Val: 85000}, + LumMod: &attrValInt{Val: intPtr(15000)}, + LumOff: &attrValInt{Val: intPtr(85000)}, }, }, }, @@ -1734,8 +1733,8 @@ func (f *File) drawPlotAreaTxPr() *cTxPr { SolidFill: &aSolidFill{ SchemeClr: &aSchemeClr{ Val: "tx1", - LumMod: &attrValInt{Val: 15000}, - LumOff: &attrValInt{Val: 85000}, + LumMod: &attrValInt{Val: intPtr(15000)}, + LumOff: &attrValInt{Val: intPtr(85000)}, }, }, Latin: &aLatin{Typeface: "+mn-lt"}, diff --git a/comment.go b/comment.go index 99630c9..486a035 100644 --- a/comment.go +++ b/comment.go @@ -101,8 +101,7 @@ func (f *File) AddComment(sheet, cell, format string) error { drawingVML = strings.Replace(sheetRelationshipsDrawingVML, "..", "xl", -1) } else { // Add first comment for given sheet. - sheetPath, _ := f.sheetMap[trimSheetName(sheet)] - sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetPath, "xl/worksheets/") + ".rels" + sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(f.sheetMap[trimSheetName(sheet)], "xl/worksheets/") + ".rels" rID := f.addRels(sheetRels, SourceRelationshipDrawingVML, sheetRelationshipsDrawingVML, "") f.addRels(sheetRels, SourceRelationshipComments, sheetRelationshipsComments, "") f.addSheetLegacyDrawing(sheet, rID) @@ -256,23 +255,23 @@ func (f *File) addComment(commentsXML, cell string, formatSet *formatComment) { { RPr: &xlsxRPr{ B: " ", - Sz: &attrValFloat{Val: 9}, + Sz: &attrValFloat{Val: float64Ptr(9)}, Color: &xlsxColor{ Indexed: 81, }, - RFont: &attrValString{Val: defaultFont}, - Family: &attrValInt{Val: 2}, + RFont: &attrValString{Val: stringPtr(defaultFont)}, + Family: &attrValInt{Val: intPtr(2)}, }, T: a, }, { RPr: &xlsxRPr{ - Sz: &attrValFloat{Val: 9}, + Sz: &attrValFloat{Val: float64Ptr(9)}, Color: &xlsxColor{ Indexed: 81, }, - RFont: &attrValString{Val: defaultFont}, - Family: &attrValInt{Val: 2}, + RFont: &attrValString{Val: stringPtr(defaultFont)}, + Family: &attrValInt{Val: intPtr(2)}, }, T: t, }, diff --git a/excelize.go b/excelize.go index a2e20ff..135028c 100644 --- a/excelize.go +++ b/excelize.go @@ -203,11 +203,17 @@ func checkSheet(xlsx *xlsxWorksheet) { // relationship type, target and target mode. func (f *File) addRels(relPath, relType, target, targetMode string) int { rels := f.relsReader(relPath) - rID := 0 if rels == nil { rels = &xlsxRelationships{} } - rID = len(rels.Relationships) + 1 + var rID int + for _, rel := range rels.Relationships { + ID, _ := strconv.Atoi(strings.TrimPrefix(rel.ID, "rId")) + if ID > rID { + rID = ID + } + } + rID++ var ID bytes.Buffer ID.WriteString("rId") ID.WriteString(strconv.Itoa(rID)) diff --git a/excelize_test.go b/excelize_test.go index 6929a4f..1d6ed24 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -48,7 +48,7 @@ func TestOpenFile(t *testing.T) { assert.EqualError(t, f.SetCellDefault("Sheet2", "A", strconv.FormatFloat(float64(-100.1588), 'f', -1, 64)), `cannot convert cell "A" to coordinates: invalid cell name "A"`) - f.SetCellInt("Sheet2", "A1", 100) + assert.NoError(t, f.SetCellInt("Sheet2", "A1", 100)) // Test set cell integer value with illegal row number. assert.EqualError(t, f.SetCellInt("Sheet2", "A", 100), `cannot convert cell "A" to coordinates: invalid cell name "A"`) @@ -80,8 +80,10 @@ func TestOpenFile(t *testing.T) { _, err = f.GetCellFormula("Sheet1", "B") assert.EqualError(t, err, `cannot convert cell "B" to coordinates: invalid cell name "B"`) // Test get shared cell formula - f.GetCellFormula("Sheet2", "H11") - f.GetCellFormula("Sheet2", "I11") + _, err = f.GetCellFormula("Sheet2", "H11") + assert.NoError(t, err) + _, err = f.GetCellFormula("Sheet2", "I11") + assert.NoError(t, err) getSharedForumula(&xlsxWorksheet{}, "") // Test read cell value with given illegal rows number. @@ -91,10 +93,14 @@ func TestOpenFile(t *testing.T) { assert.EqualError(t, err, `cannot convert cell "A" to coordinates: invalid cell name "A"`) // Test read cell value with given lowercase column number. - f.GetCellValue("Sheet2", "a5") - f.GetCellValue("Sheet2", "C11") - f.GetCellValue("Sheet2", "D11") - f.GetCellValue("Sheet2", "D12") + _, err = f.GetCellValue("Sheet2", "a5") + assert.NoError(t, err) + _, err = f.GetCellValue("Sheet2", "C11") + assert.NoError(t, err) + _, err = f.GetCellValue("Sheet2", "D11") + assert.NoError(t, err) + _, err = f.GetCellValue("Sheet2", "D12") + assert.NoError(t, err) // Test SetCellValue function. assert.NoError(t, f.SetCellValue("Sheet2", "F1", " Hello")) assert.NoError(t, f.SetCellValue("Sheet2", "G1", []byte("World"))) @@ -147,7 +153,8 @@ func TestOpenFile(t *testing.T) { // Test completion column. f.SetCellValue("Sheet2", "M2", nil) // Test read cell value with given axis large than exists row. - f.GetCellValue("Sheet2", "E231") + _, err = f.GetCellValue("Sheet2", "E231") + assert.NoError(t, err) // Test get active worksheet of XLSX and get worksheet name of XLSX by given worksheet index. f.GetSheetName(f.GetActiveSheetIndex()) // Test get worksheet index of XLSX by given worksheet name. @@ -302,13 +309,10 @@ func TestSetCellHyperLink(t *testing.T) { assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellHyperLink.xlsx"))) - file := NewFile() - for row := 1; row <= 65530; row++ { - cell, err := CoordinatesToCellName(1, row) - assert.NoError(t, err) - assert.NoError(t, file.SetCellHyperLink("Sheet1", cell, "https://github.com/360EntSecGroup-Skylar/excelize", "External")) - } - assert.EqualError(t, file.SetCellHyperLink("Sheet1", "A65531", "https://github.com/360EntSecGroup-Skylar/excelize", "External"), "over maximum limit hyperlinks in a worksheet") + f = NewFile() + f.workSheetReader("Sheet1") + f.Sheet["xl/worksheets/sheet1.xml"].Hyperlinks = &xlsxHyperlinks{Hyperlink: make([]xlsxHyperlink, 65530)} + assert.EqualError(t, f.SetCellHyperLink("Sheet1", "A65531", "https://github.com/360EntSecGroup-Skylar/excelize", "External"), "over maximum limit hyperlinks in a worksheet") f = NewFile() f.workSheetReader("Sheet1") @@ -1013,6 +1017,8 @@ func TestSetActiveSheet(t *testing.T) { f.WorkBook.BookViews = &xlsxBookViews{WorkBookView: []xlsxWorkBookView{}} f.Sheet["xl/worksheets/sheet1.xml"].SheetViews = &xlsxSheetViews{SheetView: []xlsxSheetView{}} f.SetActiveSheet(1) + f.Sheet["xl/worksheets/sheet1.xml"].SheetViews = nil + f.SetActiveSheet(1) } func TestSetSheetVisible(t *testing.T) { diff --git a/lib.go b/lib.go index edac98a..86f8d16 100644 --- a/lib.go +++ b/lib.go @@ -198,6 +198,15 @@ func CoordinatesToCellName(col, row int) (string, error) { // boolPtr returns a pointer to a bool with the given value. func boolPtr(b bool) *bool { return &b } +// intPtr returns a pointer to a int with the given value. +func intPtr(i int) *int { return &i } + +// float64Ptr returns a pofloat64er to a float64 with the given value. +func float64Ptr(f float64) *float64 { return &f } + +// stringPtr returns a pointer to a string with the given value. +func stringPtr(s string) *string { return &s } + // defaultTrue returns true if b is nil, or the pointed value. func defaultTrue(b *bool) bool { if b == nil { diff --git a/shape.go b/shape.go index f284e43..2ea66ea 100644 --- a/shape.go +++ b/shape.go @@ -275,8 +275,7 @@ func (f *File) AddShape(sheet, cell, format string) error { drawingXML = strings.Replace(sheetRelationshipsDrawingXML, "..", "xl", -1) } else { // Add first shape for given sheet. - name, _ := f.sheetMap[trimSheetName(sheet)] - sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(name, "xl/worksheets/") + ".rels" + sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(f.sheetMap[trimSheetName(sheet)], "xl/worksheets/") + ".rels" rID := f.addRels(sheetRels, SourceRelationshipDrawingML, sheetRelationshipsDrawingXML, "") f.addSheetDrawing(sheet, rID) } @@ -362,7 +361,7 @@ func (f *File) addDrawingShape(sheet, drawingXML, cell string, formatSet *format FontRef: &aFontRef{ Idx: "minor", SchemeClr: &attrValString{ - Val: "tx1", + Val: stringPtr("tx1"), }, }, }, @@ -422,7 +421,7 @@ func (f *File) addDrawingShape(sheet, drawingXML, cell string, formatSet *format if len(srgbClr) == 6 { paragraph.R.RPr.SolidFill = &aSolidFill{ SrgbClr: &attrValString{ - Val: srgbClr, + Val: stringPtr(srgbClr), }, } } @@ -454,7 +453,7 @@ func setShapeRef(color string, i int) *aRef { return &aRef{ Idx: i, SrgbClr: &attrValString{ - Val: strings.Replace(strings.ToUpper(color), "#", "", -1), + Val: stringPtr(strings.Replace(strings.ToUpper(color), "#", "", -1)), }, } } diff --git a/sheet.go b/sheet.go index 7412fce..954de5b 100644 --- a/sheet.go +++ b/sheet.go @@ -249,6 +249,11 @@ func (f *File) SetActiveSheet(index int) { } for idx, name := range f.GetSheetMap() { xlsx, _ := f.workSheetReader(name) + if xlsx.SheetViews == nil { + xlsx.SheetViews = &xlsxSheetViews{ + SheetView: []xlsxSheetView{{WorkbookViewID: 0}}, + } + } if len(xlsx.SheetViews.SheetView) > 0 { xlsx.SheetViews.SheetView[0].TabSelected = false } @@ -305,11 +310,15 @@ func (f *File) SetSheetName(oldName, newName string) { // string. func (f *File) GetSheetName(index int) string { wb := f.workbookReader() - realIdx := index - 1 // sheets are 1 based index, but we're checking against an array - if wb == nil || realIdx < 0 || realIdx >= len(wb.Sheets.Sheet) { + if wb == nil || index < 1 { return "" } - return wb.Sheets.Sheet[realIdx].Name + for _, sheet := range wb.Sheets.Sheet { + if index == sheet.SheetID { + return sheet.Name + } + } + return "" } // GetSheetIndex provides a function to get worksheet index of XLSX by given @@ -342,8 +351,8 @@ func (f *File) GetSheetMap() map[int]string { wb := f.workbookReader() sheetMap := map[int]string{} if wb != nil { - for i, sheet := range wb.Sheets.Sheet { - sheetMap[i+1] = sheet.Name + for _, sheet := range wb.Sheets.Sheet { + sheetMap[sheet.SheetID] = sheet.Name } } return sheetMap @@ -384,8 +393,7 @@ func (f *File) SetSheetBackground(sheet, picture string) error { } file, _ := ioutil.ReadFile(picture) name := f.addMedia(file, ext) - sheetPath, _ := f.sheetMap[trimSheetName(sheet)] - sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetPath, "xl/worksheets/") + ".rels" + sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(f.sheetMap[trimSheetName(sheet)], "xl/worksheets/") + ".rels" rID := f.addRels(sheetRels, SourceRelationshipImage, strings.Replace(name, "xl", "..", 1), "") f.addSheetPicture(sheet, rID) f.setContentTypePartImageExtensions() diff --git a/stream_test.go b/stream_test.go index 8371a4e..4482bd1 100644 --- a/stream_test.go +++ b/stream_test.go @@ -42,7 +42,7 @@ func TestStreamWriter(t *testing.T) { assert.NoError(t, file.SaveAs(filepath.Join("test", "TestStreamWriter.xlsx"))) // Test error exceptions - streamWriter, err = file.NewStreamWriter("SheetN") + _, err = file.NewStreamWriter("SheetN") assert.EqualError(t, err, "sheet SheetN is not exist") } diff --git a/styles.go b/styles.go index fa0507e..56c7196 100644 --- a/styles.go +++ b/styles.go @@ -1957,13 +1957,13 @@ func (f *File) NewConditionalStyle(style string) (int, error) { // Documents generated by excelize start with Calibri. func (f *File) GetDefaultFont() string { font := f.readDefaultFont() - return font.Name.Val + return *font.Name.Val } // SetDefaultFont changes the default font in the workbook. func (f *File) SetDefaultFont(fontName string) { font := f.readDefaultFont() - font.Name.Val = fontName + font.Name.Val = stringPtr(fontName) s := f.stylesReader() s.Fonts.Font[0] = font custom := true @@ -1987,10 +1987,10 @@ func (f *File) setFont(formatStyle *formatStyle) *xlsxFont { formatStyle.Font.Color = "#000000" } fnt := xlsxFont{ - Sz: &attrValFloat{Val: formatStyle.Font.Size}, + Sz: &attrValFloat{Val: float64Ptr(formatStyle.Font.Size)}, Color: &xlsxColor{RGB: getPaletteColor(formatStyle.Font.Color)}, - Name: &attrValString{Val: formatStyle.Font.Family}, - Family: &attrValInt{Val: 2}, + Name: &attrValString{Val: stringPtr(formatStyle.Font.Family)}, + Family: &attrValInt{Val: intPtr(2)}, } if formatStyle.Font.Bold { fnt.B = &formatStyle.Font.Bold @@ -1998,8 +1998,8 @@ func (f *File) setFont(formatStyle *formatStyle) *xlsxFont { if formatStyle.Font.Italic { fnt.I = &formatStyle.Font.Italic } - if fnt.Name.Val == "" { - fnt.Name.Val = f.GetDefaultFont() + if *fnt.Name.Val == "" { + *fnt.Name.Val = f.GetDefaultFont() } if formatStyle.Font.Strike { strike := true @@ -2007,7 +2007,7 @@ func (f *File) setFont(formatStyle *formatStyle) *xlsxFont { } val, ok := fontUnderlineType[formatStyle.Font.Underline] if ok { - fnt.U = &attrValString{Val: val} + fnt.U = &attrValString{Val: stringPtr(val)} } return &fnt } diff --git a/styles_test.go b/styles_test.go index 36a78ed..e6faccb 100644 --- a/styles_test.go +++ b/styles_test.go @@ -175,7 +175,7 @@ func TestNewStyle(t *testing.T) { styles := f.stylesReader() fontID := styles.CellXfs.Xf[styleID].FontID font := styles.Fonts.Font[fontID] - assert.Contains(t, font.Name.Val, "Times New Roman", "Stored font should contain font name") + assert.Contains(t, *font.Name.Val, "Times New Roman", "Stored font should contain font name") assert.Equal(t, 2, styles.CellXfs.Count, "Should have 2 styles") } diff --git a/table.go b/table.go index d26f8fd..c5a704c 100644 --- a/table.go +++ b/table.go @@ -77,8 +77,7 @@ func (f *File) AddTable(sheet, hcell, vcell, format string) error { sheetRelationshipsTableXML := "../tables/table" + strconv.Itoa(tableID) + ".xml" tableXML := strings.Replace(sheetRelationshipsTableXML, "..", "xl", -1) // Add first table for given sheet. - sheetPath, _ := f.sheetMap[trimSheetName(sheet)] - sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetPath, "xl/worksheets/") + ".rels" + sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(f.sheetMap[trimSheetName(sheet)], "xl/worksheets/") + ".rels" rID := f.addRels(sheetRels, SourceRelationshipTable, sheetRelationshipsTableXML, "") f.addSheetTable(sheet, rID) err = f.addTable(sheet, tableXML, hcol, hrow, vcol, vrow, tableID, formatSet) diff --git a/xmlChart.go b/xmlChart.go index a02da2a..84c1a3b 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -141,25 +141,25 @@ type aSchemeClr struct { // attrValInt directly maps the val element with integer data type as an // attribute。 type attrValInt struct { - Val int `xml:"val,attr"` + Val *int `xml:"val,attr"` } // attrValFloat directly maps the val element with float64 data type as an // attribute。 type attrValFloat struct { - Val float64 `xml:"val,attr"` + Val *float64 `xml:"val,attr"` } // attrValBool directly maps the val element with boolean data type as an // attribute。 type attrValBool struct { - Val bool `xml:"val,attr"` + Val *bool `xml:"val,attr"` } // attrValString directly maps the val element with string data type as an // attribute。 type attrValString struct { - Val string `xml:"val,attr"` + Val *string `xml:"val,attr"` } // aCs directly maps the a:cs element. diff --git a/xmlPivotTable.go b/xmlPivotTable.go index 0549c5e..6e1dfb8 100644 --- a/xmlPivotTable.go +++ b/xmlPivotTable.go @@ -187,7 +187,7 @@ type xlsxItem struct { F bool `xml:"f,attr,omitempty"` M bool `xml:"m,attr,omitempty"` C bool `xml:"c,attr,omitempty"` - X int `xml:"x,attr,omitempty,omitempty"` + X int `xml:"x,attr,omitempty"` D bool `xml:"d,attr,omitempty"` E bool `xml:"e,attr,omitempty"` } diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 8f39adf..57fd43f 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -27,7 +27,7 @@ type xlsxWorksheet struct { ProtectedRanges *xlsxInnerXML `xml:"protectedRanges"` Scenarios *xlsxInnerXML `xml:"scenarios"` AutoFilter *xlsxAutoFilter `xml:"autoFilter"` - SortState *xlsxInnerXML `xml:"sortState"` + SortState *xlsxSortState `xml:"sortState"` DataConsolidate *xlsxInnerXML `xml:"dataConsolidate"` CustomSheetViews *xlsxCustomSheetViews `xml:"customSheetViews"` MergeCells *xlsxMergeCells `xml:"mergeCells"` @@ -47,7 +47,7 @@ type xlsxWorksheet struct { SmartTags *xlsxInnerXML `xml:"smartTags"` Drawing *xlsxDrawing `xml:"drawing"` LegacyDrawing *xlsxLegacyDrawing `xml:"legacyDrawing"` - LegacyDrawingHF *xlsxInnerXML `xml:"legacyDrawingHF"` + LegacyDrawingHF *xlsxLegacyDrawingHF `xml:"legacyDrawingHF"` DrawingHF *xlsxDrawingHF `xml:"drawingHF"` Picture *xlsxPicture `xml:"picture"` OleObjects *xlsxInnerXML `xml:"oleObjects"` @@ -328,6 +328,16 @@ type xlsxRow struct { C []xlsxC `xml:"c"` } +// xlsxSortState directly maps the sortState element. This collection +// preserves the AutoFilter sort state. +type xlsxSortState struct { + ColumnSort bool `xml:"columnSort,attr,omitempty"` + CaseSensitive bool `xml:"caseSensitive,attr,omitempty"` + SortMethod string `xml:"sortMethod,attr,omitempty"` + Ref string `xml:"ref,attr"` + Content string `xml:",innerxml"` +} + // xlsxCustomSheetViews directly maps the customSheetViews element. This is a // collection of custom sheet views. type xlsxCustomSheetViews struct { @@ -424,7 +434,7 @@ type DataValidation struct { ShowErrorMessage bool `xml:"showErrorMessage,attr,omitempty"` ShowInputMessage bool `xml:"showInputMessage,attr,omitempty"` Sqref string `xml:"sqref,attr"` - Type string `xml:"type,attr"` + Type string `xml:"type,attr,omitempty"` Formula1 string `xml:",innerxml"` Formula2 string `xml:",innerxml"` } @@ -448,7 +458,7 @@ type DataValidation struct { type xlsxC struct { XMLName xml.Name `xml:"c"` XMLSpace xml.Attr `xml:"space,attr,omitempty"` - R string `xml:"r,attr"` // Cell ID, e.g. A1 + R string `xml:"r,attr,omitempty"` // Cell ID, e.g. A1 S int `xml:"s,attr,omitempty"` // Style reference. // Str string `xml:"str,attr,omitempty"` // Style reference. T string `xml:"t,attr,omitempty"` // Type. @@ -665,6 +675,14 @@ type xlsxLegacyDrawing struct { RID string `xml:"http://schemas.openxmlformats.org/officeDocument/2006/relationships id,attr,omitempty"` } +// xlsxLegacyDrawingHF specifies the explicit relationship to the part +// containing the VML defining pictures rendered in the header / footer of the +// sheet. +type xlsxLegacyDrawingHF struct { + XMLName xml.Name `xml:"legacyDrawingHF"` + RID string `xml:"http://schemas.openxmlformats.org/officeDocument/2006/relationships id,attr,omitempty"` +} + type xlsxInnerXML struct { Content string `xml:",innerxml"` } -- cgit v1.2.1 From 1666d04559d9f5b579ab7c850ccc95863c31bd25 Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 24 Dec 2019 01:09:28 +0800 Subject: optimization: checking error in unit tests --- .travis.yml | 1 + cell_test.go | 18 +++-- cellmerged_test.go | 22 +++--- chart_test.go | 21 ++---- col_test.go | 53 +++++++++----- datavalidation_test.go | 46 +++++------- excelize.go | 2 +- excelize_test.go | 186 +++++++++++++++++++++++++++---------------------- file_test.go | 4 +- picture_test.go | 35 +++------- pivotTable_test.go | 12 ++-- rows_test.go | 47 ++++++------- sheet_test.go | 2 +- sparkline_test.go | 60 ++++++++-------- stream.go | 2 +- table.go | 2 +- 16 files changed, 267 insertions(+), 246 deletions(-) diff --git a/.travis.yml b/.travis.yml index faf9916..5012f86 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,6 +7,7 @@ go: - 1.10.x - 1.11.x - 1.12.x + - 1.13.x os: - linux diff --git a/cell_test.go b/cell_test.go index 7d3339f..1efbc5a 100644 --- a/cell_test.go +++ b/cell_test.go @@ -54,8 +54,8 @@ func TestSetCellFloat(t *testing.T) { sheet := "Sheet1" t.Run("with no decimal", func(t *testing.T) { f := NewFile() - f.SetCellFloat(sheet, "A1", 123.0, -1, 64) - f.SetCellFloat(sheet, "A2", 123.0, 1, 64) + assert.NoError(t, f.SetCellFloat(sheet, "A1", 123.0, -1, 64)) + assert.NoError(t, f.SetCellFloat(sheet, "A2", 123.0, 1, 64)) val, err := f.GetCellValue(sheet, "A1") assert.NoError(t, err) assert.Equal(t, "123", val, "A1 should be 123") @@ -66,7 +66,7 @@ func TestSetCellFloat(t *testing.T) { t.Run("with a decimal and precision limit", func(t *testing.T) { f := NewFile() - f.SetCellFloat(sheet, "A1", 123.42, 1, 64) + assert.NoError(t, f.SetCellFloat(sheet, "A1", 123.42, 1, 64)) val, err := f.GetCellValue(sheet, "A1") assert.NoError(t, err) assert.Equal(t, "123.4", val, "A1 should be 123.4") @@ -74,7 +74,7 @@ func TestSetCellFloat(t *testing.T) { t.Run("with a decimal and no limit", func(t *testing.T) { f := NewFile() - f.SetCellFloat(sheet, "A1", 123.42, -1, 64) + assert.NoError(t, f.SetCellFloat(sheet, "A1", 123.42, -1, 64)) val, err := f.GetCellValue(sheet, "A1") assert.NoError(t, err) assert.Equal(t, "123.42", val, "A1 should be 123.42") @@ -101,7 +101,7 @@ func TestGetCellFormula(t *testing.T) { assert.EqualError(t, err, "sheet SheetN is not exist") // Test get cell formula on no formula cell. - f.SetCellValue("Sheet1", "A1", true) + assert.NoError(t, f.SetCellValue("Sheet1", "A1", true)) _, err = f.GetCellFormula("Sheet1", "A1") assert.NoError(t, err) } @@ -109,7 +109,9 @@ func TestGetCellFormula(t *testing.T) { func ExampleFile_SetCellFloat() { f := NewFile() var x = 3.14159265 - f.SetCellFloat("Sheet1", "A1", x, 2, 64) + if err := f.SetCellFloat("Sheet1", "A1", x, 2, 64); err != nil { + fmt.Println(err) + } val, _ := f.GetCellValue("Sheet1", "A1") fmt.Println(val) // Output: 3.14 @@ -122,7 +124,9 @@ func BenchmarkSetCellValue(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { for j := 0; j < len(values); j++ { - f.SetCellValue("Sheet1", fmt.Sprint(cols[j], i), values[j]) + if err := f.SetCellValue("Sheet1", fmt.Sprint(cols[j], i), values[j]); err != nil { + b.Error(err) + } } } } diff --git a/cellmerged_test.go b/cellmerged_test.go index 1da0eb3..e880d05 100644 --- a/cellmerged_test.go +++ b/cellmerged_test.go @@ -21,14 +21,20 @@ func TestMergeCell(t *testing.T) { assert.NoError(t, f.MergeCell("Sheet1", "H7", "B15")) assert.NoError(t, f.MergeCell("Sheet1", "D11", "F13")) assert.NoError(t, f.MergeCell("Sheet1", "G10", "K12")) - f.SetCellValue("Sheet1", "G11", "set value in merged cell") - f.SetCellInt("Sheet1", "H11", 100) - f.SetCellValue("Sheet1", "I11", float64(0.5)) - f.SetCellHyperLink("Sheet1", "J11", "https://github.com/360EntSecGroup-Skylar/excelize", "External") - f.SetCellFormula("Sheet1", "G12", "SUM(Sheet1!B19,Sheet1!C19)") - f.GetCellValue("Sheet1", "H11") - f.GetCellValue("Sheet2", "A6") // Merged cell ref is single coordinate. - f.GetCellFormula("Sheet1", "G12") + assert.NoError(t, f.SetCellValue("Sheet1", "G11", "set value in merged cell")) + assert.NoError(t, f.SetCellInt("Sheet1", "H11", 100)) + assert.NoError(t, f.SetCellValue("Sheet1", "I11", float64(0.5))) + assert.NoError(t, f.SetCellHyperLink("Sheet1", "J11", "https://github.com/360EntSecGroup-Skylar/excelize", "External")) + assert.NoError(t, f.SetCellFormula("Sheet1", "G12", "SUM(Sheet1!B19,Sheet1!C19)")) + value, err := f.GetCellValue("Sheet1", "H11") + assert.Equal(t, "0.5", value) + assert.NoError(t, err) + value, err = f.GetCellValue("Sheet2", "A6") // Merged cell ref is single coordinate. + assert.Equal(t, "", value) + assert.NoError(t, err) + value, err = f.GetCellFormula("Sheet1", "G12") + assert.Equal(t, "SUM(Sheet1!B19,Sheet1!C19)", value) + assert.NoError(t, err) f.NewSheet("Sheet3") assert.NoError(t, f.MergeCell("Sheet3", "D11", "F13")) diff --git a/chart_test.go b/chart_test.go index bc5c30a..2379ddc 100644 --- a/chart_test.go +++ b/chart_test.go @@ -22,7 +22,7 @@ func TestChartSize(t *testing.T) { "D1": "Pear", } for cell, v := range categories { - xlsx.SetCellValue(sheet1, cell, v) + assert.NoError(t, xlsx.SetCellValue(sheet1, cell, v)) } values := map[string]int{ @@ -37,29 +37,22 @@ func TestChartSize(t *testing.T) { "D4": 8, } for cell, v := range values { - xlsx.SetCellValue(sheet1, cell, v) + assert.NoError(t, xlsx.SetCellValue(sheet1, cell, v)) } - xlsx.AddChart("Sheet1", "E4", `{"type":"col3DClustered","dimension":{"width":640, "height":480},`+ + assert.NoError(t, 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":"3D Clustered Column Chart"}}`) + `"title":{"name":"3D Clustered Column Chart"}}`)) - var ( - buffer bytes.Buffer - ) + var buffer bytes.Buffer // Save xlsx file by the given path. - err := xlsx.Write(&buffer) - if !assert.NoError(t, err) { - t.FailNow() - } + assert.NoError(t, xlsx.Write(&buffer)) newFile, err := OpenReader(&buffer) - if !assert.NoError(t, err) { - t.FailNow() - } + assert.NoError(t, err) chartsNum := newFile.countCharts() if !assert.Equal(t, 1, chartsNum, "Expected 1 chart, actual %d", chartsNum) { diff --git a/col_test.go b/col_test.go index edbdae7..cdb7edf 100644 --- a/col_test.go +++ b/col_test.go @@ -37,17 +37,29 @@ func TestColumnVisibility(t *testing.T) { t.Run("TestBook3", func(t *testing.T) { f, err := prepareTestBook3() assert.NoError(t, err) - f.GetColVisible("Sheet1", "B") + visible, err := f.GetColVisible("Sheet1", "B") + assert.Equal(t, true, visible) + assert.NoError(t, err) }) } func TestOutlineLevel(t *testing.T) { f := NewFile() - f.GetColOutlineLevel("Sheet1", "D") + level, err := f.GetColOutlineLevel("Sheet1", "D") + assert.Equal(t, uint8(0), level) + assert.NoError(t, err) + f.NewSheet("Sheet2") assert.NoError(t, f.SetColOutlineLevel("Sheet1", "D", 4)) - f.GetColOutlineLevel("Sheet1", "D") - f.GetColOutlineLevel("Shee2", "A") + + level, err = f.GetColOutlineLevel("Sheet1", "D") + assert.Equal(t, uint8(4), level) + assert.NoError(t, err) + + level, err = f.GetColOutlineLevel("Shee2", "A") + assert.Equal(t, uint8(0), level) + assert.EqualError(t, err, "sheet Shee2 is not exist") + assert.NoError(t, f.SetColWidth("Sheet2", "A", "D", 13)) assert.NoError(t, f.SetColOutlineLevel("Sheet2", "B", 2)) assert.NoError(t, f.SetRowOutlineLevel("Sheet1", 2, 7)) @@ -56,7 +68,7 @@ func TestOutlineLevel(t *testing.T) { // Test set row outline level on not exists worksheet. assert.EqualError(t, f.SetRowOutlineLevel("SheetN", 1, 4), "sheet SheetN is not exist") // Test get row outline level on not exists worksheet. - _, err := f.GetRowOutlineLevel("SheetN", 1) + _, err = f.GetRowOutlineLevel("SheetN", 1) assert.EqualError(t, err, "sheet SheetN is not exist") // Test set and get column outline level with illegal cell coordinates. @@ -68,7 +80,7 @@ func TestOutlineLevel(t *testing.T) { assert.EqualError(t, f.SetColOutlineLevel("SheetN", "E", 2), "sheet SheetN is not exist") assert.EqualError(t, f.SetRowOutlineLevel("Sheet1", 0, 1), "invalid row number 0") - level, err := f.GetRowOutlineLevel("Sheet1", 2) + level, err = f.GetRowOutlineLevel("Sheet1", 2) assert.NoError(t, err) assert.Equal(t, uint8(7), level) @@ -83,7 +95,7 @@ func TestOutlineLevel(t *testing.T) { f, err = OpenFile(filepath.Join("test", "Book1.xlsx")) assert.NoError(t, err) - f.SetColOutlineLevel("Sheet2", "B", 2) + assert.NoError(t, f.SetColOutlineLevel("Sheet2", "B", 2)) } func TestSetColStyle(t *testing.T) { @@ -105,13 +117,18 @@ func TestSetColStyle(t *testing.T) { func TestColWidth(t *testing.T) { f := NewFile() - f.SetColWidth("Sheet1", "B", "A", 12) - f.SetColWidth("Sheet1", "A", "B", 12) - f.GetColWidth("Sheet1", "A") - f.GetColWidth("Sheet1", "C") + assert.NoError(t, f.SetColWidth("Sheet1", "B", "A", 12)) + assert.NoError(t, f.SetColWidth("Sheet1", "A", "B", 12)) + width, err := f.GetColWidth("Sheet1", "A") + assert.Equal(t, float64(12), width) + assert.NoError(t, err) + width, err = f.GetColWidth("Sheet1", "C") + assert.Equal(t, float64(64), width) + assert.NoError(t, err) // Test set and get column width with illegal cell coordinates. - _, err := f.GetColWidth("Sheet1", "*") + width, err = f.GetColWidth("Sheet1", "*") + assert.Equal(t, float64(64), width) assert.EqualError(t, err, `invalid column name "*"`) assert.EqualError(t, f.SetColWidth("Sheet1", "*", "B", 1), `invalid column name "*"`) assert.EqualError(t, f.SetColWidth("Sheet1", "A", "*", 1), `invalid column name "*"`) @@ -133,8 +150,8 @@ func TestInsertCol(t *testing.T) { fillCells(f, sheet1, 10, 10) - f.SetCellHyperLink(sheet1, "A5", "https://github.com/360EntSecGroup-Skylar/excelize", "External") - f.MergeCell(sheet1, "A1", "C3") + assert.NoError(t, f.SetCellHyperLink(sheet1, "A5", "https://github.com/360EntSecGroup-Skylar/excelize", "External")) + assert.NoError(t, f.MergeCell(sheet1, "A1", "C3")) assert.NoError(t, f.AutoFilter(sheet1, "A2", "B2", `{"column":"B","expression":"x != blanks"}`)) assert.NoError(t, f.InsertCol(sheet1, "A")) @@ -151,11 +168,11 @@ func TestRemoveCol(t *testing.T) { fillCells(f, sheet1, 10, 15) - f.SetCellHyperLink(sheet1, "A5", "https://github.com/360EntSecGroup-Skylar/excelize", "External") - f.SetCellHyperLink(sheet1, "C5", "https://github.com", "External") + assert.NoError(t, f.SetCellHyperLink(sheet1, "A5", "https://github.com/360EntSecGroup-Skylar/excelize", "External")) + assert.NoError(t, f.SetCellHyperLink(sheet1, "C5", "https://github.com", "External")) - f.MergeCell(sheet1, "A1", "B1") - f.MergeCell(sheet1, "A2", "B2") + assert.NoError(t, f.MergeCell(sheet1, "A1", "B1")) + assert.NoError(t, f.MergeCell(sheet1, "A2", "B2")) assert.NoError(t, f.RemoveCol(sheet1, "A")) assert.NoError(t, f.RemoveCol(sheet1, "A")) diff --git a/datavalidation_test.go b/datavalidation_test.go index 763bad1..7e54d55 100644 --- a/datavalidation_test.go +++ b/datavalidation_test.go @@ -24,53 +24,45 @@ func TestDataValidation(t *testing.T) { dvRange := NewDataValidation(true) dvRange.Sqref = "A1:B2" - dvRange.SetRange(10, 20, DataValidationTypeWhole, DataValidationOperatorBetween) + assert.NoError(t, dvRange.SetRange(10, 20, DataValidationTypeWhole, DataValidationOperatorBetween)) dvRange.SetError(DataValidationErrorStyleStop, "error title", "error body") dvRange.SetError(DataValidationErrorStyleWarning, "error title", "error body") dvRange.SetError(DataValidationErrorStyleInformation, "error title", "error body") - f.AddDataValidation("Sheet1", dvRange) - if !assert.NoError(t, f.SaveAs(resultFile)) { - t.FailNow() - } + assert.NoError(t, f.AddDataValidation("Sheet1", dvRange)) + assert.NoError(t, f.SaveAs(resultFile)) dvRange = NewDataValidation(true) dvRange.Sqref = "A3:B4" - dvRange.SetRange(10, 20, DataValidationTypeWhole, DataValidationOperatorGreaterThan) + assert.NoError(t, dvRange.SetRange(10, 20, DataValidationTypeWhole, DataValidationOperatorGreaterThan)) dvRange.SetInput("input title", "input body") - f.AddDataValidation("Sheet1", dvRange) - if !assert.NoError(t, f.SaveAs(resultFile)) { - t.FailNow() - } + assert.NoError(t, f.AddDataValidation("Sheet1", dvRange)) + assert.NoError(t, f.SaveAs(resultFile)) dvRange = NewDataValidation(true) dvRange.Sqref = "A5:B6" - dvRange.SetDropList([]string{"1", "2", "3"}) - f.AddDataValidation("Sheet1", dvRange) - if !assert.NoError(t, f.SaveAs(resultFile)) { - t.FailNow() - } + assert.NoError(t, dvRange.SetDropList([]string{"1", "2", "3"})) + assert.NoError(t, f.AddDataValidation("Sheet1", dvRange)) + assert.NoError(t, f.SaveAs(resultFile)) } func TestDataValidationError(t *testing.T) { resultFile := filepath.Join("test", "TestDataValidationError.xlsx") f := NewFile() - f.SetCellStr("Sheet1", "E1", "E1") - f.SetCellStr("Sheet1", "E2", "E2") - f.SetCellStr("Sheet1", "E3", "E3") + assert.NoError(t, f.SetCellStr("Sheet1", "E1", "E1")) + assert.NoError(t, f.SetCellStr("Sheet1", "E2", "E2")) + assert.NoError(t, f.SetCellStr("Sheet1", "E3", "E3")) dvRange := NewDataValidation(true) dvRange.SetSqref("A7:B8") dvRange.SetSqref("A7:B8") - dvRange.SetSqrefDropList("$E$1:$E$3", true) + assert.NoError(t, dvRange.SetSqrefDropList("$E$1:$E$3", true)) err := dvRange.SetSqrefDropList("$E$1:$E$3", false) assert.EqualError(t, err, "cross-sheet sqref cell are not supported") - f.AddDataValidation("Sheet1", dvRange) - if !assert.NoError(t, f.SaveAs(resultFile)) { - t.FailNow() - } + assert.NoError(t, f.AddDataValidation("Sheet1", dvRange)) + assert.NoError(t, f.SaveAs(resultFile)) dvRange = NewDataValidation(true) err = dvRange.SetDropList(make([]string, 258)) @@ -79,13 +71,11 @@ func TestDataValidationError(t *testing.T) { return } assert.EqualError(t, err, "data validation must be 0-255 characters") - dvRange.SetRange(10, 20, DataValidationTypeWhole, DataValidationOperatorGreaterThan) + assert.NoError(t, dvRange.SetRange(10, 20, DataValidationTypeWhole, DataValidationOperatorGreaterThan)) dvRange.SetSqref("A9:B10") - f.AddDataValidation("Sheet1", dvRange) - if !assert.NoError(t, f.SaveAs(resultFile)) { - t.FailNow() - } + assert.NoError(t, f.AddDataValidation("Sheet1", dvRange)) + assert.NoError(t, f.SaveAs(resultFile)) // Test width invalid data validation formula. dvRange.Formula1 = strings.Repeat("s", dataValidationFormulaStrLen+22) diff --git a/excelize.go b/excelize.go index 135028c..94f401c 100644 --- a/excelize.go +++ b/excelize.go @@ -138,7 +138,7 @@ func (f *File) setDefaultTimeStyle(sheet, axis string, format int) error { } if s == 0 { style, _ := f.NewStyle(`{"number_format": ` + strconv.Itoa(format) + `}`) - f.SetCellStyle(sheet, axis, axis, style) + _ = f.SetCellStyle(sheet, axis, axis, style) } return err } diff --git a/excelize_test.go b/excelize_test.go index 1d6ed24..c4b600d 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -24,12 +24,11 @@ import ( func TestOpenFile(t *testing.T) { // Test update a XLSX file. f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) - if !assert.NoError(t, err) { - t.FailNow() - } + assert.NoError(t, err) // Test get all the rows in a not exists worksheet. - f.GetRows("Sheet4") + _, err = f.GetRows("Sheet4") + assert.EqualError(t, err, "sheet Sheet4 is not exist") // Test get all the rows in a worksheet. rows, err := f.GetRows("Sheet2") assert.NoError(t, err) @@ -39,10 +38,10 @@ func TestOpenFile(t *testing.T) { } t.Log("\r\n") } - f.UpdateLinkedValue() + assert.NoError(t, f.UpdateLinkedValue()) - f.SetCellDefault("Sheet2", "A1", strconv.FormatFloat(float64(100.1588), 'f', -1, 32)) - f.SetCellDefault("Sheet2", "A1", strconv.FormatFloat(float64(-100.1588), 'f', -1, 64)) + assert.NoError(t, f.SetCellDefault("Sheet2", "A1", strconv.FormatFloat(float64(100.1588), 'f', -1, 32))) + assert.NoError(t, f.SetCellDefault("Sheet2", "A1", strconv.FormatFloat(float64(-100.1588), 'f', -1, 64))) // Test set cell value with illegal row number. assert.EqualError(t, f.SetCellDefault("Sheet2", "A", strconv.FormatFloat(float64(-100.1588), 'f', -1, 64)), @@ -53,14 +52,14 @@ func TestOpenFile(t *testing.T) { // Test set cell integer value with illegal row number. assert.EqualError(t, f.SetCellInt("Sheet2", "A", 100), `cannot convert cell "A" to coordinates: invalid cell name "A"`) - f.SetCellStr("Sheet2", "C11", "Knowns") + assert.NoError(t, f.SetCellStr("Sheet2", "C11", "Knowns")) // Test max characters in a cell. - f.SetCellStr("Sheet2", "D11", strings.Repeat("c", 32769)) + assert.NoError(t, f.SetCellStr("Sheet2", "D11", strings.Repeat("c", 32769))) f.NewSheet(":\\/?*[]Maximum 31 characters allowed in sheet title.") // Test set worksheet name with illegal name. f.SetSheetName("Maximum 31 characters allowed i", "[Rename]:\\/?* Maximum 31 characters allowed in sheet title.") - f.SetCellInt("Sheet3", "A23", 10) - f.SetCellStr("Sheet3", "b230", "10") + assert.EqualError(t, f.SetCellInt("Sheet3", "A23", 10), "sheet Sheet3 is not exist") + assert.EqualError(t, f.SetCellStr("Sheet3", "b230", "10"), "sheet Sheet3 is not exist") assert.EqualError(t, f.SetCellStr("Sheet10", "b230", "10"), "sheet Sheet10 is not exist") // Test set cell string value with illegal row number. @@ -137,21 +136,21 @@ func TestOpenFile(t *testing.T) { {true, "1"}, } for _, test := range booltest { - f.SetCellValue("Sheet2", "F16", test.value) + assert.NoError(t, f.SetCellValue("Sheet2", "F16", test.value)) val, err := f.GetCellValue("Sheet2", "F16") assert.NoError(t, err) assert.Equal(t, test.expected, val) } - f.SetCellValue("Sheet2", "G2", nil) + assert.NoError(t, f.SetCellValue("Sheet2", "G2", nil)) assert.EqualError(t, f.SetCellValue("Sheet2", "G4", time.Now()), "only UTC time expected") - f.SetCellValue("Sheet2", "G4", time.Now().UTC()) + assert.NoError(t, f.SetCellValue("Sheet2", "G4", time.Now().UTC())) // 02:46:40 - f.SetCellValue("Sheet2", "G5", time.Duration(1e13)) + assert.NoError(t, f.SetCellValue("Sheet2", "G5", time.Duration(1e13))) // Test completion column. - f.SetCellValue("Sheet2", "M2", nil) + assert.NoError(t, f.SetCellValue("Sheet2", "M2", nil)) // Test read cell value with given axis large than exists row. _, err = f.GetCellValue("Sheet2", "E231") assert.NoError(t, err) @@ -161,10 +160,10 @@ func TestOpenFile(t *testing.T) { f.GetSheetIndex("Sheet1") // Test get worksheet name of XLSX by given invalid worksheet index. f.GetSheetName(4) - // Test get worksheet map of f. + // Test get worksheet map of workbook. f.GetSheetMap() for i := 1; i <= 300; i++ { - f.SetCellStr("Sheet3", "c"+strconv.Itoa(i), strconv.Itoa(i)) + assert.NoError(t, f.SetCellStr("Sheet2", "c"+strconv.Itoa(i), strconv.Itoa(i))) } assert.NoError(t, f.SaveAs(filepath.Join("test", "TestOpenFile.xlsx"))) } @@ -259,8 +258,8 @@ func TestNewFile(t *testing.T) { f.NewSheet("Sheet1") f.NewSheet("XLSXSheet2") f.NewSheet("XLSXSheet3") - f.SetCellInt("XLSXSheet2", "A23", 56) - f.SetCellStr("Sheet1", "B20", "42") + assert.NoError(t, f.SetCellInt("XLSXSheet2", "A23", 56)) + assert.NoError(t, f.SetCellStr("Sheet1", "B20", "42")) f.SetActiveSheet(0) // Test add picture to sheet with scaling and positioning. @@ -310,12 +309,14 @@ func TestSetCellHyperLink(t *testing.T) { assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellHyperLink.xlsx"))) f = NewFile() - f.workSheetReader("Sheet1") + _, err = f.workSheetReader("Sheet1") + assert.NoError(t, err) f.Sheet["xl/worksheets/sheet1.xml"].Hyperlinks = &xlsxHyperlinks{Hyperlink: make([]xlsxHyperlink, 65530)} assert.EqualError(t, f.SetCellHyperLink("Sheet1", "A65531", "https://github.com/360EntSecGroup-Skylar/excelize", "External"), "over maximum limit hyperlinks in a worksheet") f = NewFile() - f.workSheetReader("Sheet1") + _, err = f.workSheetReader("Sheet1") + assert.NoError(t, err) f.Sheet["xl/worksheets/sheet1.xml"].MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A:A"}}} err = f.SetCellHyperLink("Sheet1", "A1", "https://github.com/360EntSecGroup-Skylar/excelize", "External") assert.EqualError(t, err, `cannot convert cell "A" to coordinates: invalid cell name "A"`) @@ -341,7 +342,8 @@ func TestGetCellHyperLink(t *testing.T) { t.Log(link, target) f = NewFile() - f.workSheetReader("Sheet1") + _, err = f.workSheetReader("Sheet1") + assert.NoError(t, err) f.Sheet["xl/worksheets/sheet1.xml"].Hyperlinks = &xlsxHyperlinks{ Hyperlink: []xlsxHyperlink{{Ref: "A1"}}, } @@ -364,8 +366,8 @@ func TestSetCellFormula(t *testing.T) { t.FailNow() } - f.SetCellFormula("Sheet1", "B19", "SUM(Sheet2!D2,Sheet2!D11)") - f.SetCellFormula("Sheet1", "C19", "SUM(Sheet2!D2,Sheet2!D9)") + assert.NoError(t, f.SetCellFormula("Sheet1", "B19", "SUM(Sheet2!D2,Sheet2!D11)")) + assert.NoError(t, f.SetCellFormula("Sheet1", "C19", "SUM(Sheet2!D2,Sheet2!D9)")) // Test set cell formula with illegal rows number. assert.EqualError(t, f.SetCellFormula("Sheet1", "C", "SUM(Sheet2!D2,Sheet2!D9)"), `cannot convert cell "C" to coordinates: invalid cell name "C"`) @@ -377,10 +379,10 @@ func TestSetCellFormula(t *testing.T) { t.FailNow() } // Test remove cell formula. - f.SetCellFormula("Sheet1", "A1", "") + assert.NoError(t, f.SetCellFormula("Sheet1", "A1", "")) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellFormula2.xlsx"))) // Test remove all cell formula. - f.SetCellFormula("Sheet1", "B1", "") + assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "")) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellFormula3.xlsx"))) } @@ -471,31 +473,31 @@ func TestWriteArrayFormula(t *testing.T) { } // Line 2 contains the results of AVERAGEIF - f.SetCellStr("Sheet1", "A2", "Average") + assert.NoError(t, f.SetCellStr("Sheet1", "A2", "Average")) // Line 3 contains the average that was calculated in Go - f.SetCellStr("Sheet1", "A3", "Average (calculated)") + assert.NoError(t, f.SetCellStr("Sheet1", "A3", "Average (calculated)")) // Line 4 contains the results of the array function that calculates the standard deviation - f.SetCellStr("Sheet1", "A4", "Std. deviation") + assert.NoError(t, f.SetCellStr("Sheet1", "A4", "Std. deviation")) // Line 5 contains the standard deviations calculated in Go - f.SetCellStr("Sheet1", "A5", "Std. deviation (calculated)") + assert.NoError(t, f.SetCellStr("Sheet1", "A5", "Std. deviation (calculated)")) - f.SetCellStr("Sheet1", "B1", sample[0]) - f.SetCellStr("Sheet1", "C1", sample[1]) - f.SetCellStr("Sheet1", "D1", sample[2]) + assert.NoError(t, f.SetCellStr("Sheet1", "B1", sample[0])) + assert.NoError(t, f.SetCellStr("Sheet1", "C1", sample[1])) + assert.NoError(t, f.SetCellStr("Sheet1", "D1", sample[2])) firstResLine := 8 - f.SetCellStr("Sheet1", cell(1, firstResLine-1), "Result Values") - f.SetCellStr("Sheet1", cell(2, firstResLine-1), "Sample") + assert.NoError(t, f.SetCellStr("Sheet1", cell(1, firstResLine-1), "Result Values")) + assert.NoError(t, f.SetCellStr("Sheet1", cell(2, firstResLine-1), "Sample")) for i := 0; i != len(values); i++ { valCell := cell(1, i+firstResLine) assocCell := cell(2, i+firstResLine) - f.SetCellInt("Sheet1", valCell, values[i]) - f.SetCellStr("Sheet1", assocCell, sample[assoc[i]]) + assert.NoError(t, f.SetCellInt("Sheet1", valCell, values[i])) + assert.NoError(t, f.SetCellStr("Sheet1", assocCell, sample[assoc[i]])) } valRange := fmt.Sprintf("$A$%d:$A$%d", firstResLine, len(values)+firstResLine-1) @@ -508,11 +510,11 @@ func TestWriteArrayFormula(t *testing.T) { stdevCell := cell(i+2, 4) calcStdevCell := cell(i+2, 5) - f.SetCellInt("Sheet1", calcAvgCell, average(i)) - f.SetCellInt("Sheet1", calcStdevCell, stdev(i)) + assert.NoError(t, f.SetCellInt("Sheet1", calcAvgCell, average(i))) + assert.NoError(t, f.SetCellInt("Sheet1", calcStdevCell, stdev(i))) // Average can be done with AVERAGEIF - f.SetCellFormula("Sheet1", avgCell, fmt.Sprintf("ROUND(AVERAGEIF(%s,%s,%s),0)", assocRange, nameCell, valRange)) + assert.NoError(t, f.SetCellFormula("Sheet1", avgCell, fmt.Sprintf("ROUND(AVERAGEIF(%s,%s,%s),0)", assocRange, nameCell, valRange))) ref := stdevCell + ":" + stdevCell t := STCellFormulaTypeArray @@ -623,9 +625,9 @@ func TestSetCellStyleNumberFormat(t *testing.T) { var val float64 val, err = strconv.ParseFloat(v, 64) if err != nil { - f.SetCellValue("Sheet2", c, v) + assert.NoError(t, f.SetCellValue("Sheet2", c, v)) } else { - f.SetCellValue("Sheet2", c, val) + assert.NoError(t, f.SetCellValue("Sheet2", c, val)) } style, err := f.NewStyle(`{"fill":{"type":"gradient","color":["#FFFFFF","#E0EBF5"],"shading":5},"number_format": ` + strconv.Itoa(d) + `}`) if !assert.NoError(t, err) { @@ -652,8 +654,8 @@ func TestSetCellStyleCurrencyNumberFormat(t *testing.T) { t.FailNow() } - f.SetCellValue("Sheet1", "A1", 56) - f.SetCellValue("Sheet1", "A2", -32.3) + assert.NoError(t, f.SetCellValue("Sheet1", "A1", 56)) + assert.NoError(t, f.SetCellValue("Sheet1", "A2", -32.3)) var style int style, err = f.NewStyle(`{"number_format": 188, "decimal_places": -1}`) if !assert.NoError(t, err) { @@ -676,8 +678,8 @@ func TestSetCellStyleCurrencyNumberFormat(t *testing.T) { if !assert.NoError(t, err) { t.FailNow() } - f.SetCellValue("Sheet1", "A1", 42920.5) - f.SetCellValue("Sheet1", "A2", 42920.5) + assert.NoError(t, f.SetCellValue("Sheet1", "A1", 42920.5)) + assert.NoError(t, f.SetCellValue("Sheet1", "A2", 42920.5)) _, err = f.NewStyle(`{"number_format": 26, "lang": "zh-tw"}`) if !assert.NoError(t, err) { @@ -709,8 +711,8 @@ func TestSetCellStyleCurrencyNumberFormat(t *testing.T) { func TestSetCellStyleCustomNumberFormat(t *testing.T) { f := NewFile() - f.SetCellValue("Sheet1", "A1", 42920.5) - f.SetCellValue("Sheet1", "A2", 42920.5) + assert.NoError(t, f.SetCellValue("Sheet1", "A1", 42920.5)) + assert.NoError(t, f.SetCellValue("Sheet1", "A2", 42920.5)) style, err := f.NewStyle(`{"custom_number_format": "[$-380A]dddd\\,\\ dd\" de \"mmmm\" de \"yyyy;@"}`) if err != nil { t.Log(err) @@ -841,8 +843,8 @@ func TestSetDeleteSheet(t *testing.T) { t.FailNow() } f.DeleteSheet("Sheet1") - f.AddComment("Sheet1", "A1", "") - f.AddComment("Sheet1", "A1", `{"author":"Excelize: ","text":"This is a comment."}`) + assert.EqualError(t, f.AddComment("Sheet1", "A1", ""), "unexpected end of JSON input") + assert.NoError(t, f.AddComment("Sheet1", "A1", `{"author":"Excelize: ","text":"This is a comment."}`)) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetDeleteSheet.TestBook4.xlsx"))) }) } @@ -853,10 +855,10 @@ func TestSheetVisibility(t *testing.T) { t.FailNow() } - f.SetSheetVisible("Sheet2", false) - f.SetSheetVisible("Sheet1", false) - f.SetSheetVisible("Sheet1", true) - f.GetSheetVisible("Sheet1") + assert.NoError(t, f.SetSheetVisible("Sheet2", false)) + assert.NoError(t, f.SetSheetVisible("Sheet1", false)) + assert.NoError(t, f.SetSheetVisible("Sheet1", true)) + assert.Equal(t, true, f.GetSheetVisible("Sheet1")) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSheetVisibility.xlsx"))) } @@ -870,7 +872,7 @@ func TestCopySheet(t *testing.T) { idx := f.NewSheet("CopySheet") assert.NoError(t, f.CopySheet(1, idx)) - f.SetCellValue("CopySheet", "F1", "Hello") + assert.NoError(t, f.SetCellValue("CopySheet", "F1", "Hello")) val, err := f.GetCellValue("Sheet1", "F1") assert.NoError(t, err) assert.NotEqual(t, "Hello", val) @@ -1072,31 +1074,31 @@ func TestConditionalFormat(t *testing.T) { } // Color scales: 2 color. - f.SetConditionalFormat(sheet1, "A1:A10", `[{"type":"2_color_scale","criteria":"=","min_type":"min","max_type":"max","min_color":"#F8696B","max_color":"#63BE7B"}]`) + assert.NoError(t, f.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. - f.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"}]`) + assert.NoError(t, f.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... - f.SetConditionalFormat(sheet1, "C1:C10", fmt.Sprintf(`[{"type":"cell","criteria":"between","format":%d,"minimum":"6","maximum":"8"}]`, format1)) + assert.NoError(t, f.SetConditionalFormat(sheet1, "C1:C10", fmt.Sprintf(`[{"type":"cell","criteria":"between","format":%d,"minimum":"6","maximum":"8"}]`, format1))) // Hightlight cells rules: Greater Than... - f.SetConditionalFormat(sheet1, "D1:D10", fmt.Sprintf(`[{"type":"cell","criteria":">","format":%d,"value":"6"}]`, format3)) + assert.NoError(t, f.SetConditionalFormat(sheet1, "D1:D10", fmt.Sprintf(`[{"type":"cell","criteria":">","format":%d,"value":"6"}]`, format3))) // Hightlight cells rules: Equal To... - f.SetConditionalFormat(sheet1, "E1:E10", fmt.Sprintf(`[{"type":"top","criteria":"=","format":%d}]`, format3)) + assert.NoError(t, f.SetConditionalFormat(sheet1, "E1:E10", fmt.Sprintf(`[{"type":"top","criteria":"=","format":%d}]`, format3))) // Hightlight cells rules: Not Equal To... - f.SetConditionalFormat(sheet1, "F1:F10", fmt.Sprintf(`[{"type":"unique","criteria":"=","format":%d}]`, format2)) + assert.NoError(t, f.SetConditionalFormat(sheet1, "F1:F10", fmt.Sprintf(`[{"type":"unique","criteria":"=","format":%d}]`, format2))) // Hightlight cells rules: Duplicate Values... - f.SetConditionalFormat(sheet1, "G1:G10", fmt.Sprintf(`[{"type":"duplicate","criteria":"=","format":%d}]`, format2)) + assert.NoError(t, f.SetConditionalFormat(sheet1, "G1:G10", fmt.Sprintf(`[{"type":"duplicate","criteria":"=","format":%d}]`, format2))) // Top/Bottom rules: Top 10%. - f.SetConditionalFormat(sheet1, "H1:H10", fmt.Sprintf(`[{"type":"top","criteria":"=","format":%d,"value":"6","percent":true}]`, format1)) + assert.NoError(t, f.SetConditionalFormat(sheet1, "H1:H10", fmt.Sprintf(`[{"type":"top","criteria":"=","format":%d,"value":"6","percent":true}]`, format1))) // Top/Bottom rules: Above Average... - f.SetConditionalFormat(sheet1, "I1:I10", fmt.Sprintf(`[{"type":"average","criteria":"=","format":%d, "above_average": true}]`, format3)) + assert.NoError(t, f.SetConditionalFormat(sheet1, "I1:I10", fmt.Sprintf(`[{"type":"average","criteria":"=","format":%d, "above_average": true}]`, format3))) // Top/Bottom rules: Below Average... - f.SetConditionalFormat(sheet1, "J1:J10", fmt.Sprintf(`[{"type":"average","criteria":"=","format":%d, "above_average": false}]`, format1)) + assert.NoError(t, f.SetConditionalFormat(sheet1, "J1:J10", fmt.Sprintf(`[{"type":"average","criteria":"=","format":%d, "above_average": false}]`, format1))) // Data Bars: Gradient Fill. - f.SetConditionalFormat(sheet1, "K1:K10", `[{"type":"data_bar", "criteria":"=", "min_type":"min","max_type":"max","bar_color":"#638EC6"}]`) + assert.NoError(t, f.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. - f.SetConditionalFormat(sheet1, "L1:L10", fmt.Sprintf(`[{"type":"formula", "criteria":"L2<3", "format":%d}]`, format1)) + assert.NoError(t, f.SetConditionalFormat(sheet1, "L1:L10", fmt.Sprintf(`[{"type":"formula", "criteria":"L2<3", "format":%d}]`, format1))) // Test set invalid format set in conditional format - f.SetConditionalFormat(sheet1, "L1:L10", "") + assert.EqualError(t, f.SetConditionalFormat(sheet1, "L1:L10", ""), "unexpected end of JSON input") err = f.SaveAs(filepath.Join("test", "TestConditionalFormat.xlsx")) if !assert.NoError(t, err) { @@ -1104,9 +1106,9 @@ func TestConditionalFormat(t *testing.T) { } // Set conditional format with illegal valid type. - f.SetConditionalFormat(sheet1, "K1:K10", `[{"type":"", "criteria":"=", "min_type":"min","max_type":"max","bar_color":"#638EC6"}]`) + assert.NoError(t, f.SetConditionalFormat(sheet1, "K1:K10", `[{"type":"", "criteria":"=", "min_type":"min","max_type":"max","bar_color":"#638EC6"}]`)) // Set conditional format with illegal criteria type. - f.SetConditionalFormat(sheet1, "K1:K10", `[{"type":"data_bar", "criteria":"", "min_type":"min","max_type":"max","bar_color":"#638EC6"}]`) + assert.NoError(t, f.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. f, err = OpenFile(filepath.Join("test", "Book1.xlsx")) @@ -1156,7 +1158,7 @@ func TestSetSheetRow(t *testing.T) { t.FailNow() } - f.SetSheetRow("Sheet1", "B27", &[]interface{}{"cell", nil, int32(42), float64(42), time.Now().UTC()}) + assert.NoError(t, f.SetSheetRow("Sheet1", "B27", &[]interface{}{"cell", nil, int32(42), float64(42), time.Now().UTC()})) assert.EqualError(t, f.SetSheetRow("Sheet1", "", &[]interface{}{"cell", nil, 2}), `cannot convert cell "" to coordinates: invalid cell name ""`) @@ -1193,11 +1195,11 @@ func TestHSL(t *testing.T) { func TestProtectSheet(t *testing.T) { f := NewFile() - f.ProtectSheet("Sheet1", nil) - f.ProtectSheet("Sheet1", &FormatSheetProtection{ + assert.NoError(t, f.ProtectSheet("Sheet1", nil)) + assert.NoError(t, f.ProtectSheet("Sheet1", &FormatSheetProtection{ Password: "password", EditScenarios: false, - }) + })) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestProtectSheet.xlsx"))) // Test protect not exists worksheet. @@ -1212,7 +1214,7 @@ func TestUnprotectSheet(t *testing.T) { // Test unprotect not exists worksheet. assert.EqualError(t, f.UnprotectSheet("SheetN"), "sheet SheetN is not exist") - f.UnprotectSheet("Sheet1") + assert.NoError(t, f.UnprotectSheet("Sheet1")) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestUnprotectSheet.xlsx"))) } @@ -1325,8 +1327,12 @@ func prepareTestBook3() (*File, error) { f.NewSheet("Sheet1") f.NewSheet("XLSXSheet2") f.NewSheet("XLSXSheet3") - f.SetCellInt("XLSXSheet2", "A23", 56) - f.SetCellStr("Sheet1", "B20", "42") + if err := f.SetCellInt("XLSXSheet2", "A23", 56); err != nil { + return nil, err + } + if err := f.SetCellStr("Sheet1", "B20", "42"); err != nil { + return nil, err + } f.SetActiveSheet(0) err := f.AddPicture("Sheet1", "H2", filepath.Join("test", "images", "excel.gif"), @@ -1345,10 +1351,18 @@ func prepareTestBook3() (*File, error) { func prepareTestBook4() (*File, error) { f := NewFile() - f.SetColWidth("Sheet1", "B", "A", 12) - f.SetColWidth("Sheet1", "A", "B", 12) - f.GetColWidth("Sheet1", "A") - f.GetColWidth("Sheet1", "C") + if err := f.SetColWidth("Sheet1", "B", "A", 12); err != nil { + return f, err + } + if err := f.SetColWidth("Sheet1", "A", "B", 12); err != nil { + return f, err + } + if _, err := f.GetColWidth("Sheet1", "A"); err != nil { + return f, err + } + if _, err := f.GetColWidth("Sheet1", "C"); err != nil { + return f, err + } return f, nil } @@ -1357,13 +1371,17 @@ func fillCells(f *File, sheet string, colCount, rowCount int) { for col := 1; col <= colCount; col++ { for row := 1; row <= rowCount; row++ { cell, _ := CoordinatesToCellName(col, row) - f.SetCellStr(sheet, cell, cell) + if err := f.SetCellStr(sheet, cell, cell); err != nil { + panic(err) + } } } } func BenchmarkOpenFile(b *testing.B) { for i := 0; i < b.N; i++ { - OpenFile(filepath.Join("test", "Book1.xlsx")) + if _, err := OpenFile(filepath.Join("test", "Book1.xlsx")); err != nil { + b.Error(err) + } } } diff --git a/file_test.go b/file_test.go index 6c30f4a..97ff720 100644 --- a/file_test.go +++ b/file_test.go @@ -14,7 +14,9 @@ func BenchmarkWrite(b *testing.B) { if err != nil { panic(err) } - f.SetCellDefault("Sheet1", val, s) + if err := f.SetCellDefault("Sheet1", val, s); err != nil { + panic(err) + } } } // Save xlsx file by the given path. diff --git a/picture_test.go b/picture_test.go index 6af3904..ca38f41 100644 --- a/picture_test.go +++ b/picture_test.go @@ -25,7 +25,9 @@ func BenchmarkAddPictureFromBytes(b *testing.B) { } b.ResetTimer() for i := 1; i <= b.N; i++ { - f.AddPictureFromBytes("Sheet1", fmt.Sprint("A", i), "", "excel", ".png", imgFile) + if err := f.AddPictureFromBytes("Sheet1", fmt.Sprint("A", i), "", "excel", ".png", imgFile); err != nil { + b.Error(err) + } } } @@ -36,23 +38,14 @@ func TestAddPicture(t *testing.T) { } // Test add picture to worksheet with offset and location hyperlink. - err = f.AddPicture("Sheet2", "I9", filepath.Join("test", "images", "excel.jpg"), - `{"x_offset": 140, "y_offset": 120, "hyperlink": "#Sheet2!D8", "hyperlink_type": "Location"}`) - if !assert.NoError(t, err) { - t.FailNow() - } - + assert.NoError(t, f.AddPicture("Sheet2", "I9", filepath.Join("test", "images", "excel.jpg"), + `{"x_offset": 140, "y_offset": 120, "hyperlink": "#Sheet2!D8", "hyperlink_type": "Location"}`)) // Test add picture to worksheet with offset, external hyperlink and positioning. - err = f.AddPicture("Sheet1", "F21", filepath.Join("test", "images", "excel.jpg"), - `{"x_offset": 10, "y_offset": 10, "hyperlink": "https://github.com/360EntSecGroup-Skylar/excelize", "hyperlink_type": "External", "positioning": "oneCell"}`) - if !assert.NoError(t, err) { - t.FailNow() - } + assert.NoError(t, f.AddPicture("Sheet1", "F21", filepath.Join("test", "images", "excel.jpg"), + `{"x_offset": 10, "y_offset": 10, "hyperlink": "https://github.com/360EntSecGroup-Skylar/excelize", "hyperlink_type": "External", "positioning": "oneCell"}`)) file, err := ioutil.ReadFile(filepath.Join("test", "images", "excel.png")) - if !assert.NoError(t, err) { - t.FailNow() - } + assert.NoError(t, err) // Test add picture to worksheet from bytes. assert.NoError(t, f.AddPictureFromBytes("Sheet1", "Q1", "", "Excel Logo", ".png", file)) @@ -69,9 +62,7 @@ func TestAddPicture(t *testing.T) { func TestAddPictureErrors(t *testing.T) { xlsx, err := OpenFile(filepath.Join("test", "Book1.xlsx")) - if !assert.NoError(t, err) { - t.FailNow() - } + assert.NoError(t, err) // Test add picture to worksheet with invalid file path. err = xlsx.AddPicture("Sheet1", "G21", filepath.Join("test", "not_exists_dir", "not_exists.icon"), "") @@ -127,14 +118,10 @@ func TestGetPicture(t *testing.T) { f.deleteSheetRelationships("", "") // Try to get picture from a local storage file. - if !assert.NoError(t, f.SaveAs(filepath.Join("test", "TestGetPicture.xlsx"))) { - t.FailNow() - } + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestGetPicture.xlsx"))) f, err = OpenFile(filepath.Join("test", "TestGetPicture.xlsx")) - if !assert.NoError(t, err) { - t.FailNow() - } + assert.NoError(t, err) file, raw, err = f.GetPicture("Sheet1", "F21") assert.NoError(t, err) diff --git a/pivotTable_test.go b/pivotTable_test.go index 9bf08e8..5d841d8 100644 --- a/pivotTable_test.go +++ b/pivotTable_test.go @@ -16,13 +16,13 @@ func TestAddPivotTable(t *testing.T) { year := []int{2017, 2018, 2019} types := []string{"Meat", "Dairy", "Beverages", "Produce"} region := []string{"East", "West", "North", "South"} - f.SetSheetRow("Sheet1", "A1", &[]string{"Month", "Year", "Type", "Sales", "Region"}) + assert.NoError(t, f.SetSheetRow("Sheet1", "A1", &[]string{"Month", "Year", "Type", "Sales", "Region"})) for i := 0; i < 30; i++ { - f.SetCellValue("Sheet1", fmt.Sprintf("A%d", i+2), month[rand.Intn(12)]) - f.SetCellValue("Sheet1", fmt.Sprintf("B%d", i+2), year[rand.Intn(3)]) - f.SetCellValue("Sheet1", fmt.Sprintf("C%d", i+2), types[rand.Intn(4)]) - f.SetCellValue("Sheet1", fmt.Sprintf("D%d", i+2), rand.Intn(5000)) - f.SetCellValue("Sheet1", fmt.Sprintf("E%d", i+2), region[rand.Intn(4)]) + assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("A%d", i+2), month[rand.Intn(12)])) + assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("B%d", i+2), year[rand.Intn(3)])) + assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("C%d", i+2), types[rand.Intn(4)])) + assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("D%d", i+2), rand.Intn(5000))) + assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("E%d", i+2), region[rand.Intn(4)])) } assert.NoError(t, f.AddPivotTable(&PivotTableOption{ DataRange: "Sheet1!$A$1:$E$31", diff --git a/rows_test.go b/rows_test.go index 6494242..fc9d866 100644 --- a/rows_test.go +++ b/rows_test.go @@ -69,7 +69,7 @@ func TestRowsIterator(t *testing.T) { f = NewFile() cells := []string{"C1", "E1", "A3", "B3", "C3", "D3", "E3"} for _, cell := range cells { - f.SetCellValue("Sheet1", cell, 1) + assert.NoError(t, f.SetCellValue("Sheet1", cell, 1)) } rows, err = f.Rows("Sheet1") require.NoError(t, err) @@ -169,8 +169,12 @@ func TestRowVisibility(t *testing.T) { f.NewSheet("Sheet3") assert.NoError(t, f.SetRowVisible("Sheet3", 2, false)) assert.NoError(t, f.SetRowVisible("Sheet3", 2, true)) - f.GetRowVisible("Sheet3", 2) - f.GetRowVisible("Sheet3", 25) + visiable, err := f.GetRowVisible("Sheet3", 2) + assert.Equal(t, true, visiable) + assert.NoError(t, err) + visiable, err = f.GetRowVisible("Sheet3", 25) + assert.Equal(t, false, visiable) + assert.NoError(t, err) assert.EqualError(t, f.SetRowVisible("Sheet3", 0, true), "invalid row number 0") assert.EqualError(t, f.SetRowVisible("SheetN", 2, false), "sheet SheetN is not exist") @@ -194,7 +198,7 @@ func TestRemoveRow(t *testing.T) { ) fillCells(f, sheet1, colCount, rowCount) - f.SetCellHyperLink(sheet1, "A5", "https://github.com/360EntSecGroup-Skylar/excelize", "External") + assert.NoError(t, f.SetCellHyperLink(sheet1, "A5", "https://github.com/360EntSecGroup-Skylar/excelize", "External")) assert.EqualError(t, f.RemoveRow(sheet1, -1), "invalid row number -1") @@ -205,7 +209,7 @@ func TestRemoveRow(t *testing.T) { t.FailNow() } - f.MergeCell(sheet1, "B3", "B5") + assert.NoError(t, f.MergeCell(sheet1, "B3", "B5")) assert.NoError(t, f.RemoveRow(sheet1, 2)) if !assert.Len(t, r.SheetData.Row, rowCount-2) { @@ -255,7 +259,7 @@ func TestInsertRow(t *testing.T) { ) fillCells(xlsx, sheet1, colCount, rowCount) - xlsx.SetCellHyperLink(sheet1, "A5", "https://github.com/360EntSecGroup-Skylar/excelize", "External") + assert.NoError(t, xlsx.SetCellHyperLink(sheet1, "A5", "https://github.com/360EntSecGroup-Skylar/excelize", "External")) assert.EqualError(t, xlsx.InsertRow(sheet1, -1), "invalid row number -1") @@ -305,8 +309,8 @@ func TestDuplicateRowFromSingleRow(t *testing.T) { t.Run("FromSingleRow", func(t *testing.T) { xlsx := NewFile() - xlsx.SetCellStr(sheet, "A1", cells["A1"]) - xlsx.SetCellStr(sheet, "B1", cells["B1"]) + assert.NoError(t, xlsx.SetCellStr(sheet, "A1", cells["A1"])) + assert.NoError(t, xlsx.SetCellStr(sheet, "B1", cells["B1"])) assert.NoError(t, xlsx.DuplicateRow(sheet, 1)) if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.FromSingleRow_1"))) { @@ -358,13 +362,13 @@ func TestDuplicateRowUpdateDuplicatedRows(t *testing.T) { t.Run("UpdateDuplicatedRows", func(t *testing.T) { xlsx := NewFile() - xlsx.SetCellStr(sheet, "A1", cells["A1"]) - xlsx.SetCellStr(sheet, "B1", cells["B1"]) + assert.NoError(t, xlsx.SetCellStr(sheet, "A1", cells["A1"])) + assert.NoError(t, xlsx.SetCellStr(sheet, "B1", cells["B1"])) assert.NoError(t, xlsx.DuplicateRow(sheet, 1)) - xlsx.SetCellStr(sheet, "A2", cells["A2"]) - xlsx.SetCellStr(sheet, "B2", cells["B2"]) + assert.NoError(t, xlsx.SetCellStr(sheet, "A2", cells["A2"])) + assert.NoError(t, xlsx.SetCellStr(sheet, "B2", cells["B2"])) if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.UpdateDuplicatedRows"))) { t.FailNow() @@ -399,8 +403,7 @@ func TestDuplicateRowFirstOfMultipleRows(t *testing.T) { newFileWithDefaults := func() *File { f := NewFile() for cell, val := range cells { - f.SetCellStr(sheet, cell, val) - + assert.NoError(t, f.SetCellStr(sheet, cell, val)) } return f } @@ -514,8 +517,7 @@ func TestDuplicateRowWithLargeOffsetToMiddleOfData(t *testing.T) { newFileWithDefaults := func() *File { f := NewFile() for cell, val := range cells { - f.SetCellStr(sheet, cell, val) - + assert.NoError(t, f.SetCellStr(sheet, cell, val)) } return f } @@ -560,8 +562,7 @@ func TestDuplicateRowWithLargeOffsetToEmptyRows(t *testing.T) { newFileWithDefaults := func() *File { f := NewFile() for cell, val := range cells { - f.SetCellStr(sheet, cell, val) - + assert.NoError(t, f.SetCellStr(sheet, cell, val)) } return f } @@ -606,8 +607,7 @@ func TestDuplicateRowInsertBefore(t *testing.T) { newFileWithDefaults := func() *File { f := NewFile() for cell, val := range cells { - f.SetCellStr(sheet, cell, val) - + assert.NoError(t, f.SetCellStr(sheet, cell, val)) } return f } @@ -653,8 +653,7 @@ func TestDuplicateRowInsertBeforeWithLargeOffset(t *testing.T) { newFileWithDefaults := func() *File { f := NewFile() for cell, val := range cells { - f.SetCellStr(sheet, cell, val) - + assert.NoError(t, f.SetCellStr(sheet, cell, val)) } return f } @@ -704,7 +703,7 @@ func TestDuplicateRowInvalidRownum(t *testing.T) { t.Run(name, func(t *testing.T) { xlsx := NewFile() for col, val := range cells { - xlsx.SetCellStr(sheet, col, val) + assert.NoError(t, xlsx.SetCellStr(sheet, col, val)) } assert.EqualError(t, xlsx.DuplicateRow(sheet, row), fmt.Sprintf("invalid row number %d", row)) @@ -726,7 +725,7 @@ func TestDuplicateRowInvalidRownum(t *testing.T) { t.Run(name, func(t *testing.T) { xlsx := NewFile() for col, val := range cells { - xlsx.SetCellStr(sheet, col, val) + assert.NoError(t, xlsx.SetCellStr(sheet, col, val)) } assert.EqualError(t, xlsx.DuplicateRowTo(sheet, row1, row2), fmt.Sprintf("invalid row number %d", row1)) diff --git a/sheet_test.go b/sheet_test.go index aada60a..7a58248 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -192,7 +192,7 @@ func TestGetPageLayout(t *testing.T) { func TestSetHeaderFooter(t *testing.T) { f := excelize.NewFile() - f.SetCellStr("Sheet1", "A1", "Test SetHeaderFooter") + assert.NoError(t, f.SetCellStr("Sheet1", "A1", "Test SetHeaderFooter")) // Test set header and footer on not exists worksheet. assert.EqualError(t, f.SetHeaderFooter("SheetN", nil), "sheet SheetN is not exist") // Test set header and footer with illegal setting. diff --git a/sparkline_test.go b/sparkline_test.go index a5cb216..dca32e9 100644 --- a/sparkline_test.go +++ b/sparkline_test.go @@ -17,40 +17,40 @@ func TestAddSparkline(t *testing.T) { assert.NoError(t, f.SetCellStyle("Sheet1", "A1", "B1", style)) assert.NoError(t, f.SetSheetViewOptions("Sheet1", 0, ZoomScale(150))) - f.SetColWidth("Sheet1", "A", "A", 14) - f.SetColWidth("Sheet1", "B", "B", 50) + assert.NoError(t, f.SetColWidth("Sheet1", "A", "A", 14)) + assert.NoError(t, f.SetColWidth("Sheet1", "B", "B", 50)) // Headings - f.SetCellValue("Sheet1", "A1", "Sparkline") - f.SetCellValue("Sheet1", "B1", "Description") + assert.NoError(t, f.SetCellValue("Sheet1", "A1", "Sparkline")) + assert.NoError(t, f.SetCellValue("Sheet1", "B1", "Description")) - f.SetCellValue("Sheet1", "B2", `A default "line" sparkline.`) + assert.NoError(t, f.SetCellValue("Sheet1", "B2", `A default "line" sparkline.`)) assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ Location: []string{"A2"}, Range: []string{"Sheet3!A1:J1"}, })) - f.SetCellValue("Sheet1", "B3", `A default "column" sparkline.`) + assert.NoError(t, f.SetCellValue("Sheet1", "B3", `A default "column" sparkline.`)) assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ Location: []string{"A3"}, Range: []string{"Sheet3!A2:J2"}, Type: "column", })) - f.SetCellValue("Sheet1", "B4", `A default "win/loss" sparkline.`) + assert.NoError(t, f.SetCellValue("Sheet1", "B4", `A default "win/loss" sparkline.`)) assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ Location: []string{"A4"}, Range: []string{"Sheet3!A3:J3"}, Type: "win_loss", })) - f.SetCellValue("Sheet1", "B6", "Line with markers.") + assert.NoError(t, f.SetCellValue("Sheet1", "B6", "Line with markers.")) assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ Location: []string{"A6"}, Range: []string{"Sheet3!A1:J1"}, Markers: true, })) - f.SetCellValue("Sheet1", "B7", "Line with high and low points.") + assert.NoError(t, f.SetCellValue("Sheet1", "B7", "Line with high and low points.")) assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ Location: []string{"A7"}, Range: []string{"Sheet3!A1:J1"}, @@ -58,7 +58,7 @@ func TestAddSparkline(t *testing.T) { Low: true, })) - f.SetCellValue("Sheet1", "B8", "Line with first and last point markers.") + assert.NoError(t, f.SetCellValue("Sheet1", "B8", "Line with first and last point markers.")) assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ Location: []string{"A8"}, Range: []string{"Sheet3!A1:J1"}, @@ -66,28 +66,28 @@ func TestAddSparkline(t *testing.T) { Last: true, })) - f.SetCellValue("Sheet1", "B9", "Line with negative point markers.") + assert.NoError(t, f.SetCellValue("Sheet1", "B9", "Line with negative point markers.")) assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ Location: []string{"A9"}, Range: []string{"Sheet3!A1:J1"}, Negative: true, })) - f.SetCellValue("Sheet1", "B10", "Line with axis.") + assert.NoError(t, f.SetCellValue("Sheet1", "B10", "Line with axis.")) assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ Location: []string{"A10"}, Range: []string{"Sheet3!A1:J1"}, Axis: true, })) - f.SetCellValue("Sheet1", "B12", "Column with default style (1).") + assert.NoError(t, f.SetCellValue("Sheet1", "B12", "Column with default style (1).")) assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ Location: []string{"A12"}, Range: []string{"Sheet3!A2:J2"}, Type: "column", })) - f.SetCellValue("Sheet1", "B13", "Column with style 2.") + assert.NoError(t, f.SetCellValue("Sheet1", "B13", "Column with style 2.")) assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ Location: []string{"A13"}, Range: []string{"Sheet3!A2:J2"}, @@ -95,7 +95,7 @@ func TestAddSparkline(t *testing.T) { Style: 2, })) - f.SetCellValue("Sheet1", "B14", "Column with style 3.") + assert.NoError(t, f.SetCellValue("Sheet1", "B14", "Column with style 3.")) assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ Location: []string{"A14"}, Range: []string{"Sheet3!A2:J2"}, @@ -103,7 +103,7 @@ func TestAddSparkline(t *testing.T) { Style: 3, })) - f.SetCellValue("Sheet1", "B15", "Column with style 4.") + assert.NoError(t, f.SetCellValue("Sheet1", "B15", "Column with style 4.")) assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ Location: []string{"A15"}, Range: []string{"Sheet3!A2:J2"}, @@ -111,7 +111,7 @@ func TestAddSparkline(t *testing.T) { Style: 4, })) - f.SetCellValue("Sheet1", "B16", "Column with style 5.") + assert.NoError(t, f.SetCellValue("Sheet1", "B16", "Column with style 5.")) assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ Location: []string{"A16"}, Range: []string{"Sheet3!A2:J2"}, @@ -119,7 +119,7 @@ func TestAddSparkline(t *testing.T) { Style: 5, })) - f.SetCellValue("Sheet1", "B17", "Column with style 6.") + assert.NoError(t, f.SetCellValue("Sheet1", "B17", "Column with style 6.")) assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ Location: []string{"A17"}, Range: []string{"Sheet3!A2:J2"}, @@ -127,7 +127,7 @@ func TestAddSparkline(t *testing.T) { Style: 6, })) - f.SetCellValue("Sheet1", "B18", "Column with a user defined color.") + assert.NoError(t, f.SetCellValue("Sheet1", "B18", "Column with a user defined color.")) assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ Location: []string{"A18"}, Range: []string{"Sheet3!A2:J2"}, @@ -135,14 +135,14 @@ func TestAddSparkline(t *testing.T) { SeriesColor: "#E965E0", })) - f.SetCellValue("Sheet1", "B20", "A win/loss sparkline.") + assert.NoError(t, f.SetCellValue("Sheet1", "B20", "A win/loss sparkline.")) assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ Location: []string{"A20"}, Range: []string{"Sheet3!A3:J3"}, Type: "win_loss", })) - f.SetCellValue("Sheet1", "B21", "A win/loss sparkline with negative points highlighted.") + assert.NoError(t, f.SetCellValue("Sheet1", "B21", "A win/loss sparkline with negative points highlighted.")) assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ Location: []string{"A21"}, Range: []string{"Sheet3!A3:J3"}, @@ -150,7 +150,7 @@ func TestAddSparkline(t *testing.T) { Negative: true, })) - f.SetCellValue("Sheet1", "B23", "A left to right column (the default).") + assert.NoError(t, f.SetCellValue("Sheet1", "B23", "A left to right column (the default).")) assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ Location: []string{"A23"}, Range: []string{"Sheet3!A4:J4"}, @@ -158,7 +158,7 @@ func TestAddSparkline(t *testing.T) { Style: 20, })) - f.SetCellValue("Sheet1", "B24", "A right to left column.") + assert.NoError(t, f.SetCellValue("Sheet1", "B24", "A right to left column.")) assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ Location: []string{"A24"}, Range: []string{"Sheet3!A4:J4"}, @@ -167,16 +167,16 @@ func TestAddSparkline(t *testing.T) { Reverse: true, })) - f.SetCellValue("Sheet1", "B25", "Sparkline and text in one cell.") + assert.NoError(t, f.SetCellValue("Sheet1", "B25", "Sparkline and text in one cell.")) assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ Location: []string{"A25"}, Range: []string{"Sheet3!A4:J4"}, Type: "column", Style: 20, })) - f.SetCellValue("Sheet1", "A25", "Growth") + assert.NoError(t, f.SetCellValue("Sheet1", "A25", "Growth")) - f.SetCellValue("Sheet1", "B27", "A grouped sparkline. Changes are applied to all three.") + assert.NoError(t, f.SetCellValue("Sheet1", "B27", "A grouped sparkline. Changes are applied to all three.")) assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ Location: []string{"A27", "A28", "A29"}, Range: []string{"Sheet3!A5:J5", "Sheet3!A6:J6", "Sheet3!A7:J7"}, @@ -297,10 +297,14 @@ func prepareSparklineDataset() *File { f.NewSheet("Sheet2") f.NewSheet("Sheet3") for row, data := range sheet2 { - f.SetSheetRow("Sheet2", fmt.Sprintf("A%d", row+1), &data) + if err := f.SetSheetRow("Sheet2", fmt.Sprintf("A%d", row+1), &data); err != nil { + panic(err) + } } for row, data := range sheet3 { - f.SetSheetRow("Sheet3", fmt.Sprintf("A%d", row+1), &data) + if err := f.SetSheetRow("Sheet3", fmt.Sprintf("A%d", row+1), &data); err != nil { + panic(err) + } } return f } diff --git a/stream.go b/stream.go index 5e74e8e..1b1bbe3 100644 --- a/stream.go +++ b/stream.go @@ -167,7 +167,7 @@ func (sw *StreamWriter) Flush() error { sheetDataByte = append(sheetDataByte, sw.SheetData.Bytes()...) replaceMap := map[string][]byte{ - "XMLName": []byte{}, + "XMLName": {}, "SheetData": sheetDataByte, } sw.SheetData.Reset() diff --git a/table.go b/table.go index c5a704c..3d76690 100644 --- a/table.go +++ b/table.go @@ -139,7 +139,7 @@ func (f *File) addTable(sheet, tableXML string, x1, y1, x2, y2, i int, formatSet } name, _ := f.GetCellValue(sheet, cell) if _, err := strconv.Atoi(name); err == nil { - f.SetCellStr(sheet, cell, name) + _ = f.SetCellStr(sheet, cell, name) } if name == "" { name = "Column" + strconv.Itoa(idx) -- cgit v1.2.1 From 5f3a4bc39f9cf2987104ffe57242a0526cdd9158 Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 25 Dec 2019 00:00:50 +0800 Subject: Fix #538, added setting a major unit and tick label skip support for the chart --- cell.go | 3 ++- chart.go | 26 +++++++++++++++++++++++++- chart_test.go | 2 +- xmlChart.go | 7 ++++++- 4 files changed, 34 insertions(+), 4 deletions(-) diff --git a/cell.go b/cell.go index e59a659..da091ee 100644 --- a/cell.go +++ b/cell.go @@ -44,7 +44,8 @@ func (f *File) GetCellValue(sheet, axis string) (string, error) { }) } -// SetCellValue provides a function to set value of a cell. The following +// SetCellValue provides a function to set value of a cell. The specified +// coordinates should not be in the first row of the table. The following // shows the supported data types: // // int diff --git a/chart.go b/chart.go index 5a42c5b..b1446fb 100644 --- a/chart.go +++ b/chart.go @@ -653,10 +653,21 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // // show_val: Specifies that the value shall be shown in a data label. The show_val property is optional. The default value is false. // -// Set the primary horizontal and vertical axis options by x_axis and y_axis. The properties that can be set are: +// Set the primary horizontal and vertical axis options by x_axis and y_axis. The properties of x_axis that can be set are: // // major_grid_lines // minor_grid_lines +// major_unit +// reverse_order +// maximum +// minimum +// +// The properties of y_axis that can be set are: +// +// major_grid_lines +// minor_grid_lines +// major_unit +// tick_label_skip // reverse_order // maximum // minimum @@ -665,6 +676,10 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // // minor_grid_lines: Specifies minor gridlines. // +// major_unit: Specifies the distance between major ticks. Shall contain a positive floating-point number. The major_unit property is optional. The default value is auto. +// +// tick_label_skip: Specifies how many tick labels to skip between label that is drawn. The tick_label_skip property is optional. The default value is auto. +// // reverse_order: Specifies that the categories or values on reverse order (orientation of the chart). The reverse_order property is optional. The default value is false. // // maximum: Specifies that the fixed maximum, 0 is auto. The maximum property is optional. The default value is auto. @@ -1612,6 +1627,12 @@ func (f *File) drawPlotAreaCatAx(formatSet *formatChart) []*cAxs { if formatSet.XAxis.MinorGridlines { axs[0].MinorGridlines = &cChartLines{SpPr: f.drawPlotAreaSpPr()} } + if formatSet.XAxis.MajorUnit != 0 { + axs[0].MajorUnit = &attrValFloat{Val: float64Ptr(formatSet.XAxis.MajorUnit)} + } + if formatSet.XAxis.TickLabelSkip != 0 { + axs[0].TickLblSkip = &attrValInt{Val: intPtr(formatSet.XAxis.TickLabelSkip)} + } return axs } @@ -1658,6 +1679,9 @@ func (f *File) drawPlotAreaValAx(formatSet *formatChart) []*cAxs { if pos, ok := valTickLblPos[formatSet.Type]; ok { axs[0].TickLblPos.Val = stringPtr(pos) } + if formatSet.YAxis.MajorUnit != 0 { + axs[0].MajorUnit = &attrValFloat{Val: float64Ptr(formatSet.YAxis.MajorUnit)} + } return axs } diff --git a/chart_test.go b/chart_test.go index 2379ddc..5d0f2d1 100644 --- a/chart_test.go +++ b/chart_test.go @@ -137,7 +137,7 @@ func TestAddChart(t *testing.T) { assert.NoError(t, f.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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"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"}`)) assert.NoError(t, f.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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"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"}`)) assert.NoError(t, f.AddChart("Sheet2", "P16", `{"type":"doughnut","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$A$30:$D$37","values":"Sheet1!$B$30:$B$37"}],"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":"right","show_legend_key":false},"title":{"name":"Doughnut Chart"},"plotarea":{"show_bubble_size":false,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":false,"show_val":false},"show_blanks_as":"zero"}`)) - assert.NoError(t, f.AddChart("Sheet2", "X16", `{"type":"line","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37","line":{"width":0.25}}],"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","show_legend_key":false},"title":{"name":"Line 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","x_axis":{"major_grid_lines":true,"minor_grid_lines":true},"y_axis":{"major_grid_lines":true,"minor_grid_lines":true}}`)) + assert.NoError(t, f.AddChart("Sheet2", "X16", `{"type":"line","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37","line":{"width":0.25}}],"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","show_legend_key":false},"title":{"name":"Line 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","x_axis":{"major_grid_lines":true,"minor_grid_lines":true,"major_unit":1,"tick_label_skip":1},"y_axis":{"major_grid_lines":true,"minor_grid_lines":true,"major_unit":1}}`)) assert.NoError(t, f.AddChart("Sheet2", "P32", `{"type":"pie3D","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$A$30:$D$37","values":"Sheet1!$B$30:$B$37"}],"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":"3D Pie Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":false,"show_val":false},"show_blanks_as":"zero"}`)) assert.NoError(t, f.AddChart("Sheet2", "X32", `{"type":"pie","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$A$30:$D$37","values":"Sheet1!$B$30:$B$37"}],"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":"Pie Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":false,"show_val":false},"show_blanks_as":"gap"}`)) assert.NoError(t, f.AddChart("Sheet2", "P48", `{"type":"bar","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"2D Clustered Bar 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"}`)) diff --git a/xmlChart.go b/xmlChart.go index 84c1a3b..a28d2a7 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -357,9 +357,13 @@ type cAxs struct { CrossAx *attrValInt `xml:"crossAx"` Crosses *attrValString `xml:"crosses"` CrossBetween *attrValString `xml:"crossBetween"` + MajorUnit *attrValFloat `xml:"majorUnit"` + MinorUnit *attrValFloat `xml:"minorUnit"` Auto *attrValBool `xml:"auto"` LblAlgn *attrValString `xml:"lblAlgn"` LblOffset *attrValInt `xml:"lblOffset"` + TickLblSkip *attrValInt `xml:"tickLblSkip"` + TickMarkSkip *attrValInt `xml:"tickMarkSkip"` NoMultiLvlLbl *attrValBool `xml:"noMultiLvlLbl"` } @@ -519,8 +523,9 @@ type formatChartAxis struct { MajorTickMark string `json:"major_tick_mark"` MinorTickMark string `json:"minor_tick_mark"` MinorUnitType string `json:"minor_unit_type"` - MajorUnit int `json:"major_unit"` + MajorUnit float64 `json:"major_unit"` MajorUnitType string `json:"major_unit_type"` + TickLabelSkip int `json:"tick_label_skip"` DisplayUnits string `json:"display_units"` DisplayUnitsVisible bool `json:"display_units_visible"` DateAxis bool `json:"date_axis"` -- cgit v1.2.1 From 8b960ee1e624bd2776a351a4a3b2ad04c29bae9a Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 28 Dec 2019 15:05:44 +0800 Subject: Fix #547 and #546, add default overlay element for the chart --- chart.go | 5 +---- chart_test.go | 2 +- xmlChart.go | 10 +++++----- xmlWorksheet.go | 2 +- 4 files changed, 8 insertions(+), 11 deletions(-) diff --git a/chart.go b/chart.go index b1446fb..8b38d22 100644 --- a/chart.go +++ b/chart.go @@ -657,7 +657,6 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // // major_grid_lines // minor_grid_lines -// major_unit // reverse_order // maximum // minimum @@ -818,6 +817,7 @@ func (f *File) addChart(formatSet *formatChart) { }, }, }, + Overlay: &attrValBool{Val: boolPtr(false)}, }, View3D: &cView3D{ RotX: &attrValInt{Val: intPtr(chartView3DRotX[formatSet.Type])}, @@ -1627,9 +1627,6 @@ func (f *File) drawPlotAreaCatAx(formatSet *formatChart) []*cAxs { if formatSet.XAxis.MinorGridlines { axs[0].MinorGridlines = &cChartLines{SpPr: f.drawPlotAreaSpPr()} } - if formatSet.XAxis.MajorUnit != 0 { - axs[0].MajorUnit = &attrValFloat{Val: float64Ptr(formatSet.XAxis.MajorUnit)} - } if formatSet.XAxis.TickLabelSkip != 0 { axs[0].TickLblSkip = &attrValInt{Val: intPtr(formatSet.XAxis.TickLabelSkip)} } diff --git a/chart_test.go b/chart_test.go index 5d0f2d1..2ed7944 100644 --- a/chart_test.go +++ b/chart_test.go @@ -137,7 +137,7 @@ func TestAddChart(t *testing.T) { assert.NoError(t, f.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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"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"}`)) assert.NoError(t, f.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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"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"}`)) assert.NoError(t, f.AddChart("Sheet2", "P16", `{"type":"doughnut","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$A$30:$D$37","values":"Sheet1!$B$30:$B$37"}],"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":"right","show_legend_key":false},"title":{"name":"Doughnut Chart"},"plotarea":{"show_bubble_size":false,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":false,"show_val":false},"show_blanks_as":"zero"}`)) - assert.NoError(t, f.AddChart("Sheet2", "X16", `{"type":"line","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37","line":{"width":0.25}}],"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","show_legend_key":false},"title":{"name":"Line 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","x_axis":{"major_grid_lines":true,"minor_grid_lines":true,"major_unit":1,"tick_label_skip":1},"y_axis":{"major_grid_lines":true,"minor_grid_lines":true,"major_unit":1}}`)) + assert.NoError(t, f.AddChart("Sheet2", "X16", `{"type":"line","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37","line":{"width":0.25}}],"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","show_legend_key":false},"title":{"name":"Line 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","x_axis":{"major_grid_lines":true,"minor_grid_lines":true,"tick_label_skip":1},"y_axis":{"major_grid_lines":true,"minor_grid_lines":true,"major_unit":1}}`)) assert.NoError(t, f.AddChart("Sheet2", "P32", `{"type":"pie3D","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$A$30:$D$37","values":"Sheet1!$B$30:$B$37"}],"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":"3D Pie Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":false,"show_val":false},"show_blanks_as":"zero"}`)) assert.NoError(t, f.AddChart("Sheet2", "X32", `{"type":"pie","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$A$30:$D$37","values":"Sheet1!$B$30:$B$37"}],"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":"Pie Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":false,"show_val":false},"show_blanks_as":"gap"}`)) assert.NoError(t, f.AddChart("Sheet2", "P48", `{"type":"bar","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"2D Clustered Bar 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"}`)) diff --git a/xmlChart.go b/xmlChart.go index a28d2a7..fc38dab 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -56,11 +56,11 @@ type cChart struct { // cTitle (Title) directly maps the title element. This element specifies a // title. type cTitle struct { - Tx cTx `xml:"tx,omitempty"` - Layout string `xml:"layout,omitempty"` - Overlay attrValBool `xml:"overlay,omitempty"` - SpPr cSpPr `xml:"spPr,omitempty"` - TxPr cTxPr `xml:"txPr,omitempty"` + Tx cTx `xml:"tx,omitempty"` + Layout string `xml:"layout,omitempty"` + Overlay *attrValBool `xml:"overlay"` + SpPr cSpPr `xml:"spPr,omitempty"` + TxPr cTxPr `xml:"txPr,omitempty"` } // cTx (Chart Text) directly maps the tx element. This element specifies text diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 57fd43f..c17d12f 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -475,7 +475,7 @@ func (c *xlsxC) hasValue() bool { // http://schemas.openxmlformats.org/spreadsheetml/2006/main - currently I have // not checked it for completeness - it does as much as I need. type xlsxF struct { - Content string `xml:",chardata"` + Content string `xml:",innerxml"` T string `xml:"t,attr,omitempty"` // Formula type Ref string `xml:"ref,attr,omitempty"` // Shared formula ref Si string `xml:"si,attr,omitempty"` // Shared formula index -- cgit v1.2.1 From 5c87effc7e6c97fff36a56dea1afac8a2f06fb37 Mon Sep 17 00:00:00 2001 From: Cameron Howey Date: Sat, 28 Dec 2019 20:45:10 -0800 Subject: Stream to Excel table (#530) * Support all datatypes for StreamWriter * Support setting styles with StreamWriter **NOTE:** This is a breaking change. Values are now explicitly passed as a []interface{} for simplicity. We also let styles to be set at the same time. * Create function to write stream into a table * Write rows directly to buffer Avoiding the xml.Encoder makes the streamer faster and use less memory. Using the included benchmark, the results went from: > BenchmarkStreamWriter-4 514 2576155 ns/op 454918 B/op 6592 allocs/op down to: > BenchmarkStreamWriter-4 1614 777480 ns/op 147608 B/op 5570 allocs/op * Use AddTable instead of SetTable This requires reading the cells after they have been written, which requires additional structure for the temp file. As a bonus, we now efficiently allocate only one buffer when reading the file back into memory, using the same approach as ioutil.ReadFile. * Use an exported Cell type to handle inline styles for StreamWriter --- adjust.go | 23 ++- cell.go | 100 ++++++++---- stream.go | 495 ++++++++++++++++++++++++++++++++++++++++++++------------- stream_test.go | 82 +++++++--- 4 files changed, 540 insertions(+), 160 deletions(-) diff --git a/adjust.go b/adjust.go index c15d4b4..69ded1b 100644 --- a/adjust.go +++ b/adjust.go @@ -196,10 +196,14 @@ func (f *File) adjustAutoFilterHelper(dir adjustDirection, coordinates []int, nu // areaRefToCoordinates provides a function to convert area reference to a // pair of coordinates. func (f *File) areaRefToCoordinates(ref string) ([]int, error) { - coordinates := make([]int, 4) rng := strings.Split(ref, ":") - firstCell := rng[0] - lastCell := rng[1] + return areaRangeToCoordinates(rng[0], rng[1]) +} + +// areaRangeToCoordinates provides a function to convert cell range to a +// pair of coordinates. +func areaRangeToCoordinates(firstCell, lastCell string) ([]int, error) { + coordinates := make([]int, 4) var err error coordinates[0], coordinates[1], err = CellNameToCoordinates(firstCell) if err != nil { @@ -209,6 +213,19 @@ func (f *File) areaRefToCoordinates(ref string) ([]int, error) { return coordinates, err } +func sortCoordinates(coordinates []int) error { + if len(coordinates) != 4 { + return errors.New("coordinates length must be 4") + } + if coordinates[2] < coordinates[0] { + coordinates[2], coordinates[0] = coordinates[0], coordinates[2] + } + if coordinates[3] < coordinates[1] { + coordinates[3], coordinates[1] = coordinates[1], coordinates[3] + } + return nil +} + // coordinatesToAreaRef provides a function to convert a pair of coordinates // to area reference. func (f *File) coordinatesToAreaRef(coordinates []int) (string, error) { diff --git a/cell.go b/cell.go index da091ee..1aeddc1 100644 --- a/cell.go +++ b/cell.go @@ -83,7 +83,8 @@ func (f *File) SetCellValue(sheet, axis string, value interface{}) error { case []byte: err = f.SetCellStr(sheet, axis, string(v)) case time.Duration: - err = f.SetCellDefault(sheet, axis, strconv.FormatFloat(v.Seconds()/86400.0, 'f', -1, 32)) + _, d := setCellDuration(v) + err = f.SetCellDefault(sheet, axis, d) if err != nil { return err } @@ -131,28 +132,50 @@ func (f *File) setCellIntFunc(sheet, axis string, value interface{}) error { // setCellTimeFunc provides a method to process time type of value for // SetCellValue. func (f *File) setCellTimeFunc(sheet, axis string, value time.Time) error { - excelTime, err := timeToExcelTime(value) + xlsx, err := f.workSheetReader(sheet) if err != nil { return err } - if excelTime > 0 { - err = f.SetCellDefault(sheet, axis, strconv.FormatFloat(excelTime, 'f', -1, 64)) - if err != nil { - return err - } + cellData, col, _, err := f.prepareCell(xlsx, sheet, axis) + if err != nil { + return err + } + cellData.S = f.prepareCellStyle(xlsx, col, cellData.S) + + var isNum bool + cellData.T, cellData.V, isNum, err = setCellTime(value) + if err != nil { + return err + } + if isNum { err = f.setDefaultTimeStyle(sheet, axis, 22) if err != nil { return err } - } else { - err = f.SetCellStr(sheet, axis, value.Format(time.RFC3339Nano)) - if err != nil { - return err - } } return err } +func setCellTime(value time.Time) (t string, b string, isNum bool, err error) { + var excelTime float64 + excelTime, err = timeToExcelTime(value) + if err != nil { + return + } + isNum = excelTime > 0 + if isNum { + t, b = setCellDefault(strconv.FormatFloat(excelTime, 'f', -1, 64)) + } else { + t, b = setCellDefault(value.Format(time.RFC3339Nano)) + } + return +} + +func setCellDuration(value time.Duration) (t string, v string) { + v = strconv.FormatFloat(value.Seconds()/86400.0, 'f', -1, 32) + return +} + // 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) error { @@ -165,11 +188,15 @@ func (f *File) SetCellInt(sheet, axis string, value int) error { return err } cellData.S = f.prepareCellStyle(xlsx, col, cellData.S) - cellData.T = "" - cellData.V = strconv.Itoa(value) + cellData.T, cellData.V = setCellInt(value) return err } +func setCellInt(value int) (t string, v string) { + v = strconv.Itoa(value) + return +} + // SetCellBool provides a function to set bool type value of a cell by given // worksheet name, cell name and cell value. func (f *File) SetCellBool(sheet, axis string, value bool) error { @@ -182,13 +209,18 @@ func (f *File) SetCellBool(sheet, axis string, value bool) error { return err } cellData.S = f.prepareCellStyle(xlsx, col, cellData.S) - cellData.T = "b" + cellData.T, cellData.V = setCellBool(value) + return err +} + +func setCellBool(value bool) (t string, v string) { + t = "b" if value { - cellData.V = "1" + v = "1" } else { - cellData.V = "0" + v = "0" } - return err + return } // SetCellFloat sets a floating point value into a cell. The prec parameter @@ -210,11 +242,15 @@ func (f *File) SetCellFloat(sheet, axis string, value float64, prec, bitSize int return err } cellData.S = f.prepareCellStyle(xlsx, col, cellData.S) - cellData.T = "" - cellData.V = strconv.FormatFloat(value, 'f', prec, bitSize) + cellData.T, cellData.V = setCellFloat(value, prec, bitSize) return err } +func setCellFloat(value float64, prec, bitSize int) (t string, v string) { + v = strconv.FormatFloat(value, 'f', prec, bitSize) + return +} + // 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) error { @@ -226,21 +262,25 @@ func (f *File) SetCellStr(sheet, axis, value string) error { if err != nil { return err } + cellData.S = f.prepareCellStyle(xlsx, col, cellData.S) + cellData.T, cellData.V, cellData.XMLSpace = setCellStr(value) + return err +} + +func setCellStr(value string) (t string, v string, ns xml.Attr) { if len(value) > 32767 { value = value[0:32767] } // Leading and ending space(s) character detection. if len(value) > 0 && (value[0] == 32 || value[len(value)-1] == 32) { - cellData.XMLSpace = xml.Attr{ + ns = xml.Attr{ Name: xml.Name{Space: NameSpaceXML, Local: "space"}, Value: "preserve", } } - - cellData.S = f.prepareCellStyle(xlsx, col, cellData.S) - cellData.T = "str" - cellData.V = value - return err + t = "str" + v = value + return } // SetCellDefault provides a function to set string type value of a cell as @@ -255,11 +295,15 @@ func (f *File) SetCellDefault(sheet, axis, value string) error { return err } cellData.S = f.prepareCellStyle(xlsx, col, cellData.S) - cellData.T = "" - cellData.V = value + cellData.T, cellData.V = setCellDefault(value) return err } +func setCellDefault(value string) (t string, v string) { + v = value + return +} + // 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, error) { diff --git a/stream.go b/stream.go index 1b1bbe3..e981f78 100644 --- a/stream.go +++ b/stream.go @@ -12,20 +12,23 @@ package excelize import ( "bytes" "encoding/xml" - "errors" "fmt" + "io" "io/ioutil" "os" "reflect" + "strconv" + "strings" + "time" ) // StreamWriter defined the type of stream writer. type StreamWriter struct { - tmpFile *os.File - File *File - Sheet string - SheetID int - SheetData bytes.Buffer + File *File + Sheet string + SheetID int + rawData bufferedWriter + tableParts string } // NewStreamWriter return stream writer struct by given worksheet name for @@ -46,7 +49,7 @@ type StreamWriter struct { // row[colID] = rand.Intn(640000) // } // cell, _ := excelize.CoordinatesToCellName(1, rowID) -// if err := streamWriter.SetRow(cell, &row); err != nil { +// if err := streamWriter.SetRow(cell, row, nil); err != nil { // panic(err) // } // } @@ -62,157 +65,433 @@ func (f *File) NewStreamWriter(sheet string) (*StreamWriter, error) { if sheetID == 0 { return nil, fmt.Errorf("sheet %s is not exist", sheet) } - rsw := &StreamWriter{ + sw := &StreamWriter{ File: f, Sheet: sheet, SheetID: sheetID, } - rsw.SheetData.WriteString("") - return rsw, nil + + ws, err := f.workSheetReader(sheet) + if err != nil { + return nil, err + } + sw.rawData.WriteString(XMLHeader + ``) + return sw, nil } -// SetRow writes an array to streaming row by given worksheet name, starting -// coordinate and a pointer to array type 'slice'. Note that, cell settings -// with styles are not supported currently and after set rows, you must call the -// 'Flush' method to end the streaming writing process. The following -// shows the supported data types: +// AddTable creates an Excel table for the StreamWriter using the given +// coordinate area and format set. For example, create a table of A1:D5: // -// int -// string +// err := sw.AddTable("A1", "D5", ``) // -func (sw *StreamWriter) SetRow(axis string, slice interface{}) error { - col, row, err := CellNameToCoordinates(axis) +// Create a table of F2:H6 with format set: +// +// err := sw.AddTable("F2", "H6", `{"table_name":"table","table_style":"TableStyleMedium2","show_first_column":true,"show_last_column":true,"show_row_stripes":false,"show_column_stripes":true}`) +// +// Note that the table must be at least two lines including the header. The +// header cells must contain strings and must be unique. +// +// Currently only one table is allowed for a StreamWriter. AddTable must be +// called after the rows are written but before Flush. +// +// See File.AddTable for details on the table format. +func (sw *StreamWriter) AddTable(hcell, vcell, format string) error { + formatSet, err := parseFormatTableSet(format) if err != nil { return err } - // Make sure 'slice' is a Ptr to Slice - v := reflect.ValueOf(slice) - if v.Kind() != reflect.Ptr || v.Elem().Kind() != reflect.Slice { - return errors.New("pointer to slice expected") + + coordinates, err := areaRangeToCoordinates(hcell, vcell) + if err != nil { + return err } - v = v.Elem() - sw.SheetData.WriteString(fmt.Sprintf(``, row)) - for i := 0; i < v.Len(); i++ { - axis, err := CoordinatesToCellName(col+i, row) + sortCoordinates(coordinates) + + // Correct the minimum number of rows, the table at least two lines. + if coordinates[1] == coordinates[3] { + coordinates[3]++ + } + + // Correct table reference coordinate area, such correct C1:B3 to B1:C3. + ref, err := sw.File.coordinatesToAreaRef(coordinates) + if err != nil { + return err + } + + // create table columns using the first row + tableHeaders, err := sw.getRowValues(coordinates[1], coordinates[0], coordinates[2]) + if err != nil { + return err + } + tableColumn := make([]*xlsxTableColumn, len(tableHeaders)) + for i, name := range tableHeaders { + tableColumn[i] = &xlsxTableColumn{ + ID: i + 1, + Name: name, + } + } + + tableID := sw.File.countTables() + 1 + + name := formatSet.TableName + if name == "" { + name = "Table" + strconv.Itoa(tableID) + } + + table := xlsxTable{ + XMLNS: NameSpaceSpreadSheet, + ID: tableID, + Name: name, + DisplayName: name, + Ref: ref, + AutoFilter: &xlsxAutoFilter{ + Ref: ref, + }, + TableColumns: &xlsxTableColumns{ + Count: len(tableColumn), + TableColumn: tableColumn, + }, + TableStyleInfo: &xlsxTableStyleInfo{ + Name: formatSet.TableStyle, + ShowFirstColumn: formatSet.ShowFirstColumn, + ShowLastColumn: formatSet.ShowLastColumn, + ShowRowStripes: formatSet.ShowRowStripes, + ShowColumnStripes: formatSet.ShowColumnStripes, + }, + } + + sheetRelationshipsTableXML := "../tables/table" + strconv.Itoa(tableID) + ".xml" + tableXML := strings.Replace(sheetRelationshipsTableXML, "..", "xl", -1) + + // Add first table for given sheet. + sheetPath, _ := sw.File.sheetMap[trimSheetName(sw.Sheet)] + sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetPath, "xl/worksheets/") + ".rels" + rID := sw.File.addRels(sheetRels, SourceRelationshipTable, sheetRelationshipsTableXML, "") + + sw.tableParts = fmt.Sprintf(``, rID) + + sw.File.addContentTypePart(tableID, "table") + + b, _ := xml.Marshal(table) + sw.File.saveFileList(tableXML, b) + return nil +} + +// Extract values from a row in the StreamWriter. +func (sw *StreamWriter) getRowValues(hrow, hcol, vcol int) (res []string, err error) { + res = make([]string, vcol-hcol+1) + + r, err := sw.rawData.Reader() + if err != nil { + return nil, err + } + + dec := xml.NewDecoder(r) + for { + token, err := dec.Token() + if err == io.EOF { + return res, nil + } if err != nil { - return err + return nil, err } - switch val := v.Index(i).Interface().(type) { - case int: - sw.SheetData.WriteString(fmt.Sprintf(`%d`, axis, val)) - case string: - sw.SheetData.WriteString(sw.setCellStr(axis, val)) - default: - sw.SheetData.WriteString(sw.setCellStr(axis, fmt.Sprint(val))) + startElement, ok := getRowElement(token, hrow) + if !ok { + continue } - } - sw.SheetData.WriteString(``) - // Try to use local storage - chunk := 1 << 24 - if sw.SheetData.Len() >= chunk { - if sw.tmpFile == nil { - err := sw.createTmp() + // decode cells + var row xlsxRow + if err := dec.DecodeElement(&row, &startElement); err != nil { + return nil, err + } + for _, c := range row.C { + col, _, err := CellNameToCoordinates(c.R) if err != nil { - // can not use local storage - return nil + return nil, err } + if col < hcol || col > vcol { + continue + } + res[col-hcol] = c.V } - // use local storage - _, err := sw.tmpFile.Write(sw.SheetData.Bytes()) - if err != nil { - return nil + return res, nil + } +} + +// Check if the token is an XLSX row with the matching row number. +func getRowElement(token xml.Token, hrow int) (startElement xml.StartElement, ok bool) { + startElement, ok = token.(xml.StartElement) + if !ok { + return + } + ok = startElement.Name.Local == "row" + if !ok { + return + } + ok = false + for _, attr := range startElement.Attr { + if attr.Name.Local != "r" { + continue + } + row, _ := strconv.Atoi(attr.Value) + if row == hrow { + ok = true + return } - sw.SheetData.Reset() } - return err + return } -// Flush ending the streaming writing process. -func (sw *StreamWriter) Flush() error { - sw.SheetData.WriteString(``) +// Cell can be used directly in StreamWriter.SetRow to specify a style and +// a value. +type Cell struct { + StyleID int + Value interface{} +} - ws, err := sw.File.workSheetReader(sw.Sheet) +// SetRow writes an array to stream rows by giving a worksheet name, starting +// coordinate and a pointer to an array of values. Note that you must call the +// 'Flush' method to end the streaming writing process. +// +// As a special case, if Cell is used as a value, then the Cell.StyleID will be +// applied to that cell. +func (sw *StreamWriter) SetRow(axis string, values []interface{}) error { + col, row, err := CellNameToCoordinates(axis) if err != nil { return err } - sheetXML := fmt.Sprintf("xl/worksheets/sheet%d.xml", sw.SheetID) - delete(sw.File.Sheet, sheetXML) - delete(sw.File.checked, sheetXML) - var sheetDataByte []byte - if sw.tmpFile != nil { - // close the local storage file - if err = sw.tmpFile.Close(); err != nil { - return err - } - file, err := os.Open(sw.tmpFile.Name()) + fmt.Fprintf(&sw.rawData, ``, row) + for i, val := range values { + axis, err := CoordinatesToCellName(col+i, row) if err != nil { return err } - - sheetDataByte, err = ioutil.ReadAll(file) - if err != nil { - return err + c := xlsxC{R: axis} + if v, ok := val.(Cell); ok { + c.S = v.StyleID + val = v.Value + } else if v, ok := val.(*Cell); ok && v != nil { + c.S = v.StyleID + val = v.Value } - - if err := file.Close(); err != nil { - return err + switch val := val.(type) { + case int: + c.T, c.V = setCellInt(val) + case int8: + c.T, c.V = setCellInt(int(val)) + case int16: + c.T, c.V = setCellInt(int(val)) + case int32: + c.T, c.V = setCellInt(int(val)) + case int64: + c.T, c.V = setCellInt(int(val)) + case uint: + c.T, c.V = setCellInt(int(val)) + case uint8: + c.T, c.V = setCellInt(int(val)) + case uint16: + c.T, c.V = setCellInt(int(val)) + case uint32: + c.T, c.V = setCellInt(int(val)) + case uint64: + c.T, c.V = setCellInt(int(val)) + case float32: + c.T, c.V = setCellFloat(float64(val), -1, 32) + case float64: + c.T, c.V = setCellFloat(val, -1, 64) + case string: + c.T, c.V, c.XMLSpace = setCellStr(val) + case []byte: + c.T, c.V, c.XMLSpace = setCellStr(string(val)) + case time.Duration: + c.T, c.V = setCellDuration(val) + case time.Time: + c.T, c.V, _, err = setCellTime(val) + case bool: + c.T, c.V = setCellBool(val) + case nil: + c.T, c.V, c.XMLSpace = setCellStr("") + default: + c.T, c.V, c.XMLSpace = setCellStr(fmt.Sprint(val)) } - - err = os.Remove(sw.tmpFile.Name()) if err != nil { return err } + writeCell(&sw.rawData, c) } + sw.rawData.WriteString(``) + return sw.rawData.Sync() +} - sheetDataByte = append(sheetDataByte, sw.SheetData.Bytes()...) - replaceMap := map[string][]byte{ - "XMLName": {}, - "SheetData": sheetDataByte, +func writeCell(buf *bufferedWriter, c xlsxC) { + buf.WriteString(``) + if c.V != "" { + buf.WriteString(``) + xml.EscapeText(buf, []byte(c.V)) + buf.WriteString(``) + } + buf.WriteString(``) } -// createTmp creates a temporary file in the operating system default -// temporary directory. -func (sw *StreamWriter) createTmp() (err error) { - sw.tmpFile, err = ioutil.TempFile(os.TempDir(), "excelize-") - return err +// Flush ending the streaming writing process. +func (sw *StreamWriter) Flush() error { + sw.rawData.WriteString(``) + sw.rawData.WriteString(sw.tableParts) + sw.rawData.WriteString(``) + if err := sw.rawData.Flush(); err != nil { + return err + } + + sheetXML := fmt.Sprintf("xl/worksheets/sheet%d.xml", sw.SheetID) + delete(sw.File.Sheet, sheetXML) + delete(sw.File.checked, sheetXML) + + defer sw.rawData.Close() + b, err := sw.rawData.Bytes() + if err != nil { + return err + } + sw.File.XLSX[sheetXML] = b + return nil } -// StreamMarshalSheet provides method to serialization worksheets by field as -// streaming. -func StreamMarshalSheet(ws *xlsxWorksheet, replaceMap map[string][]byte) []byte { +// bulkAppendOtherFields bulk-appends fields in a worksheet, skipping the +// specified field names. +func bulkAppendOtherFields(w io.Writer, ws *xlsxWorksheet, skip ...string) { + skipMap := make(map[string]struct{}) + for _, name := range skip { + skipMap[name] = struct{}{} + } + s := reflect.ValueOf(ws).Elem() typeOfT := s.Type() - var marshalResult []byte - marshalResult = append(marshalResult, []byte(XMLHeader+``)...) - return marshalResult } -// setCellStr provides a function to set string type value of a cell as -// streaming. Total number of characters that a cell can contain 32767 -// characters. -func (sw *StreamWriter) setCellStr(axis, value string) string { - if len(value) > 32767 { - value = value[0:32767] +// bufferedWriter uses a temp file to store an extended buffer. Writes are +// always made to an in-memory buffer, which will always succeed. The buffer +// is written to the temp file with Sync, which may return an error. Therefore, +// Sync should be periodically called and the error checked. +type bufferedWriter struct { + tmp *os.File + buf bytes.Buffer +} + +// Write to the in-memory buffer. The err is always nil. +func (bw *bufferedWriter) Write(p []byte) (n int, err error) { + return bw.buf.Write(p) +} + +// WriteString wites to the in-memory buffer. The err is always nil. +func (bw *bufferedWriter) WriteString(p string) (n int, err error) { + return bw.buf.WriteString(p) +} + +// Reader provides read-access to the underlying buffer/file. +func (bw *bufferedWriter) Reader() (io.Reader, error) { + if bw.tmp == nil { + return bytes.NewReader(bw.buf.Bytes()), nil + } + if err := bw.Flush(); err != nil { + return nil, err + } + fi, err := bw.tmp.Stat() + if err != nil { + return nil, err } - // Leading and ending space(s) character detection. - if len(value) > 0 && (value[0] == 32 || value[len(value)-1] == 32) { - return fmt.Sprintf(`%s`, axis, value) + // os.File.ReadAt does not affect the cursor position and is safe to use here + return io.NewSectionReader(bw.tmp, 0, fi.Size()), nil +} + +// Bytes returns the entire content of the bufferedWriter. If a temp file is +// used, Bytes will efficiently allocate a buffer to prevent re-allocations. +func (bw *bufferedWriter) Bytes() ([]byte, error) { + if bw.tmp == nil { + return bw.buf.Bytes(), nil + } + + if err := bw.Flush(); err != nil { + return nil, err + } + + var buf bytes.Buffer + if fi, err := bw.tmp.Stat(); err == nil { + if size := fi.Size() + bytes.MinRead; size > bytes.MinRead { + if int64(int(size)) == size { + buf.Grow(int(size)) + } else { + return nil, bytes.ErrTooLarge + } + } + } + + if _, err := bw.tmp.Seek(0, 0); err != nil { + return nil, err + } + + _, err := buf.ReadFrom(bw.tmp) + return buf.Bytes(), err +} + +// Sync will write the in-memory buffer to a temp file, if the in-memory buffer +// has grown large enough. Any error will be returned. +func (bw *bufferedWriter) Sync() (err error) { + // Try to use local storage + const chunk = 1 << 24 + if bw.buf.Len() < chunk { + return nil + } + if bw.tmp == nil { + bw.tmp, err = ioutil.TempFile(os.TempDir(), "excelize-") + if err != nil { + // can not use local storage + return nil + } + } + return bw.Flush() +} + +// Flush the entire in-memory buffer to the temp file, if a temp file is being +// used. +func (bw *bufferedWriter) Flush() error { + if bw.tmp == nil { + return nil + } + _, err := bw.buf.WriteTo(bw.tmp) + if err != nil { + return err + } + bw.buf.Reset() + return nil +} + +// Close the underlying temp file and reset the in-memory buffer. +func (bw *bufferedWriter) Close() error { + bw.buf.Reset() + if bw.tmp == nil { + return nil } - return fmt.Sprintf(`%s`, axis, value) + defer os.Remove(bw.tmp.Name()) + return bw.tmp.Close() } diff --git a/stream_test.go b/stream_test.go index 4482bd1..015f64b 100644 --- a/stream_test.go +++ b/stream_test.go @@ -1,6 +1,8 @@ package excelize import ( + "encoding/xml" + "fmt" "math/rand" "path/filepath" "strings" @@ -9,6 +11,25 @@ import ( "github.com/stretchr/testify/assert" ) +func BenchmarkStreamWriter(b *testing.B) { + file := NewFile() + + row := make([]interface{}, 10) + for colID := 0; colID < 10; colID++ { + row[colID] = colID + } + + for n := 0; n < b.N; n++ { + streamWriter, _ := file.NewStreamWriter("Sheet1") + for rowID := 10; rowID <= 110; rowID++ { + cell, _ := CoordinatesToCellName(1, rowID) + streamWriter.SetRow(cell, row) + } + } + + b.ReportAllocs() +} + func TestStreamWriter(t *testing.T) { file := NewFile() streamWriter, err := file.NewStreamWriter("Sheet1") @@ -17,16 +38,16 @@ func TestStreamWriter(t *testing.T) { // Test max characters in a cell. row := make([]interface{}, 1) row[0] = strings.Repeat("c", 32769) - assert.NoError(t, streamWriter.SetRow("A1", &row)) + assert.NoError(t, streamWriter.SetRow("A1", row)) // Test leading and ending space(s) character characters in a cell. row = make([]interface{}, 1) row[0] = " characters" - assert.NoError(t, streamWriter.SetRow("A2", &row)) + assert.NoError(t, streamWriter.SetRow("A2", row)) row = make([]interface{}, 1) row[0] = []byte("Word") - assert.NoError(t, streamWriter.SetRow("A3", &row)) + assert.NoError(t, streamWriter.SetRow("A3", row)) for rowID := 10; rowID <= 51200; rowID++ { row := make([]interface{}, 50) @@ -34,26 +55,13 @@ func TestStreamWriter(t *testing.T) { row[colID] = rand.Intn(640000) } cell, _ := CoordinatesToCellName(1, rowID) - assert.NoError(t, streamWriter.SetRow(cell, &row)) + assert.NoError(t, streamWriter.SetRow(cell, row)) } assert.NoError(t, streamWriter.Flush()) // Save xlsx file by the given path. assert.NoError(t, file.SaveAs(filepath.Join("test", "TestStreamWriter.xlsx"))) - // Test error exceptions - _, err = file.NewStreamWriter("SheetN") - assert.EqualError(t, err, "sheet SheetN is not exist") -} - -func TestFlush(t *testing.T) { - // Test error exceptions - file := NewFile() - streamWriter, err := file.NewStreamWriter("Sheet1") - assert.NoError(t, err) - streamWriter.Sheet = "SheetN" - assert.EqualError(t, streamWriter.Flush(), "sheet SheetN is not exist") - // Test close temporary file error file = NewFile() streamWriter, err = file.NewStreamWriter("Sheet1") @@ -64,17 +72,49 @@ func TestFlush(t *testing.T) { row[colID] = rand.Intn(640000) } cell, _ := CoordinatesToCellName(1, rowID) - assert.NoError(t, streamWriter.SetRow(cell, &row)) + assert.NoError(t, streamWriter.SetRow(cell, row)) } - assert.NoError(t, streamWriter.tmpFile.Close()) + assert.NoError(t, streamWriter.rawData.Close()) assert.Error(t, streamWriter.Flush()) } +func TestStreamTable(t *testing.T) { + file := NewFile() + streamWriter, err := file.NewStreamWriter("Sheet1") + assert.NoError(t, err) + + // Write some rows. We want enough rows to force a temp file (>16MB). + assert.NoError(t, streamWriter.SetRow("A1", []interface{}{"A", "B", "C"})) + row := []interface{}{1, 2, 3} + for r := 2; r < 10000; r++ { + assert.NoError(t, streamWriter.SetRow(fmt.Sprintf("A%d", r), row)) + } + + // Write a table. + assert.NoError(t, streamWriter.AddTable("A1", "C2", ``)) + assert.NoError(t, streamWriter.Flush()) + + // Verify the table has names. + var table xlsxTable + assert.NoError(t, xml.Unmarshal(file.XLSX["xl/tables/table1.xml"], &table)) + assert.Equal(t, "A", table.TableColumns.TableColumn[0].Name) + assert.Equal(t, "B", table.TableColumns.TableColumn[1].Name) + assert.Equal(t, "C", table.TableColumns.TableColumn[2].Name) +} + +func TestNewStreamWriter(t *testing.T) { + // Test error exceptions + file := NewFile() + _, err := file.NewStreamWriter("Sheet1") + assert.NoError(t, err) + _, err = file.NewStreamWriter("SheetN") + assert.EqualError(t, err, "sheet SheetN is not exist") +} + func TestSetRow(t *testing.T) { // Test error exceptions file := NewFile() streamWriter, err := file.NewStreamWriter("Sheet1") assert.NoError(t, err) - assert.EqualError(t, streamWriter.SetRow("A", &[]interface{}{}), `cannot convert cell "A" to coordinates: invalid cell name "A"`) - assert.EqualError(t, streamWriter.SetRow("A1", []interface{}{}), `pointer to slice expected`) + assert.EqualError(t, streamWriter.SetRow("A", []interface{}{}), `cannot convert cell "A" to coordinates: invalid cell name "A"`) } -- cgit v1.2.1 From 09485b3f9f0aefc58d51462aed65c2416205c591 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 29 Dec 2019 16:02:31 +0800 Subject: Improve code coverage unit tests --- LICENSE | 4 +- README.md | 70 ++++++++++----------------- README_zh.md | 71 ++++++++++----------------- adjust.go | 4 +- adjust_test.go | 4 ++ calcchain.go | 2 +- cell.go | 2 +- cellmerged.go | 18 ++----- chart.go | 4 +- col.go | 2 +- comment.go | 2 +- comment_test.go | 2 +- datavalidation.go | 2 +- datavalidation_test.go | 2 +- date.go | 2 +- docProps.go | 2 +- docProps_test.go | 2 +- errors.go | 2 +- excelize.go | 2 +- excelize_test.go | 113 ------------------------------------------- file.go | 2 +- lib.go | 2 +- picture.go | 2 +- pivotTable.go | 2 +- rows.go | 2 +- shape.go | 2 +- shape_test.go | 28 +++++++++++ sheet.go | 2 +- sheetpr.go | 2 +- sheetview.go | 2 +- sparkline.go | 2 +- stream.go | 127 +++++++++++++++++++++++++++++-------------------- stream_test.go | 49 ++++++++++++++++++- styles.go | 2 +- styles_test.go | 4 +- table.go | 2 +- table_test.go | 125 ++++++++++++++++++++++++++++++++++++++++++++++++ templates.go | 2 +- vmlDrawing.go | 2 +- xmlApp.go | 2 +- xmlCalcChain.go | 2 +- xmlChart.go | 2 +- xmlComments.go | 2 +- xmlContentTypes.go | 2 +- xmlCore.go | 2 +- xmlDecodeDrawing.go | 2 +- xmlDrawing.go | 2 +- xmlPivotTable.go | 2 +- xmlSharedStrings.go | 2 +- xmlStyles.go | 2 +- xmlTable.go | 2 +- xmlTheme.go | 2 +- xmlWorkbook.go | 2 +- xmlWorksheet.go | 2 +- 54 files changed, 383 insertions(+), 320 deletions(-) create mode 100644 shape_test.go create mode 100644 table_test.go diff --git a/LICENSE b/LICENSE index 1962b4a..51ec1fb 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,7 @@ BSD 3-Clause License -Copyright (c) 2016-2019, 360 Enterprise Security Group, Endpoint Security, Inc. -Copyright (c) 2011-2017, Geoffrey J. Teale (complying with the tealeg/xlsx license) +Copyright (c) 2016-2020, 360 Enterprise Security Group, Endpoint Security, Inc. +Copyright (c) 2011-2017, Geoffrey J. Teale All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/README.md b/README.md index 03e33ae..d7f696c 100644 --- a/README.md +++ b/README.md @@ -31,11 +31,7 @@ Here is a minimal example usage that will create XLSX file. ```go package main -import ( - "fmt" - - "github.com/360EntSecGroup-Skylar/excelize" -) +import "github.com/360EntSecGroup-Skylar/excelize" func main() { f := excelize.NewFile() @@ -47,9 +43,8 @@ func main() { // Set active sheet of the workbook. f.SetActiveSheet(index) // Save xlsx file by the given path. - err := f.SaveAs("./Book1.xlsx") - if err != nil { - fmt.Println(err) + if err := f.SaveAs("Book1.xlsx"); err != nil { + println(err.Error()) } } ``` @@ -61,32 +56,28 @@ The following constitutes the bare to read a XLSX document. ```go package main -import ( - "fmt" - - "github.com/360EntSecGroup-Skylar/excelize" -) +import "github.com/360EntSecGroup-Skylar/excelize" func main() { - f, err := excelize.OpenFile("./Book1.xlsx") + f, err := excelize.OpenFile("Book1.xlsx") if err != nil { - fmt.Println(err) + println(err.Error()) return } // Get value from cell by given worksheet name and axis. cell, err := f.GetCellValue("Sheet1", "B2") if err != nil { - fmt.Println(err) + println(err.Error()) return } - fmt.Println(cell) + println(cell) // Get all the rows in the Sheet1. rows, err := f.GetRows("Sheet1") for _, row := range rows { for _, colCell := range row { - fmt.Print(colCell, "\t") + print(colCell, "\t") } - fmt.Println() + println() } } ``` @@ -100,11 +91,7 @@ With Excelize chart generation and management is as easy as a few lines of code. ```go package main -import ( - "fmt" - - "github.com/360EntSecGroup-Skylar/excelize" -) +import "github.com/360EntSecGroup-Skylar/excelize" func main() { categories := map[string]string{"A2": "Small", "A3": "Normal", "A4": "Large", "B1": "Apple", "C1": "Orange", "D1": "Pear"} @@ -116,15 +103,13 @@ func main() { for k, v := range values { f.SetCellValue("Sheet1", k, v) } - err := f.AddChart("Sheet1", "E1", `{"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"}}`) - if err != nil { - fmt.Println(err) + if err := f.AddChart("Sheet1", "E1", `{"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"}}`); err != nil { + println(err.Error()) return } // Save xlsx file by the given path. - err = f.SaveAs("./Book1.xlsx") - if err != nil { - fmt.Println(err) + if err := f.SaveAs("Book1.xlsx"); err != nil { + println(err.Error()) } } ``` @@ -135,7 +120,6 @@ func main() { package main import ( - "fmt" _ "image/gif" _ "image/jpeg" _ "image/png" @@ -144,30 +128,26 @@ import ( ) func main() { - f, err := excelize.OpenFile("./Book1.xlsx") + f, err := excelize.OpenFile("Book1.xlsx") if err != nil { - fmt.Println(err) + println(err.Error()) return } // Insert a picture. - err = f.AddPicture("Sheet1", "A2", "./image1.png", "") - if err != nil { - fmt.Println(err) + if err := f.AddPicture("Sheet1", "A2", "image.png", ""); err != nil { + println(err.Error()) } // Insert a picture to worksheet with scaling. - err = f.AddPicture("Sheet1", "D2", "./image2.jpg", `{"x_scale": 0.5, "y_scale": 0.5}`) - if err != nil { - fmt.Println(err) + if err := f.AddPicture("Sheet1", "D2", "image.jpg", `{"x_scale": 0.5, "y_scale": 0.5}`); err != nil { + println(err.Error()) } // Insert a picture offset in the cell with printing support. - err = f.AddPicture("Sheet1", "H2", "./image3.gif", `{"x_offset": 15, "y_offset": 10, "print_obj": true, "lock_aspect_ratio": false, "locked": false}`) - if err != nil { - fmt.Println(err) + if err := f.AddPicture("Sheet1", "H2", "image.gif", `{"x_offset": 15, "y_offset": 10, "print_obj": true, "lock_aspect_ratio": false, "locked": false}`); err != nil { + println(err.Error()) } // Save the xlsx file with the origin path. - err = f.Save() - if err != nil { - fmt.Println(err) + if err = f.Save(); err != nil { + println(err.Error()) } } ``` diff --git a/README_zh.md b/README_zh.md index 57cd645..c1ee83e 100644 --- a/README_zh.md +++ b/README_zh.md @@ -30,11 +30,7 @@ go get github.com/360EntSecGroup-Skylar/excelize ```go package main -import ( - "fmt" - - "github.com/360EntSecGroup-Skylar/excelize" -) +import "github.com/360EntSecGroup-Skylar/excelize" func main() { f := excelize.NewFile() @@ -46,9 +42,8 @@ func main() { // 设置工作簿的默认工作表 f.SetActiveSheet(index) // 根据指定路径保存文件 - err := f.SaveAs("./Book1.xlsx") - if err != nil { - fmt.Println(err) + if err := f.SaveAs("Book1.xlsx"); err != nil { + println(err.Error()) } } ``` @@ -60,32 +55,28 @@ func main() { ```go package main -import ( - "fmt" - - "github.com/360EntSecGroup-Skylar/excelize" -) +import "github.com/360EntSecGroup-Skylar/excelize" func main() { - f, err := excelize.OpenFile("./Book1.xlsx") + f, err := excelize.OpenFile("Book1.xlsx") if err != nil { - fmt.Println(err) + println(err.Error()) return } // 获取工作表中指定单元格的值 cell, err := f.GetCellValue("Sheet1", "B2") if err != nil { - fmt.Println(err) + println(err.Error()) return } - fmt.Println(cell) + println(cell) // 获取 Sheet1 上所有单元格 rows, err := f.GetRows("Sheet1") for _, row := range rows { for _, colCell := range row { - fmt.Print(colCell, "\t") + print(colCell, "\t") } - fmt.Println() + println() } } ``` @@ -99,11 +90,7 @@ func main() { ```go package main -import ( - "fmt" - - "github.com/360EntSecGroup-Skylar/excelize" -) +import "github.com/360EntSecGroup-Skylar/excelize" func main() { categories := map[string]string{"A2": "Small", "A3": "Normal", "A4": "Large", "B1": "Apple", "C1": "Orange", "D1": "Pear"} @@ -115,18 +102,15 @@ func main() { for k, v := range values { f.SetCellValue("Sheet1", k, v) } - err := f.AddChart("Sheet1", "E1", `{"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"}}`) - if err != nil { - fmt.Println(err) + if err := f.AddChart("Sheet1", "E1", `{"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"}}`); err != nil { + println(err.Error()) return } // 根据指定路径保存文件 - err = f.SaveAs("./Book1.xlsx") - if err != nil { - fmt.Println(err) + if err := f.SaveAs("Book1.xlsx"); err != nil { + println(err.Error()) } } - ``` ### 向 Excel 文档中插入图片 @@ -135,7 +119,6 @@ func main() { package main import ( - "fmt" _ "image/gif" _ "image/jpeg" _ "image/png" @@ -144,30 +127,26 @@ import ( ) func main() { - f, err := excelize.OpenFile("./Book1.xlsx") + f, err := excelize.OpenFile("Book1.xlsx") if err != nil { - fmt.Println(err) + println(err.Error()) return } // 插入图片 - err = f.AddPicture("Sheet1", "A2", "./image1.png", "") - if err != nil { - fmt.Println(err) + if err := f.AddPicture("Sheet1", "A2", "image.png", ""); err != nil { + println(err.Error()) } // 在工作表中插入图片,并设置图片的缩放比例 - err = f.AddPicture("Sheet1", "D2", "./image2.jpg", `{"x_scale": 0.5, "y_scale": 0.5}`) - if err != nil { - fmt.Println(err) + if err := f.AddPicture("Sheet1", "D2", "image.jpg", `{"x_scale": 0.5, "y_scale": 0.5}`); err != nil { + println(err.Error()) } // 在工作表中插入图片,并设置图片的打印属性 - err = f.AddPicture("Sheet1", "H2", "./image3.gif", `{"x_offset": 15, "y_offset": 10, "print_obj": true, "lock_aspect_ratio": false, "locked": false}`) - if err != nil { - fmt.Println(err) + if err := f.AddPicture("Sheet1", "H2", "image.gif", `{"x_offset": 15, "y_offset": 10, "print_obj": true, "lock_aspect_ratio": false, "locked": false}`); err != nil { + println(err.Error()) } // 保存文件 - err = f.Save() - if err != nil { - fmt.Println(err) + if err = f.Save(); err != nil { + println(err.Error()) } } ``` diff --git a/adjust.go b/adjust.go index 69ded1b..bedeec0 100644 --- a/adjust.go +++ b/adjust.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // @@ -213,6 +213,8 @@ func areaRangeToCoordinates(firstCell, lastCell string) ([]int, error) { return coordinates, err } +// sortCoordinates provides a function to correct the coordinate area, such +// correct C1:B3 to B1:C3. func sortCoordinates(coordinates []int) error { if len(coordinates) != 4 { return errors.New("coordinates length must be 4") diff --git a/adjust_test.go b/adjust_test.go index a0de844..13e47ff 100644 --- a/adjust_test.go +++ b/adjust_test.go @@ -114,3 +114,7 @@ func TestCoordinatesToAreaRef(t *testing.T) { assert.NoError(t, err) assert.EqualValues(t, ref, "A1:A1") } + +func TestSortCoordinates(t *testing.T) { + assert.EqualError(t, sortCoordinates(make([]int, 3)), "coordinates length must be 4") +} diff --git a/calcchain.go b/calcchain.go index a3d3820..f50fb1d 100644 --- a/calcchain.go +++ b/calcchain.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // diff --git a/cell.go b/cell.go index 1aeddc1..a659680 100644 --- a/cell.go +++ b/cell.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // diff --git a/cellmerged.go b/cellmerged.go index 5bea0bc..b952a1e 100644 --- a/cellmerged.go +++ b/cellmerged.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // @@ -43,13 +43,7 @@ func (f *File) MergeCell(sheet, hcell, vcell string) error { return err } // Correct the coordinate area, such correct C1:B3 to B1:C3. - if rect1[2] < rect1[0] { - rect1[0], rect1[2] = rect1[2], rect1[0] - } - - if rect1[3] < rect1[1] { - rect1[1], rect1[3] = rect1[3], rect1[1] - } + _ = sortCoordinates(rect1) hcell, _ = CoordinatesToCellName(rect1[0], rect1[1]) vcell, _ = CoordinatesToCellName(rect1[2], rect1[3]) @@ -123,12 +117,8 @@ func (f *File) UnmergeCell(sheet string, hcell, vcell string) error { return err } - if rect1[2] < rect1[0] { - rect1[0], rect1[2] = rect1[2], rect1[0] - } - if rect1[3] < rect1[1] { - rect1[1], rect1[3] = rect1[3], rect1[1] - } + // Correct the coordinate area, such correct C1:B3 to B1:C3. + _ = sortCoordinates(rect1) // return nil since no MergeCells in the sheet if xlsx.MergeCells == nil { diff --git a/chart.go b/chart.go index 8b38d22..b5ff3d1 100644 --- a/chart.go +++ b/chart.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // @@ -657,6 +657,7 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // // major_grid_lines // minor_grid_lines +// tick_label_skip // reverse_order // maximum // minimum @@ -666,7 +667,6 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // major_grid_lines // minor_grid_lines // major_unit -// tick_label_skip // reverse_order // maximum // minimum diff --git a/col.go b/col.go index 5d4e764..f7e6bcd 100644 --- a/col.go +++ b/col.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // diff --git a/comment.go b/comment.go index 486a035..a5b6085 100644 --- a/comment.go +++ b/comment.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // diff --git a/comment_test.go b/comment_test.go index dd07951..5b83162 100644 --- a/comment_test.go +++ b/comment_test.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // diff --git a/datavalidation.go b/datavalidation.go index 2499035..8b95b40 100644 --- a/datavalidation.go +++ b/datavalidation.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // diff --git a/datavalidation_test.go b/datavalidation_test.go index 7e54d55..c245df3 100644 --- a/datavalidation_test.go +++ b/datavalidation_test.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // diff --git a/date.go b/date.go index 8f63702..dad39b5 100644 --- a/date.go +++ b/date.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // diff --git a/docProps.go b/docProps.go index 884eb63..a61ee71 100644 --- a/docProps.go +++ b/docProps.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // diff --git a/docProps_test.go b/docProps_test.go index 30c3149..ef930ae 100644 --- a/docProps_test.go +++ b/docProps_test.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // diff --git a/errors.go b/errors.go index 8520a01..4560497 100644 --- a/errors.go +++ b/errors.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // diff --git a/excelize.go b/excelize.go index 94f401c..8fbd315 100644 --- a/excelize.go +++ b/excelize.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. diff --git a/excelize_test.go b/excelize_test.go index c4b600d..ea82828 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -894,124 +894,11 @@ func TestCopySheetError(t *testing.T) { assert.NoError(t, f.SaveAs(filepath.Join("test", "TestCopySheetError.xlsx"))) } -func TestAddTable(t *testing.T) { - f, err := prepareTestBook1() - if !assert.NoError(t, err) { - t.FailNow() - } - - err = f.AddTable("Sheet1", "B26", "A21", `{}`) - if !assert.NoError(t, err) { - t.FailNow() - } - - err = f.AddTable("Sheet2", "A2", "B5", `{"table_name":"table","table_style":"TableStyleMedium2", "show_first_column":true,"show_last_column":true,"show_row_stripes":false,"show_column_stripes":true}`) - if !assert.NoError(t, err) { - t.FailNow() - } - - err = f.AddTable("Sheet2", "F1", "F1", `{"table_style":"TableStyleMedium8"}`) - if !assert.NoError(t, err) { - t.FailNow() - } - - // Test add table with illegal formatset. - assert.EqualError(t, f.AddTable("Sheet1", "B26", "A21", `{x}`), "invalid character 'x' looking for beginning of object key string") - // Test add table with illegal cell coordinates. - assert.EqualError(t, f.AddTable("Sheet1", "A", "B1", `{}`), `cannot convert cell "A" to coordinates: invalid cell name "A"`) - assert.EqualError(t, f.AddTable("Sheet1", "A1", "B", `{}`), `cannot convert cell "B" to coordinates: invalid cell name "B"`) - - assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddTable.xlsx"))) - - // Test addTable with illegal cell coordinates. - f = NewFile() - assert.EqualError(t, f.addTable("sheet1", "", 0, 0, 0, 0, 0, nil), "invalid cell coordinates [0, 0]") - assert.EqualError(t, f.addTable("sheet1", "", 1, 1, 0, 0, 0, nil), "invalid cell coordinates [0, 0]") -} - -func TestAddShape(t *testing.T) { - f, err := prepareTestBook1() - if !assert.NoError(t, err) { - t.FailNow() - } - - assert.NoError(t, f.AddShape("Sheet1", "A30", `{"type":"rect","paragraph":[{"text":"Rectangle","font":{"color":"CD5C5C"}},{"text":"Shape","font":{"bold":true,"color":"2980B9"}}]}`)) - assert.NoError(t, f.AddShape("Sheet1", "B30", `{"type":"rect","paragraph":[{"text":"Rectangle"},{}]}`)) - assert.NoError(t, f.AddShape("Sheet1", "C30", `{"type":"rect","paragraph":[]}`)) - assert.EqualError(t, f.AddShape("Sheet3", "H1", `{"type":"ellipseRibbon", "color":{"line":"#4286f4","fill":"#8eb9ff"}, "paragraph":[{"font":{"bold":true,"italic":true,"family":"Times New Roman","size":36,"color":"#777777","underline":"single"}}], "height": 90}`), "sheet Sheet3 is not exist") - assert.EqualError(t, f.AddShape("Sheet3", "H1", ""), "unexpected end of JSON input") - assert.EqualError(t, f.AddShape("Sheet1", "A", `{"type":"rect","paragraph":[{"text":"Rectangle","font":{"color":"CD5C5C"}},{"text":"Shape","font":{"bold":true,"color":"2980B9"}}]}`), `cannot convert cell "A" to coordinates: invalid cell name "A"`) - assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddShape1.xlsx"))) - - // Test add first shape for given sheet. - f = NewFile() - assert.NoError(t, f.AddShape("Sheet1", "A1", `{"type":"ellipseRibbon", "color":{"line":"#4286f4","fill":"#8eb9ff"}, "paragraph":[{"font":{"bold":true,"italic":true,"family":"Times New Roman","size":36,"color":"#777777","underline":"single"}}], "height": 90}`)) - assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddShape2.xlsx"))) -} - func TestGetSheetComments(t *testing.T) { f := NewFile() assert.Equal(t, "", f.getSheetComments(0)) } -func TestAutoFilter(t *testing.T) { - outFile := filepath.Join("test", "TestAutoFilter%d.xlsx") - - f, err := prepareTestBook1() - if !assert.NoError(t, err) { - t.FailNow() - } - - formats := []string{ - ``, - `{"column":"B","expression":"x != blanks"}`, - `{"column":"B","expression":"x == blanks"}`, - `{"column":"B","expression":"x != nonblanks"}`, - `{"column":"B","expression":"x == nonblanks"}`, - `{"column":"B","expression":"x <= 1 and x >= 2"}`, - `{"column":"B","expression":"x == 1 or x == 2"}`, - `{"column":"B","expression":"x == 1 or x == 2*"}`, - } - - for i, format := range formats { - t.Run(fmt.Sprintf("Expression%d", i+1), func(t *testing.T) { - err = f.AutoFilter("Sheet1", "D4", "B1", format) - assert.NoError(t, err) - assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, i+1))) - }) - } - - // testing AutoFilter with illegal cell coordinates. - assert.EqualError(t, f.AutoFilter("Sheet1", "A", "B1", ""), `cannot convert cell "A" to coordinates: invalid cell name "A"`) - assert.EqualError(t, f.AutoFilter("Sheet1", "A1", "B", ""), `cannot convert cell "B" to coordinates: invalid cell name "B"`) -} - -func TestAutoFilterError(t *testing.T) { - outFile := filepath.Join("test", "TestAutoFilterError%d.xlsx") - - f, err := prepareTestBook1() - if !assert.NoError(t, err) { - t.FailNow() - } - - formats := []string{ - `{"column":"B","expression":"x <= 1 and x >= blanks"}`, - `{"column":"B","expression":"x -- y or x == *2*"}`, - `{"column":"B","expression":"x != y or x ? *2"}`, - `{"column":"B","expression":"x -- y o r x == *2"}`, - `{"column":"B","expression":"x -- y"}`, - `{"column":"A","expression":"x -- y"}`, - } - for i, format := range formats { - t.Run(fmt.Sprintf("Expression%d", i+1), func(t *testing.T) { - err = f.AutoFilter("Sheet3", "D4", "B1", format) - if assert.Error(t, err) { - assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, i+1))) - } - }) - } -} - func TestSetActiveSheet(t *testing.T) { f := NewFile() f.WorkBook.BookViews = nil diff --git a/file.go b/file.go index d8f10fa..6213bb1 100644 --- a/file.go +++ b/file.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // diff --git a/lib.go b/lib.go index 86f8d16..2d606fa 100644 --- a/lib.go +++ b/lib.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // diff --git a/picture.go b/picture.go index 01df849..80b0a52 100644 --- a/picture.go +++ b/picture.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // diff --git a/pivotTable.go b/pivotTable.go index 6045e41..8610280 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // diff --git a/rows.go b/rows.go index 687828c..20b4379 100644 --- a/rows.go +++ b/rows.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // diff --git a/shape.go b/shape.go index 2ea66ea..e9bdb42 100644 --- a/shape.go +++ b/shape.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // diff --git a/shape_test.go b/shape_test.go new file mode 100644 index 0000000..61fb443 --- /dev/null +++ b/shape_test.go @@ -0,0 +1,28 @@ +package excelize + +import ( + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestAddShape(t *testing.T) { + f, err := prepareTestBook1() + if !assert.NoError(t, err) { + t.FailNow() + } + + assert.NoError(t, f.AddShape("Sheet1", "A30", `{"type":"rect","paragraph":[{"text":"Rectangle","font":{"color":"CD5C5C"}},{"text":"Shape","font":{"bold":true,"color":"2980B9"}}]}`)) + assert.NoError(t, f.AddShape("Sheet1", "B30", `{"type":"rect","paragraph":[{"text":"Rectangle"},{}]}`)) + assert.NoError(t, f.AddShape("Sheet1", "C30", `{"type":"rect","paragraph":[]}`)) + assert.EqualError(t, f.AddShape("Sheet3", "H1", `{"type":"ellipseRibbon", "color":{"line":"#4286f4","fill":"#8eb9ff"}, "paragraph":[{"font":{"bold":true,"italic":true,"family":"Times New Roman","size":36,"color":"#777777","underline":"single"}}], "height": 90}`), "sheet Sheet3 is not exist") + assert.EqualError(t, f.AddShape("Sheet3", "H1", ""), "unexpected end of JSON input") + assert.EqualError(t, f.AddShape("Sheet1", "A", `{"type":"rect","paragraph":[{"text":"Rectangle","font":{"color":"CD5C5C"}},{"text":"Shape","font":{"bold":true,"color":"2980B9"}}]}`), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddShape1.xlsx"))) + + // Test add first shape for given sheet. + f = NewFile() + assert.NoError(t, f.AddShape("Sheet1", "A1", `{"type":"ellipseRibbon", "color":{"line":"#4286f4","fill":"#8eb9ff"}, "paragraph":[{"font":{"bold":true,"italic":true,"family":"Times New Roman","size":36,"color":"#777777","underline":"single"}}], "height": 90}`)) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddShape2.xlsx"))) +} diff --git a/sheet.go b/sheet.go index 954de5b..2654b8f 100644 --- a/sheet.go +++ b/sheet.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // diff --git a/sheetpr.go b/sheetpr.go index 086bd3a..350e189 100644 --- a/sheetpr.go +++ b/sheetpr.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // diff --git a/sheetview.go b/sheetview.go index 8a5091f..fa3cfdf 100644 --- a/sheetview.go +++ b/sheetview.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // diff --git a/sparkline.go b/sparkline.go index 47c8d5a..ef99da6 100644 --- a/sparkline.go +++ b/sparkline.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // diff --git a/stream.go b/stream.go index e981f78..9facf31 100644 --- a/stream.go +++ b/stream.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // @@ -36,20 +36,27 @@ type StreamWriter struct { // rows, you must call the 'Flush' method to end the streaming writing // process and ensure that the order of line numbers is ascending. For // example, set data for worksheet of size 102400 rows x 50 columns with -// numbers: +// numbers and style: // // file := excelize.NewFile() // streamWriter, err := file.NewStreamWriter("Sheet1") // if err != nil { // panic(err) // } -// for rowID := 1; rowID <= 102400; rowID++ { +// styleID, err := file.NewStyle(`{"font":{"color":"#777777"}}`) +// if err != nil { +// panic(err) +// } +// if err := streamWriter.SetRow("A1", []interface{}{excelize.Cell{StyleID: styleID, Value: "Data"}}); err != nil { +// panic(err) +// } +// for rowID := 2; rowID <= 102400; rowID++ { // row := make([]interface{}, 50) // for colID := 0; colID < 50; colID++ { // row[colID] = rand.Intn(640000) // } // cell, _ := excelize.CoordinatesToCellName(1, rowID) -// if err := streamWriter.SetRow(cell, row, nil); err != nil { +// if err := streamWriter.SetRow(cell, row); err != nil { // panic(err) // } // } @@ -107,7 +114,7 @@ func (sw *StreamWriter) AddTable(hcell, vcell, format string) error { if err != nil { return err } - sortCoordinates(coordinates) + _ = sortCoordinates(coordinates) // Correct the minimum number of rows, the table at least two lines. if coordinates[1] == coordinates[3] { @@ -188,7 +195,7 @@ func (sw *StreamWriter) getRowValues(hrow, hcol, vcol int) (res []string, err er return nil, err } - dec := xml.NewDecoder(r) + dec := sw.File.xmlNewDecoder(r) for { token, err := dec.Token() if err == io.EOF { @@ -248,7 +255,7 @@ func getRowElement(token xml.Token, hrow int) (startElement xml.StartElement, ok // a value. type Cell struct { StyleID int - Value interface{} + Value interface{} } // SetRow writes an array to stream rows by giving a worksheet name, starting @@ -277,47 +284,8 @@ func (sw *StreamWriter) SetRow(axis string, values []interface{}) error { c.S = v.StyleID val = v.Value } - switch val := val.(type) { - case int: - c.T, c.V = setCellInt(val) - case int8: - c.T, c.V = setCellInt(int(val)) - case int16: - c.T, c.V = setCellInt(int(val)) - case int32: - c.T, c.V = setCellInt(int(val)) - case int64: - c.T, c.V = setCellInt(int(val)) - case uint: - c.T, c.V = setCellInt(int(val)) - case uint8: - c.T, c.V = setCellInt(int(val)) - case uint16: - c.T, c.V = setCellInt(int(val)) - case uint32: - c.T, c.V = setCellInt(int(val)) - case uint64: - c.T, c.V = setCellInt(int(val)) - case float32: - c.T, c.V = setCellFloat(float64(val), -1, 32) - case float64: - c.T, c.V = setCellFloat(val, -1, 64) - case string: - c.T, c.V, c.XMLSpace = setCellStr(val) - case []byte: - c.T, c.V, c.XMLSpace = setCellStr(string(val)) - case time.Duration: - c.T, c.V = setCellDuration(val) - case time.Time: - c.T, c.V, _, err = setCellTime(val) - case bool: - c.T, c.V = setCellBool(val) - case nil: - c.T, c.V, c.XMLSpace = setCellStr("") - default: - c.T, c.V, c.XMLSpace = setCellStr(fmt.Sprint(val)) - } - if err != nil { + if err = setCellValFunc(&c, val); err != nil { + sw.rawData.WriteString(``) return err } writeCell(&sw.rawData, c) @@ -326,6 +294,61 @@ func (sw *StreamWriter) SetRow(axis string, values []interface{}) error { return sw.rawData.Sync() } +// setCellValFunc provides a function to set value of a cell. +func setCellValFunc(c *xlsxC, val interface{}) (err error) { + switch val := val.(type) { + case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: + err = setCellIntFunc(c, val) + case float32: + c.T, c.V = setCellFloat(float64(val), -1, 32) + case float64: + c.T, c.V = setCellFloat(val, -1, 64) + case string: + c.T, c.V, c.XMLSpace = setCellStr(val) + case []byte: + c.T, c.V, c.XMLSpace = setCellStr(string(val)) + case time.Duration: + c.T, c.V = setCellDuration(val) + case time.Time: + c.T, c.V, _, err = setCellTime(val) + case bool: + c.T, c.V = setCellBool(val) + case nil: + c.T, c.V, c.XMLSpace = setCellStr("") + default: + c.T, c.V, c.XMLSpace = setCellStr(fmt.Sprint(val)) + } + return err +} + +// setCellIntFunc is a wrapper of SetCellInt. +func setCellIntFunc(c *xlsxC, val interface{}) (err error) { + switch val := val.(type) { + case int: + c.T, c.V = setCellInt(val) + case int8: + c.T, c.V = setCellInt(int(val)) + case int16: + c.T, c.V = setCellInt(int(val)) + case int32: + c.T, c.V = setCellInt(int(val)) + case int64: + c.T, c.V = setCellInt(int(val)) + case uint: + c.T, c.V = setCellInt(int(val)) + case uint8: + c.T, c.V = setCellInt(int(val)) + case uint16: + c.T, c.V = setCellInt(int(val)) + case uint32: + c.T, c.V = setCellInt(int(val)) + case uint64: + c.T, c.V = setCellInt(int(val)) + default: + } + return +} + func writeCell(buf *bufferedWriter, c xlsxC) { buf.WriteString(`= 2"}`, + `{"column":"B","expression":"x == 1 or x == 2"}`, + `{"column":"B","expression":"x == 1 or x == 2*"}`, + } + + for i, format := range formats { + t.Run(fmt.Sprintf("Expression%d", i+1), func(t *testing.T) { + err = f.AutoFilter("Sheet1", "D4", "B1", format) + assert.NoError(t, err) + assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, i+1))) + }) + } + + // testing AutoFilter with illegal cell coordinates. + assert.EqualError(t, f.AutoFilter("Sheet1", "A", "B1", ""), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + assert.EqualError(t, f.AutoFilter("Sheet1", "A1", "B", ""), `cannot convert cell "B" to coordinates: invalid cell name "B"`) +} + +func TestAutoFilterError(t *testing.T) { + outFile := filepath.Join("test", "TestAutoFilterError%d.xlsx") + + f, err := prepareTestBook1() + if !assert.NoError(t, err) { + t.FailNow() + } + + formats := []string{ + `{"column":"B","expression":"x <= 1 and x >= blanks"}`, + `{"column":"B","expression":"x -- y or x == *2*"}`, + `{"column":"B","expression":"x != y or x ? *2"}`, + `{"column":"B","expression":"x -- y o r x == *2"}`, + `{"column":"B","expression":"x -- y"}`, + `{"column":"A","expression":"x -- y"}`, + } + for i, format := range formats { + t.Run(fmt.Sprintf("Expression%d", i+1), func(t *testing.T) { + err = f.AutoFilter("Sheet3", "D4", "B1", format) + if assert.Error(t, err) { + assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, i+1))) + } + }) + } + + assert.EqualError(t, f.autoFilter("Sheet1", "A1", 1, 1, &formatAutoFilter{ + Column: "-", + Expression: "-", + }), `invalid column name "-"`) + assert.EqualError(t, f.autoFilter("Sheet1", "A1", 1, 100, &formatAutoFilter{ + Column: "A", + Expression: "-", + }), `incorrect index of column 'A'`) + assert.EqualError(t, f.autoFilter("Sheet1", "A1", 1, 1, &formatAutoFilter{ + Column: "A", + Expression: "-", + }), `incorrect number of tokens in criteria '-'`) +} + +func TestParseFilterTokens(t *testing.T) { + f := NewFile() + // Test with unknown operator. + _, _, err := f.parseFilterTokens("", []string{"", "!"}) + assert.EqualError(t, err, "unknown operator: !") + // Test invalid operator in context. + _, _, err = f.parseFilterTokens("", []string{"", "<", "x != blanks"}) + assert.EqualError(t, err, "the operator '<' in expression '' is not valid in relation to Blanks/NonBlanks'") +} diff --git a/templates.go b/templates.go index 5b79b0c..a7972e6 100644 --- a/templates.go +++ b/templates.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // diff --git a/vmlDrawing.go b/vmlDrawing.go index 24b615f..f2d55f1 100644 --- a/vmlDrawing.go +++ b/vmlDrawing.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // diff --git a/xmlApp.go b/xmlApp.go index 48450e3..5668cf6 100644 --- a/xmlApp.go +++ b/xmlApp.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // diff --git a/xmlCalcChain.go b/xmlCalcChain.go index 343f15f..69d5d8c 100644 --- a/xmlCalcChain.go +++ b/xmlCalcChain.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // diff --git a/xmlChart.go b/xmlChart.go index fc38dab..b6d041e 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // diff --git a/xmlComments.go b/xmlComments.go index f13d002..687c486 100644 --- a/xmlComments.go +++ b/xmlComments.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // diff --git a/xmlContentTypes.go b/xmlContentTypes.go index fa4d347..7acfe08 100644 --- a/xmlContentTypes.go +++ b/xmlContentTypes.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // diff --git a/xmlCore.go b/xmlCore.go index 96482fc..6f71a3e 100644 --- a/xmlCore.go +++ b/xmlCore.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // diff --git a/xmlDecodeDrawing.go b/xmlDecodeDrawing.go index e11bb00..93e0e82 100644 --- a/xmlDecodeDrawing.go +++ b/xmlDecodeDrawing.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // diff --git a/xmlDrawing.go b/xmlDrawing.go index 1c24f08..5bb5977 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // diff --git a/xmlPivotTable.go b/xmlPivotTable.go index 6e1dfb8..82bbf27 100644 --- a/xmlPivotTable.go +++ b/xmlPivotTable.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // diff --git a/xmlSharedStrings.go b/xmlSharedStrings.go index 7983741..61e5727 100644 --- a/xmlSharedStrings.go +++ b/xmlSharedStrings.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // diff --git a/xmlStyles.go b/xmlStyles.go index 16a89ab..0313008 100644 --- a/xmlStyles.go +++ b/xmlStyles.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // diff --git a/xmlTable.go b/xmlTable.go index 017bda1..345337f 100644 --- a/xmlTable.go +++ b/xmlTable.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // diff --git a/xmlTheme.go b/xmlTheme.go index f764c20..76f13b4 100644 --- a/xmlTheme.go +++ b/xmlTheme.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // diff --git a/xmlWorkbook.go b/xmlWorkbook.go index 65606b0..bc59924 100644 --- a/xmlWorkbook.go +++ b/xmlWorkbook.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // diff --git a/xmlWorksheet.go b/xmlWorksheet.go index c17d12f..ed304cc 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // -- cgit v1.2.1 From 5f5ec76740704a8362e5a120b4a3582b409a5fdd Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 31 Dec 2019 01:01:16 +0800 Subject: Fix #551, handle empty rows in streaming reading --- chart.go | 16 +++++----------- excelize.go | 15 ++++++--------- picture.go | 47 +++++++++++++++++++---------------------------- pivotTable.go | 12 +++++------- rows.go | 38 ++++++++++++++++++++++++++------------ rows_test.go | 9 +++++++++ stream.go | 12 ++++++------ styles.go | 16 ++++++++-------- 8 files changed, 84 insertions(+), 81 deletions(-) diff --git a/chart.go b/chart.go index b5ff3d1..a8fcaf5 100644 --- a/chart.go +++ b/chart.go @@ -495,11 +495,7 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // // package main // -// import ( -// "fmt" -// -// "github.com/360EntSecGroup-Skylar/excelize" -// ) +// import "github.com/360EntSecGroup-Skylar/excelize" // // func main() { // categories := map[string]string{"A2": "Small", "A3": "Normal", "A4": "Large", "B1": "Apple", "C1": "Orange", "D1": "Pear"} @@ -511,15 +507,13 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // for k, v := range values { // f.SetCellValue("Sheet1", k, v) // } -// err := f.AddChart("Sheet1", "E1", `{"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"}],"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 3D Clustered Column 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","x_axis":{"reverse_order":true},"y_axis":{"maximum":7.5,"minimum":0.5}}`) -// if err != nil { -// fmt.Println(err) +// if err := f.AddChart("Sheet1", "E1", `{"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"}],"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 3D Clustered Column 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","x_axis":{"reverse_order":true},"y_axis":{"maximum":7.5,"minimum":0.5}}`); err != nil { +// println(err.Error()) // return // } // // Save xlsx file by the given path. -// err = xlsx.SaveAs("./Book1.xlsx") -// if err != nil { -// fmt.Println(err) +// if err := xlsx.SaveAs("Book1.xlsx"); err != nil { +// println(err.Error()) // } // } // diff --git a/excelize.go b/excelize.go index 8fbd315..9832c6a 100644 --- a/excelize.go +++ b/excelize.go @@ -293,17 +293,14 @@ func (f *File) UpdateLinkedValue() error { // AddVBAProject provides the method to add vbaProject.bin file which contains // functions and/or macros. The file extension should be .xlsm. For example: // -// err := f.SetSheetPrOptions("Sheet1", excelize.CodeName("Sheet1")) -// if err != nil { -// fmt.Println(err) +// if err := f.SetSheetPrOptions("Sheet1", excelize.CodeName("Sheet1")); err != nil { +// println(err.Error()) // } -// err = f.AddVBAProject("vbaProject.bin") -// if err != nil { -// fmt.Println(err) +// if err := f.AddVBAProject("vbaProject.bin"); err != nil { +// println(err.Error()) // } -// err = f.SaveAs("macros.xlsm") -// if err != nil { -// fmt.Println(err) +// if err := f.SaveAs("macros.xlsm"); err != nil { +// println(err.Error()) // } // func (f *File) AddVBAProject(bin string) error { diff --git a/picture.go b/picture.go index 80b0a52..639cb66 100644 --- a/picture.go +++ b/picture.go @@ -48,7 +48,6 @@ func parseFormatPictureSet(formatSet string) (*formatPicture, error) { // package main // // import ( -// "fmt" // _ "image/gif" // _ "image/jpeg" // _ "image/png" @@ -59,23 +58,19 @@ func parseFormatPictureSet(formatSet string) (*formatPicture, error) { // func main() { // f := excelize.NewFile() // // Insert a picture. -// err := f.AddPicture("Sheet1", "A2", "./image1.jpg", "") -// if err != nil { -// fmt.Println(err) +// if err := f.AddPicture("Sheet1", "A2", "image.jpg", ""); err != nil { +// println(err.Error()) // } // // Insert a picture scaling in the cell with location hyperlink. -// err = f.AddPicture("Sheet1", "D2", "./image1.png", `{"x_scale": 0.5, "y_scale": 0.5, "hyperlink": "#Sheet2!D8", "hyperlink_type": "Location"}`) -// if err != nil { -// fmt.Println(err) +// if err := f.AddPicture("Sheet1", "D2", "image.png", `{"x_scale": 0.5, "y_scale": 0.5, "hyperlink": "#Sheet2!D8", "hyperlink_type": "Location"}`); err != nil { +// println(err.Error()) // } // // Insert a picture offset in the cell with external hyperlink, printing and positioning support. -// err = f.AddPicture("Sheet1", "H2", "./image3.gif", `{"x_offset": 15, "y_offset": 10, "hyperlink": "https://github.com/360EntSecGroup-Skylar/excelize", "hyperlink_type": "External", "print_obj": true, "lock_aspect_ratio": false, "locked": false, "positioning": "oneCell"}`) -// if err != nil { -// fmt.Println(err) +// if err := f.AddPicture("Sheet1", "H2", "image.gif", `{"x_offset": 15, "y_offset": 10, "hyperlink": "https://github.com/360EntSecGroup-Skylar/excelize", "hyperlink_type": "External", "print_obj": true, "lock_aspect_ratio": false, "locked": false, "positioning": "oneCell"}`); err != nil { +// println(err.Error()) // } -// err = f.SaveAs("./Book1.xlsx") -// if err != nil { -// fmt.Println(err) +// if err := f.SaveAs("Book1.xlsx"); err != nil { +// println(err.Error()) // } // } // @@ -109,7 +104,6 @@ func (f *File) AddPicture(sheet, cell, picture, format string) error { // package main // // import ( -// "fmt" // _ "image/jpeg" // "io/ioutil" // @@ -119,17 +113,15 @@ func (f *File) AddPicture(sheet, cell, picture, format string) error { // func main() { // f := excelize.NewFile() // -// file, err := ioutil.ReadFile("./image1.jpg") +// file, err := ioutil.ReadFile("image.jpg") // if err != nil { -// fmt.Println(err) +// println(err.Error()) // } -// err = f.AddPictureFromBytes("Sheet1", "A2", "", "Excel Logo", ".jpg", file) -// if err != nil { -// fmt.Println(err) +// if err := f.AddPictureFromBytes("Sheet1", "A2", "", "Excel Logo", ".jpg", file); err != nil { +// println(err.Error()) // } -// err = f.SaveAs("./Book1.xlsx") -// if err != nil { -// fmt.Println(err) +// if err := f.SaveAs("Book1.xlsx"); err != nil { +// println(err.Error()) // } // } // @@ -430,19 +422,18 @@ func (f *File) getSheetRelationshipsTargetByID(sheet, rID string) string { // embed in XLSX by given worksheet and cell name. This function returns the // file name in XLSX and file contents as []byte data types. For example: // -// f, err := excelize.OpenFile("./Book1.xlsx") +// f, err := excelize.OpenFile("Book1.xlsx") // if err != nil { -// fmt.Println(err) +// println(err.Error()) // return // } // file, raw, err := f.GetPicture("Sheet1", "A2") // if err != nil { -// fmt.Println(err) +// println(err.Error()) // return // } -// err = ioutil.WriteFile(file, raw, 0644) -// if err != nil { -// fmt.Println(err) +// if err := ioutil.WriteFile(file, raw, 0644); err != nil { +// println(err.Error()) // } // func (f *File) GetPicture(sheet, cell string) (string, []byte, error) { diff --git a/pivotTable.go b/pivotTable.go index 8610280..70681ca 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -56,19 +56,17 @@ type PivotTableOption struct { // f.SetCellValue("Sheet1", fmt.Sprintf("D%d", i+2), rand.Intn(5000)) // f.SetCellValue("Sheet1", fmt.Sprintf("E%d", i+2), region[rand.Intn(4)]) // } -// err := f.AddPivotTable(&excelize.PivotTableOption{ +// if err := f.AddPivotTable(&excelize.PivotTableOption{ // DataRange: "Sheet1!$A$1:$E$31", // PivotTableRange: "Sheet1!$G$2:$M$34", // Rows: []string{"Month", "Year"}, // Columns: []string{"Type"}, // Data: []string{"Sales"}, -// }) -// if err != nil { -// fmt.Println(err) +// }); err != nil { +// println(err.Error()) // } -// err = f.SaveAs("Book1.xlsx") -// if err != nil { -// fmt.Println(err) +// if err := f.SaveAs("Book1.xlsx"); err != nil { +// println(err.Error()) // } // } // diff --git a/rows.go b/rows.go index 20b4379..d24b1a6 100644 --- a/rows.go +++ b/rows.go @@ -23,12 +23,20 @@ import ( // GetRows return all the rows in a sheet by given worksheet name (case // sensitive). For example: // -// rows, err := f.GetRows("Sheet1") -// for _, row := range rows { +// rows, err := f.Rows("Sheet1") +// if err != nil { +// println(err.Error()) +// return +// } +// for rows.Next() { +// row, err := rows.Columns() +// if err != nil { +// println(err.Error()) +// } // for _, colCell := range row { -// fmt.Print(colCell, "\t") +// print(colCell, "\t") // } -// fmt.Println() +// println() // } // func (f *File) GetRows(sheet string) ([][]string, error) { @@ -52,13 +60,13 @@ func (f *File) GetRows(sheet string) ([][]string, error) { // Rows defines an iterator to a sheet type Rows struct { - err error - f *File - rows []xlsxRow - sheet string - curRow int - totalRow int - decoder *xml.Decoder + err error + curRow, totalRow, stashRow int + sheet string + stashColumn []string + rows []xlsxRow + f *File + decoder *xml.Decoder } // Next will return true if find the next row element. @@ -80,6 +88,11 @@ func (rows *Rows) Columns() ([]string, error) { row, cellCol int columns []string ) + + if rows.stashRow >= rows.curRow { + return columns, err + } + d := rows.f.sharedStringsReader() for { token, _ := rows.decoder.Token() @@ -97,6 +110,8 @@ func (rows *Rows) Columns() ([]string, error) { return columns, err } if row > rows.curRow { + rows.stashRow = row - 1 + rows.stashColumn = columns return columns, err } } @@ -121,7 +136,6 @@ func (rows *Rows) Columns() ([]string, error) { if inElement == "row" { return columns, err } - default: } } return columns, err diff --git a/rows_test.go b/rows_test.go index fc9d866..1127bb1 100644 --- a/rows_test.go +++ b/rows_test.go @@ -136,7 +136,16 @@ func TestColumns(t *testing.T) { f := NewFile() rows, err := f.Rows("Sheet1") assert.NoError(t, err) + + rows.decoder = f.xmlNewDecoder(bytes.NewReader([]byte(`1`))) + _, err = rows.Columns() + assert.NoError(t, err) + rows.decoder = f.xmlNewDecoder(bytes.NewReader([]byte(`1`))) + rows.curRow = 1 + _, err = rows.Columns() + rows.decoder = f.xmlNewDecoder(bytes.NewReader([]byte(`1B`))) + rows.stashRow, rows.curRow = 0, 1 _, err = rows.Columns() assert.EqualError(t, err, `strconv.Atoi: parsing "A": invalid syntax`) diff --git a/stream.go b/stream.go index 9facf31..8398622 100644 --- a/stream.go +++ b/stream.go @@ -41,14 +41,14 @@ type StreamWriter struct { // file := excelize.NewFile() // streamWriter, err := file.NewStreamWriter("Sheet1") // if err != nil { -// panic(err) +// println(err.Error()) // } // styleID, err := file.NewStyle(`{"font":{"color":"#777777"}}`) // if err != nil { -// panic(err) +// println(err.Error()) // } // if err := streamWriter.SetRow("A1", []interface{}{excelize.Cell{StyleID: styleID, Value: "Data"}}); err != nil { -// panic(err) +// println(err.Error()) // } // for rowID := 2; rowID <= 102400; rowID++ { // row := make([]interface{}, 50) @@ -57,14 +57,14 @@ type StreamWriter struct { // } // cell, _ := excelize.CoordinatesToCellName(1, rowID) // if err := streamWriter.SetRow(cell, row); err != nil { -// panic(err) +// println(err.Error()) // } // } // if err := streamWriter.Flush(); err != nil { -// panic(err) +// println(err.Error()) // } // if err := file.SaveAs("Book1.xlsx"); err != nil { -// panic(err) +// println(err.Error()) // } // func (f *File) NewStreamWriter(sheet string) (*StreamWriter, error) { diff --git a/styles.go b/styles.go index 272d728..ad3e825 100644 --- a/styles.go +++ b/styles.go @@ -2321,7 +2321,7 @@ func (f *File) GetCellStyle(sheet, axis string) (int, error) { // // style, err := f.NewStyle(`{"border":[{"type":"left","color":"0000FF","style":3},{"type":"top","color":"00FF00","style":4},{"type":"bottom","color":"FFFF00","style":5},{"type":"right","color":"FF0000","style":6},{"type":"diagonalDown","color":"A020F0","style":7},{"type":"diagonalUp","color":"A020F0","style":8}]}`) // if err != nil { -// fmt.Println(err) +// println(err.Error()) // } // err = f.SetCellStyle("Sheet1", "H9", "H9", style) // @@ -2330,7 +2330,7 @@ func (f *File) GetCellStyle(sheet, axis string) (int, error) { // // style, err := f.NewStyle(`{"fill":{"type":"gradient","color":["#FFFFFF","#E0EBF5"],"shading":1}}`) // if err != nil { -// fmt.Println(err) +// println(err.Error()) // } // err = f.SetCellStyle("Sheet1", "H9", "H9", style) // @@ -2338,7 +2338,7 @@ func (f *File) GetCellStyle(sheet, axis string) (int, error) { // // style, err := f.NewStyle(`{"fill":{"type":"pattern","color":["#E0EBF5"],"pattern":1}}`) // if err != nil { -// fmt.Println(err) +// println(err.Error()) // } // err = f.SetCellStyle("Sheet1", "H9", "H9", style) // @@ -2346,7 +2346,7 @@ func (f *File) GetCellStyle(sheet, axis string) (int, error) { // // style, err := f.NewStyle(`{"alignment":{"horizontal":"center","ident":1,"justify_last_line":true,"reading_order":0,"relative_indent":1,"shrink_to_fit":true,"text_rotation":45,"vertical":"","wrap_text":true}}`) // if err != nil { -// fmt.Println(err) +// println(err.Error()) // } // err = f.SetCellStyle("Sheet1", "H9", "H9", style) // @@ -2357,7 +2357,7 @@ func (f *File) GetCellStyle(sheet, axis string) (int, error) { // f.SetCellValue("Sheet1", "H9", 42920.5) // style, err := f.NewStyle(`{"number_format": 22}`) // if err != nil { -// fmt.Println(err) +// println(err.Error()) // } // err = f.SetCellStyle("Sheet1", "H9", "H9", style) // @@ -2365,7 +2365,7 @@ func (f *File) GetCellStyle(sheet, axis string) (int, error) { // // style, err := f.NewStyle(`{"font":{"bold":true,"italic":true,"family":"Times New Roman","size":36,"color":"#777777"}}`) // if err != nil { -// fmt.Println(err) +// println(err.Error()) // } // err = f.SetCellStyle("Sheet1", "H9", "H9", style) // @@ -2373,7 +2373,7 @@ func (f *File) GetCellStyle(sheet, axis string) (int, error) { // // style, err := f.NewStyle(`{"protection":{"hidden":true, "locked":true}}`) // if err != nil { -// fmt.Println(err) +// println(err.Error()) // } // err = f.SetCellStyle("Sheet1", "H9", "H9", style) // @@ -2507,7 +2507,7 @@ func (f *File) SetCellStyle(sheet, hcell, vcell string, styleID int) error { // // format, err = f.NewConditionalStyle(`{"font":{"color":"#9A0511"},"fill":{"type":"pattern","color":["#FEC7CE"],"pattern":1}}`) // if err != nil { -// fmt.Println(err) +// println(err.Error()) // } // f.SetConditionalFormat("Sheet1", "A1:A10", fmt.Sprintf(`[{"type":"cell","criteria":">","format":%d,"value":"6"}]`, format)) // -- cgit v1.2.1 From 5ca7231ed408ac264f509ff52b5d28ff4fbda757 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 3 Jan 2020 23:57:25 +0800 Subject: optimize code and comments: use println errors instead of panic --- cell_test.go | 2 +- cellmerged.go | 197 ----------------------------------------------------- cellmerged_test.go | 169 --------------------------------------------- excelize_test.go | 2 +- file_test.go | 7 +- merge.go | 197 +++++++++++++++++++++++++++++++++++++++++++++++++++++ merge_test.go | 169 +++++++++++++++++++++++++++++++++++++++++++++ rows.go | 13 ++-- rows_test.go | 1 + sheet.go | 4 +- sheet_test.go | 12 ++-- sheetpr_test.go | 8 +-- sheetview_test.go | 24 +++---- sparkline_test.go | 4 +- 14 files changed, 407 insertions(+), 402 deletions(-) delete mode 100644 cellmerged.go delete mode 100644 cellmerged_test.go create mode 100644 merge.go create mode 100644 merge_test.go diff --git a/cell_test.go b/cell_test.go index 1efbc5a..60f8751 100644 --- a/cell_test.go +++ b/cell_test.go @@ -110,7 +110,7 @@ func ExampleFile_SetCellFloat() { f := NewFile() var x = 3.14159265 if err := f.SetCellFloat("Sheet1", "A1", x, 2, 64); err != nil { - fmt.Println(err) + println(err.Error()) } val, _ := f.GetCellValue("Sheet1", "A1") fmt.Println(val) diff --git a/cellmerged.go b/cellmerged.go deleted file mode 100644 index b952a1e..0000000 --- a/cellmerged.go +++ /dev/null @@ -1,197 +0,0 @@ -// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of -// this source code is governed by a BSD-style license that can be found in -// the LICENSE file. -// -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.10 or later. - -package excelize - -import ( - "fmt" - "strings" -) - -// MergeCell provides a function to merge cells by given coordinate area and -// sheet name. For example create a merged cell of D3:E9 on Sheet1: -// -// err := f.MergeCell("Sheet1", "D3", "E9") -// -// If you create a merged cell that overlaps with another existing merged cell, -// those merged cells that already exist will be removed. -// -// B1(x1,y1) D1(x2,y1) -// +--------------------------------+ -// | | -// | | -// A4(x3,y3) | C4(x4,y3) | -// +-----------------------------+ | -// | | | | -// | | | | -// | |B5(x1,y2) | D5(x2,y2)| -// | +--------------------------------+ -// | | -// | | -// |A8(x3,y4) C8(x4,y4)| -// +-----------------------------+ -// -func (f *File) MergeCell(sheet, hcell, vcell string) error { - rect1, err := f.areaRefToCoordinates(hcell + ":" + vcell) - if err != nil { - return err - } - // Correct the coordinate area, such correct C1:B3 to B1:C3. - _ = sortCoordinates(rect1) - - hcell, _ = CoordinatesToCellName(rect1[0], rect1[1]) - vcell, _ = CoordinatesToCellName(rect1[2], rect1[3]) - - xlsx, err := f.workSheetReader(sheet) - if err != nil { - return err - } - ref := hcell + ":" + vcell - if xlsx.MergeCells != nil { - for i := 0; i < len(xlsx.MergeCells.Cells); i++ { - cellData := xlsx.MergeCells.Cells[i] - if cellData == nil { - continue - } - cc := strings.Split(cellData.Ref, ":") - if len(cc) != 2 { - return fmt.Errorf("invalid area %q", cellData.Ref) - } - - rect2, err := f.areaRefToCoordinates(cellData.Ref) - if err != nil { - return err - } - - // Delete the merged cells of the overlapping area. - if isOverlap(rect1, rect2) { - xlsx.MergeCells.Cells = append(xlsx.MergeCells.Cells[:i], xlsx.MergeCells.Cells[i+1:]...) - i-- - - if rect1[0] > rect2[0] { - rect1[0], rect2[0] = rect2[0], rect1[0] - } - - if rect1[2] < rect2[2] { - rect1[2], rect2[2] = rect2[2], rect1[2] - } - - if rect1[1] > rect2[1] { - rect1[1], rect2[1] = rect2[1], rect1[1] - } - - if rect1[3] < rect2[3] { - rect1[3], rect2[3] = rect2[3], rect1[3] - } - hcell, _ = CoordinatesToCellName(rect1[0], rect1[1]) - vcell, _ = CoordinatesToCellName(rect1[2], rect1[3]) - ref = hcell + ":" + vcell - } - } - xlsx.MergeCells.Cells = append(xlsx.MergeCells.Cells, &xlsxMergeCell{Ref: ref}) - } else { - xlsx.MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: ref}}} - } - return err -} - -// UnmergeCell provides a function to unmerge a given coordinate area. -// For example unmerge area D3:E9 on Sheet1: -// -// err := f.UnmergeCell("Sheet1", "D3", "E9") -// -// Attention: overlapped areas will also be unmerged. -func (f *File) UnmergeCell(sheet string, hcell, vcell string) error { - xlsx, err := f.workSheetReader(sheet) - if err != nil { - return err - } - rect1, err := f.areaRefToCoordinates(hcell + ":" + vcell) - if err != nil { - return err - } - - // Correct the coordinate area, such correct C1:B3 to B1:C3. - _ = sortCoordinates(rect1) - - // return nil since no MergeCells in the sheet - if xlsx.MergeCells == nil { - return nil - } - - i := 0 - for _, cellData := range xlsx.MergeCells.Cells { - if cellData == nil { - continue - } - cc := strings.Split(cellData.Ref, ":") - if len(cc) != 2 { - return fmt.Errorf("invalid area %q", cellData.Ref) - } - - rect2, err := f.areaRefToCoordinates(cellData.Ref) - if err != nil { - return err - } - - if isOverlap(rect1, rect2) { - continue - } - xlsx.MergeCells.Cells[i] = cellData - i++ - } - xlsx.MergeCells.Cells = xlsx.MergeCells.Cells[:i] - return nil -} - -// GetMergeCells provides a function to get all merged cells from a worksheet -// currently. -func (f *File) GetMergeCells(sheet string) ([]MergeCell, error) { - var mergeCells []MergeCell - xlsx, err := f.workSheetReader(sheet) - if err != nil { - return mergeCells, err - } - if xlsx.MergeCells != nil { - mergeCells = make([]MergeCell, 0, len(xlsx.MergeCells.Cells)) - - for i := range xlsx.MergeCells.Cells { - ref := xlsx.MergeCells.Cells[i].Ref - axis := strings.Split(ref, ":")[0] - val, _ := f.GetCellValue(sheet, axis) - mergeCells = append(mergeCells, []string{ref, val}) - } - } - - return mergeCells, err -} - -// MergeCell define a merged cell data. -// It consists of the following structure. -// example: []string{"D4:E10", "cell value"} -type MergeCell []string - -// GetCellValue returns merged cell value. -func (m *MergeCell) GetCellValue() string { - return (*m)[1] -} - -// GetStartAxis returns the merge start axis. -// example: "C2" -func (m *MergeCell) GetStartAxis() string { - axis := strings.Split((*m)[0], ":") - return axis[0] -} - -// GetEndAxis returns the merge end axis. -// example: "D4" -func (m *MergeCell) GetEndAxis() string { - axis := strings.Split((*m)[0], ":") - return axis[1] -} diff --git a/cellmerged_test.go b/cellmerged_test.go deleted file mode 100644 index e880d05..0000000 --- a/cellmerged_test.go +++ /dev/null @@ -1,169 +0,0 @@ -package excelize - -import ( - "path/filepath" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestMergeCell(t *testing.T) { - f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) - if !assert.NoError(t, err) { - t.FailNow() - } - assert.EqualError(t, f.MergeCell("Sheet1", "A", "B"), `cannot convert cell "A" to coordinates: invalid cell name "A"`) - assert.NoError(t, f.MergeCell("Sheet1", "D9", "D9")) - assert.NoError(t, f.MergeCell("Sheet1", "D9", "E9")) - assert.NoError(t, f.MergeCell("Sheet1", "H14", "G13")) - assert.NoError(t, f.MergeCell("Sheet1", "C9", "D8")) - assert.NoError(t, f.MergeCell("Sheet1", "F11", "G13")) - assert.NoError(t, f.MergeCell("Sheet1", "H7", "B15")) - assert.NoError(t, f.MergeCell("Sheet1", "D11", "F13")) - assert.NoError(t, f.MergeCell("Sheet1", "G10", "K12")) - assert.NoError(t, f.SetCellValue("Sheet1", "G11", "set value in merged cell")) - assert.NoError(t, f.SetCellInt("Sheet1", "H11", 100)) - assert.NoError(t, f.SetCellValue("Sheet1", "I11", float64(0.5))) - assert.NoError(t, f.SetCellHyperLink("Sheet1", "J11", "https://github.com/360EntSecGroup-Skylar/excelize", "External")) - assert.NoError(t, f.SetCellFormula("Sheet1", "G12", "SUM(Sheet1!B19,Sheet1!C19)")) - value, err := f.GetCellValue("Sheet1", "H11") - assert.Equal(t, "0.5", value) - assert.NoError(t, err) - value, err = f.GetCellValue("Sheet2", "A6") // Merged cell ref is single coordinate. - assert.Equal(t, "", value) - assert.NoError(t, err) - value, err = f.GetCellFormula("Sheet1", "G12") - assert.Equal(t, "SUM(Sheet1!B19,Sheet1!C19)", value) - assert.NoError(t, err) - - f.NewSheet("Sheet3") - assert.NoError(t, f.MergeCell("Sheet3", "D11", "F13")) - assert.NoError(t, f.MergeCell("Sheet3", "G10", "K12")) - - assert.NoError(t, f.MergeCell("Sheet3", "B1", "D5")) // B1:D5 - assert.NoError(t, f.MergeCell("Sheet3", "E1", "F5")) // E1:F5 - - assert.NoError(t, f.MergeCell("Sheet3", "H2", "I5")) - assert.NoError(t, f.MergeCell("Sheet3", "I4", "J6")) // H2:J6 - - assert.NoError(t, f.MergeCell("Sheet3", "M2", "N5")) - assert.NoError(t, f.MergeCell("Sheet3", "L4", "M6")) // L2:N6 - - assert.NoError(t, f.MergeCell("Sheet3", "P4", "Q7")) - assert.NoError(t, f.MergeCell("Sheet3", "O2", "P5")) // O2:Q7 - - assert.NoError(t, f.MergeCell("Sheet3", "A9", "B12")) - assert.NoError(t, f.MergeCell("Sheet3", "B7", "C9")) // A7:C12 - - assert.NoError(t, f.MergeCell("Sheet3", "E9", "F10")) - assert.NoError(t, f.MergeCell("Sheet3", "D8", "G12")) - - assert.NoError(t, f.MergeCell("Sheet3", "I8", "I12")) - assert.NoError(t, f.MergeCell("Sheet3", "I10", "K10")) - - assert.NoError(t, f.MergeCell("Sheet3", "M8", "Q13")) - assert.NoError(t, f.MergeCell("Sheet3", "N10", "O11")) - - // Test get merged cells on not exists worksheet. - assert.EqualError(t, f.MergeCell("SheetN", "N10", "O11"), "sheet SheetN is not exist") - - assert.NoError(t, f.SaveAs(filepath.Join("test", "TestMergeCell.xlsx"))) - - f = NewFile() - assert.NoError(t, f.MergeCell("Sheet1", "A2", "B3")) - f.Sheet["xl/worksheets/sheet1.xml"].MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{nil, nil}} - assert.NoError(t, f.MergeCell("Sheet1", "A2", "B3")) - - f.Sheet["xl/worksheets/sheet1.xml"].MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A1"}}} - assert.EqualError(t, f.MergeCell("Sheet1", "A2", "B3"), `invalid area "A1"`) - - f.Sheet["xl/worksheets/sheet1.xml"].MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A:A"}}} - assert.EqualError(t, f.MergeCell("Sheet1", "A2", "B3"), `cannot convert cell "A" to coordinates: invalid cell name "A"`) -} - -func TestGetMergeCells(t *testing.T) { - wants := []struct { - 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", - }} - - f, err := OpenFile(filepath.Join("test", "MergeCell.xlsx")) - if !assert.NoError(t, err) { - t.FailNow() - } - sheet1 := f.GetSheetName(1) - - mergeCells, err := f.GetMergeCells(sheet1) - if !assert.Len(t, mergeCells, len(wants)) { - t.FailNow() - } - assert.NoError(t, err) - - for i, m := range mergeCells { - assert.Equal(t, wants[i].value, m.GetCellValue()) - assert.Equal(t, wants[i].start, m.GetStartAxis()) - assert.Equal(t, wants[i].end, m.GetEndAxis()) - } - - // Test get merged cells on not exists worksheet. - _, err = f.GetMergeCells("SheetN") - assert.EqualError(t, err, "sheet SheetN is not exist") -} - -func TestUnmergeCell(t *testing.T) { - f, err := OpenFile(filepath.Join("test", "MergeCell.xlsx")) - if !assert.NoError(t, err) { - t.FailNow() - } - sheet1 := f.GetSheetName(1) - - xlsx, err := f.workSheetReader(sheet1) - assert.NoError(t, err) - - mergeCellNum := len(xlsx.MergeCells.Cells) - - assert.EqualError(t, f.UnmergeCell("Sheet1", "A", "A"), `cannot convert cell "A" to coordinates: invalid cell name "A"`) - - // unmerge the mergecell that contains A1 - assert.NoError(t, f.UnmergeCell(sheet1, "A1", "A1")) - if len(xlsx.MergeCells.Cells) != mergeCellNum-1 { - t.FailNow() - } - - assert.NoError(t, f.SaveAs(filepath.Join("test", "TestUnmergeCell.xlsx"))) - - f = NewFile() - assert.NoError(t, f.MergeCell("Sheet1", "A2", "B3")) - // Test unmerged area on not exists worksheet. - assert.EqualError(t, f.UnmergeCell("SheetN", "A1", "A1"), "sheet SheetN is not exist") - - f.Sheet["xl/worksheets/sheet1.xml"].MergeCells = nil - assert.NoError(t, f.UnmergeCell("Sheet1", "H7", "B15")) - - f.Sheet["xl/worksheets/sheet1.xml"].MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{nil, nil}} - assert.NoError(t, f.UnmergeCell("Sheet1", "H15", "B7")) - - f.Sheet["xl/worksheets/sheet1.xml"].MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A1"}}} - assert.EqualError(t, f.UnmergeCell("Sheet1", "A2", "B3"), `invalid area "A1"`) - - f.Sheet["xl/worksheets/sheet1.xml"].MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A:A"}}} - assert.EqualError(t, f.UnmergeCell("Sheet1", "A2", "B3"), `cannot convert cell "A" to coordinates: invalid cell name "A"`) - -} diff --git a/excelize_test.go b/excelize_test.go index ea82828..c7f5cad 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -1259,7 +1259,7 @@ func fillCells(f *File, sheet string, colCount, rowCount int) { for row := 1; row <= rowCount; row++ { cell, _ := CoordinatesToCellName(col, row) if err := f.SetCellStr(sheet, cell, cell); err != nil { - panic(err) + println(err.Error()) } } } diff --git a/file_test.go b/file_test.go index 97ff720..8c5050c 100644 --- a/file_test.go +++ b/file_test.go @@ -12,18 +12,17 @@ func BenchmarkWrite(b *testing.B) { for col := 1; col <= 20; col++ { val, err := CoordinatesToCellName(col, row) if err != nil { - panic(err) + b.Error(err) } if err := f.SetCellDefault("Sheet1", val, s); err != nil { - panic(err) + b.Error(err) } } } // Save xlsx file by the given path. err := f.SaveAs("./test.xlsx") if err != nil { - panic(err) + b.Error(err) } } - } diff --git a/merge.go b/merge.go new file mode 100644 index 0000000..b952a1e --- /dev/null +++ b/merge.go @@ -0,0 +1,197 @@ +// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of +// this source code is governed by a BSD-style license that can be found in +// the LICENSE file. +// +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.10 or later. + +package excelize + +import ( + "fmt" + "strings" +) + +// MergeCell provides a function to merge cells by given coordinate area and +// sheet name. For example create a merged cell of D3:E9 on Sheet1: +// +// err := f.MergeCell("Sheet1", "D3", "E9") +// +// If you create a merged cell that overlaps with another existing merged cell, +// those merged cells that already exist will be removed. +// +// B1(x1,y1) D1(x2,y1) +// +--------------------------------+ +// | | +// | | +// A4(x3,y3) | C4(x4,y3) | +// +-----------------------------+ | +// | | | | +// | | | | +// | |B5(x1,y2) | D5(x2,y2)| +// | +--------------------------------+ +// | | +// | | +// |A8(x3,y4) C8(x4,y4)| +// +-----------------------------+ +// +func (f *File) MergeCell(sheet, hcell, vcell string) error { + rect1, err := f.areaRefToCoordinates(hcell + ":" + vcell) + if err != nil { + return err + } + // Correct the coordinate area, such correct C1:B3 to B1:C3. + _ = sortCoordinates(rect1) + + hcell, _ = CoordinatesToCellName(rect1[0], rect1[1]) + vcell, _ = CoordinatesToCellName(rect1[2], rect1[3]) + + xlsx, err := f.workSheetReader(sheet) + if err != nil { + return err + } + ref := hcell + ":" + vcell + if xlsx.MergeCells != nil { + for i := 0; i < len(xlsx.MergeCells.Cells); i++ { + cellData := xlsx.MergeCells.Cells[i] + if cellData == nil { + continue + } + cc := strings.Split(cellData.Ref, ":") + if len(cc) != 2 { + return fmt.Errorf("invalid area %q", cellData.Ref) + } + + rect2, err := f.areaRefToCoordinates(cellData.Ref) + if err != nil { + return err + } + + // Delete the merged cells of the overlapping area. + if isOverlap(rect1, rect2) { + xlsx.MergeCells.Cells = append(xlsx.MergeCells.Cells[:i], xlsx.MergeCells.Cells[i+1:]...) + i-- + + if rect1[0] > rect2[0] { + rect1[0], rect2[0] = rect2[0], rect1[0] + } + + if rect1[2] < rect2[2] { + rect1[2], rect2[2] = rect2[2], rect1[2] + } + + if rect1[1] > rect2[1] { + rect1[1], rect2[1] = rect2[1], rect1[1] + } + + if rect1[3] < rect2[3] { + rect1[3], rect2[3] = rect2[3], rect1[3] + } + hcell, _ = CoordinatesToCellName(rect1[0], rect1[1]) + vcell, _ = CoordinatesToCellName(rect1[2], rect1[3]) + ref = hcell + ":" + vcell + } + } + xlsx.MergeCells.Cells = append(xlsx.MergeCells.Cells, &xlsxMergeCell{Ref: ref}) + } else { + xlsx.MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: ref}}} + } + return err +} + +// UnmergeCell provides a function to unmerge a given coordinate area. +// For example unmerge area D3:E9 on Sheet1: +// +// err := f.UnmergeCell("Sheet1", "D3", "E9") +// +// Attention: overlapped areas will also be unmerged. +func (f *File) UnmergeCell(sheet string, hcell, vcell string) error { + xlsx, err := f.workSheetReader(sheet) + if err != nil { + return err + } + rect1, err := f.areaRefToCoordinates(hcell + ":" + vcell) + if err != nil { + return err + } + + // Correct the coordinate area, such correct C1:B3 to B1:C3. + _ = sortCoordinates(rect1) + + // return nil since no MergeCells in the sheet + if xlsx.MergeCells == nil { + return nil + } + + i := 0 + for _, cellData := range xlsx.MergeCells.Cells { + if cellData == nil { + continue + } + cc := strings.Split(cellData.Ref, ":") + if len(cc) != 2 { + return fmt.Errorf("invalid area %q", cellData.Ref) + } + + rect2, err := f.areaRefToCoordinates(cellData.Ref) + if err != nil { + return err + } + + if isOverlap(rect1, rect2) { + continue + } + xlsx.MergeCells.Cells[i] = cellData + i++ + } + xlsx.MergeCells.Cells = xlsx.MergeCells.Cells[:i] + return nil +} + +// GetMergeCells provides a function to get all merged cells from a worksheet +// currently. +func (f *File) GetMergeCells(sheet string) ([]MergeCell, error) { + var mergeCells []MergeCell + xlsx, err := f.workSheetReader(sheet) + if err != nil { + return mergeCells, err + } + if xlsx.MergeCells != nil { + mergeCells = make([]MergeCell, 0, len(xlsx.MergeCells.Cells)) + + for i := range xlsx.MergeCells.Cells { + ref := xlsx.MergeCells.Cells[i].Ref + axis := strings.Split(ref, ":")[0] + val, _ := f.GetCellValue(sheet, axis) + mergeCells = append(mergeCells, []string{ref, val}) + } + } + + return mergeCells, err +} + +// MergeCell define a merged cell data. +// It consists of the following structure. +// example: []string{"D4:E10", "cell value"} +type MergeCell []string + +// GetCellValue returns merged cell value. +func (m *MergeCell) GetCellValue() string { + return (*m)[1] +} + +// GetStartAxis returns the merge start axis. +// example: "C2" +func (m *MergeCell) GetStartAxis() string { + axis := strings.Split((*m)[0], ":") + return axis[0] +} + +// GetEndAxis returns the merge end axis. +// example: "D4" +func (m *MergeCell) GetEndAxis() string { + axis := strings.Split((*m)[0], ":") + return axis[1] +} diff --git a/merge_test.go b/merge_test.go new file mode 100644 index 0000000..e880d05 --- /dev/null +++ b/merge_test.go @@ -0,0 +1,169 @@ +package excelize + +import ( + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestMergeCell(t *testing.T) { + f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) + if !assert.NoError(t, err) { + t.FailNow() + } + assert.EqualError(t, f.MergeCell("Sheet1", "A", "B"), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + assert.NoError(t, f.MergeCell("Sheet1", "D9", "D9")) + assert.NoError(t, f.MergeCell("Sheet1", "D9", "E9")) + assert.NoError(t, f.MergeCell("Sheet1", "H14", "G13")) + assert.NoError(t, f.MergeCell("Sheet1", "C9", "D8")) + assert.NoError(t, f.MergeCell("Sheet1", "F11", "G13")) + assert.NoError(t, f.MergeCell("Sheet1", "H7", "B15")) + assert.NoError(t, f.MergeCell("Sheet1", "D11", "F13")) + assert.NoError(t, f.MergeCell("Sheet1", "G10", "K12")) + assert.NoError(t, f.SetCellValue("Sheet1", "G11", "set value in merged cell")) + assert.NoError(t, f.SetCellInt("Sheet1", "H11", 100)) + assert.NoError(t, f.SetCellValue("Sheet1", "I11", float64(0.5))) + assert.NoError(t, f.SetCellHyperLink("Sheet1", "J11", "https://github.com/360EntSecGroup-Skylar/excelize", "External")) + assert.NoError(t, f.SetCellFormula("Sheet1", "G12", "SUM(Sheet1!B19,Sheet1!C19)")) + value, err := f.GetCellValue("Sheet1", "H11") + assert.Equal(t, "0.5", value) + assert.NoError(t, err) + value, err = f.GetCellValue("Sheet2", "A6") // Merged cell ref is single coordinate. + assert.Equal(t, "", value) + assert.NoError(t, err) + value, err = f.GetCellFormula("Sheet1", "G12") + assert.Equal(t, "SUM(Sheet1!B19,Sheet1!C19)", value) + assert.NoError(t, err) + + f.NewSheet("Sheet3") + assert.NoError(t, f.MergeCell("Sheet3", "D11", "F13")) + assert.NoError(t, f.MergeCell("Sheet3", "G10", "K12")) + + assert.NoError(t, f.MergeCell("Sheet3", "B1", "D5")) // B1:D5 + assert.NoError(t, f.MergeCell("Sheet3", "E1", "F5")) // E1:F5 + + assert.NoError(t, f.MergeCell("Sheet3", "H2", "I5")) + assert.NoError(t, f.MergeCell("Sheet3", "I4", "J6")) // H2:J6 + + assert.NoError(t, f.MergeCell("Sheet3", "M2", "N5")) + assert.NoError(t, f.MergeCell("Sheet3", "L4", "M6")) // L2:N6 + + assert.NoError(t, f.MergeCell("Sheet3", "P4", "Q7")) + assert.NoError(t, f.MergeCell("Sheet3", "O2", "P5")) // O2:Q7 + + assert.NoError(t, f.MergeCell("Sheet3", "A9", "B12")) + assert.NoError(t, f.MergeCell("Sheet3", "B7", "C9")) // A7:C12 + + assert.NoError(t, f.MergeCell("Sheet3", "E9", "F10")) + assert.NoError(t, f.MergeCell("Sheet3", "D8", "G12")) + + assert.NoError(t, f.MergeCell("Sheet3", "I8", "I12")) + assert.NoError(t, f.MergeCell("Sheet3", "I10", "K10")) + + assert.NoError(t, f.MergeCell("Sheet3", "M8", "Q13")) + assert.NoError(t, f.MergeCell("Sheet3", "N10", "O11")) + + // Test get merged cells on not exists worksheet. + assert.EqualError(t, f.MergeCell("SheetN", "N10", "O11"), "sheet SheetN is not exist") + + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestMergeCell.xlsx"))) + + f = NewFile() + assert.NoError(t, f.MergeCell("Sheet1", "A2", "B3")) + f.Sheet["xl/worksheets/sheet1.xml"].MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{nil, nil}} + assert.NoError(t, f.MergeCell("Sheet1", "A2", "B3")) + + f.Sheet["xl/worksheets/sheet1.xml"].MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A1"}}} + assert.EqualError(t, f.MergeCell("Sheet1", "A2", "B3"), `invalid area "A1"`) + + f.Sheet["xl/worksheets/sheet1.xml"].MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A:A"}}} + assert.EqualError(t, f.MergeCell("Sheet1", "A2", "B3"), `cannot convert cell "A" to coordinates: invalid cell name "A"`) +} + +func TestGetMergeCells(t *testing.T) { + wants := []struct { + 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", + }} + + f, err := OpenFile(filepath.Join("test", "MergeCell.xlsx")) + if !assert.NoError(t, err) { + t.FailNow() + } + sheet1 := f.GetSheetName(1) + + mergeCells, err := f.GetMergeCells(sheet1) + if !assert.Len(t, mergeCells, len(wants)) { + t.FailNow() + } + assert.NoError(t, err) + + for i, m := range mergeCells { + assert.Equal(t, wants[i].value, m.GetCellValue()) + assert.Equal(t, wants[i].start, m.GetStartAxis()) + assert.Equal(t, wants[i].end, m.GetEndAxis()) + } + + // Test get merged cells on not exists worksheet. + _, err = f.GetMergeCells("SheetN") + assert.EqualError(t, err, "sheet SheetN is not exist") +} + +func TestUnmergeCell(t *testing.T) { + f, err := OpenFile(filepath.Join("test", "MergeCell.xlsx")) + if !assert.NoError(t, err) { + t.FailNow() + } + sheet1 := f.GetSheetName(1) + + xlsx, err := f.workSheetReader(sheet1) + assert.NoError(t, err) + + mergeCellNum := len(xlsx.MergeCells.Cells) + + assert.EqualError(t, f.UnmergeCell("Sheet1", "A", "A"), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + + // unmerge the mergecell that contains A1 + assert.NoError(t, f.UnmergeCell(sheet1, "A1", "A1")) + if len(xlsx.MergeCells.Cells) != mergeCellNum-1 { + t.FailNow() + } + + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestUnmergeCell.xlsx"))) + + f = NewFile() + assert.NoError(t, f.MergeCell("Sheet1", "A2", "B3")) + // Test unmerged area on not exists worksheet. + assert.EqualError(t, f.UnmergeCell("SheetN", "A1", "A1"), "sheet SheetN is not exist") + + f.Sheet["xl/worksheets/sheet1.xml"].MergeCells = nil + assert.NoError(t, f.UnmergeCell("Sheet1", "H7", "B15")) + + f.Sheet["xl/worksheets/sheet1.xml"].MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{nil, nil}} + assert.NoError(t, f.UnmergeCell("Sheet1", "H15", "B7")) + + f.Sheet["xl/worksheets/sheet1.xml"].MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A1"}}} + assert.EqualError(t, f.UnmergeCell("Sheet1", "A2", "B3"), `invalid area "A1"`) + + f.Sheet["xl/worksheets/sheet1.xml"].MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A:A"}}} + assert.EqualError(t, f.UnmergeCell("Sheet1", "A2", "B3"), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + +} diff --git a/rows.go b/rows.go index d24b1a6..40972ae 100644 --- a/rows.go +++ b/rows.go @@ -63,7 +63,6 @@ type Rows struct { err error curRow, totalRow, stashRow int sheet string - stashColumn []string rows []xlsxRow f *File decoder *xml.Decoder @@ -111,7 +110,6 @@ func (rows *Rows) Columns() ([]string, error) { } if row > rows.curRow { rows.stashRow = row - 1 - rows.stashColumn = columns return columns, err } } @@ -153,12 +151,19 @@ func (err ErrSheetNotExist) Error() string { // Rows return a rows iterator. For example: // // rows, err := f.Rows("Sheet1") +// if err != nil { +// println(err.Error()) +// return +// } // for rows.Next() { // row, err := rows.Columns() +// if err != nil { +// println(err.Error()) +// } // for _, colCell := range row { -// fmt.Print(colCell, "\t") +// print(colCell, "\t") // } -// fmt.Println() +// println() // } // func (f *File) Rows(sheet string) (*Rows, error) { diff --git a/rows_test.go b/rows_test.go index 1127bb1..9377d5e 100644 --- a/rows_test.go +++ b/rows_test.go @@ -143,6 +143,7 @@ func TestColumns(t *testing.T) { rows.decoder = f.xmlNewDecoder(bytes.NewReader([]byte(`1`))) rows.curRow = 1 _, err = rows.Columns() + assert.NoError(t, err) rows.decoder = f.xmlNewDecoder(bytes.NewReader([]byte(`1B`))) rows.stashRow, rows.curRow = 0, 1 diff --git a/sheet.go b/sheet.go index 2654b8f..19b90c6 100644 --- a/sheet.go +++ b/sheet.go @@ -339,12 +339,12 @@ func (f *File) GetSheetIndex(name string) int { // GetSheetMap provides a function to get worksheet name and index map of XLSX. // For example: // -// f, err := excelize.OpenFile("./Book1.xlsx") +// f, err := excelize.OpenFile("Book1.xlsx") // if err != nil { // return // } // for index, name := range f.GetSheetMap() { -// fmt.Println(index, name) +// println(index, name) // } // func (f *File) GetSheetMap() map[int]string { diff --git a/sheet_test.go b/sheet_test.go index 7a58248..a03066a 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -19,7 +19,7 @@ func ExampleFile_SetPageLayout() { "Sheet1", excelize.PageLayoutOrientation(excelize.OrientationLandscape), ); err != nil { - panic(err) + println(err.Error()) } if err := f.SetPageLayout( "Sheet1", @@ -27,7 +27,7 @@ func ExampleFile_SetPageLayout() { excelize.FitToHeight(2), excelize.FitToWidth(2), ); err != nil { - panic(err) + println(err.Error()) } // Output: } @@ -41,17 +41,17 @@ func ExampleFile_GetPageLayout() { fitToWidth excelize.FitToWidth ) if err := f.GetPageLayout("Sheet1", &orientation); err != nil { - panic(err) + println(err.Error()) } if err := f.GetPageLayout("Sheet1", &paperSize); err != nil { - panic(err) + println(err.Error()) } if err := f.GetPageLayout("Sheet1", &fitToHeight); err != nil { - panic(err) + println(err.Error()) } if err := f.GetPageLayout("Sheet1", &fitToWidth); err != nil { - panic(err) + println(err.Error()) } fmt.Println("Defaults:") fmt.Printf("- orientation: %q\n", orientation) diff --git a/sheetpr_test.go b/sheetpr_test.go index d1ae2f1..6a35a6e 100644 --- a/sheetpr_test.go +++ b/sheetpr_test.go @@ -40,7 +40,7 @@ func ExampleFile_SetSheetPrOptions() { excelize.AutoPageBreaks(true), excelize.OutlineSummaryBelow(false), ); err != nil { - panic(err) + println(err.Error()) } // Output: } @@ -66,7 +66,7 @@ func ExampleFile_GetSheetPrOptions() { &autoPageBreaks, &outlineSummaryBelow, ); err != nil { - panic(err) + println(err.Error()) } fmt.Println("Defaults:") fmt.Printf("- codeName: %q\n", codeName) @@ -189,7 +189,7 @@ func ExampleFile_SetPageMargins() { excelize.PageMarginRight(1.0), excelize.PageMarginTop(1.0), ); err != nil { - panic(err) + println(err.Error()) } // Output: } @@ -215,7 +215,7 @@ func ExampleFile_GetPageMargins() { &marginRight, &marginTop, ); err != nil { - panic(err) + println(err.Error()) } fmt.Println("Defaults:") fmt.Println("- marginBottom:", marginBottom) diff --git a/sheetview_test.go b/sheetview_test.go index e45b8ce..8412002 100644 --- a/sheetview_test.go +++ b/sheetview_test.go @@ -47,7 +47,7 @@ func ExampleFile_SetSheetViewOptions() { excelize.ZoomScale(80), excelize.TopLeftCell("C3"), ); err != nil { - panic(err) + println(err.Error()) } var zoomScale excelize.ZoomScale @@ -55,22 +55,22 @@ func ExampleFile_SetSheetViewOptions() { fmt.Println("- zoomScale: 80") if err := f.SetSheetViewOptions(sheet, 0, excelize.ZoomScale(500)); err != nil { - panic(err) + println(err.Error()) } if err := f.GetSheetViewOptions(sheet, 0, &zoomScale); err != nil { - panic(err) + println(err.Error()) } fmt.Println("Used out of range value:") fmt.Println("- zoomScale:", zoomScale) if err := f.SetSheetViewOptions(sheet, 0, excelize.ZoomScale(123)); err != nil { - panic(err) + println(err.Error()) } if err := f.GetSheetViewOptions(sheet, 0, &zoomScale); err != nil { - panic(err) + println(err.Error()) } fmt.Println("Used correct value:") @@ -111,7 +111,7 @@ func ExampleFile_GetSheetViewOptions() { &zoomScale, &topLeftCell, ); err != nil { - panic(err) + println(err.Error()) } fmt.Println("Default:") @@ -125,27 +125,27 @@ func ExampleFile_GetSheetViewOptions() { fmt.Println("- topLeftCell:", `"`+topLeftCell+`"`) if err := f.SetSheetViewOptions(sheet, 0, excelize.TopLeftCell("B2")); err != nil { - panic(err) + println(err.Error()) } if err := f.GetSheetViewOptions(sheet, 0, &topLeftCell); err != nil { - panic(err) + println(err.Error()) } if err := f.SetSheetViewOptions(sheet, 0, excelize.ShowGridLines(false)); err != nil { - panic(err) + println(err.Error()) } if err := f.GetSheetViewOptions(sheet, 0, &showGridLines); err != nil { - panic(err) + println(err.Error()) } if err := f.SetSheetViewOptions(sheet, 0, excelize.ShowZeros(false)); err != nil { - panic(err) + println(err.Error()) } if err := f.GetSheetViewOptions(sheet, 0, &showZeros); err != nil { - panic(err) + println(err.Error()) } fmt.Println("After change:") diff --git a/sparkline_test.go b/sparkline_test.go index dca32e9..45bf386 100644 --- a/sparkline_test.go +++ b/sparkline_test.go @@ -298,12 +298,12 @@ func prepareSparklineDataset() *File { f.NewSheet("Sheet3") for row, data := range sheet2 { if err := f.SetSheetRow("Sheet2", fmt.Sprintf("A%d", row+1), &data); err != nil { - panic(err) + println(err.Error()) } } for row, data := range sheet3 { if err := f.SetSheetRow("Sheet3", fmt.Sprintf("A%d", row+1), &data); err != nil { - panic(err) + println(err.Error()) } } return f -- cgit v1.2.1 From 9ddb52eac4e451f676dabe4eed45ee95fce38eef Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 14 Jan 2020 00:33:36 +0800 Subject: Fix #554, init combo chart support, new chart pie of pie, bar of pie chart support --- chart.go | 109 ++++++++++++++++++++++++++++++++++++++++++++++++++++++---- chart_test.go | 27 ++++++++++++++- xmlChart.go | 12 ++++--- 3 files changed, 136 insertions(+), 12 deletions(-) diff --git a/chart.go b/chart.go index a8fcaf5..f3b1cd8 100644 --- a/chart.go +++ b/chart.go @@ -16,6 +16,7 @@ import ( "errors" "io" "log" + "reflect" "strconv" "strings" ) @@ -66,6 +67,8 @@ const ( Line = "line" Pie = "pie" Pie3D = "pie3D" + PieOfPieChart = "pieOfPie" + BarOfPieChart = "barOfPie" Radar = "radar" Scatter = "scatter" Surface3D = "surface3D" @@ -123,6 +126,8 @@ var ( Line: 0, Pie: 0, Pie3D: 30, + PieOfPieChart: 0, + BarOfPieChart: 0, Radar: 0, Scatter: 0, Surface3D: 15, @@ -175,6 +180,8 @@ var ( Line: 0, Pie: 0, Pie3D: 0, + PieOfPieChart: 0, + BarOfPieChart: 0, Radar: 0, Scatter: 0, Surface3D: 20, @@ -237,6 +244,8 @@ var ( Line: 0, Pie: 0, Pie3D: 0, + PieOfPieChart: 0, + BarOfPieChart: 0, Radar: 0, Scatter: 0, Surface3D: 0, @@ -297,6 +306,8 @@ var ( Line: "General", Pie: "General", Pie3D: "General", + PieOfPieChart: "General", + BarOfPieChart: "General", Radar: "General", Scatter: "General", Surface3D: "General", @@ -351,6 +362,8 @@ var ( Line: "between", Pie: "between", Pie3D: "between", + PieOfPieChart: "between", + BarOfPieChart: "between", Radar: "between", Scatter: "between", Surface3D: "midCat", @@ -491,7 +504,7 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // AddChart provides the method to add chart in a sheet by given chart format // set (such as offset, scale, aspect ratio setting and print settings) and // properties set. For example, create 3D clustered column chart with data -// Sheet1!$A$29:$D$32: +// Sheet1!$E$1:$L$15: // // package main // @@ -507,12 +520,12 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // for k, v := range values { // f.SetCellValue("Sheet1", k, v) // } -// if err := f.AddChart("Sheet1", "E1", `{"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"}],"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 3D Clustered Column 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","x_axis":{"reverse_order":true},"y_axis":{"maximum":7.5,"minimum":0.5}}`); err != nil { +// if err := f.AddChart("Sheet1", "E1", `{"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"},"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","x_axis":{"reverse_order":true},"y_axis":{"maximum":7.5,"minimum":0.5}}`); err != nil { // println(err.Error()) // return // } // // Save xlsx file by the given path. -// if err := xlsx.SaveAs("Book1.xlsx"); err != nil { +// if err := f.SaveAs("Book1.xlsx"); err != nil { // println(err.Error()) // } // } @@ -565,6 +578,8 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // line | line chart // pie | pie chart // pie3D | 3D pie chart +// pieOfPie | pie of pie chart +// barOfPie | bar of pie chart // radar | radar chart // scatter | scatter chart // surface3D | 3D surface chart @@ -681,6 +696,34 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // // Set chart size by dimension property. The dimension property is optional. The default width is 480, and height is 290. // +// combo: Specifies tha create a chart that combines two art types in a single +// chart. For example, create a clustered column - line chart with data +// Sheet1!$E$1:$L$15: +// +// package main +// +// import "github.com/360EntSecGroup-Skylar/excelize" +// +// func main() { +// 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} +// f := excelize.NewFile() +// for k, v := range categories { +// f.SetCellValue("Sheet1", k, v) +// } +// for k, v := range values { +// f.SetCellValue("Sheet1", k, v) +// } +// if err := f.AddChart("Sheet1", "E1", `{"type":"col","series":[{"name":"Sheet1!$A$2","categories":"","values":"Sheet1!$B$2:$D$2"},{"name":"Sheet1!$A$3","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$3:$D$3"}],"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":"left","show_legend_key":false},"title":{"name":"Clustered Column - Line Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"combo":{"type":"line","series":[{"name":"Sheet1!$A$4","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$4:$D$4"}],"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":"left","show_legend_key":false},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true}}}`); err != nil { +// println(err.Error()) +// return +// } +// // Save xlsx file by the given path. +// if err := f.SaveAs("Book1.xlsx"); err != nil { +// println(err.Error()) +// } +// } +// func (f *File) AddChart(sheet, cell, format string) error { formatSet, err := parseFormatChartSet(format) if err != nil { @@ -915,6 +958,8 @@ func (f *File) addChart(formatSet *formatChart) { Line: f.drawLineChart, Pie3D: f.drawPie3DChart, Pie: f.drawPieChart, + PieOfPieChart: f.drawPieOfPieChart, + BarOfPieChart: f.drawBarOfPieChart, Radar: f.drawRadarChart, Scatter: f.drawScatterChart, Surface3D: f.drawSurface3DChart, @@ -924,8 +969,20 @@ func (f *File) addChart(formatSet *formatChart) { Bubble: f.drawBaseChart, Bubble3D: f.drawBaseChart, } - xlsxChartSpace.Chart.PlotArea = plotAreaFunc[formatSet.Type](formatSet) - + addChart := func(c, p *cPlotArea) { + immutable, mutable := reflect.ValueOf(c).Elem(), reflect.ValueOf(p).Elem() + for i := 0; i < mutable.NumField(); i++ { + field := mutable.Field(i) + if field.IsNil() { + continue + } + immutable.FieldByName(mutable.Type().Field(i).Name).Set(field) + } + } + addChart(xlsxChartSpace.Chart.PlotArea, plotAreaFunc[formatSet.Type](formatSet)) + if formatSet.Combo != nil { + addChart(xlsxChartSpace.Chart.PlotArea, plotAreaFunc[formatSet.Combo.Type](formatSet.Combo)) + } chart, _ := xml.Marshal(xlsxChartSpace) media := "xl/charts/chart" + strconv.Itoa(count+1) + ".xml" f.saveFileList(media, chart) @@ -1246,6 +1303,40 @@ func (f *File) drawPie3DChart(formatSet *formatChart) *cPlotArea { } } +// drawPieOfPieChart provides a function to draw the c:plotArea element for +// pie chart by given format sets. +func (f *File) drawPieOfPieChart(formatSet *formatChart) *cPlotArea { + return &cPlotArea{ + PieChart: &cCharts{ + OfPieType: &attrValString{ + Val: stringPtr("pie"), + }, + VaryColors: &attrValBool{ + Val: boolPtr(true), + }, + Ser: f.drawChartSeries(formatSet), + SerLines: &attrValString{}, + }, + } +} + +// drawBarOfPieChart provides a function to draw the c:plotArea element for +// pie chart by given format sets. +func (f *File) drawBarOfPieChart(formatSet *formatChart) *cPlotArea { + return &cPlotArea{ + PieChart: &cCharts{ + OfPieType: &attrValString{ + Val: stringPtr("bar"), + }, + VaryColors: &attrValBool{ + Val: boolPtr(true), + }, + Ser: f.drawChartSeries(formatSet), + SerLines: &attrValString{}, + }, + } +} + // drawRadarChart provides a function to draw the c:plotArea element for radar // chart by given format sets. func (f *File) drawRadarChart(formatSet *formatChart) *cPlotArea { @@ -1371,11 +1462,15 @@ func (f *File) drawChartShape(formatSet *formatChart) *attrValString { // drawChartSeries provides a function to draw the c:ser element by given // format sets. func (f *File) drawChartSeries(formatSet *formatChart) *[]cSer { + var baseIdx int + if formatSet.Combo != nil { + baseIdx = len(formatSet.Combo.Series) + } ser := []cSer{} for k := range formatSet.Series { ser = append(ser, cSer{ - IDx: &attrValInt{Val: intPtr(k)}, - Order: &attrValInt{Val: intPtr(k)}, + IDx: &attrValInt{Val: intPtr(k + baseIdx)}, + Order: &attrValInt{Val: intPtr(k + baseIdx)}, Tx: &cTx{ StrRef: &cStrRef{ F: formatSet.Series[k].Name, diff --git a/chart_test.go b/chart_test.go index 2ed7944..e350657 100644 --- a/chart_test.go +++ b/chart_test.go @@ -3,6 +3,7 @@ package excelize import ( "bytes" "encoding/xml" + "fmt" "path/filepath" "testing" @@ -172,7 +173,31 @@ func TestAddChart(t *testing.T) { // bubble chart assert.NoError(t, f.AddChart("Sheet2", "BD16", `{"type":"bubble","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"Bubble 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"}`)) assert.NoError(t, f.AddChart("Sheet2", "BD32", `{"type":"bubble3D","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"Bubble 3D 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","x_axis":{"major_grid_lines":true},"y_axis":{"major_grid_lines":true}}`)) + // pie of pie chart + assert.NoError(t, f.AddChart("Sheet2", "BD48", `{"type":"pieOfPie","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$A$30:$D$37","values":"Sheet1!$B$30:$B$37"}],"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":"left","show_legend_key":false},"title":{"name":"Pie of Pie 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","x_axis":{"major_grid_lines":true},"y_axis":{"major_grid_lines":true}}`)) + // bar of pie chart + assert.NoError(t, f.AddChart("Sheet2", "BD64", `{"type":"barOfPie","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$A$30:$D$37","values":"Sheet1!$B$30:$B$37"}],"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":"left","show_legend_key":false},"title":{"name":"Bar of Pie 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","x_axis":{"major_grid_lines":true},"y_axis":{"major_grid_lines":true}}`)) + // combo chart + f.NewSheet("Combo Charts") + clusteredColumnCombo := map[string][]string{ + "A1": {"line", "Clustered Column - Line Chart"}, + "I1": {"bubble", "Clustered Column - Bubble Chart"}, + "Q1": {"bubble3D", "Clustered Column - Bubble 3D Chart"}, + "Y1": {"doughnut", "Clustered Column - Doughnut Chart"}, + } + for axis, props := range clusteredColumnCombo { + assert.NoError(t, f.AddChart("Combo Charts", axis, fmt.Sprintf(`{"type":"col","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"}],"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":"left","show_legend_key":false},"title":{"name":"%s"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"combo":{"type":"%s","series":[{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true}}}`, props[1], props[0]))) + } + stackedAreaCombo := map[string][]string{ + "A16": {"line", "Stacked Area - Line Chart"}, + "I16": {"bubble", "Stacked Area - Bubble Chart"}, + "Q16": {"bubble3D", "Stacked Area - Bubble 3D Chart"}, + "Y16": {"doughnut", "Stacked Area - Doughnut Chart"}, + } + for axis, props := range stackedAreaCombo { + assert.NoError(t, f.AddChart("Combo Charts", axis, fmt.Sprintf(`{"type":"areaStacked","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"}],"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":"left","show_legend_key":false},"title":{"name":"%s"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"combo":{"type":"%s","series":[{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true}}}`, props[1], props[0]))) + } assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddChart.xlsx"))) - + // Test with unsupported chart type assert.EqualError(t, f.AddChart("Sheet2", "BD32", `{"type":"unknown","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"Bubble 3D 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"}`), "unsupported chart type unknown") } diff --git a/xmlChart.go b/xmlChart.go index b6d041e..9d6263f 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -312,6 +312,7 @@ type cPlotArea struct { LineChart *cCharts `xml:"lineChart"` PieChart *cCharts `xml:"pieChart"` Pie3DChart *cCharts `xml:"pie3DChart"` + OfPieChart *cCharts `xml:"ofPieChart"` RadarChart *cCharts `xml:"radarChart"` ScatterChart *cCharts `xml:"scatterChart"` Surface3DChart *cCharts `xml:"surface3DChart"` @@ -329,6 +330,8 @@ type cCharts struct { Grouping *attrValString `xml:"grouping"` RadarStyle *attrValString `xml:"radarStyle"` ScatterStyle *attrValString `xml:"scatterStyle"` + OfPieType *attrValString `xml:"ofPieType"` + SerLines *attrValString `xml:"serLines"` VaryColors *attrValBool `xml:"varyColors"` Wireframe *attrValBool `xml:"wireframe"` Ser *[]cSer `xml:"ser"` @@ -590,10 +593,11 @@ type formatChart struct { } `json:"fill"` Layout formatLayout `json:"layout"` } `json:"plotarea"` - ShowBlanksAs string `json:"show_blanks_as"` - ShowHiddenData bool `json:"show_hidden_data"` - SetRotation int `json:"set_rotation"` - SetHoleSize int `json:"set_hole_size"` + ShowBlanksAs string `json:"show_blanks_as"` + ShowHiddenData bool `json:"show_hidden_data"` + SetRotation int `json:"set_rotation"` + SetHoleSize int `json:"set_hole_size"` + Combo *formatChart `json:"combo"` } // formatChartLegend directly maps the format settings of the chart legend. -- cgit v1.2.1 From fa7078f06c82ed30f9573caf3c4d24d49f45df5a Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 16 Jan 2020 01:05:22 +0800 Subject: Specified combo chart as variadic parameters --- chart.go | 40 +++++++++++++++++++++++++--------------- chart_test.go | 5 +++-- xmlChart.go | 10 +++++----- 3 files changed, 33 insertions(+), 22 deletions(-) diff --git a/chart.go b/chart.go index f3b1cd8..738ed4b 100644 --- a/chart.go +++ b/chart.go @@ -696,7 +696,7 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // // Set chart size by dimension property. The dimension property is optional. The default width is 480, and height is 290. // -// combo: Specifies tha create a chart that combines two art types in a single +// combo: Specifies the create a chart that combines two art types in a single // chart. For example, create a clustered column - line chart with data // Sheet1!$E$1:$L$15: // @@ -714,7 +714,7 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // for k, v := range values { // f.SetCellValue("Sheet1", k, v) // } -// if err := f.AddChart("Sheet1", "E1", `{"type":"col","series":[{"name":"Sheet1!$A$2","categories":"","values":"Sheet1!$B$2:$D$2"},{"name":"Sheet1!$A$3","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$3:$D$3"}],"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":"left","show_legend_key":false},"title":{"name":"Clustered Column - Line Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"combo":{"type":"line","series":[{"name":"Sheet1!$A$4","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$4:$D$4"}],"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":"left","show_legend_key":false},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true}}}`); err != nil { +// if err := f.AddChart("Sheet1", "E1", `{"type":"col","series":[{"name":"Sheet1!$A$2","categories":"","values":"Sheet1!$B$2:$D$2"},{"name":"Sheet1!$A$3","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$3:$D$3"}],"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":"left","show_legend_key":false},"title":{"name":"Clustered Column - Line Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true}}`, `{"type":"line","series":[{"name":"Sheet1!$A$4","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$4:$D$4"}],"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":"left","show_legend_key":false},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true}}`); err != nil { // println(err.Error()) // return // } @@ -724,11 +724,22 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // } // } // -func (f *File) AddChart(sheet, cell, format string) error { +func (f *File) AddChart(sheet, cell, format string, combo ...string) error { formatSet, err := parseFormatChartSet(format) if err != nil { return err } + comboCharts := []*formatChart{} + for _, comboFormat := range combo { + comboChart, err := parseFormatChartSet(comboFormat) + if err != nil { + return err + } + if _, ok := chartValAxNumFmtFormatCode[comboChart.Type]; !ok { + return errors.New("unsupported chart type " + comboChart.Type) + } + comboCharts = append(comboCharts, comboChart) + } // Read sheet data. xlsx, err := f.workSheetReader(sheet) if err != nil { @@ -748,7 +759,7 @@ func (f *File) AddChart(sheet, cell, format string) error { if err != nil { return err } - f.addChart(formatSet) + f.addChart(formatSet, comboCharts) f.addContentTypePart(chartID, "chart") f.addContentTypePart(drawingID, "drawings") return err @@ -786,7 +797,7 @@ func (f *File) prepareDrawing(xlsx *xlsxWorksheet, drawingID int, sheet, drawing // addChart provides a function to create chart as xl/charts/chart%d.xml by // given format sets. -func (f *File) addChart(formatSet *formatChart) { +func (f *File) addChart(formatSet *formatChart, comboCharts []*formatChart) { count := f.countCharts() xlsxChartSpace := xlsxChartSpace{ XMLNSc: NameSpaceDrawingMLChart, @@ -980,8 +991,11 @@ func (f *File) addChart(formatSet *formatChart) { } } addChart(xlsxChartSpace.Chart.PlotArea, plotAreaFunc[formatSet.Type](formatSet)) - if formatSet.Combo != nil { - addChart(xlsxChartSpace.Chart.PlotArea, plotAreaFunc[formatSet.Combo.Type](formatSet.Combo)) + order := len(formatSet.Series) + for idx := range comboCharts { + comboCharts[idx].order = order + addChart(xlsxChartSpace.Chart.PlotArea, plotAreaFunc[comboCharts[idx].Type](comboCharts[idx])) + order += len(comboCharts[idx].Series) } chart, _ := xml.Marshal(xlsxChartSpace) media := "xl/charts/chart" + strconv.Itoa(count+1) + ".xml" @@ -1462,15 +1476,11 @@ func (f *File) drawChartShape(formatSet *formatChart) *attrValString { // drawChartSeries provides a function to draw the c:ser element by given // format sets. func (f *File) drawChartSeries(formatSet *formatChart) *[]cSer { - var baseIdx int - if formatSet.Combo != nil { - baseIdx = len(formatSet.Combo.Series) - } ser := []cSer{} for k := range formatSet.Series { ser = append(ser, cSer{ - IDx: &attrValInt{Val: intPtr(k + baseIdx)}, - Order: &attrValInt{Val: intPtr(k + baseIdx)}, + IDx: &attrValInt{Val: intPtr(k + formatSet.order)}, + Order: &attrValInt{Val: intPtr(k + formatSet.order)}, Tx: &cTx{ StrRef: &cStrRef{ F: formatSet.Series[k].Name, @@ -1506,9 +1516,9 @@ func (f *File) drawChartSeriesSpPr(i int, formatSet *formatChart) *cSpPr { Cap: "rnd", // rnd, sq, flat }, } - if i < 6 { + if i+formatSet.order < 6 { spPrLine.Ln.SolidFill = &aSolidFill{ - SchemeClr: &aSchemeClr{Val: "accent" + strconv.Itoa(i+1)}, + SchemeClr: &aSchemeClr{Val: "accent" + strconv.Itoa(i+formatSet.order+1)}, } } chartSeriesSpPr := map[string]*cSpPr{Line: spPrLine, Scatter: spPrScatter} diff --git a/chart_test.go b/chart_test.go index e350657..bb7d12c 100644 --- a/chart_test.go +++ b/chart_test.go @@ -186,7 +186,7 @@ func TestAddChart(t *testing.T) { "Y1": {"doughnut", "Clustered Column - Doughnut Chart"}, } for axis, props := range clusteredColumnCombo { - assert.NoError(t, f.AddChart("Combo Charts", axis, fmt.Sprintf(`{"type":"col","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"}],"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":"left","show_legend_key":false},"title":{"name":"%s"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"combo":{"type":"%s","series":[{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true}}}`, props[1], props[0]))) + assert.NoError(t, f.AddChart("Combo Charts", axis, fmt.Sprintf(`{"type":"col","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"}],"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":"left","show_legend_key":false},"title":{"name":"%s"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true}}`, props[1]), fmt.Sprintf(`{"type":"%s","series":[{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true}}`, props[0]))) } stackedAreaCombo := map[string][]string{ "A16": {"line", "Stacked Area - Line Chart"}, @@ -195,9 +195,10 @@ func TestAddChart(t *testing.T) { "Y16": {"doughnut", "Stacked Area - Doughnut Chart"}, } for axis, props := range stackedAreaCombo { - assert.NoError(t, f.AddChart("Combo Charts", axis, fmt.Sprintf(`{"type":"areaStacked","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"}],"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":"left","show_legend_key":false},"title":{"name":"%s"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true},"combo":{"type":"%s","series":[{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true}}}`, props[1], props[0]))) + assert.NoError(t, f.AddChart("Combo Charts", axis, fmt.Sprintf(`{"type":"areaStacked","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"}],"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":"left","show_legend_key":false},"title":{"name":"%s"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true}}`, props[1]), fmt.Sprintf(`{"type":"%s","series":[{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true}}`, props[0]))) } assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddChart.xlsx"))) // Test with unsupported chart type assert.EqualError(t, f.AddChart("Sheet2", "BD32", `{"type":"unknown","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"Bubble 3D 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"}`), "unsupported chart type unknown") + assert.EqualError(t, f.AddChart("Sheet2", "BD64", `{"type":"barOfPie","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$A$30:$D$37","values":"Sheet1!$B$30:$B$37"}],"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":"left","show_legend_key":false},"title":{"name":"Bar of Pie 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","x_axis":{"major_grid_lines":true},"y_axis":{"major_grid_lines":true}}`, `{"type":"unknown","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$A$30:$D$37","values":"Sheet1!$B$30:$B$37"}],"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":"left","show_legend_key":false},"title":{"name":"Bar of Pie 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","x_axis":{"major_grid_lines":true},"y_axis":{"major_grid_lines":true}}`), "unsupported chart type unknown") } diff --git a/xmlChart.go b/xmlChart.go index 9d6263f..5511469 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -593,11 +593,11 @@ type formatChart struct { } `json:"fill"` Layout formatLayout `json:"layout"` } `json:"plotarea"` - ShowBlanksAs string `json:"show_blanks_as"` - ShowHiddenData bool `json:"show_hidden_data"` - SetRotation int `json:"set_rotation"` - SetHoleSize int `json:"set_hole_size"` - Combo *formatChart `json:"combo"` + ShowBlanksAs string `json:"show_blanks_as"` + ShowHiddenData bool `json:"show_hidden_data"` + SetRotation int `json:"set_rotation"` + SetHoleSize int `json:"set_hole_size"` + order int } // formatChartLegend directly maps the format settings of the chart legend. -- cgit v1.2.1 From 0bb245523aada34c7b3d30f0f6e9b16d9f78e7b8 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 19 Jan 2020 00:23:00 +0800 Subject: Resolve #557, init delete chart support --- chart.go | 1240 +++------------------------------------------------------ chart_test.go | 24 ++ drawing.go | 1209 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 1287 insertions(+), 1186 deletions(-) create mode 100644 drawing.go diff --git a/chart.go b/chart.go index 738ed4b..5f06c55 100644 --- a/chart.go +++ b/chart.go @@ -12,11 +12,9 @@ package excelize import ( "bytes" "encoding/json" - "encoding/xml" "errors" + "fmt" "io" - "log" - "reflect" "strconv" "strings" ) @@ -765,1205 +763,75 @@ func (f *File) AddChart(sheet, cell, format string, combo ...string) error { return err } -// countCharts provides a function to get chart files count storage in the -// folder xl/charts. -func (f *File) countCharts() int { - count := 0 - for k := range f.XLSX { - if strings.Contains(k, "xl/charts/chart") { - count++ - } - } - return count -} - -// prepareDrawing provides a function to prepare drawing ID and XML by given -// drawingID, worksheet name and default drawingXML. -func (f *File) prepareDrawing(xlsx *xlsxWorksheet, drawingID int, sheet, drawingXML string) (int, string) { - sheetRelationshipsDrawingXML := "../drawings/drawing" + strconv.Itoa(drawingID) + ".xml" - if xlsx.Drawing != nil { - // The worksheet already has a picture or chart relationships, use the relationships drawing ../drawings/drawing%d.xml. - sheetRelationshipsDrawingXML = f.getSheetRelationshipsTargetByID(sheet, xlsx.Drawing.RID) - drawingID, _ = strconv.Atoi(strings.TrimSuffix(strings.TrimPrefix(sheetRelationshipsDrawingXML, "../drawings/drawing"), ".xml")) - drawingXML = strings.Replace(sheetRelationshipsDrawingXML, "..", "xl", -1) - } else { - // Add first picture for given sheet. - sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(f.sheetMap[trimSheetName(sheet)], "xl/worksheets/") + ".rels" - rID := f.addRels(sheetRels, SourceRelationshipDrawingML, sheetRelationshipsDrawingXML, "") - f.addSheetDrawing(sheet, rID) +// DeleteChart provides a function to delete chart in XLSX by given worksheet +// and cell name. +func (f *File) DeleteChart(sheet, cell string) (err error) { + var wsDr *xlsxWsDr + col, row, err := CellNameToCoordinates(cell) + if err != nil { + return } - return drawingID, drawingXML -} - -// addChart provides a function to create chart as xl/charts/chart%d.xml by -// given format sets. -func (f *File) addChart(formatSet *formatChart, comboCharts []*formatChart) { - count := f.countCharts() - xlsxChartSpace := xlsxChartSpace{ - XMLNSc: NameSpaceDrawingMLChart, - XMLNSa: NameSpaceDrawingML, - XMLNSr: SourceRelationship, - XMLNSc16r2: SourceRelationshipChart201506, - Date1904: &attrValBool{Val: boolPtr(false)}, - Lang: &attrValString{Val: stringPtr("en-US")}, - RoundedCorners: &attrValBool{Val: boolPtr(false)}, - Chart: cChart{ - Title: &cTitle{ - Tx: cTx{ - Rich: &cRich{ - P: aP{ - PPr: &aPPr{ - DefRPr: aRPr{ - Kern: 1200, - Strike: "noStrike", - U: "none", - Sz: 1400, - SolidFill: &aSolidFill{ - SchemeClr: &aSchemeClr{ - Val: "tx1", - LumMod: &attrValInt{ - Val: intPtr(65000), - }, - LumOff: &attrValInt{ - Val: intPtr(35000), - }, - }, - }, - Ea: &aEa{ - Typeface: "+mn-ea", - }, - Cs: &aCs{ - Typeface: "+mn-cs", - }, - Latin: &aLatin{ - Typeface: "+mn-lt", - }, - }, - }, - R: &aR{ - RPr: aRPr{ - Lang: "en-US", - AltLang: "en-US", - }, - T: formatSet.Title.Name, - }, - }, - }, - }, - TxPr: cTxPr{ - P: aP{ - PPr: &aPPr{ - DefRPr: aRPr{ - Kern: 1200, - U: "none", - Sz: 14000, - Strike: "noStrike", - }, - }, - EndParaRPr: &aEndParaRPr{ - Lang: "en-US", - }, - }, - }, - Overlay: &attrValBool{Val: boolPtr(false)}, - }, - View3D: &cView3D{ - RotX: &attrValInt{Val: intPtr(chartView3DRotX[formatSet.Type])}, - RotY: &attrValInt{Val: intPtr(chartView3DRotY[formatSet.Type])}, - Perspective: &attrValInt{Val: intPtr(chartView3DPerspective[formatSet.Type])}, - RAngAx: &attrValInt{Val: intPtr(chartView3DRAngAx[formatSet.Type])}, - }, - Floor: &cThicknessSpPr{ - Thickness: &attrValInt{Val: intPtr(0)}, - }, - SideWall: &cThicknessSpPr{ - Thickness: &attrValInt{Val: intPtr(0)}, - }, - BackWall: &cThicknessSpPr{ - Thickness: &attrValInt{Val: intPtr(0)}, - }, - PlotArea: &cPlotArea{}, - Legend: &cLegend{ - LegendPos: &attrValString{Val: stringPtr(chartLegendPosition[formatSet.Legend.Position])}, - Overlay: &attrValBool{Val: boolPtr(false)}, - }, - - PlotVisOnly: &attrValBool{Val: boolPtr(false)}, - DispBlanksAs: &attrValString{Val: stringPtr(formatSet.ShowBlanksAs)}, - ShowDLblsOverMax: &attrValBool{Val: boolPtr(false)}, - }, - SpPr: &cSpPr{ - SolidFill: &aSolidFill{ - SchemeClr: &aSchemeClr{Val: "bg1"}, - }, - Ln: &aLn{ - W: 9525, - Cap: "flat", - Cmpd: "sng", - Algn: "ctr", - SolidFill: &aSolidFill{ - SchemeClr: &aSchemeClr{Val: "tx1", - LumMod: &attrValInt{ - Val: intPtr(15000), - }, - LumOff: &attrValInt{ - Val: intPtr(85000), - }, - }, - }, - }, - }, - PrintSettings: &cPrintSettings{ - PageMargins: &cPageMargins{ - B: 0.75, - L: 0.7, - R: 0.7, - T: 0.7, - Header: 0.3, - Footer: 0.3, - }, - }, + col-- + row-- + ws, err := f.workSheetReader(sheet) + if err != nil { + return } - plotAreaFunc := map[string]func(*formatChart) *cPlotArea{ - Area: f.drawBaseChart, - AreaStacked: f.drawBaseChart, - AreaPercentStacked: f.drawBaseChart, - Area3D: f.drawBaseChart, - Area3DStacked: f.drawBaseChart, - Area3DPercentStacked: f.drawBaseChart, - Bar: f.drawBaseChart, - BarStacked: f.drawBaseChart, - BarPercentStacked: f.drawBaseChart, - Bar3DClustered: f.drawBaseChart, - Bar3DStacked: f.drawBaseChart, - Bar3DPercentStacked: f.drawBaseChart, - Bar3DConeClustered: f.drawBaseChart, - Bar3DConeStacked: f.drawBaseChart, - Bar3DConePercentStacked: f.drawBaseChart, - Bar3DPyramidClustered: f.drawBaseChart, - Bar3DPyramidStacked: f.drawBaseChart, - Bar3DPyramidPercentStacked: f.drawBaseChart, - Bar3DCylinderClustered: f.drawBaseChart, - Bar3DCylinderStacked: f.drawBaseChart, - Bar3DCylinderPercentStacked: f.drawBaseChart, - Col: f.drawBaseChart, - ColStacked: f.drawBaseChart, - ColPercentStacked: f.drawBaseChart, - Col3D: f.drawBaseChart, - Col3DClustered: f.drawBaseChart, - Col3DStacked: f.drawBaseChart, - Col3DPercentStacked: f.drawBaseChart, - Col3DCone: f.drawBaseChart, - Col3DConeClustered: f.drawBaseChart, - Col3DConeStacked: f.drawBaseChart, - Col3DConePercentStacked: f.drawBaseChart, - Col3DPyramid: f.drawBaseChart, - Col3DPyramidClustered: f.drawBaseChart, - Col3DPyramidStacked: f.drawBaseChart, - Col3DPyramidPercentStacked: f.drawBaseChart, - Col3DCylinder: f.drawBaseChart, - Col3DCylinderClustered: f.drawBaseChart, - Col3DCylinderStacked: f.drawBaseChart, - Col3DCylinderPercentStacked: f.drawBaseChart, - Doughnut: f.drawDoughnutChart, - Line: f.drawLineChart, - Pie3D: f.drawPie3DChart, - Pie: f.drawPieChart, - PieOfPieChart: f.drawPieOfPieChart, - BarOfPieChart: f.drawBarOfPieChart, - Radar: f.drawRadarChart, - Scatter: f.drawScatterChart, - Surface3D: f.drawSurface3DChart, - WireframeSurface3D: f.drawSurface3DChart, - Contour: f.drawSurfaceChart, - WireframeContour: f.drawSurfaceChart, - Bubble: f.drawBaseChart, - Bubble3D: f.drawBaseChart, + if ws.Drawing == nil { + return } - addChart := func(c, p *cPlotArea) { - immutable, mutable := reflect.ValueOf(c).Elem(), reflect.ValueOf(p).Elem() - for i := 0; i < mutable.NumField(); i++ { - field := mutable.Field(i) - if field.IsNil() { - continue + drawingXML := strings.Replace(f.getSheetRelationshipsTargetByID(sheet, ws.Drawing.RID), "..", "xl", -1) + wsDr, _ = f.drawingParser(drawingXML) + for idx, anchor := range wsDr.TwoCellAnchor { + if err = nil; anchor.From != nil && anchor.Pic == nil { + if anchor.From.Col == col && anchor.From.Row == row { + wsDr.TwoCellAnchor = append(wsDr.TwoCellAnchor[:idx], wsDr.TwoCellAnchor[idx+1:]...) } - immutable.FieldByName(mutable.Type().Field(i).Name).Set(field) - } - } - addChart(xlsxChartSpace.Chart.PlotArea, plotAreaFunc[formatSet.Type](formatSet)) - order := len(formatSet.Series) - for idx := range comboCharts { - comboCharts[idx].order = order - addChart(xlsxChartSpace.Chart.PlotArea, plotAreaFunc[comboCharts[idx].Type](comboCharts[idx])) - order += len(comboCharts[idx].Series) - } - chart, _ := xml.Marshal(xlsxChartSpace) - media := "xl/charts/chart" + strconv.Itoa(count+1) + ".xml" - f.saveFileList(media, chart) -} - -// drawBaseChart provides a function to draw the c:plotArea element for bar, -// and column series charts by given format sets. -func (f *File) drawBaseChart(formatSet *formatChart) *cPlotArea { - c := cCharts{ - BarDir: &attrValString{ - Val: stringPtr("col"), - }, - Grouping: &attrValString{ - Val: stringPtr("clustered"), - }, - VaryColors: &attrValBool{ - Val: boolPtr(true), - }, - Ser: f.drawChartSeries(formatSet), - Shape: f.drawChartShape(formatSet), - DLbls: f.drawChartDLbls(formatSet), - AxID: []*attrValInt{ - {Val: intPtr(754001152)}, - {Val: intPtr(753999904)}, - }, - Overlap: &attrValInt{Val: intPtr(100)}, - } - var ok bool - if *c.BarDir.Val, ok = plotAreaChartBarDir[formatSet.Type]; !ok { - c.BarDir = nil - } - if *c.Grouping.Val, ok = plotAreaChartGrouping[formatSet.Type]; !ok { - c.Grouping = nil - } - if *c.Overlap.Val, ok = plotAreaChartOverlap[formatSet.Type]; !ok { - c.Overlap = nil - } - catAx := f.drawPlotAreaCatAx(formatSet) - valAx := f.drawPlotAreaValAx(formatSet) - charts := map[string]*cPlotArea{ - "area": { - AreaChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "areaStacked": { - AreaChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "areaPercentStacked": { - AreaChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "area3D": { - Area3DChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "area3DStacked": { - Area3DChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "area3DPercentStacked": { - Area3DChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "bar": { - BarChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "barStacked": { - BarChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "barPercentStacked": { - BarChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "bar3DClustered": { - Bar3DChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "bar3DStacked": { - Bar3DChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "bar3DPercentStacked": { - Bar3DChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "bar3DConeClustered": { - Bar3DChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "bar3DConeStacked": { - Bar3DChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "bar3DConePercentStacked": { - Bar3DChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "bar3DPyramidClustered": { - Bar3DChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "bar3DPyramidStacked": { - Bar3DChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "bar3DPyramidPercentStacked": { - Bar3DChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "bar3DCylinderClustered": { - Bar3DChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "bar3DCylinderStacked": { - Bar3DChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "bar3DCylinderPercentStacked": { - Bar3DChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "col": { - BarChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "colStacked": { - BarChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "colPercentStacked": { - BarChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "col3D": { - Bar3DChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "col3DClustered": { - Bar3DChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "col3DStacked": { - Bar3DChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "col3DPercentStacked": { - Bar3DChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "col3DCone": { - Bar3DChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "col3DConeClustered": { - Bar3DChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "col3DConeStacked": { - Bar3DChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "col3DConePercentStacked": { - Bar3DChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "col3DPyramid": { - Bar3DChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "col3DPyramidClustered": { - Bar3DChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "col3DPyramidStacked": { - Bar3DChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "col3DPyramidPercentStacked": { - Bar3DChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "col3DCylinder": { - Bar3DChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "col3DCylinderClustered": { - Bar3DChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "col3DCylinderStacked": { - Bar3DChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "col3DCylinderPercentStacked": { - Bar3DChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "bubble": { - BubbleChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "bubble3D": { - BubbleChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - } - return charts[formatSet.Type] -} - -// drawDoughnutChart provides a function to draw the c:plotArea element for -// doughnut chart by given format sets. -func (f *File) drawDoughnutChart(formatSet *formatChart) *cPlotArea { - return &cPlotArea{ - DoughnutChart: &cCharts{ - VaryColors: &attrValBool{ - Val: boolPtr(true), - }, - Ser: f.drawChartSeries(formatSet), - HoleSize: &attrValInt{Val: intPtr(75)}, - }, - } -} - -// drawLineChart provides a function to draw the c:plotArea element for line -// chart by given format sets. -func (f *File) drawLineChart(formatSet *formatChart) *cPlotArea { - return &cPlotArea{ - LineChart: &cCharts{ - Grouping: &attrValString{ - Val: stringPtr(plotAreaChartGrouping[formatSet.Type]), - }, - VaryColors: &attrValBool{ - Val: boolPtr(false), - }, - Ser: f.drawChartSeries(formatSet), - DLbls: f.drawChartDLbls(formatSet), - Smooth: &attrValBool{ - Val: boolPtr(false), - }, - AxID: []*attrValInt{ - {Val: intPtr(754001152)}, - {Val: intPtr(753999904)}, - }, - }, - CatAx: f.drawPlotAreaCatAx(formatSet), - ValAx: f.drawPlotAreaValAx(formatSet), - } -} - -// drawPieChart provides a function to draw the c:plotArea element for pie -// chart by given format sets. -func (f *File) drawPieChart(formatSet *formatChart) *cPlotArea { - return &cPlotArea{ - PieChart: &cCharts{ - VaryColors: &attrValBool{ - Val: boolPtr(true), - }, - Ser: f.drawChartSeries(formatSet), - }, - } -} - -// drawPie3DChart provides a function to draw the c:plotArea element for 3D -// pie chart by given format sets. -func (f *File) drawPie3DChart(formatSet *formatChart) *cPlotArea { - return &cPlotArea{ - Pie3DChart: &cCharts{ - VaryColors: &attrValBool{ - Val: boolPtr(true), - }, - Ser: f.drawChartSeries(formatSet), - }, - } -} - -// drawPieOfPieChart provides a function to draw the c:plotArea element for -// pie chart by given format sets. -func (f *File) drawPieOfPieChart(formatSet *formatChart) *cPlotArea { - return &cPlotArea{ - PieChart: &cCharts{ - OfPieType: &attrValString{ - Val: stringPtr("pie"), - }, - VaryColors: &attrValBool{ - Val: boolPtr(true), - }, - Ser: f.drawChartSeries(formatSet), - SerLines: &attrValString{}, - }, - } -} - -// drawBarOfPieChart provides a function to draw the c:plotArea element for -// pie chart by given format sets. -func (f *File) drawBarOfPieChart(formatSet *formatChart) *cPlotArea { - return &cPlotArea{ - PieChart: &cCharts{ - OfPieType: &attrValString{ - Val: stringPtr("bar"), - }, - VaryColors: &attrValBool{ - Val: boolPtr(true), - }, - Ser: f.drawChartSeries(formatSet), - SerLines: &attrValString{}, - }, - } -} - -// drawRadarChart provides a function to draw the c:plotArea element for radar -// chart by given format sets. -func (f *File) drawRadarChart(formatSet *formatChart) *cPlotArea { - return &cPlotArea{ - RadarChart: &cCharts{ - RadarStyle: &attrValString{ - Val: stringPtr("marker"), - }, - VaryColors: &attrValBool{ - Val: boolPtr(false), - }, - Ser: f.drawChartSeries(formatSet), - DLbls: f.drawChartDLbls(formatSet), - AxID: []*attrValInt{ - {Val: intPtr(754001152)}, - {Val: intPtr(753999904)}, - }, - }, - CatAx: f.drawPlotAreaCatAx(formatSet), - ValAx: f.drawPlotAreaValAx(formatSet), - } -} - -// drawScatterChart provides a function to draw the c:plotArea element for -// scatter chart by given format sets. -func (f *File) drawScatterChart(formatSet *formatChart) *cPlotArea { - return &cPlotArea{ - ScatterChart: &cCharts{ - ScatterStyle: &attrValString{ - Val: stringPtr("smoothMarker"), // line,lineMarker,marker,none,smooth,smoothMarker - }, - VaryColors: &attrValBool{ - Val: boolPtr(false), - }, - Ser: f.drawChartSeries(formatSet), - DLbls: f.drawChartDLbls(formatSet), - AxID: []*attrValInt{ - {Val: intPtr(754001152)}, - {Val: intPtr(753999904)}, - }, - }, - CatAx: f.drawPlotAreaCatAx(formatSet), - ValAx: f.drawPlotAreaValAx(formatSet), - } -} - -// drawSurface3DChart provides a function to draw the c:surface3DChart element by -// given format sets. -func (f *File) drawSurface3DChart(formatSet *formatChart) *cPlotArea { - plotArea := &cPlotArea{ - Surface3DChart: &cCharts{ - Ser: f.drawChartSeries(formatSet), - AxID: []*attrValInt{ - {Val: intPtr(754001152)}, - {Val: intPtr(753999904)}, - {Val: intPtr(832256642)}, - }, - }, - CatAx: f.drawPlotAreaCatAx(formatSet), - ValAx: f.drawPlotAreaValAx(formatSet), - SerAx: f.drawPlotAreaSerAx(formatSet), - } - if formatSet.Type == WireframeSurface3D { - plotArea.Surface3DChart.Wireframe = &attrValBool{Val: boolPtr(true)} - } - return plotArea -} - -// drawSurfaceChart provides a function to draw the c:surfaceChart element by -// given format sets. -func (f *File) drawSurfaceChart(formatSet *formatChart) *cPlotArea { - plotArea := &cPlotArea{ - SurfaceChart: &cCharts{ - Ser: f.drawChartSeries(formatSet), - AxID: []*attrValInt{ - {Val: intPtr(754001152)}, - {Val: intPtr(753999904)}, - {Val: intPtr(832256642)}, - }, - }, - CatAx: f.drawPlotAreaCatAx(formatSet), - ValAx: f.drawPlotAreaValAx(formatSet), - SerAx: f.drawPlotAreaSerAx(formatSet), - } - if formatSet.Type == WireframeContour { - plotArea.SurfaceChart.Wireframe = &attrValBool{Val: boolPtr(true)} - } - return plotArea -} - -// drawChartShape provides a function to draw the c:shape element by given -// format sets. -func (f *File) drawChartShape(formatSet *formatChart) *attrValString { - shapes := map[string]string{ - Bar3DConeClustered: "cone", - Bar3DConeStacked: "cone", - Bar3DConePercentStacked: "cone", - Bar3DPyramidClustered: "pyramid", - Bar3DPyramidStacked: "pyramid", - Bar3DPyramidPercentStacked: "pyramid", - Bar3DCylinderClustered: "cylinder", - Bar3DCylinderStacked: "cylinder", - Bar3DCylinderPercentStacked: "cylinder", - Col3DCone: "cone", - Col3DConeClustered: "cone", - Col3DConeStacked: "cone", - Col3DConePercentStacked: "cone", - Col3DPyramid: "pyramid", - Col3DPyramidClustered: "pyramid", - Col3DPyramidStacked: "pyramid", - Col3DPyramidPercentStacked: "pyramid", - Col3DCylinder: "cylinder", - Col3DCylinderClustered: "cylinder", - Col3DCylinderStacked: "cylinder", - Col3DCylinderPercentStacked: "cylinder", - } - if shape, ok := shapes[formatSet.Type]; ok { - return &attrValString{Val: stringPtr(shape)} - } - return nil -} - -// drawChartSeries provides a function to draw the c:ser element by given -// format sets. -func (f *File) drawChartSeries(formatSet *formatChart) *[]cSer { - ser := []cSer{} - for k := range formatSet.Series { - ser = append(ser, cSer{ - IDx: &attrValInt{Val: intPtr(k + formatSet.order)}, - Order: &attrValInt{Val: intPtr(k + formatSet.order)}, - Tx: &cTx{ - StrRef: &cStrRef{ - F: formatSet.Series[k].Name, - }, - }, - SpPr: f.drawChartSeriesSpPr(k, formatSet), - Marker: f.drawChartSeriesMarker(k, formatSet), - DPt: f.drawChartSeriesDPt(k, formatSet), - DLbls: f.drawChartSeriesDLbls(formatSet), - Cat: f.drawChartSeriesCat(formatSet.Series[k], formatSet), - Val: f.drawChartSeriesVal(formatSet.Series[k], formatSet), - XVal: f.drawChartSeriesXVal(formatSet.Series[k], formatSet), - YVal: f.drawChartSeriesYVal(formatSet.Series[k], formatSet), - BubbleSize: f.drawCharSeriesBubbleSize(formatSet.Series[k], formatSet), - Bubble3D: f.drawCharSeriesBubble3D(formatSet), - }) - } - return &ser -} - -// drawChartSeriesSpPr provides a function to draw the c:spPr element by given -// format sets. -func (f *File) drawChartSeriesSpPr(i int, formatSet *formatChart) *cSpPr { - spPrScatter := &cSpPr{ - Ln: &aLn{ - W: 25400, - NoFill: " ", - }, - } - spPrLine := &cSpPr{ - Ln: &aLn{ - W: f.ptToEMUs(formatSet.Series[i].Line.Width), - Cap: "rnd", // rnd, sq, flat - }, - } - if i+formatSet.order < 6 { - spPrLine.Ln.SolidFill = &aSolidFill{ - SchemeClr: &aSchemeClr{Val: "accent" + strconv.Itoa(i+formatSet.order+1)}, - } - } - chartSeriesSpPr := map[string]*cSpPr{Line: spPrLine, Scatter: spPrScatter} - return chartSeriesSpPr[formatSet.Type] -} - -// drawChartSeriesDPt provides a function to draw the c:dPt element by given -// data index and format sets. -func (f *File) drawChartSeriesDPt(i int, formatSet *formatChart) []*cDPt { - dpt := []*cDPt{{ - IDx: &attrValInt{Val: intPtr(i)}, - Bubble3D: &attrValBool{Val: boolPtr(false)}, - SpPr: &cSpPr{ - SolidFill: &aSolidFill{ - SchemeClr: &aSchemeClr{Val: "accent" + strconv.Itoa(i+1)}, - }, - Ln: &aLn{ - W: 25400, - Cap: "rnd", - SolidFill: &aSolidFill{ - SchemeClr: &aSchemeClr{Val: "lt" + strconv.Itoa(i+1)}, - }, - }, - Sp3D: &aSp3D{ - ContourW: 25400, - ContourClr: &aContourClr{ - SchemeClr: &aSchemeClr{Val: "lt" + strconv.Itoa(i+1)}, - }, - }, - }, - }} - chartSeriesDPt := map[string][]*cDPt{Pie: dpt, Pie3D: dpt} - return chartSeriesDPt[formatSet.Type] -} - -// drawChartSeriesCat provides a function to draw the c:cat element by given -// chart series and format sets. -func (f *File) drawChartSeriesCat(v formatChartSeries, formatSet *formatChart) *cCat { - cat := &cCat{ - StrRef: &cStrRef{ - F: v.Categories, - }, - } - chartSeriesCat := map[string]*cCat{Scatter: nil, Bubble: nil, Bubble3D: nil} - if _, ok := chartSeriesCat[formatSet.Type]; ok || v.Categories == "" { - return nil - } - return cat -} - -// drawChartSeriesVal provides a function to draw the c:val element by given -// chart series and format sets. -func (f *File) drawChartSeriesVal(v formatChartSeries, formatSet *formatChart) *cVal { - val := &cVal{ - NumRef: &cNumRef{ - F: v.Values, - }, - } - chartSeriesVal := map[string]*cVal{Scatter: nil, Bubble: nil, Bubble3D: nil} - if _, ok := chartSeriesVal[formatSet.Type]; ok { - return nil - } - return val -} - -// drawChartSeriesMarker provides a function to draw the c:marker element by -// given data index and format sets. -func (f *File) drawChartSeriesMarker(i int, formatSet *formatChart) *cMarker { - marker := &cMarker{ - Symbol: &attrValString{Val: stringPtr("circle")}, - Size: &attrValInt{Val: intPtr(5)}, - } - if i < 6 { - marker.SpPr = &cSpPr{ - SolidFill: &aSolidFill{ - SchemeClr: &aSchemeClr{ - Val: "accent" + strconv.Itoa(i+1), - }, - }, - Ln: &aLn{ - W: 9252, - SolidFill: &aSolidFill{ - SchemeClr: &aSchemeClr{ - Val: "accent" + strconv.Itoa(i+1), - }, - }, - }, } } - chartSeriesMarker := map[string]*cMarker{Scatter: marker} - return chartSeriesMarker[formatSet.Type] + return f.deleteChart(col, row, drawingXML, wsDr) } -// drawChartSeriesXVal provides a function to draw the c:xVal element by given -// chart series and format sets. -func (f *File) drawChartSeriesXVal(v formatChartSeries, formatSet *formatChart) *cCat { - cat := &cCat{ - StrRef: &cStrRef{ - F: v.Categories, - }, - } - chartSeriesXVal := map[string]*cCat{Scatter: cat} - return chartSeriesXVal[formatSet.Type] -} - -// drawChartSeriesYVal provides a function to draw the c:yVal element by given -// chart series and format sets. -func (f *File) drawChartSeriesYVal(v formatChartSeries, formatSet *formatChart) *cVal { - val := &cVal{ - NumRef: &cNumRef{ - F: v.Values, - }, - } - chartSeriesYVal := map[string]*cVal{Scatter: val, Bubble: val, Bubble3D: val} - return chartSeriesYVal[formatSet.Type] -} - -// drawCharSeriesBubbleSize provides a function to draw the c:bubbleSize -// element by given chart series and format sets. -func (f *File) drawCharSeriesBubbleSize(v formatChartSeries, formatSet *formatChart) *cVal { - if _, ok := map[string]bool{Bubble: true, Bubble3D: true}[formatSet.Type]; !ok { - return nil - } - return &cVal{ - NumRef: &cNumRef{ - F: v.Values, - }, - } -} - -// drawCharSeriesBubble3D provides a function to draw the c:bubble3D element -// by given format sets. -func (f *File) drawCharSeriesBubble3D(formatSet *formatChart) *attrValBool { - if _, ok := map[string]bool{Bubble3D: true}[formatSet.Type]; !ok { - return nil - } - return &attrValBool{Val: boolPtr(true)} -} - -// drawChartDLbls provides a function to draw the c:dLbls element by given -// format sets. -func (f *File) drawChartDLbls(formatSet *formatChart) *cDLbls { - return &cDLbls{ - ShowLegendKey: &attrValBool{Val: boolPtr(formatSet.Legend.ShowLegendKey)}, - ShowVal: &attrValBool{Val: boolPtr(formatSet.Plotarea.ShowVal)}, - ShowCatName: &attrValBool{Val: boolPtr(formatSet.Plotarea.ShowCatName)}, - ShowSerName: &attrValBool{Val: boolPtr(formatSet.Plotarea.ShowSerName)}, - ShowBubbleSize: &attrValBool{Val: boolPtr(formatSet.Plotarea.ShowBubbleSize)}, - ShowPercent: &attrValBool{Val: boolPtr(formatSet.Plotarea.ShowPercent)}, - ShowLeaderLines: &attrValBool{Val: boolPtr(formatSet.Plotarea.ShowLeaderLines)}, - } -} - -// drawChartSeriesDLbls provides a function to draw the c:dLbls element by -// given format sets. -func (f *File) drawChartSeriesDLbls(formatSet *formatChart) *cDLbls { - dLbls := f.drawChartDLbls(formatSet) - chartSeriesDLbls := map[string]*cDLbls{Scatter: nil, Surface3D: nil, WireframeSurface3D: nil, Contour: nil, WireframeContour: nil, Bubble: nil, Bubble3D: nil} - if _, ok := chartSeriesDLbls[formatSet.Type]; ok { - return nil - } - return dLbls -} - -// drawPlotAreaCatAx provides a function to draw the c:catAx element. -func (f *File) drawPlotAreaCatAx(formatSet *formatChart) []*cAxs { - min := &attrValFloat{Val: float64Ptr(formatSet.XAxis.Minimum)} - max := &attrValFloat{Val: float64Ptr(formatSet.XAxis.Maximum)} - if formatSet.XAxis.Minimum == 0 { - min = nil - } - if formatSet.XAxis.Maximum == 0 { - max = nil - } - axs := []*cAxs{ - { - AxID: &attrValInt{Val: intPtr(754001152)}, - Scaling: &cScaling{ - Orientation: &attrValString{Val: stringPtr(orientation[formatSet.XAxis.ReverseOrder])}, - Max: max, - Min: min, - }, - Delete: &attrValBool{Val: boolPtr(false)}, - AxPos: &attrValString{Val: stringPtr(catAxPos[formatSet.XAxis.ReverseOrder])}, - NumFmt: &cNumFmt{ - FormatCode: "General", - SourceLinked: true, - }, - MajorTickMark: &attrValString{Val: stringPtr("none")}, - MinorTickMark: &attrValString{Val: stringPtr("none")}, - TickLblPos: &attrValString{Val: stringPtr("nextTo")}, - SpPr: f.drawPlotAreaSpPr(), - TxPr: f.drawPlotAreaTxPr(), - CrossAx: &attrValInt{Val: intPtr(753999904)}, - Crosses: &attrValString{Val: stringPtr("autoZero")}, - Auto: &attrValBool{Val: boolPtr(true)}, - LblAlgn: &attrValString{Val: stringPtr("ctr")}, - LblOffset: &attrValInt{Val: intPtr(100)}, - NoMultiLvlLbl: &attrValBool{Val: boolPtr(false)}, - }, - } - if formatSet.XAxis.MajorGridlines { - axs[0].MajorGridlines = &cChartLines{SpPr: f.drawPlotAreaSpPr()} - } - if formatSet.XAxis.MinorGridlines { - axs[0].MinorGridlines = &cChartLines{SpPr: f.drawPlotAreaSpPr()} - } - if formatSet.XAxis.TickLabelSkip != 0 { - axs[0].TickLblSkip = &attrValInt{Val: intPtr(formatSet.XAxis.TickLabelSkip)} - } - return axs -} - -// drawPlotAreaValAx provides a function to draw the c:valAx element. -func (f *File) drawPlotAreaValAx(formatSet *formatChart) []*cAxs { - min := &attrValFloat{Val: float64Ptr(formatSet.YAxis.Minimum)} - max := &attrValFloat{Val: float64Ptr(formatSet.YAxis.Maximum)} - if formatSet.YAxis.Minimum == 0 { - min = nil - } - if formatSet.YAxis.Maximum == 0 { - max = nil - } - axs := []*cAxs{ - { - AxID: &attrValInt{Val: intPtr(753999904)}, - Scaling: &cScaling{ - Orientation: &attrValString{Val: stringPtr(orientation[formatSet.YAxis.ReverseOrder])}, - Max: max, - Min: min, - }, - Delete: &attrValBool{Val: boolPtr(false)}, - AxPos: &attrValString{Val: stringPtr(valAxPos[formatSet.YAxis.ReverseOrder])}, - NumFmt: &cNumFmt{ - FormatCode: chartValAxNumFmtFormatCode[formatSet.Type], - SourceLinked: true, - }, - MajorTickMark: &attrValString{Val: stringPtr("none")}, - MinorTickMark: &attrValString{Val: stringPtr("none")}, - TickLblPos: &attrValString{Val: stringPtr("nextTo")}, - SpPr: f.drawPlotAreaSpPr(), - TxPr: f.drawPlotAreaTxPr(), - CrossAx: &attrValInt{Val: intPtr(754001152)}, - Crosses: &attrValString{Val: stringPtr("autoZero")}, - CrossBetween: &attrValString{Val: stringPtr(chartValAxCrossBetween[formatSet.Type])}, - }, - } - if formatSet.YAxis.MajorGridlines { - axs[0].MajorGridlines = &cChartLines{SpPr: f.drawPlotAreaSpPr()} - } - if formatSet.YAxis.MinorGridlines { - axs[0].MinorGridlines = &cChartLines{SpPr: f.drawPlotAreaSpPr()} - } - if pos, ok := valTickLblPos[formatSet.Type]; ok { - axs[0].TickLblPos.Val = stringPtr(pos) - } - if formatSet.YAxis.MajorUnit != 0 { - axs[0].MajorUnit = &attrValFloat{Val: float64Ptr(formatSet.YAxis.MajorUnit)} - } - return axs -} - -// drawPlotAreaSerAx provides a function to draw the c:serAx element. -func (f *File) drawPlotAreaSerAx(formatSet *formatChart) []*cAxs { - min := &attrValFloat{Val: float64Ptr(formatSet.YAxis.Minimum)} - max := &attrValFloat{Val: float64Ptr(formatSet.YAxis.Maximum)} - if formatSet.YAxis.Minimum == 0 { - min = nil - } - if formatSet.YAxis.Maximum == 0 { - max = nil - } - return []*cAxs{ - { - AxID: &attrValInt{Val: intPtr(832256642)}, - Scaling: &cScaling{ - Orientation: &attrValString{Val: stringPtr(orientation[formatSet.YAxis.ReverseOrder])}, - Max: max, - Min: min, - }, - Delete: &attrValBool{Val: boolPtr(false)}, - AxPos: &attrValString{Val: stringPtr(catAxPos[formatSet.XAxis.ReverseOrder])}, - TickLblPos: &attrValString{Val: stringPtr("nextTo")}, - SpPr: f.drawPlotAreaSpPr(), - TxPr: f.drawPlotAreaTxPr(), - CrossAx: &attrValInt{Val: intPtr(753999904)}, - }, - } -} - -// drawPlotAreaSpPr provides a function to draw the c:spPr element. -func (f *File) drawPlotAreaSpPr() *cSpPr { - return &cSpPr{ - Ln: &aLn{ - W: 9525, - Cap: "flat", - Cmpd: "sng", - Algn: "ctr", - SolidFill: &aSolidFill{ - SchemeClr: &aSchemeClr{ - Val: "tx1", - LumMod: &attrValInt{Val: intPtr(15000)}, - LumOff: &attrValInt{Val: intPtr(85000)}, - }, - }, - }, - } -} - -// drawPlotAreaTxPr provides a function to draw the c:txPr element. -func (f *File) drawPlotAreaTxPr() *cTxPr { - return &cTxPr{ - BodyPr: aBodyPr{ - Rot: -60000000, - SpcFirstLastPara: true, - VertOverflow: "ellipsis", - Vert: "horz", - Wrap: "square", - Anchor: "ctr", - AnchorCtr: true, - }, - P: aP{ - PPr: &aPPr{ - DefRPr: aRPr{ - Sz: 900, - B: false, - I: false, - U: "none", - Strike: "noStrike", - Kern: 1200, - Baseline: 0, - SolidFill: &aSolidFill{ - SchemeClr: &aSchemeClr{ - Val: "tx1", - LumMod: &attrValInt{Val: intPtr(15000)}, - LumOff: &attrValInt{Val: intPtr(85000)}, - }, - }, - Latin: &aLatin{Typeface: "+mn-lt"}, - Ea: &aEa{Typeface: "+mn-ea"}, - Cs: &aCs{Typeface: "+mn-cs"}, - }, - }, - EndParaRPr: &aEndParaRPr{Lang: "en-US"}, - }, - } -} - -// drawingParser provides a function to parse drawingXML. In order to solve -// the problem that the label structure is changed after serialization and -// deserialization, two different structures: decodeWsDr and encodeWsDr are -// defined. -func (f *File) drawingParser(path string) (*xlsxWsDr, int) { +// deleteChart provides a function to delete chart graphic frame by given by +// given coordinates. +func (f *File) deleteChart(col, row int, drawingXML string, wsDr *xlsxWsDr) (err error) { var ( - err error - ok bool + deWsDr *decodeWsDr + deTwoCellAnchor *decodeTwoCellAnchor ) - - if f.Drawings[path] == nil { - content := xlsxWsDr{} - content.A = NameSpaceDrawingML - content.Xdr = NameSpaceDrawingMLSpreadSheet - if _, ok = f.XLSX[path]; ok { // Append Model - decodeWsDr := decodeWsDr{} - if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(path)))). - Decode(&decodeWsDr); err != nil && err != io.EOF { - log.Printf("xml decode error: %s", err) - } - content.R = decodeWsDr.R - for _, v := range decodeWsDr.OneCellAnchor { - content.OneCellAnchor = append(content.OneCellAnchor, &xdrCellAnchor{ - EditAs: v.EditAs, - GraphicFrame: v.Content, - }) - } - for _, v := range decodeWsDr.TwoCellAnchor { - content.TwoCellAnchor = append(content.TwoCellAnchor, &xdrCellAnchor{ - EditAs: v.EditAs, - GraphicFrame: v.Content, - }) + deWsDr = new(decodeWsDr) + if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(drawingXML)))). + Decode(deWsDr); err != nil && err != io.EOF { + err = fmt.Errorf("xml decode error: %s", err) + return + } + for idx, anchor := range deWsDr.TwoCellAnchor { + deTwoCellAnchor = new(decodeTwoCellAnchor) + if err = f.xmlNewDecoder(bytes.NewReader([]byte("" + anchor.Content + ""))). + Decode(deTwoCellAnchor); err != nil && err != io.EOF { + err = fmt.Errorf("xml decode error: %s", err) + return + } + if err = nil; deTwoCellAnchor.From != nil && deTwoCellAnchor.Pic == nil { + if anchor.From.Col == col && anchor.From.Row == row { + wsDr.TwoCellAnchor = append(wsDr.TwoCellAnchor[:idx], wsDr.TwoCellAnchor[idx+1:]...) } } - f.Drawings[path] = &content } - wsDr := f.Drawings[path] - return wsDr, len(wsDr.OneCellAnchor) + len(wsDr.TwoCellAnchor) + 2 + f.Drawings[drawingXML] = wsDr + return err } -// 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) error { - col, row, err := CellNameToCoordinates(cell) - if err != nil { - return err - } - 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, colIdx, rowIdx, formatSet.OffsetX, formatSet.OffsetY, width, height) - content, cNvPrID := f.drawingParser(drawingXML) - twoCellAnchor := xdrCellAnchor{} - twoCellAnchor.EditAs = formatSet.Positioning - from := xlsxFrom{} - from.Col = colStart - from.ColOff = formatSet.OffsetX * EMU - from.Row = rowStart - from.RowOff = formatSet.OffsetY * EMU - to := xlsxTo{} - to.Col = colEnd - to.ColOff = x2 * EMU - to.Row = rowEnd - to.RowOff = y2 * EMU - twoCellAnchor.From = &from - twoCellAnchor.To = &to - - graphicFrame := xlsxGraphicFrame{ - NvGraphicFramePr: xlsxNvGraphicFramePr{ - CNvPr: &xlsxCNvPr{ - ID: cNvPrID, - Name: "Chart " + strconv.Itoa(cNvPrID), - }, - }, - Graphic: &xlsxGraphic{ - GraphicData: &xlsxGraphicData{ - URI: NameSpaceDrawingMLChart, - Chart: &xlsxChart{ - C: NameSpaceDrawingMLChart, - R: SourceRelationship, - RID: "rId" + strconv.Itoa(rID), - }, - }, - }, - } - graphic, _ := xml.Marshal(graphicFrame) - twoCellAnchor.GraphicFrame = string(graphic) - twoCellAnchor.ClientData = &xdrClientData{ - FLocksWithSheet: formatSet.FLocksWithSheet, - FPrintsWithSheet: formatSet.FPrintsWithSheet, +// countCharts provides a function to get chart files count storage in the +// folder xl/charts. +func (f *File) countCharts() int { + count := 0 + for k := range f.XLSX { + if strings.Contains(k, "xl/charts/chart") { + count++ + } } - content.TwoCellAnchor = append(content.TwoCellAnchor, &twoCellAnchor) - f.Drawings[drawingXML] = content - return err + return count } // ptToEMUs provides a function to convert pt to EMUs, 1 pt = 12700 EMUs. The diff --git a/chart_test.go b/chart_test.go index bb7d12c..d8d36d8 100644 --- a/chart_test.go +++ b/chart_test.go @@ -200,5 +200,29 @@ func TestAddChart(t *testing.T) { assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddChart.xlsx"))) // Test with unsupported chart type assert.EqualError(t, f.AddChart("Sheet2", "BD32", `{"type":"unknown","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"Bubble 3D 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"}`), "unsupported chart type unknown") + // Test add combo chart with invalid format set. + assert.EqualError(t, f.AddChart("Sheet2", "BD32", `{"type":"col","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"2D Column 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"}`, ""), "unexpected end of JSON input") + // Test add combo chart with unsupported chart type. assert.EqualError(t, f.AddChart("Sheet2", "BD64", `{"type":"barOfPie","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$A$30:$D$37","values":"Sheet1!$B$30:$B$37"}],"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":"left","show_legend_key":false},"title":{"name":"Bar of Pie 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","x_axis":{"major_grid_lines":true},"y_axis":{"major_grid_lines":true}}`, `{"type":"unknown","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$A$30:$D$37","values":"Sheet1!$B$30:$B$37"}],"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":"left","show_legend_key":false},"title":{"name":"Bar of Pie 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","x_axis":{"major_grid_lines":true},"y_axis":{"major_grid_lines":true}}`), "unsupported chart type unknown") } + +func TestDeleteChart(t *testing.T) { + f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) + assert.NoError(t, err) + assert.NoError(t, f.DeleteChart("Sheet1", "A1")) + assert.NoError(t, f.AddChart("Sheet1", "P1", `{"type":"col","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"2D Column 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"}`)) + assert.NoError(t, f.DeleteChart("Sheet1", "P1")) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestDeleteChart.xlsx"))) + // Test delete chart on not exists worksheet. + assert.EqualError(t, f.DeleteChart("SheetN", "A1"), "sheet SheetN is not exist") + // Test delete chart with invalid coordinates. + assert.EqualError(t, f.DeleteChart("Sheet1", ""), `cannot convert cell "" to coordinates: invalid cell name ""`) + // Test delete chart with unsupport charset. + f, err = OpenFile(filepath.Join("test", "Book1.xlsx")) + assert.NoError(t, err) + delete(f.Sheet, "xl/drawings/drawing1.xml") + f.XLSX["xl/drawings/drawing1.xml"] = MacintoshCyrillicCharset + assert.EqualError(t, f.DeleteChart("Sheet1", "A1"), "xml decode error: XML syntax error on line 1: invalid UTF-8") + // Test delete chart on no chart worksheet. + assert.NoError(t, NewFile().DeleteChart("Sheet1", "A1")) +} diff --git a/drawing.go b/drawing.go new file mode 100644 index 0000000..49506a3 --- /dev/null +++ b/drawing.go @@ -0,0 +1,1209 @@ +// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of +// this source code is governed by a BSD-style license that can be found in +// the LICENSE file. +// +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.10 or later. + +package excelize + +import ( + "bytes" + "encoding/xml" + "io" + "log" + "reflect" + "strconv" + "strings" +) + +// prepareDrawing provides a function to prepare drawing ID and XML by given +// drawingID, worksheet name and default drawingXML. +func (f *File) prepareDrawing(xlsx *xlsxWorksheet, drawingID int, sheet, drawingXML string) (int, string) { + sheetRelationshipsDrawingXML := "../drawings/drawing" + strconv.Itoa(drawingID) + ".xml" + if xlsx.Drawing != nil { + // The worksheet already has a picture or chart relationships, use the relationships drawing ../drawings/drawing%d.xml. + sheetRelationshipsDrawingXML = f.getSheetRelationshipsTargetByID(sheet, xlsx.Drawing.RID) + drawingID, _ = strconv.Atoi(strings.TrimSuffix(strings.TrimPrefix(sheetRelationshipsDrawingXML, "../drawings/drawing"), ".xml")) + drawingXML = strings.Replace(sheetRelationshipsDrawingXML, "..", "xl", -1) + } else { + // Add first picture for given sheet. + sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(f.sheetMap[trimSheetName(sheet)], "xl/worksheets/") + ".rels" + rID := f.addRels(sheetRels, SourceRelationshipDrawingML, sheetRelationshipsDrawingXML, "") + f.addSheetDrawing(sheet, rID) + } + return drawingID, drawingXML +} + +// addChart provides a function to create chart as xl/charts/chart%d.xml by +// given format sets. +func (f *File) addChart(formatSet *formatChart, comboCharts []*formatChart) { + count := f.countCharts() + xlsxChartSpace := xlsxChartSpace{ + XMLNSc: NameSpaceDrawingMLChart, + XMLNSa: NameSpaceDrawingML, + XMLNSr: SourceRelationship, + XMLNSc16r2: SourceRelationshipChart201506, + Date1904: &attrValBool{Val: boolPtr(false)}, + Lang: &attrValString{Val: stringPtr("en-US")}, + RoundedCorners: &attrValBool{Val: boolPtr(false)}, + Chart: cChart{ + Title: &cTitle{ + Tx: cTx{ + Rich: &cRich{ + P: aP{ + PPr: &aPPr{ + DefRPr: aRPr{ + Kern: 1200, + Strike: "noStrike", + U: "none", + Sz: 1400, + SolidFill: &aSolidFill{ + SchemeClr: &aSchemeClr{ + Val: "tx1", + LumMod: &attrValInt{ + Val: intPtr(65000), + }, + LumOff: &attrValInt{ + Val: intPtr(35000), + }, + }, + }, + Ea: &aEa{ + Typeface: "+mn-ea", + }, + Cs: &aCs{ + Typeface: "+mn-cs", + }, + Latin: &aLatin{ + Typeface: "+mn-lt", + }, + }, + }, + R: &aR{ + RPr: aRPr{ + Lang: "en-US", + AltLang: "en-US", + }, + T: formatSet.Title.Name, + }, + }, + }, + }, + TxPr: cTxPr{ + P: aP{ + PPr: &aPPr{ + DefRPr: aRPr{ + Kern: 1200, + U: "none", + Sz: 14000, + Strike: "noStrike", + }, + }, + EndParaRPr: &aEndParaRPr{ + Lang: "en-US", + }, + }, + }, + Overlay: &attrValBool{Val: boolPtr(false)}, + }, + View3D: &cView3D{ + RotX: &attrValInt{Val: intPtr(chartView3DRotX[formatSet.Type])}, + RotY: &attrValInt{Val: intPtr(chartView3DRotY[formatSet.Type])}, + Perspective: &attrValInt{Val: intPtr(chartView3DPerspective[formatSet.Type])}, + RAngAx: &attrValInt{Val: intPtr(chartView3DRAngAx[formatSet.Type])}, + }, + Floor: &cThicknessSpPr{ + Thickness: &attrValInt{Val: intPtr(0)}, + }, + SideWall: &cThicknessSpPr{ + Thickness: &attrValInt{Val: intPtr(0)}, + }, + BackWall: &cThicknessSpPr{ + Thickness: &attrValInt{Val: intPtr(0)}, + }, + PlotArea: &cPlotArea{}, + Legend: &cLegend{ + LegendPos: &attrValString{Val: stringPtr(chartLegendPosition[formatSet.Legend.Position])}, + Overlay: &attrValBool{Val: boolPtr(false)}, + }, + + PlotVisOnly: &attrValBool{Val: boolPtr(false)}, + DispBlanksAs: &attrValString{Val: stringPtr(formatSet.ShowBlanksAs)}, + ShowDLblsOverMax: &attrValBool{Val: boolPtr(false)}, + }, + SpPr: &cSpPr{ + SolidFill: &aSolidFill{ + SchemeClr: &aSchemeClr{Val: "bg1"}, + }, + Ln: &aLn{ + W: 9525, + Cap: "flat", + Cmpd: "sng", + Algn: "ctr", + SolidFill: &aSolidFill{ + SchemeClr: &aSchemeClr{Val: "tx1", + LumMod: &attrValInt{ + Val: intPtr(15000), + }, + LumOff: &attrValInt{ + Val: intPtr(85000), + }, + }, + }, + }, + }, + PrintSettings: &cPrintSettings{ + PageMargins: &cPageMargins{ + B: 0.75, + L: 0.7, + R: 0.7, + T: 0.7, + Header: 0.3, + Footer: 0.3, + }, + }, + } + plotAreaFunc := map[string]func(*formatChart) *cPlotArea{ + Area: f.drawBaseChart, + AreaStacked: f.drawBaseChart, + AreaPercentStacked: f.drawBaseChart, + Area3D: f.drawBaseChart, + Area3DStacked: f.drawBaseChart, + Area3DPercentStacked: f.drawBaseChart, + Bar: f.drawBaseChart, + BarStacked: f.drawBaseChart, + BarPercentStacked: f.drawBaseChart, + Bar3DClustered: f.drawBaseChart, + Bar3DStacked: f.drawBaseChart, + Bar3DPercentStacked: f.drawBaseChart, + Bar3DConeClustered: f.drawBaseChart, + Bar3DConeStacked: f.drawBaseChart, + Bar3DConePercentStacked: f.drawBaseChart, + Bar3DPyramidClustered: f.drawBaseChart, + Bar3DPyramidStacked: f.drawBaseChart, + Bar3DPyramidPercentStacked: f.drawBaseChart, + Bar3DCylinderClustered: f.drawBaseChart, + Bar3DCylinderStacked: f.drawBaseChart, + Bar3DCylinderPercentStacked: f.drawBaseChart, + Col: f.drawBaseChart, + ColStacked: f.drawBaseChart, + ColPercentStacked: f.drawBaseChart, + Col3D: f.drawBaseChart, + Col3DClustered: f.drawBaseChart, + Col3DStacked: f.drawBaseChart, + Col3DPercentStacked: f.drawBaseChart, + Col3DCone: f.drawBaseChart, + Col3DConeClustered: f.drawBaseChart, + Col3DConeStacked: f.drawBaseChart, + Col3DConePercentStacked: f.drawBaseChart, + Col3DPyramid: f.drawBaseChart, + Col3DPyramidClustered: f.drawBaseChart, + Col3DPyramidStacked: f.drawBaseChart, + Col3DPyramidPercentStacked: f.drawBaseChart, + Col3DCylinder: f.drawBaseChart, + Col3DCylinderClustered: f.drawBaseChart, + Col3DCylinderStacked: f.drawBaseChart, + Col3DCylinderPercentStacked: f.drawBaseChart, + Doughnut: f.drawDoughnutChart, + Line: f.drawLineChart, + Pie3D: f.drawPie3DChart, + Pie: f.drawPieChart, + PieOfPieChart: f.drawPieOfPieChart, + BarOfPieChart: f.drawBarOfPieChart, + Radar: f.drawRadarChart, + Scatter: f.drawScatterChart, + Surface3D: f.drawSurface3DChart, + WireframeSurface3D: f.drawSurface3DChart, + Contour: f.drawSurfaceChart, + WireframeContour: f.drawSurfaceChart, + Bubble: f.drawBaseChart, + Bubble3D: f.drawBaseChart, + } + addChart := func(c, p *cPlotArea) { + immutable, mutable := reflect.ValueOf(c).Elem(), reflect.ValueOf(p).Elem() + for i := 0; i < mutable.NumField(); i++ { + field := mutable.Field(i) + if field.IsNil() { + continue + } + immutable.FieldByName(mutable.Type().Field(i).Name).Set(field) + } + } + addChart(xlsxChartSpace.Chart.PlotArea, plotAreaFunc[formatSet.Type](formatSet)) + order := len(formatSet.Series) + for idx := range comboCharts { + comboCharts[idx].order = order + addChart(xlsxChartSpace.Chart.PlotArea, plotAreaFunc[comboCharts[idx].Type](comboCharts[idx])) + order += len(comboCharts[idx].Series) + } + chart, _ := xml.Marshal(xlsxChartSpace) + media := "xl/charts/chart" + strconv.Itoa(count+1) + ".xml" + f.saveFileList(media, chart) +} + +// drawBaseChart provides a function to draw the c:plotArea element for bar, +// and column series charts by given format sets. +func (f *File) drawBaseChart(formatSet *formatChart) *cPlotArea { + c := cCharts{ + BarDir: &attrValString{ + Val: stringPtr("col"), + }, + Grouping: &attrValString{ + Val: stringPtr("clustered"), + }, + VaryColors: &attrValBool{ + Val: boolPtr(true), + }, + Ser: f.drawChartSeries(formatSet), + Shape: f.drawChartShape(formatSet), + DLbls: f.drawChartDLbls(formatSet), + AxID: []*attrValInt{ + {Val: intPtr(754001152)}, + {Val: intPtr(753999904)}, + }, + Overlap: &attrValInt{Val: intPtr(100)}, + } + var ok bool + if *c.BarDir.Val, ok = plotAreaChartBarDir[formatSet.Type]; !ok { + c.BarDir = nil + } + if *c.Grouping.Val, ok = plotAreaChartGrouping[formatSet.Type]; !ok { + c.Grouping = nil + } + if *c.Overlap.Val, ok = plotAreaChartOverlap[formatSet.Type]; !ok { + c.Overlap = nil + } + catAx := f.drawPlotAreaCatAx(formatSet) + valAx := f.drawPlotAreaValAx(formatSet) + charts := map[string]*cPlotArea{ + "area": { + AreaChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "areaStacked": { + AreaChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "areaPercentStacked": { + AreaChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "area3D": { + Area3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "area3DStacked": { + Area3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "area3DPercentStacked": { + Area3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "bar": { + BarChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "barStacked": { + BarChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "barPercentStacked": { + BarChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "bar3DClustered": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "bar3DStacked": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "bar3DPercentStacked": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "bar3DConeClustered": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "bar3DConeStacked": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "bar3DConePercentStacked": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "bar3DPyramidClustered": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "bar3DPyramidStacked": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "bar3DPyramidPercentStacked": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "bar3DCylinderClustered": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "bar3DCylinderStacked": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "bar3DCylinderPercentStacked": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "col": { + BarChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "colStacked": { + BarChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "colPercentStacked": { + BarChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "col3D": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "col3DClustered": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "col3DStacked": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "col3DPercentStacked": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "col3DCone": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "col3DConeClustered": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "col3DConeStacked": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "col3DConePercentStacked": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "col3DPyramid": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "col3DPyramidClustered": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "col3DPyramidStacked": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "col3DPyramidPercentStacked": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "col3DCylinder": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "col3DCylinderClustered": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "col3DCylinderStacked": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "col3DCylinderPercentStacked": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "bubble": { + BubbleChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "bubble3D": { + BubbleChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + } + return charts[formatSet.Type] +} + +// drawDoughnutChart provides a function to draw the c:plotArea element for +// doughnut chart by given format sets. +func (f *File) drawDoughnutChart(formatSet *formatChart) *cPlotArea { + return &cPlotArea{ + DoughnutChart: &cCharts{ + VaryColors: &attrValBool{ + Val: boolPtr(true), + }, + Ser: f.drawChartSeries(formatSet), + HoleSize: &attrValInt{Val: intPtr(75)}, + }, + } +} + +// drawLineChart provides a function to draw the c:plotArea element for line +// chart by given format sets. +func (f *File) drawLineChart(formatSet *formatChart) *cPlotArea { + return &cPlotArea{ + LineChart: &cCharts{ + Grouping: &attrValString{ + Val: stringPtr(plotAreaChartGrouping[formatSet.Type]), + }, + VaryColors: &attrValBool{ + Val: boolPtr(false), + }, + Ser: f.drawChartSeries(formatSet), + DLbls: f.drawChartDLbls(formatSet), + Smooth: &attrValBool{ + Val: boolPtr(false), + }, + AxID: []*attrValInt{ + {Val: intPtr(754001152)}, + {Val: intPtr(753999904)}, + }, + }, + CatAx: f.drawPlotAreaCatAx(formatSet), + ValAx: f.drawPlotAreaValAx(formatSet), + } +} + +// drawPieChart provides a function to draw the c:plotArea element for pie +// chart by given format sets. +func (f *File) drawPieChart(formatSet *formatChart) *cPlotArea { + return &cPlotArea{ + PieChart: &cCharts{ + VaryColors: &attrValBool{ + Val: boolPtr(true), + }, + Ser: f.drawChartSeries(formatSet), + }, + } +} + +// drawPie3DChart provides a function to draw the c:plotArea element for 3D +// pie chart by given format sets. +func (f *File) drawPie3DChart(formatSet *formatChart) *cPlotArea { + return &cPlotArea{ + Pie3DChart: &cCharts{ + VaryColors: &attrValBool{ + Val: boolPtr(true), + }, + Ser: f.drawChartSeries(formatSet), + }, + } +} + +// drawPieOfPieChart provides a function to draw the c:plotArea element for +// pie chart by given format sets. +func (f *File) drawPieOfPieChart(formatSet *formatChart) *cPlotArea { + return &cPlotArea{ + PieChart: &cCharts{ + OfPieType: &attrValString{ + Val: stringPtr("pie"), + }, + VaryColors: &attrValBool{ + Val: boolPtr(true), + }, + Ser: f.drawChartSeries(formatSet), + SerLines: &attrValString{}, + }, + } +} + +// drawBarOfPieChart provides a function to draw the c:plotArea element for +// pie chart by given format sets. +func (f *File) drawBarOfPieChart(formatSet *formatChart) *cPlotArea { + return &cPlotArea{ + PieChart: &cCharts{ + OfPieType: &attrValString{ + Val: stringPtr("bar"), + }, + VaryColors: &attrValBool{ + Val: boolPtr(true), + }, + Ser: f.drawChartSeries(formatSet), + SerLines: &attrValString{}, + }, + } +} + +// drawRadarChart provides a function to draw the c:plotArea element for radar +// chart by given format sets. +func (f *File) drawRadarChart(formatSet *formatChart) *cPlotArea { + return &cPlotArea{ + RadarChart: &cCharts{ + RadarStyle: &attrValString{ + Val: stringPtr("marker"), + }, + VaryColors: &attrValBool{ + Val: boolPtr(false), + }, + Ser: f.drawChartSeries(formatSet), + DLbls: f.drawChartDLbls(formatSet), + AxID: []*attrValInt{ + {Val: intPtr(754001152)}, + {Val: intPtr(753999904)}, + }, + }, + CatAx: f.drawPlotAreaCatAx(formatSet), + ValAx: f.drawPlotAreaValAx(formatSet), + } +} + +// drawScatterChart provides a function to draw the c:plotArea element for +// scatter chart by given format sets. +func (f *File) drawScatterChart(formatSet *formatChart) *cPlotArea { + return &cPlotArea{ + ScatterChart: &cCharts{ + ScatterStyle: &attrValString{ + Val: stringPtr("smoothMarker"), // line,lineMarker,marker,none,smooth,smoothMarker + }, + VaryColors: &attrValBool{ + Val: boolPtr(false), + }, + Ser: f.drawChartSeries(formatSet), + DLbls: f.drawChartDLbls(formatSet), + AxID: []*attrValInt{ + {Val: intPtr(754001152)}, + {Val: intPtr(753999904)}, + }, + }, + CatAx: f.drawPlotAreaCatAx(formatSet), + ValAx: f.drawPlotAreaValAx(formatSet), + } +} + +// drawSurface3DChart provides a function to draw the c:surface3DChart element by +// given format sets. +func (f *File) drawSurface3DChart(formatSet *formatChart) *cPlotArea { + plotArea := &cPlotArea{ + Surface3DChart: &cCharts{ + Ser: f.drawChartSeries(formatSet), + AxID: []*attrValInt{ + {Val: intPtr(754001152)}, + {Val: intPtr(753999904)}, + {Val: intPtr(832256642)}, + }, + }, + CatAx: f.drawPlotAreaCatAx(formatSet), + ValAx: f.drawPlotAreaValAx(formatSet), + SerAx: f.drawPlotAreaSerAx(formatSet), + } + if formatSet.Type == WireframeSurface3D { + plotArea.Surface3DChart.Wireframe = &attrValBool{Val: boolPtr(true)} + } + return plotArea +} + +// drawSurfaceChart provides a function to draw the c:surfaceChart element by +// given format sets. +func (f *File) drawSurfaceChart(formatSet *formatChart) *cPlotArea { + plotArea := &cPlotArea{ + SurfaceChart: &cCharts{ + Ser: f.drawChartSeries(formatSet), + AxID: []*attrValInt{ + {Val: intPtr(754001152)}, + {Val: intPtr(753999904)}, + {Val: intPtr(832256642)}, + }, + }, + CatAx: f.drawPlotAreaCatAx(formatSet), + ValAx: f.drawPlotAreaValAx(formatSet), + SerAx: f.drawPlotAreaSerAx(formatSet), + } + if formatSet.Type == WireframeContour { + plotArea.SurfaceChart.Wireframe = &attrValBool{Val: boolPtr(true)} + } + return plotArea +} + +// drawChartShape provides a function to draw the c:shape element by given +// format sets. +func (f *File) drawChartShape(formatSet *formatChart) *attrValString { + shapes := map[string]string{ + Bar3DConeClustered: "cone", + Bar3DConeStacked: "cone", + Bar3DConePercentStacked: "cone", + Bar3DPyramidClustered: "pyramid", + Bar3DPyramidStacked: "pyramid", + Bar3DPyramidPercentStacked: "pyramid", + Bar3DCylinderClustered: "cylinder", + Bar3DCylinderStacked: "cylinder", + Bar3DCylinderPercentStacked: "cylinder", + Col3DCone: "cone", + Col3DConeClustered: "cone", + Col3DConeStacked: "cone", + Col3DConePercentStacked: "cone", + Col3DPyramid: "pyramid", + Col3DPyramidClustered: "pyramid", + Col3DPyramidStacked: "pyramid", + Col3DPyramidPercentStacked: "pyramid", + Col3DCylinder: "cylinder", + Col3DCylinderClustered: "cylinder", + Col3DCylinderStacked: "cylinder", + Col3DCylinderPercentStacked: "cylinder", + } + if shape, ok := shapes[formatSet.Type]; ok { + return &attrValString{Val: stringPtr(shape)} + } + return nil +} + +// drawChartSeries provides a function to draw the c:ser element by given +// format sets. +func (f *File) drawChartSeries(formatSet *formatChart) *[]cSer { + ser := []cSer{} + for k := range formatSet.Series { + ser = append(ser, cSer{ + IDx: &attrValInt{Val: intPtr(k + formatSet.order)}, + Order: &attrValInt{Val: intPtr(k + formatSet.order)}, + Tx: &cTx{ + StrRef: &cStrRef{ + F: formatSet.Series[k].Name, + }, + }, + SpPr: f.drawChartSeriesSpPr(k, formatSet), + Marker: f.drawChartSeriesMarker(k, formatSet), + DPt: f.drawChartSeriesDPt(k, formatSet), + DLbls: f.drawChartSeriesDLbls(formatSet), + Cat: f.drawChartSeriesCat(formatSet.Series[k], formatSet), + Val: f.drawChartSeriesVal(formatSet.Series[k], formatSet), + XVal: f.drawChartSeriesXVal(formatSet.Series[k], formatSet), + YVal: f.drawChartSeriesYVal(formatSet.Series[k], formatSet), + BubbleSize: f.drawCharSeriesBubbleSize(formatSet.Series[k], formatSet), + Bubble3D: f.drawCharSeriesBubble3D(formatSet), + }) + } + return &ser +} + +// drawChartSeriesSpPr provides a function to draw the c:spPr element by given +// format sets. +func (f *File) drawChartSeriesSpPr(i int, formatSet *formatChart) *cSpPr { + spPrScatter := &cSpPr{ + Ln: &aLn{ + W: 25400, + NoFill: " ", + }, + } + spPrLine := &cSpPr{ + Ln: &aLn{ + W: f.ptToEMUs(formatSet.Series[i].Line.Width), + Cap: "rnd", // rnd, sq, flat + }, + } + if i+formatSet.order < 6 { + spPrLine.Ln.SolidFill = &aSolidFill{ + SchemeClr: &aSchemeClr{Val: "accent" + strconv.Itoa(i+formatSet.order+1)}, + } + } + chartSeriesSpPr := map[string]*cSpPr{Line: spPrLine, Scatter: spPrScatter} + return chartSeriesSpPr[formatSet.Type] +} + +// drawChartSeriesDPt provides a function to draw the c:dPt element by given +// data index and format sets. +func (f *File) drawChartSeriesDPt(i int, formatSet *formatChart) []*cDPt { + dpt := []*cDPt{{ + IDx: &attrValInt{Val: intPtr(i)}, + Bubble3D: &attrValBool{Val: boolPtr(false)}, + SpPr: &cSpPr{ + SolidFill: &aSolidFill{ + SchemeClr: &aSchemeClr{Val: "accent" + strconv.Itoa(i+1)}, + }, + Ln: &aLn{ + W: 25400, + Cap: "rnd", + SolidFill: &aSolidFill{ + SchemeClr: &aSchemeClr{Val: "lt" + strconv.Itoa(i+1)}, + }, + }, + Sp3D: &aSp3D{ + ContourW: 25400, + ContourClr: &aContourClr{ + SchemeClr: &aSchemeClr{Val: "lt" + strconv.Itoa(i+1)}, + }, + }, + }, + }} + chartSeriesDPt := map[string][]*cDPt{Pie: dpt, Pie3D: dpt} + return chartSeriesDPt[formatSet.Type] +} + +// drawChartSeriesCat provides a function to draw the c:cat element by given +// chart series and format sets. +func (f *File) drawChartSeriesCat(v formatChartSeries, formatSet *formatChart) *cCat { + cat := &cCat{ + StrRef: &cStrRef{ + F: v.Categories, + }, + } + chartSeriesCat := map[string]*cCat{Scatter: nil, Bubble: nil, Bubble3D: nil} + if _, ok := chartSeriesCat[formatSet.Type]; ok || v.Categories == "" { + return nil + } + return cat +} + +// drawChartSeriesVal provides a function to draw the c:val element by given +// chart series and format sets. +func (f *File) drawChartSeriesVal(v formatChartSeries, formatSet *formatChart) *cVal { + val := &cVal{ + NumRef: &cNumRef{ + F: v.Values, + }, + } + chartSeriesVal := map[string]*cVal{Scatter: nil, Bubble: nil, Bubble3D: nil} + if _, ok := chartSeriesVal[formatSet.Type]; ok { + return nil + } + return val +} + +// drawChartSeriesMarker provides a function to draw the c:marker element by +// given data index and format sets. +func (f *File) drawChartSeriesMarker(i int, formatSet *formatChart) *cMarker { + marker := &cMarker{ + Symbol: &attrValString{Val: stringPtr("circle")}, + Size: &attrValInt{Val: intPtr(5)}, + } + if i < 6 { + marker.SpPr = &cSpPr{ + SolidFill: &aSolidFill{ + SchemeClr: &aSchemeClr{ + Val: "accent" + strconv.Itoa(i+1), + }, + }, + Ln: &aLn{ + W: 9252, + SolidFill: &aSolidFill{ + SchemeClr: &aSchemeClr{ + Val: "accent" + strconv.Itoa(i+1), + }, + }, + }, + } + } + chartSeriesMarker := map[string]*cMarker{Scatter: marker} + return chartSeriesMarker[formatSet.Type] +} + +// drawChartSeriesXVal provides a function to draw the c:xVal element by given +// chart series and format sets. +func (f *File) drawChartSeriesXVal(v formatChartSeries, formatSet *formatChart) *cCat { + cat := &cCat{ + StrRef: &cStrRef{ + F: v.Categories, + }, + } + chartSeriesXVal := map[string]*cCat{Scatter: cat} + return chartSeriesXVal[formatSet.Type] +} + +// drawChartSeriesYVal provides a function to draw the c:yVal element by given +// chart series and format sets. +func (f *File) drawChartSeriesYVal(v formatChartSeries, formatSet *formatChart) *cVal { + val := &cVal{ + NumRef: &cNumRef{ + F: v.Values, + }, + } + chartSeriesYVal := map[string]*cVal{Scatter: val, Bubble: val, Bubble3D: val} + return chartSeriesYVal[formatSet.Type] +} + +// drawCharSeriesBubbleSize provides a function to draw the c:bubbleSize +// element by given chart series and format sets. +func (f *File) drawCharSeriesBubbleSize(v formatChartSeries, formatSet *formatChart) *cVal { + if _, ok := map[string]bool{Bubble: true, Bubble3D: true}[formatSet.Type]; !ok { + return nil + } + return &cVal{ + NumRef: &cNumRef{ + F: v.Values, + }, + } +} + +// drawCharSeriesBubble3D provides a function to draw the c:bubble3D element +// by given format sets. +func (f *File) drawCharSeriesBubble3D(formatSet *formatChart) *attrValBool { + if _, ok := map[string]bool{Bubble3D: true}[formatSet.Type]; !ok { + return nil + } + return &attrValBool{Val: boolPtr(true)} +} + +// drawChartDLbls provides a function to draw the c:dLbls element by given +// format sets. +func (f *File) drawChartDLbls(formatSet *formatChart) *cDLbls { + return &cDLbls{ + ShowLegendKey: &attrValBool{Val: boolPtr(formatSet.Legend.ShowLegendKey)}, + ShowVal: &attrValBool{Val: boolPtr(formatSet.Plotarea.ShowVal)}, + ShowCatName: &attrValBool{Val: boolPtr(formatSet.Plotarea.ShowCatName)}, + ShowSerName: &attrValBool{Val: boolPtr(formatSet.Plotarea.ShowSerName)}, + ShowBubbleSize: &attrValBool{Val: boolPtr(formatSet.Plotarea.ShowBubbleSize)}, + ShowPercent: &attrValBool{Val: boolPtr(formatSet.Plotarea.ShowPercent)}, + ShowLeaderLines: &attrValBool{Val: boolPtr(formatSet.Plotarea.ShowLeaderLines)}, + } +} + +// drawChartSeriesDLbls provides a function to draw the c:dLbls element by +// given format sets. +func (f *File) drawChartSeriesDLbls(formatSet *formatChart) *cDLbls { + dLbls := f.drawChartDLbls(formatSet) + chartSeriesDLbls := map[string]*cDLbls{Scatter: nil, Surface3D: nil, WireframeSurface3D: nil, Contour: nil, WireframeContour: nil, Bubble: nil, Bubble3D: nil} + if _, ok := chartSeriesDLbls[formatSet.Type]; ok { + return nil + } + return dLbls +} + +// drawPlotAreaCatAx provides a function to draw the c:catAx element. +func (f *File) drawPlotAreaCatAx(formatSet *formatChart) []*cAxs { + min := &attrValFloat{Val: float64Ptr(formatSet.XAxis.Minimum)} + max := &attrValFloat{Val: float64Ptr(formatSet.XAxis.Maximum)} + if formatSet.XAxis.Minimum == 0 { + min = nil + } + if formatSet.XAxis.Maximum == 0 { + max = nil + } + axs := []*cAxs{ + { + AxID: &attrValInt{Val: intPtr(754001152)}, + Scaling: &cScaling{ + Orientation: &attrValString{Val: stringPtr(orientation[formatSet.XAxis.ReverseOrder])}, + Max: max, + Min: min, + }, + Delete: &attrValBool{Val: boolPtr(false)}, + AxPos: &attrValString{Val: stringPtr(catAxPos[formatSet.XAxis.ReverseOrder])}, + NumFmt: &cNumFmt{ + FormatCode: "General", + SourceLinked: true, + }, + MajorTickMark: &attrValString{Val: stringPtr("none")}, + MinorTickMark: &attrValString{Val: stringPtr("none")}, + TickLblPos: &attrValString{Val: stringPtr("nextTo")}, + SpPr: f.drawPlotAreaSpPr(), + TxPr: f.drawPlotAreaTxPr(), + CrossAx: &attrValInt{Val: intPtr(753999904)}, + Crosses: &attrValString{Val: stringPtr("autoZero")}, + Auto: &attrValBool{Val: boolPtr(true)}, + LblAlgn: &attrValString{Val: stringPtr("ctr")}, + LblOffset: &attrValInt{Val: intPtr(100)}, + NoMultiLvlLbl: &attrValBool{Val: boolPtr(false)}, + }, + } + if formatSet.XAxis.MajorGridlines { + axs[0].MajorGridlines = &cChartLines{SpPr: f.drawPlotAreaSpPr()} + } + if formatSet.XAxis.MinorGridlines { + axs[0].MinorGridlines = &cChartLines{SpPr: f.drawPlotAreaSpPr()} + } + if formatSet.XAxis.TickLabelSkip != 0 { + axs[0].TickLblSkip = &attrValInt{Val: intPtr(formatSet.XAxis.TickLabelSkip)} + } + return axs +} + +// drawPlotAreaValAx provides a function to draw the c:valAx element. +func (f *File) drawPlotAreaValAx(formatSet *formatChart) []*cAxs { + min := &attrValFloat{Val: float64Ptr(formatSet.YAxis.Minimum)} + max := &attrValFloat{Val: float64Ptr(formatSet.YAxis.Maximum)} + if formatSet.YAxis.Minimum == 0 { + min = nil + } + if formatSet.YAxis.Maximum == 0 { + max = nil + } + axs := []*cAxs{ + { + AxID: &attrValInt{Val: intPtr(753999904)}, + Scaling: &cScaling{ + Orientation: &attrValString{Val: stringPtr(orientation[formatSet.YAxis.ReverseOrder])}, + Max: max, + Min: min, + }, + Delete: &attrValBool{Val: boolPtr(false)}, + AxPos: &attrValString{Val: stringPtr(valAxPos[formatSet.YAxis.ReverseOrder])}, + NumFmt: &cNumFmt{ + FormatCode: chartValAxNumFmtFormatCode[formatSet.Type], + SourceLinked: true, + }, + MajorTickMark: &attrValString{Val: stringPtr("none")}, + MinorTickMark: &attrValString{Val: stringPtr("none")}, + TickLblPos: &attrValString{Val: stringPtr("nextTo")}, + SpPr: f.drawPlotAreaSpPr(), + TxPr: f.drawPlotAreaTxPr(), + CrossAx: &attrValInt{Val: intPtr(754001152)}, + Crosses: &attrValString{Val: stringPtr("autoZero")}, + CrossBetween: &attrValString{Val: stringPtr(chartValAxCrossBetween[formatSet.Type])}, + }, + } + if formatSet.YAxis.MajorGridlines { + axs[0].MajorGridlines = &cChartLines{SpPr: f.drawPlotAreaSpPr()} + } + if formatSet.YAxis.MinorGridlines { + axs[0].MinorGridlines = &cChartLines{SpPr: f.drawPlotAreaSpPr()} + } + if pos, ok := valTickLblPos[formatSet.Type]; ok { + axs[0].TickLblPos.Val = stringPtr(pos) + } + if formatSet.YAxis.MajorUnit != 0 { + axs[0].MajorUnit = &attrValFloat{Val: float64Ptr(formatSet.YAxis.MajorUnit)} + } + return axs +} + +// drawPlotAreaSerAx provides a function to draw the c:serAx element. +func (f *File) drawPlotAreaSerAx(formatSet *formatChart) []*cAxs { + min := &attrValFloat{Val: float64Ptr(formatSet.YAxis.Minimum)} + max := &attrValFloat{Val: float64Ptr(formatSet.YAxis.Maximum)} + if formatSet.YAxis.Minimum == 0 { + min = nil + } + if formatSet.YAxis.Maximum == 0 { + max = nil + } + return []*cAxs{ + { + AxID: &attrValInt{Val: intPtr(832256642)}, + Scaling: &cScaling{ + Orientation: &attrValString{Val: stringPtr(orientation[formatSet.YAxis.ReverseOrder])}, + Max: max, + Min: min, + }, + Delete: &attrValBool{Val: boolPtr(false)}, + AxPos: &attrValString{Val: stringPtr(catAxPos[formatSet.XAxis.ReverseOrder])}, + TickLblPos: &attrValString{Val: stringPtr("nextTo")}, + SpPr: f.drawPlotAreaSpPr(), + TxPr: f.drawPlotAreaTxPr(), + CrossAx: &attrValInt{Val: intPtr(753999904)}, + }, + } +} + +// drawPlotAreaSpPr provides a function to draw the c:spPr element. +func (f *File) drawPlotAreaSpPr() *cSpPr { + return &cSpPr{ + Ln: &aLn{ + W: 9525, + Cap: "flat", + Cmpd: "sng", + Algn: "ctr", + SolidFill: &aSolidFill{ + SchemeClr: &aSchemeClr{ + Val: "tx1", + LumMod: &attrValInt{Val: intPtr(15000)}, + LumOff: &attrValInt{Val: intPtr(85000)}, + }, + }, + }, + } +} + +// drawPlotAreaTxPr provides a function to draw the c:txPr element. +func (f *File) drawPlotAreaTxPr() *cTxPr { + return &cTxPr{ + BodyPr: aBodyPr{ + Rot: -60000000, + SpcFirstLastPara: true, + VertOverflow: "ellipsis", + Vert: "horz", + Wrap: "square", + Anchor: "ctr", + AnchorCtr: true, + }, + P: aP{ + PPr: &aPPr{ + DefRPr: aRPr{ + Sz: 900, + B: false, + I: false, + U: "none", + Strike: "noStrike", + Kern: 1200, + Baseline: 0, + SolidFill: &aSolidFill{ + SchemeClr: &aSchemeClr{ + Val: "tx1", + LumMod: &attrValInt{Val: intPtr(15000)}, + LumOff: &attrValInt{Val: intPtr(85000)}, + }, + }, + Latin: &aLatin{Typeface: "+mn-lt"}, + Ea: &aEa{Typeface: "+mn-ea"}, + Cs: &aCs{Typeface: "+mn-cs"}, + }, + }, + EndParaRPr: &aEndParaRPr{Lang: "en-US"}, + }, + } +} + +// drawingParser provides a function to parse drawingXML. In order to solve +// the problem that the label structure is changed after serialization and +// deserialization, two different structures: decodeWsDr and encodeWsDr are +// defined. +func (f *File) drawingParser(path string) (*xlsxWsDr, int) { + var ( + err error + ok bool + ) + + if f.Drawings[path] == nil { + content := xlsxWsDr{} + content.A = NameSpaceDrawingML + content.Xdr = NameSpaceDrawingMLSpreadSheet + if _, ok = f.XLSX[path]; ok { // Append Model + decodeWsDr := decodeWsDr{} + if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(path)))). + Decode(&decodeWsDr); err != nil && err != io.EOF { + log.Printf("xml decode error: %s", err) + } + content.R = decodeWsDr.R + for _, v := range decodeWsDr.OneCellAnchor { + content.OneCellAnchor = append(content.OneCellAnchor, &xdrCellAnchor{ + EditAs: v.EditAs, + GraphicFrame: v.Content, + }) + } + for _, v := range decodeWsDr.TwoCellAnchor { + content.TwoCellAnchor = append(content.TwoCellAnchor, &xdrCellAnchor{ + EditAs: v.EditAs, + GraphicFrame: v.Content, + }) + } + } + f.Drawings[path] = &content + } + wsDr := f.Drawings[path] + return wsDr, len(wsDr.OneCellAnchor) + len(wsDr.TwoCellAnchor) + 2 +} + +// 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) error { + col, row, err := CellNameToCoordinates(cell) + if err != nil { + return err + } + 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, colIdx, rowIdx, formatSet.OffsetX, formatSet.OffsetY, width, height) + content, cNvPrID := f.drawingParser(drawingXML) + twoCellAnchor := xdrCellAnchor{} + twoCellAnchor.EditAs = formatSet.Positioning + from := xlsxFrom{} + from.Col = colStart + from.ColOff = formatSet.OffsetX * EMU + from.Row = rowStart + from.RowOff = formatSet.OffsetY * EMU + to := xlsxTo{} + to.Col = colEnd + to.ColOff = x2 * EMU + to.Row = rowEnd + to.RowOff = y2 * EMU + twoCellAnchor.From = &from + twoCellAnchor.To = &to + + graphicFrame := xlsxGraphicFrame{ + NvGraphicFramePr: xlsxNvGraphicFramePr{ + CNvPr: &xlsxCNvPr{ + ID: cNvPrID, + Name: "Chart " + strconv.Itoa(cNvPrID), + }, + }, + Graphic: &xlsxGraphic{ + GraphicData: &xlsxGraphicData{ + URI: NameSpaceDrawingMLChart, + Chart: &xlsxChart{ + C: NameSpaceDrawingMLChart, + R: SourceRelationship, + RID: "rId" + strconv.Itoa(rID), + }, + }, + }, + } + graphic, _ := xml.Marshal(graphicFrame) + twoCellAnchor.GraphicFrame = string(graphic) + twoCellAnchor.ClientData = &xdrClientData{ + FLocksWithSheet: formatSet.FLocksWithSheet, + FPrintsWithSheet: formatSet.FPrintsWithSheet, + } + content.TwoCellAnchor = append(content.TwoCellAnchor, &twoCellAnchor) + f.Drawings[drawingXML] = content + return err +} -- cgit v1.2.1 From e2bd08c9111b0141c66adf232edb2fd729afa63f Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 21 Jan 2020 23:29:56 +0800 Subject: Make DeleteChart delete multiple charts located on the same cell --- chart.go | 31 ++++++++++++------------------- drawing.go | 4 ++-- xmlChart.go | 2 +- 3 files changed, 15 insertions(+), 22 deletions(-) diff --git a/chart.go b/chart.go index 5f06c55..2629f0b 100644 --- a/chart.go +++ b/chart.go @@ -694,9 +694,9 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // // Set chart size by dimension property. The dimension property is optional. The default width is 480, and height is 290. // -// combo: Specifies the create a chart that combines two art types in a single -// chart. For example, create a clustered column - line chart with data -// Sheet1!$E$1:$L$15: +// combo: Specifies the create a chart that combines two or more chart types +// in a single chart. For example, create a clustered column - line chart with +// data Sheet1!$E$1:$L$15: // // package main // @@ -782,10 +782,11 @@ func (f *File) DeleteChart(sheet, cell string) (err error) { } drawingXML := strings.Replace(f.getSheetRelationshipsTargetByID(sheet, ws.Drawing.RID), "..", "xl", -1) wsDr, _ = f.drawingParser(drawingXML) - for idx, anchor := range wsDr.TwoCellAnchor { - if err = nil; anchor.From != nil && anchor.Pic == nil { - if anchor.From.Col == col && anchor.From.Row == row { + for idx := 0; idx < len(wsDr.TwoCellAnchor); idx++ { + if err = nil; wsDr.TwoCellAnchor[idx].From != nil && wsDr.TwoCellAnchor[idx].Pic == nil { + if wsDr.TwoCellAnchor[idx].From.Col == col && wsDr.TwoCellAnchor[idx].From.Row == row { wsDr.TwoCellAnchor = append(wsDr.TwoCellAnchor[:idx], wsDr.TwoCellAnchor[idx+1:]...) + idx-- } } } @@ -795,26 +796,18 @@ func (f *File) DeleteChart(sheet, cell string) (err error) { // deleteChart provides a function to delete chart graphic frame by given by // given coordinates. func (f *File) deleteChart(col, row int, drawingXML string, wsDr *xlsxWsDr) (err error) { - var ( - deWsDr *decodeWsDr - deTwoCellAnchor *decodeTwoCellAnchor - ) - deWsDr = new(decodeWsDr) - if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(drawingXML)))). - Decode(deWsDr); err != nil && err != io.EOF { - err = fmt.Errorf("xml decode error: %s", err) - return - } - for idx, anchor := range deWsDr.TwoCellAnchor { + var deTwoCellAnchor *decodeTwoCellAnchor + for idx := 0; idx < len(wsDr.TwoCellAnchor); idx++ { deTwoCellAnchor = new(decodeTwoCellAnchor) - if err = f.xmlNewDecoder(bytes.NewReader([]byte("" + anchor.Content + ""))). + if err = f.xmlNewDecoder(bytes.NewReader([]byte("" + wsDr.TwoCellAnchor[idx].GraphicFrame + ""))). Decode(deTwoCellAnchor); err != nil && err != io.EOF { err = fmt.Errorf("xml decode error: %s", err) return } if err = nil; deTwoCellAnchor.From != nil && deTwoCellAnchor.Pic == nil { - if anchor.From.Col == col && anchor.From.Row == row { + if deTwoCellAnchor.From.Col == col && deTwoCellAnchor.From.Row == row { wsDr.TwoCellAnchor = append(wsDr.TwoCellAnchor[:idx], wsDr.TwoCellAnchor[idx+1:]...) + idx-- } } } diff --git a/drawing.go b/drawing.go index 49506a3..316897b 100644 --- a/drawing.go +++ b/drawing.go @@ -563,7 +563,7 @@ func (f *File) drawPie3DChart(formatSet *formatChart) *cPlotArea { // pie chart by given format sets. func (f *File) drawPieOfPieChart(formatSet *formatChart) *cPlotArea { return &cPlotArea{ - PieChart: &cCharts{ + OfPieChart: &cCharts{ OfPieType: &attrValString{ Val: stringPtr("pie"), }, @@ -580,7 +580,7 @@ func (f *File) drawPieOfPieChart(formatSet *formatChart) *cPlotArea { // pie chart by given format sets. func (f *File) drawBarOfPieChart(formatSet *formatChart) *cPlotArea { return &cPlotArea{ - PieChart: &cCharts{ + OfPieChart: &cCharts{ OfPieType: &attrValString{ Val: stringPtr("bar"), }, diff --git a/xmlChart.go b/xmlChart.go index 5511469..8d24552 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -331,10 +331,10 @@ type cCharts struct { RadarStyle *attrValString `xml:"radarStyle"` ScatterStyle *attrValString `xml:"scatterStyle"` OfPieType *attrValString `xml:"ofPieType"` - SerLines *attrValString `xml:"serLines"` VaryColors *attrValBool `xml:"varyColors"` Wireframe *attrValBool `xml:"wireframe"` Ser *[]cSer `xml:"ser"` + SerLines *attrValString `xml:"serLines"` DLbls *cDLbls `xml:"dLbls"` Shape *attrValString `xml:"shape"` HoleSize *attrValInt `xml:"holeSize"` -- cgit v1.2.1 From cbc3fd21b79fbb819c1c341fc825701c04a0b473 Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 22 Jan 2020 01:08:18 +0800 Subject: Resolve #455, init delete picture from spreadsheet support --- chart.go | 37 +------------------------------------ chart_test.go | 6 ------ drawing.go | 43 +++++++++++++++++++++++++++++++++++++++++++ picture.go | 21 +++++++++++++++++++++ picture_test.go | 15 +++++++++++++++ 5 files changed, 80 insertions(+), 42 deletions(-) diff --git a/chart.go b/chart.go index 2629f0b..227cdee 100644 --- a/chart.go +++ b/chart.go @@ -10,11 +10,8 @@ package excelize import ( - "bytes" "encoding/json" "errors" - "fmt" - "io" "strconv" "strings" ) @@ -766,7 +763,6 @@ func (f *File) AddChart(sheet, cell, format string, combo ...string) error { // DeleteChart provides a function to delete chart in XLSX by given worksheet // and cell name. func (f *File) DeleteChart(sheet, cell string) (err error) { - var wsDr *xlsxWsDr col, row, err := CellNameToCoordinates(cell) if err != nil { return @@ -781,38 +777,7 @@ func (f *File) DeleteChart(sheet, cell string) (err error) { return } drawingXML := strings.Replace(f.getSheetRelationshipsTargetByID(sheet, ws.Drawing.RID), "..", "xl", -1) - wsDr, _ = f.drawingParser(drawingXML) - for idx := 0; idx < len(wsDr.TwoCellAnchor); idx++ { - if err = nil; wsDr.TwoCellAnchor[idx].From != nil && wsDr.TwoCellAnchor[idx].Pic == nil { - if wsDr.TwoCellAnchor[idx].From.Col == col && wsDr.TwoCellAnchor[idx].From.Row == row { - wsDr.TwoCellAnchor = append(wsDr.TwoCellAnchor[:idx], wsDr.TwoCellAnchor[idx+1:]...) - idx-- - } - } - } - return f.deleteChart(col, row, drawingXML, wsDr) -} - -// deleteChart provides a function to delete chart graphic frame by given by -// given coordinates. -func (f *File) deleteChart(col, row int, drawingXML string, wsDr *xlsxWsDr) (err error) { - var deTwoCellAnchor *decodeTwoCellAnchor - for idx := 0; idx < len(wsDr.TwoCellAnchor); idx++ { - deTwoCellAnchor = new(decodeTwoCellAnchor) - if err = f.xmlNewDecoder(bytes.NewReader([]byte("" + wsDr.TwoCellAnchor[idx].GraphicFrame + ""))). - Decode(deTwoCellAnchor); err != nil && err != io.EOF { - err = fmt.Errorf("xml decode error: %s", err) - return - } - if err = nil; deTwoCellAnchor.From != nil && deTwoCellAnchor.Pic == nil { - if deTwoCellAnchor.From.Col == col && deTwoCellAnchor.From.Row == row { - wsDr.TwoCellAnchor = append(wsDr.TwoCellAnchor[:idx], wsDr.TwoCellAnchor[idx+1:]...) - idx-- - } - } - } - f.Drawings[drawingXML] = wsDr - return err + return f.deleteDrawing(col, row, drawingXML, "Chart") } // countCharts provides a function to get chart files count storage in the diff --git a/chart_test.go b/chart_test.go index d8d36d8..98f3555 100644 --- a/chart_test.go +++ b/chart_test.go @@ -217,12 +217,6 @@ func TestDeleteChart(t *testing.T) { assert.EqualError(t, f.DeleteChart("SheetN", "A1"), "sheet SheetN is not exist") // Test delete chart with invalid coordinates. assert.EqualError(t, f.DeleteChart("Sheet1", ""), `cannot convert cell "" to coordinates: invalid cell name ""`) - // Test delete chart with unsupport charset. - f, err = OpenFile(filepath.Join("test", "Book1.xlsx")) - assert.NoError(t, err) - delete(f.Sheet, "xl/drawings/drawing1.xml") - f.XLSX["xl/drawings/drawing1.xml"] = MacintoshCyrillicCharset - assert.EqualError(t, f.DeleteChart("Sheet1", "A1"), "xml decode error: XML syntax error on line 1: invalid UTF-8") // Test delete chart on no chart worksheet. assert.NoError(t, NewFile().DeleteChart("Sheet1", "A1")) } diff --git a/drawing.go b/drawing.go index 316897b..e51b6af 100644 --- a/drawing.go +++ b/drawing.go @@ -12,6 +12,7 @@ package excelize import ( "bytes" "encoding/xml" + "fmt" "io" "log" "reflect" @@ -1207,3 +1208,45 @@ func (f *File) addDrawingChart(sheet, drawingXML, cell string, width, height, rI f.Drawings[drawingXML] = content return err } + +// deleteDrawing provides a function to delete chart graphic frame by given by +// given coordinates and graphic type. +func (f *File) deleteDrawing(col, row int, drawingXML, drawingType string) (err error) { + var ( + wsDr *xlsxWsDr + deTwoCellAnchor *decodeTwoCellAnchor + ) + xdrCellAnchorFuncs := map[string]func(anchor *xdrCellAnchor) bool{ + "Chart": func(anchor *xdrCellAnchor) bool { return anchor.Pic == nil }, + "Pic": func(anchor *xdrCellAnchor) bool { return anchor.Pic != nil }, + } + decodeTwoCellAnchorFuncs := map[string]func(anchor *decodeTwoCellAnchor) bool{ + "Chart": func(anchor *decodeTwoCellAnchor) bool { return anchor.Pic == nil }, + "Pic": func(anchor *decodeTwoCellAnchor) bool { return anchor.Pic != nil }, + } + wsDr, _ = f.drawingParser(drawingXML) + for idx := 0; idx < len(wsDr.TwoCellAnchor); idx++ { + if err = nil; wsDr.TwoCellAnchor[idx].From != nil && xdrCellAnchorFuncs[drawingType](wsDr.TwoCellAnchor[idx]) { + if wsDr.TwoCellAnchor[idx].From.Col == col && wsDr.TwoCellAnchor[idx].From.Row == row { + wsDr.TwoCellAnchor = append(wsDr.TwoCellAnchor[:idx], wsDr.TwoCellAnchor[idx+1:]...) + idx-- + } + } + } + for idx := 0; idx < len(wsDr.TwoCellAnchor); idx++ { + deTwoCellAnchor = new(decodeTwoCellAnchor) + if err = f.xmlNewDecoder(bytes.NewReader([]byte("" + wsDr.TwoCellAnchor[idx].GraphicFrame + ""))). + Decode(deTwoCellAnchor); err != nil && err != io.EOF { + err = fmt.Errorf("xml decode error: %s", err) + return + } + if err = nil; deTwoCellAnchor.From != nil && decodeTwoCellAnchorFuncs[drawingType](deTwoCellAnchor) { + if deTwoCellAnchor.From.Col == col && deTwoCellAnchor.From.Row == row { + wsDr.TwoCellAnchor = append(wsDr.TwoCellAnchor[:idx], wsDr.TwoCellAnchor[idx+1:]...) + idx-- + } + } + } + f.Drawings[drawingXML] = wsDr + return err +} diff --git a/picture.go b/picture.go index 639cb66..213bae9 100644 --- a/picture.go +++ b/picture.go @@ -462,6 +462,27 @@ func (f *File) GetPicture(sheet, cell string) (string, []byte, error) { return f.getPicture(row, col, drawingXML, drawingRelationships) } +// DeletePicture provides a function to delete chart in XLSX by given +// worksheet and cell name. Note that the image file won't deleted from the +// document currently. +func (f *File) DeletePicture(sheet, cell string) (err error) { + col, row, err := CellNameToCoordinates(cell) + if err != nil { + return + } + col-- + row-- + ws, err := f.workSheetReader(sheet) + if err != nil { + return + } + if ws.Drawing == nil { + return + } + drawingXML := strings.Replace(f.getSheetRelationshipsTargetByID(sheet, ws.Drawing.RID), "..", "xl", -1) + return f.deleteDrawing(col, row, drawingXML, "Pic") +} + // getPicture provides a function to get picture base name and raw content // embed in XLSX by given coordinates and drawing relationships. func (f *File) getPicture(row, col int, drawingXML, drawingRelationships string) (ret string, buf []byte, err error) { diff --git a/picture_test.go b/picture_test.go index ca38f41..fdc6f0d 100644 --- a/picture_test.go +++ b/picture_test.go @@ -166,3 +166,18 @@ func TestAddPictureFromBytes(t *testing.T) { assert.Equal(t, 1, imageCount, "Duplicate image should only be stored once.") assert.EqualError(t, f.AddPictureFromBytes("SheetN", fmt.Sprint("A", 1), "", "logo", ".png", imgFile), "sheet SheetN is not exist") } + +func TestDeletePicture(t *testing.T) { + f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) + assert.NoError(t, err) + assert.NoError(t, f.DeletePicture("Sheet1", "A1")) + assert.NoError(t, f.AddPicture("Sheet1", "P1", filepath.Join("test", "images", "excel.jpg"), "")) + assert.NoError(t, f.DeletePicture("Sheet1", "P1")) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestDeletePicture.xlsx"))) + // Test delete picture on not exists worksheet. + assert.EqualError(t, f.DeletePicture("SheetN", "A1"), "sheet SheetN is not exist") + // Test delete picture with invalid coordinates. + assert.EqualError(t, f.DeletePicture("Sheet1", ""), `cannot convert cell "" to coordinates: invalid cell name ""`) + // Test delete picture on no chart worksheet. + assert.NoError(t, NewFile().DeletePicture("Sheet1", "A1")) +} -- cgit v1.2.1 From 68754a2075f12ba3c2bdf3646e4a3e7a3fd829f5 Mon Sep 17 00:00:00 2001 From: Jacques Boscq Date: Tue, 21 Jan 2020 23:42:44 +0100 Subject: SetColVisible() can parse a column range + typos. --- col.go | 59 ++++++++++++++++++++++++++++++++++++----------------------- col_test.go | 28 ++++++++++++++++++++++++++-- 2 files changed, 62 insertions(+), 25 deletions(-) diff --git a/col.go b/col.go index f7e6bcd..ff771f1 100644 --- a/col.go +++ b/col.go @@ -26,7 +26,7 @@ const ( // worksheet name and column name. For example, get visible state of column D // in Sheet1: // -// visiable, err := f.GetColVisible("Sheet1", "D") +// visible, err := f.GetColVisible("Sheet1", "D") // func (f *File) GetColVisible(sheet, col string) (bool, error) { visible := true @@ -52,45 +52,58 @@ func (f *File) GetColVisible(sheet, col string) (bool, error) { return visible, err } -// SetColVisible provides a function to set visible of a single column by given -// worksheet name and column name. For example, hide column D in Sheet1: +// SetColVisible provides a function to set visible columns by given worksheet +// name, columns range and visibility. +// +// For example hide column D on Sheet1: // // err := f.SetColVisible("Sheet1", "D", false) // -func (f *File) SetColVisible(sheet, col string, visible bool) error { - colNum, err := ColumnNameToNumber(col) +// Hide the columns from D to F (included) +// +// err := f.SetColVisible("Sheet1", "D:F", false) +// +func (f *File) SetColVisible(sheet, columns string, visible bool) error { + var max int + + colsTab := strings.Split(columns, ":") + min, err := ColumnNameToNumber(colsTab[0]) if err != nil { return err } - colData := xlsxCol{ - Min: colNum, - Max: colNum, - Hidden: !visible, - CustomWidth: true, + if len(colsTab) == 2 { + max, err = ColumnNameToNumber(colsTab[1]) + if err != nil { + return err + } + } else { + max = min + } + if max < min { + min, max = max, min } xlsx, err := f.workSheetReader(sheet) if err != nil { return err } - if xlsx.Cols == nil { + colData := xlsxCol{ + Min: min, + Max: max, + Width: 9, // default width + Hidden: !visible, + CustomWidth: true, + } + if xlsx.Cols != nil { + xlsx.Cols.Col = append(xlsx.Cols.Col, colData) + } else { cols := xlsxCols{} cols.Col = append(cols.Col, colData) xlsx.Cols = &cols - return err } - for v := range xlsx.Cols.Col { - if xlsx.Cols.Col[v].Min <= colNum && colNum <= xlsx.Cols.Col[v].Max { - colData = xlsx.Cols.Col[v] - } - } - colData.Min = colNum - colData.Max = colNum - colData.Hidden = !visible - colData.CustomWidth = true - xlsx.Cols.Col = append(xlsx.Cols.Col, colData) - return err + return nil } + // GetColOutlineLevel provides a function to get outline level of a single // column by given worksheet name and column name. For example, get outline // level of column D in Sheet1: diff --git a/col_test.go b/col_test.go index cdb7edf..08fac1c 100644 --- a/col_test.go +++ b/col_test.go @@ -12,17 +12,41 @@ func TestColumnVisibility(t *testing.T) { f, err := prepareTestBook1() assert.NoError(t, err) + // Hide/display a column with SetColVisible assert.NoError(t, f.SetColVisible("Sheet1", "F", false)) assert.NoError(t, f.SetColVisible("Sheet1", "F", true)) visible, err := f.GetColVisible("Sheet1", "F") assert.Equal(t, true, visible) assert.NoError(t, err) - // Test get column visiable on not exists worksheet. + // Test hiding a few columns SetColVisible(...false)... + assert.NoError(t, f.SetColVisible("Sheet1", "F:V", false)) + visible, err = f.GetColVisible("Sheet1", "F") + assert.Equal(t, false, visible) + assert.NoError(t, err) + visible, err = f.GetColVisible("Sheet1", "U") + assert.Equal(t, false, visible) + assert.NoError(t, err) + visible, err = f.GetColVisible("Sheet1", "V") + assert.Equal(t, false, visible) + assert.NoError(t, err) + // ...and displaying them back SetColVisible(...true) + assert.NoError(t, f.SetColVisible("Sheet1", "F:V", true)) + visible, err = f.GetColVisible("Sheet1", "F") + assert.Equal(t, true, visible) + assert.NoError(t, err) + visible, err = f.GetColVisible("Sheet1", "U") + assert.Equal(t, true, visible) + assert.NoError(t, err) + visible, err = f.GetColVisible("Sheet1", "G") + assert.Equal(t, true, visible) + assert.NoError(t, err) + + // Test get column visible on an inexistent worksheet. _, err = f.GetColVisible("SheetN", "F") assert.EqualError(t, err, "sheet SheetN is not exist") - // Test get column visiable with illegal cell coordinates. + // Test get column visible with illegal cell coordinates. _, err = f.GetColVisible("Sheet1", "*") assert.EqualError(t, err, `invalid column name "*"`) assert.EqualError(t, f.SetColVisible("Sheet1", "*", false), `invalid column name "*"`) -- cgit v1.2.1 From e51aff2d9562bbfb290ef76a948facb6d4660eff Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 7 Feb 2020 00:25:01 +0800 Subject: Resolve #570, flat columns for the column's operation --- .travis.yml | 2 +- CONTRIBUTING.md | 6 ++- col.go | 125 +++++++++++++++++++++++++++++++++++++++----------------- col_test.go | 4 +- merge.go | 25 +++++------- picture.go | 4 +- sheet.go | 4 +- table.go | 6 ++- xmlWorksheet.go | 8 ++-- 9 files changed, 117 insertions(+), 67 deletions(-) diff --git a/.travis.yml b/.travis.yml index 5012f86..1cb1d49 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,7 +14,7 @@ os: - osx env: - matrix: + jobs: - GOARCH=amd64 - GOARCH=386 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index afb7d4e..53c650e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -234,7 +234,9 @@ By making a contribution to this project, I certify that: Then you just add a line to every git commit message: - Signed-off-by: Ri Xu https://xuri.me +```text +Signed-off-by: Ri Xu https://xuri.me +``` Use your real name (sorry, no pseudonyms or anonymous contributions.) @@ -460,4 +462,4 @@ Do not use package math/rand to generate keys, even throwaway ones. Unseeded, the generator is completely predictable. Seeded with time.Nanoseconds(), there are just a few bits of entropy. Instead, use crypto/rand's Reader, and if you need text, print to -hexadecimal or base64 +hexadecimal or base64. diff --git a/col.go b/col.go index ff771f1..6f76800 100644 --- a/col.go +++ b/col.go @@ -13,6 +13,8 @@ import ( "errors" "math" "strings" + + "github.com/mohae/deepcopy" ) // Define the default cell size and EMU unit of measurement. @@ -59,7 +61,7 @@ func (f *File) GetColVisible(sheet, col string) (bool, error) { // // err := f.SetColVisible("Sheet1", "D", false) // -// Hide the columns from D to F (included) +// Hide the columns from D to F (included): // // err := f.SetColVisible("Sheet1", "D:F", false) // @@ -87,23 +89,31 @@ func (f *File) SetColVisible(sheet, columns string, visible bool) error { return err } colData := xlsxCol{ - Min: min, - Max: max, - Width: 9, // default width - Hidden: !visible, + Min: min, + Max: max, + Width: 9, // default width + Hidden: !visible, CustomWidth: true, } - if xlsx.Cols != nil { - xlsx.Cols.Col = append(xlsx.Cols.Col, colData) - } else { + if xlsx.Cols == nil { cols := xlsxCols{} cols.Col = append(cols.Col, colData) xlsx.Cols = &cols - } + return nil + } + xlsx.Cols.Col = flatCols(colData, xlsx.Cols.Col, func(fc, c xlsxCol) xlsxCol { + fc.BestFit = c.BestFit + fc.Collapsed = c.Collapsed + fc.CustomWidth = c.CustomWidth + fc.OutlineLevel = c.OutlineLevel + fc.Phonetic = c.Phonetic + fc.Style = c.Style + fc.Width = c.Width + return fc + }) return nil } - // GetColOutlineLevel provides a function to get outline level of a single // column by given worksheet name and column name. For example, get outline // level of column D in Sheet1: @@ -162,16 +172,16 @@ func (f *File) SetColOutlineLevel(sheet, col string, level uint8) error { xlsx.Cols = &cols return err } - for v := range xlsx.Cols.Col { - if xlsx.Cols.Col[v].Min <= colNum && colNum <= xlsx.Cols.Col[v].Max { - colData = xlsx.Cols.Col[v] - } - } - colData.Min = colNum - colData.Max = colNum - colData.OutlineLevel = level - colData.CustomWidth = true - xlsx.Cols.Col = append(xlsx.Cols.Col, colData) + xlsx.Cols.Col = flatCols(colData, xlsx.Cols.Col, func(fc, c xlsxCol) xlsxCol { + fc.BestFit = c.BestFit + fc.Collapsed = c.Collapsed + fc.CustomWidth = c.CustomWidth + fc.Hidden = c.Hidden + fc.Phonetic = c.Phonetic + fc.Style = c.Style + fc.Width = c.Width + return fc + }) return err } @@ -214,21 +224,21 @@ func (f *File) SetColStyle(sheet, columns string, styleID int) error { if xlsx.Cols == nil { xlsx.Cols = &xlsxCols{} } - var find bool - for idx, col := range xlsx.Cols.Col { - if col.Min == min && col.Max == max { - xlsx.Cols.Col[idx].Style = styleID - find = true - } - } - if !find { - xlsx.Cols.Col = append(xlsx.Cols.Col, xlsxCol{ - Min: min, - Max: max, - Width: 9, - Style: styleID, - }) - } + xlsx.Cols.Col = flatCols(xlsxCol{ + Min: min, + Max: max, + Width: 9, + Style: styleID, + }, xlsx.Cols.Col, func(fc, c xlsxCol) xlsxCol { + fc.BestFit = c.BestFit + fc.Collapsed = c.Collapsed + fc.CustomWidth = c.CustomWidth + fc.Hidden = c.Hidden + fc.OutlineLevel = c.OutlineLevel + fc.Phonetic = c.Phonetic + fc.Width = c.Width + return fc + }) return nil } @@ -261,16 +271,55 @@ func (f *File) SetColWidth(sheet, startcol, endcol string, width float64) error Width: width, CustomWidth: true, } - if xlsx.Cols != nil { - xlsx.Cols.Col = append(xlsx.Cols.Col, col) - } else { + if xlsx.Cols == nil { cols := xlsxCols{} cols.Col = append(cols.Col, col) xlsx.Cols = &cols + return err } + xlsx.Cols.Col = flatCols(col, xlsx.Cols.Col, func(fc, c xlsxCol) xlsxCol { + fc.BestFit = c.BestFit + fc.Collapsed = c.Collapsed + fc.Hidden = c.Hidden + fc.OutlineLevel = c.OutlineLevel + fc.Phonetic = c.Phonetic + fc.Style = c.Style + return fc + }) return err } +// flatCols provides a method for the column's operation functions to flatten +// and check the worksheet columns. +func flatCols(col xlsxCol, cols []xlsxCol, replacer func(fc, c xlsxCol) xlsxCol) []xlsxCol { + fc := []xlsxCol{} + for i := col.Min; i <= col.Max; i++ { + c := deepcopy.Copy(col).(xlsxCol) + c.Min, c.Max = i, i + fc = append(fc, c) + } + inFlat := func(colID int, cols []xlsxCol) (int, bool) { + for idx, c := range cols { + if c.Max == colID && c.Min == colID { + return idx, true + } + } + return -1, false + } + for _, column := range cols { + for i := column.Min; i <= column.Max; i++ { + if idx, ok := inFlat(i, fc); ok { + fc[idx] = replacer(fc[idx], column) + continue + } + c := deepcopy.Copy(column).(xlsxCol) + c.Min, c.Max = i, i + fc = append(fc, c) + } + } + return fc +} + // positionObjectPixels calculate the vertices that define the position of a // graphical object within the worksheet in pixels. // diff --git a/col_test.go b/col_test.go index 08fac1c..050c998 100644 --- a/col_test.go +++ b/col_test.go @@ -31,7 +31,7 @@ func TestColumnVisibility(t *testing.T) { assert.Equal(t, false, visible) assert.NoError(t, err) // ...and displaying them back SetColVisible(...true) - assert.NoError(t, f.SetColVisible("Sheet1", "F:V", true)) + assert.NoError(t, f.SetColVisible("Sheet1", "V:F", true)) visible, err = f.GetColVisible("Sheet1", "F") assert.Equal(t, true, visible) assert.NoError(t, err) @@ -53,7 +53,7 @@ func TestColumnVisibility(t *testing.T) { f.NewSheet("Sheet3") assert.NoError(t, f.SetColVisible("Sheet3", "E", false)) - + assert.EqualError(t, f.SetColVisible("Sheet1", "A:-1", true), "invalid column name \"-1\"") assert.EqualError(t, f.SetColVisible("SheetN", "E", false), "sheet SheetN is not exist") assert.NoError(t, f.SaveAs(filepath.Join("test", "TestColumnVisibility.xlsx"))) }) diff --git a/merge.go b/merge.go index b952a1e..f29640d 100644 --- a/merge.go +++ b/merge.go @@ -22,20 +22,17 @@ import ( // If you create a merged cell that overlaps with another existing merged cell, // those merged cells that already exist will be removed. // -// B1(x1,y1) D1(x2,y1) -// +--------------------------------+ -// | | -// | | -// A4(x3,y3) | C4(x4,y3) | -// +-----------------------------+ | -// | | | | -// | | | | -// | |B5(x1,y2) | D5(x2,y2)| -// | +--------------------------------+ -// | | -// | | -// |A8(x3,y4) C8(x4,y4)| -// +-----------------------------+ +// B1(x1,y1) D1(x2,y1) +// +------------------------+ +// | | +// A4(x3,y3) | C4(x4,y3) | +// +------------------------+ | +// | | | | +// | |B5(x1,y2) | D5(x2,y2)| +// | +------------------------+ +// | | +// |A8(x3,y4) C8(x4,y4)| +// +------------------------+ // func (f *File) MergeCell(sheet, hcell, vcell string) error { rect1, err := f.areaRefToCoordinates(hcell + ":" + vcell) diff --git a/picture.go b/picture.go index 213bae9..0b91b91 100644 --- a/picture.go +++ b/picture.go @@ -462,8 +462,8 @@ func (f *File) GetPicture(sheet, cell string) (string, []byte, error) { return f.getPicture(row, col, drawingXML, drawingRelationships) } -// DeletePicture provides a function to delete chart in XLSX by given -// worksheet and cell name. Note that the image file won't deleted from the +// DeletePicture provides a function to delete charts in XLSX by given +// worksheet and cell name. Note that the image file won't be deleted from the // document currently. func (f *File) DeletePicture(sheet, cell string) (err error) { col, row, err := CellNameToCoordinates(cell) diff --git a/sheet.go b/sheet.go index 19b90c6..a6ff2a1 100644 --- a/sheet.go +++ b/sheet.go @@ -287,8 +287,8 @@ func (f *File) GetActiveSheetIndex() int { return 0 } -// SetSheetName provides a function to set the worksheet name be given old and -// new worksheet name. Maximum 31 characters are allowed in sheet title and +// SetSheetName provides a function to set the worksheet name by given old and +// new worksheet names. Maximum 31 characters are allowed in sheet title and // this function only changes the name of the sheet and will not update the // sheet name in the formula or reference associated with the cell. So there // may be problem formula error or reference missing. diff --git a/table.go b/table.go index 566238c..55901cd 100644 --- a/table.go +++ b/table.go @@ -39,8 +39,10 @@ func parseFormatTableSet(formatSet string) (*formatTable, error) { // // err := f.AddTable("Sheet2", "F2", "H6", `{"table_name":"table","table_style":"TableStyleMedium2", "show_first_column":true,"show_last_column":true,"show_row_stripes":false,"show_column_stripes":true}`) // -// Note that the table at least two lines include string type header. Multiple -// tables coordinate areas can't have an intersection. +// Note that the table must be at least two lines including the header. The +// header cells must contain strings and must be unique, and must set the +// header row data of the table before calling the AddTable function. Multiple +// tables coordinate areas that can't have an intersection. // // table_name: The name of the table, in the same worksheet name of the table should be unique // diff --git a/xmlWorksheet.go b/xmlWorksheet.go index ed304cc..46253e6 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -278,15 +278,15 @@ type xlsxCols struct { // width and column formatting for one or more columns of the worksheet. type xlsxCol struct { BestFit bool `xml:"bestFit,attr,omitempty"` - Collapsed bool `xml:"collapsed,attr"` + Collapsed bool `xml:"collapsed,attr,omitempty"` CustomWidth bool `xml:"customWidth,attr,omitempty"` - Hidden bool `xml:"hidden,attr"` + Hidden bool `xml:"hidden,attr,omitempty"` Max int `xml:"max,attr"` Min int `xml:"min,attr"` OutlineLevel uint8 `xml:"outlineLevel,attr,omitempty"` Phonetic bool `xml:"phonetic,attr,omitempty"` - Style int `xml:"style,attr"` - Width float64 `xml:"width,attr"` + Style int `xml:"style,attr,omitempty"` + Width float64 `xml:"width,attr,omitempty"` } // xlsxDimension directly maps the dimension element in the namespace -- cgit v1.2.1 From 023dba726510a4a7a97838ac9a8f4292a90aa227 Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 13 Feb 2020 00:00:42 +0800 Subject: Fix #576, serialize by fields order on stream flush --- CODE_OF_CONDUCT.md | 6 +++--- stream.go | 29 ++++++++++++----------------- stream_test.go | 7 +++++++ 3 files changed, 22 insertions(+), 20 deletions(-) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index a84b47f..572b561 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -40,7 +40,7 @@ Project maintainers who do not follow or enforce the Code of Conduct in good fai ## Attribution -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at [https://www.contributor-covenant.org/version/2/0/code_of_conduct][version] -[homepage]: http://contributor-covenant.org -[version]: http://contributor-covenant.org/version/1/4/ +[homepage]: https://www.contributor-covenant.org +[version]: https://www.contributor-covenant.org/version/2/0/code_of_conduct diff --git a/stream.go b/stream.go index 8398622..c854d8b 100644 --- a/stream.go +++ b/stream.go @@ -27,6 +27,7 @@ type StreamWriter struct { File *File Sheet string SheetID int + worksheet *xlsxWorksheet rawData bufferedWriter tableParts string } @@ -77,15 +78,15 @@ func (f *File) NewStreamWriter(sheet string) (*StreamWriter, error) { Sheet: sheet, SheetID: sheetID, } - - ws, err := f.workSheetReader(sheet) + var err error + sw.worksheet, err = f.workSheetReader(sheet) if err != nil { return nil, err } sw.rawData.WriteString(XMLHeader + ``) - return sw, nil + return sw, err } // AddTable creates an Excel table for the StreamWriter using the given @@ -373,7 +374,9 @@ func writeCell(buf *bufferedWriter, c xlsxC) { // Flush ending the streaming writing process. func (sw *StreamWriter) Flush() error { sw.rawData.WriteString(``) + bulkAppendFields(&sw.rawData, sw.worksheet, 7, 37) sw.rawData.WriteString(sw.tableParts) + bulkAppendFields(&sw.rawData, sw.worksheet, 39, 39) sw.rawData.WriteString(``) if err := sw.rawData.Flush(); err != nil { return err @@ -392,23 +395,15 @@ func (sw *StreamWriter) Flush() error { return nil } -// bulkAppendOtherFields bulk-appends fields in a worksheet, skipping the -// specified field names. -func bulkAppendOtherFields(w io.Writer, ws *xlsxWorksheet, skip ...string) { - skipMap := make(map[string]struct{}) - for _, name := range skip { - skipMap[name] = struct{}{} - } - +// bulkAppendFields bulk-appends fields in a worksheet by specified field +// names order range. +func bulkAppendFields(w io.Writer, ws *xlsxWorksheet, from, to int) { s := reflect.ValueOf(ws).Elem() - typeOfT := s.Type() enc := xml.NewEncoder(w) for i := 0; i < s.NumField(); i++ { - f := s.Field(i) - if _, ok := skipMap[typeOfT.Field(i).Name]; ok { - continue + if from <= i && i <= to { + enc.Encode(s.Field(i).Interface()) } - enc.Encode(f.Interface()) } } diff --git a/stream_test.go b/stream_test.go index 8c5e7ea..d89dad8 100644 --- a/stream_test.go +++ b/stream_test.go @@ -92,6 +92,13 @@ func TestStreamWriter(t *testing.T) { _, err = streamWriter.rawData.Reader() assert.NoError(t, err) assert.NoError(t, os.Remove(streamWriter.rawData.tmp.Name())) + + // Test unsupport charset + file = NewFile() + delete(file.Sheet, "xl/worksheets/sheet1.xml") + file.XLSX["xl/worksheets/sheet1.xml"] = MacintoshCyrillicCharset + streamWriter, err = file.NewStreamWriter("Sheet1") + assert.EqualError(t, err, "xml decode error: XML syntax error on line 1: invalid UTF-8") } func TestStreamTable(t *testing.T) { -- cgit v1.2.1 From 52f1eee7c487a086756bda857bb6390f8b4a0ffe Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 15 Feb 2020 16:34:47 +0800 Subject: Fix #578, escape character in the formula --- xmlWorksheet.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 46253e6..dda1b78 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -475,7 +475,7 @@ func (c *xlsxC) hasValue() bool { // http://schemas.openxmlformats.org/spreadsheetml/2006/main - currently I have // not checked it for completeness - it does as much as I need. type xlsxF struct { - Content string `xml:",innerxml"` + Content string `xml:",chardata"` T string `xml:"t,attr,omitempty"` // Formula type Ref string `xml:"ref,attr,omitempty"` // Shared formula ref Si string `xml:"si,attr,omitempty"` // Shared formula index -- cgit v1.2.1 From ad883caa0f77dfc016ae99bd5fbb606953eb99a0 Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 19 Feb 2020 00:08:10 +0800 Subject: Resolve #580, revert commit https://github.com/360EntSecGroup-Skylar/excelize/commit/5ca7231ed408ac264f509ff52b5d28ff4fbda757 --- README.md | 45 +++++++++++++++++++++++++++++---------------- README_zh.md | 45 +++++++++++++++++++++++++++++---------------- cell_test.go | 2 +- chart.go | 20 ++++++++++++++------ excelize.go | 6 +++--- excelize_test.go | 2 +- picture.go | 21 +++++++++++---------- pivotTable.go | 4 ++-- rows.go | 16 ++++++++-------- sheet.go | 2 +- sheet_test.go | 12 ++++++------ sheetpr_test.go | 8 ++++---- sheetview_test.go | 24 ++++++++++++------------ sparkline_test.go | 4 ++-- stream.go | 12 ++++++------ styles.go | 16 ++++++++-------- 16 files changed, 137 insertions(+), 102 deletions(-) diff --git a/README.md b/README.md index d7f696c..c81efd1 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,11 @@ Here is a minimal example usage that will create XLSX file. ```go package main -import "github.com/360EntSecGroup-Skylar/excelize" +import ( + "fmt" + + "github.com/360EntSecGroup-Skylar/excelize" +) func main() { f := excelize.NewFile() @@ -44,7 +48,7 @@ func main() { f.SetActiveSheet(index) // Save xlsx file by the given path. if err := f.SaveAs("Book1.xlsx"); err != nil { - println(err.Error()) + fmt.Println(err) } } ``` @@ -56,28 +60,32 @@ The following constitutes the bare to read a XLSX document. ```go package main -import "github.com/360EntSecGroup-Skylar/excelize" +import ( + "fmt" + + "github.com/360EntSecGroup-Skylar/excelize" +) func main() { f, err := excelize.OpenFile("Book1.xlsx") if err != nil { - println(err.Error()) + fmt.Println(err) return } // Get value from cell by given worksheet name and axis. cell, err := f.GetCellValue("Sheet1", "B2") if err != nil { - println(err.Error()) + fmt.Println(err) return } - println(cell) + fmt.Println(cell) // Get all the rows in the Sheet1. rows, err := f.GetRows("Sheet1") for _, row := range rows { for _, colCell := range row { - print(colCell, "\t") + fmt.Print(colCell, "\t") } - println() + fmt.Println() } } ``` @@ -91,7 +99,11 @@ With Excelize chart generation and management is as easy as a few lines of code. ```go package main -import "github.com/360EntSecGroup-Skylar/excelize" +import ( + "fmt" + + "github.com/360EntSecGroup-Skylar/excelize" +) func main() { categories := map[string]string{"A2": "Small", "A3": "Normal", "A4": "Large", "B1": "Apple", "C1": "Orange", "D1": "Pear"} @@ -104,12 +116,12 @@ func main() { f.SetCellValue("Sheet1", k, v) } if err := f.AddChart("Sheet1", "E1", `{"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"}}`); err != nil { - println(err.Error()) + fmt.Println(err) return } // Save xlsx file by the given path. if err := f.SaveAs("Book1.xlsx"); err != nil { - println(err.Error()) + fmt.Println(err) } } ``` @@ -120,6 +132,7 @@ func main() { package main import ( + "fmt" _ "image/gif" _ "image/jpeg" _ "image/png" @@ -130,24 +143,24 @@ import ( func main() { f, err := excelize.OpenFile("Book1.xlsx") if err != nil { - println(err.Error()) + fmt.Println(err) return } // Insert a picture. if err := f.AddPicture("Sheet1", "A2", "image.png", ""); err != nil { - println(err.Error()) + fmt.Println(err) } // Insert a picture to worksheet with scaling. if err := f.AddPicture("Sheet1", "D2", "image.jpg", `{"x_scale": 0.5, "y_scale": 0.5}`); err != nil { - println(err.Error()) + fmt.Println(err) } // Insert a picture offset in the cell with printing support. if err := f.AddPicture("Sheet1", "H2", "image.gif", `{"x_offset": 15, "y_offset": 10, "print_obj": true, "lock_aspect_ratio": false, "locked": false}`); err != nil { - println(err.Error()) + fmt.Println(err) } // Save the xlsx file with the origin path. if err = f.Save(); err != nil { - println(err.Error()) + fmt.Println(err) } } ``` diff --git a/README_zh.md b/README_zh.md index c1ee83e..f75eec5 100644 --- a/README_zh.md +++ b/README_zh.md @@ -30,7 +30,11 @@ go get github.com/360EntSecGroup-Skylar/excelize ```go package main -import "github.com/360EntSecGroup-Skylar/excelize" +import ( + "fmt" + + "github.com/360EntSecGroup-Skylar/excelize" +) func main() { f := excelize.NewFile() @@ -43,7 +47,7 @@ func main() { f.SetActiveSheet(index) // 根据指定路径保存文件 if err := f.SaveAs("Book1.xlsx"); err != nil { - println(err.Error()) + fmt.Println(err) } } ``` @@ -55,28 +59,32 @@ func main() { ```go package main -import "github.com/360EntSecGroup-Skylar/excelize" +import ( + "fmt" + + "github.com/360EntSecGroup-Skylar/excelize" +) func main() { f, err := excelize.OpenFile("Book1.xlsx") if err != nil { - println(err.Error()) + fmt.Println(err) return } // 获取工作表中指定单元格的值 cell, err := f.GetCellValue("Sheet1", "B2") if err != nil { - println(err.Error()) + fmt.Println(err) return } - println(cell) + fmt.Println(cell) // 获取 Sheet1 上所有单元格 rows, err := f.GetRows("Sheet1") for _, row := range rows { for _, colCell := range row { - print(colCell, "\t") + fmt.Print(colCell, "\t") } - println() + fmt.Println() } } ``` @@ -90,7 +98,11 @@ func main() { ```go package main -import "github.com/360EntSecGroup-Skylar/excelize" +import ( + "fmt" + + "github.com/360EntSecGroup-Skylar/excelize" +) func main() { categories := map[string]string{"A2": "Small", "A3": "Normal", "A4": "Large", "B1": "Apple", "C1": "Orange", "D1": "Pear"} @@ -103,12 +115,12 @@ func main() { f.SetCellValue("Sheet1", k, v) } if err := f.AddChart("Sheet1", "E1", `{"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"}}`); err != nil { - println(err.Error()) + fmt.Println(err) return } // 根据指定路径保存文件 if err := f.SaveAs("Book1.xlsx"); err != nil { - println(err.Error()) + fmt.Println(err) } } ``` @@ -119,6 +131,7 @@ func main() { package main import ( + "fmt" _ "image/gif" _ "image/jpeg" _ "image/png" @@ -129,24 +142,24 @@ import ( func main() { f, err := excelize.OpenFile("Book1.xlsx") if err != nil { - println(err.Error()) + fmt.Println(err) return } // 插入图片 if err := f.AddPicture("Sheet1", "A2", "image.png", ""); err != nil { - println(err.Error()) + fmt.Println(err) } // 在工作表中插入图片,并设置图片的缩放比例 if err := f.AddPicture("Sheet1", "D2", "image.jpg", `{"x_scale": 0.5, "y_scale": 0.5}`); err != nil { - println(err.Error()) + fmt.Println(err) } // 在工作表中插入图片,并设置图片的打印属性 if err := f.AddPicture("Sheet1", "H2", "image.gif", `{"x_offset": 15, "y_offset": 10, "print_obj": true, "lock_aspect_ratio": false, "locked": false}`); err != nil { - println(err.Error()) + fmt.Println(err) } // 保存文件 if err = f.Save(); err != nil { - println(err.Error()) + fmt.Println(err) } } ``` diff --git a/cell_test.go b/cell_test.go index 60f8751..1efbc5a 100644 --- a/cell_test.go +++ b/cell_test.go @@ -110,7 +110,7 @@ func ExampleFile_SetCellFloat() { f := NewFile() var x = 3.14159265 if err := f.SetCellFloat("Sheet1", "A1", x, 2, 64); err != nil { - println(err.Error()) + fmt.Println(err) } val, _ := f.GetCellValue("Sheet1", "A1") fmt.Println(val) diff --git a/chart.go b/chart.go index 227cdee..69c2c95 100644 --- a/chart.go +++ b/chart.go @@ -503,7 +503,11 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // // package main // -// import "github.com/360EntSecGroup-Skylar/excelize" +// import ( +// "fmt" +// +// "github.com/360EntSecGroup-Skylar/excelize" +// ) // // func main() { // categories := map[string]string{"A2": "Small", "A3": "Normal", "A4": "Large", "B1": "Apple", "C1": "Orange", "D1": "Pear"} @@ -516,12 +520,12 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // f.SetCellValue("Sheet1", k, v) // } // if err := f.AddChart("Sheet1", "E1", `{"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"},"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","x_axis":{"reverse_order":true},"y_axis":{"maximum":7.5,"minimum":0.5}}`); err != nil { -// println(err.Error()) +// fmt.Println(err) // return // } // // Save xlsx file by the given path. // if err := f.SaveAs("Book1.xlsx"); err != nil { -// println(err.Error()) +// fmt.Println(err) // } // } // @@ -697,7 +701,11 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // // package main // -// import "github.com/360EntSecGroup-Skylar/excelize" +// import ( +// "fmt" +// +// "github.com/360EntSecGroup-Skylar/excelize" +// ) // // func main() { // categories := map[string]string{"A2": "Small", "A3": "Normal", "A4": "Large", "B1": "Apple", "C1": "Orange", "D1": "Pear"} @@ -710,12 +718,12 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // f.SetCellValue("Sheet1", k, v) // } // if err := f.AddChart("Sheet1", "E1", `{"type":"col","series":[{"name":"Sheet1!$A$2","categories":"","values":"Sheet1!$B$2:$D$2"},{"name":"Sheet1!$A$3","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$3:$D$3"}],"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":"left","show_legend_key":false},"title":{"name":"Clustered Column - Line Chart"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true}}`, `{"type":"line","series":[{"name":"Sheet1!$A$4","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$4:$D$4"}],"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":"left","show_legend_key":false},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true}}`); err != nil { -// println(err.Error()) +// fmt.Println(err) // return // } // // Save xlsx file by the given path. // if err := f.SaveAs("Book1.xlsx"); err != nil { -// println(err.Error()) +// fmt.Println(err) // } // } // diff --git a/excelize.go b/excelize.go index 9832c6a..e12e769 100644 --- a/excelize.go +++ b/excelize.go @@ -294,13 +294,13 @@ func (f *File) UpdateLinkedValue() error { // functions and/or macros. The file extension should be .xlsm. For example: // // if err := f.SetSheetPrOptions("Sheet1", excelize.CodeName("Sheet1")); err != nil { -// println(err.Error()) +// fmt.Println(err) // } // if err := f.AddVBAProject("vbaProject.bin"); err != nil { -// println(err.Error()) +// fmt.Println(err) // } // if err := f.SaveAs("macros.xlsm"); err != nil { -// println(err.Error()) +// fmt.Println(err) // } // func (f *File) AddVBAProject(bin string) error { diff --git a/excelize_test.go b/excelize_test.go index c7f5cad..b78aac8 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -1259,7 +1259,7 @@ func fillCells(f *File, sheet string, colCount, rowCount int) { for row := 1; row <= rowCount; row++ { cell, _ := CoordinatesToCellName(col, row) if err := f.SetCellStr(sheet, cell, cell); err != nil { - println(err.Error()) + fmt.Println(err) } } } diff --git a/picture.go b/picture.go index 0b91b91..3e24ce3 100644 --- a/picture.go +++ b/picture.go @@ -59,18 +59,18 @@ func parseFormatPictureSet(formatSet string) (*formatPicture, error) { // f := excelize.NewFile() // // Insert a picture. // if err := f.AddPicture("Sheet1", "A2", "image.jpg", ""); err != nil { -// println(err.Error()) +// fmt.Println(err) // } // // Insert a picture scaling in the cell with location hyperlink. // if err := f.AddPicture("Sheet1", "D2", "image.png", `{"x_scale": 0.5, "y_scale": 0.5, "hyperlink": "#Sheet2!D8", "hyperlink_type": "Location"}`); err != nil { -// println(err.Error()) +// fmt.Println(err) // } // // Insert a picture offset in the cell with external hyperlink, printing and positioning support. // if err := f.AddPicture("Sheet1", "H2", "image.gif", `{"x_offset": 15, "y_offset": 10, "hyperlink": "https://github.com/360EntSecGroup-Skylar/excelize", "hyperlink_type": "External", "print_obj": true, "lock_aspect_ratio": false, "locked": false, "positioning": "oneCell"}`); err != nil { -// println(err.Error()) +// fmt.Println(err) // } // if err := f.SaveAs("Book1.xlsx"); err != nil { -// println(err.Error()) +// fmt.Println(err) // } // } // @@ -104,6 +104,7 @@ func (f *File) AddPicture(sheet, cell, picture, format string) error { // package main // // import ( +// "fmt" // _ "image/jpeg" // "io/ioutil" // @@ -115,13 +116,13 @@ func (f *File) AddPicture(sheet, cell, picture, format string) error { // // file, err := ioutil.ReadFile("image.jpg") // if err != nil { -// println(err.Error()) +// fmt.Println(err) // } // if err := f.AddPictureFromBytes("Sheet1", "A2", "", "Excel Logo", ".jpg", file); err != nil { -// println(err.Error()) +// fmt.Println(err) // } // if err := f.SaveAs("Book1.xlsx"); err != nil { -// println(err.Error()) +// fmt.Println(err) // } // } // @@ -424,16 +425,16 @@ func (f *File) getSheetRelationshipsTargetByID(sheet, rID string) string { // // f, err := excelize.OpenFile("Book1.xlsx") // if err != nil { -// println(err.Error()) +// fmt.Println(err) // return // } // file, raw, err := f.GetPicture("Sheet1", "A2") // if err != nil { -// println(err.Error()) +// fmt.Println(err) // return // } // if err := ioutil.WriteFile(file, raw, 0644); err != nil { -// println(err.Error()) +// fmt.Println(err) // } // func (f *File) GetPicture(sheet, cell string) (string, []byte, error) { diff --git a/pivotTable.go b/pivotTable.go index 70681ca..ee0d94e 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -63,10 +63,10 @@ type PivotTableOption struct { // Columns: []string{"Type"}, // Data: []string{"Sales"}, // }); err != nil { -// println(err.Error()) +// fmt.Println(err) // } // if err := f.SaveAs("Book1.xlsx"); err != nil { -// println(err.Error()) +// fmt.Println(err) // } // } // diff --git a/rows.go b/rows.go index 40972ae..23f3a2c 100644 --- a/rows.go +++ b/rows.go @@ -25,18 +25,18 @@ import ( // // rows, err := f.Rows("Sheet1") // if err != nil { -// println(err.Error()) +// fmt.Println(err) // return // } // for rows.Next() { // row, err := rows.Columns() // if err != nil { -// println(err.Error()) +// fmt.Println(err) // } // for _, colCell := range row { -// print(colCell, "\t") +// fmt.Print(colCell, "\t") // } -// println() +// fmt.Println() // } // func (f *File) GetRows(sheet string) ([][]string, error) { @@ -152,18 +152,18 @@ func (err ErrSheetNotExist) Error() string { // // rows, err := f.Rows("Sheet1") // if err != nil { -// println(err.Error()) +// fmt.Println(err) // return // } // for rows.Next() { // row, err := rows.Columns() // if err != nil { -// println(err.Error()) +// fmt.Println(err) // } // for _, colCell := range row { -// print(colCell, "\t") +// fmt.Print(colCell, "\t") // } -// println() +// fmt.Println() // } // func (f *File) Rows(sheet string) (*Rows, error) { diff --git a/sheet.go b/sheet.go index a6ff2a1..48671c0 100644 --- a/sheet.go +++ b/sheet.go @@ -344,7 +344,7 @@ func (f *File) GetSheetIndex(name string) int { // return // } // for index, name := range f.GetSheetMap() { -// println(index, name) +// fmt.Println(index, name) // } // func (f *File) GetSheetMap() map[int]string { diff --git a/sheet_test.go b/sheet_test.go index a03066a..69c8f22 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -19,7 +19,7 @@ func ExampleFile_SetPageLayout() { "Sheet1", excelize.PageLayoutOrientation(excelize.OrientationLandscape), ); err != nil { - println(err.Error()) + fmt.Println(err) } if err := f.SetPageLayout( "Sheet1", @@ -27,7 +27,7 @@ func ExampleFile_SetPageLayout() { excelize.FitToHeight(2), excelize.FitToWidth(2), ); err != nil { - println(err.Error()) + fmt.Println(err) } // Output: } @@ -41,17 +41,17 @@ func ExampleFile_GetPageLayout() { fitToWidth excelize.FitToWidth ) if err := f.GetPageLayout("Sheet1", &orientation); err != nil { - println(err.Error()) + fmt.Println(err) } if err := f.GetPageLayout("Sheet1", &paperSize); err != nil { - println(err.Error()) + fmt.Println(err) } if err := f.GetPageLayout("Sheet1", &fitToHeight); err != nil { - println(err.Error()) + fmt.Println(err) } if err := f.GetPageLayout("Sheet1", &fitToWidth); err != nil { - println(err.Error()) + fmt.Println(err) } fmt.Println("Defaults:") fmt.Printf("- orientation: %q\n", orientation) diff --git a/sheetpr_test.go b/sheetpr_test.go index 6a35a6e..25b67d7 100644 --- a/sheetpr_test.go +++ b/sheetpr_test.go @@ -40,7 +40,7 @@ func ExampleFile_SetSheetPrOptions() { excelize.AutoPageBreaks(true), excelize.OutlineSummaryBelow(false), ); err != nil { - println(err.Error()) + fmt.Println(err) } // Output: } @@ -66,7 +66,7 @@ func ExampleFile_GetSheetPrOptions() { &autoPageBreaks, &outlineSummaryBelow, ); err != nil { - println(err.Error()) + fmt.Println(err) } fmt.Println("Defaults:") fmt.Printf("- codeName: %q\n", codeName) @@ -189,7 +189,7 @@ func ExampleFile_SetPageMargins() { excelize.PageMarginRight(1.0), excelize.PageMarginTop(1.0), ); err != nil { - println(err.Error()) + fmt.Println(err) } // Output: } @@ -215,7 +215,7 @@ func ExampleFile_GetPageMargins() { &marginRight, &marginTop, ); err != nil { - println(err.Error()) + fmt.Println(err) } fmt.Println("Defaults:") fmt.Println("- marginBottom:", marginBottom) diff --git a/sheetview_test.go b/sheetview_test.go index 8412002..d999875 100644 --- a/sheetview_test.go +++ b/sheetview_test.go @@ -47,7 +47,7 @@ func ExampleFile_SetSheetViewOptions() { excelize.ZoomScale(80), excelize.TopLeftCell("C3"), ); err != nil { - println(err.Error()) + fmt.Println(err) } var zoomScale excelize.ZoomScale @@ -55,22 +55,22 @@ func ExampleFile_SetSheetViewOptions() { fmt.Println("- zoomScale: 80") if err := f.SetSheetViewOptions(sheet, 0, excelize.ZoomScale(500)); err != nil { - println(err.Error()) + fmt.Println(err) } if err := f.GetSheetViewOptions(sheet, 0, &zoomScale); err != nil { - println(err.Error()) + fmt.Println(err) } fmt.Println("Used out of range value:") fmt.Println("- zoomScale:", zoomScale) if err := f.SetSheetViewOptions(sheet, 0, excelize.ZoomScale(123)); err != nil { - println(err.Error()) + fmt.Println(err) } if err := f.GetSheetViewOptions(sheet, 0, &zoomScale); err != nil { - println(err.Error()) + fmt.Println(err) } fmt.Println("Used correct value:") @@ -111,7 +111,7 @@ func ExampleFile_GetSheetViewOptions() { &zoomScale, &topLeftCell, ); err != nil { - println(err.Error()) + fmt.Println(err) } fmt.Println("Default:") @@ -125,27 +125,27 @@ func ExampleFile_GetSheetViewOptions() { fmt.Println("- topLeftCell:", `"`+topLeftCell+`"`) if err := f.SetSheetViewOptions(sheet, 0, excelize.TopLeftCell("B2")); err != nil { - println(err.Error()) + fmt.Println(err) } if err := f.GetSheetViewOptions(sheet, 0, &topLeftCell); err != nil { - println(err.Error()) + fmt.Println(err) } if err := f.SetSheetViewOptions(sheet, 0, excelize.ShowGridLines(false)); err != nil { - println(err.Error()) + fmt.Println(err) } if err := f.GetSheetViewOptions(sheet, 0, &showGridLines); err != nil { - println(err.Error()) + fmt.Println(err) } if err := f.SetSheetViewOptions(sheet, 0, excelize.ShowZeros(false)); err != nil { - println(err.Error()) + fmt.Println(err) } if err := f.GetSheetViewOptions(sheet, 0, &showZeros); err != nil { - println(err.Error()) + fmt.Println(err) } fmt.Println("After change:") diff --git a/sparkline_test.go b/sparkline_test.go index 45bf386..4b059ab 100644 --- a/sparkline_test.go +++ b/sparkline_test.go @@ -298,12 +298,12 @@ func prepareSparklineDataset() *File { f.NewSheet("Sheet3") for row, data := range sheet2 { if err := f.SetSheetRow("Sheet2", fmt.Sprintf("A%d", row+1), &data); err != nil { - println(err.Error()) + fmt.Println(err) } } for row, data := range sheet3 { if err := f.SetSheetRow("Sheet3", fmt.Sprintf("A%d", row+1), &data); err != nil { - println(err.Error()) + fmt.Println(err) } } return f diff --git a/stream.go b/stream.go index c854d8b..98cf828 100644 --- a/stream.go +++ b/stream.go @@ -42,14 +42,14 @@ type StreamWriter struct { // file := excelize.NewFile() // streamWriter, err := file.NewStreamWriter("Sheet1") // if err != nil { -// println(err.Error()) +// fmt.Println(err) // } // styleID, err := file.NewStyle(`{"font":{"color":"#777777"}}`) // if err != nil { -// println(err.Error()) +// fmt.Println(err) // } // if err := streamWriter.SetRow("A1", []interface{}{excelize.Cell{StyleID: styleID, Value: "Data"}}); err != nil { -// println(err.Error()) +// fmt.Println(err) // } // for rowID := 2; rowID <= 102400; rowID++ { // row := make([]interface{}, 50) @@ -58,14 +58,14 @@ type StreamWriter struct { // } // cell, _ := excelize.CoordinatesToCellName(1, rowID) // if err := streamWriter.SetRow(cell, row); err != nil { -// println(err.Error()) +// fmt.Println(err) // } // } // if err := streamWriter.Flush(); err != nil { -// println(err.Error()) +// fmt.Println(err) // } // if err := file.SaveAs("Book1.xlsx"); err != nil { -// println(err.Error()) +// fmt.Println(err) // } // func (f *File) NewStreamWriter(sheet string) (*StreamWriter, error) { diff --git a/styles.go b/styles.go index ad3e825..272d728 100644 --- a/styles.go +++ b/styles.go @@ -2321,7 +2321,7 @@ func (f *File) GetCellStyle(sheet, axis string) (int, error) { // // style, err := f.NewStyle(`{"border":[{"type":"left","color":"0000FF","style":3},{"type":"top","color":"00FF00","style":4},{"type":"bottom","color":"FFFF00","style":5},{"type":"right","color":"FF0000","style":6},{"type":"diagonalDown","color":"A020F0","style":7},{"type":"diagonalUp","color":"A020F0","style":8}]}`) // if err != nil { -// println(err.Error()) +// fmt.Println(err) // } // err = f.SetCellStyle("Sheet1", "H9", "H9", style) // @@ -2330,7 +2330,7 @@ func (f *File) GetCellStyle(sheet, axis string) (int, error) { // // style, err := f.NewStyle(`{"fill":{"type":"gradient","color":["#FFFFFF","#E0EBF5"],"shading":1}}`) // if err != nil { -// println(err.Error()) +// fmt.Println(err) // } // err = f.SetCellStyle("Sheet1", "H9", "H9", style) // @@ -2338,7 +2338,7 @@ func (f *File) GetCellStyle(sheet, axis string) (int, error) { // // style, err := f.NewStyle(`{"fill":{"type":"pattern","color":["#E0EBF5"],"pattern":1}}`) // if err != nil { -// println(err.Error()) +// fmt.Println(err) // } // err = f.SetCellStyle("Sheet1", "H9", "H9", style) // @@ -2346,7 +2346,7 @@ func (f *File) GetCellStyle(sheet, axis string) (int, error) { // // style, err := f.NewStyle(`{"alignment":{"horizontal":"center","ident":1,"justify_last_line":true,"reading_order":0,"relative_indent":1,"shrink_to_fit":true,"text_rotation":45,"vertical":"","wrap_text":true}}`) // if err != nil { -// println(err.Error()) +// fmt.Println(err) // } // err = f.SetCellStyle("Sheet1", "H9", "H9", style) // @@ -2357,7 +2357,7 @@ func (f *File) GetCellStyle(sheet, axis string) (int, error) { // f.SetCellValue("Sheet1", "H9", 42920.5) // style, err := f.NewStyle(`{"number_format": 22}`) // if err != nil { -// println(err.Error()) +// fmt.Println(err) // } // err = f.SetCellStyle("Sheet1", "H9", "H9", style) // @@ -2365,7 +2365,7 @@ func (f *File) GetCellStyle(sheet, axis string) (int, error) { // // style, err := f.NewStyle(`{"font":{"bold":true,"italic":true,"family":"Times New Roman","size":36,"color":"#777777"}}`) // if err != nil { -// println(err.Error()) +// fmt.Println(err) // } // err = f.SetCellStyle("Sheet1", "H9", "H9", style) // @@ -2373,7 +2373,7 @@ func (f *File) GetCellStyle(sheet, axis string) (int, error) { // // style, err := f.NewStyle(`{"protection":{"hidden":true, "locked":true}}`) // if err != nil { -// println(err.Error()) +// fmt.Println(err) // } // err = f.SetCellStyle("Sheet1", "H9", "H9", style) // @@ -2507,7 +2507,7 @@ func (f *File) SetCellStyle(sheet, hcell, vcell string, styleID int) error { // // format, err = f.NewConditionalStyle(`{"font":{"color":"#9A0511"},"fill":{"type":"pattern","color":["#FEC7CE"],"pattern":1}}`) // if err != nil { -// println(err.Error()) +// fmt.Println(err) // } // f.SetConditionalFormat("Sheet1", "A1:A10", fmt.Sprintf(`[{"type":"cell","criteria":">","format":%d,"value":"6"}]`, format)) // -- cgit v1.2.1 From 6dcb7013eeeb8902be97c564c7a5a05dddcb06b8 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 21 Feb 2020 23:07:43 +0800 Subject: Resolve #582, support to set date field subtotal and names for pivot table - typo fixed and update do.dev badge in the README. --- README.md | 2 +- README_zh.md | 2 +- pivotTable.go | 58 ++++++++++++++++++++++++++++++++++++++++++++++-------- pivotTable_test.go | 25 +++++++++++++++++++++++ 4 files changed, 77 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index c81efd1..fa1dda9 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Build Status Code Coverage Go Report Card - GoDoc + go.dev Licenses Donate

diff --git a/README_zh.md b/README_zh.md index f75eec5..44ab9b5 100644 --- a/README_zh.md +++ b/README_zh.md @@ -4,7 +4,7 @@ Build Status Code Coverage Go Report Card - GoDoc + go.dev Licenses Donate

diff --git a/pivotTable.go b/pivotTable.go index ee0d94e..696dfe7 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -20,6 +20,8 @@ import ( // PivotTableOption directly maps the format settings of the pivot table. type PivotTableOption struct { DataRange string + DataSubtotal string + DataFieldName string PivotTableRange string Rows []string Columns []string @@ -28,9 +30,29 @@ type PivotTableOption struct { } // AddPivotTable provides the method to add pivot table by given pivot table -// options. For example, create a pivot table on the Sheet1!$G$2:$M$34 area -// with the region Sheet1!$A$1:$E$31 as the data source, summarize by sum for -// sales: +// options. +// +// DataSubtotal specifies the aggregation function that applies to this data +// field. The default value is sum. The possible values for this attribute +// are: +// +// Average +// Count +// CountNums +// Max +// Min +// Product +// StdDev +// StdDevp +// Sum +// Var +// Varp +// +// DataFieldName specifies the name of the data field. Maximum 255 characters +// are allowed in data field name, excess characters will be truncated. +// +// For example, create a pivot table on the Sheet1!$G$2:$M$34 area with the +// region Sheet1!$A$1:$E$31 as the data source, summarize by sum for sales: // // package main // @@ -62,6 +84,8 @@ type PivotTableOption struct { // Rows: []string{"Month", "Year"}, // Columns: []string{"Type"}, // Data: []string{"Sales"}, +// DataSubtotal: "Sum", +// DataFieldName: "Summarize as Sum", // }); err != nil { // fmt.Println(err) // } @@ -278,9 +302,9 @@ func (f *File) addPivotTable(cacheID, pivotTableID int, pivotTableXML string, op if err != nil { return err } - for _, filedIdx := range rowFieldsIndex { + for _, fieldIdx := range rowFieldsIndex { pt.RowFields.Field = append(pt.RowFields.Field, &xlsxField{ - X: filedIdx, + X: fieldIdx, }) } @@ -297,9 +321,15 @@ func (f *File) addPivotTable(cacheID, pivotTableID int, pivotTableXML string, op if err != nil { return err } + dataFieldName := opt.DataFieldName + if len(dataFieldName) > 255 { + dataFieldName = dataFieldName[0:255] + } for _, dataField := range dataFieldsIndex { pt.DataFields.DataField = append(pt.DataFields.DataField, &xlsxDataField{ - Fld: dataField, + Name: dataFieldName, + Fld: dataField, + Subtotal: f.getFieldsSubtotal(opt), }) } @@ -336,9 +366,9 @@ func (f *File) addPivotColFields(pt *xlsxPivotTableDefinition, opt *PivotTableOp if err != nil { return err } - for _, filedIdx := range colFieldsIndex { + for _, fieldIdx := range colFieldsIndex { pt.ColFields.Field = append(pt.ColFields.Field, &xlsxField{ - X: filedIdx, + X: fieldIdx, }) } @@ -430,6 +460,18 @@ func (f *File) getPivotFieldsIndex(fields []string, opt *PivotTableOption) ([]in return pivotFieldsIndex, nil } +// getFieldsSubtotal prepare data subtotal by given fields and pivot option. +func (f *File) getFieldsSubtotal(opt *PivotTableOption) (subtotal string) { + subtotal = "sum" + for _, enum := range []string{"average", "count", "countNums", "max", "min", "product", "stdDev", "stdDevp", "sum", "var", "varp"} { + if strings.ToLower(enum) == strings.ToLower(opt.DataSubtotal) { + subtotal = enum + return + } + } + return +} + // addWorkbookPivotCache add the association ID of the pivot cache in xl/workbook.xml. func (f *File) addWorkbookPivotCache(RID int) int { wb := f.workbookReader() diff --git a/pivotTable_test.go b/pivotTable_test.go index 5d841d8..e40dbd6 100644 --- a/pivotTable_test.go +++ b/pivotTable_test.go @@ -4,6 +4,7 @@ import ( "fmt" "math/rand" "path/filepath" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -30,6 +31,8 @@ func TestAddPivotTable(t *testing.T) { Rows: []string{"Month", "Year"}, Columns: []string{"Type"}, Data: []string{"Sales"}, + DataSubtotal: "Sum", + DataFieldName: "Summarize by Sum", })) // Use different order of coordinate tests assert.NoError(t, f.AddPivotTable(&PivotTableOption{ @@ -38,6 +41,8 @@ func TestAddPivotTable(t *testing.T) { Rows: []string{"Month", "Year"}, Columns: []string{"Type"}, Data: []string{"Sales"}, + DataSubtotal: "Average", + DataFieldName: "Summarize by Average", })) assert.NoError(t, f.AddPivotTable(&PivotTableOption{ @@ -46,6 +51,8 @@ func TestAddPivotTable(t *testing.T) { Rows: []string{"Month", "Year"}, Columns: []string{"Region"}, Data: []string{"Sales"}, + DataSubtotal: "Count", + DataFieldName: "Summarize by Count", })) assert.NoError(t, f.AddPivotTable(&PivotTableOption{ DataRange: "Sheet1!$A$1:$E$31", @@ -53,12 +60,16 @@ func TestAddPivotTable(t *testing.T) { Rows: []string{"Month"}, Columns: []string{"Region", "Year"}, Data: []string{"Sales"}, + DataSubtotal: "CountNums", + DataFieldName: "Summarize by CountNums", })) assert.NoError(t, f.AddPivotTable(&PivotTableOption{ DataRange: "Sheet1!$A$1:$E$31", PivotTableRange: "Sheet1!$AE$2:$AG$33", Rows: []string{"Month", "Year"}, Data: []string{"Sales"}, + DataSubtotal: "Max", + DataFieldName: "Summarize by Max", })) f.NewSheet("Sheet2") assert.NoError(t, f.AddPivotTable(&PivotTableOption{ @@ -67,6 +78,8 @@ func TestAddPivotTable(t *testing.T) { Rows: []string{"Month"}, Columns: []string{"Region", "Type", "Year"}, Data: []string{"Sales"}, + DataSubtotal: "Min", + DataFieldName: "Summarize by Min", })) assert.NoError(t, f.AddPivotTable(&PivotTableOption{ DataRange: "Sheet1!$A$1:$E$31", @@ -74,6 +87,8 @@ func TestAddPivotTable(t *testing.T) { Rows: []string{"Month", "Type"}, Columns: []string{"Region", "Year"}, Data: []string{"Sales"}, + DataSubtotal: "Product", + DataFieldName: "Summarize by Product", })) // Test empty pivot table options @@ -135,6 +150,16 @@ func TestAddPivotTable(t *testing.T) { Data: []string{"Sales"}, }), `parameter 'DataRange' parsing error: cannot convert cell "A0" to coordinates: invalid cell name "A0"`) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddPivotTable1.xlsx"))) + // Test with field names that exceed the length limit and invalid subtotal + assert.NoError(t, f.AddPivotTable(&PivotTableOption{ + DataRange: "Sheet1!$A$1:$E$31", + PivotTableRange: "Sheet1!$G$2:$M$34", + Rows: []string{"Month", "Year"}, + Columns: []string{"Type"}, + Data: []string{"Sales"}, + DataSubtotal: "-", + DataFieldName: strings.Repeat("s", 256), + })) // Test adjust range with invalid range _, _, err := f.adjustRange("") -- cgit v1.2.1 From 8b20ea1685cdb010be8f95ffc047fa44e1a0e90a Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 25 Feb 2020 00:19:22 +0800 Subject: Fix #586, duplicate row with merged cells --- rows.go | 34 ++++++++++++++++++++++++++++++++ rows_test.go | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+) diff --git a/rows.go b/rows.go index 23f3a2c..0684b18 100644 --- a/rows.go +++ b/rows.go @@ -519,6 +519,40 @@ func (f *File) DuplicateRowTo(sheet string, row, row2 int) error { } else { xlsx.SheetData.Row = append(xlsx.SheetData.Row, rowCopy) } + return f.duplicateMergeCells(sheet, xlsx, row, row2) +} + +// duplicateMergeCells merge cells in the destination row if there are single +// row merged cells in the copied row. +func (f *File) duplicateMergeCells(sheet string, xlsx *xlsxWorksheet, row, row2 int) error { + if xlsx.MergeCells == nil { + return nil + } + if row > row2 { + row++ + } + for _, rng := range xlsx.MergeCells.Cells { + coordinates, err := f.areaRefToCoordinates(rng.Ref) + if err != nil { + return err + } + if coordinates[1] < row2 && row2 < coordinates[3] { + return nil + } + } + for i := 0; i < len(xlsx.MergeCells.Cells); i++ { + areaData := xlsx.MergeCells.Cells[i] + coordinates, _ := f.areaRefToCoordinates(areaData.Ref) + x1, y1, x2, y2 := coordinates[0], coordinates[1], coordinates[2], coordinates[3] + if y1 == y2 && y1 == row { + from, _ := CoordinatesToCellName(x1, row2) + to, _ := CoordinatesToCellName(x2, row2) + if err := f.MergeCell(sheet, from, to); err != nil { + return err + } + i++ + } + } return nil } diff --git a/rows_test.go b/rows_test.go index 9377d5e..a5ee428 100644 --- a/rows_test.go +++ b/rows_test.go @@ -693,6 +693,55 @@ func TestDuplicateRowInsertBeforeWithLargeOffset(t *testing.T) { }) } +func TestDuplicateRowInsertBeforeWithMergeCells(t *testing.T) { + const sheet = "Sheet1" + outFile := filepath.Join("test", "TestDuplicateRow.%s.xlsx") + + cells := map[string]string{ + "A1": "A1 Value", + "A2": "A2 Value", + "A3": "A3 Value", + "B1": "B1 Value", + "B2": "B2 Value", + "B3": "B3 Value", + } + + newFileWithDefaults := func() *File { + f := NewFile() + for cell, val := range cells { + assert.NoError(t, f.SetCellStr(sheet, cell, val)) + } + assert.NoError(t, f.MergeCell(sheet, "B2", "C2")) + assert.NoError(t, f.MergeCell(sheet, "C6", "C8")) + return f + } + + t.Run("InsertBeforeWithLargeOffset", func(t *testing.T) { + xlsx := newFileWithDefaults() + + assert.NoError(t, xlsx.DuplicateRowTo(sheet, 2, 1)) + assert.NoError(t, xlsx.DuplicateRowTo(sheet, 1, 8)) + + if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.InsertBeforeWithMergeCells"))) { + t.FailNow() + } + + expect := []MergeCell{ + {"B3:C3", "B2 Value"}, + {"C7:C10", ""}, + {"B1:C1", "B2 Value"}, + } + + mergeCells, err := xlsx.GetMergeCells(sheet) + assert.NoError(t, err) + for idx, val := range expect { + if !assert.Equal(t, val, mergeCells[idx]) { + t.FailNow() + } + } + }) +} + func TestDuplicateRowInvalidRownum(t *testing.T) { const sheet = "Sheet1" outFile := filepath.Join("test", "TestDuplicateRowInvalidRownum.%s.xlsx") @@ -753,6 +802,21 @@ func TestDuplicateRowInvalidRownum(t *testing.T) { } } +func TestDuplicateRowTo(t *testing.T) { + f := File{} + assert.EqualError(t, f.DuplicateRowTo("SheetN", 1, 2), "sheet SheetN is not exist") +} + +func TestDuplicateMergeCells(t *testing.T) { + f := File{} + xlsx := &xlsxWorksheet{MergeCells: &xlsxMergeCells{ + Cells: []*xlsxMergeCell{&xlsxMergeCell{Ref: "A1:-"}}, + }} + assert.EqualError(t, f.duplicateMergeCells("Sheet1", xlsx, 0, 0), `cannot convert cell "-" to coordinates: invalid cell name "-"`) + xlsx.MergeCells.Cells[0].Ref = "A1:B1" + assert.EqualError(t, f.duplicateMergeCells("SheetN", xlsx, 1, 2), "sheet SheetN is not exist") +} + func TestGetValueFrom(t *testing.T) { c := &xlsxC{T: "inlineStr"} f := NewFile() -- cgit v1.2.1 From 821a5d86725eb80b3f9e806d91eca5859497c2fa Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 26 Feb 2020 18:53:50 +0800 Subject: AddPivotTable API changed: new structure PivotTableField to hold pivot table fields for better scalability --- pivotTable.go | 119 ++++++++++++++++++++++++++++++++++--------------- pivotTable_test.go | 127 +++++++++++++++++++++++++---------------------------- rows_test.go | 2 +- 3 files changed, 144 insertions(+), 104 deletions(-) diff --git a/pivotTable.go b/pivotTable.go index 696dfe7..b7dc859 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -20,19 +20,15 @@ import ( // PivotTableOption directly maps the format settings of the pivot table. type PivotTableOption struct { DataRange string - DataSubtotal string - DataFieldName string PivotTableRange string - Rows []string - Columns []string - Data []string - Page []string + Page []PivotTableField + Rows []PivotTableField + Columns []PivotTableField + Data []PivotTableField } -// AddPivotTable provides the method to add pivot table by given pivot table -// options. -// -// DataSubtotal specifies the aggregation function that applies to this data +// PivotTableField directly maps the field settings of the pivot table. +// Subtotal specifies the aggregation function that applies to this data // field. The default value is sum. The possible values for this attribute // are: // @@ -48,8 +44,16 @@ type PivotTableOption struct { // Var // Varp // -// DataFieldName specifies the name of the data field. Maximum 255 characters +// Name specifies the name of the data field. Maximum 255 characters // are allowed in data field name, excess characters will be truncated. +type PivotTableField struct { + Data string + Name string + Subtotal string +} + +// AddPivotTable provides the method to add pivot table by given pivot table +// options. // // For example, create a pivot table on the Sheet1!$G$2:$M$34 area with the // region Sheet1!$A$1:$E$31 as the data source, summarize by sum for sales: @@ -81,11 +85,9 @@ type PivotTableOption struct { // if err := f.AddPivotTable(&excelize.PivotTableOption{ // DataRange: "Sheet1!$A$1:$E$31", // PivotTableRange: "Sheet1!$G$2:$M$34", -// Rows: []string{"Month", "Year"}, -// Columns: []string{"Type"}, -// Data: []string{"Sales"}, -// DataSubtotal: "Sum", -// DataFieldName: "Summarize as Sum", +// Rows: []excelize.PivotTableField{{Data: "Month"}, {Data: "Year"}}, +// Columns: []excelize.PivotTableField{{Data: "Type"}}, +// Data: []excelize.PivotTableField{{Data: "Sales", Name: "Summarize", Subtotal: "Sum"}}, // }); err != nil { // fmt.Println(err) // } @@ -186,6 +188,8 @@ func (f *File) adjustRange(rangeStr string) (string, []int, error) { return rng[0], []int{x1, y1, x2, y2}, nil } +// getPivotFieldsOrder provides a function to get order list of pivot table +// fields. func (f *File) getPivotFieldsOrder(dataRange string) ([]string, error) { order := []string{} // data range has been checked @@ -321,15 +325,13 @@ func (f *File) addPivotTable(cacheID, pivotTableID int, pivotTableXML string, op if err != nil { return err } - dataFieldName := opt.DataFieldName - if len(dataFieldName) > 255 { - dataFieldName = dataFieldName[0:255] - } - for _, dataField := range dataFieldsIndex { + dataFieldsSubtotals := f.getPivotTableFieldsSubtotal(opt.Data) + dataFieldsName := f.getPivotTableFieldsName(opt.Data) + for idx, dataField := range dataFieldsIndex { pt.DataFields.DataField = append(pt.DataFields.DataField, &xlsxDataField{ - Name: dataFieldName, + Name: dataFieldsName[idx], Fld: dataField, - Subtotal: f.getFieldsSubtotal(opt), + Subtotal: dataFieldsSubtotals[idx], }) } @@ -352,6 +354,18 @@ func inStrSlice(a []string, x string) int { return -1 } +// inPivotTableField provides a method to check if an element is present in +// pivot table fields list, and return the index of its location, otherwise +// return -1. +func inPivotTableField(a []PivotTableField, x string) int { + for idx, n := range a { + if x == n.Data { + return idx + } + } + return -1 +} + // addPivotColFields create pivot column fields by given pivot table // definition and option. func (f *File) addPivotColFields(pt *xlsxPivotTableDefinition, opt *PivotTableOption) error { @@ -385,9 +399,10 @@ func (f *File) addPivotFields(pt *xlsxPivotTableDefinition, opt *PivotTableOptio return err } for _, name := range order { - if inStrSlice(opt.Rows, name) != -1 { + if inPivotTableField(opt.Rows, name) != -1 { pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, &xlsxPivotField{ Axis: "axisRow", + Name: f.getPivotTableFieldName(name, opt.Rows), Items: &xlsxItems{ Count: 1, Item: []*xlsxItem{ @@ -397,9 +412,10 @@ func (f *File) addPivotFields(pt *xlsxPivotTableDefinition, opt *PivotTableOptio }) continue } - if inStrSlice(opt.Columns, name) != -1 { + if inPivotTableField(opt.Columns, name) != -1 { pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, &xlsxPivotField{ Axis: "axisCol", + Name: f.getPivotTableFieldName(name, opt.Columns), Items: &xlsxItems{ Count: 1, Item: []*xlsxItem{ @@ -409,7 +425,7 @@ func (f *File) addPivotFields(pt *xlsxPivotTableDefinition, opt *PivotTableOptio }) continue } - if inStrSlice(opt.Data, name) != -1 { + if inPivotTableField(opt.Data, name) != -1 { pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, &xlsxPivotField{ DataField: true, }) @@ -446,30 +462,61 @@ func (f *File) countPivotCache() int { // getPivotFieldsIndex convert the column of the first row in the data region // to a sequential index by given fields and pivot option. -func (f *File) getPivotFieldsIndex(fields []string, opt *PivotTableOption) ([]int, error) { +func (f *File) getPivotFieldsIndex(fields []PivotTableField, opt *PivotTableOption) ([]int, error) { pivotFieldsIndex := []int{} orders, err := f.getPivotFieldsOrder(opt.DataRange) if err != nil { return pivotFieldsIndex, err } for _, field := range fields { - if pos := inStrSlice(orders, field); pos != -1 { + if pos := inStrSlice(orders, field.Data); pos != -1 { pivotFieldsIndex = append(pivotFieldsIndex, pos) } } return pivotFieldsIndex, nil } -// getFieldsSubtotal prepare data subtotal by given fields and pivot option. -func (f *File) getFieldsSubtotal(opt *PivotTableOption) (subtotal string) { - subtotal = "sum" - for _, enum := range []string{"average", "count", "countNums", "max", "min", "product", "stdDev", "stdDevp", "sum", "var", "varp"} { - if strings.ToLower(enum) == strings.ToLower(opt.DataSubtotal) { - subtotal = enum - return +// getPivotTableFieldsSubtotal prepare fields subtotal by given pivot table fields. +func (f *File) getPivotTableFieldsSubtotal(fields []PivotTableField) []string { + field := make([]string, len(fields)) + enums := []string{"average", "count", "countNums", "max", "min", "product", "stdDev", "stdDevp", "sum", "var", "varp"} + inEnums := func(enums []string, val string) string { + for _, enum := range enums { + if strings.ToLower(enum) == strings.ToLower(val) { + return enum + } + } + return "sum" + } + for idx, fld := range fields { + field[idx] = inEnums(enums, fld.Subtotal) + } + return field +} + +// getPivotTableFieldsName prepare fields name list by given pivot table +// fields. +func (f *File) getPivotTableFieldsName(fields []PivotTableField) []string { + field := make([]string, len(fields)) + for idx, fld := range fields { + if len(fld.Name) > 255 { + field[idx] = fld.Name[0:255] + continue + } + field[idx] = fld.Name + } + return field +} + +// getPivotTableFieldName prepare field name by given pivot table fields. +func (f *File) getPivotTableFieldName(name string, fields []PivotTableField) string { + fieldsName := f.getPivotTableFieldsName(fields) + for idx, field := range fields { + if field.Data == name { + return fieldsName[idx] } } - return + return "" } // addWorkbookPivotCache add the association ID of the pivot cache in xl/workbook.xml. diff --git a/pivotTable_test.go b/pivotTable_test.go index e40dbd6..4379538 100644 --- a/pivotTable_test.go +++ b/pivotTable_test.go @@ -28,67 +28,53 @@ func TestAddPivotTable(t *testing.T) { assert.NoError(t, f.AddPivotTable(&PivotTableOption{ DataRange: "Sheet1!$A$1:$E$31", PivotTableRange: "Sheet1!$G$2:$M$34", - Rows: []string{"Month", "Year"}, - Columns: []string{"Type"}, - Data: []string{"Sales"}, - DataSubtotal: "Sum", - DataFieldName: "Summarize by Sum", + Rows: []PivotTableField{{Data: "Month"}, {Data: "Year"}}, + Columns: []PivotTableField{{Data: "Type"}}, + Data: []PivotTableField{{Data: "Sales", Subtotal: "Sum", Name: "Summarize by Sum"}}, })) // Use different order of coordinate tests assert.NoError(t, f.AddPivotTable(&PivotTableOption{ DataRange: "Sheet1!$A$1:$E$31", PivotTableRange: "Sheet1!$U$34:$O$2", - Rows: []string{"Month", "Year"}, - Columns: []string{"Type"}, - Data: []string{"Sales"}, - DataSubtotal: "Average", - DataFieldName: "Summarize by Average", + Rows: []PivotTableField{{Data: "Month"}, {Data: "Year"}}, + Columns: []PivotTableField{{Data: "Type"}}, + Data: []PivotTableField{{Data: "Sales", Subtotal: "Average", Name: "Summarize by Average"}}, })) assert.NoError(t, f.AddPivotTable(&PivotTableOption{ DataRange: "Sheet1!$A$1:$E$31", PivotTableRange: "Sheet1!$W$2:$AC$34", - Rows: []string{"Month", "Year"}, - Columns: []string{"Region"}, - Data: []string{"Sales"}, - DataSubtotal: "Count", - DataFieldName: "Summarize by Count", + Rows: []PivotTableField{{Data: "Month"}, {Data: "Year"}}, + Columns: []PivotTableField{{Data: "Region"}}, + Data: []PivotTableField{{Data: "Sales", Subtotal: "Count", Name: "Summarize by Count"}}, })) assert.NoError(t, f.AddPivotTable(&PivotTableOption{ DataRange: "Sheet1!$A$1:$E$31", PivotTableRange: "Sheet1!$G$37:$W$50", - Rows: []string{"Month"}, - Columns: []string{"Region", "Year"}, - Data: []string{"Sales"}, - DataSubtotal: "CountNums", - DataFieldName: "Summarize by CountNums", + Rows: []PivotTableField{{Data: "Month"}}, + Columns: []PivotTableField{{Data: "Region"}, {Data: "Year"}}, + Data: []PivotTableField{{Data: "Sales", Subtotal: "CountNums", Name: "Summarize by CountNums"}}, })) assert.NoError(t, f.AddPivotTable(&PivotTableOption{ DataRange: "Sheet1!$A$1:$E$31", PivotTableRange: "Sheet1!$AE$2:$AG$33", - Rows: []string{"Month", "Year"}, - Data: []string{"Sales"}, - DataSubtotal: "Max", - DataFieldName: "Summarize by Max", + Rows: []PivotTableField{{Data: "Month"}, {Data: "Year"}}, + Data: []PivotTableField{{Data: "Sales", Subtotal: "Max", Name: "Summarize by Max"}}, })) f.NewSheet("Sheet2") assert.NoError(t, f.AddPivotTable(&PivotTableOption{ DataRange: "Sheet1!$A$1:$E$31", PivotTableRange: "Sheet2!$A$1:$AR$15", - Rows: []string{"Month"}, - Columns: []string{"Region", "Type", "Year"}, - Data: []string{"Sales"}, - DataSubtotal: "Min", - DataFieldName: "Summarize by Min", + Rows: []PivotTableField{{Data: "Month"}}, + Columns: []PivotTableField{{Data: "Region"}, {Data: "Type"}, {Data: "Year"}}, + Data: []PivotTableField{{Data: "Sales", Subtotal: "Min", Name: "Summarize by Min"}}, })) assert.NoError(t, f.AddPivotTable(&PivotTableOption{ DataRange: "Sheet1!$A$1:$E$31", PivotTableRange: "Sheet2!$A$18:$AR$54", - Rows: []string{"Month", "Type"}, - Columns: []string{"Region", "Year"}, - Data: []string{"Sales"}, - DataSubtotal: "Product", - DataFieldName: "Summarize by Product", + Rows: []PivotTableField{{Data: "Month"}, {Data: "Type"}}, + Columns: []PivotTableField{{Data: "Region"}, {Data: "Year"}}, + Data: []PivotTableField{{Data: "Sales", Subtotal: "Product", Name: "Summarize by Product"}}, })) // Test empty pivot table options @@ -97,68 +83,66 @@ func TestAddPivotTable(t *testing.T) { assert.EqualError(t, f.AddPivotTable(&PivotTableOption{ DataRange: "Sheet1!$A$1:$A$1", PivotTableRange: "Sheet1!$U$34:$O$2", - Rows: []string{"Month", "Year"}, - Columns: []string{"Type"}, - Data: []string{"Sales"}, + Rows: []PivotTableField{{Data: "Month"}, {Data: "Year"}}, + Columns: []PivotTableField{{Data: "Type"}}, + Data: []PivotTableField{{Data: "Sales"}}, }), `parameter 'DataRange' parsing error: parameter is invalid`) // Test the data range of the worksheet that is not declared assert.EqualError(t, f.AddPivotTable(&PivotTableOption{ DataRange: "$A$1:$E$31", PivotTableRange: "Sheet1!$U$34:$O$2", - Rows: []string{"Month", "Year"}, - Columns: []string{"Type"}, - Data: []string{"Sales"}, + Rows: []PivotTableField{{Data: "Month"}, {Data: "Year"}}, + Columns: []PivotTableField{{Data: "Type"}}, + Data: []PivotTableField{{Data: "Sales"}}, }), `parameter 'DataRange' parsing error: parameter is invalid`) // Test the worksheet declared in the data range does not exist assert.EqualError(t, f.AddPivotTable(&PivotTableOption{ DataRange: "SheetN!$A$1:$E$31", PivotTableRange: "Sheet1!$U$34:$O$2", - Rows: []string{"Month", "Year"}, - Columns: []string{"Type"}, - Data: []string{"Sales"}, + Rows: []PivotTableField{{Data: "Month"}, {Data: "Year"}}, + Columns: []PivotTableField{{Data: "Type"}}, + Data: []PivotTableField{{Data: "Sales"}}, }), "sheet SheetN is not exist") // Test the pivot table range of the worksheet that is not declared assert.EqualError(t, f.AddPivotTable(&PivotTableOption{ DataRange: "Sheet1!$A$1:$E$31", PivotTableRange: "$U$34:$O$2", - Rows: []string{"Month", "Year"}, - Columns: []string{"Type"}, - Data: []string{"Sales"}, + Rows: []PivotTableField{{Data: "Month"}, {Data: "Year"}}, + Columns: []PivotTableField{{Data: "Type"}}, + Data: []PivotTableField{{Data: "Sales"}}, }), `parameter 'PivotTableRange' parsing error: parameter is invalid`) // Test the worksheet declared in the pivot table range does not exist assert.EqualError(t, f.AddPivotTable(&PivotTableOption{ DataRange: "Sheet1!$A$1:$E$31", PivotTableRange: "SheetN!$U$34:$O$2", - Rows: []string{"Month", "Year"}, - Columns: []string{"Type"}, - Data: []string{"Sales"}, + Rows: []PivotTableField{{Data: "Month"}, {Data: "Year"}}, + Columns: []PivotTableField{{Data: "Type"}}, + Data: []PivotTableField{{Data: "Sales"}}, }), "sheet SheetN is not exist") // Test not exists worksheet in data range assert.EqualError(t, f.AddPivotTable(&PivotTableOption{ DataRange: "SheetN!$A$1:$E$31", PivotTableRange: "Sheet1!$U$34:$O$2", - Rows: []string{"Month", "Year"}, - Columns: []string{"Type"}, - Data: []string{"Sales"}, + Rows: []PivotTableField{{Data: "Month"}, {Data: "Year"}}, + Columns: []PivotTableField{{Data: "Type"}}, + Data: []PivotTableField{{Data: "Sales"}}, }), "sheet SheetN is not exist") // Test invalid row number in data range assert.EqualError(t, f.AddPivotTable(&PivotTableOption{ DataRange: "Sheet1!$A$0:$E$31", PivotTableRange: "Sheet1!$U$34:$O$2", - Rows: []string{"Month", "Year"}, - Columns: []string{"Type"}, - Data: []string{"Sales"}, + Rows: []PivotTableField{{Data: "Month"}, {Data: "Year"}}, + Columns: []PivotTableField{{Data: "Type"}}, + Data: []PivotTableField{{Data: "Sales"}}, }), `parameter 'DataRange' parsing error: cannot convert cell "A0" to coordinates: invalid cell name "A0"`) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddPivotTable1.xlsx"))) // Test with field names that exceed the length limit and invalid subtotal assert.NoError(t, f.AddPivotTable(&PivotTableOption{ DataRange: "Sheet1!$A$1:$E$31", PivotTableRange: "Sheet1!$G$2:$M$34", - Rows: []string{"Month", "Year"}, - Columns: []string{"Type"}, - Data: []string{"Sales"}, - DataSubtotal: "-", - DataFieldName: strings.Repeat("s", 256), + Rows: []PivotTableField{{Data: "Month"}, {Data: "Year"}}, + Columns: []PivotTableField{{Data: "Type"}}, + Data: []PivotTableField{{Data: "Sales", Subtotal: "-", Name: strings.Repeat("s", 256)}}, })) // Test adjust range with invalid range @@ -173,9 +157,9 @@ func TestAddPivotTable(t *testing.T) { assert.EqualError(t, f.addPivotCache(0, "", &PivotTableOption{ DataRange: "$A$1:$E$31", PivotTableRange: "Sheet1!$U$34:$O$2", - Rows: []string{"Month", "Year"}, - Columns: []string{"Type"}, - Data: []string{"Sales"}, + Rows: []PivotTableField{{Data: "Month"}, {Data: "Year"}}, + Columns: []PivotTableField{{Data: "Type"}}, + Data: []PivotTableField{{Data: "Sales"}}, }, nil), "parameter 'DataRange' parsing error: parameter is invalid") // Test add pivot table with empty options assert.EqualError(t, f.addPivotTable(0, 0, "", &PivotTableOption{}), "parameter 'PivotTableRange' parsing error: parameter is required") @@ -185,11 +169,20 @@ func TestAddPivotTable(t *testing.T) { assert.EqualError(t, f.addPivotFields(nil, &PivotTableOption{ DataRange: "$A$1:$E$31", PivotTableRange: "Sheet1!$U$34:$O$2", - Rows: []string{"Month", "Year"}, - Columns: []string{"Type"}, - Data: []string{"Sales"}, + Rows: []PivotTableField{{Data: "Month"}, {Data: "Year"}}, + Columns: []PivotTableField{{Data: "Type"}}, + Data: []PivotTableField{{Data: "Sales"}}, }), `parameter 'DataRange' parsing error: parameter is invalid`) // Test get pivot fields index with empty data range - _, err = f.getPivotFieldsIndex([]string{}, &PivotTableOption{}) + _, err = f.getPivotFieldsIndex([]PivotTableField{}, &PivotTableOption{}) assert.EqualError(t, err, `parameter 'DataRange' parsing error: parameter is required`) } + +func TestInStrSlice(t *testing.T) { + assert.EqualValues(t, -1, inStrSlice([]string{}, "")) +} + +func TestGetPivotTableFieldName(t *testing.T) { + f := NewFile() + f.getPivotTableFieldName("-", []PivotTableField{}) +} diff --git a/rows_test.go b/rows_test.go index a5ee428..a53b0a9 100644 --- a/rows_test.go +++ b/rows_test.go @@ -810,7 +810,7 @@ func TestDuplicateRowTo(t *testing.T) { func TestDuplicateMergeCells(t *testing.T) { f := File{} xlsx := &xlsxWorksheet{MergeCells: &xlsxMergeCells{ - Cells: []*xlsxMergeCell{&xlsxMergeCell{Ref: "A1:-"}}, + Cells: []*xlsxMergeCell{{Ref: "A1:-"}}, }} assert.EqualError(t, f.duplicateMergeCells("Sheet1", xlsx, 0, 0), `cannot convert cell "-" to coordinates: invalid cell name "-"`) xlsx.MergeCells.Cells[0].Ref = "A1:B1" -- cgit v1.2.1 From 386a42dfa25f4ce5d5daf95e87ab65c528dbdd38 Mon Sep 17 00:00:00 2001 From: xxb-at-julichina <57735034+xxb-at-julichina@users.noreply.github.com> Date: Fri, 28 Feb 2020 15:53:04 +0800 Subject: Update rows.go --- rows.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rows.go b/rows.go index 0684b18..e00a627 100644 --- a/rows.go +++ b/rows.go @@ -238,7 +238,8 @@ func (f *File) SetRowHeight(sheet string, row int, height float64) error { // name and row index. func (f *File) getRowHeight(sheet string, row int) int { xlsx, _ := f.workSheetReader(sheet) - for _, v := range xlsx.SheetData.Row { + for i := range xlsx.SheetData.Row { + v := &xlsx.SheetData.Row[i] if v.R == row+1 && v.Ht != 0 { return int(convertRowHeightToPixels(v.Ht)) } -- cgit v1.2.1 From 1d87da57ecf5e13203b6441dd97160885981545e Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 1 Mar 2020 00:34:41 +0800 Subject: Resolve #492, init support for insert and remove page break --- .travis.yml | 1 + sheet.go | 109 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ sheet_test.go | 37 +++++++++++++++++++ xmlWorksheet.go | 6 ++-- 4 files changed, 150 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1cb1d49..d94d5d8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,6 +8,7 @@ go: - 1.11.x - 1.12.x - 1.13.x + - 1.14.x os: - linux diff --git a/sheet.go b/sheet.go index 48671c0..08b0e96 100644 --- a/sheet.go +++ b/sheet.go @@ -1437,6 +1437,115 @@ func (f *File) UngroupSheets() error { return nil } +// InsertPageBreak create a page break to determine where the printed page +// ends and where begins the next one by given worksheet name and axis, so the +// content before the page break will be printed on one page and after the +// page break on another. +func (f *File) InsertPageBreak(sheet, cell string) (err error) { + var ws *xlsxWorksheet + var row, col int + var rowBrk, colBrk = -1, -1 + if ws, err = f.workSheetReader(sheet); err != nil { + return + } + if col, row, err = CellNameToCoordinates(cell); err != nil { + return + } + col-- + row-- + if col == row && col == 0 { + return + } + if ws.RowBreaks == nil { + ws.RowBreaks = &xlsxBreaks{} + } + if ws.ColBreaks == nil { + ws.ColBreaks = &xlsxBreaks{} + } + + for idx, brk := range ws.RowBreaks.Brk { + if brk.ID == row { + rowBrk = idx + } + } + for idx, brk := range ws.ColBreaks.Brk { + if brk.ID == col { + colBrk = idx + } + } + + if row != 0 && rowBrk == -1 { + ws.RowBreaks.Brk = append(ws.RowBreaks.Brk, &xlsxBrk{ + ID: row, + Max: 16383, + Man: true, + }) + ws.RowBreaks.ManualBreakCount++ + } + if col != 0 && colBrk == -1 { + ws.ColBreaks.Brk = append(ws.ColBreaks.Brk, &xlsxBrk{ + ID: col, + Max: 1048575, + Man: true, + }) + ws.ColBreaks.ManualBreakCount++ + } + ws.RowBreaks.Count = len(ws.RowBreaks.Brk) + ws.ColBreaks.Count = len(ws.ColBreaks.Brk) + return +} + +// RemovePageBreak remove a page break by given worksheet name and axis. +func (f *File) RemovePageBreak(sheet, cell string) (err error) { + var ws *xlsxWorksheet + var row, col int + if ws, err = f.workSheetReader(sheet); err != nil { + return + } + if col, row, err = CellNameToCoordinates(cell); err != nil { + return + } + col-- + row-- + if col == row && col == 0 { + return + } + removeBrk := func(ID int, brks []*xlsxBrk) []*xlsxBrk { + for i, brk := range brks { + if brk.ID == ID { + brks = append(brks[:i], brks[i+1:]...) + } + } + return brks + } + if ws.RowBreaks == nil || ws.ColBreaks == nil { + return + } + rowBrks := len(ws.RowBreaks.Brk) + colBrks := len(ws.ColBreaks.Brk) + if rowBrks > 0 && rowBrks == colBrks { + ws.RowBreaks.Brk = removeBrk(row, ws.RowBreaks.Brk) + ws.ColBreaks.Brk = removeBrk(col, ws.ColBreaks.Brk) + ws.RowBreaks.Count = len(ws.RowBreaks.Brk) + ws.ColBreaks.Count = len(ws.ColBreaks.Brk) + ws.RowBreaks.ManualBreakCount-- + ws.ColBreaks.ManualBreakCount-- + return + } + if rowBrks > 0 && rowBrks > colBrks { + ws.RowBreaks.Brk = removeBrk(row, ws.RowBreaks.Brk) + ws.RowBreaks.Count = len(ws.RowBreaks.Brk) + ws.RowBreaks.ManualBreakCount-- + return + } + if colBrks > 0 && colBrks > rowBrks { + ws.ColBreaks.Brk = removeBrk(col, ws.ColBreaks.Brk) + ws.ColBreaks.Count = len(ws.ColBreaks.Brk) + ws.ColBreaks.ManualBreakCount-- + } + return +} + // relsReader provides a function to get the pointer to the structure // after deserialization of xl/worksheets/_rels/sheet%d.xml.rels. func (f *File) relsReader(path string) *xlsxRelationships { diff --git a/sheet_test.go b/sheet_test.go index 69c8f22..38d86e6 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -264,6 +264,43 @@ func TestUngroupSheets(t *testing.T) { assert.NoError(t, f.UngroupSheets()) } +func TestInsertPageBreak(t *testing.T) { + f := excelize.NewFile() + assert.NoError(t, f.InsertPageBreak("Sheet1", "A1")) + assert.NoError(t, f.InsertPageBreak("Sheet1", "B2")) + assert.NoError(t, f.InsertPageBreak("Sheet1", "C3")) + assert.NoError(t, f.InsertPageBreak("Sheet1", "C3")) + assert.EqualError(t, f.InsertPageBreak("Sheet1", "A"), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + assert.EqualError(t, f.InsertPageBreak("SheetN", "C3"), "sheet SheetN is not exist") + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestInsertPageBreak.xlsx"))) +} + +func TestRemovePageBreak(t *testing.T) { + f := excelize.NewFile() + assert.NoError(t, f.RemovePageBreak("Sheet1", "A2")) + + assert.NoError(t, f.InsertPageBreak("Sheet1", "A2")) + assert.NoError(t, f.InsertPageBreak("Sheet1", "B2")) + assert.NoError(t, f.RemovePageBreak("Sheet1", "A1")) + assert.NoError(t, f.RemovePageBreak("Sheet1", "B2")) + + assert.NoError(t, f.InsertPageBreak("Sheet1", "C3")) + assert.NoError(t, f.RemovePageBreak("Sheet1", "C3")) + + assert.NoError(t, f.InsertPageBreak("Sheet1", "A3")) + assert.NoError(t, f.RemovePageBreak("Sheet1", "B3")) + assert.NoError(t, f.RemovePageBreak("Sheet1", "A3")) + + f.NewSheet("Sheet2") + assert.NoError(t, f.InsertPageBreak("Sheet2", "B2")) + assert.NoError(t, f.InsertPageBreak("Sheet2", "C2")) + assert.NoError(t, f.RemovePageBreak("Sheet2", "B2")) + + assert.EqualError(t, f.RemovePageBreak("Sheet1", "A"), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + assert.EqualError(t, f.RemovePageBreak("SheetN", "C3"), "sheet SheetN is not exist") + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestRemovePageBreak.xlsx"))) +} + func TestGetSheetName(t *testing.T) { f, _ := excelize.OpenFile(filepath.Join("test", "Book1.xlsx")) assert.Equal(t, "Sheet1", f.GetSheetName(1)) diff --git a/xmlWorksheet.go b/xmlWorksheet.go index dda1b78..aa33819 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -357,9 +357,9 @@ type xlsxBrk struct { // xlsxBreaks directly maps a collection of the row or column breaks. type xlsxBreaks struct { - Brk *xlsxBrk `xml:"brk"` - Count int `xml:"count,attr,omitempty"` - ManualBreakCount int `xml:"manualBreakCount,attr,omitempty"` + Brk []*xlsxBrk `xml:"brk"` + Count int `xml:"count,attr,omitempty"` + ManualBreakCount int `xml:"manualBreakCount,attr,omitempty"` } // xlsxCustomSheetView directly maps the customSheetView element. -- cgit v1.2.1 From 1e3c85024d3bbc650c2f6a85fb075804af74720b Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 3 Mar 2020 00:15:03 +0800 Subject: Resolve #571, init remove conditional format support --- README.md | 2 +- README_zh.md | 2 +- styles.go | 16 ++++++++++++++++ styles_test.go | 16 ++++++++++++++++ 4 files changed, 34 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index fa1dda9..821bbd7 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,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 [godoc.org](https://godoc.org/github.com/360EntSecGroup-Skylar/excelize) and [docs reference](https://xuri.me/excelize/). +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/). ## Basic Usage diff --git a/README_zh.md b/README_zh.md index 44ab9b5..18db28f 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 使用文档请访问 [godoc.org](https://godoc.org/github.com/360EntSecGroup-Skylar/excelize) 或查看 [参考文档](https://xuri.me/excelize/)。 +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/)。 ## 快速上手 diff --git a/styles.go b/styles.go index 272d728..caf2732 100644 --- a/styles.go +++ b/styles.go @@ -2676,6 +2676,22 @@ func (f *File) SetConditionalFormat(sheet, area, formatSet string) error { return err } +// UnsetConditionalFormat provides a function to unset the conditional format +// by given worksheet name and range. +func (f *File) UnsetConditionalFormat(sheet, area string) error { + ws, err := f.workSheetReader(sheet) + if err != nil { + return err + } + for i, cf := range ws.ConditionalFormatting { + if cf.SQRef == area { + ws.ConditionalFormatting = append(ws.ConditionalFormatting[:i], ws.ConditionalFormatting[i+1:]...) + return nil + } + } + return nil +} + // drawCondFmtCellIs provides a function to create conditional formatting rule // for cell value (include between, not between, equal, not equal, greater // than and less than) by given priority, criteria type and format settings. diff --git a/styles_test.go b/styles_test.go index a536700..4e9b411 100644 --- a/styles_test.go +++ b/styles_test.go @@ -1,6 +1,8 @@ package excelize import ( + "fmt" + "path/filepath" "testing" "github.com/stretchr/testify/assert" @@ -166,6 +168,20 @@ func TestSetConditionalFormat(t *testing.T) { } } +func TestUnsetConditionalFormat(t *testing.T) { + f := NewFile() + assert.NoError(t, f.SetCellValue("Sheet1", "A1", 7)) + assert.NoError(t, f.UnsetConditionalFormat("Sheet1", "A1:A10")) + format, err := f.NewConditionalStyle(`{"font":{"color":"#9A0511"},"fill":{"type":"pattern","color":["#FEC7CE"],"pattern":1}}`) + assert.NoError(t, err) + assert.NoError(t, f.SetConditionalFormat("Sheet1", "A1:A10", fmt.Sprintf(`[{"type":"cell","criteria":">","format":%d,"value":"6"}]`, format))) + assert.NoError(t, f.UnsetConditionalFormat("Sheet1", "A1:A10")) + // Test unset conditional format on not exists worksheet. + assert.EqualError(t, f.UnsetConditionalFormat("SheetN", "A1:A10"), "sheet SheetN is not exist") + // Save xlsx file by the given path. + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestUnsetConditionalFormat.xlsx"))) +} + func TestNewStyle(t *testing.T) { f := NewFile() styleID, err := f.NewStyle(`{"font":{"bold":true,"italic":true,"family":"Times New Roman","size":36,"color":"#777777"}}`) -- cgit v1.2.1 From 83eedce70de7a1ddeb3a4446f86b13bc6ff0b5ec Mon Sep 17 00:00:00 2001 From: Vaibhav Nayak Date: Tue, 3 Mar 2020 17:01:02 +0530 Subject: Export ExcelDateToTime function to convert excel date to time Signed-off-by: Vaibhav Nayak --- date.go | 8 ++++++++ date_test.go | 32 +++++++++++++++++++++++--------- errors.go | 4 ++++ errors_test.go | 4 ++++ 4 files changed, 39 insertions(+), 9 deletions(-) diff --git a/date.go b/date.go index dad39b5..172c32c 100644 --- a/date.go +++ b/date.go @@ -172,3 +172,11 @@ func timeFromExcelTime(excelTime float64, date1904 bool) time.Time { durationPart := time.Duration(dayNanoSeconds * floatPart) return date.Add(durationDays).Add(durationPart) } + +// ExcelDateToTime converts a float-based excel date representation to a time.Time. +func ExcelDateToTime(excelDate float64, use1904Format bool) (time.Time, error) { + if excelDate < 0 { + return time.Time{}, newInvalidExcelDateError(excelDate) + } + return timeFromExcelTime(excelDate, use1904Format), nil +} diff --git a/date_test.go b/date_test.go index 2885af0..ee01356 100644 --- a/date_test.go +++ b/date_test.go @@ -28,6 +28,14 @@ var trueExpectedDateList = []dateTest{ {401769.00000000000, time.Date(3000, time.January, 1, 0, 0, 0, 0, time.UTC)}, } +var excelTimeInputList = []dateTest{ + {0.0, time.Date(1899, 12, 30, 0, 0, 0, 0, time.UTC)}, + {60.0, time.Date(1900, 2, 28, 0, 0, 0, 0, time.UTC)}, + {61.0, time.Date(1900, 3, 1, 0, 0, 0, 0, time.UTC)}, + {41275.0, time.Date(2013, 1, 1, 0, 0, 0, 0, time.UTC)}, + {401769.0, time.Date(3000, 1, 1, 0, 0, 0, 0, time.UTC)}, +} + func TestTimeToExcelTime(t *testing.T) { for i, test := range trueExpectedDateList { t.Run(fmt.Sprintf("TestData%d", i+1), func(t *testing.T) { @@ -53,15 +61,7 @@ func TestTimeToExcelTime_Timezone(t *testing.T) { } func TestTimeFromExcelTime(t *testing.T) { - trueExpectedInputList := []dateTest{ - {0.0, time.Date(1899, 12, 30, 0, 0, 0, 0, time.UTC)}, - {60.0, time.Date(1900, 2, 28, 0, 0, 0, 0, time.UTC)}, - {61.0, time.Date(1900, 3, 1, 0, 0, 0, 0, time.UTC)}, - {41275.0, time.Date(2013, 1, 1, 0, 0, 0, 0, time.UTC)}, - {401769.0, time.Date(3000, 1, 1, 0, 0, 0, 0, time.UTC)}, - } - - for i, test := range trueExpectedInputList { + for i, test := range excelTimeInputList { t.Run(fmt.Sprintf("TestData%d", i+1), func(t *testing.T) { assert.Equal(t, test.GoValue, timeFromExcelTime(test.ExcelValue, false)) }) @@ -73,3 +73,17 @@ func TestTimeFromExcelTime_1904(t *testing.T) { timeFromExcelTime(61, true) timeFromExcelTime(62, true) } + +func TestExcelDateToTime(t *testing.T) { + // Check normal case + for i, test := range excelTimeInputList { + t.Run(fmt.Sprintf("TestData%d", i+1), func(t *testing.T) { + timeValue, err := ExcelDateToTime(test.ExcelValue, false) + assert.Equal(t, test.GoValue, timeValue) + assert.NoError(t, err) + }) + } + // Check error case + _, err := ExcelDateToTime(-1, false) + assert.EqualError(t, err, "invalid date value -1.000000, negative values are not supported supported") +} diff --git a/errors.go b/errors.go index 4560497..5576ecd 100644 --- a/errors.go +++ b/errors.go @@ -22,3 +22,7 @@ func newInvalidRowNumberError(row int) error { func newInvalidCellNameError(cell string) error { return fmt.Errorf("invalid cell name %q", cell) } + +func newInvalidExcelDateError(dateValue float64) error { + return fmt.Errorf("invalid date value %f, negative values are not supported supported", dateValue) +} diff --git a/errors_test.go b/errors_test.go index 89d241c..207e80a 100644 --- a/errors_test.go +++ b/errors_test.go @@ -19,3 +19,7 @@ func TestNewInvalidCellNameError(t *testing.T) { assert.EqualError(t, newInvalidCellNameError("A"), "invalid cell name \"A\"") assert.EqualError(t, newInvalidCellNameError(""), "invalid cell name \"\"") } + +func TestNewInvalidExcelDateError(t *testing.T) { + assert.EqualError(t, newInvalidExcelDateError(-1), "invalid date value -1.000000, negative values are not supported supported") +} -- cgit v1.2.1 From 2ccb8f62edd5d1ce039e663591964b9066fd2f4e Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 9 Mar 2020 00:08:47 +0800 Subject: Remove calculated properties to make recalculate formulas in some spreadsheet applications, such as Kingsoft WPS --- excelize.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/excelize.go b/excelize.go index e12e769..0962122 100644 --- a/excelize.go +++ b/excelize.go @@ -273,6 +273,9 @@ func replaceStyleRelationshipsNameSpaceBytes(contentMarshal []byte) []byte { // // func (f *File) UpdateLinkedValue() error { + wb := f.workbookReader() + // recalculate formulas + wb.CalcPr = nil for _, name := range f.GetSheetMap() { xlsx, err := f.workSheetReader(name) if err != nil { -- cgit v1.2.1 From 9e2318cefa4ebaa7bf6b1dbc95b30ad7a32366b1 Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 10 Mar 2020 00:04:23 +0800 Subject: Resolve #470, export Style structs to allow create the style for cells by given JSON or structure --- excelize.go | 2 +- excelize_test.go | 40 ++++++++++++- shape.go | 2 +- styles.go | 176 +++++++++++++++++++++++++++++-------------------------- styles_test.go | 23 ++++++++ xmlChart.go | 2 +- xmlDrawing.go | 4 +- xmlStyles.go | 84 ++++++++++++++------------ 8 files changed, 207 insertions(+), 126 deletions(-) diff --git a/excelize.go b/excelize.go index 0962122..795120d 100644 --- a/excelize.go +++ b/excelize.go @@ -137,7 +137,7 @@ func (f *File) setDefaultTimeStyle(sheet, axis string, format int) error { return err } if s == 0 { - style, _ := f.NewStyle(`{"number_format": ` + strconv.Itoa(format) + `}`) + style, _ := f.NewStyle(&Style{NumFmt: format}) _ = f.SetCellStyle(sheet, axis, axis, style) } return err diff --git a/excelize_test.go b/excelize_test.go index b78aac8..1ce4fe9 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -580,7 +580,45 @@ func TestSetCellStyleBorder(t *testing.T) { assert.NoError(t, f.SetCellStyle("Sheet1", "M28", "K24", style)) // Test set border and solid style pattern fill for a single cell. - style, err = f.NewStyle(`{"border":[{"type":"left","color":"0000FF","style":8},{"type":"top","color":"00FF00","style":9},{"type":"bottom","color":"FFFF00","style":10},{"type":"right","color":"FF0000","style":11},{"type":"diagonalDown","color":"A020F0","style":12},{"type":"diagonalUp","color":"A020F0","style":13}],"fill":{"type":"pattern","color":["#E0EBF5"],"pattern":1}}`) + style, err = f.NewStyle(&Style{ + Border: []Border{ + { + Type: "left", + Color: "0000FF", + Style: 8, + }, + { + Type: "top", + Color: "00FF00", + Style: 9, + }, + { + Type: "bottom", + Color: "FFFF00", + Style: 10, + }, + { + Type: "right", + Color: "FF0000", + Style: 11, + }, + { + Type: "diagonalDown", + Color: "A020F0", + Style: 12, + }, + { + Type: "diagonalUp", + Color: "A020F0", + Style: 13, + }, + }, + Fill: Fill{ + Type: "pattern", + Color: []string{"#E0EBF5"}, + Pattern: 1, + }, + }) if !assert.NoError(t, err) { t.FailNow() } diff --git a/shape.go b/shape.go index e9bdb42..0455b22 100644 --- a/shape.go +++ b/shape.go @@ -378,7 +378,7 @@ func (f *File) addDrawingShape(sheet, drawingXML, cell string, formatSet *format if len(formatSet.Paragraph) < 1 { formatSet.Paragraph = []formatShapeParagraph{ { - Font: formatFont{ + Font: Font{ Bold: false, Italic: false, Underline: "none", diff --git a/styles.go b/styles.go index caf2732..175a17c 100644 --- a/styles.go +++ b/styles.go @@ -1024,16 +1024,16 @@ func (f *File) styleSheetWriter() { // parseFormatStyleSet provides a function to parse the format settings of the // cells and conditional formats. -func parseFormatStyleSet(style string) (*formatStyle, error) { - format := formatStyle{ +func parseFormatStyleSet(style string) (*Style, error) { + format := Style{ DecimalPlaces: 2, } err := json.Unmarshal([]byte(style), &format) return &format, err } -// NewStyle provides a function to create style for cells by given style -// format. Note that the color field uses RGB color code. +// NewStyle provides a function to create the style for cells by given JSON or +// structure. Note that the color field uses RGB color code. // // The following shows the border styles sorted by excelize index number: // @@ -1888,18 +1888,26 @@ func parseFormatStyleSet(style string) (*formatStyle, error) { // // f := excelize.NewFile() // f.SetCellValue("Sheet1", "A6", 42920.5) -// style, err := f.NewStyle(`{"custom_number_format": "[$-380A]dddd\\,\\ dd\" de \"mmmm\" de \"yyyy;@"}`) +// exp := "[$-380A]dddd\\,\\ dd\" de \"mmmm\" de \"yyyy;@" +// style, err := f.NewStyle(&excelize.Style{CustomNumFmt: &exp}) // err = f.SetCellStyle("Sheet1", "A6", "A6", style) // // Cell Sheet1!A6 in the Excel Application: martes, 04 de Julio de 2017 // -func (f *File) NewStyle(style string) (int, error) { +func (f *File) NewStyle(style interface{}) (int, error) { + var fs *Style + var err error var cellXfsID, fontID, borderID, fillID int - s := f.stylesReader() - fs, err := parseFormatStyleSet(style) - if err != nil { - return cellXfsID, err + switch v := style.(type) { + case string: + fs, err = parseFormatStyleSet(v) + if err != nil { + return cellXfsID, err + } + case *Style: + fs = v } + s := f.stylesReader() numFmtID := setNumFmt(s, fs) if fs.Font != nil { @@ -1978,34 +1986,34 @@ func (f *File) readDefaultFont() *xlsxFont { // setFont provides a function to add font style by given cell format // settings. -func (f *File) setFont(formatStyle *formatStyle) *xlsxFont { +func (f *File) setFont(style *Style) *xlsxFont { fontUnderlineType := map[string]string{"single": "single", "double": "double"} - if formatStyle.Font.Size < 1 { - formatStyle.Font.Size = 11 + if style.Font.Size < 1 { + style.Font.Size = 11 } - if formatStyle.Font.Color == "" { - formatStyle.Font.Color = "#000000" + if style.Font.Color == "" { + style.Font.Color = "#000000" } fnt := xlsxFont{ - Sz: &attrValFloat{Val: float64Ptr(formatStyle.Font.Size)}, - Color: &xlsxColor{RGB: getPaletteColor(formatStyle.Font.Color)}, - Name: &attrValString{Val: stringPtr(formatStyle.Font.Family)}, + Sz: &attrValFloat{Val: float64Ptr(style.Font.Size)}, + Color: &xlsxColor{RGB: getPaletteColor(style.Font.Color)}, + Name: &attrValString{Val: stringPtr(style.Font.Family)}, Family: &attrValInt{Val: intPtr(2)}, } - if formatStyle.Font.Bold { - fnt.B = &formatStyle.Font.Bold + if style.Font.Bold { + fnt.B = &style.Font.Bold } - if formatStyle.Font.Italic { - fnt.I = &formatStyle.Font.Italic + if style.Font.Italic { + fnt.I = &style.Font.Italic } if *fnt.Name.Val == "" { *fnt.Name.Val = f.GetDefaultFont() } - if formatStyle.Font.Strike { + if style.Font.Strike { strike := true fnt.Strike = &strike } - val, ok := fontUnderlineType[formatStyle.Font.Underline] + val, ok := fontUnderlineType[style.Font.Underline] if ok { fnt.U = &attrValString{Val: stringPtr(val)} } @@ -2014,36 +2022,36 @@ func (f *File) setFont(formatStyle *formatStyle) *xlsxFont { // setNumFmt provides a function to check if number format code in the range // of built-in values. -func setNumFmt(style *xlsxStyleSheet, formatStyle *formatStyle) int { +func setNumFmt(styleSheet *xlsxStyleSheet, style *Style) int { dp := "0." numFmtID := 164 // Default custom number format code from 164. - if formatStyle.DecimalPlaces < 0 || formatStyle.DecimalPlaces > 30 { - formatStyle.DecimalPlaces = 2 + if style.DecimalPlaces < 0 || style.DecimalPlaces > 30 { + style.DecimalPlaces = 2 } - for i := 0; i < formatStyle.DecimalPlaces; i++ { + for i := 0; i < style.DecimalPlaces; i++ { dp += "0" } - if formatStyle.CustomNumFmt != nil { - return setCustomNumFmt(style, formatStyle) + if style.CustomNumFmt != nil { + return setCustomNumFmt(styleSheet, style) } - _, ok := builtInNumFmt[formatStyle.NumFmt] + _, ok := builtInNumFmt[style.NumFmt] if !ok { - fc, currency := currencyNumFmt[formatStyle.NumFmt] + fc, currency := currencyNumFmt[style.NumFmt] if !currency { - return setLangNumFmt(style, formatStyle) + return setLangNumFmt(styleSheet, style) } fc = strings.Replace(fc, "0.00", dp, -1) - if formatStyle.NegRed { + if style.NegRed { fc = fc + ";[Red]" + fc } - if style.NumFmts != nil { - numFmtID = style.NumFmts.NumFmt[len(style.NumFmts.NumFmt)-1].NumFmtID + 1 + if styleSheet.NumFmts != nil { + numFmtID = styleSheet.NumFmts.NumFmt[len(styleSheet.NumFmts.NumFmt)-1].NumFmtID + 1 nf := xlsxNumFmt{ FormatCode: fc, NumFmtID: numFmtID, } - style.NumFmts.NumFmt = append(style.NumFmts.NumFmt, &nf) - style.NumFmts.Count++ + styleSheet.NumFmts.NumFmt = append(styleSheet.NumFmts.NumFmt, &nf) + styleSheet.NumFmts.Count++ } else { nf := xlsxNumFmt{ FormatCode: fc, @@ -2053,61 +2061,61 @@ func setNumFmt(style *xlsxStyleSheet, formatStyle *formatStyle) int { NumFmt: []*xlsxNumFmt{&nf}, Count: 1, } - style.NumFmts = &numFmts + styleSheet.NumFmts = &numFmts } return numFmtID } - return formatStyle.NumFmt + return style.NumFmt } // setCustomNumFmt provides a function to set custom number format code. -func setCustomNumFmt(style *xlsxStyleSheet, formatStyle *formatStyle) int { - nf := xlsxNumFmt{FormatCode: *formatStyle.CustomNumFmt} - if style.NumFmts != nil { - nf.NumFmtID = style.NumFmts.NumFmt[len(style.NumFmts.NumFmt)-1].NumFmtID + 1 - style.NumFmts.NumFmt = append(style.NumFmts.NumFmt, &nf) - style.NumFmts.Count++ +func setCustomNumFmt(styleSheet *xlsxStyleSheet, style *Style) int { + nf := xlsxNumFmt{FormatCode: *style.CustomNumFmt} + if styleSheet.NumFmts != nil { + nf.NumFmtID = styleSheet.NumFmts.NumFmt[len(styleSheet.NumFmts.NumFmt)-1].NumFmtID + 1 + styleSheet.NumFmts.NumFmt = append(styleSheet.NumFmts.NumFmt, &nf) + styleSheet.NumFmts.Count++ } else { nf.NumFmtID = 164 numFmts := xlsxNumFmts{ NumFmt: []*xlsxNumFmt{&nf}, Count: 1, } - style.NumFmts = &numFmts + styleSheet.NumFmts = &numFmts } return nf.NumFmtID } // setLangNumFmt provides a function to set number format code with language. -func setLangNumFmt(style *xlsxStyleSheet, formatStyle *formatStyle) int { - numFmts, ok := langNumFmt[formatStyle.Lang] +func setLangNumFmt(styleSheet *xlsxStyleSheet, style *Style) int { + numFmts, ok := langNumFmt[style.Lang] if !ok { return 0 } var fc string - fc, ok = numFmts[formatStyle.NumFmt] + fc, ok = numFmts[style.NumFmt] if !ok { return 0 } nf := xlsxNumFmt{FormatCode: fc} - if style.NumFmts != nil { - nf.NumFmtID = style.NumFmts.NumFmt[len(style.NumFmts.NumFmt)-1].NumFmtID + 1 - style.NumFmts.NumFmt = append(style.NumFmts.NumFmt, &nf) - style.NumFmts.Count++ + if styleSheet.NumFmts != nil { + nf.NumFmtID = styleSheet.NumFmts.NumFmt[len(styleSheet.NumFmts.NumFmt)-1].NumFmtID + 1 + styleSheet.NumFmts.NumFmt = append(styleSheet.NumFmts.NumFmt, &nf) + styleSheet.NumFmts.Count++ } else { - nf.NumFmtID = formatStyle.NumFmt + nf.NumFmtID = style.NumFmt numFmts := xlsxNumFmts{ NumFmt: []*xlsxNumFmt{&nf}, Count: 1, } - style.NumFmts = &numFmts + styleSheet.NumFmts = &numFmts } return nf.NumFmtID } // setFills provides a function to add fill elements in the styles.xml by // given cell format settings. -func setFills(formatStyle *formatStyle, fg bool) *xlsxFill { +func setFills(style *Style, fg bool) *xlsxFill { var patterns = []string{ "none", "solid", @@ -2138,15 +2146,15 @@ func setFills(formatStyle *formatStyle, fg bool) *xlsxFill { } var fill xlsxFill - switch formatStyle.Fill.Type { + switch style.Fill.Type { case "gradient": - if len(formatStyle.Fill.Color) != 2 { + if len(style.Fill.Color) != 2 { break } var gradient xlsxGradientFill - switch formatStyle.Fill.Shading { + switch style.Fill.Shading { case 0, 1, 2, 3: - gradient.Degree = variants[formatStyle.Fill.Shading] + gradient.Degree = variants[style.Fill.Shading] case 4: gradient.Type = "path" case 5: @@ -2159,7 +2167,7 @@ func setFills(formatStyle *formatStyle, fg bool) *xlsxFill { break } var stops []*xlsxGradientFillStop - for index, color := range formatStyle.Fill.Color { + for index, color := range style.Fill.Color { var stop xlsxGradientFillStop stop.Position = float64(index) stop.Color.RGB = getPaletteColor(color) @@ -2168,18 +2176,18 @@ func setFills(formatStyle *formatStyle, fg bool) *xlsxFill { gradient.Stop = stops fill.GradientFill = &gradient case "pattern": - if formatStyle.Fill.Pattern > 18 || formatStyle.Fill.Pattern < 0 { + if style.Fill.Pattern > 18 || style.Fill.Pattern < 0 { break } - if len(formatStyle.Fill.Color) < 1 { + if len(style.Fill.Color) < 1 { break } var pattern xlsxPatternFill - pattern.PatternType = patterns[formatStyle.Fill.Pattern] + pattern.PatternType = patterns[style.Fill.Pattern] if fg { - pattern.FgColor.RGB = getPaletteColor(formatStyle.Fill.Color[0]) + pattern.FgColor.RGB = getPaletteColor(style.Fill.Color[0]) } else { - pattern.BgColor.RGB = getPaletteColor(formatStyle.Fill.Color[0]) + pattern.BgColor.RGB = getPaletteColor(style.Fill.Color[0]) } fill.PatternFill = &pattern default: @@ -2192,36 +2200,36 @@ func setFills(formatStyle *formatStyle, fg bool) *xlsxFill { // text alignment in cells. There are a variety of choices for how text is // aligned both horizontally and vertically, as well as indentation settings, // and so on. -func setAlignment(formatStyle *formatStyle) *xlsxAlignment { +func setAlignment(style *Style) *xlsxAlignment { var alignment xlsxAlignment - if formatStyle.Alignment != nil { - alignment.Horizontal = formatStyle.Alignment.Horizontal - alignment.Indent = formatStyle.Alignment.Indent - alignment.JustifyLastLine = formatStyle.Alignment.JustifyLastLine - alignment.ReadingOrder = formatStyle.Alignment.ReadingOrder - alignment.RelativeIndent = formatStyle.Alignment.RelativeIndent - alignment.ShrinkToFit = formatStyle.Alignment.ShrinkToFit - alignment.TextRotation = formatStyle.Alignment.TextRotation - alignment.Vertical = formatStyle.Alignment.Vertical - alignment.WrapText = formatStyle.Alignment.WrapText + if style.Alignment != nil { + alignment.Horizontal = style.Alignment.Horizontal + alignment.Indent = style.Alignment.Indent + alignment.JustifyLastLine = style.Alignment.JustifyLastLine + alignment.ReadingOrder = style.Alignment.ReadingOrder + alignment.RelativeIndent = style.Alignment.RelativeIndent + alignment.ShrinkToFit = style.Alignment.ShrinkToFit + alignment.TextRotation = style.Alignment.TextRotation + alignment.Vertical = style.Alignment.Vertical + alignment.WrapText = style.Alignment.WrapText } return &alignment } // setProtection provides a function to set protection properties associated // with the cell. -func setProtection(formatStyle *formatStyle) *xlsxProtection { +func setProtection(style *Style) *xlsxProtection { var protection xlsxProtection - if formatStyle.Protection != nil { - protection.Hidden = formatStyle.Protection.Hidden - protection.Locked = formatStyle.Protection.Locked + if style.Protection != nil { + protection.Hidden = style.Protection.Hidden + protection.Locked = style.Protection.Locked } return &protection } // setBorders provides a function to add border elements in the styles.xml by // given borders format settings. -func setBorders(formatStyle *formatStyle) *xlsxBorder { +func setBorders(style *Style) *xlsxBorder { var styles = []string{ "none", "thin", @@ -2240,7 +2248,7 @@ func setBorders(formatStyle *formatStyle) *xlsxBorder { } var border xlsxBorder - for _, v := range formatStyle.Border { + for _, v := range style.Border { if 0 <= v.Style && v.Style < 14 { var color xlsxColor color.RGB = getPaletteColor(v.Color) diff --git a/styles_test.go b/styles_test.go index 4e9b411..5a9a771 100644 --- a/styles_test.go +++ b/styles_test.go @@ -191,6 +191,8 @@ func TestNewStyle(t *testing.T) { font := styles.Fonts.Font[fontID] assert.Contains(t, *font.Name.Val, "Times New Roman", "Stored font should contain font name") assert.Equal(t, 2, styles.CellXfs.Count, "Should have 2 styles") + _, err = f.NewStyle(&Style{}) + assert.NoError(t, err) } func TestGetDefaultFont(t *testing.T) { @@ -207,3 +209,24 @@ func TestSetDefaultFont(t *testing.T) { assert.Equal(t, s, "Ariel", "Default font should change to Ariel") assert.Equal(t, *styles.CellStyles.CellStyle[0].CustomBuiltIn, true) } + +func TestStylesReader(t *testing.T) { + f := NewFile() + // Test read styles with unsupport charset. + f.Styles = nil + f.XLSX["xl/styles.xml"] = MacintoshCyrillicCharset + assert.EqualValues(t, new(xlsxStyleSheet), f.stylesReader()) +} + +func TestThemeReader(t *testing.T) { + f := NewFile() + // Test read theme with unsupport charset. + f.XLSX["xl/theme/theme1.xml"] = MacintoshCyrillicCharset + assert.EqualValues(t, new(xlsxTheme), f.themeReader()) +} + +func TestSetCellStyle(t *testing.T) { + f := NewFile() + // Test set cell style on not exists worksheet. + assert.EqualError(t, f.SetCellStyle("SheetN", "A1", "A2", 1), "sheet SheetN is not exist") +} diff --git a/xmlChart.go b/xmlChart.go index 8d24552..03b47a1 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -604,7 +604,7 @@ type formatChart struct { type formatChartLegend struct { None bool `json:"none"` DeleteSeries []int `json:"delete_series"` - Font formatFont `json:"font"` + Font Font `json:"font"` Layout formatLayout `json:"layout"` Position string `json:"position"` ShowLegendEntry bool `json:"show_legend_entry"` diff --git a/xmlDrawing.go b/xmlDrawing.go index 5bb5977..2bad16a 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -417,8 +417,8 @@ type formatShape struct { // formatShapeParagraph directly maps the format settings of the paragraph in // the shape. type formatShapeParagraph struct { - Font formatFont `json:"font"` - Text string `json:"text"` + Font Font `json:"font"` + Text string `json:"text"` } // formatShapeColor directly maps the color settings of the shape. diff --git a/xmlStyles.go b/xmlStyles.go index 0313008..d6aa4f9 100644 --- a/xmlStyles.go +++ b/xmlStyles.go @@ -313,8 +313,28 @@ type xlsxStyleColors struct { Color string `xml:",innerxml"` } -// formatFont directly maps the styles settings of the fonts. -type formatFont struct { +// Alignment directly maps the alignment settings of the cells. +type Alignment struct { + Horizontal string `json:"horizontal"` + Indent int `json:"indent"` + JustifyLastLine bool `json:"justify_last_line"` + ReadingOrder uint64 `json:"reading_order"` + RelativeIndent int `json:"relative_indent"` + ShrinkToFit bool `json:"shrink_to_fit"` + TextRotation int `json:"text_rotation"` + Vertical string `json:"vertical"` + WrapText bool `json:"wrap_text"` +} + +// Border directly maps the border settings of the cells. +type Border struct { + Type string `json:"type"` + Color string `json:"color"` + Style int `json:"style"` +} + +// Font directly maps the font settings of the fonts. +type Font struct { Bold bool `json:"bold"` Italic bool `json:"italic"` Underline string `json:"underline"` @@ -324,38 +344,30 @@ type formatFont struct { Color string `json:"color"` } -// formatStyle directly maps the styles settings of the cells. -type formatStyle struct { - Border []struct { - Type string `json:"type"` - Color string `json:"color"` - Style int `json:"style"` - } `json:"border"` - Fill struct { - Type string `json:"type"` - Pattern int `json:"pattern"` - Color []string `json:"color"` - Shading int `json:"shading"` - } `json:"fill"` - Font *formatFont `json:"font"` - Alignment *struct { - Horizontal string `json:"horizontal"` - Indent int `json:"indent"` - JustifyLastLine bool `json:"justify_last_line"` - ReadingOrder uint64 `json:"reading_order"` - RelativeIndent int `json:"relative_indent"` - ShrinkToFit bool `json:"shrink_to_fit"` - TextRotation int `json:"text_rotation"` - Vertical string `json:"vertical"` - WrapText bool `json:"wrap_text"` - } `json:"alignment"` - Protection *struct { - Hidden bool `json:"hidden"` - Locked bool `json:"locked"` - } `json:"protection"` - NumFmt int `json:"number_format"` - DecimalPlaces int `json:"decimal_places"` - CustomNumFmt *string `json:"custom_number_format"` - Lang string `json:"lang"` - NegRed bool `json:"negred"` +// Fill directly maps the fill settings of the cells. +type Fill struct { + Type string `json:"type"` + Pattern int `json:"pattern"` + Color []string `json:"color"` + Shading int `json:"shading"` +} + +// Protection directly maps the protection settings of the cells. +type Protection struct { + Hidden bool `json:"hidden"` + Locked bool `json:"locked"` +} + +// Style directly maps the style settings of the cells. +type Style struct { + Border []Border `json:"border"` + Fill Fill `json:"fill"` + Font *Font `json:"font"` + Alignment *Alignment `json:"alignment"` + Protection *Protection `json:"protection"` + NumFmt int `json:"number_format"` + DecimalPlaces int `json:"decimal_places"` + CustomNumFmt *string `json:"custom_number_format"` + Lang string `json:"lang"` + NegRed bool `json:"negred"` } -- cgit v1.2.1 From 6ab5b991e47e7fa9e9370da93404adaf04cba34a Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 13 Mar 2020 00:48:16 +0800 Subject: Resolve #348, support delete Data Validation --- datavalidation.go | 34 +++++++++++++++++++++++++++++----- datavalidation_test.go | 17 +++++++++++++++++ test/images/chart.png | Bin 190484 -> 137555 bytes 3 files changed, 46 insertions(+), 5 deletions(-) diff --git a/datavalidation.go b/datavalidation.go index 8b95b40..1aeb1dc 100644 --- a/datavalidation.go +++ b/datavalidation.go @@ -228,14 +228,38 @@ func convDataValidationOperatior(o DataValidationOperator) string { // err = f.AddDataValidation("Sheet1", dvRange) // func (f *File) AddDataValidation(sheet string, dv *DataValidation) error { - xlsx, err := f.workSheetReader(sheet) + ws, err := f.workSheetReader(sheet) if err != nil { return err } - if nil == xlsx.DataValidations { - xlsx.DataValidations = new(xlsxDataValidations) + if nil == ws.DataValidations { + ws.DataValidations = new(xlsxDataValidations) } - xlsx.DataValidations.DataValidation = append(xlsx.DataValidations.DataValidation, dv) - xlsx.DataValidations.Count = len(xlsx.DataValidations.DataValidation) + ws.DataValidations.DataValidation = append(ws.DataValidations.DataValidation, dv) + ws.DataValidations.Count = len(ws.DataValidations.DataValidation) return err } + +// DeleteDataValidation delete data validation by given worksheet name and +// reference sequence. +func (f *File) DeleteDataValidation(sheet, sqref string) error { + ws, err := f.workSheetReader(sheet) + if err != nil { + return err + } + if ws.DataValidations == nil { + return nil + } + dv := ws.DataValidations + for i := 0; i < len(dv.DataValidation); i++ { + if dv.DataValidation[i].Sqref == sqref { + dv.DataValidation = append(dv.DataValidation[:i], dv.DataValidation[i+1:]...) + i-- + } + } + dv.Count = len(dv.DataValidation) + if dv.Count == 0 { + ws.DataValidations = nil + } + return nil +} diff --git a/datavalidation_test.go b/datavalidation_test.go index c245df3..d70b874 100644 --- a/datavalidation_test.go +++ b/datavalidation_test.go @@ -85,3 +85,20 @@ func TestDataValidationError(t *testing.T) { f = NewFile() assert.EqualError(t, f.AddDataValidation("SheetN", nil), "sheet SheetN is not exist") } + +func TestDeleteDataValidation(t *testing.T) { + f := NewFile() + assert.NoError(t, f.DeleteDataValidation("Sheet1", "A1:B2")) + + dvRange := NewDataValidation(true) + dvRange.Sqref = "A1:B2" + assert.NoError(t, dvRange.SetRange(10, 20, DataValidationTypeWhole, DataValidationOperatorBetween)) + dvRange.SetInput("input title", "input body") + assert.NoError(t, f.AddDataValidation("Sheet1", dvRange)) + + assert.NoError(t, f.DeleteDataValidation("Sheet1", "A1:B2")) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestDeleteDataValidation.xlsx"))) + + // Test delete data validation on no exists worksheet. + assert.EqualError(t, f.DeleteDataValidation("SheetN", "A1:B2"), "sheet SheetN is not exist") +} diff --git a/test/images/chart.png b/test/images/chart.png index 9fcd28a..dc30051 100644 Binary files a/test/images/chart.png and b/test/images/chart.png differ -- cgit v1.2.1 From cea3d806ecbec5027a8f0d7f10b700707131a7be Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 16 Mar 2020 00:13:01 +0800 Subject: Resolve #200, ignore empty conditional format style --- excelize_test.go | 17 ++++++++++++++--- styles.go | 10 +++++++--- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/excelize_test.go b/excelize_test.go index 1ce4fe9..815a08d 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -978,7 +978,7 @@ func TestConditionalFormat(t *testing.T) { fillCells(f, sheet1, 10, 15) - var format1, format2, format3 int + var format1, format2, format3, format4 int var err error // Rose format for bad conditional. format1, err = f.NewConditionalStyle(`{"font":{"color":"#9A0511"},"fill":{"type":"pattern","color":["#FEC7CE"],"pattern":1}}`) @@ -998,6 +998,12 @@ func TestConditionalFormat(t *testing.T) { t.FailNow() } + // conditional style with align and left border. + format4, err = f.NewConditionalStyle(`{"alignment":{"wrap_text":true},"border":[{"type":"left","color":"#000000","style":1}]}`) + if !assert.NoError(t, err) { + t.FailNow() + } + // Color scales: 2 color. assert.NoError(t, f.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. @@ -1022,8 +1028,13 @@ func TestConditionalFormat(t *testing.T) { assert.NoError(t, f.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. assert.NoError(t, f.SetConditionalFormat(sheet1, "L1:L10", fmt.Sprintf(`[{"type":"formula", "criteria":"L2<3", "format":%d}]`, format1))) - // Test set invalid format set in conditional format + // Alignment/Border cells rules. + assert.NoError(t, f.SetConditionalFormat(sheet1, "M1:M10", fmt.Sprintf(`[{"type":"cell","criteria":">","format":%d,"value":"0"}]`, format4))) + + // Test set invalid format set in conditional format. assert.EqualError(t, f.SetConditionalFormat(sheet1, "L1:L10", ""), "unexpected end of JSON input") + // Set conditional format on not exists worksheet. + assert.EqualError(t, f.SetConditionalFormat("SheetN", "L1:L10", "[]"), "sheet SheetN is not exist") err = f.SaveAs(filepath.Join("test", "TestConditionalFormat.xlsx")) if !assert.NoError(t, err) { @@ -1053,7 +1064,7 @@ func TestConditionalFormatError(t *testing.T) { fillCells(f, sheet1, 10, 15) - // Set conditional format with illegal JSON string should return error + // Set conditional format with illegal JSON string should return error. _, err := f.NewConditionalStyle("") if !assert.EqualError(t, err, "unexpected end of JSON input") { t.FailNow() diff --git a/styles.go b/styles.go index 175a17c..f2171bb 100644 --- a/styles.go +++ b/styles.go @@ -1943,9 +1943,13 @@ func (f *File) NewConditionalStyle(style string) (int, error) { return 0, err } dxf := dxf{ - Fill: setFills(fs, false), - Alignment: setAlignment(fs), - Border: setBorders(fs), + Fill: setFills(fs, false), + } + if fs.Alignment != nil { + dxf.Alignment = setAlignment(fs) + } + if len(fs.Border) > 0 { + dxf.Border = setBorders(fs) } if fs.Font != nil { dxf.Font = f.setFont(fs) -- cgit v1.2.1 From a75c6f63bea6c8e438482cb79e1725f23d7f7f9c Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 25 Mar 2020 00:13:29 +0800 Subject: #451, init struct for chart sheet --- xmlChartSheet.go | 88 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 xmlChartSheet.go diff --git a/xmlChartSheet.go b/xmlChartSheet.go new file mode 100644 index 0000000..3417eac --- /dev/null +++ b/xmlChartSheet.go @@ -0,0 +1,88 @@ +// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of +// this source code is governed by a BSD-style license that can be found in +// the LICENSE file. +// +// struct code generated by github.com/xuri/xgen +// +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.10 or later. + +package excelize + +import "encoding/xml" + +// xlsxChartsheet directly maps the chartsheet element of Chartsheet Parts in +// a SpreadsheetML document. +type xlsxChartsheet struct { + XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main chartsheet"` + SheetPr []*xlsxChartsheetPr `xml:"sheetPr"` + SheetViews []*xlsxChartsheetViews `xml:"sheetViews"` + SheetProtection []*xlsxChartsheetProtection `xml:"sheetProtection"` + CustomSheetViews []*xlsxCustomChartsheetViews `xml:"customSheetViews"` + PageMargins *xlsxPageMargins `xml:"pageMargins"` + PageSetup []*xlsxPageSetUp `xml:"pageSetup"` + HeaderFooter *xlsxHeaderFooter `xml:"headerFooter"` + Drawing []*xlsxDrawing `xml:"drawing"` + DrawingHF []*xlsxDrawingHF `xml:"drawingHF"` + Picture []*xlsxPicture `xml:"picture"` + WebPublishItems []*xlsxInnerXML `xml:"webPublishItems"` + ExtLst []*xlsxExtLst `xml:"extLst"` +} + +// xlsxChartsheetPr specifies chart sheet properties. +type xlsxChartsheetPr struct { + XMLName xml.Name `xml:"sheetPr"` + PublishedAttr bool `xml:"published,attr,omitempty"` + CodeNameAttr string `xml:"codeName,attr,omitempty"` + TabColor []*xlsxTabColor `xml:"tabColor"` +} + +// xlsxChartsheetViews specifies chart sheet views. +type xlsxChartsheetViews struct { + XMLName xml.Name `xml:"sheetViews"` + SheetView []*xlsxChartsheetView `xml:"sheetView"` + ExtLst []*xlsxExtLst `xml:"extLst"` +} + +// xlsxChartsheetView defines custom view properties for chart sheets. +type xlsxChartsheetView struct { + XMLName xml.Name `xml:"sheetView"` + TabSelectedAttr bool `xml:"tabSelected,attr,omitempty"` + ZoomScaleAttr uint32 `xml:"zoomScale,attr,omitempty"` + WorkbookViewIdAttr uint32 `xml:"workbookViewId,attr"` + ZoomToFitAttr bool `xml:"zoomToFit,attr,omitempty"` + ExtLst []*xlsxExtLst `xml:"extLst"` +} + +// xlsxChartsheetProtection collection expresses the chart sheet protection +// options to enforce when the chart sheet is protected. +type xlsxChartsheetProtection struct { + XMLName xml.Name `xml:"sheetProtection"` + AlgorithmNameAttr string `xml:"algorithmName,attr,omitempty"` + HashValueAttr []byte `xml:"hashValue,attr,omitempty"` + SaltValueAttr []byte `xml:"saltValue,attr,omitempty"` + SpinCountAttr uint32 `xml:"spinCount,attr,omitempty"` + ContentAttr bool `xml:"content,attr,omitempty"` + ObjectsAttr bool `xml:"objects,attr,omitempty"` +} + +// xlsxCustomChartsheetViews collection of custom Chart Sheet View +// information. +type xlsxCustomChartsheetViews struct { + XMLName xml.Name `xml:"customChartsheetViews"` + CustomSheetView []*xlsxCustomChartsheetView `xml:"customSheetView"` +} + +// xlsxCustomChartsheetView defines custom view properties for chart sheets. +type xlsxCustomChartsheetView struct { + XMLName xml.Name `xml:"customChartsheetView"` + GuidAttr string `xml:"guid,attr"` + ScaleAttr uint32 `xml:"scale,attr,omitempty"` + StateAttr string `xml:"state,attr,omitempty"` + ZoomToFitAttr bool `xml:"zoomToFit,attr,omitempty"` + PageMargins []*xlsxPageMargins `xml:"pageMargins"` + PageSetup []*xlsxPageSetUp `xml:"pageSetup"` + HeaderFooter []*xlsxHeaderFooter `xml:"headerFooter"` +} -- cgit v1.2.1 From 6afc468a025984aa1b265b0228f032c5ed881a3b Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 28 Mar 2020 23:47:26 +0800 Subject: Resolve #451, support create chart sheet --- LICENSE | 1 - chart.go | 68 +++++++++++++++++++++++++++++++++++++ chart_test.go | 23 +++++++++++-- drawing.go | 63 ++++++++++++++++++++++++++++++++++ excelize.go | 23 ++++--------- picture.go | 16 +++++---- sheet.go | 16 ++++----- styles.go | 2 +- xmlChartSheet.go | 2 +- xmlDrawing.go | 101 +++++++++++++++++++++++++++++++++---------------------- xmlWorksheet.go | 3 +- 11 files changed, 239 insertions(+), 79 deletions(-) diff --git a/LICENSE b/LICENSE index 51ec1fb..fe738b9 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,6 @@ BSD 3-Clause License Copyright (c) 2016-2020, 360 Enterprise Security Group, Endpoint Security, Inc. -Copyright (c) 2011-2017, Geoffrey J. Teale All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/chart.go b/chart.go index 69c2c95..df196e9 100644 --- a/chart.go +++ b/chart.go @@ -11,7 +11,9 @@ package excelize import ( "encoding/json" + "encoding/xml" "errors" + "fmt" "strconv" "strings" ) @@ -768,6 +770,72 @@ func (f *File) AddChart(sheet, cell, format string, combo ...string) error { return err } +// AddChartSheet provides the method to create a chartsheet by given chart +// format set (such as offset, scale, aspect ratio setting and print settings) +// and properties set. In Excel a chartsheet is a worksheet that only contains +// a chart. +func (f *File) AddChartSheet(sheet, format string, combo ...string) error { + // Check if the worksheet already exists + if f.GetSheetIndex(sheet) != 0 { + return errors.New("already existing name worksheet") + } + formatSet, err := parseFormatChartSet(format) + if err != nil { + return err + } + comboCharts := []*formatChart{} + for _, comboFormat := range combo { + comboChart, err := parseFormatChartSet(comboFormat) + if err != nil { + return err + } + if _, ok := chartValAxNumFmtFormatCode[comboChart.Type]; !ok { + return errors.New("unsupported chart type " + comboChart.Type) + } + comboCharts = append(comboCharts, comboChart) + } + if _, ok := chartValAxNumFmtFormatCode[formatSet.Type]; !ok { + return errors.New("unsupported chart type " + formatSet.Type) + } + cs := xlsxChartsheet{ + SheetViews: []*xlsxChartsheetViews{{ + SheetView: []*xlsxChartsheetView{{ZoomScaleAttr: 100, ZoomToFitAttr: true}}}, + }, + } + wb := f.workbookReader() + sheetID := 0 + for _, v := range wb.Sheets.Sheet { + if v.SheetID > sheetID { + sheetID = v.SheetID + } + } + sheetID++ + path := "xl/chartsheets/sheet" + strconv.Itoa(sheetID) + ".xml" + f.sheetMap[trimSheetName(sheet)] = path + f.Sheet[path] = nil + drawingID := f.countDrawings() + 1 + chartID := f.countCharts() + 1 + drawingXML := "xl/drawings/drawing" + strconv.Itoa(drawingID) + ".xml" + drawingID, drawingXML = f.prepareChartSheetDrawing(&cs, drawingID, sheet, drawingXML) + drawingRels := "xl/drawings/_rels/drawing" + strconv.Itoa(drawingID) + ".xml.rels" + drawingRID := f.addRels(drawingRels, SourceRelationshipChart, "../charts/chart"+strconv.Itoa(chartID)+".xml", "") + err = f.addSheetDrawingChart(sheet, drawingXML, formatSet.Dimension.Width, formatSet.Dimension.Height, drawingRID, &formatSet.Format) + if err != nil { + return err + } + f.addChart(formatSet, comboCharts) + f.addContentTypePart(chartID, "chart") + f.addContentTypePart(sheetID, "chartsheet") + f.addContentTypePart(drawingID, "drawings") + // Update xl/_rels/workbook.xml.rels + rID := f.addRels("xl/_rels/workbook.xml.rels", SourceRelationshipChartsheet, fmt.Sprintf("chartsheets/sheet%d.xml", sheetID), "") + // Update xl/workbook.xml + f.setWorkbook(sheet, sheetID, rID) + v, _ := xml.Marshal(cs) + f.saveFileList(path, replaceRelationshipsBytes(replaceWorkSheetsRelationshipsNameSpaceBytes(v))) + return err +} + // DeleteChart provides a function to delete chart in XLSX by given worksheet // and cell name. func (f *File) DeleteChart(sheet, cell string) (err error) { diff --git a/chart_test.go b/chart_test.go index 98f3555..351e663 100644 --- a/chart_test.go +++ b/chart_test.go @@ -200,12 +200,31 @@ func TestAddChart(t *testing.T) { assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddChart.xlsx"))) // Test with unsupported chart type assert.EqualError(t, f.AddChart("Sheet2", "BD32", `{"type":"unknown","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"Bubble 3D 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"}`), "unsupported chart type unknown") - // Test add combo chart with invalid format set. + // Test add combo chart with invalid format set assert.EqualError(t, f.AddChart("Sheet2", "BD32", `{"type":"col","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"2D Column 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"}`, ""), "unexpected end of JSON input") - // Test add combo chart with unsupported chart type. + // Test add combo chart with unsupported chart type assert.EqualError(t, f.AddChart("Sheet2", "BD64", `{"type":"barOfPie","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$A$30:$D$37","values":"Sheet1!$B$30:$B$37"}],"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":"left","show_legend_key":false},"title":{"name":"Bar of Pie 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","x_axis":{"major_grid_lines":true},"y_axis":{"major_grid_lines":true}}`, `{"type":"unknown","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$A$30:$D$37","values":"Sheet1!$B$30:$B$37"}],"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":"left","show_legend_key":false},"title":{"name":"Bar of Pie 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","x_axis":{"major_grid_lines":true},"y_axis":{"major_grid_lines":true}}`), "unsupported chart type unknown") } +func TestAddChartSheet(t *testing.T) { + 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} + f := NewFile() + for k, v := range categories { + assert.NoError(t, f.SetCellValue("Sheet1", k, v)) + } + for k, v := range values { + assert.NoError(t, f.SetCellValue("Sheet1", k, v)) + } + 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"}}`)) + + assert.EqualError(t, f.AddChartSheet("Sheet1", `{"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"}}`), "already existing name worksheet") + // Test with unsupported chart type + assert.EqualError(t, f.AddChartSheet("Chart2", `{"type":"unknown","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"}}`), "unsupported chart type unknown") + + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddChartSheet.xlsx"))) +} + func TestDeleteChart(t *testing.T) { f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) assert.NoError(t, err) diff --git a/drawing.go b/drawing.go index e51b6af..8ca1f49 100644 --- a/drawing.go +++ b/drawing.go @@ -38,6 +38,26 @@ func (f *File) prepareDrawing(xlsx *xlsxWorksheet, drawingID int, sheet, drawing return drawingID, drawingXML } +// prepareChartSheetDrawing provides a function to prepare drawing ID and XML +// by given drawingID, worksheet name and default drawingXML. +func (f *File) prepareChartSheetDrawing(xlsx *xlsxChartsheet, drawingID int, sheet, drawingXML string) (int, string) { + sheetRelationshipsDrawingXML := "../drawings/drawing" + strconv.Itoa(drawingID) + ".xml" + if xlsx.Drawing != nil { + // The worksheet already has a picture or chart relationships, use the relationships drawing ../drawings/drawing%d.xml. + sheetRelationshipsDrawingXML = f.getSheetRelationshipsTargetByID(sheet, xlsx.Drawing.RID) + drawingID, _ = strconv.Atoi(strings.TrimSuffix(strings.TrimPrefix(sheetRelationshipsDrawingXML, "../drawings/drawing"), ".xml")) + drawingXML = strings.Replace(sheetRelationshipsDrawingXML, "..", "xl", -1) + } else { + // Add first picture for given sheet. + sheetRels := "xl/chartsheets/_rels/" + strings.TrimPrefix(f.sheetMap[trimSheetName(sheet)], "xl/chartsheets/") + ".rels" + rID := f.addRels(sheetRels, SourceRelationshipDrawingML, sheetRelationshipsDrawingXML, "") + xlsx.Drawing = &xlsxDrawing{ + RID: "rId" + strconv.Itoa(rID), + } + } + return drawingID, drawingXML +} + // addChart provides a function to create chart as xl/charts/chart%d.xml by // given format sets. func (f *File) addChart(formatSet *formatChart, comboCharts []*formatChart) { @@ -1209,6 +1229,49 @@ func (f *File) addDrawingChart(sheet, drawingXML, cell string, width, height, rI return err } +// addSheetDrawingChart provides a function to add chart graphic frame for +// chartsheet by given sheet, drawingXML, width, height, relationship index +// and format sets. +func (f *File) addSheetDrawingChart(sheet, drawingXML string, width, height, rID int, formatSet *formatPicture) (err error) { + width = int(float64(width) * formatSet.XScale) + height = int(float64(height) * formatSet.YScale) + + content, cNvPrID := f.drawingParser(drawingXML) + absoluteAnchor := xdrCellAnchor{ + EditAs: formatSet.Positioning, + Pos: &xlsxPoint2D{}, + Ext: &xlsxExt{}, + } + + graphicFrame := xlsxGraphicFrame{ + NvGraphicFramePr: xlsxNvGraphicFramePr{ + CNvPr: &xlsxCNvPr{ + ID: cNvPrID, + Name: "Chart " + strconv.Itoa(cNvPrID), + }, + }, + Graphic: &xlsxGraphic{ + GraphicData: &xlsxGraphicData{ + URI: NameSpaceDrawingMLChart, + Chart: &xlsxChart{ + C: NameSpaceDrawingMLChart, + R: SourceRelationship, + RID: "rId" + strconv.Itoa(rID), + }, + }, + }, + } + graphic, _ := xml.Marshal(graphicFrame) + absoluteAnchor.GraphicFrame = string(graphic) + absoluteAnchor.ClientData = &xdrClientData{ + FLocksWithSheet: formatSet.FLocksWithSheet, + FPrintsWithSheet: formatSet.FPrintsWithSheet, + } + content.AbsoluteAnchor = append(content.AbsoluteAnchor, &absoluteAnchor) + f.Drawings[drawingXML] = content + return err +} + // deleteDrawing provides a function to delete chart graphic frame by given by // given coordinates and graphic type. func (f *File) deleteDrawing(col, row int, drawingXML, drawingType string) (err error) { diff --git a/excelize.go b/excelize.go index 795120d..3dd4311 100644 --- a/excelize.go +++ b/excelize.go @@ -228,21 +228,10 @@ func (f *File) addRels(relPath, relType, target, targetMode string) int { } // replaceWorkSheetsRelationshipsNameSpaceBytes provides a function to replace -// xl/worksheets/sheet%d.xml XML tags to self-closing for compatible Microsoft -// Office Excel 2007. -func replaceWorkSheetsRelationshipsNameSpaceBytes(workbookMarshal []byte) []byte { - var oldXmlns = []byte(``) - var newXmlns = []byte(``) - var newXmlns = []byte(``) + var newXmlns = []byte(templateNamespaceIDMap) contentMarshal = bytes.Replace(contentMarshal, oldXmlns, newXmlns, -1) return contentMarshal } @@ -354,13 +343,13 @@ func (f *File) setContentTypePartVBAProjectExtensions() { } for idx, o := range content.Overrides { if o.PartName == "/xl/workbook.xml" { - content.Overrides[idx].ContentType = "application/vnd.ms-excel.sheet.macroEnabled.main+xml" + content.Overrides[idx].ContentType = ContentTypeMacro } } if !ok { content.Defaults = append(content.Defaults, xlsxDefault{ Extension: "bin", - ContentType: "application/vnd.ms-office.vbaProject", + ContentType: ContentTypeVBA, }) } } diff --git a/picture.go b/picture.go index 3e24ce3..ddc0480 100644 --- a/picture.go +++ b/picture.go @@ -354,7 +354,7 @@ func (f *File) setContentTypePartVMLExtensions() { if !vml { content.Defaults = append(content.Defaults, xlsxDefault{ Extension: "vml", - ContentType: "application/vnd.openxmlformats-officedocument.vmlDrawing", + ContentType: ContentTypeVML, }) } } @@ -368,6 +368,7 @@ func (f *File) addContentTypePart(index int, contentType string) { } partNames := map[string]string{ "chart": "/xl/charts/chart" + strconv.Itoa(index) + ".xml", + "chartsheet": "/xl/chartsheets/sheet" + strconv.Itoa(index) + ".xml", "comments": "/xl/comments" + strconv.Itoa(index) + ".xml", "drawings": "/xl/drawings/drawing" + strconv.Itoa(index) + ".xml", "table": "/xl/tables/table" + strconv.Itoa(index) + ".xml", @@ -375,12 +376,13 @@ func (f *File) addContentTypePart(index int, contentType string) { "pivotCache": "/xl/pivotCache/pivotCacheDefinition" + strconv.Itoa(index) + ".xml", } contentTypes := map[string]string{ - "chart": "application/vnd.openxmlformats-officedocument.drawingml.chart+xml", - "comments": "application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml", - "drawings": "application/vnd.openxmlformats-officedocument.drawing+xml", - "table": "application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml", - "pivotTable": "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTable+xml", - "pivotCache": "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheDefinition+xml", + "chart": ContentTypeDrawingML, + "chartsheet": ContentTypeSpreadSheetMLChartsheet, + "comments": ContentTypeSpreadSheetMLComments, + "drawings": ContentTypeDrawing, + "table": ContentTypeSpreadSheetMLTable, + "pivotTable": ContentTypeSpreadSheetMLPivotTable, + "pivotCache": ContentTypeSpreadSheetMLPivotCacheDefinition, } s, ok := setContentType[contentType] if ok { diff --git a/sheet.go b/sheet.go index 08b0e96..11f56d9 100644 --- a/sheet.go +++ b/sheet.go @@ -50,7 +50,7 @@ func (f *File) NewSheet(name string) int { // Update docProps/app.xml f.setAppXML() // Update [Content_Types].xml - f.setContentTypes(sheetID) + f.setContentTypes("/xl/worksheets/sheet"+strconv.Itoa(sheetID)+".xml", ContentTypeSpreadSheetMLWorksheet) // Create new sheet /xl/worksheets/sheet%d.xml f.setSheet(sheetID, name) // Update xl/_rels/workbook.xml.rels @@ -151,11 +151,11 @@ func trimCell(column []xlsxC) []xlsxC { // setContentTypes provides a function to read and update property of contents // type of XLSX. -func (f *File) setContentTypes(index int) { +func (f *File) setContentTypes(partName, contentType string) { content := f.contentTypesReader() content.Overrides = append(content.Overrides, xlsxOverride{ - PartName: "/xl/worksheets/sheet" + strconv.Itoa(index) + ".xml", - ContentType: "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml", + PartName: partName, + ContentType: contentType, }) } @@ -336,8 +336,8 @@ func (f *File) GetSheetIndex(name string) int { return 0 } -// GetSheetMap provides a function to get worksheet name and index map of XLSX. -// For example: +// GetSheetMap provides a function to get worksheet and chartsheet name and +// index map of XLSX. For example: // // f, err := excelize.OpenFile("Book1.xlsx") // if err != nil { @@ -358,8 +358,8 @@ func (f *File) GetSheetMap() map[int]string { return sheetMap } -// getSheetMap provides a function to get worksheet name and XML file path map -// of XLSX. +// getSheetMap provides a function to get worksheet and chartsheet 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") diff --git a/styles.go b/styles.go index f2171bb..8d8b464 100644 --- a/styles.go +++ b/styles.go @@ -1018,7 +1018,7 @@ func (f *File) stylesReader() *xlsxStyleSheet { func (f *File) styleSheetWriter() { if f.Styles != nil { output, _ := xml.Marshal(f.Styles) - f.saveFileList("xl/styles.xml", replaceStyleRelationshipsNameSpaceBytes(output)) + f.saveFileList("xl/styles.xml", replaceWorkSheetsRelationshipsNameSpaceBytes(output)) } } diff --git a/xmlChartSheet.go b/xmlChartSheet.go index 3417eac..fae5a16 100644 --- a/xmlChartSheet.go +++ b/xmlChartSheet.go @@ -24,7 +24,7 @@ type xlsxChartsheet struct { PageMargins *xlsxPageMargins `xml:"pageMargins"` PageSetup []*xlsxPageSetUp `xml:"pageSetup"` HeaderFooter *xlsxHeaderFooter `xml:"headerFooter"` - Drawing []*xlsxDrawing `xml:"drawing"` + Drawing *xlsxDrawing `xml:"drawing"` DrawingHF []*xlsxDrawingHF `xml:"drawingHF"` Picture []*xlsxPicture `xml:"picture"` WebPublishItems []*xlsxInnerXML `xml:"webPublishItems"` diff --git a/xmlDrawing.go b/xmlDrawing.go index 2bad16a..142121d 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -13,40 +13,52 @@ import "encoding/xml" // Source relationship and namespace. const ( - SourceRelationship = "http://schemas.openxmlformats.org/officeDocument/2006/relationships" - SourceRelationshipChart = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart" - SourceRelationshipComments = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments" - SourceRelationshipImage = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" - SourceRelationshipTable = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/table" - SourceRelationshipDrawingML = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing" - SourceRelationshipDrawingVML = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing" - SourceRelationshipHyperLink = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink" - SourceRelationshipWorkSheet = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" - SourceRelationshipPivotTable = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotTable" - SourceRelationshipPivotCache = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheDefinition" - SourceRelationshipVBAProject = "http://schemas.microsoft.com/office/2006/relationships/vbaProject" - SourceRelationshipChart201506 = "http://schemas.microsoft.com/office/drawing/2015/06/chart" - SourceRelationshipChart20070802 = "http://schemas.microsoft.com/office/drawing/2007/8/2/chart" - SourceRelationshipChart2014 = "http://schemas.microsoft.com/office/drawing/2014/chart" - SourceRelationshipCompatibility = "http://schemas.openxmlformats.org/markup-compatibility/2006" - NameSpaceDrawingML = "http://schemas.openxmlformats.org/drawingml/2006/main" - NameSpaceDrawingMLChart = "http://schemas.openxmlformats.org/drawingml/2006/chart" - NameSpaceDrawingMLSpreadSheet = "http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing" - NameSpaceSpreadSheet = "http://schemas.openxmlformats.org/spreadsheetml/2006/main" - NameSpaceSpreadSheetX14 = "http://schemas.microsoft.com/office/spreadsheetml/2009/9/main" - NameSpaceSpreadSheetX15 = "http://schemas.microsoft.com/office/spreadsheetml/2010/11/main" - NameSpaceSpreadSheetExcel2006Main = "http://schemas.microsoft.com/office/excel/2006/main" - NameSpaceMacExcel2008Main = "http://schemas.microsoft.com/office/mac/excel/2008/main" - NameSpaceXML = "http://www.w3.org/XML/1998/namespace" - NameSpaceXMLSchemaInstance = "http://www.w3.org/2001/XMLSchema-instance" - StrictSourceRelationship = "http://purl.oclc.org/ooxml/officeDocument/relationships" - StrictSourceRelationshipChart = "http://purl.oclc.org/ooxml/officeDocument/relationships/chart" - StrictSourceRelationshipComments = "http://purl.oclc.org/ooxml/officeDocument/relationships/comments" - StrictSourceRelationshipImage = "http://purl.oclc.org/ooxml/officeDocument/relationships/image" - StrictNameSpaceSpreadSheet = "http://purl.oclc.org/ooxml/spreadsheetml/main" - NameSpaceDublinCore = "http://purl.org/dc/elements/1.1/" - NameSpaceDublinCoreTerms = "http://purl.org/dc/terms/" - NameSpaceDublinCoreMetadataIntiative = "http://purl.org/dc/dcmitype/" + SourceRelationship = "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + SourceRelationshipChart = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart" + SourceRelationshipComments = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments" + SourceRelationshipImage = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" + SourceRelationshipTable = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/table" + SourceRelationshipDrawingML = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing" + SourceRelationshipDrawingVML = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing" + SourceRelationshipHyperLink = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink" + SourceRelationshipWorkSheet = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" + SourceRelationshipChartsheet = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chartsheet" + SourceRelationshipPivotTable = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotTable" + SourceRelationshipPivotCache = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheDefinition" + SourceRelationshipVBAProject = "http://schemas.microsoft.com/office/2006/relationships/vbaProject" + SourceRelationshipChart201506 = "http://schemas.microsoft.com/office/drawing/2015/06/chart" + SourceRelationshipChart20070802 = "http://schemas.microsoft.com/office/drawing/2007/8/2/chart" + SourceRelationshipChart2014 = "http://schemas.microsoft.com/office/drawing/2014/chart" + SourceRelationshipCompatibility = "http://schemas.openxmlformats.org/markup-compatibility/2006" + NameSpaceDrawingML = "http://schemas.openxmlformats.org/drawingml/2006/main" + NameSpaceDrawingMLChart = "http://schemas.openxmlformats.org/drawingml/2006/chart" + NameSpaceDrawingMLSpreadSheet = "http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing" + NameSpaceSpreadSheet = "http://schemas.openxmlformats.org/spreadsheetml/2006/main" + NameSpaceSpreadSheetX14 = "http://schemas.microsoft.com/office/spreadsheetml/2009/9/main" + NameSpaceSpreadSheetX15 = "http://schemas.microsoft.com/office/spreadsheetml/2010/11/main" + NameSpaceSpreadSheetExcel2006Main = "http://schemas.microsoft.com/office/excel/2006/main" + NameSpaceMacExcel2008Main = "http://schemas.microsoft.com/office/mac/excel/2008/main" + NameSpaceXML = "http://www.w3.org/XML/1998/namespace" + NameSpaceXMLSchemaInstance = "http://www.w3.org/2001/XMLSchema-instance" + StrictSourceRelationship = "http://purl.oclc.org/ooxml/officeDocument/relationships" + StrictSourceRelationshipChart = "http://purl.oclc.org/ooxml/officeDocument/relationships/chart" + StrictSourceRelationshipComments = "http://purl.oclc.org/ooxml/officeDocument/relationships/comments" + StrictSourceRelationshipImage = "http://purl.oclc.org/ooxml/officeDocument/relationships/image" + StrictNameSpaceSpreadSheet = "http://purl.oclc.org/ooxml/spreadsheetml/main" + NameSpaceDublinCore = "http://purl.org/dc/elements/1.1/" + NameSpaceDublinCoreTerms = "http://purl.org/dc/terms/" + NameSpaceDublinCoreMetadataIntiative = "http://purl.org/dc/dcmitype/" + ContentTypeDrawing = "application/vnd.openxmlformats-officedocument.drawing+xml" + ContentTypeDrawingML = "application/vnd.openxmlformats-officedocument.drawingml.chart+xml" + ContentTypeMacro = "application/vnd.ms-excel.sheet.macroEnabled.main+xml" + ContentTypeSpreadSheetMLChartsheet = "application/vnd.openxmlformats-officedocument.spreadsheetml.chartsheet+xml" + ContentTypeSpreadSheetMLComments = "application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml" + ContentTypeSpreadSheetMLPivotCacheDefinition = "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheDefinition+xml" + ContentTypeSpreadSheetMLPivotTable = "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTable+xml" + ContentTypeSpreadSheetMLTable = "application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml" + ContentTypeSpreadSheetMLWorksheet = "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml" + ContentTypeVBA = "application/vnd.ms-office.vbaProject" + ContentTypeVML = "application/vnd.openxmlformats-officedocument.vmlDrawing" // ExtURIConditionalFormattings is the extLst child element // ([ISO/IEC29500-1:2016] section 18.2.10) of the worksheet element // ([ISO/IEC29500-1:2016] section 18.3.1.99) is extended by the addition of @@ -240,6 +252,7 @@ type xdrClientData struct { // with cells and its extents are in EMU units. type xdrCellAnchor struct { EditAs string `xml:"editAs,attr,omitempty"` + Pos *xlsxPoint2D `xml:"xdr:pos"` From *xlsxFrom `xml:"xdr:from"` To *xlsxTo `xml:"xdr:to"` Ext *xlsxExt `xml:"xdr:ext"` @@ -249,15 +262,23 @@ type xdrCellAnchor struct { ClientData *xdrClientData `xml:"xdr:clientData"` } +// xlsxPoint2D describes the position of a drawing element within a spreadsheet. +type xlsxPoint2D struct { + XMLName xml.Name `xml:"xdr:pos"` + X int `xml:"x,attr"` + Y int `xml:"y,attr"` +} + // xlsxWsDr directly maps the root element for a part of this content type shall // wsDr. type xlsxWsDr struct { - XMLName xml.Name `xml:"xdr:wsDr"` - OneCellAnchor []*xdrCellAnchor `xml:"xdr:oneCellAnchor"` - TwoCellAnchor []*xdrCellAnchor `xml:"xdr:twoCellAnchor"` - A string `xml:"xmlns:a,attr,omitempty"` - Xdr string `xml:"xmlns:xdr,attr,omitempty"` - R string `xml:"xmlns:r,attr,omitempty"` + XMLName xml.Name `xml:"xdr:wsDr"` + AbsoluteAnchor []*xdrCellAnchor `xml:"xdr:absoluteAnchor"` + OneCellAnchor []*xdrCellAnchor `xml:"xdr:oneCellAnchor"` + TwoCellAnchor []*xdrCellAnchor `xml:"xdr:twoCellAnchor"` + A string `xml:"xmlns:a,attr,omitempty"` + Xdr string `xml:"xmlns:xdr,attr,omitempty"` + R string `xml:"xmlns:r,attr,omitempty"` } // xlsxGraphicFrame (Graphic Frame) directly maps the xdr:graphicFrame element. diff --git a/xmlWorksheet.go b/xmlWorksheet.go index aa33819..316ffd7 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -12,8 +12,7 @@ package excelize import "encoding/xml" // xlsxWorksheet directly maps the worksheet element in the namespace -// http://schemas.openxmlformats.org/spreadsheetml/2006/main - currently I have -// not checked it for completeness - it does as much as I need. +// http://schemas.openxmlformats.org/spreadsheetml/2006/main. type xlsxWorksheet struct { XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main worksheet"` SheetPr *xlsxSheetPr `xml:"sheetPr"` -- cgit v1.2.1 From 3f89c6e9799c9c82af1305f080416c53d19e64c1 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 29 Mar 2020 18:44:24 +0800 Subject: remove ineffectual variable assignments and simplify code --- LICENSE | 2 +- chart.go | 72 +++++++++++++++++++++++++++-------------------------------- chart_test.go | 18 +++++++++++++-- drawing.go | 7 ++---- excelize.go | 8 +++++-- rows.go | 2 +- sheet.go | 35 +++++++++++++---------------- styles.go | 2 +- 8 files changed, 75 insertions(+), 71 deletions(-) diff --git a/LICENSE b/LICENSE index fe738b9..e0f34bb 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ BSD 3-Clause License -Copyright (c) 2016-2020, 360 Enterprise Security Group, Endpoint Security, Inc. +Copyright (c) 2016-2020 The excelize Authors. All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/chart.go b/chart.go index df196e9..cae833d 100644 --- a/chart.go +++ b/chart.go @@ -730,28 +730,14 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // } // func (f *File) AddChart(sheet, cell, format string, combo ...string) error { - formatSet, err := parseFormatChartSet(format) - if err != nil { - return err - } - comboCharts := []*formatChart{} - for _, comboFormat := range combo { - comboChart, err := parseFormatChartSet(comboFormat) - if err != nil { - return err - } - if _, ok := chartValAxNumFmtFormatCode[comboChart.Type]; !ok { - return errors.New("unsupported chart type " + comboChart.Type) - } - comboCharts = append(comboCharts, comboChart) - } // Read sheet data. xlsx, err := f.workSheetReader(sheet) if err != nil { return err } - if _, ok := chartValAxNumFmtFormatCode[formatSet.Type]; !ok { - return errors.New("unsupported chart type " + formatSet.Type) + formatSet, comboCharts, err := f.getFormatChart(format, combo) + if err != nil { + return err } // Add first picture for given sheet, create xl/drawings/ and xl/drawings/_rels/ folder. drawingID := f.countDrawings() + 1 @@ -777,31 +763,18 @@ func (f *File) AddChart(sheet, cell, format string, combo ...string) error { func (f *File) AddChartSheet(sheet, format string, combo ...string) error { // Check if the worksheet already exists if f.GetSheetIndex(sheet) != 0 { - return errors.New("already existing name worksheet") + return errors.New("the same name worksheet already exists") } - formatSet, err := parseFormatChartSet(format) + formatSet, comboCharts, err := f.getFormatChart(format, combo) if err != nil { return err } - comboCharts := []*formatChart{} - for _, comboFormat := range combo { - comboChart, err := parseFormatChartSet(comboFormat) - if err != nil { - return err - } - if _, ok := chartValAxNumFmtFormatCode[comboChart.Type]; !ok { - return errors.New("unsupported chart type " + comboChart.Type) - } - comboCharts = append(comboCharts, comboChart) - } - if _, ok := chartValAxNumFmtFormatCode[formatSet.Type]; !ok { - return errors.New("unsupported chart type " + formatSet.Type) - } cs := xlsxChartsheet{ SheetViews: []*xlsxChartsheetViews{{ SheetView: []*xlsxChartsheetView{{ZoomScaleAttr: 100, ZoomToFitAttr: true}}}, }, } + f.SheetCount++ wb := f.workbookReader() sheetID := 0 for _, v := range wb.Sheets.Sheet { @@ -819,10 +792,7 @@ func (f *File) AddChartSheet(sheet, format string, combo ...string) error { drawingID, drawingXML = f.prepareChartSheetDrawing(&cs, drawingID, sheet, drawingXML) drawingRels := "xl/drawings/_rels/drawing" + strconv.Itoa(drawingID) + ".xml.rels" drawingRID := f.addRels(drawingRels, SourceRelationshipChart, "../charts/chart"+strconv.Itoa(chartID)+".xml", "") - err = f.addSheetDrawingChart(sheet, drawingXML, formatSet.Dimension.Width, formatSet.Dimension.Height, drawingRID, &formatSet.Format) - if err != nil { - return err - } + f.addSheetDrawingChart(drawingXML, drawingRID, &formatSet.Format) f.addChart(formatSet, comboCharts) f.addContentTypePart(chartID, "chart") f.addContentTypePart(sheetID, "chartsheet") @@ -831,11 +801,35 @@ func (f *File) AddChartSheet(sheet, format string, combo ...string) error { rID := f.addRels("xl/_rels/workbook.xml.rels", SourceRelationshipChartsheet, fmt.Sprintf("chartsheets/sheet%d.xml", sheetID), "") // Update xl/workbook.xml f.setWorkbook(sheet, sheetID, rID) - v, _ := xml.Marshal(cs) - f.saveFileList(path, replaceRelationshipsBytes(replaceWorkSheetsRelationshipsNameSpaceBytes(v))) + chartsheet, _ := xml.Marshal(cs) + f.saveFileList(path, replaceRelationshipsBytes(replaceRelationshipsNameSpaceBytes(chartsheet))) return err } +// getFormatChart provides a function to check format set of the chart and +// create chart format. +func (f *File) getFormatChart(format string, combo []string) (*formatChart, []*formatChart, error) { + comboCharts := []*formatChart{} + formatSet, err := parseFormatChartSet(format) + if err != nil { + return formatSet, comboCharts, err + } + for _, comboFormat := range combo { + comboChart, err := parseFormatChartSet(comboFormat) + if err != nil { + return formatSet, comboCharts, err + } + if _, ok := chartValAxNumFmtFormatCode[comboChart.Type]; !ok { + return formatSet, comboCharts, errors.New("unsupported chart type " + comboChart.Type) + } + comboCharts = append(comboCharts, comboChart) + } + if _, ok := chartValAxNumFmtFormatCode[formatSet.Type]; !ok { + return formatSet, comboCharts, errors.New("unsupported chart type " + formatSet.Type) + } + return formatSet, comboCharts, err +} + // DeleteChart provides a function to delete chart in XLSX by given worksheet // and cell name. func (f *File) DeleteChart(sheet, cell string) (err error) { diff --git a/chart_test.go b/chart_test.go index 351e663..3b419f0 100644 --- a/chart_test.go +++ b/chart_test.go @@ -198,6 +198,8 @@ func TestAddChart(t *testing.T) { assert.NoError(t, f.AddChart("Combo Charts", axis, fmt.Sprintf(`{"type":"areaStacked","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"}],"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":"left","show_legend_key":false},"title":{"name":"%s"},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true}}`, props[1]), fmt.Sprintf(`{"type":"%s","series":[{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"plotarea":{"show_bubble_size":true,"show_cat_name":false,"show_leader_lines":false,"show_percent":true,"show_series_name":true,"show_val":true}}`, props[0]))) } assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddChart.xlsx"))) + // Test with illegal cell coordinates + assert.EqualError(t, f.AddChart("Sheet2", "A", `{"type":"col","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"2D Column 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"}`), `cannot convert cell "A" to coordinates: invalid cell name "A"`) // Test with unsupported chart type assert.EqualError(t, f.AddChart("Sheet2", "BD32", `{"type":"unknown","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"},{"name":"Sheet1!$A$33","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$33:$D$33"},{"name":"Sheet1!$A$34","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$34:$D$34"},{"name":"Sheet1!$A$35","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$35:$D$35"},{"name":"Sheet1!$A$36","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$36:$D$36"},{"name":"Sheet1!$A$37","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$37:$D$37"}],"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":"left","show_legend_key":false},"title":{"name":"Bubble 3D 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"}`), "unsupported chart type unknown") // Test add combo chart with invalid format set @@ -217,8 +219,20 @@ func TestAddChartSheet(t *testing.T) { assert.NoError(t, f.SetCellValue("Sheet1", k, v)) } 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"}}`)) - - assert.EqualError(t, f.AddChartSheet("Sheet1", `{"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"}}`), "already existing name worksheet") + // Test set the chartsheet as active sheet + var sheetID int + for idx, sheetName := range f.GetSheetMap() { + if sheetName != "Chart1" { + continue + } + sheetID = idx + } + f.SetActiveSheet(sheetID) + + // Test cell value on chartsheet + assert.EqualError(t, f.SetCellValue("Chart1", "A1", true), "sheet Chart1 is chart sheet") + // Test add chartsheet on already existing name sheet + assert.EqualError(t, f.AddChartSheet("Sheet1", `{"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"}}`), "the same name worksheet already exists") // Test with unsupported chart type assert.EqualError(t, f.AddChartSheet("Chart2", `{"type":"unknown","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"}}`), "unsupported chart type unknown") diff --git a/drawing.go b/drawing.go index 8ca1f49..b291d98 100644 --- a/drawing.go +++ b/drawing.go @@ -1232,10 +1232,7 @@ func (f *File) addDrawingChart(sheet, drawingXML, cell string, width, height, rI // addSheetDrawingChart provides a function to add chart graphic frame for // chartsheet by given sheet, drawingXML, width, height, relationship index // and format sets. -func (f *File) addSheetDrawingChart(sheet, drawingXML string, width, height, rID int, formatSet *formatPicture) (err error) { - width = int(float64(width) * formatSet.XScale) - height = int(float64(height) * formatSet.YScale) - +func (f *File) addSheetDrawingChart(drawingXML string, rID int, formatSet *formatPicture) { content, cNvPrID := f.drawingParser(drawingXML) absoluteAnchor := xdrCellAnchor{ EditAs: formatSet.Positioning, @@ -1269,7 +1266,7 @@ func (f *File) addSheetDrawingChart(sheet, drawingXML string, width, height, rID } content.AbsoluteAnchor = append(content.AbsoluteAnchor, &absoluteAnchor) f.Drawings[drawingXML] = content - return err + return } // deleteDrawing provides a function to delete chart graphic frame by given by diff --git a/excelize.go b/excelize.go index 3dd4311..520cbb7 100644 --- a/excelize.go +++ b/excelize.go @@ -156,6 +156,10 @@ func (f *File) workSheetReader(sheet string) (xlsx *xlsxWorksheet, err error) { return } if xlsx = f.Sheet[name]; f.Sheet[name] == nil { + if strings.HasPrefix(name, "xl/chartsheets") { + err = fmt.Errorf("sheet %s is chart sheet", sheet) + return + } xlsx = new(xlsxWorksheet) if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(name)))). Decode(xlsx); err != nil && err != io.EOF { @@ -227,9 +231,9 @@ func (f *File) addRels(relPath, relType, target, targetMode string) int { return rID } -// replaceWorkSheetsRelationshipsNameSpaceBytes provides a function to replace +// replaceRelationshipsNameSpaceBytes provides a function to replace // XML tags to self-closing for compatible Microsoft Office Excel 2007. -func replaceWorkSheetsRelationshipsNameSpaceBytes(contentMarshal []byte) []byte { +func replaceRelationshipsNameSpaceBytes(contentMarshal []byte) []byte { var oldXmlns = []byte(` xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">`) var newXmlns = []byte(templateNamespaceIDMap) contentMarshal = bytes.Replace(contentMarshal, oldXmlns, newXmlns, -1) diff --git a/rows.go b/rows.go index e00a627..76dd0f0 100644 --- a/rows.go +++ b/rows.go @@ -174,7 +174,7 @@ func (f *File) Rows(sheet string) (*Rows, error) { if f.Sheet[name] != nil { // flush data output, _ := xml.Marshal(f.Sheet[name]) - f.saveFileList(name, replaceWorkSheetsRelationshipsNameSpaceBytes(output)) + f.saveFileList(name, replaceRelationshipsNameSpaceBytes(output)) } var ( err error diff --git a/sheet.go b/sheet.go index 11f56d9..6ddd629 100644 --- a/sheet.go +++ b/sheet.go @@ -119,7 +119,7 @@ func (f *File) workSheetWriter() { f.Sheet[p].SheetData.Row[k].C = trimCell(v.C) } output, _ := xml.Marshal(sheet) - f.saveFileList(p, replaceRelationshipsBytes(replaceWorkSheetsRelationshipsNameSpaceBytes(output))) + f.saveFileList(p, replaceRelationshipsBytes(replaceRelationshipsNameSpaceBytes(output))) ok := f.checked[p] if ok { delete(f.Sheet, p) @@ -190,7 +190,7 @@ func (f *File) relsWriter() { if rel != nil { output, _ := xml.Marshal(rel) if strings.HasPrefix(path, "xl/worksheets/sheet/rels/sheet") { - output = replaceWorkSheetsRelationshipsNameSpaceBytes(output) + output = replaceRelationshipsNameSpaceBytes(output) } f.saveFileList(path, replaceRelationshipsBytes(output)) } @@ -211,19 +211,6 @@ func replaceRelationshipsBytes(content []byte) []byte { return bytes.Replace(content, oldXmlns, newXmlns, -1) } -// replaceRelationshipsNameSpaceBytes; Some tools that read XLSX files have -// very strict requirements about the structure of the input XML. In -// particular both Numbers on the Mac and SAS dislike inline XML namespace -// declarations, or namespace prefixes that don't match the ones that Excel -// itself uses. This is a problem because the Go XML library doesn't multiple -// namespace declarations in a single element of a document. This function is -// a horrible hack to fix that after the XML marshalling is completed. -func replaceRelationshipsNameSpaceBytes(workbookMarshal []byte) []byte { - oldXmlns := []byte(``) - newXmlns := []byte(` 0 { - maps[v.Name] = fmt.Sprintf("xl/worksheets/%s", pathInfo[pathInfoLen-1]) + if pathInfoLen > 1 { + maps[v.Name] = fmt.Sprintf("xl/%s", strings.Join(pathInfo[pathInfoLen-2:], "/")) } } } @@ -420,7 +411,10 @@ func (f *File) DeleteSheet(name string) { for _, rel := range wbRels.Relationships { if rel.ID == sheet.ID { sheetXML = fmt.Sprintf("xl/%s", rel.Target) - rels = strings.Replace(fmt.Sprintf("xl/%s.rels", rel.Target), "xl/worksheets/", "xl/worksheets/_rels/", -1) + pathInfo := strings.Split(rel.Target, "/") + if len(pathInfo) == 2 { + rels = fmt.Sprintf("xl/%s/_rels/%s.rels", pathInfo[0], pathInfo[1]) + } } } } @@ -430,6 +424,7 @@ func (f *File) DeleteSheet(name string) { delete(f.sheetMap, sheetName) delete(f.XLSX, sheetXML) delete(f.XLSX, rels) + delete(f.Relationships, rels) delete(f.Sheet, sheetXML) f.SheetCount-- } @@ -729,7 +724,7 @@ func (f *File) SearchSheet(sheet, value string, reg ...bool) ([]string, error) { if f.Sheet[name] != nil { // flush data output, _ := xml.Marshal(f.Sheet[name]) - f.saveFileList(name, replaceWorkSheetsRelationshipsNameSpaceBytes(output)) + f.saveFileList(name, replaceRelationshipsNameSpaceBytes(output)) } return f.searchSheet(name, value, regSearch) } diff --git a/styles.go b/styles.go index 8d8b464..9cf974e 100644 --- a/styles.go +++ b/styles.go @@ -1018,7 +1018,7 @@ func (f *File) stylesReader() *xlsxStyleSheet { func (f *File) styleSheetWriter() { if f.Styles != nil { output, _ := xml.Marshal(f.Styles) - f.saveFileList("xl/styles.xml", replaceWorkSheetsRelationshipsNameSpaceBytes(output)) + f.saveFileList("xl/styles.xml", replaceRelationshipsNameSpaceBytes(output)) } } -- cgit v1.2.1 From 3ce4b91be96589847823b6c1b6c123ba7880310f Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 31 Mar 2020 00:02:00 +0800 Subject: Resolve #345, fix missing comments by GetComments --- chart.go | 2 +- codelingo.yaml | 3 --- comment.go | 13 +++++++------ comment_test.go | 9 ++++++++- drawing.go | 21 +++++++-------------- drawing_test.go | 27 +++++++++++++++++++++++++++ excelize_test.go | 2 +- 7 files changed, 51 insertions(+), 26 deletions(-) delete mode 100644 codelingo.yaml create mode 100644 drawing_test.go diff --git a/chart.go b/chart.go index cae833d..2c802ee 100644 --- a/chart.go +++ b/chart.go @@ -789,7 +789,7 @@ func (f *File) AddChartSheet(sheet, format string, combo ...string) error { drawingID := f.countDrawings() + 1 chartID := f.countCharts() + 1 drawingXML := "xl/drawings/drawing" + strconv.Itoa(drawingID) + ".xml" - drawingID, drawingXML = f.prepareChartSheetDrawing(&cs, drawingID, sheet, drawingXML) + f.prepareChartSheetDrawing(&cs, drawingID, sheet) drawingRels := "xl/drawings/_rels/drawing" + strconv.Itoa(drawingID) + ".xml.rels" drawingRID := f.addRels(drawingRels, SourceRelationshipChart, "../charts/chart"+strconv.Itoa(chartID)+".xml", "") f.addSheetDrawingChart(drawingXML, drawingRID, &formatSet.Format) diff --git a/codelingo.yaml b/codelingo.yaml deleted file mode 100644 index dfe344b..0000000 --- a/codelingo.yaml +++ /dev/null @@ -1,3 +0,0 @@ -tenets: - - import: codelingo/effective-go - - import: codelingo/code-review-comments diff --git a/comment.go b/comment.go index a5b6085..610eae8 100644 --- a/comment.go +++ b/comment.go @@ -16,6 +16,7 @@ import ( "fmt" "io" "log" + "path/filepath" "strconv" "strings" ) @@ -35,8 +36,8 @@ func parseFormatCommentsSet(formatSet string) (*formatComment, error) { // the worksheet comments. func (f *File) GetComments() (comments map[string][]Comment) { comments = map[string][]Comment{} - for n := range f.sheetMap { - if d := f.commentsReader("xl" + strings.TrimPrefix(f.getSheetComments(f.GetSheetIndex(n)), "..")); d != nil { + for n, path := range f.sheetMap { + if d := f.commentsReader("xl" + strings.TrimPrefix(f.getSheetComments(filepath.Base(path)), "..")); d != nil { sheetComments := []Comment{} for _, comment := range d.CommentList.Comment { sheetComment := Comment{} @@ -60,9 +61,9 @@ func (f *File) GetComments() (comments map[string][]Comment) { } // getSheetComments provides the method to get the target comment reference by -// given worksheet index. -func (f *File) getSheetComments(sheetID int) string { - var rels = "xl/worksheets/_rels/sheet" + strconv.Itoa(sheetID) + ".xml.rels" +// given worksheet file path. +func (f *File) getSheetComments(sheetFile string) string { + var rels = "xl/worksheets/_rels/" + sheetFile + ".rels" if sheetRels := f.relsReader(rels); sheetRels != nil { for _, v := range sheetRels.Relationships { if v.Type == SourceRelationshipComments { @@ -107,7 +108,6 @@ func (f *File) AddComment(sheet, cell, format string) error { f.addSheetLegacyDrawing(sheet, rID) } commentsXML := "xl/comments" + strconv.Itoa(commentID) + ".xml" - f.addComment(commentsXML, cell, formatSet) var colCount int for i, l := range strings.Split(formatSet.Text, "\n") { if ll := len(l); ll > colCount { @@ -121,6 +121,7 @@ func (f *File) AddComment(sheet, cell, format string) error { if err != nil { return err } + f.addComment(commentsXML, cell, formatSet) f.addContentTypePart(commentID, "comments") return err } diff --git a/comment_test.go b/comment_test.go index 5b83162..955d4e8 100644 --- a/comment_test.go +++ b/comment_test.go @@ -29,10 +29,17 @@ func TestAddComments(t *testing.T) { // Test add comment on not exists worksheet. assert.EqualError(t, f.AddComment("SheetN", "B7", `{"author":"Excelize: ","text":"This is a comment."}`), "sheet SheetN is not exist") - + // Test add comment on with illegal cell coordinates + assert.EqualError(t, f.AddComment("Sheet1", "A", `{"author":"Excelize: ","text":"This is a comment."}`), `cannot convert cell "A" to coordinates: invalid cell name "A"`) if assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddComments.xlsx"))) { assert.Len(t, f.GetComments(), 2) } + + f.Comments["xl/comments2.xml"] = nil + f.XLSX["xl/comments2.xml"] = []byte(`Excelize: Excelize: `) + comments := f.GetComments() + assert.EqualValues(t, 2, len(comments["Sheet1"])) + assert.EqualValues(t, 1, len(comments["Sheet2"])) } func TestDecodeVMLDrawingReader(t *testing.T) { diff --git a/drawing.go b/drawing.go index b291d98..13bdab4 100644 --- a/drawing.go +++ b/drawing.go @@ -40,22 +40,15 @@ func (f *File) prepareDrawing(xlsx *xlsxWorksheet, drawingID int, sheet, drawing // prepareChartSheetDrawing provides a function to prepare drawing ID and XML // by given drawingID, worksheet name and default drawingXML. -func (f *File) prepareChartSheetDrawing(xlsx *xlsxChartsheet, drawingID int, sheet, drawingXML string) (int, string) { +func (f *File) prepareChartSheetDrawing(xlsx *xlsxChartsheet, drawingID int, sheet string) { sheetRelationshipsDrawingXML := "../drawings/drawing" + strconv.Itoa(drawingID) + ".xml" - if xlsx.Drawing != nil { - // The worksheet already has a picture or chart relationships, use the relationships drawing ../drawings/drawing%d.xml. - sheetRelationshipsDrawingXML = f.getSheetRelationshipsTargetByID(sheet, xlsx.Drawing.RID) - drawingID, _ = strconv.Atoi(strings.TrimSuffix(strings.TrimPrefix(sheetRelationshipsDrawingXML, "../drawings/drawing"), ".xml")) - drawingXML = strings.Replace(sheetRelationshipsDrawingXML, "..", "xl", -1) - } else { - // Add first picture for given sheet. - sheetRels := "xl/chartsheets/_rels/" + strings.TrimPrefix(f.sheetMap[trimSheetName(sheet)], "xl/chartsheets/") + ".rels" - rID := f.addRels(sheetRels, SourceRelationshipDrawingML, sheetRelationshipsDrawingXML, "") - xlsx.Drawing = &xlsxDrawing{ - RID: "rId" + strconv.Itoa(rID), - } + // Only allow one chart in a chartsheet. + sheetRels := "xl/chartsheets/_rels/" + strings.TrimPrefix(f.sheetMap[trimSheetName(sheet)], "xl/chartsheets/") + ".rels" + rID := f.addRels(sheetRels, SourceRelationshipDrawingML, sheetRelationshipsDrawingXML, "") + xlsx.Drawing = &xlsxDrawing{ + RID: "rId" + strconv.Itoa(rID), } - return drawingID, drawingXML + return } // addChart provides a function to create chart as xl/charts/chart%d.xml by diff --git a/drawing_test.go b/drawing_test.go new file mode 100644 index 0000000..0a380ed --- /dev/null +++ b/drawing_test.go @@ -0,0 +1,27 @@ +// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of +// this source code is governed by a BSD-style license that can be found in +// the LICENSE file. +// +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.10 or later. + +package excelize + +import ( + "testing" +) + +func TestDrawingParser(t *testing.T) { + f := File{ + Drawings: make(map[string]*xlsxWsDr), + XLSX: map[string][]byte{ + "charset": MacintoshCyrillicCharset, + "wsDr": []byte(``)}, + } + // Test with one cell anchor + f.drawingParser("wsDr") + // Test with unsupport charset + f.drawingParser("charset") +} diff --git a/excelize_test.go b/excelize_test.go index 815a08d..e8d3a30 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -934,7 +934,7 @@ func TestCopySheetError(t *testing.T) { func TestGetSheetComments(t *testing.T) { f := NewFile() - assert.Equal(t, "", f.getSheetComments(0)) + assert.Equal(t, "", f.getSheetComments("sheet0")) } func TestSetActiveSheet(t *testing.T) { -- cgit v1.2.1 From 736362694adff47424726a4e4e2569f8247e7fcf Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 1 Apr 2020 00:38:12 +0800 Subject: Add unit test case --- .travis.yml | 1 - file_test.go | 20 ++++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index d94d5d8..92852cf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,6 @@ install: - go get -d -t -v ./... && go build -v ./... go: - - 1.10.x - 1.11.x - 1.12.x - 1.13.x diff --git a/file_test.go b/file_test.go index 8c5050c..e27b754 100644 --- a/file_test.go +++ b/file_test.go @@ -1,7 +1,12 @@ package excelize import ( + "bufio" + "bytes" + "strings" "testing" + + "github.com/stretchr/testify/assert" ) func BenchmarkWrite(b *testing.B) { @@ -26,3 +31,18 @@ func BenchmarkWrite(b *testing.B) { } } } + +func TestWriteTo(t *testing.T) { + f := File{} + buf := bytes.Buffer{} + f.XLSX = make(map[string][]byte, 0) + f.XLSX["/d/"] = []byte("s") + _, err := f.WriteTo(bufio.NewWriter(&buf)) + assert.EqualError(t, err, "zip: write to directory") + delete(f.XLSX, "/d/") + // Test file path overflow + const maxUint16 = 1<<16 - 1 + f.XLSX[strings.Repeat("s", maxUint16+1)] = nil + _, err = f.WriteTo(bufio.NewWriter(&buf)) + assert.EqualError(t, err, "zip: FileHeader.Name too long") +} -- cgit v1.2.1 From 59f6af21a378fdde21422a92b79a7b03bba313d4 Mon Sep 17 00:00:00 2001 From: foxmeder Date: Wed, 1 Apr 2020 15:38:37 +0800 Subject: fix reading wrong string from xml such as below 0 --- rows.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/rows.go b/rows.go index 76dd0f0..d56c81c 100644 --- a/rows.go +++ b/rows.go @@ -301,10 +301,12 @@ func (f *File) sharedStringsReader() *xlsxSST { func (xlsx *xlsxC) getValueFrom(f *File, d *xlsxSST) (string, error) { switch xlsx.T { case "s": - xlsxSI := 0 - xlsxSI, _ = strconv.Atoi(xlsx.V) - if len(d.SI) > xlsxSI { - return f.formattedValue(xlsx.S, d.SI[xlsxSI].String()), nil + if xlsx.V != "" { + xlsxSI := 0 + xlsxSI, _ = strconv.Atoi(xlsx.V) + if len(d.SI) > xlsxSI { + return f.formattedValue(xlsx.S, d.SI[xlsxSI].String()), nil + } } return f.formattedValue(xlsx.S, xlsx.V), nil case "str": -- cgit v1.2.1 From 0f2a9053246c3ae45e6c7ba911a1fb135664abdf Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 2 Apr 2020 00:41:14 +0800 Subject: Performance improvements --- adjust.go | 5 +++-- drawing.go | 2 +- excelize.go | 5 ++--- lib.go | 42 ++++++++++++++++++++++++++++++++++++++++-- lib_test.go | 5 +++++ picture.go | 2 +- rows.go | 14 ++++++++------ sheet.go | 6 +++--- sparkline.go | 2 +- stream.go | 2 +- 10 files changed, 65 insertions(+), 20 deletions(-) diff --git a/adjust.go b/adjust.go index bedeec0..5056839 100644 --- a/adjust.go +++ b/adjust.go @@ -80,9 +80,10 @@ func (f *File) adjustColDimensions(xlsx *xlsxWorksheet, col, offset int) { // 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 { + for i := range xlsx.SheetData.Row { + r := &xlsx.SheetData.Row[i] if newRow := r.R + offset; r.R >= row && newRow > 0 { - f.ajustSingleRowDimensions(&xlsx.SheetData.Row[i], newRow) + f.ajustSingleRowDimensions(r, newRow) } } } diff --git a/drawing.go b/drawing.go index 13bdab4..e410599 100644 --- a/drawing.go +++ b/drawing.go @@ -1288,7 +1288,7 @@ func (f *File) deleteDrawing(col, row int, drawingXML, drawingType string) (err } for idx := 0; idx < len(wsDr.TwoCellAnchor); idx++ { deTwoCellAnchor = new(decodeTwoCellAnchor) - if err = f.xmlNewDecoder(bytes.NewReader([]byte("" + wsDr.TwoCellAnchor[idx].GraphicFrame + ""))). + if err = f.xmlNewDecoder(bytes.NewReader(stringToBytes("" + wsDr.TwoCellAnchor[idx].GraphicFrame + ""))). Decode(deTwoCellAnchor); err != nil && err != io.EOF { err = fmt.Errorf("xml decode error: %s", err) return diff --git a/excelize.go b/excelize.go index 520cbb7..ae011d9 100644 --- a/excelize.go +++ b/excelize.go @@ -234,10 +234,9 @@ func (f *File) addRels(relPath, relType, target, targetMode string) int { // replaceRelationshipsNameSpaceBytes provides a function to replace // XML tags to self-closing for compatible Microsoft Office Excel 2007. func replaceRelationshipsNameSpaceBytes(contentMarshal []byte) []byte { - var oldXmlns = []byte(` xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">`) + var oldXmlns = stringToBytes(` xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">`) var newXmlns = []byte(templateNamespaceIDMap) - contentMarshal = bytes.Replace(contentMarshal, oldXmlns, newXmlns, -1) - return contentMarshal + return bytesReplace(contentMarshal, oldXmlns, newXmlns, -1) } // UpdateLinkedValue fix linked values within a spreadsheet are not updating in diff --git a/lib.go b/lib.go index 2d606fa..83cdb4a 100644 --- a/lib.go +++ b/lib.go @@ -17,6 +17,7 @@ import ( "log" "strconv" "strings" + "unsafe" ) // ReadZipReader can be used to read an XLSX in memory without touching the @@ -103,7 +104,7 @@ func JoinCellName(col string, row int) (string, error) { if row < 1 { return "", newInvalidRowNumberError(row) } - return fmt.Sprintf("%s%d", normCol, row), nil + return normCol + strconv.Itoa(row), nil } // ColumnNameToNumber provides a function to convert Excel sheet column name @@ -190,6 +191,7 @@ func CoordinatesToCellName(col, row int) (string, error) { } colname, err := ColumnNumberToName(col) if err != nil { + // Error should never happens here. return "", fmt.Errorf("invalid cell coordinates [%d, %d]: %v", col, row, err) } return fmt.Sprintf("%s%d", colname, row), nil @@ -235,11 +237,47 @@ func namespaceStrictToTransitional(content []byte) []byte { StrictNameSpaceSpreadSheet: NameSpaceSpreadSheet, } for s, n := range namespaceTranslationDic { - content = bytes.Replace(content, []byte(s), []byte(n), -1) + content = bytesReplace(content, stringToBytes(s), stringToBytes(n), -1) } return content } +// stringToBytes cast a string to bytes pointer and assign the value of this +// pointer. +func stringToBytes(s string) []byte { + return *(*[]byte)(unsafe.Pointer(&s)) +} + +// bytesReplace replace old bytes with given new. +func bytesReplace(s, old, new []byte, n int) []byte { + if n == 0 { + return s + } + + if len(old) < len(new) { + return bytes.Replace(s, old, new, n) + } + + if n < 0 { + n = len(s) + } + + var wid, i, j, w int + for i, j = 0, 0; i < len(s) && j < n; j++ { + wid = bytes.Index(s[i:], old) + if wid < 0 { + break + } + + w += copy(s[w:], s[i:i+wid]) + w += copy(s[w:], new) + i += wid + len(old) + } + + w += copy(s[w:], s[i:]) + return s[0:w] +} + // genSheetPasswd provides a method to generate password for worksheet // protection by given plaintext. When an Excel sheet is being protected with // a password, a 16-bit (two byte) long hash is generated. To verify a diff --git a/lib_test.go b/lib_test.go index 1c30c0e..4605e70 100644 --- a/lib_test.go +++ b/lib_test.go @@ -203,3 +203,8 @@ func TestCoordinatesToCellName_Error(t *testing.T) { } } } + +func TestBytesReplace(t *testing.T) { + s := []byte{0x01} + assert.EqualValues(t, s, bytesReplace(s, []byte{}, []byte{}, 0)) +} diff --git a/picture.go b/picture.go index ddc0480..fcdaa07 100644 --- a/picture.go +++ b/picture.go @@ -510,7 +510,7 @@ func (f *File) getPicture(row, col int, drawingXML, drawingRelationships string) err = nil for _, anchor := range deWsDr.TwoCellAnchor { deTwoCellAnchor = new(decodeTwoCellAnchor) - if err = f.xmlNewDecoder(bytes.NewReader([]byte("" + anchor.Content + ""))). + if err = f.xmlNewDecoder(bytes.NewReader(stringToBytes("" + anchor.Content + ""))). Decode(deTwoCellAnchor); err != nil && err != io.EOF { err = fmt.Errorf("xml decode error: %s", err) return diff --git a/rows.go b/rows.go index d56c81c..8f000b3 100644 --- a/rows.go +++ b/rows.go @@ -424,14 +424,16 @@ func (f *File) RemoveRow(sheet string, row int) error { if row > len(xlsx.SheetData.Row) { return f.adjustHelper(sheet, rows, row, -1) } - for rowIdx := range xlsx.SheetData.Row { - if xlsx.SheetData.Row[rowIdx].R == row { - xlsx.SheetData.Row = append(xlsx.SheetData.Row[:rowIdx], - xlsx.SheetData.Row[rowIdx+1:]...)[:len(xlsx.SheetData.Row)-1] - return f.adjustHelper(sheet, rows, row, -1) + keep := 0 + for rowIdx := 0; rowIdx < len(xlsx.SheetData.Row); rowIdx++ { + v := &xlsx.SheetData.Row[rowIdx] + if v.R != row { + xlsx.SheetData.Row[keep] = *v + keep++ } } - return nil + xlsx.SheetData.Row = xlsx.SheetData.Row[:keep] + return f.adjustHelper(sheet, rows, row, -1) } // InsertRow provides a function to insert a new row after given Excel row diff --git a/sheet.go b/sheet.go index 6ddd629..a3276c2 100644 --- a/sheet.go +++ b/sheet.go @@ -206,9 +206,9 @@ func (f *File) setAppXML() { // requirements about the structure of the input XML. This function is a // horrible hack to fix that after the XML marshalling is completed. func replaceRelationshipsBytes(content []byte) []byte { - oldXmlns := []byte(`xmlns:relationships="http://schemas.openxmlformats.org/officeDocument/2006/relationships" relationships`) - newXmlns := []byte("r") - return bytes.Replace(content, oldXmlns, newXmlns, -1) + oldXmlns := stringToBytes(`xmlns:relationships="http://schemas.openxmlformats.org/officeDocument/2006/relationships" relationships`) + newXmlns := stringToBytes("r") + return bytesReplace(content, oldXmlns, newXmlns, -1) } // SetActiveSheet provides function to set default active worksheet of XLSX by diff --git a/sparkline.go b/sparkline.go index ef99da6..f1e1f40 100644 --- a/sparkline.go +++ b/sparkline.go @@ -516,7 +516,7 @@ func (f *File) appendSparkline(ws *xlsxWorksheet, group *xlsxX14SparklineGroup, for idx, ext = range decodeExtLst.Ext { if ext.URI == ExtURISparklineGroups { decodeSparklineGroups = new(decodeX14SparklineGroups) - if err = f.xmlNewDecoder(bytes.NewReader([]byte(ext.Content))). + if err = f.xmlNewDecoder(bytes.NewReader(stringToBytes(ext.Content))). Decode(decodeSparklineGroups); err != nil && err != io.EOF { return } diff --git a/stream.go b/stream.go index 98cf828..1af0b9f 100644 --- a/stream.go +++ b/stream.go @@ -365,7 +365,7 @@ func writeCell(buf *bufferedWriter, c xlsxC) { buf.WriteString(`>`) if c.V != "" { buf.WriteString(``) - xml.EscapeText(buf, []byte(c.V)) + xml.EscapeText(buf, stringToBytes(c.V)) buf.WriteString(``) } buf.WriteString(``) -- cgit v1.2.1 From 66d0272f6af59b5f0c97a304379a795420a43e8b Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 6 Apr 2020 00:23:27 +0800 Subject: Resolve #172, init rich text support --- cell.go | 166 ++++++++++++++++++++++++++++++++++++++++++++++++++++ cell_test.go | 81 +++++++++++++++++++++++++ comment.go | 8 ++- file.go | 1 + picture.go | 30 +++++----- styles.go | 14 ++++- styles_test.go | 2 + xmlDrawing.go | 2 + xmlSharedStrings.go | 59 ++++++++++++++----- 9 files changed, 331 insertions(+), 32 deletions(-) diff --git a/cell.go b/cell.go index a659680..95cfbbf 100644 --- a/cell.go +++ b/cell.go @@ -457,6 +457,172 @@ func (f *File) SetCellHyperLink(sheet, axis, link, linkType string) error { return nil } +// SetCellRichText provides a function to set cell with rich text by given +// worksheet. For example: +// +// package main +// +// import ( +// "fmt" +// +// "github.com/360EntSecGroup-Skylar/excelize" +// ) +// +// func main() { +// f := excelize.NewFile() +// if err := f.SetRowHeight("Sheet1", 1, 35); err != nil { +// fmt.Println(err) +// return +// } +// if err := f.SetColWidth("Sheet1", "A", "A", 44); err != nil { +// fmt.Println(err) +// return +// } +// if err := f.SetCellRichText("Sheet1", "A1", []excelize.RichTextRun{ +// { +// Text: "blod", +// Font: &excelize.Font{ +// Bold: true, +// Color: "2354e8", +// Family: "Times New Roman", +// }, +// }, +// { +// Text: " and ", +// Font: &excelize.Font{ +// Family: "Times New Roman", +// }, +// }, +// { +// Text: " italic", +// Font: &excelize.Font{ +// Bold: true, +// Color: "e83723", +// Italic: true, +// Family: "Times New Roman", +// }, +// }, +// { +// Text: "text with color and font-family,", +// Font: &excelize.Font{ +// Bold: true, +// Color: "2354e8", +// Family: "Times New Roman", +// }, +// }, +// { +// Text: "\r\nlarge text with ", +// Font: &excelize.Font{ +// Size: 14, +// Color: "ad23e8", +// }, +// }, +// { +// Text: "strike", +// Font: &excelize.Font{ +// Color: "e89923", +// Strike: true, +// }, +// }, +// { +// Text: " and ", +// Font: &excelize.Font{ +// Size: 14, +// Color: "ad23e8", +// }, +// }, +// { +// Text: "underline.", +// Font: &excelize.Font{ +// Color: "23e833", +// Underline: "single", +// }, +// }, +// }); err != nil { +// fmt.Println(err) +// return +// } +// style, err := f.NewStyle(&excelize.Style{ +// Alignment: &excelize.Alignment{ +// WrapText: true, +// }, +// }) +// if err != nil { +// fmt.Println(err) +// return +// } +// if err := f.SetCellStyle("Sheet1", "A1", "A1", style); err != nil { +// fmt.Println(err) +// return +// } +// if err := f.SaveAs("Book1.xlsx"); err != nil { +// fmt.Println(err) +// } +// } +// +func (f *File) SetCellRichText(sheet, cell string, runs []RichTextRun) error { + ws, err := f.workSheetReader(sheet) + if err != nil { + return err + } + cellData, col, _, err := f.prepareCell(ws, sheet, cell) + if err != nil { + return err + } + cellData.S = f.prepareCellStyle(ws, col, cellData.S) + si := xlsxSI{} + sst := f.sharedStringsReader() + textRuns := []xlsxR{} + for _, textRun := range runs { + run := xlsxR{T: &xlsxT{Val: textRun.Text}} + if strings.ContainsAny(textRun.Text, "\r\n ") { + run.T.Space = "preserve" + } + fnt := textRun.Font + if fnt != nil { + rpr := xlsxRPr{} + if fnt.Bold { + rpr.B = " " + } + if fnt.Italic { + rpr.I = " " + } + if fnt.Strike { + rpr.Strike = " " + } + if fnt.Underline != "" { + rpr.U = &attrValString{Val: &fnt.Underline} + } + if fnt.Family != "" { + rpr.RFont = &attrValString{Val: &fnt.Family} + } + if fnt.Size > 0.0 { + rpr.Sz = &attrValFloat{Val: &fnt.Size} + } + if fnt.Color != "" { + rpr.Color = &xlsxColor{RGB: getPaletteColor(fnt.Color)} + } + run.RPr = &rpr + } + textRuns = append(textRuns, run) + } + si.R = textRuns + sst.SI = append(sst.SI, si) + sst.Count++ + sst.UniqueCount++ + cellData.T, cellData.V = "s", strconv.Itoa(len(sst.SI)-1) + f.addContentTypePart(0, "sharedStrings") + rels := f.relsReader("xl/_rels/workbook.xml.rels") + for _, rel := range rels.Relationships { + if rel.Target == "sharedStrings.xml" { + return err + } + } + // Update xl/_rels/workbook.xml.rels + f.addRels("xl/_rels/workbook.xml.rels", SourceRelationshipSharedStrings, "sharedStrings.xml", "") + return err +} + // 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: diff --git a/cell_test.go b/cell_test.go index 1efbc5a..f46b4b9 100644 --- a/cell_test.go +++ b/cell_test.go @@ -141,3 +141,84 @@ func TestOverflowNumericCell(t *testing.T) { // GOARCH=amd64 - all ok; GOARCH=386 - actual: "-2147483648" assert.Equal(t, "8595602512225", val, "A1 should be 8595602512225") } + +func TestSetCellRichText(t *testing.T) { + f := NewFile() + assert.NoError(t, f.SetRowHeight("Sheet1", 1, 35)) + assert.NoError(t, f.SetColWidth("Sheet1", "A", "A", 44)) + richTextRun := []RichTextRun{ + { + Text: "blod", + Font: &Font{ + Bold: true, + Color: "2354e8", + Family: "Times New Roman", + }, + }, + { + Text: " and ", + Font: &Font{ + Family: "Times New Roman", + }, + }, + { + Text: "italic ", + Font: &Font{ + Bold: true, + Color: "e83723", + Italic: true, + Family: "Times New Roman", + }, + }, + { + Text: "text with color and font-family,", + Font: &Font{ + Bold: true, + Color: "2354e8", + Family: "Times New Roman", + }, + }, + { + Text: "\r\nlarge text with ", + Font: &Font{ + Size: 14, + Color: "ad23e8", + }, + }, + { + Text: "strike", + Font: &Font{ + Color: "e89923", + Strike: true, + }, + }, + { + Text: " and ", + Font: &Font{ + Size: 14, + Color: "ad23e8", + }, + }, + { + Text: "underline.", + Font: &Font{ + Color: "23e833", + Underline: "single", + }, + }, + } + assert.NoError(t, f.SetCellRichText("Sheet1", "A1", richTextRun)) + assert.NoError(t, f.SetCellRichText("Sheet1", "A2", richTextRun)) + style, err := f.NewStyle(&Style{ + Alignment: &Alignment{ + WrapText: true, + }, + }) + assert.NoError(t, err) + assert.NoError(t, f.SetCellStyle("Sheet1", "A1", "A1", style)) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellRichText.xlsx"))) + // Test set cell rich text on not exists worksheet + assert.EqualError(t, f.SetCellRichText("SheetN", "A1", richTextRun), "sheet SheetN is not exist") + // Test set cell rich text with illegal cell coordinates + assert.EqualError(t, f.SetCellRichText("Sheet1", "A", richTextRun), `cannot convert cell "A" to coordinates: invalid cell name "A"`) +} diff --git a/comment.go b/comment.go index 610eae8..e224502 100644 --- a/comment.go +++ b/comment.go @@ -50,7 +50,9 @@ func (f *File) GetComments() (comments map[string][]Comment) { sheetComment.Text += *comment.Text.T } for _, text := range comment.Text.R { - sheetComment.Text += text.T + if text.T != nil { + sheetComment.Text += text.T.Val + } } sheetComments = append(sheetComments, sheetComment) } @@ -263,7 +265,7 @@ func (f *File) addComment(commentsXML, cell string, formatSet *formatComment) { RFont: &attrValString{Val: stringPtr(defaultFont)}, Family: &attrValInt{Val: intPtr(2)}, }, - T: a, + T: &xlsxT{Val: a}, }, { RPr: &xlsxRPr{ @@ -274,7 +276,7 @@ func (f *File) addComment(commentsXML, cell string, formatSet *formatComment) { RFont: &attrValString{Val: stringPtr(defaultFont)}, Family: &attrValInt{Val: intPtr(2)}, }, - T: t, + T: &xlsxT{Val: t}, }, }, }, diff --git a/file.go b/file.go index 6213bb1..8fe4115 100644 --- a/file.go +++ b/file.go @@ -97,6 +97,7 @@ func (f *File) WriteToBuffer() (*bytes.Buffer, error) { f.workBookWriter() f.workSheetWriter() f.relsWriter() + f.sharedStringsWriter() f.styleSheetWriter() for path, content := range f.XLSX { diff --git a/picture.go b/picture.go index fcdaa07..a6c0f47 100644 --- a/picture.go +++ b/picture.go @@ -367,22 +367,24 @@ func (f *File) addContentTypePart(index int, contentType string) { "drawings": f.setContentTypePartImageExtensions, } partNames := map[string]string{ - "chart": "/xl/charts/chart" + strconv.Itoa(index) + ".xml", - "chartsheet": "/xl/chartsheets/sheet" + strconv.Itoa(index) + ".xml", - "comments": "/xl/comments" + strconv.Itoa(index) + ".xml", - "drawings": "/xl/drawings/drawing" + strconv.Itoa(index) + ".xml", - "table": "/xl/tables/table" + strconv.Itoa(index) + ".xml", - "pivotTable": "/xl/pivotTables/pivotTable" + strconv.Itoa(index) + ".xml", - "pivotCache": "/xl/pivotCache/pivotCacheDefinition" + strconv.Itoa(index) + ".xml", + "chart": "/xl/charts/chart" + strconv.Itoa(index) + ".xml", + "chartsheet": "/xl/chartsheets/sheet" + strconv.Itoa(index) + ".xml", + "comments": "/xl/comments" + strconv.Itoa(index) + ".xml", + "drawings": "/xl/drawings/drawing" + strconv.Itoa(index) + ".xml", + "table": "/xl/tables/table" + strconv.Itoa(index) + ".xml", + "pivotTable": "/xl/pivotTables/pivotTable" + strconv.Itoa(index) + ".xml", + "pivotCache": "/xl/pivotCache/pivotCacheDefinition" + strconv.Itoa(index) + ".xml", + "sharedStrings": "/xl/sharedStrings.xml", } contentTypes := map[string]string{ - "chart": ContentTypeDrawingML, - "chartsheet": ContentTypeSpreadSheetMLChartsheet, - "comments": ContentTypeSpreadSheetMLComments, - "drawings": ContentTypeDrawing, - "table": ContentTypeSpreadSheetMLTable, - "pivotTable": ContentTypeSpreadSheetMLPivotTable, - "pivotCache": ContentTypeSpreadSheetMLPivotCacheDefinition, + "chart": ContentTypeDrawingML, + "chartsheet": ContentTypeSpreadSheetMLChartsheet, + "comments": ContentTypeSpreadSheetMLComments, + "drawings": ContentTypeDrawing, + "table": ContentTypeSpreadSheetMLTable, + "pivotTable": ContentTypeSpreadSheetMLPivotTable, + "pivotCache": ContentTypeSpreadSheetMLPivotCacheDefinition, + "sharedStrings": ContentTypeSpreadSheetMLSharedStrings, } s, ok := setContentType[contentType] if ok { diff --git a/styles.go b/styles.go index 9cf974e..fe2bed5 100644 --- a/styles.go +++ b/styles.go @@ -13,6 +13,7 @@ import ( "bytes" "encoding/json" "encoding/xml" + "errors" "fmt" "io" "log" @@ -1022,6 +1023,15 @@ func (f *File) styleSheetWriter() { } } +// sharedStringsWriter provides a function to save xl/sharedStrings.xml after +// serialize structure. +func (f *File) sharedStringsWriter() { + if f.SharedStrings != nil { + output, _ := xml.Marshal(f.SharedStrings) + f.saveFileList("xl/sharedStrings.xml", replaceRelationshipsNameSpaceBytes(output)) + } +} + // parseFormatStyleSet provides a function to parse the format settings of the // cells and conditional formats. func parseFormatStyleSet(style string) (*Style, error) { @@ -1033,7 +1043,7 @@ func parseFormatStyleSet(style string) (*Style, error) { } // NewStyle provides a function to create the style for cells by given JSON or -// structure. Note that the color field uses RGB color code. +// structure pointer. Note that the color field uses RGB color code. // // The following shows the border styles sorted by excelize index number: // @@ -1906,6 +1916,8 @@ func (f *File) NewStyle(style interface{}) (int, error) { } case *Style: fs = v + default: + return cellXfsID, errors.New("invalid parameter type") } s := f.stylesReader() numFmtID := setNumFmt(s, fs) diff --git a/styles_test.go b/styles_test.go index 5a9a771..5681c95 100644 --- a/styles_test.go +++ b/styles_test.go @@ -193,6 +193,8 @@ func TestNewStyle(t *testing.T) { assert.Equal(t, 2, styles.CellXfs.Count, "Should have 2 styles") _, err = f.NewStyle(&Style{}) assert.NoError(t, err) + _, err = f.NewStyle(Style{}) + assert.EqualError(t, err, "invalid parameter type") } func TestGetDefaultFont(t *testing.T) { diff --git a/xmlDrawing.go b/xmlDrawing.go index 142121d..191631b 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -25,6 +25,7 @@ const ( SourceRelationshipChartsheet = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chartsheet" SourceRelationshipPivotTable = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotTable" SourceRelationshipPivotCache = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheDefinition" + SourceRelationshipSharedStrings = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings" SourceRelationshipVBAProject = "http://schemas.microsoft.com/office/2006/relationships/vbaProject" SourceRelationshipChart201506 = "http://schemas.microsoft.com/office/drawing/2015/06/chart" SourceRelationshipChart20070802 = "http://schemas.microsoft.com/office/drawing/2007/8/2/chart" @@ -55,6 +56,7 @@ const ( ContentTypeSpreadSheetMLComments = "application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml" ContentTypeSpreadSheetMLPivotCacheDefinition = "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheDefinition+xml" ContentTypeSpreadSheetMLPivotTable = "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTable+xml" + ContentTypeSpreadSheetMLSharedStrings = "application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml" ContentTypeSpreadSheetMLTable = "application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml" ContentTypeSpreadSheetMLWorksheet = "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml" ContentTypeVBA = "application/vnd.ms-office.vbaProject" diff --git a/xmlSharedStrings.go b/xmlSharedStrings.go index 61e5727..a6525df 100644 --- a/xmlSharedStrings.go +++ b/xmlSharedStrings.go @@ -28,31 +28,46 @@ type xlsxSST struct { SI []xlsxSI `xml:"si"` } -// xlsxSI directly maps the si element from the namespace -// http://schemas.openxmlformats.org/spreadsheetml/2006/main - currently I have -// not checked this for completeness - it does as much as I need. +// xlsxSI (String Item) is the representation of an individual string in the +// Shared String table. If the string is just a simple string with formatting +// applied at the cell level, then the String Item (si) should contain a +// single text element used to express the string. However, if the string in +// the cell is more complex - i.e., has formatting applied at the character +// level - then the string item shall consist of multiple rich text runs which +// collectively are used to express the string. type xlsxSI struct { - T string `xml:"t"` + T string `xml:"t,omitempty"` R []xlsxR `xml:"r"` } +// String extracts characters from a string item. func (x xlsxSI) String() string { if len(x.R) > 0 { var rows strings.Builder for _, s := range x.R { - rows.WriteString(s.T) + if s.T != nil { + rows.WriteString(s.T.Val) + } } return rows.String() } return x.T } -// xlsxR directly maps the r element from the namespace -// http://schemas.openxmlformats.org/spreadsheetml/2006/main - currently I have -// not checked this for completeness - it does as much as I need. +// xlsxR represents a run of rich text. A rich text run is a region of text +// that share a common set of properties, such as formatting properties. The +// properties are defined in the rPr element, and the text displayed to the +// user is defined in the Text (t) element. type xlsxR struct { RPr *xlsxRPr `xml:"rPr"` - T string `xml:"t"` + T *xlsxT `xml:"t"` +} + +// xlsxT directly maps the t element in the run properties. +type xlsxT struct { + XMLName xml.Name `xml:"t"` + Space string `xml:"xml:space,attr,omitempty"` + Val string `xml:",innerxml"` } // xlsxRPr (Run Properties) specifies a set of run properties which shall be @@ -61,9 +76,25 @@ type xlsxR struct { // they are directly applied to the run and supersede any formatting from // styles. type xlsxRPr struct { - B string `xml:"b,omitempty"` - Sz *attrValFloat `xml:"sz"` - Color *xlsxColor `xml:"color"` - RFont *attrValString `xml:"rFont"` - Family *attrValInt `xml:"family"` + RFont *attrValString `xml:"rFont"` + Charset *attrValInt `xml:"charset"` + Family *attrValInt `xml:"family"` + B string `xml:"b,omitempty"` + I string `xml:"i,omitempty"` + Strike string `xml:"strike,omitempty"` + Outline string `xml:"outline,omitempty"` + Shadow string `xml:"shadow,omitempty"` + Condense string `xml:"condense,omitempty"` + Extend string `xml:"extend,omitempty"` + Color *xlsxColor `xml:"color"` + Sz *attrValFloat `xml:"sz"` + U *attrValString `xml:"u"` + VertAlign *attrValString `xml:"vertAlign"` + Scheme *attrValString `xml:"scheme"` +} + +// RichTextRun directly maps the settings of the rich text run. +type RichTextRun struct { + Font *Font + Text string } -- cgit v1.2.1 From 6e90fa6b1d00b2c4ce85e79ee4054ee847fbbc87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20Mengu=C3=A9?= Date: Wed, 8 Apr 2020 18:49:13 +0200 Subject: Replace bytes.NewReader(stringToBytes(s)) with strings.NewReader(s) (#610) --- drawing.go | 2 +- picture.go | 2 +- sparkline.go | 5 ++--- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/drawing.go b/drawing.go index e410599..7c09d4d 100644 --- a/drawing.go +++ b/drawing.go @@ -1288,7 +1288,7 @@ func (f *File) deleteDrawing(col, row int, drawingXML, drawingType string) (err } for idx := 0; idx < len(wsDr.TwoCellAnchor); idx++ { deTwoCellAnchor = new(decodeTwoCellAnchor) - if err = f.xmlNewDecoder(bytes.NewReader(stringToBytes("" + wsDr.TwoCellAnchor[idx].GraphicFrame + ""))). + if err = f.xmlNewDecoder(strings.NewReader("" + wsDr.TwoCellAnchor[idx].GraphicFrame + "")). Decode(deTwoCellAnchor); err != nil && err != io.EOF { err = fmt.Errorf("xml decode error: %s", err) return diff --git a/picture.go b/picture.go index a6c0f47..306a582 100644 --- a/picture.go +++ b/picture.go @@ -512,7 +512,7 @@ func (f *File) getPicture(row, col int, drawingXML, drawingRelationships string) err = nil for _, anchor := range deWsDr.TwoCellAnchor { deTwoCellAnchor = new(decodeTwoCellAnchor) - if err = f.xmlNewDecoder(bytes.NewReader(stringToBytes("" + anchor.Content + ""))). + if err = f.xmlNewDecoder(strings.NewReader("" + anchor.Content + "")). Decode(deTwoCellAnchor); err != nil && err != io.EOF { err = fmt.Errorf("xml decode error: %s", err) return diff --git a/sparkline.go b/sparkline.go index f1e1f40..ce5be4c 100644 --- a/sparkline.go +++ b/sparkline.go @@ -10,7 +10,6 @@ package excelize import ( - "bytes" "encoding/xml" "errors" "io" @@ -509,14 +508,14 @@ func (f *File) appendSparkline(ws *xlsxWorksheet, group *xlsxX14SparklineGroup, sparklineGroupsBytes, sparklineGroupBytes, extLstBytes []byte ) decodeExtLst = new(decodeWorksheetExt) - if err = f.xmlNewDecoder(bytes.NewReader([]byte("" + ws.ExtLst.Ext + ""))). + if err = f.xmlNewDecoder(strings.NewReader("" + ws.ExtLst.Ext + "")). Decode(decodeExtLst); err != nil && err != io.EOF { return } for idx, ext = range decodeExtLst.Ext { if ext.URI == ExtURISparklineGroups { decodeSparklineGroups = new(decodeX14SparklineGroups) - if err = f.xmlNewDecoder(bytes.NewReader(stringToBytes(ext.Content))). + if err = f.xmlNewDecoder(strings.NewReader(ext.Content)). Decode(decodeSparklineGroups); err != nil && err != io.EOF { return } -- cgit v1.2.1 From a2e1da8d9d90ed71a33523cfe2c5231cd0b5fad9 Mon Sep 17 00:00:00 2001 From: echarlus Date: Wed, 8 Apr 2020 18:50:20 +0200 Subject: Fix for issue #608 (#609) --- styles.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/styles.go b/styles.go index fe2bed5..61b8e53 100644 --- a/styles.go +++ b/styles.go @@ -2732,7 +2732,7 @@ func drawCondFmtCellIs(p int, ct string, format *formatConditional) *xlsxCfRule c.Formula = append(c.Formula, format.Minimum) c.Formula = append(c.Formula, format.Maximum) } - _, ok = map[string]bool{"equal": true, "notEqual": true, "greaterThan": true, "lessThan": true}[ct] + _, ok = map[string]bool{"equal": true, "notEqual": true, "greaterThan": true, "lessThan": true, "greaterThanOrEqual": true, "lessThanOrEqual": true, "containsText": true, "notContains": true, "beginsWith": true, "endsWith": true}[ct] if ok { c.Formula = append(c.Formula, format.Value) } -- cgit v1.2.1 From e36650f4ffd3e305d2c3834620f97ec382cf6faf Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 9 Apr 2020 01:00:14 +0800 Subject: Resolve #598, filter support for AddPivotTable --- cell.go | 3 ++- pivotTable.go | 29 +++++++++++++++++++++++++++++ pivotTable_test.go | 1 + xmlPivotTable.go | 6 +++--- 4 files changed, 35 insertions(+), 4 deletions(-) diff --git a/cell.go b/cell.go index 95cfbbf..8e7ede1 100644 --- a/cell.go +++ b/cell.go @@ -458,7 +458,8 @@ func (f *File) SetCellHyperLink(sheet, axis, link, linkType string) error { } // SetCellRichText provides a function to set cell with rich text by given -// worksheet. For example: +// worksheet. For example, set rich text on the A1 cell of the worksheet named +// Sheet1: // // package main // diff --git a/pivotTable.go b/pivotTable.go index b7dc859..7061bfe 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -25,6 +25,7 @@ type PivotTableOption struct { Rows []PivotTableField Columns []PivotTableField Data []PivotTableField + Filter []PivotTableField } // PivotTableField directly maps the field settings of the pivot table. @@ -86,6 +87,7 @@ type PivotTableField struct { // DataRange: "Sheet1!$A$1:$E$31", // PivotTableRange: "Sheet1!$G$2:$M$34", // Rows: []excelize.PivotTableField{{Data: "Month"}, {Data: "Year"}}, +// Filter: []excelize.PivotTableField{{Data: "Region"}}, // Columns: []excelize.PivotTableField{{Data: "Type"}}, // Data: []excelize.PivotTableField{{Data: "Sales", Name: "Summarize", Subtotal: "Sum"}}, // }); err != nil { @@ -283,6 +285,7 @@ func (f *File) addPivotTable(cacheID, pivotTableID int, pivotTableXML string, op Count: 1, I: []*xlsxI{{}}, }, + PageFields: &xlsxPageFields{}, DataFields: &xlsxDataFields{}, PivotTableStyleInfo: &xlsxPivotTableStyleInfo{ Name: "PivotStyleLight16", @@ -320,6 +323,19 @@ func (f *File) addPivotTable(cacheID, pivotTableID int, pivotTableXML string, op return err } + // page fields + pageFieldsIndex, err := f.getPivotFieldsIndex(opt.Filter, opt) + if err != nil { + return err + } + pageFieldsName := f.getPivotTableFieldsName(opt.Filter) + for idx, pageField := range pageFieldsIndex { + pt.PageFields.PageField = append(pt.PageFields.PageField, &xlsxPageField{ + Name: pageFieldsName[idx], + Fld: pageField, + }) + } + // data fields dataFieldsIndex, err := f.getPivotFieldsIndex(opt.Data, opt) if err != nil { @@ -412,6 +428,19 @@ func (f *File) addPivotFields(pt *xlsxPivotTableDefinition, opt *PivotTableOptio }) continue } + if inPivotTableField(opt.Filter, name) != -1 { + pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, &xlsxPivotField{ + Axis: "axisPage", + Name: f.getPivotTableFieldName(name, opt.Columns), + Items: &xlsxItems{ + Count: 1, + Item: []*xlsxItem{ + {T: "default"}, + }, + }, + }) + continue + } if inPivotTableField(opt.Columns, name) != -1 { pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, &xlsxPivotField{ Axis: "axisCol", diff --git a/pivotTable_test.go b/pivotTable_test.go index 4379538..767206b 100644 --- a/pivotTable_test.go +++ b/pivotTable_test.go @@ -29,6 +29,7 @@ func TestAddPivotTable(t *testing.T) { DataRange: "Sheet1!$A$1:$E$31", PivotTableRange: "Sheet1!$G$2:$M$34", Rows: []PivotTableField{{Data: "Month"}, {Data: "Year"}}, + Filter: []PivotTableField{{Data: "Region"}}, Columns: []PivotTableField{{Data: "Type"}}, Data: []PivotTableField{{Data: "Sales", Subtotal: "Sum", Name: "Summarize by Sum"}}, })) diff --git a/xmlPivotTable.go b/xmlPivotTable.go index 82bbf27..2eff026 100644 --- a/xmlPivotTable.go +++ b/xmlPivotTable.go @@ -251,9 +251,9 @@ type xlsxPageFields struct { type xlsxPageField struct { Fld int `xml:"fld,attr"` Item int `xml:"item,attr,omitempty"` - Hier int `xml:"hier,attr"` - Name string `xml:"name,attr"` - Cap string `xml:"cap,attr"` + Hier int `xml:"hier,attr,omitempty"` + Name string `xml:"name,attr,omitempty"` + Cap string `xml:"cap,attr,omitempty"` ExtLst *xlsxExtLst `xml:"extLst"` } -- cgit v1.2.1 From 10115b5d889bb229d8707803b9413b20fe798588 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 10 Apr 2020 00:04:23 +0800 Subject: - Resolve #611, fix failure BenchmarkSetCellValue - Allow empty filter, data, and rows in the pivot table - Add more test case for pivot table --- cell_test.go | 5 ++-- pivotTable.go | 68 ++++++++++++++++++++++++++++++++++++------------------ pivotTable_test.go | 40 ++++++++++++++++++++++++++++++++ 3 files changed, 89 insertions(+), 24 deletions(-) diff --git a/cell_test.go b/cell_test.go index f46b4b9..45e2f24 100644 --- a/cell_test.go +++ b/cell_test.go @@ -3,6 +3,7 @@ package excelize import ( "fmt" "path/filepath" + "strconv" "testing" "time" @@ -122,9 +123,9 @@ func BenchmarkSetCellValue(b *testing.B) { cols := []string{"A", "B", "C", "D", "E", "F"} f := NewFile() b.ResetTimer() - for i := 0; i < b.N; i++ { + for i := 1; i <= b.N; i++ { for j := 0; j < len(values); j++ { - if err := f.SetCellValue("Sheet1", fmt.Sprint(cols[j], i), values[j]); err != nil { + if err := f.SetCellValue("Sheet1", cols[j]+strconv.Itoa(i), values[j]); err != nil { b.Error(err) } } diff --git a/pivotTable.go b/pivotTable.go index 7061bfe..cf04381 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -21,7 +21,6 @@ import ( type PivotTableOption struct { DataRange string PivotTableRange string - Page []PivotTableField Rows []PivotTableField Columns []PivotTableField Data []PivotTableField @@ -194,7 +193,6 @@ func (f *File) adjustRange(rangeStr string) (string, []int, error) { // fields. func (f *File) getPivotFieldsOrder(dataRange string) ([]string, error) { order := []string{} - // data range has been checked dataSheet, coordinates, err := f.adjustRange(dataRange) if err != nil { return order, fmt.Errorf("parameter 'DataRange' parsing error: %s", err.Error()) @@ -217,10 +215,8 @@ func (f *File) addPivotCache(pivotCacheID int, pivotCacheXML string, opt *PivotT if err != nil { return fmt.Errorf("parameter 'DataRange' parsing error: %s", err.Error()) } - order, err := f.getPivotFieldsOrder(opt.DataRange) - if err != nil { - return err - } + // data range has been checked + order, _ := f.getPivotFieldsOrder(opt.DataRange) hcell, _ := CoordinatesToCellName(coordinates[0], coordinates[1]) vcell, _ := CoordinatesToCellName(coordinates[2], coordinates[3]) pc := xlsxPivotCacheDefinition{ @@ -272,7 +268,6 @@ func (f *File) addPivotTable(cacheID, pivotTableID int, pivotTableXML string, op FirstHeaderRow: 1, }, PivotFields: &xlsxPivotFields{}, - RowFields: &xlsxRowFields{}, RowItems: &xlsxRowItems{ Count: 1, I: []*xlsxI{ @@ -285,8 +280,6 @@ func (f *File) addPivotTable(cacheID, pivotTableID int, pivotTableXML string, op Count: 1, I: []*xlsxI{{}}, }, - PageFields: &xlsxPageFields{}, - DataFields: &xlsxDataFields{}, PivotTableStyleInfo: &xlsxPivotTableStyleInfo{ Name: "PivotStyleLight16", ShowRowHeaders: true, @@ -296,33 +289,49 @@ func (f *File) addPivotTable(cacheID, pivotTableID int, pivotTableXML string, op } // pivot fields - err = f.addPivotFields(&pt, opt) - if err != nil { - return err - } + _ = f.addPivotFields(&pt, opt) // count pivot fields pt.PivotFields.Count = len(pt.PivotFields.PivotField) + // data range has been checked + _ = f.addPivotRowFields(&pt, opt) + _ = f.addPivotColFields(&pt, opt) + _ = f.addPivotPageFields(&pt, opt) + _ = f.addPivotDataFields(&pt, opt) + + pivotTable, err := xml.Marshal(pt) + f.saveFileList(pivotTableXML, pivotTable) + return err +} + +// addPivotRowFields provides a method to add row fields for pivot table by +// given pivot table options. +func (f *File) addPivotRowFields(pt *xlsxPivotTableDefinition, opt *PivotTableOption) error { // row fields rowFieldsIndex, err := f.getPivotFieldsIndex(opt.Rows, opt) if err != nil { return err } for _, fieldIdx := range rowFieldsIndex { + if pt.RowFields == nil { + pt.RowFields = &xlsxRowFields{} + } pt.RowFields.Field = append(pt.RowFields.Field, &xlsxField{ X: fieldIdx, }) } // count row fields - pt.RowFields.Count = len(pt.RowFields.Field) - - err = f.addPivotColFields(&pt, opt) - if err != nil { - return err + if pt.RowFields != nil { + pt.RowFields.Count = len(pt.RowFields.Field) } + return err +} +// addPivotPageFields provides a method to add page fields for pivot table by +// given pivot table options. +func (f *File) addPivotPageFields(pt *xlsxPivotTableDefinition, opt *PivotTableOption) error { // page fields pageFieldsIndex, err := f.getPivotFieldsIndex(opt.Filter, opt) if err != nil { @@ -330,12 +339,25 @@ func (f *File) addPivotTable(cacheID, pivotTableID int, pivotTableXML string, op } pageFieldsName := f.getPivotTableFieldsName(opt.Filter) for idx, pageField := range pageFieldsIndex { + if pt.PageFields == nil { + pt.PageFields = &xlsxPageFields{} + } pt.PageFields.PageField = append(pt.PageFields.PageField, &xlsxPageField{ Name: pageFieldsName[idx], Fld: pageField, }) } + // count page fields + if pt.PageFields != nil { + pt.PageFields.Count = len(pt.PageFields.PageField) + } + return err +} + +// addPivotDataFields provides a method to add data fields for pivot table by +// given pivot table options. +func (f *File) addPivotDataFields(pt *xlsxPivotTableDefinition, opt *PivotTableOption) error { // data fields dataFieldsIndex, err := f.getPivotFieldsIndex(opt.Data, opt) if err != nil { @@ -344,6 +366,9 @@ func (f *File) addPivotTable(cacheID, pivotTableID int, pivotTableXML string, op dataFieldsSubtotals := f.getPivotTableFieldsSubtotal(opt.Data) dataFieldsName := f.getPivotTableFieldsName(opt.Data) for idx, dataField := range dataFieldsIndex { + if pt.DataFields == nil { + pt.DataFields = &xlsxDataFields{} + } pt.DataFields.DataField = append(pt.DataFields.DataField, &xlsxDataField{ Name: dataFieldsName[idx], Fld: dataField, @@ -352,10 +377,9 @@ func (f *File) addPivotTable(cacheID, pivotTableID int, pivotTableXML string, op } // count data fields - pt.DataFields.Count = len(pt.DataFields.DataField) - - pivotTable, err := xml.Marshal(pt) - f.saveFileList(pivotTableXML, pivotTable) + if pt.DataFields != nil { + pt.DataFields.Count = len(pt.DataFields.DataField) + } return err } diff --git a/pivotTable_test.go b/pivotTable_test.go index 767206b..cc80835 100644 --- a/pivotTable_test.go +++ b/pivotTable_test.go @@ -179,6 +179,46 @@ func TestAddPivotTable(t *testing.T) { assert.EqualError(t, err, `parameter 'DataRange' parsing error: parameter is required`) } +func TestAddPivotRowFields(t *testing.T) { + f := NewFile() + // Test invalid data range + assert.EqualError(t, f.addPivotRowFields(&xlsxPivotTableDefinition{}, &PivotTableOption{ + DataRange: "Sheet1!$A$1:$A$1", + }), `parameter 'DataRange' parsing error: parameter is invalid`) +} + +func TestAddPivotPageFields(t *testing.T) { + f := NewFile() + // Test invalid data range + assert.EqualError(t, f.addPivotPageFields(&xlsxPivotTableDefinition{}, &PivotTableOption{ + DataRange: "Sheet1!$A$1:$A$1", + }), `parameter 'DataRange' parsing error: parameter is invalid`) +} + +func TestAddPivotDataFields(t *testing.T) { + f := NewFile() + // Test invalid data range + assert.EqualError(t, f.addPivotDataFields(&xlsxPivotTableDefinition{}, &PivotTableOption{ + DataRange: "Sheet1!$A$1:$A$1", + }), `parameter 'DataRange' parsing error: parameter is invalid`) +} + +func TestAddPivotColFields(t *testing.T) { + f := NewFile() + // Test invalid data range + assert.EqualError(t, f.addPivotColFields(&xlsxPivotTableDefinition{}, &PivotTableOption{ + DataRange: "Sheet1!$A$1:$A$1", + Columns: []PivotTableField{{Data: "Type"}}, + }), `parameter 'DataRange' parsing error: parameter is invalid`) +} + +func TestGetPivotFieldsOrder(t *testing.T) { + f := NewFile() + // Test get pivot fields order with not exist worksheet + _, err := f.getPivotFieldsOrder("SheetN!$A$1:$E$31") + assert.EqualError(t, err, "sheet SheetN is not exist") +} + func TestInStrSlice(t *testing.T) { assert.EqualValues(t, -1, inStrSlice([]string{}, "")) } -- cgit v1.2.1 From 1fe660df648422a53eef0c735657cb2f5237ef7b Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 23 Apr 2020 02:01:14 +0800 Subject: - Resolve #485 use sheet index instead of ID - added 3 internal function: getSheetID, getActiveSheetID, getSheetNameByID --- README.md | 9 +++- README_zh.md | 8 +++- cell.go | 2 +- chart.go | 2 +- chart_test.go | 10 ++-- col_test.go | 4 +- excelize.go | 2 +- excelize_test.go | 14 +++--- merge_test.go | 4 +- rows_test.go | 8 ++-- sheet.go | 137 ++++++++++++++++++++++++++++++++++++++----------------- sheet_test.go | 8 ++-- stream.go | 2 +- 13 files changed, 136 insertions(+), 74 deletions(-) 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) } -- cgit v1.2.1 From 2285d4dc718fb8b96c3b2291c63b39c57468b0b9 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 24 Apr 2020 08:26:16 +0800 Subject: handle the cell without r attribute in a row element --- rows.go | 16 ++++++++++++++++ rows_test.go | 11 +++++++++++ sheet.go | 10 +++++----- xmlDrawing.go | 1 + 4 files changed, 33 insertions(+), 5 deletions(-) diff --git a/rows.go b/rows.go index 8f000b3..6a67672 100644 --- a/rows.go +++ b/rows.go @@ -593,6 +593,22 @@ func checkRow(xlsx *xlsxWorksheet) error { if colCount == 0 { continue } + // check and fill the cell without r attribute in a row element + rCount := 0 + for idx, cell := range rowData.C { + rCount++ + if cell.R != "" { + lastR, _, err := CellNameToCoordinates(cell.R) + if err != nil { + return err + } + if lastR > rCount { + rCount = lastR + } + continue + } + rowData.C[idx].R, _ = CoordinatesToCellName(rCount, rowIdx+1) + } lastCol, _, err := CellNameToCoordinates(rowData.C[colCount-1].R) if err != nil { return err diff --git a/rows_test.go b/rows_test.go index e5f0524..fd7196d 100644 --- a/rows_test.go +++ b/rows_test.go @@ -831,6 +831,17 @@ func TestErrSheetNotExistError(t *testing.T) { assert.EqualValues(t, err.Error(), "sheet Sheet1 is not exist") } +func TestCheckRow(t *testing.T) { + f := NewFile() + f.XLSX["xl/worksheets/sheet1.xml"] = []byte(`12345`) + _, err := f.GetRows("Sheet1") + assert.NoError(t, err) + assert.NoError(t, f.SetCellValue("Sheet1", "A1", false)) + f = NewFile() + f.XLSX["xl/worksheets/sheet1.xml"] = []byte(`12345`) + assert.EqualError(t, f.SetCellValue("Sheet1", "A1", false), `cannot convert cell "-" to coordinates: invalid cell name "-"`) +} + func BenchmarkRows(b *testing.B) { f, _ := OpenFile(filepath.Join("test", "Book1.xlsx")) for i := 0; i < b.N; i++ { diff --git a/sheet.go b/sheet.go index 50081e8..8c7f754 100644 --- a/sheet.go +++ b/sheet.go @@ -237,7 +237,7 @@ func (f *File) SetActiveSheet(index int) { for idx, name := range f.GetSheetList() { xlsx, err := f.workSheetReader(name) if err != nil { - // Chartsheet + // Chartsheet or dialogsheet return } if xlsx.SheetViews == nil { @@ -365,8 +365,8 @@ func (f *File) GetSheetIndex(name string) int { return idx } -// GetSheetMap provides a function to get worksheet and chartsheet name and -// ID map of XLSX. For example: +// GetSheetMap provides a function to get worksheet, chartsheet and +// dialogsheet ID and name map of XLSX. For example: // // f, err := excelize.OpenFile("Book1.xlsx") // if err != nil { @@ -387,8 +387,8 @@ func (f *File) GetSheetMap() map[int]string { return sheetMap } -// GetSheetList provides a function to get worksheet and chartsheet name list -// of workbook. +// GetSheetList provides a function to get worksheet, chartsheet and +// dialogsheet name list of workbook. func (f *File) GetSheetList() (list []string) { wb := f.workbookReader() if wb != nil { diff --git a/xmlDrawing.go b/xmlDrawing.go index 191631b..a5e43a1 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -23,6 +23,7 @@ const ( SourceRelationshipHyperLink = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink" SourceRelationshipWorkSheet = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" SourceRelationshipChartsheet = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chartsheet" + SourceRelationshipDialogsheet = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/dialogsheet" SourceRelationshipPivotTable = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotTable" SourceRelationshipPivotCache = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheDefinition" SourceRelationshipSharedStrings = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings" -- cgit v1.2.1 From 48fc4c08a2a80f7826d20bf3fd5a018f8e6f3185 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 3 May 2020 18:44:43 +0800 Subject: init formula calculation engine, ref #65 and #599 --- calc.go | 605 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ calc_test.go | 116 +++++++++++ excelize.go | 8 +- lib.go | 46 +++++ sheet.go | 28 +-- xmlChartSheet.go | 4 +- 6 files changed, 788 insertions(+), 19 deletions(-) create mode 100644 calc.go create mode 100644 calc_test.go diff --git a/calc.go b/calc.go new file mode 100644 index 0000000..d962fd4 --- /dev/null +++ b/calc.go @@ -0,0 +1,605 @@ +// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of +// this source code is governed by a BSD-style license that can be found in +// the LICENSE file. +// +// Package excelize 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 Exce™ 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. + +package excelize + +import ( + "container/list" + "errors" + "fmt" + "math" + "reflect" + "strconv" + "strings" + + "github.com/xuri/efp" +) + +// Excel formula errors +const ( + formulaErrorDIV = "#DIV/0!" + formulaErrorNAME = "#NAME?" + formulaErrorNA = "#N/A" + formulaErrorNUM = "#NUM!" + formulaErrorVALUE = "#VALUE!" + formulaErrorREF = "#REF!" + formulaErrorNULL = "#NULL" + formulaErrorSPILL = "#SPILL!" + formulaErrorCALC = "#CALC!" + formulaErrorGETTINGDATA = "#GETTING_DATA" +) + +// cellRef defines the structure of a cell reference +type cellRef struct { + Col int + Row int + Sheet string +} + +// cellRef defines the structure of a cell range +type cellRange struct { + From cellRef + To cellRef +} + +type formulaFuncs struct{} + +// CalcCellValue provides a function to get calculated cell value. This +// feature is currently in beta. Array formula, table formula and some other +// formulas are not supported currently. +func (f *File) CalcCellValue(sheet, cell string) (result string, err error) { + var ( + formula string + token efp.Token + ) + if formula, err = f.GetCellFormula(sheet, cell); err != nil { + return + } + ps := efp.ExcelParser() + tokens := ps.Parse(formula) + if tokens == nil { + return + } + if token, err = f.evalInfixExp(sheet, tokens); err != nil { + return + } + result = token.TValue + return +} + +// getPriority calculate arithmetic operator priority. +func getPriority(token efp.Token) (pri int) { + var priority = map[string]int{ + "*": 2, + "/": 2, + "+": 1, + "-": 1, + } + pri, _ = priority[token.TValue] + if token.TValue == "-" && token.TType == efp.TokenTypeOperatorPrefix { + pri = 3 + } + if token.TSubType == efp.TokenSubTypeStart && token.TType == efp.TokenTypeSubexpression { // ( + pri = 0 + } + return +} + +// evalInfixExp evaluate syntax analysis by given infix expression after +// lexical analysis. Evaluate an infix expression containing formulas by +// stacks: +// +// opd - Operand +// opt - Operator +// opf - Operation formula +// opfd - Operand of the operation formula +// opft - Operator of the operation formula +// args - Arguments of the operation formula +// +func (f *File) evalInfixExp(sheet string, tokens []efp.Token) (efp.Token, error) { + var err error + opdStack, optStack, opfStack, opfdStack, opftStack, argsStack := NewStack(), NewStack(), NewStack(), NewStack(), NewStack(), NewStack() + for i := 0; i < len(tokens); i++ { + token := tokens[i] + + // out of function stack + if opfStack.Len() == 0 { + if err = f.parseToken(sheet, token, opdStack, optStack); err != nil { + return efp.Token{}, err + } + } + + // function start + if token.TType == efp.TokenTypeFunction && token.TSubType == efp.TokenSubTypeStart { + opfStack.Push(token) + continue + } + + // in function stack, walk 2 token at once + if opfStack.Len() > 0 { + var nextToken efp.Token + if i+1 < len(tokens) { + nextToken = tokens[i+1] + } + + // current token is args or range, skip next token, order required: parse reference first + if token.TSubType == efp.TokenSubTypeRange { + if !opftStack.Empty() { + // parse reference: must reference at here + result, err := f.parseReference(sheet, token.TValue) + if err != nil { + return efp.Token{TValue: formulaErrorNAME}, err + } + if len(result) != 1 { + return efp.Token{}, errors.New(formulaErrorVALUE) + } + opfdStack.Push(efp.Token{ + TType: efp.TokenTypeOperand, + TSubType: efp.TokenSubTypeNumber, + TValue: result[0], + }) + continue + } + if nextToken.TType == efp.TokenTypeArgument || nextToken.TType == efp.TokenTypeFunction { + // parse reference: reference or range at here + result, err := f.parseReference(sheet, token.TValue) + if err != nil { + return efp.Token{TValue: formulaErrorNAME}, err + } + for _, val := range result { + argsStack.Push(efp.Token{ + TType: efp.TokenTypeOperand, + TSubType: efp.TokenSubTypeNumber, + TValue: val, + }) + } + if len(result) == 0 { + return efp.Token{}, errors.New(formulaErrorVALUE) + } + continue + } + } + + // check current token is opft + if err = f.parseToken(sheet, token, opfdStack, opftStack); err != nil { + return efp.Token{}, err + } + + // current token is arg + if token.TType == efp.TokenTypeArgument { + for !opftStack.Empty() { + // calculate trigger + topOpt := opftStack.Peek().(efp.Token) + if err := calculate(opfdStack, topOpt); err != nil { + return efp.Token{}, err + } + opftStack.Pop() + } + if !opfdStack.Empty() { + argsStack.Push(opfdStack.Pop()) + } + continue + } + + // current token is function stop + if token.TType == efp.TokenTypeFunction && token.TSubType == efp.TokenSubTypeStop { + for !opftStack.Empty() { + // calculate trigger + topOpt := opftStack.Peek().(efp.Token) + if err := calculate(opfdStack, topOpt); err != nil { + return efp.Token{}, err + } + opftStack.Pop() + } + + // push opfd to args + if opfdStack.Len() > 0 { + argsStack.Push(opfdStack.Pop()) + } + // call formula function to evaluate + result, err := callFuncByName(&formulaFuncs{}, opfStack.Peek().(efp.Token).TValue, []reflect.Value{reflect.ValueOf(argsStack)}) + if err != nil { + return efp.Token{}, err + } + opfStack.Pop() + if opfStack.Len() > 0 { // still in function stack + opfdStack.Push(efp.Token{TValue: result, TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) + } else { + opdStack.Push(efp.Token{TValue: result, TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) + } + } + } + } + for optStack.Len() != 0 { + topOpt := optStack.Peek().(efp.Token) + if err = calculate(opdStack, topOpt); err != nil { + return efp.Token{}, err + } + optStack.Pop() + } + return opdStack.Peek().(efp.Token), err +} + +// calculate evaluate basic arithmetic operations. +func calculate(opdStack *Stack, opt efp.Token) error { + if opt.TValue == "-" && opt.TType == efp.TokenTypeOperatorPrefix { + opd := opdStack.Pop().(efp.Token) + opdVal, err := strconv.ParseFloat(opd.TValue, 64) + if err != nil { + return err + } + result := 0 - opdVal + opdStack.Push(efp.Token{TValue: fmt.Sprintf("%g", result), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) + } + if opt.TValue == "+" { + rOpd := opdStack.Pop().(efp.Token) + lOpd := opdStack.Pop().(efp.Token) + lOpdVal, err := strconv.ParseFloat(lOpd.TValue, 64) + if err != nil { + return err + } + rOpdVal, err := strconv.ParseFloat(rOpd.TValue, 64) + if err != nil { + return err + } + result := lOpdVal + rOpdVal + opdStack.Push(efp.Token{TValue: fmt.Sprintf("%g", result), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) + } + if opt.TValue == "-" && opt.TType == efp.TokenTypeOperatorInfix { + rOpd := opdStack.Pop().(efp.Token) + lOpd := opdStack.Pop().(efp.Token) + lOpdVal, err := strconv.ParseFloat(lOpd.TValue, 64) + if err != nil { + return err + } + rOpdVal, err := strconv.ParseFloat(rOpd.TValue, 64) + if err != nil { + return err + } + result := lOpdVal - rOpdVal + opdStack.Push(efp.Token{TValue: fmt.Sprintf("%g", result), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) + } + if opt.TValue == "*" { + rOpd := opdStack.Pop().(efp.Token) + lOpd := opdStack.Pop().(efp.Token) + lOpdVal, err := strconv.ParseFloat(lOpd.TValue, 64) + if err != nil { + return err + } + rOpdVal, err := strconv.ParseFloat(rOpd.TValue, 64) + if err != nil { + return err + } + result := lOpdVal * rOpdVal + opdStack.Push(efp.Token{TValue: fmt.Sprintf("%g", result), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) + } + if opt.TValue == "/" { + rOpd := opdStack.Pop().(efp.Token) + lOpd := opdStack.Pop().(efp.Token) + lOpdVal, err := strconv.ParseFloat(lOpd.TValue, 64) + if err != nil { + return err + } + rOpdVal, err := strconv.ParseFloat(rOpd.TValue, 64) + if err != nil { + return err + } + result := lOpdVal / rOpdVal + if rOpdVal == 0 { + return errors.New(formulaErrorDIV) + } + opdStack.Push(efp.Token{TValue: fmt.Sprintf("%g", result), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) + } + return nil +} + +// parseToken parse basic arithmetic operator priority and evaluate based on +// operators and operands. +func (f *File) parseToken(sheet string, token efp.Token, opdStack, optStack *Stack) error { + // parse reference: must reference at here + if token.TSubType == efp.TokenSubTypeRange { + result, err := f.parseReference(sheet, token.TValue) + if err != nil { + return errors.New(formulaErrorNAME) + } + if len(result) != 1 { + return errors.New(formulaErrorVALUE) + } + token.TValue = result[0] + token.TType = efp.TokenTypeOperand + token.TSubType = efp.TokenSubTypeNumber + } + if (token.TValue == "-" && token.TType == efp.TokenTypeOperatorPrefix) || token.TValue == "+" || token.TValue == "-" || token.TValue == "*" || token.TValue == "/" { + if optStack.Len() == 0 { + optStack.Push(token) + } else { + tokenPriority := getPriority(token) + topOpt := optStack.Peek().(efp.Token) + topOptPriority := getPriority(topOpt) + if tokenPriority > topOptPriority { + optStack.Push(token) + } else { + for tokenPriority <= topOptPriority { + optStack.Pop() + if err := calculate(opdStack, topOpt); err != nil { + return err + } + if optStack.Len() > 0 { + topOpt = optStack.Peek().(efp.Token) + topOptPriority = getPriority(topOpt) + continue + } + break + } + optStack.Push(token) + } + } + } + if token.TType == efp.TokenTypeSubexpression && token.TSubType == efp.TokenSubTypeStart { // ( + optStack.Push(token) + } + if token.TType == efp.TokenTypeSubexpression && token.TSubType == efp.TokenSubTypeStop { // ) + for optStack.Peek().(efp.Token).TSubType != efp.TokenSubTypeStart && optStack.Peek().(efp.Token).TType != efp.TokenTypeSubexpression { // != ( + topOpt := optStack.Peek().(efp.Token) + if err := calculate(opdStack, topOpt); err != nil { + return err + } + optStack.Pop() + } + optStack.Pop() + } + // opd + if token.TType == efp.TokenTypeOperand && token.TSubType == efp.TokenSubTypeNumber { + opdStack.Push(token) + } + return nil +} + +// parseReference parse reference and extract values by given reference +// characters and default sheet name. +func (f *File) parseReference(sheet, reference string) (result []string, err error) { + reference = strings.Replace(reference, "$", "", -1) + refs, cellRanges, cellRefs := list.New(), list.New(), list.New() + for _, ref := range strings.Split(reference, ":") { + tokens := strings.Split(ref, "!") + cr := cellRef{} + if len(tokens) == 2 { // have a worksheet name + cr.Sheet = tokens[0] + if cr.Col, cr.Row, err = CellNameToCoordinates(tokens[1]); err != nil { + return + } + if refs.Len() > 0 { + e := refs.Back() + cellRefs.PushBack(e.Value.(cellRef)) + refs.Remove(e) + } + refs.PushBack(cr) + continue + } + if cr.Col, cr.Row, err = CellNameToCoordinates(tokens[0]); err != nil { + return + } + e := refs.Back() + if e == nil { + cr.Sheet = sheet + refs.PushBack(cr) + continue + } + cellRanges.PushBack(cellRange{ + From: e.Value.(cellRef), + To: cr, + }) + refs.Remove(e) + } + if refs.Len() > 0 { + e := refs.Back() + cellRefs.PushBack(e.Value.(cellRef)) + refs.Remove(e) + } + + result, err = f.rangeResolver(cellRefs, cellRanges) + return +} + +// rangeResolver extract value as string from given reference and range list. +// This function will not ignore the empty cell. Note that the result of 3D +// range references may be different from Excel in some cases, for example, +// A1:A2:A2:B3 in Excel will include B2, but we wont. +func (f *File) rangeResolver(cellRefs, cellRanges *list.List) (result []string, err error) { + filter := map[string]string{} + // extract value from ranges + for temp := cellRanges.Front(); temp != nil; temp = temp.Next() { + cr := temp.Value.(cellRange) + if cr.From.Sheet != cr.To.Sheet { + err = errors.New(formulaErrorVALUE) + } + rng := []int{cr.From.Col, cr.From.Row, cr.To.Col, cr.To.Row} + sortCoordinates(rng) + for col := rng[0]; col <= rng[2]; col++ { + for row := rng[1]; row <= rng[3]; row++ { + var cell string + if cell, err = CoordinatesToCellName(col, row); err != nil { + return + } + if filter[cell], err = f.GetCellValue(cr.From.Sheet, cell); err != nil { + return + } + } + } + } + // extract value from references + for temp := cellRefs.Front(); temp != nil; temp = temp.Next() { + cr := temp.Value.(cellRef) + var cell string + if cell, err = CoordinatesToCellName(cr.Col, cr.Row); err != nil { + return + } + if filter[cell], err = f.GetCellValue(cr.Sheet, cell); err != nil { + return + } + } + + for _, val := range filter { + result = append(result, val) + } + return +} + +// callFuncByName calls the no error or only error return function with +// reflect by given receiver, name and parameters. +func callFuncByName(receiver interface{}, name string, params []reflect.Value) (result string, err error) { + function := reflect.ValueOf(receiver).MethodByName(name) + if function.IsValid() { + rt := function.Call(params) + if len(rt) == 0 { + return + } + if !rt[1].IsNil() { + err = rt[1].Interface().(error) + return + } + result = rt[0].Interface().(string) + return + } + err = fmt.Errorf("not support %s function", name) + return +} + +// Math and Trigonometric functions + +// SUM function adds together a supplied set of numbers and returns the sum of +// these values. The syntax of the function is: +// +// SUM(number1,[number2],...) +// +func (fn *formulaFuncs) SUM(argsStack *Stack) (result string, err error) { + var val float64 + var sum float64 + for !argsStack.Empty() { + token := argsStack.Pop().(efp.Token) + if token.TValue == "" { + continue + } + val, err = strconv.ParseFloat(token.TValue, 64) + if err != nil { + return + } + sum += val + } + result = fmt.Sprintf("%g", sum) + return +} + +// PRODUCT function returns the product (multiplication) of a supplied set of numerical values. +// The syntax of the function is: +// +// PRODUCT(number1,[number2],...) +// +func (fn *formulaFuncs) PRODUCT(argsStack *Stack) (result string, err error) { + var ( + val float64 + product float64 = 1 + ) + for !argsStack.Empty() { + token := argsStack.Pop().(efp.Token) + if token.TValue == "" { + continue + } + val, err = strconv.ParseFloat(token.TValue, 64) + if err != nil { + return + } + product = product * val + } + result = fmt.Sprintf("%g", product) + return +} + +// PRODUCT function calculates a given number, raised to a supplied power. +// The syntax of the function is: +// +// POWER(number,power) +// +func (fn *formulaFuncs) POWER(argsStack *Stack) (result string, err error) { + if argsStack.Len() != 2 { + err = errors.New("POWER requires 2 numeric arguments") + return + } + var x, y float64 + y, err = strconv.ParseFloat(argsStack.Pop().(efp.Token).TValue, 64) + if err != nil { + return + } + x, err = strconv.ParseFloat(argsStack.Pop().(efp.Token).TValue, 64) + if err != nil { + return + } + if x == 0 && y == 0 { + err = errors.New(formulaErrorNUM) + return + } + if x == 0 && y < 0 { + err = errors.New(formulaErrorDIV) + return + } + result = fmt.Sprintf("%g", math.Pow(x, y)) + return +} + +// SQRT function calculates the positive square root of a supplied number. +// The syntax of the function is: +// +// SQRT(number) +// +func (fn *formulaFuncs) SQRT(argsStack *Stack) (result string, err error) { + if argsStack.Len() != 1 { + err = errors.New("SQRT requires 1 numeric arguments") + return + } + var val float64 + val, err = strconv.ParseFloat(argsStack.Pop().(efp.Token).TValue, 64) + if err != nil { + return + } + if val < 0 { + err = errors.New(formulaErrorNUM) + return + } + result = fmt.Sprintf("%g", math.Sqrt(val)) + return +} + +// QUOTIENT function returns the integer portion of a division between two supplied numbers. +// The syntax of the function is: +// +// QUOTIENT(numerator,denominator) +// +func (fn *formulaFuncs) QUOTIENT(argsStack *Stack) (result string, err error) { + if argsStack.Len() != 2 { + err = errors.New("QUOTIENT requires 2 numeric arguments") + return + } + var x, y float64 + y, err = strconv.ParseFloat(argsStack.Pop().(efp.Token).TValue, 64) + if err != nil { + return + } + x, err = strconv.ParseFloat(argsStack.Pop().(efp.Token).TValue, 64) + if err != nil { + return + } + if y == 0 { + err = errors.New(formulaErrorDIV) + return + } + result = fmt.Sprintf("%g", math.Trunc(x/y)) + return +} diff --git a/calc_test.go b/calc_test.go new file mode 100644 index 0000000..0ebe37e --- /dev/null +++ b/calc_test.go @@ -0,0 +1,116 @@ +package excelize + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCalcCellValue(t *testing.T) { + prepareData := func() *File { + f := NewFile() + f.SetCellValue("Sheet1", "A1", 1) + f.SetCellValue("Sheet1", "A2", 2) + f.SetCellValue("Sheet1", "A3", 3) + f.SetCellValue("Sheet1", "A4", 0) + f.SetCellValue("Sheet1", "B1", 4) + return f + } + + mathCalc := map[string]string{ + "=SUM(1,2)": "3", + "=SUM(1,2+3)": "6", + "=SUM(SUM(1,2),2)": "5", + "=(-2-SUM(-4+7))*5": "-25", + "SUM(1,2,3,4,5,6,7)": "28", + "=SUM(1,2)+SUM(1,2)": "6", + "=1+SUM(SUM(1,2*3),4)": "12", + "=1+SUM(SUM(1,-2*3),4)": "0", + "=(-2-SUM(-4*(7+7)))*5": "270", + "=SUM(SUM(1+2/1)*2-3/2,2)": "6.5", + "=((3+5*2)+3)/5+(-6)/4*2+3": "3.2", + "=1+SUM(SUM(1,2*3),4)*-4/2+5+(4+2)*3": "2", + "=1+SUM(SUM(1,2*3),4)*4/3+5+(4+2)*3": "38.666666666666664", + // POWER + "=POWER(4,2)": "16", + // SQRT + "=SQRT(4)": "2", + // QUOTIENT + "=QUOTIENT(5, 2)": "2", + "=QUOTIENT(4.5, 3.1)": "1", + "=QUOTIENT(-10, 3)": "-3", + } + for formula, expected := range mathCalc { + f := prepareData() + assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula)) + result, err := f.CalcCellValue("Sheet1", "C1") + assert.NoError(t, err) + assert.Equal(t, expected, result) + } + mathCalcError := map[string]string{ + // POWER + "=POWER(0,0)": "#NUM!", + "=POWER(0,-1)": "#DIV/0!", + "=POWER(1)": "POWER requires 2 numeric arguments", + // SQRT + "=SQRT(-1)": "#NUM!", + "=SQRT(1,2)": "SQRT requires 1 numeric arguments", + // QUOTIENT + "=QUOTIENT(1,0)": "#DIV/0!", + "=QUOTIENT(1)": "QUOTIENT requires 2 numeric arguments", + } + for formula, expected := range mathCalcError { + f := prepareData() + assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula)) + result, err := f.CalcCellValue("Sheet1", "C1") + assert.EqualError(t, err, expected) + assert.Equal(t, "", result) + } + + referenceCalc := map[string]string{ + "=A1/A3": "0.3333333333333333", + "=SUM(A1:A2)": "3", + "=SUM(Sheet1!A1,A2)": "3", + "=(-2-SUM(-4+A2))*5": "0", + "=SUM(Sheet1!A1:Sheet1!A1:A2,A2)": "5", + "=SUM(A1,A2,A3)*SUM(2,3)": "30", + "=1+SUM(SUM(A1+A2/A3)*(2-3),2)": "1.3333333333333335", + "=A1/A2/SUM(A1:A2:B1)": "0.07142857142857142", + "=A1/A2/SUM(A1:A2:B1)*A3": "0.21428571428571427", + // PRODUCT + "=PRODUCT(Sheet1!A1:Sheet1!A1:A2,A2)": "4", + } + for formula, expected := range referenceCalc { + f := prepareData() + assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula)) + result, err := f.CalcCellValue("Sheet1", "C1") + assert.NoError(t, err) + assert.Equal(t, expected, result) + } + + referenceCalcError := map[string]string{ + "=1+SUM(SUM(A1+A2/A4)*(2-3),2)": "#DIV/0!", + } + for formula, expected := range referenceCalcError { + f := prepareData() + assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula)) + result, err := f.CalcCellValue("Sheet1", "C1") + assert.EqualError(t, err, expected) + assert.Equal(t, "", result) + } + + // Test get calculated cell value on not formula cell. + f := prepareData() + result, err := f.CalcCellValue("Sheet1", "A1") + assert.NoError(t, err) + assert.Equal(t, "", result) + // Test get calculated cell value on not exists worksheet. + f = prepareData() + _, err = f.CalcCellValue("SheetN", "A1") + assert.EqualError(t, err, "sheet SheetN is not exist") + // Test get calculated cell value with not support formula. + f = prepareData() + assert.NoError(t, f.SetCellFormula("Sheet1", "A1", "=UNSUPPORT(A1)")) + _, err = f.CalcCellValue("Sheet1", "A1") + assert.EqualError(t, err, "not support UNSUPPORT function") +} diff --git a/excelize.go b/excelize.go index 73bc1b5..04e2e85 100644 --- a/excelize.go +++ b/excelize.go @@ -3,9 +3,11 @@ // the LICENSE file. // Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.10 or later. +// and read from XLSX / XLSM / XLTM files. Supports reading and writing +// spreadsheet documents generated by Microsoft Exce™ 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. // // See https://xuri.me/excelize for more information about this package. package excelize diff --git a/lib.go b/lib.go index 83cdb4a..79c7cd4 100644 --- a/lib.go +++ b/lib.go @@ -12,6 +12,7 @@ package excelize import ( "archive/zip" "bytes" + "container/list" "fmt" "io" "log" @@ -305,3 +306,48 @@ func genSheetPasswd(plaintext string) string { password ^= 0xCE4B return strings.ToUpper(strconv.FormatInt(password, 16)) } + +// Stack defined an abstract data type that serves as a collection of elements. +type Stack struct { + list *list.List +} + +// NewStack create a new stack. +func NewStack() *Stack { + list := list.New() + return &Stack{list} +} + +// Push a value onto the top of the stack. +func (stack *Stack) Push(value interface{}) { + stack.list.PushBack(value) +} + +// Pop the top item of the stack and return it. +func (stack *Stack) Pop() interface{} { + e := stack.list.Back() + if e != nil { + stack.list.Remove(e) + return e.Value + } + return nil +} + +// Peek view the top item on the stack. +func (stack *Stack) Peek() interface{} { + e := stack.list.Back() + if e != nil { + return e.Value + } + return nil +} + +// Len return the number of items in the stack. +func (stack *Stack) Len() int { + return stack.list.Len() +} + +// Empty the stack. +func (stack *Stack) Empty() bool { + return stack.list.Len() == 0 +} diff --git a/sheet.go b/sheet.go index 8c7f754..fa858af 100644 --- a/sheet.go +++ b/sheet.go @@ -211,10 +211,10 @@ func replaceRelationshipsBytes(content []byte) []byte { return bytesReplace(content, oldXmlns, newXmlns, -1) } -// 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 or equal to 0 and less than -// total worksheet numbers. +// SetActiveSheet provides a function to set the default active sheet of the +// workbook by a given index. Note that the active index is different from the +// ID returned by function GetSheetMap(). It should be greater or equal to 0 +// and less than the total worksheet numbers. func (f *File) SetActiveSheet(index int) { if index < 0 { index = 0 @@ -327,9 +327,9 @@ func (f *File) getSheetNameByID(ID int) string { 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. +// GetSheetName provides a function to get the sheet name of the workbook by +// the given sheet index. If the given sheet index is invalid, it will return +// an empty string. func (f *File) GetSheetName(index int) (name string) { for idx, sheet := range f.GetSheetList() { if idx == index { @@ -352,9 +352,9 @@ func (f *File) getSheetID(name string) int { 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 -1. +// GetSheetIndex provides a function to get a sheet index of the workbook by +// the given sheet name. If the given sheet name is invalid, it will return an +// integer type value -1. func (f *File) GetSheetIndex(name string) int { var idx = -1 for index, sheet := range f.GetSheetList() { @@ -365,8 +365,8 @@ func (f *File) GetSheetIndex(name string) int { return idx } -// GetSheetMap provides a function to get worksheet, chartsheet and -// dialogsheet ID and name map of XLSX. For example: +// GetSheetMap provides a function to get worksheets, chart sheets, dialog +// sheets ID and name map of the workbook. For example: // // f, err := excelize.OpenFile("Book1.xlsx") // if err != nil { @@ -387,8 +387,8 @@ func (f *File) GetSheetMap() map[int]string { return sheetMap } -// GetSheetList provides a function to get worksheet, chartsheet and -// dialogsheet name list of workbook. +// GetSheetList provides a function to get worksheets, chart sheets, and +// dialog sheets name list of the workbook. func (f *File) GetSheetList() (list []string) { wb := f.workbookReader() if wb != nil { diff --git a/xmlChartSheet.go b/xmlChartSheet.go index fae5a16..30a0693 100644 --- a/xmlChartSheet.go +++ b/xmlChartSheet.go @@ -51,7 +51,7 @@ type xlsxChartsheetView struct { XMLName xml.Name `xml:"sheetView"` TabSelectedAttr bool `xml:"tabSelected,attr,omitempty"` ZoomScaleAttr uint32 `xml:"zoomScale,attr,omitempty"` - WorkbookViewIdAttr uint32 `xml:"workbookViewId,attr"` + WorkbookViewIDAttr uint32 `xml:"workbookViewId,attr"` ZoomToFitAttr bool `xml:"zoomToFit,attr,omitempty"` ExtLst []*xlsxExtLst `xml:"extLst"` } @@ -78,7 +78,7 @@ type xlsxCustomChartsheetViews struct { // xlsxCustomChartsheetView defines custom view properties for chart sheets. type xlsxCustomChartsheetView struct { XMLName xml.Name `xml:"customChartsheetView"` - GuidAttr string `xml:"guid,attr"` + GUIDAttr string `xml:"guid,attr"` ScaleAttr uint32 `xml:"scale,attr,omitempty"` StateAttr string `xml:"state,attr,omitempty"` ZoomToFitAttr bool `xml:"zoomToFit,attr,omitempty"` -- cgit v1.2.1 From bdf05386408d439fda7cd495105f59cb2eaf8099 Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 4 May 2020 13:40:04 +0800 Subject: fn: ABS, GCD, LCM, POWER, PRODUCT, SIGN, SQRT, SUM, QUOTIENT --- calc.go | 209 +++++++++++++++++++++++++++++++++++++++++++++++++++++------ calc_test.go | 51 +++++++++++++-- 2 files changed, 233 insertions(+), 27 deletions(-) diff --git a/calc.go b/calc.go index d962fd4..5ebdcf7 100644 --- a/calc.go +++ b/calc.go @@ -412,7 +412,7 @@ func (f *File) parseReference(sheet, reference string) (result []string, err err // rangeResolver extract value as string from given reference and range list. // This function will not ignore the empty cell. Note that the result of 3D // range references may be different from Excel in some cases, for example, -// A1:A2:A2:B3 in Excel will include B2, but we wont. +// A1:A2:A2:B3 in Excel will include B1, but we wont. func (f *File) rangeResolver(cellRefs, cellRanges *list.List) (result []string, err error) { filter := map[string]string{} // extract value from ranges @@ -475,14 +475,58 @@ func callFuncByName(receiver interface{}, name string, params []reflect.Value) ( // Math and Trigonometric functions -// SUM function adds together a supplied set of numbers and returns the sum of -// these values. The syntax of the function is: +// ABS function returns the absolute value of any supplied number. The syntax +// of the function is: // -// SUM(number1,[number2],...) +// ABS(number) // -func (fn *formulaFuncs) SUM(argsStack *Stack) (result string, err error) { +func (fn *formulaFuncs) ABS(argsStack *Stack) (result string, err error) { + if argsStack.Len() != 1 { + err = errors.New("ABS requires 1 numeric arguments") + return + } var val float64 - var sum float64 + val, err = strconv.ParseFloat(argsStack.Pop().(efp.Token).TValue, 64) + if err != nil { + return + } + result = fmt.Sprintf("%g", math.Abs(val)) + return +} + +// gcd returns the greatest common divisor of two supplied integers. +func gcd(x, y float64) float64 { + x, y = math.Trunc(x), math.Trunc(y) + if x == 0 { + return y + } + if y == 0 { + return x + } + for x != y { + if x > y { + x = x - y + } else { + y = y - x + } + } + return x +} + +// GCD function returns the greatest common divisor of two or more supplied +// integers.The syntax of the function is: +// +// GCD(number1,[number2],...) +// +func (fn *formulaFuncs) GCD(argsStack *Stack) (result string, err error) { + if argsStack.Len() == 0 { + err = errors.New("GCD requires at least 1 argument") + return + } + var ( + val float64 + nums = []float64{} + ) for !argsStack.Empty() { token := argsStack.Pop().(efp.Token) if token.TValue == "" { @@ -492,21 +536,51 @@ func (fn *formulaFuncs) SUM(argsStack *Stack) (result string, err error) { if err != nil { return } - sum += val + nums = append(nums, val) } - result = fmt.Sprintf("%g", sum) + if nums[0] < 0 { + err = errors.New("GCD only accepts positive arguments") + return + } + if len(nums) == 1 { + result = fmt.Sprintf("%g", nums[0]) + return + } + cd := nums[0] + for i := 1; i < len(nums); i++ { + if nums[i] < 0 { + err = errors.New("GCD only accepts positive arguments") + return + } + cd = gcd(cd, nums[i]) + } + result = fmt.Sprintf("%g", cd) return } -// PRODUCT function returns the product (multiplication) of a supplied set of numerical values. -// The syntax of the function is: +// lcm returns the least common multiple of two supplied integers. +func lcm(a, b float64) float64 { + a = math.Trunc(a) + b = math.Trunc(b) + if a == 0 && b == 0 { + return 0 + } + return a * b / gcd(a, b) +} + +// LCM function returns the least common multiple of two or more supplied +// integers. The syntax of the function is: // -// PRODUCT(number1,[number2],...) +// LCM(number1,[number2],...) // -func (fn *formulaFuncs) PRODUCT(argsStack *Stack) (result string, err error) { +func (fn *formulaFuncs) LCM(argsStack *Stack) (result string, err error) { + if argsStack.Len() == 0 { + err = errors.New("LCM requires at least 1 argument") + return + } var ( - val float64 - product float64 = 1 + val float64 + nums = []float64{} ) for !argsStack.Empty() { token := argsStack.Pop().(efp.Token) @@ -517,13 +591,29 @@ func (fn *formulaFuncs) PRODUCT(argsStack *Stack) (result string, err error) { if err != nil { return } - product = product * val + nums = append(nums, val) } - result = fmt.Sprintf("%g", product) + if nums[0] < 0 { + err = errors.New("LCM only accepts positive arguments") + return + } + if len(nums) == 1 { + result = fmt.Sprintf("%g", nums[0]) + return + } + cm := nums[0] + for i := 1; i < len(nums); i++ { + if nums[i] < 0 { + err = errors.New("LCM only accepts positive arguments") + return + } + cm = lcm(cm, nums[i]) + } + result = fmt.Sprintf("%g", cm) return } -// PRODUCT function calculates a given number, raised to a supplied power. +// POWER function calculates a given number, raised to a supplied power. // The syntax of the function is: // // POWER(number,power) @@ -554,8 +644,62 @@ func (fn *formulaFuncs) POWER(argsStack *Stack) (result string, err error) { return } -// SQRT function calculates the positive square root of a supplied number. -// The syntax of the function is: +// PRODUCT function returns the product (multiplication) of a supplied set of +// numerical values. The syntax of the function is: +// +// PRODUCT(number1,[number2],...) +// +func (fn *formulaFuncs) PRODUCT(argsStack *Stack) (result string, err error) { + var ( + val float64 + product float64 = 1 + ) + for !argsStack.Empty() { + token := argsStack.Pop().(efp.Token) + if token.TValue == "" { + continue + } + val, err = strconv.ParseFloat(token.TValue, 64) + if err != nil { + return + } + product = product * val + } + result = fmt.Sprintf("%g", product) + return +} + +// SIGN function returns the arithmetic sign (+1, -1 or 0) of a supplied +// number. I.e. if the number is positive, the Sign function returns +1, if +// the number is negative, the function returns -1 and if the number is 0 +// (zero), the function returns 0. The syntax of the function is: +// +// SIGN(number) +// +func (fn *formulaFuncs) SIGN(argsStack *Stack) (result string, err error) { + if argsStack.Len() != 1 { + err = errors.New("SIGN requires 1 numeric arguments") + return + } + var val float64 + val, err = strconv.ParseFloat(argsStack.Pop().(efp.Token).TValue, 64) + if err != nil { + return + } + if val < 0 { + result = "-1" + return + } + if val > 0 { + result = "1" + return + } + result = "0" + return +} + +// SQRT function calculates the positive square root of a supplied number. The +// syntax of the function is: // // SQRT(number) // @@ -577,8 +721,31 @@ func (fn *formulaFuncs) SQRT(argsStack *Stack) (result string, err error) { return } -// QUOTIENT function returns the integer portion of a division between two supplied numbers. -// The syntax of the function is: +// SUM function adds together a supplied set of numbers and returns the sum of +// these values. The syntax of the function is: +// +// SUM(number1,[number2],...) +// +func (fn *formulaFuncs) SUM(argsStack *Stack) (result string, err error) { + var val float64 + var sum float64 + for !argsStack.Empty() { + token := argsStack.Pop().(efp.Token) + if token.TValue == "" { + continue + } + val, err = strconv.ParseFloat(token.TValue, 64) + if err != nil { + return + } + sum += val + } + result = fmt.Sprintf("%g", sum) + return +} + +// QUOTIENT function returns the integer portion of a division between two +// supplied numbers. The syntax of the function is: // // QUOTIENT(numerator,denominator) // diff --git a/calc_test.go b/calc_test.go index 0ebe37e..84fa955 100644 --- a/calc_test.go +++ b/calc_test.go @@ -18,6 +18,35 @@ func TestCalcCellValue(t *testing.T) { } mathCalc := map[string]string{ + // ABS + "=ABS(-1)": "1", + "=ABS(-6.5)": "6.5", + "=ABS(6.5)": "6.5", + "=ABS(0)": "0", + "=ABS(2-4.5)": "2.5", + // GCD + "=GCD(1,5)": "1", + "=GCD(15,10,25)": "5", + "=GCD(0,8,12)": "4", + "=GCD(7,2)": "1", + // LCM + "=LCM(1,5)": "5", + "=LCM(15,10,25)": "150", + "=LCM(1,8,12)": "24", + "=LCM(7,2)": "14", + // POWER + "=POWER(4,2)": "16", + // PRODUCT + "=PRODUCT(3,6)": "18", + // SIGN + "=SIGN(9.5)": "1", + "=SIGN(-9.5)": "-1", + "=SIGN(0)": "0", + "=SIGN(0.00000001)": "1", + "=SIGN(6-7)": "-1", + // SQRT + "=SQRT(4)": "2", + // SUM "=SUM(1,2)": "3", "=SUM(1,2+3)": "6", "=SUM(SUM(1,2),2)": "5", @@ -31,10 +60,6 @@ func TestCalcCellValue(t *testing.T) { "=((3+5*2)+3)/5+(-6)/4*2+3": "3.2", "=1+SUM(SUM(1,2*3),4)*-4/2+5+(4+2)*3": "2", "=1+SUM(SUM(1,2*3),4)*4/3+5+(4+2)*3": "38.666666666666664", - // POWER - "=POWER(4,2)": "16", - // SQRT - "=SQRT(4)": "2", // QUOTIENT "=QUOTIENT(5, 2)": "2", "=QUOTIENT(4.5, 3.1)": "1", @@ -48,10 +73,23 @@ func TestCalcCellValue(t *testing.T) { assert.Equal(t, expected, result) } mathCalcError := map[string]string{ + // ABS + "=ABS(1,2)": "ABS requires 1 numeric arguments", + "=ABS(~)": `cannot convert cell "~" to coordinates: invalid cell name "~"`, + // GCD + "=GCD()": "GCD requires at least 1 argument", + "=GCD(-1)": "GCD only accepts positive arguments", + "=GCD(1,-1)": "GCD only accepts positive arguments", + // LCM + "=LCM()": "LCM requires at least 1 argument", + "=LCM(-1)": "LCM only accepts positive arguments", + "=LCM(1,-1)": "LCM only accepts positive arguments", // POWER "=POWER(0,0)": "#NUM!", "=POWER(0,-1)": "#DIV/0!", "=POWER(1)": "POWER requires 2 numeric arguments", + // SIGN + "=SIGN()": "SIGN requires 1 numeric arguments", // SQRT "=SQRT(-1)": "#NUM!", "=SQRT(1,2)": "SQRT requires 1 numeric arguments", @@ -68,6 +106,9 @@ func TestCalcCellValue(t *testing.T) { } referenceCalc := map[string]string{ + // PRODUCT + "=PRODUCT(Sheet1!A1:Sheet1!A1:A2,A2)": "4", + // SUM "=A1/A3": "0.3333333333333333", "=SUM(A1:A2)": "3", "=SUM(Sheet1!A1,A2)": "3", @@ -77,8 +118,6 @@ func TestCalcCellValue(t *testing.T) { "=1+SUM(SUM(A1+A2/A3)*(2-3),2)": "1.3333333333333335", "=A1/A2/SUM(A1:A2:B1)": "0.07142857142857142", "=A1/A2/SUM(A1:A2:B1)*A3": "0.21428571428571427", - // PRODUCT - "=PRODUCT(Sheet1!A1:Sheet1!A1:A2,A2)": "4", } for formula, expected := range referenceCalc { f := prepareData() -- cgit v1.2.1 From 789adf9202b4bc04e74ea8fe72138f1942b2cc0c Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 4 May 2020 18:18:05 +0800 Subject: fn: ACOS, ACOSH, ACOT, ACOTH, ARABIC, ASIN, ASINH, ATANH, ATAN2, BASE --- calc.go | 362 +++++++++++++++++++++++++++++++++++++++++++++++++++++------ calc_test.go | 71 +++++++++++- 2 files changed, 394 insertions(+), 39 deletions(-) diff --git a/calc.go b/calc.go index 5ebdcf7..7c912eb 100644 --- a/calc.go +++ b/calc.go @@ -102,11 +102,17 @@ func getPriority(token efp.Token) (pri int) { // opf - Operation formula // opfd - Operand of the operation formula // opft - Operator of the operation formula +// +// Evaluate arguments of the operation formula by list: +// // args - Arguments of the operation formula // +// TODO: handle subtypes: Nothing, Text, Logical, Error, Concatenation, Intersection, Union +// func (f *File) evalInfixExp(sheet string, tokens []efp.Token) (efp.Token, error) { var err error - opdStack, optStack, opfStack, opfdStack, opftStack, argsStack := NewStack(), NewStack(), NewStack(), NewStack(), NewStack(), NewStack() + opdStack, optStack, opfStack, opfdStack, opftStack := NewStack(), NewStack(), NewStack(), NewStack(), NewStack() + argsList := list.New() for i := 0; i < len(tokens); i++ { token := tokens[i] @@ -155,7 +161,7 @@ func (f *File) evalInfixExp(sheet string, tokens []efp.Token) (efp.Token, error) return efp.Token{TValue: formulaErrorNAME}, err } for _, val := range result { - argsStack.Push(efp.Token{ + argsList.PushBack(efp.Token{ TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber, TValue: val, @@ -184,11 +190,20 @@ func (f *File) evalInfixExp(sheet string, tokens []efp.Token) (efp.Token, error) opftStack.Pop() } if !opfdStack.Empty() { - argsStack.Push(opfdStack.Pop()) + argsList.PushBack(opfdStack.Pop()) } continue } + // current token is logical + if token.TType == efp.OperatorsInfix && token.TSubType == efp.TokenSubTypeLogical { + } + + // current token is text + if token.TType == efp.TokenTypeOperand && token.TSubType == efp.TokenSubTypeText { + argsList.PushBack(token) + } + // current token is function stop if token.TType == efp.TokenTypeFunction && token.TSubType == efp.TokenSubTypeStop { for !opftStack.Empty() { @@ -202,13 +217,14 @@ func (f *File) evalInfixExp(sheet string, tokens []efp.Token) (efp.Token, error) // push opfd to args if opfdStack.Len() > 0 { - argsStack.Push(opfdStack.Pop()) + argsList.PushBack(opfdStack.Pop()) } // call formula function to evaluate - result, err := callFuncByName(&formulaFuncs{}, opfStack.Peek().(efp.Token).TValue, []reflect.Value{reflect.ValueOf(argsStack)}) + result, err := callFuncByName(&formulaFuncs{}, strings.ReplaceAll(opfStack.Peek().(efp.Token).TValue, "_xlfn.", ""), []reflect.Value{reflect.ValueOf(argsList)}) if err != nil { return efp.Token{}, err } + argsList.Init() opfStack.Pop() if opfStack.Len() > 0 { // still in function stack opfdStack.Push(efp.Token{TValue: result, TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) @@ -480,13 +496,13 @@ func callFuncByName(receiver interface{}, name string, params []reflect.Value) ( // // ABS(number) // -func (fn *formulaFuncs) ABS(argsStack *Stack) (result string, err error) { - if argsStack.Len() != 1 { +func (fn *formulaFuncs) ABS(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { err = errors.New("ABS requires 1 numeric arguments") return } var val float64 - val, err = strconv.ParseFloat(argsStack.Pop().(efp.Token).TValue, 64) + val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64) if err != nil { return } @@ -494,6 +510,236 @@ func (fn *formulaFuncs) ABS(argsStack *Stack) (result string, err error) { return } +// ACOS function calculates the arccosine (i.e. the inverse cosine) of a given +// number, and returns an angle, in radians, between 0 and π. The syntax of +// the function is: +// +// ACOS(number) +// +func (fn *formulaFuncs) ACOS(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("ACOS requires 1 numeric arguments") + return + } + var val float64 + val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64) + if err != nil { + return + } + result = fmt.Sprintf("%g", math.Acos(val)) + return +} + +// ACOSH function calculates the inverse hyperbolic cosine of a supplied number. +// of the function is: +// +// ACOSH(number) +// +func (fn *formulaFuncs) ACOSH(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("ACOSH requires 1 numeric arguments") + return + } + var val float64 + val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64) + if err != nil { + return + } + result = fmt.Sprintf("%g", math.Acosh(val)) + return +} + +// ACOT function calculates the arccotangent (i.e. the inverse cotangent) of a +// given number, and returns an angle, in radians, between 0 and π. The syntax +// of the function is: +// +// ACOT(number) +// +func (fn *formulaFuncs) ACOT(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("ACOT requires 1 numeric arguments") + return + } + var val float64 + val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64) + if err != nil { + return + } + result = fmt.Sprintf("%g", math.Pi/2-math.Atan(val)) + return +} + +// ACOTH function calculates the hyperbolic arccotangent (coth) of a supplied +// value. The syntax of the function is: +// +// ACOTH(number) +// +func (fn *formulaFuncs) ACOTH(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("ACOTH requires 1 numeric arguments") + return + } + var val float64 + val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64) + if err != nil { + return + } + result = fmt.Sprintf("%g", math.Atanh(1/val)) + return +} + +// ARABIC function converts a Roman numeral into an Arabic numeral. The syntax +// of the function is: +// +// ARABIC(text) +// +func (fn *formulaFuncs) ARABIC(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("ARABIC requires 1 numeric arguments") + return + } + val, last, prefix := 0.0, 0.0, 1.0 + for _, char := range argsList.Front().Value.(efp.Token).TValue { + digit := 0.0 + switch char { + case '-': + prefix = -1 + continue + case 'I': + digit = 1 + case 'V': + digit = 5 + case 'X': + digit = 10 + case 'L': + digit = 50 + case 'C': + digit = 100 + case 'D': + digit = 500 + case 'M': + digit = 1000 + } + val += digit + switch { + case last == digit && (last == 5 || last == 50 || last == 500): + result = formulaErrorVALUE + return + case 2*last == digit: + result = formulaErrorVALUE + return + } + if last < digit { + val -= 2 * last + } + last = digit + } + result = fmt.Sprintf("%g", prefix*val) + return +} + +// ASIN function calculates the arcsine (i.e. the inverse sine) of a given +// number, and returns an angle, in radians, between -π/2 and π/2. The syntax +// of the function is: +// +// ASIN(number) +// +func (fn *formulaFuncs) ASIN(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("ASIN requires 1 numeric arguments") + return + } + var val float64 + val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64) + if err != nil { + return + } + result = fmt.Sprintf("%g", math.Asin(val)) + return +} + +// ASINH function calculates the inverse hyperbolic sine of a supplied number. +// The syntax of the function is: +// +// ASINH(number) +// +func (fn *formulaFuncs) ASINH(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("ASINH requires 1 numeric arguments") + return + } + var val float64 + val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64) + if err != nil { + return + } + result = fmt.Sprintf("%g", math.Asinh(val)) + return +} + +// ATAN function calculates the arctangent (i.e. the inverse tangent) of a +// given number, and returns an angle, in radians, between -π/2 and +π/2. The +// syntax of the function is: +// +// ATAN(number) +// +func (fn *formulaFuncs) ATAN(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("ATAN requires 1 numeric arguments") + return + } + var val float64 + val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64) + if err != nil { + return + } + result = fmt.Sprintf("%g", math.Atan(val)) + return +} + +// ATANH function calculates the inverse hyperbolic tangent of a supplied +// number. The syntax of the function is: +// +// ATANH(number) +// +func (fn *formulaFuncs) ATANH(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("ATANH requires 1 numeric arguments") + return + } + var val float64 + val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64) + if err != nil { + return + } + result = fmt.Sprintf("%g", math.Atanh(val)) + return +} + +// ATAN2 function calculates the arctangent (i.e. the inverse tangent) of a +// given set of x and y coordinates, and returns an angle, in radians, between +// -π/2 and +π/2. The syntax of the function is: +// +// ATAN2(x_num,y_num) +// +func (fn *formulaFuncs) ATAN2(argsList *list.List) (result string, err error) { + if argsList.Len() != 2 { + err = errors.New("ATAN2 requires 2 numeric arguments") + return + } + var x, y float64 + x, err = strconv.ParseFloat(argsList.Back().Value.(efp.Token).TValue, 64) + if err != nil { + return + } + y, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64) + if err != nil { + return + } + result = fmt.Sprintf("%g", math.Atan2(x, y)) + return +} + // gcd returns the greatest common divisor of two supplied integers. func gcd(x, y float64) float64 { x, y = math.Trunc(x), math.Trunc(y) @@ -513,13 +759,55 @@ func gcd(x, y float64) float64 { return x } +// BASE function converts a number into a supplied base (radix), and returns a +// text representation of the calculated value. The syntax of the function is: +// +// BASE(number,radix,[min_length]) +// +func (fn *formulaFuncs) BASE(argsList *list.List) (result string, err error) { + if argsList.Len() < 2 { + err = errors.New("BASE requires at least 2 arguments") + return + } + if argsList.Len() > 3 { + err = errors.New("BASE allows at most 3 arguments") + return + } + var number float64 + var radix, minLength int + number, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64) + if err != nil { + return + } + radix, err = strconv.Atoi(argsList.Front().Next().Value.(efp.Token).TValue) + if err != nil { + return + } + if radix < 2 || radix > 36 { + err = errors.New("radix must be an integer ≥ 2 and ≤ 36") + return + } + if argsList.Len() > 2 { + minLength, err = strconv.Atoi(argsList.Back().Value.(efp.Token).TValue) + if err != nil { + return + } + } + result = strconv.FormatInt(int64(number), radix) + if len(result) < minLength { + result = strings.Repeat("0", minLength-len(result)) + result + } + result = strings.ToUpper(result) + return +} + // GCD function returns the greatest common divisor of two or more supplied -// integers.The syntax of the function is: +// integers. The syntax of the function is: // // GCD(number1,[number2],...) // -func (fn *formulaFuncs) GCD(argsStack *Stack) (result string, err error) { - if argsStack.Len() == 0 { +func (fn *formulaFuncs) GCD(argsList *list.List) (result string, err error) { + if argsList.Len() == 0 { err = errors.New("GCD requires at least 1 argument") return } @@ -527,8 +815,8 @@ func (fn *formulaFuncs) GCD(argsStack *Stack) (result string, err error) { val float64 nums = []float64{} ) - for !argsStack.Empty() { - token := argsStack.Pop().(efp.Token) + for arg := argsList.Front(); arg != nil; arg = arg.Next() { + token := arg.Value.(efp.Token) if token.TValue == "" { continue } @@ -573,8 +861,8 @@ func lcm(a, b float64) float64 { // // LCM(number1,[number2],...) // -func (fn *formulaFuncs) LCM(argsStack *Stack) (result string, err error) { - if argsStack.Len() == 0 { +func (fn *formulaFuncs) LCM(argsList *list.List) (result string, err error) { + if argsList.Len() == 0 { err = errors.New("LCM requires at least 1 argument") return } @@ -582,8 +870,8 @@ func (fn *formulaFuncs) LCM(argsStack *Stack) (result string, err error) { val float64 nums = []float64{} ) - for !argsStack.Empty() { - token := argsStack.Pop().(efp.Token) + for arg := argsList.Front(); arg != nil; arg = arg.Next() { + token := arg.Value.(efp.Token) if token.TValue == "" { continue } @@ -618,17 +906,17 @@ func (fn *formulaFuncs) LCM(argsStack *Stack) (result string, err error) { // // POWER(number,power) // -func (fn *formulaFuncs) POWER(argsStack *Stack) (result string, err error) { - if argsStack.Len() != 2 { +func (fn *formulaFuncs) POWER(argsList *list.List) (result string, err error) { + if argsList.Len() != 2 { err = errors.New("POWER requires 2 numeric arguments") return } var x, y float64 - y, err = strconv.ParseFloat(argsStack.Pop().(efp.Token).TValue, 64) + x, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64) if err != nil { return } - x, err = strconv.ParseFloat(argsStack.Pop().(efp.Token).TValue, 64) + y, err = strconv.ParseFloat(argsList.Back().Value.(efp.Token).TValue, 64) if err != nil { return } @@ -649,13 +937,13 @@ func (fn *formulaFuncs) POWER(argsStack *Stack) (result string, err error) { // // PRODUCT(number1,[number2],...) // -func (fn *formulaFuncs) PRODUCT(argsStack *Stack) (result string, err error) { +func (fn *formulaFuncs) PRODUCT(argsList *list.List) (result string, err error) { var ( val float64 product float64 = 1 ) - for !argsStack.Empty() { - token := argsStack.Pop().(efp.Token) + for arg := argsList.Front(); arg != nil; arg = arg.Next() { + token := arg.Value.(efp.Token) if token.TValue == "" { continue } @@ -676,13 +964,13 @@ func (fn *formulaFuncs) PRODUCT(argsStack *Stack) (result string, err error) { // // SIGN(number) // -func (fn *formulaFuncs) SIGN(argsStack *Stack) (result string, err error) { - if argsStack.Len() != 1 { +func (fn *formulaFuncs) SIGN(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { err = errors.New("SIGN requires 1 numeric arguments") return } var val float64 - val, err = strconv.ParseFloat(argsStack.Pop().(efp.Token).TValue, 64) + val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64) if err != nil { return } @@ -703,13 +991,13 @@ func (fn *formulaFuncs) SIGN(argsStack *Stack) (result string, err error) { // // SQRT(number) // -func (fn *formulaFuncs) SQRT(argsStack *Stack) (result string, err error) { - if argsStack.Len() != 1 { +func (fn *formulaFuncs) SQRT(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { err = errors.New("SQRT requires 1 numeric arguments") return } var val float64 - val, err = strconv.ParseFloat(argsStack.Pop().(efp.Token).TValue, 64) + val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64) if err != nil { return } @@ -726,11 +1014,11 @@ func (fn *formulaFuncs) SQRT(argsStack *Stack) (result string, err error) { // // SUM(number1,[number2],...) // -func (fn *formulaFuncs) SUM(argsStack *Stack) (result string, err error) { +func (fn *formulaFuncs) SUM(argsList *list.List) (result string, err error) { var val float64 var sum float64 - for !argsStack.Empty() { - token := argsStack.Pop().(efp.Token) + for arg := argsList.Front(); arg != nil; arg = arg.Next() { + token := arg.Value.(efp.Token) if token.TValue == "" { continue } @@ -749,17 +1037,17 @@ func (fn *formulaFuncs) SUM(argsStack *Stack) (result string, err error) { // // QUOTIENT(numerator,denominator) // -func (fn *formulaFuncs) QUOTIENT(argsStack *Stack) (result string, err error) { - if argsStack.Len() != 2 { +func (fn *formulaFuncs) QUOTIENT(argsList *list.List) (result string, err error) { + if argsList.Len() != 2 { err = errors.New("QUOTIENT requires 2 numeric arguments") return } var x, y float64 - y, err = strconv.ParseFloat(argsStack.Pop().(efp.Token).TValue, 64) + x, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64) if err != nil { return } - x, err = strconv.ParseFloat(argsStack.Pop().(efp.Token).TValue, 64) + y, err = strconv.ParseFloat(argsList.Back().Value.(efp.Token).TValue, 64) if err != nil { return } diff --git a/calc_test.go b/calc_test.go index 84fa955..bb8ae8a 100644 --- a/calc_test.go +++ b/calc_test.go @@ -24,6 +24,49 @@ func TestCalcCellValue(t *testing.T) { "=ABS(6.5)": "6.5", "=ABS(0)": "0", "=ABS(2-4.5)": "2.5", + // ACOS + "=ACOS(-1)": "3.141592653589793", + "=ACOS(0)": "1.5707963267948966", + // ACOSH + "=ACOSH(1)": "0", + "=ACOSH(2.5)": "1.566799236972411", + "=ACOSH(5)": "2.2924316695611777", + // ACOT + "=_xlfn.ACOT(1)": "0.7853981633974483", + "=_xlfn.ACOT(-2)": "2.677945044588987", + "=_xlfn.ACOT(0)": "1.5707963267948966", + // ACOTH + "=_xlfn.ACOTH(-5)": "-0.2027325540540822", + "=_xlfn.ACOTH(1.1)": "1.5222612188617113", + "=_xlfn.ACOTH(2)": "0.5493061443340548", + // ARABIC + `=_xlfn.ARABIC("IV")`: "4", + `=_xlfn.ARABIC("-IV")`: "-4", + `=_xlfn.ARABIC("MCXX")`: "1120", + `=_xlfn.ARABIC("")`: "0", + // ASIN + "=ASIN(-1)": "-1.5707963267948966", + "=ASIN(0)": "0", + // ASINH + "=ASINH(0)": "0", + "=ASINH(-0.5)": "-0.48121182505960347", + "=ASINH(2)": "1.4436354751788103", + // ATAN + "=ATAN(-1)": "-0.7853981633974483", + "=ATAN(0)": "0", + "=ATAN(1)": "0.7853981633974483", + // ATANH + "=ATANH(-0.8)": "-1.0986122886681098", + "=ATANH(0)": "0", + "=ATANH(0.5)": "0.5493061443340548", + // ATAN2 + "=ATAN2(1,1)": "0.7853981633974483", + "=ATAN2(1,-1)": "-0.7853981633974483", + "=ATAN2(4,0)": "0", + // BASE + "=BASE(12,2)": "1100", + "=BASE(12,2,8)": "00001100", + "=BASE(100000,16)": "186A0", // GCD "=GCD(1,5)": "1", "=GCD(15,10,25)": "5", @@ -74,8 +117,32 @@ func TestCalcCellValue(t *testing.T) { } mathCalcError := map[string]string{ // ABS - "=ABS(1,2)": "ABS requires 1 numeric arguments", - "=ABS(~)": `cannot convert cell "~" to coordinates: invalid cell name "~"`, + "=ABS()": "ABS requires 1 numeric arguments", + "=ABS(~)": `cannot convert cell "~" to coordinates: invalid cell name "~"`, + // ACOS + "=ACOS()": "ACOS requires 1 numeric arguments", + // ACOSH + "=ACOSH()": "ACOSH requires 1 numeric arguments", + // ACOT + "=_xlfn.ACOT()": "ACOT requires 1 numeric arguments", + // ACOTH + "=_xlfn.ACOTH()": "ACOTH requires 1 numeric arguments", + // ARABIC + "_xlfn.ARABIC()": "ARABIC requires 1 numeric arguments", + // ASIN + "=ASIN()": "ASIN requires 1 numeric arguments", + // ASINH + "=ASINH()": "ASINH requires 1 numeric arguments", + // ATAN + "=ATAN()": "ATAN requires 1 numeric arguments", + // ATANH + "=ATANH()": "ATANH requires 1 numeric arguments", + // ATAN2 + "=ATAN2()": "ATAN2 requires 2 numeric arguments", + // BASE + "=BASE()": "BASE requires at least 2 arguments", + "=BASE(1,2,3,4)": "BASE allows at most 3 arguments", + "=BASE(1,1)": "radix must be an integer ≥ 2 and ≤ 36", // GCD "=GCD()": "GCD requires at least 1 argument", "=GCD(-1)": "GCD only accepts positive arguments", -- cgit v1.2.1 From 6f796b88e68e927c71e51e22278f4d43b935e00a Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 4 May 2020 21:22:11 +0800 Subject: fn: CEILING, CEILING.MATH --- calc.go | 101 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- calc_test.go | 29 +++++++++++++++-- 2 files changed, 126 insertions(+), 4 deletions(-) diff --git a/calc.go b/calc.go index 7c912eb..568f044 100644 --- a/calc.go +++ b/calc.go @@ -220,7 +220,9 @@ func (f *File) evalInfixExp(sheet string, tokens []efp.Token) (efp.Token, error) argsList.PushBack(opfdStack.Pop()) } // call formula function to evaluate - result, err := callFuncByName(&formulaFuncs{}, strings.ReplaceAll(opfStack.Peek().(efp.Token).TValue, "_xlfn.", ""), []reflect.Value{reflect.ValueOf(argsList)}) + result, err := callFuncByName(&formulaFuncs{}, strings.NewReplacer( + "_xlfn", "", ".", "").Replace(opfStack.Peek().(efp.Token).TValue), + []reflect.Value{reflect.ValueOf(argsList)}) if err != nil { return efp.Token{}, err } @@ -801,6 +803,103 @@ func (fn *formulaFuncs) BASE(argsList *list.List) (result string, err error) { return } +// CEILING function rounds a supplied number away from zero, to the nearest +// multiple of a given number. The syntax of the function is: +// +// CEILING(number,significance) +// +func (fn *formulaFuncs) CEILING(argsList *list.List) (result string, err error) { + if argsList.Len() == 0 { + err = errors.New("CEILING requires at least 1 argument") + return + } + if argsList.Len() > 2 { + err = errors.New("CEILING allows at most 2 arguments") + return + } + var number, significance float64 + number, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64) + if err != nil { + return + } + significance = 1 + if number < 0 { + significance = -1 + } + if argsList.Len() > 1 { + significance, err = strconv.ParseFloat(argsList.Back().Value.(efp.Token).TValue, 64) + if err != nil { + return + } + } + if significance < 0 && number > 0 { + err = errors.New("negative sig to CEILING invalid") + return + } + if argsList.Len() == 1 { + result = fmt.Sprintf("%g", math.Ceil(number)) + return + } + number, res := math.Modf(number / significance) + if res > 0 { + number++ + } + result = fmt.Sprintf("%g", number*significance) + return +} + +// CEILINGMATH function rounds a supplied number up to a supplied multiple of +// significance. The syntax of the function is: +// +// CEILING.MATH(number,[significance],[mode]) +// +func (fn *formulaFuncs) CEILINGMATH(argsList *list.List) (result string, err error) { + if argsList.Len() == 0 { + err = errors.New("CEILING.MATH requires at least 1 argument") + return + } + if argsList.Len() > 3 { + err = errors.New("CEILING.MATH allows at most 3 arguments") + return + } + var number, significance, mode float64 = 0, 1, 1 + number, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64) + if err != nil { + return + } + if number < 0 { + significance = -1 + } + if argsList.Len() > 1 { + significance, err = strconv.ParseFloat(argsList.Front().Next().Value.(efp.Token).TValue, 64) + if err != nil { + return + } + } + if argsList.Len() == 1 { + result = fmt.Sprintf("%g", math.Ceil(number)) + return + } + if argsList.Len() > 2 { + mode, err = strconv.ParseFloat(argsList.Back().Value.(efp.Token).TValue, 64) + if err != nil { + return + } + } + val, res := math.Modf(number / significance) + _, _ = res, mode + if res != 0 { + if number > 0 { + val++ + } else if mode < 0 { + val-- + } + } + + result = fmt.Sprintf("%g", val*significance) + return +} + // GCD function returns the greatest common divisor of two or more supplied // integers. The syntax of the function is: // diff --git a/calc_test.go b/calc_test.go index bb8ae8a..a14cc0c 100644 --- a/calc_test.go +++ b/calc_test.go @@ -67,6 +67,22 @@ func TestCalcCellValue(t *testing.T) { "=BASE(12,2)": "1100", "=BASE(12,2,8)": "00001100", "=BASE(100000,16)": "186A0", + // CEILING + "=CEILING(22.25,0.1)": "22.3", + "=CEILING(22.25,0.5)": "22.5", + "=CEILING(22.25,1)": "23", + "=CEILING(22.25,10)": "30", + "=CEILING(22.25,20)": "40", + "=CEILING(-22.25,-0.1)": "-22.3", + "=CEILING(-22.25,-1)": "-23", + "=CEILING(-22.25,-5)": "-25", + // _xlfn.CEILING.MATH + "=_xlfn.CEILING.MATH(15.25,1)": "16", + "=_xlfn.CEILING.MATH(15.25,0.1)": "15.3", + "=_xlfn.CEILING.MATH(15.25,5)": "20", + "=_xlfn.CEILING.MATH(-15.25,1)": "-15", + "=_xlfn.CEILING.MATH(-15.25,1,1)": "-15", // should be 16 + "=_xlfn.CEILING.MATH(-15.25,10)": "-10", // GCD "=GCD(1,5)": "1", "=GCD(15,10,25)": "5", @@ -123,11 +139,11 @@ func TestCalcCellValue(t *testing.T) { "=ACOS()": "ACOS requires 1 numeric arguments", // ACOSH "=ACOSH()": "ACOSH requires 1 numeric arguments", - // ACOT + // _xlfn.ACOT "=_xlfn.ACOT()": "ACOT requires 1 numeric arguments", - // ACOTH + // _xlfn.ACOTH "=_xlfn.ACOTH()": "ACOTH requires 1 numeric arguments", - // ARABIC + // _xlfn.ARABIC "_xlfn.ARABIC()": "ARABIC requires 1 numeric arguments", // ASIN "=ASIN()": "ASIN requires 1 numeric arguments", @@ -143,6 +159,13 @@ func TestCalcCellValue(t *testing.T) { "=BASE()": "BASE requires at least 2 arguments", "=BASE(1,2,3,4)": "BASE allows at most 3 arguments", "=BASE(1,1)": "radix must be an integer ≥ 2 and ≤ 36", + // CEILING + "=CEILING()": "CEILING requires at least 1 argument", + "=CEILING(1,2,3)": "CEILING allows at most 2 arguments", + "=CEILING(1,-1)": "negative sig to CEILING invalid", + // _xlfn.CEILING.MATH + "=_xlfn.CEILING.MATH()": "CEILING.MATH requires at least 1 argument", + "=_xlfn.CEILING.MATH(1,2,3,4)": "CEILING.MATH allows at most 3 arguments", // GCD "=GCD()": "GCD requires at least 1 argument", "=GCD(-1)": "GCD only accepts positive arguments", -- cgit v1.2.1 From 5c82f2269dfa82c5be3afd7ac140aacbf2221829 Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 5 May 2020 17:27:19 +0800 Subject: #65 fn: CEILING.PRECISE, COMBIN, COMBINA, COS, COSH, COT, COTH, CSC --- calc.go | 319 ++++++++++++++++++++++++++++++++++++++++++++++------------- calc_test.go | 61 +++++++++++- 2 files changed, 312 insertions(+), 68 deletions(-) diff --git a/calc.go b/calc.go index 568f044..ed25a58 100644 --- a/calc.go +++ b/calc.go @@ -504,8 +504,7 @@ func (fn *formulaFuncs) ABS(argsList *list.List) (result string, err error) { return } var val float64 - val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64) - if err != nil { + if val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { return } result = fmt.Sprintf("%g", math.Abs(val)) @@ -524,8 +523,7 @@ func (fn *formulaFuncs) ACOS(argsList *list.List) (result string, err error) { return } var val float64 - val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64) - if err != nil { + if val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { return } result = fmt.Sprintf("%g", math.Acos(val)) @@ -543,8 +541,7 @@ func (fn *formulaFuncs) ACOSH(argsList *list.List) (result string, err error) { return } var val float64 - val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64) - if err != nil { + if val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { return } result = fmt.Sprintf("%g", math.Acosh(val)) @@ -563,8 +560,7 @@ func (fn *formulaFuncs) ACOT(argsList *list.List) (result string, err error) { return } var val float64 - val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64) - if err != nil { + if val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { return } result = fmt.Sprintf("%g", math.Pi/2-math.Atan(val)) @@ -582,8 +578,7 @@ func (fn *formulaFuncs) ACOTH(argsList *list.List) (result string, err error) { return } var val float64 - val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64) - if err != nil { + if val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { return } result = fmt.Sprintf("%g", math.Atanh(1/val)) @@ -652,8 +647,7 @@ func (fn *formulaFuncs) ASIN(argsList *list.List) (result string, err error) { return } var val float64 - val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64) - if err != nil { + if val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { return } result = fmt.Sprintf("%g", math.Asin(val)) @@ -671,8 +665,7 @@ func (fn *formulaFuncs) ASINH(argsList *list.List) (result string, err error) { return } var val float64 - val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64) - if err != nil { + if val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { return } result = fmt.Sprintf("%g", math.Asinh(val)) @@ -691,8 +684,7 @@ func (fn *formulaFuncs) ATAN(argsList *list.List) (result string, err error) { return } var val float64 - val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64) - if err != nil { + if val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { return } result = fmt.Sprintf("%g", math.Atan(val)) @@ -710,8 +702,7 @@ func (fn *formulaFuncs) ATANH(argsList *list.List) (result string, err error) { return } var val float64 - val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64) - if err != nil { + if val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { return } result = fmt.Sprintf("%g", math.Atanh(val)) @@ -730,12 +721,10 @@ func (fn *formulaFuncs) ATAN2(argsList *list.List) (result string, err error) { return } var x, y float64 - x, err = strconv.ParseFloat(argsList.Back().Value.(efp.Token).TValue, 64) - if err != nil { + if x, err = strconv.ParseFloat(argsList.Back().Value.(efp.Token).TValue, 64); err != nil { return } - y, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64) - if err != nil { + if y, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { return } result = fmt.Sprintf("%g", math.Atan2(x, y)) @@ -777,12 +766,10 @@ func (fn *formulaFuncs) BASE(argsList *list.List) (result string, err error) { } var number float64 var radix, minLength int - number, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64) - if err != nil { + if number, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { return } - radix, err = strconv.Atoi(argsList.Front().Next().Value.(efp.Token).TValue) - if err != nil { + if radix, err = strconv.Atoi(argsList.Front().Next().Value.(efp.Token).TValue); err != nil { return } if radix < 2 || radix > 36 { @@ -790,8 +777,7 @@ func (fn *formulaFuncs) BASE(argsList *list.List) (result string, err error) { return } if argsList.Len() > 2 { - minLength, err = strconv.Atoi(argsList.Back().Value.(efp.Token).TValue) - if err != nil { + if minLength, err = strconv.Atoi(argsList.Back().Value.(efp.Token).TValue); err != nil { return } } @@ -817,18 +803,15 @@ func (fn *formulaFuncs) CEILING(argsList *list.List) (result string, err error) err = errors.New("CEILING allows at most 2 arguments") return } - var number, significance float64 - number, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64) - if err != nil { + var number, significance float64 = 0, 1 + if number, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { return } - significance = 1 if number < 0 { significance = -1 } if argsList.Len() > 1 { - significance, err = strconv.ParseFloat(argsList.Back().Value.(efp.Token).TValue, 64) - if err != nil { + if significance, err = strconv.ParseFloat(argsList.Back().Value.(efp.Token).TValue, 64); err != nil { return } } @@ -863,16 +846,14 @@ func (fn *formulaFuncs) CEILINGMATH(argsList *list.List) (result string, err err return } var number, significance, mode float64 = 0, 1, 1 - number, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64) - if err != nil { + if number, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { return } if number < 0 { significance = -1 } if argsList.Len() > 1 { - significance, err = strconv.ParseFloat(argsList.Front().Next().Value.(efp.Token).TValue, 64) - if err != nil { + if significance, err = strconv.ParseFloat(argsList.Front().Next().Value.(efp.Token).TValue, 64); err != nil { return } } @@ -881,13 +862,11 @@ func (fn *formulaFuncs) CEILINGMATH(argsList *list.List) (result string, err err return } if argsList.Len() > 2 { - mode, err = strconv.ParseFloat(argsList.Back().Value.(efp.Token).TValue, 64) - if err != nil { + if mode, err = strconv.ParseFloat(argsList.Back().Value.(efp.Token).TValue, 64); err != nil { return } } val, res := math.Modf(number / significance) - _, _ = res, mode if res != 0 { if number > 0 { val++ @@ -895,11 +874,231 @@ func (fn *formulaFuncs) CEILINGMATH(argsList *list.List) (result string, err err val-- } } + result = fmt.Sprintf("%g", val*significance) + return +} +// CEILINGPRECISE function rounds a supplied number up (regardless of the +// number's sign), to the nearest multiple of a given number. The syntax of +// the function is: +// +// CEILING.PRECISE(number,[significance]) +// +func (fn *formulaFuncs) CEILINGPRECISE(argsList *list.List) (result string, err error) { + if argsList.Len() == 0 { + err = errors.New("CEILING.PRECISE requires at least 1 argument") + return + } + if argsList.Len() > 2 { + err = errors.New("CEILING.PRECISE allows at most 2 arguments") + return + } + var number, significance float64 = 0, 1 + if number, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { + return + } + if number < 0 { + significance = -1 + } + if argsList.Len() == 1 { + result = fmt.Sprintf("%g", math.Ceil(number)) + return + } + if argsList.Len() > 1 { + if significance, err = strconv.ParseFloat(argsList.Back().Value.(efp.Token).TValue, 64); err != nil { + return + } + significance = math.Abs(significance) + if significance == 0 { + result = "0" + return + } + } + val, res := math.Modf(number / significance) + if res != 0 { + if number > 0 { + val++ + } + } result = fmt.Sprintf("%g", val*significance) return } +// COMBIN function calculates the number of combinations (in any order) of a +// given number objects from a set. The syntax of the function is: +// +// COMBIN(number,number_chosen) +// +func (fn *formulaFuncs) COMBIN(argsList *list.List) (result string, err error) { + if argsList.Len() != 2 { + err = errors.New("COMBIN requires 2 argument") + return + } + var number, chosen, val float64 = 0, 0, 1 + if number, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { + return + } + if chosen, err = strconv.ParseFloat(argsList.Back().Value.(efp.Token).TValue, 64); err != nil { + return + } + number, chosen = math.Trunc(number), math.Trunc(chosen) + if chosen > number { + err = errors.New("COMBIN requires number >= number_chosen") + return + } + if chosen == number || chosen == 0 { + result = "1" + return + } + for c := float64(1); c <= chosen; c++ { + val *= (number + 1 - c) / c + } + result = fmt.Sprintf("%g", math.Ceil(val)) + return +} + +// COMBINA function calculates the number of combinations, with repetitions, +// of a given number objects from a set. The syntax of the function is: +// +// COMBINA(number,number_chosen) +// +func (fn *formulaFuncs) COMBINA(argsList *list.List) (result string, err error) { + if argsList.Len() != 2 { + err = errors.New("COMBINA requires 2 argument") + return + } + var number, chosen float64 + if number, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { + return + } + if chosen, err = strconv.ParseFloat(argsList.Back().Value.(efp.Token).TValue, 64); err != nil { + return + } + number, chosen = math.Trunc(number), math.Trunc(chosen) + if number < chosen { + err = errors.New("COMBINA requires number > number_chosen") + return + } + if number == 0 { + result = "0" + return + } + args := list.New() + args.PushBack(efp.Token{ + TValue: fmt.Sprintf("%g", number+chosen-1), + TType: efp.TokenTypeOperand, + TSubType: efp.TokenSubTypeNumber, + }) + args.PushBack(efp.Token{ + TValue: fmt.Sprintf("%g", number-1), + TType: efp.TokenTypeOperand, + TSubType: efp.TokenSubTypeNumber, + }) + return fn.COMBIN(args) +} + +// COS function calculates the cosine of a given angle. The syntax of the +// function is: +// +// COS(number) +// +func (fn *formulaFuncs) COS(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("COS requires 1 numeric arguments") + return + } + var val float64 + if val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { + return + } + result = fmt.Sprintf("%g", math.Cos(val)) + return +} + +// COSH function calculates the hyperbolic cosine (cosh) of a supplied number. +// The syntax of the function is: +// +// COSH(number) +// +func (fn *formulaFuncs) COSH(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("COSH requires 1 numeric arguments") + return + } + var val float64 + if val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { + return + } + result = fmt.Sprintf("%g", math.Cosh(val)) + return +} + +// COT function calculates the cotangent of a given angle. The syntax of the +// function is: +// +// COT(number) +// +func (fn *formulaFuncs) COT(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("COT requires 1 numeric arguments") + return + } + var val float64 + if val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { + return + } + if val == 0 { + err = errors.New(formulaErrorNAME) + return + } + result = fmt.Sprintf("%g", math.Tan(val)) + return +} + +// COTH function calculates the hyperbolic cotangent (coth) of a supplied +// angle. The syntax of the function is: +// +// COTH(number) +// +func (fn *formulaFuncs) COTH(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("COTH requires 1 numeric arguments") + return + } + var val float64 + if val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { + return + } + if val == 0 { + err = errors.New(formulaErrorNAME) + return + } + result = fmt.Sprintf("%g", math.Tanh(val)) + return +} + +// CSC function calculates the cosecant of a given angle. The syntax of the +// function is: +// +// CSC(number) +// +func (fn *formulaFuncs) CSC(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("CSC requires 1 numeric arguments") + return + } + var val float64 + if val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { + return + } + if val == 0 { + err = errors.New(formulaErrorNAME) + return + } + result = fmt.Sprintf("%g", 1/math.Sin(val)) + return +} + // GCD function returns the greatest common divisor of two or more supplied // integers. The syntax of the function is: // @@ -919,8 +1118,7 @@ func (fn *formulaFuncs) GCD(argsList *list.List) (result string, err error) { if token.TValue == "" { continue } - val, err = strconv.ParseFloat(token.TValue, 64) - if err != nil { + if val, err = strconv.ParseFloat(token.TValue, 64); err != nil { return } nums = append(nums, val) @@ -974,8 +1172,7 @@ func (fn *formulaFuncs) LCM(argsList *list.List) (result string, err error) { if token.TValue == "" { continue } - val, err = strconv.ParseFloat(token.TValue, 64) - if err != nil { + if val, err = strconv.ParseFloat(token.TValue, 64); err != nil { return } nums = append(nums, val) @@ -1011,12 +1208,10 @@ func (fn *formulaFuncs) POWER(argsList *list.List) (result string, err error) { return } var x, y float64 - x, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64) - if err != nil { + if x, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { return } - y, err = strconv.ParseFloat(argsList.Back().Value.(efp.Token).TValue, 64) - if err != nil { + if y, err = strconv.ParseFloat(argsList.Back().Value.(efp.Token).TValue, 64); err != nil { return } if x == 0 && y == 0 { @@ -1037,17 +1232,13 @@ func (fn *formulaFuncs) POWER(argsList *list.List) (result string, err error) { // PRODUCT(number1,[number2],...) // func (fn *formulaFuncs) PRODUCT(argsList *list.List) (result string, err error) { - var ( - val float64 - product float64 = 1 - ) + var val, product float64 = 0, 1 for arg := argsList.Front(); arg != nil; arg = arg.Next() { token := arg.Value.(efp.Token) if token.TValue == "" { continue } - val, err = strconv.ParseFloat(token.TValue, 64) - if err != nil { + if val, err = strconv.ParseFloat(token.TValue, 64); err != nil { return } product = product * val @@ -1069,8 +1260,7 @@ func (fn *formulaFuncs) SIGN(argsList *list.List) (result string, err error) { return } var val float64 - val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64) - if err != nil { + if val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { return } if val < 0 { @@ -1096,8 +1286,7 @@ func (fn *formulaFuncs) SQRT(argsList *list.List) (result string, err error) { return } var val float64 - val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64) - if err != nil { + if val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { return } if val < 0 { @@ -1114,15 +1303,13 @@ func (fn *formulaFuncs) SQRT(argsList *list.List) (result string, err error) { // SUM(number1,[number2],...) // func (fn *formulaFuncs) SUM(argsList *list.List) (result string, err error) { - var val float64 - var sum float64 + var val, sum float64 for arg := argsList.Front(); arg != nil; arg = arg.Next() { token := arg.Value.(efp.Token) if token.TValue == "" { continue } - val, err = strconv.ParseFloat(token.TValue, 64) - if err != nil { + if val, err = strconv.ParseFloat(token.TValue, 64); err != nil { return } sum += val @@ -1142,12 +1329,10 @@ func (fn *formulaFuncs) QUOTIENT(argsList *list.List) (result string, err error) return } var x, y float64 - x, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64) - if err != nil { + if x, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { return } - y, err = strconv.ParseFloat(argsList.Back().Value.(efp.Token).TValue, 64) - if err != nil { + if y, err = strconv.ParseFloat(argsList.Back().Value.(efp.Token).TValue, 64); err != nil { return } if y == 0 { diff --git a/calc_test.go b/calc_test.go index a14cc0c..5d441ff 100644 --- a/calc_test.go +++ b/calc_test.go @@ -83,6 +83,44 @@ func TestCalcCellValue(t *testing.T) { "=_xlfn.CEILING.MATH(-15.25,1)": "-15", "=_xlfn.CEILING.MATH(-15.25,1,1)": "-15", // should be 16 "=_xlfn.CEILING.MATH(-15.25,10)": "-10", + // _xlfn.CEILING.PRECISE + "=_xlfn.CEILING.PRECISE(22.25,0.1)": "22.3", + "=_xlfn.CEILING.PRECISE(22.25,0.5)": "22.5", + "=_xlfn.CEILING.PRECISE(22.25,1)": "23", + "=_xlfn.CEILING.PRECISE(22.25)": "23", + "=_xlfn.CEILING.PRECISE(22.25,10)": "30", + "=_xlfn.CEILING.PRECISE(22.25,0)": "0", + "=_xlfn.CEILING.PRECISE(-22.25,1)": "-22", + "=_xlfn.CEILING.PRECISE(-22.25,-1)": "-22", + "=_xlfn.CEILING.PRECISE(-22.25,5)": "-20", + // COMBIN + "=COMBIN(6,1)": "6", + "=COMBIN(6,2)": "15", + "=COMBIN(6,3)": "20", + "=COMBIN(6,4)": "15", + "=COMBIN(6,5)": "6", + "=COMBIN(6,6)": "1", + // _xlfn.COMBINA + "=_xlfn.COMBINA(6,1)": "6", + "=_xlfn.COMBINA(6,2)": "21", + "=_xlfn.COMBINA(6,3)": "56", + "=_xlfn.COMBINA(6,4)": "126", + "=_xlfn.COMBINA(6,5)": "252", + "=_xlfn.COMBINA(6,6)": "462", + // COS + "=COS(0.785398163)": "0.707106781467586", + "=COS(0)": "1", + // COSH + "=COSH(0)": "1", + "=COSH(0.5)": "1.1276259652063807", + "=COSH(-2)": "3.7621956910836314", + // _xlfn.COT + "_xlfn.COT(0.785398163397448)": "0.9999999999999992", + // _xlfn.COTH + "_xlfn.COTH(-3.14159265358979)": "-0.9962720762207499", + // _xlfn.CSC + "_xlfn.CSC(-6)": "3.5788995472544056", + "_xlfn.CSC(1.5707963267949)": "1", // GCD "=GCD(1,5)": "1", "=GCD(15,10,25)": "5", @@ -144,7 +182,7 @@ func TestCalcCellValue(t *testing.T) { // _xlfn.ACOTH "=_xlfn.ACOTH()": "ACOTH requires 1 numeric arguments", // _xlfn.ARABIC - "_xlfn.ARABIC()": "ARABIC requires 1 numeric arguments", + "=_xlfn.ARABIC()": "ARABIC requires 1 numeric arguments", // ASIN "=ASIN()": "ASIN requires 1 numeric arguments", // ASINH @@ -166,6 +204,27 @@ func TestCalcCellValue(t *testing.T) { // _xlfn.CEILING.MATH "=_xlfn.CEILING.MATH()": "CEILING.MATH requires at least 1 argument", "=_xlfn.CEILING.MATH(1,2,3,4)": "CEILING.MATH allows at most 3 arguments", + // _xlfn.CEILING.PRECISE + "=_xlfn.CEILING.PRECISE()": "CEILING.PRECISE requires at least 1 argument", + "=_xlfn.CEILING.PRECISE(1,2,3)": "CEILING.PRECISE allows at most 2 arguments", + // COMBIN + "=COMBIN()": "COMBIN requires 2 argument", + "=COMBIN(-1,1)": "COMBIN requires number >= number_chosen", + // _xlfn.COMBINA + "=_xlfn.COMBINA()": "COMBINA requires 2 argument", + "=_xlfn.COMBINA(-1,1)": "COMBINA requires number > number_chosen", + "=_xlfn.COMBINA(-1,-1)": "COMBIN requires number >= number_chosen", + // COS + "=COS()": "COS requires 1 numeric arguments", + // COSH + "=COSH()": "COSH requires 1 numeric arguments", + // _xlfn.COT + "=COT()": "COT requires 1 numeric arguments", + // _xlfn.COTH + "=COTH()": "COTH requires 1 numeric arguments", + // _xlfn.CSC + "_xlfn.CSC()": "CSC requires 1 numeric arguments", + "_xlfn.CSC(0)": "#NAME?", // GCD "=GCD()": "GCD requires at least 1 argument", "=GCD(-1)": "GCD only accepts positive arguments", -- cgit v1.2.1 From 97e3f4ce6822bea6d65961c0399f7563450b69b4 Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 6 May 2020 00:01:31 +0800 Subject: #65 fn: CSCH, DECIMAL, DEGREES, EVEN, EXP, FACT, FACTDOUBLE, FLOOR, FLOOR.MATH, FLOOR.PRECISE, INT, ISO.CEILING, LN, LOG, LOG10, MDETERM --- calc.go | 719 +++++++++++++++++++++++++++++++++++++++++++++++++++-------- calc_test.go | 178 +++++++++++++-- 2 files changed, 777 insertions(+), 120 deletions(-) diff --git a/calc.go b/calc.go index ed25a58..2ab3d61 100644 --- a/calc.go +++ b/calc.go @@ -37,19 +37,26 @@ const ( formulaErrorGETTINGDATA = "#GETTING_DATA" ) -// cellRef defines the structure of a cell reference +// cellRef defines the structure of a cell reference. type cellRef struct { Col int Row int Sheet string } -// cellRef defines the structure of a cell range +// cellRef defines the structure of a cell range. type cellRange struct { From cellRef To cellRef } +// formulaArg is the argument of a formula or function. +type formulaArg struct { + Value string + Matrix []string +} + +// formulaFuncs is the type of the formula functions. type formulaFuncs struct{} // CalcCellValue provides a function to get calculated cell value. This @@ -140,7 +147,7 @@ func (f *File) evalInfixExp(sheet string, tokens []efp.Token) (efp.Token, error) if token.TSubType == efp.TokenSubTypeRange { if !opftStack.Empty() { // parse reference: must reference at here - result, err := f.parseReference(sheet, token.TValue) + result, _, err := f.parseReference(sheet, token.TValue) if err != nil { return efp.Token{TValue: formulaErrorNAME}, err } @@ -156,16 +163,16 @@ func (f *File) evalInfixExp(sheet string, tokens []efp.Token) (efp.Token, error) } if nextToken.TType == efp.TokenTypeArgument || nextToken.TType == efp.TokenTypeFunction { // parse reference: reference or range at here - result, err := f.parseReference(sheet, token.TValue) + result, matrix, err := f.parseReference(sheet, token.TValue) if err != nil { return efp.Token{TValue: formulaErrorNAME}, err } - for _, val := range result { - argsList.PushBack(efp.Token{ - TType: efp.TokenTypeOperand, - TSubType: efp.TokenSubTypeNumber, - TValue: val, - }) + for idx, val := range result { + arg := formulaArg{Value: val} + if idx < len(matrix) { + arg.Matrix = matrix[idx] + } + argsList.PushBack(arg) } if len(result) == 0 { return efp.Token{}, errors.New(formulaErrorVALUE) @@ -190,7 +197,9 @@ func (f *File) evalInfixExp(sheet string, tokens []efp.Token) (efp.Token, error) opftStack.Pop() } if !opfdStack.Empty() { - argsList.PushBack(opfdStack.Pop()) + argsList.PushBack(formulaArg{ + Value: opfdStack.Pop().(efp.Token).TValue, + }) } continue } @@ -201,7 +210,9 @@ func (f *File) evalInfixExp(sheet string, tokens []efp.Token) (efp.Token, error) // current token is text if token.TType == efp.TokenTypeOperand && token.TSubType == efp.TokenSubTypeText { - argsList.PushBack(token) + argsList.PushBack(formulaArg{ + Value: token.TValue, + }) } // current token is function stop @@ -217,7 +228,9 @@ func (f *File) evalInfixExp(sheet string, tokens []efp.Token) (efp.Token, error) // push opfd to args if opfdStack.Len() > 0 { - argsList.PushBack(opfdStack.Pop()) + argsList.PushBack(formulaArg{ + Value: opfdStack.Pop().(efp.Token).TValue, + }) } // call formula function to evaluate result, err := callFuncByName(&formulaFuncs{}, strings.NewReplacer( @@ -324,7 +337,7 @@ func calculate(opdStack *Stack, opt efp.Token) error { func (f *File) parseToken(sheet string, token efp.Token, opdStack, optStack *Stack) error { // parse reference: must reference at here if token.TSubType == efp.TokenSubTypeRange { - result, err := f.parseReference(sheet, token.TValue) + result, _, err := f.parseReference(sheet, token.TValue) if err != nil { return errors.New(formulaErrorNAME) } @@ -383,7 +396,7 @@ func (f *File) parseToken(sheet string, token efp.Token, opdStack, optStack *Sta // parseReference parse reference and extract values by given reference // characters and default sheet name. -func (f *File) parseReference(sheet, reference string) (result []string, err error) { +func (f *File) parseReference(sheet, reference string) (result []string, matrix [][]string, err error) { reference = strings.Replace(reference, "$", "", -1) refs, cellRanges, cellRefs := list.New(), list.New(), list.New() for _, ref := range strings.Split(reference, ":") { @@ -423,7 +436,7 @@ func (f *File) parseReference(sheet, reference string) (result []string, err err refs.Remove(e) } - result, err = f.rangeResolver(cellRefs, cellRanges) + result, matrix, err = f.rangeResolver(cellRefs, cellRanges) return } @@ -431,7 +444,7 @@ func (f *File) parseReference(sheet, reference string) (result []string, err err // This function will not ignore the empty cell. Note that the result of 3D // range references may be different from Excel in some cases, for example, // A1:A2:A2:B3 in Excel will include B1, but we wont. -func (f *File) rangeResolver(cellRefs, cellRanges *list.List) (result []string, err error) { +func (f *File) rangeResolver(cellRefs, cellRanges *list.List) (result []string, matrix [][]string, err error) { filter := map[string]string{} // extract value from ranges for temp := cellRanges.Front(); temp != nil; temp = temp.Next() { @@ -441,16 +454,21 @@ func (f *File) rangeResolver(cellRefs, cellRanges *list.List) (result []string, } rng := []int{cr.From.Col, cr.From.Row, cr.To.Col, cr.To.Row} sortCoordinates(rng) - for col := rng[0]; col <= rng[2]; col++ { - for row := rng[1]; row <= rng[3]; row++ { - var cell string + matrix = [][]string{} + for row := rng[1]; row <= rng[3]; row++ { + var matrixRow = []string{} + for col := rng[0]; col <= rng[2]; col++ { + var cell, value string if cell, err = CoordinatesToCellName(col, row); err != nil { return } - if filter[cell], err = f.GetCellValue(cr.From.Sheet, cell); err != nil { + if value, err = f.GetCellValue(cr.From.Sheet, cell); err != nil { return } + filter[cell] = value + matrixRow = append(matrixRow, value) } + matrix = append(matrix, matrixRow) } } // extract value from references @@ -500,11 +518,11 @@ func callFuncByName(receiver interface{}, name string, params []reflect.Value) ( // func (fn *formulaFuncs) ABS(argsList *list.List) (result string, err error) { if argsList.Len() != 1 { - err = errors.New("ABS requires 1 numeric arguments") + err = errors.New("ABS requires 1 numeric argument") return } var val float64 - if val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { + if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { return } result = fmt.Sprintf("%g", math.Abs(val)) @@ -519,11 +537,11 @@ func (fn *formulaFuncs) ABS(argsList *list.List) (result string, err error) { // func (fn *formulaFuncs) ACOS(argsList *list.List) (result string, err error) { if argsList.Len() != 1 { - err = errors.New("ACOS requires 1 numeric arguments") + err = errors.New("ACOS requires 1 numeric argument") return } var val float64 - if val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { + if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { return } result = fmt.Sprintf("%g", math.Acos(val)) @@ -537,11 +555,11 @@ func (fn *formulaFuncs) ACOS(argsList *list.List) (result string, err error) { // func (fn *formulaFuncs) ACOSH(argsList *list.List) (result string, err error) { if argsList.Len() != 1 { - err = errors.New("ACOSH requires 1 numeric arguments") + err = errors.New("ACOSH requires 1 numeric argument") return } var val float64 - if val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { + if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { return } result = fmt.Sprintf("%g", math.Acosh(val)) @@ -556,11 +574,11 @@ func (fn *formulaFuncs) ACOSH(argsList *list.List) (result string, err error) { // func (fn *formulaFuncs) ACOT(argsList *list.List) (result string, err error) { if argsList.Len() != 1 { - err = errors.New("ACOT requires 1 numeric arguments") + err = errors.New("ACOT requires 1 numeric argument") return } var val float64 - if val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { + if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { return } result = fmt.Sprintf("%g", math.Pi/2-math.Atan(val)) @@ -574,11 +592,11 @@ func (fn *formulaFuncs) ACOT(argsList *list.List) (result string, err error) { // func (fn *formulaFuncs) ACOTH(argsList *list.List) (result string, err error) { if argsList.Len() != 1 { - err = errors.New("ACOTH requires 1 numeric arguments") + err = errors.New("ACOTH requires 1 numeric argument") return } var val float64 - if val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { + if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { return } result = fmt.Sprintf("%g", math.Atanh(1/val)) @@ -592,11 +610,11 @@ func (fn *formulaFuncs) ACOTH(argsList *list.List) (result string, err error) { // func (fn *formulaFuncs) ARABIC(argsList *list.List) (result string, err error) { if argsList.Len() != 1 { - err = errors.New("ARABIC requires 1 numeric arguments") + err = errors.New("ARABIC requires 1 numeric argument") return } val, last, prefix := 0.0, 0.0, 1.0 - for _, char := range argsList.Front().Value.(efp.Token).TValue { + for _, char := range argsList.Front().Value.(formulaArg).Value { digit := 0.0 switch char { case '-': @@ -643,11 +661,11 @@ func (fn *formulaFuncs) ARABIC(argsList *list.List) (result string, err error) { // func (fn *formulaFuncs) ASIN(argsList *list.List) (result string, err error) { if argsList.Len() != 1 { - err = errors.New("ASIN requires 1 numeric arguments") + err = errors.New("ASIN requires 1 numeric argument") return } var val float64 - if val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { + if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { return } result = fmt.Sprintf("%g", math.Asin(val)) @@ -661,11 +679,11 @@ func (fn *formulaFuncs) ASIN(argsList *list.List) (result string, err error) { // func (fn *formulaFuncs) ASINH(argsList *list.List) (result string, err error) { if argsList.Len() != 1 { - err = errors.New("ASINH requires 1 numeric arguments") + err = errors.New("ASINH requires 1 numeric argument") return } var val float64 - if val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { + if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { return } result = fmt.Sprintf("%g", math.Asinh(val)) @@ -680,11 +698,11 @@ func (fn *formulaFuncs) ASINH(argsList *list.List) (result string, err error) { // func (fn *formulaFuncs) ATAN(argsList *list.List) (result string, err error) { if argsList.Len() != 1 { - err = errors.New("ATAN requires 1 numeric arguments") + err = errors.New("ATAN requires 1 numeric argument") return } var val float64 - if val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { + if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { return } result = fmt.Sprintf("%g", math.Atan(val)) @@ -698,11 +716,11 @@ func (fn *formulaFuncs) ATAN(argsList *list.List) (result string, err error) { // func (fn *formulaFuncs) ATANH(argsList *list.List) (result string, err error) { if argsList.Len() != 1 { - err = errors.New("ATANH requires 1 numeric arguments") + err = errors.New("ATANH requires 1 numeric argument") return } var val float64 - if val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { + if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { return } result = fmt.Sprintf("%g", math.Atanh(val)) @@ -721,10 +739,10 @@ func (fn *formulaFuncs) ATAN2(argsList *list.List) (result string, err error) { return } var x, y float64 - if x, err = strconv.ParseFloat(argsList.Back().Value.(efp.Token).TValue, 64); err != nil { + if x, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { return } - if y, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { + if y, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { return } result = fmt.Sprintf("%g", math.Atan2(x, y)) @@ -766,10 +784,10 @@ func (fn *formulaFuncs) BASE(argsList *list.List) (result string, err error) { } var number float64 var radix, minLength int - if number, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { return } - if radix, err = strconv.Atoi(argsList.Front().Next().Value.(efp.Token).TValue); err != nil { + if radix, err = strconv.Atoi(argsList.Front().Next().Value.(formulaArg).Value); err != nil { return } if radix < 2 || radix > 36 { @@ -777,7 +795,7 @@ func (fn *formulaFuncs) BASE(argsList *list.List) (result string, err error) { return } if argsList.Len() > 2 { - if minLength, err = strconv.Atoi(argsList.Back().Value.(efp.Token).TValue); err != nil { + if minLength, err = strconv.Atoi(argsList.Back().Value.(formulaArg).Value); err != nil { return } } @@ -804,14 +822,14 @@ func (fn *formulaFuncs) CEILING(argsList *list.List) (result string, err error) return } var number, significance float64 = 0, 1 - if number, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { return } if number < 0 { significance = -1 } if argsList.Len() > 1 { - if significance, err = strconv.ParseFloat(argsList.Back().Value.(efp.Token).TValue, 64); err != nil { + if significance, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { return } } @@ -846,14 +864,14 @@ func (fn *formulaFuncs) CEILINGMATH(argsList *list.List) (result string, err err return } var number, significance, mode float64 = 0, 1, 1 - if number, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { return } if number < 0 { significance = -1 } if argsList.Len() > 1 { - if significance, err = strconv.ParseFloat(argsList.Front().Next().Value.(efp.Token).TValue, 64); err != nil { + if significance, err = strconv.ParseFloat(argsList.Front().Next().Value.(formulaArg).Value, 64); err != nil { return } } @@ -862,7 +880,7 @@ func (fn *formulaFuncs) CEILINGMATH(argsList *list.List) (result string, err err return } if argsList.Len() > 2 { - if mode, err = strconv.ParseFloat(argsList.Back().Value.(efp.Token).TValue, 64); err != nil { + if mode, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { return } } @@ -894,7 +912,7 @@ func (fn *formulaFuncs) CEILINGPRECISE(argsList *list.List) (result string, err return } var number, significance float64 = 0, 1 - if number, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { return } if number < 0 { @@ -905,7 +923,7 @@ func (fn *formulaFuncs) CEILINGPRECISE(argsList *list.List) (result string, err return } if argsList.Len() > 1 { - if significance, err = strconv.ParseFloat(argsList.Back().Value.(efp.Token).TValue, 64); err != nil { + if significance, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { return } significance = math.Abs(significance) @@ -935,10 +953,10 @@ func (fn *formulaFuncs) COMBIN(argsList *list.List) (result string, err error) { return } var number, chosen, val float64 = 0, 0, 1 - if number, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { return } - if chosen, err = strconv.ParseFloat(argsList.Back().Value.(efp.Token).TValue, 64); err != nil { + if chosen, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { return } number, chosen = math.Trunc(number), math.Trunc(chosen) @@ -968,10 +986,10 @@ func (fn *formulaFuncs) COMBINA(argsList *list.List) (result string, err error) return } var number, chosen float64 - if number, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { return } - if chosen, err = strconv.ParseFloat(argsList.Back().Value.(efp.Token).TValue, 64); err != nil { + if chosen, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { return } number, chosen = math.Trunc(number), math.Trunc(chosen) @@ -984,15 +1002,11 @@ func (fn *formulaFuncs) COMBINA(argsList *list.List) (result string, err error) return } args := list.New() - args.PushBack(efp.Token{ - TValue: fmt.Sprintf("%g", number+chosen-1), - TType: efp.TokenTypeOperand, - TSubType: efp.TokenSubTypeNumber, + args.PushBack(formulaArg{ + Value: fmt.Sprintf("%g", number+chosen-1), }) - args.PushBack(efp.Token{ - TValue: fmt.Sprintf("%g", number-1), - TType: efp.TokenTypeOperand, - TSubType: efp.TokenSubTypeNumber, + args.PushBack(formulaArg{ + Value: fmt.Sprintf("%g", number-1), }) return fn.COMBIN(args) } @@ -1004,11 +1018,11 @@ func (fn *formulaFuncs) COMBINA(argsList *list.List) (result string, err error) // func (fn *formulaFuncs) COS(argsList *list.List) (result string, err error) { if argsList.Len() != 1 { - err = errors.New("COS requires 1 numeric arguments") + err = errors.New("COS requires 1 numeric argument") return } var val float64 - if val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { + if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { return } result = fmt.Sprintf("%g", math.Cos(val)) @@ -1022,11 +1036,11 @@ func (fn *formulaFuncs) COS(argsList *list.List) (result string, err error) { // func (fn *formulaFuncs) COSH(argsList *list.List) (result string, err error) { if argsList.Len() != 1 { - err = errors.New("COSH requires 1 numeric arguments") + err = errors.New("COSH requires 1 numeric argument") return } var val float64 - if val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { + if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { return } result = fmt.Sprintf("%g", math.Cosh(val)) @@ -1040,11 +1054,11 @@ func (fn *formulaFuncs) COSH(argsList *list.List) (result string, err error) { // func (fn *formulaFuncs) COT(argsList *list.List) (result string, err error) { if argsList.Len() != 1 { - err = errors.New("COT requires 1 numeric arguments") + err = errors.New("COT requires 1 numeric argument") return } var val float64 - if val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { + if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { return } if val == 0 { @@ -1062,11 +1076,11 @@ func (fn *formulaFuncs) COT(argsList *list.List) (result string, err error) { // func (fn *formulaFuncs) COTH(argsList *list.List) (result string, err error) { if argsList.Len() != 1 { - err = errors.New("COTH requires 1 numeric arguments") + err = errors.New("COTH requires 1 numeric argument") return } var val float64 - if val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { + if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { return } if val == 0 { @@ -1084,11 +1098,11 @@ func (fn *formulaFuncs) COTH(argsList *list.List) (result string, err error) { // func (fn *formulaFuncs) CSC(argsList *list.List) (result string, err error) { if argsList.Len() != 1 { - err = errors.New("CSC requires 1 numeric arguments") + err = errors.New("CSC requires 1 numeric argument") return } var val float64 - if val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { + if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { return } if val == 0 { @@ -1099,6 +1113,297 @@ func (fn *formulaFuncs) CSC(argsList *list.List) (result string, err error) { return } +// CSCH function calculates the hyperbolic cosecant (csch) of a supplied +// angle. The syntax of the function is: +// +// CSCH(number) +// +func (fn *formulaFuncs) CSCH(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("CSCH requires 1 numeric argument") + return + } + var val float64 + if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + return + } + if val == 0 { + err = errors.New(formulaErrorNAME) + return + } + result = fmt.Sprintf("%g", 1/math.Sinh(val)) + return +} + +// DECIMAL function converts a text representation of a number in a specified +// base, into a decimal value. The syntax of the function is: +// +// DECIMAL(text,radix) +// +func (fn *formulaFuncs) DECIMAL(argsList *list.List) (result string, err error) { + if argsList.Len() != 2 { + err = errors.New("DECIMAL requires 2 numeric arguments") + return + } + var text = argsList.Front().Value.(formulaArg).Value + var radix int + if radix, err = strconv.Atoi(argsList.Back().Value.(formulaArg).Value); err != nil { + return + } + if len(text) > 2 && (strings.HasPrefix(text, "0x") || strings.HasPrefix(text, "0X")) { + text = text[2:] + } + val, err := strconv.ParseInt(text, radix, 64) + if err != nil { + err = errors.New(formulaErrorNUM) + return + } + result = fmt.Sprintf("%g", float64(val)) + return +} + +// DEGREES function converts radians into degrees. The syntax of the function +// is: +// +// DEGREES(angle) +// +func (fn *formulaFuncs) DEGREES(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("DEGREES requires 1 numeric argument") + return + } + var val float64 + if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + return + } + if val == 0 { + err = errors.New(formulaErrorNAME) + return + } + result = fmt.Sprintf("%g", 180.0/math.Pi*val) + return +} + +// EVEN function rounds a supplied number away from zero (i.e. rounds a +// positive number up and a negative number down), to the next even number. +// The syntax of the function is: +// +// EVEN(number) +// +func (fn *formulaFuncs) EVEN(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("EVEN requires 1 numeric argument") + return + } + var number float64 + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + return + } + sign := math.Signbit(number) + m, frac := math.Modf(number / 2) + val := m * 2 + if frac != 0 { + if !sign { + val += 2 + } else { + val -= 2 + } + } + result = fmt.Sprintf("%g", val) + return +} + +// EXP function calculates the value of the mathematical constant e, raised to +// the power of a given number. The syntax of the function is: +// +// EXP(number) +// +func (fn *formulaFuncs) EXP(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("EXP requires 1 numeric argument") + return + } + var number float64 + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + return + } + result = strings.ToUpper(fmt.Sprintf("%g", math.Exp(number))) + return +} + +// fact returns the factorial of a supplied number. +func fact(number float64) float64 { + val := float64(1) + for i := float64(2); i <= number; i++ { + val *= i + } + return val +} + +// FACT function returns the factorial of a supplied number. The syntax of the +// function is: +// +// FACT(number) +// +func (fn *formulaFuncs) FACT(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("FACT requires 1 numeric argument") + return + } + var number float64 + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + return + } + if number < 0 { + err = errors.New(formulaErrorNUM) + } + result = strings.ToUpper(fmt.Sprintf("%g", fact(number))) + return +} + +// FACTDOUBLE function returns the double factorial of a supplied number. The +// syntax of the function is: +// +// FACTDOUBLE(number) +// +func (fn *formulaFuncs) FACTDOUBLE(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("FACTDOUBLE requires 1 numeric argument") + return + } + var number, val float64 = 0, 1 + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + return + } + if number < 0 { + err = errors.New(formulaErrorNUM) + } + for i := math.Trunc(number); i > 1; i -= 2 { + val *= i + } + result = strings.ToUpper(fmt.Sprintf("%g", val)) + return +} + +// FLOOR function rounds a supplied number towards zero to the nearest +// multiple of a specified significance. The syntax of the function is: +// +// FLOOR(number,significance) +// +func (fn *formulaFuncs) FLOOR(argsList *list.List) (result string, err error) { + if argsList.Len() != 2 { + err = errors.New("FLOOR requires 2 numeric arguments") + return + } + var number, significance float64 = 0, 1 + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + return + } + if significance, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + return + } + if significance < 0 && number >= 0 { + err = errors.New(formulaErrorNUM) + } + val := number + val, res := math.Modf(val / significance) + if res != 0 { + if number < 0 && res < 0 { + val-- + } + } + result = strings.ToUpper(fmt.Sprintf("%g", val*significance)) + return +} + +// FLOORMATH function rounds a supplied number down to a supplied multiple of +// significance. The syntax of the function is: +// +// FLOOR.MATH(number,[significance],[mode]) +// +func (fn *formulaFuncs) FLOORMATH(argsList *list.List) (result string, err error) { + if argsList.Len() == 0 { + err = errors.New("FLOOR.MATH requires at least 1 argument") + return + } + if argsList.Len() > 3 { + err = errors.New("FLOOR.MATH allows at most 3 arguments") + return + } + var number, significance, mode float64 = 0, 1, 1 + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + return + } + if number < 0 { + significance = -1 + } + if argsList.Len() > 1 { + if significance, err = strconv.ParseFloat(argsList.Front().Next().Value.(formulaArg).Value, 64); err != nil { + return + } + } + if argsList.Len() == 1 { + result = fmt.Sprintf("%g", math.Floor(number)) + return + } + if argsList.Len() > 2 { + if mode, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + return + } + } + val, res := math.Modf(number / significance) + if res != 0 && number < 0 && mode > 0 { + val-- + } + result = fmt.Sprintf("%g", val*significance) + return +} + +// FLOORPRECISE function rounds a supplied number down to a supplied multiple +// of significance. The syntax of the function is: +// +// FLOOR.PRECISE(number,[significance]) +// +func (fn *formulaFuncs) FLOORPRECISE(argsList *list.List) (result string, err error) { + if argsList.Len() == 0 { + err = errors.New("FLOOR.PRECISE requires at least 1 argument") + return + } + if argsList.Len() > 2 { + err = errors.New("FLOOR.PRECISE allows at most 2 arguments") + return + } + var number, significance float64 = 0, 1 + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + return + } + if number < 0 { + significance = -1 + } + if argsList.Len() == 1 { + result = fmt.Sprintf("%g", math.Floor(number)) + return + } + if argsList.Len() > 1 { + if significance, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + return + } + significance = math.Abs(significance) + if significance == 0 { + result = "0" + return + } + } + val, res := math.Modf(number / significance) + if res != 0 { + if number < 0 { + val-- + } + } + result = fmt.Sprintf("%g", val*significance) + return +} + // GCD function returns the greatest common divisor of two or more supplied // integers. The syntax of the function is: // @@ -1114,11 +1419,11 @@ func (fn *formulaFuncs) GCD(argsList *list.List) (result string, err error) { nums = []float64{} ) for arg := argsList.Front(); arg != nil; arg = arg.Next() { - token := arg.Value.(efp.Token) - if token.TValue == "" { + token := arg.Value.(formulaArg).Value + if token == "" { continue } - if val, err = strconv.ParseFloat(token.TValue, 64); err != nil { + if val, err = strconv.ParseFloat(token, 64); err != nil { return } nums = append(nums, val) @@ -1143,6 +1448,74 @@ func (fn *formulaFuncs) GCD(argsList *list.List) (result string, err error) { return } +// INT function truncates a supplied number down to the closest integer. The +// syntax of the function is: +// +// INT(number) +// +func (fn *formulaFuncs) INT(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("INT requires 1 numeric argument") + return + } + var number float64 + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + return + } + val, frac := math.Modf(number) + if frac < 0 { + val-- + } + result = fmt.Sprintf("%g", val) + return +} + +// ISOCEILING function rounds a supplied number up (regardless of the number's +// sign), to the nearest multiple of a supplied significance. The syntax of +// the function is: +// +// ISO.CEILING(number,[significance]) +// +func (fn *formulaFuncs) ISOCEILING(argsList *list.List) (result string, err error) { + if argsList.Len() == 0 { + err = errors.New("ISO.CEILING requires at least 1 argument") + return + } + if argsList.Len() > 2 { + err = errors.New("ISO.CEILING allows at most 2 arguments") + return + } + var number, significance float64 = 0, 1 + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + return + } + if number < 0 { + significance = -1 + } + if argsList.Len() == 1 { + result = fmt.Sprintf("%g", math.Ceil(number)) + return + } + if argsList.Len() > 1 { + if significance, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + return + } + significance = math.Abs(significance) + if significance == 0 { + result = "0" + return + } + } + val, res := math.Modf(number / significance) + if res != 0 { + if number > 0 { + val++ + } + } + result = fmt.Sprintf("%g", val*significance) + return +} + // lcm returns the least common multiple of two supplied integers. func lcm(a, b float64) float64 { a = math.Trunc(a) @@ -1168,11 +1541,11 @@ func (fn *formulaFuncs) LCM(argsList *list.List) (result string, err error) { nums = []float64{} ) for arg := argsList.Front(); arg != nil; arg = arg.Next() { - token := arg.Value.(efp.Token) - if token.TValue == "" { + token := arg.Value.(formulaArg).Value + if token == "" { continue } - if val, err = strconv.ParseFloat(token.TValue, 64); err != nil { + if val, err = strconv.ParseFloat(token, 64); err != nil { return } nums = append(nums, val) @@ -1197,6 +1570,151 @@ func (fn *formulaFuncs) LCM(argsList *list.List) (result string, err error) { return } +// LN function calculates the natural logarithm of a given number. The syntax +// of the function is: +// +// LN(number) +// +func (fn *formulaFuncs) LN(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("LN requires 1 numeric argument") + return + } + var number float64 + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + return + } + result = fmt.Sprintf("%g", math.Log(number)) + return +} + +// LOG function calculates the logarithm of a given number, to a supplied +// base. The syntax of the function is: +// +// LOG(number,[base]) +// +func (fn *formulaFuncs) LOG(argsList *list.List) (result string, err error) { + if argsList.Len() == 0 { + err = errors.New("LOG requires at least 1 argument") + return + } + if argsList.Len() > 2 { + err = errors.New("LOG allows at most 2 arguments") + return + } + var number, base float64 = 0, 10 + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + return + } + if argsList.Len() > 1 { + if base, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + return + } + } + if number == 0 { + err = errors.New(formulaErrorNUM) + return + } + if base == 0 { + err = errors.New(formulaErrorNUM) + return + } + if base == 1 { + err = errors.New(formulaErrorDIV) + return + } + result = fmt.Sprintf("%g", math.Log(number)/math.Log(base)) + return +} + +// LOG10 function calculates the base 10 logarithm of a given number. The +// syntax of the function is: +// +// LOG10(number) +// +func (fn *formulaFuncs) LOG10(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("LOG10 requires 1 numeric argument") + return + } + var number float64 + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + return + } + result = fmt.Sprintf("%g", math.Log10(number)) + return +} + +func minor(sqMtx [][]float64, idx int) [][]float64 { + ret := [][]float64{} + for i := range sqMtx { + if i == 0 { + continue + } + row := []float64{} + for j := range sqMtx { + if j == idx { + continue + } + row = append(row, sqMtx[i][j]) + } + ret = append(ret, row) + } + return ret +} + +// det determinant of the 2x2 matrix. +func det(sqMtx [][]float64) float64 { + if len(sqMtx) == 2 { + m00 := sqMtx[0][0] + m01 := sqMtx[0][1] + m10 := sqMtx[1][0] + m11 := sqMtx[1][1] + return m00*m11 - m10*m01 + } + var res, sgn float64 = 0, 1 + for j := range sqMtx { + res += sgn * sqMtx[0][j] * det(minor(sqMtx, j)) + sgn *= -1 + } + return res +} + +// MDETERM calculates the determinant of a square matrix. The +// syntax of the function is: +// +// MDETERM(array) +// +func (fn *formulaFuncs) MDETERM(argsList *list.List) (result string, err error) { + var num float64 + var rows int + var numMtx = [][]float64{} + var strMtx = [][]string{} + for arg := argsList.Front(); arg != nil; arg = arg.Next() { + if len(arg.Value.(formulaArg).Matrix) == 0 { + break + } + strMtx = append(strMtx, arg.Value.(formulaArg).Matrix) + rows++ + } + for _, row := range strMtx { + if len(row) != rows { + err = errors.New(formulaErrorVALUE) + return + } + numRow := []float64{} + for _, ele := range row { + if num, err = strconv.ParseFloat(ele, 64); err != nil { + return + } + numRow = append(numRow, num) + } + numMtx = append(numMtx, numRow) + } + result = fmt.Sprintf("%g", det(numMtx)) + return +} + // POWER function calculates a given number, raised to a supplied power. // The syntax of the function is: // @@ -1208,10 +1726,10 @@ func (fn *formulaFuncs) POWER(argsList *list.List) (result string, err error) { return } var x, y float64 - if x, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { + if x, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { return } - if y, err = strconv.ParseFloat(argsList.Back().Value.(efp.Token).TValue, 64); err != nil { + if y, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { return } if x == 0 && y == 0 { @@ -1234,11 +1752,11 @@ func (fn *formulaFuncs) POWER(argsList *list.List) (result string, err error) { func (fn *formulaFuncs) PRODUCT(argsList *list.List) (result string, err error) { var val, product float64 = 0, 1 for arg := argsList.Front(); arg != nil; arg = arg.Next() { - token := arg.Value.(efp.Token) - if token.TValue == "" { + token := arg.Value.(formulaArg) + if token.Value == "" { continue } - if val, err = strconv.ParseFloat(token.TValue, 64); err != nil { + if val, err = strconv.ParseFloat(token.Value, 64); err != nil { return } product = product * val @@ -1256,11 +1774,11 @@ func (fn *formulaFuncs) PRODUCT(argsList *list.List) (result string, err error) // func (fn *formulaFuncs) SIGN(argsList *list.List) (result string, err error) { if argsList.Len() != 1 { - err = errors.New("SIGN requires 1 numeric arguments") + err = errors.New("SIGN requires 1 numeric argument") return } var val float64 - if val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { + if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { return } if val < 0 { @@ -1282,18 +1800,23 @@ func (fn *formulaFuncs) SIGN(argsList *list.List) (result string, err error) { // func (fn *formulaFuncs) SQRT(argsList *list.List) (result string, err error) { if argsList.Len() != 1 { - err = errors.New("SQRT requires 1 numeric arguments") + err = errors.New("SQRT requires 1 numeric argument") return } - var val float64 - if val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { + var res float64 + var value = argsList.Front().Value.(formulaArg).Value + if value == "" { + result = "0" return } - if val < 0 { + if res, err = strconv.ParseFloat(value, 64); err != nil { + return + } + if res < 0 { err = errors.New(formulaErrorNUM) return } - result = fmt.Sprintf("%g", math.Sqrt(val)) + result = fmt.Sprintf("%g", math.Sqrt(res)) return } @@ -1305,11 +1828,11 @@ func (fn *formulaFuncs) SQRT(argsList *list.List) (result string, err error) { func (fn *formulaFuncs) SUM(argsList *list.List) (result string, err error) { var val, sum float64 for arg := argsList.Front(); arg != nil; arg = arg.Next() { - token := arg.Value.(efp.Token) - if token.TValue == "" { + token := arg.Value.(formulaArg) + if token.Value == "" { continue } - if val, err = strconv.ParseFloat(token.TValue, 64); err != nil { + if val, err = strconv.ParseFloat(token.Value, 64); err != nil { return } sum += val @@ -1329,10 +1852,10 @@ func (fn *formulaFuncs) QUOTIENT(argsList *list.List) (result string, err error) return } var x, y float64 - if x, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { + if x, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { return } - if y, err = strconv.ParseFloat(argsList.Back().Value.(efp.Token).TValue, 64); err != nil { + if y, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { return } if y == 0 { diff --git a/calc_test.go b/calc_test.go index 5d441ff..c66de8c 100644 --- a/calc_test.go +++ b/calc_test.go @@ -14,6 +14,7 @@ func TestCalcCellValue(t *testing.T) { f.SetCellValue("Sheet1", "A3", 3) f.SetCellValue("Sheet1", "A4", 0) f.SetCellValue("Sheet1", "B1", 4) + f.SetCellValue("Sheet1", "B2", 5) return f } @@ -115,22 +116,108 @@ func TestCalcCellValue(t *testing.T) { "=COSH(0.5)": "1.1276259652063807", "=COSH(-2)": "3.7621956910836314", // _xlfn.COT - "_xlfn.COT(0.785398163397448)": "0.9999999999999992", + "=_xlfn.COT(0.785398163397448)": "0.9999999999999992", // _xlfn.COTH - "_xlfn.COTH(-3.14159265358979)": "-0.9962720762207499", + "=_xlfn.COTH(-3.14159265358979)": "-0.9962720762207499", // _xlfn.CSC - "_xlfn.CSC(-6)": "3.5788995472544056", - "_xlfn.CSC(1.5707963267949)": "1", + "=_xlfn.CSC(-6)": "3.5788995472544056", + "=_xlfn.CSC(1.5707963267949)": "1", + // _xlfn.CSCH + "=_xlfn.CSCH(-3.14159265358979)": "-0.08658953753004724", + // _xlfn.DECIMAL + `=_xlfn.DECIMAL("1100",2)`: "12", + `=_xlfn.DECIMAL("186A0",16)`: "100000", + `=_xlfn.DECIMAL("31L0",32)`: "100000", + `=_xlfn.DECIMAL("70122",8)`: "28754", + // DEGREES + "=DEGREES(1)": "57.29577951308232", + "=DEGREES(2.5)": "143.2394487827058", + // EVEN + "=EVEN(23)": "24", + "=EVEN(2.22)": "4", + "=EVEN(0)": "0", + "=EVEN(-0.3)": "-2", + "=EVEN(-11)": "-12", + "=EVEN(-4)": "-4", + // EXP + "=EXP(100)": "2.6881171418161356E+43", + "=EXP(0.1)": "1.1051709180756477", + "=EXP(0)": "1", + "=EXP(-5)": "0.006737946999085467", + // FACT + "=FACT(3)": "6", + "=FACT(6)": "720", + "=FACT(10)": "3.6288E+06", + // FACTDOUBLE + "=FACTDOUBLE(5)": "15", + "=FACTDOUBLE(8)": "384", + "=FACTDOUBLE(13)": "135135", + // FLOOR + "=FLOOR(26.75,0.1)": "26.700000000000003", + "=FLOOR(26.75,0.5)": "26.5", + "=FLOOR(26.75,1)": "26", + "=FLOOR(26.75,10)": "20", + "=FLOOR(26.75,20)": "20", + "=FLOOR(-26.75,-0.1)": "-26.700000000000003", + "=FLOOR(-26.75,-1)": "-26", + "=FLOOR(-26.75,-5)": "-25", + // _xlfn.FLOOR.MATH + "=_xlfn.FLOOR.MATH(58.55)": "58", + "=_xlfn.FLOOR.MATH(58.55,0.1)": "58.5", + "=_xlfn.FLOOR.MATH(58.55,5)": "55", + "=_xlfn.FLOOR.MATH(58.55,1,1)": "58", + "=_xlfn.FLOOR.MATH(-58.55,1)": "-59", + "=_xlfn.FLOOR.MATH(-58.55,1,-1)": "-58", + "=_xlfn.FLOOR.MATH(-58.55,1,1)": "-59", // should be -58 + "=_xlfn.FLOOR.MATH(-58.55,10)": "-60", + // _xlfn.FLOOR.PRECISE + "=_xlfn.FLOOR.PRECISE(26.75,0.1)": "26.700000000000003", + "=_xlfn.FLOOR.PRECISE(26.75,0.5)": "26.5", + "=_xlfn.FLOOR.PRECISE(26.75,1)": "26", + "=_xlfn.FLOOR.PRECISE(26.75)": "26", + "=_xlfn.FLOOR.PRECISE(26.75,10)": "20", + "=_xlfn.FLOOR.PRECISE(26.75,0)": "0", + "=_xlfn.FLOOR.PRECISE(-26.75,1)": "-27", + "=_xlfn.FLOOR.PRECISE(-26.75,-1)": "-27", + "=_xlfn.FLOOR.PRECISE(-26.75,-5)": "-30", // GCD "=GCD(1,5)": "1", "=GCD(15,10,25)": "5", "=GCD(0,8,12)": "4", "=GCD(7,2)": "1", + // INT + "=INT(100.9)": "100", + "=INT(5.22)": "5", + "=INT(5.99)": "5", + "=INT(-6.1)": "-7", + "=INT(-100.9)": "-101", + // ISO.CEILING + "=ISO.CEILING(22.25)": "23", + "=ISO.CEILING(22.25,1)": "23", + "=ISO.CEILING(22.25,0.1)": "22.3", + "=ISO.CEILING(22.25,10)": "30", + "=ISO.CEILING(-22.25,1)": "-22", + "=ISO.CEILING(-22.25,0.1)": "-22.200000000000003", + "=ISO.CEILING(-22.25,5)": "-20", // LCM "=LCM(1,5)": "5", "=LCM(15,10,25)": "150", "=LCM(1,8,12)": "24", "=LCM(7,2)": "14", + // LN + "=LN(1)": "0", + "=LN(100)": "4.605170185988092", + "=LN(0.5)": "-0.6931471805599453", + // LOG + "=LOG(64,2)": "6", + "=LOG(100)": "2", + "=LOG(4,0.5)": "-2", + "=LOG(500)": "2.6989700043360183", + // LOG10 + "=LOG10(100)": "2", + "=LOG10(1000)": "3", + "=LOG10(0.001)": "-3", + "=LOG10(25)": "1.3979400086720375", // POWER "=POWER(4,2)": "16", // PRODUCT @@ -171,26 +258,26 @@ func TestCalcCellValue(t *testing.T) { } mathCalcError := map[string]string{ // ABS - "=ABS()": "ABS requires 1 numeric arguments", + "=ABS()": "ABS requires 1 numeric argument", "=ABS(~)": `cannot convert cell "~" to coordinates: invalid cell name "~"`, // ACOS - "=ACOS()": "ACOS requires 1 numeric arguments", + "=ACOS()": "ACOS requires 1 numeric argument", // ACOSH - "=ACOSH()": "ACOSH requires 1 numeric arguments", + "=ACOSH()": "ACOSH requires 1 numeric argument", // _xlfn.ACOT - "=_xlfn.ACOT()": "ACOT requires 1 numeric arguments", + "=_xlfn.ACOT()": "ACOT requires 1 numeric argument", // _xlfn.ACOTH - "=_xlfn.ACOTH()": "ACOTH requires 1 numeric arguments", + "=_xlfn.ACOTH()": "ACOTH requires 1 numeric argument", // _xlfn.ARABIC - "=_xlfn.ARABIC()": "ARABIC requires 1 numeric arguments", + "=_xlfn.ARABIC()": "ARABIC requires 1 numeric argument", // ASIN - "=ASIN()": "ASIN requires 1 numeric arguments", + "=ASIN()": "ASIN requires 1 numeric argument", // ASINH - "=ASINH()": "ASINH requires 1 numeric arguments", + "=ASINH()": "ASINH requires 1 numeric argument", // ATAN - "=ATAN()": "ATAN requires 1 numeric arguments", + "=ATAN()": "ATAN requires 1 numeric argument", // ATANH - "=ATANH()": "ATANH requires 1 numeric arguments", + "=ATANH()": "ATANH requires 1 numeric argument", // ATAN2 "=ATAN2()": "ATAN2 requires 2 numeric arguments", // BASE @@ -215,33 +302,75 @@ func TestCalcCellValue(t *testing.T) { "=_xlfn.COMBINA(-1,1)": "COMBINA requires number > number_chosen", "=_xlfn.COMBINA(-1,-1)": "COMBIN requires number >= number_chosen", // COS - "=COS()": "COS requires 1 numeric arguments", + "=COS()": "COS requires 1 numeric argument", // COSH - "=COSH()": "COSH requires 1 numeric arguments", + "=COSH()": "COSH requires 1 numeric argument", // _xlfn.COT - "=COT()": "COT requires 1 numeric arguments", + "=COT()": "COT requires 1 numeric argument", // _xlfn.COTH - "=COTH()": "COTH requires 1 numeric arguments", + "=COTH()": "COTH requires 1 numeric argument", // _xlfn.CSC - "_xlfn.CSC()": "CSC requires 1 numeric arguments", - "_xlfn.CSC(0)": "#NAME?", + "=_xlfn.CSC()": "CSC requires 1 numeric argument", + "=_xlfn.CSC(0)": "#NAME?", + // _xlfn.CSCH + "=_xlfn.CSCH()": "CSCH requires 1 numeric argument", + "=_xlfn.CSCH(0)": "#NAME?", + // _xlfn.DECIMAL + "=_xlfn.DECIMAL()": "DECIMAL requires 2 numeric arguments", + `=_xlfn.DECIMAL("2000", 2)`: "#NUM!", + // DEGREES + "=DEGREES()": "DEGREES requires 1 numeric argument", + // EVEN + "=EVEN()": "EVEN requires 1 numeric argument", + // EXP + "=EXP()": "EXP requires 1 numeric argument", + // FACT + "=FACT()": "FACT requires 1 numeric argument", + "=FACT(-1)": "#NUM!", + // FACTDOUBLE + "=FACTDOUBLE()": "FACTDOUBLE requires 1 numeric argument", + "=FACTDOUBLE(-1)": "#NUM!", + // FLOOR + "=FLOOR()": "FLOOR requires 2 numeric arguments", + "=FLOOR(1,-1)": "#NUM!", + // _xlfn.FLOOR.MATH + "=_xlfn.FLOOR.MATH()": "FLOOR.MATH requires at least 1 argument", + "=_xlfn.FLOOR.MATH(1,2,3,4)": "FLOOR.MATH allows at most 3 arguments", + // _xlfn.FLOOR.PRECISE + "=_xlfn.FLOOR.PRECISE()": "FLOOR.PRECISE requires at least 1 argument", + "=_xlfn.FLOOR.PRECISE(1,2,3)": "FLOOR.PRECISE allows at most 2 arguments", // GCD "=GCD()": "GCD requires at least 1 argument", "=GCD(-1)": "GCD only accepts positive arguments", "=GCD(1,-1)": "GCD only accepts positive arguments", + // INT + "=INT()": "INT requires 1 numeric argument", + // ISO.CEILING + "=ISO.CEILING()": "ISO.CEILING requires at least 1 argument", + "=ISO.CEILING(1,2,3)": "ISO.CEILING allows at most 2 arguments", // LCM "=LCM()": "LCM requires at least 1 argument", "=LCM(-1)": "LCM only accepts positive arguments", "=LCM(1,-1)": "LCM only accepts positive arguments", + // LN + "=LN()": "LN requires 1 numeric argument", + // LOG + "=LOG()": "LOG requires at least 1 argument", + "=LOG(1,2,3)": "LOG allows at most 2 arguments", + "=LOG(0,0)": "#NUM!", + "=LOG(1,0)": "#NUM!", + "=LOG(1,1)": "#DIV/0!", + // LOG10 + "=LOG10()": "LOG10 requires 1 numeric argument", // POWER "=POWER(0,0)": "#NUM!", "=POWER(0,-1)": "#DIV/0!", "=POWER(1)": "POWER requires 2 numeric arguments", // SIGN - "=SIGN()": "SIGN requires 1 numeric arguments", + "=SIGN()": "SIGN requires 1 numeric argument", // SQRT "=SQRT(-1)": "#NUM!", - "=SQRT(1,2)": "SQRT requires 1 numeric arguments", + "=SQRT(1,2)": "SQRT requires 1 numeric argument", // QUOTIENT "=QUOTIENT(1,0)": "#DIV/0!", "=QUOTIENT(1)": "QUOTIENT requires 2 numeric arguments", @@ -255,6 +384,8 @@ func TestCalcCellValue(t *testing.T) { } referenceCalc := map[string]string{ + // MDETERM + "=MDETERM(A1:B2)": "-3", // PRODUCT "=PRODUCT(Sheet1!A1:Sheet1!A1:A2,A2)": "4", // SUM @@ -277,6 +408,9 @@ func TestCalcCellValue(t *testing.T) { } referenceCalcError := map[string]string{ + // MDETERM + "=MDETERM(A1:B3)": "#VALUE!", + // SUM "=1+SUM(SUM(A1+A2/A4)*(2-3),2)": "#DIV/0!", } for formula, expected := range referenceCalcError { -- cgit v1.2.1 From 1f73a19e0f3bff9869e333675957dd5c027d0ab9 Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 6 May 2020 00:32:53 +0800 Subject: Resolve #628, omit number format empty --- xmlStyles.go | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/xmlStyles.go b/xmlStyles.go index d6aa4f9..b5ec41d 100644 --- a/xmlStyles.go +++ b/xmlStyles.go @@ -186,12 +186,12 @@ type xlsxCellStyles struct { // workbook. type xlsxCellStyle struct { XMLName xml.Name `xml:"cellStyle"` - BuiltInID *int `xml:"builtinId,attr,omitempty"` - CustomBuiltIn *bool `xml:"customBuiltin,attr,omitempty"` - Hidden *bool `xml:"hidden,attr,omitempty"` - ILevel *bool `xml:"iLevel,attr,omitempty"` Name string `xml:"name,attr"` XfID int `xml:"xfId,attr"` + BuiltInID *int `xml:"builtinId,attr,omitempty"` + ILevel *int `xml:"iLevel,attr,omitempty"` + Hidden *bool `xml:"hidden,attr,omitempty"` + CustomBuiltIn *bool `xml:"customBuiltin,attr,omitempty"` } // xlsxCellStyleXfs directly maps the cellStyleXfs element. This element @@ -209,19 +209,19 @@ type xlsxCellStyleXfs struct { // xlsxXf directly maps the xf element. A single xf element describes all of the // formatting for a cell. type xlsxXf struct { - ApplyAlignment bool `xml:"applyAlignment,attr"` - ApplyBorder bool `xml:"applyBorder,attr"` - ApplyFill bool `xml:"applyFill,attr"` - ApplyFont bool `xml:"applyFont,attr"` - ApplyNumberFormat bool `xml:"applyNumberFormat,attr"` - ApplyProtection bool `xml:"applyProtection,attr"` - BorderID int `xml:"borderId,attr"` - FillID int `xml:"fillId,attr"` - FontID int `xml:"fontId,attr"` - NumFmtID int `xml:"numFmtId,attr"` - PivotButton bool `xml:"pivotButton,attr,omitempty"` + NumFmtID int `xml:"numFmtId,attr,omitempty"` + FontID int `xml:"fontId,attr,omitempty"` + FillID int `xml:"fillId,attr,omitempty"` + BorderID int `xml:"borderId,attr,omitempty"` + XfID *int `xml:"xfId,attr,omitempty"` QuotePrefix bool `xml:"quotePrefix,attr,omitempty"` - XfID *int `xml:"xfId,attr"` + PivotButton bool `xml:"pivotButton,attr,omitempty"` + ApplyNumberFormat bool `xml:"applyNumberFormat,attr,omitempty"` + ApplyFont bool `xml:"applyFont,attr,omitempty"` + ApplyFill bool `xml:"applyFill,attr,omitempty"` + ApplyBorder bool `xml:"applyBorder,attr,omitempty"` + ApplyAlignment bool `xml:"applyAlignment,attr,omitempty"` + ApplyProtection bool `xml:"applyProtection,attr,omitempty"` Alignment *xlsxAlignment `xml:"alignment"` Protection *xlsxProtection `xml:"protection"` } -- cgit v1.2.1 From de34ecaacee83633977298d0424e8eb5eaacb876 Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 7 May 2020 00:15:54 +0800 Subject: #65 fn: MOD, MROUND, MULTINOMIAL, MUNIT, ODD, PI, RADIANS, RAND, RANDBETWEEN, ROMAN --- calc.go | 338 ++++++++++++++++++++++++++++++++++++++++++++++++++++++----- calc_test.go | 76 ++++++++++++-- 2 files changed, 382 insertions(+), 32 deletions(-) diff --git a/calc.go b/calc.go index 2ab3d61..fb94e27 100644 --- a/calc.go +++ b/calc.go @@ -12,13 +12,16 @@ package excelize import ( + "bytes" "container/list" "errors" "fmt" "math" + "math/rand" "reflect" "strconv" "strings" + "time" "github.com/xuri/efp" ) @@ -1715,6 +1718,168 @@ func (fn *formulaFuncs) MDETERM(argsList *list.List) (result string, err error) return } +// MOD function returns the remainder of a division between two supplied +// numbers. The syntax of the function is: +// +// MOD(number,divisor) +// +func (fn *formulaFuncs) MOD(argsList *list.List) (result string, err error) { + if argsList.Len() != 2 { + err = errors.New("MOD requires 2 numeric arguments") + return + } + var number, divisor float64 + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + return + } + if divisor, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + return + } + if divisor == 0 { + err = errors.New(formulaErrorDIV) + return + } + trunc, rem := math.Modf(number / divisor) + if rem < 0 { + trunc-- + } + result = fmt.Sprintf("%g", number-divisor*trunc) + return +} + +// MROUND function rounds a supplied number up or down to the nearest multiple +// of a given number. The syntax of the function is: +// +// MOD(number,multiple) +// +func (fn *formulaFuncs) MROUND(argsList *list.List) (result string, err error) { + if argsList.Len() != 2 { + err = errors.New("MROUND requires 2 numeric arguments") + return + } + var number, multiple float64 = 0, 1 + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + return + } + if multiple, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + return + } + if multiple == 0 { + err = errors.New(formulaErrorNUM) + return + } + if multiple < 0 && number > 0 || + multiple > 0 && number < 0 { + err = errors.New(formulaErrorNUM) + return + } + number, res := math.Modf(number / multiple) + if math.Trunc(res+0.5) > 0 { + number++ + } + result = fmt.Sprintf("%g", number*multiple) + return +} + +// MULTINOMIAL function calculates the ratio of the factorial of a sum of +// supplied values to the product of factorials of those values. The syntax of +// the function is: +// +// MULTINOMIAL(number1,[number2],...) +// +func (fn *formulaFuncs) MULTINOMIAL(argsList *list.List) (result string, err error) { + var val, num, denom float64 = 0, 0, 1 + for arg := argsList.Front(); arg != nil; arg = arg.Next() { + token := arg.Value.(formulaArg) + if token.Value == "" { + continue + } + if val, err = strconv.ParseFloat(token.Value, 64); err != nil { + return + } + num += val + denom *= fact(val) + } + result = fmt.Sprintf("%g", fact(num)/denom) + return +} + +// MUNIT function returns the unit matrix for a specified dimension. The +// syntax of the function is: +// +// MUNIT(dimension) +// +func (fn *formulaFuncs) MUNIT(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("MUNIT requires 1 numeric argument") + return + } + var dimension int + if dimension, err = strconv.Atoi(argsList.Front().Value.(formulaArg).Value); err != nil { + return + } + matrix := make([][]float64, 0, dimension) + for i := 0; i < dimension; i++ { + row := make([]float64, dimension) + for j := 0; j < dimension; j++ { + if i == j { + row[j] = float64(1.0) + } else { + row[j] = float64(0.0) + } + } + matrix = append(matrix, row) + } + return +} + +// ODD function ounds a supplied number away from zero (i.e. rounds a positive +// number up and a negative number down), to the next odd number. The syntax +// of the function is: +// +// ODD(number) +// +func (fn *formulaFuncs) ODD(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("ODD requires 1 numeric argument") + return + } + var number float64 + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + return + } + if number == 0 { + result = "1" + return + } + sign := math.Signbit(number) + m, frac := math.Modf((number - 1) / 2) + val := m*2 + 1 + if frac != 0 { + if !sign { + val += 2 + } else { + val -= 2 + } + } + result = fmt.Sprintf("%g", val) + return +} + +// PI function returns the value of the mathematical constant π (pi), accurate +// to 15 digits (14 decimal places). The syntax of the function is: +// +// PI() +// +func (fn *formulaFuncs) PI(argsList *list.List) (result string, err error) { + if argsList.Len() != 0 { + err = errors.New("PI accepts no arguments") + return + } + result = fmt.Sprintf("%g", math.Pi) + return +} + // POWER function calculates a given number, raised to a supplied power. // The syntax of the function is: // @@ -1765,6 +1930,154 @@ func (fn *formulaFuncs) PRODUCT(argsList *list.List) (result string, err error) return } +// QUOTIENT function returns the integer portion of a division between two +// supplied numbers. The syntax of the function is: +// +// QUOTIENT(numerator,denominator) +// +func (fn *formulaFuncs) QUOTIENT(argsList *list.List) (result string, err error) { + if argsList.Len() != 2 { + err = errors.New("QUOTIENT requires 2 numeric arguments") + return + } + var x, y float64 + if x, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + return + } + if y, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + return + } + if y == 0 { + err = errors.New(formulaErrorDIV) + return + } + result = fmt.Sprintf("%g", math.Trunc(x/y)) + return +} + +// RADIANS function converts radians into degrees. The syntax of the function is: +// +// RADIANS(angle) +// +func (fn *formulaFuncs) RADIANS(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("RADIANS requires 1 numeric argument") + return + } + var angle float64 + if angle, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + return + } + result = fmt.Sprintf("%g", math.Pi/180.0*angle) + return +} + +// RAND function generates a random real number between 0 and 1. The syntax of +// the function is: +// +// RAND() +// +func (fn *formulaFuncs) RAND(argsList *list.List) (result string, err error) { + if argsList.Len() != 0 { + err = errors.New("RAND accepts no arguments") + return + } + result = fmt.Sprintf("%g", rand.New(rand.NewSource(time.Now().UnixNano())).Float64()) + return +} + +// RANDBETWEEN function generates a random integer between two supplied +// integers. The syntax of the function is: +// +// RANDBETWEEN(bottom,top) +// +func (fn *formulaFuncs) RANDBETWEEN(argsList *list.List) (result string, err error) { + if argsList.Len() != 2 { + err = errors.New("RANDBETWEEN requires 2 numeric arguments") + return + } + var bottom, top int64 + if bottom, err = strconv.ParseInt(argsList.Front().Value.(formulaArg).Value, 10, 64); err != nil { + return + } + if top, err = strconv.ParseInt(argsList.Back().Value.(formulaArg).Value, 10, 64); err != nil { + return + } + if top < bottom { + err = errors.New(formulaErrorNUM) + return + } + result = fmt.Sprintf("%g", float64(rand.New(rand.NewSource(time.Now().UnixNano())).Int63n(top-bottom+1)+bottom)) + return +} + +// romanNumerals defined a numeral system that originated in ancient Rome and +// remained the usual way of writing numbers throughout Europe well into the +// Late Middle Ages. +type romanNumerals struct { + n float64 + s string +} + +var romanTable = [][]romanNumerals{{{1000, "M"}, {900, "CM"}, {500, "D"}, {400, "CD"}, {100, "C"}, {90, "XC"}, {50, "L"}, {40, "XL"}, {10, "X"}, {9, "IX"}, {5, "V"}, {4, "IV"}, {1, "I"}}, + {{1000, "M"}, {950, "LM"}, {900, "CM"}, {500, "D"}, {450, "LD"}, {400, "CD"}, {100, "C"}, {95, "VC"}, {90, "XC"}, {50, "L"}, {45, "VL"}, {40, "XL"}, {10, "X"}, {9, "IX"}, {5, "V"}, {4, "IV"}, {1, "I"}}, + {{1000, "M"}, {990, "XM"}, {950, "LM"}, {900, "CM"}, {500, "D"}, {490, "XD"}, {450, "LD"}, {400, "CD"}, {100, "C"}, {99, "IC"}, {90, "XC"}, {50, "L"}, {45, "VL"}, {40, "XL"}, {10, "X"}, {9, "IX"}, {5, "V"}, {4, "IV"}, {1, "I"}}, + {{1000, "M"}, {995, "VM"}, {990, "XM"}, {950, "LM"}, {900, "CM"}, {500, "D"}, {495, "VD"}, {490, "XD"}, {450, "LD"}, {400, "CD"}, {100, "C"}, {99, "IC"}, {90, "XC"}, {50, "L"}, {45, "VL"}, {40, "XL"}, {10, "X"}, {9, "IX"}, {5, "V"}, {4, "IV"}, {1, "I"}}, + {{1000, "M"}, {999, "IM"}, {995, "VM"}, {990, "XM"}, {950, "LM"}, {900, "CM"}, {500, "D"}, {499, "ID"}, {495, "VD"}, {490, "XD"}, {450, "LD"}, {400, "CD"}, {100, "C"}, {99, "IC"}, {90, "XC"}, {50, "L"}, {45, "VL"}, {40, "XL"}, {10, "X"}, {9, "IX"}, {5, "V"}, {4, "IV"}, {1, "I"}}} + +// ROMAN function converts an arabic number to Roman. I.e. for a supplied +// integer, the function returns a text string depicting the roman numeral +// form of the number. The syntax of the function is: +// +// ROMAN(number,[form]) +// +func (fn *formulaFuncs) ROMAN(argsList *list.List) (result string, err error) { + if argsList.Len() == 0 { + err = errors.New("ROMAN requires at least 1 argument") + return + } + if argsList.Len() > 2 { + err = errors.New("ROMAN allows at most 2 arguments") + return + } + var number float64 + var form int + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + return + } + if argsList.Len() > 1 { + if form, err = strconv.Atoi(argsList.Back().Value.(formulaArg).Value); err != nil { + return + } + if form < 0 { + form = 0 + } else if form > 4 { + form = 4 + } + } + decimalTable := romanTable[0] + switch form { + case 1: + decimalTable = romanTable[1] + case 2: + decimalTable = romanTable[2] + case 3: + decimalTable = romanTable[3] + case 4: + decimalTable = romanTable[4] + } + val := math.Trunc(number) + buf := bytes.Buffer{} + for _, r := range decimalTable { + for val >= r.n { + buf.WriteString(r.s) + val -= r.n + } + } + result = buf.String() + return +} + // SIGN function returns the arithmetic sign (+1, -1 or 0) of a supplied // number. I.e. if the number is positive, the Sign function returns +1, if // the number is negative, the function returns -1 and if the number is 0 @@ -1840,28 +2153,3 @@ func (fn *formulaFuncs) SUM(argsList *list.List) (result string, err error) { result = fmt.Sprintf("%g", sum) return } - -// QUOTIENT function returns the integer portion of a division between two -// supplied numbers. The syntax of the function is: -// -// QUOTIENT(numerator,denominator) -// -func (fn *formulaFuncs) QUOTIENT(argsList *list.List) (result string, err error) { - if argsList.Len() != 2 { - err = errors.New("QUOTIENT requires 2 numeric arguments") - return - } - var x, y float64 - if x, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { - return - } - if y, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { - return - } - if y == 0 { - err = errors.New(formulaErrorDIV) - return - } - result = fmt.Sprintf("%g", math.Trunc(x/y)) - return -} diff --git a/calc_test.go b/calc_test.go index c66de8c..2b35e48 100644 --- a/calc_test.go +++ b/calc_test.go @@ -218,10 +218,54 @@ func TestCalcCellValue(t *testing.T) { "=LOG10(1000)": "3", "=LOG10(0.001)": "-3", "=LOG10(25)": "1.3979400086720375", + // MOD + "=MOD(6,4)": "2", + "=MOD(6,3)": "0", + "=MOD(6,2.5)": "1", + "=MOD(6,1.333)": "0.6680000000000001", + // MROUND + "=MROUND(333.7,0.5)": "333.5", + "=MROUND(333.8,1)": "334", + "=MROUND(333.3,2)": "334", + "=MROUND(555.3,400)": "400", + "=MROUND(555,1000)": "1000", + "=MROUND(-555.7,-1)": "-556", + "=MROUND(-555.4,-1)": "-555", + "=MROUND(-1555,-1000)": "-2000", + // MULTINOMIAL + "=MULTINOMIAL(3,1,2,5)": "27720", + // _xlfn.MUNIT + "=_xlfn.MUNIT(4)": "", // not support currently + // ODD + "=ODD(22)": "23", + "=ODD(1.22)": "3", + "=ODD(1.22+4)": "7", + "=ODD(0)": "1", + "=ODD(-1.3)": "-3", + "=ODD(-10)": "-11", + "=ODD(-3)": "-3", + // PI + "=PI()": "3.141592653589793", // POWER "=POWER(4,2)": "16", // PRODUCT "=PRODUCT(3,6)": "18", + // QUOTIENT + "=QUOTIENT(5,2)": "2", + "=QUOTIENT(4.5,3.1)": "1", + "=QUOTIENT(-10,3)": "-3", + // RADIANS + "=RADIANS(50)": "0.8726646259971648", + "=RADIANS(-180)": "-3.141592653589793", + "=RADIANS(180)": "3.141592653589793", + "=RADIANS(360)": "6.283185307179586", + // ROMAN + "=ROMAN(499,0)": "CDXCIX", + "=ROMAN(1999,0)": "MCMXCIX", + "=ROMAN(1999,1)": "MLMVLIV", + "=ROMAN(1999,2)": "MXMIX", + "=ROMAN(1999,3)": "MVMIV", + "=ROMAN(1999,4)": "MIM", // SIGN "=SIGN(9.5)": "1", "=SIGN(-9.5)": "-1", @@ -244,10 +288,6 @@ func TestCalcCellValue(t *testing.T) { "=((3+5*2)+3)/5+(-6)/4*2+3": "3.2", "=1+SUM(SUM(1,2*3),4)*-4/2+5+(4+2)*3": "2", "=1+SUM(SUM(1,2*3),4)*4/3+5+(4+2)*3": "38.666666666666664", - // QUOTIENT - "=QUOTIENT(5, 2)": "2", - "=QUOTIENT(4.5, 3.1)": "1", - "=QUOTIENT(-10, 3)": "-3", } for formula, expected := range mathCalc { f := prepareData() @@ -362,18 +402,40 @@ func TestCalcCellValue(t *testing.T) { "=LOG(1,1)": "#DIV/0!", // LOG10 "=LOG10()": "LOG10 requires 1 numeric argument", + // MOD + "=MOD()": "MOD requires 2 numeric arguments", + "=MOD(6,0)": "#DIV/0!", + // MROUND + "=MROUND()": "MROUND requires 2 numeric arguments", + "=MROUND(1,0)": "#NUM!", + // _xlfn.MUNIT + "=_xlfn.MUNIT()": "MUNIT requires 1 numeric argument", // not support currently + // ODD + "=ODD()": "ODD requires 1 numeric argument", + // PI + "=PI(1)": "PI accepts no arguments", // POWER "=POWER(0,0)": "#NUM!", "=POWER(0,-1)": "#DIV/0!", "=POWER(1)": "POWER requires 2 numeric arguments", + // QUOTIENT + "=QUOTIENT(1,0)": "#DIV/0!", + "=QUOTIENT(1)": "QUOTIENT requires 2 numeric arguments", + // RADIANS + "=RADIANS()": "RADIANS requires 1 numeric argument", + // RAND + "=RAND(1)": "RAND accepts no arguments", + // RANDBETWEEN + "=RANDBETWEEN()": "RANDBETWEEN requires 2 numeric arguments", + "=RANDBETWEEN(2,1)": "#NUM!", + // ROMAN + "=ROMAN()": "ROMAN requires at least 1 argument", + "=ROMAN(1,2,3)": "ROMAN allows at most 2 arguments", // SIGN "=SIGN()": "SIGN requires 1 numeric argument", // SQRT "=SQRT(-1)": "#NUM!", "=SQRT(1,2)": "SQRT requires 1 numeric argument", - // QUOTIENT - "=QUOTIENT(1,0)": "#DIV/0!", - "=QUOTIENT(1)": "QUOTIENT requires 2 numeric arguments", } for formula, expected := range mathCalcError { f := prepareData() -- cgit v1.2.1 From 08185c398a0bf352662efa64ebb17c8746556969 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 8 May 2020 00:31:17 +0800 Subject: #65, fn: ROUND, ROUNDDOWN, ROUNDUP, SEC, SECH, SIN, SINH, SQRTPI, TAN, TANH, TRUNC --- calc.go | 260 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ calc_test.go | 81 ++++++++++++++++++- 2 files changed, 339 insertions(+), 2 deletions(-) diff --git a/calc.go b/calc.go index fb94e27..cc39ba5 100644 --- a/calc.go +++ b/calc.go @@ -2078,6 +2078,141 @@ func (fn *formulaFuncs) ROMAN(argsList *list.List) (result string, err error) { return } +type roundMode byte + +const ( + closest roundMode = iota + down + up +) + +// round rounds a supplied number up or down. +func (fn *formulaFuncs) round(number, digits float64, mode roundMode) float64 { + significance := 1.0 + if digits > 0 { + significance = math.Pow(1/10.0, digits) + } else { + significance = math.Pow(10.0, -digits) + } + val, res := math.Modf(number / significance) + switch mode { + case closest: + const eps = 0.499999999 + if res >= eps { + val++ + } else if res <= -eps { + val-- + } + case down: + case up: + if res > 0 { + val++ + } else if res < 0 { + val-- + } + } + return val * significance +} + +// ROUND function rounds a supplied number up or down, to a specified number +// of decimal places. The syntax of the function is: +// +// ROUND(number,num_digits) +// +func (fn *formulaFuncs) ROUND(argsList *list.List) (result string, err error) { + if argsList.Len() != 2 { + err = errors.New("ROUND requires 2 numeric arguments") + return + } + var number, digits float64 + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + return + } + if digits, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + return + } + result = fmt.Sprintf("%g", fn.round(number, digits, closest)) + return +} + +// ROUNDDOWN function rounds a supplied number down towards zero, to a +// specified number of decimal places. The syntax of the function is: +// +// ROUNDDOWN(number,num_digits) +// +func (fn *formulaFuncs) ROUNDDOWN(argsList *list.List) (result string, err error) { + if argsList.Len() != 2 { + err = errors.New("ROUNDDOWN requires 2 numeric arguments") + return + } + var number, digits float64 + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + return + } + if digits, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + return + } + result = fmt.Sprintf("%g", fn.round(number, digits, down)) + return +} + +// ROUNDUP function rounds a supplied number up, away from zero, to a +// specified number of decimal places. The syntax of the function is: +// +// ROUNDUP(number,num_digits) +// +func (fn *formulaFuncs) ROUNDUP(argsList *list.List) (result string, err error) { + if argsList.Len() != 2 { + err = errors.New("ROUNDUP requires 2 numeric arguments") + return + } + var number, digits float64 + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + return + } + if digits, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + return + } + result = fmt.Sprintf("%g", fn.round(number, digits, up)) + return +} + +// SEC function calculates the secant of a given angle. The syntax of the +// function is: +// +// SEC(number) +// +func (fn *formulaFuncs) SEC(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("SEC requires 1 numeric argument") + return + } + var number float64 + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + return + } + result = fmt.Sprintf("%g", math.Cos(number)) + return +} + +// SECH function calculates the hyperbolic secant (sech) of a supplied angle. +// The syntax of the function is: +// +// SECH(number) +// +func (fn *formulaFuncs) SECH(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("SECH requires 1 numeric argument") + return + } + var number float64 + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + return + } + result = fmt.Sprintf("%g", 1/math.Cosh(number)) + return +} + // SIGN function returns the arithmetic sign (+1, -1 or 0) of a supplied // number. I.e. if the number is positive, the Sign function returns +1, if // the number is negative, the function returns -1 and if the number is 0 @@ -2106,6 +2241,42 @@ func (fn *formulaFuncs) SIGN(argsList *list.List) (result string, err error) { return } +// SIN function calculates the sine of a given angle. The syntax of the +// function is: +// +// SIN(number) +// +func (fn *formulaFuncs) SIN(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("SIN requires 1 numeric argument") + return + } + var number float64 + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + return + } + result = fmt.Sprintf("%g", math.Sin(number)) + return +} + +// SINH function calculates the hyperbolic sine (sinh) of a supplied number. +// The syntax of the function is: +// +// SINH(number) +// +func (fn *formulaFuncs) SINH(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("SINH requires 1 numeric argument") + return + } + var number float64 + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + return + } + result = fmt.Sprintf("%g", math.Sinh(number)) + return +} + // SQRT function calculates the positive square root of a supplied number. The // syntax of the function is: // @@ -2133,6 +2304,24 @@ func (fn *formulaFuncs) SQRT(argsList *list.List) (result string, err error) { return } +// SQRTPI function returns the square root of a supplied number multiplied by +// the mathematical constant, π. The syntax of the function is: +// +// SQRTPI(number) +// +func (fn *formulaFuncs) SQRTPI(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("SQRTPI requires 1 numeric argument") + return + } + var number float64 + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + return + } + result = fmt.Sprintf("%g", math.Sqrt(number*math.Pi)) + return +} + // SUM function adds together a supplied set of numbers and returns the sum of // these values. The syntax of the function is: // @@ -2153,3 +2342,74 @@ func (fn *formulaFuncs) SUM(argsList *list.List) (result string, err error) { result = fmt.Sprintf("%g", sum) return } + +// TAN function calculates the tangent of a given angle. The syntax of the +// function is: +// +// TAN(number) +// +func (fn *formulaFuncs) TAN(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("TAN requires 1 numeric argument") + return + } + var number float64 + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + return + } + result = fmt.Sprintf("%g", math.Tan(number)) + return +} + +// TANH function calculates the hyperbolic tangent (tanh) of a supplied +// number. The syntax of the function is: +// +// TANH(number) +// +func (fn *formulaFuncs) TANH(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("TANH requires 1 numeric argument") + return + } + var number float64 + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + return + } + result = fmt.Sprintf("%g", math.Tanh(number)) + return +} + +// TRUNC function truncates a supplied number to a specified number of decimal +// places. The syntax of the function is: +// +// TRUNC(number,[number_digits]) +// +func (fn *formulaFuncs) TRUNC(argsList *list.List) (result string, err error) { + if argsList.Len() == 0 { + err = errors.New("TRUNC requires at least 1 argument") + return + } + var number, digits, adjust, rtrim float64 + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + return + } + if argsList.Len() > 1 { + if digits, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + return + } + digits = math.Floor(digits) + } + adjust = math.Pow(10, digits) + x := int((math.Abs(number) - math.Abs(float64(int(number)))) * adjust) + if x != 0 { + if rtrim, err = strconv.ParseFloat(strings.TrimRight(strconv.Itoa(x), "0"), 64); err != nil { + return + } + } + if (digits > 0) && (rtrim < adjust/10) { + result = fmt.Sprintf("%g", number) + return + } + result = fmt.Sprintf("%g", float64(int(number*adjust))/adjust) + return +} diff --git a/calc_test.go b/calc_test.go index 2b35e48..6225d50 100644 --- a/calc_test.go +++ b/calc_test.go @@ -266,14 +266,55 @@ func TestCalcCellValue(t *testing.T) { "=ROMAN(1999,2)": "MXMIX", "=ROMAN(1999,3)": "MVMIV", "=ROMAN(1999,4)": "MIM", + // ROUND + "=ROUND(100.319,1)": "100.30000000000001", + "=ROUND(5.28,1)": "5.300000000000001", + "=ROUND(5.9999,3)": "6.000000000000002", + "=ROUND(99.5,0)": "100", + "=ROUND(-6.3,0)": "-6", + "=ROUND(-100.5,0)": "-101", + "=ROUND(-22.45,1)": "-22.5", + "=ROUND(999,-1)": "1000", + "=ROUND(991,-1)": "990", + // ROUNDDOWN + "=ROUNDDOWN(99.999,1)": "99.9", + "=ROUNDDOWN(99.999,2)": "99.99000000000002", + "=ROUNDDOWN(99.999,0)": "99", + "=ROUNDDOWN(99.999,-1)": "90", + "=ROUNDDOWN(-99.999,2)": "-99.99000000000002", + "=ROUNDDOWN(-99.999,-1)": "-90", + // ROUNDUP + "=ROUNDUP(11.111,1)": "11.200000000000001", + "=ROUNDUP(11.111,2)": "11.120000000000003", + "=ROUNDUP(11.111,0)": "12", + "=ROUNDUP(11.111,-1)": "20", + "=ROUNDUP(-11.111,2)": "-11.120000000000003", + "=ROUNDUP(-11.111,-1)": "-20", + // SEC + "=_xlfn.SEC(-3.14159265358979)": "-1", + "=_xlfn.SEC(0)": "1", + // SECH + "=_xlfn.SECH(-3.14159265358979)": "0.0862667383340547", + "=_xlfn.SECH(0)": "1", // SIGN "=SIGN(9.5)": "1", "=SIGN(-9.5)": "-1", "=SIGN(0)": "0", "=SIGN(0.00000001)": "1", "=SIGN(6-7)": "-1", + // SIN + "=SIN(0.785398163)": "0.7071067809055092", + // SINH + "=SINH(0)": "0", + "=SINH(0.5)": "0.5210953054937474", + "=SINH(-2)": "-3.626860407847019", // SQRT "=SQRT(4)": "2", + // SQRTPI + "=SQRTPI(5)": "3.963327297606011", + "=SQRTPI(0.2)": "0.7926654595212022", + "=SQRTPI(100)": "17.72453850905516", + "=SQRTPI(0)": "0", // SUM "=SUM(1,2)": "3", "=SUM(1,2+3)": "6", @@ -288,6 +329,20 @@ func TestCalcCellValue(t *testing.T) { "=((3+5*2)+3)/5+(-6)/4*2+3": "3.2", "=1+SUM(SUM(1,2*3),4)*-4/2+5+(4+2)*3": "2", "=1+SUM(SUM(1,2*3),4)*4/3+5+(4+2)*3": "38.666666666666664", + // TAN + "=TAN(1.047197551)": "1.732050806782486", + "=TAN(0)": "0", + // TANH + "=TANH(0)": "0", + "=TANH(0.5)": "0.46211715726000974", + "=TANH(-2)": "-0.9640275800758169", + // TRUNC + "=TRUNC(99.999,1)": "99.9", + "=TRUNC(99.999,2)": "99.99", + "=TRUNC(99.999)": "99", + "=TRUNC(99.999,-1)": "90", + "=TRUNC(-99.999,2)": "-99.99", + "=TRUNC(-99.999,-1)": "-90", } for formula, expected := range mathCalc { f := prepareData() @@ -431,11 +486,33 @@ func TestCalcCellValue(t *testing.T) { // ROMAN "=ROMAN()": "ROMAN requires at least 1 argument", "=ROMAN(1,2,3)": "ROMAN allows at most 2 arguments", + // ROUND + "=ROUND()": "ROUND requires 2 numeric arguments", + // ROUNDDOWN + "=ROUNDDOWN()": "ROUNDDOWN requires 2 numeric arguments", + // ROUNDUP + "=ROUNDUP()": "ROUNDUP requires 2 numeric arguments", + // SEC + "=_xlfn.SEC()": "SEC requires 1 numeric argument", + // _xlfn.SECH + "=_xlfn.SECH()": "SECH requires 1 numeric argument", // SIGN "=SIGN()": "SIGN requires 1 numeric argument", + // SIN + "=SIN()": "SIN requires 1 numeric argument", + // SINH + "=SINH()": "SINH requires 1 numeric argument", // SQRT - "=SQRT(-1)": "#NUM!", - "=SQRT(1,2)": "SQRT requires 1 numeric argument", + "=SQRT()": "SQRT requires 1 numeric argument", + "=SQRT(-1)": "#NUM!", + // SQRTPI + "=SQRTPI()": "SQRTPI requires 1 numeric argument", + // TAN + "=TAN()": "TAN requires 1 numeric argument", + // TANH + "=TANH()": "TANH requires 1 numeric argument", + // TRUNC + "=TRUNC()": "TRUNC requires at least 1 argument", } for formula, expected := range mathCalcError { f := prepareData() -- cgit v1.2.1 From 4188dc7a4a650200c697fd47ad28ba346bf786a6 Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 9 May 2020 00:32:36 +0800 Subject: - fn: SUMSQ - resolve ineffectual assignment - handle exception with invalid formula - update range resolver --- calc.go | 123 ++++++++++++++++++++++++++++++++++++++++++++++++----------- calc_test.go | 22 ++++++++--- 2 files changed, 116 insertions(+), 29 deletions(-) diff --git a/calc.go b/calc.go index cc39ba5..f85d16a 100644 --- a/calc.go +++ b/calc.go @@ -259,12 +259,18 @@ func (f *File) evalInfixExp(sheet string, tokens []efp.Token) (efp.Token, error) } optStack.Pop() } + if opdStack.Len() == 0 { + return efp.Token{}, errors.New("formula not valid") + } return opdStack.Peek().(efp.Token), err } // calculate evaluate basic arithmetic operations. func calculate(opdStack *Stack, opt efp.Token) error { if opt.TValue == "-" && opt.TType == efp.TokenTypeOperatorPrefix { + if opdStack.Len() < 1 { + return errors.New("formula not valid") + } opd := opdStack.Pop().(efp.Token) opdVal, err := strconv.ParseFloat(opd.TValue, 64) if err != nil { @@ -274,6 +280,9 @@ func calculate(opdStack *Stack, opt efp.Token) error { opdStack.Push(efp.Token{TValue: fmt.Sprintf("%g", result), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) } if opt.TValue == "+" { + if opdStack.Len() < 2 { + return errors.New("formula not valid") + } rOpd := opdStack.Pop().(efp.Token) lOpd := opdStack.Pop().(efp.Token) lOpdVal, err := strconv.ParseFloat(lOpd.TValue, 64) @@ -288,6 +297,9 @@ func calculate(opdStack *Stack, opt efp.Token) error { opdStack.Push(efp.Token{TValue: fmt.Sprintf("%g", result), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) } if opt.TValue == "-" && opt.TType == efp.TokenTypeOperatorInfix { + if opdStack.Len() < 2 { + return errors.New("formula not valid") + } rOpd := opdStack.Pop().(efp.Token) lOpd := opdStack.Pop().(efp.Token) lOpdVal, err := strconv.ParseFloat(lOpd.TValue, 64) @@ -302,6 +314,9 @@ func calculate(opdStack *Stack, opt efp.Token) error { opdStack.Push(efp.Token{TValue: fmt.Sprintf("%g", result), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) } if opt.TValue == "*" { + if opdStack.Len() < 2 { + return errors.New("formula not valid") + } rOpd := opdStack.Pop().(efp.Token) lOpd := opdStack.Pop().(efp.Token) lOpdVal, err := strconv.ParseFloat(lOpd.TValue, 64) @@ -316,6 +331,9 @@ func calculate(opdStack *Stack, opt efp.Token) error { opdStack.Push(efp.Token{TValue: fmt.Sprintf("%g", result), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) } if opt.TValue == "/" { + if opdStack.Len() < 2 { + return errors.New("formula not valid") + } rOpd := opdStack.Pop().(efp.Token) lOpd := opdStack.Pop().(efp.Token) lOpdVal, err := strconv.ParseFloat(lOpd.TValue, 64) @@ -444,12 +462,13 @@ func (f *File) parseReference(sheet, reference string) (result []string, matrix } // rangeResolver extract value as string from given reference and range list. -// This function will not ignore the empty cell. Note that the result of 3D -// range references may be different from Excel in some cases, for example, -// A1:A2:A2:B3 in Excel will include B1, but we wont. +// This function will not ignore the empty cell. For example, +// A1:A2:A2:B3 will be reference A1:B3. func (f *File) rangeResolver(cellRefs, cellRanges *list.List) (result []string, matrix [][]string, err error) { + var fromRow, toRow, fromCol, toCol int = 1, 1, 1, 1 + var sheet string filter := map[string]string{} - // extract value from ranges + // prepare value range for temp := cellRanges.Front(); temp != nil; temp = temp.Next() { cr := temp.Value.(cellRange) if cr.From.Sheet != cr.To.Sheet { @@ -457,22 +476,59 @@ func (f *File) rangeResolver(cellRefs, cellRanges *list.List) (result []string, } rng := []int{cr.From.Col, cr.From.Row, cr.To.Col, cr.To.Row} sortCoordinates(rng) - matrix = [][]string{} - for row := rng[1]; row <= rng[3]; row++ { + if cr.From.Row < fromRow { + fromRow = cr.From.Row + } + if cr.From.Col < fromCol { + fromCol = cr.From.Col + } + if cr.To.Row > fromRow { + toRow = cr.To.Row + } + if cr.To.Col > toCol { + toCol = cr.To.Col + } + if cr.From.Sheet != "" { + sheet = cr.From.Sheet + } + } + for temp := cellRefs.Front(); temp != nil; temp = temp.Next() { + cr := temp.Value.(cellRef) + if cr.Sheet != "" { + sheet = cr.Sheet + } + if cr.Row < fromRow { + fromRow = cr.Row + } + if cr.Col < fromCol { + fromCol = cr.Col + } + if cr.Row > fromRow { + toRow = cr.Row + } + if cr.Col > toCol { + toCol = cr.Col + } + } + // extract value from ranges + if cellRanges.Len() > 0 { + for row := fromRow; row <= toRow; row++ { var matrixRow = []string{} - for col := rng[0]; col <= rng[2]; col++ { + for col := fromCol; col <= toCol; col++ { var cell, value string if cell, err = CoordinatesToCellName(col, row); err != nil { return } - if value, err = f.GetCellValue(cr.From.Sheet, cell); err != nil { + if value, err = f.GetCellValue(sheet, cell); err != nil { return } filter[cell] = value matrixRow = append(matrixRow, value) + result = append(result, value) } matrix = append(matrix, matrixRow) } + return } // extract value from references for temp := cellRefs.Front(); temp != nil; temp = temp.Next() { @@ -824,7 +880,7 @@ func (fn *formulaFuncs) CEILING(argsList *list.List) (result string, err error) err = errors.New("CEILING allows at most 2 arguments") return } - var number, significance float64 = 0, 1 + number, significance, res := 0.0, 1.0, 0.0 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { return } @@ -844,7 +900,7 @@ func (fn *formulaFuncs) CEILING(argsList *list.List) (result string, err error) result = fmt.Sprintf("%g", math.Ceil(number)) return } - number, res := math.Modf(number / significance) + number, res = math.Modf(number / significance) if res > 0 { number++ } @@ -866,7 +922,7 @@ func (fn *formulaFuncs) CEILINGMATH(argsList *list.List) (result string, err err err = errors.New("CEILING.MATH allows at most 3 arguments") return } - var number, significance, mode float64 = 0, 1, 1 + number, significance, mode := 0.0, 1.0, 1.0 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { return } @@ -914,7 +970,7 @@ func (fn *formulaFuncs) CEILINGPRECISE(argsList *list.List) (result string, err err = errors.New("CEILING.PRECISE allows at most 2 arguments") return } - var number, significance float64 = 0, 1 + number, significance := 0.0, 1.0 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { return } @@ -955,7 +1011,7 @@ func (fn *formulaFuncs) COMBIN(argsList *list.List) (result string, err error) { err = errors.New("COMBIN requires 2 argument") return } - var number, chosen, val float64 = 0, 0, 1 + number, chosen, val := 0.0, 0.0, 1.0 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { return } @@ -1274,7 +1330,7 @@ func (fn *formulaFuncs) FACTDOUBLE(argsList *list.List) (result string, err erro err = errors.New("FACTDOUBLE requires 1 numeric argument") return } - var number, val float64 = 0, 1 + number, val := 0.0, 1.0 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { return } @@ -1298,7 +1354,7 @@ func (fn *formulaFuncs) FLOOR(argsList *list.List) (result string, err error) { err = errors.New("FLOOR requires 2 numeric arguments") return } - var number, significance float64 = 0, 1 + var number, significance float64 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { return } @@ -1333,7 +1389,7 @@ func (fn *formulaFuncs) FLOORMATH(argsList *list.List) (result string, err error err = errors.New("FLOOR.MATH allows at most 3 arguments") return } - var number, significance, mode float64 = 0, 1, 1 + number, significance, mode := 0.0, 1.0, 1.0 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { return } @@ -1376,7 +1432,7 @@ func (fn *formulaFuncs) FLOORPRECISE(argsList *list.List) (result string, err er err = errors.New("FLOOR.PRECISE allows at most 2 arguments") return } - var number, significance float64 = 0, 1 + var number, significance float64 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { return } @@ -1488,7 +1544,7 @@ func (fn *formulaFuncs) ISOCEILING(argsList *list.List) (result string, err erro err = errors.New("ISO.CEILING allows at most 2 arguments") return } - var number, significance float64 = 0, 1 + var number, significance float64 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { return } @@ -1605,7 +1661,7 @@ func (fn *formulaFuncs) LOG(argsList *list.List) (result string, err error) { err = errors.New("LOG allows at most 2 arguments") return } - var number, base float64 = 0, 10 + number, base := 0.0, 10.0 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { return } @@ -1757,7 +1813,7 @@ func (fn *formulaFuncs) MROUND(argsList *list.List) (result string, err error) { err = errors.New("MROUND requires 2 numeric arguments") return } - var number, multiple float64 = 0, 1 + var number, multiple float64 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { return } @@ -1788,7 +1844,7 @@ func (fn *formulaFuncs) MROUND(argsList *list.List) (result string, err error) { // MULTINOMIAL(number1,[number2],...) // func (fn *formulaFuncs) MULTINOMIAL(argsList *list.List) (result string, err error) { - var val, num, denom float64 = 0, 0, 1 + val, num, denom := 0.0, 0.0, 1.0 for arg := argsList.Front(); arg != nil; arg = arg.Next() { token := arg.Value.(formulaArg) if token.Value == "" { @@ -1915,7 +1971,7 @@ func (fn *formulaFuncs) POWER(argsList *list.List) (result string, err error) { // PRODUCT(number1,[number2],...) // func (fn *formulaFuncs) PRODUCT(argsList *list.List) (result string, err error) { - var val, product float64 = 0, 1 + val, product := 0.0, 1.0 for arg := argsList.Front(); arg != nil; arg = arg.Next() { token := arg.Value.(formulaArg) if token.Value == "" { @@ -2088,7 +2144,7 @@ const ( // round rounds a supplied number up or down. func (fn *formulaFuncs) round(number, digits float64, mode roundMode) float64 { - significance := 1.0 + var significance float64 if digits > 0 { significance = math.Pow(1/10.0, digits) } else { @@ -2343,6 +2399,27 @@ func (fn *formulaFuncs) SUM(argsList *list.List) (result string, err error) { return } +// SUMSQ function returns the sum of squares of a supplied set of values. The +// syntax of the function is: +// +// SUMSQ(number1,[number2],...) +// +func (fn *formulaFuncs) SUMSQ(argsList *list.List) (result string, err error) { + var val, sq float64 + for arg := argsList.Front(); arg != nil; arg = arg.Next() { + token := arg.Value.(formulaArg) + if token.Value == "" { + continue + } + if val, err = strconv.ParseFloat(token.Value, 64); err != nil { + return + } + sq += val * val + } + result = fmt.Sprintf("%g", sq) + return +} + // TAN function calculates the tangent of a given angle. The syntax of the // function is: // diff --git a/calc_test.go b/calc_test.go index 6225d50..6d0f853 100644 --- a/calc_test.go +++ b/calc_test.go @@ -329,6 +329,9 @@ func TestCalcCellValue(t *testing.T) { "=((3+5*2)+3)/5+(-6)/4*2+3": "3.2", "=1+SUM(SUM(1,2*3),4)*-4/2+5+(4+2)*3": "2", "=1+SUM(SUM(1,2*3),4)*4/3+5+(4+2)*3": "38.666666666666664", + // SUMSQ + "=SUMSQ(A1:A4)": "14", + "=SUMSQ(A1,B1,A2,B2,6)": "82", // TAN "=TAN(1.047197551)": "1.732050806782486", "=TAN(0)": "0", @@ -349,7 +352,7 @@ func TestCalcCellValue(t *testing.T) { assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula)) result, err := f.CalcCellValue("Sheet1", "C1") assert.NoError(t, err) - assert.Equal(t, expected, result) + assert.Equal(t, expected, result, formula) } mathCalcError := map[string]string{ // ABS @@ -507,6 +510,13 @@ func TestCalcCellValue(t *testing.T) { "=SQRT(-1)": "#NUM!", // SQRTPI "=SQRTPI()": "SQRTPI requires 1 numeric argument", + // SUM + "=SUM((": "formula not valid", + "=SUM(-)": "formula not valid", + "=SUM(1+)": "formula not valid", + "=SUM(1-)": "formula not valid", + "=SUM(1*)": "formula not valid", + "=SUM(1/)": "formula not valid", // TAN "=TAN()": "TAN requires 1 numeric argument", // TANH @@ -519,7 +529,7 @@ func TestCalcCellValue(t *testing.T) { assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula)) result, err := f.CalcCellValue("Sheet1", "C1") assert.EqualError(t, err, expected) - assert.Equal(t, "", result) + assert.Equal(t, "", result, formula) } referenceCalc := map[string]string{ @@ -535,15 +545,15 @@ func TestCalcCellValue(t *testing.T) { "=SUM(Sheet1!A1:Sheet1!A1:A2,A2)": "5", "=SUM(A1,A2,A3)*SUM(2,3)": "30", "=1+SUM(SUM(A1+A2/A3)*(2-3),2)": "1.3333333333333335", - "=A1/A2/SUM(A1:A2:B1)": "0.07142857142857142", - "=A1/A2/SUM(A1:A2:B1)*A3": "0.21428571428571427", + "=A1/A2/SUM(A1:A2:B1)": "0.041666666666666664", + "=A1/A2/SUM(A1:A2:B1)*A3": "0.125", } for formula, expected := range referenceCalc { f := prepareData() assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula)) result, err := f.CalcCellValue("Sheet1", "C1") assert.NoError(t, err) - assert.Equal(t, expected, result) + assert.Equal(t, expected, result, formula) } referenceCalcError := map[string]string{ @@ -557,7 +567,7 @@ func TestCalcCellValue(t *testing.T) { assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula)) result, err := f.CalcCellValue("Sheet1", "C1") assert.EqualError(t, err, expected) - assert.Equal(t, "", result) + assert.Equal(t, "", result, formula) } // Test get calculated cell value on not formula cell. -- cgit v1.2.1 From 882abb80988b7c50286dd2e6c6589fab10662db6 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 10 May 2020 16:56:08 +0800 Subject: - formula engine: reduce cyclomatic complexity - styles: allow empty and default cell formats, #628 --- calc.go | 303 +++++++++++++++++++++++++++++++++------------------------ calc_test.go | 2 +- cell.go | 4 +- go.mod | 1 + go.sum | 2 + styles.go | 16 +-- styles_test.go | 6 +- xmlStyles.go | 26 ++--- 8 files changed, 206 insertions(+), 154 deletions(-) diff --git a/calc.go b/calc.go index f85d16a..3d4a8be 100644 --- a/calc.go +++ b/calc.go @@ -63,8 +63,8 @@ type formulaArg struct { type formulaFuncs struct{} // CalcCellValue provides a function to get calculated cell value. This -// feature is currently in beta. Array formula, table formula and some other -// formulas are not supported currently. +// feature is currently in working processing. Array formula, table formula +// and some other formulas are not supported currently. func (f *File) CalcCellValue(sheet, cell string) (result string, err error) { var ( formula string @@ -265,6 +265,89 @@ func (f *File) evalInfixExp(sheet string, tokens []efp.Token) (efp.Token, error) return opdStack.Peek().(efp.Token), err } +// calcAdd evaluate addition arithmetic operations. +func calcAdd(opdStack *Stack) error { + if opdStack.Len() < 2 { + return errors.New("formula not valid") + } + rOpd := opdStack.Pop().(efp.Token) + lOpd := opdStack.Pop().(efp.Token) + lOpdVal, err := strconv.ParseFloat(lOpd.TValue, 64) + if err != nil { + return err + } + rOpdVal, err := strconv.ParseFloat(rOpd.TValue, 64) + if err != nil { + return err + } + result := lOpdVal + rOpdVal + opdStack.Push(efp.Token{TValue: fmt.Sprintf("%g", result), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) + return nil +} + +// calcAdd evaluate subtraction arithmetic operations. +func calcSubtract(opdStack *Stack) error { + if opdStack.Len() < 2 { + return errors.New("formula not valid") + } + rOpd := opdStack.Pop().(efp.Token) + lOpd := opdStack.Pop().(efp.Token) + lOpdVal, err := strconv.ParseFloat(lOpd.TValue, 64) + if err != nil { + return err + } + rOpdVal, err := strconv.ParseFloat(rOpd.TValue, 64) + if err != nil { + return err + } + result := lOpdVal - rOpdVal + opdStack.Push(efp.Token{TValue: fmt.Sprintf("%g", result), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) + return nil +} + +// calcAdd evaluate multiplication arithmetic operations. +func calcMultiply(opdStack *Stack) error { + if opdStack.Len() < 2 { + return errors.New("formula not valid") + } + rOpd := opdStack.Pop().(efp.Token) + lOpd := opdStack.Pop().(efp.Token) + lOpdVal, err := strconv.ParseFloat(lOpd.TValue, 64) + if err != nil { + return err + } + rOpdVal, err := strconv.ParseFloat(rOpd.TValue, 64) + if err != nil { + return err + } + result := lOpdVal * rOpdVal + opdStack.Push(efp.Token{TValue: fmt.Sprintf("%g", result), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) + return nil +} + +// calcAdd evaluate division arithmetic operations. +func calcDivide(opdStack *Stack) error { + if opdStack.Len() < 2 { + return errors.New("formula not valid") + } + rOpd := opdStack.Pop().(efp.Token) + lOpd := opdStack.Pop().(efp.Token) + lOpdVal, err := strconv.ParseFloat(lOpd.TValue, 64) + if err != nil { + return err + } + rOpdVal, err := strconv.ParseFloat(rOpd.TValue, 64) + if err != nil { + return err + } + result := lOpdVal / rOpdVal + if rOpdVal == 0 { + return errors.New(formulaErrorDIV) + } + opdStack.Push(efp.Token{TValue: fmt.Sprintf("%g", result), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) + return nil +} + // calculate evaluate basic arithmetic operations. func calculate(opdStack *Stack, opt efp.Token) error { if opt.TValue == "-" && opt.TType == efp.TokenTypeOperatorPrefix { @@ -279,80 +362,69 @@ func calculate(opdStack *Stack, opt efp.Token) error { result := 0 - opdVal opdStack.Push(efp.Token{TValue: fmt.Sprintf("%g", result), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) } + if opt.TValue == "+" { - if opdStack.Len() < 2 { - return errors.New("formula not valid") - } - rOpd := opdStack.Pop().(efp.Token) - lOpd := opdStack.Pop().(efp.Token) - lOpdVal, err := strconv.ParseFloat(lOpd.TValue, 64) - if err != nil { + if err := calcAdd(opdStack); err != nil { return err } - rOpdVal, err := strconv.ParseFloat(rOpd.TValue, 64) - if err != nil { - return err - } - result := lOpdVal + rOpdVal - opdStack.Push(efp.Token{TValue: fmt.Sprintf("%g", result), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) } if opt.TValue == "-" && opt.TType == efp.TokenTypeOperatorInfix { - if opdStack.Len() < 2 { - return errors.New("formula not valid") - } - rOpd := opdStack.Pop().(efp.Token) - lOpd := opdStack.Pop().(efp.Token) - lOpdVal, err := strconv.ParseFloat(lOpd.TValue, 64) - if err != nil { + if err := calcSubtract(opdStack); err != nil { return err } - rOpdVal, err := strconv.ParseFloat(rOpd.TValue, 64) - if err != nil { - return err - } - result := lOpdVal - rOpdVal - opdStack.Push(efp.Token{TValue: fmt.Sprintf("%g", result), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) } if opt.TValue == "*" { - if opdStack.Len() < 2 { - return errors.New("formula not valid") - } - rOpd := opdStack.Pop().(efp.Token) - lOpd := opdStack.Pop().(efp.Token) - lOpdVal, err := strconv.ParseFloat(lOpd.TValue, 64) - if err != nil { + if err := calcMultiply(opdStack); err != nil { return err } - rOpdVal, err := strconv.ParseFloat(rOpd.TValue, 64) - if err != nil { - return err - } - result := lOpdVal * rOpdVal - opdStack.Push(efp.Token{TValue: fmt.Sprintf("%g", result), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) } if opt.TValue == "/" { - if opdStack.Len() < 2 { - return errors.New("formula not valid") - } - rOpd := opdStack.Pop().(efp.Token) - lOpd := opdStack.Pop().(efp.Token) - lOpdVal, err := strconv.ParseFloat(lOpd.TValue, 64) - if err != nil { + if err := calcDivide(opdStack); err != nil { return err } - rOpdVal, err := strconv.ParseFloat(rOpd.TValue, 64) - if err != nil { - return err - } - result := lOpdVal / rOpdVal - if rOpdVal == 0 { - return errors.New(formulaErrorDIV) - } - opdStack.Push(efp.Token{TValue: fmt.Sprintf("%g", result), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) } return nil } +// parseOperatorPrefixToken parse operator prefix token. +func (f *File) parseOperatorPrefixToken(optStack, opdStack *Stack, token efp.Token) (err error) { + if optStack.Len() == 0 { + optStack.Push(token) + } else { + tokenPriority := getPriority(token) + topOpt := optStack.Peek().(efp.Token) + topOptPriority := getPriority(topOpt) + if tokenPriority > topOptPriority { + optStack.Push(token) + } else { + for tokenPriority <= topOptPriority { + optStack.Pop() + if err = calculate(opdStack, topOpt); err != nil { + return + } + if optStack.Len() > 0 { + topOpt = optStack.Peek().(efp.Token) + topOptPriority = getPriority(topOpt) + continue + } + break + } + optStack.Push(token) + } + } + return +} + +// isOperatorPrefixToken determine if the token is parse operator prefix +// token. +func isOperatorPrefixToken(token efp.Token) bool { + if (token.TValue == "-" && token.TType == efp.TokenTypeOperatorPrefix) || + token.TValue == "+" || token.TValue == "-" || token.TValue == "*" || token.TValue == "/" { + return true + } + return false +} + // parseToken parse basic arithmetic operator priority and evaluate based on // operators and operands. func (f *File) parseToken(sheet string, token efp.Token, opdStack, optStack *Stack) error { @@ -369,30 +441,9 @@ func (f *File) parseToken(sheet string, token efp.Token, opdStack, optStack *Sta token.TType = efp.TokenTypeOperand token.TSubType = efp.TokenSubTypeNumber } - if (token.TValue == "-" && token.TType == efp.TokenTypeOperatorPrefix) || token.TValue == "+" || token.TValue == "-" || token.TValue == "*" || token.TValue == "/" { - if optStack.Len() == 0 { - optStack.Push(token) - } else { - tokenPriority := getPriority(token) - topOpt := optStack.Peek().(efp.Token) - topOptPriority := getPriority(topOpt) - if tokenPriority > topOptPriority { - optStack.Push(token) - } else { - for tokenPriority <= topOptPriority { - optStack.Pop() - if err := calculate(opdStack, topOpt); err != nil { - return err - } - if optStack.Len() > 0 { - topOpt = optStack.Peek().(efp.Token) - topOptPriority = getPriority(topOpt) - continue - } - break - } - optStack.Push(token) - } + if isOperatorPrefixToken(token) { + if err := f.parseOperatorPrefixToken(optStack, opdStack, token); err != nil { + return err } } if token.TType == efp.TokenTypeSubexpression && token.TSubType == efp.TokenSubTypeStart { // ( @@ -461,11 +512,44 @@ func (f *File) parseReference(sheet, reference string) (result []string, matrix return } +// prepareValueRange prepare value range. +func prepareValueRange(cr cellRange, valueRange []int) { + if cr.From.Row < valueRange[0] { + valueRange[0] = cr.From.Row + } + if cr.From.Col < valueRange[2] { + valueRange[2] = cr.From.Col + } + if cr.To.Row > valueRange[0] { + valueRange[1] = cr.To.Row + } + if cr.To.Col > valueRange[3] { + valueRange[3] = cr.To.Col + } +} + +// prepareValueRef prepare value reference. +func prepareValueRef(cr cellRef, valueRange []int) { + if cr.Row < valueRange[0] { + valueRange[0] = cr.Row + } + if cr.Col < valueRange[2] { + valueRange[2] = cr.Col + } + if cr.Row > valueRange[0] { + valueRange[1] = cr.Row + } + if cr.Col > valueRange[3] { + valueRange[3] = cr.Col + } +} + // rangeResolver extract value as string from given reference and range list. -// This function will not ignore the empty cell. For example, -// A1:A2:A2:B3 will be reference A1:B3. +// This function will not ignore the empty cell. For example, A1:A2:A2:B3 will +// be reference A1:B3. func (f *File) rangeResolver(cellRefs, cellRanges *list.List) (result []string, matrix [][]string, err error) { - var fromRow, toRow, fromCol, toCol int = 1, 1, 1, 1 + // value range order: from row, to row, from column, to column + valueRange := []int{1, 1, 1, 1} var sheet string filter := map[string]string{} // prepare value range @@ -476,18 +560,7 @@ func (f *File) rangeResolver(cellRefs, cellRanges *list.List) (result []string, } rng := []int{cr.From.Col, cr.From.Row, cr.To.Col, cr.To.Row} sortCoordinates(rng) - if cr.From.Row < fromRow { - fromRow = cr.From.Row - } - if cr.From.Col < fromCol { - fromCol = cr.From.Col - } - if cr.To.Row > fromRow { - toRow = cr.To.Row - } - if cr.To.Col > toCol { - toCol = cr.To.Col - } + prepareValueRange(cr, valueRange) if cr.From.Sheet != "" { sheet = cr.From.Sheet } @@ -497,24 +570,13 @@ func (f *File) rangeResolver(cellRefs, cellRanges *list.List) (result []string, if cr.Sheet != "" { sheet = cr.Sheet } - if cr.Row < fromRow { - fromRow = cr.Row - } - if cr.Col < fromCol { - fromCol = cr.Col - } - if cr.Row > fromRow { - toRow = cr.Row - } - if cr.Col > toCol { - toCol = cr.Col - } + prepareValueRef(cr, valueRange) } // extract value from ranges if cellRanges.Len() > 0 { - for row := fromRow; row <= toRow; row++ { + for row := valueRange[0]; row <= valueRange[1]; row++ { var matrixRow = []string{} - for col := fromCol; col <= toCol; col++ { + for col := valueRange[2]; col <= valueRange[3]; col++ { var cell, value string if cell, err = CoordinatesToCellName(col, row); err != nil { return @@ -672,28 +734,15 @@ func (fn *formulaFuncs) ARABIC(argsList *list.List) (result string, err error) { err = errors.New("ARABIC requires 1 numeric argument") return } + charMap := map[rune]float64{'I': 1, 'V': 5, 'X': 10, 'L': 50, 'C': 100, 'D': 500, 'M': 1000} val, last, prefix := 0.0, 0.0, 1.0 for _, char := range argsList.Front().Value.(formulaArg).Value { digit := 0.0 - switch char { - case '-': + if char == '-' { prefix = -1 continue - case 'I': - digit = 1 - case 'V': - digit = 5 - case 'X': - digit = 10 - case 'L': - digit = 50 - case 'C': - digit = 100 - case 'D': - digit = 500 - case 'M': - digit = 1000 } + digit, _ = charMap[char] val += digit switch { case last == digit && (last == 5 || last == 50 || last == 500): @@ -850,7 +899,7 @@ func (fn *formulaFuncs) BASE(argsList *list.List) (result string, err error) { return } if radix < 2 || radix > 36 { - err = errors.New("radix must be an integer ≥ 2 and ≤ 36") + err = errors.New("radix must be an integer >= 2 and <= 36") return } if argsList.Len() > 2 { diff --git a/calc_test.go b/calc_test.go index 6d0f853..fc107cb 100644 --- a/calc_test.go +++ b/calc_test.go @@ -381,7 +381,7 @@ func TestCalcCellValue(t *testing.T) { // BASE "=BASE()": "BASE requires at least 2 arguments", "=BASE(1,2,3,4)": "BASE allows at most 3 arguments", - "=BASE(1,1)": "radix must be an integer ≥ 2 and ≤ 36", + "=BASE(1,1)": "radix must be an integer >= 2 and <= 36", // CEILING "=CEILING()": "CEILING requires at least 1 argument", "=CEILING(1,2,3)": "CEILING allows at most 2 arguments", diff --git a/cell.go b/cell.go index a69f4d9..63db194 100644 --- a/cell.go +++ b/cell.go @@ -730,9 +730,9 @@ func (f *File) formattedValue(s int, v string) string { return v } styleSheet := f.stylesReader() - ok := builtInNumFmtFunc[styleSheet.CellXfs.Xf[s].NumFmtID] + ok := builtInNumFmtFunc[*styleSheet.CellXfs.Xf[s].NumFmtID] if ok != nil { - return ok(styleSheet.CellXfs.Xf[s].NumFmtID, v) + return ok(*styleSheet.CellXfs.Xf[s].NumFmtID, v) } return v } diff --git a/go.mod b/go.mod index 420c64e..e411f19 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 github.com/stretchr/testify v1.3.0 + github.com/xuri/efp v0.0.0-20191019043341-b7dc4fe9aa91 golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 golang.org/x/text v0.3.2 // indirect diff --git a/go.sum b/go.sum index 54492ac..ff969f0 100644 --- a/go.sum +++ b/go.sum @@ -9,6 +9,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/xuri/efp v0.0.0-20191019043341-b7dc4fe9aa91 h1:gp02YctZuIPTk0t7qI+wvg3VQwTPyNmSGG6ZqOsjSL8= +github.com/xuri/efp v0.0.0-20191019043341-b7dc4fe9aa91/go.mod h1:uBiSUepVYMhGTfDeBKKasV4GpgBlzJ46gXUBAqV8qLk= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a h1:gHevYm0pO4QUbwy8Dmdr01R5r1BuKtfYqRqF0h/Cbh0= golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= diff --git a/styles.go b/styles.go index 61b8e53..72b2071 100644 --- a/styles.go +++ b/styles.go @@ -2299,21 +2299,21 @@ func setBorders(style *Style) *xlsxBorder { // cell. func setCellXfs(style *xlsxStyleSheet, fontID, numFmtID, fillID, borderID int, applyAlignment, applyProtection bool, alignment *xlsxAlignment, protection *xlsxProtection) int { var xf xlsxXf - xf.FontID = fontID + xf.FontID = intPtr(fontID) if fontID != 0 { - xf.ApplyFont = true + xf.ApplyFont = boolPtr(true) } - xf.NumFmtID = numFmtID + xf.NumFmtID = intPtr(numFmtID) if numFmtID != 0 { - xf.ApplyNumberFormat = true + xf.ApplyNumberFormat = boolPtr(true) } - xf.FillID = fillID - xf.BorderID = borderID + xf.FillID = intPtr(fillID) + xf.BorderID = intPtr(borderID) style.CellXfs.Count++ xf.Alignment = alignment - xf.ApplyAlignment = applyAlignment + xf.ApplyAlignment = boolPtr(applyAlignment) if applyProtection { - xf.ApplyProtection = applyProtection + xf.ApplyProtection = boolPtr(applyProtection) xf.Protection = protection } xfID := 0 diff --git a/styles_test.go b/styles_test.go index 5681c95..1ff0e4e 100644 --- a/styles_test.go +++ b/styles_test.go @@ -33,9 +33,9 @@ func TestStyleFill(t *testing.T) { styles := xl.stylesReader() style := styles.CellXfs.Xf[styleID] if testCase.expectFill { - assert.NotEqual(t, style.FillID, 0, testCase.label) + assert.NotEqual(t, *style.FillID, 0, testCase.label) } else { - assert.Equal(t, style.FillID, 0, testCase.label) + assert.Equal(t, *style.FillID, 0, testCase.label) } } } @@ -188,7 +188,7 @@ func TestNewStyle(t *testing.T) { assert.NoError(t, err) styles := f.stylesReader() fontID := styles.CellXfs.Xf[styleID].FontID - font := styles.Fonts.Font[fontID] + font := styles.Fonts.Font[*fontID] assert.Contains(t, *font.Name.Val, "Times New Roman", "Stored font should contain font name") assert.Equal(t, 2, styles.CellXfs.Count, "Should have 2 styles") _, err = f.NewStyle(&Style{}) diff --git a/xmlStyles.go b/xmlStyles.go index b5ec41d..42d535b 100644 --- a/xmlStyles.go +++ b/xmlStyles.go @@ -209,19 +209,19 @@ type xlsxCellStyleXfs struct { // xlsxXf directly maps the xf element. A single xf element describes all of the // formatting for a cell. type xlsxXf struct { - NumFmtID int `xml:"numFmtId,attr,omitempty"` - FontID int `xml:"fontId,attr,omitempty"` - FillID int `xml:"fillId,attr,omitempty"` - BorderID int `xml:"borderId,attr,omitempty"` - XfID *int `xml:"xfId,attr,omitempty"` - QuotePrefix bool `xml:"quotePrefix,attr,omitempty"` - PivotButton bool `xml:"pivotButton,attr,omitempty"` - ApplyNumberFormat bool `xml:"applyNumberFormat,attr,omitempty"` - ApplyFont bool `xml:"applyFont,attr,omitempty"` - ApplyFill bool `xml:"applyFill,attr,omitempty"` - ApplyBorder bool `xml:"applyBorder,attr,omitempty"` - ApplyAlignment bool `xml:"applyAlignment,attr,omitempty"` - ApplyProtection bool `xml:"applyProtection,attr,omitempty"` + NumFmtID *int `xml:"numFmtId,attr"` + FontID *int `xml:"fontId,attr"` + FillID *int `xml:"fillId,attr"` + BorderID *int `xml:"borderId,attr"` + XfID *int `xml:"xfId,attr"` + QuotePrefix *bool `xml:"quotePrefix,attr"` + PivotButton *bool `xml:"pivotButton,attr"` + ApplyNumberFormat *bool `xml:"applyNumberFormat,attr"` + ApplyFont *bool `xml:"applyFont,attr"` + ApplyFill *bool `xml:"applyFill,attr"` + ApplyBorder *bool `xml:"applyBorder,attr"` + ApplyAlignment *bool `xml:"applyAlignment,attr"` + ApplyProtection *bool `xml:"applyProtection,attr"` Alignment *xlsxAlignment `xml:"alignment"` Protection *xlsxProtection `xml:"protection"` } -- cgit v1.2.1 From 0feb819d4c08ab52e806214b23d673001bd9fe3e Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 11 May 2020 00:01:22 +0800 Subject: updated test and go.mod --- calc.go | 141 +++++++++++++++++++++----- calc_test.go | 317 +++++++++++++++++++++++++++++++++++++++++------------------ go.mod | 10 +- go.sum | 31 ++++-- 4 files changed, 368 insertions(+), 131 deletions(-) diff --git a/calc.go b/calc.go index 3d4a8be..61b6dac 100644 --- a/calc.go +++ b/calc.go @@ -644,6 +644,7 @@ func (fn *formulaFuncs) ABS(argsList *list.List) (result string, err error) { } var val float64 if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } result = fmt.Sprintf("%g", math.Abs(val)) @@ -663,6 +664,7 @@ func (fn *formulaFuncs) ACOS(argsList *list.List) (result string, err error) { } var val float64 if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } result = fmt.Sprintf("%g", math.Acos(val)) @@ -681,6 +683,7 @@ func (fn *formulaFuncs) ACOSH(argsList *list.List) (result string, err error) { } var val float64 if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } result = fmt.Sprintf("%g", math.Acosh(val)) @@ -700,6 +703,7 @@ func (fn *formulaFuncs) ACOT(argsList *list.List) (result string, err error) { } var val float64 if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } result = fmt.Sprintf("%g", math.Pi/2-math.Atan(val)) @@ -718,6 +722,7 @@ func (fn *formulaFuncs) ACOTH(argsList *list.List) (result string, err error) { } var val float64 if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } result = fmt.Sprintf("%g", math.Atanh(1/val)) @@ -774,6 +779,7 @@ func (fn *formulaFuncs) ASIN(argsList *list.List) (result string, err error) { } var val float64 if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } result = fmt.Sprintf("%g", math.Asin(val)) @@ -792,6 +798,7 @@ func (fn *formulaFuncs) ASINH(argsList *list.List) (result string, err error) { } var val float64 if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } result = fmt.Sprintf("%g", math.Asinh(val)) @@ -811,6 +818,7 @@ func (fn *formulaFuncs) ATAN(argsList *list.List) (result string, err error) { } var val float64 if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } result = fmt.Sprintf("%g", math.Atan(val)) @@ -829,6 +837,7 @@ func (fn *formulaFuncs) ATANH(argsList *list.List) (result string, err error) { } var val float64 if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } result = fmt.Sprintf("%g", math.Atanh(val)) @@ -848,34 +857,17 @@ func (fn *formulaFuncs) ATAN2(argsList *list.List) (result string, err error) { } var x, y float64 if x, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } if y, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } result = fmt.Sprintf("%g", math.Atan2(x, y)) return } -// gcd returns the greatest common divisor of two supplied integers. -func gcd(x, y float64) float64 { - x, y = math.Trunc(x), math.Trunc(y) - if x == 0 { - return y - } - if y == 0 { - return x - } - for x != y { - if x > y { - x = x - y - } else { - y = y - x - } - } - return x -} - // BASE function converts a number into a supplied base (radix), and returns a // text representation of the calculated value. The syntax of the function is: // @@ -893,9 +885,11 @@ func (fn *formulaFuncs) BASE(argsList *list.List) (result string, err error) { var number float64 var radix, minLength int if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } if radix, err = strconv.Atoi(argsList.Front().Next().Value.(formulaArg).Value); err != nil { + err = errors.New(formulaErrorVALUE) return } if radix < 2 || radix > 36 { @@ -904,6 +898,7 @@ func (fn *formulaFuncs) BASE(argsList *list.List) (result string, err error) { } if argsList.Len() > 2 { if minLength, err = strconv.Atoi(argsList.Back().Value.(formulaArg).Value); err != nil { + err = errors.New(formulaErrorVALUE) return } } @@ -931,6 +926,7 @@ func (fn *formulaFuncs) CEILING(argsList *list.List) (result string, err error) } number, significance, res := 0.0, 1.0, 0.0 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } if number < 0 { @@ -938,6 +934,7 @@ func (fn *formulaFuncs) CEILING(argsList *list.List) (result string, err error) } if argsList.Len() > 1 { if significance, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } } @@ -973,6 +970,7 @@ func (fn *formulaFuncs) CEILINGMATH(argsList *list.List) (result string, err err } number, significance, mode := 0.0, 1.0, 1.0 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } if number < 0 { @@ -980,6 +978,7 @@ func (fn *formulaFuncs) CEILINGMATH(argsList *list.List) (result string, err err } if argsList.Len() > 1 { if significance, err = strconv.ParseFloat(argsList.Front().Next().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } } @@ -989,6 +988,7 @@ func (fn *formulaFuncs) CEILINGMATH(argsList *list.List) (result string, err err } if argsList.Len() > 2 { if mode, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } } @@ -1021,6 +1021,7 @@ func (fn *formulaFuncs) CEILINGPRECISE(argsList *list.List) (result string, err } number, significance := 0.0, 1.0 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } if number < 0 { @@ -1032,6 +1033,7 @@ func (fn *formulaFuncs) CEILINGPRECISE(argsList *list.List) (result string, err } if argsList.Len() > 1 { if significance, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } significance = math.Abs(significance) @@ -1062,9 +1064,11 @@ func (fn *formulaFuncs) COMBIN(argsList *list.List) (result string, err error) { } number, chosen, val := 0.0, 0.0, 1.0 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } if chosen, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } number, chosen = math.Trunc(number), math.Trunc(chosen) @@ -1095,9 +1099,11 @@ func (fn *formulaFuncs) COMBINA(argsList *list.List) (result string, err error) } var number, chosen float64 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } if chosen, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } number, chosen = math.Trunc(number), math.Trunc(chosen) @@ -1131,6 +1137,7 @@ func (fn *formulaFuncs) COS(argsList *list.List) (result string, err error) { } var val float64 if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } result = fmt.Sprintf("%g", math.Cos(val)) @@ -1149,6 +1156,7 @@ func (fn *formulaFuncs) COSH(argsList *list.List) (result string, err error) { } var val float64 if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } result = fmt.Sprintf("%g", math.Cosh(val)) @@ -1167,10 +1175,11 @@ func (fn *formulaFuncs) COT(argsList *list.List) (result string, err error) { } var val float64 if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } if val == 0 { - err = errors.New(formulaErrorNAME) + err = errors.New(formulaErrorDIV) return } result = fmt.Sprintf("%g", math.Tan(val)) @@ -1189,10 +1198,11 @@ func (fn *formulaFuncs) COTH(argsList *list.List) (result string, err error) { } var val float64 if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } if val == 0 { - err = errors.New(formulaErrorNAME) + err = errors.New(formulaErrorDIV) return } result = fmt.Sprintf("%g", math.Tanh(val)) @@ -1211,10 +1221,11 @@ func (fn *formulaFuncs) CSC(argsList *list.List) (result string, err error) { } var val float64 if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } if val == 0 { - err = errors.New(formulaErrorNAME) + err = errors.New(formulaErrorDIV) return } result = fmt.Sprintf("%g", 1/math.Sin(val)) @@ -1233,10 +1244,11 @@ func (fn *formulaFuncs) CSCH(argsList *list.List) (result string, err error) { } var val float64 if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } if val == 0 { - err = errors.New(formulaErrorNAME) + err = errors.New(formulaErrorDIV) return } result = fmt.Sprintf("%g", 1/math.Sinh(val)) @@ -1256,6 +1268,7 @@ func (fn *formulaFuncs) DECIMAL(argsList *list.List) (result string, err error) var text = argsList.Front().Value.(formulaArg).Value var radix int if radix, err = strconv.Atoi(argsList.Back().Value.(formulaArg).Value); err != nil { + err = errors.New(formulaErrorVALUE) return } if len(text) > 2 && (strings.HasPrefix(text, "0x") || strings.HasPrefix(text, "0X")) { @@ -1263,7 +1276,7 @@ func (fn *formulaFuncs) DECIMAL(argsList *list.List) (result string, err error) } val, err := strconv.ParseInt(text, radix, 64) if err != nil { - err = errors.New(formulaErrorNUM) + err = errors.New(formulaErrorVALUE) return } result = fmt.Sprintf("%g", float64(val)) @@ -1282,10 +1295,11 @@ func (fn *formulaFuncs) DEGREES(argsList *list.List) (result string, err error) } var val float64 if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } if val == 0 { - err = errors.New(formulaErrorNAME) + err = errors.New(formulaErrorDIV) return } result = fmt.Sprintf("%g", 180.0/math.Pi*val) @@ -1305,6 +1319,7 @@ func (fn *formulaFuncs) EVEN(argsList *list.List) (result string, err error) { } var number float64 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } sign := math.Signbit(number) @@ -1333,6 +1348,7 @@ func (fn *formulaFuncs) EXP(argsList *list.List) (result string, err error) { } var number float64 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } result = strings.ToUpper(fmt.Sprintf("%g", math.Exp(number))) @@ -1360,6 +1376,7 @@ func (fn *formulaFuncs) FACT(argsList *list.List) (result string, err error) { } var number float64 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } if number < 0 { @@ -1381,10 +1398,12 @@ func (fn *formulaFuncs) FACTDOUBLE(argsList *list.List) (result string, err erro } number, val := 0.0, 1.0 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } if number < 0 { err = errors.New(formulaErrorNUM) + return } for i := math.Trunc(number); i > 1; i -= 2 { val *= i @@ -1405,13 +1424,16 @@ func (fn *formulaFuncs) FLOOR(argsList *list.List) (result string, err error) { } var number, significance float64 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } if significance, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } if significance < 0 && number >= 0 { err = errors.New(formulaErrorNUM) + return } val := number val, res := math.Modf(val / significance) @@ -1440,6 +1462,7 @@ func (fn *formulaFuncs) FLOORMATH(argsList *list.List) (result string, err error } number, significance, mode := 0.0, 1.0, 1.0 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } if number < 0 { @@ -1447,6 +1470,7 @@ func (fn *formulaFuncs) FLOORMATH(argsList *list.List) (result string, err error } if argsList.Len() > 1 { if significance, err = strconv.ParseFloat(argsList.Front().Next().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } } @@ -1456,6 +1480,7 @@ func (fn *formulaFuncs) FLOORMATH(argsList *list.List) (result string, err error } if argsList.Len() > 2 { if mode, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } } @@ -1483,6 +1508,7 @@ func (fn *formulaFuncs) FLOORPRECISE(argsList *list.List) (result string, err er } var number, significance float64 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } if number < 0 { @@ -1494,6 +1520,7 @@ func (fn *formulaFuncs) FLOORPRECISE(argsList *list.List) (result string, err er } if argsList.Len() > 1 { if significance, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } significance = math.Abs(significance) @@ -1512,6 +1539,25 @@ func (fn *formulaFuncs) FLOORPRECISE(argsList *list.List) (result string, err er return } +// gcd returns the greatest common divisor of two supplied integers. +func gcd(x, y float64) float64 { + x, y = math.Trunc(x), math.Trunc(y) + if x == 0 { + return y + } + if y == 0 { + return x + } + for x != y { + if x > y { + x = x - y + } else { + y = y - x + } + } + return x +} + // GCD function returns the greatest common divisor of two or more supplied // integers. The syntax of the function is: // @@ -1532,6 +1578,7 @@ func (fn *formulaFuncs) GCD(argsList *list.List) (result string, err error) { continue } if val, err = strconv.ParseFloat(token, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } nums = append(nums, val) @@ -1568,6 +1615,7 @@ func (fn *formulaFuncs) INT(argsList *list.List) (result string, err error) { } var number float64 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } val, frac := math.Modf(number) @@ -1595,6 +1643,7 @@ func (fn *formulaFuncs) ISOCEILING(argsList *list.List) (result string, err erro } var number, significance float64 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } if number < 0 { @@ -1606,6 +1655,7 @@ func (fn *formulaFuncs) ISOCEILING(argsList *list.List) (result string, err erro } if argsList.Len() > 1 { if significance, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } significance = math.Abs(significance) @@ -1654,6 +1704,7 @@ func (fn *formulaFuncs) LCM(argsList *list.List) (result string, err error) { continue } if val, err = strconv.ParseFloat(token, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } nums = append(nums, val) @@ -1690,6 +1741,7 @@ func (fn *formulaFuncs) LN(argsList *list.List) (result string, err error) { } var number float64 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } result = fmt.Sprintf("%g", math.Log(number)) @@ -1712,10 +1764,12 @@ func (fn *formulaFuncs) LOG(argsList *list.List) (result string, err error) { } number, base := 0.0, 10.0 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } if argsList.Len() > 1 { if base, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } } @@ -1747,6 +1801,7 @@ func (fn *formulaFuncs) LOG10(argsList *list.List) (result string, err error) { } var number float64 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } result = fmt.Sprintf("%g", math.Log10(number)) @@ -1835,9 +1890,11 @@ func (fn *formulaFuncs) MOD(argsList *list.List) (result string, err error) { } var number, divisor float64 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } if divisor, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } if divisor == 0 { @@ -1864,9 +1921,11 @@ func (fn *formulaFuncs) MROUND(argsList *list.List) (result string, err error) { } var number, multiple float64 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } if multiple, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } if multiple == 0 { @@ -1900,6 +1959,7 @@ func (fn *formulaFuncs) MULTINOMIAL(argsList *list.List) (result string, err err continue } if val, err = strconv.ParseFloat(token.Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } num += val @@ -1921,6 +1981,7 @@ func (fn *formulaFuncs) MUNIT(argsList *list.List) (result string, err error) { } var dimension int if dimension, err = strconv.Atoi(argsList.Front().Value.(formulaArg).Value); err != nil { + err = errors.New(formulaErrorVALUE) return } matrix := make([][]float64, 0, dimension) @@ -1951,6 +2012,7 @@ func (fn *formulaFuncs) ODD(argsList *list.List) (result string, err error) { } var number float64 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } if number == 0 { @@ -1997,9 +2059,11 @@ func (fn *formulaFuncs) POWER(argsList *list.List) (result string, err error) { } var x, y float64 if x, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } if y, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } if x == 0 && y == 0 { @@ -2027,6 +2091,7 @@ func (fn *formulaFuncs) PRODUCT(argsList *list.List) (result string, err error) continue } if val, err = strconv.ParseFloat(token.Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } product = product * val @@ -2047,9 +2112,11 @@ func (fn *formulaFuncs) QUOTIENT(argsList *list.List) (result string, err error) } var x, y float64 if x, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } if y, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } if y == 0 { @@ -2071,6 +2138,7 @@ func (fn *formulaFuncs) RADIANS(argsList *list.List) (result string, err error) } var angle float64 if angle, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } result = fmt.Sprintf("%g", math.Pi/180.0*angle) @@ -2103,9 +2171,11 @@ func (fn *formulaFuncs) RANDBETWEEN(argsList *list.List) (result string, err err } var bottom, top int64 if bottom, err = strconv.ParseInt(argsList.Front().Value.(formulaArg).Value, 10, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } if top, err = strconv.ParseInt(argsList.Back().Value.(formulaArg).Value, 10, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } if top < bottom { @@ -2148,10 +2218,12 @@ func (fn *formulaFuncs) ROMAN(argsList *list.List) (result string, err error) { var number float64 var form int if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } if argsList.Len() > 1 { if form, err = strconv.Atoi(argsList.Back().Value.(formulaArg).Value); err != nil { + err = errors.New(formulaErrorVALUE) return } if form < 0 { @@ -2231,9 +2303,11 @@ func (fn *formulaFuncs) ROUND(argsList *list.List) (result string, err error) { } var number, digits float64 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } if digits, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } result = fmt.Sprintf("%g", fn.round(number, digits, closest)) @@ -2252,9 +2326,11 @@ func (fn *formulaFuncs) ROUNDDOWN(argsList *list.List) (result string, err error } var number, digits float64 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } if digits, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } result = fmt.Sprintf("%g", fn.round(number, digits, down)) @@ -2273,9 +2349,11 @@ func (fn *formulaFuncs) ROUNDUP(argsList *list.List) (result string, err error) } var number, digits float64 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } if digits, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } result = fmt.Sprintf("%g", fn.round(number, digits, up)) @@ -2294,6 +2372,7 @@ func (fn *formulaFuncs) SEC(argsList *list.List) (result string, err error) { } var number float64 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } result = fmt.Sprintf("%g", math.Cos(number)) @@ -2312,6 +2391,7 @@ func (fn *formulaFuncs) SECH(argsList *list.List) (result string, err error) { } var number float64 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } result = fmt.Sprintf("%g", 1/math.Cosh(number)) @@ -2332,6 +2412,7 @@ func (fn *formulaFuncs) SIGN(argsList *list.List) (result string, err error) { } var val float64 if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } if val < 0 { @@ -2358,6 +2439,7 @@ func (fn *formulaFuncs) SIN(argsList *list.List) (result string, err error) { } var number float64 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } result = fmt.Sprintf("%g", math.Sin(number)) @@ -2376,6 +2458,7 @@ func (fn *formulaFuncs) SINH(argsList *list.List) (result string, err error) { } var number float64 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } result = fmt.Sprintf("%g", math.Sinh(number)) @@ -2399,6 +2482,7 @@ func (fn *formulaFuncs) SQRT(argsList *list.List) (result string, err error) { return } if res, err = strconv.ParseFloat(value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } if res < 0 { @@ -2421,6 +2505,7 @@ func (fn *formulaFuncs) SQRTPI(argsList *list.List) (result string, err error) { } var number float64 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } result = fmt.Sprintf("%g", math.Sqrt(number*math.Pi)) @@ -2440,6 +2525,7 @@ func (fn *formulaFuncs) SUM(argsList *list.List) (result string, err error) { continue } if val, err = strconv.ParseFloat(token.Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } sum += val @@ -2461,6 +2547,7 @@ func (fn *formulaFuncs) SUMSQ(argsList *list.List) (result string, err error) { continue } if val, err = strconv.ParseFloat(token.Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } sq += val * val @@ -2481,6 +2568,7 @@ func (fn *formulaFuncs) TAN(argsList *list.List) (result string, err error) { } var number float64 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } result = fmt.Sprintf("%g", math.Tan(number)) @@ -2499,6 +2587,7 @@ func (fn *formulaFuncs) TANH(argsList *list.List) (result string, err error) { } var number float64 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } result = fmt.Sprintf("%g", math.Tanh(number)) @@ -2517,10 +2606,12 @@ func (fn *formulaFuncs) TRUNC(argsList *list.List) (result string, err error) { } var number, digits, adjust, rtrim float64 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } if argsList.Len() > 1 { if digits, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } digits = math.Floor(digits) diff --git a/calc_test.go b/calc_test.go index fc107cb..7592078 100644 --- a/calc_test.go +++ b/calc_test.go @@ -77,13 +77,16 @@ func TestCalcCellValue(t *testing.T) { "=CEILING(-22.25,-0.1)": "-22.3", "=CEILING(-22.25,-1)": "-23", "=CEILING(-22.25,-5)": "-25", + "=CEILING(22.25)": "23", // _xlfn.CEILING.MATH - "=_xlfn.CEILING.MATH(15.25,1)": "16", - "=_xlfn.CEILING.MATH(15.25,0.1)": "15.3", - "=_xlfn.CEILING.MATH(15.25,5)": "20", - "=_xlfn.CEILING.MATH(-15.25,1)": "-15", - "=_xlfn.CEILING.MATH(-15.25,1,1)": "-15", // should be 16 - "=_xlfn.CEILING.MATH(-15.25,10)": "-10", + "=_xlfn.CEILING.MATH(15.25,1)": "16", + "=_xlfn.CEILING.MATH(15.25,0.1)": "15.3", + "=_xlfn.CEILING.MATH(15.25,5)": "20", + "=_xlfn.CEILING.MATH(-15.25,1)": "-15", + "=_xlfn.CEILING.MATH(-15.25,1,1)": "-15", // should be 16 + "=_xlfn.CEILING.MATH(-15.25,10)": "-10", + "=_xlfn.CEILING.MATH(-15.25)": "-15", + "=_xlfn.CEILING.MATH(-15.25,-5,-1)": "-10", // _xlfn.CEILING.PRECISE "=_xlfn.CEILING.PRECISE(22.25,0.1)": "22.3", "=_xlfn.CEILING.PRECISE(22.25,0.5)": "22.5", @@ -101,6 +104,7 @@ func TestCalcCellValue(t *testing.T) { "=COMBIN(6,4)": "15", "=COMBIN(6,5)": "6", "=COMBIN(6,6)": "1", + "=COMBIN(0,0)": "1", // _xlfn.COMBINA "=_xlfn.COMBINA(6,1)": "6", "=_xlfn.COMBINA(6,2)": "21", @@ -108,6 +112,7 @@ func TestCalcCellValue(t *testing.T) { "=_xlfn.COMBINA(6,4)": "126", "=_xlfn.COMBINA(6,5)": "252", "=_xlfn.COMBINA(6,6)": "462", + "=_xlfn.COMBINA(0,0)": "0", // COS "=COS(0.785398163)": "0.707106781467586", "=COS(0)": "1", @@ -125,10 +130,11 @@ func TestCalcCellValue(t *testing.T) { // _xlfn.CSCH "=_xlfn.CSCH(-3.14159265358979)": "-0.08658953753004724", // _xlfn.DECIMAL - `=_xlfn.DECIMAL("1100",2)`: "12", - `=_xlfn.DECIMAL("186A0",16)`: "100000", - `=_xlfn.DECIMAL("31L0",32)`: "100000", - `=_xlfn.DECIMAL("70122",8)`: "28754", + `=_xlfn.DECIMAL("1100",2)`: "12", + `=_xlfn.DECIMAL("186A0",16)`: "100000", + `=_xlfn.DECIMAL("31L0",32)`: "100000", + `=_xlfn.DECIMAL("70122",8)`: "28754", + `=_xlfn.DECIMAL("0x70122",8)`: "28754", // DEGREES "=DEGREES(1)": "57.29577951308232", "=DEGREES(2.5)": "143.2394487827058", @@ -181,6 +187,9 @@ func TestCalcCellValue(t *testing.T) { "=_xlfn.FLOOR.PRECISE(-26.75,-1)": "-27", "=_xlfn.FLOOR.PRECISE(-26.75,-5)": "-30", // GCD + "=GCD(0)": "0", + `=GCD("",1)`: "1", + "=GCD(1,0)": "1", "=GCD(1,5)": "1", "=GCD(15,10,25)": "5", "=GCD(0,8,12)": "4", @@ -199,11 +208,15 @@ func TestCalcCellValue(t *testing.T) { "=ISO.CEILING(-22.25,1)": "-22", "=ISO.CEILING(-22.25,0.1)": "-22.200000000000003", "=ISO.CEILING(-22.25,5)": "-20", + "=ISO.CEILING(-22.25,0)": "0", // LCM "=LCM(1,5)": "5", "=LCM(15,10,25)": "150", "=LCM(1,8,12)": "24", "=LCM(7,2)": "14", + "=LCM(7)": "7", + `=LCM("",1)`: "1", + `=LCM(0,0)`: "0", // LN "=LN(1)": "0", "=LN(100)": "4.605170185988092", @@ -219,10 +232,11 @@ func TestCalcCellValue(t *testing.T) { "=LOG10(0.001)": "-3", "=LOG10(25)": "1.3979400086720375", // MOD - "=MOD(6,4)": "2", - "=MOD(6,3)": "0", - "=MOD(6,2.5)": "1", - "=MOD(6,1.333)": "0.6680000000000001", + "=MOD(6,4)": "2", + "=MOD(6,3)": "0", + "=MOD(6,2.5)": "1", + "=MOD(6,1.333)": "0.6680000000000001", + "=MOD(-10.23,1)": "0.7699999999999996", // MROUND "=MROUND(333.7,0.5)": "333.5", "=MROUND(333.8,1)": "334", @@ -233,7 +247,8 @@ func TestCalcCellValue(t *testing.T) { "=MROUND(-555.4,-1)": "-555", "=MROUND(-1555,-1000)": "-2000", // MULTINOMIAL - "=MULTINOMIAL(3,1,2,5)": "27720", + "=MULTINOMIAL(3,1,2,5)": "27720", + `=MULTINOMIAL("",3,1,2,5)`: "27720", // _xlfn.MUNIT "=_xlfn.MUNIT(4)": "", // not support currently // ODD @@ -249,7 +264,8 @@ func TestCalcCellValue(t *testing.T) { // POWER "=POWER(4,2)": "16", // PRODUCT - "=PRODUCT(3,6)": "18", + "=PRODUCT(3,6)": "18", + `=PRODUCT("",3,6)`: "18", // QUOTIENT "=QUOTIENT(5,2)": "2", "=QUOTIENT(4.5,3.1)": "1", @@ -260,12 +276,14 @@ func TestCalcCellValue(t *testing.T) { "=RADIANS(180)": "3.141592653589793", "=RADIANS(360)": "6.283185307179586", // ROMAN - "=ROMAN(499,0)": "CDXCIX", - "=ROMAN(1999,0)": "MCMXCIX", - "=ROMAN(1999,1)": "MLMVLIV", - "=ROMAN(1999,2)": "MXMIX", - "=ROMAN(1999,3)": "MVMIV", - "=ROMAN(1999,4)": "MIM", + "=ROMAN(499,0)": "CDXCIX", + "=ROMAN(1999,0)": "MCMXCIX", + "=ROMAN(1999,1)": "MLMVLIV", + "=ROMAN(1999,2)": "MXMIX", + "=ROMAN(1999,3)": "MVMIV", + "=ROMAN(1999,4)": "MIM", + "=ROMAN(1999,-1)": "MCMXCIX", + "=ROMAN(1999,5)": "MIM", // ROUND "=ROUND(100.319,1)": "100.30000000000001", "=ROUND(5.28,1)": "5.300000000000001", @@ -317,6 +335,7 @@ func TestCalcCellValue(t *testing.T) { "=SQRTPI(0)": "0", // SUM "=SUM(1,2)": "3", + `=SUM("",1,2)`: "3", "=SUM(1,2+3)": "6", "=SUM(SUM(1,2),2)": "5", "=(-2-SUM(-4+7))*5": "-25", @@ -330,8 +349,9 @@ func TestCalcCellValue(t *testing.T) { "=1+SUM(SUM(1,2*3),4)*-4/2+5+(4+2)*3": "2", "=1+SUM(SUM(1,2*3),4)*4/3+5+(4+2)*3": "38.666666666666664", // SUMSQ - "=SUMSQ(A1:A4)": "14", - "=SUMSQ(A1,B1,A2,B2,6)": "82", + "=SUMSQ(A1:A4)": "14", + "=SUMSQ(A1,B1,A2,B2,6)": "82", + `=SUMSQ("",A1,B1,A2,B2,6)`: "82", // TAN "=TAN(1.047197551)": "1.732050806782486", "=TAN(0)": "0", @@ -356,173 +376,269 @@ func TestCalcCellValue(t *testing.T) { } mathCalcError := map[string]string{ // ABS - "=ABS()": "ABS requires 1 numeric argument", - "=ABS(~)": `cannot convert cell "~" to coordinates: invalid cell name "~"`, + "=ABS()": "ABS requires 1 numeric argument", + `=ABS("X")`: "#VALUE!", + "=ABS(~)": `cannot convert cell "~" to coordinates: invalid cell name "~"`, // ACOS - "=ACOS()": "ACOS requires 1 numeric argument", + "=ACOS()": "ACOS requires 1 numeric argument", + `=ACOS("X")`: "#VALUE!", // ACOSH - "=ACOSH()": "ACOSH requires 1 numeric argument", + "=ACOSH()": "ACOSH requires 1 numeric argument", + `=ACOSH("X")`: "#VALUE!", // _xlfn.ACOT - "=_xlfn.ACOT()": "ACOT requires 1 numeric argument", + "=_xlfn.ACOT()": "ACOT requires 1 numeric argument", + `=_xlfn.ACOT("X")`: "#VALUE!", // _xlfn.ACOTH - "=_xlfn.ACOTH()": "ACOTH requires 1 numeric argument", + "=_xlfn.ACOTH()": "ACOTH requires 1 numeric argument", + `=_xlfn.ACOTH("X")`: "#VALUE!", // _xlfn.ARABIC "=_xlfn.ARABIC()": "ARABIC requires 1 numeric argument", // ASIN - "=ASIN()": "ASIN requires 1 numeric argument", + "=ASIN()": "ASIN requires 1 numeric argument", + `=ASIN("X")`: "#VALUE!", // ASINH - "=ASINH()": "ASINH requires 1 numeric argument", + "=ASINH()": "ASINH requires 1 numeric argument", + `=ASINH("X")`: "#VALUE!", // ATAN - "=ATAN()": "ATAN requires 1 numeric argument", + "=ATAN()": "ATAN requires 1 numeric argument", + `=ATAN("X")`: "#VALUE!", // ATANH - "=ATANH()": "ATANH requires 1 numeric argument", + "=ATANH()": "ATANH requires 1 numeric argument", + `=ATANH("X")`: "#VALUE!", // ATAN2 - "=ATAN2()": "ATAN2 requires 2 numeric arguments", + "=ATAN2()": "ATAN2 requires 2 numeric arguments", + `=ATAN2("X",0)`: "#VALUE!", + `=ATAN2(0,"X")`: "#VALUE!", // BASE "=BASE()": "BASE requires at least 2 arguments", "=BASE(1,2,3,4)": "BASE allows at most 3 arguments", "=BASE(1,1)": "radix must be an integer >= 2 and <= 36", + `=BASE("X",2)`: "#VALUE!", + `=BASE(1,"X")`: "#VALUE!", + `=BASE(1,2,"X")`: "#VALUE!", // CEILING "=CEILING()": "CEILING requires at least 1 argument", "=CEILING(1,2,3)": "CEILING allows at most 2 arguments", "=CEILING(1,-1)": "negative sig to CEILING invalid", + `=CEILING("X",0)`: "#VALUE!", + `=CEILING(0,"X")`: "#VALUE!", // _xlfn.CEILING.MATH "=_xlfn.CEILING.MATH()": "CEILING.MATH requires at least 1 argument", "=_xlfn.CEILING.MATH(1,2,3,4)": "CEILING.MATH allows at most 3 arguments", + `=_xlfn.CEILING.MATH("X")`: "#VALUE!", + `=_xlfn.CEILING.MATH(1,"X")`: "#VALUE!", + `=_xlfn.CEILING.MATH(1,2,"X")`: "#VALUE!", // _xlfn.CEILING.PRECISE "=_xlfn.CEILING.PRECISE()": "CEILING.PRECISE requires at least 1 argument", "=_xlfn.CEILING.PRECISE(1,2,3)": "CEILING.PRECISE allows at most 2 arguments", + `=_xlfn.CEILING.PRECISE("X",2)`: "#VALUE!", + `=_xlfn.CEILING.PRECISE(1,"X")`: "#VALUE!", // COMBIN - "=COMBIN()": "COMBIN requires 2 argument", - "=COMBIN(-1,1)": "COMBIN requires number >= number_chosen", + "=COMBIN()": "COMBIN requires 2 argument", + "=COMBIN(-1,1)": "COMBIN requires number >= number_chosen", + `=COMBIN("X",1)`: "#VALUE!", + `=COMBIN(-1,"X")`: "#VALUE!", // _xlfn.COMBINA - "=_xlfn.COMBINA()": "COMBINA requires 2 argument", - "=_xlfn.COMBINA(-1,1)": "COMBINA requires number > number_chosen", - "=_xlfn.COMBINA(-1,-1)": "COMBIN requires number >= number_chosen", + "=_xlfn.COMBINA()": "COMBINA requires 2 argument", + "=_xlfn.COMBINA(-1,1)": "COMBINA requires number > number_chosen", + "=_xlfn.COMBINA(-1,-1)": "COMBIN requires number >= number_chosen", + `=_xlfn.COMBINA("X",1)`: "#VALUE!", + `=_xlfn.COMBINA(-1,"X")`: "#VALUE!", // COS - "=COS()": "COS requires 1 numeric argument", + "=COS()": "COS requires 1 numeric argument", + `=COS("X")`: "#VALUE!", // COSH - "=COSH()": "COSH requires 1 numeric argument", + "=COSH()": "COSH requires 1 numeric argument", + `=COSH("X")`: "#VALUE!", // _xlfn.COT - "=COT()": "COT requires 1 numeric argument", + "=COT()": "COT requires 1 numeric argument", + `=COT("X")`: "#VALUE!", + "=COT(0)": "#DIV/0!", // _xlfn.COTH - "=COTH()": "COTH requires 1 numeric argument", + "=COTH()": "COTH requires 1 numeric argument", + `=COTH("X")`: "#VALUE!", + "=COTH(0)": "#DIV/0!", // _xlfn.CSC - "=_xlfn.CSC()": "CSC requires 1 numeric argument", - "=_xlfn.CSC(0)": "#NAME?", + "=_xlfn.CSC()": "CSC requires 1 numeric argument", + `=_xlfn.CSC("X")`: "#VALUE!", + "=_xlfn.CSC(0)": "#DIV/0!", // _xlfn.CSCH - "=_xlfn.CSCH()": "CSCH requires 1 numeric argument", - "=_xlfn.CSCH(0)": "#NAME?", + "=_xlfn.CSCH()": "CSCH requires 1 numeric argument", + `=_xlfn.CSCH("X")`: "#VALUE!", + "=_xlfn.CSCH(0)": "#DIV/0!", // _xlfn.DECIMAL "=_xlfn.DECIMAL()": "DECIMAL requires 2 numeric arguments", - `=_xlfn.DECIMAL("2000", 2)`: "#NUM!", + `=_xlfn.DECIMAL("X", 2)`: "#VALUE!", + `=_xlfn.DECIMAL(2000, "X")`: "#VALUE!", // DEGREES - "=DEGREES()": "DEGREES requires 1 numeric argument", + "=DEGREES()": "DEGREES requires 1 numeric argument", + `=DEGREES("X")`: "#VALUE!", + "=DEGREES(0)": "#DIV/0!", // EVEN - "=EVEN()": "EVEN requires 1 numeric argument", + "=EVEN()": "EVEN requires 1 numeric argument", + `=EVEN("X")`: "#VALUE!", // EXP - "=EXP()": "EXP requires 1 numeric argument", + "=EXP()": "EXP requires 1 numeric argument", + `=EXP("X")`: "#VALUE!", // FACT - "=FACT()": "FACT requires 1 numeric argument", - "=FACT(-1)": "#NUM!", + "=FACT()": "FACT requires 1 numeric argument", + `=FACT("X")`: "#VALUE!", + "=FACT(-1)": "#NUM!", // FACTDOUBLE - "=FACTDOUBLE()": "FACTDOUBLE requires 1 numeric argument", - "=FACTDOUBLE(-1)": "#NUM!", + "=FACTDOUBLE()": "FACTDOUBLE requires 1 numeric argument", + `=FACTDOUBLE("X")`: "#VALUE!", + "=FACTDOUBLE(-1)": "#NUM!", // FLOOR - "=FLOOR()": "FLOOR requires 2 numeric arguments", - "=FLOOR(1,-1)": "#NUM!", + "=FLOOR()": "FLOOR requires 2 numeric arguments", + `=FLOOR("X",-1)`: "#VALUE!", + `=FLOOR(1,"X")`: "#VALUE!", + "=FLOOR(1,-1)": "#NUM!", // _xlfn.FLOOR.MATH "=_xlfn.FLOOR.MATH()": "FLOOR.MATH requires at least 1 argument", "=_xlfn.FLOOR.MATH(1,2,3,4)": "FLOOR.MATH allows at most 3 arguments", + `=_xlfn.FLOOR.MATH("X",2,3)`: "#VALUE!", + `=_xlfn.FLOOR.MATH(1,"X",3)`: "#VALUE!", + `=_xlfn.FLOOR.MATH(1,2,"X")`: "#VALUE!", // _xlfn.FLOOR.PRECISE "=_xlfn.FLOOR.PRECISE()": "FLOOR.PRECISE requires at least 1 argument", "=_xlfn.FLOOR.PRECISE(1,2,3)": "FLOOR.PRECISE allows at most 2 arguments", + `=_xlfn.FLOOR.PRECISE("X",2)`: "#VALUE!", + `=_xlfn.FLOOR.PRECISE(1,"X")`: "#VALUE!", // GCD "=GCD()": "GCD requires at least 1 argument", "=GCD(-1)": "GCD only accepts positive arguments", "=GCD(1,-1)": "GCD only accepts positive arguments", + `=GCD("X")`: "#VALUE!", // INT - "=INT()": "INT requires 1 numeric argument", + "=INT()": "INT requires 1 numeric argument", + `=INT("X")`: "#VALUE!", // ISO.CEILING "=ISO.CEILING()": "ISO.CEILING requires at least 1 argument", "=ISO.CEILING(1,2,3)": "ISO.CEILING allows at most 2 arguments", + `=ISO.CEILING("X",2)`: "#VALUE!", + `=ISO.CEILING(1,"X")`: "#VALUE!", // LCM "=LCM()": "LCM requires at least 1 argument", "=LCM(-1)": "LCM only accepts positive arguments", "=LCM(1,-1)": "LCM only accepts positive arguments", + `=LCM("X")`: "#VALUE!", // LN - "=LN()": "LN requires 1 numeric argument", + "=LN()": "LN requires 1 numeric argument", + `=LN("X")`: "#VALUE!", // LOG "=LOG()": "LOG requires at least 1 argument", "=LOG(1,2,3)": "LOG allows at most 2 arguments", + `=LOG("X",1)`: "#VALUE!", + `=LOG(1,"X")`: "#VALUE!", "=LOG(0,0)": "#NUM!", "=LOG(1,0)": "#NUM!", "=LOG(1,1)": "#DIV/0!", // LOG10 - "=LOG10()": "LOG10 requires 1 numeric argument", + "=LOG10()": "LOG10 requires 1 numeric argument", + `=LOG10("X")`: "#VALUE!", // MOD - "=MOD()": "MOD requires 2 numeric arguments", - "=MOD(6,0)": "#DIV/0!", + "=MOD()": "MOD requires 2 numeric arguments", + "=MOD(6,0)": "#DIV/0!", + `=MOD("X",0)`: "#VALUE!", + `=MOD(6,"X")`: "#VALUE!", // MROUND - "=MROUND()": "MROUND requires 2 numeric arguments", - "=MROUND(1,0)": "#NUM!", + "=MROUND()": "MROUND requires 2 numeric arguments", + "=MROUND(1,0)": "#NUM!", + "=MROUND(1,-1)": "#NUM!", + `=MROUND("X",0)`: "#VALUE!", + `=MROUND(1,"X")`: "#VALUE!", + // MULTINOMIAL + `=MULTINOMIAL("X")`: "#VALUE!", // _xlfn.MUNIT - "=_xlfn.MUNIT()": "MUNIT requires 1 numeric argument", // not support currently + "=_xlfn.MUNIT()": "MUNIT requires 1 numeric argument", // not support currently + `=_xlfn.MUNIT("X")`: "#VALUE!", // not support currently // ODD - "=ODD()": "ODD requires 1 numeric argument", + "=ODD()": "ODD requires 1 numeric argument", + `=ODD("X")`: "#VALUE!", // PI "=PI(1)": "PI accepts no arguments", // POWER - "=POWER(0,0)": "#NUM!", - "=POWER(0,-1)": "#DIV/0!", - "=POWER(1)": "POWER requires 2 numeric arguments", + `=POWER("X",1)`: "#VALUE!", + `=POWER(1,"X")`: "#VALUE!", + "=POWER(0,0)": "#NUM!", + "=POWER(0,-1)": "#DIV/0!", + "=POWER(1)": "POWER requires 2 numeric arguments", + // PRODUCT + `=PRODUCT("X")`: "#VALUE!", // QUOTIENT - "=QUOTIENT(1,0)": "#DIV/0!", - "=QUOTIENT(1)": "QUOTIENT requires 2 numeric arguments", + `=QUOTIENT("X",1)`: "#VALUE!", + `=QUOTIENT(1,"X")`: "#VALUE!", + "=QUOTIENT(1,0)": "#DIV/0!", + "=QUOTIENT(1)": "QUOTIENT requires 2 numeric arguments", // RADIANS - "=RADIANS()": "RADIANS requires 1 numeric argument", + `=RADIANS("X")`: "#VALUE!", + "=RADIANS()": "RADIANS requires 1 numeric argument", // RAND "=RAND(1)": "RAND accepts no arguments", // RANDBETWEEN - "=RANDBETWEEN()": "RANDBETWEEN requires 2 numeric arguments", - "=RANDBETWEEN(2,1)": "#NUM!", + `=RANDBETWEEN("X",1)`: "#VALUE!", + `=RANDBETWEEN(1,"X")`: "#VALUE!", + "=RANDBETWEEN()": "RANDBETWEEN requires 2 numeric arguments", + "=RANDBETWEEN(2,1)": "#NUM!", // ROMAN "=ROMAN()": "ROMAN requires at least 1 argument", "=ROMAN(1,2,3)": "ROMAN allows at most 2 arguments", + `=ROMAN("X")`: "#VALUE!", + `=ROMAN("X",1)`: "#VALUE!", // ROUND - "=ROUND()": "ROUND requires 2 numeric arguments", + "=ROUND()": "ROUND requires 2 numeric arguments", + `=ROUND("X",1)`: "#VALUE!", + `=ROUND(1,"X")`: "#VALUE!", // ROUNDDOWN - "=ROUNDDOWN()": "ROUNDDOWN requires 2 numeric arguments", + "=ROUNDDOWN()": "ROUNDDOWN requires 2 numeric arguments", + `=ROUNDDOWN("X",1)`: "#VALUE!", + `=ROUNDDOWN(1,"X")`: "#VALUE!", // ROUNDUP - "=ROUNDUP()": "ROUNDUP requires 2 numeric arguments", + "=ROUNDUP()": "ROUNDUP requires 2 numeric arguments", + `=ROUNDUP("X",1)`: "#VALUE!", + `=ROUNDUP(1,"X")`: "#VALUE!", // SEC - "=_xlfn.SEC()": "SEC requires 1 numeric argument", + "=_xlfn.SEC()": "SEC requires 1 numeric argument", + `=_xlfn.SEC("X")`: "#VALUE!", // _xlfn.SECH - "=_xlfn.SECH()": "SECH requires 1 numeric argument", + "=_xlfn.SECH()": "SECH requires 1 numeric argument", + `=_xlfn.SECH("X")`: "#VALUE!", // SIGN - "=SIGN()": "SIGN requires 1 numeric argument", + "=SIGN()": "SIGN requires 1 numeric argument", + `=SIGN("X")`: "#VALUE!", // SIN - "=SIN()": "SIN requires 1 numeric argument", + "=SIN()": "SIN requires 1 numeric argument", + `=SIN("X")`: "#VALUE!", // SINH - "=SINH()": "SINH requires 1 numeric argument", + "=SINH()": "SINH requires 1 numeric argument", + `=SINH("X")`: "#VALUE!", // SQRT - "=SQRT()": "SQRT requires 1 numeric argument", - "=SQRT(-1)": "#NUM!", + "=SQRT()": "SQRT requires 1 numeric argument", + `=SQRT("X")`: "#VALUE!", + "=SQRT(-1)": "#NUM!", // SQRTPI - "=SQRTPI()": "SQRTPI requires 1 numeric argument", + "=SQRTPI()": "SQRTPI requires 1 numeric argument", + `=SQRTPI("X")`: "#VALUE!", // SUM - "=SUM((": "formula not valid", - "=SUM(-)": "formula not valid", - "=SUM(1+)": "formula not valid", - "=SUM(1-)": "formula not valid", - "=SUM(1*)": "formula not valid", - "=SUM(1/)": "formula not valid", + "=SUM((": "formula not valid", + "=SUM(-)": "formula not valid", + "=SUM(1+)": "formula not valid", + "=SUM(1-)": "formula not valid", + "=SUM(1*)": "formula not valid", + "=SUM(1/)": "formula not valid", + `=SUM("X")`: "#VALUE!", + // SUMSQ + `=SUMSQ("X")`: "#VALUE!", // TAN - "=TAN()": "TAN requires 1 numeric argument", + "=TAN()": "TAN requires 1 numeric argument", + `=TAN("X")`: "#VALUE!", // TANH - "=TANH()": "TANH requires 1 numeric argument", + "=TANH()": "TANH requires 1 numeric argument", + `=TANH("X")`: "#VALUE!", // TRUNC - "=TRUNC()": "TRUNC requires at least 1 argument", + "=TRUNC()": "TRUNC requires at least 1 argument", + `=TRUNC("X")`: "#VALUE!", + `=TRUNC(1,"X")`: "#VALUE!", } for formula, expected := range mathCalcError { f := prepareData() @@ -570,6 +686,17 @@ func TestCalcCellValue(t *testing.T) { assert.Equal(t, "", result, formula) } + volatileFuncs := []string{ + "=RAND()", + "=RANDBETWEEN(1,2)", + } + for _, formula := range volatileFuncs { + f := prepareData() + assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula)) + _, err := f.CalcCellValue("Sheet1", "C1") + assert.NoError(t, err) + } + // Test get calculated cell value on not formula cell. f := prepareData() result, err := f.CalcCellValue("Sheet1", "A1") diff --git a/go.mod b/go.mod index e411f19..f94f33b 100644 --- a/go.mod +++ b/go.mod @@ -4,10 +4,14 @@ go 1.12 require ( github.com/davecgh/go-spew v1.1.1 // indirect + github.com/kr/text v0.2.0 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 - github.com/stretchr/testify v1.3.0 + github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect + github.com/stretchr/testify v1.5.1 github.com/xuri/efp v0.0.0-20191019043341-b7dc4fe9aa91 - golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a - golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 + golang.org/x/image v0.0.0-20200430140353-33d19683fad8 + golang.org/x/net v0.0.0-20200506145744-7e3656a0809f golang.org/x/text v0.3.2 // indirect + gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect + gopkg.in/yaml.v2 v2.2.8 // indirect ) diff --git a/go.sum b/go.sum index ff969f0..7fa49fe 100644 --- a/go.sum +++ b/go.sum @@ -1,24 +1,39 @@ -github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/xuri/efp v0.0.0-20191019043341-b7dc4fe9aa91 h1:gp02YctZuIPTk0t7qI+wvg3VQwTPyNmSGG6ZqOsjSL8= github.com/xuri/efp v0.0.0-20191019043341-b7dc4fe9aa91/go.mod h1:uBiSUepVYMhGTfDeBKKasV4GpgBlzJ46gXUBAqV8qLk= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a h1:gHevYm0pO4QUbwy8Dmdr01R5r1BuKtfYqRqF0h/Cbh0= -golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/image v0.0.0-20200430140353-33d19683fad8 h1:6WW6V3x1P/jokJBpRQYUJnMHRP6isStQwCozxnU7XQw= +golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f h1:QBjCr1Fz5kw158VqdE9JfI9cJnl/ymnJWAdMuinqL7Y= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -- cgit v1.2.1 From dfea8f96edc326717822ec9c4b92f462d0fe1255 Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 12 May 2020 23:26:26 +0800 Subject: - New API: SetSheetFormatPr and GetSheetFormatPr - typo fix, resolve #635 --- sheet.go | 2 +- sheetpr.go | 197 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- sheetpr_test.go | 160 ++++++++++++++++++++++++++++++++++++++++++++ xmlPivotCache.go | 11 ++++ 4 files changed, 367 insertions(+), 3 deletions(-) diff --git a/sheet.go b/sheet.go index fa858af..6a935b1 100644 --- a/sheet.go +++ b/sheet.go @@ -354,7 +354,7 @@ func (f *File) getSheetID(name string) int { // GetSheetIndex provides a function to get a sheet index of the workbook by // the given sheet name. If the given sheet name is invalid, it will return an -// integer type value -1. +// integer type value 0. func (f *File) GetSheetIndex(name string) int { var idx = -1 for index, sheet := range f.GetSheetList() { diff --git a/sheetpr.go b/sheetpr.go index 350e189..dbfb734 100644 --- a/sheetpr.go +++ b/sheetpr.go @@ -313,7 +313,7 @@ type PageMarginsOptionsPtr interface { // SetPageMargins provides a function to set worksheet page margins. // // Available options: -// PageMarginBotom(float64) +// PageMarginBottom(float64) // PageMarginFooter(float64) // PageMarginHeader(float64) // PageMarginLeft(float64) @@ -339,7 +339,7 @@ func (f *File) SetPageMargins(sheet string, opts ...PageMarginsOptions) error { // GetPageMargins provides a function to get worksheet page margins. // // Available options: -// PageMarginBotom(float64) +// PageMarginBottom(float64) // PageMarginFooter(float64) // PageMarginHeader(float64) // PageMarginLeft(float64) @@ -357,3 +357,196 @@ func (f *File) GetPageMargins(sheet string, opts ...PageMarginsOptionsPtr) error } return err } + +// SheetFormatPrOptions is an option of the formatting properties of a +// worksheet. See SetSheetFormatPr(). +type SheetFormatPrOptions interface { + setSheetFormatPr(formatPr *xlsxSheetFormatPr) +} + +// SheetFormatPrOptionsPtr is a writable SheetFormatPrOptions. See +// GetSheetFormatPr(). +type SheetFormatPrOptionsPtr interface { + SheetFormatPrOptions + getSheetFormatPr(formatPr *xlsxSheetFormatPr) +} + +type ( + // BaseColWidth specifies the number of characters of the maximum digit width + // of the normal style's font. This value does not include margin padding or + // extra padding for gridlines. It is only the number of characters. + BaseColWidth uint8 + // DefaultColWidth specifies the default column width measured as the number + // of characters of the maximum digit width of the normal style's font. + DefaultColWidth float64 + // DefaultRowHeight specifies the default row height measured in point size. + // Optimization so we don't have to write the height on all rows. This can be + // written out if most rows have custom height, to achieve the optimization. + DefaultRowHeight float64 + // CustomHeight specifies the custom height. + CustomHeight bool + // ZeroHeight specifies if rows are hidden. + ZeroHeight bool + // ThickTop specifies if rows have a thick top border by default. + ThickTop bool + // ThickBottom specifies if rows have a thick bottom border by default. + ThickBottom bool +) + +// setSheetFormatPr provides a method to set the number of characters of the +// maximum digit width of the normal style's font. +func (p BaseColWidth) setSheetFormatPr(fp *xlsxSheetFormatPr) { + fp.BaseColWidth = uint8(p) +} + +// setSheetFormatPr provides a method to set the number of characters of the +// maximum digit width of the normal style's font. +func (p *BaseColWidth) getSheetFormatPr(fp *xlsxSheetFormatPr) { + if fp == nil { + *p = 0 + return + } + *p = BaseColWidth(fp.BaseColWidth) +} + +// setSheetFormatPr provides a method to set the default column width measured +// as the number of characters of the maximum digit width of the normal +// style's font. +func (p DefaultColWidth) setSheetFormatPr(fp *xlsxSheetFormatPr) { + fp.DefaultColWidth = float64(p) +} + +// getSheetFormatPr provides a method to get the default column width measured +// as the number of characters of the maximum digit width of the normal +// style's font. +func (p *DefaultColWidth) getSheetFormatPr(fp *xlsxSheetFormatPr) { + if fp == nil { + *p = 0 + return + } + *p = DefaultColWidth(fp.DefaultColWidth) +} + +// setSheetFormatPr provides a method to set the default row height measured +// in point size. +func (p DefaultRowHeight) setSheetFormatPr(fp *xlsxSheetFormatPr) { + fp.DefaultRowHeight = float64(p) +} + +// getSheetFormatPr provides a method to get the default row height measured +// in point size. +func (p *DefaultRowHeight) getSheetFormatPr(fp *xlsxSheetFormatPr) { + if fp == nil { + *p = 15 + return + } + *p = DefaultRowHeight(fp.DefaultRowHeight) +} + +// setSheetFormatPr provides a method to set the custom height. +func (p CustomHeight) setSheetFormatPr(fp *xlsxSheetFormatPr) { + fp.CustomHeight = bool(p) +} + +// getSheetFormatPr provides a method to get the custom height. +func (p *CustomHeight) getSheetFormatPr(fp *xlsxSheetFormatPr) { + if fp == nil { + *p = false + return + } + *p = CustomHeight(fp.CustomHeight) +} + +// setSheetFormatPr provides a method to set if rows are hidden. +func (p ZeroHeight) setSheetFormatPr(fp *xlsxSheetFormatPr) { + fp.ZeroHeight = bool(p) +} + +// getSheetFormatPr provides a method to get if rows are hidden. +func (p *ZeroHeight) getSheetFormatPr(fp *xlsxSheetFormatPr) { + if fp == nil { + *p = false + return + } + *p = ZeroHeight(fp.ZeroHeight) +} + +// setSheetFormatPr provides a method to set if rows have a thick top border +// by default. +func (p ThickTop) setSheetFormatPr(fp *xlsxSheetFormatPr) { + fp.ThickTop = bool(p) +} + +// getSheetFormatPr provides a method to get if rows have a thick top border +// by default. +func (p *ThickTop) getSheetFormatPr(fp *xlsxSheetFormatPr) { + if fp == nil { + *p = false + return + } + *p = ThickTop(fp.ThickTop) +} + +// setSheetFormatPr provides a method to set if rows have a thick bottom +// border by default. +func (p ThickBottom) setSheetFormatPr(fp *xlsxSheetFormatPr) { + fp.ThickBottom = bool(p) +} + +// setSheetFormatPr provides a method to set if rows have a thick bottom +// border by default. +func (p *ThickBottom) getSheetFormatPr(fp *xlsxSheetFormatPr) { + if fp == nil { + *p = false + return + } + *p = ThickBottom(fp.ThickBottom) +} + +// SetSheetFormatPr provides a function to set worksheet formatting properties. +// +// Available options: +// BaseColWidth(uint8) +// DefaultColWidth(float64) +// DefaultRowHeight(float64) +// CustomHeight(bool) +// ZeroHeight(bool) +// ThickTop(bool) +// ThickBottom(bool) +func (f *File) SetSheetFormatPr(sheet string, opts ...SheetFormatPrOptions) error { + s, err := f.workSheetReader(sheet) + if err != nil { + return err + } + fp := s.SheetFormatPr + if fp == nil { + fp = new(xlsxSheetFormatPr) + s.SheetFormatPr = fp + } + for _, opt := range opts { + opt.setSheetFormatPr(fp) + } + return err +} + +// GetSheetFormatPr provides a function to get worksheet formatting properties. +// +// Available options: +// BaseColWidth(uint8) +// DefaultColWidth(float64) +// DefaultRowHeight(float64) +// CustomHeight(bool) +// ZeroHeight(bool) +// ThickTop(bool) +// ThickBottom(bool) +func (f *File) GetSheetFormatPr(sheet string, opts ...SheetFormatPrOptionsPtr) error { + s, err := f.workSheetReader(sheet) + if err != nil { + return err + } + fp := s.SheetFormatPr + for _, opt := range opts { + opt.getSheetFormatPr(fp) + } + return err +} diff --git a/sheetpr_test.go b/sheetpr_test.go index 25b67d7..6e03151 100644 --- a/sheetpr_test.go +++ b/sheetpr_test.go @@ -307,3 +307,163 @@ func TestGetPageMargins(t *testing.T) { // Test get page margins on not exists worksheet. assert.EqualError(t, f.GetPageMargins("SheetN"), "sheet SheetN is not exist") } + +func ExampleFile_SetSheetFormatPr() { + f := excelize.NewFile() + const sheet = "Sheet1" + + if err := f.SetSheetFormatPr(sheet, + excelize.BaseColWidth(1.0), + excelize.DefaultColWidth(1.0), + excelize.DefaultRowHeight(1.0), + excelize.CustomHeight(true), + excelize.ZeroHeight(true), + excelize.ThickTop(true), + excelize.ThickBottom(true), + ); err != nil { + fmt.Println(err) + } + // Output: +} + +func ExampleFile_GetSheetFormatPr() { + f := excelize.NewFile() + const sheet = "Sheet1" + + var ( + baseColWidth excelize.BaseColWidth + defaultColWidth excelize.DefaultColWidth + defaultRowHeight excelize.DefaultRowHeight + customHeight excelize.CustomHeight + zeroHeight excelize.ZeroHeight + thickTop excelize.ThickTop + thickBottom excelize.ThickBottom + ) + + if err := f.GetSheetFormatPr(sheet, + &baseColWidth, + &defaultColWidth, + &defaultRowHeight, + &customHeight, + &zeroHeight, + &thickTop, + &thickBottom, + ); err != nil { + fmt.Println(err) + } + fmt.Println("Defaults:") + fmt.Println("- baseColWidth:", baseColWidth) + fmt.Println("- defaultColWidth:", defaultColWidth) + fmt.Println("- defaultRowHeight:", defaultRowHeight) + fmt.Println("- customHeight:", customHeight) + fmt.Println("- zeroHeight:", zeroHeight) + fmt.Println("- thickTop:", thickTop) + fmt.Println("- thickBottom:", thickBottom) + // Output: + // Defaults: + // - baseColWidth: 0 + // - defaultColWidth: 0 + // - defaultRowHeight: 15 + // - customHeight: false + // - zeroHeight: false + // - thickTop: false + // - thickBottom: false +} + +func TestSheetFormatPrOptions(t *testing.T) { + const sheet = "Sheet1" + + testData := []struct { + container excelize.SheetFormatPrOptionsPtr + nonDefault excelize.SheetFormatPrOptions + }{ + {new(excelize.BaseColWidth), excelize.BaseColWidth(1.0)}, + {new(excelize.DefaultColWidth), excelize.DefaultColWidth(1.0)}, + {new(excelize.DefaultRowHeight), excelize.DefaultRowHeight(1.0)}, + {new(excelize.CustomHeight), excelize.CustomHeight(true)}, + {new(excelize.ZeroHeight), excelize.ZeroHeight(true)}, + {new(excelize.ThickTop), excelize.ThickTop(true)}, + {new(excelize.ThickBottom), excelize.ThickBottom(true)}, + } + + for i, test := range testData { + t.Run(fmt.Sprintf("TestData%d", i), func(t *testing.T) { + + opt := test.nonDefault + t.Logf("option %T", opt) + + def := deepcopy.Copy(test.container).(excelize.SheetFormatPrOptionsPtr) + val1 := deepcopy.Copy(def).(excelize.SheetFormatPrOptionsPtr) + val2 := deepcopy.Copy(def).(excelize.SheetFormatPrOptionsPtr) + + f := excelize.NewFile() + // Get the default value + assert.NoError(t, f.GetSheetFormatPr(sheet, def), opt) + // Get again and check + assert.NoError(t, f.GetSheetFormatPr(sheet, val1), opt) + if !assert.Equal(t, val1, def, opt) { + t.FailNow() + } + // Set the same value + assert.NoError(t, f.SetSheetFormatPr(sheet, val1), opt) + // Get again and check + assert.NoError(t, f.GetSheetFormatPr(sheet, val1), opt) + if !assert.Equal(t, val1, def, "%T: value should not have changed", opt) { + t.FailNow() + } + // Set a different value + assert.NoError(t, f.SetSheetFormatPr(sheet, test.nonDefault), opt) + assert.NoError(t, f.GetSheetFormatPr(sheet, val1), opt) + // Get again and compare + assert.NoError(t, f.GetSheetFormatPr(sheet, val2), opt) + if !assert.Equal(t, val1, val2, "%T: value should not have changed", opt) { + t.FailNow() + } + // Value should not be the same as the default + if !assert.NotEqual(t, def, val1, "%T: value should have changed from default", opt) { + t.FailNow() + } + // Restore the default value + assert.NoError(t, f.SetSheetFormatPr(sheet, def), opt) + assert.NoError(t, f.GetSheetFormatPr(sheet, val1), opt) + if !assert.Equal(t, def, val1) { + t.FailNow() + } + }) + } +} + +func TestSetSheetFormatPr(t *testing.T) { + f := excelize.NewFile() + assert.NoError(t, f.GetSheetFormatPr("Sheet1")) + f.Sheet["xl/worksheets/sheet1.xml"].SheetFormatPr = nil + assert.NoError(t, f.SetSheetFormatPr("Sheet1", excelize.BaseColWidth(1.0))) + // Test set formatting properties on not exists worksheet. + assert.EqualError(t, f.SetSheetFormatPr("SheetN"), "sheet SheetN is not exist") +} + +func TestGetSheetFormatPr(t *testing.T) { + f := excelize.NewFile() + assert.NoError(t, f.GetSheetFormatPr("Sheet1")) + f.Sheet["xl/worksheets/sheet1.xml"].SheetFormatPr = nil + var ( + baseColWidth excelize.BaseColWidth + defaultColWidth excelize.DefaultColWidth + defaultRowHeight excelize.DefaultRowHeight + customHeight excelize.CustomHeight + zeroHeight excelize.ZeroHeight + thickTop excelize.ThickTop + thickBottom excelize.ThickBottom + ) + assert.NoError(t, f.GetSheetFormatPr("Sheet1", + &baseColWidth, + &defaultColWidth, + &defaultRowHeight, + &customHeight, + &zeroHeight, + &thickTop, + &thickBottom, + )) + // Test get formatting properties on not exists worksheet. + assert.EqualError(t, f.GetSheetFormatPr("SheetN"), "sheet SheetN is not exist") +} diff --git a/xmlPivotCache.go b/xmlPivotCache.go index 45b48de..feaec54 100644 --- a/xmlPivotCache.go +++ b/xmlPivotCache.go @@ -1,3 +1,14 @@ +// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of +// this source code is governed by a BSD-style license that can be found in +// the LICENSE file. +// +// Package excelize 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 Exce™ 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. + package excelize import "encoding/xml" -- cgit v1.2.1 From 9baa1bbc9865bee1b3c8981ab98eb8c9049c40e4 Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 14 May 2020 22:36:00 +0800 Subject: Fix #637, improve the compatibility of the auto filter with Office 2007 - 2013 --- table.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/table.go b/table.go index 55901cd..ae47471 100644 --- a/table.go +++ b/table.go @@ -290,6 +290,20 @@ func (f *File) AutoFilter(sheet, hcell, vcell, format string) error { return err } ref := cellStart + ":" + cellEnd + wb := f.workbookReader() + d := xlsxDefinedName{ + Name: "_xlnm._FilterDatabase", + Hidden: true, + LocalSheetID: intPtr(f.GetSheetIndex(sheet)), + Data: fmt.Sprintf("%s!%s", sheet, ref), + } + if wb.DefinedNames != nil { + wb.DefinedNames.DefinedName = append(wb.DefinedNames.DefinedName, d) + } else { + wb.DefinedNames = &xlsxDefinedNames{ + DefinedName: []xlsxDefinedName{d}, + } + } refRange := vcol - hcol return f.autoFilter(sheet, ref, refRange, hcol, formatSet) } -- cgit v1.2.1 From c815e4b84b9b777c30d127f384b38105afa2640d Mon Sep 17 00:00:00 2001 From: yuemanxilou Date: Fri, 15 May 2020 14:03:02 +0800 Subject: avoid duplicate filter database in workbook defined name --- table.go | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/table.go b/table.go index ae47471..5a0e46f 100644 --- a/table.go +++ b/table.go @@ -281,28 +281,38 @@ func (f *File) AutoFilter(sheet, hcell, vcell, format string) error { formatSet, _ := parseAutoFilterSet(format) var cellStart, cellEnd string - cellStart, err = CoordinatesToCellName(hcol, hrow) - if err != nil { + if cellStart, err = CoordinatesToCellName(hcol, hrow); err != nil { return err } - cellEnd, err = CoordinatesToCellName(vcol, vrow) - if err != nil { + if cellEnd, err = CoordinatesToCellName(vcol, vrow); err != nil { return err } - ref := cellStart + ":" + cellEnd + ref, filterDB := cellStart+":"+cellEnd, "_xlnm._FilterDatabase" wb := f.workbookReader() + sheetID := f.GetSheetIndex(sheet) + filterRange := fmt.Sprintf("%s!%s", sheet, ref) d := xlsxDefinedName{ - Name: "_xlnm._FilterDatabase", + Name: filterDB, Hidden: true, - LocalSheetID: intPtr(f.GetSheetIndex(sheet)), - Data: fmt.Sprintf("%s!%s", sheet, ref), + LocalSheetID: intPtr(sheetID), + Data: filterRange, } - if wb.DefinedNames != nil { - wb.DefinedNames.DefinedName = append(wb.DefinedNames.DefinedName, d) - } else { + if wb.DefinedNames == nil { wb.DefinedNames = &xlsxDefinedNames{ DefinedName: []xlsxDefinedName{d}, } + } else { + var definedNameExists bool + for idx := range wb.DefinedNames.DefinedName { + definedName := wb.DefinedNames.DefinedName[idx] + if definedName.Name == filterDB && *definedName.LocalSheetID == sheetID && definedName.Hidden { + wb.DefinedNames.DefinedName[idx].Data = filterRange + definedNameExists = true + } + } + if !definedNameExists { + wb.DefinedNames.DefinedName = append(wb.DefinedNames.DefinedName, d) + } } refRange := vcol - hcol return f.autoFilter(sheet, ref, refRange, hcol, formatSet) -- cgit v1.2.1 From 98221a332ff9c37c9b20c44e9efdbe4c22a5cf5c Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 17 May 2020 17:36:53 +0800 Subject: Merge pull request #410 --- picture.go | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++-- picture_test.go | 21 +++++++++++++++++++++ rows.go | 3 ++- xmlDrawing.go | 1 + 4 files changed, 75 insertions(+), 3 deletions(-) diff --git a/picture.go b/picture.go index 306a582..71c3b8e 100644 --- a/picture.go +++ b/picture.go @@ -32,6 +32,7 @@ func parseFormatPictureSet(formatSet string) (*formatPicture, error) { FPrintsWithSheet: true, FLocksWithSheet: false, NoChangeAspect: false, + Autofit: false, OffsetX: 0, OffsetY: 0, XScale: 1.0, @@ -244,8 +245,12 @@ func (f *File) addDrawingPicture(sheet, drawingXML, cell, file string, width, he if err != nil { return err } - width = int(float64(width) * formatSet.XScale) - height = int(float64(height) * formatSet.YScale) + if formatSet.Autofit { + width, height, col, row, err = f.drawingResize(sheet, cell, float64(width), float64(height), formatSet) + if err != nil { + return err + } + } col-- row-- colStart, rowStart, _, _, colEnd, rowEnd, x2, y2 := @@ -578,3 +583,47 @@ func (f *File) drawingsWriter() { } } } + +// drawingResize calculate the height and width after resizing. +func (f *File) drawingResize(sheet string, cell string, width, height float64, formatSet *formatPicture) (w, h, c, r int, err error) { + var mergeCells []MergeCell + mergeCells, err = f.GetMergeCells(sheet) + if err != nil { + return + } + var rng []int + var inMergeCell bool + if c, r, err = CellNameToCoordinates(cell); err != nil { + return + } + cellWidth, cellHeight := f.getColWidth(sheet, c), f.getRowHeight(sheet, r) + for _, mergeCell := range mergeCells { + if inMergeCell, err = f.checkCellInArea(cell, mergeCell[0]); err != nil { + return + } + if inMergeCell { + rng, _ = areaRangeToCoordinates(mergeCell.GetStartAxis(), mergeCell.GetEndAxis()) + sortCoordinates(rng) + } + } + if inMergeCell { + c, r = rng[0], rng[1] + for col := rng[0] - 1; col < rng[2]; col++ { + cellWidth += f.getColWidth(sheet, col) + } + for row := rng[1] - 1; row < rng[3]; row++ { + cellHeight += f.getRowHeight(sheet, row) + } + } + if float64(cellWidth) < width { + asp := float64(cellWidth) / width + width, height = float64(cellWidth), height*asp + } + if float64(cellHeight) < height { + asp := float64(cellHeight) / height + height, width = float64(cellHeight), width*asp + } + width, height = width-float64(formatSet.OffsetX), height-float64(formatSet.OffsetY) + w, h = int(width*formatSet.XScale), int(height*formatSet.YScale) + return +} diff --git a/picture_test.go b/picture_test.go index fdc6f0d..015d854 100644 --- a/picture_test.go +++ b/picture_test.go @@ -47,6 +47,15 @@ func TestAddPicture(t *testing.T) { file, err := ioutil.ReadFile(filepath.Join("test", "images", "excel.png")) assert.NoError(t, err) + // Test add picture to worksheet with autofit. + assert.NoError(t, f.AddPicture("Sheet1", "A30", filepath.Join("test", "images", "excel.jpg"), `{"autofit": true}`)) + assert.NoError(t, f.AddPicture("Sheet1", "B30", filepath.Join("test", "images", "excel.jpg"), `{"x_offset": 10, "y_offset": 10, "autofit": true}`)) + f.NewSheet("AddPicture") + assert.NoError(t, f.SetRowHeight("AddPicture", 10, 30)) + assert.NoError(t, f.MergeCell("AddPicture", "B3", "D9")) + assert.NoError(t, f.AddPicture("AddPicture", "C6", filepath.Join("test", "images", "excel.jpg"), `{"autofit": true}`)) + assert.NoError(t, f.AddPicture("AddPicture", "A1", filepath.Join("test", "images", "excel.jpg"), `{"autofit": true}`)) + // Test add picture to worksheet from bytes. assert.NoError(t, f.AddPictureFromBytes("Sheet1", "Q1", "", "Excel Logo", ".png", file)) // Test add picture to worksheet from bytes with illegal cell coordinates. @@ -181,3 +190,15 @@ func TestDeletePicture(t *testing.T) { // Test delete picture on no chart worksheet. assert.NoError(t, NewFile().DeletePicture("Sheet1", "A1")) } + +func TestDrawingResize(t *testing.T) { + f := NewFile() + // Test calculate drawing resize on not exists worksheet. + _, _, _, _, err := f.drawingResize("SheetN", "A1", 1, 1, nil) + assert.EqualError(t, err, "sheet SheetN is not exist") + // Test calculate drawing resize with invalid coordinates. + _, _, _, _, err = f.drawingResize("Sheet1", "", 1, 1, nil) + assert.EqualError(t, err, `cannot convert cell "" to coordinates: invalid cell name ""`) + f.Sheet["xl/worksheets/sheet1.xml"].MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A:A"}}} + assert.EqualError(t, f.AddPicture("Sheet1", "A1", filepath.Join("test", "images", "excel.jpg"), `{"autofit": true}`), `cannot convert cell "A" to coordinates: invalid cell name "A"`) +} diff --git a/rows.go b/rows.go index 6a67672..17216df 100644 --- a/rows.go +++ b/rows.go @@ -148,7 +148,8 @@ func (err ErrSheetNotExist) Error() string { return fmt.Sprintf("sheet %s is not exist", string(err.SheetName)) } -// Rows return a rows iterator. For example: +// Rows returns a rows iterator, used for streaming reading data for a +// worksheet with a large data. For example: // // rows, err := f.Rows("Sheet1") // if err != nil { diff --git a/xmlDrawing.go b/xmlDrawing.go index a5e43a1..808bed5 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -419,6 +419,7 @@ type formatPicture struct { FPrintsWithSheet bool `json:"print_obj"` FLocksWithSheet bool `json:"locked"` NoChangeAspect bool `json:"lock_aspect_ratio"` + Autofit bool `json:"autofit"` OffsetX int `json:"x_offset"` OffsetY int `json:"y_offset"` XScale float64 `json:"x_scale"` -- cgit v1.2.1 From 2efc7107ff30dc7f1e1a798082616ee488f99489 Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 21 May 2020 22:57:58 +0800 Subject: - transform the range to the matrix on the first arg of the formula - typo fix - reset cell with and height when insert picture into merged cell with autofit --- calc.go | 22 ++++++++++------------ excelize.go | 9 +++++---- lib.go | 8 ++++---- picture.go | 1 + 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/calc.go b/calc.go index 61b6dac..bff7866 100644 --- a/calc.go +++ b/calc.go @@ -56,7 +56,7 @@ type cellRange struct { // formulaArg is the argument of a formula or function. type formulaArg struct { Value string - Matrix []string + Matrix [][]string } // formulaFuncs is the type of the formula functions. @@ -172,8 +172,8 @@ func (f *File) evalInfixExp(sheet string, tokens []efp.Token) (efp.Token, error) } for idx, val := range result { arg := formulaArg{Value: val} - if idx < len(matrix) { - arg.Matrix = matrix[idx] + if idx == 0 { + arg.Matrix = matrix } argsList.PushBack(arg) } @@ -1850,17 +1850,13 @@ func det(sqMtx [][]float64) float64 { // func (fn *formulaFuncs) MDETERM(argsList *list.List) (result string, err error) { var num float64 - var rows int var numMtx = [][]float64{} - var strMtx = [][]string{} - for arg := argsList.Front(); arg != nil; arg = arg.Next() { - if len(arg.Value.(formulaArg).Matrix) == 0 { - break - } - strMtx = append(strMtx, arg.Value.(formulaArg).Matrix) - rows++ + var strMtx = argsList.Front().Value.(formulaArg).Matrix + if argsList.Len() < 1 { + return } - for _, row := range strMtx { + var rows = len(strMtx) + for _, row := range argsList.Front().Value.(formulaArg).Matrix { if len(row) != rows { err = errors.New(formulaErrorVALUE) return @@ -2630,3 +2626,5 @@ func (fn *formulaFuncs) TRUNC(argsList *list.List) (result string, err error) { result = fmt.Sprintf("%g", float64(int(number*adjust))/adjust) return } + +// Statistical functions diff --git a/excelize.go b/excelize.go index 04e2e85..3fd25aa 100644 --- a/excelize.go +++ b/excelize.go @@ -28,7 +28,7 @@ import ( "golang.org/x/net/html/charset" ) -// File define a populated XLSX file struct. +// File define a populated spreadsheet file struct. type File struct { checked map[string]bool sheetMap map[string]string @@ -52,8 +52,8 @@ type File struct { type charsetTranscoderFn func(charset string, input io.Reader) (rdr io.Reader, err error) -// OpenFile take the name of an XLSX file and returns a populated XLSX file -// struct for it. +// OpenFile take the name of an spreadsheet file and returns a populated +// spreadsheet file struct for it. func OpenFile(filename string) (*File, error) { file, err := os.Open(filename) if err != nil { @@ -83,7 +83,8 @@ func newFile() *File { } } -// OpenReader take an io.Reader and return a populated XLSX file. +// OpenReader read data stream from io.Reader and return a populated +// spreadsheet file. func OpenReader(r io.Reader) (*File, error) { b, err := ioutil.ReadAll(r) if err != nil { diff --git a/lib.go b/lib.go index 79c7cd4..41b03c7 100644 --- a/lib.go +++ b/lib.go @@ -21,7 +21,7 @@ import ( "unsafe" ) -// ReadZipReader can be used to read an XLSX in memory without touching the +// ReadZipReader can be used to read the spreadsheet in memory without touching the // filesystem. func ReadZipReader(r *zip.Reader) (map[string][]byte, int, error) { fileList := make(map[string][]byte, len(r.File)) @@ -160,8 +160,8 @@ func ColumnNumberToName(num int) (string, error) { // // Example: // -// CellCoordinates("A1") // returns 1, 1, nil -// CellCoordinates("Z3") // returns 26, 3, nil +// excelize.CellNameToCoordinates("A1") // returns 1, 1, nil +// excelize.CellNameToCoordinates("Z3") // returns 26, 3, nil // func CellNameToCoordinates(cell string) (int, int, error) { const msg = "cannot convert cell %q to coordinates: %v" @@ -184,7 +184,7 @@ func CellNameToCoordinates(cell string) (int, int, error) { // // Example: // -// CoordinatesToCellName(1, 1) // returns "A1", nil +// excelize.CoordinatesToCellName(1, 1) // returns "A1", nil // func CoordinatesToCellName(col, row int) (string, error) { if col < 1 || row < 1 { diff --git a/picture.go b/picture.go index 71c3b8e..cac1af2 100644 --- a/picture.go +++ b/picture.go @@ -607,6 +607,7 @@ func (f *File) drawingResize(sheet string, cell string, width, height float64, f } } if inMergeCell { + cellWidth, cellHeight = 0, 0 c, r = rng[0], rng[1] for col := rng[0] - 1; col < rng[2]; col++ { cellWidth += f.getColWidth(sheet, col) -- cgit v1.2.1