feat[Collections]: ReverseLookupDictionary for fast value access

This commit is contained in:
Drew Tingen
2023-06-14 06:09:58 -04:00
parent 4e430731cc
commit 73a716f9b9
3 changed files with 600 additions and 0 deletions

View File

@@ -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<int, string> dict = new ReverseLookupDictionary<int, string>();
dict.Set(1, "10");
dict.Set(2, "10");
dict.Set(3, "30");
Assert.AreEqual(2, dict.Count);
}
[Test]
public void KeysTest()
{
ReverseLookupDictionary<int, string> dict = new ReverseLookupDictionary<int, string>();
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<int, string> dict = new ReverseLookupDictionary<int, string>();
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<int, string> dict = new ReverseLookupDictionary<int, string>();
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<int, string> dict = new ReverseLookupDictionary<int, string>();
Assert.IsFalse(dict.ContainsKey(1));
dict.Set(1, "10");
Assert.IsTrue(dict.ContainsKey(1));
}
[Test]
public void ContainsValueTest()
{
ReverseLookupDictionary<int, string> dict = new ReverseLookupDictionary<int, string>();
Assert.IsFalse(dict.ContainsValue("10"));
dict.Set(1, "10");
Assert.IsTrue(dict.ContainsValue("10"));
}
[Test]
public void AddTest()
{
ReverseLookupDictionary<int, string> dict = new ReverseLookupDictionary<int, string>();
dict.Add(1, "10");
Assert.AreEqual(1, dict.Count);
Assert.Throws<ArgumentException>(() => dict.Add(1, "10"));
Assert.Throws<ArgumentException>(() => 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<int, string> dict = new ReverseLookupDictionary<int, string>();
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<int, string> dict = new ReverseLookupDictionary<int, string>();
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<int, string> dict = new ReverseLookupDictionary<int, string>();
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<int, string> dict = new ReverseLookupDictionary<int, string>();
dict.Add(1, "10");
Assert.AreEqual(1, dict.Count);
dict.RemoveKey(1);
Assert.AreEqual(0, dict.Count);
}
[Test]
public void RemoveValueSingleTest()
{
ReverseLookupDictionary<int, string> dict = new ReverseLookupDictionary<int, string>();
dict.Add(1, "10");
Assert.AreEqual(1, dict.Count);
dict.RemoveValue("10");
Assert.AreEqual(0, dict.Count);
}
[Test]
public void RemoveValueMultipleTest1()
{
ReverseLookupDictionary<int, string> dict = new ReverseLookupDictionary<int, string>();
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<int, string> dict = new ReverseLookupDictionary<int, string>();
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<int, string> dict = new ReverseLookupDictionary<int, string>();
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<int, string> dict = new ReverseLookupDictionary<int, string>();
IEnumerable<int> 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<int, string> dict = new ReverseLookupDictionary<int, string>();
IEnumerable<int> 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
}
}

View File

@@ -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<TKey, TValue> : IDictionary<TKey, TValue>
{
private readonly Dictionary<TKey, TValue> m_KeyToValue;
private readonly Dictionary<TValue, IcdHashSet<TKey>> m_ValueToKeys;
#region Properties
public int Count { get { return m_KeyToValue.Count; } }
public bool IsReadOnly { get { return false; } }
[NotNull]
public ICollection<TKey> Keys { get { return m_KeyToValue.Keys; } }
[NotNull]
public ICollection<TValue> Values { get { return m_ValueToKeys.Keys; } }
#endregion
/// <summary>
/// Constructor.
/// </summary>
public ReverseLookupDictionary() :
this(EqualityComparer<TKey>.Default, EqualityComparer<TValue>.Default)
{
}
/// <summary>
/// Constructor
/// </summary>
/// <param name="keyComparer"></param>
/// <param name="valueComparer"></param>
public ReverseLookupDictionary(IEqualityComparer<TKey> keyComparer, IEqualityComparer<TValue> valueComparer)
{
m_KeyToValue = new Dictionary<TKey, TValue>(keyComparer);
m_ValueToKeys = new Dictionary<TValue, IcdHashSet<TKey>>(valueComparer);
}
/// <summary>
/// Constructor.
/// </summary>
/// <param name="dict"></param>
/// <param name="keyComparer"></param>
/// <param name="valueComparer"></param>
public ReverseLookupDictionary([NotNull] Dictionary<TKey, TValue> dict, IEqualityComparer<TKey> keyComparer, IEqualityComparer<TValue> valueComparer)
: this(keyComparer, valueComparer)
{
if (dict == null)
throw new ArgumentNullException("dict");
foreach (KeyValuePair<TKey, TValue> kvp in dict)
Add(kvp.Key, kvp.Value);
}
/// <summary>
/// Constructor.
/// </summary>
/// <param name="dict"></param>
public ReverseLookupDictionary([NotNull] Dictionary<TKey, TValue> dict)
: this(dict, EqualityComparer<TKey>.Default, EqualityComparer<TValue>.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<KeyValuePair<TValue, IEnumerable<TKey>>> GetReverseDictionary()
{
// Cast stuff a layer deep cause weird
return m_ValueToKeys.Select(kvp => new KeyValuePair<TValue, IEnumerable<TKey>>(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<TKey>()).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<TKey> 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<TKey> keys = m_ValueToKeys[value];
keys.Remove(key);
if (keys.Count == 0)
m_ValueToKeys.Remove(value);
return true;
}
/// <summary>
/// Removes the value from the collection, and any keys that were using it
/// </summary>
/// <param name="value"></param>
/// <returns>true if items were removed, false if not</returns>
/// <exception cref="ArgumentNullException"></exception>
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<TKey> 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<TKey> keys)
{
// ReSharper disable CompareNonConstrainedGenericWithNull
if (value == null)
// ReSharper restore CompareNonConstrainedGenericWithNull
throw new ArgumentNullException("value");
IcdHashSet<TKey> keysInternal;
if (m_ValueToKeys.TryGetValue(value, out keysInternal))
{
keys = keysInternal;
return true;
}
keys = Enumerable.Empty<TKey>();
return false;
}
#endregion
#region IDictionary
[NotNull]
TValue IDictionary<TKey, TValue>.this[[NotNull] TKey key] { get { return GetValue(key); } set { Set(key, value); } }
bool IDictionary<TKey, TValue>.Remove([NotNull] TKey key)
{
return RemoveKey(key);
}
#endregion
#region ICollection
void ICollection<KeyValuePair<TKey, TValue>>.Add(KeyValuePair<TKey, TValue> item)
{
Add(item.Key, item.Value);
}
bool ICollection<KeyValuePair<TKey, TValue>>.Contains(KeyValuePair<TKey, TValue> item)
{
return (m_KeyToValue as IDictionary<TKey, TValue>).Contains(item);
}
bool ICollection<KeyValuePair<TKey, TValue>>.Remove(KeyValuePair<TKey, TValue> item)
{
return RemoveKey(item.Key);
}
void ICollection<KeyValuePair<TKey, TValue>>.CopyTo([NotNull] KeyValuePair<TKey, TValue>[] array, int arrayIndex)
{
(m_KeyToValue as IDictionary<TKey, TValue>).CopyTo(array, arrayIndex);
}
#endregion
#region IEnumerable
[NotNull]
public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator()
{
return m_KeyToValue.GetEnumerator();
}
[NotNull]
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
#endregion
}
}

View File

@@ -85,6 +85,7 @@
<Compile Include="Collections\IcdSortedDictionary.cs" />
<Compile Include="Collections\INotifyCollectionChanged.cs" />
<Compile Include="Collections\PriorityQueue.cs" />
<Compile Include="Collections\ReverseLookupDictionary.cs" />
<Compile Include="Collections\WeakKeyDictionary.cs" />
<Compile Include="Comparers\FileNameEqualityComparer.cs" />
<Compile Include="Comparers\PredicateComparer.cs" />