Kotlin iterate a collection and map a certain value to new map - kotlin

I have a map of collections . I need to get a list of ids from that..
val m1 = mapOf("id" to 1, "name" to "Alice")
val m2 = mapOf("id" to 2, "name" to "Bob")
val m3 = mapOf("id" to 3, "name" to "Tom")
val nameList = listOf(m1, m2, m3)
The result shall be [1, 2, 3]

Assuming you want a list as per the example, not a map as per the title, I would do it like this:
val result = nameList.map {
it.getValue("id").also { id ->
require(id is Int) { "id must be an Int" }
} as Int
}
This has the advantage of handling the following errors cleanly:
The id key is missing: NoSuchElementException: Key id is missing in the map
The id value is not an Int: IllegalArgumentException: id must be an Int

First things first, I believe that if you can, you should use classes instead of maps for storing heterogeneous data like this. So instead of your maps, you can use:
data class Person(val id: Int, val name: String)
val m1 = Person(id = 1, name = "Alice")
val m2 = Person(id = 2, name = "Bob")
val m3 = Person(id = 3, name = "Tom")
val list = listOf(m1, m2, m3)
val idsList = list.map { it.id } // no error handling required, rely on the type system
Now, if you really want to use maps like that, you have several options.
If you're certain the id key will be present and its value will be an Int, you can use the following:
nameList.map { it["id"] as Int }
This will fail with NullPointerException if id is not present in one of the maps or with ClassCastException if it's not an Int.
Normally you should make sure your map matches your contract at creation time, and not when accessing this kind of information.
But if you need to handle errors here for some reason, you can use the following instead:
nameList.map {
(it.getValue("id") as? Int) ?: error("'id' is not an Int")
}
getValue fails on absent keys with NoSuchElementException, and the error() call fails with IllegalStateException. You can also use other kinds of exceptions using throw or require().
If you want to just ignore the entries that don't have a valid integer id, you can use the following:
nameList.mapNotNull { it["id"] as? Int }
If you want to ignore the entries that don't have an id, but fail on those who have a non-integer id, you can use this:
nameList.mapNotNull { map ->
map["id"]?.let { id ->
(id as? Int) ?: error("'id' is not an Int")
}
}
These 2 last examples rely on mapNotNull, which filters the elements out if their mapped value is null.

Related

Kotlin, is it possible to access the pair of a map?

Is it possible to access the whole Pair of a map, not only the key or a value?
Let's say we have a map
map = mapOf(Pair("Example1", 1), Pair("Example2", 2), Pair("Example3",
3))
I would like to access the second pair and put it into a variable, something like I would do with a list:
val ex2 = map[1] #this would result with {"Example2", 2}
And then i would be able to access the pair's key/value like:
ex2.key / ex2.value
More specifically, I would like to use this in my function to return a specific pair of the map.
Not sure if this would help
val mapString = mutableMapOf(1 to "Person", 2 to "Animal")
val (id, creature) = 1 to mapString.getValue(1)
Log.e("MapPair", "$id, $creature")
prints
1, Person
or if you're iterating through the entire map
mapString.forEach {
val (id, creature) = it.key to it.value
Log.e("MapPair", "$id : $creature")
}
prints
1 : Person
2 : Animal
or using Pair
val key = 1
val pair = Pair(key, mapString.getValue(key))
Log.e("MapPair", "$pair")
prints
(1, Person)
or if you're iterating through the entire map using Pair
mapString.forEach {
val pair = Pair(it.key, it.value)
Log.e("MapPair", "$pair")
}
prints
(1, Person)
(2, Animal)
Update: For iterating through the map you can also go with Destructuring Declarations
val mapString = mutableMapOf(1 to "Person", 2 to "Animal")
for ((key, value) in mapString) {
Log.e("MapComponents", "$key, $value")
}
From your comment, it seems like you want to fetch the key corresponding to a given value.
val map = mapOf("Chicken" to 20, "Egg" to 10, "Bread" to 5)
val valueToFind = 20
val key = map.toList().find { it.second == valueToFind }?.first
println(key)
Output:
Chicken
If the value doesn't exist, it will give null.

How can use filter and contains with multi-ArrayList in Kotlin so I only have elements which match the condition?

I have a class called Person
data class Person(
val id: Int,
val name: String
)
data class IDs(
val id : Int,
val active : Boolean )
and an array list that has numbers of ids and another list of Persons
val myStu = listOf<Person>(Person(1, "Name_1"), Person(2, "Name_2"), Person(3, "Name_3"))
var ids = listOf<IDs>(IDs(1,false),IDs(2,true),IDs(3,true))
var newIds = listOf<Int>(2,3,4,6)
First I want to apply two actions to the myStu, first is to have a list that include all the items from myStu that his id matches the id in the IDS and only if the active is true
myStu or the new list will have the values
Person(2, "Name_2"), Person(3, "Name_3"))
Then do action two , I need to add a new item to the new list that their id does not exist in the newIds , in another word we will add a new person Person(4,"None") and (6,"None) , 4 and 6 values come from newIds list
the final output will be :
id= 2 name = "Name_2", id= 3 name = "Name_3", id= 4 name = "None" , id =6 name="None"
I want to write the code with filter , I failed with first step because I don't know how to use contains() with the list inside the filter
val newArr = myStu.filter {
ids.contains(it.id)
}
The "easiest" way of doing that would be to use filter directly, there's no need for contains. If we were to use contains, then we would need to also search for which element contained the id, in order to get the status. We can just do a .any() to do both at the same time.
V1
val activeStu = myStu.filter { person -> ids.any { it.id == person.id && it.active } }
val result = newIds.map { newId ->
activeStu.find { it.id == newId } ?: Person(id = newId, name = "None")
}
Another method, that might work a bit better if we have big lists, would be to first transform the IDs list into a map. That way the second part of our code is a bit more efficient, since there is no search involved.
V2
val idsMap = ids.associate { it.id to it.active }
val activeStu = myStu.filter { idsMap[it.id] ?: false }
//the creation of the result list is the same
Version without creating 2 new lists. This works, but it might be quite ineficient processing wise, and also harder to understand what is going on IMO.
V3
val result = newIds.map { newId ->
//try to find an IDs with the current newId and status as true
when (ids.find { it.id == newId }?.active) {
//if found, then find the corresponding Person
true -> myStu.find { it.id == newId } ?: Person(newId, "None") // if this happens, it means that an IDs with status true existed and no Person had that id. Not sure what you want in this scenario, this version creates one of the "none" persons.
//if not found, then create a new one
else -> Person(newId, "None")
}
}
Note: depending on what version of kotlin you have, you might have to change the when statement to this:
when (ids.find { it.id == newId }?.active == true)
Since I think I remember that null didn't used to be treated as false in old versions (I've run this with version 1.4.20).
Btw, you can also use this version with the idsMap from V2, just replace the when(...) with when(idsMap[newId] or when(idsMap[newId] == true) depending on the kotlin version.

Kotlin filter list by predicate

I am trying to filter a list based on a condition that a property inside the list is an enum type. But I get an error on the filter function. Can anyone tell me how to resolve this error and why it is happening?
Type inference failed. The value of the type parameter T should be mentioned in input types (argument types, receiver type or expected type). Try to specify it explicitly.
My code is below:
data class Person(
val name: String,
val ageInDays: Int,
val currentStatus: List<Status>,
)
data class Status(
val name: String,
val activity: Activity
)
enum class Activity {
COOK,
CLEAN,
SLEEP,
}
fun main() {
var build = listOf(
Person("abc", 3655, listOf(
Status("abcProc1", Activity.COOK),
Status("abcProc2", Activity.CLEAN),
Status("abcProc2", Activity.SLEEP),
)
),
Person("ghi", 500, listOf(
Status("ghiProc", Activity.COOK),
Status("ghiProc", Activity.SLEEP),
)
),
Person("def", 1000,listOf(
Status("defProc", Activity.SLEEP)
)
)
)
println(build.filter { it.currentStatus.contains(Activity.CLEAN) })
}
currentStatus is a List<Status>. The only type of object that could be in the list is a Status (or subtype thereof). So it doesn't make sense to call contains on the list with an argument that is not a Status. An Activity is not a subtype of Status.
Assuming you want to filter your list of Status to only include instances of Status for which the activity property is Activity.CLEAN, you would do it like:
build.filter { it.currentStatus.any { status -> status.activity == Activity.CLEAN } }
or slightly less efficient but possibly clearer logic:
build.filter { it.currentStatus.map(Status::activity).contains(Activity.CLEAN) }

How can I loop over an array of sets/maps with varying data types

I want to make it so that I keep my code dry and create 3 (or more, or less) buttons with somewhat the same structure. So I create a list of objects to loop over and put the data inside the object to use in several places in the AppButton.
I might think a bit too Pythonic, because that's my main language and I only recently started using Kotlin. What I normally do in Python:
app_buttons = [
dict(
text="....",
icon="....",
uri_string="....",
),
...
]
I've tried something similar in Kotlin with mapOf:
val appButtons = arrayOf(
mapOf(
"title" to getString(R.string.app_btn_example1),
"icon" to R.drawable.ic_some_icon_1_64,
"uriString" to "myapp://example1",
),
...
)
and then loop over them and getting from the map:
for (entry in appButtons) {
buttons.add(
AppButton(
entry.get("text"),
entry.get("icon"),
) {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(entry.get("uriString"))).apply {
val name = getString(R.string.saved_account_key)
putExtra(name, sharedPref.getString(name, null))
}
startActivity(intent)
}
)
}
But then I get Type mismatch. Required String. Found {Comparable & java.io.Serializable}?. I don't know what types to put where...
Ok different approach, using setOf and destructuring:
val appButtons = arrayOf(
setOf(
getString(R.string.app_btn_example1),
R.drawable.ic_some_icon_1_64,
"myapp://example1",
),
...
)
for ((text, icon, uriString) in appButtons) {
buttons.add(
AppButton(
text,
icon
) {
...
}
)
}
But now I get the following:
Destructuring declaration initializer of type Set<{Comparable<*> & java.io.Serializable}> must have a 'component1()' function
Destructuring declaration initializer of type Set<{Comparable<*> & java.io.Serializable}> must have a 'component2()' function
Destructuring declaration initializer of type Set<{Comparable<*> & java.io.Serializable}> must have a 'component3()' function
How do I make this work? How do I create a basic list of objects and loop over them with the correct types? It feels so simple in Python. I'm clearly missing something.
Rather than using maps, you should create a data class. For example:
data class ButtonModel(
val title: String,
val icon: Int,
val uriString: String,
)
You can then create the array like this:
val appButtons = arrayOf(
ButtonModel(
title = getString(R.string.app_btn_example1),
icon = R.drawable.ic_some_icon_1_64,
uriString = "myapp://example1",
),
...
)
Or without the parameter labels if you prefer:
val appButtons = arrayOf(
ButtonModel(
getString(R.string.app_btn_example1),
R.drawable.ic_some_icon_1_64,
"myapp://example1",
),
...
)
Then, rather than getting them with get or [], you can just use the dot syntax:
buttons.add(
AppButton(
entry.text,
entry.icon,
) {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(entry.uriString)).apply {
val name = getString(R.string.saved_account_key)
putExtra(name, sharedPref.getString(name, null))
}
startActivity(intent)
}
)

Why filtering out null map keys doesn't change type to not nullable in kotlin?

I have a list of objects with an optional id as String and I want to make a map out of it.
I want to have the keys of my map as non nullable: so something like this:
data class Foo(
val id: String? = null
val someStuff: String? = null,
)
val foo = listOf(Foo("id1"), Foo())
val bar = foo.filterNot { it.id == null }.associateBy { it.id }
Here bar type is Map<String?, Foo> but not Map<String, Foo>
My workaround is to add a non null asserted call: !!, but it doesn't seem clean.
Is there an easy and safe way to do this?
This looks like something that contracts could help with, but currently a contract expression can't access properties of the class in use.
As a workaround, you could define a 2nd class that has a non-null id, like so
data class Foo(
val id: String? = null,
val someStuff: String? = null
)
data class Foo2(
val id: String,
val someStuff: String? = null
)
val foo = listOf(Foo("id1"), Foo())
val bar = foo
.mapNotNull { if (it.id != null) Foo2(it.id, it.someStuff) else null }
.associateBy { it.id }
There's a six-year-old open feature request for Map.filterNotNullKeys() and a four-year old open feature request for Map.associateByNotNull().
In my opinion, the associateBy { it.id!! } would be cleanest for readability. But you could do it like this:
val bar = foo.mapNotNull { it.id?.run { it.id to it } }.toMap()
As for your actual question, that logic is way too many steps for the compiler to infer. Your last function call to associateBy sees a nullable, so it infers a nullable. For the compiler to figure this out, it would have to step back and see that the List that you call associateBy on happens to have filtered out certain objects in a way that happens to ensure that a certain nullable property won't be null within this specific list, and it's the same property that you are associating with. Now imagine it has to do this for every call to any generic function, and the various lambdas involved could potentially have multiple lines of code. Compile times would skyrocket.