Merge values in map kotlin

Viewed 38856

I need merge maps mapA andmapB with pairs of "name" - "phone number" into the final map, sticking together the values for duplicate keys, separated by commas. Duplicate values should be added only once. I need the most idiomatic and correct in terms of language approach.

For example:

val mapA = mapOf("Emergency" to "112", "Fire department" to "101", "Police" to "102")
val mapB = mapOf("Emergency" to "911", "Police" to "102")

The final result should look like this:

{"Emergency" to "112, 911", "Fire department" to "101", "Police" to "102"}

This is my function:

fun mergePhoneBooks(mapA: Map<String, String>, mapB: Map<String, String>): Map<String, String> {
    val unionList: MutableMap <String, String> = mapA.toMutableMap()
    unionList.forEach { (key, value) -> TODO() } // here's I can't come on with a beatiful solution

    return unionList
}
13 Answers

How about:

val unionList = (mapA.asSequence() + mapB.asSequence())
    .distinct()
    .groupBy({ it.key }, { it.value })
    .mapValues { (_, values) -> values.joinToString(",") }

Result:

{Emergency=112,911, Fire department=101, Police=102}

This will:

  • produce a lazy Sequence of both maps' key-value pairs
  • group them by key (result: Map<String, List<String>)
  • map their values to comma-joined strings (result: Map<String, String>)

You can do the following:

(mapA.keys + mapB.keys).associateWith {
    setOf(mapA[it], mapB[it]).filterNotNull().joinToString()
}
  1. put all keys in a set
  2. iterate over that set and and associate each element with the set of values
  3. remove the null values from the value set
  4. concatenate the elements in the value list using joinToString().

While I looked at the other solutions I couldn't believe that there isn't an easier way (or ways as easy as the accepted answer without the need to recreate a Map, intermediate new lists, etc.). Here are 3 (of many ;-)) solutions I came up with:

  1. Using the keys and mapping the values later:

     (mapA.keys.asSequence() + mapB.keys)
         .associateWith {
           sequenceOf(mapA[it], mapB[it]) // one of the sides may have null values in it (i.e. no entry in the map)...
               .filterNotNull()
               .distinct()
               .toList() // or if you require/prefer, do the following instead: joinToString()
         }
    
  2. Using groupingBy and fold (or have a look at: Group by key and fold each group simultaneously (KEEP)):

     (mapA.asSequence() + mapB.asSequence())
       .groupingBy { it.key }
       .fold({ _, _ -> mutableSetOf() }) { _, accumulator, element ->
         accumulator.apply {
           add(element.value)
         }
       }
    

    You could also just use an empty String instead and concatenate in the fold operation the way you need it. My first approach just used a sequenceOf instead of the MutableSet. It depends on what you require and what you want to do with the result afterwards. Be sure to use the overloaded fold-function that accepts an initial value selector, which creates a new initial value every time a new key is encountered. Thanks xzt for noting it.

  3. Using Javas Map.merge, but ignoring duplicates in the value and also just concatenating the values:

     val mergedMap: Map<String, String> = mapA.toMutableMap().apply {
       mapB.forEach { key, value ->
         merge(key, value) { currentValue, addedValue ->
           "$currentValue, $addedValue" // just concatenate... no duplicates-check..
         }
       }
     }
    

    This, of course, can also be written differently, but this way we ensure that mergedMap is still just a Map<String, String> when accessed again.

In Kotlin you could do this:

fun main() {
    val map1 = mapOf("A" to 1, "B" to 2)
    val map2 = mapOf("A" to 5, "B" to 2)
    val result: Map<String, Int> = listOf(map1, map2)
        .fold(mapOf()) { accMap, map ->
            accMap.merge(map, Int::plus)
        }
    println(result) // Prints: {A=6, B=4}
}

private fun <T, V> Map<T, V>.merge(another: Map<T, V>, mergeFunction: (V, V) -> V): Map<T, V> =
    toMutableMap()
        .apply {
            another.forEach { (key, value) ->
                merge(key, value, mergeFunction)
            }
        }

Here is my approach with universal map-merging helper function:

fun <K, V, R> Pair<Map<K, V>, Map<K, V>>.merge(merger: (V?, V?) -> R): Map<K, R> {
    return (first.keys.asSequence() + second.keys.asSequence())
            .associateWith { merger(first[it], second[it]) }
}

fun main() {
    val mapA = mapOf("Emergency" to "112", "Fire department" to "101", "Police" to "102")
    val mapB = mapOf("Emergency" to "911", "Police" to "102")
    val result = (mapA to mapB).merge { a, b -> 
            listOf(a, b).filterNotNull().distinct().joinToString(",", "(", ")") }
    println(result)
}

Output:

{Emergency=(112,911), Fire department=(101), Police=(102)}

I would write something like

fun Map<String, String>.mergeWith(another: Map<String, String>): Map<String, String> {
  val unionList: MutableMap<String, String> = toMutableMap()
  for ((key, value) in another) {
    unionList[key] = listOfNotNull(unionList[key], value).toSet().joinToString(", ")
  }
  return unionList
}

val mergedMap = mapA.mergeWith(mapB)
    val mapA = mapOf("Emergency" to "112", "Fire department" to "101", "Police" to "102")
    val mapB = mapOf("Emergency" to "911", "Police" to "102")

    val result = (mapA.entries + mapB.entries)
        .groupBy({ it.key }, { it.value })
        .mapValues {(_, value) -> 
            value.joinToString(", ")
        }

A more generic approach (as this post comes up when searching for kotlin and merging maps):

fun <K, V1, V2, R> Map<K, V1>.mergeReduce(other: Map<K, V2>, reduce: (key: K, value1: V1?, value2: V2?) -> R): Map<K, R> =
    (this.keys + other.keys).associateWith { reduce(it, this[it], other[it]) }

It allows for Maps with different types of values to be merged, increased freedom with a custom reducer and increased readability.

Your problem can than be solved as:

mapA.mergeReduce(mapB) { _, value1, value2 -> listOfNotNull(value1, value2).joinToString(", ") }

Another approach:

val mapA = mapOf("Emergency" to "112", "Fire department" to "101", "Police" to "102")
val mapB = mapOf("Emergency" to "911", "Police" to "102")

val result = mapA.toMutableMap()
mapB.forEach {
    var value = result[it.key]
    value = if (value == null || value == it.value) it.value else value + ", ${it.value}"
    result[it.key] = value
}

Or using infix extension function:

infix fun Map<String, String>.mergeWith(anotherMap: Map<String, String>): Map<String, String> {
    val result = this.toMutableMap()
    anotherMap.forEach {
        var value = result[it.key]
        value = if (value == null || value == it.value) it.value else value + ", ${it.value}"
        result[it.key] = value
    }
    return result
}

val result = mapA mergeWith mapB

For some strange reason not part of SDK.

Created my own extension:

fun <K,V>Map<K,Collection<V>>.merge(other: Map<K,Collection<V>>) =
    (this.keys + other.keys).associateWith {key ->
        setOf(this[key], other[key]).filterNotNull()
    }.mapValues { (a,b) -> b.flatten().toSet() 
}

Use like

val map1 = mapOf("a" to listOf(1,2,3), "b" to listOf(4,5,6))
val map2 = mapOf("a" to listOf(1,9), "b" to listOf(7))
val merge = map1.merge(map2) // {"a" = [1,2,3,9], "b" = [4,5,6,7]}

This does not exactly answer the question posed by the OP, but I think many people coming to this post will, given its title, be looking for a simple way to merge two maps. The solution is that the + operator does the job and merges with deduplication. BUT in the event of a key being duplicated, the last value wins, you don't get the list of all values.

val mapA = mapOf("Emergency" to "112", "Fire department" to "101", "Police" to "102")
val mapB = mapOf("Emergency" to "911", "Police" to "102", "Ambulance" to "103")
val mapC = mapA + mapB
println(mapC)

gives

{Emergency=911, Fire department=101, Police=102, Ambulance=103}

Here's my solution:

val result = (mapA + (mapB - mapA.keys)).mapValues {
    (setOf(it.value) + mapB[it.key]).filterNotNull().joinToString()
}

It creates a map of A plus the values from B that are not in A. Then it maps all values to a set and adds the value from B to that set, ultimately removing all null values from the set and transforming it into a list, which you can use to create the desired output format.

(mapA.data.asSequence() + mapB.asSequence())
   .map { Pair(it.key, it.value) }
   .toMap()
Related