The `AddSlicer` function now support create pivot table slicer

This commit is contained in:
xuri 2023-09-27 00:05:59 +08:00
parent 9c079e5eec
commit c62d23e0a1
No known key found for this signature in database
GPG Key ID: BA5E5BB1C948EDF7
10 changed files with 610 additions and 188 deletions

View File

@ -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("<extLst>" + pc.ExtLst.Ext + "</extLst>")).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
}

View File

@ -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())
}

334
slicer.go
View File

@ -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), "<extLst>"), "</extLst>")}
@ -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), "<extLst>"), "</extLst>")}
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), "<extLst>"), "</extLst>")}
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("<extLst>" + pc.ExtLst.Ext + "</extLst>")).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), "<extLst>"), "</extLst>")}
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), "<extLst>"), "</extLst>")}
return err

View File

@ -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(`<timelines xmlns="%s"><timeline name="a"/></timelines>`, 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("<ext uri=\"%s\"></ext>", ExtURITimelineRefs)}}, 1))
assert.NoError(t, f.addSheetTableSlicer(&xlsxWorksheet{ExtLst: &xlsxExtLst{Ext: fmt.Sprintf("<ext uri=\"%s\"></ext>", 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(`<slicerCacheDefinition xmlns="%s" name="Slicer2" sourceName="B1"><extLst><ext uri="%s"/></extLst></slicerCacheDefinition>`, 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(`<slicerCacheDefinition xmlns="%s" name="Slicer1" sourceName="B1"><extLst><ext uri="%s"/></extLst></slicerCacheDefinition>`, 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(`<slicerCacheDefinition xmlns="%s" name="Slicer1" sourceName="B1"><extLst><ext uri="%s"><tableSlicerCache tableId="1" column="2"/></ext></extLst></slicerCacheDefinition>`, 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(`<slicerCacheDefinition xmlns="%s" name="Slicer1" sourceName="B1"></slicerCacheDefinition>`, 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(`<pivotCacheDefinition xmlns="%s"><extLst><ext uri="%s"><x14:pivotCacheDefinition pivotCacheId="1"/></ext></extLst></pivotCacheDefinition>`, NameSpaceSpreadSheet.Value, ExtURIPivotCacheDefinition)))
_, err := f.addPivotCacheSlicer(&PivotTableOptions{
pivotCacheXML: pivotCacheXML,
})
assert.NoError(t, err)
}

View File

@ -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), "<extLst>"), "</extLst>")}

View File

@ -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), "<extLst>"), "</extLst>")}

View File

@ -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

View File

@ -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"`
}

View File

@ -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"`
}

View File

@ -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"`