Programing: Decimalize Latitude Longitude

By Xah Lee. Date: . Last updated: .

Here is a programing exercise, with solution's in emacs lisp, python, ruby.

Problem

Write a function “latitude-longitude-decimalize”.

It should take a string like this: "37°26′36.42″N 06°15′14.28″W". The return value should be a pair of numbers, like this: [37.44345 -6.25396].

Feel free to use { Perl, Python, Ruby, PHP, Emacs Lisp, Java, …}

Solutions

Emacs Lisp

solution by Xah Lee.

(defun latitude-longitude-decimalize-xah (lat-lon-str)
  "Convert latitude longitude string LAT-LON-STR in minutes second format to decimal.

For example: 「\"37°26′36.42″N 06°15′14.28″W\"」
becomes 「[37.44345 -6.253966666666667]」"
  (interactive)
  (let ($tmpPair (tt2 lat-lon-str) $latStr $latNum $lonStr $lonNum
 $deg $min $sec $sign ($c (/ 1.0 60.0))
)

    (setq tt2 (replace-regexp-in-string "''" "″" tt2 t t) )
    (setq tt2 (replace-regexp-in-string "\"" "″" tt2 t t) )
    (setq tt2 (replace-regexp-in-string "'" "′" tt2 t t) )

    (setq $tmpPair (split-string tt2 " +"))
    (when (not (equal (length $tmpPair) 2)) (user-error "Error: input can contain only one space"))

    (setq $latStr (elt $tmpPair 0))
    (setq $lonStr (elt $tmpPair 1))
    (if (string-match "\\`\\([0-9]+\\)°\\([0-9]+\\)′\\([.0-9]+\\)″\\(.?\\)\\'" $latStr )
        (progn
            (setq $deg  (string-to-number (match-string 1 $latStr) ))
            (setq $min  (string-to-number (match-string 2 $latStr) ))
            (setq $sec  (string-to-number (match-string 3 $latStr) ))
            (setq $sign (match-string 4 $latStr))
            (setq $latNum (+ $deg (* (+ $min (* $sec $c)) $c)))
            (cond
                 ((string= (downcase $sign) "n") nil)
                 ((string= $sign "") nil)
                 ((string= (downcase $sign) "s") (setq $latNum (* -1 $latNum) ))
                 (t (user-error "Your input is malformed. Your latitude ends with a char that's not N or S"))
                 )
            )
      (progn (user-error "Your latitude is malformed") )
      )

(if (string-match "\\`\\([0-9]+\\)°\\([0-9]+\\)′\\([.0-9]+\\)″\\(.?\\)\\'" $lonStr )
        (progn
            (setq $deg  (string-to-number (match-string 1 $lonStr) ))
            (setq $min  (string-to-number (match-string 2 $lonStr) ))
            (setq $sec  (string-to-number (match-string 3 $lonStr) ))
            (setq $sign (match-string 4 $lonStr))
            (setq $lonNum (+ $deg (* (+ $min (* $sec $c)) $c)))
            (cond
                 ((string= (downcase $sign) "e") nil)
                 ((string= $sign "") nil)
                 ((string= (downcase $sign) "w") (setq $lonNum (* -1 $lonNum) ))
                 (t (user-error "Your input is malformed. Your longitude ends with a char that's not E or W"))
                 )
            )
      (progn (user-error "Your longitude is malformed") )
      )
(vector $latNum $lonNum) ) )

(message "%S" (latitude-longitude-decimalize-xah "37°26′36.42″N 06°15′14.28″W"))
;; "[37.44345 -6.253966666666667]"

solution by Jon Snader. http://irreal.org/blog/?p=795

;; -*- coding: utf-8 -*-
;; by Jon Snader 2012-04-24 http://irreal.org/blog/?p=795

(require 'cl)

(defun latitude-longitude-decimalize-jcs (lat-lon-str)
  "Decimalize latitude longitude
 \"37°26′36.42″N 06°15′14.28″W\" ⇒ (37.44345 -6.253966666666667)"
  (let (result)
    (dolist (ll (split-string lat-lon-str " +"))
      (destructuring-bind (deg min sec dir)
          (split-string ll "[°′″]")
        (if (string= "" dir) (error "malformed lat-lon-str %s" ll))
        (let ((factor (if (member dir '("S" "W")) -1 1)))
          (push (* factor (+ (string-to-number deg)
                             (/ (string-to-number min) 60.0)
                             (/ (string-to-number sec) 3600.0))) result))))
    (reverse result)))

(message "%S" (latitude-longitude-decimalize-jcs "37°26′36.42″N 06°15′14.28″W"))
;; (37.44345 -6.253966666666667)

[Jorge A. Alfaro Murillo https://plus.google.com/108846296933670938573/posts] http://pastebin.com/16d67W71

;; -*- coding: utf-8 -*-

;; by Jorge A. Alfaro Murillo 2012-04-26 https://plus.google.com/+JorgeAAlfaroMurillo/posts http://pastebin.com/16d67W71

;; Answer to Programing Problem: Decimalize Latitude Longitude.
;; Found in http://xahlee.blogspot.com/2012/04/programing-problem decimalize-latitude.html

;; it is very elisp in that it uses a regexp for splitting the
;; strings, and the fact that characters are also numbers in elisp:
;; N=78<S=83 and E=69<W=87

(defun latitude-longitude-decimalize-jaam (lat-lon-str)
  "Decimalize latitude longitude. Converts a string of the form \"37°26′36.42″N 06°15′14.28″W\" to a list with two numbers of the form (37.44345 -6.253966666666667)."
  (let* ((lat-strings (split-string (car (split-string lat-lon-str)) "[^0-9A-Z.]+"))
         (lon-strings (split-string (cadr (split-string lat-lon-str)) "[^0-9A-Z.]+"))
         (lat-sign (- 1 (* 2(/ (string-to-char (nth 3 lat-strings)) 83))))
         (lon-sign (- 1 (* 2(/ (string-to-char (nth 3 lon-strings)) 87))))
         (lat (* lat-sign
                 (+ (string-to-number (nth 0 lat-strings))
                    (/ (string-to-number (nth 1 lat-strings)) 60.0)
                    (/ (string-to-number (nth 2 lat-strings)) (* 60.0 60.0)))))
         (lon (* lon-sign
                 (+ (string-to-number (nth 0 lon-strings))
                    (/ (string-to-number (nth 1 lon-strings)) 60.0)
                    (/ (string-to-number (nth 2 lon-strings)) (* 60.0 60.0))))))
    (list lat lon)))

(message "%s" (latitude-longitude-decimalize-jaam "37°26′36.42″N 06°15′14.28″W"))
;; "(37.44345 -6.253966666666667)"

Mickey's solution ( http://www.masteringemacs.org/articles/2012/04/25/fun-emacs-calc/ ) This solution didn't account North/West sign.

;; -*- coding: utf-8 -*-
;; by Mickey 2012-04-25 http://www.masteringemacs.org/articles/2012/04/25/fun-emacs-calc/

(defun latitude-longitude-decimalize-mickey (lat-lon-str)
  (let ((hms (split-string lat-lon-str "[°′″NW ]" t)))
    (flet ((to-deg ()
                   (string-to-number
                    (calc-eval (format "deg(%s@ %s' %s\")"
                                       (pop hms) (pop hms) (pop hms))))))
      (list (to-deg) (to-deg)))))

(message "%s" (latitude-longitude-decimalize-mickey "37°26′36.42″N 06°15′14.28″W"))
;; "(37.44345 6.25396666667)"

emacs calc solution by Aaron Hawley. This solution does not qualify, but interesting anyway.

C-x * * [37@26'36.6",_6@15'14.28"] m d V M c d

Python

[Kurt Schwehr https://plus.google.com/102101513984015207006/posts]'s solution in python, at http://schwehr.org/blog/archives/2011-11.html .

# -*- coding: utf-8 -*-
# 2011-11 by Kurt Schwehr http://schwehr.org/blog/archives/2011-11.html
# 2014-01-21 trivial mod to accept Unicode input by Xah Lee

# Wish I'd been up for writing an elisp version!  This is from 2011-11.  My blog is offline at the moment.  -kurt

# https://plus.google.com/u/0/113859563190964307534/posts/aqmFrjcY7WH
# Here is my answers to emacs lisp exercise: latitude-longitude-decimalize. The first is dumb. It hard codes the characters, which I think source-highlight mangled pretty badly. The code wasn't very pretty to start with. The second solution is much more robust. It allows and character(s) to be the separator(s).

def latitude_longitude_decimalize_kurt1(lat_lon_str):
    lat_str, lon_str = lat_lon_str.split()
    lat = int(lat_str.split(u'°')[0])
    lat += int(lat_str.split(u'°')[1].split(u'′')[0]) / 60.
    lat += float(lat_str.split(u'′')[1][:-4]) / 3600.
    if 'S' in lat_str:
        lat = -lat

    lon = int(lon_str.split(u'°')[0])
    lon += int(lon_str.split(u'°')[1].split(u'′')[0]) / 60.
    lon += float(lon_str.split(u'′')[1][:-4]) / 3600.
    if 'W' in lon_str:
        lon = -lon

    return lat,lon

print latitude_longitude_decimalize_kurt1( u"37°26′36.42″N 06°15′14.28″W")
# (37.44333333333333, -6.253888888888889)

Kurt's second solution, using regex. (note: it didn't take account of North/South/East/West)

# -*- coding: utf-8 -*-
# by Kurt Schwehr 2011-11 http://schwehr.org/blog/archives/2011-11.html

# this solution is much more robust. It allows and character(s) to be the separator(s).

import re

rawstr = r"""(?P<lat_deg>\d{1,2})\D+
(?P<lat_min>\d{1,2})\D+
(?P<lat_sec>\d{1,2}(\.\d+))\D+
(?P<lat_hemi>[NS])
\s+
(?P<lon_deg>\d{1,3})\D+
(?P<lon_min>\d{1,2})\D+
(?P<lon_sec>\d{1,2}(\.\d+))\D+
(?P<lon_hemi>[EW])
"""

compile_obj = re.compile(rawstr,  re.VERBOSE)

def decimalize_latitude_longitude_kurt2(lat_lon_str):
    g = compile_obj.search(lat_lon_str).groupdict() # m is match
    lat = int(g['lat_deg']) + int(g['lat_min'])/60. + float(g['lat_sec']) / 3600.
    if g['lat_hemi'] == 'S':
        lat = -lat
    lon = int(g['lon_deg']) + int(g['lon_min'])/60. + float(g['lon_sec']) / 3600.
    if g['lon_hemi'] == 'S':
        lon = -lon
    return {'y':lat, 'x':lon}

print decimalize_latitude_longitude_kurt2(u"37°26′36.42″N 06°15′14.28″W")
# {'y': 37.44345, 'x': 6.253966666666667}

Python 3

By Ian Kelly. . http://groups.google.com/group/comp.lang.python/msg/ce03aab1d5864c01

# Python 3

# by Ian Kelly. 2011-11-29 http://groups.google.com/group/comp.lang.python/msg/ce03aab1d5864c01
# trivially modified by Xah Lee for unicode input

import re

def decimalize_latitude_longitude_Ian_Kelly(lat_lon_str):
    regex = r"""(\d+)°(\d+)′([\d+.]+)″([NS])\s*(\d+)°(\d+)′([\d+.]+)″([EW])"""
    match = re.match(regex, lat_lon_str)
    if not match:
        raise ValueError("Invalid input string: {0:r}".format(lat_lon_str))
    def decimalize(degrees, minutes, seconds, direction):
        decimal = int(degrees) + int(minutes) / 60 + float(seconds) / 3600
        if direction in 'SW':
            decimal = -decimal
        return decimal
    latitude = decimalize(*match.groups()[:4])
    longitude = decimalize(*match.groups()[4:8])
    return latitude, longitude

print(decimalize_latitude_longitude_Ian_Kelly("37°26′36.42″N 06°15′14.28″W"))
# (37.44345, -6.253966666666667)

Ruby

port of Kurt's python code to ruby. by [Jean-Sébastien Ney https://plus.google.com/114233876886322311595/posts]. https://gist.github.com/49658505d40e11cb1769

# -*- coding: utf-8 -*-
# Ruby

# by Jean-Sébastien Ney https://plus.google.com/114233876886322311595/posts https://gist.github.com/49658505d40e11cb1769

# Decimalize latitude longitude
# "37°26′36.42″N 06°15′14.28″W" ⇒ (37.44345 -6.253966666666667)

$regex = %r{
  (?<lat_deg>\d{1,2})\D+
  (?<lat_min>\d{1,2})\D+
  (?<lat_sec>\d{1,2}(\.\d+))\D+
  (?<lat_hemi>[NS])
  \s+
  (?<lng_deg>\d{1,3})\D+
  (?<lng_min>\d{1,2})\D+
  (?<lng_sec>\d{1,2}(\.\d+))\D+
  (?<lng_hemi>[EW])
}x

def decimalize_latitude_longitude_jsn(lat_lon_str)
  if g = $regex.match(lat_lon_str)
    lat = g['lat_deg'].to_i + g['lat_min'].to_f/60 + g['lat_sec'].to_f/3600
    lat = -lat if g['lat_hemi'] == 'S'
    lng = g['lng_deg'].to_i + g['lng_min'].to_f/60 + g['lng_sec'].to_f/3600
    lng = -lng if g['lng_hemi'] == 'W'
    {y:lat, x:lng}
  end
end

p decimalize_latitude_longitude_jsn "37°26′36.42″N 06°15′14.28″W"
# {:y=>37.44345, :x=>-6.253966666666667}

Test Data

Las Vegas
36°10′30″N 115°08′11″W
36.175  -115.136389

NYC
40°39′51″N 73°56′19″W
40.664167 -73.938611

Shanghai
31°12′0″N 121°30′0″E
31.2 121.5

London
51°30′26″N 0°7′39″W
51.507222 -0.1275

Tokyo
35°41′22.22″N 139°41′30.12″E
35.689506 139.6917

SF
37°46′45.48″N 122°25′9.12″W
37.7793 -122.4192