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 &le; second
384      * &lt; 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 }