001 package edu.nrao.sss.measure; 002 003 import static edu.nrao.sss.math.MathUtil.MC_FINAL_CALC; 004 import static edu.nrao.sss.math.MathUtil.MC_INTERM_CALCS; 005 006 import java.math.BigDecimal; 007 import java.math.RoundingMode; 008 009 import javax.xml.bind.annotation.XmlAccessType; 010 import javax.xml.bind.annotation.XmlAccessorType; 011 import javax.xml.bind.annotation.XmlElement; 012 import javax.xml.bind.annotation.XmlType; 013 014 import edu.nrao.sss.math.MathUtil; 015 import edu.nrao.sss.util.StringUtil; 016 017 /** 018 * A time of day. 019 * <p> 020 * This class represents the time of day in hours, minutes, and seconds, 021 * without regard to any particular date. 022 * The default length of a day is 24 hours, or 86,400 seconds. 023 * However, clients may create days whose lengths are equal to any positive 024 * number of seconds.</p> 025 * <p> 026 * <b><u>Rollover</u></b><br/> 027 * Many operations on a time-of-day object can lead to a time that is less 028 * than zero or greater than the length of the day. These operations may 029 * choose not to raise an exception, but to instead roll past midnight in 030 * either direction. For example, using a 24-hour day, the {@code add} 031 * methods might take a time of 22:00:00 and an addend of 5 hours 43 032 * minutes to give a new time of day of 03:43:00. In general, rollover 033 * is the preferred behavior for methods of this class.</p> 034 * <p> 035 * <b><u>Start of Day vs. End of Day</u></b><br/> 036 * Related to rollover is the representation of an end-of-day time. 037 * If we use a 24-hour day, midnight can be represented by both 038 * 00:00:00.0 and 24:00:00.0. <i>In general this class represents 039 * midnight </i>only<i> by 00:00:00.0.</i> For example the following 040 * method call, {@code myTime.set(24, 0, 0.0);}, will result in a time 041 * of day that behaves as a beginning-of-day value, not as an 042 * end-of-day value. Likewise, any addition or subtraction that 043 * lands exactly on 24:00:00.0 will behave like 00:00:00.0.</p> 044 * <p> 045 * There is, however, one way to set an end-of-day value, and that 046 * is by calling the {@code setToEndOfDay} method. After a call 047 * to this method, the time of day will act like an end-of-day value 048 * and will have a text representation of 24:00:00.0.</p> 049 * <p> 050 * <b>Version Info:</b> 051 * <table style="margin-left:2em"> 052 * <tr><td>$Revision: 1707 $</td></tr> 053 * <tr><td>$Date: 2008-11-14 10:23:59 -0700 (Fri, 14 Nov 2008) $</td></tr> 054 * <tr><td>$Author: dharland $</td></tr> 055 * </table></p> 056 * 057 * @author David M. Harland 058 * @since 2006-07-27 059 */ 060 @XmlAccessorType(XmlAccessType.NONE) 061 @XmlType(propOrder= {"xmlTime","xmlDayLength"}) 062 public class TimeOfDay 063 implements Cloneable, Comparable<TimeOfDay> 064 { 065 private static final char TEXT_SEPARATOR = ':'; 066 067 private static final BigDecimal THOUSAND = new BigDecimal("1000.0"); 068 069 private static final int PRECISION = MC_FINAL_CALC.getPrecision(); 070 071 private static final BigDecimal DEFAULT_TIME = BigDecimal.ZERO; 072 073 /** 074 * The popular notion that a day is 24 hours long. This 075 * corresponds to 86,400.0 SI seconds. 076 */ 077 public static final BigDecimal STANDARD_DAY_LENGTH = new BigDecimal("86400.0"); 078 079 /** 080 * The length of a sidereal day, in SI seconds. 081 * The is value is 86,164.0905382. 082 */ 083 public static final BigDecimal SIDEREAL_DAY_LENGTH = new BigDecimal("86164.0905382"); 084 085 private BigDecimal secondsSinceMidnight; 086 087 //Note: the TimeOfDayInterval relies on the fact that this value is 088 //set at construction time and may not be altered subsequently. 089 private BigDecimal secondsPerDay; 090 091 //============================================================================ 092 // CONSTRUCTION 093 //============================================================================ 094 095 /** 096 * Creates instance whose length of day is the standard twenty four hours. 097 * The time of day is 00:00:00.0. 098 */ 099 public TimeOfDay() 100 { 101 this(STANDARD_DAY_LENGTH); 102 } 103 104 /** 105 * Creates instance whose length of day is given by 106 * {@code secondsInDay}. 107 * The time of day is 00:00:00.0. 108 * 109 * @param secondsInOneDay the number of seconds in one day. 110 */ 111 public TimeOfDay(BigDecimal secondsInOneDay) 112 { 113 //Need positive, finite, number of seconds 114 if (secondsInOneDay == null || 115 secondsInOneDay.signum() <= 0 || 116 MathUtil.doubleValueIsInfinite(secondsInOneDay)) 117 throw new IllegalArgumentException( 118 "A day must have a positive, finite, number of seconds. " + 119 secondsInOneDay + " is not a valid value."); 120 121 secondsPerDay = rescale(secondsInOneDay); 122 123 setAndRescale(DEFAULT_TIME); 124 } 125 126 private BigDecimal rescale(BigDecimal original) 127 { 128 BigDecimal rescaled; 129 130 int precision = original.precision(); 131 132 if (precision < PRECISION) 133 { 134 int newScale = 135 (original.signum() == 0) ? 1 : PRECISION - precision + original.scale(); 136 137 rescaled = original.setScale(newScale); 138 } 139 else if (precision > PRECISION) 140 { 141 rescaled = original.round(MC_FINAL_CALC); 142 } 143 else 144 { 145 rescaled = original; 146 } 147 148 return rescaled; 149 } 150 151 /** 152 * Creates a new time of day based on {@code timeText}. 153 * <p> 154 * The parsed text can be in any of the forms supported by 155 * {@link Longitude#parse(String)}. The most commonly used 156 * forms are <tt>hh:mm:ss.s</tt> and <tt>99h 99m 99.9s</tt>.</p> 157 * 158 * @param timeText the text to be parsed and converted into a time of day. 159 * If this value is <i>null</i> or <tt>""</tt> (the empty 160 * string), a new time corresponding to the start of the 161 * day is returned. 162 * 163 * @return a new time of day based on {@code timeText}. 164 * 165 * @throws IllegalArgumentException if {@code timeText} cannot be parsed. 166 * 167 * @see #parse(String, BigDecimal) 168 */ 169 public static TimeOfDay parse(String timeText) 170 { 171 return TimeOfDay.parse(timeText, TimeOfDay.STANDARD_DAY_LENGTH); 172 } 173 174 /** 175 * Creates a new time of day by parsing {@code timeText}. 176 * <p> 177 * The parsed text can be in any of the forms supported by 178 * {@link Longitude#parse(String)}. The most commonly used 179 * forms are <tt>hh:mm:ss.s</tt> and <tt>99h 99m 99.9s</tt>.</p> 180 * 181 * @param timeText the text to be parsed and converted into a time of day. 182 * If this value is <i>null</i> or <tt>""</tt> (the empty 183 * string), a new time corresponding to the start of the 184 * day is returned. 185 * 186 * @param secondsInOneDay the length of a day, in seconds. This class has two 187 * constants that express time in SI seconds, 188 * {@link #STANDARD_DAY_LENGTH} and 189 * {@link #SIDEREAL_DAY_LENGTH}, that my be used here. 190 * 191 * @return a new time of day based on {@code timeText}. 192 * 193 * @throws IllegalArgumentException if {@code timeText} cannot be parsed. 194 */ 195 public static TimeOfDay parse(String timeText, BigDecimal secondsInOneDay) 196 { 197 TimeOfDay result = new TimeOfDay(secondsInOneDay); 198 199 result.parseTimeOfDay(timeText); 200 201 return result; 202 } 203 204 /** Does the actual parsing. */ 205 private void parseTimeOfDay(String timeText) 206 { 207 //The parsing mechanism of Longitude is very nearly what we want 208 //here, so use it as a helper. 209 try 210 { 211 //Don't let longitude force to a 24hr day, since this class allows 212 //arbitrary day lengths. 213 Longitude helper = Longitude.parse(timeText, false); 214 Number[] pieces = helper.toHms(); 215 this.set(pieces[0].intValue(), 216 pieces[1].intValue(), (BigDecimal)pieces[2]); 217 } 218 //We want to accept one form that Longitude does not: 219 //hours and minutes w/ colon (hh:mm) 220 catch (Exception ex) 221 { 222 //We also support hh:mm, so see if we have that now. 223 String[] pieces = timeText.split(Character.toString(TEXT_SEPARATOR),-1); 224 if (pieces.length == 2) 225 { 226 int hours = Integer.parseInt(pieces[0]); 227 int minutes = Integer.parseInt(pieces[1]); 228 this.set(hours, minutes, BigDecimal.ZERO); 229 } 230 else 231 { 232 throw new IllegalArgumentException("Could not parse " + timeText + "."); 233 } 234 } 235 } 236 237 //============================================================================ 238 // SETTING THE TIME OF DAY 239 //============================================================================ 240 241 /** 242 * Sets the time of day. 243 * <p> 244 * While there are no restrictions on the values of the individual 245 * parameters, their combination must result in a positive finite number 246 * of seconds.</p> 247 * 248 * @param hourOfDay meant to represent the hour of the day. Typically this 249 * is a value in the range [0,23]. 250 * 251 * @param minuteOfHour meant to represent the minute of an hour. Typically 252 * this is a value in the range [0,59]. 253 * 254 * @param secondOfMinute meant to represent the second of a minute. 255 * Typically this is a value in the range [0.0,60.0). 256 * 257 * @throws IllegalArgumentException if the number of seconds calculated from 258 * the parameters is negative, infinite, or not a number. 259 */ 260 public void set(int hourOfDay, int minuteOfHour, BigDecimal secondOfMinute) 261 { 262 BigDecimal secFromMin = 263 TimeUnits.MINUTE.convertTo(TimeUnits.SECOND, new BigDecimal(minuteOfHour)); 264 265 BigDecimal secFromHr = 266 TimeUnits.HOUR.convertTo(TimeUnits.SECOND, new BigDecimal(hourOfDay)); 267 268 set(secondOfMinute.add(secFromMin).add(secFromHr)); 269 } 270 271 /** 272 * Sets the time of day. 273 * <p> 274 * While there are no restrictions on the values of the individual 275 * parameters, their combination must result in a positive finite number 276 * of seconds.</p> 277 * 278 * @param hourOfDay meant to represent the hour of the day. Typically this 279 * is a value in the range [0,23]. 280 * 281 * @param minuteOfHour meant to represent the minute of an hour. Typically 282 * this is a value in the range [0,59]. 283 * 284 * @param secondOfMinute meant to represent the second of a minute. 285 * Typically this is a value in the range [0.0,60.0). 286 * 287 * @throws IllegalArgumentException if the number of seconds calculated from 288 * the parameters is negative, infinite, or not a number. 289 */ 290 public void set(int hourOfDay, int minuteOfHour, String secondOfMinute) 291 { 292 set(hourOfDay, minuteOfHour, new BigDecimal(secondOfMinute)); 293 } 294 295 /** 296 * Sets the time of day to the given seconds-of-day. 297 * <p> 298 * The {@code secondOfDay} parameter must be a non-negative finite value. 299 * If it is greater than the length of a day, it will be rolled passed 300 * as many midnights as it takes to make it less than the length of a 301 * day.</p> 302 * 303 * @param secondOfDay the number of seconds that have elapsed since 304 * midnight to the desired time of day. 305 * 306 * @throws IllegalArgumentException if {@code secondOfDay} is negative, 307 * infinite, or not a number. 308 */ 309 public void set(BigDecimal secondOfDay) 310 { 311 //Need positive, finite, number of seconds 312 if (secondOfDay == null || 313 secondOfDay.signum() < 0 || 314 MathUtil.doubleValueIsInfinite(secondOfDay)) 315 throw new IllegalArgumentException( 316 "The time of day must be a non-negative, finite, number of seconds. " + 317 secondOfDay + " is not a valid time of day."); 318 319 normalizeAndSet(secondOfDay); 320 } 321 322 /** 323 * Sets this time of day to the end of the day. 324 * <p> 325 * This end-of-day value can be reached in no other way. It can 326 * not be set by the other setters, and it can not be reached via 327 * addition or subtraction.</p> 328 * <p> 329 * After this method has been called, this time of day behaves 330 * like an end-of-day value. All other values will be before, and 331 * less than, this value. Note that 332 * any non-negative increment to this time of day -- even an increment 333 * of zero -- will result in rollover.</p> 334 */ 335 public void setToEndOfDay() 336 { 337 secondsSinceMidnight = secondsPerDay; 338 } 339 340 /** 341 * Sets the value and units of this time of day based on {@code timeText}. 342 * See {@link #parse(String)} for the expected format of 343 * {@code timeText}. 344 * <p> 345 * If the parsing fails, this time of day will be kept in its current 346 * state.</p> 347 * 348 * @param timeText a string that will be converted into 349 * a time of day. 350 * 351 * @throws IllegalArgumentException if {@code timeText} is not in 352 * the expected form. 353 */ 354 public void set(String timeText) 355 { 356 if ((timeText == null) || timeText.equals("")) 357 { 358 set(DEFAULT_TIME); 359 } 360 else 361 { 362 BigDecimal oldTime = secondsSinceMidnight; 363 364 try { 365 this.parseTimeOfDay(timeText); 366 } 367 catch (Exception ex) { 368 secondsSinceMidnight = oldTime; 369 throw new IllegalArgumentException("Could not parse " + timeText, ex); 370 } 371 } 372 } 373 374 private void setAndRescale(BigDecimal newSecondsSinceMidnight) 375 { 376 secondsSinceMidnight = rescale(newSecondsSinceMidnight); 377 } 378 379 //============================================================================ 380 // TIME OF DAY QUERIES 381 //============================================================================ 382 383 /** 384 * Returns the hour of the day. 385 * For example, if the time of day is 12:34:56.789, 386 * the value returned will be 12. 387 * Contrast this with the call:<pre> 388 * getTimeSinceMidnight().toUnits(TimeUnits.HOUR);</pre> 389 * which would return 12.58244138888889. 390 * 391 * @return the hour of this time of day's day. 392 */ 393 public int getHourOfDay() 394 { 395 return TimeUnits.SECOND.convertTo(TimeUnits.HOUR, 396 secondsSinceMidnight).intValue(); 397 } 398 399 /** 400 * Returns the minute of this time of day's hour. 401 * For example, if the time of day is 12:34:56.789, 402 * the value returned will be 34. 403 * Contrast this with the call:<pre> 404 * getTimeSinceMidnight().toUnits(TimeUnits.MINUTE);</pre> 405 * which would return 754.9464833333333. 406 * 407 * @return the minute of this time of day's hour. 408 */ 409 public int getMinuteOfHour() 410 { 411 int wholeHours = getHourOfDay(); 412 413 int hourMinutes = 414 TimeUnits.HOUR.convertTo(TimeUnits.MINUTE, 415 new BigDecimal(wholeHours)).intValue(); 416 417 int wholeMinutes = 418 TimeUnits.SECOND.convertTo(TimeUnits.MINUTE, 419 secondsSinceMidnight).intValue(); 420 421 return wholeMinutes - hourMinutes; 422 } 423 424 /** 425 * Returns the second of this time of day's minute. 426 * For example, if the time of day is 12:34:56.789, 427 * the value returned will be 56.789. 428 * Contrast this with the call:<pre> 429 * getTimeSinceMidnight().toUnits(TimeUnits.SECOND);</pre> 430 * which would return 45,296.789, and to:<pre> 431 * getSecondOfMinuteWhole();</pre> 432 * which would return 56; 433 * 434 * @return the second of this time of day's minute. 435 */ 436 public BigDecimal getSecondOfMinute() 437 { 438 int wholeMinutes = 439 TimeUnits.SECOND.convertTo(TimeUnits.MINUTE, 440 secondsSinceMidnight).intValue(); 441 442 BigDecimal minuteSeconds = 443 TimeUnits.MINUTE.convertTo(TimeUnits.SECOND, new BigDecimal(wholeMinutes)); 444 445 return secondsSinceMidnight.subtract(minuteSeconds); 446 } 447 448 /** 449 * Returns the whole number of seconds of this time of day's minute. 450 * For example, if the time of day is 12:34:56.789, 451 * the value returned will be 56. 452 * Contrast this with the call:<pre> 453 * getTimeSinceMidnight().toUnits(TimeUnits.SECOND);</pre> 454 * which would return 45,296.789, and to:<pre> 455 * getSecondOfMinute();</pre> 456 * which would return 56.789; 457 * 458 * @return the whole number of seconds in this time of day's minute. 459 * 460 * @since 2008-09-26 461 */ 462 public int getSecondOfMinuteWhole() 463 { 464 return getSecondOfMinute().intValue(); 465 } 466 467 /** 468 * Returns the number of milliseconds in this time of day's seconds. 469 * For example, if the time of day is 12:34:56.7890123, 470 * the value returned will be 789.0123. 471 * 472 * @return the number of milliseconds in this time of day's seconds. 473 * 474 * @since 2008-09-26 475 */ 476 public BigDecimal getMilliOfSecond() 477 { 478 BigDecimal decimalSeconds = getSecondOfMinute(); 479 BigDecimal intSeconds = new BigDecimal(decimalSeconds.intValue()); 480 481 return rescale(decimalSeconds.subtract(intSeconds).multiply(THOUSAND)); 482 } 483 484 /** 485 * Returns the amount of time that has elapsed from midnight until 486 * this time of day. 487 * 488 * @return the amount of time since the previous midnight. 489 */ 490 public TimeDuration getTimeSinceMidnight() 491 { 492 return new TimeDuration(secondsSinceMidnight, TimeUnits.SECOND); 493 } 494 495 /** 496 * Returns the amount of time that will elapse from this time of day 497 * until midnight. 498 * 499 * @return the amount of time until the next midnight. 500 */ 501 public TimeDuration getTimeUntilMidnight() 502 { 503 BigDecimal secondsUntilMidnight = 504 secondsPerDay.subtract(secondsSinceMidnight); 505 506 return new TimeDuration(secondsUntilMidnight, TimeUnits.SECOND); 507 } 508 509 /** 510 * Returns <i>true</i> if this time of day is earlier than {@code other}. 511 * @param other the time to be tested against this one. 512 * @return <i>true</i> if this time of day is earlier than {@code other}. 513 */ 514 public boolean isBefore(TimeOfDay other) 515 { 516 return this.compareTo(other) < 0; 517 } 518 519 /** 520 * Returns <i>true</i> if this time of day is later than {@code other}. 521 * @param other the time to be tested against this one. 522 * @return <i>true</i> if this time of day is later than {@code other}. 523 */ 524 public boolean isAfter(TimeOfDay other) 525 { 526 return this.compareTo(other) > 0; 527 } 528 529 /** 530 * Returns <i>true</i> if this time of day is in its default state, 531 * no matter how it got there. 532 * <p> 533 * A time of day is in its <i>default state</i> if both its current time 534 * and the length of its day are the same as those of a time of day 535 * newly created via the {@link #TimeOfDay() no-argument constructor}.</p> 536 * 537 * @return <i>true</i> if this time of day is in its default state. 538 */ 539 public boolean isInDefaultState() 540 { 541 return secondsSinceMidnight.equals(DEFAULT_TIME) && 542 secondsPerDay.equals(STANDARD_DAY_LENGTH); 543 } 544 545 /** 546 * Returns the amount of time until this time of day equals {@code other}. 547 * <p> 548 * If the other time of day is equal to this one, the duration returned 549 * will have a length of zero. If the other time is later than this one, 550 * the duration will be a simple subtraction of this from other. If the 551 * other time, though, is earlier than this one, the duration will represent 552 * the distance from this time, through midnight, to the other time traveling 553 * forward in time. 554 * That is, the direction of time is always from this one to the other, 555 * even if that represents a longer duration than traversing time in the 556 * opposite direction.</p> 557 * <p> 558 * <b>Note</b> that the other time must have the same length of day 559 * as this one.</p> 560 * 561 * @param other the time of day to be reached from this one. 562 * 563 * @return the duration from this time of day to {@code other}. 564 * 565 * @throws IllegalArgumentException if {@code other}'s length of day is not 566 * equal to this object's length of day. 567 */ 568 public TimeDuration timeUntil(TimeOfDay other) 569 { 570 //Make sure both times have same length of day 571 if (!other.secondsPerDay.equals(this.secondsPerDay)) 572 throw new IllegalArgumentException( 573 "The other time must have the same length of day as this one."); 574 575 BigDecimal deltaSeconds = 576 other.secondsSinceMidnight.subtract(this.secondsSinceMidnight); 577 578 //Need to go from now, to midnight, then to other 579 if (deltaSeconds.signum() < 0) 580 { 581 deltaSeconds = 582 this.secondsPerDay.subtract(this.secondsSinceMidnight) 583 .add(other.secondsSinceMidnight); 584 } 585 586 return new TimeDuration(deltaSeconds, TimeUnits.SECOND); 587 } 588 589 /** 590 * Returns the time of day as a fraction of the length of 591 * day. The length of day is set at the time this time 592 * of day is constructed. 593 * 594 * @return the fraction of the day that has passed from 595 * midnight to this time of day. 596 */ 597 public BigDecimal toFractionOfDay() 598 { 599 return secondsSinceMidnight.divide(secondsPerDay, MC_INTERM_CALCS); 600 } 601 602 /** 603 * Non-public method that returns a value of unspecified time units 604 * that have passed from midnight to this time of day. This method 605 * is useful for comparing one time to another. Clients should make 606 * no assumptions, though, about the units of time being returned. 607 */ 608 BigDecimal size() 609 { 610 return secondsSinceMidnight; 611 } 612 613 //============================================================================ 614 // LENGTH OF DAY 615 //============================================================================ 616 617 /** 618 * Returns the length of day on which this time of day is based. 619 * @return the length of day on which this time of day is based. 620 */ 621 public TimeDuration getLengthOfDay() 622 { 623 return new TimeDuration(secondsPerDay, TimeUnits.SECOND); 624 } 625 626 //============================================================================ 627 // ARITHMETIC 628 //============================================================================ 629 630 /** 631 * Adds the given amount of time to this time of day. 632 * If the addition causes a time less than zero or greater than 633 * the last moment of the day, rollovers will occur until the 634 * result is a valid time of day. 635 * 636 * @param value an amount of time. 637 * @param units the units in which {@code value} is expressed. 638 * @return this time of day, after the addition has been performed. 639 * @throws IllegalArgumentException if {@code value} is infinite. 640 */ 641 public TimeOfDay add(String value, TimeUnits units) 642 { 643 return add(new BigDecimal(value), units); 644 } 645 646 /** 647 * Adds the given amount of time to this time of day. 648 * If the addition causes a time less than zero or greater than 649 * the last moment of the day, rollovers will occur until the 650 * result is a valid time of day. 651 * 652 * @param value an amount of time. 653 * @param units the units in which {@code value} is expressed. 654 * @return this time of day, after the addition has been performed. 655 * @throws IllegalArgumentException if {@code value} is infinite. 656 */ 657 public TimeOfDay add(BigDecimal value, TimeUnits units) 658 { 659 //Do not accept +/-infinity or NaN 660 if (MathUtil.doubleValueIsInfinite(value)) 661 throw new IllegalArgumentException("Cannot add " + value + 662 units.getSymbol() + 663 " to time of day. Value must be a real, finite, number."); 664 665 normalizeAndSet(secondsSinceMidnight.add(units.convertTo(TimeUnits.SECOND, 666 value))); 667 return this; 668 } 669 670 /** 671 * Adds the given amount of time to this time of day. 672 * If the addition causes a time less than zero or greater than 673 * the last moment of the day, rollovers will occur until the 674 * result is a valid time of day. 675 * 676 * @param duration a length of time. 677 * @return this time of day, after the addition has been performed. 678 * @throws IllegalArgumentException if {@code duration}'s value is 679 * infinite or NaN. 680 */ 681 public TimeOfDay add(TimeDuration duration) 682 { 683 return add(duration.getValue(), duration.getUnits()); 684 } 685 686 /** 687 * Subtracts the given amount of time from this time of day. 688 * If the subtraction causes a time less than zero or greater than 689 * the last moment of the day, rollovers will occur until the 690 * result is a valid time of day. 691 * 692 * @param value an amount of time. 693 * @param units the units in which {@code value} is expressed. 694 * @return this time of day, after the subtraction has been performed. 695 * @throws IllegalArgumentException if {@code value} is infinite. 696 */ 697 public TimeOfDay subtract(String value, TimeUnits units) 698 { 699 return subtract(new BigDecimal(value), units); 700 } 701 702 /** 703 * Subtracts the given amount of time from this time of day. 704 * If the subtraction causes a time less than zero or greater than 705 * the last moment of the day, rollovers will occur until the 706 * result is a valid time of day. 707 * 708 * @param value an amount of time. 709 * @param units the units in which {@code value} is expressed. 710 * @return this time of day, after the subtraction has been performed. 711 * @throws IllegalArgumentException if {@code value} is infinite. 712 */ 713 public TimeOfDay subtract(BigDecimal value, TimeUnits units) 714 { 715 return add(value.negate(), units); 716 } 717 718 /** 719 * Subtracts the given amount of time from this time of day. 720 * If the subtraction causes a time less than zero or greater than 721 * the last moment of the day, rollovers will occur until the 722 * result is a valid time of day. 723 * 724 * @param duration a length of time. 725 * @return this time of day, after the subtraction has been performed. 726 * @throws IllegalArgumentException if {@code duration}'s value is 727 * infinite. 728 */ 729 public TimeOfDay subtract(TimeDuration duration) 730 { 731 return add(duration.getValue().negate(), duration.getUnits()); 732 } 733 734 /** 735 * Transforms {@code seconds} into a non-negative value that is less 736 * than the number of seconds in a day and uses the transformed amount 737 * to set this time of day. 738 * 739 * This private method does NOT check for infinity or NaN; you must do 740 * that prior to the call. 741 */ 742 private void normalizeAndSet(BigDecimal seconds) 743 { 744 //Put seconds into range [0.0 - secondsPerDay) 745 if (seconds.signum() < 0) 746 { 747 //Mimic multiples = Math.ceil(seconds/ -secondsPerDay) 748 BigDecimal[] divAndRem = 749 seconds.divideAndRemainder(secondsPerDay.negate(), MC_INTERM_CALCS); 750 751 BigDecimal multiples = divAndRem[0]; //equiv to floor(x) 752 if (divAndRem[1].signum() < 0) 753 multiples = multiples.add(BigDecimal.ONE); 754 755 seconds = seconds.add(secondsPerDay.multiply(multiples)); 756 } 757 else if (seconds.compareTo(secondsPerDay) >= 0) 758 { 759 BigDecimal multiples = 760 seconds.divideToIntegralValue(secondsPerDay, MC_INTERM_CALCS); 761 762 seconds = seconds.subtract(secondsPerDay.multiply(multiples)); 763 } 764 765 //Exactly hitting top of clock 766 if (seconds.compareTo(secondsPerDay) == 0) 767 seconds = BigDecimal.ZERO; 768 769 setAndRescale(seconds); 770 771 assert secondsSinceMidnight.compareTo(BigDecimal.ZERO) >= 0 : secondsSinceMidnight; 772 assert secondsSinceMidnight.compareTo(secondsPerDay) < 0 : secondsSinceMidnight; 773 } 774 775 //============================================================================ 776 // CONVERSION TO OTHER FORMS 777 //============================================================================ 778 779 /** 780 * Creates an angular representation of this time of day. 781 * The angle returned is based on the ratio of the time of day to the 782 * length of a day. The best way to understand this is to picture a round 783 * clock with a single hand and with the zero point, or start of the day, 784 * at the top. The angle made, in a clockwise direction, between the current 785 * position of the hand and the top of the clock is the value returned. 786 * 787 * @return 788 * this time of day, expressed as an angle. 789 */ 790 public Angle toAngle() 791 { 792 BigDecimal fraction = secondsSinceMidnight.divide(secondsPerDay, 793 MC_INTERM_CALCS); 794 return 795 new Angle(fraction.multiply(new BigDecimal("100.0")), ArcUnits.PERCENT); 796 } 797 798 /** 799 * Creates a text representation of this time of day. 800 * The format of the returned string is <tt>H:M:S</tt>, 801 * where <tt>H, M,</tt> and <tt>S</tt> are as described 802 * in {@link #parse(String)}. 803 * 804 * @see #toString(int, int) 805 */ 806 public String toString() 807 { 808 return toString(0, -1); 809 } 810 811 /** 812 * Creates a text representation of this time of day. 813 * The format of the returned string is <tt>H:M:S</tt>, 814 * where <tt>H, M,</tt> and <tt>S</tt> are as described 815 * in {@link #parse(String)}. 816 * 817 * @param minFracDigits the minimum number of places after the decimal point 818 * for the seconds field. 819 * 820 * @param maxFracDigits the maximum number of places after the decimal point 821 * for the seconds field. 822 */ 823 public String toString(int minFracDigits, int maxFracDigits) 824 { 825 BigDecimal seconds = getSecondOfMinute(); 826 int minutes = getMinuteOfHour(); 827 int hours = getHourOfDay(); 828 829 //Rollover logic 830 //We have to worry about things like 59.999 being rounded to 60.0 831 if (maxFracDigits >= 0) 832 { 833 if (seconds.setScale(maxFracDigits, RoundingMode.HALF_UP).doubleValue() == 60.0) 834 { 835 seconds = BigDecimal.ZERO; 836 minutes++; 837 } 838 839 if (minutes == 60) 840 { 841 minutes = 0; 842 hours++; 843 } 844 845 if (hours == 24) 846 hours = 0; 847 } 848 849 //Formatting logic 850 StringBuilder buff = new StringBuilder(); 851 852 if (hours < 10) 853 buff.append('0'); 854 buff.append(hours).append(TEXT_SEPARATOR); 855 856 if (minutes < 10) 857 buff.append('0'); 858 buff.append(minutes).append(TEXT_SEPARATOR); 859 860 if (seconds.compareTo(BigDecimal.TEN) < 0) 861 buff.append('0'); 862 863 if (maxFracDigits >= 0) 864 buff.append(StringUtil.getInstance().formatNoScientificNotation( 865 seconds, minFracDigits, maxFracDigits)); 866 else //no rounding or truncation 867 buff.append( 868 StringUtil.getInstance().formatNoScientificNotation(seconds)); 869 870 return buff.toString(); 871 } 872 873 //=========================================================================== 874 // HELPERS FOR PERSISTENCE MECHANISMS 875 //=========================================================================== 876 877 @XmlElement(name="secondsPerDay") 878 @SuppressWarnings("unused") 879 private BigDecimal getXmlDayLength() 880 { 881 return secondsPerDay.compareTo(STANDARD_DAY_LENGTH) == 0 ? 882 null : secondsPerDay.stripTrailingZeros(); 883 } 884 @SuppressWarnings("unused") 885 private void setXmlDayLength(BigDecimal v) 886 { 887 secondsPerDay = (v == null) ? STANDARD_DAY_LENGTH : rescale(v); 888 } 889 890 @XmlElement(name="secondsSinceMidnight") 891 @SuppressWarnings("unused") 892 private BigDecimal getXmlTime() 893 { 894 return secondsSinceMidnight.stripTrailingZeros(); 895 } 896 @SuppressWarnings("unused") 897 private void setXmlTime(BigDecimal v) 898 { 899 setAndRescale(v == null ? BigDecimal.ZERO : v); 900 } 901 902 //============================================================================ 903 // 904 //============================================================================ 905 906 /** 907 * Returns a time of day that is equal to this one. 908 * <p> 909 * If anything goes wrong during the cloning procedure, 910 * a {@code RuntimeException} will be thrown.</p> 911 */ 912 public TimeOfDay clone() 913 { 914 TimeOfDay clone = null; 915 916 try 917 { 918 //This line takes care of the primitive fields properly 919 clone = (TimeOfDay)super.clone(); 920 } 921 catch (Exception ex) 922 { 923 throw new RuntimeException(ex); 924 } 925 926 return clone; 927 } 928 929 /** Returns <i>true</i> if {@code o} is equal to this time of day. */ 930 public boolean equals(Object o) 931 { 932 //Quick exit if o is this 933 if (o == this) 934 return true; 935 936 //Quick exit if o is null 937 if (o == null) 938 return false; 939 940 //Quick exit if classes are different 941 if (!o.getClass().equals(this.getClass())) 942 return false; 943 944 TimeOfDay other = (TimeOfDay)o; 945 946 return compareTo(other) == 0; 947 } 948 949 /** Returns a hash code value for this time of day. */ 950 public int hashCode() 951 { 952 //Taken from the Effective Java book by Joshua Bloch. 953 //The constants 17 & 37 are arbitrary & carry no meaning. 954 int result = 17; 955 956 result = 37 * result + secondsSinceMidnight.hashCode(); 957 result = 37 * result + secondsPerDay.hashCode(); 958 959 return result; 960 } 961 962 /** 963 * Compares this time of day to {@code other} for order. 964 * <p> 965 * One time is deemed to be "less than" the other if it is proportionately 966 * closer to the beginning of its day than the other.</p> 967 * 968 * @param other the time to which this one is compared. 969 * 970 * @return a negative integer, zero, or a positive integer as this time 971 * is less than, equal to, or greater than the other time. 972 */ 973 public int compareTo(TimeOfDay other) 974 { 975 int answer; 976 977 //If the days are the same length, do straight comparsion 978 if (this.secondsPerDay.compareTo(other.secondsPerDay) == 0) 979 { 980 answer = this.secondsSinceMidnight.compareTo(other.secondsSinceMidnight); 981 } 982 //Otherwise, compare fraction-of-day values 983 else 984 { 985 answer = this.toFractionOfDay().compareTo(other.toFractionOfDay()); 986 } 987 988 return answer; 989 } 990 991 //============================================================================ 992 // 993 //============================================================================ 994 995 /* 996 public static void main(String[] args) 997 { 998 for (String arg : args) 999 { 1000 System.out.print("timeText = " + arg); 1001 String output, roundedOutput; 1002 try 1003 { 1004 TimeOfDay tod = TimeOfDay.parse(arg); 1005 output = tod.toString(); 1006 roundedOutput = tod.toString(2, 2); 1007 } 1008 catch (Exception ex) 1009 { 1010 output = ex.toString(); 1011 roundedOutput = "not computed"; 1012 } 1013 System.out.print(", time of day = " + output); 1014 System.out.println(", rounded(2,2) = " + roundedOutput); 1015 } 1016 } 1017 */ 1018 /* 1019 //This method is here for quick, manual, testing. 1020 public static void main(String[] args) 1021 { 1022 System.out.println(); 1023 1024 TimeOfDay midDay = new TimeOfDay(TimeOfDay.STANDARD_DAY_LENGTH); 1025 midDay.set(12, 0, "0.0"); 1026 1027 TimeOfDay endDay = new TimeOfDay(TimeOfDay.STANDARD_DAY_LENGTH); 1028 endDay.setToEndOfDay(); 1029 1030 TimeOfDay startDay = new TimeOfDay(TimeOfDay.STANDARD_DAY_LENGTH); 1031 1032 for (String arg : args) 1033 { 1034 TimeOfDay tod = TimeOfDay.parse(arg, TimeOfDay.STANDARD_DAY_LENGTH); 1035 display("arg = " + arg, tod); 1036 1037 TimeOfDay todClone = tod.clone(); 1038 display("clone", todClone); 1039 1040 compare("Compare orig to clone", tod, todClone); 1041 compare("Compare orig to midDay", tod, midDay); 1042 compare("Compare orig to endDay", tod, endDay); 1043 compare("Compare midDay to endDay", midDay, endDay); 1044 compare("Compare startDay to endDay", startDay, endDay); 1045 } 1046 } 1047 private static void display(String title, TimeOfDay tod) 1048 { 1049 System.out.println(title); 1050 System.out.println(" Time of Day: " + tod); 1051 System.out.println(); 1052 1053 System.out.println(" Hour of Day: " + tod.getHourOfDay()); 1054 System.out.println(" Minute of Hour: " + tod.getMinuteOfHour()); 1055 System.out.println(" Second of Minute: " + tod.getSecondOfMinute()); 1056 System.out.println(); 1057 1058 System.out.println(" Hours: " + tod.getTimeSinceMidnight().toUnits(TimeUnits.HOUR)); 1059 System.out.println(" Minutes: " + tod.getTimeSinceMidnight().toUnits(TimeUnits.MINUTE)); 1060 System.out.println(" Seconds: " + tod.getTimeSinceMidnight().toUnits(TimeUnits.SECOND)); 1061 System.out.println(); 1062 1063 System.out.println(" Fraction of Day: " + tod.toFractionOfDay()); 1064 System.out.println(" Angle (degrees): " + tod.toAngle().toUnits(ArcUnits.DEGREE)); 1065 System.out.println(" Hash Code : " + tod.hashCode()); 1066 System.out.println(); 1067 } 1068 private static void compare(String title, TimeOfDay tod1, TimeOfDay tod2) 1069 { 1070 System.out.println(title); 1071 1072 System.out.println(" 1: " + tod1 + ", 2: " + tod2); 1073 System.out.println(); 1074 System.out.println(" 1.equals(2): " + tod1.equals(tod2)); 1075 System.out.println(" 2.equals(1): " + tod2.equals(tod1)); 1076 System.out.println(" 1.compareTo(2): " + tod1.compareTo(tod2)); 1077 System.out.println(" 2.compareTo(1): " + tod2.compareTo(tod1)); 1078 System.out.println(" 1.isBefore(2): " + tod1.isBefore(tod2)); 1079 System.out.println(" 2.isBefore(1): " + tod2.isBefore(tod1)); 1080 System.out.println(" 1.isAfter(2): " + tod1.isAfter(tod2)); 1081 System.out.println(" 2.isAfter(1): " + tod2.isAfter(tod1)); 1082 System.out.println(" 1.timeUntil(2): " + tod1.timeUntil(tod2)); 1083 System.out.println(" 2.timeUntil(1): " + tod2.timeUntil(tod1)); 1084 System.out.println(); 1085 } 1086 */ 1087 }