UTCScale.java

  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. import java.util.ArrayList;
  19. import java.util.Arrays;
  20. import java.util.Collection;
  21. import java.util.Comparator;
  22. import java.util.List;

  23. import org.hipparchus.CalculusFieldElement;
  24. import org.hipparchus.util.FastMath;

  25. /** Coordinated Universal Time.
  26.  * <p>UTC is related to TAI using step adjustments from time to time
  27.  * according to IERS (International Earth Rotation Service) rules. Before 1972,
  28.  * these adjustments were piecewise linear offsets. Since 1972, these adjustments
  29.  * are piecewise constant offsets, which require introduction of leap seconds.</p>
  30.  * <p>Leap seconds are always inserted as additional seconds at the last minute
  31.  * of the day, pushing the next day forward. Such minutes are therefore more
  32.  * than 60 seconds long. In theory, there may be seconds removal instead of seconds
  33.  * insertion, but up to now (2010) it has never been used. As an example, when a
  34.  * one second leap was introduced at the end of 2005, the UTC time sequence was
  35.  * 2005-12-31T23:59:59 UTC, followed by 2005-12-31T23:59:60 UTC, followed by
  36.  * 2006-01-01T00:00:00 UTC.</p>
  37.  * <p>This is intended to be accessed thanks to {@link TimeScales},
  38.  * so there is no public constructor.</p>
  39.  * @author Luc Maisonobe
  40.  * @see AbsoluteDate
  41.  */
  42. public class UTCScale implements TimeScale {

  43.     /** Number of seconds in one day. */
  44.     private static final long SEC_PER_DAY = 86400L;

  45.     /** Number of attoseconds in one second. */
  46.     private static final long ATTOS_PER_NANO = 1000000000L;

  47.     /** Slope conversion factor from seconds per day to nanoseconds per second. */
  48.     private static final long SLOPE_FACTOR = SEC_PER_DAY * ATTOS_PER_NANO;

  49.     /** International Atomic Scale. */
  50.     private final TimeScale tai;

  51.     /** base UTC-TAI offsets (may lack the pre-1975 offsets). */
  52.     private final Collection<? extends OffsetModel> baseOffsets;

  53.     /** UTC-TAI offsets. */
  54.     private final UTCTAIOffset[] offsets;

  55.     /** Package private constructor for the factory.
  56.      * Used to create the prototype instance of this class that is used to
  57.      * clone all subsequent instances of {@link UTCScale}. Initializes the offset
  58.      * table that is shared among all instances.
  59.      * @param tai TAI time scale this UTC time scale references.
  60.      * @param baseOffsets UTC-TAI base offsets (may lack the pre-1975 offsets)
  61.      */
  62.     UTCScale(final TimeScale tai, final Collection<? extends OffsetModel> baseOffsets) {

  63.         this.tai         = tai;
  64.         this.baseOffsets = baseOffsets;

  65.         // copy input so the original list is unmodified
  66.         final List<OffsetModel> offsetModels = new ArrayList<>(baseOffsets);
  67.         offsetModels.sort(Comparator.comparing(OffsetModel::getStart));
  68.         if (offsetModels.get(0).getStart().getYear() > 1968) {
  69.             // the pre-1972 linear offsets are missing, add them manually
  70.             // excerpt from UTC-TAI.history file:
  71.             //  1961  Jan.  1 - 1961  Aug.  1     1.422 818 0s + (MJD - 37 300) x 0.001 296s
  72.             //        Aug.  1 - 1962  Jan.  1     1.372 818 0s +        ""
  73.             //  1962  Jan.  1 - 1963  Nov.  1     1.845 858 0s + (MJD - 37 665) x 0.001 123 2s
  74.             //  1963  Nov.  1 - 1964  Jan.  1     1.945 858 0s +        ""
  75.             //  1964  Jan.  1 -       April 1     3.240 130 0s + (MJD - 38 761) x 0.001 296s
  76.             //        April 1 -       Sept. 1     3.340 130 0s +        ""
  77.             //        Sept. 1 - 1965  Jan.  1     3.440 130 0s +        ""
  78.             //  1965  Jan.  1 -       March 1     3.540 130 0s +        ""
  79.             //        March 1 -       Jul.  1     3.640 130 0s +        ""
  80.             //        Jul.  1 -       Sept. 1     3.740 130 0s +        ""
  81.             //        Sept. 1 - 1966  Jan.  1     3.840 130 0s +        ""
  82.             //  1966  Jan.  1 - 1968  Feb.  1     4.313 170 0s + (MJD - 39 126) x 0.002 592s
  83.             //  1968  Feb.  1 - 1972  Jan.  1     4.213 170 0s +        ""
  84.             // the slopes in second per day correspond in fact to values in scaled nanoseconds per seconds:
  85.             //  0.0012960 s/d → 15 ns/s
  86.             //  0.0011232 s/d → 13 ns/s
  87.             //  0.0025920 s/d → 30 ns/s
  88.             // CHECKSTYLE: stop MultipleStringLiterals check
  89.             offsetModels.add( 0, linearModel(1961,  1, 1, 37300, "1.4228180", "0.001296"));
  90.             offsetModels.add( 1, linearModel(1961,  8, 1, 37300, "1.3728180", "0.001296"));
  91.             offsetModels.add( 2, linearModel(1962,  1, 1, 37665, "1.8458580", "0.0011232"));
  92.             offsetModels.add( 3, linearModel(1963, 11, 1, 37665, "1.9458580", "0.0011232"));
  93.             offsetModels.add( 4, linearModel(1964,  1, 1, 38761, "3.2401300", "0.001296"));
  94.             offsetModels.add( 5, linearModel(1964,  4, 1, 38761, "3.3401300", "0.001296"));
  95.             offsetModels.add( 6, linearModel(1964,  9, 1, 38761, "3.4401300", "0.001296"));
  96.             offsetModels.add( 7, linearModel(1965,  1, 1, 38761, "3.5401300", "0.001296"));
  97.             offsetModels.add( 8, linearModel(1965,  3, 1, 38761, "3.6401300", "0.001296"));
  98.             offsetModels.add( 9, linearModel(1965,  7, 1, 38761, "3.7401300", "0.001296"));
  99.             offsetModels.add(10, linearModel(1965,  9, 1, 38761, "3.8401300", "0.001296"));
  100.             offsetModels.add(11, linearModel(1966,  1, 1, 39126, "4.3131700", "0.002592"));
  101.             offsetModels.add(12, linearModel(1968,  2, 1, 39126, "4.2131700", "0.002592"));
  102.             // CHECKSTYLE: resume MultipleStringLiterals check
  103.         }

  104.         // create cache
  105.         this.offsets = new UTCTAIOffset[offsetModels.size()];

  106.         UTCTAIOffset previous = null;

  107.         // link the offsets together
  108.         for (int i = 0; i < offsetModels.size(); ++i) {

  109.             final OffsetModel    o      = offsetModels.get(i);
  110.             final DateComponents date   = o.getStart();
  111.             final int            mjdRef = o.getMJDRef();
  112.             final TimeOffset offset = o.getOffset();
  113.             final int            slope  = o.getSlope();

  114.             // start of the leap
  115.             final TimeOffset previousOffset = (previous == null) ?
  116.                                               TimeOffset.ZERO :
  117.                                               previous.getOffset(date, TimeComponents.H00);
  118.             final AbsoluteDate leapStart   = new AbsoluteDate(date, tai).shiftedBy(previousOffset);

  119.             // end of the leap
  120.             final long         dt          = (date.getMJD() - mjdRef) * SEC_PER_DAY;
  121.             final TimeOffset drift       = TimeOffset.NANOSECOND.multiply(slope * FastMath.abs(dt));
  122.             final TimeOffset startOffset = dt < 0 ? offset.subtract(drift) : offset.add(drift);
  123.             final AbsoluteDate leapEnd     = new AbsoluteDate(date, tai).shiftedBy(startOffset);

  124.             // leap computed at leap start and in UTC scale
  125.             final TimeOffset leap           = leapEnd.accurateDurationFrom(leapStart).
  126.                                              multiply(1000000000).
  127.                                              divide(1000000000 + slope);

  128.             final AbsoluteDate reference = AbsoluteDate.createMJDDate(mjdRef, 0, tai).shiftedBy(offset);
  129.             previous = new UTCTAIOffset(leapStart, date.getMJD(), leap, offset, mjdRef, slope, reference);
  130.             this.offsets[i] = previous;

  131.         }

  132.     }

  133.     /** Get the base offsets.
  134.      * @return base offsets (may lack the pre-1975 offsets)
  135.      * @since 12.0
  136.      */
  137.     public Collection<? extends OffsetModel> getBaseOffsets() {
  138.         return baseOffsets;
  139.     }

  140.     /**
  141.      * Returns the UTC-TAI offsets underlying this UTC scale.
  142.      * <p>
  143.      * Modifications to the returned list will not affect this UTC scale instance.
  144.      * @return new non-null modifiable list of UTC-TAI offsets time-sorted from
  145.      *         earliest to latest
  146.      */
  147.     public List<UTCTAIOffset> getUTCTAIOffsets() {
  148.         return Arrays.asList(offsets);
  149.     }

  150.     /** {@inheritDoc} */
  151.     @Override
  152.     public TimeOffset offsetFromTAI(final AbsoluteDate date) {
  153.         final int offsetIndex = findOffsetIndex(date);
  154.         if (offsetIndex < 0) {
  155.             // the date is before the first known leap
  156.             return TimeOffset.ZERO;
  157.         } else {
  158.             return offsets[offsetIndex].getOffset(date).negate();
  159.         }
  160.     }

  161.     /** {@inheritDoc} */
  162.     @Override
  163.     public <T extends CalculusFieldElement<T>> T offsetFromTAI(final FieldAbsoluteDate<T> date) {
  164.         final int offsetIndex = findOffsetIndex(date.toAbsoluteDate());
  165.         if (offsetIndex < 0) {
  166.             // the date is before the first known leap
  167.             return date.getField().getZero();
  168.         } else {
  169.             return offsets[offsetIndex].getOffset(date).negate();
  170.         }
  171.     }

  172.     /** {@inheritDoc} */
  173.     @Override
  174.     public TimeOffset offsetToTAI(final DateComponents date,
  175.                                   final TimeComponents time) {

  176.         // take offset from local time into account, but ignoring seconds,
  177.         // so when we parse an hour like 23:59:60.5 during leap seconds introduction,
  178.         // we do not jump to next day
  179.         final int minuteInDay = time.getHour() * 60 + time.getMinute() - time.getMinutesFromUTC();
  180.         final int correction  = minuteInDay < 0 ? (minuteInDay - 1439) / 1440 : minuteInDay / 1440;

  181.         // find close neighbors, assuming date in TAI, i.e a date earlier than real UTC date
  182.         final int mjd = date.getMJD() + correction;
  183.         final UTCTAIOffset offset = findOffset(mjd);
  184.         if (offset == null) {
  185.             // the date is before the first known leap
  186.             return TimeOffset.ZERO;
  187.         } else {
  188.             return offset.getOffset(date, time);
  189.         }

  190.     }

  191.     /** {@inheritDoc} */
  192.     public String getName() {
  193.         return "UTC";
  194.     }

  195.     /** {@inheritDoc} */
  196.     public String toString() {
  197.         return getName();
  198.     }

  199.     /** Get the date of the first known leap second.
  200.      * @return date of the first known leap second
  201.      */
  202.     public AbsoluteDate getFirstKnownLeapSecond() {
  203.         return offsets[0].getDate();
  204.     }

  205.     /** Get the date of the last known leap second.
  206.      * @return date of the last known leap second
  207.      */
  208.     public AbsoluteDate getLastKnownLeapSecond() {
  209.         return offsets[offsets.length - 1].getDate();
  210.     }

  211.     /** {@inheritDoc} */
  212.     @Override
  213.     public boolean insideLeap(final AbsoluteDate date) {
  214.         final int offsetIndex = findOffsetIndex(date);
  215.         if (offsetIndex < 0) {
  216.             // the date is before the first known leap
  217.             return false;
  218.         } else {
  219.             return date.compareTo(offsets[offsetIndex].getValidityStart()) < 0;
  220.         }
  221.     }

  222.     /** {@inheritDoc} */
  223.     @Override
  224.     public <T extends CalculusFieldElement<T>> boolean insideLeap(final FieldAbsoluteDate<T> date) {
  225.         return insideLeap(date.toAbsoluteDate());
  226.     }

  227.     /** {@inheritDoc} */
  228.     @Override
  229.     public int minuteDuration(final AbsoluteDate date) {
  230.         final int offsetIndex = findOffsetIndex(date);
  231.         final UTCTAIOffset offset;
  232.         if (offsetIndex >= 0 &&
  233.                 date.compareTo(offsets[offsetIndex].getValidityStart()) < 0) {
  234.             // the date is during the leap itself
  235.             offset = offsets[offsetIndex];
  236.         } else if (offsetIndex + 1 < offsets.length &&
  237.             offsets[offsetIndex + 1].getDate().durationFrom(date) <= 60.0) {
  238.             // the date is after a leap, but it may be just before the next one
  239.             // the next leap will start in one minute, it will extend the current minute
  240.             offset = offsets[offsetIndex + 1];
  241.         } else {
  242.             offset = null;
  243.         }
  244.         if (offset != null) {
  245.             // since this method returns an int we can't return the precise duration in
  246.             // all cases, but we can bound it. Some leaps are more than 1s. See #694
  247.             return 60 + (int) (offset.getLeap().getSeconds() +
  248.                                FastMath.min(1, offset.getLeap().getAttoSeconds()));
  249.         }
  250.         // no leap is expected within the next minute
  251.         return 60;
  252.     }

  253.     /** {@inheritDoc} */
  254.     @Override
  255.     public <T extends CalculusFieldElement<T>> int minuteDuration(final FieldAbsoluteDate<T> date) {
  256.         return minuteDuration(date.toAbsoluteDate());
  257.     }

  258.     /** {@inheritDoc} */
  259.     @Override
  260.     public TimeOffset getLeap(final AbsoluteDate date) {
  261.         final int offsetIndex = findOffsetIndex(date);
  262.         if (offsetIndex < 0) {
  263.             // the date is before the first known leap
  264.             return TimeOffset.ZERO;
  265.         } else {
  266.             return offsets[offsetIndex].getLeap();
  267.         }
  268.     }

  269.     /** {@inheritDoc} */
  270.     @Override
  271.     public <T extends CalculusFieldElement<T>> T getLeap(final FieldAbsoluteDate<T> date) {
  272.         return date.getField().getZero().newInstance(getLeap(date.toAbsoluteDate()).toDouble());
  273.     }

  274.     /** Find the index of the offset valid at some date.
  275.      * @param date date at which offset is requested
  276.      * @return index of the offset valid at this date, or -1 if date is before first offset.
  277.      */
  278.     private int findOffsetIndex(final AbsoluteDate date) {
  279.         int inf = 0;
  280.         int sup = offsets.length;
  281.         while (sup - inf > 1) {
  282.             final int middle = (inf + sup) >>> 1;
  283.             if (date.compareTo(offsets[middle].getDate()) < 0) {
  284.                 sup = middle;
  285.             } else {
  286.                 inf = middle;
  287.             }
  288.         }
  289.         if (sup == offsets.length) {
  290.             // the date is after the last known leap second
  291.             return offsets.length - 1;
  292.         } else if (date.compareTo(offsets[inf].getDate()) < 0) {
  293.             // the date is before the first known leap
  294.             return -1;
  295.         } else {
  296.             return inf;
  297.         }
  298.     }

  299.     /** Find the offset valid at some date.
  300.      * @param mjd Modified Julian Day of the date at which offset is requested
  301.      * @return offset valid at this date, or null if date is before first offset.
  302.      */
  303.     private UTCTAIOffset findOffset(final int mjd) {
  304.         int inf = 0;
  305.         int sup = offsets.length;
  306.         while (sup - inf > 1) {
  307.             final int middle = (inf + sup) >>> 1;
  308.             if (mjd < offsets[middle].getMJD()) {
  309.                 sup = middle;
  310.             } else {
  311.                 inf = middle;
  312.             }
  313.         }
  314.         if (sup == offsets.length) {
  315.             // the date is after the last known leap second
  316.             return offsets[offsets.length - 1];
  317.         } else if (mjd < offsets[inf].getMJD()) {
  318.             // the date is before the first known leap
  319.             return null;
  320.         } else {
  321.             return offsets[inf];
  322.         }
  323.     }

  324.     /** Create a linear model.
  325.      * @param year year
  326.      * @param month month
  327.      * @param day day
  328.      * @param mjdRef reference date for the linear model
  329.      * @param offset offset
  330.      * @param slope slope
  331.      * @return linear model
  332.      */
  333.     private OffsetModel linearModel(final int year, final int month, final int day,
  334.                                     final int mjdRef, final String offset, final String slope) {
  335.         return new OffsetModel(new DateComponents(year, month, day),
  336.                                mjdRef,
  337.                                TimeOffset.parse(offset),
  338.                                (int) (TimeOffset.parse(slope).getAttoSeconds()  / SLOPE_FACTOR));
  339.     }

  340. }