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