1 /* Copyright 2002-2023 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