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",
|
Language = "zh-Hans",
|
||||||
Theme = ThemeOption.Light,
|
Theme = ThemeOption.Light,
|
||||||
Sort = new SortSettings { Card = CardSortOption.CreatedAt },
|
Sort = new SortSettings { Card = CardSortOption.Title },
|
||||||
ColumnWidth = new ColumnWidthSettings { Preset = ColumnWidthPreset.Wide },
|
ColumnWidth = new ColumnWidthSettings { Preset = ColumnWidthPreset.Wide },
|
||||||
Confirmations = new ConfirmationSettings { DiscardEdit = false },
|
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")]
|
[EnumMember(Value = "id")]
|
||||||
Id,
|
Id,
|
||||||
|
|
||||||
/// <summary>Creation time, oldest first.</summary>
|
|
||||||
[EnumMember(Value = "created-at")]
|
|
||||||
CreatedAt,
|
|
||||||
|
|
||||||
/// <summary>Last modification time, most recently modified first.</summary>
|
/// <summary>Last modification time, most recently modified first.</summary>
|
||||||
[EnumMember(Value = "updated-at")]
|
[EnumMember(Value = "updated-at")]
|
||||||
UpdatedAt,
|
UpdatedAt,
|
||||||
|
|||||||
Reference in new issue
Block a user