diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml
index 4f26b1b9dcd..9a1633c3ae8 100644
--- a/.github/workflows/go.yml
+++ b/.github/workflows/go.yml
@@ -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]
diff --git a/calc.go b/calc.go
index 8560675d55e..459616f752e 100644
--- a/calc.go
+++ b/calc.go
@@ -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))
+ dbcs, search := name == "FINDB" || name == "SEARCHB", name == "SEARCH" || name == "SEARCHB"
+ if search {
+ findText, withinText = strings.ToUpper(findText), strings.ToUpper(withinText)
+ }
+ 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
}
- result++
}
- return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
+ 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("")
- }
- startNum--
- endNum := startNum + int(numCharsArg.Number)
- if endNum > textLen+1 {
- return newStringFormulaArg(text[startNum:])
+ 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++
+ }
+ }
}
- 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))
+// 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])"
+ }
+ 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
}
- str = str[1:]
- pattern = pattern[1:]
+ exp += string(char)
}
- return len(str) == 0 && len(pattern) == 0
+ 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
- }
- 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)
+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++
}
- 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
}
}
diff --git a/calc_test.go b/calc_test.go
index 65ace820956..ba5a35b4f5b 100644
--- a/calc_test.go
+++ b/calc_test.go
@@ -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!"},
diff --git a/cell.go b/cell.go
index c2ac31f49db..36265d9fe08 100644
--- a/cell.go
+++ b/cell.go
@@ -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
diff --git a/picture.go b/picture.go
index 1d6b5f06f24..b8978ea950d 100644
--- a/picture.go
+++ b/picture.go
@@ -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 {
diff --git a/pivotTable.go b/pivotTable.go
index 4c8dee282c3..dbee2f6bcfd 100644
--- a/pivotTable.go
+++ b/pivotTable.go
@@ -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.Pointer && !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.Pointer && !immutableField.IsNil() && immutableField.Elem().Kind() == reflect.Bool {
+ mutable.FieldByName(field).SetBool(immutableField.Elem().Bool())
+ }
+ }
+ return pivotTableField
+}
diff --git a/pivotTable_test.go b/pivotTable_test.go
index 2de3e07f89a..9c45d16e84a 100644
--- a/pivotTable_test.go
+++ b/pivotTable_test.go
@@ -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")
}
diff --git a/sheet.go b/sheet.go
index b93c2317939..00977ec389a 100644
--- a/sheet.go
+++ b/sheet.go
@@ -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()
diff --git a/sheet_test.go b/sheet_test.go
index 8d42fd54dba..bc4c6fdc7c6 100644
--- a/sheet_test.go
+++ b/sheet_test.go
@@ -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
diff --git a/sparkline_test.go b/sparkline_test.go
index 0d1511d040e..048ed2b8655 100644
--- a/sparkline_test.go
+++ b/sparkline_test.go
@@ -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: `
-
- `}
+ ws.(*xlsxWorksheet).ExtLst = &xlsxExtLst{Ext: fmt.Sprintf(``, 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 = ``
+ ws.(*xlsxWorksheet).ExtLst.Ext = fmt.Sprintf(``, ExtURISparklineGroups)
assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOptions{
Location: []string{"A2"},
Range: []string{"Sheet3!A1:J1"},
diff --git a/styles.go b/styles.go
index fe2e3f69fcb..13904679e13 100644
--- a/styles.go
+++ b/styles.go
@@ -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) {
diff --git a/styles_test.go b/styles_test.go
index f202bcf02ae..9ef6a894df7 100644
--- a/styles_test.go
+++ b/styles_test.go
@@ -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: `
-
- `}
+ ws.(*xlsxWorksheet).ExtLst = &xlsxExtLst{Ext: fmt.Sprintf(``, 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"}},
},
}
diff --git a/table.go b/table.go
index 6aa1552edda..ae4fc780fa9 100644
--- a/table.go
+++ b/table.go
@@ -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
}
diff --git a/xmlDrawing.go b/xmlDrawing.go
index ba386556843..688e601637b 100644
--- a/xmlDrawing.go
+++ b/xmlDrawing.go
@@ -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.
diff --git a/xmlStyles.go b/xmlStyles.go
index 1dda04a0aa6..e7de8856294 100644
--- a/xmlStyles.go
+++ b/xmlStyles.go
@@ -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.