Reputation: 2227
I am working on a searching/filtering functionality where a user should be able to filter a list of events to fit a pattern which he will make in runtime.
I made a function filter which is looping over all the constraints the user has set, and then filtering the result.
My problem is that I am copying the list many times, and I was wondering if there is a way where I can do this kind of complex filtering in a more declarative (kotlin-) way without side effects.
fun filter(query: Filter, eventsIn: List<Event>): List<Event> {
var events = eventsIn
query.filters.forEach { filter ->
if (filter.key is EventFiltersListStuff){
events = when (filter.key as EventFiltersListStuff) {
PLACE -> events.filter { event -> (filter.value as List<*>).contains(event.location.place) }
AREA -> events.filter { event -> (filter.value as List<*>).contains(event.location.area) }
CATEGORY -> events.filter { event -> (filter.value as List<*>).any{it in event.category} }
GENRE -> events.filter { event -> (filter.value as List<*>).contains(event.genre) }
}
} else {
events = when (filter.key as EventFilters) {
TITLE -> events.filter { event -> event.title.contains(filter.value as String, true) }
PRICELT -> events.filter { event -> event.price <= filter.value as Int }
PRICEGT -> events.filter { event -> event.price >= filter.value as Int }
TIMELT -> events.filter { event -> event.time <= filter.value as Int }
TIMEGT -> events.filter { event -> event.time >= filter.value as Int }
}
}
}
return events
}
The model looks like this
data class Event(
val title: String,
val genre: String,
val image: String,
val link: String,
val category: List<String>,
val price: Int,
val text: String,
val tickets: String,
var time: Long,
val location: Location
)
I have two enums one is for inclusive filtering, where a user can filter a list based on multiple instances of the attribute in focus.
The other one is non inclusive, and will just remove all entities which is not matching the query.
enum class EventFiltersListStuff(val str: String, ) : FilterType {
PLACE("place"),
AREA("area"),
CATEGORY("category"),
GENRE("genre");
override fun str(): String = str
}
enum class EventFilters(val str: String, ) : FilterType {
PRICELT("priceLT"),
PRICEGT("priceGT"),
TIMELT("timestampLT"),
TIMEGT("timestampGT"),
TITLE("title");
override fun str(): String = str
}
The next code block is less relevant, but I'll include is for transparency, Because it is used in the function which is the core of my question.
interface FilterType {
fun str(): String
}
class Filter private constructor(val filters: Map<FilterType, Any>) {
class Builder {
private var filters: MutableMap<FilterType, Any> = mutableMapOf()
fun filters(key: FilterType, value: Any) = apply {
this.filters[key] = when (this.filters[key]) {
is List<*> -> (this.filters[key] as List<*>) + listOf(value)
is Comparable<*> -> listOf(this.filters[key], value)
else -> value
}
}
fun build(): Filter {
return Filter(filters)
}
}
}
I am also using the Filter
to generate a filter in a GraphQL query, this is the reason I am having strings in the enums.
Upvotes: 0
Views: 911
Reputation: 1151
Simple and efficient solution would be just to filter each event with conjunction of all filters.
typealias SingleFilter = Map.Entry<FilterType, Any> // you may want to remodel it as a sealed class, more on that later
fun SingleFilter.isMatching(event: Event): Boolean = ...
fun List<Event>.applyFilters(filters: List<SingleFilter>) = filter { event -> filters.all { it.isMatching(event) }
Your model could be greatly improved by remodeling your filters as a sealed class, replacing instance checks and enums with polymorphism. This will have a huge benefit of being type-safe. You can create a hierarchy with two groups for your two filtering cases. You can define mapping between filter names and classes in different part of your code, decoupling serialization from filter logic.
Upvotes: 2