diff --git a/src/EPPlus/FormulaParsing/DependencyChain/ArrayFormulaOutput.cs b/src/EPPlus/FormulaParsing/DependencyChain/ArrayFormulaOutput.cs index c44e548855..f8b83172ae 100644 --- a/src/EPPlus/FormulaParsing/DependencyChain/ArrayFormulaOutput.cs +++ b/src/EPPlus/FormulaParsing/DependencyChain/ArrayFormulaOutput.cs @@ -10,17 +10,18 @@ Date Author Change ************************************************************************************************* 05/14/2024 EPPlus Software AB Initial release EPPlus 7 *************************************************************************************************/ -using OfficeOpenXml.FormulaParsing.LexicalAnalysis; using OfficeOpenXml.Core.CellStore; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup; using OfficeOpenXml.Filter; +using OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup; using OfficeOpenXml.FormulaParsing.FormulaExpressions; +using OfficeOpenXml.FormulaParsing.LexicalAnalysis; +using OfficeOpenXml.FormulaParsing.Ranges; using OfficeOpenXml.Utils; using OfficeOpenXml.Utils.TypeConversion; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; namespace OfficeOpenXml.FormulaParsing { @@ -37,7 +38,14 @@ internal static void FillArrayFromRangeInfo(RpnFormula f, IRangeInfo array, Rang var rows = sf.EndRow - sf.StartRow + 1; var cols = sf.EndCol - sf.StartCol + 1; var wsIx = ws.IndexInList; - for (int r = 0; r < rows; r++) + + // Determine physical boundary and default value for virtual rows + var virtualImr = array as InMemoryRange; + var hasVirtual = virtualImr != null && virtualImr.HasVirtualRows; + var physicalRowLimit = hasVirtual ? Math.Min(rows, virtualImr.PhysicalRows) : rows; + + // Phase 1: Physical rows - read actual cell values via GetOffset + for (int r = 0; r < physicalRowLimit; r++) { for (int c = 0; c < cols; c++) { @@ -46,9 +54,8 @@ internal static void FillArrayFromRangeInfo(RpnFormula f, IRangeInfo array, Rang if (r < nr && c < nc) { var val = array.GetOffset(r, c); - if(ConvertUtil.IsNumeric(val) && val is double dbl && dbl == 0d) + if (ConvertUtil.IsNumeric(val) && val is double dbl && dbl == 0d) { - // avoid -0 val = 0d; } ws.SetValueInner(row, col, val ?? 0D); @@ -58,7 +65,35 @@ internal static void FillArrayFromRangeInfo(RpnFormula f, IRangeInfo array, Rang ws.SetValueInner(row, col, ErrorValues.NAError); } var id = ExcelCellBase.GetCellId(wsIx, row, col); - depChain.processedCells.Add(id); + depChain.processedCells.Add(id); + } + } + + // Phase 2: Virtual rows - write pre-computed default value directly + if (hasVirtual && physicalRowLimit < rows) + { + var defaultVal = virtualImr.VirtualDefaultValue ?? 0D; + if (ConvertUtil.IsNumeric(defaultVal) && defaultVal is double dblDefault && dblDefault == 0d) + { + defaultVal = 0d; + } + for (int r = physicalRowLimit; r < rows; r++) + { + for (int c = 0; c < cols; c++) + { + var row = sr + r; + var col = sc + c; + if (r < nr && c < nc) + { + ws.SetValueInner(row, col, defaultVal); + } + else + { + ws.SetValueInner(row, col, ErrorValues.NAError); + } + var id = ExcelCellBase.GetCellId(wsIx, row, col); + depChain.processedCells.Add(id); + } } } @@ -74,7 +109,7 @@ private static bool HasSpill(ExcelWorksheet ws, int fIx, int startRow, int start { if (r == startRow && c == startColumn) continue; object f = -1; - if (fIx!=-1 && ws._formulas.Exists(r, c, ref f) && f != null) + if (fIx != -1 && ws._formulas.Exists(r, c, ref f) && f != null) { if (f is int intfIx && intfIx == fIx) { @@ -87,7 +122,7 @@ private static bool HasSpill(ExcelWorksheet ws, int fIx, int startRow, int start else { var v = ws.GetValueInner(r, c); - if(v!=null) + if (v != null) { rowOff = r - startRow; colOff = c - startColumn; @@ -98,7 +133,7 @@ private static bool HasSpill(ExcelWorksheet ws, int fIx, int startRow, int start } rowOff = colOff = 0; return false; - } + } internal static SimpleAddress[] FillDynamicArrayFromRangeInfo(RpnFormula f, IRangeInfo array, RangeHashset rd, RpnOptimizedDependencyChain depChain) { var nr = array.Size.NumberOfRows; @@ -107,13 +142,13 @@ internal static SimpleAddress[] FillDynamicArrayFromRangeInfo(RpnFormula f, IRan var startRow = f._row; var startCol = f._column; var wsIx = ws.IndexInList; - + f._flags |= FormulaFlags.IsDynamic; var md = depChain._parsingContext.Package.Workbook.Metadata; //d.GetDynamicArrayIndex(out int cm); md.GetDynamicArrayId(out uint cm); var metaData = f._ws._metadataStore.GetValue(startRow, startCol); - metaData.cm= cm; + metaData.cm = cm; f._ws._metadataStore.SetValue(f._row, f._column, metaData); @@ -134,7 +169,7 @@ internal static SimpleAddress[] FillDynamicArrayFromRangeInfo(RpnFormula f, IRan } } SimpleAddress[] dirtyRange; - if(f._arrayIndex==-1) + if (f._arrayIndex == -1) { var endRow = startRow + array.Size.NumberOfRows - 1; var endCol = f._column + array.Size.NumberOfCols - 1; @@ -151,7 +186,7 @@ internal static SimpleAddress[] FillDynamicArrayFromRangeInfo(RpnFormula f, IRan CleanupSharedFormulaValues(f, ws, sf, endRow, endCol); sf.EndRow = endRow; sf.EndCol = endCol; - + } FillArrayFromRangeInfo(f, array, rd, depChain); return dirtyRange; @@ -212,10 +247,10 @@ private static void CleanupSharedFormulaValues(RpnFormula f, ExcelWorksheet ws, } } - private static SimpleAddress[] GetDirtyRange(int fromRow, int fromCol, int toRow, int toCol, int prevToRow=0, int prevToCol=0) + private static SimpleAddress[] GetDirtyRange(int fromRow, int fromCol, int toRow, int toCol, int prevToRow = 0, int prevToCol = 0) { - if(prevToRow == 0) prevToRow = fromRow; - if(prevToCol == 0) prevToCol = fromCol; + if (prevToRow == 0) prevToRow = fromRow; + if (prevToCol == 0) prevToCol = fromCol; if (prevToRow == toRow && prevToCol == toCol) { return new SimpleAddress[0]; @@ -226,15 +261,15 @@ private static SimpleAddress[] GetDirtyRange(int fromRow, int fromCol, int toRow { new SimpleAddress(fromRow, Math.Min(prevToCol+1, toCol+1), Math.Min(prevToRow, toRow), Math.Max(prevToCol, toCol)), new SimpleAddress(Math.Min(prevToRow + 1, toRow + 1), fromCol, Math.Max(prevToRow, toRow), Math.Max(prevToCol, toCol)) - }; + }; } - else if(prevToRow != toRow) + else if (prevToRow != toRow) { - return new SimpleAddress[] { new SimpleAddress(Math.Min(prevToRow+1, toRow), fromCol, Math.Max(prevToRow, toRow), Math.Max(prevToCol,toCol)) }; - } + return new SimpleAddress[] { new SimpleAddress(Math.Min(prevToRow + 1, toRow), fromCol, Math.Max(prevToRow, toRow), Math.Max(prevToCol, toCol)) }; + } else { - return new SimpleAddress[] { new SimpleAddress(fromRow, Math.Min(prevToCol+1, toCol), Math.Max(prevToRow, toRow), Math.Max(prevToCol, toCol)) }; + return new SimpleAddress[] { new SimpleAddress(fromRow, Math.Min(prevToCol + 1, toCol), Math.Max(prevToRow, toRow), Math.Max(prevToCol, toCol)) }; } } diff --git a/src/EPPlus/FormulaParsing/Excel/Functions/RefAndLookup/LookupUtils/XlookupScanner.cs b/src/EPPlus/FormulaParsing/Excel/Functions/RefAndLookup/LookupUtils/XlookupScanner.cs index 0e9e903c1f..b502f43d74 100644 --- a/src/EPPlus/FormulaParsing/Excel/Functions/RefAndLookup/LookupUtils/XlookupScanner.cs +++ b/src/EPPlus/FormulaParsing/Excel/Functions/RefAndLookup/LookupUtils/XlookupScanner.cs @@ -150,16 +150,28 @@ private int FindIndexInternal() return -1; } + private int GetMaxItemsRow(IRangeInfo lookupRange) { + var adjusted = lookupRange.GetAddressDimensionAdjusted(0); + if (adjusted != null) + { + return adjusted.ToRow - adjusted.FromRow + 1; + } if (lookupRange.Address.ToRow > lookupRange.Dimension.ToRow) { return lookupRange.Dimension.ToRow - lookupRange.Address.FromRow + 1; - } + } return _lookupRange.Size.NumberOfRows; } + private int GetMaxItemsColumns(IRangeInfo lookupRange) { + var adjusted = lookupRange.GetAddressDimensionAdjusted(0); + if (adjusted != null) + { + return adjusted.ToCol - adjusted.FromCol + 1; + } if (lookupRange.Address.ToCol > lookupRange.Dimension.ToCol) { return lookupRange.Dimension.ToCol - lookupRange.Address.FromCol + 1; diff --git a/src/EPPlus/FormulaParsing/Excel/Operators/RangeOperationsOperator.cs b/src/EPPlus/FormulaParsing/Excel/Operators/RangeOperationsOperator.cs index eb1b6f4842..9fc72fe635 100644 --- a/src/EPPlus/FormulaParsing/Excel/Operators/RangeOperationsOperator.cs +++ b/src/EPPlus/FormulaParsing/Excel/Operators/RangeOperationsOperator.cs @@ -10,16 +10,11 @@ Date Author Change ************************************************************************************************* 05/30/2022 EPPlus Software AB EPPlus 6.1 *************************************************************************************************/ -using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions; -using OfficeOpenXml.FormulaParsing.Excel.Functions.Text; using OfficeOpenXml.FormulaParsing.FormulaExpressions; using OfficeOpenXml.FormulaParsing.LexicalAnalysis; using OfficeOpenXml.FormulaParsing.Ranges; -using OfficeOpenXml.Utils; using OfficeOpenXml.Utils.TypeConversion; using System; -using System.Globalization; -using System.Linq; namespace OfficeOpenXml.FormulaParsing.Excel.Operators { @@ -42,18 +37,30 @@ private static CompileResult ApplyOperator(CompileResult l, CompileResult r, Ope internal static InMemoryRange Negate(IRangeInfo ri) { InMemoryRange imr; - if(ri.IsInMemoryRange==false) + if (ri.IsInMemoryRange == false) { - imr = new InMemoryRange(ri.Size); + var physicalRows = RangeHelper.GetPhysicalRows(ri); + var logicalRows = ri.Size.NumberOfRows; + if (physicalRows < logicalRows) + { + imr = new InMemoryRange( + new RangeDefinition(physicalRows, ri.Size.NumberOfCols), + logicalRows); + } + else + { + imr = new InMemoryRange(ri.Size); + } } else { imr = (InMemoryRange)ri; } + int rows = imr.PhysicalRows; for (int c = 0; c < ri.Size.NumberOfCols; c++) { - for (int r = 0; r < ri.Size.NumberOfRows; r++) + for (int r = 0; r < rows; r++) { var d = ConvertUtil.GetValueDouble(ri.GetOffset(r, c), false, true); @@ -67,20 +74,62 @@ internal static InMemoryRange Negate(IRangeInfo ri) } } } + + if (imr.HasVirtualRows) + { + // Compute the negation of an empty cell: -(0) = 0 + // Use the upstream default if one exists, otherwise null (= 0) + var srcDefault = imr.VirtualDefaultValue; + if (srcDefault == null) + { + srcDefault = 0d; + } + var d = ConvertUtil.GetValueDouble(srcDefault, false, false); + if (double.IsNaN(d)) + { + imr.VirtualDefaultValue = ErrorValues.ValueError; + } + else + { + imr.VirtualDefaultValue = d == 0d ? 0d : -d; + } + } + return imr; } + private static InMemoryRange CreateRange(IRangeInfo l, IRangeInfo r, FormulaRangeAddress address) { var width = Math.Max(l.Size.NumberOfCols, r.Size.NumberOfCols); - var height = Math.Max(l.Size.NumberOfRows, r.Size.NumberOfRows); - var rangeDef = new RangeDefinition(height, width); - if(address != null) + + int logicalHeight = Math.Max(l.Size.NumberOfRows, r.Size.NumberOfRows); + int physicalHeight = Math.Max( + RangeHelper.GetPhysicalRows(l), + RangeHelper.GetPhysicalRows(r)); + + if (physicalHeight >= logicalHeight) + { + // No virtual rows needed - existing behavior + var rangeDef = new RangeDefinition(logicalHeight, width); + if (address != null) + { + return new InMemoryRange(address, rangeDef); + } + else + { + return new InMemoryRange(rangeDef); + } + } + + // Virtual range: small backing array, large logical size + var physicalDef = new RangeDefinition(physicalHeight, width); + if (address != null) { - return new InMemoryRange(address, rangeDef); + return new InMemoryRange(address, physicalDef, logicalHeight); } else { - return new InMemoryRange(rangeDef); + return new InMemoryRange(physicalDef, logicalHeight); } } @@ -105,7 +154,7 @@ private static void SetValue(Operators op, InMemoryRange resultRange, int row, i { var res = ApplyOperator(leftVal, rightVal, op, out bool error, context); var resultValue = res.ResultValue; - if(!(resultValue is bool) && ConvertUtil.IsNumeric(resultValue) && res.ResultNumeric == 0d) + if (!(resultValue is bool) && ConvertUtil.IsNumeric(resultValue) && res.ResultNumeric == 0d) { // avoid -0 results. resultValue = 0d; @@ -115,7 +164,7 @@ private static void SetValue(Operators op, InMemoryRange resultRange, int row, i private static bool ShouldUseSingleRow(RangeDefinition lSize, RangeDefinition rSize) { - if((lSize.NumberOfRows == 1 || rSize.NumberOfRows == 1) && lSize.NumberOfCols == rSize.NumberOfCols) + if ((lSize.NumberOfRows == 1 || rSize.NumberOfRows == 1) && lSize.NumberOfCols == rSize.NumberOfCols) { return true; } @@ -143,11 +192,11 @@ private static bool SingleRowSingleCol(RangeDefinition lSize, RangeDefinition rS private static bool AddressIsNotAvailable(RangeDefinition lSize, RangeDefinition rSize, int row, int col) { - if(row >= lSize.NumberOfRows || row >=rSize.NumberOfRows) + if (row >= lSize.NumberOfRows || row >= rSize.NumberOfRows) { return true; } - else if(col >= lSize.NumberOfCols || col >= rSize.NumberOfCols) + else if (col >= lSize.NumberOfCols || col >= rSize.NumberOfCols) { return true; } @@ -193,22 +242,48 @@ private static object GetCellValue(IRangeInfo range, int rowOffset, int colOffse catch { throw; - } + } + } + + private static void SetVirtualDefault( + InMemoryRange resultRange, + CompileResult nullLeft, + CompileResult nullRight, + Operators op, + ParsingContext context) + { + if (!resultRange.HasVirtualRows) return; + + var res = ApplyOperator(nullLeft, nullRight, op, out bool error, context); + if (error) + { + resultRange.VirtualDefaultValue = ExcelErrorValue.Create(eErrorType.Value); + } + else + { + var resultValue = res.ResultValue; + if (!(resultValue is bool) && ConvertUtil.IsNumeric(resultValue) && res.ResultNumeric == 0d) + { + resultValue = 0d; + } + resultRange.VirtualDefaultValue = resultValue; + } } - public static InMemoryRange ApplySingleValueRight(CompileResult left, CompileResult right, Operators op, ParsingContext context) + public static InMemoryRange ApplySingleValueRight( + CompileResult left, CompileResult right, Operators op, ParsingContext context) { var lr = left.Result as IRangeInfo; - if(lr == null && left.Result is FormulaRangeAddress fra) + if (lr == null && left.Result is FormulaRangeAddress fra) { lr = context.ExcelDataProvider.GetRange(fra); } - else if(left.Address != null && left.Result is not InMemoryRange) + else if (left.Address != null && left.Result is not InMemoryRange) { lr = context.ExcelDataProvider.GetRange(left.Address); } var resultRange = CreateRange(lr, InMemoryRange.Empty, lr.Address); - for (var row = 0; row < resultRange.Size.NumberOfRows; row++) + for (var row = 0; row < resultRange.PhysicalRows; row++) { for (var col = 0; col < resultRange.Size.NumberOfCols; col++) { @@ -217,10 +292,16 @@ public static InMemoryRange ApplySingleValueRight(CompileResult left, CompileRes SetValue(op, resultRange, row, col, lcr, right, context); } } + + // Compute default for virtual rows: null op scalar + SetVirtualDefault(resultRange, + CompileResultFactory.Create(null), right, op, context); + return resultRange; } - public static InMemoryRange ApplySingleValueLeft(CompileResult left, CompileResult right, Operators op, ParsingContext context) + public static InMemoryRange ApplySingleValueLeft( + CompileResult left, CompileResult right, Operators op, ParsingContext context) { var rr = right.Result as IRangeInfo; if (rr == null && right.Result is FormulaRangeAddress fra) @@ -232,7 +313,7 @@ public static InMemoryRange ApplySingleValueLeft(CompileResult left, CompileResu rr = context.ExcelDataProvider.GetRange(right.Address); } var resultRange = CreateRange(InMemoryRange.Empty, rr, rr.Address); - for (var row = 0; row < resultRange.Size.NumberOfRows; row++) + for (var row = 0; row < resultRange.PhysicalRows; row++) { for (var col = 0; col < resultRange.Size.NumberOfCols; col++) { @@ -242,18 +323,83 @@ public static InMemoryRange ApplySingleValueLeft(CompileResult left, CompileResu SetValue(op, resultRange, row, col, left, rcr, context); } } + + // Compute default for virtual rows: scalar op null + SetVirtualDefault(resultRange, + left, CompileResultFactory.Create(null), op, context); + return resultRange; } + private static void SetVirtualDefaultForRanges( + InMemoryRange resultRange, + IRangeInfo lr, + IRangeInfo rr, + bool shouldUseSingleRow, + bool shouldUseSingleCell, + bool singleRowSingleCol, + Operators op, + ParsingContext context) + { + if (!resultRange.HasVirtualRows) return; + + CompileResult virtualLeft, virtualRight; + if (shouldUseSingleRow) + { + if (lr.Size.NumberOfRows == 1) + { + virtualLeft = CompileResultFactory.Create(GetCellValue(lr, 0, 0)); + virtualRight = CompileResultFactory.Create(null); + } + else + { + virtualLeft = CompileResultFactory.Create(null); + virtualRight = CompileResultFactory.Create(GetCellValue(rr, 0, 0)); + } + } + else if (shouldUseSingleCell) + { + if (lr.Size.NumberOfCols == 1 && lr.Size.NumberOfRows == 1) + { + virtualLeft = CompileResultFactory.Create(GetCellValue(lr, 0, 0)); + virtualRight = CompileResultFactory.Create(null); + } + else + { + virtualLeft = CompileResultFactory.Create(null); + virtualRight = CompileResultFactory.Create(GetCellValue(rr, 0, 0)); + } + } + else if (singleRowSingleCol) + { + if (lr.Size.NumberOfRows == 1) + { + virtualLeft = CompileResultFactory.Create(GetCellValue(lr, 0, 0)); + virtualRight = CompileResultFactory.Create(null); + } + else + { + virtualLeft = CompileResultFactory.Create(null); + virtualRight = CompileResultFactory.Create(GetCellValue(rr, 0, 0)); + } + } + else + { + virtualLeft = CompileResultFactory.Create(null); + virtualRight = CompileResultFactory.Create(null); + } + SetVirtualDefault(resultRange, virtualLeft, virtualRight, op, context); + } + private static InMemoryRange ApplyRanges(CompileResult left, CompileResult right, Operators op, ParsingContext context, FormulaRangeAddress intersectAddress) { var lr = left.Result as IRangeInfo; var rr = right.Result as IRangeInfo; - if(lr == null && left.Result is FormulaRangeAddress fral) + if (lr == null && left.Result is FormulaRangeAddress fral) { lr = new RangeInfo(fral); } - if(rr == null && right.Result is FormulaRangeAddress frar) + if (rr == null && right.Result is FormulaRangeAddress frar) { rr = new RangeInfo(frar); } @@ -263,7 +409,7 @@ private static InMemoryRange ApplyRanges(CompileResult left, CompileResult right var shouldUseSingleRow = ShouldUseSingleRow(lr.Size, rr.Size); var shouldUseSingleCell = ShouldUseSingleCell(lr.Size, rr.Size); var singleRowSingleCol = SingleRowSingleCol(lr.Size, rr.Size); - for (var row = 0; row < resultRange.Size.NumberOfRows; row++) + for (var row = 0; row < resultRange.PhysicalRows; row++) { for (var col = 0; col < resultRange.Size.NumberOfCols; col++) { @@ -340,7 +486,11 @@ private static InMemoryRange ApplyRanges(CompileResult left, CompileResult right } } + SetVirtualDefaultForRanges(resultRange, lr, rr, + shouldUseSingleRow, shouldUseSingleCell, singleRowSingleCol, + op, context); + return resultRange; } } -} +} \ No newline at end of file diff --git a/src/EPPlus/FormulaParsing/ExcelUtilities/RangeFlattener.cs b/src/EPPlus/FormulaParsing/ExcelUtilities/RangeFlattener.cs index 4ec22eec53..cca8f970c5 100644 --- a/src/EPPlus/FormulaParsing/ExcelUtilities/RangeFlattener.cs +++ b/src/EPPlus/FormulaParsing/ExcelUtilities/RangeFlattener.cs @@ -10,13 +10,10 @@ Date Author Change ************************************************************************************************* 21/06/2023 EPPlus Software AB Initial release EPPlus 7 *************************************************************************************************/ +using OfficeOpenXml.FormulaParsing.Ranges; using OfficeOpenXml.Utils.TypeConversion; -using OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup; -using OfficeOpenXml.Utils; using System; using System.Collections.Generic; -using System.Linq; -using System.Text; namespace OfficeOpenXml.FormulaParsing.ExcelUtilities { @@ -32,8 +29,9 @@ internal class RangeFlattener public static List FlattenRange(IRangeInfo r1, bool addNullifEmpty = true) { var result = new List(); + int rows = RangeHelper.GetPhysicalRows(r1); - for (var row = 0; row < r1.Size.NumberOfRows; row++) + for (var row = 0; row < rows; row++) { for (var column = 0; column < r1.Size.NumberOfCols; column++) { @@ -67,7 +65,8 @@ public static List FlattenRangeObject(IRangeInfo r1) return result; } /// - /// Produces two lists based on the supplied ranges. The lists will contain all data from positions where both ranges has numeric values. + /// Produces two lists based on the supplied ranges. + /// The lists will contain all data from positions where both ranges has numeric values. /// /// range 1 /// range 2 @@ -122,4 +121,4 @@ public static void GetNumericPairLists(IRangeInfo r1, IRangeInfo r2, bool dataPo } } } -} +} \ No newline at end of file diff --git a/src/EPPlus/FormulaParsing/FormulaExpressions/FunctionCompilers/CustomArrayBehaviourCompiler.cs b/src/EPPlus/FormulaParsing/FormulaExpressions/FunctionCompilers/CustomArrayBehaviourCompiler.cs index 933300b724..42934d567c 100644 --- a/src/EPPlus/FormulaParsing/FormulaExpressions/FunctionCompilers/CustomArrayBehaviourCompiler.cs +++ b/src/EPPlus/FormulaParsing/FormulaExpressions/FunctionCompilers/CustomArrayBehaviourCompiler.cs @@ -40,18 +40,18 @@ internal CustomArrayBehaviourCompiler(ExcelFunction function, ParsingContext con public override CompileResult Compile(IEnumerable children, ParsingContext context) { var args = new List(); - + if (!children.Any()) return new CompileResult(eErrorType.Value); var rangeArgs = new Dictionary(); var otherArgs = new Dictionary(); - for(var ix = 0; ix < children.Count(); ix++) + for (var ix = 0; ix < children.Count(); ix++) { var cr = children.ElementAt(ix); - if(cr.DataType == DataType.ExcelRange && Function.ArrayBehaviourConfig.CanBeArrayArg(ix) && (cr.Result is IRangeInfo || cr.Result is FormulaRangeAddress)) + if (cr.DataType == DataType.ExcelRange && Function.ArrayBehaviourConfig.CanBeArrayArg(ix) && (cr.Result is IRangeInfo || cr.Result is FormulaRangeAddress)) { var range = cr.Result as IRangeInfo; - if(range == null && cr.Result is FormulaRangeAddress fra) + if (range == null && cr.Result is FormulaRangeAddress fra) { range = new RangeInfo(fra); } @@ -71,7 +71,7 @@ public override CompileResult Compile(IEnumerable children, Parsi } else { - if(cr.DataType == DataType.ExcelRange) + if (cr.DataType == DataType.ExcelRange) { cr = CompileResultFactory.Create(cr.Result); } @@ -79,55 +79,78 @@ public override CompileResult Compile(IEnumerable children, Parsi } } - if(rangeArgs.Count == 0) + if (rangeArgs.Count == 0) { var defaultCompiler = new DefaultCompiler(Function); return defaultCompiler.Compile(children, context); } short maxWidth = 0; - var maxHeight = 0; - foreach(var rangeArg in rangeArgs.Values) + var maxPhysicalHeight = 0; + var maxLogicalHeight = 0; + foreach (var rangeArg in rangeArgs.Values) { - if(rangeArg.Size.NumberOfCols > maxWidth) + if (rangeArg.Size.NumberOfCols > maxWidth) { maxWidth = rangeArg.Size.NumberOfCols; } - if(rangeArg.Size.NumberOfRows > maxHeight) + + int physical = RangeHelper.GetPhysicalRows(rangeArg); + if (physical > maxPhysicalHeight) + { + maxPhysicalHeight = physical; + } + if (rangeArg.Size.NumberOfRows > maxLogicalHeight) { - maxHeight= rangeArg.Size.NumberOfRows; + maxLogicalHeight = rangeArg.Size.NumberOfRows; } } - var resultRangeDef = new RangeDefinition(maxHeight, maxWidth); InMemoryRange resultRange; - if(rangeArgs.Count==1) + if (maxPhysicalHeight < maxLogicalHeight) { - resultRange = new InMemoryRange(rangeArgs.First().Value.Address, resultRangeDef); + var physicalDef = new RangeDefinition(maxPhysicalHeight, maxWidth); + if (rangeArgs.Count == 1) + { + resultRange = new InMemoryRange(rangeArgs.First().Value.Address, physicalDef, maxLogicalHeight); + } + else + { + resultRange = new InMemoryRange(physicalDef, maxLogicalHeight); + } } else { - resultRange = new InMemoryRange(resultRangeDef); + var rangeDef = new RangeDefinition(maxLogicalHeight, maxWidth); + if (rangeArgs.Count == 1) + { + resultRange = new InMemoryRange(rangeArgs.First().Value.Address, rangeDef); + } + else + { + resultRange = new InMemoryRange(rangeDef); + } } + var nArgs = children.Count(); - for(var row = 0; row < resultRange.Size.NumberOfRows; row++) + for (var row = 0; row < resultRange.PhysicalRows; row++) { - for(var col = 0; col < resultRange.Size.NumberOfCols; col++) + for (var col = 0; col < resultRange.Size.NumberOfCols; col++) { bool isError = false; var argList = new List(); - for(var argIx = 0; argIx < nArgs; argIx++) + for (var argIx = 0; argIx < nArgs; argIx++) { - if(rangeArgs.ContainsKey(argIx)) + if (rangeArgs.ContainsKey(argIx)) { var range = rangeArgs[argIx]; var r = row; var c = col; - if(range.Size.NumberOfCols == 1 && range.Size.NumberOfRows == resultRange.Size.NumberOfRows) + if (range.Size.NumberOfCols == 1 && range.Size.NumberOfRows == resultRange.Size.NumberOfRows) { c = 0; } - if(range.Size.NumberOfRows == 1 && range.Size.NumberOfCols == resultRange.Size.NumberOfCols) + if (range.Size.NumberOfRows == 1 && range.Size.NumberOfCols == resultRange.Size.NumberOfCols) { r = 0; } @@ -149,12 +172,12 @@ public override CompileResult Compile(IEnumerable children, Parsi continue; } } - + } else { var arg = otherArgs[argIx]; - if(arg.DataType == DataType.LambdaCalculation) + if (arg.DataType == DataType.LambdaCalculation) { var calculator = arg.ResultValue as LambdaCalculator; calculator.BeginCalculation(); @@ -180,4 +203,4 @@ public override CompileResult Compile(IEnumerable children, Parsi } } } -} +} \ No newline at end of file diff --git a/src/EPPlus/FormulaParsing/LexicalAnalysis/FormulaAddress.cs b/src/EPPlus/FormulaParsing/LexicalAnalysis/FormulaAddress.cs index 5be173d0d2..ef5e769632 100644 --- a/src/EPPlus/FormulaParsing/LexicalAnalysis/FormulaAddress.cs +++ b/src/EPPlus/FormulaParsing/LexicalAnalysis/FormulaAddress.cs @@ -708,16 +708,15 @@ public bool IsSingleCell /// /// The that contains the address /// + public bool HasFormulas(ExcelPackage package) { if (_hasFormulas.HasValue) return _hasFormulas.Value; - if (package == null || WorksheetIx < 0) { _hasFormulas = false; return false; } - var ws = package.Workbook.GetWorksheetByIndexInList(WorksheetIx); if (ws == null) { @@ -725,10 +724,30 @@ public bool HasFormulas(ExcelPackage package) return false; } - // Check if range contains formulas - for (var row = FromRow; row <= ToRow; row++) + // Clamp to worksheet dimension to avoid iterating 1M rows + // for full column references + var dim = ws.Dimension; + var fromRow = FromRow; + var toRow = ToRow; + var fromCol = FromCol; + var toCol = ToCol; + if (dim != null) + { + fromRow = Math.Max(fromRow, dim._fromRow); + toRow = Math.Min(toRow, dim._toRow); + fromCol = Math.Max(fromCol, dim._fromCol); + toCol = Math.Min(toCol, dim._toCol); + } + else + { + // No dimension means no data, hence no formulas + _hasFormulas = false; + return false; + } + + for (var row = fromRow; row <= toRow; row++) { - for (var col = FromCol; col <= ToCol; col++) + for (var col = fromCol; col <= toCol; col++) { if (ws._formulas.GetValue(row, col) != null) { @@ -737,7 +756,6 @@ public bool HasFormulas(ExcelPackage package) } } } - _hasFormulas = false; return false; } diff --git a/src/EPPlus/FormulaParsing/Ranges/InMemoryRange.cs b/src/EPPlus/FormulaParsing/Ranges/InMemoryRange.cs index 1ad86b06e8..ce1166a4f4 100644 --- a/src/EPPlus/FormulaParsing/Ranges/InMemoryRange.cs +++ b/src/EPPlus/FormulaParsing/Ranges/InMemoryRange.cs @@ -25,6 +25,7 @@ namespace OfficeOpenXml.FormulaParsing.Ranges [DebuggerDisplay("{Size}")] public class InMemoryRange : IRangeInfo { + /// /// The constructor /// @@ -32,24 +33,67 @@ public class InMemoryRange : IRangeInfo public InMemoryRange(RangeDefinition rangeDef) { _cells = new ICellInfo[rangeDef.NumberOfRows, rangeDef.NumberOfCols]; + _physicalRows = rangeDef.NumberOfRows; Size = rangeDef; _address = new FormulaRangeAddress() { FromRow = 0, FromCol = 0, ToRow = rangeDef.NumberOfRows - 1, ToCol = rangeDef.NumberOfCols - 1 }; } /// /// The constructor /// - /// The worksheet address that should be used for this range. Will be used for implicit intersection. + /// The worksheet address that should be used for this range. + /// Will be used for implicit intersection. /// Defines the size of the range public InMemoryRange(FormulaRangeAddress address, RangeDefinition rangeDef) { - if (address?._context != null) + if (address != null && address._context != null) { _ws = address._context.Package.Workbook.GetWorksheetByIndexInList(address._context.CurrentCell.WorksheetIx); } _address = address; _cells = new ICellInfo[rangeDef.NumberOfRows, rangeDef.NumberOfCols]; + _physicalRows = rangeDef.NumberOfRows; Size = rangeDef; } + + /// + /// Constructor for a virtual range with a small physical backing array but large logical size. + /// Rows beyond .NumberOfRows are virtual and return null. + /// + /// The worksheet address for implicit intersection. + /// Defines the physical (backed) size of the range. + /// The full logical row count (e.g. 1048576 for a full column). + internal InMemoryRange(FormulaRangeAddress address, RangeDefinition physicalDef, int logicalRows) + { + if (address != null && address._context != null) + { + _ws = address._context.Package.Workbook.GetWorksheetByIndexInList(address._context.CurrentCell.WorksheetIx); + } + _address = address; + _physicalRows = physicalDef.NumberOfRows; + _cells = new ICellInfo[physicalDef.NumberOfRows, physicalDef.NumberOfCols]; + Size = new RangeDefinition(logicalRows, physicalDef.NumberOfCols); + } + + /// + /// Constructor for a virtual range without an address. + /// Rows beyond .NumberOfRows are virtual and return null. + /// + /// Defines the physical (backed) size of the range. + /// The full logical row count. + internal InMemoryRange(RangeDefinition physicalDef, int logicalRows) + { + _physicalRows = physicalDef.NumberOfRows; + _cells = new ICellInfo[physicalDef.NumberOfRows, physicalDef.NumberOfCols]; + Size = new RangeDefinition(logicalRows, physicalDef.NumberOfCols); + _address = new FormulaRangeAddress() + { + FromRow = 0, + FromCol = 0, + ToRow = logicalRows - 1, + ToCol = physicalDef.NumberOfCols - 1 + }; + } + /// /// Constructor /// @@ -57,6 +101,7 @@ public InMemoryRange(FormulaRangeAddress address, RangeDefinition rangeDef) public InMemoryRange(List> range) { Size = new RangeDefinition(range.Count, (short)range[0].Count); + _physicalRows = Size.NumberOfRows; _cells = new ICellInfo[Size.NumberOfRows, Size.NumberOfCols]; for (int c = 0; c < Size.NumberOfCols; c++) { @@ -71,10 +116,12 @@ public InMemoryRange(List> range) /// /// Constructor /// - /// Another used as clone for this range. The address of the supplied range will not be copied. + /// Another used as clone for this range. + /// The address of the supplied range will not be copied. public InMemoryRange(IRangeInfo ri) { Size = ri.Size; + _physicalRows = Size.NumberOfRows; _cells = new ICellInfo[Size.NumberOfRows, Size.NumberOfCols]; for (int c = 0; c < Size.NumberOfCols; c++) { @@ -101,6 +148,8 @@ public InMemoryRange(int rows, short cols) private readonly ICellInfo[,] _cells; private int _colIx = -1; private int _rowIndex = 0; + private readonly int _physicalRows; + private object _virtualDefaultValue; // null means "no default" (backward compat) private static InMemoryRange _empty = new InMemoryRange(new RangeDefinition(0, 0)); @@ -109,6 +158,35 @@ public InMemoryRange(int rows, short cols) /// public static InMemoryRange Empty => _empty; + /// + /// Number of rows backed by the physical cell array. + /// For non-virtual ranges this equals Size.NumberOfRows. + /// + internal int PhysicalRows + { + get { return _physicalRows; } + } + + + /// + /// The value returned for rows beyond PhysicalRows. + /// When null (default), virtual rows return null. + /// When set, virtual rows return this value instead. + /// + internal object VirtualDefaultValue + { + get { return _virtualDefaultValue; } + set { _virtualDefaultValue = value; } + } + + /// + /// True if the range has virtual (unstored) rows beyond PhysicalRows. + /// + internal bool HasVirtualRows + { + get { return Size.NumberOfRows > _physicalRows; } + } + /// /// Sets the value for a cell. /// @@ -117,6 +195,7 @@ public InMemoryRange(int rows, short cols) /// The value to set public void SetValue(int row, int col, object val) { + if (row >= _physicalRows) return; var c = new InMemoryCellInfo(val); _cells[row, col] = c; } @@ -129,6 +208,7 @@ public void SetValue(int row, int col, object val) /// The cell public void SetCell(int row, int col, ICellInfo cell) { + if (row >= _physicalRows) return; _cells[row, col] = cell; } /// @@ -165,7 +245,7 @@ public void SetCell(int row, int col, ICellInfo cell) public FormulaRangeAddress Dimension { get - { + { return _address; } } @@ -190,7 +270,7 @@ object IEnumerator.Current /// /// The addresses for the range, if more than one. /// - public FormulaRangeAddress[] Addresses => [_address]; + public FormulaRangeAddress[] Addresses => new FormulaRangeAddress[] { _address }; /// /// Dispose @@ -226,12 +306,10 @@ public int GetNCells() /// The value of the cell public object GetOffset(int rowOffset, int colOffset) { + if (rowOffset >= _physicalRows) + return _virtualDefaultValue; // was: return null; var c = _cells[rowOffset, colOffset]; - if (c == null) - { - return null; - } - return c.Value; + return c == null ? null : c.Value; } /// @@ -242,23 +320,43 @@ public object GetOffset(int rowOffset, int colOffset) /// The ending row offset from the top-left cell. /// The ending column offset from the top-left cell /// The value of the cell - public IRangeInfo GetOffset(int rowOffsetStart, int colOffsetStart, int rowOffsetEnd, int colOffsetEnd) + public IRangeInfo GetOffset(int rowOffsetStart, int colOffsetStart, + int rowOffsetEnd, int colOffsetEnd) { - var nRows = Math.Abs(rowOffsetEnd - rowOffsetStart); - var nCols = (short)Math.Abs(colOffsetEnd- colOffsetStart); - nRows++; - nCols++; - var rangeDef = new RangeDefinition(nRows, nCols); - var result = new InMemoryRange(rangeDef); - var rowIx = 0; - for(var row = rowOffsetStart; row <= rowOffsetEnd; row++) + var logicalRows = rowOffsetEnd - rowOffsetStart + 1; + var nCols = (short)(colOffsetEnd - colOffsetStart + 1); + + if (rowOffsetStart >= _physicalRows) + { + var emptyRange = new InMemoryRange(new RangeDefinition(0, nCols), logicalRows); + emptyRange._virtualDefaultValue = _virtualDefaultValue; // propagate + return emptyRange; + } + + var physicalEnd = Math.Min(rowOffsetEnd, _physicalRows - 1); + var physicalRows = physicalEnd - rowOffsetStart + 1; + + InMemoryRange result; + if (physicalRows < logicalRows) + { + result = new InMemoryRange(new RangeDefinition(physicalRows, nCols), logicalRows); + result._virtualDefaultValue = _virtualDefaultValue; // propagate + } + else + { + result = new InMemoryRange(new RangeDefinition(physicalRows, nCols)); + } + + for (var row = rowOffsetStart; row <= physicalEnd; row++) { var colIx = 0; for (var col = colOffsetStart; col <= colOffsetEnd; col++) { - result.SetValue(rowIx, colIx++, _cells[row, col].Value); + var cell = _cells[row, col]; + var val = cell != null ? cell.Value : null; + result.SetValue(row - rowOffsetStart, colIx, val); + colIx++; } - rowIx++; } return result; } @@ -280,20 +378,12 @@ public bool IsHidden(int rowOffset, int colOffset) /// public object GetValue(int row, int col) { - if (_address == null) - { - var c = _cells[row, col]; - if (c == null) return null; - return c.Value; - - - } - else - { - var c = _cells[row-_address.FromRow, col-Address.FromCol]; - if (c == null) return null; - return c.Value; - } + int r = _address == null ? row : row - _address.FromRow; + if (r >= _physicalRows) return _virtualDefaultValue; + int c = _address == null ? col : col - _address.FromCol; + var cell = _cells[r, c]; + if (cell == null) return null; + return cell.Value; } /// /// Get cell @@ -303,6 +393,13 @@ public object GetValue(int row, int col) /// public ICellInfo GetCell(int row, int col) { + if (row >= _physicalRows) + { + // Return a virtual cell with the default value if set + if (_virtualDefaultValue != null) + return new InMemoryCellInfo(_virtualDefaultValue); + return null; + } var c = _cells[row, col]; if (c == null) return null; return c; @@ -320,7 +417,7 @@ public bool MoveNext() } _colIx = 0; _rowIndex++; - if (_rowIndex >= Size.NumberOfRows) return false; + if (_rowIndex >= _physicalRows) return false; return true; } /// @@ -341,15 +438,26 @@ IEnumerator IEnumerable.GetEnumerator() internal static InMemoryRange CloneRange(IRangeInfo ri) { - var ret = new InMemoryRange(ri.Size); - for(int r=0;r < ri.Size.NumberOfRows;r++) + var isVirtual = ri is InMemoryRange && ((InMemoryRange)ri).HasVirtualRows; + int physRows = isVirtual ? ((InMemoryRange)ri).PhysicalRows : ri.Size.NumberOfRows; + + var ret = isVirtual + ? new InMemoryRange(new RangeDefinition(physRows, ri.Size.NumberOfCols), + ri.Size.NumberOfRows) + : new InMemoryRange(ri.Size); + + if (isVirtual) { - for (int c = 0; c < ri.Size.NumberOfCols;c++) + ret._virtualDefaultValue = ((InMemoryRange)ri)._virtualDefaultValue; + } + + for (int r = 0; r < physRows; r++) + { + for (int c = 0; c < ri.Size.NumberOfCols; c++) { - ret.SetValue(r,c,ri.GetOffset(r,c)); + ret.SetValue(r, c, ri.GetOffset(r, c)); } } - return ret; } @@ -357,7 +465,7 @@ internal static InMemoryRange GetFromArray(params object[] values) { var rows = values.GetUpperBound(0) + 1; var ir = new InMemoryRange(rows, 1); - for(int r=0;r < rows;r++) + for (int r = 0; r < rows; r++) { ir.SetValue(r, 0, values[r]); } @@ -365,12 +473,26 @@ internal static InMemoryRange GetFromArray(params object[] values) } /// - /// Get the address adjusted inside the dimension of the worksheet. Not applicable on InMemoryRange's, as no addresses us used. + /// Get the address adjusted inside the dimension of the worksheet. + /// Not applicable on InMemoryRange's, as no addresses us used. /// /// Not applicable on InMemoryRange's. /// The address. public FormulaRangeAddress GetAddressDimensionAdjusted(int index) { + if (index > 0) return null; + + if (HasVirtualRows) + { + // Return address clamped to physical rows + return new FormulaRangeAddress() + { + FromRow = _address.FromRow, + FromCol = _address.FromCol, + ToRow = _address.FromRow + _physicalRows - 1, + ToCol = _address.ToCol + }; + } return _address; } @@ -378,7 +500,7 @@ internal IEnumerable SerializeToTokens() { var sb = new StringBuilder(); sb.Append("{"); - for (var row = 0; row < Size.NumberOfRows; row++) + for (var row = 0; row < _physicalRows; row++) { for (var col = 0; col < Size.NumberOfCols; col++) { @@ -389,7 +511,7 @@ internal IEnumerable SerializeToTokens() sb.Append(","); } } - if(row < Size.NumberOfRows - 1) + if (row < _physicalRows - 1) { sb.Append(";"); } @@ -406,4 +528,4 @@ public IRangeInfo GetRangeInfoByValue() return this; } } -} +} \ No newline at end of file diff --git a/src/EPPlus/FormulaParsing/Ranges/RangeHelper.cs b/src/EPPlus/FormulaParsing/Ranges/RangeHelper.cs new file mode 100644 index 0000000000..fe2b9d3849 --- /dev/null +++ b/src/EPPlus/FormulaParsing/Ranges/RangeHelper.cs @@ -0,0 +1,48 @@ +/************************************************************************************************* + Required Notice: Copyright (C) EPPlus Software AB. + This software is licensed under PolyForm Noncommercial License 1.0.0 + and may only be used for noncommercial purposes + https://polyformproject.org/licenses/noncommercial/1.0.0/ + + A commercial license to use this software can be purchased at https://epplussoftware.com + ************************************************************************************************* + Date Author Change + ************************************************************************************************* + 03/03/2026 EPPlus Software AB Virtual InMemoryRange support + *************************************************************************************************/ + +namespace OfficeOpenXml.FormulaParsing.Ranges +{ + /// + /// Helper methods for working with range physical/logical size. + /// + internal static class RangeHelper + { + /// + /// Returns the effective number of data rows for a range. + /// For virtual InMemoryRanges: the physical row count. + /// For worksheet-backed ranges: constrained by worksheet dimension. + /// For other ranges: Size.NumberOfRows. + /// + internal static int GetPhysicalRows(IRangeInfo range) + { + if (range is InMemoryRange imr) + return imr.PhysicalRows; + + if (!range.IsInMemoryRange && range.Worksheet?.Dimension != null) + { + var adjusted = range.GetAddressDimensionAdjusted(0); + if (adjusted != null) + { + // If the adjusted address is invalid (no column overlap), + // the range column has no data at all + if (adjusted.FromCol > adjusted.ToCol || adjusted.FromRow > adjusted.ToRow) + return 0; + return adjusted.ToRow - adjusted.FromRow + 1; + } + } + + return range.Size.NumberOfRows; + } + } +} \ No newline at end of file diff --git a/src/EPPlusTest/FormulaParsing/EntireColumnTests.cs b/src/EPPlusTest/FormulaParsing/EntireColumnTests.cs new file mode 100644 index 0000000000..8fb3cead80 --- /dev/null +++ b/src/EPPlusTest/FormulaParsing/EntireColumnTests.cs @@ -0,0 +1,233 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using OfficeOpenXml; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace EPPlusTest.FormulaParsing +{ + [TestClass] + public class EntireColumnTests + { + private ExcelPackage _package; + private ExcelWorkbook _workbook; + + [TestInitialize] + public void Initialize() + { + _package = new ExcelPackage(); + _workbook = _package.Workbook; + } + + [TestCleanup] + public void Cleanup() + { + _package.Dispose(); + } + + [TestMethod] + public void FullColumnRefPlusScalar_EmptyColumn_ShouldReturnOneInFirstAndLastRow() + { + // Arrange + var ws = _workbook.Worksheets.Add("Sheet1"); + // Column B is entirely empty/blank + ws.Cells["C1"].Formula = "B:B+1"; + + // Act + ws.Calculate(); + + // Assert - first row of spill result + Assert.AreEqual(1d, ws.Cells["C1"].Value, + "First row should be 0 + 1 = 1"); + var lastRow = ws.Dimension != null ? ws.Dimension.End.Row : 1; + Assert.AreEqual(ExcelPackage.MaxRows, ws.Dimension.End.Row, + "Spill should extend to the last row of the worksheet"); + Assert.AreEqual(1d, ws.Cells["C" + lastRow].Value, + "Last row of spill should be 0 + 1 = 1"); + } + + [TestMethod] + public void FullColumnRefPlusScalar_WithData_PhysicalRowsCalculatedAndVirtualRowsGetDefault() + { + // Arrange - B has data in rows 1-3, formula in C1 + var ws = _workbook.Worksheets.Add("Sheet1"); + ws.Cells["B1"].Value = 10d; + ws.Cells["B2"].Value = 20d; + ws.Cells["B3"].Value = 30d; + ws.Cells["C1"].Formula = "B:B+5"; + + // Act + ws.Calculate(); + + // Assert - physical rows get their actual calculated values + Assert.AreEqual(15d, ws.Cells["C1"].Value); + Assert.AreEqual(25d, ws.Cells["C2"].Value); + Assert.AreEqual(35d, ws.Cells["C3"].Value); + // Virtual rows beyond data: empty + 5 = 5 + Assert.AreEqual(5d, ws.Cells["C4"].Value, + "First virtual row should be 0 + 5 = 5"); + Assert.AreEqual(5d, ws.Cells["C" + ExcelPackage.MaxRows].Value, + "Last row should be 0 + 5 = 5"); + } + + [TestMethod] + public void FullColumnRefEqualsString_VirtualRowsShouldBeFalse() + { + // Arrange - comparison operator: A:A="Hello" + // Virtual rows are empty, empty != "Hello" => FALSE + var ws = _workbook.Worksheets.Add("Sheet1"); + ws.Cells["A1"].Value = "Hello"; + ws.Cells["A2"].Value = "World"; + ws.Cells["A3"].Value = "Hello"; + ws.Cells["B1"].Formula = "A:A=\"Hello\""; + + // Act + ws.Calculate(); + + // Assert - physical rows + Assert.AreEqual(true, ws.Cells["B1"].Value); + Assert.AreEqual(false, ws.Cells["B2"].Value); + Assert.AreEqual(true, ws.Cells["B3"].Value); + // Virtual rows: empty = "Hello" => FALSE + Assert.AreEqual(false, ws.Cells["B4"].Value, + "Virtual row: empty = \"Hello\" should be FALSE"); + Assert.AreEqual(false, ws.Cells["B" + ExcelPackage.MaxRows].Value, + "Last row should also be FALSE"); + } + + [TestMethod] + public void TwoFullColumnRefsMultiplied_VirtualRowsShouldBeZero() + { + // Arrange - (A:A=x) * (B:B=y) pattern used in MATCH criteria + var ws = _workbook.Worksheets.Add("Sheet1"); + ws.Cells["A1"].Value = "Alpha"; + ws.Cells["A2"].Value = "Beta"; + ws.Cells["B1"].Value = "X"; + ws.Cells["B2"].Value = "Y"; + ws.Cells["C1"].Value = 100d; + ws.Cells["C2"].Value = 200d; + + // MATCH(1, (A:A="Alpha")*(B:B="X"), 0) should find row 1 + ws.Cells["E1"].Formula = "MATCH(1,(A:A=\"Alpha\")*(B:B=\"X\"),0)"; + // INDEX to retrieve the value + ws.Cells["F1"].Formula = "INDEX(C:C,MATCH(1,(A:A=\"Alpha\")*(B:B=\"X\"),0))"; + + // Act + ws.Calculate(); + + // Assert + Assert.AreEqual(1, ws.Cells["E1"].Value, + "MATCH should find row 1"); + Assert.AreEqual(100d, ws.Cells["F1"].Value, + "INDEX should return 100 for Alpha+X"); + } + + [TestMethod] + public void CrossSheetFullColumnRef_IndexMatch_ShouldWork() + { + // Arrange - the original problem pattern from the design doc + var dataWs = _workbook.Worksheets.Add("Data"); + dataWs.Cells["A1"].Value = "Item1"; + dataWs.Cells["A2"].Value = "Item2"; + dataWs.Cells["A3"].Value = "Item1"; + dataWs.Cells["B1"].Value = "Day Shift"; + dataWs.Cells["B2"].Value = "Day Shift"; + dataWs.Cells["B3"].Value = "Night Shift"; + dataWs.Cells["C1"].Value = 50d; + dataWs.Cells["C2"].Value = 75d; + dataWs.Cells["C3"].Value = 90d; + + var ws = _workbook.Worksheets.Add("Formulas"); + ws.Cells["A1"].Value = "Item1"; + ws.Cells["B1"].Formula = + "IFERROR(INDEX(Data!$C:$C,MATCH(1,(Data!$A:$A=A1)*(Data!$B:$B=\"Day Shift\"),0)),\"-\")"; + // Also test a lookup that should NOT match + ws.Cells["A2"].Value = "NoMatch"; + ws.Cells["B2"].Formula = + "IFERROR(INDEX(Data!$C:$C,MATCH(1,(Data!$A:$A=A2)*(Data!$B:$B=\"Day Shift\"),0)),\"-\")"; + + // Act + _workbook.Calculate(); + + // Assert + Assert.AreEqual(50d, ws.Cells["B1"].Value, + "Should find Item1 + Day Shift => 50"); + Assert.AreEqual("-", ws.Cells["B2"].Value, + "NoMatch should fall through to IFERROR => \"-\""); + } + [TestMethod] + public void ScalarDivideFullColumnRef_VirtualRowsShouldBeDivByZeroError() + { + // Arrange - 1/B:B where B has data in rows 1-2 + // Virtual default: 1 / null = 1 / 0 = #DIV/0! + var ws = _workbook.Worksheets.Add("Sheet1"); + ws.Cells["B1"].Value = 2d; + ws.Cells["B2"].Value = 4d; + ws.Cells["C1"].Formula = "1/B:B"; + + // Act + ws.Calculate(); + + // Assert - physical rows + Assert.AreEqual(0.5d, ws.Cells["C1"].Value); + Assert.AreEqual(0.25d, ws.Cells["C2"].Value); + // Virtual rows: 1 / 0 => #DIV/0! + var virtualVal = ws.Cells["C3"].Value; + Assert.IsInstanceOfType(virtualVal, typeof(ExcelErrorValue), + "Virtual row: 1/0 should produce an error"); + Assert.AreEqual(eErrorType.Div0, ((ExcelErrorValue)virtualVal).Type, + "Error should be #DIV/0!"); + // Last row should also be #DIV/0! + var lastVal = ws.Cells["C" + ExcelPackage.MaxRows].Value; + Assert.IsInstanceOfType(lastVal, typeof(ExcelErrorValue), + "Last row should also be #DIV/0!"); + } + + [TestMethod] + public void FullColumnRefConcat_VirtualRowsShouldConcatEmpty() + { + // Arrange - A:A&"!" via the Concat operator + // Virtual default: "" & "!" = "!" + var ws = _workbook.Worksheets.Add("Sheet1"); + ws.Cells["A1"].Value = "Hello"; + ws.Cells["A2"].Value = "World"; + ws.Cells["B1"].Formula = "A:A&\"!\""; + + // Act + ws.Calculate(); + + // Assert - physical rows + Assert.AreEqual("Hello!", ws.Cells["B1"].Value); + Assert.AreEqual("World!", ws.Cells["B2"].Value); + // Virtual rows: "" & "!" = "!" + Assert.AreEqual("!", ws.Cells["B3"].Value, + "Virtual row: empty & \"!\" should be \"!\""); + Assert.AreEqual("!", ws.Cells["B" + ExcelPackage.MaxRows].Value, + "Last row should also be \"!\""); + } + + [TestMethod] + public void NegateFullColumnRef_VirtualRowsShouldBeZero() + { + // Arrange - negation: -A:A + // Virtual default: -(null) = -(0) = 0 + var ws = _workbook.Worksheets.Add("Sheet1"); + ws.Cells["A1"].Value = 5d; + ws.Cells["A2"].Value = -3d; + ws.Cells["B1"].Formula = "-A:A"; + + // Act + ws.Calculate(); + + // Assert - physical rows + Assert.AreEqual(-5d, ws.Cells["B1"].Value); + Assert.AreEqual(3d, ws.Cells["B2"].Value); + // Virtual rows: -(0) = 0 + Assert.AreEqual(0d, ws.Cells["B3"].Value, + "Virtual row: -0 should be 0"); + Assert.AreEqual(0d, ws.Cells["B" + ExcelPackage.MaxRows].Value, + "Last row should also be 0"); + } + } +} \ No newline at end of file diff --git a/src/EPPlusTest/Issues/FormulaCalculationIssues.cs b/src/EPPlusTest/Issues/FormulaCalculationIssues.cs index 3747cf7a10..07b9bb3a81 100644 --- a/src/EPPlusTest/Issues/FormulaCalculationIssues.cs +++ b/src/EPPlusTest/Issues/FormulaCalculationIssues.cs @@ -3,6 +3,7 @@ using OfficeOpenXml.FormulaParsing; using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions; using OfficeOpenXml.FormulaParsing.LexicalAnalysis; +using OfficeOpenXml.FormulaParsing.Logging; using OfficeOpenXml.Sorting; using System; using System.Collections.Generic;