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