diff --git a/src/ObservableCollections/Internal/AlternateIndexList.cs b/src/ObservableCollections/Internal/AlternateIndexList.cs new file mode 100644 index 0000000..6c276fa --- /dev/null +++ b/src/ObservableCollections/Internal/AlternateIndexList.cs @@ -0,0 +1,211 @@ +#pragma warning disable CS0436 + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Runtime.InteropServices; + +namespace ObservableCollections.Internal +{ + public class AlternateIndexList : IEnumerable + { + List list; // alternate index is ordered + + public AlternateIndexList() + { + this.list = new(); + } + + public AlternateIndexList(IEnumerable<(int OrderedAlternateIndex, T Value)> values) + { + this.list = values.Select(x => new IndexedValue(x.OrderedAlternateIndex, x.Value)).ToList(); + } + + void UpdateAlternateIndex(int startIndex, int incr) + { + var span = CollectionsMarshal.AsSpan(list); + for (int i = startIndex; i < span.Length; i++) + { + span[i].AlternateIndex += incr; + } + } + + public T this[int index] + { + get => list[index].Value; + } + + public int Count => list.Count; + + public void Insert(int alternateIndex, T value) + { + var index = list.BinarySearch(alternateIndex); + if (index < 0) + { + index = ~index; + } + list.Insert(index, new(alternateIndex, value)); + UpdateAlternateIndex(index + 1, 1); + } + + public void InsertRange(int startingAlternateIndex, IEnumerable values) + { + var index = list.BinarySearch(startingAlternateIndex); + if (index < 0) + { + index = ~index; + } + + using var iter = new InsertIterator(startingAlternateIndex, values); + list.InsertRange(index, iter); + UpdateAlternateIndex(index + iter.ConsumedCount, iter.ConsumedCount); + } + + public void Remove(T value) + { + var index = list.FindIndex(x => EqualityComparer.Default.Equals(x.Value, value)); + if (index != -1) + { + list.RemoveAt(index); + UpdateAlternateIndex(index, -1); + } + } + + public void RemoveAt(int alternateIndex) + { + var index = list.BinarySearch(alternateIndex); + if (index != -1) + { + list.RemoveAt(index); + UpdateAlternateIndex(index, -1); + } + } + + public void RemoveRange(int alternateIndex, int count) + { + var index = list.BinarySearch(alternateIndex); + if (index < 0) + { + index = ~index; + } + + list.RemoveRange(index, count); + UpdateAlternateIndex(index, -count); + } + + public bool TryGetAtAlternateIndex(int alternateIndex, [MaybeNullWhen(true)] out T value) + { + var index = list.BinarySearch(alternateIndex); + if (index < 0) + { + value = default!; + return false; + } + value = list[index].Value!; + return true; + } + + public bool TrySetAtAlternateIndex(int alternateIndex, T value) + { + var index = list.BinarySearch(alternateIndex); + if (index < 0) + { + return false; + } + CollectionsMarshal.AsSpan(list)[index].Value = value; + return true; + } + + public void Clear() + { + list.Clear(); + } + + // Can't implement add, reverse and sort because alternate index is unknown + // void Add(); + // void AddRange(); + // void Reverse(); + // void Sort(); + + public IEnumerator GetEnumerator() + { + foreach (var item in list) + { + yield return item.Value; + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public IEnumerable<(int AlternateIndex, T Value)> GetIndexedValues() + { + foreach (var item in list) + { + yield return (item.AlternateIndex, item.Value); + } + } + + class InsertIterator(int startingIndex, IEnumerable values) : IEnumerable, IEnumerator + { + IEnumerator iter = values.GetEnumerator(); + IndexedValue current; + + public int ConsumedCount { get; private set; } + + public IndexedValue Current => current; + + object IEnumerator.Current => Current; + + public void Dispose() => iter.Reset(); + + public bool MoveNext() + { + if (iter.MoveNext()) + { + ConsumedCount++; + current = new(startingIndex++, iter.Current); + return true; + } + return false; + } + + public void Reset() => iter.Reset(); + + public IEnumerator GetEnumerator() => this; + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } + + struct IndexedValue : IComparable + { + public int AlternateIndex; // mutable + public T Value; // mutable + + public IndexedValue(int alternateIndex, T value) + { + this.AlternateIndex = alternateIndex; + this.Value = value; + } + + public static implicit operator IndexedValue(int alternateIndex) // for query + { + return new IndexedValue(alternateIndex, default!); + } + + public int CompareTo(IndexedValue other) + { + return AlternateIndex.CompareTo(other.AlternateIndex); + } + + public override string ToString() + { + return (AlternateIndex, Value).ToString(); + } + } + } +} diff --git a/src/ObservableCollections/ObservableCollections.csproj b/src/ObservableCollections/ObservableCollections.csproj index 07827b9..afa1786 100644 --- a/src/ObservableCollections/ObservableCollections.csproj +++ b/src/ObservableCollections/ObservableCollections.csproj @@ -25,4 +25,11 @@ + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + diff --git a/tests/ObservableCollections.Tests/AlternateIndexListTest.cs b/tests/ObservableCollections.Tests/AlternateIndexListTest.cs new file mode 100644 index 0000000..28eda2f --- /dev/null +++ b/tests/ObservableCollections.Tests/AlternateIndexListTest.cs @@ -0,0 +1,100 @@ +using ObservableCollections.Internal; + +namespace ObservableCollections.Tests; + +public class AlternateIndexListTest +{ + [Fact] + public void Insert() + { + var list = new AlternateIndexList(); + + list.Insert(0, "foo"); + list.Insert(1, "bar"); + list.Insert(2, "baz"); + list.GetIndexedValues().Should().Equal((0, "foo"), (1, "bar"), (2, "baz")); + + list.Insert(1, "new-bar"); + list.GetIndexedValues().Should().Equal((0, "foo"), (1, "new-bar"), (2, "bar"), (3, "baz")); + + + list.Insert(6, "zoo"); + list.GetIndexedValues().Should().Equal((0, "foo"), (1, "new-bar"), (2, "bar"), (3, "baz"), (6, "zoo")); + } + + [Fact] + public void InsertRange() + { + var list = new AlternateIndexList(); + + list.Insert(0, "foo"); + list.Insert(1, "bar"); + list.Insert(2, "baz"); + + list.InsertRange(1, new[] { "new-foo", "new-bar", "new-baz" }); + list.GetIndexedValues().Should().Equal((0, "foo"), (1, "new-foo"), (2, "new-bar"), (3, "new-baz"), (4, "bar"), (5, "baz")); + } + + [Fact] + public void InsertSparsed() + { + var list = new AlternateIndexList(); + + list.Insert(2, "foo"); + list.Insert(8, "baz"); // baz + list.Insert(4, "bar"); + list.GetIndexedValues().Should().Equal((2, "foo"), (4, "bar"), (9, "baz")); + + list.InsertRange(3, new[] { "new-foo", "new-bar", "new-baz" }); + list.GetIndexedValues().Should().Equal((2, "foo"), (3, "new-foo"), (4, "new-bar"), (5, "new-baz"), (7, "bar"), (12, "baz")); + + list.InsertRange(1, new[] { "zoo" }); + list.GetIndexedValues().Should().Equal((1, "zoo"), (3, "foo"), (4, "new-foo"), (5, "new-bar"), (6, "new-baz"), (8, "bar"), (13, "baz")); + } + + [Fact] + public void Remove() + { + var list = new AlternateIndexList(); + + list.Insert(0, "foo"); + list.Insert(1, "bar"); + list.Insert(2, "baz"); + + list.Remove("bar"); + list.GetIndexedValues().Should().Equal((0, "foo"), (1, "baz")); + + list.RemoveAt(0); + list.GetIndexedValues().Should().Equal((0, "baz")); + } + + [Fact] + public void RemoveRange() + { + var list = new AlternateIndexList(); + + list.Insert(0, "foo"); + list.Insert(1, "bar"); + list.Insert(2, "baz"); + + list.RemoveRange(1, 2); + list.GetIndexedValues().Should().Equal((0, "foo")); + } + + [Fact] + public void TryGetSet() + { + var list = new AlternateIndexList(); + + list.Insert(0, "foo"); + list.Insert(2, "bar"); + list.Insert(4, "baz"); + + list.TryGetAtAlternateIndex(2, out var bar).Should().BeTrue(); + bar.Should().Be("bar"); + + list.TrySetAtAlternateIndex(4, "new-baz").Should().BeTrue(); + list.TryGetAtAlternateIndex(4, out var baz).Should().BeTrue(); + baz.Should().Be("new-baz"); + } +}