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  
19  import java.io.Serializable;
20  import java.text.DecimalFormat;
21  import java.text.DecimalFormatSymbols;
22  import java.util.Locale;
23  
24  import java.util.concurrent.TimeUnit;
25  import org.hipparchus.util.FastMath;
26  import org.orekit.utils.Constants;
27  
28  /** Holder for date and time components.
29   * <p>This class is a simple holder with no processing methods.</p>
30   * <p>Instance of this class are guaranteed to be immutable.</p>
31   * @see AbsoluteDate
32   * @see DateComponents
33   * @see TimeComponents
34   * @author Luc Maisonobe
35   */
36  public class DateTimeComponents implements Serializable, Comparable<DateTimeComponents> {
37  
38      /**
39       * The Julian Epoch.
40       *
41       * @see TimeScales#getJulianEpoch()
42       */
43      public static final DateTimeComponents JULIAN_EPOCH =
44              new DateTimeComponents(DateComponents.JULIAN_EPOCH, TimeComponents.H12);
45  
46      /** Serializable UID. */
47      private static final long serialVersionUID = 5061129505488924484L;
48  
49      /** Date component. */
50      private final DateComponents date;
51  
52      /** Time component. */
53      private final TimeComponents time;
54  
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  
64      /** Build an instance from raw level components.
65       * @param year year number (may be 0 or negative for BC years)
66       * @param month month number from 1 to 12
67       * @param day day number from 1 to 31
68       * @param hour hour number from 0 to 23
69       * @param minute minute number from 0 to 59
70       * @param second second number from 0.0 to 60.0 (excluded)
71       * @exception IllegalArgumentException if inconsistent arguments
72       * are given (parameters out of range, february 29 for non-leap years,
73       * dates during the gregorian leap in 1582 ...)
74       */
75      public DateTimeComponents(final int year, final int month, final int day,
76                                final int hour, final int minute, final double second)
77          throws IllegalArgumentException {
78          this.date = new DateComponents(year, month, day);
79          this.time = new TimeComponents(hour, minute, second);
80      }
81  
82      /** Build an instance from raw level components.
83       * @param year year number (may be 0 or negative for BC years)
84       * @param month month enumerate
85       * @param day day number from 1 to 31
86       * @param hour hour number from 0 to 23
87       * @param minute minute number from 0 to 59
88       * @param second second number from 0.0 to 60.0 (excluded)
89       * @exception IllegalArgumentException if inconsistent arguments
90       * are given (parameters out of range, february 29 for non-leap years,
91       * dates during the gregorian leap in 1582 ...)
92       */
93      public DateTimeComponents(final int year, final Month month, final int day,
94                                final int hour, final int minute, final double second)
95          throws IllegalArgumentException {
96          this.date = new DateComponents(year, month, day);
97          this.time = new TimeComponents(hour, minute, second);
98      }
99  
100     /** Build an instance from raw level components.
101      * <p>The hour is set to 00:00:00.000.</p>
102      * @param year year number (may be 0 or negative for BC years)
103      * @param month month number from 1 to 12
104      * @param day day number from 1 to 31
105      * @exception IllegalArgumentException if inconsistent arguments
106      * are given (parameters out of range, february 29 for non-leap years,
107      * dates during the gregorian leap in 1582 ...)
108      */
109     public DateTimeComponents(final int year, final int month, final int day)
110         throws IllegalArgumentException {
111         this.date = new DateComponents(year, month, day);
112         this.time = TimeComponents.H00;
113     }
114 
115     /** Build an instance from raw level components.
116      * <p>The hour is set to 00:00:00.000.</p>
117      * @param year year number (may be 0 or negative for BC years)
118      * @param month month enumerate
119      * @param day day number from 1 to 31
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      */
124     public DateTimeComponents(final int year, final Month month, final int day)
125         throws IllegalArgumentException {
126         this.date = new DateComponents(year, month, day);
127         this.time = TimeComponents.H00;
128     }
129 
130     /** Build an instance from a seconds offset with respect to another one.
131      * @param reference reference date/time
132      * @param offset offset from the reference in seconds
133      * @see #offsetFrom(DateTimeComponents)
134      */
135     public DateTimeComponents(final DateTimeComponents reference,
136                               final double offset) {
137 
138         // extract linear data from reference date/time
139         int    day     = reference.getDate().getJ2000Day();
140         double seconds = reference.getTime().getSecondsInLocalDay();
141 
142         // apply offset
143         seconds += offset;
144 
145         // fix range
146         final int dayShift = (int) FastMath.floor(seconds / Constants.JULIAN_DAY);
147         seconds -= Constants.JULIAN_DAY * dayShift;
148         day     += dayShift;
149         final TimeComponents tmpTime = new TimeComponents(seconds);
150 
151         // set up components
152         this.date = new DateComponents(day);
153         this.time = new TimeComponents(tmpTime.getHour(), tmpTime.getMinute(), tmpTime.getSecond(),
154                                        reference.getTime().getMinutesFromUTC());
155 
156     }
157 
158     /** Build an instance from a seconds offset with respect to another one.
159      * @param reference reference date/time
160      * @param offset offset from the reference
161      * @param timeUnit the {@link TimeUnit} for the offset
162      * @see #offsetFrom(DateTimeComponents, TimeUnit)
163      * @since 12.1
164      */
165     public DateTimeComponents(final DateTimeComponents reference,
166         final long offset, final TimeUnit timeUnit) {
167 
168         // extract linear data from reference date/time
169         int    day     = reference.getDate().getJ2000Day();
170         double seconds = reference.getTime().getSecondsInLocalDay();
171 
172         // apply offset
173         long offsetInNanos = TimeUnit.NANOSECONDS.convert(offset, timeUnit);
174         final long daysInNanoseconds = TimeUnit.NANOSECONDS.convert((long) Constants.JULIAN_DAY, TimeUnit.SECONDS);
175         final int nanoDayShift = (int) FastMath.floorDiv(offsetInNanos, daysInNanoseconds);
176         offsetInNanos -= daysInNanoseconds * nanoDayShift;
177 
178         seconds += offsetInNanos / (double) TimeUnit.SECONDS.toNanos(1);
179 
180         // fix range
181         final int dayShift = (int) FastMath.floor(seconds / Constants.JULIAN_DAY);
182         seconds -= Constants.JULIAN_DAY * dayShift;
183         day     += dayShift + nanoDayShift;
184         final TimeComponents tmpTime = new TimeComponents(seconds);
185 
186         // set up components
187         this.date = new DateComponents(day);
188         this.time = new TimeComponents(tmpTime.getHour(), tmpTime.getMinute(), tmpTime.getSecond(),
189             reference.getTime().getMinutesFromUTC());
190 
191     }
192 
193     /** Parse a string in ISO-8601 format to build a date/time.
194      * <p>The supported formats are all date formats supported by {@link DateComponents#parseDate(String)}
195      * and all time formats supported by {@link TimeComponents#parseTime(String)} separated
196      * by the standard time separator 'T', or date components only (in which case a 00:00:00 hour is
197      * implied). Typical examples are 2000-01-01T12:00:00Z or 1976W186T210000.
198      * </p>
199      * @param string string to parse
200      * @return a parsed date/time
201      * @exception IllegalArgumentException if string cannot be parsed
202      */
203     public static DateTimeComponents parseDateTime(final String string) {
204 
205         // is there a time ?
206         final int tIndex = string.indexOf('T');
207         if (tIndex > 0) {
208             return new DateTimeComponents(DateComponents.parseDate(string.substring(0, tIndex)),
209                                           TimeComponents.parseTime(string.substring(tIndex + 1)));
210         }
211 
212         return new DateTimeComponents(DateComponents.parseDate(string), TimeComponents.H00);
213 
214     }
215 
216     /** Compute the seconds offset between two instances.
217      * @param dateTime dateTime to subtract from the instance
218      * @return offset in seconds between the two instants
219      * (positive if the instance is posterior to the argument)
220      * @see #DateTimeComponents(DateTimeComponents, double)
221      */
222     public double offsetFrom(final DateTimeComponents dateTime) {
223         final int dateOffset = date.getJ2000Day() - dateTime.date.getJ2000Day();
224         final double timeOffset = time.getSecondsInUTCDay() - dateTime.time.getSecondsInUTCDay();
225         return Constants.JULIAN_DAY * dateOffset + timeOffset;
226     }
227 
228     /** Compute the seconds offset between two instances.
229      * @param dateTime dateTime to subtract from the instance
230      * @param timeUnit the desired {@link TimeUnit}
231      * @return offset in the given timeunit between the two instants (positive
232      * if the instance is posterior to the argument), rounded to the nearest integer {@link TimeUnit}
233      * @see #DateTimeComponents(DateTimeComponents, long, TimeUnit)
234      * @since 12.1
235      */
236     public long offsetFrom(final DateTimeComponents dateTime, final TimeUnit timeUnit) {
237         final int dateOffset = date.getJ2000Day() - dateTime.date.getJ2000Day();
238         final double timeOffset = time.getSecondsInUTCDay() - dateTime.time.getSecondsInUTCDay();
239 
240         final long multiplier = timeUnit.convert(1, TimeUnit.SECONDS);
241 
242         return timeUnit.convert(Math.round(Constants.JULIAN_DAY * dateOffset), TimeUnit.SECONDS) +
243             FastMath.round(timeOffset * multiplier);
244     }
245 
246     /** Get the date component.
247      * @return date component
248      */
249     public DateComponents getDate() {
250         return date;
251     }
252 
253     /** Get the time component.
254      * @return time component
255      */
256     public TimeComponents getTime() {
257         return time;
258     }
259 
260     /** {@inheritDoc} */
261     public int compareTo(final DateTimeComponents other) {
262         final int dateComparison = date.compareTo(other.date);
263         if (dateComparison < 0) {
264             return -1;
265         } else if (dateComparison > 0) {
266             return 1;
267         }
268         return time.compareTo(other.time);
269     }
270 
271     /** {@inheritDoc} */
272     public boolean equals(final Object other) {
273         try {
274             final DateTimeComponents otherDateTime = (DateTimeComponents) other;
275             return otherDateTime != null &&
276                    date.equals(otherDateTime.date) && time.equals(otherDateTime.time);
277         } catch (ClassCastException cce) {
278             return false;
279         }
280     }
281 
282     /** {@inheritDoc} */
283     public int hashCode() {
284         return (date.hashCode() << 16) ^ time.hashCode();
285     }
286 
287     /** Return a string representation of this pair.
288      * <p>The format used is ISO8601 including the UTC offset.</p>
289      * @return string representation of this pair
290      */
291     public String toString() {
292         return date.toString() + 'T' + time.toString();
293     }
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 
309     /**
310      * Return a string representation of this date-time, rounded to millisecond
311      * precision.
312      *
313      * <p>The format used is ISO8601 including the UTC offset.</p>
314      *
315      * @param minuteDuration 60, 61, or 62 seconds depending on the date being close to a
316      *                       leap second introduction and the magnitude of the leap
317      *                       second.
318      * @return string representation of this date, time, and UTC offset
319      * @see #toString(int, int)
320      */
321     public String toString(final int minuteDuration) {
322         return toString(minuteDuration, 3);
323     }
324 
325     /**
326      * Return a string representation of this date-time, rounded to the given precision.
327      *
328      * <p>The format used is ISO8601 including the UTC offset.</p>
329      *
330      * @param minuteDuration 59, 60, 61, or 62 seconds depending on the date being close
331      *                       to a leap second introduction and the magnitude of the leap
332      *                       second.
333      * @param fractionDigits the number of digits to include after the decimal point in
334      *                       the string representation of the seconds. The date and time
335      *                       is first rounded as necessary. {@code fractionDigits} must
336      *                       be greater than or equal to {@code 0}.
337      * @return string representation of this date, time, and UTC offset
338      * @see #toStringRfc3339()
339      * @see #toStringWithoutUtcOffset()
340      * @see #toStringWithoutUtcOffset(int, int)
341      * @since 11.0
342      */
343     public String toString(final int minuteDuration, final int fractionDigits) {
344         return toStringWithoutUtcOffset(minuteDuration, fractionDigits) +
345                 time.formatUtcOffset();
346     }
347 
348     /**
349      * Return a string representation of this date-time, rounded to the given precision.
350      *
351      * <p>The format used is ISO8601 without the UTC offset.</p>
352      *
353      * @param minuteDuration 59, 60, 61, or 62 seconds depending on the date being close
354      *                       to a leap second introduction and the magnitude of the leap
355      *                       second.
356      * @param fractionDigits the number of digits to include after the decimal point in
357      *                       the string representation of the seconds. The date and time
358      *                       is first rounded as necessary. {@code fractionDigits} must
359      *                       be greater than or equal to {@code 0}.
360      * @return string representation of this date, time, and UTC offset
361      * @see #toStringRfc3339()
362      * @see #toStringWithoutUtcOffset()
363      * @see #toString(int, int)
364      * @since 11.1
365      */
366     public String toStringWithoutUtcOffset(final int minuteDuration,
367                                            final int fractionDigits) {
368         final DecimalFormat secondsFormat =
369                 new DecimalFormat("00", new DecimalFormatSymbols(Locale.US));
370         secondsFormat.setMaximumFractionDigits(fractionDigits);
371         secondsFormat.setMinimumFractionDigits(fractionDigits);
372         final DateTimeComponents rounded = roundIfNeeded(minuteDuration, fractionDigits);
373         return rounded.getDate().toString() + 'T' +
374                 rounded.getTime().toStringWithoutUtcOffset(secondsFormat);
375     }
376 
377     /**
378      * Round this date-time to the given precision if needed to prevent rounding up to an
379      * invalid seconds number. This is useful, for example, when writing custom date-time
380      * formatting methods so one does not, e.g., end up with "60.0" seconds during a
381      * normal minute when the value of seconds is {@code 59.999}. This method will instead
382      * round up the minute, hour, day, month, and year as needed.
383      *
384      * @param minuteDuration 59, 60, 61, or 62 seconds depending on the date being close
385      *                       to a leap second introduction and the magnitude of the leap
386      *                       second.
387      * @param fractionDigits the number of decimal digits after the decimal point in the
388      *                       seconds number that will be printed. This date-time is
389      *                       rounded to {@code fractionDigits} after the decimal point if
390      *                       necessary to prevent rounding up to {@code minuteDuration}.
391      *                       {@code fractionDigits} must be greater than or equal to
392      *                       {@code 0}.
393      * @return a date-time within {@code 0.5 * 10**-fractionDigits} seconds of this, and
394      * with a seconds number that will not round up to {@code minuteDuration} when rounded
395      * to {@code fractionDigits} after the decimal point.
396      * @since 11.3
397      */
398     public DateTimeComponents roundIfNeeded(final int minuteDuration,
399                                             final int fractionDigits) {
400         double second = time.getSecond();
401         final double wrap = minuteDuration - 0.5 * FastMath.pow(10, -fractionDigits);
402         if (second >= wrap) {
403             // we should wrap around to the next minute
404             int minute = time.getMinute();
405             int hour   = time.getHour();
406             int j2000  = date.getJ2000Day();
407             second = 0;
408             ++minute;
409             if (minute > 59) {
410                 minute = 0;
411                 ++hour;
412                 if (hour > 23) {
413                     hour = 0;
414                     ++j2000;
415                 }
416             }
417             return new DateTimeComponents(
418                     new DateComponents(j2000),
419                     new TimeComponents(hour, minute, second));
420         }
421         return this;
422     }
423 
424     /**
425      * Represent the given date and time as a string according to the format in RFC 3339.
426      * RFC3339 is a restricted subset of ISO 8601 with a well defined grammar. This method
427      * includes enough precision to represent the point in time without rounding up to the
428      * next minute.
429      *
430      * <p>RFC3339 is unable to represent BC years, years of 10000 or more, time zone
431      * offsets of 100 hours or more, or NaN. In these cases the value returned from this
432      * method will not be valid RFC3339 format.
433      *
434      * @return RFC 3339 format string.
435      * @see <a href="https://tools.ietf.org/html/rfc3339#page-8">RFC 3339</a>
436      * @see AbsoluteDate#toStringRfc3339(TimeScale)
437      * @see #toString(int, int)
438      * @see #toStringWithoutUtcOffset()
439      */
440     public String toStringRfc3339() {
441         final DateComponents d = this.getDate();
442         final TimeComponents t = this.getTime();
443         // date
444         final String dateString = String.format("%04d-%02d-%02dT",
445                 d.getYear(), d.getMonth(), d.getDay());
446         // time
447         final String timeString;
448         if (t.getSecondsInLocalDay() != 0) {
449             final DecimalFormat format = new DecimalFormat("00.##############", new DecimalFormatSymbols(Locale.US));
450             timeString = String.format("%02d:%02d:", t.getHour(), t.getMinute()) +
451                     format.format(t.getSecond());
452         } else {
453             // shortcut for midnight local time
454             timeString = "00:00:00";
455         }
456         // offset
457         final int minutesFromUTC = t.getMinutesFromUTC();
458         final String timeZoneString;
459         if (minutesFromUTC == 0) {
460             timeZoneString = "Z";
461         } else {
462             // sign must be accounted for separately because there is no -0 in Java.
463             final String sign = minutesFromUTC < 0 ? "-" : "+";
464             final int utcOffset = FastMath.abs(minutesFromUTC);
465             final int hourOffset = utcOffset / 60;
466             final int minuteOffset = utcOffset % 60;
467             timeZoneString = sign + String.format("%02d:%02d", hourOffset, minuteOffset);
468         }
469         return dateString + timeString + timeZoneString;
470     }
471 
472 }
473