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.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