Kotlin - Merge two nested data class [duplicate]

Data class

data class A(
    var data: List<Data>
) {
    data class Data(
        var key: String,
        var count: Long = 0,
        var sub: List<Data>? = null
    )
}

A class data values expressed in json.

[
  {
    "data": [
      {
        "key": "ALLOGENE THERAPEUTICS",
        "count": 47,
        "sub": [
          {
            "key": "N",
            "count": 46,
            "sub": [
              {
                "key": "S1",
                "count": 1
              },
              {
                "key": "S2",
                "count": 13
              }
            ]
          },
          {
            "key": "B+",
            "count": 1,
            "sub": [
              {
                "key": "S1",
                "count": 2
              },
              {
                "key": "S2",
                "count": 1
              }
            ]
          }
        ]
      },
      {
        "key": "CELLECTIS",
        "count": 5,
        "sub": [
          {
            "key": "B+",
            "count": 2,
            "sub": [
              {
                "key": "S1",
                "count": 3
              },
              {
                "key": "S2",
                "count": 5
              }
            ]
          },
          {
            "key": "B",
            "count": 2,
            "sub": [
              {
                "key": "S1",
                "count": 6
              },
              {
                "key": "S2",
                "count": 1
              }
            ]
          },
          {
            "key": "N",
            "count": 1,
            "sub": [
              {
                "key": "S1",
                "count": 8
              },
              {
                "key": "S2",
                "count": 4
              }
            ]
          }
        ]
      },
      {
        "key": "PFIZER",
        "count": 5,
        "sub": [
          {
            "key": "N",
            "count": 5,
            "sub": [
              {
                "key": "S1",
                "count": 83
              },
              {
                "key": "S2",
                "count": 1
              }
            ]
          }
        ]
      }
    ]
  }
]

I would like to combine elements with key values of "ALLOGENE THERAPEUTICS" and "CELECTIS" and replace the key value with "STUB".

When the elements are combined, all the "count" values must be combined.

And elements that are not there must be added.

Therefore, the results should be as follows.

[
  {
    "data": [
      {
        "key": "STUB",
        "count": 52, // "ALLOGENE THERAPEUTICS"(47) + "CELECTIS"(5) = 52
        "sub": [
          {
            "key": "N", 
            "count": 47,  // 46 + 1
            "sub": [
              {
                "key": "S1",
                "count": 9
              },
              {
                "key": "S2",
                "count": 17
              }
            ]
          },
          {
            "key": "B+",
            "count": 3,
            "sub": [
              {
                "key": "S1",
                "count": 5
              },
              {
                "key": "S2",
                "count": 6
              }
            ]
          },
          {
            "key": "B",
            "count": 5,
            "sub": [
              {
                "key": "S1",
                "count": 11
              },
              {
                "key": "S2",
                "count": 7
              }
            ]
          }
        ]
      },
      {
        "key": "PFIZER",
        "count": 5,
        "sub": [
          {
            "key": "N",
            "count": 5,
            "sub": [
              {
                "key": "S1",
                "count": 83
              },
              {
                "key": "S2",
                "count": 1
              }
            ]
          }
        ]
      }
    ]
  }
]

How can I code the work neatly with Kotlin?

For reference, the values of the data class are expressed as json, and the result value must be data class.

This is the progress so far:

create a function for Data that creates a merged copy

data class Data(
    var key: String,
    var count: Long = 0,
    var sub: List<Data> = emptyList()
) {
    fun mergedWith(other: Data): Data {
        return copy(
            count = count + other.count,
            sub = sub + other.sub
        )
    }
}

fold the consolidation list into a single data item and add them back together.

val consolidatedKeys = listOf("ALLOGENE THERAPEUTICS", "CELECTIS")
val (consolidatedValues, nonconsolidatedValues) = a.data.partition { it.key in consolidatedKeys }
val consolidatedData = when {
    consolidatedValues.isEmpty() -> emptyList()
    else -> listOf(consolidatedValues.fold(A.Data("STUB", 0), A.Data::mergedWith))
}
val result = A(consolidatedData + nonconsolidatedValues)

And combine the sub-elements.

consolidatedData.forEach { x ->
            x.sub
                .groupBy { group -> group.key }
                .map { A.Data(it.key, it.value.sumOf { c -> c.count }) }
}

This is the current situation.

In this way, elements with depth of 2 will work normally, but elements with depth of 3 will not be added.

For example, up to "N" below STUB is combined, but "S1" and "S2" below "N" are not combined.

Therefore, the current result is output in this way.

[
  {
    "data": [
      {
        "key": "STUB",
        "count": 52, <--------- WORK FINE
        "sub": [
          {
            "key": "N", 
            "count": 47, <--------- WORK FINE
            "sub": [] <--------- EMPTY !!
          },
          {
            "key": "B+",
            "count": 3, <--------- WORK FINE
            "sub": [] <--------- EMPTY !!
          },
          {
            "key": "B",
            "count": 5, <--------- WORK FINE
            "sub": [] <--------- EMPTY !!
          }
        ]
      },
      {
        "key": "PFIZER",
        "count": 5,
        "sub": [
          {
            "key": "N",
            "count": 5,
            "sub": [
              {
                "key": "S1",
                "count": 83
              },
              {
                "key": "S2",
                "count": 1
              }
            ]
          }
        ]
      }
    ]
  }
]

How can all the sub-elements be combined and implemented?


Solution 1:

First break down your problem. You can create a function for Data that creates a merged copy:

fun mergedWith(other: Data): Data {
    return copy(
        count = count + other.count,
        sub = when {
            sub == null && other.sub == null -> null
            else -> sub.orEmpty() + other.sub.orEmpty()
        }
    )
}

I recommend if possible that you use a non-nullable List for your sub parameter, and use emptyList() when there's nothing in it. This makes it simpler since there aren't two different ways to represent a lack of items and you won't have to deal with nullability:

data class Data(
    var key: String,
    var count: Long = 0,
    var sub: List<Data> = emptyList()
) {
    fun mergedWith(other: Data): Data {
        return copy(
            count = count + other.count,
            sub = sub + other.sub
        )
    }
}

Then you can split your list into ones that you want to consolidate vs. the rest. Then fold the consolidation list into a single data item and add them back together.

val consolidatedKeys = listOf("ALLOGENE THERAPEUTICS", "CELECTIS")
val (consolidatedValues, nonconsolidatedValues) = a.data.partition { it.key in consolidatedKeys }
val consolidatedData = when {
    consolidatedValues.isEmpty() -> emptyList()
    else -> listOf(consolidatedValues.fold(A.Data("STUB", 0), A.Data::mergedWith))
}
val result = A(consolidatedData + nonconsolidatedValues)