I have multiple different data classes in kotlin and they are saved in extra lists with the exact same length. Now I want to combine them into a result list which contains them as a map with the same length (1000 entries). I tried it with a for loop and a prefilled list of maps, but that looks a bit messy to me, so I asked myself if there is a cleaner way of doing this, especially with kotlin.
data class One(
val a: Double,
val b: Double,
var c: Double
)
data class Two(
val d: Double,
val e: Double,
)
fun main() {
val oneList: List<One> = listOf(One(1.0, 0.0, 2.0), One(3.0, 5.0, 10.0))
val twoList: List<Two> = listOf(Two(5.0, 2.0), Two(7.0, 1.0))
val results: List<MutableMap<String,Double>> = (0..1).map{ mutableMapOf() }
for(i in 0 .. 1){
results[i]["a"] = oneList[i].a
results[i]["b"] = oneList[i].b
results[i]["c"] = oneList[i].c
results[i]["d"] = twoList[i].d
results[i]["e"] = twoList[i].e
}
}
The problem is, that I have about 10 classes with around 2-3 members in each object, whereby the code would be about 30 lines... The result should look like this:
[
{
a: 1.0,
b: 0.0,
c: 2.0,
d: 5.0,
e: 2.0
},
{
a: 3.0,
b: 5.0,
c: 10.0,
d: 7.0,
e: 1.0
}
]
CodePudding user response:
You could use .zip in order to link the 2 lists and then directly create the list using .map and a simple mapOf() to create the map.
val results = oneList.zip(twoList).map { (one, two) ->
mapOf("a" to one.a, "b" to one.b, "c" to one.c, "d" to two.d, "e" to two.e)
}
As for actually doing this for a lot of classes ... not 100% sure. You could
use reflexion maybe, but that is not something you use in an actual program usually. Another way would be to create a function in each of the classes that would give a map and then just add those maps together in the .map
from above. That way, the code would look a bit cleaner. Something along the lines of:
data class One(
val a: Double,
val b: Double,
var c: Double
){
fun toMap(): Map<String, Double> {
return mapOf("a" to a, "b" to b, "c" to c)
}
}
//.....
val results = oneList.zip(twoList).map { (one, two) -> one.toMap() two.toMap() }
But you'll soon notice that zip
doesn't work with more than 2 lists. I would suggest implementing something like this: Zip multiple lists SO. But that as well won't work, since your classes are of a different type. What I would do is create a abstract class with a toMap()
fun, and then all the classes that you need there can inherit it. It would look something like this:
abstract class MotherClass(){
abstract fun toMap(): Map<String, Double>
}
data class One(
val a: Double,
val b: Double,
var c: Double
):MotherClass() {
override fun toMap(): Map<String, Double> {
return mapOf("a" to a, "b" to b, "c" to c)
}
}
// ...
val results = zip(oneList, twoList /*, threeList, etc */).map { list -> list.map { it.toMap() } }
CodePudding user response:
So at the end of the day, you want a String
representation of a property name, mapped to its value? I think you have two choices there:
- use reflection to fetch all the member names, filter on the types you want (e.g. only the
Double
s), so yourString
s are derived directly from the class at runtime - define a
String
somewhere that acts as a label for each property, and try to tie it as closely to the class as you can, so it's easy to maintain. Because it's not derived from the property itself, you'll have to keep the property name and its label in sync - there's no inherent connection between the two things that could automate it
You're already doing the latter in your code - you're using hardcoded arbitrary labels like "a"
and "b"
when building your results
map, and that's where you're connecting a label with a property (e.g. results[i]["a"] = oneList[i].a
). So here's another way you could approach that!
interface Mappable {
// allows us to declare classes as having the property map we're using
abstract val propertyMap: Map<String, Double>
}
data class One(
val a: Double,
val b: Double,
var c: Double
) : Mappable {
// you have to do this association somewhere - may as well be in the class itself
// you could also use reflection to build this map automatically
override val propertyMap = mapOf("a" to a, "b" to b, "c" to c)
}
data class Two(
val d: Double,
val e: Double,
) : Mappable {
override val propertyMap = mapOf("d" to d, "e" to e)
}
fun main() {
val oneList = listOf(One(1.0, 0.0, 2.0), One(3.0, 5.0, 10.0))
val twoList = listOf(Two(5.0, 2.0), Two(7.0, 1.0))
combine(oneList, twoList).forEach(::println)
}
fun combine(vararg lists: List<Mappable>): List<Map<String, Double>> {
// lists could be different lengths, so we go until one of them runs out
val length = lists.minOf { it.size }
return (0 until length).map { index ->
// create a combined map for each index
mutableMapOf<String, Double>().apply {
// visit each list at this index, grabbing the property map from each object
// and adding its contents to the combined map we're building
lists.map { it.elementAt(index).propertyMap }.forEach(this::putAll)
}
}
}
>> {a=1.0, b=0.0, c=2.0, d=5.0, e=2.0}
{a=3.0, b=5.0, c=10.0, d=7.0, e=1.0}
The problem really is that artificial connection you're introducing between a property, and a label you explicitly define for it - which you have to ensure you maintain, and if you don't, you got bugs. I think that's unavoidable unless you use reflection.
Also if by any chance this data is originally coming from something arbitrary like JSON (where the problem of validation and labelling a property is earlier in the chain) you could take a look at Kotlin's map delegate and see if that's a better fit for this approach
CodePudding user response:
import kotlin.reflect.full.declaredMemberProperties
data class One(val a: Double, val b: Double, var c: Double)
data class Two(val d: Double, val e: Double)
val oneList: List<One> = listOf(One(1.0, 0.0, 2.0), One(3.0, 5.0, 10.0))
val twoList: List<Two> = listOf(Two(5.0, 2.0), Two(7.0, 1.0))
fun compounded(vararg lists: List<Any>): List<Map<String, Double>> {
return lists
.mapIndexed { index, _ ->
lists
.flatMap {
it[index]::class.declaredMemberProperties.map { prop ->
mapOf(prop.name to prop.call(it[index]) as Double)
}
}
.fold(mapOf()) { acc, map ->
mutableMapOf<String, Double>().apply { putAll(acc map) }
}
}
}
val result = compounded(oneList, twoList)