diff --git a/CHANGELOG.md b/CHANGELOG.md index f744978..179a748 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added + - Added IcdOrderedDictionary collection + - Added PriorityQueue collection ## [3.2.0] - 2018-05-09 ### Added diff --git a/ICD.Common.Utils.Tests/Collections/IcdOrderedDictionaryTest.cs b/ICD.Common.Utils.Tests/Collections/IcdOrderedDictionaryTest.cs new file mode 100644 index 0000000..dc6effd --- /dev/null +++ b/ICD.Common.Utils.Tests/Collections/IcdOrderedDictionaryTest.cs @@ -0,0 +1,126 @@ +using System.Linq; +using ICD.Common.Utils.Collections; +using NUnit.Framework; + +namespace ICD.Common.Utils.Tests.Collections +{ + [TestFixture] + public sealed class IcdOrderedDictionaryTest + { + #region Properties + + [Test] + public void CountTest() + { + IcdOrderedDictionary dict = new IcdOrderedDictionary + { + {0, 0}, + {1, 10}, + {2, 20} + }; + + Assert.AreEqual(3, dict.Count); + } + + [Test] + public void IsReadOnlyTest() + { + IcdOrderedDictionary dict = new IcdOrderedDictionary(); + + Assert.IsFalse(dict.IsReadOnly); + } + + [Test] + public void KeysTest() + { + IcdOrderedDictionary dict = new IcdOrderedDictionary + { + {0, 0}, + {1, 10}, + {-1, -10} + }; + + int[] keys = dict.Keys.ToArray(); + + Assert.AreEqual(3, keys.Length); + Assert.AreEqual(-1, keys[0]); + Assert.AreEqual(0, keys[1]); + Assert.AreEqual(1, keys[2]); + } + + [Test] + public void ValuesTest() + { + IcdOrderedDictionary dict = new IcdOrderedDictionary + { + {0, 0}, + {1, 10}, + {-1, -10} + }; + + int[] values = dict.Values.ToArray(); + + Assert.AreEqual(3, values.Length); + Assert.AreEqual(-10, values[0]); + Assert.AreEqual(0, values[1]); + Assert.AreEqual(10, values[2]); + } + + [Test] + public void IndexerTest() + { + IcdOrderedDictionary dict = new IcdOrderedDictionary + { + {0, 0}, + {1, 10}, + {-1, -10} + }; + + Assert.AreEqual(0, dict[0]); + Assert.AreEqual(10, dict[1]); + Assert.AreEqual(-10, dict[-1]); + } + + #endregion + + #region Methods + + [Test] + public void GetEnumeratorTest() + { + Assert.Inconclusive(); + } + + [Test] + public void AddTest() + { + Assert.Inconclusive(); + } + + [Test] + public void ClearTest() + { + Assert.Inconclusive(); + } + + [Test] + public void ContainsKeyTest() + { + Assert.Inconclusive(); + } + + [Test] + public void RemoveTest() + { + Assert.Inconclusive(); + } + + [Test] + public void TryGetValueTest() + { + Assert.Inconclusive(); + } + + #endregion + } +} diff --git a/ICD.Common.Utils.Tests/Collections/PriorityQueueTest.cs b/ICD.Common.Utils.Tests/Collections/PriorityQueueTest.cs new file mode 100644 index 0000000..caa319f --- /dev/null +++ b/ICD.Common.Utils.Tests/Collections/PriorityQueueTest.cs @@ -0,0 +1,185 @@ +using System.Collections.Generic; +using ICD.Common.Utils.Collections; +using NUnit.Framework; + +namespace ICD.Common.Utils.Tests.Collections +{ + [TestFixture] + public sealed class PriorityQueueTest + { + #region Properties + + [Test] + public void CountTest() + { + PriorityQueue queue = new PriorityQueue(); + + Assert.AreEqual(0, queue.Count); + + queue.Enqueue(1); + queue.Enqueue(2); + queue.Enqueue(3); + + Assert.AreEqual(3, queue.Count); + + queue.Clear(); + + Assert.AreEqual(0, queue.Count); + } + + [Test] + public void IsSynchronizedTest() + { + PriorityQueue queue = new PriorityQueue(); + + Assert.IsFalse(queue.IsSynchronized); + } + + [Test] + public void SyncRootTest() + { + PriorityQueue queue = new PriorityQueue(); + + Assert.AreEqual(queue, queue.SyncRoot); + } + + #endregion + + #region Methods + + [Test] + public void ClearTest() + { + PriorityQueue queue = new PriorityQueue(); + + queue.Enqueue(1); + queue.Enqueue(2); + queue.Enqueue(3); + + Assert.AreEqual(3, queue.Count); + + queue.Clear(); + + Assert.AreEqual(0, queue.Count); + } + + [Test] + public void EnqueueTest() + { + PriorityQueue queue = new PriorityQueue(); + + queue.Enqueue(1); + queue.Enqueue(2); + queue.Enqueue(3); + + Assert.AreEqual(3, queue.Count); + } + + [Test] + public void EnqueuePriorityTest() + { + PriorityQueue queue = new PriorityQueue(); + + queue.Enqueue(1, 3); + queue.Enqueue(2, 2); + queue.Enqueue(3, 1); + + Assert.AreEqual(3, queue.Count); + + List dequeue = new List + { + queue.Dequeue(), + queue.Dequeue(), + queue.Dequeue() + }; + + Assert.AreEqual(3, dequeue[0]); + Assert.AreEqual(2, dequeue[1]); + Assert.AreEqual(1, dequeue[2]); + } + + [Test] + public void EnqueueFirstTest() + { + PriorityQueue queue = new PriorityQueue(); + + queue.Enqueue(1, int.MaxValue); + queue.Enqueue(2, int.MinValue); + queue.EnqueueFirst(3); + + Assert.AreEqual(3, queue.Count); + + Assert.AreEqual(3, queue.Dequeue()); + } + + [Test] + public void EnqueueRemoveTest() + { + PriorityQueue queue = new PriorityQueue(); + + queue.Enqueue(1); + queue.Enqueue(2); + queue.Enqueue(3); + + queue.EnqueueRemove(4, i => i == 2); + + Assert.AreEqual(3, queue.Count); + + List dequeue = new List + { + queue.Dequeue(), + queue.Dequeue(), + queue.Dequeue() + }; + + Assert.AreEqual(1, dequeue[0]); + Assert.AreEqual(4, dequeue[1]); + Assert.AreEqual(3, dequeue[2]); + } + + [Test] + public void EnqueueRemovePriorityTest() + { + Assert.Inconclusive(); + } + + [Test] + public void DequeueTest() + { + PriorityQueue queue = new PriorityQueue(); + + queue.Enqueue(1, 3); + queue.Enqueue(2, 2); + queue.Enqueue(3, 1); + + Assert.AreEqual(3, queue.Count); + + List dequeue = new List + { + queue.Dequeue(), + queue.Dequeue(), + queue.Dequeue() + }; + + Assert.AreEqual(3, dequeue[0]); + Assert.AreEqual(2, dequeue[1]); + Assert.AreEqual(1, dequeue[2]); + + Assert.AreEqual(0, queue.Count); + } + + [Test] + public void GetEnumeratorTest() + { + Assert.Inconclusive(); + } + + [Test] + public void CopyToTest() + { + Assert.Inconclusive(); + } + + #endregion + } +} diff --git a/ICD.Common.Utils/Collections/IcdOrderedDictionary.cs b/ICD.Common.Utils/Collections/IcdOrderedDictionary.cs new file mode 100644 index 0000000..a688522 --- /dev/null +++ b/ICD.Common.Utils/Collections/IcdOrderedDictionary.cs @@ -0,0 +1,159 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using ICD.Common.Utils.Extensions; + +namespace ICD.Common.Utils.Collections +{ + public sealed class IcdOrderedDictionary : IDictionary + { + private readonly List m_OrderedKeys; + private readonly Dictionary m_Dictionary; + private readonly IComparer m_Comparer; + + #region Properties + + public int Count { get { return m_Dictionary.Count; } } + + public bool IsReadOnly { get { return false; } } + + public ICollection Keys { get { return m_OrderedKeys; } } + + public ICollection Values + { + get + { + return m_OrderedKeys.Select(k => m_Dictionary[k]) + .ToArray(Count); + } + } + + public TValue this[TKey key] + { + get { return m_Dictionary[key]; } + set + { +// ReSharper disable once CompareNonConstrainedGenericWithNull + if (key == null) + throw new ArgumentNullException("key"); + + if (!ContainsKey(key)) + m_OrderedKeys.AddSorted(key, m_Comparer); + + m_Dictionary[key] = value; + } + } + + #endregion + + /// + /// Constructor. + /// + public IcdOrderedDictionary() + : this(Comparer.Default) + { + } + + /// + /// Constructor. + /// + /// + public IcdOrderedDictionary(IComparer comparer) + { + if (comparer == null) + throw new ArgumentNullException("comparer"); + + m_Comparer = comparer; + m_OrderedKeys = new List(); + m_Dictionary = new Dictionary(); + } + + #region Methods + + public IEnumerator> GetEnumerator() + { + return m_OrderedKeys.Select(k => new KeyValuePair(k, m_Dictionary[k])) + .GetEnumerator(); + } + + public void Add(TKey key, TValue value) + { +// ReSharper disable once CompareNonConstrainedGenericWithNull + if (key == null) + throw new ArgumentNullException("key"); + + if (m_Dictionary.ContainsKey(key)) + throw new ArgumentException("An item with the same key has already been added.", "key"); + + this[key] = value; + } + + public void Clear() + { + m_OrderedKeys.Clear(); + m_Dictionary.Clear(); + } + + public bool ContainsKey(TKey key) + { + return m_Dictionary.ContainsKey(key); + } + + public bool Remove(TKey key) + { +// ReSharper disable once CompareNonConstrainedGenericWithNull + if (key == null) + throw new ArgumentNullException("key"); + + if (!m_Dictionary.Remove(key)) + return false; + + m_OrderedKeys.Remove(key); + + return true; + } + + public bool TryGetValue(TKey key, out TValue value) + { + return m_Dictionary.TryGetValue(key, out value); + } + + #endregion + + #region Private Methods + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + void ICollection>.Add(KeyValuePair item) + { + Add(item.Key, item.Value); + } + + bool ICollection>.Contains(KeyValuePair item) + { + TValue value; + return TryGetValue(item.Key, out value) && + EqualityComparer.Default.Equals(value, item.Value); + } + + void ICollection>.CopyTo(KeyValuePair[] array, int index) + { + foreach (KeyValuePair kvp in this) + { + array.SetValue(kvp, index); + index++; + } + } + + bool ICollection>.Remove(KeyValuePair item) + { + return (this as ICollection>).Contains(item) && Remove(item.Key); + } + + #endregion + } +} diff --git a/ICD.Common.Utils/Collections/PriorityQueue.cs b/ICD.Common.Utils/Collections/PriorityQueue.cs new file mode 100644 index 0000000..e66ddfe --- /dev/null +++ b/ICD.Common.Utils/Collections/PriorityQueue.cs @@ -0,0 +1,244 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using ICD.Common.Properties; +using ICD.Common.Utils.Extensions; + +namespace ICD.Common.Utils.Collections +{ + /// + /// Provides a first-in first-out collection with enhanced insertion features. + /// + public sealed class PriorityQueue : IEnumerable, ICollection + { + private readonly IcdOrderedDictionary> m_PriorityToQueue; + private int m_Count; + + #region Properties + + /// + /// Gets the number of items in the collection. + /// + public int Count { get { return m_Count; } } + + /// + /// Is the collection thread safe? + /// + public bool IsSynchronized { get { return false; } } + + /// + /// Gets a reference for locking. + /// + public object SyncRoot { get { return this; } } + + #endregion + + /// + /// Constructor. + /// + public PriorityQueue() + { + m_PriorityToQueue = new IcdOrderedDictionary>(); + } + + #region Methods + + /// + /// Clears the collection. + /// + [PublicAPI] + public void Clear() + { + m_PriorityToQueue.Clear(); + m_Count = 0; + } + + /// + /// Adds the item to the end of the queue. + /// + /// + [PublicAPI] + public void Enqueue(T item) + { + Enqueue(item, int.MaxValue); + } + + /// + /// Adds the item to the queue with the given priority. + /// Lower values are dequeued first. + /// + /// + /// + [PublicAPI] + public void Enqueue(T item, int priority) + { + if (!m_PriorityToQueue.ContainsKey(priority)) + m_PriorityToQueue.Add(priority, new List()); + + m_PriorityToQueue[priority].Add(item); + m_Count++; + } + + /// + /// Enqueues the item at the beginning of the queue. + /// + /// + [PublicAPI] + public void EnqueueFirst(T item) + { + const int priority = int.MinValue; + + if (!m_PriorityToQueue.ContainsKey(priority)) + m_PriorityToQueue.Add(priority, new List()); + + m_PriorityToQueue[priority].Insert(0, item); + m_Count++; + } + + /// + /// Removes any items in the queue matching the predicate. + /// Inserts the given item in the position of the first removed item, or at the end of the queue. + /// This is useful for reducing duplication, or replacing items with something more pertinant. + /// + /// + /// + [PublicAPI] + public void EnqueueRemove(T item, Func remove) + { + if (remove == null) + throw new ArgumentNullException("remove"); + + EnqueueRemove(item, remove, int.MaxValue); + } + + /// + /// Removes any items in the queue matching the predicate. + /// Inserts the given item in the position of the first removed item, or at the end of the queue. + /// This is useful for reducing duplication, or replacing items with something more pertinant. + /// + /// + /// + /// + [PublicAPI] + public void EnqueueRemove(T item, Func remove, int priority) + { + if (remove == null) + throw new ArgumentNullException("remove"); + + bool inserted = false; + + foreach (KeyValuePair> kvp in m_PriorityToQueue) + { + int[] removeIndices = + kvp.Value + .FindIndices(v => remove(v)) + .Reverse() + .ToArray(); + + if (removeIndices.Length == 0) + continue; + + foreach (int removeIndex in removeIndices) + { + kvp.Value.RemoveAt(removeIndex); + m_Count--; + } + + if (!inserted) + { + int insertIndex = removeIndices[0]; + kvp.Value.Insert(insertIndex, item); + m_Count++; + + inserted = true; + } + } + + if (!inserted) + Enqueue(item, priority); + } + + /// + /// Dequeues the first item with the lowest priority value. + /// + /// + [PublicAPI] + public T Dequeue() + { + T output; + if (TryDequeue(out output)) + return output; + + throw new InvalidOperationException("The queue is empty."); + } + + /// + /// Attempts to dequeue an item from the queue. + /// + /// + /// + [PublicAPI] + public bool TryDequeue(out T output) + { + output = default(T); + + KeyValuePair> kvp; + if (!m_PriorityToQueue.TryFirst(out kvp)) + return false; + + int priority = kvp.Key; + List queue = kvp.Value; + + output = queue[0]; + queue.RemoveAt(0); + + if (queue.Count == 0) + m_PriorityToQueue.Remove(priority); + + m_Count--; + + return true; + } + + /// + /// Gets an enumerator for the items. + /// + /// + public IEnumerator GetEnumerator() + { + return m_PriorityToQueue.Values + .SelectMany(v => v) + .GetEnumerator(); + } + + /// + /// Copies the collection to the target array. + /// + /// + /// + public void CopyTo(Array array, int index) + { + foreach (T item in this) + { + array.SetValue(item, index); + index++; + } + } + + #endregion + + #region Private Methods + + /// + /// Gets an enumerator for the items. + /// + /// + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + #endregion + } +} diff --git a/ICD.Common.Utils/ICD.Common.Utils_SimplSharp.csproj b/ICD.Common.Utils/ICD.Common.Utils_SimplSharp.csproj index ece69a3..87c6a3b 100644 --- a/ICD.Common.Utils/ICD.Common.Utils_SimplSharp.csproj +++ b/ICD.Common.Utils/ICD.Common.Utils_SimplSharp.csproj @@ -74,6 +74,8 @@ + +