From 7291e787bd11f1634201329f0e1a5bae1fe8f803 Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 2 Nov 2023 00:15:41 +0800 Subject: [PATCH] Support update volatile dependencies on inserting/deleting columns/rows --- adjust.go | 86 ++++++++++++++++++++++++++++++++++++++++--------- adjust_test.go | 20 ++++++++++++ calcchain.go | 36 +++++++++++++++++++++ excelize.go | 19 +++++------ file.go | 1 + table.go | 2 +- templates.go | 5 +-- xmlCalcChain.go | 49 ++++++++++++++++++++++++++-- xmlTable.go | 6 ++-- 9 files changed, 192 insertions(+), 32 deletions(-) diff --git a/adjust.go b/adjust.go index 608e249af6..67e6b85db3 100644 --- a/adjust.go +++ b/adjust.go @@ -38,7 +38,7 @@ const ( // row: Index number of the row we're inserting/deleting before // offset: Number of rows/column to insert/delete negative values indicate deletion // -// TODO: adjustPageBreaks, adjustComments, adjustDataValidations, adjustProtectedCells +// TODO: adjustComments, adjustDataValidations, adjustDrawings, adjustPageBreaks, adjustProtectedCells func (f *File) adjustHelper(sheet string, dir adjustDirection, num, offset int) error { ws, err := f.workSheetReader(sheet) if err != nil { @@ -66,6 +66,9 @@ func (f *File) adjustHelper(sheet string, dir adjustDirection, num, offset int) if err = f.adjustCalcChain(dir, num, offset, sheetID); err != nil { return err } + if err = f.adjustVolatileDeps(dir, num, offset, sheetID); err != nil { + return err + } if ws.MergeCells != nil && len(ws.MergeCells.Cells) == 0 { ws.MergeCells = nil } @@ -498,7 +501,8 @@ func (f *File) adjustTable(ws *xlsxWorksheet, sheet string, dir adjustDirection, t.AutoFilter.Ref = t.Ref } _ = f.setTableColumns(sheet, true, x1, y1, x2, &t) - t.TotalsRowCount = 0 + // Currently doesn't support query table + t.TableType, t.TotalsRowCount, t.ConnectionID = "", 0, 0 table, _ := xml.Marshal(t) f.saveFileList(tableXML, table) } @@ -578,7 +582,6 @@ func (f *File) adjustMergeCells(ws *xlsxWorksheet, dir adjustDirection, num, off if dir == rows { if y1 == num && y2 == num && offset < 0 { f.deleteMergeCell(ws, i) - i-- continue } @@ -586,7 +589,6 @@ func (f *File) adjustMergeCells(ws *xlsxWorksheet, dir adjustDirection, num, off } else { if x1 == num && x2 == num && offset < 0 { f.deleteMergeCell(ws, i) - i-- continue } @@ -642,18 +644,15 @@ func (f *File) deleteMergeCell(ws *xlsxWorksheet, idx int) { } } -// adjustCalcChainRef update the cell reference in calculation chain when -// inserting or deleting rows or columns. -func (f *File) adjustCalcChainRef(i, c, r, offset int, dir adjustDirection) { +// adjustCellName returns updated cell name by giving column/row number and +// offset on inserting or deleting rows or columns. +func adjustCellName(cell string, dir adjustDirection, c, r, offset int) (string, error) { if dir == rows { if rn := r + offset; rn > 0 { - f.CalcChain.C[i].R, _ = CoordinatesToCellName(c, rn) + return CoordinatesToCellName(c, rn) } - return - } - if nc := c + offset; nc > 0 { - f.CalcChain.C[i].R, _ = CoordinatesToCellName(nc, r) } + return CoordinatesToCellName(c+offset, r) } // adjustCalcChain provides a function to update the calculation chain when @@ -665,7 +664,8 @@ func (f *File) adjustCalcChain(dir adjustDirection, num, offset, sheetID int) er // If sheet ID is omitted, it is assumed to be the same as the i value of // the previous cell. var prevSheetID int - for index, c := range f.CalcChain.C { + for i := 0; i < len(f.CalcChain.C); i++ { + c := f.CalcChain.C[i] if c.I == 0 { c.I = prevSheetID } @@ -680,16 +680,72 @@ func (f *File) adjustCalcChain(dir adjustDirection, num, offset, sheetID int) er if dir == rows && num <= rowNum { if num == rowNum && offset == -1 { _ = f.deleteCalcChain(c.I, c.R) + i-- continue } - f.adjustCalcChainRef(index, colNum, rowNum, offset, dir) + f.CalcChain.C[i].R, _ = adjustCellName(c.R, dir, colNum, rowNum, offset) } if dir == columns && num <= colNum { if num == colNum && offset == -1 { _ = f.deleteCalcChain(c.I, c.R) + i-- continue } - f.adjustCalcChainRef(index, colNum, rowNum, offset, dir) + f.CalcChain.C[i].R, _ = adjustCellName(c.R, dir, colNum, rowNum, offset) + } + } + return nil +} + +// adjustVolatileDepsTopic updates the volatile dependencies topic when +// inserting or deleting rows or columns. +func (vt *xlsxVolTypes) adjustVolatileDepsTopic(cell string, dir adjustDirection, indexes []int) (int, error) { + num, offset, i1, i2, i3, i4 := indexes[0], indexes[1], indexes[2], indexes[3], indexes[4], indexes[5] + colNum, rowNum, err := CellNameToCoordinates(cell) + if err != nil { + return i4, err + } + if dir == rows && num <= rowNum { + if num == rowNum && offset == -1 { + vt.deleteVolTopicRef(i1, i2, i3, i4) + i4-- + return i4, err + } + vt.VolType[i1].Main[i2].Tp[i3].Tr[i4].R, _ = adjustCellName(cell, dir, colNum, rowNum, offset) + } + if dir == columns && num <= colNum { + if num == colNum && offset == -1 { + vt.deleteVolTopicRef(i1, i2, i3, i4) + i4-- + return i4, err + } + if name, _ := adjustCellName(cell, dir, colNum, rowNum, offset); name != "" { + vt.VolType[i1].Main[i2].Tp[i3].Tr[i4].R, _ = adjustCellName(cell, dir, colNum, rowNum, offset) + } + } + return i4, err +} + +// adjustVolatileDeps updates the volatile dependencies when inserting or +// deleting rows or columns. +func (f *File) adjustVolatileDeps(dir adjustDirection, num, offset, sheetID int) error { + volTypes, err := f.volatileDepsReader() + if err != nil || volTypes == nil { + return err + } + for i1 := 0; i1 < len(volTypes.VolType); i1++ { + for i2 := 0; i2 < len(volTypes.VolType[i1].Main); i2++ { + for i3 := 0; i3 < len(volTypes.VolType[i1].Main[i2].Tp); i3++ { + for i4 := 0; i4 < len(volTypes.VolType[i1].Main[i2].Tp[i3].Tr); i4++ { + ref := volTypes.VolType[i1].Main[i2].Tp[i3].Tr[i4] + if ref.S != sheetID { + continue + } + if i4, err = volTypes.adjustVolatileDepsTopic(ref.R, dir, []int{num, offset, i1, i2, i3, i4}); err != nil { + return err + } + } + } } } return nil diff --git a/adjust_test.go b/adjust_test.go index 985d759e38..b2106bc0ec 100644 --- a/adjust_test.go +++ b/adjust_test.go @@ -927,3 +927,23 @@ func TestAdjustFormula(t *testing.T) { assert.NoError(t, f.InsertCols("Sheet1", "A", 1)) }) } + +func TestAdjustVolatileDeps(t *testing.T) { + f := NewFile() + f.Pkg.Store(defaultXMLPathVolatileDeps, []byte(fmt.Sprintf(`
`, NameSpaceSpreadSheet.Value))) + assert.NoError(t, f.InsertCols("Sheet1", "A", 1)) + assert.NoError(t, f.InsertRows("Sheet1", 2, 1)) + assert.Equal(t, "D3", f.VolatileDeps.VolType[0].Main[0].Tp[0].Tr[1].R) + assert.NoError(t, f.RemoveCol("Sheet1", "D")) + assert.NoError(t, f.RemoveRow("Sheet1", 4)) + assert.Len(t, f.VolatileDeps.VolType[0].Main[0].Tp[0].Tr, 1) + + f = NewFile() + f.Pkg.Store(defaultXMLPathVolatileDeps, MacintoshCyrillicCharset) + assert.EqualError(t, f.InsertRows("Sheet1", 2, 1), "XML syntax error on line 1: invalid UTF-8") + + f = NewFile() + f.Pkg.Store(defaultXMLPathVolatileDeps, []byte(fmt.Sprintf(`
`, NameSpaceSpreadSheet.Value))) + assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), f.InsertCols("Sheet1", "A", 1)) + f.volatileDepsWriter() +} diff --git a/calcchain.go b/calcchain.go index c35dd7d480..a7cd259cc7 100644 --- a/calcchain.go +++ b/calcchain.go @@ -81,3 +81,39 @@ func (c xlsxCalcChainCollection) Filter(fn func(v xlsxCalcChainC) bool) []xlsxCa } return results } + +// volatileDepsReader provides a function to get the pointer to the structure +// after deserialization of xl/volatileDependencies.xml. +func (f *File) volatileDepsReader() (*xlsxVolTypes, error) { + if f.VolatileDeps == nil { + volatileDeps, ok := f.Pkg.Load(defaultXMLPathVolatileDeps) + if !ok { + return f.VolatileDeps, nil + } + f.VolatileDeps = new(xlsxVolTypes) + if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(volatileDeps.([]byte)))). + Decode(f.VolatileDeps); err != nil && err != io.EOF { + return f.VolatileDeps, err + } + } + return f.VolatileDeps, nil +} + +// volatileDepsWriter provides a function to save xl/volatileDependencies.xml +// after serialize structure. +func (f *File) volatileDepsWriter() { + if f.VolatileDeps != nil { + output, _ := xml.Marshal(f.VolatileDeps) + f.saveFileList(defaultXMLPathVolatileDeps, output) + } +} + +// deleteVolTopicRef provides a function to remove cell reference on the +// volatile dependencies topic. +func (vt *xlsxVolTypes) deleteVolTopicRef(i1, i2, i3, i4 int) { + for i := range vt.VolType[i1].Main[i2].Tp[i3].Tr { + if i == i4 { + vt.VolType[i1].Main[i2].Tp[i3].Tr = append(vt.VolType[i1].Main[i2].Tp[i3].Tr[:i], vt.VolType[i1].Main[i2].Tp[i3].Tr[i+1:]...) + } + } +} diff --git a/excelize.go b/excelize.go index 79b4b687ab..ae80ce0df2 100644 --- a/excelize.go +++ b/excelize.go @@ -29,31 +29,32 @@ import ( // File define a populated spreadsheet file struct. type File struct { mu sync.Mutex - options *Options - xmlAttr sync.Map checked sync.Map + options *Options + sharedStringItem [][]uint + sharedStringsMap map[string]int + sharedStringTemp *os.File sheetMap map[string]string streams map[string]*StreamWriter tempFiles sync.Map - sharedStringsMap map[string]int - sharedStringItem [][]uint - sharedStringTemp *os.File + xmlAttr sync.Map CalcChain *xlsxCalcChain + CharsetReader charsetTranscoderFn Comments map[string]*xlsxComments ContentTypes *xlsxTypes + DecodeVMLDrawing map[string]*decodeVmlDrawing Drawings sync.Map Path string + Pkg sync.Map + Relationships sync.Map SharedStrings *xlsxSST Sheet sync.Map SheetCount int Styles *xlsxStyleSheet Theme *decodeTheme - DecodeVMLDrawing map[string]*decodeVmlDrawing VMLDrawing map[string]*vmlDrawing + VolatileDeps *xlsxVolTypes WorkBook *xlsxWorkbook - Relationships sync.Map - Pkg sync.Map - CharsetReader charsetTranscoderFn } // charsetTranscoderFn set user-defined codepage transcoder function for open diff --git a/file.go b/file.go index 7f2a036c71..dc29b50dfd 100644 --- a/file.go +++ b/file.go @@ -176,6 +176,7 @@ func (f *File) writeToZip(zw *zip.Writer) error { f.commentsWriter() f.contentTypesWriter() f.drawingsWriter() + f.volatileDepsWriter() f.vmlDrawingWriter() f.workBookWriter() f.workSheetWriter() diff --git a/table.go b/table.go index b9b32f6696..9d5820d974 100644 --- a/table.go +++ b/table.go @@ -292,7 +292,7 @@ func (f *File) setTableColumns(sheet string, showHeaderRow bool, x1, y1, x2 int, } header = append(header, name) if column := getTableColumn(name); column != nil { - column.ID, column.DataDxfID = idx, 0 + column.ID, column.DataDxfID, column.QueryTableFieldID = idx, 0, 0 tableColumns = append(tableColumns, column) continue } diff --git a/templates.go b/templates.go index c94a09b406..41a107c474 100644 --- a/templates.go +++ b/templates.go @@ -218,16 +218,17 @@ const ( ) const ( + defaultTempFileSST = "sharedStrings" + defaultXMLPathCalcChain = "xl/calcChain.xml" defaultXMLPathContentTypes = "[Content_Types].xml" defaultXMLPathDocPropsApp = "docProps/app.xml" defaultXMLPathDocPropsCore = "docProps/core.xml" - defaultXMLPathCalcChain = "xl/calcChain.xml" defaultXMLPathSharedStrings = "xl/sharedStrings.xml" defaultXMLPathStyles = "xl/styles.xml" defaultXMLPathTheme = "xl/theme/theme1.xml" + defaultXMLPathVolatileDeps = "xl/volatileDependencies.xml" defaultXMLPathWorkbook = "xl/workbook.xml" defaultXMLPathWorkbookRels = "xl/_rels/workbook.xml.rels" - defaultTempFileSST = "sharedStrings" ) // IndexedColorMapping is the table of default mappings from indexed color value diff --git a/xmlCalcChain.go b/xmlCalcChain.go index 9c1d1ee21f..0941cf5e50 100644 --- a/xmlCalcChain.go +++ b/xmlCalcChain.go @@ -11,10 +11,15 @@ package excelize -import "encoding/xml" +import ( + "encoding/xml" + "sync" +) -// xlsxCalcChain directly maps the calcChain element. This element represents the root of the calculation chain. +// xlsxCalcChain directly maps the calcChain element. This element represents +// the root of the calculation chain. type xlsxCalcChain struct { + mu sync.Mutex XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main calcChain"` C []xlsxCalcChainC `xml:"c"` } @@ -82,3 +87,43 @@ type xlsxCalcChainC struct { T bool `xml:"t,attr,omitempty"` A bool `xml:"a,attr,omitempty"` } + +// xlsxVolTypes maps the volatileDependencies part provides a cache of data that +// supports Real Time Data (RTD) and CUBE functions in the workbook. +type xlsxVolTypes struct { + mu sync.Mutex + XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main volTypes"` + VolType []xlsxVolType `xml:"volType"` + ExtLst *xlsxExtLst `xml:"extLst"` +} + +// xlsxVolType represents dependency information for a specific type of external +// data server. +type xlsxVolType struct { + Type string `xml:"type,attr"` + Main []xlsxVolMain `xml:"main"` +} + +// xlsxVolMain represents dependency information for all topics within a +// volatile dependency type that share the same first string or function +// argument. +type xlsxVolMain struct { + First string `xml:"first,attr"` + Tp []xlsxVolTopic `xml:"tp"` +} + +// xlsxVolTopic represents dependency information for all topics within a +// volatile dependency type that share the same first string or argument. +type xlsxVolTopic struct { + T string `xml:"t,attr,omitempty"` + V string `xml:"v"` + Stp []string `xml:"stp"` + Tr []xlsxVolTopicRef `xml:"tr"` +} + +// xlsxVolTopicRef represents the reference to a cell that depends on this +// topic. Each topic can have one or more cells dependencies. +type xlsxVolTopicRef struct { + R string `xml:"r,attr"` + S int `xml:"s,attr"` +} diff --git a/xmlTable.go b/xmlTable.go index 193113586d..4fc00b1cca 100644 --- a/xmlTable.go +++ b/xmlTable.go @@ -37,12 +37,12 @@ type xlsxTable struct { DataDxfID int `xml:"dataDxfId,attr,omitempty"` TotalsRowDxfID int `xml:"totalsRowDxfId,attr,omitempty"` HeaderRowBorderDxfID int `xml:"headerRowBorderDxfId,attr,omitempty"` - TableBorderDxfId int `xml:"tableBorderDxfId,attr,omitempty"` - TotalsRowBorderDxfId int `xml:"totalsRowBorderDxfId,attr,omitempty"` + TableBorderDxfID int `xml:"tableBorderDxfId,attr,omitempty"` + TotalsRowBorderDxfID int `xml:"totalsRowBorderDxfId,attr,omitempty"` HeaderRowCellStyle string `xml:"headerRowCellStyle,attr,omitempty"` DataCellStyle string `xml:"dataCellStyle,attr,omitempty"` TotalsRowCellStyle string `xml:"totalsRowCellStyle,attr,omitempty"` - ConnectionId int `xml:"connectionId,attr,omitempty"` + ConnectionID int `xml:"connectionId,attr,omitempty"` AutoFilter *xlsxAutoFilter `xml:"autoFilter"` TableColumns *xlsxTableColumns `xml:"tableColumns"` TableStyleInfo *xlsxTableStyleInfo `xml:"tableStyleInfo"`