DateTimeComponents.java

  1. /* Copyright 2002-2024 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.concurrent.TimeUnit;
  23. import org.hipparchus.util.FastMath;
  24. import org.orekit.utils.Constants;

  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.     /** Serializable UID. */
  42.     private static final long serialVersionUID = 5061129505488924484L;

  43.     /** Date component. */
  44.     private final DateComponents date;

  45.     /** Time component. */
  46.     private final TimeComponents time;

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

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

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

  89.     /** Build an instance from raw level components.
  90.      * <p>The hour is set to 00:00:00.000.</p>
  91.      * @param year year number (may be 0 or negative for BC years)
  92.      * @param month month number from 1 to 12
  93.      * @param day day number from 1 to 31
  94.      * @exception IllegalArgumentException if inconsistent arguments
  95.      * are given (parameters out of range, february 29 for non-leap years,
  96.      * dates during the gregorian leap in 1582 ...)
  97.      */
  98.     public DateTimeComponents(final int year, final int month, final int day)
  99.         throws IllegalArgumentException {
  100.         this.date = new DateComponents(year, month, day);
  101.         this.time = TimeComponents.H00;
  102.     }

  103.     /** Build an instance from raw level components.
  104.      * <p>The hour is set to 00:00:00.000.</p>
  105.      * @param year year number (may be 0 or negative for BC years)
  106.      * @param month month enumerate
  107.      * @param day day number from 1 to 31
  108.      * @exception IllegalArgumentException if inconsistent arguments
  109.      * are given (parameters out of range, february 29 for non-leap years,
  110.      * dates during the gregorian leap in 1582 ...)
  111.      */
  112.     public DateTimeComponents(final int year, final Month month, final int day)
  113.         throws IllegalArgumentException {
  114.         this.date = new DateComponents(year, month, day);
  115.         this.time = TimeComponents.H00;
  116.     }

  117.     /** Build an instance from a seconds offset with respect to another one.
  118.      * @param reference reference date/time
  119.      * @param offset offset from the reference in seconds
  120.      * @see #offsetFrom(DateTimeComponents)
  121.      */
  122.     public DateTimeComponents(final DateTimeComponents reference,
  123.                               final double offset) {

  124.         // extract linear data from reference date/time
  125.         int    day     = reference.getDate().getJ2000Day();
  126.         double seconds = reference.getTime().getSecondsInLocalDay();

  127.         // apply offset
  128.         seconds += offset;

  129.         // fix range
  130.         final int dayShift = (int) FastMath.floor(seconds / Constants.JULIAN_DAY);
  131.         seconds -= Constants.JULIAN_DAY * dayShift;
  132.         day     += dayShift;
  133.         final TimeComponents tmpTime = new TimeComponents(seconds);

  134.         // set up components
  135.         this.date = new DateComponents(day);
  136.         this.time = new TimeComponents(tmpTime.getHour(), tmpTime.getMinute(), tmpTime.getSecond(),
  137.                                        reference.getTime().getMinutesFromUTC());

  138.     }

  139.     /** Build an instance from a seconds offset with respect to another one.
  140.      * @param reference reference date/time
  141.      * @param offset offset from the reference
  142.      * @param timeUnit the {@link TimeUnit} for the offset
  143.      * @see #offsetFrom(DateTimeComponents, TimeUnit)
  144.      * @since 12.1
  145.      */
  146.     public DateTimeComponents(final DateTimeComponents reference,
  147.         final long offset, final TimeUnit timeUnit) {

  148.         // extract linear data from reference date/time
  149.         int    day     = reference.getDate().getJ2000Day();
  150.         double seconds = reference.getTime().getSecondsInLocalDay();

  151.         // apply offset
  152.         long offsetInNanos = TimeUnit.NANOSECONDS.convert(offset, timeUnit);
  153.         final long daysInNanoseconds = TimeUnit.NANOSECONDS.convert((long) Constants.JULIAN_DAY, TimeUnit.SECONDS);
  154.         final int nanoDayShift = (int) FastMath.floorDiv(offsetInNanos, daysInNanoseconds);
  155.         offsetInNanos -= daysInNanoseconds * nanoDayShift;

  156.         seconds += offsetInNanos / (double) TimeUnit.SECONDS.toNanos(1);

  157.         // fix range
  158.         final int dayShift = (int) FastMath.floor(seconds / Constants.JULIAN_DAY);
  159.         seconds -= Constants.JULIAN_DAY * dayShift;
  160.         day     += dayShift + nanoDayShift;
  161.         final TimeComponents tmpTime = new TimeComponents(seconds);

  162.         // set up components
  163.         this.date = new DateComponents(day);
  164.         this.time = new TimeComponents(tmpTime.getHour(), tmpTime.getMinute(), tmpTime.getSecond(),
  165.             reference.getTime().getMinutesFromUTC());

  166.     }

  167.     /** Parse a string in ISO-8601 format to build a date/time.
  168.      * <p>The supported formats are all date formats supported by {@link DateComponents#parseDate(String)}
  169.      * and all time formats supported by {@link TimeComponents#parseTime(String)} separated
  170.      * by the standard time separator 'T', or date components only (in which case a 00:00:00 hour is
  171.      * implied). Typical examples are 2000-01-01T12:00:00Z or 1976W186T210000.
  172.      * </p>
  173.      * @param string string to parse
  174.      * @return a parsed date/time
  175.      * @exception IllegalArgumentException if string cannot be parsed
  176.      */
  177.     public static DateTimeComponents parseDateTime(final String string) {

  178.         // is there a time ?
  179.         final int tIndex = string.indexOf('T');
  180.         if (tIndex > 0) {
  181.             return new DateTimeComponents(DateComponents.parseDate(string.substring(0, tIndex)),
  182.                                           TimeComponents.parseTime(string.substring(tIndex + 1)));
  183.         }

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

  185.     }

  186.     /** Compute the seconds offset between two instances.
  187.      * @param dateTime dateTime to subtract from the instance
  188.      * @return offset in seconds between the two instants
  189.      * (positive if the instance is posterior to the argument)
  190.      * @see #DateTimeComponents(DateTimeComponents, double)
  191.      */
  192.     public double offsetFrom(final DateTimeComponents dateTime) {
  193.         final int dateOffset = date.getJ2000Day() - dateTime.date.getJ2000Day();
  194.         final double timeOffset = time.getSecondsInUTCDay() - dateTime.time.getSecondsInUTCDay();
  195.         return Constants.JULIAN_DAY * dateOffset + timeOffset;
  196.     }

  197.     /** Compute the seconds offset between two instances.
  198.      * @param dateTime dateTime to subtract from the instance
  199.      * @param timeUnit the desired {@link TimeUnit}
  200.      * @return offset in the given timeunit between the two instants (positive
  201.      * if the instance is posterior to the argument), rounded to the nearest integer {@link TimeUnit}
  202.      * @see #DateTimeComponents(DateTimeComponents, long, TimeUnit)
  203.      * @since 12.1
  204.      */
  205.     public long offsetFrom(final DateTimeComponents dateTime, final TimeUnit timeUnit) {
  206.         final int dateOffset = date.getJ2000Day() - dateTime.date.getJ2000Day();
  207.         final double timeOffset = time.getSecondsInUTCDay() - dateTime.time.getSecondsInUTCDay();

  208.         final long multiplier = timeUnit.convert(1, TimeUnit.SECONDS);

  209.         return timeUnit.convert(Math.round(Constants.JULIAN_DAY * dateOffset), TimeUnit.SECONDS) +
  210.             FastMath.round(timeOffset * multiplier);
  211.     }

  212.     /** Get the date component.
  213.      * @return date component
  214.      */
  215.     public DateComponents getDate() {
  216.         return date;
  217.     }

  218.     /** Get the time component.
  219.      * @return time component
  220.      */
  221.     public TimeComponents getTime() {
  222.         return time;
  223.     }

  224.     /** {@inheritDoc} */
  225.     public int compareTo(final DateTimeComponents other) {
  226.         final int dateComparison = date.compareTo(other.date);
  227.         if (dateComparison < 0) {
  228.             return -1;
  229.         } else if (dateComparison > 0) {
  230.             return 1;
  231.         }
  232.         return time.compareTo(other.time);
  233.     }

  234.     /** {@inheritDoc} */
  235.     public boolean equals(final Object other) {
  236.         try {
  237.             final DateTimeComponents otherDateTime = (DateTimeComponents) other;
  238.             return otherDateTime != null &&
  239.                    date.equals(otherDateTime.date) && time.equals(otherDateTime.time);
  240.         } catch (ClassCastException cce) {
  241.             return false;
  242.         }
  243.     }

  244.     /** {@inheritDoc} */
  245.     public int hashCode() {
  246.         return (date.hashCode() << 16) ^ time.hashCode();
  247.     }

  248.     /** Return a string representation of this pair.
  249.      * <p>The format used is ISO8601 including the UTC offset.</p>
  250.      * @return string representation of this pair
  251.      */
  252.     public String toString() {
  253.         return date.toString() + 'T' + time.toString();
  254.     }

  255.     /**
  256.      * Get a string representation of the date-time without the offset from UTC. The
  257.      * format used is ISO6801, except without the offset from UTC.
  258.      *
  259.      * @return a string representation of the date-time.
  260.      * @see #toStringWithoutUtcOffset(int, int)
  261.      * @see #toString(int, int)
  262.      * @see #toStringRfc3339()
  263.      */
  264.     public String toStringWithoutUtcOffset() {
  265.         return date.toString() + 'T' + time.toStringWithoutUtcOffset();
  266.     }


  267.     /**
  268.      * Return a string representation of this date-time, rounded to millisecond
  269.      * precision.
  270.      *
  271.      * <p>The format used is ISO8601 including the UTC offset.</p>
  272.      *
  273.      * @param minuteDuration 60, 61, or 62 seconds depending on the date being close to a
  274.      *                       leap second introduction and the magnitude of the leap
  275.      *                       second.
  276.      * @return string representation of this date, time, and UTC offset
  277.      * @see #toString(int, int)
  278.      */
  279.     public String toString(final int minuteDuration) {
  280.         return toString(minuteDuration, 3);
  281.     }

  282.     /**
  283.      * Return a string representation of this date-time, rounded to the given precision.
  284.      *
  285.      * <p>The format used is ISO8601 including the UTC offset.</p>
  286.      *
  287.      * @param minuteDuration 59, 60, 61, or 62 seconds depending on the date being close
  288.      *                       to a leap second introduction and the magnitude of the leap
  289.      *                       second.
  290.      * @param fractionDigits the number of digits to include after the decimal point in
  291.      *                       the string representation of the seconds. The date and time
  292.      *                       is first rounded as necessary. {@code fractionDigits} must
  293.      *                       be greater than or equal to {@code 0}.
  294.      * @return string representation of this date, time, and UTC offset
  295.      * @see #toStringRfc3339()
  296.      * @see #toStringWithoutUtcOffset()
  297.      * @see #toStringWithoutUtcOffset(int, int)
  298.      * @since 11.0
  299.      */
  300.     public String toString(final int minuteDuration, final int fractionDigits) {
  301.         return toStringWithoutUtcOffset(minuteDuration, fractionDigits) +
  302.                 time.formatUtcOffset();
  303.     }

  304.     /**
  305.      * Return a string representation of this date-time, rounded to the given precision.
  306.      *
  307.      * <p>The format used is ISO8601 without the UTC offset.</p>
  308.      *
  309.      * @param minuteDuration 59, 60, 61, or 62 seconds depending on the date being close
  310.      *                       to a leap second introduction and the magnitude of the leap
  311.      *                       second.
  312.      * @param fractionDigits the number of digits to include after the decimal point in
  313.      *                       the string representation of the seconds. The date and time
  314.      *                       is first rounded as necessary. {@code fractionDigits} must
  315.      *                       be greater than or equal to {@code 0}.
  316.      * @return string representation of this date, time, and UTC offset
  317.      * @see #toStringRfc3339()
  318.      * @see #toStringWithoutUtcOffset()
  319.      * @see #toString(int, int)
  320.      * @since 11.1
  321.      */
  322.     public String toStringWithoutUtcOffset(final int minuteDuration,
  323.                                            final int fractionDigits) {
  324.         final DecimalFormat secondsFormat =
  325.                 new DecimalFormat("00", new DecimalFormatSymbols(Locale.US));
  326.         secondsFormat.setMaximumFractionDigits(fractionDigits);
  327.         secondsFormat.setMinimumFractionDigits(fractionDigits);
  328.         final DateTimeComponents rounded = roundIfNeeded(minuteDuration, fractionDigits);
  329.         return rounded.getDate().toString() + 'T' +
  330.                 rounded.getTime().toStringWithoutUtcOffset(secondsFormat);
  331.     }

  332.     /**
  333.      * Round this date-time to the given precision if needed to prevent rounding up to an
  334.      * invalid seconds number. This is useful, for example, when writing custom date-time
  335.      * formatting methods so one does not, e.g., end up with "60.0" seconds during a
  336.      * normal minute when the value of seconds is {@code 59.999}. This method will instead
  337.      * round up the minute, hour, day, month, and year as needed.
  338.      *
  339.      * @param minuteDuration 59, 60, 61, or 62 seconds depending on the date being close
  340.      *                       to a leap second introduction and the magnitude of the leap
  341.      *                       second.
  342.      * @param fractionDigits the number of decimal digits after the decimal point in the
  343.      *                       seconds number that will be printed. This date-time is
  344.      *                       rounded to {@code fractionDigits} after the decimal point if
  345.      *                       necessary to prevent rounding up to {@code minuteDuration}.
  346.      *                       {@code fractionDigits} must be greater than or equal to
  347.      *                       {@code 0}.
  348.      * @return a date-time within {@code 0.5 * 10**-fractionDigits} seconds of this, and
  349.      * with a seconds number that will not round up to {@code minuteDuration} when rounded
  350.      * to {@code fractionDigits} after the decimal point.
  351.      * @since 11.3
  352.      */
  353.     public DateTimeComponents roundIfNeeded(final int minuteDuration,
  354.                                             final int fractionDigits) {
  355.         double second = time.getSecond();
  356.         final double wrap = minuteDuration - 0.5 * FastMath.pow(10, -fractionDigits);
  357.         if (second >= wrap) {
  358.             // we should wrap around to the next minute
  359.             int minute = time.getMinute();
  360.             int hour   = time.getHour();
  361.             int j2000  = date.getJ2000Day();
  362.             second = 0;
  363.             ++minute;
  364.             if (minute > 59) {
  365.                 minute = 0;
  366.                 ++hour;
  367.                 if (hour > 23) {
  368.                     hour = 0;
  369.                     ++j2000;
  370.                 }
  371.             }
  372.             return new DateTimeComponents(
  373.                     new DateComponents(j2000),
  374.                     new TimeComponents(hour, minute, second));
  375.         }
  376.         return this;
  377.     }

  378.     /**
  379.      * Represent the given date and time as a string according to the format in RFC 3339.
  380.      * RFC3339 is a restricted subset of ISO 8601 with a well defined grammar. This method
  381.      * includes enough precision to represent the point in time without rounding up to the
  382.      * next minute.
  383.      *
  384.      * <p>RFC3339 is unable to represent BC years, years of 10000 or more, time zone
  385.      * offsets of 100 hours or more, or NaN. In these cases the value returned from this
  386.      * method will not be valid RFC3339 format.
  387.      *
  388.      * @return RFC 3339 format string.
  389.      * @see <a href="https://tools.ietf.org/html/rfc3339#page-8">RFC 3339</a>
  390.      * @see AbsoluteDate#toStringRfc3339(TimeScale)
  391.      * @see #toString(int, int)
  392.      * @see #toStringWithoutUtcOffset()
  393.      */
  394.     public String toStringRfc3339() {
  395.         final DateComponents d = this.getDate();
  396.         final TimeComponents t = this.getTime();
  397.         // date
  398.         final String dateString = String.format("%04d-%02d-%02dT",
  399.                 d.getYear(), d.getMonth(), d.getDay());
  400.         // time
  401.         final String timeString;
  402.         if (t.getSecondsInLocalDay() != 0) {
  403.             final DecimalFormat format = new DecimalFormat("00.##############", new DecimalFormatSymbols(Locale.US));
  404.             timeString = String.format("%02d:%02d:", t.getHour(), t.getMinute()) +
  405.                     format.format(t.getSecond());
  406.         } else {
  407.             // shortcut for midnight local time
  408.             timeString = "00:00:00";
  409.         }
  410.         // offset
  411.         final int minutesFromUTC = t.getMinutesFromUTC();
  412.         final String timeZoneString;
  413.         if (minutesFromUTC == 0) {
  414.             timeZoneString = "Z";
  415.         } else {
  416.             // sign must be accounted for separately because there is no -0 in Java.
  417.             final String sign = minutesFromUTC < 0 ? "-" : "+";
  418.             final int utcOffset = FastMath.abs(minutesFromUTC);
  419.             final int hourOffset = utcOffset / 60;
  420.             final int minuteOffset = utcOffset % 60;
  421.             timeZoneString = sign + String.format("%02d:%02d", hourOffset, minuteOffset);
  422.         }
  423.         return dateString + timeString + timeZoneString;
  424.     }

  425. }