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