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 }