Support update volatile dependencies on inserting/deleting columns/rows

This commit is contained in:
xuri 2023-11-02 00:15:41 +08:00
parent b41a6cc3cd
commit 7291e787bd
No known key found for this signature in database
GPG Key ID: BA5E5BB1C948EDF7
9 changed files with 192 additions and 32 deletions

View File

@ -38,7 +38,7 @@ const (
// row: Index number of the row we're inserting/deleting before // row: Index number of the row we're inserting/deleting before
// offset: Number of rows/column to insert/delete negative values indicate deletion // 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 { func (f *File) adjustHelper(sheet string, dir adjustDirection, num, offset int) error {
ws, err := f.workSheetReader(sheet) ws, err := f.workSheetReader(sheet)
if err != nil { 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 { if err = f.adjustCalcChain(dir, num, offset, sheetID); err != nil {
return err return err
} }
if err = f.adjustVolatileDeps(dir, num, offset, sheetID); err != nil {
return err
}
if ws.MergeCells != nil && len(ws.MergeCells.Cells) == 0 { if ws.MergeCells != nil && len(ws.MergeCells.Cells) == 0 {
ws.MergeCells = nil ws.MergeCells = nil
} }
@ -498,7 +501,8 @@ func (f *File) adjustTable(ws *xlsxWorksheet, sheet string, dir adjustDirection,
t.AutoFilter.Ref = t.Ref t.AutoFilter.Ref = t.Ref
} }
_ = f.setTableColumns(sheet, true, x1, y1, x2, &t) _ = 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) table, _ := xml.Marshal(t)
f.saveFileList(tableXML, table) f.saveFileList(tableXML, table)
} }
@ -578,7 +582,6 @@ func (f *File) adjustMergeCells(ws *xlsxWorksheet, dir adjustDirection, num, off
if dir == rows { if dir == rows {
if y1 == num && y2 == num && offset < 0 { if y1 == num && y2 == num && offset < 0 {
f.deleteMergeCell(ws, i) f.deleteMergeCell(ws, i)
i--
continue continue
} }
@ -586,7 +589,6 @@ func (f *File) adjustMergeCells(ws *xlsxWorksheet, dir adjustDirection, num, off
} else { } else {
if x1 == num && x2 == num && offset < 0 { if x1 == num && x2 == num && offset < 0 {
f.deleteMergeCell(ws, i) f.deleteMergeCell(ws, i)
i--
continue continue
} }
@ -642,18 +644,15 @@ func (f *File) deleteMergeCell(ws *xlsxWorksheet, idx int) {
} }
} }
// adjustCalcChainRef update the cell reference in calculation chain when // adjustCellName returns updated cell name by giving column/row number and
// inserting or deleting rows or columns. // offset on inserting or deleting rows or columns.
func (f *File) adjustCalcChainRef(i, c, r, offset int, dir adjustDirection) { func adjustCellName(cell string, dir adjustDirection, c, r, offset int) (string, error) {
if dir == rows { if dir == rows {
if rn := r + offset; rn > 0 { 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 // 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 // If sheet ID is omitted, it is assumed to be the same as the i value of
// the previous cell. // the previous cell.
var prevSheetID int 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 { if c.I == 0 {
c.I = prevSheetID c.I = prevSheetID
} }
@ -680,16 +680,72 @@ func (f *File) adjustCalcChain(dir adjustDirection, num, offset, sheetID int) er
if dir == rows && num <= rowNum { if dir == rows && num <= rowNum {
if num == rowNum && offset == -1 { if num == rowNum && offset == -1 {
_ = f.deleteCalcChain(c.I, c.R) _ = f.deleteCalcChain(c.I, c.R)
i--
continue 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 dir == columns && num <= colNum {
if num == colNum && offset == -1 { if num == colNum && offset == -1 {
_ = f.deleteCalcChain(c.I, c.R) _ = f.deleteCalcChain(c.I, c.R)
i--
continue 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 return nil

View File

@ -927,3 +927,23 @@ func TestAdjustFormula(t *testing.T) {
assert.NoError(t, f.InsertCols("Sheet1", "A", 1)) assert.NoError(t, f.InsertCols("Sheet1", "A", 1))
}) })
} }
func TestAdjustVolatileDeps(t *testing.T) {
f := NewFile()
f.Pkg.Store(defaultXMLPathVolatileDeps, []byte(fmt.Sprintf(`<volTypes xmlns="%s"><volType><main><tp><tr r="C2" s="2"/><tr r="C2" s="1"/><tr r="D3" s="1"/></tp></main></volType></volTypes>`, 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(`<volTypes xmlns="%s"><volType><main><tp><tr r="A" s="1"/></tp></main></volType></volTypes>`, NameSpaceSpreadSheet.Value)))
assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), f.InsertCols("Sheet1", "A", 1))
f.volatileDepsWriter()
}

View File

@ -81,3 +81,39 @@ func (c xlsxCalcChainCollection) Filter(fn func(v xlsxCalcChainC) bool) []xlsxCa
} }
return results 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:]...)
}
}
}

View File

@ -29,31 +29,32 @@ import (
// File define a populated spreadsheet file struct. // File define a populated spreadsheet file struct.
type File struct { type File struct {
mu sync.Mutex mu sync.Mutex
options *Options
xmlAttr sync.Map
checked sync.Map checked sync.Map
options *Options
sharedStringItem [][]uint
sharedStringsMap map[string]int
sharedStringTemp *os.File
sheetMap map[string]string sheetMap map[string]string
streams map[string]*StreamWriter streams map[string]*StreamWriter
tempFiles sync.Map tempFiles sync.Map
sharedStringsMap map[string]int xmlAttr sync.Map
sharedStringItem [][]uint
sharedStringTemp *os.File
CalcChain *xlsxCalcChain CalcChain *xlsxCalcChain
CharsetReader charsetTranscoderFn
Comments map[string]*xlsxComments Comments map[string]*xlsxComments
ContentTypes *xlsxTypes ContentTypes *xlsxTypes
DecodeVMLDrawing map[string]*decodeVmlDrawing
Drawings sync.Map Drawings sync.Map
Path string Path string
Pkg sync.Map
Relationships sync.Map
SharedStrings *xlsxSST SharedStrings *xlsxSST
Sheet sync.Map Sheet sync.Map
SheetCount int SheetCount int
Styles *xlsxStyleSheet Styles *xlsxStyleSheet
Theme *decodeTheme Theme *decodeTheme
DecodeVMLDrawing map[string]*decodeVmlDrawing
VMLDrawing map[string]*vmlDrawing VMLDrawing map[string]*vmlDrawing
VolatileDeps *xlsxVolTypes
WorkBook *xlsxWorkbook WorkBook *xlsxWorkbook
Relationships sync.Map
Pkg sync.Map
CharsetReader charsetTranscoderFn
} }
// charsetTranscoderFn set user-defined codepage transcoder function for open // charsetTranscoderFn set user-defined codepage transcoder function for open

View File

@ -176,6 +176,7 @@ func (f *File) writeToZip(zw *zip.Writer) error {
f.commentsWriter() f.commentsWriter()
f.contentTypesWriter() f.contentTypesWriter()
f.drawingsWriter() f.drawingsWriter()
f.volatileDepsWriter()
f.vmlDrawingWriter() f.vmlDrawingWriter()
f.workBookWriter() f.workBookWriter()
f.workSheetWriter() f.workSheetWriter()

View File

@ -292,7 +292,7 @@ func (f *File) setTableColumns(sheet string, showHeaderRow bool, x1, y1, x2 int,
} }
header = append(header, name) header = append(header, name)
if column := getTableColumn(name); column != nil { 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) tableColumns = append(tableColumns, column)
continue continue
} }

View File

@ -218,16 +218,17 @@ const (
) )
const ( const (
defaultTempFileSST = "sharedStrings"
defaultXMLPathCalcChain = "xl/calcChain.xml"
defaultXMLPathContentTypes = "[Content_Types].xml" defaultXMLPathContentTypes = "[Content_Types].xml"
defaultXMLPathDocPropsApp = "docProps/app.xml" defaultXMLPathDocPropsApp = "docProps/app.xml"
defaultXMLPathDocPropsCore = "docProps/core.xml" defaultXMLPathDocPropsCore = "docProps/core.xml"
defaultXMLPathCalcChain = "xl/calcChain.xml"
defaultXMLPathSharedStrings = "xl/sharedStrings.xml" defaultXMLPathSharedStrings = "xl/sharedStrings.xml"
defaultXMLPathStyles = "xl/styles.xml" defaultXMLPathStyles = "xl/styles.xml"
defaultXMLPathTheme = "xl/theme/theme1.xml" defaultXMLPathTheme = "xl/theme/theme1.xml"
defaultXMLPathVolatileDeps = "xl/volatileDependencies.xml"
defaultXMLPathWorkbook = "xl/workbook.xml" defaultXMLPathWorkbook = "xl/workbook.xml"
defaultXMLPathWorkbookRels = "xl/_rels/workbook.xml.rels" defaultXMLPathWorkbookRels = "xl/_rels/workbook.xml.rels"
defaultTempFileSST = "sharedStrings"
) )
// IndexedColorMapping is the table of default mappings from indexed color value // IndexedColorMapping is the table of default mappings from indexed color value

View File

@ -11,10 +11,15 @@
package excelize 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 { type xlsxCalcChain struct {
mu sync.Mutex
XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main calcChain"` XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main calcChain"`
C []xlsxCalcChainC `xml:"c"` C []xlsxCalcChainC `xml:"c"`
} }
@ -82,3 +87,43 @@ type xlsxCalcChainC struct {
T bool `xml:"t,attr,omitempty"` T bool `xml:"t,attr,omitempty"`
A bool `xml:"a,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"`
}

View File

@ -37,12 +37,12 @@ type xlsxTable struct {
DataDxfID int `xml:"dataDxfId,attr,omitempty"` DataDxfID int `xml:"dataDxfId,attr,omitempty"`
TotalsRowDxfID int `xml:"totalsRowDxfId,attr,omitempty"` TotalsRowDxfID int `xml:"totalsRowDxfId,attr,omitempty"`
HeaderRowBorderDxfID int `xml:"headerRowBorderDxfId,attr,omitempty"` HeaderRowBorderDxfID int `xml:"headerRowBorderDxfId,attr,omitempty"`
TableBorderDxfId int `xml:"tableBorderDxfId,attr,omitempty"` TableBorderDxfID int `xml:"tableBorderDxfId,attr,omitempty"`
TotalsRowBorderDxfId int `xml:"totalsRowBorderDxfId,attr,omitempty"` TotalsRowBorderDxfID int `xml:"totalsRowBorderDxfId,attr,omitempty"`
HeaderRowCellStyle string `xml:"headerRowCellStyle,attr,omitempty"` HeaderRowCellStyle string `xml:"headerRowCellStyle,attr,omitempty"`
DataCellStyle string `xml:"dataCellStyle,attr,omitempty"` DataCellStyle string `xml:"dataCellStyle,attr,omitempty"`
TotalsRowCellStyle string `xml:"totalsRowCellStyle,attr,omitempty"` TotalsRowCellStyle string `xml:"totalsRowCellStyle,attr,omitempty"`
ConnectionId int `xml:"connectionId,attr,omitempty"` ConnectionID int `xml:"connectionId,attr,omitempty"`
AutoFilter *xlsxAutoFilter `xml:"autoFilter"` AutoFilter *xlsxAutoFilter `xml:"autoFilter"`
TableColumns *xlsxTableColumns `xml:"tableColumns"` TableColumns *xlsxTableColumns `xml:"tableColumns"`
TableStyleInfo *xlsxTableStyleInfo `xml:"tableStyleInfo"` TableStyleInfo *xlsxTableStyleInfo `xml:"tableStyleInfo"`