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