Case-Insensitive Dictionary with String Key-Type in C#

Case-INsensitive Dictionary with string key-type in C#

This seemed related, but I didn't understand it properly: c# Dictionary: making the Key case-insensitive through declarations

It is indeed related. The solution is to tell the dictionary instance not to use the standard string compare method (which is case sensitive) but rather to use a case insensitive one. This is done using the appropriate constructor:

var dict = new Dictionary<string, YourClass>(
StringComparer.InvariantCultureIgnoreCase);

The constructor expects an IEqualityComparer which tells the dictionary how to compare keys.

StringComparer.InvariantCultureIgnoreCase gives you an IEqualityComparer instance which compares strings in a case-insensitive manner.

Ignore case in Dictionary ContainsKey Where key is dynamic

You could add an extension to the Dictionary which determines if the key is of type string, and if so, uses case insensitive comparison; otherwise, it uses the default comparison.

public static class DictionaryExtension
{
public static bool ContainsKeyIgnoreCase<TKey, TValue>(this Dictionary<TKey, TValue> dictionary, TKey key)
{
bool? keyExists;

var keyString = key as string;
if (keyString != null)
{
// Key is a string.
// Using string.Equals to perform case insensitive comparison of the dictionary key.
keyExists =
dictionary.Keys.OfType<string>()
.Any(k => string.Equals(k, keyString, StringComparison.InvariantCultureIgnoreCase));
}
else
{
// Key is any other type, use default comparison.
keyExists = dictionary.ContainsKey(key);
}

return keyExists ?? false;
}
}

You can then use it like this:

var foo = new Foo();
var dictionary =
new Dictionary<dynamic, string>
{
{ 1, "One" }, // key is numeric
{ "Two", "Two" }, // key is string
{ foo, "Foo" } // key is object
};

dictionary.ContainsKeyIgnoreCase("two"); // Returns true
dictionary.ContainsKeyIgnoreCase("TwO"); // Returns true
dictionary.ContainsKeyIgnoreCase("aBc"); // Returns false
dictionary.ContainsKeyIgnoreCase(1); // Returns true
dictionary.ContainsKeyIgnoreCase(2); // Returns false
dictionary.ContainsKeyIgnoreCase(foo); // Returns true
dictionary.ContainsKeyIgnoreCase(new Foo()); // Returns false

Note:

The extension example above is using StringComparer.InvariantCultureIgnoreCase. You may need to modify the comparison for your needs.

Case insensitive access for generic dictionary

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).

Case Insensitive Dictionary with char key in C#

The Dictionary class has a constructor that takes any IEqualityComparer. What you need to do is implement a simple case-insensitive IEqualityComparer<char> and pass it to the constructor, and it will be used when evaluating the key.

This is a similar question for implementing IComparer<char> without case sensitivity. IEqualityComparer will be practically identical:

  • What is the correct way to compare char ignoring case?

O(1) Dictionary both case sensitive and insensitive

I recently ran across this question since I needed to solve for the same scenario, with the following guidelines:

  • Both types of lookups should be fast (avoid O(n) for case-sensitive path).
  • Use a single dictionary rather than two separate dictionaries (avoid double the space requirement).
  • Avoid needless allocations via string conversion (.ToLower()) when performing lookups.
  • Trade a reasonable amount of additional needed memory for satisfying the above.

I didn't feel that any of the current answers met the above and, as a result, I came up with the following dictionary definition:

Dictionary<string, (string Key, T Value)> _dict = 
new Dictionary<string, (string Key, T Value)>(StringComparer.OrdinalIgnoreCase);

This allows for case-insensitive hashing of the key, while using a ValueTuple to store the actual key in its raw string form for additional case-sensitive comparisons, if need be, alongside the value. The full implementation looks like the following:

public class MultiCaseDictionary<T>
{
public MultiCaseDictionary() : this(0) { }

public MultiCaseDictionary(int capacity)
{
_dict = new(capacity, StringComparer.OrdinalIgnoreCase);
}

private readonly Dictionary<string, (string Key, T Value)> _dict;

public bool TryGetValue(string key, out T value, bool isCaseInsensitive = true)
{
if (_dict.TryGetValue(key, out (string Key, T Value) match))
{
if (isCaseInsensitive)
{
value = match.Value;
return true;
}

if (key.Equals(match.Key))
{
value = match.Value;
return true;
}
}

value = default;
return false;
}

public void Add(string key, T value)
{
_dict[key] = (key, value);
}
}

This structure results in excellent performance for both the case sensitive and insensitive paths, and the case-sensitive path is slightly slower than the case-sensitive (optimal) path in the accepted answer due to needing to perform extra work.

However, it is orders of magnitude faster for the case-insensitive path, due to the accepted answer having a runtime complexity of O(n). In addition, the case-insensitive search of MultiCaseDictionary is actually faster than the case-sensitive search of CaseSensitiveDictionary.

Finally, the Add path is slower in the MultiCaseDictionary due to needing to construct a ValueTuple. The following benchmark demonstrates both implementations, running on .NET 6:

Implementations

public class MultiCaseDictionary<T>
{
public MultiCaseDictionary() : this(0) { }

public MultiCaseDictionary(int capacity)
{
_dict = new(capacity, StringComparer.OrdinalIgnoreCase);
}

private readonly Dictionary<string, (string Key, T Value)> _dict;

public bool TryGetValue(string key, out T value, bool isCaseInsensitive = true)
{
if (_dict.TryGetValue(key, out (string Key, T Value) match))
{
if (isCaseInsensitive)
{
value = match.Value;
return true;
}

if (key.Equals(match.Key))
{
value = match.Value;
return true;
}
}

value = default;
return false;
}

public void Add(string key, T value)
{
_dict[key] = (key, value);
}
}

public class CaseSensitiveDictionary<T>
{
public CaseSensitiveDictionary() : this(0) { }

public CaseSensitiveDictionary(int capacity)
{
_dict = new(capacity);
}

private readonly Dictionary<string, T> _dict;

public bool TryGetValue(string key, out T value, bool isCaseInsensitive = true)
{
if (!isCaseInsensitive)
{
if (_dict.TryGetValue(key, out value))
{
return true;
}
}
else
{
key = _dict.Keys.FirstOrDefault(k =>
string.Compare(key, k, StringComparison.OrdinalIgnoreCase) == 0);
if (key == null)
{
value = default;
return false;
}

value = _dict[key];
return true;
}

value = default;
return false;
}

public void Add(string key, T value)
{
_dict[key] = value;
}
}

Benchmarks

The benchmarks preload 1000 entries for TryGetValue tests to demonstrate O(n) behavior. In addition, they insert 10 entries for Add tests to check space requirements and add performance degradation.

[SimpleJob(RuntimeMoniker.Net60, invocationCount: 50000)]
[MemoryDiagnoser]
[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)]
[CategoriesColumn]
public class CaseSensitiveDictionaryBenchmarks
{
// use longer key to simulate expected hash costs
private const string Key = "100000500";
private const int Capacity = 1000;
private const int Iterations = 9;

private MultiCaseDictionary<int> _multiCaseDictionary;
private CaseSensitiveDictionary<int> _caseSensitiveDictionary;

[IterationSetup(Targets = new string[] {
nameof(MultiCaseDictionary_Search),
nameof(MultiCaseDictionary_InsensitiveSearch)
})]
public void SetupMultiCase()
{
_multiCaseDictionary = new MultiCaseDictionary<int>(Capacity);
for (int i = 100000000; i < 100001000; i++)
{
_multiCaseDictionary.Add(i.ToString(), i);
}
}

[IterationSetup(Targets = new string[] {
nameof(CaseSensitiveDictionary_Search),
nameof(CaseSensitiveDictionary_InsensitiveSearch),
})]
public void SetupCaseSensitive()
{
_caseSensitiveDictionary = new CaseSensitiveDictionary<int>(Capacity);
for (int i = 100000000; i < 100001000; i++)
{
_caseSensitiveDictionary.Add(i.ToString(), i);
}
}

[Benchmark(Baseline = true), BenchmarkCategory("TryGetValue")]
public int CaseSensitiveDictionary_Search()
{
int result;
for (int i = 0; i < Iterations; i++)
{
_caseSensitiveDictionary.TryGetValue(Key, out result, false);
}
_caseSensitiveDictionary.TryGetValue(Key, out result, false);
return result;
}

[Benchmark, BenchmarkCategory("TryGetValue")]
public int CaseSensitiveDictionary_InsensitiveSearch()
{
int result;
for (int i = 0; i < Iterations; i++)
{
_caseSensitiveDictionary.TryGetValue(Key, out result, true);
}
_caseSensitiveDictionary.TryGetValue(Key, out result, true);
return result;
}

[Benchmark, BenchmarkCategory("TryGetValue")]
public int MultiCaseDictionary_Search()
{
int result;
for (int i = 0; i < Iterations; i++)
{
_multiCaseDictionary.TryGetValue(Key, out result, false);
}
_multiCaseDictionary.TryGetValue(Key, out result, false);
return result;
}

[Benchmark, BenchmarkCategory("TryGetValue")]
public int MultiCaseDictionary_InsensitiveSearch()
{
int result;
for (int i = 0; i < Iterations; i++)
{
_multiCaseDictionary.TryGetValue(Key, out result, true);
}
_multiCaseDictionary.TryGetValue(Key, out result, true);
return result;
}

[Benchmark(Baseline = true), BenchmarkCategory("Add")]
public CaseSensitiveDictionary<int> CaseSensitiveDictonary_Add()
{
_caseSensitiveDictionary = new CaseSensitiveDictionary<int>(Capacity);
for (int i = 100000000; i < 100000010; i++)
{
_caseSensitiveDictionary.Add(i.ToString(), i);
}

return _caseSensitiveDictionary;
}

[Benchmark, BenchmarkCategory("Add")]
public MultiCaseDictionary<int> MultiCaseDictionary_Add()
{
_multiCaseDictionary = new MultiCaseDictionary<int>(Capacity);
for (int i = 100000000; i < 100000010; i++)
{
_multiCaseDictionary.Add(i.ToString(), i);
}

return _multiCaseDictionary;
}
}

Results
benchmarkresults


BenchmarkDotNet=v0.13.2, OS=macOS Catalina 10.15.7 (19H1417) [Darwin 19.6.0]
Intel Core i5-1038NG7 CPU 2.00GHz, 1 CPU, 8 logical and 4 physical cores
.NET SDK=6.0.400
[Host] : .NET 6.0.8 (6.0.822.36306), X64 RyuJIT AVX2
Job-NQOSHT : .NET 6.0.8 (6.0.822.36306), X64 RyuJIT AVX2

Runtime=.NET 6.0 InvocationCount=50000















































































































MethodCategoriesMeanErrorStdDevMedianRatioRatioSDGen0AllocatedAlloc Ratio
CaseSensitiveDictonary_AddAdd2,423.0 ns24.67 ns20.60 ns2,418.3 ns1.000.0010.000031440 B1.00
MultiCaseDictionary_AddAdd3,100.2 ns61.03 ns59.94 ns3,109.2 ns1.270.0212.820040264 B1.28
CaseSensitiveDictionary_SearchTryGetValue251.8 ns6.47 ns18.67 ns246.1 ns1.000.000.0600240 B1.00
CaseSensitiveDictionary_InsensitiveSearchTryGetValue95,573.4 ns1,888.58 ns2,768.26 ns95,401.3 ns378.7729.360.40001280 B5.33
MultiCaseDictionary_SearchTryGetValue265.5 ns5.11 ns10.43 ns262.2 ns1.050.07--0.00
MultiCaseDictionary_InsensitiveSearchTryGetValue233.1 ns4.83 ns14.16 ns226.2 ns0.930.09--0.00

c# Dictionary: making the Key case-insensitive through declarations

When you add an element to the outer dictionary, you'll likely create a new instance of the nested dictionary, add it at this point, making use of the overloaded constructor that takes an IEqualityComparer<TKey>.

_customRecordSet.Add(0, new Dictionary<string, CustomClass>(StringComparer.InvariantCultureIgnoreCase));


Update 08/03/2017: Anecdotally, I read somewhere (I think in "Writing High-Performance .NET Code") that StringComparer.OrdinalIgnoreCase is more efficient when simply wanting to disregard the case of characters. This, however, is entirely unfounded by myself so YMMV.

Case insensitive dictionary in TypeScript

At runtime you can get fairly close to what you're asking for using a Proxy.

TypeScript can't do much to help with strongly typing the keys past just string, since the type system does not have the ability to represent the effect of string operations on string literal types. So if you wanted an object with keystring as a known key, the compiler would have no idea that KEYSTRING was also a known key. There are suggestions for manipulating string literal types, such as using regular expressions on them, but nothing is currently part of the language. Luckily for you, I think, you're asking for a dictionary type, which usually means the keys are just string. As long as you don't expect the compiler to keep track of valid keys for you, you'll be okay.

Here's a possible implementation of using a Proxy. Note that I haven't completely tested every possible edge case, but the basic idea is to get in the way of everything that accepts a property key and replace it with a canonical version:

function dictWithCanonicalKeys<V>(canon: (prop: keyof any) => keyof any) {
return new Proxy<{ [k: string]: V }>(
{},
{
get(target, prop) {
return Reflect.get(target, canon(prop));
},
set(target, prop, value) {
return Reflect.set(target, canon(prop), value);
},
has(target, prop) {
return Reflect.has(target, canon(prop));
},
defineProperty(target, prop, attribs) {
return Reflect.defineProperty(target, canon(prop), attribs);
},
deleteProperty(target, prop) {
return Reflect.deleteProperty(target, canon(prop));
},
getOwnPropertyDescriptor(target, prop) {
return Reflect.getOwnPropertyDescriptor(target, canon(prop));
}
}
);
}

And let's use it, passing in a canonicalizing function that converts the key to lowercase (if it's a string, that is... it leaves symbol keys alone):

const dict = dictWithCanonicalKeys<number | undefined>(
p => (typeof p === "string" ? p.toLowerCase() : p)
);

dict.keyString = 1;
console.log(dict.KEYSTRING); // 1
dict.KEYstring = "two"; // error! "two" is not a number
dict.KEYstring = 2;
console.log(dict.keyStrinG); // 2
console.log(JSON.stringify(dict)); // {"keystring": 2};
console.log("KeyString" in dict); // true
Object.keys(dict).forEach(k => console.log(k + ":" + dict[k])); // keystring:2

That all looks reasonable to me.


Finally, a caveat: you should be careful introducing things like this to your code. It would be quite surprising to most developers to find that their supposedly plain objects have case-insensitive keys (console.log(Object.keys(dict).indexOf("keyString")); // -1?!), and even if you can explain it to developers, their IDEs will not understand and might flag a key that differs in case from an expected one (e.g., KeyString) as an error. If you're doing this to deal with user input, it might be cleaner to just define strict barriers where the input is allowed into your program, and canonicalize the case there, so the dictionary can be blissfully unaware and be implemented as a plain object.

Of course, only you know your use case, and maybe a case-insensitive dictionary-like object is the right choice. If so, I hope the above code helps you. Good luck!

Link to code

How can I create a generic case-insensitive multi-key dictionary using ValueTuples?

You're painting yourself into that corner by using a tuple. Tuples are meant to represent a bag of values, not an entity.

You can implement your own key type in a tuple friendly way:

public struct Key : IEquatable<Key>
{
private readonly int hashCode;

public Key(int key1, string key2)
{
this.Key1 = key1;
this.Key2 = key2;
this.hashCode = HashCode.Combine(key1, StringComparer.OrdinalIgnoreCase.GetHashCode(Key2));
}

public int Key1 { get; }
public string Key2 { get; }

public bool Equals(Key other)
=> this.hashCode == other.hashCode
&& this.Key1 == other.Key1
&& string.Equals(this.Key2, other.Key2, StringComparison.OrdinalIgnoreCase);

public override bool Equals(object obj)
=> obj is Key key && this.Equals(key);

public override int GetHashCode() => this.hashCode;

public static implicit operator (int key1, string key2)(Key key)
=> (key1: key.Key1, key2: key.Key2);

public static implicit operator Key((int key1, string key2) key)
=> new Key(key.key1, key.key2);

public void Deconstruct(out int key1, out string key2)
=> (key1, key2) = (this.Key1, this.Key2);
}

You can even use it with tuples or "like" tuples:

var key = new Key(1, "one");
var (k1, k2) = key;
(int key1, string key2) t = key;
t.key1 = 2;
t.key2 = "two";
key = t;

If you really want to stay with tuples, define your own comparer:

public class MyTupleComparer : IEqualityComparer<(int key1, string key2)>
{
public bool Equals((int key1, string key2) x, (int key1, string key2) y)
=> x.key1 == y.key1
&& string.Equals(x.key2, y.key2, StringComparison.OrdinalIgnoreCase);

public int GetHashCode((int key1, string key2) obj)
=> HashCode.Combine(obj.key1, StringComparer.OrdinalIgnoreCase.GetHashCode(obj.key2));
}


Related Topics



Leave a reply



Submit