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?
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")]
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 ;-)
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
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With