Skip to content

Bug/s870 #2013

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Jun 5, 2025
3 changes: 1 addition & 2 deletions src/EPPlus/Core/CellStore/CellStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1175,7 +1175,6 @@ public void Dispose()

internal bool NextCell(ref int row, ref int col)
{

return NextCell(ref row, ref col, 0, 0, ExcelPackage.MaxRows, ExcelPackage.MaxColumns);
}
internal bool NextCell(ref int row, ref int col, int minRow, int minColPos, int maxRow, int maxColPos)
Expand Down Expand Up @@ -1590,7 +1589,7 @@ internal bool GetPrevCell(ref int row, ref int colPos, int startRow, int startCo
}
else
{
return GetPrevCell(ref colPos, ref row, startRow, startColPos, endColPos);
return GetPrevCell(ref row, ref colPos, startRow, startColPos, endColPos);
}
}
}
Expand Down
7 changes: 3 additions & 4 deletions src/EPPlus/Drawing/Chart/ExcelChartAxis.cs
Original file line number Diff line number Diff line change
Expand Up @@ -528,12 +528,11 @@ public void RemoveTitle()
/// Or Category Axis to Serie Axis and vice versa.
/// </summary>
/// <param name="type"></param>
/// <param name="throwWarning">Set to false to allow axisTypes with unexpected behaviour</param>
/// <param name="throwException">Set to false to allow axisTypes with unexpected behaviour</param>
/// <exception cref="InvalidOperationException"></exception>
/// <exception cref="InvalidOperationException"></exception>
public void ChangeAxisType(eAxisType type, bool throwWarning = true)
public void ChangeAxisType(eAxisType type, bool throwException = true)
{
if (throwWarning && _chart.IsAxisTypeSupported(type, this) == false)
if (throwException && _chart.IsAxisTypeSupported(type, this) == false)
{
throw new InvalidOperationException($"Chart Name:{_chart.Name} of Type: {_chart.ChartType.ToEnumString()} " +
$"Cannot change axis with ID:{this.Id} from AxisType:{AxisType} To Type: {type.ToString()}. Data would be missrepresented in chart.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,25 @@ Date Author Change
*************************************************************************************************
22/3/2023 EPPlus Software AB EPPlus v7
*************************************************************************************************/
using System;

using OfficeOpenXml.Utils;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup.LookupUtils
{
internal static class LookupBinarySearch
{
private static int SearchAsc(object s, IRangeInfo lookupRange, IComparer<object> comparer, LookupRangeDirection? direction = null)
{
var nRows = lookupRange.Size.NumberOfRows;
var nCols = lookupRange.Size.NumberOfCols;
//Only look at relevant values. We use this to omit null values above and below actual values
var valueRange = ValueFinder.RangeByValue(lookupRange.Worksheet, lookupRange.Address);

var nRows = valueRange.ToRow - valueRange.FromRow + 1;
var nCols = valueRange.ToCol - valueRange.FromCol + 1;

if (nRows == 0 && nCols == 0) return -1;
int low = 0, high = nCols > nRows ? nCols : nRows, mid;
if(direction.HasValue)
if (direction.HasValue)
{
high = direction.Value == LookupRangeDirection.Vertical ? nRows : nCols;
}
Expand All @@ -38,18 +41,17 @@ private static int SearchAsc(object s, IRangeInfo lookupRange, IComparer<object>
var row = nRows >= nCols ? mid : 0;
if (direction.HasValue)
{

col = direction.Value == LookupRangeDirection.Vertical ? 0 : mid;
row = direction.Value == LookupRangeDirection.Vertical ? mid : 0;
}

//Row and col are 0-based if equal we will be past the last value due to GetOffset
if(row == nRows || col == nCols)
if (row == nRows || col == nCols)
{
break;
}
var val = lookupRange.GetOffset(row, col);

var val = lookupRange.GetValue(valueRange.FromRow + row, valueRange.FromCol + col);

var result = comparer.Compare(s, val);

Expand All @@ -60,8 +62,9 @@ private static int SearchAsc(object s, IRangeInfo lookupRange, IComparer<object>
low = mid + 1;

else
return mid;
return valueRange.FromRow - 1 + mid;
}

return ~low;
}

Expand Down
103 changes: 103 additions & 0 deletions src/EPPlus/Utils/ValueFinder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
using OfficeOpenXml.FormulaParsing.Excel.Functions.Information;
using OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup;
using OfficeOpenXml.FormulaParsing.LexicalAnalysis;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;


namespace OfficeOpenXml.Utils
{
internal static class ValueFinder
{
internal static List<int> FirstValueCell(ExcelWorksheet sheet, FormulaRangeAddress address)
{
var fromRow = address.FromRow;
var fromCol = address.FromCol;
var someValue = sheet._values.GetValue(address.FromRow, address.FromCol);
var valueOfValue = sheet._values.GetValue(address.FromRow, address.FromCol)._value;
if (valueOfValue == null)
{
while (sheet._values.NextCell(ref fromRow, ref fromCol, address.FromRow, address.FromCol, address.ToRow, address.ToCol))
{
if (sheet._values.GetValue(fromRow, fromCol)._value != null)
{
return new List<int> { fromRow, fromCol - 1 };
}
}
}
return new List<int> { fromRow, fromCol };
}

internal static List<int> LastValueCell(ExcelWorksheet sheet, FormulaRangeAddress address)
{
//The range might refer to cells outside the worksheet dimension if no value has been set.
//Therefore take the lower value between toRow and toCol and the dimension version of them
var toRow = sheet.Dimension._toRow < address.ToRow ? sheet.Dimension._toRow : address.ToRow;
var toCol = sheet.Dimension._toCol < address.ToCol ? sheet.Dimension._toCol : address.ToCol;

if (sheet._values.GetValue(toRow, toCol)._value == null)
{
while (sheet._values.PrevCell(ref toRow, ref toCol) && toRow > 0)
{
if (toCol > 0 && sheet._values.GetValue(toRow, toCol)._value != null)
{
return new List<int> { toRow, toCol };
}
}
return null;
}
return new List<int> { toRow, toCol };
}

internal static SimpleAddress RangeByValue(ExcelWorksheet sheet, FormulaRangeAddress address)
{
var fvc = FirstValueCell(sheet, address);
var lvc = LastValueCell(sheet, address);

var fromRow = fvc[0];
var toRow = lvc[0];
int fromCol, toCol;

if (fvc[1] == address.FromRow)
{
fromCol = fvc[1];
}
else
{
int r = fromRow, c = address.FromCol;
while (sheet._values.NextCellByColumn(ref r, ref c, fromRow, toRow, address.ToCol - address.FromCol))
{
if (sheet._values.GetValue(r, c)._value != null)
{
break;
}
r++;
}
fromCol = c;
}

if (lvc[1] == address.ToCol)
{
toCol = lvc[1];
}
else
{
int r = toRow, c = address.ToCol;
while (sheet._values.PrevCellByColumn(ref r, ref c, fromRow, toRow, address.ToCol - address.FromCol))
{
if (sheet._values.GetValue(r, c)._value != null)
{
break;
}
r--;
}
toCol = c;
}

SimpleAddress subRange = new SimpleAddress { FromRow = Math.Min(fromRow, toRow), FromCol = Math.Min(fromCol, toCol), ToRow = Math.Max(fromRow, toRow), ToCol = Math.Max(fromCol, toCol) };
return subRange;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,6 @@ public void LookupShouldReturnResultFromMatchingSecondArrayHorizontal()
sheet.Calculate();
var result = sheet.Cells["D1"].Value;
Assert.AreEqual("B", result);

}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using OfficeOpenXml;
using System.Collections.Generic;

namespace EPPlusTest.FormulaParsing.Excel.Functions.RefAndLookup
{
Expand Down Expand Up @@ -290,6 +291,91 @@ public void ApproximateStringsShouldFind()

// }
//}

[TestMethod]
public void SC870_EpplusOnly()
{
using (var p = OpenPackage("EpplusNullAboveAndBelow.xlsx", true))
{
var wb = p.Workbook;
var ws = wb.Worksheets.Add("VLookupTest");
List<int> searchValues = new List<int> { 1, 2, 4, 7, 11, 16, 21, 27 };
List<int> resultValues = new List<int> { 400, 365, 315, 280, 250, 215, 200, 170 };

ws.Cells["B6:B13"].LoadFromCollection(searchValues);
ws.Cells["C6:C13"].LoadFromCollection(resultValues);

ws.Cells["A11"].Value = 1;

//Testing that VLookup (or rather binary search lookup) can handle values of 'null' in a range above and below target.
ws.Cells["F6"].Formula = "VLOOKUP(A11, B:C, 2, TRUE)";

ws.Calculate();

var outputValue = ws.Cells["F6"].Value;
Assert.AreEqual(400, outputValue);

//Ensure it works for each of the values
for (int i = 1; i < searchValues.Count; i++)
{
var formulaCell = ws.Cells[6 + i, 6];
formulaCell.Formula = $"VLOOKUP({searchValues[i]}, B:C, 2, TRUE)";
formulaCell.Calculate();
Assert.AreEqual(resultValues[i], formulaCell.Value);
}

//Save Workbook
SaveAndCleanup(p);
}
}

[TestMethod]
public void SC870()
{
using (var package = OpenTemplatePackage("s870.xlsx"))
{
var wb = package.Workbook;
var worksheet = package.Workbook.Worksheets[0];

foreach (var sheet in package.Workbook.Worksheets)
{
sheet.Hidden = eWorkSheetHidden.Visible;
}

worksheet.Cells["F15"].Formula = "VLOOKUP(B11, Salgsfragt!B:C, 2, TRUE)";

var sWs = package.Workbook.Worksheets.GetByName("Salgsfragt");
sWs.Cells["B4"].Value = null;
sWs.Cells["B2"].Value = null;

worksheet.Cells["F15"].Calculate();

var someVal = worksheet.Cells["F15"].Value;
var errorText = worksheet.Cells["D8"].Text;

var cellEuItemPrice = worksheet.Cells["C18"];
var cellEuTransportPrice = worksheet.Cells["C19"];
var cellEuTotal = worksheet.Cells["C20"];

var cellDKItemPrice = worksheet.Cells["C24"];
var cellDKTransportPrice = worksheet.Cells["C25"];
var cellDKTotal = worksheet.Cells["C26"];

worksheet.Calculate();
decimal tolerance = 0.1M;

Assert.AreEqual(301.01M, (decimal)cellEuItemPrice.GetCellValue<double>(), tolerance);
Assert.AreEqual(53.62M, (decimal)cellEuTransportPrice.GetCellValue<double>(), tolerance);
Assert.AreEqual(354.62M, (decimal)cellEuTotal.GetCellValue<double>(), tolerance);

Assert.AreEqual(2245.50M, (decimal)cellDKItemPrice.GetCellValue<double>(), tolerance);
Assert.AreEqual(400M, (decimal)cellDKTransportPrice.GetCellValue<double>(), tolerance);
Assert.AreEqual(2645.50M, (decimal)cellDKTotal.GetCellValue<double>(), tolerance);

SaveAndCleanup(package);
}
}

[TestMethod]
public void PriorAddressExpressionWorksheetShouldBeCleared()
{
Expand Down
14 changes: 8 additions & 6 deletions src/EPPlusTest/Issues/ChartIssues.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using OfficeOpenXml;
using OfficeOpenXml.Drawing;
using OfficeOpenXml.Drawing.Chart;
using OfficeOpenXml.Drawing.Chart.Style;
using OfficeOpenXml;
using System.IO;
using System.Drawing;
using System.Text;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Globalization;
using System.IO;
using System.Security.Principal;
using System.Text;

namespace EPPlusTest.Issues
{
Expand Down Expand Up @@ -386,9 +388,9 @@ public void i886()
}

[TestMethod]
public void CreateStringLitterals()
public void CreateStringLiterals()
{
using (var package = OpenPackage("LitteralsSetting.xlsx", true))
using (var package = OpenPackage("LiteralsSetting.xlsx", true))
{
var wb = package.Workbook;
var ws = wb.Worksheets.Add("NewWork");
Expand Down