1 /* Copyright 2002-2020 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 import java.util.regex.Matcher;
24 import java.util.regex.Pattern;
25
26 import org.hipparchus.util.FastMath;
27 import org.orekit.errors.OrekitIllegalArgumentException;
28 import org.orekit.errors.OrekitMessages;
29 import org.orekit.utils.Constants;
30
31
32 /** Class representing a time within the day broken up as hour,
33 * minute and second components.
34 * <p>Instances of this class are guaranteed to be immutable.</p>
35 * @see DateComponents
36 * @see DateTimeComponents
37 * @author Luc Maisonobe
38 */
39 public class TimeComponents implements Serializable, Comparable<TimeComponents> {
40
41 /** Constant for commonly used hour 00:00:00. */
42 public static final TimeComponentsmeComponents">TimeComponents H00 = new TimeComponents(0, 0, 0);
43
44 /** Constant for commonly used hour 12:00:00. */
45 public static final TimeComponentsTimeComponents">TimeComponents H12 = new TimeComponents(12, 0, 0);
46
47 /** Serializable UID. */
48 private static final long serialVersionUID = 20160331L;
49
50 /** Format for hours and minutes. */
51 private static final DecimalFormat TWO_DIGITS = new DecimalFormat("00");
52
53 /** Format for seconds. */
54 private static final DecimalFormat SECONDS_FORMAT =
55 new DecimalFormat("00.000", new DecimalFormatSymbols(Locale.US));
56
57 /** Basic and extends formats for local time, with optional timezone. */
58 private static final Pattern ISO8601_FORMATS = Pattern.compile("^(\\d\\d):?(\\d\\d):?(\\d\\d(?:[.,]\\d+)?)?(?:Z|([-+]\\d\\d(?::?\\d\\d)?))?$");
59
60 /** Hour number. */
61 private final int hour;
62
63 /** Minute number. */
64 private final int minute;
65
66 /** Second number. */
67 private final double second;
68
69 /** Offset between the specified date and UTC.
70 * <p>
71 * Always an integral number of minutes, as per ISO-8601 standard.
72 * </p>
73 * @since 7.2
74 */
75 private final int minutesFromUTC;
76
77 /** Build a time from its clock elements.
78 * <p>Note that seconds between 60.0 (inclusive) and 61.0 (exclusive) are allowed
79 * in this method, since they do occur during leap seconds introduction
80 * in the {@link UTCScale UTC} time scale.</p>
81 * @param hour hour number from 0 to 23
82 * @param minute minute number from 0 to 59
83 * @param second second number from 0.0 to 61.0 (excluded)
84 * @exception IllegalArgumentException if inconsistent arguments
85 * are given (parameters out of range)
86 */
87 public TimeComponents(final int hour, final int minute, final double second)
88 throws IllegalArgumentException {
89 this(hour, minute, second, 0);
90 }
91
92 /** Build a time from its clock elements.
93 * <p>Note that seconds between 60.0 (inclusive) and 61.0 (exclusive) are allowed
94 * in this method, since they do occur during leap seconds introduction
95 * in the {@link UTCScale UTC} time scale.</p>
96 * @param hour hour number from 0 to 23
97 * @param minute minute number from 0 to 59
98 * @param second second number from 0.0 to 61.0 (excluded)
99 * @param minutesFromUTC offset between the specified date and UTC, as an
100 * integral number of minutes, as per ISO-8601 standard
101 * @exception IllegalArgumentException if inconsistent arguments
102 * are given (parameters out of range)
103 * @since 7.2
104 */
105 public TimeComponents(final int hour, final int minute, final double second,
106 final int minutesFromUTC)
107 throws IllegalArgumentException {
108
109 // range check
110 if ((hour < 0) || (hour > 23) ||
111 (minute < 0) || (minute > 59) ||
112 (second < 0) || (second >= 61.0)) {
113 throw new OrekitIllegalArgumentException(OrekitMessages.NON_EXISTENT_HMS_TIME,
114 hour, minute, second);
115 }
116
117 this.hour = hour;
118 this.minute = minute;
119 this.second = second;
120 this.minutesFromUTC = minutesFromUTC;
121
122 }
123
124 /**
125 * Build a time from the second number within the day.
126 *
127 * <p>If the {@code secondInDay} is less than {@code 60.0} then {@link #getSecond()}
128 * will be less than {@code 60.0}, otherwise it will be less than {@code 61.0}. This constructor
129 * may produce an invalid value of {@link #getSecond()} during a negative leap second,
130 * through there has never been one. For more control over the number of seconds in
131 * the final minute use {@link #fromSeconds(int, double, double, int)}.
132 *
133 * <p>This constructor is always in UTC (i.e. {@link #getMinutesFromUTC() will return
134 * 0}).
135 *
136 * @param secondInDay second number from 0.0 to {@link Constants#JULIAN_DAY} {@code +
137 * 1} (excluded)
138 * @throws OrekitIllegalArgumentException if seconds number is out of range
139 * @see #fromSeconds(int, double, double, int)
140 * @see #TimeComponents(int, double)
141 */
142 public TimeComponents(final double secondInDay)
143 throws OrekitIllegalArgumentException {
144 this(0, secondInDay);
145 }
146
147 /**
148 * Build a time from the second number within the day.
149 *
150 * <p>The second number is defined here as the sum
151 * {@code secondInDayA + secondInDayB} from 0.0 to {@link Constants#JULIAN_DAY}
152 * {@code + 1} (excluded). The two parameters are used for increased accuracy.
153 *
154 * <p>If the sum is less than {@code 60.0} then {@link #getSecond()} will be less
155 * than {@code 60.0}, otherwise it will be less than {@code 61.0}. This constructor
156 * may produce an invalid value of {@link #getSecond()} during a negative leap second,
157 * through there has never been one. For more control over the number of seconds in
158 * the final minute use {@link #fromSeconds(int, double, double, int)}.
159 *
160 * <p>This constructor is always in UTC (i.e. {@link #getMinutesFromUTC()} will
161 * return 0).
162 *
163 * @param secondInDayA first part of the second number
164 * @param secondInDayB last part of the second number
165 * @throws OrekitIllegalArgumentException if seconds number is out of range
166 * @see #fromSeconds(int, double, double, int)
167 */
168 public TimeComponents(final int secondInDayA, final double secondInDayB)
169 throws OrekitIllegalArgumentException {
170 // if the total is at least 86400 then assume there is a leap second
171 this(
172 (Constants.JULIAN_DAY - secondInDayA) - secondInDayB > 0 ? secondInDayA : secondInDayA - 1,
173 secondInDayB,
174 (Constants.JULIAN_DAY - secondInDayA) - secondInDayB > 0 ? 0 : 1,
175 (Constants.JULIAN_DAY - secondInDayA) - secondInDayB > 0 ? 60 : 61);
176 }
177
178 /**
179 * Build a time from the second number within the day.
180 *
181 * <p>The seconds past midnight is the sum {@code secondInDayA + secondInDayB +
182 * leap}. The two parameters are used for increased accuracy. Only the first part of
183 * the sum ({@code secondInDayA + secondInDayB}) is used to compute the hours and
184 * minutes. The third parameter ({@code leap}) is added directly to the second value
185 * ({@link #getSecond()}) to implement leap seconds. These three quantities must
186 * satisfy the following constraints. This first guarantees the hour and minute are
187 * valid, the second guarantees the second is valid.
188 *
189 * <pre>
190 * {@code 0 <= secondInDayA + secondInDayB < 86400}
191 * {@code 0 <= (secondInDayA + secondInDayB) % 60 + leap < minuteDuration}
192 * {@code 0 <= leap <= minuteDuration - 60 if minuteDuration >= 60}
193 * {@code 0 >= leap >= minuteDuration - 60 if minuteDuration < 60}
194 * </pre>
195 *
196 * <p>If the seconds of minute ({@link #getSecond()}) computed from {@code
197 * secondInDayA + secondInDayB + leap} is greater than or equal to {@code
198 * minuteDuration} then the second of minute will be set to {@code
199 * FastMath.nextDown(minuteDuration)}. This prevents rounding to an invalid seconds of
200 * minute number when the input values have greater precision than a {@code double}.
201 *
202 * <p>This constructor is always in UTC (i.e. {@link #getMinutesFromUTC() will return
203 * 0}).
204 *
205 * <p>If {@code secondsInDayB} or {@code leap} is NaN then the hour and minute will
206 * be determined from {@code secondInDayA} and the second of minute will be NaN.
207 *
208 * <p>This constructor is private to avoid confusion with the other constructors that
209 * would be caused by overloading. Use {@link #fromSeconds(int, double, double,
210 * int)}.
211 *
212 * @param secondInDayA first part of the second number.
213 * @param secondInDayB last part of the second number.
214 * @param leap magnitude of the leap second if this point in time is during
215 * a leap second, otherwise {@code 0.0}. This value is not used
216 * to compute hours and minutes, but it is added to the computed
217 * second of minute.
218 * @param minuteDuration number of seconds in the current minute, normally {@code 60}.
219 * @throws OrekitIllegalArgumentException if the inequalities above do not hold.
220 * @see #fromSeconds(int, double, double, int)
221 * @since 10.2
222 */
223 private TimeComponents(final int secondInDayA,
224 final double secondInDayB,
225 final double leap,
226 final int minuteDuration) throws OrekitIllegalArgumentException {
227
228 // split the numbers as a whole number of seconds
229 // and a fractional part between 0.0 (included) and 1.0 (excluded)
230 final int carry = (int) FastMath.floor(secondInDayB);
231 int wholeSeconds = secondInDayA + carry;
232 final double fractional = secondInDayB - carry;
233
234 // range check
235 if (wholeSeconds < 0 || wholeSeconds >= Constants.JULIAN_DAY) {
236 throw new OrekitIllegalArgumentException(
237 OrekitMessages.OUT_OF_RANGE_SECONDS_NUMBER_DETAIL,
238 // this can produce some strange messages due to rounding
239 secondInDayA + secondInDayB,
240 0,
241 Constants.JULIAN_DAY);
242 }
243 final int maxExtraSeconds = minuteDuration - 60;
244 if (leap * maxExtraSeconds < 0 ||
245 FastMath.abs(leap) > FastMath.abs(maxExtraSeconds)) {
246 throw new OrekitIllegalArgumentException(
247 OrekitMessages.OUT_OF_RANGE_SECONDS_NUMBER_DETAIL,
248 leap, 0, maxExtraSeconds);
249 }
250
251 // extract the time components
252 hour = wholeSeconds / 3600;
253 wholeSeconds -= 3600 * hour;
254 minute = wholeSeconds / 60;
255 wholeSeconds -= 60 * minute;
256 // at this point ((minuteDuration - wholeSeconds) - leap) - fractional > 0
257 // or else one of the preconditions was violated. Even if there is not violation,
258 // naiveSecond may round to minuteDuration, creating an invalid time.
259 // In that case round down to preserve a valid time at the cost of up to 1 ULP of error.
260 // See #676 and #681.
261 final double naiveSecond = wholeSeconds + (leap + fractional);
262 if (naiveSecond < 0) {
263 throw new OrekitIllegalArgumentException(
264 OrekitMessages.OUT_OF_RANGE_SECONDS_NUMBER_DETAIL,
265 naiveSecond, 0, minuteDuration);
266 }
267 if (naiveSecond < minuteDuration || Double.isNaN(naiveSecond)) {
268 second = naiveSecond;
269 } else {
270 second = FastMath.nextDown((double) minuteDuration);
271 }
272 minutesFromUTC = 0;
273
274 }
275
276 /**
277 * Build a time from the second number within the day.
278 *
279 * <p>The seconds past midnight is the sum {@code secondInDayA + secondInDayB +
280 * leap}. The two parameters are used for increased accuracy. Only the first part of
281 * the sum ({@code secondInDayA + secondInDayB}) is used to compute the hours and
282 * minutes. The third parameter ({@code leap}) is added directly to the second value
283 * ({@link #getSecond()}) to implement leap seconds. These three quantities must
284 * satisfy the following constraints. This first guarantees the hour and minute are
285 * valid, the second guarantees the second is valid.
286 *
287 * <pre>
288 * {@code 0 <= secondInDayA + secondInDayB < 86400}
289 * {@code 0 <= (secondInDayA + secondInDayB) % 60 + leap <= minuteDuration}
290 * {@code 0 <= leap <= minuteDuration - 60 if minuteDuration >= 60}
291 * {@code 0 >= leap >= minuteDuration - 60 if minuteDuration < 60}
292 * </pre>
293 *
294 * <p>If the seconds of minute ({@link #getSecond()}) computed from {@code
295 * secondInDayA + secondInDayB + leap} is greater than or equal to {@code 60 + leap}
296 * then the second of minute will be set to {@code FastMath.nextDown(60 + leap)}. This
297 * prevents rounding to an invalid seconds of minute number when the input values have
298 * greater precision than a {@code double}.
299 *
300 * <p>This constructor is always in UTC (i.e. {@link #getMinutesFromUTC() will return
301 * 0}).
302 *
303 * <p>If {@code secondsInDayB} or {@code leap} is NaN then the hour and minute will
304 * be determined from {@code secondInDayA} and the second of minute will be NaN.
305 *
306 * @param secondInDayA first part of the second number.
307 * @param secondInDayB last part of the second number.
308 * @param leap magnitude of the leap second if this point in time is during
309 * a leap second, otherwise {@code 0.0}. This value is not used
310 * to compute hours and minutes, but it is added to the computed
311 * second of minute.
312 * @param minuteDuration number of seconds in the current minute, normally {@code 60}.
313 * @return new time components for the specified time.
314 * @throws OrekitIllegalArgumentException if the inequalities above do not hold.
315 * @since 10.2
316 */
317 public static TimeComponents fromSeconds(final int secondInDayA,
318 final double secondInDayB,
319 final double leap,
320 final int minuteDuration) {
321 return new TimeComponents(secondInDayA, secondInDayB, leap, minuteDuration);
322 }
323
324 /** Parse a string in ISO-8601 format to build a time.
325 * <p>The supported formats are:
326 * <ul>
327 * <li>basic and extended format local time: hhmmss, hh:mm:ss (with optional decimals in seconds)</li>
328 * <li>optional UTC time: hhmmssZ, hh:mm:ssZ</li>
329 * <li>optional signed hours UTC offset: hhmmss+HH, hhmmss-HH, hh:mm:ss+HH, hh:mm:ss-HH</li>
330 * <li>optional signed basic hours and minutes UTC offset: hhmmss+HHMM, hhmmss-HHMM, hh:mm:ss+HHMM, hh:mm:ss-HHMM</li>
331 * <li>optional signed extended hours and minutes UTC offset: hhmmss+HH:MM, hhmmss-HH:MM, hh:mm:ss+HH:MM, hh:mm:ss-HH:MM</li>
332 * </ul>
333 *
334 * <p> As shown by the list above, only the complete representations defined in section 4.2
335 * of ISO-8601 standard are supported, neither expended representations nor representations
336 * with reduced accuracy are supported.
337 *
338 * @param string string to parse
339 * @return a parsed time
340 * @exception IllegalArgumentException if string cannot be parsed
341 */
342 public static TimeComponents parseTime(final String string) {
343
344 // is the date a calendar date ?
345 final Matcher timeMatcher = ISO8601_FORMATS.matcher(string);
346 if (timeMatcher.matches()) {
347 final int hour = Integer.parseInt(timeMatcher.group(1));
348 final int minute = Integer.parseInt(timeMatcher.group(2));
349 final double second = timeMatcher.group(3) == null ? 0.0 : Double.parseDouble(timeMatcher.group(3).replace(',', '.'));
350 final String offset = timeMatcher.group(4);
351 final int minutesFromUTC;
352 if (offset == null) {
353 // no offset from UTC is given
354 minutesFromUTC = 0;
355 } else {
356 // we need to parse an offset from UTC
357 // the sign is mandatory and the ':' separator is optional
358 // so we can have offsets given as -06:00 or +0100
359 final int sign = offset.codePointAt(0) == '-' ? -1 : +1;
360 final int hourOffset = Integer.parseInt(offset.substring(1, 3));
361 final int minutesOffset = offset.length() <= 3 ? 0 : Integer.parseInt(offset.substring(offset.length() - 2));
362 minutesFromUTC = sign * (minutesOffset + 60 * hourOffset);
363 }
364 return new TimeComponents(hour, minute, second, minutesFromUTC);
365 }
366
367 throw new OrekitIllegalArgumentException(OrekitMessages.NON_EXISTENT_TIME, string);
368
369 }
370
371 /** Get the hour number.
372 * @return hour number from 0 to 23
373 */
374 public int getHour() {
375 return hour;
376 }
377
378 /** Get the minute number.
379 * @return minute minute number from 0 to 59
380 */
381 public int getMinute() {
382 return minute;
383 }
384
385 /** Get the seconds number.
386 * @return second second number from 0.0 to 61.0 (excluded). Note that 60 ≤ second
387 * < 61 only occurs during a leap second.
388 */
389 public double getSecond() {
390 return second;
391 }
392
393 /** Get the offset between the specified date and UTC.
394 * <p>
395 * The offset is always an integral number of minutes, as per ISO-8601 standard.
396 * </p>
397 * @return offset in minutes between the specified date and UTC
398 * @since 7.2
399 */
400 public int getMinutesFromUTC() {
401 return minutesFromUTC;
402 }
403
404 /** Get the second number within the local day, <em>without</em> applying the {@link #getMinutesFromUTC() offset from UTC}.
405 * @return second number from 0.0 to Constants.JULIAN_DAY
406 * @see #getSecondsInUTCDay()
407 * @since 7.2
408 */
409 public double getSecondsInLocalDay() {
410 return second + 60 * minute + 3600 * hour;
411 }
412
413 /** Get the second number within the UTC day, applying the {@link #getMinutesFromUTC() offset from UTC}.
414 * @return second number from {@link #getMinutesFromUTC() -getMinutesFromUTC()}
415 * to Constants.JULIAN_DAY {@link #getMinutesFromUTC() + getMinutesFromUTC()}
416 * @see #getSecondsInLocalDay()
417 * @since 7.2
418 */
419 public double getSecondsInUTCDay() {
420 return second + 60 * (minute - minutesFromUTC) + 3600 * hour;
421 }
422
423 /** Get a string representation of the time.
424 * @return string representation of the time
425 */
426 public String toString() {
427 StringBuilder builder = new StringBuilder().
428 append(TWO_DIGITS.format(hour)).append(':').
429 append(TWO_DIGITS.format(minute)).append(':').
430 append(SECONDS_FORMAT.format(second));
431 if (minutesFromUTC != 0) {
432 builder = builder.
433 append(minutesFromUTC < 0 ? '-' : '+').
434 append(TWO_DIGITS.format(FastMath.abs(minutesFromUTC) / 60)).append(':').
435 append(TWO_DIGITS.format(FastMath.abs(minutesFromUTC) % 60));
436 }
437 return builder.toString();
438 }
439
440 /** {@inheritDoc} */
441 public int compareTo(final TimeComponents other) {
442 return Double.compare(getSecondsInUTCDay(), other.getSecondsInUTCDay());
443 }
444
445 /** {@inheritDoc} */
446 public boolean equals(final Object other) {
447 try {
448 final TimeComponentsg/orekit/time/TimeComponents.html#TimeComponents">TimeComponents otherTime = (TimeComponents) other;
449 return otherTime != null &&
450 hour == otherTime.hour &&
451 minute == otherTime.minute &&
452 second == otherTime.second &&
453 minutesFromUTC == otherTime.minutesFromUTC;
454 } catch (ClassCastException cce) {
455 return false;
456 }
457 }
458
459 /** {@inheritDoc} */
460 public int hashCode() {
461 final long bits = Double.doubleToLongBits(second);
462 return ((hour << 16) ^ ((minute - minutesFromUTC) << 8)) ^ (int) (bits ^ (bits >>> 32));
463 }
464
465 }