40

I am trying to reverse a Map in Kotlin. So far, I have come up with:

mapOf("foo" to 42)
  .toList()
  .map { (k, v) -> v to k }
  .toMap()

Is there any better way of doing this without using a middleman(middlelist)?

Grzegorz Piwowarek
  • 13,172
  • 8
  • 62
  • 93

6 Answers6

69

Since the Map consists of Entrys and it is not Iterable you can use Map#entries instead. It will be mapped to Map#entrySet to create a backed view of Set<Entry>, for example:

val reversed = map.entries.associateBy({ it.value }) { it.key }

OR use Iterable#associate, which will create additional Pairs.

val reversed = map.entries.associate{(k,v)-> v to k}

OR using Map#forEach:

val reversed = mutableMapOf<Int, String>().also {
    //     v-- use `forEach` here     
    map.forEach { (k, v) -> it.put(v, k) } 
}.toMap()
// ^--- you can add `toMap()` to create an immutable Map.
Mike Samuel
  • 118,113
  • 30
  • 216
  • 245
holi-java
  • 29,655
  • 7
  • 72
  • 83
  • 4
    So to answer the question of "Is there any better way of doing this without using a middleman (middlelist)?" the answer is no? :P – aug Jul 28 '17 at 18:46
  • @aug yeah. I'll answer the question further. – holi-java Jul 28 '17 at 18:46
  • 1
    for the last solution "it[v] = k" is more compact and expressive than it.put(v, k) – DPM Jan 31 '18 at 17:10
  • 1
    What if the given map is not a 1to1 mapping ? What if the values aren't distinct enough to serve as keys ? The output of such operation will in general be a `Map>`, where `K` is the key type and `V` the value type of the input map. – foo Aug 28 '19 at 21:18
6

Here is a simple extension function that reverse a map - without generating unneeded garbage (like pairs, intermediate data structures and unnecessary closures )

fun <K, V> Map<K, V>.reversed() = HashMap<V, K>().also { newMap ->
    entries.forEach { newMap.put(it.value, it.key) }
}

note that apply is inlined, and entries.forEach is also inlined (which is not the same for Map::forEach)

bennyl
  • 2,886
  • 2
  • 29
  • 43
4

In case your map is not a 1-1 mapping and you want the inversion to be a list of values:

mapOf(1 to "AAA", 2 to "BBB", 3 to "BBB").toList()
        .groupBy { pair -> pair.second } // Pair<Int, String>
        .mapValues { entry -> 
          entry.value.map { it.first } // Entry<String, List<Pair<Int, String>>
        }

Yair Galler
  • 111
  • 3
  • 4
4

If you need to reverse a multimap like m: Map<K, List<V>> to a Map<V, List<K>> you can do

m
  .flatMap { it.value.map { oneValue -> oneValue to it.key } }
  .groupBy({ it.first }, { it.second })
  .toMap()

In sequence,

  • mapOf('a' to listOf('b', 'c'), 'd' to listOf('b')) gets flat mapped to a sequence like
  • listOf('b' to 'a', 'c' to 'a', 'b' to 'd') which gets grouped to
  • listOf('b' to listOf('a', 'd'), 'c' to listOf('a')) which then gets converted to a map.

This probably creates intermediate objects.

Mike Samuel
  • 118,113
  • 30
  • 216
  • 245
1

I'm still learning the ins and outs of Kotlin, but I had the same requirement and as of Kotlin 1.2 it appears that you can iterate over a Map and so map() it directly like this:

@Test
fun testThatReverseIsInverseOfMap() {
    val intMap = mapOf(1 to "one", 2 to "two", 3 to "three")
    val revMap = intMap.map{(k,v) -> v to k}.toMap()
    assertTrue(intMap.keys.toTypedArray() contentEquals revMap.values.toTypedArray())
    assertTrue(intMap.values.toTypedArray() contentEquals revMap.keys.toTypedArray())
}
Eoin
  • 555
  • 3
  • 9
1

This is my take on a 1:1 map

    private fun <K, V> Map<K, V>.reverseOneToOneMap(): Map<V, K> {
        val result = this.entries.associateBy({ it.value }) { it.key }
        if (result.size != this.size) {
            throw RuntimeException("Map must be 1:1")
        }
        return result
    }
the_prole
  • 8,275
  • 16
  • 78
  • 163