TimeComponents.java

/* Copyright 2002-2020 CS Group
 * Licensed to CS Group (CS) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * CS licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.orekit.time;

import java.io.Serializable;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.hipparchus.util.FastMath;
import org.orekit.errors.OrekitIllegalArgumentException;
import org.orekit.errors.OrekitMessages;


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

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

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

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

    /** Format for hours and minutes. */
    private static final DecimalFormat TWO_DIGITS = new DecimalFormat("00");

    /** Format for seconds. */
    private static final DecimalFormat SECONDS_FORMAT =
        new DecimalFormat("00.000", new DecimalFormatSymbols(Locale.US));

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

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

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

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

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

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

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

        // range check
        if ((hour   < 0) || (hour   >  23) ||
            (minute < 0) || (minute >  59) ||
            (second < 0) || (second >= 61.0)) {
            throw new OrekitIllegalArgumentException(OrekitMessages.NON_EXISTENT_HMS_TIME,
                                                     hour, minute, second);
        }

        this.hour           = hour;
        this.minute         = minute;
        this.second         = second;
        this.minutesFromUTC = minutesFromUTC;

    }

    /** Build a time from the second number within the day.
     * <p>
     * This constructor is always in UTC (i.e. {@link #getMinutesFromUTC() will return 0}).
     * </p>
     * @param secondInDay second number from 0.0 to {@link
     * org.orekit.utils.Constants#JULIAN_DAY} (excluded)
     * @exception OrekitIllegalArgumentException if seconds number is out of range
     */
    public TimeComponents(final double secondInDay)
        throws OrekitIllegalArgumentException {
        this(0, secondInDay);
    }

    /** Build a time from the second number within the day.
     * <p>
     * The second number is defined here as the sum
     * {@code secondInDayA + secondInDayB} from 0.0 to {@link
     * org.orekit.utils.Constants#JULIAN_DAY} (excluded). The two parameters
     * are used for increased accuracy.
     * </p>
     * <p>
     * This constructor is always in UTC (i.e. {@link #getMinutesFromUTC() will return 0}).
     * </p>
     * @param secondInDayA first part of the second number
     * @param secondInDayB last part of the second number
     * @exception OrekitIllegalArgumentException if seconds number is out of range
     */
    public TimeComponents(final int secondInDayA, final double secondInDayB)
        throws OrekitIllegalArgumentException {

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

        // range check
        if (wholeSeconds < 0 || wholeSeconds > 86400) {
            // beware, 86400 must be allowed to cope with leap seconds introduction days
            throw new OrekitIllegalArgumentException(OrekitMessages.OUT_OF_RANGE_SECONDS_NUMBER,
                                                     wholeSeconds + fractional);
        }

        // extract the time components
        hour           = wholeSeconds / 3600;
        wholeSeconds  -= 3600 * hour;
        minute         = wholeSeconds / 60;
        wholeSeconds  -= 60 * minute;
        second         = wholeSeconds + fractional;
        minutesFromUTC = 0;

    }

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

        // is the date a calendar date ?
        final Matcher timeMatcher = ISO8601_FORMATS.matcher(string);
        if (timeMatcher.matches()) {
            final int    hour      = Integer.parseInt(timeMatcher.group(1));
            final int    minute    = Integer.parseInt(timeMatcher.group(2));
            final double second    = timeMatcher.group(3) == null ? 0.0 : Double.parseDouble(timeMatcher.group(3).replace(',', '.'));
            final String offset    = timeMatcher.group(4);
            final int    minutesFromUTC;
            if (offset == null) {
                // no offset from UTC is given
                minutesFromUTC = 0;
            } else {
                // we need to parse an offset from UTC
                // the sign is mandatory and the ':' separator is optional
                // so we can have offsets given as -06:00 or +0100
                final int sign          = offset.codePointAt(0) == '-' ? -1 : +1;
                final int hourOffset    = Integer.parseInt(offset.substring(1, 3));
                final int minutesOffset = offset.length() <= 3 ? 0 : Integer.parseInt(offset.substring(offset.length() - 2));
                minutesFromUTC          = sign * (minutesOffset + 60 * hourOffset);
            }
            return new TimeComponents(hour, minute, second, minutesFromUTC);
        }

        throw new OrekitIllegalArgumentException(OrekitMessages.NON_EXISTENT_TIME, string);

    }

    /** Get the hour number.
     * @return hour number from 0 to 23
     */
    public int getHour() {
        return hour;
    }

    /** Get the minute number.
     * @return minute minute number from 0 to 59
     */
    public int getMinute() {
        return minute;
    }

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

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

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

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

    /** Get a string representation of the time.
     * @return string representation of the time
     */
    public String toString() {
        StringBuilder builder  = new StringBuilder().
                                 append(TWO_DIGITS.format(hour)).append(':').
                                 append(TWO_DIGITS.format(minute)).append(':').
                                 append(SECONDS_FORMAT.format(second));
        if (minutesFromUTC != 0) {
            builder = builder.
                      append(minutesFromUTC < 0 ? '-' : '+').
                      append(TWO_DIGITS.format(FastMath.abs(minutesFromUTC) / 60)).append(':').
                      append(TWO_DIGITS.format(FastMath.abs(minutesFromUTC) % 60));
        }
        return builder.toString();
    }

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

    /** {@inheritDoc} */
    public boolean equals(final Object other) {
        try {
            final TimeComponents otherTime = (TimeComponents) other;
            return otherTime != null &&
                   hour           == otherTime.hour   &&
                   minute         == otherTime.minute &&
                   second         == otherTime.second &&
                   minutesFromUTC == otherTime.minutesFromUTC;
        } catch (ClassCastException cce) {
            return false;
        }
    }

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

}