How to collect to Map from List where null values are excluded/skipped?
This code doesn't skip null values:
val map = listOf(Pair("a", 1), Pair("b", null), Pair("c", 3), Pair("d", null))
.associateBy({ it.first }, { it.second })
println(map)
Workaround solution. But collects into mutable map:
val map2 = listOf(Pair("a", 1), Pair("b", null), Pair("c", 3), Pair("d", null))
.mapNotNull {
if (it.second != null) it else null
}.toMap()
println(map2)
So is there more convenient way to do this? Also I want to get Map<String, Int> type, not Map<String, Int?>
Actually, a slight change to pwolaq's answer guarantees that the second item is non-nullable:
val map = listOf(Pair("a", 1), Pair("b", null), Pair("c", 3), Pair("d", null))
.mapNotNull { p -> p.second?.let { Pair(p.first, it) } }
.toMap()
println(map)
This will give you a Map<String, Int>, since mapNotNull ignores anything that maps to null, and using let with the safe call operator ?. returns null if its receiver (p.second) is null.
This is basically what you stated in your question, made shorter with let.
You want to filter out null values, then you should use filter method:
val map = listOf(Pair("a", 1), Pair("b", null), Pair("c", 3), Pair("d", null))
.filter { it.second != null }
.toMap()
println(map)
A more readable solution might be using the associateBy function with the double bang expression (!!):
val map: Map<String, Int> = listOf(
Pair("a", 1),
Pair("b", null),
Pair("c", 3),
Pair("d", null)
)
.filter { it.first != null && it.second != null }
.associateBy({ it.first!! }, { it.second!! })
println(map)
Since Kotlin 1.6, there is also a stable buildMap function that can be used to write custom helper functions that are performant without sacrificing readability:
fun <T, K : Any, V : Any> Iterable<T>.associateByNotNull(
keySelector: (T) -> K?,
valueTransform: (T) -> V?,
): Map<K, V> = buildMap {
for (item in this#associateByNotNull) {
val key = keySelector(item) ?: continue
val value = valueTransform(item) ?: continue
this[key] = value
}
}
Note that writing this as a "low-level" for loop eliminates the need for the creation of intermediate collections.
Related
I'm looking for an idiomatic way of converting a list of pairs where Pair.first is a key and Pair.second is a list of values. This procedural approach works but I was hoping to find a more idiomatic way that doesn't require creating the mutable lists directly.
val pairs: Pair<String, List<Int>>
val res = mutableMapOf<String, List<Int>>()
pairs.forEach {
res.getOrPut(it.first, ::mutableListOf).addAll(it.second)
}
This code can get wrapped in an extension function like follows but it doesn't seem very generic:
fun <K, V> List<Pair<K, Collection<V>>>.toMultimap(): Map<K, List<V>> {
var res = mutableMapOf<K, MutableList<V>>()
forEach {
res.getOrPut(it.first, ::mutableListOf).addAll(it.second)
}
return res
}
Using pairs.toMap doesn't work because it overwrites map keys with a last in wins approach. groupBy works comes close, it creates keys to values in a list of lists structure.
val pairs2 = listOf(
Pair("a", listOf(1, 2, 3)),
Pair("b", listOf(6, 7)),
Pair("a", listOf(4, 5)),
Pair("b", listOf(8, 9)),
)
val res = pairs2.groupBy({ it.first }, { it.second })
println(res)
{a=[[1, 2, 3], [4, 5]], b=[[6, 7], [8, 9]]}
It is possible to then flatten the map but the downside here is that its pretty inefficient as this creates double the required hashmaps and lists per key (one for groupby and another for flatten). If there
val res = pairs2.groupBy({ it.first }, { it.second }).mapValues { it.value.flatten() }
println(res)
{a=[1, 2, 3, 4, 5], b=[6, 7, 8, 9]}
Looking to see if there are any better approaches to accomplishing this transform.
Rather than groupBy, use groupingBy, which produces a Grouping. This is an intermediate object on which you can do all kinds of fold/reduce operations. In your case:
fun <K, V> List<Pair<K, Collection<V>>>.toMultimap() =
groupingBy { it.first }
.fold(emptyList<V>()) { acc, (_, new) -> acc + new }
If you don't like the fact that + creates too many new lists, you can do something like this:
groupingBy { it.first }
.fold({ _, _ -> mutableListOf<V>() }) { _, acc, (_, new) ->
acc.addAll(new)
acc
}
val pairs: List<Pair<String, List<Int>>> = listOf(
Pair("a", listOf(1, 2, 3)),
Pair("b", listOf(6, 7)),
Pair("a", listOf(4, 5)),
Pair("b", listOf(8, 9)),
)
val result = pairs
.map { it.first }.distinct()
.associateWith { first -> pairs.filter { it.first == first }.flatMap { it.second } }
println(result) // {a=[1, 2, 3, 4, 5], b=[6, 7, 8, 9]}
Here's one way you could do it.
val pairs2 = listOf(
Pair("a", listOf(1, 2, 3)),
Pair("b", listOf(6, 7)),
Pair("a", listOf(4, 5)),
Pair("b", listOf(8, 9)),
)
val res = pairs2.flatMap { pair ->
pair.second.map { value ->
pair.first to value
}
}.groupBy({ it.first }, { it.second })
println(res) // {a=[1, 2, 3, 4, 5], b=[6, 7, 8, 9]}
Essentially, you flatten the list in each pair, preserving the original key, and then you convert the whole thing into your Map.
If you're worried about the lists taking up too much space, you could instead work with sequences, though maybe that's overkill.
val res = pairs2.asSequence().flatMap { pair ->
pair.second.asSequence().map { value ->
pair.first to value
}
}.groupBy({ it.first }, { it.second })
The core idea here is that flatMap is excellent at allowing you to add/remove elements from your original list on the fly.
I want to be able to check that collection a contains exactly all of the elements of b, but an equality based check is not sufficient; for example:
data class Person(val name: String, val age: Int)
val a = listOf(Person("Alice", 10), Person("Bob", 13))
val b = listOf(Person("Alice", 10), Person("Alice", 10))
fun main() {
println(a.containsAll(b))
}
true
Whilst this is technically true, it's not the result I want, because a only contains one Person("Alice", 10), whereas b contains two of them.
The above example should fail, whilst the below should pass.
data class Person(val name: String, val age: Int)
val a = listOf(Person("Alice", 10), Person("Alice", 10), Person("Bob", 13))
val b = listOf(Person("Alice", 10), Person("Alice", 10))
fun main() {
println(a.containsAllExact(b))
}
Is there a way to do this?
You could add an extension function for this, something like:
fun List<Person>.containsAllExact(list2: List<Person>): Boolean {
val occurences1 = this.groupingBy{ it }.eachCount()
val occurences2 = list2.groupingBy{ it }.eachCount()
return occurences2.all{
it.value <= occurences1.getOrDefault(it.key, 0)
}
}
One way I can think of is to make a mutable copy of a, and try to remove every element of b, then check if all elements of b can be removed:
println(
a.toMutableList().let { aCopy ->
b.all(aCopy::remove)
}
)
Or as an extension function:
fun <T> Iterable<T>.strictlyContainsAll(other: Iterable<T>) =
toMutableList().let { copy ->
other.all(copy::remove)
}
I think this would do it
println(a.containsAll(b) && (a.size - b.size == (a-b).size))
Edit: it doesn't work because it gives false negatives also. for example with
val a = listOf(Person("Alice", 10), Person("Alice", 10), Person("Bob", 13))
val b = listOf(Person("Alice", 10))
How can I convert a Map<String, ArrayList<Int>> to a Map<String, Int>?
My objects are as follows:
val mapOfItems = mapOf("x" to listOf(1, 2), "y" to listOf(1, 3), "z" to listOf(2, 3))
I want to get an object like this:
val mapOfSummedItems = mapOf("x" to 3, "y" to 4, "z" to 5)
I tried:
mapOfItems.map { (key, value) -> SummedItems(key, value.sum()) }
.groupBy { it.key }
data class SummedItems(val key: String, val sum: Int)
That does not give me what I want that gives me an Map<String, ArrayList<SummedItems>>
.map() on a Map generates a List. While you can make this a List<Pair> and then use associate* operators to get back into a Map, you can also just map the values directly:
mapOfItems.mapValues { it.value.sum() }
var chart_values: MutableSet<MutableMap.MutableEntry<String, Any>>? = mutableSetOf()
Printing chart_values:
[ground={},
ground_level={0=115, 1=4, 2=0, 3=37, 4=63, 5=44, 6=40, 7=9},
ground_over={0=3, 1=3, 2=3, 3=3, 4=3, 5=3, 6=3}
date_of_birth=1988-07-18T00:00Z]
I would like to remove ground={} from the chart_values
Given that chartValues is of type MutableSet<MutableMap.MutableEntry<String, Any>>? you can do the following to remove any entry with an empty map as a value:
chartValues?.removeAll { (_, value) ->
(value as? Map<*, *>)?.isEmpty() == true
}
as? is called safe cast operator and will return the casted object or null if the cast did not succeed.
Note:
You might be better off using a MutableMap<String, Any>
use val instead of var, since you want to mutate the collection an not the reference to it
BTW:
Your set can be actually a map Map<String, Any>
chart_values doesnt comply with kotlins naming convetions. Use camel case
Since the value is of type Any - which should be changes to more specific type if possible - we have to check for instance first
val chart_values: MutableSet<MutableMap.MutableEntry<String, Any>>? = mutableSetOf()
val withoutEmptyValues = chart_values.filter { (_, value) -> value is Collection<*> && value.isNotEmpty() }
EDIT
If some elements are no collections:
val withoutEmptyValues = chart_values.filter { (_, value) -> if(value is Collection<*>) value.isNotEmpty() else true }
Test
I can't create instances of MutableMap.MutableEntry, so I created a mao which does it for me internally:
val map: MutableMap<String, Any> = mutableMapOf(
"ground" to listOf<Int>(1, 2, 3),
"empty" to emptyList<Double>(),
"date" to "1988-07-18T00:00Z"
)
val withoutEmptyValues = map
.filter { (_, value) -> if (value is Collection<*>) value.isNotEmpty() else true }
assertThat(withoutEmptyValues).isNotEmpty.isEqualTo(
mutableMapOf<String, Any>(
"ground" to listOf<Int>(1, 2, 3),
"date" to "1988-07-18T00:00Z"
)
)
fun removeMapEntryIfEmpty() {
val iterator: MutableIterator<MutableMap.MutableEntry<String, Any>> =
player.chart_values?.iterator()!!
iterator.forEach {
// it: MutableMap.MutableEntry<String, Any>
if (it.value.toString() == "{}") {
iterator.remove()
}
}
}
If I have a Kotlin sequence, every invocation of take(n) restarts the sequence.
val items = generateSequence(0) {
if (it > 9) null else it + 1
}
#Test fun `take doesn't remember position`() {
assertEquals(listOf(0, 1), items.take(2).toList())
assertEquals(listOf(0, 1, 2), items.take(3).toList())
}
Is there an easy way of write say, another(n) such that
#Test fun `another does remember position`() {
assertEquals(listOf(0, 1), items.another(2).toList())
assertEquals(listOf(2, 3, 4), items.another(3).toList())
}
I suppose that I have to have something that isn't the Sequence to keep the state, so maybe what I'm actually asking for is a nice definition of fun Iterator<T>.another(count: Int): List<T>
Sequence does not remember its position, but its iterator does remember:
val iterator : Iterator<Int> = items.iterator()
Now all you need is something like take(n) but for Iterator<T>:
public fun <T> Iterator<T>.another(n: Int): List<T> {
require(n >= 0) { "Requested element count $n is less than zero." }
if (n == 0) return emptyList()
var count = 0
val list = ArrayList<T>(n)
for (item in this) {
list.add(item)
if (++count == n)
break
}
return list
}
What about this:
#Test
fun `another does remember position`() {
val items: Sequence<Int> = generateSequence(0) {
if (it > 9) null else it + 1
}
val (first, rest) = items.another(2)
assertEquals(listOf(0, 1), first.toList())
assertEquals(listOf(2, 3, 4), rest.another(3).first.toList())
}
fun <T> Sequence<T>.another(n: Int): Pair<Sequence<T>, Sequence<T>> {
return this.take(n) to this.drop(n)
}
To answer the last part of your question:
I suppose that I have to have something that isn't the Sequence to keep the state, so maybe what I'm actually asking for is a nice definition of fun Iterator.another(count: Int): List
One such implementation would be:
fun <T> Iterator<T>.another(count: Int): List<T> {
val collectingList = mutableListOf<T>()
while (hasNext() && collectingList.size < count) {
collectingList.add(next())
}
return collectingList.toList()
}
This passes your test if you use the iterator produced by the sequence:
#Test
fun `another does remember position`() {
val items = generateSequence(0) {
if (it > 9) null else it + 1
}.iterator() //Use the iterator of this sequence.
assertEquals(listOf(0, 1), items.another(2))
assertEquals(listOf(2, 3, 4), items.another(3))
}
To me what you've described is an iterator, since it's something that allows you to go over a collection or sequence etc. but also remember its last position.
NB the implementation above wasn't written to take into consideration what should happen for non-positive counts passed in, and if the count is larger than what's left to iterate over you'll be returned a list which has smaller size than n. I suppose you could consider this an exercise for yourself :-)
Sequence does not remember its position, but its iterator does remember:
val iterator : Iterator<Int> = items.iterator()
Unfortunately there is no take(n) for an iterator, so to use the one from stdlib you need to wrap iter into an Iterable:
val iterable : Iterable<Int> = items.iterator().asIterable()
fun <T> Iterator<T>.asIterable() : Iterable<T> = object : Iterable<T> {
private val iter = this#asIterable
override fun iterator() = iter
}
That makes itareble.take(n) remember its position, but unfortunately there is a of-by-one error because the standard .take(n) asks for one element too many:
public fun <T> Iterable<T>.take(n: Int): List<T> {
require(n >= 0) { "Requested element count $n is less than zero." }
if (n == 0) return emptyList()
if (this is Collection<T>) {
if (n >= size) return toList()
if (n == 1) return listOf(first())
}
var count = 0
val list = ArrayList<T>(n)
for (item in this) {
if (count++ == n)
break
list.add(item)
}
return list.optimizeReadOnlyList()
}
That can be fixed with a little tweak:
public fun <T> Iterable<T>.take2(n: Int): List<T> {
require(n >= 0) { "Requested element count $n is less than zero." }
if (n == 0) return emptyList()
if (this is Collection<T>) {
if (n >= size) return toList()
if (n == 1) return listOf(first())
}
var count = 0
val list = ArrayList<T>(n)
for (item in this) {
list.add(item)
//count++
if (++count == n)
break
}
return list
}
Now both of you tests pass:
#Test fun `take does not remember position`() {
assertEquals(listOf(0, 1), items.take2(2).toList())
assertEquals(listOf(0, 1, 2), items.take2(3).toList())
}
#Test fun `another does remember position`() {
assertEquals(listOf(0, 1), iter.take2(2).toList())
assertEquals(listOf(2, 3, 4), iter.take2(3).toList())
}
You could create a function generateStatefulSequence which creates a sequence which keeps its state by using a second sequence's iterator to provide the values.
The iterator is captured in the closure of that function.
On each iteration the seed lambda ({ i.nextOrNull() }) of the returned sequence starts off with the next value provided by the iterator.
// helper
fun <T> Iterator<T>.nextOrNull() = if(hasNext()) { next() } else null
fun <T : Any> generateStatefulSequence(seed: T?, nextFunction: (T) -> T?): Sequence<T> {
val i = generateSequence(seed) {
nextFunction(it)
}.iterator()
return generateSequence(
seedFunction = { i.nextOrNull() },
nextFunction = { i.nextOrNull() }
)
}
Usage:
val s = generateStatefulSequence(0) { if (it > 9) null else it + 1 }
println(s.take(2).toList()) // [0, 1]
println(s.take(3).toList()) // [2, 3, 4]
println(s.take(10).toList()) // [5, 6, 7, 8, 9, 10]
Try it out
Here is a nice definition of fun Iterator<T>.another(count: Int): List<T> as requested:
fun <T> Iterator<T>.another(count: Int): List<T> =
if (count > 0 && hasNext()) listOf(next()) + this.another(count - 1)
else emptyList()
As another workaround (similar to the suggestion by Willi Mentzel above) would be to create a asStateful() extension method that converts any sequence into a one that will remember the position, by wrapping it into an Iterable that always yields the same iterator.
class StatefulIterable<out T>(wrapped: Sequence<T>): Iterable<T> {
private val iterator = wrapped.iterator()
override fun iterator() = iterator
}
fun <T> Sequence<T>.asStateful(): Sequence<T> = StatefulIterable(this).asSequence()
Then you can do:
val items = generateSequence(0) {
if (it > 9) null else it + 1
}.asStateful()
#Test fun `stateful sequence does remember position`() {
assertEquals(listOf(0, 1), items.take(2).toList())
assertEquals(listOf(2, 3, 4), items.take(3).toList())
}
Try it here: https://pl.kotl.in/Yine8p6wn