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.text.DecimalFormat; 21 import java.text.DecimalFormatSymbols; 22 import java.util.Locale; 23 import java.util.regex.Matcher; 24 import java.util.regex.Pattern; 25 26 import org.hipparchus.util.FastMath; 27 import org.orekit.errors.OrekitIllegalArgumentException; 28 import org.orekit.errors.OrekitMessages; 29 import org.orekit.utils.Constants; 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, 0); 43 44 /** Constant for commonly used hour 12:00:00. */ 45 public static final TimeComponents H12 = new TimeComponents(12, 0, 0); 46 47 /** Serializable UID. */ 48 private static final long serialVersionUID = 20160331L; 49 50 /** Formatting symbols used in {@link #toString()}. */ 51 private static final DecimalFormatSymbols US_SYMBOLS = 52 new DecimalFormatSymbols(Locale.US); 53 54 /** Basic and extends formats for local time, with optional timezone. */ 55 private static final Pattern ISO8601_FORMATS = Pattern.compile("^(\\d\\d):?(\\d\\d):?(\\d\\d(?:[.,]\\d+)?)?(?:Z|([-+]\\d\\d(?::?\\d\\d)?))?$"); 56 57 /** Hour number. */ 58 private final int hour; 59 60 /** Minute number. */ 61 private final int minute; 62 63 /** Second number. */ 64 private final double second; 65 66 /** Offset between the specified date and UTC. 67 * <p> 68 * Always an integral number of minutes, as per ISO-8601 standard. 69 * </p> 70 * @since 7.2 71 */ 72 private final int minutesFromUTC; 73 74 /** Build a time from its clock elements. 75 * <p>Note that seconds between 60.0 (inclusive) and 61.0 (exclusive) are allowed 76 * in this method, since they do occur during leap seconds introduction 77 * in the {@link UTCScale UTC} time scale.</p> 78 * @param hour hour number from 0 to 23 79 * @param minute minute number from 0 to 59 80 * @param second second number from 0.0 to 61.0 (excluded) 81 * @exception IllegalArgumentException if inconsistent arguments 82 * are given (parameters out of range) 83 */ 84 public TimeComponents(final int hour, final int minute, final double second) 85 throws IllegalArgumentException { 86 this(hour, minute, second, 0); 87 } 88 89 /** Build a time from its clock elements. 90 * <p>Note that seconds between 60.0 (inclusive) and 61.0 (exclusive) are allowed 91 * in this method, since they do occur during leap seconds introduction 92 * in the {@link UTCScale UTC} time scale.</p> 93 * @param hour hour number from 0 to 23 94 * @param minute minute number from 0 to 59 95 * @param second second number from 0.0 to 61.0 (excluded) 96 * @param minutesFromUTC offset between the specified date and UTC, as an 97 * integral number of minutes, as per ISO-8601 standard 98 * @exception IllegalArgumentException if inconsistent arguments 99 * are given (parameters out of range) 100 * @since 7.2 101 */ 102 public TimeComponents(final int hour, final int minute, final double second, 103 final int minutesFromUTC) 104 throws IllegalArgumentException { 105 106 // range check 107 if (hour < 0 || hour > 23 || 108 minute < 0 || minute > 59 || 109 second < 0 || second >= 61.0) { 110 throw new OrekitIllegalArgumentException(OrekitMessages.NON_EXISTENT_HMS_TIME, 111 hour, minute, second); 112 } 113 114 this.hour = hour; 115 this.minute = minute; 116 this.second = second; 117 this.minutesFromUTC = minutesFromUTC; 118 119 } 120 121 /** 122 * Build a time from the second number within the day. 123 * 124 * <p>If the {@code secondInDay} is less than {@code 60.0} then {@link #getSecond()} 125 * will be less than {@code 60.0}, otherwise it will be less than {@code 61.0}. This constructor 126 * may produce an invalid value of {@link #getSecond()} during a negative leap second, 127 * through there has never been one. For more control over the number of seconds in 128 * the final minute use {@link #fromSeconds(int, double, double, int)}. 129 * 130 * <p>This constructor is always in UTC (i.e. {@link #getMinutesFromUTC() will return 131 * 0}). 132 * 133 * @param secondInDay second number from 0.0 to {@link Constants#JULIAN_DAY} {@code + 134 * 1} (excluded) 135 * @throws OrekitIllegalArgumentException if seconds number is out of range 136 * @see #fromSeconds(int, double, double, int) 137 * @see #TimeComponents(int, double) 138 */ 139 public TimeComponents(final double secondInDay) 140 throws OrekitIllegalArgumentException { 141 this(0, secondInDay); 142 } 143 144 /** 145 * Build a time from the second number within the day. 146 * 147 * <p>The second number is defined here as the sum 148 * {@code secondInDayA + secondInDayB} from 0.0 to {@link Constants#JULIAN_DAY} 149 * {@code + 1} (excluded). The two parameters are used for increased accuracy. 150 * 151 * <p>If the sum is less than {@code 60.0} then {@link #getSecond()} will be less 152 * than {@code 60.0}, otherwise it will be less than {@code 61.0}. This constructor 153 * may produce an invalid value of {@link #getSecond()} during a negative leap second, 154 * through there has never been one. For more control over the number of seconds in 155 * the final minute use {@link #fromSeconds(int, double, double, int)}. 156 * 157 * <p>This constructor is always in UTC (i.e. {@link #getMinutesFromUTC()} will 158 * return 0). 159 * 160 * @param secondInDayA first part of the second number 161 * @param secondInDayB last part of the second number 162 * @throws OrekitIllegalArgumentException if seconds number is out of range 163 * @see #fromSeconds(int, double, double, int) 164 */ 165 public TimeComponents(final int secondInDayA, final double secondInDayB) 166 throws OrekitIllegalArgumentException { 167 // if the total is at least 86400 then assume there is a leap second 168 this( 169 (Constants.JULIAN_DAY - secondInDayA) - secondInDayB > 0 ? secondInDayA : secondInDayA - 1, 170 secondInDayB, 171 (Constants.JULIAN_DAY - secondInDayA) - secondInDayB > 0 ? 0 : 1, 172 (Constants.JULIAN_DAY - secondInDayA) - secondInDayB > 0 ? 60 : 61); 173 } 174 175 /** 176 * Build a time from the second number within the day. 177 * 178 * <p>The seconds past midnight is the sum {@code secondInDayA + secondInDayB + 179 * leap}. The two parameters are used for increased accuracy. Only the first part of 180 * the sum ({@code secondInDayA + secondInDayB}) is used to compute the hours and 181 * minutes. The third parameter ({@code leap}) is added directly to the second value 182 * ({@link #getSecond()}) to implement leap seconds. These three quantities must 183 * satisfy the following constraints. This first guarantees the hour and minute are 184 * valid, the second guarantees the second is valid. 185 * 186 * <pre> 187 * {@code 0 <= secondInDayA + secondInDayB < 86400} 188 * {@code 0 <= (secondInDayA + secondInDayB) % 60 + leap < minuteDuration} 189 * {@code 0 <= leap <= minuteDuration - 60 if minuteDuration >= 60} 190 * {@code 0 >= leap >= minuteDuration - 60 if minuteDuration < 60} 191 * </pre> 192 * 193 * <p>If the seconds of minute ({@link #getSecond()}) computed from {@code 194 * secondInDayA + secondInDayB + leap} is greater than or equal to {@code 195 * minuteDuration} then the second of minute will be set to {@code 196 * FastMath.nextDown(minuteDuration)}. This prevents rounding to an invalid seconds of 197 * minute number when the input values have greater precision than a {@code double}. 198 * 199 * <p>This constructor is always in UTC (i.e. {@link #getMinutesFromUTC() will return 200 * 0}). 201 * 202 * <p>If {@code secondsInDayB} or {@code leap} is NaN then the hour and minute will 203 * be determined from {@code secondInDayA} and the second of minute will be NaN. 204 * 205 * <p>This constructor is private to avoid confusion with the other constructors that 206 * would be caused by overloading. Use {@link #fromSeconds(int, double, double, 207 * int)}. 208 * 209 * @param secondInDayA first part of the second number. 210 * @param secondInDayB last part of the second number. 211 * @param leap magnitude of the leap second if this point in time is during 212 * a leap second, otherwise {@code 0.0}. This value is not used 213 * to compute hours and minutes, but it is added to the computed 214 * second of minute. 215 * @param minuteDuration number of seconds in the current minute, normally {@code 60}. 216 * @throws OrekitIllegalArgumentException if the inequalities above do not hold. 217 * @see #fromSeconds(int, double, double, int) 218 * @since 10.2 219 */ 220 private TimeComponents(final int secondInDayA, 221 final double secondInDayB, 222 final double leap, 223 final int minuteDuration) throws OrekitIllegalArgumentException { 224 225 // split the numbers as a whole number of seconds 226 // and a fractional part between 0.0 (included) and 1.0 (excluded) 227 final int carry = (int) FastMath.floor(secondInDayB); 228 int wholeSeconds = secondInDayA + carry; 229 final double fractional = secondInDayB - carry; 230 231 // range check 232 if (wholeSeconds < 0 || wholeSeconds >= Constants.JULIAN_DAY) { 233 throw new OrekitIllegalArgumentException( 234 OrekitMessages.OUT_OF_RANGE_SECONDS_NUMBER_DETAIL, 235 // this can produce some strange messages due to rounding 236 secondInDayA + secondInDayB, 237 0, 238 Constants.JULIAN_DAY); 239 } 240 final int maxExtraSeconds = minuteDuration - 60; 241 if (leap * maxExtraSeconds < 0 || 242 FastMath.abs(leap) > FastMath.abs(maxExtraSeconds)) { 243 throw new OrekitIllegalArgumentException( 244 OrekitMessages.OUT_OF_RANGE_SECONDS_NUMBER_DETAIL, 245 leap, 0, maxExtraSeconds); 246 } 247 248 // extract the time components 249 hour = wholeSeconds / 3600; 250 wholeSeconds -= 3600 * hour; 251 minute = wholeSeconds / 60; 252 wholeSeconds -= 60 * minute; 253 // at this point ((minuteDuration - wholeSeconds) - leap) - fractional > 0 254 // or else one of the preconditions was violated. Even if there is not violation, 255 // naiveSecond may round to minuteDuration, creating an invalid time. 256 // In that case round down to preserve a valid time at the cost of up to 1 ULP of error. 257 // See #676 and #681. 258 final double naiveSecond = wholeSeconds + (leap + fractional); 259 if (naiveSecond < 0) { 260 throw new OrekitIllegalArgumentException( 261 OrekitMessages.OUT_OF_RANGE_SECONDS_NUMBER_DETAIL, 262 naiveSecond, 0, minuteDuration); 263 } 264 if (naiveSecond < minuteDuration || Double.isNaN(naiveSecond)) { 265 second = naiveSecond; 266 } else { 267 second = FastMath.nextDown((double) minuteDuration); 268 } 269 minutesFromUTC = 0; 270 271 } 272 273 /** 274 * Build a time from the second number within the day. 275 * 276 * <p>The seconds past midnight is the sum {@code secondInDayA + secondInDayB + 277 * leap}. The two parameters are used for increased accuracy. Only the first part of 278 * the sum ({@code secondInDayA + secondInDayB}) is used to compute the hours and 279 * minutes. The third parameter ({@code leap}) is added directly to the second value 280 * ({@link #getSecond()}) to implement leap seconds. These three quantities must 281 * satisfy the following constraints. This first guarantees the hour and minute are 282 * valid, the second guarantees the second is valid. 283 * 284 * <pre> 285 * {@code 0 <= secondInDayA + secondInDayB < 86400} 286 * {@code 0 <= (secondInDayA + secondInDayB) % 60 + leap <= minuteDuration} 287 * {@code 0 <= leap <= minuteDuration - 60 if minuteDuration >= 60} 288 * {@code 0 >= leap >= minuteDuration - 60 if minuteDuration < 60} 289 * </pre> 290 * 291 * <p>If the seconds of minute ({@link #getSecond()}) computed from {@code 292 * secondInDayA + secondInDayB + leap} is greater than or equal to {@code 60 + leap} 293 * then the second of minute will be set to {@code FastMath.nextDown(60 + leap)}. This 294 * prevents rounding to an invalid seconds of minute number when the input values have 295 * greater precision than a {@code double}. 296 * 297 * <p>This constructor is always in UTC (i.e. {@link #getMinutesFromUTC() will return 298 * 0}). 299 * 300 * <p>If {@code secondsInDayB} or {@code leap} is NaN then the hour and minute will 301 * be determined from {@code secondInDayA} and the second of minute will be NaN. 302 * 303 * @param secondInDayA first part of the second number. 304 * @param secondInDayB last part of the second number. 305 * @param leap magnitude of the leap second if this point in time is during 306 * a leap second, otherwise {@code 0.0}. This value is not used 307 * to compute hours and minutes, but it is added to the computed 308 * second of minute. 309 * @param minuteDuration number of seconds in the current minute, normally {@code 60}. 310 * @return new time components for the specified time. 311 * @throws OrekitIllegalArgumentException if the inequalities above do not hold. 312 * @since 10.2 313 */ 314 public static TimeComponents fromSeconds(final int secondInDayA, 315 final double secondInDayB, 316 final double leap, 317 final int minuteDuration) { 318 return new TimeComponents(secondInDayA, secondInDayB, leap, minuteDuration); 319 } 320 321 /** Parse a string in ISO-8601 format to build a time. 322 * <p>The supported formats are: 323 * <ul> 324 * <li>basic and extended format local time: hhmmss, hh:mm:ss (with optional decimals in seconds)</li> 325 * <li>optional UTC time: hhmmssZ, hh:mm:ssZ</li> 326 * <li>optional signed hours UTC offset: hhmmss+HH, hhmmss-HH, hh:mm:ss+HH, hh:mm:ss-HH</li> 327 * <li>optional signed basic hours and minutes UTC offset: hhmmss+HHMM, hhmmss-HHMM, hh:mm:ss+HHMM, hh:mm:ss-HHMM</li> 328 * <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> 329 * </ul> 330 * 331 * <p> As shown by the list above, only the complete representations defined in section 4.2 332 * of ISO-8601 standard are supported, neither expended representations nor representations 333 * with reduced accuracy are supported. 334 * 335 * @param string string to parse 336 * @return a parsed time 337 * @exception IllegalArgumentException if string cannot be parsed 338 */ 339 public static TimeComponents parseTime(final String string) { 340 341 // is the date a calendar date ? 342 final Matcher timeMatcher = ISO8601_FORMATS.matcher(string); 343 if (timeMatcher.matches()) { 344 final int hour = Integer.parseInt(timeMatcher.group(1)); 345 final int minute = Integer.parseInt(timeMatcher.group(2)); 346 final double second = timeMatcher.group(3) == null ? 0.0 : Double.parseDouble(timeMatcher.group(3).replace(',', '.')); 347 final String offset = timeMatcher.group(4); 348 final int minutesFromUTC; 349 if (offset == null) { 350 // no offset from UTC is given 351 minutesFromUTC = 0; 352 } else { 353 // we need to parse an offset from UTC 354 // the sign is mandatory and the ':' separator is optional 355 // so we can have offsets given as -06:00 or +0100 356 final int sign = offset.codePointAt(0) == '-' ? -1 : +1; 357 final int hourOffset = Integer.parseInt(offset.substring(1, 3)); 358 final int minutesOffset = offset.length() <= 3 ? 0 : Integer.parseInt(offset.substring(offset.length() - 2)); 359 minutesFromUTC = sign * (minutesOffset + 60 * hourOffset); 360 } 361 return new TimeComponents(hour, minute, second, minutesFromUTC); 362 } 363 364 throw new OrekitIllegalArgumentException(OrekitMessages.NON_EXISTENT_TIME, string); 365 366 } 367 368 /** Get the hour number. 369 * @return hour number from 0 to 23 370 */ 371 public int getHour() { 372 return hour; 373 } 374 375 /** Get the minute number. 376 * @return minute minute number from 0 to 59 377 */ 378 public int getMinute() { 379 return minute; 380 } 381 382 /** Get the seconds number. 383 * @return second second number from 0.0 to 61.0 (excluded). Note that 60 ≤ second 384 * < 61 only occurs during a leap second. 385 */ 386 public double getSecond() { 387 return second; 388 } 389 390 /** Get the offset between the specified date and UTC. 391 * <p> 392 * The offset is always an integral number of minutes, as per ISO-8601 standard. 393 * </p> 394 * @return offset in minutes between the specified date and UTC 395 * @since 7.2 396 */ 397 public int getMinutesFromUTC() { 398 return minutesFromUTC; 399 } 400 401 /** Get the second number within the local day, <em>without</em> applying the {@link #getMinutesFromUTC() offset from UTC}. 402 * @return second number from 0.0 to Constants.JULIAN_DAY 403 * @see #getSecondsInUTCDay() 404 * @since 7.2 405 */ 406 public double getSecondsInLocalDay() { 407 return second + 60 * minute + 3600 * hour; 408 } 409 410 /** Get the second number within the UTC day, applying the {@link #getMinutesFromUTC() offset from UTC}. 411 * @return second number from {@link #getMinutesFromUTC() -getMinutesFromUTC()} 412 * to Constants.JULIAN_DAY {@link #getMinutesFromUTC() + getMinutesFromUTC()} 413 * @see #getSecondsInLocalDay() 414 * @since 7.2 415 */ 416 public double getSecondsInUTCDay() { 417 return second + 60 * (minute - minutesFromUTC) + 3600 * hour; 418 } 419 420 /** 421 * Package private method that allows specification of seconds format. Allows access 422 * from {@link DateTimeComponents#toString(int, int)}. Access from outside of rounding 423 * methods would result in invalid times, see #590, #591. 424 * 425 * @param secondsFormat for the seconds. 426 * @return string without UTC offset. 427 */ 428 String toStringWithoutUtcOffset(final DecimalFormat secondsFormat) { 429 return String.format("%02d:%02d:%s", hour, minute, secondsFormat.format(second)); 430 } 431 432 /** 433 * Get a string representation of the time without the offset from UTC. 434 * 435 * @return a string representation of the time in an ISO 8601 like format. 436 * @see #formatUtcOffset() 437 * @see #toString() 438 */ 439 public String toStringWithoutUtcOffset() { 440 // create formats here as they are not thread safe 441 // Format for seconds to prevent rounding up to an invalid time. See #591 442 final DecimalFormat secondsFormat = 443 new DecimalFormat("00.000###########", US_SYMBOLS); 444 return toStringWithoutUtcOffset(secondsFormat); 445 } 446 447 /** 448 * Get the UTC offset as a string in ISO8601 format. For example, {@code +00:00}. 449 * 450 * @return the UTC offset as a string. 451 * @see #toStringWithoutUtcOffset() 452 * @see #toString() 453 */ 454 public String formatUtcOffset() { 455 final int hourOffset = FastMath.abs(minutesFromUTC) / 60; 456 final int minuteOffset = FastMath.abs(minutesFromUTC) % 60; 457 return (minutesFromUTC < 0 ? '-' : '+') + 458 String.format("%02d:%02d", hourOffset, minuteOffset); 459 } 460 461 /** 462 * Get a string representation of the time including the offset from UTC. 463 * 464 * @return string representation of the time in an ISO 8601 like format including the 465 * UTC offset. 466 * @see #toStringWithoutUtcOffset() 467 * @see #formatUtcOffset() 468 */ 469 public String toString() { 470 return toStringWithoutUtcOffset() + formatUtcOffset(); 471 } 472 473 /** {@inheritDoc} */ 474 public int compareTo(final TimeComponents other) { 475 return Double.compare(getSecondsInUTCDay(), other.getSecondsInUTCDay()); 476 } 477 478 /** {@inheritDoc} */ 479 public boolean equals(final Object other) { 480 try { 481 final TimeComponents otherTime = (TimeComponents) other; 482 return otherTime != null && 483 hour == otherTime.hour && 484 minute == otherTime.minute && 485 second == otherTime.second && 486 minutesFromUTC == otherTime.minutesFromUTC; 487 } catch (ClassCastException cce) { 488 return false; 489 } 490 } 491 492 /** {@inheritDoc} */ 493 public int hashCode() { 494 final long bits = Double.doubleToLongBits(second); 495 return ((hour << 16) ^ ((minute - minutesFromUTC) << 8)) ^ (int) (bits ^ (bits >>> 32)); 496 } 497 498 }