Get intersection of two maps with different values in Kotlin

The name of the pictureThe name of the pictureThe name of the pictureClash Royale CLAN TAG#URR8PPP



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:


Boolean


@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.


actual


newDogsMap[it.key]!!



How can I improve it?



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")]





You say "union" but your code does an intersection. expected contains only the updated entries (that both existed already and are a part of the updated batch).
– Marko Topolnik
Aug 10 at 12:03


expected





You are right - I want to do an intersection. But what I need is an intersection on maps - as far as I know Kotlin only supports intersection on lists.
– mreichelt
Aug 13 at 7:12





If both of your maps contain equal value types, you can use merge as long as you have a MutableMap. If you haven't and you do not want to work with null-safe-operators, then I don't know any simpler approach then the one shown. It just gets more complicated instead or you write more code and I don't think that it gets more readable by omitting another null-safe-operator ;-)
– Roland
Aug 13 at 9:19



merge


MutableMap


null


null




3 Answers
3



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.


null



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


null


!!


?.


mapNotNull


firstOrNull()



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.


cute


data class Dog


MutableMap


merge


MutableMap



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 ;-)


map1


map2





Thanks a lot for your answer! I like it because it solves my issue to not handle nullability explicitly - and it works! Still I wonder if it could be better readability-wise - I'm pretty sure a future me would stumble over this code and wonder what it does. As Marko pointed out, I want to do an intersection - though Kotlin does only provide an intersection of iterables. Something like oldIds.intersect(newIds) works, but I would need oldDogsMap.intersect(newDogsMap) that returns a map containing a pair of both values. Is there a function that does that?
– mreichelt
Aug 12 at 14:26


oldIds.intersect(newIds)


oldDogsMap.intersect(newDogsMap)





Well there is something similar, but with your current data class or the setup you have it isn't doable that easily. I will update my answer to show you another approach, that might work for you, i.e. by adding cute as a property to Dog.
– Roland
Aug 13 at 9:07


data class


cute


Dog





This is just my badly made example - the Dog class can't (and shouldn't) contain its meta info, because that is just used at one point in the code.
– mreichelt
Aug 13 at 9:12


Dog





well... if it could, you could do the whole example using a MutableMap and its merge-method. If you don't, then... well.. you could still ensure that it looks as two equal maps (e.g. both contain a Pair even though the Pair of the second contains null as values... ) ... but then you wouldn't win so much ;-)
– Roland
Aug 13 at 9:15


MutableMap


merge


Pair


Pair


null





If you do not want to enhance the data class, nor use a MutableMap then I would rather hide all that functionality in the first place (extension function?) and so the callers see nice code, whereas the maintainer needs to ensure the functionality with appropriate tests... Win-win for all ;-) I updated the answer in that regard....
– Roland
Aug 13 at 9:47



data class


MutableMap



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





This looks elegant, but it removes data of the first map that I need to be preserved. I don't want to replace the data entirely, but I want to keep some meta info on it.
– mreichelt
Aug 13 at 8:34





Instead of entry.setValue(it) you can write whatever you want to merge the new data into the old.
– Marko Topolnik
Aug 13 at 8:43


entry.setValue(it)





But it introduces mutability of the original map as well as the data class.
– mreichelt
Aug 13 at 9:17





This is up to you to decide. Mutability is more efficient, but if you have a reason to keep everything immutable and copy the data, then you have to go with computing the intersection and then computing the new map from it.
– Marko Topolnik
Aug 13 at 9:32



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:


List<Pair<Dog?, Boolean>>


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.


flatMap





But this returns List<Pair<Dog?, Boolean>> instead of List<Pair<Dog, Boolean>>... maybe not what the OP wanted...
– Roland
Aug 10 at 12:27


List<Pair<Dog?, Boolean>>


List<Pair<Dog, Boolean>>





You may require something like dogsAreCute.mapNotNull cuteDog -> newDogs.firstOrNull it.id == cuteDog.first.id ?.let cuteDog to it .map it.second to it.first.second instead... However then you just replaced the !! unsafe operator with some ?./firstNotNull/mapNotNull safe operators, which might not be as readable as the !! itself...
– Roland
Aug 10 at 12:30


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


!!


?.


firstNotNull


mapNotNull


!!





@Roland: Ah ye, you are right. I'm so spoiled by smart cast, I did not even check the actual return type :)
– DVarga
Aug 10 at 13:10







By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Popular posts from this blog

Firebase Auth - with Email and Password - Check user already registered

Dynamically update html content plain JS

How to determine optimal route across keyboard