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