HiveBrain v1.2.0
Get Started
← Back to all entries
patternswiftMinor

Parsing and converting DMS Coordinates from String to Double

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
coordinatesdmsparsingdoubleandconvertingfromstring

Problem

I have some coordinates looking like


N47° 15' 36.75",E011° 20' 38.28",+001906.00

and I've created a class to parse and convert them to Double:

struct PLNWaypointCoordinate {
    var latitude: Double = 0.0
    var longitude: Double = 0.0

    init(coordinateString: String) {
        self.latitude = convertCoordinate(string: coordinateString.components(separatedBy: ",")[0])
        self.longitude = convertCoordinate(string: coordinateString.components(separatedBy: ",")[1])
    }

    private func convertCoordinate(string: String) -> Double {
        var separatedCoordinate = string.characters.split(separator: " ").map(String.init)

        let direction = separatedCoordinate[0].components(separatedBy: CharacterSet.letters.inverted).first
        let degrees = Double(separatedCoordinate[0].components(separatedBy: CharacterSet.decimalDigits.inverted)[1])
        let minutes = Double(separatedCoordinate[1].components(separatedBy: CharacterSet.decimalDigits.inverted)[0])
        let seconds = Double(separatedCoordinate[2].components(separatedBy: CharacterSet.decimalDigits.inverted)[0])

        return convert(degrees: degrees!, minutes: minutes!, seconds: seconds!, direction: direction!)
}

    private func convert(degrees: Double, minutes: Double, seconds: Double, direction: String) -> Double {
        let sign = (direction == "W" || direction == "S") ? -1.0 : 1.0
        return (degrees + (minutes + seconds/60.0)/60.0) * sign
    }

}


Is there a better and safer way to perform this conversion?

Last method I've picked up here. Sorry, but I can't find the link to reference it.

Solution

You have defined a value type (struct) and not a class, which is good. In

var latitude: Double = 0.0
var longitude: Double = 0.0


you can omit the type annotations because the compiler can infer the
type automatically:

var latitude = 0.0
var longitude = 0.0


But actually I would go the other way around and replace the initial
values by an init() method. The reason is that – since you defined
your own init method – there is no default memberwise initializer anymore.
Therefore I would start with

struct PLNWaypointCoordinate {
    var latitude: Double
    var longitude: Double

    init(latitude: Double, longitude: Double) {
        self.latitude = latitude
        self.longitude = longitude
    }

    // ...
}


which allows you to create a value not only from a string, but
also from a given latitude and longitude:

let c = PLNWaypointCoordinate(latitude: ..., longitude: ...)


Your init(coordinateString: String) method assumes that the given
string is in a valid format and can crash otherwise (by accesssing
out-of-bounds array elements or unwrapping nils). As
@Ashley already said in his answer, you should define a
failable initializer instead, which returns nil if the input is
invalid:

init?(coordinateString: String) { ... }


Alternatively, define a throwing initializer:

init(coordinateString: String) throws { ... }


which throws an error for invalid input.

Your helper methods convertCoordinate and convert are private,
which is good. They do not use any properties of the value, which means
that they can be made static.

The conversion helper function is very lenient, it will for example
accept X47 15 36.75 instead of N47° 15' 36.75"
as latitude. It does not check that the coordinate starts with
a valid direction, or that the proper separators are used.

I don't know what the final part +001906.00 stands for, but that
is ignored completely in your code.

There is also a flaw in your conversion, the fractional part
of the seconds is ignored, e.g. 36.75" is taken as 36 seconds.

I would suggest to use Scanner instead, which makes it relatively
easy to check for a valid input and simultaneously
parse the values into variables. To distinguish
between latitude and longitude, two arguments for the positive
and the negative direction are passed to the helper method.

The complete code then looks like this:

struct PLNWaypointCoordinate {
    var latitude: Double
    var longitude: Double

    init(latitude: Double, longitude: Double) {
        self.latitude = latitude
        self.longitude = longitude
    }

    init?(coordinateString: String) {
        let components = coordinateString.components(separatedBy: ",")
        guard components.count >= 2,
            let latitude = PLNWaypointCoordinate.convertCoordinate(coordinate: components[0],
                                                                   positiveDirection: "N",
                                                                   negativeDirection: "S"),
            let longitude = PLNWaypointCoordinate.convertCoordinate(coordinate: components[1],
                                                                    positiveDirection: "E",
                                                                    negativeDirection: "W")
            else {
                return nil
        }
        self.init(latitude: latitude, longitude: longitude)
    }

    private static func convertCoordinate(coordinate: String,
                                          positiveDirection: String,
                                          negativeDirection: String) -> Double? {
        // Determine the sign from the first character:
        let sign: Double
        let scanner = Scanner(string: coordinate)
        if scanner.scanString(positiveDirection, into: nil) {
            sign = 1.0
        } else if scanner.scanString(negativeDirection, into: nil) {
            sign = -1.0
        } else {
            return nil
        }

        // Parse degrees, minutes, seconds:
        var degrees = 0
        var minutes = 0
        var seconds = 0.0
        guard scanner.scanInt(°rees),         // Degrees (integer),
            scanner.scanString("°", into: nil),  // followed by °,
            scanner.scanInt(&minutes),           // minutes (integer)
            scanner.scanString("'", into: nil),  // followed by '
            scanner.scanDouble(&seconds),        // seconds (floating point),
            scanner.scanString("\"", into: nil), // followed by ",
            scanner.isAtEnd                      // and nothing else.
            else { return nil }

        return sign * (Double(degrees) + Double(minutes)/60.0 + seconds/3600.0)
    }
}

Code Snippets

var latitude: Double = 0.0
var longitude: Double = 0.0
var latitude = 0.0
var longitude = 0.0
struct PLNWaypointCoordinate {
    var latitude: Double
    var longitude: Double

    init(latitude: Double, longitude: Double) {
        self.latitude = latitude
        self.longitude = longitude
    }

    // ...
}
let c = PLNWaypointCoordinate(latitude: ..., longitude: ...)
init?(coordinateString: String) { ... }

Context

StackExchange Code Review Q#153291, answer score: 5

Revisions (0)

No revisions yet.