001    package edu.nrao.sss.measure;
002    
003    import java.math.BigDecimal;
004    import java.util.Arrays;
005    import java.util.Comparator;
006    
007    import javax.xml.bind.annotation.XmlAccessType;
008    import javax.xml.bind.annotation.XmlAccessorType;
009    import javax.xml.bind.annotation.XmlElement;
010    import javax.xml.bind.annotation.XmlType;
011    
012    import static edu.nrao.sss.math.MathUtil.MC_FINAL_CALC;
013    
014    import edu.nrao.sss.math.MathUtil;
015    import edu.nrao.sss.util.StringUtil;
016    
017    /**
018     * A measure of distance or length.
019     * <p>
020     * <b>Version Info:</b>
021     * <table style="margin-left:2em">
022     *   <tr><td>$Revision: 1816 $</td></tr>
023     *   <tr><td>$Date: 2008-12-23 10:21:00 -0700 (Tue, 23 Dec 2008) $</td></tr>
024     *   <tr><td>$Author: dharland $</td></tr>
025     * </table></p>
026     *  
027     * @author David M. Harland
028     * @since 2006-05-03
029     */
030    @XmlAccessorType(XmlAccessType.NONE)
031    @XmlType(propOrder= {"xmlValue","units"})
032    public class Distance
033      implements Cloneable, Comparable<Distance>, java.io.Serializable
034    {
035            private static final long serialVersionUID = 1L;
036      
037      private static final BigDecimal    DEFAULT_VALUE = BigDecimal.ZERO;
038      private static final DistanceUnits DEFAULT_UNITS = DistanceUnits.KILOMETER;
039    
040      //Used by equals, hashCode, and compareTo methods
041      private static DistanceUnits STD_UNITS = DistanceUnits.KILOMETER;
042    
043      private static final int PRECISION = MC_FINAL_CALC.getPrecision(); 
044    
045      private BigDecimal    value;
046      private DistanceUnits units;
047    
048      //===========================================================================
049      // CONSTRUCTORS
050      //===========================================================================
051    
052      /** Creates a new distance of zero kilometers. */
053      public Distance()
054      {
055        set(DEFAULT_VALUE, DEFAULT_UNITS);
056      }
057      
058      /**
059       * Creates a new distance of {@code kilometers} kilometers.
060       * See {@link #setValue(BigDecimal)} for information
061       * about valid parameter values and exceptions that might
062       * be thrown.
063       * 
064       * @param kilometers the magnitude of this distance in kilometers.
065       */
066      public Distance(BigDecimal kilometers)
067      {
068        set(kilometers, DistanceUnits.KILOMETER);
069      }
070      
071      /**
072       * Creates a new distance of {@code kilometers} kilometers.
073       * See {@link #setValue(BigDecimal)} for information
074       * about valid parameter values and exceptions that might
075       * be thrown.
076       * 
077       * @param kilometers the magnitude of this distance in kilometers.
078       */
079      public Distance(String kilometers)
080      {
081        set(kilometers, DistanceUnits.KILOMETER);
082      }
083      
084      /**
085       * Creates a new distance with the given magnitude and units.
086       * See {@link #set(BigDecimal, DistanceUnits)} for information
087       * about valid parameter values and exceptions that might
088       * be thrown.
089       * 
090       * @param value the magnitude of this distance.
091       *
092       * @param units the units in which {@code value} is expressed.
093       */
094      public Distance(BigDecimal value, DistanceUnits units)
095      {
096        set(value, units);
097      }
098      
099      /**
100       * Creates a new distance with the given magnitude and units.
101       * See {@link #set(BigDecimal, DistanceUnits)} for information
102       * about valid parameter values and exceptions that might
103       * be thrown.
104       * 
105       * @param value the magnitude of this distance.
106       *
107       * @param units the units in which {@code value} is expressed.
108       */
109      public Distance(String value, DistanceUnits units)
110      {
111        set(value, units);
112      }
113    
114      /**
115       * Resets this distance so that it is equal to a distance created
116       * via the no-argument constructor.
117       */
118      public void reset()
119      {
120        set(DEFAULT_VALUE, DEFAULT_UNITS);
121      }
122    
123      //===========================================================================
124      // GETTING & SETTING THE PROPERTIES
125      //===========================================================================
126    
127      /**
128       * Returns the magnitude of this distance.
129       * @return the magnitude of this distance.
130       */
131      public BigDecimal getValue()
132      {
133        return value;
134      }
135      
136      /**
137       * Returns the units of this distance.
138       * @return the units of this distance.
139       */
140      @XmlElement
141      public DistanceUnits getUnits()
142      {
143        return units;
144      }
145      
146      /**
147       * Sets the magnitude and units of this distance.
148       * <p>
149       * See {@link #setValue(BigDecimal)} for more information on legal
150       * values for <tt>value</tt>.</p>
151       * 
152       * @param value the new magnitude of this distance.
153       * @param units the units in which {@code value} is expressed.
154       */
155      public final void set(BigDecimal value, DistanceUnits units)
156      {
157        setValue(value);
158        setUnits(units);
159      }
160      
161      /**
162       * Sets the magnitude and units of this distance.
163       * <p>
164       * See {@link #setValue(String)} for more information on legal
165       * values for <tt>value</tt>.</p>
166       * 
167       * @param value the new magnitude of this distance.
168       * @param units the units in which {@code value} is expressed.
169       */
170      public final void set(String value, DistanceUnits units)
171      {
172        setValue(value);
173        setUnits(units);
174      }
175    
176      /**
177       * Sets the magnitude of this distance to {@code newValue}.
178       * <p>
179       * Note that the <tt>units</tt> of this distance are unaffected by
180       * this method.</p>
181       * 
182       * @param newValue
183       *   the new magnitude for this distance.
184       *   This value may not be <i>null</i> but may be negative or infinite.
185       *
186       * @throws NumberFormatException
187       *   if {@code newValue} is <i>null</i>.
188       */
189      public final void setValue(BigDecimal newValue)
190      {
191        if (newValue == null)
192          throw new NumberFormatException("newValue=" + newValue +
193          " is not a valid distance.  It must be non-null.");
194        
195        setAndRescale(newValue);
196      }
197      
198      private void setAndRescale(BigDecimal newValue)
199      {
200        int precision = newValue.precision();
201        
202        if (precision < PRECISION)
203        {
204          int newScale =
205            (newValue.signum() == 0) ? 1 : PRECISION - precision + newValue.scale();
206          
207          value = newValue.setScale(newScale);
208        }
209        else if (precision > PRECISION)
210        {
211          value = newValue.round(MC_FINAL_CALC);
212        }
213        else
214        {
215          value = newValue;
216        }
217      }
218    
219      /**
220       * Sets the magnitude of this distance to {@code newValue}.
221       * <p>
222       * Note that the <tt>units</tt> of this distance are unaffected by
223       * this method.</p>
224       * 
225       * @param newValue
226       *   the new magnitude for this distance.
227       *   This value may not be <i>null</i> but may be negative or infinite.
228       *   The allowable representations of infinity are
229       *   <tt>"infinity", "+infinity", </tt>and<tt> "-infinity"</tt>;
230       *   these values are not case sensitive.
231       *
232       * @throws NumberFormatException
233       *   if {@code newValue} is <i>null</i>.
234       */
235      public final void setValue(String newValue)
236      {
237        if (newValue == null)
238          throw new NumberFormatException("newValue may not be null.");
239        
240        BigDecimal newBD;
241        
242        newValue = newValue.trim().toLowerCase();
243        
244        if (newValue.equals("infinity") ||
245            newValue.equals("+infinity") || newValue.equals("-infinity"))
246        {
247          newBD = MathUtil.getInfiniteValue(newValue.startsWith("-") ? -1 : +1);
248        }
249        else
250        {
251          newBD = new BigDecimal(newValue);
252        }
253        
254        setAndRescale(newBD);
255      }
256      
257      /**
258       * Sets the units of this distance to {@code newUnits}.
259       * <p>
260       * Note that the <tt>value</tt> of this distance is unaffected by
261       * this method.  Contrast this with {@link #convertTo(DistanceUnits)}.</p>
262       * 
263       * @param newUnits the new units for this distance.  If {@code newUnits} is
264       *                 <i>null</i> it will be replaced with a non-null default
265       *                 type.
266       */
267      public final void setUnits(DistanceUnits newUnits)
268      {
269        units = (newUnits == null) ? DistanceUnits.getDefault() : newUnits;
270      }
271    
272      /**
273       * Sets the value and units of this distance based on {@code distanceString}.
274       * See {@link #parse(String)} for the expected format of
275       * {@code distanceString}.
276       * <p>
277       * If the parsing fails, this distance will be kept in its current
278       * state.</p>
279       *                                  
280       * @param distanceString
281       *   a string that will be converted into a distance.
282       * 
283       * @throws IllegalArgumentException
284       *   if {@code distanceString} is not in the expected form.
285       *   
286       * @since 2008-10-01
287       */
288      public void set(String distanceString)
289      {
290        if (distanceString == null || distanceString.equals(""))
291        {
292          this.reset();
293        }
294        else
295        {
296          DistanceUnits oldUnits = units;
297          BigDecimal    oldValue = value;
298          
299          try {
300            this.parseDistance(distanceString);
301          }
302          catch (Exception ex) {
303            set(oldValue, oldUnits);
304            throw new IllegalArgumentException("Could not parse " +
305                                               distanceString, ex);
306          }
307        }
308      }
309    
310      //===========================================================================
311      // HELPS FOR PERSISTENCE MECHANISMS
312      //===========================================================================
313      
314      //JAXB was having trouble with the overloaded setValue methods.
315      //These methods work around that trouble.
316      @XmlElement(name="value")
317      @SuppressWarnings("unused")
318      private BigDecimal getXmlValue()  { return getValue().stripTrailingZeros(); }
319      
320      @SuppressWarnings("unused")
321      private void setXmlValue(BigDecimal v)  { setValue(v); }
322    
323      //===========================================================================
324      // DERIVED QUERIES
325      //===========================================================================
326    
327      /**
328       * Returns <i>true</i> if this distance is in its default state,
329       * no matter how it got there.
330       * <p>
331       * A distance is in its <i>default state</i> if both its value and
332       * its units are the same as those of a distance newly created via
333       * the {@link #Distance() no-argument constructor}.
334       * A distance whose most recent update came via the
335       * {@link #reset() reset} method is also in its default state.</p>
336       * 
337       * @return <i>true</i> if this distance is in its default state.
338       */
339      public boolean isInDefaultState()
340      {
341        return (value == DEFAULT_VALUE) &&
342               units.equals(DEFAULT_UNITS);
343      }
344    
345      /**
346       * Returns <i>true</i> if this distance is infinite.
347       * @return <i>true</i> if this distance is infinite.
348       */
349      public boolean isInfinite()
350      {
351        return MathUtil.doubleValueIsInfinite(value);
352      }
353    
354      //===========================================================================
355      // CONVERSION TO, AND EXPRESSION IN, OTHER UNITS
356      //===========================================================================
357    
358      /**
359       * Converts this measure of distance to the new units.
360       * <p>
361       * After this method is complete this distance will have units of
362       * {@code newUnits} and its <tt>value</tt> will have been converted
363       * accordingly.</p>
364       *  
365       * @param newUnits the new units for this distance.
366       *                 If {@code newUnits} is <i>null</i> an
367       *                 {@code IllegalArgumentException} will be thrown.
368       * 
369       * @return this distance.  The reason for this return type is to allow
370       *         code of this nature:
371       *         {@code double kilometers = 
372       *         myDistance.convertTo(DistanceUnits.KILOMETERS).getValue();}
373       */
374      public Distance convertTo(DistanceUnits newUnits)
375      {
376        if (newUnits == null)
377          throw new IllegalArgumentException("May not convert to NULL units.");
378      
379        if (!newUnits.equals(units))
380        {
381          set(toUnits(newUnits), newUnits);
382        }
383        
384        return this;
385      }
386      
387      /**
388       * Returns the magnitude of this distance in {@code otherUnits}.
389       * <p>
390       * Note that this method does not alter the state of this distance.
391       * Contrast this with {@link #convertTo(DistanceUnits)}.</p>
392       * 
393       * @param otherUnits the units in which to express this distance's magnitude.
394       * 
395       * @return this distance's value converted to {@code otherUnits}.
396       * 
397       * @throws IllegalArgumentException if {@code otherUnits} is <i>null</i>.
398       */
399      public BigDecimal toUnits(DistanceUnits otherUnits)
400      {
401        if (otherUnits == null)
402          throw new IllegalArgumentException("May not convert to NULL units.");
403    
404        BigDecimal answer = value;
405        
406        //No conversion for zero, infinite, or if no change of units
407        if (!otherUnits.equals(units) && 
408            value.signum() != 0 && !isInfinite())
409        {
410          answer = units.convertTo(otherUnits, value);
411        }
412        
413        return answer;
414      }
415    
416      //===========================================================================
417      // ARITHMETIC
418      //===========================================================================
419    
420      /**
421       * Adds {@code other} distance to this one.
422       * 
423       * @param other the distance to be added to this distance.
424       * 
425       * @return this distance, after the addition.
426       */
427      public Distance add(Distance other)
428      {
429        //TODO when we work on INFINITY, we need to consider other being infinite
430        //     and the result being inf, too
431        if (!isInfinite())
432          setAndRescale(value.add(other.toUnits(this.units)));
433        
434        return this;
435      }
436      
437      /**
438       * Subtracts {@code other} distance from this one.
439       * 
440       * @param other the distance to be subtracted from this distance.
441       * 
442       * @return this distance, after the subtraction.
443       */
444      public Distance subtract(Distance other)
445      {
446        //TODO when we work on INFINITY, we need to consider other being infinite
447        //     and the result being inf, too
448        if (!isInfinite())
449          setAndRescale(value.subtract(other.toUnits(this.units)));
450        
451        return this;
452      }
453      
454      //===========================================================================
455      // PARSING
456      //===========================================================================
457    
458      /**
459       * Returns a new distance based on {@code distanceString}.
460       * <p>
461       * <b><u>Valid Formats</u></b><br/>
462       * Let R be the text representation of a real number.<br/>
463       * Let w represent zero or more whitespace characters.<br/>
464       * Let S be a valid {@link DistanceUnits units} symbol.<br/>
465       * <br/>
466       * <i>Format One</i>: <tt>wRw</tt>.  The given number will be defined to be
467       * in units of {@link DistanceUnits#KILOMETER kilometers}.<br/>
468       * <br/>
469       * <i>Format Two</i>: <tt>wRwSw</tt>.</p>
470       * <p>
471       * <b><u>Special Cases</u></b><br/>
472       * A {@code distanceString} of <i>null</i> or <tt>""</tt> (the empty
473       * string) will <i>not</i> result in an {@code IllegalArgumentException},
474       * but will instead return a distance of zero kilometers.</p>
475       * 
476       * @param distanceString a string that will be converted into
477       *                       a distance.
478       * 
479       * @return a new distance.
480       * 
481       * @throws IllegalArgumentException if {@code distanceString} is not in
482       *                                  the expected form.
483       */
484      public static Distance parse(String distanceString)
485      {
486        Distance newDistance = new Distance();
487        
488        //null & "" are permissible
489        if ((distanceString != null) && !distanceString.equals(""))
490        {
491          try {
492            newDistance.parseDistance(distanceString);
493          }
494          catch (Exception ex) {
495            throw new IllegalArgumentException("Could not parse " + distanceString, ex);
496          }
497        }
498        
499        return newDistance;
500      }
501      
502      /**
503       * If parsing was successful, this distance's units & value will have been
504       * valued.  Otherwise an exception is thrown.
505       */
506      private void parseDistance(String distanceString)
507      {
508        //Quick exit if text represents infinity
509        if (parseInfiniteDistance(distanceString))
510          return;
511        
512        //Eliminate whitespace
513        distanceString = distanceString.replaceAll("\\s", "");
514    
515        //Assume we have a number followed (optionally) by a symbol
516        units = null;
517        
518        int unitsPos = -1;
519    
520        //Sort units by length of symbol, longer symbols before shorter.
521        //This helps w/ discovering which unit is contained in distanceString.
522        DistanceUnits[] sortedUnits = DistanceUnits.values();
523        Arrays.sort(sortedUnits,
524                    new Comparator<DistanceUnits>() {
525                      public int compare(DistanceUnits a, DistanceUnits b) {
526                        return b.getSymbol().length() - a.getSymbol().length();
527                      }
528                    });
529    
530        //Figure out what kind of units we have
531        for (DistanceUnits u : sortedUnits) 
532        {
533          if (distanceString.endsWith(u.getSymbol()))
534          {
535            units = u;
536            unitsPos = distanceString.lastIndexOf(u.getSymbol());
537            break;
538          }
539        }
540        
541        //If unitsPos < 0, we either have no units or garbage.
542        //The Double.parseDouble method will fail if it is garbage.
543        //If we survive that parsing, we will assume default units.
544        String numberString =
545          (unitsPos < 0) ? distanceString : distanceString.substring(0, unitsPos);
546        
547        setValue(new BigDecimal(numberString));
548    
549        //If we got this far, Double.parseDouble was successful.
550        //If the units are still null, use kilometers.
551        if (units == null)
552          units = DistanceUnits.KILOMETER;
553      }
554      
555      //TODO see if this can be generalized in EnumUtil, perhaps
556      /** Returns <i>true</i> if parsed distance was infinite. */
557      private boolean parseInfiniteDistance(String distText)
558      {
559        final String origText = distText; //in case we need to throw exception
560        
561        boolean isInfinite;
562       
563        final String INF_TEXT = "infinity";
564        
565        char    signChar    = distText.charAt(0);
566        boolean negate      = (signChar == '-');
567        boolean hasSignChar = negate || (signChar == '+');
568        
569        //Strip off "+" or "-"
570        if (hasSignChar)
571          distText = distText.substring(1);
572        
573        int testLength   = INF_TEXT.length();
574        int actualLength = distText.length();
575     
576        //Might have "infinity" with no units
577        if (actualLength == testLength)
578        {
579          isInfinite = distText.equalsIgnoreCase(INF_TEXT);
580          
581          if (isInfinite)
582            set(MathUtil.getInfiniteValue(negate ? -1 : +1), STD_UNITS);
583        }
584        //Might have "infinity" followed by units
585        else if (actualLength > testLength)
586        {
587          String testString = distText.substring(0, testLength);
588    
589          isInfinite = testString.equalsIgnoreCase(INF_TEXT);
590          
591          if (isInfinite)
592          {
593            DistanceUnits du =
594              DistanceUnits.fromString(distText.substring(testLength, actualLength));
595            
596            if (du == null)
597              throw new IllegalArgumentException("Could not parse '" + origText +
598                "'. This looked like an infinite distance but units could not be determined.");
599    
600            set(MathUtil.getInfiniteValue(negate ? -1 : +1), du);
601          }
602        }
603        //String too short to hold "infinity"
604        else //actualLength < testLength
605        {
606          isInfinite = false;
607        }
608        
609        return isInfinite;
610      }
611    
612      //===========================================================================
613      // UTILITY METHODS
614      //===========================================================================
615    
616      /** Returns a text representation of this distance. */
617      @Override
618      public String toString()
619      {
620        return StringUtil.getInstance().formatNoScientificNotation(getValue()) +
621               getUnits().getSymbol();
622      }
623      
624      /**
625       * Returns a text representation of this distance.
626       * 
627       * @param minFracDigits the minimum number of places after the decimal point.
628       *                      
629       * @param maxFracDigits the maximum number of places after the decimal point.
630       * 
631       * @return a text representation of this distance.
632       */
633      public String toString(int minFracDigits, int maxFracDigits)
634      {
635        return StringUtil.getInstance().formatNoScientificNotation(value,
636                                                                   minFracDigits,
637                                                                   maxFracDigits) +
638               getUnits().getSymbol();
639      }
640    
641      /** Returns a distance that is equal to this one. */
642      @Override
643      public Distance clone()
644      {
645        //Since this class has only primitive (& immutable) attributes,
646        //the clone in Object is all we need.
647        try
648        {
649          return (Distance)super.clone();
650        }
651        catch (CloneNotSupportedException ex)
652        {
653          //We'll never get here, but just in case...
654          throw new RuntimeException(ex);
655        }
656      }
657      
658      /** Returns <i>true</i> if {@code o} is equal to this distance. */
659      @Override
660      public boolean equals(Object o)
661      {
662        //Quick exit if o is this
663        if (o == this)
664          return true;
665        
666        //Quick exit if o is null
667        if (o == null)
668          return false;
669        
670        //Quick exit if classes are different
671        if (!o.getClass().equals(this.getClass()))
672          return false;
673        
674        Distance other = (Distance)o;
675    
676        //Treat two infinite values of same sign as equal,
677        //regardless of actual BigDecimal values
678        if (isInfinite() && other.isInfinite())
679          return value.signum() == other.value.signum();
680    
681        //Ignore stored units; equality is based purely on magnitude in std units
682        return compareTo(other) == 0;
683      }
684      
685      /** Returns a hash code value for this distance. */
686      @Override
687      public int hashCode()
688      {
689        if (isInfinite())
690          return value.signum() > 0 ? "+infinity".hashCode() : "-infinity".hashCode();
691          
692        String crude = value.toPlainString() + units.getSymbol();
693        return crude.hashCode();
694      }
695    
696      /** Compares this distance with the {@code otherDist} for order. */
697      public int compareTo(Distance otherDist)
698      {
699        //Treat two infinite values of same sign as equal,
700        //regardless of actual BigDecimal values
701        if (isInfinite() && otherDist.isInfinite())
702          return value.signum() - otherDist.value.signum();
703        
704        //Avoid doing two unit conversions
705        return value.compareTo(otherDist.toUnits(units));
706      }
707      
708      //This is here for quick & dirty testing
709      /*public static void main(String args[])
710      {
711        Distance d1 = new Distance(1.2, DistanceUnits.LIGHT_MINUTE);
712        System.out.println("d1 = " + d1);
713        for (DistanceUnits units : DistanceUnits.values())
714        {
715          d1.convertTo(units);
716          System.out.println("d1 = " + d1);
717        }
718        
719        System.out.println();
720    
721        Distance d2 = new Distance(987.654321, DistanceUnits.MILE);
722        System.out.println("d2 = " + d2);
723        for (DistanceUnits units : DistanceUnits.values())
724        {
725          d2.convertTo(units);
726          System.out.println("d2 = " + d2);
727        }
728        
729        System.out.println();
730    
731        Distance d3 = new Distance(10.0, DistanceUnits.METER);
732        System.out.println("d3 = " + d3);
733        for (DistanceUnits units : DistanceUnits.values())
734        {
735          d3.convertTo(units);
736          System.out.println("d3 = " + d3);
737        }
738      }*/
739      
740      /*public static void main(String[] args)
741      {
742        for (String arg : args)
743        {
744          System.out.println("INPUT:  " + arg);
745          Distance d = Distance.parse(arg);
746          System.out.println("OUTPUT: " + d.toString());
747          System.out.println();
748        }
749      }*/
750    }