001    package edu.nrao.sss.measure;
002    
003    import java.math.MathContext;
004    import java.math.RoundingMode;
005    import java.util.ArrayList;
006    import java.util.Collection;
007    import java.util.SortedSet;
008    import java.util.TreeSet;
009    
010    /**
011     * A collection of disjoint (non-overlapping, non-contiguous) frequency ranges.
012     * These ranges are said to be "covered" portions of the spectrum.
013     * <p>
014     * The span of this spectrum goes from the low frequency of its lowest covered
015     * range through the high frequency of its highest covered range.  This spectrum
016     * contains only covered frequency <i>ranges</i> -- it does not handle
017     * covered frequency <i>points</i>.  (An approximation of a covered single
018     * frequency can be made by using an extremely narrow range.)</p> 
019     * <p>
020     * <b>Version Info:</b>
021     * <table style="margin-left:2em">
022     *   <tr><td>$Revision$</td></tr>
023     *   <tr><td>$Date$</td></tr>
024     *   <tr><td>$Author$</td></tr>
025     * </table></p>
026     * 
027     * @author David M. Harland
028     * @since 2007-01-11
029     */
030    public class FrequencySpectrum
031      implements Cloneable
032    {
033      private static FrequencyUnits STD_UNITS = FrequencyUnits.HERTZ;
034    
035      private static final MathContext PRECISION =
036        new MathContext(MathContext.DECIMAL128.getPrecision(),RoundingMode.HALF_UP);
037      
038      private SortedSet<FrequencyRange> coveredRanges;
039      
040      /**
041       * Creates a new spectrum with no covered ranges and, therefore, a span of
042       * zero Hertz.
043       */
044      public FrequencySpectrum()
045      {
046        coveredRanges = new TreeSet<FrequencyRange>();
047      }
048      
049      /**
050       * Creates a new spectrum using the given collection of covered ranges.
051       * @param ranges the portions of this spectrum that are covered.
052       */
053      public FrequencySpectrum(Collection<FrequencyRange> ranges)
054      {
055        this();
056        
057        for (FrequencyRange range : ranges)
058          addCoveredRange(range);
059      }
060      
061      //============================================================================
062      // 
063      //============================================================================
064      
065      /**
066       * Returns the regions of this spectrum that are covered.
067       * The returned ranges are copies of those held by this spectrum, so they
068       * may be altered by the client without affecting this object.
069       * 
070       * @return the regions of this spectrum that are covered.
071       */
072      public SortedSet<FrequencyRange> getCoveredRanges()
073      {
074        SortedSet<FrequencyRange> result = new TreeSet<FrequencyRange>();
075        
076        for (FrequencyRange coveredRange : coveredRanges)
077          result.add(coveredRange.clone());
078        
079        return result;
080      }
081      
082      //============================================================================
083      // DERIVED QUERIES
084      //============================================================================
085      
086      /**
087       * Returns a range whose low frequency is that of the lowest covered range
088       * in this spectrum and whose high frequency is that of the highest covered
089       * range.
090       * <p>
091       * For example if this spectrum has these covered ranges:
092       * <tt>10GHz-15GHz</tt> and <tt>50GHz-60GHz</tt>,
093       * then the span of this spectrum is <tt>10GHz-60GHz</tt>.</p>
094       * 
095       * @return a range representing the lowest and highest frequencies covered
096       *         herein.
097       */
098      public FrequencyRange getSpan()
099      {
100        if (coveredRanges.size() > 0)
101          return new FrequencyRange(coveredRanges.first().getLowFrequency(),
102                                    coveredRanges.last().getHighFrequency());
103        else
104          return new FrequencyRange(new Frequency(), new Frequency());
105      }
106      
107      /**
108       * Returns <i>true</i> if this spectrum covers the given frequency.
109       * 
110       * @param frequency
111       *   the frequency to be tested for containment.
112       *   
113       * @return <i>true</i> if this spectrum covers the given frequency.
114       */
115      public boolean covers(Frequency frequency)
116      {
117        if (frequency == null)
118          return false;
119        
120        for (FrequencyRange range : coveredRanges)
121          if (range.contains(frequency))
122            return true;
123        
124        return false;
125      }
126      
127      /**
128       * Returns the amount of this spectrum that is covered.
129       * <p>
130       * For example if this spectrum has these covered ranges:
131       * <tt>10GHz-15GHz</tt> and <tt>50GHz-60GHz</tt>,
132       * then the amount covered is <tt>15GHz</tt>
133       * (<tt>5GHz + 10GHz</tt>).</p>
134       * 
135       * @return the amount of this spectrum that is covered.
136       */
137      public Frequency getAmountCovered()
138      {
139        Frequency amountCovered = new Frequency("0.0");
140        
141        for (FrequencyRange r : coveredRanges)
142          amountCovered.add(r.getWidth());
143    
144        return amountCovered;
145      }
146      
147      /**
148       * Returns the amount of the target range that is covered by the
149       * covered portions of this spectrum.  In other words, the amount
150       * returned is the size of the intersection between the covered
151       * portions of this spectrum and the target range.
152       * <p>
153       * This method is useful when the range you want to test for coverage
154       * has a different span than the natural span of this spectrum.
155       * The span of this spectrum is defined by the low frequency of the
156       * lowest covered portion and the high frequency of the highest
157       * covered portion of this spectrum.</p>
158       * <p>
159       * For example if this spectrum has these covered ranges:
160       * <tt>10GHz-15GHz</tt> and <tt>50GHz-60GHz</tt>,
161       * and if the target range is <tt>0GHz-100GHz</tt>,
162       * then the amount covered is <tt>15GHz</tt>.
163       * If, however, the target range is <tt>35GHz-55GHz</tt>,
164       * then the amount covered is <tt>5GHz</tt>.</p>
165       *  
166       * @param targetRange a range to be tested for coverage.
167       * 
168       * @return the amount of the target range that is covered by the
169       *         covered portions of this spectrum.
170       */
171      public Frequency getAmountCovered(FrequencyRange targetRange)
172      {
173        Frequency amountCovered;
174        
175        //If the target range completely contains this spectrum, get a quick result
176        if (targetRange.contains(getSpan()))
177        {
178          amountCovered = getAmountCovered();
179        }
180        //Otherwise, sum the overlapping regions
181        else
182        {
183          amountCovered = new Frequency("0.0");
184          
185          for (FrequencyRange r : coveredRanges)
186            if (targetRange.overlaps(r))
187              amountCovered.add(targetRange.getOverlapWith(r).getWidth());
188        }
189        
190        return amountCovered;
191      }
192      
193      /**
194       * Returns a number that represents the portion of this spectrum
195       * that is covered.
196       * <p>
197       * For example if this spectrum has these covered ranges:
198       * <tt>10GHz-15GHz</tt> and <tt>50GHz-60GHz</tt>,
199       * then the portion of this spectrum that is covered is <tt>0.30</tt>.
200       * (The covered range is <tt>15GHz</tt> and the total span is
201       * <tt>50GHz</tt>.)</p>
202       * 
203       * @return the fraction of this spectrum that is covered.
204       *         The value returned is between zero and one, inclusive
205       *         (i.e., [0-1]).
206       */
207      public double getFractionCovered()
208      {
209        return getAmountCovered().toUnits(STD_UNITS)
210                                 .divide(getSpan().getWidth().toUnits(STD_UNITS),
211                                         PRECISION).doubleValue();
212      }
213      
214      /**
215       * Returns a number that represents the portion of the target range
216       * that is covered by the covered portions of this spectrum.
217       * <p>
218       * For example if this spectrum has these covered ranges:
219       * <tt>10GHz-15GHz</tt> and <tt>50GHz-60GHz</tt>,
220       * and if the target range is <tt>0GHz-100GHz</tt>,
221       * then the portion of this spectrum that is covered is <tt>0.15</tt>.
222       * If, however, the target range is <tt>35GHz-55GHz</tt>,
223       * then the portion of this spectrum that is covered is <tt>0.25</tt>
224       * (<tt>5GHz</tt> of overlap with the <tt>20GHz</tt>-wide target range).</p>
225       * 
226       * @param targetRange a range to be tested for coverage.
227       * 
228       * @return the fraction of the target range that is covered by the
229       *         covered portions of this spectrum.
230       *         The value returned is between zero and one, inclusive
231       *         (i.e., [0-1]).
232       */
233      public double getAmountCoveredAsFractionOf(FrequencyRange targetRange)
234      {
235        return
236          getAmountCovered(targetRange).toUnits(STD_UNITS)
237                                       .divide(targetRange.getWidth().toUnits(STD_UNITS),
238                                               PRECISION).doubleValue();
239      }
240      
241      /**
242       * Returns a quantity that represents the smallest gap between
243       * {@code targetRange} and the nearest of the covered ranges of this
244       * spectrum.
245       * <p>
246       * If the target range overlaps one or more of this spectrum's covered
247       * ranges, the returned range will have a width of zero and an arbitrary
248       * center frequency.  If this spectrum has no covered ranges, the size
249       * of the range returned will be infinite.</p>
250       * 
251       * @param targetRange the range for which a gap is calculated.
252       * 
253       * @return the smallest gap between {@code targetRange} and the nearest
254       *         of this spectrum's covered ranges.
255       */
256      public FrequencyRange getSmallestGapTo(FrequencyRange targetRange)
257      {
258        FrequencyRange smallestGap = new FrequencyRange(); //an infinite range
259        
260        for (FrequencyRange coveredRange : coveredRanges)
261        {
262          if (coveredRange.overlaps(targetRange))
263          {
264            Frequency zeroFreq = new Frequency();
265            smallestGap.setCenterAndWidth(zeroFreq, zeroFreq);
266            break;
267          }
268          
269          FrequencyRange gap = coveredRange.getGapBetween(targetRange);
270          
271          if (gap.getWidth().compareTo(smallestGap.getWidth()) < 0)
272            smallestGap = gap;
273        }
274    
275        return smallestGap;
276      }
277      
278      //============================================================================
279      // ADDING & REMOVING RANGES
280      //============================================================================
281    
282      /**
283       * Removes all covered ranges from this spectrum. 
284       */
285      public void clear()
286      {
287        coveredRanges.clear();
288      }
289      
290      /** @deprecated Use {@link #set(String)}. */
291      @Deprecated public void setCoveredRanges(String spectrumText)
292      {
293        set(spectrumText);
294      }
295      
296      /**
297       * Removes all covered ranges from this spectrum and adds new ranges found
298       * in {@code spectrumText}.
299       * See {@link #parse(String)} for details about the parsing methodology.
300       * 
301       * @param spectrumText text representation of a {@code FrequencySpectrum}.
302       */
303      public void set(String spectrumText)
304      {
305        if (spectrumText == null || spectrumText.equals(""))
306        {
307          clear();
308        }
309        else
310        {
311          TreeSet<FrequencyRange> previousRanges = new TreeSet<FrequencyRange>(coveredRanges);
312          clear();
313          try {
314            parseSpectrum(spectrumText, ",", "-");
315          }
316          catch (IllegalArgumentException ex) {
317            coveredRanges = previousRanges;
318            throw ex;
319          }
320        }
321      }
322      
323      /**
324       * Adds new covered ranges to this spectrum by parsing {@code spectrumText}.
325       * See {@link #parse(String)} for details about the parsing methodology.
326       * 
327       * @param spectrumText text representation of a {@code FrequencySpectrum}.
328       */
329      public void addCoveredRanges(String spectrumText)
330      {
331        parseSpectrum(spectrumText, ",", "-");
332      }
333      
334      /**
335       * Adds a new frequency range to our set of covered ranges.
336       * <p>
337       * The addition of a new range can take one of several tracks:</p>
338       * <ol>
339       *   <li>If {@code newRange} is <i>null</i>, this object is not changed.</li>
340       *   <li>If {@code newRange} has no overlap with any existing range, nor is
341       *       contiguous with any existing range, then a copy of it is added to
342       *       this object.</li>
343       *   <li>If {@code newRange} overlaps, or is continguous, with one or more
344       *       existing ranges, the single union of all those ranges replaces
345       *       those ranges.</li> 
346       * </ol>
347       * <p><u>Examples:</u></br>
348       * <pre>
349       *   Existing Ranges: ~~~~~~    ~~~~      ~~~~~~~~~~~~    ~~~~~~~   ~~~~~~~~~
350       *   New Range:                       +++
351       *   After Addition:  ~~~~~~~~  ~~~~  ~~~ ~~~~~~~~~~~~    ~~~~~~~   ~~~~~~~~~
352       * </pre>
353       * <pre>
354       *   Existing Ranges: ~~~~~~    ~~~~      ~~~~~~~~~~~~    ~~~~~~~   ~~~~~~~~~
355       *   New Range:                             +++
356       *   After Addition:  ~~~~~~~~  ~~~~      ~~~~~~~~~~~~    ~~~~~~~   ~~~~~~~~~
357       * </pre>
358       * <pre>
359       *   Existing Ranges: ~~~~~~    ~~~~      ~~~~~~~~~~~~    ~~~~~~~   ~~~~~~~~~
360       *   New Range:          +++++
361       *   After Addition:  ~~~~~~~~  ~~~~      ~~~~~~~~~~~~    ~~~~~~~   ~~~~~~~~~
362       * </pre>
363       * <pre>
364       *   Existing Ranges: ~~~~~~    ~~~~      ~~~~~~~~~~~~    ~~~~~~~   ~~~~~~~~~
365       *   New Range:          +++++++++
366       *   After Addition:  ~~~~~~~~~~~~~~      ~~~~~~~~~~~~    ~~~~~~~   ~~~~~~~~~
367       * </pre>
368       * <pre>
369       *   Existing Ranges: ~~~~~~    ~~~~      ~~~~~~~~~~~~    ~~~~~~~   ~~~~~~~~~
370       *   New Range:          ++++++++++++++++++++++++++++++++++++
371       *   After Addition:  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~   ~~~~~~~~~
372       * </pre>
373       * 
374       * @param newRange the range to be added to our set of covered ranges.
375       * @return this instance.
376       */
377      public FrequencySpectrum addCoveredRange(FrequencyRange newRange)
378      {
379        //Quick exit if range is null
380        if (newRange == null)
381          return this;
382    
383        FrequencyRange copyOfNewRange = newRange.clone();
384        
385        ArrayList<FrequencyRange> rangesToRemove = new ArrayList<FrequencyRange>();
386        
387        //Because the incoming range may overlap several of the existing ranges,
388        //we choose to merge the existing ranges into the (copy of) the new one.
389        //We also note which ranges overlapped and were contiguous with the
390        //new range and remove them before we leave this method.
391        for (FrequencyRange coveredRange : coveredRanges)
392        {
393          if (newRange.overlaps(coveredRange) ||
394              newRange.isContiguousWith(coveredRange))
395          {
396            copyOfNewRange.mergeWith(coveredRange);
397            rangesToRemove.add(coveredRange);
398          }
399        }
400    
401        //If we did any mergers, remove the now redundant ranges.
402        for (FrequencyRange unwantedRange : rangesToRemove)
403          coveredRanges.remove(unwantedRange);
404    
405        //Add the new range, which may have been merged with 0+ covered ranges
406        coveredRanges.add(copyOfNewRange);
407    
408        return this;
409      }
410      
411      /**
412       * Removes a frequency range from our set of covered ranges.
413       * </ol>
414       * <p><u>Examples:</u></br>
415       * <pre>
416       *   Existing Ranges: ~~~~~~    ~~~~      ~~~~~~~~~~~~    ~~~~~~~   ~~~~~~~~~
417       *   Unwanted Range:                  ---
418       *   After Removal:   ~~~~~~~~  ~~~~      ~~~~~~~~~~~~    ~~~~~~~   ~~~~~~~~~
419       * </pre>
420       * <pre>
421       *   Existing Ranges: ~~~~~~    ~~~~      ~~~~~~~~~~~~    ~~~~~~~   ~~~~~~~~~
422       *   Unwanted Range:                        ---
423       *   After Removal:   ~~~~~~~~  ~~~~      ~~   ~~~~~~~    ~~~~~~~   ~~~~~~~~~
424       * </pre>
425       * <pre>
426       *   Existing Ranges: ~~~~~~    ~~~~      ~~~~~~~~~~~~    ~~~~~~~   ~~~~~~~~~
427       *   Unwanted Range:     -----
428       *   After Removal:   ~~~       ~~~~      ~~~~~~~~~~~~    ~~~~~~~   ~~~~~~~~~
429       * </pre>
430       * <pre>
431       *   Existing Ranges: ~~~~~~    ~~~~      ~~~~~~~~~~~~    ~~~~~~~   ~~~~~~~~~
432       *   Unwanted Range:     ---------
433       *   After Removal:   ~~~         ~~      ~~~~~~~~~~~~    ~~~~~~~   ~~~~~~~~~
434       * </pre>
435       * <pre>
436       *   Existing Ranges: ~~~~~~    ~~~~      ~~~~~~~~~~~~    ~~~~~~~   ~~~~~~~~~
437       *   Unwanted Range:     ------------------------------------
438       *   After Removal:   ~~~                                    ~~~~   ~~~~~~~~~
439       * </pre>
440       * 
441       * @param unwantedRange the range to be removed from our set of covered ranges.
442       * @return this instance.
443       */
444      public FrequencySpectrum removeCoveredRange(FrequencyRange unwantedRange)
445      {
446        //Quick exit if range is null
447        if (unwantedRange == null)
448          return this;
449     
450        ArrayList<FrequencyRange> rangesToRemove = new ArrayList<FrequencyRange>();
451        ArrayList<FrequencyRange> rangesToTrim   = new ArrayList<FrequencyRange>();
452        FrequencyRange            rangeToSplit   = null;
453    
454        //Identify the ranges to be removed and those to be altered
455        for (FrequencyRange coveredRange : coveredRanges)
456        {
457          //Completely contained ranges will be removed.
458          if (unwantedRange.contains(coveredRange))
459          {
460            rangesToRemove.add(coveredRange);
461          }
462          //Unwanted ranges is not same as covered range, but IS contained by it.
463          //The covered range needs to be split into two disjoint ranges.
464          else if (coveredRange.contains(unwantedRange))
465          {
466            if (rangeToSplit != null)
467              throw new RuntimeException(
468                "PROGRAMMER ERROR: Only one range can contain the unwanted range.");
469            
470            rangeToSplit = coveredRange;
471            //we could break the loop here, but we keep going for Q/A
472          }
473          //Partially overlapping ranges will be trimmed.
474          else if (unwantedRange.overlaps(coveredRange))
475          {
476            rangesToTrim.add(coveredRange);
477          }
478        }
479        
480        //Check the above algorithm for errors.  Runtime exception if faulty.
481        //We could handle instead with an assertion that is deactivated in production.
482        validateRemoveCoveredRange(rangesToRemove, rangesToTrim, rangeToSplit);
483        
484        //Perform the needed actions
485        coveredRanges.removeAll(rangesToRemove);
486        trimRanges(rangesToTrim, unwantedRange);
487        splitRange(rangeToSplit, unwantedRange);
488        
489        return this;
490      }
491      
492      /**
493       * Modifies this spectrum to be the intersection of its covered ranges
494       * with {@code targetRange}. 
495       * If this range does not intersect with {@code other}, it will have
496       * no covered ranges after the intersection.
497       * 
498       * @param targetRange the frequency range with which this spectrum
499       *                    should be intersected.
500       *                    
501       * @return this spectrum after the intersection.
502       */
503      public FrequencySpectrum intersectWith(FrequencyRange targetRange)
504      {
505        ArrayList<FrequencyRange> overlappingRanges =
506          new ArrayList<FrequencyRange>();
507        
508        //Find all the overlapping ranges
509        for (FrequencyRange coveredRange : coveredRanges)
510        {
511          if (coveredRange.overlaps(targetRange))
512            overlappingRanges.add(coveredRange.getOverlapWith(targetRange));
513        }
514        
515        //Remove all existing ranges
516        coveredRanges.clear();
517        
518        //Add all the overlapping ranges.
519        //Note: we could probably safely add these directly to the underlying list.
520        //      However, we take the safer, though slower, approach here.
521        for (FrequencyRange overlappingRange : overlappingRanges)
522          addCoveredRange(overlappingRange);
523        
524        return this;
525      }
526      
527      /** Aids removeCoveredRange. */
528      private void trimRanges(ArrayList<FrequencyRange> rangesToTrim,
529                              FrequencyRange unwantedRange)
530      {
531        //Trim the 0, 1, or 2 partially overlapping ranges
532        for (FrequencyRange rangeToTrim : rangesToTrim)
533        {
534          //We remove the high end of this range
535          if (rangeToTrim.getLowFrequency()
536                         .compareTo(unwantedRange.getLowFrequency()) < 0)
537          {
538            rangeToTrim.set(  rangeToTrim.getLowFrequency(),
539                            unwantedRange.getLowFrequency());
540          }
541          //We remove the low end of this range
542          else if (rangeToTrim.getHighFrequency()
543                              .compareTo(unwantedRange.getHighFrequency()) > 0)
544          {
545            rangeToTrim.set(unwantedRange.getHighFrequency(),
546                              rangeToTrim.getHighFrequency());
547          }
548        }
549      }
550      
551      /** Aids removeCoveredRange. */
552      private void splitRange(FrequencyRange rangeToSplit,
553                              FrequencyRange unwantedRange)
554      {
555        if (rangeToSplit != null)
556        {
557          FrequencyRange newLow  =
558            new FrequencyRange(rangeToSplit.getLowFrequency(),
559                               unwantedRange.getLowFrequency());
560          FrequencyRange newHigh =
561            new FrequencyRange(unwantedRange.getHighFrequency(),
562                               rangeToSplit.getHighFrequency());
563          
564          coveredRanges.remove(rangeToSplit);
565          coveredRanges.add(newLow);
566          coveredRanges.add(newHigh);
567        }
568      }
569    
570      /**
571       * Applies some Q/A to removeCoveredRange intermediate results
572       * and throws runtime exception if algorithm's logic is faulty.
573       */
574      private
575        void validateRemoveCoveredRange(ArrayList<FrequencyRange> rangesToRemove,
576                                        ArrayList<FrequencyRange> rangesToTrim,
577                                        FrequencyRange            rangeToSplit)
578      {
579        //If we're splitting a range, we cannot be removing or trimming any other
580        if (rangeToSplit != null)
581        {
582          if (rangesToRemove.size() > 0 || rangesToTrim.size() > 0)
583            throw new RuntimeException(
584               "PROGRAMMER ERROR: if we're splitting a range," +
585               " there should be no ranges to remove or trim." +
586               " ToRemove=" + rangesToRemove.size() +
587               ", ToTrim=" + rangesToTrim.size());
588        }
589        else  //no ranges to split
590        {
591          //Ranges to split s/b 0, 1, or 2
592          int trimCount = rangesToTrim.size();
593          if (trimCount > 2)
594            throw new RuntimeException(
595              "PROGRAMMER ERROR: the maximum number of ranges to trim is 2. " +
596              " The algorithm produced " + trimCount);
597        }
598      }
599      
600      //============================================================================
601      // TEXT
602      //============================================================================
603    
604      /** Creates a text representation of this spectrum. */
605      @Override
606      public String toString()
607      {
608        if (coveredRanges.size() > 0)
609        {
610          StringBuilder buff = new StringBuilder();
611    
612          FrequencyRange lastRange = coveredRanges.last();
613          
614          for (FrequencyRange coveredRange : coveredRanges)
615          {
616            buff.append(coveredRange.toString());
617            
618            if (coveredRange != lastRange)
619              buff.append(", ");
620          }
621    
622          return buff.toString();
623        }
624        else
625        {
626          return "";
627        }
628      }
629      
630      /**
631       * Creates and returns a new frequency spectrum, based on
632       * {@code spectrumText}.
633       * <p>
634       * This is a convenience method that is equivalent to calling
635       * {@link #parse(String, String, String)
636       * FrequencySpectrum.parse(spectrumText, ",", "-")}.</p>
637       *  
638       * @param spectrumText
639       *   text representation of a {@code FrequencySpectrum}.
640       * 
641       * @return
642       *   a new spectrum, based on {@code spectrumText}.
643       */
644      public static FrequencySpectrum parse(String spectrumText)
645      {
646        return FrequencySpectrum.parse(spectrumText, ",", "-");
647      }
648    
649      /**
650       * Creates and returns a new frequency spectrum, based on
651       * {@code spectrumText}.
652       * <p>
653       * This is a convenience method that is equivalent to calling
654       * {@link #parse(String, String, String)
655       * FrequencySpectrum.parse(spectrumText, rangeSeparator, "-")}.</p>
656       *  
657       * @param spectrumText
658       *   text representation of a {@code FrequencySpectrum}.
659       * 
660       * @param rangeSeparator
661       *   text used to separate the endpoints of a frequency range.
662       *                       
663       * @return a new spectrum, based on {@code spectrumText}.
664       */
665      public static FrequencySpectrum parse(String spectrumText,
666                                            String rangeSeparator)
667      {
668        return FrequencySpectrum.parse(spectrumText, rangeSeparator, "-");
669      }
670      
671      /**
672       * Creates and returns a new frequency spectrum, based on
673       * {@code spectrumText}.
674       * <p>
675       * The {@code spectrumText} is nothing more than a list of delimited
676       * frequency ranges.  To learn more about how frequency ranges are parsed,
677       * see {@link FrequencyRange#parse(String)}.
678       * Each of these ranges is delimited by {@code rangeSeparator}.</p>
679       * <p>
680       * This method will attempt to parse the entire text and make use of
681       * any ranges that are successfully interpreted.  If there are any parsing
682       * errors, an {@code IllegalArgumentException} will be thrown, listing
683       * each of the errors in its message.</p>
684       * <p>
685       * If {@code spectrumText} is <i>null</i> or the empty string (<tt>""</tt>),
686       * the returned range will be equal to one created via the no-argument
687       * constructor.</p>
688       * 
689       * @param spectrumText
690       *   text representation of a {@code FrequencySpectrum}.
691       * 
692       * @param rangeSeparator
693       *   text used to separate the endpoints of a frequency range.
694       *                       
695       * @param endPointSeparator
696       *   text that separates one frequency range from another in
697       *   {@code spectrumText}.
698       *                          
699       * @return a new spectrum, based on {@code spectrumText}.
700       */
701      public static FrequencySpectrum parse(String spectrumText,
702                                            String rangeSeparator,
703                                            String endPointSeparator)
704      {
705        FrequencySpectrum spectrum = new FrequencySpectrum();
706        
707        if ((spectrumText != null) && !spectrumText.equals(""))
708        {
709          try {
710            spectrum.parseSpectrum(spectrumText, rangeSeparator, endPointSeparator);
711          }
712          catch (Exception ex) {
713            throw new IllegalArgumentException("Could not parse " +
714                                               spectrumText, ex);
715          }
716        }
717    
718        return spectrum;
719      }
720      
721      /**
722       * Adds new covered ranges to this spectrum by parsing {@code spectrumText}.
723       * Each successfully parsed range is added to this spectrum.
724       * If one or more parsing errors were detected, an
725       * {@code IllegalArgumentException} will be thrown, with each of the
726       * errors listed in its message.
727       *  
728       * @param spectrumText text representation of a {@code FrequencySpectrum}.
729       * 
730       * @param rangeSeparator
731       *   text used to separate the endpoints of a frequency range.
732       *                       
733       * @param endPointSeparator text that separates one frequency range from
734       *                          another in {@code spectrumText}.
735       */
736      private void parseSpectrum(String spectrumText,
737                                 String rangeSeparator, String endPointSeparator)
738      {
739        final String EOL = System.getProperty("line.separator");
740        
741        StringBuilder errBuff = new StringBuilder();
742        
743        String[] ranges = spectrumText.split(rangeSeparator, -1);
744    
745        for (String rangeText : ranges)
746        {
747          try {
748            addCoveredRange(FrequencyRange.parse(rangeText, endPointSeparator));
749          }
750          catch (Exception ex) {
751            errBuff.append(ex.getMessage()).append(EOL);
752          }
753        }
754        
755        if (errBuff.length() > 0)
756          throw new IllegalArgumentException(errBuff.toString());
757      }
758      
759      //============================================================================
760      // 
761      //============================================================================
762    
763      /**
764       *  Returns a copy of this spectrum.
765       *  <p>
766       *  If anything goes wrong during the cloning procedure,
767       *  a {@code RuntimeException} will be thrown.</p>
768       */
769      @Override
770      public FrequencySpectrum clone()
771      {
772        FrequencySpectrum clone = null;
773        
774        try
775        {
776          clone = (FrequencySpectrum)super.clone();
777          
778          clone.coveredRanges = new TreeSet<FrequencyRange>();
779          for (FrequencyRange r : this.coveredRanges)
780            clone.coveredRanges.add(r.clone());
781        }
782        catch (CloneNotSupportedException ex)
783        {
784          throw new RuntimeException(ex);
785        }
786        
787        return clone;
788      }
789    
790      /** Returns <i>true</i> if {@code o} is equal to this spectrum. */
791      @Override
792      public boolean equals(Object o)
793      {
794        //Quick exit if o is this
795        if (o == this)
796          return true;
797        
798        //Quick exit if o is null
799        if (o == null)
800          return false;
801        
802        //Quick exit if classes are different
803        if (!o.getClass().equals(this.getClass()))
804          return false;
805        
806        FrequencySpectrum other = (FrequencySpectrum)o;
807        
808        SortedSet<FrequencyRange> these =
809          new TreeSet<FrequencyRange>(this.coveredRanges);
810        
811        SortedSet<FrequencyRange> those =
812          new TreeSet<FrequencyRange>(other.coveredRanges);
813    
814        return those.equals(these);
815      }
816    
817      /** Returns a hash code value for this spectrum. */
818      public int hashCode()
819      {
820        return coveredRanges.hashCode();
821      }
822    }