diff --git a/ICD.Common.Utils.Tests/Collections/ReverseLookupDictionaryTest.cs b/ICD.Common.Utils.Tests/Collections/ReverseLookupDictionaryTest.cs new file mode 100644 index 0000000..e1946cc --- /dev/null +++ b/ICD.Common.Utils.Tests/Collections/ReverseLookupDictionaryTest.cs @@ -0,0 +1,300 @@ +using System; +using System.Collections.Generic; +using ICD.Common.Utils.Collections; +using ICD.Common.Utils.Extensions; +using NUnit.Framework; + +namespace ICD.Common.Utils.Tests.Collections +{ + [TestFixture] + public class ReverseLookupDictionaryTest + { + + //todo: Finish! + #region Properties + + [Test] + public void CountTest() + { + ReverseLookupDictionary dict = new ReverseLookupDictionary(); + + dict.Set(1, "10"); + dict.Set(2, "10"); + dict.Set(3, "30"); + + Assert.AreEqual(2, dict.Count); + } + + [Test] + public void KeysTest() + { + ReverseLookupDictionary dict = new ReverseLookupDictionary(); + + dict.Set(1, "10"); + dict.Set(2, "10"); + dict.Set(3, "30"); + + Assert.AreEqual(2, dict.Keys.Count); + + Assert.IsTrue(dict.Keys.Contains(2)); + Assert.IsTrue(dict.Keys.Contains(3)); + Assert.IsFalse(dict.Keys.Contains(4)); + } + + [Test] + public void ValuesTest() + { + ReverseLookupDictionary dict = new ReverseLookupDictionary(); + + dict.Set(1, "10"); + dict.Set(1, "20"); + dict.Set(3, "30"); + + Assert.AreEqual(2, dict.Values.Count); + + Assert.IsTrue(dict.Values.Contains("20")); + Assert.IsTrue(dict.Values.Contains("30")); + Assert.IsFalse(dict.Values.Contains("40")); + } + + #endregion + + #region Methods + + [Test] + public void ClearTest() + { + ReverseLookupDictionary dict = new ReverseLookupDictionary(); + + dict.Set(1, "10"); + dict.Set(1, "20"); + dict.Set(3, "30"); + + Assert.AreEqual(2, dict.Count); + + dict.Clear(); + + Assert.AreEqual(0, dict.Count); + } + + [Test] + public void ContainsKeyTest() + { + ReverseLookupDictionary dict = new ReverseLookupDictionary(); + + Assert.IsFalse(dict.ContainsKey(1)); + + dict.Set(1, "10"); + + Assert.IsTrue(dict.ContainsKey(1)); + } + + [Test] + public void ContainsValueTest() + { + ReverseLookupDictionary dict = new ReverseLookupDictionary(); + + Assert.IsFalse(dict.ContainsValue("10")); + + dict.Set(1, "10"); + + Assert.IsTrue(dict.ContainsValue("10")); + } + + [Test] + public void AddTest() + { + ReverseLookupDictionary dict = new ReverseLookupDictionary(); + + dict.Add(1, "10"); + + Assert.AreEqual(1, dict.Count); + + Assert.Throws(() => dict.Add(1, "10")); + Assert.Throws(() => dict.Add(1, "20")); + + Assert.DoesNotThrow(() => dict.Add(2, "10")); + Assert.DoesNotThrow(() => dict.Add(3, "20")); + + Assert.AreEqual(3, dict.Count); + } + + [Test] + public void SetTest() + { + ReverseLookupDictionary dict = new ReverseLookupDictionary(); + + dict.Set(1, "10"); + dict.Set(1, "20"); + dict.Set(3, "30"); + dict.Set(4, "20"); + + Assert.AreEqual("20", dict.GetValue(1)); + Assert.AreEqual("30", dict.GetValue(3)); + Assert.AreEqual("40", dict.GetValue(4)); + + CollectionAssert.AreEquivalent(new[]{1,4}, dict.GetKeys("20")); + CollectionAssert.AreEquivalent(new[]{3}, dict.GetKeys("30")); + } + + [Test] + public void GetKeysTest() + { + ReverseLookupDictionary dict = new ReverseLookupDictionary(); + + dict.Set(1, "10"); + dict.Set(1, "Odd"); + dict.Add(2, "Even"); + dict.Set(3, "Odd"); + dict.Add(4, "Value"); + dict.Set(4, "Even"); + dict.Add(5, "Odd"); + dict.Add(6, "Even"); + + CollectionAssert.AreEquivalent(new[]{1,3,5}, dict.GetKeys("Odd")); + CollectionAssert.AreEquivalent(new[]{2,4,6}, dict.GetKeys("Even")); + } + + [Test] + public void GetValueTest() + { + ReverseLookupDictionary dict = new ReverseLookupDictionary(); + + dict.Set(1, "10"); + dict.Set(1, "20"); + dict.Set(3, "30"); + dict.Set(2, "20"); + + Assert.AreEqual("20", dict.GetValue(1)); + Assert.AreEqual("30", dict.GetValue(3)); + Assert.AreEqual("20", dict.GetValue(2)); + } + + [Test] + public void RemoveKeyTest() + { + ReverseLookupDictionary dict = new ReverseLookupDictionary(); + + dict.Add(1, "10"); + + Assert.AreEqual(1, dict.Count); + + dict.RemoveKey(1); + + Assert.AreEqual(0, dict.Count); + } + + [Test] + public void RemoveValueSingleTest() + { + ReverseLookupDictionary dict = new ReverseLookupDictionary(); + + dict.Add(1, "10"); + + Assert.AreEqual(1, dict.Count); + + dict.RemoveValue("10"); + + Assert.AreEqual(0, dict.Count); + } + + [Test] + public void RemoveValueMultipleTest1() + { + ReverseLookupDictionary dict = new ReverseLookupDictionary(); + + dict.Add(1, "10"); + dict.Add(2, "10"); + dict.Add(3, "10"); + + Assert.AreEqual(3, dict.Count); + + dict.RemoveValue("10"); + + Assert.AreEqual(0, dict.Count); + } + + [Test] + public void RemoveValueWithOthersTest() + { + ReverseLookupDictionary dict = new ReverseLookupDictionary(); + + dict.Add(1, "10"); + dict.Add(2, "10"); + dict.Add(3, "10"); + dict.Add(4, "20"); + dict.Add(5, "20"); + dict.Add(6, "some other string"); + + Assert.AreEqual(6, dict.Count); + + dict.RemoveValue("10"); + + Assert.AreEqual(3, dict.Count); + } + + [Test] + public void TryGetValueTest() + { + ReverseLookupDictionary dict = new ReverseLookupDictionary(); + + string value; + Assert.IsFalse(dict.TryGetValue(1, out value)); + // ReSharper disable once ExpressionIsAlwaysNull + Assert.AreEqual(null, value); + + dict.Add(1, "10"); + + Assert.IsTrue(dict.TryGetValue(1, out value)); + Assert.AreEqual("10", value); + } + + [Test] + public void TryGetKeysMultipleTest() + { + ReverseLookupDictionary dict = new ReverseLookupDictionary(); + + IEnumerable value; + Assert.IsFalse(dict.TryGetKeys("10", out value)); + + dict.Add(1, "10"); + dict.Add(11, "100"); + + Assert.IsTrue(dict.TryGetKeys("10", out value)); + CollectionAssert.AreEquivalent(new[]{1}, value); + Assert.IsTrue(dict.TryGetKeys("100", out value)); + CollectionAssert.AreEquivalent(new[]{11}, value); + + + dict.Add(2, "10"); + dict.Add(3, "10"); + dict.Add(12, "100"); + dict.Add(13, "100"); + + Assert.IsTrue(dict.TryGetKeys("10", out value)); + CollectionAssert.AreEquivalent(new[]{1,2,3}, value); + + Assert.IsTrue(dict.TryGetKeys("100", out value)); + CollectionAssert.AreEquivalent(new[]{11,12,13}, value); + + Assert.IsFalse(dict.TryGetKeys("string", out value)); + + } + + [Test] + public void TryGetKeysSingleTest() + { + ReverseLookupDictionary dict = new ReverseLookupDictionary(); + + IEnumerable value; + Assert.IsFalse(dict.TryGetKeys("10", out value)); + + dict.Add(1, "10"); + + Assert.IsTrue(dict.TryGetKeys("10", out value)); + CollectionAssert.AreEquivalent(new[]{1}, value); + } + + #endregion + } +} \ No newline at end of file diff --git a/ICD.Common.Utils/Collections/ReverseLookupDictionary.cs b/ICD.Common.Utils/Collections/ReverseLookupDictionary.cs new file mode 100644 index 0000000..cab5deb --- /dev/null +++ b/ICD.Common.Utils/Collections/ReverseLookupDictionary.cs @@ -0,0 +1,299 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using ICD.Common.Properties; +using ICD.Common.Utils.Extensions; +#if !SIMPLSHARP +using System.Diagnostics; +#endif + +namespace ICD.Common.Utils.Collections +{ +#if !SIMPLSHARP + [DebuggerDisplay("Count = {Count}")] +#endif + public class ReverseLookupDictionary : IDictionary + { + private readonly Dictionary m_KeyToValue; + private readonly Dictionary> m_ValueToKeys; + + #region Properties + + public int Count { get { return m_KeyToValue.Count; } } + + public bool IsReadOnly { get { return false; } } + + [NotNull] + public ICollection Keys { get { return m_KeyToValue.Keys; } } + + [NotNull] + public ICollection Values { get { return m_ValueToKeys.Keys; } } + + #endregion + + /// + /// Constructor. + /// + public ReverseLookupDictionary() : + this(EqualityComparer.Default, EqualityComparer.Default) + { + } + + /// + /// Constructor + /// + /// + /// + public ReverseLookupDictionary(IEqualityComparer keyComparer, IEqualityComparer valueComparer) + { + m_KeyToValue = new Dictionary(keyComparer); + m_ValueToKeys = new Dictionary>(valueComparer); + } + + /// + /// Constructor. + /// + /// + /// + /// + public ReverseLookupDictionary([NotNull] Dictionary dict, IEqualityComparer keyComparer, IEqualityComparer valueComparer) + : this(keyComparer, valueComparer) + { + if (dict == null) + throw new ArgumentNullException("dict"); + + foreach (KeyValuePair kvp in dict) + Add(kvp.Key, kvp.Value); + } + + /// + /// Constructor. + /// + /// + public ReverseLookupDictionary([NotNull] Dictionary dict) + : this(dict, EqualityComparer.Default, EqualityComparer.Default) + { + } + + #region Methods + + public void Clear() + { + m_KeyToValue.Clear(); + m_ValueToKeys.Clear(); + } + + public bool ContainsKey([NotNull] TKey key) + { +// ReSharper disable CompareNonConstrainedGenericWithNull + if (key == null) +// ReSharper restore CompareNonConstrainedGenericWithNull + throw new ArgumentNullException("key"); + + return m_KeyToValue.ContainsKey(key); + } + + public bool ContainsValue([NotNull] TValue value) + { + return m_ValueToKeys.ContainsKey(value); + } + + public IEnumerable>> GetReverseDictionary() + { + // Cast stuff a layer deep cause weird + return m_ValueToKeys.Select(kvp => new KeyValuePair>(kvp.Key, kvp.Value.ToArray(kvp.Value.Count))); + } + + public void Add([NotNull] TKey key, [NotNull] TValue value) + { +// ReSharper disable CompareNonConstrainedGenericWithNull + if (key == null) +// ReSharper restore CompareNonConstrainedGenericWithNull + throw new ArgumentNullException("key"); + +// ReSharper disable CompareNonConstrainedGenericWithNull + if (value == null) +// ReSharper restore CompareNonConstrainedGenericWithNull + throw new ArgumentNullException("value"); + + if (ContainsKey(key)) + throw new ArgumentException("Key is already present in the collection", "key"); + + + m_KeyToValue.Add(key, value); + m_ValueToKeys.GetOrAddNew(value, () => new IcdHashSet()).Add(key); + } + + public void Set([NotNull] TKey key, [NotNull] TValue value) + { +// ReSharper disable CompareNonConstrainedGenericWithNull + if (key == null) +// ReSharper restore CompareNonConstrainedGenericWithNull + throw new ArgumentNullException("key"); + +// ReSharper disable CompareNonConstrainedGenericWithNull + if (value == null) +// ReSharper restore CompareNonConstrainedGenericWithNull + throw new ArgumentNullException("value"); + + RemoveKey(key); + + Add(key, value); + } + + [NotNull] + public IEnumerable GetKeys([NotNull] TValue value) + { +// ReSharper disable CompareNonConstrainedGenericWithNull + if (value == null) +// ReSharper restore CompareNonConstrainedGenericWithNull + throw new ArgumentNullException("value"); + + return m_ValueToKeys[value]; + } + + [NotNull] + public TValue GetValue([NotNull] TKey key) + { +// ReSharper disable CompareNonConstrainedGenericWithNull + if (key == null) +// ReSharper restore CompareNonConstrainedGenericWithNull + throw new ArgumentNullException("key"); + + return m_KeyToValue[key]; + } + + public bool RemoveKey([NotNull] TKey key) + { +// ReSharper disable CompareNonConstrainedGenericWithNull + if (key == null) +// ReSharper restore CompareNonConstrainedGenericWithNull + throw new ArgumentNullException("key"); + + if (!ContainsKey(key)) + return false; + + TValue value = m_KeyToValue[key]; + + m_KeyToValue.Remove(key); + + IcdHashSet keys = m_ValueToKeys[value]; + keys.Remove(key); + + if (keys.Count == 0) + m_ValueToKeys.Remove(value); + + return true; + } + + /// + /// Removes the value from the collection, and any keys that were using it + /// + /// + /// true if items were removed, false if not + /// + public bool RemoveValue([NotNull] TValue value) + { +// ReSharper disable CompareNonConstrainedGenericWithNull + if (value == null) +// ReSharper restore CompareNonConstrainedGenericWithNull + throw new ArgumentNullException("value"); + + if (!ContainsValue(value)) + return false; + + IcdHashSet keys = m_ValueToKeys[value]; + + m_ValueToKeys.Remove(value); + + foreach (TKey key in keys) + { + m_KeyToValue.Remove(key); + } + + return true; + } + + public bool TryGetValue([NotNull] TKey key, out TValue value) + { +// ReSharper disable CompareNonConstrainedGenericWithNull + if (key == null) +// ReSharper restore CompareNonConstrainedGenericWithNull + throw new ArgumentNullException("key"); + + return m_KeyToValue.TryGetValue(key, out value); + } + + public bool TryGetKeys([NotNull] TValue value, out IEnumerable keys) + { +// ReSharper disable CompareNonConstrainedGenericWithNull + if (value == null) +// ReSharper restore CompareNonConstrainedGenericWithNull + throw new ArgumentNullException("value"); + + IcdHashSet keysInternal; + if (m_ValueToKeys.TryGetValue(value, out keysInternal)) + { + keys = keysInternal; + return true; + } + keys = Enumerable.Empty(); + return false; + } + + #endregion + + #region IDictionary + + [NotNull] + TValue IDictionary.this[[NotNull] TKey key] { get { return GetValue(key); } set { Set(key, value); } } + + bool IDictionary.Remove([NotNull] TKey key) + { + return RemoveKey(key); + } + + #endregion + + #region ICollection + + void ICollection>.Add(KeyValuePair item) + { + Add(item.Key, item.Value); + } + + bool ICollection>.Contains(KeyValuePair item) + { + return (m_KeyToValue as IDictionary).Contains(item); + } + + bool ICollection>.Remove(KeyValuePair item) + { + return RemoveKey(item.Key); + } + + void ICollection>.CopyTo([NotNull] KeyValuePair[] array, int arrayIndex) + { + (m_KeyToValue as IDictionary).CopyTo(array, arrayIndex); + } + + #endregion + + #region IEnumerable + + [NotNull] + public IEnumerator> GetEnumerator() + { + return m_KeyToValue.GetEnumerator(); + } + + [NotNull] + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + #endregion + } +} \ No newline at end of file diff --git a/ICD.Common.Utils/ICD.Common.Utils_SimplSharp.csproj b/ICD.Common.Utils/ICD.Common.Utils_SimplSharp.csproj index 33e38a3..dd01d46 100644 --- a/ICD.Common.Utils/ICD.Common.Utils_SimplSharp.csproj +++ b/ICD.Common.Utils/ICD.Common.Utils_SimplSharp.csproj @@ -85,6 +85,7 @@ +