amp
amp

Reputation: 12352

Get differences between 2 maps in kotlin

I am trying to detect the differences between two maps in kotlin.

I have setup the below sample to make it easier to explain what I am trying to achieve:

fun main() = runBlocking {

    val firstMapOfAnimals = mapOf(
        Pair("key1", Dog(name = "Dog aaa")),
        Pair("key2", Dog(name = "Dog bbb", breed = "Bulldog")),
        Pair("key4", Cat(name = "Cat ddd", color = "White")),
        Pair("key5", Cat(name = "Cat eee", color = "Blue")),
        Pair("key6", Cat(name = "Cat fff", color = "Blue"))
    )

    val secondMapOfAnimals = mapOf(
        Pair("key2", Dog(name = "Dog BBB")),
        Pair("key3", Dog(name = "Dog CCC")),
        Pair("key4", Cat(name = "Cat DDD", color = "Grey")),
        Pair("key6", Dog(name = "Dog FFF", breed = "Husky"))
    )

    val diffResult = diff(firstMapOfAnimals, secondMapOfAnimals)

    val expectedResultMap = mapOf(
        Pair("key2", Dog(name = "Dog BBB", breed = "Bulldog")),
        Pair("key3", Dog(name = "Dog CCC")),
        Pair("key4", Cat(name = "Cat DDD", color = "Grey")),
        Pair("key6", Dog(name = "Dog FFF", breed = "Husky"))
    )

    println("Actual: $diffResult")
    println("Expected: $expectedResultMap")

}

private fun diff(
    firstMap: Map<String, Animal>,
    secondMap: Map<String, Animal>
): Map<String, Animal> {
    val result = mapOf<String, Animal>()
    //TODO: get differences between firstMap and secondMap
    return result
}

abstract class Animal

data class Dog(
    val name: String,
    val breed: String = "breed"
) : Animal()

data class Cat(
    val name: String,
    val color: String = "black"
) : Animal()

My real code is a bit more complex but I want to start simple.

Basically, I need to write the diff() method body to achieve the expected printed result. Currently, this is the output:

Actual: {}
Expected: {key2=Dog(name=Dog BBB, breed=Bulldog), key3=Dog(name=Dog CCC, breed=breed), key4=Cat(name=Cat DDD, color=Grey), key6=Dog(name=Dog FFF, breed=Husky)}

I believe that this can be solved with a combination of operators, but due to my still limited knowledge of kotlin, I'm not sure how I can achieve that...

Can someone point me in some direction?

Upvotes: 1

Views: 2794

Answers (2)

You want rather specific diff. Will try to formalize it based on provided example:

  1. If key is present in the first map, but not in the second, it's discarded
  2. If key is present in the second map, but not in the first, it's retained
  3. If key is present in both maps and values are equal, it's discarded
  4. If key is present in both maps, but values are of different types, the value from second map takes precedence
  5. If key is present in both maps and values are of the same type, then resulting diff value should contain differing properties
    • Non-default value of property should take precedence
    • If both properties' values are non-default, then the the second takes precedence ((?) even if they both are equal)

The first 4 steps could be done like this:

secondMap.map { (key, secondValue) ->
    val firstValue = firstMap[key] ?: return@map key to secondValue
    if (firstValue == secondValue) return@map null //will filter out them later
    if (firstValue.javaClass != secondValue.javaClass) key to secondValue
    else key to animalDiff(firstValue, secondValue)
}.filterNotNull().toMap()

But the 5-th step (embedded in animalDiff function) is rather tricky. The problem is that there is no way to get default value of argument in Kotlin even via reflection (cause it may be not just a compile-time constant, but any arbitrary expression, including function call(s)).

You may do the following trick: add default values for all properties of all subclasses of Animal class, so that they could be constructed via no-arg reflection method; afterwards default values could be determined from this dummy instance.

data class Dog(
    val name: String = "",
    val breed: String = "breed"
) : Animal()

data class Cat(
    val name: String = "",
    val color: String = "black"
) : Animal()

private fun <T : Animal> animalDiff(first: T, second: T): T {
    val clazz: KClass<T> = first::class as KClass<T>
    val dummyInstance = clazz.createInstance()
    val constructor = clazz.primaryConstructor!!
    val constructorArgs = clazz.memberProperties.associate { prop: KProperty1<T, *> ->
        val defaultValue = prop.get(dummyInstance)
        val firstPropValue = prop.get(first)
        val secondPropValue = prop.get(second)
        val resultPropValue = when {
            // secondPropValue == firstPropValue -> defaultValue //uncomment, if it makes sense for your diff logic
            secondPropValue != defaultValue -> secondPropValue
            firstPropValue != defaultValue -> firstPropValue
            else -> defaultValue
        }
        //Need to convert KProperty into KParameter to pass it into constructor
        //Should work fine for data classes
        val constructorParam = constructor.parameters.find { it.name == prop.name }!!
        return@associate constructorParam to resultPropValue
    }

    return constructor.callBy(constructorArgs)
}

Upvotes: 0

m0skit0
m0skit0

Reputation: 25873

You can use existing minus() operator extension function:

secondMapOfAnimals.minus(firstMapOfAnimals)

Or more concisely:

secondMapOfAnimals - firstMapOfAnimals

Also note you can use to() infix extension function to create Pairs:

"key1" to Dog(name = "Dog aaa")

instead of

Pair("key1", Dog(name = "Dog aaa"))

Upvotes: 5

Related Questions