From b6e0bb8e13c4ee507c418334d33d6ecd4ae5b9f6 Mon Sep 17 00:00:00 2001 From: Ivan Hristov Date: Fri, 3 Jan 2025 13:07:20 +0200 Subject: [PATCH 1/9] Support table referencing --- adjust.go | 9 +++++---- calc.go | 28 ++++++++++++++++++++++++---- cell.go | 9 +++++---- excelize.go | 13 +++++++++++++ styles.go | 7 ++++--- table.go | 2 ++ 6 files changed, 53 insertions(+), 15 deletions(-) diff --git a/adjust.go b/adjust.go index 6696056919..d2501b1fc7 100644 --- a/adjust.go +++ b/adjust.go @@ -17,6 +17,7 @@ import ( "io" "strconv" "strings" + "sync" "unicode" "github.com/xuri/efp" @@ -508,9 +509,9 @@ type arrayFormulaOperandToken struct { // setCoordinates convert each corner cell reference in the array formula cell // range to the coordinate number. -func (af *arrayFormulaOperandToken) setCoordinates() error { +func (af *arrayFormulaOperandToken) setCoordinates(tableRefs *sync.Map) error { for i, ref := range strings.Split(af.sourceCellRef, ":") { - cellRef, col, row, err := parseRef(ref) + cellRef, col, row, err := parseRef(ref, tableRefs) if err != nil { return err } @@ -575,7 +576,7 @@ func transformArrayFormula(tokens []efp.Token, afs []arrayFormulaOperandToken) s // getArrayFormulaTokens returns parsed formula token and operand related token // list for in array formula. -func getArrayFormulaTokens(sheet, formula string, definedNames []DefinedName) ([]efp.Token, []arrayFormulaOperandToken, error) { +func getArrayFormulaTokens(sheet, formula string, definedNames []DefinedName, tableRefs *sync.Map) ([]efp.Token, []arrayFormulaOperandToken, error) { var ( ps = efp.ExcelParser() tokens = ps.Parse(formula) @@ -594,7 +595,7 @@ func getArrayFormulaTokens(sheet, formula string, definedNames []DefinedName) ([ operandTokenIndex: i, sourceCellRef: tokenVal, } - if err := arrayFormulaOperandToken.setCoordinates(); err != nil { + if err := arrayFormulaOperandToken.setCoordinates(tableRefs); err != nil { return tokens, arrayFormulaOperandTokens, err } arrayFormulaOperandTokens = append(arrayFormulaOperandTokens, arrayFormulaOperandToken) diff --git a/calc.go b/calc.go index 71d4bd3b1c..4b66485560 100644 --- a/calc.go +++ b/calc.go @@ -98,9 +98,13 @@ const ( tfmmss = `(([0-9])+):(([0-9])+\.([0-9])+)( (am|pm))?` tfhhmmss = `(([0-9])+):(([0-9])+):(([0-9])+(\.([0-9])+)?)( (am|pm))?` timeSuffix = `( (` + tfhh + `|` + tfhhmm + `|` + tfmmss + `|` + tfhhmmss + `))?$` + tableRef = `^(\w+)\[([^\]]+)\]$` + + tableRefPartsCnt = 2 ) var ( + errNotExistingTable = errors.New("not existing table") // tokenPriority defined basic arithmetic operator priority tokenPriority = map[string]int{ "^": 5, @@ -211,6 +215,7 @@ var ( criteriaL, criteriaG, } + tableRefRe = regexp.MustCompile(tableRef) ) // calcContext defines the formula execution context. @@ -1487,13 +1492,28 @@ func (f *File) parseToken(ctx *calcContext, sheet string, token efp.Token, opdSt } // parseRef parse reference for a cell, column name or row number. -func parseRef(ref string) (cellRef, bool, bool, error) { +func parseRef(ref string, tableRefs *sync.Map) (cellRef, bool, bool, error) { var ( err, colErr, rowErr error cr cellRef cell = ref - tokens = strings.Split(ref, "!") ) + + submatch := tableRefRe.FindStringSubmatch(ref) + if len(submatch) == tableRefPartsCnt { + tableName := submatch[0] + rawRef, ok := tableRefs.Load(tableName) + if !ok { + return cr, false, false, fmt.Errorf("referencing table `%s`: %w", tableName, errNotExistingTable) + } + + ref, ok = rawRef.(string) + if !ok { + panic(fmt.Sprintf("unexpected reference type %T", ref)) + } + } + + tokens := strings.Split(ref, "!") if len(tokens) == 2 { // have a worksheet cr.Sheet, cell = tokens[0], tokens[1] } @@ -1546,7 +1566,7 @@ func (f *File) parseReference(ctx *calcContext, sheet, reference string) (formul if len(ranges) > 1 { var cr cellRange for i, ref := range ranges { - cellRef, col, row, err := parseRef(ref) + cellRef, col, row, err := parseRef(ref, f.tableRefs) if err != nil { return newErrorFormulaArg(formulaErrorNAME, "invalid reference"), errors.New("invalid reference") } @@ -1570,7 +1590,7 @@ func (f *File) parseReference(ctx *calcContext, sheet, reference string) (formul cellRanges.PushBack(cr) return f.rangeResolver(ctx, cellRefs, cellRanges) } - cellRef, _, _, err := parseRef(reference) + cellRef, _, _, err := parseRef(reference, f.tableRefs) if err != nil { return newErrorFormulaArg(formulaErrorNAME, "invalid reference"), errors.New("invalid reference") } diff --git a/cell.go b/cell.go index d642d82897..e44fb0521f 100644 --- a/cell.go +++ b/cell.go @@ -20,6 +20,7 @@ import ( "reflect" "strconv" "strings" + "sync" "time" "unicode/utf8" ) @@ -810,7 +811,7 @@ func (f *File) SetCellFormula(sheet, cell, formula string, opts ...FormulaOpts) } c.F.T = *opt.Type if c.F.T == STCellFormulaTypeArray && opt.Ref != nil { - if err = ws.setArrayFormula(sheet, &xlsxF{Ref: *opt.Ref, Content: formula}, f.GetDefinedName()); err != nil { + if err = ws.setArrayFormula(sheet, &xlsxF{Ref: *opt.Ref, Content: formula}, f.GetDefinedName(), f.tableRefs); err != nil { return err } } @@ -831,7 +832,7 @@ func (f *File) SetCellFormula(sheet, cell, formula string, opts ...FormulaOpts) // setArrayFormula transform the array formula in an array formula range to the // normal formula and set cells in this range to the formula as the normal // formula. -func (ws *xlsxWorksheet) setArrayFormula(sheet string, formula *xlsxF, definedNames []DefinedName) error { +func (ws *xlsxWorksheet) setArrayFormula(sheet string, formula *xlsxF, definedNames []DefinedName, tableRef *sync.Map) error { if len(strings.Split(formula.Ref, ":")) < 2 { return nil } @@ -840,7 +841,7 @@ func (ws *xlsxWorksheet) setArrayFormula(sheet string, formula *xlsxF, definedNa return err } _ = sortCoordinates(coordinates) - tokens, arrayFormulaOperandTokens, err := getArrayFormulaTokens(sheet, formula.Content, definedNames) + tokens, arrayFormulaOperandTokens, err := getArrayFormulaTokens(sheet, formula.Content, definedNames, tableRef) if err != nil { return err } @@ -879,7 +880,7 @@ func (f *File) setArrayFormulaCells() error { for _, row := range ws.SheetData.Row { for _, cell := range row.C { if cell.F != nil && cell.F.T == STCellFormulaTypeArray { - if err = ws.setArrayFormula(sheetN, cell.F, definedNames); err != nil { + if err = ws.setArrayFormula(sheetN, cell.F, definedNames, f.tableRefs); err != nil { return err } } diff --git a/excelize.go b/excelize.go index 4de1ac1104..ed64d61140 100644 --- a/excelize.go +++ b/excelize.go @@ -16,6 +16,7 @@ import ( "archive/zip" "bytes" "encoding/xml" + "fmt" "io" "os" "path/filepath" @@ -39,6 +40,7 @@ type File struct { streams map[string]*StreamWriter tempFiles sync.Map xmlAttr sync.Map + tableRefs *sync.Map CalcChain *xlsxCalcChain CharsetReader charsetTranscoderFn Comments map[string]*xlsxComments @@ -140,6 +142,7 @@ func newFile() *File { checked: sync.Map{}, sheetMap: make(map[string]string), tempFiles: sync.Map{}, + tableRefs: &sync.Map{}, Comments: make(map[string]*xlsxComments), Drawings: sync.Map{}, sharedStringsMap: make(map[string]int), @@ -203,6 +206,16 @@ 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, t.Ref) + } } if f.CalcChain, err = f.calcChainReader(); err != nil { return f, err diff --git a/styles.go b/styles.go index 3cab1e8a59..0d6a37534a 100644 --- a/styles.go +++ b/styles.go @@ -21,6 +21,7 @@ import ( "sort" "strconv" "strings" + "sync" ) // stylesReader provides a function to get the pointer to the structure after @@ -2791,7 +2792,7 @@ func (f *File) SetConditionalFormat(sheet, rangeRef string, opts []ConditionalFo if err != nil { return err } - SQRef, mastCell, err := prepareConditionalFormatRange(rangeRef) + SQRef, mastCell, err := prepareConditionalFormatRange(rangeRef, f.tableRefs) if err != nil { return err } @@ -2852,7 +2853,7 @@ func (f *File) SetConditionalFormat(sheet, rangeRef string, opts []ConditionalFo // prepareConditionalFormatRange returns checked cell range and master cell // reference by giving conditional formatting range reference. -func prepareConditionalFormatRange(rangeRef string) (string, string, error) { +func prepareConditionalFormatRange(rangeRef string, tableRefs *sync.Map) (string, string, error) { var SQRef, mastCell string if rangeRef == "" { return SQRef, mastCell, ErrParameterRequired @@ -2864,7 +2865,7 @@ func prepareConditionalFormatRange(rangeRef string) (string, string, error) { if j > 1 { return SQRef, mastCell, ErrParameterInvalid } - cellRef, col, row, err := parseRef(ref) + cellRef, col, row, err := parseRef(ref, tableRefs) if err != nil { return SQRef, mastCell, err } diff --git a/table.go b/table.go index 0fb8a7119f..daebffcb72 100644 --- a/table.go +++ b/table.go @@ -187,6 +187,7 @@ func (f *File) DeleteTable(name string) error { if tbl.RID == table.rID { ws.TableParts.TableParts = append(ws.TableParts.TableParts[:i], ws.TableParts.TableParts[i+1:]...) f.Pkg.Delete(table.tableXML) + f.tableRefs.Delete(table.Name) _ = f.removeContentTypesPart(ContentTypeSpreadSheetMLTable, "/"+table.tableXML) f.deleteSheetRelationships(sheet, tbl.RID) break @@ -396,6 +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, t.Ref) return err } From f1fd951f70651eb1e2dad68172371489809337df Mon Sep 17 00:00:00 2001 From: Ivan Hristov Date: Fri, 3 Jan 2025 16:14:22 +0200 Subject: [PATCH 2/9] Make it work - was performing the parsing of refs incorrectly in the wrong place --- adjust.go | 9 +++--- calc.go | 89 +++++++++++++++++++++++++++++++++++++++------------- calc_test.go | 13 ++++++++ cell.go | 9 +++--- excelize.go | 18 ++++++++++- styles.go | 7 ++--- table.go | 2 +- 7 files changed, 109 insertions(+), 38 deletions(-) diff --git a/adjust.go b/adjust.go index d2501b1fc7..6696056919 100644 --- a/adjust.go +++ b/adjust.go @@ -17,7 +17,6 @@ import ( "io" "strconv" "strings" - "sync" "unicode" "github.com/xuri/efp" @@ -509,9 +508,9 @@ type arrayFormulaOperandToken struct { // setCoordinates convert each corner cell reference in the array formula cell // range to the coordinate number. -func (af *arrayFormulaOperandToken) setCoordinates(tableRefs *sync.Map) error { +func (af *arrayFormulaOperandToken) setCoordinates() error { for i, ref := range strings.Split(af.sourceCellRef, ":") { - cellRef, col, row, err := parseRef(ref, tableRefs) + cellRef, col, row, err := parseRef(ref) if err != nil { return err } @@ -576,7 +575,7 @@ func transformArrayFormula(tokens []efp.Token, afs []arrayFormulaOperandToken) s // getArrayFormulaTokens returns parsed formula token and operand related token // list for in array formula. -func getArrayFormulaTokens(sheet, formula string, definedNames []DefinedName, tableRefs *sync.Map) ([]efp.Token, []arrayFormulaOperandToken, error) { +func getArrayFormulaTokens(sheet, formula string, definedNames []DefinedName) ([]efp.Token, []arrayFormulaOperandToken, error) { var ( ps = efp.ExcelParser() tokens = ps.Parse(formula) @@ -595,7 +594,7 @@ func getArrayFormulaTokens(sheet, formula string, definedNames []DefinedName, ta operandTokenIndex: i, sourceCellRef: tokenVal, } - if err := arrayFormulaOperandToken.setCoordinates(tableRefs); err != nil { + if err := arrayFormulaOperandToken.setCoordinates(); err != nil { return tokens, arrayFormulaOperandTokens, err } arrayFormulaOperandTokens = append(arrayFormulaOperandTokens, arrayFormulaOperandToken) diff --git a/calc.go b/calc.go index 4b66485560..f17fbb7405 100644 --- a/calc.go +++ b/calc.go @@ -98,13 +98,14 @@ const ( tfmmss = `(([0-9])+):(([0-9])+\.([0-9])+)( (am|pm))?` tfhhmmss = `(([0-9])+):(([0-9])+):(([0-9])+(\.([0-9])+)?)( (am|pm))?` timeSuffix = `( (` + tfhh + `|` + tfhhmm + `|` + tfmmss + `|` + tfhhmmss + `))?$` - tableRef = `^(\w+)\[([^\]]+)\]$` - tableRefPartsCnt = 2 + tableRefPartsCnt = 3 ) var ( - errNotExistingTable = errors.New("not existing table") + errNotExistingTable = errors.New("not existing table") + errNotExistingColumn = errors.New("not existing column") + errInvalidTableRef = errors.New("invalid table ref") // tokenPriority defined basic arithmetic operator priority tokenPriority = map[string]int{ "^": 5, @@ -215,7 +216,7 @@ var ( criteriaL, criteriaG, } - tableRefRe = regexp.MustCompile(tableRef) + tableRefRe = regexp.MustCompile(`^(\w+)\[([^\]]+)\]$`) ) // calcContext defines the formula execution context. @@ -1492,28 +1493,14 @@ func (f *File) parseToken(ctx *calcContext, sheet string, token efp.Token, opdSt } // parseRef parse reference for a cell, column name or row number. -func parseRef(ref string, tableRefs *sync.Map) (cellRef, bool, bool, error) { +func parseRef(ref string) (cellRef, bool, bool, error) { var ( err, colErr, rowErr error cr cellRef cell = ref + tokens = strings.Split(ref, "!") ) - submatch := tableRefRe.FindStringSubmatch(ref) - if len(submatch) == tableRefPartsCnt { - tableName := submatch[0] - rawRef, ok := tableRefs.Load(tableName) - if !ok { - return cr, false, false, fmt.Errorf("referencing table `%s`: %w", tableName, errNotExistingTable) - } - - ref, ok = rawRef.(string) - if !ok { - panic(fmt.Sprintf("unexpected reference type %T", ref)) - } - } - - tokens := strings.Split(ref, "!") if len(tokens) == 2 { // have a worksheet cr.Sheet, cell = tokens[0], tokens[1] } @@ -1529,6 +1516,58 @@ func parseRef(ref string, tableRefs *sync.Map) (cellRef, bool, bool, error) { return cr, false, false, err } +func pickColumnInTableRef(tblRef tableRef, colName string) (string, error) { + offset := -1 + + // Column ID is not reliable for order so we need to iterate through them. + for i, otherColName := range tblRef.columns { + if colName == otherColName { + offset = i + } + } + + if offset == -1 { + return "", fmt.Errorf("column `%s` not in table: %w", colName, errNotExistingColumn) + } + + coords, err := rangeRefToCoordinates(tblRef.ref) + if err != nil { + return "", err + } + + return coordinatesToRangeRef([]int{coords[0] + offset, coords[1], coords[2], coords[3]}) +} + +func tryParseAsTableRef(ref string, tableRefs *sync.Map) (string, error) { + submatch := tableRefRe.FindStringSubmatch(ref) + tableName := submatch[1] + colName := submatch[2] + + // Fallback to regular ref. + if len(submatch) != tableRefPartsCnt { + return ref, nil + } + + rawTblRef, ok := tableRefs.Load(tableName) + if !ok { + return "", fmt.Errorf("referencing table `%s`: %w", tableName, errNotExistingTable) + } + + tblRef, ok := rawTblRef.(tableRef) + if !ok { + panic(fmt.Sprintf("unexpected reference type %T", ref)) + } + + if !strings.Contains(tblRef.ref, ":") { + if len(tblRef.columns) != 1 && tblRef.columns[0] != colName { + return "", fmt.Errorf("column `%s` not in table `%s`: %w", colName, tableName, errNotExistingColumn) + } + return tblRef.ref, nil + } + + return pickColumnInTableRef(tblRef, colName) +} + // prepareCellRange checking and convert cell reference to a cell range. func (cr *cellRange) prepareCellRange(col, row bool, cellRef cellRef) error { if col { @@ -1562,11 +1601,16 @@ func (cr *cellRange) prepareCellRange(col, row bool, cellRef cellRef) error { // characters and default sheet name. func (f *File) parseReference(ctx *calcContext, sheet, reference string) (formulaArg, error) { reference = strings.ReplaceAll(reference, "$", "") + reference, err := tryParseAsTableRef(reference, f.tableRefs) + if err != nil { + return newErrorFormulaArg(formulaErrorNAME, "invalid reference"), err + } + ranges, cellRanges, cellRefs := strings.Split(reference, ":"), list.New(), list.New() if len(ranges) > 1 { var cr cellRange for i, ref := range ranges { - cellRef, col, row, err := parseRef(ref, f.tableRefs) + cellRef, col, row, err := parseRef(ref) if err != nil { return newErrorFormulaArg(formulaErrorNAME, "invalid reference"), errors.New("invalid reference") } @@ -1590,8 +1634,9 @@ func (f *File) parseReference(ctx *calcContext, sheet, reference string) (formul cellRanges.PushBack(cr) return f.rangeResolver(ctx, cellRefs, cellRanges) } - cellRef, _, _, err := parseRef(reference, f.tableRefs) + cellRef, _, _, err := parseRef(reference) if err != nil { + fmt.Println(err) return newErrorFormulaArg(formulaErrorNAME, "invalid reference"), errors.New("invalid reference") } if cellRef.Sheet == "" { diff --git a/calc_test.go b/calc_test.go index fc28a05955..2f579092ed 100644 --- a/calc_test.go +++ b/calc_test.go @@ -6447,6 +6447,19 @@ func TestCalcCellResolver(t *testing.T) { assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") } +func TestTableReference(t *testing.T) { + f := NewFile() + + assert.NoError(t, f.AddTable("Sheet1", &Table{Range: "A2:C5", Name: "FieryTable"}), "adding table") + assert.NoError(t, f.SetCellValue("Sheet1", "A3", "Foo"), "set A3") + + assert.NoError(t, f.SetCellFormula("Sheet1", "A1", "=INDEX(FieryTable[Column1], 2)"), "set cell formula for A1") + + res, err := f.CalcCellValue("Sheet1", "A1") + assert.NoError(t, err, "calculating cell value") + assert.Equal(t, res, "Foo", "asserting cell value calculated by referencing a table") +} + func TestEvalInfixExp(t *testing.T) { f := NewFile() arg, err := f.evalInfixExp(nil, "Sheet1", "A1", []efp.Token{ diff --git a/cell.go b/cell.go index e44fb0521f..d642d82897 100644 --- a/cell.go +++ b/cell.go @@ -20,7 +20,6 @@ import ( "reflect" "strconv" "strings" - "sync" "time" "unicode/utf8" ) @@ -811,7 +810,7 @@ func (f *File) SetCellFormula(sheet, cell, formula string, opts ...FormulaOpts) } c.F.T = *opt.Type if c.F.T == STCellFormulaTypeArray && opt.Ref != nil { - if err = ws.setArrayFormula(sheet, &xlsxF{Ref: *opt.Ref, Content: formula}, f.GetDefinedName(), f.tableRefs); err != nil { + if err = ws.setArrayFormula(sheet, &xlsxF{Ref: *opt.Ref, Content: formula}, f.GetDefinedName()); err != nil { return err } } @@ -832,7 +831,7 @@ func (f *File) SetCellFormula(sheet, cell, formula string, opts ...FormulaOpts) // setArrayFormula transform the array formula in an array formula range to the // normal formula and set cells in this range to the formula as the normal // formula. -func (ws *xlsxWorksheet) setArrayFormula(sheet string, formula *xlsxF, definedNames []DefinedName, tableRef *sync.Map) error { +func (ws *xlsxWorksheet) setArrayFormula(sheet string, formula *xlsxF, definedNames []DefinedName) error { if len(strings.Split(formula.Ref, ":")) < 2 { return nil } @@ -841,7 +840,7 @@ func (ws *xlsxWorksheet) setArrayFormula(sheet string, formula *xlsxF, definedNa return err } _ = sortCoordinates(coordinates) - tokens, arrayFormulaOperandTokens, err := getArrayFormulaTokens(sheet, formula.Content, definedNames, tableRef) + tokens, arrayFormulaOperandTokens, err := getArrayFormulaTokens(sheet, formula.Content, definedNames) if err != nil { return err } @@ -880,7 +879,7 @@ func (f *File) setArrayFormulaCells() error { for _, row := range ws.SheetData.Row { for _, cell := range row.C { if cell.F != nil && cell.F.T == STCellFormulaTypeArray { - if err = ws.setArrayFormula(sheetN, cell.F, definedNames, f.tableRefs); err != nil { + if err = ws.setArrayFormula(sheetN, cell.F, definedNames); err != nil { return err } } diff --git a/excelize.go b/excelize.go index ed64d61140..b6c0d218ad 100644 --- a/excelize.go +++ b/excelize.go @@ -61,6 +61,11 @@ type File struct { WorkBook *xlsxWorkbook } +type tableRef struct { + ref string + columns []string +} + // 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) @@ -214,7 +219,7 @@ func OpenReader(r io.Reader, opts ...Options) (*File, error) { return nil, fmt.Errorf("parsing table %s: %w", k, err) } - f.tableRefs.Store(t.Name, t.Ref) + f.tableRefs.Store(t.Name, tableRefFromXLSXTable(t)) } } if f.CalcChain, err = f.calcChainReader(); err != nil { @@ -230,6 +235,17 @@ func OpenReader(r io.Reader, opts ...Options) (*File, error) { return f, err } +func tableRefFromXLSXTable(t xlsxTable) tableRef { + tblRef := tableRef{ + ref: t.Ref, + columns: make([]string, 0, t.TableColumns.Count), + } + for _, col := range t.TableColumns.TableColumn { + tblRef.columns = append(tblRef.columns, col.Name) + } + return tblRef +} + // getOptions provides a function to parse the optional settings for open // and reading spreadsheet. func (f *File) getOptions(opts ...Options) *Options { diff --git a/styles.go b/styles.go index 0d6a37534a..3cab1e8a59 100644 --- a/styles.go +++ b/styles.go @@ -21,7 +21,6 @@ import ( "sort" "strconv" "strings" - "sync" ) // stylesReader provides a function to get the pointer to the structure after @@ -2792,7 +2791,7 @@ func (f *File) SetConditionalFormat(sheet, rangeRef string, opts []ConditionalFo if err != nil { return err } - SQRef, mastCell, err := prepareConditionalFormatRange(rangeRef, f.tableRefs) + SQRef, mastCell, err := prepareConditionalFormatRange(rangeRef) if err != nil { return err } @@ -2853,7 +2852,7 @@ func (f *File) SetConditionalFormat(sheet, rangeRef string, opts []ConditionalFo // prepareConditionalFormatRange returns checked cell range and master cell // reference by giving conditional formatting range reference. -func prepareConditionalFormatRange(rangeRef string, tableRefs *sync.Map) (string, string, error) { +func prepareConditionalFormatRange(rangeRef string) (string, string, error) { var SQRef, mastCell string if rangeRef == "" { return SQRef, mastCell, ErrParameterRequired @@ -2865,7 +2864,7 @@ func prepareConditionalFormatRange(rangeRef string, tableRefs *sync.Map) (string if j > 1 { return SQRef, mastCell, ErrParameterInvalid } - cellRef, col, row, err := parseRef(ref, tableRefs) + cellRef, col, row, err := parseRef(ref) if err != nil { return SQRef, mastCell, err } diff --git a/table.go b/table.go index daebffcb72..8809a33765 100644 --- a/table.go +++ b/table.go @@ -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, t.Ref) + f.tableRefs.Store(t.Name, tableRefFromXLSXTable(t)) return err } From 8af092e076c86d32ebd17d2c7a176cec7cba5a23 Mon Sep 17 00:00:00 2001 From: Ivan Hristov Date: Fri, 3 Jan 2025 16:19:03 +0200 Subject: [PATCH 3/9] Remove unnecessary log --- calc.go | 1 - 1 file changed, 1 deletion(-) diff --git a/calc.go b/calc.go index f17fbb7405..78672dd779 100644 --- a/calc.go +++ b/calc.go @@ -1636,7 +1636,6 @@ func (f *File) parseReference(ctx *calcContext, sheet, reference string) (formul } cellRef, _, _, err := parseRef(reference) if err != nil { - fmt.Println(err) return newErrorFormulaArg(formulaErrorNAME, "invalid reference"), errors.New("invalid reference") } if cellRef.Sheet == "" { From c5de21def70aefd7673016effdc1963ba5dd33d6 Mon Sep 17 00:00:00 2001 From: Ivan Hristov Date: Fri, 3 Jan 2025 16:45:58 +0200 Subject: [PATCH 4/9] Cover with tests and fix some corner cases --- calc.go | 14 ++++---------- calc_test.go | 53 +++++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 52 insertions(+), 15 deletions(-) diff --git a/calc.go b/calc.go index 78672dd779..a7f8ddd2a2 100644 --- a/calc.go +++ b/calc.go @@ -1530,6 +1530,7 @@ func pickColumnInTableRef(tblRef tableRef, colName string) (string, error) { return "", fmt.Errorf("column `%s` not in table: %w", colName, errNotExistingColumn) } + // Tables having just a single cell are invalid. Hence it is safe to assume it should always be a range reference. coords, err := rangeRefToCoordinates(tblRef.ref) if err != nil { return "", err @@ -1540,14 +1541,14 @@ func pickColumnInTableRef(tblRef tableRef, colName string) (string, error) { func tryParseAsTableRef(ref string, tableRefs *sync.Map) (string, error) { submatch := tableRefRe.FindStringSubmatch(ref) - tableName := submatch[1] - colName := submatch[2] - // Fallback to regular ref. if len(submatch) != tableRefPartsCnt { return ref, nil } + tableName := submatch[1] + colName := submatch[2] + rawTblRef, ok := tableRefs.Load(tableName) if !ok { return "", fmt.Errorf("referencing table `%s`: %w", tableName, errNotExistingTable) @@ -1558,13 +1559,6 @@ func tryParseAsTableRef(ref string, tableRefs *sync.Map) (string, error) { panic(fmt.Sprintf("unexpected reference type %T", ref)) } - if !strings.Contains(tblRef.ref, ":") { - if len(tblRef.columns) != 1 && tblRef.columns[0] != colName { - return "", fmt.Errorf("column `%s` not in table `%s`: %w", colName, tableName, errNotExistingColumn) - } - return tblRef.ref, nil - } - return pickColumnInTableRef(tblRef, colName) } diff --git a/calc_test.go b/calc_test.go index 2f579092ed..389df1ad47 100644 --- a/calc_test.go +++ b/calc_test.go @@ -6448,16 +6448,59 @@ func TestCalcCellResolver(t *testing.T) { } func TestTableReference(t *testing.T) { + f := sheetWithTables(t) + + assert.NoError(t, f.SetCellFormula("Sheet1", "A1", "=INDEX(FieryTable[Column1], 2)"), "cell formula for A1") + assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "=INDEX(FieryTable[Column2], 2)"), "cell formula for A2") + assert.NoError(t, f.SetCellFormula("Sheet1", "C1", "=B1*2"), "cell formula for A3") + assert.NoError(t, f.SetCellFormula("Sheet1", "D1", "=INDEX(FrostyTable[Column1], 2)"), "cell formula for A1") + + res, err := f.CalcCellValue("Sheet1", "A1") + assert.NoError(t, err, "calculating cell A1") + assert.Equal(t, res, "Foo", "A1 calc is wrong") + + res, err = f.CalcCellValue("Sheet1", "B1") + assert.NoError(t, err, "calculating cell B1") + assert.Equal(t, res, "12.5", "B1 calc is wrong") + + res, err = f.CalcCellValue("Sheet1", "C1") + assert.NoError(t, err, "calculating cell C1") + assert.Equal(t, res, "25", "C1 calc is wrong") + + res, err = f.CalcCellValue("Sheet1", "D1") + assert.NoError(t, err, "calculating cell D1") + assert.Equal(t, res, "Hedgehog", "D1 calc is wrong") +} + +func TestTableReferenceToNotExistingTable(t *testing.T) { + f := sheetWithTables(t) + assert.NoError(t, f.SetCellFormula("Sheet1", "A1", "=INDEX(NotExisting[Column1], 2)"), "cell formula for A1") + + _, err := f.CalcCellValue("Sheet1", "A1") + assert.Error(t, err, "A1 calc is wrong") +} + +func TestTableReferenceToNotExistingColumn(t *testing.T) { + f := sheetWithTables(t) + assert.NoError(t, f.SetCellFormula("Sheet1", "A1", "=INDEX(FieryTable[NotExisting], 2)"), "cell formula for A1") + + _, err := f.CalcCellValue("Sheet1", "A1") + assert.Error(t, err, "A1 calc is wrong") +} + +func sheetWithTables(t *testing.T) *File { f := NewFile() - assert.NoError(t, f.AddTable("Sheet1", &Table{Range: "A2:C5", Name: "FieryTable"}), "adding table") + // Multi column with default column names + assert.NoError(t, f.AddTable("Sheet1", &Table{Range: "A2:C5", Name: "FieryTable"}), "adding FieryTable") assert.NoError(t, f.SetCellValue("Sheet1", "A3", "Foo"), "set A3") + assert.NoError(t, f.SetCellValue("Sheet1", "B3", "12.5"), "set A3") - assert.NoError(t, f.SetCellFormula("Sheet1", "A1", "=INDEX(FieryTable[Column1], 2)"), "set cell formula for A1") + // Single column with renamed column + assert.NoError(t, f.AddTable("Sheet1", &Table{Range: "A8:A9", Name: "FrostyTable"}), "adding FrostyTable") + assert.NoError(t, f.SetCellValue("Sheet1", "A9", "Hedgehog"), "set A3") - res, err := f.CalcCellValue("Sheet1", "A1") - assert.NoError(t, err, "calculating cell value") - assert.Equal(t, res, "Foo", "asserting cell value calculated by referencing a table") + return f } func TestEvalInfixExp(t *testing.T) { From 45799dc607364c77f6aa8032e3821652eda96ae8 Mon Sep 17 00:00:00 2001 From: Ivan Hristov Date: Fri, 3 Jan 2025 17:17:09 +0200 Subject: [PATCH 5/9] Follow conventions for placing actual vs expected --- calc_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/calc_test.go b/calc_test.go index 389df1ad47..eaafdcea61 100644 --- a/calc_test.go +++ b/calc_test.go @@ -6457,19 +6457,19 @@ func TestTableReference(t *testing.T) { res, err := f.CalcCellValue("Sheet1", "A1") assert.NoError(t, err, "calculating cell A1") - assert.Equal(t, res, "Foo", "A1 calc is wrong") + assert.Equal(t, "Foo", res, "A1 calc is wrong") res, err = f.CalcCellValue("Sheet1", "B1") assert.NoError(t, err, "calculating cell B1") - assert.Equal(t, res, "12.5", "B1 calc is wrong") + assert.Equal(t, "12.5", res, "B1 calc is wrong") res, err = f.CalcCellValue("Sheet1", "C1") assert.NoError(t, err, "calculating cell C1") - assert.Equal(t, res, "25", "C1 calc is wrong") + assert.Equal(t, "25", res, "C1 calc is wrong") res, err = f.CalcCellValue("Sheet1", "D1") assert.NoError(t, err, "calculating cell D1") - assert.Equal(t, res, "Hedgehog", "D1 calc is wrong") + assert.Equal(t, "Hedgehog", res, "D1 calc is wrong") } func TestTableReferenceToNotExistingTable(t *testing.T) { From bf299b0ad746bf09984972b5f824a92a603ed3f6 Mon Sep 17 00:00:00 2001 From: Ivan Hristov Date: Fri, 3 Jan 2025 17:38:28 +0200 Subject: [PATCH 6/9] Fix returned range --- calc.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/calc.go b/calc.go index a7f8ddd2a2..fdd109c355 100644 --- a/calc.go +++ b/calc.go @@ -1536,7 +1536,8 @@ func pickColumnInTableRef(tblRef tableRef, colName string) (string, error) { return "", err } - return coordinatesToRangeRef([]int{coords[0] + offset, coords[1], coords[2], coords[3]}) + col := coords[0] + offset + return coordinatesToRangeRef([]int{col, coords[1], col, coords[3]}) } func tryParseAsTableRef(ref string, tableRefs *sync.Map) (string, error) { From dc0abb967dfe6d9940aa28fb3615c10bc74f20f3 Mon Sep 17 00:00:00 2001 From: Ivan Hristov Date: Fri, 3 Jan 2025 17:46:04 +0200 Subject: [PATCH 7/9] Fix starting row --- calc.go | 3 +-- calc_test.go | 10 +++++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/calc.go b/calc.go index fdd109c355..83952e7adc 100644 --- a/calc.go +++ b/calc.go @@ -105,7 +105,6 @@ const ( var ( errNotExistingTable = errors.New("not existing table") errNotExistingColumn = errors.New("not existing column") - errInvalidTableRef = errors.New("invalid table ref") // tokenPriority defined basic arithmetic operator priority tokenPriority = map[string]int{ "^": 5, @@ -1537,7 +1536,7 @@ func pickColumnInTableRef(tblRef tableRef, colName string) (string, error) { } col := coords[0] + offset - return coordinatesToRangeRef([]int{col, coords[1], col, coords[3]}) + return coordinatesToRangeRef([]int{col, coords[1] + 1, col, coords[3]}) } func tryParseAsTableRef(ref string, tableRefs *sync.Map) (string, error) { diff --git a/calc_test.go b/calc_test.go index eaafdcea61..9fcf6732a0 100644 --- a/calc_test.go +++ b/calc_test.go @@ -6450,10 +6450,10 @@ func TestCalcCellResolver(t *testing.T) { func TestTableReference(t *testing.T) { f := sheetWithTables(t) - assert.NoError(t, f.SetCellFormula("Sheet1", "A1", "=INDEX(FieryTable[Column1], 2)"), "cell formula for A1") - assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "=INDEX(FieryTable[Column2], 2)"), "cell formula for A2") + assert.NoError(t, f.SetCellFormula("Sheet1", "A1", "=INDEX(FieryTable[Column1], 1)"), "cell formula for A1") + assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "=INDEX(FieryTable[Column2], 1)"), "cell formula for A2") assert.NoError(t, f.SetCellFormula("Sheet1", "C1", "=B1*2"), "cell formula for A3") - assert.NoError(t, f.SetCellFormula("Sheet1", "D1", "=INDEX(FrostyTable[Column1], 2)"), "cell formula for A1") + assert.NoError(t, f.SetCellFormula("Sheet1", "D1", "=INDEX(FrostyTable[Column1], 1)"), "cell formula for A1") res, err := f.CalcCellValue("Sheet1", "A1") assert.NoError(t, err, "calculating cell A1") @@ -6474,7 +6474,7 @@ func TestTableReference(t *testing.T) { func TestTableReferenceToNotExistingTable(t *testing.T) { f := sheetWithTables(t) - assert.NoError(t, f.SetCellFormula("Sheet1", "A1", "=INDEX(NotExisting[Column1], 2)"), "cell formula for A1") + assert.NoError(t, f.SetCellFormula("Sheet1", "A1", "=INDEX(NotExisting[Column1], 1)"), "cell formula for A1") _, err := f.CalcCellValue("Sheet1", "A1") assert.Error(t, err, "A1 calc is wrong") @@ -6482,7 +6482,7 @@ func TestTableReferenceToNotExistingTable(t *testing.T) { func TestTableReferenceToNotExistingColumn(t *testing.T) { f := sheetWithTables(t) - assert.NoError(t, f.SetCellFormula("Sheet1", "A1", "=INDEX(FieryTable[NotExisting], 2)"), "cell formula for A1") + assert.NoError(t, f.SetCellFormula("Sheet1", "A1", "=INDEX(FieryTable[NotExisting], 1)"), "cell formula for A1") _, err := f.CalcCellValue("Sheet1", "A1") assert.Error(t, err, "A1 calc is wrong") From dd96f5a1fa2fc525fc5f716851a4e07a916a2778 Mon Sep 17 00:00:00 2001 From: Ivan Hristov Date: Mon, 6 Jan 2025 16:31:52 +0200 Subject: [PATCH 8/9] Handle cross sheet references of tables --- calc.go | 7 ++- calc_test.go | 23 ++++++++ excelize.go | 144 +++++++++++++++++++++++++++++++++++++++++++---- excelize_test.go | 2 +- table.go | 2 +- 5 files changed, 165 insertions(+), 13 deletions(-) diff --git a/calc.go b/calc.go index 83952e7adc..eb992709eb 100644 --- a/calc.go +++ b/calc.go @@ -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) { diff --git a/calc_test.go b/calc_test.go index 9fcf6732a0..02915f1624 100644 --- a/calc_test.go +++ b/calc_test.go @@ -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") diff --git a/excelize.go b/excelize.go index b6c0d218ad..dd683895b3 100644 --- a/excelize.go +++ b/excelize.go @@ -19,6 +19,7 @@ import ( "fmt" "io" "os" + "path" "path/filepath" "strconv" "strings" @@ -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 @@ -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) @@ -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 @@ -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 { diff --git a/excelize_test.go b/excelize_test.go index 7416409a49..7ed65f6662 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -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{ diff --git a/table.go b/table.go index 8809a33765..c58270310a 100644 --- a/table.go +++ b/table.go @@ -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 } From 72cb2f8c3e542327b582550cfab97a2c5f38ebdf Mon Sep 17 00:00:00 2001 From: Ivan Hristov Date: Mon, 6 Jan 2025 17:26:18 +0200 Subject: [PATCH 9/9] Reference tableRefs directly from File struct --- calc.go | 6 +++--- excelize.go | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/calc.go b/calc.go index eb992709eb..1ac7df6302 100644 --- a/calc.go +++ b/calc.go @@ -1544,7 +1544,7 @@ func pickColumnInTableRef(tblRef tableRef, colName string) (string, error) { return fmt.Sprintf("%s!%s", tblRef.sheet, rangeRef), nil } -func tryParseAsTableRef(ref string, tableRefs *sync.Map) (string, error) { +func (f *File) tryParseAsTableRef(ref string) (string, error) { submatch := tableRefRe.FindStringSubmatch(ref) // Fallback to regular ref. if len(submatch) != tableRefPartsCnt { @@ -1554,7 +1554,7 @@ func tryParseAsTableRef(ref string, tableRefs *sync.Map) (string, error) { tableName := submatch[1] colName := submatch[2] - rawTblRef, ok := tableRefs.Load(tableName) + rawTblRef, ok := f.tableRefs.Load(tableName) if !ok { return "", fmt.Errorf("referencing table `%s`: %w", tableName, errNotExistingTable) } @@ -1600,7 +1600,7 @@ func (cr *cellRange) prepareCellRange(col, row bool, cellRef cellRef) error { // characters and default sheet name. func (f *File) parseReference(ctx *calcContext, sheet, reference string) (formulaArg, error) { reference = strings.ReplaceAll(reference, "$", "") - reference, err := tryParseAsTableRef(reference, f.tableRefs) + reference, err := f.tryParseAsTableRef(reference) if err != nil { return newErrorFormulaArg(formulaErrorNAME, "invalid reference"), err } diff --git a/excelize.go b/excelize.go index dd683895b3..3229f15c20 100644 --- a/excelize.go +++ b/excelize.go @@ -43,7 +43,7 @@ type File struct { streams map[string]*StreamWriter tempFiles sync.Map xmlAttr sync.Map - tableRefs *sync.Map + tableRefs sync.Map CalcChain *xlsxCalcChain CharsetReader charsetTranscoderFn Comments map[string]*xlsxComments @@ -158,7 +158,7 @@ func newFile() *File { checked: sync.Map{}, sheetMap: make(map[string]string), tempFiles: sync.Map{}, - tableRefs: &sync.Map{}, + tableRefs: sync.Map{}, Comments: make(map[string]*xlsxComments), Drawings: sync.Map{}, sharedStringsMap: make(map[string]int),