Skip to content

Instantly share code, notes, and snippets.

@mscdex
Forked from jgrahamc/loc_parser.go
Last active August 29, 2015 14:22
Show Gist options
  • Save mscdex/c51f683593e8670a7ee6 to your computer and use it in GitHub Desktop.
Save mscdex/c51f683593e8670a7ee6 to your computer and use it in GitHub Desktop.

Revisions

  1. @jgrahamc jgrahamc created this gist Mar 27, 2014.
    220 changes: 220 additions & 0 deletions loc_parser.go
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,220 @@
    // loc_parser: functions to parse the textual part of a LOC record
    // stored in our DNS. The key function here is parseLOCString which
    // should be passed a dns.LOC and a string containing the latitude,
    // longitude etc.
    //
    // This is an implementation of RFC 1876. Read it for background as
    // the format in a dns.LOC is slightly unusual.
    //
    // Copyright (c) 2014 CloudFlare, Inc.

    package loc

    import (
    "github.com/cloudflare/dns"
    "regexp"
    "strconv"
    )

    // locReD is the regexp to capture a value in a latitude or longitude.
    //
    // locReM is for the other values (they can be negative) and can have
    // an optional 'm' after. locReOM is an optional version of
    // locReM. Note that the m character after a number has no meaning at
    // all, the values are always in metres.

    var locReD = "(\\d+)(?: (\\d+))?(?: (\\d+(?:\\.\\d+)?))?"
    var locReM = "(?: (-?\\d+(?:\\.\\d+)?)m?)"
    var locReOM = locReM + "?"
    var locReString = locReD + " (N|S) " + locReD + " (E|W)" + locReM +
    locReOM + locReOM + locReOM

    // Note that we are ignoring the error on the Compile() here. The
    // regular expression is static and this will compile. If it doesn't
    // the entire program is borked.

    var locRe, _ = regexp.Compile(locReString)

    // parseSizePrecision parses the siz, hp and vp parts of a LOC string
    // and returns them in the weird 8 bit format required. See RFC 1876
    // for specification and justification. The p string contains the LOC
    // value to parse. It may be empty in which case the default value d
    // is returned. The boolean return is false if the parsing fails.
    func parseSizePrecision(p string, d uint8) (uint8, bool) {
    if p == "" {
    return d, true
    }

    f, err := strconv.ParseFloat(p, 64)
    if err != nil || f < 0 || f > 90000000 {
    return 0, false
    }

    // Conversion from m to cm

    f *= 100

    var exponent uint8 = 0
    for f >= 10 {
    exponent += 1
    f /= 10
    }

    // Here both f and exponent will be in the range 0 to 9 and these
    // get packed into a byte in the following manner. The result?
    // Look at the value in hex and you can read it. e.g. 6e3 (i.e.
    // 6,000) is 0x63.

    return uint8(f) << 4 + exponent, true
    }

    // parseLatLong parses a latitude/longitude string (see ParseString
    // below for format) and returns the value as a single uint32. If the
    // bool value is false there was a problem with the format. The limit
    // parameter specifies the limit for the number of degrees.
    func parseLatLong(d, m, s string, limit uint64) (uint32, bool) {
    n, err := strconv.ParseUint(d, 10, 8)
    if err != nil || n > limit {
    return 0, false
    }
    pos := float64(n) * 60

    if m != "" {
    n, err = strconv.ParseUint(m, 10, 8)
    if err != nil || n > 59 {
    return 0, false
    }
    pos += float64(n)
    }

    pos *= 60

    if s != "" {
    f, err := strconv.ParseFloat(s, 64)
    if err != nil || f > 59.999 {
    return 0, false
    }
    pos += f
    }

    pos *= 1000

    return uint32(pos), pos <= float64(limit * dns.LOC_DEGREES)
    }

    // parseLOCString parses the string representation of a LOC record and
    // fills in the fields in a newly created LOC appropriately. If the
    // function returns nil then there was a parsing error, otherwise
    // returns a pointer to a new LOC.
    //
    // Worth reading RFC 1876, Appendix A to understand.
    func parseLOCString(l string) *dns.LOC {
    loc := new(dns.LOC)

    // The string l will be in the following format:
    //
    // d1 [m1 [s1]] {"N"|"S"} d2 [m2 [s2]] {"E"|"W"}
    // alt["m"] [siz["m"] [hp["m"] [vp["m"]]]]
    //
    // d1 is the latitude, d2 is the longitude, alt is the altitude,
    // siz is the size of the planet, hp and vp are the horiz and vert
    // precisions. See RFC 1876 for full detail.
    //
    // Examples:
    //
    // 42 21 54 N 71 06 18 W -24m 30m
    // 42 21 43.952 N 71 5 6.344 W -24m 1m 200m
    // 52 14 05 N 00 08 50 E 10m
    // 2 7 19 S 116 2 25 E 10m
    // 42 21 28.764 N 71 00 51.617 W -44m 2000m
    // 59 N 10 E 15.0 30.0 2000.0 5.0

    parts := locRe.FindStringSubmatch(l)
    if parts == nil {
    return nil
    }

    // Quick reference to the matches
    //
    // parts[1] == latitude degrees
    // parts[2] == latitude minutes (optional)
    // parts[3] == latitude seconds (optional)
    // parts[4] == N or S
    //
    // parts[5] == longitude degrees
    // parts[6] == longitude minutes (optional)
    // parts[7] == longitude seconds (optional)
    // parts[8] == E or W
    //
    // parts[9] == altitude
    //
    // These are completely optional:
    //
    // parts[10] == size
    // parts[11] == horizontal precision
    // parts[12] == vertical precision

    // Convert latitude and longitude to a 32-bit unsigned integer

    latitude, ok := parseLatLong(parts[1], parts[2], parts[3], 90)
    if !ok {
    return nil
    }

    loc.Latitude = dns.LOC_EQUATOR
    if parts[4] == "N" {
    loc.Latitude += latitude
    } else {
    loc.Latitude -= latitude
    }

    longitude, ok := parseLatLong(parts[5], parts[6], parts[7], 180)
    if !ok {
    return nil
    }

    loc.Longitude = dns.LOC_PRIMEMERIDIAN
    if parts[8] == "E" {
    loc.Longitude += longitude
    } else {
    loc.Longitude -= longitude
    }

    // Now parse the altitude. Seriously, read RFC 1876 if you want to
    // understand all the values and conversions here. But altitudes
    // are unsigned 32-bit numbers that start 100,000m below 'sea
    // level' and are expressed in cm.
    //
    // == (2^32-1)/100
    // - 100,000
    // == 42949672.95
    // - 100000
    // == 42849672.95

    f, err := strconv.ParseFloat(parts[9], 64)
    if err != nil || f < -dns.LOC_ALTITUDEBASE || f > 42849672.95 {
    return nil
    }

    loc.Altitude = (uint32)((f + dns.LOC_ALTITUDEBASE) * 100)

    // Default values for the optional components, see RFC 1876 for
    // this weird encoding. But top nibble is mantissa, bottom nibble
    // is exponent. Values are in cm. So, for example, 0x12 means 1 *
    // 10^2 or 100cm.

    // 0x12 == 1e2cm == 1m
    if loc.Size, ok = parseSizePrecision(parts[10], 0x12); !ok {
    return nil
    }
    // 0x16 == 1e6cm == 10,000m == 10km
    if loc.HorizPre, ok = parseSizePrecision(parts[11], 0x16); !ok {
    return nil
    }
    // 0x13 == 1e3cm == 10m
    if loc.VertPre, ok = parseSizePrecision(parts[12], 0x13); !ok {
    return nil
    }

    return loc
    }
    286 changes: 286 additions & 0 deletions loc_parser_test.go
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,286 @@
    package loc

    import (
    "strings"
    "testing"
    )

    func TestParseSizePrecision(t *testing.T) {
    n, ok := parseSizePrecision("", 0x98)
    if n != 0x98 || !ok {
    t.Error("")
    }

    n, ok = parseSizePrecision("-1", 0x98)
    if ok {
    t.Error("")
    }

    n, ok = parseSizePrecision("ddd", 0x98)
    if ok {
    t.Error("")
    }

    n, ok = parseSizePrecision("100000000", 0x98)
    if ok {
    t.Error("")
    }

    n, ok = parseSizePrecision("0", 0x98)
    if !ok || n != 0x00 {
    t.Error("0")
    }

    n, ok = parseSizePrecision("0.1", 0x98)
    if !ok || n != 0x11 {
    t.Error("0.1")
    }

    n, ok = parseSizePrecision("900", 0x98)
    if !ok || n != 0x94 {
    t.Error("900")
    }

    n, ok = parseSizePrecision("900.1", 0x98)
    if !ok || n != 0x94 {
    t.Error("900")
    }

    n, ok = parseSizePrecision("12345.8", 0x98)
    if !ok || n != 0x16 {
    t.Error("12345.8")
    }

    n, ok = parseSizePrecision("90000000", 0x98)
    if !ok || n != 0x99 {
    t.Error("9000000")
    }
    }

    func TestParseLatLong(t *testing.T) {
    _, ok := parseLatLong("", "", "", 90)
    if ok {
    t.Error("")
    }

    _, ok = parseLatLong("a", "b", "c", 90)
    if ok {
    t.Error("")
    }

    _, ok = parseLatLong("50", "1.2", "0", 90)
    if ok {
    t.Error("")
    }

    l, ok := parseLatLong("90", "", "", 90)
    if !ok || l != 90*60*60*1000 {
    t.Error("90")
    }

    l, ok = parseLatLong("90", "", "", 89)
    if ok {
    t.Error("90 > 89")
    }

    l, ok = parseLatLong("90", "1", "0", 90)
    if ok {
    t.Error("90 > 90 1 0")
    }

    l, ok = parseLatLong("90", "0", "0", 90)
    if !ok || l != 90*60*60*1000 {
    t.Error("90 0 0")
    }

    l, ok = parseLatLong("0", "0", "0", 90)
    if !ok || l != 0 {
    t.Error("0 0 0")
    }

    l, ok = parseLatLong("89", "2", "3", 90)
    if !ok || l != ((89*60+2)*60+3)*1000 {
    t.Error("89 2 3")
    }

    l, ok = parseLatLong("54", "4", "3.8", 90)
    if !ok || l != ((54*60+4)*60+3.8)*1000 {
    t.Error("54 4 3.8")
    }
    }

    func TestParseString(t *testing.T) {
    l := parseLOCString("")
    if l != nil {
    t.Error("")
    }
    l = parseLOCString("random stuff")
    if l != nil {
    t.Error("")
    }
    l = parseLOCString("0 N")
    if l != nil {
    t.Error("")
    }
    l = parseLOCString("0 N 0 E 0")
    if l == nil {
    t.Error("")
    }
    if l.Latitude != 1<<31 || l.Longitude != 1<<31 || l.Altitude != 10000000 {
    t.Error("0 N 0 E 0")
    }
    if l.Size != 0x12 || l.HorizPre != 0x16 || l.VertPre != 0x13 {
    t.Error("")
    }

    l = parseLOCString("89 N 23 E 0")
    if l == nil {
    t.Error("")
    }
    if l.Latitude != 1<<31+89*60*60*1000 || l.Longitude != 1<<31+23*60*60*1000 || l.Altitude != 10000000 {
    t.Error("89 N 23 E 0")
    }
    if l.Size != 0x12 || l.HorizPre != 0x16 || l.VertPre != 0x13 {
    t.Error("")
    }

    l = parseLOCString("89 S 23 W 0")
    if l == nil {
    t.Error("")
    }
    if l.Latitude != 1<<31-89*60*60*1000 || l.Longitude != 1<<31-23*60*60*1000 || l.Altitude != 10000000 {
    t.Error("89 N 23 E 0")
    }
    if l.Size != 0x12 || l.HorizPre != 0x16 || l.VertPre != 0x13 {
    t.Error("")
    }

    l = parseLOCString("89 2 1.4 N 23 6 9.8 E 0")
    if l == nil {
    t.Error("")
    }
    if l.Latitude != 1<<31+((89*60+2)*60+1.4)*1000 || l.Longitude != 1<<31+((23*60+6)*60+9.8)*1000 || l.Altitude != 10000000 {
    t.Error("89 2 1.4 N 23 6 9.8 E 0")
    }
    if l.Size != 0x12 || l.HorizPre != 0x16 || l.VertPre != 0x13 {
    t.Error("")
    }

    l = parseLOCString("59 N 10 E 15.0 30.0 2000.0 5.0")
    if l == nil {
    t.Error("")
    }
    if l.Latitude != 1<<31+59*60*60*1000 || l.Longitude != 1<<31+10*60*60*1000 || l.Altitude != 10001500 {
    t.Error("59 N 10 E 15.0 30.0 2000.0 5.0")
    }
    if l.Size != 0x33 || l.HorizPre != 0x25 || l.VertPre != 0x52 {
    t.Error("")
    }

    l = parseLOCString("42 21 54 N 71 06 18 W -24m 30m")
    if l == nil {
    t.Error("")
    }
    if l.Latitude != 1<<31+((42*60+21)*60+54)*1000 || l.Longitude != 1<<31-((71*60+6)*60+18)*1000 || l.Altitude != 10000000-24*100 {
    t.Error("42 21 54 N 71 06 18 W -24m 30m")
    }
    if l.Size != 0x33 || l.HorizPre != 0x16 || l.VertPre != 0x13 {
    t.Error("")
    }

    l = parseLOCString("42 21 43.952 N 71 5 6.344 W -24m 1m 200m")
    if l == nil {
    t.Error("")
    }
    if l.Latitude != 1<<31+((42*60+21)*60+43.952)*1000 || l.Longitude != 1<<31-((71*60+5)*60+6.344)*1000 || l.Altitude != 10000000-24*100 {
    t.Errorf("42 21 43.952 N 71 5 6.344 W -24m 1m 200m")
    }
    if l.Size != 0x12 || l.HorizPre != 0x24 || l.VertPre != 0x13 {
    t.Error("")
    }

    l = parseLOCString("52 14 05 N 00 08 50 E 10m")
    if l == nil {
    t.Error("")
    }
    if l.Latitude != 1<<31+((52*60+14)*60+5)*1000 || l.Longitude != 1<<31+((0*60+8)*60+50)*1000 || l.Altitude != 10000000+10*100 {
    t.Errorf("52 14 05 N 00 08 50 E 10m")
    }
    if l.Size != 0x12 || l.HorizPre != 0x16 || l.VertPre != 0x13 {
    t.Error("")
    }

    l = parseLOCString("2 7 19 S 116 2 25 E 10m")
    if l == nil {
    t.Error("")
    }
    if l.Latitude != 1<<31-((2*60+7)*60+19)*1000 || l.Longitude != 1<<31+((116*60+2)*60+25)*1000 || l.Altitude != 10000000+10*100 {
    t.Errorf("2 7 19 S 116 2 25 E 10m")
    }
    if l.Size != 0x12 || l.HorizPre != 0x16 || l.VertPre != 0x13 {
    t.Error("")
    }

    l = parseLOCString("42 21 28.764 N 71 00 51.617 W -44m 2000m")
    if l == nil {
    t.Error("")
    }
    if l.Latitude != 1<<31+((42*60+21)*60+28.764)*1000 || l.Longitude != 1<<31-((71*60+0)*60+51.617)*1000 || l.Altitude != 10000000-44*100 {
    t.Errorf("42 21 28.764 N 71 00 51.617 W -44m 2000m")
    }
    if l.Size != 0x25 || l.HorizPre != 0x16 || l.VertPre != 0x13 {
    t.Error("")
    }
    }

    func TestStringLOC(t *testing.T) {
    l := parseLOCString("59 N 10 E 15.0 30.0 2000.0 5.0")
    if l == nil {
    t.Error("")
    }
    if !strings.HasSuffix(l.String(), "59 00 0.000 N 10 00 0.000 E 15m 30m 2000m 5m") {
    t.Error("")
    }
    l = parseLOCString("42 21 54 N 71 06 18 W -24m 30m")
    if l == nil {
    t.Error("")
    }
    if !strings.HasSuffix(l.String(), "42 21 54.000 N 71 06 18.000 W -24m 30m 10000m 10m") {
    t.Error("")
    }
    l = parseLOCString("42 21 43.952 N 71 5 6.344 W -24m 1m 200m")
    if l == nil {
    t.Error("")
    }
    if !strings.HasSuffix(l.String(), "42 21 43.952 N 71 05 6.344 W -24m 1m 200m 10m") {
    t.Error("")
    }
    l = parseLOCString("52 14 05 N 00 08 50 E 10m")
    if l == nil {
    t.Error("")
    }
    if !strings.HasSuffix(l.String(), "52 14 5.000 N 00 08 50.000 E 10m 1m 10000m 10m") {
    t.Error("")
    }
    l = parseLOCString("2 7 19 S 116 2 25 E 10m")
    if l == nil {
    t.Error("")
    }
    if !strings.HasSuffix(l.String(), "02 07 19.000 S 116 02 25.000 E 10m 1m 10000m 10m") {
    t.Error("")
    }
    l = parseLOCString("42 21 28.764 N 71 00 51.617 W -44m 2000m")
    if l == nil {
    t.Error("")
    }
    if !strings.HasSuffix(l.String(), "42 21 28.764 N 71 00 51.617 W -44m 2000m 10000m 10m") {
    t.Error("")
    }
    l = parseLOCString("42 21 28.764 N 71 00 51.617 W -44.55m 2000m")
    if l == nil {
    t.Error("")
    }
    if !strings.HasSuffix(l.String(), "42 21 28.764 N 71 00 51.617 W -44.55m 2000m 10000m 10m") {
    t.Error(l.String())
    }
    }