Skip to content

Commit

Permalink
Merge pull request #72 from anmcgrath/range-sorting
Browse files Browse the repository at this point in the history
Range sorting
  • Loading branch information
anmcgrath authored May 19, 2024
2 parents 5f1d3d4 + 239084b commit be07426
Show file tree
Hide file tree
Showing 38 changed files with 1,067 additions and 135 deletions.
207 changes: 207 additions & 0 deletions src/BlazorDatasheet.Core/Commands/SortRangeCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
using BlazorDatasheet.Core.Data;
using BlazorDatasheet.Core.Data.Cells;
using BlazorDatasheet.DataStructures.Geometry;
using BlazorDatasheet.DataStructures.Store;
using BlazorDatasheet.Formula.Core;

namespace BlazorDatasheet.Core.Commands;

public class SortRangeCommand : IUndoableCommand
{
private readonly IRegion _region;
public IRegion? SortedRegion;
private readonly List<ColumnSortOptions> _sortOptions;
public int[] OldIndices = Array.Empty<int>();
private readonly RegionRestoreData<string> _typeRestoreData = new();

/// <summary>
/// Sorts the specified region on values using the specified sort options.
/// </summary>
/// <param name="region">The region to sort</param>
/// <param name="sortOptions">The column sort options, if null the default sort (sort on column 0 ascending) will be used.
/// If two column values are equal, then the next option will be used for the sort, equivalent to a ThenBy</param>
public SortRangeCommand(IRegion region, List<ColumnSortOptions>? sortOptions = null)
{
_region = region;
_sortOptions = sortOptions ?? new List<ColumnSortOptions>()
{ new(0, true) };
}

/// <summary>
/// Sorts the specified region on values using the specified sort options.
/// </summary>
/// <param name="region">The region to sort</param>
/// <param name="sortOption">The column sort options, if null the default sort (sort on column 0 ascending) will be used.
/// If two column values are equal, then the next option will be used for the sort, equivalent to a ThenBy</param>
public SortRangeCommand(IRegion region, ColumnSortOptions sortOption)
{
_region = region;
_sortOptions = new List<ColumnSortOptions> { sortOption };
}

public bool Execute(Sheet sheet)
{
var store = sheet.Cells.GetCellDataStore();

var rowCollection = store.GetNonEmptyRowData(_region);
var rowIndices = new Span<int>(rowCollection.RowIndicies);
var rowData = new Span<RowData<CellValue>>(rowCollection.Rows);

rowData.Sort(rowIndices, Comparison);

SortedRegion = new Region(_region.Top, _region.Top + rowIndices.Length, _region.Left, _region.Right);
SortedRegion = SortedRegion.GetIntersection(sheet.Region);

if (SortedRegion == null)
return true;

sheet.BatchUpdates();

var formulaData = sheet.Cells.GetFormulaStore().GetSubStore(_region, false);
var typeData =
(ConsolidatedDataStore<string>)sheet.Cells.GetTypeStore().GetSubStore(_region, false);

// clear any row data that has been shifted (which should be all non-empty rows)
for (int i = 0; i < rowData.Length; i++)
{
var row = rowData[i].Row;
var rowReg = new Region(row, row, _region.Left, _region.Right);
sheet.Cells.ClearCellsImpl(new[] { rowReg });
_typeRestoreData.Merge(sheet.Cells.GetTypeStore().Clear(rowReg));
}

for (int i = 0; i < rowData.Length; i++)
{
var newRowNo = _region.Top + i;
var oldRowNo = rowData[i].Row;

for (int j = 0; j < rowData[i].ColumnIndices.Length; j++)
{
var col = rowData[i].ColumnIndices[j];
var val = rowData[i].Values[j];
var formula = formulaData.Get(oldRowNo, col);
var type = typeData.Get(oldRowNo, col);

sheet.Cells.SetCellTypeImpl(new Region(newRowNo, col), type);

if (formula == null)
sheet.Cells.SetValueImpl(newRowNo, col, val);
else
{
formula.ShiftReferences((newRowNo - oldRowNo), 0);
sheet.Cells.SetFormulaImpl(newRowNo, col, formula);
}
}
}

sheet.EndBatchUpdates();

OldIndices = rowIndices.ToArray();

return true;
}

private int Comparison(RowData<CellValue> x, RowData<CellValue> y)
{
for (int i = 0; i < _sortOptions.Count; i++)
{
var sortOption = _sortOptions[i];
var xValue = x.GetColumnData(sortOption.ColumnIndex + _region.Left);
var yValue = y.GetColumnData(sortOption.ColumnIndex + _region.Left);

if (xValue?.Data == null && yValue?.Data == null)
continue;

// null comparisons shouldn't depend on the sort order -
// null values always end up last.
if (xValue?.Data == null)
return 1;
if (yValue?.Data == null)
return -1;

int comparison = xValue.CompareTo(yValue);

if (comparison == 0)
continue;

comparison = sortOption.Ascending ? comparison : -comparison;

return comparison;
}

return 0;
}

public bool Undo(Sheet sheet)
{
if (SortedRegion == null)
return true;

var rowCollection = sheet.Cells.GetCellDataStore().GetRowData(SortedRegion);
var formulaCollection = sheet.Cells.GetFormulaStore().GetSubStore(SortedRegion, false);
var typeCollection = (ConsolidatedDataStore<string>)sheet.Cells.GetTypeStore().GetSubStore(SortedRegion);

sheet.BatchUpdates();
sheet.Cells.ClearCellsImpl(new[] { SortedRegion });
sheet.Cells.GetTypeStore().Clear(SortedRegion);

var rowData = rowCollection.Rows;
var rowIndices = rowCollection.RowIndicies;
for (int i = 0; i < OldIndices.Length; i++)
{
var newRowNo = OldIndices[i];
for (int j = 0; j < rowData[i].ColumnIndices.Length; j++)
{
var col = rowData[i].ColumnIndices[j];
var formula = formulaCollection.Get(rowIndices[i], col);
var type = typeCollection.Get(rowIndices[i], col);
if (formula == null)
{
var val = rowData[i].Values[j];
sheet.Cells.SetValueImpl(newRowNo, col, val);
}
else
{
formula.ShiftReferences((newRowNo - rowIndices[i]), 0);
sheet.Cells.SetFormulaImpl(newRowNo, col, formula);
}

sheet.Cells.SetCellTypeImpl(new Region(newRowNo, col), type);
}
}

var restoreData = new CellStoreRestoreData()
{
TypeRestoreData = _typeRestoreData
};

sheet.Cells.Restore(restoreData);
sheet.EndBatchUpdates();

return true;
}
}

public class ColumnSortOptions
{
/// <summary>
/// The column index, relative to the range being sorted.
/// </summary>
public int ColumnIndex { get; set; }

/// <summary>
/// Whether to sort in ascending order.
/// </summary>
public bool Ascending { get; set; }

/// <summary>
///
/// </summary>
/// <param name="columnIndex"></param>
/// <param name="ascending"></param>
public ColumnSortOptions(int columnIndex, bool ascending)
{
ColumnIndex = columnIndex;
Ascending = ascending;
}
}
6 changes: 5 additions & 1 deletion src/BlazorDatasheet.Core/Data/Cells/CellStore.Data.cs
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,11 @@ public CellValue GetCellValue(int row, int col)
return _dataStore.Get(row, col)!;
}

public IMatrixDataStore<CellValue> GetStore()
/// <summary>
/// Returns the sparse matrix store that holds the cell data.
/// </summary>
/// <returns></returns>
public IMatrixDataStore<CellValue> GetCellDataStore()
{
return _dataStore;
}
Expand Down
10 changes: 6 additions & 4 deletions src/BlazorDatasheet.Core/Data/Cells/CellStore.Formula.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public partial class CellStore
/// <summary>
/// Cell FORMULA
/// </summary>
private readonly IMatrixDataStore<CellFormula?> _formulaStore = new SparseMatrixStore<CellFormula?>();
private readonly IMatrixDataStore<CellFormula?> _formulaStore = new SparseMatrixStoreByRows<CellFormula?>();

/// <summary>
/// Set the formula string for a row and col, and calculate the sheet.
Expand Down Expand Up @@ -72,7 +72,7 @@ private CellStoreRestoreData CopyFormula(IRegion fromRegion, IRegion toRegion)

foreach (var formula in formulaToCopy)
{
if(formula.data == null)
if (formula.data == null)
continue;
var clonedFormula = formula.data.Clone();
clonedFormula.ShiftReferences(offset.row, offset.col);
Expand All @@ -85,7 +85,7 @@ private CellStoreRestoreData CopyFormula(IRegion fromRegion, IRegion toRegion)
internal CellStoreRestoreData ClearFormulaImpl(int row, int col)
{
var restoreData = _formulaStore.Clear(row, col);
_sheet.FormulaEngine.RemoveFromDependencyGraph(row, col);
_sheet.FormulaEngine.RemoveFormula(row, col);
return new CellStoreRestoreData() { FormulaRestoreData = restoreData };
}

Expand All @@ -94,7 +94,7 @@ internal CellStoreRestoreData ClearFormulaImpl(IEnumerable<IRegion> regions)
var clearedData = _formulaStore.Clear(regions);
foreach (var clearedFormula in clearedData.DataRemoved)
{
_sheet.FormulaEngine.RemoveFromDependencyGraph(clearedFormula.row, clearedFormula.col);
_sheet.FormulaEngine.RemoveFormula(clearedFormula.row, clearedFormula.col);
}

return new CellStoreRestoreData()
Expand Down Expand Up @@ -134,4 +134,6 @@ public void SetFormula(int row, int col, CellFormula parsedFormula)
{
_sheet.Commands.ExecuteCommand(new SetParsedFormulaCommand(row, col, parsedFormula));
}

internal IMatrixDataStore<CellFormula?> GetFormulaStore() => _formulaStore;
}
9 changes: 7 additions & 2 deletions src/BlazorDatasheet.Core/Data/Cells/CellStore.Types.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,13 @@ public string GetCellType(int row, int col)
/// <param name="region"></param>
/// <param name="type"></param>
/// <returns></returns>
internal CellStoreRestoreData SetCellTypeImpl(IRegion region, string type)
internal CellStoreRestoreData SetCellTypeImpl(IRegion region, string? type)
{
var restoreData = new CellStoreRestoreData();
restoreData.TypeRestoreData = _typeStore.Add(region, type);
if (string.IsNullOrEmpty(type))
restoreData.TypeRestoreData = _typeStore.Clear(region);
else
restoreData.TypeRestoreData = _typeStore.Add(region, type);
_sheet.MarkDirty(region);
return restoreData;
}
Expand Down Expand Up @@ -64,4 +67,6 @@ public void SetType(IEnumerable<IRegion> regions, string type)
/// <param name="type"></param>
/// <returns></returns>
public void SetType(int row, int col, string type) => SetType(new Region(row, col), type);

internal ConsolidatedDataStore<string> GetTypeStore() => _typeStore;
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ public partial class CellStore
/// <summary>
/// Stores whether cells are valid.
/// </summary>
private readonly IMatrixDataStore<bool?> _validStore = new SparseMatrixStore<bool?>();
private readonly IMatrixDataStore<bool?> _validStore = new SparseMatrixStoreByRows<bool?>();

internal void ValidateRegion(IRegion region)
{
Expand Down
2 changes: 1 addition & 1 deletion src/BlazorDatasheet.Core/Data/Cells/CellStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public partial class CellStore
public CellStore(Sheet sheet)
{
_sheet = sheet;
_dataStore = new SparseMatrixStore2<CellValue>(_defaultCellValue);
_dataStore = new SparseMatrixStoreByRows<CellValue>(_defaultCellValue);
}

/// <summary>
Expand Down
16 changes: 16 additions & 0 deletions src/BlazorDatasheet.Core/Data/Sheet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using BlazorDatasheet.Core.Commands;
using BlazorDatasheet.Core.Data.Cells;
using BlazorDatasheet.Core.Edit;
using BlazorDatasheet.Core.Events;
using BlazorDatasheet.Core.Events.Visual;
using BlazorDatasheet.Core.Formats;
using BlazorDatasheet.Core.Interfaces;
Expand Down Expand Up @@ -85,6 +86,10 @@ public class Sheet
/// </summary>
public event EventHandler<DirtySheetEventArgs>? SheetDirty;

public event EventHandler<BeforeRangeSortEventArgs>? BeforeRangeSort;

public event EventHandler<RangeSortedEventArgs>? RangeSorted;

#endregion

/// <summary>
Expand Down Expand Up @@ -301,6 +306,17 @@ public void BatchUpdates()
_isBatchingChanges = true;
}

public void SortRange(IRegion region, List<ColumnSortOptions>? sortOptions = null)
{
var beforeArgs = new BeforeRangeSortEventArgs(region, sortOptions);
BeforeRangeSort?.Invoke(this, beforeArgs);
var cmd = new SortRangeCommand(region, sortOptions);
if (!beforeArgs.Cancel)
Commands.ExecuteCommand(cmd);
var afterArgs = new RangeSortedEventArgs(region, cmd.SortedRegion, cmd.OldIndices);
RangeSorted?.Invoke(this, afterArgs);
}

/// <summary>
/// Ends the batching of dirty cells and regions, and emits the dirty sheet event.
/// </summary>
Expand Down
17 changes: 17 additions & 0 deletions src/BlazorDatasheet.Core/Events/BeforeRangeSortEventArgs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System.ComponentModel;
using BlazorDatasheet.Core.Commands;
using BlazorDatasheet.DataStructures.Geometry;

namespace BlazorDatasheet.Core.Events;

public class BeforeRangeSortEventArgs : CancelEventArgs
{
public IRegion Region { get; }
public IList<ColumnSortOptions>? SortOptions { get; }

public BeforeRangeSortEventArgs(IRegion region, IList<ColumnSortOptions>? sortOptions)
{
Region = region;
SortOptions = sortOptions;
}
}
2 changes: 1 addition & 1 deletion src/BlazorDatasheet.Core/FormulaEngine/Environment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,6 @@ private CellValue[][] GetValuesInRange(SheetRange range)
var region = range.Region.GetIntersection(range.Sheet.Region);
if (region == null)
return Array.Empty<CellValue[]>();
return range.Sheet.Cells.GetStore().GetData(region);
return range.Sheet.Cells.GetCellDataStore().GetData(region);
}
}
Loading

0 comments on commit be07426

Please sign in to comment.