簡體   English   中英

在 Kotlin 中獲取兩個具有不同值的地圖的交集

[英]Get intersection of two maps with different values in Kotlin

我有兩個列表:一個包含應保留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變量的更好方法是什么? 我特別不喜歡我首先過濾兩個列表中包含的鍵,然后我必須使用newDogsMap[it.key]!! 顯式獲取空安全值。

我該如何改進它?

編輯:問題重新定義

感謝 Marko 更新:我想做一個交叉點,而不是一個聯合。 簡單的是在列表上做交集:

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

但我真正想要的是地圖上的交叉點:

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

干得好:

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 )安全。

這樣,您就可以mapNotNull所有那些null操作(例如!!?.mapNotNullfirstOrNull()等)。

另一種方法是將cute作為屬性添加到data class Dog並為新的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)

如果走這條路線,您應該寧願真正地允許該中間對象執行操作,例如,現在我可以從該中間對象中取出map1map2 ,如果我看一下它的名稱,這可能不合適。所以我們有了下一個施工現場;-)

您可以嘗試類似:

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的其他答案中,他的方法很可能是更復雜的解決方案。

為簡化起見,假設您具有以下條件:

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

你可以試試這個:

val intersection = map1.mapNotNull { (map1Key, map1Value) ->
    map2[map1Key]
        ?.let { map2Value -> map1Value to map2Value }
        ?.let { pair -> map1Key to pair }
}.toMap()

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM