Skip to content

Commit 6629fbf

Browse files
authored
Fixes array formula calculation for single cells containing range operations (#1657)
* Fixes array formula calculation for single cell arrays. #1649 * Range operations using conditional operators are now identified as dynamic array formulas * Replaced ApplyWithDynamicResult method with Apply * Minor changes to Update of table array property
1 parent 429f2f2 commit 6629fbf

File tree

7 files changed

+207
-142
lines changed

7 files changed

+207
-142
lines changed

src/EPPlus/FormulaParsing/DependencyChain/RpnFormulaExecution.cs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,47 @@ private static void ExecuteChain(RpnOptimizedDependencyChain depChain, ExcelRang
128128
}
129129
}
130130
}
131+
132+
if (depChain.HasAnyArrayFormula) //Array formulas has been update. Check if we need to set the array flag on any calculated tables on intersecting tables.
133+
{
134+
UpdateTableArrayFlag(range);
135+
}
136+
}
137+
138+
private static void UpdateTableArrayFlag(ExcelRangeBase range)
139+
{
140+
//Check table formulas that needs the array flag updated for the columns formulas.
141+
foreach (var table in range.Worksheet.Tables)
142+
{
143+
if (table.Address.Collide(range) != eAddressCollition.No)
144+
{
145+
foreach (var c in table.Columns)
146+
{
147+
if (string.IsNullOrEmpty(c.CalculatedColumnFormula) == false)
148+
{
149+
var ca = c.DataAddress;
150+
if (ca.Collide(range) != eAddressCollition.No)
151+
{
152+
var ma = ca.Intersect(range);
153+
c.IsCalculatedFormulaArray = IsCFArray(range.Worksheet, ma);
154+
}
155+
}
156+
}
157+
}
158+
}
159+
}
160+
161+
private static bool IsCFArray(ExcelWorksheet ws, ExcelAddressBase ma)
162+
{
163+
for(int row=ma._fromRow;row<=ma._toRow; row++)
164+
{
165+
var f=(CellFlags)ws._flags.GetValue(row, ma._fromCol);
166+
if((f & CellFlags.ArrayFormula)!=0)
167+
{
168+
return true;
169+
}
170+
}
171+
return false;
131172
}
132173

133174
private static object SetAndReturnValueError(RpnOptimizedDependencyChain depChain, Exception ex, RpnFormula f)
@@ -581,6 +622,7 @@ private static void SetValueToWorkbook(RpnOptimizedDependencyChain depChain, Rpn
581622
if (f._arrayIndex >= 0 && (f._flags & FormulaFlags.IsDynamic) == 0) //A legacy array formula, Fill the referenced range.
582623
{
583624
ArrayFormulaOutput.FillArrayFromRangeInfo(f, ri, rd, depChain);
625+
depChain.HasAnyArrayFormula = true;
584626
}
585627
else
586628
{
@@ -611,6 +653,7 @@ private static void SetValueToWorkbook(RpnOptimizedDependencyChain depChain, Rpn
611653
{
612654
RecalculateDirtyCells(dirtyRange, depChain, rd);
613655
}
656+
depChain.HasAnyArrayFormula = true;
614657
}
615658
else
616659
{
@@ -1111,6 +1154,10 @@ private static void ApplyOperator(ParsingContext context, Token opToken, RpnForm
11111154
if (OperatorsDict.Instance.TryGetValue(opToken.Value, out IOperator op))
11121155
{
11131156
var result = op.Apply(c2, c1, context);
1157+
if (result.ResultType == CompileResultType.DynamicArray_AlwaysSetCellAsDynamic)
1158+
{
1159+
f._flags |= FormulaFlags.IsAllwaysDynamic;
1160+
}
11141161
PushResult(context, f, result);
11151162
}
11161163
}

src/EPPlus/FormulaParsing/DependencyChain/RpnOptimizedDependencyChain.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ internal class RpnOptimizedDependencyChain
2222
internal List<int> _startOfChain = new List<int>();
2323
internal bool HasDynamicArrayFormula=false;
2424
internal Dictionary<int, Dictionary<string, CompileResult>> _expressionCache = new Dictionary<int, Dictionary<string, CompileResult>>();
25+
internal bool HasAnyArrayFormula { get; set; } = false;
2526
public RpnOptimizedDependencyChain(ExcelWorkbook wb, ExcelCalculationOption options)
2627
{
2728
_tokenizer = SourceCodeTokenizer.Default;

src/EPPlus/FormulaParsing/Excel/Operators/Operator.cs

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -525,32 +525,6 @@ public static IOperator LessThanOrEqual
525525
}
526526
}
527527

528-
//private static IOperator _percent;
529-
//public static IOperator Percent
530-
//{
531-
// get
532-
// {
533-
// if (_percent == null)
534-
// {
535-
// _percent = new Operator(Operators.Percent, PrecedencePercent, (l, r, ctx) =>
536-
// {
537-
// l = l ?? CompileResult.ZeroInt;
538-
// r = r ?? CompileResult.ZeroInt;
539-
// if (l.DataType == DataType.Integer && r.DataType == DataType.Integer)
540-
// {
541-
// return new CompileResult(l.ResultNumeric * r.ResultNumeric, DataType.Integer);
542-
// }
543-
// else if (CanDoNumericOperation(l, r))
544-
// {
545-
// return new CompileResult(l.ResultNumeric * r.ResultNumeric, DataType.Decimal);
546-
// }
547-
// return new CompileResult(eErrorType.Value);
548-
// });
549-
// }
550-
// return _percent;
551-
// }
552-
//}
553-
554528
private static object GetObjFromOther(CompileResult obj, CompileResult other)
555529
{
556530
if (obj.Result == null)

src/EPPlus/FormulaParsing/Excel/Operators/RangeOperationsOperator.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -265,21 +265,21 @@ private static bool AddressIsNotAvailable(RangeDefinition lSize, RangeDefinition
265265

266266
public static CompileResult Apply(CompileResult left, CompileResult right, Operators op, ParsingContext context)
267267
{
268-
if(left.DataType == DataType.ExcelRange && right.DataType != DataType.ExcelRange)
268+
if (left.DataType == DataType.ExcelRange && right.DataType != DataType.ExcelRange)
269269
{
270270
InMemoryRange resultRange = ApplySingleValueRight(left, right, op, context);
271-
return new AddressCompileResult(resultRange, DataType.ExcelRange, resultRange.Address);
271+
return new DynamicArrayCompileResult(resultRange, DataType.ExcelRange, resultRange.Address, CompileResultType.DynamicArray_AlwaysSetCellAsDynamic);
272272
}
273-
else if(left.DataType != DataType.ExcelRange && right.DataType == DataType.ExcelRange)
273+
else if (left.DataType != DataType.ExcelRange && right.DataType == DataType.ExcelRange)
274274
{
275275
InMemoryRange resultRange = ApplySingleValueLeft(left, right, op, context);
276-
return new AddressCompileResult(resultRange, DataType.ExcelRange, resultRange.Address);
276+
return new DynamicArrayCompileResult(resultRange, DataType.ExcelRange, resultRange.Address, CompileResultType.DynamicArray_AlwaysSetCellAsDynamic);
277277
}
278-
if(left.DataType == DataType.ExcelRange && right.DataType == DataType.ExcelRange)
278+
if (left.DataType == DataType.ExcelRange && right.DataType == DataType.ExcelRange)
279279
{
280280
var interSectAddress = left.Address?.GetIntersectingRowOrColumns(right.Address);
281281
InMemoryRange resultRange = ApplyRanges(left, right, op, context, interSectAddress);
282-
return new AddressCompileResult(resultRange, DataType.ExcelRange, interSectAddress);
282+
return new DynamicArrayCompileResult(resultRange, DataType.ExcelRange, interSectAddress, CompileResultType.DynamicArray_AlwaysSetCellAsDynamic);
283283
}
284284
return CompileResult.Empty;
285285
}

src/EPPlus/Table/ExcelTableColumn.cs

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,6 @@ public string DataCellStyleName
200200
}
201201
}
202202
}
203-
internal const string CALCULATEDCOLUMNFORMULA_PATH = "d:calculatedColumnFormula";
204203

205204
ExcelTableSlicer _slicer = null;
206205
/// <summary>
@@ -244,6 +243,7 @@ public ExcelTableSlicer AddSlicer()
244243
{
245244
return _tbl.WorkSheet.Drawings.AddTableSlicer(this);
246245
}
246+
const string CALCULATEDCOLUMNFORMULA_PATH = "d:calculatedColumnFormula";
247247
/// <summary>
248248
/// Sets a calculated column Formula.
249249
/// Be carefull with this property since it is not validated.
@@ -274,6 +274,35 @@ public string CalculatedColumnFormula
274274
}
275275
}
276276
}
277+
const string CALCULATEDCOLUMNFORMULA_ARRAY_PATH = CALCULATEDCOLUMNFORMULA_PATH + "/@array";
278+
/// <summary>
279+
/// If the calculated formula is an array formula.
280+
/// This property will be set if the formula calculation evaluate the formula as an array formula.
281+
/// See <see cref="CalculationExtension.Calculate(ExcelWorkbook)"/>
282+
/// </summary>
283+
/// <exception cref="InvalidOperationException">If the <see cref="CalculatedColumnFormula"></see> is null or empty.</exception>
284+
public bool? IsCalculatedFormulaArray
285+
{
286+
get
287+
{
288+
return GetXmlNodeBoolNullable(CALCULATEDCOLUMNFORMULA_ARRAY_PATH);
289+
}
290+
set
291+
{
292+
if (value.HasValue)
293+
{
294+
if (string.IsNullOrEmpty(CalculatedColumnFormula))
295+
{
296+
throw new InvalidOperationException($"IsCalculatedFormulaArray: No formula set on column {Name}");
297+
}
298+
SetXmlNodeBool(CALCULATEDCOLUMNFORMULA_ARRAY_PATH, value.Value);
299+
}
300+
else
301+
{
302+
DeleteNode(CALCULATEDCOLUMNFORMULA_ARRAY_PATH);
303+
}
304+
}
305+
}
277306
internal void SetFormula(string formula)
278307
{
279308
SetXmlNodeString(CALCULATEDCOLUMNFORMULA_PATH, formula);
@@ -293,6 +322,17 @@ public ExcelTable Table
293322
return _tbl;
294323
}
295324
}
325+
326+
internal ExcelAddressBase DataAddress
327+
{
328+
get
329+
{
330+
var c = _tbl.Address._fromCol + Position;
331+
return new ExcelAddressBase(_tbl.ShowHeader ? _tbl.Address._fromRow+1: _tbl.Address._fromRow, c, _tbl.ShowTotal ? _tbl.Address._toRow-1 : _tbl.Address._toRow, c);
332+
333+
}
334+
}
335+
296336
internal void SetTableFormula(bool clear)
297337
{
298338
int fromRow = _tbl.ShowHeader ? _tbl.Address._fromRow + 1 : _tbl.Address._fromRow;

0 commit comments

Comments
 (0)