8f46fef64c21 — jgindin@chagall 2 years ago
Refactor LatitudeLongitudeParser to be simpler and easier to move to kotlin.
M Android/src/main/java/com/gindin/zmanim/android/ZmanimActivity.java +5 -3
@@ 1,5 1,5 @@ 
 /*
- * Copyright (c) 2021. Jay R. Gindin
+ * Copyright (c) 2022. Jay R. Gindin
  */
 
 package com.gindin.zmanim.android;

          
@@ 517,10 517,12 @@ public class ZmanimActivity
     // The user didn't grant our permissions. The app can work, but they'll have to tell us where we are.
     if ( ( lastKnownLocation != null ) && !lastKnownLocation.isInvalid ) {
 
-      LatitudeLongitudeParser.Holder holder = LatitudeLongitudeParser.parse( Double.toString( lastKnownLocation.getLatitude() ), "", "", LatitudeLongitudeParser.Direction.NORTH );
+      LatitudeLongitudeParser holder = LatitudeLongitudeParser.latitude(
+        LatitudeLongitudeParser.Direction.NORTH, Double.toString( lastKnownLocation.getLatitude() ), "", "" );
       String latitude = holder.toFullString( false );
 
-      holder = LatitudeLongitudeParser.parse( Double.toString( lastKnownLocation.getLongitude() ), "", "", LatitudeLongitudeParser.Direction.EAST );
+      holder = LatitudeLongitudeParser.longitude(
+        LatitudeLongitudeParser.Direction.EAST, Double.toString( lastKnownLocation.getLongitude() ), "", "" );
       String longitude = holder.toFullString( false );
 
       locationManagementPrefs

          
M Android/src/main/java/com/gindin/zmanim/android/location/acquirers/LatitudeLongitudeLocationAcquirer.java +3 -3
@@ 1,5 1,5 @@ 
 /*
- * Copyright (c) 2021. Jay R. Gindin
+ * Copyright (c) 2022. Jay R. Gindin
  */
 
 package com.gindin.zmanim.android.location.acquirers;

          
@@ 68,7 68,7 @@ public class LatitudeLongitudeLocationAc
     }
 
     try {
-      ourLocation.atLatitude( LatitudeLongitudeParser.LATITUDE_PARSER.parseAsDouble( latitudePref ) );
+      ourLocation.atLatitude( LatitudeLongitudeParser.latitudeValue( latitudePref ) );
 
       return null;
     }

          
@@ 90,7 90,7 @@ public class LatitudeLongitudeLocationAc
     }
 
     try {
-      ourLocation.atLongitude( LatitudeLongitudeParser.LONGITUDE_PARSER.parseAsDouble( longitudePref ) );
+      ourLocation.atLongitude( LatitudeLongitudeParser.longitudeValue( longitudePref ) );
 
       return null;
     }

          
M Android/src/main/java/com/gindin/zmanim/android/prefs/LongitudeLatitudePreference.java +20 -18
@@ 1,5 1,5 @@ 
 /*
- * Copyright (c) 2014. Jay R. Gindin
+ * Copyright (c) 2022. Jay R. Gindin
  */
 
 package com.gindin.zmanim.android.prefs;

          
@@ 33,7 33,7 @@ public class LongitudeLatitudePreference
   private View                                              view;
 
   private boolean                                           showingLatitude;
-  private LatitudeLongitudeParser.Holder                    valueHolder;
+  private LatitudeLongitudeParser                           valueHolder;
 
 
   public LongitudeLatitudePreference(

          
@@ 51,10 51,10 @@ public class LongitudeLatitudePreference
     this.showingLatitude = showingLatitude;
 
     if ( showingLatitude ) {
-      valueHolder = LatitudeLongitudeParser.LATITUDE_PARSER.parse( value );
+      valueHolder = LatitudeLongitudeParser.latitude( value );
     }
     else {
-      valueHolder = LatitudeLongitudeParser.LONGITUDE_PARSER.parse( value );
+      valueHolder = LatitudeLongitudeParser.longitude( value );
     }
   }
 

          
@@ 84,16 84,16 @@ public class LongitudeLatitudePreference
 
     ValueFilter minutesFilter = new ValueFilter( 0, 60 );
 
-    EditText degrees = (EditText)view.findViewById( R.id.lat_long_editor_degrees_editor );
-    degrees.setText( String.format( "%d", valueHolder.getDegrees() ) );
+    EditText degrees = view.findViewById( R.id.lat_long_editor_degrees_editor );
+    degrees.setText( String.format( "%d", valueHolder.degrees ) );
     degrees.setFilters( new InputFilter[] { degreesFilter } );
 
-    EditText minutes = (EditText)view.findViewById( R.id.lat_long_editor_minutes_editor );
-    minutes.setText( String.format( "%d", valueHolder.getMinutes() ) );
+    EditText minutes = view.findViewById( R.id.lat_long_editor_minutes_editor );
+    minutes.setText( String.format( "%d", valueHolder.minutes ) );
     minutes.setFilters( new InputFilter[] { minutesFilter } );
 
-    TextView seconds = (TextView)view.findViewById( R.id.lat_long_editor_seconds_editor );
-    seconds.setText( String.format( "%f", valueHolder.getSeconds() ) );
+    TextView seconds = view.findViewById( R.id.lat_long_editor_seconds_editor );
+    seconds.setText( String.format( "%f", valueHolder.seconds ) );
 
 
     // Configuring the spinner comes straight from the Android JavaDocs:

          
@@ 112,13 112,13 @@ public class LongitudeLatitudePreference
     ArrayAdapter adapter = ArrayAdapter.createFromResource( getContext(), arrayResource, android.R.layout.simple_spinner_item );
     adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
 
-    Spinner spinner = (Spinner)view.findViewById( R.id.lat_long_editor_ns_ew_picker );
+    Spinner spinner = view.findViewById( R.id.lat_long_editor_ns_ew_picker );
     spinner.setAdapter( adapter );
 
-    TextView label = (TextView)view.findViewById( R.id.lat_long_editor_ns_text );
+    TextView label = view.findViewById( R.id.lat_long_editor_ns_text );
     label.setText( labelText );
 
-    LatitudeLongitudeParser.Direction direction = valueHolder.getDirection();
+    LatitudeLongitudeParser.Direction direction = valueHolder.direction;
     if ( ( LatitudeLongitudeParser.Direction.NORTH == direction ) || ( LatitudeLongitudeParser.Direction.EAST == direction ) ) {
       spinner.setSelection( 0, true );
     }

          
@@ 137,27 137,29 @@ public class LongitudeLatitudePreference
     }
 
     // Have to convert the degrees, minutes, N/S (or E/W) to something we can store...
-    TextView value = (TextView)view.findViewById( R.id.lat_long_editor_degrees_editor );
+    TextView value = view.findViewById( R.id.lat_long_editor_degrees_editor );
     String degreesText = value.getText().toString();
 
-    value = (TextView)view.findViewById( R.id.lat_long_editor_minutes_editor );
+    value = view.findViewById( R.id.lat_long_editor_minutes_editor );
     String minutesText = value.getText().toString();
 
-    value = (TextView)view.findViewById( R.id.lat_long_editor_seconds_editor );
+    value = view.findViewById( R.id.lat_long_editor_seconds_editor );
     String secondsText = value.getText().toString();
 
     LatitudeLongitudeParser.Direction direction;
-    Spinner spinner = (Spinner)view.findViewById( R.id.lat_long_editor_ns_ew_picker );
+    Spinner spinner = view.findViewById( R.id.lat_long_editor_ns_ew_picker );
     int item = spinner.getSelectedItemPosition();
+    LatitudeLongitudeParser holder;
     if ( showingLatitude ) {
       direction = ( 0 == item ? LatitudeLongitudeParser.Direction.NORTH : LatitudeLongitudeParser.Direction.SOUTH );
+      holder = LatitudeLongitudeParser.latitude( direction, degreesText, minutesText, secondsText );
     }
     else {
       direction = ( 0 == item ? LatitudeLongitudeParser.Direction.EAST : LatitudeLongitudeParser.Direction.WEST );
+      holder = LatitudeLongitudeParser.longitude( direction, degreesText, minutesText, secondsText );
     }
 
 
-    LatitudeLongitudeParser.Holder holder = LatitudeLongitudeParser.parse( degreesText, minutesText, secondsText, direction );
     String newValue = holder.toFullString( false );
 
     if ( callChangeListener( newValue ) ) {

          
M ZmanLib/src/main/java/com/gindin/zmanlib/location/LatitudeLongitudeParser.java +243 -254
@@ 1,221 1,205 @@ 
 /*
- * Copyright (c) 2014. Jay R. Gindin
+ * Copyright (c) 2022. Jay R. Gindin
  */
 
 package com.gindin.zmanlib.location;
 
+import com.gindin.util.Pair;
+
 /**
  * Used to parse a string into a latitude or a longitude.
- *<p>
- *
+ * <p>
+ * <p>
  * Supported formats are:
- *  <ul>
- *    <li> DDD MM['] [SS["]] -- Assumed North
- *    <li> [N|S] DDD MM['] [SS["]]
- *    <li> DDD.MM[N|S]
- *  </ul>
+ * <ul>
+ *   <li> DDD MM['] [SS["]] -- Assumed North
+ *   <li> [N|S] DDD MM['] [SS["]]
+ *   <li> DDD.MM[N|S]
+ * </ul>
  */
-public abstract class LatitudeLongitudeParser {
+public class LatitudeLongitudeParser {
+
+  private static final int                                  MAX_LATITUDE = 90;
 
-  public static enum Direction {
+  private static final int                                  MAX_LONGITUDE = 180;
+
+  public enum Direction {
     NORTH( "N" ),
     SOUTH( "S" ),
     EAST( "E" ),
     WEST( "W" );
 
 
-    private final String                                    abbreviation;
+    private final String abbreviation;
+
 
     Direction( String abbreviation ) {
       this.abbreviation = abbreviation;
     }
   }
 
-  public static class Holder {
+  private static final int                                  MINUTES_PER_DEGREE = 60;
+  private static final int                                  SECONDS_PER_MINUTE = 60;
 
-    private Direction                                       direction;
-    private int                                             minutes;
-    private int                                             degrees;
-    private double                                          seconds = 0;
-
-
-    public Holder() {
-      this( Direction.NORTH, 0, 0, 0 );
-    }
+  public final Direction                                    direction;
+  public final int                                          minutes;
+  public final int                                          degrees;
+  public final double                                       seconds;
 
 
-    public Holder(
-      Direction direction,
-      int       degrees,
-      int       minutes,
-      double    seconds
-    ) {
-      setDirection( direction );
-      setDegrees( degrees );
-      setMinutes( minutes );
-      setSeconds( seconds );
+  @SuppressWarnings( "AssignmentToMethodParameter" )
+  protected LatitudeLongitudeParser(
+    Direction direction,
+    int       degrees,
+    int       minutes,
+    double    seconds,
+    int       maxDegrees
+  ) {
+
+    if ( degrees < 0 ) {
+      if ( Direction.NORTH == direction ) {
+        direction = Direction.SOUTH;
+      }
+      else if ( Direction.EAST == direction ) {
+        direction = Direction.WEST;
+      }
+
+      degrees = degrees * -1;
     }
 
+    this.direction = direction;
+    this.minutes = minutes;
+    this.seconds = seconds;
+
+    // If the value is still too large, then just choose zero.
+    if ( degrees > maxDegrees ) {
+      this.degrees = 0;
+    }
+    else {
+      this.degrees = degrees;
+    }
+  }
+
 
-    public Direction getDirection() {
-      return direction;
+  public String toFullString( boolean withMarkers ) {
+    return direction.abbreviation + " " + formattedDMS( withMarkers );
+  }
+
+
+  public String toSimpleString( boolean withMarkers ) {
+    String str;
+    if ( ( Direction.SOUTH == direction ) || ( Direction.WEST == direction ) ) {
+      str = direction.abbreviation + " ";
     }
-
-    void setDirection( Direction direction ) {
-      this.direction = direction;
+    else {
+      str = "";
     }
 
-    void updateDirection() {
+    return str + formattedDMS( withMarkers );
+  }
+
+
+  private String formattedDMS( boolean withMarkers ) {
 
-      if ( getDegrees() < 1 ) {
-        if ( Direction.NORTH == getDirection() ) {
-          setDirection( Direction.SOUTH );
-        }
-        else if ( Direction.EAST == getDirection() ) {
-          setDirection( Direction.WEST );
-        }
+    StringBuilder str = new StringBuilder().append( degrees );
+    if ( ( 0 != minutes ) || ( 0 != seconds ) ) {
+      str.append( " " ).append( minutes );
+      if ( withMarkers ) {
+        str.append( "'" );
+      }
+    }
 
-        setDegrees( getDegrees() * -1 );
+    if ( 0 != seconds ) {
+      str.append( " " ).append( seconds );
+      if ( withMarkers ) {
+        str.append( "\"" );
       }
     }
 
-
-    public int getDegrees() {
-      return degrees;
-    }
-
-    void setDegrees( int degrees ) {
-      this.degrees = degrees;
-    }
-
-
-    public int getMinutes() {
-      return minutes;
-    }
-
-    void setMinutes( int minutes ) {
-      this.minutes = minutes;
-    }
-
+    return str.toString();
+  }
 
-    public double getSeconds() {
-      return seconds;
-    }
-
-    void setSeconds( double seconds ) {
-      this.seconds = seconds;
-    }
-
-
-    public String toFullString( boolean withMarkers ) {
-      return direction.abbreviation + " " + formattedDMS( withMarkers );
-    }
-
-
-    public String toSimpleString( boolean withMarkers ) {
-      String str;
-      if ( ( Direction.SOUTH == direction ) || ( Direction.WEST == direction ) ) {
-        str = direction.abbreviation + " ";
-      }
-      else {
-        str = "";
-      }
-
-      return str + formattedDMS( withMarkers );
-    }
+  static public LatitudeLongitudeParser latitude(
+    Direction direction,
+    int       degrees,
+    int       minutes,
+    int       seconds
+  ) {
+    return new LatitudeLongitudeParser( direction, degrees, minutes, seconds, MAX_LATITUDE );
+  }
 
 
-    private String formattedDMS( boolean withMarkers ) {
-
-      StringBuilder str = new StringBuilder().append( degrees );
-      if ( ( 0 != minutes ) || ( 0 != seconds ) ) {
-        str.append( " " ).append( minutes );
-        if ( withMarkers ) {
-          str.append( "'" );
-        }
-      }
-
-      if ( 0 != seconds ) {
-        str.append( " " ).append( seconds );
-        if ( withMarkers ) {
-          str.append( "\"" );
-        }
-      }
-
-      return str.toString();
-    }
+  static public LatitudeLongitudeParser latitude(
+    Direction direction,
+    String    degreesText,
+    String    minutesText,
+    String    secondsText
+  ) {
+    return parse( direction, degreesText, minutesText, secondsText, MAX_LATITUDE );
   }
 
-  public static final LatitudeLongitudeParser               LATITUDE_PARSER = new LatitudeParser();
-  public static final LatitudeLongitudeParser               LONGITUDE_PARSER = new LongitudeParser();
-
-  protected abstract double getMax();
+  static public LatitudeLongitudeParser latitude( String value ) {
+    return parse( value, Direction.NORTH, MAX_LATITUDE );
+  }
 
 
-  @SuppressWarnings( "AssignmentToMethodParameter" )
-  public Holder parse( String value ) {
-
-    Holder holder = new Holder();
-
-    // Make sure there's no extra whitespace...
-    value = value.trim();
+  static public double latitudeValue( String value ) {
+    
+    double latitude = parseAsDouble( value, Direction.NORTH, MAX_LATITUDE );
 
-    // NOTE: I used to try to parse the value directly into a double. I don't think this really makes sense, though,
-    //  and quite possibly explains why so many users got confused and couldn't make this functionality work the
-    //  way they thought it should.
-//    try {
-//      return Double.parseDouble( value );
-//    }
-//    catch ( NumberFormatException e ) {
-//      // Well, keep trying to parse.
-//    }
-//
-    value = extractDirection( value, holder );
-
-    // Clean up the input....
-    value = value.replace( '.', ' ' );
-    value = value.replace( '\'', ' ' );
-    value = value.replace( '\"', ' ' );
-    value = value.trim();
-
-
-    String[] latitudeComponents = value.split( " " );
-    if ( 0 == latitudeComponents.length ) {
-      throw new IllegalArgumentException( "Can't parse value: " + value );
+    // Latitude must be -90 <= lat <= 90
+    if ( ( latitude < -MAX_LATITUDE ) || ( latitude > MAX_LATITUDE ) ) {
+      latitude = 0;
     }
 
-    holder.setDegrees( Integer.parseInt( latitudeComponents[ 0 ].trim() ) );
-    holder.updateDirection();
-
-    if ( latitudeComponents.length > 1 ) {
-      holder.setMinutes( Integer.parseInt( latitudeComponents[ 1 ].trim() ) );
-    }
-
-    if ( latitudeComponents.length > 2 ) {
-      holder.setSeconds( Integer.parseInt( latitudeComponents[ 2 ].trim() ) );
-    }
-
-    // Ahhh, but wait. IFF the value is too large, then perhaps what the user was entering was, after all, in
-    //  decimal format?
-    // NOTE: Removed this for the same reason as why I removed the blanket parsing into a double...
-//    if ( holder.getDegrees() > getMax() && ( holder.getMinutes() > 0 ) ) {
-//      holder.setDegrees( Double.parseDouble( latitudeComponents[0] + "." + latitudeComponents[1] ) );
-//    }
-
-    // If the value is still too large, then just choose zero.
-    if ( holder.getDegrees() > getMax() ) {
-      holder.setDegrees( 0 );
-    }
-
-    return holder;
+    return latitude;
   }
 
 
-  public static Holder parse(
-     String                             degreesText,
-     String                             minutesText,
-     String                             secondsText,
-     LatitudeLongitudeParser.Direction  direction
+  static public LatitudeLongitudeParser longitude(
+    Direction direction,
+    int       degrees,
+    int       minutes,
+    int       seconds
+  ) {
+    return new LatitudeLongitudeParser( direction, degrees, minutes, seconds, MAX_LONGITUDE );
+  }
+
+  
+  static public LatitudeLongitudeParser longitude(
+    Direction direction,
+    String    degreesText,
+    String    minutesText,
+    String    secondsText
+  ) {
+    return parse( direction, degreesText, minutesText, secondsText, MAX_LONGITUDE );
+  }
+
+
+  static public LatitudeLongitudeParser longitude( String value ) {
+    return parse( value, Direction.EAST, MAX_LONGITUDE );
+  }
+
+
+    static public double longitudeValue( String value ) {
+
+    double longitude = parseAsDouble( value, Direction.EAST, MAX_LONGITUDE );
+
+    // Longitude must be -180 <= long <= 180
+    if ( ( longitude < -MAX_LONGITUDE ) || ( longitude > MAX_LONGITUDE ) ) {
+      longitude = MAX_LONGITUDE;
+    }
+
+    return longitude;
+  }
+
+
+  private static LatitudeLongitudeParser parse(
+    Direction direction,
+    String    degreesText,
+    String    minutesText,
+    String    secondsText,
+    int       maxDegrees
   ) {
 
     boolean parseMinutes = true;

          
@@ 223,7 207,7 @@ public abstract class LatitudeLongitudeP
     int degrees;
     int minutes = 0;
     double seconds = 0;
-
+    
     try {
       degrees = Integer.parseInt( degreesText );
     }

          
@@ 236,10 220,10 @@ public abstract class LatitudeLongitudeP
         degrees = (int)degreesDouble;
 
         // Now, convert the decimal portion to the minutes. http://geography.about.com/library/howto/htdegrees.htm
-        double doubleMinutes = Math.abs( ( ( degreesDouble - degrees ) * 60 ) );
+        double doubleMinutes = Math.abs( ( ( degreesDouble - degrees ) * MINUTES_PER_DEGREE ) );
         minutes = (int)doubleMinutes;
 
-        seconds = ( ( doubleMinutes - minutes ) * 60 );
+        seconds = ( ( doubleMinutes - minutes ) * SECONDS_PER_MINUTE );
 
         parseMinutes = false;
       }

          
@@ 260,7 244,6 @@ public abstract class LatitudeLongitudeP
         minutes = 0;
       }
 
-
       try {
         seconds = Double.parseDouble( secondsText );
       }

          
@@ 268,28 251,27 @@ public abstract class LatitudeLongitudeP
         seconds = 0;
       }
     }
-
-    final Holder holder = new Holder( direction, degrees, minutes, seconds );
-    holder.updateDirection();
-
-    return holder;
+    
+    return new LatitudeLongitudeParser( direction, degrees, minutes, seconds, maxDegrees );
   }
 
 
   /**
    * Attempts to parse the specified string representing a latitude or longitude into a double containing the
-   *  decimal representation.
+   * decimal representation.
    */
-  public double parseAsDouble( String value )
-    throws IllegalArgumentException {
+   private static double parseAsDouble(
+     String     value,
+     Direction  defaultDirection,
+     int        maxDegress
+   ) {
 
-    Holder holder = parse( value );
+     final LatitudeLongitudeParser parsed = parse( value, defaultDirection, maxDegress );
 
-    // See http://stackoverflow.com/questions/6945008/converting-value-longitude-valuesdmscompass-direction-format-to-correspondi
-    //noinspection MagicNumber
-    double convertedDegrees = holder.getDegrees() + ( ( ( (double)holder.getSeconds() / 60 ) ) + holder.getMinutes() ) / (double)60;
+     // See http://stackoverflow.com/questions/6945008/converting-value-longitude-valuesdmscompass-direction-format-to-correspondi
+    double convertedDegrees = parsed.degrees + ( ( ( parsed.seconds / SECONDS_PER_MINUTE ) ) + parsed.minutes ) / (double)MINUTES_PER_DEGREE;
 
-    if ( ( Direction.SOUTH == holder.getDirection() ) || ( Direction.WEST == holder.getDirection() ) ) {
+    if ( ( Direction.SOUTH == parsed.direction ) || ( Direction.WEST == parsed.direction ) ) {
       convertedDegrees = -1 * convertedDegrees;
     }
 

          
@@ 298,96 280,103 @@ public abstract class LatitudeLongitudeP
 
 
   @SuppressWarnings( "AssignmentToMethodParameter" )
-  private String extractDirection(
-    String value,
-    Holder holder
+  private static LatitudeLongitudeParser parse(
+    String    value,
+    Direction defaultDirection,
+    int       maxDegrees
+  ) {
+
+    // Make sure there's no extra whitespace...
+    value = value.trim();
+
+    // NOTE: I used to try to parse the value directly into a double. I don't think this really makes sense, though,
+    //  and quite possibly explains why so many users got confused and couldn't make this functionality work the
+    //  way they thought it should.
+//    try {
+//      return Double.parseDouble( value );
+//    }
+//    catch ( NumberFormatException e ) {
+//      // Well, keep trying to parse.
+//    }
+//
+    final Pair<Direction, String> extracted = extractDirection( value, defaultDirection );
+    value = extracted.second;
+
+    // Clean up the input....
+    value = value.replace( '.', ' ' );
+    value = value.replace( '\'', ' ' );
+    value = value.replace( '\"', ' ' );
+    value = value.trim();
+
+
+    String[] latitudeComponents = value.split( " " );
+    if ( 0 == latitudeComponents.length ) {
+      throw new IllegalArgumentException( "Can't parse value: " + value );
+    }
+
+    int degrees = Integer.parseInt( latitudeComponents[ 0 ].trim() );
+
+    int minutes;
+    if ( latitudeComponents.length > 1 ) {
+      minutes = Integer.parseInt( latitudeComponents[ 1 ].trim() );
+    }
+    else {
+      minutes = 0;
+    }
+
+    int seconds;
+    if ( latitudeComponents.length > 2 ) {
+      seconds = Integer.parseInt( latitudeComponents[ 2 ].trim() );
+    }
+    else {
+      seconds = 0;
+    }
+
+    // Ahhh, but wait. IFF the value is too large, then perhaps what the user was entering was, after all, in
+    //  decimal format?
+    // NOTE: Removed this for the same reason as why I removed the blanket parsing into a double...
+//    if ( holder.getDegrees() > getMax() && ( holder.getMinutes() > 0 ) ) {
+//      holder.setDegrees( Double.parseDouble( latitudeComponents[0] + "." + latitudeComponents[1] ) );
+//    }
+
+    return new LatitudeLongitudeParser( extracted.first, degrees, minutes, seconds, maxDegrees );
+  }
+
+
+  private static Pair<Direction, String> extractDirection(
+    String    value,
+    Direction defaultDirection
   ) {
 
     char test = value.charAt( 0 );
     if ( ( 'S' == test ) || ( 's' == test ) ) {
-      value = value.substring( 1 );
-      holder.setDirection( Direction.SOUTH );
+      return new Pair<>( Direction.SOUTH, value.substring( 1 ) );
     }
     else if ( ( 'W' == test ) || ( 'w' == test ) ) {
-      value = value.substring( 1 );
-      holder.setDirection( Direction.WEST );
+      return new Pair<>( Direction.WEST, value.substring( 1 ) );
     }
     else if ( ( 'N' == test ) || ( 'n' == test ) ) {
-      value = value.substring( 1 );
-      holder.setDirection( Direction.NORTH );
+      return new Pair<>( Direction.NORTH, value.substring( 1 ) );
     }
     else if ( ( 'E' == test ) || ( 'e' == test ) ) {
-      value = value.substring( 1 );
-      holder.setDirection( Direction.EAST );
+      return new Pair<>( Direction.EAST, value.substring( 1 ) );
     }
 
     test = value.charAt( value.length() - 1 );
     if ( ( 'S' == test ) || ( 's' == test ) ) {
-      value = value.substring( 0, value.length() - 1 );
-      holder.setDirection( Direction.SOUTH );
+      return new Pair<>( Direction.SOUTH, value.substring( 0, value.length() - 1 ) );
     }
     else if ( ( 'W' == test ) || ( 'w' == test ) ) {
-      value = value.substring( 0, value.length() - 1 );
-      holder.setDirection( Direction.WEST );
+      return new Pair<>( Direction.WEST, value.substring( 0, value.length() - 1 ) );
     }
     else if ( ( 'N' == test ) || ( 'n' == test ) ) {
-      value = value.substring( 0, value.length() - 1 );
-      holder.setDirection( Direction.NORTH );
+      return new Pair<>( Direction.NORTH, value.substring( 0, value.length() - 1 ) );
     }
     else if ( ( 'E' == test ) || ( 'e' == test ) ) {
-      value = value.substring( 0, value.length() - 1 );
-      holder.setDirection( Direction.EAST );
-    }
-
-    return value.trim();
-  }
-
-
-  static class LatitudeParser
-    extends LatitudeLongitudeParser {
-
-    @Override
-    protected double getMax() {
-      //noinspection MagicNumber
-      return 90;
+      return new Pair<>( Direction.EAST, value.substring( 0, value.length() - 1 ) );
     }
 
-
-    @Override
-    public double parseAsDouble( String value ) throws IllegalArgumentException {
-      double parsed = super.parseAsDouble( value );
-
-      // Latitude must be -90 <= lat <= 90
-      if ( ( parsed < -90 ) || ( parsed > 90 ) ) {
-        parsed = 0;
-      }
-
-      return parsed;
-    }
+    return new Pair<>( defaultDirection, value );
   }
 
-
-
-  static class LongitudeParser
-    extends LatitudeLongitudeParser {
-
-    @Override
-    protected double getMax() {
-      //noinspection MagicNumber
-      return 180;
-    }
-
-
-    @Override
-    public double parseAsDouble( String value ) throws IllegalArgumentException {
-      double parsed = super.parseAsDouble( value );
-
-      // Longitude must be -180 <= long <= 180
-      if ( ( parsed < -180 ) || ( parsed > 180 ) ) {
-        parsed = 180;
-      }
-
-      return parsed;
-    }
-  }
 }

          
M ZmanLib/src/test/java/com/gindin/zmanlib/location/UT_LatitudeLongitudeParser.java +134 -128
@@ 1,5 1,5 @@ 
 /*
- * Copyright (c) 2014. Jay R. Gindin
+ * Copyright (c) 2022. Jay R. Gindin
  */
 
 package com.gindin.zmanlib.location;

          
@@ 16,13 16,12 @@ import java.util.Collection;
 
 /**
  * Test our ability to properly parse latitude & longitude.
- *<p/>
- *
+ * <p/>
+ * <p>
  * City data comes from: http://www.infoplease.com/ipa/A0001769.html, http://www.infoplease.com/ipa/A0001796.html
- *
- *<p/>
+ * <p>
+ * <p/>
  * A good converter seems to be: http://www.csgnetwork.com/gpscoordconv.html
- *
  */
 @SuppressWarnings( "MagicNumber" )
 @RunWith( Parameterized.class )

          
@@ 30,19 29,23 @@ public class UT_LatitudeLongitudeParser 
 
 
   private static class CityInfo {
-    final String                                            cityName;
-    final LatitudeLongitudeParser.Holder                    latitude;
-    final LatitudeLongitudeParser.Holder                    longitude;
-    final double                                            convertedLatitude;
-    final double                                            convertedLongitude;
+    final String cityName;
+
+    final LatitudeLongitudeParser latitude;
+
+    final LatitudeLongitudeParser longitude;
+
+    final double convertedLatitude;
+
+    final double convertedLongitude;
 
 
     private CityInfo(
-      String                          cityName,
-      LatitudeLongitudeParser.Holder  latitude,
-      LatitudeLongitudeParser.Holder  longitude,
-      double                          convertedLatitude,
-      double                          convertedLongitude
+      String cityName,
+      LatitudeLongitudeParser latitude,
+      LatitudeLongitudeParser longitude,
+      double convertedLatitude,
+      double convertedLongitude
     ) {
       this.cityName = cityName;
       this.latitude = latitude;

          
@@ 59,96 62,82 @@ public class UT_LatitudeLongitudeParser 
   }
 
 
-  /** Info for the city we're currently testing. */
-  private final CityInfo                                    cityInfoToTest;
+  /**
+   * Info for the city we're currently testing.
+   */
+  private final CityInfo cityInfoToTest;
+
 
   @Parameterized.Parameters( name = "{0}" )
-  public static Collection<CityInfo[]> gatherTestData() {
+  public static Collection<CityInfo> gatherTestData() {
 
-    CityInfo[][] cityInfos = new CityInfo[][] {
+    return Arrays.asList(
 
-      {
-        new CityInfo(
-          "Aberdeen, Scotland",
-          new LatitudeLongitudeParser.Holder( LatitudeLongitudeParser.Direction.NORTH, 57, 9, 0 ),
-          new LatitudeLongitudeParser.Holder( LatitudeLongitudeParser.Direction.WEST, 2, 9, 0 ),
-          57.15,
-          -2.15
-        )
-      },
+      new CityInfo(
+        "Aberdeen, Scotland",
+        LatitudeLongitudeParser.latitude( LatitudeLongitudeParser.Direction.NORTH, 57, 9, 0 ),
+        LatitudeLongitudeParser.longitude( LatitudeLongitudeParser.Direction.WEST, 2, 9, 0 ),
+        57.15,
+        -2.15
+      ),
 
-      {
-        new CityInfo(
-          "Adelaide, Australia",
-          new LatitudeLongitudeParser.Holder( LatitudeLongitudeParser.Direction.SOUTH, 34, 55, 0 ),
-          new LatitudeLongitudeParser.Holder( LatitudeLongitudeParser.Direction.EAST, 138, 36, 0 ),
-          -34.91666666666666666667,
-          138.6
-        )
-      },
+      new CityInfo(
+        "Adelaide, Australia",
+        LatitudeLongitudeParser.latitude( LatitudeLongitudeParser.Direction.SOUTH, 34, 55, 0 ),
+        LatitudeLongitudeParser.longitude( LatitudeLongitudeParser.Direction.EAST, 138, 36, 0 ),
+        -34.91666666666666666667,
+        138.6
+      ),
 
-      {
-        new CityInfo(
-          "Tel Aviv",
-          new LatitudeLongitudeParser.Holder( LatitudeLongitudeParser.Direction.NORTH, 32, 4, 0 ),
-          new LatitudeLongitudeParser.Holder( LatitudeLongitudeParser.Direction.EAST, 34, 46, 0 ),
-          32.06666666666666666667,
-          34.76666666666666666667
-        )
-      },
+      new CityInfo(
+        "Tel Aviv",
+        LatitudeLongitudeParser.latitude( LatitudeLongitudeParser.Direction.NORTH, 32, 4, 0 ),
+        LatitudeLongitudeParser.longitude( LatitudeLongitudeParser.Direction.EAST, 34, 46, 0 ),
+        32.06666666666666666667,
+        34.76666666666666666667
+      ),
 
-      {
-        new CityInfo(
-          "Seattle, WA",
-          new LatitudeLongitudeParser.Holder( LatitudeLongitudeParser.Direction.NORTH, 47, 37, 0 ),
-          new LatitudeLongitudeParser.Holder( LatitudeLongitudeParser.Direction.WEST, 122, 20, 0 ),
-          47.61666666666666666667,
-          -122.33333333333333333333
-        )
-      },
+      new CityInfo(
+        "Seattle, WA",
+        LatitudeLongitudeParser.latitude( LatitudeLongitudeParser.Direction.NORTH, 47, 37, 0 ),
+        LatitudeLongitudeParser.longitude( LatitudeLongitudeParser.Direction.WEST, 122, 20, 0 ),
+        47.61666666666666666667,
+        -122.33333333333333333333
+      ),
 
-      {
-        new CityInfo(
-          "New York, NY",
-          new LatitudeLongitudeParser.Holder( LatitudeLongitudeParser.Direction.NORTH, 40, 47, 0 ),
-          new LatitudeLongitudeParser.Holder( LatitudeLongitudeParser.Direction.WEST, 73, 58, 0 ),
-          40.78333333333333333333,
-          -73.96666666666666666667
-        )
-      },
+      new CityInfo(
+        "New York, NY",
+        LatitudeLongitudeParser.latitude( LatitudeLongitudeParser.Direction.NORTH, 40, 47, 0 ),
+        LatitudeLongitudeParser.longitude( LatitudeLongitudeParser.Direction.WEST, 73, 58, 0 ),
+        40.78333333333333333333,
+        -73.96666666666666666667
+      ),
 
-      {
-        new CityInfo(
-          "San Francisco, Calif.",
-          new LatitudeLongitudeParser.Holder( LatitudeLongitudeParser.Direction.NORTH, 37, 47, 0 ),
-          new LatitudeLongitudeParser.Holder( LatitudeLongitudeParser.Direction.WEST, 122, 26, 0 ),
-          37.78333333333333333333,
-          -122.43333333333333333333
-        )
-      },
+      new CityInfo(
+        "San Francisco, Calif.",
+        LatitudeLongitudeParser.latitude( LatitudeLongitudeParser.Direction.NORTH, 37, 47, 0 ),
+        LatitudeLongitudeParser.longitude( LatitudeLongitudeParser.Direction.WEST, 122, 26, 0 ),
+        37.78333333333333333333,
+        -122.43333333333333333333
+      ),
 
-      {
-        new CityInfo(
-          "Phoenix, Ariz.",
-          new LatitudeLongitudeParser.Holder( LatitudeLongitudeParser.Direction.NORTH, 33, 29, 0 ),
-          new LatitudeLongitudeParser.Holder( LatitudeLongitudeParser.Direction.WEST, 112, 4, 0 ),
-          33.48333333333333333333,
-          -112.06666666666666666667
-        )
-      },
+      new CityInfo(
+        "Phoenix, Ariz.",
+        LatitudeLongitudeParser.latitude( LatitudeLongitudeParser.Direction.NORTH, 33, 29, 0 ),
+        LatitudeLongitudeParser.longitude( LatitudeLongitudeParser.Direction.WEST, 112, 4, 0 ),
+        33.48333333333333333333,
+        -112.06666666666666666667
+      ),
 
-      {
-        new CityInfo(
-          "Ottawa, Ont., Can.",
-          new LatitudeLongitudeParser.Holder( LatitudeLongitudeParser.Direction.NORTH, 45, 24, 0 ),
-          new LatitudeLongitudeParser.Holder( LatitudeLongitudeParser.Direction.WEST, 73, 43, 0 ),
-          45.4,
-          -73.71666666666666666667
-        )
-      }
-    };
+      new CityInfo(
+        "Ottawa, Ont., Can.",
+        LatitudeLongitudeParser.latitude( LatitudeLongitudeParser.Direction.NORTH, 45, 24, 0 ),
+        LatitudeLongitudeParser.longitude( LatitudeLongitudeParser.Direction.WEST, 73, 43, 0 ),
+        45.4,
+        -73.71666666666666666667
+      )
+    );
 
-    return Arrays.asList( cityInfos );
   }
 
 

          
@@ 162,6 151,7 @@ public class UT_LatitudeLongitudeParser 
     fullString( false );
   }
 
+
   @Test
   public void testFullStringParsingWithMarkers() {
     fullString( true );

          
@@ 173,6 163,7 @@ public class UT_LatitudeLongitudeParser 
     simpleString( false );
   }
 
+
   @Test
   public void testSimpleStringParsingWithMarkers() {
     simpleString( true );

          
@@ 182,60 173,74 @@ public class UT_LatitudeLongitudeParser 
   @Test
   public void fullDecimalInDegrees() {
 
-    final LatitudeLongitudeParser.Holder latitude = cityInfoToTest.latitude;
-    LatitudeLongitudeParser.Holder holder = LatitudeLongitudeParser.parse(
-      Double.toString( cityInfoToTest.convertedLatitude ), "", "", latitude.getDirection() );
-    Assert.assertEquals( cityInfoToTest.cityName + " latitude.getDegrees()", latitude.getDegrees(), holder.getDegrees() );
+    final LatitudeLongitudeParser latitude = cityInfoToTest.latitude;
+    LatitudeLongitudeParser holder = LatitudeLongitudeParser.latitude( latitude.direction,
+      Double.toString( cityInfoToTest.convertedLatitude ), "", "" );
+    Assert.assertEquals( cityInfoToTest.cityName + " latitude.degrees", latitude.degrees,
+      holder.degrees );
 
     // This gets a bit complicated because IF the actual is ONE degree larger AND we have seconds, THEN that has
     //  happened because of rounding...
-    Assert.assertThat( cityInfoToTest.cityName + " latitude.getMinutes()", latitude, new DegreesWithRoundingMatcher( holder ) );
+    Assert.assertThat( cityInfoToTest.cityName + " latitude.getMinutes()", latitude,
+      new DegreesWithRoundingMatcher( holder ) );
 
-    if ( latitude.getSeconds() != 0 ) {
-      Assert.assertEquals( cityInfoToTest.cityName + " latitude.getSeconds()", latitude.getSeconds(), holder.getSeconds(), 0 );
+    if ( latitude.seconds != 0 ) {
+      Assert.assertEquals( cityInfoToTest.cityName + " latitude.seconds", latitude.seconds,
+        holder.seconds, 0 );
     }
 
-    final LatitudeLongitudeParser.Holder longitude = cityInfoToTest.longitude;
-    holder = LatitudeLongitudeParser.parse(
-      Double.toString( cityInfoToTest.convertedLongitude ), "", "", longitude.getDirection() );
-    Assert.assertEquals( cityInfoToTest.cityName + " longitude.getDegrees()", longitude.getDegrees(), holder.getDegrees() );
-    Assert.assertThat( cityInfoToTest.cityName + " longitude.getMinutes()", longitude, new DegreesWithRoundingMatcher( holder ) );
+    final LatitudeLongitudeParser longitude = cityInfoToTest.longitude;
+    holder = LatitudeLongitudeParser.longitude( longitude.direction,
+      Double.toString( cityInfoToTest.convertedLongitude ), "", "" );
+    Assert.assertEquals( cityInfoToTest.cityName + " longitude.degrees", longitude.degrees,
+      holder.degrees );
+    Assert.assertThat( cityInfoToTest.cityName + " longitude.getMinutes()", longitude,
+      new DegreesWithRoundingMatcher( holder ) );
 
-    if ( longitude.getSeconds() != 0 ) {
-      Assert.assertEquals( cityInfoToTest.cityName + " longitude.getSeconds()", longitude.getSeconds(), holder.getSeconds(), 0 );
+    if ( longitude.seconds != 0 ) {
+      Assert.assertEquals( cityInfoToTest.cityName + " longitude.seconds", longitude.seconds,
+        holder.seconds, 0 );
     }
   }
 
 
   private void fullString( boolean withMarkers ) {
 
-    double converted = LatitudeLongitudeParser.LATITUDE_PARSER.parseAsDouble( cityInfoToTest.latitude.toFullString( withMarkers ) );
-    Assert.assertEquals( cityInfoToTest.cityName + " latitude", cityInfoToTest.convertedLatitude, converted, 0.1 );
+    double converted = LatitudeLongitudeParser.latitudeValue(
+      cityInfoToTest.latitude.toFullString( withMarkers ) );
+    Assert.assertEquals( cityInfoToTest.cityName + " latitude", cityInfoToTest.convertedLatitude,
+      converted, 0.1 );
 
-    converted = LatitudeLongitudeParser.LONGITUDE_PARSER.parseAsDouble( cityInfoToTest.longitude.toFullString( withMarkers ) );
-    Assert.assertEquals( cityInfoToTest.cityName + " longitude", cityInfoToTest.convertedLongitude, converted, 0.1 );
+    converted = LatitudeLongitudeParser.longitudeValue(
+      cityInfoToTest.longitude.toFullString( withMarkers ) );
+    Assert.assertEquals( cityInfoToTest.cityName + " longitude", cityInfoToTest.convertedLongitude,
+      converted, 0.1 );
   }
 
 
   private void simpleString( boolean withMarkers ) {
 
-    double converted = LatitudeLongitudeParser.LATITUDE_PARSER.parseAsDouble( cityInfoToTest.latitude.toSimpleString( withMarkers ) );
-    Assert.assertEquals( cityInfoToTest.cityName + " latitude", cityInfoToTest.convertedLatitude, converted, 0.1 );
+    double converted = LatitudeLongitudeParser.latitudeValue(
+      cityInfoToTest.latitude.toSimpleString( withMarkers ) );
+    Assert.assertEquals( cityInfoToTest.cityName + " latitude", cityInfoToTest.convertedLatitude,
+      converted, 0.1 );
 
-    converted = LatitudeLongitudeParser.LONGITUDE_PARSER.parseAsDouble( cityInfoToTest.longitude.toSimpleString( withMarkers ) );
-    Assert.assertEquals( cityInfoToTest.cityName + " longitude", cityInfoToTest.convertedLongitude, converted, 0.1 );
+    converted = LatitudeLongitudeParser.longitudeValue(
+      cityInfoToTest.longitude.toSimpleString( withMarkers ) );
+    Assert.assertEquals( cityInfoToTest.cityName + " longitude", cityInfoToTest.convertedLongitude,
+      converted, 0.1 );
   }
 
 
+  private static class DegreesWithRoundingMatcher
+    extends BaseMatcher<LatitudeLongitudeParser> {
+
+    private final LatitudeLongitudeParser actual;
+
+    private String mismatchReason;
 
 
-  private static class DegreesWithRoundingMatcher
-    extends BaseMatcher<LatitudeLongitudeParser.Holder> {
-
-    private final LatitudeLongitudeParser.Holder            actual;
-    private String                                          mismatchReason;
-
-    DegreesWithRoundingMatcher(LatitudeLongitudeParser.Holder actual) {
+    DegreesWithRoundingMatcher( LatitudeLongitudeParser actual ) {
       this.actual = actual;
     }
 

          
@@ 243,25 248,26 @@ public class UT_LatitudeLongitudeParser 
     @Override
     public boolean matches( Object item ) {
 
-      LatitudeLongitudeParser.Holder expected = (LatitudeLongitudeParser.Holder)item;
+      LatitudeLongitudeParser expected = (LatitudeLongitudeParser)item;
 
-      int diff = expected.getMinutes() - actual.getMinutes();
+      int diff = expected.minutes - actual.minutes;
       if ( diff == 0 ) {
         return true;
       }
 
       if ( diff == 1 ) {
 
-        if ( ( expected.getSeconds() == 0 ) && ( actual.getSeconds() > 0 ) ) {
+        if ( ( expected.seconds == 0 ) && ( actual.seconds > 0 ) ) {
           return true;
         }
       }
 
-      mismatchReason = "diff " + diff + " and expected seconds: " + expected.getSeconds() +
-        " but actual seconds: " + actual.getSeconds();
+      mismatchReason = "diff " + diff + " and expected seconds: " + expected.seconds +
+        " but actual seconds: " + actual.seconds;
       return false;
     }
 
+
     @Override
     public void describeTo( Description description ) {