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