From c62d23e0a113f192ce39469a80bb66d67dd7cbec Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 27 Sep 2023 00:05:59 +0800 Subject: [PATCH] The `AddSlicer` function now support create pivot table slicer --- pivotTable.go | 56 +++++--- pivotTable_test.go | 18 ++- slicer.go | 334 ++++++++++++++++++++++++++++++++++----------- slicer_test.go | 259 +++++++++++++++++++++++++++-------- sparkline.go | 4 +- styles.go | 4 +- templates.go | 25 +++- xmlPivotCache.go | 54 +++++--- xmlSlicers.go | 43 +++++- xmlWorkbook.go | 1 + 10 files changed, 610 insertions(+), 188 deletions(-) diff --git a/pivotTable.go b/pivotTable.go index 2775af7f2b..720f2b193d 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -33,6 +33,8 @@ import ( // PivotStyleMedium1 - PivotStyleMedium28 // PivotStyleDark1 - PivotStyleDark28 type PivotTableOptions struct { + pivotTableXML string + pivotCacheXML string pivotTableSheetName string DataRange string PivotTableRange string @@ -286,7 +288,7 @@ func (f *File) addPivotCache(pivotCacheXML string, opts *PivotTableOptions) erro SaveData: false, RefreshOnLoad: true, CreatedVersion: pivotTableVersion, - RefreshedVersion: pivotTableVersion, + RefreshedVersion: pivotTableRefreshedVersion, MinRefreshableVersion: pivotTableVersion, CacheSource: &xlsxCacheSource{ Type: "worksheet", @@ -301,23 +303,9 @@ func (f *File) addPivotCache(pivotCacheXML string, opts *PivotTableOptions) erro pc.CacheSource.WorksheetSource = &xlsxWorksheetSource{Name: opts.DataRange} } for _, name := range order { - rowOptions, rowOk := f.getPivotTableFieldOptions(name, opts.Rows) - columnOptions, colOk := f.getPivotTableFieldOptions(name, opts.Columns) - sharedItems := xlsxSharedItems{ - Count: 0, - } - s := xlsxString{} - if (rowOk && !rowOptions.DefaultSubtotal) || (colOk && !columnOptions.DefaultSubtotal) { - s = xlsxString{ - V: "", - } - sharedItems.Count++ - sharedItems.S = &s - } - pc.CacheFields.CacheField = append(pc.CacheFields.CacheField, &xlsxCacheField{ Name: name, - SharedItems: &sharedItems, + SharedItems: &xlsxSharedItems{ContainsBlank: true, M: []xlsxMissing{{}}}, }) } pc.CacheFields.Count = len(pc.CacheFields.CacheField) @@ -349,13 +337,13 @@ func (f *File) addPivotTable(cacheID, pivotTableID int, pivotTableXML string, op CacheID: cacheID, RowGrandTotals: &opts.RowGrandTotals, ColGrandTotals: &opts.ColGrandTotals, - UpdatedVersion: pivotTableVersion, + UpdatedVersion: pivotTableRefreshedVersion, MinRefreshableVersion: pivotTableVersion, ShowDrill: &opts.ShowDrill, UseAutoFormatting: &opts.UseAutoFormatting, PageOverThenDown: &opts.PageOverThenDown, MergeItem: &opts.MergeItem, - CreatedVersion: pivotTableVersion, + CreatedVersion: 3, CompactData: &opts.CompactData, ShowError: &opts.ShowError, DataCaption: "Values", @@ -788,6 +776,8 @@ func (f *File) getPivotTable(sheet, pivotTableXML, pivotCacheRels string) (Pivot } dataRange := fmt.Sprintf("%s!%s", pc.CacheSource.WorksheetSource.Sheet, pc.CacheSource.WorksheetSource.Ref) opts = PivotTableOptions{ + pivotTableXML: pivotTableXML, + pivotCacheXML: pivotCacheXML, pivotTableSheetName: sheet, DataRange: dataRange, PivotTableRange: fmt.Sprintf("%s!%s", sheet, pt.Location.Ref), @@ -886,3 +876,33 @@ func extractPivotTableField(data string, fld *xlsxPivotField) PivotTableField { } return pivotTableField } + +// genPivotCacheDefinitionID generates a unique pivot table cache definition ID. +func (f *File) genPivotCacheDefinitionID() int { + var ( + ID int + decodeExtLst = new(decodeExtLst) + decodeX14PivotCacheDefinition = new(decodeX14PivotCacheDefinition) + ) + f.Pkg.Range(func(k, v interface{}) bool { + if strings.Contains(k.(string), "xl/pivotCache/pivotCacheDefinition") { + pc, err := f.pivotCacheReader(k.(string)) + if err != nil { + return true + } + if pc.ExtLst != nil { + _ = f.xmlNewDecoder(strings.NewReader("" + pc.ExtLst.Ext + "")).Decode(decodeExtLst) + for _, ext := range decodeExtLst.Ext { + if ext.URI == ExtURIPivotCacheDefinition { + _ = f.xmlNewDecoder(strings.NewReader(ext.Content)).Decode(decodeX14PivotCacheDefinition) + if ID < decodeX14PivotCacheDefinition.PivotCacheID { + ID = decodeX14PivotCacheDefinition.PivotCacheID + } + } + } + } + } + return true + }) + return ID + 1 +} diff --git a/pivotTable_test.go b/pivotTable_test.go index f6d0707524..bba445dc77 100644 --- a/pivotTable_test.go +++ b/pivotTable_test.go @@ -26,6 +26,8 @@ func TestPivotTable(t *testing.T) { assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("E%d", row), region[rand.Intn(4)])) } expected := &PivotTableOptions{ + pivotTableXML: "xl/pivotTables/pivotTable1.xml", + pivotCacheXML: "xl/pivotCache/pivotCacheDefinition1.xml", DataRange: "Sheet1!A1:E31", PivotTableRange: "Sheet1!G2:M34", Name: "PivotTable1", @@ -374,5 +376,19 @@ func TestGetPivotFieldsOrder(t *testing.T) { func TestGetPivotTableFieldName(t *testing.T) { f := NewFile() - f.getPivotTableFieldName("-", []PivotTableField{}) + assert.Empty(t, f.getPivotTableFieldName("-", []PivotTableField{})) +} + +func TestGetPivotTableFieldOptions(t *testing.T) { + f := NewFile() + _, ok := f.getPivotTableFieldOptions("-", []PivotTableField{}) + assert.False(t, ok) +} + +func TestGenPivotCacheDefinitionID(t *testing.T) { + f := NewFile() + // Test generate pivot table cache definition ID with unsupported charset + f.Pkg.Store("xl/pivotCache/pivotCacheDefinition1.xml", MacintoshCyrillicCharset) + assert.Equal(t, 1, f.genPivotCacheDefinitionID()) + assert.NoError(t, f.Close()) } diff --git a/slicer.go b/slicer.go index c62b33768a..63d9dad1f9 100644 --- a/slicer.go +++ b/slicer.go @@ -27,12 +27,15 @@ import ( // Name specifies the slicer name, should be an existing field name of the given // table or pivot table, this setting is required. // -// Table specifies the name of the table or pivot table, this setting is -// required. -// // Cell specifies the left top cell coordinates the position for inserting the // slicer, this setting is required. // +// TableSheet specifies the worksheet name of the table or pivot table, this +// setting is required. +// +// TableName specifies the name of the table or pivot table, this setting is +// required. +// // Caption specifies the caption of the slicer, this setting is optional. // // Macro used for set macro for the slicer, the workbook extension should be @@ -51,8 +54,9 @@ import ( // Format specifies the format of the slicer, this setting is optional. type SlicerOptions struct { Name string - Table string Cell string + TableSheet string + TableName string Caption string Macro string Width uint @@ -63,38 +67,44 @@ type SlicerOptions struct { } // AddSlicer function inserts a slicer by giving the worksheet name and slicer -// settings. The pivot table slicer is not supported currently. +// settings. // // For example, insert a slicer on the Sheet1!E1 with field Column1 for the // table named Table1: // // err := f.AddSlicer("Sheet1", &excelize.SlicerOptions{ -// Name: "Column1", -// Table: "Table1", -// Cell: "E1", -// Caption: "Column1", -// Width: 200, -// Height: 200, +// Name: "Column1", +// Cell: "E1", +// TableSheet: "Sheet1", +// TableName: "Table1", +// Caption: "Column1", +// Width: 200, +// Height: 200, // }) func (f *File) AddSlicer(sheet string, opts *SlicerOptions) error { opts, err := parseSlicerOptions(opts) if err != nil { return err } - table, colIdx, err := f.getSlicerSource(sheet, opts) + table, pivotTable, colIdx, err := f.getSlicerSource(opts) if err != nil { return err } - slicerID, err := f.addSheetSlicer(sheet) + extURI, ns := ExtURISlicerListX14, NameSpaceDrawingMLA14 + if table != nil { + extURI = ExtURISlicerListX15 + ns = NameSpaceDrawingMLSlicerX15 + } + slicerID, err := f.addSheetSlicer(sheet, extURI) if err != nil { return err } - slicerCacheName, err := f.setSlicerCache(colIdx, opts, table) + slicerCacheName, err := f.setSlicerCache(sheet, colIdx, opts, table, pivotTable) if err != nil { return err } - slicerName, err := f.addDrawingSlicer(sheet, opts) - if err != nil { + slicerName := f.genSlicerName(opts.Name) + if err := f.addDrawingSlicer(sheet, slicerName, ns, opts); err != nil { return err } return f.addSlicer(slicerID, xlsxSlicer{ @@ -112,7 +122,7 @@ func parseSlicerOptions(opts *SlicerOptions) (*SlicerOptions, error) { if opts == nil { return nil, ErrParameterRequired } - if opts.Name == "" || opts.Table == "" || opts.Cell == "" { + if opts.Name == "" || opts.Cell == "" || opts.TableSheet == "" || opts.TableName == "" { return nil, ErrParameterInvalid } if opts.Width == 0 { @@ -165,34 +175,51 @@ func (f *File) countSlicerCache() int { // getSlicerSource returns the slicer data source table or pivot table settings // and the index of the given slicer fields in the table or pivot table // column. -func (f *File) getSlicerSource(sheet string, opts *SlicerOptions) (*Table, int, error) { +func (f *File) getSlicerSource(opts *SlicerOptions) (*Table, *PivotTableOptions, int, error) { var ( table *Table + pivotTable *PivotTableOptions colIdx int - tables, err = f.GetTables(sheet) + err error + dataRange string + tables []Table + pivotTables []PivotTableOptions ) - if err != nil { - return table, colIdx, err + if tables, err = f.GetTables(opts.TableSheet); err != nil { + return table, pivotTable, colIdx, err } for _, tbl := range tables { - if tbl.Name == opts.Table { + if tbl.Name == opts.TableName { table = &tbl + dataRange = fmt.Sprintf("%s!%s", opts.TableSheet, tbl.Range) break } } if table == nil { - return table, colIdx, newNoExistTableError(opts.Table) + if pivotTables, err = f.GetPivotTables(opts.TableSheet); err != nil { + return table, pivotTable, colIdx, err + } + for _, tbl := range pivotTables { + if tbl.Name == opts.TableName { + pivotTable = &tbl + dataRange = tbl.DataRange + break + } + } + if pivotTable == nil { + return table, pivotTable, colIdx, newNoExistTableError(opts.TableName) + } } - order, _ := f.getTableFieldsOrder(sheet, fmt.Sprintf("%s!%s", sheet, table.Range)) + order, _ := f.getTableFieldsOrder(opts.TableSheet, dataRange) if colIdx = inStrSlice(order, opts.Name, true); colIdx == -1 { - return table, colIdx, newInvalidSlicerNameError(opts.Name) + return table, pivotTable, colIdx, newInvalidSlicerNameError(opts.Name) } - return table, colIdx, err + return table, pivotTable, colIdx, err } // addSheetSlicer adds a new slicer and updates the namespace and relationships // parts of the worksheet by giving the worksheet name. -func (f *File) addSheetSlicer(sheet string) (int, error) { +func (f *File) addSheetSlicer(sheet, extURI string) (int, error) { var ( slicerID = f.countSlicers() + 1 ws, err = f.workSheetReader(sheet) @@ -208,7 +235,7 @@ func (f *File) addSheetSlicer(sheet string) (int, error) { return slicerID, err } for _, ext := range decodeExtLst.Ext { - if ext.URI == ExtURISlicerListX15 { + if ext.URI == extURI { _ = f.xmlNewDecoder(strings.NewReader(ext.Content)).Decode(slicerList) for _, slicer := range slicerList.Slicer { if slicer.RID != "" { @@ -225,12 +252,12 @@ func (f *File) addSheetSlicer(sheet string) (int, error) { sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetXMLPath, "xl/worksheets/") + ".rels" rID := f.addRels(sheetRels, SourceRelationshipSlicer, sheetRelationshipsSlicerXML, "") f.addSheetNameSpace(sheet, NameSpaceSpreadSheetX14) - return slicerID, f.addSheetTableSlicer(ws, rID) + return slicerID, f.addSheetTableSlicer(ws, rID, extURI) } // addSheetTableSlicer adds a new table slicer for the worksheet by giving the -// worksheet relationships ID. -func (f *File) addSheetTableSlicer(ws *xlsxWorksheet, rID int) error { +// worksheet relationships ID and extension URI. +func (f *File) addSheetTableSlicer(ws *xlsxWorksheet, rID int, extURI string) error { var ( decodeExtLst = new(decodeExtLst) err error @@ -245,13 +272,17 @@ func (f *File) addSheetTableSlicer(ws *xlsxWorksheet, rID int) error { slicerListBytes, _ = xml.Marshal(&xlsxX14SlicerList{ Slicer: []*xlsxX14Slicer{{RID: "rId" + strconv.Itoa(rID)}}, }) - decodeExtLst.Ext = append(decodeExtLst.Ext, &xlsxExt{ - xmlns: []xml.Attr{{Name: xml.Name{Local: "xmlns:" + NameSpaceSpreadSheetX15.Name.Local}, Value: NameSpaceSpreadSheetX15.Value}}, - URI: ExtURISlicerListX15, Content: string(slicerListBytes), - }) + ext := &xlsxExt{ + xmlns: []xml.Attr{{Name: xml.Name{Local: "xmlns:" + NameSpaceSpreadSheetX14.Name.Local}, Value: NameSpaceSpreadSheetX14.Value}}, + URI: extURI, Content: string(slicerListBytes), + } + if extURI == ExtURISlicerListX15 { + ext.xmlns = []xml.Attr{{Name: xml.Name{Local: "xmlns:" + NameSpaceSpreadSheetX15.Name.Local}, Value: NameSpaceSpreadSheetX15.Value}} + } + decodeExtLst.Ext = append(decodeExtLst.Ext, ext) sort.Slice(decodeExtLst.Ext, func(i, j int) bool { - return inStrSlice(extensionURIPriority, decodeExtLst.Ext[i].URI, false) < - inStrSlice(extensionURIPriority, decodeExtLst.Ext[j].URI, false) + return inStrSlice(worksheetExtURIPriority, decodeExtLst.Ext[i].URI, false) < + inStrSlice(worksheetExtURIPriority, decodeExtLst.Ext[j].URI, false) }) extLstBytes, err = xml.Marshal(decodeExtLst) ws.ExtLst = &xlsxExtLst{Ext: strings.TrimSuffix(strings.TrimPrefix(string(extLstBytes), ""), "")} @@ -275,6 +306,49 @@ func (f *File) addSlicer(slicerID int, slicer xlsxSlicer) error { return err } +// genSlicerName generates a unique slicer cache name by giving the slicer name. +func (f *File) genSlicerName(name string) string { + var ( + cnt int + slicerName string + names []string + ) + f.Pkg.Range(func(k, v interface{}) bool { + if strings.Contains(k.(string), "xl/slicers/slicer") { + slicers, err := f.slicerReader(k.(string)) + if err != nil { + return true + } + for _, slicer := range slicers.Slicer { + names = append(names, slicer.Name) + } + } + if strings.Contains(k.(string), "xl/timelines/timeline") { + timelines, err := f.timelineReader(k.(string)) + if err != nil { + return true + } + for _, timeline := range timelines.Timeline { + names = append(names, timeline.Name) + } + } + return true + }) + slicerName = name + for { + tmp := slicerName + if cnt > 0 { + tmp = fmt.Sprintf("%s %d", slicerName, cnt) + } + if inStrSlice(names, tmp, true) == -1 { + slicerName = tmp + break + } + cnt++ + } + return slicerName +} + // genSlicerNames generates a unique slicer cache name by giving the slicer name. func (f *File) genSlicerCacheName(name string) string { var ( @@ -316,7 +390,7 @@ func (f *File) genSlicerCacheName(name string) string { // setSlicerCache check if a slicer cache already exists or add a new slicer // cache by giving the column index, slicer, table options, and returns the // slicer cache name. -func (f *File) setSlicerCache(colIdx int, opts *SlicerOptions, table *Table) (string, error) { +func (f *File) setSlicerCache(sheet string, colIdx int, opts *SlicerOptions, table *Table, pivotTable *PivotTableOptions) (string, error) { var ok bool var slicerCacheName string f.Pkg.Range(func(k, v interface{}) bool { @@ -326,7 +400,15 @@ func (f *File) setSlicerCache(colIdx int, opts *SlicerOptions, table *Table) (st Decode(slicerCache); err != nil && err != io.EOF { return true } - if slicerCache.ExtLst == nil { + if pivotTable != nil && slicerCache.PivotTables != nil { + for _, tbl := range slicerCache.PivotTables.PivotTable { + if tbl.Name == pivotTable.Name { + ok, slicerCacheName = true, slicerCache.Name + return false + } + } + } + if table == nil || slicerCache.ExtLst == nil { return true } ext := new(xlsxExt) @@ -346,7 +428,7 @@ func (f *File) setSlicerCache(colIdx int, opts *SlicerOptions, table *Table) (st return slicerCacheName, nil } slicerCacheName = f.genSlicerCacheName(opts.Name) - return slicerCacheName, f.addSlicerCache(slicerCacheName, colIdx, opts, table) + return slicerCacheName, f.addSlicerCache(slicerCacheName, colIdx, opts, table, pivotTable) } // slicerReader provides a function to get the pointer to the structure @@ -367,11 +449,31 @@ func (f *File) slicerReader(slicerXML string) (*xlsxSlicers, error) { return slicer, nil } +// timelineReader provides a function to get the pointer to the structure +// after deserialization of xl/timelines/timeline%d.xml. +func (f *File) timelineReader(timelineXML string) (*xlsxTimelines, error) { + content, ok := f.Pkg.Load(timelineXML) + timeline := &xlsxTimelines{ + XMLNSXMC: SourceRelationshipCompatibility.Value, + XMLNSX: NameSpaceSpreadSheet.Value, + XMLNSXR10: NameSpaceSpreadSheetXR10.Value, + } + if ok && content != nil { + if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(content.([]byte)))). + Decode(timeline); err != nil && err != io.EOF { + return nil, err + } + } + return timeline, nil +} + // addSlicerCache adds a new slicer cache by giving the slicer cache name, -// column index, slicer, and table options. -func (f *File) addSlicerCache(slicerCacheName string, colIdx int, opts *SlicerOptions, table *Table) error { +// column index, slicer, and table or pivot table options. +func (f *File) addSlicerCache(slicerCacheName string, colIdx int, opts *SlicerOptions, table *Table, pivotTable *PivotTableOptions) error { var ( + sortOrder string slicerCacheBytes, tableSlicerBytes, extLstBytes []byte + extURI = ExtURISlicerCachesX14 slicerCacheID = f.countSlicerCache() + 1 decodeExtLst = new(decodeExtLst) slicerCache = xlsxSlicerCacheDefinition{ @@ -381,52 +483,108 @@ func (f *File) addSlicerCache(slicerCacheName string, colIdx int, opts *SlicerOp XMLNSXR10: NameSpaceSpreadSheetXR10.Value, Name: slicerCacheName, SourceName: opts.Name, - ExtLst: &xlsxExtLst{}, } ) - var sortOrder string if opts.ItemDesc { sortOrder = "descending" } - tableSlicerBytes, _ = xml.Marshal(&xlsxTableSlicerCache{ - TableID: table.tID, - Column: colIdx + 1, - SortOrder: sortOrder, - }) - decodeExtLst.Ext = append(decodeExtLst.Ext, &xlsxExt{ - xmlns: []xml.Attr{{Name: xml.Name{Local: "xmlns:" + NameSpaceSpreadSheetX15.Name.Local}, Value: NameSpaceSpreadSheetX15.Value}}, - URI: ExtURISlicerCacheDefinition, Content: string(tableSlicerBytes), - }) - extLstBytes, _ = xml.Marshal(decodeExtLst) - slicerCache.ExtLst = &xlsxExtLst{Ext: strings.TrimSuffix(strings.TrimPrefix(string(extLstBytes), ""), "")} + if pivotTable != nil { + pivotCacheID, err := f.addPivotCacheSlicer(pivotTable) + if err != nil { + return err + } + slicerCache.PivotTables = &xlsxSlicerCachePivotTables{ + PivotTable: []xlsxSlicerCachePivotTable{ + {TabID: f.getSheetID(opts.TableSheet), Name: pivotTable.Name}, + }, + } + slicerCache.Data = &xlsxSlicerCacheData{ + Tabular: &xlsxTabularSlicerCache{ + PivotCacheID: pivotCacheID, + SortOrder: sortOrder, + ShowMissing: boolPtr(false), + Items: &xlsxTabularSlicerCacheItems{ + Count: 1, I: []xlsxTabularSlicerCacheItem{{S: true}}, + }, + }, + } + } + if table != nil { + tableSlicerBytes, _ = xml.Marshal(&xlsxTableSlicerCache{ + TableID: table.tID, + Column: colIdx + 1, + SortOrder: sortOrder, + }) + decodeExtLst.Ext = append(decodeExtLst.Ext, &xlsxExt{ + xmlns: []xml.Attr{{Name: xml.Name{Local: "xmlns:" + NameSpaceSpreadSheetX15.Name.Local}, Value: NameSpaceSpreadSheetX15.Value}}, + URI: ExtURISlicerCacheDefinition, Content: string(tableSlicerBytes), + }) + extLstBytes, _ = xml.Marshal(decodeExtLst) + slicerCache.ExtLst = &xlsxExtLst{Ext: strings.TrimSuffix(strings.TrimPrefix(string(extLstBytes), ""), "")} + extURI = ExtURISlicerCachesX15 + } slicerCacheXML := "xl/slicerCaches/slicerCache" + strconv.Itoa(slicerCacheID) + ".xml" slicerCacheBytes, _ = xml.Marshal(slicerCache) f.saveFileList(slicerCacheXML, slicerCacheBytes) if err := f.addContentTypePart(slicerCacheID, "slicerCache"); err != nil { return err } - if err := f.addWorkbookSlicerCache(slicerCacheID, ExtURISlicerCachesX15); err != nil { + if err := f.addWorkbookSlicerCache(slicerCacheID, extURI); err != nil { return err } return f.SetDefinedName(&DefinedName{Name: slicerCacheName, RefersTo: formulaErrorNA}) } +// addPivotCacheSlicer adds a new slicer cache by giving the pivot table options +// and returns pivot table cache ID. +func (f *File) addPivotCacheSlicer(opts *PivotTableOptions) (int, error) { + var ( + pivotCacheID int + pivotCacheBytes, extLstBytes []byte + decodeExtLst = new(decodeExtLst) + decodeX14PivotCacheDefinition = new(decodeX14PivotCacheDefinition) + ) + pc, err := f.pivotCacheReader(opts.pivotCacheXML) + if err != nil { + return pivotCacheID, err + } + if pc.ExtLst != nil { + _ = f.xmlNewDecoder(strings.NewReader("" + pc.ExtLst.Ext + "")).Decode(decodeExtLst) + for _, ext := range decodeExtLst.Ext { + if ext.URI == ExtURIPivotCacheDefinition { + _ = f.xmlNewDecoder(strings.NewReader(ext.Content)).Decode(decodeX14PivotCacheDefinition) + return decodeX14PivotCacheDefinition.PivotCacheID, err + } + } + } + pivotCacheID = f.genPivotCacheDefinitionID() + pivotCacheBytes, _ = xml.Marshal(&xlsxX14PivotCacheDefinition{PivotCacheID: pivotCacheID}) + ext := &xlsxExt{ + xmlns: []xml.Attr{{Name: xml.Name{Local: "xmlns:" + NameSpaceSpreadSheetX14.Name.Local}, Value: NameSpaceSpreadSheetX14.Value}}, + URI: ExtURIPivotCacheDefinition, Content: string(pivotCacheBytes), + } + decodeExtLst.Ext = append(decodeExtLst.Ext, ext) + extLstBytes, _ = xml.Marshal(decodeExtLst) + pc.ExtLst = &xlsxExtLst{Ext: strings.TrimSuffix(strings.TrimPrefix(string(extLstBytes), ""), "")} + pivotCache, err := xml.Marshal(pc) + f.saveFileList(opts.pivotCacheXML, pivotCache) + return pivotCacheID, err +} + // addDrawingSlicer adds a slicer shape and fallback shape by giving the -// worksheet name, slicer options, and returns slicer name. -func (f *File) addDrawingSlicer(sheet string, opts *SlicerOptions) (string, error) { - var slicerName string +// worksheet name, slicer name, and slicer options. +func (f *File) addDrawingSlicer(sheet, slicerName string, ns xml.Attr, opts *SlicerOptions) error { drawingID := f.countDrawings() + 1 drawingXML := "xl/drawings/drawing" + strconv.Itoa(drawingID) + ".xml" ws, err := f.workSheetReader(sheet) if err != nil { - return slicerName, err + return err } drawingID, drawingXML = f.prepareDrawing(ws, drawingID, sheet, drawingXML) content, twoCellAnchor, cNvPrID, err := f.twoCellAnchorShape(sheet, drawingXML, opts.Cell, opts.Width, opts.Height, opts.Format) if err != nil { - return slicerName, err + return err } - slicerName = fmt.Sprintf("%s %d", opts.Name, cNvPrID) graphicFrame := xlsxGraphicFrame{ NvGraphicFramePr: xlsxNvGraphicFramePr{ CNvPr: &xlsxCNvPr{ @@ -474,14 +632,14 @@ func (f *File) addDrawingSlicer(sheet string, opts *SlicerOptions) (string, erro FLocksWithSheet: *opts.Format.Locked, FPrintsWithSheet: *opts.Format.PrintObject, } - choice := xlsxChoice{ - XMLNSSle15: NameSpaceDrawingMLSlicerX15.Value, - Requires: NameSpaceDrawingMLSlicerX15.Name.Local, - Content: string(graphic), + choice := xlsxChoice{Requires: ns.Name.Local, Content: string(graphic)} + if ns.Value == NameSpaceDrawingMLA14.Value { // pivot table slicer + choice.XMLNSA14 = ns.Value } - fallback := xlsxFallback{ - Content: string(shape), + if ns.Value == NameSpaceDrawingMLSlicerX15.Value { // table slicer + choice.XMLNSSle15 = ns.Value } + fallback := xlsxFallback{Content: string(shape)} choiceBytes, _ := xml.Marshal(choice) shapeBytes, _ := xml.Marshal(fallback) twoCellAnchor.AlternateContent = append(twoCellAnchor.AlternateContent, &xlsxAlternateContent{ @@ -490,7 +648,7 @@ func (f *File) addDrawingSlicer(sheet string, opts *SlicerOptions) (string, erro }) content.TwoCellAnchor = append(content.TwoCellAnchor, twoCellAnchor) f.Drawings.Store(drawingXML, content) - return slicerName, f.addContentTypePart(drawingID, "drawings") + return f.addContentTypePart(drawingID, "drawings") } // addWorkbookSlicerCache add the association ID of the slicer cache in @@ -502,7 +660,8 @@ func (f *File) addWorkbookSlicerCache(slicerCacheID int, URI string) error { idx int appendMode bool decodeExtLst = new(decodeExtLst) - decodeSlicerCaches *decodeX15SlicerCaches + decodeSlicerCaches = new(decodeSlicerCaches) + x14SlicerCaches = new(xlsxX14SlicerCaches) x15SlicerCaches = new(xlsxX15SlicerCaches) ext *xlsxExt slicerCacheBytes, slicerCachesBytes, extLstBytes []byte @@ -518,24 +677,37 @@ func (f *File) addWorkbookSlicerCache(slicerCacheID int, URI string) error { } for idx, ext = range decodeExtLst.Ext { if ext.URI == URI { - if URI == ExtURISlicerCachesX15 { - decodeSlicerCaches = new(decodeX15SlicerCaches) - _ = f.xmlNewDecoder(strings.NewReader(ext.Content)).Decode(decodeSlicerCaches) - slicerCache := xlsxX14SlicerCache{RID: fmt.Sprintf("rId%d", rID)} - slicerCacheBytes, _ = xml.Marshal(slicerCache) + _ = f.xmlNewDecoder(strings.NewReader(ext.Content)).Decode(decodeSlicerCaches) + slicerCache := xlsxX14SlicerCache{RID: fmt.Sprintf("rId%d", rID)} + slicerCacheBytes, _ = xml.Marshal(slicerCache) + if URI == ExtURISlicerCachesX14 { // pivot table slicer + x14SlicerCaches.Content = decodeSlicerCaches.Content + string(slicerCacheBytes) + x14SlicerCaches.XMLNS = NameSpaceSpreadSheetX14.Value + slicerCachesBytes, _ = xml.Marshal(x14SlicerCaches) + } + if URI == ExtURISlicerCachesX15 { // table slicer x15SlicerCaches.Content = decodeSlicerCaches.Content + string(slicerCacheBytes) x15SlicerCaches.XMLNS = NameSpaceSpreadSheetX14.Value slicerCachesBytes, _ = xml.Marshal(x15SlicerCaches) - decodeExtLst.Ext[idx].Content = string(slicerCachesBytes) - appendMode = true } + decodeExtLst.Ext[idx].Content = string(slicerCachesBytes) + appendMode = true } } } if !appendMode { + slicerCache := xlsxX14SlicerCache{RID: fmt.Sprintf("rId%d", rID)} + slicerCacheBytes, _ = xml.Marshal(slicerCache) + if URI == ExtURISlicerCachesX14 { + x14SlicerCaches.Content = string(slicerCacheBytes) + x14SlicerCaches.XMLNS = NameSpaceSpreadSheetX14.Value + slicerCachesBytes, _ = xml.Marshal(x14SlicerCaches) + decodeExtLst.Ext = append(decodeExtLst.Ext, &xlsxExt{ + xmlns: []xml.Attr{{Name: xml.Name{Local: "xmlns:" + NameSpaceSpreadSheetX14.Name.Local}, Value: NameSpaceSpreadSheetX14.Value}}, + URI: ExtURISlicerCachesX14, Content: string(slicerCachesBytes), + }) + } if URI == ExtURISlicerCachesX15 { - slicerCache := xlsxX14SlicerCache{RID: fmt.Sprintf("rId%d", rID)} - slicerCacheBytes, _ = xml.Marshal(slicerCache) x15SlicerCaches.Content = string(slicerCacheBytes) x15SlicerCaches.XMLNS = NameSpaceSpreadSheetX14.Value slicerCachesBytes, _ = xml.Marshal(x15SlicerCaches) @@ -545,6 +717,10 @@ func (f *File) addWorkbookSlicerCache(slicerCacheID int, URI string) error { }) } } + sort.Slice(decodeExtLst.Ext, func(i, j int) bool { + return inStrSlice(workbookExtURIPriority, decodeExtLst.Ext[i].URI, false) < + inStrSlice(workbookExtURIPriority, decodeExtLst.Ext[j].URI, false) + }) extLstBytes, err = xml.Marshal(decodeExtLst) wb.ExtLst = &xlsxExtLst{Ext: strings.TrimSuffix(strings.TrimPrefix(string(extLstBytes), ""), "")} return err diff --git a/slicer_test.go b/slicer_test.go index 663a4e1311..da6fa915f1 100644 --- a/slicer_test.go +++ b/slicer_test.go @@ -2,6 +2,7 @@ package excelize import ( "fmt" + "math/rand" "os" "path/filepath" "testing" @@ -19,21 +20,24 @@ func TestAddSlicer(t *testing.T) { Range: "A1:D5", })) assert.NoError(t, f.AddSlicer("Sheet1", &SlicerOptions{ - Name: "Column1", - Table: "Table1", - Cell: "E1", - Caption: "Column1", + Name: "Column1", + Cell: "E1", + TableSheet: "Sheet1", + TableName: "Table1", + Caption: "Column1", })) assert.NoError(t, f.AddSlicer("Sheet1", &SlicerOptions{ - Name: "Column1", - Table: "Table1", - Cell: "I1", - Caption: "Column1", + Name: "Column1", + Cell: "I1", + TableSheet: "Sheet1", + TableName: "Table1", + Caption: "Column1", })) assert.NoError(t, f.AddSlicer("Sheet1", &SlicerOptions{ Name: colName, - Table: "Table1", Cell: "M1", + TableSheet: "Sheet1", + TableName: "Table1", Caption: colName, Macro: "Button1_Click", Width: 200, @@ -41,38 +45,152 @@ func TestAddSlicer(t *testing.T) { DisplayHeader: &disable, ItemDesc: true, })) + // Test create two pivot tables in a new worksheet + _, err := f.NewSheet("Sheet2") + assert.NoError(t, err) + // Create some data in a sheet + month := []string{"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"} + year := []int{2017, 2018, 2019} + types := []string{"Meat", "Dairy", "Beverages", "Produce"} + region := []string{"East", "West", "North", "South"} + assert.NoError(t, f.SetSheetRow("Sheet2", "A1", &[]string{"Month", "Year", "Type", "Sales", "Region"})) + for row := 2; row < 32; row++ { + assert.NoError(t, f.SetCellValue("Sheet2", fmt.Sprintf("A%d", row), month[rand.Intn(12)])) + assert.NoError(t, f.SetCellValue("Sheet2", fmt.Sprintf("B%d", row), year[rand.Intn(3)])) + assert.NoError(t, f.SetCellValue("Sheet2", fmt.Sprintf("C%d", row), types[rand.Intn(4)])) + assert.NoError(t, f.SetCellValue("Sheet2", fmt.Sprintf("D%d", row), rand.Intn(5000))) + assert.NoError(t, f.SetCellValue("Sheet2", fmt.Sprintf("E%d", row), region[rand.Intn(4)])) + } + assert.NoError(t, f.AddPivotTable(&PivotTableOptions{ + DataRange: "Sheet2!A1:E31", + PivotTableRange: "Sheet2!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(&PivotTableOptions{ + DataRange: "Sheet2!A1:E31", + PivotTableRange: "Sheet2!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"}}, + RowGrandTotals: true, + ColGrandTotals: true, + ShowDrill: true, + ShowRowHeaders: true, + ShowColHeaders: true, + ShowLastColumn: true, + })) + // Test add a pivot table slicer + assert.NoError(t, f.AddSlicer("Sheet2", &SlicerOptions{ + Name: "Month", + Cell: "G42", + TableSheet: "Sheet2", + TableName: "PivotTable1", + Caption: "Month", + })) + // Test add a pivot table slicer with duplicate field name + assert.NoError(t, f.AddSlicer("Sheet2", &SlicerOptions{ + Name: "Month", + Cell: "K42", + TableSheet: "Sheet2", + TableName: "PivotTable1", + Caption: "Month", + })) + // Test add a pivot table slicer for another pivot table in a worksheet + assert.NoError(t, f.AddSlicer("Sheet2", &SlicerOptions{ + Name: "Region", + Cell: "O42", + TableSheet: "Sheet2", + TableName: "PivotTable2", + Caption: "Region", + ItemDesc: true, + })) // Test add a table slicer with empty slicer options assert.Equal(t, ErrParameterRequired, f.AddSlicer("Sheet1", nil)) // Test add a table slicer with invalid slicer options for _, opts := range []*SlicerOptions{ - {Table: "Table1", Cell: "Q1"}, - {Name: "Column", Cell: "Q1"}, - {Name: "Column", Table: "Table1"}, + {Cell: "Q1", TableSheet: "Sheet1", TableName: "Table1"}, + {Name: "Column", Cell: "Q1", TableSheet: "Sheet1"}, + {Name: "Column", TableSheet: "Sheet1", TableName: "Table1"}, } { assert.Equal(t, ErrParameterInvalid, f.AddSlicer("Sheet1", opts)) } // Test add a table slicer with not exist worksheet assert.EqualError(t, f.AddSlicer("SheetN", &SlicerOptions{ - Name: "Column2", - Table: "Table1", - Cell: "Q1", + Name: "Column2", + Cell: "Q1", + TableSheet: "SheetN", + TableName: "Table1", }), "sheet SheetN does not exist") // Test add a table slicer with not exist table name assert.Equal(t, newNoExistTableError("Table2"), f.AddSlicer("Sheet1", &SlicerOptions{ - Name: "Column2", - Table: "Table2", - Cell: "Q1", + Name: "Column2", + Cell: "Q1", + TableSheet: "Sheet1", + TableName: "Table2", })) // Test add a table slicer with invalid slicer name assert.Equal(t, newInvalidSlicerNameError("Column6"), f.AddSlicer("Sheet1", &SlicerOptions{ - Name: "Column6", - Table: "Table1", - Cell: "Q1", + Name: "Column6", + Cell: "Q1", + TableSheet: "Sheet1", + TableName: "Table1", })) + workbookPath := filepath.Join("test", "TestAddSlicer.xlsm") file, err := os.ReadFile(filepath.Join("test", "vbaProject.bin")) assert.NoError(t, err) assert.NoError(t, f.AddVBAProject(file)) - assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddSlicer.xlsm"))) + assert.NoError(t, f.SaveAs(workbookPath)) + assert.NoError(t, f.Close()) + + // Test add a pivot table slicer with unsupported charset pivot table + f, err = OpenFile(workbookPath) + assert.NoError(t, err) + f.Pkg.Store("xl/pivotTables/pivotTable2.xml", MacintoshCyrillicCharset) + assert.EqualError(t, f.AddSlicer("Sheet2", &SlicerOptions{ + Name: "Month", + Cell: "G42", + TableSheet: "Sheet2", + TableName: "PivotTable1", + Caption: "Month", + }), "XML syntax error on line 1: invalid UTF-8") + assert.NoError(t, f.Close()) + + // Test add a pivot table slicer with workbook which contains timeline + f, err = OpenFile(workbookPath) + assert.NoError(t, err) + f.Pkg.Store("xl/timelines/timeline1.xml", []byte(fmt.Sprintf(``, NameSpaceSpreadSheetX15.Value))) + assert.NoError(t, f.AddSlicer("Sheet2", &SlicerOptions{ + Name: "Month", + Cell: "G42", + TableSheet: "Sheet2", + TableName: "PivotTable1", + Caption: "Month", + })) + assert.NoError(t, f.Close()) + + // Test add a pivot table slicer with unsupported charset timeline + f, err = OpenFile(workbookPath) + assert.NoError(t, err) + f.Pkg.Store("xl/timelines/timeline1.xml", MacintoshCyrillicCharset) + assert.NoError(t, f.AddSlicer("Sheet2", &SlicerOptions{ + Name: "Month", + Cell: "G42", + TableSheet: "Sheet2", + TableName: "PivotTable1", + Caption: "Month", + })) assert.NoError(t, f.Close()) // Test add a table slicer with invalid worksheet extension list @@ -85,9 +203,10 @@ func TestAddSlicer(t *testing.T) { assert.True(t, ok) ws.(*xlsxWorksheet).ExtLst = &xlsxExtLst{Ext: "<>"} assert.Error(t, f.AddSlicer("Sheet1", &SlicerOptions{ - Name: "Column1", - Table: "Table1", - Cell: "E1", + Name: "Column1", + Cell: "E1", + TableSheet: "Sheet1", + TableName: "Table1", })) assert.NoError(t, f.Close()) @@ -99,9 +218,10 @@ func TestAddSlicer(t *testing.T) { })) f.Pkg.Store("xl/slicers/slicer2.xml", MacintoshCyrillicCharset) assert.EqualError(t, f.AddSlicer("Sheet1", &SlicerOptions{ - Name: "Column1", - Table: "Table1", - Cell: "E1", + Name: "Column1", + Cell: "E1", + TableName: "Table1", + TableSheet: "Sheet1", }), "XML syntax error on line 1: invalid UTF-8") assert.NoError(t, f.Close()) @@ -113,9 +233,10 @@ func TestAddSlicer(t *testing.T) { })) f.WorkBook.ExtLst = &xlsxExtLst{Ext: "<>"} assert.Error(t, f.AddSlicer("Sheet1", &SlicerOptions{ - Name: "Column1", - Table: "Table1", - Cell: "E1", + Name: "Column1", + Cell: "E1", + TableName: "Table1", + TableSheet: "Sheet1", })) assert.NoError(t, f.Close()) @@ -128,9 +249,10 @@ func TestAddSlicer(t *testing.T) { f.ContentTypes = nil f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset) assert.EqualError(t, f.AddSlicer("Sheet1", &SlicerOptions{ - Name: "Column1", - Table: "Table1", - Cell: "E1", + Name: "Column1", + Cell: "E1", + TableName: "Table1", + TableSheet: "Sheet1", }), "XML syntax error on line 1: invalid UTF-8") f.ContentTypes = nil f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset) @@ -145,10 +267,11 @@ func TestAddSlicer(t *testing.T) { })) f.Pkg.Store("xl/drawings/drawing2.xml", MacintoshCyrillicCharset) assert.EqualError(t, f.AddSlicer("Sheet1", &SlicerOptions{ - Name: "Column1", - Table: "Table1", - Cell: "E1", - Caption: "Column1", + Name: "Column1", + Cell: "E1", + TableSheet: "Sheet1", + TableName: "Table1", + Caption: "Column1", }), "XML syntax error on line 1: invalid UTF-8") assert.NoError(t, f.Close()) } @@ -156,7 +279,7 @@ func TestAddSlicer(t *testing.T) { func TestAddSheetSlicer(t *testing.T) { f := NewFile() // Test add sheet slicer with not exist worksheet name - _, err := f.addSheetSlicer("SheetN") + _, err := f.addSheetSlicer("SheetN", ExtURISlicerListX15) assert.EqualError(t, err, "sheet SheetN does not exist") assert.NoError(t, f.Close()) } @@ -164,41 +287,41 @@ func TestAddSheetSlicer(t *testing.T) { func TestAddSheetTableSlicer(t *testing.T) { f := NewFile() // Test add sheet table slicer with invalid worksheet extension - assert.Error(t, f.addSheetTableSlicer(&xlsxWorksheet{ExtLst: &xlsxExtLst{Ext: "<>"}}, 0)) + assert.Error(t, f.addSheetTableSlicer(&xlsxWorksheet{ExtLst: &xlsxExtLst{Ext: "<>"}}, 0, ExtURISlicerListX15)) // Test add sheet table slicer with existing worksheet extension - assert.NoError(t, f.addSheetTableSlicer(&xlsxWorksheet{ExtLst: &xlsxExtLst{Ext: fmt.Sprintf("", ExtURITimelineRefs)}}, 1)) + assert.NoError(t, f.addSheetTableSlicer(&xlsxWorksheet{ExtLst: &xlsxExtLst{Ext: fmt.Sprintf("", ExtURITimelineRefs)}}, 1, ExtURISlicerListX15)) assert.NoError(t, f.Close()) } func TestSetSlicerCache(t *testing.T) { f := NewFile() f.Pkg.Store("xl/slicerCaches/slicerCache1.xml", MacintoshCyrillicCharset) - _, err := f.setSlicerCache(1, &SlicerOptions{}, &Table{}) + _, err := f.setSlicerCache("Sheet1", 1, &SlicerOptions{}, &Table{}, nil) assert.NoError(t, err) assert.NoError(t, f.Close()) f = NewFile() f.Pkg.Store("xl/slicerCaches/slicerCache2.xml", []byte(fmt.Sprintf(``, NameSpaceSpreadSheetX14.Value, ExtURISlicerCacheDefinition))) - _, err = f.setSlicerCache(1, &SlicerOptions{}, &Table{}) + _, err = f.setSlicerCache("Sheet1", 1, &SlicerOptions{}, &Table{}, nil) assert.NoError(t, err) assert.NoError(t, f.Close()) f = NewFile() f.Pkg.Store("xl/slicerCaches/slicerCache2.xml", []byte(fmt.Sprintf(``, NameSpaceSpreadSheetX14.Value, ExtURISlicerCacheDefinition))) - _, err = f.setSlicerCache(1, &SlicerOptions{}, &Table{}) + _, err = f.setSlicerCache("Sheet1", 1, &SlicerOptions{}, &Table{}, nil) assert.NoError(t, err) assert.NoError(t, f.Close()) f = NewFile() f.Pkg.Store("xl/slicerCaches/slicerCache2.xml", []byte(fmt.Sprintf(``, NameSpaceSpreadSheetX14.Value, ExtURISlicerCacheDefinition))) - _, err = f.setSlicerCache(1, &SlicerOptions{}, &Table{tID: 1}) + _, err = f.setSlicerCache("Sheet1", 1, &SlicerOptions{}, &Table{tID: 1}, nil) assert.NoError(t, err) assert.NoError(t, f.Close()) f = NewFile() f.Pkg.Store("xl/slicerCaches/slicerCache2.xml", []byte(fmt.Sprintf(``, NameSpaceSpreadSheetX14.Value))) - _, err = f.setSlicerCache(1, &SlicerOptions{}, &Table{tID: 1}) + _, err = f.setSlicerCache("Sheet1", 1, &SlicerOptions{}, &Table{tID: 1}, nil) assert.NoError(t, err) assert.NoError(t, f.Close()) } @@ -207,31 +330,36 @@ func TestAddSlicerCache(t *testing.T) { f := NewFile() f.ContentTypes = nil f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset) - assert.EqualError(t, f.addSlicerCache("Slicer1", 0, &SlicerOptions{}, &Table{}), "XML syntax error on line 1: invalid UTF-8") + assert.EqualError(t, f.addSlicerCache("Slicer1", 0, &SlicerOptions{}, &Table{}, nil), "XML syntax error on line 1: invalid UTF-8") + // Test add a pivot table cache slicer with unsupported charset + pivotCacheXML := "xl/pivotCache/pivotCacheDefinition1.xml" + f.Pkg.Store(pivotCacheXML, MacintoshCyrillicCharset) + assert.EqualError(t, f.addSlicerCache("Slicer1", 0, &SlicerOptions{}, nil, + &PivotTableOptions{pivotCacheXML: pivotCacheXML}), "XML syntax error on line 1: invalid UTF-8") assert.NoError(t, f.Close()) } func TestAddDrawingSlicer(t *testing.T) { f := NewFile() // Test add a drawing slicer with not exist worksheet - _, err := f.addDrawingSlicer("SheetN", &SlicerOptions{ - Name: "Column2", - Table: "Table1", - Cell: "Q1", - }) - assert.EqualError(t, err, "sheet SheetN does not exist") + assert.EqualError(t, f.addDrawingSlicer("SheetN", "Column2", NameSpaceDrawingMLSlicerX15, &SlicerOptions{ + Name: "Column2", + Cell: "Q1", + TableSheet: "SheetN", + TableName: "Table1", + }), "sheet SheetN does not exist") // Test add a drawing slicer with invalid cell reference - _, err = f.addDrawingSlicer("Sheet1", &SlicerOptions{ - Name: "Column2", - Table: "Table1", - Cell: "A", - }) - assert.EqualError(t, err, "cannot convert cell \"A\" to coordinates: invalid cell name \"A\"") + assert.EqualError(t, f.addDrawingSlicer("Sheet1", "Column2", NameSpaceDrawingMLSlicerX15, &SlicerOptions{ + Name: "Column2", + Cell: "A", + TableSheet: "Sheet1", + TableName: "Table1", + }), "cannot convert cell \"A\" to coordinates: invalid cell name \"A\"") assert.NoError(t, f.Close()) } func TestAddWorkbookSlicerCache(t *testing.T) { - // Test add a workbook slicer cache with with unsupported charset workbook + // Test add a workbook slicer cache with unsupported charset workbook f := NewFile() f.WorkBook = nil f.Pkg.Store("xl/workbook.xml", MacintoshCyrillicCharset) @@ -245,3 +373,14 @@ func TestGenSlicerCacheName(t *testing.T) { assert.Equal(t, "Slicer_Column_11", f.genSlicerCacheName("Column 1")) assert.NoError(t, f.Close()) } + +func TestAddPivotCacheSlicer(t *testing.T) { + f := NewFile() + pivotCacheXML := "xl/pivotCache/pivotCacheDefinition1.xml" + // Test add a pivot table cache slicer with existing extension list + f.Pkg.Store(pivotCacheXML, []byte(fmt.Sprintf(``, NameSpaceSpreadSheet.Value, ExtURIPivotCacheDefinition))) + _, err := f.addPivotCacheSlicer(&PivotTableOptions{ + pivotCacheXML: pivotCacheXML, + }) + assert.NoError(t, err) +} diff --git a/sparkline.go b/sparkline.go index 7bb50f0f9e..810d21365c 100644 --- a/sparkline.go +++ b/sparkline.go @@ -528,8 +528,8 @@ func (f *File) appendSparkline(ws *xlsxWorksheet, group *xlsxX14SparklineGroup, }) } sort.Slice(decodeExtLst.Ext, func(i, j int) bool { - return inStrSlice(extensionURIPriority, decodeExtLst.Ext[i].URI, false) < - inStrSlice(extensionURIPriority, decodeExtLst.Ext[j].URI, false) + return inStrSlice(worksheetExtURIPriority, decodeExtLst.Ext[i].URI, false) < + inStrSlice(worksheetExtURIPriority, decodeExtLst.Ext[j].URI, false) }) extLstBytes, err = xml.Marshal(decodeExtLst) ws.ExtLst = &xlsxExtLst{Ext: strings.TrimSuffix(strings.TrimPrefix(string(extLstBytes), ""), "")} diff --git a/styles.go b/styles.go index 528529548a..1de83bcb03 100644 --- a/styles.go +++ b/styles.go @@ -2603,8 +2603,8 @@ func (f *File) appendCfRule(ws *xlsxWorksheet, rule *xlsxX14CfRule) error { }) } sort.Slice(decodeExtLst.Ext, func(i, j int) bool { - return inStrSlice(extensionURIPriority, decodeExtLst.Ext[i].URI, false) < - inStrSlice(extensionURIPriority, decodeExtLst.Ext[j].URI, false) + return inStrSlice(worksheetExtURIPriority, decodeExtLst.Ext[i].URI, false) < + inStrSlice(worksheetExtURIPriority, decodeExtLst.Ext[j].URI, false) }) extLstBytes, err = xml.Marshal(decodeExtLst) ws.ExtLst = &xlsxExtLst{Ext: strings.TrimSuffix(strings.TrimPrefix(string(extLstBytes), ""), "")} diff --git a/templates.go b/templates.go index 1c6f4ddb16..c94a09b406 100644 --- a/templates.go +++ b/templates.go @@ -22,6 +22,7 @@ var ( NameSpaceDocumentPropertiesVariantTypes = xml.Attr{Name: xml.Name{Local: "vt", Space: "xmlns"}, Value: "http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"} NameSpaceDrawing2016SVG = xml.Attr{Name: xml.Name{Local: "asvg", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/drawing/2016/SVG/main"} NameSpaceDrawingML = xml.Attr{Name: xml.Name{Local: "a", Space: "xmlns"}, Value: "http://schemas.openxmlformats.org/drawingml/2006/main"} + NameSpaceDrawingMLA14 = xml.Attr{Name: xml.Name{Local: "a14", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/drawing/2010/main"} NameSpaceDrawingMLChart = xml.Attr{Name: xml.Name{Local: "c", Space: "xmlns"}, Value: "http://schemas.openxmlformats.org/drawingml/2006/chart"} NameSpaceDrawingMLSlicer = xml.Attr{Name: xml.Name{Local: "sle", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/drawing/2010/slicer"} NameSpaceDrawingMLSlicerX15 = xml.Attr{Name: xml.Name{Local: "sle15", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/drawing/2012/slicer"} @@ -117,7 +118,7 @@ const ( ExtURISlicerCacheHideItemsWithNoData = "{470722E0-AACD-4C17-9CDC-17EF765DBC7E}" ExtURISlicerCachesX14 = "{BBE1A952-AA13-448e-AADC-164F8A28A991}" ExtURISlicerCachesX15 = "{46BE6895-7355-4a93-B00E-2C351335B9C9}" - ExtURISlicerListX14 = "{A8765BA9-456A-4DAB-B4F3-ACF838C121DE}" + ExtURISlicerListX14 = "{A8765BA9-456A-4dab-B4F3-ACF838C121DE}" ExtURISlicerListX15 = "{3A4CF648-6AED-40f4-86FF-DC5316D8AED3}" ExtURISparklineGroups = "{05C60535-1F16-4fd2-B633-F4F36F0B64E0}" ExtURISVG = "{96DAC541-7B7A-43D3-8B79-37D633B846F1}" @@ -129,8 +130,25 @@ const ( ExtURIWorkbookPrX15 = "{140A7094-0E35-4892-8432-C4D2E57EDEB5}" ) -// extensionURIPriority is the priority of URI in the extension lists. -var extensionURIPriority = []string{ +// workbookExtURIPriority is the priority of URI in the workbook extension lists. +var workbookExtURIPriority = []string{ + ExtURIPivotCachesX14, + ExtURISlicerCachesX14, + ExtURISlicerCachesX15, + ExtURIWorkbookPrX14, + ExtURIPivotCachesX15, + ExtURIPivotTableReferences, + ExtURITimelineCachePivotCaches, + ExtURITimelineCacheRefs, + ExtURIWorkbookPrX15, + ExtURIDataModel, + ExtURICalcFeatures, + ExtURIExternalLinkPr, + ExtURIModelTimeGroupings, +} + +// worksheetExtURIPriority is the priority of URI in the worksheet extension lists. +var worksheetExtURIPriority = []string{ ExtURIConditionalFormattings, ExtURIDataValidations, ExtURISparklineGroups, @@ -167,6 +185,7 @@ const ( // Excel 2007 or in compatibility mode. Slicer can only be used with // PivotTables created in Excel 2007 or a newer version of Excel. pivotTableVersion = 3 + pivotTableRefreshedVersion = 8 defaultDrawingScale = 1.0 defaultChartDimensionWidth = 480 defaultChartDimensionHeight = 260 diff --git a/xmlPivotCache.go b/xmlPivotCache.go index 1925fa4d23..9f5a84165a 100644 --- a/xmlPivotCache.go +++ b/xmlPivotCache.go @@ -120,26 +120,26 @@ type xlsxCacheField struct { // those values that are referenced in multiple places across all the // PivotTable parts. type xlsxSharedItems struct { - ContainsSemiMixedTypes bool `xml:"containsSemiMixedTypes,attr,omitempty"` - ContainsNonDate bool `xml:"containsNonDate,attr,omitempty"` - ContainsDate bool `xml:"containsDate,attr,omitempty"` - ContainsString bool `xml:"containsString,attr,omitempty"` - ContainsBlank bool `xml:"containsBlank,attr,omitempty"` - ContainsMixedTypes bool `xml:"containsMixedTypes,attr,omitempty"` - ContainsNumber bool `xml:"containsNumber,attr,omitempty"` - ContainsInteger bool `xml:"containsInteger,attr,omitempty"` - MinValue float64 `xml:"minValue,attr,omitempty"` - MaxValue float64 `xml:"maxValue,attr,omitempty"` - MinDate string `xml:"minDate,attr,omitempty"` - MaxDate string `xml:"maxDate,attr,omitempty"` - Count int `xml:"count,attr"` - LongText bool `xml:"longText,attr,omitempty"` - M *xlsxMissing `xml:"m"` - N *xlsxNumber `xml:"n"` - B *xlsxBoolean `xml:"b"` - E *xlsxError `xml:"e"` - S *xlsxString `xml:"s"` - D *xlsxDateTime `xml:"d"` + ContainsSemiMixedTypes bool `xml:"containsSemiMixedTypes,attr,omitempty"` + ContainsNonDate bool `xml:"containsNonDate,attr,omitempty"` + ContainsDate bool `xml:"containsDate,attr,omitempty"` + ContainsString bool `xml:"containsString,attr,omitempty"` + ContainsBlank bool `xml:"containsBlank,attr,omitempty"` + ContainsMixedTypes bool `xml:"containsMixedTypes,attr,omitempty"` + ContainsNumber bool `xml:"containsNumber,attr,omitempty"` + ContainsInteger bool `xml:"containsInteger,attr,omitempty"` + MinValue float64 `xml:"minValue,attr,omitempty"` + MaxValue float64 `xml:"maxValue,attr,omitempty"` + MinDate string `xml:"minDate,attr,omitempty"` + MaxDate string `xml:"maxDate,attr,omitempty"` + Count int `xml:"count,attr"` + LongText bool `xml:"longText,attr,omitempty"` + M []xlsxMissing `xml:"m"` + N []xlsxNumber `xml:"n"` + B []xlsxBoolean `xml:"b"` + E []xlsxError `xml:"e"` + S []xlsxString `xml:"s"` + D []xlsxDateTime `xml:"d"` } // xlsxMissing represents a value that was not specified. @@ -226,3 +226,17 @@ type xlsxMeasureGroups struct{} // xlsxMaps represents the PivotTable OLAP measure group - Dimension maps. type xlsxMaps struct{} + +// xlsxX14PivotCacheDefinition specifies the extended properties of a pivot +// table cache definition. +type xlsxX14PivotCacheDefinition struct { + XMLName xml.Name `xml:"x14:pivotCacheDefinition"` + PivotCacheID int `xml:"pivotCacheId,attr"` +} + +// decodeX14PivotCacheDefinition defines the structure used to parse the +// x14:pivotCacheDefinition element of a pivot table cache. +type decodeX14PivotCacheDefinition struct { + XMLName xml.Name `xml:"pivotCacheDefinition"` + PivotCacheID int `xml:"pivotCacheId,attr"` +} diff --git a/xmlSlicers.go b/xmlSlicers.go index e259de8990..56dde04ef9 100644 --- a/xmlSlicers.go +++ b/xmlSlicers.go @@ -126,6 +126,13 @@ type xlsxX14Slicer struct { RID string `xml:"r:id,attr"` } +// xlsxX14SlicerCaches directly maps the x14:slicerCache element. +type xlsxX14SlicerCaches struct { + XMLName xml.Name `xml:"x14:slicerCaches"` + XMLNS string `xml:"xmlns:x14,attr"` + Content string `xml:",innerxml"` +} + // xlsxX15SlicerCaches directly maps the x14:slicerCache element. type xlsxX14SlicerCache struct { XMLName xml.Name `xml:"x14:slicerCache"` @@ -160,9 +167,39 @@ type decodeSlicer struct { RID string `xml:"id,attr"` } -// decodeX15SlicerCaches defines the structure used to parse the -// x15:slicerCaches element of a slicer cache. -type decodeX15SlicerCaches struct { +// decodeSlicerCaches defines the structure used to parse the +// x14:slicerCaches and x15:slicerCaches element of a slicer cache. +type decodeSlicerCaches struct { XMLName xml.Name `xml:"slicerCaches"` Content string `xml:",innerxml"` } + +// xlsxTimelines is a mechanism for filtering data in pivot table views, cube +// functions and charts based on non-worksheet pivot tables. In the case of +// using OLAP Timeline source data, a Timeline is based on a key attribute of +// an OLAP hierarchy. In the case of using native Timeline source data, a +// Timeline is based on a data table column. +type xlsxTimelines struct { + XMLName xml.Name `xml:"http://schemas.microsoft.com/office/spreadsheetml/2010/11/main timelines"` + XMLNSXMC string `xml:"xmlns:mc,attr"` + XMLNSX string `xml:"xmlns:x,attr"` + XMLNSXR10 string `xml:"xmlns:xr10,attr"` + Timeline []xlsxTimeline `xml:"timeline"` +} + +// xlsxTimeline is timeline view specifies the display of a timeline on a +// worksheet. +type xlsxTimeline struct { + Name string `xml:"name,attr"` + XR10UID string `xml:"xr10:uid,attr,omitempty"` + Cache string `xml:"cache,attr"` + Caption string `xml:"caption,attr,omitempty"` + ShowHeader *bool `xml:"showHeader,attr"` + ShowSelectionLabel *bool `xml:"showSelectionLabel,attr"` + ShowTimeLevel *bool `xml:"showTimeLevel,attr"` + ShowHorizontalScrollbar *bool `xml:"showHorizontalScrollbar,attr"` + Level int `xml:"level,attr"` + SelectionLevel int `xml:"selectionLevel,attr"` + ScrollPosition string `xml:"scrollPosition,attr,omitempty"` + Style string `xml:"style,attr,omitempty"` +} diff --git a/xmlWorkbook.go b/xmlWorkbook.go index 0a3586f8e7..cc953975ed 100644 --- a/xmlWorkbook.go +++ b/xmlWorkbook.go @@ -245,6 +245,7 @@ type xlsxAlternateContent struct { // AlternateContent elements. type xlsxChoice struct { XMLName xml.Name `xml:"mc:Choice"` + XMLNSA14 string `xml:"xmlns:a14,attr,omitempty"` XMLNSSle15 string `xml:"xmlns:sle15,attr,omitempty"` Requires string `xml:"Requires,attr,omitempty"` Content string `xml:",innerxml"`