diff --git a/Recyclable.Collections.Compatibility.List.Tests/RecyclableListCompatibilityListTests.cs b/Recyclable.Collections.Compatibility.List.Tests/RecyclableListCompatibilityListTests.cs index 32227f2..c5ae40f 100644 --- a/Recyclable.Collections.Compatibility.List.Tests/RecyclableListCompatibilityListTests.cs +++ b/Recyclable.Collections.Compatibility.List.Tests/RecyclableListCompatibilityListTests.cs @@ -34,6 +34,30 @@ namespace Recyclable.CollectionsTests } } + [Theory] + [MemberData(nameof(RecyclableLongListTestData.SourceDataWithItemIndexVariants), MemberType = typeof(RecyclableLongListTestData))] + public void BinarySearchShouldFindAllItemsWhenWithCustomComparer(string testCase, IEnumerable testData, int itemsCount, in long[] itemIndexes) + { + _ = testData.Any().Should().BeTrue("we need items on the list that we can look for"); + + // Prepare + using var list = new RecyclableList(testData, itemsCount); + var comparer = Comparer.Default; + var expectedData = testData.ToList(); + + foreach (var itemIndex in itemIndexes) + { + var expectedItem = expectedData[(int)itemIndex]; + var expected = expectedData.BinarySearch(expectedItem, comparer); + + // Act + var actual = list.BinarySearch(expectedItem, comparer); + + // Validate + _ = actual.Should().Be(expected); + } + } + [Theory] [MemberData(nameof(RecyclableLongListTestData.SourceDataVariants), MemberType = typeof(RecyclableLongListTestData))] public void ConvertAllShouldConvertAllItems(string testCase, IEnumerable testData, int itemsCount) diff --git a/Recyclable.Collections.Compatibility.List.Tests/RecyclableLongListCompatibilityListTests.cs b/Recyclable.Collections.Compatibility.List.Tests/RecyclableLongListCompatibilityListTests.cs index 0f49f12..bf5e587 100644 --- a/Recyclable.Collections.Compatibility.List.Tests/RecyclableLongListCompatibilityListTests.cs +++ b/Recyclable.Collections.Compatibility.List.Tests/RecyclableLongListCompatibilityListTests.cs @@ -34,6 +34,113 @@ namespace Recyclable.CollectionsTests } } + [Theory] + [MemberData(nameof(RecyclableLongListTestData.SourceDataWithBlockSizeWithItemIndexVariants), MemberType = typeof(RecyclableLongListTestData))] + public void BinarySearchShouldFindAllItemsWhenConstrainedRange(string testCase, IEnumerable testData, int itemsCount, int minBlockSize, in long[] itemIndexes) + { + _ = testData.Any().Should().BeTrue("we need items on the list that we can look for"); + + // Prepare + using var list = new RecyclableLongList(testData, minBlockSize, initialCapacity: itemsCount); + var comparer = Comparer.Default; + var expectedData = testData.ToList(); + + foreach (var itemIndex in itemIndexes) + { + var rangeSize = (int)Math.Min(itemsCount - itemIndex, 4); + var expectedItem = expectedData[(int)itemIndex]; + var startItemIndex = (int)(rangeSize > 2 ? Math.Max(0, itemIndex - 2) : itemIndex); + var expected = expectedData.BinarySearch(startItemIndex, rangeSize, expectedItem, comparer); + + // Act + var actual = list.BinarySearch(startItemIndex, rangeSize, expectedItem, comparer); + + // Validate + _ = actual.Should().Be(expected); + } + } + + [Theory] + [MemberData(nameof(RecyclableLongListTestData.SourceDataWithBlockSizeWithItemIndexVariants), MemberType = typeof(RecyclableLongListTestData))] + public void BinarySearchShouldFindAllItemsWhenWithCustomComparer(string testCase, IEnumerable testData, int itemsCount, int minBlockSize, in long[] itemIndexes) + { + _ = testData.Any().Should().BeTrue("we need items on the list that we can look for"); + + // Prepare + using var list = new RecyclableLongList(testData, minBlockSize, initialCapacity: itemsCount); + var comparer = Comparer.Default; + var expectedData = testData.ToList(); + + foreach (var itemIndex in itemIndexes) + { + var expectedItem = expectedData[(int)itemIndex]; + var expected = expectedData.BinarySearch(expectedItem, comparer); + + // Act + var actual = list.BinarySearch(expectedItem, comparer); + + // Validate + _ = actual.Should().Be(expected); + } + } + + [Theory] + [MemberData(nameof(RecyclableLongListTestData.SourceDataWithBlockSizeWithItemIndexVariants), MemberType = typeof(RecyclableLongListTestData))] + public void BinarySearchShouldNotFindNonExistingItems(string testCase, IEnumerable testData, int itemsCount, int minBlockSize, in long[] itemIndexes) + { + _ = testData.Any().Should().BeTrue("we need items on the list that we can look for"); + + // Prepare + var expectedData = testData.ToList(); + + foreach (var itemIndex in itemIndexes) + { + using var list = new RecyclableLongList(testData, minBlockSize, initialCapacity: itemsCount); + list.RemoveAt((int)itemIndex); + var expectedItem = expectedData[(int)itemIndex]; + var expectedRangedData = testData.ToList(); + expectedRangedData.RemoveAt((int)itemIndex); + var expected = expectedRangedData.BinarySearch(expectedItem); + + // Act + var actual = list.BinarySearch(expectedItem); + + // Validate + _ = actual.Should().Be(expected); + } + } + + [Theory] + [MemberData(nameof(RecyclableLongListTestData.SourceDataWithBlockSizeWithItemIndexVariants), MemberType = typeof(RecyclableLongListTestData))] + public void BinarySearchShouldNotFindNonExistingItemsWhenConstrainedRange(string testCase, IEnumerable testData, int itemsCount, int minBlockSize, in long[] itemIndexes) + { + _ = testData.Any().Should().BeTrue("we need items on the list that we can look for"); + + // Prepare + + var comparer = Comparer.Default; + var expectedData = testData.ToList(); + + foreach (var itemIndex in itemIndexes) + { + using var list = new RecyclableLongList(testData, minBlockSize, initialCapacity: itemsCount); + list.RemoveAt(itemIndex); + var expectedItem = expectedData[(int)itemIndex]; + var expectedRangedData = testData.ToList(); + expectedRangedData.RemoveAt((int)itemIndex); + // Now we have 1 element less on the lists + var rangeSize = (int)Math.Max(Math.Min(itemsCount - itemIndex - 2, 4), 0); + int startItemIndex = (int)(rangeSize > 2 ? Math.Max(0, itemIndex - 2 - 1) : Math.Max(0, itemIndex - 1)); + var expected = expectedRangedData.BinarySearch(startItemIndex, rangeSize, expectedItem, comparer); + + // Act + var actual = list.BinarySearch(startItemIndex, rangeSize, expectedItem, comparer); + + // Validate + _ = actual.Should().Be(expected); + } + } + [Theory] [MemberData(nameof(RecyclableLongListTestData.SourceDataWithBlockSizeWithItemIndexVariants), MemberType = typeof(RecyclableLongListTestData))] public void ConvertAllShouldConvertAllItems(string testCase, IEnumerable testData, int minBlockSize, int itemsCount) diff --git a/Recyclable.Collections.Compatibility.List/RecyclableLongList.Compatibility.List.cs b/Recyclable.Collections.Compatibility.List/RecyclableLongList.Compatibility.List.cs index 04e36ca..0ff4b97 100644 --- a/Recyclable.Collections.Compatibility.List/RecyclableLongList.Compatibility.List.cs +++ b/Recyclable.Collections.Compatibility.List/RecyclableLongList.Compatibility.List.cs @@ -24,34 +24,210 @@ namespace Recyclable.Collections int blockIndex = 0, blockSize = list._blockSize, - itemIndex; + foundItemIndex; var comparer = Comparer.Default; var lastFullBlockIndex = list._lastBlockWithData; Span memoryBlocksSpan = list._memoryBlocks; while (blockIndex < lastFullBlockIndex) { - itemIndex = new Span(memoryBlocksSpan[blockIndex], 0, blockSize).BinarySearch(item, comparer); - if (itemIndex >= 0) + foundItemIndex = new Span(memoryBlocksSpan[blockIndex], 0, blockSize).BinarySearch(item, comparer); + if (foundItemIndex >= 0) { - return (blockIndex << list._blockSizePow2BitShift) + itemIndex; + return (blockIndex << list._blockSizePow2BitShift) + foundItemIndex; + } + else if (-foundItemIndex <= blockSize && foundItemIndex <= RecyclableDefaults.ItemNotFoundIndex) + { + return foundItemIndex - (blockIndex << list._blockSizePow2BitShift); } blockIndex++; } - itemIndex = new Span(memoryBlocksSpan[blockIndex], 0, list._nextItemIndex != 0 ? list._nextItemIndex : blockSize).BinarySearch(item, comparer); - return itemIndex >= 0 ? (blockIndex << list._blockSizePow2BitShift) + itemIndex : RecyclableDefaults.ItemNotFoundIndex; + foundItemIndex = new Span(memoryBlocksSpan[blockIndex], 0, list._nextItemIndex != 0 ? list._nextItemIndex : blockSize).BinarySearch(item, comparer); + return foundItemIndex >= 0 ? (blockIndex << list._blockSizePow2BitShift) + foundItemIndex : foundItemIndex - (blockIndex << list._blockSizePow2BitShift); } [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] public static int BinarySearch(this RecyclableLongList list, T item, IComparer? comparer) - //=> Array.BinarySearch(list._memoryBlock, 0, list._count, item, comparer); - => throw new NotImplementedException(); + { + if (list._longCount == 0) + { + return RecyclableDefaults.ItemNotFoundIndex; + } + + comparer ??= Comparer.Default; + int blockIndex = 0, + blockSize = list._blockSize, + foundItemIndex; + var lastFullBlockIndex = list._lastBlockWithData; + Span memoryBlocksSpan = list._memoryBlocks; + while (blockIndex < lastFullBlockIndex) + { + foundItemIndex = new Span(memoryBlocksSpan[blockIndex], 0, blockSize).BinarySearch(item, comparer); + if (foundItemIndex >= 0) + { + return (blockIndex << list._blockSizePow2BitShift) + foundItemIndex; + } + + blockIndex++; + } + + foundItemIndex = new Span(memoryBlocksSpan[blockIndex], 0, list._nextItemIndex != 0 ? list._nextItemIndex : blockSize).BinarySearch(item, comparer); + return foundItemIndex >= 0 ? (blockIndex << list._blockSizePow2BitShift) + foundItemIndex : RecyclableDefaults.ItemNotFoundIndex; + } [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] public static int BinarySearch(this RecyclableLongList list, int index, int count, T item, IComparer? comparer) - //=> Array.BinarySearch(list._memoryBlock, index, count, item, comparer); - => throw new NotImplementedException(); + { + if (list._longCount == 0) + { + return RecyclableDefaults.ItemNotFoundIndex; + } + + comparer ??= Comparer.Default; + Span memoryBlocksSpan = list._memoryBlocks; + byte blockShift = list._blockSizePow2BitShift; + int blockSizeMinus1 = list._blockSizeMinus1; + long low = index, + high = index + count - 1; + + int compareResult = comparer.Compare(memoryBlocksSpan[(int)(low >> blockShift)][(int)(low & blockSizeMinus1)], item); + if (compareResult == 0) + { + return checked((int)low); + } + else if (compareResult > 0) + { + return RecyclableDefaults.ItemNotFoundIndex; + } + + while (low <= high) + { + // i might overflow if lo and hi are both large positive numbers. + long med = low + ((high - low) >> 1); + + compareResult = comparer.Compare(memoryBlocksSpan[(int)(med >> blockShift)][(int)(med & blockSizeMinus1)], item); + if (compareResult == 0) + { + return checked((int)med); + } + + if (compareResult < 0) + { + low = med + 1; + } + else + { + high = med - 1; + } + } + + return checked((int)~low); + + // ATTEMPT 2 + //int blockSizeMinus1 = list._blockSizeMinus1; + //long low = index, + // high = Math.Min(index + (count != 0 ? count : list._longCount) - 1, list._longCount - 1); + //byte blockShift = list._blockSizePow2BitShift; + + //comparer ??= Comparer.Default; + //Span memoryBlocksSpan = list._memoryBlocks; + + //while (low <= high) + //{ + // long mid = low + ((high - low) / 2); + + // // Compare the target with the middle element of the array + // int comparisonResult = comparer.Compare(item, memoryBlocksSpan[(int)mid >> blockShift][mid & blockSizeMinus1]); + // if (comparisonResult == 0) + // { + // return (int)mid; + // } + // else if (comparisonResult < 0) + // { + // low = mid + 1; + // } + // else + // { + // high = mid - 1; + // low++; + // } + //} + + //return low != 0 ? (int)-low : RecyclableDefaults.ItemNotFoundIndex ; + + + // ATTEMPT 3 + //if (list._longCount == 0 || (count == 0 && index == 0)) + //{ + // return RecyclableDefaults.ItemNotFoundIndex; + //} + + //comparer ??= Comparer.Default; + //byte blockSizePow2BitShift = list._blockSizePow2BitShift; + //int blockSize = list._blockSize, + // startItemIndex = index & list._blockSizeMinus1, + // blockIndex = index >> blockSizePow2BitShift, + // lastItemIndex = (index + count) & list._blockSizeMinus1, + // lastFullBlockIndex = ((index + count) >> blockSizePow2BitShift) - (lastItemIndex != 0 /*|| count == 1*/ ? 1 : 0); + + //Span memoryBlocksSpan = list._memoryBlocks; + //if (comparer.Compare(item, memoryBlocksSpan[blockIndex][startItemIndex]) < 0) + //{ + // return RecyclableDefaults.ItemNotFoundIndex; + //} + + //int itemIndex = + // new Span + // ( + // memoryBlocksSpan[blockIndex], + // startItemIndex, + // Math.Max(0, Math.Min(blockSize, (int)Math.Min(count, list._longCount - startItemIndex))) + // ) + // .BinarySearch(item, comparer); + + //if (itemIndex >= 0) + //{ + // return (blockIndex << blockSizePow2BitShift) + itemIndex; + //} + //else if ((itemIndex < RecyclableDefaults.ItemNotFoundIndex || blockIndex > 0) && -itemIndex <= blockSize) + //{ + // return itemIndex - (blockIndex << blockSizePow2BitShift); + //} + ////else if (blockIndex == 0) + ////{ + //// return itemIndex; + ////} + + //blockIndex++; + //while (blockIndex <= lastFullBlockIndex) + //{ + // itemIndex = new Span(memoryBlocksSpan[blockIndex], 0, blockSize).BinarySearch(item, comparer); + // if (itemIndex >= 0) + // { + // return (blockIndex << blockSizePow2BitShift) + itemIndex; + // } + // else if (itemIndex <= RecyclableDefaults.ItemNotFoundIndex) + // { + // return itemIndex - (blockIndex << blockSizePow2BitShift); + // } + + // blockIndex++; + //} + + //if (blockIndex <= list._lastBlockWithData) + //{ + // itemIndex = new Span(memoryBlocksSpan[blockIndex], 0, lastItemIndex + 1).BinarySearch(item, comparer); + // switch (itemIndex) + // { + // case >= 0: return (blockIndex << blockSizePow2BitShift) + itemIndex; + // case < RecyclableDefaults.ItemNotFoundIndex: return itemIndex - (blockIndex << blockSizePow2BitShift); + // case RecyclableDefaults.ItemNotFoundIndex: return -(blockIndex << blockSizePow2BitShift) - (blockSize > 1 ? 1 : 0); + // } + //} + + //return checked((int)-list._longCount); + } public static RecyclableList ConvertAll(this RecyclableLongList list, Converter converter) {