diff --git a/Recyclable.Collections/RecyclableSortedList.cs b/Recyclable.Collections/RecyclableSortedList.cs index 9847554..9a7af04 100644 --- a/Recyclable.Collections/RecyclableSortedList.cs +++ b/Recyclable.Collections/RecyclableSortedList.cs @@ -1,81 +1,280 @@ -using Recyclable.Collections.Linq; +using System.Collections; +using System.Numerics; +using System.Runtime.CompilerServices; +using Recyclable.Collections.Pools; namespace Recyclable.Collections { - internal class RecyclableSortedList : RecyclableLongList<(TKey Key, TValue Value)> - where TKey : notnull - { - private static readonly IComparer<(TKey, TValue)> _comparer = new FrequencyTupleComparer(); - private uint _isUpdating; - private readonly SystemRandomNumberGenerator _randomNumberGenerator = new(); + internal sealed class RecyclableSortedList : IEnumerable<(TKey Key, TValue Value)>, IDisposable + where TKey : notnull + { + private static readonly bool _needsClearing = !typeof(TKey).IsValueType || !typeof(TValue).IsValueType; - protected void ListUpdated() - { - QuickSort(); - } + private TKey[] _keys; + private TValue[] _values; + private int _count; + private readonly IComparer _comparer; + private bool _disposed; - public RecyclableSortedList() : base() - { - } + public RecyclableSortedList(int initialCapacity = RecyclableDefaults.InitialCapacity, IComparer? comparer = null) + { + if (initialCapacity < 1) + { + initialCapacity = 1; + } - public RecyclableSortedList(Dictionary source, int blockSize = RecyclableDefaults.BlockSize) - : this(source.Select(x => (x.Key, x.Value)), blockSize) - { - } + if (!BitOperations.IsPow2((uint)initialCapacity)) + { + initialCapacity = (int)BitOperations.RoundUpToPowerOf2((uint)initialCapacity); + } - public RecyclableSortedList(IEnumerable<(TKey Key, TValue Value)> source, int blockSize = RecyclableDefaults.BlockSize) - : base(source, blockSize) - { - QuickSort(); - } + _keys = initialCapacity >= RecyclableDefaults.MinPooledArrayLength + ? RecyclableArrayPool.RentShared(initialCapacity) + : new TKey[initialCapacity]; + _values = initialCapacity >= RecyclableDefaults.MinPooledArrayLength + ? RecyclableArrayPool.RentShared(initialCapacity) + : new TValue[initialCapacity]; + _count = 0; + _comparer = comparer ?? Comparer.Default; + } - public RecyclableSortedList(int blockSize = RecyclableDefaults.BlockSize) - : base(blockSize) - { - } + public int Count => _count; - public bool IsUpdating => _isUpdating > 0; + public TValue this[TKey key] + { + get + { + int index = BinarySearch(key); + if (index >= 0) + { + return _values[index]; + } - public void Add(TKey key, TValue value) => Add((key, value)); + ThrowHelper.ThrowArgumentOutOfRangeException(nameof(key), "Key not found"); + return default!; + } + set + { + int index = BinarySearch(key); + if (index >= 0) + { + _values[index] = value; + return; + } - public void BeginUpdate() => _isUpdating++; + Insert(~index, key, value); + } + } - public new void Clear() - { - BeginUpdate(); - try - { - base.Clear(); - } - finally - { - EndUpdate(); - } - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Add(TKey key, TValue value) + { + int index = BinarySearch(key); + if (index >= 0) + { + ThrowHelper.ThrowArgumentOutOfRangeException(nameof(key), "An element with the same key already exists."); + } - public void EndUpdate(bool raiseListUpdated = true) - { - if (_isUpdating > 0) - { - _isUpdating--; - if (raiseListUpdated && _isUpdating == 0) - { - ListUpdated(); - } - } - } + Insert(~index, key, value); + } - public void QuickSort() - { - BeginUpdate(); - try - { - QuickSortExtensions<(TKey, TValue)>.QuickSort(this, _comparer, _randomNumberGenerator); - } - finally - { - EndUpdate(false); - } - } - } -} \ No newline at end of file + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool ContainsKey(TKey key) => BinarySearch(key) >= 0; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryGetValue(TKey key, out TValue value) + { + int index = BinarySearch(key); + if (index >= 0) + { + value = _values[index]; + return true; + } + + value = default!; + return false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Remove(TKey key) + { + int index = BinarySearch(key); + if (index < 0) + { + return false; + } + + if (index < _count - 1) + { + Array.Copy(_keys, index + 1, _keys, index, _count - index - 1); + Array.Copy(_values, index + 1, _values, index, _count - index - 1); + } + + _count--; + if (_needsClearing) + { + _keys[_count] = default!; + _values[_count] = default!; + } + + return true; + } + + public void Clear() + { + if (_needsClearing && _count > 0) + { + Array.Clear(_keys, 0, _count); + Array.Clear(_values, 0, _count); + } + + _count = 0; + } + + public TKey GetKey(int index) => _keys[index]; + public TValue GetValue(int index) => _values[index]; + + public Enumerator GetEnumerator() => new(this); + IEnumerator<(TKey Key, TValue Value)> IEnumerable<(TKey Key, TValue Value)>.GetEnumerator() => new Enumerator(this); + IEnumerator IEnumerable.GetEnumerator() => new Enumerator(this); + + public void Dispose() + { + if (_disposed) + { + return; + } + + if (_keys.Length >= RecyclableDefaults.MinPooledArrayLength) + { + RecyclableArrayPool.ReturnShared(_keys, _needsClearing); + } + + if (_values.Length >= RecyclableDefaults.MinPooledArrayLength) + { + RecyclableArrayPool.ReturnShared(_values, _needsClearing); + } + + _keys = Array.Empty(); + _values = Array.Empty(); + _count = 0; + _disposed = true; + GC.SuppressFinalize(this); + } + + private void Insert(int index, TKey key, TValue value) + { + if (_count == _keys.Length) + { + Grow(_count + 1); + } + + if (index < _count) + { + Array.Copy(_keys, index, _keys, index + 1, _count - index); + Array.Copy(_values, index, _values, index + 1, _count - index); + } + + _keys[index] = key; + _values[index] = value; + _count++; + } + + private void Grow(int min) + { + int newSize = _keys.Length << 1; + if (newSize < min) + { + newSize = min; + } + + var newKeys = newSize >= RecyclableDefaults.MinPooledArrayLength + ? RecyclableArrayPool.RentShared(newSize) + : new TKey[newSize]; + var newValues = newSize >= RecyclableDefaults.MinPooledArrayLength + ? RecyclableArrayPool.RentShared(newSize) + : new TValue[newSize]; + + Array.Copy(_keys, newKeys, _count); + Array.Copy(_values, newValues, _count); + + if (_keys.Length >= RecyclableDefaults.MinPooledArrayLength) + { + RecyclableArrayPool.ReturnShared(_keys, _needsClearing); + } + + if (_values.Length >= RecyclableDefaults.MinPooledArrayLength) + { + RecyclableArrayPool.ReturnShared(_values, _needsClearing); + } + + _keys = newKeys; + _values = newValues; + } + + private int BinarySearch(TKey key) + { + int low = 0; + int high = _count - 1; + while (low <= high) + { + int mid = (low + high) >> 1; + int cmp = _comparer.Compare(_keys[mid], key); + if (cmp == 0) + { + return mid; + } + + if (cmp < 0) + { + low = mid + 1; + } + else + { + high = mid - 1; + } + } + + return ~low; + } + + public struct Enumerator : IEnumerator<(TKey Key, TValue Value)> + { + private readonly RecyclableSortedList _list; + private int _index; + private (TKey, TValue) _current; + + internal Enumerator(RecyclableSortedList list) + { + _list = list; + _index = 0; + _current = default!; + } + + public (TKey Key, TValue Value) Current => _current; + object IEnumerator.Current => _current; + + public bool MoveNext() + { + if (_index >= _list._count) + { + return false; + } + + _current = (_list._keys[_index], _list._values[_index]); + _index++; + return true; + } + + public void Reset() + { + _index = 0; + _current = default!; + } + + public void Dispose() + { + } + } + } +} diff --git a/Recyclable.CollectionsTests/RecyclableSortedListTests.cs b/Recyclable.CollectionsTests/RecyclableSortedListTests.cs index e5ddd9a..e9580b0 100644 --- a/Recyclable.CollectionsTests/RecyclableSortedListTests.cs +++ b/Recyclable.CollectionsTests/RecyclableSortedListTests.cs @@ -1,93 +1,97 @@ -using FluentAssertions; +using FluentAssertions; using Recyclable.Collections; +using System.Linq; namespace Recyclable.CollectionsTests { - public class RecyclableSortedListTests - { - private static readonly (int, string)[] _testData = new[] { (5, "a"), (4, "d"), (3, "c"), (2, "b"), (1, "a") }; + public class RecyclableSortedListTests + { + [Theory] + [MemberData(nameof(RecyclableLongListTestData.ItemsCountTestCases), MemberType = typeof(RecyclableLongListTestData))] + public void AddShouldStoreItemsSorted(int itemsCount) + { + using var list = new RecyclableSortedList(); + foreach (var key in RecyclableLongListTestData.CreateTestData(itemsCount).Reverse()) + { + list.Add(key, -key); + } - [Theory] - [MemberData(nameof(RecyclableLongListTestData.ItemsCountTestCases), MemberType = typeof(RecyclableLongListTestData))] - public void ShouldBeSortedWhenCreated(int itemsCount) - { - // Act - var data = RecyclableLongListTestData.CreateTestData(itemsCount) - .Select(x => (itemsCount - x + 1, x)) - .ToArray(); - using var sortedList = new RecyclableSortedList(data, 2); - - // Validate - _ = sortedList.Should().HaveCount(itemsCount) - .And.BeInAscendingOrder(item => item.Key); - } - - [Fact] - public void ShouldBeEmptyWhenNotInitialized() - { - // Act - using var sortedList = new RecyclableSortedList(2); - - // Validate - _ = sortedList.Should().BeEmpty(); - } - - [Fact] - public void ShouldNotBeSortedWhenInitialized() - { - // Act - using var sortedList = new RecyclableSortedList(2) - { - _testData[0], - _testData[1], - _testData[2], - _testData[3], - _testData[4] - }; - - // Validate - _ = sortedList.Should().NotBeEmpty() - .And.ContainInConsecutiveOrder(_testData); - } - - [Theory] - [MemberData(nameof(RecyclableLongListTestData.ItemsCountTestCases), MemberType = typeof(RecyclableLongListTestData))] - public void ShouldBeSortedWhenUpdateEnds(int itemsCount) - { - // Prepare - using var sortedList = new RecyclableSortedList(2); - - // Act - sortedList.BeginUpdate(); - foreach (var value in RecyclableLongListTestData.CreateTestData(itemsCount)) - { - sortedList.Add((itemsCount - value + 1, value)); - } - - // Validate - _ = sortedList.Should().BeInDescendingOrder(x => x.Key); - - // Act - sortedList.EndUpdate(); - - // Validate - _ = sortedList.Should().BeInAscendingOrder(x => x.Key); - } - - [Theory] - [MemberData(nameof(RecyclableLongListTestData.ItemsCountTestCases), MemberType = typeof(RecyclableLongListTestData))] - public void ShouldBeEmptyAfterClear(int itemsCount) - { - // Prepare - var data = RecyclableLongListTestData.CreateTestData(itemsCount) - .Select(x => (x, x)); - using var sortedList = new RecyclableSortedList(data); - - // Act - sortedList.Clear(); - - // Validate - _ = sortedList.Should().BeEmpty(); - } + _ = list.Should().HaveCount(itemsCount) + .And.BeInAscendingOrder(x => x.Key); + for (int i = 0; i < itemsCount; i++) + { + long expectedKey = i + 1; + list.GetKey(i).Should().Be(expectedKey); + list.GetValue(i).Should().Be(-expectedKey); + } } -} \ No newline at end of file + + [Fact] + public void IndexerShouldUpdateValue() + { + using var list = new RecyclableSortedList(); + list.Add(1, "a"); + + list[1] = "b"; + + _ = list[1].Should().Be("b"); + } + + [Theory] + [MemberData(nameof(RecyclableLongListTestData.ItemsCountTestCases), MemberType = typeof(RecyclableLongListTestData))] + public void RemoveShouldDeleteItem(int itemsCount) + { + using var list = new RecyclableSortedList(); + foreach (var key in RecyclableLongListTestData.CreateTestData(itemsCount)) + { + list.Add(key, key); + } + + if (itemsCount > 0) + { + long keyToRemove = (itemsCount + 1) / 2; + list.Remove(keyToRemove).Should().BeTrue(); + _ = list.ContainsKey(keyToRemove).Should().BeFalse(); + _ = list.Should().HaveCount(itemsCount - 1); + } + else + { + list.Remove(0).Should().BeFalse(); + _ = list.Should().BeEmpty(); + } + } + + [Fact] + public void TryGetValueShouldReturnValue() + { + using var list = new RecyclableSortedList(); + list.Add(1, "a"); + + var found = list.TryGetValue(1, out var value); + + _ = found.Should().BeTrue(); + _ = value.Should().Be("a"); + } + + [Theory] + [MemberData(nameof(RecyclableLongListTestData.ItemsCountTestCases), MemberType = typeof(RecyclableLongListTestData))] + public void EnumeratorShouldYieldItemsSorted(int itemsCount) + { + using var list = new RecyclableSortedList(); + foreach (var key in RecyclableLongListTestData.CreateTestData(itemsCount).Reverse()) + { + list.Add(key, key); + } + + var actual = new List(); + var enumerator = list.GetEnumerator(); + while (enumerator.MoveNext()) + { + actual.Add(enumerator.Current.Key); + } + + var expected = RecyclableLongListTestData.CreateTestData(itemsCount).ToList(); + _ = actual.Should().ContainInConsecutiveOrder(expected); + } + } +}