Skip to content

Commit 1847c91

Browse files
authored
Fixes issues #2224 and #2226 (#2230)
1 parent 73cfad4 commit 1847c91

5 files changed

Lines changed: 192 additions & 8 deletions

File tree

src/EPPlus/ConditionalFormatting/ExcelConditionalFormattingHelper.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ public static double GetAttributeDouble(
202202
try
203203
{
204204
var value = node.Attributes[attribute].Value;
205-
return double.Parse(value, NumberStyles.Number, CultureInfo.InvariantCulture);
205+
return double.Parse(value, NumberStyles.Number | NumberStyles.AllowExponent, CultureInfo.InvariantCulture);
206206
}
207207
catch
208208
{

src/EPPlus/FormulaParsing/DependencyChain/RpnFormulaExecution.cs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,28 +10,29 @@ Date Author Change
1010
*************************************************************************************************
1111
05/14/2024 EPPlus Software AB Initial release EPPlus 7
1212
*************************************************************************************************/
13+
using OfficeOpenXml.CellPictures;
1314
using OfficeOpenXml.Core.CellStore;
1415
using OfficeOpenXml.Core.RangeQuadTree;
16+
using OfficeOpenXml.FormulaParsing.DependencyChain;
1517
using OfficeOpenXml.FormulaParsing.Excel.Functions;
1618
using OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup;
1719
using OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup.LookupUtils;
1820
using OfficeOpenXml.FormulaParsing.Excel.Operators;
1921
using OfficeOpenXml.FormulaParsing.Exceptions;
2022
using OfficeOpenXml.FormulaParsing.FormulaExpressions;
23+
using OfficeOpenXml.FormulaParsing.FormulaExpressions.VariableStorage;
2124
using OfficeOpenXml.FormulaParsing.LexicalAnalysis;
2225
using OfficeOpenXml.FormulaParsing.Ranges;
26+
using OfficeOpenXml.Utils.EnumUtils;
27+
using OfficeOpenXml.Utils.TypeConversion;
2328
using System;
2429
using System.Collections.Generic;
2530
using System.Linq;
2631
using System.Runtime.CompilerServices;
2732
using System.Threading;
33+
using System.Xml.Linq;
2834
using static OfficeOpenXml.ExcelAddressBase;
2935
using static OfficeOpenXml.ExcelWorksheet;
30-
using OfficeOpenXml.Utils.TypeConversion;
31-
using OfficeOpenXml.Utils.EnumUtils;
32-
using OfficeOpenXml.CellPictures;
33-
using OfficeOpenXml.FormulaParsing.DependencyChain;
34-
using OfficeOpenXml.FormulaParsing.FormulaExpressions.VariableStorage;
3536

3637
namespace OfficeOpenXml.FormulaParsing
3738
{
@@ -284,6 +285,7 @@ private static object ExecuteChain(RpnOptimizedDependencyChain depChain, ExcelWo
284285
try
285286
{
286287
var f = new RpnFormula(ws, cell.Row, cell.Column);
288+
depChain._parsingContext.CurrentCell = new FormulaCellAddress(ws?.Index??-1, -1, 0);
287289
f.SetFormula(formula, depChain);
288290
return AddChainForFormula(depChain, f, options, writeToCell).Result;
289291
}

src/EPPlus/FormulaParsing/FormulaExpressions/FormulaExecutor.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -385,7 +385,7 @@ public static Dictionary<int, Expression> CompileExpressions(ref LambdaFormulaSe
385385
{
386386
func = new FunctionExpression(t.Value, parsingContext, tokenIx);
387387
}
388-
else if (parsingContext.Package.Workbook.Names.ContainsKey(t.Value) || parsingContext.CurrentWorksheet.Names.ContainsKey(t.Value))
388+
else if (parsingContext.Package.Workbook.Names.ContainsKey(t.Value) || (parsingContext.CurrentWorksheet !=null && parsingContext.CurrentWorksheet.Names.ContainsKey(t.Value)))
389389
{
390390
var wbNames = parsingContext.Package.Workbook.Names;
391391
var name = wbNames.ContainsKey(t.Value) ? wbNames[t.Value] : null;
@@ -542,7 +542,7 @@ private static void ExtractArray(IList<Token> exps, int i, out IRangeInfo range,
542542
array.Add(int.Parse(t.Value));
543543
break;
544544
case TokenType.Decimal:
545-
array.Add(double.Parse(t.Value, NumberStyles.Number, CultureInfo.InvariantCulture));
545+
array.Add(double.Parse(t.Value, NumberStyles.Number | NumberStyles.AllowExponent, CultureInfo.InvariantCulture));
546546
break;
547547
case TokenType.StringContent:
548548
array.Add(t.Value.Substring(1, t.Value.Length-2).Replace("\"\"","\"")); //Remove double quotes.

src/EPPlusTest/Issues/DefinedNameIssues.cs

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using Microsoft.VisualStudio.TestTools.UnitTesting;
22
using OfficeOpenXml;
3+
using OfficeOpenXml.FormulaParsing;
34
using System;
45
using System.Collections.Generic;
56
using System.IO;
@@ -71,5 +72,184 @@ public void DefinedNamesQuoteError()
7172
SaveAndCleanup(package);
7273
}
7374
}
75+
[TestMethod]
76+
public void issue2224()
77+
{
78+
79+
var cases = new (string Name, string Description, Action<ExcelWorkbook, ExcelWorksheet> Setup)[]
80+
{
81+
(
82+
"SciArrayFormula",
83+
"Inline array literal containing scientific-notation constants stored in Formula",
84+
(workbook, sheet) =>
85+
{
86+
workbook.Names.AddFormula(
87+
"SciArrayFormula",
88+
"{4.02506300418233E-305,3.33761291040418E-308}");
89+
sheet.Cells["B1"].Formula = "SciArrayFormula";
90+
}
91+
),
92+
(
93+
"UndefinedUdfName",
94+
"Workbook-level name that references an undefined UDF (Main.SAPF4Help)",
95+
(workbook, sheet) =>
96+
{
97+
workbook.Names.AddFormula("SAPFuncF4Help", "Main.SAPF4Help()");
98+
}
99+
),
100+
(
101+
"CubeSetName",
102+
"Workbook-level name that uses CUBESET against ThisWorkbookDataModel",
103+
(workbook, sheet) =>
104+
{
105+
workbook.Names.AddFormula(
106+
"Slicer_PC_P210",
107+
"CUBESET(\"ThisWorkbookDataModel\",\"[DIM_PC].[PC_P2].&[RS]\",\"Slicer\")");
108+
}
109+
)
110+
};
111+
var i = 1;
112+
foreach (var (name, description, setup) in cases)
113+
{
114+
var xlsxFile = $"issue2224-{i++}.xlsx";
115+
using (var package = OpenPackage(xlsxFile, true))
116+
{
117+
var worksheet = package.Workbook.Worksheets.Add("Sheet1");
118+
worksheet.Cells["A1"].Value = 1;
119+
setup(package.Workbook, worksheet);
120+
SaveAndCleanup(package);
121+
}
122+
123+
using var reopened = new ExcelPackage(new FileInfo(xlsxFile));
124+
125+
Console.WriteLine($"Case: {name}");
126+
Console.WriteLine($" Description: {description}");
127+
128+
try
129+
{
130+
reopened.Workbook.Calculate(new ExcelCalculationOption { AllowCircularReferences = true });
131+
Console.WriteLine(" Result: calculation succeeded (unexpected)\n");
132+
}
133+
catch (Exception ex)
134+
{
135+
Console.WriteLine($" Result: {ex.GetType().Name} - {ex.Message}\n");
136+
}
137+
}
138+
}
139+
[TestMethod]
140+
public void issue2226()
141+
{
142+
static (ExcelPackage pkg, ExcelWorksheet ws1, ExcelWorksheet ws2) CreateWorkbook(bool includeSheetScopedName)
143+
{
144+
var pkg = new ExcelPackage();
145+
var ws1 = pkg.Workbook.Worksheets.Add("Sheet1");
146+
var ws2 = pkg.Workbook.Worksheets.Add("Sheet2");
147+
148+
// Workbook-scoped name "MyTable" (should be used by formulas on Sheet2)
149+
ws2.Cells["A1"].Value = 1;
150+
ws2.Cells["B1"].Value = 2;
151+
ws2.Cells["A2"].Value = 10;
152+
ws2.Cells["B2"].Value = 20;
153+
pkg.Workbook.Names.Add("MyTable", ws2.Cells["A1:B2"]);
154+
155+
if (includeSheetScopedName)
156+
{
157+
// Sheet-scoped name "MyTable" on Sheet1 (should NOT affect formulas on Sheet2)
158+
ws1.Cells["A1"].Value = 1;
159+
ws1.Cells["B1"].Value = 2;
160+
ws1.Cells["A2"].Value = 1;
161+
ws1.Cells["B2"].Value = 2;
162+
ws1.Names.Add("MyTable", ws1.Cells["A1:B2"]);
163+
}
164+
165+
// Put the same formula in multiple cells so we can test different calculation APIs
166+
// without accidental caching/order effects between tests.
167+
ws2.Cells["C1"].Formula = "HLOOKUP(1,MyTable,2,FALSE)"; // used for string-eval + address-eval
168+
ws2.Cells["C2"].Formula = "HLOOKUP(1,MyTable,2,FALSE)"; // used for range.Calculate()
169+
ws2.Cells["C3"].Formula = "HLOOKUP(1,MyTable,2,FALSE)"; // used for workbook.Calculate()
170+
return (pkg, ws1, ws2);
171+
}
172+
173+
static void RunTest(string name, Func<(ExcelPackage pkg, ExcelWorksheet ws1, ExcelWorksheet ws2), string> run)
174+
{
175+
Console.WriteLine($"\n=== {name} ===");
176+
var ctx = CreateWorkbook(includeSheetScopedName: true);
177+
using (ctx.pkg)
178+
{
179+
Console.WriteLine(run(ctx));
180+
}
181+
}
182+
183+
Console.WriteLine("Expected (Excel semantics): 10");
184+
185+
RunTest("Mode A: ws.Calculate(formula-string) is wrong", ctx =>
186+
{
187+
object? inWs2;
188+
object? inWs1;
189+
try { inWs2 = ctx.ws2.Calculate(ctx.ws2.Cells["C1"].Formula); }
190+
catch (Exception ex) { inWs2 = $"EXCEPTION: {ex.GetType().Name}: {ex.Message}"; }
191+
Assert.AreEqual(inWs2, 10);
192+
try { inWs1 = ctx.ws1.Calculate(ctx.ws2.Cells["C1"].Formula); }
193+
catch (Exception ex) { inWs1 = $"EXCEPTION: {ex.GetType().Name}: {ex.Message}"; }
194+
Assert.AreEqual(inWs1, 1);
195+
return $"ws2.Calculate(formula) => {inWs2}\nws1.Calculate(formula) => {inWs1}";
196+
});
197+
198+
RunTest("Mode B2: range.Calculate() is right", ctx =>
199+
{
200+
var before = ctx.ws2.Cells["C2"].Value;
201+
try { ctx.ws2.Cells["C2"].Calculate(); }
202+
catch (Exception ex) { return $"Before => {before}\nEXCEPTION: {ex.GetType().Name}: {ex.Message}"; }
203+
Assert.AreEqual(ctx.ws2.Cells["C2"].Value, 10);
204+
return $"Before => {before}\nAfter => {ctx.ws2.Cells["C2"].Value}";
205+
});
206+
207+
RunTest("Mode B3: worksheet.Calculate() is right", ctx =>
208+
{
209+
var before = ctx.ws2.Cells["C2"].Value;
210+
try { ctx.ws2.Calculate(); }
211+
catch (Exception ex) { return $"Before => {before}\nEXCEPTION: {ex.GetType().Name}: {ex.Message}"; }
212+
Assert.AreEqual(ctx.ws2.Cells["C2"].Value, 10);
213+
return $"Before => {before}\nAfter => {ctx.ws2.Cells["C2"].Value}";
214+
});
215+
216+
RunTest("Mode B: workbook.Calculate() is right", ctx =>
217+
{
218+
var before = ctx.ws2.Cells["C3"].Value;
219+
try { ctx.pkg.Workbook.Calculate(); }
220+
catch (Exception ex) { return $"Before => {before}\nEXCEPTION: {ex.GetType().Name}: {ex.Message}"; }
221+
Assert.AreEqual(ctx.ws2.Cells["C2"].Value, 10);
222+
return $"Before => {before}\nAfter => {ctx.ws2.Cells["C3"].Value}";
223+
});
224+
225+
RunTest("Mode C: ws.Calculate(address) is right", ctx =>
226+
{
227+
object? fromWs2;
228+
object? fromWs1;
229+
try { fromWs2 = ctx.ws2.Calculate("'Sheet2'!C1"); }
230+
catch (Exception ex) { fromWs2 = $"EXCEPTION: {ex.GetType().Name}: {ex.Message}"; }
231+
Assert.AreEqual(fromWs2, 10);
232+
try { fromWs1 = ctx.ws1.Calculate("'Sheet2'!C1"); }
233+
catch (Exception ex) { fromWs1 = $"EXCEPTION: {ex.GetType().Name}: {ex.Message}"; }
234+
return $"ws2.Calculate(\"'Sheet2'!C1\") => {fromWs2}\nws1.Calculate(\"'Sheet2'!C1\") => {fromWs1}";
235+
Assert.AreEqual(fromWs1, 10);
236+
});
237+
238+
RunTest("Sanity: removing sheet-scoped name fixes formula-string eval", ctx =>
239+
{
240+
// Demonstrate the fix within one workbook instance.
241+
object? before;
242+
object? after;
243+
try { before = ctx.ws2.Calculate(ctx.ws2.Cells["C1"].Formula); }
244+
catch (Exception ex) { before = $"EXCEPTION: {ex.GetType().Name}: {ex.Message}"; }
245+
246+
ctx.ws1.Names.Remove("MyTable");
247+
248+
try { after = ctx.ws2.Calculate(ctx.ws2.Cells["C1"].Formula); }
249+
catch (Exception ex) { after = $"EXCEPTION: {ex.GetType().Name}: {ex.Message}"; }
250+
251+
return $"Before removing ws1-scoped name => {before}\nAfter removing ws1-scoped name => {after}";
252+
});
253+
}
74254
}
75255
}

src/EPPlusTest/Issues/FormulaCalculationIssues.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
using Microsoft.VisualStudio.TestTools.UnitTesting;
22
using OfficeOpenXml;
33
using OfficeOpenXml.FormulaParsing;
4+
using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions;
45
using OfficeOpenXml.FormulaParsing.LexicalAnalysis;
56
using OfficeOpenXml.Sorting;
67
using System;
78
using System.Collections.Generic;
89
using System.Diagnostics;
910
using System.IO;
1011
using System.Linq;
12+
using static OfficeOpenXml.ExcelErrorValue;
1113

1214
namespace EPPlusTest.Issues
1315
{

0 commit comments

Comments
 (0)