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.util.regex.Matcher;
21  import java.util.regex.Pattern;
22  
23  import org.hipparchus.util.FastMath;
24  import org.orekit.errors.OrekitIllegalArgumentException;
25  import org.orekit.errors.OrekitMessages;
26  import org.orekit.utils.Constants;
27  
28  
29  /** Class representing a time within the day broken up as hour,
30   * minute and second components.
31   * <p>Instances of this class are guaranteed to be immutable.</p>
32   * @see DateComponents
33   * @see DateTimeComponents
34   * @author Luc Maisonobe
35   */
36  public class TimeComponents implements Serializable, Comparable<TimeComponents> {
37  
38      /** Constant for commonly used hour 00:00:00. */
39      public static final TimeComponents H00   = new TimeComponents(0, 0, TimeOffset.ZERO);
40  
41      /** Constant for commonly used hour 12:00:00. */
42      public static final TimeComponents H12 = new TimeComponents(12, 0, TimeOffset.ZERO);
43  
44      // CHECKSTYLE: stop ConstantName
45      /** Constant for NaN time.
46       * @since 13.0
47       */
48      public static final TimeComponents NaN   = new TimeComponents(0, 0, TimeOffset.NaN);
49      // CHECKSTYLE: resume ConstantName
50  
51      /** Wrapping limits for rounding to next minute.
52       * @since 13.0
53       */
54      private static final TimeOffset[] WRAPPING = new TimeOffset[] {
55          new TimeOffset(59L, 500000000000000000L), // round to second
56          new TimeOffset(59L, 950000000000000000L), // round to 10⁻¹ second
57          new TimeOffset(59L, 995000000000000000L), // round to 10⁻² second
58          new TimeOffset(59L, 999500000000000000L), // round to 10⁻³ second
59          new TimeOffset(59L, 999950000000000000L), // round to 10⁻⁴ second
60          new TimeOffset(59L, 999995000000000000L), // round to 10⁻⁵ second
61          new TimeOffset(59L, 999999500000000000L), // round to 10⁻⁶ second
62          new TimeOffset(59L, 999999950000000000L), // round to 10⁻⁷ second
63          new TimeOffset(59L, 999999995000000000L), // round to 10⁻⁸ second
64          new TimeOffset(59L, 999999999500000000L), // round to 10⁻⁹ second
65          new TimeOffset(59L, 999999999950000000L), // round to 10⁻¹⁰ second
66          new TimeOffset(59L, 999999999995000000L), // round to 10⁻¹¹ second
67          new TimeOffset(59L, 999999999999500000L), // round to 10⁻¹² second
68          new TimeOffset(59L, 999999999999950000L), // round to 10⁻¹³ second
69          new TimeOffset(59L, 999999999999995000L), // round to 10⁻¹⁴ second
70          new TimeOffset(59L, 999999999999999500L), // round to 10⁻¹⁵ second
71          new TimeOffset(59L, 999999999999999950L), // round to 10⁻¹⁶ second
72          new TimeOffset(59L, 999999999999999995L)  // round to 10⁻¹⁷ second
73      };
74  
75      /** Offset values for rounding attoseconds.
76       * @since 13.0
77       */
78      // CHECKSTYLE: stop Indentation check */
79      private static final long[] ROUNDING = new long[] {
80          500000000000000000L, // round to second
81           50000000000000000L, // round to 10⁻¹ second
82            5000000000000000L, // round to 10⁻² second
83             500000000000000L, // round to 10⁻³ second
84              50000000000000L, // round to 10⁻⁴ second
85               5000000000000L, // round to 10⁻⁵ second
86                500000000000L, // round to 10⁻⁶ second
87                 50000000000L, // round to 10⁻⁷ second
88                  5000000000L, // round to 10⁻⁸ second
89                   500000000L, // round to 10⁻⁹ second
90                    50000000L, // round to 10⁻¹⁰ second
91                     5000000L, // round to 10⁻¹¹ second
92                      500000L, // round to 10⁻¹² second
93                       50000L, // round to 10⁻¹³ second
94                        5000L, // round to 10⁻¹⁴ second
95                         500L, // round to 10⁻¹⁵ second
96                          50L, // round to 10⁻¹⁶ second
97                           5L, // round to 10⁻¹⁷ second
98                           0L, // round to 10⁻¹⁸ second
99      };
100     // CHECKSTYLE: resume Indentation check */
101 
102     /** Serializable UID. */
103     private static final long serialVersionUID = 20240712L;
104 
105     /** Basic and extends formats for local time, with optional timezone. */
106     private static final Pattern ISO8601_FORMATS = Pattern.compile("^(\\d\\d):?(\\d\\d):?(\\d\\d(?:[.,]\\d+)?)?(?:Z|([-+]\\d\\d(?::?\\d\\d)?))?$");
107 
108     /** Number of seconds in one hour. */
109     private static final int HOUR = 3600;
110 
111     /** Number of seconds in one minute. */
112     private static final int MINUTE = 60;
113 
114     /** Constant for 23 hours. */
115     private static final int TWENTY_THREE = 23;
116 
117     /** Constant for 59 minutes. */
118     private static final int FIFTY_NINE = 59;
119 
120     /** Constant for 23:59. */
121     private static final TimeOffset TWENTY_THREE_FIFTY_NINE =
122         new TimeOffset(TWENTY_THREE * HOUR + FIFTY_NINE * MINUTE, 0L);
123 
124     /** Hour number. */
125     private final int hour;
126 
127     /** Minute number. */
128     private final int minute;
129 
130     /** Second number. */
131     private final TimeOffset second;
132 
133     /** Offset between the specified date and UTC.
134      * <p>
135      * Always an integral number of minutes, as per ISO-8601 standard.
136      * </p>
137      * @since 7.2
138      */
139     private final int minutesFromUTC;
140 
141     /** Build a time from its clock elements.
142      * <p>Note that seconds between 60.0 (inclusive) and 61.0 (exclusive) are allowed
143      * in this method, since they do occur during leap seconds introduction
144      * in the {@link UTCScale UTC} time scale.</p>
145      * @param hour hour number from 0 to 23
146      * @param minute minute number from 0 to 59
147      * @param second second number from 0.0 to 61.0 (excluded)
148      * @exception IllegalArgumentException if inconsistent arguments
149      * are given (parameters out of range)
150      */
151     public TimeComponents(final int hour, final int minute, final double second)
152         throws IllegalArgumentException {
153         this(hour, minute, new TimeOffset(second));
154     }
155 
156     /** Build a time from its clock elements.
157      * <p>Note that seconds between 60.0 (inclusive) and 61.0 (exclusive) are allowed
158      * in this method, since they do occur during leap seconds introduction
159      * in the {@link UTCScale UTC} time scale.</p>
160      * @param hour hour number from 0 to 23
161      * @param minute minute number from 0 to 59
162      * @param second second number from 0.0 to 61.0 (excluded)
163      * @exception IllegalArgumentException if inconsistent arguments
164      * are given (parameters out of range)
165      * @since 13.0
166      */
167     public TimeComponents(final int hour, final int minute, final TimeOffset second)
168         throws IllegalArgumentException {
169         this(hour, minute, second, 0);
170     }
171 
172     /** Build a time from its clock elements.
173      * <p>Note that seconds between 60.0 (inclusive) and 61.0 (exclusive) are allowed
174      * in this method, since they do occur during leap seconds introduction
175      * in the {@link UTCScale UTC} time scale.</p>
176      * @param hour hour number from 0 to 23
177      * @param minute minute number from 0 to 59
178      * @param second second number from 0.0 to 61.0 (excluded)
179      * @param minutesFromUTC offset between the specified date and UTC, as an
180      * integral number of minutes, as per ISO-8601 standard
181      * @exception IllegalArgumentException if inconsistent arguments
182      * are given (parameters out of range)
183      * @since 7.2
184      */
185     public TimeComponents(final int hour, final int minute, final double second, final int minutesFromUTC)
186         throws IllegalArgumentException {
187         this(hour, minute, new TimeOffset(second), minutesFromUTC);
188     }
189 
190     /** Build a time from its clock elements.
191      * <p>Note that seconds between 60.0 (inclusive) and 61.0 (exclusive) are allowed
192      * in this method, since they do occur during leap seconds introduction
193      * in the {@link UTCScale UTC} time scale.</p>
194      * @param hour hour number from 0 to 23
195      * @param minute minute number from 0 to 59
196      * @param second second number from 0.0 to 62.0 (excluded, more than 61 s occurred on
197      *               the 1961 leap second, which was between 1 and 2 seconds in duration)
198      * @param minutesFromUTC offset between the specified date and UTC, as an
199      * integral number of minutes, as per ISO-8601 standard
200      * @exception IllegalArgumentException if inconsistent arguments
201      * are given (parameters out of range)
202      * @since 13.0
203      */
204     public TimeComponents(final int hour, final int minute, final TimeOffset second,
205                           final int minutesFromUTC)
206         throws IllegalArgumentException {
207 
208         // range check
209         if (hour < 0 || hour > 23 ||
210             minute < 0 || minute > 59 ||
211             second.getSeconds() < 0L || second.getSeconds() >= 62L) {
212             throw new OrekitIllegalArgumentException(OrekitMessages.NON_EXISTENT_HMS_TIME,
213                                                      hour, minute, second.toDouble());
214         }
215 
216         this.hour           = hour;
217         this.minute         = minute;
218         this.second         = second;
219         this.minutesFromUTC = minutesFromUTC;
220 
221     }
222 
223     /**
224      * Build a time from the second number within the day.
225      *
226      * <p>If the {@code secondInDay} is less than {@code 60.0} then {@link #getSecond()}
227      * and {@link #getSplitSecond()} will be less than {@code 60.0}, otherwise they will be
228      * less than {@code 61.0}. This constructor may produce an invalid value of
229      * {@link #getSecond()} and {@link #getSplitSecond()} during a negative leap second,
230      * through there has never been one. For more control over the number of seconds in
231      * the final minute use {@link #TimeComponents(TimeOffset, TimeOffset, int)}.
232      *
233      * <p>This constructor is always in UTC (i.e. {@link #getMinutesFromUTC() will return
234      * 0}).
235      *
236      * @param secondInDay second number from 0.0 to {@link Constants#JULIAN_DAY} {@code +
237      *                    1} (excluded)
238      * @throws OrekitIllegalArgumentException if seconds number is out of range
239      * @see #TimeComponents(TimeOffset, TimeOffset, int)
240      * @see #TimeComponents(int, double)
241      */
242     public TimeComponents(final double secondInDay)
243             throws OrekitIllegalArgumentException {
244         this(new TimeOffset(secondInDay));
245     }
246 
247     /**
248      * Build a time from the second number within the day.
249      *
250      * <p>The second number is defined here as the sum
251      * {@code secondInDayA + secondInDayB} from 0.0 to {@link Constants#JULIAN_DAY}
252      * {@code + 1} (excluded). The two parameters are used for increased accuracy.
253      *
254      * <p>If the sum is less than {@code 60.0} then {@link #getSecond()} will be less
255      * than {@code 60.0}, otherwise it will be less than {@code 61.0}. This constructor
256      * may produce an invalid value of {@link #getSecond()} during a negative leap second,
257      * through there has never been one. For more control over the number of seconds in
258      * the final minute use {@link #TimeComponents(TimeOffset, TimeOffset, int)}.
259      *
260      * <p>This constructor is always in UTC (i.e. {@link #getMinutesFromUTC()} will
261      * return 0).
262      *
263      * @param secondInDayA first part of the second number
264      * @param secondInDayB last part of the second number
265      * @throws OrekitIllegalArgumentException if seconds number is out of range
266      * @see #TimeComponents(TimeOffset, TimeOffset, int)
267      */
268     public TimeComponents(final int secondInDayA, final double secondInDayB)
269             throws OrekitIllegalArgumentException {
270 
271         // if the total is at least 86400 then assume there is a leap second
272         final TimeOffset aPlusB = new TimeOffset(secondInDayA).add(new TimeOffset(secondInDayB));
273         final TimeComponents tc     = aPlusB.compareTo(TimeOffset.DAY) >= 0 ?
274                                       new TimeComponents(aPlusB.subtract(TimeOffset.SECOND), TimeOffset.SECOND, 61) :
275                                       new TimeComponents(aPlusB, TimeOffset.ZERO, 60);
276 
277         this.hour           = tc.hour;
278         this.minute         = tc.minute;
279         this.second         = tc.second;
280         this.minutesFromUTC = tc.minutesFromUTC;
281 
282     }
283 
284     /**
285      * Build a time from the second number within the day.
286      *
287      * <p>If the {@code secondInDay} is less than {@code 60.0} then {@link #getSecond()}
288      * will be less than {@code 60.0}, otherwise it will be less than {@code 61.0}. This constructor
289      * may produce an invalid value of {@link #getSecond()} during a negative leap second,
290      * through there has never been one. For more control over the number of seconds in
291      * the final minute use {@link #TimeComponents(TimeOffset, TimeOffset, int)}.
292      *
293      * <p>This constructor is always in UTC (i.e. {@link #getMinutesFromUTC() will return
294      * 0}).
295      *
296      * @param splitSecondInDay second number from 0.0 to {@link Constants#JULIAN_DAY} {@code +
297      *                    1} (excluded)
298      * @see #TimeComponents(TimeOffset, TimeOffset, int)
299      * @see #TimeComponents(int, double)
300      * @since 13.0
301      */
302     public TimeComponents(final TimeOffset splitSecondInDay) {
303         if (splitSecondInDay.compareTo(TimeOffset.ZERO) < 0) {
304             // negative time
305             throw new OrekitIllegalArgumentException(OrekitMessages.OUT_OF_RANGE_SECONDS_NUMBER_DETAIL,
306                                                      splitSecondInDay.toDouble(),
307                                                      0, TimeOffset.DAY_WITH_POSITIVE_LEAP.getSeconds());
308         } else if (splitSecondInDay.compareTo(TimeOffset.DAY) >= 0) {
309             // if the total is at least 86400 then assume there is a leap second
310             if (splitSecondInDay.compareTo(TimeOffset.DAY_WITH_POSITIVE_LEAP) >= 0) {
311                 // more than one leap second is too much
312                 throw new OrekitIllegalArgumentException(OrekitMessages.OUT_OF_RANGE_SECONDS_NUMBER_DETAIL,
313                                                          splitSecondInDay.toDouble(),
314                                                          0, TimeOffset.DAY_WITH_POSITIVE_LEAP.getSeconds());
315             } else {
316                 hour   = TWENTY_THREE;
317                 minute = FIFTY_NINE;
318                 second = splitSecondInDay.subtract(TWENTY_THREE_FIFTY_NINE);
319             }
320         } else {
321             // regular time within day
322             hour   = (int) splitSecondInDay.getSeconds() / HOUR;
323             minute = ((int) splitSecondInDay.getSeconds() % HOUR) / MINUTE;
324             second = splitSecondInDay.subtract(new TimeOffset(hour * HOUR + minute * MINUTE, 0L));
325         }
326 
327         minutesFromUTC = 0;
328 
329     }
330 
331     /**
332      * Build a time from the second number within the day.
333      *
334      * <p>The seconds past midnight is the sum {@code secondInDay + leap}. Only the part
335      * {@code secondInDay} is used to compute the hours and minutes. The second parameter
336      * ({@code leap}) is added directly to the second value ({@link #getSecond()}) to
337      * implement leap seconds. These two quantities must satisfy the following constraints.
338      * This first guarantees the hour and minute are valid, the second guarantees the second
339      * is valid.
340      *
341      * <pre>
342      *     {@code 0 <= secondInDay < 86400}
343      *     {@code 0 <= secondInDay % 60 + leap <= minuteDuration}
344      *     {@code 0 <= leap <= minuteDuration - 60 if minuteDuration >= 60}
345      *     {@code 0 >= leap >= minuteDuration - 60 if minuteDuration <  60}
346      * </pre>
347      *
348      * <p>If the seconds of minute ({@link #getSecond()}) computed from {@code
349      * secondInDay + leap} is greater than or equal to {@code 60 + leap}
350      * then the second of minute will be set to {@code FastMath.nextDown(60 + leap)}. This
351      * prevents rounding to an invalid seconds of minute number when the input values have
352      * greater precision than a {@code double}.
353      *
354      * <p>This constructor is always in UTC (i.e. {@link #getMinutesFromUTC() will return
355      * 0}).
356      *
357      * <p>If {@code secondsInDay} or {@code leap} is NaN then the hour and minute will
358      * be set arbitrarily and the second of minute will be NaN.
359      *
360      * @param secondInDay    part of the second number.
361      * @param leap           magnitude of the leap second if this point in time is during
362      *                       a leap second, otherwise {@code 0.0}. This value is not used
363      *                       to compute hours and minutes, but it is added to the computed
364      *                       second of minute.
365      * @param minuteDuration number of seconds in the current minute, normally {@code 60}.
366      * @throws OrekitIllegalArgumentException if the inequalities above do not hold.
367      * @since 10.2
368      */
369     public TimeComponents(final TimeOffset secondInDay, final TimeOffset leap, final int minuteDuration) {
370 
371         minutesFromUTC = 0;
372 
373         if (secondInDay.isNaN()) {
374             // special handling for NaN
375             hour   = 0;
376             minute = 0;
377             second = secondInDay;
378             return;
379         }
380 
381         // range check
382         if (secondInDay.compareTo(TimeOffset.ZERO) < 0 || secondInDay.compareTo(TimeOffset.DAY) >= 0) {
383             throw new OrekitIllegalArgumentException(OrekitMessages.OUT_OF_RANGE_SECONDS_NUMBER_DETAIL,
384                                                      // this can produce some strange messages due to rounding
385                                                      secondInDay.toDouble(), 0, Constants.JULIAN_DAY);
386         }
387         final int maxExtraSeconds = minuteDuration - MINUTE;
388         if (leap.getSeconds() * maxExtraSeconds < 0 || FastMath.abs(leap.getSeconds()) > FastMath.abs(maxExtraSeconds)) {
389             throw new OrekitIllegalArgumentException(OrekitMessages.OUT_OF_RANGE_SECONDS_NUMBER_DETAIL,
390                                                      leap, 0, maxExtraSeconds);
391         }
392 
393         // extract the time components
394         int wholeSeconds = (int) secondInDay.getSeconds();
395         hour           = wholeSeconds / HOUR;
396         wholeSeconds  -= HOUR * hour;
397         minute         = wholeSeconds / MINUTE;
398         wholeSeconds  -= MINUTE * minute;
399         // at this point ((minuteDuration - wholeSeconds) - leap) - fractional > 0
400         // or else one of the preconditions was violated. Even if there is no violation,
401         // naiveSecond may round to minuteDuration, creating an invalid time.
402         // In that case round down to preserve a valid time at the cost of up to 1as of error.
403         // See #676 and #681.
404         final TimeOffset naiveSecond = new TimeOffset(wholeSeconds, secondInDay.getAttoSeconds()).add(leap);
405         if (naiveSecond.compareTo(TimeOffset.ZERO) < 0) {
406             throw new OrekitIllegalArgumentException(
407                     OrekitMessages.OUT_OF_RANGE_SECONDS_NUMBER_DETAIL,
408                     naiveSecond, 0, minuteDuration);
409         }
410         if (naiveSecond.getSeconds() < minuteDuration) {
411             second = naiveSecond;
412         } else {
413             second = new TimeOffset(minuteDuration - 1, 999999999999999999L);
414         }
415 
416     }
417 
418     /** Parse a string in ISO-8601 format to build a time.
419      * <p>The supported formats are:
420      * <ul>
421      *   <li>basic and extended format local time: hhmmss, hh:mm:ss (with optional decimals in seconds)</li>
422      *   <li>optional UTC time: hhmmssZ, hh:mm:ssZ</li>
423      *   <li>optional signed hours UTC offset: hhmmss+HH, hhmmss-HH, hh:mm:ss+HH, hh:mm:ss-HH</li>
424      *   <li>optional signed basic hours and minutes UTC offset: hhmmss+HHMM, hhmmss-HHMM, hh:mm:ss+HHMM, hh:mm:ss-HHMM</li>
425      *   <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>
426      * </ul>
427      *
428      * <p> As shown by the list above, only the complete representations defined in section 4.2
429      * of ISO-8601 standard are supported, neither expended representations nor representations
430      * with reduced accuracy are supported.
431      *
432      * @param string string to parse
433      * @return a parsed time
434      * @exception IllegalArgumentException if string cannot be parsed
435      */
436     public static TimeComponents parseTime(final String string) {
437 
438         // is the date a calendar date ?
439         final Matcher timeMatcher = ISO8601_FORMATS.matcher(string);
440         if (timeMatcher.matches()) {
441             final int        hour    = Integer.parseInt(timeMatcher.group(1));
442             final int        minute  = Integer.parseInt(timeMatcher.group(2));
443             final TimeOffset second  = timeMatcher.group(3) == null ?
444                                        TimeOffset.ZERO :
445                                        TimeOffset.parse(timeMatcher.group(3).replace(',', '.'));
446             final String     offset  = timeMatcher.group(4);
447             final int    minutesFromUTC;
448             if (offset == null) {
449                 // no offset from UTC is given
450                 minutesFromUTC = 0;
451             } else {
452                 // we need to parse an offset from UTC
453                 // the sign is mandatory and the ':' separator is optional
454                 // so we can have offsets given as -06:00 or +0100
455                 final int sign          = offset.codePointAt(0) == '-' ? -1 : +1;
456                 final int hourOffset    = Integer.parseInt(offset.substring(1, 3));
457                 final int minutesOffset = offset.length() <= 3 ? 0 : Integer.parseInt(offset.substring(offset.length() - 2));
458                 minutesFromUTC          = sign * (minutesOffset + MINUTE * hourOffset);
459             }
460             return new TimeComponents(hour, minute, second, minutesFromUTC);
461         }
462 
463         throw new OrekitIllegalArgumentException(OrekitMessages.NON_EXISTENT_TIME, string);
464 
465     }
466 
467     /** Get the hour number.
468      * @return hour number from 0 to 23
469      */
470     public int getHour() {
471         return hour;
472     }
473 
474     /** Get the minute number.
475      * @return minute minute number from 0 to 59
476      */
477     public int getMinute() {
478         return minute;
479     }
480 
481     /** Get the seconds number.
482      * @return second second number from 0.0 to 61.0 (excluded). Note that 60 &le; second
483      * &lt; 61 only occurs during a leap second.
484      */
485     public double getSecond() {
486         return second.toDouble();
487     }
488 
489     /** Get the seconds number.
490      * @return second second number from 0.0 to 61.0 (excluded). Note that 60 &le; second
491      * &lt; 61 only occurs during a leap second.
492      */
493     public TimeOffset getSplitSecond() {
494         return second;
495     }
496 
497     /** Get the offset between the specified date and UTC.
498      * <p>
499      * The offset is always an integral number of minutes, as per ISO-8601 standard.
500      * </p>
501      * @return offset in minutes between the specified date and UTC
502      * @since 7.2
503      */
504     public int getMinutesFromUTC() {
505         return minutesFromUTC;
506     }
507 
508     /** Get the second number within the local day, <em>without</em> applying the {@link #getMinutesFromUTC() offset from UTC}.
509      * @return second number from 0.0 to Constants.JULIAN_DAY
510      * @see #getSplitSecondsInLocalDay()
511      * @see #getSecondsInUTCDay()
512      * @since 7.2
513      */
514     public double getSecondsInLocalDay() {
515         return getSplitSecondsInLocalDay().toDouble();
516     }
517 
518     /** Get the second number within the local day, <em>without</em> applying the {@link #getMinutesFromUTC() offset from UTC}.
519      * @return second number from 0.0 to Constants.JULIAN_DAY
520      * @see #getSecondsInLocalDay()
521      * @see #getSplitSecondsInUTCDay()
522      * @since 13.0
523      */
524     public TimeOffset getSplitSecondsInLocalDay() {
525         return new TimeOffset((long) MINUTE * minute + (long) HOUR * hour, 0L).add(second);
526     }
527 
528     /** Get the second number within the UTC day, applying the {@link #getMinutesFromUTC() offset from UTC}.
529      * @return second number from {@link #getMinutesFromUTC() -getMinutesFromUTC()}
530      * to Constants.JULIAN_DAY {@link #getMinutesFromUTC() + getMinutesFromUTC()}
531      * @see #getSplitSecondsInUTCDay()
532      * @see #getSecondsInLocalDay()
533      * @since 7.2
534      */
535     public double getSecondsInUTCDay() {
536         return getSplitSecondsInUTCDay().toDouble();
537     }
538 
539     /** Get the second number within the UTC day, applying the {@link #getMinutesFromUTC() offset from UTC}.
540      * @return second number from {@link #getMinutesFromUTC() -getMinutesFromUTC()}
541      * to Constants.JULIAN_DAY {@link #getMinutesFromUTC() + getMinutesFromUTC()}
542      * @see #getSecondsInUTCDay()
543      * @see #getSplitSecondsInLocalDay()
544      * @since 13.0
545      */
546     public TimeOffset getSplitSecondsInUTCDay() {
547         return new TimeOffset((long) MINUTE * (minute - minutesFromUTC) + (long) HOUR * hour, 0L).add(second);
548     }
549 
550     /**
551      * Round this time to the given precision if needed to prevent rounding up to an
552      * invalid seconds number. This is useful, for example, when writing custom date-time
553      * formatting methods so one does not, e.g., end up with "60.0" seconds during a
554      * normal minute when the value of seconds is {@code 59.999}. This method will instead
555      * round up the minute, hour, day, month, and year as needed.
556      *
557      * @param minuteDuration 59, 60, 61, or 62 seconds depending on the date being close
558      *                       to a leap second introduction and the magnitude of the leap
559      *                       second.
560      * @param fractionDigits the number of decimal digits after the decimal point in the
561      *                       seconds number that will be printed. This date-time is
562      *                       rounded to {@code fractionDigits} after the decimal point if
563      *                       necessary to prevent rounding up to {@code minuteDuration}.
564      *                       {@code fractionDigits} must be greater than or equal to
565      *                       {@code 0}.
566      * @return the instance itself if no rounding was needed, or a time within
567      * {@code 0.5 * 10**-fractionDigits} seconds of this, and with a seconds number that
568      * will not round up to {@code minuteDuration} when rounded to {@code fractionDigits}
569      * after the decimal point
570      * @since 13.0
571      */
572     public TimeComponents wrapIfNeeded(final int minuteDuration, final int fractionDigits) {
573         TimeOffset wrappedSecond = second;
574 
575         // adjust limit according to current minute duration
576         final TimeOffset limit = WRAPPING[FastMath.min(fractionDigits, WRAPPING.length - 1)].
577                                 add(new TimeOffset(minuteDuration - 60, 0L));
578 
579         if (wrappedSecond.compareTo(limit) >= 0) {
580             // we should wrap around to the next minute
581             int wrappedMinute = minute;
582             int wrappedHour   = hour;
583             wrappedSecond = TimeOffset.ZERO;
584             ++wrappedMinute;
585             if (wrappedMinute > 59) {
586                 wrappedMinute = 0;
587                 ++wrappedHour;
588                 if (wrappedHour > 23) {
589                     wrappedHour = 0;
590                 }
591             }
592             return new TimeComponents(wrappedHour, wrappedMinute, wrappedSecond);
593         }
594         return this;
595     }
596 
597     /**
598      * Package private method that allows specification of seconds format. Allows access from
599      * {@link DateTimeComponents#toString(int, int)}. Access from outside of rounding methods would result in invalid
600      * times, see #590, #591.
601      *
602      * @param fractionDigits the number of digits to include after the decimal point in the string representation of the
603      *                       seconds. The date and time is first rounded as necessary. {@code fractionDigits} must be
604      *                       greater than or equal to {@code 0}.
605      * @return string without UTC offset.
606      * @since 13.0
607      */
608     String toStringWithoutUtcOffset(final int fractionDigits) {
609 
610         if (second.isFinite()) {
611             // general case for regular times
612             final long      rounding = ROUNDING[FastMath.min(fractionDigits, ROUNDING.length - 1)];
613             final TimeComponents rounded  = new TimeComponents(hour, minute,
614                                                                new TimeOffset(second.getSeconds(),
615                                                                               second.getAttoSeconds() + rounding));
616             final StringBuilder builder = new StringBuilder();
617             builder.append(String.format("%02d:%02d:%02d",
618                                          rounded.hour, rounded.minute, rounded.second.getSeconds()));
619             if (fractionDigits > 0) {
620                 builder.append('.');
621                 builder.append(String.format("%018d", rounded.second.getAttoSeconds()), 0, fractionDigits);
622             }
623             return builder.toString();
624         } else if (second.isNaN()) {
625             // special handling for NaN
626             return String.format("%02d:%02d:NaN", hour, minute);
627         } else if (second.isNegativeInfinity()) {
628             // special handling for -∞
629             return String.format("%02d:%02d:-∞", hour, minute);
630         } else {
631             // special handling for +∞
632             return String.format("%02d:%02d:+∞", hour, minute);
633         }
634 
635     }
636 
637     /**
638      * Get a string representation of the time without the offset from UTC.
639      *
640      * @return a string representation of the time in an ISO 8601 like format.
641      * @see #formatUtcOffset()
642      * @see #toString()
643      */
644     public String toStringWithoutUtcOffset() {
645         // create formats here as they are not thread safe
646         // Format for seconds to prevent rounding up to an invalid time. See #591
647         final String formatted = toStringWithoutUtcOffset(18);
648         int last = formatted.length() - 1;
649         while (last > 11 && formatted.charAt(last) == '0') {
650             // we want to remove final zeros (but keeping milliseconds for compatibility)
651             --last;
652         }
653         return formatted.substring(0, last + 1);
654     }
655 
656     /**
657      * Get the UTC offset as a string in ISO8601 format. For example, {@code +00:00}.
658      *
659      * @return the UTC offset as a string.
660      * @see #toStringWithoutUtcOffset()
661      * @see #toString()
662      */
663     public String formatUtcOffset() {
664         final int hourOffset = FastMath.abs(minutesFromUTC) / MINUTE;
665         final int minuteOffset = FastMath.abs(minutesFromUTC) % MINUTE;
666         return (minutesFromUTC < 0 ? '-' : '+') +
667                 String.format("%02d:%02d", hourOffset, minuteOffset);
668     }
669 
670     /**
671      * Get a string representation of the time including the offset from UTC.
672      *
673      * @return string representation of the time in an ISO 8601 like format including the
674      * UTC offset.
675      * @see #toStringWithoutUtcOffset()
676      * @see #formatUtcOffset()
677      */
678     public String toString() {
679         return toStringWithoutUtcOffset() + formatUtcOffset();
680     }
681 
682     /** {@inheritDoc} */
683     public int compareTo(final TimeComponents other) {
684         return getSplitSecondsInUTCDay().compareTo(other.getSplitSecondsInUTCDay());
685     }
686 
687     /** {@inheritDoc} */
688     public boolean equals(final Object other) {
689         try {
690             final TimeComponents otherTime = (TimeComponents) other;
691             return otherTime != null &&
692                    hour           == otherTime.hour   &&
693                    minute         == otherTime.minute &&
694                    second.compareTo(otherTime.second) == 0 &&
695                    minutesFromUTC == otherTime.minutesFromUTC;
696         } catch (ClassCastException cce) {
697             return false;
698         }
699     }
700 
701     /** {@inheritDoc} */
702     public int hashCode() {
703         return ((hour << 16) ^ ((minute - minutesFromUTC) << 8)) ^ second.hashCode();
704     }
705 
706 }