Mapping an iterable 1-to-n in kotlin - kotlin

What I am trying to do is use the Iterable.map but instead of transforming every one value to one new value, I want to transform one value to multiple new values.
For example:
val myList = listOf("test", "123", "another.test", "test2")
val result = myList.map {
if(it.contains(".")) {
return#map it.split(".")
} else {
return#map it
}
}
//desired output: ["test", "123", "another", "test", "test2"]
This code would result in a List which contains both strings and lists of strings (type Any).
How could I most elegantly implement this?

One quick way to do this is using flatMap.
val output = myList.flatMap { if(it.contains(".")) it.split(".") else listOf(it) }
The flatMap method transforms the each element using given function and then flatten the result to a single list.
https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/flat-map.html

Related

Kotlin sort one List with key and Enum with key and order

I receive data from Request information as list data (List) below code. That data has a "key" parameter by which I want to sort it.
data class ApplianceSetting(
#SerializedName("key") val key: String,
#SerializedName("value") var value: Any,
(...)
I have the required order in the SettingsUtilEnum and want to sort items by that.
After that, I can convert the list using map{} the data and use the function of Enum getSettingByMode() and get the list of Enum values. Then I will sort them and convert them again to List.
But that sounds too inefficient. Is there a better way.
enum class SettingsUtilEnum(
var settingKey: String,
override val order: Int = 99,
var settingName: String = "",
) : AbstractOrderEnum {
FIRST_MODE("first.mode", 0),
SECOND_MODE("second.mode", 1),
(...)
UNKNOWN_MODE("", 99);
companion object {
#JvmStatic
fun getSettingByMode(settingKey: String): SettingsUtilEnum? {
return values().find { it.settingKey == settingKey }
}
k
private fun initDataObserver() {
(activity as FavouriteActivity).viewModel.applianceSettings.observe(activity as FavouriteActivity
) { data ->
(controlRecyclerView.adapter as FavouriteAdditionalControlsAdapter)
val adapter = (controlRecyclerView.adapter as FavouriteAdditionalControlsAdapter)
// public final var data: List<ApplianceSetting>
// old code:
// data.settings
adapter.data = sortAndGetControlModes(data)
adapter.notifyDataSetChanged()
}
}
// TODO: sortAndGetControlModes
private fun sortAndGetControlModes(data: ApplianceSettingsList) =
data.settings.map {
getSettingByMode(it.key)
?: UNKNOWN_MODE.apply {
// If in future new modes are added -> put them as tail
settingKey = it.key
}
}.sortedBy { it.order }
// error i need to return again List<ApplianceSetting>
If you want to compare keys with theirs ASCII values you can just use sortBy { it.key }
If you want to expand possibilities of comparison you can use function sortedWith with passing custom comparator as argument.
Comparator used to compare its two arguments for order. Returns zero if the arguments are equal, a negative number if the first argument is less than the second, or a positive number if the first argument is greater than the second.
Example:
You can use it like that if you want to sort by integer value of key parameter:
data.settings.sortedWith { a, b ->
when {
a.key.toInt() < b.key.toInt() -> -1
a.key.toInt() > b.key.toInt() -> 1
else -> 0
}
}
I fixed it using sortedBy and as comparator I am using received value (order) from getSettingByMode(), if item is not found (null) I give him order value of 99 and put it on tail position:
private fun sortAndGetControlModes(data: ApplianceSettingsList) =
data.settings.sortedBy {
getSettingByMode(it.key)?.order ?:99
}

Kotlin combine lists based on common property

I have two functions (GetPodsOne and GetPodsTwo) that return me a big csv string. I then do some processing to discard the part of the string I don't want. See snippet below.
var podValues = execGetPodsOne()
val testPodValuesLst: List<String> = podValues.split(",").map { it -> it.substringAfterLast("/") }
testPodValuesLst.forEach { it ->
 println("value from testPodList=$it")
 }
podValues = execGetPodsTwo()
val sitPodValuesLst: List<String> = podValues.split(",").map { it -> it.substringAfterLast("/") }
sitPodValuesLst.forEach { it ->
 println("value from sitPodList=$it")
 }
This leaves me with two lists. See output of the above below:
value from testPodList=api-car-v1:0.0.118
value from testPodList=api-dog-v1:0.0.11
value from testPodList=api-plane-v1:0.0.36
value from sitPodList=api-car-v1:0.0.119
value from sitPodList=api-dog-v1:0.0.12
value from sitPodList=api-plane-v1:0.0.37
What i would like to do is end up with the objects inside a data class like below:
data class ImageVersions(val apiName: String, val testPodVersion: String, val sitPodVersion: String)
api-car-v1, 0.0.118, 0.0.119
api-dog-v1, 0.0.11, 0.0.12
api-plane-v1, 0.0.36, 0.0.37
I've used test and sit above but I'm going to have maybe another 5 environments eventually. Looking for a nice way to get the versions for each api and easily combine into that ImageVersions data class.
thanks
Considering that you're going to have maybe another 5 environments eventually, I tried to write something that will scale well:
enum class Env { Test, Sit }
data class ImageVersions(val apiName: String, val versions: Map<Env, String?>)
fun String.getNameAndVersion() = substringBefore(':') to substringAfter(':')
fun getVersions(envMap: Map<Env, List<String>>): List<ImageVersions> {
val envApiNameMap = envMap.mapValues { it.value.associate(String::getNameAndVersion) }
val allApiNames = envApiNameMap.flatMap { it.value.keys }.distinct()
return allApiNames.map { apiName ->
ImageVersions(apiName, envApiNameMap.mapValues { it.value[apiName] })
}
}
Playground example
So instead of separate val testPodVersion: String, val sitPodVersion: String, here you have a map. Now the structure of ImageVersions always remains the same irrespective of how many environments you have.
getNameAndVersion is a helper function to extract apiName and version from the original string.
getVersions accepts a list of versions corresponding to each environment and returns a list of ImageVersions
envApiNameMap is same as envMap just that the list is now a map of apiName and its version.
allApiNames contains all the available apiNames from all environments.
Then for every apiName, we take all the versions of that apiName from all the environments.
In future, if your have another environment, just add it in the Env enum and pass an extra map entry in the envMap of getVersions. You need not modify this function every time you have a new environment.
How about this:
val testPodValuesMap = testPodValuesLst.associate { it.split(':').zipWithNext().single() }
val sitPodValuesMap = sitPodValuesLst.associate { it.split(':').zipWithNext().single() }
val mergedMap = (testPodValuesMap.keys + sitPodValuesMap.keys).associateWith { key ->
testPodValuesMap.getValue(key) to sitPodValuesMap.getValue(key)
}
val imageVersions = mergedMap.map { (k, v) -> ImageVersions(k, v.first, v.second) }
println(imageVersions.joinToString("\n"))
which prints
ImageVersions(apiName=api-car-v1, testPodVersion=0.0.118, sitPodVersion=0.0.119)
ImageVersions(apiName=api-dog-v1, testPodVersion=0.0.11, sitPodVersion=0.0.12)
ImageVersions(apiName=api-plane-v1, testPodVersion=0.0.36, sitPodVersion=0.0.37)
As a first step I would extract the apiNames from both lists:
val apiNames = list1.map { it.replace("value from ", "").split("[=:]".toRegex())[1] }
.plus(list2.map { it.replace("value from ", "").split("[=:]".toRegex())[1] })
.distinct()
Then I'd create the ImageVersions instances by looping over apiNames:
val result = apiNames
.map { apiName ->
ImageVersions(
apiName,
(list1.firstOrNull { it.contains(apiName) } ?: "").split(":")[1],
(list2.firstOrNull { it.contains(apiName) } ?: "").split(":")[1]
)
}
.toList()
The reason to first extract the apiNames is, that apiNames missing in one of the two lists will still end up in the final result.
Kotlin Playground

Kotlin: How to flatten list of Hashmaps

How can I flatten a list of HashMaps in Kotlin?
var listOfMaps: List<Map<String, String>> = listOf(mapOf("test" to "test1"), mapOf("test2" to "test3"), mapOf("test4" to "test5"))
I would like to get:Map<String,String> with all key value paires
val map:Map<String, String> = listOfMaps
.flatMap { it.entries }
.associate { it.key to it.value }
you can do something like this if you don't know if the list can be empty.
val map = listOfMaps.fold(mapOf<String, String>()) {acc, value -> acc + value }
If the list never will be empty you can use reduce instead.
Thank you Demigod for the comments
You could use fold:
listOfMaps.fold(
mutableMapOf<String, String>(),
{ acc, item -> acc.also { it.putAll(item) } }
)
The first parameter mutableMapOf<String, String>() creates an empty map to put the values into. This is called the accumulator
The second parameter is a function which takes two arguments
The accumulator
An item from the original list
This function is run sequentially against all items in the list. In our case it adds all the items from each map to the accumulator.
Note: This function does not account for duplicate keys. If a later map has the same key as an earlier one then the value just gets overridden.
Also note (pun intended): We use acc.also {} as we want to return the actual map, not the return value from the addAll method
Well... seeing lots of solutions, I will add my two cents here:
If you don't mind losing the values of duplicated keys you can use something as follows:
listOfMaps.flatMap { it.entries }.associate{ it.key to it.value } // or: it.toPair() if you will
// depending on how large those lists can become, you may want to consider also using asSequence
If you instead want to collect all entries including duplicate keys (i.e. saving all the values), use the following instead (which then gives you a Map<String, List<String>>):
listOfMaps.flatMap { it.entries }.groupBy({ it.key }) { it.value }
Also here the comment regarding asSequence holds...
Finally if you can omit those maps within the list and just use a Pair instead, that will spare you the flatMap { it.entries }-call and make things even easier, e.g. you could just call .toMap() then for the first case and groupBy directly for the second and the question regarding asSequence no longer arises.
An extra addition to this, if you have single value maps, maybe you want to switch to a List<Prair<String, String>>. In that case, the solution is straight forward:
You would have something like:
var listOfMaps: List<Pair<String, String>> = listOf("test" to "test1", "test2" to "test3", "test4" to "test5")
and toMap() would dsolve it all:
listOfMaps.toMap()
If you don't have duplicate keys or don't care for them, you can do it like this:
val flattenedMap = listOfMaps.flatMap { it.toList() }.toMap()
If you have duplicate keys and care for them, you can do it like this:
val flattenedMap = mutableMapOf<String, MutableList<String>>().apply {
listOfMaps.flatMap { it.toList() }.forEach {
getOrPut(it.first) {
mutableListOf()
}.add(it.second)
}
}
The result will be Map<String, List<String>> then of course.
This solution works with same keys (and different collection as values) by merging them together instead of overwriting them
/**
* Merge two maps with lists
*/
fun <K,V>Map<K,Collection<V>>.mergeLists(other: Map<K,Collection<V>>) =
(this.keys + other.keys).associateWith {key ->
setOf(this[key], other[key]).filterNotNull()
}.mapValues { (_,b) -> b.flatten().distinct() }
/**
* Merge two maps with sets
*/
fun <K,V>Map<K,Collection<V>>.mergeSets(other: Map<K,Collection<V>>) =
(this.keys + other.keys).associateWith {key ->
setOf(this[key], other[key]).filterNotNull()
}.mapValues { (_,b) -> b.flatten().toSet() }
Then use like e.g. listOfMaps.reduce { a, b -> a.mergeSets(b) }
Test:
#Test
fun `should merge two maps with as lists or sets`() {
// GIVEN
val map1 = mapOf(
"a" to listOf(1, 2, 3),
"b" to listOf(4, 5, 6),
"c" to listOf(10),
"e" to emptyList()
)
val map2 = mapOf(
"a" to listOf(1, 9),
"b" to listOf(7),
"d" to listOf(null)
)
// WHEN
val mergedAsLists = map1.mergeLists(map2)
val mergedAsSets = map1.mergeSets(map2)
// THEN
listOf(mergedAsLists, mergedAsSets).forEach { merged ->
assertThat(merged.keys).containsOnly("a", "b", "c", "d", "e")
assertThat(merged["a"]).containsOnly(1,2,3,9)
assertThat(merged["b"]).containsOnly(4,5,6,7)
assertThat(merged["c"]).containsOnly(10)
assertThat(merged["d"]).containsOnly(null)
assertThat(merged["e"]).isEmpty()
}
}

Removing elements from a list which are not in another list - Kotlin

I have two mutableLists, listOfA has so many objects including duplicates while listOfB has fewer. So I want to use listOfB to filter similar objects in listOfA so all list will have equal number of objects with equivalent keys at the end. Code below could explain more.
fun main() {
test()
}
data class ObjA(val key: String, val value: String)
data class ObjB(val key: String, val value: String, val ref: Int)
fun test() {
val listOfA = mutableListOf(
ObjA("one", ""),
ObjA("one", "o"),
ObjA("one", "on"),
ObjA("one", "one"),
ObjA("two", ""),
ObjA("two", "2"),
ObjA("two", "two"),
ObjA("three", "3"),
ObjA("four", "4"),
ObjA("five", "five")
)
//Use this list's object keys to get object with similar keys in above array.
val listOfB = mutableListOf(
ObjB("one", "i", 2),
ObjB("two", "ii", 5)
)
val distinctListOfA = listOfA.distinctBy { it.key } //Remove duplicates in listOfA
/*
val desiredList = doSomething to compare keys in distinctListOfA and listOfB
for (o in desiredList) {
println("key: ${o.key}, value: ${o.value}")
}
*/
/* I was hoping to get this kind of output with duplicates removed and comparison made.
key: one, value: one
key: two, value: two
*/
}
If you want to operate directly on that distinctListOfA you may want to use removeAll to remove all the matching entries from it. Just be sure that you initialize the keys of B only once so that it doesn't get evaluated every time the predicate is applied:
val keysOfB = listOfB.map { it.key } // or listOfB.map { it.key }.also { keysOfB ->
distinctListOfA.removeAll {
it.key !in keysOfB
}
//} // if "also" was used you need it
If you have a MutableMap<String, ObjA> in place after evaluating your unique values (and I think it may make more sense to operate on a Map here), the following might be what you are after:
val map : MutableMap<String, ObjA> = ...
map.keys.retainAll(listOfB.map { it.key })
retainAll just keeps those values that are matching the given collection entries and after applying it the map now contains only the keys one and two.
In case you want to keep your previous lists/maps and rather want a new list/map instead, you may just call something like the following before operating on it:
val newList = distinctListOfA.toList() // creates a new list with the same entries
val newMap = yourPreviousMap.toMutableMap() // create a new map with the same entries
I tried this
primaryList.removeAll { primaryItem ->
secondaryList.any { it.equals(primary.id, true) }
}
PrimaryList here is a list of objects
SecondaryList here is a list of strings

Kotlin - from a list of Maps, to a map grouped by key

I have a List<Map<Branch,Pair<String, Any>>> that I would like to convert in a single Map<Branch,List<Pair<String, Any>>> .
So if I have an initial list with simply 2 elements :
List
1. branch1 -> Pair(key1,value1)
branch2 -> Pair(key2,value2)
2. branch1 -> Pair(key1a,value1a)
I want to end up with :
Map
branch1 -> Pair(key1,value1)
Pair(key1a,value1a)
branch2 -> Pair(key2,value2)
so a kind of groupBy, using all the values of the keys in the initially nested maps..
I have tried with
list.groupBy{it-> it.keys.first()}
but obviously it doesn't work, as it uses only the first key. I want the same, but using all keys as individual values.
What is the most idiomatic way of doing this in Kotlin ? I have an ugly looking working version in Java, but I am quite sure Kotlin has a nice way of doing it.. it's just that I am not finding it so far !
Any idea ?
Thanks
The following:
val result =
listOfMaps.asSequence()
.flatMap {
it.asSequence()
}.groupBy({ it.key }, { it.value })
will give you the result of type Map<Branch,List<Pair<String, Any>>> with the contents you requested.
val list: List<Map<Branch, Pair<String, Any>>> = listOf()
val map = list
.flatMap { it.entries }
.groupBy { it.key }
.mapValues { entry -> entry.value.map { it.value } }
I've managed to write this.
data class Branch(val name: String)
data class Key(val name: String)
data class Value(val name: String)
val sharedBranch = Branch("1")
val listOfMaps: List<Map<Branch, Pair<Key, Value>>> = listOf(
mapOf(sharedBranch to Pair(Key("1"), Value("1")),
Branch("2") to Pair(Key("2"), Value("2"))),
mapOf(sharedBranch to Pair(Key("1a"), Value("1a")))
)
val mapValues: Map<Branch, List<Pair<Key, Value>>> = listOfMaps.asSequence()
.flatMap { map -> map.entries.asSequence() }
.groupBy(Map.Entry<Branch, Pair<Key, Value>>::key)
.mapValues { it.value.map(Map.Entry<Branch, Pair<Key, Value>>::value) }
println(mapValues)
Is it appliable for your needs?
Everyone else is using flatMap, but you can also consider using fold, which is a common operation for reducing a larger collection into a smaller one. (For example, you can fold a list of integers into a single sum; here, a list of maps into a single map).
Perhaps others will find this easier to read than the flatMap versions above:
val listOfMaps: List<Map<Key, Value>> = ...
val mergedMaps: Map<Key, List<Value>> =
listOfMaps
.fold(mutableMapOf()) { acc, curr ->
curr.forEach { entry -> acc.merge(entry.key, listOf(entry.value)) { new, old -> new + old } }
acc
}
What the above code is doing:
Create a new, empty map. This will be acc (that is, the accumulator).
Iterate through our list of maps.
Work on one map (curr) at a time.
For the current map, run over each of its key/value pairs.
For each key/value, call merge on acc, passing in a list of size one (wrapping the value). If nothing is associated with the key yet, that list is added; otherwise, it is appended to the list already there.
Return the accumulating map, so it's used again in the next step.
Surprised nobody has mentioned the associate function.
val listy: List<Map<String, Int>> =
listOf(mapOf("A" to 1, "B" to 2), mapOf("C" to 3, "D" to 4))
val flattened = listy
.flatMap { it.asSequence() }
.associate { it.key to it.value }
println(flattened)
will print out {A=1, B=2, C=3, D=4}
Extract it to an extension function
private fun <K, V> List<Map<K, V>>.group(): Map<K, List<V>> =
asSequence().flatMap { it.asSequence() }.groupBy({ it.key }, { it.value })
Use it like so:
val list = yourListOfMaps
val grouped = list.group()