Andrew V. Jackson
Andrew V. Jackson

Reputation: 283

How to filter an array in Swift with optional multiple criteria?

I have several criteria to filter array with. These criteria are optionals and stored in struct, because user can select only part of them. I have an array of models. I tried to use filter method, but you have to provide non optional criteria to it. What approach should be to get rid of optionals and add that criteria to filter method?

Filter struct with filtering options

struct Filter {
  var brand: String?
  var price: Int?
  var consuption: Int?
}

Model class

class CarOffer {
  var brand: String
  var price: Int
  var color: String
  var consumption: Int
}

And here what I tried to do, but no luck because filter.price is optional and I don't know will it be or not. I understand that I have to remove an optional, but how to add a filter criteria in filter method depending on it's optionality? Or I am have choosen wrong approach?

let offers: [CarOffer] = […]

func applyFilter(filter: Filter) -> [CarOffer] {

let filteredOffers = offers.filter { $0.brand == filter.brand && $0.price <= filter.price && $0.consumption <= filter.consumption }

return filteredOffers
}

Upvotes: 3

Views: 6225

Answers (3)

Cristik
Cristik

Reputation: 32853

You could convert the filters to closures, and add an initializer that allows easy pass of filters that we do not care about:

struct Filter {
    var brand: (String) -> Bool
    var price: (Int) -> Bool
    var consuption: (Int) -> Bool

    init(brand: @escaping (String) -> Bool = { _ in return true },
         price: @escaping (Int) -> Bool = { _ in  return true },
         consuption: @escaping (Int) -> Bool = { _ in  return true }) {
        self.brand = brand
        self.price = price
        self.consuption = consuption
    }
}

This gives the best flexibility, as from this point on you can add any kind of filtering that you want. Like adding a filer based on your original structure, optionals for fields to ignore:

init(brand: String? = nil,
     price: Int?  = nil,
     consuption: Int? = nil) {
    self.brand = { brand == nil || brand == $0 }
    self.price = { price == nil || price! <= $0 }
    self.consuption = { consuption == nil || consuption! <= $0 }
}

Upvotes: 1

Alexander
Alexander

Reputation: 63321

You would have an easier time by simplifying and breaking up your code into smaller pieces. There's no reason why a function to filter an array by some conditions, also has to be responsible for figuring out if an element meets those conditions. You've mentally trapped yourself thinking that the filter predicate has be one one long chain of && conditions in a closure.

struct CarOffer {
    let brand: String
    let price: Int
    let color: String
    let consumption: Int
}

struct CarFilter {
    let brand: String?
    let price: Int?
    let consumption: Int?

    func matches(car: CarOffer) -> Bool {
        if let brand = self.brand, brand != car.brand { return false }
        if let price = self.price, price != car.price { return false }
        if let consumption = self.consumption, consumption != car.consumption { return false }
        return true
    }
}

extension Sequence where Element == CarOffer {
    func filter(carFilter: CarFilter) -> [CarOffer] {
        return self.filter(carFilter.matches)
    }
}

let filter = CarFilter(brand: nil, price: nil, consumption: nil)
let offers: [CarOffer] = [] //...
let filteredOffers = offers.filter(carFilter: filter)

Upvotes: 4

David Pasztor
David Pasztor

Reputation: 54745

You can simply use a default value instead of filters Optional values. If you use the default value of the offer, filter will simply return true in case the optional properties were nil.

func applyFilter(filter: Filter) -> [CarOffer] {

    let filteredOffers = offers.filter { $0.brand == filter.brand && $0.price <= (filter.price ?? $0.price) && $0.consumption <= (filter.consumption ?? $0.consumption) }

    return filteredOffers
}

Upvotes: 3

Related Questions