DateTimeComponents.java

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

  18. import java.io.IOException;
  19. import java.io.Serializable;

  20. import java.util.concurrent.TimeUnit;
  21. import org.hipparchus.util.FastMath;
  22. import org.orekit.errors.OrekitInternalError;
  23. import org.orekit.utils.Constants;
  24. import org.orekit.utils.formatting.FastLongFormatter;

  25. /** Holder for date and time components.
  26.  * <p>This class is a simple holder with no processing methods.</p>
  27.  * <p>Instance of this class are guaranteed to be immutable.</p>
  28.  * @see AbsoluteDate
  29.  * @see DateComponents
  30.  * @see TimeComponents
  31.  * @author Luc Maisonobe
  32.  */
  33. public class DateTimeComponents implements Serializable, Comparable<DateTimeComponents> {

  34.     /**
  35.      * The Julian Epoch.
  36.      *
  37.      * @see TimeScales#getJulianEpoch()
  38.      */
  39.     public static final DateTimeComponents JULIAN_EPOCH =
  40.             new DateTimeComponents(DateComponents.JULIAN_EPOCH, TimeComponents.H12);

  41.     /** Format for one 4 digits integer field.
  42.      * @since 13.0.3
  43.      */
  44.     private static final FastLongFormatter PADDED_FOUR_DIGITS_INTEGER = new FastLongFormatter(4, true);

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

  49.     /** Serializable UID. */
  50.     private static final long serialVersionUID = 20240720L;

  51.     /** Date component. */
  52.     private final DateComponents date;

  53.     /** Time component. */
  54.     private final TimeComponents time;

  55.     /** Build a new instance from its components.
  56.      * @param date date component
  57.      * @param time time component
  58.      */
  59.     public DateTimeComponents(final DateComponents date, final TimeComponents time) {
  60.         this.date = date;
  61.         this.time = time;
  62.     }

  63.     /** Build an instance from raw level components.
  64.      * @param year year number (may be 0 or negative for BC years)
  65.      * @param month month number from 1 to 12
  66.      * @param day day number from 1 to 31
  67.      * @param hour hour number from 0 to 23
  68.      * @param minute minute number from 0 to 59
  69.      * @param second second number from 0.0 to 60.0 (excluded)
  70.      * @exception IllegalArgumentException if inconsistent arguments
  71.      * are given (parameters out of range, february 29 for non-leap years,
  72.      * dates during the gregorian leap in 1582 ...)
  73.      */
  74.     public DateTimeComponents(final int year, final int month, final int day,
  75.                               final int hour, final int minute, final double second)
  76.         throws IllegalArgumentException {
  77.         this(year, month, day, hour, minute, new TimeOffset(second));
  78.     }

  79.     /** Build an instance from raw level components.
  80.      * @param year year number (may be 0 or negative for BC years)
  81.      * @param month month number from 1 to 12
  82.      * @param day day number from 1 to 31
  83.      * @param hour hour number from 0 to 23
  84.      * @param minute minute number from 0 to 59
  85.      * @param second second number from 0.0 to 60.0 (excluded)
  86.      * @exception IllegalArgumentException if inconsistent arguments
  87.      * are given (parameters out of range, february 29 for non-leap years,
  88.      * dates during the gregorian leap in 1582 ...)
  89.      * @since 13.0
  90.      */
  91.     public DateTimeComponents(final int year, final int month, final int day,
  92.                               final int hour, final int minute, final TimeOffset second)
  93.         throws IllegalArgumentException {
  94.         this.date = new DateComponents(year, month, day);
  95.         this.time = new TimeComponents(hour, minute, second);
  96.     }

  97.     /** Build an instance from raw level components.
  98.      * @param year year number (may be 0 or negative for BC years)
  99.      * @param month month enumerate
  100.      * @param day day number from 1 to 31
  101.      * @param hour hour number from 0 to 23
  102.      * @param minute minute number from 0 to 59
  103.      * @param second second number from 0.0 to 60.0 (excluded)
  104.      * @exception IllegalArgumentException if inconsistent arguments
  105.      * are given (parameters out of range, february 29 for non-leap years,
  106.      * dates during the gregorian leap in 1582 ...)
  107.      */
  108.     public DateTimeComponents(final int year, final Month month, final int day,
  109.                               final int hour, final int minute, final double second)
  110.         throws IllegalArgumentException {
  111.         this(year, month, day, hour, minute, new TimeOffset(second));
  112.     }

  113.     /** Build an instance from raw level components.
  114.      * @param year year number (may be 0 or negative for BC years)
  115.      * @param month month enumerate
  116.      * @param day day number from 1 to 31
  117.      * @param hour hour number from 0 to 23
  118.      * @param minute minute number from 0 to 59
  119.      * @param second second number from 0.0 to 60.0 (excluded)
  120.      * @exception IllegalArgumentException if inconsistent arguments
  121.      * are given (parameters out of range, february 29 for non-leap years,
  122.      * dates during the gregorian leap in 1582 ...)
  123.      * @since 13.0
  124.      */
  125.     public DateTimeComponents(final int year, final Month month, final int day,
  126.                               final int hour, final int minute, final TimeOffset second)
  127.         throws IllegalArgumentException {
  128.         this.date = new DateComponents(year, month, day);
  129.         this.time = new TimeComponents(hour, minute, second);
  130.     }

  131.     /** Build an instance from raw level components.
  132.      * <p>The hour is set to 00:00:00.000.</p>
  133.      * @param year year number (may be 0 or negative for BC years)
  134.      * @param month month number from 1 to 12
  135.      * @param day day number from 1 to 31
  136.      * @exception IllegalArgumentException if inconsistent arguments
  137.      * are given (parameters out of range, february 29 for non-leap years,
  138.      * dates during the gregorian leap in 1582 ...)
  139.      */
  140.     public DateTimeComponents(final int year, final int month, final int day)
  141.         throws IllegalArgumentException {
  142.         this.date = new DateComponents(year, month, day);
  143.         this.time = TimeComponents.H00;
  144.     }

  145.     /** Build an instance from raw level components.
  146.      * <p>The hour is set to 00:00:00.000.</p>
  147.      * @param year year number (may be 0 or negative for BC years)
  148.      * @param month month enumerate
  149.      * @param day day number from 1 to 31
  150.      * @exception IllegalArgumentException if inconsistent arguments
  151.      * are given (parameters out of range, february 29 for non-leap years,
  152.      * dates during the gregorian leap in 1582 ...)
  153.      */
  154.     public DateTimeComponents(final int year, final Month month, final int day)
  155.         throws IllegalArgumentException {
  156.         this.date = new DateComponents(year, month, day);
  157.         this.time = TimeComponents.H00;
  158.     }

  159.     /** Build an instance from a seconds offset with respect to another one.
  160.      * @param reference reference date/time
  161.      * @param offset offset from the reference in seconds
  162.      * @see #offsetFrom(DateTimeComponents)
  163.      */
  164.     public DateTimeComponents(final DateTimeComponents reference, final double offset) {
  165.         this(reference, new TimeOffset(offset));
  166.     }

  167.     /** Build an instance from a seconds offset with respect to another one.
  168.      * @param reference reference date/time
  169.      * @param offset offset from the reference in seconds
  170.      * @see #offsetFrom(DateTimeComponents)
  171.      * @since 13.0
  172.      */
  173.     public DateTimeComponents(final DateTimeComponents reference, final TimeOffset offset) {

  174.         // extract linear data from reference date/time
  175.         int    day     = reference.getDate().getJ2000Day();
  176.         TimeOffset seconds = reference.getTime().getSplitSecondsInLocalDay();

  177.         // apply offset
  178.         seconds = seconds.add(offset);

  179.         // fix range
  180.         final int dayShift = (int) FastMath.floor(seconds.toDouble() / Constants.JULIAN_DAY);
  181.         if (dayShift != 0) {
  182.             seconds = seconds.subtract(new TimeOffset(dayShift * TimeOffset.DAY.getSeconds(), 0L));
  183.         }
  184.         day     += dayShift;
  185.         final TimeComponents tmpTime = new TimeComponents(seconds);

  186.         // set up components
  187.         this.date = new DateComponents(day);
  188.         this.time = new TimeComponents(tmpTime.getHour(), tmpTime.getMinute(), tmpTime.getSplitSecond(),
  189.                                        reference.getTime().getMinutesFromUTC());

  190.     }

  191.     /** Build an instance from a seconds offset with respect to another one.
  192.      * @param reference reference date/time
  193.      * @param offset offset from the reference
  194.      * @param timeUnit the {@link TimeUnit} for the offset
  195.      * @see #offsetFrom(DateTimeComponents, TimeUnit)
  196.      * @since 12.1
  197.      */
  198.     public DateTimeComponents(final DateTimeComponents reference,
  199.                               final long offset, final TimeUnit timeUnit) {

  200.         // extract linear data from reference date/time
  201.         int       day     = reference.getDate().getJ2000Day();
  202.         TimeOffset seconds = reference.getTime().getSplitSecondsInLocalDay();

  203.         // apply offset
  204.         seconds = seconds.add(new TimeOffset(offset, timeUnit));

  205.         // fix range
  206.         final long dayShift = seconds.getSeconds() / TimeOffset.DAY.getSeconds() +
  207.                               (seconds.getSeconds() < 0L ? -1L : 0L);
  208.         if (dayShift != 0) {
  209.             seconds = seconds.subtract(new TimeOffset(dayShift, TimeOffset.DAY));
  210.             day    += dayShift;
  211.         }
  212.         final TimeComponents tmpTime = new TimeComponents(seconds);

  213.         // set up components
  214.         this.date = new DateComponents(day);
  215.         this.time = new TimeComponents(tmpTime.getHour(), tmpTime.getMinute(), tmpTime.getSplitSecond(),
  216.             reference.getTime().getMinutesFromUTC());

  217.     }

  218.     /** Parse a string in ISO-8601 format to build a date/time.
  219.      * <p>The supported formats are all date formats supported by {@link DateComponents#parseDate(String)}
  220.      * and all time formats supported by {@link TimeComponents#parseTime(String)} separated
  221.      * by the standard time separator 'T', or date components only (in which case a 00:00:00 hour is
  222.      * implied). Typical examples are 2000-01-01T12:00:00Z or 1976W186T210000.
  223.      * </p>
  224.      * @param string string to parse
  225.      * @return a parsed date/time
  226.      * @exception IllegalArgumentException if string cannot be parsed
  227.      */
  228.     public static DateTimeComponents parseDateTime(final String string) {

  229.         // is there a time ?
  230.         final int tIndex = string.indexOf('T');
  231.         if (tIndex > 0) {
  232.             return new DateTimeComponents(DateComponents.parseDate(string.substring(0, tIndex)),
  233.                                           TimeComponents.parseTime(string.substring(tIndex + 1)));
  234.         }

  235.         return new DateTimeComponents(DateComponents.parseDate(string), TimeComponents.H00);

  236.     }

  237.     /** Compute the seconds offset between two instances.
  238.      * @param dateTime dateTime to subtract from the instance
  239.      * @return offset in seconds between the two instants
  240.      * (positive if the instance is posterior to the argument)
  241.      * @see #DateTimeComponents(DateTimeComponents, TimeOffset)
  242.      */
  243.     public double offsetFrom(final DateTimeComponents dateTime) {
  244.         final int dateOffset = date.getJ2000Day() - dateTime.date.getJ2000Day();
  245.         final TimeOffset timeOffset = time.getSplitSecondsInUTCDay().
  246.                                      subtract(dateTime.time.getSplitSecondsInUTCDay());
  247.         return Constants.JULIAN_DAY * dateOffset + timeOffset.toDouble();
  248.     }

  249.     /** Compute the seconds offset between two instances.
  250.      * @param dateTime dateTime to subtract from the instance
  251.      * @param timeUnit the desired {@link TimeUnit}
  252.      * @return offset in the given timeunit between the two instants (positive
  253.      * if the instance is posterior to the argument), rounded to the nearest integer {@link TimeUnit}
  254.      * @see #DateTimeComponents(DateTimeComponents, long, TimeUnit)
  255.      * @since 12.1
  256.      */
  257.     public long offsetFrom(final DateTimeComponents dateTime, final TimeUnit timeUnit) {
  258.         final int dateOffset = date.getJ2000Day() - dateTime.date.getJ2000Day();
  259.         final TimeOffset timeOffset = time.getSplitSecondsInUTCDay().
  260.                                      subtract(dateTime.time.getSplitSecondsInUTCDay());
  261.         return TimeOffset.DAY.getRoundedTime(timeUnit) * dateOffset + timeOffset.getRoundedTime(timeUnit);
  262.     }

  263.     /** Get the date component.
  264.      * @return date component
  265.      */
  266.     public DateComponents getDate() {
  267.         return date;
  268.     }

  269.     /** Get the time component.
  270.      * @return time component
  271.      */
  272.     public TimeComponents getTime() {
  273.         return time;
  274.     }

  275.     /** {@inheritDoc} */
  276.     public int compareTo(final DateTimeComponents other) {
  277.         final int dateComparison = date.compareTo(other.date);
  278.         if (dateComparison < 0) {
  279.             return -1;
  280.         } else if (dateComparison > 0) {
  281.             return 1;
  282.         }
  283.         return time.compareTo(other.time);
  284.     }

  285.     /** {@inheritDoc} */
  286.     public boolean equals(final Object other) {
  287.         try {
  288.             final DateTimeComponents otherDateTime = (DateTimeComponents) other;
  289.             return otherDateTime != null &&
  290.                    date.equals(otherDateTime.date) && time.equals(otherDateTime.time);
  291.         } catch (ClassCastException cce) {
  292.             return false;
  293.         }
  294.     }

  295.     /** {@inheritDoc} */
  296.     public int hashCode() {
  297.         return (date.hashCode() << 16) ^ time.hashCode();
  298.     }

  299.     /** Return a string representation of this pair.
  300.      * <p>The format used is ISO8601 including the UTC offset.</p>
  301.      * @return string representation of this pair
  302.      */
  303.     public String toString() {
  304.         return date.toString() + 'T' + time.toString();
  305.     }

  306.     /**
  307.      * Get a string representation of the date-time without the offset from UTC. The
  308.      * format used is ISO6801, except without the offset from UTC.
  309.      *
  310.      * @return a string representation of the date-time.
  311.      * @see #toStringWithoutUtcOffset(int, int)
  312.      * @see #toString(int, int)
  313.      * @see #toStringRfc3339()
  314.      */
  315.     public String toStringWithoutUtcOffset() {
  316.         return date.toString() + 'T' + time.toStringWithoutUtcOffset();
  317.     }


  318.     /**
  319.      * Return a string representation of this date-time, rounded to millisecond
  320.      * precision.
  321.      *
  322.      * <p>The format used is ISO8601 including the UTC offset.</p>
  323.      *
  324.      * @param minuteDuration 60, 61, or 62 seconds depending on the date being close to a
  325.      *                       leap second introduction and the magnitude of the leap
  326.      *                       second.
  327.      * @return string representation of this date, time, and UTC offset
  328.      * @see #toString(int, int)
  329.      */
  330.     public String toString(final int minuteDuration) {
  331.         return toString(minuteDuration, 3);
  332.     }

  333.     /**
  334.      * Return a string representation of this date-time, rounded to the given precision.
  335.      *
  336.      * <p>The format used is ISO8601 including the UTC offset.</p>
  337.      *
  338.      * @param minuteDuration 59, 60, 61, or 62 seconds depending on the date being close
  339.      *                       to a leap second introduction and the magnitude of the leap
  340.      *                       second.
  341.      * @param fractionDigits the number of digits to include after the decimal point in
  342.      *                       the string representation of the seconds. The date and time
  343.      *                       is first rounded as necessary. {@code fractionDigits} must
  344.      *                       be greater than or equal to {@code 0}.
  345.      * @return string representation of this date, time, and UTC offset
  346.      * @see #toStringRfc3339()
  347.      * @see #toStringWithoutUtcOffset()
  348.      * @see #toStringWithoutUtcOffset(int, int)
  349.      * @since 11.0
  350.      */
  351.     public String toString(final int minuteDuration, final int fractionDigits) {
  352.         return toStringWithoutUtcOffset(minuteDuration, fractionDigits) +
  353.                 time.formatUtcOffset();
  354.     }

  355.     /**
  356.      * Return a string representation of this date-time, rounded to the given precision.
  357.      *
  358.      * <p>The format used is ISO8601 without the UTC offset.</p>
  359.      *
  360.      * @param minuteDuration 59, 60, 61, or 62 seconds depending on the date being close
  361.      *                       to a leap second introduction and the magnitude of the leap
  362.      *                       second.
  363.      * @param fractionDigits the number of digits to include after the decimal point in
  364.      *                       the string representation of the seconds. The date and time
  365.      *                       are first rounded as necessary. {@code fractionDigits} must
  366.      *                       be greater than or equal to {@code 0}.
  367.      * @return string representation of this date, time, and UTC offset
  368.      * @see #toStringRfc3339()
  369.      * @see #toStringWithoutUtcOffset()
  370.      * @see #toString(int, int)
  371.      * @since 11.1
  372.      */
  373.     public String toStringWithoutUtcOffset(final int minuteDuration,
  374.                                            final int fractionDigits) {
  375.         final DateTimeComponents rounded = roundIfNeeded(minuteDuration, fractionDigits);
  376.         return rounded.getDate().toString() + 'T' +
  377.                rounded.getTime().toStringWithoutUtcOffset(fractionDigits);
  378.     }

  379.     /**
  380.      * Round this date-time to the given precision if needed to prevent rounding up to an
  381.      * invalid seconds number. This is useful, for example, when writing custom date-time
  382.      * formatting methods so one does not, e.g., end up with "60.0" seconds during a
  383.      * normal minute when the value of seconds is {@code 59.999}. This method will instead
  384.      * round up the minute, hour, day, month, and year as needed.
  385.      *
  386.      * @param minuteDuration 59, 60, 61, or 62 seconds depending on the date being close
  387.      *                       to a leap second introduction and the magnitude of the leap
  388.      *                       second.
  389.      * @param fractionDigits the number of decimal digits after the decimal point in the
  390.      *                       seconds number that will be printed. This date-time is
  391.      *                       rounded to {@code fractionDigits} after the decimal point if
  392.      *                       necessary to prevent rounding up to {@code minuteDuration}.
  393.      *                       {@code fractionDigits} must be greater than or equal to
  394.      *                       {@code 0}.
  395.      * @return a date-time within {@code 0.5 * 10**-fractionDigits} seconds of this, and
  396.      * with a seconds number that will not round up to {@code minuteDuration} when rounded
  397.      * to {@code fractionDigits} after the decimal point.
  398.      * @since 11.3
  399.      */
  400.     public DateTimeComponents roundIfNeeded(final int minuteDuration, final int fractionDigits) {

  401.         final TimeComponents wrappedTime = time.wrapIfNeeded(minuteDuration, fractionDigits);
  402.         if (wrappedTime == time) {
  403.             // no wrapping was needed
  404.             return this;
  405.         } else {
  406.             if (wrappedTime.getHour() < time.getHour()) {
  407.                 // we have wrapped around next day
  408.                 return new DateTimeComponents(new DateComponents(date, 1), wrappedTime);
  409.             } else {
  410.                 // only the time was wrapped
  411.                 return new DateTimeComponents(date, wrappedTime);
  412.             }
  413.         }

  414.     }

  415.     /**
  416.      * Represent the given date and time as a string according to the format in RFC 3339.
  417.      * RFC3339 is a restricted subset of ISO 8601 with a well defined grammar. This method
  418.      * includes enough precision to represent the point in time without rounding up to the
  419.      * next minute.
  420.      *
  421.      * <p>RFC3339 is unable to represent BC years, years of 10000 or more, time zone
  422.      * offsets of 100 hours or more, or NaN. In these cases the value returned from this
  423.      * method will not be valid RFC3339 format.
  424.      *
  425.      * @return RFC 3339 format string.
  426.      * @see <a href="https://tools.ietf.org/html/rfc3339#page-8">RFC 3339</a>
  427.      * @see AbsoluteDate#toStringRfc3339(TimeScale)
  428.      * @see #toString(int, int)
  429.      * @see #toStringWithoutUtcOffset()
  430.      */
  431.     public String toStringRfc3339() {
  432.         final StringBuilder builder = new StringBuilder();
  433.         final DateComponents d = this.getDate();
  434.         final TimeComponents t = this.getTime();
  435.         try {
  436.             // date
  437.             PADDED_FOUR_DIGITS_INTEGER.appendTo(builder, d.getYear());
  438.             builder.append('-');
  439.             PADDED_TWO_DIGITS_INTEGER.appendTo(builder, d.getMonth());
  440.             builder.append('-');
  441.             PADDED_TWO_DIGITS_INTEGER.appendTo(builder, d.getDay());
  442.             builder.append('T');
  443.             // time
  444.             if (!t.getSplitSecondsInLocalDay().isZero()) {
  445.                 final String formatted = t.toStringWithoutUtcOffset(18);
  446.                 int          last      = formatted.length() - 1;
  447.                 while (formatted.charAt(last) == '0') {
  448.                     // we want to remove final zeros
  449.                     --last;
  450.                 }
  451.                 if (formatted.charAt(last) == '.') {
  452.                     // remove the decimal point if no decimals follow
  453.                     --last;
  454.                 }
  455.                 builder.append(formatted.substring(0, last + 1));
  456.             } else {
  457.                 // shortcut for midnight local time
  458.                 builder.append("00:00:00");
  459.             }
  460.             // offset
  461.             final int    minutesFromUTC = t.getMinutesFromUTC();
  462.             if (minutesFromUTC == 0) {
  463.                 builder.append("Z");
  464.             } else {
  465.                 // sign must be accounted for separately because there is no -0 in Java.
  466.                 final String sign         = minutesFromUTC < 0 ? "-" : "+";
  467.                 final int    utcOffset    = FastMath.abs(minutesFromUTC);
  468.                 final int    hourOffset   = utcOffset / 60;
  469.                 final int    minuteOffset = utcOffset % 60;
  470.                 builder.append(sign);
  471.                 PADDED_TWO_DIGITS_INTEGER.appendTo(builder, hourOffset);
  472.                 builder.append(':');
  473.                 PADDED_TWO_DIGITS_INTEGER.appendTo(builder, minuteOffset);
  474.             }
  475.             return builder.toString();
  476.         } catch (IOException ioe) {
  477.             // this should never happen
  478.             throw new OrekitInternalError(ioe);
  479.         }
  480.     }

  481. }