diff --git a/README.md b/README.md index 3608dc9..9e69183 100644 --- a/README.md +++ b/README.md @@ -138,10 +138,10 @@ 1. ✅ `Span` 1. ✅ `T[]` 1. ✅ Release 0.0.5 - 1. 👉 Implement `List` compatibility pack in `RecyclableList` - 1. 🅿️ Overflow review + 1. ✅ Implement `List` compatibility pack in `RecyclableList` + 1. 👉 Overflow review 1. 🅿️ Add type casting to `long` for `<<` & `>>` operations, where required - 1. 🅿️ Make type castings `checked` + 1. 🅿️ Make type castings `checked`, where required 1. 🅿️ Release 0.0.6 1. 🅿️ Implement `List` compatibility pack in `RecyclableLongList` 1. 🅿️ Port `RecyclableLongList` optimizations to `RecyclableList` @@ -198,6 +198,12 @@ 1. 🅿️ Pools 1. 🅿️ Multi-threading 1. 🅿️ Add comparison tests vs `T[]` / `List` equivalents + 1. 🅿️ Add test cases for indexes beyond items count + 1. 🅿️ Cover border cases + 1. 🅿️ Non-existing items in `.Contains...()`, `.Exists...()`, `.Find...()`, `.IndexOf...()`, `.Last...()` + 1. 🅿️ Duplicated items in `.Contains...()`, `.Exists...()`, `.Find...()`, `.IndexOf...()`, `.Last...()` + 1. 🅿️ Append test data in reversed order in `.AddRange()`, `.InsertRange()` and similar tests + 1. 🅿️ Switch tests to use Source / Target data variants in `.AddRange()`, `.InsertRange()` and similar tests 1. 🅿️ Cleanup 1. 🅿️ Replace `LastBlockWithData` property with `_lastBlockWithData` field 1. 🅿️ Replace all internal properties refs with fields, where possible diff --git a/Recyclable.Collections.Benchmarks/Benchmarks/BinarySearch_BestAndWorstCases.cs b/Recyclable.Collections.Benchmarks/Benchmarks/BinarySearch_BestAndWorstCases.cs new file mode 100644 index 0000000..3f5abd3 --- /dev/null +++ b/Recyclable.Collections.Benchmarks/Benchmarks/BinarySearch_BestAndWorstCases.cs @@ -0,0 +1,65 @@ +namespace Recyclable.Collections.Benchmarks +{ + public partial class RecyclableCollectionsBenchmarks + { + public void Array_BinarySearch_BestAndWorstCases() + { + var data = TestObjects; + var list = TestObjects; + var dataCount = TestObjectCountForSlowMethods; + for (var i = 0; i < dataCount; i++) + { + DoNothing(Array.BinarySearch(list, data[i])); + DoNothing(Array.BinarySearch(list, data[^(i + 1)])); + } + } + + public void List_BinarySearch_BestAndWorstCases() + { + var data = TestObjects; + var list = TestObjectsAsList; + var dataCount = TestObjectCountForSlowMethods; + for (var i = 0; i < dataCount; i++) + { + DoNothing(list.BinarySearch(data[i])); + DoNothing(list.BinarySearch(data[^(i + 1)])); + } + } + + public void PooledList_BinarySearch_BestAndWorstCases() + { + var data = TestObjects; + var list = TestObjectsAsPooledList; + var dataCount = TestObjectCountForSlowMethods; + for (var i = 0; i < dataCount; i++) + { + DoNothing(list.BinarySearch(data[i])); + DoNothing(list.BinarySearch(data[^(i + 1)])); + } + } + + public void RecyclableList_BinarySearch_BestAndWorstCases() + { + var data = TestObjects; + var list = TestObjectsAsRecyclableList; + var dataCount = TestObjectCountForSlowMethods; + for (var i = 0; i < dataCount; i++) + { + DoNothing(list.BinarySearch(data[i])); + DoNothing(list.BinarySearch(data[^(i + 1)])); + } + } + + // public void RecyclableLongList_BinarySearch_BestAndWorstCases() + // { + // var data = TestObjects; + // var list = TestObjectsAsRecyclableLongList; + // var dataCount = TestObjectCountForSlowMethods; + // for (var i = 0; i < dataCount; i++) + // { + // DoNothing(list.BinarySearch(data[i])); + // DoNothing(list.BinarySearch(data[^(i + 1)])); + // } + // } + } +} diff --git a/Recyclable.Collections.Benchmarks/Benchmarks/ConvertAll.cs b/Recyclable.Collections.Benchmarks/Benchmarks/ConvertAll.cs new file mode 100644 index 0000000..55ec249 --- /dev/null +++ b/Recyclable.Collections.Benchmarks/Benchmarks/ConvertAll.cs @@ -0,0 +1,48 @@ +namespace Recyclable.Collections.Benchmarks +{ + public partial class RecyclableCollectionsBenchmarks + { + public void Array_ConvertAll() + { + var data = TestObjects; + var list = TestObjects; + var dataCount = TestObjectCountForSlowMethods; + DoNothing(Array.ConvertAll(list, static (item) => (long) item)); + } + + public void List_ConvertAll() + { + var data = TestObjects; + var list = TestObjectsAsList; + var dataCount = TestObjectCountForSlowMethods; + DoNothing(list.ConvertAll(static (item) => (long)item)); + } + + public void PooledList_ConvertAll() + { + var data = TestObjects; + var list = TestObjectsAsPooledList; + var dataCount = TestObjectCountForSlowMethods; + using var converted = list.ConvertAll(static (item) => (long)item); + DoNothing(converted); + } + + public void RecyclableList_ConvertAll() + { + var data = TestObjects; + var list = TestObjectsAsRecyclableList; + var dataCount = TestObjectCountForSlowMethods; + using RecyclableList converted = list.ConvertAll(static (item) => (long)item); + DoNothing(converted); + } + + // public void RecyclableLongList_ConvertAll() + // { + // var data = TestObjects; + // var list = TestObjectsAsRecyclableLongList; + // var dataCount = TestObjectCountForSlowMethods; + // using RecyclableList converted = list.ConvertAll(static (item) => (long)item); + // DoNothing(converted); + // } + } +} diff --git a/Recyclable.Collections.Benchmarks/Benchmarks/Exists_BestAndWorstCases.cs b/Recyclable.Collections.Benchmarks/Benchmarks/Exists_BestAndWorstCases.cs new file mode 100644 index 0000000..f2d7b3f --- /dev/null +++ b/Recyclable.Collections.Benchmarks/Benchmarks/Exists_BestAndWorstCases.cs @@ -0,0 +1,65 @@ +namespace Recyclable.Collections.Benchmarks +{ + public partial class RecyclableCollectionsBenchmarks + { + public void Array_Exists_BestAndWorstCases() + { + var data = TestObjects; + var list = TestObjects; + var dataCount = TestObjectCountForSlowMethods; + for (var i = 0; i < dataCount; i++) + { + DoNothing(Array.Exists(list, (x) => x == data[i])); + DoNothing(Array.Exists(list, (x) => x == data[^(i + 1)])); + } + } + + public void List_Exists_BestAndWorstCases() + { + var data = TestObjects; + var list = TestObjectsAsList; + var dataCount = TestObjectCountForSlowMethods; + for (var i = 0; i < dataCount; i++) + { + DoNothing(list.Exists((x) => x == data[i])); + DoNothing(list.Exists((x) => x == data[^(i + 1)])); + } + } + + public void PooledList_Exists_BestAndWorstCases() + { + var data = TestObjects; + var list = TestObjectsAsPooledList; + var dataCount = TestObjectCountForSlowMethods; + for (var i = 0; i < dataCount; i++) + { + DoNothing(list.Exists((x) => x == data[i])); + DoNothing(list.Exists((x) => x == data[^(i + 1)])); + } + } + + public void RecyclableList_Exists_BestAndWorstCases() + { + var data = TestObjects; + var list = TestObjectsAsRecyclableList; + var dataCount = TestObjectCountForSlowMethods; + for (var i = 0; i < dataCount; i++) + { + DoNothing(list.Exists((x) => x == data[i])); + DoNothing(list.Exists((x) => x == data[^(i + 1)])); + } + } + + // public void RecyclableLongList_Exists_BestAndWorstCases() + // { + // var data = TestObjects; + // var list = TestObjectsAsRecyclableLongList; + // var dataCount = TestObjectCountForSlowMethods; + // for (var i = 0; i < dataCount; i++) + // { + // DoNothing(list.Exists((x) => x == data[i])); + // DoNothing(list.Exists((x) => x == data[^(i + 1)])); + // } + // } + } +} diff --git a/Recyclable.Collections.Benchmarks/Benchmarks/FindAll_BestAndWorstCases.cs b/Recyclable.Collections.Benchmarks/Benchmarks/FindAll_BestAndWorstCases.cs new file mode 100644 index 0000000..fb4443c --- /dev/null +++ b/Recyclable.Collections.Benchmarks/Benchmarks/FindAll_BestAndWorstCases.cs @@ -0,0 +1,80 @@ +namespace Recyclable.Collections.Benchmarks +{ + public partial class RecyclableCollectionsBenchmarks + { + public void Array_FindAll_BestAndWorstCases() + { + var data = TestObjects; + var list = TestObjects; + var dataCount = TestObjectCountForSlowMethods; + for (var i = 0; i < dataCount; i++) + { + DoNothing(Array.FindAll(list, (item) => item == data[i])); + DoNothing(Array.FindAll(list, (item) => item == data[^(i + 1)])); + } + } + + public void List_FindAll_BestAndWorstCases() + { + var data = TestObjects; + var list = TestObjectsAsList; + var dataCount = TestObjectCountForSlowMethods; + for (var i = 0; i < dataCount; i++) + { + DoNothing(list.FindAll((item) => item == data[i])); + DoNothing(list.FindAll((item) => item == data[^(i + 1)])); + } + } + + public void PooledList_FindAll_BestAndWorstCases() + { + var data = TestObjects; + var list = TestObjectsAsPooledList; + var dataCount = TestObjectCountForSlowMethods; + for (var i = 0; i < dataCount; i++) + { + var temp = list.FindAll((item) => item == data[i]); + DoNothing(temp); + temp.Dispose(); + + temp = list.FindAll((item) => item == data[^(i + 1)]); + DoNothing(temp); + temp.Dispose(); + } + } + + public void RecyclableList_FindAll_BestAndWorstCases() + { + var data = TestObjects; + var list = TestObjectsAsRecyclableList; + var dataCount = TestObjectCountForSlowMethods; + for (var i = 0; i < dataCount; i++) + { + var temp = list.FindAll((item) => item == data[i]); + DoNothing(temp); + temp.Dispose(); + + temp = list.FindAll((item) => item == data[^(i + 1)]); + DoNothing(temp); + temp.Dispose(); + } + } + + // public void RecyclableLongList_FindAll_BestAndWorstCases() + // { + // var data = TestObjects; + // var list = TestObjectsAsRecyclableLongList; + // var dataCount = TestObjectCountForSlowMethods; + // for (var i = 0; i < dataCount; i++) + // { + // var temp = list.FindAll((item) => item == data[i]); + // DoNothing(temp); + // temp.Dispose(); + + // temp = list.FindAll((item) => item == data[^(i + 1)]); + // DoNothing(temp); + // temp.Dispose(); + // } + // } + } +} diff --git a/Recyclable.Collections.Benchmarks/Benchmarks/FindLastIndex_BestAndWorstCases.cs b/Recyclable.Collections.Benchmarks/Benchmarks/FindLastIndex_BestAndWorstCases.cs new file mode 100644 index 0000000..b7338e1 --- /dev/null +++ b/Recyclable.Collections.Benchmarks/Benchmarks/FindLastIndex_BestAndWorstCases.cs @@ -0,0 +1,63 @@ +namespace Recyclable.Collections.Benchmarks +{ + public partial class RecyclableCollectionsBenchmarks + { + public void Array_FindLastIndex_BestAndWorstCases() + { + var data = TestObjects; + var list = TestObjects; + var dataCount = TestObjectCountForSlowMethods; + // Console.WriteLine($"{nameof(TestObjectCount)}={TestObjectCount}"); + // Console.WriteLine($"{nameof(TestObjectCountForSlowMethods)}={TestObjectCountForSlowMethods}"); + for (var i = 0; i < dataCount; i++) + { + // Console.WriteLine($"data.Length={data.Length}"); + // Console.WriteLine($"{nameof(TestObjectCount)}={TestObjectCount}"); + // Console.WriteLine($"{nameof(TestObjectCountForSlowMethods)}={TestObjectCountForSlowMethods}"); + DoNothing(Array.FindLastIndex(list, 0, (x) => x == data[i])); + DoNothing(Array.FindLastIndex(list, 0, (x) => x == data[^(i + 1)])); + } + } + + public void List_FindLastIndex_BestAndWorstCases() + { + var data = TestObjects; + var list = TestObjectsAsList; + var dataCount = TestObjectCountForSlowMethods; + // Console.WriteLine($"{nameof(TestObjectCount)}={TestObjectCount}"); + // Console.WriteLine($"{nameof(TestObjectCountForSlowMethods)}={TestObjectCountForSlowMethods}"); + for (var i = 0; i < dataCount; i++) + { + // Console.WriteLine($"data.Length={data.Length}"); + // Console.WriteLine($"{nameof(TestObjectCount)}={TestObjectCount}"); + // Console.WriteLine($"{nameof(TestObjectCountForSlowMethods)}={TestObjectCountForSlowMethods}"); + DoNothing(list.FindLastIndex(0, (x) => x == data[i])); + DoNothing(list.FindLastIndex(0, (x) => x == data[^(i + 1)])); + } + } + + public void RecyclableList_FindLastIndex_BestAndWorstCases() + { + var data = TestObjects; + var list = TestObjectsAsRecyclableList; + var dataCount = TestObjectCountForSlowMethods; + for (var i = 0; i < dataCount; i++) + { + DoNothing(list.FindLastIndex(0, (x) => x == data[i])); + DoNothing(list.FindLastIndex(0, (x) => x == data[^(i + 1)])); + } + } + + // public void RecyclableLongList_FindLastIndex_BestAndWorstCases() + // { + // var data = TestObjects; + // var list = TestObjectsAsRecyclableLongList; + // var dataCount = TestObjectCountForSlowMethods; + // for (var i = 0; i < dataCount; i++) + // { + // DoNothing(list.FindLastIndex(0, (x) => x == data[i])); + // DoNothing(list.FindLastIndex(0, (x) => x == data[^(i + 1)])); + // } + // } + } +} diff --git a/Recyclable.Collections.Benchmarks/Benchmarks/FindLast_BestAndWorstCases.cs b/Recyclable.Collections.Benchmarks/Benchmarks/FindLast_BestAndWorstCases.cs new file mode 100644 index 0000000..ef7ac79 --- /dev/null +++ b/Recyclable.Collections.Benchmarks/Benchmarks/FindLast_BestAndWorstCases.cs @@ -0,0 +1,53 @@ +namespace Recyclable.Collections.Benchmarks +{ + public partial class RecyclableCollectionsBenchmarks + { + public void Array_FindLast_BestAndWorstCases() + { + var data = TestObjects; + var list = TestObjects; + var dataCount = TestObjectCountForSlowMethods; + for (var i = 0; i < dataCount; i++) + { + DoNothing(Array.FindLast(list, (x) => x == data[i])); + DoNothing(Array.FindLast(list, (x) => x == data[^(i + 1)])); + } + } + + public void List_FindLast_BestAndWorstCases() + { + var data = TestObjects; + var list = TestObjectsAsList; + var dataCount = TestObjectCountForSlowMethods; + for (var i = 0; i < dataCount; i++) + { + DoNothing(list.FindLast((x) => x == data[i])); + DoNothing(list.FindLast((x) => x == data[^(i + 1)])); + } + } + + public void RecyclableList_FindLast_BestAndWorstCases() + { + var data = TestObjects; + var list = TestObjectsAsRecyclableList; + var dataCount = TestObjectCountForSlowMethods; + for (var i = 0; i < dataCount; i++) + { + DoNothing(list.FindLast((x) => x == data[i])); + DoNothing(list.FindLast((x) => x == data[^(i + 1)])); + } + } + + // public void RecyclableLongList_FindLast_BestAndWorstCases() + // { + // var data = TestObjects; + // var list = TestObjectsAsRecyclableLongList; + // var dataCount = TestObjectCountForSlowMethods; + // for (var i = 0; i < dataCount; i++) + // { + // DoNothing(list.FindLast((x) => x == data[i])); + // DoNothing(list.FindLast((x) => x == data[^(i + 1)])); + // } + // } + } +} diff --git a/Recyclable.Collections.Benchmarks/Benchmarks/Find_BestAndWorstCases.cs b/Recyclable.Collections.Benchmarks/Benchmarks/Find_BestAndWorstCases.cs new file mode 100644 index 0000000..4f677da --- /dev/null +++ b/Recyclable.Collections.Benchmarks/Benchmarks/Find_BestAndWorstCases.cs @@ -0,0 +1,53 @@ +namespace Recyclable.Collections.Benchmarks +{ + public partial class RecyclableCollectionsBenchmarks + { + public void Array_Find_BestAndWorstCases() + { + var data = TestObjects; + var list = TestObjects; + var dataCount = TestObjectCountForSlowMethods; + for (var i = 0; i < dataCount; i++) + { + DoNothing(Array.Find(list, (x) => x == data[i])); + DoNothing(Array.Find(list, (x) => x == data[^(i + 1)])); + } + } + + public void List_Find_BestAndWorstCases() + { + var data = TestObjects; + var list = TestObjectsAsList; + var dataCount = TestObjectCountForSlowMethods; + for (var i = 0; i < dataCount; i++) + { + DoNothing(list.Find((x) => x == data[i])); + DoNothing(list.Find((x) => x == data[^(i + 1)])); + } + } + + public void RecyclableList_Find_BestAndWorstCases() + { + var data = TestObjects; + var list = TestObjectsAsRecyclableList; + var dataCount = TestObjectCountForSlowMethods; + for (var i = 0; i < dataCount; i++) + { + DoNothing(list.Find((x) => x == data[i])); + DoNothing(list.Find((x) => x == data[^(i + 1)])); + } + } + + // public void RecyclableLongList_Find_BestAndWorstCases() + // { + // var data = TestObjects; + // var list = TestObjectsAsRecyclableLongList; + // var dataCount = TestObjectCountForSlowMethods; + // for (var i = 0; i < dataCount; i++) + // { + // DoNothing(list.Find((x) => x == data[i])); + // DoNothing(list.Find((x) => x == data[^(i + 1)])); + // } + // } + } +} diff --git a/Recyclable.Collections.Benchmarks/Benchmarks/RecyclableCollections.Benchmarks.cs b/Recyclable.Collections.Benchmarks/Benchmarks/RecyclableCollections.Benchmarks.cs index 904cdc3..55f0b46 100644 --- a/Recyclable.Collections.Benchmarks/Benchmarks/RecyclableCollections.Benchmarks.cs +++ b/Recyclable.Collections.Benchmarks/Benchmarks/RecyclableCollections.Benchmarks.cs @@ -6,9 +6,11 @@ namespace Recyclable.Collections.Benchmarks public enum RecyclableCollectionsBenchmarkType { Unknown, Add_WithCapacity, Add, AddRange_WithCapacity, AddRangeWhenSourceIsArray, AddRangeWhenSourceIsIEnumerable, - AddRangeWhenSourceIsIList, AddRangeWhenSourceIsList, AddRangeWhenSourceIsSameType, Contains_FirstItems, - Contains_LastItems, Count, Create_WithCapacity, Create, ForEach, GetItem, IndexOf_BestAndWorstCases, IndexOf_FirstItems, - IndexOf_LastItems, LongCount, Remove_FirstItems, Remove_LastItems, RemoveAt_FirstItems, RemoveAt_LastItems, + AddRangeWhenSourceIsIList, AddRangeWhenSourceIsList, AddRangeWhenSourceIsSameType, BinarySearch_BestAndWorstCases, + Contains_FirstItems, Contains_LastItems, ConvertAll, Count, Create_WithCapacity, Create, Exists_BestAndWorstCases, + Find_BestAndWorstCases, FindAll_BestAndWorstCases, FindLast_BestAndWorstCases, FindLastIndex_BestAndWorstCases, + ForEach, GetItem, IndexOf_BestAndWorstCases,IndexOf_FirstItems, IndexOf_LastItems, LongCount, + Remove_FirstItems, Remove_LastItems, RemoveAt_FirstItems, RemoveAt_LastItems, SetItem, VersionedForEach } @@ -31,11 +33,18 @@ namespace Recyclable.Collections.Benchmarks RecyclableCollectionsBenchmarkType.AddRangeWhenSourceIsIList, RecyclableCollectionsBenchmarkType.AddRangeWhenSourceIsList, RecyclableCollectionsBenchmarkType.AddRangeWhenSourceIsSameType, + RecyclableCollectionsBenchmarkType.BinarySearch_BestAndWorstCases, RecyclableCollectionsBenchmarkType.Contains_FirstItems, RecyclableCollectionsBenchmarkType.Contains_LastItems, + RecyclableCollectionsBenchmarkType.ConvertAll, RecyclableCollectionsBenchmarkType.Count, RecyclableCollectionsBenchmarkType.Create_WithCapacity, RecyclableCollectionsBenchmarkType.Create, + RecyclableCollectionsBenchmarkType.Exists_BestAndWorstCases, + RecyclableCollectionsBenchmarkType.Find_BestAndWorstCases, + RecyclableCollectionsBenchmarkType.FindAll_BestAndWorstCases, + RecyclableCollectionsBenchmarkType.FindLast_BestAndWorstCases, + RecyclableCollectionsBenchmarkType.FindLastIndex_BestAndWorstCases, RecyclableCollectionsBenchmarkType.ForEach, RecyclableCollectionsBenchmarkType.GetItem, RecyclableCollectionsBenchmarkType.IndexOf_BestAndWorstCases, diff --git a/Recyclable.Collections.Benchmarks/POC/PocBenchmarkBase.cs b/Recyclable.Collections.Benchmarks/POC/PocBenchmarkBase.cs index 5d2cbac..368112a 100644 --- a/Recyclable.Collections.Benchmarks/POC/PocBenchmarkBase.cs +++ b/Recyclable.Collections.Benchmarks/POC/PocBenchmarkBase.cs @@ -60,7 +60,7 @@ namespace Recyclable.Collections.Benchmarks.POC // [Params(RecyclableDefaults.MinItemsCountForParallelization * 3)] // [Params(1_048_576)] public virtual int TestObjectCount { get; set; } = 0; //8_192;//131_072; //1; //33_554_432; //524_288; //131_072; - public virtual int TestObjectCountForSlowMethods => (TestObjectCount / 2) + 1;// Math.Min(TestObjectCount, 131_072); + public virtual int TestObjectCountForSlowMethods { get; private set; } // [Params(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160, 170, 180, 190, 200)] // [Params(10, 20, 50, 100)] @@ -103,6 +103,8 @@ namespace Recyclable.Collections.Benchmarks.POC { Console.WriteLine($"******* SETTING UP TEST CASE FOR BENCHMARK {{{DataType}}} *******"); + TestObjectCountForSlowMethods = (TestObjectCount / 2) + 1; + BlockSize = TestObjectCount switch { 0 => 1, diff --git a/Recyclable.Collections.Benchmarks/POC/RecyclableLongListPoc.Benchmarks.cs b/Recyclable.Collections.Benchmarks/POC/RecyclableLongListPoc.Benchmarks.cs index e8c2abe..b9dfec1 100644 --- a/Recyclable.Collections.Benchmarks/POC/RecyclableLongListPoc.Benchmarks.cs +++ b/Recyclable.Collections.Benchmarks/POC/RecyclableLongListPoc.Benchmarks.cs @@ -5,24 +5,10 @@ using Recyclable.Collections.Benchmarks.Core; namespace Recyclable.Collections.Benchmarks.POC { - public enum RecyclableLongListPocBenchmarkType - { - EnsureCapacity_ByPowOf2, - EnsureCapacityV1_ByPowOf2, - EnsureCapacityV3_ByPowOf2, - } - [MemoryDiagnoser] - public partial class RecyclableLongListPocBenchmarks : PocBenchmarkBase + public partial class RecyclableLongListPocBenchmarks : PocBenchmarkBase { - [Params(RecyclableLongListPocBenchmarkType.EnsureCapacity_ByPowOf2)] - public override RecyclableLongListPocBenchmarkType DataType { get => base.DataType; set => base.DataType = value; } - - // [Params(BenchmarkType.Array, BenchmarkType.List, BenchmarkType.RecyclableList, BenchmarkType.RecyclableLongList)] - [Params(RecyclableLongListPocBenchmarkType.EnsureCapacityV1_ByPowOf2)] - public override RecyclableLongListPocBenchmarkType BaseDataType { get => base.BaseDataType; set => base.BaseDataType = value; } - - private long[]? _testArray; + private long[]? _testArray; public long[] TestObjects => _testArray ?? throw new NullReferenceException("Something is wrong and test objects are null"); public RecyclableLongList TestObjectsAsRecyclableLongList => _testRecyclableLongList ?? throw new NullReferenceException($"Something is wrong and {nameof(RecyclableLongList)} is null"); @@ -31,34 +17,12 @@ namespace Recyclable.Collections.Benchmarks.POC internal PooledList TestPooledList => _testPooledList ?? throw new NullReferenceException($"Something is wrong and {nameof(PooledList)} is null"); private static readonly MethodInfo _ensureCapacityNewFunc; - private static readonly MethodInfo _ensureCapacityV1Func; - private static readonly MethodInfo _ensureCapacityV3Func; static RecyclableLongListPocBenchmarks() { _ensureCapacityNewFunc = typeof(RecyclableLongList).GetMethod("EnsureCapacity", BindingFlags.Static | BindingFlags.NonPublic) ?? throw new NullReferenceException($"Method EnsureCapacity not found in class {nameof(RecyclableLongList)}"); - _ensureCapacityV1Func = typeof(RecyclableLongListV1).GetMethod("EnsureCapacity", BindingFlags.Instance | BindingFlags.NonPublic) - ?? throw new NullReferenceException($"Method EnsureCapacity not found in class {nameof(RecyclableLongListV1)}"); - - _ensureCapacityV3Func = typeof(RecyclableLongListV3).GetMethod("EnsureCapacity", BindingFlags.Static | BindingFlags.NonPublic) - ?? throw new NullReferenceException($"Method EnsureCapacity not found in class {nameof(RecyclableLongListV3)}"); - } - - public static void Run() - { - var benchmark = new RecyclableLongListPocBenchmarks(); - benchmark.Setup(); - // benchmark.RecyclableLongList_EnsureCapacity_ByPowOf2(); - // benchmark.RecyclableLongList_EnsureCapacityV1_ByPowOf2(); - // benchmark.RecyclableLongList_EnsureCapacityV2_ByPowOf2(); - // benchmark.RecyclableLongList_EnsureCapacityV3_ByPowOf2(); - // benchmark.RecyclableLongList_EnsureCapacity_ByBlockSize(); - // benchmark.RecyclableLongList_EnsureCapacityV1_ByBlockSize(); - // benchmark.RecyclableLongList_EnsureCapacityV2_ByBlockSize(); - // benchmark.RecyclableLongList_EnsureCapacityV3_ByBlockSize(); - benchmark.Cleanup(); } public override void Setup() @@ -67,7 +31,7 @@ namespace Recyclable.Collections.Benchmarks.POC _testArray = DataGenerator.EnumerateTestObjects(TestObjectCount); base.Setup(); - } + } public override void Cleanup() { @@ -79,18 +43,5 @@ namespace Recyclable.Collections.Benchmarks.POC base.Cleanup(); } - - protected override Action? GetTestMethod(RecyclableLongListPocBenchmarkType benchmarkType) => benchmarkType switch - { - RecyclableLongListPocBenchmarkType.EnsureCapacity_ByPowOf2 => () => RecyclableLongList_EnsureCapacity_ByPowOf2(), - RecyclableLongListPocBenchmarkType.EnsureCapacityV1_ByPowOf2 => () => RecyclableLongList_EnsureCapacityV1_ByPowOf2(), - RecyclableLongListPocBenchmarkType.EnsureCapacityV3_ByPowOf2 => () => RecyclableLongList_EnsureCapacityV3_ByPowOf2(), - _ => throw CreateUnknownBenchmarkTypeException(benchmarkType), - }; - - protected override void PrepareData(T benchmarkType) - { - base.PrepareData(benchmarkType); - } - } + } } diff --git a/Recyclable.Collections/CompareResult.cs b/Recyclable.Collections/CompareResult.cs new file mode 100644 index 0000000..633965d --- /dev/null +++ b/Recyclable.Collections/CompareResult.cs @@ -0,0 +1,9 @@ +namespace Recyclable.Collections +{ + public static class CompareResult + { + public const int XLessThanY = -1; + public const int XEqualY = 0; + public const int XGreaterThenY = 1; + } +} diff --git a/Recyclable.Collections/ComparisonToComparerAdapter.cs b/Recyclable.Collections/ComparisonToComparerAdapter.cs new file mode 100644 index 0000000..38582b0 --- /dev/null +++ b/Recyclable.Collections/ComparisonToComparerAdapter.cs @@ -0,0 +1,32 @@ +namespace Recyclable.Collections +{ + public readonly struct ComparisonToComparerAdapter : IComparer + { + private readonly Comparison _comparison; + + public ComparisonToComparerAdapter(Comparison comparison) + { + _comparison = comparison; + } + + public int Compare(T? x, T? y) + { + if (x == null && y == null) + { + return CompareResult.XEqualY; + } + else if (x != null && y == null) + { + return CompareResult.XGreaterThenY; + } + else if (y != null && x == null) + { + return CompareResult.XLessThanY; + } + else + { + return _comparison(x!, y!); + } + } + } +} diff --git a/Recyclable.Collections/Pools/RecyclableArrayPool.cs b/Recyclable.Collections/Pools/RecyclableArrayPool.cs index dfe5c66..cad307a 100644 --- a/Recyclable.Collections/Pools/RecyclableArrayPool.cs +++ b/Recyclable.Collections/Pools/RecyclableArrayPool.cs @@ -7,7 +7,12 @@ namespace Recyclable.Collections.Pools internal static readonly OneSizeArrayPool[] _pools = new OneSizeArrayPool[32]; public static T[] RentShared(int blockSize) => _pools[31 - BitOperations.LeadingZeroCount((uint)blockSize)].Rent(); public static void ReturnShared(in T[] memoryBlock, bool needsClearing) - => _pools[31 - BitOperations.LeadingZeroCount((uint)memoryBlock.Length)].ReturnUnchecked(memoryBlock, needsClearing); + { + if (BitOperations.IsPow2(memoryBlock.Length)) + { + _pools[31 - BitOperations.LeadingZeroCount((uint)memoryBlock.Length)].ReturnUnchecked(memoryBlock, needsClearing); + } + } public static ref OneSizeArrayPool Shared(int blockSize) => ref _pools[31 - BitOperations.LeadingZeroCount((uint)blockSize)]; diff --git a/Recyclable.Collections/RecyclableList.AddRange.cs b/Recyclable.Collections/RecyclableList.AddRange.cs index 2f153c0..7c605dd 100644 --- a/Recyclable.Collections/RecyclableList.AddRange.cs +++ b/Recyclable.Collections/RecyclableList.AddRange.cs @@ -1,48 +1,44 @@ -using System; using System.Collections; -using System.Collections.Generic; -using System.Linq; using System.Numerics; using System.Runtime.CompilerServices; -using System.Threading.Tasks; namespace Recyclable.Collections { - public static class zRecyclableListAddRange + public static class zRecyclableListAddRange { private static void AddRangeEnumerated(this RecyclableList list, IEnumerable items, int growByCount) { - int targetItemIndex = list._count; - Span memorySpan; - - int i; var enumerator = items.GetEnumerator(); - - int capacity = list._capacity; - memorySpan = new(list._memoryBlock); - if (enumerator.MoveNext()) + if (!enumerator.MoveNext()) { - int available = capacity - targetItemIndex; - do - { - if (targetItemIndex + growByCount > capacity) - { - capacity = RecyclableListHelpers.ResizeAndCopy(list, targetItemIndex, checked((int)BitOperations.RoundUpToPowerOf2((uint)(targetItemIndex + growByCount)))); - memorySpan = new(list._memoryBlock); - available = capacity - targetItemIndex; - } + return; + } - for (i = 0; i < available; i++) + int capacity = list._capacity, + targetItemIndex = list._count, + available = capacity - targetItemIndex, + i; + + Span memorySpan = new(list._memoryBlock); + do + { + if (targetItemIndex + growByCount > capacity) + { + capacity = RecyclableListHelpers.EnsureCapacity(list, targetItemIndex, checked((int)BitOperations.RoundUpToPowerOf2((uint)(targetItemIndex + growByCount)))); + memorySpan = new(list._memoryBlock); + available = capacity - targetItemIndex; + } + + for (i = 0; i < available; i++) + { + memorySpan[targetItemIndex++] = enumerator.Current; + if (!enumerator.MoveNext()) { - memorySpan[targetItemIndex++] = enumerator.Current; - if (!enumerator.MoveNext()) - { - break; - } + break; } } - while (i >= available); } + while (i >= available); list._capacity = capacity; list._count = targetItemIndex; @@ -50,7 +46,7 @@ namespace Recyclable.Collections private static void AddRangeWithKnownCount(this RecyclableList list, IEnumerable items, int currentItemsCount, int requiredAdditionalCapacity) { - list._capacity = RecyclableListHelpers.ResizeAndCopy(list, currentItemsCount, checked((int)BitOperations.RoundUpToPowerOf2((uint)(currentItemsCount + requiredAdditionalCapacity)))); + list._capacity = RecyclableListHelpers.EnsureCapacity(list, currentItemsCount, checked((int)BitOperations.RoundUpToPowerOf2((uint)(currentItemsCount + requiredAdditionalCapacity)))); Span memorySpan = new(list._memoryBlock); foreach (var item in items) @@ -66,10 +62,10 @@ namespace Recyclable.Collections { if (list._capacity < list._count + items.Length) { - list._capacity = RecyclableListHelpers.ResizeAndCopy(list, list._count, checked((int)BitOperations.RoundUpToPowerOf2((uint)(list._count + items.Length)))); + list._capacity = RecyclableListHelpers.EnsureCapacity(list, list._count, checked((int)BitOperations.RoundUpToPowerOf2((uint)(list._count + items.Length)))); } - Array.Copy(items, items.GetLowerBound(0), list._memoryBlock, list._count, items.LongLength); + Array.Copy(items, items.GetLowerBound(0), list._memoryBlock, list._count, items.Length); list._count += items.Length; list._version++; } @@ -79,7 +75,7 @@ namespace Recyclable.Collections { if (list._capacity < list._count + items.Length) { - list._capacity = RecyclableListHelpers.ResizeAndCopy(list, list._count, checked((int)BitOperations.RoundUpToPowerOf2((uint)(list._count + items.Length)))); + list._capacity = RecyclableListHelpers.EnsureCapacity(list, list._count, checked((int)BitOperations.RoundUpToPowerOf2((uint)(list._count + items.Length)))); } new Span(items).CopyTo(new Span(list._memoryBlock, list._count, items.Length)); @@ -92,7 +88,7 @@ namespace Recyclable.Collections { if (list._capacity < list._count + items.Length) { - list._capacity = RecyclableListHelpers.ResizeAndCopy(list, list._count, checked((int)BitOperations.RoundUpToPowerOf2((uint)(list._count + items.Length)))); + list._capacity = RecyclableListHelpers.EnsureCapacity(list, list._count, checked((int)BitOperations.RoundUpToPowerOf2((uint)(list._count + items.Length)))); } items.CopyTo(new Span(list._memoryBlock, list._count, items.Length)); @@ -105,7 +101,7 @@ namespace Recyclable.Collections { if (list._capacity < list._count + items.Length) { - list._capacity = RecyclableListHelpers.ResizeAndCopy(list, list._count, checked((int)BitOperations.RoundUpToPowerOf2((uint)(list._count + items.Length)))); + list._capacity = RecyclableListHelpers.EnsureCapacity(list, list._count, checked((int)BitOperations.RoundUpToPowerOf2((uint)(list._count + items.Length)))); } items.CopyTo(new Span(list._memoryBlock, list._count, items.Length)); @@ -118,7 +114,7 @@ namespace Recyclable.Collections { if (list._capacity < list._count + items.Count) { - list._capacity = RecyclableListHelpers.ResizeAndCopy(list, list._count, checked((int)BitOperations.RoundUpToPowerOf2((uint)(list._count + items.Count)))); + list._capacity = RecyclableListHelpers.EnsureCapacity(list, list._count, checked((int)BitOperations.RoundUpToPowerOf2((uint)(list._count + items.Count)))); } items.CopyTo(list._memoryBlock, list._count); @@ -131,7 +127,7 @@ namespace Recyclable.Collections { if (list._capacity < list._count + items.Count) { - list._capacity = RecyclableListHelpers.ResizeAndCopy(list, list._count, checked((int)BitOperations.RoundUpToPowerOf2((uint)(list._count + items.Count)))); + list._capacity = RecyclableListHelpers.EnsureCapacity(list, list._count, checked((int)BitOperations.RoundUpToPowerOf2((uint)(list._count + items.Count)))); } items.CopyTo(list._memoryBlock, list._count); @@ -144,7 +140,7 @@ namespace Recyclable.Collections { if (list._capacity < list._count + items.Count) { - list._capacity = RecyclableListHelpers.ResizeAndCopy(list, list._count, checked((int)BitOperations.RoundUpToPowerOf2((uint)(list._count + items.Count)))); + list._capacity = RecyclableListHelpers.EnsureCapacity(list, list._count, checked((int)BitOperations.RoundUpToPowerOf2((uint)(list._count + items.Count)))); } items.CopyTo(list._memoryBlock, list._count); @@ -157,7 +153,7 @@ namespace Recyclable.Collections { if (list._capacity < list._count + items._count) { - list._capacity = RecyclableListHelpers.ResizeAndCopy(list, list._count, checked((int)BitOperations.RoundUpToPowerOf2((uint)(list._count + items._count)))); + list._capacity = RecyclableListHelpers.EnsureCapacity(list, list._count, checked((int)BitOperations.RoundUpToPowerOf2((uint)(list._count + items._count)))); } new Span(items._memoryBlock, 0, items._count).CopyTo(new Span(list._memoryBlock, list._count, items._count)); @@ -183,7 +179,7 @@ namespace Recyclable.Collections int targetCapacity = oldCount + (int)sourceItemsCount; if (list._capacity < targetCapacity) { - list._capacity = RecyclableListHelpers.ResizeAndCopy(list, oldCount, checked((int)BitOperations.RoundUpToPowerOf2((uint)targetCapacity))); + list._capacity = RecyclableListHelpers.EnsureCapacity(list, oldCount, checked((int)BitOperations.RoundUpToPowerOf2((uint)targetCapacity))); } // TODO: Avoid unnecessary range operator - pass as arguments instead. @@ -233,7 +229,7 @@ namespace Recyclable.Collections { if (targetItemIndex + growByCount > capacity) { - capacity = RecyclableListHelpers.ResizeAndCopy(list, targetItemIndex, checked((int)BitOperations.RoundUpToPowerOf2((uint)(targetItemIndex + growByCount)))); + capacity = RecyclableListHelpers.EnsureCapacity(list, targetItemIndex, checked((int)BitOperations.RoundUpToPowerOf2((uint)(targetItemIndex + growByCount)))); memorySpan = new(list._memoryBlock); available = capacity - targetItemIndex; } @@ -260,7 +256,7 @@ namespace Recyclable.Collections int sourceItemsCount = items.Count; if (list._capacity < list._count + sourceItemsCount) { - list._capacity = RecyclableListHelpers.ResizeAndCopy(list, list._count, checked((int)BitOperations.RoundUpToPowerOf2((uint)(list._count + sourceItemsCount)))); + list._capacity = RecyclableListHelpers.EnsureCapacity(list, list._count, checked((int)BitOperations.RoundUpToPowerOf2((uint)(list._count + sourceItemsCount)))); } Span memorySpan = new(list._memoryBlock, list._count, sourceItemsCount); diff --git a/Recyclable.Collections/RecyclableList.Compatibility.List.Find.cs b/Recyclable.Collections/RecyclableList.Compatibility.List.Find.cs new file mode 100644 index 0000000..3ebd098 --- /dev/null +++ b/Recyclable.Collections/RecyclableList.Compatibility.List.Find.cs @@ -0,0 +1,271 @@ +using System.Numerics; + +namespace Recyclable.Collections +{ + public static class zRecyclableListCompatibilityListFind + { + public static bool Exists(this RecyclableList list, Predicate match) + { + int sourceItemsCount = list._count; + if (sourceItemsCount == 0) + { + return false; + } + + ReadOnlySpan sourceSpan = new(list._memoryBlock); + for (var itemIndex = 0; itemIndex < sourceItemsCount; itemIndex++) + { + if (match(sourceSpan[itemIndex])) + { + return true; + } + } + + return false; + } + + public static T? Find(this RecyclableList list, Predicate match) + { + int sourceItemsCount = list._count; + if (sourceItemsCount == 0) + { + return default; + } + + ReadOnlySpan sourceSpan = new(list._memoryBlock); + for (var itemIndex = 0; itemIndex < sourceItemsCount; itemIndex++) + { + if (match(sourceSpan[itemIndex])) + { + return sourceSpan[itemIndex]; + } + } + + return default; + } + + public static RecyclableList FindAll(this RecyclableList list, Predicate match) + { + int sourceItemsCount = list._count; + if (sourceItemsCount == 0) + { + return new(); + } + + ReadOnlySpan sourceSpan = new(list._memoryBlock); + RecyclableList result = new(sourceItemsCount >> 3); + Span resultSpan = result._memoryBlock; + int capacity = result._capacity, + resultItemsCount = 0; + + for (var itemIndex = 0; itemIndex < sourceItemsCount; itemIndex++) + { + if (match(sourceSpan[itemIndex])) + { + if (resultItemsCount + 1 > capacity) + { + capacity = checked((int)BitOperations.RoundUpToPowerOf2((uint)resultItemsCount + 1)); + _ = RecyclableListHelpers.EnsureCapacity(result, resultItemsCount, capacity); + resultSpan = result._memoryBlock; + } + + resultSpan[resultItemsCount++] = sourceSpan[itemIndex]; + } + } + + result._capacity = capacity; + result._count = resultItemsCount; + return result; + } + public static RecyclableList FindAllIndexes(this RecyclableList list, Predicate match) + { + int sourceItemsCount = list._count; + if (sourceItemsCount == 0) + { + return new(); + } + + ReadOnlySpan sourceSpan = new(list._memoryBlock); + RecyclableList result = new(sourceItemsCount >> 3); + Span resultSpan = result._memoryBlock; + int capacity = result._capacity, + resultItemsCount = 0; + + for (var itemIndex = 0; itemIndex < sourceItemsCount; itemIndex++) + { + if (match(sourceSpan[itemIndex])) + { + if (resultItemsCount + 1 > capacity) + { + capacity = checked((int)BitOperations.RoundUpToPowerOf2((uint)resultItemsCount + 1)); + _ = RecyclableListHelpers.EnsureCapacity(result, resultItemsCount, capacity); + resultSpan = result._memoryBlock; + } + + resultSpan[resultItemsCount++] = itemIndex; + } + } + + result._capacity = capacity; + result._count = resultItemsCount; + return result; + } + + public static int FindIndex(this RecyclableList list, int startIndex, int count, Predicate match) + { + int sourceItemsCount = list._count; + if (sourceItemsCount == 0 || startIndex >= sourceItemsCount) + { + return RecyclableDefaults.ItemNotFoundIndex; + } + + sourceItemsCount = Math.Min(sourceItemsCount, startIndex + sourceItemsCount); + ReadOnlySpan sourceSpan = new(list._memoryBlock); + for (var itemIndex = startIndex; itemIndex < sourceItemsCount; itemIndex++) + { + if (match(sourceSpan[itemIndex])) + { + return itemIndex; + } + } + + return RecyclableDefaults.ItemNotFoundIndex; + } + + public static int FindIndex(this RecyclableList list, int startIndex, Predicate match) + { + int sourceItemsCount = list._count; + if (sourceItemsCount == 0 || startIndex >= sourceItemsCount) + { + return RecyclableDefaults.ItemNotFoundIndex; + } + + sourceItemsCount = Math.Min(sourceItemsCount, startIndex + sourceItemsCount); + ReadOnlySpan sourceSpan = new(list._memoryBlock); + for (var itemIndex = startIndex; itemIndex < sourceItemsCount; itemIndex++) + { + if (match(sourceSpan[itemIndex])) + { + return itemIndex; + } + } + + return RecyclableDefaults.ItemNotFoundIndex; + } + + public static int FindIndex(this RecyclableList list, Predicate match) + { + int sourceItemsCount = list._count; + if (sourceItemsCount == 0) + { + return RecyclableDefaults.ItemNotFoundIndex; + } + + ReadOnlySpan sourceSpan = new(list._memoryBlock); + for (var itemIndex = 0; itemIndex < sourceItemsCount; itemIndex++) + { + if (match(sourceSpan[itemIndex])) + { + return itemIndex; + } + } + + return RecyclableDefaults.ItemNotFoundIndex; + } + + public static T? FindLast(this RecyclableList list, Predicate match) + { + if (list._count == 0) + { + return default; + } + + ReadOnlySpan sourceSpan = new(list._memoryBlock); + for (var itemIndex = list._count - 1; itemIndex >= 0; itemIndex--) + { + if (match(sourceSpan[itemIndex])) + { + return sourceSpan[itemIndex]; + } + } + + return default; + } + + public static int FindLastIndex(this RecyclableList list, int startIndex, int count, Predicate match) + { + if (count == 0) + { + if (startIndex != -1) + { + ThrowHelper.ThrowArgumentOutOfRangeException_Index(); + } + } + else if (count < 0 || startIndex - count + 1 < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeException_Count(); + } + + if (startIndex < 0) + { + return RecyclableDefaults.ItemNotFoundIndex; + } + + int sourceItemsCount = count; // startIndex - count + 1 >= 0 ? count : startIndex + 1; + if (sourceItemsCount == 0) + { + return RecyclableDefaults.ItemNotFoundIndex; + } + + int lastItemIndex = startIndex - sourceItemsCount; + ReadOnlySpan sourceSpan = new(list._memoryBlock); + for (var itemIndex = startIndex; itemIndex > lastItemIndex; itemIndex--) + { + if (match(sourceSpan[itemIndex])) + { + return itemIndex; + } + } + + return RecyclableDefaults.ItemNotFoundIndex; + } + + public static int FindLastIndex(this RecyclableList list, int startIndex, Predicate match) + { + if (list._count == 0 || startIndex < 0) + { + return RecyclableDefaults.ItemNotFoundIndex; + } + + ReadOnlySpan sourceSpan = new(list._memoryBlock); + for (var itemIndex = startIndex; itemIndex >= 0; itemIndex--) + { + if (match(sourceSpan[itemIndex])) + { + return itemIndex; + } + } + + return RecyclableDefaults.ItemNotFoundIndex; + } + + public static int FindLastIndex(this RecyclableList list, Predicate match) + { + if (list._count == 0) + { + return RecyclableDefaults.ItemNotFoundIndex; + } + + ReadOnlySpan sourceSpan = new(list._memoryBlock); + for (var itemIndex = list._count - 1; itemIndex >= 0; itemIndex--) + { + if (match(sourceSpan[itemIndex])) + { + return itemIndex; + } + } + + return RecyclableDefaults.ItemNotFoundIndex; + } + } +} diff --git a/Recyclable.Collections/RecyclableList.Compatibility.List.IndexOf.cs b/Recyclable.Collections/RecyclableList.Compatibility.List.IndexOf.cs new file mode 100644 index 0000000..f6f263b --- /dev/null +++ b/Recyclable.Collections/RecyclableList.Compatibility.List.IndexOf.cs @@ -0,0 +1,25 @@ +namespace Recyclable.Collections +{ + public static class zRecyclableListCompatibilityListIndexOf + { + public static int IndexOf(this RecyclableList list, T item, int index) => list._count != 0 + ? Array.IndexOf(list._memoryBlock, item, index, list._count - index) + : RecyclableDefaults.ItemNotFoundIndex; + + public static int IndexOf(this RecyclableList list, T item, int index, int count) => list._count != 0 + ? Array.IndexOf(list._memoryBlock, item, index, count) + : RecyclableDefaults.ItemNotFoundIndex; + + public static int LastIndexOf(this RecyclableList list, T item) => list._count != 0 + ? Array.LastIndexOf(list._memoryBlock, item, list._count - 1) + : RecyclableDefaults.ItemNotFoundIndex; + + public static int LastIndexOf(this RecyclableList list, T item, int index) => list._count != 0 + ? Array.LastIndexOf(list._memoryBlock, item, index) + : RecyclableDefaults.ItemNotFoundIndex; + + public static int LastIndexOf(this RecyclableList list, T item, int index, int count) => list._count != 0 + ? Array.LastIndexOf(list._memoryBlock, item, index, count) + : RecyclableDefaults.ItemNotFoundIndex; + } +} diff --git a/Recyclable.Collections/RecyclableList.Compatibility.List.InsertRange.cs b/Recyclable.Collections/RecyclableList.Compatibility.List.InsertRange.cs new file mode 100644 index 0000000..4022043 --- /dev/null +++ b/Recyclable.Collections/RecyclableList.Compatibility.List.InsertRange.cs @@ -0,0 +1,357 @@ +using System.Collections; +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace Recyclable.Collections +{ + public static class zRecyclableListCompatibilityListInsertRange + { + private static void InsertRangeEnumerated(this RecyclableList list, int index, IEnumerable items, int growByCount) + { + var enumerator = items.GetEnumerator(); + if (!enumerator.MoveNext()) + { + return; + } + + int capacity = list._capacity, + added = 0, + available = capacity - list._count, + i, + movingBlockLength = list._count - index; + + T[] memoryBlock = list._memoryBlock; + if (available > 0) + { + Array.Copy(memoryBlock, index, memoryBlock, capacity - movingBlockLength, movingBlockLength); + } + + Span memorySpan = new(memoryBlock); + do + { + if (index + available + movingBlockLength > capacity) + { + var capacityBeforeIncrease = capacity; + capacity = checked((int)BitOperations.RoundUpToPowerOf2((uint)(capacity + growByCount))); + _ = RecyclableListHelpers.EnsureCapacity(list, memoryBlock.Length, capacity); + memorySpan = new(memoryBlock = list._memoryBlock); + available = capacity - index - movingBlockLength; + Array.Copy(memoryBlock, capacityBeforeIncrease - movingBlockLength, memoryBlock, capacity - movingBlockLength, movingBlockLength); + } + + for (i = 0; i < available; i++) + { + memorySpan[index++] = enumerator.Current; + if (!enumerator.MoveNext()) + { + added++; + break; + } + } + + added += i; + } + while (i == available); + + if (i != available) + { + Array.Copy(memoryBlock, capacity - movingBlockLength, memoryBlock, index, movingBlockLength); + if (RecyclableList._needsClearing) + { + Array.Clear(memoryBlock, capacity - movingBlockLength, movingBlockLength); + } + } + + list._count += added; + list._capacity = capacity; + } + + private static void InsertRangeWithKnownCount(this RecyclableList list, int index, IEnumerable items, int currentItemsCount, int requiredAdditionalCapacity) + { + list._capacity = RecyclableListHelpers.EnsureCapacity(list, currentItemsCount, checked((int)BitOperations.RoundUpToPowerOf2((uint)(currentItemsCount + requiredAdditionalCapacity)))); + + Array.Copy(list._memoryBlock, index, list._memoryBlock, index + requiredAdditionalCapacity, list._count - index); + Span memorySpan = new(list._memoryBlock); + foreach (var item in items) + { + memorySpan[index++] = item; + } + + list._count += requiredAdditionalCapacity; + list._version++; + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + public static void InsertRange(this RecyclableList list, int index, in Array items) + { + if (list._capacity < list._count + items.Length) + { + list._capacity = RecyclableListHelpers.EnsureCapacity(list, list._count, checked((int)BitOperations.RoundUpToPowerOf2((uint)(list._count + items.Length)))); + } + + T[] memoryBlock = list._memoryBlock; + Array.Copy(memoryBlock, index, memoryBlock, index + items.Length, list._count - index); + Array.Copy(items, items.GetLowerBound(0), memoryBlock, index, items.Length); + list._count += items.Length; + list._version++; + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + public static void InsertRange(this RecyclableList list, int index, in T[] items) + { + if (list._capacity < list._count + items.Length) + { + list._capacity = RecyclableListHelpers.EnsureCapacity(list, list._count, checked((int)BitOperations.RoundUpToPowerOf2((uint)(list._count + items.Length)))); + } + + T[] memoryBlock = list._memoryBlock; + Array.Copy(memoryBlock, index, memoryBlock, index + items.Length, list._count - index); + new Span(items).CopyTo(new Span(memoryBlock, index, items.Length)); + list._count += items.Length; + list._version++; + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + public static void InsertRange(this RecyclableList list, int index, ReadOnlySpan items) + { + if (list._capacity < list._count + items.Length) + { + list._capacity = RecyclableListHelpers.EnsureCapacity(list, list._count, checked((int)BitOperations.RoundUpToPowerOf2((uint)(list._count + items.Length)))); + } + + T[] memoryBlock = list._memoryBlock; + Array.Copy(memoryBlock, index, memoryBlock, index + items.Length, list._count - index); + items.CopyTo(new Span(memoryBlock, index, items.Length)); + list._count += items.Length; + list._version++; + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + public static void InsertRange(this RecyclableList list, int index, Span items) + { + if (list._capacity < list._count + items.Length) + { + list._capacity = RecyclableListHelpers.EnsureCapacity(list, list._count, checked((int)BitOperations.RoundUpToPowerOf2((uint)(list._count + items.Length)))); + } + + T[] memoryBlock = list._memoryBlock; + Array.Copy(memoryBlock, index, memoryBlock, index + items.Length, list._count - index); + items.CopyTo(new Span(memoryBlock, index, items.Length)); + list._count += items.Length; + list._version++; + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + public static void InsertRange(this RecyclableList list, int index, List items) + { + if (list._capacity < list._count + items.Count) + { + list._capacity = RecyclableListHelpers.EnsureCapacity(list, list._count, checked((int)BitOperations.RoundUpToPowerOf2((uint)(list._count + items.Count)))); + } + + T[] memoryBlock = list._memoryBlock; + Array.Copy(memoryBlock, index, memoryBlock, index + items.Count, list._count - index); + items.CopyTo(memoryBlock, index); + list._count += items.Count; + list._version++; + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + public static void InsertRange(this RecyclableList list, int index, ICollection items) + { + if (list._capacity < list._count + items.Count) + { + list._capacity = RecyclableListHelpers.EnsureCapacity(list, list._count, checked((int)BitOperations.RoundUpToPowerOf2((uint)(list._count + items.Count)))); + } + + T[] memoryBlock = list._memoryBlock; + Array.Copy(memoryBlock, index, memoryBlock, index + items.Count, list._count - index); + items.CopyTo(list._memoryBlock, index); + list._count += items.Count; + list._version++; + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + public static void InsertRange(this RecyclableList list, int index, ICollection items) + { + if (list._capacity < list._count + items.Count) + { + list._capacity = RecyclableListHelpers.EnsureCapacity(list, list._count, checked((int)BitOperations.RoundUpToPowerOf2((uint)(list._count + items.Count)))); + } + + T[] memoryBlock = list._memoryBlock; + Array.Copy(memoryBlock, index, memoryBlock, index + items.Count, list._count - index); + items.CopyTo(memoryBlock, index); + list._count += items.Count; + list._version++; + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + public static void InsertRange(this RecyclableList list, int index, RecyclableList items) + { + if (list._capacity < list._count + items._count) + { + list._capacity = RecyclableListHelpers.EnsureCapacity(list, list._count, checked((int)BitOperations.RoundUpToPowerOf2((uint)(list._count + items._count)))); + } + + T[] memoryBlock = list._memoryBlock; + Array.Copy(memoryBlock, index, memoryBlock, index + items.Count, list._count - index); + new Span(items._memoryBlock, 0, items._count).CopyTo(new(list._memoryBlock, index, items._count)); + list._count += items._count; + list._version++; + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + public static void InsertRange(this RecyclableList list, int index, RecyclableLongList items) + { + if (items._longCount == 0) + { + return; + } + + int oldCount = list._count; + long sourceItemsCount = items._longCount; + if (oldCount + sourceItemsCount > int.MaxValue) + { + ThrowHelper.ThrowArgumentOutOfRangeException(nameof(items), $"The total number of items in source and target table exceeds the maximum capacity of {nameof(RecyclableList)}, equal {int.MaxValue}. Please consider using {nameof(RecyclableLongList)}, instead"); + } + + int targetCapacity = oldCount + (int)sourceItemsCount; + if (list._capacity < targetCapacity) + { + list._capacity = RecyclableListHelpers.EnsureCapacity(list, oldCount, checked((int)BitOperations.RoundUpToPowerOf2((uint)targetCapacity))); + } + + T[] memoryBlock = list._memoryBlock; + Array.Copy(memoryBlock, index, memoryBlock, index + items.Count, oldCount - index); + Span targetSpan = new(list._memoryBlock, index, list._capacity - index), + itemsSpan; + + int blockIndex, + sourceBlockSize = items._blockSize, + lastBlockIndex = (int)(sourceItemsCount >> items._blockSizePow2BitShift) - 1; + + for (blockIndex = 0; blockIndex <= lastBlockIndex; blockIndex++) + { + itemsSpan = new(items._memoryBlocks[blockIndex], 0, sourceBlockSize); + itemsSpan.CopyTo(targetSpan); + targetSpan = targetSpan[sourceBlockSize..]; + } + + if (blockIndex == 0) + { + itemsSpan = new(items._memoryBlocks[blockIndex], 0, (int)sourceItemsCount); + itemsSpan.CopyTo(targetSpan); + } + else if (blockIndex <= items._lastBlockWithData) + { + itemsSpan = new(items._memoryBlocks[blockIndex], 0, items._nextItemIndex > 0 ? items._nextItemIndex : sourceBlockSize); + itemsSpan.CopyTo(targetSpan); + } + + list._count = targetCapacity; + list._version++; + } + + public static IEnumerator InsertRange(this RecyclableList list, int index, IEnumerable source, int growByCount = RecyclableDefaults.MinPooledArrayLength) + { + int targetItemIndex = list._count; + Span memorySpan; + + int i; + var enumerator = source.GetEnumerator(); + + int capacity = list._capacity; + memorySpan = new(list._memoryBlock); + if (enumerator.MoveNext()) + { + int available = capacity - targetItemIndex; + do + { + if (targetItemIndex + growByCount > capacity) + { + capacity = RecyclableListHelpers.EnsureCapacity(list, targetItemIndex, checked((int)BitOperations.RoundUpToPowerOf2((uint)(targetItemIndex + growByCount)))); + memorySpan = new(list._memoryBlock); + available = capacity - targetItemIndex; + } + + for (i = 0; i < available; i++) + { + memorySpan[targetItemIndex++] = (T)enumerator.Current; + if (!enumerator.MoveNext()) + { + break; + } + } + } + while (i >= available); + } + + list._capacity = capacity; + list._count = targetItemIndex; + return enumerator; + } + + public static void InsertRange(this RecyclableList list, int index, IReadOnlyList items) + { + int sourceItemsCount = items.Count; + if (list._capacity < list._count + sourceItemsCount) + { + list._capacity = RecyclableListHelpers.EnsureCapacity(list, list._count, checked((int)BitOperations.RoundUpToPowerOf2((uint)(list._count + sourceItemsCount)))); + } + + Span memorySpan = new(list._memoryBlock, list._count, sourceItemsCount); + for (var sourceItemIndex = 0; sourceItemIndex < sourceItemsCount; sourceItemIndex++) + { + memorySpan[sourceItemIndex] = items[sourceItemIndex]; + } + + list._count += sourceItemsCount; + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + public static void InsertRange(this RecyclableList list, int index, IEnumerable items, int growByCount = RecyclableDefaults.MinPooledArrayLength) + { + if (items is RecyclableList sourceRecyclableList) + { + InsertRange(list, index, sourceRecyclableList); + } + else if (items is RecyclableLongList sourceRecyclableLongList) + { + InsertRange(list, index, sourceRecyclableLongList); + } + else if (items is T[] sourceArray) + { + InsertRange(list, index, sourceArray); + } + else if (items is Array sourceArrayWithObjects) + { + InsertRange(list, index, sourceArrayWithObjects); + } + else if (items is List sourceList) + { + InsertRange(list, index, sourceList); + } + else if (items is ICollection sourceICollection) + { + InsertRange(list, index, sourceICollection); + } + else if (items is ICollection sourceICollectionWithObjects) + { + InsertRange(list, index, sourceICollectionWithObjects); + } + else if (items is IReadOnlyList sourceIReadOnlyList) + { + InsertRange(list, index, sourceIReadOnlyList); + } + else if (items.TryGetNonEnumeratedCount(out var requiredAdditionalCapacity) && requiredAdditionalCapacity != 0) + { + InsertRangeWithKnownCount(list, index, items, list._count, requiredAdditionalCapacity); + } + else + { + InsertRangeEnumerated(list, index, items, growByCount); + } + } + } +} \ No newline at end of file diff --git a/Recyclable.Collections/RecyclableList.Compatibility.List.cs b/Recyclable.Collections/RecyclableList.Compatibility.List.cs new file mode 100644 index 0000000..94cad31 --- /dev/null +++ b/Recyclable.Collections/RecyclableList.Compatibility.List.cs @@ -0,0 +1,213 @@ +using System.Collections.ObjectModel; +using System.Numerics; +using Recyclable.Collections.Pools; + +namespace Recyclable.Collections +{ + + public static class zRecyclableListCompatibilityList + { + public static ReadOnlyCollection AsReadOnly(this RecyclableList list) => new(list); + + public static int BinarySearch(this RecyclableList list, T item) + => Array.BinarySearch(list._memoryBlock, 0, list._count, item); + + public static int BinarySearch(this RecyclableList list, T item, IComparer? comparer) + => Array.BinarySearch(list._memoryBlock, 0, list._count, item, comparer); + + public static int BinarySearch(this RecyclableList list, int index, int count, T item, IComparer? comparer) + => Array.BinarySearch(list._memoryBlock, index, count, item, comparer); + + public static RecyclableList ConvertAll(this RecyclableList list, Converter converter) + { + int itemsCount = list._count; + if (itemsCount == 0) + { + return new(); + } + + RecyclableList result = new(itemsCount); + TOutput[] resultSpan = result._memoryBlock; + ReadOnlySpan sourceSpan = list._memoryBlock; + for (int bufferIndex = 0; bufferIndex < itemsCount; bufferIndex++) + { + resultSpan[bufferIndex] = converter(sourceSpan[bufferIndex]); + } + + result._count = itemsCount; + return result; + } + + public static void CopyTo(this RecyclableList list, T[] array) => Array.Copy(list._memoryBlock, array, list._count); + + public static void CopyTo(this RecyclableList list, int index, T[] array, int arrayIndex, int count) => Array.Copy(list._memoryBlock, index, array, arrayIndex, count); + + public static void ForEach(this RecyclableList list, Action action) + { + int sourceItemsCount = list._count; + if (sourceItemsCount == 0) + { + return; + } + + ReadOnlySpan sourceSpan = new(list._memoryBlock); + for (var itemIndex = 0; itemIndex < sourceItemsCount; itemIndex++) + { + action(sourceSpan[itemIndex]); + } + } + + public static RecyclableList GetRange(this RecyclableList list, int index, int count) + { + int sourceItemsCount = index + count <= list._count ? count : list._count - index; + if (sourceItemsCount <= 0) + { + return new(); + } + + ReadOnlySpan sourceSpan = new(list._memoryBlock); + RecyclableList result = new(sourceItemsCount); + Span resultSpan = result._memoryBlock; + int resultItemsCount = 0; + sourceItemsCount += index; + for (var itemIndex = index; itemIndex < sourceItemsCount; itemIndex++) + { + resultSpan[resultItemsCount++] = sourceSpan[itemIndex]; + } + + result._count = resultItemsCount; + return result; + } + + public static int RemoveAll(this RecyclableList list, Predicate match) + { + using var allIndexes = list.FindAllIndexes(match); + int foundItemsCount = allIndexes._count; + if (foundItemsCount == 0) + { + return foundItemsCount; + } + + for (int removed = 0; removed < foundItemsCount; removed++) + { + var indexToRemove = allIndexes[removed] - removed; + Array.Copy(list._memoryBlock, indexToRemove + 1, list._memoryBlock, indexToRemove, list._count - indexToRemove - removed - 1); + } + + if (RecyclableList._needsClearing) + { + Array.Clear(list._memoryBlock, list._count - foundItemsCount, foundItemsCount); + } + + list._count -= foundItemsCount; + list._version++; + return foundItemsCount; + } + + public static void RemoveRange(this RecyclableList list, int index, int count) + { + int oldCount = list._count; + count = count - index + 1 <= oldCount ? count : oldCount - index + 1; + if (count == 0) + { + return; + } + + if (index + count < oldCount) + { + Array.Copy(list._memoryBlock, index + count, list._memoryBlock, index, oldCount - count - index); + } + + if (RecyclableList._needsClearing) + { + Array.Clear(list._memoryBlock, oldCount - count, count); + } + + list._count -= count; + list._version++; + } + + public static void Reverse(this RecyclableList list) + { + Array.Reverse(list._memoryBlock, 0, list._count); + list._version++; + } + + public static void Reverse(this RecyclableList list, int index, int count) + { + Array.Reverse(list._memoryBlock, index, count); + list._version++; + } + + public static void Sort(this RecyclableList list) + { + Array.Sort(list._memoryBlock, 0, list._count); + list._version++; + } + + public static void Sort(this RecyclableList list, Comparison comparison) + { + Array.Sort(list._memoryBlock, 0, list._count, new ComparisonToComparerAdapter(comparison)); + list._version++; + } + + public static void Sort(this RecyclableList list, IComparer? comparer) + { + Array.Sort(list._memoryBlock, 0, list._count, comparer); + list._version++; + } + + public static void Sort(this RecyclableList list, int index, int count, IComparer? comparer) + { + Array.Sort(list._memoryBlock, index, count, comparer); + list._version++; + } + + public static T[] ToArray(this RecyclableList list) + { + T[] result = new T[list._count]; + new Span(list._memoryBlock, 0, list._count).CopyTo(new Span(result)); + return result; + } + + public static void TrimExcess(this RecyclableList list) + { + var requiredCapacity = list._count >= RecyclableDefaults.InitialCapacity ? list._count : RecyclableDefaults.InitialCapacity; + + T[] oldMemoryBlock = list._memoryBlock; + T[] result = requiredCapacity >= RecyclableDefaults.MinPooledArrayLength && BitOperations.IsPow2(requiredCapacity) + ? RecyclableArrayPool.RentShared(requiredCapacity) + : new T[requiredCapacity]; + + new Span(oldMemoryBlock, 0, list._count).CopyTo(new(result)); + + if (oldMemoryBlock.Length >= RecyclableDefaults.MinPooledArrayLength) + { + RecyclableArrayPool.ReturnShared(oldMemoryBlock, RecyclableList._needsClearing); + } + + list._memoryBlock = result; + list._capacity = requiredCapacity; + } + + public static bool TrueForAll(this RecyclableList list, Predicate match) + { + int sourceItemsCount = list._count; + if (sourceItemsCount == 0) + { + return false; + } + + ReadOnlySpan sourceSpan = new(list._memoryBlock); + for (var itemIndex = 0; itemIndex < sourceItemsCount; itemIndex++) + { + if (!match(sourceSpan[itemIndex])) + { + return false; + } + } + + return true; + } + } +} diff --git a/Recyclable.Collections/RecyclableList.Helpers.cs b/Recyclable.Collections/RecyclableList.Helpers.cs index dd64d43..8ffb960 100644 --- a/Recyclable.Collections/RecyclableList.Helpers.cs +++ b/Recyclable.Collections/RecyclableList.Helpers.cs @@ -7,104 +7,10 @@ namespace Recyclable.Collections { private static readonly bool NeedsClearing = !typeof(T).IsValueType; - [Obsolete("This method MAY be removed in the near future.")] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool Contains(RecyclableList arrays, T item) => arrays.Any(x => x.Contains(item)); - - [Obsolete("This method MAY be removed in the near future.")] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void CopyTo(RecyclableList arrays, long startingIndex, int blockSize, int lastBlockSize, T[] destinationArray, int destinationArrayIndex) - { - Span arrayMemory = destinationArray.AsSpan(); - arrayMemory = arrayMemory[destinationArrayIndex..]; - // TODO: Convert "/" & "%" to bit-shifting - int startingArrayIndex = (int)(startingIndex / blockSize); - startingIndex %= blockSize; - if (arrays._count > 0) - { - ReadOnlySpan sourceMemory = arrays[0].AsSpan(); - var maxToCopy = (int)Math.Min(blockSize - startingIndex, arrayMemory.Length); - if (arrays._count == 1) - { - maxToCopy = Math.Min(maxToCopy, lastBlockSize); - } - - sourceMemory = sourceMemory[(int)startingIndex..maxToCopy]; - sourceMemory.CopyTo(arrayMemory); - arrayMemory = arrayMemory[maxToCopy..]; - } - - for (var arrayIdx = startingArrayIndex + 1; arrayIdx < arrays._count - 1 && arrayMemory.Length > 0; arrayIdx++) - { - ReadOnlySpan sourceMemory = arrays[arrayIdx].AsSpan(); - var maxToCopy = Math.Min(blockSize, arrayMemory.Length); - sourceMemory = sourceMemory[..maxToCopy]; - sourceMemory.CopyTo(arrayMemory); - arrayMemory = arrayMemory[maxToCopy..]; - } - - if (arrays._count > 1 && arrayMemory.Length > 0) - { - ReadOnlySpan sourceMemory = arrays[^1].AsSpan(); - var maxToCopy = Math.Min(lastBlockSize, arrayMemory.Length); - sourceMemory = sourceMemory[..maxToCopy]; - sourceMemory.CopyTo(arrayMemory); - } - } - - [Obsolete("This method WILL be removed in the near future. Please use the existing enumerators instead of it.")] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static IEnumerable Enumerate(RecyclableList arrays, int chunkSize, long totalCount) - { - long currentCount = 0; - for (var arrayIdx = 0; (arrayIdx < arrays._count) && (currentCount < totalCount); arrayIdx++) - { - var array = arrays[arrayIdx]; - currentCount += Math.Min(chunkSize, totalCount); - switch (currentCount < totalCount) - { - case true: - for (var valueIdx = 0; valueIdx < chunkSize; valueIdx++) - { - yield return array[valueIdx]; - } - - break; - - case false: - var partialCount = (int)(totalCount % chunkSize); - int maxCount = partialCount > 0 ? partialCount : chunkSize; - for (var valueIdx = 0; valueIdx < maxCount; valueIdx++) - { - yield return array[valueIdx]; - } - - break; - } - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static long LongIndexOf(RecyclableList arrays, int blockSize, T item, IEqualityComparer comparer) - { - for (var arrayIdx = 0; arrayIdx < arrays._count; arrayIdx++) - { - var array = arrays[arrayIdx]; - Span arrayMemory = array.AsSpan(); - for (int memoryIdx = 0; memoryIdx < arrayMemory.Length; memoryIdx++) - { - if (comparer.Equals(arrayMemory[memoryIdx], item)) - { - return (arrayIdx * (long)blockSize) + memoryIdx; - } - } - } - - return -1; - } + public static int CalculateIndex(int index, int lowerBound) => index >= 0 ? index + lowerBound : RecyclableDefaults.ItemNotFoundIndex; [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] - public static void ResizeAndCopy(RecyclableList list) + public static void EnsureCapacity(RecyclableList list) { // TODO: Measure performance // T[] newMemoryBlock = RecyclableListHelpers._arrayPool.Rent(_capacity <<= 1); @@ -129,14 +35,38 @@ namespace Recyclable.Collections } [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] - public static int ResizeAndCopy(RecyclableList sourceList, int oldSize, int newSize) + public static int EnsureCapacity(RecyclableList sourceList, int newSize) { T[] oldMemoryBlock = sourceList._memoryBlock; sourceList._memoryBlock = newSize >= RecyclableDefaults.MinPooledArrayLength ? RecyclableArrayPool.RentShared(newSize) : new T[newSize]; - // & WAS SLOWER WITHOUT + // & WAS SLOWER AS ARRAY + new Span(oldMemoryBlock, 0, sourceList._capacity).CopyTo(sourceList._memoryBlock); + + if (oldMemoryBlock.Length >= RecyclableDefaults.MinPooledArrayLength) + { + // TODO: Measure gain vs relying on arrayPool to clear + //if (NeedsClearing) + //{ + // Array.Clear(source); + //} + + // If anything, it has been already cleared above, so we don't need to repeat it. + RecyclableArrayPool.ReturnShared(oldMemoryBlock, NeedsClearing); + } + + return newSize; + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + public static int EnsureCapacity(RecyclableList sourceList, int oldSize, int newSize) + { + T[] oldMemoryBlock = sourceList._memoryBlock; + sourceList._memoryBlock = newSize >= RecyclableDefaults.MinPooledArrayLength + ? RecyclableArrayPool.RentShared(newSize) + : new T[newSize]; // & WAS SLOWER AS ARRAY new Span(oldMemoryBlock, 0, oldSize).CopyTo(sourceList._memoryBlock); diff --git a/Recyclable.Collections/RecyclableList.cs b/Recyclable.Collections/RecyclableList.cs index cc90c49..9812d49 100644 --- a/Recyclable.Collections/RecyclableList.cs +++ b/Recyclable.Collections/RecyclableList.cs @@ -8,7 +8,7 @@ namespace Recyclable.Collections [Serializable] public sealed partial class RecyclableList : IList, IReadOnlyList, IDisposable { - private static readonly bool _needsClearing = !typeof(T).IsValueType; + internal static readonly bool _needsClearing = !typeof(T).IsValueType; public static explicit operator ReadOnlySpan(RecyclableList source) => new(source._memoryBlock, 0, source._count); @@ -41,6 +41,22 @@ namespace Recyclable.Collections } } + public RecyclableList(int initialCapacity, int lowerBound) + { + if (initialCapacity >= RecyclableDefaults.InitialCapacity) + { + _memoryBlock = initialCapacity >= RecyclableDefaults.MinPooledArrayLength + ? RecyclableArrayPool.RentShared(checked((int)BitOperations.RoundUpToPowerOf2((uint)initialCapacity))) + : new T[initialCapacity]; + _capacity = _memoryBlock.Length; + } + else + { + _memoryBlock = new T[RecyclableDefaults.InitialCapacity]; + _capacity = RecyclableDefaults.InitialCapacity; + } + } + public RecyclableList(in Array source) { _capacity = RecyclableDefaults.InitialCapacity; @@ -168,14 +184,30 @@ namespace Recyclable.Collections { if (_capacity != value) { - _capacity = RecyclableListHelpers.ResizeAndCopy(this, _count, checked((int)BitOperations.RoundUpToPowerOf2((uint)value))); + _capacity = RecyclableListHelpers.EnsureCapacity(this, _count, checked((int)BitOperations.RoundUpToPowerOf2((uint)value))); _version++; } } } internal int _count; - public int Count => _count; + public int Count + { + get => _count; + set + { + if (_count != value) + { + if (value > _capacity) + { + _capacity = RecyclableListHelpers.EnsureCapacity(this, _capacity, checked((int)BitOperations.RoundUpToPowerOf2((uint)value))); + } + + _count = value; + _version++; + } + } + } public bool IsReadOnly => false; @@ -184,7 +216,14 @@ namespace Recyclable.Collections { if (_count >= _capacity) { - RecyclableListHelpers.ResizeAndCopy(this); + if (BitOperations.IsPow2(_capacity)) + { + RecyclableListHelpers.EnsureCapacity(this); + } + else + { + _capacity = RecyclableListHelpers.EnsureCapacity(this, (int)BitOperations.RoundUpToPowerOf2((uint)_capacity)); + } } _memoryBlock[_count++] = item; @@ -217,7 +256,9 @@ namespace Recyclable.Collections public void CopyTo(T[] array, int index) => Array.Copy(_memoryBlock, 0, array, index, _count); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public int IndexOf(T itemToFind) => _count != 0 ? Array.IndexOf(_memoryBlock, itemToFind, 0, _count) : RecyclableDefaults.ItemNotFoundIndex; + public int IndexOf(T itemToFind) => _count != 0 + ? Array.IndexOf(_memoryBlock, itemToFind, 0, _count) + : RecyclableDefaults.ItemNotFoundIndex; [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] public void Insert(int index, T item) @@ -225,11 +266,13 @@ namespace Recyclable.Collections int oldCount = _count; if (_capacity < oldCount + 1) { - _capacity = RecyclableListHelpers.ResizeAndCopy(this, oldCount, checked((int)BitOperations.RoundUpToPowerOf2((uint)oldCount + 1))); + _capacity = RecyclableListHelpers.EnsureCapacity(this, oldCount, checked((int)BitOperations.RoundUpToPowerOf2((uint)oldCount + 1))); } if (index < oldCount) { + // TODO: Compare performance + // Array.Copy(_memoryBlock, index, _memoryBlock, index + 1, oldCount); new Span(_memoryBlock, index, oldCount -= index) .CopyTo(new Span(_memoryBlock, index + 1, oldCount)); } diff --git a/Recyclable.Collections/RecyclableQueue.Helpers.cs b/Recyclable.Collections/RecyclableQueue.Helpers.cs index 4b11912..45c0edf 100644 --- a/Recyclable.Collections/RecyclableQueue.Helpers.cs +++ b/Recyclable.Collections/RecyclableQueue.Helpers.cs @@ -2,12 +2,109 @@ using System.Runtime.CompilerServices; namespace Recyclable.Collections { - internal static class RecyclableQueueHelpers + internal static class RecyclableQueueHelpers { + [Obsolete("This method MAY be removed in the near future.")] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static bool Contains(RecyclableList arrays, T item) => arrays.Any(x => x.Contains(item)); + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int ToArrayIndex(long index, int blockSize) => (int)(index / blockSize); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static long ToItemIndex(long index, int blockSize) => index % blockSize; + + [Obsolete("This method MAY be removed in the near future.")] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void CopyTo(RecyclableList arrays, long startingIndex, int blockSize, int lastBlockSize, T[] destinationArray, int destinationArrayIndex) + { + Span arrayMemory = destinationArray.AsSpan(); + arrayMemory = arrayMemory[destinationArrayIndex..]; + // TODO: Convert "/" & "%" to bit-shifting + int startingArrayIndex = (int)(startingIndex / blockSize); + startingIndex %= blockSize; + if (arrays._count > 0) + { + ReadOnlySpan sourceMemory = arrays[0].AsSpan(); + var maxToCopy = (int)Math.Min(blockSize - startingIndex, arrayMemory.Length); + if (arrays._count == 1) + { + maxToCopy = Math.Min(maxToCopy, lastBlockSize); + } + + sourceMemory = sourceMemory[(int)startingIndex..maxToCopy]; + sourceMemory.CopyTo(arrayMemory); + arrayMemory = arrayMemory[maxToCopy..]; + } + + for (var arrayIdx = startingArrayIndex + 1; arrayIdx < arrays._count - 1 && arrayMemory.Length > 0; arrayIdx++) + { + ReadOnlySpan sourceMemory = arrays[arrayIdx].AsSpan(); + var maxToCopy = Math.Min(blockSize, arrayMemory.Length); + sourceMemory = sourceMemory[..maxToCopy]; + sourceMemory.CopyTo(arrayMemory); + arrayMemory = arrayMemory[maxToCopy..]; + } + + if (arrays._count > 1 && arrayMemory.Length > 0) + { + ReadOnlySpan sourceMemory = arrays[^1].AsSpan(); + var maxToCopy = Math.Min(lastBlockSize, arrayMemory.Length); + sourceMemory = sourceMemory[..maxToCopy]; + sourceMemory.CopyTo(arrayMemory); + } + } + + [Obsolete("This method WILL be removed in the near future. Please use the existing enumerators instead of it.")] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static IEnumerable Enumerate(RecyclableList arrays, int chunkSize, long totalCount) + { + long currentCount = 0; + for (var arrayIdx = 0; (arrayIdx < arrays._count) && (currentCount < totalCount); arrayIdx++) + { + var array = arrays[arrayIdx]; + currentCount += Math.Min(chunkSize, totalCount); + switch (currentCount < totalCount) + { + case true: + for (var valueIdx = 0; valueIdx < chunkSize; valueIdx++) + { + yield return array[valueIdx]; + } + + break; + + case false: + var partialCount = (int)(totalCount % chunkSize); + int maxCount = partialCount > 0 ? partialCount : chunkSize; + for (var valueIdx = 0; valueIdx < maxCount; valueIdx++) + { + yield return array[valueIdx]; + } + + break; + } + } + } + + [Obsolete] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static long LongIndexOf(RecyclableList arrays, int blockSize, T item, IEqualityComparer comparer) + { + for (var arrayIdx = 0; arrayIdx < arrays._count; arrayIdx++) + { + var array = arrays[arrayIdx]; + Span arrayMemory = array.AsSpan(); + for (int memoryIdx = 0; memoryIdx < arrayMemory.Length; memoryIdx++) + { + if (comparer.Equals(arrayMemory[memoryIdx], item)) + { + return (arrayIdx * (long)blockSize) + memoryIdx; + } + } + } + + return -1; + } } } diff --git a/Recyclable.Collections/RecyclableQueue.cs b/Recyclable.Collections/RecyclableQueue.cs index 4153c1e..7e12a09 100644 --- a/Recyclable.Collections/RecyclableQueue.cs +++ b/Recyclable.Collections/RecyclableQueue.cs @@ -99,8 +99,8 @@ namespace Recyclable.Collections private void SetItem(long index, T value) { long absoluteIndex = GetAbsoluteIndex(index, RemovedCount); - int arrayIndex = RecyclableQueueHelpers.ToArrayIndex(absoluteIndex, BlockSize); - long itemIndex = RecyclableQueueHelpers.ToItemIndex(absoluteIndex, BlockSize); + int arrayIndex = RecyclableQueueHelpers.ToArrayIndex(absoluteIndex, BlockSize); + long itemIndex = RecyclableQueueHelpers.ToItemIndex(absoluteIndex, BlockSize); Memory[arrayIndex][itemIndex] = value; } @@ -108,8 +108,8 @@ namespace Recyclable.Collections private T GetItem(long index) { long absoluteIndex = GetAbsoluteIndex(index, RemovedCount); - int arrayIndex = RecyclableQueueHelpers.ToArrayIndex(absoluteIndex, BlockSize); - long itemIndex = RecyclableQueueHelpers.ToItemIndex(absoluteIndex, BlockSize); + int arrayIndex = RecyclableQueueHelpers.ToArrayIndex(absoluteIndex, BlockSize); + long itemIndex = RecyclableQueueHelpers.ToItemIndex(absoluteIndex, BlockSize); return Memory[arrayIndex][itemIndex]; } @@ -166,13 +166,13 @@ namespace Recyclable.Collections } #pragma warning disable CS0618 - public bool Contains(T item) => RecyclableListHelpers.Contains(Memory, item); - public void CopyTo(T[] array, int arrayIndex) => RecyclableListHelpers.CopyTo(Memory, RemovedCount, BlockSize, (int)(LongCount % BlockSize), array, arrayIndex); - public IEnumerable Enumerate() => RecyclableListHelpers.Enumerate(Memory, BlockSize, LongCount); - public IEnumerator GetEnumerator() => RecyclableListHelpers.Enumerate(Memory, BlockSize, LongCount).GetEnumerator(); - IEnumerator IEnumerable.GetEnumerator() => RecyclableListHelpers.Enumerate(Memory, BlockSize, LongCount).GetEnumerator(); - public int IndexOf(T item) => (int)GetRelativeIndex(RecyclableListHelpers.LongIndexOf(Memory, BlockSize, item, _equalityComparer)); - public long LongIndexOf(T item) => GetRelativeIndex(RecyclableListHelpers.LongIndexOf(Memory, BlockSize, item, _equalityComparer)); + public bool Contains(T item) => RecyclableQueueHelpers.Contains(Memory, item); + public void CopyTo(T[] array, int arrayIndex) => RecyclableQueueHelpers.CopyTo(Memory, RemovedCount, BlockSize, (int)(LongCount % BlockSize), array, arrayIndex); + public IEnumerable Enumerate() => RecyclableQueueHelpers.Enumerate(Memory, BlockSize, LongCount); + public IEnumerator GetEnumerator() => RecyclableQueueHelpers.Enumerate(Memory, BlockSize, LongCount).GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => RecyclableQueueHelpers.Enumerate(Memory, BlockSize, LongCount).GetEnumerator(); + public int IndexOf(T item) => (int)GetRelativeIndex(RecyclableQueueHelpers.LongIndexOf(Memory, BlockSize, item, _equalityComparer)); + public long LongIndexOf(T item) => GetRelativeIndex(RecyclableQueueHelpers.LongIndexOf(Memory, BlockSize, item, _equalityComparer)); #pragma warning restore CS0618 public void Insert(int index, T item) => throw new NotSupportedException(); diff --git a/Recyclable.Collections/ThrowHelper.cs b/Recyclable.Collections/ThrowHelper.cs index 8350947..d50e7e3 100644 --- a/Recyclable.Collections/ThrowHelper.cs +++ b/Recyclable.Collections/ThrowHelper.cs @@ -8,15 +8,9 @@ namespace Recyclable.Collections /// This is a helper method to throw with the given message. /// /// Always - public static void ThrowArgumentOutOfRangeException(string argumentName, string message) - { - Contract.EnsuresOnThrow(false); - 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(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"); } } diff --git a/Recyclable.CollectionsTests/RecyclableListTests.cs b/Recyclable.CollectionsTests/RecyclableListTests.cs index c75652c..1997e88 100644 --- a/Recyclable.CollectionsTests/RecyclableListTests.cs +++ b/Recyclable.CollectionsTests/RecyclableListTests.cs @@ -1,7 +1,9 @@ using FluentAssertions; using Recyclable.Collections; using Recyclable.Collections.Linq; +using Recyclable.Collections.Pools; using System.Collections; +using System.Numerics; #pragma warning disable xUnit1026, RCS1163, IDE0060, RCS1235 @@ -14,12 +16,17 @@ namespace Recyclable.CollectionsTests { // Prepare using var list = new RecyclableList(); + long?[] testData = new long?[] { null, default }; // Act - list.AddRange(new long?[] { null, default }); + list.AddRange(testData); // Validate - _ = list.Should().HaveCount(2).And.AllSatisfy(x => x.Should().BeNull()); + List expectedData = new(); + expectedData.AddRange(testData); + + _ = list.Count.Should().Be(testData.Length); + _ = list.Should().HaveCount(testData.Length).And.AllSatisfy(x => x.Should().BeNull()).And.Equal(expectedData); } [Theory] @@ -44,7 +51,7 @@ namespace Recyclable.CollectionsTests } else if (testCase.Contains("IEnumerable[", StringComparison.OrdinalIgnoreCase)) { - list.AddRange((IEnumerable)testData); + _ = list.AddRange((IEnumerable)testData); } else if (testCase.Contains("IReadOnlyList[", StringComparison.OrdinalIgnoreCase)) { @@ -88,9 +95,12 @@ namespace Recyclable.CollectionsTests } // Validate + List expectedData = new(itemsCount); + expectedData.AddRange(testData); + _ = list.Capacity.Should().BeGreaterThanOrEqualTo(itemsCount); _ = list.Count.Should().Be(itemsCount); - _ = list.Should().Equal(testData); + _ = list.Should().Equal(expectedData); } [Theory] @@ -115,7 +125,7 @@ namespace Recyclable.CollectionsTests } else if (testCase.Contains("IEnumerable[", StringComparison.OrdinalIgnoreCase)) { - list.AddRange((IEnumerable)testData); + _ = list.AddRange((IEnumerable)testData); } else if (testCase.Contains("IReadOnlyList[", StringComparison.OrdinalIgnoreCase)) { @@ -159,9 +169,12 @@ namespace Recyclable.CollectionsTests } // Validate + List expectedData = new(itemsCount); + expectedData.AddRange(testData); + _ = list.Capacity.Should().Be(RecyclableDefaults.InitialCapacity); _ = list.Count.Should().Be(itemsCount); - _ = list.Should().BeEmpty(); + _ = list.Should().Equal(expectedData); } [Theory] @@ -170,6 +183,7 @@ namespace Recyclable.CollectionsTests { // Prepare using var list = new RecyclableList(); + itemsCount <<= 1; // Act if (testCase.Contains("Array[", StringComparison.OrdinalIgnoreCase)) @@ -189,8 +203,8 @@ namespace Recyclable.CollectionsTests } else if (testCase.Contains("IEnumerable[", StringComparison.OrdinalIgnoreCase)) { - list.AddRange((IEnumerable)testData); - list.AddRange((IEnumerable)testData); + _ = list.AddRange((IEnumerable)testData); + _ = list.AddRange((IEnumerable)testData); } else if (testCase.Contains("IReadOnlyList[", StringComparison.OrdinalIgnoreCase)) { @@ -245,9 +259,12 @@ namespace Recyclable.CollectionsTests } // Validate - var expectedData = testData.Concat(testData); - _ = list.Capacity.Should().BeGreaterThanOrEqualTo(itemsCount * 2); - _ = list.Count.Should().Be(itemsCount * 2); + List expectedData = new(itemsCount); + expectedData.AddRange(testData); + expectedData.AddRange(testData); + + _ = list.Capacity.Should().BeGreaterThanOrEqualTo(itemsCount); + _ = list.Count.Should().Be(itemsCount); _ = list.Should().Equal(expectedData); } @@ -256,36 +273,43 @@ namespace Recyclable.CollectionsTests public void AddShouldAcceptDuplicates(string testCase, IEnumerable testData, int itemsCount) { // Prepare - testData = testData.Concat(testData); + using var list = new RecyclableList(); + testData = testData.Concat(testData.Reverse()); + itemsCount <<= 1; // Act - using var list = new RecyclableList(); foreach (var index in testData) { list.Add(index); } // Validate - _ = list.Capacity.Should().BeGreaterThanOrEqualTo(itemsCount * 2); - _ = list.Count.Should().Be(itemsCount * 2); - _ = list.Should().Equal(testData); + List expectedData = new(itemsCount); + expectedData.AddRange(testData); + + _ = list.Capacity.Should().BeGreaterThanOrEqualTo(itemsCount); + _ = list.Count.Should().Be(itemsCount); + _ = list.Should().Equal(expectedData); } [Fact] public void AddShouldAcceptNulls() { // Prepare -#pragma warning disable IDE0028 + long?[] testData = new long?[] { null, default }; using var list = new RecyclableList(); // Act - list.Add(null); - list.Add(default); -#pragma warning restore IDE0028 + foreach (var item in testData) + { + list.Add(item); + } // Validate + List expectedData = new(testData); + _ = list.Capacity.Should().BeGreaterThanOrEqualTo(RecyclableDefaults.InitialCapacity); - _ = list.Should().AllSatisfy(x => x.Should().BeNull()); + _ = list.Should().AllSatisfy(x => x.Should().BeNull()).And.Equal(expectedData); } [Theory] @@ -303,17 +327,22 @@ namespace Recyclable.CollectionsTests } // Validate + var expectedData = testData.ToList(); + _ = list.Capacity.Should().BeGreaterThanOrEqualTo(itemsCount); _ = list.Count.Should().Be(itemsCount); - _ = list.Should().Equal(testData); + _ = list.Should().Equal(expectedData); } [Theory] [MemberData(nameof(RecyclableLongListTestData.SourceDataVariants), MemberType = typeof(RecyclableLongListTestData))] public void AddShouldAddItemWhenAfterAddRange(string testCase, IEnumerable testData, int itemsCount) { - // Act + // Prepare using var list = new RecyclableList(); + itemsCount <<= 1; + + // Act list.AddRange(testData); foreach (var index in testData) { @@ -321,29 +350,33 @@ namespace Recyclable.CollectionsTests } // Validate - _ = list.Capacity.Should().BeGreaterThanOrEqualTo(itemsCount * 2); - _ = list.Count.Should().Be(itemsCount * 2); - _ = list.Should().Equal(testData.Concat(testData)); + List expectedData = new(itemsCount); + expectedData.AddRange(testData); + expectedData.AddRange(testData); + + _ = list.Capacity.Should().BeGreaterThanOrEqualTo(itemsCount); + _ = list.Count.Should().Be(itemsCount); + _ = list.Should().Equal(expectedData); } [Theory] [MemberData(nameof(RecyclableLongListTestData.SourceRefDataVariants), MemberType = typeof(RecyclableLongListTestData))] public void ClearShouldClearItemsWhenReferenceType(string testCase, IEnumerable testData, int itemsCount) { + //* Clearing empty list is tested in . if (itemsCount == 0) { - /// Clearing empty list is tested in . return; } - // Arrange + // Prepare using var list = new RecyclableList(testData, itemsCount); _ = list.AsArray[..itemsCount].Should().AllSatisfy(x => x.Should().NotBeNull()); // Act list.Clear(); - // Assert + // Validate _ = list.AsArray[..itemsCount].Should().AllSatisfy(x => x.Should().BeNull()); } @@ -382,15 +415,18 @@ namespace Recyclable.CollectionsTests public void ConstructorShouldAcceptDuplicates(string testCase, IEnumerable testData, int itemsCount) { // Prepare - testData = testData.Concat(testData); + testData = testData.Concat(testData.Reverse()); + itemsCount <<= 1; // Act - using var list = new RecyclableList(testData, itemsCount * 2); + using var list = new RecyclableList(testData, itemsCount); // Validate - _ = list.Capacity.Should().BeGreaterThanOrEqualTo(itemsCount * 2); - _ = list.Count.Should().Be(itemsCount * 2); - _ = list.Should().Equal(testData); + List expectedData = new(testData); + + _ = list.Capacity.Should().BeGreaterThanOrEqualTo(itemsCount); + _ = list.Count.Should().Be(itemsCount); + _ = list.Should().Equal(expectedData); } [Theory] @@ -401,23 +437,33 @@ namespace Recyclable.CollectionsTests using var list = new RecyclableList(testData, itemsCount); // Validate + List expectedData = new(testData); + _ = list.Capacity.Should().BeGreaterThanOrEqualTo(itemsCount); _ = list.Count.Should().Be(itemsCount); - _ = list.Should().Equal(testData); + _ = list.Should().Equal(expectedData); } [Theory] [MemberData(nameof(RecyclableLongListTestData.SourceDataWithItemIndexVariants), MemberType = typeof(RecyclableLongListTestData))] public void ContainsShouldFindAllItems(string testCase, IEnumerable testData, int itemsCount, in long[] itemIndexes) { - // Prepare - using var list = new RecyclableList(testData, itemsCount); _ = testData.Any().Should().BeTrue("we need items on the list that we can look for"); - // Act & Validate + // Prepare + using var list = new RecyclableList(testData, itemsCount); + var expectedData = testData.ToList(); + foreach (var itemIndex in itemIndexes) { - _ = list.Contains(list[(int)itemIndex]).Should().BeTrue($"we searched for {list[(int)itemIndex]}"); + long expectedItem = expectedData[(int)itemIndex]; + bool expected = expectedData.Contains(expectedItem); + + // Act + var actual = list.Contains(expectedItem); + + // Validate + _ = actual.Should().Be(expected); } } @@ -425,7 +471,7 @@ namespace Recyclable.CollectionsTests public void ContainsShouldNotFindAnythingWhenListEmpty() { // Prepare - using var list = new RecyclableList(); + using var list = new RecyclableList(); // Validate _ = list.Contains(0).Should().BeFalse(); @@ -436,17 +482,41 @@ namespace Recyclable.CollectionsTests [MemberData(nameof(RecyclableLongListTestData.SourceDataWithItemIndexVariants), MemberType = typeof(RecyclableLongListTestData))] public void ContainsShouldNotFindNonExistingItems(string testCase, IEnumerable testData, int itemsCount, in long[] itemIndexes) { - // Prepare - using var list = new RecyclableList(testData, itemsCount); _ = testData.Any().Should().BeTrue("we need items on the list that we can look for"); - // Act & Validate + // Prepare + using var list = new RecyclableList(testData, itemsCount); + var expectedData = testData.ToList(); + foreach (var itemIndex in itemIndexes) { - _ = list.Contains(-list[(int)itemIndex]).Should().BeFalse(); + long expectedItem = -expectedData[(int)itemIndex]; + var expected = expectedData.Contains(expectedItem); + + // Act + var actual = list.Contains(expectedItem); + + // Validate + _ = actual.Should().Be(expected); } } + [Theory] + [MemberData(nameof(RecyclableLongListTestData.SourceDataVariants), MemberType = typeof(RecyclableLongListTestData))] + public void ConvertAllShouldConvertAllItems(string testCase, IEnumerable testData, int itemsCount) + { + // Prepare + using var list = new RecyclableList(testData, itemsCount); + var expectedData = testData.ToList().ConvertAll(item => item + 0.1); + + // Act + using var actual = list.ConvertAll(item => item + 0.1); + + // Validate + _ = actual.Count.Should().Be(itemsCount); + _ = actual.Should().Equal(expectedData); + } + [Theory] [MemberData(nameof(RecyclableLongListTestData.SourceDataVariants), MemberType = typeof(RecyclableLongListTestData))] public void CopyToShouldCopyAllItemsInTheCorrectOrder(string testCase, IEnumerable testData, int itemsCount) @@ -459,6 +529,27 @@ namespace Recyclable.CollectionsTests list.CopyTo(copiedItems, 0); // Validate + long[] expectedData = new long[itemsCount]; + testData.ToList().CopyTo(expectedData, 0); + + _ = copiedItems.Should().Equal(expectedData); + } + + [Theory] + [MemberData(nameof(RecyclableLongListTestData.SourceDataVariants), MemberType = typeof(RecyclableLongListTestData))] + public void CopyToShouldCopyAllItemsInTheCorrectOrderWhenFromStart(string testCase, IEnumerable testData, int itemsCount) + { + // Prepare + using var list = new RecyclableList(testData, itemsCount); + long[] copiedItems = new long[itemsCount]; + + // Act + list.CopyTo(copiedItems); + + // Validate + long[] expectedData = new long[itemsCount]; + testData.ToList().CopyTo(expectedData); + _ = copiedItems.Should().Equal(testData); } @@ -466,13 +557,13 @@ namespace Recyclable.CollectionsTests [MemberData(nameof(RecyclableLongListTestData.SourceRefDataVariants), MemberType = typeof(RecyclableLongListTestData))] public void DisposeShouldClearItemsWhenReferenceType(string testCase, IEnumerable testData, int itemsCount) { + //* Disposing empty list is tested in . if (itemsCount == 0) { - /// Disposing empty list is tested in . return; } - // Arrange + // Prepare using var list = new RecyclableList(testData, itemsCount); var listArray = list.AsArray; _ = list.AsArray[..itemsCount].Should().NotBeNull(); @@ -481,22 +572,22 @@ namespace Recyclable.CollectionsTests // Act list.Dispose(); - // Assert + // Validate _ = listArray[..itemsCount].Should().AllSatisfy(x => x.Should().BeNull()); } [Fact] public void DisposeShouldReturnPooledArrayOnlyOnce() { - // Arrange + // Prepare var testData = RecyclableLongListTestData.CreateTestData(RecyclableDefaults.MinPooledArrayLength); - RecyclableList firstInstance = new(testData); + using RecyclableList firstInstance = new(testData); var firstArray = firstInstance.AsArray; // Act firstInstance.Dispose(); - // Assert + // Validate using RecyclableList sameInstance = new(testData); _ = sameInstance.AsArray.Should().BeSameAs(firstArray); @@ -526,17 +617,386 @@ namespace Recyclable.CollectionsTests { // Prepare using var list = new RecyclableList(testData, itemsCount); + using var enumerator = list.GetEnumerator(); + var yieldedItems = new List(itemsCount); // Act - var yieldedItems = new List(itemsCount); - using var enumerator = list.GetEnumerator(); while (enumerator.MoveNext()) { yieldedItems.Add(enumerator.Current); } // Validate - _ = yieldedItems.Should().Equal(testData); + List expectedData = new(testData); + + _ = yieldedItems.Should().Equal(expectedData); + } + + [Theory] + [MemberData(nameof(RecyclableLongListTestData.SourceDataWithItemIndexVariants), MemberType = typeof(RecyclableLongListTestData))] + public void ExistsShouldFindAllItems(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 expectedData = testData.ToList(); + + foreach (var itemIndex in itemIndexes) + { + var expectedItem = expectedData[(int)itemIndex]; + var expected = expectedData.Exists(item => item == expectedItem); + + // Act + var actual = list.Exists(item => item == expectedItem); + + // Validate + _ = actual.Should().Be(expected); + } + } + + [Theory] + [MemberData(nameof(RecyclableLongListTestData.SourceDataWithItemIndexVariants), MemberType = typeof(RecyclableLongListTestData))] + public void ExistsShouldNotFindNonExistingItems(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 expectedData = testData.ToList(); + + foreach (var itemIndex in itemIndexes) + { + var expectedItem = expectedData[(int)itemIndex]; + var expected = expectedData.Exists(item => item == -expectedItem); + + // Act + var actual = list.Exists(item => item == -expectedItem); + + // Validate + _ = actual.Should().Be(expected); + } + } + + [Theory] + [MemberData(nameof(RecyclableLongListTestData.SourceDataWithItemIndexVariants), MemberType = typeof(RecyclableLongListTestData))] + public void FindAllIndexesShouldReturnCorrectIndexes(string testCase, IEnumerable testData, int itemsCount, in long[] itemIndexes) + { + //* There is no equivalent of .FindAllIndexes in List, so no comparison here. + + // Prepare + using var list = new RecyclableList(testData, itemsCount); + + foreach (var itemIndex in itemIndexes) + { + // Act + using var actual = list.FindAllIndexes(item => item == itemIndex + 1); + + // Validate + _ = actual.Count.Should().Be(1); + if (itemsCount > 0) + { + _ = actual.Count.Should().Be(1); + _ = actual.Should().AllSatisfy(x => x.Should().Be((int)itemIndex)); + } + } + } + + [Theory] + [MemberData(nameof(RecyclableLongListTestData.ItemsCountTestCases), MemberType = typeof(RecyclableLongListTestData))] + public void FindAllShouldReturnCorrectIndexes(int itemsCount) + { + const int expectedItem = 1; + + // Prepare + var testData = Enumerable.Range(0, itemsCount).Select(value => value % 2 == 0 ? expectedItem : -expectedItem); + using var list = new RecyclableList(testData, itemsCount); + + // Act + using var actual = list.FindAll(x => x == expectedItem); + + // Validate + var expectedData = testData.ToList().FindAll(x => x == expectedItem); + + _ = actual.Count.Should().Be(expectedData.Count); + _ = actual.Should().Equal(expectedData); + if (expectedData.Count > 0) + { + _ = actual.Should().AllSatisfy(x => x.Should().Be(expectedItem)).And.Equal(expectedData); + } + } + + [Theory] + [MemberData(nameof(RecyclableLongListTestData.ItemsCountTestCases), MemberType = typeof(RecyclableLongListTestData))] + public void FindAllShouldNotFindNonExistingItems(int itemsCount) + { + const int expectedItem = 1; + + // Prepare + var testData = Enumerable.Range(0, itemsCount).Select(_ => expectedItem); + using var list = new RecyclableList(testData, itemsCount); + + // Act + using var foundItems = list.FindAll(x => x == -expectedItem); + + // Validate + var expectedData = testData.ToList().FindAll(x => x == -expectedItem); + + _ = foundItems.Count.Should().Be(expectedData.Count); + _ = foundItems.Should().Equal(expectedData); + } + + [Theory] + [MemberData(nameof(RecyclableLongListTestData.SourceDataWithItemIndexVariants), MemberType = typeof(RecyclableLongListTestData))] + public void FindIndexShouldReturnCorrectIndexes(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 expectedItem = expectedData[(int)itemIndex]; + + // Act + var actual = list.FindIndex(item => item == expectedItem); + + // Validate + var expected = expectedData.FindIndex(item => item == expectedItem); + + _ = actual.Should().Be((int)itemIndex).And.Be(expected); + } + } + + [Theory] + [MemberData(nameof(RecyclableLongListTestData.SourceDataWithItemIndexVariants), MemberType = typeof(RecyclableLongListTestData))] + public void FindIndexShouldReturnCorrectIndexesWhenConstrainedCount(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 expectedItem = expectedData[(int)itemIndex]; + var expected = expectedData.FindIndex((int)itemIndex, (int)(itemsCount - itemIndex), item => item == expectedItem); + + // Act + var actual = list.FindIndex((int)itemIndex, (int)(itemsCount - itemIndex), item => item == expectedItem); + + // Validate + _ = actual.Should().Be((int)itemIndex).And.Be(expected); + } + } + + [Theory] + [MemberData(nameof(RecyclableLongListTestData.SourceDataWithItemIndexVariants), MemberType = typeof(RecyclableLongListTestData))] + public void FindIndexShouldReturnCorrectIndexesWhenConstrainedIndex(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 expectedItem = expectedData[(int)itemIndex]; + var expected = expectedData.FindIndex(Math.Min(1, (int)itemIndex), item => item == expectedItem); + + // Act + var actual = list.FindIndex(Math.Min(1, (int)itemIndex), item => item == expectedItem); + + // Validate + _ = actual.Should().Be((int)itemIndex).And.Be(expected); + } + } + + [Theory] + [MemberData(nameof(RecyclableLongListTestData.SourceDataWithItemIndexVariants), MemberType = typeof(RecyclableLongListTestData))] + public void FindLastIndexShouldReturnCorrectIndexes(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 expectedItem = expectedData[(int)itemIndex]; + var expected = expectedData.FindLastIndex(x => x == expectedItem); + + // Act + var actual = list.FindLastIndex(x => x == expectedItem); + + // Validate + _ = actual.Should().Be((int)itemIndex).And.Be(expected); + } + } + + [Theory] + [MemberData(nameof(RecyclableLongListTestData.SourceDataWithItemIndexVariants), MemberType = typeof(RecyclableLongListTestData))] + public void FindLastIndexShouldReturnCorrectIndexesWhenConstrainedCount(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 expectedItem = expectedData[(int)itemIndex]; + + // Act & Validate + var expected = expectedData.FindLastIndex(itemsCount - 1, itemsCount, x => x == expectedItem); + _ = list.FindLastIndex(itemsCount - 1, itemsCount, x => x == expectedItem).Should().Be((int)itemIndex).And.Be(expected); + } + } + + [Theory] + [MemberData(nameof(RecyclableLongListTestData.SourceDataWithItemIndexVariants), MemberType = typeof(RecyclableLongListTestData))] + public void FindLastIndexShouldReturnCorrectIndexesWhenConstrainedIndex(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 expectedItem = expectedData[(int)itemIndex]; + var expected = expectedData.FindLastIndex(itemsCount - 1, x => x == expectedItem); + + // Act + var actual = list.FindLastIndex(itemsCount - 1, x => x == expectedItem); + + // Validate + _ = actual.Should().Be((int)itemIndex).And.Be(expected); + } + } + + [Theory] + [MemberData(nameof(RecyclableLongListTestData.SourceDataWithItemIndexVariants), MemberType = typeof(RecyclableLongListTestData))] + public void FindLastIndexShouldThrowArgumentOutOfRange(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 expectedItem = expectedData[(int)itemIndex]; + int expected = int.MinValue; + + // Act & Validate + if (itemsCount - 1 != -1) + { + Assert.Throws(() => expected = expectedData.FindLastIndex(itemsCount - 1, itemsCount + 3, x => x == expectedItem)); + Assert.Throws(() => _ = list.FindLastIndex(itemsCount - 1, itemsCount + 3, x => x == expectedItem).Should().Be((int)itemIndex).And.Be(expected)); + } + else + { + expected = expectedData.FindLastIndex(itemsCount - 1, itemsCount + 3, x => x == expectedItem); + _ = list.FindLastIndex(itemsCount - 1, itemsCount + 3, x => x == expectedItem).Should().Be((int)itemIndex).And.Be(expected); + } + } + } + + [Theory] + [MemberData(nameof(RecyclableLongListTestData.SourceDataWithItemIndexVariants), MemberType = typeof(RecyclableLongListTestData))] + public void FindLastShouldReturnCorrectItem(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 expectedItem = expectedData[(int)itemIndex]; + var expected = expectedData.FindLast(item => item == expectedItem); + + // Act + var actual = list.FindLast(item => item == expectedItem); + + // Validate + _ = actual.Should().Be(expectedData[(int)itemIndex]).And.Be(expected); + } + } + + [Theory] + [MemberData(nameof(RecyclableLongListTestData.SourceDataWithItemIndexVariants), MemberType = typeof(RecyclableLongListTestData))] + public void FindShouldFindAllItems(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 expectedData = testData.ToList(); + + foreach (var itemIndex in itemIndexes) + { + long expectedItem = expectedData[(int)itemIndex]; + var expected = expectedData.Find(item => item == expectedItem); + + // Act + var actual = list.Find(item => item == expectedData[(int)itemIndex]); + + // Validate + _ = actual.Should().Be(expectedData[(int)itemIndex]); + } + } + + [Theory] + [MemberData(nameof(RecyclableLongListTestData.SourceDataWithItemIndexVariants), MemberType = typeof(RecyclableLongListTestData))] + public void FindShouldNotFindNonExistingItems(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 expectedData = testData.ToList(); + + foreach (var itemIndex in itemIndexes) + { + var expected = expectedData.Find(item => item == -expectedData[(int)itemIndex]); + + // Act + var actual = list.Find(item => item == -expectedData[(int)itemIndex]); + + // Validate + actual.Should().Be(0).And.Be(expected); + } + } + + [Theory] + [MemberData(nameof(RecyclableLongListTestData.SourceDataVariants), MemberType = typeof(RecyclableLongListTestData))] + public void ForEachShouldGiveItemsInCorrectOrder(string testCase, IEnumerable testData, int itemsCount) + { + // Prepare + using var list = new RecyclableList(testData, itemsCount); + var expectedData = testData.ToList(); + var expected = new List(itemsCount); + expectedData.ForEach(expected.Add); + + // Act + var yieldedItems = new List(itemsCount); + list.ForEach(yieldedItems.Add); + + // Validate + _ = yieldedItems.Should().Equal(testData).And.Equal(expected); + } + + [Theory] + [MemberData(nameof(RecyclableLongListTestData.EmptySourceDataVariants), MemberType = typeof(RecyclableLongListTestData))] + public void ForEachShouldDoNothingWhenEmptyList(string testCase, IEnumerable testData, int itemsCount) + { + // Prepare + using var list = new RecyclableList(testData, itemsCount); + var expectedData = testData.ToList(); + var expected = new List(itemsCount); + expectedData.ForEach(expected.Add); + + // Act + var yieldedItems = new List(itemsCount); + list.ForEach(item => yieldedItems.Add(item)); + + // Validate + _ = yieldedItems.Should().BeEmpty().And.Equal(expected); } [Theory] @@ -544,12 +1004,42 @@ namespace Recyclable.CollectionsTests public void GetByIndexShouldReturnCorrectItem(string testCase, IEnumerable testData, int itemsCount) { // Prepare - using var list = new RecyclableLongList(testData, itemsCount); + using var list = new RecyclableList(testData, itemsCount); + var expectedData = testData.ToList(); - // Act & Validate foreach (var item in testData) { - _ = list[item - 1].Should().Be(item); + var expected = expectedData[(int)item - 1]; + + // Act + var actual = list[(int)item - 1]; + + // Validate + _ = actual.Should().Be(item).And.Be(expected); + } + } + + [Theory] + [MemberData(nameof(RecyclableLongListTestData.SourceDataWithItemIndexVariants), MemberType = typeof(RecyclableLongListTestData))] + public void GetRangeShouldReturnCorrectItems(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 expectedData = testData.ToList(); + + foreach (var itemIndex in itemIndexes) + { + int startingIndex = (int)Math.Min(1, itemIndex); + var expectedRangeItems = expectedData.GetRange(startingIndex, (int)itemIndex); + + // Act + using var rangeItems = list.GetRange(startingIndex, (int)itemIndex); + + // Validate + _ = rangeItems.Count.Should().Be(expectedRangeItems.Count); + _ = rangeItems.Should().Equal(expectedRangeItems); } } @@ -557,9 +1047,10 @@ namespace Recyclable.CollectionsTests public void IndexOfShouldNotFindAnythingWhenListEmpty() { // Prepare - using var list = new RecyclableList(); + using var list = new RecyclableList(); - // Validate + // Act & Validate + _ = list.IndexOf(-1).Should().Be(-1); _ = list.IndexOf(0).Should().Be(-1); _ = list.IndexOf(1).Should().Be(-1); } @@ -568,14 +1059,21 @@ namespace Recyclable.CollectionsTests [MemberData(nameof(RecyclableLongListTestData.SourceDataWithItemIndexVariants), MemberType = typeof(RecyclableLongListTestData))] public void IndexOfShouldNotFindNonExistingItems(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); - _ = testData.Any().Should().BeTrue("we need items on the list that we can look for"); + var expectedData = testData.ToList(); - // Act & Validate foreach (var itemIndex in itemIndexes) { - _ = list.IndexOf(-list[(int)itemIndex]).Should().Be(-1); + var expected = expectedData.IndexOf(-expectedData[(int)itemIndex]); + + // Act + var actual = list.IndexOf(-expectedData[(int)itemIndex]); + + // Validate + _ = actual.Should().Be(-1).And.Be(expected); } } @@ -585,11 +1083,133 @@ namespace Recyclable.CollectionsTests { // Prepare using var list = new RecyclableList(testData, itemsCount); + var expectedData = testData.ToList(); + + foreach (var itemIndex in itemIndexes) + { + var expected = expectedData.IndexOf(expectedData[(int)itemIndex]); + + // Act + var actual = list.IndexOf(expectedData[(int)itemIndex]); + + // Validate + _ = actual.Should().Be((int)itemIndex).And.Be(expected); + } + } + + [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 LastIndexOfShouldReturnCorrectIndexes(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.LastIndexOf(expectedData[(int)itemIndex]); + + // Act + var actual = list.LastIndexOf(expectedData[(int)itemIndex]); + + // Validate + _ = actual.Should().Be((int)itemIndex).And.Be(expected); + } + } + + [Theory] + [MemberData(nameof(RecyclableLongListTestData.SourceDataWithItemIndexVariants), MemberType = typeof(RecyclableLongListTestData))] + public void LastIndexOfShouldReturnCorrectIndexesWhenConstrainedCount(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.LastIndexOf(expectedData[(int)itemIndex], itemsCount - 1, itemsCount); + + // Act + var actual = list.LastIndexOf(expectedData[(int)itemIndex], itemsCount - 1, itemsCount); + + // Validate + _ = actual.Should().Be((int)itemIndex).And.Be(expected); + } + } + + [Theory] + [MemberData(nameof(RecyclableLongListTestData.SourceDataWithItemIndexVariants), MemberType = typeof(RecyclableLongListTestData))] + public void LastIndexOfShouldReturnCorrectIndexesWhenConstrainedIndex(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.LastIndexOf(expectedData[(int)itemIndex], itemsCount - 1); + + // Act + var actual = list.LastIndexOf(expectedData[(int)itemIndex], itemsCount - 1); + + // Validate + _ = actual.Should().Be((int)itemIndex).And.Be(expected); + } + } + + [Theory] + [MemberData(nameof(RecyclableLongListTestData.SourceDataWithItemIndexVariants), MemberType = typeof(RecyclableLongListTestData))] + public void LastIndexOfShouldThrowArgumentOutOfRangeWhenCountTooBig(string testCase, IEnumerable testData, int itemsCount, in long[] itemIndexes) + { + // Prepare + using var list = new RecyclableList(testData, itemsCount); + var expectedData = testData.ToList(); // Act & Validate foreach (var itemIndex in itemIndexes) { - _ = list.IndexOf(list[(int)itemIndex]).Should().Be((int)itemIndex); + _ = Assert.Throws(() => _ = expectedData.LastIndexOf(expectedData[(int)itemIndex], itemsCount - 1, itemsCount + 3).Should().Be((int)itemIndex)); + _ = Assert.Throws(() => _ = list.LastIndexOf(expectedData[(int)itemIndex], itemsCount - 1, itemsCount + 3).Should().Be((int)itemIndex)); } } @@ -600,8 +1220,8 @@ namespace Recyclable.CollectionsTests foreach (var itemIndex in itemIndexes) { // Prepare - var testDataList = testData.ToList(); - var item = testDataList[(int)itemIndex]; + var expectedData = testData.ToList(); + var item = expectedData[(int)itemIndex]; // Act using RecyclableList list = new(testData); @@ -609,7 +1229,7 @@ namespace Recyclable.CollectionsTests // Validate _ = list.Count.Should().Be(itemsCount); - _ = list.Should().Equal(testDataList); + _ = list.Should().Equal(expectedData); // Act list.Clear(); @@ -623,17 +1243,17 @@ namespace Recyclable.CollectionsTests // Validate _ = list.Count.Should().Be(itemsCount); - _ = list.Should().Equal(testDataList); + _ = list.Should().Equal(expectedData); // Prepare - testDataList.RemoveAt((int)itemIndex); + expectedData.RemoveAt((int)itemIndex); // Act _ = list.Remove(item).Should().BeTrue(); _ = list.IndexOf(item).Should().Be(RecyclableDefaults.ItemNotFoundIndex); _ = list.Contains(item).Should().BeFalse(); _ = list.Count.Should().Be(itemsCount - 1); - _ = list.Should().Equal(testDataList); + _ = list.Should().Equal(expectedData); } } @@ -645,8 +1265,8 @@ namespace Recyclable.CollectionsTests { // Prepare using var list = new RecyclableList(testData, itemsCount); - var item = list[(int)itemIndex]; var expectedItems = list.ToList(); + var item = expectedItems[(int)itemIndex]; expectedItems.Insert((int)itemIndex, item); // Act @@ -661,11 +1281,54 @@ namespace Recyclable.CollectionsTests } } + [Theory] + [MemberData(nameof(RecyclableLongListTestData.SourceDataWithItemIndexVariants), MemberType = typeof(RecyclableLongListTestData))] + public void InsertRangeShouldMoveItems(string testCase, IEnumerable testData, int itemsCount, in long[] itemIndexes) + { + foreach (var itemIndex in itemIndexes) + { + // Prepare + using var list = new RecyclableList(testData.Reverse(), itemsCount); + var expectedItems = testData.Reverse().ToList(); + var item = expectedItems[^1]; + expectedItems.InsertRange((int)itemIndex, testData); + + // Act + list.InsertRange((int)itemIndex, testData); + + // Validate + _ = list[(int)itemIndex].Should().Be(item); + _ = list.Count.Should().Be(itemsCount << 1); + _ = list.Capacity.Should().BeGreaterThanOrEqualTo(itemsCount << 1); + _ = list.Should().Equal(expectedItems); + } + } + + [Theory] + [MemberData(nameof(RecyclableLongListTestData.SourceDataWithItemIndexVariants), MemberType = typeof(RecyclableLongListTestData))] + public void RemoveAllShouldRemoveTheCorrectItems(string testCase, IEnumerable testData, int itemsCount, in long[] itemIndexes) + { + // Prepare + using var list = new RecyclableList(testData, itemsCount); + var itemsToRemove = itemIndexes.Select(x => list[(int)x]).ToArray(); + var expectedItems = testData.ToList(); + var expectedResult = expectedItems.RemoveAll(itemsToRemove.Contains); + + // Act + var actualResult = list.RemoveAll(itemsToRemove.Contains); + + // Validate + _ = actualResult.Should().Be(expectedResult); + _ = list.Capacity.Should().BeGreaterThanOrEqualTo(itemsCount); + _ = list.Count.Should().Be(itemsCount - itemsToRemove.Length); + _ = list.Should().Equal(expectedItems); + } + [Theory] [MemberData(nameof(RecyclableLongListTestData.SourceRefDataWithItemIndexVariants), MemberType = typeof(RecyclableLongListTestData))] public void RemoveAtShouldClearItemWhenReferenceType(string testCase, IEnumerable testData, int itemsCount, in long[] indexesToRemove) { - // Arrange + // Prepare foreach (int indexToRemove in indexesToRemove) { using var list = new RecyclableList(testData, itemsCount); @@ -674,7 +1337,7 @@ namespace Recyclable.CollectionsTests // Act list.RemoveAt(indexToRemove); - // Assert + // Validate _ = list[list.Count].Should().BeNull(); } } @@ -700,23 +1363,46 @@ namespace Recyclable.CollectionsTests } } + [Theory] + [MemberData(nameof(RecyclableLongListTestData.SourceDataWithItemIndexVariants), MemberType = typeof(RecyclableLongListTestData))] + public void RemoveRangeShouldRemoveCorrectItems(string testCase, IEnumerable testData, int itemsCount, in long[] itemIndexes) + { + _ = testData.Any().Should().BeTrue("we need items on the list that we can look for"); + + foreach (var itemIndex in itemIndexes) + { + // Prepare + int startingIndex = (int)Math.Min(1, itemIndex); + var expectedData = testData.ToList(); + expectedData.RemoveRange(startingIndex, (int)itemIndex); + + // Act + using var list = new RecyclableList(testData, itemsCount); + list.RemoveRange(startingIndex, (int)itemIndex); + + // Validate + _ = list.Count.Should().Be(expectedData.Count); + _ = list.Should().Equal(expectedData); + } + } + [Theory] [MemberData(nameof(RecyclableLongListTestData.SourceRefDataWithItemIndexVariants), MemberType = typeof(RecyclableLongListTestData))] public void RemoveShouldClearItemWhenReferenceType(string testCase, IEnumerable testData, int itemsCount, in long[] indexesToRemove) { - var expectedItems = testData.ToArray(); foreach (int indexToRemove in indexesToRemove) { - // Arrange - var itemToRemove = expectedItems[indexToRemove]; + // Prepare using var list = new RecyclableList(testData, itemsCount); + var expectedItems = testData.ToArray(); + var item = expectedItems[indexToRemove]; _ = list[^1].Should().NotBeNull(); // Act - list.Remove(itemToRemove); + list.Remove(item); - // Assert + // Validate _ = list[list.Count].Should().BeNull(); } } @@ -730,19 +1416,59 @@ namespace Recyclable.CollectionsTests // Prepare using var list = new RecyclableList(testData, itemsCount); var expectedItems = testData.ToList(); - var item = list[(int)itemIndex]; - expectedItems.RemoveAt((int)itemIndex); + var item = expectedItems[(int)itemIndex]; + var expected = expectedItems.Remove(item); // Act - _ = list.Remove(item).Should().BeTrue(); + var actual = list.Remove(item); // Validate + _ = actual.Should().BeTrue().And.Be(expected); _ = list.Capacity.Should().BeGreaterThanOrEqualTo(itemsCount); _ = list.Count.Should().Be(itemsCount - 1); _ = list.Should().Equal(expectedItems); } } + [Theory] + [MemberData(nameof(RecyclableLongListTestData.SourceDataVariants), MemberType = typeof(RecyclableLongListTestData))] + public void ReverseShouldSwapItemsInCorrectOrder(string testCase, IEnumerable testData, int itemsCount) + { + // Prepare + using var list = new RecyclableList(testData, itemsCount); + var expectedData = testData.ToList(); + expectedData.Reverse(); + + // Act + list.Reverse(); + + // Validate + _ = list.Should().Equal(expectedData); + } + + [Theory] + [MemberData(nameof(RecyclableLongListTestData.SourceDataWithItemIndexVariants), MemberType = typeof(RecyclableLongListTestData))] + public void ReverseShouldSwapItemsInCorrectOrderWhenConstrained(string testCase, IEnumerable testData, int itemsCount, long[] itemIndexes) + { + // Prepare + var expectedData = testData.ToArray(); + + foreach (var itemIndex in itemIndexes) + { + // Prepare + using var list = new RecyclableList(testData, itemsCount); + var startingItemIndex = (int)Math.Min(1, itemIndex); + var expectedRangeItems = expectedData.ToList(); + expectedRangeItems.Reverse(startingItemIndex, (int)itemIndex); + + // Act + list.Reverse(startingItemIndex, (int)itemIndex); + + // Validate + _ = list.Should().Equal(expectedRangeItems); + } + } + [Theory] [MemberData(nameof(RecyclableLongListTestData.SourceDataVariants), MemberType = typeof(RecyclableLongListTestData))] public void SetByIndexShouldReturnCorrectItem(string testCase, IEnumerable testData, int itemsCount) @@ -759,11 +1485,326 @@ namespace Recyclable.CollectionsTests // Validate expectedData[(int)item - 1] = item + 1; - list[(int)item - 1].Should().Be(item + 1); - list.Should().Equal(expectedData); + _ = list[(int)item - 1].Should().Be(item + 1); + _ = list.Should().Equal(expectedData); } } + [Theory] + [MemberData(nameof(RecyclableLongListTestData.SourceDataVariants), MemberType = typeof(RecyclableLongListTestData))] + public void SortShouldSortItemsInCorrectOrder(string testCase, IEnumerable testData, int itemsCount) + { + // Prepare + using var list = new RecyclableList(testData.Reverse(), itemsCount); + var expectedData = testData.Reverse().ToList(); + expectedData.Sort(); + + // Act + list.Sort(); + + // Validate + _ = list.Should().Equal(expectedData); + } + + [Theory] + [MemberData(nameof(RecyclableLongListTestData.SourceDataVariants), MemberType = typeof(RecyclableLongListTestData))] + public void SortShouldSortItemsInCorrectOrderWhenWithComparer(string testCase, IEnumerable testData, int itemsCount) + { + // Prepare + using var list = new RecyclableList(testData.Reverse(), itemsCount); + IComparer comparer = Comparer.Default; + var expectedData = testData.Reverse().ToList(); + expectedData.Sort(comparer); + + // Act + list.Sort(comparer); + + // Validate + _ = list.Should().Equal(expectedData); + } + + [Theory] + [MemberData(nameof(RecyclableLongListTestData.SourceDataVariants), MemberType = typeof(RecyclableLongListTestData))] + public void SortShouldSortItemsInCorrectOrderWhenWithComparison(string testCase, IEnumerable testData, int itemsCount) + { + // Prepare + using var list = new RecyclableList(testData.Reverse(), itemsCount); + + static int comparison(long x, long y) + { + return (x - y) switch + { + > 0 => 1, + < 0 => -1, + _ => 0, + }; + } + + var expectedData = testData.Reverse().ToList(); + expectedData.Sort(comparison); + + // Act + list.Sort(comparison); + + // Validate + _ = list.Should().Equal(expectedData); + } + + [Theory] + [MemberData(nameof(RecyclableLongListTestData.SourceDataWithItemIndexVariants), MemberType = typeof(RecyclableLongListTestData))] + public void SortShouldSortItemsInCorrectOrderWhenConstrained(string testCase, IEnumerable testData, int itemsCount, long[] itemIndexes) + { + // Prepare + IComparer comparer = Comparer.Default; + var expectedData = testData.Reverse().ToArray(); + + foreach (var itemIndex in itemIndexes) + { + // Prepare + using var list = new RecyclableList(testData.Reverse(), itemsCount); + var startingIndex = (int)Math.Min(1, itemIndex); + + var expectedSortedData = expectedData.ToList(); + expectedSortedData.Sort(startingIndex, (int)itemIndex, comparer); + + // Act + list.Sort(startingIndex, (int)itemIndex, comparer); + + // Validate + _ = list.Should().Equal(expectedSortedData); + } + } + + [Theory] + [MemberData(nameof(RecyclableLongListTestData.SourceDataVariants), MemberType = typeof(RecyclableLongListTestData))] + public void ToArrayShouldReturnAllItemsInCorrectOrder(string testCase, IEnumerable testData, int itemsCount) + { + // Prepare + using var list = new RecyclableList(testData, itemsCount); + var expectedData = testData.ToList().ToArray(); + + // Act + var returnedItems = list.ToArray(); + + // Validate + _ = returnedItems.Length.Should().Be(itemsCount); + _ = returnedItems.Should().Equal(expectedData); + } + + [Theory] + [MemberData(nameof(RecyclableLongListTestData.SourceDataVariants), MemberType = typeof(RecyclableLongListTestData))] + public void TrimExcessShouldDecreaseCapacity(string testCase, IEnumerable testData, int itemsCount) + { + // Prepare + using var list = new RecyclableList(testData, itemsCount); + var expectedCapacity = itemsCount >= RecyclableDefaults.InitialCapacity ? itemsCount : RecyclableDefaults.InitialCapacity; + + // Act + list.TrimExcess(); + + // Validate + _ = list.Capacity.Should().Be(expectedCapacity); + _ = list.Count.Should().Be(itemsCount); + _ = list.Should().Equal(testData); + } + + [Theory] + [MemberData(nameof(RecyclableLongListTestData.SourceDataVariants), MemberType = typeof(RecyclableLongListTestData))] + public void TrimExcessShouldNotBreakAdd(string testCase, IEnumerable testData, int itemsCount) + { + // Prepare + using var list = new RecyclableList(testData, itemsCount); + var expectedCapacity = (int)BitOperations.RoundUpToPowerOf2((uint)itemsCount + 1); + expectedCapacity = expectedCapacity >= RecyclableDefaults.InitialCapacity ? expectedCapacity : RecyclableDefaults.InitialCapacity; + + // Act + list.TrimExcess(); + list.Add(-1); + + // Validate + _ = list.Capacity.Should().Be(expectedCapacity); + _ = list.Count.Should().Be(itemsCount + 1); + _ = list.Should().Equal(testData.Append(-1)); + } + + [Theory] + [MemberData(nameof(RecyclableLongListTestData.SourceDataVariants), MemberType = typeof(RecyclableLongListTestData))] + public void TrimExcessShouldNotBreakAddRange(string testCase, IEnumerable testData, int itemsCount) + { + // Prepare + using var list = new RecyclableList(testData, itemsCount); + var expectedCapacity = (int)BitOperations.RoundUpToPowerOf2((uint)itemsCount << 1); + expectedCapacity = expectedCapacity >= RecyclableDefaults.InitialCapacity ? expectedCapacity : RecyclableDefaults.InitialCapacity; + + // Act + list.TrimExcess(); + list.AddRange(testData); + + // Validate + _ = list.Capacity.Should().BeGreaterThanOrEqualTo(expectedCapacity); + _ = list.Count.Should().Be(itemsCount << 1); + _ = list.Should().Equal(testData.Concat(testData)); + } + + [Theory] + [MemberData(nameof(RecyclableLongListTestData.SourceDataVariants), MemberType = typeof(RecyclableLongListTestData))] + public void TrimExcessShouldNotBreakClear(string testCase, IEnumerable testData, int itemsCount) + { + // Prepare + using var list = new RecyclableList(testData, itemsCount); + + // Act + list.TrimExcess(); + list.Clear(); + + // Validate + _ = list.Capacity.Should().Be(Math.Max(RecyclableDefaults.InitialCapacity, itemsCount)); + _ = list.Count.Should().Be(0); + _ = list.Should().BeEmpty(); + } + + [Theory] + [MemberData(nameof(RecyclableLongListTestData.SourceDataWithItemIndexVariants), MemberType = typeof(RecyclableLongListTestData))] + public void TrimExcessShouldNotBreakInsert(string testCase, IEnumerable testData, int itemsCount, long[] itemIndexes) + { + // Prepare + var expectedCapacity = (int)BitOperations.RoundUpToPowerOf2((uint)itemsCount + 1); + expectedCapacity = expectedCapacity >= RecyclableDefaults.InitialCapacity ? expectedCapacity : RecyclableDefaults.InitialCapacity; + var expectedData = testData.ToArray(); + + foreach (var itemIndex in itemIndexes) + { + using var list = new RecyclableList(testData, itemsCount); + var expectedItemsAfterInsert = expectedData.ToList(); + expectedItemsAfterInsert.Insert((int)itemIndex, -3); + + // Act + list.TrimExcess(); + list.Insert((int)itemIndex, -3); + + // Validate + _ = list.Capacity.Should().BeGreaterThanOrEqualTo(expectedCapacity); + _ = list.Count.Should().Be(expectedItemsAfterInsert.Count); + _ = list.Should().Equal(expectedItemsAfterInsert); + } + } + + [Theory] + [MemberData(nameof(RecyclableLongListTestData.SourceDataWithItemIndexVariants), MemberType = typeof(RecyclableLongListTestData))] + public void TrimExcessShouldNotBreakRemove(string testCase, IEnumerable testData, int itemsCount, long[] itemIndexes) + { + // Prepare + var expectedData = testData.ToArray(); + + foreach (var itemIndex in itemIndexes) + { + using var list = new RecyclableList(testData, itemsCount); + var expectedItemsAfterInsert = expectedData.ToList(); + expectedItemsAfterInsert.RemoveAt((int)itemIndex); + + // Act + list.TrimExcess(); + list.RemoveAt((int)itemIndex); + + // Validate + _ = list.Capacity.Should().BeGreaterThanOrEqualTo(itemsCount); + _ = list.Count.Should().Be(expectedItemsAfterInsert.Count); + _ = list.Should().Equal(expectedItemsAfterInsert); + } + } + + [Theory] + [MemberData(nameof(RecyclableLongListTestData.SourceDataWithItemIndexVariants), MemberType = typeof(RecyclableLongListTestData))] + public void TrimExcessShouldNotBreakIndexOf(string testCase, IEnumerable testData, int itemsCount, long[] itemIndexes) + { + // Prepare + var expectedData = testData.ToArray(); + + foreach (var itemIndex in itemIndexes) + { + using var list = new RecyclableList(testData, itemsCount); + + // Act + list.TrimExcess(); + var actual = list.IndexOf(expectedData[(int)itemIndex]); + + // Validate + _ = actual.Should().Be((int)itemIndex); + } + } + + [Theory] + [MemberData(nameof(RecyclableLongListTestData.SourceDataVariants), MemberType = typeof(RecyclableLongListTestData))] + public void TrimmedArraysShouldNotBePooled(string testCase, IEnumerable testData, int itemsCount) + { + // Prepare + using var list = new RecyclableList(testData, itemsCount); + + // Act + list.TrimExcess(); + var trimmedMemoryBlock = list._memoryBlock; + + list.Dispose(); + + // Validate + _ = list.Capacity.Should().Be(0); + _ = list.Count.Should().Be(0); + _ = list.Should().BeEmpty(); + + if (itemsCount >= RecyclableDefaults.MinPooledArrayLength) + { + var newMemoryBlock = RecyclableArrayPool.RentShared(itemsCount); + if (BitOperations.IsPow2(itemsCount)) + { + _ = newMemoryBlock.Should().BeSameAs(trimmedMemoryBlock); + } + else + { + _ = newMemoryBlock.Should().NotBeSameAs(trimmedMemoryBlock); + } + } + } + + [Theory] + [MemberData(nameof(RecyclableLongListTestData.SourceDataWithItemIndexVariants), MemberType = typeof(RecyclableLongListTestData))] + public void TrueForAllShouldBeFalseWhenNoMatch(string testCase, IEnumerable testData, int itemsCount, long[] itemIndexes) + { + // Prepare + var expectedData = testData.ToList(); + + foreach (var itemIndex in itemIndexes) + { + using RecyclableList list = new(testData, itemsCount); + Predicate condition = item => item == itemIndex; + var expected = expectedData.TrueForAll(condition); + + // Act + var actual = list.TrueForAll(condition); + + // Validate + _ = actual.Should().Be(expected).And.BeFalse(); + } + } + + [Theory] + [MemberData(nameof(RecyclableLongListTestData.SourceDataWithItemIndexVariants), MemberType = typeof(RecyclableLongListTestData))] + public void TrueForAllShouldBeTrueWhenMatched(string testCase, IEnumerable testData, int itemsCount, long[] itemIndexes) + { + // Prepare + using RecyclableList list = new(testData, itemsCount); + int index = 0; + Predicate condition = item => item == ++index; + var expected = testData.ToList().TrueForAll(condition); + + index = 0; + + // Act + var actual = list.TrueForAll(condition); + + // Validate + _ = actual.Should().Be(expected).And.BeTrue(); + } + [Theory] [MemberData(nameof(RecyclableLongListTestData.SourceDataVariants), MemberType = typeof(RecyclableLongListTestData))] public void VersionedEnumerateShouldRaiseExceptionWhenCollectionIsModified(string testCase, IEnumerable testData, int itemsCount) @@ -772,7 +1813,7 @@ namespace Recyclable.CollectionsTests using var list = new RecyclableList(testData, itemsCount); // Act - Assert.Throws(() => + _ = Assert.Throws(() => { list.Add(-1); foreach (var item in (IRecyclableVersionedList)list) diff --git a/Recyclable.CollectionsTests/RecyclableLongListTestData.cs b/Recyclable.CollectionsTests/RecyclableLongListTestData.cs index 8a67fbe..a9f5a30 100644 --- a/Recyclable.CollectionsTests/RecyclableLongListTestData.cs +++ b/Recyclable.CollectionsTests/RecyclableLongListTestData.cs @@ -1,7 +1,6 @@ using MoreLinq; using Open.Numeric.Primes; using Recyclable.Collections; -using System.Collections; using System.Numerics; namespace Recyclable.CollectionsTests @@ -34,6 +33,8 @@ namespace Recyclable.CollectionsTests 0, 1, 2, 3, 7, 10, 16, RecyclableDefaults.MinPooledArrayLength - 5, RecyclableDefaults.MinPooledArrayLength - 1, RecyclableDefaults.MinPooledArrayLength, RecyclableDefaults.MinPooledArrayLength + 1, RecyclableDefaults.MinPooledArrayLength + 5, RecyclableDefaults.MinPooledArrayLength * 100 }; + public static readonly IEnumerable ItemsCountTestCases = ItemsCountVariants.Select(x => new object[] { (int)x }); + public static readonly IEnumerable BlockSizeVariants = new int[] { 1, 2, 4, 16, RecyclableDefaults.MinPooledArrayLength - 5, (int)BitOperations.RoundUpToPowerOf2(RecyclableDefaults.MinPooledArrayLength)