Swift Enumeration Order and Comparison

Swift Enumeration order and comparison

So long as you give your enum an underlying type, it’ll conform to the protocol RawRepresentable.

This means you can write a generic comparison operator for any type that is raw representable, and has a raw type that is comparable, like so:

func <<T: RawRepresentable where T.RawValue: Comparable>(a: T, b: T) -> Bool {
return a.rawValue < b.rawValue
}

which will mean your enum will automatically have a < operator:

enum E: Int {  // this would work with Double and String also
// btw, no need to give a seed value of 0,
// that happens automatically for Ints
case A, B, C, D, E
}

E.A < E.C // returns true

The only bit of boilerplate you’ll still have to do is tag your enum as Comparable in case you want to use it with generic algorithms that require that:

extension E: Comparable { }
// (no need for anything else - requirements are already fulfilled)

let a: [E] = [.C, .E, .A]
let b = sorted(a)
// b will now be [.A, .C, .E]

Making it conform to Comparable will also give it <=, >, and >= operators automatically (supplied by the standard library).

How to do an if-else comparison on enums with arguments

The trick is to not actually check with == but rather use the case keyword in conjunction with a single = in your if statement. This is a little counter intuitive in the beginning but just like if let, you get used to it pretty fast:

enum Normal {
case one
case two, three
}

enum NormalRaw: Int {
case one = 1
case two, three
}

enum NormalArg {
case one(Int)
case two, three
}

let normalOne = Normal.one
let normalRawOne = NormalRaw.one
let normalArgOne = NormalArg.one(1)

if case .one = normalOne {
print("A normal one") //prints "A normal one"
}

if case .one = normalRawOne {
print("A normal \(normalRawOne.rawValue)") //prints "A normal 1"
}

if case .one(let value) = normalArgOne {
print("A normal \(value)") //prints "A normal 1"
}

The point is that in Swift you only get equation of enums for free if your enum uses a raw type or if you have no associated values (try it out, you can't have both at the same time). Swift however does not know how to compare cases with associated values - I mean how could it? Let's look at this example:

Normal.one == .one //true
Normal.one == .two //false

NormalRaw.one == .one //true
NormalRaw.one == .two //false

NormalArg.one(1) == .one(1) //Well...?
NormalArg.one(2) == .one(1) //Well...?
NormalArg.one(1) == .two //Well...?

Maybe this makes it clearer why this cannot work out of the box:

class Special {
var name: String?
var special: Special?
}

enum SpecialEnum {
case one(Special)
case two
}

var special1 = Special()
special1.name = "Hello"

var special2 = Special()
special2.name = "World"
special2.special = special1

SpecialEnum.one(special1) == SpecialEnum.one(special2) //Well...?

So if you want enums with associated values, you'll have to implement Equatable protocol in your enum by yourself:

enum NormalArg: Equatable {
case one(Int)
case two

static func ==(lhs: NormalArg, rhs: NormalArg) -> Bool {
switch (lhs, rhs) {
case (let .one(a1), let .one(a2)):
return a1 == a2
case (.two,.two):
return true
default:
return false
}
}
}

Extend Int enum with comparison operators

It was easier than I thought. So, basically you can use Self instead of creating an additional protocol.

enum Difficulty: Int {
case Easy = 0
case Normal
case Hard
}

extension RawRepresentable where RawValue: Comparable {
static func <(lhs: Self, rhs: Self) -> Bool {
return lhs.rawValue < rhs.rawValue
}

static func >(lhs: Self, rhs: Self) -> Bool {
return lhs.rawValue > rhs.rawValue
}

static func <=(lhs: Self, rhs: Self) -> Bool {
return lhs.rawValue <= rhs.rawValue
}

static func >=(lhs: Self, rhs: Self) -> Bool {
return lhs.rawValue >= rhs.rawValue
}
}

let easy = Difficulty.Easy
print(easy > .Hard) // false

How to test equality of Swift enums with associated values

Swift 4.1+

As @jedwidz has helpfully pointed out, from Swift 4.1 (due to SE-0185, Swift also supports synthesizing Equatable and Hashable for enums with associated values.

So if you're on Swift 4.1 or newer, the following will automatically synthesize the necessary methods such that XCTAssert(t1 == t2) works. The key is to add the Equatable protocol to your enum.

enum SimpleToken: Equatable {
case Name(String)
case Number(Int)
}
let t1 = SimpleToken.Number(123)
let t2 = SimpleToken.Number(123)

Before Swift 4.1

As others have noted, Swift doesn't synthesize the necessary equality operators automatically. Let me propose a cleaner (IMHO) implementation, though:

enum SimpleToken: Equatable {
case Name(String)
case Number(Int)
}

public func ==(lhs: SimpleToken, rhs: SimpleToken) -> Bool {
switch (lhs, rhs) {
case let (.Name(a), .Name(b)),
let (.Number(a), .Number(b)):
return a == b
default:
return false
}
}

It's far from ideal — there's a lot of repetition — but at least you don't need to do nested switches with if-statements inside.

iOS Swift - Sort array by enum pattern

Here is one way to approach it. First use map to associate a sorting Int index with each item. Use a dictionary to keep track of the last index associated with each Kind and increment it by the number of different kinds. This will give a unique sorting index to every item in your array with items being sorted into the desired patten due to the increments added to repeated Kinds.

enum Kind: Int, CaseIterable {
case movie, tv, trailer, genre, article
}

struct Item: CustomStringConvertible {
var description: String { "\(name): \(kind)" }

let id: Int
let name: String
let kind: Kind
}

let items: [Item] = [
.init(id: 1, name: "D", kind: .tv),
.init(id: 2, name: "B", kind: .movie),
.init(id: 3, name: "F", kind: .trailer),
.init(id: 4, name: "H", kind: .genre),
.init(id: 5, name: "J", kind: .article),
.init(id: 6, name: "C", kind: .tv),
.init(id: 7, name: "A", kind: .movie),
.init(id: 8, name: "E", kind: .trailer),
.init(id: 9, name: "G", kind: .genre),
.init(id: 10, name: "I", kind: .article)]

// Dictionary used to generate a unique sorting index for each kind
var dict: [Kind: Int] = [:]

typealias IndexedItem = (index: Int, element: Item)

// Assign a sorting index to each item. Repeated Kinds will be incremented by
// allCases.count so that they sort into the next group
let items2: [IndexedItem] = items.map { item in
dict[item.kind, default: item.kind.rawValue] += Kind.allCases.count
return (dict[item.kind]!, item)
}

let result = items2.sorted { $0.index < $1.index }.map(\.element)
print(result)

Output

[B: movie, D: tv, F: trailer, H: genre, J: article, A: movie, C: tv, E: trailer, G: genre, I: article]


Radix Sort - A faster sort

Since all of the indices are unique, we can create the result array with a radix sort:

// Assign a sorting index to each item.  Repeated Kinds will be incremented by
// allCases.count so that they sort into the next group
let cases = Kind.allCases.count
let items2: [IndexedItem] = items.map { item in
dict[item.kind, default: item.kind.rawValue - cases] += cases
return (dict[item.kind]!, item)
}

// Use a radix sort to order the items
let maxIndex = dict.values.max() ?? -1
var slots = [Item?](repeating: nil, count: maxIndex + 1)
items2.forEach { slots[$0.index] = $0.element }
let result = slots.compactMap { $0 }

This amounts to creating an array of nil large enough to hold the largest index, putting the items into the array using their index, and then removing the nils (empty slots) with compactMap(). This sorting algorithm is O(n) instead of O(n log n) like the general sorting algorithm.

How to compare enum with associated values by ignoring its associated value in Swift?

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.

Can i use if to compare enum which contains parameter?

Yes, it is. You have at least two options on top of the switch statement you already wrote.

  • a. Use Equatable
  • b. if case

Use Equatable

Make your enum Equatable, and use ==. In your case you don't have to write any code other than:

enum Foo: Equatable {
case a(Int)
}

if f == .a(10) {
print("yes")
}

Use if case

If you don't won't or can't easily make your type Equatable you can always use if case:

if case .a = f {
print("yes")
}

if case .a(let x) = f, x == 10 {
print("yes")
}

The second example is also handy if you want to capture the value to use it in the if branch, without necessarily inspecting it for the equatable check

if case .a(let x) = f {
print(x)
}

How do I get the count of a Swift enum?

As of Swift 4.2 (Xcode 10) you can declare
conformance to the CaseIterable protocol, this works for all
enumerations without associated values:

enum Stuff: CaseIterable {
case first
case second
case third
case forth
}

The number of cases is now simply obtained with

print(Stuff.allCases.count) // 4

For more information, see

  • SE-0194 Derived Collection of Enum Cases


Related Topics



Leave a reply



Submit