diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 98d38fb0f6..8f670922dc 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -6,7 +6,7 @@ jobs: strategy: matrix: go-version: [1.24.x, 1.25.x] - os: [ubuntu-24.04, macos-13, windows-latest] + os: [ubuntu-24.04, macos-15-intel, windows-latest] targetplatform: [x86, x64] runs-on: ${{ matrix.os }} diff --git a/adjust.go b/adjust.go index c07862e08c..05695736f9 100644 --- a/adjust.go +++ b/adjust.go @@ -75,7 +75,7 @@ func (f *File) adjustHelper(sheet string, dir adjustDirection, num, offset int) if err != nil { return err } - f.calcCache.Clear() + f.clearCalcCache() sheetID := f.getSheetID(sheet) if dir == rows { err = f.adjustRowDimensions(sheet, ws, num, offset) diff --git a/calc.go b/calc.go index 120413d974..96bfdf11d7 100644 --- a/calc.go +++ b/calc.go @@ -193,7 +193,8 @@ var ( return fmt.Sprintf("R[%d]C[%d]", row, col), nil }, } - formulaFormats = []*regexp.Regexp{ + formulaFnNameReplacer = strings.NewReplacer("_xlfn.", "", ".", "dot") + formulaFormats = []*regexp.Regexp{ regexp.MustCompile(`^(\d+)$`), regexp.MustCompile(`^=(.*)$`), regexp.MustCompile(`^<>(.*)$`), @@ -839,8 +840,8 @@ type formulaFuncs struct { // Z.TEST // ZTEST func (f *File) CalcCellValue(sheet, cell string, opts ...Options) (result string, err error) { - cacheKey := fmt.Sprintf("%s!%s", sheet, cell) - if cachedResult, found := f.calcCache.Load(cacheKey); found { + entry := sheet + "!" + cell + if cachedResult, ok := f.calcCache.Load(entry); ok { return cachedResult.(string), nil } options := f.getOptions(opts...) @@ -850,7 +851,7 @@ func (f *File) CalcCellValue(sheet, cell string, opts ...Options) (result string token formulaArg ) if token, err = f.calcCellValue(&calcContext{ - entry: fmt.Sprintf("%s!%s", sheet, cell), + entry: entry, maxCalcIterations: options.MaxCalcIterations, iterations: make(map[string]uint), iterationsCache: make(map[string]formulaArg), @@ -866,7 +867,7 @@ func (f *File) CalcCellValue(sheet, cell string, opts ...Options) (result string if precision > 15 { result, err = f.formattedValue(&xlsxC{S: styleIdx, V: strings.ToUpper(strconv.FormatFloat(decimal, 'G', 15, 64))}, rawCellValue, CellTypeNumber) if err == nil { - f.calcCache.Store(cacheKey, result) + f.calcCache.Store(entry, result) } return } @@ -874,17 +875,23 @@ func (f *File) CalcCellValue(sheet, cell string, opts ...Options) (result string result, err = f.formattedValue(&xlsxC{S: styleIdx, V: strings.ToUpper(strconv.FormatFloat(decimal, 'f', -1, 64))}, rawCellValue, CellTypeNumber) } if err == nil { - f.calcCache.Store(cacheKey, result) + f.calcCache.Store(entry, result) } return } result, err = f.formattedValue(&xlsxC{S: styleIdx, V: token.Value()}, rawCellValue, CellTypeInlineString) if err == nil { - f.calcCache.Store(cacheKey, result) + f.calcCache.Store(entry, result) } return } +// clearCalcCache clear all calculation related caches. +func (f *File) clearCalcCache() { + f.calcCache.Clear() + f.formulaArgCache.Clear() +} + // calcCellValue calculate cell value by given context, worksheet name and cell // reference. 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 } prepareEvalInfixExp(opfStack, opftStack, opfdStack, argsStack) // call formula function to evaluate - arg := callFuncByName(&formulaFuncs{f: f, sheet: sheet, cell: cell, ctx: ctx}, strings.NewReplacer( - "_xlfn.", "", ".", "dot").Replace(opfStack.Peek().(efp.Token).TValue), + arg := callFuncByName(&formulaFuncs{f: f, sheet: sheet, cell: cell, ctx: ctx}, + formulaFnNameReplacer.Replace(opfStack.Peek().(efp.Token).TValue), []reflect.Value{reflect.ValueOf(argsStack.Peek().(*list.List))}) if arg.Type == ArgError && opfStack.Len() == 1 { return arg @@ -1651,7 +1658,11 @@ func (f *File) cellResolver(ctx *calcContext, sheet, cell string) (formulaArg, e value string err error ) - ref := fmt.Sprintf("%s!%s", sheet, cell) + ref := sheet + "!" + cell + if cached, ok := f.formulaArgCache.Load(ref); ok { + return cached.(formulaArg), err + } + if formula, _ := f.getCellFormula(sheet, cell, true); len(formula) != 0 { ctx.mu.Lock() if ctx.entry != ref { @@ -1660,6 +1671,7 @@ func (f *File) cellResolver(ctx *calcContext, sheet, cell string) (formulaArg, e ctx.mu.Unlock() arg, _ = f.calcCellValue(ctx, sheet, cell) ctx.iterationsCache[ref] = arg + f.formulaArgCache.Store(ref, arg) return arg, nil } ctx.mu.Unlock() @@ -1674,29 +1686,29 @@ func (f *File) cellResolver(ctx *calcContext, sheet, cell string) (formulaArg, e cellType, _ := f.GetCellType(sheet, cell) switch cellType { case CellTypeBool: - return arg.ToBool(), err + arg = arg.ToBool() case CellTypeNumber, CellTypeUnset: if arg.Value() == "" { - return newEmptyFormulaArg(), err + arg = newEmptyFormulaArg() + } else { + arg = arg.ToNumber() } - return arg.ToNumber(), err case CellTypeInlineString, CellTypeSharedString: - return arg, err case CellTypeFormula: - if value != "" { - return arg, err + if value == "" { + arg = newEmptyFormulaArg() } - return newEmptyFormulaArg(), err case CellTypeDate: if value, err = f.GetCellValue(sheet, cell); err == nil { if num := newStringFormulaArg(value).ToNumber(); num.Type == ArgNumber { - return num, err + arg = num } } - return arg, err default: - return newErrorFormulaArg(value, value), err + arg = newErrorFormulaArg(value, value) } + f.formulaArgCache.Store(ref, arg) + return arg, err } // 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) return } + // Detect whole column/row reference, limit to actual data range + if valueRange[1] == TotalRows { + actualMaxRow := 0 + for _, rowData := range ws.SheetData.Row { + if rowData.R > actualMaxRow { + actualMaxRow = rowData.R + } + } + if actualMaxRow > 0 && actualMaxRow < TotalRows { + valueRange[1] = actualMaxRow + } + } + if valueRange[3] == MaxColumns { + actualMaxCol := 0 + for _, rowData := range ws.SheetData.Row { + for _, cell := range rowData.C { + col, _, err := CellNameToCoordinates(cell.R) + if err == nil && col > actualMaxCol { + actualMaxCol = col + } + } + } + if actualMaxCol > 0 && actualMaxCol < MaxColumns { + valueRange[3] = actualMaxCol + } + } + for row := valueRange[0]; row <= valueRange[1]; row++ { colMax := 0 if row <= len(ws.SheetData.Row) { rowData := &ws.SheetData.Row[row-1] colMax = min(valueRange[3], len(rowData.C)) } - var matrixRow []formulaArg for col := valueRange[2]; col <= valueRange[3]; col++ { value := newEmptyFormulaArg() diff --git a/calc_test.go b/calc_test.go index 65e1ffae4c..3d29afbfec 100644 --- a/calc_test.go +++ b/calc_test.go @@ -6474,6 +6474,20 @@ func TestCalcRangeResolver(t *testing.T) { cellRefs.PushBack(cellRef{Col: 1, Row: TotalRows + 1, Sheet: "SheetN"}) _, err = f.rangeResolver(&calcContext{}, cellRefs, cellRanges) assert.Equal(t, ErrMaxRows, err) + t.Run("for_range_resolver_error", func(t *testing.T) { + f := NewFile() + assert.NoError(t, f.SetCellValue("Sheet1", "A1", "test")) + cellRefs := list.New() + cellRanges := list.New() + cellRanges.PushBack(cellRange{ + From: cellRef{Col: 1, Row: 1, Sheet: "Sheet1"}, + To: cellRef{Col: 1, Row: 1, Sheet: "Sheet1"}, + }) + f.SharedStrings = nil + f.Pkg.Store(defaultXMLPathSharedStrings, MacintoshCyrillicCharset) + _, err := f.rangeResolver(&calcContext{}, cellRefs, cellRanges) + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") + }) } func TestCalcBahttextAppendDigit(t *testing.T) { diff --git a/cell.go b/cell.go index 3106108f16..54266bc73b 100644 --- a/cell.go +++ b/cell.go @@ -182,7 +182,7 @@ func (c *xlsxC) hasValue() bool { // removeFormula delete formula for the cell. func (f *File) removeFormula(c *xlsxC, ws *xlsxWorksheet, sheet string) error { - f.calcCache.Clear() + f.clearCalcCache() if c.F != nil && c.Vm == nil { sheetID := f.getSheetID(sheet) if err := f.deleteCalcChain(sheetID, c.R); err != nil { @@ -795,7 +795,7 @@ func (f *File) SetCellFormula(sheet, cell, formula string, opts ...FormulaOpts) if err != nil { return err } - f.calcCache.Clear() + f.clearCalcCache() if formula == "" { ws.deleteSharedFormula(c) c.F = nil @@ -1371,7 +1371,7 @@ func (f *File) SetCellRichText(sheet, cell string, runs []RichTextRun) error { if si.R, err = setRichText(runs); err != nil { return err } - f.calcCache.Clear() + f.clearCalcCache() for idx, strItem := range sst.SI { if reflect.DeepEqual(strItem, si) { c.T, c.V = "s", strconv.Itoa(idx) diff --git a/excelize.go b/excelize.go index 0d0a578c14..f6faa1c6e9 100644 --- a/excelize.go +++ b/excelize.go @@ -42,6 +42,7 @@ type File struct { tempFiles sync.Map xmlAttr sync.Map calcCache sync.Map + formulaArgCache sync.Map CalcChain *xlsxCalcChain CharsetReader func(charset string, input io.Reader) (rdr io.Reader, err error) Comments map[string]*xlsxComments diff --git a/merge.go b/merge.go index a7a60af554..cf77ad2b4b 100644 --- a/merge.go +++ b/merge.go @@ -115,7 +115,7 @@ func (f *File) UnmergeCell(sheet, topLeftCell, bottomRightCell string) error { if err = ws.mergeOverlapCells(); err != nil { return err } - f.calcCache.Clear() + f.clearCalcCache() i := 0 for _, mergeCell := range ws.MergeCells.Cells { if rect2, _ := rangeRefToCoordinates(mergeCell.Ref); isOverlap(rect1, rect2) { diff --git a/pivotTable.go b/pivotTable.go index c8cccd8173..fbf6aab7f6 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -163,7 +163,7 @@ func (f *File) AddPivotTable(opts *PivotTableOptions) error { if err != nil { return err } - f.calcCache.Clear() + f.clearCalcCache() pivotTableID := f.countPivotTables() + 1 pivotCacheID := f.countPivotCache() + 1 @@ -1062,7 +1062,7 @@ func (f *File) DeletePivotTable(sheet, name string) error { if err != nil { return err } - f.calcCache.Clear() + f.clearCalcCache() pivotTableCaches := map[string]int{} pivotTables, _ := f.getPivotTables() for _, sheetPivotTables := range pivotTables { diff --git a/sheet.go b/sheet.go index 69313ac091..e041705e95 100644 --- a/sheet.go +++ b/sheet.go @@ -384,7 +384,7 @@ func (f *File) SetSheetName(source, target string) error { if target == source { return err } - f.calcCache.Clear() + f.clearCalcCache() wb, _ := f.workbookReader() for k, v := range wb.Sheets.Sheet { if v.Name == source { @@ -580,7 +580,7 @@ func (f *File) DeleteSheet(sheet string) error { if idx, _ := f.GetSheetIndex(sheet); f.SheetCount == 1 || idx == -1 { return nil } - f.calcCache.Clear() + f.clearCalcCache() wb, _ := f.workbookReader() wbRels, _ := f.relsReader(f.getWorkbookRelsPath()) activeSheetName := f.GetSheetName(f.GetActiveSheetIndex()) @@ -769,7 +769,7 @@ func (f *File) copySheet(from, to int) error { if err != nil { return err } - f.calcCache.Clear() + f.clearCalcCache() worksheet := &xlsxWorksheet{} deepcopy.Copy(worksheet, sheet) toSheetID := strconv.Itoa(f.getSheetID(f.GetSheetName(to))) @@ -1773,7 +1773,7 @@ func (f *File) SetDefinedName(definedName *DefinedName) error { if err != nil { return err } - f.calcCache.Clear() + f.clearCalcCache() d := xlsxDefinedName{ Name: definedName.Name, Comment: definedName.Comment, @@ -1816,7 +1816,7 @@ func (f *File) DeleteDefinedName(definedName *DefinedName) error { if err != nil { return err } - f.calcCache.Clear() + f.clearCalcCache() if wb.DefinedNames != nil { for idx, dn := range wb.DefinedNames.DefinedName { scope := "Workbook" diff --git a/table.go b/table.go index 547fcbc896..8409f64253 100644 --- a/table.go +++ b/table.go @@ -118,7 +118,7 @@ func (f *File) AddTable(sheet string, table *Table) error { return err } f.addSheetNameSpace(sheet, SourceRelationship) - f.calcCache.Clear() + f.clearCalcCache() if err = f.addTable(sheet, tableXML, coordinates[0], coordinates[1], coordinates[2], coordinates[3], tableID, options); err != nil { return err } @@ -178,7 +178,7 @@ func (f *File) DeleteTable(name string) error { if err != nil { return err } - f.calcCache.Clear() + f.clearCalcCache() for sheet, tables := range tbls { for _, table := range tables { if table.Name != name {