Skip to content

#2010 - String issues in formula calculations #2011

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 4 commits into from
May 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ public RpnOptimizedDependencyChain(ExcelWorkbook wb, ExcelCalculationOption opti
config.CacheExpressions = options.CacheExpressions;
config.PrecisionAndRoundingStrategy = options.PrecisionAndRoundingStrategy;
config.AlwaysRefreshImageFunction = options.AlwaysRefreshImageFunction;
config.EnableUnicodeAwareStringOperations = options.EnableUnicodeAwareStringOperations;
});

}
Expand All @@ -48,6 +49,10 @@ internal void AddFormulaToChain(RpnFormula f, FormulaRangeAddress[] addresses)
QuadTree<ulong> qr;
foreach (var address in addresses)
{
if(address.ExternalReferenceIx > 0)
{
return;
}
var ix = address.WorksheetIx; ;
if (FormulaRangeReferences.TryGetValue(ix, out qr) == false)
{
Expand Down
2 changes: 2 additions & 0 deletions src/EPPlus/FormulaParsing/Excel/Functions/Logical/Switch.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ public Switch()
}
public override string NamespacePrefix => "_xlfn.";
public override int ArgumentMinLength => 3;

public override ExcelFunctionArrayBehaviour ArrayBehaviour => ExcelFunctionArrayBehaviour.FirstArgCouldBeARange;
public override CompileResult Execute(IList<FunctionArgument> arguments, ParsingContext context)
{
var expression = arguments[0].ValueFirst;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public override CompileResult Execute(IList<FunctionArgument> arguments, Parsing
if (e2 != null) return CompileResult.GetErrorResult(e2.Type);
}
var start = 1d;
if(argCount > 2)
if(argCount > 2 && arguments[2].DataType != DataType.Empty)
{
start = ArgToDecimal(arguments, 2, out ExcelErrorValue e3);
if (e3 != null) return CompileResult.GetErrorResult(e3.Type);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,25 @@ public virtual int Compare(object x, object y, int sortOrder)
}
else
{
if (x != null && y == null) return -1;
if (x == null && y != null) return 1;
v = ComparerUtil.CompareObjects(x, y);
}
return v * (sortOrder > -1 ? 1 : -1);
}

public virtual int Compare(object x, object y, int sortOrder, ParsingContext context)
{
int v = 0;
if (_matchMode == LookupMatchMode.Wildcard || _matchMode == LookupMatchMode.ExactMatchWithWildcard)
{
v = _vm.IsMatch(x, y);
}
else
{
v = ComparerUtil.CompareObjects(x, y, context);
}
return v * (sortOrder > -1 ? 1 : -1);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,33 @@ public override CompileResult Execute(IList<FunctionArgument> arguments, Parsing
}
var range = arg1.ValueAsRangeInfo;
var rangeDef = new RangeDefinition(range.Size.NumberOfRows, range.Size.NumberOfCols);
var sortIndex = 1;
var sortIndexes = new List<int> { 1 };
if(arguments.Count > 1)
{
sortIndex = ArgToInt(arguments, 1, out ExcelErrorValue e1, 1);
if (e1 != null) return CompileResult.GetErrorResult(e1.Type);
if (arguments[1].IsExcelRange)
{
sortIndexes.Clear();
var rng = arguments[1].ValueAsRangeInfo;
for(var row = 0; row < rng.Size.NumberOfRows; row++)
{
for(var col = 0; col < rng.Size.NumberOfCols; col++)
{
var indexObj = rng.GetOffset(row, col);
if(indexObj is not int index)
{
return CompileResult.GetErrorResult(eErrorType.Value);
}
sortIndexes.Add(index);
}
}
}
else
{
var sortIndex = ArgToInt(arguments, 1, out ExcelErrorValue e1, 1);
if (e1 != null) return CompileResult.GetErrorResult(e1.Type);
sortIndexes[0] = sortIndex;
}

}
var sortOrder = 1;
if(arguments.Count > 2)
Expand All @@ -61,17 +83,21 @@ public override CompileResult Execute(IList<FunctionArgument> arguments, Parsing

//Validate
var maxIndex = byCol ? range.Size.NumberOfCols : range.Size.NumberOfRows;
if (sortIndex < 1 || sortIndex > maxIndex) return CreateResult(eErrorType.Value);
foreach(var sortIndex in sortIndexes)
{
if (sortIndex < 1 || sortIndex > maxIndex) return CreateResult(eErrorType.Value);
}

if (sortOrder != -1 && sortOrder != 1) return CreateResult(eErrorType.Value);
var sortedRange = GetSortedRange(range, sortIndex, sortOrder, byCol);
var sortedRange = GetSortedRange(range, sortIndexes, sortOrder, byCol, context);
return CreateDynamicArrayResult(sortedRange, DataType.ExcelRange);
}

private InMemoryRange GetSortedRange(IRangeInfo sourceRange, int sortIndex, int sortOrder, bool byCol)
private InMemoryRange GetSortedRange(IRangeInfo sourceRange, List<int> sortIndexes, int sortOrder, bool byCol, ParsingContext context)
{
return byCol ?
_sorter.SortByCol(sourceRange, sortIndex, sortOrder) :
_sorter.SortByRow(sourceRange, sortIndex, sortOrder);
_sorter.SortByCol(sourceRange, sortIndexes, sortOrder, context) :
_sorter.SortByRow(sourceRange, sortIndexes, sortOrder, context);
}
/// <summary>
/// If the function is allowed in a pivot table calculated field
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ namespace OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup.Sorting
internal class InMemoryRangeSorter
{
private readonly LookupComparer _comparer = new LookupComparer(LookupMatchMode.ExactMatch);
public InMemoryRange SortByRow(IRangeInfo sourceRange, int colIndex, int sortOrder)
public InMemoryRange SortByCol(IRangeInfo sourceRange, List<int> rowIndexes, int sortOrder, ParsingContext context)
{
var rangeDef = new RangeDefinition(sourceRange.Size.NumberOfRows, sourceRange.Size.NumberOfCols);
var sortedRange = new InMemoryRange(rangeDef);
Expand All @@ -38,28 +38,41 @@ public InMemoryRange SortByRow(IRangeInfo sourceRange, int colIndex, int sortOrd
}
columns.Add(rows);
}
var colIx = colIndex - 1;
var colToSortList = columns[colIx].ToList();
var sortedList = colToSortList.Where(x => x.Value != null).ToList();
sortedList.Sort((a, b) => _comparer.Compare(a.Value, b.Value, sortOrder));
var nullValues = colToSortList.Where(x => x.Value == null);
sortedList.AddRange(nullValues);
for (var row = 0; row < sortedList.Count; row++)
columns.Sort((a, b) =>
{
var sortedColItem = sortedList[row];
sortedRange.SetValue(row, colIx, sortedColItem.Value);
for (var col = 0; col < columns.Count; col++)
foreach (var rowIx in rowIndexes)
{
if (col == colIx) continue;
var colItem = columns[col].GetByOriginalIndex(sortedColItem.OriginalIndex);
sortedRange.SetValue(row, col, colItem.Value);

var aColVal = a.GetByOriginalIndex(rowIx - 1);
var bColVal = b.GetByOriginalIndex(rowIx - 1);
if (aColVal.Value == null)
{
if (bColVal.Value == null) return 0;
return 1;
}
else if (bColVal.Value == null)
{
return -1;
}
var res = _comparer.Compare(aColVal.Value, bColVal.Value, sortOrder, context);
if (res != 0) return res;
}
return 0;
});
var colIx = 0;
foreach (var col in columns)
{
var cellIx = 0;
foreach (var cell in col.ToList())
{
sortedRange.SetValue(cellIx, colIx, cell.Value);
cellIx++;
}
colIx++;
}
return sortedRange;
}

public InMemoryRange SortByCol(IRangeInfo sourceRange, int rowIndex, int sortOrder)
public InMemoryRange SortByRow(IRangeInfo sourceRange, List<int> colIndexes, int sortOrder, ParsingContext context)
{
var rangeDef = new RangeDefinition(sourceRange.Size.NumberOfRows, sourceRange.Size.NumberOfCols);
var sortedRange = new InMemoryRange(rangeDef);
Expand All @@ -75,20 +88,36 @@ public InMemoryRange SortByCol(IRangeInfo sourceRange, int rowIndex, int sortOrd
}
rows.Add(cols);
}
var rowIx = rowIndex - 1;
var rowToSortList = rows[rowIx].ToList();
rowToSortList.Sort((a, b) => _comparer.Compare(a.Value, b.Value, sortOrder));
for (var col = 0; col < rowToSortList.Count; col++)
rows.Sort((a, b) =>
{
foreach(var colIx in colIndexes)
{
var aColVal = a.GetByOriginalIndex(colIx - 1);
var bColVal = b.GetByOriginalIndex(colIx - 1);
if(aColVal.Value == null)
{
if (bColVal.Value == null) return 0;
return 1;
}
else if(bColVal.Value == null)
{
return -1;
}
var res = _comparer.Compare(aColVal.Value, bColVal.Value, sortOrder, context);
if (res != 0) return res;
}
return 0;
});
var rowIx = 0;
foreach(var row in rows)
{
var sortedRowItem = rowToSortList[col];
sortedRange.SetValue(rowIx, col, sortedRowItem.Value);
for (var row = 0; row < rows.Count; row++)
var cellIx = 0;
foreach(var cell in row.ToList())
{
if (row == rowIx) continue;
var colItem = rows[row].GetByOriginalIndex(sortedRowItem.OriginalIndex);
sortedRange.SetValue(row, col, colItem.Value);

sortedRange.SetValue(rowIx, cellIx, cell.Value);
cellIx++;
}
rowIx++;
}
return sortedRange;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,18 @@ public SortByComparer() : base(LookupMatchMode.ExactMatch)

public override int Compare(object x, object y)
{
return Compare(x, y, 1);
if (x == null && y != null)
{
return 1;
}
else if (x != null && y == null)
{
return -1;
}
return ComparerUtil.CompareObjects(x, y) * 1;
}

public override int Compare(object x, object y, int sortOrder)
public override int Compare(object x, object y, int sortOrder, ParsingContext context)
{
// null values should always be sorted last
if(x == null && y != null)
Expand All @@ -41,7 +49,7 @@ public override int Compare(object x, object y, int sortOrder)
{
return -1;
}
return ComparerUtil.CompareObjects(x, y) * sortOrder;
return ComparerUtil.CompareObjects(x, y, context) * sortOrder;
}
}
}
5 changes: 5 additions & 0 deletions src/EPPlus/FormulaParsing/Excel/Functions/Text/Left.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Date Author Change
using System.Collections.Generic;
using OfficeOpenXml.FormulaParsing.Excel.Functions.Metadata;
using OfficeOpenXml.FormulaParsing.FormulaExpressions;
using OfficeOpenXml.Utils;

namespace OfficeOpenXml.FormulaParsing.Excel.Functions.Text
{
Expand All @@ -38,6 +39,10 @@ public override CompileResult Execute(IList<FunctionArgument> arguments, Parsing
}
if (str.Length < length)
length = str.Length;
if(context.Configuration.EnableUnicodeAwareStringOperations)
{
return CreateResult(str.UnitcodeSubstring(length), DataType.String);
}
return CreateResult(str.Substring(0, length), DataType.String);
}
}
Expand Down
8 changes: 8 additions & 0 deletions src/EPPlus/FormulaParsing/Excel/Functions/Text/Rept.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ namespace OfficeOpenXml.FormulaParsing.Excel.Functions.Text
internal class Rept : ExcelFunction
{
public override int ArgumentMinLength => 2;

public override ExcelFunctionArrayBehaviour ArrayBehaviour => ExcelFunctionArrayBehaviour.Custom;

public override void ConfigureArrayBehaviour(ArrayBehaviourConfig config)
{
config.SetArrayParameterIndexes(0, 1);
}

public override CompileResult Execute(IList<FunctionArgument> arguments, ParsingContext context)
{
var str = ArgToString(arguments, 0);
Expand Down
30 changes: 30 additions & 0 deletions src/EPPlus/FormulaParsing/Excel/Functions/Text/Right.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Date Author Change
using System.Collections.Generic;
using OfficeOpenXml.FormulaParsing.Excel.Functions.Metadata;
using OfficeOpenXml.FormulaParsing.FormulaExpressions;
using OfficeOpenXml.Utils;

namespace OfficeOpenXml.FormulaParsing.Excel.Functions.Text
{
Expand All @@ -36,6 +37,35 @@ public override CompileResult Execute(IList<FunctionArgument> arguments, Parsing
var startIx = str.Length - length;
if (startIx < 0)
startIx = 0;

int startIndex = str.Length;
if(context.Configuration.EnableUnicodeAwareStringOperations)
{
int unicodeLength = 0;
for (int i = str.Length - 1; i >= 0; i--)
{
char c = str[i];

// Handle surrogate pairs correctly
if (char.IsLowSurrogate(c) && i > 0 && char.IsHighSurrogate(str[i - 1]))
{
i--; // Move past the high surrogate
}

unicodeLength++;

if (unicodeLength == length)
{
startIndex = i; // Set the correct start position
break;
}
}
}
if (context.Configuration.EnableUnicodeAwareStringOperations)
{
var res = str.UnitcodeSubstring(startIndex, str.Length - startIx);
return CreateResult(res, DataType.String);
}
return CreateResult(str.Substring(startIx, str.Length - startIx), DataType.String);
}
}
Expand Down
5 changes: 5 additions & 0 deletions src/EPPlus/FormulaParsing/Excel/Functions/Text/Text.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ public class Text : ExcelFunction
/// Minimum arguments
/// </summary>
public override int ArgumentMinLength => 1;

/// <summary>
/// First arg could be a range
/// </summary>
public override ExcelFunctionArrayBehaviour ArrayBehaviour => ExcelFunctionArrayBehaviour.FirstArgCouldBeARange;
/// <summary>
/// Execute function
/// </summary>
Expand Down
Loading