Checking for multiple strings in an array

Hi,

I'm a beginner and I'm also new to this forum.

I have an array of strings, and I want to do something only if my array contains specific things. Is there a shorter/better way to write the following other than repeating && multiple times?

var array = ["red", "purple", "orange", "yellow", "pink", "green", "blue"]

if array.contains("red") && array.contains("blue") && array.contains("green") && array.contains("yellow") {
//do something
}

Hi @klausycat and welcome to the SE forums.

Here is a more advanced solution that does exactly the same as your solution (okay it creates an extra array instead of individual strings, but it computes the same value).

if ["red", "blue", "green", "yellow"].allSatisfy(array.contains) {
  print("works")
}

We can write the operation out in its full form.

let elements = ["red", "blue", "green", "yellow"]
let containsAll = elements.allSatisfy { (element: String) -> Bool in 
  return array.contains(element)
}

if containsAll {
  print("works")
}

Also note that array.contains(_:) is an O(n) operation because it will iterate the whole array over and over to check if the elements are present.

I'm no expert in things like performance costs for copying collections, but you can "potentially" optimize the speed (I leave the measurement to other people).

let set = Set(array)
if ["red", "blue", "green", "yellow"].allSatisfy(set.contains) {
  print("works")
}

In this case set.contains(_:) is an O(1) operation.

1 Like

You could use a Set instead of an Array like this:

var spectrum: Set = ["red", "purple", "orange", "yellow", "pink", "green", "blue"]

if spectrum.isSuperset(of: ["red", "blue", "green", "yellow"]) {
    // ...
}

Here, spectrum has to be a Set, but the argument to isSuperset(of:) can be any Sequence.

7 Likes

Thank you so much for the great answer and quick response! This is very helpful. I can't wait to try it on my project.

To avoid traversing the array repeatedly, you’re better off writing a single test.

I could read your answer before you removed it, if you do that you get a false result because it requires the inner contains return true once to complete the outer contains. You probably meant the same thing as I wrote above allSatisfy on inverted collections (switched the positions).

I had misread the original question as "or" instead of "and".

I think the best solution to the "and" version is actually imperative, to allow finishing early so it is not always necessary to walk the entire array even once:

var itemsOfInterest: Set = ["red", "blue", "green", "yellow"]

for item in array {
  itemsOfInterest.remove(item)
  if itemsOfInterest.isEmpty { break }
}

if itemsOfInterest.isEmpty {
  // the array contains every item from the set
}

And for completeness, here's a solution to the "or" version:

let itemsOfInterest: Set = ["red", "blue", "green", "yellow"]

let arrayIsInteresting = array.contains{ itemsOfInterest.contains($0) }

if arrayIsInteresting {
  // the array contains at least one item from the set
}

Edit:

The "and" version could also be done with subtract, though I don’t know off the top of my head if it finishes early when possible:

if itemsOfInterest.subtracting(array).isEmpty {
  // the array contains every item from the set
}

Edit 2:

It appears that subtract does not finish early in the current implementation.

2 Likes

In comparison isSuperset(of:) requires O(m) operations where m is the number of elements in your itemsOfInterest. The last solution I presented above with the set is basically the same as isSuperset(of:).

If you start with a Set, that is true. But if you start with an array and have to write Set(array) then it must walk the entire array.

Why would it walk the entire array? Isn't it copied into a new set then? The comparison operations are still O(m) but you get some copying overhead, or am I wrong?

This is the step that walks the array.

2 Likes
let requiredList = ["red", "green", "blue"]
let array        = ["red", "purple", "orange", "yellow", "pink", "green", "blue"]

let isSatified = !requiredList.map { array.contains($0) }.contains(false)

//1. Will check if each element in requiredList is contained in the array. This will result in an array of boolean values
//2. See if the result from step1 contains a false
//3. Negate the result in step2, and that will tell you if it was satisfied.

print("isSatified = \(isSatified)")

Somu, that will walk the entire array for every element in the required list, giving a runtime of m*n.

1 Like

Thanks a lot @Nevin for pointing it out.

Pardon my ignorance, would using lazy make it any better or would have no impact ?

let isSatified = !requiredList.lazy.map { array.contains($0) }.contains(false)

I think that lazy will make the map lazily evaluated, and make the .contains(false) terminate once it finds a false, without evaluating the remaining mapping transforms (I.e the inner .contains).

However, it will also make the closure escaping and probably cause heap allocations(?)

Thanks @sveinhal I thought LazySequence (struct) would be allocated in the stack.

Please note: I have a very limited understanding, so I could be completely wrong.

Is there a way to see how things would be executed using Xcode / command line ?

I might be mistaken, and Swift may be able to optimize it, but .map on a lazy sequence needs its transform closure to be escaping and stored for later execution, as the actual mapping is delayed until the sequence needs to be walked. This allocation I think will happen on the heap. It also means that any variables captured by the stack, must also be allocated on the heap, if I am not mistaken. This means that the array on which you call array.contains($0) is also stored on the heap. Only when you call the latter .contains(false) will the lazily mapped sequence actually be walked, and the closure will be executed until it returns false. Only then will the lazy sequence be deallocated, and its heap allocations with it.

But again, I might be mistaken.

2 Likes

Thanks for that explanation @sveinhal, I guess I have been using lazy loosely without understanding its consequences.

Would the following be slightly better ?

let isSatified = requiredList.first { !array.contains($0) } == nil