Case insensitive access for generic dictionary
Asked Answered
J

4

377

I have an application that use managed dlls. One of those dlls return a generic dictionary:

Dictionary<string, int> MyDictionary;  

The dictionary contains keys with upper and lower case.

On another side I am getting a list of potential keys (string) however I cannot guarantee the case. I am trying to get the value in the dictionary using the keys. But of course the following will fail since I have a case mismatch:

bool Success = MyDictionary.TryGetValue( MyIndex, out TheValue );  

I was hoping the TryGetValue would have an ignore case flag like mentioned in the MSDN doc, but it seems this is not valid for generic dictionaries.

Is there a way to get the value of that dictionary ignoring the key case? Is there a better workaround than creating a new copy of the dictionary with the proper StringComparer.OrdinalIgnoreCase parameter?

Jawbone answered 5/11, 2012 at 10:42 Comment(1)
F
767

There's no way to specify a StringComparer at the point where you try to get a value. If you think about it, "foo".GetHashCode() and "FOO".GetHashCode() are totally different so there's no reasonable way you could implement a case-insensitive get on a case-sensitive hash map.

You can, however, create a case-insensitive dictionary in the first place using:-

var comparer = StringComparer.OrdinalIgnoreCase;
var caseInsensitiveDictionary = new Dictionary<string, int>(comparer);

Or create a new case-insensitive dictionary with the contents of an existing case-sensitive dictionary (if you're sure there are no case collisions):-

var oldDictionary = ...;
var comparer = StringComparer.OrdinalIgnoreCase;
var newDictionary = new Dictionary<string, int>(oldDictionary, comparer);

This new dictionary then uses the GetHashCode() implementation on StringComparer.OrdinalIgnoreCase so comparer.GetHashCode("foo") and comparer.GetHashcode("FOO") give you the same value.

Alternately, if there are only a few elements in the dictionary, and/or you only need to lookup once or twice, you can treat the original dictionary as an IEnumerable<KeyValuePair<TKey, TValue>> and just iterate over it:-

var myKey = ...;
var myDictionary = ...;
var comparer = StringComparer.OrdinalIgnoreCase;
var value = myDictionary.FirstOrDefault(x => String.Equals(x.Key, myKey, comparer)).Value;

Or if you prefer, without the LINQ:-

var myKey = ...;
var myDictionary = ...;
var comparer = StringComparer.OrdinalIgnoreCase;
int? value;
foreach (var element in myDictionary)
{
  if (String.Equals(element.Key, myKey, comparer))
  {
    value = element.Value;
    break;
  }
}

This saves you the cost of creating a new data structure, but in return the cost of a lookup is O(n) instead of O(1).

Feculent answered 5/11, 2012 at 10:44 Comment(7)
There is no reason to keep the old dictionary around and instantiate the new one as any case-collisions will cause it to explode. If you know you won't get collisions then you may as well use case insensitive from the start.Algetic
It's been ten years that I've been using .NET and I now just figured this out!! Why do you use Ordinal instead of CurrentCulture?Tetrarch
Well, it depends on the behaviour you want. If the user is providing the key via the UI (or if you need to consider e.g. ss and ß equal) then you'll need to use a different culture, but given that the value is being used as the key for a hashmap coming from an external dependency, I think 'OrdinalCulture' is a reasonable assumption.Feculent
@RhysBevilaqua These dictionaries are often returned by other stuff, though. Knowing there will be no case collisions in the stuff you're dealing with doesn't magically make existing systems create their dictionaries as case insensitive.Chemosmosis
"You can, however, create a case-insensitive dictionary in the first place." But what if the Dictionary is part of an Interface? Is there any way to force this attribute?Hybridize
You mean is there any way to define a property of an interface such that implementations of that interface must provide a case-insensitive implementation? If so, you should ask a new question, and link to it here.Feculent
default(KeyValuePair<T, U>) is not null -- it's a KeyValuePair where Key=default(T) and Value=default(U). So you can't use the ?. operator in the LINQ example; you'll need to grab FirstOrDefault() and then (for this particular case) check to see if Key == null.Necessitous
I
76

For you LINQers out there that never use a regular dictionary constructor

myCollection.ToDictionary(x => x.PartNumber, x => x.PartDescription, StringComparer.OrdinalIgnoreCase)
Islander answered 9/3, 2017 at 19:41 Comment(1)
C# also has the Constructor: Dictionary(IDictionary<TKey, TValue> dictionary, IEqualityComparer<TKey>? comparer); that lets you effectively recreate the same dictionary with a new Comparer.Yugoslavia
O
44

There is much simpler way:

using System;
using System.Collections.Generic;
....
var caseInsensitiveDictionary = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
Otti answered 28/9, 2020 at 13:40 Comment(2)
This would work for Dictionary<string,T>, the type of the values isn't important here. Also, what would case insensitive even mean if the key wasn't a string?Stoughton
As someone who has lots of sorted dictionaries with string keys, why this didn't get more upvote is lost on me...Octuple
S
12

Its not very elegant but in case you cant change the creation of dictionary, and all you need is a dirty hack, how about this:

var item = MyDictionary.Where(x => x.Key.ToLower() == MyIndex.ToLower()).FirstOrDefault();
    if (item != null)
    {
        TheValue = item.Value;
    }
Seraphine answered 3/12, 2014 at 8:58 Comment(5)
or just this: new Dictionary<string,int>(otherDict, StringComparer.CurrentCultureIgnoreCase);Tetrarch
As per "Best Practices for Using Strings in the .NET Framework" use ToUpperInvariant instead of ToLower. msdn.microsoft.com/en-us/library/dd465121%28v=vs.110%29.aspxMaurey
This was good for me, where I had to retrospectively check keys in an insensitive manner. I streamlined it a bit more var item = MyDictionary.FirstOrDefault(x => x.Key.ToUpperInvariant() == keyValueToCheck.ToUpperInvariant());Agnusago
Why not just dict.Keys.Contains("bla", appropriate comparer) ? Furthermore, you wont get null for FirstOrDefault since keyvaluepair in C# is a struct.Ainu
This is a BETTER answer for folks who want to do a SINGLE case-insensitive lookup on a dictionary that is normally case-sensitive. GetMemberBinder.IgnoreCase much?Consternation

© 2022 - 2024 — McMap. All rights reserved.