grouping hierarchically in Kotlin - kotlin

I have two entities : Item, and ItemWrapper.
ItemWrapper includes Items.
ItemWrapper can have an ItemWrapper parent reference
For simplicity, Item is a string "a", "b", ...
ItemWrapper simply contains those items + other ItemWrappers.
They also have their own id.
So basically I have an input list like this :
id
title
parent_id
items
1
Wrapper #1
null
{a,b,c}
2
Wrapper #2
null
{x,y,z}
3
Wrapper #3
1
{w,u}
4
Wrapper #4
1
null
I need to write an efficient kotlin function that take this input and returns :
{
"Wrapper #1" : {
childrenWrappers: [
"Wrapper #3" : {
childrenWrappers: null,
items: w,u
},
"Wrapper #4" : {
childrenWrappers: null,
items: null
}
],
items: a,b,c
},
"Wrapper #2" : {
childrenWrappers: null,
items: x,y,z
}
}
The solution can use any data structures or classes,
It doesn't have to be in JSON representation (though I will encode it to json for the API transfer)

Assuming your data structure looks like the following:
data class ItemWrapper(
val id: Int,
val title: String,
val items: List<String>,
val parentId: Int? = null,
)
and your expected result data structure looks like this:
data class ItemWrapperNode(
val items: List<String>,
val childrenWrappers: Map<String, ItemWrapperNode>,
)
we can write conversion functions:
fun List<ItemWrapper>.toTree(): Map<String, ItemWrapperNode> =
groupBy { it.parentId }.childrenOf(null)
fun Map<Int?,List<ItemWrapper>>.childrenOf(parentId: Int?): Map<String, ItemWrapperNode> =
get(parentId)
?.map { it.title to ItemWrapperNode(it.items, childrenOf(it.id)) }
?.toMap() ?: emptyMap()
Then for a list itemWrappers of ItemWrappers
you can call itemWrappers.toTree() to obtain the desired output.
If you want to restrict the maximum depth of recursion, we can extend these functions in the following way by introducing an additional parameter maxDepth which is decremented on each recursion step:
fun List<ItemWrapper>.toTree(maxDepth: Int): Map<String, ItemWrapperNode> =
groupBy { it.parentId }.childrenOf(null, maxDepth)
fun Map<Int?,List<ItemWrapper>>.childrenOf(parentId: Int?, maxDepth: Int): Map<String, ItemWrapperNode> =
if(maxDepth <= 0) emptyMap()
else get(parentId)
?.map { it.title to ItemWrapperNode(it.items, childrenOf(it.id, maxDepth - 1)) }
?.toMap() ?: emptyMap()
Then, we can for instance write:
val wrappers = listOf(
ItemWrapper(1, "ItemWrapper #1", listOf("a")),
ItemWrapper(2, "ItemWrapper #2", listOf("b")),
ItemWrapper(3, "ItemWrapper #3", listOf("c"), 1),
ItemWrapper(4, "ItemWrapper #4", listOf("d"), 1),
)
println(wrappers.toTree(0))
This yields an empty map as result. Using a maxDepth of 1 only ItemWrappers #1 and #2 are returned. With maxDepth of 2 or higher we obtain all ItemWrappers.

Related

Kotlin generic object sort: how would I sort by an object list parameters count or size

I wrote a nifty generic function for a Kotlin js project that will sort a List of objects by parameter.
For instance, a list of book objects look like
data class Book(val id: Int, val title: String, val year: Int, val authors:List<Author>)
will be sorted by my generic function:
fun <T> sortColumnData(data: List<T>, direction: SORTDIRECTION, selector: KProperty1<T, Comparable<*>>) :List<T> {
val sortedData = when (direction) {
SORTDIRECTION.UP -> data.sortedWith(compareBy(selector))
SORTDIRECTION.DOWN -> data.sortedWith(compareByDescending(selector))
}
return sortedData
}
And, I can pass the selector in very conveniently : Book::title
I need some direction in how to write a sortColumnData function that will sort by author count.
No need to reinvent the wheel, see the Kotlin standard library Ordering|Kotlin
data class Author(val name: String)
data class Book(val id: Int, val title: String, val year: Int, val authors: List<Author>)
val authorA = Author("A")
val authorB = Author("B")
val list = listOf(
Book(1, "Book 1", 2000, listOf(authorA)),
Book(2, "Book 2", 2000, listOf(authorA)),
Book(3, "Book 3", 2000, listOf(authorA, authorB)),
Book(4, "Book 4", 2000, listOf(authorB))
)
list.sortedBy { it.title }.forEach(::println)
list.sortedByDescending { it.title }.forEach(::println)
list.sortedBy { it.authors.size }.forEach(::println)
list.sortedByDescending { it.authors.size }.forEach(::println)
You can also use Method Referencing:
val result = list.sortedBy(Book::title)
which is equivalent to:
val result = list.sortedBy { it.title }
For list.sortedBy { it.authors.size } it is not possible to use Method Reference.
––––––––––––––––––––
Edit:
You can add comparator functions to your data class, one for each comparison you want to do. You then add a second sortColumnData function with a KFunction2 argument instead of a KProperty1 one.
import kotlin.reflect.KFunction2
data class Author(val name: String)
data class Book(val id: Int, val title: String, val year: Int, val authors: List<Author>): Comparable<Book> {
override fun compareTo(other: Book) = compareValuesBy(this, other, { it.id }, { it.id })
fun compareByAuthorCount(other: Book) = compareValuesBy(this, other, { it.authors.size }, { it.authors.size })
fun compareByTitleLength(other: Book) = compareValuesBy(this, other, { it.title.length }, { it.title.length })
}
val authorA = Author("A")
val authorB = Author("B")
val list = listOf(
Book(1, "Book 1 - 123", 2000, listOf(authorA)),
Book(2, "Book 2 - 1", 2000, listOf(authorA)),
Book(3, "Book 3 - 12", 2000, listOf(authorA, authorB)),
Book(4, "Book 4 - ", 2000, listOf(authorB))
)
enum class SortDirection { UP, DOWN }
fun <T : Comparable<T>> sortColumnData(data: List<T>, direction: SortDirection, comparator: KFunction2<T, T, Int>): List<T> {
return when (direction) {
SortDirection.UP -> data.sortedWith(comparator)
SortDirection.DOWN -> data.sortedWith(comparator).reversed()
}
}
sortColumnData(list, SortDirection.DOWN, Book::compareTo).forEach(::println)
// Ids: 4, 3, 2, 1
sortColumnData(list, SortDirection.UP, Book::compareByAuthorCount).forEach(::println)
// Ids: 1, 2, 4, 3 (UP = from least to most authors)
sortColumnData(list, SortDirection.UP, Book::compareByTitleLength).forEach(::println)
// Ids: 4, 2, 3, 1 (UP = from shortest to longest title)

Kotlin: mutable map of mutable list won't update the list

(Kotlin newbie here) I have a text file with rows that look like these:
1-1-1
1-1-2
1-1-3
2-1-1
2-1-2
etc.
I have to transform these data to a map where the key is the first 2 elements and the value is a list of the third elements that that match the key. For example, the above records will transform into this JSON:
1-1: [1, 2, 3]
2-1: [1, 2]
etc.
I'm unable to increment the list. Here's a simplified version, I get stuck on the "else":
fun main () {
val l1 = mutableListOf("1-1-1", "1-1-2", "1-1-3", "2-1-1", "2-1-2")
val m = mutableMapOf<String, List<Int>>()
for (e in l1) {
val c = e.split("-")
val key = "${c[0]}-${c[1]}"
if (m[key] == null) m[key] = listOf(c[2].toInt())
else println("How do I append to the list?")
}
println(m)
}
Output:
{1-1=[1], 2-1=[1]}
But I want:
{1-1=[1, 2, 3], 2-1=[1, 2]}
Thank you (comments about idiomatic form are welcome!)
If we continue to follow your strategy, what you need is for the value type to be a MutableList. Then you can add to the existing MutableList when there's already an existing list for that key:
fun main() {
val l1 = mutableListOf("1-1-1", "1-1-2", "1-1-3", "2-1-1", "2-1-2")
val m = mutableMapOf<String, MutableList<Int>>()
for (e in l1) {
val c = e.split("-")
val key = "${c[0]}-${c[1]}"
if (m[key] == null) m[key] = mutableListOf(c[2].toInt())
else m[key]!!.add(c[2].toInt())
}
println(m)
}
This can be more natural using getOrPut(). It returns the existing MutableList or creates one and puts it in the map if it's missing. Then we don't have to deal with if/else, and can simply add the new item to the list.
fun main() {
val l1 = mutableListOf("1-1-1", "1-1-2", "1-1-3", "2-1-1", "2-1-2")
val m = mutableMapOf<String, MutableList<Int>>()
for (e in l1) {
val c = e.split("-")
val key = "${c[0]}-${c[1]}"
m.getOrPut(key, ::mutableListOf).add(c[2].toInt())
}
println(m)
}
But we can use the map and groupBy functions to create it more simply:
val m = l1.map { it.split("-") }
.groupBy(
{ "${it[0]}-${it[1]}" }, // keys
{ it[2].toInt() } // values
)
You can achieve your desired output with a single call to groupBy of the Kotlin standard library.
val input = listOf("1-1-1", "1-1-2", "1-1-3", "2-1-1", "2-1-2")
val result = input.groupBy(
{ it.substringBeforeLast("-") }, // extract key from item
{ it.substringAfterLast("-").toInt() } // extract value from item
)
The first lambda function extracts the key to group by of every list item. The second lambda function provides the value to use for each list item.
You can also do it by first mapping your values to Pairs and then group them as follows:
fun main(args: Array<String>) {
val input = listOf("1-1-1", "1-1-2", "1-1-3", "2-1-1", "2-1-2")
val result = input.map {
val values = it.split("-")
"${values[0]}-${values[1]}" to values[2]
}.groupBy ({ it.first }) { it.second }
println(result)
}

Having trouble with deserialising JSON nullable fields in Kotlin with custom decoder

I am having difficulty decoding this JSON with Kotlin Serialization. It works well when the data field is not empty. However when the data field is null and the errors field is present I get this runtime exception:
kotlinx.serialization.json.internal.JsonDecodingException: Unexpected JSON token at offset 7: Expected start of the object '{', but had ':' instead
JSON input: {"data":null,"errors":[{"path":null,"locations":[{"line":3,"column":5,"sourceName":null}],"message":"Validation error of type FieldUndefined: Field 'mee' in type 'Query' is undefined # 'mee'"}]})
The JSON pretty printed:
{
"data": null,
"errors": [{
"path": null,
"locations": [{
"line": 3,
"column": 5,
"sourceName": null
}],
"message": "Validation error of type FieldUndefined: Field 'mee' in type 'Query' is undefined # 'mee'"
}]
}
The code which is mostly stolen from How to serialize a generic class with kontlinx.serialization? :
class ApiResponse<T>(
#SerialName("data")
val data: T? = null,
#SerialName("errors")
val errors: List<ErrorResponse>? = null
)
#Serializable
class ErrorResponse(
val path: String? = null,
val locations: List<Location>? = null,
val errorType: String? = null,
val message: String? = null
)
#Serializable
data class Location(
val line: Int? = 0,
val column: Int? = 0,
val sourceName: String? = null
)
#ExperimentalSerializationApi
class ApiResponseSerializer<T>(private val dataSerializer: KSerializer<T>) : KSerializer<ApiResponse<T>> {
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("ApiResponseDataSerializer") {
val dataDescriptor = dataSerializer.descriptor
element("data", dataDescriptor)
element("errors", ErrorResponse.serializer().descriptor)
}
override fun deserialize(decoder: Decoder): ApiResponse<T> =
decoder.decodeStructure(descriptor) {
var data: T? = null
var errors: List<ErrorResponse>? = null
val listSerializer = ListSerializer(ErrorResponse.serializer())
loop# while (true) {
when (val i = decodeElementIndex(descriptor)) {
0 -> data = decodeSerializableElement(descriptor, i, dataSerializer, null)
1 -> errors = decodeSerializableElement(descriptor, i, ListSerializer(ErrorResponse.serializer()), null)
CompositeDecoder.DECODE_DONE -> break
else -> throw SerializationException("Unknown index $i")
}
}
ApiResponse(data, errors)
}
override fun serialize(encoder: Encoder, value: ApiResponse<T>) {
encoder.encodeStructure(descriptor) {
val listSerializer = ListSerializer(ErrorResponse.serializer())
encodeNullableSerializableElement(descriptor, 0, dataSerializer, value.data)
value.errors?.let {
encodeNullableSerializableElement(descriptor, 1, listSerializer, it)
}
}
}
}
I tried using decodeNullableSerializableElement, but I got a compilation error. I couldn't find a way to fix that.
Type mismatch: inferred type is KSerializer<T> but DeserializationStrategy<TypeVariable(T)?> was expected
Any help would be appreciated, I am very new to Android and Kotlin.
Always pays to come back after a good nights sleep. Not sure why I had so much trouble with decodeNullableSerializableElement yesterday, but today I played around and got it working.
Made 3 changes
Made T optional in class parameter
Added .nullable (could not get this to work yesterday) to both serialisers
Changed decodeSerializableElement to decodeNullableSerializableElement
Relevant changes below:
#ExperimentalSerializationApi
class ApiResponseSerializer<T>(private val dataSerializer: KSerializer<T?>) : KSerializer<ApiResponse<T>> {
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("ApiResponseDataSerializer") {
val dataDescriptor = dataSerializer.descriptor
element("data", dataDescriptor.nullable)
element("errors", ErrorResponse.serializer().descriptor.nullable)
}
override fun deserialize(decoder: Decoder): ApiResponse<T> =
decoder.decodeStructure(descriptor) {
var data: T? = null
var errors: List<ErrorResponse>? = null
val listSerializer = ListSerializer(ErrorResponse.serializer()).nullable
loop# while (true) {
when (val i = decodeElementIndex(descriptor)) {
0 -> data = decodeNullableSerializableElement(descriptor, i, dataSerializer, null)
1 -> errors = decodeNullableSerializableElement(descriptor, i, listSerializer, null)
CompositeDecoder.DECODE_DONE -> break
else -> throw SerializationException("Unknown index $i")
}
}
ApiResponse(data, errors)
}
override fun serialize(encoder: Encoder, value: ApiResponse<T>) {
encoder.encodeStructure(descriptor) {
val listSerializer = ListSerializer(ErrorResponse.serializer())
encodeNullableSerializableElement(descriptor, 0, dataSerializer, value.data)
value.errors?.let {
encodeNullableSerializableElement(descriptor, 1, listSerializer, it)
}
}
}
}

Kotlin - How to get a property from an object in collection

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.

Convert String referential datatype to real referential datatypes

I have the following dataclasses:
data class JsonNpc(
val name: String,
val neighbours: JsonPreferences
)
data class JsonPreferences(
val loves: List<String>,
val hates: List<String>
)
I have a list of these, and they reference each other through strings like:
[
JsonNpc(
"first",
JsonPreferences(
listOf("second"),
listOf()
)
),
JsonNpc(
"second",
JsonPreferences(
listOf(),
listOf("first")
)
)
]
note that a likes b does not mean b likes a
I also have the Dataclasses
data class Npc(
val name: String,
val neighbours: NeighbourPreferences,
)
data class NeighbourPreferences(
val loves: List<Npc>,
val hates: List<Npc>
)
And I want to convert the String reference types to the normal reference types.
What I have tried:
recursively creating the npcs (and excluding any that are already in the chain, as that would lead to infinite recursion):
Does not work, as the Npc can not be fully created and the List is immutable (I dont want it to be mutable)
I have managed to find a way to do this. It did not work with Npc as a data class, as I needed a real constructor
fun parseNpcs(map: Map<String, JsonNpc>): Map<String, Npc> {
val resultMap: MutableMap<String, Npc> = mutableMapOf()
for (value in map.values) {
if(resultMap.containsKey(value.name))
continue
Npc(value, map, resultMap)
}
return resultMap
}
class Npc(jsonNpc: JsonNpc, infoList: Map<String, JsonNpc>, resultMap: MutableMap<String, Npc>) {
val name: String
val neighbourPreferences: NeighbourPreferences
init {
this.name = jsonNpc.name
resultMap[name] = this
val lovesNpc = jsonNpc.neighbours.loves.map {
resultMap[it] ?: Npc(infoList[it] ?: error("Missing an Npc"), infoList, resultMap)
}
val hatesNpc = jsonNpc.neighbours.hates.map {
resultMap[it] ?: Npc(infoList[it] ?: error("Missing an Npc"), infoList, resultMap)
}
this.neighbourPreferences = NeighbourPreferences(
lovesNpc, hatesNpc
)
}
}
data class NeighbourPreferences(
val loves: List<Npc>,
val hates: List<Npc>
)
checking in the debugger, the people carry the same references for each Neighbour, so the Guide is always one Npc instance.