Reputation: 4415
I have a list of custom objects. I would like to retrieve any 2 that meet 2 different conditions.
E.g.
Assume
class Person {
val age: Int
//etc
}
And I have a List<Person>
. I would like to get 2 Person
that have age == 21
or age == 31
I know I can do a loop for that and break
using some flag once I find both, but I was wondering if there was some more Kotlin idiomatic way for this instead of:
var p1: Person?
var p2: Person?
list.forEach{
if(it.age == 21) {
p1 = it
}
else if(it.age == 31) {
p2 = it
}
if(p1 != null && p2 != null) break
}
Upvotes: 3
Views: 1800
Reputation: 7882
Your solution is close to optimal. But it doesn't take first objects meeting criteria. For listOf(Person(id=1, age=21), Person(id=2, age=21), Person(id=3, age=31))
it will return p1 = Person(id=2, age=21)
.
To fix this you need to have additional comparsions with null
in your conditional expressions (after implementing this, your second if
-statement could be merged into branches of the first one to avoid repeating checks). Also, you can't use break
inside forEach
- it should be substituted with simple loop.
All together:
var p1: Person? = null
var p2: Person? = null
for (it in list) {
if (p1 == null && it.age == 21) {
p1 = it
if (p2 != null) break
} else if (p2 == null && it.age == 31) {
p2 = it
if (p1 != null) break
}
}
To generalize this you may create extension function:
fun <T> Iterable<T>.takeFirstTwo(predicate1: (T) -> Boolean, predicate2: (T) -> Boolean): Pair<T?, T?> {
var p1: T? = null
var p2: T? = null
for (it in this) {
if (p1 == null && predicate1(it)) {
p1 = it
if (p2 != null) break
} else if (p2 == null && predicate2(it)) {
p2 = it
if (p1 != null) break
}
}
return p1 to p2
}
//Usage:
val is21 = { it: Person -> it.age == 21}
val is31 = { it: Person -> it.age == 31}
val (p1, p2) = list.takeFirstTwo(is21, is31)
Upvotes: 1
Reputation: 1989
As a lot of people mentioned that find
will probably be the best solution for this kind of problem but we will have to use it twice, once for 21
and other time for 31
.
even though find
will return a single object: <T> Iterable<T>.find(predicate: (T) -> Boolean): T?
it doesn't mean we can't find another one in the same iteration.
for example:
var ageParam = -1
var p1: Person? = null
var p2: Person? = list.find { person ->
p1?.let {
//p1 - the first person found, now we will iterate until age == ageParam(21/31)
person.age == ageParam
} ?: run {
//p1 - the first person hasn't found yet.
if (person.age == 21) {
p1 = person // saving person
ageParam = 31 // setting next condition to test
} else if (person.age == 31) {
p1 = person
ageParam = 21
}
false // it's false in order to keep iterate after P1 found
}
}
Upvotes: 1
Reputation: 23091
No, you are trying to do quite a specific operation, so I don't think there is a built-in function and you will need to implement it yourself. Since you want to only iterate the list once, I think it would be best to:
data class Person(val age: Int)
fun <T> Iterable<T>.findFirstTwo(
predicateA: (T) -> Boolean,
predicateB: (T) -> Boolean
): Pair<T, T> {
var first: T? = null
var second: T? = null
val iterator = iterator()
var remainingPredicate: (T) -> Boolean = { false }
while (first == null && iterator.hasNext()) {
val item = iterator.next()
when {
predicateA(item) -> {
first = item
remainingPredicate = predicateB
}
predicateB(item) -> {
first = item
remainingPredicate = predicateA
}
}
}
while (second == null && iterator.hasNext()) {
val item = iterator.next()
if (remainingPredicate(item)) {
second = item
}
}
require(first != null && second != null) { "Input does not satisfy predicates" }
return Pair(first, second)
}
fun main() {
val people = (1..20).map { Person(it) }
val (seven, twelve) = people.findFirstTwo({ it.age == 12 }, { it.age == 7 })
val (one, nineteen) = people.findFirstTwo({ it.age == 1 }, { it.age == 19 })
val error = try {
people.findFirstTwo({ it.age == 100 }, { it.age == 3 })
} catch (e: Exception) {
e.message
}
println(one)
println(seven)
println(twelve)
println(nineteen)
print(error)
}
Output:
Person(age=1)
Person(age=7)
Person(age=12)
Person(age=19)
Input does not satisfy predicates
Upvotes: 1
Reputation: 2964
I can only think of 2 possible solutions:
val list = listOf(Person(21), Person(23), Person(31), Person(20))
var p1: Person? = list.find{ it.age == 21 }
var p2: Person? = list.find{ it.age == 31 }
This will iterate twice, but will stop the iteration as soon as a result is found.
val list = listOf(Person(21), Person(23), Person(31), Person(20))
var p1: Person? = null
var p2: Person? = null
//forEach is a method, not an actual loop, you can't break out of it, this is a bypass (you can also enclose it into a fun and return from there to act as a break)
run loop@{
list.forEach {
when {
p1 == null && it.age == 21 -> p1 = it
p2 == null && it.age == 31 -> p2 = it
}
if (p1 != null && p2 != null) return@loop
}
}
The p1 == null
check will make is so that we don't reasign the same value multiple times, thus getting the first result. It is better even if we aren't necessarily looking for the first result, since it won't make multiple assignments.
Ps: both solutions will leave you with nullable p1
and p2
, you must remember to deal with that accordingly afterwards.
Upvotes: 1
Reputation: 12953
I do not think there is such a method in List
, the closest statement would be get first match using list.first{it.age == 21 || it.age == 31 }
, which will get the first item matching given predicate then breaks the loop, may be you can write your own extension to filter first n
numbers
fun <T> Iterable<T>.firstN(n: Int, predicate: (T) -> Boolean): List<T> {
val output = ArrayList<T>()
var count = 0
for (element in this){
if(count == n) break
if (predicate(element)){
count++
output.add(element)
}
}
return output
}
You can do below to get first two elements
with(list.firstN(2){ it.age == 21 || it.age == 31}){
if(size == 2){
val (p1, p2) = this
}
}
Upvotes: 0