Parsing a string into multiple types

I am getting some data from a network response and it comes in as a string but can really be multiple types of data such as an Int, Bool, Date. Is there a nicer way to do this than multiple if statements?
Currently I have (in an enum init)

 let value: String = someFieldFromNetworkResponse
 if let value = Int(value) {
     self = .int(value)
 } else if let value = parseDate(value) {
     self = .data(value)
 } else if let value = parseBool(value) {
     self = .bool(value)
 } else {
     self = .unknown
 }

With the actual use case being to parse an associated enum:

public enum IdentificationChallengeNotice: JSONObjectConvertible, Equatable {
    case remainingAttempts(_ attempts: Int)
    case validUntil(_ date: Date)
    case passcodeMatches(_ matches: Bool)
    case other(type: String, value: String)
    public init(jsonDictionary: JSONDictionary) throws {
        let type: String = jsonDictionary.json(atKeyPath: "type") ?? .unknown
        let value: String = jsonDictionary.json(atKeyPath: "value") ?? ""
        if type == "remainingAttempts", let value = Int(value) {
            self = .remainingAttempts(value)
        } else if type == "validUntil", let value = parseDate(value) {
            self = .validUntil(value)
        } else if ... {
        ...
        } else {
            self = .other(type: type, value: value)
        }
}

I'm trying to do something like this but it doesn't seem possible to have a let in a where clause:

let value: String = someFieldFromNetworkResponse
switch value {
case let v where let value = Int(v):
    self = .int(value)
// etc
}

or even something like this would be even more ideal:

switch value {
case let value = Int($0):
    self = .int(value)
// etc
}

There's a different way, but I'm not sure if it's nicer or not

func parseBool(_ str: String) -> Bool? {
    return Bool(str)
}
enum Foo {
    case int(Int)
    case bool(Bool)
    case unknown

    init(string: String) {
        self = Int(string).map { .int($0) } ?? parseBool(string).map { .bool($0) } ?? .unknown
    }
}

Hmm interesting. Sorry I should have shared the use case where there's a type field that drives which type it is:

public enum IdentificationChallengeNotice: JSONObjectConvertible, Equatable {
    case remainingAttempts(_ attempts: Int)
    case validUntil(_ date: Date)
    case passcodeMatches(_ matches: Bool)
    case other(type: String, value: String)
    public init(jsonDictionary: JSONDictionary) throws {
        let type: String = jsonDictionary.json(atKeyPath: "type") ?? .unknown
        let value: String = jsonDictionary.json(atKeyPath: "value") ?? ""
        if type == "remainingAttempts", let value = Int(value) {
            self = .remainingAttempts(value)
        } else if type == "validUntil", let value = parseDate(value) {
            self = .validUntil(value)
        } else if ... {
        ...
        } else {
            self = .other(type: type, value: value)
        }
}

In that case a bunch of ifs is the nicest solution

Here's a bonus, in case your keyboard breaks and no longer is able to type `if`
import Foundation
func parseDate(_ str: String) -> Date? { Date() }
public enum IdentificationChallengeNotice {
    case remainingAttempts(_ attempts: Int)
    case validUntil(_ date: Date)
    case passcodeMatches(_ matches: Bool)
    case other(type: String, value: String)
    public init() throws {
        let type: String = "foo"
        let value: String = "bar"
        self = {
            switch type {
                case "remainingAttempts":
                    return Int(value).map { Self.remainingAttempts($0) }
                case "validUntil":
                    return parseDate(value).map { Self.validUntil($0) }
                default:
                    return nil
            }
        }() ?? Self.other(type: type, value: value)
    }
}

There are probably some ways to push this into a switch, such as:

func ~=<T>(pattern: (String) -> T?, value: String) -> Bool {
    return pattern(value) != nil
}
switch (type, value) {
  case ("remainingAttempts", Int.init): self = .remainingAttempts(Int(value)!)
  case ("validUntil", parseDate): self = .validUntil(extractDate(value))
  ...
  default: .other(type: type, value: value)
}

but it makes me think that switch is probably not the right tool for this kind of thing.


I don't personally think there's anything wrong with just using if statements and you can use return to break up your if statements to make it look a little cleaner:

if type == "remainingAttempts", let intValue = Int(value) {
  self = .remainingAttempts(intValue)
  return
}

if type == "validUntil", let dateValue = parseDate(value) {
  self = .validUntil(dateValue)
  return
}

...

self = .other(type: type, value: value)

Yep, I'm find with the if else's it just seems like it's so close to being possible in a switch statement that I thought I was missing something.

public enum IdentificationChallengeNotice: JSONObjectConvertible, Equatable {
    case remainingAttempts(_ attempts: Int)
    case validUntil(_ date: Date)
    case passcodeMatches(_ matches: Bool)
    case other(type: String, value: String)
    public init(jsonDictionary: JSONDictionary) throws {
        let type: String = jsonDictionary.json(atKeyPath: "type") ?? .unknown
        let value: String = jsonDictionary.json(atKeyPath: "value") ?? ""
        switch type {
        case "remainingAttempts", where let value = Int(value):
            self = .remainingAttempts(value)
        case "validUntil", where let value = parseDate(value):
            self = .validUntil(value)
        // etc
        default:
            self = .other(type: type, value: value)
        }
}

Seems like valid swift, except you can't have a let value = ... in a where clause. I'm not really sure why not either.

I believe it’s because the case evaluation has to return a Bool i.e case foo and case foo where foo.bar evaluates to a true/false value which is used to decide whether the match is successful or not. I suppose an optional binding (where let a = b) could be supported by treating it as a Bool, however if you really want to do optional binding, the only way to do it now would be inside the case statement:

// For convenience.
func other() -> Self {
  return .other(type: type, value: value)
}

switch type {
  case “validUntil”: 
    if let value = parseDate(value) {
      self = .validUntil(value)
    } else {
      self = other()
    }
  ...
}