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/.travis.yml b/.travis.yml index 6c061a8..92852cf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,18 +4,17 @@ install: - go get -d -t -v ./... && go build -v ./... go: - - 1.8.x - - 1.9.x - - 1.10.x - 1.11.x - 1.12.x + - 1.13.x + - 1.14.x os: - linux - osx env: - matrix: + jobs: - GOARCH=amd64 - GOARCH=386 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/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/LICENSE b/LICENSE index 1962b4a..e0f34bb 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,6 @@ 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 The excelize Authors. All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/README.md b/README.md index eae0072..b3106df 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ -

Excelize logo

+

Excelize logo

Build Status Code Coverage Go Report Card - GoDoc + go.dev Licenses Donate

@@ -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.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/). +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. @@ -47,8 +52,7 @@ 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 { + if err := f.SaveAs("Book1.xlsx"); err != nil { fmt.Println(err) } } @@ -68,7 +72,7 @@ import ( ) func main() { - f, err := excelize.OpenFile("./Book1.xlsx") + f, err := excelize.OpenFile("Book1.xlsx") if err != nil { fmt.Println(err) return @@ -116,14 +120,12 @@ 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 { + 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 { fmt.Println(err) return } // Save xlsx file by the given path. - err = f.SaveAs("./Book1.xlsx") - if err != nil { + if err := f.SaveAs("Book1.xlsx"); err != nil { fmt.Println(err) } } @@ -144,29 +146,25 @@ import ( ) func main() { - f, err := excelize.OpenFile("./Book1.xlsx") + f, err := excelize.OpenFile("Book1.xlsx") if err != nil { fmt.Println(err) return } // Insert a picture. - err = f.AddPicture("Sheet1", "A2", "./image1.png", "") - if err != nil { + if err := f.AddPicture("Sheet1", "A2", "image.png", ""); err != nil { fmt.Println(err) } // 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 { + if err := f.AddPicture("Sheet1", "D2", "image.jpg", `{"x_scale": 0.5, "y_scale": 0.5}`); err != nil { fmt.Println(err) } // 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 { + 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 { fmt.Println(err) } // Save the xlsx file with the origin path. - err = f.Save() - if err != nil { + if err = f.Save(); err != nil { fmt.Println(err) } } @@ -182,6 +180,4 @@ This program is under the terms of the BSD 3-Clause License. See [https://openso The Excel logo is a trademark of [Microsoft Corporation](https://aka.ms/trademarks-usage). This artwork is an adaptation. -Some struct of XML originally by [tealeg/xlsx](https://github.com/tealeg/xlsx). Licensed under the [BSD 3-Clause License](https://github.com/tealeg/xlsx/blob/master/LICENSE). - gopher.{ai,svg,png} was created by [Takuya Ueda](https://twitter.com/tenntenn). Licensed under the [Creative Commons 3.0 Attributions license](http://creativecommons.org/licenses/by/3.0/). diff --git a/README_zh.md b/README_zh.md index dfed749..deba22a 100644 --- a/README_zh.md +++ b/README_zh.md @@ -1,10 +1,10 @@ -

Excelize logo

+

Excelize logo

Build Status Code Coverage Go Report Card - GoDoc + go.dev Licenses Donate

@@ -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,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 文档的简单例子: @@ -46,8 +52,7 @@ func main() { // 设置工作簿的默认工作表 f.SetActiveSheet(index) // 根据指定路径保存文件 - err := f.SaveAs("./Book1.xlsx") - if err != nil { + if err := f.SaveAs("Book1.xlsx"); err != nil { fmt.Println(err) } } @@ -67,7 +72,7 @@ import ( ) func main() { - f, err := excelize.OpenFile("./Book1.xlsx") + f, err := excelize.OpenFile("Book1.xlsx") if err != nil { fmt.Println(err) return @@ -115,18 +120,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 { + 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 { fmt.Println(err) return } // 根据指定路径保存文件 - err = f.SaveAs("./Book1.xlsx") - if err != nil { + if err := f.SaveAs("Book1.xlsx"); err != nil { fmt.Println(err) } } - ``` ### 向 Excel 文档中插入图片 @@ -144,29 +146,25 @@ import ( ) func main() { - f, err := excelize.OpenFile("./Book1.xlsx") + f, err := excelize.OpenFile("Book1.xlsx") if err != nil { fmt.Println(err) return } // 插入图片 - err = f.AddPicture("Sheet1", "A2", "./image1.png", "") - if err != nil { + if err := f.AddPicture("Sheet1", "A2", "image.png", ""); err != nil { fmt.Println(err) } // 在工作表中插入图片,并设置图片的缩放比例 - err = f.AddPicture("Sheet1", "D2", "./image2.jpg", `{"x_scale": 0.5, "y_scale": 0.5}`) - if err != nil { + if err := f.AddPicture("Sheet1", "D2", "image.jpg", `{"x_scale": 0.5, "y_scale": 0.5}`); err != nil { fmt.Println(err) } // 在工作表中插入图片,并设置图片的打印属性 - 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 { + 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 { fmt.Println(err) } // 保存文件 - err = f.Save() - if err != nil { + if err = f.Save(); err != nil { fmt.Println(err) } } @@ -182,6 +180,4 @@ func main() { Excel 徽标是 [Microsoft Corporation](https://aka.ms/trademarks-usage) 的商标,项目的图片是一种改编。 -本类库中部分 XML 结构体的定义参考了开源项目:[tealeg/xlsx](https://github.com/tealeg/xlsx),遵循 [BSD 3-Clause License](https://github.com/tealeg/xlsx/blob/master/LICENSE) 开源许可协议。 - gopher.{ai,svg,png} 由 [Takuya Ueda](https://twitter.com/tenntenn) 创作,遵循 [Creative Commons 3.0 Attributions license](http://creativecommons.org/licenses/by/3.0/) 创作共用授权条款。 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! diff --git a/adjust.go b/adjust.go index 51db57e..5056839 100644 --- a/adjust.go +++ b/adjust.go @@ -1,15 +1,18 @@ -// 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. // // 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 "strings" +import ( + "errors" + "strings" +) type adjustDirection bool @@ -27,8 +30,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,9 +49,16 @@ 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) + _ = checkRow(xlsx) + + if xlsx.MergeCells != nil && len(xlsx.MergeCells.Cells) == 0 { + xlsx.MergeCells = nil + } + return nil } @@ -71,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) } } } @@ -139,48 +149,103 @@ 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 +// areaRefToCoordinates provides a function to convert area reference to a +// pair of coordinates. +func (f *File) areaRefToCoordinates(ref string) ([]int, error) { + rng := strings.Split(ref, ":") + 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 { + return coordinates, err + } + coordinates[2], coordinates[3], err = CellNameToCoordinates(lastCell) + 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") + } + 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) { + 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 // or deleting rows or columns. func (f *File) adjustMergeCells(xlsx *xlsxWorksheet, dir adjustDirection, num, offset int) error { @@ -188,58 +253,82 @@ func (f *File) adjustMergeCells(xlsx *xlsxWorksheet, dir adjustDirection, num, o return nil } - for i, areaData := range xlsx.MergeCells.Cells { - rng := strings.Split(areaData.Ref, ":") - firstCell := rng[0] - lastCell := rng[1] - - firstCol, firstRow, err := CellNameToCoordinates(firstCell) + 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 } - - 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) - } 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 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 firstCell, err = CoordinatesToCellName(firstCol, firstRow); err != nil { + 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 } - - if lastCell, err = CoordinatesToCellName(lastCol, lastRow); err != nil { - return err - } - - areaData.Ref = firstCell + ":" + lastCell + } + return nil +} + +// 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 +} + +// deleteMergeCell provides a function to delete merged cell by given index. +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) + } +} + +// 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..13e47ff 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) { @@ -67,3 +85,36 @@ 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")) +} + +func TestCoordinatesToAreaRef(t *testing.T) { + f := NewFile() + _, err := f.coordinatesToAreaRef([]int{}) + assert.EqualError(t, err, "coordinates length must be 4") + _, err = f.coordinatesToAreaRef([]int{1, -1, 1, 1}) + assert.EqualError(t, err, "invalid cell coordinates [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}) + 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/calc.go b/calc.go new file mode 100644 index 0000000..bff7866 --- /dev/null +++ b/calc.go @@ -0,0 +1,2630 @@ +// 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 ( + "bytes" + "container/list" + "errors" + "fmt" + "math" + "math/rand" + "reflect" + "strconv" + "strings" + "time" + + "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 +} + +// 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 +// 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 + 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 +// +// 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 := NewStack(), NewStack(), NewStack(), NewStack(), NewStack() + argsList := list.New() + 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, matrix, err := f.parseReference(sheet, token.TValue) + if err != nil { + return efp.Token{TValue: formulaErrorNAME}, err + } + for idx, val := range result { + arg := formulaArg{Value: val} + if idx == 0 { + arg.Matrix = matrix + } + argsList.PushBack(arg) + } + 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() { + argsList.PushBack(formulaArg{ + Value: opfdStack.Pop().(efp.Token).TValue, + }) + } + 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(formulaArg{ + Value: token.TValue, + }) + } + + // 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 { + argsList.PushBack(formulaArg{ + Value: opfdStack.Pop().(efp.Token).TValue, + }) + } + // call formula function to evaluate + 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 + } + argsList.Init() + 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() + } + if opdStack.Len() == 0 { + return efp.Token{}, errors.New("formula not valid") + } + 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 { + 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 { + return err + } + result := 0 - opdVal + opdStack.Push(efp.Token{TValue: fmt.Sprintf("%g", result), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) + } + + if opt.TValue == "+" { + if err := calcAdd(opdStack); err != nil { + return err + } + } + if opt.TValue == "-" && opt.TType == efp.TokenTypeOperatorInfix { + if err := calcSubtract(opdStack); err != nil { + return err + } + } + if opt.TValue == "*" { + if err := calcMultiply(opdStack); err != nil { + return err + } + } + if opt.TValue == "/" { + if err := calcDivide(opdStack); err != nil { + return err + } + } + 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 { + // 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 isOperatorPrefixToken(token) { + if err := f.parseOperatorPrefixToken(optStack, opdStack, token); err != nil { + return err + } + } + 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, matrix [][]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, matrix, err = f.rangeResolver(cellRefs, cellRanges) + 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. +func (f *File) rangeResolver(cellRefs, cellRanges *list.List) (result []string, matrix [][]string, err error) { + // 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 + 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) + prepareValueRange(cr, valueRange) + 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 + } + prepareValueRef(cr, valueRange) + } + // extract value from ranges + if cellRanges.Len() > 0 { + for row := valueRange[0]; row <= valueRange[1]; row++ { + var matrixRow = []string{} + for col := valueRange[2]; col <= valueRange[3]; col++ { + var cell, value string + if cell, err = CoordinatesToCellName(col, row); err != nil { + return + } + 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() { + 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 + +// ABS function returns the absolute value of any supplied number. The syntax +// of the function is: +// +// ABS(number) +// +func (fn *formulaFuncs) ABS(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("ABS requires 1 numeric argument") + return + } + 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)) + 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 argument") + return + } + 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)) + 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 argument") + return + } + 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)) + 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 argument") + return + } + 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)) + 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 argument") + return + } + 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)) + 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 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 + if char == '-' { + prefix = -1 + continue + } + digit, _ = charMap[char] + 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 argument") + return + } + 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)) + 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 argument") + return + } + 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)) + 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 argument") + return + } + 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)) + 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 argument") + return + } + 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)) + 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 + 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 +} + +// 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 + 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 { + err = errors.New("radix must be an integer >= 2 and <= 36") + return + } + if argsList.Len() > 2 { + if minLength, err = strconv.Atoi(argsList.Back().Value.(formulaArg).Value); err != nil { + err = errors.New(formulaErrorVALUE) + return + } + } + result = strconv.FormatInt(int64(number), radix) + if len(result) < minLength { + result = strings.Repeat("0", minLength-len(result)) + result + } + result = strings.ToUpper(result) + 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 + } + 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 { + significance = -1 + } + if argsList.Len() > 1 { + 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("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 + } + 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 { + significance = -1 + } + if argsList.Len() > 1 { + if significance, err = strconv.ParseFloat(argsList.Front().Next().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) + return + } + } + if argsList.Len() == 1 { + result = fmt.Sprintf("%g", math.Ceil(number)) + return + } + if argsList.Len() > 2 { + if mode, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) + return + } + } + val, res := math.Modf(number / significance) + if res != 0 { + if number > 0 { + val++ + } else if mode < 0 { + 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 + } + 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 { + 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 { + err = errors.New(formulaErrorVALUE) + 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 + } + 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) + 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.(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) + if number < chosen { + err = errors.New("COMBINA requires number > number_chosen") + return + } + if number == 0 { + result = "0" + return + } + args := list.New() + args.PushBack(formulaArg{ + Value: fmt.Sprintf("%g", number+chosen-1), + }) + args.PushBack(formulaArg{ + Value: fmt.Sprintf("%g", number-1), + }) + 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 argument") + return + } + 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)) + 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 argument") + return + } + 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)) + 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 argument") + return + } + 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(formulaErrorDIV) + 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 argument") + return + } + 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(formulaErrorDIV) + 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 argument") + return + } + 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(formulaErrorDIV) + return + } + result = fmt.Sprintf("%g", 1/math.Sin(val)) + 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 { + err = errors.New(formulaErrorVALUE) + return + } + if val == 0 { + err = errors.New(formulaErrorDIV) + 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 { + err = errors.New(formulaErrorVALUE) + 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(formulaErrorVALUE) + 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 { + err = errors.New(formulaErrorVALUE) + return + } + if val == 0 { + err = errors.New(formulaErrorDIV) + 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 { + err = errors.New(formulaErrorVALUE) + 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 { + err = errors.New(formulaErrorVALUE) + 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 { + err = errors.New(formulaErrorVALUE) + 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 + } + 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 + } + 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 + 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) + 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 + } + 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 { + significance = -1 + } + if argsList.Len() > 1 { + if significance, err = strconv.ParseFloat(argsList.Front().Next().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) + 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 { + err = errors.New(formulaErrorVALUE) + 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 + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) + 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 { + err = errors.New(formulaErrorVALUE) + 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 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(argsList *list.List) (result string, err error) { + if argsList.Len() == 0 { + err = errors.New("GCD requires at least 1 argument") + return + } + var ( + val float64 + nums = []float64{} + ) + for arg := argsList.Front(); arg != nil; arg = arg.Next() { + token := arg.Value.(formulaArg).Value + if token == "" { + continue + } + if val, err = strconv.ParseFloat(token, 64); err != nil { + err = errors.New(formulaErrorVALUE) + return + } + nums = append(nums, val) + } + 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 +} + +// 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 { + err = errors.New(formulaErrorVALUE) + 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 + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) + 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 { + err = errors.New(formulaErrorVALUE) + 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) + 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: +// +// LCM(number1,[number2],...) +// +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 + } + var ( + val float64 + nums = []float64{} + ) + for arg := argsList.Front(); arg != nil; arg = arg.Next() { + token := arg.Value.(formulaArg).Value + if token == "" { + continue + } + if val, err = strconv.ParseFloat(token, 64); err != nil { + err = errors.New(formulaErrorVALUE) + return + } + nums = append(nums, val) + } + 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 +} + +// 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 { + err = errors.New(formulaErrorVALUE) + 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 + } + 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 + } + } + 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 { + err = errors.New(formulaErrorVALUE) + 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 numMtx = [][]float64{} + var strMtx = argsList.Front().Value.(formulaArg).Matrix + if argsList.Len() < 1 { + return + } + var rows = len(strMtx) + for _, row := range argsList.Front().Value.(formulaArg).Matrix { + 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 +} + +// 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 { + 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 { + 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 + 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 { + 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) { + 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 == "" { + continue + } + if val, err = strconv.ParseFloat(token.Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) + 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 { + err = errors.New(formulaErrorVALUE) + 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 { + err = errors.New(formulaErrorVALUE) + 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: +// +// POWER(number,power) +// +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 + 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 { + err = errors.New(formulaErrorNUM) + return + } + if x == 0 && y < 0 { + err = errors.New(formulaErrorDIV) + return + } + result = fmt.Sprintf("%g", math.Pow(x, y)) + 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(argsList *list.List) (result string, err error) { + val, product := 0.0, 1.0 + 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 { + err = errors.New(formulaErrorVALUE) + return + } + product = product * val + } + result = fmt.Sprintf("%g", product) + 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 { + 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 { + 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 { + err = errors.New(formulaErrorVALUE) + 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 { + 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 { + 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 { + 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 { + 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 +} + +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 { + var significance float64 + 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 { + 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)) + 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 { + 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)) + 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 { + 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)) + 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 { + err = errors.New(formulaErrorVALUE) + 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 { + err = errors.New(formulaErrorVALUE) + 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 +// (zero), the function returns 0. The syntax of the function is: +// +// SIGN(number) +// +func (fn *formulaFuncs) SIGN(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("SIGN requires 1 numeric argument") + return + } + var val float64 + if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) + return + } + if val < 0 { + result = "-1" + return + } + if val > 0 { + result = "1" + return + } + result = "0" + 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 { + err = errors.New(formulaErrorVALUE) + 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 { + err = errors.New(formulaErrorVALUE) + 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: +// +// SQRT(number) +// +func (fn *formulaFuncs) SQRT(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("SQRT requires 1 numeric argument") + return + } + var res float64 + var value = argsList.Front().Value.(formulaArg).Value + if value == "" { + result = "0" + return + } + if res, err = strconv.ParseFloat(value, 64); err != nil { + err = errors.New(formulaErrorVALUE) + return + } + if res < 0 { + err = errors.New(formulaErrorNUM) + return + } + result = fmt.Sprintf("%g", math.Sqrt(res)) + 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 { + err = errors.New(formulaErrorVALUE) + 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: +// +// SUM(number1,[number2],...) +// +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.(formulaArg) + if token.Value == "" { + continue + } + if val, err = strconv.ParseFloat(token.Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) + return + } + sum += val + } + result = fmt.Sprintf("%g", sum) + 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 { + err = errors.New(formulaErrorVALUE) + 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: +// +// 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 { + err = errors.New(formulaErrorVALUE) + 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 { + err = errors.New(formulaErrorVALUE) + 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 { + 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) + } + 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 +} + +// Statistical functions diff --git a/calc_test.go b/calc_test.go new file mode 100644 index 0000000..7592078 --- /dev/null +++ b/calc_test.go @@ -0,0 +1,714 @@ +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) + f.SetCellValue("Sheet1", "B2", 5) + return f + } + + 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", + // 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", + // 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", + "=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)": "-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", + "=_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", + "=COMBIN(0,0)": "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", + "=_xlfn.COMBINA(0,0)": "0", + // 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", + // _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("0x70122",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(0)": "0", + `=GCD("",1)`: "1", + "=GCD(1,0)": "1", + "=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", + "=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", + "=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", + // MOD + "=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", + "=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", + `=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", + `=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", + "=ROMAN(1999,-1)": "MCMXCIX", + "=ROMAN(1999,5)": "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", + "=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", + // SUMSQ + "=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", + // 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() + assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula)) + result, err := f.CalcCellValue("Sheet1", "C1") + assert.NoError(t, err) + assert.Equal(t, expected, result, formula) + } + mathCalcError := map[string]string{ + // ABS + "=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("X")`: "#VALUE!", + // ACOSH + "=ACOSH()": "ACOSH requires 1 numeric argument", + `=ACOSH("X")`: "#VALUE!", + // _xlfn.ACOT + "=_xlfn.ACOT()": "ACOT requires 1 numeric argument", + `=_xlfn.ACOT("X")`: "#VALUE!", + // _xlfn.ACOTH + "=_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("X")`: "#VALUE!", + // ASINH + "=ASINH()": "ASINH requires 1 numeric argument", + `=ASINH("X")`: "#VALUE!", + // ATAN + "=ATAN()": "ATAN requires 1 numeric argument", + `=ATAN("X")`: "#VALUE!", + // ATANH + "=ATANH()": "ATANH requires 1 numeric argument", + `=ATANH("X")`: "#VALUE!", + // ATAN2 + "=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("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("X",1)`: "#VALUE!", + `=_xlfn.COMBINA(-1,"X")`: "#VALUE!", + // COS + "=COS()": "COS requires 1 numeric argument", + `=COS("X")`: "#VALUE!", + // COSH + "=COSH()": "COSH requires 1 numeric argument", + `=COSH("X")`: "#VALUE!", + // _xlfn.COT + "=COT()": "COT requires 1 numeric argument", + `=COT("X")`: "#VALUE!", + "=COT(0)": "#DIV/0!", + // _xlfn.COTH + "=COTH()": "COTH requires 1 numeric argument", + `=COTH("X")`: "#VALUE!", + "=COTH(0)": "#DIV/0!", + // _xlfn.CSC + "=_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("X")`: "#VALUE!", + "=_xlfn.CSCH(0)": "#DIV/0!", + // _xlfn.DECIMAL + "=_xlfn.DECIMAL()": "DECIMAL requires 2 numeric arguments", + `=_xlfn.DECIMAL("X", 2)`: "#VALUE!", + `=_xlfn.DECIMAL(2000, "X")`: "#VALUE!", + // DEGREES + "=DEGREES()": "DEGREES requires 1 numeric argument", + `=DEGREES("X")`: "#VALUE!", + "=DEGREES(0)": "#DIV/0!", + // EVEN + "=EVEN()": "EVEN requires 1 numeric argument", + `=EVEN("X")`: "#VALUE!", + // EXP + "=EXP()": "EXP requires 1 numeric argument", + `=EXP("X")`: "#VALUE!", + // FACT + "=FACT()": "FACT requires 1 numeric argument", + `=FACT("X")`: "#VALUE!", + "=FACT(-1)": "#NUM!", + // FACTDOUBLE + "=FACTDOUBLE()": "FACTDOUBLE requires 1 numeric argument", + `=FACTDOUBLE("X")`: "#VALUE!", + "=FACTDOUBLE(-1)": "#NUM!", + // FLOOR + "=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("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("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("X")`: "#VALUE!", + // MOD + "=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(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("X")`: "#VALUE!", // not support currently + // ODD + "=ODD()": "ODD requires 1 numeric argument", + `=ODD("X")`: "#VALUE!", + // PI + "=PI(1)": "PI accepts no arguments", + // POWER + `=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("X",1)`: "#VALUE!", + `=QUOTIENT(1,"X")`: "#VALUE!", + "=QUOTIENT(1,0)": "#DIV/0!", + "=QUOTIENT(1)": "QUOTIENT requires 2 numeric arguments", + // RADIANS + `=RADIANS("X")`: "#VALUE!", + "=RADIANS()": "RADIANS requires 1 numeric argument", + // RAND + "=RAND(1)": "RAND accepts no arguments", + // RANDBETWEEN + `=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("X",1)`: "#VALUE!", + `=ROUND(1,"X")`: "#VALUE!", + // ROUNDDOWN + "=ROUNDDOWN()": "ROUNDDOWN requires 2 numeric arguments", + `=ROUNDDOWN("X",1)`: "#VALUE!", + `=ROUNDDOWN(1,"X")`: "#VALUE!", + // ROUNDUP + "=ROUNDUP()": "ROUNDUP requires 2 numeric arguments", + `=ROUNDUP("X",1)`: "#VALUE!", + `=ROUNDUP(1,"X")`: "#VALUE!", + // SEC + "=_xlfn.SEC()": "SEC requires 1 numeric argument", + `=_xlfn.SEC("X")`: "#VALUE!", + // _xlfn.SECH + "=_xlfn.SECH()": "SECH requires 1 numeric argument", + `=_xlfn.SECH("X")`: "#VALUE!", + // SIGN + "=SIGN()": "SIGN requires 1 numeric argument", + `=SIGN("X")`: "#VALUE!", + // SIN + "=SIN()": "SIN requires 1 numeric argument", + `=SIN("X")`: "#VALUE!", + // SINH + "=SINH()": "SINH requires 1 numeric argument", + `=SINH("X")`: "#VALUE!", + // SQRT + "=SQRT()": "SQRT requires 1 numeric argument", + `=SQRT("X")`: "#VALUE!", + "=SQRT(-1)": "#NUM!", + // SQRTPI + "=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("X")`: "#VALUE!", + // SUMSQ + `=SUMSQ("X")`: "#VALUE!", + // TAN + "=TAN()": "TAN requires 1 numeric argument", + `=TAN("X")`: "#VALUE!", + // TANH + "=TANH()": "TANH requires 1 numeric argument", + `=TANH("X")`: "#VALUE!", + // TRUNC + "=TRUNC()": "TRUNC requires at least 1 argument", + `=TRUNC("X")`: "#VALUE!", + `=TRUNC(1,"X")`: "#VALUE!", + } + 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, formula) + } + + referenceCalc := map[string]string{ + // MDETERM + "=MDETERM(A1:B2)": "-3", + // PRODUCT + "=PRODUCT(Sheet1!A1:Sheet1!A1:A2,A2)": "4", + // SUM + "=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.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, formula) + } + + 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 { + 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, 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") + 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/calcchain.go b/calcchain.go index ce679e5..f50fb1d 100644 --- a/calcchain.go +++ b/calcchain.go @@ -1,24 +1,34 @@ -// 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. // // 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 ( + "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 } @@ -56,7 +66,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/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.go b/cell.go index 44a590c..97feb0a 100644 --- a/cell.go +++ b/cell.go @@ -1,11 +1,11 @@ -// 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. // // 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 @@ -47,7 +47,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 @@ -85,7 +86,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 } @@ -97,7 +99,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 } @@ -133,28 +135,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 { @@ -169,11 +193,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 { @@ -188,15 +216,20 @@ func (f *File) SetCellBool(sheet, axis string, value bool) error { return err } cellData.S = f.prepareCellStyle(xlsx, col, cellData.S) - cellData.T = "b" - if value { - cellData.V = "1" - } else { - cellData.V = "0" - } + cellData.T, cellData.V = setCellBool(value) return err } +func setCellBool(value bool) (t string, v string) { + t = "b" + if value { + v = "1" + } else { + v = "0" + } + return +} + // SetCellFloat sets a floating point value into a cell. The prec parameter // specifies how many places after the decimal will be shown while -1 is a // special value that will use as many decimal places as necessary to @@ -218,11 +251,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 { @@ -236,21 +273,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 space(s) character detection. - if len(value) > 0 && value[0] == 32 { - cellData.XMLSpace = xml.Attr{ + // Leading and ending space(s) character detection. + if len(value) > 0 && (value[0] == 32 || value[len(value)-1] == 32) { + 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 @@ -265,11 +306,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) { @@ -284,9 +329,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 { rwMutex.Lock() defer rwMutex.Unlock() xlsx, err := f.workSheetReader(sheet) @@ -299,7 +350,7 @@ func (f *File) SetCellFormula(sheet, axis, formula string) error { } if formula == "" { cellData.F = nil - f.deleteCalcChain(f.GetSheetIndex(sheet), axis) + f.deleteCalcChain(f.getSheetID(sheet), axis) return err } @@ -308,6 +359,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 } @@ -391,7 +453,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{ @@ -406,64 +470,170 @@ 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: +// SetCellRichText provides a function to set cell with rich text by given +// worksheet. For example, set rich text on the A1 cell of the worksheet named +// Sheet1: // -// err := f.MergeCell("Sheet1", "D3", "E9") +// package main // -// 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) +// 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 } - - vcol, vrow, err := CellNameToCoordinates(vcell) + cellData, col, _, err := f.prepareCell(ws, sheet, cell) if err != nil { return err } - - if hcol == vcol && hrow == vrow { - return err - } - - // Correct the coordinate area, such correct C1:B3 to B1:C3. - if vcol < hcol { - hcol, vcol = vcol, hcol - } - - if vrow < hrow { - hrow, vrow = vrow, hrow - } - - hcell, _ = CoordinatesToCellName(hcol, hrow) - vcell, _ = CoordinatesToCellName(vcol, vrow) - - 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 - } + 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" } - xlsx.MergeCells.Cells = append(xlsx.MergeCells.Cells, &xlsxMergeCell{Ref: ref}) - } else { - xlsx.MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: hcell + ":" + vcell}}} + 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 } @@ -488,12 +658,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 } @@ -571,9 +743,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 } @@ -597,7 +769,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 } @@ -611,7 +783,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 @@ -621,11 +793,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 + } - firstCol, firstRow, _ := CellNameToCoordinates(rng[0]) - lastCol, lastRow, _ := CellNameToCoordinates(rng[1]) + return cellInRef([]int{col, row}, coordinates), err +} - return col >= firstCol && col <= lastCol && row >= firstRow && row <= lastRow, err +// 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] +} + +// 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 d4a5b02..45e2f24 100644 --- a/cell_test.go +++ b/cell_test.go @@ -2,12 +2,16 @@ package excelize import ( "fmt" + "path/filepath" + "strconv" "testing" + "time" "github.com/stretchr/testify/assert" ) func TestCheckCellInArea(t *testing.T) { + f := NewFile() expectedTrueCellInAreaList := [][2]string{ {"c2", "A1:AAZ32"}, {"B9", "A1:B9"}, @@ -17,7 +21,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) @@ -32,13 +36,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) } @@ -47,8 +55,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") @@ -59,7 +67,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") @@ -67,17 +75,44 @@ 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") }) + 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) { + // Test get cell formula on not exist worksheet. + f := NewFile() + _, err := f.GetCellFormula("SheetN", "A1") + assert.EqualError(t, err, "sheet SheetN is not exist") + + // Test get cell formula on no formula cell. + assert.NoError(t, f.SetCellValue("Sheet1", "A1", true)) + _, err = f.GetCellFormula("Sheet1", "A1") + assert.NoError(t, err) } 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 @@ -88,9 +123,103 @@ 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++ { - f.SetCellValue("Sheet1", fmt.Sprint(cols[j], i), values[j]) + if err := f.SetCellValue("Sheet1", cols[j]+strconv.Itoa(i), values[j]); err != nil { + b.Error(err) + } } } } + +func TestOverflowNumericCell(t *testing.T) { + f, err := OpenFile(filepath.Join("test", "OverflowNumericCell.xlsx")) + if !assert.NoError(t, err) { + t.FailNow() + } + val, err := f.GetCellValue("Sheet1", "A1") + assert.NoError(t, err) + // 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/chart.go b/chart.go index d669a47..8fa0b5d 100644 --- a/chart.go +++ b/chart.go @@ -1,159 +1,255 @@ -// 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. // // 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/json" "encoding/xml" + "errors" + "fmt" "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" + PieOfPieChart = "pieOfPie" + BarOfPieChart = "barOfPie" + Radar = "radar" + Scatter = "scatter" + Surface3D = "surface3D" + WireframeSurface3D = "wireframeSurface3D" + Contour = "contour" + WireframeContour = "wireframeContour" + Bubble = "bubble" + Bubble3D = "bubble3D" ) // 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, + PieOfPieChart: 0, + BarOfPieChart: 0, + Radar: 0, + Scatter: 0, + Surface3D: 15, + WireframeSurface3D: 15, + Contour: 90, + WireframeContour: 90, } 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, + PieOfPieChart: 0, + BarOfPieChart: 0, + Radar: 0, + Scatter: 0, + Surface3D: 20, + WireframeSurface3D: 20, + 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, - 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, + plotAreaChartOverlap = map[string]int{ + BarStacked: 100, + BarPercentStacked: 100, + ColStacked: 100, + ColPercentStacked: 100, + } + chartView3DPerspective = map[string]int{ + Contour: 0, + WireframeContour: 0, } 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, + PieOfPieChart: 0, + BarOfPieChart: 0, + Radar: 0, + Scatter: 0, + Surface3D: 0, + WireframeSurface3D: 0, + Contour: 0, + Bubble: 0, + Bubble3D: 0, } chartLegendPosition = map[string]string{ "bottom": "b", @@ -163,96 +259,196 @@ 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", + PieOfPieChart: "General", + BarOfPieChart: "General", + Radar: "General", + Scatter: "General", + Surface3D: "General", + WireframeSurface3D: "General", + Contour: "General", + WireframeContour: "General", + Bubble: "General", + Bubble3D: "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", + PieOfPieChart: "between", + BarOfPieChart: "between", + Radar: "between", + Scatter: "between", + Surface3D: "midCat", + WireframeSurface3D: "midCat", + Contour: "midCat", + WireframeContour: "midCat", + Bubble: "midCat", + Bubble3D: "midCat", } 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", @@ -266,6 +462,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 @@ -301,7 +501,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 // @@ -321,47 +521,74 @@ 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 { +// 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 { // fmt.Println(err) // return // } // // Save xlsx file by the given path. -// err = xlsx.SaveAs("./Book1.xlsx") -// if err != nil { +// if err := f.SaveAs("Book1.xlsx"); err != nil { // fmt.Println(err) // } // } // // 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 +// pieOfPie | pie of pie chart +// barOfPie | bar of pie chart +// radar | radar chart +// scatter | scatter chart +// surface3D | 3D surface chart +// wireframeSurface3D | 3D wireframe surface chart +// contour | contour chart +// 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. // @@ -370,6 +597,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 // @@ -377,6 +605,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 @@ -433,12 +663,32 @@ 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 +// tick_label_skip // reverse_order // maximum // minimum // +// The properties of y_axis that can be set are: +// +// major_grid_lines +// minor_grid_lines +// major_unit +// reverse_order +// maximum +// minimum +// +// major_grid_lines: Specifies major gridlines. +// +// 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. @@ -447,13 +697,45 @@ 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. // -func (f *File) AddChart(sheet, cell, format string) error { - formatSet, err := parseFormatChartSet(format) +// 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 +// +// 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"} +// 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}}`, `{"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 { +// fmt.Println(err) +// return +// } +// // Save xlsx file by the given path. +// if err := f.SaveAs("Book1.xlsx"); err != nil { +// fmt.Println(err) +// } +// } +// +func (f *File) AddChart(sheet, cell, format string, combo ...string) error { + // Read sheet data. + xlsx, err := f.workSheetReader(sheet) if err != nil { return err } - // Read sheet data. - xlsx, err := f.workSheetReader(sheet) + formatSet, comboCharts, err := f.getFormatChart(format, combo) if err != nil { return err } @@ -462,17 +744,112 @@ 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 } - f.addChart(formatSet) + f.addChart(formatSet, comboCharts) f.addContentTypePart(chartID, "chart") f.addContentTypePart(drawingID, "drawings") 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) != -1 { + return errors.New("the same name worksheet already exists") + } + formatSet, comboCharts, err := f.getFormatChart(format, combo) + if err != nil { + return err + } + cs := xlsxChartsheet{ + SheetViews: []*xlsxChartsheetViews{{ + SheetView: []*xlsxChartsheetView{{ZoomScaleAttr: 100, ZoomToFitAttr: true}}}, + }, + } + f.SheetCount++ + 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" + 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) + 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) + 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) { + 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, "Chart") +} + // countCharts provides a function to get chart files count storage in the // folder xl/charts. func (f *File) countCharts() int { @@ -485,823 +862,12 @@ func (f *File) countCharts() int { 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. - rID := f.addSheetRelationships(sheet, SourceRelationshipDrawingML, sheetRelationshipsDrawingXML, "") - f.addSheetDrawing(sheet, rID) +// 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 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) { - count := f.countCharts() - xlsxChartSpace := xlsxChartSpace{ - XMLNSc: NameSpaceDrawingMLChart, - XMLNSa: NameSpaceDrawingML, - XMLNSr: SourceRelationship, - XMLNSc16r2: SourceRelationshipChart201506, - Date1904: &attrValBool{Val: false}, - Lang: &attrValString{Val: "en-US"}, - RoundedCorners: &attrValBool{Val: 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: 65000, - }, - LumOff: &attrValInt{ - Val: 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", - }, - }, - }, - }, - View3D: &cView3D{ - RotX: &attrValInt{Val: chartView3DRotX[formatSet.Type]}, - RotY: &attrValInt{Val: chartView3DRotY[formatSet.Type]}, - DepthPercent: &attrValInt{Val: chartView3DDepthPercent[formatSet.Type]}, - RAngAx: &attrValInt{Val: chartView3DRAngAx[formatSet.Type]}, - }, - Floor: &cThicknessSpPr{ - Thickness: &attrValInt{Val: 0}, - }, - SideWall: &cThicknessSpPr{ - Thickness: &attrValInt{Val: 0}, - }, - BackWall: &cThicknessSpPr{ - Thickness: &attrValInt{Val: 0}, - }, - PlotArea: &cPlotArea{}, - Legend: &cLegend{ - LegendPos: &attrValString{Val: chartLegendPosition[formatSet.Legend.Position]}, - Overlay: &attrValBool{Val: false}, - }, - - PlotVisOnly: &attrValBool{Val: false}, - DispBlanksAs: &attrValString{Val: formatSet.ShowBlanksAs}, - ShowDLblsOverMax: &attrValBool{Val: 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: 15000, - }, - LumOff: &attrValInt{ - Val: 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, - 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, - } - xlsxChartSpace.Chart.PlotArea = plotAreaFunc[formatSet.Type](formatSet) - - 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: "col", - }, - Grouping: &attrValString{ - Val: "clustered", - }, - VaryColors: &attrValBool{ - Val: true, - }, - Ser: f.drawChartSeries(formatSet), - DLbls: f.drawChartDLbls(formatSet), - AxID: []*attrValInt{ - {Val: 754001152}, - {Val: 753999904}, - }, - } - var ok bool - c.BarDir.Val, ok = plotAreaChartBarDir[formatSet.Type] - if !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" { - c.Overlap = &attrValInt{Val: 100} - } - 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, - }, - "col": { - BarChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "colStacked": { - BarChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "colPercentStacked": { - BarChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "col3DClustered": { - Bar3DChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "col3D": { - Bar3DChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "col3DStacked": { - Bar3DChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "col3DPercentStacked": { - Bar3DChart: &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: true, - }, - Ser: f.drawChartSeries(formatSet), - HoleSize: &attrValInt{Val: 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: plotAreaChartGrouping[formatSet.Type], - }, - VaryColors: &attrValBool{ - Val: false, - }, - Ser: f.drawChartSeries(formatSet), - DLbls: f.drawChartDLbls(formatSet), - Smooth: &attrValBool{ - Val: false, - }, - AxID: []*attrValInt{ - {Val: 754001152}, - {Val: 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: 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: true, - }, - Ser: f.drawChartSeries(formatSet), - }, - } -} - -// 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: "marker", - }, - VaryColors: &attrValBool{ - Val: false, - }, - Ser: f.drawChartSeries(formatSet), - DLbls: f.drawChartDLbls(formatSet), - AxID: []*attrValInt{ - {Val: 754001152}, - {Val: 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: "smoothMarker", // line,lineMarker,marker,none,smooth,smoothMarker - }, - VaryColors: &attrValBool{ - Val: false, - }, - Ser: f.drawChartSeries(formatSet), - DLbls: f.drawChartDLbls(formatSet), - AxID: []*attrValInt{ - {Val: 754001152}, - {Val: 753999904}, - }, - }, - CatAx: f.drawPlotAreaCatAx(formatSet), - ValAx: f.drawPlotAreaValAx(formatSet), - } -} - -// 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: k}, - Order: &attrValInt{Val: k}, - 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), - }) - } - 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: 25400, - Cap: "rnd", // rnd, sq, flat - 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] -} - -// 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: i}, - Bubble3D: &attrValBool{Val: 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{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} - 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{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] -} - -// 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{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] -} - -// 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: "circle"}, - Size: &attrValInt{Val: 5}, - 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{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] -} - -// 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{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} - 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{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} - return chartSeriesYVal[formatSet.Type] -} - -// 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}, - } -} - -// 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{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] -} - -// 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} - if formatSet.XAxis.Minimum == 0 { - min = nil - } - if formatSet.XAxis.Maximum == 0 { - max = nil - } - return []*cAxs{ - { - AxID: &attrValInt{Val: 754001152}, - Scaling: &cScaling{ - Orientation: &attrValString{Val: orientation[formatSet.XAxis.ReverseOrder]}, - Max: max, - Min: min, - }, - Delete: &attrValBool{Val: false}, - AxPos: &attrValString{Val: catAxPos[formatSet.XAxis.ReverseOrder]}, - NumFmt: &cNumFmt{ - FormatCode: "General", - SourceLinked: true, - }, - MajorTickMark: &attrValString{Val: "none"}, - MinorTickMark: &attrValString{Val: "none"}, - TickLblPos: &attrValString{Val: "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}, - }, - } -} - -// 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} - if formatSet.YAxis.Minimum == 0 { - min = nil - } - if formatSet.YAxis.Maximum == 0 { - max = nil - } - return []*cAxs{ - { - AxID: &attrValInt{Val: 753999904}, - Scaling: &cScaling{ - Orientation: &attrValString{Val: orientation[formatSet.YAxis.ReverseOrder]}, - Max: max, - Min: min, - }, - Delete: &attrValBool{Val: false}, - AxPos: &attrValString{Val: valAxPos[formatSet.YAxis.ReverseOrder]}, - NumFmt: &cNumFmt{ - FormatCode: chartValAxNumFmtFormatCode[formatSet.Type], - SourceLinked: true, - }, - MajorTickMark: &attrValString{Val: "none"}, - MinorTickMark: &attrValString{Val: "none"}, - TickLblPos: &attrValString{Val: "nextTo"}, - SpPr: f.drawPlotAreaSpPr(), - TxPr: f.drawPlotAreaTxPr(), - CrossAx: &attrValInt{Val: 754001152}, - Crosses: &attrValString{Val: "autoZero"}, - CrossBetween: &attrValString{Val: chartValAxCrossBetween[formatSet.Type]}, - }, - } -} - -// 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: 15000}, - LumOff: &attrValInt{Val: 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: 15000}, - LumOff: &attrValInt{Val: 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) { - cNvPrID := 1 - if f.Drawings[path] == nil { - content := xlsxWsDr{} - content.A = NameSpaceDrawingML - content.Xdr = NameSpaceDrawingMLSpreadSheet - _, ok := f.XLSX[path] - if ok { // Append Model - 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, - GraphicFrame: v.Content, - }) - } - for _, v := range decodeWsDr.TwoCellAnchor { - content.TwoCellAnchor = append(content.TwoCellAnchor, &xdrCellAnchor{ - EditAs: v.EditAs, - GraphicFrame: v.Content, - }) - } - } - f.Drawings[path] = &content - } - return f.Drawings[path], cNvPrID -} - -// 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: f.countCharts() + f.countMedia() + 1, - 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 + return int(12700 * pt) } diff --git a/chart_test.go b/chart_test.go index 98baedd..b35cb98 100644 --- a/chart_test.go +++ b/chart_test.go @@ -3,6 +3,8 @@ package excelize import ( "bytes" "encoding/xml" + "fmt" + "path/filepath" "testing" "github.com/stretchr/testify/assert" @@ -10,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", @@ -21,7 +23,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{ @@ -36,29 +38,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":"Fruit 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) { @@ -101,3 +96,160 @@ 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":"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 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"}`)) + 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","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"}`)) + 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":"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"}`)) + // 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", "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}}`)) + // 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}}`, 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"}, + "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}}`, 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 + 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 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"}}`)) + // Test set the chartsheet as active sheet + var sheetIdx int + for idx, sheetName := range f.GetSheetList() { + if sheetName != "Chart1" { + continue + } + sheetIdx = idx + } + f.SetActiveSheet(sheetIdx) + + // 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") + + 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) + 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 on no chart worksheet. + assert.NoError(t, NewFile().DeleteChart("Sheet1", "A1")) +} 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/col.go b/col.go index 6b73fdc..6f76800 100644 --- a/col.go +++ b/col.go @@ -1,15 +1,21 @@ -// 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. // // 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 "math" +import ( + "errors" + "math" + "strings" + + "github.com/mohae/deepcopy" +) // Define the default cell size and EMU unit of measurement. const ( @@ -22,7 +28,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 @@ -48,43 +54,64 @@ 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 } + colData := xlsxCol{ + Min: min, + Max: max, + Width: 9, // default width + Hidden: !visible, + CustomWidth: true, + } if xlsx.Cols == nil { cols := xlsxCols{} cols.Col = append(cols.Col, colData) xlsx.Cols = &cols - return err + return nil } - 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 + 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 @@ -116,12 +143,15 @@ func (f *File) GetColOutlineLevel(sheet, col string) (uint8, error) { } // 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 @@ -142,19 +172,76 @@ 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 } +// 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{} + } + 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 +} + // SetColWidth provides a function to set the width of a single column or // multiple columns. For example: // @@ -184,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 new file mode 100644 index 0000000..fcb1619 --- /dev/null +++ b/col_test.go @@ -0,0 +1,211 @@ +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() + 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 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", "V:F", 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 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 "*"`) + + 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"))) + }) + + t.Run("TestBook3", func(t *testing.T) { + f, err := prepareTestBook3() + assert.NoError(t, err) + visible, err := f.GetColVisible("Sheet1", "B") + assert.Equal(t, true, visible) + assert.NoError(t, err) + }) +} + +func TestOutlineLevel(t *testing.T) { + f := NewFile() + 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)) + + 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)) + 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", "*") + 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(7), 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) + + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestOutlineLevel.xlsx"))) + + f, err = OpenFile(filepath.Join("test", "Book1.xlsx")) + assert.NoError(t, err) + assert.NoError(t, 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() + 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. + 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 "*"`) + + // 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(0) + + fillCells(f, sheet1, 10, 10) + + 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")) + + // 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(0) + + fillCells(f, sheet1, 10, 15) + + 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")) + + 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")) + + // 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/comment.go b/comment.go index 79f6fec..e224502 100644 --- a/comment.go +++ b/comment.go @@ -1,18 +1,22 @@ -// 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. // // 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 ( + "bytes" "encoding/json" "encoding/xml" "fmt" + "io" + "log" + "path/filepath" "strconv" "strings" ) @@ -32,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{} @@ -42,8 +46,13 @@ 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 + if text.T != nil { + sheetComment.Text += text.T.Val + } } sheetComments = append(sheetComments, sheetComment) } @@ -54,10 +63,10 @@ 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" - if sheetRels := f.workSheetRelsReader(rels); sheetRels != nil { +// 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 { return v.Target @@ -95,12 +104,12 @@ 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, "") + 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) } 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 { @@ -114,6 +123,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 } @@ -239,6 +249,7 @@ func (f *File) addComment(commentsXML, cell string, formatSet *formatComment) { }, } } + defaultFont := f.GetDefaultFont() cmt := xlsxComment{ Ref: cell, AuthorID: 0, @@ -247,25 +258,25 @@ 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: "Calibri"}, - Family: &attrValInt{Val: 2}, + RFont: &attrValString{Val: stringPtr(defaultFont)}, + Family: &attrValInt{Val: intPtr(2)}, }, - T: a, + T: &xlsxT{Val: a}, }, { RPr: &xlsxRPr{ - Sz: &attrValFloat{Val: 9}, + Sz: &attrValFloat{Val: float64Ptr(9)}, Color: &xlsxColor{ Indexed: 81, }, - RFont: &attrValString{Val: "Calibri"}, - Family: &attrValInt{Val: 2}, + RFont: &attrValString{Val: stringPtr(defaultFont)}, + Family: &attrValInt{Val: intPtr(2)}, }, - T: t, + T: &xlsxT{Val: t}, }, }, }, @@ -277,24 +288,36 @@ 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 // 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] @@ -314,12 +337,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/comment_test.go b/comment_test.go new file mode 100644 index 0000000..955d4e8 --- /dev/null +++ b/comment_test.go @@ -0,0 +1,63 @@ +// 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 ( + "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") + // 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) { + 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.go b/datavalidation.go index 8fb9623..1aeb1dc 100644 --- a/datavalidation.go +++ b/datavalidation.go @@ -1,11 +1,11 @@ -// 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. // // 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 @@ -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 @@ -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 } @@ -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 afb659c..d70b874 100644 --- a/datavalidation_test.go +++ b/datavalidation_test.go @@ -1,16 +1,17 @@ -// 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. // // 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 ( "path/filepath" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -19,57 +20,49 @@ import ( func TestDataValidation(t *testing.T) { resultFile := filepath.Join("test", "TestDataValidation.xlsx") - xlsx := NewFile() + f := NewFile() 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") - xlsx.AddDataValidation("Sheet1", dvRange) - if !assert.NoError(t, xlsx.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") - xlsx.AddDataValidation("Sheet1", dvRange) - if !assert.NoError(t, xlsx.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"}) - xlsx.AddDataValidation("Sheet1", dvRange) - if !assert.NoError(t, xlsx.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") - xlsx := NewFile() - xlsx.SetCellStr("Sheet1", "E1", "E1") - xlsx.SetCellStr("Sheet1", "E2", "E2") - xlsx.SetCellStr("Sheet1", "E3", "E3") + f := NewFile() + 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") - xlsx.AddDataValidation("Sheet1", dvRange) - if !assert.NoError(t, xlsx.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)) @@ -78,11 +71,34 @@ 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") - xlsx.AddDataValidation("Sheet1", dvRange) - if !assert.NoError(t, xlsx.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) + 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") +} + +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/date.go b/date.go index b49a695..172c32c 100644 --- a/date.go +++ b/date.go @@ -1,11 +1,11 @@ -// 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. // // 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 @@ -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/docProps.go b/docProps.go new file mode 100644 index 0000000..a61ee71 --- /dev/null +++ b/docProps.go @@ -0,0 +1,156 @@ +// 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" + "fmt" + "io" + "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) (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, err = &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, + }, 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", + } + 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) + } + } + 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 +} + +// GetDocProps provides a function to get document core properties. +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 + } + ret, err = &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 + + return +} diff --git a/docProps_test.go b/docProps_test.go new file mode 100644 index 0000000..ef930ae --- /dev/null +++ b/docProps_test.go @@ -0,0 +1,69 @@ +// 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 ( + "path/filepath" + "testing" + + "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) { + 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.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) { + 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.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/drawing.go b/drawing.go new file mode 100644 index 0000000..7c09d4d --- /dev/null +++ b/drawing.go @@ -0,0 +1,1305 @@ +// 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" + "fmt" + "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 +} + +// 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 string) { + sheetRelationshipsDrawingXML := "../drawings/drawing" + strconv.Itoa(drawingID) + ".xml" + // 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 +} + +// 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{ + OfPieChart: &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{ + OfPieChart: &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 +} + +// 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(drawingXML string, rID int, formatSet *formatPicture) { + 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 +} + +// 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(strings.NewReader("" + 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/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/errors.go b/errors.go index 3404c7e..5576ecd 100644 --- a/errors.go +++ b/errors.go @@ -1,11 +1,11 @@ -// 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. // // 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 @@ -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") +} diff --git a/excelize.go b/excelize.go index 41fba37..3fd25aa 100644 --- a/excelize.go +++ b/excelize.go @@ -1,11 +1,13 @@ -// 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. // 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. +// 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 @@ -14,22 +16,25 @@ import ( "archive/zip" "bytes" "encoding/xml" + "errors" "fmt" "io" "io/ioutil" "os" + "path" "strconv" "strings" + + "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 CalcChain *xlsxCalcChain Comments map[string]*xlsxComments ContentTypes *xlsxTypes - DrawingRels map[string]*xlsxWorkbookRels Drawings map[string]*xlsxWsDr Path string SharedStrings *xlsxSST @@ -40,13 +45,15 @@ 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 + CharsetReader charsetTranscoderFn } -// OpenFile take the name of an XLSX file and returns a populated XLSX file -// struct for it. +type charsetTranscoderFn func(charset string, input io.Reader) (rdr io.Reader, err error) + +// 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 { @@ -61,7 +68,23 @@ func OpenFile(filename string) (*File, error) { return f, nil } -// OpenReader take an io.Reader and return a populated XLSX file. +// newFile is 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 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 { @@ -70,6 +93,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 } @@ -77,18 +111,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), - 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), - XLSX: file, - } + f := newFile() + f.SheetCount, f.XLSX = sheetCount, file f.CalcChain = f.calcChainReader() f.sheetMap = f.getSheetMap() f.Styles = f.stylesReader() @@ -96,6 +120,17 @@ 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. @@ -105,34 +140,50 @@ func (f *File) setDefaultTimeStyle(sheet, axis string, format int) error { return err } if s == 0 { - style, _ := f.NewStyle(`{"number_format": ` + strconv.Itoa(format) + `}`) - f.SetCellStyle(sheet, axis, axis, style) + style, _ := f.NewStyle(&Style{NumFmt: format}) + _ = f.SetCellStyle(sheet, axis, axis, style) } return err } // 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 { + 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 { + 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 @@ -145,32 +196,50 @@ 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 } -// 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(``) - workbookMarshal = bytes.Replace(workbookMarshal, oldXmlns, newXmlns, -1) - return workbookMarshal +// 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) + if rels == nil { + rels = &xlsxRelationships{} + } + 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)) + rels.Relationships = append(rels.Relationships, xlsxRelationship{ + ID: ID.String(), + Type: relType, + Target: target, + TargetMode: targetMode, + }) + f.Relationships[relPath] = rels + return rID +} + +// replaceRelationshipsNameSpaceBytes provides a function to replace +// XML tags to self-closing for compatible Microsoft Office Excel 2007. +func replaceRelationshipsNameSpaceBytes(contentMarshal []byte) []byte { + var oldXmlns = stringToBytes(` xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">`) + var newXmlns = []byte(templateNamespaceIDMap) + return bytesReplace(contentMarshal, oldXmlns, newXmlns, -1) } // UpdateLinkedValue fix linked values within a spreadsheet are not updating in @@ -199,7 +268,10 @@ func replaceWorkSheetsRelationshipsNameSpaceBytes(workbookMarshal []byte) []byte // // func (f *File) UpdateLinkedValue() error { - for _, name := range f.GetSheetMap() { + wb := f.workbookReader() + // recalculate formulas + wb.CalcPr = nil + for _, name := range f.GetSheetList() { xlsx, err := f.workSheetReader(name) if err != nil { return err @@ -216,48 +288,74 @@ 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 +// AddVBAProject provides the method to add vbaProject.bin file which contains +// functions and/or macros. The file extension should be .xlsm. For example: +// +// if err := f.SetSheetPrOptions("Sheet1", excelize.CodeName("Sheet1")); err != nil { +// fmt.Println(err) +// } +// if err := f.AddVBAProject("vbaProject.bin"); err != nil { +// fmt.Println(err) +// } +// if err := f.SaveAs("macros.xlsm"); 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 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}) + if path.Ext(bin) != ".bin" { + return errors.New("unsupported VBA project extension") + } + f.setContentTypePartVBAProjectExtensions() + wb := f.relsReader("xl/_rels/workbook.xml.rels") + 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 } } - - return mergeCells, err + rID++ + if !ok { + wb.Relationships = append(wb.Relationships, xlsxRelationship{ + ID: "rId" + strconv.Itoa(rID), + Target: "vbaProject.bin", + Type: SourceRelationshipVBAProject, + }) + } + file, _ := ioutil.ReadFile(bin) + f.XLSX["xl/vbaProject.bin"] = file + return 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] +// 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 = ContentTypeMacro + } + } + if !ok { + content.Defaults = append(content.Defaults, xlsxDefault{ + Extension: "bin", + ContentType: ContentTypeVBA, + }) + } } 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/excelize_test.go b/excelize_test.go index 87fd806..8ee8051 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -1,12 +1,16 @@ package excelize import ( + "bytes" + "compress/gzip" + "encoding/xml" "fmt" "image/color" _ "image/gif" _ "image/jpeg" _ "image/png" "io/ioutil" + "math" "os" "path/filepath" "strconv" @@ -20,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) @@ -35,28 +38,28 @@ 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)), `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"`) - 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. @@ -76,8 +79,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. @@ -87,10 +92,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"))) @@ -127,33 +136,34 @@ 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. - 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. 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"))) } @@ -182,6 +192,36 @@ 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") + _, 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") + + // 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) { // Test write file with broken file struct. f := File{} @@ -192,14 +232,14 @@ 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) { // 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) }) @@ -218,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. @@ -244,30 +284,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() @@ -292,13 +308,18 @@ 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() + _, 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() + _, 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"`) } func TestGetCellHyperLink(t *testing.T) { @@ -319,6 +340,24 @@ 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() + _, err = f.workSheetReader("Sheet1") + assert.NoError(t, err) + 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) { @@ -327,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"`) @@ -340,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"))) } @@ -381,76 +420,110 @@ 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() +// 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.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") + f := NewFile() - 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()) + 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") } - // Test get merged cells on not exists worksheet. - _, err = f.GetMergeCells("SheetN") - assert.EqualError(t, err, "sheet SheetN is not exist") + // 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 + assert.NoError(t, f.SetCellStr("Sheet1", "A2", "Average")) + + // Line 3 contains the average that was calculated in Go + assert.NoError(t, f.SetCellStr("Sheet1", "A3", "Average (calculated)")) + + // Line 4 contains the results of the array function that calculates the standard deviation + assert.NoError(t, f.SetCellStr("Sheet1", "A4", "Std. deviation")) + + // Line 5 contains the standard deviations calculated in Go + assert.NoError(t, f.SetCellStr("Sheet1", "A5", "Std. deviation (calculated)")) + + 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 + 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) + + 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) + 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) + + assert.NoError(t, f.SetCellInt("Sheet1", calcAvgCell, average(i))) + assert.NoError(t, f.SetCellInt("Sheet1", calcStdevCell, stdev(i))) + + // Average can be done with AVERAGEIF + assert.NoError(t, 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) { @@ -507,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() } @@ -552,9 +663,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) { @@ -581,8 +692,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) { @@ -605,8 +716,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) { @@ -638,8 +749,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) @@ -696,7 +807,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() } @@ -724,7 +835,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() } @@ -770,8 +881,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"))) }) } @@ -782,52 +893,14 @@ 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"))) } -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) { @@ -835,9 +908,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(0, idx)) - f.SetCellValue("Sheet4", "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) @@ -851,258 +924,61 @@ 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(-1, -2), "sheet is not exist") + if !assert.EqualError(t, f.CopySheet(-1, -2), "invalid worksheet index") { t.FailNow() } 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() - } - - 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", "") - - assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddShape.xlsx"))) -} - -func TestAddComments(t *testing.T) { - f, err := prepareTestBook1() - if !assert.NoError(t, err) { - t.FailNow() - } - - s := strings.Repeat("c", 32768) - f.AddComment("Sheet1", "A30", `{"author":"`+s+`","text":"`+s+`"}`) - f.AddComment("Sheet2", "B7", `{"author":"Excelize: ","text":"This is a comment."}`) - - 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)) + assert.Equal(t, "", f.getSheetComments("sheet0")) } -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 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 TestInsertCol(t *testing.T) { +func TestSetActiveSheet(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"))) + 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) + f.Sheet["xl/worksheets/sheet1.xml"].SheetViews = nil + f.SetActiveSheet(1) } -func TestRemoveCol(t *testing.T) { +func TestSetSheetVisible(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"))) + f.WorkBook.Sheets.Sheet[0].Name = "SheetN" + assert.EqualError(t, f.SetSheetVisible("Sheet1", false), "sheet SheetN is not exist") } -func TestSetPane(t *testing.T) { +func TestGetActiveSheetIndex(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", "") + f.WorkBook.BookViews = nil + assert.Equal(t, 0, f.GetActiveSheetIndex()) +} - assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetPane.xlsx"))) +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() + _, err := f.getSheetView("SheetN", 0) + assert.EqualError(t, err, "sheet SheetN is not exist") } func TestConditionalFormat(t *testing.T) { f := NewFile() - sheet1 := f.GetSheetName(1) + sheet1 := f.GetSheetName(0) 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}}`) @@ -1122,32 +998,43 @@ 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. - 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)) - // Test set invalid format set in conditional format - f.SetConditionalFormat(sheet1, "L1:L10", "") + assert.NoError(t, f.SetConditionalFormat(sheet1, "L1:L10", fmt.Sprintf(`[{"type":"formula", "criteria":"L2<3", "format":%d}]`, format1))) + // 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) { @@ -1155,9 +1042,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")) @@ -1173,11 +1060,11 @@ 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) - // 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() @@ -1189,7 +1076,16 @@ 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]) + 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) { @@ -1198,7 +1094,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 ""`) @@ -1208,48 +1104,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)) @@ -1275,29 +1129,13 @@ 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) - 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. @@ -1312,7 +1150,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"))) } @@ -1322,6 +1160,72 @@ 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 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 { @@ -1359,8 +1263,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"), @@ -1379,10 +1287,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 } @@ -1391,7 +1307,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 { + fmt.Println(err) + } + } + } +} + +func BenchmarkOpenFile(b *testing.B) { + for i := 0; i < b.N; i++ { + if _, err := OpenFile(filepath.Join("test", "Book1.xlsx")); err != nil { + b.Error(err) } } } diff --git a/file.go b/file.go index a9e7eec..8fe4115 100644 --- a/file.go +++ b/file.go @@ -1,11 +1,11 @@ -// 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. // // 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 @@ -33,23 +33,18 @@ 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() - 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,22 +92,23 @@ 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.sharedStringsWriter() f.styleSheetWriter() 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 } } diff --git a/file_test.go b/file_test.go new file mode 100644 index 0000000..e27b754 --- /dev/null +++ b/file_test.go @@ -0,0 +1,48 @@ +package excelize + +import ( + "bufio" + "bytes" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +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 { + b.Error(err) + } + if err := f.SetCellDefault("Sheet1", val, s); err != nil { + b.Error(err) + } + } + } + // Save xlsx file by the given path. + err := f.SaveAs("./test.xlsx") + if err != nil { + b.Error(err) + } + } +} + +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") +} diff --git a/go.mod b/go.mod index b96dbe2..f94f33b 100644 --- a/go.mod +++ b/go.mod @@ -1,7 +1,17 @@ -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/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-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 106a417..7fa49fe 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,39 @@ +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-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/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= diff --git a/lib.go b/lib.go index b99b175..41b03c7 100644 --- a/lib.go +++ b/lib.go @@ -1,35 +1,35 @@ -// 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. // // 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 ( "archive/zip" "bytes" + "container/list" "fmt" "io" "log" "strconv" "strings" + "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) + 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 +58,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() @@ -104,7 +105,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 @@ -159,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" @@ -183,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 { @@ -191,6 +192,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 @@ -199,6 +201,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 { @@ -227,11 +238,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 @@ -259,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/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/merge.go b/merge.go new file mode 100644 index 0000000..f29640d --- /dev/null +++ b/merge.go @@ -0,0 +1,194 @@ +// 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..afe75aa --- /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(0) + + 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(0) + + 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/picture.go b/picture.go index 3cfcbf5..cac1af2 100644 --- a/picture.go +++ b/picture.go @@ -1,11 +1,11 @@ -// 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. // // 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 @@ -14,7 +14,9 @@ import ( "encoding/json" "encoding/xml" "errors" + "fmt" "image" + "io" "io/ioutil" "os" "path" @@ -30,6 +32,7 @@ func parseFormatPictureSet(formatSet string) (*formatPicture, error) { FPrintsWithSheet: true, FLocksWithSheet: false, NoChangeAspect: false, + Autofit: false, OffsetX: 0, OffsetY: 0, XScale: 1.0, @@ -46,7 +49,6 @@ func parseFormatPictureSet(formatSet string) (*formatPicture, error) { // package main // // import ( -// "fmt" // _ "image/gif" // _ "image/jpeg" // _ "image/png" @@ -57,22 +59,18 @@ func parseFormatPictureSet(formatSet string) (*formatPicture, error) { // func main() { // f := excelize.NewFile() // // Insert a picture. -// err := f.AddPicture("Sheet1", "A2", "./image1.jpg", "") -// if err != nil { +// if err := f.AddPicture("Sheet1", "A2", "image.jpg", ""); err != nil { // fmt.Println(err) // } // // 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 { +// if err := f.AddPicture("Sheet1", "D2", "image.png", `{"x_scale": 0.5, "y_scale": 0.5, "hyperlink": "#Sheet2!D8", "hyperlink_type": "Location"}`); err != nil { // fmt.Println(err) // } // // 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 { +// 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 { // fmt.Println(err) // } -// err = f.SaveAs("./Book1.xlsx") -// if err != nil { +// if err := f.SaveAs("Book1.xlsx"); err != nil { // fmt.Println(err) // } // } @@ -117,16 +115,14 @@ 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) // } -// err = f.AddPictureFromBytes("Sheet1", "A2", "", "Excel Logo", ".jpg", file) -// if err != nil { +// if err := f.AddPictureFromBytes("Sheet1", "A2", "", "Excel Logo", ".jpg", file); err != nil { // fmt.Println(err) // } -// err = f.SaveAs("./Book1.xlsx") -// if err != nil { +// if err := f.SaveAs("Book1.xlsx"); err != nil { // fmt.Println(err) // } // } @@ -155,14 +151,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 +169,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 +178,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 @@ -279,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 := @@ -302,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 = cNvPrID pic.NvPicPr.CNvPr.Descr = file pic.NvPicPr.CNvPr.Name = "Picture " + strconv.Itoa(cNvPrID) if hyperlinkRID != 0 { @@ -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 { @@ -385,7 +328,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] @@ -416,7 +359,7 @@ func (f *File) setContentTypePartVMLExtensions() { if !vml { content.Defaults = append(content.Defaults, xlsxDefault{ Extension: "vml", - ContentType: "application/vnd.openxmlformats-officedocument.vmlDrawing", + ContentType: ContentTypeVML, }) } } @@ -429,16 +372,24 @@ 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", + "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": "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": ContentTypeDrawingML, + "chartsheet": ContentTypeSpreadSheetMLChartsheet, + "comments": ContentTypeSpreadSheetMLComments, + "drawings": ContentTypeDrawing, + "table": ContentTypeSpreadSheetMLTable, + "pivotTable": ContentTypeSpreadSheetMLPivotTable, + "pivotCache": ContentTypeSpreadSheetMLPivotCacheDefinition, + "sharedStrings": ContentTypeSpreadSheetMLSharedStrings, } s, ok := setContentType[contentType] if ok { @@ -465,9 +416,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 { @@ -481,7 +432,7 @@ 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) // return @@ -491,76 +442,128 @@ func (f *File) getSheetRelationshipsTargetByID(sheet, rID string) string { // fmt.Println(err) // return // } -// err = ioutil.WriteFile(file, raw, 0644) -// if err != nil { +// if err := ioutil.WriteFile(file, raw, 0644); err != nil { // fmt.Println(err) // } // 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) - - drawingRelationships := strings.Replace( - strings.Replace(target, "../drawings", "xl/drawings/_rels", -1), ".xml", ".xml.rels", -1) - - wsDr, _ := f.drawingParser(drawingXML) - - for _, anchor := range wsDr.TwoCellAnchor { - if anchor.From != nil && anchor.Pic != nil { - if anchor.From.Col == col && anchor.From.Row == row { - xlsxWorkbookRelation := f.getDrawingRelationships(drawingRelationships, - anchor.Pic.BlipFill.Blip.Embed) - _, ok := supportImageTypes[filepath.Ext(xlsxWorkbookRelation.Target)] - if ok { - return filepath.Base(xlsxWorkbookRelation.Target), - []byte(f.XLSX[strings.Replace(xlsxWorkbookRelation.Target, - "..", "xl", -1)]), err - } - } - } - } - _, ok := f.XLSX[drawingXML] if !ok { return "", nil, err } - 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 { - 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 + drawingRelationships := strings.Replace( + strings.Replace(target, "../drawings", "xl/drawings/_rels", -1), ".xml", ".xml.rels", -1) + + return f.getPicture(row, col, drawingXML, drawingRelationships) +} + +// 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) + 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) { + var ( + wsDr *xlsxWsDr + ok bool + deWsDr *decodeWsDr + drawRel *xlsxRelationship + deTwoCellAnchor *decodeTwoCellAnchor + ) + + wsDr, _ = f.drawingParser(drawingXML) + 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)))). + 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(strings.NewReader("" + 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 { + drawRel = f.getDrawingRelationships(drawingRelationships, deTwoCellAnchor.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 "", []byte{}, err + 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 } // 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 @@ -570,31 +573,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() { @@ -605,3 +583,48 @@ 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 { + cellWidth, cellHeight = 0, 0 + 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 890092e..015d854 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" @@ -20,49 +25,53 @@ 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) + } } } 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"), - `{"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 = xlsx.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.jpg")) - if !assert.NoError(t, err) { - t.FailNow() - } + 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, 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) { 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"), "") @@ -83,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)) { @@ -97,37 +106,33 @@ 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"))) { - t.FailNow() - } + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestGetPicture.xlsx"))) - xlsx, err = OpenFile(filepath.Join("test", "TestGetPicture.xlsx")) - if !assert.NoError(t, err) { - t.FailNow() - } + f, err = OpenFile(filepath.Join("test", "TestGetPicture.xlsx")) + assert.NoError(t, err) - 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)) { @@ -136,7 +141,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) @@ -151,11 +163,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") { @@ -163,4 +173,32 @@ 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")) +} + +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/pivotTable.go b/pivotTable.go new file mode 100644 index 0000000..cf04381 --- /dev/null +++ b/pivotTable.go @@ -0,0 +1,593 @@ +// 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 ( + "encoding/xml" + "errors" + "fmt" + "strconv" + "strings" +) + +// PivotTableOption directly maps the format settings of the pivot table. +type PivotTableOption struct { + DataRange string + PivotTableRange string + Rows []PivotTableField + Columns []PivotTableField + Data []PivotTableField + Filter []PivotTableField +} + +// 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: +// +// Average +// Count +// CountNums +// Max +// Min +// Product +// StdDev +// StdDevp +// Sum +// Var +// Varp +// +// 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: +// +// 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)]) +// } +// if err := f.AddPivotTable(&excelize.PivotTableOption{ +// 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 { +// fmt.Println(err) +// } +// if err := f.SaveAs("Book1.xlsx"); 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 +} + +// getPivotFieldsOrder provides a function to get order list of pivot table +// fields. +func (f *File) getPivotFieldsOrder(dataRange string) ([]string, error) { + order := []string{} + 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()) + } + // data range has been checked + order, _ := f.getPivotFieldsOrder(opt.DataRange) + 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{}, + RowItems: &xlsxRowItems{ + Count: 1, + I: []*xlsxI{ + { + []*xlsxX{{}, {}}, + }, + }, + }, + ColItems: &xlsxColItems{ + Count: 1, + I: []*xlsxI{{}}, + }, + PivotTableStyleInfo: &xlsxPivotTableStyleInfo{ + Name: "PivotStyleLight16", + ShowRowHeaders: true, + ShowColHeaders: true, + ShowLastColumn: true, + }, + } + + // pivot fields + _ = 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 + 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 { + return err + } + 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 { + return err + } + 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, + Subtotal: dataFieldsSubtotals[idx], + }) + } + + // count data fields + if pt.DataFields != nil { + pt.DataFields.Count = len(pt.DataFields.DataField) + } + 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 +} + +// 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 { + 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 _, fieldIdx := range colFieldsIndex { + pt.ColFields.Field = append(pt.ColFields.Field, &xlsxField{ + X: fieldIdx, + }) + } + + // 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 { + order, err := f.getPivotFieldsOrder(opt.DataRange) + if err != nil { + return err + } + for _, name := range order { + 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{ + {T: "default"}, + }, + }, + }) + 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", + Name: f.getPivotTableFieldName(name, opt.Columns), + Items: &xlsxItems{ + Count: 1, + Item: []*xlsxItem{ + {T: "default"}, + }, + }, + }) + continue + } + if inPivotTableField(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 []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.Data); pos != -1 { + pivotFieldsIndex = append(pivotFieldsIndex, pos) + } + } + return pivotFieldsIndex, nil +} + +// 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 "" +} + +// 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..cc80835 --- /dev/null +++ b/pivotTable_test.go @@ -0,0 +1,229 @@ +package excelize + +import ( + "fmt" + "math/rand" + "path/filepath" + "strings" + "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"} + assert.NoError(t, f.SetSheetRow("Sheet1", "A1", &[]string{"Month", "Year", "Type", "Sales", "Region"})) + for i := 0; i < 30; i++ { + 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", + 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"}}, + })) + // 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: []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: []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: []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: []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: []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: []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 + 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: []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: []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: []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: []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: []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: []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: []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: []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 + _, _, 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: []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") + // 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: []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([]PivotTableField{}, &PivotTableOption{}) + 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{}, "")) +} + +func TestGetPivotTableFieldName(t *testing.T) { + f := NewFile() + f.getPivotTableFieldName("-", []PivotTableField{}) +} diff --git a/rows.go b/rows.go index b228fc2..17216df 100644 --- a/rows.go +++ b/rows.go @@ -1,19 +1,21 @@ -// 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. // // 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 ( "bytes" "encoding/xml" + "errors" "fmt" "io" + "log" "math" "strconv" ) @@ -21,8 +23,16 @@ 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 { +// fmt.Println(err) +// return +// } +// for rows.Next() { +// row, err := rows.Columns() +// if err != nil { +// fmt.Println(err) +// } // for _, colCell := range row { // fmt.Print(colCell, "\t") // } @@ -30,95 +40,38 @@ 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 + curRow, totalRow, stashRow int + sheet string + rows []xlsxRow + f *File + decoder *xml.Decoder } // 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 - } - } - } + rows.curRow++ + return rows.curRow <= rows.totalRow } // Error will return the error when the find next row element @@ -128,23 +81,62 @@ 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 + var ( + err error + inElement string + row, cellCol int + columns []string + ) + + if rows.stashRow >= rows.curRow { + return columns, err } - startElement := rows.token.(xml.StartElement) - r := xlsxRow{} - _ = rows.decoder.DecodeElement(&r, &startElement) + d := rows.f.sharedStringsReader() - columns := make([]string, len(r.C)) - for _, colCell := range r.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 { + rows.stashRow = row - 1 + 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 + } } - 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 @@ -153,14 +145,22 @@ 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: +// 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 { +// fmt.Println(err) +// return +// } // for rows.Next() { -// row, err := rows.Columns() +// row, err := rows.Columns() +// if err != nil { +// fmt.Println(err) +// } // for _, colCell := range row { // fmt.Print(colCell, "\t") // } @@ -168,31 +168,22 @@ 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 { + if f.Sheet[name] != nil { + // flush data output, _ := xml.Marshal(f.Sheet[name]) - f.saveFileList(name, replaceWorkSheetsRelationshipsNameSpaceBytes(output)) + f.saveFileList(name, replaceRelationshipsNameSpaceBytes(output)) } - return &Rows{ - f: f, - decoder: xml.NewDecoder(bytes.NewReader(f.readXML(name))), - }, 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 + var ( + err error + inElement string + row int + rows Rows + ) + decoder := f.xmlNewDecoder(bytes.NewReader(f.readXML(name))) for { token, _ := decoder.Token() if token == nil { @@ -202,23 +193,23 @@ func (f *File) getTotalRowsCols(name string) (int, int, error) { 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 + 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 tr, tc, nil + rows.f = f + rows.sheet = name + rows.decoder = f.xmlNewDecoder(bytes.NewReader(f.readXML(name))) + return &rows, nil } // SetRowHeight provides a function to set the height of a single row. For @@ -248,7 +239,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)) } @@ -286,15 +278,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 } @@ -304,20 +302,21 @@ 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].R) > 0 { - value := "" - for _, v := range d.SI[xlsxSI].R { - value += v.T + 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 value, nil } - return f.formattedValue(xlsx.S, d.SI[xlsxSI].T), nil + return f.formattedValue(xlsx.S, xlsx.V), nil case "str": return f.formattedValue(xlsx.S, xlsx.V), nil case "inlineStr": - return f.formattedValue(xlsx.S, xlsx.IS.T), 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 } @@ -364,8 +363,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) // @@ -373,6 +372,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 @@ -421,16 +423,18 @@ 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 { - 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 @@ -439,6 +443,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) @@ -517,6 +525,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 } @@ -552,6 +594,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 f7d49b4..fd7196d 100644 --- a/rows_test.go +++ b/rows_test.go @@ -1,27 +1,29 @@ package excelize import ( + "bytes" "fmt" "path/filepath" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) 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() } - collectedRows := make([][]string, 0) + var collectedRows [][]string for rows.Next() { columns, err := rows.Columns() assert.NoError(t, err) @@ -31,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,8 +42,43 @@ func TestRows(t *testing.T) { t.FailNow() } - r := Rows{} - r.Columns() + 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) { + const ( + sheet2 = "Sheet2" + expectedNumRow = 11 + ) + f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) + require.NoError(t, err) + + rows, err := f.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) + + // Valued cell sparse distribution test + f = NewFile() + cells := []string{"C1", "E1", "A3", "B3", "C3", "D3", "E3"} + for _, cell := range cells { + assert.NoError(t, 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) { @@ -55,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") @@ -95,86 +132,135 @@ 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(`1`))) + _, err = rows.Columns() + assert.NoError(t, err) + 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 + _, 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) { - 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)) + 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") - 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) { - xlsx := NewFile() - sheet1 := xlsx.GetSheetName(1) - r, err := xlsx.workSheetReader(sheet1) + f := NewFile() + sheet1 := f.GetSheetName(0) + 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") + assert.NoError(t, 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") + assert.NoError(t, 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) { xlsx := NewFile() - sheet1 := xlsx.GetSheetName(1) + sheet1 := xlsx.GetSheetName(0) r, err := xlsx.workSheetReader(sheet1) assert.NoError(t, err) const ( @@ -183,7 +269,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") @@ -206,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)) @@ -233,8 +319,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"))) { @@ -286,13 +372,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() @@ -327,8 +413,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 } @@ -442,8 +527,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 } @@ -488,8 +572,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 } @@ -534,8 +617,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 } @@ -581,8 +663,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 } @@ -612,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") @@ -632,7 +762,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)) @@ -654,7 +784,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)) @@ -672,6 +802,61 @@ 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{{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() + 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") +} + +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++ { + 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] == "" { diff --git a/shape.go b/shape.go index c90963c..0455b22 100644 --- a/shape.go +++ b/shape.go @@ -1,11 +1,11 @@ -// 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. // // 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 @@ -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: // @@ -275,7 +275,8 @@ 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, "") + sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(f.sheetMap[trimSheetName(sheet)], "xl/worksheets/") + ".rels" + rID := f.addRels(sheetRels, SourceRelationshipDrawingML, sheetRelationshipsDrawingXML, "") f.addSheetDrawing(sheet, rID) } err = f.addDrawingShape(sheet, drawingXML, cell, formatSet) @@ -360,7 +361,7 @@ func (f *File) addDrawingShape(sheet, drawingXML, cell string, formatSet *format FontRef: &aFontRef{ Idx: "minor", SchemeClr: &attrValString{ - Val: "tx1", + Val: stringPtr("tx1"), }, }, }, @@ -377,11 +378,11 @@ 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", - Family: "Calibri", + Family: f.GetDefaultFont(), Size: 11, Color: "#000000", }, @@ -409,11 +410,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, }, @@ -421,6 +417,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: stringPtr(srgbClr), + }, + } + } shape.TxBody.P = append(shape.TxBody.P, paragraph) } twoCellAnchor.Sp = &shape @@ -449,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/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 32d12d1..6a935b1 100644 --- a/sheet.go +++ b/sheet.go @@ -1,11 +1,11 @@ -// 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. // // 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 @@ -14,9 +14,13 @@ import ( "encoding/json" "encoding/xml" "errors" + "fmt" + "io" "io/ioutil" + "log" "os" "path" + "reflect" "regexp" "strconv" "strings" @@ -30,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) @@ -46,24 +50,29 @@ 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 - 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 + return f.GetSheetIndex(name) } // 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 } @@ -79,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 } @@ -92,7 +106,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,21 +119,29 @@ 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(replaceRelationshipsNameSpaceBytes(output))) ok := f.checked[p] if ok { + delete(f.Sheet, p) f.checked[p] = false } } } } -// 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++ } @@ -129,21 +151,22 @@ 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, }) } // 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 @@ -160,50 +183,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 = replaceRelationshipsNameSpaceBytes(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. @@ -211,30 +202,29 @@ func (f *File) setAppXML() { f.saveFileList("docProps/app.xml", []byte(templateDocpropsApp)) } -// 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(``) - return bytes.Replace(workbookMarshal, oldXmlns, newXmlns, -1) +// 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 := 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 -// 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. +// 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 < 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{} + } if len(wb.BookViews.WorkBookView) > 0 { wb.BookViews.WorkBookView[0].ActiveTab = activeTab } else { @@ -244,8 +234,17 @@ func (f *File) SetActiveSheet(index int) { } } } - for idx, name := range f.GetSheetMap() { - xlsx, _ := f.workSheetReader(name) + for idx, name := range f.GetSheetList() { + xlsx, err := f.workSheetReader(name) + if err != nil { + // Chartsheet or dialogsheet + return + } + if xlsx.SheetViews == nil { + xlsx.SheetViews = &xlsxSheetViews{ + SheetView: []xlsxSheetView{{WorkbookViewID: 0}}, + } + } if len(xlsx.SheetViews.SheetView) > 0 { xlsx.SheetViews.SheetView[0].TabSelected = false } @@ -263,20 +262,39 @@ 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 +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 { + 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 { + return wb.Sheets.Sheet[0].SheetID + } + } 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. @@ -293,48 +311,64 @@ 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 { - 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 - } - } +func (f *File) getSheetNameByID(ID int) string { + wb := f.workbookReader() + if wb == nil || ID < 1 { + return "" + } + for _, sheet := range wb.Sheets.Sheet { + if ID == sheet.SheetID { + 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. -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 - } - } +// 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 { + name = sheet } } - return 0 + return } -// GetSheetMap provides a function to get worksheet name and index map of XLSX. -// For example: +// 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 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 0. +func (f *File) GetSheetIndex(name string) int { + var idx = -1 + for index, sheet := range f.GetSheetList() { + if sheet == trimSheetName(name) { + idx = index + } + } + return idx +} + +// 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") +// f, err := excelize.OpenFile("Book1.xlsx") // if err != nil { // return // } @@ -343,27 +377,45 @@ 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 } -// getSheetMap provides a function to get worksheet name and XML file path map of -// XLSX. +// 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 { + 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 { - 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.relsReader("xl/_rels/workbook.xml.rels") + maps := map[string]string{} + for _, v := range content.Sheets.Sheet { + for _, rel := range rels.Relationships { + if rel.ID == v.ID { + // 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 > 1 { + maps[v.Name] = fmt.Sprintf("xl/%s", strings.Join(pathInfo[pathInfoLen-2:], "/")) + } + } + } } return maps } @@ -382,7 +434,8 @@ 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), "") + 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() return err @@ -394,22 +447,45 @@ 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) == -1 { + return + } + sheetName := trimSheetName(name) + wb := f.workbookReader() + 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:]...) + var sheetXML, rels string + if wbRels != nil { + for _, rel := range wbRels.Relationships { + if rel.ID == sheet.ID { + sheetXML = fmt.Sprintf("xl/%s", rel.Target) + pathInfo := strings.Split(rel.Target, "/") + if len(pathInfo) == 2 { + rels = fmt.Sprintf("xl/%s/_rels/%s.rels", pathInfo[0], pathInfo[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.Relationships, rels) + delete(f.Sheet, sheetXML) f.SheetCount-- } } + 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())) } @@ -417,7 +493,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:]...) @@ -448,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) @@ -457,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("sheet" + strconv.Itoa(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 } @@ -470,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] @@ -482,7 +560,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 @@ -510,7 +588,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 } @@ -687,69 +765,242 @@ 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 + 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)) + f.saveFileList(name, replaceRelationshipsNameSpaceBytes(output)) } - xml.NewDecoder(bytes.NewReader(f.readXML(name))) - d := f.sharedStringsReader() + return f.searchSheet(name, value, regSearch) +} - decoder := xml.NewDecoder(bytes.NewReader(f.readXML(name))) +// 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) (result []string, err error) { + var ( + cellName, inElement string + cellCol, row int + d *xlsxSST + ) + + d = f.sharedStringsReader() + decoder := f.xmlNewDecoder(bytes.NewReader(f.readXML(name))) for { - token, _ := decoder.Token() - if token == nil { + var token xml.Token + token, err = decoder.Token() + if err != nil || token == nil { + if err == io.EOF { + err = nil + } break } switch startElement := token.(type) { 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 - } - } - - cellCol, _, err := CellNameToCoordinates(colCell.R) - if err != nil { - return result, err - } - cellName, err := CoordinatesToCellName(cellCol, r.R) - if err != nil { - return result, err - } - result = append(result, cellName) + row, err = attrValToInt("r", startElement.Attr) + if err != nil { + return } } + 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 + } + } else { + if val != value { + continue + } + } + 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: } } - return result, nil + 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 +} + +// 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 @@ -846,6 +1097,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 ( @@ -885,11 +1140,43 @@ 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: // PageLayoutOrientation(string) -// PageLayoutPaperSize(int) +// PageLayoutPaperSize(int) // // The following shows the paper size sorted by excelize index number: // @@ -1034,6 +1321,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 { @@ -1047,39 +1336,295 @@ func (f *File) GetPageLayout(sheet string, opts ...PageLayoutOptionPtr) error { return err } -// 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 { - if f.WorkSheetRels[path] == nil { - _, ok := f.XLSX[path] - if ok { - c := xlsxWorkbookRels{} - _ = xml.Unmarshal(namespaceStrictToTransitional(f.readXML(path)), &c) - f.WorkSheetRels[path] = &c +// SetDefinedName provides a function to set the defined names of the workbook +// or worksheet. If not specified scope, 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.getSheetID(definedName.Scope); sheetID != 0 { + sheetID-- + d.LocalSheetID = &sheetID } } - return f.WorkSheetRels[path] + if wb.DefinedNames != nil { + for _, dn := range wb.DefinedNames.DefinedName { + var scope string + if dn.LocalSheetID != nil { + 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") + } + } + wb.DefinedNames.DefinedName = append(wb.DefinedNames.DefinedName, d) + return nil + } + wb.DefinedNames = &xlsxDefinedNames{ + DefinedName: []xlsxDefinedName{d}, + } + return nil } -// 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) +// 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.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:]...) + 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 { + 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.getSheetNameByID(*dn.LocalSheetID + 1) + } + definedNames = append(definedNames, 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.GetSheetList() + 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() + for index, sheet := range f.GetSheetList() { + if activeSheet == index { + continue + } + ws, _ := f.workSheetReader(sheet) + sheetViews := ws.SheetViews.SheetView + if len(sheetViews) > 0 { + for idx := range sheetViews { + ws.SheetViews.SheetView[idx].TabSelected = false + } + } + } + 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 { + var err error + + if f.Relationships[path] == nil { + _, ok := f.XLSX[path] + if ok { + c := xlsxRelationships{} + 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] } // 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/sheet_test.go b/sheet_test.go index 7db982a..0014220 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -2,50 +2,91 @@ package excelize_test import ( "fmt" + "path/filepath" + "strings" "testing" - "github.com/360EntSecGroup-Skylar/excelize" + "github.com/360EntSecGroup-Skylar/excelize/v2" + "github.com/mohae/deepcopy" "github.com/stretchr/testify/assert" ) 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) + fmt.Println(err) } - if err := xl.SetPageLayout( + if err := f.SetPageLayout( "Sheet1", excelize.PageLayoutPaperSize(10), + excelize.FitToHeight(2), + excelize.FitToWidth(2), ); err != nil { - panic(err) + fmt.Println(err) } // Output: } func ExampleFile_GetPageLayout() { - xl := excelize.NewFile() + f := excelize.NewFile() var ( orientation excelize.PageLayoutOrientation paperSize excelize.PageLayoutPaperSize + fitToHeight excelize.FitToHeight + fitToWidth excelize.FitToWidth ) - if err := xl.GetPageLayout("Sheet1", &orientation); err != nil { - panic(err) + if err := f.GetPageLayout("Sheet1", &orientation); err != nil { + fmt.Println(err) } - if err := xl.GetPageLayout("Sheet1", &paperSize); err != nil { - panic(err) + if err := f.GetPageLayout("Sheet1", &paperSize); err != nil { + fmt.Println(err) + } + if err := f.GetPageLayout("Sheet1", &fitToHeight); err != nil { + fmt.Println(err) + } + + if err := f.GetPageLayout("Sheet1", &fitToWidth); err != nil { + fmt.Println(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 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 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) { @@ -57,6 +98,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 { @@ -69,26 +112,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 +140,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() } @@ -106,6 +149,35 @@ 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) + + // 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) { f := excelize.NewFile() // Test set page layout on not exists worksheet. @@ -117,3 +189,135 @@ 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() + 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. + 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"))) +} + +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 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"))) +} + +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()) +} + +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(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) { + 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) +} diff --git a/sheetpr.go b/sheetpr.go index 66761f3..dbfb734 100644 --- a/sheetpr.go +++ b/sheetpr.go @@ -1,11 +1,11 @@ -// 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. // // 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 @@ -191,3 +191,362 @@ 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: +// PageMarginBottom(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: +// PageMarginBottom(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 +} + +// 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 48d330e..6e03151 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), @@ -40,13 +40,13 @@ func ExampleFile_SetSheetPrOptions() { excelize.AutoPageBreaks(true), excelize.OutlineSummaryBelow(false), ); err != nil { - panic(err) + fmt.Println(err) } // Output: } 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, @@ -66,7 +66,7 @@ func ExampleFile_GetSheetPrOptions() { &autoPageBreaks, &outlineSummaryBelow, ); err != nil { - panic(err) + fmt.Println(err) } fmt.Println("Defaults:") fmt.Printf("- codeName: %q\n", codeName) @@ -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,11 +138,332 @@ 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() } }) } } + +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 { + fmt.Println(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 { + fmt.Println(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") +} + +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/sheetview.go b/sheetview.go index 8ffc9bc..fa3cfdf 100644 --- a/sheetview.go +++ b/sheetview.go @@ -1,47 +1,68 @@ -// 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. // // 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 "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 is a SheetViewOption. + // 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 is a SheetViewOption. + // 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 is a SheetViewOption. + // ShowFormulas is a SheetViewOption. It specifies a flag indicating whether + // this sheet should display formulas. ShowFormulas bool - // ShowGridLines is a SheetViewOption. + // ShowGridLines is a SheetViewOption. It specifies a flag indicating whether + // this sheet should display gridlines. ShowGridLines bool - // ShowRowColHeaders is a SheetViewOption. + // ShowRowColHeaders is a SheetViewOption. It specifies a flag indicating + // whether the sheet should display row and column headings. ShowRowColHeaders bool - // ZoomScale is a SheetViewOption. + // 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 is a SheetViewOption. + // 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 is a SheetViewOption. - ShowWhiteSpace bool - // ShowZeros is a SheetViewOption. + // 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 // WindowProtection is a SheetViewOption. WindowProtection bool */ @@ -89,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)) } @@ -98,7 +127,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) } @@ -126,17 +155,23 @@ 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) // 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 { view, err := f.getSheetView(name, viewIndex) if err != nil { @@ -149,18 +184,24 @@ 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) // ShowGridLines(bool) // 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 { diff --git a/sheetview_test.go b/sheetview_test.go index b565a12..d999875 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), @@ -47,30 +47,30 @@ func ExampleFile_SetSheetViewOptions() { excelize.ZoomScale(80), excelize.TopLeftCell("C3"), ); err != nil { - panic(err) + fmt.Println(err) } var zoomScale excelize.ZoomScale fmt.Println("Default:") fmt.Println("- zoomScale: 80") - if err := xl.SetSheetViewOptions(sheet, 0, excelize.ZoomScale(500)); err != nil { - panic(err) + if err := f.SetSheetViewOptions(sheet, 0, excelize.ZoomScale(500)); err != nil { + fmt.Println(err) } - if err := xl.GetSheetViewOptions(sheet, 0, &zoomScale); err != nil { - panic(err) + if err := f.GetSheetViewOptions(sheet, 0, &zoomScale); err != nil { + fmt.Println(err) } fmt.Println("Used out of range value:") fmt.Println("- zoomScale:", zoomScale) - if err := xl.SetSheetViewOptions(sheet, 0, excelize.ZoomScale(123)); err != nil { - panic(err) + if err := f.SetSheetViewOptions(sheet, 0, excelize.ZoomScale(123)); err != nil { + fmt.Println(err) } - if err := xl.GetSheetViewOptions(sheet, 0, &zoomScale); err != nil { - panic(err) + if err := f.GetSheetViewOptions(sheet, 0, &zoomScale); err != nil { + fmt.Println(err) } fmt.Println("Used correct value:") @@ -87,7 +87,7 @@ func ExampleFile_SetSheetViewOptions() { } func ExampleFile_GetSheetViewOptions() { - xl := excelize.NewFile() + f := excelize.NewFile() const sheet = "Sheet1" var ( @@ -95,21 +95,23 @@ func ExampleFile_GetSheetViewOptions() { rightToLeft excelize.RightToLeft showFormulas excelize.ShowFormulas showGridLines excelize.ShowGridLines + showZeros excelize.ShowZeros showRowColHeaders excelize.ShowRowColHeaders zoomScale excelize.ZoomScale topLeftCell excelize.TopLeftCell ) - if err := xl.GetSheetViewOptions(sheet, 0, + if err := f.GetSheetViewOptions(sheet, 0, &defaultGridColor, &rightToLeft, &showFormulas, &showGridLines, + &showZeros, &showRowColHeaders, &zoomScale, &topLeftCell, ); err != nil { - panic(err) + fmt.Println(err) } fmt.Println("Default:") @@ -117,28 +119,38 @@ 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+`"`) - if err := xl.SetSheetViewOptions(sheet, 0, excelize.TopLeftCell("B2")); err != nil { - panic(err) + if err := f.SetSheetViewOptions(sheet, 0, excelize.TopLeftCell("B2")); err != nil { + fmt.Println(err) } - if err := xl.GetSheetViewOptions(sheet, 0, &topLeftCell); err != nil { - panic(err) + if err := f.GetSheetViewOptions(sheet, 0, &topLeftCell); err != nil { + fmt.Println(err) } - if err := xl.SetSheetViewOptions(sheet, 0, excelize.ShowGridLines(false)); err != nil { - panic(err) + if err := f.SetSheetViewOptions(sheet, 0, excelize.ShowGridLines(false)); err != nil { + fmt.Println(err) } - if err := xl.GetSheetViewOptions(sheet, 0, &showGridLines); err != nil { - panic(err) + if err := f.GetSheetViewOptions(sheet, 0, &showGridLines); err != nil { + fmt.Println(err) + } + + if err := f.SetSheetViewOptions(sheet, 0, excelize.ShowZeros(false)); err != nil { + fmt.Println(err) + } + + if err := f.GetSheetViewOptions(sheet, 0, &showZeros); err != nil { + fmt.Println(err) } fmt.Println("After change:") fmt.Println("- showGridLines:", showGridLines) + fmt.Println("- showZeros:", showZeros) fmt.Println("- topLeftCell:", topLeftCell) // Output: @@ -147,24 +159,26 @@ func ExampleFile_GetSheetViewOptions() { // - rightToLeft: false // - showFormulas: false // - showGridLines: true + // - showZeros: true // - showRowColHeaders: true // - zoomScale: 0 // - topLeftCell: "" // After change: // - showGridLines: false + // - showZeros: false // - topLeftCell: B2 } 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)) } diff --git a/sparkline.go b/sparkline.go new file mode 100644 index 0000000..ce5be4c --- /dev/null +++ b/sparkline.go @@ -0,0 +1,542 @@ +// 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 ( + "encoding/xml" + "errors" + "io" + "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 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 +// +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 + sparklineGroupsBytes, extBytes []byte + ) + + // parameter validation + 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"} + if opt.Type != "" { + 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.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 + if err = f.appendSparkline(ws, group, groups); err != nil { + return + } + } else { + groups = &xlsxX14SparklineGroups{ + XMLNSXM: NameSpaceSpreadSheetExcel2006Main, + SparklineGroups: []*xlsxX14SparklineGroup{group}, + } + if sparklineGroupsBytes, err = xml.Marshal(groups); err != nil { + return + } + if extBytes, err = xml.Marshal(&xlsxWorksheetExt{ + URI: ExtURISparklineGroups, + Content: string(sparklineGroupsBytes), + }); err != nil { + return + } + ws.ExtLst.Ext = string(extBytes) + } + + return +} + +// 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, + }) + } +} + +// 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(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(strings.NewReader(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 new file mode 100644 index 0000000..4b059ab --- /dev/null +++ b/sparkline_test.go @@ -0,0 +1,310 @@ +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))) + + assert.NoError(t, f.SetColWidth("Sheet1", "A", "A", 14)) + assert.NoError(t, f.SetColWidth("Sheet1", "B", "B", 50)) + // Headings + assert.NoError(t, f.SetCellValue("Sheet1", "A1", "Sparkline")) + assert.NoError(t, f.SetCellValue("Sheet1", "B1", "Description")) + + 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"}, + })) + + 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", + })) + + 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", + })) + + 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, + })) + + 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"}, + High: true, + Low: true, + })) + + 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"}, + First: true, + Last: true, + })) + + 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, + })) + + 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, + })) + + 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", + })) + + 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"}, + Type: "column", + Style: 2, + })) + + 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"}, + Type: "column", + Style: 3, + })) + + 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"}, + Type: "column", + Style: 4, + })) + + 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"}, + Type: "column", + Style: 5, + })) + + 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"}, + Type: "column", + Style: 6, + })) + + 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"}, + Type: "column", + SeriesColor: "#E965E0", + })) + + 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", + })) + + 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"}, + Type: "win_loss", + Negative: true, + })) + + 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"}, + Type: "column", + Style: 20, + })) + + 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"}, + Type: "column", + Style: 20, + Reverse: true, + })) + + 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, + })) + assert.NoError(t, f.SetCellValue("Sheet1", "A25", "Growth")) + + 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"}, + 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 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{ + {-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 { + if err := f.SetSheetRow("Sheet2", fmt.Sprintf("A%d", row+1), &data); err != nil { + fmt.Println(err) + } + } + for row, data := range sheet3 { + if err := f.SetSheetRow("Sheet3", fmt.Sprintf("A%d", row+1), &data); err != nil { + fmt.Println(err) + } + } + return f +} diff --git a/stream.go b/stream.go new file mode 100644 index 0000000..838751d --- /dev/null +++ b/stream.go @@ -0,0 +1,515 @@ +// 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" + "fmt" + "io" + "io/ioutil" + "os" + "reflect" + "strconv" + "strings" + "time" +) + +// StreamWriter defined the type of stream writer. +type StreamWriter struct { + File *File + Sheet string + SheetID int + worksheet *xlsxWorksheet + rawData bufferedWriter + tableParts string +} + +// 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 and style: +// +// file := excelize.NewFile() +// streamWriter, err := file.NewStreamWriter("Sheet1") +// if err != nil { +// fmt.Println(err) +// } +// styleID, err := file.NewStyle(`{"font":{"color":"#777777"}}`) +// if err != nil { +// fmt.Println(err) +// } +// if err := streamWriter.SetRow("A1", []interface{}{excelize.Cell{StyleID: styleID, Value: "Data"}}); err != nil { +// fmt.Println(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); err != nil { +// fmt.Println(err) +// } +// } +// if err := streamWriter.Flush(); err != nil { +// fmt.Println(err) +// } +// if err := file.SaveAs("Book1.xlsx"); err != nil { +// fmt.Println(err) +// } +// +func (f *File) NewStreamWriter(sheet string) (*StreamWriter, error) { + sheetID := f.getSheetID(sheet) + if sheetID == 0 { + return nil, fmt.Errorf("sheet %s is not exist", sheet) + } + sw := &StreamWriter{ + File: f, + Sheet: sheet, + SheetID: sheetID, + } + var err error + sw.worksheet, err = f.workSheetReader(sheet) + if err != nil { + return nil, err + } + sw.rawData.WriteString(XMLHeader + ``) + return sw, err +} + +// AddTable creates an Excel table for the StreamWriter using the given +// coordinate area and format set. For example, create a table of A1:D5: +// +// err := sw.AddTable("A1", "D5", ``) +// +// 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 + } + + coordinates, err := areaRangeToCoordinates(hcell, vcell) + if err != nil { + return err + } + _ = 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 := sw.File.xmlNewDecoder(r) + for { + token, err := dec.Token() + if err == io.EOF { + return res, nil + } + if err != nil { + return nil, err + } + startElement, ok := getRowElement(token, hrow) + if !ok { + continue + } + // 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 { + return nil, err + } + if col < hcol || col > vcol { + continue + } + res[col-hcol] = c.V + } + 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 + } + } + return +} + +// Cell can be used directly in StreamWriter.SetRow to specify a style and +// a value. +type Cell struct { + StyleID int + Value interface{} +} + +// 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 + } + + fmt.Fprintf(&sw.rawData, ``, row) + for i, val := range values { + axis, err := CoordinatesToCellName(col+i, row) + 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 = setCellValFunc(&c, val); err != nil { + sw.rawData.WriteString(``) + return err + } + writeCell(&sw.rawData, c) + } + sw.rawData.WriteString(``) + 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(``) + if c.V != "" { + buf.WriteString(``) + xml.EscapeText(buf, stringToBytes(c.V)) + buf.WriteString(``) + } + buf.WriteString(``) +} + +// 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 + } + + 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 +} + +// 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() + enc := xml.NewEncoder(w) + for i := 0; i < s.NumField(); i++ { + if from <= i && i <= to { + enc.Encode(s.Field(i).Interface()) + } + } +} + +// 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 + } + // 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 + } + defer os.Remove(bw.tmp.Name()) + return bw.tmp.Close() +} diff --git a/stream_test.go b/stream_test.go new file mode 100644 index 0000000..d89dad8 --- /dev/null +++ b/stream_test.go @@ -0,0 +1,174 @@ +package excelize + +import ( + "encoding/xml" + "fmt" + "io/ioutil" + "math/rand" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "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") + 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)) + + // Test set cell with style. + styleID, err := file.NewStyle(`{"font":{"color":"#777777"}}`) + assert.NoError(t, err) + assert.NoError(t, streamWriter.SetRow("A4", []interface{}{Cell{StyleID: styleID}})) + assert.NoError(t, streamWriter.SetRow("A5", []interface{}{&Cell{StyleID: styleID, Value: "cell"}})) + assert.EqualError(t, streamWriter.SetRow("A6", []interface{}{time.Now()}), "only UTC time expected") + + 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.Flush()) + // Save xlsx file by the given path. + assert.NoError(t, file.SaveAs(filepath.Join("test", "TestStreamWriter.xlsx"))) + + // 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.rawData.Close()) + assert.Error(t, streamWriter.Flush()) + + streamWriter.rawData.tmp, err = ioutil.TempFile(os.TempDir(), "excelize-") + assert.NoError(t, err) + _, 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) { + 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) + + assert.NoError(t, streamWriter.AddTable("A1", "C1", ``)) + + // Test add table with illegal formatset. + assert.EqualError(t, streamWriter.AddTable("B26", "A21", `{x}`), "invalid character 'x' looking for beginning of object key string") + // Test add table with illegal cell coordinates. + assert.EqualError(t, streamWriter.AddTable("A", "B1", `{}`), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + assert.EqualError(t, streamWriter.AddTable("A1", "B", `{}`), `cannot convert cell "B" to coordinates: invalid cell name "B"`) +} + +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"`) +} + +func TestSetCellValFunc(t *testing.T) { + c := &xlsxC{} + assert.NoError(t, setCellValFunc(c, 128)) + assert.NoError(t, setCellValFunc(c, int8(-128))) + assert.NoError(t, setCellValFunc(c, int16(-32768))) + assert.NoError(t, setCellValFunc(c, int32(-2147483648))) + assert.NoError(t, setCellValFunc(c, int64(-9223372036854775808))) + assert.NoError(t, setCellValFunc(c, uint(128))) + assert.NoError(t, setCellValFunc(c, uint8(255))) + assert.NoError(t, setCellValFunc(c, uint16(65535))) + assert.NoError(t, setCellValFunc(c, uint32(4294967295))) + assert.NoError(t, setCellValFunc(c, uint64(18446744073709551615))) + assert.NoError(t, setCellValFunc(c, float32(100.1588))) + assert.NoError(t, setCellValFunc(c, float64(100.1588))) + assert.NoError(t, setCellValFunc(c, " Hello")) + assert.NoError(t, setCellValFunc(c, []byte(" Hello"))) + assert.NoError(t, setCellValFunc(c, time.Now().UTC())) + assert.NoError(t, setCellValFunc(c, time.Duration(1e13))) + assert.NoError(t, setCellValFunc(c, true)) + assert.NoError(t, setCellValFunc(c, nil)) + assert.NoError(t, setCellValFunc(c, complex64(5+10i))) +} diff --git a/styles.go b/styles.go index d6d267d..72b2071 100644 --- a/styles.go +++ b/styles.go @@ -1,18 +1,22 @@ -// 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. // // 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 ( + "bytes" "encoding/json" "encoding/xml" + "errors" "fmt" + "io" + "log" "math" "strconv" "strings" @@ -852,7 +856,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 @@ -997,11 +1001,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 } @@ -1010,22 +1019,31 @@ 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)) + } +} + +// 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) (*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 pointer. Note that the color field uses RGB color code. // // The following shows the border styles sorted by excelize index number: // @@ -1880,26 +1898,33 @@ 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 + default: + return cellXfsID, errors.New("invalid parameter type") } + s := f.stylesReader() 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, f.setFont(fs)) fontID = s.Fonts.Count - 1 } @@ -1930,12 +1955,16 @@ 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 = setFont(fs) + dxf.Font = f.setFont(fs) } dxfStr, _ := xml.Marshal(dxf) if s.Dxfs == nil { @@ -1948,67 +1977,97 @@ 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 = stringPtr(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) *font { +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" } - f := font{ - B: formatStyle.Font.Bold, - I: formatStyle.Font.Italic, - Sz: &attrValInt{Val: formatStyle.Font.Size}, - Color: &xlsxColor{RGB: getPaletteColor(formatStyle.Font.Color)}, - Name: &attrValString{Val: formatStyle.Font.Family}, - Family: &attrValInt{Val: 2}, + fnt := xlsxFont{ + 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 f.Name.Val == "" { - f.Name.Val = "Calibri" - f.Scheme = &attrValString{Val: "minor"} + if style.Font.Bold { + fnt.B = &style.Font.Bold } - val, ok := fontUnderlineType[formatStyle.Font.Underline] + if style.Font.Italic { + fnt.I = &style.Font.Italic + } + if *fnt.Name.Val == "" { + *fnt.Name.Val = f.GetDefaultFont() + } + if style.Font.Strike { + strike := true + fnt.Strike = &strike + } + val, ok := fontUnderlineType[style.Font.Underline] if ok { - f.U = &attrValString{Val: val} + fnt.U = &attrValString{Val: stringPtr(val)} } - return &f + return &fnt } // 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, @@ -2018,61 +2077,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", @@ -2103,15 +2162,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: @@ -2124,7 +2183,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) @@ -2133,18 +2192,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: @@ -2157,36 +2216,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", @@ -2205,7 +2264,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) @@ -2240,21 +2299,21 @@ func setBorders(formatStyle *formatStyle) *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 @@ -2328,7 +2387,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) // } @@ -2641,6 +2700,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. @@ -2657,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) } @@ -2776,8 +2851,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 } diff --git a/styles_test.go b/styles_test.go index 54321bb..1ff0e4e 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" @@ -25,15 +27,15 @@ 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() 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) } } } @@ -165,3 +167,68 @@ func TestSetConditionalFormat(t *testing.T) { assert.EqualValues(t, testCase.rules, cf[0].CfRule, testCase.label) } } + +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"}}`) + assert.NoError(t, err) + 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.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) { + 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) +} + +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/table.go b/table.go index f3819d3..5a0e46f 100644 --- a/table.go +++ b/table.go @@ -1,11 +1,11 @@ -// 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. // // 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 @@ -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 // @@ -77,7 +79,8 @@ 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, "") + 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) if err != nil { @@ -115,35 +118,30 @@ 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 } 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) @@ -283,15 +281,39 @@ 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: filterDB, + Hidden: true, + LocalSheetID: intPtr(sheetID), + Data: filterRange, + } + 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) } diff --git a/table_test.go b/table_test.go new file mode 100644 index 0000000..89c03e2 --- /dev/null +++ b/table_test.go @@ -0,0 +1,125 @@ +package excelize + +import ( + "fmt" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +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 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))) + } + }) + } + + 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 17fc8d4..a7972e6 100644 --- a/templates.go +++ b/templates.go @@ -1,11 +1,11 @@ -// 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. // // 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. @@ -27,9 +27,9 @@ const templateContentTypes = `` -const templateStyles = `` +const templateStyles = `` -const templateSheet = `` +const templateSheet = `` const templateWorkbookRels = `` @@ -38,3 +38,5 @@ const templateDocpropsCore = `` const templateTheme = `` + +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}">` 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/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/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 new file mode 100644 index 0000000..9da5091 Binary files /dev/null and b/test/OverflowNumericCell.xlsx differ diff --git a/test/SharedStrings.xlsx b/test/SharedStrings.xlsx index 7b722d9..bcea2c8 100644 Binary files a/test/SharedStrings.xlsx and b/test/SharedStrings.xlsx differ 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 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/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/vmlDrawing.go b/vmlDrawing.go index 8b1d00f..f2d55f1 100644 --- a/vmlDrawing.go +++ b/vmlDrawing.go @@ -1,11 +1,11 @@ -// 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. // // 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 new file mode 100644 index 0000000..5668cf6 --- /dev/null +++ b/xmlApp.go @@ -0,0 +1,61 @@ +// 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 "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 + 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 +} + +// xlsxVectorVariant specifies the set of hyperlinks that were in this +// document when last saved. +type xlsxVectorVariant struct { + Content string `xml:",innerxml"` +} + +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 9c916bf..69d5d8c 100644 --- a/xmlCalcChain.go +++ b/xmlCalcChain.go @@ -1,11 +1,11 @@ -// 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. // // 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 @@ -18,6 +18,61 @@ type xlsxCalcChain struct { } // xlsxCalcChainC directly maps the c element. +// +// 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"` diff --git a/xmlChart.go b/xmlChart.go index 163812d..03b47a1 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -1,76 +1,76 @@ -// 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. // // 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" -// 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"` + 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. +// 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"` + 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 c:title element. This element specifies a +// 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"` + 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"` @@ -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. @@ -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 @@ -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"` @@ -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,232 @@ 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"` + RAngAx *attrValInt `xml:"rAngAx"` + DepthPercent *attrValInt `xml:"depthPercent"` + Perspective *attrValInt `xml:"perspective"` + ExtLst *xlsxExtLst `xml:"extLst"` } -// 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"` + BubbleChart *cCharts `xml:"bubbleChart"` + DoughnutChart *cCharts `xml:"doughnutChart"` + 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"` + 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. 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"` + BarDir *attrValString `xml:"barDir"` + BubbleScale *attrValFloat `xml:"bubbleScale"` + Grouping *attrValString `xml:"grouping"` + RadarStyle *attrValString `xml:"radarStyle"` + ScatterStyle *attrValString `xml:"scatterStyle"` + OfPieType *attrValString `xml:"ofPieType"` + 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"` + Smooth *attrValBool `xml:"smooth"` + Overlap *attrValInt `xml:"overlap"` + AxID []*attrValInt `xml:"axId"` } -// cAxs directly maps the c:catAx and c:valAx element. +// 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"` + AxID *attrValInt `xml:"axId"` + Scaling *cScaling `xml:"scaling"` + 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"` + TickLblPos *attrValString `xml:"tickLblPos"` + SpPr *cSpPr `xml:"spPr"` + TxPr *cTxPr `xml:"txPr"` + 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"` } -// cScaling directly maps the c:scaling element. This element contains +// cChartLines directly maps the chart lines content model. +type cChartLines struct { + SpPr *cSpPr `xml:"spPr"` +} + +// 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"` + 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"` + BubbleSize *cVal `xml:"bubbleSize"` + Bubble3D *attrValBool `xml:"bubble3D"` } -// cMarker (Marker) directly maps the c:marker element. This element specifies a +// 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"` @@ -496,11 +521,14 @@ 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"` + MinorGridlines bool `json:"minor_grid_lines"` 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"` @@ -569,13 +597,14 @@ type formatChart struct { 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. 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"` @@ -588,8 +617,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"` diff --git a/xmlChartSheet.go b/xmlChartSheet.go new file mode 100644 index 0000000..30a0693 --- /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"` +} diff --git a/xmlComments.go b/xmlComments.go index 5ffbecf..687c486 100644 --- a/xmlComments.go +++ b/xmlComments.go @@ -1,11 +1,11 @@ -// 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. // // 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 @@ -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. diff --git a/xmlContentTypes.go b/xmlContentTypes.go index e99b0b3..7acfe08 100644 --- a/xmlContentTypes.go +++ b/xmlContentTypes.go @@ -1,11 +1,11 @@ -// 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. // // 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 new file mode 100644 index 0000000..6f71a3e --- /dev/null +++ b/xmlCore.go @@ -0,0 +1,89 @@ +// 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 "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/xmlDecodeDrawing.go b/xmlDecodeDrawing.go index eead575..93e0e82 100644 --- a/xmlDecodeDrawing.go +++ b/xmlDecodeDrawing.go @@ -1,29 +1,65 @@ -// 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. // // 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" -// 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/xmlDrawing.go b/xmlDrawing.go index 89496c4..808bed5 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -1,11 +1,11 @@ -// 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. // // 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 @@ -13,32 +13,74 @@ 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" + 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" + 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" + 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" + 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 + // 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}" + 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"} +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 @@ -213,6 +255,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"` @@ -222,15 +265,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. @@ -368,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"` @@ -390,8 +442,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/xmlPivotCache.go b/xmlPivotCache.go new file mode 100644 index 0000000..feaec54 --- /dev/null +++ b/xmlPivotCache.go @@ -0,0 +1,229 @@ +// 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" + +// 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 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"` + 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 { + 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 +// 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 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,omitempty"` + MappingCount int `xml:"mappingCount,attr,omitempty"` + MemberPropertyField bool `xml:"memberPropertyField,attr,omitempty"` + SharedItems *xlsxSharedItems `xml:"sharedItems"` + FieldGroup *xlsxFieldGroup `xml:"fieldGroup"` + MpMap *xlsxX `xml:"mpMap"` + ExtLst *xlsxExtLst `xml:"extLst"` +} + +// 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"` + 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 { +} diff --git a/xmlPivotTable.go b/xmlPivotTable.go new file mode 100644 index 0000000..2eff026 --- /dev/null +++ b/xmlPivotTable.go @@ -0,0 +1,294 @@ +// 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 "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"` + 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,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"` + 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,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,omitempty"` + MergeItem bool `xml:"mergeItem,attr,omitempty"` + ShowDropZones bool `xml:"showDropZones,attr,omitempty"` + 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"` + 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,omitempty"` + 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. +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,omitempty"` + ColPageCount int `xml:"colPageCount,attr,omitempty"` +} + +// 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,omitempty"` + Axis string `xml:"axis,attr,omitempty"` + 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,omitempty"` + NumFmtID string `xml:"numFmtId,attr,omitempty"` + Outline bool `xml:"outline,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,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,omitempty"` + RankBy int `xml:"rankBy,attr,omitempty"` + 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"` + 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,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"` + D bool `xml:"d,attr,omitempty"` + E bool `xml:"e,attr,omitempty"` +} + +// 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"` + Field []*xlsxField `xml:"field"` +} + +// 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 { +} + +// xlsxColFields represents the collection of fields that are on the column +// axis of the PivotTable. +type xlsxColFields struct { + Count int `xml:"count,attr"` + Field []*xlsxField `xml:"field"` +} + +// 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,omitempty"` + Name string `xml:"name,attr,omitempty"` + Cap string `xml:"cap,attr,omitempty"` + 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,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"` +} + +// 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,omitempty"` + ShowColStripes bool `xml:"showColStripes,attr,omitempty"` + ShowLastColumn bool `xml:"showLastColumn,attr,omitempty"` +} diff --git a/xmlSharedStrings.go b/xmlSharedStrings.go index 3fcf3d5..a6525df 100644 --- a/xmlSharedStrings.go +++ b/xmlSharedStrings.go @@ -1,15 +1,18 @@ -// 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. // // 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" + "strings" +) // xlsxSST directly maps the sst element from the namespace // http://schemas.openxmlformats.org/spreadsheetml/2006/main. String values may @@ -25,20 +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"` } -// 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. +// 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 { + if s.T != nil { + rows.WriteString(s.T.Val) + } + } + return rows.String() + } + return x.T +} + +// 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 @@ -47,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 } diff --git a/xmlStyles.go b/xmlStyles.go index fc53f77..42d535b 100644 --- a/xmlStyles.go +++ b/xmlStyles.go @@ -1,11 +1,11 @@ -// 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. // // 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 @@ -82,28 +82,23 @@ type xlsxFonts struct { Font []*xlsxFont `xml:"font"` } -// font directly maps the font element. -type font 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"` - Color *xlsxColor `xml:"color"` - Sz *attrValInt `xml:"sz"` - U *attrValString `xml:"u"` - Scheme *attrValString `xml:"scheme"` -} - -// xlsxFont directly maps the font element. This element defines the properties -// for one of the fonts used in this workbook. +// 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"` + 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"` + 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"` } // xlsxFills directly maps the fills element. This element defines the cell @@ -191,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 @@ -214,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"` - QuotePrefix bool `xml:"quotePrefix,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"` } @@ -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"` @@ -318,48 +313,61 @@ type xlsxStyleColors struct { Color string `xml:",innerxml"` } -// 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"` +// 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"` } -// 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"` +// 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"` + Family string `json:"family"` + Size float64 `json:"size"` + Strike bool `json:"strike"` + Color string `json:"color"` +} + +// 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"` } diff --git a/xmlTable.go b/xmlTable.go index 6d27dc9..345337f 100644 --- a/xmlTable.go +++ b/xmlTable.go @@ -1,11 +1,11 @@ -// 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. // // 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 @@ -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/xmlTheme.go b/xmlTheme.go index 01d0054..76f13b4 100644 --- a/xmlTheme.go +++ b/xmlTheme.go @@ -1,11 +1,11 @@ -// 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. // // 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 ad66f42..bc59924 100644 --- a/xmlWorkbook.go +++ b/xmlWorkbook.go @@ -1,24 +1,24 @@ -// 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. // // 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" -// 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"` @@ -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"` @@ -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"` @@ -176,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"` } @@ -204,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"` @@ -289,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 +} diff --git a/xmlWorksheet.go b/xmlWorksheet.go index f3323cb..316ffd7 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -1,48 +1,65 @@ -// 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. // // 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" // 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"` - 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"` + ProtectedRanges *xlsxInnerXML `xml:"protectedRanges"` + Scenarios *xlsxInnerXML `xml:"scenarios"` AutoFilter *xlsxAutoFilter `xml:"autoFilter"` + SortState *xlsxSortState `xml:"sortState"` + DataConsolidate *xlsxInnerXML `xml:"dataConsolidate"` + CustomSheetViews *xlsxCustomSheetViews `xml:"customSheetViews"` 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"` PageSetUp *xlsxPageSetUp `xml:"pageSetup"` 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 *xlsxLegacyDrawingHF `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"` } // 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 @@ -53,49 +70,55 @@ 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"` + XMLName xml.Name `xml:"headerFooter"` + 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"` } -// 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 { - Content string `xml:",chardata"` +// 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:",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"` - 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 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"` + 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 int `xml:"verticalDpi,attr,omitempty"` } // xlsxPrintOptions directly maps the printOptions element in the namespace @@ -103,44 +126,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"` } @@ -154,13 +181,13 @@ 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"` 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"` @@ -202,16 +229,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 @@ -231,6 +260,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"` } @@ -239,22 +269,23 @@ 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 // 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 @@ -265,7 +296,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 @@ -295,6 +327,74 @@ 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 { + XMLName xml.Name `xml:"customSheetViews"` + 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 *xlsxExtLst `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"` @@ -303,13 +403,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"` @@ -324,16 +426,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"` + Type string `xml:"type,attr,omitempty"` + Formula1 string `xml:",innerxml"` + Formula2 string `xml:",innerxml"` } // xlsxC directly maps the c element in the namespace @@ -353,22 +455,19 @@ 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 *xlsxIS `xml:"is"` + XMLName xml.Name `xml:"c"` XMLSpace xml.Attr `xml:"space,attr,omitempty"` + 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. + F *xlsxF `xml:"f,omitempty"` // Formula + V string `xml:"v,omitempty"` // Value + IS *xlsxSI `xml:"is"` } -// 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"` +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 @@ -384,27 +483,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"` - AutoFilter bool `xml:"autoFilter,attr,omitempty"` - DeleteColumns bool `xml:"deleteColumns,attr,omitempty"` - DeleteRows bool `xml:"deleteRows,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"` - 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"` + 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 @@ -415,9 +515,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 @@ -425,8 +526,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 @@ -482,7 +584,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"` } @@ -491,6 +593,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"` } @@ -535,6 +638,7 @@ type xlsxHyperlink struct { // // type xlsxTableParts struct { + XMLName xml.Name `xml:"tableParts"` Count int `xml:"count,attr,omitempty"` TableParts []*xlsxTablePart `xml:"tablePart"` } @@ -552,7 +656,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 @@ -565,7 +670,121 @@ 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"` +} + +// 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"` +} + +// xlsxWorksheetExt directly maps the ext element in the worksheet. +type xlsxWorksheetExt struct { + XMLName xml.Name `xml:"ext"` + 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. @@ -627,3 +846,27 @@ 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 +} + +// FormatPageMargins directly maps the settings of page margins +type FormatPageMargins struct { + Bottom string + Footer string + Header string + Left string + Right string + Top string +}