From b1c7e527c4230f0b60449a7d55090397f78733f2 Mon Sep 17 00:00:00 2001 From: Chris Cameron Date: Thu, 10 May 2018 11:48:33 -0400 Subject: [PATCH 1/5] feat: Initial commit of IcdOrderedDictionary --- CHANGELOG.md | 2 + .../Collections/IcdOrderedDictionaryTest.cs | 126 ++++++++++++++ .../Collections/IcdOrderedDictionary.cs | 159 ++++++++++++++++++ .../ICD.Common.Utils_SimplSharp.csproj | 1 + 4 files changed, 288 insertions(+) create mode 100644 ICD.Common.Utils.Tests/Collections/IcdOrderedDictionaryTest.cs create mode 100644 ICD.Common.Utils/Collections/IcdOrderedDictionary.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index f744978..b6061b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ 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 + - Adding IcdOrderedDictionary 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..a32b648 --- /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, 1}, + {2, 2} + }; + + 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, 1}, + {-1, -1} + }; + + 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, 1}, + {-1, -1} + }; + + int[] keys = dict.Values.ToArray(); + + Assert.AreEqual(3, keys.Length); + Assert.AreEqual(-1, keys[0]); + Assert.AreEqual(0, keys[1]); + Assert.AreEqual(1, keys[2]); + } + + [Test] + public void IndexerTest() + { + IcdOrderedDictionary dict = new IcdOrderedDictionary + { + {0, 0}, + {1, 1}, + {-1, -1} + }; + + Assert.AreEqual(0, dict[0]); + Assert.AreEqual(1, dict[1]); + Assert.AreEqual(-1, 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/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/ICD.Common.Utils_SimplSharp.csproj b/ICD.Common.Utils/ICD.Common.Utils_SimplSharp.csproj index ece69a3..3a062cd 100644 --- a/ICD.Common.Utils/ICD.Common.Utils_SimplSharp.csproj +++ b/ICD.Common.Utils/ICD.Common.Utils_SimplSharp.csproj @@ -74,6 +74,7 @@ + From 87427096d22a0bb0d933539d3bb28a277430e8f7 Mon Sep 17 00:00:00 2001 From: Chris Cameron Date: Thu, 10 May 2018 13:51:43 -0400 Subject: [PATCH 2/5] feat: Initial commit of PriorityQueue collection --- CHANGELOG.md | 3 +- .../Collections/PriorityQueueTest.cs | 140 +++++++++++++++++ ICD.Common.Utils/Collections/PriorityQueue.cs | 148 ++++++++++++++++++ .../ICD.Common.Utils_SimplSharp.csproj | 1 + 4 files changed, 291 insertions(+), 1 deletion(-) create mode 100644 ICD.Common.Utils.Tests/Collections/PriorityQueueTest.cs create mode 100644 ICD.Common.Utils/Collections/PriorityQueue.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index b6061b6..179a748 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] ### Added - - Adding IcdOrderedDictionary collection + - Added IcdOrderedDictionary collection + - Added PriorityQueue collection ## [3.2.0] - 2018-05-09 ### Added diff --git a/ICD.Common.Utils.Tests/Collections/PriorityQueueTest.cs b/ICD.Common.Utils.Tests/Collections/PriorityQueueTest.cs new file mode 100644 index 0000000..5163e0d --- /dev/null +++ b/ICD.Common.Utils.Tests/Collections/PriorityQueueTest.cs @@ -0,0 +1,140 @@ +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 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/PriorityQueue.cs b/ICD.Common.Utils/Collections/PriorityQueue.cs new file mode 100644 index 0000000..c0abdc0 --- /dev/null +++ b/ICD.Common.Utils/Collections/PriorityQueue.cs @@ -0,0 +1,148 @@ +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++; + } + + /// + /// Dequeues the first item with the lowest priority value. + /// + /// + [PublicAPI] + public T Dequeue() + { + KeyValuePair> kvp; + if (!m_PriorityToQueue.TryFirst(out kvp)) + throw new InvalidOperationException("The queue is empty."); + + int priority = kvp.Key; + List queue = kvp.Value; + + T output = queue[0]; + queue.RemoveAt(0); + + if (queue.Count == 0) + m_PriorityToQueue.Remove(priority); + + m_Count--; + + return output; + } + + /// + /// 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 3a062cd..87c6a3b 100644 --- a/ICD.Common.Utils/ICD.Common.Utils_SimplSharp.csproj +++ b/ICD.Common.Utils/ICD.Common.Utils_SimplSharp.csproj @@ -75,6 +75,7 @@ + From da8947aaa06e720f621ca6b9bc3a90c9e63504ab Mon Sep 17 00:00:00 2001 From: Chris Cameron Date: Thu, 10 May 2018 13:55:57 -0400 Subject: [PATCH 3/5] feat: Adding TryDequeue method to PriorityQueue --- ICD.Common.Utils/Collections/PriorityQueue.cs | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/ICD.Common.Utils/Collections/PriorityQueue.cs b/ICD.Common.Utils/Collections/PriorityQueue.cs index c0abdc0..2601c13 100644 --- a/ICD.Common.Utils/Collections/PriorityQueue.cs +++ b/ICD.Common.Utils/Collections/PriorityQueue.cs @@ -87,14 +87,31 @@ namespace ICD.Common.Utils.Collections [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)) - throw new InvalidOperationException("The queue is empty."); + return false; int priority = kvp.Key; List queue = kvp.Value; - T output = queue[0]; + output = queue[0]; queue.RemoveAt(0); if (queue.Count == 0) @@ -102,7 +119,7 @@ namespace ICD.Common.Utils.Collections m_Count--; - return output; + return true; } /// From d9cf0bd3cf30028414b6a742ffcd8c9224b77555 Mon Sep 17 00:00:00 2001 From: Chris Cameron Date: Thu, 10 May 2018 17:01:16 -0400 Subject: [PATCH 4/5] refactor: Changing ordered dictionary tests to avoid false positives --- .../Collections/IcdOrderedDictionaryTest.cs | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/ICD.Common.Utils.Tests/Collections/IcdOrderedDictionaryTest.cs b/ICD.Common.Utils.Tests/Collections/IcdOrderedDictionaryTest.cs index a32b648..dc6effd 100644 --- a/ICD.Common.Utils.Tests/Collections/IcdOrderedDictionaryTest.cs +++ b/ICD.Common.Utils.Tests/Collections/IcdOrderedDictionaryTest.cs @@ -15,8 +15,8 @@ namespace ICD.Common.Utils.Tests.Collections IcdOrderedDictionary dict = new IcdOrderedDictionary { {0, 0}, - {1, 1}, - {2, 2} + {1, 10}, + {2, 20} }; Assert.AreEqual(3, dict.Count); @@ -36,8 +36,8 @@ namespace ICD.Common.Utils.Tests.Collections IcdOrderedDictionary dict = new IcdOrderedDictionary { {0, 0}, - {1, 1}, - {-1, -1} + {1, 10}, + {-1, -10} }; int[] keys = dict.Keys.ToArray(); @@ -54,16 +54,16 @@ namespace ICD.Common.Utils.Tests.Collections IcdOrderedDictionary dict = new IcdOrderedDictionary { {0, 0}, - {1, 1}, - {-1, -1} + {1, 10}, + {-1, -10} }; - int[] keys = dict.Values.ToArray(); + int[] values = dict.Values.ToArray(); - Assert.AreEqual(3, keys.Length); - Assert.AreEqual(-1, keys[0]); - Assert.AreEqual(0, keys[1]); - Assert.AreEqual(1, keys[2]); + Assert.AreEqual(3, values.Length); + Assert.AreEqual(-10, values[0]); + Assert.AreEqual(0, values[1]); + Assert.AreEqual(10, values[2]); } [Test] @@ -72,13 +72,13 @@ namespace ICD.Common.Utils.Tests.Collections IcdOrderedDictionary dict = new IcdOrderedDictionary { {0, 0}, - {1, 1}, - {-1, -1} + {1, 10}, + {-1, -10} }; Assert.AreEqual(0, dict[0]); - Assert.AreEqual(1, dict[1]); - Assert.AreEqual(-1, dict[-1]); + Assert.AreEqual(10, dict[1]); + Assert.AreEqual(-10, dict[-1]); } #endregion From db8e725fa9c7f04a01ccbe2637e3e7221d3247be Mon Sep 17 00:00:00 2001 From: Chris Cameron Date: Thu, 10 May 2018 17:02:14 -0400 Subject: [PATCH 5/5] feat: Adding additional enqueue methods to PriorityQueue --- .../Collections/PriorityQueueTest.cs | 45 +++++++++++ ICD.Common.Utils/Collections/PriorityQueue.cs | 79 +++++++++++++++++++ 2 files changed, 124 insertions(+) diff --git a/ICD.Common.Utils.Tests/Collections/PriorityQueueTest.cs b/ICD.Common.Utils.Tests/Collections/PriorityQueueTest.cs index 5163e0d..caa319f 100644 --- a/ICD.Common.Utils.Tests/Collections/PriorityQueueTest.cs +++ b/ICD.Common.Utils.Tests/Collections/PriorityQueueTest.cs @@ -98,6 +98,51 @@ namespace ICD.Common.Utils.Tests.Collections 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() { diff --git a/ICD.Common.Utils/Collections/PriorityQueue.cs b/ICD.Common.Utils/Collections/PriorityQueue.cs index 2601c13..e66ddfe 100644 --- a/ICD.Common.Utils/Collections/PriorityQueue.cs +++ b/ICD.Common.Utils/Collections/PriorityQueue.cs @@ -80,6 +80,85 @@ namespace ICD.Common.Utils.Collections 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. ///