139

After reading How to test equality of Swift enums with associated values, I implemented the following enum:

enum CardRank {
    case Number(Int)
    case Jack
    case Queen
    case King
    case Ace
}

func ==(a: CardRank, b: CardRank) -> Bool {
    switch (a, b) {
    case (.Number(let a), .Number(let b))   where a == b: return true
    case (.Jack, .Jack): return true
    case (.Queen, .Queen): return true
    case (.King, .King): return true
    case (.Ace, .Ace): return true
    default: return false
    }
}

The following code works:

let card: CardRank = CardRank.Jack
if card == CardRank.Jack {
    print("You played a jack!")
} else if card == CardRank.Number(2) {
    print("A two cannot be played at this time.")
}

However, this doesn't compile:

let number = CardRank.Number(5)
if number == CardRank.Number {
    print("You must play a face card!")
}

... and it gives the following error message:

Binary operator '==' cannot be applied to operands of type 'CardRank' and '(Int) -> CardRank'

I'm assuming this is because it's expecting a full type and CardRank.Number does not specify an entire type, whereas CardRank.Number(2) did. However, in this case, I want it to match any number; not just a specific one.

Obviously I can use a switch statement, but the whole point of implementing the == operator was to avoid this verbose solution:

switch number {
case .Number:
    print("You must play a face card!")
default:
    break
}

Is there any way to compare an enum with associated values while ignoring its associated value?

Note: I realize that I could change the case in the == method to case (.Number, .Number): return true, but, although it would return true correctly, my comparison would still look like its being compared to a specific number (number == CardRank.Number(2); where 2 is a dummy value) rather than any number (number == CardRank.Number).

3
  • 2
    You can reduce the Jack, Queen, King, Ace cases in the == operator implementation to just: case (let x, let y) where x == y: return true Commented Apr 3, 2017 at 19:57
  • forums.swift.org/t/… and forums.swift.org/t/… Commented Jun 25, 2019 at 8:11
  • Swift Evolution Pitch: is case expressions "It's often useful to check whether or not an enum matches a specific case. This is trivial for simple enums without associated values, but is not well-supported today for enum cases with associated values." Commented Apr 19, 2024 at 19:28

11 Answers 11

128

In Swift 2+ you can use the if-case pattern match:

let number = CardRank.Number(5)
if case .Number = number {
    // Is a number
} else {
    // Something else
}

If you're looking to avoid verbosity, you might consider adding an isNumber computed property to your enum that implements your switch statement.

Sign up to request clarification or add additional context in comments.

7 Comments

This is great for control flow, but it kinda sucks when you just want a simple expression, eg: assert(number == .Number). I can only hope this is improved in later versions of Swift. =/
Also sucks for things like while loop conditions etc. In Swift 3 you can remove the (_) for a cleaner code.
Thanks @Etan, I've added that to the answer. You can actually omit the wildcard in Swift 2 too. This answer was written before the language feature was released, so I didn't know that yet! :-D
Can you negate the if case? Doesn't seem possible to do if case .Number != number
@PeterWarbo, you cannot do negation with this pattern-matching syntax. You'll have to fall back to a default case in a switch block for now.
|
40

Unfortunately in Swift 1.x there isn't another way so you have to use switch which isn't as elegant as Swift 2's version where you can use if case:

if case .Number = number {
    //ignore the value
}
if case .Number(let x) = number {
    //without ignoring
}

2 Comments

Sadly, this only works in if statements, not as an expression.
@Raphael in Swift 5.9 we get if and switch expressions so I think this might work as an expression.
32

In Swift 4.2 Equatable will be synthesized if all your associated values conform to Equatable. All you need to do is add Equatable.

enum CardRank: Equatable {
    case Number(Int)
    case Jack
    case Queen
    case King
    case Ace
}

https://developer.apple.com/documentation/swift/equatable?changes=_3

1 Comment

Though this will compare associated values.
19

What I usually do to compare if two enum cases "match" no matter their associated value is:

I have a protocol Matchable:

protocol Matchable {
  static func ~= (lhs: Self, rhs: Self) -> Bool
}

Then I make enums conform to it:

extension CardRank: Matchable {
  static func ~= (lhs: Self, rhs: Self) -> Bool {
    switch (lhs, rhs) {
      case
        (.number, .number),
        (.jack, .jack),
        (.queen, .queen),
        (.king, .king),
        (.ace, .ace):
        return true
        
      default:
        return false
    }
  }
}

let card1: CardRank = .number(1)
let card2: CardRank = .number(2)
let card3: CardRank = .jack

print(card1 ~= card2) // true
print(card1 ~= card3) // false

3 Comments

I think this is a great answer, because it shows its doesn't behave the same as people might expect == to... You also should make a !~= and make it return the opposite.
I like this, although I wouldn't use a custom operator. Instead I would create a member function called matchesCaseType so the above would read card1.matchesCaseType(card2) which IMO is much clearer at the call site, a tenet of the Swift API Design Guidelines (swift.org/documentation/api-design-guidelines). Plus, it allows for easy negation as well... if !card1.matchesCaseType(card2) { ... }
This is great for unit tests when we don't care what the associated value is.
6

I didn't want to conform Equatable (it didn't help me either) and I wanted to filter for other cases than a specific one, so instead of simply writing card != .Number I had to write the following. (I adjusted my code to this question.)

enum CardRank {
    ...
    var isNumber: Bool {
       if case .Number = self { return true }
       return false
    }
}

So I can write not a number in a complex condition:

if something && !card.isNumber { ... }

I wish I could just write card != .Number, but the compiler was always complaining with Type of expression is ambiguous without more context. Maybe in an upcoming swift version!

Comments

5

Here's a simpler approach:

enum CardRank {
    case Two
    case Three
    case Four
    case Five
    case Six
    case Seven
    case Eight
    case Nine
    case Ten
    case Jack
    case Queen
    case King
    case Ace

    var isFaceCard: Bool {
        return (self == Jack) || (self == Queen) || (self == King)
    }
}

There's no need to overload the == operator, and checking for card type does not require confusing syntax:

let card = CardRank.Jack

if card == CardRank.Jack {
    print("You played a jack")
} else if !card.isFaceCard {
    print("You must play a face card!")
}

1 Comment

Though it doesn't answer the overarching question, IMO this is a more elegant solution (enumerated Numbers aside) in scenarios similar to OP's and shouldn't be downvoted.
3
extension CardRank {
    func isSameCaseAs(_ other: CardRank) -> Bool {
        switch (self, other) {
        case (.Number, .Number),
            (.Jack, .Jack),
            (.Queen, .Queen),
            (.King, .King),
            (.Ace, .Ace):
            return true
        default:
            return false
        }
    }
}

let number = CardRank.Number(1)
let otherNumber = CardRank.Number(2)
number.isSameCaseAs(otherNumber) // true

Just create an extension and ignore the associated types.

Comments

2

You don't need func == or Equatable. Just use an enumeration case pattern.

let rank = CardRank.Ace
if case .Ace = rank { print("Snoopy") }

Comments

-1

From Swift 5.3, you can use the Comparable Enums feature:

enum CardRank: Comparable {
    case Number(Int)
    case Jack
    case Queen
    case King
    case Ace
}

let cards: [CardRank] = [
    .Queen, .Number(8), .Ace, .Number(3), .King
]

print(cards.sorted())
// [.Number(3), .Number(8), .Queen, .King, .Ace]

1 Comment

Wouldn't this compare the associated values as well, which the question has asked to ignore?
-1

Building on the if-case pattern, if you need to pluck out the associated object from the enum…

enum Fruit {
  case apple (AppleStruct)
  case banana (BananaStruct)
}

struct AppleStruct {
  func commitOriginalSin()
}

struct BananaStruct {
  func slipAndFall()
}

if case .banana(let fruit) = someFruit {
  fruit.slipAndFall()
}

… and in SwiftUI…

if case .banana(let fruit) = someFruit {
  BananaView(fruit)
}
if case .apple(let fruit) = someFruit {
  AppleView(fruit)
}

Comments

-2
let cardRank = CardRank.Number(10)    
guard let case .Number(someNumber) = cardRank else {
      return
}
print(someNum)

2 Comments

While this code snippet may be the solution, including an explanation really helps to improve the quality of your post. Remember that you are answering the question for readers in the future, and those people might not know the reasons for your code suggestion.
As mentioned in my previous comment, it is recommended to also explain the code instead of only including the code for the answer to be more useful for future readers. Not just provide the code for solving the issue but also explain why and how it solves it. Also, I want to note that people can pretty much bote however they want. People cannot really be held accountable for their votes. See also Should 'drive by' downvoting be more effectively caught?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.