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