1

I have this enum:

enum Foo {
    case a(x: Int)
    case b(x: Int)
    case c
    case d
}

and foo

let foo = Foo.a(x: 10)

I want to check if foo is either a, b or c, regardless of what x is.

With a switch statement, I could do:

switch foo {
case .a, .b, .c:
    ...
case .d:
    break
}

but it's a bit long winded.

I thought I could do the same with if case:

if case .a, .b, .c = foo { ... }

This produced a compiler error.

I then found this question, and tried this:

if [Foo.a, Foo.b, Foo.c].contains(foo) { ... }

The compiler thought the array was of type [Any], so this doesn't work either...

What can I do except extracting it as a method and calling that method? Is there something new in Swift 4.2 that solves this?

Sweeper
  • 213,210
  • 22
  • 193
  • 313

3 Answers3

1

Swift doesn't support that because Foo instances need pattern matching, as they are not Equatable. And the only separator that allows multiple pattern matches is ,, and that operator corresponds to an and operation, you can't have or's.

One ugly (and I'd say incorrect, or misleading) approach would be to add conformance to Equatable and ignore the associated values:

enum Foo: Equatable {
    case a(x: Int)
    case b(x: Int)
    case c
    case d

    static func ==(_ lhs: Foo, _ rhs: Foo) -> Bool {
        switch (lhs, rhs) {
        case (.a, .a): return true
        case (.b, .b): return true
        case (.c, .c): return true
        case (.d, .d): return true
        default: return false
        }
    }
}

You can then do something like this:

if [Foo.a(x: 0), Foo.b(x: 0), Foo.c].contains(foo) { ... }

Another approach would be to add an index property, and use that when testing:

enum Foo {
    case a(x: Int)
    case b(x: Int)
    case c
    case d

    var index: Int {
        switch self {
        case .a: return 0
        case .b: return 1
        case .c: return 2
        case .d: return 3
        }
    }
}

And use it along the lines of

if [Foo.a(x: 0), Foo.b(x: 0), Foo.c].map({ $0.index }).contains(foo.index) { ... }

Both solutions are more verbose than the simple switch, and they would be feasible only if you need to use them many times.

Alternatively, you could extend Array with something like this:

extension Array where Element == Foo {
    func matchesCase(_ foo: Foo) -> Bool {
        return contains {
            switch ($0, foo) {
            case (.a, .a): return true
            case (.b, .b): return true
            case (.c, .c): return true
            case (.d, .d): return true
            default: return false
            }
        }
    }
}

, and use it like this:

if [Foo.a(x: 0), Foo.b(x: 0), Foo.c].matchesCase(foo) { ... }

And a fourth solution :). Adding a sameCase function:

enum Foo {
    case a(x: Int)
    case b(x: Int)
    case c
    case d

    func sameCase(_ foo: Foo) -> Bool {
        switch self {
        // a little bit more verbose, but don't risk missing new cases
        case .a: if case .a = foo { return true } else { return false }
        case .b: if case .b = foo { return true } else { return false }
        case .c: if case .c = foo { return true } else { return false }
        case .d: if case .d = foo { return true } else { return false }
        }
    }
}

Usage:

if [Foo.a(x: 0), Foo.b(x: 0), Foo.c].contains(where: foo.sameCase) { ... }
// or
if foo.sameCase(.a(x: 0)) || foo.sameCase(.b(x: 0)) || foo.sameCase(.c) { ... }
Cristik
  • 30,989
  • 25
  • 91
  • 127
0

If you plan on repeating this test multiple times in multiple places, then duplicating the long winded version over and over would indeed be annoying; however, you can simply encapsulate this bit of code in an extension.

extension Foo {
  var isABorC: Bool {
    switch self {
    case .a, .b, .c:
      return true
    default:
      return false
    }
  }
}

So now your test becomes something like this:

if foo.isABorC { ... }

Or you could simply make it part of the enum declaration:

enum Foo {
  case a(x: Int)
  case b(x: Int)
  case c
  case d

  var isABorC: Bool {
    switch self {
    case .a, .b, .c:
      return true
    case .d:
      return false
    }
  }
}

There is an example is the swift(4.2) documentation using a nested enum to implement ranks for a deck of cards where one might add an isFaceCard var.

The bottom line is: you need not endlessly duplicate this bit of text ad nauseam. You can hide it until you identify a more elegant solution.

John Morris
  • 396
  • 2
  • 10
-3

Unfortunately, there is no other way.

This is because Foo.a is of type (Int) -> Foo. The reason you could not use the array.contains is because a closure and Foo are different types so the compiler assumes you wanted an array of Any.

To see this phenomenon yourself, try out this code:

print(type(of: Foo.a))

You will get (Int) -> Foo.