From f816ae771b933d1d9f62c875b28c7418e46908c5 Mon Sep 17 00:00:00 2001 From: Jack Kanarish Date: Wed, 30 May 2018 16:50:25 -0400 Subject: [PATCH 1/2] chore: merge util changes from dev needed for sharp bug backport to metlife 5.2 --- ICD.Common.Utils/Collections/IcdHashSet.cs | 78 ++++- .../Collections/IcdOrderedDictionary.cs | 159 ++++++++++ ICD.Common.Utils/Collections/PriorityQueue.cs | 244 ++++++++++++++++ .../Collections/WeakKeyDictionary.cs | 273 ++++++++++++++++++ .../Extensions/EnumerableExtensions.cs | 71 +++++ .../ICD.Common.Utils_SimplSharp.csproj | 3 + 6 files changed, 814 insertions(+), 14 deletions(-) create mode 100644 ICD.Common.Utils/Collections/IcdOrderedDictionary.cs create mode 100644 ICD.Common.Utils/Collections/PriorityQueue.cs create mode 100644 ICD.Common.Utils/Collections/WeakKeyDictionary.cs diff --git a/ICD.Common.Utils/Collections/IcdHashSet.cs b/ICD.Common.Utils/Collections/IcdHashSet.cs index 8561c70..17fbe65 100644 --- a/ICD.Common.Utils/Collections/IcdHashSet.cs +++ b/ICD.Common.Utils/Collections/IcdHashSet.cs @@ -67,6 +67,11 @@ namespace ICD.Common.Utils.Collections #region Methods + /// + /// Returns a set containing all of this sets items plus all of the items in the given set. + /// + /// + /// [PublicAPI] public IcdHashSet Union(IEnumerable set) { @@ -80,6 +85,11 @@ namespace ICD.Common.Utils.Collections return unionSet; } + /// + /// Returns a new set of this sets items exluding the items in the given set. + /// + /// + /// [PublicAPI] public IcdHashSet Subtract(IEnumerable set) { @@ -94,13 +104,11 @@ namespace ICD.Common.Utils.Collections return subtractSet; } - [PublicAPI] - public bool IsSubsetOf(IcdHashSet set) - { - IcdHashSet setToCompare = set ?? NullSet; - return this.All(setToCompare.Contains); - } - + /// + /// Returns all of the items that are common between this set and the given set. + /// + /// + /// [PublicAPI] public IcdHashSet Intersection(IcdHashSet set) { @@ -126,32 +134,74 @@ namespace ICD.Common.Utils.Collections [PublicAPI] public IcdHashSet NonIntersection(IcdHashSet set) { - return Subtract(set).Union(set.Subtract(this)); + IcdHashSet setToCompare = set ?? NullSet; + + return Subtract(set).Union(setToCompare.Subtract(this)); } + /// + /// Returns true if the given set contains all of the items in this set. + /// + /// + /// + [PublicAPI] + public bool IsSubsetOf(IcdHashSet set) + { + IcdHashSet setToCompare = set ?? NullSet; + + return this.All(setToCompare.Contains); + } + + /// + /// Returns true if the given set contains all of the items in this set, and the sets are not equal. + /// + /// + /// [PublicAPI] public bool IsProperSubsetOf(IcdHashSet set) { IcdHashSet setToCompare = set ?? NullSet; - // Is a proper subset if A is a subset of B and A != B - return (IsSubsetOf(setToCompare) && !setToCompare.IsSubsetOf(this)); + return IsSubsetOf(setToCompare) && !setToCompare.IsSubsetOf(this); } + /// + /// Returns true if this set contains all of the items in the given set. + /// + /// + /// [PublicAPI] public bool IsSupersetOf(IcdHashSet set) { IcdHashSet setToCompare = set ?? NullSet; + return setToCompare.IsSubsetOf(this); } + /// + /// Returns true if this set contains all of the items in the given set, and the sets are not equal. + /// + /// + /// [PublicAPI] public bool IsProperSupersetOf(IcdHashSet set) { IcdHashSet setToCompare = set ?? NullSet; - // B is a proper superset of A if B is a superset of A and A != B - return (IsSupersetOf(setToCompare) && !setToCompare.IsSupersetOf(this)); + return IsSupersetOf(setToCompare) && !setToCompare.IsSupersetOf(this); + } + + /// + /// Returns true if this set contains all of the items in the given set, and vice versa. + /// + /// + /// + [PublicAPI] + public bool SetEquals(IcdHashSet set) + { + IcdHashSet setToCompare = set ?? NullSet; + + return IsSupersetOf(setToCompare) && setToCompare.IsSupersetOf(this); } #endregion @@ -165,9 +215,9 @@ namespace ICD.Common.Utils.Collections /// public bool Add(T item) { -// ReSharper disable CompareNonConstrainedGenericWithNull + // ReSharper disable CompareNonConstrainedGenericWithNull if (item == null) -// ReSharper restore CompareNonConstrainedGenericWithNull + // ReSharper restore CompareNonConstrainedGenericWithNull throw new ArgumentNullException("item"); if (m_Dict.ContainsKey(item)) 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/Collections/WeakKeyDictionary.cs b/ICD.Common.Utils/Collections/WeakKeyDictionary.cs new file mode 100644 index 0000000..402718c --- /dev/null +++ b/ICD.Common.Utils/Collections/WeakKeyDictionary.cs @@ -0,0 +1,273 @@ +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 +{ + public sealed class WeakKeyReference : WeakReference + { + private readonly int m_HashCode; + + public new T Target { get { return (T)base.Target; } } + + public int HashCode { get { return m_HashCode; } } + + /// + /// Constructor. + /// + /// + public WeakKeyReference(T key) + : this(key, EqualityComparer.Default) + { + } + + /// + /// Constructor. + /// + /// + /// + public WeakKeyReference(T key, IEqualityComparer comparer) + : base(key) + { + if (comparer == null) + throw new ArgumentNullException("comparer"); + + // Retain the object's hash code immediately so that even + // if the target is GC'ed we will be able to find and + // remove the dead weak reference. + m_HashCode = comparer.GetHashCode(key); + } + } + + public sealed class WeakKeyComparer : IEqualityComparer> + { + private readonly IEqualityComparer m_Comparer; + + /// + /// Constructor. + /// + /// + public WeakKeyComparer(IEqualityComparer comparer) + { + if (comparer == null) + comparer = EqualityComparer.Default; + + m_Comparer = comparer; + } + + public int GetHashCode(WeakKeyReference weakKey) + { + return weakKey.HashCode; + } + + // Note: There are actually 9 cases to handle here. + // + // Let Wa = Alive Weak Reference + // Let Wd = Dead Weak Reference + // Let S = Strong Reference + // + // x | y | Equals(x,y) + // ------------------------------------------------- + // Wa | Wa | comparer.Equals(x.Target, y.Target) + // Wa | Wd | false + // Wa | S | comparer.Equals(x.Target, y) + // Wd | Wa | false + // Wd | Wd | x == y + // Wd | S | false + // S | Wa | comparer.Equals(x, y.Target) + // S | Wd | false + // S | S | comparer.Equals(x, y) + // ------------------------------------------------- + public bool Equals(WeakKeyReference x, WeakKeyReference y) + { + bool xIsDead; + bool yIsDead; + + T first = GetTarget(x, out xIsDead); + T second = GetTarget(y, out yIsDead); + + if (xIsDead) + return yIsDead && x == y; + + return !yIsDead && m_Comparer.Equals(first, second); + } + + private static T GetTarget(WeakKeyReference obj, out bool isDead) + { + T target = obj.Target; + isDead = !obj.IsAlive; + return target; + } + } + + /// + /// WeakDictionary keeps weak references to keys and drops key/value pairs once the key is garbage collected. + /// + /// + /// + public sealed class WeakKeyDictionary : IDictionary + { + private readonly Dictionary, TValue> m_Dictionary; + private readonly IEqualityComparer m_Comparer; + + #region Properties + + public int Count { get { return GetAliveKvps().Count(); } } + + public bool IsReadOnly { get { return false; } } + + public ICollection Keys { get { return GetAliveKvps().Select(kvp => kvp.Key).ToArray(); } } + + public ICollection Values { get { return GetAliveKvps().Select(kvp => kvp.Value).ToArray(); } } + + public TValue this[TKey key] + { + get + { + TValue output; + if (TryGetValue(key, out output)) + return output; + + throw new KeyNotFoundException(); + } + set + { +// ReSharper disable once CompareNonConstrainedGenericWithNull + if (key == null) + throw new ArgumentNullException("key"); + + WeakKeyReference weakKey = new WeakKeyReference(key, m_Comparer); + m_Dictionary[weakKey] = value; + } + } + + #endregion + + public WeakKeyDictionary() + : this(0) + { + } + + public WeakKeyDictionary(int capacity) + : this(capacity, EqualityComparer.Default) + { + } + + public WeakKeyDictionary(IEqualityComparer comparer) + : this(0, comparer) + { + } + + public WeakKeyDictionary(int capacity, IEqualityComparer comparer) + { + m_Comparer = comparer; + + WeakKeyComparer keyComparer = new WeakKeyComparer(m_Comparer); + m_Dictionary = new Dictionary, TValue>(capacity, keyComparer); + } + + #region Methods + + public void Add(TKey key, TValue value) + { +// ReSharper disable once CompareNonConstrainedGenericWithNull + if (key == null) + throw new ArgumentNullException("key"); + + WeakKeyReference weakKey = new WeakKeyReference(key, m_Comparer); + m_Dictionary.Add(weakKey, value); + } + + public bool ContainsKey(TKey key) + { + TValue unused; + return TryGetValue(key, out unused); + } + + public bool Remove(TKey key) + { + return m_Dictionary.Remove(new WeakKeyReference(key, m_Comparer)); + } + + public bool TryGetValue(TKey key, out TValue value) + { + return m_Dictionary.TryGetValue(new WeakKeyReference(key, m_Comparer), out value); + } + + public void Clear() + { + m_Dictionary.Clear(); + } + + /// + /// Removes the left-over weak references for entries in the dictionary + /// whose key or value has already been reclaimed by the garbage + /// collector. This will reduce the dictionary's Count by the number + /// of dead key-value pairs that were eliminated. + /// + [PublicAPI] + public void RemoveCollectedEntries() + { + IEnumerable> toRemove = + m_Dictionary.Select(pair => pair.Key) + .Where(weakKey => !weakKey.IsAlive) + .ToArray(); + + m_Dictionary.RemoveAll(toRemove); + } + + #region ICollection + + void ICollection>.Add(KeyValuePair item) + { + Add(item.Key, item.Value); + } + + bool ICollection>.Remove(KeyValuePair item) + { + return Remove(item.Key); + } + + bool ICollection>.Contains(KeyValuePair item) + { + return ContainsKey(item.Key); + } + + void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) + { + GetAliveKvps().CopyTo(array, arrayIndex); + } + + private IEnumerable> GetAliveKvps() + { + foreach (KeyValuePair, TValue> kvp in m_Dictionary) + { + WeakKeyReference weakKey = kvp.Key; + TKey key = weakKey.Target; + if (weakKey.IsAlive) + yield return new KeyValuePair(key, kvp.Value); + } + } + + #endregion + + #region IEnumerator + + public IEnumerator> GetEnumerator() + { + return GetAliveKvps().GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + #endregion + + #endregion + } +} diff --git a/ICD.Common.Utils/Extensions/EnumerableExtensions.cs b/ICD.Common.Utils/Extensions/EnumerableExtensions.cs index 5233bd5..244b936 100644 --- a/ICD.Common.Utils/Extensions/EnumerableExtensions.cs +++ b/ICD.Common.Utils/Extensions/EnumerableExtensions.cs @@ -578,6 +578,23 @@ namespace ICD.Common.Utils.Extensions return extends.Where(i => !comparer.Equals(item, i)); } + /// + /// Copies all the elements of the current one-dimensional array to the specified one-dimensional array + /// starting at the specified destination array index. The index is specified as a 32-bit integer. + /// + /// + /// + /// + /// + public static void CopyTo(this IEnumerable extends, T[] array, int index) + { + if (extends == null) + throw new ArgumentNullException("extends"); + + ICollection collection = extends as ICollection ?? extends.ToArray(); + collection.CopyTo(array, index); + } + /// /// Returns the sequence as a IcdHashSet. /// @@ -801,6 +818,17 @@ namespace ICD.Common.Utils.Extensions return output; } + /// + /// Wraps this object instance into an IEnumerable consisting of a single item. + /// + /// Type of the object. + /// The instance that will be wrapped. + /// An IEnumerable<T> consisting of a single item. + public static IEnumerable Yield(this T item) + { + yield return item; + } + /// /// Given a sequence [A, B, C] returns a sequence [[A, B], [B, C]] /// @@ -1057,6 +1085,49 @@ namespace ICD.Common.Utils.Extensions } } + // since S# can't do anonymous types + private struct TryParseStruct + { + public readonly T value; + public readonly bool isParsed; + + public TryParseStruct(T value, bool isParsed) + { + this.value = value; + this.isParsed = isParsed; + } + } + + // since Func<...,T> can't specify `out` parameters + public delegate bool TryParseDelegate(string input, out T output); + + /// + /// Attempts to parse each value of the enumerable, + /// throwing away the values that don't parse correctly. + /// + /// type to parse to + /// enumerable of strings to parse + /// TryParse function for given type + /// enumerable of successfully parsed values + public static IEnumerable TryParseSkipFailures(this IEnumerable extends, + TryParseDelegate tryParseFunc) + { + if (extends == null) + throw new ArgumentNullException("extends"); + + if (tryParseFunc == null) + throw new ArgumentNullException("tryParseFunc"); + + return extends.Select(str => + { + T value; + bool isParsed = tryParseFunc(str, out value); + return new TryParseStruct(value, isParsed); + }) + .Where(v => v.isParsed) + .Select(v => v.value); + } + #if SIMPLSHARP /// diff --git a/ICD.Common.Utils/ICD.Common.Utils_SimplSharp.csproj b/ICD.Common.Utils/ICD.Common.Utils_SimplSharp.csproj index 52ac708..b663b60 100644 --- a/ICD.Common.Utils/ICD.Common.Utils_SimplSharp.csproj +++ b/ICD.Common.Utils/ICD.Common.Utils_SimplSharp.csproj @@ -74,6 +74,9 @@ + + + From c03833bf9dfc932ce078fca18150a7f946fe5f00 Mon Sep 17 00:00:00 2001 From: Jack Kanarish Date: Thu, 31 May 2018 17:28:02 -0400 Subject: [PATCH 2/2] fix: dont insert at an index which was removed --- ICD.Common.Utils/Collections/PriorityQueue.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ICD.Common.Utils/Collections/PriorityQueue.cs b/ICD.Common.Utils/Collections/PriorityQueue.cs index e66ddfe..7ec5e14 100644 --- a/ICD.Common.Utils/Collections/PriorityQueue.cs +++ b/ICD.Common.Utils/Collections/PriorityQueue.cs @@ -148,7 +148,11 @@ namespace ICD.Common.Utils.Collections if (!inserted) { int insertIndex = removeIndices[0]; - kvp.Value.Insert(insertIndex, item); + + if (insertIndex >= kvp.Value.Count) + kvp.Value.Add(item); + else + kvp.Value.Insert(insertIndex, item); m_Count++; inserted = true;