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 }