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 22 import java.util.concurrent.TimeUnit; 23 import org.hipparchus.util.FastMath; 24 import org.orekit.errors.OrekitInternalError; 25 import org.orekit.utils.Constants; 26 import org.orekit.utils.formatting.FastLongFormatter; 27 28 /** Holder for date and time components. 29 * <p>This class is a simple holder with no processing methods.</p> 30 * <p>Instance of this class are guaranteed to be immutable.</p> 31 * @see AbsoluteDate 32 * @see DateComponents 33 * @see TimeComponents 34 * @author Luc Maisonobe 35 */ 36 public class DateTimeComponents implements Serializable, Comparable<DateTimeComponents> { 37 38 /** 39 * The Julian Epoch. 40 * 41 * @see TimeScales#getJulianEpoch() 42 */ 43 public static final DateTimeComponents JULIAN_EPOCH = 44 new DateTimeComponents(DateComponents.JULIAN_EPOCH, TimeComponents.H12); 45 46 /** Format for one 4 digits integer field. 47 * @since 13.0.3 48 */ 49 private static final FastLongFormatter PADDED_FOUR_DIGITS_INTEGER = new FastLongFormatter(4, true); 50 51 /** Format for one 2 digits integer field. 52 * @since 13.0.3 53 */ 54 private static final FastLongFormatter PADDED_TWO_DIGITS_INTEGER = new FastLongFormatter(2, true); 55 56 /** Serializable UID. */ 57 private static final long serialVersionUID = 20240720L; 58 59 /** Date component. */ 60 private final DateComponents date; 61 62 /** Time component. */ 63 private final TimeComponents time; 64 65 /** Build a new instance from its components. 66 * @param date date component 67 * @param time time component 68 */ 69 public DateTimeComponents(final DateComponents date, final TimeComponents time) { 70 this.date = date; 71 this.time = time; 72 } 73 74 /** Build an instance from raw level components. 75 * @param year year number (may be 0 or negative for BC years) 76 * @param month month number from 1 to 12 77 * @param day day number from 1 to 31 78 * @param hour hour number from 0 to 23 79 * @param minute minute number from 0 to 59 80 * @param second second number from 0.0 to 60.0 (excluded) 81 * @exception IllegalArgumentException if inconsistent arguments 82 * are given (parameters out of range, february 29 for non-leap years, 83 * dates during the gregorian leap in 1582 ...) 84 */ 85 public DateTimeComponents(final int year, final int month, final int day, 86 final int hour, final int minute, final double second) 87 throws IllegalArgumentException { 88 this(year, month, day, hour, minute, new TimeOffset(second)); 89 } 90 91 /** Build an instance from raw level components. 92 * @param year year number (may be 0 or negative for BC years) 93 * @param month month number from 1 to 12 94 * @param day day number from 1 to 31 95 * @param hour hour number from 0 to 23 96 * @param minute minute number from 0 to 59 97 * @param second second number from 0.0 to 60.0 (excluded) 98 * @exception IllegalArgumentException if inconsistent arguments 99 * are given (parameters out of range, february 29 for non-leap years, 100 * dates during the gregorian leap in 1582 ...) 101 * @since 13.0 102 */ 103 public DateTimeComponents(final int year, final int month, final int day, 104 final int hour, final int minute, final TimeOffset second) 105 throws IllegalArgumentException { 106 this.date = new DateComponents(year, month, day); 107 this.time = new TimeComponents(hour, minute, second); 108 } 109 110 /** Build an instance from raw level components. 111 * @param year year number (may be 0 or negative for BC years) 112 * @param month month enumerate 113 * @param day day number from 1 to 31 114 * @param hour hour number from 0 to 23 115 * @param minute minute number from 0 to 59 116 * @param second second number from 0.0 to 60.0 (excluded) 117 * @exception IllegalArgumentException if inconsistent arguments 118 * are given (parameters out of range, february 29 for non-leap years, 119 * dates during the gregorian leap in 1582 ...) 120 */ 121 public DateTimeComponents(final int year, final Month month, final int day, 122 final int hour, final int minute, final double second) 123 throws IllegalArgumentException { 124 this(year, month, day, hour, minute, new TimeOffset(second)); 125 } 126 127 /** Build an instance from raw level components. 128 * @param year year number (may be 0 or negative for BC years) 129 * @param month month enumerate 130 * @param day day number from 1 to 31 131 * @param hour hour number from 0 to 23 132 * @param minute minute number from 0 to 59 133 * @param second second number from 0.0 to 60.0 (excluded) 134 * @exception IllegalArgumentException if inconsistent arguments 135 * are given (parameters out of range, february 29 for non-leap years, 136 * dates during the gregorian leap in 1582 ...) 137 * @since 13.0 138 */ 139 public DateTimeComponents(final int year, final Month month, final int day, 140 final int hour, final int minute, final TimeOffset second) 141 throws IllegalArgumentException { 142 this.date = new DateComponents(year, month, day); 143 this.time = new TimeComponents(hour, minute, second); 144 } 145 146 /** Build an instance from raw level components. 147 * <p>The hour is set to 00:00:00.000.</p> 148 * @param year year number (may be 0 or negative for BC years) 149 * @param month month number from 1 to 12 150 * @param day day number from 1 to 31 151 * @exception IllegalArgumentException if inconsistent arguments 152 * are given (parameters out of range, february 29 for non-leap years, 153 * dates during the gregorian leap in 1582 ...) 154 */ 155 public DateTimeComponents(final int year, final int month, final int day) 156 throws IllegalArgumentException { 157 this.date = new DateComponents(year, month, day); 158 this.time = TimeComponents.H00; 159 } 160 161 /** Build an instance from raw level components. 162 * <p>The hour is set to 00:00:00.000.</p> 163 * @param year year number (may be 0 or negative for BC years) 164 * @param month month enumerate 165 * @param day day number from 1 to 31 166 * @exception IllegalArgumentException if inconsistent arguments 167 * are given (parameters out of range, february 29 for non-leap years, 168 * dates during the gregorian leap in 1582 ...) 169 */ 170 public DateTimeComponents(final int year, final Month month, final int day) 171 throws IllegalArgumentException { 172 this.date = new DateComponents(year, month, day); 173 this.time = TimeComponents.H00; 174 } 175 176 /** Build an instance from a seconds offset with respect to another one. 177 * @param reference reference date/time 178 * @param offset offset from the reference in seconds 179 * @see #offsetFrom(DateTimeComponents) 180 */ 181 public DateTimeComponents(final DateTimeComponents reference, final double offset) { 182 this(reference, new TimeOffset(offset)); 183 } 184 185 /** Build an instance from a seconds offset with respect to another one. 186 * @param reference reference date/time 187 * @param offset offset from the reference in seconds 188 * @see #offsetFrom(DateTimeComponents) 189 * @since 13.0 190 */ 191 public DateTimeComponents(final DateTimeComponents reference, final TimeOffset offset) { 192 193 // extract linear data from reference date/time 194 int day = reference.getDate().getJ2000Day(); 195 TimeOffset seconds = reference.getTime().getSplitSecondsInLocalDay(); 196 197 // apply offset 198 seconds = seconds.add(offset); 199 200 // fix range 201 final int dayShift = (int) FastMath.floor(seconds.toDouble() / Constants.JULIAN_DAY); 202 if (dayShift != 0) { 203 seconds = seconds.subtract(new TimeOffset(dayShift * TimeOffset.DAY.getSeconds(), 0L)); 204 } 205 day += dayShift; 206 final TimeComponents tmpTime = new TimeComponents(seconds); 207 208 // set up components 209 this.date = new DateComponents(day); 210 this.time = new TimeComponents(tmpTime.getHour(), tmpTime.getMinute(), tmpTime.getSplitSecond(), 211 reference.getTime().getMinutesFromUTC()); 212 213 } 214 215 /** Build an instance from a seconds offset with respect to another one. 216 * @param reference reference date/time 217 * @param offset offset from the reference 218 * @param timeUnit the {@link TimeUnit} for the offset 219 * @see #offsetFrom(DateTimeComponents, TimeUnit) 220 * @since 12.1 221 */ 222 public DateTimeComponents(final DateTimeComponents reference, 223 final long offset, final TimeUnit timeUnit) { 224 225 // extract linear data from reference date/time 226 int day = reference.getDate().getJ2000Day(); 227 TimeOffset seconds = reference.getTime().getSplitSecondsInLocalDay(); 228 229 // apply offset 230 seconds = seconds.add(new TimeOffset(offset, timeUnit)); 231 232 // fix range 233 final long dayShift = seconds.getSeconds() / TimeOffset.DAY.getSeconds() + 234 (seconds.getSeconds() < 0L ? -1L : 0L); 235 if (dayShift != 0) { 236 seconds = seconds.subtract(new TimeOffset(dayShift, TimeOffset.DAY)); 237 day += dayShift; 238 } 239 final TimeComponents tmpTime = new TimeComponents(seconds); 240 241 // set up components 242 this.date = new DateComponents(day); 243 this.time = new TimeComponents(tmpTime.getHour(), tmpTime.getMinute(), tmpTime.getSplitSecond(), 244 reference.getTime().getMinutesFromUTC()); 245 246 } 247 248 /** Parse a string in ISO-8601 format to build a date/time. 249 * <p>The supported formats are all date formats supported by {@link DateComponents#parseDate(String)} 250 * and all time formats supported by {@link TimeComponents#parseTime(String)} separated 251 * by the standard time separator 'T', or date components only (in which case a 00:00:00 hour is 252 * implied). Typical examples are 2000-01-01T12:00:00Z or 1976W186T210000. 253 * </p> 254 * @param string string to parse 255 * @return a parsed date/time 256 * @exception IllegalArgumentException if string cannot be parsed 257 */ 258 public static DateTimeComponents parseDateTime(final String string) { 259 260 // is there a time ? 261 final int tIndex = string.indexOf('T'); 262 if (tIndex > 0) { 263 return new DateTimeComponents(DateComponents.parseDate(string.substring(0, tIndex)), 264 TimeComponents.parseTime(string.substring(tIndex + 1))); 265 } 266 267 return new DateTimeComponents(DateComponents.parseDate(string), TimeComponents.H00); 268 269 } 270 271 /** Compute the seconds offset between two instances. 272 * @param dateTime dateTime to subtract from the instance 273 * @return offset in seconds between the two instants 274 * (positive if the instance is posterior to the argument) 275 * @see #DateTimeComponents(DateTimeComponents, TimeOffset) 276 */ 277 public double offsetFrom(final DateTimeComponents dateTime) { 278 final int dateOffset = date.getJ2000Day() - dateTime.date.getJ2000Day(); 279 final TimeOffset timeOffset = time.getSplitSecondsInUTCDay(). 280 subtract(dateTime.time.getSplitSecondsInUTCDay()); 281 return Constants.JULIAN_DAY * dateOffset + timeOffset.toDouble(); 282 } 283 284 /** Compute the seconds offset between two instances. 285 * @param dateTime dateTime to subtract from the instance 286 * @param timeUnit the desired {@link TimeUnit} 287 * @return offset in the given timeunit between the two instants (positive 288 * if the instance is posterior to the argument), rounded to the nearest integer {@link TimeUnit} 289 * @see #DateTimeComponents(DateTimeComponents, long, TimeUnit) 290 * @since 12.1 291 */ 292 public long offsetFrom(final DateTimeComponents dateTime, final TimeUnit timeUnit) { 293 final int dateOffset = date.getJ2000Day() - dateTime.date.getJ2000Day(); 294 final TimeOffset timeOffset = time.getSplitSecondsInUTCDay(). 295 subtract(dateTime.time.getSplitSecondsInUTCDay()); 296 return TimeOffset.DAY.getRoundedTime(timeUnit) * dateOffset + timeOffset.getRoundedTime(timeUnit); 297 } 298 299 /** Get the date component. 300 * @return date component 301 */ 302 public DateComponents getDate() { 303 return date; 304 } 305 306 /** Get the time component. 307 * @return time component 308 */ 309 public TimeComponents getTime() { 310 return time; 311 } 312 313 /** {@inheritDoc} */ 314 public int compareTo(final DateTimeComponents other) { 315 final int dateComparison = date.compareTo(other.date); 316 if (dateComparison < 0) { 317 return -1; 318 } else if (dateComparison > 0) { 319 return 1; 320 } 321 return time.compareTo(other.time); 322 } 323 324 /** {@inheritDoc} */ 325 public boolean equals(final Object other) { 326 try { 327 final DateTimeComponents otherDateTime = (DateTimeComponents) other; 328 return otherDateTime != null && 329 date.equals(otherDateTime.date) && time.equals(otherDateTime.time); 330 } catch (ClassCastException cce) { 331 return false; 332 } 333 } 334 335 /** {@inheritDoc} */ 336 public int hashCode() { 337 return (date.hashCode() << 16) ^ time.hashCode(); 338 } 339 340 /** Return a string representation of this pair. 341 * <p>The format used is ISO8601 including the UTC offset.</p> 342 * @return string representation of this pair 343 */ 344 public String toString() { 345 return date.toString() + 'T' + time.toString(); 346 } 347 348 /** 349 * Get a string representation of the date-time without the offset from UTC. The 350 * format used is ISO6801, except without the offset from UTC. 351 * 352 * @return a string representation of the date-time. 353 * @see #toStringWithoutUtcOffset(int, int) 354 * @see #toString(int, int) 355 * @see #toStringRfc3339() 356 */ 357 public String toStringWithoutUtcOffset() { 358 return date.toString() + 'T' + time.toStringWithoutUtcOffset(); 359 } 360 361 362 /** 363 * Return a string representation of this date-time, rounded to millisecond 364 * precision. 365 * 366 * <p>The format used is ISO8601 including the UTC offset.</p> 367 * 368 * @param minuteDuration 60, 61, or 62 seconds depending on the date being close to a 369 * leap second introduction and the magnitude of the leap 370 * second. 371 * @return string representation of this date, time, and UTC offset 372 * @see #toString(int, int) 373 */ 374 public String toString(final int minuteDuration) { 375 return toString(minuteDuration, 3); 376 } 377 378 /** 379 * Return a string representation of this date-time, rounded to the given precision. 380 * 381 * <p>The format used is ISO8601 including the UTC offset.</p> 382 * 383 * @param minuteDuration 59, 60, 61, or 62 seconds depending on the date being close 384 * to a leap second introduction and the magnitude of the leap 385 * second. 386 * @param fractionDigits the number of digits to include after the decimal point in 387 * the string representation of the seconds. The date and time 388 * is first rounded as necessary. {@code fractionDigits} must 389 * be greater than or equal to {@code 0}. 390 * @return string representation of this date, time, and UTC offset 391 * @see #toStringRfc3339() 392 * @see #toStringWithoutUtcOffset() 393 * @see #toStringWithoutUtcOffset(int, int) 394 * @since 11.0 395 */ 396 public String toString(final int minuteDuration, final int fractionDigits) { 397 return toStringWithoutUtcOffset(minuteDuration, fractionDigits) + 398 time.formatUtcOffset(); 399 } 400 401 /** 402 * Return a string representation of this date-time, rounded to the given precision. 403 * 404 * <p>The format used is ISO8601 without the UTC offset.</p> 405 * 406 * @param minuteDuration 59, 60, 61, or 62 seconds depending on the date being close 407 * to a leap second introduction and the magnitude of the leap 408 * second. 409 * @param fractionDigits the number of digits to include after the decimal point in 410 * the string representation of the seconds. The date and time 411 * are first rounded as necessary. {@code fractionDigits} must 412 * be greater than or equal to {@code 0}. 413 * @return string representation of this date, time, and UTC offset 414 * @see #toStringRfc3339() 415 * @see #toStringWithoutUtcOffset() 416 * @see #toString(int, int) 417 * @since 11.1 418 */ 419 public String toStringWithoutUtcOffset(final int minuteDuration, 420 final int fractionDigits) { 421 final DateTimeComponents rounded = roundIfNeeded(minuteDuration, fractionDigits); 422 return rounded.getDate().toString() + 'T' + 423 rounded.getTime().toStringWithoutUtcOffset(fractionDigits); 424 } 425 426 /** 427 * Round this date-time to the given precision if needed to prevent rounding up to an 428 * invalid seconds number. This is useful, for example, when writing custom date-time 429 * formatting methods so one does not, e.g., end up with "60.0" seconds during a 430 * normal minute when the value of seconds is {@code 59.999}. This method will instead 431 * round up the minute, hour, day, month, and year as needed. 432 * 433 * @param minuteDuration 59, 60, 61, or 62 seconds depending on the date being close 434 * to a leap second introduction and the magnitude of the leap 435 * second. 436 * @param fractionDigits the number of decimal digits after the decimal point in the 437 * seconds number that will be printed. This date-time is 438 * rounded to {@code fractionDigits} after the decimal point if 439 * necessary to prevent rounding up to {@code minuteDuration}. 440 * {@code fractionDigits} must be greater than or equal to 441 * {@code 0}. 442 * @return a date-time within {@code 0.5 * 10**-fractionDigits} seconds of this, and 443 * with a seconds number that will not round up to {@code minuteDuration} when rounded 444 * to {@code fractionDigits} after the decimal point. 445 * @since 11.3 446 */ 447 public DateTimeComponents roundIfNeeded(final int minuteDuration, final int fractionDigits) { 448 449 final TimeComponents wrappedTime = time.wrapIfNeeded(minuteDuration, fractionDigits); 450 if (wrappedTime == time) { 451 // no wrapping was needed 452 return this; 453 } else { 454 if (wrappedTime.getHour() < time.getHour()) { 455 // we have wrapped around next day 456 return new DateTimeComponents(new DateComponents(date, 1), wrappedTime); 457 } else { 458 // only the time was wrapped 459 return new DateTimeComponents(date, wrappedTime); 460 } 461 } 462 463 } 464 465 /** 466 * Represent the given date and time as a string according to the format in RFC 3339. 467 * RFC3339 is a restricted subset of ISO 8601 with a well defined grammar. This method 468 * includes enough precision to represent the point in time without rounding up to the 469 * next minute. 470 * 471 * <p>RFC3339 is unable to represent BC years, years of 10000 or more, time zone 472 * offsets of 100 hours or more, or NaN. In these cases the value returned from this 473 * method will not be valid RFC3339 format. 474 * 475 * @return RFC 3339 format string. 476 * @see <a href="https://tools.ietf.org/html/rfc3339#page-8">RFC 3339</a> 477 * @see AbsoluteDate#toStringRfc3339(TimeScale) 478 * @see #toString(int, int) 479 * @see #toStringWithoutUtcOffset() 480 */ 481 public String toStringRfc3339() { 482 final StringBuilder builder = new StringBuilder(); 483 final DateComponents d = this.getDate(); 484 final TimeComponents t = this.getTime(); 485 try { 486 // date 487 PADDED_FOUR_DIGITS_INTEGER.appendTo(builder, d.getYear()); 488 builder.append('-'); 489 PADDED_TWO_DIGITS_INTEGER.appendTo(builder, d.getMonth()); 490 builder.append('-'); 491 PADDED_TWO_DIGITS_INTEGER.appendTo(builder, d.getDay()); 492 builder.append('T'); 493 // time 494 if (!t.getSplitSecondsInLocalDay().isZero()) { 495 final String formatted = t.toStringWithoutUtcOffset(18); 496 int last = formatted.length() - 1; 497 while (formatted.charAt(last) == '0') { 498 // we want to remove final zeros 499 --last; 500 } 501 if (formatted.charAt(last) == '.') { 502 // remove the decimal point if no decimals follow 503 --last; 504 } 505 builder.append(formatted.substring(0, last + 1)); 506 } else { 507 // shortcut for midnight local time 508 builder.append("00:00:00"); 509 } 510 // offset 511 final int minutesFromUTC = t.getMinutesFromUTC(); 512 if (minutesFromUTC == 0) { 513 builder.append("Z"); 514 } else { 515 // sign must be accounted for separately because there is no -0 in Java. 516 final String sign = minutesFromUTC < 0 ? "-" : "+"; 517 final int utcOffset = FastMath.abs(minutesFromUTC); 518 final int hourOffset = utcOffset / 60; 519 final int minuteOffset = utcOffset % 60; 520 builder.append(sign); 521 PADDED_TWO_DIGITS_INTEGER.appendTo(builder, hourOffset); 522 builder.append(':'); 523 PADDED_TWO_DIGITS_INTEGER.appendTo(builder, minuteOffset); 524 } 525 return builder.toString(); 526 } catch (IOException ioe) { 527 // this should never happen 528 throw new OrekitInternalError(ioe); 529 } 530 } 531 532 } 533