From 941cf4f3fd0ecee7e23b955b0652cf80f9a23ec6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Mon, 14 Jul 2025 16:25:29 +0200 Subject: [PATCH] Add RecyclableSortedDictionary with tests --- .../RecyclableSortedDictionary.cs | 287 ++++++++++++++++++ .../RecyclableSortedDictionaryTests.cs | 96 ++++++ 2 files changed, 383 insertions(+) create mode 100644 Recyclable.Collections/RecyclableSortedDictionary.cs create mode 100644 Recyclable.CollectionsTests/RecyclableSortedDictionaryTests.cs diff --git a/Recyclable.Collections/RecyclableSortedDictionary.cs b/Recyclable.Collections/RecyclableSortedDictionary.cs new file mode 100644 index 0000000..5b40d5d --- /dev/null +++ b/Recyclable.Collections/RecyclableSortedDictionary.cs @@ -0,0 +1,287 @@ +using System.Collections; +using System.Collections.Generic; +using System.Numerics; +using System.Runtime.CompilerServices; +using Recyclable.Collections.Pools; + +namespace Recyclable.Collections +{ + internal sealed class RecyclableSortedDictionary : IEnumerable>, IDisposable + where TKey : notnull + { + private static readonly bool _needsClearing = !typeof(TKey).IsValueType || !typeof(TValue).IsValueType; + + private readonly IComparer _comparer; + private TKey[] _keys; + private TValue[] _values; + private int _count; + private bool _disposed; + + public RecyclableSortedDictionary(int initialCapacity = RecyclableDefaults.InitialCapacity, IComparer? comparer = null) + { + if (initialCapacity < 1) + { + initialCapacity = 1; + } + + if (!BitOperations.IsPow2((uint)initialCapacity)) + { + initialCapacity = (int)BitOperations.RoundUpToPowerOf2((uint)initialCapacity); + } + + _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 int Count => _count; + + public TValue this[TKey key] + { + get + { + int index = BinarySearch(key); + if (index < _count && _comparer.Compare(_keys[index], key) == 0) + { + return _values[index]; + } + + ThrowHelper.ThrowArgumentOutOfRangeException(nameof(key), "Key not found"); + return default!; + } + set + { + int index = BinarySearch(key); + if (index < _count && _comparer.Compare(_keys[index], key) == 0) + { + _values[index] = value; + return; + } + + InsertAt(index, key, value); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Add(TKey key, TValue value) + { + int index = BinarySearch(key); + if (index < _count && _comparer.Compare(_keys[index], key) == 0) + { + throw new ArgumentException("An element with the same key already exists.", nameof(key)); + } + + InsertAt(index, key, value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool ContainsKey(TKey key) + { + int index = BinarySearch(key); + return index < _count && _comparer.Compare(_keys[index], key) == 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Remove(TKey key) + { + int index = BinarySearch(key); + if (index >= _count || _comparer.Compare(_keys[index], key) != 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; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryGetValue(TKey key, out TValue value) + { + int index = BinarySearch(key); + if (index < _count && _comparer.Compare(_keys[index], key) == 0) + { + value = _values[index]; + return true; + } + + value = default!; + return false; + } + + public void Clear() + { + if (_needsClearing) + { + 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> IEnumerable>.GetEnumerator() => new Enumerator(this); + IEnumerator IEnumerable.GetEnumerator() => new Enumerator(this); + + private void InsertAt(int index, TKey key, TValue value) + { + if (_count == _keys.Length) + { + Grow(); + } + + 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 newSize; + if (_keys.Length >= RecyclableDefaults.MaxPooledBlockSize) + { + newSize = RecyclableDefaults.MaxPooledBlockSize; + } + else + { + int doubled = _keys.Length << 1; + newSize = (int)Math.Min(doubled, RecyclableDefaults.MaxPooledBlockSize); + } + + 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 void Dispose() + { + if (_disposed) + { + return; + } + + Clear(); + if (_keys.Length >= RecyclableDefaults.MinPooledArrayLength) + { + RecyclableArrayPool.ReturnShared(_keys, _needsClearing); + } + if (_values.Length >= RecyclableDefaults.MinPooledArrayLength) + { + RecyclableArrayPool.ReturnShared(_values, _needsClearing); + } + + _keys = Array.Empty(); + _values = Array.Empty(); + _disposed = true; + GC.SuppressFinalize(this); + } + + public struct Enumerator : IEnumerator> + { + private readonly RecyclableSortedDictionary _dictionary; + private int _index; + private KeyValuePair _current; + + internal Enumerator(RecyclableSortedDictionary dictionary) + { + _dictionary = dictionary; + _index = 0; + _current = default; + } + + public KeyValuePair Current => _current; + object IEnumerator.Current => _current; + + public bool MoveNext() + { + if (_index >= _dictionary._count) + { + return false; + } + + _current = new KeyValuePair(_dictionary._keys[_index], _dictionary._values[_index]); + _index++; + return true; + } + + public void Reset() + { + _index = 0; + _current = default; + } + + public void Dispose() + { + } + } + } +} diff --git a/Recyclable.CollectionsTests/RecyclableSortedDictionaryTests.cs b/Recyclable.CollectionsTests/RecyclableSortedDictionaryTests.cs new file mode 100644 index 0000000..8fa94c2 --- /dev/null +++ b/Recyclable.CollectionsTests/RecyclableSortedDictionaryTests.cs @@ -0,0 +1,96 @@ +using FluentAssertions; +using Recyclable.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace Recyclable.CollectionsTests +{ + public class RecyclableSortedDictionaryTests + { + [Fact] + public void AddShouldStoreUniqueItemsInOrder() + { + using var dict = new RecyclableSortedDictionary(); + var values = new[] { 4, 1, 3, 2, 5 }; + foreach (var v in values) + { + dict.Add(v, v); + } + + var result = new List(); + foreach (var kv in dict) + { + result.Add(kv.Key); + } + + _ = result.Should().Equal(values.OrderBy(x => x)); + } + + [Fact] + public void IndexerShouldUpdateValue() + { + using var dict = new RecyclableSortedDictionary(); + dict.Add(1, "a"); + + dict[1] = "b"; + + _ = dict[1].Should().Be("b"); + } + + [Fact] + public void RemoveShouldDeleteItem() + { + using var dict = new RecyclableSortedDictionary(); + var values = new[] { 4, 1, 3, 2, 5 }; + foreach (var v in values) + { + dict.Add(v, v.ToString()); + } + + dict.Remove(3).Should().BeTrue(); + _ = dict.ContainsKey(3).Should().BeFalse(); + _ = dict.Count.Should().Be(values.Length - 1); + } + + [Fact] + public void EnumeratorShouldYieldItemsInOrder() + { + using var dict = new RecyclableSortedDictionary(); + foreach (var v in new[] { 3, 1, 4, 2 }) + { + dict.Add(v, v); + } + + var result = new List(); + foreach (var item in dict) + { + result.Add(item.Key); + } + + _ = result.Should().Equal(new[] { 1, 2, 3, 4 }); + } + + [Fact] + public void TryGetValueShouldReturnValue() + { + using var dict = new RecyclableSortedDictionary(); + dict.Add(1, "a"); + + var found = dict.TryGetValue(1, out var value); + + _ = found.Should().BeTrue(); + _ = value.Should().Be("a"); + } + + [Fact] + public void ShouldBeEmptyAfterClear() + { + using var dict = new RecyclableSortedDictionary(); + dict.Add(1, 1); + dict.Add(2, 2); + + dict.Clear(); + _ = dict.Should().BeEmpty(); + } + } +}