Tuesday, February 25, 2014

BiDirectional Dictionary

I had the need for a dictionary in a personal project recently, but I ran into a bit of a snag.  I needed to be able to look up values from either direction.  That is, both forward (Key to Value) and backward (Value to Key).  Your standard C# Dictionary out of the box doesn't support this behavior.

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:

  1. 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.
  2. Rewrote the entire Remove() function to remove from the internal lists and if its the last entry remove the keys as well.

Code Snippet
  1. public class BidirectionalDictionary<TFirst, TSecond>
  2. {
  3.     private IDictionary<TFirst, IEnumerable<TSecond>> Forward { get; set; }
  4.     private IDictionary<TSecond, IEnumerable<TFirst>> Backward { get; set; }
  5.  
  6.     private static readonly IEnumerable<TFirst> EmptyFirstList = new List<TFirst>();
  7.     private static readonly IEnumerable<TSecond> EmptySecondList = new List<TSecond>();
  8.  
  9.     public BidirectionalDictionary(IDictionary<TFirst, IEnumerable<TSecond>> forward, IDictionary<TSecond, IEnumerable<TFirst>> backward)
  10.     {
  11.         Forward = forward;
  12.         Backward = backward;
  13.     }
  14.  
  15.     public BidirectionalDictionary()
  16.     {
  17.         Forward = new Dictionary<TFirst, IEnumerable<TSecond>>();
  18.         Backward = new Dictionary<TSecond, IEnumerable<TFirst>>();
  19.     }
  20.  
  21.     public void Add(TFirst first, TSecond second)
  22.     {
  23.         IEnumerable<TSecond> secondList;
  24.         if (!Forward.TryGetValue(first, out secondList))
  25.         {
  26.             secondList = new List<TSecond>();
  27.             Forward[first] = secondList;
  28.         }
  29.  
  30.         IEnumerable<TFirst> firstList;
  31.         if (!Backward.TryGetValue(second, out firstList))
  32.         {
  33.             firstList = new List<TFirst>();
  34.             Backward[second] = firstList;
  35.         }
  36.  
  37.         Forward[first] = secondList.Append(second);
  38.         Backward[second] = firstList.Append(first);
  39.     }
  40.  
  41.     public IEnumerable<TSecond> GetByFirst(TFirst first)
  42.     {
  43.         return Forward.ContainsKey(first) ? Forward[first] : EmptySecondList;
  44.     }
  45.  
  46.     public IEnumerable<TFirst> GetBySecond(TSecond second)
  47.     {
  48.         return Backward.ContainsKey(second) ? Backward[second] : EmptyFirstList;
  49.     }
  50.  
  51.     public void Remove(TFirst first, TSecond second)
  52.     {
  53.         IEnumerable<TSecond> secondList;
  54.         if (Forward.TryGetValue(first, out secondList))
  55.         {
  56.             if (secondList.Contains(second))
  57.             {
  58.                 secondList.ToList().Remove(second);
  59.                 if (secondList.Any())
  60.                     Forward[first] = secondList;
  61.                 else
  62.                     Forward.Remove(first);
  63.             }
  64.             else
  65.                 Forward.Remove(first);
  66.         }
  67.  
  68.         IEnumerable<TFirst> firstList;
  69.         if (Backward.TryGetValue(second, out firstList))
  70.         {
  71.             if (firstList.Contains(first))
  72.             {
  73.                 firstList.ToList().Remove(first);
  74.                 if (firstList.Any())
  75.                     Backward[second] = firstList;
  76.                 else
  77.                     Backward.Remove(second);
  78.             }
  79.             else
  80.                 Backward.Remove(second);
  81.         }
  82.     }
  83. }

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
  1. [TestFixture]
  2. public class BidirectionalDictionaryTests
  3. {
  4.     private static BidirectionalDictionary<string, string> _dictionary;
  5.         
  6.     [SetUp]
  7.     private void OnSetup()
  8.     {
  9.         _dictionary = new BidirectionalDictionary<string, string>();
  10.         _dictionary.Add("FirstValue", "FirstLookupValue");
  11.     }
  12.  
  13.     [TestCase("FirstValue", "FirstLookupValue", true)]
  14.     [TestCase("SecondValue", "FirstLookupValue", false)]
  15.     public void GetByFirstTest(string firstValue, string secondValue, bool expectedResult)
  16.     {
  17.         var results = _dictionary.GetByFirst(firstValue);
  18.  
  19.         Assert.That(results.ToList().Contains(secondValue), Is.EqualTo(expectedResult));
  20.     }
  21.  
  22.     [TestCase("SecondValue", "FirstLookupValue", true)]
  23.     [TestCase("SecondLookupValue", "FirstLookupValue", false)]
  24.     public void GetBySecondTest(string secondValue, string firstValue, bool expectedResult)
  25.     {
  26.         var results = _dictionary.GetBySecond(secondValue);
  27.  
  28.         Assert.That(results.ToList().Contains(firstValue), Is.EqualTo(expectedResult));
  29.     }
  30.  
  31.     [Test]
  32.     public void Remove_Test()
  33.     {
  34.         _dictionary.Remove("FirstValue", "FirstLookupValue");
  35.  
  36.         var results = _dictionary.GetByFirst("FirstValue");
  37.  
  38.         Assert.That(results.ToList().Contains("FirstValue"), Is.False);
  39.  
  40.         results = _dictionary.GetBySecond("FirstLookupValue");
  41.  
  42.         Assert.That(results.ToList().Contains("FirstLookupValue"), Is.False);
  43.     }
  44. }

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.

1 comment :