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

  21. import org.hipparchus.util.FastMath;
  22. import org.orekit.errors.OrekitIllegalArgumentException;
  23. import org.orekit.errors.OrekitMessages;
  24. import org.orekit.utils.Constants;


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

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

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

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

  43.     /** Wrapping limits for rounding to next minute.
  44.      * @since 13.0
  45.      */
  46.     private static final TimeOffset[] WRAPPING = new TimeOffset[] {
  47.         new TimeOffset(59L, 500000000000000000L), // round to second
  48.         new TimeOffset(59L, 950000000000000000L), // round to 10⁻¹ second
  49.         new TimeOffset(59L, 995000000000000000L), // round to 10⁻² second
  50.         new TimeOffset(59L, 999500000000000000L), // round to 10⁻³ second
  51.         new TimeOffset(59L, 999950000000000000L), // round to 10⁻⁴ second
  52.         new TimeOffset(59L, 999995000000000000L), // round to 10⁻⁵ second
  53.         new TimeOffset(59L, 999999500000000000L), // round to 10⁻⁶ second
  54.         new TimeOffset(59L, 999999950000000000L), // round to 10⁻⁷ second
  55.         new TimeOffset(59L, 999999995000000000L), // round to 10⁻⁸ second
  56.         new TimeOffset(59L, 999999999500000000L), // round to 10⁻⁹ second
  57.         new TimeOffset(59L, 999999999950000000L), // round to 10⁻¹⁰ second
  58.         new TimeOffset(59L, 999999999995000000L), // round to 10⁻¹¹ second
  59.         new TimeOffset(59L, 999999999999500000L), // round to 10⁻¹² second
  60.         new TimeOffset(59L, 999999999999950000L), // round to 10⁻¹³ second
  61.         new TimeOffset(59L, 999999999999995000L), // round to 10⁻¹⁴ second
  62.         new TimeOffset(59L, 999999999999999500L), // round to 10⁻¹⁵ second
  63.         new TimeOffset(59L, 999999999999999950L), // round to 10⁻¹⁶ second
  64.         new TimeOffset(59L, 999999999999999995L)  // round to 10⁻¹⁷ second
  65.     };

  66.     /** Offset values for rounding attoseconds.
  67.      * @since 13.0
  68.      */
  69.     // CHECKSTYLE: stop Indentation check */
  70.     private static final long[] ROUNDING = new long[] {
  71.         500000000000000000L, // round to second
  72.          50000000000000000L, // round to 10⁻¹ second
  73.           5000000000000000L, // round to 10⁻² second
  74.            500000000000000L, // round to 10⁻³ second
  75.             50000000000000L, // round to 10⁻⁴ second
  76.              5000000000000L, // round to 10⁻⁵ second
  77.               500000000000L, // round to 10⁻⁶ second
  78.                50000000000L, // round to 10⁻⁷ second
  79.                 5000000000L, // round to 10⁻⁸ second
  80.                  500000000L, // round to 10⁻⁹ second
  81.                   50000000L, // round to 10⁻¹⁰ second
  82.                    5000000L, // round to 10⁻¹¹ second
  83.                     500000L, // round to 10⁻¹² second
  84.                      50000L, // round to 10⁻¹³ second
  85.                       5000L, // round to 10⁻¹⁴ second
  86.                        500L, // round to 10⁻¹⁵ second
  87.                         50L, // round to 10⁻¹⁶ second
  88.                          5L, // round to 10⁻¹⁷ second
  89.                          0L, // round to 10⁻¹⁸ second
  90.     };
  91.     // CHECKSTYLE: resume Indentation check */

  92.     /** Serializable UID. */
  93.     private static final long serialVersionUID = 20240712L;

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

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

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

  100.     /** Constant for 23 hours. */
  101.     private static final int TWENTY_THREE = 23;

  102.     /** Constant for 59 minutes. */
  103.     private static final int FIFTY_NINE = 59;

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

  107.     /** Hour number. */
  108.     private final int hour;

  109.     /** Minute number. */
  110.     private final int minute;

  111.     /** Second number. */
  112.     private final TimeOffset second;

  113.     /** Offset between the specified date and UTC.
  114.      * <p>
  115.      * Always an integral number of minutes, as per ISO-8601 standard.
  116.      * </p>
  117.      * @since 7.2
  118.      */
  119.     private final int minutesFromUTC;

  120.     /** Build a time from its clock elements.
  121.      * <p>Note that seconds between 60.0 (inclusive) and 61.0 (exclusive) are allowed
  122.      * in this method, since they do occur during leap seconds introduction
  123.      * in the {@link UTCScale UTC} time scale.</p>
  124.      * @param hour hour number from 0 to 23
  125.      * @param minute minute number from 0 to 59
  126.      * @param second second number from 0.0 to 61.0 (excluded)
  127.      * @exception IllegalArgumentException if inconsistent arguments
  128.      * are given (parameters out of range)
  129.      */
  130.     public TimeComponents(final int hour, final int minute, final double second)
  131.         throws IllegalArgumentException {
  132.         this(hour, minute, new TimeOffset(second));
  133.     }

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

  149.     /** Build a time from its clock elements.
  150.      * <p>Note that seconds between 60.0 (inclusive) and 61.0 (exclusive) are allowed
  151.      * in this method, since they do occur during leap seconds introduction
  152.      * in the {@link UTCScale UTC} time scale.</p>
  153.      * @param hour hour number from 0 to 23
  154.      * @param minute minute number from 0 to 59
  155.      * @param second second number from 0.0 to 61.0 (excluded)
  156.      * @param minutesFromUTC offset between the specified date and UTC, as an
  157.      * integral number of minutes, as per ISO-8601 standard
  158.      * @exception IllegalArgumentException if inconsistent arguments
  159.      * are given (parameters out of range)
  160.      * @since 7.2
  161.      */
  162.     public TimeComponents(final int hour, final int minute, final double second, final int minutesFromUTC)
  163.         throws IllegalArgumentException {
  164.         this(hour, minute, new TimeOffset(second), minutesFromUTC);
  165.     }

  166.     /** Build a time from its clock elements.
  167.      * <p>Note that seconds between 60.0 (inclusive) and 61.0 (exclusive) are allowed
  168.      * in this method, since they do occur during leap seconds introduction
  169.      * in the {@link UTCScale UTC} time scale.</p>
  170.      * @param hour hour number from 0 to 23
  171.      * @param minute minute number from 0 to 59
  172.      * @param second second number from 0.0 to 62.0 (excluded, more than 61 s occurred on
  173.      *               the 1961 leap second, which was between 1 and 2 seconds in duration)
  174.      * @param minutesFromUTC offset between the specified date and UTC, as an
  175.      * integral number of minutes, as per ISO-8601 standard
  176.      * @exception IllegalArgumentException if inconsistent arguments
  177.      * are given (parameters out of range)
  178.      * @since 13.0
  179.      */
  180.     public TimeComponents(final int hour, final int minute, final TimeOffset second,
  181.                           final int minutesFromUTC)
  182.         throws IllegalArgumentException {

  183.         // range check
  184.         if (hour < 0 || hour > 23 ||
  185.             minute < 0 || minute > 59 ||
  186.             second.getSeconds() < 0L || second.getSeconds() >= 62L) {
  187.             throw new OrekitIllegalArgumentException(OrekitMessages.NON_EXISTENT_HMS_TIME,
  188.                                                      hour, minute, second.toDouble());
  189.         }

  190.         this.hour           = hour;
  191.         this.minute         = minute;
  192.         this.second         = second;
  193.         this.minutesFromUTC = minutesFromUTC;

  194.     }

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

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

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

  246.         this.hour           = tc.hour;
  247.         this.minute         = tc.minute;
  248.         this.second         = tc.second;
  249.         this.minutesFromUTC = tc.minutesFromUTC;

  250.     }

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

  293.         minutesFromUTC = 0;

  294.     }

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

  334.         minutesFromUTC = 0;

  335.         if (secondInDay.isNaN()) {
  336.             // special handling for NaN
  337.             hour   = 0;
  338.             minute = 0;
  339.             second = secondInDay;
  340.             return;
  341.         }

  342.         // range check
  343.         if (secondInDay.compareTo(TimeOffset.ZERO) < 0 || secondInDay.compareTo(TimeOffset.DAY) >= 0) {
  344.             throw new OrekitIllegalArgumentException(OrekitMessages.OUT_OF_RANGE_SECONDS_NUMBER_DETAIL,
  345.                                                      // this can produce some strange messages due to rounding
  346.                                                      secondInDay.toDouble(), 0, Constants.JULIAN_DAY);
  347.         }
  348.         final int maxExtraSeconds = minuteDuration - MINUTE;
  349.         if (leap.getSeconds() * maxExtraSeconds < 0 || FastMath.abs(leap.getSeconds()) > FastMath.abs(maxExtraSeconds)) {
  350.             throw new OrekitIllegalArgumentException(OrekitMessages.OUT_OF_RANGE_SECONDS_NUMBER_DETAIL,
  351.                                                      leap, 0, maxExtraSeconds);
  352.         }

  353.         // extract the time components
  354.         int wholeSeconds = (int) secondInDay.getSeconds();
  355.         hour           = wholeSeconds / HOUR;
  356.         wholeSeconds  -= HOUR * hour;
  357.         minute         = wholeSeconds / MINUTE;
  358.         wholeSeconds  -= MINUTE * minute;
  359.         // at this point ((minuteDuration - wholeSeconds) - leap) - fractional > 0
  360.         // or else one of the preconditions was violated. Even if there is no violation,
  361.         // naiveSecond may round to minuteDuration, creating an invalid time.
  362.         // In that case round down to preserve a valid time at the cost of up to 1as of error.
  363.         // See #676 and #681.
  364.         final TimeOffset naiveSecond = new TimeOffset(wholeSeconds, secondInDay.getAttoSeconds()).add(leap);
  365.         if (naiveSecond.compareTo(TimeOffset.ZERO) < 0) {
  366.             throw new OrekitIllegalArgumentException(
  367.                     OrekitMessages.OUT_OF_RANGE_SECONDS_NUMBER_DETAIL,
  368.                     naiveSecond, 0, minuteDuration);
  369.         }
  370.         if (naiveSecond.getSeconds() < minuteDuration) {
  371.             second = naiveSecond;
  372.         } else {
  373.             second = new TimeOffset(minuteDuration - 1, 999999999999999999L);
  374.         }

  375.     }

  376.     /** Parse a string in ISO-8601 format to build a time.
  377.      * <p>The supported formats are:
  378.      * <ul>
  379.      *   <li>basic and extended format local time: hhmmss, hh:mm:ss (with optional decimals in seconds)</li>
  380.      *   <li>optional UTC time: hhmmssZ, hh:mm:ssZ</li>
  381.      *   <li>optional signed hours UTC offset: hhmmss+HH, hhmmss-HH, hh:mm:ss+HH, hh:mm:ss-HH</li>
  382.      *   <li>optional signed basic hours and minutes UTC offset: hhmmss+HHMM, hhmmss-HHMM, hh:mm:ss+HHMM, hh:mm:ss-HHMM</li>
  383.      *   <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>
  384.      * </ul>
  385.      *
  386.      * <p> As shown by the list above, only the complete representations defined in section 4.2
  387.      * of ISO-8601 standard are supported, neither expended representations nor representations
  388.      * with reduced accuracy are supported.
  389.      *
  390.      * @param string string to parse
  391.      * @return a parsed time
  392.      * @exception IllegalArgumentException if string cannot be parsed
  393.      */
  394.     public static TimeComponents parseTime(final String string) {

  395.         // is the date a calendar date ?
  396.         final Matcher timeMatcher = ISO8601_FORMATS.matcher(string);
  397.         if (timeMatcher.matches()) {
  398.             final int        hour    = Integer.parseInt(timeMatcher.group(1));
  399.             final int        minute  = Integer.parseInt(timeMatcher.group(2));
  400.             final TimeOffset second  = timeMatcher.group(3) == null ?
  401.                                        TimeOffset.ZERO :
  402.                                        TimeOffset.parse(timeMatcher.group(3).replace(',', '.'));
  403.             final String     offset  = timeMatcher.group(4);
  404.             final int    minutesFromUTC;
  405.             if (offset == null) {
  406.                 // no offset from UTC is given
  407.                 minutesFromUTC = 0;
  408.             } else {
  409.                 // we need to parse an offset from UTC
  410.                 // the sign is mandatory and the ':' separator is optional
  411.                 // so we can have offsets given as -06:00 or +0100
  412.                 final int sign          = offset.codePointAt(0) == '-' ? -1 : +1;
  413.                 final int hourOffset    = Integer.parseInt(offset.substring(1, 3));
  414.                 final int minutesOffset = offset.length() <= 3 ? 0 : Integer.parseInt(offset.substring(offset.length() - 2));
  415.                 minutesFromUTC          = sign * (minutesOffset + MINUTE * hourOffset);
  416.             }
  417.             return new TimeComponents(hour, minute, second, minutesFromUTC);
  418.         }

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

  420.     }

  421.     /** Get the hour number.
  422.      * @return hour number from 0 to 23
  423.      */
  424.     public int getHour() {
  425.         return hour;
  426.     }

  427.     /** Get the minute number.
  428.      * @return minute minute number from 0 to 59
  429.      */
  430.     public int getMinute() {
  431.         return minute;
  432.     }

  433.     /** Get the seconds number.
  434.      * @return second second number from 0.0 to 61.0 (excluded). Note that 60 &le; second
  435.      * &lt; 61 only occurs during a leap second.
  436.      */
  437.     public double getSecond() {
  438.         return second.toDouble();
  439.     }

  440.     /** Get the seconds number.
  441.      * @return second second number from 0.0 to 61.0 (excluded). Note that 60 &le; second
  442.      * &lt; 61 only occurs during a leap second.
  443.      */
  444.     public TimeOffset getSplitSecond() {
  445.         return second;
  446.     }

  447.     /** Get the offset between the specified date and UTC.
  448.      * <p>
  449.      * The offset is always an integral number of minutes, as per ISO-8601 standard.
  450.      * </p>
  451.      * @return offset in minutes between the specified date and UTC
  452.      * @since 7.2
  453.      */
  454.     public int getMinutesFromUTC() {
  455.         return minutesFromUTC;
  456.     }

  457.     /** Get the second number within the local day, <em>without</em> applying the {@link #getMinutesFromUTC() offset from UTC}.
  458.      * @return second number from 0.0 to Constants.JULIAN_DAY
  459.      * @see #getSplitSecondsInLocalDay()
  460.      * @see #getSecondsInUTCDay()
  461.      * @since 7.2
  462.      */
  463.     public double getSecondsInLocalDay() {
  464.         return getSplitSecondsInLocalDay().toDouble();
  465.     }

  466.     /** Get the second number within the local day, <em>without</em> applying the {@link #getMinutesFromUTC() offset from UTC}.
  467.      * @return second number from 0.0 to Constants.JULIAN_DAY
  468.      * @see #getSecondsInLocalDay()
  469.      * @see #getSplitSecondsInUTCDay()
  470.      * @since 13.0
  471.      */
  472.     public TimeOffset getSplitSecondsInLocalDay() {
  473.         return new TimeOffset((long) MINUTE * minute + (long) HOUR * hour, 0L).add(second);
  474.     }

  475.     /** Get the second number within the UTC day, applying the {@link #getMinutesFromUTC() offset from UTC}.
  476.      * @return second number from {@link #getMinutesFromUTC() -getMinutesFromUTC()}
  477.      * to Constants.JULIAN_DAY {@link #getMinutesFromUTC() + getMinutesFromUTC()}
  478.      * @see #getSplitSecondsInUTCDay()
  479.      * @see #getSecondsInLocalDay()
  480.      * @since 7.2
  481.      */
  482.     public double getSecondsInUTCDay() {
  483.         return getSplitSecondsInUTCDay().toDouble();
  484.     }

  485.     /** Get the second number within the UTC day, applying the {@link #getMinutesFromUTC() offset from UTC}.
  486.      * @return second number from {@link #getMinutesFromUTC() -getMinutesFromUTC()}
  487.      * to Constants.JULIAN_DAY {@link #getMinutesFromUTC() + getMinutesFromUTC()}
  488.      * @see #getSecondsInUTCDay()
  489.      * @see #getSplitSecondsInLocalDay()
  490.      * @since 13.0
  491.      */
  492.     public TimeOffset getSplitSecondsInUTCDay() {
  493.         return new TimeOffset((long) MINUTE * (minute - minutesFromUTC) + (long) HOUR * hour, 0L).add(second);
  494.     }

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

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

  522.         if (wrappedSecond.compareTo(limit) >= 0) {
  523.             // we should wrap around to the next minute
  524.             int wrappedMinute = minute;
  525.             int wrappedHour   = hour;
  526.             wrappedSecond = TimeOffset.ZERO;
  527.             ++wrappedMinute;
  528.             if (wrappedMinute > 59) {
  529.                 wrappedMinute = 0;
  530.                 ++wrappedHour;
  531.                 if (wrappedHour > 23) {
  532.                     wrappedHour = 0;
  533.                 }
  534.             }
  535.             return new TimeComponents(wrappedHour, wrappedMinute, wrappedSecond);
  536.         }
  537.         return this;
  538.     }

  539.     /**
  540.      * Package private method that allows specification of seconds format. Allows access from
  541.      * {@link DateTimeComponents#toString(int, int)}. Access from outside of rounding methods would result in invalid
  542.      * times, see #590, #591.
  543.      *
  544.      * @param fractionDigits the number of digits to include after the decimal point in the string representation of the
  545.      *                       seconds. The date and time is first rounded as necessary. {@code fractionDigits} must be
  546.      *                       greater than or equal to {@code 0}.
  547.      * @return string without UTC offset.
  548.      * @since 13.0
  549.      */
  550.     String toStringWithoutUtcOffset(final int fractionDigits) {

  551.         if (second.isFinite()) {
  552.             // general case for regular times
  553.             final long      rounding = ROUNDING[FastMath.min(fractionDigits, ROUNDING.length - 1)];
  554.             final TimeComponents rounded  = new TimeComponents(hour, minute,
  555.                                                                new TimeOffset(second.getSeconds(),
  556.                                                                               second.getAttoSeconds() + rounding));
  557.             final StringBuilder builder = new StringBuilder();
  558.             builder.append(String.format("%02d:%02d:%02d",
  559.                                          rounded.hour, rounded.minute, rounded.second.getSeconds()));
  560.             if (fractionDigits > 0) {
  561.                 builder.append('.');
  562.                 builder.append(String.format("%018d", rounded.second.getAttoSeconds()), 0, fractionDigits);
  563.             }
  564.             return builder.toString();
  565.         } else if (second.isNaN()) {
  566.             // special handling for NaN
  567.             return String.format("%02d:%02d:NaN", hour, minute);
  568.         } else if (second.isNegativeInfinity()) {
  569.             // special handling for -∞
  570.             return String.format("%02d:%02d:-∞", hour, minute);
  571.         } else {
  572.             // special handling for +∞
  573.             return String.format("%02d:%02d:+∞", hour, minute);
  574.         }

  575.     }

  576.     /**
  577.      * Get a string representation of the time without the offset from UTC.
  578.      *
  579.      * @return a string representation of the time in an ISO 8601 like format.
  580.      * @see #formatUtcOffset()
  581.      * @see #toString()
  582.      */
  583.     public String toStringWithoutUtcOffset() {
  584.         // create formats here as they are not thread safe
  585.         // Format for seconds to prevent rounding up to an invalid time. See #591
  586.         final String formatted = toStringWithoutUtcOffset(18);
  587.         int last = formatted.length() - 1;
  588.         while (last > 11 && formatted.charAt(last) == '0') {
  589.             // we want to remove final zeros (but keeping milliseconds for compatibility)
  590.             --last;
  591.         }
  592.         return formatted.substring(0, last + 1);
  593.     }

  594.     /**
  595.      * Get the UTC offset as a string in ISO8601 format. For example, {@code +00:00}.
  596.      *
  597.      * @return the UTC offset as a string.
  598.      * @see #toStringWithoutUtcOffset()
  599.      * @see #toString()
  600.      */
  601.     public String formatUtcOffset() {
  602.         final int hourOffset = FastMath.abs(minutesFromUTC) / MINUTE;
  603.         final int minuteOffset = FastMath.abs(minutesFromUTC) % MINUTE;
  604.         return (minutesFromUTC < 0 ? '-' : '+') +
  605.                 String.format("%02d:%02d", hourOffset, minuteOffset);
  606.     }

  607.     /**
  608.      * Get a string representation of the time including the offset from UTC.
  609.      *
  610.      * @return string representation of the time in an ISO 8601 like format including the
  611.      * UTC offset.
  612.      * @see #toStringWithoutUtcOffset()
  613.      * @see #formatUtcOffset()
  614.      */
  615.     public String toString() {
  616.         return toStringWithoutUtcOffset() + formatUtcOffset();
  617.     }

  618.     /** {@inheritDoc} */
  619.     public int compareTo(final TimeComponents other) {
  620.         return getSplitSecondsInUTCDay().compareTo(other.getSplitSecondsInUTCDay());
  621.     }

  622.     /** {@inheritDoc} */
  623.     public boolean equals(final Object other) {
  624.         try {
  625.             final TimeComponents otherTime = (TimeComponents) other;
  626.             return otherTime != null &&
  627.                    hour           == otherTime.hour   &&
  628.                    minute         == otherTime.minute &&
  629.                    second.compareTo(otherTime.second) == 0 &&
  630.                    minutesFromUTC == otherTime.minutesFromUTC;
  631.         } catch (ClassCastException cce) {
  632.             return false;
  633.         }
  634.     }

  635.     /** {@inheritDoc} */
  636.     public int hashCode() {
  637.         return ((hour << 16) ^ ((minute - minutesFromUTC) << 8)) ^ second.hashCode();
  638.     }

  639. }