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 }