42

I have a custom OptionSetType struct in Swift. How can I enumerate all values of an instance?

This is my OptionSetType:

struct WeekdaySet: OptionSetType {
    let rawValue: UInt8

    init(rawValue: UInt8) {
        self.rawValue = rawValue
    }

    static let Sunday        = WeekdaySet(rawValue: 1 << 0)
    static let Monday        = WeekdaySet(rawValue: 1 << 1)
    static let Tuesday       = WeekdaySet(rawValue: 1 << 2)
    static let Wednesday     = WeekdaySet(rawValue: 1 << 3)
    static let Thursday      = WeekdaySet(rawValue: 1 << 4)
    static let Friday        = WeekdaySet(rawValue: 1 << 5)
    static let Saturday      = WeekdaySet(rawValue: 1 << 6)
}

I would like to something like this:

let weekdays: WeekdaySet = [.Monday, .Tuesday]
for weekday in weekdays {
    // Do something with weekday
}
Florian
  • 5,326
  • 4
  • 26
  • 35
  • 2
    @MartinR has a great answer in terms of implementation, but there's something to think about in terms of *use case* for anyone looking at this kind of scenario. Remember that `OptionSet`s are not restricted sets of values the way `enum`s are, so enumerating an arbitrary set could give you a result of (per this example) `[.Monday, .Friday, WeekdaySet(rawValue: 128)]`. Always sanitize your inputs! – rickster Nov 30 '16 at 20:52
  • While there are some great answers, note that using `OptionSet` for similar data types is just not a good idea. Option sets are mainly used for *option masks* because they are defined by integer bits. A far better solution would be a combination of a normal `enum` with a `Set`. – Sulthan Nov 30 '20 at 20:26
  • What is an "option mask"? – Peter Schorn May 18 '21 at 07:21

5 Answers5

47

As of Swift 4, there are no methods in the standard library to enumerate the elements of an OptionSetType (Swift 2) resp. OptionSet (Swift 3, 4).

Here is a possible implementation which simply checks each bit of the underlying raw value, and for each bit which is set, the corresponding element is returned. The "overflow multiplication" &* 2 is used as left-shift because << is only defined for the concrete integer types, but not for the IntegerType protocol.

Swift 2.2:

public extension OptionSetType where RawValue : IntegerType {

    func elements() -> AnySequence<Self> {
        var remainingBits = self.rawValue
        var bitMask: RawValue = 1
        return AnySequence {
            return AnyGenerator {
                while remainingBits != 0 {
                    defer { bitMask = bitMask &* 2 }
                    if remainingBits & bitMask != 0 {
                        remainingBits = remainingBits & ~bitMask
                        return Self(rawValue: bitMask)
                    }
                }
                return nil
            }
        }
    }
}

Example usage:

let weekdays: WeekdaySet = [.Monday, .Tuesday]
for weekday in weekdays.elements() {
    print(weekday)
}

// Output:
// WeekdaySet(rawValue: 2)
// WeekdaySet(rawValue: 4)

Swift 3:

public extension OptionSet where RawValue : Integer {

    func elements() -> AnySequence<Self> {
        var remainingBits = rawValue
        var bitMask: RawValue = 1
        return AnySequence {
            return AnyIterator {
                while remainingBits != 0 {
                    defer { bitMask = bitMask &* 2 }
                    if remainingBits & bitMask != 0 {
                        remainingBits = remainingBits & ~bitMask
                        return Self(rawValue: bitMask)
                    }
                }
                return nil
            }
        }
    }
}

Swift 4:

public extension OptionSet where RawValue: FixedWidthInteger {

    func elements() -> AnySequence<Self> {
        var remainingBits = rawValue
        var bitMask: RawValue = 1
        return AnySequence {
            return AnyIterator {
                while remainingBits != 0 {
                    defer { bitMask = bitMask &* 2 }
                    if remainingBits & bitMask != 0 {
                        remainingBits = remainingBits & ~bitMask
                        return Self(rawValue: bitMask)
                    }
                }
                return nil
            }
        }
    }
}
Martin R
  • 529,903
  • 94
  • 1,240
  • 1,382
  • 3
    Thanks, Martin. Your solutions looks OK. However, I have the feeling that this should be easier. I've filed a bug with Apple. – Florian Aug 20 '15 at 06:59
  • 4
    I'd hardly call it a bug. OptionSetType is NOT an array, it merely conforms to the ArrayLiteralConvertable protocol meaning that it can be instantiated using an array without recourse to specialised init methods. Once you've finished creating it the array itself is no longer relevant; it has been converted to an integer. As such I think Martin's solution is very elegant, working as it does with any OptionSetType that uses an unsigned integer as a raw value. – Ash Jun 05 '16 at 08:34
  • Is it possible to use this in generic func? I'm trying this in playground: `func set(value: T) { let elements = value.elements() }`. Getting: `T is not convertible to T.Element` – Zmey Jul 15 '16 at 17:32
  • 1
    @Zmey: You have do add another constraint: `set`. – Compare http://swiftdoc.org/v3.0/protocol/OptionSet/: *"To inherit all the default implementations from the OptionSet protocol, the Element type must be Self, the default."* – I'll check if one can get rid of that restriction, but I don't think that is essential. – Martin R Jul 15 '16 at 17:39
  • 1
    @Zmey: As it turned out, the restriction is not necessary. I have rewritten the answer completely :) – Martin R Jul 15 '16 at 18:50
  • @Florian: *"Your solutions looks OK. However, I have the feeling that this should be easier"* – I don't know what answer you expect. If your question is *"Does the Swift standard library provide a method to enumerate an option set"* then the answer is a just "No". – Martin R Jul 17 '16 at 13:51
  • Why defer in the swift 3 version but not swift 2? It already exists since 2.0 right? – Fr4nc3sc0NL Jul 21 '16 at 13:31
  • @Fr4nc3sc0NL: You are completely right – fixed! Don't know why I overlooked that :) Thanks for the feedback! – Martin R Jul 21 '16 at 13:45
  • 2
    NP :) I started reading some more about defer and it looks like NSHipster advices against this pattern: http://nshipster.com/guard-and-defer/ (scroll down to "(Any Other) Defer Considered Harmful" – Fr4nc3sc0NL Jul 21 '16 at 15:18
  • How do you get a simple count of this? AnySequence does not seem to have a simple of counting the elements in it – Marchy Sep 09 '16 at 21:21
  • @Marchy: Yes, a sequence need not have a last element, a sequence may generate "infinitely" many elements. For an option set, the number of elements would be the number for 1-bits in its rawValue. See for example "Counting bit sets" in https://graphics.stanford.edu/~seander/bithacks.html for various methods to compute that effectively. – Martin R Nov 01 '16 at 07:52
  • 1
    @MartinR Swift 4 (via Xcode 9 Beta) requires `BinaryInteger` instead of `Integer`. Also, the overflow multiplication `&*` causes a compiler error. – David James Jun 11 '17 at 09:46
  • 2
    @DavidJames: It compiles (and runs) with `RawValue: FixedWidthInteger`. I'll update the answer accordingly, thanks for the notice! – Martin R Jun 11 '17 at 09:56
  • @TMin: Thank you for the edit suggestion. However, there is no FixedWidthInteger protocol in Swift 3 (Xcode 8). You can use the Swift 4 version in Xcode 9 (with Swift 4 or Swift 3.2 mode). – Martin R Sep 26 '17 at 05:42
  • possibly a topic for meta but top grosser with 25-30 upvotes should be automatically become an accepted answer – Anton Tropashko Jul 20 '18 at 12:11
  • @AntonTropashko: You can ask that on Meta, but I am fairly sure that such a suggestion will be denied (and it probably has been asked before). It is one of the Stack Exchange principles that only the question author can accept an answer (which *she/de* considers most helpful) and that accepting is not mandatory. – We may like that or not, but that's how it is and we have to live with it! – Martin R Jul 20 '18 at 12:21
  • @Ash I don't understand the relevance of the fact that it's not an array. Arrays aren't the only types that provide access to their elements. – Peter Schorn Jun 05 '21 at 23:32
  • @PeterSchorn Option sets also provide access to their elements, just not ordered access. But that comment was made five years ago, so here's an update. An option set is basically a bit mask. Option A's raw value is 1, Option B's is 2, but Option C's is 4. A raw value of 3 would be A & B, and it's fine to define this as a value. However, this complicates enumeration. Do you enumerate only the provided values, or all possible combinations? It's similar to the reasoning why CGPoints can't be multiplied - are they vectors, or complex numbers for use in 3D algebra? Better to let the user decide. – Ash Jun 06 '21 at 07:07
  • @PeterSchorn actually, perhaps more to the point, I believe what I was originally getting at in that comment was that although you can create an option set using an array, an option set in and of itself is not an array and so does not *automatically* conform to the Iterator protocol. In other words, Apple don't say that it should be possible to iterate an option set, so it not being possible to do this is not a bug. – Ash Jun 06 '21 at 07:25
  • I also acknowledge that this is not a bug, but I would consider it a missing feature. I think it would be useful to add an iterator that iterates over all of the individual elements; that is, those with only a single bit set to 1. – Peter Schorn Jun 08 '21 at 04:16
16

Based on the previous answers I created a generic Swift 4 solution with IteratorProtocol:

public struct OptionSetIterator<Element: OptionSet>: IteratorProtocol where Element.RawValue == Int {
    private let value: Element

    public init(element: Element) {
        self.value = element
    }

    private lazy var remainingBits = value.rawValue
    private var bitMask = 1

    public mutating func next() -> Element? {
        while remainingBits != 0 {
            defer { bitMask = bitMask &* 2 }
            if remainingBits & bitMask != 0 {
                remainingBits = remainingBits & ~bitMask
                return Element(rawValue: bitMask)
            }
        }
        return nil
    }
}

Then in OptionSet extension implement makeIterator()

assuming your OptionSets will be Int:

extension OptionSet where Self.RawValue == Int {
   public func makeIterator() -> OptionSetIterator<Self> {
      return OptionSetIterator(element: self)
   }
}

Right now every time you create an OptionSet, just conform it to Sequence.

struct WeekdaySet: OptionSet, Sequence {
    let rawValue: Int

    ...
}

You should now be able to iterate over it:

let weekdays: WeekdaySet = [.monday, .tuesday]
for weekday in weekdays {
    // Do something with weekday
}

I'd also create a typealias to be explicit on what is used:

typealias SequenceOptionSet = OptionSet & Sequence

milo
  • 427
  • 5
  • 14
1

Here you go. I also added a convenience initializer to cut down on some of the boilerplate:

enum Day: Int {
  case Sun, Mon, Tue, Wed, Thu, Fri, Sat
}

struct WeekdaySet: OptionSetType {

  let rawValue: UInt8

  init(rawValue: UInt8) {
    self.rawValue = rawValue
  }

  init(_ rawValue: UInt8) {
    self.init(rawValue: rawValue)
  }

  static let Sunday = WeekdaySet(1 << 0)
  static let Monday = WeekdaySet(1 << 1)
  static let Tuesday = WeekdaySet(1 << 2)
  static let Wednesday = WeekdaySet(1 << 3)
  static let Thursday = WeekdaySet(1 << 4)
  static let Friday = WeekdaySet(1 << 5)
  static let Saturday = WeekdaySet(1 << 6)
  static let AllDays = [Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday]

  subscript(indexes: Day...) -> [WeekdaySet] {
    var weekdaySets = [WeekdaySet]()

    for i in indexes {
      weekdaySets.append(WeekdaySet.AllDays[i.rawValue])
    }

    return weekdaySets
  }

}

for weekday in WeekdaySet()[Day.Mon, Day.Tue] {
  print(weekday)
}
Scott Gardner
  • 8,603
  • 1
  • 44
  • 36
  • Thanks, Scott. However, I want to enumerate the values of a specific instance of WeekdaySet, not just all weekdays. – Florian Aug 20 '15 at 06:58
1

Using the approach made in this mini-library (https://github.com/allexks/Options) you can just make your weekdays of a regular enum type:

enum Weekday: CaseIterable {
    case sunday
    case monday
    case tuesday
    case wednesday
    case thursday
    case friday
    case saturday
}

This way you can create an OptionSet easily:

let weekdays: Options<Weekday> = [.monday, .tuesday]

Then in order to iterate you can use the following convenince property provided by the library:

for weekday in weekdays.decomposed {
    // Do something with weekday
}
-1
/// Day rawValues used in WeekdaySet. Day proper names capitalized.
enum Day: UInt8, CaseIterable {
    case Sunday    = 0b00000001
    case Monday    = 0b00000010
    case Tuesday   = 0b00000100
    case Wednesday = 0b00001000
    case Thursday  = 0b00010000
    case Friday    = 0b00100000
    case Saturday  = 0b01000000
    var description: String {
        return "\(self)"
    }
}

/// Seven days of the week represented with binary options.
struct WeekdaySet: OptionSet {
    
    /// WeekdaySet initialized with Day (not with Weekday)
    static let Sunday    = WeekdaySet(.Sunday)
    static let Monday    = WeekdaySet(.Monday)
    static let Tuesday   = WeekdaySet(.Tuesday)
    static let Wednesday = WeekdaySet(.Wednesday)
    static let Thursday  = WeekdaySet(.Thursday)
    static let Friday    = WeekdaySet(.Friday)
    static let Saturday  = WeekdaySet(.Saturday)
    
    /// WeekdaySet initialized with Weekday (not with Day)
    static let all: WeekdaySet = [.Sunday, .Monday, .Tuesday, .Wednesday, .Thursday, .Friday, .Saturday]
    static let weekend: WeekdaySet = [.Saturday, .Sunday]
    static let midweek: WeekdaySet = [.Tuesday, .Wednesday, .Thursday]
    static let humpday: WeekdaySet = .Wednesday
    
    /// OptionSet conformance
    let rawValue: UInt8
    init(rawValue: UInt8) {
        self.rawValue = rawValue
    }
    
    /// Init using the enum Day
    init (_ day: Day) {
        self.rawValue = day.rawValue
    }
    
}

extension WeekdaySet: CaseIterable {

    static var allCases: [WeekdaySet] {
        [
            .Sunday,
            .Monday,
            .Tuesday,
            .Wednesday,
            .Thursday,
            .Friday,
            .Saturday,
        ]
    }
    
    /// Computed instance property to filter static allCases
    var array: [WeekdaySet] {
        get {
            return WeekdaySet.allCases.filter { self.contains($0) }
        }
    }
}

extension WeekdaySet: Sequence {
 
    typealias Iterator = AnyIterator<WeekdaySet>
    
    func makeIterator() -> Iterator {
        var iterator = array.makeIterator()
        return AnyIterator {
            return iterator.next()
        }
    }
}

extension WeekdaySet: CustomStringConvertible {
    var description: String {
        if self.array.count < 2 {
            return Day(rawValue: self.rawValue)?.description ?? ""
            
        }
        var results: [String] = []
        for weekday in self.array {
            results.append(Day(rawValue: weekday.rawValue)?.description ?? "")
        }
        return String(describing: results)
    }
}

/// A example set of weekdays
let customSet: WeekdaySet = [.Monday, .Tuesday]

/// Example usages:
print("Does the example set contain humpday?", customSet.contains(.humpday))

for weekday in customSet {
    print("Is \(weekday) midweek?", WeekdaySet.midweek.contains(weekday))
}

print("Thursday:", WeekdaySet.Thursday)
print("Weekend names:", WeekdaySet.weekend)
print("All names", WeekdaySet.all)

// Printed results:
// Does the example set contain humpday? false
// Is Monday midweek? false
// Is Tuesday midweek? true
// Thursday: Thursday
// Weekend names: ["Sunday", "Saturday"]
// All names ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]