diff options
author | Arne Juul <arnej@verizonmedia.com> | 2020-07-08 08:06:54 +0000 |
---|---|---|
committer | Arne Juul <arnej@verizonmedia.com> | 2020-07-15 15:39:19 +0000 |
commit | 576b4e2d9b35440d7cf0e6ccf19213b2d5cd6102 (patch) | |
tree | baa50399b1e794279d8796846ff512a9acb1a671 /vespajlib | |
parent | 032ada87b533a581d1ea05f99d6d88146b0b5b5c (diff) |
add simpler utility for degree parsing
Diffstat (limited to 'vespajlib')
-rw-r--r-- | vespajlib/src/main/java/com/yahoo/geo/ParseDegree.java | 267 | ||||
-rw-r--r-- | vespajlib/src/test/java/com/yahoo/geo/ParseDegreeTestCase.java | 266 |
2 files changed, 533 insertions, 0 deletions
diff --git a/vespajlib/src/main/java/com/yahoo/geo/ParseDegree.java b/vespajlib/src/main/java/com/yahoo/geo/ParseDegree.java new file mode 100644 index 00000000000..fe783b95414 --- /dev/null +++ b/vespajlib/src/main/java/com/yahoo/geo/ParseDegree.java @@ -0,0 +1,267 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +package com.yahoo.geo; + +/** + * utility for parsing geographical coordinates + * + * @author arnej27959 + **/ +public class ParseDegree { + /** + * the parsed latitude (degrees north if positive) + **/ + public double latitude = 0; + public boolean foundLatitude = false; + + /** + * the parsed longitude (degrees east if positive) + **/ + public double longitude = 0; + public boolean foundLongitude = false; + + public static boolean isDigit(char ch) { + return (ch >= '0' && ch <= '9'); + } + public static boolean isCompassDirection(char ch) { + return (ch == 'N' || ch == 'S' || ch == 'E' || ch == 'W'); + } + + private String parseString = null; + private int len = 0; + private int pos = 0; + + private char getNextChar() throws IllegalArgumentException { + if (pos == len) { + pos++; + return 0; + } else if (pos > len) { + throw new IllegalArgumentException("position after end of string"); + } else { + return parseString.charAt(pos++); + } + } + + /** + * Parse the given string. + * + * The string must contain either a latitude or a longitude. + * A latitude should contain "N" or "S" and a number signifying + * degrees north or south, or a signed number. + * A longitude should contain "E" or "W" and a number + * signifying degrees east or west, or a signed number. + * <br> + * Fractional degrees are recommended as the main input format, + * but degrees plus fractional minutes may be used for testing. + * You can use the degree sign (U+00B0 as seen in unicode at + * http://www.unicode.org/charts/PDF/U0080.pdf) to separate + * degrees from minutes, put the direction (NSEW) between as a + * separator, or use a small letter 'o' as a replacement for the + * degrees sign. + * <br> + * Some valid input formats: <br> + * "37.416383" and "-122.024683" → Sunnyvale <br> + * "N37.416383" and "W122.024683" → Sunnyvale <br> + * "37N24.983" and "122W01.481" → same <br> + * "N37\u00B024.983" and "W122\u00B001.481" → same <br> + * "63.418417" and "10.433033" → Trondheim <br> + * "N63.418417" and "E10.433033" → same <br> + * "N63o25.105" and "E10o25.982" → same <br> + * "E10o25.982" and "N63o25.105" → same <br> + * "N63.418417" and "E10.433033" → same <br> + * "63N25.105" and "10E25.982" → same <br> + * @param assume_n_s Latitude assumed, otherwise longitude + * @param latorlong Latitude or longitude + * + **/ + public ParseDegree(boolean assume_n_s, String lat_or_long) throws IllegalArgumentException { + this.parseString = lat_or_long; + this.len = parseString.length(); + consumeString(assume_n_s); + } + + private void consumeString(boolean assume_n_s) throws IllegalArgumentException { + char ch = getNextChar(); + + double degrees = 0.0; + double minutes = 0.0; + double seconds = 0.0; + boolean degSet = false; + boolean minSet = false; + boolean secSet = false; + boolean dirSet = false; + boolean foundDot = false; + boolean foundDigits = false; + + boolean findingLatitude = false; + boolean findingLongitude = false; + + double sign = +1.0; + + int lastpos = -1; + + // sign must be first character in string if present: + if (ch == '+') { + // unary plus is a nop + ch = getNextChar(); + } else if (ch == '-') { + sign = -1.0; + ch = getNextChar(); + } + do { + // did we find a valid char? + boolean valid = false; + if (pos == lastpos) { + throw new RuntimeException("internal logic error at '"+parseString+"' pos:"+pos); + } else { + lastpos = pos; + } + + // first, see if we can find some number + double accum = 0.0; + + if (isDigit(ch) || ch == '.') { + valid = true; + if (foundDigits) { + throw new IllegalArgumentException("found digits after not consuming previous digits"); + } + double divider = 1.0; + foundDot = false; + while (isDigit(ch)) { + foundDigits = true; + accum *= 10; + accum += (ch - '0'); + ch = getNextChar(); + } + if (ch == '.') { + foundDot = true; + ch = getNextChar(); + while (isDigit(ch)) { + foundDigits = true; + accum *= 10; + accum += (ch - '0'); + divider *= 10; + ch = getNextChar(); + } + } + if (!foundDigits) { + throw new IllegalArgumentException("just a . is not a valid number"); + } + accum /= divider; + } + + // next, did we find a separator after the number? + // degree sign is a separator after degrees, before minutes + if (ch == '\u00B0' || ch == 'o') { + valid = true; + if (degSet) { + throw new IllegalArgumentException("degrees sign only valid just after degrees"); + } + if (!foundDigits) { + throw new IllegalArgumentException("must have number before degrees sign"); + } + if (foundDot) { + throw new IllegalArgumentException("cannot have fractional degrees before degrees sign"); + } + ch = getNextChar(); + } + // apostrophe is a separator after minutes, before seconds + if (ch == '\'') { + if (minSet || !degSet || !foundDigits) { + throw new IllegalArgumentException("minutes sign only valid just after minutes"); + } + if (foundDot) { + throw new IllegalArgumentException("cannot have fractional minutes before minutes sign"); + } + ch = getNextChar(); + } + + // if we found some number, assign it into the next unset variable + if (foundDigits) { + valid = true; + if (degSet) { + if (minSet) { + if (secSet) { + throw new IllegalArgumentException("extra number after full field"); + } else { + seconds = accum; + secSet = true; + } + } else { + minutes = accum; + minSet = true; + if (foundDot) { + secSet = true; + } + } + } else { + degrees = accum; + degSet = true; + if (foundDot) { + minSet = true; + secSet = true; + } + } + foundDot = false; + foundDigits = false; + } + + // there may to be a direction (NSEW) somewhere, too + if (isCompassDirection(ch)) { + valid = true; + if (dirSet) { + throw new IllegalArgumentException("already set direction once, cannot add direction: "+ch); + } + dirSet = true; + if (ch == 'S' || ch == 'W') { + sign = -1; + } else { + sign = 1; + } + if (ch == 'E' || ch == 'W') { + findingLongitude = true; + } else { + findingLatitude = true; + } + ch = getNextChar(); + } + + // lastly, did we find the end-of-string? + if (ch == 0) { + valid = true; + if (!dirSet) { + if (assume_n_s) { + findingLatitude = true; + } else { + findingLongitude = true; + } + } + if (!degSet) { + throw new IllegalArgumentException("end of field without any number seen"); + } + degrees += minutes / 60.0; + degrees += seconds / 3600.0; + degrees *= sign; + + if (findingLatitude) { + if (degrees < -90.0 || degrees > 90.0) { + throw new IllegalArgumentException("out of range [-90,+90]: "+degrees); + } + latitude = degrees; + foundLatitude = true; + } else if (findingLongitude) { + if (degrees < -180.0 || degrees > 180.0) { + throw new IllegalArgumentException("out of range [-180,+180]: "+degrees); + } + longitude = degrees; + foundLongitude = true; + } + break; + } + if (!valid) { + throw new IllegalArgumentException("invalid character: "+ch); + } + } while (ch != 0); + // everything parsed OK + } +} diff --git a/vespajlib/src/test/java/com/yahoo/geo/ParseDegreeTestCase.java b/vespajlib/src/test/java/com/yahoo/geo/ParseDegreeTestCase.java new file mode 100644 index 00000000000..3ca2d7ce4f8 --- /dev/null +++ b/vespajlib/src/test/java/com/yahoo/geo/ParseDegreeTestCase.java @@ -0,0 +1,266 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.geo; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * Tests for the ParseDegree class. + * + * @author arnej27959 + */ +public class ParseDegreeTestCase { + + private static final double delta = 0.000000000001; + + private ParseDegree parser; + + private void checkLat(boolean ans, String to_parse, double expected) { + parser = new ParseDegree(ans, to_parse); + assertEquals(expected, parser.latitude, delta); + assertTrue(parser.foundLatitude); + assertFalse(parser.foundLongitude); + } + private void checkLon(boolean ans, String to_parse, double expected) { + parser = new ParseDegree(ans, to_parse); + assertEquals(expected, parser.longitude, delta); + assertFalse(parser.foundLatitude); + assertTrue(parser.foundLongitude); + } + private void checkLat(String to_parse, double expected) { + checkLat(true, to_parse, expected); + checkLat(false, to_parse, expected); + } + private void checkLon(String to_parse, double expected) { + checkLon(true, to_parse, expected); + checkLon(false, to_parse, expected); + } + + private void checkZeroLat(boolean ans, String to_parse) { + checkLat(ans, to_parse, 0d); + } + + private void checkZeroLon(boolean ans, String to_parse) { + checkLon(ans, to_parse, 0d); + } + + /** + * Tests different inputs that should all produce 0 or -0. + */ + @Test + public void testZero() { + checkZeroLat(true, "0"); + checkZeroLat(true, "0.0"); + checkZeroLat(true, "0o0.0"); + checkZeroLat(true, "0o0'0"); + checkZeroLat(true, "0\u00B00'0"); + + checkZeroLon(false, "0"); + checkZeroLon(false, "0.0"); + checkZeroLon(false, "0o0.0"); + checkZeroLon(false, "0o0'0"); + checkZeroLon(false, "0\u00B00'0"); + + checkZeroLat(false, "N0"); + checkZeroLat(false, "N0.0"); + checkZeroLat(false, "N0\u00B00'0"); + checkZeroLat(false, "S0"); + checkZeroLat(false, "S0.0"); + checkZeroLat(false, "S0o0'0"); + checkZeroLat(false, "S0\u00B00'0"); + + checkZeroLon(true, "E0"); + checkZeroLon(true, "E0.0"); + checkZeroLon(true, "E0\u00B00'0"); + checkZeroLon(true, "W0"); + checkZeroLon(true, "W0.0"); + checkZeroLon(true, "W0o0'0"); + checkZeroLon(true, "W0\u00B00'0"); + } + + /** + * Tests inputs that are close to 0. + */ + @Test + public void testNearZero() { + checkLat("N0.0001", 0.0001); + checkLat("S0.0001", -0.0001); + checkLon("E0.0001", 0.0001); + checkLon("W0.0001", -0.0001); + + checkLat("N0.000001", 0.000001); + checkLat("S0.000001", -0.000001); + checkLon("E0.000001", 0.000001); + checkLon("W0.000001", -0.000001); + + checkLat("N0\u00B00'1", 1/3600d); + checkLat("S0\u00B00'1", -1/3600d); + checkLon("E0\u00B00'1", 1/3600d); + checkLon("W0\u00B00'1", -1/3600d); + } + + /** + * Tests inputs that are close to latitude 90/-90 degrees and longitude 180/-180 degrees. + */ + @Test + public void testNearBoundary() { + checkLat("N89.9999", 89.9999); + checkLat("S89.9999", -89.9999); + checkLon("E179.9999", 179.9999); + checkLon("W179.9999", -179.9999); + + checkLat("N89.999999", 89.999999); + checkLat("S89.999999", -89.999999); + checkLon("E179.999999", 179.999999); + checkLon("W179.999999", -179.999999); + + checkLat("N89\u00B059'59", 89+59/60d+59/3600d); + checkLat("S89\u00B059'59", -(89+59/60d+59/3600d)); + checkLon("E179\u00B059'59", 179+59/60d+59/3600d); + checkLon("W179\u00B059'59", -(179+59/60d+59/3600d)); + } + + /** + * Tests inputs that are on latitude 90/-90 degrees and longitude 180/-180 degrees. + */ + @Test + public void testOnBoundary() { + checkLat("N90", 90d); + checkLat("N90\u00B00'0", 90d); + checkLat("S90", -90d); + checkLat("S90\u00B00'0", -90d); + + checkLon("E180", 180d); + checkLon("E180\u00B00'0", 180d); + checkLon("W180", -180d); + checkLon("W180\u00B00'0", -180d); + } + + /** + * Tests inputs that are above latitude 90/-90 degrees and longitude 180/-180 degrees. + */ + @Test + public void testAboveBoundary() { + String message = ""; + try { + parser = new ParseDegree(false, "N90.0001"); + } catch (IllegalArgumentException e) { + message = e.getMessage(); + } + assertEquals("out of range [-90,+90]: 90.0001", message); + try { + parser = new ParseDegree(false, "S90.0001"); + } catch (IllegalArgumentException e) { + message = e.getMessage(); + } + assertEquals("out of range [-90,+90]: -90.0001", message); + try { + parser = new ParseDegree(true, "E180.0001"); + } catch (IllegalArgumentException e) { + message = e.getMessage(); + } + assertEquals("out of range [-180,+180]: 180.0001", message); + try { + parser = new ParseDegree(true, "W180.0001"); + } catch (IllegalArgumentException e) { + message = e.getMessage(); + } + assertEquals("out of range [-180,+180]: -180.0001", message); + try { + parser = new ParseDegree(false, "N90.000001"); + } catch (IllegalArgumentException e) { + message = e.getMessage(); + } + assertEquals("out of range [-90,+90]: 90.000001", message); + try { + parser = new ParseDegree(false, "S90.000001"); + } catch (IllegalArgumentException e) { + message = e.getMessage(); + } + assertEquals("out of range [-90,+90]: -90.000001", message); + try { + parser = new ParseDegree(true, "E180.000001"); + } catch (IllegalArgumentException e) { + message = e.getMessage(); + } + assertEquals("out of range [-180,+180]: 180.000001", message); + try { + parser = new ParseDegree(true, "W180.000001"); + } catch (IllegalArgumentException e) { + message = e.getMessage(); + } + assertEquals("out of range [-180,+180]: -180.000001", message); + } + + /** + * Tests various inputs that contain syntax errors. + */ + @Test + public void testInputErrors() { + String message = ""; + try { + parser = new ParseDegree(false, "N90S90"); + } catch (IllegalArgumentException e) { + message = e.getMessage(); + } + assertEquals("already set direction once, cannot add direction: S", message); + try { + parser = new ParseDegree(false, "E120W120"); + } catch (IllegalArgumentException e) { + message = e.getMessage(); + } + assertEquals("already set direction once, cannot add direction: W", message); + try { + parser = new ParseDegree(false, "E"); + } catch (IllegalArgumentException e) { + message = e.getMessage(); + } + assertEquals("end of field without any number seen", message); + try { + parser = new ParseDegree(false, ""); + } catch (IllegalArgumentException e) { + message = e.getMessage(); + } + assertEquals("end of field without any number seen", message); + try { + parser = new ParseDegree(false, "NW25"); + } catch (IllegalArgumentException e) { + message = e.getMessage(); + } + assertEquals("already set direction once, cannot add direction: W", message); + try { + parser = new ParseDegree(false, "N16.25\u00B0"); + } catch (IllegalArgumentException e) { + message = e.getMessage(); + } + assertEquals("cannot have fractional degrees before degrees sign", message); + try { + parser = new ParseDegree(false, "N16\u00B022.40'"); + } catch (IllegalArgumentException e) { + message = e.getMessage(); + } + assertEquals("cannot have fractional minutes before minutes sign", message); + try { + parser = new ParseDegree(false, ""); + } catch (IllegalArgumentException e) { + message = e.getMessage(); + } + assertEquals("end of field without any number seen", message); + try { + parser = new ParseDegree(false, "Yahoo!"); + } catch (IllegalArgumentException e) { + message = e.getMessage(); + } + assertEquals("invalid character: Y", message); + try { + parser = new ParseDegree(false, "N63O025.105"); + } catch (IllegalArgumentException e) { + message = e.getMessage(); + } + assertEquals("invalid character: O", message); + } + +} |