From 80499d0936dcdf72190610e0f8d033c2c96e7f9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Mon, 14 Jul 2025 11:22:58 +0200 Subject: [PATCH] Add RecyclableSortedSet and long list sort extensions --- ...lableLongList.Compatibility.List.Sorted.cs | 39 ++++ Recyclable.Collections/RecyclableSortedSet.cs | 215 ++++++++++++++++++ .../RecyclableSortedSetTests.cs | 68 ++++++ 3 files changed, 322 insertions(+) create mode 100644 Recyclable.Collections/RecyclableLongList.Compatibility.List.Sorted.cs create mode 100644 Recyclable.Collections/RecyclableSortedSet.cs create mode 100644 Recyclable.CollectionsTests/RecyclableSortedSetTests.cs diff --git a/Recyclable.Collections/RecyclableLongList.Compatibility.List.Sorted.cs b/Recyclable.Collections/RecyclableLongList.Compatibility.List.Sorted.cs new file mode 100644 index 0000000..7e28d19 --- /dev/null +++ b/Recyclable.Collections/RecyclableLongList.Compatibility.List.Sorted.cs @@ -0,0 +1,39 @@ +using System.Runtime.CompilerServices; +using System.Collections.Generic; +using Recyclable.Collections.Linq; + +namespace Recyclable.Collections +{ + [Obsolete("It'll be moved to Recyclable.Collections.Compatibility.List NuGet package in the next release.")] + public static class zRecyclableLongListCompatibilityListSorted + { + public static void Sort(this RecyclableLongList list) + { + QuickSortExtensions.QuickSort(list); + list._version++; + } + + public static void Sort(this RecyclableLongList list, Comparison comparison) + { + QuickSortExtensions.QuickSort(list, new ComparisonToComparerAdapter(comparison), new SystemRandomNumberGenerator()); + list._version++; + } + + public static void Sort(this RecyclableLongList list, IComparer? comparer) + { + QuickSortExtensions.QuickSort(list, comparer ?? Comparer.Default, new SystemRandomNumberGenerator()); + list._version++; + } + + public static void Sort(this RecyclableLongList list, long index, long count, IComparer? comparer) + { + if (count == 0) + { + return; + } + + QuickSortExtensions.QuickSort(list, (int)index, (int)(index + count - 1), comparer ?? Comparer.Default, new SystemRandomNumberGenerator()); + list._version++; + } + } +} diff --git a/Recyclable.Collections/RecyclableSortedSet.cs b/Recyclable.Collections/RecyclableSortedSet.cs new file mode 100644 index 0000000..e5ad458 --- /dev/null +++ b/Recyclable.Collections/RecyclableSortedSet.cs @@ -0,0 +1,215 @@ +using System.Collections; +using System.Numerics; +using System.Runtime.CompilerServices; +using Recyclable.Collections.Pools; + +namespace Recyclable.Collections +{ + internal sealed class RecyclableSortedSet : IEnumerable, IDisposable where T : notnull + { + private static readonly bool _needsClearing = !typeof(T).IsValueType; + + private readonly IComparer _comparer; + + private T[] _items; + private int _count; + private bool _disposed; + + public RecyclableSortedSet(int initialCapacity = RecyclableDefaults.InitialCapacity, IComparer? comparer = null) + { + if (initialCapacity < 1) + { + initialCapacity = 1; + } + + if (!BitOperations.IsPow2((uint)initialCapacity)) + { + initialCapacity = (int)BitOperations.RoundUpToPowerOf2((uint)initialCapacity); + } + + _items = initialCapacity >= RecyclableDefaults.MinPooledArrayLength + ? RecyclableArrayPool.RentShared(initialCapacity) + : new T[initialCapacity]; + _count = 0; + _comparer = comparer ?? Comparer.Default; + } + + public int Count => _count; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Add(T item) + { + int index = BinarySearch(item); + if (index < _count && _comparer.Compare(_items[index], item) == 0) + { + return false; + } + + if (_count == _items.Length) + { + Grow(); + } + + if (index < _count) + { + Array.Copy(_items, index, _items, index + 1, _count - index); + } + + _items[index] = item; + _count++; + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Contains(T item) + { + int index = BinarySearch(item); + return index < _count && _comparer.Compare(_items[index], item) == 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Remove(T item) + { + int index = BinarySearch(item); + if (index >= _count || _comparer.Compare(_items[index], item) != 0) + { + return false; + } + + if (index < _count - 1) + { + Array.Copy(_items, index + 1, _items, index, _count - index - 1); + } + + _count--; + if (_needsClearing) + { + _items[_count] = default!; + } + + return true; + } + + public void Clear() + { + if (_needsClearing) + { + Array.Clear(_items, 0, _count); + } + + _count = 0; + } + + public Enumerator GetEnumerator() => new(this); + IEnumerator IEnumerable.GetEnumerator() => new Enumerator(this); + IEnumerator IEnumerable.GetEnumerator() => new Enumerator(this); + + private void Grow() + { + int newSize; + if (_items.Length >= RecyclableDefaults.MaxPooledBlockSize) + { + newSize = RecyclableDefaults.MaxPooledBlockSize; + } + else + { + var doubled = _items.Length << 1; + newSize = (int)Math.Min(doubled, RecyclableDefaults.MaxPooledBlockSize); + } + + var newItems = newSize >= RecyclableDefaults.MinPooledArrayLength + ? RecyclableArrayPool.RentShared(newSize) + : new T[newSize]; + Array.Copy(_items, newItems, _count); + + if (_items.Length >= RecyclableDefaults.MinPooledArrayLength) + { + RecyclableArrayPool.ReturnShared(_items, _needsClearing); + } + + _items = newItems; + } + + private int BinarySearch(T item) + { + int low = 0; + int high = _count - 1; + while (low <= high) + { + int mid = (low + high) >> 1; + int cmp = _comparer.Compare(_items[mid], item); + if (cmp == 0) + { + return mid; + } + + if (cmp < 0) + { + low = mid + 1; + } + else + { + high = mid - 1; + } + } + + return low; + } + + public void Dispose() + { + if (_disposed) + { + return; + } + + Clear(); + if (_items.Length >= RecyclableDefaults.MinPooledArrayLength) + { + RecyclableArrayPool.ReturnShared(_items, _needsClearing); + } + + _items = Array.Empty(); + _disposed = true; + GC.SuppressFinalize(this); + } + + public struct Enumerator : IEnumerator + { + private readonly RecyclableSortedSet _set; + private int _index; + private T? _current; + + internal Enumerator(RecyclableSortedSet set) + { + _set = set; + _index = 0; + _current = default; + } + + public T Current => _current!; + object IEnumerator.Current => _current!; + + public bool MoveNext() + { + if (_index >= _set._count) + { + return false; + } + + _current = _set._items[_index++]; + return true; + } + + public void Reset() + { + _index = 0; + _current = default; + } + + public void Dispose() + { + } + } + } +} diff --git a/Recyclable.CollectionsTests/RecyclableSortedSetTests.cs b/Recyclable.CollectionsTests/RecyclableSortedSetTests.cs new file mode 100644 index 0000000..7563169 --- /dev/null +++ b/Recyclable.CollectionsTests/RecyclableSortedSetTests.cs @@ -0,0 +1,68 @@ +using FluentAssertions; +using Recyclable.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace Recyclable.CollectionsTests +{ + public class RecyclableSortedSetTests + { + [Fact] + public void AddShouldStoreUniqueItemsInOrder() + { + using var set = new RecyclableSortedSet(); + var values = new[] { 4, 1, 3, 2, 5 }; + foreach (var v in values) + { + set.Add(v).Should().BeTrue(); + } + + set.Add(3).Should().BeFalse(); + _ = set.Should().Equal(values.OrderBy(x => x)); + } + + [Fact] + public void RemoveShouldDeleteItem() + { + using var set = new RecyclableSortedSet(); + var values = new[] { 4, 1, 3, 2, 5 }; + foreach (var v in values) + { + set.Add(v); + } + + set.Remove(3).Should().BeTrue(); + _ = set.Contains(3).Should().BeFalse(); + _ = set.Should().Equal(values.Where(x => x != 3).OrderBy(x => x)); + } + + [Fact] + public void EnumeratorShouldYieldItemsInOrder() + { + using var set = new RecyclableSortedSet(); + foreach (var v in new[] { 3, 1, 4, 2 }) + { + set.Add(v); + } + + var result = new List(); + foreach (var item in set) + { + result.Add(item); + } + + _ = result.Should().Equal(new[] { 1, 2, 3, 4 }); + } + + [Fact] + public void ShouldBeEmptyAfterClear() + { + using var set = new RecyclableSortedSet(); + set.Add(1); + set.Add(2); + + set.Clear(); + _ = set.Should().BeEmpty(); + } + } +}