124

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).

Community
  • 1
  • 1
Senseful
  • 86,719
  • 67
  • 308
  • 465
  • 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` – Alexander Apr 03 '17 at 19:57
  • https://forums.swift.org/t/swift-enum-property-without-initializing-the-enum-case-with-an-associated-value/17539 and https://forums.swift.org/t/request-ability-to-refer-to-an-enum-case-in-abstract-without-its-associated-value/410 – pkamb Jun 25 '19 at 08:11

10 Answers10

110

Edit: As Etan points out, you can omit the (_) wildcard match to use this more cleanly:

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

Unfortunately, I don't believe that there's an easier way than your switch approach in Swift 1.2.

In Swift 2, however, you can use the new 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.

Iulian Onofrei
  • 9,188
  • 10
  • 67
  • 113
Ronald Martin
  • 4,278
  • 3
  • 27
  • 32
  • 3
    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. =/ – Jeremy Jun 21 '16 at 20:47
  • 4
    Also sucks for things like while loop conditions etc. In Swift 3 you can remove the (_) for a cleaner code. – Etan Oct 14 '16 at 19:08
  • 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 – Ronald Martin Oct 14 '16 at 19:47
  • Also: If an enum has a single case with associated values, the if-case pattern needs to be used even for those cases that do not have associated values. – Etan Oct 14 '16 at 20:24
  • 1
    Can you negate the if case? Doesn't seem possible to do `if case .Number != number` – Peter Warbo Jun 23 '17 at 08:07
  • 1
    @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. – Ronald Martin Jun 23 '17 at 18:41
  • How to use this in conditional modifiers? Like ‘number == .Number ? X : X’ ? – Björn Mar 20 '22 at 14:47
38

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
}
Qbyte
  • 12,753
  • 4
  • 41
  • 57
25

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

nambatee
  • 1,478
  • 16
  • 27
15

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
Rouger
  • 543
  • 5
  • 13
  • 1
    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. – Magoo Sep 12 '22 at 07:46
  • 1
    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 (https://www.swift.org/documentation/api-design-guidelines). Plus, it allows for easy negation as well... `if !card1.matchesCaseType(card2) { ... }` – Mark A. Donohoe Oct 11 '22 at 18:45
3

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!")
}
Mike Taverne
  • 9,156
  • 2
  • 42
  • 58
  • 5
    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. – Nathan Hosselton Jan 19 '17 at 17:14
3

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!

Display Name
  • 4,502
  • 2
  • 47
  • 63
1

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

let rank = CardRank.Ace
if case .Ace = rank { print("Snoopy") }
Edward Brey
  • 40,302
  • 20
  • 199
  • 253
1
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.

SmileBot
  • 19,393
  • 7
  • 65
  • 62
-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]
lochiwei
  • 1,240
  • 9
  • 16
-2
let cardRank = CardRank.Number(10)    
guard let case .Number(someNumber) = cardRank else {
      return
}
print(someNum)
  • While this code snippet may be the solution, [including an explanation](https://meta.stackexchange.com/questions/114762/explaining-entirely-‌​code-based-answers) 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. – dan1st Aug 20 '23 at 15:36