在Kotlin中获取两个具有不同值的地图的交集

时间:2018-08-10 11:35:58

标签: unit-testing collections kotlin functional-programming

我有两个列表:一个包含应保留\n的旧数据,以及应与旧数据合并的新数据。可以通过此单元测试来最好地看出这一点:

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) } 变量的更好方法是什么?我特别不喜欢首先过滤两个列表中包含的键,但是随后必须显式使用actual来获取null安全值。

我该如何改善?

编辑:问题已重新定义

由于Marko而更新:我想做一个十字路口,而不是一个联合。 简单的是在列表上进行交集:

newDogsMap[it.key]!!

但是我真正想要的是地图上的交集:

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

3 个答案:

答案 0 :(得分:3)

您在这里:

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

请注意,我仅关注“您如何在此处省略!!”;-)

当然也可以采用其他方式解决问题:

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

您只需要提供适当的外部条目即可,而且您安全(null-)。

这样,您就可以避免所有那些null安全的操作(例如!!?.mapNotNullfirstOrNull()等)。

另一种方法是将cute作为属性添加到data class Dog并为新狗使用MutableMap。这样,您可以使用自己的合并功能merge来适当地MutableMap。但是,正如您在评论中所说,您不希望使用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) } } ,所以那将不起作用。

如果您不喜欢这里发生的事情,而是想将其隐藏给任何人,则还可以提供适当的扩展功能。但是命名它可能已经不是那么容易了……这是一个例子:

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

现在您可以在要通过其键与地图相交并立即映射到其他值的任何地方调用此函数,如下所示:

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)

请注意,我还不太喜欢命名。但是,您会明白这一点;-)该函数的所有调用方都有一个不错的/简短的接口,无需了解其实际实现方式。但是,功能的维护者当然应该对其进行相应的测试。

也许还可以提供以下帮助?现在,我们引入一个中间对象只是为了更好地命名...仍然没有说服力,但是也许可以帮助某人:

map1

如果您走这条路线,您应该注意应该真正允许中间对象执行的操作,例如现在我可以从该中间体中取出map2A,如果我看一下它的名称,这可能不合适。因此,我们有下一个施工现场;-)

答案 1 :(得分:1)

您可以尝试以下方法:

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 }

这首先将可爱的狗与新狗配对或为空,然后如果有新狗,则映射到该对:新狗和原始地图中的可爱度信息。

更新:Roland是正确的,它返回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 }

在使用flatMap的其他答案中,他的方法很可能是更复杂的解决方案。

答案 2 :(得分:1)

为简化起见,假设您具有以下条件:

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

关于内存和性能的最佳选择是直接在可变映射中更新条目:

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

如果您有理由坚持使用不变的地图,则必须分配临时对象并整体上做更多的工作。您可以这样做:

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