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