mirror of
https://github.com/radzenhq/radzen-blazor.git
synced 2026-07-08 18:16:08 +00:00
Three independent additions to the Spreadsheet API, motivated by the gap analysis against Excel COM, Office.js, Aspose, and Syncfusion: Multi-key sort (Pass 3) - New SortKey class: ColumnIndex (relative to the sort range), Order, SortOn, CustomList, CustomColor, CaseSensitive. - New SortOn enum: Values, CellColor, FontColor. - Worksheet.Sort(range, params SortKey[]) — stable, supports multiple sort levels, custom-list ordering, case sensitivity, sort by color. - Table.Sort(params SortKey[]) overload using table-relative indices. - Existing single-key Sort(range, order, keyIndex) retained for back-compat; redirects to the new API. - Empty cells always sort to the bottom regardless of direction (matches Excel and pre-existing test). - Cell.Clone now preserves Format (was dropped on the floor; the bug was masked because no caller of Clone read Format until color sort). Advanced filter criteria (Pass 2A) - TopFilterCriterion — Top/Bottom N items, by count or percent. - DynamicFilterCriterion — full DynamicFilterType enum with 34 values covering above/below average, today/yesterday/tomorrow, this/last/ next week/month/quarter/year, year-to-date, individual months, and Q1..Q4. - CellColorFilterCriterion — match cells by background or font color. - New OnApply(sheet, range) hook on FilterCriterion lets distribution- aware criteria (Top, AboveAverage) compute thresholds before the per-row Matches loop. Default no-op preserves existing behavior. - Visitor extended with three new Visit overloads + base no-op defaults. Per-column AutoFilter API (Pass 2B) - AutoFilter.ApplyValueFilter(col, values), ApplyCustomFilter(col, criterion), ApplyTopFilter(col, count, percent, bottom), ApplyDynamicFilter(col, type), ApplyColorFilter(col, color, fontColor), ClearColumnFilter(col), GetColumnFilter(col). - Column indices on these methods are RELATIVE to the AutoFilter range, matching the convention used by SortKey and Office.js TableSort. Tests - MultiKeySortContractTests: 11 facts (multi-key, custom list, case-sensitive, color sort, range bounds, no-op edges). - FilterCriterionContractTests: 11 facts (Top/Bottom by count and percent, AboveAverage, Today, ThisMonth, Quarter1, color filters, visitor dispatch). - AutoFilterPerColumnTests: 12 facts (each Apply* method, replace semantics, per-column clear, validation errors). 3,706 tests pass.
228 lines
8.9 KiB
C#
228 lines
8.9 KiB
C#
using Radzen;
|
|
using Radzen.Documents.Spreadsheet;
|
|
using Xunit;
|
|
|
|
namespace Radzen.Blazor.Spreadsheet.Tests;
|
|
|
|
#nullable enable
|
|
|
|
// Contract for the multi-key sort API: SortKey + SortOn enum + new
|
|
// Worksheet.Sort(range, params SortKey[]) overload. Column indices in
|
|
// SortKey are relative to the range's left edge.
|
|
public class MultiKeySortContractTests
|
|
{
|
|
private static (Workbook wb, Worksheet ws) Build(int rows, int cols)
|
|
{
|
|
var wb = new Workbook();
|
|
var ws = wb.AddSheet("Sheet1", rows, cols);
|
|
return (wb, ws);
|
|
}
|
|
|
|
private static RangeRef Range(int r1, int c1, int r2, int c2) =>
|
|
new(new CellRef(r1, c1), new CellRef(r2, c2));
|
|
|
|
// ── SortKey shape ───────────────────────────────────────────────────────
|
|
[Fact]
|
|
public void SortKey_DefaultsAreSensible()
|
|
{
|
|
var key = new SortKey { ColumnIndex = 0 };
|
|
Assert.Equal(SortOrder.Ascending, key.Order);
|
|
Assert.Equal(SortOn.Values, key.SortOn);
|
|
Assert.False(key.CaseSensitive);
|
|
Assert.Null(key.CustomList);
|
|
}
|
|
|
|
// ── Multi-key sort ──────────────────────────────────────────────────────
|
|
[Fact]
|
|
public void Sort_TwoKeys_ShouldUseSecondaryWhenPrimaryEquals()
|
|
{
|
|
// Region asc, then Q1 desc
|
|
var (_, ws) = Build(4, 2);
|
|
ws.Cells[0, 0].SetValue("EMEA"); ws.Cells[0, 1].Value = 100.0;
|
|
ws.Cells[1, 0].SetValue("AMER"); ws.Cells[1, 1].Value = 50.0;
|
|
ws.Cells[2, 0].SetValue("EMEA"); ws.Cells[2, 1].Value = 200.0;
|
|
ws.Cells[3, 0].SetValue("AMER"); ws.Cells[3, 1].Value = 75.0;
|
|
|
|
ws.Sort(Range(0, 0, 3, 1),
|
|
new SortKey { ColumnIndex = 0, Order = SortOrder.Ascending },
|
|
new SortKey { ColumnIndex = 1, Order = SortOrder.Descending });
|
|
|
|
// Expected order: AMER 75, AMER 50, EMEA 200, EMEA 100
|
|
Assert.Equal("AMER", ws.Cells[0, 0].Value); Assert.Equal(75.0, ws.Cells[0, 1].Value);
|
|
Assert.Equal("AMER", ws.Cells[1, 0].Value); Assert.Equal(50.0, ws.Cells[1, 1].Value);
|
|
Assert.Equal("EMEA", ws.Cells[2, 0].Value); Assert.Equal(200.0, ws.Cells[2, 1].Value);
|
|
Assert.Equal("EMEA", ws.Cells[3, 0].Value); Assert.Equal(100.0, ws.Cells[3, 1].Value);
|
|
}
|
|
|
|
[Fact]
|
|
public void Sort_SingleKey_ShouldBeRelativeToRange()
|
|
{
|
|
// Sort B1:D3 by column index 0 (which is sheet column B, the FIRST column of the range)
|
|
var (_, ws) = Build(3, 4);
|
|
// A column is unrelated (left of range)
|
|
ws.Cells[0, 0].SetValue("a"); ws.Cells[1, 0].SetValue("b"); ws.Cells[2, 0].SetValue("c");
|
|
// B column = sort key
|
|
ws.Cells[0, 1].Value = 30.0;
|
|
ws.Cells[1, 1].Value = 10.0;
|
|
ws.Cells[2, 1].Value = 20.0;
|
|
ws.Cells[0, 2].SetValue("X"); ws.Cells[1, 2].SetValue("Y"); ws.Cells[2, 2].SetValue("Z");
|
|
|
|
ws.Sort(Range(0, 1, 2, 3), new SortKey { ColumnIndex = 0 });
|
|
|
|
// A column untouched
|
|
Assert.Equal("a", ws.Cells[0, 0].Value);
|
|
// B column sorted ascending: 10, 20, 30
|
|
Assert.Equal(10.0, ws.Cells[0, 1].Value);
|
|
Assert.Equal(20.0, ws.Cells[1, 1].Value);
|
|
Assert.Equal(30.0, ws.Cells[2, 1].Value);
|
|
// C column moves with B
|
|
Assert.Equal("Y", ws.Cells[0, 2].Value);
|
|
Assert.Equal("Z", ws.Cells[1, 2].Value);
|
|
Assert.Equal("X", ws.Cells[2, 2].Value);
|
|
}
|
|
|
|
// ── Custom list ─────────────────────────────────────────────────────────
|
|
[Fact]
|
|
public void Sort_WithCustomList_ShouldUseListOrder()
|
|
{
|
|
var (_, ws) = Build(4, 1);
|
|
ws.Cells[0, 0].SetValue("Mar");
|
|
ws.Cells[1, 0].SetValue("Jan");
|
|
ws.Cells[2, 0].SetValue("Feb");
|
|
ws.Cells[3, 0].SetValue("Apr");
|
|
|
|
ws.Sort(Range(0, 0, 3, 0),
|
|
new SortKey { ColumnIndex = 0, CustomList = ["Jan", "Feb", "Mar", "Apr"] });
|
|
|
|
Assert.Equal("Jan", ws.Cells[0, 0].Value);
|
|
Assert.Equal("Feb", ws.Cells[1, 0].Value);
|
|
Assert.Equal("Mar", ws.Cells[2, 0].Value);
|
|
Assert.Equal("Apr", ws.Cells[3, 0].Value);
|
|
}
|
|
|
|
[Fact]
|
|
public void Sort_WithCustomList_DescendingShouldReverse()
|
|
{
|
|
var (_, ws) = Build(3, 1);
|
|
ws.Cells[0, 0].SetValue("Feb");
|
|
ws.Cells[1, 0].SetValue("Jan");
|
|
ws.Cells[2, 0].SetValue("Mar");
|
|
|
|
ws.Sort(Range(0, 0, 2, 0),
|
|
new SortKey
|
|
{
|
|
ColumnIndex = 0,
|
|
Order = SortOrder.Descending,
|
|
CustomList = ["Jan", "Feb", "Mar"],
|
|
});
|
|
|
|
Assert.Equal("Mar", ws.Cells[0, 0].Value);
|
|
Assert.Equal("Feb", ws.Cells[1, 0].Value);
|
|
Assert.Equal("Jan", ws.Cells[2, 0].Value);
|
|
}
|
|
|
|
[Fact]
|
|
public void Sort_WithCustomList_ValuesNotInListShouldComeAfter()
|
|
{
|
|
var (_, ws) = Build(4, 1);
|
|
ws.Cells[0, 0].SetValue("Apple");
|
|
ws.Cells[1, 0].SetValue("Jan");
|
|
ws.Cells[2, 0].SetValue("Banana");
|
|
ws.Cells[3, 0].SetValue("Feb");
|
|
|
|
ws.Sort(Range(0, 0, 3, 0),
|
|
new SortKey { ColumnIndex = 0, CustomList = ["Jan", "Feb"] });
|
|
|
|
// Items in the list come first, in list order
|
|
Assert.Equal("Jan", ws.Cells[0, 0].Value);
|
|
Assert.Equal("Feb", ws.Cells[1, 0].Value);
|
|
// Items not in the list follow, alphabetically
|
|
Assert.Equal("Apple", ws.Cells[2, 0].Value);
|
|
Assert.Equal("Banana", ws.Cells[3, 0].Value);
|
|
}
|
|
|
|
// ── Case sensitivity ────────────────────────────────────────────────────
|
|
[Fact]
|
|
public void Sort_CaseInsensitive_ShouldTreatVariantsAsEqual_AndPreserveOrder()
|
|
{
|
|
var (_, ws) = Build(3, 2);
|
|
ws.Cells[0, 0].SetValue("apple"); ws.Cells[0, 1].Value = 1.0;
|
|
ws.Cells[1, 0].SetValue("Apple"); ws.Cells[1, 1].Value = 2.0;
|
|
ws.Cells[2, 0].SetValue("APPLE"); ws.Cells[2, 1].Value = 3.0;
|
|
|
|
ws.Sort(Range(0, 0, 2, 1), new SortKey { ColumnIndex = 0 });
|
|
// Stable: original order preserved when keys are equal
|
|
Assert.Equal(1.0, ws.Cells[0, 1].Value);
|
|
Assert.Equal(2.0, ws.Cells[1, 1].Value);
|
|
Assert.Equal(3.0, ws.Cells[2, 1].Value);
|
|
}
|
|
|
|
[Fact]
|
|
public void Sort_CaseSensitive_ShouldOrderUppercaseSeparately()
|
|
{
|
|
var (_, ws) = Build(3, 1);
|
|
ws.Cells[0, 0].SetValue("apple");
|
|
ws.Cells[1, 0].SetValue("Apple");
|
|
ws.Cells[2, 0].SetValue("APPLE");
|
|
|
|
ws.Sort(Range(0, 0, 2, 0),
|
|
new SortKey { ColumnIndex = 0, CaseSensitive = true });
|
|
|
|
// Ordinal sort: 'A' (0x41) < 'a' (0x61), so APPLE < Apple < apple.
|
|
Assert.Equal("APPLE", ws.Cells[0, 0].Value);
|
|
Assert.Equal("Apple", ws.Cells[1, 0].Value);
|
|
Assert.Equal("apple", ws.Cells[2, 0].Value);
|
|
}
|
|
|
|
// ── Sort by color ───────────────────────────────────────────────────────
|
|
[Fact]
|
|
public void Sort_ByCellColor_ShouldGroupMatchingColorFirst()
|
|
{
|
|
var (_, ws) = Build(4, 1);
|
|
ws.Cells[0, 0].SetValue("a");
|
|
ws.Cells[1, 0].SetValue("b"); ws.Cells[1, 0].Format = new Format { BackgroundColor = "#FFFF00" };
|
|
ws.Cells[2, 0].SetValue("c");
|
|
ws.Cells[3, 0].SetValue("d"); ws.Cells[3, 0].Format = new Format { BackgroundColor = "#FFFF00" };
|
|
|
|
ws.Sort(Range(0, 0, 3, 0),
|
|
new SortKey
|
|
{
|
|
ColumnIndex = 0,
|
|
SortOn = SortOn.CellColor,
|
|
CustomColor = "#FFFF00",
|
|
Order = SortOrder.Ascending,
|
|
});
|
|
|
|
// Yellow rows come first (preserving their relative order).
|
|
Assert.Equal("b", ws.Cells[0, 0].Value);
|
|
Assert.Equal("d", ws.Cells[1, 0].Value);
|
|
Assert.Equal("a", ws.Cells[2, 0].Value);
|
|
Assert.Equal("c", ws.Cells[3, 0].Value);
|
|
}
|
|
|
|
// ── Empty + no-op cases ─────────────────────────────────────────────────
|
|
[Fact]
|
|
public void Sort_EmptyKeys_ShouldBeNoOp()
|
|
{
|
|
var (_, ws) = Build(3, 1);
|
|
ws.Cells[0, 0].Value = 3.0;
|
|
ws.Cells[1, 0].Value = 1.0;
|
|
ws.Cells[2, 0].Value = 2.0;
|
|
|
|
ws.Sort(Range(0, 0, 2, 0)); // params, no keys
|
|
|
|
// Order unchanged
|
|
Assert.Equal(3.0, ws.Cells[0, 0].Value);
|
|
Assert.Equal(1.0, ws.Cells[1, 0].Value);
|
|
Assert.Equal(2.0, ws.Cells[2, 0].Value);
|
|
}
|
|
|
|
[Fact]
|
|
public void Sort_KeyOutsideRange_ShouldThrow()
|
|
{
|
|
var (_, ws) = Build(3, 2);
|
|
Assert.Throws<System.ArgumentOutOfRangeException>(() =>
|
|
ws.Sort(Range(0, 0, 2, 1), new SortKey { ColumnIndex = 5 }));
|
|
}
|
|
}
|