1 /* Copyright 2002-2016 CS Systèmes d'Information 2 * Licensed to CS Systèmes d'Information (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 import java.util.regex.Matcher; 24 import java.util.regex.Pattern; 25 26 import org.apache.commons.math3.util.FastMath; 27 import org.orekit.errors.OrekitIllegalArgumentException; 28 import org.orekit.errors.OrekitMessages; 29 30 31 /** Class representing a time within the day broken up as hour, 32 * minute and second components. 33 * <p>Instances of this class are guaranteed to be immutable.</p> 34 * @see DateComponents 35 * @see DateTimeComponents 36 * @author Luc Maisonobe 37 */ 38 public class TimeComponents implements Serializable, Comparable<TimeComponents> { 39 40 /** Constant for commonly used hour 00:00:00. */ 41 public static final TimeComponents H00 = new TimeComponents(0, 0, 0); 42 43 /** Constant for commonly used hour 12:00:00. */ 44 public static final TimeComponents H12 = new TimeComponents(12, 0, 0); 45 46 /** Serializable UID. */ 47 private static final long serialVersionUID = 20160331L; 48 49 /** Format for hours and minutes. */ 50 private static final DecimalFormat TWO_DIGITS = new DecimalFormat("00"); 51 52 /** Format for seconds. */ 53 private static final DecimalFormat SECONDS_FORMAT = 54 new DecimalFormat("00.000", new DecimalFormatSymbols(Locale.US)); 55 56 /** Basic and extends formats for local time, UTC time. */ 57 private static Pattern ISO8601_FORMATS = Pattern.compile("^(\\d\\d):?(\\d\\d):?(\\d\\d(?:[.,]\\d+)?)?(?:Z|([-+]\\d\\d(?::?\\d\\d)?))?$"); 58 59 /** Hour number. */ 60 private final int hour; 61 62 /** Minute number. */ 63 private final int minute; 64 65 /** Second number. */ 66 private final double second; 67 68 /** Offset between the specified date and UTC. 69 * <p> 70 * Always an integral number of minutes, as per ISO-8601 standard. 71 * </p> 72 * @since 7.2 73 */ 74 private final int minutesFromUTC; 75 76 /** Build a time from its clock elements. 77 * <p>Note that seconds between 60.0 (inclusive) and 61.0 (exclusive) are allowed 78 * in this method, since they do occur during leap seconds introduction 79 * in the {@link UTCScale UTC} time scale.</p> 80 * @param hour hour number from 0 to 23 81 * @param minute minute number from 0 to 59 82 * @param second second number from 0.0 to 61.0 (excluded) 83 * @exception IllegalArgumentException if inconsistent arguments 84 * are given (parameters out of range) 85 */ 86 public TimeComponents(final int hour, final int minute, final double second) 87 throws IllegalArgumentException { 88 this(hour, minute, second, 0); 89 } 90 91 /** Build a time from its clock elements. 92 * <p>Note that seconds between 60.0 (inclusive) and 61.0 (exclusive) are allowed 93 * in this method, since they do occur during leap seconds introduction 94 * in the {@link UTCScale UTC} time scale.</p> 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 61.0 (excluded) 98 * @param minutesFromUTC offset between the specified date and UTC, as an 99 * integral number of minutes, as per ISO-8601 standard 100 * @exception IllegalArgumentException if inconsistent arguments 101 * are given (parameters out of range) 102 * @since 7.2 103 */ 104 public TimeComponents(final int hour, final int minute, final double second, 105 final int minutesFromUTC) 106 throws IllegalArgumentException { 107 108 // range check 109 if ((hour < 0) || (hour > 23) || 110 (minute < 0) || (minute > 59) || 111 (second < 0) || (second >= 61.0)) { 112 throw new OrekitIllegalArgumentException(OrekitMessages.NON_EXISTENT_HMS_TIME, 113 hour, minute, second); 114 } 115 116 this.hour = hour; 117 this.minute = minute; 118 this.second = second; 119 this.minutesFromUTC = minutesFromUTC; 120 121 } 122 123 /** Build a time from the second number within the day. 124 * <p> 125 * This constructor is always in UTC (i.e. {@link #getMinutesFromUTC() will return 0}). 126 * </p> 127 * @param secondInDay second number from 0.0 to {@link 128 * org.orekit.utils.Constants#JULIAN_DAY} (excluded) 129 * @exception IllegalArgumentException if seconds number is out of range 130 */ 131 public TimeComponents(final double secondInDay) { 132 this(0, secondInDay); 133 } 134 135 /** Build a time from the second number within the day. 136 * <p> 137 * The second number is defined here as the sum 138 * {@code secondInDayA + secondInDayB} from 0.0 to {@link 139 * org.orekit.utils.Constants#JULIAN_DAY} (excluded). The two parameters 140 * are used for increased accuracy. 141 * </p> 142 * <p> 143 * This constructor is always in UTC (i.e. {@link #getMinutesFromUTC() will return 0}). 144 * </p> 145 * @param secondInDayA first part of the second number 146 * @param secondInDayB last part of the second number 147 * @exception IllegalArgumentException if seconds number is out of range 148 */ 149 public TimeComponents(final int secondInDayA, final double secondInDayB) { 150 151 // split the numbers as a whole number of seconds 152 // and a fractional part between 0.0 (included) and 1.0 (excluded) 153 final int carry = (int) FastMath.floor(secondInDayB); 154 int wholeSeconds = secondInDayA + carry; 155 final double fractional = secondInDayB - carry; 156 157 // range check 158 if (wholeSeconds < 0 || wholeSeconds > 86400) { 159 // beware, 86400 must be allowed to cope with leap seconds introduction days 160 throw new OrekitIllegalArgumentException(OrekitMessages.OUT_OF_RANGE_SECONDS_NUMBER, 161 wholeSeconds); 162 } 163 164 // extract the time components 165 hour = wholeSeconds / 3600; 166 wholeSeconds -= 3600 * hour; 167 minute = wholeSeconds / 60; 168 wholeSeconds -= 60 * minute; 169 second = wholeSeconds + fractional; 170 minutesFromUTC = 0; 171 172 } 173 174 /** Parse a string in ISO-8601 format to build a time. 175 * <p>The supported formats are: 176 * <ul> 177 * <li>basic and extended format local time: hhmmss, hh:mm:ss (with optional decimals in seconds)</li> 178 * <li>optional UTC time: hhmmssZ, hh:mm:ssZ</li> 179 * <li>optional signed hours UTC offset: hhmmss+HH, hhmmss-HH, hh:mm:ss+HH, hh:mm:ss-HH</li> 180 * <li>optional signed basic hours and minutes UTC offset: hhmmss+HHMM, hhmmss-HHMM, hh:mm:ss+HHMM, hh:mm:ss-HHMM</li> 181 * <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> 182 * </ul> 183 * As shown by the list above, only the complete representations defined in section 4.2 184 * of ISO-8601 standard are supported, neither expended representations nor representations 185 * with reduced accuracy are supported. 186 * </p> 187 * @param string string to parse 188 * @return a parsed time 189 * @exception IllegalArgumentException if string cannot be parsed 190 */ 191 public static TimeComponents parseTime(final String string) { 192 193 // is the date a calendar date ? 194 final Matcher timeMatcher = ISO8601_FORMATS.matcher(string); 195 if (timeMatcher.matches()) { 196 final int hour = Integer.parseInt(timeMatcher.group(1)); 197 final int minute = Integer.parseInt(timeMatcher.group(2)); 198 final double second = Double.parseDouble(timeMatcher.group(3).replace(',', '.')); 199 final String offset = timeMatcher.group(4); 200 final int minutesFromUTC; 201 if (offset == null) { 202 // no offset from UTC is given 203 minutesFromUTC = 0; 204 } else { 205 // we need to parse an offset from UTC 206 // the sign is mandatory and the ':' separator is optional 207 // so we can have offsets given as -06:00 or +0100 208 final int sign = offset.codePointAt(0) == '-' ? -1 : +1; 209 final int hourOffset = Integer.parseInt(offset.substring(1, 3)); 210 final int minutesOffset = offset.length() <= 3 ? 0 : Integer.parseInt(offset.substring(offset.length() - 2)); 211 minutesFromUTC = sign * (minutesOffset + 60 * hourOffset); 212 } 213 return new TimeComponents(hour, minute, second, minutesFromUTC); 214 } 215 216 throw new OrekitIllegalArgumentException(OrekitMessages.NON_EXISTENT_TIME, string); 217 218 } 219 220 /** Get the hour number. 221 * @return hour number from 0 to 23 222 */ 223 public int getHour() { 224 return hour; 225 } 226 227 /** Get the minute number. 228 * @return minute minute number from 0 to 59 229 */ 230 public int getMinute() { 231 return minute; 232 } 233 234 /** Get the seconds number. 235 * @return second second number from 0.0 to 60.0 (excluded) 236 */ 237 public double getSecond() { 238 return second; 239 } 240 241 /** Get the offset between the specified date and UTC. 242 * <p> 243 * The offset is always an integral number of minutes, as per ISO-8601 standard. 244 * </p> 245 * @return offset in minutes between the specified date and UTC 246 * @since 7.2 247 */ 248 public int getMinutesFromUTC() { 249 return minutesFromUTC; 250 } 251 252 /** Get the second number within the day. 253 * @return second number from 0.0 to Constants.JULIAN_DAY 254 * @deprecated as of 7.2, replaced with either {@link #getSecondsInLocalDay()} 255 * or {@link #getSecondsInUTCDay()} 256 */ 257 @Deprecated 258 public double getSecondsInDay() { 259 return getSecondsInLocalDay(); 260 } 261 262 /** Get the second number within the local day, <em>without</em> applying the {@link #getMinutesFromUTC() offset from UTC}. 263 * @return second number from 0.0 to Constants.JULIAN_DAY 264 * @see #getSecondsInUTCDay() 265 * @since 7.2 266 */ 267 public double getSecondsInLocalDay() { 268 return second + 60 * minute + 3600 * hour; 269 } 270 271 /** Get the second number within the UTC day, applying the {@link #getMinutesFromUTC() offset from UTC}. 272 * @return second number from {@link #getMinutesFromUTC() -getMinutesFromUTC()} 273 * to Constants.JULIAN_DAY {@link #getMinutesFromUTC() + getMinutesFromUTC()} 274 * @see #getSecondsInLocalDay() 275 * @since 7.2 276 */ 277 public double getSecondsInUTCDay() { 278 return second + 60 * (minute - minutesFromUTC) + 3600 * hour; 279 } 280 281 /** Get a string representation of the time. 282 * @return string representation of the time 283 */ 284 public String toString() { 285 StringBuilder builder = new StringBuilder(). 286 append(TWO_DIGITS.format(hour)).append(':'). 287 append(TWO_DIGITS.format(minute)).append(':'). 288 append(SECONDS_FORMAT.format(second)); 289 if (minutesFromUTC != 0) { 290 builder = builder. 291 append(minutesFromUTC < 0 ? '-' : '+'). 292 append(TWO_DIGITS.format(FastMath.abs(minutesFromUTC) / 60)).append(':'). 293 append(TWO_DIGITS.format(FastMath.abs(minutesFromUTC) % 60)); 294 } 295 return builder.toString(); 296 } 297 298 /** {@inheritDoc} */ 299 public int compareTo(final TimeComponents other) { 300 return Double.compare(getSecondsInUTCDay(), other.getSecondsInUTCDay()); 301 } 302 303 /** {@inheritDoc} */ 304 public boolean equals(final Object other) { 305 try { 306 final TimeComponents otherTime = (TimeComponents) other; 307 return otherTime != null && 308 hour == otherTime.hour && 309 minute == otherTime.minute && 310 second == otherTime.second && 311 minutesFromUTC == otherTime.minutesFromUTC; 312 } catch (ClassCastException cce) { 313 return false; 314 } 315 } 316 317 /** {@inheritDoc} */ 318 public int hashCode() { 319 final long bits = Double.doubleToLongBits(second); 320 return ((hour << 16) ^ ((minute - minutesFromUTC) << 8)) ^ (int) (bits ^ (bits >>> 32)); 321 } 322 323 }