4
\$\begingroup\$

Inspired by this question and its answers, I've made my own version.

  /// <summary>
  /// A one-one relation bidirectional map.
  /// <para>
  /// A one-one relation means that each entry of type <typeparamref name="TFirst"/> can correspond to exactly one 
  /// entry of type <typeparamref name="TSecond"/> and visa versa.
  /// </para>
  /// The map doesn't support null objects because each element is both key and value in its relation and keys can't be null.
  /// </summary>
  /// <typeparam name="TFirst">Any type</typeparam>
  /// <typeparam name="TSecond">Any type</typeparam>
  public class BidirectionalMap<TFirst, TSecond> : IEnumerable<KeyValuePair<TFirst, TSecond>>
  {
    private readonly Dictionary<TFirst, TSecond> primary;
    private readonly Dictionary<TSecond, TFirst> secondary;

    public BidirectionalMap()
    {
      primary = new Dictionary<TFirst, TSecond>();
      secondary = new Dictionary<TSecond, TFirst>();
    }

    /// <summary>
    /// Creates a BidirectionalMap initialized with the specified <paramref name="capacity"/>.
    /// </summary>
    /// <param name="capacity">The desired capacity for the map.</param>
    /// <exception cref="ArgumentOutOfRangeException">If capacity is out of range (&lt; 0)</exception>
    public BidirectionalMap(int capacity)
    {
      primary = new Dictionary<TFirst, TSecond>(capacity);
      secondary = new Dictionary<TSecond, TFirst>(capacity);
    }

    /// <summary>
    /// Creates a BidirectionalMap with the specified equality comparers.
    /// </summary>
    /// <param name="firstComparer">Equality comparer for <typeparamref name="TFirst"/>. If null, the default comparer is used.</param>
    /// <param name="secondComparer">Equality comparer for <typeparamref name="TSecond"/>. If null, the default comparer is used.</param>
    public BidirectionalMap(IEqualityComparer<TFirst> firstComparer, IEqualityComparer<TSecond> secondComparer)
    {
      primary = new Dictionary<TFirst, TSecond>(firstComparer);
      secondary = new Dictionary<TSecond, TFirst>(secondComparer);
    }

    /// <summary>
    /// Creates a BidirectionalMap from the <paramref name="source"/> dictionary.
    /// </summary>
    /// <param name="source">The source dictionary from which to create a one-one relation map</param>
    /// <exception cref="ArgumentException">If <paramref name="source"/> contains doublets in values</exception>
    /// <exception cref="ArgumentNullException">If <paramref name="source"/> contains null values</exception>
    public BidirectionalMap(IDictionary<TFirst, TSecond> source)
    {
      primary = new Dictionary<TFirst, TSecond>(source);
      secondary = source.ToDictionary(kvp => kvp.Value, kvp => kvp.Key);
    }

    /// <summary>
    /// Creates a BidirectionalMap from the <paramref name="inverseSource"/> dictionary.
    /// </summary>
    /// <param name="inverseSource">The source dictionary from which to create a one-one relation map</param>
    /// <exception cref="ArgumentException">If <paramref name="inverseSource"/> contains doublets in values</exception>
    /// <exception cref="ArgumentNullException">If <paramref name="inverseSource"/> contains null values</exception>
    public BidirectionalMap(IDictionary<TSecond, TFirst> inverseSource)
    {
      primary = inverseSource.ToDictionary(kvp => kvp.Value, kvp => kvp.Key);
      secondary = new Dictionary<TSecond, TFirst>(inverseSource);
    }

    public int Count => primary.Count;

    public ICollection<TFirst> PrimaryKeys => primary.Keys;
    public ICollection<TSecond> SecondaryKeys => secondary.Keys;

    // This should be useful only for enumeration by TSecond as key
    public IReadOnlyDictionary<TSecond, TFirst> Inverse => secondary;

    public TSecond this[TFirst first]
    {
      get { return primary[first]; }
      set { Set(first, value); }
    }

    public TFirst this[TSecond second]
    {
      get { return secondary[second]; }
      set { Set(value, second); }
    }

    private void Set(TFirst first, TSecond second)
    {
      // Remove both the entries related to first and second if any
      Remove(first);
      Remove(second);
      // Now it should be safe to add the new relation.
      Add(first, second);
    }

    public void Add(TFirst first, TSecond second)
    {
      try
      {
        primary.Add(first, second);
      }
      catch (ArgumentNullException)
      {
        // If first is null, we end here and can rethrow with no harm done.
        throw new ArgumentNullException(nameof(first));
      }
      catch (ArgumentException)
      {
        // If the key is present in primary, then we end here and can rethrow with no harm done.
        throw new ArgumentException(nameof(first), $"{first} already present in the dictionary");
      }

      try
      {
        secondary.Add(second, first);
      }
      catch (ArgumentNullException)
      {
        // If second is null, we end here, and primary must be rolled back - because first was added successfully
        primary.Remove(first);
        throw new ArgumentNullException(nameof(second));
      }
      catch (ArgumentException)
      {
        // If second exists in secondary, secondary throws, and primary must be rolled back - because first was added successfully
        primary.Remove(first);
        throw new ArgumentException(nameof(second), $"{second} already present in the dictionary");
      }
    }

    public bool Remove(TFirst first)
    {
      if (primary.TryGetValue(first, out var second))
      {
        secondary.Remove(second);
        primary.Remove(first);
        return true;
      }

      return false;
    }

    public bool Remove(TSecond second)
    {
      if (secondary.TryGetValue(second, out var first))
      {
        primary.Remove(first);
        secondary.Remove(second);
        return true;
      }

      return false;
    }

    public bool TryGetValue(TFirst first, out TSecond second)
    {
      return primary.TryGetValue(first, out second);
    }

    public bool TryGetValue(TSecond second, out TFirst first)
    {
      return secondary.TryGetValue(second, out first);
    }

    public bool Contains(TFirst first)
    {
      return primary.ContainsKey(first);
    }

    public bool Contains(TSecond second)
    {
      return secondary.ContainsKey(second);
    }

    public void Clear()
    {
      primary.Clear();
      secondary.Clear();
    }

    public IEnumerator<KeyValuePair<TFirst, TSecond>> GetEnumerator()
    {
      return primary.GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
      return GetEnumerator();
    }
  }

It doesn't implement IDictionary<T, S> for either directions, but has just about the same public interface for both. The directions should therefore be regarded as equal.


Here are a set of unit tests - not complete but covering the most parts:

  [TestClass]
  public class BidirectionalMapTests
  {
    class TestObject<T>
    {
      public TestObject(T value)
      {
        Value = value;
      }

      public T Value { get; }

      public static implicit operator T(TestObject<T> to) => to.Value;
      public static implicit operator TestObject<T>(T value) => new TestObject<T>(value);

      public override string ToString()
      {
        return Value?.ToString() ?? "";
      }
    }

    [TestMethod]
    public void InitializeFromSourceDictionary()
    {
      Dictionary<string, int> source = new Dictionary<string, int>
      {
        { "a", 1 },
        { "b", 2 }
      };

      BidirectionalMap<string, int> map = new BidirectionalMap<string, int>(source);

      Assert.AreEqual(1, map["a"]);
      Assert.AreEqual("b", map[2]);
    }

    [TestMethod]
    public void InvalidInitializeFromSourceDictionary()
    {
      TestObject<string> one = new TestObject<string>("1");

      Dictionary<string, TestObject<string>> source = new Dictionary<string, TestObject<string>>
      {
        { "a", one },
        { "b", one }
      };

      BidirectionalMap<string, TestObject<string>> map = null;

      Assert.ThrowsException<ArgumentException>(() => map = new BidirectionalMap<string, TestObject<string>>(source));

      Dictionary<TestObject<string>, string> inverseSource = new Dictionary<TestObject<string>, string>
      {
        { "a", "1" },
        { "b", "1" }
      };

      Assert.ThrowsException<ArgumentException>(() => map = new BidirectionalMap<string, TestObject<string>>(inverseSource));

      source = new Dictionary<string, TestObject<string>>
      {
        { "a", null },
        { "b", "1" }
      };

      Assert.ThrowsException<ArgumentNullException>(() => map = new BidirectionalMap<string, TestObject<string>>(source));
    }

    [TestMethod]
    public void Add()
    {
      BidirectionalMap<string, int> map = new BidirectionalMap<string, int>();

      map.Add("a", 1);
      map.Add("b", 2);

      Assert.AreEqual(1, map["a"]);
      Assert.AreEqual("b", map[2]);
      Assert.AreEqual(2, map.Count);
    }

    [TestMethod]
    public void InvalidAdd()
    {
      BidirectionalMap<string, int> map = new BidirectionalMap<string, int>();

      map.Add("a", 1);
      Assert.ThrowsException<ArgumentException>(() => map.Add("a", 2));
      Assert.ThrowsException<ArgumentException>(() => map.Add("b", 1));
      Assert.AreEqual(1, map["a"]);
    }

    [TestMethod]
    public void AddNull()
    {
      BidirectionalMap<string, string> map = new BidirectionalMap<string, string>();

      Assert.ThrowsException<ArgumentNullException>(() => map.Add(null, "a"));
      Assert.ThrowsException<ArgumentNullException>(() => map.Add("a", null));
      Assert.AreEqual(0, map.Count);
    }

    [TestMethod]
    public void Remove()
    {
      BidirectionalMap<string, int> map = new BidirectionalMap<string, int>();

      map.Add("a", 1);
      map.Add("b", 2);
      Assert.AreEqual(2, map.Count);

      map.Remove("a");
      Assert.AreEqual(1, map.Count);

      map.Remove(2);
      Assert.AreEqual(0, map.Count);
    }

    [TestMethod]
    public void RemoveNonExistingValue()
    {
      BidirectionalMap<string, int> map = new BidirectionalMap<string, int>();

      map.Add("a", 1);
      map.Add("b", 2);

      Assert.IsFalse(map.Remove("c"));
      Assert.AreEqual(2, map.Count);
    }

    [TestMethod]
    public void Set()
    {
      BidirectionalMap<string, int> map = new BidirectionalMap<string, int>();

      map.Add("a", 1);
      map.Add("b", 2);

      map["a"] = 3;

      Assert.AreEqual(2, map.Count);
      Assert.IsTrue(map.TryGetValue("a", out int second));
      Assert.AreEqual(3, second);
      Assert.IsTrue(map.TryGetValue(3, out string first));
      Assert.AreEqual("a", first);

      Assert.IsFalse(map.TryGetValue(1, out _));
    }

    [TestMethod]
    public void SetWithExistingSecondValue()
    {
      BidirectionalMap<string, int> map = new BidirectionalMap<string, int>();

      map.Add("a", 1);
      map.Add("b", 2);

      map["a"] = 2;

      Assert.AreEqual(1, map.Count);
      Assert.IsTrue(map.TryGetValue("a", out int second));
      Assert.AreEqual(2, second);
      Assert.IsTrue(map.TryGetValue(2, out string first));
      Assert.AreEqual("a", first);

      Assert.IsFalse(map.TryGetValue("b", out _));
    }

    [TestMethod]
    public void TryGetValue()
    {
      BidirectionalMap<string, int> map = new BidirectionalMap<string, int>
      {
        { "a", 1 },
        { "b", 2 }
      };

      Assert.IsTrue(map.TryGetValue("a", out int second));
      Assert.AreEqual(1, second);

      Assert.IsTrue(map.TryGetValue(2, out string first));
      Assert.AreEqual("b", first);

      Assert.IsFalse(map.TryGetValue("c", out _));
      Assert.IsFalse(map.TryGetValue(3, out _));
    }
  }

Any comments are welcome, but the implementation of Add(), Set() and Remove() are the most vulnerable parts.

I think that the naming TFirst, TSecond, primary and secondary could be better in order to reflect their equal status, but I didn't find any better. May be you have any suggestions?

\$\endgroup\$
  • 2
    \$\begingroup\$ This solution is an example of how enthusiasm can make one blind about the obvious pitfalls - as mentioned by Peter in his answer - embarrassing. \$\endgroup\$ – Henrik Hansen Sep 5 at 8:18
  • 2
    \$\begingroup\$ I know this feeling. Code Review is sometimes cruel ;-P \$\endgroup\$ – t3chb0t Sep 5 at 8:32
  • \$\begingroup\$ for the naming: en.wikipedia.org/wiki/Bijection they mention X and Y. But I'm not sure we should use these mathematical names as member names in C#. What do you think? \$\endgroup\$ – dfhwze Sep 5 at 11:44
  • \$\begingroup\$ @dfhwze: I don't like single char names, but they are at least neutral. I'll consider that. \$\endgroup\$ – Henrik Hansen Sep 5 at 12:14
4
\$\begingroup\$

If TFirst and TSecond are the same, nearly all of the API becomes useless because the compiler can't disambiguate the method calls.

I think a better design would be for Inverse to be a BidirectionalMap<TSecond, TFirst>, so that the methods don't need to be duplicated. Then one obvious unit test would be ReferenceEquals(map, map.Inverse.Inverse). And I think the obstacle to implementing at least IReadOnlyDictionary<TFirst, TSecond> would have been removed.


Add seems overly complicated and slightly fragile. I don't think the tiny performance improvement justifies the complexity over

    public void Add(TFirst first, TSecond second)
    {
      if (first == null) throw new ArgumentNullException(nameof(first));
      if (primary.ContainsKey(first)) throw new ArgumentException(nameof(first), $"{first} already present in the dictionary");
      if (second == null) throw new ArgumentNullException(nameof(second));
      if (secondary.ContainsKey(second)) throw new ArgumentException(nameof(second), $"{second} already present in the dictionary");

      primary.Add(first, second);
      secondary.Add(second, first);
    }
\$\endgroup\$
  • \$\begingroup\$ Oh, I just realized that you mentioned the same bug but with different examples hehe. \$\endgroup\$ – t3chb0t Sep 5 at 8:10
  • 1
    \$\begingroup\$ @HenrikHansen then let it be a secret review ;) \$\endgroup\$ – t3chb0t Sep 5 at 8:21
4
\$\begingroup\$

Set and Remove are compact and clean code. Add is convoluted with these exception handlers:

  try
  {
    primary.Add(first, second);
  }
  catch (ArgumentNullException)
  {
    // If first is null, we end here and can rethrow with no harm done.
    throw new ArgumentNullException(nameof(first));
  }
  catch (ArgumentException)
  {
    // If the key is present in primary, then we end here and can rethrow with no harm done.
    throw new ArgumentException(nameof(first), $"{first} already present in the dictionary");
  }

I would opt for calling sand-box methods TryAdd and Remove instead:

public void Add(TFirst first, TSecond second)
{
    if (!primary.TryAdd(first, second) && !secondary.TryAdd(second, first))
    {
        primary.Remove(first);
        throw new InvalidOperationException("The tuple violates the bijective constraint");
    }
}

You can make the body a bit more verbose if you which to notify the caller which of the arguments violates the constraint.

public void Add(TFirst first, TSecond second)
{
    if (!primary.TryAdd(first, second))
    {
        throw new InvalidOperationException("The first arg violates the bijective constraint");
    }

    if (!secondary.TryAdd(second, first))
    {
        primary.Remove(first);
        throw new InvalidOperationException("The second arg violates the bijective constraint");
    }
}

You could even check for a combination if you really need to:

public void Add(TFirst first, TSecond second)
{
    var primaryAdded = primary.TryAdd(first, second);
    var secondaryAdded = secondary.TryAdd(second, first);

    if (primaryAdded && !secondaryAdded)
    {
        primary.Remove(first);
        throw new InvalidOperationException("The second arg violates the bijective constraint");
    }

    if (!primaryAdded && secondaryAdded)
    {
        secondary.Remove(second);
        throw new InvalidOperationException("The first arg violates the bijective constraint");
    }

    if (!primaryAdded && !secondaryAdded)
    {
        throw new InvalidOperationException("Both args violate the bijective constraint");
    }
}
\$\endgroup\$
  • 1
    \$\begingroup\$ I didn't know about TryAdd() as it is still in preview for .NET Standard. But I could maybe make an extension. My argument for the convoluted approach was to let the dictionaries handle invalid input for performance reasons. Personally I don't find it that convoluted - the workflow and logic is easy followed IMO. I'll make a self-answer later in the evening that - I think - will build on the idea of an Inverse. \$\endgroup\$ – Henrik Hansen Sep 5 at 11:58
2
\$\begingroup\$

My question is a disaster, and I don't know where my thoughts were while writing it. Funny enough I managed to write a lot of unittest even with the same type for the two keys without getting any compiler errors. Anyway below is a new version that builds on the Inverse concept, and it seems to do the trick all the way - but I'm sure someone can find something to criticize. I have experimented with the naming, but I'm not sure if they are final.

  /// <summary>
  /// A one-one relation bidirectional map.
  /// <para>
  /// A one-one relation means that each entry of type <typeparamref name="TPrimary"/> can correspond to exactly one 
  /// entry of type <typeparamref name="TSecondary"/> and visa versa.
  /// </para>
  /// The map doesn't support null elements because each element is both key and value in its relation and keys can't be null.
  /// </summary>
  /// <typeparam name="TPrimary">Any type</typeparam>
  /// <typeparam name="TSecondary">Any type</typeparam>
  public sealed class BidirectionalMap<TPrimary, TSecondary> : IEnumerable<KeyValuePair<TPrimary, TSecondary>>
  {
    private readonly Dictionary<TPrimary, TSecondary> map;
    private readonly Dictionary<TSecondary, TPrimary> inverseMap;

    /// <summary>
    /// Creates a BidirectionalMap from the provided dictionaries.
    /// Should be used only to create the Inverse.
    /// </summary>
    /// <param name="map"></param>
    /// <param name="inverseMap"></param>
    private BidirectionalMap(BidirectionalMap<TSecondary, TPrimary> inverse, Dictionary<TPrimary, TSecondary> map, Dictionary<TSecondary, TPrimary> inverseMap)
    {
      this.map = map;
      this.inverseMap = inverseMap;
      Inverse = inverse;
    }

    private BidirectionalMap(int capacity, IEqualityComparer<TPrimary> primaryComparer, IEqualityComparer<TSecondary> secondaryComparer)
    {
      map = new Dictionary<TPrimary, TSecondary>(capacity, primaryComparer);
      inverseMap = new Dictionary<TSecondary, TPrimary>(capacity, secondaryComparer);
      Inverse = new BidirectionalMap<TSecondary, TPrimary>(this, inverseMap, map);
    }

    public BidirectionalMap()
    {
      map = new Dictionary<TPrimary, TSecondary>();
      inverseMap = new Dictionary<TSecondary, TPrimary>();
      Inverse = new BidirectionalMap<TSecondary, TPrimary>(this, inverseMap, map);
    }

    /// <summary>
    /// Creates a BidirectionalMap initialized with the specified <paramref name="capacity"/>.
    /// </summary>
    /// <param name="capacity">The desired capacity for the map.</param>
    /// <exception cref="ArgumentOutOfRangeException">If capacity is out of range (&lt; 0)</exception>
    public BidirectionalMap(int capacity) : this(capacity, null, null)
    {
    }

    /// <summary>
    /// Creates a BidirectionalMap with the specified equality comparers.
    /// </summary>
    /// <param name="mapComparer">Equality comparer for <typeparamref name="TPrimary"/>. If null, the default comparer is used.</param>
    /// <param name="inverseComparer">Equality comparer for <typeparamref name="TSecondary"/>. If null, the default comparer is used.</param>
    public BidirectionalMap(IEqualityComparer<TPrimary> mapComparer, IEqualityComparer<TSecondary> inverseComparer)
      : this(0, mapComparer, inverseComparer)
    {
    }

    /// <summary>
    /// Creates a BidirectionalMap from the <paramref name="source"/> dictionary.
    /// </summary>
    /// <param name="source">The source dictionary from which to create a one-one relation map</param>
    /// <exception cref="ArgumentException">If <paramref name="source"/> contains doublets in values</exception>
    /// <exception cref="ArgumentNullException">If <paramref name="source"/> contains null keys and/or values</exception>
    public BidirectionalMap(IDictionary<TPrimary, TSecondary> source)
    {
      map = new Dictionary<TPrimary, TSecondary>(source);
      inverseMap = new Dictionary<TSecondary, TPrimary>(source.ToDictionary(kvp => kvp.Value, kvp => kvp.Key));
      Inverse = new BidirectionalMap<TSecondary, TPrimary>(this, inverseMap, map);
    }

    /// <summary>
    /// Creates a BidirectionalMap from the <paramref name="inverseSource"/> dictionary.
    /// </summary>
    /// <param name="inverseSource">The source dictionary from which to create a one-one relation map</param>
    /// <exception cref="ArgumentException">If <paramref name="inverseSource"/> contains doublets in values</exception>
    /// <exception cref="ArgumentNullException">If <paramref name="inverseSource"/> contains null keys and/or values</exception>
    public BidirectionalMap(IDictionary<TSecondary, TPrimary> inverseSource)
    {
      map = new Dictionary<TPrimary, TSecondary>(inverseSource.ToDictionary(kvp => kvp.Value, kvp => kvp.Key));
      inverseMap = new Dictionary<TSecondary, TPrimary>(inverseSource);
      Inverse = new BidirectionalMap<TSecondary, TPrimary>(this, inverseMap, map);
    }

    public BidirectionalMap<TSecondary, TPrimary> Inverse { get; }
    public int Count => map.Count;

    public ICollection<TPrimary> Keys => map.Keys;
    public ICollection<TSecondary> InverseKeys => inverseMap.Keys;

    public TSecondary this[TPrimary key]
    {
      get { return map[key]; }
      set { Set(key, value); }
    }

    public void Set(TPrimary primary, TSecondary secondary)
    {
      // Remove both the entries related to primary and secondary if any
      Remove(primary, secondary);
      // Now it should be safe to add the new relation.
      Add(primary, secondary);
    }

    public void Add(TPrimary primary, TSecondary secondary)
    {
      if (primary == null) throw new ArgumentNullException(nameof(primary));
      if (map.ContainsKey(primary)) throw new ArgumentException(nameof(primary), $"{primary} already present in the dictionary");
      if (secondary == null) throw new ArgumentNullException(nameof(secondary));
      if (inverseMap.ContainsKey(secondary)) throw new ArgumentException(nameof(secondary), $"{secondary} already present in the dictionary");

      map.Add(primary, secondary);
      inverseMap.Add(secondary, primary);
    }

    private bool Remove(TPrimary primary, TSecondary secondary)
    {
      bool result = false;

      if (map.TryGetValue(primary, out var primaryValue))
      {
        inverseMap.Remove(primaryValue);
        map.Remove(primary);
        result = true;
      }

      if (inverseMap.TryGetValue(secondary, out var secondaryValue))
      {
        map.Remove(secondaryValue);
        inverseMap.Remove(secondary);
        result = true;
      }

      return result;
    }

    public bool Remove(TPrimary primary)
    {
      if (map.TryGetValue(primary, out var secondary))
      {
        inverseMap.Remove(secondary);
        map.Remove(primary);
        return true;
      }

      return false;
    }

    public bool TryGetValue(TPrimary primary, out TSecondary secondary)
    {
      return map.TryGetValue(primary, out secondary);
    }

    public bool ContainsKey(TPrimary primary)
    {
      return map.ContainsKey(primary);
    }

    public void Clear()
    {
      map.Clear();
      inverseMap.Clear();
    }

    public IEnumerator<KeyValuePair<TPrimary, TSecondary>> GetEnumerator()
    {
      return map.GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
      return GetEnumerator();
    }
  }

The corresponding unittests:

  [TestClass]
  public class BidirectionalMapTests
  {
    class TestObject<T>
    {
      public TestObject(T value)
      {
        Value = value;
      }

      public T Value { get; }

      public static implicit operator T(TestObject<T> to) => to.Value;
      public static implicit operator TestObject<T>(T value) => new TestObject<T>(value);

      public override string ToString()
      {
        return Value?.ToString() ?? "";
      }
    }

    [TestMethod]
    public void InitializeFromSourceDictionary()
    {
      Dictionary<string, int> source = new Dictionary<string, int>
      {
        { "a", 1 },
        { "b", 2 }
      };

      BidirectionalMap<string, int> map = new BidirectionalMap<string, int>(source);

      Assert.AreEqual(1, map["a"]);
      Assert.AreEqual("b", map.Inverse[2]);
    }

    [TestMethod]
    public void InvalidInitializeFromSourceDictionary()
    {
      TestObject<string> one = new TestObject<string>("1");

      Dictionary<string, TestObject<string>> source = new Dictionary<string, TestObject<string>>
      {
        { "a", one },
        { "b", one }
      };

      BidirectionalMap<string, TestObject<string>> map = null;

      Assert.ThrowsException<ArgumentException>(() => map = new BidirectionalMap<string, TestObject<string>>(source));

      Dictionary<TestObject<string>, string> inverseSource = new Dictionary<TestObject<string>, string>
      {
        { "a", "1" },
        { "b", "1" }
      };

      Assert.ThrowsException<ArgumentException>(() => map = new BidirectionalMap<string, TestObject<string>>(inverseSource));

      source = new Dictionary<string, TestObject<string>>
      {
        { "a", null },
        { "b", "1" }
      };

      Assert.ThrowsException<ArgumentNullException>(() => map = new BidirectionalMap<string, TestObject<string>>(source));
    }

    [TestMethod]
    public void Add()
    {
      BidirectionalMap<string, int> map = new BidirectionalMap<string, int>();

      map.Add("a", 1);
      map.Add("b", 2);

      Assert.AreEqual(1, map["a"]);
      Assert.AreEqual("b", map.Inverse[2]);
      Assert.AreEqual(2, map.Count);
    }

    [TestMethod]
    public void InvalidAdd()
    {
      BidirectionalMap<string, int> map = new BidirectionalMap<string, int>();

      map.Add("a", 1);
      Assert.ThrowsException<ArgumentException>(() => map.Add("a", 2));
      Assert.ThrowsException<ArgumentException>(() => map.Add("b", 1));
      Assert.AreEqual(1, map["a"]);
    }

    [TestMethod]
    public void AddNull()
    {
      BidirectionalMap<string, string> map = new BidirectionalMap<string, string>();

      Assert.ThrowsException<ArgumentNullException>(() => map.Add(null, "a"));
      Assert.ThrowsException<ArgumentNullException>(() => map.Add("a", null));
      Assert.AreEqual(0, map.Count);
    }

    [TestMethod]
    public void Remove()
    {
      BidirectionalMap<string, int> map = new BidirectionalMap<string, int>();

      map.Add("a", 1);
      map.Add("b", 2);
      Assert.AreEqual(2, map.Count);

      map.Remove("a");
      Assert.AreEqual(1, map.Count);

      map.Inverse.Remove(2);
      Assert.AreEqual(0, map.Count);
    }

    [TestMethod]
    public void RemoveNonExistingValue()
    {
      BidirectionalMap<string, int> map = new BidirectionalMap<string, int>();

      map.Add("a", 1);
      map.Add("b", 2);

      Assert.IsFalse(map.Remove("c"));
      Assert.AreEqual(2, map.Count);
    }

    [TestMethod]
    public void Set()
    {
      BidirectionalMap<string, int> map = new BidirectionalMap<string, int>();

      map.Add("a", 1);
      map.Add("b", 2);

      map["a"] = 3;

      Assert.AreEqual(2, map.Count);
      Assert.IsTrue(map.TryGetValue("a", out int second));
      Assert.AreEqual(3, second);
      Assert.IsTrue(map.Inverse.TryGetValue(3, out string first));
      Assert.AreEqual("a", first);

      Assert.IsFalse(map.Inverse.TryGetValue(1, out _));
    }

    [TestMethod]
    public void SetWithExistingSecondValue()
    {
      BidirectionalMap<string, int> map = new BidirectionalMap<string, int>();

      map.Add("a", 1);
      map.Add("b", 2);

      map["a"] = 2;

      Assert.AreEqual(1, map.Count);
      Assert.IsTrue(map.TryGetValue("a", out int second));
      Assert.AreEqual(2, second);
      Assert.IsTrue(map.Inverse.TryGetValue(2, out string first));
      Assert.AreEqual("a", first);

      Assert.IsFalse(map.TryGetValue("b", out _));
    }

    [TestMethod]
    public void TryGetValue()
    {
      BidirectionalMap<string, int> map = new BidirectionalMap<string, int>
      {
        { "a", 1 },
        { "b", 2 }
      };

      Assert.IsTrue(map.TryGetValue("a", out int second));
      Assert.AreEqual(1, second);

      Assert.IsTrue(map.Inverse.TryGetValue(2, out string first));
      Assert.AreEqual("b", first);

      Assert.IsFalse(map.TryGetValue("c", out _));
      Assert.IsFalse(map.Inverse.TryGetValue(3, out _));
    }

    [TestMethod]
    public void Indexer()
    {
      BidirectionalMap<string, string> map = new BidirectionalMap<string, string>
      {
        { "a", "1" },
        { "b", "2" }
      };

      Assert.AreEqual("1", map["a"]);
      Assert.AreEqual("b", map.Inverse["2"]);
    }

    [TestMethod]
    public void Inverse()
    {
      BidirectionalMap<string, int> map = new BidirectionalMap<string, int>();
      Assert.AreEqual(map, map.Inverse.Inverse);

      map = new BidirectionalMap<string, int>(10);
      Assert.AreEqual(map, map.Inverse.Inverse);

      map = new BidirectionalMap<string, int>(EqualityComparer<string>.Default, EqualityComparer<int>.Default);
      Assert.AreEqual(map, map.Inverse.Inverse);

      map = new BidirectionalMap<string, int>(EqualityComparer<string>.Default, EqualityComparer<int>.Default);
      Assert.AreEqual(map, map.Inverse.Inverse);

      Dictionary<string, int> source = new Dictionary<string, int>
      {
        { "a", 1 },
        { "b", 2 }
      };
      map = new BidirectionalMap<string, int>(source);
      Assert.AreEqual(map, map.Inverse.Inverse);

      Dictionary<int, string> inverseSource = new Dictionary<int, string>
      {
        { 1, "a" },
        { 2, "b" }
      };
      map = new BidirectionalMap<string, int>(inverseSource);
      Assert.AreEqual(map, map.Inverse.Inverse);



    }
  }
\$\endgroup\$
  • 2
    \$\begingroup\$ This would be cool if you named the dictionaries map and pam for the inverted one :-P The code would nicely align then in a couple of places. \$\endgroup\$ – t3chb0t Sep 5 at 18:17
  • 1
    \$\begingroup\$ I also think that now with single APIs for each operation it'd should be save to actually implement the IDictionary interface. \$\endgroup\$ – t3chb0t Sep 5 at 18:25
  • 2
    \$\begingroup\$ I'm shocked that they actually allow the comparer to be null. I first thought it was a bug in your code but then checked the docs and indeed, it is ok to pass a null :-\ everytime I think I know the framework they have to surprise me with new (un)consistency conventions. \$\endgroup\$ – t3chb0t Sep 5 at 18:50
  • 1
    \$\begingroup\$ @t3chb0t: Initially I actually threw ArgumentNullException in a null check, but also realized that it was unnecessary. \$\endgroup\$ – Henrik Hansen Sep 5 at 19:00
  • 2
    \$\begingroup\$ I think we can, I would leave only one of the constructors TPrimary, TSecondary in order to be consistent with the other APIs and add a factory method FromInverse instead, to still support what the now removed cunstructor allowed. \$\endgroup\$ – t3chb0t Sep 5 at 19:08

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.