Armin
Armin

Reputation: 135

Kotlin: How to flatten list of Hashmaps

How can I flatten a list of HashMaps in Kotlin?

var listOfMaps: List<Map<String, String>> = listOf(mapOf("test" to "test1"), mapOf("test2" to "test3"), mapOf("test4" to "test5"))

I would like to get:Map<String,String> with all key value paires

Upvotes: 3

Views: 7241

Answers (7)

spyro
spyro

Reputation: 515

This solution works with same keys (and different collection as values) by merging them together instead of overwriting them

/**
 * Merge two maps with lists
 */
fun <K,V>Map<K,Collection<V>>.mergeLists(other: Map<K,Collection<V>>) =
    (this.keys + other.keys).associateWith {key ->
        setOf(this[key], other[key]).filterNotNull()
    }.mapValues { (_,b) -> b.flatten().distinct() }

/**
 * Merge two maps with sets
 */
fun <K,V>Map<K,Collection<V>>.mergeSets(other: Map<K,Collection<V>>) =
    (this.keys + other.keys).associateWith {key ->
        setOf(this[key], other[key]).filterNotNull()
    }.mapValues { (_,b) -> b.flatten().toSet() }

Then use like e.g. listOfMaps.reduce { a, b -> a.mergeSets(b) }

Test:

@Test
fun `should merge two maps with as lists or sets`() {

    // GIVEN
    val map1 = mapOf(
        "a" to listOf(1, 2, 3),
        "b" to listOf(4, 5, 6),
        "c" to listOf(10),
        "e" to emptyList()
    )

    val map2 = mapOf(
        "a" to listOf(1, 9),
        "b" to listOf(7),
        "d" to listOf(null)
    )

    // WHEN
    val mergedAsLists = map1.mergeLists(map2)
    val mergedAsSets = map1.mergeSets(map2)

    // THEN
    listOf(mergedAsLists, mergedAsSets).forEach { merged ->
        assertThat(merged.keys).containsOnly("a", "b", "c", "d", "e")
        assertThat(merged["a"]).containsOnly(1,2,3,9)
        assertThat(merged["b"]).containsOnly(4,5,6,7)
        assertThat(merged["c"]).containsOnly(10)
        assertThat(merged["d"]).containsOnly(null)
        assertThat(merged["e"]).isEmpty()
    }

}

Upvotes: 0

mreparaz
mreparaz

Reputation: 129

you can do something like this if you don't know if the list can be empty.

val map = listOfMaps.fold(mapOf<String, String>()) {acc, value -> acc + value }

If the list never will be empty you can use reduce instead.

Thank you Demigod for the comments

Upvotes: 4

Juan Rada
Juan Rada

Reputation: 3766

val map:Map<String, String> = listOfMaps
    .flatMap { it.entries }
    .associate { it.key to it.value }

Upvotes: 8

Roland
Roland

Reputation: 23232

Well... seeing lots of solutions, I will add my two cents here:

If you don't mind losing the values of duplicated keys you can use something as follows:

listOfMaps.flatMap { it.entries }.associate{ it.key to it.value } // or: it.toPair() if you will
// depending on how large those lists can become, you may want to consider also using asSequence

If you instead want to collect all entries including duplicate keys (i.e. saving all the values), use the following instead (which then gives you a Map<String, List<String>>):

listOfMaps.flatMap { it.entries }.groupBy({ it.key }) { it.value }

Also here the comment regarding asSequence holds...

Finally if you can omit those maps within the list and just use a Pair instead, that will spare you the flatMap { it.entries }-call and make things even easier, e.g. you could just call .toMap() then for the first case and groupBy directly for the second and the question regarding asSequence no longer arises.

Upvotes: 2

Willi Mentzel
Willi Mentzel

Reputation: 29844

If you don't have duplicate keys or don't care for them, you can do it like this:

val flattenedMap = listOfMaps.flatMap { it.toList() }.toMap()

If you have duplicate keys and care for them, you can do it like this:

val flattenedMap = mutableMapOf<String, MutableList<String>>().apply {
    listOfMaps.flatMap { it.toList() }.forEach {
        getOrPut(it.first) {
            mutableListOf()
        }.add(it.second)
    }
}

The result will be Map<String, List<String>> then of course.

Upvotes: 0

Joan
Joan

Reputation: 356

An extra addition to this, if you have single value maps, maybe you want to switch to a List<Prair<String, String>>. In that case, the solution is straight forward:

You would have something like:

var listOfMaps: List<Pair<String, String>> = listOf("test" to "test1", "test2" to "test3", "test4" to "test5")

and toMap() would dsolve it all:

listOfMaps.toMap()

Upvotes: 0

Jahnold
Jahnold

Reputation: 7683

You could use fold:

listOfMaps.fold(
        mutableMapOf<String, String>(),
        { acc, item -> acc.also { it.putAll(item) } }
)

The first parameter mutableMapOf<String, String>() creates an empty map to put the values into. This is called the accumulator

The second parameter is a function which takes two arguments

  1. The accumulator
  2. An item from the original list

This function is run sequentially against all items in the list. In our case it adds all the items from each map to the accumulator.

Note: This function does not account for duplicate keys. If a later map has the same key as an earlier one then the value just gets overridden.

Also note (pun intended): We use acc.also {} as we want to return the actual map, not the return value from the addAll method

Upvotes: 2

Related Questions