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