Search code examples
androidkotlinfilterpredicate

Conditionally Combine Lambdas in Kotlin for Filtering with Multiple Predicates


Suppose I have the following predicates as predefined lambdas for a Villager POJO.

    val matchesSearch: (Villager, String) -> Boolean =
        { villager: Villager, query: String -> villager.name.contains(query) }

    val matchesGender: (Villager, Int) -> Boolean =
        { villager: Villager, filter: Int -> filter == villager.gender }

    val matchesPersonality: (Villager, Int) -> Boolean =
        { villager: Villager, filter: Int -> filter == villager.personality }

    val matchesSpecies: (Villager, Int) -> Boolean =
        { villager: Villager, filter: Int -> filter == villager.species }

    val matchesHobby: (Villager, Int) -> Boolean =
        { villager: Villager, filter: Int -> filter == villager.hobby }

I only want to apply the predicate IF a certain condition is met. For example, only filter by gender if a gender filter has been applied. Or I only want to match against the search query if one has been entered.

fun getVillagersThatMatchQueryAndFilters(list: List<Villager>): List<Villager> {

    val conditions = ArrayList<Predicate<Villager>>()

    if (searchQuery.isNotEmpty()) {
        // TODO: apply the matchesSearch() predicate lambda
        conditions.add(Predicate { villager -> villager.name.contains(query) })
    }
    if (genderFilter > 0) {
        // TODO: apply the matchesGender() predicate lambda
        conditions.add(Predicate { genderFilter == villager.gender })
    }
    if (personalityFilter > 0) {
        // TODO: apply the matchesPersonality() predicate lambda
        conditions.add(Predicate { personalityFilter == villager.personality })
    }
    if (speciesFilter > 0) {
        // TODO: apply the matchesSpecies() predicate lambda
        conditions.add(Predicate { speciesFilter == villager.species })
    }
    if (hobbyFilter > 0) {
        // TODO: apply the matchesHobby() predicate lambda
        conditions.add(Predicate { hobbyFilter == villager.hobby })
    }

    return list.filter {
        // TODO: match on all conditionally applied predicates
        conditions.allPredicatesCombinedWithAnd() // this doesn't actually exist
    }
}

Previously I had done list.filter{ } within each condition and applied the necessary predicate, however, since I could have up to 5 predicates being applied, it is quite terrible for performance as it iterates over the list each time .filter() is called.

Is there a way to programmatically iterate over a List<Predicate<T>> and combine predicates with .and() or && so that I may apply the filter ONCE? Or if not, how can I conditionally combine these predicates?

I would use the final Java example here that uses Predicates.stream() and Collectors but it requires Android API level 24 (higher than my current Android min).

SOLUTION BELOW - thanks to suggestions from @Laurence and @IR42

Works for < Android API 24

Create a list of the lambdas and conditionally add any predicates that you wish to match list items against. Then use Collections.Aggregates.all() method to ensure you are filtering against ALL predicates. (The equivalent of doing pred1.and(pred2).and(etc.)... for all items in the list.)

Huge performance saver if you have a large number of predicates to be filtered against as list.filter() is only called ONCE.

fun getVillagersThatMatchQueryAndFilters(list: List<Villager>): List<Villager> {

    val conditions = ArrayList<(Villager) -> Boolean>()

    if (searchQuery.isNotEmpty()) {
        conditions.add{ it.name.contains(query) }
    }
    if (genderFilter > 0) {
        conditions.add{ genderFilter == it.gender }
    }
    if (personalityFilter > 0) {
        conditions.add{ personalityFilter == it.personality }
    }
    if (speciesFilter > 0) {
        conditions.add{ speciesFilter == it.species }
    }
    if (hobbyFilter > 0) {
        conditions.add{ hobbyFilter == it.hobby }
    }

    return list.filter { candidate -> conditions.all { it(candidate) } }
}

Solution

  • There is the all extension function, and as a point of interest the any extension function as well. I think you just have to use them in your filter block on your predicates. Here is a very simple example with a silly set of predicates:

    fun main() {
        val thingsToFilter = listOf(-1,2,5)
    
        val predicates = listOf(
            { value: Int -> value > 0 },
            { value: Int -> value > 4 }
        )
    
        val filtered = thingsToFilter.filter {candidate ->
            predicates.all{ it(candidate)}
        }
    
        print(filtered) // Outputs [5]
    
    }