diff --git a/.editorconfig b/.editorconfig index 76e37ea..a9c857c 100644 --- a/.editorconfig +++ b/.editorconfig @@ -223,9 +223,20 @@ dotnet_naming_style.begins_with_i.capitalization = pascal_case # IDE0057: Use range operator dotnet_diagnostic.IDE0057.severity = silent +# VSSpell001: Spell Check +dotnet_diagnostic.VSSpell001.severity = none + +# VSSpell002: Spell Check +dotnet_diagnostic.VSSpell002.severity = none + +# IDE0306: Simplify collection initialization +dotnet_diagnostic.IDE0306.severity = silent + dotnet_diagnostic.CS1591.severity = suggestion csharp_style_prefer_method_group_conversion = true:silent csharp_style_prefer_top_level_statements = true:silent +csharp_style_prefer_primary_constructors = true:suggestion +csharp_prefer_system_threading_lock = true:suggestion #dotnet_diagnostic.SA0001.severity = suggestion [*.{cs,vb}] dotnet_style_coalesce_expression = true:warning @@ -234,4 +245,10 @@ tab_width = 4 indent_size = 4 end_of_line = crlf dotnet_style_null_propagation = true:warning -indent_style = tab \ No newline at end of file +indent_style = tab +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:warning +dotnet_style_prefer_auto_properties = true:suggestion +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_prefer_simplified_boolean_expressions = true:warning +dotnet_diagnostic.CA2007.severity = error \ No newline at end of file diff --git a/Trie/ConcurrentTrie.cs b/Trie/ConcurrentTrie.cs index c360d65..2f7d096 100644 --- a/Trie/ConcurrentTrie.cs +++ b/Trie/ConcurrentTrie.cs @@ -6,22 +6,13 @@ namespace Open.Collections; /// /// A generic Trie collection. /// -public sealed class ConcurrentTrie - : TrieBase +public sealed class ConcurrentTrie( + IEqualityComparer? equalityComparer = null) + : TrieBase(() => new Node(equalityComparer)) where TKey : notnull { - /// - /// Constructs a . - /// - public ConcurrentTrie(IEqualityComparer? equalityComparer = null) - : base(() => new Node(equalityComparer)) - { } - - private sealed class Node : NodeBase + private sealed class Node(IEqualityComparer? equalityComparer) : NodeBase { - public Node(IEqualityComparer? equalityComparer) - => _equalityComparer = equalityComparer; - private readonly object _valueSync = new(); protected override void SetValue(TValue value) @@ -31,7 +22,7 @@ protected override void SetValue(TValue value) private readonly object _childSync = new(); - private readonly IEqualityComparer? _equalityComparer; + private readonly IEqualityComparer? _equalityComparer = equalityComparer; private ConcurrentDictionary>? _children; protected override void UpdateRecent(TKey key, ITrieNode child) diff --git a/Trie/Open.Collections.Trie.csproj b/Trie/Open.Collections.Trie.csproj index ca0fdee..aa1f8a0 100644 --- a/Trie/Open.Collections.Trie.csproj +++ b/Trie/Open.Collections.Trie.csproj @@ -26,6 +26,7 @@ snupkg logo.png README.md + IDE0130; @@ -44,11 +45,11 @@ - + - + \ No newline at end of file diff --git a/Trie/StringJoinPool.cs b/Trie/StringJoinPool.cs index 1280e0a..1905883 100644 --- a/Trie/StringJoinPool.cs +++ b/Trie/StringJoinPool.cs @@ -12,22 +12,12 @@ namespace Open.Collections; /// /// Useful for (re)generating cache keys. /// -public class StringJoinPool +public class StringJoinPool( + ITrie pool, ReadOnlyMemory separator) { - private readonly ReadOnlyMemory _separator; - private readonly ITrie _pool; + private readonly ITrie _pool = pool ?? throw new ArgumentNullException(nameof(pool)); private StringBuilder? _reusableBuilder; - /// - /// Constructs a . - /// - /// If the supplied pool is null. - public StringJoinPool(ITrie pool, ReadOnlyMemory separator) - { - _pool = pool ?? throw new ArgumentNullException(nameof(pool)); - _separator = separator; - } - /// public StringJoinPool(ITrie pool, string? separator = null) : this(pool, separator is null ? ReadOnlyMemory.Empty : separator.AsMemory()) @@ -75,7 +65,7 @@ string Build(ReadOnlySpan segments) int len = segments.Length; try { - if (_separator.IsEmpty) + if (separator.IsEmpty) { for (int i = 0; i < len; i++) AppendSegment(sb, segments[i]); @@ -85,7 +75,7 @@ string Build(ReadOnlySpan segments) Debug.Assert(segments.Length != 0); AppendSegment(sb, segments[0]); - var sepSpan = _separator.Span; + var sepSpan = separator.Span; int sLen = sepSpan.Length; for (int i = 1; i < len; i++) diff --git a/Trie/System.Diagnostics.CodeAnalysis/MaybeNullWhenAttribute.cs b/Trie/System.Diagnostics.CodeAnalysis/MaybeNullWhenAttribute.cs index a8f0c0c..ce5e8ea 100644 --- a/Trie/System.Diagnostics.CodeAnalysis/MaybeNullWhenAttribute.cs +++ b/Trie/System.Diagnostics.CodeAnalysis/MaybeNullWhenAttribute.cs @@ -1,4 +1,5 @@ #if NETSTANDARD2_0 + namespace System.Diagnostics.CodeAnalysis; // Use a shim for simplicity. @@ -6,18 +7,15 @@ namespace System.Diagnostics.CodeAnalysis; /// /// Indicates that the output may be null even if the corresponding type disallows it. /// +/// +/// Constructs a . +/// [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] -internal sealed class MaybeNullWhenAttribute : Attribute +internal sealed class MaybeNullWhenAttribute(bool returnValue) : Attribute { - /// - /// Constructs a . - /// - public MaybeNullWhenAttribute(bool returnValue) - => ReturnValue = returnValue; - /// /// The return value condition. /// - public bool ReturnValue { get; } + public bool ReturnValue { get; } = returnValue; } #endif \ No newline at end of file diff --git a/Trie/Trie.cs b/Trie/Trie.cs index 9ac5a8b..393c47d 100644 --- a/Trie/Trie.cs +++ b/Trie/Trie.cs @@ -4,34 +4,25 @@ namespace Open.Collections; /// /// A generic Trie collection. /// -public sealed class Trie - : TrieBase +public sealed class Trie( + IEqualityComparer? equalityComparer = null) + : TrieBase(() => new Node(equalityComparer)) where TKey : notnull { - /// - /// Constructs a . - /// - public Trie(IEqualityComparer? equalityComparer = null) - : base(() => new Node(equalityComparer)) - { } - - private sealed class Node : NodeBase + private sealed class Node(IEqualityComparer? equalityComparer) + : NodeBase { - private readonly IEqualityComparer? _equalityComparer; private Dictionary>? _children; - public Node(IEqualityComparer? equalityComparer) - => _equalityComparer = equalityComparer; - public override ITrieNode GetOrAddChild(TKey key) { var children = _children; if (children is null) - Children = _children = children = _equalityComparer is null ? new() : new(_equalityComparer); + Children = _children = children = equalityComparer is null ? new() : new(equalityComparer); else if (TryGetChildFrom(children, key, out var c)) return c; - var child = new Node(_equalityComparer); + var child = new Node(equalityComparer); children[key] = child; return child; } diff --git a/Trie/TrieBase.cs b/Trie/TrieBase.cs index 77c5dd2..0e65568 100644 --- a/Trie/TrieBase.cs +++ b/Trie/TrieBase.cs @@ -221,34 +221,23 @@ internal abstract class NodeBase : ITrieNode { protected IDictionary>? Children; - private readonly struct ValueContainer + private readonly struct ValueContainer(bool isSet, TValue value) { - public ValueContainer(bool isSet, TValue value) - { - IsSet = isSet; - Value = value; - } - public ValueContainer(TValue value) : this(true, value) { } - public bool IsSet { get; } - public TValue Value { get; } + public bool IsSet { get; } = isSet; + public TValue Value { get; } = value; } private ValueContainer _value; - private readonly struct Recent + private readonly struct Recent( + bool exists, TKey key, ITrieNode child) { - public Recent(bool exists, TKey key, ITrieNode child) - { - Exists = exists; - Key = key; - Child = child; - } - public bool Exists { get; } - public TKey Key { get; } - public ITrieNode Child { get; } + public bool Exists { get; } = exists; + public TKey Key { get; } = key; + public ITrieNode Child { get; } = child; } // It's not uncommon to have a 'hot path' that will be requested frequently. diff --git a/benchmarking/Benchmarks/CollectionBenchmark.cs b/benchmarking/Benchmarks/CollectionBenchmark.cs index 184f2d2..3d002bf 100644 --- a/benchmarking/Benchmarks/CollectionBenchmark.cs +++ b/benchmarking/Benchmarks/CollectionBenchmark.cs @@ -72,13 +72,9 @@ protected override IEnumerable TestOnceInternal() } } -public class CollectionBenchmark : CollectionBenchmark +public class CollectionBenchmark( + uint size, uint repeat, Func> factory) : CollectionBenchmark(size, repeat, factory, _ => new object()) { - public CollectionBenchmark(uint size, uint repeat, Func> factory) - : base(size, repeat, factory, _ => new object()) - { - } - public static TimedResult[] Results(uint size, uint repeat, Func> factory, Func itemFactory) => new CollectionBenchmark(size, repeat, factory, itemFactory).Result; diff --git a/benchmarking/Benchmarks/CollectionParallelBenchmark.cs b/benchmarking/Benchmarks/CollectionParallelBenchmark.cs index 67209b3..721e1c9 100644 --- a/benchmarking/Benchmarks/CollectionParallelBenchmark.cs +++ b/benchmarking/Benchmarks/CollectionParallelBenchmark.cs @@ -6,12 +6,10 @@ namespace Open.Collections; -public class CollectionParallelBenchmark : CollectionBenchmark +public class CollectionParallelBenchmark( + uint size, uint repeat, Func> factory, Func itemFactory) + : CollectionBenchmark(size, repeat, factory, itemFactory) { - public CollectionParallelBenchmark(uint size, uint repeat, Func> factory, Func itemFactory) : base(size, repeat, factory, itemFactory) - { - } - protected override IEnumerable TestOnceInternal() { ICollection c = Param(); @@ -115,13 +113,10 @@ protected override IEnumerable TestOnceInternal() } } -public class CollectionParallelBenchmark : CollectionParallelBenchmark +public class CollectionParallelBenchmark( + uint size, uint repeat, Func> factory) + : CollectionParallelBenchmark(size, repeat, factory, _ => new object()) { - public CollectionParallelBenchmark(uint size, uint repeat, Func> factory) - : base(size, repeat, factory, _ => new object()) - { - } - public static TimedResult[] Results(uint size, uint repeat, Func> factory, Func itemFactory) => new CollectionParallelBenchmark(size, repeat, factory, itemFactory).Result; diff --git a/benchmarking/Benchmarks/DictionaryParallelBenchmark.cs b/benchmarking/Benchmarks/DictionaryParallelBenchmark.cs index 9b94aff..dd876d7 100644 --- a/benchmarking/Benchmarks/DictionaryParallelBenchmark.cs +++ b/benchmarking/Benchmarks/DictionaryParallelBenchmark.cs @@ -7,13 +7,10 @@ namespace Open.Collections; -public class DictionaryParallelBenchmark : CollectionParallelBenchmark> +public class DictionaryParallelBenchmark( + uint size, uint repeat, Func> factory) + : CollectionParallelBenchmark>(size, repeat, factory, i => new KeyValuePair(i, new object())) { - public DictionaryParallelBenchmark(uint size, uint repeat, Func> factory) - : base(size, repeat, factory, i => new KeyValuePair(i, new object())) - { - } - protected override IEnumerable TestOnceInternal() { //foreach (TimedResult t in base.TestOnceInternal()) @@ -23,6 +20,7 @@ protected override IEnumerable TestOnceInternal() int testSize; checked { testSize = (int)TestSize; } + const int mixSize = 100; var c = (IDictionary)Param(); yield return TimedResult.Measure("Fill (.Add(keyValuePair)) (In Parallel)", @@ -32,8 +30,8 @@ protected override IEnumerable TestOnceInternal() () => Parallel.For(0, testSize, i => c[i] = _items[i].Value)); #if DEBUG - for (int i = 0; i < testSize; ++i) - Debug.Assert(c[i] == _items[i].Value); + for (int i = 0; i < testSize; ++i) + Debug.Assert(c[i] == _items[i].Value); #endif //yield return TimedResult.Measure("Enumerate (8 times)", () => diff --git a/benchmarking/Benchmarks/LinkedListBenchmark.cs b/benchmarking/Benchmarks/LinkedListBenchmark.cs index fc5a0a8..c5c8655 100644 --- a/benchmarking/Benchmarks/LinkedListBenchmark.cs +++ b/benchmarking/Benchmarks/LinkedListBenchmark.cs @@ -4,12 +4,10 @@ namespace Open.Collections; -public class LinkedListBenchmark : BenchmarkBase>> +public class LinkedListBenchmark( + uint size, uint repeat, Func> factory) + : BenchmarkBase>>(size, repeat, factory) { - public LinkedListBenchmark(uint size, uint repeat, Func> factory) : base(size, repeat, factory) - { - } - protected readonly object _item = new(); protected override IEnumerable TestOnceInternal() diff --git a/benchmarking/Benchmarks/LinkedListParallelBenchmark.cs b/benchmarking/Benchmarks/LinkedListParallelBenchmark.cs index c29c025..ae5990b 100644 --- a/benchmarking/Benchmarks/LinkedListParallelBenchmark.cs +++ b/benchmarking/Benchmarks/LinkedListParallelBenchmark.cs @@ -5,12 +5,10 @@ namespace Open.Collections; -public class LinkedListParallelBenchmark : LinkedListBenchmark +public class LinkedListParallelBenchmark( + uint size, uint repeat, Func> factory) + : LinkedListBenchmark(size, repeat, factory) { - public LinkedListParallelBenchmark(uint size, uint repeat, Func> factory) : base(size, repeat, factory) - { - } - protected override IEnumerable TestOnceInternal() { ILinkedList c = Param(); diff --git a/benchmarking/Benchmarks/ListParallelBenchmark.cs b/benchmarking/Benchmarks/ListParallelBenchmark.cs index 9a8e061..3f17069 100644 --- a/benchmarking/Benchmarks/ListParallelBenchmark.cs +++ b/benchmarking/Benchmarks/ListParallelBenchmark.cs @@ -4,13 +4,10 @@ namespace Open.Collections; -public class ListParallelBenchmark : CollectionParallelBenchmark +public class ListParallelBenchmark( + uint size, uint repeat, Func> factory) + : CollectionParallelBenchmark(size, repeat, factory) { - public ListParallelBenchmark(uint size, uint repeat, Func> factory) - : base(size, repeat, factory) - { - } - // Get/Set (mutating entry) operations have no benefit to synchronization and are inherently thread safe. //protected override IEnumerable TestOnceInternal() //{ diff --git a/benchmarking/Benchmarks/QueueBenchmark.cs b/benchmarking/Benchmarks/QueueBenchmark.cs index 0839c64..42a7f10 100644 --- a/benchmarking/Benchmarks/QueueBenchmark.cs +++ b/benchmarking/Benchmarks/QueueBenchmark.cs @@ -4,13 +4,8 @@ namespace Open.Collections; -public class QueueBenchmark : BenchmarkBase>> +public class QueueBenchmark(uint size, uint repeat, Func> queueFactory) : BenchmarkBase>>(size, repeat, queueFactory) { - public QueueBenchmark(uint size, uint repeat, Func> queueFactory) - : base(size, repeat, queueFactory) - { - } - protected readonly object _item = new(); protected override IEnumerable TestOnceInternal() diff --git a/benchmarking/Benchmarks/QueueParallelBenchmark.cs b/benchmarking/Benchmarks/QueueParallelBenchmark.cs index f9d23d3..dde600d 100644 --- a/benchmarking/Benchmarks/QueueParallelBenchmark.cs +++ b/benchmarking/Benchmarks/QueueParallelBenchmark.cs @@ -5,12 +5,8 @@ namespace Open.Collections; -public class QueueParallelBenchmark : QueueBenchmark +public class QueueParallelBenchmark(uint size, uint repeat, Func> factory) : QueueBenchmark(size, repeat, factory) { - public QueueParallelBenchmark(uint size, uint repeat, Func> factory) : base(size, repeat, factory) - { - } - protected override IEnumerable TestOnceInternal() { IQueue queue = Param(); diff --git a/benchmarking/Benchmarks/SubsetBufferedBench.cs b/benchmarking/Benchmarks/SubsetBufferedBench.cs index 81553f0..40525ce 100644 --- a/benchmarking/Benchmarks/SubsetBufferedBench.cs +++ b/benchmarking/Benchmarks/SubsetBufferedBench.cs @@ -1,6 +1,5 @@ using BenchmarkDotNet.Attributes; using System; -using System.Collections.Generic; using System.Linq; namespace Open.Collections.Benchmarks; @@ -22,7 +21,7 @@ namespace Open.Collections.Benchmarks; //[System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1822:Mark members as static")] public class SubsetBufferedBench { - IReadOnlyList FullSet = Array.Empty(); + int[] FullSet = Array.Empty(); ReadOnlyMemory FullMemorySet = ReadOnlyMemory.Empty; [Params(3, 7)] @@ -31,7 +30,7 @@ public class SubsetBufferedBench [Params(9, 32)] public int Range { - get => FullSet.Count; + get => FullSet.Length; set { int[] s = Enumerable.Range(0, value).ToArray(); diff --git a/benchmarking/Benchmarks/TrieBenchmarks.cs b/benchmarking/Benchmarks/TrieBenchmarks.cs index 881cfe1..2c173ff 100644 --- a/benchmarking/Benchmarks/TrieBenchmarks.cs +++ b/benchmarking/Benchmarks/TrieBenchmarks.cs @@ -35,7 +35,7 @@ public void GlobalSetup() { int x = 0; trie = new(); - dictionary = new(); + dictionary = []; ctrie = new(); cdictionary = new(); keys = GenerateTree(Depth, NodeSize) @@ -70,6 +70,7 @@ public static IEnumerable GenerateTree(int depth, int nodeCount, Stack { yield return key; } + stack.Pop(); } } @@ -82,6 +83,7 @@ public int TrieLookup() { trie.TryGetValue(key.Item1, out result); } + return result; } @@ -94,6 +96,7 @@ public int TrieWalkLookup() string[] k = key.Item1; result = trie.GetChild(k[0]).GetChild(k[1]).GetChild(k[2]).GetChild(k[3]).Value; } + return result; } @@ -105,6 +108,7 @@ public int TrieLookupWithToArray() { trie.TryGetValue(key.Item1.ToArray(), out result); } + return result; } @@ -116,6 +120,7 @@ public int DictionaryLookup() { result = dictionary[key.Item2]; } + return result; } @@ -128,6 +133,7 @@ public int DictionaryLookupKeyConcat() string[] k = key.Item1; result = dictionary[$"{k[0]}/{k[1]}/{k[2]}/{k[3]}"]; } + return result; } @@ -139,6 +145,7 @@ public int DictionaryLookupWithJoin() { result = dictionary[string.Join("/", key.Item1)]; } + return result; } diff --git a/benchmarking/Open.Collections.Benchmarking.csproj b/benchmarking/Open.Collections.Benchmarking.csproj index cd1aa13..4e951ed 100644 --- a/benchmarking/Open.Collections.Benchmarking.csproj +++ b/benchmarking/Open.Collections.Benchmarking.csproj @@ -2,9 +2,10 @@ Exe - net7.0 + net9.0 Open.Collections latest + IDE0305;IDE0301;IDE0130; @@ -14,7 +15,7 @@ - + diff --git a/benchmarking/Program.cs b/benchmarking/Program.cs index 3e0d9c3..9e20caa 100644 --- a/benchmarking/Program.cs +++ b/benchmarking/Program.cs @@ -13,6 +13,7 @@ namespace Open.Collections.Benchmarks; [System.Diagnostics.CodeAnalysis.SuppressMessage("Roslynator", "RCS1213:Remove unused member declaration.", Justification = "")] internal static class Program { + [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0022:Use expression body for method")] static void Main() { //BenchmarkRunner.Run(); @@ -141,6 +142,7 @@ static void LinkedListTests() // report.Test(1000); //} + [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0028:Simplify collection initialization")] static void ListTests() { Console.WriteLine("::: Synchronized Lists :::\n"); @@ -164,6 +166,7 @@ static void ListTests() report.Test(4000, 4); } + [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0028:Simplify collection initialization")] static void HashSetTests() { Console.WriteLine("::: Synchronized HashSets :::\n"); diff --git a/source/ArrayPoolSegment.cs b/source/ArrayPoolSegment.cs index 3c28cc4..7abf862 100644 --- a/source/ArrayPoolSegment.cs +++ b/source/ArrayPoolSegment.cs @@ -1,6 +1,5 @@ -using System; -using System.Buffers; -using System.Diagnostics.CodeAnalysis; +using System.Buffers; +using System.Collections; namespace Open.Collections; @@ -8,28 +7,71 @@ namespace Open.Collections; /// Represents a segment of an array rented from an . /// /// The type of the elements in the array. -public readonly struct ArrayPoolSegment : IDisposable +public readonly struct ArrayPoolSegment : IDisposable, IEnumerable { + /// + /// The segment of the array. + /// public readonly ArraySegment Segment; + + /// + /// The used to rent the array. + /// public readonly ArrayPool? Pool; + private readonly bool _clear; + /// + /// Constructs a new . + /// public ArrayPoolSegment( - int length, + ArraySegment segment, ArrayPool? pool = null, bool clearArrayOnDispose = false) { + Segment = segment; + Pool = pool; _clear = clearArrayOnDispose; + } + + /// + /// Constructs a new from the . + /// + public ArrayPoolSegment( + int length, + ArrayPool? pool = null, + bool clearArrayOnDispose = false) + { Pool = pool; T[]? array = pool?.Rent(length) ?? new T[length]; Segment = new(array, 0, length); + _clear = clearArrayOnDispose; } + /// + /// Forms a slice out of the segment + /// starting at the specified . + /// + public ArrayPoolSegment Slice(int index) + => new(Segment.Slice(index), Pool, _clear); + + /// + /// Forms a slice out of the segment + /// starting at the specified + /// and extending for the . + /// + public ArrayPoolSegment Slice(int index, int count) + => new(Segment.Slice(index, count), Pool, _clear); + /// /// Returns the array to the pool. /// /// - public void Dispose() => Pool?.Return(Segment.Array, _clear); + public void Dispose() => Pool?.Return(Segment.Array!, _clear); + + /// + public IEnumerator GetEnumerator() => Segment.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); /// /// Implicitly converts the to an . diff --git a/source/CollectionWrapper.cs b/source/CollectionWrapper.cs index ce35ff2..2e1b664 100644 --- a/source/CollectionWrapper.cs +++ b/source/CollectionWrapper.cs @@ -1,29 +1,38 @@ -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; - -namespace Open.Collections; +namespace Open.Collections; +/// +/// A disposable wrapper for a collection. +/// [ExcludeFromCodeCoverage] -public class CollectionWrapper - : ReadOnlyCollectionWrapper, ICollection, IAddMultiple +public class CollectionWrapper( + TCollection source, bool owner = false) + : ReadOnlyCollectionWrapper(source, owner), ICollection, IAddMultiple where TCollection : class, ICollection { - public CollectionWrapper(TCollection source, bool owner = false) - : base(source, owner) - { - } - - protected readonly object Sync = new(); // Could possibly override.. - /// /// The underlying object used for synchronization. + /// +#if NET9_0_OR_GREATER + protected readonly Lock Sync = new(); +#else + protected readonly object Sync = new(); +#endif + + /// + /// The object used for synchronization. /// This is exposed to allow for more complex synchronization operations. /// +#if NET9_0_OR_GREATER + public Lock SyncRoot => Sync; +#else public object SyncRoot => Sync; +#endif #region Implementation of ICollection + /// + /// Manages adding an item to the collection. + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] protected virtual void AddInternal(in T item) => InternalUnsafeSource!.Add(item); @@ -36,8 +45,13 @@ public virtual void Add(T item) AddInternal(in item); } +#if NET9_0_OR_GREATER + /// + public virtual void AddThese(T item1, T item2, params ReadOnlySpan items) +#else /// public virtual void AddThese(T item1, T item2, params T[] items) +#endif { AssertIsAlive(); AddInternal(in item1); @@ -47,13 +61,26 @@ public virtual void AddThese(T item1, T item2, params T[] items) } /// - /// Adds mutliple items to the collection. + /// Adds multiple items to the collection. /// It's important to avoid locking for too long so an array is used to add multiple items. /// An enumerable is potentially slow as it may be yielding to a process. /// /// The items to add. [MethodImpl(MethodImplOptions.AggressiveInlining)] public virtual void AddRange(IEnumerable items) + { + AssertIsAlive(); + if (items is null) return; + foreach (var i in items) + AddInternal(in i); + } + + /// +#if NET9_0_OR_GREATER + [OverloadResolutionPriority(1)] +#endif + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public virtual void AddRange(ReadOnlySpan items) { AssertIsAlive(); foreach (var i in items) diff --git a/source/ConcurrentHashSet.cs b/source/ConcurrentHashSet.cs index e55ab57..3132bde 100644 --- a/source/ConcurrentHashSet.cs +++ b/source/ConcurrentHashSet.cs @@ -1,12 +1,17 @@ using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; namespace Open.Collections; +/// +/// A thread-safe hash by wrapping a . +/// [ExcludeFromCodeCoverage] public sealed class ConcurrentHashSet : DictionaryToHashSetWrapper + where T : notnull { + /// + /// Construct a new instance with optional initial values. + /// public ConcurrentHashSet(IEnumerable? intialValues = null) : base(new ConcurrentDictionary()) { diff --git a/source/DictionaryToHashSetWrapper.cs b/source/DictionaryToHashSetWrapper.cs index 6f7a81a..76b98e1 100644 --- a/source/DictionaryToHashSetWrapper.cs +++ b/source/DictionaryToHashSetWrapper.cs @@ -1,17 +1,19 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; +using System.Collections; namespace Open.Collections; -public class DictionaryToHashSetWrapper : ISet +/// +/// A wrapper for a to implement . +/// +[method: ExcludeFromCodeCoverage] +public class DictionaryToHashSetWrapper( + IDictionary source) + : ISet { - protected readonly IDictionary InternalSource; - - [ExcludeFromCodeCoverage] - public DictionaryToHashSetWrapper(IDictionary source) - => InternalSource = source; + /// + /// The internal source dictionary. + /// + protected readonly IDictionary InternalSource = source; /// [ExcludeFromCodeCoverage] @@ -38,6 +40,7 @@ public virtual bool Add(T item) { return false; } + return true; } @@ -65,7 +68,7 @@ public virtual bool Add(T item) /// Returns a copy of the underlying keys. /// [ExcludeFromCodeCoverage] - public HashSet ToHashSet() => new(InternalSource.Keys); + public HashSet ToHashSet() => [.. InternalSource.Keys]; /// [ExcludeFromCodeCoverage] diff --git a/source/DictionaryWrapper.cs b/source/DictionaryWrapper.cs index 0e9a6b8..f075f69 100644 --- a/source/DictionaryWrapper.cs +++ b/source/DictionaryWrapper.cs @@ -1,13 +1,10 @@ -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; - -namespace Open.Collections; +namespace Open.Collections; /// [ExcludeFromCodeCoverage] public class DictionaryWrapper : DictionaryWrapperBase> + where TKey : notnull { /// public DictionaryWrapper() @@ -33,18 +30,21 @@ protected override TValue GetValueInternal(TKey key) protected override void SetValueInternal(TKey key, TValue value) => InternalSource[key] = value; + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override ICollection GetKeys() => new ReadOnlyCollectionAdapter( ThrowIfDisposed(InternalSource.Keys), () => InternalSource.Count); + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override ICollection GetValues() => new ReadOnlyCollectionAdapter( ThrowIfDisposed(InternalSource.Values), () => InternalSource.Count); + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void AddInternal(TKey key, TValue value) => InternalSource.Add(key, value); @@ -61,6 +61,11 @@ public override bool Remove(TKey key) /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override bool TryGetValue(TKey key, out TValue value) + public override bool TryGetValue(TKey key, +#if NET9_0_OR_GREATER + [MaybeNullWhen(false)] +#else +#endif + out TValue value) => InternalSource.TryGetValue(key, out value); } diff --git a/source/DictionaryWrapperBase.cs b/source/DictionaryWrapperBase.cs index a6e8273..61042fd 100644 --- a/source/DictionaryWrapperBase.cs +++ b/source/DictionaryWrapperBase.cs @@ -1,19 +1,15 @@ -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; - -namespace Open.Collections; +namespace Open.Collections; +/// +/// A base class for wrapping a collection as a dictionary. +/// [ExcludeFromCodeCoverage] -public abstract class DictionaryWrapperBase - : CollectionWrapper, TCollection>, IDictionary +public abstract class DictionaryWrapperBase( + TCollection source, bool owner = false) + : CollectionWrapper, TCollection>(source, owner), IDictionary + where TKey : notnull where TCollection : class, ICollection> { - protected DictionaryWrapperBase(TCollection source, bool owner = false) - : base(source, owner) - { - } - /// public TValue this[TKey key] { @@ -21,13 +17,23 @@ public TValue this[TKey key] set => SetValueInternal(key, value); } + /// + /// Get the value for the key. + /// protected abstract TValue GetValueInternal(TKey key); + /// + /// Set the value for the key. + /// protected abstract void SetValueInternal(TKey key, TValue value); ICollection? _keys; /// public ICollection Keys => _keys ??= GetKeys(); + + /// + /// Get the keys. + /// protected abstract ICollection GetKeys(); ICollection? _values; @@ -35,8 +41,14 @@ public TValue this[TKey key] /// public ICollection Values => _values ??= GetValues(); + /// + /// Get the values. + /// protected abstract ICollection GetValues(); + /// + /// Add a key and value to the dictionary. + /// protected abstract void AddInternal(TKey key, TValue value); /// @@ -51,5 +63,10 @@ public void Add(TKey key, TValue value) public abstract bool Remove(TKey key); /// - public abstract bool TryGetValue(TKey key, out TValue value); + public abstract bool TryGetValue(TKey key, +#if NET9_0_OR_GREATER + [MaybeNullWhen(false)] +#else +#endif + out TValue value); } diff --git a/source/Extensions.ByteArray.cs b/source/Extensions.ByteArray.cs index bbdc3ad..d82b3e8 100644 --- a/source/Extensions.ByteArray.cs +++ b/source/Extensions.ByteArray.cs @@ -1,6 +1,4 @@ -using System; -using System.Diagnostics.Contracts; -using System.Text; +using System.Text; namespace Open.Collections; diff --git a/source/Extensions.Combinations.cs b/source/Extensions.Combinations.cs index 57fa121..cc70231 100644 --- a/source/Extensions.Combinations.cs +++ b/source/Extensions.Combinations.cs @@ -1,10 +1,4 @@ -using System; -using System.Buffers; -using System.Collections.Generic; -using System.Diagnostics; -using System.Diagnostics.Contracts; -using System.Linq; -using System.Runtime.CompilerServices; +using System.Buffers; namespace Open.Collections; @@ -150,6 +144,9 @@ public static IEnumerable> CombinationsDistinct(this IEnumera return source.Count == 0 ? Enumerable.Empty>() : CombinationsCore(source, true, buffer); } + /// + /// Enumerates all possible combinations of values. + /// [Obsolete("Deprecated in favor of using .Subsets(length) or .Combinations(length) depending on intent.")] public static IEnumerable Combinations(this IEnumerable elements, int length, bool uniqueOnly) { @@ -160,13 +157,13 @@ public static IEnumerable Combinations(this IEnumerable elements, int Contract.EndContractBlock(); if (length == 0) return Enumerable.Empty(); - IReadOnlyList? source = elements as IReadOnlyList ?? elements.ToArray(); + IReadOnlyList source = elements as IReadOnlyList ?? elements.ToArray(); int count = source.Count; return count == 0 ? Enumerable.Empty() : uniqueOnly ? source.Subsets(length) - : CombinationsCore(source, length, true).Select(e => e.Array.AsCopy(length)); + : CombinationsCore(source, length, true).Select(e => e.Array!.ToArrayOfLength(length)); } /// @@ -241,7 +238,7 @@ public static IEnumerable CombinationsDistinct(this IEnumerable eleme /// Enumerates all possible combinations of values. /// Results can be different permutations of another set. /// - /// [0, 0], [0, 1], [1, 0], [1, 1] where [0, 1] and [1, 0] are a different permutatation of the same set. + /// [0, 0], [0, 1], [1, 0], [1, 1] where [0, 1] and [1, 0] are a different permutation of the same set. /// The elements to draw from. public static IEnumerable Combinations(this IEnumerable elements) { @@ -255,7 +252,7 @@ public static IEnumerable Combinations(this IEnumerable elements) /// Enumerates all possible distinct set combinations. /// In contrast a set that has its items reordered is not distinct from the original. /// - /// [0, 0], [0, 1], [1, 1] where [1, 0] is not included as it is not a disticnt set from [0, 1]. + /// [0, 0], [0, 1], [1, 1] where [1, 0] is not included as it is not a distinct set from [0, 1]. /// The elements to draw from. public static IEnumerable CombinationsDistinct(this IEnumerable elements) { diff --git a/source/Extensions.ConcurrentBag.cs b/source/Extensions.ConcurrentBag.cs index 8278c21..d88a3ba 100644 --- a/source/Extensions.ConcurrentBag.cs +++ b/source/Extensions.ConcurrentBag.cs @@ -1,22 +1,20 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Diagnostics.Contracts; -using System.Linq; -using System.Threading.Tasks; +using System.Collections.Concurrent; namespace Open.Collections; public static partial class Extensions { + /// + /// Attempts to take items from the while the is true. + /// public static IEnumerable TryTakeWhile(this ConcurrentBag target, Func, bool> predicate) { if (target is null) throw new ArgumentNullException(nameof(target)); Contract.EndContractBlock(); - return TryTakeWhileCpre(target, predicate); + return TryTakeWhileCore(target, predicate); - static IEnumerable TryTakeWhileCpre(ConcurrentBag target, Func, bool> predicate) + static IEnumerable TryTakeWhileCore(ConcurrentBag target, Func, bool> predicate) { while (!target.IsEmpty && predicate(target) && target.TryTake(out T? value)) { @@ -25,6 +23,7 @@ static IEnumerable TryTakeWhileCpre(ConcurrentBag target, Func public static IEnumerable TryTakeWhile(this ConcurrentBag target, Func predicate) { if (target is null) throw new ArgumentNullException(nameof(target)); @@ -33,6 +32,9 @@ public static IEnumerable TryTakeWhile(this ConcurrentBag target, Func< return TryTakeWhile(target, _ => predicate()); } + /// + /// Trims the to the specified . + /// public static void Trim(this ConcurrentBag target, int maxSize) { foreach (T? _ in TryTakeWhile(target, t => t.Count > maxSize)) @@ -40,6 +42,9 @@ public static void Trim(this ConcurrentBag target, int maxSize) } } + /// + /// Trims the to the specified and calls the for each trimmed item. + /// public static Task TrimAsync(this ConcurrentBag target, int maxSize, Action handler) { if (target is null) throw new ArgumentNullException(nameof(target)); @@ -52,5 +57,9 @@ public static Task TrimAsync(this ConcurrentBag target, int maxSize, Actio ); } - public static Task ClearAsync(this ConcurrentBag target, Action handler) => TrimAsync(target, 0, handler); + /// + /// Clears the and calls the for each item. + /// + public static Task ClearAsync(this ConcurrentBag target, Action handler) + => TrimAsync(target, 0, handler); } diff --git a/source/Extensions.ConcurrentDictionary.cs b/source/Extensions.ConcurrentDictionary.cs index b0d1d7a..b716dce 100644 --- a/source/Extensions.ConcurrentDictionary.cs +++ b/source/Extensions.ConcurrentDictionary.cs @@ -1,16 +1,14 @@ -using System; -using System.Collections.Concurrent; -using System.Diagnostics.Contracts; -using System.Threading.Tasks; +using System.Collections.Concurrent; namespace Open.Collections; public static partial class Extensions { /// - /// Shortcut for removeing a value without needing an 'out' parameter. + /// Shortcut for removing a value without needing an 'out' parameter. /// public static bool TryRemove(this ConcurrentDictionary target, TKey key) + where TKey : notnull { if (target is null) throw new ArgumentNullException(nameof(target)); Contract.EndContractBlock(); @@ -28,6 +26,7 @@ public static TValue GetOrAdd( out bool updated, TKey key, Func valueFactory) + where TKey : notnull { if (source is null) throw new ArgumentNullException(nameof(source)); @@ -59,6 +58,7 @@ public static TValue GetOrAdd( out bool updated, TKey key, TValue value) + where TKey : notnull { if (source is null) throw new ArgumentNullException(nameof(source)); @@ -81,7 +81,10 @@ public static TValue GetOrAdd( /// /// Will return true if the existing value is past due. /// - public static bool UpdateRequired(this ConcurrentDictionary source, TKey key, TimeSpan timeBeforeExpires) + public static bool UpdateRequired( + this ConcurrentDictionary source, TKey key, TimeSpan timeBeforeExpires) + where TKey : notnull { if (source is null) throw new ArgumentNullException(nameof(source)); @@ -113,6 +116,7 @@ public static Lazy GetOrAddSafely( this ConcurrentDictionary> source, TKey key, Func valueFactory) + where TKey : notnull { if (source is null) throw new ArgumentNullException(nameof(source)); @@ -144,6 +148,7 @@ public static Lazy> GetOrAddSafely( this ConcurrentDictionary>> source, TKey key, Func> valueFactory) + where TKey : notnull { if (source is null) throw new ArgumentNullException(nameof(source)); diff --git a/source/Extensions.Generic.Synchronized.cs b/source/Extensions.Generic.Synchronized.cs index eade5b2..bd17e43 100644 --- a/source/Extensions.Generic.Synchronized.cs +++ b/source/Extensions.Generic.Synchronized.cs @@ -1,8 +1,4 @@ using Open.Threading; -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Diagnostics.Contracts; namespace Open.Collections; @@ -18,18 +14,23 @@ internal static void ValidateMillisecondsTimeout(int? millisecondsTimeout) } /// - /// Thread safe value for syncronizing acquiring a value from a generic dictionary. + /// Thread safe value for synchronizing acquiring a value from a generic dictionary. /// /// True if a value was acquired. public static bool TryGetValueSynchronized( this IDictionary target, - TKey key, out TValue value) + TKey key, +#if NETSTANDARD2_0 +#else + [MaybeNullWhen(false)] +#endif + out TValue value) { if (target is null) throw new ArgumentNullException(nameof(target)); if (key is null) throw new ArgumentNullException(nameof(key)); Contract.EndContractBlock(); - TValue result = default!; + TValue? result = default; bool success = ThreadSafety.SynchronizeRead(target, key, () => ThreadSafety.SynchronizeRead(target, () => target.TryGetValue(key, out result) @@ -42,10 +43,10 @@ public static bool TryGetValueSynchronized( } /// - /// Attempts to acquire a specified type from a generic dictonary. + /// Attempts to acquire a specified type from a generic dictionary. /// - [SuppressMessage("Style", "IDE0046:Convert to conditional expression")] - public static TValue GetValueSynchronized(this IDictionary target, TKey key, bool throwIfNotExists = true) + public static TValue GetValueSynchronized( + this IDictionary target, TKey key) { if (target is null) throw new ArgumentNullException(nameof(target)); if (key is null) throw new ArgumentNullException(nameof(key)); @@ -53,14 +54,26 @@ public static TValue GetValueSynchronized(this IDictionary + /// Attempts to acquire a specified type from a generic dictionary or returns a default value. + /// + public static TValue GetValueSynchronized( + this IDictionary target, TKey key, TValue defaultValue) + { + if (target is null) throw new ArgumentNullException(nameof(target)); + if (key is null) throw new ArgumentNullException(nameof(key)); + Contract.EndContractBlock(); + + bool exists = target.TryGetValueSynchronized(key, out TValue? value); + + return exists ? value! : defaultValue; } /// - /// Thread safe value for syncronizing adding a value to list only if it does not exist. + /// Thread safe value for synchronizing adding a value to list only if it does not exist. /// public static void RegisterSynchronized(this ICollection target, T value) { @@ -74,12 +87,13 @@ public static void RegisterSynchronized(this ICollection target, T value) } /// - /// Thread safe shortcut for adding a value or updating based on exising value. + /// Thread safe shortcut for adding a value or updating based on existing value. /// If no value exists, it adds the provided value. /// If a value exists, it sets the value using the updateValueFactory. /// public static T AddOrUpdateSynchronized(this IDictionary target, TKey key, T value, Func updateValueFactory) + where TKey : notnull { if (target is null) throw new ArgumentNullException(nameof(target)); if (key is null) throw new ArgumentNullException(nameof(key)); @@ -101,7 +115,7 @@ public static T AddOrUpdateSynchronized(this IDictionary targe } else { - // Fallback for if the action changed. Will end up locking the collection but what can we do.. :( + // Fall-back for if the action changed. Will end up locking the collection but what can we do.. :( ThreadSafety.SynchronizeWrite(target, () => valueUsed = target.AddOrUpdate(key, value, updateValueFactory)); } }); @@ -110,13 +124,14 @@ public static T AddOrUpdateSynchronized(this IDictionary targe } /// - /// Thread safe shortcut for adding a value or updating based on exising value. + /// Thread safe shortcut for adding a value or updating based on existing value. /// If no value exists, it adds the value using the newValueFactory. /// If a value exists, it sets the value using the updateValueFactory. /// public static T AddOrUpdateSynchronized(this IDictionary target, TKey key, Func newValueFactory, Func updateValueFactory) + where TKey : notnull { if (target is null) throw new ArgumentNullException(nameof(target)); if (key is null) throw new ArgumentNullException(nameof(key)); @@ -159,6 +174,10 @@ public static T AddOrUpdateSynchronized(this IDictionary targe return valueUsed; } + /// + /// Thread safe shortcut for adding a value to list. + /// + /// public static void AddSynchronized(this ICollection target, T value) { if (target is null) throw new ArgumentNullException(nameof(target)); @@ -169,12 +188,13 @@ public static void AddSynchronized(this ICollection target, T value) /// Thread safe shortcut for adding a value to list within a dictionary. /// public static void AddToSynchronized(this IDictionary> c, TKey key, TValue value) + where TKey : notnull { if (c is null) throw new ArgumentNullException(nameof(c)); if (key is null) throw new ArgumentNullException(nameof(key)); Contract.EndContractBlock(); - IList? list = c.GetOrAddSynchronized(key, _ => new List()); + IList? list = c.GetOrAddSynchronized(key, _ => []); list.AddSynchronized(value); } @@ -182,6 +202,7 @@ public static void AddToSynchronized(this IDictionary public static void EnsureDefaultSynchronized(this IDictionary target, TKey key, T defaultValue) + where TKey : notnull { if (target is null) throw new ArgumentNullException(nameof(target)); if (key is null) throw new ArgumentNullException(nameof(key)); @@ -197,6 +218,7 @@ public static void EnsureDefaultSynchronized(this IDictionary /// public static void EnsureDefaultSynchronized(this IDictionary target, TKey key, Func defaultValueFactory) + where TKey : notnull { if (target is null) throw new ArgumentNullException(nameof(target)); if (key is null) throw new ArgumentNullException(nameof(key)); @@ -218,13 +240,14 @@ public static T GetOrAddSynchronized( T value, int millisecondsTimeout = SYNC_TIMEOUT_DEFAULT_MILLISECONDS, bool throwsOnTimeout = true) + where TKey : notnull { if (target is null) throw new ArgumentNullException(nameof(target)); if (key is null) throw new ArgumentNullException(nameof(key)); ValidateMillisecondsTimeout(millisecondsTimeout); Contract.EndContractBlock(); - T result = default!; + T? result = default; bool condition(bool _) => !target.TryGetValue(key, out result); void render() @@ -236,7 +259,7 @@ void render() if (!ThreadSafety.SynchronizeReadWrite(target, condition, render, millisecondsTimeout, throwsOnTimeout)) return value; // Value doesn't exist and timeout exceeded? Return the add value... - return result; + return result!; } /// @@ -248,6 +271,7 @@ public static T GetOrAddSynchronized( TKey key, Func valueFactory, int millisecondsTimeout = SYNC_TIMEOUT_DEFAULT_MILLISECONDS) + where TKey : notnull { if (target is null) throw new ArgumentNullException(nameof(target)); if (key is null) throw new ArgumentNullException(nameof(key)); @@ -255,11 +279,11 @@ public static T GetOrAddSynchronized( ValidateMillisecondsTimeout(millisecondsTimeout); Contract.EndContractBlock(); - T result = default!; + T? result = default; // Note, the following sync read is on the TARGET and not the key. See below. bool condition(bool _) => !ThreadSafety.SynchronizeRead(target, () => target.TryGetValue(key, out result)); - // Once a per value write lock is established, execute the scheduler, and syncronize adding... + // Once a per value write lock is established, execute the scheduler, and synchronize adding... void render() => target.GetOrAddSynchronized(key, result = valueFactory(key), millisecondsTimeout); // This will queue up subsequent reads for the same value. @@ -269,12 +293,12 @@ public static T GetOrAddSynchronized( // ^^^ What actually happens... // 1) Value is checked for without a lock and if acquired returns it using the 'condition' query. // 2) Value is checked for WITH a lock and if acquired returns it using the 'condition' query. - // 3) A localized lock is acquired for the the key which tells other _threads to wait while the value is generated and added. + // 3) A localized lock is acquired for the key which tells other _threads to wait while the value is generated and added. // 4) Value is checked for without a lock and if acquired returns it using the 'condition' query. // 5) The value is then rendered using the ensureRendered query without locking the entire collection. This allows for other values to be added. // 6) The rendered value is then used to add to the collection if the value is missing, locking the collection if an add is necessary. - return result; + return result!; } /// @@ -288,6 +312,7 @@ public static bool TryAddSynchronized( TKey key, T value, int millisecondsTimeout = SYNC_TIMEOUT_DEFAULT_MILLISECONDS) + where TKey : notnull { if (target is null) throw new ArgumentNullException(nameof(target)); if (key is null) throw new ArgumentNullException(nameof(key)); @@ -316,6 +341,7 @@ public static bool TryAddSynchronized( TKey key, Func valueFactory, int millisecondsTimeout = SYNC_TIMEOUT_DEFAULT_MILLISECONDS) + where TKey : notnull { if (target is null) throw new ArgumentNullException(nameof(target)); if (key is null) throw new ArgumentNullException(nameof(key)); @@ -343,6 +369,7 @@ public static bool TryRemoveSynchronized( this IDictionary target, TKey key, int millisecondsTimeout = SYNC_TIMEOUT_DEFAULT_MILLISECONDS) + where TKey : notnull { if (target is null) throw new ArgumentNullException(nameof(target)); if (key is null) throw new ArgumentNullException(nameof(key)); @@ -368,6 +395,7 @@ public static bool TryRemoveSynchronized( TKey key, out T value, int millisecondsTimeout = SYNC_TIMEOUT_DEFAULT_MILLISECONDS) + where TKey : notnull { if (target is null) throw new ArgumentNullException(nameof(target)); if (key is null) throw new ArgumentNullException(nameof(key)); diff --git a/source/Extensions.Generic.cs b/source/Extensions.Generic.cs index a7b9a0c..9c7505a 100644 --- a/source/Extensions.Generic.cs +++ b/source/Extensions.Generic.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.Contracts; - -namespace Open.Collections; +namespace Open.Collections; public static partial class Extensions { @@ -27,8 +23,8 @@ public static void SetOrRemove( /// /// Adds a value to list only if it does not exist. - /// NOT THREAD SAFE: Use only when a collection local or is assured single threaded. /// + /// NOT THREAD SAFE: Use only when a dictionary is assured to be single threaded. public static void Register(this ICollection target, T value) { if (target is null) throw new ArgumentNullException(nameof(target)); @@ -38,7 +34,13 @@ public static void Register(this ICollection target, T value) target.Add(value); } - public static void AddRange(this ICollection target, IEnumerable values) + /// + /// Adds each value to the end of the collection. + /// + /// If the is null. + public static void AddRange( + this ICollection target, + IEnumerable values) { if (target is null) throw new ArgumentNullException(nameof(target)); Contract.EndContractBlock(); @@ -50,14 +52,39 @@ public static void AddRange(this ICollection target, IEnumerable values target.Add(value); } + /// +#if NET9_0_OR_GREATER + [OverloadResolutionPriority(1)] +#endif + public static void AddRange( + this ICollection target, + ReadOnlySpan values) + { + if (target is null) throw new ArgumentNullException(nameof(target)); + Contract.EndContractBlock(); + + foreach (T value in values) + target.Add(value); + } + + /// + /// Adds each value to the end of the collection. + /// +#if NET9_0_OR_GREATER + public static void AddThese(this ICollection target, T a, T b, params ReadOnlySpan more) +#else public static void AddThese(this ICollection target, T a, T b, params T[] more) +#endif { target.Add(a); target.Add(b); - if (more.Length != 0) - target.AddRange(more); + foreach (T value in more) + target.Add(value); } + /// + /// Removes each value from the collection. + /// public static int Remove(this ICollection target, IEnumerable values) { if (target is null) throw new ArgumentNullException(nameof(target)); @@ -68,28 +95,39 @@ public static int Remove(this ICollection target, IEnumerable values) { foreach (T? value in values) { - if ( - target.Remove(value)) + if (target.Remove(value)) count++; } } + return count; } /// - /// Shortcut for adding a value or updating based on exising value. + /// Shortcut for adding a value or updating based on existing value. /// If no value exists, it adds the provided value. /// If a value exists, it sets the value using the updateValueFactory. - /// NOT THREAD SAFE: Use only when a dictionary local or is assured single threaded. /// + /// NOT THREAD SAFE: Use only when a dictionary is assured to be single threaded. public static T AddOrUpdate(this IDictionary target, TKey key, T value, T updateValue) + where TKey : notnull { if (target is null) throw new ArgumentNullException(nameof(target)); if (key is null) throw new ArgumentNullException(nameof(key)); Contract.EndContractBlock(); +#if NET9_0_OR_GREATER + if (target is Dictionary d) + { + ref var val = ref System.Runtime.InteropServices.CollectionsMarshal.GetValueRefOrNullRef(d, key); + return System.Runtime.CompilerServices.Unsafe.IsNullRef(ref val) + ? (val = value) + : (val = updateValue); + } +#endif + T valueUsed; if (target.TryGetValue(key, out _)) target[key] = valueUsed = updateValue; @@ -100,19 +138,30 @@ public static T AddOrUpdate(this IDictionary target, TKey key, } /// - /// Shortcut for adding a value or updating based on exising value. + /// Shortcut for adding a value or updating based on existing value. /// If no value exists, it adds the provided value. /// If a value exists, it sets the value using the updateValueFactory. - /// NOT THREAD SAFE: Use only when a dictionary local or is assured single threaded. /// + /// NOT THREAD SAFE: Use only when a dictionary is assured to be single threaded. public static T AddOrUpdate(this IDictionary target, TKey key, T value, Func updateValueFactory) + where TKey : notnull { if (target is null) throw new ArgumentNullException(nameof(target)); if (key is null) throw new ArgumentNullException(nameof(key)); if (updateValueFactory is null) throw new ArgumentNullException(nameof(updateValueFactory)); Contract.EndContractBlock(); +#if NET9_0_OR_GREATER + if (target is Dictionary d) + { + ref var val = ref System.Runtime.InteropServices.CollectionsMarshal.GetValueRefOrNullRef(d, key); + return System.Runtime.CompilerServices.Unsafe.IsNullRef(ref val) + ? (val = value) + : (val = updateValueFactory(key, val)); + } +#endif + T valueUsed; if (target.TryGetValue(key, out T? old)) target[key] = valueUsed = updateValueFactory(key, old); @@ -123,14 +172,15 @@ public static T AddOrUpdate(this IDictionary target, TKey key, } /// - /// Shortcut for adding a value or updating based on exising value. + /// Shortcut for adding a value or updating based on existing value. /// If no value exists, it adds the value using the newValueFactory. /// If a value exists, it sets the value using the updateValueFactory. - /// NOT THREAD SAFE: Use only when a dictionary local or is assured single threaded. /// + /// NOT THREAD SAFE: Use only when a dictionary is assured to be single threaded. public static T AddOrUpdate(this IDictionary target, TKey key, Func newValueFactory, Func updateValueFactory) + where TKey : notnull { if (target is null) throw new ArgumentNullException(nameof(target)); if (key is null) throw new ArgumentNullException(nameof(key)); @@ -138,6 +188,16 @@ public static T AddOrUpdate(this IDictionary target, TKey key, if (updateValueFactory is null) throw new ArgumentNullException(nameof(updateValueFactory)); Contract.EndContractBlock(); +#if NET9_0_OR_GREATER + if (target is Dictionary d) + { + ref var val = ref System.Runtime.InteropServices.CollectionsMarshal.GetValueRefOrNullRef(d, key); + return System.Runtime.CompilerServices.Unsafe.IsNullRef(ref val) + ? (val = newValueFactory(key)) + : (val = updateValueFactory(key, val)); + } +#endif + T valueUsed; if (target.TryGetValue(key, out T? old)) target[key] = valueUsed = updateValueFactory(key, old); @@ -148,46 +208,87 @@ public static T AddOrUpdate(this IDictionary target, TKey key, } /// - /// Thread safe shortcut for adding a value to list within a dictionary. + /// Shortcut for adding a value to list within a dictionary. /// public static void AddTo(this IDictionary> c, TKey key, TValue value) + where TKey : notnull { if (c is null) throw new ArgumentNullException(nameof(c)); if (key is null) throw new ArgumentNullException(nameof(key)); Contract.EndContractBlock(); - IList? list = c.GetOrAdd(key, _ => new List()); + IList? list = c.GetOrAdd(key, _ => []); list.Add(value); } /// - /// Shortcut for ensuring a cacheKey contains a action. If no action exists, it adds the provided defaultValue. - /// NOT THREAD SAFE: Use only when a dictionary local or is assured single threaded. + /// Shortcut for ensuring a cacheKey contains a action. If no value exists, it adds the provided defaultValue. /// + /// NOT THREAD SAFE: Use only when a dictionary is assured to be single threaded. + [Obsolete("Use TryAdd instead.")] public static void EnsureDefault(this IDictionary target, TKey key, T defaultValue) + where TKey : notnull + => TryAdd(target, key, defaultValue); + + /// + /// Shortcut for ensuring a cacheKey contains a Value. If no value exists, it adds it using the provided defaultValueFactory. + /// + /// NOT THREAD SAFE: Use only when a dictionary is assured to be single threaded. + [Obsolete("Use TryAdd instead.")] + public static void EnsureDefault(this IDictionary target, TKey key, + Func defaultValueFactory) + where TKey : notnull + => TryAdd(target, key, defaultValueFactory); + + /// + /// Attempts to add a value to a dictionary if it does not already exist. + /// + /// NOT THREAD SAFE: Use only when a dictionary is assured to be single threaded. + public static bool TryAdd(this IDictionary target, TKey key, T value) + where TKey : notnull { if (target is null) throw new ArgumentNullException(nameof(target)); if (key is null) throw new ArgumentNullException(nameof(key)); Contract.EndContractBlock(); - if (!target.ContainsKey(key)) - target.Add(key, defaultValue); +#if NET9_0_OR_GREATER + if (target is Dictionary d) + { + ref var val = ref System.Runtime.InteropServices.CollectionsMarshal.GetValueRefOrAddDefault(d, key, out bool exists); + if (!exists) val = value; + return !exists; + } +#endif + if (target.ContainsKey(key)) + return false; + + target.Add(key, value); + return true; } - /// - /// Shortcut for ensuring a cacheKey contains a Value. If no action exists, it adds it using the provided defaultValueFactory. - /// NOT THREAD SAFE: Use only when a dictionary local or is assured single threaded. - /// - public static void EnsureDefault(this IDictionary target, TKey key, + /// + public static bool TryAdd(this IDictionary target, TKey key, Func defaultValueFactory) + where TKey : notnull { if (target is null) throw new ArgumentNullException(nameof(target)); if (key is null) throw new ArgumentNullException(nameof(key)); if (defaultValueFactory is null) throw new ArgumentNullException(nameof(defaultValueFactory)); Contract.EndContractBlock(); - if (!target.ContainsKey(key)) - target.Add(key, defaultValueFactory(key)); +#if NET9_0_OR_GREATER + if (target is Dictionary d) + { + ref var val = ref System.Runtime.InteropServices.CollectionsMarshal.GetValueRefOrAddDefault(d, key, out bool exists); + if (!exists) val = defaultValueFactory(key); + return !exists; + } +#endif + if (target.ContainsKey(key)) + return false; + + target.Add(key, defaultValueFactory(key)); + return true; } /// @@ -196,6 +297,7 @@ public static void EnsureDefault(this IDictionary target, TKey public static T GetOrDefault( this IDictionary target, TKey key) + where TKey : notnull { if (target is null) throw new ArgumentNullException(nameof(target)); if (key is null) throw new ArgumentNullException(nameof(key)); @@ -211,6 +313,7 @@ public static T GetOrDefault( this IDictionary target, TKey key, T defaultValue) + where TKey : notnull { if (target is null) throw new ArgumentNullException(nameof(target)); if (key is null) throw new ArgumentNullException(nameof(key)); @@ -226,6 +329,7 @@ public static T GetOrDefault( this IDictionary target, TKey key, Func valueFactory) + where TKey : notnull { if (target is null) throw new ArgumentNullException(nameof(target)); if (key is null) throw new ArgumentNullException(nameof(key)); @@ -237,38 +341,103 @@ public static T GetOrDefault( /// /// Tries to acquire a value from the dictionary. If no value is present it adds it using the valueFactory response. - /// NOT THREAD SAFE: Use only when a dictionary local or is assured single threaded. /// + /// NOT THREAD SAFE: Use only when a dictionary is assured to be single threaded. public static T GetOrAdd( this IDictionary target, TKey key, Func valueFactory) + where TKey : notnull { if (target is null) throw new ArgumentNullException(nameof(target)); if (key is null) throw new ArgumentNullException(nameof(key)); if (valueFactory is null) throw new ArgumentNullException(nameof(valueFactory)); Contract.EndContractBlock(); - if (!target.TryGetValue(key, out T? value)) +#if NET9_0_OR_GREATER + if (target is Dictionary d) + { + ref var val = ref System.Runtime.InteropServices.CollectionsMarshal.GetValueRefOrAddDefault(d, key, out bool exists); + if (!exists) val = valueFactory(key); + return val!; + } +#endif + if (!target.TryGetValue(key, out var value)) target.Add(key, value = valueFactory(key)); + return value; } /// /// Tries to acquire a value from the dictionary. If no value is present it adds the value provided. - /// NOT THREAD SAFE: Use only when a dictionary local or is assured single threaded. /// + /// NOT THREAD SAFE: Use only when a dictionary is assured to be single threaded. public static T GetOrAdd( this IDictionary target, - TKey key, - T value) + TKey key, T value) + where TKey : notnull { if (target is null) throw new ArgumentNullException(nameof(target)); if (key is null) throw new ArgumentNullException(nameof(key)); Contract.EndContractBlock(); +#if NET9_0_OR_GREATER + if (target is Dictionary d) + { + ref var val = ref System.Runtime.InteropServices.CollectionsMarshal.GetValueRefOrAddDefault(d, key, out bool exists); + if (!exists) val = value; + return val!; + } +#endif + if (!target.TryGetValue(key, out T? v)) target.Add(key, v = value); return v; } + + /// + /// Tries to update an existing value in the dictionary. + /// + /// NOT THREAD SAFE: Use only when a dictionary is assured to be single threaded. + /// + /// if the value was overwritten; + /// if there was no original value + /// or if is and the values are equal. + /// + public static bool TryUpdate( + this IDictionary target, + TKey key, T value, + bool compareExisting = false) + where TKey : notnull + { + if (target is null) throw new ArgumentNullException(nameof(target)); + if (key is null) throw new ArgumentNullException(nameof(key)); + Contract.EndContractBlock(); + +#if NET9_0_OR_GREATER + if (target is Dictionary d) + { + ref var val = ref System.Runtime.InteropServices.CollectionsMarshal.GetValueRefOrNullRef(d, key); + if (System.Runtime.CompilerServices.Unsafe.IsNullRef(ref val)) + return false; + + if (compareExisting && !AreEqual(val, value)) + return false; + + val = value; + return true; + } +#endif + + if (!target.TryGetValue(key, out var v)) + return false; + + if (compareExisting && !AreEqual(v, value)) + return false; + + target[key] = value; + return true; + + static bool AreEqual(T a, T b) => a is null ? b is null : a.Equals(b); + } } diff --git a/source/Extensions.Permutations.cs b/source/Extensions.Permutations.cs index 843bdc6..583aea4 100644 --- a/source/Extensions.Permutations.cs +++ b/source/Extensions.Permutations.cs @@ -1,116 +1,248 @@ -using Open.Disposable; -using System; -using System.Buffers; -using System.Collections.Generic; -using System.Diagnostics.Contracts; -using System.Linq; -using System.Runtime.CompilerServices; +using System.Buffers; +using System.Numerics; namespace Open.Collections; public static partial class Extensions { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - static IEnumerable PermutationsCore(Action> appendElements, T[] buffer, int count) + /// + /// Uses Heap's algorithm to iterate through all possible permutations. + /// + public static IEnumerable> Permutations(this Memory set) { - if (count == 0) yield break; - if (count > buffer.Length) - throw new ArgumentOutOfRangeException(nameof(buffer), buffer, "Length is less than the number of elements."); + int n = set.Length; + if (n == 0) yield break; + yield return set; + if (n == 1) yield break; - // based on: https://stackoverflow.com/questions/1145703/permutation-of-string-without-recursion + var lease = MemoryPool.Shared.Rent(n); + var c = lease.Memory.Slice(0, n); + c.Span.Clear(); // Initialize counter array - int max = 1; - for (int i = 2; i <= count; i++) max *= i; - - int[]? a = new int[count]; - var pos = new LinkedList(); - - for (int j = 0; j < max; ++j) + int i = 0; + while (i < n) { - appendElements(pos); + int cValue = c.Span[i]; + if (cValue < i) + { + var span = set.Span; + if (i % 2 == 0) + Swap(ref span[0], ref span[i]); + else + Swap(ref span[cValue], ref span[i]); - int i; - int n = j; - int c = 0; + yield return set; - for (i = count; i > 0; --i) - { - int m = n; n /= i; - a[c++] = m % i; + c.Span[i] = cValue + 1; + i = 0; } - - // Avoid copy if not needed. - for (i = 0; i < count; i++) + else { - int index = a[i]; - var node = pos.First; - for (int ni = 0; ni < index; ni++) - node = node.Next; - buffer[i] = node.Value; - pos.Remove(node); + c.Span[i] = 0; + i++; } + } + } + + /// + public static IEnumerable> Permutations(this T[] set) + => set.AsMemory().Permutations(); + + /// + /// A fast, memory efficient way to get a unique permutation based upon an index of all possible permutations. + /// Modifies the source span to produce the results. Be sure to copy the source if you want to keep it. + /// + /// The set to permute. + /// The index of the permutation. + /// Is not in lexicographic order. + /// The permuted as a . + public static ReadOnlySpan Permutation(this Span source, BigInteger n) + { + if (n <= ulong.MaxValue) + { + return n < 0 + ? throw new ArgumentOutOfRangeException(nameof(n), n, "Must be at least zero.") + : Permutation(source, (ulong)n); + } + + int size = source.Length; + int i = 0, j; + for (; i < size; ++i) + { + if (n <= long.MaxValue) + return Permutation(source, (long)n, i); - yield return buffer; + j = i + 1; + int index = (int)(n % j); + if (index != i) + Swap(ref source[index], ref source[i]); + n /= j; } + + return source; } - static IEnumerable PermutationsCore(IReadOnlyCollection elements, T[] buffer) + /// + public static ReadOnlySpan Permutation(this Span source, ulong n) { - int count = elements.Count; - if (count == 0) return Enumerable.Empty(); - if (count > buffer.Length) - throw new ArgumentOutOfRangeException(nameof(buffer), buffer, "Length is less than the number of elements."); - Contract.EndContractBlock(); + int size = source.Length; + int i = 0; + uint j = 0; + for (; i < size; ++i) + { + if (n <= long.MaxValue) + return Permutation(source, (long)n, i); - return PermutationsCore(pos => pos.AddRange(elements), buffer, count); + int index = (int)(n % ++j); + if (index != i) + Swap(ref source[index], ref source[i]); + n /= j; + } + + return source; } - static IEnumerable PermutationsCore(ReadOnlyMemory elements, T[] buffer) + /// + public static ReadOnlySpan Permutation(this Span source, long n, int i = 0) { - int count = elements.Length; - if (count == 0) return Enumerable.Empty(); - if (count > buffer.Length) - throw new ArgumentOutOfRangeException(nameof(buffer), buffer, "Length is less than the number of elements."); - Contract.EndContractBlock(); + int size = source.Length; + for (int j; i < size; ++i) + { + if (n < int.MaxValue) + return Permutation(source, (int)n, i); + + j = i + 1; + int index = (int)(n % j); + if (index != i) + Swap(ref source[index], ref source[i]); + n /= j; + } - return PermutationsCore(pos => + return source; + } + + /// + public static ReadOnlySpan Permutation(this Span source, int n, int i = 0) + { + if (n < 0) throw new ArgumentOutOfRangeException(nameof(n), n, "Must be at least zero."); + + if (n == 0) return source; + n--; + + int size = source.Length; + for (int j; i < size; ++i) { - foreach (var e in elements.Span) - pos.AddLast(e); - }, buffer, count); + j = i + 1; + int index = n % j; + if (index != i) + Swap(ref source[index], ref source[i]); + n /= j; + } + + return source; + } + + /// + /// Permutes the to the next permutation. + /// + /// The sequence to permute. + /// + /// To reset the sequence after reaching the end (returns false) simply reverse the . + /// if there are more permutations in the sequence; otherwise . + /// + public static bool NextLexicographic(this Span span) + where T : IComparable + { + // 1. Find the largest index i such that array[i] < array[i + 1] + int i = span.Length - 2; + while (i >= 0 && span[i].CompareTo(span[i + 1]) >= 0) i--; + + // If no such index exists, the permutation is the last permutation + if (i < 0) + return false; + + // 2. Find the largest index j greater than i such that array[i] < array[j] + int j = span.Length - 1; + while (span[i].CompareTo(span[j]) >= 0) j--; + + // 3. Swap the values at array[i] and array[j] + Swap(ref span[i], ref span[j]); + + // 4. Reverse the sequence from array[i + 1] up to the last element + span.Slice(i + 1).Reverse(); + + return true; } + /// + public static bool NextLexicographic(this Span span) + { + // 1. Find the largest index i such that span[i] < span[i + 1] + int i = span.Length - 2; + while (i >= 0 && span[i] >= span[i + 1]) i--; + + // If no such index exists, the permutation is the last permutation + if (i < 0) + return false; + + // 2. Find the largest index j greater than i such that span[i] < span[j] + int j = span.Length - 1; + while (span[i] >= span[j]) j--; + + // 3. Swap the values at span[i] and span[j] + Swap(ref span[i], ref span[j]); + + // 4. Reverse the sequence from span[i + 1] up to the last element + span.Slice(i + 1).Reverse(); + + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Swap(ref T a, ref T b) + => (b, a) = (a, b); + /// The elements to draw from. - /// The buffer array that is filled with the values and returned as the yielded value instead of a new array + /// The buffer that is filled with the values and returned as the yielded value instead of a copy. /// - public static IEnumerable Permutations(this IReadOnlyCollection elements, T[] buffer) + public static IEnumerable> Permutations(this IReadOnlyCollection elements, Memory buffer) { if (elements is null) throw new ArgumentNullException(nameof(elements)); - if (buffer is null) throw new ArgumentNullException(nameof(buffer)); + + int count = elements.Count; + if (count == 0) return Enumerable.Empty>(); Contract.EndContractBlock(); - return PermutationsCore(elements, buffer); + var b = buffer.Slice(0, elements.Count); + elements.CopyToSpan(b.Span); + + return b.Permutations(); } /// The elements to draw from. - /// The buffer array that is filled with the values and returned as the yielded value instead of a new array + /// The buffer that is filled with the values and returned as the yielded value instead of a copy. /// - public static IEnumerable Permutations(this ReadOnlyMemory elements, T[] buffer) + public static IEnumerable> Permutations(this ReadOnlyMemory elements, Memory buffer) { - if (buffer is null) throw new ArgumentNullException(nameof(buffer)); + int count = elements.Length; + if (count == 0) return Enumerable.Empty>(); Contract.EndContractBlock(); - return PermutationsCore(elements, buffer); + var b = buffer.Slice(0, elements.Length); + elements.CopyTo(b); + + return b.Permutations(); } - /// - public static IEnumerable Permutations(this IEnumerable elements, T[] buffer) + /// + public static IEnumerable> Permutations(this IEnumerable elements, Memory buffer) { if (elements is null) throw new ArgumentNullException(nameof(elements)); - if (buffer is null) throw new ArgumentNullException(nameof(buffer)); Contract.EndContractBlock(); - return PermutationsCore(elements is IReadOnlyCollection c ? c : elements.ToArray(), buffer); + return elements is IReadOnlyCollection c + ? Permutations(c, buffer) + : Permutations(elements.ToArray().AsMemory(), buffer); } /// @@ -127,13 +259,15 @@ static IEnumerable> PermutationsBufferedCore(IReadOnlyCollecti int count = elements.Count; if (count == 0) yield break; + // We're using an ArrayPool here instead of a MemoryPool because benchmarking shows it can be slightly faster for smaller sizes. ArrayPool? pool = count > 128 ? ArrayPool.Shared : null; T[]? buffer = pool?.Rent(count) ?? new T[count]; - var readBuffer = new ReadOnlyMemory(buffer, 0, count); try { - foreach (T[]? _ in PermutationsCore(elements, buffer)) - yield return readBuffer; + var m = buffer.AsMemory().Slice(0, count); + elements.CopyToSpan(m.Span); + foreach (var b in m.Permutations()) + yield return b; } finally { @@ -148,13 +282,15 @@ public static IEnumerable> PermutationsBuffered(this ReadOn int count = elements.Length; if (count == 0) yield break; + // We're using an ArrayPool here instead of a MemoryPool because benchmarking shows it can be slightly faster for smaller sizes. ArrayPool? pool = count > 128 ? ArrayPool.Shared : null; T[]? buffer = pool?.Rent(count) ?? new T[count]; - var readBuffer = new ReadOnlyMemory(buffer, 0, count); try { - foreach (T[]? _ in PermutationsCore(elements, buffer)) - yield return readBuffer; + var m = buffer.AsMemory().Slice(0, count); + elements.CopyTo(m); + foreach (var b in Permutations(elements, buffer)) + yield return b; } finally { @@ -187,9 +323,9 @@ public static IEnumerable Permutations(this ReadOnlyMemory elements) } /// - /// Enumerates all possible unique permutations of a given set. + /// Using Heap's algorithm, enumerates all possible unique permutations of a given set. /// - /// [A, B, C] results in [A, B, C], [A, C, B], [B, A, C], [C, A, B], [B, C, A], [C, B, A] + /// [A, B, C] results in [A, B, C], [B, A, C], [C, A, B], [A, C, B], [B, C, A], [C, B, A] /// The elements to derive from. public static IEnumerable Permutations(this IEnumerable elements) { diff --git a/source/Extensions.Stream.cs b/source/Extensions.Stream.cs index 57a75f8..e25479b 100644 --- a/source/Extensions.Stream.cs +++ b/source/Extensions.Stream.cs @@ -1,8 +1,4 @@ -using System; -using System.Buffers; -using System.IO; -using System.Threading; -using System.Threading.Tasks; +using System.Buffers; namespace Open.Collections; @@ -11,10 +7,6 @@ public static partial class Extensions /// /// Copies the source stream to the target. /// - [System.Diagnostics.CodeAnalysis.SuppressMessage( - "Reliability", - "CA2016:Forward the 'CancellationToken' parameter to methods", - Justification = "Is required to ensure recieved data is not lost.")] public static async ValueTask DualBufferCopyToAsync( this Stream source, Stream target, @@ -38,11 +30,11 @@ public static async ValueTask DualBufferCopyToAsync( if (n == 0) break; // Preemptive request before yielding. - Task? current = cancellationToken.IsCancellationRequested ? null : source.ReadAsync(cCurrent, 0, bufferSize); -#if NETSTANDARD2_1_OR_GREATER - await target.WriteAsync(cNext.AsMemory(0, n)).ConfigureAwait(false); + Task current = source.ReadAsync(cCurrent, 0, bufferSize, cancellationToken); +#if NETSTANDARD2_0 + await target.WriteAsync(cNext, 0, n, cancellationToken).ConfigureAwait(false); #else - await target.WriteAsync(cNext, 0, n).ConfigureAwait(false); + await target.WriteAsync(cNext.AsMemory(0, n), cancellationToken).ConfigureAwait(false); #endif if (current is null) throw new OperationCanceledException(); (cCurrent, cNext) = (cNext, cCurrent); diff --git a/source/Extensions.Subsets.cs b/source/Extensions.Subsets.cs index 456fa39..5e73434 100644 --- a/source/Extensions.Subsets.cs +++ b/source/Extensions.Subsets.cs @@ -1,7 +1,4 @@ -using System; -using System.Buffers; -using System.Collections.Generic; -using System.Diagnostics.Contracts; +using System.Buffers; namespace Open.Collections; @@ -37,6 +34,7 @@ static IEnumerable> SubsetsCore(IReadOnlyList source, int count, Me buffer.Span[0] = e; yield return buffer; } + yield break; } @@ -84,7 +82,7 @@ static IEnumerable> SubsetsCore(IReadOnlyList source, int count, Me public static IEnumerable> SubsetsBuffered(this IReadOnlyList source, int count) { using var lease = MemoryPool.Shared.Rent(count); - Memory buffer = lease.Memory; + Memory buffer = lease.Memory.Slice(0, count); ReadOnlyMemory readBuffer = buffer; foreach (Memory _ in Subsets(source, count, buffer)) yield return readBuffer; @@ -99,7 +97,7 @@ public static IEnumerable> SubsetsBuffered(this IReadOnlyLi public static IEnumerable Subsets(this IReadOnlyList source, int count) { using var lease = MemoryPool.Shared.Rent(count); - Memory buffer = lease.Memory; + Memory buffer = lease.Memory.Slice(0, count); foreach (Memory _ in Subsets(source, count, buffer)) { var a = new T[count]; @@ -146,6 +144,7 @@ static IEnumerable> SubsetsCore(ReadOnlyMemory source, int count, M buffer.Span[0] = source.Span[i]; yield return buffer; } + yield break; } @@ -192,7 +191,7 @@ static IEnumerable> SubsetsCore(ReadOnlyMemory source, int count, M public static IEnumerable> SubsetsBuffered(this ReadOnlyMemory source, int count) { using var lease = MemoryPool.Shared.Rent(count); - Memory buffer = lease.Memory; + Memory buffer = lease.Memory.Slice(0, count); ReadOnlyMemory readBuffer = buffer; foreach (Memory _ in Subsets(source, count, buffer)) yield return readBuffer; @@ -202,7 +201,7 @@ public static IEnumerable> SubsetsBuffered(this ReadOnlyMem public static IEnumerable Subsets(this ReadOnlyMemory source, int count) { using var lease = MemoryPool.Shared.Rent(count); - Memory buffer = lease.Memory; + Memory buffer = lease.Memory.Slice(0, count); foreach (Memory _ in Subsets(source, count, buffer)) { var a = new T[count]; diff --git a/source/Extensions.SubsetsProgressive.cs b/source/Extensions.SubsetsProgressive.cs index ee66325..429aec4 100644 --- a/source/Extensions.SubsetsProgressive.cs +++ b/source/Extensions.SubsetsProgressive.cs @@ -1,7 +1,4 @@ -using System; -using System.Buffers; -using System.Collections.Generic; -using System.Diagnostics.Contracts; +using System.Buffers; namespace Open.Collections; @@ -35,6 +32,7 @@ static IEnumerable SubsetsProgressiveCore(IReadOnlyList source, int coun buffer[0] = e; yield return buffer; } + yield break; } @@ -66,6 +64,7 @@ static IEnumerable SubsetsProgressiveCore(IReadOnlyList source, int coun yield return buffer; } + ++n; } } diff --git a/source/Extensions.cs b/source/Extensions.cs index b45d425..dbe047f 100644 --- a/source/Extensions.cs +++ b/source/Extensions.cs @@ -1,19 +1,13 @@ using Microsoft.Extensions.Primitives; using Open.Text; -using System; using System.Buffers; using System.Collections; -using System.Collections.Generic; using System.Collections.Immutable; using System.Collections.ObjectModel; -using System.Diagnostics.Contracts; using System.Dynamic; -using System.Linq; using System.Linq.Expressions; using System.Text; -using System.Threading; using System.Threading.Channels; -using System.Threading.Tasks; namespace Open.Collections; @@ -31,8 +25,8 @@ public static ExpandoObject ToExpando(this IEnumerable)expando; + ExpandoObject expando = new(); + var expandoDic = (IDictionary)expando!; // go through the items in the dictionary and copy over the key value pairs) @@ -75,6 +69,10 @@ public static ExpandoObject ToExpando(this IEnumerable + /// Clones the source 2D array into a new 2D array. + /// + /// The is null. public static T[,] BiClone(this T[,] source) { if (source is null) throw new ArgumentNullException(nameof(source)); @@ -90,6 +88,10 @@ public static ExpandoObject ToExpando(this IEnumerable + /// Overwrites the target 2D array with the source 2D array. + /// + /// The or is null. public static void Overwrite(this T[,] source, T[,] target) { if (source is null) throw new ArgumentNullException(nameof(source)); @@ -99,6 +101,10 @@ public static void Overwrite(this T[,] source, T[,] target) source.ForEach((x, y, value) => target[x, y] = value); } + /// + /// Iterates through the 2D array and executes the closure for each element. + /// + /// The or is null. public static void ForEach(this T[,] source, Action closure) { if (source is null) throw new ArgumentNullException(nameof(source)); @@ -117,45 +123,37 @@ public static void ForEach(this T[,] source, Action closure) } } - public static T[] AsCopy(this T[] source, int? length = null) + /// + /// Creates a copy of the source array with a new length. + /// + /// If the is less than the length, the copy will contain all elements up to that length. If is greater then there will be untouched elements after that. + /// The is null. + public static T[] ToArrayOfLength(this T[] source, int length) { if (source is null) throw new ArgumentNullException(nameof(source)); Contract.EndContractBlock(); - var newArray = new T[length ?? source.Length]; + var newArray = new T[length]; int len = Math.Min(newArray.Length, source.Length); for (int i = 0; i < len; i++) newArray[i] = source[i]; return newArray; } - public static IEnumerable> CopyEachUsing( - this IEnumerable> source, - ArrayPool pool) - { - if (source is null) throw new ArgumentNullException(nameof(source)); - if (pool is null) throw new ArgumentNullException(nameof(pool)); - Contract.EndContractBlock(); - - return CopyUsingCore(source, pool); - - static IEnumerable> CopyUsingCore(IEnumerable> source, ArrayPool pool) - { - foreach (ReadOnlyMemory item in source) - { - int len = item.Length; - T[]? a = pool.Rent(len); - item.CopyTo(a); - yield return new ArraySegment(a, 0, len); - } - } - } + /// + /// If the source is already a collection, it will be returned as is. If not, it will be converted to a list. + /// + public static ICollection ToCollection(this IEnumerable? source) + => source is null ? [] : source as ICollection ?? source.ToList(); - public static ICollection AsCollection(this IEnumerable source) - => source is null - ? null! - : source as ICollection ?? source.ToArray(); + /// + public static IReadOnlyCollection ToReadOnlyCollection(this IEnumerable source) + => source is null ? [] : source as IReadOnlyCollection ?? Array.AsReadOnly(source.ToArray()); + /// + /// Iterates over the source in parallel. + /// + /// The or are null. public static void ForEach(this IEnumerable target, ParallelOptions? parallelOptions, Action closure) { if (target is null) throw new ArgumentNullException(nameof(target)); @@ -175,13 +173,21 @@ public static void ForEach(this IEnumerable target, ParallelOptions? paral closure); } - public static void ForEach(this IEnumerable target, Action closure, ushort parallel) + /// + /// Iterates over the source in parallel. + /// + /// The or are null. + public static void ForEach(this IEnumerable target, Action closure, ushort maxConcurrency) => target.ForEach( - parallel == 0 + maxConcurrency == 0 ? null - : new ParallelOptions { MaxDegreeOfParallelism = parallel }, + : new ParallelOptions { MaxDegreeOfParallelism = maxConcurrency }, closure); + /// + /// Iterates over the source and optionally can do so in parallel. + /// + /// The or are null. public static void ForEach(this IEnumerable target, Action closure, bool allowParallel = false) { if (target is null) throw new ArgumentNullException(nameof(target)); @@ -200,6 +206,10 @@ public static void ForEach(this IEnumerable target, Action closure, boo closure(t); } + /// + /// Iterates over the source in parallel with a lock. + /// + /// The or are null. public static void ForEach(this ISynchronizedCollection target, ParallelOptions? parallelOptions, Action closure) { if (target is null) throw new ArgumentNullException(nameof(target)); @@ -222,13 +232,21 @@ public static void ForEach(this ISynchronizedCollection target, ParallelOp }); } - public static void ForEach(this ISynchronizedCollection target, Action closure, ushort parallel) + /// + /// Iterates over the source in parallel with a lock. + /// + /// The or are null. + public static void ForEach(this ISynchronizedCollection target, Action closure, ushort maxConcurrency) => target.ForEach( - parallel == 0 + maxConcurrency == 0 ? null - : new ParallelOptions { MaxDegreeOfParallelism = parallel }, + : new ParallelOptions { MaxDegreeOfParallelism = maxConcurrency }, closure); + /// + /// Iterates over the source with a lock and optionally can do so in parallel. + /// + /// The or are null. public static void ForEach(this ISynchronizedCollection target, Action closure, bool allowParallel = false) { if (target is null) throw new ArgumentNullException(nameof(target)); @@ -250,7 +268,13 @@ public static void ForEach(this ISynchronizedCollection target, Action }); } - [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1068:CancellationToken parameters must come last", Justification = "Allows for simpler implementation. Other methods cover non-cancellable case.")] + /// + /// Iterates over the source and can be canceled. + /// + /// The or are null. +#pragma warning disable IDE0079 // Remove unnecessary suppression + [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1068:CancellationToken parameters must come last", Justification = "Allows for simpler implementation. Other methods cover non-cancelable case.")] +#pragma warning restore IDE0079 // Remove unnecessary suppression public static void ForEach(this IEnumerable target, CancellationToken token, Action closure) { if (target is null) throw new ArgumentNullException(nameof(target)); @@ -264,7 +288,10 @@ public static void ForEach(this IEnumerable target, CancellationToken toke } } - [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1068:CancellationToken parameters must come last", Justification = "Allows for simpler implementation. Other methods cover non-cancellable case.")] + /// + /// Iterates over the source with a lock and can be canceled. + /// + /// The or are null. public static void ForEach(this ISynchronizedCollection target, CancellationToken token, Action closure) { if (target is null) throw new ArgumentNullException(nameof(target)); @@ -281,19 +308,35 @@ public static void ForEach(this ISynchronizedCollection target, Cancellati }); } - public static IEnumerable Shuffle(this IEnumerable target) + /// + /// Randomizes the order of the source. + /// + public static IEnumerable Shuffle( + this IEnumerable target, Random? rnd = null) { if (target is null) throw new ArgumentNullException(nameof(target)); Contract.EndContractBlock(); - var r = new Random(); + var r = rnd ?? new Random(); return target.OrderBy(_ => r.Next()); } - // Ensures an optimized means of acquiring Any(); - public static bool HasAny(this IEnumerable source) => source.HasAtLeast(1); + /// + /// Tests the count of the source to see if there's any items. + /// + /// If the is null. + /// + /// First checks the type to see if a count can be acquired directly. If not, it will iterate through the source to count the items. + /// + public static bool HasAny(this IEnumerable source) + => source.HasAtLeast(1); + /// + /// Tests the count of the source to see if there's at least the number of items. + /// + /// If the is less than 1. + /// public static bool HasAtLeast(this IEnumerable source, int minimum) { if (source is null) @@ -306,8 +349,12 @@ public static bool HasAtLeast(this IEnumerable source, int minimum) { case T[] array: return array.Length >= minimum; + case IReadOnlyCollection collection: + return collection.Count >= minimum; case ICollection collection: return collection.Count >= minimum; + case ICollection collection: + return collection.Count >= minimum; } using IEnumerator? e = source.GetEnumerator(); @@ -316,9 +363,13 @@ public static bool HasAtLeast(this IEnumerable source, int minimum) if (--minimum == 0) return true; } + return false; } + /// + /// Synchronizes enumerating by locking on the enumerator. + /// public static bool ConcurrentTryMoveNext(this IEnumerator source, out T item) { // Always lock on next to prevent concurrency issues. @@ -330,10 +381,14 @@ public static bool ConcurrentTryMoveNext(this IEnumerator source, out T it return true; } } + item = default!; return false; } + /// + /// Synchronizes enumerating by locking on the enumerator and invokes the provided handlers depending on if .MoveNext() was true. + /// public static bool ConcurrentMoveNext(this IEnumerator source, Action trueHandler, Action? falseHandler = null) { // Always lock on next to prevent concurrency issues. @@ -345,11 +400,12 @@ public static bool ConcurrentMoveNext(this IEnumerator source, Action t return true; } } + falseHandler?.Invoke(); return false; } - static async Task PreCacheWorker(IEnumerator e, Channel queue) + static async Task PreCacheWorker(IEnumerator e, Channel queue, CancellationToken cancellationToken) { try { @@ -361,7 +417,7 @@ static async Task PreCacheWorker(IEnumerator e, Channel queue) retry: if (queue.Writer.TryWrite(value)) continue; - if (await queue.Writer.WaitToWriteAsync()) goto retry; + if (await queue.Writer.WaitToWriteAsync(cancellationToken).ConfigureAwait(false)) goto retry; break; } @@ -378,14 +434,14 @@ static async Task PreCacheWorker(IEnumerator e, Channel queue) /// /// Similar to a buffer but is loaded by another thread and attempts keep the buffer full while contents are being pulled. /// - public static IEnumerable PreCache(this IEnumerable source, int count = 1) + public static IEnumerable PreCache(this IEnumerable source, int count = 1, CancellationToken cancellationToken = default) { if (source is null) throw new ArgumentNullException(nameof(source)); Contract.EndContractBlock(); - return PreCacheCore(source, count); + return PreCacheCore(source, count, cancellationToken); - static IEnumerable PreCacheCore(IEnumerable source, int count) + static IEnumerable PreCacheCore(IEnumerable source, int count, CancellationToken cancellationToken) { if (count <= 0) { @@ -402,7 +458,7 @@ static IEnumerable PreCacheCore(IEnumerable source, int count) else yield break; // Queue up. - PreCacheWorker(e, queue).ConfigureAwait(false); + PreCacheWorker(e, queue, cancellationToken).ConfigureAwait(false); // Dequeue into the enumerable. do @@ -410,12 +466,12 @@ static IEnumerable PreCacheCore(IEnumerable source, int count) while (queue.Reader.TryRead(out T? item)) yield return item; } - while (queue.Reader.WaitToReadAsync().AsTask().Result); + while (queue.Reader.WaitToReadAsync(cancellationToken).AsTask().Result); - Task? complete = queue.Reader.Completion; + Task complete = queue.Reader.Completion; if (complete.IsFaulted) { - throw complete.Exception.InnerException; + throw complete.Exception.InnerException ?? complete.Exception; } } } @@ -445,70 +501,6 @@ public static string ToConcatenatedString(this IEnumerable source, Func - /// Shortcut to String.Join() using "," as a default value. - /// - public static string Join(this string[] array, char separator) - { - if (array is null) - throw new ArgumentNullException(nameof(array)); - Contract.EndContractBlock(); - - return string.Join(separator + string.Empty, array); - } - - public static string Join(this string[] array, string separator = ",") - { - if (array is null) - throw new ArgumentNullException(nameof(array)); - if (separator is null) - throw new ArgumentNullException(nameof(separator)); - Contract.EndContractBlock(); - - return string.Join(separator, array); - } - - /// - /// Concatenates a set of values into a single string using a character as a separator. - /// - public static string JoinToString(this IEnumerable source, char separator) - { - if (source is null) throw new ArgumentNullException(nameof(source)); - Contract.EndContractBlock(); - - var sb = new StringBuilder(); - using (IEnumerator? enumerator = source.GetEnumerator()) - { - if (enumerator.MoveNext()) - sb.Append(enumerator.Current); - while (enumerator.MoveNext()) - sb.Append(separator).Append(enumerator.Current); - } - - return sb.ToString(); - } - - /// - /// Concatenates set of values into a single string using another string as a separator. - /// - public static string JoinToString(this IEnumerable source, string separator) - { - if (source is null) throw new ArgumentNullException(nameof(source)); - if (separator is null) throw new ArgumentNullException(nameof(separator)); - Contract.EndContractBlock(); - - var sb = new StringBuilder(); - using (IEnumerator? enumerator = source.GetEnumerator()) - { - if (enumerator.MoveNext()) - sb.Append(enumerator.Current); - while (enumerator.MoveNext()) - sb.Append(separator).Append(enumerator.Current); - } - - return sb.ToString(); - } - /*public static T ValidateNotNull(this T target) { @@ -516,23 +508,28 @@ public static string JoinToString(this IEnumerable source, string separato return target; }*/ - public static Dictionary ToDictionary(this ParallelQuery> source) - { - if (source is null) throw new ArgumentNullException(nameof(source)); - Contract.EndContractBlock(); - - return source.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); - } - +#if NET9_0_OR_GREATER +#else + /// + /// Returns a dictionary from the source key-value pairs. + /// + /// If the is null. public static Dictionary ToDictionary(this IEnumerable> source) + where TKey : notnull { if (source is null) throw new ArgumentNullException(nameof(source)); Contract.EndContractBlock(); return source.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); } +#endif + /// + /// Converts an enumerable to a sorted dictionary. + /// + /// If the is null. public static SortedDictionary ToSortedDictionary(this IEnumerable> source) + where TKey : notnull { if (source is null) throw new ArgumentNullException(nameof(source)); Contract.EndContractBlock(); @@ -544,8 +541,13 @@ public static SortedDictionary ToSortedDictionary(th return result; } + /// + /// Converts an enumerable to a sorted dictionary. + /// + /// If the , , or are null. public static SortedDictionary ToSortedDictionary(this IEnumerable source, Func keySelector, Func valueSelector) + where TKey : notnull { if (source is null) throw new ArgumentNullException(nameof(source)); if (keySelector is null) throw new ArgumentNullException(nameof(keySelector)); @@ -559,7 +561,12 @@ public static SortedDictionary ToSortedDictionary + /// Converts a grouping to a sorted dictionary. + /// + /// If the is null. public static SortedDictionary> ToSortedDictionary(this IEnumerable> source) + where TKey : notnull { if (source is null) throw new ArgumentNullException(nameof(source)); Contract.EndContractBlock(); @@ -627,18 +634,8 @@ public static bool IsEquivalentTo(this IEnumerable source, IEnumerable if (source is null || target is null) return false; - if (source is IReadOnlyCollection sC && target is IReadOnlyCollection tC && sC.Count != tC.Count) - return false; - - using IEnumerator? enumSource = source.GetEnumerator(); - using IEnumerator? enumTarget = target.GetEnumerator(); - while (enumSource.MoveNext() && enumTarget.MoveNext()) - { - if (!enumSource.Current.Equals(enumTarget.Current)) - return false; - } - - return true; + // Both are not null, okay go. + return source.SequenceEqual(target); } /// @@ -649,7 +646,7 @@ public static string[] ToStringArray(this IEnumerable list) if (list is null) throw new ArgumentNullException(nameof(list)); Contract.EndContractBlock(); - return list.Select(r => r!.ToString()).ToArray(); + return list.Select(r => r!.ToString() ?? "null").ToArray(); } /// @@ -723,6 +720,7 @@ private static IQueryable ApplyOrderBy(IQueryable collection, OrderByIn expr = Expression.Property(expr, pi); type = pi.PropertyType; } + Type? delegateTypeSource = typeof(Func<,>); //var delegateTypeSourceArgs = delegateTypeSource.GetGenericArguments(); @@ -738,7 +736,7 @@ private static IQueryable ApplyOrderBy(IQueryable collection, OrderByIn ? "OrderBy" : "OrderByDescending"; - //TODO: apply caching to the generic methodsinfos? + //TODO: apply caching to the generic method-infos? System.Reflection.MethodInfo[]? methods = typeof(Queryable).GetMethods(); System.Reflection.MethodInfo? r1 = methods .Single(method => method.Name == methodName @@ -748,8 +746,9 @@ private static IQueryable ApplyOrderBy(IQueryable collection, OrderByIn object? result = r1 .MakeGenericMethod(typeof(T), type) - .Invoke(null, new object[] { collection, lambda }); + .Invoke(null, [collection, lambda]); + Debug.Assert(result is not null); return (IOrderedQueryable)result; } @@ -788,6 +787,7 @@ private static IEnumerable ParseOrderBy(StringSegment orderBy) $"Invalid OrderBy string '{item}'. Order By Format: Property, Property2 ASC, Property2 DESC"); } } + i++; } @@ -805,6 +805,9 @@ private class OrderByInfo } #endregion + /// + /// A nullable struct version of FirstOrDefault. + /// public static T? NullableFirstOrDefault(this IEnumerable source) where T : struct { @@ -816,6 +819,9 @@ private class OrderByInfo return null; } + /// + /// Rotates through each enumerable and returns the next value until none are left. + /// public static IEnumerable Weave(this IEnumerable> source) { LinkedList>? queue = null; @@ -835,7 +841,7 @@ public static IEnumerable Weave(this IEnumerable> source) if (queue is null) yield break; - // Start by getting the first enuerator if it exists. + // Start by getting the first enumerator if it exists. LinkedListNode>? n = queue.First; while (n is not null) { @@ -898,9 +904,37 @@ public static int IndexOf(this T[] source, T value) if (source[i]?.Equals(value) ?? value is null) return i; } + return -1; } + /// + /// Optimized method for getting a node by index + /// + public static LinkedListNode GetNodeAt(this LinkedList list, int index) + { + LinkedListNode current; + + // Determine whether to start from the beginning or the end. + int count = list.Count; + if (index < count / 2) + { + // Start from the beginning + current = list.First!; + for (int i = 0; i < index; i++) + current = current.Next!; + + return current; + } + + // Start from the end + current = list.Last!; + for (int i = count - 1; i > index; i--) + current = current.Previous!; + + return current; + } + /// /// Copies the results to the provided span up to its length or until the end of the results. /// @@ -913,37 +947,30 @@ public static int IndexOf(this T[] source, T value) /// public static Span CopyToSpan(this IEnumerable source, Span target) { - int len = target.Length; - if (len == 0) return target; + int tLen = target.Length; + if (tLen == 0) return target; + + if (source is T[] a) + { + int sLen = a.Length; + if (tLen < sLen) + { + a.AsSpan(0, tLen).CopyTo(target); + return target; + } + + a.CopyTo(target); + return tLen == sLen ? target : target.Slice(0, sLen); + } int count = 0; foreach (T? e in source) { target[count] = e; - if (len == ++count) return target; + if (tLen == ++count) return target; } - return target.Slice(0, count); - } - - /// - /// Builds an immutable array using the contents of the span. - /// - public static ImmutableArray ToImmutableArray(this ReadOnlySpan span) - { - ImmutableArray.Builder? builder = ImmutableArray.CreateBuilder(span.Length); - foreach (T? e in span) - builder.Add(e); - return builder.MoveToImmutable(); - } - - /// - public static ImmutableArray ToImmutableArray(this Span span) - { - ImmutableArray.Builder? builder = ImmutableArray.CreateBuilder(span.Length); - foreach (T? e in span) - builder.Add(e); - return builder.MoveToImmutable(); + return tLen == count ? target : target.Slice(0, count); } /// @@ -972,7 +999,7 @@ public static ReadOnlyCollection ToReadOnlyCollection(this Span span) public static ReadOnlyCollection ToReadOnlyCollection(this ReadOnlyMemory memory) => memory.Span.ToReadOnlyCollection(); - /// + /// public static ReadOnlyCollection ToReadOnlyCollection(this Memory memory) => memory.Span.ToReadOnlyCollection(); @@ -1000,22 +1027,23 @@ public static IEnumerator Preflight( yield return source.Current; } + /// + /// Executes an action when the begins enumeration. + /// public static IEnumerable BeforeGetEnumerator( this IEnumerable source, Action before) => new PreflightEnumerable(source, before); - private class PreflightEnumerable : IEnumerable + private class PreflightEnumerable( + IEnumerable source, Action before) + : IEnumerable { - private readonly IEnumerable _source; - private readonly Action _before; + private readonly IEnumerable _source = source ?? throw new ArgumentNullException(nameof(source)); - public PreflightEnumerable(IEnumerable source, Action before) - { - _source = source ?? throw new ArgumentNullException(nameof(source)); - _before = before ?? throw new ArgumentNullException(nameof(before)); - } + private readonly Action _before = before ?? throw new ArgumentNullException(nameof(before)); + /// public IEnumerator GetEnumerator() { _before(); @@ -1024,4 +1052,86 @@ public IEnumerator GetEnumerator() IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } + + private const string MustBeAtLeast0 = "Must be at least 0."; + private const string MustBeLessThanTheSize = "Must be less than the size."; + + /// + /// Creates a segment from the array. + /// + public static ArraySegment AsSegment(this T[] array) + => new(array); + + /// + /// Creates a segment from the array starting at the specified . + /// + public static ArraySegment AsSegment(this T[] array, int offset) + { + if (array is null) throw new ArgumentNullException(nameof(array)); + if (offset < 0) + throw new ArgumentOutOfRangeException(nameof(offset), offset, MustBeAtLeast0); + if (offset > array.Length) + throw new ArgumentOutOfRangeException(nameof(offset), offset, MustBeLessThanTheSize); + Contract.EndContractBlock(); + + return new(array, offset, array.Length - offset); + } + + /// + /// Creates a segment from the array starting at the specified and extending for the . + /// + public static ArraySegment AsSegment(this T[] array, int offset, int count) + { + if (array is null) throw new ArgumentNullException(nameof(array)); + if (offset < 0) + throw new ArgumentOutOfRangeException(nameof(offset), offset, MustBeAtLeast0); + Contract.EndContractBlock(); + + return new(array, offset, count); + } + +#if NETSTANDARD2_0 + /// + /// Enumerates the source segment. + /// + public static IEnumerator GetEnumerator(this ArraySegment source) + { + int start = source.Offset; + int end = start + source.Count; + for (int i = start; i < end; i++) + yield return source.Array[i]; + } + + /// + /// Forms a slice out of the segment starting at the specified . + /// + public static ArraySegment Slice(this ArraySegment array, int offset) + { + if (offset < 0) + throw new ArgumentOutOfRangeException(nameof(offset), offset, MustBeAtLeast0); + if (offset > array.Count) + throw new ArgumentOutOfRangeException(nameof(offset), offset, MustBeLessThanTheSize); + Contract.EndContractBlock(); + + return new ArraySegment(array.Array, array.Offset + offset, array.Count - offset); + } + + /// + /// Forms a slice out of the segment + /// starting at the specified + /// and extending for the. + /// + public static ArraySegment Slice(this ArraySegment source, int offset, int count) + { + if (offset < 0) + throw new ArgumentOutOfRangeException(nameof(offset), offset, MustBeAtLeast0); + if (count < 0) + throw new ArgumentOutOfRangeException(nameof(count), count, MustBeAtLeast0); + if (offset > source.Count) + throw new ArgumentOutOfRangeException(nameof(offset), offset, MustBeLessThanTheSize); + Contract.EndContractBlock(); + + return new ArraySegment(source.Array, source.Offset + offset, count); + } +#endif } diff --git a/source/IAddMultiple.cs b/source/IAddMultiple.cs index d962ef8..e25520c 100644 --- a/source/IAddMultiple.cs +++ b/source/IAddMultiple.cs @@ -1,19 +1,29 @@ -using System.Collections.Generic; +namespace Open.Collections; -namespace Open.Collections; +/// +/// Represents a collection that can add multiple items. +/// public interface IAddMultiple { + /// + /// Adds all the items in to this collection. + /// + /// The items to add. + void AddRange(IEnumerable items); + // Note: "AddThese" is the name because Add can have multiple signatures. /// Adds more than one item. /// First item to add. /// Additional item to add. /// Extended param items to add. - void AddThese(T item1, T item2, params T[] items); +#if NET9_0_OR_GREATER + void AddThese(T item1, T item2, params System.ReadOnlySpan items); - /// - /// Adds all the items in to this collection. - /// - /// The items to add. - void AddRange(IEnumerable items); + /// /> + [OverloadResolutionPriority(1)] + void AddRange(ReadOnlySpan items); +#else + void AddThese(T item1, T item2, params T[] items); +#endif } diff --git a/source/IIndexedDictionary.cs b/source/IIndexedDictionary.cs index 69c3e20..0509ab7 100644 --- a/source/IIndexedDictionary.cs +++ b/source/IIndexedDictionary.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; - -namespace Open.Collections; +namespace Open.Collections; /// /// Represents a generic items of key/value pairs that are ordered independently of the key and value. diff --git a/source/IndexedDictionary.cs b/source/IndexedDictionary.cs index bbeb2c9..4636e83 100644 --- a/source/IndexedDictionary.cs +++ b/source/IndexedDictionary.cs @@ -1,22 +1,19 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Runtime.CompilerServices; - -namespace Open.Collections; +namespace Open.Collections; /// /// A minimal implementation of that is inherently not thread safe. /// public class IndexedDictionary : DictionaryWrapper, IIndexedDictionary + where TKey : notnull { private const string OUTOFSYNC = "Collection is out of sync possibly due to unsynchronized access by multiple threads."; private readonly List> _entries; private readonly Dictionary _indexes; + /// + /// Constructs a new instance with the specified capacity. + /// [ExcludeFromCodeCoverage] public IndexedDictionary(int capacity) : base(capacity) @@ -25,13 +22,16 @@ public IndexedDictionary(int capacity) _indexes = new(capacity); } + /// + /// Constructs a new instance. + /// public IndexedDictionary() - : base() { - _entries = new(); - _indexes = new(); + _entries = []; + _indexes = []; } + /// protected override void OnDispose() { base.OnDispose(); @@ -39,6 +39,9 @@ protected override void OnDispose() _indexes.Clear(); } + /// + /// Sets the value for the key. + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void SetValueInternal(TKey key, TValue value) => SetValue(key, value); @@ -46,12 +49,14 @@ protected override void OnDispose() public override IEnumerator> GetEnumerator() => _entries.GetEnumerator().Preflight(ThrowIfDisposedDelegate); + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override ICollection GetKeys() => new ReadOnlyCollectionAdapter( ThrowIfDisposed(_entries.Select(e => e.Key)), () => _entries.Count); + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override ICollection GetValues() => new ReadOnlyCollectionAdapter( @@ -70,8 +75,13 @@ public override int Count } #if NETSTANDARD2_0 +#pragma warning disable IDE0079 // Remove unnecessary suppression [SuppressMessage("Roslynator", "RCS1242:Do not pass non-read-only struct by read-only reference.", Justification = "KeyValuePairs are not truly readonly until NET Standard 2.1.")] +#pragma warning restore IDE0079 // Remove unnecessary suppression #endif + // Even though in later versions of .NET KeyValuePair is readonly, + // We allow the `in` keyword to keep the API consistent and easy to implement. + // This doesn't change the performance of the code. private int AddToLists(in KeyValuePair kvp) { int i = _entries.Count; @@ -82,6 +92,7 @@ private int AddToLists(in KeyValuePair kvp) return i; } + /// [ExcludeFromCodeCoverage] [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void AddInternal(in KeyValuePair item) @@ -98,6 +109,7 @@ protected override void AddInternal(in KeyValuePair item) return AddToLists(in kvp); } + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void AddInternal(TKey key, TValue value) => Add(key, value); @@ -157,6 +169,7 @@ private void RemoveIndex(int index, TKey key) Debug.Assert(_entries.Count == InternalSource.Count); } + /// public override void Clear() { base.Clear(); diff --git a/source/ItemChangedEventArgs.cs b/source/ItemChangedEventArgs.cs index cf3981c..9a2e70d 100644 --- a/source/ItemChangedEventArgs.cs +++ b/source/ItemChangedEventArgs.cs @@ -1,46 +1,87 @@ -using System; - -// ReSharper disable NotAccessedField.Global -// ReSharper disable MemberCanBeProtected.Global - -namespace Open.Collections; +namespace Open.Collections; +/// +/// The possible changes to an item. +/// public enum ItemChange { + /// + /// Default value indicating no change. + /// None, + + /// + /// The item was added to the collection. + /// Added, + + /// + /// The item was removed from the collection. + /// Removed, + + /// + /// The item was inserted into the collection. + /// Inserted, + + /// + /// The item was replaced in the collection. + /// Modified } -public class ItemChangedEventArgs : EventArgs +/// +/// Event arguments for item changes. +/// +public class ItemChangedEventArgs( + ItemChange action, T value, int version) + : EventArgs { - public readonly ItemChange Change; - public readonly T Value; - public readonly int Version; - - public ItemChangedEventArgs(ItemChange action, T value, int version) - { - Change = action; - Value = value; - Version = version; - } + /// + /// The action that caused the event to be raised. + /// + public readonly ItemChange Change = action; + + /// + /// The value that was changed to. + /// + public readonly T Value = value; + + /// + /// The version of the collection at the time of the change. + /// + public readonly int Version = version; } -public class ItemChangedEventArgs : ItemChangedEventArgs +/// +/// Event arguments for item changes with an index. +/// +public class ItemChangedEventArgs( + ItemChange action, TIndex index, TValue value, int version) + : ItemChangedEventArgs(action, value, version) { - public readonly TIndex Index; - public ItemChangedEventArgs(ItemChange action, TIndex index, TValue value, int version) - : base(action, value, version) => Index = index; + /// + /// The index of the item that was changed. + /// + public readonly TIndex Index = index; } +/// +/// A static helper class for creating instances. +/// public static class ItemChangeEventArgs { + /// + /// Creates a new instance. + /// public static ItemChangedEventArgs CreateArgs( this ItemChange change, T value, int version) => new(change, value, version); + /// + /// Creates a new instance. + /// public static ItemChangedEventArgs CreateArgs( this ItemChange change, TIndex index, TValue value, int version) => new(change, index, value, version); diff --git a/source/KeyValuePair.cs b/source/KeyValuePair.cs index 4ba8f52..02a2092 100644 --- a/source/KeyValuePair.cs +++ b/source/KeyValuePair.cs @@ -1,11 +1,14 @@ -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; - namespace Open.Collections; +/// +/// A static helper class for creating instances. +/// public static class KeyValuePair { + /// + /// Creates a new . + /// + [Pure] [ExcludeFromCodeCoverage] [MethodImpl(MethodImplOptions.AggressiveInlining)] public static KeyValuePair Create(TKey key, TValue value) => new(key, value); diff --git a/source/LazyList.cs b/source/LazyList.cs index 0835796..b85ce38 100644 --- a/source/LazyList.cs +++ b/source/LazyList.cs @@ -4,10 +4,6 @@ */ using Open.Threading; -using System; -using System.Collections.Generic; -using System.Diagnostics.Contracts; -using System.Threading; namespace Open.Collections; @@ -15,23 +11,20 @@ namespace Open.Collections; /// A a thread-safe list for caching the results of an enumerable. /// Note: should be disposed manually whenever possible as the locking mechanism is a ReaderWriterLockSlim. /// -public sealed class LazyList : LazyListUnsafe +public sealed class LazyList( + IEnumerable source, bool isEndless = false) + : LazyListUnsafe(source) { - ReaderWriterLockSlim Sync; + ReaderWriterLockSlim Sync = new(LockRecursionPolicy.NoRecursion); int _safeCount; /// /// A value indicating whether the results are known or expected to be finite. /// A list that was constructed as endless but has reached the end of the results will return false. /// - public bool IsEndless { get; private set; } - - public LazyList(IEnumerable source, bool isEndless = false) : base(source) - { - Sync = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion); // This is important as it's possible to recurse infinitely to generate a result. :( - IsEndless = isEndless; // To indicate if a source is not allowed to fully enumerate. - } + public bool IsEndless { get; private set; } = isEndless; // To indicate if a source is not allowed to fully enumerate. + /// protected override void OnDispose() { using (Sync.WriteLock()) base.OnDispose(); @@ -51,15 +44,16 @@ public override int IndexOf(T item) return base.IndexOf(item); } + /// protected override bool EnsureIndex(int maxIndex) { if (maxIndex < _safeCount) return true; // This is where the fun begins... - // Mutliple threads can be out of sync (probably through a memory barrier) + // Multiple threads can be out of sync (probably through a memory barrier) // And a sync read operation must be done to ensure safety. - int count = Sync.Read(() => _cached.Count); + int count = Sync.Read(() => Cached.Count); if (maxIndex < count) { // We're still within the existing results, but safe count is not up to date. @@ -70,42 +64,43 @@ protected override bool EnsureIndex(int maxIndex) return true; } - if (_enumerator is null) + if (Enumerator is null) return false; // This very well could be a simple lock{} statement but the ReaderWriterLockSlim recursion protection is actually quite useful. using var uLock = Sync.UpgradableReadLock(); - // Note: Within an upgradable read, other reads pile up. + // Note: Within an upgradeable read, other reads pile up. - int c = _cached.Count; + int c = Cached.Count; if (_safeCount != c) // Always do comparisons outside of interlocking first. Interlocked.CompareExchange(ref _safeCount, c, _safeCount); if (maxIndex < _safeCount) return true; - if (_enumerator is null) + if (Enumerator is null) return false; using var wLock = Sync.WriteLock(); - while (_enumerator.MoveNext()) + while (Enumerator.MoveNext()) { - if (_cached.Count == int.MaxValue) - throw new Exception("Reached maximium contents for a single list. Cannot memoize further."); + if (Cached.Count == int.MaxValue) + throw new Exception("Reached maximum contents for a single list. Cannot memoize further."); - _cached.Add(_enumerator.Current); + Cached.Add(Enumerator.Current); - if (maxIndex < _cached.Count) + if (maxIndex < Cached.Count) return true; } IsEndless = false; - DisposeOf(ref _enumerator); + DisposeOf(ref Enumerator); return false; } + /// protected override void Finish() { if (IsEndless) diff --git a/source/LazyListUnsafe.cs b/source/LazyListUnsafe.cs index e8f1449..8d7b165 100644 --- a/source/LazyListUnsafe.cs +++ b/source/LazyListUnsafe.cs @@ -4,10 +4,6 @@ */ using Open.Disposable; -using System; -using System.Collections.Generic; -using System.Diagnostics.Contracts; -using System.Threading; namespace Open.Collections; @@ -16,21 +12,24 @@ namespace Open.Collections; /// Only use if you know the results are finite and access is thread safe. /// Note: disposing releases the underlying enumerator if it never reached the end of the results. /// -public class LazyListUnsafe : DisposableBase, IReadOnlyList +public class LazyListUnsafe(IEnumerable source) + : DisposableBase, IReadOnlyList { - protected List _cached; - protected IEnumerator _enumerator; + /// + /// The memoized results. + /// + protected List Cached = []; - public LazyListUnsafe(IEnumerable source) - { - _enumerator = source.GetEnumerator(); - _cached = new List(); - } + /// + /// The enumerator for the source. + /// + protected IEnumerator Enumerator = source.GetEnumerator(); + /// protected override void OnDispose() { - DisposeOf(ref _enumerator); - Nullify(ref _cached)?.Clear(); + DisposeOf(ref Enumerator); + Nullify(ref Cached)?.Clear(); } const string MUST_BE_AT_LEAST_ZERO = "Must be at least zero."; @@ -49,7 +48,7 @@ public T this[int index] throw new ArgumentOutOfRangeException(nameof(index), GREATER_THAN_TOTAL); Contract.EndContractBlock(); - return _cached[index]; + return Cached[index]; } } @@ -60,7 +59,7 @@ public int Count { AssertIsAlive(); Finish(); - return _cached.Count; + return Cached.Count; } } @@ -77,7 +76,7 @@ public bool TryGetValueAt(int index, out T value) if (EnsureIndex(index)) { - value = _cached[index]; + value = Cached[index]; return true; } @@ -103,7 +102,7 @@ public virtual int IndexOf(T item) for (int index = 0; EnsureIndex(index); index++) { - T? value = _cached[index]; + T? value = Cached[index]; if (value is null) { if (item is null) return index; @@ -137,30 +136,36 @@ public Span CopyTo(T[] array, int startIndex = 0) System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); + /// + /// Ensures that the index is available by enumerating to that index and memoizing results. + /// protected virtual bool EnsureIndex(int maxIndex) { - if (maxIndex < _cached.Count) + if (maxIndex < Cached.Count) return true; - if (_enumerator is null) + if (Enumerator is null) return false; - while (_enumerator.MoveNext()) + while (Enumerator.MoveNext()) { - if (_cached.Count == int.MaxValue) - throw new Exception("Reached maximium contents for a single list. Cannot memoize further."); + if (Cached.Count == int.MaxValue) + throw new Exception("Reached maximum contents for a single list. Cannot memoize further."); - _cached.Add(_enumerator.Current); + Cached.Add(Enumerator.Current); - if (maxIndex < _cached.Count) + if (maxIndex < Cached.Count) return true; } - DisposeOf(ref _enumerator); + DisposeOf(ref Enumerator); return false; } + /// + /// Ensures all results are memoized. + /// protected virtual void Finish() { while (EnsureIndex(int.MaxValue)) { } diff --git a/source/LinkedList/ILinkedList.cs b/source/LinkedList/ILinkedList.cs index 8753ada..0caf549 100644 --- a/source/LinkedList/ILinkedList.cs +++ b/source/LinkedList/ILinkedList.cs @@ -1,14 +1,15 @@ -using System.Collections.Generic; - -namespace Open.Collections; +namespace Open.Collections; +/// +/// An interface for a linked list. +/// public interface ILinkedList : ICollection { /// - LinkedListNode First { get; } + LinkedListNode? First { get; } /// - LinkedListNode Last { get; } + LinkedListNode? Last { get; } /// LinkedListNode AddAfter(LinkedListNode node, T item); diff --git a/source/LinkedList/LinkedList.Standard.cs b/source/LinkedList/LinkedList.Standard.cs index e286b1e..de52a17 100644 --- a/source/LinkedList/LinkedList.Standard.cs +++ b/source/LinkedList/LinkedList.Standard.cs @@ -1,17 +1,23 @@ -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; - -namespace Open.Collections; +namespace Open.Collections; +/// +/// The container for . +/// public static class LinkedList { + /// + /// A standard implementation that allows for the interface. + /// + /// This allows for other linked lists to be used interchangeably. public sealed class Standard : LinkedList, ILinkedList { + /// [ExcludeFromCodeCoverage] - public Standard() + public Standard() : base() { } + /// [ExcludeFromCodeCoverage] public Standard(IEnumerable initial) : base(initial) { diff --git a/source/ListWrapper.cs b/source/ListWrapper.cs index c465d07..e7dd336 100644 --- a/source/ListWrapper.cs +++ b/source/ListWrapper.cs @@ -1,18 +1,13 @@ -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; +namespace Open.Collections; -namespace Open.Collections; -public class ListWrapper - : CollectionWrapper, IList +/// +/// A wrapper for that allows for easy extension. +/// +public class ListWrapper( + TList source, bool owner = false) + : CollectionWrapper(source, owner), IList where TList : class, IList { - [ExcludeFromCodeCoverage] - public ListWrapper(TList source, bool owner = false) - : base(source, owner) - { - } - /// [ExcludeFromCodeCoverage] public virtual T this[int index] @@ -37,15 +32,22 @@ public virtual void RemoveAt(int index) => InternalSource.RemoveAt(index); } +/// +/// A wrapper for that allows for easy extension. +/// [ExcludeFromCodeCoverage] public class ListWrapper : ListWrapper> { + /// + /// Initializes a new instance of the class. + /// public ListWrapper(IList source, bool owner = false) : base(source, owner) { } + /// public ListWrapper(int capacity = 0) : base(new List(capacity)) { diff --git a/source/NonGeneric/Extensions.Synchronized.cs b/source/NonGeneric/Extensions.Synchronized.cs index 6e32848..df2a3e6 100644 --- a/source/NonGeneric/Extensions.Synchronized.cs +++ b/source/NonGeneric/Extensions.Synchronized.cs @@ -1,12 +1,11 @@ using Open.Threading; -using System; using System.Collections; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Diagnostics.Contracts; namespace Open.Collections.NonGeneric; +/// +/// Extensions for non-generic collections. +/// [SuppressMessage("ReSharper", "ParameterOnlyUsedForPreconditionCheck.Global")] public static partial class Extensions { diff --git a/source/NonGeneric/Extensions.cs b/source/NonGeneric/Extensions.cs index efdc533..4f683f0 100644 --- a/source/NonGeneric/Extensions.cs +++ b/source/NonGeneric/Extensions.cs @@ -1,6 +1,4 @@ -using System; -using System.Collections; -using System.Diagnostics.Contracts; +using System.Collections; namespace Open.Collections.NonGeneric; diff --git a/source/Open.Collections.csproj b/source/Open.Collections.csproj index 51de950..a6235ca 100644 --- a/source/Open.Collections.csproj +++ b/source/Open.Collections.csproj @@ -1,9 +1,10 @@  - netstandard2.0;netstandard2.1 + netstandard2.0;netstandard2.1;net9.0 latest enable + true true true true @@ -18,7 +19,7 @@ https://github.com/Open-NET-Libraries/Open.Collections/ https://github.com/Open-NET-Libraries/Open.Collections/ git - 3.1.4 + 4.2.0 MIT true @@ -26,10 +27,12 @@ snupkg logo.png README.md + IDE0130;CA1510;CA1068;IDE0305;IDE0301;RCS1196; - + + @@ -41,8 +44,6 @@ True \ - - True @@ -50,13 +51,19 @@ - - - + + + + - + + + + $(NoWarn);nullable + + \ No newline at end of file diff --git a/source/OrderedDictionary.cs b/source/OrderedDictionary.cs index fc47464..8cfff36 100644 --- a/source/OrderedDictionary.cs +++ b/source/OrderedDictionary.cs @@ -1,20 +1,18 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Runtime.CompilerServices; - -namespace Open.Collections; +namespace Open.Collections; +/// +/// A dictionary that maintains the order of items as they are added +/// and can be accessed by index. +/// public class OrderedDictionary : DictionaryWrapperBase>>, IDictionary + where TKey : notnull { /// [ExcludeFromCodeCoverage] public OrderedDictionary() : base(new LinkedList>(), true) - => _lookup = new(); + => _lookup = []; /// [ExcludeFromCodeCoverage] @@ -23,9 +21,14 @@ public OrderedDictionary(int capacity) => _lookup = new(capacity); private Dictionary>> _lookup; + + /// + /// The dictionary that can look up the node for a key. + /// protected Dictionary>> Lookup => _lookup ?? throw new ObjectDisposedException(GetType().ToString()); + /// [ExcludeFromCodeCoverage] protected override void OnDispose() { @@ -34,17 +37,22 @@ protected override void OnDispose() base.OnDispose(); } + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override TValue GetValueInternal(TKey key) => Lookup[key].Value.Value; + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void SetValueInternal(TKey key, TValue value) => SetValue(key, value); + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void AddInternal(in KeyValuePair kvp) => AddNode(in kvp); - /// + /// + /// Updates a value by key and returns true if the value changed. + /// public virtual bool SetValue(TKey key, TValue value) { AssertIsAlive(); @@ -61,11 +69,13 @@ public virtual bool SetValue(TKey key, TValue value) return true; } + /// protected override ICollection GetKeys() => new ReadOnlyCollectionAdapter( ThrowIfDisposed(InternalSource.Select(e => e.Key)), () => InternalSource.Count); + /// protected override ICollection GetValues() => new ReadOnlyCollectionAdapter( ThrowIfDisposed(InternalSource.Select(e => e.Value)), @@ -76,8 +86,13 @@ protected override ICollection GetValues() protected override void AddInternal(TKey key, TValue value) => AddInternal(KeyValuePair.Create(key, value)); + /// + /// Adds a node to the lookup dictionary. + /// #if NETSTANDARD2_0 +#pragma warning disable IDE0079 // Remove unnecessary suppression [SuppressMessage("Roslynator", "RCS1242:Do not pass non-read-only struct by read-only reference.", Justification = "KeyValuePairs are not truly readonly until NET Standard 2.1.")] +#pragma warning restore IDE0079 // Remove unnecessary suppression #endif protected virtual LinkedListNode> AddNode( in KeyValuePair kvp) @@ -129,13 +144,14 @@ public override void Clear() base.Clear(); } + /// public override bool Remove(KeyValuePair item) { var key = item.Key; if (Lookup.TryGetValue(key, out var node) && (node.Value.Value?.Equals(item.Value) ?? item.Value is null)) { - Debug.Assert(key?.Equals(node.Value.Key) ?? node.Value.Key is null); + Debug.Assert(key.Equals(node.Value.Key)); bool removed = Lookup.Remove(key); Debug.Assert(removed); InternalSource.Remove(node); diff --git a/source/Queue/Extensions.cs b/source/Queue/Extensions.cs index e863e77..bafa17c 100644 --- a/source/Queue/Extensions.cs +++ b/source/Queue/Extensions.cs @@ -1,6 +1,4 @@ using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; namespace Open.Collections; diff --git a/source/Queue/IQueue.cs b/source/Queue/IQueue.cs index 742208a..bb868dd 100644 --- a/source/Queue/IQueue.cs +++ b/source/Queue/IQueue.cs @@ -1,15 +1,28 @@ namespace Open.Collections; +/// +/// A queue interface. +/// public interface IQueue { /// void Enqueue(T item); /// - bool TryDequeue(out T item); + bool TryDequeue( +#if NETSTANDARD2_0 +#else + [MaybeNullWhen(false)] +#endif + out T item); /// - bool TryPeek(out T item); + bool TryPeek( +#if NETSTANDARD2_0 +#else + [MaybeNullWhen(false)] +#endif + out T item); /// int Count { get; } diff --git a/source/Queue/Queue.Concurrent.cs b/source/Queue/Queue.Concurrent.cs index 6c51dbc..2af79c0 100644 --- a/source/Queue/Queue.Concurrent.cs +++ b/source/Queue/Queue.Concurrent.cs @@ -1,10 +1,13 @@ using System.Collections.Concurrent; -using System.Diagnostics.CodeAnalysis; namespace Open.Collections; public static partial class Queue { + /// + /// A standard implementation that allows for the interface. + /// + /// This allows for other queues to be used interchangeably. public sealed class Concurrent : ConcurrentQueue, IQueue { #if NETSTANDARD2_0 diff --git a/source/Queue/Queue.Standard.cs b/source/Queue/Queue.Standard.cs index 8dd7554..995aa72 100644 --- a/source/Queue/Queue.Standard.cs +++ b/source/Queue/Queue.Standard.cs @@ -1,17 +1,26 @@ -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; - -namespace Open.Collections; +namespace Open.Collections; +/// +/// Static collection of queue implementations. +/// public static partial class Queue { + /// + /// A standard queue implementation that is based upon . + /// public class Standard : Queue, IQueue { + /// + /// Construct an empty queue. + /// [ExcludeFromCodeCoverage] - protected Standard() + public Standard() { } + /// + /// Construct a queue with an initial set of items. + /// [ExcludeFromCodeCoverage] public Standard(IEnumerable initial) : base(initial) { @@ -38,12 +47,16 @@ public virtual bool TryPeek(out T item) /// [ExcludeFromCodeCoverage] - public new virtual bool TryDequeue(out T item) + public new virtual bool TryDequeue( + [MaybeNullWhen(false)] + out T item) => base.TryDequeue(out item); /// [ExcludeFromCodeCoverage] - public new virtual bool TryPeek(out T item) + public new virtual bool TryPeek( + [MaybeNullWhen(false)] + out T item) => base.TryPeek(out item); #endif diff --git a/source/ReadOnlyCollectionAdapter.cs b/source/ReadOnlyCollectionAdapter.cs index 5413294..dbacc66 100644 --- a/source/ReadOnlyCollectionAdapter.cs +++ b/source/ReadOnlyCollectionAdapter.cs @@ -1,33 +1,29 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Runtime.CompilerServices; +using System.Collections; namespace Open.Collections; -public sealed class ReadOnlyCollectionAdapter +/// +/// A read-only collection adapter that can be used to wrap an existing collection. +/// +[method: ExcludeFromCodeCoverage] +public sealed class ReadOnlyCollectionAdapter( + IEnumerable source, Func getCount) : IReadOnlyCollection, ICollection { - readonly IEnumerable _source; - readonly Func _getCount; - readonly Func _contains; - - [ExcludeFromCodeCoverage] - public ReadOnlyCollectionAdapter(IEnumerable source, Func getCount) - { - _source = source ?? throw new ArgumentNullException(nameof(source)); - _getCount = getCount ?? throw new ArgumentNullException(nameof(getCount)); - _contains = source is ICollection c + readonly IEnumerable _source = source ?? throw new ArgumentNullException(nameof(source)); + readonly Func _getCount = getCount ?? throw new ArgumentNullException(nameof(getCount)); + readonly Func _contains = source is ICollection c ? item => c.Contains(item) : item => source.Contains(item); - } + /// + /// Initializes a new instance of the class. + /// [ExcludeFromCodeCoverage] public ReadOnlyCollectionAdapter(IReadOnlyCollection source) : this(source, () => source.Count) { } + /// [ExcludeFromCodeCoverage] public ReadOnlyCollectionAdapter(ICollection source) : this(source, () => source.Count) { } diff --git a/source/ReadOnlyCollectionWrapper.cs b/source/ReadOnlyCollectionWrapper.cs index 412c83f..2768c52 100644 --- a/source/ReadOnlyCollectionWrapper.cs +++ b/source/ReadOnlyCollectionWrapper.cs @@ -1,19 +1,29 @@ using Open.Disposable; -using System; using System.Collections; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; namespace Open.Collections; +/// +/// A disposable aware read-only wrapper for a collection. +/// public class ReadOnlyCollectionWrapper : DisposableBase, IReadOnlyCollection where TCollection : class, ICollection { + /// + /// The underlying collection. + /// protected TCollection? InternalUnsafeSource; + + /// + /// If , will call if the source is when this is disposed. + /// protected readonly bool SourceOwned; + /// + /// The underlying collection. + /// + /// If this has been disposed. protected TCollection InternalSource => InternalUnsafeSource ?? throw new ObjectDisposedException(GetType().ToString()); @@ -27,6 +37,7 @@ protected TCollection InternalSource /// /// If the is . [ExcludeFromCodeCoverage] + [SuppressMessage("Style", "IDE0290:Use primary constructor")] public ReadOnlyCollectionWrapper(TCollection source, bool owner = false) { InternalUnsafeSource = source ?? throw new ArgumentNullException(nameof(source)); @@ -36,13 +47,23 @@ public ReadOnlyCollectionWrapper(TCollection source, bool owner = false) private void ThrowIfDisposedInternal() => base.AssertIsAlive(); private Action? _throwIfDisposed; + + /// + /// A delegate to throw an exception if this has been disposed. + /// protected Action ThrowIfDisposedDelegate => _throwIfDisposed ??= ThrowIfDisposedInternal; + /// + /// Produces an enumerable that will throw an exception if this has been disposed. + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] protected IEnumerable ThrowIfDisposed(IEnumerable source) => source.Preflight(ThrowIfDisposedDelegate).BeforeGetEnumerator(ThrowIfDisposedDelegate); + /// + /// A utility for ensuring the source is not disposed. + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] protected T2 ThrowIfDisposed(T2 source) { @@ -97,6 +118,7 @@ public virtual void Export(ICollection to) => to.AddRange(InternalSource); #region Dispose + /// [ExcludeFromCodeCoverage] protected override void OnDispose() { diff --git a/source/SortDirection.cs b/source/SortDirection.cs index a2c2fac..ad59f15 100644 --- a/source/SortDirection.cs +++ b/source/SortDirection.cs @@ -1,7 +1,17 @@ namespace Open.Collections; +/// +/// Represents the direction of sorting. +/// public enum SortDirection : sbyte { + /// + /// Ascending Order + /// Ascending = +1, + + /// + /// Descending Order + /// Descending = -1 } diff --git a/source/Subsets.cs b/source/Subsets.cs index 1c95962..0ce9b07 100644 --- a/source/Subsets.cs +++ b/source/Subsets.cs @@ -1,12 +1,12 @@ -using System; -using System.Buffers; -using System.Collections.Generic; +using System.Buffers; using System.Collections.Immutable; using System.Collections.ObjectModel; -using System.Diagnostics.Contracts; namespace Open.Collections; +/// +/// Provides methods for generating subsets of a set. +/// public static class Subsets { internal static IEnumerable IndexesInternal(int sourceLength, int subsetLength, int[] buffer) @@ -18,6 +18,7 @@ internal static IEnumerable IndexesInternal(int sourceLength, int subsetL buffer[0] = i; yield return buffer; } + yield break; } diff --git a/source/Synchronized/ConcurrentList.cs b/source/Synchronized/ConcurrentList.cs index fcf865b..c027f75 100644 --- a/source/Synchronized/ConcurrentList.cs +++ b/source/Synchronized/ConcurrentList.cs @@ -1,11 +1,5 @@ using Open.Threading; -using System; using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; -using System.Threading; namespace Open.Collections.Synchronized; @@ -16,6 +10,8 @@ namespace Open.Collections.Synchronized; public sealed class ConcurrentList : ListWrapper>, ISynchronizedCollection { int _count; + + /// [ExcludeFromCodeCoverage] public override int Count { @@ -29,6 +25,7 @@ public override int Count private readonly Queue.Concurrent _buffer = new(); private readonly ReaderWriterLockSlim RWLock = new(); + /// [ExcludeFromCodeCoverage] protected override void OnDispose() { @@ -68,12 +65,17 @@ private List Grow() capacity = int.MaxValue; break; } + capacity *= 2; } + list.Capacity = capacity; return list; } + /// + /// Gets or sets the capacity of the list. + /// public int Capacity { get => InternalSource.Capacity; @@ -84,11 +86,17 @@ public int Capacity } } + /// + /// Constructs a new instance with the specified capacity. + /// [ExcludeFromCodeCoverage] public ConcurrentList(int capacity) : base(new List(capacity)) { } + /// + /// Constructs a new instance. + /// [ExcludeFromCodeCoverage] - public ConcurrentList() : base(new List()) { } + public ConcurrentList() : base([]) { } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void AssertValidIndex(int index) @@ -111,6 +119,7 @@ public override T this[int index] } } + /// protected override void AddInternal(in T item) { _buffer.Enqueue(item); diff --git a/source/Synchronized/ISynchronizedCollection.cs b/source/Synchronized/ISynchronizedCollection.cs index 4804b67..81bb3c9 100644 --- a/source/Synchronized/ISynchronizedCollection.cs +++ b/source/Synchronized/ISynchronizedCollection.cs @@ -1,8 +1,8 @@ -using System; -using System.Collections.Generic; - -namespace Open.Collections; +namespace Open.Collections; +/// +/// Interface for a collection that can be synchronized (thread-safe). +/// public interface ISynchronizedCollection : ICollection { /// diff --git a/source/Synchronized/ISynchronizedCollectionWrapper.cs b/source/Synchronized/ISynchronizedCollectionWrapper.cs index 71c62b1..e2d6da9 100644 --- a/source/Synchronized/ISynchronizedCollectionWrapper.cs +++ b/source/Synchronized/ISynchronizedCollectionWrapper.cs @@ -1,8 +1,8 @@ -using System; -using System.Collections.Generic; - -namespace Open.Collections.Synchronized; +namespace Open.Collections.Synchronized; +/// +/// Interface for a wrapper around a collection that allows for tread-safe modification. +/// public interface ISynchronizedCollectionWrapper : ISynchronizedCollection where TCollection : ICollection diff --git a/source/Synchronized/LockSynchronizedCollectionWrapper.cs b/source/Synchronized/LockSynchronizedCollectionWrapper.cs index 517c302..8acaa43 100644 --- a/source/Synchronized/LockSynchronizedCollectionWrapper.cs +++ b/source/Synchronized/LockSynchronizedCollectionWrapper.cs @@ -1,19 +1,17 @@ using Open.Threading; -using System; -using System.Collections.Generic; using System.Collections.Immutable; -using System.Diagnostics.CodeAnalysis; -using System.Linq; namespace Open.Collections.Synchronized; -public class LockSynchronizedCollectionWrapper - : CollectionWrapper, ISynchronizedCollectionWrapper - where TCollection : class, ICollection +/// +/// A disposable synchronized wrapper for a collection. +/// +public class LockSynchronizedCollectionWrapper( + TCollection source, bool owner = false) + : CollectionWrapper(source, owner), ISynchronizedCollectionWrapper + where TCollection : class, ICollection { - protected LockSynchronizedCollectionWrapper(TCollection source, bool owner = false) - : base(source, owner) { } - + /// protected override void OnBeforeDispose() { ThreadSafety.Lock(Sync, () => { }, 1000); @@ -30,7 +28,11 @@ public override void Add(T item) } /// +#if NET9_0_OR_GREATER + public override void AddThese(T item1, T item2, params ReadOnlySpan items) +#else public override void AddThese(T item1, T item2, params T[] items) +#endif { lock (Sync) { diff --git a/source/Synchronized/LockSynchronizedDictionaryWrapper.cs b/source/Synchronized/LockSynchronizedDictionaryWrapper.cs index ef44733..e47a2cf 100644 --- a/source/Synchronized/LockSynchronizedDictionaryWrapper.cs +++ b/source/Synchronized/LockSynchronizedDictionaryWrapper.cs @@ -1,18 +1,13 @@ -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; +namespace Open.Collections.Synchronized; -namespace Open.Collections.Synchronized; - -/// +/// +/// A Monitor synchronized wrapper for a dictionary. +/// [ExcludeFromCodeCoverage] -public class LockSynchronizedDictionaryWrapper - : LockSynchronizedCollectionWrapper, TDictionary>, IDictionary +public class LockSynchronizedDictionaryWrapper(TDictionary dictionary) + : LockSynchronizedCollectionWrapper, TDictionary>(dictionary), IDictionary where TDictionary : class, IDictionary { - /// - public LockSynchronizedDictionaryWrapper(TDictionary dictionary) : base(dictionary) { } - /// public virtual TValue this[TKey key] { @@ -64,22 +59,25 @@ public virtual bool Remove(TKey key) /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool TryGetValue(TKey key, out TValue value) + public bool TryGetValue(TKey key, +#if NET9_0_OR_GREATER + [MaybeNullWhen(false)] +#endif + out TValue value) => InternalSource.TryGetValue(key, out value); } [ExcludeFromCodeCoverage] -public class LockSynchronizedDictionaryWrapper - : LockSynchronizedDictionaryWrapper> +public class LockSynchronizedDictionaryWrapper( + IDictionary dictionary) + : LockSynchronizedDictionaryWrapper>(dictionary) { - public LockSynchronizedDictionaryWrapper(IDictionary dictionary) : base(dictionary) - { - } } [ExcludeFromCodeCoverage] public class LockSynchronizedDictionary : LockSynchronizedDictionaryWrapper + where TKey : notnull { public LockSynchronizedDictionary(int capacity) : base(new Dictionary(capacity)) { } public LockSynchronizedDictionary() : base(new Dictionary()) { } diff --git a/source/Synchronized/LockSynchronizedHashSet.cs b/source/Synchronized/LockSynchronizedHashSet.cs index 956c6c7..37ac5ee 100644 --- a/source/Synchronized/LockSynchronizedHashSet.cs +++ b/source/Synchronized/LockSynchronizedHashSet.cs @@ -1,17 +1,25 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; - -namespace Open.Collections.Synchronized; +namespace Open.Collections.Synchronized; +/// +/// A Monitor synchronized . +/// public sealed class LockSynchronizedHashSet : LockSynchronizedCollectionWrapper>, ISet { + /// + /// Constructs a new instance. + /// [ExcludeFromCodeCoverage] - public LockSynchronizedHashSet() : base(new HashSet()) { } + public LockSynchronizedHashSet() : base([]) { } + /// + /// Constructs a new instance with the specified capacity. + /// [ExcludeFromCodeCoverage] - public LockSynchronizedHashSet(IEnumerable collection) : base(new HashSet(collection)) { } + public LockSynchronizedHashSet(IEnumerable collection) : base([.. collection]) { } + /// + /// Constructs a new instance with the specified capacity and comparer. + /// [ExcludeFromCodeCoverage] public LockSynchronizedHashSet(IEnumerable collection, IEqualityComparer comparer) : base(new HashSet(collection, comparer)) { } diff --git a/source/Synchronized/LockSynchronizedIndexedDictionary.cs b/source/Synchronized/LockSynchronizedIndexedDictionary.cs index cec792a..d6c0e1b 100644 --- a/source/Synchronized/LockSynchronizedIndexedDictionary.cs +++ b/source/Synchronized/LockSynchronizedIndexedDictionary.cs @@ -1,16 +1,13 @@ -using System.Diagnostics.CodeAnalysis; +namespace Open.Collections.Synchronized; -namespace Open.Collections.Synchronized; - -/// +/// +/// A Monitor synchronized . +/// [ExcludeFromCodeCoverage] // Nothing worth covering here yet. -public sealed class LockSynchronizedIndexedDictionary - : LockSynchronizedDictionaryWrapper>, IIndexedDictionary +public sealed class LockSynchronizedIndexedDictionary(int capacity = 0) + : LockSynchronizedDictionaryWrapper>(new IndexedDictionary(capacity)), IIndexedDictionary + where TKey : notnull { - /// - public LockSynchronizedIndexedDictionary(int capacity = 0) - : base(new IndexedDictionary(capacity)) { } - /// public TKey GetKeyAt(int index) => InternalSource.GetKeyAt(index); diff --git a/source/Synchronized/LockSynchronizedLinkedList.cs b/source/Synchronized/LockSynchronizedLinkedList.cs index 9ef7751..a475b70 100644 --- a/source/Synchronized/LockSynchronizedLinkedList.cs +++ b/source/Synchronized/LockSynchronizedLinkedList.cs @@ -1,26 +1,31 @@ using Open.Threading; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; namespace Open.Collections.Synchronized; // LinkedLists are a bit different and don't have an default interface. // Overriding the .Value property of the nodes is beyond the scope of this. All that's needed is to synchronize the collection. -public sealed class LockSynchronizedLinkedList : LockSynchronizedCollectionWrapper>, ILinkedList + +/// +/// A Monitor synchronized wrapper for a . +/// +public sealed class LockSynchronizedLinkedList + : LockSynchronizedCollectionWrapper>, ILinkedList { + /// public LockSynchronizedLinkedList() : base(new LinkedList()) { } + /// [ExcludeFromCodeCoverage] public LockSynchronizedLinkedList(IEnumerable collection) : base(new LinkedList(collection)) { } /// [ExcludeFromCodeCoverage] - public LinkedListNode First + public LinkedListNode? First => InternalSource.First; /// [ExcludeFromCodeCoverage] - public LinkedListNode Last + public LinkedListNode? Last => InternalSource.Last; /// diff --git a/source/Synchronized/LockSynchronizedList.cs b/source/Synchronized/LockSynchronizedList.cs index 55dfd58..effad64 100644 --- a/source/Synchronized/LockSynchronizedList.cs +++ b/source/Synchronized/LockSynchronizedList.cs @@ -1,13 +1,24 @@ -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; - -namespace Open.Collections.Synchronized; +namespace Open.Collections.Synchronized; +/// +/// A synchronized list. +/// [ExcludeFromCodeCoverage] public sealed class LockSynchronizedList : LockSynchronizedListWrapper { - public LockSynchronizedList() : base(new List()) { } + /// + /// Constructs a new instance. + /// + public LockSynchronizedList() : base([]) { } + + /// + /// Constructs a new instance with the specified capacity. + /// public LockSynchronizedList(int capacity = 0) : base(new List(capacity)) { } - public LockSynchronizedList(IEnumerable collection) : base(new List(collection)) { } + + /// + /// Constructs a new instance with the specified collection. + /// + public LockSynchronizedList(IEnumerable collection) : base([.. collection]) { } } diff --git a/source/Synchronized/LockSynchronizedListWrapper.cs b/source/Synchronized/LockSynchronizedListWrapper.cs index 5334df8..e371bbf 100644 --- a/source/Synchronized/LockSynchronizedListWrapper.cs +++ b/source/Synchronized/LockSynchronizedListWrapper.cs @@ -1,15 +1,14 @@ -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; - -namespace Open.Collections.Synchronized; +namespace Open.Collections.Synchronized; +/// +/// A Monitor synchronized list wrapper. +/// [ExcludeFromCodeCoverage] -public class LockSynchronizedListWrapper - : LockSynchronizedCollectionWrapper, IList +public class LockSynchronizedListWrapper( + TList list, bool owner = false) + : LockSynchronizedCollectionWrapper(list, owner), IList where TList : class, IList { - public LockSynchronizedListWrapper(TList list, bool owner = false) : base(list, owner) { } - // This is a simplified version. // It could be possible to allow indexed values to change independently of one another. // If that fine grained of read-write control is necessary, then use the ThreadSafety utility and extensions. @@ -40,11 +39,10 @@ public void RemoveAt(int index) } } +/// [ExcludeFromCodeCoverage] -public class LockSynchronizedListWrapper - : LockSynchronizedListWrapper> +public class LockSynchronizedListWrapper( + IList list, bool owner = false) + : LockSynchronizedListWrapper>(list, owner) { - public LockSynchronizedListWrapper(IList list, bool owner = false) : base(list, owner) - { - } } \ No newline at end of file diff --git a/source/Synchronized/LockSynchronizedOrderedDictionary.cs b/source/Synchronized/LockSynchronizedOrderedDictionary.cs index 10c2e3b..d0f19e9 100644 --- a/source/Synchronized/LockSynchronizedOrderedDictionary.cs +++ b/source/Synchronized/LockSynchronizedOrderedDictionary.cs @@ -1,13 +1,12 @@ -using System.Diagnostics.CodeAnalysis; +namespace Open.Collections.Synchronized; -namespace Open.Collections.Synchronized; - -/// +/// +/// A synchronized . +/// [ExcludeFromCodeCoverage] -public sealed class LockSynchronizedOrderedDictionary - : LockSynchronizedDictionaryWrapper> +public sealed class LockSynchronizedOrderedDictionary( + int capacity = 0) + : LockSynchronizedDictionaryWrapper>(new OrderedDictionary(capacity)) + where TKey : notnull { - /// - public LockSynchronizedOrderedDictionary(int capacity = 0) - : base(new OrderedDictionary(capacity)) { } } diff --git a/source/Synchronized/LockSynchronizedQueue.cs b/source/Synchronized/LockSynchronizedQueue.cs index 4d95d2e..423b3ab 100644 --- a/source/Synchronized/LockSynchronizedQueue.cs +++ b/source/Synchronized/LockSynchronizedQueue.cs @@ -1,6 +1,4 @@ -using System.Diagnostics.CodeAnalysis; - -namespace Open.Collections.Synchronized; +namespace Open.Collections.Synchronized; [ExcludeFromCodeCoverage] public class LockSynchronizedQueue : Queue.Standard, IQueue @@ -17,7 +15,12 @@ public class LockSynchronizedQueue : Queue.Standard, IQueue } /// - public override bool TryDequeue(out T item) + public override bool TryDequeue( +#if NETSTANDARD2_0 +#else + [MaybeNullWhen(false)] +#endif + out T item) { if (Count == 0) { @@ -30,7 +33,12 @@ public override bool TryDequeue(out T item) } /// - public override bool TryPeek(out T item) + public override bool TryPeek( +#if NETSTANDARD2_0 +#else + [MaybeNullWhen(false)] +#endif + out T item) { if (Count == 0) { diff --git a/source/Synchronized/ReadWriteSynchronizedCollectionWrapper.cs b/source/Synchronized/ReadWriteSynchronizedCollectionWrapper.cs index fc2ae02..fe26efc 100644 --- a/source/Synchronized/ReadWriteSynchronizedCollectionWrapper.cs +++ b/source/Synchronized/ReadWriteSynchronizedCollectionWrapper.cs @@ -1,24 +1,21 @@ using Open.Threading; -using System; -using System.Collections.Generic; using System.Collections.Immutable; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Threading; namespace Open.Collections.Synchronized; -public class ReadWriteSynchronizedCollectionWrapper - : CollectionWrapper, ISynchronizedCollectionWrapper +/// +/// A disposable read-write synchronized wrapper for a collection. +/// +public class ReadWriteSynchronizedCollectionWrapper( + TCollection source, bool owner = false) + : CollectionWrapper(source, owner), ISynchronizedCollectionWrapper where TCollection : class, ICollection { + /// + /// The used for synchronization. + /// protected ReaderWriterLockSlim RWLock = new(LockRecursionPolicy.SupportsRecursion); // Support recursion for read -> write locks. - protected ReadWriteSynchronizedCollectionWrapper(TCollection source, bool owner = false) - : base(source, owner) - { - } - #region Implementation of ICollection /// @@ -31,7 +28,11 @@ public override void Add(T item) } /// +#if NET9_0_OR_GREATER + public override void AddThese(T item1, T item2, params ReadOnlySpan items) +#else public override void AddThese(T item1, T item2, params T[] items) +#endif { using var write = RWLock.WriteLock(); AddInternal(item1); @@ -118,10 +119,12 @@ public override Span CopyTo(Span span) #endregion #region Dispose - protected override void OnBeforeDispose() => + /// + protected override void OnBeforeDispose() // Give everything else a chance to finish up. - RWLock.TryWrite(1000, () => { }); + => RWLock.TryWrite(1000, () => { }); + /// protected override void OnDispose() { RWLock.Dispose(); diff --git a/source/Synchronized/ReadWriteSynchronizedDictionaryWrapper.cs b/source/Synchronized/ReadWriteSynchronizedDictionaryWrapper.cs index 44c1aff..21e2f6e 100644 --- a/source/Synchronized/ReadWriteSynchronizedDictionaryWrapper.cs +++ b/source/Synchronized/ReadWriteSynchronizedDictionaryWrapper.cs @@ -1,19 +1,12 @@ using Open.Threading; -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; namespace Open.Collections.Synchronized; -/// -public class ReadWriteSynchronizedDictionaryWrapper - : ReadWriteSynchronizedCollectionWrapper, TDictionary>, IDictionary +public class ReadWriteSynchronizedDictionaryWrapper( + TDictionary dictionary, bool owner = false) + : ReadWriteSynchronizedCollectionWrapper, TDictionary>(dictionary, owner), IDictionary where TDictionary : class, IDictionary { - /// - public ReadWriteSynchronizedDictionaryWrapper(TDictionary dictionary, bool owner = false) : base(dictionary, owner) { } - /// [ExcludeFromCodeCoverage] public virtual TValue this[TKey key] @@ -29,6 +22,7 @@ public virtual TValue this[TKey key] InternalSource[key] = value; return; } + using var write = RWLock.WriteLock(); InternalSource[key] = value; } @@ -79,7 +73,11 @@ public virtual bool Remove(TKey key) /// [ExcludeFromCodeCoverage] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool TryGetValue(TKey key, out TValue value) + public bool TryGetValue(TKey key, +#if NET9_0_OR_GREATER + [MaybeNullWhen(false)] +#endif + out TValue value) => InternalSource.TryGetValue(key, out value); /// @@ -104,17 +102,16 @@ public virtual bool IfNotContainsKey(TKey key, Action> } [ExcludeFromCodeCoverage] -public class ReadWriteSynchronizedDictionaryWrapper - : ReadWriteSynchronizedDictionaryWrapper> +public class ReadWriteSynchronizedDictionaryWrapper( + IDictionary dictionary, bool owner = false) + : ReadWriteSynchronizedDictionaryWrapper>(dictionary, owner) { - public ReadWriteSynchronizedDictionaryWrapper(IDictionary dictionary, bool owner = false) : base(dictionary, owner) - { - } } [ExcludeFromCodeCoverage] public class ReadWriteSynchronizedDictionary : ReadWriteSynchronizedDictionaryWrapper + where TKey : notnull { public ReadWriteSynchronizedDictionary() : base(new Dictionary()) { } diff --git a/source/Synchronized/ReadWriteSynchronizedHashSet.cs b/source/Synchronized/ReadWriteSynchronizedHashSet.cs index 8aa4f55..494a128 100644 --- a/source/Synchronized/ReadWriteSynchronizedHashSet.cs +++ b/source/Synchronized/ReadWriteSynchronizedHashSet.cs @@ -1,24 +1,33 @@ using Open.Threading; -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; namespace Open.Collections.Synchronized; +/// +/// A synchronized that uses a for thread safety. +/// public sealed class ReadWriteSynchronizedHashSet : ReadWriteSynchronizedCollectionWrapper>, ISet { + /// + /// Constructs a new instance. + /// [ExcludeFromCodeCoverage] - public ReadWriteSynchronizedHashSet() : base(new HashSet()) { } + public ReadWriteSynchronizedHashSet() : base([]) { } + /// + /// Constructs a new instance with the specified capacity. + /// + /// [ExcludeFromCodeCoverage] - public ReadWriteSynchronizedHashSet(IEnumerable collection) : base(new HashSet(collection)) { } + public ReadWriteSynchronizedHashSet(IEnumerable collection) : base([.. collection]) { } + /// + /// Constructs a new instance with the specified capacity and comparer. + /// [ExcludeFromCodeCoverage] public ReadWriteSynchronizedHashSet(IEnumerable collection, IEqualityComparer comparer) : base(new HashSet(collection, comparer)) { } - // Asumes that .Contains is a thread-safe read-only operation. + // Assumes that .Contains is a thread-safe read-only operation. // But any potentially iterative operation will be locked. /// diff --git a/source/Synchronized/ReadWriteSynchronizedLinkedList.cs b/source/Synchronized/ReadWriteSynchronizedLinkedList.cs index 3377598..f7f4a9e 100644 --- a/source/Synchronized/ReadWriteSynchronizedLinkedList.cs +++ b/source/Synchronized/ReadWriteSynchronizedLinkedList.cs @@ -1,28 +1,37 @@ using Open.Threading; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; namespace Open.Collections.Synchronized; +/// +/// A synchronized wrapper for that uses a for thread safety. +/// +/// public sealed class ReadWriteSynchronizedLinkedList : ReadWriteSynchronizedCollectionWrapper>, ILinkedList { + /// + /// Constructs a new instance. + /// [ExcludeFromCodeCoverage] public ReadWriteSynchronizedLinkedList() - : base(new LinkedList()) { } + : base([]) { } + /// + /// Constructs a new instance with the specified collection. + /// + /// [ExcludeFromCodeCoverage] public ReadWriteSynchronizedLinkedList(IEnumerable collection) : base(new LinkedList(collection)) { } /// [ExcludeFromCodeCoverage] - public LinkedListNode First + public LinkedListNode? First => InternalSource.First; /// [ExcludeFromCodeCoverage] - public LinkedListNode Last + public LinkedListNode? Last => InternalSource.Last; /// diff --git a/source/Synchronized/ReadWriteSynchronizedList.cs b/source/Synchronized/ReadWriteSynchronizedList.cs index e7a832e..5052a60 100644 --- a/source/Synchronized/ReadWriteSynchronizedList.cs +++ b/source/Synchronized/ReadWriteSynchronizedList.cs @@ -1,18 +1,27 @@ -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; - -namespace Open.Collections.Synchronized; +namespace Open.Collections.Synchronized; +/// +/// A synchronized that uses a for thread safety. +/// [ExcludeFromCodeCoverage] public sealed class ReadWriteSynchronizedList : ReadWriteSynchronizedListWrapper { + /// + /// Constructs a new instance. + /// public ReadWriteSynchronizedList() - : base(new List()) { } + : base([]) { } + /// + /// Constructs a new instance with the specified capacity. + /// public ReadWriteSynchronizedList(int capacity = 0) : base(new List(capacity)) { } + /// + /// Constructs a new instance with the specified collection. + /// public ReadWriteSynchronizedList(IEnumerable collection) - : base(new List(collection)) { } + : base([.. collection]) { } } diff --git a/source/Synchronized/ReadWriteSynchronizedListWrapper.cs b/source/Synchronized/ReadWriteSynchronizedListWrapper.cs index 6b649cf..0f6aca7 100644 --- a/source/Synchronized/ReadWriteSynchronizedListWrapper.cs +++ b/source/Synchronized/ReadWriteSynchronizedListWrapper.cs @@ -1,16 +1,15 @@ using Open.Threading; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; namespace Open.Collections.Synchronized; -public class ReadWriteSynchronizedListWrapper - : ReadWriteSynchronizedCollectionWrapper, IList +/// +/// A synchronized wrapper for a list that uses a for synchronization. +/// +public class ReadWriteSynchronizedListWrapper( + TList list, bool owner = false) + : ReadWriteSynchronizedCollectionWrapper(list, owner), IList where TList : class, IList { - [ExcludeFromCodeCoverage] - public ReadWriteSynchronizedListWrapper(TList list, bool owner = false) : base(list, owner) { } - // This is a simplified version. // It could be possible to allow indexed values to change independently of one another. // If that fine grained of read-write control is necessary, then use the ThreadSafety utility and extensions. @@ -58,12 +57,12 @@ public override bool Remove(T item) } } +/// +/// A synchronized wrapper for a list that uses a for synchronization. +/// [ExcludeFromCodeCoverage] -public class ReadWriteSynchronizedListWrapper - : ReadWriteSynchronizedListWrapper> +public class ReadWriteSynchronizedListWrapper( + IList list, bool owner = false) + : ReadWriteSynchronizedListWrapper>(list, owner) { - public ReadWriteSynchronizedListWrapper(IList list, bool owner = false) - : base(list, owner) - { - } } \ No newline at end of file diff --git a/source/Synchronized/Readme.md b/source/Synchronized/Readme.md index be8f371..9db7399 100644 --- a/source/Synchronized/Readme.md +++ b/source/Synchronized/Readme.md @@ -19,7 +19,7 @@ A "snapshot" is simply a copy of the collection at a given moment that can then ### Reads Any operation that the collection cannot be changing (adding/removing) while executing. -ie. ```.Contains(item)``` +i.e. ```.Contains(item)``` ### Writes diff --git a/source/Synchronized/TrackedCollectionWrapper.cs b/source/Synchronized/TrackedCollectionWrapper.cs index e88a3b1..e8e766f 100644 --- a/source/Synchronized/TrackedCollectionWrapper.cs +++ b/source/Synchronized/TrackedCollectionWrapper.cs @@ -1,22 +1,26 @@ using Open.Threading; -using System; using System.Collections; -using System.Collections.Generic; using System.Collections.Immutable; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Threading; namespace Open.Collections.Synchronized; +/// +/// A wrapper for a collection that tracks changes and provides synchronization. +/// public class TrackedCollectionWrapper : ModificationSynchronizedBase, ICollection, IAddMultiple, ISynchronizedCollectionWrapper where TCollection : class, ICollection { + /// + /// The internal source collection. + /// protected TCollection? InternalUnsafeSource; + + /// + /// The internal source collection when not disposed. + /// protected TCollection InternalSource => InternalUnsafeSource ?? throw new ObjectDisposedException(GetType().ToString()); @@ -30,6 +34,9 @@ protected TCollection InternalSource /// public event EventHandler>? Changed; + /// + /// True if there are listeners for the event. + /// protected bool HasChangedListeners => Changed is not null; /// @@ -37,14 +44,21 @@ protected TCollection InternalSource /// public event EventHandler? Cleared; + /// + /// Initializes a new instance of the class. + /// [ExcludeFromCodeCoverage] public TrackedCollectionWrapper(TCollection collection, ModificationSynchronizer? sync = null) : base(sync) => InternalUnsafeSource = collection ?? throw new ArgumentNullException(nameof(collection)); + /// + /// Initializes a new instance of the class. + /// [ExcludeFromCodeCoverage] public TrackedCollectionWrapper(TCollection collection, out ModificationSynchronizer sync) : base(out sync) => InternalUnsafeSource = collection ?? throw new ArgumentNullException(nameof(collection)); + /// [ExcludeFromCodeCoverage] protected override ModificationSynchronizer InitSync(object? sync = null) { @@ -52,6 +66,7 @@ protected override ModificationSynchronizer InitSync(object? sync = null) return new ReadWriteModificationSynchronizer(sync as ReaderWriterLockSlim); } + /// [ExcludeFromCodeCoverage] protected override void OnDispose() { @@ -65,6 +80,11 @@ protected override void OnDispose() private void ThrowIfDisposedInternal() => base.AssertIsAlive(); private Action? _throwIfDisposed; + + /// + /// The delegate to invoke to throw an exception if disposed. + /// + [ExcludeFromCodeCoverage] protected Action ThrowIfDisposedDelegate => _throwIfDisposed ??= ThrowIfDisposedInternal; @@ -76,18 +96,30 @@ public int Count return InternalSource.Count; }); + /// + /// Adds an item to the internal collection. + /// [ExcludeFromCodeCoverage] protected virtual void AddInternal(T item) => InternalSource.Add(item); + /// + /// Invoked after the collection has been modified. + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void OnModified() => Modified?.Invoke(this, EventArgs.Empty); + /// + /// Invoked after a change or group of changes has been made. + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] protected void OnChanged(ItemChange change, T item, int version) => Changed?.Invoke(this, change.CreateArgs(item, version)); + /// + /// Invoked after a change or group of changes has been made. + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] protected virtual void OnChanged(ItemChange change, TIndex index, T item, int version) => Changed?.Invoke(this, change.CreateArgs(index, item, version)); @@ -100,6 +132,9 @@ private void OnAdded(T item, int version) private void OnRemoved(T item, int version) => OnChanged(ItemChange.Removed, item, version); + /// + /// Invoked after the collection has been cleared. + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] protected virtual void OnCleared(int version) => Cleared?.Invoke(this, version); @@ -114,8 +149,11 @@ public void Add(T item) return true; }, version => OnAdded(item, version)); - +#if NET9_0_OR_GREATER + /// +#else /// +#endif public void AddThese(T item1, T item2, params T[] items) => Sync!.Modifying(AssertIsAliveDelegate, () => @@ -163,6 +201,23 @@ public void AddRange(IEnumerable items) }); } +#if NET9_0_OR_GREATER + /// + [Obsolete("This method has to make a copy of items. Use the local AddThese(T, T, T[]) instead.")] + [OverloadResolutionPriority(-1)] + public void AddThese(T item1, T item2, params ReadOnlySpan items) + => AddThese(item1, item2, items.ToArray()); + + /// + [Obsolete("This method has to make a copy of items. Use the local AddThese(T, T, T[]) instead.")] + [OverloadResolutionPriority(-1)] + public void AddRange(ReadOnlySpan items) + => AddThese(default!, default!, items.ToArray()); +#endif + + /// + /// Clears the internal collection. + /// [ExcludeFromCodeCoverage] protected virtual void ClearInternal() => InternalUnsafeSource!.Clear(); @@ -273,6 +328,9 @@ public virtual bool IfNotContains(T item, Action action) public T[] Snapshot() => Sync!.Reading(() => InternalSource.ToArray()); + /// + /// Synchronizes exporting the internal collection to the specified collection. + /// public void Export(ICollection to) => Sync!.Reading(() => to.AddRange(InternalSource)); } diff --git a/source/Synchronized/TrackedDictionaryWrapper.cs b/source/Synchronized/TrackedDictionaryWrapper.cs index 44efb4d..fbf56da 100644 --- a/source/Synchronized/TrackedDictionaryWrapper.cs +++ b/source/Synchronized/TrackedDictionaryWrapper.cs @@ -1,39 +1,53 @@ using Open.Threading; -using System.Collections.Generic; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; namespace Open.Collections.Synchronized; +/// +/// A synchronized dictionary that can be tracked for changes. +/// public class TrackedDictionaryWrapper : TrackedCollectionWrapper, TDictionary>, IDictionary + where TKey : notnull where TDictionary : class, IDictionary { + /// + /// Construct a new instance with the provide dictionary and optional synchronizer. + /// [ExcludeFromCodeCoverage] public TrackedDictionaryWrapper(TDictionary dictionary, ModificationSynchronizer? sync = null) : base(dictionary, sync) { } + /// + /// Construct a new instance with the provide dictionary and a new synchronizer. + /// [ExcludeFromCodeCoverage] public TrackedDictionaryWrapper(TDictionary dictionary, out ModificationSynchronizer sync) : base(dictionary, out sync) { } + /// [ExcludeFromCodeCoverage] public ICollection Keys => InternalSource.Keys; + /// [ExcludeFromCodeCoverage] public ICollection Values => InternalSource.Values; /// [ExcludeFromCodeCoverage] - public bool TryGetValue(TKey key, out TValue value) + public bool TryGetValue(TKey key, +#if NET9_0_OR_GREATER + [MaybeNullWhen(false)] +#else +#endif + out TValue value) { - TValue v = default!; + TValue? v = default; bool result = Sync!.Reading(() => InternalSource.TryGetValue(key, out v)); - value = v; + value = v!; return result; } @@ -45,6 +59,7 @@ public TValue this[TKey key] set => SetValue(key, value); } + /// public bool SetValue(TKey key, TValue value) => Sync!.Modifying( AssertIsAliveDelegate, @@ -71,6 +86,7 @@ public bool ContainsKey(TKey key) => Sync!.Reading( () => AssertIsAlive() && InternalSource.ContainsKey(key)); + /// protected virtual int AddSynchronized(TKey key, TValue value) { Sync!.Modifying( @@ -96,7 +112,7 @@ public void Add(TKey key, TValue value) /// public bool Remove(TKey key) { - TValue value = default!; + TValue? value = default; return Sync!.Modifying( () => AssertIsAlive() && InternalSource.TryGetValue(key, out value), @@ -109,20 +125,24 @@ public bool Remove(TKey key) version => { if (HasChangedListeners) // Avoid creating KVP unnecessarily. - OnChanged(ItemChange.Removed, KeyValuePair.Create(key, value), version); + OnChanged(ItemChange.Removed, KeyValuePair.Create(key, value!), version); }); } } +/// public class TrackedDictionaryWrapper : TrackedDictionaryWrapper> + where TKey : notnull { + /// [ExcludeFromCodeCoverage] public TrackedDictionaryWrapper(IDictionary dictionary, ModificationSynchronizer? sync = null) : base(dictionary, sync) { } + /// [ExcludeFromCodeCoverage] public TrackedDictionaryWrapper(IDictionary dictionary, out ModificationSynchronizer sync) : base(dictionary, out sync) @@ -130,33 +150,50 @@ public TrackedDictionaryWrapper(IDictionary dictionary, out Modifi } } +/// public class TrackedDictionary : TrackedDictionaryWrapper + where TKey : notnull { + /// + /// Constructs a new instance with the specified capacity and optional synchronizer. + /// [ExcludeFromCodeCoverage] public TrackedDictionary(int capacity, ModificationSynchronizer? sync = null) : base(new Dictionary(capacity), sync) { } + /// + /// Constructs a new instance with the specified capacity and a new synchronizer. + /// [ExcludeFromCodeCoverage] public TrackedDictionary(int capacity, out ModificationSynchronizer sync) : base(new Dictionary(capacity), out sync) { } + /// + /// Constructs a new instance with an optional synchronizer. + /// [ExcludeFromCodeCoverage] public TrackedDictionary(ModificationSynchronizer? sync = null) : base(new Dictionary(), sync) { } + /// + /// Constructs a new instance with a new synchronizer. + /// [ExcludeFromCodeCoverage] public TrackedDictionary(out ModificationSynchronizer sync) : base(new Dictionary(), out sync) { } + /// + /// Constructs a new instance. + /// [ExcludeFromCodeCoverage] public TrackedDictionary() : this(null) { } } diff --git a/source/Synchronized/TrackedIndexedDictionaryWrapper.cs b/source/Synchronized/TrackedIndexedDictionaryWrapper.cs index e5b31d4..ca06c52 100644 --- a/source/Synchronized/TrackedIndexedDictionaryWrapper.cs +++ b/source/Synchronized/TrackedIndexedDictionaryWrapper.cs @@ -1,12 +1,11 @@ using Open.Threading; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; namespace Open.Collections.Synchronized; /// public class TrackedIndexedDictionaryWrapper : TrackedDictionaryWrapper, IIndexedDictionary + where TKey : notnull where TDictionary : class, IIndexedDictionary { /// @@ -109,6 +108,9 @@ public bool SetValueAt(int index, TValue value, out TKey key) return result; } + /// + /// Synchronizes adding an item to the internal collection. + /// protected override int AddSynchronized(TKey key, TValue value) { int index = -1; @@ -133,8 +135,12 @@ protected override int AddSynchronized(TKey key, TValue value) => AddSynchronized(key, value); } +/// +/// A synchronized wrapper for a dictionary that uses a for synchronization. +/// public class TrackedIndexedDictionaryWrapper : TrackedIndexedDictionaryWrapper> + where TKey : notnull { /// [ExcludeFromCodeCoverage] @@ -151,21 +157,28 @@ public TrackedIndexedDictionaryWrapper(IIndexedDictionary dictiona } } +/// +/// A synchronized dictionary that uses a for synchronization. +/// public sealed class TrackedIndexedDictionary : TrackedIndexedDictionaryWrapper + where TKey : notnull { + /// [ExcludeFromCodeCoverage] public TrackedIndexedDictionary(int capacity, ModificationSynchronizer? sync = null) : base(new IndexedDictionary(capacity), sync) { } + /// [ExcludeFromCodeCoverage] public TrackedIndexedDictionary(int capacity, out ModificationSynchronizer sync) : base(new IndexedDictionary(capacity), out sync) { } + /// [ExcludeFromCodeCoverage] public TrackedIndexedDictionary(ModificationSynchronizer? sync) : base(new IndexedDictionary(), sync) @@ -187,9 +200,11 @@ public TrackedIndexedDictionary() : this(null) /// [ExcludeFromCodeCoverage] - public override TKey GetKeyAt(int index) => InternalSource.GetKeyAt(index); + public override TKey GetKeyAt(int index) + => InternalSource.GetKeyAt(index); /// [ExcludeFromCodeCoverage] - public override TValue GetValueAt(int index) => InternalSource.GetValueAt(index); + public override TValue GetValueAt(int index) + => InternalSource.GetValueAt(index); } diff --git a/source/Synchronized/TrackedListWrapper.cs b/source/Synchronized/TrackedListWrapper.cs index a9be517..8edf60b 100644 --- a/source/Synchronized/TrackedListWrapper.cs +++ b/source/Synchronized/TrackedListWrapper.cs @@ -1,17 +1,23 @@ using Open.Threading; -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; namespace Open.Collections.Synchronized; +/// +/// A synchronized list that tracks changes. +/// public class TrackedListWrapper : TrackedCollectionWrapper>, IList { + /// + /// Constructs a new instance with the specified list and optional modification synchronizer. + /// [ExcludeFromCodeCoverage] public TrackedListWrapper(IList list, ModificationSynchronizer? sync = null) : base(list, sync) { } + /// + /// Constructs a new instance with the specified list and a new modification synchronizer. + /// [ExcludeFromCodeCoverage] public TrackedListWrapper(IList list, out ModificationSynchronizer sync) : base(list, out sync) { @@ -24,6 +30,7 @@ public T this[int index] set => SetValue(index, value); } + /// public bool SetValue(int index, T value) => Sync!.Modifying( AssertIsAliveDelegate, @@ -93,7 +100,7 @@ public T RemoveAt(int index) void IList.RemoveAt(int index) => RemoveAt(index); /// - /// Synchonizes finding an item (), and if found, replaces it with the . + /// Synchronizes finding an item (), and if found, replaces it with the . /// /// If is true and the is not found. public bool Replace(T target, T replacement, bool throwIfNotFound = false) @@ -115,32 +122,50 @@ public bool Replace(T target, T replacement, bool throwIfNotFound = false) } } +/// +/// A synchronized list that tracks changes. +/// public sealed class TrackedList : TrackedListWrapper { + /// + /// Constructs a new instance with the specified initial capacity and optional modification synchronizer. + /// [ExcludeFromCodeCoverage] public TrackedList(int capacity, ModificationSynchronizer? sync = null) : base(new List(capacity), sync) { } + /// + /// Constructs a new instance with the specified initial capacity and a new modification synchronizer. + /// [ExcludeFromCodeCoverage] public TrackedList(int capacity, out ModificationSynchronizer sync) : base(new List(capacity), out sync) { } + /// + /// Constructs a new instance using the provided modification synchronizer. + /// [ExcludeFromCodeCoverage] public TrackedList(ModificationSynchronizer? sync) - : base(new List(), sync) + : base([], sync) { } + /// + /// Constructs a new instance with a new modification synchronizer. + /// [ExcludeFromCodeCoverage] public TrackedList(out ModificationSynchronizer sync) - : base(new List(), out sync) + : base([], out sync) { } + /// + /// Constructs a new instance. + /// [ExcludeFromCodeCoverage] public TrackedList() : this(null) { } } \ No newline at end of file diff --git a/source/_Imports.cs b/source/_Imports.cs new file mode 100644 index 0000000..4002d13 --- /dev/null +++ b/source/_Imports.cs @@ -0,0 +1,4 @@ +global using System.Diagnostics; +global using System.Diagnostics.CodeAnalysis; +global using System.Diagnostics.Contracts; +global using System.Runtime.CompilerServices; diff --git a/testing/Open.Collections.Tests/BasicCollectionTests.cs b/testing/Open.Collections.Tests/BasicCollectionTests.cs index 2c6a99b..fed78bb 100644 --- a/testing/Open.Collections.Tests/BasicCollectionTests.cs +++ b/testing/Open.Collections.Tests/BasicCollectionTests.cs @@ -7,18 +7,14 @@ using Xunit; namespace Open.Collections.Tests; -public abstract class BasicCollectionTests +public abstract class BasicCollectionTests(TCollection collection) where TCollection : ICollection, new() { - protected BasicCollectionTests(TCollection collection) - => Collection = collection; - protected BasicCollectionTests() : this(new()) { } - protected readonly TCollection Collection; + protected readonly TCollection Collection = collection; - [Fact] - public virtual TCollection AssertWhenDisposed() + protected virtual TCollection AssertWhenDisposedCore() { // Policy: // Ideally an exception should throw whenever access occurs after disposal. @@ -40,6 +36,10 @@ public virtual TCollection AssertWhenDisposed() return c; } + [Fact] + public void AssertWhenDisposed() + => AssertWhenDisposedCore(); + protected static void ThrowsDisposed(Action action) => Assert.Throws(action); @@ -131,6 +131,7 @@ public void Contains() Collection.Add(3); search = 2; } + Collection.Contains(search).Should().BeTrue(); if (Collection is not ISynchronizedCollectionWrapper> c) return; diff --git a/testing/Open.Collections.Tests/BasicDictionaryTests.cs b/testing/Open.Collections.Tests/BasicDictionaryTests.cs index 8257f28..ba7c5a8 100644 --- a/testing/Open.Collections.Tests/BasicDictionaryTests.cs +++ b/testing/Open.Collections.Tests/BasicDictionaryTests.cs @@ -5,18 +5,14 @@ using Xunit; namespace Open.Collections.Tests; -public abstract class BasicDictionaryTests +public abstract class BasicDictionaryTests(TDictionary dictionary) where TDictionary : IDictionary, new() { - protected BasicDictionaryTests(TDictionary dictionary) - => Dictionary = dictionary; - protected BasicDictionaryTests() : this(new()) { } - protected readonly TDictionary Dictionary; + protected readonly TDictionary Dictionary = dictionary; - [Fact] - public TDictionary AssertWhenDisposed() + protected TDictionary AssertWhenDisposedCore() { TDictionary d = new(); d.Add(5, 10); @@ -33,6 +29,10 @@ public TDictionary AssertWhenDisposed() return d; } + [Fact] + public void AssertWhenDisposed() + => AssertWhenDisposedCore(); + static void ThrowsDisposed(Action action) => Assert.Throws(action); diff --git a/testing/Open.Collections.Tests/BasicLinkedListTests.cs b/testing/Open.Collections.Tests/BasicLinkedListTests.cs index f7fd98c..92ef919 100644 --- a/testing/Open.Collections.Tests/BasicLinkedListTests.cs +++ b/testing/Open.Collections.Tests/BasicLinkedListTests.cs @@ -3,12 +3,9 @@ using Xunit; namespace Open.Collections.Tests; -public abstract class BasicLinkedListTests : BasicCollectionTests +public abstract class BasicLinkedListTests(TList collection) : BasicCollectionTests(collection) where TList : ILinkedList, new() { - protected BasicLinkedListTests(TList collection) - : base(collection) { } - protected BasicLinkedListTests() : this(new()) { } diff --git a/testing/Open.Collections.Tests/BasicListTests.cs b/testing/Open.Collections.Tests/BasicListTests.cs index d80e120..4b762f2 100644 --- a/testing/Open.Collections.Tests/BasicListTests.cs +++ b/testing/Open.Collections.Tests/BasicListTests.cs @@ -4,17 +4,15 @@ using Xunit; namespace Open.Collections.Tests; -public abstract class BasicListTests - : BasicCollectionTests +public abstract class BasicListTests(TList list) + : BasicCollectionTests(list) where TList : IList, new() { - protected BasicListTests(TList list) : base(list) { } - protected BasicListTests() : this(new()) { } - public override TList AssertWhenDisposed() + protected override TList AssertWhenDisposedCore() { - var list = base.AssertWhenDisposed(); + var list = base.AssertWhenDisposedCore(); if (list is not IDisposable) return list; ThrowsDisposed(() => list.IndexOf(5)); return list; diff --git a/testing/Open.Collections.Tests/Collections/ConcurrentListTests.cs b/testing/Open.Collections.Tests/Collections/ConcurrentListTests.cs index 5ce5ff9..6e21ba4 100644 --- a/testing/Open.Collections.Tests/Collections/ConcurrentListTests.cs +++ b/testing/Open.Collections.Tests/Collections/ConcurrentListTests.cs @@ -1,9 +1,10 @@ using Open.Collections.Synchronized; -namespace Open.Collections.Tests; +namespace Open.Collections.Tests.Collections; + public class ConcurrentListTests : BasicListTests> { - public ConcurrentListTests() : base(new()) + public ConcurrentListTests() : base([]) { } } diff --git a/testing/Open.Collections.Tests/Collections/LockSyncDictionaryTests.cs b/testing/Open.Collections.Tests/Collections/LockSyncDictionaryTests.cs index 9400be5..5777b03 100644 --- a/testing/Open.Collections.Tests/Collections/LockSyncDictionaryTests.cs +++ b/testing/Open.Collections.Tests/Collections/LockSyncDictionaryTests.cs @@ -1,12 +1,9 @@ using Open.Collections.Synchronized; -namespace Open.Collections.Tests; +namespace Open.Collections.Tests.Collections; + public class LockSyncDictionaryTests - : ParallelDictionaryTests> -{ -} + : ParallelDictionaryTests>; public class LockSyncIndexedDictionaryTests - : ParallelDictionaryTests> -{ -} \ No newline at end of file + : ParallelDictionaryTests>; \ No newline at end of file diff --git a/testing/Open.Collections.Tests/Collections/LockSyncLinkedListTests.cs b/testing/Open.Collections.Tests/Collections/LockSyncLinkedListTests.cs index 3251a1f..ae56c87 100644 --- a/testing/Open.Collections.Tests/Collections/LockSyncLinkedListTests.cs +++ b/testing/Open.Collections.Tests/Collections/LockSyncLinkedListTests.cs @@ -1,10 +1,11 @@ using Open.Collections.Synchronized; -namespace Open.Collections.Tests; +namespace Open.Collections.Tests.Collections; + public class LockSyncLinkedListTests : BasicLinkedListTests> { - public LockSyncLinkedListTests() : base(new()) + public LockSyncLinkedListTests() : base([]) { } } diff --git a/testing/Open.Collections.Tests/Collections/LockSyncListTests.cs b/testing/Open.Collections.Tests/Collections/LockSyncListTests.cs index eb60f4b..6b8e1f2 100644 --- a/testing/Open.Collections.Tests/Collections/LockSyncListTests.cs +++ b/testing/Open.Collections.Tests/Collections/LockSyncListTests.cs @@ -1,7 +1,5 @@ using Open.Collections.Synchronized; -namespace Open.Collections.Tests; +namespace Open.Collections.Tests.Collections; public class LockSyncListTests - : ParallelListTests> -{ -} + : ParallelListTests>; diff --git a/testing/Open.Collections.Tests/Collections/OrderedDictionaryTests.cs b/testing/Open.Collections.Tests/Collections/OrderedDictionaryTests.cs index 42e3bb3..ce0138e 100644 --- a/testing/Open.Collections.Tests/Collections/OrderedDictionaryTests.cs +++ b/testing/Open.Collections.Tests/Collections/OrderedDictionaryTests.cs @@ -1,11 +1,9 @@ using FluentAssertions; using Xunit; -namespace Open.Collections.Tests; +namespace Open.Collections.Tests.Collections; -public class OrderedDictionaryTests : OrderedDictionaryTests> -{ -} +public class OrderedDictionaryTests : OrderedDictionaryTests>; public class IndexedDictionaryTests : OrderedDictionaryTests> { diff --git a/testing/Open.Collections.Tests/Collections/ReadWriteSyncLDictionaryTests.cs b/testing/Open.Collections.Tests/Collections/ReadWriteSyncLDictionaryTests.cs index 58a1ce3..cceabf9 100644 --- a/testing/Open.Collections.Tests/Collections/ReadWriteSyncLDictionaryTests.cs +++ b/testing/Open.Collections.Tests/Collections/ReadWriteSyncLDictionaryTests.cs @@ -1,12 +1,8 @@ using Open.Collections.Synchronized; -namespace Open.Collections.Tests; +namespace Open.Collections.Tests.Collections; public class ReadWriteSyncDictionaryTests - : ParallelDictionaryTests> -{ -} + : ParallelDictionaryTests>; public class ReadWriteSyncIndexedDictionaryTests - : ParallelDictionaryTests> -{ -} \ No newline at end of file + : ParallelDictionaryTests>; \ No newline at end of file diff --git a/testing/Open.Collections.Tests/Collections/ReadWriteSyncLinkedListTests.cs b/testing/Open.Collections.Tests/Collections/ReadWriteSyncLinkedListTests.cs index 26e0482..15f85f8 100644 --- a/testing/Open.Collections.Tests/Collections/ReadWriteSyncLinkedListTests.cs +++ b/testing/Open.Collections.Tests/Collections/ReadWriteSyncLinkedListTests.cs @@ -1,10 +1,10 @@ using Open.Collections.Synchronized; -namespace Open.Collections.Tests; +namespace Open.Collections.Tests.Collections; public class ReadWriteSyncLinkedListTests : BasicLinkedListTests> { - public ReadWriteSyncLinkedListTests() : base(new()) + public ReadWriteSyncLinkedListTests() : base([]) { } } diff --git a/testing/Open.Collections.Tests/Collections/ReadWriteSyncListTests.cs b/testing/Open.Collections.Tests/Collections/ReadWriteSyncListTests.cs index 010de79..14c185f 100644 --- a/testing/Open.Collections.Tests/Collections/ReadWriteSyncListTests.cs +++ b/testing/Open.Collections.Tests/Collections/ReadWriteSyncListTests.cs @@ -1,7 +1,5 @@ using Open.Collections.Synchronized; -namespace Open.Collections.Tests; +namespace Open.Collections.Tests.Collections; public class ReadWriteSyncListTests - : ParallelListTests> -{ -} + : ParallelListTests>; diff --git a/testing/Open.Collections.Tests/Collections/TrackedDictionaryTests.cs b/testing/Open.Collections.Tests/Collections/TrackedDictionaryTests.cs index 2c0f137..2699fb9 100644 --- a/testing/Open.Collections.Tests/Collections/TrackedDictionaryTests.cs +++ b/testing/Open.Collections.Tests/Collections/TrackedDictionaryTests.cs @@ -1,12 +1,8 @@ using Open.Collections.Synchronized; -namespace Open.Collections.Tests; +namespace Open.Collections.Tests.Collections; public class TrackedDictionaryTests - : ParallelDictionaryTests> -{ -} + : ParallelDictionaryTests>; public class TrackedIndexedDictionaryTests - : ParallelDictionaryTests> -{ -} \ No newline at end of file + : ParallelDictionaryTests>; \ No newline at end of file diff --git a/testing/Open.Collections.Tests/CombinationTests.cs b/testing/Open.Collections.Tests/CombinationTests.cs index 64ee22e..fea5d6c 100644 --- a/testing/Open.Collections.Tests/CombinationTests.cs +++ b/testing/Open.Collections.Tests/CombinationTests.cs @@ -6,19 +6,19 @@ namespace Open.Collections.Tests; public class CombinationTests { - static readonly ImmutableArray Set1 = ImmutableArray.Create(1, 2); - static readonly ImmutableArray Set2 = ImmutableArray.Create('A', 'C', 'E'); - static readonly ImmutableArray Set3 = ImmutableArray.Create(0, 1); + static readonly ImmutableArray Set1 = [1, 2]; + static readonly ImmutableArray Set2 = ['A', 'C', 'E']; + static readonly ImmutableArray Set3 = [0, 1]; [Fact] public void TestCombination1() { - int[][] expected = new int[][] { - new int[] { 1, 1 }, - new int[] { 1, 2 }, - new int[] { 2, 1 }, - new int[] { 2, 2 }, - }; + int[][] expected = [ + [1, 1], + [1, 2], + [2, 1], + [2, 2], + ]; int[][] actual = Set1.Combinations().ToArray(); Assert.Equal(expected, actual); } @@ -26,11 +26,11 @@ public void TestCombination1() [Fact] public void TestCombination1Distinct() { - int[][] expected = new int[][] { - new int[] { 1, 1 }, - new int[] { 1, 2 }, - new int[] { 2, 2 }, - }; + int[][] expected = [ + [1, 1], + [1, 2], + [2, 2], + ]; int[][] actual = Set1.CombinationsDistinct().ToArray(); Assert.Equal(expected, actual); } @@ -38,17 +38,17 @@ public void TestCombination1Distinct() [Fact] public void TestCombination2() { - char[][] expected = new char[][] { - new char[] { 'A', 'A' }, - new char[] { 'A', 'C' }, - new char[] { 'A', 'E' }, - new char[] { 'C', 'A' }, - new char[] { 'C', 'C' }, - new char[] { 'C', 'E' }, - new char[] { 'E', 'A' }, - new char[] { 'E', 'C' }, - new char[] { 'E', 'E' }, - }; + char[][] expected = [ + ['A', 'A'], + ['A', 'C'], + ['A', 'E'], + ['C', 'A'], + ['C', 'C'], + ['C', 'E'], + ['E', 'A'], + ['E', 'C'], + ['E', 'E'], + ]; char[][] actual = Set2.Combinations(2).ToArray(); Assert.Equal(expected, actual); } @@ -56,14 +56,14 @@ public void TestCombination2() [Fact] public void TestCombination2Distinct() { - char[][] expected = new char[][] { - new char[] { 'A', 'A' }, - new char[] { 'A', 'C' }, - new char[] { 'A', 'E' }, - new char[] { 'C', 'C' }, - new char[] { 'C', 'E' }, - new char[] { 'E', 'E' }, - }; + char[][] expected = [ + ['A', 'A'], + ['A', 'C'], + ['A', 'E'], + ['C', 'C'], + ['C', 'E'], + ['E', 'E'], + ]; char[][] actual = Set2.CombinationsDistinct(2).ToArray(); Assert.Equal(expected, actual); } @@ -71,24 +71,24 @@ public void TestCombination2Distinct() [Fact] public void TestCombination3() { - int[][] expected = new int[][] { - new int[] { 0, 0, 0, 0 }, - new int[] { 0, 0, 0, 1 }, - new int[] { 0, 0, 1, 0 }, - new int[] { 0, 0, 1, 1 }, - new int[] { 0, 1, 0, 0 }, - new int[] { 0, 1, 0, 1 }, - new int[] { 0, 1, 1, 0 }, - new int[] { 0, 1, 1, 1 }, - new int[] { 1, 0, 0, 0 }, - new int[] { 1, 0, 0, 1 }, - new int[] { 1, 0, 1, 0 }, - new int[] { 1, 0, 1, 1 }, - new int[] { 1, 1, 0, 0 }, - new int[] { 1, 1, 0, 1 }, - new int[] { 1, 1, 1, 0 }, - new int[] { 1, 1, 1, 1 }, - }; + int[][] expected = [ + [0, 0, 0, 0], + [0, 0, 0, 1], + [0, 0, 1, 0], + [0, 0, 1, 1], + [0, 1, 0, 0], + [0, 1, 0, 1], + [0, 1, 1, 0], + [0, 1, 1, 1], + [1, 0, 0, 0], + [1, 0, 0, 1], + [1, 0, 1, 0], + [1, 0, 1, 1], + [1, 1, 0, 0], + [1, 1, 0, 1], + [1, 1, 1, 0], + [1, 1, 1, 1], + ]; int[][] actual = Set3.Combinations(4).ToArray(); Assert.Equal(expected, actual); } diff --git a/testing/Open.Collections.Tests/Open.Collections.Tests.csproj b/testing/Open.Collections.Tests/Open.Collections.Tests.csproj index 0b54a3a..7e8c728 100644 --- a/testing/Open.Collections.Tests/Open.Collections.Tests.csproj +++ b/testing/Open.Collections.Tests/Open.Collections.Tests.csproj @@ -1,9 +1,10 @@  - net6.0 + net9.0 false + IDE0305;IDE0301; @@ -11,18 +12,18 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/testing/Open.Collections.Tests/OrderedDictionaryTests.cs b/testing/Open.Collections.Tests/OrderedDictionaryTests.cs index d5a6915..cfc4bb3 100644 --- a/testing/Open.Collections.Tests/OrderedDictionaryTests.cs +++ b/testing/Open.Collections.Tests/OrderedDictionaryTests.cs @@ -4,12 +4,9 @@ using Xunit; namespace Open.Collections.Tests; -public abstract class OrderedDictionaryTests : BasicDictionaryTests +public abstract class OrderedDictionaryTests(TDictionary dictionary) : BasicDictionaryTests(dictionary) where TDictionary : IDictionary, new() { - protected OrderedDictionaryTests(TDictionary dictionary) - : base(dictionary) { } - protected OrderedDictionaryTests() : this(new()) { } diff --git a/testing/Open.Collections.Tests/ParallelDictionaryTests.cs b/testing/Open.Collections.Tests/ParallelDictionaryTests.cs index fb438bb..986ccf3 100644 --- a/testing/Open.Collections.Tests/ParallelDictionaryTests.cs +++ b/testing/Open.Collections.Tests/ParallelDictionaryTests.cs @@ -4,13 +4,10 @@ using Xunit; namespace Open.Collections.Tests; -public abstract class ParallelDictionaryTests - : BasicDictionaryTests +public abstract class ParallelDictionaryTests(TDictionary dictionary) + : BasicDictionaryTests(dictionary) where TDictionary : IDictionary, new() { - protected ParallelDictionaryTests(TDictionary dictionary) - : base(dictionary) { } - protected ParallelDictionaryTests() : this(new()) { } diff --git a/testing/Open.Collections.Tests/ParallelListTests.cs b/testing/Open.Collections.Tests/ParallelListTests.cs index db9d8a3..81955af 100644 --- a/testing/Open.Collections.Tests/ParallelListTests.cs +++ b/testing/Open.Collections.Tests/ParallelListTests.cs @@ -4,13 +4,11 @@ using Xunit; namespace Open.Collections.Tests; -public abstract class ParallelListTests - : BasicListTests +public abstract class ParallelListTests( + TList list) + : BasicListTests(list) where TList : IList, new() { - protected ParallelListTests(TList list) - : base(list) { } - protected ParallelListTests() : this(new()) { } diff --git a/testing/Open.Collections.Tests/PermutationTests.cs b/testing/Open.Collections.Tests/PermutationTests.cs index e910c7c..1b5d684 100644 --- a/testing/Open.Collections.Tests/PermutationTests.cs +++ b/testing/Open.Collections.Tests/PermutationTests.cs @@ -1,4 +1,5 @@ -using System; +using FluentAssertions; +using System; using System.Collections.Immutable; using System.Linq; using Xunit; @@ -7,31 +8,54 @@ namespace Open.Collections.Tests; public class PermutationTests { - static readonly ImmutableArray Set1 = ImmutableArray.Create(1, 2); + static readonly ImmutableArray Set1 = [1, 2]; static readonly ReadOnlyMemory Set2 = new char[] { 'A', 'B', 'C' }; [Fact] - public void TestPermutation1() + public void TestPermutation1a() { - int[][] expected = new int[][] { - new int[] { 1, 2 }, - new int[] { 2, 1 }, - }; + int[][] expected = [ + [1, 2], + [2, 1], + ]; int[][] actual = Set1.Permutations().ToArray(); + actual.Length.Should().Be(2); + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(2)] + [InlineData(3)] + [InlineData(4)] + [InlineData(5)] + [InlineData(6)] + [InlineData(12)] + [InlineData(24)] + public void TestPermutation1b(int bufferLength) + { + int[][] expected = [ + [1, 2], + [2, 1], + ]; + + int[] buffer = new int[bufferLength]; + + int[][] actual = Set1.Permutations(buffer).Select(b => b.ToArray()).ToArray(); + actual.Length.Should().Be(2); Assert.Equal(expected, actual); } [Fact] public void TestPermutation2() { - char[][] expected = new char[][] { - new char[] { 'A', 'B', 'C' }, - new char[] { 'B', 'A', 'C' }, - new char[] { 'C', 'A', 'B' }, - new char[] { 'A', 'C', 'B' }, - new char[] { 'B', 'C', 'A' }, - new char[] { 'C', 'B', 'A' }, - }; + char[][] expected = [ + ['A', 'B', 'C'], + ['B', 'A', 'C'], + ['C', 'A', 'B'], + ['A', 'C', 'B'], + ['B', 'C', 'A'], + ['C', 'B', 'A'], + ]; char[][] actual = Set2.Permutations().ToArray(); Assert.Equal(expected, actual); } diff --git a/testing/Open.Collections.Tests/PermutorTests.cs b/testing/Open.Collections.Tests/PermutorTests.cs new file mode 100644 index 0000000..804aea7 --- /dev/null +++ b/testing/Open.Collections.Tests/PermutorTests.cs @@ -0,0 +1,137 @@ +using FluentAssertions; +using System; +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace Open.Collections.Tests; + +public class PermutorTests +{ + [Fact] + public void TestNoDuplicatePermutations() + { + int[] numbers = [1, 2, 3, 4]; + var permutations = numbers.Permutations().Select(p => string.Join(",", p.Span.ToArray())).ToHashSet(); + + int expectedCount = Factorial(numbers.Length); + Assert.Equal(expectedCount, permutations.Count); + } + + [Fact] + public void TestSpecificPermutation() + { + int[] numbers = [1, 2, 3]; + var permutations = numbers.AsMemory().Permutations().Select(m => m.ToArray()).ToList(); + permutations.Count.Should().Be(6); + int[] expectedPermutation = [2, 1, 3]; + Assert.Contains(expectedPermutation, permutations); + } + + [Fact] + public void TestExactPermutationsFor123() + { + int[] numbers = [1, 2, 3]; + var expectedPermutations = new List + { + "1,2,3", + "2,1,3", + "3,1,2", + "1,3,2", + "2,3,1", + "3,2,1" + }; + + var permutations = numbers.AsMemory().Permutations() + .Select(p => string.Join(",", p.Span.ToArray())) + .ToList(); + + Assert.Equal(expectedPermutations.Count, permutations.Count); + foreach (string perm in expectedPermutations) + { + Assert.Contains(perm, permutations); + } + } + + [Fact] + public void TestStableLexicographicOrder() + { + int[] original = [1, 2, 3]; + Span span = original.AsSpan(); + + var permutations = new List(); + do + { + permutations.Add(string.Join(",", span.ToArray())); + } + while (span.NextLexicographic()); + + var expectedPermutations = new List + { + "1,2,3", + "1,3,2", + "2,1,3", + "2,3,1", + "3,1,2", + "3,2,1" + }; + + Assert.Equal(expectedPermutations, permutations); + } + + [Fact] + public void TestStableHeapsAlgorithmOrder() + { + int[] original = [1, 2, 3]; + var permutations = new List(); + foreach (var s in original.Permutations()) + permutations.Add(string.Join(",", s.ToArray())); + + var expectedPermutations = new List + { + "1,2,3", + "2,1,3", + "3,1,2", + "1,3,2", + "2,3,1", + "3,2,1" + }; + + Assert.Equal(expectedPermutations, permutations); + } + + [Fact] + public void TestStableIndexedOrder() + { + int[] original = [1, 2, 3]; + var permutations = new List(); + for (int i = 0; i < 6; ++i) + { + string s = string.Join(",", original.ToArray().AsSpan().Permutation(i).ToArray()); + int c = permutations.IndexOf(s); + Assert.True(c == -1, $"{s} already exists in the set at index [{c}] of {permutations.Count}."); + permutations.Contains(s).Should().BeFalse(s + " "); + permutations.Add(s); + } + + var expectedPermutations = new List + { + "1,2,3", + "3,1,2", + "3,2,1", + "2,3,1", + "1,3,2", + "2,1,3", + }; + + Assert.Equal(expectedPermutations, permutations); + } + + private static int Factorial(int n) + { + int result = 1; + for (int i = 2; i <= n; i++) + result *= i; + return result; + } +} diff --git a/testing/Open.Collections.Tests/SubsetTests.cs b/testing/Open.Collections.Tests/SubsetTests.cs index 5a74c0e..a276346 100644 --- a/testing/Open.Collections.Tests/SubsetTests.cs +++ b/testing/Open.Collections.Tests/SubsetTests.cs @@ -1,3 +1,4 @@ +using System; using System.Buffers; using System.Collections.Immutable; using System.Linq; @@ -7,23 +8,26 @@ namespace Open.Collections.Tests; public class SubsetTests { - static readonly ImmutableArray Set1 = ImmutableArray.Create(1, 2, 3); - static readonly ImmutableArray Set2 = ImmutableArray.Create('A', 'C', 'E'); - static readonly ImmutableArray Set3 = ImmutableArray.Create('A', 'B', 'C', 'D'); + static readonly ImmutableArray Set1 = [1, 2, 3]; + static readonly ImmutableArray Set2 = ['A', 'C', 'E']; + static readonly ImmutableArray Set3 = ['A', 'B', 'C', 'D']; static readonly ImmutableArray Set4 = Enumerable.Range(1, 5).ToImmutableArray(); static readonly ImmutableArray Set5 = Enumerable.Range(1, 11).ToImmutableArray(); [Fact] public void TestSubset1_2() { - int[][] expected = new int[][] { - new int[] { 1, 2 }, - new int[] { 1, 3 }, - new int[] { 2, 3 }, - }; + int[][] expected = [ + [1, 2], + [1, 3], + [2, 3], + ]; int[][] actual = Set1.Subsets(2).ToArray(); Assert.Equal(expected, actual); + ReadOnlyMemory mem = Set1.ToArray(); + Assert.Equal(expected, mem.Subsets(2).ToArray()); + int[][] progressive = Set1.SubsetsProgressive(2).ToArray(); Assert.Equal(expected, progressive); } @@ -31,11 +35,11 @@ public void TestSubset1_2() [Fact] public void TestSubset2_2() { - char[][] expected = new char[][] { - new char[] { 'A', 'C' }, - new char[] { 'A', 'E' }, - new char[] { 'C', 'E' }, - }; + char[][] expected = [ + ['A', 'C'], + ['A', 'E'], + ['C', 'E'], + ]; char[][] actual = Set2.Subsets(2).ToArray(); Assert.Equal(expected, actual); @@ -46,14 +50,14 @@ public void TestSubset2_2() [Fact] public void TestSubset3_2() { - char[][] expected = new char[][] { - new char[] { 'A', 'B' }, - new char[] { 'A', 'C' }, - new char[] { 'B', 'C' }, - new char[] { 'A', 'D' }, - new char[] { 'B', 'D' }, - new char[] { 'C', 'D' }, - }; + char[][] expected = [ + ['A', 'B'], + ['A', 'C'], + ['B', 'C'], + ['A', 'D'], + ['B', 'D'], + ['C', 'D'], + ]; Assert.Equal(expected.Length, Set3.Subsets(2).Count()); char[][] progressive = Set3.SubsetsProgressive(2).ToArray(); @@ -63,12 +67,12 @@ public void TestSubset3_2() [Fact] public void TestSubset3_3() { - char[][] expected = new char[][] { - new char[] { 'A', 'B', 'C' }, - new char[] { 'A', 'B', 'D' }, - new char[] { 'A', 'C', 'D' }, - new char[] { 'B', 'C', 'D' }, - }; + char[][] expected = [ + ['A', 'B', 'C'], + ['A', 'B', 'D'], + ['A', 'C', 'D'], + ['B', 'C', 'D'], + ]; char[][] actual = Set3.Subsets(3).ToArray(); Assert.Equal(expected, actual); @@ -96,13 +100,13 @@ static T[] Selector(ArrayPoolSegment e) [Fact] public void TestSubset4_4() { - int[][] expected = new int[][] { - new int[] { 1, 2, 3, 4 }, - new int[] { 1, 2, 3, 5 }, - new int[] { 1, 2, 4, 5 }, - new int[] { 1, 3, 4, 5 }, - new int[] { 2, 3, 4, 5 }, - }; + int[][] expected = [ + [1, 2, 3, 4], + [1, 2, 3, 5], + [1, 2, 4, 5], + [1, 3, 4, 5], + [2, 3, 4, 5], + ]; int[][] actual = Set4.Subsets(4).ToArray(); Assert.Equal(expected, actual); @@ -123,18 +127,18 @@ public void TestSubset4_4() [Fact] public void TestSubset4_3() { - int[][] expected = new int[][] { - new int[] { 1, 2, 3 }, - new int[] { 1, 2, 4 }, - new int[] { 1, 3, 4 }, - new int[] { 2, 3, 4 }, - new int[] { 1, 2, 5 }, - new int[] { 1, 3, 5 }, - new int[] { 1, 4, 5 }, - new int[] { 2, 3, 5 }, - new int[] { 2, 4, 5 }, - new int[] { 3, 4, 5 }, - }; + int[][] expected = [ + [1, 2, 3], + [1, 2, 4], + [1, 3, 4], + [2, 3, 4], + [1, 2, 5], + [1, 3, 5], + [1, 4, 5], + [2, 3, 5], + [2, 4, 5], + [3, 4, 5], + ]; int[][] actual = Set4.SubsetsProgressive(3).ToArray(); Assert.Equal(expected, actual); diff --git a/testing/Open.Collections.Tests/TrieTests.cs b/testing/Open.Collections.Tests/TrieTests.cs index fd9f5fd..ad2da83 100644 --- a/testing/Open.Collections.Tests/TrieTests.cs +++ b/testing/Open.Collections.Tests/TrieTests.cs @@ -4,15 +4,15 @@ namespace Open.Collections.Tests; public static class TrieTests { - static readonly string[] Examples = new[] - { + static readonly string[] Examples = + [ "", "abcd", "dcba", "abcdef", "the brown fox", "xxx" - }; + ]; [Fact] public static void TrieValidate()