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