6

Given a Swift enum as such:

enum PerformerPosition: Int {

    case String_Violin1
    case String_Violin2
    case String_Viola
    case String_Cello
    case String_CB

    case Wind_Oboe
    case Wind_Clarinet
    case Wind_Flute
    ...

}

(For the needs of the project, I am not able to have a nested enum.) I would like to randomly select an enum value with the String_ prefix, only.

The only way I know so far is to do a random enum value from all the available cases, as such:

private static let _count: PerformerPosition.RawValue = {
    // find the maximum enum value
    var maxValue: Int = 0
    while let _ = PerformerPosition(rawValue: maxValue) { 
        maxValue += 1
    }
    return maxValue
}()

static func randomPerformer() -> PerformerPosition {
    // pick and return a new value
    let rand = arc4random_uniform(UInt32(count))
    return PlayerPosition(rawValue: Int(rand))!
}

How could I make it so I am able to pick a random value based on the String_ prefix only without having to resort to hard-coding an upper value (for example, new String_ prefix positions may be added)? Thanks

Hamish
  • 78,605
  • 19
  • 187
  • 280
daspianist
  • 5,336
  • 8
  • 50
  • 94

3 Answers3

2

So you don't want to change any of the code even if a new position is added. Right?

To do that, you need to get all the enum cases dynamically, instead of hardcoding them. According to this answer, you can use this method to get all the cases:

protocol EnumCollection : Hashable {}
extension EnumCollection {
    static func cases() -> AnySequence<Self> {
        typealias S = Self
        return AnySequence { () -> AnyIterator<S> in
            var raw = 0
            return AnyIterator {
                let current : Self = withUnsafePointer(to: &raw) { $0.withMemoryRebound(to: S.self, capacity: 1) { $0.pointee } }
                guard current.hashValue == raw else { return nil }
                raw += 1
                return current
            }
        }
    }
}

After you got this cases method, you can easily get what you want by:

let startingWithString = Array(PerformerPosition.cases().filter { "\($0)".hasPrefix("String_") })
let rand = arc4random_uniform(UInt32(startingWithString.count))
let randomPosition = startingWithString[Int(rand)]
Community
  • 1
  • 1
Sweeper
  • 213,210
  • 22
  • 193
  • 313
  • 1
    Great solution, and thanks for pointing out the bit about how to get access to all the cases. – daspianist Mar 31 '17 at 16:39
  • This is a working solution but I would consider this a very dirty solution. – Sulthan Mar 31 '17 at 16:46
  • @Sulthan It is. But the OP seems to not want to change code in parallel when he/she add new enum cases, so that's why I gave this solution. – Sweeper Mar 31 '17 at 16:49
  • 1
    It is more resource intensive. However, at the moment I cannot change the data structure and it definitely solves the issue at hand. – daspianist Mar 31 '17 at 16:49
1

If you have different subsets in your enum and you need to access them separately, I would recommend to identify the subsets somehow, consider:

enum PerformerPosition: Int {

    case String_Violin1
    case String_Violin2
    case String_Viola
    case String_Cello
    case String_CB

    case Wind_Oboe
    case Wind_Clarinet
    case Wind_Flute

    static let strings: [PerformerPosition] = [
        .String_Violin1, .String_Violin2, .String_Viola, .String_Cello, .String_CB
    ]

    static let winds: [PerformerPosition] = [
        .Wind_Oboe, .Wind_Clarinet, .String_Viola, .Wind_Flute
    ]
}

Note that this will enable you several things:

  1. You won't need the String_ and Wind_ prefixes. You can simply use .violin1, .oboe, etc.

  2. You can simply generate random position from your array

  3. You can directly check the type of your position:

    let isString = PerformerPosition.strings.contains(.violin1)
    
Sulthan
  • 128,090
  • 22
  • 218
  • 270
  • This is a very helpful suggestion to a potentially more optimal data structure and the type checking is esp helpful. Thanks for suggesting this – daspianist Mar 31 '17 at 16:48
0

I made this in PlayGround:

func randomPerformer() -> PerformerPosition {
    // pick and return a new value
    let rand = arc4random_uniform(_count)
    let string = String(describing:PerformerPosition(rawValue: rand))
    if(string.contains("String_")){
            return PerformerPosition(rawValue: rand)!
    }else{
            return randomPerformer()
    }
}
Aitor Pagán
  • 423
  • 7
  • 18
  • This is a good first step. My issue is that `rand` still takes into account all of the performers (including those who do not have the `String_` prefix). While it will call upon `randomPerformer()` again if the outcome is not satisfactory, I would want to solve the issue in the `rand` part. Maybe There needs to be a dedicated `randString`. – daspianist Mar 31 '17 at 16:34
  • Yes, i didnt understood what you really wanted, sorry – Aitor Pagán Mar 31 '17 at 16:56