Skip to content

Commit 418be6d

Browse files
authored
This closes qax-os#2223, limit calculation engine processing to actual data ranges (qax-os#2242)
- Cache formula argument for calculation engine - Optimized string concatenation - Upgrade macos-13 image with macos-15-intel image in GitHub Action
1 parent 4ff4208 commit 418be6d

File tree

10 files changed

+89
-36
lines changed

10 files changed

+89
-36
lines changed

.github/workflows/go.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ jobs:
66
strategy:
77
matrix:
88
go-version: [1.24.x, 1.25.x]
9-
os: [ubuntu-24.04, macos-13, windows-latest]
9+
os: [ubuntu-24.04, macos-15-intel, windows-latest]
1010
targetplatform: [x86, x64]
1111

1212
runs-on: ${{ matrix.os }}

adjust.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ func (f *File) adjustHelper(sheet string, dir adjustDirection, num, offset int)
7575
if err != nil {
7676
return err
7777
}
78-
f.calcCache.Clear()
78+
f.clearCalcCache()
7979
sheetID := f.getSheetID(sheet)
8080
if dir == rows {
8181
err = f.adjustRowDimensions(sheet, ws, num, offset)

calc.go

Lines changed: 59 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,8 @@ var (
193193
return fmt.Sprintf("R[%d]C[%d]", row, col), nil
194194
},
195195
}
196-
formulaFormats = []*regexp.Regexp{
196+
formulaFnNameReplacer = strings.NewReplacer("_xlfn.", "", ".", "dot")
197+
formulaFormats = []*regexp.Regexp{
197198
regexp.MustCompile(`^(\d+)$`),
198199
regexp.MustCompile(`^=(.*)$`),
199200
regexp.MustCompile(`^<>(.*)$`),
@@ -839,8 +840,8 @@ type formulaFuncs struct {
839840
// Z.TEST
840841
// ZTEST
841842
func (f *File) CalcCellValue(sheet, cell string, opts ...Options) (result string, err error) {
842-
cacheKey := fmt.Sprintf("%s!%s", sheet, cell)
843-
if cachedResult, found := f.calcCache.Load(cacheKey); found {
843+
entry := sheet + "!" + cell
844+
if cachedResult, ok := f.calcCache.Load(entry); ok {
844845
return cachedResult.(string), nil
845846
}
846847
options := f.getOptions(opts...)
@@ -850,7 +851,7 @@ func (f *File) CalcCellValue(sheet, cell string, opts ...Options) (result string
850851
token formulaArg
851852
)
852853
if token, err = f.calcCellValue(&calcContext{
853-
entry: fmt.Sprintf("%s!%s", sheet, cell),
854+
entry: entry,
854855
maxCalcIterations: options.MaxCalcIterations,
855856
iterations: make(map[string]uint),
856857
iterationsCache: make(map[string]formulaArg),
@@ -866,25 +867,31 @@ func (f *File) CalcCellValue(sheet, cell string, opts ...Options) (result string
866867
if precision > 15 {
867868
result, err = f.formattedValue(&xlsxC{S: styleIdx, V: strings.ToUpper(strconv.FormatFloat(decimal, 'G', 15, 64))}, rawCellValue, CellTypeNumber)
868869
if err == nil {
869-
f.calcCache.Store(cacheKey, result)
870+
f.calcCache.Store(entry, result)
870871
}
871872
return
872873
}
873874
if !strings.HasPrefix(result, "0") {
874875
result, err = f.formattedValue(&xlsxC{S: styleIdx, V: strings.ToUpper(strconv.FormatFloat(decimal, 'f', -1, 64))}, rawCellValue, CellTypeNumber)
875876
}
876877
if err == nil {
877-
f.calcCache.Store(cacheKey, result)
878+
f.calcCache.Store(entry, result)
878879
}
879880
return
880881
}
881882
result, err = f.formattedValue(&xlsxC{S: styleIdx, V: token.Value()}, rawCellValue, CellTypeInlineString)
882883
if err == nil {
883-
f.calcCache.Store(cacheKey, result)
884+
f.calcCache.Store(entry, result)
884885
}
885886
return
886887
}
887888

889+
// clearCalcCache clear all calculation related caches.
890+
func (f *File) clearCalcCache() {
891+
f.calcCache.Clear()
892+
f.formulaArgCache.Clear()
893+
}
894+
888895
// calcCellValue calculate cell value by given context, worksheet name and cell
889896
// reference.
890897
func (f *File) calcCellValue(ctx *calcContext, sheet, cell string) (result formulaArg, err error) {
@@ -1106,8 +1113,8 @@ func (f *File) evalInfixExpFunc(ctx *calcContext, sheet, cell string, token, nex
11061113
}
11071114
prepareEvalInfixExp(opfStack, opftStack, opfdStack, argsStack)
11081115
// call formula function to evaluate
1109-
arg := callFuncByName(&formulaFuncs{f: f, sheet: sheet, cell: cell, ctx: ctx}, strings.NewReplacer(
1110-
"_xlfn.", "", ".", "dot").Replace(opfStack.Peek().(efp.Token).TValue),
1116+
arg := callFuncByName(&formulaFuncs{f: f, sheet: sheet, cell: cell, ctx: ctx},
1117+
formulaFnNameReplacer.Replace(opfStack.Peek().(efp.Token).TValue),
11111118
[]reflect.Value{reflect.ValueOf(argsStack.Peek().(*list.List))})
11121119
if arg.Type == ArgError && opfStack.Len() == 1 {
11131120
return arg
@@ -1651,7 +1658,11 @@ func (f *File) cellResolver(ctx *calcContext, sheet, cell string) (formulaArg, e
16511658
value string
16521659
err error
16531660
)
1654-
ref := fmt.Sprintf("%s!%s", sheet, cell)
1661+
ref := sheet + "!" + cell
1662+
if cached, ok := f.formulaArgCache.Load(ref); ok {
1663+
return cached.(formulaArg), err
1664+
}
1665+
16551666
if formula, _ := f.getCellFormula(sheet, cell, true); len(formula) != 0 {
16561667
ctx.mu.Lock()
16571668
if ctx.entry != ref {
@@ -1660,6 +1671,7 @@ func (f *File) cellResolver(ctx *calcContext, sheet, cell string) (formulaArg, e
16601671
ctx.mu.Unlock()
16611672
arg, _ = f.calcCellValue(ctx, sheet, cell)
16621673
ctx.iterationsCache[ref] = arg
1674+
f.formulaArgCache.Store(ref, arg)
16631675
return arg, nil
16641676
}
16651677
ctx.mu.Unlock()
@@ -1674,29 +1686,29 @@ func (f *File) cellResolver(ctx *calcContext, sheet, cell string) (formulaArg, e
16741686
cellType, _ := f.GetCellType(sheet, cell)
16751687
switch cellType {
16761688
case CellTypeBool:
1677-
return arg.ToBool(), err
1689+
arg = arg.ToBool()
16781690
case CellTypeNumber, CellTypeUnset:
16791691
if arg.Value() == "" {
1680-
return newEmptyFormulaArg(), err
1692+
arg = newEmptyFormulaArg()
1693+
} else {
1694+
arg = arg.ToNumber()
16811695
}
1682-
return arg.ToNumber(), err
16831696
case CellTypeInlineString, CellTypeSharedString:
1684-
return arg, err
16851697
case CellTypeFormula:
1686-
if value != "" {
1687-
return arg, err
1698+
if value == "" {
1699+
arg = newEmptyFormulaArg()
16881700
}
1689-
return newEmptyFormulaArg(), err
16901701
case CellTypeDate:
16911702
if value, err = f.GetCellValue(sheet, cell); err == nil {
16921703
if num := newStringFormulaArg(value).ToNumber(); num.Type == ArgNumber {
1693-
return num, err
1704+
arg = num
16941705
}
16951706
}
1696-
return arg, err
16971707
default:
1698-
return newErrorFormulaArg(value, value), err
1708+
arg = newErrorFormulaArg(value, value)
16991709
}
1710+
f.formulaArgCache.Store(ref, arg)
1711+
return arg, err
17001712
}
17011713

17021714
// rangeResolver extract value as string from given reference and range list.
@@ -1735,13 +1747,39 @@ func (f *File) rangeResolver(ctx *calcContext, cellRefs, cellRanges *list.List)
17351747
return
17361748
}
17371749

1750+
// Detect whole column/row reference, limit to actual data range
1751+
if valueRange[1] == TotalRows {
1752+
actualMaxRow := 0
1753+
for _, rowData := range ws.SheetData.Row {
1754+
if rowData.R > actualMaxRow {
1755+
actualMaxRow = rowData.R
1756+
}
1757+
}
1758+
if actualMaxRow > 0 && actualMaxRow < TotalRows {
1759+
valueRange[1] = actualMaxRow
1760+
}
1761+
}
1762+
if valueRange[3] == MaxColumns {
1763+
actualMaxCol := 0
1764+
for _, rowData := range ws.SheetData.Row {
1765+
for _, cell := range rowData.C {
1766+
col, _, err := CellNameToCoordinates(cell.R)
1767+
if err == nil && col > actualMaxCol {
1768+
actualMaxCol = col
1769+
}
1770+
}
1771+
}
1772+
if actualMaxCol > 0 && actualMaxCol < MaxColumns {
1773+
valueRange[3] = actualMaxCol
1774+
}
1775+
}
1776+
17381777
for row := valueRange[0]; row <= valueRange[1]; row++ {
17391778
colMax := 0
17401779
if row <= len(ws.SheetData.Row) {
17411780
rowData := &ws.SheetData.Row[row-1]
17421781
colMax = min(valueRange[3], len(rowData.C))
17431782
}
1744-
17451783
var matrixRow []formulaArg
17461784
for col := valueRange[2]; col <= valueRange[3]; col++ {
17471785
value := newEmptyFormulaArg()

calc_test.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6474,6 +6474,20 @@ func TestCalcRangeResolver(t *testing.T) {
64746474
cellRefs.PushBack(cellRef{Col: 1, Row: TotalRows + 1, Sheet: "SheetN"})
64756475
_, err = f.rangeResolver(&calcContext{}, cellRefs, cellRanges)
64766476
assert.Equal(t, ErrMaxRows, err)
6477+
t.Run("for_range_resolver_error", func(t *testing.T) {
6478+
f := NewFile()
6479+
assert.NoError(t, f.SetCellValue("Sheet1", "A1", "test"))
6480+
cellRefs := list.New()
6481+
cellRanges := list.New()
6482+
cellRanges.PushBack(cellRange{
6483+
From: cellRef{Col: 1, Row: 1, Sheet: "Sheet1"},
6484+
To: cellRef{Col: 1, Row: 1, Sheet: "Sheet1"},
6485+
})
6486+
f.SharedStrings = nil
6487+
f.Pkg.Store(defaultXMLPathSharedStrings, MacintoshCyrillicCharset)
6488+
_, err := f.rangeResolver(&calcContext{}, cellRefs, cellRanges)
6489+
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
6490+
})
64776491
}
64786492

64796493
func TestCalcBahttextAppendDigit(t *testing.T) {

cell.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ func (c *xlsxC) hasValue() bool {
182182

183183
// removeFormula delete formula for the cell.
184184
func (f *File) removeFormula(c *xlsxC, ws *xlsxWorksheet, sheet string) error {
185-
f.calcCache.Clear()
185+
f.clearCalcCache()
186186
if c.F != nil && c.Vm == nil {
187187
sheetID := f.getSheetID(sheet)
188188
if err := f.deleteCalcChain(sheetID, c.R); err != nil {
@@ -795,7 +795,7 @@ func (f *File) SetCellFormula(sheet, cell, formula string, opts ...FormulaOpts)
795795
if err != nil {
796796
return err
797797
}
798-
f.calcCache.Clear()
798+
f.clearCalcCache()
799799
if formula == "" {
800800
ws.deleteSharedFormula(c)
801801
c.F = nil
@@ -1371,7 +1371,7 @@ func (f *File) SetCellRichText(sheet, cell string, runs []RichTextRun) error {
13711371
if si.R, err = setRichText(runs); err != nil {
13721372
return err
13731373
}
1374-
f.calcCache.Clear()
1374+
f.clearCalcCache()
13751375
for idx, strItem := range sst.SI {
13761376
if reflect.DeepEqual(strItem, si) {
13771377
c.T, c.V = "s", strconv.Itoa(idx)

excelize.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ type File struct {
4242
tempFiles sync.Map
4343
xmlAttr sync.Map
4444
calcCache sync.Map
45+
formulaArgCache sync.Map
4546
CalcChain *xlsxCalcChain
4647
CharsetReader func(charset string, input io.Reader) (rdr io.Reader, err error)
4748
Comments map[string]*xlsxComments

merge.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ func (f *File) UnmergeCell(sheet, topLeftCell, bottomRightCell string) error {
115115
if err = ws.mergeOverlapCells(); err != nil {
116116
return err
117117
}
118-
f.calcCache.Clear()
118+
f.clearCalcCache()
119119
i := 0
120120
for _, mergeCell := range ws.MergeCells.Cells {
121121
if rect2, _ := rangeRefToCoordinates(mergeCell.Ref); isOverlap(rect1, rect2) {

pivotTable.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ func (f *File) AddPivotTable(opts *PivotTableOptions) error {
163163
if err != nil {
164164
return err
165165
}
166-
f.calcCache.Clear()
166+
f.clearCalcCache()
167167
pivotTableID := f.countPivotTables() + 1
168168
pivotCacheID := f.countPivotCache() + 1
169169

@@ -1062,7 +1062,7 @@ func (f *File) DeletePivotTable(sheet, name string) error {
10621062
if err != nil {
10631063
return err
10641064
}
1065-
f.calcCache.Clear()
1065+
f.clearCalcCache()
10661066
pivotTableCaches := map[string]int{}
10671067
pivotTables, _ := f.getPivotTables()
10681068
for _, sheetPivotTables := range pivotTables {

sheet.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -384,7 +384,7 @@ func (f *File) SetSheetName(source, target string) error {
384384
if target == source {
385385
return err
386386
}
387-
f.calcCache.Clear()
387+
f.clearCalcCache()
388388
wb, _ := f.workbookReader()
389389
for k, v := range wb.Sheets.Sheet {
390390
if v.Name == source {
@@ -580,7 +580,7 @@ func (f *File) DeleteSheet(sheet string) error {
580580
if idx, _ := f.GetSheetIndex(sheet); f.SheetCount == 1 || idx == -1 {
581581
return nil
582582
}
583-
f.calcCache.Clear()
583+
f.clearCalcCache()
584584
wb, _ := f.workbookReader()
585585
wbRels, _ := f.relsReader(f.getWorkbookRelsPath())
586586
activeSheetName := f.GetSheetName(f.GetActiveSheetIndex())
@@ -769,7 +769,7 @@ func (f *File) copySheet(from, to int) error {
769769
if err != nil {
770770
return err
771771
}
772-
f.calcCache.Clear()
772+
f.clearCalcCache()
773773
worksheet := &xlsxWorksheet{}
774774
deepcopy.Copy(worksheet, sheet)
775775
toSheetID := strconv.Itoa(f.getSheetID(f.GetSheetName(to)))
@@ -1773,7 +1773,7 @@ func (f *File) SetDefinedName(definedName *DefinedName) error {
17731773
if err != nil {
17741774
return err
17751775
}
1776-
f.calcCache.Clear()
1776+
f.clearCalcCache()
17771777
d := xlsxDefinedName{
17781778
Name: definedName.Name,
17791779
Comment: definedName.Comment,
@@ -1816,7 +1816,7 @@ func (f *File) DeleteDefinedName(definedName *DefinedName) error {
18161816
if err != nil {
18171817
return err
18181818
}
1819-
f.calcCache.Clear()
1819+
f.clearCalcCache()
18201820
if wb.DefinedNames != nil {
18211821
for idx, dn := range wb.DefinedNames.DefinedName {
18221822
scope := "Workbook"

table.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ func (f *File) AddTable(sheet string, table *Table) error {
118118
return err
119119
}
120120
f.addSheetNameSpace(sheet, SourceRelationship)
121-
f.calcCache.Clear()
121+
f.clearCalcCache()
122122
if err = f.addTable(sheet, tableXML, coordinates[0], coordinates[1], coordinates[2], coordinates[3], tableID, options); err != nil {
123123
return err
124124
}
@@ -178,7 +178,7 @@ func (f *File) DeleteTable(name string) error {
178178
if err != nil {
179179
return err
180180
}
181-
f.calcCache.Clear()
181+
f.clearCalcCache()
182182
for sheet, tables := range tbls {
183183
for _, table := range tables {
184184
if table.Name != name {

0 commit comments

Comments
 (0)