From 90ae27ce5dd365677bcc1845c90254bf17d17235 Mon Sep 17 00:00:00 2001 From: mlemanczyk Date: Mon, 25 Dec 2023 16:34:20 +0100 Subject: [PATCH] Additional RecyclableList.IndexOf() & RecyclableLongList.IndexOf() variants moved from Recyclable.Collections.Compatibility.List package. Tests extended. --- Recyclable.Collections/MathUtils.cs | 9 + Recyclable.Collections/RecyclableList.cs | 10 + .../RecyclableLongList.IndexOfHelpers.cs | 292 +++++++++++++++++- Recyclable.Collections/RecyclableLongList.cs | 77 ++++- Recyclable.Collections/ThrowHelper.cs | 22 +- .../RecyclableListTests.cs | 40 +++ .../RecyclableLongListTests.cs | 134 ++++++++ 7 files changed, 576 insertions(+), 8 deletions(-) diff --git a/Recyclable.Collections/MathUtils.cs b/Recyclable.Collections/MathUtils.cs index c7e37f8..860552a 100644 --- a/Recyclable.Collections/MathUtils.cs +++ b/Recyclable.Collections/MathUtils.cs @@ -31,5 +31,14 @@ namespace Recyclable.Collections return result; } } + + public static long Min(long number1, long number2, long number3) => + number1 <= number2 + ? number1 <= number3 + ? number1 + : number3 + : number2 <= number3 + ? number2 + : number3; } } diff --git a/Recyclable.Collections/RecyclableList.cs b/Recyclable.Collections/RecyclableList.cs index 8c739f6..f5ce6db 100644 --- a/Recyclable.Collections/RecyclableList.cs +++ b/Recyclable.Collections/RecyclableList.cs @@ -244,6 +244,16 @@ namespace Recyclable.Collections ? Array.IndexOf(_memoryBlock, itemToFind, 0, _count) : RecyclableDefaults.ItemNotFoundIndex; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int IndexOf(T item, int index) => _count != 0 + ? Array.IndexOf(_memoryBlock, item, index, _count - index) + : RecyclableDefaults.ItemNotFoundIndex; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int IndexOf(T item, int index, int count) => _count != 0 + ? Array.IndexOf(_memoryBlock, item, index, count) + : RecyclableDefaults.ItemNotFoundIndex; + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] public void Insert(int index, T item) { diff --git a/Recyclable.Collections/RecyclableLongList.IndexOfHelpers.cs b/Recyclable.Collections/RecyclableLongList.IndexOfHelpers.cs index a669f3d..cd97d07 100644 --- a/Recyclable.Collections/RecyclableLongList.IndexOfHelpers.cs +++ b/Recyclable.Collections/RecyclableLongList.IndexOfHelpers.cs @@ -193,7 +193,7 @@ namespace Recyclable.Collections #endif [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] - public static long IndexOfParallel(RecyclableLongList list, T item) + public static long IndexOfParallel(RecyclableLongList list, T item) { // & WAS SLOWER //long itemsCount = Array.IndexOf(list._memoryBlocks[0], item, 0, (int)Math.Min(list._blockSize, list._longCount)); @@ -294,6 +294,183 @@ namespace Recyclable.Collections return RecyclableDefaults.ItemNotFoundIndexLong; } + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + public static long IndexOfParallel(RecyclableLongList list, T item, int firstBlockIndex) + { + IndexOfSynchronizationContext context = IndexOfSynchronizationContextPool.GetWithOneParticipant(); + + int blockIndex = firstBlockIndex, + lastBlockWithData = list._lastBlockWithData, + lastFullBlockWithData = lastBlockWithData - BlocksForEachTask; + + while (blockIndex < lastFullBlockWithData) + { + // At this point itemIndex is limited to blockSize range - int. + + // & WAS SLOWER + // _ = ThreadPool.QueueUserWorkItem((_) => + // Task.Factory.StartNew(() => + context.AddParticipant(); + var startingBlock = blockIndex; + _ = Task.Run(() => IndexOfInConsecutiveBlocks(context, list, startingBlock, item)); + + // & WAS SLOWER + // ScheduleIndexOfTask(context, list, blockIndex, item); + if (context._isItemFound) + { + break; + } + + blockIndex += BlocksForEachTask; + } + + if (!context._isItemFound) + { + while (blockIndex < lastBlockWithData) + { + // & WAS SLOWER + // _ = ThreadPool.QueueUserWorkItem((_) => + // Task.Factory.StartNew(() => + var startingBlock = blockIndex; + context.AddParticipant(); + _ = Task.Run(() => IndexOfIn1Block(context, list, startingBlock, item)); + + // & WAS SLOWER + // ScheduleIndexOfTaskSingle(context, list, blockIndex, item); + if (context._isItemFound) + { + break; + } + + blockIndex++; + } + } + + if (!context._isItemFound) + { + int itemIndex = list._nextItemIndex != 0 + ? Array.IndexOf(list._memoryBlocks[lastBlockWithData], item, 0, list._nextItemIndex) + : Array.IndexOf(list._memoryBlocks[lastBlockWithData], item, 0, list._blockSize); + + if (!context._isItemFound && itemIndex >= 0) + { + ExchangeFoundItemIndexIfLower(context, lastBlockWithData, ((long)lastBlockWithData << list._blockSizePow2BitShift) + itemIndex); + } + } + + context.SignalAndWait(); + if (context.Exception == null) + { + try + { + return context.FoundItemIndex; + } + finally + { + context.Dispose(); + } + } + + try + { + context.Exception.CaptureAndReThrow(); + } + finally + { + context.Dispose(); + } + + return RecyclableDefaults.ItemNotFoundIndexLong; + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + public static long IndexOfParallel(RecyclableLongList list, T item, int firstBlockIndex, long count) + { + IndexOfSynchronizationContext context = IndexOfSynchronizationContextPool.GetWithOneParticipant(); + + int blockIndex = firstBlockIndex, + lastBlockWithData = (int)Math.Min((count >> list._blockSizePow2BitShift) + firstBlockIndex + ((count & list._blockSizeMinus1) > 0 ? 1 : 0) - 1, list._lastBlockWithData), + lastFullBlockWithData = lastBlockWithData - BlocksForEachTask; + + while (blockIndex < lastFullBlockWithData) + { + // At this point itemIndex is limited to blockSize range - int. + + // & WAS SLOWER + // _ = ThreadPool.QueueUserWorkItem((_) => + // Task.Factory.StartNew(() => + context.AddParticipant(); + var startingBlock = blockIndex; + _ = Task.Run(() => IndexOfInConsecutiveBlocks(context, list, startingBlock, item)); + + // & WAS SLOWER + // ScheduleIndexOfTask(context, list, blockIndex, item); + if (context._isItemFound) + { + break; + } + + blockIndex += BlocksForEachTask; + } + + if (!context._isItemFound) + { + count -= (blockIndex - firstBlockIndex) << list._blockSizePow2BitShift; + while (blockIndex < lastBlockWithData) + { + // & WAS SLOWER + // _ = ThreadPool.QueueUserWorkItem((_) => + // Task.Factory.StartNew(() => + var startingBlock = blockIndex; + context.AddParticipant(); + _ = Task.Run(() => IndexOfIn1Block(context, list, startingBlock, item)); + + // & WAS SLOWER + // ScheduleIndexOfTaskSingle(context, list, blockIndex, item); + if (context._isItemFound) + { + break; + } + + blockIndex++; + count -= list._blockSize; + } + } + + if (!context._isItemFound && count > 0) + { + int itemIndex = Array.IndexOf(list._memoryBlocks[lastBlockWithData], item, 0, (int)count); + if (!context._isItemFound && itemIndex >= 0) + { + ExchangeFoundItemIndexIfLower(context, lastBlockWithData, ((long)lastBlockWithData << list._blockSizePow2BitShift) + itemIndex); + } + } + + context.SignalAndWait(); + if (context.Exception == null) + { + try + { + return context.FoundItemIndex; + } + finally + { + context.Dispose(); + } + } + + try + { + context.Exception.CaptureAndReThrow(); + } + finally + { + context.Dispose(); + } + + return RecyclableDefaults.ItemNotFoundIndexLong; + } + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] public static long IndexOfSequential(RecyclableLongList list, in T item) //, in T[][] memoryBlocks, int lastBlockIndex, int blockSize, int nextItemIndex) { @@ -344,6 +521,119 @@ namespace Recyclable.Collections return itemIndex >= 0 ? itemIndex + ((long)list._lastBlockWithData << list._blockSizePow2BitShift) : RecyclableDefaults.ItemNotFoundIndexLong; } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + public static long IndexOfSequential(RecyclableLongList list, in T item, int firstBlockIndex, long count) + { + int blockIndex, + blockSize = list._blockSize, + itemIndex, + lastBlockWithData = (int)Math.Min((count >> list._blockSizePow2BitShift) + firstBlockIndex + ((count & list._blockSizeMinus1) > 0 ? 1 : 0) - 1, list._lastBlockWithData); + + byte blockSizePow2BitShift = list._blockSizePow2BitShift; + + ReadOnlySpan memoryBlocksSpan = new(list._memoryBlocks, 0, lastBlockWithData + 1); + for (blockIndex = firstBlockIndex; blockIndex < lastBlockWithData && count > blockSize; blockIndex += 4) + { + + itemIndex = Array.IndexOf(memoryBlocksSpan[blockIndex], item, 0, blockSize); + if (itemIndex >= 0) + { + return itemIndex + ((long)blockIndex << blockSizePow2BitShift); + } + + count -= blockSize; + if (blockIndex + 1 < lastBlockWithData && count > blockSize) + { + itemIndex = Array.IndexOf(memoryBlocksSpan[blockIndex + 1], item, 0, blockSize); + if (itemIndex >= 0) + { + return itemIndex + ((long)(blockIndex + 1) << blockSizePow2BitShift); + } + + count -= blockSize; + if (blockIndex + 2 < lastBlockWithData && count > blockSize) + { + itemIndex = Array.IndexOf(memoryBlocksSpan[blockIndex + 2], item, 0, blockSize); + if (itemIndex >= 0) + { + return itemIndex + ((long)(blockIndex + 2) << blockSizePow2BitShift); + } + + count -= blockSize; + if (blockIndex + 3 < lastBlockWithData && count > blockSize) + { + itemIndex = Array.IndexOf(memoryBlocksSpan[blockIndex + 3], item, 0, blockSize); + if (itemIndex >= 0) + { + return itemIndex + ((long)(blockIndex + 3) << blockSizePow2BitShift); + } + + count -= blockSize; + } + } + } + } + + itemIndex = Array.IndexOf(memoryBlocksSpan[lastBlockWithData], item, 0, (int)count); + + return itemIndex >= 0 ? itemIndex + ((long)lastBlockWithData << blockSizePow2BitShift) : RecyclableDefaults.ItemNotFoundIndexLong; + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + public static long IndexOfSequential(RecyclableLongList list, in T item, int firstBlockIndex) + { + int blockIndex, + blockSize = list._blockSize, + itemIndex, + lastBlockWithData = list._lastBlockWithData; + + byte blockSizePow2BitShift = list._blockSizePow2BitShift; + + ReadOnlySpan memoryBlocksSpan = new(list._memoryBlocks, 0, lastBlockWithData + 1); + for (blockIndex = firstBlockIndex; blockIndex < lastBlockWithData; blockIndex += 4) + { + + itemIndex = Array.IndexOf(memoryBlocksSpan[blockIndex], item, 0, blockSize); + if (itemIndex >= 0) + { + return itemIndex + ((long)blockIndex << blockSizePow2BitShift); + } + + if (blockIndex + 1 < lastBlockWithData) + { + itemIndex = Array.IndexOf(memoryBlocksSpan[blockIndex + 1], item, 0, blockSize); + if (itemIndex >= 0) + { + return itemIndex + ((long)(blockIndex + 1) << blockSizePow2BitShift); + } + + if (blockIndex + 2 < lastBlockWithData) + { + itemIndex = Array.IndexOf(memoryBlocksSpan[blockIndex + 2], item, 0, blockSize); + if (itemIndex >= 0) + { + return itemIndex + ((long)(blockIndex + 2) << blockSizePow2BitShift); + } + + if (blockIndex + 3 < lastBlockWithData) + { + itemIndex = Array.IndexOf(memoryBlocksSpan[blockIndex + 3], item, 0, blockSize); + if (itemIndex >= 0) + { + return itemIndex + ((long)(blockIndex + 3) << blockSizePow2BitShift); + } + } + } + } + } + + itemIndex = list._nextItemIndex != 0 + ? Array.IndexOf(memoryBlocksSpan[lastBlockWithData], item, 0, list._nextItemIndex) + : Array.IndexOf(memoryBlocksSpan[lastBlockWithData], item, 0, list._blockSize); + + return itemIndex >= 0 ? itemIndex + ((long)lastBlockWithData << blockSizePow2BitShift) : RecyclableDefaults.ItemNotFoundIndexLong; + } } } } diff --git a/Recyclable.Collections/RecyclableLongList.cs b/Recyclable.Collections/RecyclableLongList.cs index a2daf6d..9047d9b 100644 --- a/Recyclable.Collections/RecyclableLongList.cs +++ b/Recyclable.Collections/RecyclableLongList.cs @@ -374,7 +374,7 @@ namespace Recyclable.Collections return RecyclableDefaults.ItemNotFoundIndex; } - int itemIndex = Array.IndexOf(_memoryBlocks[0], item, 0, _longCount < _blockSize ? _nextItemIndex : _blockSize); + int itemIndex = Array.IndexOf(_memoryBlocks[0], item, 0, _blockSize); if (itemIndex >= 0) { return itemIndex; @@ -397,6 +397,81 @@ namespace Recyclable.Collections } } + public int IndexOf(T item, int index) + { + if (_lastBlockWithData == 0) + { + return Array.IndexOf(_memoryBlocks[0], item, index, _longCount < _blockSize ? _nextItemIndex : _blockSize); + } + else if (_longCount == 0) + { + return RecyclableDefaults.ItemNotFoundIndex; + } + + int firstBlockIndex = index >> _blockSizePow2BitShift; + int firstItemIndex = index & _blockSizeMinus1; + int itemIndex = Array.IndexOf(_memoryBlocks[firstBlockIndex], item, firstItemIndex, (firstBlockIndex == _lastBlockWithData ? _nextItemIndex : _blockSize) - firstItemIndex); + if (itemIndex >= 0) + { + return (firstBlockIndex << _blockSizePow2BitShift) + itemIndex; + } + else if (_longCount < RecyclableDefaults.MinItemsCountForParallelization) + { + return (int)RecyclableLongList.IndexOfHelpers.IndexOfSequential(this, item, firstBlockIndex + 1); + } + else + { + if (firstBlockIndex + 1 <= _lastBlockWithData) + { + itemIndex = Array.IndexOf(_memoryBlocks[firstBlockIndex + 1], item, 0, (int)Math.Min(firstBlockIndex + 1 == _lastBlockWithData ? _nextItemIndex : _blockSize, _longCount - _blockSize)); + return itemIndex >= 0 ? ((firstBlockIndex + 1) << _blockSizePow2BitShift) + itemIndex : checked((int)RecyclableLongList.IndexOfHelpers.IndexOfParallel(this, item, firstBlockIndex + 2)); + } + else + { + return RecyclableDefaults.ItemNotFoundIndex; + } + } + } + + public int IndexOf(T item, int index, int count) + { + if (_lastBlockWithData == 0) + { + return Array.IndexOf(_memoryBlocks[0], item, index, count); + } + else if (_longCount == 0 || count == 0) + { + return RecyclableDefaults.ItemNotFoundIndex; + } + + int blockSize = _blockSize, + firstBlockIndex = index >> _blockSizePow2BitShift, + firstItemIndex = index & _blockSizeMinus1; + + int firstBlockItemsCount = Math.Min(blockSize - firstItemIndex, count); + int itemIndex = Array.IndexOf(_memoryBlocks[firstBlockIndex], item, firstItemIndex, firstBlockItemsCount); + if (itemIndex >= 0) + { + return (firstBlockIndex << _blockSizePow2BitShift) + itemIndex; + } + else if (_longCount < RecyclableDefaults.MinItemsCountForParallelization) + { + return (int)RecyclableLongList.IndexOfHelpers.IndexOfSequential(this, item, firstBlockIndex + 1, count - firstBlockItemsCount); + } + else + { + if (_longCount > blockSize) + { + itemIndex = Array.IndexOf(_memoryBlocks[firstBlockIndex + 1], item, 0, (int)MathUtils.Min(blockSize, _longCount - blockSize, count - firstBlockItemsCount)); + return itemIndex >= 0 ? ((firstBlockIndex + 1) << _blockSizePow2BitShift) + itemIndex : checked((int)RecyclableLongList.IndexOfHelpers.IndexOfParallel(this, item, firstBlockIndex + 2, count - firstBlockItemsCount - blockSize)); + } + else + { + return RecyclableDefaults.ItemNotFoundIndex; + } + } + } + public void Insert(int index, T item) { if (index > _longCount || index < 0) diff --git a/Recyclable.Collections/ThrowHelper.cs b/Recyclable.Collections/ThrowHelper.cs index dfedd85..3556aca 100644 --- a/Recyclable.Collections/ThrowHelper.cs +++ b/Recyclable.Collections/ThrowHelper.cs @@ -1,14 +1,24 @@ -namespace Recyclable.Collections +#pragma warning disable IDE0022, CA2208 + +namespace Recyclable.Collections { internal static class ThrowHelper { - /// - /// This is a helper method to throw with the given message. - /// - /// Always + public static void ThrowArgumentException(string argumentName) => throw new ArgumentException("Parameter has invalid value. ", argumentName); + public static void ThrowArgumentException(string argumentName, Exception? innerException) => throw new ArgumentException("Parameter has invalid value. ", argumentName, innerException); + public static void ThrowArgumentException(string argumentName, string message) => throw new ArgumentException(message, argumentName); + public static void ThrowArgumentException(string argumentName, string message, Exception? innerException) => throw new ArgumentException(message, argumentName, innerException); + public static void ThrowArgumentException_Index() => throw new ArgumentException("Parameter has invalid value", "index"); + public static void ThrowArgumentException_Index(Exception? innerException) => throw new ArgumentException("Parameter has invalid value", "index", innerException); + public static void ThrowArgumentException_Count() => throw new ArgumentException("Parameter has invalid value", "count"); + public static void ThrowArgumentException_Count(Exception? innerException) => throw new ArgumentException("Parameter has invalid value", "count", innerException); + public static void ThrowArgumentOutOfRangeException(string argumentName) => throw new ArgumentOutOfRangeException(argumentName); + public static void ThrowArgumentOutOfRangeException(string argumentName, Exception? innerException) => throw new ArgumentOutOfRangeException($"Argument {argumentName} is out of range", innerException); public static void ThrowArgumentOutOfRangeException(string argumentName, string message) => throw new ArgumentOutOfRangeException(argumentName, message); public static void ThrowWrongReturnedArraySizeException(int returnedLength, int blockSize) => throw new InvalidOperationException($"The array size {returnedLength} doesn't match the size {blockSize} of the pool buffer and cannot be returned to this pool."); public static void ThrowArgumentOutOfRangeException_Index() => throw new ArgumentOutOfRangeException("index", "Parameter `index` has invalid value. It must be within the no. of elements in the collection"); - public static void ThrowArgumentOutOfRangeException_Count() => throw new ArgumentOutOfRangeException("index", "Parameter `count` has invalid value. Combined with index, it must be within the no. of elements in the collection"); + public static void ThrowArgumentOutOfRangeException_Index(Exception? innerException) => throw new ArgumentOutOfRangeException("Parameter `index` has invalid value. It must be within the no. of elements in the collection", innerException); + public static void ThrowArgumentOutOfRangeException_Count() => throw new ArgumentOutOfRangeException("count", "Parameter `count` has invalid value. Combined with index, it must be within the no. of elements in the collection"); + public static void ThrowArgumentOutOfRangeException_Count(Exception? innerException) => throw new ArgumentOutOfRangeException("Parameter `count` has invalid value. Combined with index, it must be within the no. of elements in the collection", innerException); } } diff --git a/Recyclable.CollectionsTests/RecyclableListTests.cs b/Recyclable.CollectionsTests/RecyclableListTests.cs index 828ab17..237175b 100644 --- a/Recyclable.CollectionsTests/RecyclableListTests.cs +++ b/Recyclable.CollectionsTests/RecyclableListTests.cs @@ -671,6 +671,46 @@ namespace Recyclable.CollectionsTests } } + [Theory] + [MemberData(nameof(RecyclableLongListTestData.SourceDataWithItemIndexVariants), MemberType = typeof(RecyclableLongListTestData))] + public void IndexOfShouldReturnCorrectIndexesWhenConstrainedCount(string testCase, IEnumerable testData, int itemsCount, in long[] itemIndexes) + { + // Prepare + using var list = new RecyclableList(testData, itemsCount); + var expectedData = testData.ToList(); + + foreach (var itemIndex in itemIndexes) + { + var expected = expectedData.IndexOf(expectedData[(int)itemIndex], (int)itemIndex, (int)(itemsCount - itemIndex)); + + // Act + var actual = list.IndexOf(expectedData[(int)itemIndex], (int)itemIndex, (int)(itemsCount - itemIndex)); + + // Validate + _ = actual.Should().Be((int)itemIndex).And.Be(expected); + } + } + + [Theory] + [MemberData(nameof(RecyclableLongListTestData.SourceDataWithItemIndexVariants), MemberType = typeof(RecyclableLongListTestData))] + public void IndexOfShouldReturnCorrectIndexesWhenConstrainedIndex(string testCase, IEnumerable testData, int itemsCount, in long[] itemIndexes) + { + // Prepare + using var list = new RecyclableList(testData, itemsCount); + var expectedData = testData.ToList(); + + foreach (var itemIndex in itemIndexes) + { + var expected = expectedData.IndexOf(expectedData[(int)itemIndex], (int)itemIndex); + + // Act + var actual = list.IndexOf(expectedData[(int)itemIndex], (int)itemIndex); + + // Validate + _ = actual.Should().Be((int)itemIndex).And.Be(expected); + } + } + [Theory] [MemberData(nameof(RecyclableLongListTestData.SourceDataWithItemIndexVariants), MemberType = typeof(RecyclableLongListTestData))] public void InitializeSortClearAddRangeRemoveShouldSucceed(string testCase, IEnumerable testData, int itemsCount, in long[] itemIndexes) diff --git a/Recyclable.CollectionsTests/RecyclableLongListTests.cs b/Recyclable.CollectionsTests/RecyclableLongListTests.cs index 5a40e42..f6f5bf7 100644 --- a/Recyclable.CollectionsTests/RecyclableLongListTests.cs +++ b/Recyclable.CollectionsTests/RecyclableLongListTests.cs @@ -648,6 +648,91 @@ namespace Recyclable.CollectionsTests } } + [Theory] + [MemberData(nameof(RecyclableLongListTestData.SourceDataWithBlockSizeWithItemIndexVariants), MemberType = typeof(RecyclableLongListTestData))] + public void IndexOfParallelShouldReturnCorrectIndexes(string testCase, IEnumerable unusedTestData, long itemsCount, int targetBlockSize, in long[] itemIndexes) + { + const long ItemsCount = RecyclableDefaults.MinItemsCountForParallelization * 3; + + var testData = RecyclableLongListTestData.CreateTestData(ItemsCount).ToArray(); + // Prepare + using var list = new RecyclableLongList(testData, minBlockSize: targetBlockSize, initialCapacity: ItemsCount); + + // Act & Validate + foreach (long itemIndex in itemIndexes) + { + var actual = list.IndexOf(testData[itemIndex]); + _ = actual.Should().Be((int)itemIndex); + } + } + + [Theory] + [MemberData(nameof(RecyclableLongListTestData.SourceDataWithBlockSizeWithItemIndexWithRangeVariants), MemberType = typeof(RecyclableLongListTestData))] + public void IndexOfParallelShouldReturnCorrectIndexesWhenConstrainedCount(string testCase, IEnumerable unusedTestData, int itemsCount, int minBlockSize, in (long ItemIndex, long RangedItemsCount)[] itemRanges) + { + const long ItemsCount = RecyclableDefaults.MinItemsCountForParallelization * 3; + + var testData = RecyclableLongListTestData.CreateTestData(ItemsCount).ToArray(); + // Prepare + using var list = new RecyclableLongList(testData, minBlockSize: minBlockSize, initialCapacity: ItemsCount); + + // Act & Validate + foreach (var (itemIndex, rangedItemsCount) in itemRanges) + { + TestWith(list, (int)itemIndex, (int)rangedItemsCount); + TestWith(list, (int)(itemIndex - 1), 3); + TestWith(list, (int)(itemIndex - 1), (int)(itemIndex + 1)); + TestWith(list, (int)Math.Max(0, itemIndex - RecyclableDefaults.MinItemsCountForParallelization), (int)Math.Min(ItemsCount - itemIndex + RecyclableDefaults.MinItemsCountForParallelization, itemIndex + RecyclableDefaults.MinItemsCountForParallelization)); + } + + static void TestWith(RecyclableLongList list, int index, int count) + { + index = Math.Max(0, index); + count = Math.Min(count, list.Count - index); + _ = list.IndexOf(list[index], index, count).Should().Be(count > 0 ? index : -1); + _ = list.IndexOf(list[index], Math.Max(0, index - 1), 3).Should().Be(index); + _ = list.IndexOf(list[Math.Max(0, index + count - 1)], index, count).Should().Be(count > 0 ? Math.Max(0, index + count - 1) : -1); + _ = list.IndexOf(0, index, count).Should().Be(-1); + + if (index > 0) + { + _ = list.IndexOf(list[index - 1], index, count).Should().Be(-1); + } + } + } + + [Theory] + [MemberData(nameof(RecyclableLongListTestData.SourceDataWithBlockSizeWithItemIndexVariants), MemberType = typeof(RecyclableLongListTestData))] + public void IndexOfParallelShouldReturnCorrectIndexesWhenConstrainedIndex(string testCase, IEnumerable unusedTestData, long itemsCount, int targetBlockSize, in long[] itemIndexes) + { + const long ItemsCount = RecyclableDefaults.MinItemsCountForParallelization * 3; + + var testData = RecyclableLongListTestData.CreateTestData(ItemsCount).ToArray(); + // Prepare + using var list = new RecyclableLongList(testData, minBlockSize: targetBlockSize, initialCapacity: ItemsCount); + + // Act & Validate + foreach(var itemIndex in itemIndexes) + { + TestWith(list, (int)Math.Max(itemIndex - 1, 0)); + TestWith(list, (int)Math.Max(0, itemIndex - RecyclableDefaults.MinItemsCountForParallelization)); + } + + static void TestWith(RecyclableLongList list, int index) + { + var count = list.Count - 1; + _ = list.IndexOf(list[index], index).Should().Be(index); + _ = list.IndexOf(list[count], index).Should().Be(count); + _ = list.IndexOf(-1, index).Should().Be(-1); + _ = list.IndexOf(0, index).Should().Be(-1); + + if (index > 0) + { + _ = list.IndexOf(list[index - 1], index).Should().Be(-1); + } + } + } + [Fact] public void IndexOfShouldNotFindAnythingWhenListEmpty() { @@ -688,6 +773,55 @@ namespace Recyclable.CollectionsTests } } + [Theory] + [MemberData(nameof(RecyclableLongListTestData.SourceDataWithBlockSizeWithItemIndexWithRangeVariants), MemberType = typeof(RecyclableLongListTestData))] + public void IndexOfShouldReturnCorrectIndexesWhenConstrainedCount(string testCase, IEnumerable testData, int itemsCount, int minBlockSize, in (long ItemIndex, long RangedItemsCount)[] itemRanges) + { + // Prepare + using var list = new RecyclableLongList(testData, minBlockSize: minBlockSize, itemsCount); + var expectedData = testData.ToList(); + + // Act & Validate + foreach (var (itemIndex, rangedItemsCount) in itemRanges) + { + TestWith(list, expectedData, itemIndex, rangedItemsCount, expectedData[(int)itemIndex]); + TestWith(list, expectedData, itemIndex, (int)(itemsCount - itemIndex), expectedData[(int)itemIndex]); + TestWith(list, expectedData, itemIndex, rangedItemsCount, expectedData[(int)Math.Max(0, itemIndex + rangedItemsCount - 1)]); + } + + static void TestWith(RecyclableLongList list, List expectedData, long itemIndex, long rangedItemsCount, long item) + { + var expected = expectedData.IndexOf(item, (int)itemIndex, (int)rangedItemsCount); + var actual = list.IndexOf(item, (int)itemIndex, (int)rangedItemsCount); + + _ = actual.Should().Be(expected); + } + } + + [Theory] + [MemberData(nameof(RecyclableLongListTestData.SourceDataWithItemIndexVariants), MemberType = typeof(RecyclableLongListTestData))] + public void IndexOfShouldReturnCorrectIndexesWhenConstrainedIndex(string testCase, IEnumerable testData, int itemsCount, in long[] itemIndexes) + { + // Prepare + using var list = new RecyclableLongList(testData, initialCapacity: itemsCount); + var expectedData = testData.ToList(); + + foreach (var itemIndex in itemIndexes) + { + TestWith(list, expectedData[(int)itemIndex], itemIndex); + TestWith(list, expectedData[itemsCount - 1], itemIndex); + } + + void TestWith(RecyclableLongList list, long item, long itemIndex) + { + var expected = expectedData.IndexOf(item, (int)itemIndex); + // Act + var actual = list.IndexOf(item, (int)itemIndex); + // Validate + _ = actual.Should().Be(expected); + } + } + [Theory] [MemberData(nameof(RecyclableLongListTestData.SourceDataWithBlockSizeWithItemIndexVariants), MemberType = typeof(RecyclableLongListTestData))] public void InitializeSortClearAddRangeRemoveShouldSucceed(string testCase, IEnumerable testData, long itemsCount, int targetBlockSize, in long[] itemIndexes)