This is a drop-in solution to a problem that I often find myself facing: a situation where two programmers have defined the same concept in two different ways, and I am forced to translate between them. Obviously, a refactor is better, but sometimes you're simply not given the time needed to do so. This is also particularly helpful in mapping between and Enum value and a displayable string (Yes, you can use enum.toString() as long as you don't want punctuation, or spaces, and your front-end guys and back-end guys agree on what to call something)
First, the object:
/// <summary>
/// This is a dictionary guaranteed to have only one of each value and key.
/// It may be searched either by TFirst or by TSecond, giving a unique answer because it is 1 to 1.
/// In any IDictionary methods that take in Key/Value pairs, Key is TFirst and Value is TSecond
///
/// This class violates the injection/surjection contract for the idictionary interface as a low-risk implementation detail
/// </summary>
/// <typeparam name="TFirst">The type of the "key"</typeparam>
/// <typeparam name="TSecond">The type of the "value"</typeparam>
public class BijectiveBidirectionalMap<TFirst, TSecond> : IDictionary<TFirst, TSecond>
{
IDictionary<TFirst, TSecond> firstToSecond = new Dictionary<TFirst, TSecond>();
IDictionary<TSecond, TFirst> secondToFirst = new Dictionary<TSecond, TFirst>();
public BijectiveBidirectionalMap()
{
if (typeof(TFirst) == typeof(TSecond)) throw new ArgumentException("Cannot map between identical types.");
}
#region Exception throwing methods
public bool ContainsKey(TFirst key)
{
return firstToSecond.ContainsKey(key);
}
public bool ContainsKey(TSecond key)
{
return secondToFirst.ContainsKey(key);
}
/// <summary>
/// Tries to add the pair to the dictionary.
/// Throws an exception if either element is already in the dictionary
/// </summary>
/// <param name="first"></param>
/// <param name="second"></param>
public void Add(TFirst first, TSecond second)
{
if (firstToSecond.ContainsKey(first) || secondToFirst.ContainsKey(second))
throw new ArgumentException("Duplicate first or second");
firstToSecond.Add(first, second);
secondToFirst.Add(second, first);
}
public bool Remove(TFirst key)
{
return TryRemoveByFirst(key);
}
public bool Remove(TSecond key)
{
return TryRemoveBySecond(key);
}
public bool TryGetValue(TFirst key, out TSecond value)
{
return TryGetByFirst(key, out value);
}
public bool TryGetValue(TSecond key, out TFirst value)
{
return TryGetBySecond(key, out value);
}
public TSecond this[TFirst key]
{
get { return GetByFirst(key); }
set { Add(key, value); }
}
public TFirst this[TSecond key]
{
get { return GetBySecond(key); }
set { Add(value, key); }
}
public ICollection<TFirst> Keys
{
get { return firstToSecond.Keys; }
}
public ICollection<TSecond> Values
{
get { return firstToSecond.Values; }
}
/// <summary>
/// Find the TSecond corresponding to the TFirst first
/// Throws an exception if first is not in the dictionary.
/// </summary>
/// <param name="first">the key to search for</param>
/// <returns>the value corresponding to first</returns>
private TSecond GetByFirst(TFirst first)
{
TSecond second;
if (!firstToSecond.TryGetValue(first, out second))
throw new ArgumentException("first");
return second;
}
/// <summary>
/// Find the TFirst corresponing to the Second second.
/// Throws an exception if second is not in the dictionary.
/// </summary>
/// <param name="second">the key to search for</param>
/// <returns>the value corresponding to second</returns>
private TFirst GetBySecond(TSecond second)
{
TFirst first;
if (!secondToFirst.TryGetValue(second, out first))
throw new ArgumentException("second");
return first;
}
/// <summary>
/// Remove the record containing first.
/// If first is not in the dictionary, throws an Exception.
/// </summary>
/// <param name="first">the key of the record to delete</param>
private void RemoveByFirst(TFirst first)
{
TSecond second;
if (!firstToSecond.TryGetValue(first, out second))
throw new ArgumentException("first");
firstToSecond.Remove(first);
secondToFirst.Remove(second);
}
/// <summary>
/// Remove the record containing second.
/// If second is not in the dictionary, throws an Exception.
/// </summary>
/// <param name="second">the key of the record to delete</param>
private void RemoveBySecond(TSecond second)
{
TFirst first;
if (!secondToFirst.TryGetValue(second, out first))
throw new ArgumentException("second");
secondToFirst.Remove(second);
firstToSecond.Remove(first);
}
#endregion
#region Try methods
/// <summary>
/// Tries to add the pair to the dictionary.
/// Returns false if either element is already in the dictionary
/// </summary>
/// <param name="first"></param>
/// <param name="second"></param>
/// <returns>true if successfully added, false if either element are already in the dictionary</returns>
private Boolean TryAdd(TFirst first, TSecond second)
{
if (firstToSecond.ContainsKey(first) || secondToFirst.ContainsKey(second))
return false;
firstToSecond.Add(first, second);
secondToFirst.Add(second, first);
return true;
}
/// <summary>
/// Find the TSecond corresponding to the TFirst first.
/// Returns false if first is not in the dictionary.
/// </summary>
/// <param name="first">the key to search for</param>
/// <param name="second">the corresponding value</param>
/// <returns>true if first is in the dictionary, false otherwise</returns>
private Boolean TryGetByFirst(TFirst first, out TSecond second)
{
return firstToSecond.TryGetValue(first, out second);
}
/// <summary>
/// Find the TFirst corresponding to the TSecond second.
/// Returns false if second is not in the dictionary.
/// </summary>
/// <param name="second">the key to search for</param>
/// <param name="first">the corresponding value</param>
/// <returns>true if second is in the dictionary, false otherwise</returns>
private Boolean TryGetBySecond(TSecond second, out TFirst first)
{
return secondToFirst.TryGetValue(second, out first);
}
/// <summary>
/// Remove the record containing first, if there is one.
/// </summary>
/// <param name="first"></param>
/// <returns> If first is not in the dictionary, returns false, otherwise true</returns>
private Boolean TryRemoveByFirst(TFirst first)
{
TSecond second;
if (!firstToSecond.TryGetValue(first, out second))
return false;
firstToSecond.Remove(first);
secondToFirst.Remove(second);
return true;
}
/// <summary>
/// Remove the record containing second, if there is one.
/// </summary>
/// <param name="second"></param>
/// <returns> If second is not in the dictionary, returns false, otherwise true</returns>
private Boolean TryRemoveBySecond(TSecond second)
{
TFirst first;
if (!secondToFirst.TryGetValue(second, out first))
return false;
secondToFirst.Remove(second);
firstToSecond.Remove(first);
return true;
}
#endregion
public bool Remove(KeyValuePair<TFirst, TSecond> item)
{
bool success = false;
if (firstToSecond.Contains(item))
{
success = firstToSecond.Remove(item.Key);
if (success) success = secondToFirst.Remove(item.Value);
}
return success;
}
/// <summary>
/// The number of pairs stored in the dictionary
/// </summary>
public Int32 Count
{
get { return firstToSecond.Count; }
}
public bool IsReadOnly
{
get { return firstToSecond.IsReadOnly; }
}
public void Add(KeyValuePair<TFirst, TSecond> item)
{
Add(item.Key, item.Value);
}
/// <summary>
/// Removes all items from the dictionary.
/// </summary>
public void Clear()
{
firstToSecond.Clear();
secondToFirst.Clear();
}
public bool Contains(KeyValuePair<TFirst, TSecond> item)
{
return (firstToSecond.Contains(item));
}
public void CopyTo(KeyValuePair<TFirst, TSecond>[] array, int arrayIndex)
{
CopyTo(array, arrayIndex);
}
public IEnumerator<KeyValuePair<TFirst, TSecond>> GetEnumerator()
{
return firstToSecond.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
And second, a rediculously simplified usage:
public enum AwesomeEnum
{
Hello,
World
}
private void demo()
{
BijectiveBidirectionalMap mapping =
new BijectiveBidirectionalMap();
mapping.Add(AwesomeEnum.Hello, "Hello,");
mapping.Add(AwesomeEnum.World, "World!");
AwesomeEnum ae = mapping["Hello,"];
string s = mapping[AwesomeEnum.World];
}