diff --git a/Recyclable.Collections/RecyclableArrayList.cs b/Recyclable.Collections/RecyclableArrayList.cs index 581916f..21bd7ce 100644 --- a/Recyclable.Collections/RecyclableArrayList.cs +++ b/Recyclable.Collections/RecyclableArrayList.cs @@ -293,6 +293,8 @@ namespace Recyclable.Collections _count = targetItemIdx; } + public T[] AsArray() => _memoryBlock; + public void Clear() { _count = 0; diff --git a/Recyclable.Collections/RecyclableList.cs b/Recyclable.Collections/RecyclableList.cs index cb8b308..dafb764 100644 --- a/Recyclable.Collections/RecyclableList.cs +++ b/Recyclable.Collections/RecyclableList.cs @@ -274,39 +274,143 @@ namespace Recyclable.Collections public void AddRange(in T[] items) { - long sourceItemsCount = items.LongLength; - long oldLongCount = _longCount; - long targetCapacity = oldLongCount + sourceItemsCount; + long targetCapacity = _longCount + items.LongLength; if (_capacity < targetCapacity) { _ = EnsureCapacity(targetCapacity); } - int blockSize = _blockSize; - int targetItemIdx = (int)(oldLongCount % blockSize); - int targetBlockIdx = (int)(oldLongCount / blockSize) + (targetItemIdx > 0 ? 1 : 0); + int blockSize = _blockSize, targetBlockIndex = _lastBlockIndex, toCopy = 0; + Span itemsSpan = new(items); Span memoryBlocksSpan = new(_memoryBlocks); - int memoryBlocksCount = memoryBlocksSpan.Length; - Span targetBlockSpan = new(memoryBlocksSpan[targetBlockIdx]); - for (var i = 0L; i < sourceItemsCount; i++) + Span targetBlockArraySpan = new(memoryBlocksSpan[targetBlockIndex], _nextItemIndex, blockSize - _nextItemIndex); + while (!itemsSpan.IsEmpty) { - targetBlockSpan[targetItemIdx++] = items[i]; - if (targetItemIdx == blockSize) + if (targetBlockArraySpan.Length < itemsSpan.Length) { - targetBlockIdx++; - if (targetBlockIdx == memoryBlocksCount) + toCopy = targetBlockArraySpan.Length; + itemsSpan[..toCopy].CopyTo(targetBlockArraySpan); + targetBlockIndex++; + targetBlockArraySpan = new(memoryBlocksSpan[targetBlockIndex]); + } + else + { + toCopy = itemsSpan.Length; + itemsSpan[..toCopy].CopyTo(targetBlockArraySpan); + targetBlockArraySpan = targetBlockArraySpan[..toCopy]; + } + + itemsSpan = itemsSpan[toCopy..]; + } + + _nextItemIndex = toCopy; + _longCount = targetCapacity; + _lastBlockIndex = targetBlockIndex; + } + + public void AddRange(in RecyclableArrayList items) + { + long targetCapacity = _longCount + items.Count; + if (_capacity < targetCapacity) + { + _ = EnsureCapacity(targetCapacity); + } + + int blockSize = _blockSize, targetBlockIndex = _lastBlockIndex, toCopy = 0; + Span itemsSpan = new(items.AsArray()); + Span memoryBlocksSpan = new(_memoryBlocks); + Span targetBlockArraySpan = new(memoryBlocksSpan[targetBlockIndex], _nextItemIndex, blockSize - _nextItemIndex); + while (!itemsSpan.IsEmpty) + { + if (targetBlockArraySpan.Length < itemsSpan.Length) + { + toCopy = targetBlockArraySpan.Length; + itemsSpan[..toCopy].CopyTo(targetBlockArraySpan); + targetBlockIndex++; + targetBlockArraySpan = new(memoryBlocksSpan[targetBlockIndex]); + } + else + { + toCopy = itemsSpan.Length; + itemsSpan[..toCopy].CopyTo(targetBlockArraySpan); + targetBlockArraySpan = targetBlockArraySpan[..toCopy]; + } + + itemsSpan = itemsSpan[toCopy..]; + } + + _nextItemIndex = toCopy; + _longCount = targetCapacity; + _lastBlockIndex = targetBlockIndex; + + } + + public void AddRange(in RecyclableList items) + { + long itemsCount = items.LongCount; + long targetCapacity = _longCount + itemsCount; + if (_capacity < targetCapacity) + { + _ = EnsureCapacity(targetCapacity); + } + + int blockSize = _blockSize, + sourceBlockIndex = 0, + targetBlockIndex = _lastBlockIndex, + toCopy = 0; + + Span sourceMemoryBlocksSpan = new(items._memoryBlocks); + Span itemsSpan = new(sourceMemoryBlocksSpan[sourceBlockIndex], 0, items._blockSize); + Span targetMemoryBlocksSpan = new(_memoryBlocks); + Span targetBlockArraySpan = new(targetMemoryBlocksSpan[targetBlockIndex], _nextItemIndex, blockSize - _nextItemIndex); + long copiedCount = 0; + while (copiedCount < itemsCount) + { + toCopy = (int)Math.Min(itemsCount - copiedCount, itemsSpan.Length); + if (targetBlockArraySpan.Length < toCopy) + { + toCopy = targetBlockArraySpan.Length; + itemsSpan[..toCopy].CopyTo(targetBlockArraySpan); + targetBlockIndex++; + targetBlockArraySpan = new(targetMemoryBlocksSpan[targetBlockIndex], 0, blockSize); + itemsSpan = itemsSpan[toCopy..]; + copiedCount += toCopy; + } + else + { + itemsSpan[..toCopy].CopyTo(targetBlockArraySpan); + targetBlockArraySpan = targetBlockArraySpan[toCopy..]; + itemsSpan = itemsSpan[toCopy..]; + if (itemsSpan.IsEmpty && sourceBlockIndex + 1 < sourceMemoryBlocksSpan.Length) { - break; + sourceBlockIndex++; + itemsSpan = new(sourceMemoryBlocksSpan[sourceBlockIndex], 0, items._blockSize); } - targetBlockSpan = new Span(memoryBlocksSpan[targetBlockIdx]); - targetItemIdx = 0; + copiedCount += toCopy; + if (targetBlockArraySpan.IsEmpty && copiedCount < itemsCount) + { + targetBlockIndex++; + targetBlockArraySpan = new(targetMemoryBlocksSpan[targetBlockIndex]); + + } } } + if (targetCapacity % blockSize == 0) + { + _nextItemIndex = 0; + _lastBlockIndex = targetBlockIndex + 1; + } + else + { + _nextItemIndex = (int)(targetCapacity % blockSize); + _lastBlockIndex = targetBlockIndex; + + } + _longCount = targetCapacity; - _lastBlockIndex = targetBlockIdx; - _nextItemIndex = targetItemIdx; + } public void AddRange(in List items) @@ -324,11 +428,12 @@ namespace Recyclable.Collections int targetBlockIdx = (int)(oldLongCount / blockSize) + (targetItemIdx > 0 ? 1 : 0); Span memoryBlocksSpan = new(_memoryBlocks); var memoryBlocksCount = memoryBlocksSpan.Length; - Span targetBlockSpan = new(memoryBlocksSpan[targetBlockIdx]); + Span targetBlockSpan = new(memoryBlocksSpan[targetBlockIdx], 0, blockSize); + for (int i = 0; i < sourceItemsCount; i++) { - targetBlockSpan[targetItemIdx++] = items[i]; - if (targetItemIdx == blockSize) + targetBlockSpan[targetItemIdx] = items[i]; + if (targetItemIdx + 1 == blockSize) { targetBlockIdx++; if (targetBlockIdx == memoryBlocksCount) @@ -336,9 +441,13 @@ namespace Recyclable.Collections break; } - targetBlockSpan = new Span(memoryBlocksSpan[targetBlockIdx]); + targetBlockSpan = new Span(memoryBlocksSpan[targetBlockIdx], 0, blockSize); targetItemIdx = 0; } + else + { + targetItemIdx++; + } } _longCount = targetCapacity; @@ -383,7 +492,7 @@ namespace Recyclable.Collections _nextItemIndex = targetItemIdx; } - private void AddRange(IEnumerable source, int growByCount = RecyclableDefaults.MinPooledArrayLength) + public void AddRange(IEnumerable source, int growByCount = RecyclableDefaults.MinPooledArrayLength) { if (source is T[] sourceArray) { @@ -403,6 +512,12 @@ namespace Recyclable.Collections return; } + if (source is RecyclableList sourceRecyclableList) + { + AddRange(sourceRecyclableList); + return; + } + if (source is IList sourceIList) { AddRange(sourceIList); diff --git a/Recyclable.CollectionsTests/RecyclableListTests.cs b/Recyclable.CollectionsTests/RecyclableListTests.cs index a407527..8258ea0 100644 --- a/Recyclable.CollectionsTests/RecyclableListTests.cs +++ b/Recyclable.CollectionsTests/RecyclableListTests.cs @@ -50,6 +50,25 @@ namespace Recyclable.CollectionsTests _ = list.Should().BeEquivalentTo(testNumbers); } + [Fact] + public void AddShouldAddItemWhenAfterAddRange() + { + // Prepare + int[] testNumbers = new[] { 1, 2, 2, 3 }; + + // Act + using var list = new RecyclableList(); + list.AddRange(_testData); + foreach (var index in testNumbers) + { + list.Add(index); + } + + // Validate + _ = list.LongCount.Should().Be(_totalObjectCount + testNumbers.Length); + _ = list.Should().BeEquivalentTo(_testData.Concat(testNumbers)); + } + [Fact] public void ConstructorShouldAcceptDuplicates() { @@ -300,7 +319,7 @@ namespace Recyclable.CollectionsTests { // Prepare var testData = _testData.ToArray(); - var list = new RecyclableList(); + using var list = new RecyclableList(); // Act @@ -309,8 +328,8 @@ namespace Recyclable.CollectionsTests // Validate _ = list.Capacity.Should().Be(114_688, "when capacity == 0, then we allocate as much memory as needed, only"); _ = list.Should().HaveCount(testData.Length) - .And.ContainInConsecutiveOrder(testData) - .And.BeEquivalentTo(testData); + .And.ContainInConsecutiveOrder(testData) + .And.BeEquivalentTo(testData); } [Fact] @@ -318,7 +337,7 @@ namespace Recyclable.CollectionsTests { // Prepare var testData = _testData.ToList(); - var list = new RecyclableList(); + using var list = new RecyclableList(); // Act list.AddRange(testData); @@ -330,6 +349,61 @@ namespace Recyclable.CollectionsTests .And.BeEquivalentTo(testData); } + [Fact] + public void AddRangeShouldAddItemsInCorrectOrderWhenSourceIsRecyclableList() + { + // Prepare + var testData = _testData.ToRecyclableList(); + using var list = new RecyclableList(); + + // Act + list.AddRange(testData); + + // Validate + _ = list.Capacity.Should().Be(114_688, "when capacity == 0, then we allocate as much memory as needed, only"); + _ = list.Should().HaveCount(testData.Count) + .And.ContainInConsecutiveOrder(testData) + .And.BeEquivalentTo(testData); + } + + [Fact] + public void AddRangeShouldNotOverrideItemsWhenSourceIsArray() + { + // Prepare + var testData = _testData.ToArray(); + var list = new RecyclableList(); + + // Act + list.AddRange(testData); + list.AddRange(testData); + + // Validate + var expectedData = testData.Concat(testData).ToArray(); + _ = list.Capacity.Should().Be(229376L, "when capacity == 0, then we allocate as much memory as needed, only"); + _ = list.Should().HaveCount(expectedData.Length) + .And.ContainInConsecutiveOrder(expectedData) + .And.BeEquivalentTo(expectedData); + } + + [Fact] + public void AddRangeShouldNotOverrideItemsWhenSourceIsRecyclableList() + { + // Prepare + using var testData = _testData.ToRecyclableList(); + using var list = new RecyclableList(); + + // Act + list.AddRange(testData); + list.AddRange(testData); + + // Validate + var expectedData = _testData.Concat(_testData).ToArray(); + _ = list.Capacity.Should().Be(229376L, "when capacity == 0, then we allocate as much memory as needed, only"); + _ = list.Should().HaveCount(expectedData.Length) + .And.ContainInConsecutiveOrder(expectedData) + .And.BeEquivalentTo(expectedData); + } + [Fact] public void InsertAtTheBeginningShouldMoveItems() {