diff --git a/src/Kinetic/Linq/ObservableView.GroupBy.cs b/src/Kinetic/Linq/ObservableView.GroupBy.cs index dbecce4..ac9f465 100644 --- a/src/Kinetic/Linq/ObservableView.GroupBy.cs +++ b/src/Kinetic/Linq/ObservableView.GroupBy.cs @@ -43,7 +43,7 @@ public static ObserverBuilder> GroupBy>, TResult> resultSelector) { return source.ContinueWith, ListChange>( - new() { KeySelector = keySelector, ResultSelector = resultSelector }); + new(keyComparer: null, keySelector, resultSelector)); } public static ObserverBuilder> GroupBy( @@ -53,7 +53,7 @@ public static ObserverBuilder> GroupBy keyComparer) { return source.ContinueWith, ListChange>( - new() { KeyComparer = keyComparer, KeySelector = keySelector, ResultSelector = resultSelector }); + new(keyComparer, keySelector, resultSelector)); } public static ObserverBuilder>> GroupBy( @@ -102,40 +102,174 @@ public static ObserverBuilder> GroupBy>> GroupBy( + this ObserverBuilder> source, Func> keySelector) + { + return source.GroupBy(keySelector, ObservableGrouping.Create); + } + + public static ObserverBuilder>> GroupBy( + this ObserverBuilder> source, Func> keySelector, IEqualityComparer keyComparer) + { + return source.GroupBy(keySelector, resultSelector: ObservableGrouping.Create, keyComparer); + } + + public static ObserverBuilder>> GroupBy( + this ObserverBuilder> source, + Func> keySelector, + Func>, ObserverBuilder>> resultSelector) + { + return source.GroupBy(keySelector, (k, b) => ObservableGrouping.Create(k, resultSelector(b))); + } + + public static ObserverBuilder>> GroupBy( + this ObserverBuilder> source, + Func> keySelector, + Func>, ObserverBuilder>> resultSelector, + IEqualityComparer keyComparer) + { + return source.GroupBy(keySelector, (k, b) => ObservableGrouping.Create(k, resultSelector(b)), keyComparer); + } + + public static ObserverBuilder> GroupBy( + this ObserverBuilder> source, + Func> keySelector, + Func>, TResult> resultSelector) + { + return source.ContinueWith, ListChange>( + new(keyComparer: null, keySelector, resultSelector)); + } + + public static ObserverBuilder> GroupBy( + this ObserverBuilder> source, + Func> keySelector, + Func>, TResult> resultSelector, + IEqualityComparer keyComparer) + { + return source.ContinueWith, ListChange>( + new(keyComparer, keySelector, resultSelector)); + } + + public static ObserverBuilder>> GroupBy( + this ReadOnlyObservableList source, Func> keySelector) + { + return source.Changed.ToBuilder().GroupBy(s => keySelector(s).Changed.ToBuilder(), resultSelector: ObservableGrouping.Create); + } + + public static ObserverBuilder>> GroupBy( + this ReadOnlyObservableList source, Func> keySelector, IEqualityComparer keyComparer) + { + return source.Changed.ToBuilder().GroupBy(s => keySelector(s).Changed.ToBuilder(), resultSelector: ObservableGrouping.Create, keyComparer); + } + + public static ObserverBuilder>> GroupBy( + this ReadOnlyObservableList source, + Func> keySelector, + Func>, ObserverBuilder>> resultSelector) + { + return source.Changed.ToBuilder().GroupBy(s => keySelector(s).Changed.ToBuilder(), (k, b) => ObservableGrouping.Create(k, resultSelector(b))); + } + + public static ObserverBuilder>> GroupBy( + this ReadOnlyObservableList source, + Func> keySelector, + Func>, ObserverBuilder>> resultSelector, + IEqualityComparer keyComparer) + { + return source.Changed.ToBuilder().GroupBy(s => keySelector(s).Changed.ToBuilder(), (k, b) => ObservableGrouping.Create(k, resultSelector(b)), keyComparer); + } + + public static ObserverBuilder> GroupBy( + this ReadOnlyObservableList source, + Func> keySelector, + Func>, TResult> resultSelector) + { + return source.Changed.ToBuilder().GroupBy(s => keySelector(s).Changed.ToBuilder(), resultSelector); + } + + public static ObserverBuilder> GroupBy( + this ReadOnlyObservableList source, + Func> keySelector, + Func>, TResult> resultSelector, + IEqualityComparer keyComparer) + { + return source.Changed.ToBuilder().GroupBy(s => keySelector(s).Changed.ToBuilder(), resultSelector, keyComparer); + } + private readonly struct GroupByStateMachineFactory : IStateMachineFactory, ListChange> { - public IEqualityComparer? KeyComparer { get; init; } - public required Func KeySelector { get; init; } - public required Func>, TResult> ResultSelector { get; init; } + private readonly IEqualityComparer? _keyComparer; + private readonly Delegate _keySelector; + private readonly Func>, TResult> _resultSelector; + + public GroupByStateMachineFactory( + IEqualityComparer? keyComparer, + Func keySelector, + Func>, TResult> resultSelector) + { + _keyComparer = keyComparer; + _keySelector = keySelector; + _resultSelector = resultSelector; + } + + public GroupByStateMachineFactory( + IEqualityComparer? keyComparer, + Func> keySelector, + Func>, TResult> resultSelector) + { + _keyComparer = keyComparer; + _keySelector = keySelector; + _resultSelector = resultSelector; + } public void Create(in TContinuation continuation, ObserverStateMachine> source) - where TContinuation : struct, IStateMachine> => - source.ContinueWith>( - new(continuation, KeyComparer, KeySelector, ResultSelector)); + where TContinuation : struct, IStateMachine> + { + var keyComparer = typeof(TKey).IsValueType + ? _keyComparer is { } && _keyComparer == EqualityComparer.Default ? null : _keyComparer + : _keyComparer ?? EqualityComparer.Default; + + if (_keySelector is Func staticKeySelector) + { + source.ContinueWith, TContinuation>>( + new(continuation, new() { KeySelector = staticKeySelector }, keyComparer, _resultSelector)); + } + else + if (_keySelector is Func> dynamicKeySelector) + { + source.ContinueWith, GroupingDynamicItem.Factory, TContinuation>>( + new(continuation, new() { KeySelector = dynamicKeySelector }, keyComparer, _resultSelector)); + } + else + { + throw new NotSupportedException(); + } + } } - private struct GroupByStateMachine : IStateMachine> + private struct GroupByStateMachine : IGroupByStateMachine + where TItem : IGroupingItem + where TItemFactory : IGroupingItemFactory where TContinuation : struct, IStateMachine> { private TContinuation _continuation; - private readonly IEqualityComparer? _keyComparer; - private readonly Func _keySelector; - private readonly Func>, TResult> _resultSelector; + private TItemFactory _itemFactory; - private readonly List<(int GroupingIndex, int ItemIndex)> _indices = new(); + private readonly List _items = new(); private readonly List?> _groupings = new(); + private readonly IEqualityComparer? _keyComparer; + private readonly Func>, TResult> _resultSelector; + public GroupByStateMachine( in TContinuation continuation, + in TItemFactory itemFactory, IEqualityComparer? keyComparer, - Func keySelector, Func>, TResult> resultSelector) { _continuation = continuation; - _keyComparer = typeof(TKey).IsValueType - ? keyComparer is { } && keyComparer == EqualityComparer.Default ? null : keyComparer - : keyComparer ?? EqualityComparer.Default; - _keySelector = keySelector; + _itemFactory = itemFactory; + _keyComparer = keyComparer; _resultSelector = resultSelector; } @@ -160,66 +294,143 @@ public void OnNext(ListChange value) { case ListChangeAction.RemoveAll: { - _groupings.Clear(); - _indices.Clear(); + _itemFactory.DisposeAll(_items); + _items.Clear(); + _groupings.Clear(); _continuation.OnNext(ListChange.RemoveAll()); break; } case ListChangeAction.Remove: { - var indices = _indices[value.OldIndex]; - var grouping = _groupings[indices.GroupingIndex]; + var index = value.OldIndex; + var item = _items[index]; - Debug.Assert(grouping is { }); - RemoveItem(grouping, indices.GroupingIndex, indices.ItemIndex); + RemoveItemFromGroup(item); - foreach (ref var candidate in CollectionsMarshal.AsSpan(_indices)) - { - if (candidate.ItemIndex > indices.ItemIndex) - candidate.ItemIndex -= 1; - } + _items.RemoveAt(index); + _itemFactory.Dispose(item); + _itemFactory.SetOriginalIndexes( + items: CollectionsMarshal.AsSpan(_items).Slice(index), + indexChange: -1); break; } case ListChangeAction.Insert: { - var (item, itemIndex) = (value.NewItem, value.NewIndex); - var (grouping, groupingIndex) = GetGrouping(item); - - _indices.Insert(itemIndex, (groupingIndex, grouping.Add(item))); - + _itemFactory.Create(value.NewIndex, value.NewItem, ref this, replacement: false); break; } case ListChangeAction.Replace: { - var item = value.NewItem; - var itemIndex = value.OldIndex; - var indices = _indices[itemIndex]; - var oldGrouping = _groupings[indices.GroupingIndex]; - var newGrouping = GetGrouping(item); - - if (oldGrouping == newGrouping.Item1) - { - oldGrouping.Replace(indices.ItemIndex, item); - } - else - { - Debug.Assert(oldGrouping is { }); - RemoveItem(oldGrouping, indices.GroupingIndex, indices.ItemIndex); - - _indices[itemIndex] = (newGrouping.Item2, newGrouping.Item1.Add(item)); - } + _itemFactory.Create(value.NewIndex, value.NewItem, ref this, replacement: true); + break; + } + case ListChangeAction.Move: + { + var newIndex = value.NewIndex; + var oldIndex = value.OldIndex; + var item = _items[oldIndex]; + + _items.RemoveAt(oldIndex); + _items.Insert(newIndex, item); + + var (start, length, indexChange) = newIndex > oldIndex + ? (oldIndex, newIndex - oldIndex, -1) + : (newIndex, oldIndex - newIndex, 1); + + var items = CollectionsMarshal.AsSpan(_items).Slice(start, length); + + _itemFactory.SetOriginalIndexes(items, indexChange); + _itemFactory.SetOriginalIndex(item, newIndex); break; } } } - private (Grouping, int) GetGrouping(TSource item) + + public void AddItemDeferred(int index, TItem item) => + _items.Insert(index, item); + + public void AddItem(int index, TItem item, TSource source, TKey key) + { + var (grouping, groupingIndex) = GetGrouping(key); + + item.GroupingIndex = groupingIndex; + item.GroupingItemIndex = grouping.Add(source); + + _items.Insert(index, item); + _itemFactory.SetOriginalIndexes( + items: CollectionsMarshal.AsSpan(_items).Slice(index), + indexChange: 1); + } + + public void UpdateItem(int index, TItem item, TSource source, TKey key) + { + var (grouping, groupingIndex) = GetGrouping(key); + + if (groupingIndex != item.GroupingIndex) + { + RemoveItemFromGroup(item); + + item.GroupingIndex = groupingIndex; + item.GroupingItemIndex = grouping.Add(source); + + if (typeof(TItem).IsValueType) + _items[index] = item; + } + } + + public void ReplaceItem(int index, TItem item, TSource source, TKey key) + { + var oldItem = _items[index]; + var (grouping, groupingIndex) = GetGrouping(key); + + if (groupingIndex == oldItem.GroupingIndex) + { + item.GroupingIndex = oldItem.GroupingIndex; + item.GroupingItemIndex = oldItem.GroupingItemIndex; + + _itemFactory.Dispose(oldItem); + _items[index] = item; + + grouping.Replace(item.GroupingIndex, source); + } + else + { + RemoveItemFromGroup(oldItem); + + item.GroupingIndex = groupingIndex; + item.GroupingItemIndex = grouping.Add(source); + + _items[index] = item; + } + } + + private void RemoveItemFromGroup(TItem item) + { + var grouping = _groupings[item.GroupingIndex]; + + Debug.Assert(grouping is { }); + grouping.Remove(item.GroupingItemIndex); + + if (grouping.IsEmpty) + { + _groupings[item.GroupingIndex] = null; + _continuation.OnNext(ListChange.Remove(item.GroupingIndex)); + } + + foreach (ref var candidate in CollectionsMarshal.AsSpan(_items)) + { + if (candidate.GroupingIndex == item.GroupingIndex) + candidate.GroupingItemIndex -= 1; + } + } + + private (Grouping, int) GetGrouping(TKey key) { - var key = _keySelector(item); var hash = key is null ? 0 : _keyComparer?.GetHashCode(key) ?? EqualityComparer.Default.GetHashCode(key); var freeIndex = -1; @@ -272,22 +483,11 @@ public void OnNext(ListChange value) _groupings[freeIndex] = grouping; } - _continuation.OnNext(ListChange.Insert(currentIndex, _resultSelector(key, grouping.ToBuilder()))); + _continuation.OnNext(ListChange.Insert(freeIndex, _resultSelector(key, grouping.ToBuilder()))); return (grouping, freeIndex); } } - - private void RemoveItem(Grouping grouping, int groupingIndex, int itemIndex) - { - grouping.Remove(itemIndex); - - if (grouping.IsEmpty) - { - _groupings[groupingIndex] = null; - _continuation.OnNext(ListChange.Remove(groupingIndex)); - } - } } private sealed class Grouping : IObservable>, IDisposable @@ -327,7 +527,201 @@ public void Remove(int index) } public void Replace(int index, TElement item) => - _items?.OnNext( - ListChange.Replace(index, item)); + _items?.OnNext(ListChange.Replace(index, item)); + } + + private interface IGroupByStateMachine : IStateMachine> + { + void AddItemDeferred(int index, TItem item); + void AddItem(int index, TItem item, TSource source, TKey key); + void UpdateItem(int index, TItem item, TSource source, TKey key); + void ReplaceItem(int index, TItem item, TSource source, TKey key); + } + + private interface IGroupingItem + { + int GroupingIndex { get; set; } + int GroupingItemIndex { get; set; } + } + + private interface IGroupingItemFactory + where TItem : IGroupingItem + { + void Create(int sourceIndex, TSource source, ref TGroupBy groupBy, bool replacement) + where TGroupBy : struct, IGroupByStateMachine; + + void Dispose(TItem item); + void DisposeAll(List items); + + void SetOriginalIndex(TItem item, int index); + void SetOriginalIndexes(ReadOnlySpan items, int indexChange); + } + + private struct GroupingStaticItem : IGroupingItem + { + public int GroupingIndex { get; set; } + public int GroupingItemIndex { get; set; } + + public readonly struct Factory : IGroupingItemFactory + { + public required Func KeySelector { get; init; } + + public void Create(int index, TSource source, ref TGroupBy groupBy, bool replacement) + where TGroupBy : struct, IGroupByStateMachine + { + var key = KeySelector(source); + var item = new GroupingStaticItem(); + + if (replacement) + groupBy.ReplaceItem(index, item, source, key); + else + groupBy.AddItem(index, item, source, key); + } + + public void Dispose(GroupingStaticItem item) { } + public void DisposeAll(List items) { } + + public void SetOriginalIndex(GroupingStaticItem item, int index) { } + public void SetOriginalIndexes(ReadOnlySpan items, int indexChange) { } + } + } + + private sealed class GroupingDynamicItem : IGroupingItem + { + public int GroupingIndex { get; set; } + public int GroupingItemIndex { get; set; } + + private readonly TSource _source; + private int _sourceIndex; + + private IDisposable? _keyChanged; + + private GroupingDynamicItem(int sourceIndex, TSource source, bool replacement) + { + _source = source; + _sourceIndex = sourceIndex; + + GroupingIndex = -1; + GroupingItemIndex = replacement ? 0 : -1; + } + + public readonly struct Factory : IGroupingItemFactory> + { + public required Func> KeySelector { get; init; } + + public void Create(int index, TSource source, ref TGroupBy groupBy, bool replacement) + where TGroupBy : struct, IGroupByStateMachine> + { + var item = new GroupingDynamicItem(index, source, replacement); + + item._keyChanged = KeySelector(source) + .ContinueWith, object>( + new() { Item = item, GroupBy = new StateMachineReference, TGroupBy>(ref groupBy) }) + .Subscribe(); + + if (item.GroupingIndex == -1) + { + // The selector has an async code inside which hasn't finished yet. + groupBy.AddItemDeferred(index, item); + } + } + + public void Dispose(GroupingDynamicItem item) + { + item._sourceIndex = -1; + + item._keyChanged?.Dispose(); + item._keyChanged = null; + } + + public void DisposeAll(List> items) + { + foreach (var item in items) + Dispose(item); + } + + public void SetOriginalIndex(GroupingDynamicItem item, int index) => + item._sourceIndex = index; + + public void SetOriginalIndexes(ReadOnlySpan> items, int indexChange) + { + foreach (var item in items) + item._sourceIndex += indexChange; + } + } + + private readonly struct StateMachineFactory : IStateMachineFactory + where TGroupBy : struct, IGroupByStateMachine> + { + public required readonly GroupingDynamicItem Item { get; init; } + public required readonly StateMachineReference, TGroupBy> GroupBy { get; init; } + + public void Create(in TContinuation continuation, ObserverStateMachine source) + where TContinuation : struct, IStateMachine + { + source.ContinueWith>( + new(continuation, Item, GroupBy)); + } + } + + private struct StateMachine : IStateMachine + where TGroupBy : struct, IGroupByStateMachine> + where TContinution : struct, IStateMachine + { + private TContinution _continuation; + + private readonly GroupingDynamicItem _item; + private readonly StateMachineReference, TGroupBy> _groupBy; + + public StateMachine( + in TContinution continution, + GroupingDynamicItem item, + StateMachineReference, TGroupBy> groupBy) + { + _continuation = continution; + + _item = item; + _groupBy = groupBy; + } + + public StateMachineBox Box => + _continuation.Box; + + public void Initialize(StateMachineBox box) => + _continuation.Initialize(box); + + public void Dispose() => + _continuation.Dispose(); + + public void OnCompleted() => + _continuation.OnCompleted(); + + public void OnError(Exception error) => + _continuation.OnError(error); + + public void OnNext(TKey value) + { + // If there's no key changed subscription then it might be possible + // that the item was disposed while waiting to be processed. So, + // an additional check is required to see if the item was used. + if (_item._sourceIndex == -1) + return; + + if (_item._keyChanged is { }) + { + if (_item.GroupingItemIndex != -1) + _groupBy.Target.UpdateItem(_item._sourceIndex, _item, _item._source, value); + else + _groupBy.Target.ReplaceItem(_item._sourceIndex, _item, _item._source, value); + } + else + { + if (_item.GroupingItemIndex == -1) + _groupBy.Target.AddItem(_item._sourceIndex, _item, _item._source, value); + else + _groupBy.Target.ReplaceItem(_item._sourceIndex, _item, _item._source, value); + } + } + } } } \ No newline at end of file diff --git a/test/Kinetic.Tests/ObservableViewTests.cs b/test/Kinetic.Tests/ObservableViewTests.cs index b19ba76..c2781f1 100644 --- a/test/Kinetic.Tests/ObservableViewTests.cs +++ b/test/Kinetic.Tests/ObservableViewTests.cs @@ -7,7 +7,7 @@ namespace Kinetic.Linq.Tests; public class ObservableViewTests { [Fact] - public void GroupByWithoutComparer() + public void GroupByStaticWithoutComparer() { var list = new ObservableList(); var groupings = list.GroupBy(item => item.Name.Get()).ToView(); @@ -68,7 +68,7 @@ public void GroupByWithoutComparer() Assert.Equal(new[] { itemC }, aGroupingOrdered); Assert.Equal(new[] { itemB }, bGroupingOrdered); - list[1] = itemD; + list[0] = itemD; Assert.Equal(new[] { bGrouping }, groupings); Assert.Equal(new[] { itemB, itemD }, bGrouping); @@ -83,7 +83,7 @@ public void GroupByWithoutComparer() } [Fact] - public void GroupByWithComparer() + public void GroupByStaticWithComparer() { var list = new ObservableList(); var groupings = list.GroupBy(item => item.Name.Get(), StringComparer.OrdinalIgnoreCase).ToView(); @@ -144,7 +144,7 @@ public void GroupByWithComparer() Assert.Equal(new[] { itemC }, aGroupingOrdered); Assert.Equal(new[] { itemB }, bGroupingOrdered); - list[1] = itemD; + list[0] = itemD; Assert.Equal(new[] { bGrouping }, groupings); Assert.Equal(new[] { itemB, itemD }, bGrouping); @@ -158,6 +158,184 @@ public void GroupByWithComparer() Assert.Empty(groupingsOrdered); } + [Fact] + public void GroupByDynamicWithoutComparer() + { + var list = new ObservableList(); + var groupings = list.GroupBy(item => item.Name).ToView(); + var groupingsOrdered = list.GroupBy(item => item.Name, items => items.OrderBy(item => item.Number)).ToView(); + + var itemA = new Item(0, "A"); + var itemB = new Item(1, "B"); + var itemC = new Item(2, "A"); + var itemD = new Item(3, "B"); + + list.Add(itemB); + list.Add(itemC); + list.Add(itemD); + list.Add(itemA); + + var aGrouping = groupings.First(g => g.Key == "A"); + var bGrouping = groupings.First(g => g.Key == "B"); + var aGroupingOrdered = groupingsOrdered.First(g => g.Key == "A"); + var bGroupingOrdered = groupingsOrdered.First(g => g.Key == "B"); + + Assert.Equal(new[] { bGrouping, aGrouping }, groupings); + Assert.Equal(new[] { itemC, itemA }, aGrouping); + Assert.Equal(new[] { itemB, itemD }, bGrouping); + + Assert.Equal(new[] { bGroupingOrdered, aGroupingOrdered }, groupingsOrdered); + Assert.Equal(new[] { itemA, itemC }, aGroupingOrdered); + Assert.Equal(new[] { itemB, itemD }, bGroupingOrdered); + + list.Move(1, 0); + + Assert.Equal(new[] { bGrouping, aGrouping }, groupings); + Assert.Equal(new[] { itemC, itemA }, aGrouping); + Assert.Equal(new[] { itemB, itemD }, bGrouping); + + Assert.Equal(new[] { bGroupingOrdered, aGroupingOrdered }, groupingsOrdered); + Assert.Equal(new[] { itemA, itemC }, aGroupingOrdered); + Assert.Equal(new[] { itemB, itemD }, bGroupingOrdered); + + itemA.Number.Set(5); + itemB.Number.Set(4); + + Assert.Equal(new[] { bGrouping, aGrouping }, groupings); + Assert.Equal(new[] { itemC, itemA }, aGrouping); + Assert.Equal(new[] { itemB, itemD }, bGrouping); + + Assert.Equal(new[] { bGroupingOrdered, aGroupingOrdered }, groupingsOrdered); + Assert.Equal(new[] { itemC, itemA }, aGroupingOrdered); + Assert.Equal(new[] { itemD, itemB }, bGroupingOrdered); + + list.Remove(itemA); + list.Remove(itemD); + + Assert.Equal(new[] { bGrouping, aGrouping }, groupings); + Assert.Equal(new[] { itemC }, aGrouping); + Assert.Equal(new[] { itemB }, bGrouping); + + Assert.Equal(new[] { bGroupingOrdered, aGroupingOrdered }, groupingsOrdered); + Assert.Equal(new[] { itemC }, aGroupingOrdered); + Assert.Equal(new[] { itemB }, bGroupingOrdered); + + list[0] = itemD; + + Assert.Equal(new[] { bGrouping }, groupings); + Assert.Equal(new[] { itemB, itemD }, bGrouping); + + Assert.Equal(new[] { bGroupingOrdered }, groupingsOrdered); + Assert.Equal(new[] { itemD, itemB }, bGroupingOrdered); + + itemD.Name.Set("A"); + + aGrouping = groupings.First(g => g.Key == "A"); + aGroupingOrdered = groupingsOrdered.First(g => g.Key == "A"); + + Assert.Equal(new[] { bGrouping, aGrouping }, groupings); + Assert.Equal(new[] { itemD }, aGrouping); + Assert.Equal(new[] { itemB }, bGrouping); + + Assert.Equal(new[] { bGroupingOrdered, aGroupingOrdered }, groupingsOrdered); + Assert.Equal(new[] { itemD }, aGroupingOrdered); + Assert.Equal(new[] { itemB }, bGroupingOrdered); + + list.Clear(); + + Assert.Empty(groupings); + Assert.Empty(groupingsOrdered); + } + + [Fact] + public void GroupByDynamicWithComparer() + { + var list = new ObservableList(); + var groupings = list.GroupBy(item => item.Name, StringComparer.OrdinalIgnoreCase).ToView(); + var groupingsOrdered = list.GroupBy(item => item.Name, items => items.OrderBy(item => item.Number), StringComparer.OrdinalIgnoreCase).ToView(); + + var itemA = new Item(0, "a"); + var itemB = new Item(1, "B"); + var itemC = new Item(2, "A"); + var itemD = new Item(3, "b"); + + list.Add(itemB); + list.Add(itemC); + list.Add(itemD); + list.Add(itemA); + + var aGrouping = groupings.First(g => g.Key == "A"); + var bGrouping = groupings.First(g => g.Key == "B"); + var aGroupingOrdered = groupingsOrdered.First(g => g.Key == "A"); + var bGroupingOrdered = groupingsOrdered.First(g => g.Key == "B"); + + Assert.Equal(new[] { bGrouping, aGrouping }, groupings); + Assert.Equal(new[] { itemC, itemA }, aGrouping); + Assert.Equal(new[] { itemB, itemD }, bGrouping); + + Assert.Equal(new[] { bGroupingOrdered, aGroupingOrdered }, groupingsOrdered); + Assert.Equal(new[] { itemA, itemC }, aGroupingOrdered); + Assert.Equal(new[] { itemB, itemD }, bGroupingOrdered); + + list.Move(1, 0); + + Assert.Equal(new[] { bGrouping, aGrouping }, groupings); + Assert.Equal(new[] { itemC, itemA }, aGrouping); + Assert.Equal(new[] { itemB, itemD }, bGrouping); + + Assert.Equal(new[] { bGroupingOrdered, aGroupingOrdered }, groupingsOrdered); + Assert.Equal(new[] { itemA, itemC }, aGroupingOrdered); + Assert.Equal(new[] { itemB, itemD }, bGroupingOrdered); + + itemA.Number.Set(5); + itemB.Number.Set(4); + + Assert.Equal(new[] { bGrouping, aGrouping }, groupings); + Assert.Equal(new[] { itemC, itemA }, aGrouping); + Assert.Equal(new[] { itemB, itemD }, bGrouping); + + Assert.Equal(new[] { bGroupingOrdered, aGroupingOrdered }, groupingsOrdered); + Assert.Equal(new[] { itemC, itemA }, aGroupingOrdered); + Assert.Equal(new[] { itemD, itemB }, bGroupingOrdered); + + list.Remove(itemA); + list.Remove(itemD); + + Assert.Equal(new[] { bGrouping, aGrouping }, groupings); + Assert.Equal(new[] { itemC }, aGrouping); + Assert.Equal(new[] { itemB }, bGrouping); + + Assert.Equal(new[] { bGroupingOrdered, aGroupingOrdered }, groupingsOrdered); + Assert.Equal(new[] { itemC }, aGroupingOrdered); + Assert.Equal(new[] { itemB }, bGroupingOrdered); + + list[0] = itemD; + + Assert.Equal(new[] { bGrouping }, groupings); + Assert.Equal(new[] { itemB, itemD }, bGrouping); + + Assert.Equal(new[] { bGroupingOrdered }, groupingsOrdered); + Assert.Equal(new[] { itemD, itemB }, bGroupingOrdered); + + itemD.Name.Set("A"); + + aGrouping = groupings.First(g => g.Key == "A"); + aGroupingOrdered = groupingsOrdered.First(g => g.Key == "A"); + + Assert.Equal(new[] { bGrouping, aGrouping }, groupings); + Assert.Equal(new[] { itemD }, aGrouping); + Assert.Equal(new[] { itemB }, bGrouping); + + Assert.Equal(new[] { bGroupingOrdered, aGroupingOrdered }, groupingsOrdered); + Assert.Equal(new[] { itemD }, aGroupingOrdered); + Assert.Equal(new[] { itemB }, bGroupingOrdered); + + list.Clear(); + + Assert.Empty(groupings); + Assert.Empty(groupingsOrdered); + } + [Fact] public void OrderByStatic() {