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