Add new exported function `DeletePivotTable`

- Support adding pivot table by specific table name
- Update unit tests
This commit is contained in:
xuri 2023-10-03 00:59:31 +08:00
parent 1c7c417c70
commit 0861faf2f2
No known key found for this signature in database
GPG Key ID: BA5E5BB1C948EDF7
4 changed files with 267 additions and 23 deletions

View File

@ -157,20 +157,18 @@ func (f *File) AddPivotTable(opts *PivotTableOptions) error {
sheetRelationshipsPivotTableXML := "../pivotTables/pivotTable" + strconv.Itoa(pivotTableID) + ".xml"
pivotTableXML := strings.ReplaceAll(sheetRelationshipsPivotTableXML, "..", "xl")
pivotCacheXML := "xl/pivotCache/pivotCacheDefinition" + strconv.Itoa(pivotCacheID) + ".xml"
err = f.addPivotCache(pivotCacheXML, opts)
if err != nil {
if err = f.addPivotCache(pivotCacheXML, opts); err != nil {
return err
}
// workbook pivot cache
workBookPivotCacheRID := f.addRels(f.getWorkbookRelsPath(), SourceRelationshipPivotCache, fmt.Sprintf("/xl/pivotCache/pivotCacheDefinition%d.xml", pivotCacheID), "")
workBookPivotCacheRID := f.addRels(f.getWorkbookRelsPath(), SourceRelationshipPivotCache, strings.TrimPrefix(pivotCacheXML, "xl/"), "")
cacheID := f.addWorkbookPivotCache(workBookPivotCacheRID)
pivotCacheRels := "xl/pivotTables/_rels/pivotTable" + strconv.Itoa(pivotTableID) + ".xml.rels"
// rId not used
_ = f.addRels(pivotCacheRels, SourceRelationshipPivotCache, fmt.Sprintf("../pivotCache/pivotCacheDefinition%d.xml", pivotCacheID), "")
err = f.addPivotTable(cacheID, pivotTableID, pivotTableXML, opts)
if err != nil {
if err = f.addPivotTable(cacheID, pivotTableID, pivotTableXML, opts); err != nil {
return err
}
pivotTableSheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(pivotTableSheetPath, "xl/worksheets/") + ".rels"
@ -195,11 +193,14 @@ func (f *File) parseFormatPivotTableSet(opts *PivotTableOptions) (*xlsxWorksheet
return nil, "", ErrNameLength
}
opts.pivotTableSheetName = pivotTableSheetName
dataRange := f.getDefinedNameRefTo(opts.DataRange, pivotTableSheetName)
if dataRange == "" {
dataRange = opts.DataRange
_, dataRangeRef, err := f.getPivotTableDataRange(pivotTableSheetName, opts.DataRange, opts.DataRange)
if err != nil {
return nil, "", err
}
dataSheetName, _, err := f.adjustRange(dataRange)
if dataRangeRef == "" {
dataRangeRef = opts.DataRange
}
dataSheetName, _, err := f.adjustRange(dataRangeRef)
if err != nil {
return nil, "", newPivotTableDataRangeError(err.Error())
}
@ -248,11 +249,17 @@ func (f *File) adjustRange(rangeStr string) (string, []int, error) {
// fields.
func (f *File) getTableFieldsOrder(sheetName, dataRange string) ([]string, error) {
var order []string
ref := f.getDefinedNameRefTo(dataRange, sheetName)
if ref == "" {
ref = dataRange
if dataRange == "" {
return order, newPivotTableDataRangeError(ErrParameterRequired.Error())
}
dataSheet, coordinates, err := f.adjustRange(ref)
_, dataRangeRef, err := f.getPivotTableDataRange(sheetName, dataRange, dataRange)
if err != nil {
return order, err
}
if dataRangeRef == "" {
dataRangeRef = dataRange
}
dataSheet, coordinates, err := f.adjustRange(dataRangeRef)
if err != nil {
return order, newPivotTableDataRangeError(err.Error())
}
@ -271,17 +278,20 @@ func (f *File) getTableFieldsOrder(sheetName, dataRange string) ([]string, error
func (f *File) addPivotCache(pivotCacheXML string, opts *PivotTableOptions) error {
// validate data range
definedNameRef := true
dataRange := f.getDefinedNameRefTo(opts.DataRange, opts.pivotTableSheetName)
if dataRange == "" {
definedNameRef = false
dataRange = opts.DataRange
_, dataRangeRef, err := f.getPivotTableDataRange(opts.pivotTableSheetName, opts.DataRange, opts.DataRange)
if err != nil {
return err
}
dataSheet, coordinates, err := f.adjustRange(dataRange)
if dataRangeRef == "" {
definedNameRef = false
dataRangeRef = opts.DataRange
}
dataSheet, coordinates, err := f.adjustRange(dataRangeRef)
if err != nil {
return newPivotTableDataRangeError(err.Error())
}
// data range has been checked
order, _ := f.getTableFieldsOrder(opts.pivotTableSheetName, opts.DataRange)
order, _ := f.getTableFieldsOrder(opts.pivotTableSheetName, dataRangeRef)
hCell, _ := CoordinatesToCellName(coordinates[0], coordinates[1])
vCell, _ := CoordinatesToCellName(coordinates[2], coordinates[3])
pc := xlsxPivotCacheDefinition{
@ -751,6 +761,32 @@ func (f *File) GetPivotTables(sheet string) ([]PivotTableOptions, error) {
return pivotTables, nil
}
// getPivotTableDataRange returns pivot table data range name and reference from
// cell reference, table name or defined name.
func (f *File) getPivotTableDataRange(sheet, ref, name string) (string, string, error) {
dataRange := fmt.Sprintf("%s!%s", sheet, ref)
dataRangeRef, isTable := dataRange, false
if name != "" {
dataRange = name
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 dataRange, dataRangeRef, err
}
for _, table := range tables {
if table.Name == name {
dataRangeRef, isTable = fmt.Sprintf("%s!%s", sheetName, table.Range), true
}
}
}
if !isTable {
dataRangeRef = f.getDefinedNameRefTo(name, sheet)
}
}
return dataRange, dataRangeRef, nil
}
// getPivotTable provides a function to get a pivot table definition by given
// worksheet name, pivot table XML path and pivot cache relationship XML path.
func (f *File) getPivotTable(sheet, pivotTableXML, pivotCacheRels string) (PivotTableOptions, error) {
@ -774,7 +810,10 @@ func (f *File) getPivotTable(sheet, pivotTableXML, pivotCacheRels string) (Pivot
if err != nil {
return opts, err
}
dataRange := fmt.Sprintf("%s!%s", pc.CacheSource.WorksheetSource.Sheet, pc.CacheSource.WorksheetSource.Ref)
dataRange, dataRangeRef, err := f.getPivotTableDataRange(sheet, pc.CacheSource.WorksheetSource.Ref, pc.CacheSource.WorksheetSource.Name)
if err != nil {
return opts, err
}
opts = PivotTableOptions{
pivotTableXML: pivotTableXML,
pivotCacheXML: pivotCacheXML,
@ -799,7 +838,7 @@ func (f *File) getPivotTable(sheet, pivotTableXML, pivotCacheRels string) (Pivot
opts.ShowLastColumn = si.ShowLastColumn
opts.PivotTableStyleName = si.Name
}
order, _ := f.getTableFieldsOrder(pt.Name, dataRange)
order, err := f.getTableFieldsOrder(pt.Name, dataRangeRef)
f.extractPivotTableFields(order, pt, &opts)
return opts, err
}
@ -906,3 +945,71 @@ func (f *File) genPivotCacheDefinitionID() int {
})
return ID + 1
}
// deleteWorkbookPivotCache remove workbook pivot cache and pivot cache
// relationships.
func (f *File) deleteWorkbookPivotCache(opt PivotTableOptions) error {
rID, err := f.deleteWorkbookRels(SourceRelationshipPivotCache, strings.TrimPrefix(strings.TrimPrefix(opt.pivotCacheXML, "/"), "xl/"))
if err != nil {
return err
}
wb, err := f.workbookReader()
if err != nil {
return err
}
if wb.PivotCaches != nil {
for i, pivotCache := range wb.PivotCaches.PivotCache {
if pivotCache.RID == rID {
wb.PivotCaches.PivotCache = append(wb.PivotCaches.PivotCache[:i], wb.PivotCaches.PivotCache[i+1:]...)
}
}
if len(wb.PivotCaches.PivotCache) == 0 {
wb.PivotCaches = nil
}
}
return err
}
// DeletePivotTable delete a pivot table by giving the worksheet name and pivot
// table name. Note that this function does not clean cell values in the pivot
// table range.
func (f *File) DeletePivotTable(sheet, name string) error {
sheetXML, ok := f.getSheetXMLPath(sheet)
if !ok {
return ErrSheetNotExist{sheet}
}
rels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetXML, "xl/worksheets/") + ".rels"
sheetRels, err := f.relsReader(rels)
if err != nil {
return err
}
if sheetRels == nil {
sheetRels = &xlsxRelationships{}
}
opts, err := f.GetPivotTables(sheet)
if err != nil {
return err
}
pivotTableCaches := map[string]int{}
for _, sheetName := range f.GetSheetList() {
sheetPivotTables, _ := f.GetPivotTables(sheetName)
for _, sheetPivotTable := range sheetPivotTables {
pivotTableCaches[sheetPivotTable.pivotCacheXML]++
}
}
for _, v := range sheetRels.Relationships {
for _, opt := range opts {
if v.Type == SourceRelationshipPivotTable {
pivotTableXML := strings.ReplaceAll(v.Target, "..", "xl")
if opt.Name == name && opt.pivotTableXML == pivotTableXML {
if pivotTableCaches[opt.pivotCacheXML] == 1 {
err = f.deleteWorkbookPivotCache(opt)
}
f.deleteSheetRelationships(sheet, v.ID)
return err
}
}
}
}
return newNoExistTableError(name)
}

View File

@ -246,6 +246,14 @@ func TestPivotTable(t *testing.T) {
Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
Data: []PivotTableField{{Data: "Sales", Subtotal: "-", Name: strings.Repeat("s", MaxFieldLength+1)}},
}))
// Test delete pivot table
pivotTables, err = f.GetPivotTables("Sheet1")
assert.Len(t, pivotTables, 7)
assert.NoError(t, err)
assert.NoError(t, f.DeletePivotTable("Sheet1", "PivotTable1"))
pivotTables, err = f.GetPivotTables("Sheet1")
assert.Len(t, pivotTables, 6)
assert.NoError(t, err)
// Test add pivot table with invalid sheet name
assert.EqualError(t, f.AddPivotTable(&PivotTableOptions{
@ -253,6 +261,10 @@ func TestPivotTable(t *testing.T) {
PivotTableRange: "Sheet:1!G2:M34",
Rows: []PivotTableField{{Data: "Year"}},
}), ErrSheetNameInvalid.Error())
// Test delete pivot table with not exists worksheet
assert.EqualError(t, f.DeletePivotTable("SheetN", "PivotTable1"), "sheet SheetN does not exist")
// Test delete pivot table with not exists pivot table name
assert.EqualError(t, f.DeletePivotTable("Sheet1", "PivotTableN"), "table PivotTableN does not exist")
// Test adjust range with invalid range
_, _, err = f.adjustRange("")
assert.EqualError(t, err, ErrParameterRequired.Error())
@ -263,7 +275,7 @@ func TestPivotTable(t *testing.T) {
_, err = f.getTableFieldsOrder("", "")
assert.EqualError(t, err, `parameter 'DataRange' parsing error: parameter is required`)
// Test add pivot cache with empty data range
assert.EqualError(t, f.addPivotCache("", &PivotTableOptions{}), "parameter 'DataRange' parsing error: parameter is required")
assert.EqualError(t, f.addPivotCache("", &PivotTableOptions{}), "parameter 'DataRange' parsing error: parameter is invalid")
// Test add pivot cache with invalid data range
assert.EqualError(t, f.addPivotCache("", &PivotTableOptions{
DataRange: "A1:E31",
@ -334,6 +346,89 @@ func TestPivotTable(t *testing.T) {
assert.NoError(t, f.Close())
}
func TestPivotTableDataRange(t *testing.T) {
f := NewFile()
// Create table in a worksheet
assert.NoError(t, f.AddTable("Sheet1", &Table{
Name: "Table1",
Range: "A1:D5",
}))
for row := 2; row < 6; row++ {
assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("A%d", row), rand.Intn(10)))
assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("B%d", row), rand.Intn(10)))
assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("C%d", row), rand.Intn(10)))
assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("D%d", row), rand.Intn(10)))
}
// Test add pivot table with table data range
opts := PivotTableOptions{
DataRange: "Table1",
PivotTableRange: "Sheet1!G2:K7",
Rows: []PivotTableField{{Data: "Column1"}},
Columns: []PivotTableField{{Data: "Column2"}},
RowGrandTotals: true,
ColGrandTotals: true,
ShowDrill: true,
ShowRowHeaders: true,
ShowColHeaders: true,
ShowLastColumn: true,
ShowError: true,
PivotTableStyleName: "PivotStyleLight16",
}
assert.NoError(t, f.AddPivotTable(&opts))
assert.NoError(t, f.DeletePivotTable("Sheet1", "PivotTable1"))
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddPivotTable2.xlsx")))
assert.NoError(t, f.Close())
assert.NoError(t, f.AddPivotTable(&opts))
// Test delete pivot table with unsupported table relationships charset
f.Pkg.Store("xl/tables/table1.xml", MacintoshCyrillicCharset)
assert.EqualError(t, f.DeletePivotTable("Sheet1", "PivotTable1"), "XML syntax error on line 1: invalid UTF-8")
// Test delete pivot table with unsupported worksheet relationships charset
f.Relationships.Delete("xl/worksheets/_rels/sheet1.xml.rels")
f.Pkg.Store("xl/worksheets/_rels/sheet1.xml.rels", MacintoshCyrillicCharset)
assert.EqualError(t, f.DeletePivotTable("Sheet1", "PivotTable1"), "XML syntax error on line 1: invalid UTF-8")
// Test delete pivot table without worksheet relationships
f.Relationships.Delete("xl/worksheets/_rels/sheet1.xml.rels")
f.Pkg.Delete("xl/worksheets/_rels/sheet1.xml.rels")
assert.EqualError(t, f.DeletePivotTable("Sheet1", "PivotTable1"), "table PivotTable1 does not exist")
}
func TestParseFormatPivotTableSet(t *testing.T) {
f := NewFile()
// Create table in a worksheet
assert.NoError(t, f.AddTable("Sheet1", &Table{
Name: "Table1",
Range: "A1:D5",
}))
// Test parse format pivot table options with unsupported table relationships charset
f.Pkg.Store("xl/tables/table1.xml", MacintoshCyrillicCharset)
_, _, err := f.parseFormatPivotTableSet(&PivotTableOptions{
DataRange: "Table1",
PivotTableRange: "Sheet1!G2:K7",
Rows: []PivotTableField{{Data: "Column1"}},
})
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
}
func TestAddPivotCache(t *testing.T) {
f := NewFile()
// Create table in a worksheet
assert.NoError(t, f.AddTable("Sheet1", &Table{
Name: "Table1",
Range: "A1:D5",
}))
// Test add pivot table cache with unsupported table relationships charset
f.Pkg.Store("xl/tables/table1.xml", MacintoshCyrillicCharset)
assert.EqualError(t, f.addPivotCache("xl/pivotCache/pivotCacheDefinition1.xml", &PivotTableOptions{
DataRange: "Table1",
PivotTableRange: "Sheet1!G2:K7",
Rows: []PivotTableField{{Data: "Column1"}},
}), "XML syntax error on line 1: invalid UTF-8")
}
func TestAddPivotRowFields(t *testing.T) {
f := NewFile()
// Test invalid data range
@ -372,6 +467,15 @@ func TestGetPivotFieldsOrder(t *testing.T) {
// Test get table fields order with not exist worksheet
_, err := f.getTableFieldsOrder("", "SheetN!A1:E31")
assert.EqualError(t, err, "sheet SheetN does not exist")
// Create table in a worksheet
assert.NoError(t, f.AddTable("Sheet1", &Table{
Name: "Table1",
Range: "A1:D5",
}))
// Test get table fields order with unsupported table relationships charset
f.Pkg.Store("xl/tables/table1.xml", MacintoshCyrillicCharset)
_, err = f.getTableFieldsOrder("Sheet1", "Table")
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
}
func TestGetPivotTableFieldName(t *testing.T) {
@ -392,3 +496,16 @@ func TestGenPivotCacheDefinitionID(t *testing.T) {
assert.Equal(t, 1, f.genPivotCacheDefinitionID())
assert.NoError(t, f.Close())
}
func TestDeleteWorkbookPivotCache(t *testing.T) {
f := NewFile()
// Test delete workbook pivot table cache with unsupported workbook charset
f.WorkBook = nil
f.Pkg.Store("xl/workbook.xml", MacintoshCyrillicCharset)
assert.EqualError(t, f.deleteWorkbookPivotCache(PivotTableOptions{pivotCacheXML: "pivotCache/pivotCacheDefinition1.xml"}), "XML syntax error on line 1: invalid UTF-8")
// Test delete workbook pivot table cache with unsupported workbook relationships charset
f.Relationships.Delete("xl/_rels/workbook.xml.rels")
f.Pkg.Store("xl/_rels/workbook.xml.rels", MacintoshCyrillicCharset)
assert.EqualError(t, f.deleteWorkbookPivotCache(PivotTableOptions{pivotCacheXML: "pivotCache/pivotCacheDefinition1.xml"}), "XML syntax error on line 1: invalid UTF-8")
}

View File

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

View File

@ -170,6 +170,26 @@ func (f *File) getWorkbookRelsPath() (path string) {
return
}
// deleteWorkbookRels provides a function to delete relationships in
// xl/_rels/workbook.xml.rels by given type and target.
func (f *File) deleteWorkbookRels(relType, relTarget string) (string, error) {
var rID string
rels, err := f.relsReader(f.getWorkbookRelsPath())
if err != nil {
return rID, err
}
if rels == nil {
rels = &xlsxRelationships{}
}
for k, v := range rels.Relationships {
if v.Type == relType && v.Target == relTarget {
rID = v.ID
rels.Relationships = append(rels.Relationships[:k], rels.Relationships[k+1:]...)
}
}
return rID, err
}
// workbookReader provides a function to get the pointer to the workbook.xml
// structure after deserialization.
func (f *File) workbookReader() (*xlsxWorkbook, error) {