1   /* Copyright 2002-2025 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.util.ArrayList;
20  import java.util.Arrays;
21  import java.util.Collection;
22  import java.util.Comparator;
23  import java.util.List;
24  
25  import org.hipparchus.CalculusFieldElement;
26  import org.hipparchus.util.FastMath;
27  
28  /** Coordinated Universal Time.
29   * <p>UTC is related to TAI using step adjustments from time to time
30   * according to IERS (International Earth Rotation Service) rules. Before 1972,
31   * these adjustments were piecewise linear offsets. Since 1972, these adjustments
32   * are piecewise constant offsets, which require introduction of leap seconds.</p>
33   * <p>Leap seconds are always inserted as additional seconds at the last minute
34   * of the day, pushing the next day forward. Such minutes are therefore more
35   * than 60 seconds long. In theory, there may be seconds removal instead of seconds
36   * insertion, but up to now (2010) it has never been used. As an example, when a
37   * one second leap was introduced at the end of 2005, the UTC time sequence was
38   * 2005-12-31T23:59:59 UTC, followed by 2005-12-31T23:59:60 UTC, followed by
39   * 2006-01-01T00:00:00 UTC.</p>
40   * <p>This is intended to be accessed thanks to {@link TimeScales},
41   * so there is no public constructor.</p>
42   * @author Luc Maisonobe
43   * @see AbsoluteDate
44   */
45  public class UTCScale implements TimeScale {
46  
47      /** Number of seconds in one day. */
48      private static final long SEC_PER_DAY = 86400L;
49  
50      /** Number of attoseconds in one second. */
51      private static final long ATTOS_PER_NANO = 1000000000L;
52  
53      /** Slope conversion factor from seconds per day to nanoseconds per second. */
54      private static final long SLOPE_FACTOR = SEC_PER_DAY * ATTOS_PER_NANO;
55  
56      /** International Atomic Scale. */
57      private final TimeScale tai;
58  
59      /** base UTC-TAI offsets (may lack the pre-1975 offsets). */
60      private final Collection<? extends OffsetModel> baseOffsets;
61  
62      /** UTC-TAI offsets. */
63      private final UTCTAIOffset[] offsets;
64  
65      /** Package private constructor for the factory.
66       * Used to create the prototype instance of this class that is used to
67       * clone all subsequent instances of {@link UTCScale}. Initializes the offset
68       * table that is shared among all instances.
69       * @param tai TAI time scale this UTC time scale references.
70       * @param baseOffsets UTC-TAI base offsets (may lack the pre-1975 offsets)
71       */
72      UTCScale(final TimeScale tai, final Collection<? extends OffsetModel> baseOffsets) {
73  
74          this.tai         = tai;
75          this.baseOffsets = baseOffsets;
76  
77          // copy input so the original list is unmodified
78          final List<OffsetModel> offsetModels = new ArrayList<>(baseOffsets);
79          offsetModels.sort(Comparator.comparing(OffsetModel::getStart));
80          if (offsetModels.get(0).getStart().getYear() > 1968) {
81              // the pre-1972 linear offsets are missing, add them manually
82              // excerpt from UTC-TAI.history file:
83              //  1961  Jan.  1 - 1961  Aug.  1     1.422 818 0s + (MJD - 37 300) x 0.001 296s
84              //        Aug.  1 - 1962  Jan.  1     1.372 818 0s +        ""
85              //  1962  Jan.  1 - 1963  Nov.  1     1.845 858 0s + (MJD - 37 665) x 0.001 123 2s
86              //  1963  Nov.  1 - 1964  Jan.  1     1.945 858 0s +        ""
87              //  1964  Jan.  1 -       April 1     3.240 130 0s + (MJD - 38 761) x 0.001 296s
88              //        April 1 -       Sept. 1     3.340 130 0s +        ""
89              //        Sept. 1 - 1965  Jan.  1     3.440 130 0s +        ""
90              //  1965  Jan.  1 -       March 1     3.540 130 0s +        ""
91              //        March 1 -       Jul.  1     3.640 130 0s +        ""
92              //        Jul.  1 -       Sept. 1     3.740 130 0s +        ""
93              //        Sept. 1 - 1966  Jan.  1     3.840 130 0s +        ""
94              //  1966  Jan.  1 - 1968  Feb.  1     4.313 170 0s + (MJD - 39 126) x 0.002 592s
95              //  1968  Feb.  1 - 1972  Jan.  1     4.213 170 0s +        ""
96              // the slopes in second per day correspond in fact to values in scaled nanoseconds per seconds:
97              //  0.0012960 s/d → 15 ns/s
98              //  0.0011232 s/d → 13 ns/s
99              //  0.0025920 s/d → 30 ns/s
100             // CHECKSTYLE: stop MultipleStringLiterals check
101             offsetModels.add( 0, linearModel(1961,  1, 1, 37300, "1.4228180", "0.001296"));
102             offsetModels.add( 1, linearModel(1961,  8, 1, 37300, "1.3728180", "0.001296"));
103             offsetModels.add( 2, linearModel(1962,  1, 1, 37665, "1.8458580", "0.0011232"));
104             offsetModels.add( 3, linearModel(1963, 11, 1, 37665, "1.9458580", "0.0011232"));
105             offsetModels.add( 4, linearModel(1964,  1, 1, 38761, "3.2401300", "0.001296"));
106             offsetModels.add( 5, linearModel(1964,  4, 1, 38761, "3.3401300", "0.001296"));
107             offsetModels.add( 6, linearModel(1964,  9, 1, 38761, "3.4401300", "0.001296"));
108             offsetModels.add( 7, linearModel(1965,  1, 1, 38761, "3.5401300", "0.001296"));
109             offsetModels.add( 8, linearModel(1965,  3, 1, 38761, "3.6401300", "0.001296"));
110             offsetModels.add( 9, linearModel(1965,  7, 1, 38761, "3.7401300", "0.001296"));
111             offsetModels.add(10, linearModel(1965,  9, 1, 38761, "3.8401300", "0.001296"));
112             offsetModels.add(11, linearModel(1966,  1, 1, 39126, "4.3131700", "0.002592"));
113             offsetModels.add(12, linearModel(1968,  2, 1, 39126, "4.2131700", "0.002592"));
114             // CHECKSTYLE: resume MultipleStringLiterals check
115         }
116 
117         // create cache
118         this.offsets = new UTCTAIOffset[offsetModels.size()];
119 
120         UTCTAIOffset previous = null;
121 
122         // link the offsets together
123         for (int i = 0; i < offsetModels.size(); ++i) {
124 
125             final OffsetModel    o      = offsetModels.get(i);
126             final DateComponents date   = o.getStart();
127             final int            mjdRef = o.getMJDRef();
128             final TimeOffset offset = o.getOffset();
129             final int            slope  = o.getSlope();
130 
131             // start of the leap
132             final TimeOffset previousOffset = (previous == null) ?
133                                               TimeOffset.ZERO :
134                                               previous.getOffset(date, TimeComponents.H00);
135             final AbsoluteDate leapStart   = new AbsoluteDate(date, tai).shiftedBy(previousOffset);
136 
137             // end of the leap
138             final long         dt          = (date.getMJD() - mjdRef) * SEC_PER_DAY;
139             final TimeOffset drift       = TimeOffset.NANOSECOND.multiply(slope * FastMath.abs(dt));
140             final TimeOffset startOffset = dt < 0 ? offset.subtract(drift) : offset.add(drift);
141             final AbsoluteDate leapEnd     = new AbsoluteDate(date, tai).shiftedBy(startOffset);
142 
143             // leap computed at leap start and in UTC scale
144             final TimeOffset leap           = leapEnd.accurateDurationFrom(leapStart).
145                                              multiply(1000000000).
146                                              divide(1000000000 + slope);
147 
148             final AbsoluteDate reference = AbsoluteDate.createMJDDate(mjdRef, 0, tai).shiftedBy(offset);
149             previous = new UTCTAIOffset(leapStart, date.getMJD(), leap, offset, mjdRef, slope, reference);
150             this.offsets[i] = previous;
151 
152         }
153 
154     }
155 
156     /** Get the base offsets.
157      * @return base offsets (may lack the pre-1975 offsets)
158      * @since 12.0
159      */
160     public Collection<? extends OffsetModel> getBaseOffsets() {
161         return baseOffsets;
162     }
163 
164     /**
165      * Returns the UTC-TAI offsets underlying this UTC scale.
166      * <p>
167      * Modifications to the returned list will not affect this UTC scale instance.
168      * @return new non-null modifiable list of UTC-TAI offsets time-sorted from
169      *         earliest to latest
170      */
171     public List<UTCTAIOffset> getUTCTAIOffsets() {
172         return Arrays.asList(offsets);
173     }
174 
175     /** {@inheritDoc} */
176     @Override
177     public TimeOffset offsetFromTAI(final AbsoluteDate date) {
178         final int offsetIndex = findOffsetIndex(date);
179         if (offsetIndex < 0) {
180             // the date is before the first known leap
181             return TimeOffset.ZERO;
182         } else {
183             return offsets[offsetIndex].getOffset(date).negate();
184         }
185     }
186 
187     /** {@inheritDoc} */
188     @Override
189     public <T extends CalculusFieldElement<T>> T offsetFromTAI(final FieldAbsoluteDate<T> date) {
190         final int offsetIndex = findOffsetIndex(date.toAbsoluteDate());
191         if (offsetIndex < 0) {
192             // the date is before the first known leap
193             return date.getField().getZero();
194         } else {
195             return offsets[offsetIndex].getOffset(date).negate();
196         }
197     }
198 
199     /** {@inheritDoc} */
200     @Override
201     public TimeOffset offsetToTAI(final DateComponents date,
202                                   final TimeComponents time) {
203 
204         // take offset from local time into account, but ignoring seconds,
205         // so when we parse an hour like 23:59:60.5 during leap seconds introduction,
206         // we do not jump to next day
207         final int minuteInDay = time.getHour() * 60 + time.getMinute() - time.getMinutesFromUTC();
208         final int correction  = minuteInDay < 0 ? (minuteInDay - 1439) / 1440 : minuteInDay / 1440;
209 
210         // find close neighbors, assuming date in TAI, i.e a date earlier than real UTC date
211         final int mjd = date.getMJD() + correction;
212         final UTCTAIOffset offset = findOffset(mjd);
213         if (offset == null) {
214             // the date is before the first known leap
215             return TimeOffset.ZERO;
216         } else {
217             return offset.getOffset(date, time);
218         }
219 
220     }
221 
222     /** {@inheritDoc} */
223     public String getName() {
224         return "UTC";
225     }
226 
227     /** {@inheritDoc} */
228     public String toString() {
229         return getName();
230     }
231 
232     /** Get the date of the first known leap second.
233      * @return date of the first known leap second
234      */
235     public AbsoluteDate getFirstKnownLeapSecond() {
236         return offsets[0].getDate();
237     }
238 
239     /** Get the date of the last known leap second.
240      * @return date of the last known leap second
241      */
242     public AbsoluteDate getLastKnownLeapSecond() {
243         return offsets[offsets.length - 1].getDate();
244     }
245 
246     /** {@inheritDoc} */
247     @Override
248     public boolean insideLeap(final AbsoluteDate date) {
249         final int offsetIndex = findOffsetIndex(date);
250         if (offsetIndex < 0) {
251             // the date is before the first known leap
252             return false;
253         } else {
254             return date.compareTo(offsets[offsetIndex].getValidityStart()) < 0;
255         }
256     }
257 
258     /** {@inheritDoc} */
259     @Override
260     public <T extends CalculusFieldElement<T>> boolean insideLeap(final FieldAbsoluteDate<T> date) {
261         return insideLeap(date.toAbsoluteDate());
262     }
263 
264     /** {@inheritDoc} */
265     @Override
266     public int minuteDuration(final AbsoluteDate date) {
267         final int offsetIndex = findOffsetIndex(date);
268         final UTCTAIOffset offset;
269         if (offsetIndex >= 0 &&
270                 date.compareTo(offsets[offsetIndex].getValidityStart()) < 0) {
271             // the date is during the leap itself
272             offset = offsets[offsetIndex];
273         } else if (offsetIndex + 1 < offsets.length &&
274             offsets[offsetIndex + 1].getDate().durationFrom(date) <= 60.0) {
275             // the date is after a leap, but it may be just before the next one
276             // the next leap will start in one minute, it will extend the current minute
277             offset = offsets[offsetIndex + 1];
278         } else {
279             offset = null;
280         }
281         if (offset != null) {
282             // since this method returns an int we can't return the precise duration in
283             // all cases, but we can bound it. Some leaps are more than 1s. See #694
284             return 60 + (int) (offset.getLeap().getSeconds() +
285                                FastMath.min(1, offset.getLeap().getAttoSeconds()));
286         }
287         // no leap is expected within the next minute
288         return 60;
289     }
290 
291     /** {@inheritDoc} */
292     @Override
293     public <T extends CalculusFieldElement<T>> int minuteDuration(final FieldAbsoluteDate<T> date) {
294         return minuteDuration(date.toAbsoluteDate());
295     }
296 
297     /** {@inheritDoc} */
298     @Override
299     public TimeOffset getLeap(final AbsoluteDate date) {
300         final int offsetIndex = findOffsetIndex(date);
301         if (offsetIndex < 0) {
302             // the date is before the first known leap
303             return TimeOffset.ZERO;
304         } else {
305             return offsets[offsetIndex].getLeap();
306         }
307     }
308 
309     /** {@inheritDoc} */
310     @Override
311     public <T extends CalculusFieldElement<T>> T getLeap(final FieldAbsoluteDate<T> date) {
312         return date.getField().getZero().newInstance(getLeap(date.toAbsoluteDate()).toDouble());
313     }
314 
315     /** Find the index of the offset valid at some date.
316      * @param date date at which offset is requested
317      * @return index of the offset valid at this date, or -1 if date is before first offset.
318      */
319     private int findOffsetIndex(final AbsoluteDate date) {
320         int inf = 0;
321         int sup = offsets.length;
322         while (sup - inf > 1) {
323             final int middle = (inf + sup) >>> 1;
324             if (date.compareTo(offsets[middle].getDate()) < 0) {
325                 sup = middle;
326             } else {
327                 inf = middle;
328             }
329         }
330         if (sup == offsets.length) {
331             // the date is after the last known leap second
332             return offsets.length - 1;
333         } else if (date.compareTo(offsets[inf].getDate()) < 0) {
334             // the date is before the first known leap
335             return -1;
336         } else {
337             return inf;
338         }
339     }
340 
341     /** Find the offset valid at some date.
342      * @param mjd Modified Julian Day of the date at which offset is requested
343      * @return offset valid at this date, or null if date is before first offset.
344      */
345     private UTCTAIOffset findOffset(final int mjd) {
346         int inf = 0;
347         int sup = offsets.length;
348         while (sup - inf > 1) {
349             final int middle = (inf + sup) >>> 1;
350             if (mjd < offsets[middle].getMJD()) {
351                 sup = middle;
352             } else {
353                 inf = middle;
354             }
355         }
356         if (sup == offsets.length) {
357             // the date is after the last known leap second
358             return offsets[offsets.length - 1];
359         } else if (mjd < offsets[inf].getMJD()) {
360             // the date is before the first known leap
361             return null;
362         } else {
363             return offsets[inf];
364         }
365     }
366 
367     /** Create a linear model.
368      * @param year year
369      * @param month month
370      * @param day day
371      * @param mjdRef reference date for the linear model
372      * @param offset offset
373      * @param slope slope
374      * @return linear model
375      */
376     private OffsetModel linearModel(final int year, final int month, final int day,
377                                     final int mjdRef, final String offset, final String slope) {
378         return new OffsetModel(new DateComponents(year, month, day),
379                                mjdRef,
380                                TimeOffset.parse(offset),
381                                (int) (TimeOffset.parse(slope).getAttoSeconds()  / SLOPE_FACTOR));
382     }
383 
384 }