TimeComponents.java

  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. import java.io.IOException;
  19. import java.io.Serializable;
  20. import java.util.regex.Matcher;
  21. import java.util.regex.Pattern;

  22. import org.hipparchus.util.FastMath;
  23. import org.orekit.errors.OrekitIllegalArgumentException;
  24. import org.orekit.errors.OrekitInternalError;
  25. import org.orekit.errors.OrekitMessages;
  26. import org.orekit.utils.Constants;
  27. import org.orekit.utils.formatting.FastLongFormatter;


  28. /** Class representing a time within the day broken up as hour,
  29.  * minute and second components.
  30.  * <p>Instances of this class are guaranteed to be immutable.</p>
  31.  * @see DateComponents
  32.  * @see DateTimeComponents
  33.  * @author Luc Maisonobe
  34.  */
  35. public class TimeComponents implements Serializable, Comparable<TimeComponents> {

  36.     /** Constant for commonly used hour 00:00:00. */
  37.     public static final TimeComponents H00   = new TimeComponents(0, 0, TimeOffset.ZERO);

  38.     /** Constant for commonly used hour 12:00:00. */
  39.     public static final TimeComponents H12 = new TimeComponents(12, 0, TimeOffset.ZERO);

  40.     // CHECKSTYLE: stop ConstantName
  41.     /** Constant for NaN time.
  42.      * @since 13.0
  43.      */
  44.     public static final TimeComponents NaN   = new TimeComponents(0, 0, TimeOffset.NaN);
  45.     // CHECKSTYLE: resume ConstantName

  46.     /** Format for one 2 digits integer field. */
  47.     private static final FastLongFormatter PADDED_TWO_DIGITS_INTEGER = new FastLongFormatter(2, true);

  48.     /** Formatters for up to 18 digits integer field. */
  49.     private static final FastLongFormatter[] PADDED_FORMATTERS = new FastLongFormatter[] {
  50.         null,                            new FastLongFormatter( 1, true), new FastLongFormatter( 2, true),
  51.         new FastLongFormatter( 3, true), new FastLongFormatter( 4, true), new FastLongFormatter( 5, true),
  52.         new FastLongFormatter( 6, true), new FastLongFormatter( 7, true), new FastLongFormatter( 8, true),
  53.         new FastLongFormatter( 9, true), new FastLongFormatter(10, true), new FastLongFormatter(11, true),
  54.         new FastLongFormatter(12, true), new FastLongFormatter(13, true), new FastLongFormatter(14, true),
  55.         new FastLongFormatter(15, true), new FastLongFormatter(16, true), new FastLongFormatter(17, true),
  56.         new FastLongFormatter(18, true)
  57.     };

  58.     /** Scaling factors used for rounding. */
  59.     // CHECKSTYLE: stop Indentation check
  60.     private static final long[] SCALING = new long[] {
  61.        1000000000000000000L,
  62.         100000000000000000L,
  63.          10000000000000000L,
  64.           1000000000000000L,
  65.            100000000000000L,
  66.             10000000000000L,
  67.              1000000000000L,
  68.               100000000000L,
  69.                10000000000L,
  70.                 1000000000L,
  71.                  100000000L,
  72.                   10000000L,
  73.                    1000000L,
  74.                     100000L,
  75.                      10000L,
  76.                       1000L,
  77.                        100L,
  78.                         10L,
  79.                          1L
  80.     };
  81.     // CHECKSTYLE: resume Indentation check

  82.     /** Wrapping limits for rounding to next minute.
  83.      * @since 13.0
  84.      */
  85.     private static final TimeOffset[] WRAPPING = new TimeOffset[] {
  86.         new TimeOffset(59L, 500000000000000000L), // round to second
  87.         new TimeOffset(59L, 950000000000000000L), // round to 10⁻¹ second
  88.         new TimeOffset(59L, 995000000000000000L), // round to 10⁻² second
  89.         new TimeOffset(59L, 999500000000000000L), // round to 10⁻³ second
  90.         new TimeOffset(59L, 999950000000000000L), // round to 10⁻⁴ second
  91.         new TimeOffset(59L, 999995000000000000L), // round to 10⁻⁵ second
  92.         new TimeOffset(59L, 999999500000000000L), // round to 10⁻⁶ second
  93.         new TimeOffset(59L, 999999950000000000L), // round to 10⁻⁷ second
  94.         new TimeOffset(59L, 999999995000000000L), // round to 10⁻⁸ second
  95.         new TimeOffset(59L, 999999999500000000L), // round to 10⁻⁹ second
  96.         new TimeOffset(59L, 999999999950000000L), // round to 10⁻¹⁰ second
  97.         new TimeOffset(59L, 999999999995000000L), // round to 10⁻¹¹ second
  98.         new TimeOffset(59L, 999999999999500000L), // round to 10⁻¹² second
  99.         new TimeOffset(59L, 999999999999950000L), // round to 10⁻¹³ second
  100.         new TimeOffset(59L, 999999999999995000L), // round to 10⁻¹⁴ second
  101.         new TimeOffset(59L, 999999999999999500L), // round to 10⁻¹⁵ second
  102.         new TimeOffset(59L, 999999999999999950L), // round to 10⁻¹⁶ second
  103.         new TimeOffset(59L, 999999999999999995L)  // round to 10⁻¹⁷ second
  104.     };

  105.     /** Serializable UID. */
  106.     private static final long serialVersionUID = 20240712L;

  107.     /** Basic and extends formats for local time, with optional timezone. */
  108.     private static final Pattern ISO8601_FORMATS = Pattern.compile("^(\\d\\d):?(\\d\\d):?(\\d\\d(?:[.,]\\d+)?)?(?:Z|([-+]\\d\\d(?::?\\d\\d)?))?$");

  109.     /** Number of seconds in one hour. */
  110.     private static final int HOUR = 3600;

  111.     /** Number of seconds in one minute. */
  112.     private static final int MINUTE = 60;

  113.     /** Constant for 23 hours. */
  114.     private static final int TWENTY_THREE = 23;

  115.     /** Constant for 59 minutes. */
  116.     private static final int FIFTY_NINE = 59;

  117.     /** Constant for 23:59. */
  118.     private static final TimeOffset TWENTY_THREE_FIFTY_NINE =
  119.         new TimeOffset(TWENTY_THREE * HOUR + FIFTY_NINE * MINUTE, 0L);

  120.     /** Hour number. */
  121.     private final int hour;

  122.     /** Minute number. */
  123.     private final int minute;

  124.     /** Second number. */
  125.     private final TimeOffset second;

  126.     /** Offset between the specified date and UTC.
  127.      * <p>
  128.      * Always an integral number of minutes, as per ISO-8601 standard.
  129.      * </p>
  130.      * @since 7.2
  131.      */
  132.     private final int minutesFromUTC;

  133.     /** Build a time from its clock elements.
  134.      * <p>Note that seconds between 60.0 (inclusive) and 61.0 (exclusive) are allowed
  135.      * in this method, since they do occur during leap seconds introduction
  136.      * in the {@link UTCScale UTC} time scale.</p>
  137.      * @param hour hour number from 0 to 23
  138.      * @param minute minute number from 0 to 59
  139.      * @param second second number from 0.0 to 61.0 (excluded)
  140.      * @exception IllegalArgumentException if inconsistent arguments
  141.      * are given (parameters out of range)
  142.      */
  143.     public TimeComponents(final int hour, final int minute, final double second)
  144.         throws IllegalArgumentException {
  145.         this(hour, minute, new TimeOffset(second));
  146.     }

  147.     /** Build a time from its clock elements.
  148.      * <p>Note that seconds between 60.0 (inclusive) and 61.0 (exclusive) are allowed
  149.      * in this method, since they do occur during leap seconds introduction
  150.      * in the {@link UTCScale UTC} time scale.</p>
  151.      * @param hour hour number from 0 to 23
  152.      * @param minute minute number from 0 to 59
  153.      * @param second second number from 0.0 to 61.0 (excluded)
  154.      * @exception IllegalArgumentException if inconsistent arguments
  155.      * are given (parameters out of range)
  156.      * @since 13.0
  157.      */
  158.     public TimeComponents(final int hour, final int minute, final TimeOffset second)
  159.         throws IllegalArgumentException {
  160.         this(hour, minute, second, 0);
  161.     }

  162.     /** Build a time from its clock elements.
  163.      * <p>Note that seconds between 60.0 (inclusive) and 61.0 (exclusive) are allowed
  164.      * in this method, since they do occur during leap seconds introduction
  165.      * in the {@link UTCScale UTC} time scale.</p>
  166.      * @param hour hour number from 0 to 23
  167.      * @param minute minute number from 0 to 59
  168.      * @param second second number from 0.0 to 61.0 (excluded)
  169.      * @param minutesFromUTC offset between the specified date and UTC, as an
  170.      * integral number of minutes, as per ISO-8601 standard
  171.      * @exception IllegalArgumentException if inconsistent arguments
  172.      * are given (parameters out of range)
  173.      * @since 7.2
  174.      */
  175.     public TimeComponents(final int hour, final int minute, final double second, final int minutesFromUTC)
  176.         throws IllegalArgumentException {
  177.         this(hour, minute, new TimeOffset(second), minutesFromUTC);
  178.     }

  179.     /** Build a time from its clock elements.
  180.      * <p>Note that seconds between 60.0 (inclusive) and 61.0 (exclusive) are allowed
  181.      * in this method, since they do occur during leap seconds introduction
  182.      * in the {@link UTCScale UTC} time scale.</p>
  183.      * @param hour hour number from 0 to 23
  184.      * @param minute minute number from 0 to 59
  185.      * @param second second number from 0.0 to 62.0 (excluded, more than 61 s occurred on
  186.      *               the 1961 leap second, which was between 1 and 2 seconds in duration)
  187.      * @param minutesFromUTC offset between the specified date and UTC, as an
  188.      * integral number of minutes, as per ISO-8601 standard
  189.      * @exception IllegalArgumentException if inconsistent arguments
  190.      * are given (parameters out of range)
  191.      * @since 13.0
  192.      */
  193.     public TimeComponents(final int hour, final int minute, final TimeOffset second,
  194.                           final int minutesFromUTC)
  195.         throws IllegalArgumentException {

  196.         // range check
  197.         if (hour < 0 || hour > 23 ||
  198.             minute < 0 || minute > 59 ||
  199.             second.getSeconds() < 0L || second.getSeconds() >= 62L) {
  200.             throw new OrekitIllegalArgumentException(OrekitMessages.NON_EXISTENT_HMS_TIME,
  201.                                                      hour, minute, second.toDouble());
  202.         }

  203.         this.hour           = hour;
  204.         this.minute         = minute;
  205.         this.second         = second;
  206.         this.minutesFromUTC = minutesFromUTC;

  207.     }

  208.     /**
  209.      * Build a time from the second number within the day.
  210.      *
  211.      * <p>If the {@code secondInDay} is less than {@code 60.0} then {@link #getSecond()}
  212.      * and {@link #getSplitSecond()} will be less than {@code 60.0}, otherwise they will be
  213.      * less than {@code 61.0}. This constructor may produce an invalid value of
  214.      * {@link #getSecond()} and {@link #getSplitSecond()} during a negative leap second,
  215.      * through there has never been one. For more control over the number of seconds in
  216.      * the final minute use {@link #TimeComponents(TimeOffset, TimeOffset, int)}.
  217.      *
  218.      * <p>This constructor is always in UTC (i.e. {@link #getMinutesFromUTC() will return
  219.      * 0}).
  220.      *
  221.      * @param secondInDay second number from 0.0 to {@link Constants#JULIAN_DAY} {@code +
  222.      *                    1} (excluded)
  223.      * @throws OrekitIllegalArgumentException if seconds number is out of range
  224.      * @see #TimeComponents(TimeOffset, TimeOffset, int)
  225.      * @see #TimeComponents(int, double)
  226.      */
  227.     public TimeComponents(final double secondInDay)
  228.             throws OrekitIllegalArgumentException {
  229.         this(new TimeOffset(secondInDay));
  230.     }

  231.     /**
  232.      * Build a time from the second number within the day.
  233.      *
  234.      * <p>The second number is defined here as the sum
  235.      * {@code secondInDayA + secondInDayB} from 0.0 to {@link Constants#JULIAN_DAY}
  236.      * {@code + 1} (excluded). The two parameters are used for increased accuracy.
  237.      *
  238.      * <p>If the sum is less than {@code 60.0} then {@link #getSecond()} will be less
  239.      * than {@code 60.0}, otherwise it will be less than {@code 61.0}. This constructor
  240.      * may produce an invalid value of {@link #getSecond()} during a negative leap second,
  241.      * through there has never been one. For more control over the number of seconds in
  242.      * the final minute use {@link #TimeComponents(TimeOffset, TimeOffset, int)}.
  243.      *
  244.      * <p>This constructor is always in UTC (i.e. {@link #getMinutesFromUTC()} will
  245.      * return 0).
  246.      *
  247.      * @param secondInDayA first part of the second number
  248.      * @param secondInDayB last part of the second number
  249.      * @throws OrekitIllegalArgumentException if seconds number is out of range
  250.      * @see #TimeComponents(TimeOffset, TimeOffset, int)
  251.      */
  252.     public TimeComponents(final int secondInDayA, final double secondInDayB)
  253.             throws OrekitIllegalArgumentException {

  254.         // if the total is at least 86400 then assume there is a leap second
  255.         final TimeOffset aPlusB = new TimeOffset(secondInDayA).add(new TimeOffset(secondInDayB));
  256.         final TimeComponents tc     = aPlusB.compareTo(TimeOffset.DAY) >= 0 ?
  257.                                       new TimeComponents(aPlusB.subtract(TimeOffset.SECOND), TimeOffset.SECOND, 61) :
  258.                                       new TimeComponents(aPlusB, TimeOffset.ZERO, 60);

  259.         this.hour           = tc.hour;
  260.         this.minute         = tc.minute;
  261.         this.second         = tc.second;
  262.         this.minutesFromUTC = tc.minutesFromUTC;

  263.     }

  264.     /**
  265.      * Build a time from the second number within the day.
  266.      *
  267.      * <p>If the {@code secondInDay} is less than {@code 60.0} then {@link #getSecond()}
  268.      * will be less than {@code 60.0}, otherwise it will be less than {@code 61.0}. This constructor
  269.      * may produce an invalid value of {@link #getSecond()} during a negative leap second,
  270.      * through there has never been one. For more control over the number of seconds in
  271.      * the final minute use {@link #TimeComponents(TimeOffset, TimeOffset, int)}.
  272.      *
  273.      * <p>This constructor is always in UTC (i.e. {@link #getMinutesFromUTC() will return
  274.      * 0}).
  275.      *
  276.      * @param splitSecondInDay second number from 0.0 to {@link Constants#JULIAN_DAY} {@code +
  277.      *                    1} (excluded)
  278.      * @see #TimeComponents(TimeOffset, TimeOffset, int)
  279.      * @see #TimeComponents(int, double)
  280.      * @since 13.0
  281.      */
  282.     public TimeComponents(final TimeOffset splitSecondInDay) {
  283.         if (splitSecondInDay.compareTo(TimeOffset.ZERO) < 0) {
  284.             // negative time
  285.             throw new OrekitIllegalArgumentException(OrekitMessages.OUT_OF_RANGE_SECONDS_NUMBER_DETAIL,
  286.                                                      splitSecondInDay.toDouble(),
  287.                                                      0, TimeOffset.DAY_WITH_POSITIVE_LEAP.getSeconds());
  288.         } else if (splitSecondInDay.compareTo(TimeOffset.DAY) >= 0) {
  289.             // if the total is at least 86400 then assume there is a leap second
  290.             if (splitSecondInDay.compareTo(TimeOffset.DAY_WITH_POSITIVE_LEAP) >= 0) {
  291.                 // more than one leap second is too much
  292.                 throw new OrekitIllegalArgumentException(OrekitMessages.OUT_OF_RANGE_SECONDS_NUMBER_DETAIL,
  293.                                                          splitSecondInDay.toDouble(),
  294.                                                          0, TimeOffset.DAY_WITH_POSITIVE_LEAP.getSeconds());
  295.             } else {
  296.                 hour   = TWENTY_THREE;
  297.                 minute = FIFTY_NINE;
  298.                 second = splitSecondInDay.subtract(TWENTY_THREE_FIFTY_NINE);
  299.             }
  300.         } else {
  301.             // regular time within day
  302.             hour   = (int) splitSecondInDay.getSeconds() / HOUR;
  303.             minute = ((int) splitSecondInDay.getSeconds() % HOUR) / MINUTE;
  304.             second = splitSecondInDay.subtract(new TimeOffset(hour * HOUR + minute * MINUTE, 0L));
  305.         }

  306.         minutesFromUTC = 0;

  307.     }

  308.     /**
  309.      * Build a time from the second number within the day.
  310.      *
  311.      * <p>The seconds past midnight is the sum {@code secondInDay + leap}. Only the part
  312.      * {@code secondInDay} is used to compute the hours and minutes. The second parameter
  313.      * ({@code leap}) is added directly to the second value ({@link #getSecond()}) to
  314.      * implement leap seconds. These two quantities must satisfy the following constraints.
  315.      * This first guarantees the hour and minute are valid, the second guarantees the second
  316.      * is valid.
  317.      *
  318.      * <pre>
  319.      *     {@code 0 <= secondInDay < 86400}
  320.      *     {@code 0 <= secondInDay % 60 + leap <= minuteDuration}
  321.      *     {@code 0 <= leap <= minuteDuration - 60 if minuteDuration >= 60}
  322.      *     {@code 0 >= leap >= minuteDuration - 60 if minuteDuration <  60}
  323.      * </pre>
  324.      *
  325.      * <p>If the seconds of minute ({@link #getSecond()}) computed from {@code
  326.      * secondInDay + leap} is greater than or equal to {@code 60 + leap}
  327.      * then the second of minute will be set to {@code FastMath.nextDown(60 + leap)}. This
  328.      * prevents rounding to an invalid seconds of minute number when the input values have
  329.      * greater precision than a {@code double}.
  330.      *
  331.      * <p>This constructor is always in UTC (i.e. {@link #getMinutesFromUTC() will return
  332.      * 0}).
  333.      *
  334.      * <p>If {@code secondsInDay} or {@code leap} is NaN then the hour and minute will
  335.      * be set arbitrarily and the second of minute will be NaN.
  336.      *
  337.      * @param secondInDay    part of the second number.
  338.      * @param leap           magnitude of the leap second if this point in time is during
  339.      *                       a leap second, otherwise {@code 0.0}. This value is not used
  340.      *                       to compute hours and minutes, but it is added to the computed
  341.      *                       second of minute.
  342.      * @param minuteDuration number of seconds in the current minute, normally {@code 60}.
  343.      * @throws OrekitIllegalArgumentException if the inequalities above do not hold.
  344.      * @since 10.2
  345.      */
  346.     public TimeComponents(final TimeOffset secondInDay, final TimeOffset leap, final int minuteDuration) {

  347.         minutesFromUTC = 0;

  348.         if (secondInDay.isNaN()) {
  349.             // special handling for NaN
  350.             hour   = 0;
  351.             minute = 0;
  352.             second = secondInDay;
  353.             return;
  354.         }

  355.         // range check
  356.         if (secondInDay.compareTo(TimeOffset.ZERO) < 0 || secondInDay.compareTo(TimeOffset.DAY) >= 0) {
  357.             throw new OrekitIllegalArgumentException(OrekitMessages.OUT_OF_RANGE_SECONDS_NUMBER_DETAIL,
  358.                                                      // this can produce some strange messages due to rounding
  359.                                                      secondInDay.toDouble(), 0, Constants.JULIAN_DAY);
  360.         }
  361.         final int maxExtraSeconds = minuteDuration - MINUTE;
  362.         if (leap.getSeconds() * maxExtraSeconds < 0 || FastMath.abs(leap.getSeconds()) > FastMath.abs(maxExtraSeconds)) {
  363.             throw new OrekitIllegalArgumentException(OrekitMessages.OUT_OF_RANGE_SECONDS_NUMBER_DETAIL,
  364.                                                      leap, 0, maxExtraSeconds);
  365.         }

  366.         // extract the time components
  367.         int wholeSeconds = (int) secondInDay.getSeconds();
  368.         hour           = wholeSeconds / HOUR;
  369.         wholeSeconds  -= HOUR * hour;
  370.         minute         = wholeSeconds / MINUTE;
  371.         wholeSeconds  -= MINUTE * minute;
  372.         // at this point ((minuteDuration - wholeSeconds) - leap) - fractional > 0
  373.         // or else one of the preconditions was violated. Even if there is no violation,
  374.         // naiveSecond may round to minuteDuration, creating an invalid time.
  375.         // In that case round down to preserve a valid time at the cost of up to 1as of error.
  376.         // See #676 and #681.
  377.         final TimeOffset naiveSecond = new TimeOffset(wholeSeconds, secondInDay.getAttoSeconds()).add(leap);
  378.         if (naiveSecond.compareTo(TimeOffset.ZERO) < 0) {
  379.             throw new OrekitIllegalArgumentException(
  380.                     OrekitMessages.OUT_OF_RANGE_SECONDS_NUMBER_DETAIL,
  381.                     naiveSecond, 0, minuteDuration);
  382.         }
  383.         if (naiveSecond.getSeconds() < minuteDuration) {
  384.             second = naiveSecond;
  385.         } else {
  386.             second = new TimeOffset(minuteDuration - 1, 999999999999999999L);
  387.         }

  388.     }

  389.     /** Parse a string in ISO-8601 format to build a time.
  390.      * <p>The supported formats are:
  391.      * <ul>
  392.      *   <li>basic and extended format local time: hhmmss, hh:mm:ss (with optional decimals in seconds)</li>
  393.      *   <li>optional UTC time: hhmmssZ, hh:mm:ssZ</li>
  394.      *   <li>optional signed hours UTC offset: hhmmss+HH, hhmmss-HH, hh:mm:ss+HH, hh:mm:ss-HH</li>
  395.      *   <li>optional signed basic hours and minutes UTC offset: hhmmss+HHMM, hhmmss-HHMM, hh:mm:ss+HHMM, hh:mm:ss-HHMM</li>
  396.      *   <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>
  397.      * </ul>
  398.      *
  399.      * <p> As shown by the list above, only the complete representations defined in section 4.2
  400.      * of ISO-8601 standard are supported, neither expended representations nor representations
  401.      * with reduced accuracy are supported.
  402.      *
  403.      * @param string string to parse
  404.      * @return a parsed time
  405.      * @exception IllegalArgumentException if string cannot be parsed
  406.      */
  407.     public static TimeComponents parseTime(final String string) {

  408.         // is the date a calendar date ?
  409.         final Matcher timeMatcher = ISO8601_FORMATS.matcher(string);
  410.         if (timeMatcher.matches()) {
  411.             final int        hour    = Integer.parseInt(timeMatcher.group(1));
  412.             final int        minute  = Integer.parseInt(timeMatcher.group(2));
  413.             final TimeOffset second  = timeMatcher.group(3) == null ?
  414.                                        TimeOffset.ZERO :
  415.                                        TimeOffset.parse(timeMatcher.group(3).replace(',', '.'));
  416.             final String     offset  = timeMatcher.group(4);
  417.             final int    minutesFromUTC;
  418.             if (offset == null) {
  419.                 // no offset from UTC is given
  420.                 minutesFromUTC = 0;
  421.             } else {
  422.                 // we need to parse an offset from UTC
  423.                 // the sign is mandatory and the ':' separator is optional
  424.                 // so we can have offsets given as -06:00 or +0100
  425.                 final int sign          = offset.codePointAt(0) == '-' ? -1 : +1;
  426.                 final int hourOffset    = Integer.parseInt(offset.substring(1, 3));
  427.                 final int minutesOffset = offset.length() <= 3 ? 0 : Integer.parseInt(offset.substring(offset.length() - 2));
  428.                 minutesFromUTC          = sign * (minutesOffset + MINUTE * hourOffset);
  429.             }
  430.             return new TimeComponents(hour, minute, second, minutesFromUTC);
  431.         }

  432.         throw new OrekitIllegalArgumentException(OrekitMessages.NON_EXISTENT_TIME, string);

  433.     }

  434.     /** Get the hour number.
  435.      * @return hour number from 0 to 23
  436.      */
  437.     public int getHour() {
  438.         return hour;
  439.     }

  440.     /** Get the minute number.
  441.      * @return minute minute number from 0 to 59
  442.      */
  443.     public int getMinute() {
  444.         return minute;
  445.     }

  446.     /** Get the seconds number.
  447.      * @return second second number from 0.0 to 61.0 (excluded). Note that 60 &le; second
  448.      * &lt; 61 only occurs during a leap second.
  449.      */
  450.     public double getSecond() {
  451.         return second.toDouble();
  452.     }

  453.     /** Get the seconds number.
  454.      * @return second second number from 0.0 to 61.0 (excluded). Note that 60 &le; second
  455.      * &lt; 61 only occurs during a leap second.
  456.      */
  457.     public TimeOffset getSplitSecond() {
  458.         return second;
  459.     }

  460.     /** Get the offset between the specified date and UTC.
  461.      * <p>
  462.      * The offset is always an integral number of minutes, as per ISO-8601 standard.
  463.      * </p>
  464.      * @return offset in minutes between the specified date and UTC
  465.      * @since 7.2
  466.      */
  467.     public int getMinutesFromUTC() {
  468.         return minutesFromUTC;
  469.     }

  470.     /** Get the second number within the local day, <em>without</em> applying the {@link #getMinutesFromUTC() offset from UTC}.
  471.      * @return second number from 0.0 to Constants.JULIAN_DAY
  472.      * @see #getSplitSecondsInLocalDay()
  473.      * @see #getSecondsInUTCDay()
  474.      * @since 7.2
  475.      */
  476.     public double getSecondsInLocalDay() {
  477.         return getSplitSecondsInLocalDay().toDouble();
  478.     }

  479.     /** Get the second number within the local day, <em>without</em> applying the {@link #getMinutesFromUTC() offset from UTC}.
  480.      * @return second number from 0.0 to Constants.JULIAN_DAY
  481.      * @see #getSecondsInLocalDay()
  482.      * @see #getSplitSecondsInUTCDay()
  483.      * @since 13.0
  484.      */
  485.     public TimeOffset getSplitSecondsInLocalDay() {
  486.         return new TimeOffset((long) MINUTE * minute + (long) HOUR * hour, 0L).add(second);
  487.     }

  488.     /** Get the second number within the UTC day, applying the {@link #getMinutesFromUTC() offset from UTC}.
  489.      * @return second number from {@link #getMinutesFromUTC() -getMinutesFromUTC()}
  490.      * to Constants.JULIAN_DAY {@link #getMinutesFromUTC() + getMinutesFromUTC()}
  491.      * @see #getSplitSecondsInUTCDay()
  492.      * @see #getSecondsInLocalDay()
  493.      * @since 7.2
  494.      */
  495.     public double getSecondsInUTCDay() {
  496.         return getSplitSecondsInUTCDay().toDouble();
  497.     }

  498.     /** Get the second number within the UTC day, applying the {@link #getMinutesFromUTC() offset from UTC}.
  499.      * @return second number from {@link #getMinutesFromUTC() -getMinutesFromUTC()}
  500.      * to Constants.JULIAN_DAY {@link #getMinutesFromUTC() + getMinutesFromUTC()}
  501.      * @see #getSecondsInUTCDay()
  502.      * @see #getSplitSecondsInLocalDay()
  503.      * @since 13.0
  504.      */
  505.     public TimeOffset getSplitSecondsInUTCDay() {
  506.         return new TimeOffset((long) MINUTE * (minute - minutesFromUTC) + (long) HOUR * hour, 0L).add(second);
  507.     }

  508.     /**
  509.      * Round this time to the given precision if needed to prevent rounding up to an
  510.      * invalid seconds number. This is useful, for example, when writing custom date-time
  511.      * formatting methods so one does not, e.g., end up with "60.0" seconds during a
  512.      * normal minute when the value of seconds is {@code 59.999}. This method will instead
  513.      * round up the minute, hour, day, month, and year as needed.
  514.      *
  515.      * @param minuteDuration 59, 60, 61, or 62 seconds depending on the date being close
  516.      *                       to a leap second introduction and the magnitude of the leap
  517.      *                       second.
  518.      * @param fractionDigits the number of decimal digits after the decimal point in the
  519.      *                       seconds number that will be printed. This date-time is
  520.      *                       rounded to {@code fractionDigits} after the decimal point if
  521.      *                       necessary to prevent rounding up to {@code minuteDuration}.
  522.      *                       {@code fractionDigits} must be greater than or equal to
  523.      *                       {@code 0}.
  524.      * @return the instance itself if no rounding was needed, or a time within
  525.      * {@code 0.5 * 10**-fractionDigits} seconds of this, and with a seconds number that
  526.      * will not round up to {@code minuteDuration} when rounded to {@code fractionDigits}
  527.      * after the decimal point
  528.      * @since 13.0
  529.      */
  530.     public TimeComponents wrapIfNeeded(final int minuteDuration, final int fractionDigits) {
  531.         TimeOffset wrappedSecond = second;

  532.         // adjust limit according to current minute duration
  533.         final TimeOffset limit = WRAPPING[FastMath.min(fractionDigits, WRAPPING.length - 1)].
  534.                                 add(new TimeOffset(minuteDuration - 60, 0L));

  535.         if (wrappedSecond.compareTo(limit) >= 0) {
  536.             // we should wrap around to the next minute
  537.             int wrappedMinute = minute;
  538.             int wrappedHour   = hour;
  539.             wrappedSecond = TimeOffset.ZERO;
  540.             ++wrappedMinute;
  541.             if (wrappedMinute > 59) {
  542.                 wrappedMinute = 0;
  543.                 ++wrappedHour;
  544.                 if (wrappedHour > 23) {
  545.                     wrappedHour = 0;
  546.                 }
  547.             }
  548.             return new TimeComponents(wrappedHour, wrappedMinute, wrappedSecond);
  549.         }
  550.         return this;
  551.     }

  552.     /**
  553.      * Package private method that allows specification of seconds format. Allows access from
  554.      * {@link DateTimeComponents#toString(int, int)}. Access from outside of rounding methods would result in invalid
  555.      * times, see #590, #591.
  556.      *
  557.      * @param fractionDigits the number of digits to include after the decimal point in the string representation of the
  558.      *                       seconds. The date and time are first rounded as necessary. {@code fractionDigits} must be
  559.      *                       greater than or equal to {@code 0}.
  560.      * @return string without UTC offset.
  561.      * @since 13.0
  562.      */
  563.     String toStringWithoutUtcOffset(final int fractionDigits) {

  564.         try {
  565.             final StringBuilder builder = new StringBuilder();
  566.             if (second.isFinite()) {
  567.                 // general case for regular times
  568.                 final TimeComponents rounded = new TimeComponents(hour, minute, second.getRoundedOffset(fractionDigits));
  569.                 PADDED_TWO_DIGITS_INTEGER.appendTo(builder, rounded.hour);
  570.                 builder.append(':');
  571.                 PADDED_TWO_DIGITS_INTEGER.appendTo(builder, rounded.minute);
  572.                 builder.append(':');
  573.                 PADDED_TWO_DIGITS_INTEGER.appendTo(builder, rounded.second.getSeconds());
  574.                 if (fractionDigits > 0) {
  575.                     builder.append('.');
  576.                     final int index = FastMath.min(PADDED_FORMATTERS.length - 1, fractionDigits);
  577.                     PADDED_FORMATTERS[index].appendTo(builder, rounded.second.getAttoSeconds() / SCALING[index]);
  578.                 }

  579.             } else {
  580.                 PADDED_TWO_DIGITS_INTEGER.appendTo(builder, hour);
  581.                 builder.append(':');
  582.                 PADDED_TWO_DIGITS_INTEGER.appendTo(builder, minute);
  583.                 builder.append(":NaN"); // ±∞ can never happen
  584.             }

  585.             return builder.toString();

  586.         } catch (IOException ioe) {
  587.             // this should never happen
  588.             throw new OrekitInternalError(ioe);
  589.         }

  590.     }

  591.     /**
  592.      * Get a string representation of the time without the offset from UTC.
  593.      *
  594.      * @return a string representation of the time in an ISO 8601 like format.
  595.      * @see #formatUtcOffset()
  596.      * @see #toString()
  597.      */
  598.     public String toStringWithoutUtcOffset() {
  599.         // create formats here as they are not thread safe
  600.         // Format for seconds to prevent rounding up to an invalid time. See #591
  601.         final String formatted = toStringWithoutUtcOffset(18);
  602.         int last = formatted.length() - 1;
  603.         while (last > 11 && formatted.charAt(last) == '0') {
  604.             // we want to remove final zeros (but keeping milliseconds for compatibility)
  605.             --last;
  606.         }
  607.         return formatted.substring(0, last + 1);
  608.     }

  609.     /**
  610.      * Get the UTC offset as a string in ISO8601 format. For example, {@code +00:00}.
  611.      *
  612.      * @return the UTC offset as a string.
  613.      * @see #toStringWithoutUtcOffset()
  614.      * @see #toString()
  615.      */
  616.     public String formatUtcOffset() {
  617.         try {
  618.             final int           hourOffset   = FastMath.abs(minutesFromUTC) / MINUTE;
  619.             final int           minuteOffset = FastMath.abs(minutesFromUTC) % MINUTE;
  620.             final StringBuilder builder      = new StringBuilder();
  621.             builder.append(minutesFromUTC < 0 ? '-' : '+');
  622.             PADDED_TWO_DIGITS_INTEGER.appendTo(builder, hourOffset);
  623.             builder.append(':');
  624.             PADDED_TWO_DIGITS_INTEGER.appendTo(builder, minuteOffset);
  625.             return builder.toString();
  626.         }
  627.         catch (IOException ioe) {
  628.             // this should never happen
  629.             throw new OrekitInternalError(ioe);
  630.         }
  631.     }

  632.     /**
  633.      * Get a string representation of the time including the offset from UTC.
  634.      *
  635.      * @return string representation of the time in an ISO 8601 like format including the
  636.      * UTC offset.
  637.      * @see #toStringWithoutUtcOffset()
  638.      * @see #formatUtcOffset()
  639.      */
  640.     public String toString() {
  641.         return toStringWithoutUtcOffset() + formatUtcOffset();
  642.     }

  643.     /** {@inheritDoc} */
  644.     public int compareTo(final TimeComponents other) {
  645.         return getSplitSecondsInUTCDay().compareTo(other.getSplitSecondsInUTCDay());
  646.     }

  647.     /** {@inheritDoc} */
  648.     public boolean equals(final Object other) {
  649.         try {
  650.             final TimeComponents otherTime = (TimeComponents) other;
  651.             return otherTime != null &&
  652.                    hour           == otherTime.hour   &&
  653.                    minute         == otherTime.minute &&
  654.                    second.compareTo(otherTime.second) == 0 &&
  655.                    minutesFromUTC == otherTime.minutesFromUTC;
  656.         } catch (ClassCastException cce) {
  657.             return false;
  658.         }
  659.     }

  660.     /** {@inheritDoc} */
  661.     public int hashCode() {
  662.         return ((hour << 16) ^ ((minute - minutesFromUTC) << 8)) ^ second.hashCode();
  663.     }

  664. }