stringparsingswift3

Swift parse string with different formats


I am working on a String parser in which the input can have various formats, and I don't know in advance which format is being used, so I need to write something that is flexible.

The first step is to check the first few characters, I can check that by using eg:

func parse(input: String) -> String {

   let result: String

   if (input.hasPrefix("foo") {
     result = doFoo(input)
   }
   else if (input.hasPrefix("bar") {
     result = doBar(input)
   }
   else if (input.hasPrefix("baz") {
     result = doBaz(input)
   }
   else {
     result = doBasic(input)
   }

   return result
}

and every doXXX() function has it's own parsing code, which again could have multiple options, such as different delimiters, etc.

This could easily turn into lots of if-else code, and I am wondering if with Swift there is a simpler way to do this. Maybe using switch-case statements, or something else? Could I use an enum for this?

EDIT: the code is inside a String extension.


Solution

  • Here's how I would do it:

    // This pattern matching operator defines what it means to have a
    // closure as a pattern.  If the closure evaluates to true when called
    // with `value` as an arg, then the `pattern` matches the `value`.
    func ~=<T>(pattern: (T) -> Bool, value: T) -> Bool {
        return pattern(value)
    }
    
    // This type alias is just here to make the next line a bit more readable.
    // A `BoolInstanceMethod<T, U>` is a closure type that represents an unapplied
    // instance method that ultimately returns a Bool.
    
    // For example, `String.hasPrefix` has type `(String) -> (String) -> Bool`.
    // The first argument, of type `T` (String, in this case) is the instance
    // this method will be called on.
    
    // Say we call this: String.hasPrefix("The quick brown fox").
    // The result has type `(String) -> Bool`.
    // It's equivalent to "The quick brown fox".hasPrefix.
    
    // We then call the resulting closure with the argument to hasPrefix
    // For example: String.hasPrefix("The quick brown fox")("The")
    // This has type `Bool`. It's the same as: "The quick brown fox".hasPrefix("The)
    typealias BoolInstanceMethod<T, U> = (_ instance: T) -> (_ arg: U) -> Bool
    
    // This function wraps a given instance method, in such a way as to reverse the
    // order of the curried arguments. The given instance method is usually called as:
    // Type.instanceMethod(instance)(arg), but this function allows you to swap it, to
    // call it as: apply(Type.instanceMethod)(arg)(instance)
    func apply<T, U>(instanceMethod: @escaping BoolInstanceMethod<T, U>) -> (_ arg: U) -> (_ instance: T) -> Bool {
        return { arg in
            return { instance in
                return instanceMethod(instance)(arg)
            }
        }
    }
    
    // Dummy functions to satisfy the compiler
    func doFoo(_: String) -> String { return "" }
    func doBar(_: String) -> String { return "" }
    func doBaz(_: String) -> String { return "" }
    func doBasic(_: String) -> String { return "" }
    
    func parse(input: String) -> String {
        let result: String
        
        // The predicate of choice is made, in this case, String.hasPrefix.
        let hasPrefix = apply(instanceMethod: String.hasPrefix)
        
        // The switch calls `~=` for every case, giving it hasPrefix(...) and "input"
        // as args. The first case that makes `~=` yield `true` is executed.
        switch input {
        case hasPrefix("foo"): result = doFoo(input)
        case hasPrefix("bar"): result = doBar(input)
        case hasPrefix("baz"): result = doBaz(input)
        default: result = doBasic(input)
        }
        
        return result
    }
    
    // You could also implement parse like this:
    func parse2(input: String) -> String {
        // You can save repeated application of the `input` parameter by doing it
        // just once at the end (see the `return` of this func).
        let action: (String) -> String
        
        // The predicate of choice is made, in this case, String.hasPrefix.
        let hasPrefix = apply(instanceMethod: String.hasPrefix)
        
        // The switch calls `~=` for every case, giving it hasPrefix(...) and "input"
        // as args. The first case that makes `~=` yield `true` is executed.
        
        switch input {
        case hasPrefix("foo"): action = doFoo
        case hasPrefix("bar"): action = doBar
        case hasPrefix("baz"): action = doBaz
        default: action = doBasic
        }
        
        return action(input)
    }