1 /* Copyright 2002-2024 CS GROUP 2 * Licensed to CS GROUP (CS) under one or more 3 * contributor license agreements. See the NOTICE file distributed with 4 * this work for additional information regarding copyright ownership. 5 * CS licenses this file to You under the Apache License, Version 2.0 6 * (the "License"); you may not use this file except in compliance with 7 * the License. You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 package org.orekit.time; 18 19 import java.io.Serializable; 20 import java.util.regex.Matcher; 21 import java.util.regex.Pattern; 22 23 import org.hipparchus.util.FastMath; 24 import org.orekit.errors.OrekitIllegalArgumentException; 25 import org.orekit.errors.OrekitMessages; 26 import org.orekit.utils.Constants; 27 28 29 /** Class representing a time within the day broken up as hour, 30 * minute and second components. 31 * <p>Instances of this class are guaranteed to be immutable.</p> 32 * @see DateComponents 33 * @see DateTimeComponents 34 * @author Luc Maisonobe 35 */ 36 public class TimeComponents implements Serializable, Comparable<TimeComponents> { 37 38 /** Constant for commonly used hour 00:00:00. */ 39 public static final TimeComponents H00 = new TimeComponents(0, 0, TimeOffset.ZERO); 40 41 /** Constant for commonly used hour 12:00:00. */ 42 public static final TimeComponents H12 = new TimeComponents(12, 0, TimeOffset.ZERO); 43 44 // CHECKSTYLE: stop ConstantName 45 /** Constant for NaN time. 46 * @since 13.0 47 */ 48 public static final TimeComponents NaN = new TimeComponents(0, 0, TimeOffset.NaN); 49 // CHECKSTYLE: resume ConstantName 50 51 /** Wrapping limits for rounding to next minute. 52 * @since 13.0 53 */ 54 private static final TimeOffset[] WRAPPING = new TimeOffset[] { 55 new TimeOffset(59L, 500000000000000000L), // round to second 56 new TimeOffset(59L, 950000000000000000L), // round to 10⁻¹ second 57 new TimeOffset(59L, 995000000000000000L), // round to 10⁻² second 58 new TimeOffset(59L, 999500000000000000L), // round to 10⁻³ second 59 new TimeOffset(59L, 999950000000000000L), // round to 10⁻⁴ second 60 new TimeOffset(59L, 999995000000000000L), // round to 10⁻⁵ second 61 new TimeOffset(59L, 999999500000000000L), // round to 10⁻⁶ second 62 new TimeOffset(59L, 999999950000000000L), // round to 10⁻⁷ second 63 new TimeOffset(59L, 999999995000000000L), // round to 10⁻⁸ second 64 new TimeOffset(59L, 999999999500000000L), // round to 10⁻⁹ second 65 new TimeOffset(59L, 999999999950000000L), // round to 10⁻¹⁰ second 66 new TimeOffset(59L, 999999999995000000L), // round to 10⁻¹¹ second 67 new TimeOffset(59L, 999999999999500000L), // round to 10⁻¹² second 68 new TimeOffset(59L, 999999999999950000L), // round to 10⁻¹³ second 69 new TimeOffset(59L, 999999999999995000L), // round to 10⁻¹⁴ second 70 new TimeOffset(59L, 999999999999999500L), // round to 10⁻¹⁵ second 71 new TimeOffset(59L, 999999999999999950L), // round to 10⁻¹⁶ second 72 new TimeOffset(59L, 999999999999999995L) // round to 10⁻¹⁷ second 73 }; 74 75 /** Offset values for rounding attoseconds. 76 * @since 13.0 77 */ 78 // CHECKSTYLE: stop Indentation check */ 79 private static final long[] ROUNDING = new long[] { 80 500000000000000000L, // round to second 81 50000000000000000L, // round to 10⁻¹ second 82 5000000000000000L, // round to 10⁻² second 83 500000000000000L, // round to 10⁻³ second 84 50000000000000L, // round to 10⁻⁴ second 85 5000000000000L, // round to 10⁻⁵ second 86 500000000000L, // round to 10⁻⁶ second 87 50000000000L, // round to 10⁻⁷ second 88 5000000000L, // round to 10⁻⁸ second 89 500000000L, // round to 10⁻⁹ second 90 50000000L, // round to 10⁻¹⁰ second 91 5000000L, // round to 10⁻¹¹ second 92 500000L, // round to 10⁻¹² second 93 50000L, // round to 10⁻¹³ second 94 5000L, // round to 10⁻¹⁴ second 95 500L, // round to 10⁻¹⁵ second 96 50L, // round to 10⁻¹⁶ second 97 5L, // round to 10⁻¹⁷ second 98 0L, // round to 10⁻¹⁸ second 99 }; 100 // CHECKSTYLE: resume Indentation check */ 101 102 /** Serializable UID. */ 103 private static final long serialVersionUID = 20240712L; 104 105 /** Basic and extends formats for local time, with optional timezone. */ 106 private static final Pattern ISO8601_FORMATS = Pattern.compile("^(\\d\\d):?(\\d\\d):?(\\d\\d(?:[.,]\\d+)?)?(?:Z|([-+]\\d\\d(?::?\\d\\d)?))?$"); 107 108 /** Number of seconds in one hour. */ 109 private static final int HOUR = 3600; 110 111 /** Number of seconds in one minute. */ 112 private static final int MINUTE = 60; 113 114 /** Constant for 23 hours. */ 115 private static final int TWENTY_THREE = 23; 116 117 /** Constant for 59 minutes. */ 118 private static final int FIFTY_NINE = 59; 119 120 /** Constant for 23:59. */ 121 private static final TimeOffset TWENTY_THREE_FIFTY_NINE = 122 new TimeOffset(TWENTY_THREE * HOUR + FIFTY_NINE * MINUTE, 0L); 123 124 /** Hour number. */ 125 private final int hour; 126 127 /** Minute number. */ 128 private final int minute; 129 130 /** Second number. */ 131 private final TimeOffset second; 132 133 /** Offset between the specified date and UTC. 134 * <p> 135 * Always an integral number of minutes, as per ISO-8601 standard. 136 * </p> 137 * @since 7.2 138 */ 139 private final int minutesFromUTC; 140 141 /** Build a time from its clock elements. 142 * <p>Note that seconds between 60.0 (inclusive) and 61.0 (exclusive) are allowed 143 * in this method, since they do occur during leap seconds introduction 144 * in the {@link UTCScale UTC} time scale.</p> 145 * @param hour hour number from 0 to 23 146 * @param minute minute number from 0 to 59 147 * @param second second number from 0.0 to 61.0 (excluded) 148 * @exception IllegalArgumentException if inconsistent arguments 149 * are given (parameters out of range) 150 */ 151 public TimeComponents(final int hour, final int minute, final double second) 152 throws IllegalArgumentException { 153 this(hour, minute, new TimeOffset(second)); 154 } 155 156 /** Build a time from its clock elements. 157 * <p>Note that seconds between 60.0 (inclusive) and 61.0 (exclusive) are allowed 158 * in this method, since they do occur during leap seconds introduction 159 * in the {@link UTCScale UTC} time scale.</p> 160 * @param hour hour number from 0 to 23 161 * @param minute minute number from 0 to 59 162 * @param second second number from 0.0 to 61.0 (excluded) 163 * @exception IllegalArgumentException if inconsistent arguments 164 * are given (parameters out of range) 165 * @since 13.0 166 */ 167 public TimeComponents(final int hour, final int minute, final TimeOffset second) 168 throws IllegalArgumentException { 169 this(hour, minute, second, 0); 170 } 171 172 /** Build a time from its clock elements. 173 * <p>Note that seconds between 60.0 (inclusive) and 61.0 (exclusive) are allowed 174 * in this method, since they do occur during leap seconds introduction 175 * in the {@link UTCScale UTC} time scale.</p> 176 * @param hour hour number from 0 to 23 177 * @param minute minute number from 0 to 59 178 * @param second second number from 0.0 to 61.0 (excluded) 179 * @param minutesFromUTC offset between the specified date and UTC, as an 180 * integral number of minutes, as per ISO-8601 standard 181 * @exception IllegalArgumentException if inconsistent arguments 182 * are given (parameters out of range) 183 * @since 7.2 184 */ 185 public TimeComponents(final int hour, final int minute, final double second, final int minutesFromUTC) 186 throws IllegalArgumentException { 187 this(hour, minute, new TimeOffset(second), minutesFromUTC); 188 } 189 190 /** Build a time from its clock elements. 191 * <p>Note that seconds between 60.0 (inclusive) and 61.0 (exclusive) are allowed 192 * in this method, since they do occur during leap seconds introduction 193 * in the {@link UTCScale UTC} time scale.</p> 194 * @param hour hour number from 0 to 23 195 * @param minute minute number from 0 to 59 196 * @param second second number from 0.0 to 62.0 (excluded, more than 61 s occurred on 197 * the 1961 leap second, which was between 1 and 2 seconds in duration) 198 * @param minutesFromUTC offset between the specified date and UTC, as an 199 * integral number of minutes, as per ISO-8601 standard 200 * @exception IllegalArgumentException if inconsistent arguments 201 * are given (parameters out of range) 202 * @since 13.0 203 */ 204 public TimeComponents(final int hour, final int minute, final TimeOffset second, 205 final int minutesFromUTC) 206 throws IllegalArgumentException { 207 208 // range check 209 if (hour < 0 || hour > 23 || 210 minute < 0 || minute > 59 || 211 second.getSeconds() < 0L || second.getSeconds() >= 62L) { 212 throw new OrekitIllegalArgumentException(OrekitMessages.NON_EXISTENT_HMS_TIME, 213 hour, minute, second.toDouble()); 214 } 215 216 this.hour = hour; 217 this.minute = minute; 218 this.second = second; 219 this.minutesFromUTC = minutesFromUTC; 220 221 } 222 223 /** 224 * Build a time from the second number within the day. 225 * 226 * <p>If the {@code secondInDay} is less than {@code 60.0} then {@link #getSecond()} 227 * and {@link #getSplitSecond()} will be less than {@code 60.0}, otherwise they will be 228 * less than {@code 61.0}. This constructor may produce an invalid value of 229 * {@link #getSecond()} and {@link #getSplitSecond()} during a negative leap second, 230 * through there has never been one. For more control over the number of seconds in 231 * the final minute use {@link #TimeComponents(TimeOffset, TimeOffset, int)}. 232 * 233 * <p>This constructor is always in UTC (i.e. {@link #getMinutesFromUTC() will return 234 * 0}). 235 * 236 * @param secondInDay second number from 0.0 to {@link Constants#JULIAN_DAY} {@code + 237 * 1} (excluded) 238 * @throws OrekitIllegalArgumentException if seconds number is out of range 239 * @see #TimeComponents(TimeOffset, TimeOffset, int) 240 * @see #TimeComponents(int, double) 241 */ 242 public TimeComponents(final double secondInDay) 243 throws OrekitIllegalArgumentException { 244 this(new TimeOffset(secondInDay)); 245 } 246 247 /** 248 * Build a time from the second number within the day. 249 * 250 * <p>The second number is defined here as the sum 251 * {@code secondInDayA + secondInDayB} from 0.0 to {@link Constants#JULIAN_DAY} 252 * {@code + 1} (excluded). The two parameters are used for increased accuracy. 253 * 254 * <p>If the sum is less than {@code 60.0} then {@link #getSecond()} will be less 255 * than {@code 60.0}, otherwise it will be less than {@code 61.0}. This constructor 256 * may produce an invalid value of {@link #getSecond()} during a negative leap second, 257 * through there has never been one. For more control over the number of seconds in 258 * the final minute use {@link #TimeComponents(TimeOffset, TimeOffset, int)}. 259 * 260 * <p>This constructor is always in UTC (i.e. {@link #getMinutesFromUTC()} will 261 * return 0). 262 * 263 * @param secondInDayA first part of the second number 264 * @param secondInDayB last part of the second number 265 * @throws OrekitIllegalArgumentException if seconds number is out of range 266 * @see #TimeComponents(TimeOffset, TimeOffset, int) 267 */ 268 public TimeComponents(final int secondInDayA, final double secondInDayB) 269 throws OrekitIllegalArgumentException { 270 271 // if the total is at least 86400 then assume there is a leap second 272 final TimeOffset aPlusB = new TimeOffset(secondInDayA).add(new TimeOffset(secondInDayB)); 273 final TimeComponents tc = aPlusB.compareTo(TimeOffset.DAY) >= 0 ? 274 new TimeComponents(aPlusB.subtract(TimeOffset.SECOND), TimeOffset.SECOND, 61) : 275 new TimeComponents(aPlusB, TimeOffset.ZERO, 60); 276 277 this.hour = tc.hour; 278 this.minute = tc.minute; 279 this.second = tc.second; 280 this.minutesFromUTC = tc.minutesFromUTC; 281 282 } 283 284 /** 285 * Build a time from the second number within the day. 286 * 287 * <p>If the {@code secondInDay} is less than {@code 60.0} then {@link #getSecond()} 288 * will be less than {@code 60.0}, otherwise it will be less than {@code 61.0}. This constructor 289 * may produce an invalid value of {@link #getSecond()} during a negative leap second, 290 * through there has never been one. For more control over the number of seconds in 291 * the final minute use {@link #TimeComponents(TimeOffset, TimeOffset, int)}. 292 * 293 * <p>This constructor is always in UTC (i.e. {@link #getMinutesFromUTC() will return 294 * 0}). 295 * 296 * @param splitSecondInDay second number from 0.0 to {@link Constants#JULIAN_DAY} {@code + 297 * 1} (excluded) 298 * @see #TimeComponents(TimeOffset, TimeOffset, int) 299 * @see #TimeComponents(int, double) 300 * @since 13.0 301 */ 302 public TimeComponents(final TimeOffset splitSecondInDay) { 303 if (splitSecondInDay.compareTo(TimeOffset.ZERO) < 0) { 304 // negative time 305 throw new OrekitIllegalArgumentException(OrekitMessages.OUT_OF_RANGE_SECONDS_NUMBER_DETAIL, 306 splitSecondInDay.toDouble(), 307 0, TimeOffset.DAY_WITH_POSITIVE_LEAP.getSeconds()); 308 } else if (splitSecondInDay.compareTo(TimeOffset.DAY) >= 0) { 309 // if the total is at least 86400 then assume there is a leap second 310 if (splitSecondInDay.compareTo(TimeOffset.DAY_WITH_POSITIVE_LEAP) >= 0) { 311 // more than one leap second is too much 312 throw new OrekitIllegalArgumentException(OrekitMessages.OUT_OF_RANGE_SECONDS_NUMBER_DETAIL, 313 splitSecondInDay.toDouble(), 314 0, TimeOffset.DAY_WITH_POSITIVE_LEAP.getSeconds()); 315 } else { 316 hour = TWENTY_THREE; 317 minute = FIFTY_NINE; 318 second = splitSecondInDay.subtract(TWENTY_THREE_FIFTY_NINE); 319 } 320 } else { 321 // regular time within day 322 hour = (int) splitSecondInDay.getSeconds() / HOUR; 323 minute = ((int) splitSecondInDay.getSeconds() % HOUR) / MINUTE; 324 second = splitSecondInDay.subtract(new TimeOffset(hour * HOUR + minute * MINUTE, 0L)); 325 } 326 327 minutesFromUTC = 0; 328 329 } 330 331 /** 332 * Build a time from the second number within the day. 333 * 334 * <p>The seconds past midnight is the sum {@code secondInDay + leap}. Only the part 335 * {@code secondInDay} is used to compute the hours and minutes. The second parameter 336 * ({@code leap}) is added directly to the second value ({@link #getSecond()}) to 337 * implement leap seconds. These two quantities must satisfy the following constraints. 338 * This first guarantees the hour and minute are valid, the second guarantees the second 339 * is valid. 340 * 341 * <pre> 342 * {@code 0 <= secondInDay < 86400} 343 * {@code 0 <= secondInDay % 60 + leap <= minuteDuration} 344 * {@code 0 <= leap <= minuteDuration - 60 if minuteDuration >= 60} 345 * {@code 0 >= leap >= minuteDuration - 60 if minuteDuration < 60} 346 * </pre> 347 * 348 * <p>If the seconds of minute ({@link #getSecond()}) computed from {@code 349 * secondInDay + leap} is greater than or equal to {@code 60 + leap} 350 * then the second of minute will be set to {@code FastMath.nextDown(60 + leap)}. This 351 * prevents rounding to an invalid seconds of minute number when the input values have 352 * greater precision than a {@code double}. 353 * 354 * <p>This constructor is always in UTC (i.e. {@link #getMinutesFromUTC() will return 355 * 0}). 356 * 357 * <p>If {@code secondsInDay} or {@code leap} is NaN then the hour and minute will 358 * be set arbitrarily and the second of minute will be NaN. 359 * 360 * @param secondInDay part of the second number. 361 * @param leap magnitude of the leap second if this point in time is during 362 * a leap second, otherwise {@code 0.0}. This value is not used 363 * to compute hours and minutes, but it is added to the computed 364 * second of minute. 365 * @param minuteDuration number of seconds in the current minute, normally {@code 60}. 366 * @throws OrekitIllegalArgumentException if the inequalities above do not hold. 367 * @since 10.2 368 */ 369 public TimeComponents(final TimeOffset secondInDay, final TimeOffset leap, final int minuteDuration) { 370 371 minutesFromUTC = 0; 372 373 if (secondInDay.isNaN()) { 374 // special handling for NaN 375 hour = 0; 376 minute = 0; 377 second = secondInDay; 378 return; 379 } 380 381 // range check 382 if (secondInDay.compareTo(TimeOffset.ZERO) < 0 || secondInDay.compareTo(TimeOffset.DAY) >= 0) { 383 throw new OrekitIllegalArgumentException(OrekitMessages.OUT_OF_RANGE_SECONDS_NUMBER_DETAIL, 384 // this can produce some strange messages due to rounding 385 secondInDay.toDouble(), 0, Constants.JULIAN_DAY); 386 } 387 final int maxExtraSeconds = minuteDuration - MINUTE; 388 if (leap.getSeconds() * maxExtraSeconds < 0 || FastMath.abs(leap.getSeconds()) > FastMath.abs(maxExtraSeconds)) { 389 throw new OrekitIllegalArgumentException(OrekitMessages.OUT_OF_RANGE_SECONDS_NUMBER_DETAIL, 390 leap, 0, maxExtraSeconds); 391 } 392 393 // extract the time components 394 int wholeSeconds = (int) secondInDay.getSeconds(); 395 hour = wholeSeconds / HOUR; 396 wholeSeconds -= HOUR * hour; 397 minute = wholeSeconds / MINUTE; 398 wholeSeconds -= MINUTE * minute; 399 // at this point ((minuteDuration - wholeSeconds) - leap) - fractional > 0 400 // or else one of the preconditions was violated. Even if there is no violation, 401 // naiveSecond may round to minuteDuration, creating an invalid time. 402 // In that case round down to preserve a valid time at the cost of up to 1as of error. 403 // See #676 and #681. 404 final TimeOffset naiveSecond = new TimeOffset(wholeSeconds, secondInDay.getAttoSeconds()).add(leap); 405 if (naiveSecond.compareTo(TimeOffset.ZERO) < 0) { 406 throw new OrekitIllegalArgumentException( 407 OrekitMessages.OUT_OF_RANGE_SECONDS_NUMBER_DETAIL, 408 naiveSecond, 0, minuteDuration); 409 } 410 if (naiveSecond.getSeconds() < minuteDuration) { 411 second = naiveSecond; 412 } else { 413 second = new TimeOffset(minuteDuration - 1, 999999999999999999L); 414 } 415 416 } 417 418 /** Parse a string in ISO-8601 format to build a time. 419 * <p>The supported formats are: 420 * <ul> 421 * <li>basic and extended format local time: hhmmss, hh:mm:ss (with optional decimals in seconds)</li> 422 * <li>optional UTC time: hhmmssZ, hh:mm:ssZ</li> 423 * <li>optional signed hours UTC offset: hhmmss+HH, hhmmss-HH, hh:mm:ss+HH, hh:mm:ss-HH</li> 424 * <li>optional signed basic hours and minutes UTC offset: hhmmss+HHMM, hhmmss-HHMM, hh:mm:ss+HHMM, hh:mm:ss-HHMM</li> 425 * <li>optional signed extended hours and minutes UTC offset: hhmmss+HH:MM, hhmmss-HH:MM, hh:mm:ss+HH:MM, hh:mm:ss-HH:MM</li> 426 * </ul> 427 * 428 * <p> As shown by the list above, only the complete representations defined in section 4.2 429 * of ISO-8601 standard are supported, neither expended representations nor representations 430 * with reduced accuracy are supported. 431 * 432 * @param string string to parse 433 * @return a parsed time 434 * @exception IllegalArgumentException if string cannot be parsed 435 */ 436 public static TimeComponents parseTime(final String string) { 437 438 // is the date a calendar date ? 439 final Matcher timeMatcher = ISO8601_FORMATS.matcher(string); 440 if (timeMatcher.matches()) { 441 final int hour = Integer.parseInt(timeMatcher.group(1)); 442 final int minute = Integer.parseInt(timeMatcher.group(2)); 443 final TimeOffset second = timeMatcher.group(3) == null ? 444 TimeOffset.ZERO : 445 TimeOffset.parse(timeMatcher.group(3).replace(',', '.')); 446 final String offset = timeMatcher.group(4); 447 final int minutesFromUTC; 448 if (offset == null) { 449 // no offset from UTC is given 450 minutesFromUTC = 0; 451 } else { 452 // we need to parse an offset from UTC 453 // the sign is mandatory and the ':' separator is optional 454 // so we can have offsets given as -06:00 or +0100 455 final int sign = offset.codePointAt(0) == '-' ? -1 : +1; 456 final int hourOffset = Integer.parseInt(offset.substring(1, 3)); 457 final int minutesOffset = offset.length() <= 3 ? 0 : Integer.parseInt(offset.substring(offset.length() - 2)); 458 minutesFromUTC = sign * (minutesOffset + MINUTE * hourOffset); 459 } 460 return new TimeComponents(hour, minute, second, minutesFromUTC); 461 } 462 463 throw new OrekitIllegalArgumentException(OrekitMessages.NON_EXISTENT_TIME, string); 464 465 } 466 467 /** Get the hour number. 468 * @return hour number from 0 to 23 469 */ 470 public int getHour() { 471 return hour; 472 } 473 474 /** Get the minute number. 475 * @return minute minute number from 0 to 59 476 */ 477 public int getMinute() { 478 return minute; 479 } 480 481 /** Get the seconds number. 482 * @return second second number from 0.0 to 61.0 (excluded). Note that 60 ≤ second 483 * < 61 only occurs during a leap second. 484 */ 485 public double getSecond() { 486 return second.toDouble(); 487 } 488 489 /** Get the seconds number. 490 * @return second second number from 0.0 to 61.0 (excluded). Note that 60 ≤ second 491 * < 61 only occurs during a leap second. 492 */ 493 public TimeOffset getSplitSecond() { 494 return second; 495 } 496 497 /** Get the offset between the specified date and UTC. 498 * <p> 499 * The offset is always an integral number of minutes, as per ISO-8601 standard. 500 * </p> 501 * @return offset in minutes between the specified date and UTC 502 * @since 7.2 503 */ 504 public int getMinutesFromUTC() { 505 return minutesFromUTC; 506 } 507 508 /** Get the second number within the local day, <em>without</em> applying the {@link #getMinutesFromUTC() offset from UTC}. 509 * @return second number from 0.0 to Constants.JULIAN_DAY 510 * @see #getSplitSecondsInLocalDay() 511 * @see #getSecondsInUTCDay() 512 * @since 7.2 513 */ 514 public double getSecondsInLocalDay() { 515 return getSplitSecondsInLocalDay().toDouble(); 516 } 517 518 /** Get the second number within the local day, <em>without</em> applying the {@link #getMinutesFromUTC() offset from UTC}. 519 * @return second number from 0.0 to Constants.JULIAN_DAY 520 * @see #getSecondsInLocalDay() 521 * @see #getSplitSecondsInUTCDay() 522 * @since 13.0 523 */ 524 public TimeOffset getSplitSecondsInLocalDay() { 525 return new TimeOffset((long) MINUTE * minute + (long) HOUR * hour, 0L).add(second); 526 } 527 528 /** Get the second number within the UTC day, applying the {@link #getMinutesFromUTC() offset from UTC}. 529 * @return second number from {@link #getMinutesFromUTC() -getMinutesFromUTC()} 530 * to Constants.JULIAN_DAY {@link #getMinutesFromUTC() + getMinutesFromUTC()} 531 * @see #getSplitSecondsInUTCDay() 532 * @see #getSecondsInLocalDay() 533 * @since 7.2 534 */ 535 public double getSecondsInUTCDay() { 536 return getSplitSecondsInUTCDay().toDouble(); 537 } 538 539 /** Get the second number within the UTC day, applying the {@link #getMinutesFromUTC() offset from UTC}. 540 * @return second number from {@link #getMinutesFromUTC() -getMinutesFromUTC()} 541 * to Constants.JULIAN_DAY {@link #getMinutesFromUTC() + getMinutesFromUTC()} 542 * @see #getSecondsInUTCDay() 543 * @see #getSplitSecondsInLocalDay() 544 * @since 13.0 545 */ 546 public TimeOffset getSplitSecondsInUTCDay() { 547 return new TimeOffset((long) MINUTE * (minute - minutesFromUTC) + (long) HOUR * hour, 0L).add(second); 548 } 549 550 /** 551 * Round this time to the given precision if needed to prevent rounding up to an 552 * invalid seconds number. This is useful, for example, when writing custom date-time 553 * formatting methods so one does not, e.g., end up with "60.0" seconds during a 554 * normal minute when the value of seconds is {@code 59.999}. This method will instead 555 * round up the minute, hour, day, month, and year as needed. 556 * 557 * @param minuteDuration 59, 60, 61, or 62 seconds depending on the date being close 558 * to a leap second introduction and the magnitude of the leap 559 * second. 560 * @param fractionDigits the number of decimal digits after the decimal point in the 561 * seconds number that will be printed. This date-time is 562 * rounded to {@code fractionDigits} after the decimal point if 563 * necessary to prevent rounding up to {@code minuteDuration}. 564 * {@code fractionDigits} must be greater than or equal to 565 * {@code 0}. 566 * @return the instance itself if no rounding was needed, or a time within 567 * {@code 0.5 * 10**-fractionDigits} seconds of this, and with a seconds number that 568 * will not round up to {@code minuteDuration} when rounded to {@code fractionDigits} 569 * after the decimal point 570 * @since 13.0 571 */ 572 public TimeComponents wrapIfNeeded(final int minuteDuration, final int fractionDigits) { 573 TimeOffset wrappedSecond = second; 574 575 // adjust limit according to current minute duration 576 final TimeOffset limit = WRAPPING[FastMath.min(fractionDigits, WRAPPING.length - 1)]. 577 add(new TimeOffset(minuteDuration - 60, 0L)); 578 579 if (wrappedSecond.compareTo(limit) >= 0) { 580 // we should wrap around to the next minute 581 int wrappedMinute = minute; 582 int wrappedHour = hour; 583 wrappedSecond = TimeOffset.ZERO; 584 ++wrappedMinute; 585 if (wrappedMinute > 59) { 586 wrappedMinute = 0; 587 ++wrappedHour; 588 if (wrappedHour > 23) { 589 wrappedHour = 0; 590 } 591 } 592 return new TimeComponents(wrappedHour, wrappedMinute, wrappedSecond); 593 } 594 return this; 595 } 596 597 /** 598 * Package private method that allows specification of seconds format. Allows access from 599 * {@link DateTimeComponents#toString(int, int)}. Access from outside of rounding methods would result in invalid 600 * times, see #590, #591. 601 * 602 * @param fractionDigits the number of digits to include after the decimal point in the string representation of the 603 * seconds. The date and time is first rounded as necessary. {@code fractionDigits} must be 604 * greater than or equal to {@code 0}. 605 * @return string without UTC offset. 606 * @since 13.0 607 */ 608 String toStringWithoutUtcOffset(final int fractionDigits) { 609 610 if (second.isFinite()) { 611 // general case for regular times 612 final long rounding = ROUNDING[FastMath.min(fractionDigits, ROUNDING.length - 1)]; 613 final TimeComponents rounded = new TimeComponents(hour, minute, 614 new TimeOffset(second.getSeconds(), 615 second.getAttoSeconds() + rounding)); 616 final StringBuilder builder = new StringBuilder(); 617 builder.append(String.format("%02d:%02d:%02d", 618 rounded.hour, rounded.minute, rounded.second.getSeconds())); 619 if (fractionDigits > 0) { 620 builder.append('.'); 621 builder.append(String.format("%018d", rounded.second.getAttoSeconds()), 0, fractionDigits); 622 } 623 return builder.toString(); 624 } else if (second.isNaN()) { 625 // special handling for NaN 626 return String.format("%02d:%02d:NaN", hour, minute); 627 } else if (second.isNegativeInfinity()) { 628 // special handling for -∞ 629 return String.format("%02d:%02d:-∞", hour, minute); 630 } else { 631 // special handling for +∞ 632 return String.format("%02d:%02d:+∞", hour, minute); 633 } 634 635 } 636 637 /** 638 * Get a string representation of the time without the offset from UTC. 639 * 640 * @return a string representation of the time in an ISO 8601 like format. 641 * @see #formatUtcOffset() 642 * @see #toString() 643 */ 644 public String toStringWithoutUtcOffset() { 645 // create formats here as they are not thread safe 646 // Format for seconds to prevent rounding up to an invalid time. See #591 647 final String formatted = toStringWithoutUtcOffset(18); 648 int last = formatted.length() - 1; 649 while (last > 11 && formatted.charAt(last) == '0') { 650 // we want to remove final zeros (but keeping milliseconds for compatibility) 651 --last; 652 } 653 return formatted.substring(0, last + 1); 654 } 655 656 /** 657 * Get the UTC offset as a string in ISO8601 format. For example, {@code +00:00}. 658 * 659 * @return the UTC offset as a string. 660 * @see #toStringWithoutUtcOffset() 661 * @see #toString() 662 */ 663 public String formatUtcOffset() { 664 final int hourOffset = FastMath.abs(minutesFromUTC) / MINUTE; 665 final int minuteOffset = FastMath.abs(minutesFromUTC) % MINUTE; 666 return (minutesFromUTC < 0 ? '-' : '+') + 667 String.format("%02d:%02d", hourOffset, minuteOffset); 668 } 669 670 /** 671 * Get a string representation of the time including the offset from UTC. 672 * 673 * @return string representation of the time in an ISO 8601 like format including the 674 * UTC offset. 675 * @see #toStringWithoutUtcOffset() 676 * @see #formatUtcOffset() 677 */ 678 public String toString() { 679 return toStringWithoutUtcOffset() + formatUtcOffset(); 680 } 681 682 /** {@inheritDoc} */ 683 public int compareTo(final TimeComponents other) { 684 return getSplitSecondsInUTCDay().compareTo(other.getSplitSecondsInUTCDay()); 685 } 686 687 /** {@inheritDoc} */ 688 public boolean equals(final Object other) { 689 try { 690 final TimeComponents otherTime = (TimeComponents) other; 691 return otherTime != null && 692 hour == otherTime.hour && 693 minute == otherTime.minute && 694 second.compareTo(otherTime.second) == 0 && 695 minutesFromUTC == otherTime.minutesFromUTC; 696 } catch (ClassCastException cce) { 697 return false; 698 } 699 } 700 701 /** {@inheritDoc} */ 702 public int hashCode() { 703 return ((hour << 16) ^ ((minute - minutesFromUTC) << 8)) ^ second.hashCode(); 704 } 705 706 }