Add new exported function `DeletePivotTable`
- Support adding pivot table by specific table name - Update unit tests
This commit is contained in:
parent
1c7c417c70
commit
0861faf2f2
149
pivotTable.go
149
pivotTable.go
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
2
sheet.go
2
sheet.go
|
@ -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 {
|
||||
|
|
20
workbook.go
20
workbook.go
|
@ -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) {
|
||||
|
|
Loading…
Reference in New Issue