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 org.hamcrest.MatcherAssert;
20  import org.hipparchus.random.RandomGenerator;
21  import org.hipparchus.random.Well1024a;
22  import org.hipparchus.util.Binary64Field;
23  import org.junit.jupiter.api.AfterEach;
24  import org.junit.jupiter.api.Assertions;
25  import org.junit.jupiter.api.BeforeEach;
26  import org.junit.jupiter.api.Test;
27  import org.orekit.OrekitMatchers;
28  import org.orekit.Utils;
29  import org.orekit.errors.OrekitException;
30  import org.orekit.errors.OrekitMessages;
31  import org.orekit.utils.Constants;
32  
33  import java.lang.reflect.Field;
34  import java.util.ArrayList;
35  import java.util.Collections;
36  import java.util.List;
37  import java.util.concurrent.ExecutorService;
38  import java.util.concurrent.Executors;
39  import java.util.concurrent.TimeUnit;
40  
41  public class UTCScaleTest {
42  
43      @Test
44      public void testAfter() {
45          AbsoluteDate d1 = new AbsoluteDate(new DateComponents(2020, 12, 31),
46                                             new TimeComponents(23, 59, 59),
47                                             utc);
48          Assertions.assertEquals("2020-12-31T23:59:59.000Z", d1.toString());
49      }
50  
51      @Test
52      public void testNoLeap() {
53          Assertions.assertEquals("UTC", utc.toString());
54          AbsoluteDate d1 = new AbsoluteDate(new DateComponents(1999, 12, 31),
55                                             new TimeComponents(23, 59, 59),
56                                             utc);
57          AbsoluteDate d2 = new AbsoluteDate(new DateComponents(2000, 1, 1),
58                                             new TimeComponents(0, 0, 1),
59                                             utc);
60          Assertions.assertEquals(2.0, d2.durationFrom(d1), 1.0e-10);
61      }
62  
63      @Test
64      public void testLeap2006() {
65          AbsoluteDate leapDate =
66              new AbsoluteDate(new DateComponents(2006, 1, 1), TimeComponents.H00, utc);
67          AbsoluteDate d1 = leapDate.shiftedBy(-1);
68          AbsoluteDate d2 = leapDate.shiftedBy(+1);
69          Assertions.assertEquals(2.0, d2.durationFrom(d1), 1.0e-10);
70  
71          AbsoluteDate d3 = new AbsoluteDate(new DateComponents(2005, 12, 31),
72                                             new TimeComponents(23, 59, 59),
73                                             utc);
74          AbsoluteDate d4 = new AbsoluteDate(new DateComponents(2006, 1, 1),
75                                             new TimeComponents(0, 0, 1),
76                                             utc);
77          Assertions.assertEquals(3.0, d4.durationFrom(d3), 1.0e-10);
78      }
79  
80      @Test
81      public void testDuringLeap() {
82          AbsoluteDate d = new AbsoluteDate(new DateComponents(1983, 6, 30),
83                                            new TimeComponents(23, 59, 59),
84                                            utc);
85          Assertions.assertEquals("1983-06-30T23:58:59.000", d.shiftedBy(-60).toString(utc));
86          Assertions.assertEquals(60, utc.minuteDuration(d.shiftedBy(-60)));
87          Assertions.assertFalse(utc.insideLeap(d.shiftedBy(-60)));
88          Assertions.assertEquals("1983-06-30T23:59:59.000", d.toString(utc));
89          Assertions.assertEquals(61, utc.minuteDuration(d));
90          Assertions.assertFalse(utc.insideLeap(d));
91          d = d.shiftedBy(0.251);
92          Assertions.assertEquals("1983-06-30T23:59:59.251", d.toString(utc));
93          Assertions.assertEquals(61, utc.minuteDuration(d));
94          Assertions.assertFalse(utc.insideLeap(d));
95          d = d.shiftedBy(0.251);
96          Assertions.assertEquals("1983-06-30T23:59:59.502", d.toString(utc));
97          Assertions.assertEquals(61, utc.minuteDuration(d));
98          Assertions.assertFalse(utc.insideLeap(d));
99          d = d.shiftedBy(0.251);
100         Assertions.assertEquals("1983-06-30T23:59:59.753", d.toString(utc));
101         Assertions.assertEquals(61, utc.minuteDuration(d));
102         Assertions.assertFalse(utc.insideLeap(d));
103         d = d.shiftedBy( 0.251);
104         Assertions.assertEquals("1983-06-30T23:59:60.004", d.toString(utc));
105         Assertions.assertEquals(61, utc.minuteDuration(d));
106         Assertions.assertTrue(utc.insideLeap(d));
107         d = d.shiftedBy(0.251);
108         Assertions.assertEquals("1983-06-30T23:59:60.255", d.toString(utc));
109         Assertions.assertEquals(61, utc.minuteDuration(d));
110         Assertions.assertTrue(utc.insideLeap(d));
111         d = d.shiftedBy(0.251);
112         Assertions.assertEquals("1983-06-30T23:59:60.506", d.toString(utc));
113         Assertions.assertEquals(61, utc.minuteDuration(d));
114         d = d.shiftedBy(0.251);
115         Assertions.assertEquals("1983-06-30T23:59:60.757", d.toString(utc));
116         Assertions.assertEquals(61, utc.minuteDuration(d));
117         Assertions.assertTrue(utc.insideLeap(d));
118         d = d.shiftedBy(0.251);
119         Assertions.assertEquals("1983-07-01T00:00:00.008", d.toString(utc));
120         Assertions.assertEquals(60, utc.minuteDuration(d));
121         Assertions.assertFalse(utc.insideLeap(d));
122     }
123 
124     @Test
125     public void testWrapBeforeLeap() {
126         AbsoluteDate t = new AbsoluteDate("2015-06-30T23:59:59.999999", utc);
127         Assertions.assertEquals("2015-06-30T23:59:60.000+00:00",
128                 t.getComponents(utc).toString(utc.minuteDuration(t)));
129     }
130 
131     @Test
132     public void testMinuteDuration() {
133         final AbsoluteDate t0 = new AbsoluteDate("1983-06-30T23:58:59.000", utc);
134         for (double dt = 0; dt < 63; dt += 0.3) {
135             if (dt < 1.0) {
136                 // before the minute of the leap
137                 Assertions.assertEquals(60, utc.minuteDuration(t0.shiftedBy(dt)));
138             } else if (dt < 62.0) {
139                 // during the minute of the leap
140                 Assertions.assertEquals(61, utc.minuteDuration(t0.shiftedBy(dt)));
141             } else {
142                 // after the minute of the leap
143                 Assertions.assertEquals(60, utc.minuteDuration(t0.shiftedBy(dt)));
144             }
145         }
146     }
147 
148     /**
149      * Check the consistency of minute duration with the other data in each offset. Checks
150      * table hard coded in UTCScale.
151      *
152      * @throws ReflectiveOperationException on error.
153      */
154     @Test
155     public void testMinuteDurationConsistentWithLeap() throws ReflectiveOperationException {
156         // setup
157         // get the offsets array, makes this test easier to write
158         Field field = UTCScale.class.getDeclaredField("offsets");
159         field.setAccessible(true);
160         UTCTAIOffset[] offsets = (UTCTAIOffset[]) field.get(utc);
161 
162         // action
163         for (UTCTAIOffset offset : offsets) {
164             // average of start and end of leap second, definitely inside
165             final AbsoluteDate start = offset.getDate();
166             final AbsoluteDate end = offset.getValidityStart();
167             AbsoluteDate d = start.shiftedBy(end.durationFrom(start) / 2.0);
168             int excess = utc.minuteDuration(d) - 60;
169             TimeOffset leap = offset.getLeap();
170             // verify
171             Assertions.assertTrue(leap.toDouble() <= excess, "at MJD" + offset.getMJD() + ": " + leap + " <= " + excess);
172             Assertions.assertTrue(leap.toDouble() > (excess - 1));
173             // before the leap starts but still in the same minute
174             d = start.shiftedBy(-30);
175             int newExcess = utc.minuteDuration(d) - 60;
176             TimeOffset newLeap = offset.getLeap();
177             // verify
178             Assertions.assertTrue(newLeap.toDouble() <= newExcess, "at MJD" + offset.getMJD() + ": " + newLeap + " <= " + newExcess);
179             Assertions.assertTrue(leap.toDouble() > (excess - 1));
180             Assertions.assertEquals(excess, newExcess);
181             Assertions.assertEquals(leap, newLeap);
182             MatcherAssert.assertThat("" + offset.getValidityStart(), leap.toDouble(),
183                                      OrekitMatchers.numberCloseTo(end.durationFrom(start), 1e-16, 1));
184         }
185     }
186 
187     @Test
188     public void testSymmetry() {
189         TimeScale scale = TimeScalesFactory.getGPS();
190         for (double dt = -10000; dt < 10000; dt += 123.456789) {
191             AbsoluteDate date = AbsoluteDate.J2000_EPOCH.shiftedBy(dt * Constants.JULIAN_DAY);
192             double dt1 = scale.offsetFromTAI(date).toDouble();
193             DateTimeComponents components = date.getComponents(scale);
194             double dt2 = scale.offsetToTAI(components.getDate(), components.getTime()).toDouble();
195             Assertions.assertEquals( 0.0, dt1 + dt2, 1.0e-10);
196         }
197     }
198 
199     @Test
200     public void testOffsets() {
201 
202         // we arbitrary put UTC == TAI before 1961-01-01
203         checkOffset(1950,  1,  1,   0);
204 
205         // excerpt from UTC-TAI.history file:
206         //  1961  Jan.  1 - 1961  Aug.  1     1.422 818 0s + (MJD - 37 300) x 0.001 296s
207         //        Aug.  1 - 1962  Jan.  1     1.372 818 0s +        ""
208         //  1962  Jan.  1 - 1963  Nov.  1     1.845 858 0s + (MJD - 37 665) x 0.001 123 2s
209         //  1963  Nov.  1 - 1964  Jan.  1     1.945 858 0s +        ""
210         //  1964  Jan.  1 -       April 1     3.240 130 0s + (MJD - 38 761) x 0.001 296s
211         //        April 1 -       Sept. 1     3.340 130 0s +        ""
212         //        Sept. 1 - 1965  Jan.  1     3.440 130 0s +        ""
213         //  1965  Jan.  1 -       March 1     3.540 130 0s +        ""
214         //        March 1 -       Jul.  1     3.640 130 0s +        ""
215         //        Jul.  1 -       Sept. 1     3.740 130 0s +        ""
216         //        Sept. 1 - 1966  Jan.  1     3.840 130 0s +        ""
217         //  1966  Jan.  1 - 1968  Feb.  1     4.313 170 0s + (MJD - 39 126) x 0.002 592s
218         //  1968  Feb.  1 - 1972  Jan.  1     4.213 170 0s +        ""
219         checkOffset(1961,  1,  2,  -(1.422818 +   1 * 0.001296));  // MJD 37300 +   1
220         checkOffset(1961,  8,  2,  -(1.372818 + 213 * 0.001296));  // MJD 37300 + 213
221         checkOffset(1962,  1,  2,  -(1.845858 +   1 * 0.0011232)); // MJD 37665 +   1
222         checkOffset(1963, 11,  2,  -(1.945858 + 670 * 0.0011232)); // MJD 37665 + 670
223         checkOffset(1964,  1,  2,  -(3.240130 - 365 * 0.001296));  // MJD 38761 - 365
224         checkOffset(1964,  4,  2,  -(3.340130 - 274 * 0.001296));  // MJD 38761 - 274
225         checkOffset(1964,  9,  2,  -(3.440130 - 121 * 0.001296));  // MJD 38761 - 121
226         checkOffset(1965,  1,  2,  -(3.540130 +   1 * 0.001296));  // MJD 38761 +   1
227         checkOffset(1965,  3,  2,  -(3.640130 +  60 * 0.001296));  // MJD 38761 +  60
228         checkOffset(1965,  7,  2,  -(3.740130 + 182 * 0.001296));  // MJD 38761 + 182
229         checkOffset(1965,  9,  2,  -(3.840130 + 244 * 0.001296));  // MJD 38761 + 244
230         checkOffset(1966,  1,  2,  -(4.313170 +   1 * 0.002592));  // MJD 39126 +   1
231         checkOffset(1968,  2,  2,  -(4.213170 + 762 * 0.002592));  // MJD 39126 + 762
232 
233         // since 1972-01-01, offsets are only whole seconds
234         checkOffset(1972,  3,  5, -10);
235         checkOffset(1972,  7, 14, -11);
236         checkOffset(1979, 12, 31, -18);
237         checkOffset(1980,  1, 22, -19);
238         checkOffset(2006,  7,  7, -33);
239 
240     }
241 
242     private void checkOffset(int year, int month, int day, double offset) {
243         AbsoluteDate date = new AbsoluteDate(year, month, day, utc);
244         Assertions.assertEquals(offset, utc.offsetFromTAI(date).toDouble(), 1.0e-10);
245     }
246 
247     @Test
248     public void testCreatingInLeapDateUTC() {
249         AbsoluteDate previous = null;
250         final double step = 0.0625;
251         for (double seconds = 59.0; seconds < 61.0; seconds += step) {
252             final AbsoluteDate date = new AbsoluteDate(2008, 12, 31, 23, 59, seconds, utc);
253             if (previous != null) {
254                 Assertions.assertEquals(step, date.durationFrom(previous), 1.0e-12);
255             }
256             previous = date;
257         }
258         AbsoluteDate ad0 = new AbsoluteDate("2008-12-31T23:59:60", utc);
259         Assertions.assertTrue(ad0.toString(utc).startsWith("2008-12-31T23:59:"));
260         AbsoluteDate ad1 = new AbsoluteDate("2008-12-31T23:59:59", utc).shiftedBy(1);
261         Assertions.assertEquals(0, ad1.durationFrom(ad0), 1.0e-15);
262         Assertions.assertEquals(1, new AbsoluteDate("2009-01-01T00:00:00", utc).durationFrom(ad0), 1.0e-15);
263         Assertions.assertEquals(2, new AbsoluteDate("2009-01-01T00:00:01", utc).durationFrom(ad0), 1.0e-15);
264     }
265 
266     @Test
267     public void testCreatingInLeapDateLocalTime50HoursWest() {
268         // yes, I know, there are no time zones 50 hours West of UTC, this is a stress test
269         AbsoluteDate previous = null;
270         final double step = 0.0625;
271         for (double seconds = 59.0; seconds < 61.0; seconds += step) {
272             final AbsoluteDate date = new AbsoluteDate(new DateComponents(2008, 12, 29),
273                                                        new TimeComponents(21, 59, seconds, -50 * 60),
274                                                        utc);
275             if (previous != null) {
276                 Assertions.assertEquals(step, date.durationFrom(previous), 1.0e-12);
277             }
278             previous = date;
279         }
280         AbsoluteDate ad0 = new AbsoluteDate("2008-12-29T21:59:60-50:00", utc);
281         Assertions.assertTrue(ad0.toString(utc).startsWith("2008-12-31T23:59:"));
282         AbsoluteDate ad1 = new AbsoluteDate("2008-12-29T21:59:59-50:00", utc).shiftedBy(1);
283         Assertions.assertEquals(0, ad1.durationFrom(ad0), 1.0e-15);
284         Assertions.assertEquals(1, new AbsoluteDate("2008-12-29T22:00:00-50:00", utc).durationFrom(ad0), 1.0e-15);
285         Assertions.assertEquals(2, new AbsoluteDate("2008-12-29T22:00:01-50:00", utc).durationFrom(ad0), 1.0e-15);
286     }
287 
288     @Test
289     public void testCreatingInLeapDateLocalTime50HoursEast() {
290         // yes, I know, there are no time zones 50 hours East of UTC, this is a stress test
291         AbsoluteDate previous = null;
292         final double step = 0.0625;
293         for (double seconds = 59.0; seconds < 61.0; seconds += step) {
294             final AbsoluteDate date = new AbsoluteDate(new DateComponents(2009, 1, 3),
295                                                        new TimeComponents(1, 59, seconds, +50 * 60),
296                                                        utc);
297             if (previous != null) {
298                 Assertions.assertEquals(step, date.durationFrom(previous), 1.0e-12);
299             }
300             previous = date;
301         }
302         AbsoluteDate ad0 = new AbsoluteDate("2009-01-03T01:59:60+50:00", utc);
303         Assertions.assertTrue(ad0.toString(utc).startsWith("2008-12-31T23:59:"));
304         AbsoluteDate ad1 = new AbsoluteDate("2009-01-03T01:59:59+50:00", utc).shiftedBy(1);
305         Assertions.assertEquals(0, ad1.durationFrom(ad0), 1.0e-15);
306         Assertions.assertEquals(1, new AbsoluteDate("2009-01-03T02:00:00+50:00", utc).durationFrom(ad0), 1.0e-15);
307         Assertions.assertEquals(2, new AbsoluteDate("2009-01-03T02:00:01+50:00", utc).durationFrom(ad0), 1.0e-15);
308     }
309 
310     @Test
311     public void testDisplayDuringLeap() {
312         AbsoluteDate t0 = utc.getLastKnownLeapSecond().shiftedBy(-1.0);
313         for (double dt = 0.0; dt < 3.0; dt += 0.375) {
314             AbsoluteDate t = t0.shiftedBy(dt);
315             double seconds = t.getComponents(utc).getTime().getSecond();
316             if (dt < 2.0) {
317                 Assertions.assertEquals(dt + 59.0, seconds, 1.0e-12);
318             } else {
319                 Assertions.assertEquals(dt - 2.0, seconds, 1.0e-12);
320             }
321         }
322     }
323 
324     @Test
325     public void testMultithreading() {
326 
327         // generate reference offsets using a single thread
328         RandomGenerator random = new Well1024a(6392073424L);
329         List<AbsoluteDate> datesList = new ArrayList<>();
330         List<Double> offsetsList = new ArrayList<>();
331         AbsoluteDate reference = utc.getFirstKnownLeapSecond().shiftedBy(-Constants.JULIAN_YEAR);
332         double testRange = utc.getLastKnownLeapSecond().durationFrom(reference) + Constants.JULIAN_YEAR;
333         for (int i = 0; i < 10000; ++i) {
334             AbsoluteDate randomDate = reference.shiftedBy(random.nextDouble() * testRange);
335             datesList.add(randomDate);
336             offsetsList.add(utc.offsetFromTAI(randomDate).toDouble());
337         }
338 
339         // check the offsets in multi-threaded mode
340         ExecutorService executorService = Executors.newFixedThreadPool(100);
341 
342         for (int i = 0; i < datesList.size(); ++i) {
343             final AbsoluteDate date = datesList.get(i);
344             final double offset = offsetsList.get(i);
345             executorService.execute(() -> Assertions.assertEquals(offset, utc.offsetFromTAI(date).toDouble(), 1.0e-12));
346         }
347 
348         try {
349             executorService.shutdown();
350             Assertions.assertTrue(executorService.awaitTermination(3, TimeUnit.SECONDS));
351         } catch (InterruptedException ie) {
352             Assertions.fail(ie.getLocalizedMessage());
353         }
354 
355     }
356 
357     @Test
358     public void testIssue89() {
359         AbsoluteDate firstDayLastLeap = utc.getLastKnownLeapSecond().shiftedBy(10.0);
360         AbsoluteDate rebuilt = new AbsoluteDate(firstDayLastLeap.toString(utc), utc);
361         Assertions.assertEquals(0.0, rebuilt.durationFrom(firstDayLastLeap), 1.0e-12);
362     }
363 
364     @Test
365     public void testOffsetToTAIBeforeFirstLeapSecond() {
366         TimeScale scale = TimeScalesFactory.getUTC();
367         // time before first leap second
368         DateComponents dateComponents = new DateComponents(1950, 1, 1);
369         double actual = scale.offsetToTAI(dateComponents, TimeComponents.H00).toDouble();
370         Assertions.assertEquals(0.0, actual, 1.0e-10);
371     }
372 
373     @Test
374     public void testEmptyOffsets() {
375         Utils.setDataRoot("no-data");
376 
377         TimeScalesFactory.addUTCTAIOffsetsLoader(Collections::emptyList);
378 
379         try {
380             TimeScalesFactory.getUTC();
381             Assertions.fail("an exception should have been thrown");
382         } catch (OrekitException oe) {
383             Assertions.assertEquals(OrekitMessages.NO_IERS_UTC_TAI_HISTORY_DATA_LOADED, oe.getSpecifier());
384         }
385 
386     }
387 
388     @Test
389     public void testInfinityRegularDate() {
390         TimeScale scale = TimeScalesFactory.getUTC();
391         Assertions.assertEquals(-37.0,
392                                 scale.offsetFromTAI(AbsoluteDate.FUTURE_INFINITY).toDouble(),
393                                 1.0e-15);
394         Assertions.assertEquals(0.0,
395                                 scale.offsetFromTAI(AbsoluteDate.PAST_INFINITY).toDouble(),
396                                 1.0e-15);
397     }
398 
399     @Test
400     public void testInfinityFieldDate() {
401         TimeScale scale = TimeScalesFactory.getUTC();
402         Assertions.assertEquals(-37.0,
403                             scale.offsetFromTAI(FieldAbsoluteDate.getFutureInfinity(Binary64Field.getInstance())).getReal(),
404                             1.0e-15);
405         Assertions.assertEquals(0.0,
406                             scale.offsetFromTAI(FieldAbsoluteDate.getPastInfinity(Binary64Field.getInstance())).getReal(),
407                             1.0e-15);
408     }
409 
410     @Test
411     public void testFirstAndLast() {
412         // action
413         AbsoluteDate first = utc.getFirstKnownLeapSecond();
414         AbsoluteDate last = utc.getLastKnownLeapSecond();
415 
416         // verify
417         //AbsoluteDate d = new AbsoluteDate(1961, 1, 1, utc);
418         Assertions.assertEquals(new AbsoluteDate(2016, 12, 31, 23, 59, 60, utc), last);
419         Assertions.assertEquals(new AbsoluteDate(1960, 12, 31, 23, 59, 60, utc), first);
420     }
421 
422     @Test
423     public void testGetUTCTAIOffsets() {
424         final List<UTCTAIOffset> offsets = utc.getUTCTAIOffsets();
425         Assertions.assertEquals(41, offsets.size());
426         final UTCTAIOffset firstOffset = offsets.get(0);
427         final UTCTAIOffset lastOffset = offsets.get(offsets.size() - 1);
428         Assertions.assertEquals(37300, firstOffset.getMJD()); // 1961-01-01
429         Assertions.assertEquals(57754, lastOffset.getMJD()); // 2017-01-01
430     }
431 
432     @BeforeEach
433     public void setUp() {
434         Utils.setDataRoot("regular-data");
435         utc = TimeScalesFactory.getUTC();
436     }
437 
438     @AfterEach
439     public void tearDown() {
440         utc = null;
441     }
442 
443     private UTCScale utc;
444 
445 }