In Kotlin is there function or a way to also have an index when using the all extension?
With this kind of situation:
val givenKeys = arrayOf("SHIFT", "BUILDING", "FLOOR")
val givenVals = arrayOf("NIGHT", "ALPHA", "THIRD")
val successfulMatch = mapOf(
Pair("SHIFT", "NIGHT"), Pair("BUILDING", "ALPHA"), Pair("FLOOR", "THIRD")
)
val unsuccessfulMatch = mapOf(
Pair("SHIFT", "NIGHT"), Pair("BUILDING", "BETA"), Pair("FLOOR", "FIRST")
)
fun isMatch(candidate: Map<String, String>, keys: Array<String>, vals: Array<String>): Boolean {
var matches = true
keys.forEachIndexed { i, key ->
if(!candidate.containsKey(key) || vals[i] != candidate[key]) {
matches = false
}
}
return matches
}
isMatch(successfulMatch, givenKeys, givenVals) // returns true
isMatch(unsuccessfulMatch, givenKeys, givenVals) // returns false
I want to do something like this
fun isMatch(candidate: Map<String, String>, keys: Array<String>, vals: Array<String>): Boolean {
return keys.allIndex {i, key ->
candidate.containsKey(key) && vals.any {it == candidate[key]}
}
}
Is there any function like that?
You can use withIndex:
return keys.withIndex().all { (i, key) ->
//...
}
Note that it creates an Iterable<IndexedValue>, so you would typically use the dereference operator ( ) for the lambda parameter.
I have never worked before with Kotlin so I have a newbie question. I am working with an existing codebase, so I am wondering about a few things. I see that there is a function getDepartmentById which looks like this:
fun getDepartmentById(ctx: Ctx, params: JsonObject): Either<Failure, FlatResp> =
getOneByIdFlattened(ctx, params.right(), getDepartmentByIdSql(ctx), flattenOne = flattenerToType(MainAccessType.DEPARTMENT))
fun getDepartmentById(ctx: Ctx, id: Long): Either<Failure, FlatResp> =
getDepartmentById(ctx, jsonObject("id" to id))
Calling that function returns either Failure or FlatResp. From what I can see in the code FlatResponse is typealias for Map<MainAccessType, Entities>.
The function getOneByIdFlattened looks like this:
fun getOneByIdFlattened(ctx: Ctx,
params: Either<Long, JsonObject>,
statement: String,
rowConverter: (Row) -> Map<String, Any?> = ::mapFromDbNames,
grouper: (List<Map<String, Any?>>) -> List<Map<String, Any?>> = ::identity,
flattenOne: (List<Map<String, Any?>>) -> FlatResp
): Either<Failure, FlatResp> =
either.eager {
val id = when (params) {
is Either.Left -> Either.Right(params.value)
is Either.Right -> params.value.idL?.right()
?: Failure.JsonError(SErr(GlowErrs.MISSING_ID, "You must provide id")).left()
}.bind()
val dbDataList: List<Map<String, Any?>> = doQuery(ctx, statement, mapOf(
"courierIds" to ctx.user.courierIds,
"id" to id,
"count" to 1,
"offset" to 0,
"departmentIds" to ctx.userDepartments,
"customerIds" to ctx.user.customerIds
),
rowConverter, false
).bind()
val result = flattenOne(grouper(dbDataList))
addUpdatedAtEpoch(result)
}
I wonder how can I get from a FlatResp a property of an object, that looks like this:
So, for example if I want to get just name from this object what would be the best way to do this?
Also, I wonder why is this function returning a collection, and not just a single object when it should get a single row by id from DB?
This is the sql function:
private fun getDepartmentByIdSql(ctx: Ctx) =
"""select ${createSelectFields(departmentKeys)}
from department dept
where dept.id = :id
${
when (ctx.user.role) {
UserRoles.ADMIN -> ""
else -> "and dept.id = any (:departmentIds) "
}
}"""
So there are a lot of things wrong in the snippet provided. Given Snippets:
1
fun getDepartmentById(ctx: Ctx, params: JsonObject): Either<Failure, FlatResp> =
getOneByIdFlattened(ctx, params.right(), getDepartmentByIdSql(ctx), flattenOne = flattenerToType(MainAccessType.DEPARTMENT))
2
fun getDepartmentById(ctx: Ctx, id: Long): Either<Failure, FlatResp> =
getDepartmentById(ctx, jsonObject("id" to id))
3
private fun getDepartmentByIdSql(ctx: Ctx) =
"""select ${createSelectFields(departmentKeys)}
from department dept
where dept.id = :id
${
when (ctx.user.role) {
UserRoles.ADMIN -> ""
else -> "and dept.id = any (:departmentIds) "
}
}"""
4
fun getOneByIdFlattened(ctx: Ctx,
params: Either<Long, JsonObject>,
statement: String,
rowConverter: (Row) -> Map<String, Any?> = ::mapFromDbNames,
grouper: (List<Map<String, Any?>>) -> List<Map<String, Any?>> = ::identity,
flattenOne: (List<Map<String, Any?>>) -> FlatResp
): Either<Failure, FlatResp> =
either.eager {
val id = when (params) {
is Either.Left -> Either.Right(params.value)
is Either.Right -> params.value.idL?.right()
?: Failure.JsonError(SErr(GlowErrs.MISSING_ID, "You must provide id")).left()
}.bind()
val dbDataList: List<Map<String, Any?>> = doQuery(ctx, statement, mapOf(
"courierIds" to ctx.user.courierIds,
"id" to id,
"count" to 1,
"offset" to 0,
"departmentIds" to ctx.userDepartments,
"customerIds" to ctx.user.customerIds
),
rowConverter, false
).bind()
val result = flattenOne(grouper(dbDataList))
addUpdatedAtEpoch(result)
}
Issues:
in snippet 2, jsonObject should be JsonObject(..)
I have no idea what the following lines do :
//snippet1:
flattenOne = flattenerToType(MainAccessType.DEPARTMENT))
//snipper 4
either.eager {...block...}
addUpdatedAtEpoch(result)
doQuery(ctx, statement, mapOf(..)
//snippet3
UserRoles.ADMIN -> ""
"""select ${createSelectFields(departmentKeys)}
They are all probably some extension functions or util files made by your company or from some famous libraries like anko orsplitties . plus these are mixes with function calls of your own class, like createSelectFields or ctx.user.courierIds. also if i have to guess, then this seems like an unusual way of performing some operation on an sql dB
based on just code completion by android studio, i have been able to figure out the classes as following:
class Entities
typealias FlatResp = Map<MainAccessType, Entities>
class Ctx
sealed class Either<A,B>(val a:A?, val b:B?){
val value:A? = null
class Left<A>(val aa:A):Either<A,A>(aa,aa)
class Right<B>(val bv:B):Either<B,B>(bb,bb)
}
class Failure
class Row
class JsonObject(val pair:Pair<String,Long>):JSONObject(){
fun right():Either<Long,JsonObject>{
}
}
class jsonObject()
enum class MainAccessType{DEPARTMENT}
fun getDepartmentById(ctx: Ctx, params: JsonObject): Either<Failure, FlatResp> {
return getOneByIdFlattened(
ctx,
params.right(),
getDepartmentByIdSql(ctx),
flattenOne = flattenerToType(MainAccessType.DEPARTMENT))
}
fun getDepartmentById(ctx: Ctx, id: Long): Either<Failure, FlatResp> {
return getDepartmentById(ctx, JsonObject("id" to id))
}
fun getOneByIdFlattened(ctx: Ctx,
params: Either<Long, JsonObject>,
statement: String,
rowConverter: (Row) -> Map<String, Any?> = ::mapFromDbNames,
grouper: (List<Map<String, Any?>>) -> List<Map<String, Any?>> = ::identity,
flattenOne: (List<Map<String, Any?>>) -> FlatResp
): Either<Failure, FlatResp> {
return either.eager {
val id = when (params) {
is Either.Left -> Either.Right(params.value)
is Either.Right -> params.value.idL?.right()
?: Failure.JsonError(SErr(GlowErrs.MISSING_ID, "You must provide id")).left()
}.bind()
val dbDataList: List<Map<String, Any?>> = doQuery(ctx, statement, mapOf(
"courierIds" to ctx.user.courierIds,
"id" to id,
"count" to 1,
"offset" to 0,
"departmentIds" to ctx.userDepartments,
"customerIds" to ctx.user.customerIds
),
rowConverter, false
).bind()
val result = flattenOne(grouper(dbDataList))
addUpdatedAtEpoch(result)
}
}
private fun getDepartmentByIdSql(ctx: Ctx) =
"""select ${createSelectFields(departmentKeys)}
from department dept
where dept.id = :id
${
when (ctx.user.role) {
UserRoles.ADMIN -> ""
else -> "and dept.id = any (:departmentIds) "
}
}"""
fun mapFromDbNames(row:Row): Map<String,Any?>{
}
fun identity(param : List<Map<String, Any?>>): List<Map<String, Any?>>{
}
This is still not correct and has a lots of red lines in it. but what you can do is keep this as a starter in a separate file, compare and fix the code accordingly and then maybe we can tell what would be a better way:
replace inline functions (fun xyz(...) = someValue ) to block functions. (alt+enter in windows, cmd+n in mac)
instead of typeAlias, use map directly
::something means a function is passed as parameter . its similar to how we pass runnables in java 8, but even more shorthand. you can do ctrl+click( for mac its cmd+click) on that function and goto that function to check what its params are, what its return type are. do the same for various classes/ extension fucntions, variables too. this will help the most
instead of passing something into something which is being passed into another thing (like val bot = Robot(Petrol("5Litres") ) ) , split into different lines to make it understandable ( val amount = "5litres"; val equipment = Petrol(amount) ; val bot = Robot(equipment) )
try to not use 3rd party library/ replace with your own understandable code.
repeat steps 1-5
Hope this gives someplace to start. kotlin is a beautiful language but is also very easy to make unreadable.
Mapping Map values
I wonder how can I get from a FlatResp a property of an object, that looks like this:
So, for example if I want to get just name from this object what would be the best way to do this?
TL;DR
Without data to work with, here's my guess:
val extractedNames: Map<Long, String?> = destinationDepartment
.mapValues { (_, userData: Map<String, Any?>) ->
when (val name = userData["name"]) {
is String -> name
else -> null
}
}
println(extractedNames)
// {1=Bergen, 2=Cindy, 3=Dave}
Intro
Kotlin is great for manipulating collections. For a more general of how to work with collections in Kotlin, I think the docs are really clear Collection transformation operations#Map.
Let's see how that works for this example. You want to extract a specific element, so for that we can use map().
From your screenshot it looks like this is a Map<Long, Map>, where the value is a Map<String, Any?>. I'll assume you want to change the Map<Long, Map> to a Map<Long, String>, where the key is the database ID and the value is user's name.
Test data
So I've got something to test with, I made a new Map:
val destinationDepartment: Map<Long, Map<String, Any?>> =
mapOf(
1L to mapOf(
"id" to 1,
"name" to "Bergen",
"createdAt" to LocalDateTime.now(),
"updatedAt" to LocalDateTime.now(),
),
2L to mapOf(
"id" to 2,
"name" to "Cindy",
"createdAt" to LocalDateTime.now(),
"updatedAt" to LocalDateTime.now(),
),
3L to mapOf(
"id" to 3,
"name" to "Dave",
"createdAt" to LocalDateTime.now(),
"updatedAt" to LocalDateTime.now(),
),
)
Basic noop
First, set up the basics. A Map can be converted to a list of Entries. When we call map(), it will iterates over each Entry, and applies a lambda - which is something we must write. In this instance, the lambda receives the key and value of the Map, and must return a new value.
Aside: the Java equivalent is map.entrySet().stream().map(...)...
Here, the lambda just returns a pair (created with to).
val extractedNames = destinationDepartment
.map { (id: Long, userData: Map<String, Any?>) ->
id to userData
}
println(extractedNames)
// Output: [(1, {id=1, name=Bergen, createdAt=2021-08-19T11:00:07.447660, updatedAt=2021-08-19T11:00:07.449969}),
// (2, {id=2, name=Cindy, createdAt=2021-08-19T11:00:07.463813, updatedAt=2021-08-19T11:00:07.463845}),
// (3, {id=3, name=Dave, createdAt=2021-08-19T11:00:07.463875, updatedAt=2021-08-19T11:00:07.463890})]
Pretty boring! But now we're set up for the next step - extracting name from userData: Map<String, Any?>.
Extracting name
val extractedNames = destinationDepartment
.map { (id: Long, userData: Map<String, Any?>) ->
val name = userData["name"]
id to name
}
println(extractedNames)
// Output: [(1, Bergen), (2, Cindy), (3, Dave)]
Now there's loads of ways to improve this. Making sure that name is a String, not Any?, filtering out blank or null names, mapping to DTOs, sorting. Again, the Kotlin documentation would be a good start. I'll start by listing one really good improvement.
Converting List<Pair<>> to Map<>
If you look at the type of val extractedNames, you'll see that it's a list, not a map.
val extractedNames: List<Pair<Long, Any?>> = ...
That's because the lambda we wrote in the map() function is returning a Pair<Long, String>. Kotlin doesn't know that this is still considered a Map. We can convert any List<Pair<>> back to a map with toMap()
toMap()
val extractedNames: Map<Long, Any?> = destinationDepartment
.map { (id: Long, userData: Map<String, Any?>) ->
val name = userData["name"]
id to name
}
.toMap() // convert List<Pair<>> to a Map<>
println(extractedNames)
// Output: {1=Bergen, 2=Cindy, 3=Dave}
But this is also not great. Why is id: Long in the lambda if we're not using it? Because we're only extracting the name from userData, we're only mapping the values of the Map. We don't need id: Long at all. Fortunately Kotlin has another useful method: mapValues() - and it returns a Map<>, so we can drop the toMap(). Let's use it.
mapValues()
val extractedNames: Map<Long, Any?> = destinationDepartment
.mapValues { (id: Long, userData: Map<String, Any?>) ->
val name = userData["name"]
id to name
}
println(extractedNames)
// {1=(1, Bergen), 2=(2, Cindy), 3=(3, Dave)}
Umm weird. Why are the ids in the values? That's because the mapValues() lambda should return the new value, and in our lambda we're returning both the id and name - oops! Let's only return the name.
Fixing mapValues()
val extractedNames: Map<Long, Any?> = destinationDepartment
.mapValues { (_, userData: Map<String, Any?>) ->
userData["name"]
}
println(extractedNames)
// {1=(1, Bergen), 2=(2, Cindy), 3=(3, Dave)}
Better! Because id is not used, an underscore can be used instead
Aside: Note that the lambda doesn't have a return. Read Returning a value from a lambda expression for an explanation.
I am playing with Kotlin and I am trying to convert a working Scala code to Kotlin. Everything seems to go pretty well but the compiler gives me this error and I dont know how to handle it.
Type mismatch: inferred type is Any but ExQuestion was expected for this line: return makeMap(questions, add2)
I am using a generic function because I need to access members of type A when building the map and the members would be visible through the lambda function provided.
Here's the code which you can copy into the Kotlin sandbox:
data class ExQuestion(val area: String, val rId: String, val text: String, val rIdAnswer: String, val line: Long)
fun main() {
fun <A> makeMap(list: List<A>, addValue: (A, MutableMap<String, A>) -> Unit): Map<String, A> {
val map = mutableMapOf<String, A>()
for( item in list) {
addValue(item, map)
}
return map
}
val add2: (ExQuestion, MutableMap<String, ExQuestion>) -> Unit =
{ question: ExQuestion, map: MutableMap<String, ExQuestion> ->
val key = question.rId
if (map[key] == null) {
map[key] = question
} else {
println("Id Frage mehrfach vorhanden - " + key)
}
}
val questions = listOf(ExQuestion("Area", "Q01", "text", "A01",1))
return makeMap(questions, add2)
}
Working code:
data class ExQuestion(val area: String, val rId: String, val text: String, val rIdAnswer: String, val line: Long)
fun main() {
fun <A> makeMap(list: List<A>, addValue: (A, MutableMap<String, A>) -> Unit): Map<String, A> {
val map = mutableMapOf<String, A>()
for( item in list) {
addValue(item, map)
}
return map
}
val add2: (ExQuestion, MutableMap<String, ExQuestion>) -> Unit =
{ question: ExQuestion, map: MutableMap<String, ExQuestion> ->
val key = question.rId
if (map[key] == null) {
map[key] = question
} else {
println("Id Frage mehrfach vorhanden - " + key)
}
}
val questions = listOf(ExQuestion("Area", "Q01", "text", "A01",1))
val map = makeMap(questions, add2)
println(map.values)
}
I'm not sure what your question is, but you can convert your list of questions to a map keyed on rId by doing:
val map = questions.map { it.rId to it }.toMap()
println(map)
Result:
{Q01=ExQuestion(area=Area, rId=Q01, text=text, rIdAnswer=A01, line=1)}
Update in response to comments.
You can achieve that without a mutable map by doing something like this:
val map = questions
.groupBy { it.rId }
.mapValues { (key, values) ->
if (values.size > 1) println("Id Frage mehrfach vorhanden - $key")
values.first()
}
However, I think your mutable map solution is fine and arguably clearer, so this is just for demonstration.
Is there a way to merge kotlin data classes without specifying all the properties?
data class MyDataClass(val prop1: String, val prop2: Int, ...//many props)
with a function with the following signature:
fun merge(left: MyDataClass, right: MyDataClass): MyDataClass
where this function checks each property on both classes and where they are different uses the left parameter to create a new MyDataClass.
Is this possible possible using kotlin-reflect, or some other means?
EDIT: more clarity
Here is a better description of what i want to be able to do
data class Bob(
val name: String?,
val age: Int?,
val remoteId: String?,
val id: String)
#Test
fun bob(){
val original = Bob(id = "local_id", name = null, age = null, remoteId = null)
val withName = original.copy(name = "Ben")
val withAge = original.copy(age = 1)
val withRemoteId = original.copy(remoteId = "remote_id")
//TODO: merge without accessing all properties
// val result =
assertThat(result).isEqualTo(Bob(id = "local_id", name = "Ben", age=1, remoteId = "remote_id"))
}
If you want to copy values from the right when values in the left are null then you can do the following:
inline infix fun <reified T : Any> T.merge(other: T): T {
val propertiesByName = T::class.declaredMemberProperties.associateBy { it.name }
val primaryConstructor = T::class.primaryConstructor
?: throw IllegalArgumentException("merge type must have a primary constructor")
val args = primaryConstructor.parameters.associateWith { parameter ->
val property = propertiesByName[parameter.name]
?: throw IllegalStateException("no declared member property found with name '${parameter.name}'")
(property.get(this) ?: property.get(other))
}
return primaryConstructor.callBy(args)
}
Usage:
data class MyDataClass(val prop1: String?, val prop2: Int?)
val a = MyDataClass(null, 1)
val b = MyDataClass("b", 2)
val c = a merge b // MyDataClass(prop1=b, prop2=1)
A class-specific way to combine data classes when we can define the fields we want to combine would be:
data class SomeData(val dataA: Int?, val dataB: String?, val dataC: Boolean?) {
fun combine(newData: SomeData): SomeData {
//Let values of new data replace corresponding values of this instance, otherwise fall back on the current values.
return this.copy(dataA = newData.dataA ?: dataA,
dataB = newData.dataB ?: dataB,
dataC = newData.dataC ?: dataC)
}
}
#mfulton26's solution merges properties that are part of primary constructor only. I have extended that to support all properties
inline infix fun <reified T : Any> T.merge(other: T): T {
val nameToProperty = T::class.declaredMemberProperties.associateBy { it.name }
val primaryConstructor = T::class.primaryConstructor!!
val args = primaryConstructor.parameters.associate { parameter ->
val property = nameToProperty[parameter.name]!!
parameter to (property.get(other) ?: property.get(this))
}
val mergedObject = primaryConstructor.callBy(args)
nameToProperty.values.forEach { it ->
run {
val property = it as KMutableProperty<*>
val value = property.javaGetter!!.invoke(other) ?: property.javaGetter!!.invoke(this)
property.javaSetter!!.invoke(mergedObject, value)
}
}
return mergedObject
}
Your requirements are exactly the same as copying the left value:
fun merge(left: MyDataClass, right: MyDataClass) = left.copy()
Perhaps one of use isn't properly understanding the other. Please elaborate if this isn't what you want.
Note that since right isn't used, you could make it a vararg and "merge" as many as you like :)
fun merge(left: MyDataClass, vararg right: MyDataClass) = left.copy()
val totallyNewData = merge(data1, data2, data3, data4, ...)
EDIT
Classes in Kotlin don't keep track of their deltas. Think of what you get as you're going through this process. After the first change you have
current = Bob("Ben", null, null, "local_id")
next = Bob(null, 1, null, "local_id")
How is it supposed to know that you want next to apply the change to age but not name? If you're just updating based on nullability,
#mfulton has a good answer. Otherwise you need to provide the information yourself.
infix fun <T : Any> T.merge(mapping: KProperty1<T, *>.() -> Any?): T {
//data class always has primary constructor ---v
val constructor = this::class.primaryConstructor!!
//calculate the property order
val order = constructor.parameters.mapIndexed { index, it -> it.name to index }
.associate { it };
// merge properties
#Suppress("UNCHECKED_CAST")
val merged = (this::class as KClass<T>).declaredMemberProperties
.sortedWith(compareBy{ order[it.name]})
.map { it.mapping() }
.toTypedArray()
return constructor.call(*merged);
}
Edit
infix fun <T : Any> T.merge(right: T): T {
val left = this;
return left merge mapping# {
// v--- implement your own merge strategy
return#mapping this.get(left) ?: this.get(right);
};
}
Example
val original = Bob(id = "local_id", name = null, age = null, remoteId = null)
val withName = original.copy(name = "Ben")
val withAge = original.copy(age = 1)
val withRemoteId = original.copy(remoteId = "remote_id")
val result = withName merge withAge merge withRemoteId;