feat: drop created-at card sort and add card display ordering
This commit is contained in:
1 parent
27b507e439
commit
bb4f96a9c5
4 files changed
+117
-5
No files matched your search
@@ -0,0 +1,46 @@
|
||||
using YKanBan.Models;
|
||||
using YKanBan.Storage.AppData;
|
||||
|
||||
namespace YKanBan.Tests.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for the in-column card display order: each sort option plus the id
|
||||
/// ascending tie-breaker.
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class CardOrderingTests
|
||||
{
|
||||
private static CardModel Card(long id, string title = "", long updatedAt = 0) =>
|
||||
new(id, 1, title, string.Empty, 0, updatedAt, []);
|
||||
|
||||
private static long[] Ids(IEnumerable<CardModel> cards, CardSortOption option) =>
|
||||
CardOrdering.Sort(cards, option).Select(card => card.Id).ToArray();
|
||||
|
||||
[TestMethod]
|
||||
public void IdSortsAscending()
|
||||
{
|
||||
CollectionAssert.AreEqual(new[] { 1L, 2L, 3L }, Ids([Card(3), Card(1), Card(2)], CardSortOption.Id));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void UpdatedAtSortsMostRecentFirstThenById()
|
||||
{
|
||||
CardModel[] cards = [Card(1, updatedAt: 10), Card(2, updatedAt: 30), Card(3, updatedAt: 10), Card(4, updatedAt: 20)];
|
||||
CollectionAssert.AreEqual(new[] { 2L, 4L, 1L, 3L }, Ids(cards, CardSortOption.UpdatedAt));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TitleSortsByCodePointWithEmptyFirstThenById()
|
||||
{
|
||||
CardModel[] cards = [Card(1, "b"), Card(2, "B"), Card(3, ""), Card(4, "a"), Card(5, "b")];
|
||||
CollectionAssert.AreEqual(new[] { 3L, 2L, 4L, 1L, 5L }, Ids(cards, CardSortOption.Title));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TitleSortsSupplementaryCharactersAfterBmpPrivateUse()
|
||||
{
|
||||
// U+1F600 is a surrogate pair in UTF-16, so ordinal comparison would put it before U+E000.
|
||||
CardModel[] cards = [Card(1, "\U0001F600"), Card(2, "")];
|
||||
CollectionAssert.AreEqual(new[] { 2L, 1L }, Ids(cards, CardSortOption.Title));
|
||||
}
|
||||
}
|
||||
@@ -145,7 +145,7 @@ public class AppConfigStoreTests
|
||||
{
|
||||
Language = "zh-Hans",
|
||||
Theme = ThemeOption.Light,
|
||||
Sort = new SortSettings { Card = CardSortOption.CreatedAt },
|
||||
Sort = new SortSettings { Card = CardSortOption.Title },
|
||||
ColumnWidth = new ColumnWidthSettings { Preset = ColumnWidthPreset.Wide },
|
||||
Confirmations = new ConfirmationSettings { DiscardEdit = false },
|
||||
};
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
using System.Text;
|
||||
using YKanBan.Storage.AppData;
|
||||
|
||||
namespace YKanBan.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Computes the in-column card display order for a <see cref="CardSortOption"/>.
|
||||
/// Columns are not affected: they always stay in id order. Every option falls
|
||||
/// back to id ascending as the final tie-breaker, so the order is total.
|
||||
/// </summary>
|
||||
public static class CardOrdering
|
||||
{
|
||||
/// <summary>
|
||||
/// Orders cards for display.
|
||||
/// </summary>
|
||||
/// <param name="cards">The cards to order, typically those of one column.</param>
|
||||
/// <param name="option">The selected sort option.</param>
|
||||
/// <returns>The cards in display order.</returns>
|
||||
public static IReadOnlyList<CardModel> Sort(IEnumerable<CardModel> cards, CardSortOption option)
|
||||
{
|
||||
IOrderedEnumerable<CardModel> ordered = option switch
|
||||
{
|
||||
CardSortOption.Id => cards.OrderBy(card => card.Id),
|
||||
|
||||
// Most recently modified first.
|
||||
CardSortOption.UpdatedAt => cards.OrderByDescending(card => card.UpdatedAt).ThenBy(card => card.Id),
|
||||
|
||||
// Code point order, matching SQLite's BINARY collation; the empty title sorts first.
|
||||
CardSortOption.Title => cards.OrderBy(card => card.Title, CodePointComparer.Instance).ThenBy(card => card.Id),
|
||||
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(option), option, "Unknown card sort option."),
|
||||
};
|
||||
return ordered.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares strings by Unicode code points. Unlike ordinal UTF-16 comparison
|
||||
/// it orders supplementary characters after U+E000–U+FFFF, as BINARY does on UTF-8.
|
||||
/// </summary>
|
||||
private sealed class CodePointComparer : IComparer<string>
|
||||
{
|
||||
public static readonly CodePointComparer Instance = new();
|
||||
|
||||
public int Compare(string? x, string? y)
|
||||
{
|
||||
if (x is null || y is null)
|
||||
{
|
||||
return x is null ? (y is null ? 0 : -1) : 1;
|
||||
}
|
||||
|
||||
StringRuneEnumerator left = x.EnumerateRunes();
|
||||
StringRuneEnumerator right = y.EnumerateRunes();
|
||||
while (true)
|
||||
{
|
||||
bool hasLeft = left.MoveNext();
|
||||
bool hasRight = right.MoveNext();
|
||||
if (!hasLeft || !hasRight)
|
||||
{
|
||||
return hasLeft.CompareTo(hasRight);
|
||||
}
|
||||
|
||||
int result = left.Current.Value.CompareTo(right.Current.Value);
|
||||
if (result != 0)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,10 +13,6 @@ public enum CardSortOption
|
||||
[EnumMember(Value = "id")]
|
||||
Id,
|
||||
|
||||
/// <summary>Creation time, oldest first.</summary>
|
||||
[EnumMember(Value = "created-at")]
|
||||
CreatedAt,
|
||||
|
||||
/// <summary>Last modification time, most recently modified first.</summary>
|
||||
[EnumMember(Value = "updated-at")]
|
||||
UpdatedAt,
|
||||
|
||||
Reference in new issue
Block a user