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