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