001    package edu.nrao.sss.measure;
002    
003    import static edu.nrao.sss.math.MathUtil.MC_FINAL_CALC;
004    
005    import java.math.BigDecimal;
006    import java.math.RoundingMode;
007    import java.util.Arrays;
008    import java.util.Comparator;
009    import java.util.Date;
010    import java.util.EnumMap;
011    import java.util.EnumSet;
012    import java.util.Map;
013    import java.util.Set;
014    import java.util.SortedSet;
015    import java.util.TreeSet;
016    import java.util.regex.Pattern;
017    
018    import javax.xml.bind.annotation.XmlAccessType;
019    import javax.xml.bind.annotation.XmlAccessorType;
020    import javax.xml.bind.annotation.XmlElement;
021    import javax.xml.bind.annotation.XmlType;
022    
023    import edu.nrao.sss.math.MathUtil;
024    import edu.nrao.sss.util.StringUtil;
025    
026    //TODO work on infinity.  Make private static +/-INFINITY BigDecimal variables
027    //   Whenever a value becomes infinite, use the private statics
028    
029    /**
030     * A length of time without a defined starting or ending point.
031     * Contrast this with a {@link TimeInterval}, which is the span
032     * from one instant in time to another.
033     * <p>
034     * This duration currently works with only the following units:
035     * <ul>
036     *   <li>{@code TimeUnits.NANOSECOND}</li>
037     *   <li>{@code TimeUnits.MICROSECOND}</li>
038     *   <li>{@code TimeUnits.MILLISECOND}</li>
039     *   <li>{@code TimeUnits.SECOND}</li>
040     *   <li>{@code TimeUnits.MINUTE}</li>
041     *   <li>{@code TimeUnits.HOUR}</li>
042     * </ul>
043     * This set of legal units is provided by {@link #getLegalUnits()}.</p>
044     * <p>
045     * <b><u>Note About Accuracy</u></b><br/>
046     * This class originally used java's primitive <tt>double</tt> type
047     * for storage and calculation.  Certain transformations, though, 
048     * led to results that where not accurate enough for many purposes.
049     * Because of that, the internal references to <tt>double</tt>
050     * have been replaced with references to {@link BigDecimal}.</p>
051     * <p>
052     * <b>Version Info:</b>
053     * <table style="margin-left:2em">
054     *   <tr><td>$Revision: 1816 $</td>
055     *   <tr><td>$Date: 2008-12-23 10:21:00 -0700 (Tue, 23 Dec 2008) $</td>
056     *   <tr><td>$Author: dharland $</td>
057     * </table></p>
058     * 
059     * @author David M. Harland
060     * @since 2006-03-30
061     */
062    @XmlAccessorType(XmlAccessType.NONE)
063    @XmlType(propOrder={"xmlValue","units"})
064    public class TimeDuration
065      implements Cloneable, Comparable<TimeDuration>, java.io.Serializable
066    {
067            private static final long serialVersionUID = 1L;
068    
069      private static final char TEXT_SEPARATOR = ':';
070    
071      private static final BigDecimal DEFAULT_VALUE = BigDecimal.ZERO;
072      private static final TimeUnits  DEFAULT_UNITS = TimeUnits.HOUR;
073    
074      private static final int PRECISION = MC_FINAL_CALC.getPrecision(); 
075    
076      //Used by equals, hashCode, and compareTo methods
077      //private static final TimeUnits STD_UNITS = TimeUnits.MILLISECOND;
078      
079      //Sets the legal time units for all methods
080      private static final EnumSet<TimeUnits> LEGAL_TIME_UNITS =
081        EnumSet.range(TimeUnits.HOUR, TimeUnits.NANOSECOND);
082      
083      //Used by normalization methods
084      private static final Map<TimeUnits, Long> NORMALIZATION_FACTORS =
085        new EnumMap<TimeUnits, Long>(TimeUnits.class);
086      
087      static
088      {
089        NORMALIZATION_FACTORS.put(TimeUnits.NANOSECOND,  1000000000L);
090        NORMALIZATION_FACTORS.put(TimeUnits.MICROSECOND,    1000000L);
091        NORMALIZATION_FACTORS.put(TimeUnits.MILLISECOND,       1000L);
092        NORMALIZATION_FACTORS.put(TimeUnits.SECOND,              60L);
093        NORMALIZATION_FACTORS.put(TimeUnits.MINUTE,              60L);
094        NORMALIZATION_FACTORS.put(TimeUnits.HOUR, Long.MAX_VALUE);  //Not 24.
095      }
096      
097      private BigDecimal value;
098      private TimeUnits  units;
099    
100      //===========================================================================
101      // CONSTRUCTORS
102      //===========================================================================
103    
104      /** Creates a new duration of zero hours. */
105      public TimeDuration()
106      {
107        set(DEFAULT_VALUE, DEFAULT_UNITS);
108      }
109    
110      /**
111       * Creates a new duration of {@code hours} hours.
112       * See {@link #setValue(BigDecimal)} for information
113       * about valid parameter values and exceptions that might
114       * be thrown.
115       * 
116       * @param hours the length of this duration in hours.
117       */
118      public TimeDuration(BigDecimal hours)
119      {
120        set(hours, TimeUnits.HOUR);
121      }
122    
123      /**
124       * Creates a new duration of {@code hours} hours.
125       * See {@link #setValue(String)} for information
126       * about valid parameter values and exceptions that might
127       * be thrown.
128       * 
129       * @param hours the length of this duration in hours.
130       */
131      public TimeDuration(String hours)
132      {
133        set(hours, TimeUnits.HOUR);
134      }
135    
136      /**
137       * Creates a new duration of the given length.
138       * See {@link #set(BigDecimal, TimeUnits)} for information
139       * about valid parameter values and exceptions that might
140       * be thrown.
141       * 
142       * @param value the length of this duration.
143       *
144       * @param units
145       *   the units in which {@code value} is expressed.
146       *   See the {@link TimeDuration class comments} for a list of legal values.
147       */
148      public TimeDuration(BigDecimal value, TimeUnits units)
149      {
150        set(value, units);
151      }
152    
153      /**
154       * Creates a new duration of the given length.
155       * See {@link #set(String, TimeUnits)} for information
156       * about valid parameter values and exceptions that might
157       * be thrown.
158       * 
159       * @param value the length of this duration.
160       *
161       * @param units
162       *   the units in which {@code time} is expressed.
163       *   See the {@link TimeDuration class comments} for a list of legal values.
164       */
165      public TimeDuration(String value, TimeUnits units)
166      {
167        set(value, units);
168      }
169      
170      /**
171       *  Resets this duration to its initial state.
172       *  A reset duration has the same state as a new duration created
173       *  with the no-argument constructor. 
174       */
175      public void reset()
176      {
177        set(DEFAULT_VALUE, DEFAULT_UNITS);
178      }
179    
180      //===========================================================================
181      // GETTING & SETTING THE PROPERTIES
182      //===========================================================================
183      
184      /**
185       * Returns the length of this duration.
186       * @return the length of this duration.
187       */
188      public BigDecimal getValue()
189      {
190        return value;
191      }
192      
193      /**
194       * Returns the units of this duration.
195       * @return the units of this duration.
196       */
197      @XmlElement
198      public TimeUnits getUnits()
199      {
200        return units;
201      }
202    
203      /**
204       * Sets the length and units of this duration.
205       * <p>
206       * See {@link #setValue(BigDecimal)} for more information on legal
207       * values for <tt>value</tt>.</p>
208       * 
209       * @param value the length of this duration.
210       * @param units the units in which {@code value} is expressed.
211       */
212      public final void set(BigDecimal value, TimeUnits units)
213      {
214        setValue(value);
215        setUnits(units);
216      }
217    
218      /**
219       * Sets the length and units of this duration.
220       * <p>
221       * See {@link #setValue(String)} for more information on legal
222       * values for <tt>value</tt>.</p>
223       * 
224       * @param value the length of this duration.
225       * @param units the units in which {@code time} is expressed.
226       */
227      public final void set(String value, TimeUnits units)
228      {
229        setValue(value);
230        setUnits(units);
231      }
232    
233      /**
234       * Sets the length of this duration to {@code newValue}.
235       * <p>
236       * Note that the <tt>units</tt> of this duration are unaffected by
237       * this method.</p>
238       * 
239       * @param newValue
240       *   the new magnitude for this duration.
241       *   This value may not be negative or <i>null</i> but may be infinite.
242       *
243       * @throws NumberFormatException
244       *   if {@code newValue} is <i>null</i> or negative.
245       */
246      public final void setValue(BigDecimal newValue)
247      {
248        if (newValue == null || newValue.signum() < 0)
249          throw new NumberFormatException("newValue=" + newValue +
250          " is not a valid time of day.  It must be non-null and non-negative.");
251        
252        setAndRescale(newValue);
253      }
254      
255      private void setAndRescale(BigDecimal newValue)
256      {
257        int precision = newValue.precision();
258        
259        if (precision < PRECISION)
260        {
261          int newScale =
262            (newValue.signum() == 0) ? 1 : PRECISION - precision + newValue.scale();
263          
264          value = newValue.setScale(newScale);
265        }
266        else if (precision > PRECISION)
267        {
268          value = newValue.round(MC_FINAL_CALC);
269        }
270        else
271        {
272          value = newValue;
273        }
274      }
275    
276      /**
277       * Sets the length of this duration to {@code newValue}.
278       * <p>
279       * Note that the <tt>units</tt> of this duration are unaffected by
280       * this method.</p>
281       * 
282       * @param newValue
283       *   the new magnitude for this duration.
284       *   This value may not be negative or <i>null</i> but may be infinite.
285       *   The allowable representations of infinity are
286       *   <tt>"infinity", "+infinity", </tt>and<tt> "-infinity"</tt>;
287       *   these values are not case sensitive.
288       *
289       * @throws NumberFormatException
290       *   if {@code newValue} is <i>null</i> or negative.
291       */
292      public final void setValue(String newValue)
293      {
294        if (newValue == null)
295          throw new NumberFormatException("newValue may not be null.");
296        
297        BigDecimal newBD;
298        
299        newValue = newValue.trim().toLowerCase();
300        
301        if (newValue.equals("infinity") ||
302            newValue.equals("+infinity") || newValue.equals("-infinity"))
303        {
304          newBD = MathUtil.getInfiniteValue(newValue.startsWith("-") ? -1 : +1);
305        }
306        else
307        {
308          newBD = new BigDecimal(newValue);
309        }
310        
311        if (newBD.signum() < 0)
312          throw new NumberFormatException("newValue=" + newValue +
313          " is not a valid duration.  It may not be negative.");
314        
315        setAndRescale(newBD);
316      }
317      
318      /**
319       * Sets the units of this duration to {@code newUnits}.
320       * <p>
321       * Note that the <tt>value</tt> of this duration is unaffected by
322       * this method.  Contrast this with {@link #convertTo(TimeUnits)}.</p>
323       * 
324       * @param newUnits
325       *   the new units for this duration.
326       *   See the {@link TimeDuration class comments} for a list of legal values.
327       *
328       * @throws IllegalArgumentException
329       *   if the parameter is outside the described range.
330       */
331      public final void setUnits(TimeUnits newUnits)
332      {
333        if (!LEGAL_TIME_UNITS.contains(newUnits))
334          throw new IllegalArgumentException("Illegal newUnits: " + newUnits);
335    
336        units = newUnits;
337      }
338      
339      /**
340       * Sets the length of this duration.
341       * <p>
342       * While there are no restrictions on the values of the individual
343       * parameters, their combination must not result in a negative duration.</p>
344       * 
345       * @throws IllegalArgumentException
346       *   if the combination of parameters results in a negative duration.
347       */
348      public final void set(long hours, int minutes, BigDecimal seconds)
349      {
350        BigDecimal msH = TimeUnits.HOUR.toUnits(TimeUnits.MILLISECOND)
351                                  .multiply(new BigDecimal(hours));
352        
353        BigDecimal msM = TimeUnits.MINUTE.toUnits(TimeUnits.MILLISECOND)
354                                  .multiply(new BigDecimal(minutes));
355    
356        BigDecimal msS = TimeUnits.SECOND.toUnits(TimeUnits.MILLISECOND)
357                                  .multiply(seconds);
358        
359        BigDecimal milliseconds = msH.add(msM).add(msS);
360        
361        if (milliseconds.signum() < 0)
362          throw new IllegalArgumentException("Bad combination of parameters: " +
363                                             hours +"hrs " + minutes + "min " +
364                                             seconds + "sec.");
365        
366        setAndRescale(milliseconds);
367        units = TimeUnits.MILLISECOND;
368      }
369      
370      /**
371       * Sets the length of this duration.
372       * <p>
373       * While there are no restrictions on the values of the individual
374       * parameters, their combination must not result in a negative duration.</p>
375       * 
376       * @throws IllegalArgumentException
377       *   if the combination of parameters results in a negative duration.
378       */
379      public final void set(long hours, int minutes, String seconds)
380      {
381        set(hours, minutes, new BigDecimal(seconds));
382      }
383      
384      /**
385       * Sets this duration to be equal to {@code other}.
386       * <p>
387       * The code:<pre>
388       *   TimeDuration td = new TimeDuration().set(otherDuration);</pre>
389       * gives the same result as:<pre>
390       *   TimeDuration td = otherDuration.clone();</pre>
391       * This method is better suited to those situations where the target
392       * duration has already been created.</p>
393       *  
394       * @param other the duration to which this duration will be made equal.
395       */
396      public void set(TimeDuration other)
397      {
398        if (other == null)
399          throw new IllegalArgumentException("May not use NULL other in set(TimeDuration).");
400    
401        //Do not need to check for valid value/units
402        this.value = other.value;
403        this.units = other.units;
404      }
405      
406      /**
407       * Sets the value and units of this duration based on {@code timeText}.
408       * See {@link #parse(String)} for the expected format of {@code timeText}.
409       * <p>
410       * If the parsing fails, this duration will be kept in its current
411       * state.</p>
412       *                                  
413       * @param timeText
414       *   a string that will be converted into a time duration.
415       * 
416       * @throws IllegalArgumentException
417       *   if {@code timeText} is not in the expected form.
418       *   
419       * @since 2008-10-01
420       */
421      public void set(String timeText)
422      {
423        if (timeText == null || timeText.equals(""))
424        {
425          this.reset();
426        }
427        else
428        {
429          TimeUnits  oldUnits = units;
430          BigDecimal oldValue = value;
431          
432          try {
433            this.parseDuration(timeText);
434          }
435          catch (Exception ex) {
436            set(oldValue, oldUnits);
437            throw new IllegalArgumentException("Could not parse " +
438                                               timeText, ex);
439          }
440        }
441      }
442    
443      //===========================================================================
444      // HELPS FOR PERSISTENCE MECHANISMS
445      //===========================================================================
446      
447      //JAXB was having trouble with the overloaded setValue methods.
448      //These methods work around that trouble.
449      @XmlElement(name="value")
450      @SuppressWarnings("unused")
451      private BigDecimal getXmlValue()  { return getValue().stripTrailingZeros(); }
452      @SuppressWarnings("unused")
453      private void setXmlValue(BigDecimal v)  { setValue(v); }
454    
455      //===========================================================================
456      // DERIVED QUERIES
457      //===========================================================================
458    
459      /**
460       * Returns <i>true</i> if this duration is in its default state,
461       * no matter how it got there.
462       * <p>
463       * A duration is in its <i>default state</i> if both its value and
464       * its units are the same as those of a duration newly created via
465       * the {@link #TimeDuration() no-argument constructor}.
466       * A duration whose most recent update came via the
467       * {@link #reset() reset} method is also in its default state.</p>
468       * 
469       * @return <i>true</i> if this duration is in its default state.
470       */
471      public boolean isInDefaultState()
472      {
473        return value.equals(DEFAULT_VALUE) &&
474               units.equals(DEFAULT_UNITS);
475      }
476    
477      /**
478       * Returns <i>true</i> if this duration is infinite.
479       * @return <i>true</i> if this duration is infinite.
480       */
481      public boolean isInfinite()
482      {
483        return MathUtil.doubleValueIsInfinite(value);
484      }
485    
486      //===========================================================================
487      // CONVERSION TO, AND EXPRESSION IN, OTHER UNITS
488      //===========================================================================
489    
490      /**
491       * Converts this duration to the new units.
492       * <p>
493       * After this method is complete this duration will have units of
494       * {@code units} and its <tt>value</tt> will have been converted
495       * accordingly.</p>
496       *  
497       * @param newUnits the new units for this duration.
498       *                 If {@code newUnits} is <i>null</i> an
499       *                 {@code IllegalArgumentException} will be thrown.
500       *                 See the {@link TimeDuration class comments} for
501       *                 a list of legal values.
502       * 
503       * @return this duration.  The reason for this return type is to allow
504       *         code of this nature:
505       *         {@code BigDecimal hours = 
506       *         myDuration.convertTo(TimeUnits.HOUR).getValue();}
507       *
508       * @throws IllegalArgumentException if the parameter is outside the
509       *         described range.
510       */
511      public TimeDuration convertTo(TimeUnits newUnits)
512      {
513        if (newUnits == null)
514          throw new
515            IllegalArgumentException("NULL is not a valid value for newUnits.");
516      
517        if (!newUnits.equals(units))
518        {
519          set(toUnits(newUnits), newUnits);
520        }
521    
522        return this;
523      }
524      
525      /**
526       * Returns the length of this duration in {@code otherUnits}.
527       * <p>
528       * Note that this method does not alter the state of this duration.
529       * Contrast this with {@link #convertTo(TimeUnits)}.</p>
530       * <p>
531       * <b><u>Example:</u></b><br/>
532       * Let this duration be of length 2 hours 13 minutes 45.6789 seconds.
533       * The values returned by this method for the legal values of
534       * {@code unit} are (to 5 decimal places):
535       * <p><table style="margin-left:3em">
536       *   <tr><th><u>Units</u></th><th><u>Value</u></th></tr>
537       *   <tr><td>MILLISECOND</td> <td align="right">8,025,678.90000</td></tr>
538       *   <tr><td>SECOND</td>      <td align="right">8,025.67890</td></tr>
539       *   <tr><td>MINUTE</td>      <td align="right">133.76132</td></tr>
540       *   <tr><td>HOUR</td>        <td align="right">2.22936</td></tr>
541       * </table></p>
542       * <p>
543       * Contrast this with {@link #toWholeUnits(TimeUnits)}.</p>
544       * <p>
545       * In addition to working with the legal time units described in the
546       * class comments, this method will also convert to:
547       * <ul>
548       *   <li>{@link TimeUnits#DAY}</li>
549       *   <li>{@link TimeUnits#YEAR}</li>
550       * </ul></p>
551       * 
552       * @param otherUnits the units in which to express this duration's length.
553       *                   If {@code newUnits} is <i>null</i>, it will be treated as
554       *                   {@link TimeUnits#HOUR}.
555       *                   See the {@link TimeDuration class comments} for
556       *                   a list of legal values.
557       * 
558       * @return this duration's value converted to {@code otherUnits}.
559       *
560       * @throws IllegalArgumentException if the parameter is outside the
561       *         described range.
562       */
563      public BigDecimal toUnits(TimeUnits otherUnits)
564      {
565        BigDecimal answer = value;
566        
567        if (otherUnits == null)
568          otherUnits = TimeUnits.HOUR;
569    
570        if ((!LEGAL_TIME_UNITS.contains(otherUnits)) &&
571            !otherUnits.equals(TimeUnits.DAY) &&
572            !otherUnits.equals(TimeUnits.YEAR))
573          throw new IllegalArgumentException("Illegal otherUnits: " + otherUnits);
574        
575        //No conversion for zero, infinite, or if no change of units
576        if (!otherUnits.equals(units) && 
577            value.signum() != 0 && !isInfinite())
578        {
579          answer = units.convertTo(otherUnits, value);
580        }
581        
582        return answer;
583      }
584      
585      /**
586       * Returns a set of {@link TimeUnits} that are legal for use with
587       * {@code TimeDuration} instances.
588       * @return a set of legal {@code TimeUnits}.
589       */
590      public static Set<TimeUnits> getLegalUnits()
591      {
592        return LEGAL_TIME_UNITS.clone();
593      }
594      
595      /**
596       * Converts the value and units of this time duration so that the value
597       * is normal.  By "normal" we mean that it falls in the natural range
598       * for its units.  For example, the natural range for <tt>MINUTES</tt>
599       * is deemed to be [1.0-60.0).  For units that are smaller than one
600       * second, the natural range is [1.0-1000.0).  Even after normalization
601       * the numeric value may be outside the natural range of its units if
602       * there were no smaller or larger legal units available.
603       * 
604       * @return this duration, after normalization.
605       */
606      public TimeDuration normalize()
607      {
608        //In the Frequency class we use a better algorithm, but all the Freq'Units
609        //are 10^x, where x%3==0 and all the units are legal.
610        
611        SortedSet<TimeUnits> validUnits = new TreeSet<TimeUnits>(LEGAL_TIME_UNITS);
612        
613        TimeUnits  newUnits = validUnits.last();  //in case we don't break from loop
614        BigDecimal seconds  = toUnits(TimeUnits.SECOND);
615    
616        for (TimeUnits tu : validUnits)
617        {
618          if (seconds.compareTo(tu.toSeconds()) >= 0)
619          {
620            newUnits = tu;
621            break;
622          }
623        }
624        
625        return convertTo(newUnits);
626      }
627      
628      /**
629       * Returns the length of this duration in a whole number of {@code units}.
630       * <p>
631       * <b><u>Example:</u></b><br/>
632       * Let this duration be of length 2 hours 13 minutes 45.6789 seconds.
633       * The values returned by this method for the legal values of
634       * {@code unit} are:
635       * <p><table style="margin-left:3em">
636       *   <tr><th><u>Units</u></th><th><u>Value</u></th></tr>
637       *   <tr><td>MILLISECOND</td> <td align="right">8,025,678</td></tr>
638       *   <tr><td>SECOND</td>      <td align="right">8,025</td></tr>
639       *   <tr><td>MINUTE</td>      <td align="right">133</td></tr>
640       *   <tr><td>HOUR</td>        <td align="right">2</td></tr>
641       * </table></p>
642       * <p>
643       * Contrast this with {@link #toUnits(TimeUnits)}.</p>
644       * 
645       * @param otherUnits the units of time in which the return value
646       *              is expressed.
647       *              See the {@link TimeDuration class comments} for
648       *              a list of legal values.
649       *              
650       * @return the length of this duration in a whole number of {@code units}.
651       *
652       * @throws IllegalArgumentException if the parameter is outside the
653       *         described range.
654       */
655      public long toWholeUnits(TimeUnits otherUnits)
656      {
657        return toUnits(otherUnits).longValue();
658      }
659      
660      /**
661       * For a duration that is thought of as HH:MM:SS.xxx, returns the part
662       * corresponding to {@code units}.
663       * <p>
664       * <b><u>Range of Returned Value:</u></b><br/>
665       * The value returned will be in its "natural" range.
666       * The table below gives the natural ranges for each unit.  Note that since
667       * hours is the largest legal unit of time, it has no upper bound.
668       * <p><table style="margin-left:3em">
669       *   <tr><th><u>Units</u></th><th><u>Range</u></th></tr>
670       *   <tr><td>MICROSECOND</td> <td>0 &lt;= x &lt; 1000</td></tr>
671       *   <tr><td>MILLISECOND</td> <td>0 &lt;= x &lt; 1000</td></tr>
672       *   <tr><td>SECOND</td>      <td>0 &lt;= x &lt; 60</td></tr>
673       *   <tr><td>MINUTE</td>      <td>0 &lt;= x &lt; 60</td></tr>
674       *   <tr><td>HOUR</td>        <td>0 &lt;= x</td></tr>
675       * </table></p>
676       * <p>
677       * <b><u>Example:</u></b><br/>
678       * Let this duration be of length 2 hours 13 minutes 45.6789 seconds.
679       * The values returned by this method for the legal values of
680       * {@code unit} are (to 5 decimal places):
681       * <p><table style="margin-left:3em">
682       *   <tr><th><u>Units</u></th><th><u>Value</u></th></tr>
683       *   <tr><td>MILLISECOND</td> <td align="right">678.90000</td></tr>
684       *   <tr><td>SECOND</td>      <td align="right">45.67890</td></tr>
685       *   <tr><td>MINUTE</td>      <td align="right">13.76132</td></tr>
686       *   <tr><td>HOUR</td>        <td align="right">2.22936</td></tr>
687       * </table></p>
688       * 
689       * @param units
690       *   the units of time in which the return value is expressed.
691       *   See the {@link TimeDuration class comments} for a list of legal values.
692       *
693       * @return
694       *   the part of this duration corresponding to {@code units}.
695       *
696       * @throws IllegalArgumentException
697       *   if the parameter is outside the described range.
698       *   
699       * @see #getIntegralPart(TimeUnits)
700       */
701      public BigDecimal getPart(TimeUnits units)
702      {
703        return toUnits(units).remainder(
704                 new BigDecimal(NORMALIZATION_FACTORS.get(units)));
705      }
706      
707      /**
708       * For a duration that is thought of as HH:MM:SS.xxx, returns the part
709       * corresponding to {@code units}, truncated to an integral value.
710       * <p>
711       * <b><u>Range of Returned Value:</u></b><br/>
712       * The value returned will be in its "natural" range.
713       * The table below gives the natural ranges for each unit.  Note that since
714       * hours is the largest legal unit of time, it has no upper bound.
715       * <p><table style="margin-left:3em">
716       *   <tr><th><u>Units</u></th><th><u>Range</u></th></tr>
717       *   <tr><td>MICROSECOND</td> <td>0 &lt;= x &lt; 1000</td></tr>
718       *   <tr><td>MILLISECOND</td> <td>0 &lt;= x &lt; 1000</td></tr>
719       *   <tr><td>SECOND</td>      <td>0 &lt;= x &lt; 60</td></tr>
720       *   <tr><td>MINUTE</td>      <td>0 &lt;= x &lt; 60</td></tr>
721       *   <tr><td>HOUR</td>        <td>0 &lt;= x</td></tr>
722       * </table></p>
723       * <p>
724       * <b><u>Example:</u></b><br/>
725       * Let this duration be of length 2 hours 13 minutes 45.6789 seconds.
726       * The values returned by this method for the legal values of
727       * {@code unit} are:
728       * <p><table style="margin-left:3em">
729       *   <tr><th><u>Units</u></th><th><u>Value</u></th></tr>
730       *   <tr><td>MILLISECOND</td> <td align="right">678</td></tr>
731       *   <tr><td>SECOND</td>      <td align="right">45</td></tr>
732       *   <tr><td>MINUTE</td>      <td align="right">13</td></tr>
733       *   <tr><td>HOUR</td>        <td align="right">2</td></tr>
734       * </table></p>
735       * 
736       * @param units
737       *   the units of time in which the return value is expressed.
738       *   See the {@link TimeDuration class comments} for a list of legal values.
739       *
740       * @return
741       *   the part of this duration corresponding to {@code units}.
742       *
743       * @throws IllegalArgumentException
744       *   if the parameter is outside the described range.
745       *   
746       * @see #getPart(TimeUnits)
747       */
748      public long getIntegralPart(TimeUnits units)
749      {
750        return toWholeUnits(units) % NORMALIZATION_FACTORS.get(units);
751      }
752      
753      /**
754       * Returns a time interval that begins on {@code start} and lasts
755       * as long as this duration.
756       * 
757       * @param start the starting time for the returned interval.
758       * 
759       * @return an interval of time that begins on {@code start} and lasts
760       *         as long as this duration.
761       */
762      public TimeInterval toIntervalStartingOn(Date start)
763      {
764        Date end =
765          new Date(start.getTime() + toUnits(TimeUnits.MILLISECOND).longValue());
766        
767        return new TimeInterval(start, end);
768      }
769      
770      /**
771       * Returns a time interval that ends on {@code end} and lasts
772       * as long as this duration.
773       *
774       * @param end the ending time for the returned interval.
775       * 
776       * @return an interval of time that ends on {@code end} and lasts
777       *         as long as this duration.
778       */
779      public TimeInterval toIntervalEndingOn(Date end)
780      {
781        Date start =
782          new Date(end.getTime() - toUnits(TimeUnits.MILLISECOND).longValue());
783        
784        return new TimeInterval(start, end);
785      }
786      
787      /**
788       * Returns a time interval that is centered on {@code center} and lasts
789       * as long as this duration.
790       * <p>
791       * If rounding prevents us from locating the endpoints the exact same
792       * distance from {@code center}, this method will have a bias toward
793       * creating an interval whose starting point is closer to 
794       * {@code center} than its ending point.</p> 
795       *
796       * @param center the center time for the returned interval.
797       * 
798       * @return an interval of time that is centered on {@code center} and
799       *         lasts as long as this duration.
800       */
801      public TimeInterval toIntervalCenteredOn(Date center)
802      {
803        long totalLength      = toUnits(TimeUnits.MILLISECOND).longValue();
804        long firstHalfLength  = totalLength / 2L;
805        long secondHalfLength = totalLength - firstHalfLength;
806        long centerTime       = center.getTime();
807        
808        Date start = new Date(centerTime -  firstHalfLength);
809        Date end   = new Date(centerTime + secondHalfLength);
810        
811        return new TimeInterval(start, end);
812      }
813    
814      //===========================================================================
815      // ARITHMETIC
816      //===========================================================================
817    
818      /**
819       * Adds the given time to this duration.
820       * 
821       * @param other the duration to be added to this duration.
822       * 
823       * @return this duration, after the addition.
824       */
825      public TimeDuration add(TimeDuration other)
826      {
827        //TODO when we work on INFINITY, we need to consider other being infinite
828        //     and the result being inf, too
829        if (!isInfinite())
830          setAndRescale(value.add(other.toUnits(this.units)));
831        
832        return this;
833      }
834       
835      /**
836       * Subtracts {@code other} from this duration.  If the subtraction would
837       * result in a negative interval, this interval is set to a length of zero.
838       * 
839       * @param other the amount by which to reduce this duration.
840       */
841      public TimeDuration subtract(TimeDuration other)
842      {
843        //TODO when we work on INFINITY, we need to consider other being infinite
844        //     and the result being inf, too
845        if (!isInfinite())
846        {
847          BigDecimal diff = value.subtract(other.toUnits(this.units));
848          if (diff.signum() < 0)
849            diff = BigDecimal.ZERO;
850          setAndRescale(diff);
851        }
852        
853        return this;
854      }
855      
856      /**
857       * Multiplies this duration by {@code multiplier}.
858       * 
859       * @param multiplier
860       *   the number by which this duration should be multiplied.
861       *   This value must not result in a duration that is
862       *   negative.
863       *        
864       * @return this duration, after the multiplication.
865       */
866      public TimeDuration multiplyBy(String multiplier)
867      {
868        return multiplyBy(new BigDecimal(multiplier));
869      }
870      
871      /**
872       * Multiplies this duration by {@code multiplier}.
873       * 
874       * @param multiplier
875       *   the number by which this duration should be multiplied.
876       *   This value must not result in a duration that is
877       *   negative.
878       *        
879       * @return this duration, after the multiplication.
880       */
881      public TimeDuration multiplyBy(BigDecimal multiplier)
882      {
883        if (!isInfinite())
884        {
885          BigDecimal result = value.multiply(multiplier);
886        
887          if (result.signum() < 0)
888            throw new ArithmeticException(
889              "Result of operation may not be negative.  This duration was not changed.");
890    
891          setAndRescale(result);
892        }
893        
894        return this;
895      }
896    
897      //===========================================================================
898      // AS TEXT
899      //===========================================================================
900      
901      /** Returns a text representation of this time duration. */
902      public String toString()
903      {
904        return StringUtil.getInstance().formatNoScientificNotation(getValue()) +
905               getUnits().getSymbol();
906      }
907      
908      /**
909       * Returns a text representation of this duration.
910       * 
911       * @param minFracDigits the minimum number of places after the decimal point.
912       *                      
913       * @param maxFracDigits the maximum number of places after the decimal point.
914       * 
915       * @return a text representation of this frequency.
916       */
917      public String toString(int minFracDigits, int maxFracDigits)
918      {
919        return StringUtil.getInstance().formatNoScientificNotation(value,
920                                                                   minFracDigits,
921                                                                   maxFracDigits) +
922               getUnits().getSymbol();
923      }
924    
925      /**
926       * Returns a text representation of this duration in
927       * hours, minutes, and seconds.  The parts are separated
928       * by the colon (':') character.
929       */
930      public String toStringHms()
931      {
932        return toStringHms(0, -1);
933      }
934      
935      /**
936       * Returns a text representation of this duration in
937       * hours, minutes, and seconds.  The parts are separated
938       * by the colon (':') character.
939       * 
940       * @param minFracDigits
941       *   the minimum number of places after the decimal point
942       *   for the seconds field.
943       *                      
944       * @param maxFracDigits
945       *   the maximum number of places after the decimal point
946       *   for the seconds field.  If this value is less than zero,
947       *   no rounding or truncating will be performed.
948       */
949      public String toStringHms(int minFracDigits, int maxFracDigits)
950      {
951        BigDecimal seconds = getPart(TimeUnits.SECOND);
952        long       minutes = getIntegralPart(TimeUnits.MINUTE);
953        long       hours   = toWholeUnits(TimeUnits.HOUR);
954                    
955        //Rollover logic
956        //We have to worry about things like 59.999 being rounded to 60.0
957        if (maxFracDigits >= 0)
958        {
959          if (seconds.setScale(maxFracDigits, RoundingMode.HALF_UP).doubleValue() == 60.0)
960          {
961            seconds = BigDecimal.ZERO;
962            minutes++;
963          }
964          
965          if (minutes == 60L)
966          {
967            minutes = 0L;
968            hours++;
969          }
970        }
971    
972        //Formatting logic
973        StringBuilder buff = new StringBuilder();
974        
975        buff.append(hours).append(':');
976        
977        if (minutes < 10)
978          buff.append('0');
979        buff.append(minutes).append(':');
980        
981        if (seconds.compareTo(BigDecimal.TEN) < 0)
982          buff.append('0');
983    
984        if (maxFracDigits >= 0)
985          buff.append(StringUtil.getInstance().formatNoScientificNotation(
986                      seconds, minFracDigits, maxFracDigits));
987        else //no rounding or truncation
988          buff.append(StringUtil.getInstance().formatNoScientificNotation(seconds));
989          
990        return buff.toString();
991      }
992    
993      /**
994       * Returns a string where the hours, minutes, and seconds are separated
995       * by the given string.
996       * 
997       * @param separator the separator to use between the hours and minutes,
998       *                  and minutes and seconds, fields.
999       *                  
1000       * @return a text representation of this duration.
1001       */
1002      public String toString(String separator)
1003      {
1004        return toString(separator, 0, -1);
1005      }
1006    
1007      /**
1008       * Returns a string where the hours, minutes, and seconds are separated
1009       * by the given string.
1010       * 
1011       * @param separator the separator to use between the hours and minutes,
1012       *                  and minutes and seconds, fields.
1013       * 
1014       * @param minFracDigits the minimum number of places after the decimal point
1015       *                      for the seconds field.
1016       *                      
1017       * @param maxFracDigits the maximum number of places after the decimal point
1018       *                      for the seconds field.
1019       *                  
1020       * @return a text representation of this duration.
1021       */
1022      public String toString(String separator, int minFracDigits, int maxFracDigits)
1023      {
1024        long       hours   = getIntegralPart(TimeUnits.HOUR);
1025        long       minutes = getIntegralPart(TimeUnits.MINUTE);
1026        BigDecimal seconds = getPart(TimeUnits.SECOND);
1027    
1028        StringBuilder buff = new StringBuilder();
1029        
1030        if (hours < 10)
1031          buff.append('0');
1032        
1033        buff.append(hours).append(separator);
1034        
1035        if (minutes < 10)
1036          buff.append('0');
1037    
1038        buff.append(minutes).append(separator);
1039        
1040        if (seconds.compareTo(BigDecimal.TEN) < 0)
1041          buff.append('0');
1042    
1043        if (maxFracDigits >= 0)
1044          buff.append(StringUtil.getInstance().formatNoScientificNotation(
1045                      seconds, minFracDigits, maxFracDigits));
1046        else
1047          buff.append(StringUtil.getInstance().formatNoScientificNotation(seconds));
1048        
1049        return buff.toString();
1050      }
1051    
1052      //===========================================================================
1053      // PARSING
1054      //===========================================================================
1055      
1056      /**
1057       * Creates a time duration based on {@code timeText}.
1058       * <p>
1059       * The parsed text can be in many different forms.  All of the forms
1060       * supported by {@link Longitude#parse(String)} are supported here.
1061       * Additionally, the form <tt>mm:ss.s</tt> (minutes and seconds) is supported.
1062       * A naked number will be treated as a number of seconds.</p>
1063       * 
1064       * @param timeText
1065       *   the text to be parsed and converted into a time duration.
1066       *   If this value is <i>null</i> or <tt>""</tt> (the empty
1067       *   string), a new duration of zero length is returned.
1068       *
1069       * @return a new time duration based on {@code timeText}.
1070       * 
1071       * @throws IllegalArgumentException if {@code timeText} cannot be parsed.
1072       */
1073      public static TimeDuration parse(String timeText)
1074      {
1075        return TimeDuration.parse(timeText, TimeOfDay.STANDARD_DAY_LENGTH);
1076      }
1077      
1078      /**
1079       * Creates a new time duration by parsing {@code timeText}.
1080       * See {@link #parse(String)} for the expected format of {@code timeText}.
1081       * 
1082       * @param timeText
1083       *   the text to be parsed and converted into a time duration.
1084       *   If this value is <i>null</i> or <tt>""</tt> (the empty
1085       *   string), a new duration of zero length is returned.
1086       *                  
1087       * @param secondsInOneDay
1088       *   the length of a day, in seconds.  The {@link TimeOfDay} class has two
1089       *   constants that express time in SI seconds,
1090       *   {@link TimeOfDay#STANDARD_DAY_LENGTH} and
1091       *   {@link TimeOfDay#SIDEREAL_DAY_LENGTH}, that my be used here.
1092       *                  
1093       * @return
1094       *  a new time duration based on {@code timeText}.
1095       * 
1096       * @throws IllegalArgumentException if {@code timeText} cannot be parsed.
1097       */
1098      public static TimeDuration parse(String timeText, BigDecimal secondsInOneDay)
1099      {
1100        TimeDuration newDuration = new TimeDuration(secondsInOneDay);
1101        
1102        //null & "" are permissible
1103        if (timeText != null && !timeText.equals(""))
1104        {
1105          try {
1106            newDuration.parseDuration(timeText);
1107          }
1108          catch (Exception ex) {
1109            throw new IllegalArgumentException("Could not parse " + timeText, ex);
1110          }
1111        }
1112        
1113        return newDuration;
1114      }
1115      
1116      /**
1117       * If parsing was successful, this duration's units & value will have been
1118       * valued.  Otherwise an exception is thrown.
1119       */
1120      private void parseDuration(String timeText)
1121      {
1122        //See if we're successful treating the text as a simple duration (value + units)
1123        try {
1124          parseSimpleTimeDuration(timeText);
1125        }
1126        //If not, try as complex form.
1127        //If that fails, we let the exception cascade upward.
1128        catch (Exception ex) {
1129          parseComplexTimeDuration(timeText);
1130        }
1131      }
1132      
1133      /**
1134       * Used for hms and ":" forms.  Eg, "12h 34m 56.789s" and "12:34:56.789". 
1135       * Does the actual parsing.
1136       */
1137      private void parseComplexTimeDuration(String timeText)
1138      {
1139        //The parsing mechanism of Longitude is very nearly what we want
1140        //here, so use it as a helper.
1141        try
1142        {
1143          Longitude helper = Longitude.parse(timeText, false);
1144          Number[] pieces = helper.toHms();
1145          this.set(pieces[0].longValue(),
1146                   pieces[1].intValue(), (BigDecimal)pieces[2]);
1147        }
1148        //We want to accept one form that Longitude does not:
1149        //minutes and seconds w/ colon (mm:ss.s)
1150        catch (Exception lonParseEx)
1151        {
1152          String[] pieces = timeText.split(Character.toString(TEXT_SEPARATOR),-1);
1153          if (pieces.length == 2)
1154          {
1155            int        minutes = Integer.parseInt(pieces[0]);
1156            BigDecimal seconds = new BigDecimal(pieces[1]);
1157            this.set(0L, minutes, seconds);
1158          }
1159          else
1160          {
1161            throw new IllegalArgumentException("Could not parse " +timeText+ ".");
1162          }
1163        }
1164      }
1165    
1166      private static final Pattern ANY_ALPHA      = Pattern.compile(".*[a-zA-Z].*");
1167      private static final Pattern ANY_WHITESPACE = Pattern.compile(".*\\s.*");
1168    
1169      /**
1170       * Used to see if string is form such as "123.45" or "98.765m".
1171       * If parsing was successful, this duration's units & value will have been
1172       * valued.  Otherwise an exception is thrown.
1173       */
1174      private void parseSimpleTimeDuration(String timeText)
1175      {
1176        timeText = timeText.trim();
1177        
1178        //If we have whitespace and all the other characters are digits,
1179        //then we're dealing with "hh mm ss.sss" or "mm ss.sss".  This
1180        //method does not handle multipart text.
1181        //It is OK for us to have whitespace between the digits and
1182        //the units, though, as in "123.45 h".
1183        if (ANY_WHITESPACE.matcher(timeText).matches())
1184        {
1185          if (!ANY_ALPHA.matcher(timeText).matches())
1186          {
1187            throw new IllegalArgumentException("Cannot parse " + timeText);
1188          }
1189        }
1190        
1191        //Quick exit if text represents infinity
1192        if (parseInfiniteDuration(timeText))
1193          return;
1194        
1195        //Eliminate whitespace
1196        timeText = timeText.replaceAll("\\s", "");
1197    
1198        //Assume we have a number followed (optionally) by a symbol
1199        units = null;
1200        
1201        int unitsPos = -1;
1202    
1203        //Sort units by length of symbol, longer symbols before shorter.
1204        //This helps w/ discovering which unit is contained in timeText.
1205        TimeUnits[] sortedUnits = TimeUnits.values();
1206        Arrays.sort(sortedUnits,
1207                    new Comparator<TimeUnits>() {
1208                      public int compare(TimeUnits a, TimeUnits b) {
1209                        return b.getSymbol().length() - a.getSymbol().length();
1210                      }
1211                    });
1212    
1213        //Figure out what kind of units we have
1214        for (TimeUnits u : sortedUnits) 
1215        {
1216          if (timeText.endsWith(u.getSymbol()))
1217          {
1218            units = u;
1219            unitsPos = timeText.lastIndexOf(u.getSymbol());
1220            break;
1221          }
1222        }
1223        
1224        //If unitsPos < 0, we either have no units or garbage.
1225        //The BigDecimal constructor will fail if it is garbage.
1226        //If we survive that parsing, we will assume default units.
1227        String numberString =
1228          (unitsPos < 0) ? timeText : timeText.substring(0, unitsPos);
1229        
1230        setValue(new BigDecimal(numberString));
1231    
1232        //If we got this far, BigDecimal constructor was successful.
1233        //If the units are still null, use seconds.
1234        if (units == null)
1235          units = DEFAULT_UNITS;
1236      }
1237      
1238      //TODO see if this can be generalized in EnumUtil, perhaps
1239      /** Returns <i>true</i> if parsed duration was infinite. */
1240      private boolean parseInfiniteDuration(String timeText)
1241      {
1242        final String origText = timeText; //in case we need to throw exception
1243        
1244        boolean isInfinite;
1245       
1246        final String INF_TEXT = "infinity";
1247        
1248        char    signChar    = timeText.charAt(0);
1249        boolean negate      = (signChar == '-');
1250        boolean hasSignChar = negate || (signChar == '+');
1251        
1252        //Strip off "+" or "-"
1253        if (hasSignChar)
1254          timeText = timeText.substring(1);
1255        
1256        int testLength   = INF_TEXT.length();
1257        int actualLength = timeText.length();
1258     
1259        //Might have "infinity" with no units
1260        if (actualLength == testLength)
1261        {
1262          isInfinite = timeText.equalsIgnoreCase(INF_TEXT);
1263          
1264          if (isInfinite)
1265            set(MathUtil.getInfiniteValue(negate ? -1 : +1), DEFAULT_UNITS);
1266        }
1267        //Might have "infinity" followed by units
1268        else if (actualLength > testLength)
1269        {
1270          String testString = timeText.substring(0, testLength);
1271    
1272          isInfinite = testString.equalsIgnoreCase(INF_TEXT);
1273          
1274          if (isInfinite)
1275          {
1276            TimeUnits tu =
1277              TimeUnits.fromString(timeText.substring(testLength, actualLength));
1278            
1279            if (tu == null)
1280              throw new IllegalArgumentException("Could not parse '" + origText +
1281                "'. This looked like an infinite duration but units could not be determined.");
1282    
1283            set(MathUtil.getInfiniteValue(negate ? -1 : +1), tu);
1284          }
1285        }
1286        //String too short to hold "infinity"
1287        else //actualLength < testLength
1288        {
1289          isInfinite = false;
1290        }
1291        
1292        return isInfinite;
1293      }
1294    
1295      //===========================================================================
1296      // UTILITY METHODS
1297      //===========================================================================
1298    
1299      /** Returns a duration that is equal to this one. */
1300      @Override
1301      public TimeDuration clone()
1302      {
1303        //Since this class has only primitive (& immutable) attributes,
1304        //the clone in Object is all we need.
1305        try
1306        {
1307          return (TimeDuration)super.clone();
1308        }
1309        catch (CloneNotSupportedException ex)
1310        {
1311          //We'll never get here, but just in case...
1312          throw new RuntimeException(ex);
1313        }
1314      }
1315      
1316      /** Returns <i>true</i> if {@code o} is equal to this duration. */
1317      @Override
1318      public boolean equals(Object o)
1319      {
1320        //Quick exit if o is this
1321        if (o == this)
1322          return true;
1323        
1324        //Quick exit if o is null
1325        if (o == null)
1326          return false;
1327        
1328        //Quick exit if classes are different
1329        if (!o.getClass().equals(this.getClass()))
1330          return false;
1331        
1332        TimeDuration other = (TimeDuration)o;
1333    
1334        //Treat two infinite values of same sign as equal,
1335        //regardless of actual BigDecimal values
1336        if (isInfinite() && other.isInfinite())
1337          return value.signum() == other.value.signum();
1338        
1339        //Ignore stored units; equality is based purely on magnitude in std units
1340        return compareTo(other) == 0;
1341      }
1342      
1343      /** Returns a hash code value for this duration. */
1344      @Override
1345      public int hashCode()
1346      {
1347        if (isInfinite())
1348          return value.signum() > 0 ? "+infinity".hashCode() : "-infinity".hashCode();
1349          
1350        String crude = value.toPlainString() + units.getSymbol();
1351        return crude.hashCode();
1352      }
1353    
1354      /** Compares this duration with the {@code otherDur} for order. */
1355      public int compareTo(TimeDuration otherDur)
1356      {
1357        //Treat two infinite values of same sign as equal,
1358        //regardless of actual BigDecimal values
1359        if (isInfinite() && otherDur.isInfinite())
1360          return value.signum() - otherDur.value.signum();
1361        
1362        //Avoid doing two unit conversions
1363        return value.compareTo(otherDur.toUnits(units));
1364      }
1365    
1366      //===========================================================================
1367      // 
1368      //===========================================================================
1369    
1370      /*
1371      public static void main(String[] args)
1372      {
1373        TimeDuration td = new TimeDuration(1.0, TimeUnits.HOUR);
1374        System.out.println(td.toUnits(TimeUnits.HOUR) + "hr = " +
1375                           td.toUnits(TimeUnits.MINUTE) + "min = " +
1376                           td.toUnits(TimeUnits.SECOND) + "sec = " +
1377                           td.toUnits(TimeUnits.MILLISECOND) + "ms = " +
1378                           td.toUnits(TimeUnits.MICROSECOND) + TimeUnits.MICROSECOND.getSymbol());
1379      
1380        td.set(12, 34, 48);
1381        System.out.println(td.toStringHms() + " = " + td.toUnits(TimeUnits.HOUR) + "hr");
1382        
1383        td.set(365 * 24, TimeUnits.HOUR);
1384        System.out.println(td.toStringHms() + " = " + td.toUnits(TimeUnits.HOUR) + "hr = " +
1385                           td.toUnits(TimeUnits.DAY) +"day");
1386        
1387        TimeInterval ti = td.toIntervalEndingOn(new Date());
1388        System.out.println(ti);
1389        
1390        ti = td.toIntervalStartingOn(new Date());
1391        System.out.println(ti);
1392        
1393        TimeDuration td2 = new TimeDuration();
1394        td.set(22,33,44);
1395        td2.set(33,44,55);
1396        System.out.println("td="+td.toStringHms()+", td2="+td2.toStringHms());
1397        td.add(td2);
1398        System.out.println("td+td2="+td.toStringHms());
1399        td.set(22,33,44);
1400        td2.subtract(td);
1401        System.out.println("td2-td="+td2.toStringHms());
1402      }*/
1403      
1404      //This is here for quick & dirty testing
1405      /*public static void main(String args[])
1406      {
1407        TimeDuration t1 = new TimeDuration(50.0, TimeUnits.HOUR);
1408        System.out.println("t1 = " + t1 + ", = " + t1.toStringHms() + ", = " + t1.toUnits(TimeUnits.MILLISECOND) + TimeUnits.MILLISECOND.getSymbol());
1409        t1.convertTo(TimeUnits.MINUTE);
1410        System.out.println("t1 = " + t1 + ", = " + t1.toStringHms() + ", = " + t1.toUnits(TimeUnits.MILLISECOND) + TimeUnits.MILLISECOND.getSymbol());
1411        t1.convertTo(TimeUnits.SECOND);
1412        System.out.println("t1 = " + t1 + ", = " + t1.toStringHms() + ", = " + t1.toUnits(TimeUnits.MILLISECOND) + TimeUnits.MILLISECOND.getSymbol());
1413        t1.convertTo(TimeUnits.MILLISECOND);
1414        System.out.println("t1 = " + t1 + ", = " + t1.toStringHms() + ", = " + t1.toUnits(TimeUnits.MILLISECOND) + TimeUnits.MILLISECOND.getSymbol());
1415        t1.convertTo(TimeUnits.HOUR);
1416        System.out.println("t1 = " + t1 + ", = " + t1.toStringHms() + ", = " + t1.toUnits(TimeUnits.MILLISECOND) + TimeUnits.MILLISECOND.getSymbol());
1417    
1418        System.out.println();
1419    
1420        TimeDuration t2 = new TimeDuration(123456789.12345, TimeUnits.MILLISECOND);
1421        System.out.println("t2 = " + t2 + ", = " + t2.toStringHms() + ", = " + t2.toUnits(TimeUnits.MILLISECOND) + TimeUnits.MILLISECOND.getSymbol());
1422        t2.convertTo(TimeUnits.HOUR);
1423        System.out.println("t2 = " + t2 + ", = " + t2.toStringHms() + ", = " + t2.toUnits(TimeUnits.HOUR) + TimeUnits.HOUR.getSymbol());
1424        t2.convertTo(TimeUnits.MINUTE);
1425        System.out.println("t2 = " + t2 + ", = " + t2.toStringHms() + ", = " + t2.toUnits(TimeUnits.MINUTE) + TimeUnits.MINUTE.getSymbol());
1426        t2.convertTo(TimeUnits.SECOND);
1427        System.out.println("t2 = " + t2 + ", = " + t2.toStringHms() + ", = " + t2.toUnits(TimeUnits.SECOND) + TimeUnits.SECOND.getSymbol());
1428        t2.convertTo(TimeUnits.MILLISECOND);
1429        System.out.println("t2 = " + t2 + ", = " + t2.toStringHms() + ", = " + t2.toUnits(TimeUnits.MILLISECOND) + TimeUnits.MILLISECOND.getSymbol());
1430      }*/
1431      /*
1432      public static void main(String args[])
1433      {
1434        double seconds = 100000.0;
1435        
1436        for (int i=1; i <= 14; i++)
1437        {
1438          TimeDuration td = new TimeDuration(seconds, TimeUnits.SECOND);
1439          td = td.normalize();
1440          System.out.println("seconds = " + seconds + ", duration = " + td);
1441          seconds /= 10.0;
1442        }
1443      }*/
1444      /*
1445      public static void main(String args[])
1446      {
1447        //Deal w/ proper display when rounding HMS/DMS (ie, no 60 mins or secs)
1448        String[] text =
1449        {
1450         "0:00:59.95", "0:59:59.95", "99:59:59.95"
1451        };
1452        
1453        TimeDuration td;
1454    
1455        for (int t=0; t < text.length; t++)
1456        {
1457          td = TimeDuration.parse(text[t]);
1458          System.out.print("text = " + text[t]);
1459          System.out.print(", ...Hms() = " + td.toStringHms());
1460          System.out.print(", ...Hms(1,1) = " + td.toStringHms(1,1));
1461          System.out.print(", toString() = " + td.toString());
1462          System.out.println();
1463        }
1464      }
1465      */
1466    }