std/time/utc▲toc

UTC time scales and leap second support.

1. UTC time calculation

The world's standard time scale is Universal Coordinated Time (UTC). UTC is directly based on International Atomic Time (TAI) (ts-taistd/time/instant/ts-tai: timescale) and uses standard SI seconds. The UTC time scale differs from TAI in that UTC stays within 1 second of UT1; the time scale based on the rotation of the Earth. To keep UTC close to UT1 about every 1.5 year a leap second is added to the day (appearing as a 60th second in the last minute of the day, e.g. 23:59:60h).

In order to calculate SI second durations between time instants, we need to know when a leap second is inserted. For example, there was a leap second inserted on 2017-01-01Z, so:

time(2017,1,1,0,0,0) - time(2016,12,31,23,0,0)
time(2017,1,1,0,0,0) - time(2016,12,31,23,0,0)

equals 3601 SI seconds to account for the extra leap second. We also need to know the inserted leap seconds to convert UTC time to TAI which forms the basis of many other time scales (see std/time/astro).

The occurrence of leap seconds cannot be reliably predicted though and the IERS usually announces leap seconds about half a year in advance. The IETF leap second table contains a list of all currently announced leap seconds. As such, any future duration calculation in UTC is essentially non-deterministic since it may be off by a number of (as yet unannounced) leap seconds. This means in practice:

1.1. Effect types and future leap seconds

Since UTC time calculations depend on a leap second table (leaps-tablestd/time/utc/leaps-table: V), the UTC time scale can only be created from such table (ts-utc-createstd/time/utc/ts-utc-create: (leaps : leaps-table) -> timescale). For most precise UTC time, you can use ts-utc-load to automatically download and cache the latest IERS leap second information. This can be somewhat cumbersome though as this is a function with an iostd/core/io: E effect.

International Time (TI) (or Temps International) is a proposed time scale that matches exactly UTC up to some date but with no further leap seconds added after that – which makes time calculations robust and deterministic with regard to future dates. For this reason, this library defaults to using TI instead of UTC. The TI time scale (ts-tistd/time/utc/ts-ti: timescale) is defined to exactly match UTC before the compiler release date (currently 2019) but ignores any future leap seconds.

1.2. UTC between 1961 and 1972

UTC only got integral leap seconds after 1972-01-01Z. Before 1972, the UTC time was broadcast using a combination of fixed time steps (i.e. mini leap seconds) and an offset from the atomic clock frequency. By adjusting the frequency, UTC time was effectively running at a linearly adjusted rate relative to TAI. We call this adjustment drift. For example, between 1963-11-01Z and 1964-01-01Z the frequency offset was -130×10-10 which means UTC was running ahead of TAI by 0.0011232 seconds per day. The drift was then defined as:

 (TAI-UTC) = 1.9458580 + 0.0011232×Δ(1962-01-01)

where the function Δ(date) returns the number of days since date. Therefore, the difference started as 2.6972788s (as there were 669 days between 1962-01-01 and 1963-11-01) and increased linearly up to 2.7657940s on 1964-01-01Z. For dates between 1961-01-01Z and 1972-01-01Z the library calculates the UTC time based on the historical USNO drift data (see Table 1).

Here are some interesting time steps that occurred in this time frame:

UTC was only established in 1960 (Arias and Guinot [1]), and the TAI instant 1961-01-01 00:00:01.422818Z TAI was set to be UTC instant 1961-01-01Z exactly.

1.3. UTC before 1961

The TAI timescale was established with a 1958-01-01Z epoch, where the difference (UT2 - TAI) was set to be approximately zero1. In the time frame 1958 up to 1961 the relation between TAI and UTC is not formally defined.

We can however use the time steps and frequency adjustments as broadcast by the NIST WWV radio station (see Explanatory Supplement to the Astronomical Almanac [2, pages 86–87]). Historically there were 18 20ms time steps from 1958. The frequency offset in the year 1958 is not clear, and is described in the Supplement as “.. an offset of about -100×10-10 during 1958”. We fixed the offset at -85×10-10 in order to have a zero difference from TAI at exactly 1958-01-01Z.

In the library, this makes UTC coincide with TAI up to 1958-01-01, interpolated with ‘drift’ up to 1972-01-01, and using integral leap-seconds after that.

Interestingly, in the time before 1961, most leap steps occurred at 19:00h instead of at the last second of the day, for example, on 1959-01-28 there was a 0.02s leap step:

instant(1959,1,28,19,0,0,0.50,cal=cal-tai).time.show == "1959-01-28T18:59:60.007866Z"
instant(1959,1,28,19,0,0,0.52,cal=cal-tai).time.show == "1959-01-28T19:00:00.007866Z"
instant(1959,1,28,19,0,0,0.50,cal=cal-tai).time.show == "1959-01-28T18:59:60.007866Z"
instant(1959,1,28,19,0,0,0.52,cal=cal-tai).time.show == "1959-01-28T19:00:00.007866Z"

showing the seconds at 60 just before 19:00h.

1.4. All historical leap second adjustments

Table 1 shows all leap second time steps from 1958 up to 2017. This table is calculated and uses historical data for time steps before 1972.

Date (UTC)   Time steps     (TAI–UTC) (TAI–UTC) Drift   Frequency
Just before   On the date  Offset
1958-01-01 +0 0.000000 0.000000 0.000000s + 0.00073458×Δ(1958-01-01) -85×10-10
1958-01-15, 19:00 +0.020 0.010866 0.030866 0.020000s + 0.00073458×Δ(  "  ) -85×10-10
1958-02-05, 19:00 +0.020 0.046292 0.066292 0.040000s + 0.00073458×Δ(  "  ) -85×10-10
1958-02-19, 19:00 +0.020 0.076576 0.096576 0.060000s + 0.00073458×Δ(  "  ) -85×10-10
1958-04-09, 19:00 +0.020 0.132570 0.152570 0.080000s + 0.00073458×Δ(  "  ) -85×10-10
1958-06-11, 19:00 +0.020 0.198849 0.218849 0.100000s + 0.00073458×Δ(  "  ) -85×10-10
1958-07-02, 19:00 +0.020 0.234275 0.254275 0.120000s + 0.00073458×Δ(  "  ) -85×10-10
1958-07-16, 19:00 +0.020 0.264559 0.284559 0.140000s + 0.00073458×Δ(  "  ) -85×10-10
1958-10-22, 19:00 +0.020 0.356548 0.376548 0.160000s + 0.00073458×Δ(  "  ) -85×10-10
1958-11-26, 19:00 +0.020 0.402258 0.422258 0.180000s + 0.00073458×Δ(  "  ) -85×10-10
1958-12-24, 19:00 +0.020 0.442827 0.462827 0.200000s + 0.00073458×Δ(  "  ) -85×10-10
1959-01-01 +0 0.468122 0.468122 0.468122s + 0.0008640×Δ(1959-01-01) -100×10-10
1959-01-28, 19:00 +0.020 0.492134 0.512134 0.488122s + 0.0008640×Δ(  "  ) -100×10-10
1959-02-25, 19:00 +0.020 0.536326 0.556326 0.508122s + 0.0008640×Δ(  "  ) -100×10-10
1959-04-05, 19:00 +0.020 0.590022 0.610022 0.528122s + 0.0008640×Δ(  "  ) -100×10-10
1959-08-26, 19:00 +0.020 0.733574 0.753574 0.548122s + 0.0008640×Δ(  "  ) -100×10-10
1959-09-30, 19:00 +0.020 0.783814 0.803814 0.568122s + 0.0008640×Δ(  "  ) -100×10-10
1959-11-04, 19:00 +0.020 0.834054 0.854054 0.588122s + 0.0008640×Δ(  "  ) -100×10-10
1959-11-18, 19:00 +0.020 0.866150 0.886150 0.608122s + 0.0008640×Δ(  "  ) -100×10-10
1959-12-16, 19:00 +0.020 0.910342 0.930342 0.628122s + 0.0008640×Δ(  "  ) -100×10-10
 
1960-01-01 +0 0.943482 0.943482 0.943482s + 0.0012960×Δ(1960-01-01) -150×10-10
1961-01-01 +0.005 1.417818 1.422818 1.422818s + 0.0012960×Δ(1961-01-01) -150×10-10
1961-08-01 -0.050 1.697570 1.647570 1.372818s + 0.0012960×Δ(  "  ) -150×10-10
1962-01-01 +0 1.845858 1.845858 1.845858s + 0.0011232×Δ(1962-01-01) -130×10-10
1963-11-01 +0.100 2.597279 2.697279 1.945858s + 0.0011232×Δ(  "  ) -130×10-10
1964-01-01 +0 2.765794 2.765794 3.240130s + 0.0012960×Δ(1965-01-01) -150×10-10
1964-04-01 +0.100 2.883730 2.983730 3.340130s + 0.0012960×Δ(  "  ) -150×10-10
1964-09-01 +0.100 3.182018 3.282018 3.440130s + 0.0012960×Δ(  "  ) -150×10-10
1965-01-01 +0.100 3.440130 3.540130 3.540130s + 0.0012960×Δ(  "  ) -150×10-10
1965-03-01 +0.100 3.616594 3.716594 3.640130s + 0.0012960×Δ(  "  ) -150×10-10
1965-07-01 +0.100 3.874706 3.974706 3.740130s + 0.0012960×Δ(  "  ) -150×10-10
1965-09-01 +0.100 4.055058 4.155058 3.840130s + 0.0012960×Δ(  "  ) -150×10-10
1966-01-01 +0 4.313170 4.313170 4.313170s + 0.0025920×Δ(1966-01-01) -300×10-10
1968-02-01 -0.100 6.285682 6.185682 4.213170s + 0.0025920×Δ(  "  ) -300×10-10
 
1972-01-01 +0.107758 9.892242 10
1972-07-01 +1 10 11
1973-01-01 +1 11 12
1974-01-01 +1 12 13
1975-01-01 +1 13 14
1976-01-01 +1 14 15
1977-01-01 +1 15 16
1978-01-01 +1 16 17
1979-01-01 +1 17 18
1980-01-01 +1 18 19
1981-07-01 +1 19 20
1982-07-01 +1 20 21
1983-07-01 +1 21 22
1985-07-01 +1 22 23
1988-01-01 +1 23 24
1990-01-01 +1 24 25
1991-01-01 +1 25 26
1992-07-01 +1 26 27
1993-07-01 +1 27 28
1994-07-01 +1 28 29
1996-01-01 +1 29 30
1997-07-01 +1 30 31
1999-01-01 +1 31 32
2006-01-01 +1 32 33
2009-01-01 +1 33 34
2012-07-01 +1 34 35
2015-07-01 +1 35 36
2017-01-01 +1 36 37

Table 1. Leap second time steps since 1958. The function Δ(date) returns the number of days since date. Entries after 1972 are based on IETF data. The entries before 1972 are derived from the Explanatory Supplement to the Astronomical Almanac, by P. Kenneth Seidelmann [2, pages 86–87]. Entries before 1961 are based on the time steps broadcast by the NIST WWV radio station.

– Daan Leijen, 2016.

References

[1]E.F. Arias and B. Guinot. Coordinated universal time UTC: historical background and perspectives. Journées Systèmes de Référence Spatio-Temporels. 2004. pdf🔎
[2]P.K. Seidelmann. Explanatory Supplement to the Astronomical Almanac. University Science Books, 1992. book 🔎
[3]B. Guinot. History of the Bureau International de l'Heure. In “Polar Motion: Historical and Scientific Problems”, ASP Conference Series, Vol. 208, Edited by Steven Dick, Dennis McCarthy, and Brian Luzum. ISBN: 1-58381-039-0, 2000., p.175. pdf 🔎
[4]Jay Lieske. Precession Matrix Based on IAU (1976). System of Astronomical Constants, Astronomy & Astrophysics, Vol. 73, pages 282–284. 1979. wikipedia 🔎

.

type leap-adjuststd/time/utc/leap-adjust: V

Leap second adjustments. For an instant i after start:
TAI-offset = offset + (drift * days(i - drift-start)).

type leaps-tablestd/time/utc/leaps-table: V

A leap second table describes when UTC leap seconds occur.

Leap second table upto (but not including) 1972-01-01 UTC.

Default TI leaps table has leap second information up to the compiler release (currently leaps-table-y2017std/time/utc/leaps-table-y2017: leaps-table).

Leap second table up to 2017-01-01Z.

Parse the standard UTC leap second adjustment file in the “old” .dat format as in http://​maia.​usno.​navy.​mil/​ser7/​tai-​utc.​dat, where entries have the shape

   1961 JAN  1 =JD 2437300.5  TAI-UTC=   1.4228180 S + (MJD - 37300.) X 0.001296 S

which specifies the start time (jdstd/time/instant/jd: (i : instant, ts : timescale) -> ddouble 2437300.5), new TAI-UTC offset (1.4228180s), and the drift, starting at 37300 MJD of 0.001296s per day. Lines that start with # are comments. As an extension you can have an expiration date on a line that starts with #@ followed by seconds since the NTP epoch (1900-01-01). Just as in a standard IETF leap second file.

 NTP time scale is equal to the UTC time scale (ts-utc).

 Unix time scale is equal to the UTC time scale (ts-utc).

Create a new UTC-SLS time scale from a provided leap second table leaps. Implements a UTC time scale except without ever showing leap seconds. UTC-SLS is equivalent to UTC except for the last 1000 seconds of a day where a leap second occurs. On such day, the leap second time step (positive or negative) is distributed over the last 1000 seconds of the day. On the full hour, UTC and UTC-SLS are equal again.
This is a recommended time scale to use for time stamps or communication with other services since it avoids any potential trouble with leap seconds while still being quite precise. See also: https://​www.​cl.​cam.​ac.​uk/​~mgk25/​time/​utc-​sls.
You can create a UTC-SLS time scale based on the latest IETF leap second data using cal-utc-sls-load.

effect utcstd/time/utc/utc: HX

utcstd/time/utc/utc: HX effect.

alias utc-timestampstd/time/utc/utc-timestamp: V = timestampstd/time/timestamp/timestamp: V

utc-timestampstd/time/utc/utc-timestamp: V is UTC time since 2000-01-01.

fun get-leap-steps( table : ?leaps-tablestd/time/utc/leaps-table: V ) : liststd/core/list: V -> V<(utc-timestampstd/time/utc/utc-timestamp: V, timespanstd/time/timestamp/timespan: V, timespanstd/time/timestamp/timespan: V, (timespanstd/time/timestamp/timespan: V, utc-timestampstd/time/utc/utc-timestamp: V, ddoublestd/num/ddouble/ddouble: V))>

Get a list of leap second steps in a triple, NTP start time, offset just before, and the new offset at that time, the base offset, the drift start date and the drift rate.

fun ntp-instant( ntp : ddoublestd/num/ddouble/ddouble: V, leap : ?intstd/core/types/int: V ) : instantstd/time/instant/instant: V

Convert an NTP time in seconds since the NTP epoch (1900-01-01Z) to an instant. Also takes an optional leapstd/time/timestamp/leap: (t : timestamp) -> int argument if the NTP time is inside a leap second.

fun ntp-timestamp( i : instantstd/time/instant/instant: V ) : ddoublestd/num/ddouble/ddouble: V

Return the NTP time of an instant since the NTP epoch (1900-01-01) Since NTP time stamps are ambigious, times inside a leap second show as occurring in the second after the leap second.

val ts-ntp: timescalestd/time/instant/timescale: V

 NTP time scale. It equals the ts-tistd/time/utc/ts-ti: timescale time scale.

val ts-ti: timescalestd/time/instant/timescale: V

The TI (International Time) time scale with a 2000-01-01Z UTC epoch. This is the default time scale used in this library. It was a proposed time scale at the 2004 ITU-R meeting as a replacement of UTC without future leap seconds. In this library, we define TI to match exactly UTC up to the compiler release date (currently 2017) but ignore any possible future leap seconds after that date. This is the preferred time scale in this library as it guarantees deterministic time calculations for any future date, i.e. before 2017-01-01Z, TI == UTC, while after that, TI == TAI - 37s.

val ts-ti-sls: timescalestd/time/instant/timescale: V

TI time scale with smoothed leap seconds.
Implements a TI time scale (ts-tistd/time/utc/ts-ti: timescale) except without ever showing leap seconds. TI-SLS is equivalent to TI except for the last 1000 seconds of a day where a leap second occurs. On such day, the leap second time step (positive or negative) is distributed over the last 1000 seconds of the day. On the full hour, TI and TI-SLS are equal again.

val ts-unix: timescalestd/time/instant/timescale: V

 Unix time scale based on Unix seconds. It equals the ts-tistd/time/utc/ts-ti: timescale time scale.

fun unix-instant( u : doublestd/core/types/double: V, frac : ?doublestd/core/types/double: V, ts : ?timescalestd/time/instant/timescale: V ) : instantstd/time/instant/instant: V

Given a Unix time stamp in (fractional) seconds (secs) and an optional separate fraction of seconds frac (for increased precision for nanosecond timestamps), return an instantstd/time/instant/instant: V. that is secs + frac seconds after the Unix epoch (1970-01-01Z).

Unfortunately, Unix time stamps are ambigious. The seconds secs are interpreted as: val days = secs / 86400 and val dsecs = secs % 86400, where days is the number of days since 1970-01-01Z and dsecs is the SI seconds into the day. This means that one cannot represent a possible extra leap second since it will look as the first second of the next day. For example, here is how the time stamps look around the leap second of 1973-01-01Z:

> instant(1972,12,31,23,59,59).unix-timestamp
94694399

> instant(1972,12,31,23,59,60).unix-timestamp
94694400

> instant(1973,1,1).unix-timestamp
94694400

Internally, this library uses proper timestampstd/time/timestamp/timestamp: Vs that can keep track of leap seconds. To indicate a time in a leap second, you can use a fraction frac that is larger than 1.0. For example:

> unix-instant(94694399.0).time
1972-12-31T23:59:59Z

> unix-instant(94694399.0,1.0).time
1972-12-31T23:59:60Z

> unix-instant(94694400.0).time
1973-01-01T00:00:00Z

This works well for systems that support CLOCK_UTC.

fun unix-instant( u : intstd/core/types/int: V, frac : ?doublestd/core/types/double: V, ts : ?timescalestd/time/instant/timescale: V ) : instantstd/time/instant/instant: V

Create an instant from raw unix seconds since the unix epoch (1970-01-01T00:00:10 TAI) Use a fraction > 1 to indicate a time inside a leap second.

fun unix-instant( t : timespanstd/time/timestamp/timespan: V, leap : ?intstd/core/types/int: V, ts : ?timescalestd/time/instant/timescale: V ) : instantstd/time/instant/instant: V

Create an instant from raw unix seconds since the unix epoch (1970-01-01T00:00:10 TAI) and optional leap seconds to designate instants inside a leap seconds.

fun unix-timestamp( i : instantstd/time/instant/instant: V ) : ddoublestd/num/ddouble/ddouble: V

Get a standard Unix timestamp in fractional seconds since the Unix epoch (1970-01-01Z). Since Unix time stamps are ambigioous, instants inside a leap seconds show as occurring in the second after that.

fun utc-timescale( name : stringstd/core/types/string: V, leaps : leaps-tablestd/time/utc/leaps-table: V ) : timescalestd/time/instant/timescale: V

Create a new time scale based on UTC seconds with a given namestd/time/instant/name: (timescale : timescale) -> string and a leap second table.

private import std/core/typesstd/core/types, std/core/hndstd/core/hnd, std/corestd/core, std/text/parsestd/text/parse, std/num/doublestd/num/double, std/num/ddoublestd/num/ddouble, std/time/timestampstd/time/timestamp, std/time/durationstd/time/duration, std/time/instantstd/time/instant

1.The difference TAI-UT2 was supposed to be zero on 1958-01-01Z TAI but different observatories used their own versions of UT2 leading to longitude errors of a “few 0.01s” [3]. At 1958-01-01 TAI the ΔT = TT - UT1 was 31.166s ± 0.003s (see historical ΔT), which equals 1958-01-01T00:00:00.018 UT1 time. When we calculate UT2 using the current definition [2, page 85]:

UT2 = UT1 + 0.022·sin(2πτ) - 0.012·cos(2πτ) - 0.006·sin(4πτ) + 0.007·cos(4πτ)

where τ is the fraction of the Besselian year [4]:

τ = 1900.0 + (JDTT - 2415020.31352) / 365.242198781

we get 1958-01-01T00:00:00.013023922Z UT2. A difference of about 0.013s with TAI.