Converting a String to a Double without Obj-C

Update: Kyle Sluder pointed out that this code does not consider locale at all. As such, it is not preferable to NSNumberFormatter in most cases.

It all began with a ‘simple’ request on the Apple Developer Forums for a way to convert a string to a double using ‘native Swift’. They received a couple answers which leveraged Cocoa (NSNumberFormatter and NSScanner) and C (strtod()) and the original post has a solution casting String to NSString to use doubleValue. None of these are ‘Swift only’ solutions, though. What follows is almost pure Swift. I did not want to implement pow because… reasons.

Parser Combinator

Note: (This parser combinator business is only necessary if we need to extract the string from some context. If you know the string is well formed, you can skip it.)

The first part of an answer would, in my opinion, be parser combinators : Something like Rob Rix's Madness library. It doesn't actually have Float parsing, but it is a much more general solution and you can use the code below with it. The main reason that I ended up taking a swing at this question is that I have made a library similar to but with less focus than Madness. I based mine on this paper by Hutton and Meijer and will eventually release it but Madness is publicly available now.

Using Madness, I would make a combinator that looked for digit+, followed–without spaces–by a period, followed–again without spaces–by digit+.

Conversion

This strategy is based on both the natural number parsing outlined in the Hutton/Meijer paper and scientific notation. We will model our value with a Significand and an exponent. Once we pass the decimal point, we decrement the exponent.
After some flailing, I wanted a state machine to model the transitions from before to after the decimal point which meant “Let’s use an enum!”.

The enum, DoubleParsingState, has five values. .Failed represents a failed parse. For simplicity’s sake, this implementation assumes that the input string has been verified already and is well formed. Any non-numeric character or extra decimal point encountered means a failed parse. .Initial represents a parse before any charcters have been encountered. this allows use to avoid having to return 0 if we never process a character.
The instance method processCharacter(_) on .Initial or .BeforeDecimal(Int) calls through to a private method named _processCharacterBeforeDecimal(_:accum:). _processCharacterBeforeDecimal(_:accum:)  is straightforward enough and returns .BeforeDecimal(Int) if a digit was processed–where the associated value is the significand with an implied exponent of 0, .Decimal(Int) with the same associated value if a period is processed, or .Failed if an invalid character was provided.
The instance method processCharacter(_) on .Decimal(Int) and .AfterDecimal(Int, Int) both call through to _processCharacterAfterDecimal(_:accum:) which returns .ActerDecimal(Int, Int) after processing a digit and .Failed otherwise.

Make it a Double

Now that we have a significand and an exponent, we want to actually make make a value with the desired type and we have what we need to do that. If we have parsed input without failing, we multiply the significand by 10 to the power of our exponent and we call it a day.

The code

    import Darwin // I don't want to reimplement pow.

    public enum DoubleParsingState {
        case Initial
        case BeforeDecimal(Int)
        case Decimal(Int)
        case AfterDecimal(significand:Int, exponent:Int)
        case Failed
        
        init() {
            self = .Initial
        }
        
        private func _isDigit(char:Character) -> Bool { return "0"..."9" ~= char }
        private func _proc(x:Int, newDigit:Int) -> Int {
            return (x*10) + newDigit
        }
        
        private func _processCharacterBeforeDecimal(char:Character, accum:Int) -> DoubleParsingState {
            switch char {
            case let c where _isDigit(c):
                if let n = String(c).toInt() {
                    return .BeforeDecimal(_proc(accum, newDigit: n))
                } else {
                    fatalError("could not process character because isDigit implementation failed with false positive. (before)")
                }
            case ".":
                return .Decimal(accum)
            default:
                return .Failed
            }
        }
        
        private func _processCharacterAfterDecimal(char:Character, accum:(significand:Int, exponent:Int)) -> DoubleParsingState {
            switch char {
            case let c where _isDigit(c):
                if let n = String(c).toInt() {
                    return .AfterDecimal(significand:_proc(accum.significand, newDigit: n), exponent:accum.exponent - 1)
                } else {
                    fatalError("could not process character because isDigit implementation failed with false positive. (after)")
                }
            default:
                return .Failed
            }
        }
        
        public func processCharacter(char:Character) -> DoubleParsingState {
            switch self {
            case .Initial:
                return _processCharacterBeforeDecimal(char, accum: 0)
            case let .BeforeDecimal(n):
                return _processCharacterBeforeDecimal(char, accum: n)
            case let .Decimal(n):
                return _processCharacterAfterDecimal(char, accum: (significand: n, exponent:0))
            case let .AfterDecimal(s, e):
                return _processCharacterAfterDecimal(char, accum: (significand: s, exponent:e))
            case Failed:
                return .Failed
            }
        }
        
        public var value:Double? {
            switch self {
            case .Initial:
                return nil
            case let .BeforeDecimal(n):
                return Double(n)
            case let .Decimal(n):
                return Double(n)
            case let .AfterDecimal(significand, exponent):
                return Double(significand) * pow(10.0, Double(exponent))
            case Failed:
                return nil
            }
        }
    }

    // example use
    func doIt(input:String) -> Double? {
        let parse:DoubleParsingState = reduce(input, .Initial) { (state, element) in
            state.processCharacter(element)
        }

        return parse.value
    }
    
    doIt("1.23")

Summary

This is, most likely, slower than calling into Cocoa or using strtod but speed isn’t everything. I was delighted at how focused this solution seemed when it was finished. that said, it this enum only models the act parsing and should probably be wrapped in some other type in order to hide the details. This code could probably be folded nicely into Madness and the library I am working on since parsing Doubles is not an unheard of task.