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.Serializable;

  19. import java.util.concurrent.TimeUnit;
  20. import org.hipparchus.util.FastMath;
  21. import org.orekit.utils.Constants;

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

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

  38.     /** Serializable UID. */
  39.     private static final long serialVersionUID = 20240720L;

  40.     /** Date component. */
  41.     private final DateComponents date;

  42.     /** Time component. */
  43.     private final TimeComponents time;

  44.     /** Build a new instance from its components.
  45.      * @param date date component
  46.      * @param time time component
  47.      */
  48.     public DateTimeComponents(final DateComponents date, final TimeComponents time) {
  49.         this.date = date;
  50.         this.time = time;
  51.     }

  52.     /** Build an instance from raw level components.
  53.      * @param year year number (may be 0 or negative for BC years)
  54.      * @param month month number from 1 to 12
  55.      * @param day day number from 1 to 31
  56.      * @param hour hour number from 0 to 23
  57.      * @param minute minute number from 0 to 59
  58.      * @param second second number from 0.0 to 60.0 (excluded)
  59.      * @exception IllegalArgumentException if inconsistent arguments
  60.      * are given (parameters out of range, february 29 for non-leap years,
  61.      * dates during the gregorian leap in 1582 ...)
  62.      */
  63.     public DateTimeComponents(final int year, final int month, final int day,
  64.                               final int hour, final int minute, final double second)
  65.         throws IllegalArgumentException {
  66.         this(year, month, day, hour, minute, new TimeOffset(second));
  67.     }

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

  86.     /** Build an instance from raw level components.
  87.      * @param year year number (may be 0 or negative for BC years)
  88.      * @param month month enumerate
  89.      * @param day day number from 1 to 31
  90.      * @param hour hour number from 0 to 23
  91.      * @param minute minute number from 0 to 59
  92.      * @param second second number from 0.0 to 60.0 (excluded)
  93.      * @exception IllegalArgumentException if inconsistent arguments
  94.      * are given (parameters out of range, february 29 for non-leap years,
  95.      * dates during the gregorian leap in 1582 ...)
  96.      */
  97.     public DateTimeComponents(final int year, final Month month, final int day,
  98.                               final int hour, final int minute, final double second)
  99.         throws IllegalArgumentException {
  100.         this(year, month, day, hour, minute, new TimeOffset(second));
  101.     }

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

  120.     /** Build an instance from raw level components.
  121.      * <p>The hour is set to 00:00:00.000.</p>
  122.      * @param year year number (may be 0 or negative for BC years)
  123.      * @param month month number from 1 to 12
  124.      * @param day day number from 1 to 31
  125.      * @exception IllegalArgumentException if inconsistent arguments
  126.      * are given (parameters out of range, february 29 for non-leap years,
  127.      * dates during the gregorian leap in 1582 ...)
  128.      */
  129.     public DateTimeComponents(final int year, final int month, final int day)
  130.         throws IllegalArgumentException {
  131.         this.date = new DateComponents(year, month, day);
  132.         this.time = TimeComponents.H00;
  133.     }

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

  148.     /** Build an instance from a seconds offset with respect to another one.
  149.      * @param reference reference date/time
  150.      * @param offset offset from the reference in seconds
  151.      * @see #offsetFrom(DateTimeComponents)
  152.      */
  153.     public DateTimeComponents(final DateTimeComponents reference, final double offset) {
  154.         this(reference, new TimeOffset(offset));
  155.     }

  156.     /** Build an instance from a seconds offset with respect to another one.
  157.      * @param reference reference date/time
  158.      * @param offset offset from the reference in seconds
  159.      * @see #offsetFrom(DateTimeComponents)
  160.      * @since 13.0
  161.      */
  162.     public DateTimeComponents(final DateTimeComponents reference, final TimeOffset offset) {

  163.         // extract linear data from reference date/time
  164.         int    day     = reference.getDate().getJ2000Day();
  165.         TimeOffset seconds = reference.getTime().getSplitSecondsInLocalDay();

  166.         // apply offset
  167.         seconds = seconds.add(offset);

  168.         // fix range
  169.         final int dayShift = (int) FastMath.floor(seconds.toDouble() / Constants.JULIAN_DAY);
  170.         if (dayShift != 0) {
  171.             seconds = seconds.subtract(new TimeOffset(dayShift * TimeOffset.DAY.getSeconds(), 0L));
  172.         }
  173.         day     += dayShift;
  174.         final TimeComponents tmpTime = new TimeComponents(seconds);

  175.         // set up components
  176.         this.date = new DateComponents(day);
  177.         this.time = new TimeComponents(tmpTime.getHour(), tmpTime.getMinute(), tmpTime.getSplitSecond(),
  178.                                        reference.getTime().getMinutesFromUTC());

  179.     }

  180.     /** Build an instance from a seconds offset with respect to another one.
  181.      * @param reference reference date/time
  182.      * @param offset offset from the reference
  183.      * @param timeUnit the {@link TimeUnit} for the offset
  184.      * @see #offsetFrom(DateTimeComponents, TimeUnit)
  185.      * @since 12.1
  186.      */
  187.     public DateTimeComponents(final DateTimeComponents reference,
  188.                               final long offset, final TimeUnit timeUnit) {

  189.         // extract linear data from reference date/time
  190.         int       day     = reference.getDate().getJ2000Day();
  191.         TimeOffset seconds = reference.getTime().getSplitSecondsInLocalDay();

  192.         // apply offset
  193.         seconds = seconds.add(new TimeOffset(offset, timeUnit));

  194.         // fix range
  195.         final long dayShift = seconds.getSeconds() / TimeOffset.DAY.getSeconds() +
  196.                               (seconds.getSeconds() < 0L ? -1L : 0L);
  197.         if (dayShift != 0) {
  198.             seconds = seconds.subtract(new TimeOffset(dayShift, TimeOffset.DAY));
  199.             day    += dayShift;
  200.         }
  201.         final TimeComponents tmpTime = new TimeComponents(seconds);

  202.         // set up components
  203.         this.date = new DateComponents(day);
  204.         this.time = new TimeComponents(tmpTime.getHour(), tmpTime.getMinute(), tmpTime.getSplitSecond(),
  205.             reference.getTime().getMinutesFromUTC());

  206.     }

  207.     /** Parse a string in ISO-8601 format to build a date/time.
  208.      * <p>The supported formats are all date formats supported by {@link DateComponents#parseDate(String)}
  209.      * and all time formats supported by {@link TimeComponents#parseTime(String)} separated
  210.      * by the standard time separator 'T', or date components only (in which case a 00:00:00 hour is
  211.      * implied). Typical examples are 2000-01-01T12:00:00Z or 1976W186T210000.
  212.      * </p>
  213.      * @param string string to parse
  214.      * @return a parsed date/time
  215.      * @exception IllegalArgumentException if string cannot be parsed
  216.      */
  217.     public static DateTimeComponents parseDateTime(final String string) {

  218.         // is there a time ?
  219.         final int tIndex = string.indexOf('T');
  220.         if (tIndex > 0) {
  221.             return new DateTimeComponents(DateComponents.parseDate(string.substring(0, tIndex)),
  222.                                           TimeComponents.parseTime(string.substring(tIndex + 1)));
  223.         }

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

  225.     }

  226.     /** Compute the seconds offset between two instances.
  227.      * @param dateTime dateTime to subtract from the instance
  228.      * @return offset in seconds between the two instants
  229.      * (positive if the instance is posterior to the argument)
  230.      * @see #DateTimeComponents(DateTimeComponents, TimeOffset)
  231.      */
  232.     public double offsetFrom(final DateTimeComponents dateTime) {
  233.         final int dateOffset = date.getJ2000Day() - dateTime.date.getJ2000Day();
  234.         final TimeOffset timeOffset = time.getSplitSecondsInUTCDay().
  235.                                      subtract(dateTime.time.getSplitSecondsInUTCDay());
  236.         return Constants.JULIAN_DAY * dateOffset + timeOffset.toDouble();
  237.     }

  238.     /** Compute the seconds offset between two instances.
  239.      * @param dateTime dateTime to subtract from the instance
  240.      * @param timeUnit the desired {@link TimeUnit}
  241.      * @return offset in the given timeunit between the two instants (positive
  242.      * if the instance is posterior to the argument), rounded to the nearest integer {@link TimeUnit}
  243.      * @see #DateTimeComponents(DateTimeComponents, long, TimeUnit)
  244.      * @since 12.1
  245.      */
  246.     public long offsetFrom(final DateTimeComponents dateTime, final TimeUnit timeUnit) {
  247.         final int dateOffset = date.getJ2000Day() - dateTime.date.getJ2000Day();
  248.         final TimeOffset timeOffset = time.getSplitSecondsInUTCDay().
  249.                                      subtract(dateTime.time.getSplitSecondsInUTCDay());
  250.         return TimeOffset.DAY.getRoundedTime(timeUnit) * dateOffset + timeOffset.getRoundedTime(timeUnit);
  251.     }

  252.     /** Get the date component.
  253.      * @return date component
  254.      */
  255.     public DateComponents getDate() {
  256.         return date;
  257.     }

  258.     /** Get the time component.
  259.      * @return time component
  260.      */
  261.     public TimeComponents getTime() {
  262.         return time;
  263.     }

  264.     /** {@inheritDoc} */
  265.     public int compareTo(final DateTimeComponents other) {
  266.         final int dateComparison = date.compareTo(other.date);
  267.         if (dateComparison < 0) {
  268.             return -1;
  269.         } else if (dateComparison > 0) {
  270.             return 1;
  271.         }
  272.         return time.compareTo(other.time);
  273.     }

  274.     /** {@inheritDoc} */
  275.     public boolean equals(final Object other) {
  276.         try {
  277.             final DateTimeComponents otherDateTime = (DateTimeComponents) other;
  278.             return otherDateTime != null &&
  279.                    date.equals(otherDateTime.date) && time.equals(otherDateTime.time);
  280.         } catch (ClassCastException cce) {
  281.             return false;
  282.         }
  283.     }

  284.     /** {@inheritDoc} */
  285.     public int hashCode() {
  286.         return (date.hashCode() << 16) ^ time.hashCode();
  287.     }

  288.     /** Return a string representation of this pair.
  289.      * <p>The format used is ISO8601 including the UTC offset.</p>
  290.      * @return string representation of this pair
  291.      */
  292.     public String toString() {
  293.         return date.toString() + 'T' + time.toString();
  294.     }

  295.     /**
  296.      * Get a string representation of the date-time without the offset from UTC. The
  297.      * format used is ISO6801, except without the offset from UTC.
  298.      *
  299.      * @return a string representation of the date-time.
  300.      * @see #toStringWithoutUtcOffset(int, int)
  301.      * @see #toString(int, int)
  302.      * @see #toStringRfc3339()
  303.      */
  304.     public String toStringWithoutUtcOffset() {
  305.         return date.toString() + 'T' + time.toStringWithoutUtcOffset();
  306.     }


  307.     /**
  308.      * Return a string representation of this date-time, rounded to millisecond
  309.      * precision.
  310.      *
  311.      * <p>The format used is ISO8601 including the UTC offset.</p>
  312.      *
  313.      * @param minuteDuration 60, 61, or 62 seconds depending on the date being close to a
  314.      *                       leap second introduction and the magnitude of the leap
  315.      *                       second.
  316.      * @return string representation of this date, time, and UTC offset
  317.      * @see #toString(int, int)
  318.      */
  319.     public String toString(final int minuteDuration) {
  320.         return toString(minuteDuration, 3);
  321.     }

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

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

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

  390.         final TimeComponents wrappedTime = time.wrapIfNeeded(minuteDuration, fractionDigits);
  391.         if (wrappedTime == time) {
  392.             // no wrapping was needed
  393.             return this;
  394.         } else {
  395.             if (wrappedTime.getHour() < time.getHour()) {
  396.                 // we have wrapped around next day
  397.                 return new DateTimeComponents(new DateComponents(date, 1), wrappedTime);
  398.             } else {
  399.                 // only the time was wrapped
  400.                 return new DateTimeComponents(date, wrappedTime);
  401.             }
  402.         }

  403.     }

  404.     /**
  405.      * Represent the given date and time as a string according to the format in RFC 3339.
  406.      * RFC3339 is a restricted subset of ISO 8601 with a well defined grammar. This method
  407.      * includes enough precision to represent the point in time without rounding up to the
  408.      * next minute.
  409.      *
  410.      * <p>RFC3339 is unable to represent BC years, years of 10000 or more, time zone
  411.      * offsets of 100 hours or more, or NaN. In these cases the value returned from this
  412.      * method will not be valid RFC3339 format.
  413.      *
  414.      * @return RFC 3339 format string.
  415.      * @see <a href="https://tools.ietf.org/html/rfc3339#page-8">RFC 3339</a>
  416.      * @see AbsoluteDate#toStringRfc3339(TimeScale)
  417.      * @see #toString(int, int)
  418.      * @see #toStringWithoutUtcOffset()
  419.      */
  420.     public String toStringRfc3339() {
  421.         final DateComponents d = this.getDate();
  422.         final TimeComponents t = this.getTime();
  423.         // date
  424.         final String dateString = String.format("%04d-%02d-%02dT",
  425.                 d.getYear(), d.getMonth(), d.getDay());
  426.         // time
  427.         final String timeString;
  428.         if (!t.getSplitSecondsInLocalDay().isZero()) {
  429.             final String formatted = t.toStringWithoutUtcOffset(18);
  430.             int last = formatted.length() - 1;
  431.             while (formatted.charAt(last) == '0') {
  432.                 // we want to remove final zeros
  433.                 --last;
  434.             }
  435.             if (formatted.charAt(last) == '.') {
  436.                 // remove the decimal point if no decimals follow
  437.                 --last;
  438.             }
  439.             timeString = formatted.substring(0, last + 1);
  440.         } else {
  441.             // shortcut for midnight local time
  442.             timeString = "00:00:00";
  443.         }
  444.         // offset
  445.         final int minutesFromUTC = t.getMinutesFromUTC();
  446.         final String timeZoneString;
  447.         if (minutesFromUTC == 0) {
  448.             timeZoneString = "Z";
  449.         } else {
  450.             // sign must be accounted for separately because there is no -0 in Java.
  451.             final String sign = minutesFromUTC < 0 ? "-" : "+";
  452.             final int utcOffset = FastMath.abs(minutesFromUTC);
  453.             final int hourOffset = utcOffset / 60;
  454.             final int minuteOffset = utcOffset % 60;
  455.             timeZoneString = sign + String.format("%02d:%02d", hourOffset, minuteOffset);
  456.         }
  457.         return dateString + timeString + timeZoneString;
  458.     }

  459. }