|
1 | 1 | using Microsoft.VisualStudio.TestTools.UnitTesting; |
2 | 2 | using OfficeOpenXml; |
| 3 | +using OfficeOpenXml.FormulaParsing; |
3 | 4 | using System; |
4 | 5 | using System.Collections.Generic; |
5 | 6 | using System.IO; |
@@ -71,5 +72,184 @@ public void DefinedNamesQuoteError() |
71 | 72 | SaveAndCleanup(package); |
72 | 73 | } |
73 | 74 | } |
| 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 | + } |
74 | 254 | } |
75 | 255 | } |
0 commit comments