A91G
A91G

Reputation: 80

Filter an ArrayList using multiple object fields/multiple conditions

So I have an interesting problem with filtering collections, I have a data class as follows:

data class Route(
    val id: Int,
    val name: String,
    val weeklyPrice: Double?,
    val monthlyPrice: Double?,
    val seasonalPrice: Double?,
)

I'm displaying an ArrayList in the UI and the user has the ability to filter the list based on whether the route has "weekly/monthly/seasonal" prices, bear in mind I'm using checkboxes not radio buttons so the user can filter using multiple options. ie. only weekly, or both weekly and monthly and so on.

My issue is that using the filter function and a predicate I can only filter based on one attribute at a time like so:

routes.filter {
    it.weeklyPrice != null
}

But sometimes I'd actually filter out values I wanted to keep based on another condition, for example a user doesn't want routes with weekly prices, so I filter those out but he wants ones with monthly ones but some of them were already filtered out based on the fact that they had weekly prices as well.

Any help would be appreciated, I'm sure it can be done I just haven't figured out how, maybe the problem is in how the data is represented in the first place? I'm not sure.

Much thanks in advance.

Upvotes: 4

Views: 6911

Answers (3)

A91G
A91G

Reputation: 80

Ok, here's what I ended up doing.. which I did before seeing @kcoppock's answer above.

I used a MutableSet to hold all routes that I actively want to show to the user and used the following code to listen for all CheckedChanged events for each box:

view.cb_weekly.setOnCheckedChangeListener { _, isChecked ->
    weeklyIncluded = isChecked
    if (weeklyIncluded) {
        activeRoutes.addAll(routes.filter { route ->
            route.weeklyPrice != null
        })
    } else {
        activeRoutes.removeAll(routes.filter { route ->
            route.weeklyPrice != null && route.monthlyPrice == null && route.seasonalPrice == null
        })
        if (!monthlyIncluded && seasonalIncluded) {
            activeRoutes.removeAll(routes.filter { route ->
                (route.weeklyPrice != null || route.monthlyPrice != null) && route.seasonalPrice == null
            })
        } else if (!seasonalIncluded && monthlyIncluded) {
            activeRoutes.removeAll(routes.filter { route ->
                (route.weeklyPrice != null || route.seasonalPrice != null) && route.monthlyPrice == null
            })
        } else if (!seasonalIncluded && !monthlyIncluded) {
            activeRoutes.clear()
        }
    }
    drawRoutes(activeRoutes)
}

Bear in mind, the answer I marked as correct (not this one) is probably the better way to do this.

Upvotes: 0

Kevin Coppock
Kevin Coppock

Reputation: 134664

One approach would be to generate a list of filters, e.g. List<(Route) -> Boolean>, and then just invalidate your view whenever the list of filters change.

So for example you have something like:

// Use your own logic based on user selection here
private fun generateFilters() = listOf<(Route) -> Boolean>(
    { it.id != 0 },
    { it.weeklyPrice != null }
)

Then, your view is generated based on this set of filter, i.e.:

private fun filterRoutes(routes: List<Route>, filters: List<(Route) -> Boolean>) =
    routes.filter { route -> filters.all { filter -> filter(route) } 

You could even add an extension function on List<T> to provide a filterAll functionality, something like:

private fun <T> List<T>.filterAll(filters: List<(T) -> Boolean>) =
    filter { item -> filters.all { filter -> filter(item) } }

Then your view logic just becomes:

routers.filterAll(generateFilters())

Upvotes: 6

Sergio
Sergio

Reputation: 30645

If you want to keep values, cache all routes and then filter them:

val allRoutes = ... // somewhere initialized
val filtered = allRoutes.filter {
    it.monthlyPrice != null
}
// use filtered routes, but keep reference to allRoutes

Upvotes: 0

Related Questions