This fixes #1643, fixes #1647 and fixes #1653

- Correction cell type when formatting date type cell value
- Add check for MID and MIDB formula functions num_chars arguments, prevent panic on specifying a negative number
- Ref #65, add support for 2 formula functions: SEARCH and SEARCHB
- Fix a v2.8.0 regression bug, error on set print area and print titles with built-in special defined name
- Add new exported function `GetPivotTables` for get pivot tables
- Add a new `Name` field in the `PivotTableOptions` to support specify pivot table name
- Using relative cell reference in the pivot table docs and unit tests
- Support adding slicer content type part internally
- Add new exported source relationship and namespace `NameSpaceSpreadSheetXR10`, `ContentTypeSlicer`, `ContentTypeSlicerCache`, and `SourceRelationshipSlicer`
- Add new exported extended URI `ExtURIPivotCacheDefinition`
- Fix formula argument wildcard match issues
- Update GitHub Actions configuration, test on Go 1.21.x with 1.21.1 and later
- Avoid corrupted workbooks generated by improving compatibility with internally indexed color styles
This commit is contained in:
xuri 2023-09-08 00:09:41 +08:00
parent ff5657ba87
commit ae64bcaabe
No known key found for this signature in database
GPG Key ID: BA5E5BB1C948EDF7
15 changed files with 506 additions and 161 deletions

View File

@ -5,7 +5,7 @@ jobs:
test:
strategy:
matrix:
go-version: [1.16.x, 1.17.x, 1.18.x, 1.19.x, 1.20.x]
go-version: [1.16.x, 1.17.x, 1.18.x, 1.19.x, 1.20.x, '>=1.21.1']
os: [ubuntu-latest, macos-latest, windows-latest]
targetplatform: [x86, x64]

191
calc.go
View File

@ -706,6 +706,8 @@ type formulaFuncs struct {
// ROWS
// RRI
// RSQ
// SEARCH
// SEARCHB
// SEC
// SECH
// SECOND
@ -9303,7 +9305,7 @@ func (fn *formulaFuncs) FdotDISTdotRT(argsList *list.List) formulaArg {
return fn.FDIST(argsList)
}
// prepareFinvArgs checking and prepare arguments for the formula function
// prepareFinvArgs checking and prepare arguments for the formula functions
// F.INV, F.INV.RT and FINV.
func (fn *formulaFuncs) prepareFinvArgs(name string, argsList *list.List) formulaArg {
if argsList.Len() != 3 {
@ -13612,17 +13614,16 @@ func (fn *formulaFuncs) FINDB(argsList *list.List) formulaArg {
return fn.find("FINDB", argsList)
}
// find is an implementation of the formula functions FIND and FINDB.
func (fn *formulaFuncs) find(name string, argsList *list.List) formulaArg {
// prepareFindArgs checking and prepare arguments for the formula functions
// FIND, FINDB, SEARCH and SEARCHB.
func (fn *formulaFuncs) prepareFindArgs(name string, argsList *list.List) formulaArg {
if argsList.Len() < 2 {
return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires at least 2 arguments", name))
}
if argsList.Len() > 3 {
return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s allows at most 3 arguments", name))
}
findText := argsList.Front().Value.(formulaArg).Value()
withinText := argsList.Front().Next().Value.(formulaArg).Value()
startNum, result := 1, 1
startNum := 1
if argsList.Len() == 3 {
numArg := argsList.Back().Value.(formulaArg).ToNumber()
if numArg.Type != ArgNumber {
@ -13633,19 +13634,44 @@ func (fn *formulaFuncs) find(name string, argsList *list.List) formulaArg {
}
startNum = int(numArg.Number)
}
return newListFormulaArg([]formulaArg{newNumberFormulaArg(float64(startNum))})
}
// find is an implementation of the formula functions FIND, FINDB, SEARCH and
// SEARCHB.
func (fn *formulaFuncs) find(name string, argsList *list.List) formulaArg {
args := fn.prepareFindArgs(name, argsList)
if args.Type != ArgList {
return args
}
findText := argsList.Front().Value.(formulaArg).Value()
withinText := argsList.Front().Next().Value.(formulaArg).Value()
startNum := int(args.List[0].Number)
if findText == "" {
return newNumberFormulaArg(float64(startNum))
}
for idx := range withinText {
if result < startNum {
result++
}
if strings.Index(withinText[idx:], findText) == 0 {
return newNumberFormulaArg(float64(result))
}
result++
dbcs, search := name == "FINDB" || name == "SEARCHB", name == "SEARCH" || name == "SEARCHB"
if search {
findText, withinText = strings.ToUpper(findText), strings.ToUpper(withinText)
}
return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
offset, ok := matchPattern(findText, withinText, dbcs, startNum)
if !ok {
return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
}
result := offset
if dbcs {
var pre int
for idx := range withinText {
if pre > offset {
break
}
if idx-pre > 1 {
result++
}
pre = idx
}
}
return newNumberFormulaArg(float64(result))
}
// LEFT function returns a specified number of characters from the start of a
@ -13780,20 +13806,37 @@ func (fn *formulaFuncs) mid(name string, argsList *list.List) formulaArg {
return numCharsArg
}
startNum := int(startNumArg.Number)
if startNum < 0 {
if startNum < 1 || numCharsArg.Number < 0 {
return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
}
if name == "MIDB" {
textLen := len(text)
if startNum > textLen {
return newStringFormulaArg("")
var result string
var cnt, offset int
for _, char := range text {
offset++
var dbcs bool
if utf8.RuneLen(char) > 1 {
dbcs = true
offset++
}
if cnt == int(numCharsArg.Number) {
break
}
if offset+1 > startNum {
if dbcs {
if cnt+2 > int(numCharsArg.Number) {
result += string(char)[:1]
break
}
result += string(char)
cnt += 2
} else {
result += string(char)
cnt++
}
}
}
startNum--
endNum := startNum + int(numCharsArg.Number)
if endNum > textLen+1 {
return newStringFormulaArg(text[startNum:])
}
return newStringFormulaArg(text[startNum:endNum])
return newStringFormulaArg(result)
}
// MID
textLen := utf8.RuneCountInString(text)
@ -13922,6 +13965,23 @@ func (fn *formulaFuncs) RIGHTB(argsList *list.List) formulaArg {
return fn.leftRight("RIGHTB", argsList)
}
// SEARCH function returns the position of a specified character or sub-string
// within a supplied text string. The syntax of the function is:
//
// SEARCH(search_text,within_text,[start_num])
func (fn *formulaFuncs) SEARCH(argsList *list.List) formulaArg {
return fn.find("SEARCH", argsList)
}
// SEARCHB functions locate one text string within a second text string, and
// return the number of the starting position of the first text string from the
// first character of the second text string. The syntax of the function is:
//
// SEARCHB(search_text,within_text,[start_num])
func (fn *formulaFuncs) SEARCHB(argsList *list.List) formulaArg {
return fn.find("SEARCHB", argsList)
}
// SUBSTITUTE function replaces one or more instances of a given text string,
// within an original text string. The syntax of the function is:
//
@ -14255,46 +14315,57 @@ func (fn *formulaFuncs) CHOOSE(argsList *list.List) formulaArg {
return arg.Value.(formulaArg)
}
// deepMatchRune finds whether the text deep matches/satisfies the pattern
// string.
func deepMatchRune(str, pattern []rune, simple bool) bool {
for len(pattern) > 0 {
switch pattern[0] {
default:
if len(str) == 0 || str[0] != pattern[0] {
return false
}
case '?':
if len(str) == 0 && !simple {
return false
}
case '*':
return deepMatchRune(str, pattern[1:], simple) ||
(len(str) > 0 && deepMatchRune(str[1:], pattern, simple))
}
str = str[1:]
pattern = pattern[1:]
// matchPatternToRegExp convert find text pattern to regular expression.
func matchPatternToRegExp(findText string, dbcs bool) (string, bool) {
var (
exp string
wildCard bool
mark = "."
)
if dbcs {
mark = "(?:(?:[\\x00-\\x0081])|(?:[\\xFF61-\\xFFA0])|(?:[\\xF8F1-\\xF8F4])|[0-9A-Za-z])"
}
return len(str) == 0 && len(pattern) == 0
for _, char := range findText {
if strings.ContainsAny(string(char), ".+$^[](){}|/") {
exp += fmt.Sprintf("\\%s", string(char))
continue
}
if char == '?' {
wildCard = true
exp += mark
continue
}
if char == '*' {
wildCard = true
exp += ".*"
continue
}
exp += string(char)
}
return fmt.Sprintf("^%s", exp), wildCard
}
// matchPattern finds whether the text matches or satisfies the pattern
// string. The pattern supports '*' and '?' wildcards in the pattern string.
func matchPattern(pattern, name string) (matched bool) {
if pattern == "" {
return name == pattern
func matchPattern(findText, withinText string, dbcs bool, startNum int) (int, bool) {
exp, wildCard := matchPatternToRegExp(findText, dbcs)
offset := 1
for idx := range withinText {
if offset < startNum {
offset++
continue
}
if wildCard {
if ok, _ := regexp.MatchString(exp, withinText[idx:]); ok {
break
}
}
if strings.Index(withinText[idx:], findText) == 0 {
break
}
offset++
}
if pattern == "*" {
return true
}
rName, rPattern := make([]rune, 0, len(name)), make([]rune, 0, len(pattern))
for _, r := range name {
rName = append(rName, r)
}
for _, r := range pattern {
rPattern = append(rPattern, r)
}
return deepMatchRune(rName, rPattern, false)
return offset, utf8.RuneCountInString(withinText) != offset-1
}
// compareFormulaArg compares the left-hand sides and the right-hand sides'
@ -14319,7 +14390,7 @@ func compareFormulaArg(lhs, rhs, matchMode formulaArg, caseSensitive bool) byte
ls, rs = strings.ToLower(ls), strings.ToLower(rs)
}
if matchMode.Number == matchModeWildcard {
if matchPattern(rs, ls) {
if _, ok := matchPattern(rs, ls, false, 0); ok {
return criteriaEq
}
}

View File

@ -764,6 +764,30 @@ func TestCalcCellValue(t *testing.T) {
"=ROUNDUP(-11.111,2)": "-11.12",
"=ROUNDUP(-11.111,-1)": "-20",
"=ROUNDUP(ROUNDUP(100,1),-1)": "100",
// SEARCH
"=SEARCH(\"s\",F1)": "1",
"=SEARCH(\"s\",F1,2)": "5",
"=SEARCH(\"e\",F1)": "4",
"=SEARCH(\"e*\",F1)": "4",
"=SEARCH(\"?e\",F1)": "3",
"=SEARCH(\"??e\",F1)": "2",
"=SEARCH(6,F2)": "2",
"=SEARCH(\"?\",\"你好world\")": "1",
"=SEARCH(\"?l\",\"你好world\")": "5",
"=SEARCH(\"?+\",\"你好 1+2\")": "4",
"=SEARCH(\" ?+\",\"你好 1+2\")": "3",
// SEARCHB
"=SEARCHB(\"s\",F1)": "1",
"=SEARCHB(\"s\",F1,2)": "5",
"=SEARCHB(\"e\",F1)": "4",
"=SEARCHB(\"e*\",F1)": "4",
"=SEARCHB(\"?e\",F1)": "3",
"=SEARCHB(\"??e\",F1)": "2",
"=SEARCHB(6,F2)": "2",
"=SEARCHB(\"?\",\"你好world\")": "5",
"=SEARCHB(\"?l\",\"你好world\")": "7",
"=SEARCHB(\"?+\",\"你好 1+2\")": "6",
"=SEARCHB(\" ?+\",\"你好 1+2\")": "5",
// SEC
"=_xlfn.SEC(-3.14159265358979)": "-1",
"=_xlfn.SEC(0)": "1",
@ -1707,6 +1731,7 @@ func TestCalcCellValue(t *testing.T) {
"=FIND(\"i\",\"Original Text\",4)": "5",
"=FIND(\"\",\"Original Text\")": "1",
"=FIND(\"\",\"Original Text\",2)": "2",
"=FIND(\"s\",\"Sales\",2)": "5",
// FINDB
"=FINDB(\"T\",\"Original Text\")": "10",
"=FINDB(\"t\",\"Original Text\")": "13",
@ -1714,6 +1739,7 @@ func TestCalcCellValue(t *testing.T) {
"=FINDB(\"i\",\"Original Text\",4)": "5",
"=FINDB(\"\",\"Original Text\")": "1",
"=FINDB(\"\",\"Original Text\",2)": "2",
"=FINDB(\"s\",\"Sales\",2)": "5",
// LEFT
"=LEFT(\"Original Text\")": "O",
"=LEFT(\"Original Text\",4)": "Orig",
@ -1752,14 +1778,18 @@ func TestCalcCellValue(t *testing.T) {
"=MID(\"255 years\",3,1)": "5",
"=MID(\"text\",3,6)": "xt",
"=MID(\"text\",6,0)": "",
"=MID(\"オリジナルテキスト\",6,4)": "テキスト",
"=MID(\"オリジナルテキスト\",3,5)": "ジナルテキ",
"=MID(\"你好World\",5,1)": "r",
"=MID(\"\u30AA\u30EA\u30B8\u30CA\u30EB\u30C6\u30AD\u30B9\u30C8\",6,4)": "\u30C6\u30AD\u30B9\u30C8",
"=MID(\"\u30AA\u30EA\u30B8\u30CA\u30EB\u30C6\u30AD\u30B9\u30C8\",3,5)": "\u30B8\u30CA\u30EB\u30C6\u30AD",
// MIDB
"=MIDB(\"Original Text\",7,1)": "a",
"=MIDB(\"Original Text\",4,7)": "ginal T",
"=MIDB(\"255 years\",3,1)": "5",
"=MIDB(\"text\",3,6)": "xt",
"=MIDB(\"text\",6,0)": "",
"=MIDB(\"你好World\",5,1)": "W",
"=MIDB(\"\u30AA\u30EA\u30B8\u30CA\u30EB\u30C6\u30AD\u30B9\u30C8\",6,4)": "\u30B8\u30CA",
"=MIDB(\"\u30AA\u30EA\u30B8\u30CA\u30EB\u30C6\u30AD\u30B9\u30C8\",3,5)": "\u30EA\u30B8\xe3",
// PROPER
"=PROPER(\"this is a test sentence\")": "This Is A Test Sentence",
"=PROPER(\"THIS IS A TEST SENTENCE\")": "This Is A Test Sentence",
@ -2695,6 +2725,17 @@ func TestCalcCellValue(t *testing.T) {
"=ROUNDUP()": {"#VALUE!", "ROUNDUP requires 2 numeric arguments"},
`=ROUNDUP("X",1)`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
`=ROUNDUP(1,"X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
// SEARCH
"=SEARCH()": {"#VALUE!", "SEARCH requires at least 2 arguments"},
"=SEARCH(1,A1,1,1)": {"#VALUE!", "SEARCH allows at most 3 arguments"},
"=SEARCH(2,A1)": {"#VALUE!", "#VALUE!"},
"=SEARCH(1,A1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
// SEARCHB
"=SEARCHB()": {"#VALUE!", "SEARCHB requires at least 2 arguments"},
"=SEARCHB(1,A1,1,1)": {"#VALUE!", "SEARCHB allows at most 3 arguments"},
"=SEARCHB(2,A1)": {"#VALUE!", "#VALUE!"},
"=SEARCHB(\"?w\",\"你好world\")": {"#VALUE!", "#VALUE!"},
"=SEARCHB(1,A1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
// SEC
"=_xlfn.SEC()": {"#VALUE!", "SEC requires 1 numeric argument"},
`=_xlfn.SEC("X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"},
@ -3781,12 +3822,14 @@ func TestCalcCellValue(t *testing.T) {
"=LOWER(1,2)": {"#VALUE!", "LOWER requires 1 argument"},
// MID
"=MID()": {"#VALUE!", "MID requires 3 arguments"},
"=MID(\"\",-1,1)": {"#VALUE!", "#VALUE!"},
"=MID(\"\",0,1)": {"#VALUE!", "#VALUE!"},
"=MID(\"\",1,-1)": {"#VALUE!", "#VALUE!"},
"=MID(\"\",\"\",1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
"=MID(\"\",1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
// MIDB
"=MIDB()": {"#VALUE!", "MIDB requires 3 arguments"},
"=MIDB(\"\",-1,1)": {"#VALUE!", "#VALUE!"},
"=MIDB(\"\",0,1)": {"#VALUE!", "#VALUE!"},
"=MIDB(\"\",1,-1)": {"#VALUE!", "#VALUE!"},
"=MIDB(\"\",\"\",1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
"=MIDB(\"\",1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"},
// PROPER
@ -4684,14 +4727,6 @@ func TestCalcCompareFormulaArg(t *testing.T) {
assert.Equal(t, compareFormulaArg(formulaArg{Type: ArgUnknown}, formulaArg{Type: ArgUnknown}, newNumberFormulaArg(matchModeMaxLess), false), criteriaErr)
}
func TestCalcMatchPattern(t *testing.T) {
assert.True(t, matchPattern("", ""))
assert.True(t, matchPattern("file/*", "file/abc/bcd/def"))
assert.True(t, matchPattern("*", ""))
assert.False(t, matchPattern("?", ""))
assert.False(t, matchPattern("file/?", "file/abc/bcd/def"))
}
func TestCalcTRANSPOSE(t *testing.T) {
cellData := [][]interface{}{
{"a", "d"},
@ -5376,7 +5411,6 @@ func TestCalcXLOOKUP(t *testing.T) {
"=XLOOKUP()": {"#VALUE!", "XLOOKUP requires at least 3 arguments"},
"=XLOOKUP($C3,$C5:$C5,$C6:$C17,NA(),0,2,1)": {"#VALUE!", "XLOOKUP allows at most 6 arguments"},
"=XLOOKUP($C3,$C5,$C6,NA(),0,2)": {"#N/A", "#N/A"},
"=XLOOKUP(\"?\",B2:B9,C2:C9,NA(),2)": {"#N/A", "#N/A"},
"=XLOOKUP($C3,$C4:$D5,$C6:$C17,NA(),0,2)": {"#VALUE!", "#VALUE!"},
"=XLOOKUP($C3,$C5:$C5,$C6:$G17,NA(),0,-2)": {"#VALUE!", "#VALUE!"},
"=XLOOKUP($C3,$C5:$G5,$C6:$F7,NA(),0,2)": {"#VALUE!", "#VALUE!"},

View File

@ -565,7 +565,7 @@ func (c *xlsxC) getCellDate(f *File, raw bool) (string, error) {
c.V = strconv.FormatFloat(excelTime, 'G', 15, 64)
}
}
return f.formattedValue(c, raw, CellTypeBool)
return f.formattedValue(c, raw, CellTypeDate)
}
// getValueFrom return a value from a column/row cell, this function is

View File

@ -506,6 +506,8 @@ func (f *File) addContentTypePart(index int, contentType string) error {
"pivotTable": "/xl/pivotTables/pivotTable" + strconv.Itoa(index) + ".xml",
"pivotCache": "/xl/pivotCache/pivotCacheDefinition" + strconv.Itoa(index) + ".xml",
"sharedStrings": "/xl/sharedStrings.xml",
"slicer": "/xl/slicers/slicer" + strconv.Itoa(index) + ".xml",
"slicerCache": "/xl/slicerCaches/slicerCache" + strconv.Itoa(index) + ".xml",
}
contentTypes := map[string]string{
"chart": ContentTypeDrawingML,
@ -516,6 +518,8 @@ func (f *File) addContentTypePart(index int, contentType string) error {
"pivotTable": ContentTypeSpreadSheetMLPivotTable,
"pivotCache": ContentTypeSpreadSheetMLPivotCacheDefinition,
"sharedStrings": ContentTypeSpreadSheetMLSharedStrings,
"slicer": ContentTypeSlicer,
"slicerCache": ContentTypeSlicerCache,
}
s, ok := setContentType[contentType]
if ok {

View File

@ -12,10 +12,17 @@
package excelize
import (
"bytes"
"encoding/xml"
"fmt"
"io"
"path/filepath"
"reflect"
"strconv"
"strings"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
// PivotTableOptions directly maps the format settings of the pivot table.
@ -29,6 +36,7 @@ type PivotTableOptions struct {
pivotTableSheetName string
DataRange string
PivotTableRange string
Name string
Rows []PivotTableField
Columns []PivotTableField
Data []PivotTableField
@ -115,8 +123,8 @@ type PivotTableField struct {
// f.SetCellValue("Sheet1", fmt.Sprintf("E%d", row), region[rand.Intn(4)])
// }
// if err := f.AddPivotTable(&excelize.PivotTableOptions{
// DataRange: "Sheet1!$A$1:$E$31",
// PivotTableRange: "Sheet1!$G$2:$M$34",
// DataRange: "Sheet1!A1:E31",
// PivotTableRange: "Sheet1!G2:M34",
// Rows: []excelize.PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
// Filter: []excelize.PivotTableField{{Data: "Region"}},
// Columns: []excelize.PivotTableField{{Data: "Type", DefaultSubtotal: true}},
@ -181,6 +189,9 @@ func (f *File) parseFormatPivotTableSet(opts *PivotTableOptions) (*xlsxWorksheet
if err != nil {
return nil, "", fmt.Errorf("parameter 'PivotTableRange' parsing error: %s", err.Error())
}
if len(opts.Name) > MaxFieldLength {
return nil, "", ErrNameLength
}
opts.pivotTableSheetName = pivotTableSheetName
dataRange := f.getDefinedNameRefTo(opts.DataRange, pivotTableSheetName)
if dataRange == "" {
@ -334,7 +345,7 @@ func (f *File) addPivotTable(cacheID, pivotTableID int, pivotTableXML string, op
return opts.PivotTableStyleName
}
pt := xlsxPivotTableDefinition{
Name: fmt.Sprintf("Pivot Table%d", pivotTableID),
Name: opts.Name,
CacheID: cacheID,
RowGrandTotals: &opts.RowGrandTotals,
ColGrandTotals: &opts.ColGrandTotals,
@ -376,7 +387,9 @@ func (f *File) addPivotTable(cacheID, pivotTableID int, pivotTableXML string, op
ShowLastColumn: opts.ShowLastColumn,
},
}
if pt.Name == "" {
pt.Name = fmt.Sprintf("PivotTable%d", pivotTableID)
}
// pivot fields
_ = f.addPivotFields(&pt, opts)
@ -604,8 +617,8 @@ func (f *File) addPivotFields(pt *xlsxPivotTableDefinition, opts *PivotTableOpti
return err
}
// countPivotTables provides a function to get drawing files count storage in
// the folder xl/pivotTables.
// countPivotTables provides a function to get pivot table files count storage
// in the folder xl/pivotTables.
func (f *File) countPivotTables() int {
count := 0
f.Pkg.Range(func(k, v interface{}) bool {
@ -617,8 +630,8 @@ func (f *File) countPivotTables() int {
return count
}
// countPivotCache provides a function to get drawing files count storage in
// the folder xl/pivotCache.
// countPivotCache provides a function to get pivot table cache definition files
// count storage in the folder xl/pivotCache.
func (f *File) countPivotCache() int {
count := 0
f.Pkg.Range(func(k, v interface{}) bool {
@ -719,3 +732,157 @@ func (f *File) addWorkbookPivotCache(RID int) int {
})
return cacheID
}
// GetPivotTables returns all pivot table definitions in a worksheet by given
// worksheet name.
func (f *File) GetPivotTables(sheet string) ([]PivotTableOptions, error) {
var pivotTables []PivotTableOptions
name, ok := f.getSheetXMLPath(sheet)
if !ok {
return pivotTables, newNoExistSheetError(sheet)
}
rels := "xl/worksheets/_rels/" + strings.TrimPrefix(name, "xl/worksheets/") + ".rels"
sheetRels, err := f.relsReader(rels)
if err != nil {
return pivotTables, err
}
if sheetRels == nil {
sheetRels = &xlsxRelationships{}
}
for _, v := range sheetRels.Relationships {
if v.Type == SourceRelationshipPivotTable {
pivotTableXML := strings.ReplaceAll(v.Target, "..", "xl")
pivotCacheRels := "xl/pivotTables/_rels/" + filepath.Base(v.Target) + ".rels"
pivotTable, err := f.getPivotTable(sheet, pivotTableXML, pivotCacheRels)
if err != nil {
return pivotTables, err
}
pivotTables = append(pivotTables, pivotTable)
}
}
return pivotTables, nil
}
// getPivotTable provides a function to get a pivot table definition by given
// worksheet name, pivot table XML path and pivot cache relationship XML path.
func (f *File) getPivotTable(sheet, pivotTableXML, pivotCacheRels string) (PivotTableOptions, error) {
var opts PivotTableOptions
rels, err := f.relsReader(pivotCacheRels)
if err != nil {
return opts, err
}
var pivotCacheXML string
for _, v := range rels.Relationships {
if v.Type == SourceRelationshipPivotCache {
pivotCacheXML = strings.ReplaceAll(v.Target, "..", "xl")
break
}
}
pc, err := f.pivotCacheReader(pivotCacheXML)
if err != nil {
return opts, err
}
pt, err := f.pivotTableReader(pivotTableXML)
if err != nil {
return opts, err
}
dataRange := fmt.Sprintf("%s!%s", pc.CacheSource.WorksheetSource.Sheet, pc.CacheSource.WorksheetSource.Ref)
opts = PivotTableOptions{
pivotTableSheetName: sheet,
DataRange: dataRange,
PivotTableRange: fmt.Sprintf("%s!%s", sheet, pt.Location.Ref),
Name: pt.Name,
}
fields := []string{"RowGrandTotals", "ColGrandTotals", "ShowDrill", "UseAutoFormatting", "PageOverThenDown", "MergeItem", "CompactData", "ShowError"}
immutable, mutable := reflect.ValueOf(*pt), reflect.ValueOf(&opts).Elem()
for _, field := range fields {
immutableField := immutable.FieldByName(field)
if immutableField.Kind() == reflect.Ptr && !immutableField.IsNil() && immutableField.Elem().Kind() == reflect.Bool {
mutable.FieldByName(field).SetBool(immutableField.Elem().Bool())
}
}
if si := pt.PivotTableStyleInfo; si != nil {
opts.ShowRowHeaders = si.ShowRowHeaders
opts.ShowColHeaders = si.ShowColHeaders
opts.ShowRowStripes = si.ShowRowStripes
opts.ShowColStripes = si.ShowColStripes
opts.ShowLastColumn = si.ShowLastColumn
opts.PivotTableStyleName = si.Name
}
order, _ := f.getPivotFieldsOrder(&PivotTableOptions{DataRange: dataRange, pivotTableSheetName: pt.Name})
f.extractPivotTableFields(order, pt, &opts)
return opts, err
}
// pivotTableReader provides a function to get the pointer to the structure
// after deserialization of xl/pivotTables/pivotTable%d.xml.
func (f *File) pivotTableReader(path string) (*xlsxPivotTableDefinition, error) {
content, ok := f.Pkg.Load(path)
pivotTable := &xlsxPivotTableDefinition{}
if ok && content != nil {
if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(content.([]byte)))).
Decode(pivotTable); err != nil && err != io.EOF {
return nil, err
}
}
return pivotTable, nil
}
// pivotCacheReader provides a function to get the pointer to the structure
// after deserialization of xl/pivotCache/pivotCacheDefinition%d.xml.
func (f *File) pivotCacheReader(path string) (*xlsxPivotCacheDefinition, error) {
content, ok := f.Pkg.Load(path)
pivotCache := &xlsxPivotCacheDefinition{}
if ok && content != nil {
if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(content.([]byte)))).
Decode(pivotCache); err != nil && err != io.EOF {
return nil, err
}
}
return pivotCache, nil
}
// extractPivotTableFields provides a function to extract all pivot table fields
// settings by given pivot table fields.
func (f *File) extractPivotTableFields(order []string, pt *xlsxPivotTableDefinition, opts *PivotTableOptions) {
for fieldIdx, field := range pt.PivotFields.PivotField {
if field.Axis == "axisRow" {
opts.Rows = append(opts.Rows, extractPivotTableField(order[fieldIdx], field))
}
if field.Axis == "axisCol" {
opts.Columns = append(opts.Columns, extractPivotTableField(order[fieldIdx], field))
}
if field.Axis == "axisPage" {
opts.Filter = append(opts.Filter, extractPivotTableField(order[fieldIdx], field))
}
}
if pt.DataFields != nil {
for _, field := range pt.DataFields.DataField {
opts.Data = append(opts.Data, PivotTableField{
Data: order[field.Fld],
Name: field.Name,
Subtotal: cases.Title(language.English).String(field.Subtotal),
})
}
}
}
// extractPivotTableField provides a function to extract pivot table field
// settings by given pivot table fields.
func extractPivotTableField(data string, fld *xlsxPivotField) PivotTableField {
pivotTableField := PivotTableField{
Data: data,
}
fields := []string{"Compact", "Name", "Outline", "Subtotal", "DefaultSubtotal"}
immutable, mutable := reflect.ValueOf(*fld), reflect.ValueOf(&pivotTableField).Elem()
for _, field := range fields {
immutableField := immutable.FieldByName(field)
if immutableField.Kind() == reflect.String {
mutable.FieldByName(field).SetString(immutableField.String())
}
if immutableField.Kind() == reflect.Ptr && !immutableField.IsNil() && immutableField.Elem().Kind() == reflect.Bool {
mutable.FieldByName(field).SetBool(immutableField.Elem().Bool())
}
}
return pivotTableField
}

View File

@ -10,7 +10,7 @@ import (
"github.com/stretchr/testify/assert"
)
func TestAddPivotTable(t *testing.T) {
func TestPivotTable(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"}
@ -25,25 +25,33 @@ func TestAddPivotTable(t *testing.T) {
assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("D%d", row), rand.Intn(5000)))
assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("E%d", row), region[rand.Intn(4)]))
}
assert.NoError(t, f.AddPivotTable(&PivotTableOptions{
DataRange: "Sheet1!$A$1:$E$31",
PivotTableRange: "Sheet1!$G$2:$M$34",
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
Filter: []PivotTableField{{Data: "Region"}},
Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
Data: []PivotTableField{{Data: "Sales", Subtotal: "Sum", Name: "Summarize by Sum"}},
RowGrandTotals: true,
ColGrandTotals: true,
ShowDrill: true,
ShowRowHeaders: true,
ShowColHeaders: true,
ShowLastColumn: true,
ShowError: true,
}))
expected := &PivotTableOptions{
DataRange: "Sheet1!A1:E31",
PivotTableRange: "Sheet1!G2:M34",
Name: "PivotTable1",
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
Filter: []PivotTableField{{Data: "Region"}},
Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
Data: []PivotTableField{{Data: "Sales", Subtotal: "Sum", Name: "Summarize by Sum"}},
RowGrandTotals: true,
ColGrandTotals: true,
ShowDrill: true,
ShowRowHeaders: true,
ShowColHeaders: true,
ShowLastColumn: true,
ShowError: true,
PivotTableStyleName: "PivotStyleLight16",
}
assert.NoError(t, f.AddPivotTable(expected))
// Test get pivot table
pivotTables, err := f.GetPivotTables("Sheet1")
assert.NoError(t, err)
assert.Len(t, pivotTables, 1)
assert.Equal(t, *expected, pivotTables[0])
// Use different order of coordinate tests
assert.NoError(t, f.AddPivotTable(&PivotTableOptions{
DataRange: "Sheet1!$A$1:$E$31",
PivotTableRange: "Sheet1!$U$34:$O$2",
DataRange: "Sheet1!A1:E31",
PivotTableRange: "Sheet1!U34:O2",
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
Data: []PivotTableField{{Data: "Sales", Subtotal: "Average", Name: "Summarize by Average"}},
@ -54,10 +62,15 @@ func TestAddPivotTable(t *testing.T) {
ShowColHeaders: true,
ShowLastColumn: true,
}))
// Test get pivot table with default style name
pivotTables, err = f.GetPivotTables("Sheet1")
assert.NoError(t, err)
assert.Len(t, pivotTables, 2)
assert.Equal(t, "PivotStyleLight16", pivotTables[1].PivotTableStyleName)
assert.NoError(t, f.AddPivotTable(&PivotTableOptions{
DataRange: "Sheet1!$A$1:$E$31",
PivotTableRange: "Sheet1!$W$2:$AC$34",
DataRange: "Sheet1!A1:E31",
PivotTableRange: "Sheet1!W2:AC34",
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
Columns: []PivotTableField{{Data: "Region"}},
Data: []PivotTableField{{Data: "Sales", Subtotal: "Count", Name: "Summarize by Count"}},
@ -69,8 +82,8 @@ func TestAddPivotTable(t *testing.T) {
ShowLastColumn: true,
}))
assert.NoError(t, f.AddPivotTable(&PivotTableOptions{
DataRange: "Sheet1!$A$1:$E$31",
PivotTableRange: "Sheet1!$G$42:$W$55",
DataRange: "Sheet1!A1:E31",
PivotTableRange: "Sheet1!G42:W55",
Rows: []PivotTableField{{Data: "Month"}},
Columns: []PivotTableField{{Data: "Region", DefaultSubtotal: true}, {Data: "Year"}},
Data: []PivotTableField{{Data: "Sales", Subtotal: "CountNums", Name: "Summarize by CountNums"}},
@ -82,8 +95,8 @@ func TestAddPivotTable(t *testing.T) {
ShowLastColumn: true,
}))
assert.NoError(t, f.AddPivotTable(&PivotTableOptions{
DataRange: "Sheet1!$A$1:$E$31",
PivotTableRange: "Sheet1!$AE$2:$AG$33",
DataRange: "Sheet1!A1:E31",
PivotTableRange: "Sheet1!AE2:AG33",
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
Data: []PivotTableField{{Data: "Sales", Subtotal: "Max", Name: "Summarize by Max"}, {Data: "Sales", Subtotal: "Average", Name: "Average of Sales"}},
RowGrandTotals: true,
@ -95,8 +108,8 @@ func TestAddPivotTable(t *testing.T) {
}))
// Create pivot table with empty subtotal field name and specified style
assert.NoError(t, f.AddPivotTable(&PivotTableOptions{
DataRange: "Sheet1!$A$1:$E$31",
PivotTableRange: "Sheet1!$AJ$2:$AP1$35",
DataRange: "Sheet1!A1:E31",
PivotTableRange: "Sheet1!AJ2:AP135",
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
Filter: []PivotTableField{{Data: "Region"}},
Columns: []PivotTableField{},
@ -109,11 +122,11 @@ func TestAddPivotTable(t *testing.T) {
ShowLastColumn: true,
PivotTableStyleName: "PivotStyleLight19",
}))
_, err := f.NewSheet("Sheet2")
_, err = f.NewSheet("Sheet2")
assert.NoError(t, err)
assert.NoError(t, f.AddPivotTable(&PivotTableOptions{
DataRange: "Sheet1!$A$1:$E$31",
PivotTableRange: "Sheet2!$A$1:$AN$17",
DataRange: "Sheet1!A1:E31",
PivotTableRange: "Sheet2!A1:AN17",
Rows: []PivotTableField{{Data: "Month"}},
Columns: []PivotTableField{{Data: "Region", DefaultSubtotal: true}, {Data: "Type", DefaultSubtotal: true}, {Data: "Year"}},
Data: []PivotTableField{{Data: "Sales", Subtotal: "Min", Name: "Summarize by Min"}},
@ -125,8 +138,8 @@ func TestAddPivotTable(t *testing.T) {
ShowLastColumn: true,
}))
assert.NoError(t, f.AddPivotTable(&PivotTableOptions{
DataRange: "Sheet1!$A$1:$E$31",
PivotTableRange: "Sheet2!$A$20:$AR$60",
DataRange: "Sheet1!A1:E31",
PivotTableRange: "Sheet2!A20:AR60",
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Type"}},
Columns: []PivotTableField{{Data: "Region", DefaultSubtotal: true}, {Data: "Year"}},
Data: []PivotTableField{{Data: "Sales", Subtotal: "Product", Name: "Summarize by Product"}},
@ -140,13 +153,13 @@ func TestAddPivotTable(t *testing.T) {
// Create pivot table with many data, many rows, many cols and defined name
assert.NoError(t, f.SetDefinedName(&DefinedName{
Name: "dataRange",
RefersTo: "Sheet1!$A$1:$E$31",
RefersTo: "Sheet1!A1:E31",
Comment: "Pivot Table Data Range",
Scope: "Sheet2",
}))
assert.NoError(t, f.AddPivotTable(&PivotTableOptions{
DataRange: "dataRange",
PivotTableRange: "Sheet2!$A$65:$AJ$100",
PivotTableRange: "Sheet2!A65:AJ100",
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
Columns: []PivotTableField{{Data: "Region", DefaultSubtotal: true}, {Data: "Type"}},
Data: []PivotTableField{{Data: "Sales", Subtotal: "Sum", Name: "Sum of Sales"}, {Data: "Sales", Subtotal: "Average", Name: "Average of Sales"}},
@ -160,58 +173,64 @@ func TestAddPivotTable(t *testing.T) {
// Test empty pivot table options
assert.EqualError(t, f.AddPivotTable(nil), ErrParameterRequired.Error())
// Test add pivot table with custom name which exceeds the max characters limit
assert.Equal(t, ErrNameLength, f.AddPivotTable(&PivotTableOptions{
DataRange: "dataRange",
PivotTableRange: "Sheet2!A65:AJ100",
Name: strings.Repeat("c", MaxFieldLength+1),
}))
// Test invalid data range
assert.EqualError(t, f.AddPivotTable(&PivotTableOptions{
DataRange: "Sheet1!$A$1:$A$1",
PivotTableRange: "Sheet1!$U$34:$O$2",
DataRange: "Sheet1!A1:A1",
PivotTableRange: "Sheet1!U34:O2",
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
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(&PivotTableOptions{
DataRange: "$A$1:$E$31",
PivotTableRange: "Sheet1!$U$34:$O$2",
DataRange: "A1:E31",
PivotTableRange: "Sheet1!U34:O2",
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
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(&PivotTableOptions{
DataRange: "SheetN!$A$1:$E$31",
PivotTableRange: "Sheet1!$U$34:$O$2",
DataRange: "SheetN!A1:E31",
PivotTableRange: "Sheet1!U34:O2",
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
Data: []PivotTableField{{Data: "Sales"}},
}), "sheet SheetN does not exist")
// Test the pivot table range of the worksheet that is not declared
assert.EqualError(t, f.AddPivotTable(&PivotTableOptions{
DataRange: "Sheet1!$A$1:$E$31",
PivotTableRange: "$U$34:$O$2",
DataRange: "Sheet1!A1:E31",
PivotTableRange: "U34:O2",
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
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(&PivotTableOptions{
DataRange: "Sheet1!$A$1:$E$31",
PivotTableRange: "SheetN!$U$34:$O$2",
DataRange: "Sheet1!A1:E31",
PivotTableRange: "SheetN!U34:O2",
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
Data: []PivotTableField{{Data: "Sales"}},
}), "sheet SheetN does not exist")
// Test not exists worksheet in data range
assert.EqualError(t, f.AddPivotTable(&PivotTableOptions{
DataRange: "SheetN!$A$1:$E$31",
PivotTableRange: "Sheet1!$U$34:$O$2",
DataRange: "SheetN!A1:E31",
PivotTableRange: "Sheet1!U34:O2",
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
Data: []PivotTableField{{Data: "Sales"}},
}), "sheet SheetN does not exist")
// Test invalid row number in data range
assert.EqualError(t, f.AddPivotTable(&PivotTableOptions{
DataRange: "Sheet1!$A$0:$E$31",
PivotTableRange: "Sheet1!$U$34:$O$2",
DataRange: "Sheet1!A0:E31",
PivotTableRange: "Sheet1!U34:O2",
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
Data: []PivotTableField{{Data: "Sales"}},
@ -219,8 +238,8 @@ func TestAddPivotTable(t *testing.T) {
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(&PivotTableOptions{
DataRange: "Sheet1!$A$1:$E$31",
PivotTableRange: "Sheet1!$G$2:$M$34",
DataRange: "Sheet1!A1:E31",
PivotTableRange: "Sheet1!G2:M34",
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
Data: []PivotTableField{{Data: "Sales", Subtotal: "-", Name: strings.Repeat("s", MaxFieldLength+1)}},
@ -228,8 +247,8 @@ func TestAddPivotTable(t *testing.T) {
// Test add pivot table with invalid sheet name
assert.EqualError(t, f.AddPivotTable(&PivotTableOptions{
DataRange: "Sheet:1!$A$1:$E$31",
PivotTableRange: "Sheet:1!$G$2:$M$34",
DataRange: "Sheet:1!A1:E31",
PivotTableRange: "Sheet:1!G2:M34",
Rows: []PivotTableField{{Data: "Year"}},
}), ErrSheetNameInvalid.Error())
// Test adjust range with invalid range
@ -245,8 +264,8 @@ func TestAddPivotTable(t *testing.T) {
assert.EqualError(t, f.addPivotCache("", &PivotTableOptions{}), "parameter 'DataRange' parsing error: parameter is required")
// Test add pivot cache with invalid data range
assert.EqualError(t, f.addPivotCache("", &PivotTableOptions{
DataRange: "$A$1:$E$31",
PivotTableRange: "Sheet1!$U$34:$O$2",
DataRange: "A1:E31",
PivotTableRange: "Sheet1!U34:O2",
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
Data: []PivotTableField{{Data: "Sales"}},
@ -257,8 +276,8 @@ func TestAddPivotTable(t *testing.T) {
assert.EqualError(t, f.addPivotTable(0, 0, "", &PivotTableOptions{}), "parameter 'PivotTableRange' parsing error: parameter is required")
// Test add pivot fields with empty data range
assert.EqualError(t, f.addPivotFields(nil, &PivotTableOptions{
DataRange: "$A$1:$E$31",
PivotTableRange: "Sheet1!$U$34:$O$2",
DataRange: "A1:E31",
PivotTableRange: "Sheet1!U34:O2",
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
Data: []PivotTableField{{Data: "Sales"}},
@ -271,17 +290,53 @@ func TestAddPivotTable(t *testing.T) {
f.ContentTypes = nil
f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset)
assert.EqualError(t, f.AddPivotTable(&PivotTableOptions{
DataRange: "Sheet1!$A$1:$E$31",
PivotTableRange: "Sheet1!$G$2:$M$34",
DataRange: "Sheet1!A1:E31",
PivotTableRange: "Sheet1!G2:M34",
Rows: []PivotTableField{{Data: "Year"}},
}), "XML syntax error on line 1: invalid UTF-8")
assert.NoError(t, f.Close())
// Test get pivot table without pivot table
f = NewFile()
pivotTables, err = f.GetPivotTables("Sheet1")
assert.NoError(t, err)
assert.Len(t, pivotTables, 0)
// Test get pivot table with not exists worksheet
_, err = f.GetPivotTables("SheetN")
assert.EqualError(t, err, "sheet SheetN does not exist")
// Test get pivot table with unsupported charset worksheet relationships
f.Pkg.Store("xl/worksheets/_rels/sheet1.xml.rels", MacintoshCyrillicCharset)
_, err = f.GetPivotTables("Sheet1")
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
assert.NoError(t, f.Close())
// Test get pivot table with unsupported charset pivot cache definition
f, err = OpenFile(filepath.Join("test", "TestAddPivotTable1.xlsx"))
assert.NoError(t, err)
f.Pkg.Store("xl/pivotCache/pivotCacheDefinition1.xml", MacintoshCyrillicCharset)
_, err = f.GetPivotTables("Sheet1")
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
assert.NoError(t, f.Close())
// Test get pivot table with unsupported charset pivot table relationships
f, err = OpenFile(filepath.Join("test", "TestAddPivotTable1.xlsx"))
assert.NoError(t, err)
f.Pkg.Store("xl/pivotTables/_rels/pivotTable1.xml.rels", MacintoshCyrillicCharset)
_, err = f.GetPivotTables("Sheet1")
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
assert.NoError(t, f.Close())
// Test get pivot table with unsupported charset pivot table
f, err = OpenFile(filepath.Join("test", "TestAddPivotTable1.xlsx"))
assert.NoError(t, err)
f.Pkg.Store("xl/pivotTables/pivotTable1.xml", MacintoshCyrillicCharset)
_, err = f.GetPivotTables("Sheet1")
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
assert.NoError(t, f.Close())
}
func TestAddPivotRowFields(t *testing.T) {
f := NewFile()
// Test invalid data range
assert.EqualError(t, f.addPivotRowFields(&xlsxPivotTableDefinition{}, &PivotTableOptions{
DataRange: "Sheet1!$A$1:$A$1",
DataRange: "Sheet1!A1:A1",
}), `parameter 'DataRange' parsing error: parameter is invalid`)
}
@ -289,7 +344,7 @@ func TestAddPivotPageFields(t *testing.T) {
f := NewFile()
// Test invalid data range
assert.EqualError(t, f.addPivotPageFields(&xlsxPivotTableDefinition{}, &PivotTableOptions{
DataRange: "Sheet1!$A$1:$A$1",
DataRange: "Sheet1!A1:A1",
}), `parameter 'DataRange' parsing error: parameter is invalid`)
}
@ -297,7 +352,7 @@ func TestAddPivotDataFields(t *testing.T) {
f := NewFile()
// Test invalid data range
assert.EqualError(t, f.addPivotDataFields(&xlsxPivotTableDefinition{}, &PivotTableOptions{
DataRange: "Sheet1!$A$1:$A$1",
DataRange: "Sheet1!A1:A1",
}), `parameter 'DataRange' parsing error: parameter is invalid`)
}
@ -305,7 +360,7 @@ func TestAddPivotColFields(t *testing.T) {
f := NewFile()
// Test invalid data range
assert.EqualError(t, f.addPivotColFields(&xlsxPivotTableDefinition{}, &PivotTableOptions{
DataRange: "Sheet1!$A$1:$A$1",
DataRange: "Sheet1!A1:A1",
Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
}), `parameter 'DataRange' parsing error: parameter is invalid`)
}
@ -313,7 +368,7 @@ func TestAddPivotColFields(t *testing.T) {
func TestGetPivotFieldsOrder(t *testing.T) {
f := NewFile()
// Test get pivot fields order with not exist worksheet
_, err := f.getPivotFieldsOrder(&PivotTableOptions{DataRange: "SheetN!$A$1:$E$31"})
_, err := f.getPivotFieldsOrder(&PivotTableOptions{DataRange: "SheetN!A1:E31"})
assert.EqualError(t, err, "sheet SheetN does not exist")
}

View File

@ -1595,7 +1595,7 @@ func (f *File) SetDefinedName(definedName *DefinedName) error {
if definedName.Name == "" || definedName.RefersTo == "" {
return ErrParameterInvalid
}
if err := checkDefinedName(definedName.Name); err != nil {
if err := checkDefinedName(definedName.Name); err != nil && inStrSlice(builtInDefinedNames[:2], definedName.Name, false) == -1 {
return err
}
wb, err := f.workbookReader()

View File

@ -276,6 +276,16 @@ func TestDefinedName(t *testing.T) {
RefersTo: "Sheet1!$A$2:$D$5",
Comment: "defined name comment",
}))
assert.NoError(t, f.SetDefinedName(&DefinedName{
Name: builtInDefinedNames[0],
RefersTo: "Sheet1!$A$1:$Z$100",
Scope: "Sheet1",
}))
assert.NoError(t, f.SetDefinedName(&DefinedName{
Name: builtInDefinedNames[1],
RefersTo: "Sheet1!$A:$A,Sheet1!$1:$1",
Scope: "Sheet1",
}))
assert.EqualError(t, f.SetDefinedName(&DefinedName{
Name: "Amount",
RefersTo: "Sheet1!$A$2:$D$5",
@ -297,7 +307,7 @@ func TestDefinedName(t *testing.T) {
Name: "Amount",
}))
assert.Exactly(t, "Sheet1!$A$2:$D$5", f.GetDefinedName()[0].RefersTo)
assert.Len(t, f.GetDefinedName(), 1)
assert.Len(t, f.GetDefinedName(), 3)
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestDefinedName.xlsx")))
// Test set defined name with unsupported charset workbook
f.WorkBook = nil

View File

@ -267,16 +267,14 @@ func TestAddSparkline(t *testing.T) {
// Test creating a conditional format with existing extension lists
ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml")
assert.True(t, ok)
ws.(*xlsxWorksheet).ExtLst = &xlsxExtLst{Ext: `
<ext uri="{A8765BA9-456A-4dab-B4F3-ACF838C121DE}"><x14:slicerList /></ext>
<ext uri="{05C60535-1F16-4fd2-B633-F4F36F0B64E0}"><x14:sparklineGroups /></ext>`}
ws.(*xlsxWorksheet).ExtLst = &xlsxExtLst{Ext: fmt.Sprintf(`<ext uri="%s"><x14:slicerList /></ext><ext uri="%s"><x14:sparklineGroups /></ext>`, ExtURISlicerListX14, ExtURISparklineGroups)}
assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOptions{
Location: []string{"A3"},
Range: []string{"Sheet3!A2:J2"},
Type: "column",
}))
// Test creating a conditional format with invalid extension list characters
ws.(*xlsxWorksheet).ExtLst.Ext = `<ext uri="{05C60535-1F16-4fd2-B633-F4F36F0B64E0}"><x14:sparklineGroups><x14:sparklineGroup></x14:sparklines></x14:sparklineGroup></x14:sparklineGroups></ext>`
ws.(*xlsxWorksheet).ExtLst.Ext = fmt.Sprintf(`<ext uri="%s"><x14:sparklineGroups><x14:sparklineGroup></x14:sparklines></x14:sparklineGroup></x14:sparklineGroups></ext>`, ExtURISparklineGroups)
assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOptions{
Location: []string{"A2"},
Range: []string{"Sheet3!A1:J1"},

View File

@ -1127,7 +1127,7 @@ func (f *File) getThemeColor(clr *xlsxColor) string {
if len(clr.RGB) == 8 {
return strings.TrimPrefix(clr.RGB, "FF")
}
if f.Styles.Colors != nil && clr.Indexed < len(f.Styles.Colors.IndexedColors.RgbColor) {
if f.Styles.Colors != nil && f.Styles.Colors.IndexedColors != nil && clr.Indexed < len(f.Styles.Colors.IndexedColors.RgbColor) {
return strings.TrimPrefix(ThemeColor(strings.TrimPrefix(f.Styles.Colors.IndexedColors.RgbColor[clr.Indexed].RGB, "FF"), clr.Tint), "FF")
}
if clr.Indexed < len(IndexedColorMapping) {

View File

@ -1,6 +1,7 @@
package excelize
import (
"fmt"
"math"
"path/filepath"
"strings"
@ -180,9 +181,7 @@ func TestSetConditionalFormat(t *testing.T) {
// Test creating a conditional format with existing extension lists
ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml")
assert.True(t, ok)
ws.(*xlsxWorksheet).ExtLst = &xlsxExtLst{Ext: `
<ext uri="{A8765BA9-456A-4dab-B4F3-ACF838C121DE}"><x14:slicerList /></ext>
<ext uri="{05C60535-1F16-4fd2-B633-F4F36F0B64E0}"><x14:sparklineGroups /></ext>`}
ws.(*xlsxWorksheet).ExtLst = &xlsxExtLst{Ext: fmt.Sprintf(`<ext uri="%s"><x14:slicerList /></ext><ext uri="%s"><x14:sparklineGroups /></ext>`, ExtURISlicerListX14, ExtURISparklineGroups)}
assert.NoError(t, f.SetConditionalFormat("Sheet1", "A1:A2", []ConditionalFormatOptions{{Type: "data_bar", Criteria: "=", MinType: "min", MaxType: "max", BarBorderColor: "#0000FF", BarColor: "#638EC6", BarSolid: true}}))
f = NewFile()
// Test creating a conditional format with invalid extension list characters
@ -573,7 +572,7 @@ func TestGetStyle(t *testing.T) {
// Test get style with custom color index
f.Styles.Colors = &xlsxStyleColors{
IndexedColors: xlsxIndexedColors{
IndexedColors: &xlsxIndexedColors{
RgbColor: []xlsxColor{{RGB: "FF012345"}},
},
}

View File

@ -427,7 +427,6 @@ func (f *File) AutoFilter(sheet, rangeRef string, opts []AutoFilterOptions) erro
_ = sortCoordinates(coordinates)
// Correct reference range, such correct C1:B3 to B1:C3.
ref, _ := f.coordinatesToRangeRef(coordinates, true)
filterDB := "_xlnm._FilterDatabase"
wb, err := f.workbookReader()
if err != nil {
return err
@ -438,7 +437,7 @@ func (f *File) AutoFilter(sheet, rangeRef string, opts []AutoFilterOptions) erro
}
filterRange := fmt.Sprintf("'%s'!%s", sheet, ref)
d := xlsxDefinedName{
Name: filterDB,
Name: builtInDefinedNames[2],
Hidden: true,
LocalSheetID: intPtr(sheetID),
Data: filterRange,
@ -451,7 +450,7 @@ func (f *File) AutoFilter(sheet, rangeRef string, opts []AutoFilterOptions) erro
var definedNameExists bool
for idx := range wb.DefinedNames.DefinedName {
definedName := wb.DefinedNames.DefinedName[idx]
if definedName.Name == filterDB && *definedName.LocalSheetID == sheetID && definedName.Hidden {
if definedName.Name == builtInDefinedNames[2] && *definedName.LocalSheetID == sheetID && definedName.Hidden {
wb.DefinedNames.DefinedName[idx].Data = filterRange
definedNameExists = true
}

View File

@ -29,6 +29,7 @@ var (
NameSpaceSpreadSheetExcel2006Main = xml.Attr{Name: xml.Name{Local: "xne", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/excel/2006/main"}
NameSpaceSpreadSheetX14 = xml.Attr{Name: xml.Name{Local: "x14", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/spreadsheetml/2009/9/main"}
NameSpaceSpreadSheetX15 = xml.Attr{Name: xml.Name{Local: "x15", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/spreadsheetml/2010/11/main"}
NameSpaceSpreadSheetXR10 = xml.Attr{Name: xml.Name{Local: "xr10", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/spreadsheetml/2016/revision10"}
SourceRelationship = xml.Attr{Name: xml.Name{Local: "r", Space: "xmlns"}, Value: "http://schemas.openxmlformats.org/officeDocument/2006/relationships"}
SourceRelationshipChart20070802 = xml.Attr{Name: xml.Name{Local: "c14", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/drawing/2007/8/2/chart"}
SourceRelationshipChart2014 = xml.Attr{Name: xml.Name{Local: "c16", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/drawing/2014/chart"}
@ -43,6 +44,8 @@ const (
ContentTypeDrawingML = "application/vnd.openxmlformats-officedocument.drawingml.chart+xml"
ContentTypeMacro = "application/vnd.ms-excel.sheet.macroEnabled.main+xml"
ContentTypeSheetML = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"
ContentTypeSlicer = "application/vnd.ms-excel.slicer+xml"
ContentTypeSlicerCache = "application/vnd.ms-excel.slicerCache+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"
@ -74,6 +77,7 @@ const (
SourceRelationshipPivotCache = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheDefinition"
SourceRelationshipPivotTable = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotTable"
SourceRelationshipSharedStrings = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings"
SourceRelationshipSlicer = "http://schemas.microsoft.com/office/2007/relationships/slicer"
SourceRelationshipTable = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/table"
SourceRelationshipVBAProject = "http://schemas.microsoft.com/office/2006/relationships/vbaProject"
SourceRelationshipWorkSheet = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet"
@ -97,6 +101,7 @@ const (
ExtURIDrawingBlip = "{28A0092B-C50C-407E-A947-70E740481C1C}"
ExtURIIgnoredErrors = "{01252117-D84E-4E92-8308-4BE1C098FCBB}"
ExtURIMacExcelMX = "{64002731-A6B0-56B0-2670-7721B7C09600}"
ExtURIPivotCacheDefinition = "{725AE2AE-9491-48be-B2B4-4EB974FC3084}"
ExtURIProtectedRanges = "{FC87AEE6-9EDD-4A0A-B7FB-166176984837}"
ExtURISlicerCachesListX14 = "{BBE1A952-AA13-448e-AADC-164F8A28A991}"
ExtURISlicerListX14 = "{A8765BA9-456A-4DAB-B4F3-ACF838C121DE}"
@ -222,6 +227,9 @@ var supportedDrawingUnderlineTypes = []string{
// supportedPositioning defined supported positioning types.
var supportedPositioning = []string{"absolute", "oneCell", "twoCell"}
// builtInDefinedNames defined built-in defined names are built with a _xlnm prefix.
var builtInDefinedNames = []string{"_xlnm.Print_Area", "_xlnm.Print_Titles", "_xlnm._FilterDatabase"}
// xlsxCNvPr 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.

View File

@ -311,8 +311,8 @@ type xlsxIndexedColors struct {
// legacy color palette has been modified (backwards compatibility settings) or
// a custom color has been selected while using this workbook.
type xlsxStyleColors struct {
IndexedColors xlsxIndexedColors `xml:"indexedColors"`
MruColors xlsxInnerXML `xml:"mruColors"`
IndexedColors *xlsxIndexedColors `xml:"indexedColors"`
MruColors xlsxInnerXML `xml:"mruColors"`
}
// Alignment directly maps the alignment settings of the cells.