diff --git a/src/libraries/System.Collections/ref/System.Collections.cs b/src/libraries/System.Collections/ref/System.Collections.cs index 405eea4de0a4a3..5409b03b22c5ad 100644 --- a/src/libraries/System.Collections/ref/System.Collections.cs +++ b/src/libraries/System.Collections/ref/System.Collections.cs @@ -378,6 +378,51 @@ public void Dispose() { } void System.Collections.IEnumerator.Reset() { } } } + + public partial class PriorityQueue + { + public PriorityQueue() { } + public PriorityQueue(System.Collections.Generic.IComparer? comparer) { } + public PriorityQueue(System.Collections.Generic.IEnumerable<(TElement element, TPriority priority)> items) { } + public PriorityQueue(System.Collections.Generic.IEnumerable<(TElement element, TPriority priority)> items, System.Collections.Generic.IComparer? comparer) { } + public PriorityQueue(int initialCapacity) { } + public PriorityQueue(int initialCapacity, System.Collections.Generic.IComparer? comparer) { } + public System.Collections.Generic.IComparer Comparer { get { throw null; } } + public int Count { get { throw null; } } + public System.Collections.Generic.PriorityQueue.UnorderedItemsCollection UnorderedItems { get { throw null; } } + public void Clear() { } + public TElement Dequeue() { throw null; } + public void Enqueue(TElement element, TPriority priority) { } + public TElement EnqueueDequeue(TElement element, TPriority priority) { throw null; } + public void EnqueueRange(System.Collections.Generic.IEnumerable<(TElement element, TPriority priority)> items) { } + public void EnqueueRange(System.Collections.Generic.IEnumerable elements, TPriority priority) { } + public int EnsureCapacity(int capacity) { throw null; } + public TElement Peek() { throw null; } + public void TrimExcess() { } + public bool TryDequeue([System.Diagnostics.CodeAnalysis.MaybeNullWhenAttribute(false)] out TElement element, [System.Diagnostics.CodeAnalysis.MaybeNullWhenAttribute(false)] out TPriority priority) { throw null; } + public bool TryPeek([System.Diagnostics.CodeAnalysis.MaybeNullWhenAttribute(false)] out TElement element, [System.Diagnostics.CodeAnalysis.MaybeNullWhenAttribute(false)] out TPriority priority) { throw null; } + public sealed partial class UnorderedItemsCollection : System.Collections.Generic.IEnumerable<(TElement element, TPriority priority)>, System.Collections.Generic.IReadOnlyCollection<(TElement element, TPriority priority)>, System.Collections.ICollection, System.Collections.IEnumerable + { + internal UnorderedItemsCollection(PriorityQueue queue) { } + public int Count { get { throw null; } } + bool System.Collections.ICollection.IsSynchronized { get { throw null; } } + object System.Collections.ICollection.SyncRoot { get { throw null; } } + void ICollection.CopyTo(System.Array array, int index) { } + public System.Collections.Generic.PriorityQueue.UnorderedItemsCollection.Enumerator GetEnumerator() { throw null; } + System.Collections.Generic.IEnumerator<(TElement element, TPriority priority)> System.Collections.Generic.IEnumerable<(TElement element, TPriority priority)>.GetEnumerator() { throw null; } + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; } + public partial struct Enumerator : System.Collections.Generic.IEnumerator<(TElement element, TPriority priority)>, System.Collections.IEnumerator, System.IDisposable + { + (TElement element, TPriority priority) IEnumerator<(TElement element, TPriority priority)>.Current { get { throw null; } } + public void Dispose() { } + public bool MoveNext() { throw null; } + public (TElement element, TPriority priority) Current { get { throw null; } } + object System.Collections.IEnumerator.Current { get { throw null; } } + void System.Collections.IEnumerator.Reset() { } + } + } + } + public partial class Queue : System.Collections.Generic.IEnumerable, System.Collections.Generic.IReadOnlyCollection, System.Collections.ICollection, System.Collections.IEnumerable { public Queue() { } diff --git a/src/libraries/System.Collections/src/System.Collections.csproj b/src/libraries/System.Collections/src/System.Collections.csproj index 9f8e4b8bb6295d..c8fd9f78b68ec0 100644 --- a/src/libraries/System.Collections/src/System.Collections.csproj +++ b/src/libraries/System.Collections/src/System.Collections.csproj @@ -13,6 +13,7 @@ + diff --git a/src/libraries/System.Collections/src/System/Collections/Generic/PriorityQueue.cs b/src/libraries/System.Collections/src/System/Collections/Generic/PriorityQueue.cs new file mode 100644 index 00000000000000..80fca47e814ace --- /dev/null +++ b/src/libraries/System.Collections/src/System/Collections/Generic/PriorityQueue.cs @@ -0,0 +1,715 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace System.Collections.Generic +{ + /// + /// Represents a data structure in which each element has an associated priority + /// that determines the order in which the pair is dequeued. + /// + /// The type of the element. + /// The type of the priority. + public class PriorityQueue + { + /// + /// Represents an implicit heap-ordered complete d-ary tree, stored as an array. + /// + private (TElement Element, TPriority Priority)[] _nodes; + + private UnorderedItemsCollection? _unorderedItems; + + /// + /// The number of nodes in the heap. + /// + private int _size; + + /// + /// Version updated on mutation to help validate enumerators operate on a consistent state. + /// + private int _version; + + /// + /// When the underlying buffer for the heap nodes grows to accomodate more nodes, + /// this is the minimum the capacity will grow by. + /// + private const int MinimumElementsToGrowBy = 4; + + /// + /// The index at which the heap root is maintained. + /// + private const int RootIndex = 0; + + /// + /// Specifies the arity of the d-ary heap, which here is quaternary. + /// + private const int Arity = 4; + + /// + /// The binary logarithm of . + /// + private const int Log2Arity = 2; + + /// + /// Creates an empty priority queue. + /// + public PriorityQueue() + : this(initialCapacity: 0, comparer: null) + { + } + + /// + /// Creates an empty priority queue with the specified initial capacity for its underlying array. + /// + public PriorityQueue(int initialCapacity) + : this(initialCapacity, comparer: null) + { + } + + /// + /// Creates an empty priority queue with the specified priority comparer. + /// + public PriorityQueue(IComparer? comparer) + : this(initialCapacity: 0, comparer) + { + } + + /// + /// Creates an empty priority queue with the specified priority comparer and + /// the specified initial capacity for its underlying array. + /// + public PriorityQueue(int initialCapacity, IComparer? comparer) + { + if (initialCapacity < 0) + { + throw new ArgumentOutOfRangeException( + nameof(initialCapacity), initialCapacity, SR.ArgumentOutOfRange_NeedNonNegNum); + } + + _nodes = (initialCapacity == 0) + ? Array.Empty<(TElement, TPriority)>() + : new (TElement, TPriority)[initialCapacity]; + + Comparer = comparer ?? Comparer.Default; + } + + /// + /// Creates a priority queue populated with the specified elements and priorities. + /// + public PriorityQueue(IEnumerable<(TElement element, TPriority priority)> items) + : this(items, comparer: null) + { + } + + /// + /// Creates a priority queue populated with the specified elements and priorities, + /// and with the specified priority comparer. + /// + public PriorityQueue(IEnumerable<(TElement element, TPriority priority)> items, IComparer? comparer) + { + if (items is null) + { + throw new ArgumentNullException(nameof(items)); + } + + _nodes = EnumerableHelpers.ToArray(items, out _size); + Comparer = comparer ?? Comparer.Default; + + if (_size > 1) + { + Heapify(); + } + } + + /// + /// Gets the current amount of items in the priority queue. + /// + public int Count => _size; + + /// + /// Gets the priority comparer of the priority queue. + /// + public IComparer Comparer { get; } + + /// + /// Gets a collection that enumerates the elements of the queue. + /// + public UnorderedItemsCollection UnorderedItems => _unorderedItems ??= new UnorderedItemsCollection(this); + + /// + /// Enqueues the specified element and associates it with the specified priority. + /// + public void Enqueue(TElement element, TPriority priority) + { + EnsureEnoughCapacityBeforeAddingNode(); + + // Virtually add the node at the end of the underlying array. + // Note that the node being enqueued does not need to be physically placed + // there at this point, as such an assignment would be redundant. + _size++; + _version++; + + // Restore the heap order + int lastNodeIndex = GetLastNodeIndex(); + MoveUp((element, priority), lastNodeIndex); + } + + /// + /// Gets the element associated with the minimal priority. + /// + /// The queue is empty. + public TElement Peek() + { + if (_size == 0) + { + throw new InvalidOperationException(SR.InvalidOperation_EmptyQueue); + } + + return _nodes[RootIndex].Element; + } + + /// + /// Dequeues the element associated with the minimal priority. + /// + /// The queue is empty. + public TElement Dequeue() + { + if (_size == 0) + { + throw new InvalidOperationException(SR.InvalidOperation_EmptyQueue); + } + + TElement element = _nodes[RootIndex].Element; + Remove(RootIndex); + return element; + } + + /// + /// Dequeues the element associated with the minimal priority + /// + /// + /// if the priority queue is non-empty; otherwise. + /// + public bool TryDequeue([MaybeNullWhen(false)] out TElement element, [MaybeNullWhen(false)] out TPriority priority) + { + if (_size != 0) + { + (element, priority) = _nodes[RootIndex]; + Remove(RootIndex); + return true; + } + + element = default; + priority = default; + return false; + } + + /// + /// Gets the element associated with the minimal priority. + /// + /// + /// if the priority queue is non-empty; otherwise. + /// + public bool TryPeek([MaybeNullWhen(false)] out TElement element, [MaybeNullWhen(false)] out TPriority priority) + { + if (_size != 0) + { + (element, priority) = _nodes[RootIndex]; + return true; + } + + element = default; + priority = default; + return false; + } + + /// + /// Combined enqueue/dequeue operation, generally more efficient than sequential Enqueue/Dequeue calls. + /// + public TElement EnqueueDequeue(TElement element, TPriority priority) + { + (TElement Element, TPriority Priority) root = _nodes[RootIndex]; + + if (Comparer.Compare(priority, root.Priority) <= 0) + { + return element; + } + else + { + (TElement Element, TPriority Priority) newRoot = (element, priority); + _nodes[RootIndex] = newRoot; + + MoveDown(newRoot, RootIndex); + _version++; + + return root.Element; + } + } + + /// + /// Enqueues a collection of element/priority pairs. + /// + public void EnqueueRange(IEnumerable<(TElement Element, TPriority Priority)> items) + { + if (items is null) + { + throw new ArgumentNullException(nameof(items)); + } + + if (_size == 0) + { + _nodes = EnumerableHelpers.ToArray(items, out _size); + + if (_size > 1) + { + Heapify(); + } + } + else + { + foreach ((TElement element, TPriority priority) in items) + { + Enqueue(element, priority); + } + } + } + + /// + /// Enqueues a collection of elements, each associated with the specified priority. + /// + public void EnqueueRange(IEnumerable elements, TPriority priority) + { + if (elements is null) + { + throw new ArgumentNullException(nameof(elements)); + } + + if (_size == 0) + { + using (IEnumerator enumerator = elements.GetEnumerator()) + { + if (enumerator.MoveNext()) + { + _nodes = new (TElement, TPriority)[MinimumElementsToGrowBy]; + _nodes[0] = (enumerator.Current, priority); + _size = 1; + + while (enumerator.MoveNext()) + { + EnsureEnoughCapacityBeforeAddingNode(); + _nodes[_size++] = (enumerator.Current, priority); + } + + if (_size > 1) + { + Heapify(); + } + } + } + } + else + { + foreach (TElement element in elements) + { + Enqueue(element, priority); + } + } + } + + /// + /// Removes all items from the priority queue. + /// + public void Clear() + { + if (RuntimeHelpers.IsReferenceOrContainsReferences<(TElement, TPriority)>()) + { + // Clear the elements so that the gc can reclaim the references + Array.Clear(_nodes, 0, _size); + } + _size = 0; + _version++; + } + + /// + /// Ensures that the priority queue has the specified capacity + /// and resizes its underlying array if necessary. + /// + public int EnsureCapacity(int capacity) + { + if (capacity < 0) + { + throw new ArgumentOutOfRangeException(nameof(capacity), capacity, SR.ArgumentOutOfRange_NeedNonNegNum); + } + + if (_nodes.Length < capacity) + { + SetCapacity(Math.Max(capacity, ComputeCapacityForNextGrowth())); + } + + return _nodes.Length; + } + + /// + /// Sets the capacity to the actual number of items in the priority queue, + /// if that is less than 90 percent of current capacity. + /// + public void TrimExcess() + { + int threshold = (int)(_nodes.Length * 0.9); + if (_size < threshold) + { + SetCapacity(_size); + } + } + + private void EnsureEnoughCapacityBeforeAddingNode() + { + Debug.Assert(_size <= _nodes.Length); + if (_size == _nodes.Length) + { + SetCapacity(ComputeCapacityForNextGrowth()); + } + } + + private int ComputeCapacityForNextGrowth() + { + const int GrowthFactor = 2; + const int MaxArrayLength = 0X7FEFFFFF; + + int newCapacity = Math.Max(_nodes.Length * GrowthFactor, _nodes.Length + MinimumElementsToGrowBy); + + // Allow the structure to grow to maximum possible capacity (~2G elements) before encountering overflow. + // Note that this check works even when _nodes.Length overflowed thanks to the (uint) cast. + + if ((uint)newCapacity > MaxArrayLength) + { + newCapacity = MaxArrayLength; + } + + return newCapacity; + } + + /// + /// Grows or shrinks the array holding nodes. Capacity must be >= _size. + /// + private void SetCapacity(int capacity) + { + Array.Resize(ref _nodes, capacity); + _version++; + } + + /// + /// Removes the node at the specified index. + /// + private void Remove(int indexOfNodeToRemove) + { + // The idea is to replace the specified node by the very last + // node and shorten the array by one. + + int lastNodeIndex = GetLastNodeIndex(); + (TElement Element, TPriority Priority) lastNode = _nodes[lastNodeIndex]; + _nodes[lastNodeIndex] = default; + _size--; + _version++; + + // In case we wanted to remove the node that was the last one, + // we are done. + + if (indexOfNodeToRemove == lastNodeIndex) + { + return; + } + + // Our last node was erased from the array and needs to be + // inserted again. Of course, we will overwrite the node we + // wanted to remove. After that operation, we will need + // to restore the heap property (in general). + + (TElement Element, TPriority Priority) nodeToRemove = _nodes[indexOfNodeToRemove]; + + int relation = Comparer.Compare(lastNode.Priority, nodeToRemove.Priority); + _nodes[indexOfNodeToRemove] = lastNode; + + if (relation < 0) + { + MoveUp(lastNode, indexOfNodeToRemove); + } + else + { + MoveDown(lastNode, indexOfNodeToRemove); + } + } + + /// + /// Gets the index of the last node in the heap. + /// + private int GetLastNodeIndex() => _size - 1; + + /// + /// Gets the index of an element's parent. + /// + private int GetParentIndex(int index) => (index - 1) >> Log2Arity; + + /// + /// Gets the index of the first child of an element. + /// + private int GetFirstChildIndex(int index) => Arity * index + 1; + + /// + /// Converts an unordered list into a heap. + /// + private void Heapify() + { + // Leaves of the tree are in fact 1-element heaps, for which there + // is no need to correct them. The heap property needs to be restored + // only for higher nodes, starting from the first node that has children. + // It is the parent of the very last element in the array. + + int lastNodeIndex = GetLastNodeIndex(); + int lastParentWithChildren = GetParentIndex(lastNodeIndex); + + for (int index = lastParentWithChildren; index >= 0; --index) + { + MoveDown(_nodes[index], index); + } + } + + /// + /// Moves a node up in the tree to restore heap order. + /// + private void MoveUp((TElement element, TPriority priority) node, int nodeIndex) + { + // Instead of swapping items all the way to the root, we will perform + // a similar optimization as in the insertion sort. + + while (nodeIndex > 0) + { + int parentIndex = GetParentIndex(nodeIndex); + (TElement Element, TPriority Priority) parent = _nodes[parentIndex]; + + if (Comparer.Compare(node.priority, parent.Priority) < 0) + { + _nodes[nodeIndex] = parent; + nodeIndex = parentIndex; + } + else + { + break; + } + } + + _nodes[nodeIndex] = node; + } + + /// + /// Moves a node down in the tree to restore heap order. + /// + private void MoveDown((TElement element, TPriority priority) node, int nodeIndex) + { + // The node to move down will not actually be swapped every time. + // Rather, values on the affected path will be moved up, thus leaving a free spot + // for this value to drop in. Similar optimization as in the insertion sort. + + int i; + while ((i = GetFirstChildIndex(nodeIndex)) < _size) + { + // Check if the current node (pointed by 'nodeIndex') should really be extracted + // first, or maybe one of its children should be extracted earlier. + (TElement Element, TPriority Priority) topChild = _nodes[i]; + int childrenIndexesLimit = Math.Min(i + Arity, _size); + int topChildIndex = i; + + while (++i < childrenIndexesLimit) + { + (TElement Element, TPriority Priority) child = _nodes[i]; + if (Comparer.Compare(child.Priority, topChild.Priority) < 0) + { + topChild = child; + topChildIndex = i; + } + } + + // In case no child needs to be extracted earlier than the current node, + // there is nothing more to do - the right spot was found. + if (Comparer.Compare(node.priority, topChild.Priority) <= 0) + { + break; + } + + // Move the top child up by one node and now investigate the + // node that was considered to be the top child (recursive). + _nodes[nodeIndex] = topChild; + nodeIndex = topChildIndex; + } + + _nodes[nodeIndex] = node; + } + + public sealed class UnorderedItemsCollection : IReadOnlyCollection<(TElement element, TPriority priority)>, ICollection + { + private readonly PriorityQueue _queue; + + internal UnorderedItemsCollection(PriorityQueue queue) + { + _queue = queue; + } + + public int Count => _queue._size; + object ICollection.SyncRoot => this; + bool ICollection.IsSynchronized => false; + + void ICollection.CopyTo(Array array, int index) + { + if (array is null) + { + throw new ArgumentNullException(nameof(array)); + } + + if (array.Rank != 1) + { + throw new ArgumentException(SR.Arg_RankMultiDimNotSupported, nameof(array)); + } + + if (array.GetLowerBound(0) != 0) + { + throw new ArgumentException(SR.Arg_NonZeroLowerBound, nameof(array)); + } + + if (index < 0 || index > array.Length) + { + throw new ArgumentOutOfRangeException(nameof(index), index, SR.ArgumentOutOfRange_Index); + } + + if (array.Length - index < _queue._size) + { + throw new ArgumentException(SR.Argument_InvalidOffLen); + } + + try + { + Array.Copy(_queue._nodes, 0, array, index, _queue._size); + } + catch (ArrayTypeMismatchException) + { + throw new ArgumentException(SR.Argument_InvalidArrayType, nameof(array)); + } + } + + public struct Enumerator : IEnumerator<(TElement element, TPriority priority)> + { + private readonly PriorityQueue _queue; + private readonly int _version; + + private int _index; + private (TElement element, TPriority priority)? _currentElement; + + private const int FirstCallToEnumerator = -2; + private const int EndOfEnumeration = -1; + + internal Enumerator(PriorityQueue queue) + { + _queue = queue; + _version = queue._version; + _index = FirstCallToEnumerator; + _currentElement = default; + } + + public void Dispose() + { + _index = EndOfEnumeration; + } + + public bool MoveNext() + { + if (_version != _queue._version) + { + throw new InvalidOperationException(SR.InvalidOperation_EnumFailedVersion); + } + + if (_index == FirstCallToEnumerator) + { + if (_queue._size > 0) + { + _index = 0; + _currentElement = _queue._nodes[_index]; + return true; + } + else + { + _index = EndOfEnumeration; + return false; + } + } + + if (_index == EndOfEnumeration) + { + return false; + } + + // advance enumerator + _index++; + + if (_index < _queue._size) + { + _currentElement = _queue._nodes[_index]; + return true; + } + else + { + _index = EndOfEnumeration; + _currentElement = default; + return false; + } + } + + public (TElement element, TPriority priority) Current + { + get + { + if (_index < 0) + { + ThrowEnumerationNotStartedOrEnded(); + } + return _currentElement!.Value; + } + } + + private void ThrowEnumerationNotStartedOrEnded() + { + Debug.Assert(_index == FirstCallToEnumerator || _index == EndOfEnumeration); + + string message = _index == FirstCallToEnumerator + ? SR.InvalidOperation_EnumNotStarted + : SR.InvalidOperation_EnumEnded; + + throw new InvalidOperationException(message); + } + + object IEnumerator.Current => Current; + + void IEnumerator.Reset() + { + if (_version != _queue._version) + { + throw new InvalidOperationException(SR.InvalidOperation_EnumFailedVersion); + } + + _index = FirstCallToEnumerator; + _currentElement = default; + } + } + + public Enumerator GetEnumerator() + => new Enumerator(_queue); + + IEnumerator<(TElement element, TPriority priority)> IEnumerable<(TElement element, TPriority priority)>.GetEnumerator() + => GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + } + } +} diff --git a/src/libraries/System.Collections/tests/Generic/PriorityQueue/PriorityQueue.Generic.Tests.cs b/src/libraries/System.Collections/tests/Generic/PriorityQueue/PriorityQueue.Generic.Tests.cs new file mode 100644 index 00000000000000..7100b9fdb9fe50 --- /dev/null +++ b/src/libraries/System.Collections/tests/Generic/PriorityQueue/PriorityQueue.Generic.Tests.cs @@ -0,0 +1,356 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Xunit; + +namespace System.Collections.Tests +{ + public abstract class PriorityQueue_Generic_Tests : TestBase<(TElement, TPriority)> + { + #region Helper methods + + protected IEnumerable<(TElement, TPriority)> GenericIEnumerableFactory(int count) + { + const int MagicValue = 34; + int seed = count * MagicValue; + for (int i = 0; i < count; i++) + { + yield return CreateT(seed++); + } + } + + protected PriorityQueue GenericPriorityQueueFactory( + int initialCapacity, int countOfItemsToGenerate, out List<(TElement element, TPriority priority)> generatedItems) + { + generatedItems = this.GenericIEnumerableFactory(countOfItemsToGenerate).ToList(); + + var queue = new PriorityQueue(initialCapacity); + foreach (var (element, priority) in generatedItems) + { + queue.Enqueue(element, priority); + } + + return queue; + } + + #endregion + + #region Constructors + + [Fact] + public void PriorityQueue_Generic_Constructor() + { + var queue = new PriorityQueue(); + + Assert.Equal(expected: 0, queue.Count); + Assert.Empty(queue.UnorderedItems); + Assert.Equal(queue.Comparer, Comparer.Default); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void PriorityQueue_Generic_Constructor_int(int initialCapacity) + { + var queue = new PriorityQueue(initialCapacity); + + Assert.Empty(queue.UnorderedItems); + } + + [Fact] + public void PriorityQueue_Generic_Constructor_int_Negative_ThrowsArgumentOutOfRangeException() + { + AssertExtensions.Throws("initialCapacity", () => new PriorityQueue(-1)); + AssertExtensions.Throws("initialCapacity", () => new PriorityQueue(int.MinValue)); + } + + [Fact] + public void PriorityQueue_Generic_Constructor_IComparer() + { + IComparer comparer = Comparer.Default; + var queue = new PriorityQueue(comparer); + + Assert.Equal(comparer, queue.Comparer); + } + + [Fact] + public void PriorityQueue_Generic_Constructor_IComparer_Null() + { + var queue = new PriorityQueue((IComparer)null); + Assert.Equal(Comparer.Default, queue.Comparer); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void PriorityQueue_Generic_Constructor_int_IComparer(int initialCapacity) + { + IComparer comparer = Comparer.Default; + var queue = new PriorityQueue(initialCapacity); + + Assert.Empty(queue.UnorderedItems); + Assert.Equal(comparer, queue.Comparer); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void PriorityQueue_Generic_Constructor_IEnumerable(int count) + { + HashSet<(TElement, TPriority)> itemsToEnqueue = this.GenericIEnumerableFactory(count).ToHashSet(); + PriorityQueue queue = new PriorityQueue(itemsToEnqueue); + Assert.True(itemsToEnqueue.SetEquals(queue.UnorderedItems)); + } + + #endregion + + #region Enqueue, Dequeue, Peek + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void PriorityQueue_Generic_Enqueue(int count) + { + PriorityQueue queue = GenericPriorityQueueFactory(count, count, out var generatedItems); + HashSet<(TElement, TPriority)> expectedItems = generatedItems.ToHashSet(); + + Assert.Equal(count, queue.Count); + var actualItems = queue.UnorderedItems.ToArray(); + Assert.True(expectedItems.SetEquals(actualItems)); + } + + [Fact] + public void PriorityQueue_Generic_Dequeue_EmptyCollection() + { + var queue = new PriorityQueue(); + + Assert.False(queue.TryDequeue(out _, out _)); + Assert.Throws(() => queue.Dequeue()); + } + + [Fact] + public void PriorityQueue_Generic_Peek_EmptyCollection() + { + var queue = new PriorityQueue(); + + Assert.False(queue.TryPeek(out _, out _)); + Assert.Throws(() => queue.Peek()); + } + + [Theory] + [MemberData(nameof(ValidPositiveCollectionSizes))] + public void PriorityQueue_Generic_Peek_PositiveCount(int count) + { + IReadOnlyCollection<(TElement, TPriority)> itemsToEnqueue = this.GenericIEnumerableFactory(count).ToArray(); + (TElement element, TPriority priority) expectedPeek = itemsToEnqueue.First(); + PriorityQueue queue = new PriorityQueue(); + + foreach (var (element, priority) in itemsToEnqueue) + { + if (queue.Comparer.Compare(priority, expectedPeek.priority) < 0) + { + expectedPeek = (element, priority); + } + + queue.Enqueue(element, priority); + + var actualPeekElement = queue.Peek(); + var actualTryPeekSuccess = queue.TryPeek(out TElement actualTryPeekElement, out TPriority actualTryPeekPriority); + + Assert.Equal(expectedPeek.element, actualPeekElement); + Assert.True(actualTryPeekSuccess); + Assert.Equal(expectedPeek.element, actualTryPeekElement); + Assert.Equal(expectedPeek.priority, actualTryPeekPriority); + } + } + + [Theory] + [InlineData(0, 5)] + [InlineData(1, 1)] + [InlineData(3, 100)] + public void PriorityQueue_Generic_PeekAndDequeue(int initialCapacity, int count) + { + PriorityQueue queue = this.GenericPriorityQueueFactory(initialCapacity, count, out var generatedItems); + + var expectedPeekPriorities = generatedItems + .Select(x => x.priority) + .OrderBy(x => x, queue.Comparer) + .ToArray(); + + for (var i = 0; i < count; ++i) + { + var expectedPeekPriority = expectedPeekPriorities[i]; + + var actualTryPeekSuccess = queue.TryPeek(out TElement actualTryPeekElement, out TPriority actualTryPeekPriority); + var actualTryDequeueSuccess = queue.TryDequeue(out TElement actualTryDequeueElement, out TPriority actualTryDequeuePriority); + + Assert.True(actualTryPeekSuccess); + Assert.True(actualTryDequeueSuccess); + Assert.Equal(expectedPeekPriority, actualTryPeekPriority); + Assert.Equal(expectedPeekPriority, actualTryDequeuePriority); + } + + Assert.Equal(expected: 0, queue.Count); + Assert.False(queue.TryPeek(out _, out _)); + Assert.False(queue.TryDequeue(out _, out _)); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void PriorityQueue_Generic_EnqueueRange_IEnumerable(int count) + { + HashSet<(TElement, TPriority)> itemsToEnqueue = this.GenericIEnumerableFactory(count).ToHashSet(); + PriorityQueue queue = new PriorityQueue(); + + queue.EnqueueRange(itemsToEnqueue); + + Assert.True(itemsToEnqueue.SetEquals(queue.UnorderedItems)); + } + + #endregion + + #region Clear + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void PriorityQueue_Generic_Clear(int count) + { + PriorityQueue queue = this.GenericPriorityQueueFactory(initialCapacity: 0, count, out _); + + Assert.Equal(count, queue.Count); + queue.Clear(); + Assert.Equal(expected: 0, queue.Count); + } + + #endregion + + #region EnsureCapacity, TrimExcess + + [Fact] + public void PriorityQueue_Generic_EnsureCapacity_Negative() + { + PriorityQueue queue = new PriorityQueue(); + AssertExtensions.Throws("capacity", () => queue.EnsureCapacity(-1)); + AssertExtensions.Throws("capacity", () => queue.EnsureCapacity(int.MinValue)); + } + + [Theory] + [InlineData(0, 0)] + [InlineData(0, 5)] + [InlineData(1, 1)] + [InlineData(3, 100)] + public void PriorityQueue_Generic_TrimExcess_ValidQueueThatHasntBeenRemovedFrom(int initialCapacity, int count) + { + PriorityQueue queue = this.GenericPriorityQueueFactory(initialCapacity, count, out _); + queue.TrimExcess(); + } + + [Theory] + [MemberData(nameof(ValidCollectionSizes))] + public void PriorityQueue_Generic_TrimExcess_Repeatedly(int count) + { + PriorityQueue queue = this.GenericPriorityQueueFactory(initialCapacity: 0, count, out _); + + Assert.Equal(count, queue.Count); + queue.TrimExcess(); + queue.TrimExcess(); + queue.TrimExcess(); + Assert.Equal(count, queue.Count); + } + + [Theory] + [MemberData(nameof(ValidPositiveCollectionSizes))] + public void PriorityQueue_Generic_EnsureCapacityAndTrimExcess(int count) + { + IReadOnlyCollection<(TElement, TPriority)> itemsToEnqueue = this.GenericIEnumerableFactory(count).ToArray(); + PriorityQueue queue = new PriorityQueue(); + int expectedCount = 0; + Random random = new Random(Seed: 34); + int getNextEnsureCapacity() => random.Next(0, count * 2); + void trimAndEnsureCapacity() + { + queue.TrimExcess(); + + int capacityAfterEnsureCapacity = queue.EnsureCapacity(getNextEnsureCapacity()); + Assert.Equal(capacityAfterEnsureCapacity, GetUnderlyingBufferCapacity(queue)); + + int capacityAfterTrimExcess = (queue.Count < (int)(capacityAfterEnsureCapacity * 0.9)) ? queue.Count : capacityAfterEnsureCapacity; + queue.TrimExcess(); + Assert.Equal(capacityAfterTrimExcess, GetUnderlyingBufferCapacity(queue)); + }; + + foreach (var (element, priority) in itemsToEnqueue) + { + trimAndEnsureCapacity(); + queue.Enqueue(element, priority); + expectedCount++; + Assert.Equal(expectedCount, queue.Count); + } + + while (expectedCount > 0) + { + queue.Dequeue(); + trimAndEnsureCapacity(); + expectedCount--; + Assert.Equal(expectedCount, queue.Count); + } + + trimAndEnsureCapacity(); + Assert.Equal(0, queue.Count); + } + + private static int GetUnderlyingBufferCapacity(PriorityQueue queue) + { + FieldInfo nodesType = queue.GetType().GetField("_nodes", BindingFlags.NonPublic | BindingFlags.Instance); + var nodes = ((TElement Element, TPriority Priority)[])nodesType.GetValue(queue); + return nodes.Length; + } + + #endregion + + #region Enumeration + + [Theory] + [MemberData(nameof(ValidPositiveCollectionSizes))] + public void PriorityQueue_Enumeration_OrderingIsConsistent(int count) + { + PriorityQueue queue = this.GenericPriorityQueueFactory(initialCapacity: 0, count, out _); + + (TElement, TPriority)[] firstEnumeration = queue.UnorderedItems.ToArray(); + (TElement, TPriority)[] secondEnumeration = queue.UnorderedItems.ToArray(); + + Assert.Equal(firstEnumeration.Length, count); + Assert.True(firstEnumeration.SequenceEqual(secondEnumeration)); + } + + [Theory] + [MemberData(nameof(ValidPositiveCollectionSizes))] + public void PriorityQueue_Enumeration_InvalidationOnModifiedCollection(int count) + { + IReadOnlyCollection<(TElement, TPriority)> itemsToEnqueue = this.GenericIEnumerableFactory(count).ToArray(); + PriorityQueue queue = new PriorityQueue(); + queue.EnqueueRange(itemsToEnqueue.Take(count - 1)); + var enumerator = queue.UnorderedItems.GetEnumerator(); + + (TElement element, TPriority priority) = itemsToEnqueue.Last(); + queue.Enqueue(element, priority); + Assert.Throws(() => enumerator.MoveNext()); + } + + [Theory] + [MemberData(nameof(ValidPositiveCollectionSizes))] + public void PriorityQueue_Enumeration_InvalidationOnModifiedCapacity(int count) + { + PriorityQueue queue = this.GenericPriorityQueueFactory(initialCapacity: 0, count, out _); + var enumerator = queue.UnorderedItems.GetEnumerator(); + + int capacityBefore = GetUnderlyingBufferCapacity(queue); + queue.EnsureCapacity(count * 2 + 4); + int capacityAfter = GetUnderlyingBufferCapacity(queue); + + Assert.NotEqual(capacityBefore, capacityAfter); + Assert.Throws(() => enumerator.MoveNext()); + } + + #endregion + } +} diff --git a/src/libraries/System.Collections/tests/Generic/PriorityQueue/PriorityQueue.Generic.cs b/src/libraries/System.Collections/tests/Generic/PriorityQueue/PriorityQueue.Generic.cs new file mode 100644 index 00000000000000..624649bdbaa6fb --- /dev/null +++ b/src/libraries/System.Collections/tests/Generic/PriorityQueue/PriorityQueue.Generic.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Collections.Tests +{ + public class PriorityQueue_Generic_Tests_string_string : PriorityQueue_Generic_Tests + { + protected override (string, string) CreateT(int seed) + { + var element = this.CreateString(seed); + var priority = this.CreateString(seed); + return (element, priority); + } + + protected string CreateString(int seed) + { + int stringLength = seed % 10 + 5; + Random rand = new Random(seed); + byte[] bytes = new byte[stringLength]; + rand.NextBytes(bytes); + return Convert.ToBase64String(bytes); + } + } + + public class PriorityQueue_Generic_Tests_int_int : PriorityQueue_Generic_Tests + { + protected override (int, int) CreateT(int seed) + { + var element = this.CreateInt(seed); + var priority = this.CreateInt(seed); + return (element, priority); + } + + protected int CreateInt(int seed) => new Random(seed).Next(); + } +} diff --git a/src/libraries/System.Collections/tests/Generic/PriorityQueue/PriorityQueue.Tests.cs b/src/libraries/System.Collections/tests/Generic/PriorityQueue/PriorityQueue.Tests.cs new file mode 100644 index 00000000000000..10ef97993b3c97 --- /dev/null +++ b/src/libraries/System.Collections/tests/Generic/PriorityQueue/PriorityQueue.Tests.cs @@ -0,0 +1,99 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Xunit; + +namespace System.Collections.Tests +{ + public class PriorityQueue_NonGeneric_Tests : TestBase + { + protected PriorityQueue SmallPriorityQueueFactory(out HashSet<(string, int)> items) + { + items = new HashSet<(string, int)> + { + ("one", 1), + ("two", 2), + ("three", 3) + }; + var queue = new PriorityQueue(items); + + return queue; + } + + [Fact] + public void PriorityQueue_Generic_EnqueueDequeue_SmallerThanMin() + { + PriorityQueue queue = SmallPriorityQueueFactory(out HashSet<(string, int)> enqueuedItems); + + string actualElement = queue.EnqueueDequeue("zero", 0); + + Assert.Equal("zero", actualElement); + Assert.True(enqueuedItems.SetEquals(queue.UnorderedItems)); + } + + [Fact] + public void PriorityQueue_Generic_EnqueueDequeue_LargerThanMin() + { + PriorityQueue queue = SmallPriorityQueueFactory(out HashSet<(string, int)> enqueuedItems); + + string actualElement = queue.EnqueueDequeue("four", 4); + + Assert.Equal("one", actualElement); + Assert.Equal("two", queue.Dequeue()); + Assert.Equal("three", queue.Dequeue()); + Assert.Equal("four", queue.Dequeue()); + } + + [Fact] + public void PriorityQueue_Generic_EnqueueDequeue_EqualToMin() + { + PriorityQueue queue = SmallPriorityQueueFactory(out HashSet<(string, int)> enqueuedItems); + + string actualElement = queue.EnqueueDequeue("one-not-to-enqueue", 1); + + Assert.Equal("one-not-to-enqueue", actualElement); + Assert.True(enqueuedItems.SetEquals(queue.UnorderedItems)); + } + + [Fact] + public void PriorityQueue_Generic_Constructor_IEnumerable_Null() + { + (string, int)[] itemsToEnqueue = new(string, int)[] { (null, 0), ("one", 1) } ; + PriorityQueue queue = new PriorityQueue(itemsToEnqueue); + Assert.Null(queue.Dequeue()); + Assert.Equal("one", queue.Dequeue()); + } + + [Fact] + public void PriorityQueue_Generic_Enqueue_Null() + { + PriorityQueue queue = new PriorityQueue(); + + queue.Enqueue(element: null, 1); + queue.Enqueue(element: "zero", 0); + queue.Enqueue(element: "two", 2); + + Assert.Equal("zero", queue.Dequeue()); + Assert.Null(queue.Dequeue()); + Assert.Equal("two", queue.Dequeue()); + } + + [Fact] + public void PriorityQueue_Generic_EnqueueRange_Null() + { + PriorityQueue queue = new PriorityQueue(); + + queue.EnqueueRange(new string[] { null, null, null }, 0); + queue.EnqueueRange(new string[] { "not null" }, 1); + queue.EnqueueRange(new string[] { null, null, null }, 0); + + for (int i = 0; i < 6; ++i) + { + Assert.Null(queue.Dequeue()); + } + + Assert.Equal("not null", queue.Dequeue()); + } + } +} diff --git a/src/libraries/System.Collections/tests/System.Collections.Tests.csproj b/src/libraries/System.Collections/tests/System.Collections.Tests.csproj index 412f68fc8425f9..eb646000406169 100644 --- a/src/libraries/System.Collections/tests/System.Collections.Tests.csproj +++ b/src/libraries/System.Collections/tests/System.Collections.Tests.csproj @@ -95,6 +95,9 @@ + + +