How to combine fields of objects by a certain attribute? - kotlin

I need to filter these Person objects by name, then count the number of Devices for each unique name.
data class Device(val name: String, val price: Int)
data class Person(val name: String, val age: Int, val devices: List<Device>)
data class PersonFinal(val name: String, val age: Int, val count: Int)
val person1 = Person("Jack", 20, listOf(Device("Samsung", 1500), Device("iPhone", 2500)))
val person2 = Person("Jack", 20, listOf(Device("Samsung", 3500), Device("iPhone", 5500)))
val person3 = Person("John", 20, emptyList())
The final result should be:
// [PersonFinal(name=Jack, age=20, count=4), PersonFinal(name=John, age=20, count=0)]
How can I do this using Kotlin?

If someone needed, my final solution looks like:
listOf(person1, person2, person3)
.groupBy { it.name }
.map { (_, list) ->
PersonFinal(list.first().name, list.first().age, list.sumBy { it.devices.count() })
}
Prints [PersonFinal(name=Jack, age=20, count=4), PersonFinal(name=John, age=20, count=0)]

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 data class constructors not getting picked up

I am creating a data class in kotlin as such
data class User(val name: String, val age: Int)
{
constructor(name: String, age: Int, size: String): this(name, age) {
}
}
In my main function, I can access the objects as such:
fun main(){
val x = User("foo", 5, "M")
println(x.name)
println(x.age)
println(x.size) // does not work
}
My problem is that I can't get access to size.
What I am trying to do is, create a data class where top level params are the common items that will be accessed, and in the constructors, have additional params that fit certain situations. The purpose is so that I can do something like
// something along the lines of
if (!haveSize()){
val person = User("foo", 5, "M")
} else {
val person = User("foo", 5)
}
}
Any ideas?
In Kotlin you do not need separate constructors for defining optional constructor params. You can define them all in a single constructor with default values or make them nullable, like this:
data class User(val name: String, val age: Int, val size: String = "M")
fun main(){
val x = User("foo", 5, "L")
val y = User("foo", 5)
println(x.size) // "L" from call site
println(y.size) // "M" from default param
}
You can not access size variable, because this is from secondary construct, but we have alternative variant.
data class User(var name: String, var age: Int) {
var size: String
init {
size = "size"
}
constructor(name: String, age: Int, size: String) : this(name, age) {
this.size = size
}
}
In short, you want to have one property that can be one of a limited number of options. This could be solved using generics, or sealed inheritance.
Generics
Here I've added an interface, MountDetails, with a generic parameter, T. There's a single property, val c, which is of type T.
data class User(
val mountOptions: MountOptions,
val mountDetails: MountDetails<*>,
)
data class MountOptions(
val a: String,
val b: String
)
interface MountDetails<T : Any> {
val c: T
}
data class MountOneDetails(override val c: Int) : MountDetails<Int>
data class MountTwoDetails(override val c: String) : MountDetails<String>
Because the implementations MountDetails (MountOneDetails and MountTwoDetails) specify the type of T to be Int or String, val c can always be accessed.
fun anotherCaller(user: User) {
println(user.mountOptions.a)
println(user.mountOptions.b)
println(user.mountDetails)
}
fun main() {
val mt = MountOptions("foo", "bar")
val mountOneDetails = MountOneDetails(111)
anotherCaller(User(mt, mountOneDetails))
val mountTwoDetails = MountTwoDetails("mount two")
anotherCaller(User(mt, mountTwoDetails))
}
Output:
foo
bar
MountOneDetails(c=111)
foo
bar
MountTwoDetails(c=mount two)
Generics have downsides though. If there are lots of generic parameters it's messy, and it can be difficult at runtime to determine the type of classes thanks to type-erasure.
Sealed inheritance
Since you only have a limited number of mount details, a much neater solution is sealed classes and interfaces.
data class User(val mountOptions: MountOptions)
sealed interface MountOptions {
val a: String
val b: String
}
data class MountOneOptions(
override val a: String,
override val b: String,
val integerData: Int,
) : MountOptions
data class MountTwoOptions(
override val a: String,
override val b: String,
val stringData: String,
) : MountOptions
The benefit here is that there's fewer classes, and the typings are more specific. It's also easy to add or remove an additional mount details, and any exhaustive when statements will cause a compiler error.
fun anotherCaller(user: User) {
println(user.mountOptions.a)
println(user.mountOptions.b)
// use an exhaustive when to determine the actual type
when (user.mountOptions) {
is MountOneOptions -> println(user.mountOptions.integerData)
is MountTwoOptions -> println(user.mountOptions.stringData)
// no need for an 'else' branch
}
}
fun main() {
val mountOne = MountOneOptions("foo", "bar", 111)
anotherCaller(User(mountOne))
val mountTwo = MountTwoOptions("foo", "bar", "mount two")
anotherCaller(User(mountTwo))
}
Output:
foo
bar
111
foo
bar
mount two
This is really the "default values" answer provided by Hubert Grzeskowiak adjusted to your example:
data class OneDetails(val c: Int)
data class TwoDetails(val c: String)
data class MountOptions(val a: String, val b: String)
data class User(
val mountOptions: MountOptions,
val detailsOne: OneDetails? = null,
val detailsTwo: TwoDetails? = null
)
fun main() {
fun anotherCaller(user: User) = println(user)
val mt = MountOptions("foo", "bar")
val one = OneDetails(1)
val two = TwoDetails("2")
val switch = "0"
when (switch) {
"0" -> anotherCaller(User(mt))
"1" -> anotherCaller(User(mt, detailsOne = one))
"2" -> anotherCaller(User(mt, detailsTwo = two))
"12" -> anotherCaller(User(mt, detailsOne = one, detailsTwo = two))
else -> throw IllegalArgumentException(switch)
}
}

In Kotlin, are there graceful ways of passing values from one instance to another, which have the same name?

I have the following classes
class A(
val value1: String,
val value2: String,
val value3: String,
val value4: String,
val value5: String,
)
class B(
val value1: String,
val value2: String,
val value3: String,
val value4: String,
val value5: String,
) {
compaion object {
from(a: A) = B(
value1 = a.value1,
value2 = a.value2,
value3 = a.value3,
value4 = a.value4,
value5 = a.value5,
)
}
}
I write codes as follows when I want to create an instance of B from A
val a: A = getAFromSomewhere()
val b: B = B.from(a)
I have a lot of codes as above and It's very boring for me to write the factory method, 'from'. Is there any easy way of writing this kind of codes in Kotlin??
You might be interested in the MapStruct library.
https://mapstruct.org/
It helps map between two objects(DTO, Entity, etc..).
Code
In this example we want to map between a Person (Model) and a PersonDto (DTO).
data class Person(var firstName: String?, var lastName: String?, var phoneNumber: String?, var birthdate: LocalDate?)
data class PersonDto(var firstName: String?, var lastName: String?, var phone: String?, var birthdate: LocalDate?)
The MapStruct converter:
#Mapper
interface PersonConverter {
#Mapping(source = "phoneNumber", target = "phone")
fun convertToDto(person: Person) : PersonDto
#InheritInverseConfiguration
fun convertToModel(personDto: PersonDto) : Person
}
Usage:
val converter = Mappers.getMapper(PersonConverter::class.java) // or PersonConverterImpl()
val person = Person("Samuel", "Jackson", "0123 334466", LocalDate.of(1948, 12, 21))
val personDto = converter.convertToDto(person)
println(personDto)
val personModel = converter.convertToModel(personDto)
println(personModel)
From: https://github.com/mapstruct/mapstruct-examples/tree/master/mapstruct-kotlin

I want to merge two lists of Mcqs and True false type questions in List of quiz type

The data class of Mcqs look like this:
data class Mcqss(
var answer: String,
val mcqs: String,
val option1: String,
val option2: String,
val option3: String,
val option4: String,
var topicId: String,
var sequence: String,
)
True false data class:
data class tf(
val answer: String,
val question: String,
val topicId: String,
val sequence: String,
)
Quiz data class:
data class quiz(
var topicId: String,
var sequence: String,
var mcq_question:String,
var trf_question:String
)
Function to combine two lists:
fun <T, U> combine(first: ArrayList<Mcqss>, second: ArrayList<tf>): MutableList<Any> {
val list: MutableList<Any> = first.map { i -> i }.toMutableList()
list.addAll(second.map { i -> i })
return list
}
But when I execute this line it gives me a class cast exception:
val joined: ArrayList<quiz> = combine<Any,Any>(mcqlist, tfs) as ArrayList<quiz>
for (item in joined) {
item.sequence
}
Any suggestions please.
Please try next code:
fun combine(first: ArrayList<Mcqss>, second: ArrayList<Tf>): ArrayList<Quiz> {
// I assume the sizes of `first` and `second` lists are the same
require(first.size == second.size)
val result = mutableListOf<Quiz>()
for (i in 0 until first.size) {
val quiz = Quiz(first[i].topicId, first[i].sequence, ...)
result.add(quiz)
}
return ArrayList(result)
}
val joined: ArrayList<Quiz> = combine(mcqlist, tfs)
I would recommend to name classes starting with a capital letter, e.g. quiz->Quiz.
val mcqlist: List<Mcqss> = ...
val tfs: List<TrueFalse> = ...
val joined = mcqlist + tfs
for (item in joined) {
if (item is Mcqss) {
println(item.option1)
} else if (item is TrueFalse) {
println(item.question)
}
}

Is there a way to generate kotlin dsl using data

We are using kotlin dsl to as a user friendly builder to take input and generate data. Is there a way to do the opposite of that ?
ie, convert existing data into dsl ?
Can this kotlin representation be converted to dsl ?
val person = Person("John", 25)
val person = person {
name = "John"
age = 25
}
Unless you're really crazy about { and some commas, below is an absolutely valid Kotlin code:
data class Person(
val name: String,
val age: Int
)
val person = Person(
name = "John",
age = 25
)
I seems really close to what you want and comes out-of-the-box.
Of course, you can achieve the syntax you want by writing some extra code, like:
import kotlin.properties.Delegates
data class Person(
val name: String,
val age: Int
)
class PersonDSL{
lateinit var name: String
var age: Int by Delegates.notNull<Int>()
fun toPerson(): Person = Person(this.name, this.age)
}
fun person(config: PersonDSL.() -> Unit): Person{
val dsl = PersonDSL()
dsl.config()
return dsl.toPerson()
}
fun main(){
val person = person {
name = "John"
age = 25
}
println(person) // Person(name=John, age=25)
}
But why do that?