diff --git a/errors.go b/errors.go index b460dfd..b12b06c 100644 --- a/errors.go +++ b/errors.go @@ -264,6 +264,12 @@ func newInvalidStyleID(styleID int) error { return fmt.Errorf("invalid style ID %d", styleID) } +// newNoExistSlicerError defined the error message on receiving the non existing +// slicer name. +func newNoExistSlicerError(name string) error { + return fmt.Errorf("slicer %s does not exist", name) +} + // newNoExistTableError defined the error message on receiving the non existing // table name. func newNoExistTableError(name string) error { diff --git a/pivotTable.go b/pivotTable.go index 490b487..6205c54 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -785,12 +785,11 @@ func (f *File) getPivotTableDataRange(opts *PivotTableOptions) error { opts.pivotDataRange = opts.DataRange return nil } - for _, sheetName := range f.GetSheetList() { - tables, err := f.GetTables(sheetName) - e := ErrSheetNotExist{sheetName} - if err != nil && err.Error() != newNotWorksheetError(sheetName).Error() && err.Error() != e.Error() { - return err - } + tbls, err := f.getTables() + if err != nil { + return err + } + for sheetName, tables := range tbls { for _, table := range tables { if table.Name == opts.DataRange { opts.pivotDataRange, opts.namedDataRange = fmt.Sprintf("%s!%s", sheetName, table.Range), true @@ -1016,8 +1015,8 @@ func (f *File) DeletePivotTable(sheet, name string) error { return err } pivotTableCaches := map[string]int{} - for _, sheetName := range f.GetSheetList() { - sheetPivotTables, _ := f.GetPivotTables(sheetName) + pivotTables, _ := f.getPivotTables() + for _, sheetPivotTables := range pivotTables { for _, sheetPivotTable := range sheetPivotTables { pivotTableCaches[sheetPivotTable.pivotCacheXML]++ } @@ -1038,3 +1037,17 @@ func (f *File) DeletePivotTable(sheet, name string) error { } return newNoExistTableError(name) } + +// getPivotTables provides a function to get all pivot tables in a workbook. +func (f *File) getPivotTables() (map[string][]PivotTableOptions, error) { + pivotTables := map[string][]PivotTableOptions{} + for _, sheetName := range f.GetSheetList() { + pts, err := f.GetPivotTables(sheetName) + e := ErrSheetNotExist{sheetName} + if err != nil && err.Error() != newNotWorksheetError(sheetName).Error() && err.Error() != e.Error() { + return pivotTables, err + } + pivotTables[sheetName] = append(pivotTables[sheetName], pts...) + } + return pivotTables, nil +} diff --git a/pivotTable_test.go b/pivotTable_test.go index 58b1dbe..50f95bf 100644 --- a/pivotTable_test.go +++ b/pivotTable_test.go @@ -343,6 +343,8 @@ func TestPivotTable(t *testing.T) { 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") + _, err = f.getPivotTables() + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") assert.NoError(t, f.Close()) } diff --git a/slicer.go b/slicer.go index a7f26ed..7b4a2d8 100644 --- a/slicer.go +++ b/slicer.go @@ -53,17 +53,23 @@ import ( // // Format specifies the format of the slicer, this setting is optional. type SlicerOptions struct { - Name string - Cell string - TableSheet string - TableName string - Caption string - Macro string - Width uint - Height uint - DisplayHeader *bool - ItemDesc bool - Format GraphicOptions + slicerXML string + slicerCacheXML string + slicerCacheName string + slicerSheetName string + slicerSheetRID string + drawingXML string + Name string + Cell string + TableSheet string + TableName string + Caption string + Macro string + Width uint + Height uint + DisplayHeader *bool + ItemDesc bool + Format GraphicOptions } // AddSlicer function inserts a slicer by giving the worksheet name and slicer @@ -99,7 +105,7 @@ func (f *File) AddSlicer(sheet string, opts *SlicerOptions) error { if err != nil { return err } - slicerCacheName, err := f.setSlicerCache(sheet, colIdx, opts, table, pivotTable) + slicerCacheName, err := f.setSlicerCache(colIdx, opts, table, pivotTable) if err != nil { return err } @@ -224,7 +230,6 @@ func (f *File) addSheetSlicer(sheet, extURI string) (int, error) { slicerID = f.countSlicers() + 1 ws, err = f.workSheetReader(sheet) decodeExtLst = new(decodeExtLst) - slicerList = new(decodeSlicerList) ) if err != nil { return slicerID, err @@ -236,6 +241,7 @@ func (f *File) addSheetSlicer(sheet, extURI string) (int, error) { } for _, ext := range decodeExtLst.Ext { if ext.URI == extURI { + slicerList := new(decodeSlicerList) _ = f.xmlNewDecoder(strings.NewReader(ext.Content)).Decode(slicerList) for _, slicer := range slicerList.Slicer { if slicer.RID != "" { @@ -390,14 +396,13 @@ 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(sheet string, colIdx int, opts *SlicerOptions, table *Table, pivotTable *PivotTableOptions) (string, error) { +func (f *File) setSlicerCache(colIdx int, opts *SlicerOptions, table *Table, pivotTable *PivotTableOptions) (string, error) { var ok bool var slicerCacheName string f.Pkg.Range(func(k, v interface{}) bool { if strings.Contains(k.(string), "xl/slicerCaches/slicerCache") { - slicerCache := &xlsxSlicerCacheDefinition{} - if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(v.([]byte)))). - Decode(slicerCache); err != nil && err != io.EOF { + slicerCache, err := f.slicerCacheReader(k.(string)) + if err != nil { return true } if pivotTable != nil && slicerCache.PivotTables != nil { @@ -449,6 +454,20 @@ func (f *File) slicerReader(slicerXML string) (*xlsxSlicers, error) { return slicer, nil } +// slicerCacheReader provides a function to get the pointer to the structure +// after deserialization of xl/slicerCaches/slicerCache%d.xml. +func (f *File) slicerCacheReader(slicerCacheXML string) (*xlsxSlicerCacheDefinition, error) { + content, ok := f.Pkg.Load(slicerCacheXML) + slicerCache := &xlsxSlicerCacheDefinition{} + if ok && content != nil { + if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(content.([]byte)))). + Decode(slicerCache); err != nil && err != io.EOF { + return nil, err + } + } + return slicerCache, 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) { @@ -586,6 +605,7 @@ func (f *File) addDrawingSlicer(sheet, slicerName string, ns xml.Attr, opts *Sli return err } graphicFrame := xlsxGraphicFrame{ + Macro: opts.Macro, NvGraphicFramePr: xlsxNvGraphicFramePr{ CNvPr: &xlsxCNvPr{ ID: cNvPrID, @@ -725,3 +745,306 @@ func (f *File) addWorkbookSlicerCache(slicerCacheID int, URI string) error { wb.ExtLst = &xlsxExtLst{Ext: strings.TrimSuffix(strings.TrimPrefix(string(extLstBytes), ""), "")} return err } + +// GetSlicers provides the method to get all slicers in a worksheet by a given +// worksheet name. Note that, this function does not support getting the height, +// width, and graphic options of the slicer shape currently. +func (f *File) GetSlicers(sheet string) ([]SlicerOptions, error) { + var ( + slicers []SlicerOptions + ws, err = f.workSheetReader(sheet) + decodeExtLst = new(decodeExtLst) + ) + if err != nil { + return slicers, err + } + if ws.ExtLst == nil { + return slicers, err + } + target := f.getSheetRelationshipsTargetByID(sheet, ws.Drawing.RID) + drawingXML := strings.TrimPrefix(strings.ReplaceAll(target, "..", "xl"), "/") + if err = f.xmlNewDecoder(strings.NewReader("" + ws.ExtLst.Ext + "")). + Decode(decodeExtLst); err != nil && err != io.EOF { + return slicers, err + } + for _, ext := range decodeExtLst.Ext { + if ext.URI == ExtURISlicerListX14 || ext.URI == ExtURISlicerListX15 { + slicerList := new(decodeSlicerList) + _ = f.xmlNewDecoder(strings.NewReader(ext.Content)).Decode(&slicerList) + for _, slicer := range slicerList.Slicer { + if slicer.RID != "" { + opts, err := f.getSlicers(sheet, slicer.RID, drawingXML) + if err != nil { + return slicers, err + } + slicers = append(slicers, opts...) + } + } + } + } + return slicers, err +} + +// getSlicerCache provides a function to get a slicer cache by given slicer +// cache name and slicer options. +func (f *File) getSlicerCache(slicerCacheName string, opt *SlicerOptions) *xlsxSlicerCacheDefinition { + var ( + err error + slicerCache *xlsxSlicerCacheDefinition + ) + f.Pkg.Range(func(k, v interface{}) bool { + if strings.Contains(k.(string), "xl/slicerCaches/slicerCache") { + slicerCache, err = f.slicerCacheReader(k.(string)) + if err != nil { + return true + } + if slicerCache.Name == slicerCacheName { + opt.slicerCacheXML = k.(string) + return false + } + } + return true + }) + return slicerCache +} + +// getSlicers provides a function to get slicers options by given worksheet +// name, slicer part relationship ID and drawing part path. +func (f *File) getSlicers(sheet, rID, drawingXML string) ([]SlicerOptions, error) { + var ( + opts []SlicerOptions + sheetRelationshipsSlicerXML = f.getSheetRelationshipsTargetByID(sheet, rID) + slicerXML = strings.ReplaceAll(sheetRelationshipsSlicerXML, "..", "xl") + slicers, err = f.slicerReader(slicerXML) + ) + if err != nil { + return opts, err + } + for _, slicer := range slicers.Slicer { + opt := SlicerOptions{ + slicerXML: slicerXML, + slicerCacheName: slicer.Cache, + slicerSheetName: sheet, + slicerSheetRID: rID, + drawingXML: drawingXML, + Name: slicer.Name, + Caption: slicer.Caption, + DisplayHeader: slicer.ShowCaption, + } + slicerCache := f.getSlicerCache(slicer.Cache, &opt) + if slicerCache == nil { + return opts, err + } + if err := f.extractTableSlicer(slicerCache, &opt); err != nil { + return opts, err + } + if err := f.extractPivotTableSlicer(slicerCache, &opt); err != nil { + return opts, err + } + if err = f.extractSlicerCellAnchor(drawingXML, &opt); err != nil { + return opts, err + } + opts = append(opts, opt) + } + return opts, err +} + +// extractTableSlicer extract table slicer options from slicer cache. +func (f *File) extractTableSlicer(slicerCache *xlsxSlicerCacheDefinition, opt *SlicerOptions) error { + if slicerCache.ExtLst != nil { + tables, err := f.getTables() + if err != nil { + return err + } + ext := new(xlsxExt) + _ = f.xmlNewDecoder(strings.NewReader(slicerCache.ExtLst.Ext)).Decode(ext) + if ext.URI == ExtURISlicerCacheDefinition { + tableSlicerCache := new(decodeTableSlicerCache) + _ = f.xmlNewDecoder(strings.NewReader(ext.Content)).Decode(tableSlicerCache) + opt.ItemDesc = tableSlicerCache.SortOrder == "descending" + for sheetName, sheetTables := range tables { + for _, table := range sheetTables { + if tableSlicerCache.TableID == table.tID { + opt.TableName = table.Name + opt.TableSheet = sheetName + } + } + } + } + } + return nil +} + +// extractPivotTableSlicer extract pivot table slicer options from slicer cache. +func (f *File) extractPivotTableSlicer(slicerCache *xlsxSlicerCacheDefinition, opt *SlicerOptions) error { + pivotTables, err := f.getPivotTables() + if err != nil { + return err + } + if slicerCache.PivotTables != nil { + for _, pt := range slicerCache.PivotTables.PivotTable { + opt.TableName = pt.Name + for sheetName, sheetPivotTables := range pivotTables { + for _, pivotTable := range sheetPivotTables { + if opt.TableName == pivotTable.Name { + opt.TableSheet = sheetName + } + } + } + } + if slicerCache.Data != nil && slicerCache.Data.Tabular != nil { + opt.ItemDesc = slicerCache.Data.Tabular.SortOrder == "descending" + } + } + return nil +} + +// extractSlicerCellAnchor extract slicer drawing object from two cell anchor by +// giving drawing part path and slicer options. +func (f *File) extractSlicerCellAnchor(drawingXML string, opt *SlicerOptions) error { + var ( + wsDr *xlsxWsDr + deCellAnchor = new(decodeCellAnchor) + deChoice = new(decodeChoice) + err error + ) + if wsDr, _, err = f.drawingParser(drawingXML); err != nil { + return err + } + wsDr.mu.Lock() + defer wsDr.mu.Unlock() + cond := func(ac *xlsxAlternateContent) bool { + if ac != nil { + _ = f.xmlNewDecoder(strings.NewReader(ac.Content)).Decode(&deChoice) + if deChoice.XMLNSSle15 == NameSpaceDrawingMLSlicerX15.Value || deChoice.XMLNSA14 == NameSpaceDrawingMLA14.Value { + if deChoice.GraphicFrame.NvGraphicFramePr.CNvPr.Name == opt.Name { + return true + } + } + } + return false + } + for _, anchor := range wsDr.TwoCellAnchor { + for _, ac := range anchor.AlternateContent { + if cond(ac) { + if anchor.From != nil { + opt.Macro = deChoice.GraphicFrame.Macro + if opt.Cell, err = CoordinatesToCellName(anchor.From.Col+1, anchor.From.Row+1); err != nil { + return err + } + } + return err + } + } + _ = f.xmlNewDecoder(strings.NewReader("" + anchor.GraphicFrame + "")).Decode(&deCellAnchor) + for _, ac := range deCellAnchor.AlternateContent { + if cond(ac) { + if deCellAnchor.From != nil { + opt.Macro = deChoice.GraphicFrame.Macro + if opt.Cell, err = CoordinatesToCellName(deCellAnchor.From.Col+1, deCellAnchor.From.Row+1); err != nil { + return err + } + } + return err + } + } + } + return err +} + +// getAllSlicers provides a function to get all slicers in a workbook. +func (f *File) getAllSlicers() (map[string][]SlicerOptions, error) { + slicers := map[string][]SlicerOptions{} + for _, sheetName := range f.GetSheetList() { + sles, err := f.GetSlicers(sheetName) + e := ErrSheetNotExist{sheetName} + if err != nil && err.Error() != newNotWorksheetError(sheetName).Error() && err.Error() != e.Error() { + return slicers, err + } + slicers[sheetName] = append(slicers[sheetName], sles...) + } + return slicers, nil +} + +// DeleteSlicer provides the method to delete a slicer by a given slicer name. +func (f *File) DeleteSlicer(name string) error { + sles, err := f.getAllSlicers() + if err != nil { + return err + } + for _, slicers := range sles { + for _, slicer := range slicers { + if slicer.Name != name { + continue + } + _ = f.deleteSlicer(slicer) + return f.deleteSlicerCache(sles, slicer) + } + } + return newNoExistSlicerError(name) +} + +// getSlicers provides a function to delete slicer by given slicer options. +func (f *File) deleteSlicer(opts SlicerOptions) error { + slicers, err := f.slicerReader(opts.slicerXML) + if err != nil { + return err + } + for i := 0; i < len(slicers.Slicer); i++ { + if slicers.Slicer[i].Name == opts.Name { + slicers.Slicer = append(slicers.Slicer[:i], slicers.Slicer[i+1:]...) + i-- + } + } + if len(slicers.Slicer) == 0 { + var ( + extLstBytes []byte + ws, err = f.workSheetReader(opts.slicerSheetName) + decodeExtLst = new(decodeExtLst) + ) + if err != nil { + return err + } + if err = f.xmlNewDecoder(strings.NewReader("" + ws.ExtLst.Ext + "")). + Decode(decodeExtLst); err != nil && err != io.EOF { + return err + } + for i, ext := range decodeExtLst.Ext { + if ext.URI == ExtURISlicerListX14 || ext.URI == ExtURISlicerListX15 { + slicerList := new(decodeSlicerList) + _ = f.xmlNewDecoder(strings.NewReader(ext.Content)).Decode(slicerList) + for _, slicer := range slicerList.Slicer { + if slicer.RID == opts.slicerSheetRID { + decodeExtLst.Ext = append(decodeExtLst.Ext[:i], decodeExtLst.Ext[i+1:]...) + extLstBytes, err = xml.Marshal(decodeExtLst) + ws.ExtLst = &xlsxExtLst{Ext: strings.TrimSuffix(strings.TrimPrefix(string(extLstBytes), ""), "")} + f.Pkg.Delete(opts.slicerXML) + _ = f.removeContentTypesPart(ContentTypeSlicer, "/"+opts.slicerXML) + f.deleteSheetRelationships(opts.slicerSheetName, opts.slicerSheetRID) + return err + } + } + } + } + } + output, err := xml.Marshal(slicers) + f.saveFileList(opts.slicerXML, output) + return err +} + +// deleteSlicerCache provides a function to delete the slicer cache by giving +// slicer options if the slicer cache is no longer used. +func (f *File) deleteSlicerCache(sles map[string][]SlicerOptions, opts SlicerOptions) error { + for _, slicers := range sles { + for _, slicer := range slicers { + if slicer.Name != opts.Name && slicer.slicerCacheName == opts.slicerCacheName { + return nil + } + } + } + if err := f.DeleteDefinedName(&DefinedName{Name: opts.slicerCacheName}); err != nil { + return err + } + f.Pkg.Delete(opts.slicerCacheXML) + return f.removeContentTypesPart(ContentTypeSlicerCache, "/"+opts.slicerCacheXML) +} diff --git a/slicer_test.go b/slicer_test.go index da6fa91..5a79a80 100644 --- a/slicer_test.go +++ b/slicer_test.go @@ -5,12 +5,13 @@ import ( "math/rand" "os" "path/filepath" + "strings" "testing" "github.com/stretchr/testify/assert" ) -func TestAddSlicer(t *testing.T) { +func TestSlicer(t *testing.T) { f := NewFile() disable, colName := false, "_!@#$%^&*()-+=|\\/<>" assert.NoError(t, f.SetCellValue("Sheet1", "B1", colName)) @@ -45,8 +46,29 @@ func TestAddSlicer(t *testing.T) { DisplayHeader: &disable, ItemDesc: true, })) + // Test get table slicers + slicers, err := f.GetSlicers("Sheet1") + assert.NoError(t, err) + assert.Equal(t, "Column1", slicers[0].Name) + assert.Equal(t, "E1", slicers[0].Cell) + assert.Equal(t, "Sheet1", slicers[0].TableSheet) + assert.Equal(t, "Table1", slicers[0].TableName) + assert.Equal(t, "Column1", slicers[0].Caption) + assert.Equal(t, "Column1 1", slicers[1].Name) + assert.Equal(t, "I1", slicers[1].Cell) + assert.Equal(t, "Sheet1", slicers[1].TableSheet) + assert.Equal(t, "Table1", slicers[1].TableName) + assert.Equal(t, "Column1", slicers[1].Caption) + assert.Equal(t, colName, slicers[2].Name) + assert.Equal(t, "M1", slicers[2].Cell) + assert.Equal(t, "Sheet1", slicers[2].TableSheet) + assert.Equal(t, "Table1", slicers[2].TableName) + assert.Equal(t, colName, slicers[2].Caption) + assert.Equal(t, "Button1_Click", slicers[2].Macro) + assert.False(t, *slicers[2].DisplayHeader) + assert.True(t, slicers[2].ItemDesc) // Test create two pivot tables in a new worksheet - _, err := f.NewSheet("Sheet2") + _, 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"} @@ -116,6 +138,25 @@ func TestAddSlicer(t *testing.T) { Caption: "Region", ItemDesc: true, })) + // Test get pivot table slicers + slicers, err = f.GetSlicers("Sheet2") + assert.NoError(t, err) + assert.Equal(t, "Month", slicers[0].Name) + assert.Equal(t, "G42", slicers[0].Cell) + assert.Equal(t, "Sheet2", slicers[0].TableSheet) + assert.Equal(t, "PivotTable1", slicers[0].TableName) + assert.Equal(t, "Month", slicers[0].Caption) + assert.Equal(t, "Month 1", slicers[1].Name) + assert.Equal(t, "K42", slicers[1].Cell) + assert.Equal(t, "Sheet2", slicers[1].TableSheet) + assert.Equal(t, "PivotTable1", slicers[1].TableName) + assert.Equal(t, "Month", slicers[1].Caption) + assert.Equal(t, "Region", slicers[2].Name) + assert.Equal(t, "O42", slicers[2].Cell) + assert.Equal(t, "Sheet2", slicers[2].TableSheet) + assert.Equal(t, "PivotTable2", slicers[2].TableName) + assert.Equal(t, "Region", slicers[2].Caption) + assert.True(t, slicers[2].ItemDesc) // 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 @@ -167,6 +208,48 @@ func TestAddSlicer(t *testing.T) { }), "XML syntax error on line 1: invalid UTF-8") assert.NoError(t, f.Close()) + // Test open a workbook and get already exist slicers + f, err = OpenFile(workbookPath) + assert.NoError(t, err) + slicers, err = f.GetSlicers("Sheet1") + assert.NoError(t, err) + assert.Equal(t, "Column1", slicers[0].Name) + assert.Equal(t, "E1", slicers[0].Cell) + assert.Equal(t, "Sheet1", slicers[0].TableSheet) + assert.Equal(t, "Table1", slicers[0].TableName) + assert.Equal(t, "Column1", slicers[0].Caption) + assert.Equal(t, "Column1 1", slicers[1].Name) + assert.Equal(t, "I1", slicers[1].Cell) + assert.Equal(t, "Sheet1", slicers[1].TableSheet) + assert.Equal(t, "Table1", slicers[1].TableName) + assert.Equal(t, "Column1", slicers[1].Caption) + assert.Equal(t, colName, slicers[2].Name) + assert.Equal(t, "M1", slicers[2].Cell) + assert.Equal(t, "Sheet1", slicers[2].TableSheet) + assert.Equal(t, "Table1", slicers[2].TableName) + assert.Equal(t, colName, slicers[2].Caption) + assert.Equal(t, "Button1_Click", slicers[2].Macro) + assert.False(t, *slicers[2].DisplayHeader) + assert.True(t, slicers[2].ItemDesc) + slicers, err = f.GetSlicers("Sheet2") + assert.NoError(t, err) + assert.Equal(t, "Month", slicers[0].Name) + assert.Equal(t, "G42", slicers[0].Cell) + assert.Equal(t, "Sheet2", slicers[0].TableSheet) + assert.Equal(t, "PivotTable1", slicers[0].TableName) + assert.Equal(t, "Month", slicers[0].Caption) + assert.Equal(t, "Month 1", slicers[1].Name) + assert.Equal(t, "K42", slicers[1].Cell) + assert.Equal(t, "Sheet2", slicers[1].TableSheet) + assert.Equal(t, "PivotTable1", slicers[1].TableName) + assert.Equal(t, "Month", slicers[1].Caption) + assert.Equal(t, "Region", slicers[2].Name) + assert.Equal(t, "O42", slicers[2].Cell) + assert.Equal(t, "Sheet2", slicers[2].TableSheet) + assert.Equal(t, "PivotTable2", slicers[2].TableName) + assert.Equal(t, "Region", slicers[2].Caption) + assert.True(t, slicers[2].ItemDesc) + // Test add a pivot table slicer with workbook which contains timeline f, err = OpenFile(workbookPath) assert.NoError(t, err) @@ -274,6 +357,113 @@ func TestAddSlicer(t *testing.T) { Caption: "Column1", }), "XML syntax error on line 1: invalid UTF-8") assert.NoError(t, f.Close()) + + f = NewFile() + // Test get sheet slicers without slicer + slicers, err = f.GetSlicers("Sheet1") + assert.NoError(t, err) + assert.Empty(t, slicers) + // Test get sheet slicers with not exist worksheet name + _, err = f.GetSlicers("SheetN") + assert.EqualError(t, err, "sheet SheetN does not exist") + assert.NoError(t, f.Close()) + + f, err = OpenFile(workbookPath) + assert.NoError(t, err) + // Test get sheet slicers with unsupported charset slicer cache + f.Pkg.Store("xl/slicerCaches/slicerCache1.xml", MacintoshCyrillicCharset) + _, err = f.GetSlicers("Sheet1") + assert.NoError(t, err) + // Test get sheet slicers with unsupported charset slicer + f.Pkg.Store("xl/slicers/slicer1.xml", MacintoshCyrillicCharset) + _, err = f.GetSlicers("Sheet1") + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") + // Test get sheet slicers with invalid worksheet extension list + ws, ok = f.Sheet.Load("xl/worksheets/sheet1.xml") + assert.True(t, ok) + ws.(*xlsxWorksheet).ExtLst.Ext = "<>" + _, err = f.GetSlicers("Sheet1") + assert.Error(t, err) + assert.NoError(t, f.Close()) + + f, err = OpenFile(workbookPath) + assert.NoError(t, err) + // Test get sheet slicers without slicer cache + f.Pkg.Range(func(k, v interface{}) bool { + if strings.Contains(k.(string), "xl/slicerCaches/slicerCache") { + f.Pkg.Delete(k.(string)) + } + return true + }) + slicers, err = f.GetSlicers("Sheet1") + assert.NoError(t, err) + assert.Empty(t, slicers) + assert.NoError(t, f.Close()) + // Test open a workbook and get sheet slicer with invalid cell reference in the drawing part + f, err = OpenFile(workbookPath) + assert.NoError(t, err) + f.Pkg.Store("xl/drawings/drawing1.xml", []byte(fmt.Sprintf(`-1-1`, NameSpaceDrawingMLSpreadSheet.Value, NameSpaceDrawingMLSlicerX15.Value))) + _, err = f.GetSlicers("Sheet1") + assert.Equal(t, newCoordinatesToCellNameError(0, 0), err) + // Test get sheet slicer without slicer shape in the drawing part + f.Drawings.Delete("xl/drawings/drawing1.xml") + f.Pkg.Store("xl/drawings/drawing1.xml", []byte(fmt.Sprintf(``, NameSpaceDrawingMLSpreadSheet.Value))) + _, err = f.GetSlicers("Sheet1") + assert.NoError(t, err) + f.Drawings.Delete("xl/drawings/drawing1.xml") + // Test get sheet slicers with unsupported charset drawing part + f.Pkg.Store("xl/drawings/drawing1.xml", MacintoshCyrillicCharset) + _, err = f.GetSlicers("Sheet1") + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") + // Test get sheet slicers with unsupported charset table + f.Pkg.Store("xl/tables/table1.xml", MacintoshCyrillicCharset) + _, err = f.GetSlicers("Sheet1") + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") + // Test get sheet slicers with unsupported charset pivot table + f.Pkg.Store("xl/pivotTables/pivotTable1.xml", MacintoshCyrillicCharset) + _, err = f.GetSlicers("Sheet2") + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") + assert.NoError(t, f.Close()) + + // Test create a workbook and get sheet slicer with invalid cell reference in the drawing part + f = NewFile() + assert.NoError(t, f.AddTable("Sheet1", &Table{ + Name: "Table1", + Range: "A1:D5", + })) + assert.NoError(t, f.AddSlicer("Sheet1", &SlicerOptions{ + Name: "Column1", + Cell: "E1", + TableSheet: "Sheet1", + TableName: "Table1", + Caption: "Column1", + })) + drawing, ok := f.Drawings.Load("xl/drawings/drawing1.xml") + assert.True(t, ok) + drawing.(*xlsxWsDr).TwoCellAnchor[0].From = &xlsxFrom{Col: -1, Row: -1} + _, err = f.GetSlicers("Sheet1") + assert.Equal(t, newCoordinatesToCellNameError(0, 0), err) + assert.NoError(t, f.Close()) + + // Test open a workbook and delete slicers + f, err = OpenFile(workbookPath) + assert.NoError(t, err) + for _, name := range []string{colName, "Column1 1", "Column1"} { + assert.NoError(t, f.DeleteSlicer(name)) + } + for _, name := range []string{"Month", "Month 1", "Region"} { + assert.NoError(t, f.DeleteSlicer(name)) + } + // Test delete slicer with no exits slicer name + assert.Equal(t, newNoExistSlicerError("x"), f.DeleteSlicer("x")) + assert.NoError(t, f.Close()) + + // Test open a workbook and delete sheet slicer with unsupported charset slicer cache + f, err = OpenFile(workbookPath) + assert.NoError(t, err) + f.Pkg.Store("xl/slicers/slicer1.xml", MacintoshCyrillicCharset) + assert.EqualError(t, f.DeleteSlicer("Column1"), "XML syntax error on line 1: invalid UTF-8") + assert.NoError(t, f.Close()) } func TestAddSheetSlicer(t *testing.T) { @@ -296,36 +486,81 @@ func TestAddSheetTableSlicer(t *testing.T) { func TestSetSlicerCache(t *testing.T) { f := NewFile() f.Pkg.Store("xl/slicerCaches/slicerCache1.xml", MacintoshCyrillicCharset) - _, err := f.setSlicerCache("Sheet1", 1, &SlicerOptions{}, &Table{}, nil) + _, err := f.setSlicerCache(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("Sheet1", 1, &SlicerOptions{}, &Table{}, nil) + _, err = f.setSlicerCache(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("Sheet1", 1, &SlicerOptions{}, &Table{}, nil) + _, err = f.setSlicerCache(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("Sheet1", 1, &SlicerOptions{}, &Table{tID: 1}, nil) + _, err = f.setSlicerCache(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("Sheet1", 1, &SlicerOptions{}, &Table{tID: 1}, nil) + _, err = f.setSlicerCache(1, &SlicerOptions{}, &Table{tID: 1}, nil) assert.NoError(t, err) assert.NoError(t, f.Close()) } +func TestDeleteSlicer(t *testing.T) { + f, slicerXML := NewFile(), "xl/slicers/slicer1.xml" + assert.NoError(t, f.AddTable("Sheet1", &Table{ + Name: "Table1", + Range: "A1:D5", + })) + assert.NoError(t, f.AddSlicer("Sheet1", &SlicerOptions{ + Name: "Column1", + Cell: "E1", + TableSheet: "Sheet1", + TableName: "Table1", + Caption: "Column1", + })) + // Test delete sheet slicers with invalid worksheet extension list + ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml") + assert.True(t, ok) + ws.(*xlsxWorksheet).ExtLst.Ext = "<>" + assert.Error(t, f.deleteSlicer(SlicerOptions{ + slicerXML: slicerXML, + slicerSheetName: "Sheet1", + Name: "Column1", + })) + // Test delete slicer with unsupported charset worksheet + f.Sheet.Delete("xl/worksheets/sheet1.xml") + f.Pkg.Store("xl/worksheets/sheet1.xml", MacintoshCyrillicCharset) + assert.EqualError(t, f.deleteSlicer(SlicerOptions{ + slicerXML: slicerXML, + slicerSheetName: "Sheet1", + Name: "Column1", + }), "XML syntax error on line 1: invalid UTF-8") + // Test delete slicer with unsupported charset slicer + f.Pkg.Store(slicerXML, MacintoshCyrillicCharset) + assert.EqualError(t, f.deleteSlicer(SlicerOptions{slicerXML: slicerXML}), "XML syntax error on line 1: invalid UTF-8") + assert.NoError(t, f.Close()) +} + +func TestDeleteSlicerCache(t *testing.T) { + f := NewFile() + // Test delete slicer cache with unsupported charset workbook + f.WorkBook = nil + f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset) + assert.EqualError(t, f.deleteSlicerCache(nil, SlicerOptions{}), "XML syntax error on line 1: invalid UTF-8") + assert.NoError(t, f.Close()) +} + func TestAddSlicerCache(t *testing.T) { f := NewFile() f.ContentTypes = nil diff --git a/table.go b/table.go index 5ca7894..aec7b26 100644 --- a/table.go +++ b/table.go @@ -173,11 +173,11 @@ func (f *File) DeleteTable(name string) error { if err := checkDefinedName(name); err != nil { return err } - for _, sheet := range f.GetSheetList() { - tables, err := f.GetTables(sheet) - if err != nil { - return err - } + tbls, err := f.getTables() + if err != nil { + return err + } + for sheet, tables := range tbls { for _, table := range tables { if table.Name != name { continue @@ -201,6 +201,20 @@ func (f *File) DeleteTable(name string) error { return newNoExistTableError(name) } +// getTables provides a function to get all tables in a workbook. +func (f *File) getTables() (map[string][]Table, error) { + tables := map[string][]Table{} + for _, sheetName := range f.GetSheetList() { + tbls, err := f.GetTables(sheetName) + e := ErrSheetNotExist{sheetName} + if err != nil && err.Error() != newNotWorksheetError(sheetName).Error() && err.Error() != e.Error() { + return tables, err + } + tables[sheetName] = append(tables[sheetName], tbls...) + } + return tables, nil +} + // countTables provides a function to get table files count storage in the // folder xl/tables. func (f *File) countTables() int { diff --git a/xmlDecodeDrawing.go b/xmlDecodeDrawing.go index 5c900fc..a59e7c4 100644 --- a/xmlDecodeDrawing.go +++ b/xmlDecodeDrawing.go @@ -24,7 +24,7 @@ type decodeCellAnchor struct { Sp *decodeSp `xml:"sp"` Pic *decodePic `xml:"pic"` ClientData *decodeClientData `xml:"clientData"` - AlternateContent []*xlsxAlternateContent `xml:"mc:AlternateContent"` + AlternateContent []*xlsxAlternateContent `xml:"AlternateContent"` Content string `xml:",innerxml"` } @@ -46,6 +46,28 @@ type decodeCellAnchorPos struct { ClientData *xlsxInnerXML `xml:"clientData"` } +// decodeChoice defines the structure used to deserialize the mc:Choice element. +type decodeChoice struct { + XMLName xml.Name `xml:"Choice"` + XMLNSA14 string `xml:"a14,attr"` + XMLNSSle15 string `xml:"sle15,attr"` + Requires string `xml:"Requires,attr"` + GraphicFrame decodeGraphicFrame `xml:"graphicFrame"` +} + +// decodeGraphicFrame defines the structure used to deserialize the +// xdr:graphicFrame element. +type decodeGraphicFrame struct { + Macro string `xml:"macro,attr"` + NvGraphicFramePr decodeNvGraphicFramePr `xml:"nvGraphicFramePr"` +} + +// decodeNvGraphicFramePr defines the structure used to deserialize the +// xdr:nvGraphicFramePr element. +type decodeNvGraphicFramePr struct { + CNvPr decodeCNvPr `xml:"cNvPr"` +} + // decodeSp defines the structure used to deserialize the sp element. type decodeSp struct { Macro string `xml:"macro,attr,omitempty"` @@ -56,7 +78,7 @@ type decodeSp struct { SpPr *decodeSpPr `xml:"spPr"` } -// decodeSp (Non-Visual Properties for a Shape) directly maps the nvSpPr +// decodeNvSpPr (Non-Visual Properties for a Shape) directly maps the nvSpPr // element. This element specifies all non-visual properties for a shape. This // element is a container for the non-visual identification properties, shape // properties and application properties that are to be associated with a diff --git a/xmlSlicers.go b/xmlSlicers.go index 6e68897..5c20923 100644 --- a/xmlSlicers.go +++ b/xmlSlicers.go @@ -149,9 +149,10 @@ type xlsxX15SlicerCaches struct { // decodeTableSlicerCache defines the structure used to parse the // x15:tableSlicerCache element of the table slicer cache. type decodeTableSlicerCache struct { - XMLName xml.Name `xml:"tableSlicerCache"` - TableID int `xml:"tableId,attr"` - Column int `xml:"column,attr"` + XMLName xml.Name `xml:"tableSlicerCache"` + TableID int `xml:"tableId,attr"` + Column int `xml:"column,attr"` + SortOrder string `xml:"sortOrder,attr"` } // decodeSlicerList defines the structure used to parse the x14:slicerList