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 }