Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Get intersection of two maps with different values in Kotlin

I have two lists: one with old data where a Boolean should preserved, and new data that should be merged with the old data. This can best be seen by this unit test:

@Test
fun mergeNewDataWithOld() {

    // dog names can be treated as unique IDs here
    data class Dog(val id: String, val owner: String)


    val dogsAreCute: List<Pair<Dog, Boolean>> = listOf(
            Dog("Kessi", "Marc") to true,
            Dog("Rocky", "Martin") to false,
            Dog("Molly", "Martin") to true
    )

    // loaded by the backend, so can contain new data
    val newDogs: List<Dog> = listOf(
            Dog("Kessi", "Marc"),
            Dog("Rocky", "Marc"),
            Dog("Buddy", "Martin")
    )

    // this should be the result: an intersection that preserves the extra Boolean,
    // but replaces dogs by their new updated data
    val expected = listOf(
            newDogs[0] to true,
            newDogs[1] to false
    )

    // HERE: this is the code I use to get the expected union that should contain
    // the `Boolean` value of the old list, but all new `Dog` instances by the new list:
    val oldDogsMap = dogsAreCute.associate { it.first.id to it }
    val newDogsMap = newDogs.associateBy { it.id }
    val actual = oldDogsMap
            .filterKeys { newDogsMap.containsKey(it) }
            .map { newDogsMap[it.key]!! to it.value.second }

    assertEquals(expected, actual)
}

My question here is: What is a better way to write the code to get my actual variable? I especially dislike that I first filter the keys that are contained by both lists, but then I have to use newDogsMap[it.key]!! explicitly to get the null-safe values.

How can I improve it?

Edit: Problem redefined

Updated thanks to Marko: I want to do an intersection, not a union. What is easy is to do an intersection on lists:

val list1 = listOf(1, 2, 3)
val list2 = listOf(4, 3, 2)
list1.intersect(list2)
// [2, 3]

But what I actually want is an intersection on maps:

val map1 = mapOf(1 to true, 2 to false, 3 to true)
val map2 = mapOf(4 to "four", 3 to "three", 2 to "two")
// TODO: how to do get the intersection of maps?
// For example something like:
// [2 to Pair(false, "two"), 3 to Pair(true, "three")]
like image 447
mreichelt Avatar asked Aug 10 '18 11:08

mreichelt


3 Answers

Here you go:

val actual = oldDogsMap.flatMap { oDEntry ->
        newDogsMap.filterKeys { oDEntry.key == it }
                .map { it.value to oDEntry.value.second }
    }

Note that I only concentrated on the "how do you omit the !! in here" ;-)

Or the other way around works of course too:

val actual = newDogsMap.flatMap { nDE ->
        oldDogsMap.filterKeys { nDE.key == it }
                .map { nDE.value to it.value.second }
    }

You just need to have the appropriate outer entry available and you are (null-)safe.

That way you spare all those null-safe operations (e.g. !!, ?., mapNotNull, firstOrNull(), etc.).

Another approach is to add cute as a property to the data class Dog and use a MutableMap for the new dogs instead. This way you can merge the values appropriately using your own merge-function. But as you said in comments, you do not want a MutableMap, so that's not going to work then.

If you don't like what's going on here and rather want to hide it to anyone, you can also just supply an appropriate extension function. But naming it may already not be so easy... Here is an example:

inline fun <K, V, W, T> Map<K, V>.intersectByKeyAndMap(otherMap : Map<K, W>, transformationFunction : (V, W) -> T) = flatMap { oldEntry ->
        otherMap.filterKeys { it == oldEntry.key }
                .map { transformationFunction(oldEntry.value, it.value) }
}

And now you can call this function anywhere you want to intersect maps by their keys and immediately map to some other value, like the following:

val actual = oldDogsMap.intersectByKeyAndMap(newDogsMap) { old, new -> new to old.second }

Note that I am not a huge fan of the naming yet. But you will get the point ;-) All the callers of the function have a nice/short interface and don't need to understand how it's really implemented. The maintainer of the function however should of course test it accordingly.

Maybe also something like the following helps? Now we introduce an intermediate object just to get the naming better... Still not that convinced, but maybe it helps someone:

class IntersectedMapIntermediate<K, V, W>(val map1 : Map<K, V>, val map2 : Map<K, W>) {
    inline fun <reified T> mappingValuesTo(transformation: (V, W) -> T) = map1.flatMap { oldEntry ->
        map2.filterKeys { it == oldEntry.key }
                .map { transformation(oldEntry.value, it.value) }
    }
}
fun <K, V, W> Map<K, V>.intersectByKey(otherMap : Map<K, W>) = IntersectedMapIntermediate(this, otherMap)

If you go this route, you should rather take care of what the intermediate object should really be allowed to do, e.g. now I can take map1 or map2 out of that intermediate, which might not be appropriate if I look at its name... so we have the next construction site ;-)

like image 129
Roland Avatar answered Sep 28 '22 01:09

Roland


To simplify things, let's say you have the following:

val data = mutableMapOf("a" to 1, "b" to 2)
val updateBatch = mapOf("a" to 10, "c" to 3)

The best option in terms of memory and performance is to update the entries directly in the mutable map:

data.entries.forEach { entry ->
    updateBatch[entry.key]?.also { entry.setValue(it) }
}

If you have a reason to stick to immutable maps, you'll have to allocate temporary objects and do more work overall. You can do it like this:

val data = mapOf("a" to 1, "b" to 2)
val updateBatch = mapOf("a" to 10, "c" to 3)

val updates = updateBatch
        .filterKeys(data::containsKey)
        .mapValues { computeNewVal(data[it.key]) }
val newData = data + updates
like image 23
Marko Topolnik Avatar answered Sep 28 '22 02:09

Marko Topolnik


You could try something like:

val actual = dogsAreCute.map {cuteDog -> cuteDog to newDogs.firstOrNull { it.id ==  cuteDog.first.id } }
            .filter { it.second != null }
            .map { it.second to it.first.second }

This first pairs cute dogs to a new dog or null, then if there is a new dog, maps to the pair: new dog and cuteness information from the original map.

Update: Roland is right, this returns the type of List<Pair<Dog?, Boolean>>, so here is the proposed fix for the type for this approach:

val actual = dogsAreCute.mapNotNull { cuteDog ->
        newDogs.firstOrNull { it.id == cuteDog.first.id }?.let { cuteDog to it } }
            .map { it.second to it.first.second }

Most probably his approach in the other answer using flatMap is a more sophisticated solution.

like image 27
DVarga Avatar answered Sep 28 '22 03:09

DVarga