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