Case insensitive key for hashmap kotlin?

Viewed 911

I have a hashmap with string as key and integer as value. I want the keys to be case insensitive.

val items = HashMap<String, Int>()
    items["key1"] = 90
    items["Key1"] = 80
    items["C"] = 70
    for ((k, v) in items) {
        println("$k = $v")
    }

This takes key1 and Key1 as separate entries

3 Answers

For this, you would need either to provide some extension function that would put and get the entry in some defined way (e.g. using every time lowercase() String's method) keeping keys case insensitive

fun HashMap<String, Int>.putInsensitive(k: String, v: Int) {
    this[k.lowercase()] = v
}

fun HashMap<String, Int>.getInsensitive(k: String, v: Int): Int? = this[k.lowercase()]

or provide your own Map interface implementation (it could even inherit from HashMap)

class InsensitiveHashMap<V> : HashMap<String, V>() {
    override fun put(key: String, value: V): V? = super.put(key.lowercase(), value)
    
    override fun get(key: String): V? = return super.get(key.lowercase())
}

Although you can fake this with an extension to HashMap (as mentioned in another answer), I don't think that's a good solution: in order to make the map's behaviour fully consistent, you'd probably need to override lots of methods.  (For example, it would be hard to ensure that it maintained the case-insensitivity if you update the map via its keySet.  And there are probably many more gotchas along the way.)

In most cases, you don't really need a HashMap specifically — you just need a Map implementation.  And so an alternative is to use another type of map that lets you provide your own Comparator, e.g.:

val items = TreeMap<String, Int>{ a, b -> 
    a.toLowerCase().compareTo(b.toLowerCase())
}

TreeMap is an implementation of SortedMap — a special type of Map that keeps its keys in order: either their natural ordering, or one you provide.  In this case, I've given a simple Comparator implementation which compares the lower-case versions of the two strings.

With this definition, the rest of the code in the question runs fine, and prints out:

C = 70
key1 = 80

…i.e. it has recognised that "Key1" should be treated the same as "key1", and updated that value instead of adding a new one.  I think this is the behaviour you want.

You don't need to take account of the fact that the map is sorted; that's just an added bonus.  You can still treat it like any other Map implementation, and everything should work.

(This is yet another example of why it's better to program to the interface, not the implementation.  If you write code that can use any Map implementation, you can give it a HashMap or a TreeMap or any other sort of map without any changes.)

A variation on the answer by @m.antkowicz, you can actually "override" the get / put operators on HashMap itself (and make it into a fantastic footgun):


operator fun HashMap<String, Int>.get(k: String): Int? = this[k.toLowerCase()]
operator fun HashMap<String, Int>.set(k: String, v: Int): Int? {
    val originalK = this[k.toLowerCase()]
    this.put(k.toLowerCase(), v )
    return originalK
}

So now you can:


fun main() {
    val items = HashMap<String, Int>()
    items["key1"] = 90
    items["Key1"] = 80
    items["C"] = 70

    for ((k, v) in items) {
        println("$k = $v")
    }
// Prints:
//    key1 = 80
//    c = 70
}
Related