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