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