1. Overview
We can reverse a map by swapping the key-value pairs. Sometimes, this can be useful in situations where we need to perform a lookup based on the values rather than the keys of a map.
In this tutorial, we’ll explore how to reverse a map in Kotlin.
2. Introduction to the Problem
An example can explain the problem quickly. Let’s say we have a map that contains the price data of a supermarket:
val priceMap = mapOf(
"Apple" to 1.99,
"Orange" to 3.49,
"Milk" to 1.79,
"Pizza" to 4.99,
)
As we can see, we constructed priceMap using the factory function mapOf(). The keys are product names in String, and the values are the products’ prices in type Double. We aim to reverse the map and change Map<String, Double> into Map<Double, String>. Also, the reversed map should look like this:
val expectedMap = mapOf(
1.99 to "Apple",
3.49 to "Orange",
1.79 to "Milk",
4.99 to "Pizza",
)
We’ll see different approaches to solving the problem. For simplicity, we’ll use unit test assertions to verify whether the result after the reversing is expected.
3. Using the map() and the toMap() Functions
Let’s first look at the implementation:
val result = priceMap.map { (k, v) -> v to k }.toMap()
assertEquals(expectedMap, result)
Map‘s map() function allows us to apply a transformation function on each entry in the map and get a list of objects in the transformed type. So, our idea is to use the map() function to transform each Entry<K, V> into a Pair<V, K> so that we convert Map<K, V> into List<Pair<V, K>>. Then, finally, we can call the toMap() function on the transformed List
4. Using the associateBy() or the associate() Functions
Kotlin’s Iterable type provides the associateBy() and the associate() functions to convert the Iterable object into a Map.
Map itself is not an Iterable. But we can get its entries in a list through Map.entries(). Then, we can apply associateBy() or associate() on the list of map entries to reverse the map. Let’s first look at how to use the associateBy() function to achieve that:
val result = priceMap.entries.associateBy({ it.value }) { it.key }
assertEquals(expectedMap, result)
Alternatively, we can reverse the given map using the associate() function:
val result = priceMap.entries.associate { (k, v) -> v to k }
assertEquals(expectedMap, result)
5. Creating an Empty MutableMap and Then Using put()
Another idea for reversing a map is creating a MutableMap and then calling put() while reversing the entries:
val result = mutableMapOf<Double, String>().apply {
priceMap.forEach { (k, v) -> put(v, k) }
}.toMap()
assertEquals(expectedMap, result)
As we can see in the code above, after creating an empty mutable map, we’ve used the apply() scope function to fill it.
It’s worth mentioning that we called toMap() at the end to convert the MutableMap to a Map (immutable).
6. When the Given Map Contains Duplicate Values
So far, we’ve seen various approaches to reversing a map. If we look at the priceMap example carefully, we can find the map doesn’t contain duplicate price (Double) values. In other words, every key’s value is unique in the map.
However, sometimes, we may be given a map with duplicate values. For example, let’s add a few new product price entries to the map:
val priceMap2 = mapOf(
"Apple" to 1.99,
"Orange" to 3.49,
"Milk" to 1.79,
"Pizza" to 4.99,
// add a few new products:
"Egg" to 1.99,
"Strawberry" to 3.49,
"Chicken" to 4.99,
"Grape" to 4.99,
)
As we can see, this time, the same price value may be associated with different products. For example, the price of Pizza and Grape are the same (4.99). In this section, we’ll discuss handling duplicate values when we need to reverse a map.
6.1. Overwriting Happens During Reversing
First, let’s try our “*associate()*” solution on the new priceMap2 map to see what result it produces:
val result = priceMap2.entries.associate { (k, v) -> v to k }
val overwrittenMap = mapOf(
1.99 to "Egg",
3.49 to "Strawberry",
1.79 to "Milk",
4.99 to "Grape",
)
assertEquals(overwrittenMap, result)
As the test shows, for duplicate price values, the latest entry with the same price overwrites the previous entries after reversing. For example, for the price of 4.99, we have “Pizza“, “Chicken” and “Grape” in the original map. After reversing the map using associate(), “Pizza” and “Chicken” are overwritten by “Grape“.
This result is acceptable only if it follows the requirement. Otherwise, we must raise an exception if duplicate values are detected in the input map. So next, let’s see it in action.
6.2. Creating an Extension to Return Result
Let’s create an extension function to perform the reverse() operation on Map:
fun <K, V> Map<K, V>.reverse(): Result<Map<V, K>> {
return runCatching {
this.entries.associate { (k, v) -> v to k }
.also { reversedMap -> require(this.size == reversedMap.size, { "Reversing failed, the map contains duplicated values" }) }
}
}
Next, let’s walk through the code quickly to understand what this extension function does.
We’ve used Kotlin’s runCatching{ … } to handle exceptions in the code above. In the runCatching block, again, we applied the “*associate()*” approach to reversing the map. After reversing the map, we check if the original and the reversed map have the same size using require() to ensure no overwrite occurs. It’s worth mentioning that the require() function throws IllegalArgumentException with the defined message when the Boolean expression is evaluated as false.
runCatching returns a Result
Next, let’s create a test to see how to use the reverse() extension function. First, let’s test with priceMap, which doesn’t have duplicate values:
val result1 = priceMap.reverse()
assertTrue(result1.isSuccess)
assertEquals(expectedMap, result1.getOrThrow())
As we can see, we can get the reversed map by calling Result‘s getOrThrow(). Next, let’s check if reverse() can throw the expected exception when handling priceMap2:
// with duplicate values
val result2 = priceMap2.reverse()
assertTrue(result2.isFailure)
assertThrows<IllegalArgumentException> { result2.getOrThrow() }
When we run the test, it passes. So, the reverse() extension works as expected.
So, the reverse() extension function raises an exception when the map to reverse contains duplicate values. But can’t we really reverse a map carrying duplicate values?
Next, let’s look at an approach to grouping the duplicate values’ keys.
6.3. Grouping Duplicate Values’ Keys
Simply put, grouping the duplicate values’ keys means transforming Map<K, V> to Map<V, List
val expectedMap2 = mapOf(
1.99 to listOf("Apple", "Egg"),
3.49 to listOf("Orange", "Strawberry"),
1.79 to listOf("Milk"),
4.99 to listOf("Pizza", "Chicken", "Grape"),
)
Next, let’s see how to group duplicate values’ keys:
val result = priceMap2.toList().groupBy { thePair -> thePair.second }
.mapValues { entry -> entry.value.map { it.first } }
assertEquals(expectedMap2, result)
The Map.toList() function returns a list of Pair<K, V> objects in the code above. Then, as the name implies, the groupBy() function groups the list of pairs by the second value, the price. So, by now, we have a map in the type Map<Double, List<Pair<String, Double>>. Finally, we use the mapValues() function to convert List<Pair<String, Double> to List
7. Conclusion
In this article, we’ve explored how to reverse a map through examples. If we look at the map’s key-value associations, there could be two scenarios: the map contains duplicate values or doesn’t.
Therefore, we’ve discussed different approaches to handle the case that the input map contains duplicate values. We’ve introduced a reverse() extension function to return a Result object. When the given map holds duplicate values, the reverse() function returns the Result object as a failure with an IllegalArgumentException.
Finally, we’ve seen how to use the groupBy() and mapValues() functions to reverse the input map and group duplicate values’ keys.
As usual, the implementation of all these examples is over on GitHub.