001    package edu.nrao.sss.measure;
002    
003    import java.math.BigDecimal;
004    import java.util.ArrayList;
005    import java.util.List;
006    
007    import javax.xml.bind.annotation.XmlElement;
008    
009    import edu.nrao.sss.util.FormatString;
010    
011    /**
012     * A range of frequencies.  The range is a <i>closed</i> interval; that is,
013     * both endpoints are themselves part of the range.
014     * <p>
015     * TODO: <i>Discuss if that's a bad idea.  It's often nice to
016     * have half-open intervals so that one can create multiple
017     * ranges that fit together perfectly w/ no overlap.  On the other hand,
018     * if the normal usage pattern is not to set up these kinds of
019     * ranges, the closed interval can be more intuitive.</p>
020     * <p>
021     * <u>Jan 2007 Note:</u> Now that we've added more methods to this class,
022     * the closed-interval choice seems less wise; half-open intervals
023     * lead to better algorithms.  Eg, if you want the gap between
024     * the lower range [l<sub>1</sub>-h<sub>1</sub>] and the higher range
025     * [l<sub>2</sub>-h<sub>2</sub>], the natural gap range is
026     * [h<sub>1</sub>-l<sub>2</sub>].  If the range were half-open, we now
027     * have 3 contiguous, non-overlapping ranges.  By keeping the range
028     * closed, we must either have overlap on the endpoints, or say that the
029     * gap-range is [(h<sub>1</sub>+epsilon)-(l<sub>2</sub>-epsilon)].
030     * The main reason for having a closed interval was to allow someone to
031     * say "this receiver has a range from 40GHz to 50GHz", where "to" is
032     * usually intended to be "through".</i></p>
033     * <p>
034     * <b>Version Info:</b>
035     * <table style="margin-left:2em">
036     *   <tr><td>$Revision: 2295 $</td></tr>
037     *   <tr><td>$Date: 2009-05-12 12:59:07 -0600 (Tue, 12 May 2009) $</td></tr>
038     *   <tr><td>$Author: dharland $</td></tr>
039     * </table></p>
040     *  
041     * @author David M. Harland
042     * @since 2006-03-31
043     */
044    public class FrequencyRange
045      implements Cloneable, Comparable<FrequencyRange>
046    {
047      private static final FrequencyUnits DEFAULT_UNITS = Frequency.DEFAULT_UNITS;
048      
049      private static final Frequency DEFAULT_LOW_FREQ =
050        new Frequency("0.0", DEFAULT_UNITS);
051      
052      private static final Frequency DEFAULT_HIGH_FREQ =
053        new Frequency("infinity", DEFAULT_UNITS);
054    
055      @XmlElement private Frequency lowFrequency;
056      @XmlElement private Frequency highFrequency;
057    
058      //===========================================================================
059      // CONSTRUCTORS & INITALIZERS
060      //===========================================================================
061      
062      /**
063       * Creates a new instance that contains all positive frequencies.
064       * The default unit of frequency used is GHz.
065       */
066      public FrequencyRange()
067      {
068        lowFrequency  = DEFAULT_LOW_FREQ.clone();
069        highFrequency = DEFAULT_HIGH_FREQ.clone();
070      }
071      
072      /**
073       * Creates a new instance with the given endpoints.
074       * <p>
075       * This method will set the lower value of its range to the lesser of the
076       * two parameter values.  If either parameter is <i>null</i>, it will be
077       * interpreted as a signal to create a new default frequency.</p>
078       * <p>
079       * Note that this method makes copies of the parameters; it
080       * does not maintain a reference to either parameter.  This
081       * is done in order to maintain the integrity of the relationship
082       * between the starting and ending points of this interval.</p>
083       * 
084       * @param frequency1 one endpoint of this range. 
085       * @param frequency2 the other endpoint of this range.
086       */
087      public FrequencyRange(Frequency frequency1, Frequency frequency2)
088      {
089        set(frequency1, frequency2);
090      }
091      
092      /**
093       * Creates a new frequency range with the given center and an extremely
094       * narrow width.
095       * 
096       * @param center
097       *   the center of this frequency range.
098       *   
099       * @since 2008-10-30
100       */
101      public FrequencyRange(Frequency center)
102      {
103        //Start with a half-width of one "ulp"
104        BigDecimal halfWidth =
105          BigDecimal.valueOf(Math.ulp(center.getValue().doubleValue()));
106        
107        //Let's try a width of 100 ULPs, pending outcome of unit tests
108        Frequency width = new Frequency(halfWidth.multiply(new BigDecimal("100")),
109                                        center.getUnits());
110        //Instantiate inst vars
111        lowFrequency  = new Frequency();
112        highFrequency = new Frequency();
113        
114        setCenterAndWidth(center, width);
115      }
116      
117      /**
118       * Resets this range so that it contains all positive frequencies.
119       * The unit of frequency is also reset to GHz.
120       */
121      public void reset()
122      {
123        lowFrequency.set(DEFAULT_LOW_FREQ.getValue(), DEFAULT_UNITS);
124        highFrequency.set(DEFAULT_HIGH_FREQ.getValue(), DEFAULT_UNITS);
125      }
126      
127      private void makeThisRangeInfinite(FrequencyUnits units)
128      {
129        if (units == null)
130          units = DEFAULT_UNITS;
131        
132        lowFrequency.set("0.0", units);
133        highFrequency.set("+infinity", units);
134      }
135    
136      //===========================================================================
137      // GETTING & SETTING THE PROPERTIES
138      //===========================================================================
139      
140      /**
141       * Sets the frequencies of this range.
142       * <p>
143       * This method will set the lower value of its range to the lesser of the
144       * two parameter values.  If either parameter is <i>null</i>, it will be
145       * interpreted as a signal to create a new default frequency.</p>
146       * <p>
147       * Note that this method makes copies of the parameters; it
148       * does not maintain a reference to either parameter.  This
149       * is done in order to maintain the integrity of the relationship
150       * between the starting and ending points of this interval.</p>
151       * 
152       * @param frequency1 one endpoint of this range. 
153       * @param frequency2 the other endpoint of this range.
154       */
155      public final void set(Frequency frequency1, Frequency frequency2)
156      {
157        //Clone parameters, or create new if null
158        Frequency f1 = (frequency1 == null) ? new Frequency() : frequency1.clone();
159        Frequency f2 = (frequency2 == null) ? new Frequency() : frequency2.clone();
160        
161        //Save references to the clones (or new freqs)
162        if (f1.compareTo(f2) <= 0)
163        {
164          lowFrequency  = f1;
165          highFrequency = f2;
166        }
167        else
168        {
169          lowFrequency  = f2;
170          highFrequency = f1;
171        }
172      }
173    
174      /**
175       * Sets the endpoints of this range based on {@code rangeText}.
176       * See {@link #parse(String)} for the expected format of
177       * {@code rangeText}.
178       * <p>
179       * If the parsing fails, this range will be kept in its current
180       * state.</p>
181       *                                  
182       * @param rangeText
183       *   a string that will be converted into a frequency range.
184       * 
185       * @throws IllegalArgumentException
186       *   if {@code rangeText} is not in the expected form.
187       *   
188       * @since 2008-10-01
189       */
190      public void set(String rangeText)
191      {
192        if (rangeText == null || rangeText.equals(""))
193        {
194          this.reset();
195        }
196        else
197        {
198          try {
199            this.parseRange(rangeText, FormatString.ENDPOINT_SEPARATOR);
200          }
201          catch (IllegalArgumentException ex) {
202            //If the parseRange method threw an exception it never reached
203            //the point of updating this range, so we don't need to restore
204            //to pre-parsing values.
205            throw ex;
206          }
207        }
208      }
209      
210      /**
211       * Returns the high endpoint of this range.
212       * <p>
213       * Note that the value returned is a copy of the one held
214       * internally by this range.  This means that any changes
215       * made by a client to the returned frequency will
216       * <i>not</i> be reflected in this range.</p>
217       * 
218       * @return a copy of the high endpoint of this range.
219       */
220      public Frequency getHighFrequency()
221      {
222        return highFrequency.clone();
223      }
224      
225      /**
226       * Returns the low endpoint of this range.
227       * <p>
228       * Note that the value returned is a copy of the one held
229       * internally by this range.  This means that any changes
230       * made by a client to the returned frequency will
231       * <i>not</i> be reflected in this range.</p>
232       * 
233       * @return a copy of the low endpoint of this range.
234       */
235      public Frequency getLowFrequency()
236      {
237        return lowFrequency.clone();
238      }
239    
240      //===========================================================================
241      // DERIVED QUERIES
242      //===========================================================================
243      
244      /**
245       * Returns the frequency that is midway
246       * between the endpoints of this range.  
247       * <p>
248       * The units for the returned frequency will be the same as those
249       * of the high frequency of this range.</p>  
250       * 
251       * @return the center of this range.
252       */
253      public Frequency getCenterFrequency()
254      {
255        return getWidth().multiplyBy("0.5").add(lowFrequency);
256      }
257      
258      /**
259       * Returns the width of this range.
260       * <p>
261       * The units for the returned frequency will be the same as those
262       * of the high frequency of this range.</p>  
263       * 
264       * @return the width of this range.
265       */
266      public Frequency getWidth()
267      {
268        Frequency width;
269        
270        //Handling the equality case separately makes it convenient in the
271        //"else" condition for dealing with infinity.
272        if (highFrequency.equals(lowFrequency))
273        {
274          width = new Frequency("0.0", highFrequency.getUnits());
275        }
276        else
277        {
278          width = highFrequency.clone();
279          if (!highFrequency.isInfinite())
280            width.subtract(lowFrequency);
281        }
282        
283        return width;
284      }
285      
286      /**
287       * Returns <i>true</i> if this range contains {@code frequency}.
288       * @param frequency the frequency to test for inclusion in this range.
289       * @return <i>true</i> if this range contains {@code frequency}.
290       *         If {@code frequency} is <i>null</i>, the return value
291       *         will be <i>false</i>.
292       * 
293       * @see Frequency#isEndPointOf(FrequencyRange)
294       */
295      public boolean contains(Frequency frequency)
296      {
297        if (frequency == null)
298          return false;
299        
300        //Rem: this is a closed interval (includes both endpoints)
301        return (frequency.compareTo(this.lowFrequency ) >= 0) &&
302               (frequency.compareTo(this.highFrequency) <= 0);
303      }
304      
305      /**
306       * Returns <i>true</i> if this range contains {@code other}.
307       * <p>
308       * Range A is said to contain range B if A's low frequency
309       * is less than or equal to B's low frequency and A's high frequency
310       * is greater than or each to B's high frequency.
311       * Notice that this means that if A equals B, it also contains B.</p>
312       * 
313       * @param other the range to test for inclusion in this range.
314       * @return <i>true</i> if this range contains {@code other}.
315       *         If {@code other} is <i>null</i>, the return value
316       *         will be <i>false</i>.
317       */
318      public boolean contains(FrequencyRange other)
319      {
320        if (other == null)
321          return false;
322    
323        return ( this.lowFrequency.compareTo(other.lowFrequency ) <= 0) && 
324               (this.highFrequency.compareTo(other.highFrequency) >= 0);
325      }
326      
327      /**
328       * Returns <i>true</i> if this frequency range overlaps with {@code other}.
329       * Remember that this range is a closed interval, that is, one that
330       * contains both of its endpoints.
331       * <p>
332       * If {@code other} is <i>null</i>, the return value is <i>false</i>.</p>
333       * 
334       * @param other another range that may overlap this one.
335       * @return <i>true</i> if this range overlaps with {@code other}.
336       */
337      public boolean overlaps(FrequencyRange other)
338      {
339        //Quick exit on null
340        if (other == null)
341          return false;
342        
343        //This logic is dependent on the fact that we're using a closed
344        //interval (range includes both endpoints).  If you switch to
345        //a half-open interval, you must change this code.
346        
347        //Easier to test for non-overlap
348        return !((other.highFrequency.compareTo(this.lowFrequency) < 0) ||
349                 (other.lowFrequency.compareTo(this.highFrequency) > 0));
350      }
351      
352      /**
353       * Returns <i>true</i> if this range overlaps any of the covered ranges
354       * of {@code spectrum}.
355       * 
356       * @param spectrum the covered ranges of which are tested for overlap
357       *                 with this range.
358       *                 
359       * @return <i>true</i> if this range overlaps any of the covered ranges
360       *         of {@code spectrum}.
361       */
362      public boolean overlaps(FrequencySpectrum spectrum)
363      {
364        for (FrequencyRange coveredRange : spectrum.getCoveredRanges())
365          if (coveredRange.overlaps(this))
366            return true;
367        
368        return false;
369      }
370      
371      /**
372       * Returns <i>true</i> if this frequency range is contiguous with
373       * {@code other}.  Two ranges are contiguous if one's low frequency
374       * is equal to the other's high frequency.
375       * If {@code other} is <i>null</i>, the return value is <i>false</i>.
376       * <p>
377       * <i>Note: we are using this definition even though this class
378       * currently defines the range as inclusive of both endpoints.
379       * So, at this time, continguous ranges technically overlap on
380       * their endpoints.  If this class is changed to a half-open
381       * interval, the behavior of this method will </i>not<i> change.</i></p>
382       * 
383       * @param other another range that may be contiguous this one.
384       * @return <i>true</i> if this range is contiguous with {@code other}.
385       */
386      public boolean isContiguousWith(FrequencyRange other)
387      {
388        return (other != null) &&
389               (this.lowFrequency.equals(other.highFrequency) ||
390                this.highFrequency.equals(other.lowFrequency));
391      }
392      
393      /**
394       * Returns a frequency spectrum that represents the overlap of this range
395       * with {@code spectrum}.  If {@code spectrum} is <i>null</i>, or has no
396       * covered regions that overlap with this range, the returned spectrum
397       * will have no covered ranges.
398       * 
399       * @param spectrum the spectrum to check for covered ranges that overlap
400       *                 with this range.
401       *                 
402       * @return a frequency spectrum that represents the overlap of this range
403       *         with {@code spectrum}.
404       */
405      public FrequencySpectrum getOverlapWith(FrequencySpectrum spectrum)
406      {
407        FrequencySpectrum result = new FrequencySpectrum();
408        
409        if (spectrum != null)
410        {
411          for (FrequencyRange coveredRange : spectrum.getCoveredRanges())
412          {
413            if (this.overlaps(coveredRange))
414              result.addCoveredRange(this.getOverlapWith(coveredRange));
415          }
416        }
417        
418        return result;
419      }
420      
421      /**
422       * Returns a new range that represents the region of overlap between this
423       * range and {@code other}.  If there is no overlap, <i>null</i> is returned.
424       * <p>
425       * See {@link #overlaps(FrequencyRange)} for how the closed-interval
426       * definition of this class impacts the results of this method.</p>
427       * 
428       * @param other another range that may overlap this one.
429       * @return the overlapping region of this range and {@code other}.
430       */
431      public FrequencyRange getOverlapWith(FrequencyRange other)
432      {
433        FrequencyRange result = null;
434        
435        if (this.overlaps(other))
436        {
437          //Choose the greater of the lows
438          Frequency low = (this.lowFrequency.compareTo(other.lowFrequency) > 0) ?
439              this.lowFrequency.clone() : other.lowFrequency.clone();
440    
441          //Choose the lesser of the highs
442          Frequency high = (this.highFrequency.compareTo(other.highFrequency) < 0) ?
443              this.highFrequency.clone() : other.highFrequency.clone();
444              
445          result = new FrequencyRange(low, high);
446        }
447    
448        return result;
449      }
450    
451      /**
452       * Returns a new range that represents the region of frequency space between
453       * this range and {@code other}.  If the other range is coincident with, or
454       * overlaps, this range, <i>null</i> is returned.  If the other range is
455       * <i>null</i>, <i>null</i> is returned.
456       * <p>
457       * See {@link #overlaps(FrequencyRange)} for how the closed-interval
458       * definition of this class impacts the results of this method.</p>
459       * 
460       * @param other another range that might not overlap this one.
461       * @return the frequency gap between this range and {@code other}.
462       */
463      public FrequencyRange getGapBetween(FrequencyRange other)
464      {
465        FrequencyRange result = null;
466    
467        if ((other != null) && !this.overlaps(other))
468        {
469          //Choose the greater of the lows
470          Frequency high = (this.lowFrequency.compareTo(other.lowFrequency) > 0) ?
471              this.lowFrequency.clone() : other.lowFrequency.clone();
472    
473          //Choose the lesser of the highs
474          Frequency low = (this.highFrequency.compareTo(other.highFrequency) < 0) ?
475              this.highFrequency.clone() : other.highFrequency.clone();
476              
477          result = new FrequencyRange(low, high);
478        }
479    
480        return result;
481      }
482      
483      /**
484       * Returns zero, one, or two ranges that represent the portions of
485       * <tt>other</tt> that are not also found in this range.  Some scenarios:
486       * <ol>
487       *   <li>
488       *     If <tt>other</tt> is:
489       *       <ol type="a"><li><i>null</i>,</li>
490       *                    <li>equal to this range, or</li>
491       *                    <li>contained within this range</li></ol>
492       *       the returned list will be empty.</li>
493       *   <li>
494       *     If <tt>other</tt> has no overlap with this range, the returned
495       *     list will contain a single element whose range is a copy
496       *     of <tt>other</tt>.</li>
497       *   <li>
498       *     If <tt>other</tt> and this range overlap, but neither contains
499       *     the other, the returned list will contain one element.</li>
500       *   <li>
501       *     If <tt>other</tt> contains this range the returned list
502       *     will contain two elements.</li>
503       * </ol>
504       * 
505       * @param other
506       *   the provider of frequencies that are in the returned range
507       *   but not in this one.
508       *   
509       * @return
510       *   the complement of this range in <tt>other</tt>.
511       */
512      public List<FrequencyRange> getComplementIn(FrequencyRange other)
513      {
514        List<FrequencyRange> result = new ArrayList<FrequencyRange>();
515        
516        if (other == null || other.equals(this) || this.contains(other)) 
517        {
518          ; //result will have zero elements
519        }
520        else if (!other.overlaps(this))
521        {
522          result.add(other.clone());
523        }
524        else if (other.contains(this))
525        {
526          //Freq'Range constructor makes clones, so it's OK to use variables here
527          result.add(new FrequencyRange(other.lowFrequency,   this.lowFrequency));
528          result.add(new FrequencyRange( this.highFrequency, other.highFrequency));
529        }
530        else //overlap w/out containment
531        {
532          //If other.low >= this.low, use this.high & other.high
533          if (other.lowFrequency.compareTo(this.lowFrequency) >= 0)
534          {
535            result.add(new FrequencyRange(this.highFrequency, other.highFrequency));
536          }
537          //If other.low < this.low, use other.low and this.low
538          else
539          {
540            result.add(new FrequencyRange(other.lowFrequency, this.lowFrequency));
541          }
542        }
543        
544        return result;
545      }
546      
547      /**
548       * Returns <i>true</i> if this frequency range is in its default state,
549       * no matter how it got there.
550       * <p>
551       * A frequency range is in its <i>default state</i> if both the values
552       * and units for the low and high points are the same as those of a range
553       * newly created via
554       * the {@link #FrequencyRange() no-argument constructor}.
555       * A frequency range whose most recent update came via the
556       * {@link #reset() reset} method is also in its default state.</p>
557       * 
558       * @return <i>true</i> if this frequency range is in its default state.
559       */
560      public boolean isInDefaultState()
561      {
562        return lowFrequency.equals(DEFAULT_LOW_FREQ) &&
563               highFrequency.equals(DEFAULT_HIGH_FREQ);
564      }
565    
566      //===========================================================================
567      // INDIRECT MANIPULATIONS
568      //===========================================================================
569    
570      /**
571       * Converts both endpoints of this range to the given units.
572       * <p>
573       * After this method is complete both endpoints of this range will have units
574       * of {@code units}, and their <tt>values</tt> will have been converted
575       * accordingly.</p>
576       *  
577       * @param newUnits the new units for the endpoints of this range.
578       *                 If {@code newUnits}
579       *                 is <i>null</i>, it will be treated as
580       *                 {@link FrequencyUnits#GIGAHERTZ}.
581       * 
582       * @return this range.
583       */
584      public FrequencyRange convertTo(FrequencyUnits newUnits)
585      {
586        lowFrequency.convertTo(newUnits);
587        highFrequency.convertTo(newUnits);
588        
589        return this;
590      }
591      
592      /**
593       * Recasts each endpoint separately by calling its
594       * {@link Frequency#normalize() normalize} method.
595       * The one exception to the above process is when the low frequency
596       * is zero and the high is not.  In that case the low frequency
597       * will be converted to the same units as the normalized high.
598       * 
599       * @return this range, after normalization.
600       */
601      public FrequencyRange normalize()
602      {
603        highFrequency.normalize();
604        
605        if (lowFrequency.getValue().signum() != 0)
606          lowFrequency.normalize();
607        else
608          lowFrequency.convertTo(highFrequency.getUnits());
609        
610        return this;
611      }
612      
613      /**
614       * Modifies this range to be the union of this range and {@code other}, 
615       * but only if the two ranges overlap or are contiguous.
616       * If the two ranges are disjoint, this range is unaltered.
617       * 
618       * @param other the range to merge into this range.
619       * 
620       * @return this range after merger with {@code other}.
621       */
622      public FrequencyRange mergeWith(FrequencyRange other)
623      {
624        //Only ranges that overlap or butt up against each other may be merged
625        if (this.overlaps(other) || this.isContiguousWith(other))
626        {
627          //Update our low frequency if other's low is lower
628          if (other.lowFrequency.compareTo(this.lowFrequency) < 0)
629          {
630            this.lowFrequency.set(other.lowFrequency.getValue(),
631                                  other.lowFrequency.getUnits());
632          }
633          //Update our high frequency if other's high is higher
634          if (other.highFrequency.compareTo(this.highFrequency) > 0)
635          {
636            this.highFrequency.set(other.highFrequency.getValue(),
637                                   other.highFrequency.getUnits());
638          }
639        }
640        
641        return this;
642      }
643      
644      /**
645       * Modifies this range to be the intersection of this range and {@code other}. 
646       * If this range does not intersect with {@code other}, it is modified
647       * so that the value of both its low and high frequencies is zero, while
648       * leaving their units unchanged.
649       * 
650       * @param other the range to intersect with this one.
651       * 
652       * @return this range after intersection with {@code other}.
653       */
654      public FrequencyRange intersectWith(FrequencyRange other)
655      {
656        if (this.overlaps(other))
657        {
658          //Choose the greater of the lows
659          if (other.lowFrequency.compareTo(this.lowFrequency) > 0)
660            this.lowFrequency.set(other.lowFrequency.getValue(),
661                                  other.lowFrequency.getUnits());
662     
663          //Choose the lesser of the highs
664          if (other.highFrequency.compareTo(this.highFrequency) < 0)
665            this.highFrequency.set(other.highFrequency.getValue(),
666                                   other.highFrequency.getUnits());
667        }
668        else //there is no intersection
669        {
670          this.lowFrequency.setValue( "0.0");
671          this.highFrequency.setValue("0.0");
672        }
673        
674        return this;
675      }
676    
677      /**
678       * Sets the endpoints of this range based on the given center frequency
679       * and bandwidth.  The units of both endpoints will be the same as those
680       * of the {@code center} frequency.
681       * <p>
682       * If either parameter is <i>null</i> this method will take no action
683       * and this range will have the same state as immediately before the call.</p>
684       * <p>
685       * If either {@code center} or {@code width} is infinite, this range will
686       * be reset so that its low frequency has a value of zero and its high
687       * frequency has an infinite value.</p>
688       * <p>
689       * If neither of the two conditions above exist, an attempt will be made
690       * to honor the new center and width.  However, if doing so would lead to
691       * a negative value for the low endpoint of this range, the width will
692       * be honored, but the center will not.  The center would instead be
693       * a frequency equal to one half the width, as the low frequency would be
694       * zero and the high frequency equal to the width.</p> 
695       * 
696       * @param center the new center frequency of this range. 
697       * 
698       * @param width the new bandwidth of this range.
699       * 
700       * @return this range after changing its endpoints.
701       */
702      public FrequencyRange setCenterAndWidth(Frequency center, Frequency width)
703      {
704        if ((center == null) || (width == null))
705        {
706          //do nothing
707        }
708        else if (center.isInfinite() || width.isInfinite())
709        {
710          makeThisRangeInfinite(center.getUnits());
711        }
712        else //finite width
713        {
714          Frequency halfWidth = width.clone().divideBy("2.0");
715    
716          //If the incoming parameters would lead to a negative value for the
717          //low endpoint, preserve the desired width, but not the center frequency
718          if (center.compareTo(halfWidth) < 0)
719          {
720            FrequencyUnits units = center.getUnits();
721    
722            lowFrequency.set("0.0", units);
723            highFrequency.set(width.toUnits(units), units);
724          }
725          else //we have enough room to honor request
726          {
727            lowFrequency  = center.clone().subtract(halfWidth);
728            highFrequency = center.clone().add(halfWidth);
729          }
730        }
731        
732        return this;
733      }
734      
735      /**
736       * Moves both the low and high endpoints of this range down by
737       * {@code shiftSize}.
738       * <p>
739       * If {@code shiftSize} is <i>null</i> this method will take no action
740       * and this range will have the same state as immediately before the call.</p>
741       * <p>
742       * If the size of the shift is infinite, this range will
743       * be reset so that its both its low and high frequencies have a value
744       * of zero.</p>
745       * <p>
746       * If neither of the two conditions above exist, an attempt will be made
747       * to honor the shift while leaving the width of this range unchanged.
748       * If, however, doing so would lead to
749       * a negative value for the low endpoint of this range, this range's
750       * width will be preserved and the value of the low frequency will be
751       * set to zero.  In otherwords, this method will make the biggest legal
752       * shift that is less than or equal than the requested shift.</p>
753       *  
754       * @param shiftSize the amount by which to decrease both the high and
755       *                  low frequencies of this range.
756       *                  
757       * @return this range after the frequency shift.
758       */
759      public FrequencyRange shiftDownBy(Frequency shiftSize)
760      {
761        if (shiftSize == null)
762        {
763          //do nothing
764        }
765        else if (shiftSize.isInfinite())
766        {
767          lowFrequency.setValue( "0.0");
768          highFrequency.setValue("0.0");
769        }
770        else //finite shift
771        {
772          setCenterAndWidth(getCenterFrequency().subtract(shiftSize), getWidth());
773        }
774        
775        return this;
776      }
777      
778      /**
779       * Moves both the low and high endpoints of this range up by
780       * {@code shiftSize}.
781       * <p>
782       * If {@code shiftSize} is <i>null</i> this method will take no action
783       * and this range will have the same state as immediately before the call.</p>
784       * <p>
785       * If the size of the shift is infinite, this range will
786       * be reset so that its both its low and high frequencies have a value
787       * of {@code Double.POSITIVE_INFINITY}.</p>
788       * <p>
789       * If neither of the two conditions above exist, the requested shift
790       * will be made.</p>
791       *  
792       * @param shiftSize the amount by which to increase both the high and
793       *                  low frequencies of this range.
794       *                  
795       * @return this range after the frequency shift.
796       */
797      public FrequencyRange shiftUpBy(Frequency shiftSize)
798      {
799        if (shiftSize == null)
800        {
801          //do nothing
802        }
803        else if (shiftSize.isInfinite())
804        {
805          lowFrequency.setValue("+infinity");
806          highFrequency.setValue("+infinity");
807        }
808        else
809        {
810          setCenterAndWidth(getCenterFrequency().add(shiftSize), getWidth());
811        }
812        
813        return this;
814      }
815    
816      /**
817       * Multiplies the low and high frequencies of this range by {@code multiplier}.
818       * 
819       * @param multiplier
820       *   the number by which the end points of this range should be multiplied.
821       *   This value must not result in a frequency magnitude that is
822       *   negative.
823       *        
824       * @return this range, after the multiplication.
825       * 
826       * @throws ArithmeticException
827       *   if the multiplication results in a negative value for an end point, and  
828       *   that end point is not allowed to be negative.
829       *   
830       * @throws NullPointerException
831       *   if <tt>multiplier</tt> is <i>null</i>.
832       *   
833       * @since 2008-11-06
834       */
835      public FrequencyRange multiplyBy(String multiplier)
836      {
837        return multiplyBy(new BigDecimal(multiplier));
838      }
839    
840      /**
841       * Multiplies the low and high frequencies of this range by {@code multiplier}.
842       * 
843       * @param multiplier
844       *   the number by which the end points of this range should be multiplied.
845       *   This value must not result in a frequency magnitude that is
846       *   negative.
847       *        
848       * @return this range, after the multiplication.
849       * 
850       * @throws ArithmeticException
851       *   if the multiplication results in a negative value for an end point, and  
852       *   that end point is not allowed to be negative.
853       *   
854       * @throws NullPointerException
855       *   if <tt>multiplier</tt> is <i>null</i>.
856       *   
857       * @since 2008-11-06
858       */
859      public FrequencyRange multiplyBy(BigDecimal multiplier)
860      {
861        lowFrequency.multiplyBy(multiplier);
862        highFrequency.multiplyBy(multiplier);
863        
864        return this;
865      }
866      
867      /**
868       * Moves this range so that its new center frequency is {@code newCenter}.
869       * <p>
870       * If {@code newCenter} is <i>null</i> this method will take no action
871       * and this range will have the same state as immediately before the call.</p>
872       * <p>
873       * If the new center is infinite, this range will
874       * be reset so that its both its low and high frequencies have a value
875       * of {@code Double.POSITIVE_INFINITY}.</p>
876       * <p>
877       * If neither of the two conditions above exist, an attempt will be made
878       * to honor the shift to the new center while leaving the width of this
879       * range unchanged.
880       * If, however, doing so would lead to
881       * a negative value for the low endpoint of this range, this range's
882       * width will be preserved and the value of the low frequency will be
883       * set to zero.  In otherwords, this method will make the biggest legal
884       * shift that is less than or equal than the requested shift, and the
885       * resulting center might not be equal to {@code newCenter}.</p>
886       * 
887       * @param newCenter the new center frequency of this range.
888       * 
889       * @return this range after its center has been shifted.
890       */
891      public FrequencyRange moveCenterTo(Frequency newCenter)
892      {
893        if (newCenter == null)
894        {
895          //do nothing
896        }
897        else
898        {
899          Frequency currentCenter = getCenterFrequency();
900          
901          if (newCenter.compareTo(currentCenter) > 0)
902          {
903            shiftUpBy(newCenter.clone().subtract(currentCenter));
904          }
905          else
906          {
907            shiftDownBy(currentCenter.subtract(newCenter));
908          }
909        }
910    
911        return this;
912      }
913      
914      /**
915       * Changes the width of this range to {@code newWidth}.
916       * <p>
917       * If {@code newWidth} is <i>null</i> this method will take no action
918       * and this range will have the same state as immediately before the call.</p>
919       * <p>
920       * If the new width is infinite, this range will
921       * be reset so that its low frequency has a value of zero and its high
922       * frequency has an infinite value.</p>
923       * <p>
924       * If neither of the two conditions above exist, an attempt will be made
925       * to honor the new width while leaving the center frequency unchanged.
926       * However, if doing so would lead to
927       * a negative value for the low endpoint of this range, the new width will
928       * be honored, but the center will be altered.  The new center will be
929       * a frequency equal to one half the width, as the low frequency would be
930       * zero and the high frequency equal to the width.</p> 
931       * 
932       * @param newWidth the new bandwidth of this range.
933       * 
934       * @return this range after the change in bandwidth
935       */
936      public FrequencyRange changeWidthTo(Frequency newWidth)
937      {
938        return setCenterAndWidth(getCenterFrequency(), newWidth);
939      }
940      
941      //===========================================================================
942      // TEXT METHODS
943      //===========================================================================
944    
945      /** Returns a text representation of this frequency range. */
946      public String toString()
947      {
948        return toString(true);
949      }
950    
951      /**
952       * Returns a text representation of this frequency range.
953    
954       * @param lowToHigh
955       *   if <true>, the low frequency is displayed before the high frequency.
956       *   
957       * @return
958       *   a text representation of this frequency range.
959       */
960      public String toString(boolean lowToHigh)
961      {
962        StringBuilder buff = new StringBuilder();
963        
964        Frequency left  = lowToHigh ? lowFrequency  : highFrequency;
965        Frequency right = lowToHigh ? highFrequency : lowFrequency;
966        
967        buff.append(left)
968            .append(FormatString.ENDPOINT_SEPARATOR)
969            .append(right);
970        
971        return buff.toString();
972        
973      }
974      
975      /**
976       * Returns a text representation of this frequency range.
977       * 
978       * @param minFracDigits the minimum number of places after the decimal point.
979       * @param maxFracDigits the maximum number of places after the decimal point.
980       * 
981       * @return a text representation of this frequency range.
982       */
983      public String toString(int minFracDigits, int maxFracDigits)
984      {
985        return toString(minFracDigits, maxFracDigits, true);
986      }
987    
988      /**
989       * Returns a text representation of this frequency range.
990       * 
991       * @param minFracDigits the minimum number of places after the decimal point.
992       * @param maxFracDigits the maximum number of places after the decimal point.
993       * @param lowToHigh
994       *   if <true>, the low frequency is displayed before the high frequency.
995       * 
996       * @return a text representation of this frequency range.
997       */
998      public String toString(int minFracDigits, int maxFracDigits, boolean lowToHigh)
999      {
1000        StringBuilder buff = new StringBuilder();
1001        
1002        Frequency left  = lowToHigh ? lowFrequency  : highFrequency;
1003        Frequency right = lowToHigh ? highFrequency : lowFrequency;
1004        
1005        buff.append(left.toString(minFracDigits, maxFracDigits));
1006        buff.append(FormatString.ENDPOINT_SEPARATOR);
1007        buff.append(right.toString(minFracDigits, maxFracDigits));
1008        
1009        return buff.toString();
1010      }
1011    
1012      /**
1013       * Creates and returns a new frequency range, based on {@code rangeText}
1014       * and a separator string of {@link FormatString#ENDPOINT_SEPARATOR}.
1015       * <p>
1016       * See {@link #parse(String, String)} for more details.</p>
1017       * 
1018       * @param rangeText text that represents a frequency range.
1019       * 
1020       * @return a new frequency range, based on {@code rangeText} and a separator
1021       *         string of {@link FormatString#ENDPOINT_SEPARATOR}.
1022       */
1023      public static FrequencyRange parse(String rangeText)
1024      {
1025        return parse(rangeText, FormatString.ENDPOINT_SEPARATOR);
1026      }
1027      
1028      /**
1029       * Creates and returns a new frequency range, based on {@code rangeText}.
1030       * <p>
1031       * The general form {@code rangeText} is
1032       * <i style="color:blue">frequencyOne</i>
1033       * <i style="color:red">EndPointSeparator</i>
1034       * <i style="color:blue">frequencyTwo</i>,
1035       * where <i>frequencyOne</i> and <i>frequencyTwo</i> follow the rules set
1036       * forth in {@link Frequency#parse(String)} and <i>endPointSeparator</i>
1037       * is text that does not collide with text that is normally found in
1038       * the frequency text.  (That is, it would be unwise to use a number,
1039       * period, or any of the alpha characters that might be present in
1040       * the units portion of a frequency in the endpoint separator.
1041       * Typical separators might be <tt>" - "</tt> or <tt>","</tt>.)
1042       * Note: using "-" (without spaces) as a separator will cause problems if either
1043       * endpoint is negative.</p>
1044       * <p>
1045       * If {@code rangeText} is <i>null</i> or the empty string (<tt>""</tt>),
1046       * the returned range will be equal to one created via the no-argument
1047       * constructor.</p>
1048       * 
1049       * @param rangeText text that represents a frequency range.
1050       * 
1051       * @param endPointSeparator the text that separates one endpoint of the range
1052       *                          from the other.
1053       *                          
1054       * @return a new frequency range, based on {@code rangeText}.
1055       */
1056      public static FrequencyRange parse(String rangeText, String endPointSeparator)
1057      {
1058        FrequencyRange range = new FrequencyRange();
1059    
1060        if ((rangeText != null) && !rangeText.equals(""))
1061        {
1062          try {
1063            range.parseRange(rangeText, endPointSeparator);
1064          }
1065          catch (Exception ex) {
1066            throw new IllegalArgumentException("Could not parse " + rangeText, ex);
1067          }
1068        }
1069    
1070        return range;
1071      }
1072      
1073      /**
1074       * If parsing was successful, this range's endpoints will have been
1075       * valued.  Otherwise an exception is thrown.
1076       */
1077      private void parseRange(String rangeText, String endPointSeparator)
1078      {
1079        String errMsg = null;
1080    
1081        String[] endPoints = rangeText.split(endPointSeparator, -1);
1082    
1083        if (endPoints.length == 2)
1084        {
1085          try
1086          {
1087            Frequency f0 = Frequency.parse(endPoints[0]);
1088            Frequency f1 = Frequency.parse(endPoints[1]);
1089    
1090            set(f0, f1);
1091          }
1092          catch (IllegalArgumentException ex)
1093          {
1094            errMsg = ex.getMessage();
1095          }
1096        }
1097        else //bad # of endpoints
1098        {
1099          errMsg = "Found " + endPoints.length + " endpoints. There should be 2.";
1100        }
1101    
1102        //Error message
1103        if (errMsg != null)
1104        {
1105          errMsg = errMsg + " [Params: rangeText=" + rangeText +
1106                            ", endPointSeparator=" + endPointSeparator + "]";
1107          
1108          throw new IllegalArgumentException(errMsg);
1109        }
1110      }
1111      
1112      //===========================================================================
1113      // UTILITY METHODS
1114      //===========================================================================
1115      
1116      /** Creates a copy of this frequency range. */
1117      public FrequencyRange clone()
1118      {
1119        FrequencyRange clone = null;
1120        
1121        try
1122        {
1123          clone = (FrequencyRange)super.clone();
1124          clone.highFrequency = this.highFrequency.clone();
1125          clone.lowFrequency  = this.lowFrequency.clone();
1126        }
1127        catch (CloneNotSupportedException ex)
1128        {
1129          throw new RuntimeException(ex);
1130        }
1131        
1132        return clone;
1133      }
1134    
1135      /** Returns <i>true</i> if {@code o} is equal to this frequency range. */
1136      public boolean equals(Object o)
1137      {
1138        //Quick exit if o is this
1139        if (o == this)
1140          return true;
1141        
1142        //Quick exit if o is null
1143        if (o == null)
1144          return false;
1145        
1146        //Quick exit if classes are different
1147        if (!o.getClass().equals(this.getClass()))
1148          return false;
1149        
1150        FrequencyRange otherRange = (FrequencyRange)o;
1151        
1152        return otherRange.highFrequency.equals(this.highFrequency) &&
1153               otherRange.lowFrequency.equals(this.lowFrequency);
1154      }
1155      
1156      /** Returns a hash code value for this frequency range. */
1157      public int hashCode()
1158      {
1159        return toString().hashCode();
1160      }
1161      
1162      /**
1163       * Compares this range to {@code other} for order.
1164       * <p>
1165       * One range is deemed to be "less than" the other if it begins
1166       * before the other.  In the case that two ranges start at the
1167       * same point, the one with a lesser endpoint is considered to be less
1168       * than the other.</p>
1169       * 
1170       * @param other the range to which this one is compared.
1171       * 
1172       * @return a negative integer, zero, or a positive integer as this range
1173       *         is less than, equal to, or greater than the other range.
1174       */
1175      public int compareTo(FrequencyRange other)
1176      {
1177        int answer = this.lowFrequency.compareTo(other.lowFrequency);
1178        
1179        //If the ranges start at the same point, compare the end points
1180        if (answer == 0)
1181          answer = this.highFrequency.compareTo(other.highFrequency);
1182        
1183        return answer;
1184      }
1185    }