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

mre*_*elt 5 collections unit-testing functional-programming kotlin

我有两个列表:一个带有Boolean应保留a的旧数据,另一个应与旧数据合并的新数据。可以通过以下单元测试来最好地看出这一点:

@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)
}
Run Code Online (Sandbox Code Playgroud)

我的问题是:编写代码来获取actual变量的更好方法是什么?我特别不喜欢首先过滤两个列表中包含的键,但是随后我必须newDogsMap[it.key]!!显式使用它来获取null安全值。

我该如何改善?

编辑:问题已重新定义

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

val list1 = listOf(1, 2, 3)
val list2 = listOf(4, 3, 2)
list1.intersect(list2)
// [2, 3]
Run Code Online (Sandbox Code Playgroud)

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

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")]
Run Code Online (Sandbox Code Playgroud)

Rol*_*and 3

干得好:

val actual = oldDogsMap.flatMap { oDEntry ->
        newDogsMap.filterKeys { oDEntry.key == it }
                .map { it.value to oDEntry.value.second }
    }
Run Code Online (Sandbox Code Playgroud)

请注意,我只关注“你如何省略!!这里的”;-)

或者反过来当然也可以:

val actual = newDogsMap.flatMap { nDE ->
        oldDogsMap.filterKeys { nDE.key == it }
                .map { nDE.value to it.value.second }
    }
Run Code Online (Sandbox Code Playgroud)

您只需要有适当的外部入口即可,您就是(null-)安全的。

这样你就可以省去所有那些null安全操作(例如!!?.mapNotNullfirstOrNull()等)。

另一种方法是将cutea 作为属性添加到 ,data class Dog并使用 aMutableMap作为新狗。这样您就可以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) }
}
Run Code Online (Sandbox Code Playgroud)

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

val actual = oldDogsMap.intersectByKeyAndMap(newDogsMap) { old, new -> new to old.second }
Run Code Online (Sandbox Code Playgroud)

请注意,我还不太喜欢这个命名。但你会明白的;-) 该函数的所有调用者都有一个漂亮/简短的接口,并且不需要了解它是如何真正实现的。然而,该功能的维护者当然应该对其进行相应的测试。

也许像下面这样的东西也有帮助?现在我们引入一个中间对象只是为了更好地命名......仍然不那么令人信服,但也许它可以帮助某人:

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)
Run Code Online (Sandbox Code Playgroud)

如果你走这条路,你应该关心中间对象真正应该被允许做什么,例如现在我可以取出map1map2取出该中间对象,如果我看它的名字,这可能不合适......所以我们有下一个施工现场;-)