1 /* Copyright 2002-2023 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 }