Fortunately I happened across a good solution from another blogger, Tim Barrass (Bidirectional Lookup). So, I began implementing it and then writing unit tests to cover it. In the process I uncovered a couple issues with adding new items and removing them that I corrected. So, here's the updated dictionary:
The changes are pretty straight-forward:
- Changed the secondList.ToList().Add(second); and firstList.ToList().Add(first); calls on lines 49 and 50 to Append to the IEnumerable<> and also to re-assign to the internal dictionaries.
- Rewrote the entire Remove() function to remove from the internal lists and if its the last entry remove the keys as well.
Code Snippet
- public class BidirectionalDictionary<TFirst, TSecond>
- {
- private IDictionary<TFirst, IEnumerable<TSecond>> Forward { get; set; }
- private IDictionary<TSecond, IEnumerable<TFirst>> Backward { get; set; }
- private static readonly IEnumerable<TFirst> EmptyFirstList = new List<TFirst>();
- private static readonly IEnumerable<TSecond> EmptySecondList = new List<TSecond>();
- public BidirectionalDictionary(IDictionary<TFirst, IEnumerable<TSecond>> forward, IDictionary<TSecond, IEnumerable<TFirst>> backward)
- {
- Forward = forward;
- Backward = backward;
- }
- public BidirectionalDictionary()
- {
- Forward = new Dictionary<TFirst, IEnumerable<TSecond>>();
- Backward = new Dictionary<TSecond, IEnumerable<TFirst>>();
- }
- public void Add(TFirst first, TSecond second)
- {
- IEnumerable<TSecond> secondList;
- if (!Forward.TryGetValue(first, out secondList))
- {
- secondList = new List<TSecond>();
- Forward[first] = secondList;
- }
- IEnumerable<TFirst> firstList;
- if (!Backward.TryGetValue(second, out firstList))
- {
- firstList = new List<TFirst>();
- Backward[second] = firstList;
- }
- Forward[first] = secondList.Append(second);
- Backward[second] = firstList.Append(first);
- }
- public IEnumerable<TSecond> GetByFirst(TFirst first)
- {
- return Forward.ContainsKey(first) ? Forward[first] : EmptySecondList;
- }
- public IEnumerable<TFirst> GetBySecond(TSecond second)
- {
- return Backward.ContainsKey(second) ? Backward[second] : EmptyFirstList;
- }
- public void Remove(TFirst first, TSecond second)
- {
- IEnumerable<TSecond> secondList;
- if (Forward.TryGetValue(first, out secondList))
- {
- if (secondList.Contains(second))
- {
- secondList.ToList().Remove(second);
- if (secondList.Any())
- Forward[first] = secondList;
- else
- Forward.Remove(first);
- }
- else
- Forward.Remove(first);
- }
- IEnumerable<TFirst> firstList;
- if (Backward.TryGetValue(second, out firstList))
- {
- if (firstList.Contains(first))
- {
- firstList.ToList().Remove(first);
- if (firstList.Any())
- Backward[second] = firstList;
- else
- Backward.Remove(second);
- }
- else
- Backward.Remove(second);
- }
- }
- }
And now the unit tests surrounding this code. I used NUnit for this project and as you can see covered nearly all of the behavior within the new class.
Code Snippet
- [TestFixture]
- public class BidirectionalDictionaryTests
- {
- private static BidirectionalDictionary<string, string> _dictionary;
- [SetUp]
- private void OnSetup()
- {
- _dictionary = new BidirectionalDictionary<string, string>();
- _dictionary.Add("FirstValue", "FirstLookupValue");
- }
- [TestCase("FirstValue", "FirstLookupValue", true)]
- [TestCase("SecondValue", "FirstLookupValue", false)]
- public void GetByFirstTest(string firstValue, string secondValue, bool expectedResult)
- {
- var results = _dictionary.GetByFirst(firstValue);
- Assert.That(results.ToList().Contains(secondValue), Is.EqualTo(expectedResult));
- }
- [TestCase("SecondValue", "FirstLookupValue", true)]
- [TestCase("SecondLookupValue", "FirstLookupValue", false)]
- public void GetBySecondTest(string secondValue, string firstValue, bool expectedResult)
- {
- var results = _dictionary.GetBySecond(secondValue);
- Assert.That(results.ToList().Contains(firstValue), Is.EqualTo(expectedResult));
- }
- [Test]
- public void Remove_Test()
- {
- _dictionary.Remove("FirstValue", "FirstLookupValue");
- var results = _dictionary.GetByFirst("FirstValue");
- Assert.That(results.ToList().Contains("FirstValue"), Is.False);
- results = _dictionary.GetBySecond("FirstLookupValue");
- Assert.That(results.ToList().Contains("FirstLookupValue"), Is.False);
- }
- }
Again, I want to thank Tim Barrass for the initial work on this dictionary. I hope this is helpful to anyone else as its worked great for me.
Two very enthusiastic thumbs up -- thanks for the link!
ReplyDelete