6

In Swift, when an enum conforms to CaseIterable, "The synthesized allCases collection provides the cases in order of their declaration."

I would like to sort an array of CaseIterable enum cases, which means conforming to Comparable. Can I access this same order of declaration to determine how such objects should be sorted?

If not, is my implementation of < below reasonable?

enum MyEnum: CaseIterable, Comparable {
    case one, two, three
}

static func < (lhs: MyEnum, rhs: MyEnum) -> Bool {
    guard let lhsIndex = allCases.firstIndex(of: lhs), let rhsIndex = allCases.firstIndex(of: rhs) else {
        fatalError("`MyEnum`'s implementation of `Comparable.<` found a case that isn't present in `allCases`.")
    }
    return lhsIndex < rhsIndex
}

Finally, is it possible to go one step further and make CaseIterable itself conform to Comparable this way? Any reason this would be a bad idea?

TylerP
  • 9,600
  • 4
  • 39
  • 43
Ben Packard
  • 26,102
  • 25
  • 102
  • 183
  • 1
    I have a generalized solution for this problem, which I outline here: HardCodedOrdering https://stackoverflow.com/a/43056896/3141234 – Alexander May 13 '20 at 18:37

5 Answers5

8

In Swift 5.3 and later you can use the synthesized conformance to Comparable for any enum that either has no associated value or has an associated value that conforms to Comparable.

For example:

enum Size: Comparable {
    case small
    case medium
    case large
    case extraLarge
}

let shirtSize = Size.small
let personSize = Size.large

if shirtSize < personSize {
    print("That shirt is too small")
}

I have an article providing information on this change, along with other features introduced in Swift 5.3: What’s new in Swift 5.3?

TwoStraws
  • 12,862
  • 3
  • 57
  • 71
3

If you don't want to make your enum conform to RawRepresentable with an Int RawValue type, like the other answers here have posted, then your approach seems reasonable to me.

As for this question:

Finally, is it possible to go one step further and make CaseIterable itself conform to Comparable this way?

Yes, you could make an extension on Comparable that contains a default implementation of the < function and constrain the extension to only apply when Self also conforms to CaseIterable, as shown below:

extension Comparable where Self: CaseIterable {
    static func < (lhs: Self, rhs: Self) -> Bool {
        guard let lhsIndex = allCases.firstIndex(of: lhs), let rhsIndex = allCases.firstIndex(of: rhs) else {
            fatalError("`\(Self.self)`'s implementation of `Comparable.<` found a case that isn't present in `allCases`.")
        }
        return lhsIndex < rhsIndex
    }
}

This will give this < function to any enum you declare as conforming to both Comparable and CaseIterable.

TylerP
  • 9,600
  • 4
  • 39
  • 43
2

You could make the enum accessed in the same order as the declaration by providing an Int raw type to your enum:

enum MyEnum: Int, CaseIterable {
Frankenstein
  • 15,732
  • 4
  • 22
  • 47
1

I think it's a pretty good solution (given the garbage constraint we have that < isn't allowed to throw ). You can reuse pieces though and not have to create an error message especially for <.

public extension Result {
  /// `success` for `Optional.some`s; `failure` for `.none`s.
  init(
    success: Success?,
    failure getFailure: @autoclosure () -> Failure
  ) {
    self =
      success.map(Self.success)
      ?? .failure( getFailure() )
  }
}
/// A type with a `comparable` property, used for `<`.
public protocol comparable: Comparable {
  associatedtype Comparable: Swift.Comparable
  var comparable: Comparable { get }
}

public extension comparable {
  static func < (comparable0: Self, comparable1: Self) -> Bool {
    comparable0.comparable < comparable1.comparable
  }
}
public extension CaseIterable where Self: Equatable {
  /// The first match for this case in `allCases.indices`.
  /// - Throws: `AnyCaseIterable.AllCasesError.noIndex`
  func getCaseIndex() throws -> AllCases.Index {
    try Result(
      success: Self.allCases.firstIndex(of: self),
      failure: AnyCaseIterable.AllCasesError.noIndex(self)
    ).get()
  }
}

public enum AnyCaseIterable {
  public enum AllCasesError<Case>: Error {
    /// No `AllCases.Index` corresponds to this case.
    case noIndex(Case)
  }
}

public extension comparable where Self: CaseIterable {
  /// The index of this case in `allCases`.
  var comparable: AllCases.Index { try! getCaseIndex() }
}
enum MyEnum: CaseIterable, comparable {
  case one, two, three
}

MyEnum.one < .three // true
-1

I think, you can use simpler approach.

enum MyEnum: Int, CaseIterable, Comparable {
    static func < (lhs: MyEnum, rhs: MyEnum) -> Bool {
        lhs.rawValue < rhs.rawValue
    }

    case one, two, three
}

Regarding your second question, I don't think it is even possible to implement the Comparable protocol for the CaseIterable protocol.