Ranges for enum and struct types

by @ralfebert · updated January 20, 2022
Xcode 13 & iOS 15
Swift Examples

Swift has a syntax for ranges of values:

0...5    // represents [0, 1, 2, 3, 4, 5]
0..<5    // represents [0, 1, 2, 3, 4]
(0...5).contains(3)   // true
(0...5).forEach { number in print(number) }

By default this is supported for numeric types. But can this be enabled for enums and structs as well? For example, in one application I have an enum type to represent musical notes and intervals:

public enum NoteLetter {
    case C, D, E, F, G, A, B
}

public struct Interval {
    var semitones : Int

    public static let unison = Interval(semitones: 0)
    public static let minorSecond = Interval(semitones: 1), majorSecond = Interval(semitones: 2)
    public static let minorThird = Interval(semitones: 3), majorThird = Interval(semitones: 4)
    public static let fourth = Interval(semitones: 5)
    public static let tritone = Interval(semitones: 6)
    public static let fifth = Interval(semitones: 7)
    public static let minorSixth = Interval(semitones: 8), majorSixth = Interval(semitones: 9)
    public static let minorSeventh = Interval(semitones: 10), majorSeventh = Interval(semitones: 11)
    public static let octave = Interval(semitones: 12)
}

It would be nice to be able to define ranges based on this type like this:

NoteLetter.C..<NoteLetter.F
Interval.fifth..<Interval.octave

By default, this will give an error:

  • Referencing operator function '..<' on 'Comparable' requires that 'NoteLetter' conform to 'Comparable'
  • Referencing operator function '..<' on 'Comparable' requires that 'Interval' conform to 'Comparable'

To be used as a range, the type needs to implement the Strideable protocol that defines how to measure the distance between two values and how to go from one value to antother one by distance:

public enum NoteLetter: Int, Strideable {

    case C, D, E, F, G, A, B

    public func distance(to other: NoteLetter) -> NoteLetter.Stride {
    return Stride(other.rawValue) - Stride(self.rawValue)
}

public func advanced(by n: NoteLetter.Stride) -> NoteLetter {
    return NoteLetter(rawValue: numericCast(Stride(self.rawValue) + n))!
}

public typealias Stride = Int
}

The same is possible for structs:

public struct Interval : Strideable {
    var semitones : Int

    public static let unison = Interval(semitones: 0)
    public static let minorSecond = Interval(semitones: 1), majorSecond = Interval(semitones: 2)
    public static let minorThird = Interval(semitones: 3), majorThird = Interval(semitones: 4)
    public static let fourth = Interval(semitones: 5)
    public static let tritone = Interval(semitones: 6)
    public static let fifth = Interval(semitones: 7)
    public static let minorSixth = Interval(semitones: 8), majorSixth = Interval(semitones: 9)
    public static let minorSeventh = Interval(semitones: 10), majorSeventh = Interval(semitones: 11)
    public static let octave = Interval(semitones: 12)

    public func distance(to other: Interval) -> Interval.Stride {
    return Stride(other.semitones) - Stride(self.semitones)
}

public func advanced(by n: Interval.Stride) -> Interval {
    return Interval(semitones: numericCast(Stride(self.semitones) + n))
}

public typealias Stride = Int

}