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 (< 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?
-
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\$user73941– user739412019年09月05日 08:18:11 +00:00Commented Sep 5, 2019 at 8:18
-
2\$\begingroup\$ I know this feeling. Code Review is sometimes cruel ;-P \$\endgroup\$t3chb0t– t3chb0t2019年09月05日 08:32:06 +00:00Commented Sep 5, 2019 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– dfhwze2019年09月05日 11:44:00 +00:00Commented Sep 5, 2019 at 11:44
-
\$\begingroup\$ @dfhwze: I don't like single char names, but they are at least neutral. I'll consider that. \$\endgroup\$user73941– user739412019年09月05日 12:14:39 +00:00Commented Sep 5, 2019 at 12:14
3 Answers 3
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);
}
-
\$\begingroup\$ Oh, I just realized that you mentioned the same bug but with different examples hehe. \$\endgroup\$t3chb0t– t3chb0t2019年09月05日 08:10:18 +00:00Commented Sep 5, 2019 at 8:10
-
1\$\begingroup\$ @HenrikHansen then let it be a secret review ;) \$\endgroup\$t3chb0t– t3chb0t2019年09月05日 08:21:12 +00:00Commented Sep 5, 2019 at 8:21
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");
}
}
-
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\$user73941– user739412019年09月05日 11:58:00 +00:00Commented Sep 5, 2019 at 11:58
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 (< 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);
}
}
-
2\$\begingroup\$ This would be cool if you named the dictionaries
map
andpam
for the inverted one :-P The code would nicely align then in a couple of places. \$\endgroup\$t3chb0t– t3chb0t2019年09月05日 18:17:27 +00:00Commented Sep 5, 2019 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– t3chb0t2019年09月05日 18:25:01 +00:00Commented Sep 5, 2019 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 anull
:-\ everytime I think I know the framework they have to surprise me with new (un)consistency conventions. \$\endgroup\$t3chb0t– t3chb0t2019年09月05日 18:50:47 +00:00Commented Sep 5, 2019 at 18:50 -
1\$\begingroup\$ @t3chb0t: Initially I actually threw
ArgumentNullException
in a null check, but also realized that it was unnecessary. \$\endgroup\$user73941– user739412019年09月05日 19:00:23 +00:00Commented Sep 5, 2019 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 methodFromInverse
instead, to still support what the now removed cunstructor allowed. \$\endgroup\$t3chb0t– t3chb0t2019年09月05日 19:08:56 +00:00Commented Sep 5, 2019 at 19:08