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