TimeComponents.java

  1. /* Copyright 2002-2022 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.text.DecimalFormat;
  20. import java.text.DecimalFormatSymbols;
  21. import java.util.Locale;
  22. import java.util.regex.Matcher;
  23. import java.util.regex.Pattern;

  24. import org.hipparchus.util.FastMath;
  25. import org.orekit.errors.OrekitIllegalArgumentException;
  26. import org.orekit.errors.OrekitMessages;
  27. import org.orekit.utils.Constants;


  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, 0);

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

  40.     /** Serializable UID. */
  41.     private static final long serialVersionUID = 20160331L;

  42.     /** Formatting symbols used in {@link #toString()}. */
  43.     private static final DecimalFormatSymbols US_SYMBOLS =
  44.             new DecimalFormatSymbols(Locale.US);

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

  47.     /** Hour number. */
  48.     private final int hour;

  49.     /** Minute number. */
  50.     private final int minute;

  51.     /** Second number. */
  52.     private final double second;

  53.     /** Offset between the specified date and UTC.
  54.      * <p>
  55.      * Always an integral number of minutes, as per ISO-8601 standard.
  56.      * </p>
  57.      * @since 7.2
  58.      */
  59.     private final int minutesFromUTC;

  60.     /** Build a time from its clock elements.
  61.      * <p>Note that seconds between 60.0 (inclusive) and 61.0 (exclusive) are allowed
  62.      * in this method, since they do occur during leap seconds introduction
  63.      * in the {@link UTCScale UTC} time scale.</p>
  64.      * @param hour hour number from 0 to 23
  65.      * @param minute minute number from 0 to 59
  66.      * @param second second number from 0.0 to 61.0 (excluded)
  67.      * @exception IllegalArgumentException if inconsistent arguments
  68.      * are given (parameters out of range)
  69.      */
  70.     public TimeComponents(final int hour, final int minute, final double second)
  71.         throws IllegalArgumentException {
  72.         this(hour, minute, second, 0);
  73.     }

  74.     /** Build a time from its clock elements.
  75.      * <p>Note that seconds between 60.0 (inclusive) and 61.0 (exclusive) are allowed
  76.      * in this method, since they do occur during leap seconds introduction
  77.      * in the {@link UTCScale UTC} time scale.</p>
  78.      * @param hour hour number from 0 to 23
  79.      * @param minute minute number from 0 to 59
  80.      * @param second second number from 0.0 to 61.0 (excluded)
  81.      * @param minutesFromUTC offset between the specified date and UTC, as an
  82.      * integral number of minutes, as per ISO-8601 standard
  83.      * @exception IllegalArgumentException if inconsistent arguments
  84.      * are given (parameters out of range)
  85.      * @since 7.2
  86.      */
  87.     public TimeComponents(final int hour, final int minute, final double second,
  88.                           final int minutesFromUTC)
  89.         throws IllegalArgumentException {

  90.         // range check
  91.         if (hour < 0 || hour > 23 ||
  92.             minute < 0 || minute > 59 ||
  93.             second < 0 || second >= 61.0) {
  94.             throw new OrekitIllegalArgumentException(OrekitMessages.NON_EXISTENT_HMS_TIME,
  95.                                                      hour, minute, second);
  96.         }

  97.         this.hour           = hour;
  98.         this.minute         = minute;
  99.         this.second         = second;
  100.         this.minutesFromUTC = minutesFromUTC;

  101.     }

  102.     /**
  103.      * Build a time from the second number within the day.
  104.      *
  105.      * <p>If the {@code secondInDay} is less than {@code 60.0} then {@link #getSecond()}
  106.      * will be less than {@code 60.0}, otherwise it will be less than {@code 61.0}. This constructor
  107.      * may produce an invalid value of {@link #getSecond()} during a negative leap second,
  108.      * through there has never been one. For more control over the number of seconds in
  109.      * the final minute use {@link #fromSeconds(int, double, double, int)}.
  110.      *
  111.      * <p>This constructor is always in UTC (i.e. {@link #getMinutesFromUTC() will return
  112.      * 0}).
  113.      *
  114.      * @param secondInDay second number from 0.0 to {@link Constants#JULIAN_DAY} {@code +
  115.      *                    1} (excluded)
  116.      * @throws OrekitIllegalArgumentException if seconds number is out of range
  117.      * @see #fromSeconds(int, double, double, int)
  118.      * @see #TimeComponents(int, double)
  119.      */
  120.     public TimeComponents(final double secondInDay)
  121.             throws OrekitIllegalArgumentException {
  122.         this(0, secondInDay);
  123.     }

  124.     /**
  125.      * Build a time from the second number within the day.
  126.      *
  127.      * <p>The second number is defined here as the sum
  128.      * {@code secondInDayA + secondInDayB} from 0.0 to {@link Constants#JULIAN_DAY}
  129.      * {@code + 1} (excluded). The two parameters are used for increased accuracy.
  130.      *
  131.      * <p>If the sum is less than {@code 60.0} then {@link #getSecond()} will be less
  132.      * than {@code 60.0}, otherwise it will be less than {@code 61.0}. This constructor
  133.      * may produce an invalid value of {@link #getSecond()} during a negative leap second,
  134.      * through there has never been one. For more control over the number of seconds in
  135.      * the final minute use {@link #fromSeconds(int, double, double, int)}.
  136.      *
  137.      * <p>This constructor is always in UTC (i.e. {@link #getMinutesFromUTC()} will
  138.      * return 0).
  139.      *
  140.      * @param secondInDayA first part of the second number
  141.      * @param secondInDayB last part of the second number
  142.      * @throws OrekitIllegalArgumentException if seconds number is out of range
  143.      * @see #fromSeconds(int, double, double, int)
  144.      */
  145.     public TimeComponents(final int secondInDayA, final double secondInDayB)
  146.             throws OrekitIllegalArgumentException {
  147.         // if the total is at least 86400 then assume there is a leap second
  148.         this(
  149.                 (Constants.JULIAN_DAY - secondInDayA) - secondInDayB > 0 ? secondInDayA : secondInDayA - 1,
  150.                 secondInDayB,
  151.                 (Constants.JULIAN_DAY - secondInDayA) - secondInDayB > 0 ? 0 : 1,
  152.                 (Constants.JULIAN_DAY - secondInDayA) - secondInDayB > 0 ? 60 : 61);
  153.     }

  154.     /**
  155.      * Build a time from the second number within the day.
  156.      *
  157.      * <p>The seconds past midnight is the sum {@code secondInDayA + secondInDayB +
  158.      * leap}. The two parameters are used for increased accuracy. Only the first part of
  159.      * the sum ({@code secondInDayA + secondInDayB}) is used to compute the hours and
  160.      * minutes. The third parameter ({@code leap}) is added directly to the second value
  161.      * ({@link #getSecond()}) to implement leap seconds. These three quantities must
  162.      * satisfy the following constraints. This first guarantees the hour and minute are
  163.      * valid, the second guarantees the second is valid.
  164.      *
  165.      * <pre>
  166.      *     {@code 0 <= secondInDayA + secondInDayB < 86400}
  167.      *     {@code 0 <= (secondInDayA + secondInDayB) % 60 + leap < minuteDuration}
  168.      *     {@code 0 <= leap <= minuteDuration - 60                        if minuteDuration >= 60}
  169.      *     {@code 0 >= leap >= minuteDuration - 60                        if minuteDuration <  60}
  170.      * </pre>
  171.      *
  172.      * <p>If the seconds of minute ({@link #getSecond()}) computed from {@code
  173.      * secondInDayA + secondInDayB + leap} is greater than or equal to {@code
  174.      * minuteDuration} then the second of minute will be set to {@code
  175.      * FastMath.nextDown(minuteDuration)}. This prevents rounding to an invalid seconds of
  176.      * minute number when the input values have greater precision than a {@code double}.
  177.      *
  178.      * <p>This constructor is always in UTC (i.e. {@link #getMinutesFromUTC() will return
  179.      * 0}).
  180.      *
  181.      * <p>If {@code secondsInDayB} or {@code leap} is NaN then the hour and minute will
  182.      * be determined from {@code secondInDayA} and the second of minute will be NaN.
  183.      *
  184.      * <p>This constructor is private to avoid confusion with the other constructors that
  185.      * would be caused by overloading. Use {@link #fromSeconds(int, double, double,
  186.      * int)}.
  187.      *
  188.      * @param secondInDayA   first part of the second number.
  189.      * @param secondInDayB   last part of the second number.
  190.      * @param leap           magnitude of the leap second if this point in time is during
  191.      *                       a leap second, otherwise {@code 0.0}. This value is not used
  192.      *                       to compute hours and minutes, but it is added to the computed
  193.      *                       second of minute.
  194.      * @param minuteDuration number of seconds in the current minute, normally {@code 60}.
  195.      * @throws OrekitIllegalArgumentException if the inequalities above do not hold.
  196.      * @see #fromSeconds(int, double, double, int)
  197.      * @since 10.2
  198.      */
  199.     private TimeComponents(final int secondInDayA,
  200.                            final double secondInDayB,
  201.                            final double leap,
  202.                            final int minuteDuration) throws OrekitIllegalArgumentException {

  203.         // split the numbers as a whole number of seconds
  204.         // and a fractional part between 0.0 (included) and 1.0 (excluded)
  205.         final int carry         = (int) FastMath.floor(secondInDayB);
  206.         int wholeSeconds        = secondInDayA + carry;
  207.         final double fractional = secondInDayB - carry;

  208.         // range check
  209.         if (wholeSeconds < 0 || wholeSeconds >= Constants.JULIAN_DAY) {
  210.             throw new OrekitIllegalArgumentException(
  211.                     OrekitMessages.OUT_OF_RANGE_SECONDS_NUMBER_DETAIL,
  212.                     // this can produce some strange messages due to rounding
  213.                     secondInDayA + secondInDayB,
  214.                     0,
  215.                     Constants.JULIAN_DAY);
  216.         }
  217.         final int maxExtraSeconds = minuteDuration - 60;
  218.         if (leap * maxExtraSeconds < 0 ||
  219.                 FastMath.abs(leap) > FastMath.abs(maxExtraSeconds)) {
  220.             throw new OrekitIllegalArgumentException(
  221.                     OrekitMessages.OUT_OF_RANGE_SECONDS_NUMBER_DETAIL,
  222.                     leap, 0, maxExtraSeconds);
  223.         }

  224.         // extract the time components
  225.         hour           = wholeSeconds / 3600;
  226.         wholeSeconds  -= 3600 * hour;
  227.         minute         = wholeSeconds / 60;
  228.         wholeSeconds  -= 60 * minute;
  229.         // at this point ((minuteDuration - wholeSeconds) - leap) - fractional > 0
  230.         // or else one of the preconditions was violated. Even if there is not violation,
  231.         // naiveSecond may round to minuteDuration, creating an invalid time.
  232.         // In that case round down to preserve a valid time at the cost of up to 1 ULP of error.
  233.         // See #676 and #681.
  234.         final double naiveSecond = wholeSeconds + (leap + fractional);
  235.         if (naiveSecond < 0) {
  236.             throw new OrekitIllegalArgumentException(
  237.                     OrekitMessages.OUT_OF_RANGE_SECONDS_NUMBER_DETAIL,
  238.                     naiveSecond, 0, minuteDuration);
  239.         }
  240.         if (naiveSecond < minuteDuration || Double.isNaN(naiveSecond)) {
  241.             second = naiveSecond;
  242.         } else {
  243.             second = FastMath.nextDown((double) minuteDuration);
  244.         }
  245.         minutesFromUTC = 0;

  246.     }

  247.     /**
  248.      * Build a time from the second number within the day.
  249.      *
  250.      * <p>The seconds past midnight is the sum {@code secondInDayA + secondInDayB +
  251.      * leap}. The two parameters are used for increased accuracy. Only the first part of
  252.      * the sum ({@code secondInDayA + secondInDayB}) is used to compute the hours and
  253.      * minutes. The third parameter ({@code leap}) is added directly to the second value
  254.      * ({@link #getSecond()}) to implement leap seconds. These three quantities must
  255.      * satisfy the following constraints. This first guarantees the hour and minute are
  256.      * valid, the second guarantees the second is valid.
  257.      *
  258.      * <pre>
  259.      *     {@code 0 <= secondInDayA + secondInDayB < 86400}
  260.      *     {@code 0 <= (secondInDayA + secondInDayB) % 60 + leap <= minuteDuration}
  261.      *     {@code 0 <= leap <= minuteDuration - 60                        if minuteDuration >= 60}
  262.      *     {@code 0 >= leap >= minuteDuration - 60                        if minuteDuration <  60}
  263.      * </pre>
  264.      *
  265.      * <p>If the seconds of minute ({@link #getSecond()}) computed from {@code
  266.      * secondInDayA + secondInDayB + leap} is greater than or equal to {@code 60 + leap}
  267.      * then the second of minute will be set to {@code FastMath.nextDown(60 + leap)}. This
  268.      * prevents rounding to an invalid seconds of minute number when the input values have
  269.      * greater precision than a {@code double}.
  270.      *
  271.      * <p>This constructor is always in UTC (i.e. {@link #getMinutesFromUTC() will return
  272.      * 0}).
  273.      *
  274.      * <p>If {@code secondsInDayB} or {@code leap} is NaN then the hour and minute will
  275.      * be determined from {@code secondInDayA} and the second of minute will be NaN.
  276.      *
  277.      * @param secondInDayA   first part of the second number.
  278.      * @param secondInDayB   last part of the second number.
  279.      * @param leap           magnitude of the leap second if this point in time is during
  280.      *                       a leap second, otherwise {@code 0.0}. This value is not used
  281.      *                       to compute hours and minutes, but it is added to the computed
  282.      *                       second of minute.
  283.      * @param minuteDuration number of seconds in the current minute, normally {@code 60}.
  284.      * @return new time components for the specified time.
  285.      * @throws OrekitIllegalArgumentException if the inequalities above do not hold.
  286.      * @since 10.2
  287.      */
  288.     public static TimeComponents fromSeconds(final int secondInDayA,
  289.                                              final double secondInDayB,
  290.                                              final double leap,
  291.                                              final int minuteDuration) {
  292.         return new TimeComponents(secondInDayA, secondInDayB, leap, minuteDuration);
  293.     }

  294.     /** Parse a string in ISO-8601 format to build a time.
  295.      * <p>The supported formats are:
  296.      * <ul>
  297.      *   <li>basic and extended format local time: hhmmss, hh:mm:ss (with optional decimals in seconds)</li>
  298.      *   <li>optional UTC time: hhmmssZ, hh:mm:ssZ</li>
  299.      *   <li>optional signed hours UTC offset: hhmmss+HH, hhmmss-HH, hh:mm:ss+HH, hh:mm:ss-HH</li>
  300.      *   <li>optional signed basic hours and minutes UTC offset: hhmmss+HHMM, hhmmss-HHMM, hh:mm:ss+HHMM, hh:mm:ss-HHMM</li>
  301.      *   <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>
  302.      * </ul>
  303.      *
  304.      * <p> As shown by the list above, only the complete representations defined in section 4.2
  305.      * of ISO-8601 standard are supported, neither expended representations nor representations
  306.      * with reduced accuracy are supported.
  307.      *
  308.      * @param string string to parse
  309.      * @return a parsed time
  310.      * @exception IllegalArgumentException if string cannot be parsed
  311.      */
  312.     public static TimeComponents parseTime(final String string) {

  313.         // is the date a calendar date ?
  314.         final Matcher timeMatcher = ISO8601_FORMATS.matcher(string);
  315.         if (timeMatcher.matches()) {
  316.             final int    hour      = Integer.parseInt(timeMatcher.group(1));
  317.             final int    minute    = Integer.parseInt(timeMatcher.group(2));
  318.             final double second    = timeMatcher.group(3) == null ? 0.0 : Double.parseDouble(timeMatcher.group(3).replace(',', '.'));
  319.             final String offset    = timeMatcher.group(4);
  320.             final int    minutesFromUTC;
  321.             if (offset == null) {
  322.                 // no offset from UTC is given
  323.                 minutesFromUTC = 0;
  324.             } else {
  325.                 // we need to parse an offset from UTC
  326.                 // the sign is mandatory and the ':' separator is optional
  327.                 // so we can have offsets given as -06:00 or +0100
  328.                 final int sign          = offset.codePointAt(0) == '-' ? -1 : +1;
  329.                 final int hourOffset    = Integer.parseInt(offset.substring(1, 3));
  330.                 final int minutesOffset = offset.length() <= 3 ? 0 : Integer.parseInt(offset.substring(offset.length() - 2));
  331.                 minutesFromUTC          = sign * (minutesOffset + 60 * hourOffset);
  332.             }
  333.             return new TimeComponents(hour, minute, second, minutesFromUTC);
  334.         }

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

  336.     }

  337.     /** Get the hour number.
  338.      * @return hour number from 0 to 23
  339.      */
  340.     public int getHour() {
  341.         return hour;
  342.     }

  343.     /** Get the minute number.
  344.      * @return minute minute number from 0 to 59
  345.      */
  346.     public int getMinute() {
  347.         return minute;
  348.     }

  349.     /** Get the seconds number.
  350.      * @return second second number from 0.0 to 61.0 (excluded). Note that 60 &le; second
  351.      * &lt; 61 only occurs during a leap second.
  352.      */
  353.     public double getSecond() {
  354.         return second;
  355.     }

  356.     /** Get the offset between the specified date and UTC.
  357.      * <p>
  358.      * The offset is always an integral number of minutes, as per ISO-8601 standard.
  359.      * </p>
  360.      * @return offset in minutes between the specified date and UTC
  361.      * @since 7.2
  362.      */
  363.     public int getMinutesFromUTC() {
  364.         return minutesFromUTC;
  365.     }

  366.     /** Get the second number within the local day, <em>without</em> applying the {@link #getMinutesFromUTC() offset from UTC}.
  367.      * @return second number from 0.0 to Constants.JULIAN_DAY
  368.      * @see #getSecondsInUTCDay()
  369.      * @since 7.2
  370.      */
  371.     public double getSecondsInLocalDay() {
  372.         return second + 60 * minute + 3600 * hour;
  373.     }

  374.     /** Get the second number within the UTC day, applying the {@link #getMinutesFromUTC() offset from UTC}.
  375.      * @return second number from {@link #getMinutesFromUTC() -getMinutesFromUTC()}
  376.      * to Constants.JULIAN_DAY {@link #getMinutesFromUTC() + getMinutesFromUTC()}
  377.      * @see #getSecondsInLocalDay()
  378.      * @since 7.2
  379.      */
  380.     public double getSecondsInUTCDay() {
  381.         return second + 60 * (minute - minutesFromUTC) + 3600 * hour;
  382.     }

  383.     /**
  384.      * Package private method that allows specification of seconds format. Allows access
  385.      * from {@link DateTimeComponents#toString(int, int)}. Access from outside of rounding
  386.      * methods would result in invalid times, see #590, #591.
  387.      *
  388.      * @param secondsFormat for the seconds.
  389.      * @return string without UTC offset.
  390.      */
  391.     String toStringWithoutUtcOffset(final DecimalFormat secondsFormat) {
  392.         return String.format("%02d:%02d:%s", hour, minute, secondsFormat.format(second));
  393.     }

  394.     /**
  395.      * Get a string representation of the time without the offset from UTC.
  396.      *
  397.      * @return a string representation of the time in an ISO 8601 like format.
  398.      * @see #formatUtcOffset()
  399.      * @see #toString()
  400.      */
  401.     public String toStringWithoutUtcOffset() {
  402.         // create formats here as they are not thread safe
  403.         // Format for seconds to prevent rounding up to an invalid time. See #591
  404.         final DecimalFormat secondsFormat =
  405.                 new DecimalFormat("00.000###########", US_SYMBOLS);
  406.         return toStringWithoutUtcOffset(secondsFormat);
  407.     }

  408.     /**
  409.      * Get the UTC offset as a string in ISO8601 format. For example, {@code +00:00}.
  410.      *
  411.      * @return the UTC offset as a string.
  412.      * @see #toStringWithoutUtcOffset()
  413.      * @see #toString()
  414.      */
  415.     public String formatUtcOffset() {
  416.         final int hourOffset = FastMath.abs(minutesFromUTC) / 60;
  417.         final int minuteOffset = FastMath.abs(minutesFromUTC) % 60;
  418.         return (minutesFromUTC < 0 ? '-' : '+') +
  419.                 String.format("%02d:%02d", hourOffset, minuteOffset);
  420.     }

  421.     /**
  422.      * Get a string representation of the time including the offset from UTC.
  423.      *
  424.      * @return string representation of the time in an ISO 8601 like format including the
  425.      * UTC offset.
  426.      * @see #toStringWithoutUtcOffset()
  427.      * @see #formatUtcOffset()
  428.      */
  429.     public String toString() {
  430.         return toStringWithoutUtcOffset() + formatUtcOffset();
  431.     }

  432.     /** {@inheritDoc} */
  433.     public int compareTo(final TimeComponents other) {
  434.         return Double.compare(getSecondsInUTCDay(), other.getSecondsInUTCDay());
  435.     }

  436.     /** {@inheritDoc} */
  437.     public boolean equals(final Object other) {
  438.         try {
  439.             final TimeComponents otherTime = (TimeComponents) other;
  440.             return otherTime != null &&
  441.                    hour           == otherTime.hour   &&
  442.                    minute         == otherTime.minute &&
  443.                    second         == otherTime.second &&
  444.                    minutesFromUTC == otherTime.minutesFromUTC;
  445.         } catch (ClassCastException cce) {
  446.             return false;
  447.         }
  448.     }

  449.     /** {@inheritDoc} */
  450.     public int hashCode() {
  451.         final long bits = Double.doubleToLongBits(second);
  452.         return ((hour << 16) ^ ((minute - minutesFromUTC) << 8)) ^ (int) (bits ^ (bits >>> 32));
  453.     }

  454. }