How can I remove duplicate objects with distinctBy from a list in Kotlin? - kotlin

How can I use distinctBy on a list of custom objects to strip out the duplicates? I want to determine "uniqueness" by multiple properties of the object, but not all of them.
I was hoping something like this would work, but no luck:
val uniqueObjects = myObjectList.distinctBy { it.myField, it.myOtherField }
Edit: I'm curious how to use distinctBy with any number of properties, not just two like in my example above.

You can create a pair:
myObjectList.distinctBy { Pair(it.myField, it.myOtherField) }
The distinctBy will use equality of Pair to determine uniqueness.

If you look at the implementation of the distinctBy, it just adds the value you pass in the lambda to a Set. And if the Set did not already contain the specified element, it adds the respective item of the original List to the new List and that new List is being returned as the result of distinctBy.
public inline fun <T, K> Iterable<T>.distinctBy(selector: (T) -> K): List<T> {
val set = HashSet<K>()
val list = ArrayList<T>()
for (e in this) {
val key = selector(e)
if (set.add(key))
list.add(e)
}
return list
}
So you can pass a composite object that holds the properties that you require to find the uniqueness.
data class Selector(val property1: String, val property2: String, ...)
And pass that Selector object inside the lambda:
myObjectList.distinctBy { Selector(it.property1, it.property2, ...) }

You can create a triple:
myObjectList.distinctBy { Triple(it.firstField, it.secondField, it.thirdField) }
The distinctBy will use equality of Triple to determine uniqueness.
*I have implemented in this way, it provides most Unique list 👍

Related

How to create a custom iterator in kotlin and add to existing class?

Hello I am trying to add a custom iterator for example to a Pair class from kotlin package to be able to use instance of that class in a for loop
Let's assume this is what I want to be able to do:
val pair: Pair<Int, String> = Pair(1, "sample data")
for (element in pair) {
println(element)
}
I know that there are plenty other ways to print elements from a pair but I specifically want to be able to use pair in a for loop and I need to add iterator() object with next() and hasNext() methods implementation to Pair class
You can do this by providing the iterator() operator for your object, either as a member function or extension function. Example using an extension function:
fun main() {
val pair: Pair<Int, String> = Pair(1, "sample data")
for (element in pair) {
println(element)
}
}
operator fun <T> Pair<T, T>.iterator(): Iterator<T> = listOf(first, second).iterator()
However, you need to be aware that this way you partially lose strongly typing. element can only be a common supertype of all elements, in most cases simply Any?.
You can read more about this in the official documentation: https://kotlinlang.org/docs/control-flow.html#for-loops

Kotlin - How to convert a list of objects into a single one after map operation?

I'm trying to wrap my head around map and reduce operations in Kotlin. At least, I guess it's reduce what I'm trying to do.
Let's say that I have a class called Car that takes any number (varargs constructor) of CarPart. Then, I have a list of CarPart which I'll do a map operation and from the result of the operation I need to build one Car using each subelement, something along these lines:
class CarPart(val description: String)
class Car(vararg val carPart: CarPart)
val carParts = listOf(CarPart("Engine"), CarPart("Steering Wheel")))
carParts.map { it.description.toUpperCase() }
.map { CarPart(it) }
.reduce { acc, carPart -> Car(carPart) } <--- I'm struggling here, is reduce what I should be doing
to construct one car from all the subelement?
PS.1: I know that the class design could be better and not take a varargs, this is just an example of a legacy application I'm refactoring and originally that's a Java class taking varargs which I can't change now.
PS.2: The example of mapping to a String and then creating an object out of that String is just for the sake of the example. The actual code grabs an object within the list.
You can simply use a the spread operator (*) over an array:
val mappedCarParts = carParts
.map { it.description.toUpperCase() }
.map { CarPart(it) }
.toTypedArray()
val car = Car(*mappedCarParts)
// Or even:
val car = carParts
.map { it.description.toUpperCase() }
.map { CarPart(it) }
.toTypedArray()
.let{ Car(*it) }
You could just extract the constructor of the Car outside of the creation of the list. I don't see any reason as to why you'd want it inside.
val car = Car(
*carParts
.map { CarPart(it.description.uppercase(Locale.getDefault())) } //keep the .toUpperCase() if you are using an old version of Kotlin
.toTypedArray()
)
We need the spread operator there in order for the vararg to know that we are passing it the elements of the list and not the list itself.

How to effectively map between Enum in Kotlin

I have two Enums,
enum class EnumKey
enum class EnumValue
and I already have a mapping from EnumKey to EnumValue.
fun EnumKey.toEnumValue(): EnumValue =
when(this) {
EnumA.KEY1 -> EnumValue.VALUE1
EnumA.KEY2 -> EnumValue.VALUE2
...
...
EnumA.KEY1000 -> EnumValue.VALUE1000
}
Now I need to have an another mapping from EnumValue to EnumKey.
Is using a Map and its reversed map created by associateBy the best way to do it? Or is there any other better ways?
Thanks!
If the enum values are somehow connected by name and they're as large as in your example, then I would advise using something like EnumValue.values().filter { it.name.contains(...) } or using regex.
If they aren't and the connection needs to be stated explicitly then I would use an object (so it's a singleton like the enums themselves) and have this mapping hidden there:
object EnumsMapping {
private val mapping = mapOf(
EnumKey.A to EnumValue.X,
EnumKey.B to EnumValue.Y,
EnumKey.C to EnumValue.Z,
)
....
and next, have the associated values available by functions in this object like:
fun getEnumValue(enumKey: EnumKey) = mapping[enumKey]
and
fun getEnumKey(enumValue: EnumValue) = mapping.filterValues { it == enumValue }.keys.single()
If it's often used or the enums are huge, and you're troubled by the performance of filtering the values every time, then you can create the association in the second way, just like you've proposed:
private val mapping2 = mapping.toList()
.associate { it.second to it.first }
and then have the second function just access this new mapping.
Writing the extension functions like you've provided, but using this object, will result in cleaner code and having the raw association still in one place.

How to get random 100 numbers in Kotlin [duplicate]

I'm new to Kotlin and lambdas and I'm trying to understand it. I'm trying to generate a list of 100 random numbers.
This works:
private val maxRandomValues = (1..100).toList()
But I want to do something like that:
private val maxRandomValues = (1..100).forEach { RandomGenerator().nextDouble() }.toList()
But this is not working. I'm trying to figure out how to use the values generated into forEach are used in the toList()
It's way better to use kotlin.collections function to do this:
List(100) {
Random.nextInt()
}
According to Collections.kt
inline fun <T> List(size: Int, init: (index: Int) -> T): List<T> = MutableList(size, init)
It's also possible to generate using range like in your case:
(1..100).map { Random.nextInt() }
The reason you can't use forEach is that it return Unit (which is sort of like void in Java, C#, etc.). map operates Iterable (in this case the range of numbers from 1 to 100) to map them to a different value. It then returns a list with all those values. In this case, it makes more sense to use the List constructor above because you're not really "mapping" the values, but creating a list
Beside using constructor: List(10, { Random.nextInt() }),
Kotlin also has a built-in function for this purpose: buildList { repeat(10) { add(Random.nextInt()) } }
See: https://kotlinlang.org/docs/constructing-collections.html

Kotlin, How to use the collection function to simplify this code

I am using Kotlin for a project, I write this code can complete the requirement:
val rewards = ArrayList<Map<String, Int>>()
rangeExams
.forEach { examAnswer ->
var reward = hashMapOf("Score" to examAnswer.answerScore)
var questionIds = examAnswer
.answers
.map { it.id }
reward.put("NewQuestion", questionIds.size)
rewards.add(reward)
}
"rangeExams" is a list of collection.
I would like to combinate Kotlin Functions of Collection,
to put elements of rangeExams into a map
and put this map to a new list,
how can I simplify this code by Kotlin ?
ExamAnswer is a pojo:
class ExamAnswer (val id: String, val answerScore: Int, val answers:List<Answer>)
Thank you for your reply
Since you add an item to the rewards for each element of rangeExams, the .forEach { ... } call can be transformed to .map { ... }.
Also, you only use the result of examAnswer.answers.map { it.id } to get its size, so you can remove .map { it.id } and use the size of the original collection.
If you don't need to mutate the maps afterwards, you can replace hashMapOf(...) with mapOf(...).
val rewards = rangeExams.map {
mapOf(
"Score" to it.answerScore,
"NewQuestion" to it.answers.size)
}
If you need to mutate the rewards list after it's created, add .toMutableList() in the end.
There is a little potential to simplify this.
Firstly, I would suggest a more functional approach, which could turn the mutable list rewards into an immutable one.
Secondly, infer the creation of the hash-map reward with the put into one line. You than can use the also the immutable version of the map, instead of the mutable one created by hashMapOf (if you need mutability, than you can just keep hashMapOf).
thirdly, you just use the questionIds to the the size. For that, you don't have to map anything, just call examAnswer.ansers.size. This short call can be inferred as well
fourthly, you can use it instead of explicitly name the param examAnswer because this block is now quite short anyway
This would lead to this code:
val rewards = rangeExams.map {
mapOf("Score" to it.answerScore,
"NewQuestion" to it.answers.size)
}