diff --git a/Recyclable.Collections/RecyclableArrayList.cs b/Recyclable.Collections/RecyclableArrayList.cs index a559252..7b83164 100644 --- a/Recyclable.Collections/RecyclableArrayList.cs +++ b/Recyclable.Collections/RecyclableArrayList.cs @@ -11,9 +11,9 @@ namespace Recyclable.Collections protected T[] _memoryBlock; - private static void ThrowArgumentOutOfRangeException() + private static void ThrowArgumentOutOfRangeException(in string message) { - throw new ArgumentOutOfRangeException("index"); + throw new ArgumentOutOfRangeException(message, (Exception?)null); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -162,6 +162,11 @@ namespace Recyclable.Collections public void AddRange(in T[] items) { + if (items.LongLength == 0) + { + return; + } + int sourceItemsCount = items.Length; int targetCapacity = _count + sourceItemsCount; if (_capacity < targetCapacity) @@ -177,6 +182,11 @@ namespace Recyclable.Collections public void AddRange(List items) { + if (items.Count == 0) + { + return; + } + var sourceItemsCount = items.Count; var targetCapacity = _count + sourceItemsCount; if (_capacity < targetCapacity) @@ -190,6 +200,11 @@ namespace Recyclable.Collections public void AddRange(IList items) { + if (items.Count == 0) + { + return; + } + var sourceItemsCount = items.Count; var targetCapacity = _count + sourceItemsCount; if (_capacity < targetCapacity) @@ -203,6 +218,11 @@ namespace Recyclable.Collections public void AddRange(RecyclableArrayList items) { + if (items.Count == 0) + { + return; + } + var sourceItemsCount = items._count; var targetCapacity = _count + sourceItemsCount; if (_capacity < targetCapacity) @@ -216,6 +236,58 @@ namespace Recyclable.Collections _count = targetCapacity; } + public void AddRange(RecyclableList items) + { + if (items.LongCount == 0) + { + return; + } + + var sourceItemsCount = items.LongCount; + if (sourceItemsCount > int.MaxValue) + { + ThrowArgumentOutOfRangeException($"The number of items exceeds the maximum capacity of {nameof(RecyclableArrayList)}, equal {int.MaxValue}, equal {int.MaxValue}. Please consider using {nameof(RecyclableList)}, instead"); + } + + if (_count + sourceItemsCount > int.MaxValue) + { + ThrowArgumentOutOfRangeException($"The total number of items in source and target table exceeds the maximum capacity of {nameof(RecyclableArrayList)}, equal {int.MaxValue}. Please consider using {nameof(RecyclableList)}, instead"); + } + + int targetCapacity = _count + (int)sourceItemsCount; + if (_capacity < targetCapacity) + { + _ = EnsureCapacity(targetCapacity); + } + + Span targetSpan = new Span(_memoryBlock)[_count..], + itemsSpan; + + int blockIndex, + sourceBlockSize = items.BlockSize, + lastBlockIndex = (int)(sourceItemsCount / sourceBlockSize) - 1; + + for (blockIndex = 0; blockIndex <= lastBlockIndex; blockIndex++) + { + itemsSpan = new(items.MemoryBlocks[blockIndex], 0, sourceBlockSize); + itemsSpan.CopyTo(targetSpan); + targetSpan = targetSpan[sourceBlockSize..]; + } + + if (blockIndex == 0) + { + itemsSpan = new(items.MemoryBlocks[blockIndex], 0, (int)sourceItemsCount); + itemsSpan.CopyTo(targetSpan); + } + else if (blockIndex < items.BlockCount) + { + itemsSpan = new(items.MemoryBlocks[blockIndex], 0, items.NextItemIndex); + itemsSpan.CopyTo(targetSpan); + } + + _count = targetCapacity; + } + public void AddRange(IEnumerable source, int growByCount = RecyclableDefaults.MinPooledArrayLength) { if (source is T[] sourceArray) @@ -375,7 +447,7 @@ namespace Recyclable.Collections if (index >= oldCount) { - ThrowArgumentOutOfRangeException(); + ThrowArgumentOutOfRangeException($"Argument \"{index}\" is out of range. The current version supports removing the last element, only "); return; } diff --git a/Recyclable.Collections/RecyclableList.cs b/Recyclable.Collections/RecyclableList.cs index a67aeb7..97e6556 100644 --- a/Recyclable.Collections/RecyclableList.cs +++ b/Recyclable.Collections/RecyclableList.cs @@ -22,6 +22,9 @@ namespace Recyclable.Collections protected ArrayPool _blockArrayPool; protected T[][] _memoryBlocks; + public int BlockSize => _blockSize; + public int BlockCount => _memoryBlocks.Length; + private long _capacity; public long Capacity { @@ -29,7 +32,8 @@ namespace Recyclable.Collections protected set => _capacity = value; } - public int Count => (int)_longCount; + public int Count => checked((int)_longCount); + public bool IsReadOnly { get; } protected long _longCount; @@ -39,8 +43,8 @@ namespace Recyclable.Collections set => _longCount = value; } - public bool IsReadOnly { get; } - public int BlockCount => _memoryBlocks.Length; + public int LastBlockIndex => _lastBlockIndex; + public int NextItemIndex => _nextItemIndex; private static void RemoveAt(RecyclableList list, long index, ArrayPool blockArrayPool) { diff --git a/Recyclable.CollectionsTests/RecyclableArrayListTests.cs b/Recyclable.CollectionsTests/RecyclableArrayListTests.cs index 7b6f475..1a9ce13 100644 --- a/Recyclable.CollectionsTests/RecyclableArrayListTests.cs +++ b/Recyclable.CollectionsTests/RecyclableArrayListTests.cs @@ -7,75 +7,163 @@ namespace Recyclable.CollectionsTests { private readonly static IEnumerable _testData = Enumerable.Range(1, 20); - [Fact] - public void AddCountShouldSucceed() + [Theory] + [MemberData(nameof(RecyclableListTestData.SourceDataVariants), MemberType = typeof(RecyclableListTestData))] + public void AddCountShouldSucceed(string testCase, IEnumerable testData, long itemsCount) { // Prepare - var testData = _testData.ToArray(); - var list = new RecyclableArrayList(); + var list = new RecyclableArrayList(); // Act foreach (var item in testData) { list.Add(item); - _ = list.Count.Should().Be(item); + _ = list.Count.Should().Be((int)item); } // Validate - _ = list.Capacity.Should().Be(32); - _ = list.Should().HaveCount(testData.Length) - .And.ContainInConsecutiveOrder(testData) - .And.BeEquivalentTo(testData); + _ = list.Capacity.Should().BeGreaterThanOrEqualTo((int)itemsCount); + _ = list.Count.Should().Be((int)itemsCount); + _ = list.Should().ContainInConsecutiveOrder(testData); } - [Fact] - public void AddRangeShouldAddItemsInCorrectOrderWhenSourceIsArray() + [Theory] + [MemberData(nameof(RecyclableListTestData.SourceDataVariants), MemberType = typeof(RecyclableListTestData))] + public void AddRangeShouldAddItemsInCorrectOrder(string testCase, IEnumerable testData, long itemsCount) { // Prepare - var testData = _testData.ToArray(); - var list = new RecyclableArrayList(); + using var list = new RecyclableArrayList(); // Act - list.AddRange(testData); + if (testData is long[] testDataArray) + { + list.AddRange(testDataArray); + } + else if (testData is List testDataList) + { + list.AddRange(testDataList); + } + else if (testData is RecyclableArrayList testDataRecyclableArrayList) + { + list.AddRange(testDataRecyclableArrayList); + } + else if (testData is RecyclableList testDataRecyclableList) + { + list.AddRange(testDataRecyclableList); + } + else if (testData is IList testDataIList) + { + list.AddRange(testDataIList); + } + else if (testData is IEnumerable testDataIEnumerable) + { + list.AddRange(testDataIEnumerable); + } + else + { + throw new InvalidCastException("Unknown type of test data"); + } // Validate - _ = list.Capacity.Should().Be(20, "when capacity == 0, then we allocate as much memory as needed, only"); - _ = list.Should().HaveCount(testData.Length) - .And.ContainInConsecutiveOrder(testData) - .And.BeEquivalentTo(testData); + _ = list.Capacity.Should().BeGreaterThanOrEqualTo((int)itemsCount, "when capacity == 0, then we should allocate as much memory as needed, only"); + _ = list.Count.Should().Be((int)itemsCount); + _ = list.Should().ContainInConsecutiveOrder(testData); } - [Fact] - public void AddRangeShouldAddItemsInCorrectOrderWhenSourceIsList() + [Theory] + [MemberData(nameof(RecyclableListTestData.SourceDataVariants), MemberType = typeof(RecyclableListTestData))] + public void AddShouldAcceptDuplicates(string testCase, IEnumerable testData, long itemsCount) { - // Prepare - var testData = _testData.ToList(); - var list = new RecyclableArrayList(); - // Act - list.AddRange(testData); + using var list = new RecyclableArrayList((int)itemsCount * 2); + foreach (var index in testData) + { + list.Add(index); + } + + foreach (var index in testData) + { + list.Add(index); + } // Validate - _ = list.Capacity.Should().Be(20, "when capacity == 0, then we allocate as much memory as needed, only"); - _ = list.Should().HaveCount(testData.Count) - .And.ContainInConsecutiveOrder(testData) - .And.BeEquivalentTo(testData); + _ = list.Count.Should().Be((int)itemsCount * 2); + _ = list.Should().ContainInConsecutiveOrder(testData.Concat(testData)); } - [Fact] - public void ConstructorShouldAddItemsInCorrectOrderWhenSourceIsIEnumerable() + [Theory] + [MemberData(nameof(RecyclableListTestData.SourceDataVariants), MemberType = typeof(RecyclableListTestData))] + public void ClearShouldRemoveAllItems(string testCase, IEnumerable testData, long itemsCount) { // Prepare - var testData = _testData; + var list = new RecyclableArrayList(testData); + _ = list.Count.Should().Be((int)itemsCount); + // Act + list.Clear(); + + // Validate + _ = list.Count.Should().Be(0); + _ = list.Should().BeEmpty(); + + } + + [Theory] + [MemberData(nameof(RecyclableListTestData.SourceDataVariants), MemberType = typeof(RecyclableListTestData))] + public void ConstructorShouldAcceptDuplicates(string testCase, IEnumerable testData, long itemsCount) + { // Act - var list = new RecyclableArrayList(testData); + using var list = new RecyclableArrayList(testData.Concat(testData), initialCapacity: (int)itemsCount * 2); // Validate - _ = list.Capacity.Should().Be(32, "when we succeed in taking count without enumerating, then we allocate as much memory as needed, only"); - _ = list.Should().HaveCount(testData.Count()) - .And.ContainInConsecutiveOrder(testData) - .And.BeEquivalentTo(testData); + _ = list.Count.Should().Be((int)itemsCount * 2); + _ = list.Should().ContainInConsecutiveOrder(testData.Concat(testData)); + } + + [Theory] + [MemberData(nameof(RecyclableListTestData.SourceDataVariants), MemberType = typeof(RecyclableListTestData))] + public void ConstructorShouldAddItemsInCorrectOrderWhenSourceIsIEnumerable(string testCase, IEnumerable testData, long itemsCount) + { + // Act + var list = new RecyclableArrayList(testData); + + // Validate + _ = list.Capacity.Should().BeGreaterThanOrEqualTo((int)itemsCount, "when we succeed in taking count without enumerating, then we allocate as much memory as needed, only"); + _ = list.Count.Should().Be((int)itemsCount); + _ = list.Should().ContainInConsecutiveOrder(testData); + } + + [Theory] + [MemberData(nameof(RecyclableListTestData.SourceDataVariants), MemberType = typeof(RecyclableListTestData))] + public void ConstructorSourceShouldInitializeList(string testCase, IEnumerable testData, long itemsCount) + { + // Act + var list = new RecyclableArrayList(testData); + + // Validate + _ = list.Count.Should().Be((int)itemsCount); + _ = list.Should().ContainInConsecutiveOrder(testData); + } + + [Theory] + [MemberData(nameof(RecyclableListTestData.SourceDataVariants), MemberType = typeof(RecyclableListTestData))] + public void ContainsShouldFindAllItems(string testCase, IEnumerable testData, long itemsCount) + { + if (itemsCount == 0) + { + return; + } + + // Prepare + using var list = new RecyclableArrayList(testData); + _ = testData.Any().Should().BeTrue("we need items on the list that we can look for"); + + // Act + foreach (var item in testData) + { + // Validate + _ = list.Contains(item).Should().BeTrue(); + } } [Fact] @@ -203,21 +291,6 @@ namespace Recyclable.CollectionsTests _ = list.Capacity.Should().Be(20); } - [Fact] - public void ConstructorSourceShouldInitializeList() - { - // Prepare - var testData = _testData.ToArray(); - - // Act - var list = new RecyclableArrayList(testData); - - // Validate - _ = list.Should().HaveCount(testData.Length) - .And.ContainInConsecutiveOrder(testData) - .And.BeEquivalentTo(testData); - } - [Fact] public void EnumerateShouldYieldAllItemsInCorrectOrder() { @@ -254,22 +327,6 @@ namespace Recyclable.CollectionsTests .And.ContainInConsecutiveOrder(testData) .And.BeEquivalentTo(testData); } - [Fact] - public void ClearShouldRemoveAllItems() - { - // Prepare - var testData = _testData.ToArray(); - var list = new RecyclableArrayList(testData); - _ = list.Count.Should().NotBe(0); - - // Act - list.Clear(); - - // Validate - _ = list.Count.Should().Be(0); - _ = list.Should().HaveCount(0); - - } [Fact] public void IndexOfShouldFindTheIndexes() @@ -286,22 +343,6 @@ namespace Recyclable.CollectionsTests } } - [Fact] - public void ContainsShouldFindAllItems() - { - // Prepare - var testData = _testData.ToArray(); - using var list = new RecyclableArrayList(testData); - _ = testData.Any().Should().BeTrue("we need items on the list that we can look for"); - - // Act - foreach (var item in testData) - { - // Validate - _ = list.Contains(item).Should().BeTrue(); - } - } - [Fact] public void ContainsShouldNotFindNonExistingItems() { @@ -333,36 +374,5 @@ namespace Recyclable.CollectionsTests _ = list.Contains(1).Should().BeTrue(); } - [Fact] - public void AddShouldAcceptDuplicates() - { - // Prepare - int[] testNumbers = new[] { 1, 2, 2, 3 }; - - // Act - using var list = new RecyclableArrayList(); - foreach (var index in testNumbers) - { - list.Add(index); - } - - // Validate - _ = list.Count.Should().Be(testNumbers.Length); - _ = list.Should().BeEquivalentTo(testNumbers); - } - - [Fact] - public void ConstructorShouldAcceptDuplicates() - { - // Prepare - int[] testNumbers = new[] { 1, 2, 2, 3 }; - - // Act - using var list = new RecyclableArrayList(testNumbers); - - // Validate - _ = list.Count.Should().Be(testNumbers.Length); - _ = list.Should().BeEquivalentTo(testNumbers); - } - } - } \ No newline at end of file + } +} \ No newline at end of file