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.