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