Получите пересечение двух карт с разными значениями в Котлине

У меня есть два списка: один со старыми данными, где следует сохранить 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]!!, чтобы получить нулевые безопасные значения.

Как я могу это улучшить?

Обновлено: проблема переопределена

Обновлено благодаря Марко: я хочу сделать перекресток, а не объединение. Легко сделать пересечение списков:

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

Вы говорите «объединение», но ваш код делает пересечение. expected содержит только обновленные записи (которые уже существуют и являются частью обновленного пакета).

Marko Topolnik 10.08.2018 14:03

Вы правы - я хочу сделать перекресток. Но мне нужно пересечение на картах - насколько я знаю, Kotlin поддерживает пересечение только в списках.

mreichelt 13.08.2018 09:12

Если обе ваши карты содержат одинаковые типы значений, вы можете использовать merge, если у вас есть MutableMap. Если вы этого не сделали и не хотите работать с null-safe-операторами, то я не знаю более простого подхода, чем показанный. Вместо этого он просто усложняется, или вы пишете больше кода, и я не думаю, что он станет более читаемым, если опустить другой null-safe-operator ;-)

Roland 13.08.2018 11:19
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
6
3
3 128
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 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 (например, !!, ?., mapNotNull, firstOrNull() и т. д.).

Другой подход - добавить 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 или map2 из этого промежуточного звена, что может не подходить, если я посмотрю на его название ... так что у нас есть следующая строительная площадка ;-)

Большое спасибо за ваш ответ! Мне это нравится, потому что он решает мою проблему, заключающуюся в том, что я не обрабатываю значение NULL явно - и это работает! Тем не менее, мне интересно, может ли он быть лучше с точки зрения читаемости - я почти уверен, что в будущем я наткнусь на этот код и задаюсь вопросом, что он делает. Как указал Марко, я хочу сделать пересечение, хотя Kotlin предоставляет только пересечение итераций. Что-то вроде oldIds.intersect(newIds) работает, но мне понадобится oldDogsMap.intersect(newDogsMap), который возвращает карту, содержащую пару обоих значений. Есть ли функция, которая это делает?

mreichelt 12.08.2018 16:26

Что ж, есть что-то подобное, но с вашим текущим data class или имеющейся у вас настройкой это сделать не так просто. Я обновлю свой ответ, чтобы показать вам другой подход, который может сработать для вас, например, добавив cute в качестве свойства в Dog.

Roland 13.08.2018 11:07

Это просто мой неудачно сделанный пример - класс Dog не может (и не должен) содержать свою метаинформацию, потому что она используется только в одном месте кода.

mreichelt 13.08.2018 11:12

ну ... если бы это было возможно, вы могли бы выполнить весь пример, используя MutableMap и его merge-метод. Если вы этого не сделаете, тогда ... ну ... вы все равно можете убедиться, что он выглядит как две равные карты (например, обе содержат Pair, хотя Pair второго содержит null в качестве значений ...) ... но тогда так много не выиграешь ;-)

Roland 13.08.2018 11:15

Если вы не хотите улучшать data class или использовать MutableMap, тогда я бы предпочел скрыть всю эту функциональность в первую очередь (функция расширения?), И поэтому вызывающие абоненты увидят хороший код, тогда как сопровождающий должен обеспечить функциональность с помощью соответствующих тестов. ... Беспроигрышный вариант для всех ;-) Я обновил ответ по этому поводу ....

Roland 13.08.2018 11:47

В итоге я использовал ваше решение с функцией расширения - в конце концов я назвал его intersectWith. Как указал Марко, реализация может использовать изменяемую карту для повышения производительности, но пока я выбрал решение flatMap. Код здесь для тех, кому в будущем понадобится функция intersectWith: gist.github.com/mreichelt/a6e4b85c204962e4f1248324ec3bb418

mreichelt 13.08.2018 13:31

Я тоже так думал вначале ... :-) Но при вызове простого intersect я ожидал, что две карты будут пересекаться, но что это вообще будет означать с картами? Поэтому я подумал, что добавление byKey, вероятно, имеет смысл. С другой стороны, я подумал, что, вероятно, лучше буду ожидать, что в качестве возвращаемого значения будет Map, но сейчас мы возвращаем List. Так что поигравшись, я остановился на intersectByKeyAndMap; еще раз: я не так уверен в этом. Но, честно говоря, intersectWith кажется мне немного вводящим в заблуждение ... (как насчет: map1.intersectByKey(map2).mappingTo(transform)?) ... но это совсем другая проблема ... ;-)

Roland 13.08.2018 13:41

Вы правы - и я ожидал, что метод intersectByKey или intersectWith также вернет Map, а не List. Скоро обновлю код ™ :-)

mreichelt 13.08.2018 14:19

хм ... однако crossctBykey не может вернуть карту напрямую, так как вы не знаете, как ее преобразовать ... Создание карты, в которой она вам не нужна, тоже не так полезно ... Это, вероятно, причина, почему merge работает с отдельными ключами / значениями и принимает функцию преобразования :-) Я обновил ответ, чтобы показать одно из многих возможных решений для промежуточных объектов ;-) Чем больше я думаю об этом ... мы делаем то, чего не должны не делайте ... настройте свой класс данных соответствующим образом, и все будет работать лучше ;-)

Roland 13.08.2018 14:25

Вы можете попробовать что-то вроде:

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 }

Это сначала связывает милых собак с новой собакой или нулевым, затем, если есть новая собака, сопоставляет пару: новая собака и информация о привлекательности из исходной карты.

Обновлять: Роланд прав, это возвращает тип 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 является более сложным решением.

Но это возвращает List<Pair<Dog?, Boolean>> вместо List<Pair<Dog, Boolean>> ... возможно, не то, что хотел OP ...

Roland 10.08.2018 14:27

Вместо этого вам может потребоваться что-то вроде dogsAreCute.mapNotNull {cuteDog -> newDogs.firstOrNull { it.id == cuteDog.first.id }?.let { cuteDog to it } }.map { it.second to it.first.second } ... Однако затем вы просто заменили небезопасный оператор !! некоторыми безопасными операторами ?. / firstNotNull / mapNotNull, которые могут быть не такими удобочитаемыми, как сам !! ...

Roland 10.08.2018 14:30

@ Роланд: Ах да, ты прав. Я настолько избалован умным кастом, что даже не проверил реальный тип возврата :)

DVarga 10.08.2018 15:10

Для упрощения предположим, что у вас есть следующее:

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

Это выглядит элегантно, но удаляет данные первой карты, которые мне нужно сохранить. Я не хочу полностью заменять данные, но хочу сохранить некоторую метаинформацию о них.

mreichelt 13.08.2018 10:34

Вместо entry.setValue(it) вы можете писать все, что хотите, чтобы объединить новые данные со старыми.

Marko Topolnik 13.08.2018 10:43

Но он вводит изменяемость исходной карты, а также класса данных.

mreichelt 13.08.2018 11:17

Это вам решать. Изменяемость более эффективна, но если у вас есть причина сохранить все неизменным и скопировать данные, тогда вам придется вычислить пересечение, а затем вычислить на его основе новую карту.

Marko Topolnik 13.08.2018 11:32

Другие вопросы по теме