diff --git a/src/EPPlus/FormulaParsing/Excel/Functions/RefAndLookup/LookupUtils/XlookupScanner.cs b/src/EPPlus/FormulaParsing/Excel/Functions/RefAndLookup/LookupUtils/XlookupScanner.cs index c244811db..91f2d7977 100644 --- a/src/EPPlus/FormulaParsing/Excel/Functions/RefAndLookup/LookupUtils/XlookupScanner.cs +++ b/src/EPPlus/FormulaParsing/Excel/Functions/RefAndLookup/LookupUtils/XlookupScanner.cs @@ -83,39 +83,43 @@ public int FindIndex() private int FindIndexInternal() { var direction = GetLookupDirection(); - int maxItems; + int maxItems; + int startOffset; if (direction == LookupRangeDirection.Vertical) { - maxItems = GetMaxItemsRow(_lookupRange); + maxItems = GetMaxItemsRow(_lookupRange, out startOffset); } else { - //dimensionItems = _lookupRange.Dimension.ToCol - _lookupRange.Dimension.FromCol + 1; - //maxItems = _lookupRange.Size.NumberOfCols > dimensionItems ? dimensionItems : _lookupRange.Size.NumberOfCols; - maxItems = GetMaxItemsColumns(_lookupRange); + maxItems = GetMaxItemsColumns(_lookupRange, out startOffset); } int closestBelowIx = -1; int closestAboveIx = -1; object closestBelow = null; object closestAbove = null; + // ix is relative to the clamped start (startOffset). The value read and the + // index returned are offset by startOffset so they stay relative to the + // range's own FromRow/FromCol, while iteration is limited to the populated + // area to preserve performance on open ranges (e.g. A:A). var ix = _searchMode == LookupSearchMode.ReverseStartingAtLast ? maxItems - 1 : 0; - while (ix >= 0) + while (ix >= 0 && ix < maxItems) { + var actualIx = startOffset + ix; object value = direction == LookupRangeDirection.Vertical ? - _lookupRange.GetOffset(ix, 0) : - _lookupRange.GetOffset(0, ix); + _lookupRange.GetOffset(actualIx, 0) : + _lookupRange.GetOffset(0, actualIx); var cr = _comparer.Compare(_lookupValue, value); if (cr == 0) { - return ix; + return actualIx; } else if (cr < 0) { if (closestAbove == null || _comparer.Compare(closestAbove, value) > 0) { closestAbove = value; - closestAboveIx = ix; + closestAboveIx = actualIx; } } else @@ -123,16 +127,12 @@ private int FindIndexInternal() if (closestBelow == null || _comparer.Compare(closestBelow, value) < 0) { closestBelow = value; - closestBelowIx = ix; + closestBelowIx = actualIx; } } if (_searchMode == LookupSearchMode.StartingAtFirst) { ix++; - if (ix >= maxItems) - { - ix = -1; - } } else { @@ -151,11 +151,14 @@ private int FindIndexInternal() } - private int GetMaxItemsRow(IRangeInfo lookupRange) + private int GetMaxItemsRow(IRangeInfo lookupRange, out int startOffset) { + startOffset = 0; var adjusted = lookupRange.GetAddressDimensionAdjusted(0); if (adjusted != null) { + startOffset = adjusted.FromRow - lookupRange.Address.FromRow; + if (startOffset < 0) startOffset = 0; return adjusted.ToRow - adjusted.FromRow + 1; } if (lookupRange.Address.ToRow > lookupRange.Dimension.ToRow) @@ -165,11 +168,14 @@ private int GetMaxItemsRow(IRangeInfo lookupRange) return _lookupRange.Size.NumberOfRows; } - private int GetMaxItemsColumns(IRangeInfo lookupRange) + private int GetMaxItemsColumns(IRangeInfo lookupRange, out int startOffset) { + startOffset = 0; var adjusted = lookupRange.GetAddressDimensionAdjusted(0); if (adjusted != null) { + startOffset = adjusted.FromCol - lookupRange.Address.FromCol; + if (startOffset < 0) startOffset = 0; return adjusted.ToCol - adjusted.FromCol + 1; } if (lookupRange.Address.ToCol > lookupRange.Dimension.ToCol) diff --git a/src/EPPlusTest/FormulaParsing/Excel/Functions/RefAndLookup/LookupScannerTests.cs b/src/EPPlusTest/FormulaParsing/Excel/Functions/RefAndLookup/LookupScannerTests.cs index ba2f71e43..ef215f994 100644 --- a/src/EPPlusTest/FormulaParsing/Excel/Functions/RefAndLookup/LookupScannerTests.cs +++ b/src/EPPlusTest/FormulaParsing/Excel/Functions/RefAndLookup/LookupScannerTests.cs @@ -150,5 +150,22 @@ public void ShouldFindExactMatch_StartAtFirst_Horizontal_1() var ix = scanner.FindIndex(); Assert.AreEqual(2, ix); } + + [TestMethod] + public void ShouldFindExactMatch_WhenRangeStartsBeforeWorksheetDimension() + { + // Arrange: Leave rows 1-4 empty. Worksheet dimension will start at Row 5. + _sheet.Cells[5, 2].Value = "Apple"; + _sheet.Cells[6, 2].Value = "Pear"; + // Lookup range B2:B6 starts at row 2 (before worksheet dimension starts). + var ri = new RangeInfo(_sheet, _sheet.Cells["B2:B6"]); + var scanner = new XlookupScanner("Pear", ri, LookupSearchMode.StartingAtFirst, LookupMatchMode.ExactMatch); + + // Act + var ix = scanner.FindIndex(); + + // Assert: "Pear" is at index 4 (relative to B2). In EPPlus 8.5.0+ this returns -1 instead. + Assert.AreEqual(4, ix); + } } } diff --git a/src/EPPlusTest/FormulaParsing/Excel/Functions/RefAndLookup/XLookupTests.cs b/src/EPPlusTest/FormulaParsing/Excel/Functions/RefAndLookup/XLookupTests.cs index 80aa4ea81..5b68da0a6 100644 --- a/src/EPPlusTest/FormulaParsing/Excel/Functions/RefAndLookup/XLookupTests.cs +++ b/src/EPPlusTest/FormulaParsing/Excel/Functions/RefAndLookup/XLookupTests.cs @@ -467,5 +467,46 @@ public void XlookupReturnEmptyString() ws.Cells["A2"].Calculate(); Assert.AreEqual("", ws.Cells["A2"].Value); } + + [TestMethod] + public void ShouldFindValue_WhenRangeStartsBeforeWorksheetDimension() + { + // Data starts at row 5, but the lookup range starts at row 2 (before the + // worksheet dimension). Regression test for lookup ranges whose FromRow + // is smaller than Worksheet.Dimension.FromRow (issue in 8.5.0-8.6.3). + _sheet.Cells[5, 1].Value = "Apple"; + _sheet.Cells[5, 2].Value = "+11"; + _sheet.Cells[6, 1].Value = "Pear"; + _sheet.Cells[6, 2].Value = "+22"; + + _sheet.Cells["E2"].Value = "Pear"; + _sheet.Cells["F2"].Formula = "XLOOKUP(E2,A2:A6,B2:B6,\"Not found\")"; + + _sheet.Calculate(); + + Assert.AreEqual("+22", _sheet.Cells["F2"].Value.ToString()); + } + + [TestMethod] + [DataRow(1, "+11")] // StartingAtFirst + [DataRow(-1, "+11")] // ReverseStartingAtLast + public void ShouldFindValue_WhenOpenRangeAndDataStartsFarDown(int searchMode, string expected) + { + // Open column ranges (A:A) where the data begins far down the sheet. + // The scanner must clamp both ends to the worksheet dimension for + // performance while still returning a range-relative index, in both + // forward and reverse search mode. + _sheet.Cells[10000, 1].Value = "Apple"; + _sheet.Cells[10000, 2].Value = "+11"; + _sheet.Cells[10001, 1].Value = "Pear"; + _sheet.Cells[10001, 2].Value = "+22"; + + _sheet.Cells["E2"].Value = "Apple"; + _sheet.Cells["F2"].Formula = $"XLOOKUP(E2,A:A,B:B,\"Not found\", 0, {searchMode})"; + + _sheet.Calculate(); + + Assert.AreEqual(expected, _sheet.Cells["F2"].Value.ToString()); + } } }