Using condition to select the sorting property in Kotlin - kotlin

I am using sortedBy() to perform sorting on the collection of objects.
Since the order may change depending on the user choice, I've ended up with the following code
val sortedList = if (sortingOrder == WordSortingOrder.BY_ALPHA) {
list.sortedBy { it.word.value }
} else {
list.sortedBy { it.createdAt }
}
Then I perform further actions on the sorted collection.
I realize that sortedBy() method expects a property to be returned.
I wonder if there is a way to embed the sorting condition in one chain of collection methods.

If your properties are of different types you won't be able to select one of them based on some condition as a result for sortedBy, as their common supertype would be inferred as Any and it is not a subtype of Comparable<R> as sortedBy expects.
Instead you can utilize sortedWith method, which takes a Comparator, and provide a comparator depending on the condition:
list.sortedWith(
if (sortingOrder == WordSortingOrder.BY_ALPHA)
compareBy { it.word.value }
else
compareBy { it.createdAt }
)
Comparators for different properties are created here with the kotlin.comparisons.compareBy function.
You can then extract the logic which selects comparator based on sorting order to a function:
list.sortedWith(comparatorFor(sortingOrder))
fun comparatorFor(sortingOrder: WordSortingOrder): Comparator<MyType> = ...

The sortedBy expects any function of type (T) -> R as its parameter. A property is a corner case of that.
Which means you can do this:
val sortedList = list
.sortedBy { if (sortingOrder == WordSortingOrder.BY_ALPHA) it.word.value else it.createdAt}
Or, if you need something more OOP-ish:
enum class WordSortingOrder(val transform: (MyObject) -> Int) {
BY_ALPHA({it.word.value}),
BY_ALPHA_REVERSED({-1 * it.word.value}),
DEFAULT({it.createdAt})
}
val sortedList = list.sortedBy { sortingOrder.transform(it)}

You can do something like:
list.sortedBy { item ->
when(sortingOrder) {
WordSortingOrder.BY_ALPHA -> item.word.value
else -> item.createdAt
}
}

You can make the lambda argument passed to sortedBy conditional:
list.sortedBy(if (sortingOrder == WordSortingOrder.BY_ALPHA) {
{ it: MyType -> it.word.value }
} else {
{ it: MyType -> it.createdAt }
})
You may find using when instead of if more readable in this scenario:
list.sortedBy(when (sortingOrder) {
WordSortingOrder.BY_ALPHA -> { it: MyType -> it.word.value }
else -> { it: MyType -> it.createdAt }
})
If your selectors have different return types then you can simply wrap your existing code within list.let { list -> ... } or use run:
list.run {
if (sortingOrder == WordSortingOrder.BY_ALPHA) {
sortedBy { it.word.value }
} else {
sortedBy { it.createdAt }
}
}
You can then continue chainging calls after the let/run.

Related

Should I get rid of big switch case?

I have a factory which includes many HTML attribute generators which returns one of them based on the type of attribute, so I wanted to see if there is a better way of doing this.
class AttributeHtmlGeneratorFactory {
fun create(property: String): AttributeHtmlGenerator {
when (property) {
"animation" -> {
return AnimationHtmlGenerator()
}
...
"left", "top" -> {
return PositionHtmlGenerator()
}
...
"scaleX" , "scaleY", ... , "direction" -> {
return UnusedAttributesHtmlGenerator()
}
this when switch has like 20 switch cases in it.
this is the interface which all these classes are using
interface AttributeHtmlGenerator {
fun generateHtml(member: KProperty1<HtmlComponentDataModel, *>, component: HtmlComponentDataModel ): String
}
and this is where and how I'm using all of these:
var result = ""
HtmlComponentDataModel::class.memberProperties.forEach { member ->
val generator = AttributeHtmlGeneratorFactory().create(member.name)
result = result.plus(generator.generateHtml(member, component))
}
return result
also, this is a simple implementation of the interface:
class ButtonFillHtmlGenerator : AttributeHtmlGenerator {
override fun generateHtml(member: KProperty1<HtmlComponentDataModel, *>, component: HtmlComponentDataModel): String {
var result = ""
member.get(component)?.let {
result = result.plus("background-color:${it};")
}
return result
}
}
is there anyway to make this better?
If you just want to reformat the when statement, I suggest you you do like this:
fun create(property: String): AttributeHtmlGenerator = when (property)
{
"animation" -> AnimationHtmlGenerator()
"left", "top" -> PositionHtmlGenerator()
"scaleX", "scaleY", "direction" -> UnusedAttributesHtmlGenerator()
else -> error("No generator found for property $property")
}
If you want to split this logic across modules, you would use a Map.
class AttributeHtmlGeneratorFactory {
private val generatorMap = mutableMapOf<String, () -> AttributeHtmlGenerator>()
init {
assignGeneratorToProperties("animation") { AnimationHtmlGenerator() }
assignGeneratorToProperties("left", "top") { PositionHtmlGenerator() }
}
fun create(property: String): AttributeHtmlGenerator {
return generatorMap[property]?.invoke() ?: error("No generator found for property $property")
}
fun assignGeneratorToProperties(vararg properties: String, provider: () -> AttributeHtmlGenerator) {
properties.forEach {
generatorMap[it] = provider
}
}
}
This way you can call assignGeneratorToProperties in parts of the code and thus split the initialization logic.
Performance-wise, when/if-else statements are really performant when you have a few cases but a HashMap outperforms them for a lot of elements. You decide what to use depending on your case.

In Kotlin, how can I test and use a value without computing it twice?

Every so often, I find myself wanting to compute a value for some sort of filter operation, but then wanting to use that value when it's already disappeared into the condition-checking thing.
For instance:
val found = list.firstOrNull { slowConversion(it).isWanted() }
if (found != null) {
something(found, slowConversion(found))
}
or
when {
other_conditions -> other_actions
list.any { it.contains(regex1) } -> something(list.firstOrNull { it.contains(regex1) } ?: "!!??")
}
For the slowConversion() I can work with a sequence mapped to pairs, although the terms first and second kinda confuse things a bit...
val pair = list.asSequence().map { it to slowConversion(it) }.firstOrNull { it.second.isWanted() }
if ( pair != null ) {
something(pair.first, pair.second)
}
or if I only want the conversion,
val converted = list.firstNotNullOfOrNull { slowConversion(it).takeIf { it.isWanted() } }
but the best I can come up with to avoid the when duplication involves moving the action part into the condition part!
fun case(s: List<String>, r: Regex) {
val match = s.firstOrNull { it.contains(r) }?.also { something(it) }
return match != null
}
when {
other_conditions -> other_actions
case(list, regex1) -> true
}
At this point, it seems I should just have a stack of function calls linked together with ||
other_things || case(list, regex1) || case(list, regex2) || catchAll(list)
Is there something better or more concise for either of these?
You can write your first example like this:
for(element in list) {
val result = slowConversion(element)
if(result.isWanted()) {
something(element, result)
break
}
}
This might not look very Kotlin-ish, but I think it's pretty straightforward & easy to understand.
For your second example, you can use the find function:
when {
other_conditions -> other_actions
else -> list.find { it.contains(regex1) }?.let(::something)
}
If you have multiple regexes, just iterate over them,
val regexes = listOf(regex1, regex2, ...)
for(regex in regexes) {
val element = list.find { it.contains(regex1) } ?: continue
something(element)
break
}

How can I create a condition to check the class used to create a KClass instance in Kotlin

In the code below I wanna know what was the class used to create a KClass instance. In the array case I found a way using java.IsArray, but how to do it for the other cases?
fun main() {
val myKClassList = listOf(String::class, Array<String>::class, Int::class)
myKClassList.forEach {
if (it.java.isArray) println("Array")
else if (??) println("String")
else if (??) println("Int")
}
}
If all you need is the string representation, you can use the simpleName or qualifiedName property:
for (it in myKClassList) {
println(it.simpleName)
}
If you need the actual class, you already have that--it's the KClass instance. So if you're doing something based on the type:
for (it in myKClassList) {
when (it) {
Array<String>::class -> println("String array")
String::class -> println("String")
Int::class -> println("Int")
//etc.
}
}
Edit If you want to treat all array types the same, we are mixing condition types so you you need when without the argument:
for (it in myKClassList) {
when {
it.java.isArray -> println("Array")
it == String::class -> println("String")
it == Int::class -> println("Int")
//etc.
}
}
If you're looking for the instance type check, consider is:
when (it) {
is Int -> println("Int")
is String -> println("String")
...
}

How to use Kotlin fold function to convert an array into a map?

I am trying to convert an Array via fold into an indexed Map. Somehow IntelliJ flags that when I return the accumulator that it expects Unit. When I remove the return it complains that I require the datatype I originally wanted to return.
The code is as follows (Item is just a data class)
constructor(vararg items: Item){
val itemMap = items.fold(mutableMapOf<Int, MutableList<Item>>(), { acc, item ->
if (acc.containsKey(item.state)) {
acc[item.state]?.add(item)
} else {
acc.put(item.state, mutableListOf(item))
}
return acc
})
}
Its a bit late here so I probably miss something very obvious. Any help would be very appreciated.
Thanks
Use the qualified return#fold operator instead of return. In Kotlin, return without a qualifier means 'return from the innermost fun (ignoring lambdas)'.
val itemMap = items.fold(mutableMapOf<Int, MutableList<Item>>(), { acc, item ->
if (acc.containsKey(item.state)) {
acc[item.state]?.add(item)
} else {
acc.put(item.state, mutableListOf(item))
}
return#fold acc
})
See Whats does “return#” mean?, Return at Labels in the language reference.
Or just use the result expression, omitting return:
val itemMap = items.fold(mutableMapOf<Int, MutableList<Item>>(), { acc, item ->
if (acc.containsKey(item.state)) {
acc[item.state]?.add(item)
} else {
acc.put(item.state, mutableListOf(item))
}
acc
})
Basically, this kind of fold is implemented in the standard library: see .groupBy { ... }.

Idiomatic way to return if not null in Kotlin

I am looking for an idiomatic way to return if not null a variable in Kotlin. For example, I would like something such as:
for (item in list) {
getNullableValue(item).? let {
return it
}
}
But it's not possible to return inside a let block in Kotlin.
Is there a good way to do this without having to do this:
for (item in list) {
val nullableValue = getNullableValue(item)
if (nullableValue != null) {
return nullableValue
}
}
Not sure if this would be called idiomatic, but you could do this:
val nullableValue = list.find { it != null }
if (nullableValue != null) {
return nullableValue
}
Edit:
Based on s1m0nw1's answer, you can probably reduce it to this:
list.find { it != null }?.let {
return it
}
It is possible to return from let, as you can read in the documentation:
The return-expression returns from the nearest enclosing function, i.e. foo. (Note that such non-local returns are supported only for lambda expressions passed to inline functions.)
let() is an inline function and therefore you automatically return from the enclosing function whenever you do return within let, like in this example:
fun foo() {
ints.forEach {
if (it == 0) return // nonlocal return from inside lambda directly to the caller of foo()
print(it)
}
}
To modify the behavior, "labels" can be used:
fun foo() {
ints.forEach lit# {
if (it == 0) return#lit
print(it)
}
}
The "right" idiomatic way of doing this is using the "first" method.
Example:
val x = listOf<Int?>(null, null, 3, null, 8).first { it != null }
His specific example would be
return list.first {getNullableValue(it) != null}
It could be something like:
for (item in list) {
getNullableValue(item)?.also {
return it
}
}
I am assuming the external loop is needed. If that is not the case, Ryba suggested solution should work.