diff --git a/ICD.Common.Utils.Tests/Collections/IcdOrderedDictionaryTest.cs b/ICD.Common.Utils.Tests/Collections/IcdOrderedDictionaryTest.cs new file mode 100644 index 0000000..0ef151a --- /dev/null +++ b/ICD.Common.Utils.Tests/Collections/IcdOrderedDictionaryTest.cs @@ -0,0 +1,92 @@ +using System.Collections.Generic; +using System.Linq; +using ICD.Common.Utils.Collections; +using NUnit.Framework; + +namespace ICD.Common.Utils.Tests.Collections +{ + [TestFixture] + public sealed class IcdOrderedDictionaryTest + { + [Test] + public void OrderingTest() + { + IcdOrderedDictionary dict = new IcdOrderedDictionary + { + {"1", "a"}, + {"2", "b"}, + {"3", "c"} + }; + + Assert.IsTrue(dict.Keys.SequenceEqual(new[] {"1", "2", "3"})); + Assert.IsTrue(dict.Values.SequenceEqual(new[] { "a", "b", "c" })); + + // Remove the key and add to the end of the dictionary + dict.Remove("2"); + dict["2"] = "d"; + + Assert.IsTrue(dict.Keys.SequenceEqual(new[] { "1", "3", "2" })); + Assert.IsTrue(dict.Values.SequenceEqual(new[] { "a", "c", "d" })); + + // No key change + dict["1"] = "e"; + + Assert.IsTrue(dict.Keys.SequenceEqual(new[] { "1", "3", "2" })); + Assert.IsTrue(dict.Values.SequenceEqual(new[] { "e", "c", "d" })); + } + + [Test] + public void GetTest() + { + IcdOrderedDictionary dict = new IcdOrderedDictionary + { + {"1", "a"}, + {"2", "b"}, + {"3", "c"} + }; + + KeyValuePair kvp = dict.Get(0); + Assert.AreEqual("1", kvp.Key); + Assert.AreEqual("a", kvp.Value); + + kvp = dict.Get(1); + Assert.AreEqual("2", kvp.Key); + Assert.AreEqual("b", kvp.Value); + + kvp = dict.Get(2); + Assert.AreEqual("3", kvp.Key); + Assert.AreEqual("c", kvp.Value); + + // Remove the key and add to the end of the dictionary + dict.Remove("2"); + dict["2"] = "d"; + + kvp = dict.Get(0); + Assert.AreEqual("1", kvp.Key); + Assert.AreEqual("a", kvp.Value); + + kvp = dict.Get(1); + Assert.AreEqual("3", kvp.Key); + Assert.AreEqual("c", kvp.Value); + + kvp = dict.Get(2); + Assert.AreEqual("2", kvp.Key); + Assert.AreEqual("d", kvp.Value); + + // No key change + dict["1"] = "e"; + + kvp = dict.Get(0); + Assert.AreEqual("1", kvp.Key); + Assert.AreEqual("e", kvp.Value); + + kvp = dict.Get(1); + Assert.AreEqual("3", kvp.Key); + Assert.AreEqual("c", kvp.Value); + + kvp = dict.Get(2); + Assert.AreEqual("2", kvp.Key); + Assert.AreEqual("d", kvp.Value); + } + } +} diff --git a/ICD.Common.Utils/Collections/IcdOrderedDictionary.cs b/ICD.Common.Utils/Collections/IcdOrderedDictionary.cs new file mode 100644 index 0000000..f624f38 --- /dev/null +++ b/ICD.Common.Utils/Collections/IcdOrderedDictionary.cs @@ -0,0 +1,651 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace ICD.Common.Utils.Collections +{ + public sealed class IcdOrderedDictionary : IDictionary, IDictionary + { + private readonly IcdOrderedDictionary m_PrivateDictionary; + + public int Count { get { return m_PrivateDictionary.Count; } } + + int ICollection.Count { get { return m_PrivateDictionary.Count; } } + + public bool IsReadOnly { get { return false; } } + + public TValue this[TKey key] + { + get + { + if (key == null) + throw new ArgumentNullException("key"); + + if (m_PrivateDictionary.Contains(key)) + return (TValue)m_PrivateDictionary[key]; + + throw new KeyNotFoundException(); + } + set + { + if (key == null) + throw new ArgumentNullException("key"); + + m_PrivateDictionary[key] = value; + } + } + + object IDictionary.this[object key] + { + get { return m_PrivateDictionary[key]; } + set { m_PrivateDictionary[key] = value; } + } + + public ICollection Keys + { + get + { + List keys = new List(m_PrivateDictionary.Count); + + foreach (TKey key in m_PrivateDictionary.Keys) + { + keys.Add(key); + } + + // Keys should be put in a ReadOnlyCollection, + // but since this is an internal class, for performance reasons, + // we choose to avoid creating yet another collection. + + return keys; + } + } + + ICollection IDictionary.Keys { get { return m_PrivateDictionary.Keys; } } + + public ICollection Values + { + get + { + List values = new List(m_PrivateDictionary.Count); + + foreach (TValue value in m_PrivateDictionary.Values) + values.Add(value); + + // Values should be put in a ReadOnlyCollection, + // but since this is an internal class, for performance reasons, + // we choose to avoid creating yet another collection. + + return values; + } + } + + ICollection IDictionary.Values { get { return m_PrivateDictionary.Values; } } + + bool IDictionary.IsFixedSize { get { return ((IDictionary)m_PrivateDictionary).IsFixedSize; } } + + bool IDictionary.IsReadOnly { get { return m_PrivateDictionary.IsReadOnly; } } + + bool ICollection.IsSynchronized { get { return ((ICollection)m_PrivateDictionary).IsSynchronized; } } + + object ICollection.SyncRoot { get { return m_PrivateDictionary; } } + + public IcdOrderedDictionary() + { + m_PrivateDictionary = new IcdOrderedDictionary(); + } + + public IcdOrderedDictionary(IEnumerable> dictionary) + { + if (dictionary == null) + return; + + m_PrivateDictionary = new IcdOrderedDictionary(); + + foreach (KeyValuePair pair in dictionary) + { + m_PrivateDictionary.Add(pair.Key, pair.Value); + } + } + + public void Add(KeyValuePair item) + { + Add(item.Key, item.Value); + } + + public void Add(TKey key, TValue value) + { + if (key == null) + throw new ArgumentNullException("key"); + + m_PrivateDictionary.Add(key, value); + } + + public KeyValuePair Get(int index) + { + DictionaryEntry entry = (DictionaryEntry)m_PrivateDictionary.ObjectsArray[index]; + return new KeyValuePair((TKey)entry.Key, (TValue)entry.Value); + } + + public void Clear() + { + m_PrivateDictionary.Clear(); + } + + public bool Contains(KeyValuePair item) + { + if (item.Key == null || !m_PrivateDictionary.Contains(item.Key)) + return false; + + return m_PrivateDictionary[item.Key].Equals(item.Value); + } + + public bool ContainsKey(TKey key) + { + if (key == null) + throw new ArgumentNullException("key"); + + return m_PrivateDictionary.Contains(key); + } + + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + if (array == null) + { + throw new ArgumentNullException("array"); + } + + if (arrayIndex < 0) + { + throw new ArgumentOutOfRangeException("arrayIndex"); + } + + if (array.Rank > 1 || arrayIndex >= array.Length || array.Length - arrayIndex < m_PrivateDictionary.Count) + { + throw new ArgumentException("array"); + } + + int index = arrayIndex; + foreach (DictionaryEntry entry in m_PrivateDictionary) + { + array[index] = new KeyValuePair((TKey)entry.Key, (TValue)entry.Value); + index++; + } + } + + public IEnumerator> GetEnumerator() + { + foreach (DictionaryEntry entry in m_PrivateDictionary) + yield return new KeyValuePair((TKey)entry.Key, (TValue)entry.Value); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public bool Remove(KeyValuePair item) + { + if (Contains(item)) + { + m_PrivateDictionary.Remove(item.Key); + return true; + } + return false; + } + + public bool Remove(TKey key) + { + if (key == null) + throw new ArgumentNullException("key"); + + if (m_PrivateDictionary.Contains(key)) + { + m_PrivateDictionary.Remove(key); + return true; + } + return false; + } + + public bool TryGetValue(TKey key, out TValue value) + { + if (key == null) + throw new ArgumentNullException("key"); + + bool keyExists = m_PrivateDictionary.Contains(key); + value = keyExists ? (TValue)m_PrivateDictionary[key] : default(TValue); + + return keyExists; + } + + void IDictionary.Add(object key, object value) + { + m_PrivateDictionary.Add(key, value); + } + + void IDictionary.Clear() + { + m_PrivateDictionary.Clear(); + } + + bool IDictionary.Contains(object key) + { + return m_PrivateDictionary.Contains(key); + } + + IDictionaryEnumerator IDictionary.GetEnumerator() + { + return m_PrivateDictionary.GetEnumerator(); + } + + void IDictionary.Remove(object key) + { + m_PrivateDictionary.Remove(key); + } + + void ICollection.CopyTo(Array array, int index) + { + m_PrivateDictionary.CopyTo(array, index); + } + } + + internal sealed class IcdOrderedDictionary : IDictionary + { + private ArrayList m_ObjectsArray; + private Hashtable m_ObjectsTable; + private readonly int m_InitialCapacity; + private readonly IEqualityComparer m_Comparer; + private readonly bool m_ReadOnly; + + /// + /// Gets the size of the table. + /// + public int Count { get { return ObjectsArray.Count; } } + + /// + /// Indicates that the collection can grow. + /// + bool IDictionary.IsFixedSize { get { return m_ReadOnly; } } + + /// + /// Indicates that the collection is not read-only + /// + public bool IsReadOnly { get { return m_ReadOnly; } } + + /// + /// Indicates that this class is not synchronized + /// + bool ICollection.IsSynchronized { get { return false; } } + + /// + /// Gets the collection of keys in the table in order. + /// + public ICollection Keys { get { return new OrderedDictionaryKeyValueCollection(ObjectsArray, true); } } + + /// + /// Returns an arrayList of the values in the table + /// + public ICollection Values { get { return new OrderedDictionaryKeyValueCollection(ObjectsArray, false); } } + + public ArrayList ObjectsArray + { + get { return m_ObjectsArray ?? (m_ObjectsArray = new ArrayList(m_InitialCapacity)); } + } + + private Hashtable ObjectsTable + { + get { return m_ObjectsTable ?? (m_ObjectsTable = new Hashtable(m_InitialCapacity, m_Comparer)); } + } + + /// + /// The SyncRoot object. Not used because IsSynchronized is false + /// + object ICollection.SyncRoot { get { return this; } } + + /// + /// Gets or sets the object at the specified index + /// + public object this[int index] + { + get { return ((DictionaryEntry)ObjectsArray[index]).Value; } + set + { + if (m_ReadOnly) + { + throw new NotSupportedException(); + } + if (index < 0 || index >= ObjectsArray.Count) + { + throw new ArgumentOutOfRangeException("index"); + } + object key = ((DictionaryEntry)ObjectsArray[index]).Key; + ObjectsArray[index] = new DictionaryEntry(key, value); + ObjectsTable[key] = value; + } + } + + /// + /// Gets or sets the object with the specified key + /// + public object this[object key] + { + get { return ObjectsTable[key]; } + set + { + if (m_ReadOnly) + { + throw new NotSupportedException(); + } + if (ObjectsTable.Contains(key)) + { + ObjectsTable[key] = value; + ObjectsArray[IndexOfKey(key)] = new DictionaryEntry(key, value); + } + else + { + Add(key, value); + } + } + } + + public IcdOrderedDictionary() : this(0) + { + } + + public IcdOrderedDictionary(int capacity) : this(capacity, null) + { + } + + public IcdOrderedDictionary(int capacity, IEqualityComparer comparer) + { + m_InitialCapacity = capacity; + m_Comparer = comparer; + } + + private IcdOrderedDictionary(IcdOrderedDictionary dictionary) + { + if (dictionary == null) + { + throw new ArgumentNullException("dictionary"); + } + + m_ReadOnly = true; + m_ObjectsArray = dictionary.m_ObjectsArray; + m_ObjectsTable = dictionary.m_ObjectsTable; + m_Comparer = dictionary.m_Comparer; + m_InitialCapacity = dictionary.m_InitialCapacity; + } + + /// + /// Adds a new entry to the table with the lowest-available index. + /// + public void Add(object key, object value) + { + if (m_ReadOnly) + { + throw new NotSupportedException(); + } + ObjectsTable.Add(key, value); + ObjectsArray.Add(new DictionaryEntry(key, value)); + } + + /// + /// Clears all elements in the table. + /// + public void Clear() + { + if (m_ReadOnly) + { + throw new NotSupportedException(); + } + ObjectsTable.Clear(); + ObjectsArray.Clear(); + } + + /// + /// Returns a readonly IcdOrderedDictionary for the given IcdOrderedDictionary. + /// + public IcdOrderedDictionary AsReadOnly() + { + return new IcdOrderedDictionary(this); + } + + /// + /// Returns true if the key exists in the table, false otherwise. + /// + public bool Contains(object key) + { + return ObjectsTable.Contains(key); + } + + /// + /// Copies the table to an array. This will not preserve order. + /// + public void CopyTo(Array array, int index) + { + ObjectsTable.CopyTo(array, index); + } + + private int IndexOfKey(object key) + { + for (int i = 0; i < ObjectsArray.Count; i++) + { + object o = ((DictionaryEntry)ObjectsArray[i]).Key; + if (m_Comparer != null) + { + if (m_Comparer.Equals(o, key)) + { + return i; + } + } + else + { + if (o.Equals(key)) + { + return i; + } + } + } + return -1; + } + + /// + /// Inserts a new object at the given index with the given key. + /// + public void Insert(int index, object key, object value) + { + if (m_ReadOnly) + { + throw new NotSupportedException(); + } + if (index > Count || index < 0) + { + throw new ArgumentOutOfRangeException("index"); + } + ObjectsTable.Add(key, value); + ObjectsArray.Insert(index, new DictionaryEntry(key, value)); + } + + /// + /// Removes the entry at the given index. + /// + public void RemoveAt(int index) + { + if (m_ReadOnly) + { + throw new NotSupportedException(); + } + if (index >= Count || index < 0) + { + throw new ArgumentOutOfRangeException("index"); + } + object key = ((DictionaryEntry)ObjectsArray[index]).Key; + ObjectsArray.RemoveAt(index); + ObjectsTable.Remove(key); + } + + /// + /// Removes the entry with the given key. + /// + public void Remove(object key) + { + if (m_ReadOnly) + { + throw new NotSupportedException(); + } + if (key == null) + { + throw new ArgumentNullException("key"); + } + + int index = IndexOfKey(key); + if (index < 0) + { + return; + } + + ObjectsTable.Remove(key); + ObjectsArray.RemoveAt(index); + } + + #region IDictionary implementation + + /// + public IDictionaryEnumerator GetEnumerator() + { + return new OrderedDictionaryEnumerator(ObjectsArray, OrderedDictionaryEnumerator.DICTIONARY_ENTRY); + } + + #endregion + + #region IEnumerable implementation + + /// + IEnumerator IEnumerable.GetEnumerator() + { + return new OrderedDictionaryEnumerator(ObjectsArray, OrderedDictionaryEnumerator.DICTIONARY_ENTRY); + } + + #endregion + + /// + /// OrderedDictionaryEnumerator works just like any other IDictionaryEnumerator, but it retrieves DictionaryEntries + /// in the order by index. + /// + private sealed class OrderedDictionaryEnumerator : IDictionaryEnumerator + { + internal const int KEYS = 1; + internal const int VALUES = 2; + internal const int DICTIONARY_ENTRY = 3; + + private readonly int m_ObjectReturnType; + private readonly IEnumerator m_ArrayEnumerator; + + internal OrderedDictionaryEnumerator(ArrayList array, int objectReturnType) + { + m_ArrayEnumerator = array.GetEnumerator(); + m_ObjectReturnType = objectReturnType; + } + + /// + /// Retrieves the current DictionaryEntry. This is the same as Entry, but not strongly-typed. + /// + public object Current + { + get + { + if (m_ObjectReturnType == KEYS) + { + return ((DictionaryEntry)m_ArrayEnumerator.Current).Key; + } + if (m_ObjectReturnType == VALUES) + { + return ((DictionaryEntry)m_ArrayEnumerator.Current).Value; + } + return Entry; + } + } + + /// + /// Retrieves the current DictionaryEntry + /// + public DictionaryEntry Entry + { + get + { + return new DictionaryEntry(((DictionaryEntry)m_ArrayEnumerator.Current).Key, + ((DictionaryEntry)m_ArrayEnumerator.Current).Value); + } + } + + /// + /// Retrieves the key of the current DictionaryEntry + /// + public object Key { get { return ((DictionaryEntry)m_ArrayEnumerator.Current).Key; } } + + /// + /// Retrieves the value of the current DictionaryEntry + /// + public object Value { get { return ((DictionaryEntry)m_ArrayEnumerator.Current).Value; } } + + /// + /// Moves the enumerator pointer to the next member + /// + public bool MoveNext() + { + return m_ArrayEnumerator.MoveNext(); + } + + /// + /// Resets the enumerator pointer to the beginning. + /// + public void Reset() + { + m_ArrayEnumerator.Reset(); + } + } + + /// + /// OrderedDictionaryKeyValueCollection implements a collection for the Values and Keys properties + /// that is "live"- it will reflect changes to the IcdOrderedDictionary on the collection made after the getter + /// was called. + /// + private sealed class OrderedDictionaryKeyValueCollection : ICollection + { + private readonly ArrayList m_Objects; + private readonly bool m_IsKeys; + + public OrderedDictionaryKeyValueCollection(ArrayList array, bool isKeys) + { + m_Objects = array; + m_IsKeys = isKeys; + } + + void ICollection.CopyTo(Array array, int index) + { + if (array == null) + throw new ArgumentNullException("array"); + if (index < 0) + throw new ArgumentOutOfRangeException("index"); + foreach (object o in m_Objects) + { + array.SetValue(m_IsKeys ? ((DictionaryEntry)o).Key : ((DictionaryEntry)o).Value, index); + index++; + } + } + + int ICollection.Count { get { return m_Objects.Count; } } + + bool ICollection.IsSynchronized { get { return false; } } + + object ICollection.SyncRoot { get { return m_Objects.SyncRoot; } } + + IEnumerator IEnumerable.GetEnumerator() + { + return new OrderedDictionaryEnumerator(m_Objects, + m_IsKeys + ? OrderedDictionaryEnumerator.KEYS + : OrderedDictionaryEnumerator.VALUES); + } + } + } +} diff --git a/ICD.Common.Utils/ICD.Common.Utils_SimplSharp.csproj b/ICD.Common.Utils/ICD.Common.Utils_SimplSharp.csproj index 0dae781..b13d1a1 100644 --- a/ICD.Common.Utils/ICD.Common.Utils_SimplSharp.csproj +++ b/ICD.Common.Utils/ICD.Common.Utils_SimplSharp.csproj @@ -80,6 +80,7 @@ +