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