diff --git a/Recyclable.Collections/RecyclableLongList.InsertRange.cs b/Recyclable.Collections/RecyclableLongList.InsertRange.cs new file mode 100644 index 0000000..25093fc --- /dev/null +++ b/Recyclable.Collections/RecyclableLongList.InsertRange.cs @@ -0,0 +1,204 @@ +using System.Collections; +using System.Numerics; +using System.Runtime.CompilerServices; +using Recyclable.Collections.Pools; + +namespace Recyclable.Collections +{ +#pragma warning disable IDE1006 // This is intentional naming for the class to show up at the end of IntelliSense list. + public static class zRecyclableLongListInsertRange +#pragma warning restore IDE1006 + { + [MethodImpl(MethodImplOptions.AggressiveOptimization)] + public static void InsertRange(this RecyclableLongList list, long startingItemIndex, T[] items) + { + if (items.Length == 0) + { + return; + } + + T[][] oldMemoryBlocks = list._memoryBlocks; + Span oldMemoryBlocksSpan = new(oldMemoryBlocks); + int newItemIndex = (int)(startingItemIndex & list._blockSizeMinus1), + sourceBlockIndex = (int)(startingItemIndex >> list._blockSizePow2BitShift); + + long newCapacity = list._longCount + items.LongLength; + list._capacity = (int)BitOperations.RoundUpToPowerOf2((ulong)newCapacity); + + // This is "requiredCapacity". We're reusing blockSize var to save on the total no. of variables. + int blockSize = (int)(list._capacity >> list._blockSizePow2BitShift) + 1; + T[][] newMemoryBlocks = blockSize >= RecyclableDefaults.MinPooledArrayLength ? RecyclableArrayPool.RentShared(blockSize) : new T[blockSize][]; + + blockSize = list._blockSize; + + // Copy references to memory block arrays which stay as-is + Array.Copy(oldMemoryBlocks, newMemoryBlocks, sourceBlockIndex); + + // Prepare the first block for items in the new memory blocks + bool usePool = blockSize >= RecyclableDefaults.MinPooledArrayLength; + Span newMemoryBlocksSpan = new(newMemoryBlocks); + + int newBlockIndex = sourceBlockIndex;// + (newItemIndex + 1 == 0 ? 1 : 0); + newMemoryBlocksSpan[newBlockIndex] = usePool ? RecyclableArrayPool.RentShared(blockSize) : new T[blockSize]; + + // Copy the starting items from the list in the current block, but to a new block + long copiedCount; + if (newItemIndex > 0) + { + Array.Copy(oldMemoryBlocksSpan[sourceBlockIndex], newMemoryBlocks[newBlockIndex], newItemIndex); + copiedCount = newItemIndex; + } + else + { + copiedCount = 0; + } + + // Add new items to the list + ReadOnlySpan itemsSpan = items; + Span newBlockArraySpan = new(newMemoryBlocksSpan[newBlockIndex], newItemIndex, blockSize - newItemIndex); + + //sourceItemIndex = 0; + if (itemsSpan.Length >= newBlockArraySpan.Length) + { + newItemIndex = 0; + if (usePool) + { + do + { + itemsSpan[..newBlockArraySpan.Length].CopyTo(newBlockArraySpan); + itemsSpan = itemsSpan[newBlockArraySpan.Length..]; + newBlockIndex++; + if (itemsSpan.Length == 0) + { + break; + } + + newBlockArraySpan = newMemoryBlocksSpan[newBlockIndex] = RecyclableArrayPool.RentShared(blockSize); + } while (itemsSpan.Length >= blockSize); + } + else + { + do + { + itemsSpan[..newBlockArraySpan.Length].CopyTo(newBlockArraySpan); + itemsSpan = itemsSpan[newBlockArraySpan.Length..]; + newBlockIndex++; + if (itemsSpan.Length == 0) + { + break; + } + + newBlockArraySpan = newMemoryBlocksSpan[newBlockIndex] = new T[blockSize]; + } while (itemsSpan.Length >= blockSize) ; + } + } + + // Copy the rest of the new items to the current block, if any + if (itemsSpan.Length > 0) + { + itemsSpan.CopyTo(newBlockArraySpan); + newItemIndex += itemsSpan.Length; + } + else + { + newItemIndex = 0; + newMemoryBlocksSpan[newBlockIndex] = usePool ? RecyclableArrayPool.RentShared(blockSize) : new T[blockSize]; + } + + long itemsCount = list._longCount - startingItemIndex; + if (itemsCount > 0) + { + // Copy the rest of items from the list after the new items + int toCopy; + itemsSpan = new(oldMemoryBlocksSpan[sourceBlockIndex], (int)copiedCount, (int)(blockSize - copiedCount)); + copiedCount = 0; + newBlockArraySpan = new(newMemoryBlocksSpan[newBlockIndex], newItemIndex, blockSize - newItemIndex); + + while (copiedCount < itemsCount) + { + toCopy = (int)Math.Min(itemsCount - copiedCount, itemsSpan.Length); + if (newBlockArraySpan.Length < toCopy) + { + toCopy = newBlockArraySpan.Length; + itemsSpan[..toCopy].CopyTo(newBlockArraySpan); + newBlockIndex++; + newBlockArraySpan = newMemoryBlocksSpan[newBlockIndex] = usePool ? RecyclableArrayPool.RentShared(blockSize) : new T[blockSize]; + itemsSpan = itemsSpan[toCopy..]; + copiedCount += toCopy; + } + else + { + itemsSpan[..toCopy].CopyTo(newBlockArraySpan); + newBlockArraySpan = newBlockArraySpan[toCopy..]; + itemsSpan = itemsSpan[toCopy..]; + if (itemsSpan.Length == 0 && sourceBlockIndex + 1 < oldMemoryBlocksSpan.Length) + { + sourceBlockIndex++; + itemsSpan = oldMemoryBlocksSpan[sourceBlockIndex]; + } + + copiedCount += toCopy; + if (newBlockArraySpan.Length == 0 && copiedCount < itemsCount) + { + newBlockIndex++; + newBlockArraySpan = newMemoryBlocksSpan[newBlockIndex] = usePool ? RecyclableArrayPool.RentShared(blockSize) : new T[blockSize]; + } + } + } + } + + list._lastBlockWithData = newBlockIndex; + if ((newCapacity & list._blockSizeMinus1) == 0) + { + list._nextItemIndex = 0; + list._nextItemBlockIndex = newBlockIndex + 1; + } + else + { + list._nextItemIndex = (int)(newCapacity & list._blockSizeMinus1); + list._nextItemBlockIndex = newBlockIndex; + } + + list._memoryBlocks = newMemoryBlocks; + list._longCount = newCapacity; + list._version++; + + // Return unused memory block arrays to the pool + if (usePool) + { + bool needsClearing = RecyclableLongList._needsClearing; + while (sourceBlockIndex < oldMemoryBlocksSpan.Length) + { + RecyclableArrayPool.ReturnShared(oldMemoryBlocksSpan[sourceBlockIndex++], needsClearing); + } + } + + // Allocate unused memory block arrays up to the capacity + newBlockIndex++; + if (usePool) + { + while (newBlockIndex < newMemoryBlocksSpan.Length) + { + newMemoryBlocksSpan[newBlockIndex++] = RecyclableArrayPool.RentShared(blockSize); + } + } + else + { + while (newBlockIndex < newMemoryBlocksSpan.Length) + { + newMemoryBlocksSpan[newBlockIndex++] = new T[blockSize]; + } + } + + if (oldMemoryBlocksSpan.Length >= RecyclableDefaults.MinPooledArrayLength) + { + RecyclableArrayPool.ReturnShared(oldMemoryBlocks, true); + } + } + + [MethodImpl(MethodImplOptions.AggressiveOptimization)] + public static void InsertRange(this RecyclableLongList list, long startingItemIndex, ICollection items) + { + } + } +} diff --git a/Recyclable.CollectionsTests/RecyclableLongList.Helpers.Tests.cs b/Recyclable.CollectionsTests/RecyclableLongList.Helpers.Tests.cs new file mode 100644 index 0000000..1626df2 --- /dev/null +++ b/Recyclable.CollectionsTests/RecyclableLongList.Helpers.Tests.cs @@ -0,0 +1,100 @@ +using FluentAssertions; +using Recyclable.Collections; +using Recyclable.Collections.TestData.xUnit; +using System.Collections; + +namespace Recyclable.CollectionsTests +{ + + public class RecyclableLongListHelperTests + { + [Theory] + [ClassData(typeof(SourceDataWithBlockSizeWithItemIndexWithRangeTheoryData))] + public void MakeRoomAndSetShouldMoveItems(string testCase, IEnumerable testData, int itemsCount, int minBlockSize, in (long ItemIndex, long RangedItemsCount)[] itemRanges) + { + // Prepare + var expectedData = testData.Reverse().ToArray(); + + foreach ((var itemIndex, var rangedItemsCount) in itemRanges) + { + using var list = new RecyclableLongList(testData.Reverse(), minBlockSize, itemsCount); + var expected = expectedData.ToList(); + var rangedTestData = expected.GetRange((int)itemIndex, (int)rangedItemsCount).ToArray(); + expected.InsertRange((int)itemIndex, rangedTestData); + + if (testCase.Contains("Array[", StringComparison.OrdinalIgnoreCase)) + { + list.InsertRange((int)itemIndex, rangedTestData); + } + else if (testCase.Contains("ICollection[", StringComparison.OrdinalIgnoreCase)) + { + return; + //list.InsertRange((int)itemIndex, (ICollection)testData); + } + else if (testCase.Contains("ICollection[", StringComparison.OrdinalIgnoreCase)) + { + return; + //list.InsertRange((int)itemIndex, (ICollection)testData); + } + else if (testCase.Contains("IEnumerable[", StringComparison.OrdinalIgnoreCase)) + { + return; + //list.InsertRange((int)itemIndex, (IEnumerable)testData); + } + else if (testCase.Contains("IReadOnlyList[", StringComparison.OrdinalIgnoreCase)) + { + return; + //list.InsertRange((int)itemIndex, (IReadOnlyList)testData); + } + else if (testCase.Contains("ReadOnlySpan[", StringComparison.OrdinalIgnoreCase)) + { + return; + //list.InsertRange((int)itemIndex, new ReadOnlySpan((long[])testData)); + } + else if (testCase.Contains("Span[", StringComparison.OrdinalIgnoreCase)) + { + return; + //list.InsertRange((int)itemIndex, new Span((long[])testData)); + } + else if (testData is long[] testDataArray) + { + return; + //list.InsertRange((int)itemIndex, testDataArray); + } + else if (testData is List testDataList) + { + return; + //list.InsertRange((int)itemIndex, testDataList); + } + else if (testData is RecyclableList testDataRecyclableList) + { + return; + //list.InsertRange((int)itemIndex, testDataRecyclableList); + } + else if (testData is RecyclableLongList testDataRecyclableLongList) + { + return; + //list.InsertRange((int)itemIndex, testDataRecyclableLongList); + } + else if (testData is IList testDataIList) + { + return; + //list.InsertRange((int)itemIndex, testDataIList); + } + else if (testData is IEnumerable testDataIEnumerable) + { + return; + //list.InsertRange((int)itemIndex, testDataIEnumerable); + } + else + { + throw new InvalidCastException("Unknown type of test data"); + } + + // Validate + list.Count.Should().Be(expected.Count); + _ = list.Should().Equal(expected); + } + } + } +}