Daniel T.
Daniel T.

Reputation: 33967

Map OptionSetType to Array

Given the following:

struct Weekdays: OptionSetType {

    let rawValue: Int
    init(rawValue: Int) { self.rawValue = rawValue }

    static let Monday = Weekdays(rawValue: 1)
    static let Tuesday = Weekdays(rawValue: 2)
    static let Wednesday = Weekdays(rawValue: 4)
    static let Thursday = Weekdays(rawValue: 8)

    static let allOptions: [Weekdays] = [.Monday, .Tuesday, .Wednesday, .Thursday]

}

I can convert an array of Ints into a Weekdays object by doing this:

let arr = [1, 4]
let weekdays = arr.reduce(Weekdays()) { $0.union(Weekdays(rawValue: $1)) }

My question is, how do I take a Weekdays object and convert it into an array of Ints?

Upvotes: 4

Views: 2911

Answers (4)

Ivorius
Ivorius

Reputation: 365

Not exactly answering the question, but might be useful to others. Based on Martin's answer I extract back the component objects:

extension FixedWidthInteger {
    init(bitComponents : [Self]) {
        self = bitComponents.reduce(0, +)
    }

    var bitComponents : [Self] {
        (0 ..< Self.bitWidth).map { 1 << $0 } .filter { self & $0 != 0 }
    }
}

extension OptionSet where RawValue: FixedWidthInteger, Self == Self.Element {
    var components : [Self] { rawValue.bitComponents.map(Self.init) }
}

Upvotes: 3

nteissler
nteissler

Reputation: 1581

You can improve the context of your extension by defining it conditionally on OptionSet.

extension OptionSet where RawValue: UnsignedInteger {
    var individualCases: [Self] {
        return (0..<(8 * MemoryLayout<RawValue>.size))
            .map { bitsToShift in RawValue(1 << bitsToShift) } // Get every possible single-bit flag
            .filter { (powerOfTwo: RawValue) -> Bool in rawValue & powerOfTwo != 0 } // filter out only the cases the receiver contains
            .map { Self(rawValue: $0) } // create the `OptionSet` (single bit) type
    }
}

let weekdays = Weekdays(rawValue: 0b11111)
weekdays.individualCases.map { $0.rawValue } // [1, 2, 4, 8, 16]

A warning: On my 13" 2019 MacBookPro, I had to provide all of the explicit types above to keep the methods type checking under 1500ms in Swift 5.0.

Thanks to MartinR for the inspiration to loop of the memory layout size.

To complete the example, I've updated the weekday type to Swift 5 below, and explicitly used the UInt8 type to make the individualCases more efficient. With UInt, it would loop over the first map and filter 64 times each, with UInt8 it only loops 8 times.

struct Weekdays: OptionSet {

    let rawValue: UInt8

    static let Monday = Weekdays(rawValue: 1)
    static let Tuesday = Weekdays(rawValue: 2)
    static let Wednesday = Weekdays(rawValue: 4)
    static let Thursday = Weekdays(rawValue: 8)
    static let Friday = Weekdays(rawValue: 16)
}

Upvotes: 1

Martin R
Martin R

Reputation: 539815

(Not necessarily better, but a different way to look at it and slightly more general).

OptionSetType inherits from RawRepresentable and therefore can be converted from and to the associated raw type, which in your case is Int.

So the "missing link" is a conversion between the raw value (e.g. 5) and an integer array of the bitwise components (e.g. [1, 4]).

This can be done with an Int extension method:

extension Int {
    init(bitComponents : [Int]) {
        self = bitComponents.reduce(0, combine: (+))
    }

    func bitComponents() -> [Int] {
        return (0 ..< 8*sizeof(Int)).map( { 1 << $0 }).filter( { self & $0 != 0 } )
    }
}

Then your conversion from an array to a Weekdays object becomes

let arr : [Int] = [1, 4]
let weekdays = Weekdays(rawValue: Int(bitComponents: arr))
print(weekdays)
// app.Weekdays(rawValue: 5)

and the reverse conversion

let array = weekdays.rawValue.bitComponents()
print(array)
// [1, 4]

Advantages:

  • The explicit definition of allOptions: is not needed.
  • It can be applied to other option set types (which have Int as a raw value).

One could also try to define the conversions as a protocol extension, e.g. of IntegerType, so that the same works with other integer raw types as well. However, this seems to be a bit complicated/ugly because the left shift operator << is not part of the IntegerType (or any) protocol.


Update for Swift 3:

extension Int {
    init(bitComponents : [Int]) {
        self = bitComponents.reduce(0, +)
    }

    func bitComponents() -> [Int] {
        return (0 ..< 8*MemoryLayout<Int>.size).map( { 1 << $0 }).filter( { self & $0 != 0 } )
    }
}

Upvotes: 10

Daniel T.
Daniel T.

Reputation: 33967

As I was writing the question, I figured it out:

let array = Weekdays.allOptions.filter { weekdays.contains($0) }.map { $0.rawValue }

Is there a better way?

Upvotes: 1

Related Questions