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.Serializable;
20 import java.util.regex.Matcher;
21 import java.util.regex.Pattern;
22
23 import org.hipparchus.util.FastMath;
24 import org.orekit.errors.OrekitIllegalArgumentException;
25 import org.orekit.errors.OrekitMessages;
26 import org.orekit.utils.Constants;
27
28
29 /** Class representing a time within the day broken up as hour,
30 * minute and second components.
31 * <p>Instances of this class are guaranteed to be immutable.</p>
32 * @see DateComponents
33 * @see DateTimeComponents
34 * @author Luc Maisonobe
35 */
36 public class TimeComponents implements Serializable, Comparable<TimeComponents> {
37
38 /** Constant for commonly used hour 00:00:00. */
39 public static final TimeComponents H00 = new TimeComponents(0, 0, TimeOffset.ZERO);
40
41 /** Constant for commonly used hour 12:00:00. */
42 public static final TimeComponents H12 = new TimeComponents(12, 0, TimeOffset.ZERO);
43
44 // CHECKSTYLE: stop ConstantName
45 /** Constant for NaN time.
46 * @since 13.0
47 */
48 public static final TimeComponents NaN = new TimeComponents(0, 0, TimeOffset.NaN);
49 // CHECKSTYLE: resume ConstantName
50
51 /** Wrapping limits for rounding to next minute.
52 * @since 13.0
53 */
54 private static final TimeOffset[] WRAPPING = new TimeOffset[] {
55 new TimeOffset(59L, 500000000000000000L), // round to second
56 new TimeOffset(59L, 950000000000000000L), // round to 10⁻¹ second
57 new TimeOffset(59L, 995000000000000000L), // round to 10⁻² second
58 new TimeOffset(59L, 999500000000000000L), // round to 10⁻³ second
59 new TimeOffset(59L, 999950000000000000L), // round to 10⁻⁴ second
60 new TimeOffset(59L, 999995000000000000L), // round to 10⁻⁵ second
61 new TimeOffset(59L, 999999500000000000L), // round to 10⁻⁶ second
62 new TimeOffset(59L, 999999950000000000L), // round to 10⁻⁷ second
63 new TimeOffset(59L, 999999995000000000L), // round to 10⁻⁸ second
64 new TimeOffset(59L, 999999999500000000L), // round to 10⁻⁹ second
65 new TimeOffset(59L, 999999999950000000L), // round to 10⁻¹⁰ second
66 new TimeOffset(59L, 999999999995000000L), // round to 10⁻¹¹ second
67 new TimeOffset(59L, 999999999999500000L), // round to 10⁻¹² second
68 new TimeOffset(59L, 999999999999950000L), // round to 10⁻¹³ second
69 new TimeOffset(59L, 999999999999995000L), // round to 10⁻¹⁴ second
70 new TimeOffset(59L, 999999999999999500L), // round to 10⁻¹⁵ second
71 new TimeOffset(59L, 999999999999999950L), // round to 10⁻¹⁶ second
72 new TimeOffset(59L, 999999999999999995L) // round to 10⁻¹⁷ second
73 };
74
75 /** Offset values for rounding attoseconds.
76 * @since 13.0
77 */
78 // CHECKSTYLE: stop Indentation check */
79 private static final long[] ROUNDING = new long[] {
80 500000000000000000L, // round to second
81 50000000000000000L, // round to 10⁻¹ second
82 5000000000000000L, // round to 10⁻² second
83 500000000000000L, // round to 10⁻³ second
84 50000000000000L, // round to 10⁻⁴ second
85 5000000000000L, // round to 10⁻⁵ second
86 500000000000L, // round to 10⁻⁶ second
87 50000000000L, // round to 10⁻⁷ second
88 5000000000L, // round to 10⁻⁸ second
89 500000000L, // round to 10⁻⁹ second
90 50000000L, // round to 10⁻¹⁰ second
91 5000000L, // round to 10⁻¹¹ second
92 500000L, // round to 10⁻¹² second
93 50000L, // round to 10⁻¹³ second
94 5000L, // round to 10⁻¹⁴ second
95 500L, // round to 10⁻¹⁵ second
96 50L, // round to 10⁻¹⁶ second
97 5L, // round to 10⁻¹⁷ second
98 0L, // round to 10⁻¹⁸ second
99 };
100 // CHECKSTYLE: resume Indentation check */
101
102 /** Serializable UID. */
103 private static final long serialVersionUID = 20240712L;
104
105 /** Basic and extends formats for local time, with optional timezone. */
106 private static final Pattern ISO8601_FORMATS = Pattern.compile("^(\\d\\d):?(\\d\\d):?(\\d\\d(?:[.,]\\d+)?)?(?:Z|([-+]\\d\\d(?::?\\d\\d)?))?$");
107
108 /** Number of seconds in one hour. */
109 private static final int HOUR = 3600;
110
111 /** Number of seconds in one minute. */
112 private static final int MINUTE = 60;
113
114 /** Constant for 23 hours. */
115 private static final int TWENTY_THREE = 23;
116
117 /** Constant for 59 minutes. */
118 private static final int FIFTY_NINE = 59;
119
120 /** Constant for 23:59. */
121 private static final TimeOffset TWENTY_THREE_FIFTY_NINE =
122 new TimeOffset(TWENTY_THREE * HOUR + FIFTY_NINE * MINUTE, 0L);
123
124 /** Hour number. */
125 private final int hour;
126
127 /** Minute number. */
128 private final int minute;
129
130 /** Second number. */
131 private final TimeOffset second;
132
133 /** Offset between the specified date and UTC.
134 * <p>
135 * Always an integral number of minutes, as per ISO-8601 standard.
136 * </p>
137 * @since 7.2
138 */
139 private final int minutesFromUTC;
140
141 /** Build a time from its clock elements.
142 * <p>Note that seconds between 60.0 (inclusive) and 61.0 (exclusive) are allowed
143 * in this method, since they do occur during leap seconds introduction
144 * in the {@link UTCScale UTC} time scale.</p>
145 * @param hour hour number from 0 to 23
146 * @param minute minute number from 0 to 59
147 * @param second second number from 0.0 to 61.0 (excluded)
148 * @exception IllegalArgumentException if inconsistent arguments
149 * are given (parameters out of range)
150 */
151 public TimeComponents(final int hour, final int minute, final double second)
152 throws IllegalArgumentException {
153 this(hour, minute, new TimeOffset(second));
154 }
155
156 /** Build a time from its clock elements.
157 * <p>Note that seconds between 60.0 (inclusive) and 61.0 (exclusive) are allowed
158 * in this method, since they do occur during leap seconds introduction
159 * in the {@link UTCScale UTC} time scale.</p>
160 * @param hour hour number from 0 to 23
161 * @param minute minute number from 0 to 59
162 * @param second second number from 0.0 to 61.0 (excluded)
163 * @exception IllegalArgumentException if inconsistent arguments
164 * are given (parameters out of range)
165 * @since 13.0
166 */
167 public TimeComponents(final int hour, final int minute, final TimeOffset second)
168 throws IllegalArgumentException {
169 this(hour, minute, second, 0);
170 }
171
172 /** Build a time from its clock elements.
173 * <p>Note that seconds between 60.0 (inclusive) and 61.0 (exclusive) are allowed
174 * in this method, since they do occur during leap seconds introduction
175 * in the {@link UTCScale UTC} time scale.</p>
176 * @param hour hour number from 0 to 23
177 * @param minute minute number from 0 to 59
178 * @param second second number from 0.0 to 61.0 (excluded)
179 * @param minutesFromUTC offset between the specified date and UTC, as an
180 * integral number of minutes, as per ISO-8601 standard
181 * @exception IllegalArgumentException if inconsistent arguments
182 * are given (parameters out of range)
183 * @since 7.2
184 */
185 public TimeComponents(final int hour, final int minute, final double second, final int minutesFromUTC)
186 throws IllegalArgumentException {
187 this(hour, minute, new TimeOffset(second), minutesFromUTC);
188 }
189
190 /** Build a time from its clock elements.
191 * <p>Note that seconds between 60.0 (inclusive) and 61.0 (exclusive) are allowed
192 * in this method, since they do occur during leap seconds introduction
193 * in the {@link UTCScale UTC} time scale.</p>
194 * @param hour hour number from 0 to 23
195 * @param minute minute number from 0 to 59
196 * @param second second number from 0.0 to 62.0 (excluded, more than 61 s occurred on
197 * the 1961 leap second, which was between 1 and 2 seconds in duration)
198 * @param minutesFromUTC offset between the specified date and UTC, as an
199 * integral number of minutes, as per ISO-8601 standard
200 * @exception IllegalArgumentException if inconsistent arguments
201 * are given (parameters out of range)
202 * @since 13.0
203 */
204 public TimeComponents(final int hour, final int minute, final TimeOffset second,
205 final int minutesFromUTC)
206 throws IllegalArgumentException {
207
208 // range check
209 if (hour < 0 || hour > 23 ||
210 minute < 0 || minute > 59 ||
211 second.getSeconds() < 0L || second.getSeconds() >= 62L) {
212 throw new OrekitIllegalArgumentException(OrekitMessages.NON_EXISTENT_HMS_TIME,
213 hour, minute, second.toDouble());
214 }
215
216 this.hour = hour;
217 this.minute = minute;
218 this.second = second;
219 this.minutesFromUTC = minutesFromUTC;
220
221 }
222
223 /**
224 * Build a time from the second number within the day.
225 *
226 * <p>If the {@code secondInDay} is less than {@code 60.0} then {@link #getSecond()}
227 * and {@link #getSplitSecond()} will be less than {@code 60.0}, otherwise they will be
228 * less than {@code 61.0}. This constructor may produce an invalid value of
229 * {@link #getSecond()} and {@link #getSplitSecond()} during a negative leap second,
230 * through there has never been one. For more control over the number of seconds in
231 * the final minute use {@link #TimeComponents(TimeOffset, TimeOffset, int)}.
232 *
233 * <p>This constructor is always in UTC (i.e. {@link #getMinutesFromUTC() will return
234 * 0}).
235 *
236 * @param secondInDay second number from 0.0 to {@link Constants#JULIAN_DAY} {@code +
237 * 1} (excluded)
238 * @throws OrekitIllegalArgumentException if seconds number is out of range
239 * @see #TimeComponents(TimeOffset, TimeOffset, int)
240 * @see #TimeComponents(int, double)
241 */
242 public TimeComponents(final double secondInDay)
243 throws OrekitIllegalArgumentException {
244 this(new TimeOffset(secondInDay));
245 }
246
247 /**
248 * Build a time from the second number within the day.
249 *
250 * <p>The second number is defined here as the sum
251 * {@code secondInDayA + secondInDayB} from 0.0 to {@link Constants#JULIAN_DAY}
252 * {@code + 1} (excluded). The two parameters are used for increased accuracy.
253 *
254 * <p>If the sum is less than {@code 60.0} then {@link #getSecond()} will be less
255 * than {@code 60.0}, otherwise it will be less than {@code 61.0}. This constructor
256 * may produce an invalid value of {@link #getSecond()} during a negative leap second,
257 * through there has never been one. For more control over the number of seconds in
258 * the final minute use {@link #TimeComponents(TimeOffset, TimeOffset, int)}.
259 *
260 * <p>This constructor is always in UTC (i.e. {@link #getMinutesFromUTC()} will
261 * return 0).
262 *
263 * @param secondInDayA first part of the second number
264 * @param secondInDayB last part of the second number
265 * @throws OrekitIllegalArgumentException if seconds number is out of range
266 * @see #TimeComponents(TimeOffset, TimeOffset, int)
267 */
268 public TimeComponents(final int secondInDayA, final double secondInDayB)
269 throws OrekitIllegalArgumentException {
270
271 // if the total is at least 86400 then assume there is a leap second
272 final TimeOffset aPlusB = new TimeOffset(secondInDayA).add(new TimeOffset(secondInDayB));
273 final TimeComponents tc = aPlusB.compareTo(TimeOffset.DAY) >= 0 ?
274 new TimeComponents(aPlusB.subtract(TimeOffset.SECOND), TimeOffset.SECOND, 61) :
275 new TimeComponents(aPlusB, TimeOffset.ZERO, 60);
276
277 this.hour = tc.hour;
278 this.minute = tc.minute;
279 this.second = tc.second;
280 this.minutesFromUTC = tc.minutesFromUTC;
281
282 }
283
284 /**
285 * Build a time from the second number within the day.
286 *
287 * <p>If the {@code secondInDay} is less than {@code 60.0} then {@link #getSecond()}
288 * will be less than {@code 60.0}, otherwise it will be less than {@code 61.0}. This constructor
289 * may produce an invalid value of {@link #getSecond()} during a negative leap second,
290 * through there has never been one. For more control over the number of seconds in
291 * the final minute use {@link #TimeComponents(TimeOffset, TimeOffset, int)}.
292 *
293 * <p>This constructor is always in UTC (i.e. {@link #getMinutesFromUTC() will return
294 * 0}).
295 *
296 * @param splitSecondInDay second number from 0.0 to {@link Constants#JULIAN_DAY} {@code +
297 * 1} (excluded)
298 * @see #TimeComponents(TimeOffset, TimeOffset, int)
299 * @see #TimeComponents(int, double)
300 * @since 13.0
301 */
302 public TimeComponents(final TimeOffset splitSecondInDay) {
303 if (splitSecondInDay.compareTo(TimeOffset.ZERO) < 0) {
304 // negative time
305 throw new OrekitIllegalArgumentException(OrekitMessages.OUT_OF_RANGE_SECONDS_NUMBER_DETAIL,
306 splitSecondInDay.toDouble(),
307 0, TimeOffset.DAY_WITH_POSITIVE_LEAP.getSeconds());
308 } else if (splitSecondInDay.compareTo(TimeOffset.DAY) >= 0) {
309 // if the total is at least 86400 then assume there is a leap second
310 if (splitSecondInDay.compareTo(TimeOffset.DAY_WITH_POSITIVE_LEAP) >= 0) {
311 // more than one leap second is too much
312 throw new OrekitIllegalArgumentException(OrekitMessages.OUT_OF_RANGE_SECONDS_NUMBER_DETAIL,
313 splitSecondInDay.toDouble(),
314 0, TimeOffset.DAY_WITH_POSITIVE_LEAP.getSeconds());
315 } else {
316 hour = TWENTY_THREE;
317 minute = FIFTY_NINE;
318 second = splitSecondInDay.subtract(TWENTY_THREE_FIFTY_NINE);
319 }
320 } else {
321 // regular time within day
322 hour = (int) splitSecondInDay.getSeconds() / HOUR;
323 minute = ((int) splitSecondInDay.getSeconds() % HOUR) / MINUTE;
324 second = splitSecondInDay.subtract(new TimeOffset(hour * HOUR + minute * MINUTE, 0L));
325 }
326
327 minutesFromUTC = 0;
328
329 }
330
331 /**
332 * Build a time from the second number within the day.
333 *
334 * <p>The seconds past midnight is the sum {@code secondInDay + leap}. Only the part
335 * {@code secondInDay} is used to compute the hours and minutes. The second parameter
336 * ({@code leap}) is added directly to the second value ({@link #getSecond()}) to
337 * implement leap seconds. These two quantities must satisfy the following constraints.
338 * This first guarantees the hour and minute are valid, the second guarantees the second
339 * is valid.
340 *
341 * <pre>
342 * {@code 0 <= secondInDay < 86400}
343 * {@code 0 <= secondInDay % 60 + leap <= minuteDuration}
344 * {@code 0 <= leap <= minuteDuration - 60 if minuteDuration >= 60}
345 * {@code 0 >= leap >= minuteDuration - 60 if minuteDuration < 60}
346 * </pre>
347 *
348 * <p>If the seconds of minute ({@link #getSecond()}) computed from {@code
349 * secondInDay + leap} is greater than or equal to {@code 60 + leap}
350 * then the second of minute will be set to {@code FastMath.nextDown(60 + leap)}. This
351 * prevents rounding to an invalid seconds of minute number when the input values have
352 * greater precision than a {@code double}.
353 *
354 * <p>This constructor is always in UTC (i.e. {@link #getMinutesFromUTC() will return
355 * 0}).
356 *
357 * <p>If {@code secondsInDay} or {@code leap} is NaN then the hour and minute will
358 * be set arbitrarily and the second of minute will be NaN.
359 *
360 * @param secondInDay part of the second number.
361 * @param leap magnitude of the leap second if this point in time is during
362 * a leap second, otherwise {@code 0.0}. This value is not used
363 * to compute hours and minutes, but it is added to the computed
364 * second of minute.
365 * @param minuteDuration number of seconds in the current minute, normally {@code 60}.
366 * @throws OrekitIllegalArgumentException if the inequalities above do not hold.
367 * @since 10.2
368 */
369 public TimeComponents(final TimeOffset secondInDay, final TimeOffset leap, final int minuteDuration) {
370
371 minutesFromUTC = 0;
372
373 if (secondInDay.isNaN()) {
374 // special handling for NaN
375 hour = 0;
376 minute = 0;
377 second = secondInDay;
378 return;
379 }
380
381 // range check
382 if (secondInDay.compareTo(TimeOffset.ZERO) < 0 || secondInDay.compareTo(TimeOffset.DAY) >= 0) {
383 throw new OrekitIllegalArgumentException(OrekitMessages.OUT_OF_RANGE_SECONDS_NUMBER_DETAIL,
384 // this can produce some strange messages due to rounding
385 secondInDay.toDouble(), 0, Constants.JULIAN_DAY);
386 }
387 final int maxExtraSeconds = minuteDuration - MINUTE;
388 if (leap.getSeconds() * maxExtraSeconds < 0 || FastMath.abs(leap.getSeconds()) > FastMath.abs(maxExtraSeconds)) {
389 throw new OrekitIllegalArgumentException(OrekitMessages.OUT_OF_RANGE_SECONDS_NUMBER_DETAIL,
390 leap, 0, maxExtraSeconds);
391 }
392
393 // extract the time components
394 int wholeSeconds = (int) secondInDay.getSeconds();
395 hour = wholeSeconds / HOUR;
396 wholeSeconds -= HOUR * hour;
397 minute = wholeSeconds / MINUTE;
398 wholeSeconds -= MINUTE * minute;
399 // at this point ((minuteDuration - wholeSeconds) - leap) - fractional > 0
400 // or else one of the preconditions was violated. Even if there is no violation,
401 // naiveSecond may round to minuteDuration, creating an invalid time.
402 // In that case round down to preserve a valid time at the cost of up to 1as of error.
403 // See #676 and #681.
404 final TimeOffset naiveSecond = new TimeOffset(wholeSeconds, secondInDay.getAttoSeconds()).add(leap);
405 if (naiveSecond.compareTo(TimeOffset.ZERO) < 0) {
406 throw new OrekitIllegalArgumentException(
407 OrekitMessages.OUT_OF_RANGE_SECONDS_NUMBER_DETAIL,
408 naiveSecond, 0, minuteDuration);
409 }
410 if (naiveSecond.getSeconds() < minuteDuration) {
411 second = naiveSecond;
412 } else {
413 second = new TimeOffset(minuteDuration - 1, 999999999999999999L);
414 }
415
416 }
417
418 /** Parse a string in ISO-8601 format to build a time.
419 * <p>The supported formats are:
420 * <ul>
421 * <li>basic and extended format local time: hhmmss, hh:mm:ss (with optional decimals in seconds)</li>
422 * <li>optional UTC time: hhmmssZ, hh:mm:ssZ</li>
423 * <li>optional signed hours UTC offset: hhmmss+HH, hhmmss-HH, hh:mm:ss+HH, hh:mm:ss-HH</li>
424 * <li>optional signed basic hours and minutes UTC offset: hhmmss+HHMM, hhmmss-HHMM, hh:mm:ss+HHMM, hh:mm:ss-HHMM</li>
425 * <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>
426 * </ul>
427 *
428 * <p> As shown by the list above, only the complete representations defined in section 4.2
429 * of ISO-8601 standard are supported, neither expended representations nor representations
430 * with reduced accuracy are supported.
431 *
432 * @param string string to parse
433 * @return a parsed time
434 * @exception IllegalArgumentException if string cannot be parsed
435 */
436 public static TimeComponents parseTime(final String string) {
437
438 // is the date a calendar date ?
439 final Matcher timeMatcher = ISO8601_FORMATS.matcher(string);
440 if (timeMatcher.matches()) {
441 final int hour = Integer.parseInt(timeMatcher.group(1));
442 final int minute = Integer.parseInt(timeMatcher.group(2));
443 final TimeOffset second = timeMatcher.group(3) == null ?
444 TimeOffset.ZERO :
445 TimeOffset.parse(timeMatcher.group(3).replace(',', '.'));
446 final String offset = timeMatcher.group(4);
447 final int minutesFromUTC;
448 if (offset == null) {
449 // no offset from UTC is given
450 minutesFromUTC = 0;
451 } else {
452 // we need to parse an offset from UTC
453 // the sign is mandatory and the ':' separator is optional
454 // so we can have offsets given as -06:00 or +0100
455 final int sign = offset.codePointAt(0) == '-' ? -1 : +1;
456 final int hourOffset = Integer.parseInt(offset.substring(1, 3));
457 final int minutesOffset = offset.length() <= 3 ? 0 : Integer.parseInt(offset.substring(offset.length() - 2));
458 minutesFromUTC = sign * (minutesOffset + MINUTE * hourOffset);
459 }
460 return new TimeComponents(hour, minute, second, minutesFromUTC);
461 }
462
463 throw new OrekitIllegalArgumentException(OrekitMessages.NON_EXISTENT_TIME, string);
464
465 }
466
467 /** Get the hour number.
468 * @return hour number from 0 to 23
469 */
470 public int getHour() {
471 return hour;
472 }
473
474 /** Get the minute number.
475 * @return minute minute number from 0 to 59
476 */
477 public int getMinute() {
478 return minute;
479 }
480
481 /** Get the seconds number.
482 * @return second second number from 0.0 to 61.0 (excluded). Note that 60 ≤ second
483 * < 61 only occurs during a leap second.
484 */
485 public double getSecond() {
486 return second.toDouble();
487 }
488
489 /** Get the seconds number.
490 * @return second second number from 0.0 to 61.0 (excluded). Note that 60 ≤ second
491 * < 61 only occurs during a leap second.
492 */
493 public TimeOffset getSplitSecond() {
494 return second;
495 }
496
497 /** Get the offset between the specified date and UTC.
498 * <p>
499 * The offset is always an integral number of minutes, as per ISO-8601 standard.
500 * </p>
501 * @return offset in minutes between the specified date and UTC
502 * @since 7.2
503 */
504 public int getMinutesFromUTC() {
505 return minutesFromUTC;
506 }
507
508 /** Get the second number within the local day, <em>without</em> applying the {@link #getMinutesFromUTC() offset from UTC}.
509 * @return second number from 0.0 to Constants.JULIAN_DAY
510 * @see #getSplitSecondsInLocalDay()
511 * @see #getSecondsInUTCDay()
512 * @since 7.2
513 */
514 public double getSecondsInLocalDay() {
515 return getSplitSecondsInLocalDay().toDouble();
516 }
517
518 /** Get the second number within the local day, <em>without</em> applying the {@link #getMinutesFromUTC() offset from UTC}.
519 * @return second number from 0.0 to Constants.JULIAN_DAY
520 * @see #getSecondsInLocalDay()
521 * @see #getSplitSecondsInUTCDay()
522 * @since 13.0
523 */
524 public TimeOffset getSplitSecondsInLocalDay() {
525 return new TimeOffset((long) MINUTE * minute + (long) HOUR * hour, 0L).add(second);
526 }
527
528 /** Get the second number within the UTC day, applying the {@link #getMinutesFromUTC() offset from UTC}.
529 * @return second number from {@link #getMinutesFromUTC() -getMinutesFromUTC()}
530 * to Constants.JULIAN_DAY {@link #getMinutesFromUTC() + getMinutesFromUTC()}
531 * @see #getSplitSecondsInUTCDay()
532 * @see #getSecondsInLocalDay()
533 * @since 7.2
534 */
535 public double getSecondsInUTCDay() {
536 return getSplitSecondsInUTCDay().toDouble();
537 }
538
539 /** Get the second number within the UTC day, applying the {@link #getMinutesFromUTC() offset from UTC}.
540 * @return second number from {@link #getMinutesFromUTC() -getMinutesFromUTC()}
541 * to Constants.JULIAN_DAY {@link #getMinutesFromUTC() + getMinutesFromUTC()}
542 * @see #getSecondsInUTCDay()
543 * @see #getSplitSecondsInLocalDay()
544 * @since 13.0
545 */
546 public TimeOffset getSplitSecondsInUTCDay() {
547 return new TimeOffset((long) MINUTE * (minute - minutesFromUTC) + (long) HOUR * hour, 0L).add(second);
548 }
549
550 /**
551 * Round this time to the given precision if needed to prevent rounding up to an
552 * invalid seconds number. This is useful, for example, when writing custom date-time
553 * formatting methods so one does not, e.g., end up with "60.0" seconds during a
554 * normal minute when the value of seconds is {@code 59.999}. This method will instead
555 * round up the minute, hour, day, month, and year as needed.
556 *
557 * @param minuteDuration 59, 60, 61, or 62 seconds depending on the date being close
558 * to a leap second introduction and the magnitude of the leap
559 * second.
560 * @param fractionDigits the number of decimal digits after the decimal point in the
561 * seconds number that will be printed. This date-time is
562 * rounded to {@code fractionDigits} after the decimal point if
563 * necessary to prevent rounding up to {@code minuteDuration}.
564 * {@code fractionDigits} must be greater than or equal to
565 * {@code 0}.
566 * @return the instance itself if no rounding was needed, or a time within
567 * {@code 0.5 * 10**-fractionDigits} seconds of this, and with a seconds number that
568 * will not round up to {@code minuteDuration} when rounded to {@code fractionDigits}
569 * after the decimal point
570 * @since 13.0
571 */
572 public TimeComponents wrapIfNeeded(final int minuteDuration, final int fractionDigits) {
573 TimeOffset wrappedSecond = second;
574
575 // adjust limit according to current minute duration
576 final TimeOffset limit = WRAPPING[FastMath.min(fractionDigits, WRAPPING.length - 1)].
577 add(new TimeOffset(minuteDuration - 60, 0L));
578
579 if (wrappedSecond.compareTo(limit) >= 0) {
580 // we should wrap around to the next minute
581 int wrappedMinute = minute;
582 int wrappedHour = hour;
583 wrappedSecond = TimeOffset.ZERO;
584 ++wrappedMinute;
585 if (wrappedMinute > 59) {
586 wrappedMinute = 0;
587 ++wrappedHour;
588 if (wrappedHour > 23) {
589 wrappedHour = 0;
590 }
591 }
592 return new TimeComponents(wrappedHour, wrappedMinute, wrappedSecond);
593 }
594 return this;
595 }
596
597 /**
598 * Package private method that allows specification of seconds format. Allows access from
599 * {@link DateTimeComponents#toString(int, int)}. Access from outside of rounding methods would result in invalid
600 * times, see #590, #591.
601 *
602 * @param fractionDigits the number of digits to include after the decimal point in the string representation of the
603 * seconds. The date and time is first rounded as necessary. {@code fractionDigits} must be
604 * greater than or equal to {@code 0}.
605 * @return string without UTC offset.
606 * @since 13.0
607 */
608 String toStringWithoutUtcOffset(final int fractionDigits) {
609
610 if (second.isFinite()) {
611 // general case for regular times
612 final long rounding = ROUNDING[FastMath.min(fractionDigits, ROUNDING.length - 1)];
613 final TimeComponents rounded = new TimeComponents(hour, minute,
614 new TimeOffset(second.getSeconds(),
615 second.getAttoSeconds() + rounding));
616 final StringBuilder builder = new StringBuilder();
617 builder.append(String.format("%02d:%02d:%02d",
618 rounded.hour, rounded.minute, rounded.second.getSeconds()));
619 if (fractionDigits > 0) {
620 builder.append('.');
621 builder.append(String.format("%018d", rounded.second.getAttoSeconds()), 0, fractionDigits);
622 }
623 return builder.toString();
624 } else if (second.isNaN()) {
625 // special handling for NaN
626 return String.format("%02d:%02d:NaN", hour, minute);
627 } else if (second.isNegativeInfinity()) {
628 // special handling for -∞
629 return String.format("%02d:%02d:-∞", hour, minute);
630 } else {
631 // special handling for +∞
632 return String.format("%02d:%02d:+∞", hour, minute);
633 }
634
635 }
636
637 /**
638 * Get a string representation of the time without the offset from UTC.
639 *
640 * @return a string representation of the time in an ISO 8601 like format.
641 * @see #formatUtcOffset()
642 * @see #toString()
643 */
644 public String toStringWithoutUtcOffset() {
645 // create formats here as they are not thread safe
646 // Format for seconds to prevent rounding up to an invalid time. See #591
647 final String formatted = toStringWithoutUtcOffset(18);
648 int last = formatted.length() - 1;
649 while (last > 11 && formatted.charAt(last) == '0') {
650 // we want to remove final zeros (but keeping milliseconds for compatibility)
651 --last;
652 }
653 return formatted.substring(0, last + 1);
654 }
655
656 /**
657 * Get the UTC offset as a string in ISO8601 format. For example, {@code +00:00}.
658 *
659 * @return the UTC offset as a string.
660 * @see #toStringWithoutUtcOffset()
661 * @see #toString()
662 */
663 public String formatUtcOffset() {
664 final int hourOffset = FastMath.abs(minutesFromUTC) / MINUTE;
665 final int minuteOffset = FastMath.abs(minutesFromUTC) % MINUTE;
666 return (minutesFromUTC < 0 ? '-' : '+') +
667 String.format("%02d:%02d", hourOffset, minuteOffset);
668 }
669
670 /**
671 * Get a string representation of the time including the offset from UTC.
672 *
673 * @return string representation of the time in an ISO 8601 like format including the
674 * UTC offset.
675 * @see #toStringWithoutUtcOffset()
676 * @see #formatUtcOffset()
677 */
678 public String toString() {
679 return toStringWithoutUtcOffset() + formatUtcOffset();
680 }
681
682 /** {@inheritDoc} */
683 public int compareTo(final TimeComponents other) {
684 return getSplitSecondsInUTCDay().compareTo(other.getSplitSecondsInUTCDay());
685 }
686
687 /** {@inheritDoc} */
688 public boolean equals(final Object other) {
689 try {
690 final TimeComponents otherTime = (TimeComponents) other;
691 return otherTime != null &&
692 hour == otherTime.hour &&
693 minute == otherTime.minute &&
694 second.compareTo(otherTime.second) == 0 &&
695 minutesFromUTC == otherTime.minutesFromUTC;
696 } catch (ClassCastException cce) {
697 return false;
698 }
699 }
700
701 /** {@inheritDoc} */
702 public int hashCode() {
703 return ((hour << 16) ^ ((minute - minutesFromUTC) << 8)) ^ second.hashCode();
704 }
705
706 }