TimeComponents.java
/* Copyright 2002-2025 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.IOException;
import java.io.Serializable;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.hipparchus.util.FastMath;
import org.orekit.errors.OrekitIllegalArgumentException;
import org.orekit.errors.OrekitInternalError;
import org.orekit.errors.OrekitMessages;
import org.orekit.utils.Constants;
import org.orekit.utils.formatting.FastLongFormatter;
/** 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, TimeOffset.ZERO);
/** Constant for commonly used hour 12:00:00. */
public static final TimeComponents H12 = new TimeComponents(12, 0, TimeOffset.ZERO);
// CHECKSTYLE: stop ConstantName
/** Constant for NaN time.
* @since 13.0
*/
public static final TimeComponents NaN = new TimeComponents(0, 0, TimeOffset.NaN);
// CHECKSTYLE: resume ConstantName
/** Format for one 2 digits integer field. */
private static final FastLongFormatter PADDED_TWO_DIGITS_INTEGER = new FastLongFormatter(2, true);
/** Formatters for up to 18 digits integer field. */
private static final FastLongFormatter[] PADDED_FORMATTERS = new FastLongFormatter[] {
null, new FastLongFormatter( 1, true), new FastLongFormatter( 2, true),
new FastLongFormatter( 3, true), new FastLongFormatter( 4, true), new FastLongFormatter( 5, true),
new FastLongFormatter( 6, true), new FastLongFormatter( 7, true), new FastLongFormatter( 8, true),
new FastLongFormatter( 9, true), new FastLongFormatter(10, true), new FastLongFormatter(11, true),
new FastLongFormatter(12, true), new FastLongFormatter(13, true), new FastLongFormatter(14, true),
new FastLongFormatter(15, true), new FastLongFormatter(16, true), new FastLongFormatter(17, true),
new FastLongFormatter(18, true)
};
/** Scaling factors used for rounding. */
// CHECKSTYLE: stop Indentation check
private static final long[] SCALING = new long[] {
1000000000000000000L,
100000000000000000L,
10000000000000000L,
1000000000000000L,
100000000000000L,
10000000000000L,
1000000000000L,
100000000000L,
10000000000L,
1000000000L,
100000000L,
10000000L,
1000000L,
100000L,
10000L,
1000L,
100L,
10L,
1L
};
// CHECKSTYLE: resume Indentation check
/** Wrapping limits for rounding to next minute.
* @since 13.0
*/
private static final TimeOffset[] WRAPPING = new TimeOffset[] {
new TimeOffset(59L, 500000000000000000L), // round to second
new TimeOffset(59L, 950000000000000000L), // round to 10⁻¹ second
new TimeOffset(59L, 995000000000000000L), // round to 10⁻² second
new TimeOffset(59L, 999500000000000000L), // round to 10⁻³ second
new TimeOffset(59L, 999950000000000000L), // round to 10⁻⁴ second
new TimeOffset(59L, 999995000000000000L), // round to 10⁻⁵ second
new TimeOffset(59L, 999999500000000000L), // round to 10⁻⁶ second
new TimeOffset(59L, 999999950000000000L), // round to 10⁻⁷ second
new TimeOffset(59L, 999999995000000000L), // round to 10⁻⁸ second
new TimeOffset(59L, 999999999500000000L), // round to 10⁻⁹ second
new TimeOffset(59L, 999999999950000000L), // round to 10⁻¹⁰ second
new TimeOffset(59L, 999999999995000000L), // round to 10⁻¹¹ second
new TimeOffset(59L, 999999999999500000L), // round to 10⁻¹² second
new TimeOffset(59L, 999999999999950000L), // round to 10⁻¹³ second
new TimeOffset(59L, 999999999999995000L), // round to 10⁻¹⁴ second
new TimeOffset(59L, 999999999999999500L), // round to 10⁻¹⁵ second
new TimeOffset(59L, 999999999999999950L), // round to 10⁻¹⁶ second
new TimeOffset(59L, 999999999999999995L) // round to 10⁻¹⁷ second
};
/** Serializable UID. */
private static final long serialVersionUID = 20240712L;
/** Basic and extends formats for local time, with optional timezone. */
private static final Pattern ISO8601_FORMATS = Pattern.compile("^(\\d\\d):?(\\d\\d):?(\\d\\d(?:[.,]\\d+)?)?(?:Z|([-+]\\d\\d(?::?\\d\\d)?))?$");
/** Number of seconds in one hour. */
private static final int HOUR = 3600;
/** Number of seconds in one minute. */
private static final int MINUTE = 60;
/** Constant for 23 hours. */
private static final int TWENTY_THREE = 23;
/** Constant for 59 minutes. */
private static final int FIFTY_NINE = 59;
/** Constant for 23:59. */
private static final TimeOffset TWENTY_THREE_FIFTY_NINE =
new TimeOffset(TWENTY_THREE * HOUR + FIFTY_NINE * MINUTE, 0L);
/** Hour number. */
private final int hour;
/** Minute number. */
private final int minute;
/** Second number. */
private final TimeOffset 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, new TimeOffset(second));
}
/** 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)
* @since 13.0
*/
public TimeComponents(final int hour, final int minute, final TimeOffset 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 {
this(hour, minute, new TimeOffset(second), 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 62.0 (excluded, more than 61 s occurred on
* the 1961 leap second, which was between 1 and 2 seconds in duration)
* @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 13.0
*/
public TimeComponents(final int hour, final int minute, final TimeOffset second,
final int minutesFromUTC)
throws IllegalArgumentException {
// range check
if (hour < 0 || hour > 23 ||
minute < 0 || minute > 59 ||
second.getSeconds() < 0L || second.getSeconds() >= 62L) {
throw new OrekitIllegalArgumentException(OrekitMessages.NON_EXISTENT_HMS_TIME,
hour, minute, second.toDouble());
}
this.hour = hour;
this.minute = minute;
this.second = second;
this.minutesFromUTC = minutesFromUTC;
}
/**
* Build a time from the second number within the day.
*
* <p>If the {@code secondInDay} is less than {@code 60.0} then {@link #getSecond()}
* and {@link #getSplitSecond()} will be less than {@code 60.0}, otherwise they will be
* less than {@code 61.0}. This constructor may produce an invalid value of
* {@link #getSecond()} and {@link #getSplitSecond()} during a negative leap second,
* through there has never been one. For more control over the number of seconds in
* the final minute use {@link #TimeComponents(TimeOffset, TimeOffset, int)}.
*
* <p>This constructor is always in UTC (i.e. {@link #getMinutesFromUTC() will return
* 0}).
*
* @param secondInDay second number from 0.0 to {@link Constants#JULIAN_DAY} {@code +
* 1} (excluded)
* @throws OrekitIllegalArgumentException if seconds number is out of range
* @see #TimeComponents(TimeOffset, TimeOffset, int)
* @see #TimeComponents(int, double)
*/
public TimeComponents(final double secondInDay)
throws OrekitIllegalArgumentException {
this(new TimeOffset(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 Constants#JULIAN_DAY}
* {@code + 1} (excluded). The two parameters are used for increased accuracy.
*
* <p>If the sum is less than {@code 60.0} then {@link #getSecond()} will be less
* than {@code 60.0}, otherwise it will be less than {@code 61.0}. This constructor
* may produce an invalid value of {@link #getSecond()} during a negative leap second,
* through there has never been one. For more control over the number of seconds in
* the final minute use {@link #TimeComponents(TimeOffset, TimeOffset, int)}.
*
* <p>This constructor is always in UTC (i.e. {@link #getMinutesFromUTC()} will
* return 0).
*
* @param secondInDayA first part of the second number
* @param secondInDayB last part of the second number
* @throws OrekitIllegalArgumentException if seconds number is out of range
* @see #TimeComponents(TimeOffset, TimeOffset, int)
*/
public TimeComponents(final int secondInDayA, final double secondInDayB)
throws OrekitIllegalArgumentException {
// if the total is at least 86400 then assume there is a leap second
final TimeOffset aPlusB = new TimeOffset(secondInDayA).add(new TimeOffset(secondInDayB));
final TimeComponents tc = aPlusB.compareTo(TimeOffset.DAY) >= 0 ?
new TimeComponents(aPlusB.subtract(TimeOffset.SECOND), TimeOffset.SECOND, 61) :
new TimeComponents(aPlusB, TimeOffset.ZERO, 60);
this.hour = tc.hour;
this.minute = tc.minute;
this.second = tc.second;
this.minutesFromUTC = tc.minutesFromUTC;
}
/**
* Build a time from the second number within the day.
*
* <p>If the {@code secondInDay} is less than {@code 60.0} then {@link #getSecond()}
* will be less than {@code 60.0}, otherwise it will be less than {@code 61.0}. This constructor
* may produce an invalid value of {@link #getSecond()} during a negative leap second,
* through there has never been one. For more control over the number of seconds in
* the final minute use {@link #TimeComponents(TimeOffset, TimeOffset, int)}.
*
* <p>This constructor is always in UTC (i.e. {@link #getMinutesFromUTC() will return
* 0}).
*
* @param splitSecondInDay second number from 0.0 to {@link Constants#JULIAN_DAY} {@code +
* 1} (excluded)
* @see #TimeComponents(TimeOffset, TimeOffset, int)
* @see #TimeComponents(int, double)
* @since 13.0
*/
public TimeComponents(final TimeOffset splitSecondInDay) {
if (splitSecondInDay.compareTo(TimeOffset.ZERO) < 0) {
// negative time
throw new OrekitIllegalArgumentException(OrekitMessages.OUT_OF_RANGE_SECONDS_NUMBER_DETAIL,
splitSecondInDay.toDouble(),
0, TimeOffset.DAY_WITH_POSITIVE_LEAP.getSeconds());
} else if (splitSecondInDay.compareTo(TimeOffset.DAY) >= 0) {
// if the total is at least 86400 then assume there is a leap second
if (splitSecondInDay.compareTo(TimeOffset.DAY_WITH_POSITIVE_LEAP) >= 0) {
// more than one leap second is too much
throw new OrekitIllegalArgumentException(OrekitMessages.OUT_OF_RANGE_SECONDS_NUMBER_DETAIL,
splitSecondInDay.toDouble(),
0, TimeOffset.DAY_WITH_POSITIVE_LEAP.getSeconds());
} else {
hour = TWENTY_THREE;
minute = FIFTY_NINE;
second = splitSecondInDay.subtract(TWENTY_THREE_FIFTY_NINE);
}
} else {
// regular time within day
hour = (int) splitSecondInDay.getSeconds() / HOUR;
minute = ((int) splitSecondInDay.getSeconds() % HOUR) / MINUTE;
second = splitSecondInDay.subtract(new TimeOffset(hour * HOUR + minute * MINUTE, 0L));
}
minutesFromUTC = 0;
}
/**
* Build a time from the second number within the day.
*
* <p>The seconds past midnight is the sum {@code secondInDay + leap}. Only the part
* {@code secondInDay} is used to compute the hours and minutes. The second parameter
* ({@code leap}) is added directly to the second value ({@link #getSecond()}) to
* implement leap seconds. These two quantities must satisfy the following constraints.
* This first guarantees the hour and minute are valid, the second guarantees the second
* is valid.
*
* <pre>
* {@code 0 <= secondInDay < 86400}
* {@code 0 <= secondInDay % 60 + leap <= minuteDuration}
* {@code 0 <= leap <= minuteDuration - 60 if minuteDuration >= 60}
* {@code 0 >= leap >= minuteDuration - 60 if minuteDuration < 60}
* </pre>
*
* <p>If the seconds of minute ({@link #getSecond()}) computed from {@code
* secondInDay + leap} is greater than or equal to {@code 60 + leap}
* then the second of minute will be set to {@code FastMath.nextDown(60 + leap)}. This
* prevents rounding to an invalid seconds of minute number when the input values have
* greater precision than a {@code double}.
*
* <p>This constructor is always in UTC (i.e. {@link #getMinutesFromUTC() will return
* 0}).
*
* <p>If {@code secondsInDay} or {@code leap} is NaN then the hour and minute will
* be set arbitrarily and the second of minute will be NaN.
*
* @param secondInDay part of the second number.
* @param leap magnitude of the leap second if this point in time is during
* a leap second, otherwise {@code 0.0}. This value is not used
* to compute hours and minutes, but it is added to the computed
* second of minute.
* @param minuteDuration number of seconds in the current minute, normally {@code 60}.
* @throws OrekitIllegalArgumentException if the inequalities above do not hold.
* @since 10.2
*/
public TimeComponents(final TimeOffset secondInDay, final TimeOffset leap, final int minuteDuration) {
minutesFromUTC = 0;
if (secondInDay.isNaN()) {
// special handling for NaN
hour = 0;
minute = 0;
second = secondInDay;
return;
}
// range check
if (secondInDay.compareTo(TimeOffset.ZERO) < 0 || secondInDay.compareTo(TimeOffset.DAY) >= 0) {
throw new OrekitIllegalArgumentException(OrekitMessages.OUT_OF_RANGE_SECONDS_NUMBER_DETAIL,
// this can produce some strange messages due to rounding
secondInDay.toDouble(), 0, Constants.JULIAN_DAY);
}
final int maxExtraSeconds = minuteDuration - MINUTE;
if (leap.getSeconds() * maxExtraSeconds < 0 || FastMath.abs(leap.getSeconds()) > FastMath.abs(maxExtraSeconds)) {
throw new OrekitIllegalArgumentException(OrekitMessages.OUT_OF_RANGE_SECONDS_NUMBER_DETAIL,
leap, 0, maxExtraSeconds);
}
// extract the time components
int wholeSeconds = (int) secondInDay.getSeconds();
hour = wholeSeconds / HOUR;
wholeSeconds -= HOUR * hour;
minute = wholeSeconds / MINUTE;
wholeSeconds -= MINUTE * minute;
// at this point ((minuteDuration - wholeSeconds) - leap) - fractional > 0
// or else one of the preconditions was violated. Even if there is no violation,
// naiveSecond may round to minuteDuration, creating an invalid time.
// In that case round down to preserve a valid time at the cost of up to 1as of error.
// See #676 and #681.
final TimeOffset naiveSecond = new TimeOffset(wholeSeconds, secondInDay.getAttoSeconds()).add(leap);
if (naiveSecond.compareTo(TimeOffset.ZERO) < 0) {
throw new OrekitIllegalArgumentException(
OrekitMessages.OUT_OF_RANGE_SECONDS_NUMBER_DETAIL,
naiveSecond, 0, minuteDuration);
}
if (naiveSecond.getSeconds() < minuteDuration) {
second = naiveSecond;
} else {
second = new TimeOffset(minuteDuration - 1, 999999999999999999L);
}
}
/** 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 TimeOffset second = timeMatcher.group(3) == null ?
TimeOffset.ZERO :
TimeOffset.parse(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 + MINUTE * 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 ≤ second
* < 61 only occurs during a leap second.
*/
public double getSecond() {
return second.toDouble();
}
/** Get the seconds number.
* @return second second number from 0.0 to 61.0 (excluded). Note that 60 ≤ second
* < 61 only occurs during a leap second.
*/
public TimeOffset getSplitSecond() {
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 #getSplitSecondsInLocalDay()
* @see #getSecondsInUTCDay()
* @since 7.2
*/
public double getSecondsInLocalDay() {
return getSplitSecondsInLocalDay().toDouble();
}
/** 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 #getSecondsInLocalDay()
* @see #getSplitSecondsInUTCDay()
* @since 13.0
*/
public TimeOffset getSplitSecondsInLocalDay() {
return new TimeOffset((long) MINUTE * minute + (long) HOUR * hour, 0L).add(second);
}
/** 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 #getSplitSecondsInUTCDay()
* @see #getSecondsInLocalDay()
* @since 7.2
*/
public double getSecondsInUTCDay() {
return getSplitSecondsInUTCDay().toDouble();
}
/** 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 #getSecondsInUTCDay()
* @see #getSplitSecondsInLocalDay()
* @since 13.0
*/
public TimeOffset getSplitSecondsInUTCDay() {
return new TimeOffset((long) MINUTE * (minute - minutesFromUTC) + (long) HOUR * hour, 0L).add(second);
}
/**
* Round this time to the given precision if needed to prevent rounding up to an
* invalid seconds number. This is useful, for example, when writing custom date-time
* formatting methods so one does not, e.g., end up with "60.0" seconds during a
* normal minute when the value of seconds is {@code 59.999}. This method will instead
* round up the minute, hour, day, month, and year as needed.
*
* @param minuteDuration 59, 60, 61, or 62 seconds depending on the date being close
* to a leap second introduction and the magnitude of the leap
* second.
* @param fractionDigits the number of decimal digits after the decimal point in the
* seconds number that will be printed. This date-time is
* rounded to {@code fractionDigits} after the decimal point if
* necessary to prevent rounding up to {@code minuteDuration}.
* {@code fractionDigits} must be greater than or equal to
* {@code 0}.
* @return the instance itself if no rounding was needed, or a time within
* {@code 0.5 * 10**-fractionDigits} seconds of this, and with a seconds number that
* will not round up to {@code minuteDuration} when rounded to {@code fractionDigits}
* after the decimal point
* @since 13.0
*/
public TimeComponents wrapIfNeeded(final int minuteDuration, final int fractionDigits) {
TimeOffset wrappedSecond = second;
// adjust limit according to current minute duration
final TimeOffset limit = WRAPPING[FastMath.min(fractionDigits, WRAPPING.length - 1)].
add(new TimeOffset(minuteDuration - 60, 0L));
if (wrappedSecond.compareTo(limit) >= 0) {
// we should wrap around to the next minute
int wrappedMinute = minute;
int wrappedHour = hour;
wrappedSecond = TimeOffset.ZERO;
++wrappedMinute;
if (wrappedMinute > 59) {
wrappedMinute = 0;
++wrappedHour;
if (wrappedHour > 23) {
wrappedHour = 0;
}
}
return new TimeComponents(wrappedHour, wrappedMinute, wrappedSecond);
}
return this;
}
/**
* Package private method that allows specification of seconds format. Allows access from
* {@link DateTimeComponents#toString(int, int)}. Access from outside of rounding methods would result in invalid
* times, see #590, #591.
*
* @param fractionDigits the number of digits to include after the decimal point in the string representation of the
* seconds. The date and time are first rounded as necessary. {@code fractionDigits} must be
* greater than or equal to {@code 0}.
* @return string without UTC offset.
* @since 13.0
*/
String toStringWithoutUtcOffset(final int fractionDigits) {
try {
final StringBuilder builder = new StringBuilder();
if (second.isFinite()) {
// general case for regular times
final TimeComponents rounded = new TimeComponents(hour, minute, second.getRoundedOffset(fractionDigits));
PADDED_TWO_DIGITS_INTEGER.appendTo(builder, rounded.hour);
builder.append(':');
PADDED_TWO_DIGITS_INTEGER.appendTo(builder, rounded.minute);
builder.append(':');
PADDED_TWO_DIGITS_INTEGER.appendTo(builder, rounded.second.getSeconds());
if (fractionDigits > 0) {
builder.append('.');
final int index = FastMath.min(PADDED_FORMATTERS.length - 1, fractionDigits);
PADDED_FORMATTERS[index].appendTo(builder, rounded.second.getAttoSeconds() / SCALING[index]);
}
} else {
PADDED_TWO_DIGITS_INTEGER.appendTo(builder, hour);
builder.append(':');
PADDED_TWO_DIGITS_INTEGER.appendTo(builder, minute);
builder.append(":NaN"); // ±∞ can never happen
}
return builder.toString();
} catch (IOException ioe) {
// this should never happen
throw new OrekitInternalError(ioe);
}
}
/**
* Get a string representation of the time without the offset from UTC.
*
* @return a string representation of the time in an ISO 8601 like format.
* @see #formatUtcOffset()
* @see #toString()
*/
public String toStringWithoutUtcOffset() {
// create formats here as they are not thread safe
// Format for seconds to prevent rounding up to an invalid time. See #591
final String formatted = toStringWithoutUtcOffset(18);
int last = formatted.length() - 1;
while (last > 11 && formatted.charAt(last) == '0') {
// we want to remove final zeros (but keeping milliseconds for compatibility)
--last;
}
return formatted.substring(0, last + 1);
}
/**
* Get the UTC offset as a string in ISO8601 format. For example, {@code +00:00}.
*
* @return the UTC offset as a string.
* @see #toStringWithoutUtcOffset()
* @see #toString()
*/
public String formatUtcOffset() {
try {
final int hourOffset = FastMath.abs(minutesFromUTC) / MINUTE;
final int minuteOffset = FastMath.abs(minutesFromUTC) % MINUTE;
final StringBuilder builder = new StringBuilder();
builder.append(minutesFromUTC < 0 ? '-' : '+');
PADDED_TWO_DIGITS_INTEGER.appendTo(builder, hourOffset);
builder.append(':');
PADDED_TWO_DIGITS_INTEGER.appendTo(builder, minuteOffset);
return builder.toString();
}
catch (IOException ioe) {
// this should never happen
throw new OrekitInternalError(ioe);
}
}
/**
* Get a string representation of the time including the offset from UTC.
*
* @return string representation of the time in an ISO 8601 like format including the
* UTC offset.
* @see #toStringWithoutUtcOffset()
* @see #formatUtcOffset()
*/
public String toString() {
return toStringWithoutUtcOffset() + formatUtcOffset();
}
/** {@inheritDoc} */
public int compareTo(final TimeComponents other) {
return getSplitSecondsInUTCDay().compareTo(other.getSplitSecondsInUTCDay());
}
/** {@inheritDoc} */
public boolean equals(final Object other) {
try {
final TimeComponents otherTime = (TimeComponents) other;
return otherTime != null &&
hour == otherTime.hour &&
minute == otherTime.minute &&
second.compareTo(otherTime.second) == 0 &&
minutesFromUTC == otherTime.minutesFromUTC;
} catch (ClassCastException cce) {
return false;
}
}
/** {@inheritDoc} */
public int hashCode() {
return ((hour << 16) ^ ((minute - minutesFromUTC) << 8)) ^ second.hashCode();
}
}