Skip to content

Commit

Permalink
#1828 - fixed an issue with shifting formula addresses on sorting a r…
Browse files Browse the repository at this point in the history
…ange (#1830)

* #1828 - fixed an issue with shifting formula addresses on sorting a range

* Improved unit test

* added unit test that creates corrupt wb

* #1828 - fixed failing test RedYellowGreen

* #1828 - more fixes on sorting with shared formulas and empty rows

* Added corrupt workbook test for sort LeftbyRight

* #1828 - added failing unit test

* Fixed sort left-to-right issue. IFS function now returns a dynamic compile result.

* Fixed sorting issue with single cell array formulas

---------

Co-authored-by: Ossian Edström <ossian.edstrom@epplussoftware.com>
Co-authored-by: swmal <{ID}+username}@users.noreply.github.com>
Co-authored-by: JanKallman <jan.kallman@epplussoftware.com>
  • Loading branch information
4 people authored Feb 6, 2025
1 parent 5805148 commit e831d60
Show file tree
Hide file tree
Showing 9 changed files with 322 additions and 18 deletions.
2 changes: 1 addition & 1 deletion src/EPPlus/Core/Worksheet/WorksheetRangeHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,7 @@ private static bool ConvertEffectedSharedFormulaIfReferenceWithinRange(ExcelWork
}
return doConvertSF;
}
private static void ConvertSharedFormulaToCellFormula(ExcelWorksheet ws, SharedFormula sf, ExcelAddressBase sfAddress)
internal static void ConvertSharedFormulaToCellFormula(ExcelWorksheet ws, SharedFormula sf, ExcelAddressBase sfAddress)
{
var sr = sfAddress._fromRow;
var sc = sfAddress._fromCol;
Expand Down
6 changes: 3 additions & 3 deletions src/EPPlus/FormulaParsing/Excel/Functions/Logical/Ifs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ namespace OfficeOpenXml.FormulaParsing.Excel.Functions.Logical
internal class Ifs : ExcelFunction
{
public override string NamespacePrefix => "_xlfn.";
public override int ArgumentMinLength => 2;
public override int ArgumentMinLength => 2;
public override CompileResult Execute(IList<FunctionArgument> arguments, ParsingContext context)
{
var maxArgs = arguments.Count < 254 ? arguments.Count : 254;
Expand All @@ -44,11 +44,11 @@ public override CompileResult Execute(IList<FunctionArgument> arguments, Parsing
var arg = arguments.ElementAt(x + 1);
if(arg.DataType==DataType.ExcelRange)
{
return CompileResultFactory.Create(arg, arg.ValueAsRangeInfo.Address);
return CompileResultFactory.CreateDynamicArray(arg, arg.ValueAsRangeInfo.Address);
}
else
{
return CompileResultFactory.Create(arg.Value);
return CompileResultFactory.CreateDynamicArray(arg.Value);
}
}
}
Expand Down
8 changes: 8 additions & 0 deletions src/EPPlus/FormulaParsing/LexicalAnalysis/FormulaAddress.cs
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,14 @@ public List<Token> TokensWithFullAddresses
}
}

public bool IsSingleCell
{
get
{
return StartRow==EndRow && StartCol==EndCol;
}
}

internal SharedFormula Clone()
{
var sh = new SharedFormula(_ws, StartRow, StartCol, EndRow, EndCol, Formula);
Expand Down
31 changes: 31 additions & 0 deletions src/EPPlus/Sorting/Internal/RangeWorksheetData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Date Author Change
05/7/2021 EPPlus Software AB EPPlus 5.7
*************************************************************************************************/
using OfficeOpenXml.Core.CellStore;
using OfficeOpenXml.Core.Worksheet;
using System;
using System.Collections.Generic;
using System.Linq;
Expand All @@ -26,13 +27,43 @@ internal class RangeWorksheetData
public RangeWorksheetData(ExcelRangeBase range)
{
var worksheet = range.Worksheet;

//Shared formulas spaning multiple cells will be converted to single cell formulas.
ConvertSharedFormulas(range);

Flags = GetItems(range, worksheet._flags);
Formulas = GetItems(range, worksheet._formulas);
Hyperlinks = GetItems(range, worksheet._hyperLinks);
Comments = GetItems(range, worksheet._commentsStore);
Metadata = GetItems(range, worksheet._metadataStore);
}

private void ConvertSharedFormulas(ExcelRangeBase range)
{
var cse = new CellStoreEnumerator<object>(range._worksheet._formulas, range);
var hs = new HashSet<int>();
foreach (var item in cse)
{
if(cse.Value is int sfIx && hs.Contains(sfIx)==false)
{
hs.Add(sfIx);
}
}
foreach (var x in hs)
{
var sf = range._worksheet._sharedFormulas[x];
if(sf.IsSingleCell==false)
{
if(sf.IsArray)
{
throw new InvalidOperationException($"Can't sort part of an array: {sf.Address}");
}
WorksheetRangeHelper.ConvertSharedFormulaToCellFormula(range._worksheet,sf, new ExcelAddressBase(sf.Address));
range._worksheet._sharedFormulas.Remove(x);
}
}
}

public Dictionary<string, byte> Flags { get; private set; }

public Dictionary<string, object> Formulas { get; private set; }
Expand Down
10 changes: 10 additions & 0 deletions src/EPPlus/Sorting/Internal/SortItemFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,21 @@ internal static class SortItemFactory
internal static List<SortItem<ExcelValue>> Create(ExcelRangeBase range)
{
var e = new CellStoreEnumerator<ExcelValue>(range.Worksheet._values, range._fromRow, range._fromCol, range._toRow, range._toCol);
var dim = range.Worksheet.Dimension;
var sortItems = new List<SortItem<ExcelValue>>();
SortItem<ExcelValue> item = new SortItem<ExcelValue>();
var cols = range._toCol - range._fromCol + 1;
if(dim != null && cols > dim.End.Column)
{
cols = dim.End.Column;
}
while (e.Next())
{
if(dim != null)
{
if (e.Row > dim.End.Row) continue;
if (e.Column > dim.End.Column) continue;
}
if (sortItems.Count == 0 || sortItems[sortItems.Count - 1].Row != e.Row)
{
item = new SortItem<ExcelValue>() { Row = e.Row, Items = new ExcelValue[cols] };
Expand Down
67 changes: 53 additions & 14 deletions src/EPPlus/Sorting/RangeSorter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,26 @@ private bool[] CreateDefaultDescendingArray(int[] sortParams)
return descending;
}

private ExcelRangeBase GetSortRange(ExcelRangeBase range, ExcelWorksheet ws)
{
if (ws.Dimension == null) return null;
var dimension = ws.Dimension;
var fromRow = range._fromRow < dimension.Start.Row ? dimension.Start.Row : range._fromRow;
var toRow = range._toRow > dimension.End.Row ? dimension.End.Row : range._toRow;
return ws.Cells[fromRow, range._fromCol, toRow, range._toCol];
}

private void ClearRowsAfter(ExcelRangeBase range, int nRows)
{
var startCol = range._fromCol;
var endCol = range._toCol;
var ws = range.Worksheet;
for(var row = range.Start.Row + nRows; row <= range.End.Row; row++)
{
ws.Cells[row, startCol, row, endCol].Clear();
}
}

public void Sort(
ExcelRangeBase range,
int[] columns,
Expand All @@ -83,12 +103,16 @@ public void Sort(
{
descending = CreateDefaultDescendingArray(columns);
}
var sortItems = SortItemFactory.Create(range);
var ws = range.Worksheet;
var r = GetSortRange(range, ws);
if (r == null) return;
var sortItems = SortItemFactory.Create(r);
var comp = new EPPlusSortComparer(columns, descending, customLists, culture ?? CultureInfo.CurrentCulture, compareOptions);
sortItems.Sort(comp);
var wsd = new RangeWorksheetData(range);
var wsd = new RangeWorksheetData(r);

ApplySortedRange(range, sortItems, wsd);
ApplySortedRange(r, sortItems, wsd);
ClearRowsAfter(r, sortItems.Count);
}

public void SortLeftToRight(
Expand Down Expand Up @@ -121,6 +145,11 @@ private void ApplySortedRange(ExcelRangeBase range, List<SortItem<ExcelValue>> s
{
//Sort the values and styles.
var nColumnsInRange = range._toCol - range._fromCol + 1;
var dim = range.Worksheet.Dimension;
if(dim != null && nColumnsInRange > dim.End.Column)
{
nColumnsInRange = dim.End.Column;
}
_worksheet._values.Clear(range._fromRow, range._fromCol, range._toRow - range._fromRow + 1, nColumnsInRange);
for (var r = 0; r < sortItems.Count; r++)
{
Expand Down Expand Up @@ -193,6 +222,10 @@ private void HandleMetadata(RangeWorksheetData wsd, int row, int col, string add
{
_worksheet._metadataStore.SetValue(row, col, wsd.Metadata[addr]);
}
else
{
_worksheet._metadataStore.Clear(row, col, 1, 1);
}
}

private void HandleFlags(RangeWorksheetData wsd, int row, int col, string addr)
Expand All @@ -201,6 +234,10 @@ private void HandleFlags(RangeWorksheetData wsd, int row, int col, string addr)
{
_worksheet._flags.SetValue(row, col, wsd.Flags[addr]);
}
else
{
_worksheet._flags.Clear(row, col, 1, 1);
}
}

private void HandleComment(RangeWorksheetData wsd, int row, int col, string addr)
Expand All @@ -212,6 +249,10 @@ private void HandleComment(RangeWorksheetData wsd, int row, int col, string addr
var comment = _worksheet._comments._list[i];
comment.Reference = ExcelCellBase.GetAddress(row, col);
}
else
{
_worksheet._commentsStore.Clear(row, col, 1, 1);
}
}

private void HandleFormula(RangeWorksheetData wsd, int row, int col, string addr, int initialRow, int initialCol)
Expand All @@ -223,26 +264,24 @@ private void HandleFormula(RangeWorksheetData wsd, int row, int col, string addr
{
var formula = wsd.Formulas[addr].ToString();
var newFormula = initialRow != row ?
AddressUtility.ShiftAddressRowsInFormula(string.Empty, formula, initialRow, row - initialRow) :
AddressUtility.ShiftAddressColumnsInFormula(string.Empty, formula, initialCol, col - initialCol);
AddressUtility.ShiftAddressRowsInFormula(string.Empty, formula, 1, row - initialRow) :
AddressUtility.ShiftAddressColumnsInFormula(string.Empty, formula, 1, col - initialCol);
_worksheet._formulas.SetValue(row, col, newFormula);
}
else if (wsd.Formulas[addr] is int)
{
int sfIx = (int)wsd.Formulas[addr];
var startAddr = new ExcelAddress(_worksheet._sharedFormulas[sfIx].Address);
var f = _worksheet._sharedFormulas[sfIx];
if (startAddr._fromRow > row)
{
f.Formula = ExcelCellBase.TranslateFromR1C1(ExcelCellBase.TranslateToR1C1(f.Formula, f.StartRow, f.StartCol), row, f.StartCol);
f.Address = ExcelCellBase.GetAddress(row, startAddr._fromCol, startAddr._toRow, startAddr._toCol);
}
else if (startAddr._toRow < row)
{
f.Address = ExcelCellBase.GetAddress(startAddr._fromRow, startAddr._fromCol, row, startAddr._toCol);
}

f.Formula = ExcelCellBase.TranslateFromR1C1(ExcelCellBase.TranslateToR1C1(f.Formula, f.StartRow, f.StartCol), row, col);
f.Address = ExcelCellBase.GetAddress(row, col, row, col);
}
}
else
{
_worksheet._formulas.Clear(row, col, 1, 1);
}
}

internal void SetWorksheetSortState(ExcelRangeBase range, int[] columnsOrRows, bool[] descending, CompareOptions compareOptions, bool leftToRight, Dictionary<int, string[]> customLists)
Expand Down
3 changes: 3 additions & 0 deletions src/EPPlusTest/EPPlus.Test.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,9 @@
<None Update="Workbooks\Issue1794.xltx">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Workbooks\Issue1828.xlsx">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Workbooks\LinestTest.xlsx">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
Expand Down
Loading

0 comments on commit e831d60

Please sign in to comment.