Jim
Jim

Reputation: 4415

Find any 2 objects in a list matching criteria in one iteration

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

Answers (6)

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

Ben Shmuel
Ben Shmuel

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

Adam Millerchip
Adam Millerchip

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:

  1. Implement it with traditional list iteration
  2. Wrap it up in a generic extension function for idiomatic Kotlin use
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

AlexT
AlexT

Reputation: 2964

I can only think of 2 possible solutions:

  1. An inefficient but simple way (2 partial iterations)
    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.

  1. Based on your own solution, only iterates once
    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

Rajan Kali
Rajan Kali

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

Ken Wolf
Ken Wolf

Reputation: 23269

You can use the filter keyword to filter any collection

    val matches = list.filter { if (it.age == 21 || it.age == 31) }

matches will be a list where all Person objects have an age of 21 or 31.

Upvotes: 0

Related Questions