Skip to content

Commit c99bddc

Browse files
swmalswmalJanKallman
authored
Fixes for various issues with LAMBDA/LET. (#2330)
* #2329 - improved handling of depdendency chain/formula stack in Lambda calculations * Modified unit test * modified unittest * #2329 - fixed issue with expression caching and lambda/variable expressions * Remove test s1031 Removed the s1031 test method that was commented out and not in use. --------- Co-authored-by: swmal <{ID}+username}@users.noreply.github.com> Co-authored-by: Jan Källman <jan.kallman@epplussoftware.com>
1 parent e9960cd commit c99bddc

File tree

8 files changed

+89
-43
lines changed

8 files changed

+89
-43
lines changed

src/EPPlus/FormulaParsing/DependencyChain/RpnFormula.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ internal enum FormulaFlags : short
3434
{
3535
IsDynamic = 1,
3636
IsAlwaysDynamic = 2,
37+
IsLambda = 4,
3738
}
3839
internal class RpnFormula
3940
{
@@ -52,6 +53,9 @@ internal class RpnFormula
5253
internal int _arrayIndex = -1;
5354
internal FormulaFlags _flags = 0;
5455
internal FunctionExpression _currentFunction = null;
56+
// saves the initial formula stack count when executing a lambda expression
57+
// to avoid popping formulas pushed before the lambda expression was invoked.
58+
internal int _lambdaFormulaStackCount = 0;
5559
private VariableStorageManager _variableStorage;
5660

5761
public bool CanBeDynamicArray
@@ -62,6 +66,14 @@ public bool CanBeDynamicArray
6266
}
6367
}
6468

69+
public bool IsLambda
70+
{
71+
get
72+
{
73+
return (_flags & FormulaFlags.IsLambda) == FormulaFlags.IsLambda;
74+
}
75+
}
76+
6577
public bool IgnoreCaching
6678
{
6779
get; set;

src/EPPlus/FormulaParsing/DependencyChain/RpnFormulaExecution.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -516,7 +516,7 @@ private static CompileResult CalculateFormulaChain(RpnOptimizedDependencyChain d
516516
cr = f._expressionStack.Pop().Compile();
517517
}
518518

519-
if (cr != null && (writeToCell || depChain._formulaStack.Count > 0)) // If calculating single cell via the FormulaParser.Parse method we should not write to the cells
519+
if (cr != null && f.IsLambda == false && (writeToCell || depChain._formulaStack.Count > 0)) // If calculating single cell via the FormulaParser.Parse method we should not write to the cells
520520
{
521521
SetValueToWorkbook(depChain, f, rd, cr, options, ref depChainPos);
522522
}
@@ -526,7 +526,7 @@ private static CompileResult CalculateFormulaChain(RpnOptimizedDependencyChain d
526526
depChain._parsingContext.Parser.Logger.Log($"Set value in Cell\t{f.GetAddress()}\t{cr.ResultValue}\t{cr.DataType}");
527527
}
528528

529-
if (depChain._formulaStack.Count > 0)
529+
if (depChain._formulaStack.Count > f._lambdaFormulaStackCount)
530530
{
531531
f = depChain._formulaStack.Pop();
532532
if (f._formulaEnumerator == null)

src/EPPlus/FormulaParsing/Excel/Functions/RefAndLookup/Hstack.cs

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -25,28 +25,16 @@ namespace OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup
2525
EPPlusVersion = "7",
2626
Description = "Combines arrays horizontally into a single array.",
2727
SupportsArrays = true)]
28-
internal class Hstack : ExcelFunction
28+
internal class Hstack : StackFunctionBase
2929
{
30-
public override string NamespacePrefix => "_xlfn.";
31-
32-
public override int ArgumentMinLength => 1;
3330
public override bool ExecutesLambda => true;
3431

3532
public override CompileResult Execute(IList<FunctionArgument> arguments, ParsingContext context)
3633
{
37-
var ranges = new List<IRangeInfo>();
38-
foreach (var arg in arguments)
34+
var ranges = GetRanges(arguments, out ExcelErrorValue err);
35+
if (err != null)
3936
{
40-
if (!arg.IsExcelRange)
41-
{
42-
var rng = new InMemoryRange(1, 1);
43-
rng.SetValue(0, 0, arg.Value);
44-
ranges.Add(rng);
45-
}
46-
else
47-
{
48-
ranges.Add(arg.ValueAsRangeInfo);
49-
}
37+
return CreateDynamicArrayResult(err, DataType.ExcelError);
5038
}
5139
var nRows = ranges.Max(x => x.Size.NumberOfRows);
5240
var nCols = Convert.ToInt16(ranges.Sum(x => x.Size.NumberOfCols));
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*************************************************************************************************
2+
Required Notice: Copyright (C) EPPlus Software AB.
3+
This software is licensed under PolyForm Noncommercial License 1.0.0
4+
and may only be used for noncommercial purposes
5+
https://polyformproject.org/licenses/noncommercial/1.0.0/
6+
7+
A commercial license to use this software can be purchased at https://epplussoftware.com
8+
*************************************************************************************************
9+
Date Author Change
10+
*************************************************************************************************
11+
22/3/2025 EPPlus Software AB EPPlus v8
12+
*************************************************************************************************/
13+
using OfficeOpenXml.FormulaParsing.FormulaExpressions;
14+
using OfficeOpenXml.FormulaParsing.Ranges;
15+
using System;
16+
using System.Collections.Generic;
17+
using System.Linq;
18+
using System.Text;
19+
20+
namespace OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup
21+
{
22+
internal abstract class StackFunctionBase : ExcelFunction
23+
{
24+
public override string NamespacePrefix => "_xlfn.";
25+
26+
public override int ArgumentMinLength => 1;
27+
28+
protected List<IRangeInfo> GetRanges(IEnumerable<FunctionArgument> arguments, out ExcelErrorValue err)
29+
{
30+
err = default;
31+
var ranges = new List<IRangeInfo>();
32+
foreach (var arg in arguments)
33+
{
34+
if (arg.Value is not IRangeInfo)
35+
{
36+
var rng = new InMemoryRange(1, 1);
37+
rng.SetValue(0, 0, arg.Value);
38+
ranges.Add(rng);
39+
}
40+
else
41+
{
42+
var r = arg.ValueAsRangeInfo;
43+
if (r == null)
44+
{
45+
err = ErrorValues.ValueError;
46+
break;
47+
}
48+
ranges.Add(r);
49+
}
50+
}
51+
return ranges;
52+
}
53+
}
54+
}

src/EPPlus/FormulaParsing/Excel/Functions/RefAndLookup/Vstack.cs

Lines changed: 4 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,8 @@ Date Author Change
1313
using OfficeOpenXml.FormulaParsing.Excel.Functions.Metadata;
1414
using OfficeOpenXml.FormulaParsing.FormulaExpressions;
1515
using OfficeOpenXml.FormulaParsing.Ranges;
16-
using System;
1716
using System.Collections.Generic;
1817
using System.Linq;
19-
using System.Text;
2018

2119
namespace OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup
2220
{
@@ -25,31 +23,14 @@ namespace OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup
2523
EPPlusVersion = "7",
2624
Description = "Combines arrays vertically into a single array.",
2725
SupportsArrays = true)]
28-
internal class Vstack : ExcelFunction
26+
internal class Vstack : StackFunctionBase
2927
{
30-
public override string NamespacePrefix => "_xlfn.";
31-
32-
public override int ArgumentMinLength => 1;
3328
public override CompileResult Execute(IList<FunctionArgument> arguments, ParsingContext context)
3429
{
35-
var ranges = new List<IRangeInfo>();
36-
foreach(var arg in arguments)
30+
var ranges = GetRanges(arguments, out ExcelErrorValue err);
31+
if (err != null)
3732
{
38-
if(!arg.IsExcelRange)
39-
{
40-
var rng = new InMemoryRange(1, 1);
41-
rng.SetValue(0, 0, arg.Value);
42-
ranges.Add(rng);
43-
}
44-
else
45-
{
46-
var r = arg.ValueAsRangeInfo;
47-
if(r==null)
48-
{
49-
return CreateDynamicArrayResult(ErrorValues.ValueError, DataType.ExcelError);
50-
}
51-
ranges.Add(r);
52-
}
33+
return CreateDynamicArrayResult(err, DataType.ExcelError);
5334
}
5435
var nRows = ranges.Sum(x => x.Size.NumberOfRows);
5536
var nCols = ranges.Max(x => x.Size.NumberOfCols);

src/EPPlus/FormulaParsing/FormulaExpressions/FunctionExpression.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,13 @@ internal string GetExpressionKey(RpnFormula f)
229229
key.Append(f._tokens[i].Value);
230230
}
231231
}
232+
else if(e.ExpressionType==ExpressionType.Variable ||
233+
e.ExpressionType==ExpressionType.LambdaVariableDeclaration ||
234+
e.ExpressionType == ExpressionType.LambdaCalculation ||
235+
e.ExpressionType == ExpressionType.LambdaInvoke)
236+
{
237+
return null;
238+
}
232239
else
233240
{
234241
var fa = e.GetAddress();

src/EPPlus/FormulaParsing/FormulaExpressions/LambdaCalculator.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,12 +202,14 @@ public CompileResult Execute(ParsingContext ctx)
202202
formula.IgnoreCaching = true;
203203
formula.ExpressionStack = _formula.ExpressionStack;
204204
formula.FunctionStack = _formula.FunctionStack;
205+
formula._flags = _formula._flags | FormulaFlags.IsLambda;
206+
formula._lambdaFormulaStackCount = ctx.DependencyChain._formulaStack.Count;
205207
var rpnTokens = new RpnTokens { Tokens = _currentTokens, Scope = _scope };
206208
formula.SetTokens(rpnTokens, ctx, _scope);
207209
// SetTokens clears the variable storage...
208210
ctx.VariableStorage.Push(_scope);
209211
var chain = ctx.DependencyChain;
210-
var compileResult = RpnFormulaExecution.ExecutePartialFormula(chain, formula, ctx.CalcOption, false);
212+
var compileResult = RpnFormulaExecution.ExecutePartialFormula(chain, formula, ctx.CalcOption, true);
211213
return CompileResultFactory.CreateDynamicArrayResult(compileResult.Result, compileResult.Address, CompileResultType.DynamicArray_AlwaysSetCellAsDynamic);
212214
}
213215

src/EPPlus/FormulaParsing/FormulaExpressions/LambdaEtaExpression.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,10 @@ public override CompileResult Compile()
8383
ExpressionStack = _rpnFormula.ExpressionStack,
8484
FunctionStack = _rpnFormula.FunctionStack
8585
};
86+
rpnFormula._flags = _rpnFormula._flags | FormulaFlags.IsLambda;
87+
rpnFormula._lambdaFormulaStackCount = Context.DependencyChain._formulaStack.Count;
8688
rpnFormula.SetTokens(rpnTokens, Context, _scope);
87-
var result = RpnFormulaExecution.ExecutePartialFormula(Context.DependencyChain, rpnFormula, Context.CalcOption, false);
89+
var result = RpnFormulaExecution.ExecutePartialFormula(Context.DependencyChain, rpnFormula, Context.CalcOption, true);
8890
if(result.DataType != DataType.LambdaCalculation)
8991
{
9092
return CompileResult.GetErrorResult(eErrorType.Value);

0 commit comments

Comments
 (0)