Skip to content

Commit 0861faf

Browse files
committed
Add new exported function DeletePivotTable
- Support adding pivot table by specific table name - Update unit tests
1 parent 1c7c417 commit 0861faf

File tree

4 files changed

+266
-22
lines changed

4 files changed

+266
-22
lines changed

pivotTable.go

+127-20
Original file line numberDiff line numberDiff line change
@@ -157,20 +157,18 @@ func (f *File) AddPivotTable(opts *PivotTableOptions) error {
157157
sheetRelationshipsPivotTableXML := "../pivotTables/pivotTable" + strconv.Itoa(pivotTableID) + ".xml"
158158
pivotTableXML := strings.ReplaceAll(sheetRelationshipsPivotTableXML, "..", "xl")
159159
pivotCacheXML := "xl/pivotCache/pivotCacheDefinition" + strconv.Itoa(pivotCacheID) + ".xml"
160-
err = f.addPivotCache(pivotCacheXML, opts)
161-
if err != nil {
160+
if err = f.addPivotCache(pivotCacheXML, opts); err != nil {
162161
return err
163162
}
164163

165164
// workbook pivot cache
166-
workBookPivotCacheRID := f.addRels(f.getWorkbookRelsPath(), SourceRelationshipPivotCache, fmt.Sprintf("/xl/pivotCache/pivotCacheDefinition%d.xml", pivotCacheID), "")
165+
workBookPivotCacheRID := f.addRels(f.getWorkbookRelsPath(), SourceRelationshipPivotCache, strings.TrimPrefix(pivotCacheXML, "xl/"), "")
167166
cacheID := f.addWorkbookPivotCache(workBookPivotCacheRID)
168167

169168
pivotCacheRels := "xl/pivotTables/_rels/pivotTable" + strconv.Itoa(pivotTableID) + ".xml.rels"
170169
// rId not used
171170
_ = f.addRels(pivotCacheRels, SourceRelationshipPivotCache, fmt.Sprintf("../pivotCache/pivotCacheDefinition%d.xml", pivotCacheID), "")
172-
err = f.addPivotTable(cacheID, pivotTableID, pivotTableXML, opts)
173-
if err != nil {
171+
if err = f.addPivotTable(cacheID, pivotTableID, pivotTableXML, opts); err != nil {
174172
return err
175173
}
176174
pivotTableSheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(pivotTableSheetPath, "xl/worksheets/") + ".rels"
@@ -195,11 +193,14 @@ func (f *File) parseFormatPivotTableSet(opts *PivotTableOptions) (*xlsxWorksheet
195193
return nil, "", ErrNameLength
196194
}
197195
opts.pivotTableSheetName = pivotTableSheetName
198-
dataRange := f.getDefinedNameRefTo(opts.DataRange, pivotTableSheetName)
199-
if dataRange == "" {
200-
dataRange = opts.DataRange
196+
_, dataRangeRef, err := f.getPivotTableDataRange(pivotTableSheetName, opts.DataRange, opts.DataRange)
197+
if err != nil {
198+
return nil, "", err
201199
}
202-
dataSheetName, _, err := f.adjustRange(dataRange)
200+
if dataRangeRef == "" {
201+
dataRangeRef = opts.DataRange
202+
}
203+
dataSheetName, _, err := f.adjustRange(dataRangeRef)
203204
if err != nil {
204205
return nil, "", newPivotTableDataRangeError(err.Error())
205206
}
@@ -248,11 +249,17 @@ func (f *File) adjustRange(rangeStr string) (string, []int, error) {
248249
// fields.
249250
func (f *File) getTableFieldsOrder(sheetName, dataRange string) ([]string, error) {
250251
var order []string
251-
ref := f.getDefinedNameRefTo(dataRange, sheetName)
252-
if ref == "" {
253-
ref = dataRange
252+
if dataRange == "" {
253+
return order, newPivotTableDataRangeError(ErrParameterRequired.Error())
254254
}
255-
dataSheet, coordinates, err := f.adjustRange(ref)
255+
_, dataRangeRef, err := f.getPivotTableDataRange(sheetName, dataRange, dataRange)
256+
if err != nil {
257+
return order, err
258+
}
259+
if dataRangeRef == "" {
260+
dataRangeRef = dataRange
261+
}
262+
dataSheet, coordinates, err := f.adjustRange(dataRangeRef)
256263
if err != nil {
257264
return order, newPivotTableDataRangeError(err.Error())
258265
}
@@ -271,17 +278,20 @@ func (f *File) getTableFieldsOrder(sheetName, dataRange string) ([]string, error
271278
func (f *File) addPivotCache(pivotCacheXML string, opts *PivotTableOptions) error {
272279
// validate data range
273280
definedNameRef := true
274-
dataRange := f.getDefinedNameRefTo(opts.DataRange, opts.pivotTableSheetName)
275-
if dataRange == "" {
281+
_, dataRangeRef, err := f.getPivotTableDataRange(opts.pivotTableSheetName, opts.DataRange, opts.DataRange)
282+
if err != nil {
283+
return err
284+
}
285+
if dataRangeRef == "" {
276286
definedNameRef = false
277-
dataRange = opts.DataRange
287+
dataRangeRef = opts.DataRange
278288
}
279-
dataSheet, coordinates, err := f.adjustRange(dataRange)
289+
dataSheet, coordinates, err := f.adjustRange(dataRangeRef)
280290
if err != nil {
281291
return newPivotTableDataRangeError(err.Error())
282292
}
283293
// data range has been checked
284-
order, _ := f.getTableFieldsOrder(opts.pivotTableSheetName, opts.DataRange)
294+
order, _ := f.getTableFieldsOrder(opts.pivotTableSheetName, dataRangeRef)
285295
hCell, _ := CoordinatesToCellName(coordinates[0], coordinates[1])
286296
vCell, _ := CoordinatesToCellName(coordinates[2], coordinates[3])
287297
pc := xlsxPivotCacheDefinition{
@@ -751,6 +761,32 @@ func (f *File) GetPivotTables(sheet string) ([]PivotTableOptions, error) {
751761
return pivotTables, nil
752762
}
753763

764+
// getPivotTableDataRange returns pivot table data range name and reference from
765+
// cell reference, table name or defined name.
766+
func (f *File) getPivotTableDataRange(sheet, ref, name string) (string, string, error) {
767+
dataRange := fmt.Sprintf("%s!%s", sheet, ref)
768+
dataRangeRef, isTable := dataRange, false
769+
if name != "" {
770+
dataRange = name
771+
for _, sheetName := range f.GetSheetList() {
772+
tables, err := f.GetTables(sheetName)
773+
e := ErrSheetNotExist{sheetName}
774+
if err != nil && err.Error() != newNotWorksheetError(sheetName).Error() && err.Error() != e.Error() {
775+
return dataRange, dataRangeRef, err
776+
}
777+
for _, table := range tables {
778+
if table.Name == name {
779+
dataRangeRef, isTable = fmt.Sprintf("%s!%s", sheetName, table.Range), true
780+
}
781+
}
782+
}
783+
if !isTable {
784+
dataRangeRef = f.getDefinedNameRefTo(name, sheet)
785+
}
786+
}
787+
return dataRange, dataRangeRef, nil
788+
}
789+
754790
// getPivotTable provides a function to get a pivot table definition by given
755791
// worksheet name, pivot table XML path and pivot cache relationship XML path.
756792
func (f *File) getPivotTable(sheet, pivotTableXML, pivotCacheRels string) (PivotTableOptions, error) {
@@ -774,7 +810,10 @@ func (f *File) getPivotTable(sheet, pivotTableXML, pivotCacheRels string) (Pivot
774810
if err != nil {
775811
return opts, err
776812
}
777-
dataRange := fmt.Sprintf("%s!%s", pc.CacheSource.WorksheetSource.Sheet, pc.CacheSource.WorksheetSource.Ref)
813+
dataRange, dataRangeRef, err := f.getPivotTableDataRange(sheet, pc.CacheSource.WorksheetSource.Ref, pc.CacheSource.WorksheetSource.Name)
814+
if err != nil {
815+
return opts, err
816+
}
778817
opts = PivotTableOptions{
779818
pivotTableXML: pivotTableXML,
780819
pivotCacheXML: pivotCacheXML,
@@ -799,7 +838,7 @@ func (f *File) getPivotTable(sheet, pivotTableXML, pivotCacheRels string) (Pivot
799838
opts.ShowLastColumn = si.ShowLastColumn
800839
opts.PivotTableStyleName = si.Name
801840
}
802-
order, _ := f.getTableFieldsOrder(pt.Name, dataRange)
841+
order, err := f.getTableFieldsOrder(pt.Name, dataRangeRef)
803842
f.extractPivotTableFields(order, pt, &opts)
804843
return opts, err
805844
}
@@ -906,3 +945,71 @@ func (f *File) genPivotCacheDefinitionID() int {
906945
})
907946
return ID + 1
908947
}
948+
949+
// deleteWorkbookPivotCache remove workbook pivot cache and pivot cache
950+
// relationships.
951+
func (f *File) deleteWorkbookPivotCache(opt PivotTableOptions) error {
952+
rID, err := f.deleteWorkbookRels(SourceRelationshipPivotCache, strings.TrimPrefix(strings.TrimPrefix(opt.pivotCacheXML, "/"), "xl/"))
953+
if err != nil {
954+
return err
955+
}
956+
wb, err := f.workbookReader()
957+
if err != nil {
958+
return err
959+
}
960+
if wb.PivotCaches != nil {
961+
for i, pivotCache := range wb.PivotCaches.PivotCache {
962+
if pivotCache.RID == rID {
963+
wb.PivotCaches.PivotCache = append(wb.PivotCaches.PivotCache[:i], wb.PivotCaches.PivotCache[i+1:]...)
964+
}
965+
}
966+
if len(wb.PivotCaches.PivotCache) == 0 {
967+
wb.PivotCaches = nil
968+
}
969+
}
970+
return err
971+
}
972+
973+
// DeletePivotTable delete a pivot table by giving the worksheet name and pivot
974+
// table name. Note that this function does not clean cell values in the pivot
975+
// table range.
976+
func (f *File) DeletePivotTable(sheet, name string) error {
977+
sheetXML, ok := f.getSheetXMLPath(sheet)
978+
if !ok {
979+
return ErrSheetNotExist{sheet}
980+
}
981+
rels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetXML, "xl/worksheets/") + ".rels"
982+
sheetRels, err := f.relsReader(rels)
983+
if err != nil {
984+
return err
985+
}
986+
if sheetRels == nil {
987+
sheetRels = &xlsxRelationships{}
988+
}
989+
opts, err := f.GetPivotTables(sheet)
990+
if err != nil {
991+
return err
992+
}
993+
pivotTableCaches := map[string]int{}
994+
for _, sheetName := range f.GetSheetList() {
995+
sheetPivotTables, _ := f.GetPivotTables(sheetName)
996+
for _, sheetPivotTable := range sheetPivotTables {
997+
pivotTableCaches[sheetPivotTable.pivotCacheXML]++
998+
}
999+
}
1000+
for _, v := range sheetRels.Relationships {
1001+
for _, opt := range opts {
1002+
if v.Type == SourceRelationshipPivotTable {
1003+
pivotTableXML := strings.ReplaceAll(v.Target, "..", "xl")
1004+
if opt.Name == name && opt.pivotTableXML == pivotTableXML {
1005+
if pivotTableCaches[opt.pivotCacheXML] == 1 {
1006+
err = f.deleteWorkbookPivotCache(opt)
1007+
}
1008+
f.deleteSheetRelationships(sheet, v.ID)
1009+
return err
1010+
}
1011+
}
1012+
}
1013+
}
1014+
return newNoExistTableError(name)
1015+
}

pivotTable_test.go

+118-1
Original file line numberDiff line numberDiff line change
@@ -246,13 +246,25 @@ func TestPivotTable(t *testing.T) {
246246
Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
247247
Data: []PivotTableField{{Data: "Sales", Subtotal: "-", Name: strings.Repeat("s", MaxFieldLength+1)}},
248248
}))
249+
// Test delete pivot table
250+
pivotTables, err = f.GetPivotTables("Sheet1")
251+
assert.Len(t, pivotTables, 7)
252+
assert.NoError(t, err)
253+
assert.NoError(t, f.DeletePivotTable("Sheet1", "PivotTable1"))
254+
pivotTables, err = f.GetPivotTables("Sheet1")
255+
assert.Len(t, pivotTables, 6)
256+
assert.NoError(t, err)
249257

250258
// Test add pivot table with invalid sheet name
251259
assert.EqualError(t, f.AddPivotTable(&PivotTableOptions{
252260
DataRange: "Sheet:1!A1:E31",
253261
PivotTableRange: "Sheet:1!G2:M34",
254262
Rows: []PivotTableField{{Data: "Year"}},
255263
}), ErrSheetNameInvalid.Error())
264+
// Test delete pivot table with not exists worksheet
265+
assert.EqualError(t, f.DeletePivotTable("SheetN", "PivotTable1"), "sheet SheetN does not exist")
266+
// Test delete pivot table with not exists pivot table name
267+
assert.EqualError(t, f.DeletePivotTable("Sheet1", "PivotTableN"), "table PivotTableN does not exist")
256268
// Test adjust range with invalid range
257269
_, _, err = f.adjustRange("")
258270
assert.EqualError(t, err, ErrParameterRequired.Error())
@@ -263,7 +275,7 @@ func TestPivotTable(t *testing.T) {
263275
_, err = f.getTableFieldsOrder("", "")
264276
assert.EqualError(t, err, `parameter 'DataRange' parsing error: parameter is required`)
265277
// Test add pivot cache with empty data range
266-
assert.EqualError(t, f.addPivotCache("", &PivotTableOptions{}), "parameter 'DataRange' parsing error: parameter is required")
278+
assert.EqualError(t, f.addPivotCache("", &PivotTableOptions{}), "parameter 'DataRange' parsing error: parameter is invalid")
267279
// Test add pivot cache with invalid data range
268280
assert.EqualError(t, f.addPivotCache("", &PivotTableOptions{
269281
DataRange: "A1:E31",
@@ -334,6 +346,89 @@ func TestPivotTable(t *testing.T) {
334346
assert.NoError(t, f.Close())
335347
}
336348

349+
func TestPivotTableDataRange(t *testing.T) {
350+
f := NewFile()
351+
// Create table in a worksheet
352+
assert.NoError(t, f.AddTable("Sheet1", &Table{
353+
Name: "Table1",
354+
Range: "A1:D5",
355+
}))
356+
for row := 2; row < 6; row++ {
357+
assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("A%d", row), rand.Intn(10)))
358+
assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("B%d", row), rand.Intn(10)))
359+
assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("C%d", row), rand.Intn(10)))
360+
assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("D%d", row), rand.Intn(10)))
361+
}
362+
// Test add pivot table with table data range
363+
opts := PivotTableOptions{
364+
DataRange: "Table1",
365+
PivotTableRange: "Sheet1!G2:K7",
366+
Rows: []PivotTableField{{Data: "Column1"}},
367+
Columns: []PivotTableField{{Data: "Column2"}},
368+
RowGrandTotals: true,
369+
ColGrandTotals: true,
370+
ShowDrill: true,
371+
ShowRowHeaders: true,
372+
ShowColHeaders: true,
373+
ShowLastColumn: true,
374+
ShowError: true,
375+
PivotTableStyleName: "PivotStyleLight16",
376+
}
377+
assert.NoError(t, f.AddPivotTable(&opts))
378+
assert.NoError(t, f.DeletePivotTable("Sheet1", "PivotTable1"))
379+
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddPivotTable2.xlsx")))
380+
assert.NoError(t, f.Close())
381+
382+
assert.NoError(t, f.AddPivotTable(&opts))
383+
384+
// Test delete pivot table with unsupported table relationships charset
385+
f.Pkg.Store("xl/tables/table1.xml", MacintoshCyrillicCharset)
386+
assert.EqualError(t, f.DeletePivotTable("Sheet1", "PivotTable1"), "XML syntax error on line 1: invalid UTF-8")
387+
388+
// Test delete pivot table with unsupported worksheet relationships charset
389+
f.Relationships.Delete("xl/worksheets/_rels/sheet1.xml.rels")
390+
f.Pkg.Store("xl/worksheets/_rels/sheet1.xml.rels", MacintoshCyrillicCharset)
391+
assert.EqualError(t, f.DeletePivotTable("Sheet1", "PivotTable1"), "XML syntax error on line 1: invalid UTF-8")
392+
393+
// Test delete pivot table without worksheet relationships
394+
f.Relationships.Delete("xl/worksheets/_rels/sheet1.xml.rels")
395+
f.Pkg.Delete("xl/worksheets/_rels/sheet1.xml.rels")
396+
assert.EqualError(t, f.DeletePivotTable("Sheet1", "PivotTable1"), "table PivotTable1 does not exist")
397+
}
398+
399+
func TestParseFormatPivotTableSet(t *testing.T) {
400+
f := NewFile()
401+
// Create table in a worksheet
402+
assert.NoError(t, f.AddTable("Sheet1", &Table{
403+
Name: "Table1",
404+
Range: "A1:D5",
405+
}))
406+
// Test parse format pivot table options with unsupported table relationships charset
407+
f.Pkg.Store("xl/tables/table1.xml", MacintoshCyrillicCharset)
408+
_, _, err := f.parseFormatPivotTableSet(&PivotTableOptions{
409+
DataRange: "Table1",
410+
PivotTableRange: "Sheet1!G2:K7",
411+
Rows: []PivotTableField{{Data: "Column1"}},
412+
})
413+
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
414+
}
415+
416+
func TestAddPivotCache(t *testing.T) {
417+
f := NewFile()
418+
// Create table in a worksheet
419+
assert.NoError(t, f.AddTable("Sheet1", &Table{
420+
Name: "Table1",
421+
Range: "A1:D5",
422+
}))
423+
// Test add pivot table cache with unsupported table relationships charset
424+
f.Pkg.Store("xl/tables/table1.xml", MacintoshCyrillicCharset)
425+
assert.EqualError(t, f.addPivotCache("xl/pivotCache/pivotCacheDefinition1.xml", &PivotTableOptions{
426+
DataRange: "Table1",
427+
PivotTableRange: "Sheet1!G2:K7",
428+
Rows: []PivotTableField{{Data: "Column1"}},
429+
}), "XML syntax error on line 1: invalid UTF-8")
430+
}
431+
337432
func TestAddPivotRowFields(t *testing.T) {
338433
f := NewFile()
339434
// Test invalid data range
@@ -372,6 +467,15 @@ func TestGetPivotFieldsOrder(t *testing.T) {
372467
// Test get table fields order with not exist worksheet
373468
_, err := f.getTableFieldsOrder("", "SheetN!A1:E31")
374469
assert.EqualError(t, err, "sheet SheetN does not exist")
470+
// Create table in a worksheet
471+
assert.NoError(t, f.AddTable("Sheet1", &Table{
472+
Name: "Table1",
473+
Range: "A1:D5",
474+
}))
475+
// Test get table fields order with unsupported table relationships charset
476+
f.Pkg.Store("xl/tables/table1.xml", MacintoshCyrillicCharset)
477+
_, err = f.getTableFieldsOrder("Sheet1", "Table")
478+
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
375479
}
376480

377481
func TestGetPivotTableFieldName(t *testing.T) {
@@ -392,3 +496,16 @@ func TestGenPivotCacheDefinitionID(t *testing.T) {
392496
assert.Equal(t, 1, f.genPivotCacheDefinitionID())
393497
assert.NoError(t, f.Close())
394498
}
499+
500+
func TestDeleteWorkbookPivotCache(t *testing.T) {
501+
f := NewFile()
502+
// Test delete workbook pivot table cache with unsupported workbook charset
503+
f.WorkBook = nil
504+
f.Pkg.Store("xl/workbook.xml", MacintoshCyrillicCharset)
505+
assert.EqualError(t, f.deleteWorkbookPivotCache(PivotTableOptions{pivotCacheXML: "pivotCache/pivotCacheDefinition1.xml"}), "XML syntax error on line 1: invalid UTF-8")
506+
507+
// Test delete workbook pivot table cache with unsupported workbook relationships charset
508+
f.Relationships.Delete("xl/_rels/workbook.xml.rels")
509+
f.Pkg.Store("xl/_rels/workbook.xml.rels", MacintoshCyrillicCharset)
510+
assert.EqualError(t, f.deleteWorkbookPivotCache(PivotTableOptions{pivotCacheXML: "pivotCache/pivotCacheDefinition1.xml"}), "XML syntax error on line 1: invalid UTF-8")
511+
}

sheet.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -1864,7 +1864,7 @@ func (f *File) RemovePageBreak(sheet, cell string) error {
18641864
}
18651865

18661866
// relsReader provides a function to get the pointer to the structure
1867-
// after deserialization of xl/worksheets/_rels/sheet%d.xml.rels.
1867+
// after deserialization of relationships parts.
18681868
func (f *File) relsReader(path string) (*xlsxRelationships, error) {
18691869
rels, _ := f.Relationships.Load(path)
18701870
if rels == nil {

0 commit comments

Comments
 (0)