From 53c34d0fa7fc0ac9f810a48fdf9274badd71d27a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Mon, 14 Jul 2025 13:49:26 +0200 Subject: [PATCH 1/3] Fix stack test for empty pop --- .../RecyclableDictionaryTests.cs | 64 +++--- .../RecyclableHashSetTests.cs | 91 +++++--- .../RecyclableLinkedListTests.cs | 54 +++-- .../RecyclableQueueTests.cs | 205 ++++++++++-------- .../RecyclableSortedListTests.cs | 85 ++++---- .../RecyclableStackTests.cs | 192 ++++++++-------- 6 files changed, 388 insertions(+), 303 deletions(-) diff --git a/Recyclable.CollectionsTests/RecyclableDictionaryTests.cs b/Recyclable.CollectionsTests/RecyclableDictionaryTests.cs index baba9b4..3aebdf3 100644 --- a/Recyclable.CollectionsTests/RecyclableDictionaryTests.cs +++ b/Recyclable.CollectionsTests/RecyclableDictionaryTests.cs @@ -9,20 +9,22 @@ namespace Recyclable.CollectionsTests { private static readonly (int, string)[] _testData = new[] { (1, "a"), (2, "b"), (3, "c"), (4, "d"), (5, "e") }; - [Fact] - public void AddShouldStoreItems() + [Theory] + [MemberData(nameof(RecyclableLongListTestData.ItemsCountTestCases), MemberType = typeof(RecyclableLongListTestData))] + public void AddShouldStoreItems(int itemsCount) { - using var dict = new RecyclableDictionary(); - foreach (var (key, value) in _testData) + using var dict = new RecyclableDictionary(); + foreach (var key in RecyclableLongListTestData.CreateTestData(itemsCount)) { - dict.Add(key, value); + dict.Add(key, -key); } - _ = dict.Should().HaveCount(_testData.Length); - for (var i = 0; i < _testData.Length; i++) + _ = dict.Should().HaveCount(itemsCount); + for (var i = 0; i < itemsCount; i++) { - dict.GetKey(i).Should().Be(_testData[i].Item1); - dict.GetValue(i).Should().Be(_testData[i].Item2); + long expectedKey = i + 1; + dict.GetKey(i).Should().Be(expectedKey); + dict.GetValue(i).Should().Be(-expectedKey); } } @@ -37,18 +39,28 @@ namespace Recyclable.CollectionsTests _ = dict[1].Should().Be("b"); } - [Fact] - public void RemoveShouldSwapWithLast() + [Theory] + [MemberData(nameof(RecyclableLongListTestData.ItemsCountTestCases), MemberType = typeof(RecyclableLongListTestData))] + public void RemoveShouldSwapWithLast(int itemsCount) { - using var dict = new RecyclableDictionary(); - foreach (var (key, value) in _testData) + using var dict = new RecyclableDictionary(); + foreach (var key in RecyclableLongListTestData.CreateTestData(itemsCount)) { - dict.Add(key, value); + dict.Add(key, key); } - dict.Remove(3).Should().BeTrue(); - _ = dict.ContainsKey(3).Should().BeFalse(); - _ = dict.Should().HaveCount(_testData.Length - 1); + if (itemsCount > 0) + { + long keyToRemove = (itemsCount + 1) / 2; + dict.Remove(keyToRemove).Should().BeTrue(); + _ = dict.ContainsKey(keyToRemove).Should().BeFalse(); + _ = dict.Should().HaveCount(itemsCount - 1); + } + else + { + dict.Remove(0).Should().BeFalse(); + _ = dict.Should().BeEmpty(); + } } [Fact] @@ -63,23 +75,25 @@ namespace Recyclable.CollectionsTests _ = value.Should().Be("a"); } - [Fact] - public void EnumeratorShouldYieldItems() + [Theory] + [MemberData(nameof(RecyclableLongListTestData.ItemsCountTestCases), MemberType = typeof(RecyclableLongListTestData))] + public void EnumeratorShouldYieldItems(int itemsCount) { - using var dict = new RecyclableDictionary(); - foreach (var (key, value) in _testData) + using var dict = new RecyclableDictionary(); + foreach (var key in RecyclableLongListTestData.CreateTestData(itemsCount)) { - dict.Add(key, value); + dict.Add(key, key); } - var actual = new List>(); + var actual = new List(); var enumerator = dict.GetEnumerator(); while (enumerator.MoveNext()) { - actual.Add(enumerator.Current); + actual.Add(enumerator.Current.Key); } - _ = actual.Should().ContainInConsecutiveOrder(_testData.Select(x => new KeyValuePair(x.Item1, x.Item2))); + var expected = RecyclableLongListTestData.CreateTestData(itemsCount).ToList(); + _ = actual.Should().ContainInConsecutiveOrder(expected); } } } diff --git a/Recyclable.CollectionsTests/RecyclableHashSetTests.cs b/Recyclable.CollectionsTests/RecyclableHashSetTests.cs index 5a5af83..9574c9e 100644 --- a/Recyclable.CollectionsTests/RecyclableHashSetTests.cs +++ b/Recyclable.CollectionsTests/RecyclableHashSetTests.cs @@ -7,49 +7,74 @@ namespace Recyclable.CollectionsTests { public class RecyclableHashSetTests { - [Fact] - public void AddShouldStoreUniqueItems() + [Theory] + [MemberData(nameof(RecyclableLongListTestData.ItemsCountTestCases), MemberType = typeof(RecyclableLongListTestData))] + public void AddShouldStoreUniqueItems(int itemsCount) { - using var set = new RecyclableHashSet(); - set.Add("a").Should().BeTrue(); - set.Add("b").Should().BeTrue(); - set.Add("a").Should().BeFalse(); - - _ = set.Should().HaveCount(2); - _ = set.Contains("a").Should().BeTrue(); - _ = set.Contains("b").Should().BeTrue(); - } - - [Fact] - public void RemoveShouldDeleteItem() - { - using var set = new RecyclableHashSet(); - set.Add(1); - set.Add(2); - - set.Remove(1).Should().BeTrue(); - - _ = set.Contains(1).Should().BeFalse(); - _ = set.Should().HaveCount(1); - } - - [Fact] - public void EnumeratorShouldYieldItems() - { - using var set = new RecyclableHashSet(); - for (int i = 0; i < 5; i++) + using var set = new RecyclableHashSet(); + foreach (var item in RecyclableLongListTestData.CreateTestData(itemsCount)) { - set.Add(i); + set.Add(item).Should().BeTrue(); } - var actual = new List(); + foreach (var item in RecyclableLongListTestData.CreateTestData(itemsCount)) + { + set.Add(item).Should().BeFalse(); + } + + _ = set.Should().HaveCount(itemsCount); + } + + [Theory] + [MemberData(nameof(RecyclableLongListTestData.ItemsCountTestCases), MemberType = typeof(RecyclableLongListTestData))] + public void RemoveShouldDeleteItem(int itemsCount) + { + using var set = new RecyclableHashSet(); + foreach (var item in RecyclableLongListTestData.CreateTestData(itemsCount)) + { + set.Add(item); + } + + if (itemsCount > 0) + { + long value = (itemsCount + 1) / 2; + set.Remove(value).Should().BeTrue(); + _ = set.Contains(value).Should().BeFalse(); + _ = set.Should().HaveCount(itemsCount - 1); + } + else + { + set.Remove(0).Should().BeFalse(); + _ = set.Should().BeEmpty(); + } + } + + [Theory] + [MemberData(nameof(RecyclableLongListTestData.ItemsCountTestCases), MemberType = typeof(RecyclableLongListTestData))] + public void EnumeratorShouldYieldItems(int itemsCount) + { + using var set = new RecyclableHashSet(); + foreach (var item in RecyclableLongListTestData.CreateTestData(itemsCount)) + { + set.Add(item); + } + + var actual = new List(); var enumerator = set.GetEnumerator(); while (enumerator.MoveNext()) { actual.Add(enumerator.Current); } - _ = actual.Should().Contain(Enumerable.Range(0, 5)).And.HaveCount(5); + var expected = RecyclableLongListTestData.CreateTestData(itemsCount).ToList(); + if (itemsCount == 0) + { + _ = actual.Should().BeEmpty(); + } + else + { + _ = actual.Should().Contain(expected).And.HaveCount(itemsCount); + } } } } diff --git a/Recyclable.CollectionsTests/RecyclableLinkedListTests.cs b/Recyclable.CollectionsTests/RecyclableLinkedListTests.cs index bf458c1..20b563e 100644 --- a/Recyclable.CollectionsTests/RecyclableLinkedListTests.cs +++ b/Recyclable.CollectionsTests/RecyclableLinkedListTests.cs @@ -8,36 +8,40 @@ namespace Recyclable.CollectionsTests { private static readonly string[] _testData = new[] { "a", "d", "c", "b", "a" }; - [Fact] - public void AddLastShouldPreserveOrder() + [Theory] + [MemberData(nameof(RecyclableLongListTestData.ItemsCountTestCases), MemberType = typeof(RecyclableLongListTestData))] + public void AddLastShouldPreserveOrder(int itemsCount) { - using var list = new RecyclableLinkedList(); - foreach (var item in _testData) + using var list = new RecyclableLinkedList(); + var expected = RecyclableLongListTestData.CreateTestData(itemsCount).ToArray(); + foreach (var item in expected) { list.AddLast(item); } - var actual = new RecyclableLongList(); + var actual = new RecyclableLongList(); foreach (var item in list) { actual.Add(item); } - _ = actual.Should().ContainInConsecutiveOrder(_testData) - .And.BeEquivalentTo(_testData); + _ = actual.Should().ContainInConsecutiveOrder(expected) + .And.BeEquivalentTo(expected); } - [Fact] - public void AddFirstShouldReverseOrder() + [Theory] + [MemberData(nameof(RecyclableLongListTestData.ItemsCountTestCases), MemberType = typeof(RecyclableLongListTestData))] + public void AddFirstShouldReverseOrder(int itemsCount) { - using var list = new RecyclableLinkedList(); - foreach (var item in _testData) + using var list = new RecyclableLinkedList(); + var source = RecyclableLongListTestData.CreateTestData(itemsCount).ToArray(); + foreach (var item in source) { list.AddFirst(item); } - var expected = _testData.Reverse().ToArray(); - var actual = new RecyclableLongList(); + var expected = source.Reverse().ToArray(); + var actual = new RecyclableLongList(); foreach (var item in list) { actual.Add(item); @@ -46,30 +50,34 @@ namespace Recyclable.CollectionsTests _ = actual.Should().ContainInConsecutiveOrder(expected); } - [Fact] - public void RemoveFirstShouldReturnItemsInOrder() + [Theory] + [MemberData(nameof(RecyclableLongListTestData.ItemsCountTestCases), MemberType = typeof(RecyclableLongListTestData))] + public void RemoveFirstShouldReturnItemsInOrder(int itemsCount) { - using var list = new RecyclableLinkedList(_testData); - var actual = new RecyclableLongList(); + var source = RecyclableLongListTestData.CreateTestData(itemsCount).ToArray(); + using var list = new RecyclableLinkedList(source); + var actual = new RecyclableLongList(); while (list.LongCount > 0) { actual.Add(list.RemoveFirst()); } - _ = actual.Should().ContainInConsecutiveOrder(_testData); + _ = actual.Should().ContainInConsecutiveOrder(source); } - [Fact] - public void RemoveLastShouldReturnItemsInReverseOrder() + [Theory] + [MemberData(nameof(RecyclableLongListTestData.ItemsCountTestCases), MemberType = typeof(RecyclableLongListTestData))] + public void RemoveLastShouldReturnItemsInReverseOrder(int itemsCount) { - using var list = new RecyclableLinkedList(_testData); - var actual = new RecyclableLongList(); + var source = RecyclableLongListTestData.CreateTestData(itemsCount).ToArray(); + using var list = new RecyclableLinkedList(source); + var actual = new RecyclableLongList(); while (list.LongCount > 0) { actual.Add(list.RemoveLast()); } - _ = actual.Should().ContainInConsecutiveOrder(_testData.Reverse()); + _ = actual.Should().ContainInConsecutiveOrder(source.Reverse()); } } } diff --git a/Recyclable.CollectionsTests/RecyclableQueueTests.cs b/Recyclable.CollectionsTests/RecyclableQueueTests.cs index 18fe0a8..e0e19df 100644 --- a/Recyclable.CollectionsTests/RecyclableQueueTests.cs +++ b/Recyclable.CollectionsTests/RecyclableQueueTests.cs @@ -7,117 +7,134 @@ namespace Recyclable.CollectionsTests { private static readonly string[] _testData = new[] { "a", "d", "c", "b", "a" }; - [Fact] - public void ShouldNotBeSortedWhenCreated() - { - // Act - using var list = new RecyclableQueue(_testData, 2); + [Theory] + [MemberData(nameof(RecyclableLongListTestData.ItemsCountTestCases), MemberType = typeof(RecyclableLongListTestData))] + public void ShouldNotBeSortedWhenCreated(int itemsCount) + { + // Act + var data = RecyclableLongListTestData.CreateTestData(itemsCount).Select(i => i.ToString()).ToArray(); + using var list = new RecyclableQueue(data, 2); - // Validate - _ = list.Should().HaveCount(_testData.Length) - .And.ContainInConsecutiveOrder(_testData); - _ = list.LongCount.Should().Be(_testData.Length); - } + // Validate + _ = list.Should().HaveCount(itemsCount) + .And.ContainInConsecutiveOrder(data); + _ = list.LongCount.Should().Be(itemsCount); + } - [Fact] - public void ShouldBeEmptyWhenNotInitialized() - { - // Act - using var list = new RecyclableQueue(2); + [Fact] + public void ShouldBeEmptyWhenNotInitialized() + { + // Act + using var list = new RecyclableQueue(2); - // Validate - _ = list.Should().BeEmpty(); - _ = list.LongCount.Should().Be(0); - } + // Validate + _ = list.Should().BeEmpty(); + _ = list.LongCount.Should().Be(0); + } - [Fact] - public void ShouldNotBeSortedWhenInitialized() - { - // Act - using var list = new RecyclableQueue(2) - { - _testData[0], - _testData[1], - _testData[2], - _testData[3], - _testData[4] - }; + [Theory] + [MemberData(nameof(RecyclableLongListTestData.ItemsCountTestCases), MemberType = typeof(RecyclableLongListTestData))] + public void ShouldNotBeSortedWhenInitialized(int itemsCount) + { + // Act + var data = RecyclableLongListTestData.CreateTestData(itemsCount).Select(i => i.ToString()).ToArray(); + using var list = new RecyclableQueue(2) + { + // populate via collection initializer + }; + foreach (var item in data) + { + list.Enqueue(item); + } - // Validate - _ = list.Should().NotBeEmpty() - .And.ContainInConsecutiveOrder(_testData) - .And.BeEquivalentTo(_testData); - } + // Validate + if (itemsCount == 0) + { + _ = list.Should().BeEmpty(); + } + else + { + _ = list.Should().NotBeEmpty() + .And.ContainInConsecutiveOrder(data) + .And.BeEquivalentTo(data); + } + } - [Fact] - public void ShouldBeEmptyAfterClear() - { - // Prepare - using var list = new RecyclableQueue(_testData); + [Theory] + [MemberData(nameof(RecyclableLongListTestData.ItemsCountTestCases), MemberType = typeof(RecyclableLongListTestData))] + public void ShouldBeEmptyAfterClear(int itemsCount) + { + // Prepare + var data = RecyclableLongListTestData.CreateTestData(itemsCount).Select(i => i.ToString()); + using var list = new RecyclableQueue(data); - // Act - list.Clear(); + // Act + list.Clear(); - // Validate - _ = list.Should().BeEmpty(); - } + // Validate + _ = list.Should().BeEmpty(); + } - [Fact] - public void DequeueShouldReturnItemsInTheSameOrder() - { - // Prepare - using var list = new RecyclableQueue(); + [Theory] + [MemberData(nameof(RecyclableLongListTestData.ItemsCountTestCases), MemberType = typeof(RecyclableLongListTestData))] + public void DequeueShouldReturnItemsInTheSameOrder(int itemsCount) + { + // Prepare + var data = RecyclableLongListTestData.CreateTestData(itemsCount).Select(i => i.ToString()).ToArray(); + using var list = new RecyclableQueue(); - // Act - for (var itemIdx = 0; itemIdx < _testData.Length; itemIdx++) - { - list.Enqueue(_testData[itemIdx]); - } + // Act + for (var itemIdx = 0; itemIdx < data.Length; itemIdx++) + { + list.Enqueue(data[itemIdx]); + } - var dequeuedItems = new RecyclableLongList(); - while (list.LongCount > 0) - { - dequeuedItems.Add(list.Dequeue()); - } + var dequeuedItems = new RecyclableLongList(); + while (list.LongCount > 0) + { + dequeuedItems.Add(list.Dequeue()); + } - // Validate - _ = dequeuedItems.Should().ContainInConsecutiveOrder(_testData) - .And.BeEquivalentTo(_testData); - } + // Validate + _ = dequeuedItems.Should().ContainInConsecutiveOrder(data) + .And.BeEquivalentTo(data); + } - [Fact] - public void DequeueShoudRaiseArgumentOutOfRangeWhenNoMoreElementsFound() - { - // Prepare - using var list = new RecyclableQueue(_testData); + [Fact] + public void DequeueShoudRaiseArgumentOutOfRangeWhenNoMoreElementsFound() + { + // Prepare + using var list = new RecyclableQueue(_testData); - // Act - while (list.LongCount > 0) - { - _ = list.Dequeue(); - } + // Act + while (list.LongCount > 0) + { + _ = list.Dequeue(); + } - _ = Assert.Throws(() => list.Dequeue()); - _ = list.LongCount.Should().Be(0); - } + _ = Assert.Throws(() => list.Dequeue()); + _ = list.LongCount.Should().Be(0); + } - [Fact] - public void GetEnumeratorShouldYieldAllItemsInTheSameOrder() - { - // Prepare - using var list = new RecyclableQueue(_testData); + [Theory] + [MemberData(nameof(RecyclableLongListTestData.ItemsCountTestCases), MemberType = typeof(RecyclableLongListTestData))] + public void GetEnumeratorShouldYieldAllItemsInTheSameOrder(int itemsCount) + { + // Prepare + var data = RecyclableLongListTestData.CreateTestData(itemsCount).Select(i => i.ToString()).ToArray(); + using var list = new RecyclableQueue(data); - // Act - var actual = new RecyclableLongList(); - using var enumerator = list.GetEnumerator(); - while (enumerator.MoveNext()) - { - actual.Add(enumerator.Current); - } + // Act + var actual = new RecyclableLongList(); + using var enumerator = list.GetEnumerator(); + while (enumerator.MoveNext()) + { + actual.Add(enumerator.Current); + } - // Validate - _ = actual.Should().ContainInConsecutiveOrder(_testData) - .And.BeEquivalentTo(_testData); - } + // Validate + _ = actual.Should().ContainInConsecutiveOrder(data) + .And.BeEquivalentTo(data); + } } } diff --git a/Recyclable.CollectionsTests/RecyclableSortedListTests.cs b/Recyclable.CollectionsTests/RecyclableSortedListTests.cs index 6f72aa2..e5ddd9a 100644 --- a/Recyclable.CollectionsTests/RecyclableSortedListTests.cs +++ b/Recyclable.CollectionsTests/RecyclableSortedListTests.cs @@ -7,17 +7,20 @@ namespace Recyclable.CollectionsTests { private static readonly (int, string)[] _testData = new[] { (5, "a"), (4, "d"), (3, "c"), (2, "b"), (1, "a") }; - [Fact] - public void ShouldBeSortedWhenCreated() - { - // Act - using var sortedList = new RecyclableSortedList(_testData, 2); + [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(_testData.Length) - .And.BeEquivalentTo(_testData) - .And.BeInAscendingOrder((item) => item.Key); - } + // Validate + _ = sortedList.Should().HaveCount(itemsCount) + .And.BeInAscendingOrder(item => item.Key); + } [Fact] public void ShouldBeEmptyWhenNotInitialized() @@ -47,40 +50,44 @@ namespace Recyclable.CollectionsTests .And.ContainInConsecutiveOrder(_testData); } - [Fact] - public void ShouldBeSortedWhenUpdateEnds() - { - // Prepare - using var sortedList = new RecyclableSortedList(2); + [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 testItem in _testData) - { - sortedList.Add(testItem); - } + // Act + sortedList.BeginUpdate(); + foreach (var value in RecyclableLongListTestData.CreateTestData(itemsCount)) + { + sortedList.Add((itemsCount - value + 1, value)); + } - // Validate - _ = sortedList.Should().BeInDescendingOrder(x => x.Key); + // Validate + _ = sortedList.Should().BeInDescendingOrder(x => x.Key); - // Act - sortedList.EndUpdate(); + // Act + sortedList.EndUpdate(); - // Validate - _ = sortedList.Should().BeInAscendingOrder(x => x.Key); - } + // Validate + _ = sortedList.Should().BeInAscendingOrder(x => x.Key); + } - [Fact] - public void ShouldBeEmptyAfterClear() - { - // Prepare - using var sortedList = new RecyclableSortedList(_testData); + [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(); + // Act + sortedList.Clear(); - // Validate - _ = sortedList.Should().BeEmpty(); - } - } + // Validate + _ = sortedList.Should().BeEmpty(); + } + } } \ No newline at end of file diff --git a/Recyclable.CollectionsTests/RecyclableStackTests.cs b/Recyclable.CollectionsTests/RecyclableStackTests.cs index b0deeec..a3f8218 100644 --- a/Recyclable.CollectionsTests/RecyclableStackTests.cs +++ b/Recyclable.CollectionsTests/RecyclableStackTests.cs @@ -7,112 +7,126 @@ namespace Recyclable.CollectionsTests { private static readonly string[] _testData = new[] { "a", "d", "c", "b", "a" }; - [Fact] - public void GetEnumeratorShouldYieldAllItemsInReversedOrder() - { - // Prepare - using var list = new RecyclableStack(_testData); + [Theory] + [MemberData(nameof(RecyclableLongListTestData.ItemsCountTestCases), MemberType = typeof(RecyclableLongListTestData))] + public void GetEnumeratorShouldYieldAllItemsInReversedOrder(int itemsCount) + { + // Prepare + var data = RecyclableLongListTestData.CreateTestData(itemsCount).Select(i => i.ToString()).ToArray(); + using var list = new RecyclableStack(data); - // Act - var actual = new RecyclableLongList(); - using var enumerator = list.GetEnumerator(); - while (enumerator.MoveNext()) - { - actual.Add(enumerator.Current); - } + // Act + var actual = new RecyclableLongList(); + using var enumerator = list.GetEnumerator(); + while (enumerator.MoveNext()) + { + actual.Add(enumerator.Current); + } - // Validate - _ = actual.Should().ContainInConsecutiveOrder(_testData.Reverse()); - } + // Validate + _ = actual.Should().ContainInConsecutiveOrder(data.Reverse()); + } - [Fact] - public void ShouldNotBeSortedWhenCreated() - { - // Act - using var sortedList = new RecyclableStack(_testData, 2); + [Theory] + [MemberData(nameof(RecyclableLongListTestData.ItemsCountTestCases), MemberType = typeof(RecyclableLongListTestData))] + public void ShouldNotBeSortedWhenCreated(int itemsCount) + { + // Act + var data = RecyclableLongListTestData.CreateTestData(itemsCount).Select(i => i.ToString()).ToArray(); + using var sortedList = new RecyclableStack(data, 2); - // Validate - _ = sortedList.Should().HaveCount(_testData.Length) - .And.ContainInConsecutiveOrder(_testData.Reverse()); - } + // Validate + _ = sortedList.Should().HaveCount(itemsCount) + .And.ContainInConsecutiveOrder(data.Reverse()); + } - [Fact] - public void ShouldBeEmptyWhenNotInitialized() - { - // Act - using var sortedList = new RecyclableStack(2); + [Fact] + public void ShouldBeEmptyWhenNotInitialized() + { + // Act + using var sortedList = new RecyclableStack(2); - // Validate - _ = sortedList.Should().BeEmpty(); - } + // Validate + _ = sortedList.Should().BeEmpty(); + } - [Fact] - public void ShouldNotBeSortedWhenInitialized() - { - // Act - using var sortedList = new RecyclableStack(2) - { - _testData[0], - _testData[1], - _testData[2], - _testData[3], - _testData[4] - }; + [Theory] + [MemberData(nameof(RecyclableLongListTestData.ItemsCountTestCases), MemberType = typeof(RecyclableLongListTestData))] + public void ShouldNotBeSortedWhenInitialized(int itemsCount) + { + // Act + var data = RecyclableLongListTestData.CreateTestData(itemsCount).Select(i => i.ToString()).ToArray(); + using var sortedList = new RecyclableStack(2); + foreach (var item in data) + { + sortedList.Push(item); + } - // Validate - _ = sortedList.Should().NotBeEmpty() - .And.ContainInConsecutiveOrder(_testData.Reverse()) - .And.BeEquivalentTo(_testData); - } + // Validate + if (itemsCount == 0) + { + _ = sortedList.Should().BeEmpty(); + } + else + { + _ = sortedList.Should().NotBeEmpty() + .And.ContainInConsecutiveOrder(data.Reverse()) + .And.BeEquivalentTo(data); + } + } - [Fact] - public void ShouldBeEmptyAfterClear() - { - // Prepare - using var sortedList = new RecyclableStack(_testData); + [Theory] + [MemberData(nameof(RecyclableLongListTestData.ItemsCountTestCases), MemberType = typeof(RecyclableLongListTestData))] + public void ShouldBeEmptyAfterClear(int itemsCount) + { + // Prepare + var data = RecyclableLongListTestData.CreateTestData(itemsCount).Select(i => i.ToString()).ToArray(); + using var sortedList = new RecyclableStack(data); - // Act - sortedList.Clear(); + // Act + sortedList.Clear(); - // Validate - _ = sortedList.Should().BeEmpty(); - } + // Validate + _ = sortedList.Should().BeEmpty(); + } - [Fact] - public void PopShouldReturnItemsInReversedOrder() - { - // Prepare - using var list = new RecyclableStack(); + [Theory] + [MemberData(nameof(RecyclableLongListTestData.ItemsCountTestCases), MemberType = typeof(RecyclableLongListTestData))] + public void PopShouldReturnItemsInReversedOrder(int itemsCount) + { + // Prepare + using var list = new RecyclableStack(); - // Act - for (var itemIdx = 0; itemIdx < _testData.Length; itemIdx++) - { - list.Push(_testData[itemIdx]); - } + // Act + var data = RecyclableLongListTestData.CreateTestData(itemsCount).Select(i => i.ToString()).ToArray(); + for (var itemIdx = 0; itemIdx < data.Length; itemIdx++) + { + list.Push(data[itemIdx]); + } - var dequeuedItems = new RecyclableLongList(); - while (list.LongCount > 0) - { - dequeuedItems.Add(list.Pop()); - } + var dequeuedItems = new RecyclableLongList(); + while (list.LongCount > 0) + { + dequeuedItems.Add(list.Pop()); + } - // Validate - _ = dequeuedItems.Should().ContainInConsecutiveOrder(_testData.Reverse()); - } + // Validate + _ = dequeuedItems.Should().ContainInConsecutiveOrder(data.Reverse()); + } - [Fact] - public void PopShoudRaiseArgumentOutOfRangeWhenNoMoreElementsFound() - { - // Prepare - using var list = new RecyclableQueue(_testData); + [Fact] + public void PopShouldRaiseArgumentOutOfRangeWhenNoMoreElementsFound() + { + // Prepare + using var list = new RecyclableStack(_testData); - // Act - while (list.LongCount > 0) - { - _ = list.Dequeue(); - } + // Act + while (list.LongCount > 0) + { + _ = list.Pop(); + } - _ = Assert.Throws(() => list.Dequeue()); - } + _ = Assert.Throws(() => list.Pop()); + } } } From 0d28f6515dd1342863ad9bc9772823624c1dbfa3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Mon, 14 Jul 2025 16:24:24 +0200 Subject: [PATCH 2/3] Implement recyclable sorted list using pooled arrays --- .../RecyclableSortedList.cs | 333 ++++++++++++++---- .../RecyclableSortedListTests.cs | 178 +++++----- 2 files changed, 357 insertions(+), 154 deletions(-) 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); + } + } +} 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 3/3] 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(); + } + } +}