18

I have a dictionary where the key is a Tuple where the first item is a Date and the second item is a string. I would like the dictionary to be case insensitive.

I know that if the key was just a string I could pass StringComparer.OrdinalIgnoreCase as a parameter when declaring the dictionary, but this does not seem to work when the key is a Tuple.

Is there some way to specify the StringComparer to use on the second item of the Tuple?

Thanks

Eric
  • 688
  • 2
  • 9
  • 23

3 Answers3

26

Use this overload of the Dictionary constructor, which allows you to specify a custom comparer for the keys. You would accompany this with creating a class that implements

IEqualityComparer<Tuple<string, DateTime>>

Which might look like this:

class CustomEqualityComparer : IEqualityComparer<Tuple<string, DateTime>>
{

    public bool Equals(Tuple<string, DateTime> lhs, Tuple<string, DateTime> rhs)
    {
        return
          StringComparer.CurrentCultureIgnoreCase.Equals(lhs.Item1, rhs.Item1)
       && lhs.Item2 == rhs.Item2;
    }


    public int GetHashCode(Tuple<string, DateTime> tuple)
    {
        return StringComparer.CurrentCultureIgnoreCase.GetHashCode(tuple.Item1)
             ^ tuple.Item2.GetHashCode();
    }
}

There are no argument checks here, so please don't treat this as production code. Also, care needs to be taken so that the Equals and GetHashCode implementations satisfy the all-important condition that if two tuples compare equal, they must have the same hash code. When dealing with custom text comparisons it is easy to introduce bugs if not extra careful: for example, using ToLowerInvariant instead of ToLower above would be a bug (albeit one that might not surface for some time).

dtb
  • 213,145
  • 36
  • 401
  • 431
Jon
  • 428,835
  • 81
  • 738
  • 806
  • 1
    Using the same StringComparer to check for equality and for getting the hash code usually works best to avoid the bugs you mention. I've edited the answer with a proposed fix; please undo if you don't like it. – dtb May 07 '13 at 21:21
  • @dtb: Definitely won't be undoing as this is a consistency improvement also IMO. Thanks! – Jon May 07 '13 at 21:41
3

I needed this in a Dictionary<Tuple<>> wrapper, so I used @Jon 's code to create a generic version

public class TupleEqualityComparer<T1, T2> : IEqualityComparer<Tuple<T1, T2>>
{
    private IEqualityComparer<T1> comparer1;
    private IEqualityComparer<T2> comparer2;

    public TupleEqualityComparer(IEqualityComparer<T1> comparer1, IEqualityComparer<T2> comparer2)
    {
        this.comparer1 = comparer1 ?? EqualityComparer<T1>.Default;
        this.comparer2 = comparer2 ?? EqualityComparer<T2>.Default;
    }

    public bool Equals(Tuple<T1, T2> lhs, Tuple<T1, T2> rhs)
    {
        return comparer1.Equals(lhs.Item1, rhs.Item1) && comparer2.Equals(lhs.Item2, rhs.Item2);
    }

    public int GetHashCode(Tuple<T1, T2> tuple)
    {
        return comparer1.GetHashCode(tuple.Item1) ^ comparer2.GetHashCode(tuple.Item2);
    }

}

public class Dictionary<TKey1, TKey2, TValue> : Dictionary<Tuple<TKey1, TKey2>, TValue>()
{
    public Dictionary() : base() { }
    public Dictionary(IEqualityComparer<TKey1> comparer1, IEqualityComparer<TKey2> comparer2) : base(new TupleEqualityComparer<TKey1, Tkey2>(comparer1, comparer2) { }

    public TValue this[TKey1 key1, TKey2 key2]
    {
        get { return base[Tuple.Create(key1, key2)]; }
    }

    public void Add(TKey1 key1, TKey2 key2, TValue value)
    {
        base.Add(Tuple.Create(key1, key2), value);
    }

    public bool ContainsKey(TKey1 key1, TKey2 key2)
    {
        return base.ContainsKey(Tuple.Create(key1, key2));
    }

    public bool TryGetValue(TKey1 key1, TKey2 key2, out TValue value)
    {
        return base.TryGetValue(Tuple.Create(key1, key2), out value);
    }
}

Usage

var dict = new Dictionary<string, DateTime, int>(
    StringComparer.OrdinalIgnoreCase, null);
dict.Add("value1", DateTime.Now, 123);
Assert.IsTrue(dict.ContainsKey("VALUe1"));
Jürgen Steinblock
  • 30,746
  • 24
  • 119
  • 189
1

Since the comparisons are going to be case-insensitive, you could use the toLower/toUpper method in the string side when making the tuples, and then always lower or upper the strings you'll have in the tuples used to retrive/compare entries in the dictionary.

Geeky Guy
  • 9,229
  • 4
  • 42
  • 62
  • 3
    Comparing strings by converting them to upper or lower case doesn't work properly in some cultures. It's better to use a StringComparer such as StringComparer.CurrentCultureIgnoreCase – dtb May 07 '13 at 21:23