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