diff --git a/Recyclable.Collections/RecyclableStack.AddRange.cs b/Recyclable.Collections/RecyclableStack.AddRange.cs new file mode 100644 index 0000000..f343eaf --- /dev/null +++ b/Recyclable.Collections/RecyclableStack.AddRange.cs @@ -0,0 +1,387 @@ +using System.Collections; +using System.Linq.Expressions; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Numerics; +using Recyclable.Collections.Pools; + +namespace Recyclable.Collections +{ + internal static class zRecyclableStackAddRange + { + private static class AddRangeHelper + { + private static readonly Action, object>? _dictionaryAdder; + private static readonly Action, object>? _sortedListAdder; + private static readonly Type? _dictionaryType; + private static readonly Type? _sortedListType; + + static AddRangeHelper() + { + Type elementType = typeof(T); + if (elementType.IsGenericType && elementType.GetGenericTypeDefinition() == typeof(KeyValuePair<,>)) + { + Type[] args = elementType.GetGenericArguments(); + _dictionaryType = typeof(RecyclableDictionary<,>).MakeGenericType(args); + MethodInfo? method = typeof(zRecyclableStackAddRange).GetMethod( + nameof(AddRange), + BindingFlags.NonPublic | BindingFlags.Static, + binder: null, + types: new[] { typeof(RecyclableStack<>).MakeGenericType(elementType), _dictionaryType }, + modifiers: null); + if (method != null) + { + var stackParam = Expression.Parameter(typeof(RecyclableStack)); + var objParam = Expression.Parameter(typeof(object)); + var call = Expression.Call(method, stackParam, Expression.Convert(objParam, _dictionaryType)); + _dictionaryAdder = Expression.Lambda, object>>(call, stackParam, objParam).Compile(); + } + } + else if (elementType.IsGenericType && elementType.GetGenericTypeDefinition() == typeof(ValueTuple<,>)) + { + Type[] args = elementType.GetGenericArguments(); + _sortedListType = typeof(RecyclableSortedList<,>).MakeGenericType(args); + MethodInfo? method = typeof(zRecyclableStackAddRange).GetMethod( + nameof(AddRange), + BindingFlags.NonPublic | BindingFlags.Static, + binder: null, + types: new[] { typeof(RecyclableStack<>).MakeGenericType(elementType), _sortedListType }, + modifiers: null); + if (method != null) + { + var stackParam = Expression.Parameter(typeof(RecyclableStack)); + var objParam = Expression.Parameter(typeof(object)); + var call = Expression.Call(method, stackParam, Expression.Convert(objParam, _sortedListType)); + _sortedListAdder = Expression.Lambda, object>>(call, stackParam, objParam).Compile(); + } + } + } + + internal static bool TryAddRange(RecyclableStack stack, IEnumerable items) + { + if (_dictionaryAdder != null && _dictionaryType!.IsInstanceOfType(items)) + { + _dictionaryAdder(stack, items); + return true; + } + + if (_sortedListAdder != null && _sortedListType!.IsInstanceOfType(items)) + { + _sortedListAdder(stack, items); + return true; + } + + return false; + } + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + internal static void AddRange(this RecyclableStack stack, ReadOnlySpan items) + { + for (int i = 0; i < items.Length; i++) + { + stack.Push(items[i]); + } + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + internal static void AddRange(this RecyclableStack stack, Span items) => AddRange(stack, (ReadOnlySpan)items); + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + internal static void AddRange(this RecyclableStack stack, T[] items) => AddRange(stack, (ReadOnlySpan)items); + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + internal static void AddRange(this RecyclableStack stack, in Array items) + { + for (int i = 0; i < items.Length; i++) + { + stack.Push((T)items.GetValue(i)!); + } + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + internal static void AddRange(this RecyclableStack stack, List items) => AddRange(stack, CollectionsMarshal.AsSpan(items)); + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + internal static void AddRange(this RecyclableStack stack, ICollection items) + { + int count = items.Count; + if (count == 0) + { + return; + } + + T[] buffer = count >= RecyclableDefaults.MinPooledArrayLength + ? RecyclableArrayPool.RentShared(checked((int)BitOperations.RoundUpToPowerOf2((uint)count))) + : new T[count]; + + items.CopyTo(buffer, 0); + + for (int i = 0; i < count; i++) + { + stack.Push(buffer[i]); + } + + if (count >= RecyclableDefaults.MinPooledArrayLength) + { + RecyclableArrayPool.ReturnShared(buffer, RecyclableStack._needsClearing); + } + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + internal static void AddRange(this RecyclableStack stack, ICollection items) + { + int count = items.Count; + if (count == 0) + { + return; + } + + T[] buffer = count >= RecyclableDefaults.MinPooledArrayLength + ? RecyclableArrayPool.RentShared(checked((int)BitOperations.RoundUpToPowerOf2((uint)count))) + : new T[count]; + + items.CopyTo(buffer, 0); + + for (int i = 0; i < count; i++) + { + stack.Push(buffer[i]); + } + + if (count >= RecyclableDefaults.MinPooledArrayLength) + { + RecyclableArrayPool.ReturnShared(buffer, RecyclableStack._needsClearing); + } + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + internal static void AddRange(this RecyclableStack stack, RecyclableList items) + { + AddRange(stack, new ReadOnlySpan(items._memoryBlock, 0, items._count)); + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + internal static void AddRange(this RecyclableStack stack, RecyclableLongList items) + { + long longCount = items._longCount; + if (longCount == 0) + { + return; + } + + int blockSize = items._blockSize; + int fullBlocks = (int)(longCount >> items._blockSizePow2BitShift); + int lastBlockLength = (int)(longCount & items._blockSizeMinus1); + + for (int blockIndex = 0; blockIndex < fullBlocks; blockIndex++) + { + T[] block = items._memoryBlocks[blockIndex]; + for (int i = 0; i < blockSize; i++) + { + stack.Push(block[i]); + } + } + + if (lastBlockLength > 0) + { + T[] block = items._memoryBlocks[fullBlocks]; + for (int i = 0; i < lastBlockLength; i++) + { + stack.Push(block[i]); + } + } + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + internal static void AddRange(this RecyclableStack stack, RecyclableHashSet items) + { + var entries = items._entries; + int shift = items._blockShift, + mask = items._blockSizeMinus1; + for (int i = 0; i < items._count; i++) + { + stack.Push(entries[i >> shift][i & mask].Value); + } + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + internal static void AddRange(this RecyclableStack stack, RecyclableStack items) + { + if (items._count == 0) + { + return; + } + + RecyclableArrayPoolChunk chunk = items._current; + while (chunk.Previous != null) + { + chunk = chunk.Previous; + } + + while (chunk != null) + { + for (int i = 0; i < chunk.Index; i++) + { + stack.Push(chunk.Value[i]); + } + chunk = chunk.Next!; + } + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + internal static void AddRange(this RecyclableStack stack, RecyclableSortedSet items) + { + for (int i = 0; i < items._count; i++) + { + stack.Push(items._items[i]); + } + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + internal static void AddRange(this RecyclableStack stack, RecyclableLinkedList items) + { + var chunk = items._head; + while (chunk != null) + { + for (int i = chunk.Bottom; i < chunk.Top; i++) + { + stack.Push(chunk.Value[i]); + } + chunk = chunk.Next; + } + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + internal static void AddRange(this RecyclableStack stack, RecyclablePriorityQueue items) + { + AddRange(stack, new ReadOnlySpan(items._heap, 0, items._size)); + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + internal static void AddRange(this RecyclableStack stack, RecyclableQueue items) + { + var chunk = items._head; + while (chunk != null) + { + for (int i = chunk.Bottom; i < chunk.Top; i++) + { + stack.Push(chunk.Value[i]); + } + chunk = chunk.Next; + } + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + internal static void AddRange(this RecyclableStack> stack, RecyclableDictionary items) + where TKey : notnull + { + var entries = items._entries; + int shift = items._blockShift, + mask = items._blockSizeMinus1; + for (int i = 0; i < items._count; i++) + { + ref var entry = ref entries[i >> shift][i & mask]; + stack.Push(new KeyValuePair(entry.Key, entry.Value)); + } + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + internal static void AddRange(this RecyclableStack<(TKey Key, TValue Value)> stack, RecyclableSortedList items) + where TKey : notnull + { + for (int i = 0; i < items._count; i++) + { + stack.Push((items._keys[i], items._values[i])); + } + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + internal static void AddRange(this RecyclableStack stack, IReadOnlyList items) + { + for (int i = 0; i < items.Count; i++) + { + stack.Push(items[i]); + } + } + + internal static IEnumerator AddRange(this RecyclableStack stack, IEnumerable source) + { + var enumerator = source.GetEnumerator(); + while (enumerator.MoveNext()) + { + stack.Push((T)enumerator.Current!); + } + return enumerator; + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + internal static void AddRange(this RecyclableStack stack, IEnumerable items) + { + if (items is RecyclableList recyclableList) + { + AddRange(stack, recyclableList); + } + else if (items is RecyclableLongList recyclableLongList) + { + AddRange(stack, recyclableLongList); + } + else if (items is T[] array) + { + AddRange(stack, array); + } + else if (items is List list) + { + AddRange(stack, list); + } + else if (items is ICollection iCollection) + { + AddRange(stack, iCollection); + } + else if (items is ICollection collection) + { + AddRange(stack, collection); + } + else if (items is RecyclableHashSet hashSet) + { + AddRange(stack, hashSet); + } + else if (items is RecyclableStack stackSource) + { + AddRange(stack, stackSource); + } + else if (items is RecyclableSortedSet sortedSet) + { + AddRange(stack, sortedSet); + } + else if (items is RecyclableLinkedList linkedList) + { + AddRange(stack, linkedList); + } + else if (items is RecyclablePriorityQueue priorityQueue) + { + AddRange(stack, priorityQueue); + } + else if (items is RecyclableQueue queue) + { + AddRange(stack, queue); + } + else if (AddRangeHelper.TryAddRange(stack, items)) + { + } + else if (items is IReadOnlyList readOnlyList) + { + AddRange(stack, readOnlyList); + } + else + { + using IEnumerator enumerator = items.GetEnumerator(); + while (enumerator.MoveNext()) + { + stack.Push(enumerator.Current); + } + } + } + } +} + diff --git a/Recyclable.CollectionsTests/RecyclableStackTests.cs b/Recyclable.CollectionsTests/RecyclableStackTests.cs index a3f8218..d215e63 100644 --- a/Recyclable.CollectionsTests/RecyclableStackTests.cs +++ b/Recyclable.CollectionsTests/RecyclableStackTests.cs @@ -128,5 +128,243 @@ namespace Recyclable.CollectionsTests _ = Assert.Throws(() => list.Pop()); } + [Fact] + public void AddRangeShouldAcceptNulls() + { + using var stack = new RecyclableStack(); + stack.AddRange(new long?[] { null, default }); + _ = stack.Should().HaveCount(2).And.AllSatisfy(x => x.Should().BeNull()); + } + + [Theory] + [MemberData(nameof(RecyclableLongListTestData.SourceDataVariants), MemberType = typeof(RecyclableLongListTestData))] + public void AddRangeShouldAddItemsInCorrectOrder(string testCase, IEnumerable testData, int itemsCount) + { + using var stack = new RecyclableStack(); + + if (testCase.Contains("Array[", StringComparison.OrdinalIgnoreCase)) + { + stack.AddRange((Array)testData); + } + else if (testCase.Contains("ICollection[", StringComparison.OrdinalIgnoreCase)) + { + stack.AddRange((System.Collections.ICollection)testData); + } + else if (testCase.Contains("ICollection[", StringComparison.OrdinalIgnoreCase)) + { + stack.AddRange((ICollection)testData); + } + else if (testCase.Contains("IEnumerable[", StringComparison.OrdinalIgnoreCase)) + { + _ = stack.AddRange((System.Collections.IEnumerable)testData); + } + else if (testCase.Contains("IReadOnlyList[", StringComparison.OrdinalIgnoreCase)) + { + stack.AddRange((IReadOnlyList)testData); + } + else if (testCase.Contains("ReadOnlySpan[", StringComparison.OrdinalIgnoreCase)) + { + stack.AddRange(new ReadOnlySpan((long[])testData)); + } + else if (testCase.Contains("Span[", StringComparison.OrdinalIgnoreCase)) + { + stack.AddRange(new Span((long[])testData)); + } + else if (testData is long[] array) + { + stack.AddRange(array); + } + else if (testData is List list) + { + stack.AddRange(list); + } + else if (testData is RecyclableList rList) + { + stack.AddRange(rList); + } + else if (testData is RecyclableLongList rLongList) + { + stack.AddRange(rLongList); + } + else if (testData is IList iList) + { + stack.AddRange((ICollection)iList); + } + else if (testData is IEnumerable enumerable) + { + stack.AddRange(enumerable); + } + else + { + throw new InvalidCastException("Unknown type of test data"); + } + + List expected = new(itemsCount); + expected.AddRange(testData); + expected.Reverse(); + + _ = stack.Count.Should().Be(itemsCount); + _ = stack.Should().Equal(expected); + } + + [Theory] + [MemberData(nameof(RecyclableLongListTestData.EmptySourceDataVariants), MemberType = typeof(RecyclableLongListTestData))] + public void AddRangeShouldDoNothingWhenSourceIsEmpty(string testCase, IEnumerable testData, int itemsCount) + { + using var stack = new RecyclableStack(); + + if (testCase.Contains("Array[", StringComparison.OrdinalIgnoreCase)) + { + stack.AddRange((Array)testData); + } + else if (testCase.Contains("ICollection[", StringComparison.OrdinalIgnoreCase)) + { + stack.AddRange((System.Collections.ICollection)testData); + } + else if (testCase.Contains("ICollection[", StringComparison.OrdinalIgnoreCase)) + { + stack.AddRange((ICollection)testData); + } + else if (testCase.Contains("IEnumerable[", StringComparison.OrdinalIgnoreCase)) + { + _ = stack.AddRange((System.Collections.IEnumerable)testData); + } + else if (testCase.Contains("IReadOnlyList[", StringComparison.OrdinalIgnoreCase)) + { + stack.AddRange((IReadOnlyList)testData); + } + else if (testCase.Contains("ReadOnlySpan[", StringComparison.OrdinalIgnoreCase)) + { + stack.AddRange(new ReadOnlySpan((long[])testData)); + } + else if (testCase.Contains("Span[", StringComparison.OrdinalIgnoreCase)) + { + stack.AddRange(new Span((long[])testData)); + } + else if (testData is long[] array) + { + stack.AddRange(array); + } + else if (testData is List list) + { + stack.AddRange(list); + } + else if (testData is RecyclableList rList) + { + stack.AddRange(rList); + } + else if (testData is RecyclableLongList rLongList) + { + stack.AddRange(rLongList); + } + else if (testData is IList iList) + { + stack.AddRange((ICollection)iList); + } + else if (testData is IEnumerable enumerable) + { + stack.AddRange(enumerable); + } + else + { + throw new InvalidCastException("Unknown type of test data"); + } + + List expected = new(itemsCount); + expected.AddRange(testData); + expected.Reverse(); + + _ = stack.Count.Should().Be(itemsCount); + _ = stack.Should().Equal(expected); + } + + [Theory] + [MemberData(nameof(RecyclableLongListTestData.SourceDataVariants), MemberType = typeof(RecyclableLongListTestData))] + public void AddRangeShouldNotOverrideItems(string testCase, IEnumerable testData, int itemsCount) + { + using var stack = new RecyclableStack(); + itemsCount = checked(itemsCount << 1); + + if (testCase.Contains("Array[", StringComparison.OrdinalIgnoreCase)) + { + stack.AddRange((Array)testData); + stack.AddRange((Array)testData); + } + else if (testCase.Contains("ICollection[", StringComparison.OrdinalIgnoreCase)) + { + stack.AddRange((System.Collections.ICollection)testData); + stack.AddRange((System.Collections.ICollection)testData); + } + else if (testCase.Contains("ICollection[", StringComparison.OrdinalIgnoreCase)) + { + stack.AddRange((ICollection)testData); + stack.AddRange((ICollection)testData); + } + else if (testCase.Contains("IEnumerable[", StringComparison.OrdinalIgnoreCase)) + { + _ = stack.AddRange((System.Collections.IEnumerable)testData); + _ = stack.AddRange((System.Collections.IEnumerable)testData); + } + else if (testCase.Contains("IReadOnlyList[", StringComparison.OrdinalIgnoreCase)) + { + stack.AddRange((IReadOnlyList)testData); + stack.AddRange((IReadOnlyList)testData); + } + else if (testCase.Contains("ReadOnlySpan[", StringComparison.OrdinalIgnoreCase)) + { + var span = new ReadOnlySpan((long[])testData); + stack.AddRange(span); + stack.AddRange(span); + } + else if (testCase.Contains("Span[", StringComparison.OrdinalIgnoreCase)) + { + var span = new Span((long[])testData); + stack.AddRange(span); + stack.AddRange(span); + } + else if (testData is long[] array) + { + stack.AddRange(array); + stack.AddRange(array); + } + else if (testData is List list) + { + stack.AddRange(list); + stack.AddRange(list); + } + else if (testData is RecyclableList rList) + { + stack.AddRange(rList); + stack.AddRange(rList); + } + else if (testData is RecyclableLongList rLongList) + { + stack.AddRange(rLongList); + stack.AddRange(rLongList); + } + else if (testData is IList iList) + { + stack.AddRange((ICollection)iList); + stack.AddRange((ICollection)iList); + } + else if (testData is IEnumerable enumerable) + { + stack.AddRange(enumerable); + stack.AddRange(enumerable); + } + else + { + throw new InvalidCastException("Unknown type of test data"); + } + + List expected = new(itemsCount); + expected.AddRange(testData); + expected.AddRange(testData); + expected.Reverse(); + + _ = stack.Count.Should().Be(itemsCount); + _ = stack.Should().Equal(expected); + } + } }