Skip to content

Commit

Permalink
Handle cross sheet references of tables
Browse files Browse the repository at this point in the history
  • Loading branch information
Ivan Hristov committed Jan 6, 2025
1 parent dc0abb9 commit dd96f5a
Show file tree
Hide file tree
Showing 5 changed files with 165 additions and 13 deletions.
7 changes: 6 additions & 1 deletion calc.go
Original file line number Diff line number Diff line change
Expand Up @@ -1536,7 +1536,12 @@ func pickColumnInTableRef(tblRef tableRef, colName string) (string, error) {
}

col := coords[0] + offset
return coordinatesToRangeRef([]int{col, coords[1] + 1, col, coords[3]})
rangeRef, err := coordinatesToRangeRef([]int{col, coords[1] + 1, col, coords[3]})
if err != nil {
return "", err
}

return fmt.Sprintf("%s!%s", tblRef.sheet, rangeRef), nil
}

func tryParseAsTableRef(ref string, tableRefs *sync.Map) (string, error) {
Expand Down
23 changes: 23 additions & 0 deletions calc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6472,6 +6472,29 @@ func TestTableReference(t *testing.T) {
assert.Equal(t, "Hedgehog", res, "D1 calc is wrong")
}

func TestTableRefenceFromOtherSheet(t *testing.T) {
f := sheetWithTables(t)

_, err := f.NewSheet("Sheet2")
assert.NoError(t, err, "creating Sheet2")

assert.NoError(t, f.SetCellFormula("Sheet2", "A1", "=INDEX(FieryTable[Column1], 1)"), "cell formula for A1")

res, err := f.CalcCellValue("Sheet2", "A1")
assert.NoError(t, err, "calculating cell A1")
assert.Equal(t, "Foo", res, "A1 calc is wrong")
}

func TestTableReferenceWithDeletedTable(t *testing.T) {
f := sheetWithTables(t)

assert.NoError(t, f.SetCellFormula("Sheet1", "A1", "=INDEX(FieryTable[Column1], 1)"), "cell formula for A1")
assert.NoError(t, f.DeleteTable("FieryTable"), "deleting table")

_, err := f.CalcCellValue("Sheet1", "A1")
assert.Error(t, err, "A1 calc is wrong")
}

func TestTableReferenceToNotExistingTable(t *testing.T) {
f := sheetWithTables(t)
assert.NoError(t, f.SetCellFormula("Sheet1", "A1", "=INDEX(NotExisting[Column1], 1)"), "cell formula for A1")
Expand Down
144 changes: 134 additions & 10 deletions excelize.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"fmt"
"io"
"os"
"path"
"path/filepath"
"strconv"
"strings"
Expand All @@ -27,6 +28,8 @@ import (
"golang.org/x/net/html/charset"
)

const targetModeExternal = "external"

// File define a populated spreadsheet file struct.
type File struct {
mu sync.Mutex
Expand Down Expand Up @@ -63,9 +66,17 @@ type File struct {

type tableRef struct {
ref string
sheet string
columns []string
}

type relationMetadata struct {
wb *xlsxWorkbook
wbRels *xlsxRelationships
relsPerSheet map[string]*xlsxRelationships
tables map[string]*xlsxTable
}

// charsetTranscoderFn set user-defined codepage transcoder function for open
// the spreadsheet from non-UTF-8 encoding.
type charsetTranscoderFn func(charset string, input io.Reader) (rdr io.Reader, err error)
Expand Down Expand Up @@ -211,16 +222,10 @@ func OpenReader(r io.Reader, opts ...Options) (*File, error) {
f.SheetCount = sheetCount
for k, v := range file {
f.Pkg.Store(k, v)
}

if strings.Contains(k, "xl/tables/table") {
var t xlsxTable
dec := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(v)))
if err := dec.Decode(&t); err != nil && err != io.EOF {
return nil, fmt.Errorf("parsing table %s: %w", k, err)
}

f.tableRefs.Store(t.Name, tableRefFromXLSXTable(t))
}
if err := f.storeRelations(file); err != nil {
return f, err
}
if f.CalcChain, err = f.calcChainReader(); err != nil {
return f, err
Expand All @@ -235,9 +240,128 @@ func OpenReader(r io.Reader, opts ...Options) (*File, error) {
return f, err
}

func tableRefFromXLSXTable(t xlsxTable) tableRef {
func (f *File) storeRelations(files map[string][]byte) error {
relMetadata, err := f.parseRelationMetadata(files)
if err != nil {
return err
}
if relMetadata.wb == nil || relMetadata.wbRels == nil {
return nil
}

sheetRelIDs := make(map[string]string)
for _, sheet := range relMetadata.wb.Sheets.Sheet {
sheetRelIDs[sheet.ID] = sheet.Name
}

sheetBaseToSheetNames := make(map[string]string)
for _, rel := range relMetadata.wbRels.Relationships {
sheetName, ok := sheetRelIDs[rel.ID]

if !ok || strings.ToLower(rel.TargetMode) == targetModeExternal || rel.Type != SourceRelationshipWorkSheet {
continue
}

sheetBaseToSheetNames[fmt.Sprintf("%s.rels", path.Base(rel.Target))] = sheetName
}

tableBaseToSheetNames := make(map[string]string)
for key, sheetRels := range relMetadata.relsPerSheet {
sheetName, ok := sheetBaseToSheetNames[key]
if !ok {
continue
}

for _, rel := range sheetRels.Relationships {
if strings.ToLower(rel.TargetMode) == targetModeExternal || rel.Type != SourceRelationshipTable {
continue
}

tableBaseToSheetNames[path.Base(rel.Target)] = sheetName
}
}

for key, t := range relMetadata.tables {
if sheetName, ok := tableBaseToSheetNames[key]; ok {
f.tableRefs.Store(t.Name, tableRefFromXLSXTable(t, sheetName))
}
}

return nil
}

func (f *File) parseRelationMetadata(files map[string][]byte) (*relationMetadata, error) {
var err error
relMetadata := &relationMetadata{
relsPerSheet: map[string]*xlsxRelationships{},
tables: map[string]*xlsxTable{},
}

for k, v := range files {
switch {
case strings.Contains(k, "xl/workbook.xml") && v != nil:
relMetadata.wb, err = f.parseWorkbook(v)
if err != nil {
return nil, err
}
case strings.Contains(k, "xl/_rels/workbook.xml.rels") && v != nil:
relMetadata.wbRels, err = f.parseRelationships(v)
if err != nil {
return nil, fmt.Errorf("workbook rels: %w", err)
}
case strings.Contains(k, "xl/worksheets/_rels") && v != nil:
sheetRels, err := f.parseRelationships(v)
if err != nil {
return nil, fmt.Errorf("workbook sheet rel %s: %w", k, err)
}
relMetadata.relsPerSheet[path.Base(k)] = sheetRels
case strings.Contains(k, "xl/tables") && v != nil:
table, err := f.parseTable(v)
if err != nil {
return nil, fmt.Errorf("table %s: %w", k, err)
}
relMetadata.tables[path.Base(k)] = table
}
}

return relMetadata, nil
}

func (f *File) parseWorkbook(v []byte) (*xlsxWorkbook, error) {
var wb *xlsxWorkbook

dec := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(v)))
if err := dec.Decode(&wb); err != nil && err != io.EOF {
return nil, fmt.Errorf("decoding workbook: %w", err)
}

return wb, nil
}

func (f *File) parseRelationships(v []byte) (*xlsxRelationships, error) {
var rels *xlsxRelationships

dec := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(v)))
if err := dec.Decode(&rels); err != nil && err != io.EOF {
return nil, fmt.Errorf("decoding relationships: %w", err)
}

return rels, nil
}

func (f *File) parseTable(v []byte) (*xlsxTable, error) {
var table *xlsxTable
dec := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(v)))
if err := dec.Decode(&table); err != nil && err != io.EOF {
return nil, fmt.Errorf("parsing table: %w", err)
}
return table, nil
}

func tableRefFromXLSXTable(t *xlsxTable, sheet string) tableRef {
tblRef := tableRef{
ref: t.Ref,
sheet: sheet,
columns: make([]string, 0, t.TableColumns.Count),
}
for _, col := range t.TableColumns.TableColumn {
Expand Down
2 changes: 1 addition & 1 deletion excelize_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ func TestOpenReader(t *testing.T) {
defaultXMLPathWorkbookRels,
} {
_, err = OpenReader(preset(defaultXMLPath, false))
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
assert.ErrorContains(t, err, "XML syntax error on line 1: invalid UTF-8")
}
// Test open workbook without internal XML parts
for _, defaultXMLPath := range []string{
Expand Down
2 changes: 1 addition & 1 deletion table.go
Original file line number Diff line number Diff line change
Expand Up @@ -397,7 +397,7 @@ func (f *File) addTable(sheet, tableXML string, x1, y1, x2, y2, i int, opts *Tab
}
table, err := xml.Marshal(t)
f.saveFileList(tableXML, table)
f.tableRefs.Store(t.Name, tableRefFromXLSXTable(t))
f.tableRefs.Store(t.Name, tableRefFromXLSXTable(&t, sheet))
return err
}

Expand Down

0 comments on commit dd96f5a

Please sign in to comment.