aaronstacy
aaronstacy

Reputation: 6428

Check if string contains optional string in Swift, but only if it's not nil

I'd like to add an optional "filter" parameter to a function that processes a list of strings. If the filter is nil, I'd like to process all the strings, otherwise I'd only like to process the ones that contain the filter. Roughly:

func process(items: [String], filter: String?) {
  for item in items {
    if filter == nil || item.contains(filter) {
      // Do something with item
    }
  }
}

The typechecker complains about passing filter into contains since it's optional, but contains takes a String. I could of course force-unwrap, but that seems ugly. The above would compile in Kotlin because it would smart-cast away the optional, but what's the idiomatic way to express this in Swift?

Thanks!

Upvotes: 1

Views: 933

Answers (2)

matt
matt

Reputation: 534893

The key here is (and always was going to be) the nil-coalescing operator. This lets you unwrap an Optional safely if it is not nil, but if it is nil, you substitute another value.

The issue with the question, however, is that it poses itself in an unSwifty way. You would never write a filtering function that takes an Optional String. You would write a filtering function that takes an Optional filtering function! Instead of limiting the caller to contains and a String, you want to allow any type of array and any filtering function. That is the Swifty way to write this function.

And while we are at it, the function should not predetermine what is done with the members of the array either. That should be another function that the caller passes in!

Thus we end up with this far more general and much Swiftier statement of the goal:

func process<T>(_ arr: [T],
                     filtering filterPred: ((T)->Bool)? = nil,
                     processing processPred: ((T)->Void)) {
    // ...
}

Okay! So let's write this function. The processing part is easy; this is the predicate to a forEach call. Like this:

func process<T>(_ arr: [T],
                     filtering filterPred: ((T)->Bool)? = nil,
                     processing processPred: ((T)->Void)) {
    arr.forEach(processPred)
}

Very good! But we have neglected to deal with the filtering function. Let's take care of that. Clearly, this is where the rubber meets the road. And just as clearly, it is evident that this is a function to be passed into filter. But we can only do that by unwrapping if it is not nil. What if it is nil? The OP has specified that in that case we should let all element through the filter.

So consider a predicate function that is to be passed into filter. What is the function that lets everything through? It is:

{ _ in true }

So that is what to put after the nil-coalescing operator! Therefore, this is the full answer:

func process<T>(_ arr: [T],
                     filtering filterPred: ((T)->Bool)? = nil,
                     processing processPred: ((T)->Void)) {
    arr.filter(filterPred ?? { _ in true }).forEach(processPred)
}

And here are two tests:

    let arr = [1,2,3,4]
    self.process(arr) { print($0) }
    self.process(arr) { $0 % 2 == 0 } processing: { print($0) }

Upvotes: 1

vacawama
vacawama

Reputation: 154513

There is a map on Optional that executes the provided closure only if the optional is not nil. You can use map along with the nil coalescing operator ?? to provide the default value of true if the map returns nil because the filter is nil:

func process(items: [String], filter: String?) {
  for item in items {
    if filter.map(item.contains) ?? true {
      // process item
    }
  }
}

Upvotes: 6

Related Questions