Time conventions and conversions
Time Conventions and Conversions
Background
The accurate timing of events is an important measure in many areas of computing.
Computer benchmarking or network timeouts may use the computer clock only to compute the difference in time between events. For other tasks, such as measuring the time of events in a way that is independent of individual computers or when logging an event as having occurred at a particular time for later evaluation, an external time system is necessary to allow software to "tag" an event in time with a timestamp that can establish its position within an event history in that time system.
There are many such timestamp standards in use, and sometimes data with one system's timestamp may need to be used with a system that contains other data using a different system's timestamp. To properly correlate such mixed data, conversion of dates and times between timestamps in different systems is required.
The current task is to create a function or functions to perform timestamp conversions between time conventions, systems established as social conventions for tracking events in time.
Time system epochs
When converting between time conventions, the origin (time 0) of each convention needs to be considered, along with the historical times of past adjustment of the system if any. The time 0 (sometimes, time 1) of a system can be referred to as its epoch.
UTC Time
The ISO 8601 standard created the time convention of Coordinated Universal Time, or UTC.
The ISO 8601 standard expresses dates and times in the format "yyyy-mm-ddTHH:MM:SS.xxx", where yyyy is 4-digit year, mm is 2-digit month, dd is 2-digit day of month, HH is 2-digit, 24-hour hour, MM is 2-digit minutes past the hour, SS is seconds past the minute, T is the letter T used to separate days from hours, and xxx is an optional decimal fraction of a second. In addition, the timestamp may have a timezone such as GMT appended to the UTC string (note however that time zones will not be covered in this task).
UTC defines Day 1 of the year 1 Common Era (AD) as 0001-1-1T00:00:00. Exceptionally for a mostly continuous variable, there is no 0 year in the conventional AD/BC, CE/BCE calendar. Instead, the previous year is defined to start at 0000:1:1T00:00:00Z, so the years going backwards in the CE/BCE system are are ...3 CE, 2 CE, 1 CE, 1 BCE, 2 BCE... whereas in UTC these are ...3, 2, 1, 0, -1...
The actual day 0, the UTC epoch start, is then 1 BCE, and the Day 0 epoch is defined as at the last day of year 1 BCE, or December 31, 1 BCE (BC), which is 0000-12-31T00:00:00 in the ISO 8601 standard for UTC time stamps.
UNIX Time
The Unix operating system time() function was the basis for the convention of Unix system time.
The Unix epoch (or Unix time or POSIX time or Unix timestamp) is the number of non-leap seconds that have elapsed since January 1, 1970 (midnight Coordinated Universal Time, or UTC, also GMT), not counting leap seconds. The time() function in Unix actually returns the number of (non-leap) milliseconds since the Unix epoch as a floating point number, which would be the Unix timestamp.
The Unix "epoch" is thus expressed in UTC 8601 terms as 1970-01-01T00:00:00.
NTP Time
The Network Time Protocol (or NTP) defines the prime epoch as beginning at UTC 1900-01-01T00:00:00. NTP timestamps are defined as the number of non-leap seconds since the NTP epoch.
Leap seconds
The moon's tidal drag on Earth makes the Earth's rotation slow very slightly over the years, at a rate that is usually less than a second a year but varies from year to year. Because of a desire to keep the time of day in synchrony with the Earth's rotation, the exact duration of a year varies because of leap seconds. On certain years there have been leap seconds added at midnight mostly between 31 December and 1 January and occasionally at midnight between June 30 and July 1. These changes have been used to keep the UTC time of day in sync with the astronomical day. The Unix, NTP, and UTC time conventions essentially ignore leap seconds in their timekeeping. On the other hand, there other time conventions, including the TAI and GPS conventions, which advance their second clock even during leap seconds. This has caused TAI time and GPS time to progressively run ahead of UTC time by a slowly increasing number of seconds, even though the second measure itself is constant between conventions.
The leap second table
A file is kept current at the NIST ftp site, filed as ftp.boulder.nist.gov/pub/time/leap-seconds.list, which contains a history of each timestamp (in NTP time) in which the leap seconds were added and ignored in the counting of seconds in NTP, Unix, and UTC conventions.
The leap-seconds.list file currently contains data including the following:
Timestamp of last update of this file in NTP seconds: 3929093563 Planned expiration date of the file in NTP timestamp seconds: 3960057600 2272060800 10 # 1 Jan 1972 2287785600 11 # 1 Jul 1972 2303683200 12 # 1 Jan 1973 2335219200 13 # 1 Jan 1974 2366755200 14 # 1 Jan 1975 2398291200 15 # 1 Jan 1976 2429913600 16 # 1 Jan 1977 2461449600 17 # 1 Jan 1978 2492985600 18 # 1 Jan 1979 2524521600 19 # 1 Jan 1980 2571782400 20 # 1 Jul 1981 2603318400 21 # 1 Jul 1982 2634854400 22 # 1 Jul 1983 2698012800 23 # 1 Jul 1985 2776982400 24 # 1 Jan 1988 2840140800 25 # 1 Jan 1990 2871676800 26 # 1 Jan 1991 2918937600 27 # 1 Jul 1992 2950473600 28 # 1 Jul 1993 2982009600 29 # 1 Jul 1994 3029443200 30 # 1 Jan 1996 3076704000 31 # 1 Jul 1997 3124137600 32 # 1 Jan 1999 3345062400 33 # 1 Jan 2006 3439756800 34 # 1 Jan 2009 3550089600 35 # 1 Jul 2012 3644697600 36 # 1 Jul 2015 3692217600 37 # 1 Jan 2017
According to the leap-seconds.list file, then, an offset of 37 seconds started in 2017 and should be current through 2024. Lesser whole-second adjustments would be correct for dates from 1972 through 2016. So, for example, for the date of "October 5, 1998", or UTC "1998-10-05", falls after 1997, when the leap seconds last went up to 31, and before 1999, when the total leap seconds then went up to 32. So, the leap seconds value correction for that date is 31 seconds. This is the number of seconds a UTC timestamp would lag behind a TAI time timestamp (see below).
The leap-seconds.list file provided by NIST includes a series of NTP timestamps to indicate the date and time of each update to the leap seconds, as given in the table above.
TAI Time
The above mentioned TAI time, or international atomic clock time, is defined as the number of seconds since UTC 1958-01-01T00:00:00, but with counting the leap seconds not counted by UTC.
GPS Time
Similarly to TAI time, the GPS time, or Global Positioning System convention time, is defined as the number of seconds since UTC 1980-01-06T00:00:00. Since the UTC time in 1980 had already fallen behind TAI time by 19 seconds, GPS time's offset is always 19 less than the (positive) TAI offset used in conversion to TAI from any given UTC standard timestamp.
Task:
In your chosen programming language, write a function or functions to convert between timestamps in UTC, Unix, NTP, TAI, and GPS time conventions. Timestamps should be capable of 1 millisecond precision as a minimum, with microsecond or nanosecond precision optional. Negative values for a timestamp before its epoch can be used when necessary. Add or subtract the proper number of leap seconds as appropriate when converting between leap-second-ignoring (UTC, Unix, NTP) and leap-second-counting (TAI, GPS) timestamp standards.
Demonstrate the use of the function or functions to output values to fill in blanks in the following table:
UTC Unix NTP TAI GPS __________________________________________________________________________________________ 0000-12-31T00:00:00 1900-01-01T00:00:00 1958-01-01T00:00:00 1970-01-01T00:00:00 1980-01-06T00:00:00 1989-12-31T00:00:00 1990-01-01T00:00:00 2025-06-01T00:00:00 2050-01-01T00:00:00 1810753809.806 154956295.688 780673454.121 2871676795 2335219189 3029443171 996796823 996796824 996796825 996796826 996796804.250 996796805.500 996796806.750 996796807.9999
- References
[1] Wikipedia reference
[2] Non-paywalled version of the ISO 8601 standard for UTC
[3] Discussion of the leap-seconds.list file
[4] Comparisons of different time conventions and timestamps
[5] Online time stamp converter
FreeBASIC
#include once "datetime.bi"
#include once "vbcompat.bi"
Const UTC_EPOCH As Double = 0
Dim Shared As Double UNIX_EPOCH, NTP_EPOCH, TAI_EPOCH, GPS_EPOCH
UNIX_EPOCH = Dateserial(1970, 1, 1) * 86400
NTP_EPOCH = Dateserial(1900, 1, 1) * 86400
TAI_EPOCH = Dateserial(1958, 6, 1) * 86400
GPS_EPOCH = Dateserial(1980, 1, 1) * 86400
' TDICT equivalent
Type TDictEntry
As Double key
As Integer value
End Type
Dim Shared TDICT(27) As TDictEntry = { _
(2272060800, 10), (2287785600, 11), (2303683200, 12), (2335219200, 13), _
(2366755200, 14), (2398291200, 15), (2429913600, 16), (2461449600, 17), _
(2492985600, 18), (2524521600, 19), (2571782400, 20), (2603318400, 21), _
(2634854400, 22), (2698012800, 23), (2776982400, 24), (2840140800, 25), _
(2871676800, 26), (2918937600, 27), (2950473600, 28), (2982009600, 29), _
(3029443200, 30), (3076704000, 31), (3124137600, 32), (3345062400, 33), _
(3439756800, 34), (3550089600, 35), (3644697600, 36), (3692217600, 37) }
Function UTC_to_Unix(utc_string As String) As Double
Dim As Double y, m, d, hh, mm, ss
If Len(utc_string) >= 19 Then
y = Val(Mid(utc_string, 1, 4))
m = Val(Mid(utc_string, 6, 2))
d = Val(Mid(utc_string, 9, 2))
hh = Val(Mid(utc_string, 12, 2))
mm = Val(Mid(utc_string, 15, 2))
ss = Val(Mid(utc_string, 18, 2))
End If
Return Dateserial(y, m, d) * 86400 + Timeserial(hh, mm, ss) - UNIX_EPOCH
End Function
Function UTC_to_NTP(utc_string As String) As Double
Return UTC_to_Unix(utc_string) + UNIX_EPOCH - NTP_EPOCH
End Function
Function NTP_to_Unix(ntp_secs As Double) As Double
Return ntp_secs + NTP_EPOCH - UNIX_EPOCH
End Function
Function Unix_to_NTP(unix_secs As Double) As Double
Return unix_secs + UNIX_EPOCH - NTP_EPOCH
End Function
Function Unix_to_UTC(unix_secs As Double) As String
Dim As Double dd = (unix_secs + UNIX_EPOCH) / 86400
Dim As Integer a, m, d, hh, mm, ss
a = Year(dd)
m = Month(dd)
d = Day(dd)
Dim As Double fracDay = dd - Int(dd)
hh = Int(fracDay * 24)
mm = Int((fracDay * 24 - hh) * 60)
ss = Int(((fracDay * 24 - hh) * 60 - mm) * 60)
Return Format(a, "0000") & "-" & Format(m, "00") & "-" & Format(d, "00") & "T" & _
Format(hh, "00") & ":" & Format(mm, "00") & ":" & Format(ss, "00")
End Function
Function NTP_to_UTC(ntp_seconds As Double) As String
Return Unix_to_UTC(NTP_to_Unix(ntp_seconds))
End Function
Function TAI_to_GPS(tai_secs As Double) As Double
Return tai_secs + TAI_EPOCH - GPS_EPOCH - 19
End Function
Function GPS_to_TAI(gps_secs As Double) As Double
Return gps_secs + GPS_EPOCH - TAI_EPOCH + 19
End Function
Function NTP_to_TAI(ntp_secs As Double) As Double
Dim tai As Double = ntp_secs + NTP_EPOCH - TAI_EPOCH
If ntp_secs < TDICT(0).key Then Return tai
If ntp_secs >= TDICT(Ubound(TDICT)).key Then Return tai + TDICT(Ubound(TDICT)).value
Dim i As Integer
For i = 0 To Ubound(TDICT)
If TDICT(i).key > ntp_secs Then
Return tai + TDICT(i - 1).value
End If
Next
Return tai
End Function
Function TAI_to_NTP(tai_secs As Double) As Double
Dim ntp As Double = tai_secs + TAI_EPOCH - NTP_EPOCH
If ntp < TDICT(0).key Then Return ntp
Dim As Integer delta, i
If ntp >= TDICT(Ubound(TDICT)).key Then
delta = TDICT(Ubound(TDICT)).value
Else
For i = 0 To Ubound(TDICT)
If TDICT(i).key > ntp Then
delta = TDICT(i - 1).value
Exit For
End If
Next
End If
If ntp - delta < TDICT(0).key Then
Return TDICT(0).key - 1 + (ntp - Int(ntp))
Elseif ntp - delta < TDICT(i - 1).key Then
Return ntp - delta + 1
Else
Return ntp - delta
End If
End Function
Function NTP_to_GPS(ntp_secs As Double) As Double
Return TAI_to_GPS(NTP_to_TAI(ntp_secs))
End Function
Function GPS_to_NTP(gps_secs As Double) As Double
Return TAI_to_NTP(GPS_to_TAI(gps_secs))
End Function
Function TAI_to_Unix(tai_secs As Double) As Double
Return NTP_to_Unix(TAI_to_NTP(tai_secs))
End Function
Function Unix_to_TAI(unix_secs As Double) As Double
Return NTP_to_TAI(Unix_to_NTP(unix_secs))
End Function
Function TAI_to_UTC(tai_secs As Double) As String
Return NTP_to_UTC(TAI_to_NTP(tai_secs))
End Function
Function UTC_to_TAI(utc_string As String) As Double
Return NTP_to_TAI(UTC_to_NTP(utc_string))
End Function
Function GPS_to_Unix(gps_secs As Double) As Double
Return TAI_to_Unix(GPS_to_TAI(gps_secs))
End Function
Function Unix_to_GPS(unix_secs As Double) As Double
Return TAI_to_GPS(Unix_to_TAI(unix_secs))
End Function
Function UTC_to_GPS(utc_string As String) As Double
Return TAI_to_GPS(UTC_to_TAI(utc_string))
End Function
Function GPS_to_UTC(gps_secs As Double) As String
Return TAI_to_UTC(GPS_to_TAI(gps_secs))
End Function
' Main program
Print " UTC"; Spc(21); "Unix"; Spc(17); "NTP"; Spc(17); "TAI"; Spc(17); "GPS"
Print String(100, "_")
Dim utc_dates(8) As String = { _
"0001-01-01T00:00:00", "1900-01-01T00:00:00", "1958-01-01T00:00:00", _
"1970-01-01T00:00:00", "1980-01-06T00:00:00", "1989-12-31T00:00:00", _
"1990-01-01T00:00:00", "2025-06-01T00:00:00", "2050-01-01T00:00:00" }
Dim As Integer i
For i = 0 To 8
Print Left(utc_dates(i) & Space(22), 22);
Print Using "############.#### "; UTC_to_Unix(utc_dates(i));
Print Using "############.#### "; UTC_to_NTP(utc_dates(i));
Print Using "############.#### "; UTC_to_TAI(utc_dates(i));
Print Using "############.####"; UTC_to_GPS(utc_dates(i))
Next i
Dim unix_times(2) As Double = {1810753809.806, 154956295.688, 780673454.121}
For i = 0 To 2
Print Left(Unix_to_UTC(unix_times(i)) & Space(22), 22);
Print Using "############.#### "; unix_times(i);
Print Using "############.#### "; Unix_to_NTP(unix_times(i));
Print Using "############.#### "; Unix_to_TAI(unix_times(i));
Print Using "############.####"; Unix_to_GPS(unix_times(i))
Next i
Dim As Double ntp_times(2) = {2871676795, 2335219189, 3029443171}
For i = 0 To 2
Print Left(NTP_to_UTC(ntp_times(i)) & Space(22), 22);
Print Using "############.#### "; NTP_to_Unix(ntp_times(i));
Print Using "############.#### "; ntp_times(i);
Print Using "############.#### "; NTP_to_TAI(ntp_times(i));
Print Using "############.####"; NTP_to_GPS(ntp_times(i))
Next i
Dim As Double tai_times(3) = {996796823, 996796824, 996796825, 996796826}
For i = 0 To 3
Print Left(TAI_to_UTC(tai_times(i)) & Space(22), 22);
Print Using "############.#### "; TAI_to_Unix(tai_times(i));
Print Using "############.#### "; TAI_to_NTP(tai_times(i));
Print Using "############.#### "; tai_times(i);
Print Using "############.####"; TAI_to_GPS(tai_times(i))
Next i
Dim As Double gps_times(3) = {996796804.250, 996796805.5, 996796806.750, 996796807.9999}
For i = 0 To 3
Print Left(GPS_to_UTC(gps_times(i)) & Space(22), 22);
Print Using "############.#### "; GPS_to_Unix(gps_times(i));
Print Using "############.#### "; GPS_to_NTP(gps_times(i));
Print Using "############.#### "; GPS_to_TAI(gps_times(i));
Print Using "############.####"; gps_times(i)
Next i
Sleep
- Output:
UTC Unix NTP TAI GPS ____________________________________________________________________________________________________ 0001-01-01T00:00:00 -62135596800.0000 -59926608000.0000 -61769952000.0000 -62451129619.0000 1900-01-01T00:00:00 -2208988800.0000 0.0000 -1843344000.0000 -2524521619.0000 1958-01-01T00:00:00 -378691200.0000 1830297600.0000 -13046400.0000 -694224019.0000 1970-01-01T00:00:00 0.0000 2208988800.0000 365644800.0000 -315532819.0000 1980-01-06T00:00:00 315964800.0000 2524953600.0000 681609619.0000 432000.0000 1989-12-31T00:00:00 631065600.0000 2840054400.0000 996710424.0000 315532805.0000 1990-01-01T00:00:00 631152000.0000 2840140800.0000 996796825.0000 315619206.0000 2025-06-01T00:00:00 1748736000.0000 3957724800.0000 2114380837.0000 1433203218.0000 2050-01-01T00:00:00 2524608000.0000 4733596800.0000 2890252837.0000 2209075218.0000 2027-05-19T19:10:09 1810753809.8060 4019742609.8060 2176398646.8060 1495221027.8060 1974-11-29T11:24:55 154956295.6880 2363945095.6880 520601108.6880 -160576510.3120 1994-09-27T13:44:14 780673454.1210 2989662254.1210 1146318283.1210 465140664.1210 1990-12-31T23:59:55 662687995.0000 2871676795.0000 1028332820.0000 347155201.0000 1973-12-31T23:59:48 126230389.0000 2335219189.0000 491875201.0000 -189302418.0000 1995-12-31T23:59:31 820454371.0000 3029443171.0000 1186099200.0000 504921581.0000 1989-12-31T23:59:58 631151999.0000 2840140799.0000 996796823.0000 315619204.0000 1990-01-01T00:00:00 631152000.0000 2840140800.0000 996796824.0000 315619205.0000 1990-01-01T00:00:00 631152000.0000 2840140800.0000 996796825.0000 315619206.0000 1990-01-01T00:00:01 631152001.0000 2840140801.0000 996796826.0000 315619207.0000 2011-08-02T23:59:49 1312329589.2500 3521318389.2500 1677974423.2500 996796804.2500 2011-08-02T23:59:50 1312329590.5000 3521318390.5000 1677974424.5000 996796805.5000 2011-08-02T23:59:51 1312329591.7500 3521318391.7500 1677974425.7500 996796806.7500 2011-08-02T23:59:52 1312329592.9999 3521318392.9999 1677974426.9999 996796807.9999
Java
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
public final class TimeConventionsAndConversions {
public static void main(String[] args) {
System.out.println(" UTC Unix NTP"
+ " TAI GPS");
System.out.println("-".repeat(107));
List<String> utcs = List.of(
"0001-01-01T00:00:00", "1900-01-01T00:00:00", "1958-01-01T00:00:00",
"1970-01-01T00:00:00", "1980-01-06T00:00:00", "1989-12-31T00:00:00",
"1990-01-01T00:00:00", "2025-06-01T00:00:00", "2050-01-01T00:00:00" );
utcs.forEach( s -> System.out.println(String.format("%-23s %16.0f %20.0f %20.0f %20.0f",
s, utc2unix.apply(s), utc2ntp.apply(s), utc2tai.apply(s), utc2gps.apply(s) )));
List<Double> unixs = List.of( 1810753809.806, 154956295.688, 780673454.121 );
unixs.forEach( i -> { String dateTime = unix2utc.apply(i).replace(" ", "T");
System.out.println(String.format("%-23s %20.3f %20.3f %20.3f %20.3f",
dateTime, i, unix2ntp.apply(i), unix2tai.apply(i), unix2gps.apply(i)));
});
List<Double> ntps = List.of( 2871676795.0, 2335219189.0, 3029443171.0 );
ntps.forEach( i -> { String dateTime = ntp2utc.apply(i).replace(" ", "T");
System.out.println(String.format("%-23s %16.0f %20.0f %20.0f %20.0f",
dateTime, ntp2unix.apply(i), i, ntp2tai(i), ntp2gps.apply(i)));
});
List<Double> tais = List.of( 996796823.0, 996796824.0, 996796825.0, 996796826.0 );
tais.forEach( i -> { String dateTime = tai2utc.apply(i).replace(" ", "T");
System.out.println(String.format("%-23s %16.0f %20.0f %20.0f %20.0f",
dateTime, tai2unix.apply(i), tai2ntp(i), i, tai2gps.apply(i)));
});
List<Double> gpss = List.of( 996796804.250, 996796805.5, 996796806.750, 996796807.9999 );
gpss.forEach( i -> { String dateTime = gps2utc.apply(i).replace(" ", "T");
System.out.println(String.format("%-24s %20.4f %20.4f %20.4f %20.4f",
dateTime, gps2unix.apply(i), gps2ntp.apply(i), gps2tai.apply(i), i));
});
}
private static Function<String, Double> string2Seconds = s -> {
Instant instant = LocalDateTime.parse(s).atOffset(ZoneOffset.UTC).toInstant();
return instant.getEpochSecond() + (double) instant.getNano() / 1_000_000_000;
};
private static Function<Double, String> seconds2String = i -> {
final long seconds = (long) Math.floor(i);
final int nanos = Math.toIntExact(Math.round(( i - seconds ) * 10_000)) * 100_000;
return LocalDateTime.ofInstant(Instant.ofEpochSecond(seconds, nanos), ZoneOffset.UTC)
.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
};
private static final double UNIX_EPOCH = string2Seconds.apply("1970-01-01T00:00:00");
private static final double NTP_EPOCH = string2Seconds.apply("1900-01-01T00:00:00");
private static final double TAI_EPOCH = string2Seconds.apply("1958-06-01T00:00:00");
private static final double GPS_EPOCH = string2Seconds.apply("1980-01-01T00:00:00");
private static Function<String, Double> utc2unix = s -> string2Seconds.apply(s) - UNIX_EPOCH;
private static Function<String, Double> utc2ntp = s -> string2Seconds.apply(s) + UNIX_EPOCH - NTP_EPOCH;
private static Function<Double, Double> ntp2unix = i -> i + NTP_EPOCH - UNIX_EPOCH;
private static Function<Double, Double> unix2ntp = i -> i + UNIX_EPOCH - NTP_EPOCH;
private static Function<Double, String> unix2utc = i -> seconds2String.apply(i);
private static Function<Double, String> ntp2utc = i -> unix2utc.apply(ntp2unix.apply(i));
private static Function<Double, Double> tai2gps = i -> i + TAI_EPOCH - GPS_EPOCH - 19.0;
private static Function<Double, Double> gps2tai = i -> i + GPS_EPOCH - TAI_EPOCH + 19.0;
private static double ntp2tai(double ntpSeconds) {
final double tai = ntpSeconds + NTP_EPOCH - TAI_EPOCH;
if ( ntpSeconds < CHANGE_TIMES.getFirst() ) {
return tai;
}
if ( ntpSeconds >= CHANGE_TIMES.getLast() ) {
return tai + NTP_TO_LS.get(CHANGE_TIMES.getLast());
}
int i = 0;
while ( ntpSeconds >= CHANGE_TIMES.get(i) && i < CHANGE_TIMES.size() ) {
i += 1;
}
return tai + NTP_TO_LS.get(CHANGE_TIMES.get(i - 1));
}
private static double tai2ntp(double taiSeconds) {
final double ntp = taiSeconds + TAI_EPOCH - NTP_EPOCH;
if ( ntp < CHANGE_TIMES.getFirst() ) {
return ntp;
}
int index = 0;
long delta = 0;
if ( ntp >= CHANGE_TIMES.getLast() ) {
delta = NTP_TO_LS.get(CHANGE_TIMES.getLast());
} else {
for ( int i = 1; i < CHANGE_TIMES.size(); i++ ) {
if ( ntp < CHANGE_TIMES.get(i) ) {
delta = NTP_TO_LS.get(CHANGE_TIMES.get(i - 1));
index = i;
break;
}
}
}
if ( ntp - delta < CHANGE_TIMES.getFirst() ) {
return CHANGE_TIMES.getFirst() - 1 + ntp % 1;
}
if ( ntp - delta < CHANGE_TIMES.get(index - 1) ) {
return ntp - delta + 1;
}
return ntp - delta;
}
private static Function<Double, Double> ntp2gps = i -> tai2gps.apply(ntp2tai(i));
private static Function<Double, Double> gps2ntp = i -> tai2ntp(gps2tai.apply(i));
private static Function<Double, Double> tai2unix = i -> ntp2unix.apply(tai2ntp(i));
private static Function<Double, Double> unix2tai = i -> ntp2tai(unix2ntp.apply(i));
private static Function<Double, String> tai2utc = i -> ntp2utc.apply(tai2ntp(i));
private static Function<String, Double> utc2tai = s -> ntp2tai(utc2ntp.apply(s));
private static Function<Double, Double> gps2unix = i -> tai2unix.apply(gps2tai.apply(i));
private static Function<Double, Double> unix2gps = i -> tai2gps.apply(unix2tai.apply(i));
private static Function<String, Double> utc2gps = s -> tai2gps.apply(utc2tai.apply(s));
private static Function<Double, String> gps2utc = i -> tai2utc.apply(gps2tai.apply(i));
private static final Map<Long, Integer> NTP_TO_LS = Map.ofEntries(
Map.entry(2272060800L, 10), Map.entry(2287785600L, 11), Map.entry(2303683200L, 12),
Map.entry(2335219200L, 13), Map.entry(2366755200L, 14), Map.entry(2398291200L, 15),
Map.entry(2429913600L, 16), Map.entry(2461449600L, 17), Map.entry(2492985600L, 18),
Map.entry(2524521600L, 19), Map.entry(2571782400L, 20), Map.entry(2603318400L, 21),
Map.entry(2634854400L, 22), Map.entry(2698012800L, 23), Map.entry(2776982400L, 24),
Map.entry(2840140800L, 25), Map.entry(2871676800L, 26), Map.entry(2918937600L, 27),
Map.entry(2950473600L, 28), Map.entry(2982009600L, 29), Map.entry(3029443200L, 30),
Map.entry(3076704000L, 31), Map.entry(3124137600L, 32), Map.entry(3345062400L, 33),
Map.entry(3439756800L, 34), Map.entry(3550089600L, 35), Map.entry(3644697600L, 36),
Map.entry(3692217600L, 37) );
private static final List<Long> CHANGE_TIMES = NTP_TO_LS.keySet().stream().sorted().toList();
}
- Output:
UTC Unix NTP TAI GPS ----------------------------------------------------------------------------------------------------------- 0001-01-01T00:00:00 -62135596800 -59926608000 -61769952000 -62451129619 1900-01-01T00:00:00 -2208988800 0 -1843344000 -2524521619 1958-01-01T00:00:00 -378691200 1830297600 -13046400 -694224019 1970-01-01T00:00:00 0 2208988800 365644800 -315532819 1980-01-06T00:00:00 315964800 2524953600 681609619 432000 1989-12-31T00:00:00 631065600 2840054400 996710424 315532805 1990-01-01T00:00:00 631152000 2840140800 996796825 315619206 2025-06-01T00:00:00 1748736000 3957724800 2114380837 1433203218 2050-01-01T00:00:00 2524608000 4733596800 2890252837 2209075218 2027-05-19T19:10:09.806 1810753809.806 4019742609.806 2176398646.806 1495221027.806 1974-11-29T11:24:55.688 154956295.688 2363945095.688 520601108.688 -160576510.312 1994-09-27T13:44:14.121 780673454.121 2989662254.121 1146318283.121 465140664.121 1990-12-31T23:59:55 662687995 2871676795 1028332820 347155201 1973-12-31T23:59:49 126230389 2335219189 491875201 -189302418 1995-12-31T23:59:31 820454371 3029443171 1186099200 504921581 1989-12-31T23:59:59 631151999 2840140799 996796823 315619204 1990-01-01T00:00:00 631152000 2840140800 996796824 315619205 1990-01-01T00:00:00 631152000 2840140800 996796825 315619206 1990-01-01T00:00:01 631152001 2840140801 996796826 315619207 2011-08-02T23:59:49.25 1312329589.2500 3521318389.2500 1677974423.2500 996796804.2500 2011-08-02T23:59:50.5 1312329590.5000 3521318390.5000 1677974424.5000 996796805.5000 2011-08-02T23:59:51.75 1312329591.7500 3521318391.7500 1677974425.7500 996796806.7500 2011-08-02T23:59:52.9999 1312329592.9999 3521318392.9999 1677974426.9999 996796807.9999
Julia
""" timestamp conversions for rsetta code """
using NanoDates
using Printf
# internal calculations are in nanoseconds for a NanoDate
const SECONDS_PER_DAY = 86400
const NANOSECONDS_PER_SECOND = Int128(1_000_000_000)
const NANOSECONDS_PER_MILLISECOND = Int128(1_000_000)
const UNIX_EPOCH_NANOSECONDS = 62135683200 * NANOSECONDS_PER_SECOND
const NTP_EPOCH_NANOSECONDS = 59926694400 * NANOSECONDS_PER_SECOND
const TAI_EPOCH_NANOSECONDS = 61756992000 * NANOSECONDS_PER_SECOND
const GPS_EPOCH_NANOSECONDS = 62451648000 * NANOSECONDS_PER_SECOND
const TAI_GPS_DIFFERENCE_NANOSECONDS = 19 * NANOSECONDS_PER_SECOND
const NTP_TO_LEAP_SECONDS = [
(2272060800, 10),
(2287785600, 11),
(2303683200, 12),
(2335219200, 13),
(2366755200, 14),
(2398291200, 15),
(2429913600, 16),
(2461449600, 17),
(2492985600, 18),
(2524521600, 19),
(2571782400, 20),
(2603318400, 21),
(2634854400, 22),
(2698012800, 23),
(2776982400, 24),
(2840140800, 25),
(2871676800, 26),
(2918937600, 27),
(2950473600, 28),
(2982009600, 29),
(3029443200, 30),
(3076704000, 31),
(3124137600, 32),
(3345062400, 33),
(3439756800, 34),
(3550089600, 35),
(3644697600, 36),
(3692217600, 37),
];
"""
Finds the index of the leap second that should be used based on the given NTP time in seconds.
"""
function leap_seconds_current_index(ntp_secs)
ntp_secs < NTP_TO_LEAP_SECONDS[begin][begin] && return nothing
for (i, pair) in enumerate(NTP_TO_LEAP_SECONDS)
ntp_secs < pair[begin] && return i - 1
end
return length(NTP_TO_LEAP_SECONDS) - 1
end
from_utc(utc_string::AbstractString) = NanoDate(utc_string)
from_unix(unix_secs::Integer) =
NanoDate(unix_secs * NANOSECONDS_PER_SECOND + UNIX_EPOCH_NANOSECONDS)
from_unix(unix_secs) =
NanoDate((Int128(round(unix_secs * NANOSECONDS_PER_SECOND)) + UNIX_EPOCH_NANOSECONDS))
from_ntp(ntp_secs::Integer) =
NanoDate(ntp_secs * NANOSECONDS_PER_SECOND + NTP_EPOCH_NANOSECONDS)
from_ntp(ntp_secs) =
NanoDate(round(ntp_secs * NANOSECONDS_PER_SECOND) + NTP_EPOCH_NANOSECONDS)
function from_tai_nsec(tai_nsec::Int128)
ntp_secs =
(tai_nsec + TAI_EPOCH_NANOSECONDS - NTP_EPOCH_NANOSECONDS) ÷ NANOSECONDS_PER_SECOND
nsecs = tai_nsec % NANOSECONDS_PER_SECOND
idx = leap_seconds_current_index(ntp_secs)
isnothing(idx) &&
return NanoDate(ntp_secs * NANOSECONDS_PER_SECOND + nsecs + NTP_EPOCH_NANOSECONDS)
delta = NTP_TO_LEAP_SECONDS[idx][end]
return NanoDate(
(
ntp_secs - delta < NTP_TO_LEAP_SECONDS[begin][begin] ?
NANOSECONDS_PER_SECOND * NTP_TO_LEAP_SECONDS[begin][begin] -
NANOSECONDS_PER_SECOND + (tai_nsec % NANOSECONDS_PER_SECOND) :
ntp_secs - delta < NTP_TO_LEAP_SECONDS[idx][begin] ?
(ntp_secs - delta) * NANOSECONDS_PER_SECOND + NANOSECONDS_PER_SECOND + nsecs :
(ntp_secs - delta) * NANOSECONDS_PER_SECOND + nsecs
) + NTP_EPOCH_NANOSECONDS,
)
end
from_tai(tai_secs::Integer) = from_tai_nsec(tai_secs * NANOSECONDS_PER_SECOND)
from_tai(tai_secs) = from_tai_nsec(Int128(round(tai_secs * NANOSECONDS_PER_SECOND)))
function from_gps_nsec(gps_nsecs::Int128)
return from_tai_nsec(
gps_nsecs + GPS_EPOCH_NANOSECONDS - TAI_EPOCH_NANOSECONDS +
TAI_GPS_DIFFERENCE_NANOSECONDS,
)
end
from_gps(gps_secs::Integer) = from_gps_nsec(gps_secs * NANOSECONDS_PER_SECOND)
from_gps(gps_secs) = from_gps_nsec(Int128(round(gps_secs * NANOSECONDS_PER_SECOND)))
function to_utc(nd::NanoDate; precision = 3)
fmt = Printf.Format("%07.$(precision)f")
s = string(nd)
return replace(
s,
r"(\d\d\.\d+)" => (ns) -> Printf.format(fmt, parse(Float64, ns))
)
end
function to_unix(nd::NanoDate)
return NanoDates.value(nd) % NANOSECONDS_PER_SECOND == 0 ?
(NanoDates.value(nd) - UNIX_EPOCH_NANOSECONDS) ÷ NANOSECONDS_PER_SECOND :
(NanoDates.value(nd) - UNIX_EPOCH_NANOSECONDS) / NANOSECONDS_PER_SECOND
end
function to_ntp(nd::NanoDate)
return NanoDates.value(nd) % NANOSECONDS_PER_SECOND == 0 ?
(NanoDates.value(nd) - NTP_EPOCH_NANOSECONDS) ÷ NANOSECONDS_PER_SECOND :
(NanoDates.value(nd) - NTP_EPOCH_NANOSECONDS) / NANOSECONDS_PER_SECOND
end
function to_tai_nsec(nd::NanoDate)
ntp_sec = (NanoDates.value(nd) - NTP_EPOCH_NANOSECONDS) / NANOSECONDS_PER_SECOND
idx = leap_seconds_current_index(ntp_sec)
return idx == nothing ? NanoDates.value(nd) - TAI_EPOCH_NANOSECONDS :
NanoDates.value(nd) - TAI_EPOCH_NANOSECONDS +
NANOSECONDS_PER_SECOND * NTP_TO_LEAP_SECONDS[idx][end]
end
function to_tai(nd::NanoDate)
tai_nsec = to_tai_nsec(nd)
return tai_nsec % NANOSECONDS_PER_SECOND == 0 ?
tai_nsec ÷ NANOSECONDS_PER_SECOND :
tai_nsec / NANOSECONDS_PER_SECOND
end
function to_gps(nd::NanoDate)
gps_nsec =
to_tai_nsec(nd) + TAI_EPOCH_NANOSECONDS - GPS_EPOCH_NANOSECONDS -
TAI_GPS_DIFFERENCE_NANOSECONDS
return gps_nsec % NANOSECONDS_PER_SECOND == 0 ?
gps_nsec ÷ NANOSECONDS_PER_SECOND :
gps_nsec / NANOSECONDS_PER_SECOND
end
"""
print_stamps(nd::NanoDate, s, str_idx; precision = "3", sfmt = Printf.Format("%.3f"))
Prints formatted timestamp information based on the given `NanoDate` object.
# Arguments
- `nd::NanoDate`: The date and time to be formatted and printed.
- `s`: A string to be printed when `str_idx` matches the index.
- `str_idx`: An integer indicating which string index to print.
- `precision`: A string specifying the desired precision for the UTC representation. Defaults to "3".
- `sfmt`: A `Printf.Format` object specifying the format for floating-point numbers. Defaults to `Printf.Format("%.3f")`.
# Description
The function prints out various time representations of the given `NanoDate` object `nd`. It uses `str_idx` to determine which string to print, and formats the output based on whether the time is an integer second or not. The representations include UTC, Unix, NTP, TAI, and GPS time formats.
"""
function print_stamps(
nd::NanoDate,
s,
str_idx;
precision = "3",
sfmt = Printf.Format("%.3f"),
)
int = NanoDates.value(nd) % NANOSECONDS_PER_SECOND == 0
print(rpad(str_idx == 1 ? s : to_utc(nd; precision), 26))
print(rpad(str_idx == 2 ? s : int ? to_unix(nd) : Printf.format(sfmt, to_unix(nd)), 17))
print(rpad(str_idx == 3 ? s : int ? to_ntp(nd) : Printf.format(sfmt, to_ntp(nd)), 17))
print(rpad(str_idx == 4 ? s : int ? to_tai(nd) : Printf.format(sfmt, to_tai(nd)), 17))
println(rpad(str_idx == 5 ? s : int ? to_gps(nd) : Printf.format(sfmt, to_gps(nd)), 17))
end
"""
timestamp_conversions()
Converts and prints timestamp information across various time standards including UTC, Unix, NTP, TAI, and GPS.
The function performs the following steps:
- Prints a header with the names of the time standards.
- Iterates over a list of predefined UTC timestamps, converting each to a normalized date and time, and prints the results.
- Converts and prints a specific floating-point UTC timestamp.
- Converts and prints a list of Unix timestamps.
- Converts and prints a list of NTP timestamps.
- Converts and prints a list of TAI timestamps.
- Converts and prints a list of GPS timestamps with specified precision.
Each conversion relies on helper functions (`from_utc`, `from_unix`, `from_ntp`, `from_tai`, `from_gps`) to parse and convert the timestamps, and `print_stamps` to format and display the results.
"""
function timestamp_conversions()
@printf("%17s%13s%22s%16s%17s\n", "UTC", "Unix", "NTP", "TAI", "GPS")
println("_"^96)
for s in [
"0001-01-01T00:00:00",
"1900-01-01T00:00:00",
"1958-01-01T00:00:00",
"1970-01-01T00:00:00",
"1980-01-06T00:00:00",
"1989-12-31T00:00:00",
"1990-01-01T00:00:00",
"2025-06-01T00:00:00",
"2050-01-01T00:00:00",
]
nd = from_utc(s)
isnothing(nd) && throw("Parse error parsing $s")
print_stamps(nd, s, 1)
end
floating = "1900-01-01T00:00:07.36"
nd = from_utc(floating)
isnothing(nd) && throw("Parse error parsing $floating")
print_stamps(nd, floating, 1)
for unix in [1810753809.806, 154956295.688, 780673454.121]
nd = from_unix(unix)
print_stamps(nd, @sprintf("%.3f", unix), 2)
end
for ntp in [2871676795, 2335219189, 3029443171]
nd = from_ntp(ntp)
print_stamps(nd, string(ntp), 3)
end
for tai in [996796823, 996796824, 996796825, 996796826, 1293840030, 1293840031, 1293840032, 1293840033,]
nd = from_tai(tai)
print_stamps(nd, string(tai), 4)
end
for gps in [996796804.250, 996796805.5, 996796806.750, 996796807.9999]
nd = from_gps(gps)
print_stamps(
nd,
@sprintf("%.4f", gps),
5,
precision = "4",
sfmt = Printf.Format("%.4f"),
)
end
end
timestamp_conversions()
- Output:
UTC Unix NTP TAI GPS ________________________________________________________________________________________________ 0001-01-01T00:00:00 -62135596800 -59926608000 -61756905600 -62451561619 1900-01-01T00:00:00 -2208988800 0 -1830297600 -2524953619 1958-01-01T00:00:00 -378691200 1830297600 0 -694656019 1970-01-01T00:00:00 0 2208988800 378691200 -315964819 1980-01-06T00:00:00 315964800 2524953600 694656019 0 1989-12-31T00:00:00 631065600 2840054400 1009756824 315100805 1990-01-01T00:00:00 631152000 2840140800 1009843225 315187206 2025-06-01T00:00:00 1748736000 3957724800 2127427236 1432771217 2050-01-01T00:00:00 2524608000 4733596800 2903299236 2208643217 1900-01-01T00:00:07.36 -2208988792.640 7.360 -1830297592.640 -2524953611.640 2027-05-19T19:10:009.806 1810753809.806 4019742609.806 2189445045.806 1494789026.806 1974-11-29T11:24:055.688 154956295.688 2363945095.688 533647508.688 -161008510.312 1994-09-27T13:44:014.121 780673454.121 2989662254.121 1159364683.121 464708664.121 1990-12-31T23:59:55 662687995 2871676795 1041379220 346723201 1973-12-31T23:59:49 126230389 2335219189 504921601 -189734418 1995-12-31T23:59:31 820454371 3029443171 1199145600 504489581 1989-08-02T23:59:59 618105599 2827094399 996796823 302140804 1989-08-03T00:00:00 618105600 2827094400 996796824 302140805 1989-08-03T00:00:01 618105601 2827094401 996796825 302140806 1989-08-03T00:00:02 618105602 2827094402 996796826 302140807 1998-12-31T23:59:59 915148799 3124137599 1293840030 599184011 1999-01-01T00:00:00 915148800 3124137600 1293840031 599184013 1999-01-01T00:00:00 915148800 3124137600 1293840032 599184013 1999-01-01T00:00:01 915148801 3124137601 1293840033 599184014 2011-08-07T23:59:49.2500 1312761589.2500 3521750389.2500 1691452823.2500 996796804.2500 2011-08-07T23:59:50.5000 1312761590.5000 3521750390.5000 1691452824.5000 996796805.5000 2011-08-07T23:59:51.7500 1312761591.7500 3521750391.7500 1691452825.7500 996796806.7500 2011-08-07T23:59:52.9999 1312761592.9999 3521750392.9999 1691452826.9999 996796807.9999
Phix
with javascript_semantics
include timedate.e
function to_ka(string s) return apply(split(s)[1..2],to_number) end function
constant UNIX_EPOCH = parse_date_string("1970-01-01",{"YYYY-MM-DD"}),
NTP_EPOCH = parse_date_string("1900-01-01",{"YYYY-MM-DD"}),
TAI_EPOCH = parse_date_string("1958-01-01",{"YYYY-MM-DD"}),
GPS_EPOCH = parse_date_string("1980-01-06",{"YYYY-MM-DD"}),
-- from (eg) https://data.iana.org/time-zones/tzdb/leap-seconds.list
-- (or from task description, expires on 28 June 2025)
leap_seconds = """
2272060800 10 # 1 Jan 1972
2287785600 11 # 1 Jul 1972
2303683200 12 # 1 Jan 1973
2335219200 13 # 1 Jan 1974
2366755200 14 # 1 Jan 1975
2398291200 15 # 1 Jan 1976
2429913600 16 # 1 Jan 1977
2461449600 17 # 1 Jan 1978
2492985600 18 # 1 Jan 1979
2524521600 19 # 1 Jan 1980
2571782400 20 # 1 Jul 1981
2603318400 21 # 1 Jul 1982
2634854400 22 # 1 Jul 1983
2698012800 23 # 1 Jul 1985
2776982400 24 # 1 Jan 1988
2840140800 25 # 1 Jan 1990
2871676800 26 # 1 Jan 1991
2918937600 27 # 1 Jul 1992
2950473600 28 # 1 Jul 1993
2982009600 29 # 1 Jul 1994
3029443200 30 # 1 Jan 1996
3076704000 31 # 1 Jul 1997
3124137600 32 # 1 Jan 1999
3345062400 33 # 1 Jan 2006
3439756800 34 # 1 Jan 2009
3550089600 35 # 1 Jul 2012
3644697600 36 # 1 Jul 2015
3692217600 37 # 1 Jan 2017
""",
{tdkeys, tdadjs} = columnize(apply(split(leap_seconds,"\n"),to_ka))
function ntp_to_tai(atom ntp_secs)
atom tai = ntp_secs + timedate_diff(TAI_EPOCH,NTP_EPOCH)
if ntp_secs >= tdkeys[1] then
if ntp_secs >= tdkeys[$] then
tai += tdadjs[$]
else
atom pa = 0
for i,k in tdkeys do
if k > ntp_secs then
tai += pa
exit
end if
pa = tdadjs[i]
end for
end if
end if
return tai
end function
function tai_to_ntp(atom tai_secs)
atom ntp = tai_secs + timedate_diff(NTP_EPOCH,TAI_EPOCH),
tk1 = tdkeys[1]
if ntp >= tk1 then
atom delta, pk, pa
if ntp >= tdkeys[$] then
delta = tdadjs[$]
else
for i,k in tdkeys do
if k > ntp then
delta = pa
exit
end if
pk = k
pa = tdadjs[i]
end for
end if
if ntp - delta < tk1 then
ntp = tk1 - 1 + (ntp - floor(ntp))
elsif ntp - delta < pk then
ntp = ntp - delta + 1
else
ntp -= delta
end if
end if
-- hmmm...
--?{tai_secs,ntp_to_tai(ntp)}
--assert(tai_secs == ntp_to_tai(ntp))
return ntp
end function
function tai_to_gps(atom tai)
return tai + timedate_diff(GPS_EPOCH,TAI_EPOCH) - 19
end function
function gps_to_tai(atom gps)
return gps - timedate_diff(GPS_EPOCH,TAI_EPOCH) + 19
end function
printf(1,"%11s %24s %17s %19s %19s\n%s\n",{"UTC","Unix","NTP","TAI","GPS",repeat('_',100)})
string s
timedate td
atom unix, ntp, tai, gps
procedure printem(string set)
if set="s" then
td = parse_date_string(s,{"YYYY-MM-DD'T'hh:mm:ss"})
else
if set="unix" then
td = adjust_timedate(UNIX_EPOCH,unix)
elsif set="ntp" or set="tai" then
td = adjust_timedate(NTP_EPOCH,ntp)
end if
if td[DT_MSEC] then
s = format_timedate(td,"YYYY-MM-DD'T'hh:mm:ss.ms")
else
s = format_timedate(td,"YYYY-MM-DD'T'hh:mm:ss")
end if
end if
unix = timedate_diff(UNIX_EPOCH,td)
ntp = timedate_diff(NTP_EPOCH,td)
if set!="tai" then
tai = ntp_to_tai(ntp)
end if
gps = tai_to_gps(tai)
printf(1,"%-23s %17.4F %17.4F %19.4F %19.4F\n",{s,unix,ntp,tai,gps})
end procedure
for s in {"0001-01-01T00:00:00", "1900-01-01T00:00:00", "1958-01-01T00:00:00",
"1970-01-01T00:00:00", "1980-01-06T00:00:00", "1989-12-31T00:00:00",
"1990-01-01T00:00:00", "2025-06-01T00:00:00", "2050-01-01T00:00:00"} do
-- "1990-01-06T00:00:00", -- (matches Python [typo])
-- "1900-01-01T00:00:07.36"} do -- see note
printem("s")
end for
for unix in {1810753809.806, 154956295.688, 780673454.121} do
printem("unix")
end for
for ntp in {2871676795, 2335219189, 3029443171} do
printem("ntp")
end for
for tai in {996796823, 996796824, 996796825, 996796826} do
--for tai in {996796823, 996796824, 996796825, 996796826,
-- 1293840030, 1293840031, 1293840032, 1293840033} do
ntp = tai_to_ntp(tai)
printem("tai")
end for
for gps in {996796804.250, 996796805.5, 996796806.750, 996796807.9999} do
ntp = tai_to_ntp(gps_to_tai(gps))
printem("ntp")
end for
- Output:
UTC Unix NTP TAI GPS ____________________________________________________________________________________________________ 0001-01-01T00:00:00 -62135596800 -59926608000 -61756905600 -62451561619 1900-01-01T00:00:00 -2208988800 0 -1830297600 -2524953619 1958-01-01T00:00:00 -378691200 1830297600 0 -694656019 1970-01-01T00:00:00 0 2208988800 378691200 -315964819 1980-01-06T00:00:00 315964800 2524953600 694656019 0 1989-12-31T00:00:00 631065600 2840054400 1009756824 315100805 1990-01-01T00:00:00 631152000 2840140800 1009843225 315187206 2025-06-01T00:00:00 1748736000 3957724800 2127427237 1432771218 2050-01-01T00:00:00 2524608000 4733596800 2903299237 2208643218 2027-05-19T19:10:09.806 1810753809.806 4019742609.806 2189445046.806 1494789027.806 1974-11-29T11:24:55.688 154956295.688 2363945095.688 533647508.688 -161008510.312 1994-09-27T13:44:14.121 780673454.121 2989662254.121 1159364683.121 464708664.121 1990-12-31T23:59:55 662687995 2871676795 1041379220 346723201 1973-12-31T23:59:49 126230389 2335219189 504921601 -189734418 1995-12-31T23:59:31 820454371 3029443171 1199145600 504489581 1989-08-02T23:59:59 618105599 2827094399 996796823 302140804 1989-08-03T00:00:00 618105600 2827094400 996796824 302140805 1989-08-03T00:00:01 618105601 2827094401 996796825 302140806 1989-08-03T00:00:02 618105602 2827094402 996796826 302140807 2011-08-07T23:59:49.250 1312761589.25 3521750389.25 1691452823.25 996796804.25 2011-08-07T23:59:50.500 1312761590.5 3521750390.5 1691452824.5 996796805.5 2011-08-07T23:59:51.750 1312761591.75 3521750391.75 1691452825.75 996796806.75 2011-08-07T23:59:52.999 1312761592.9999 3521750392.9999 1691452826.9999 996796807.9999
Note the additional "1900-01-01T00:00:07.36" test of Rust/Julia may not quite work as expected: In 1.0.6+ the ".36" is eaten by the non-following-"." "ss" of the format string and sets the milliseconds correctly (code to do that added just now for this very task, that also copes with >3 decimal places). In 1.0.5 and earlier you could pass an extra format string to parse_date_string() that ends with ".ms", however ms expects to find an absolute/3-digit (integer) value, 000..999, so it would need to be "1900-01-01T00:00:07.360" otherwise NTP would come out as "7.036" and the others as "-~.964".
Python
#!/bin/python
#!/bin/python
""" " Time system conversions"""
from datetime import datetime, timezone
from math import trunc
from re import sub
UTC_EPOCH = 0
SECS_PER_DAY = 86400
UNIX_EPOCH = datetime(1970, 1, 1).toordinal() * SECS_PER_DAY
NTP_EPOCH = datetime(1900, 1, 1).toordinal() * SECS_PER_DAY
TAI_EPOCH = datetime(1958, 1, 1).toordinal() * SECS_PER_DAY
GPS_EPOCH = datetime(1980, 1, 6).toordinal() * SECS_PER_DAY
NTP_TO_LEAP_SECONDS = {
2272060800: 10,
2287785600: 11,
2303683200: 12,
2335219200: 13,
2366755200: 14,
2398291200: 15,
2429913600: 16,
2461449600: 17,
2492985600: 18,
2524521600: 19,
2571782400: 20,
2603318400: 21,
2634854400: 22,
2698012800: 23,
2776982400: 24,
2840140800: 25,
2871676800: 26,
2918937600: 27,
2950473600: 28,
2982009600: 29,
3029443200: 30,
3076704000: 31,
3124137600: 32,
3345062400: 33,
3439756800: 34,
3550089600: 35,
3644697600: 36,
3692217600: 37,
}
CHANGE_TIMES = sorted(NTP_TO_LEAP_SECONDS.keys())
# The 8 conversions below do not need leap second adjustments
def UTC_to_Unix(utc_string):
"""UTC to Unix timestamp"""
return datetime.fromisoformat(utc_string).toordinal() * SECS_PER_DAY - UNIX_EPOCH
def UTC_to_NTP(utc_string):
"""UTC to NTP timestamp"""
return UTC_to_Unix(utc_string) + UNIX_EPOCH - NTP_EPOCH
def NTP_to_Unix(ntp_secs):
"""NTP to Unix timestamp"""
return ntp_secs + NTP_EPOCH - UNIX_EPOCH
def Unix_to_NTP(unix_secs):
"""Unix to NTP timestamp"""
return unix_secs + UNIX_EPOCH - NTP_EPOCH
def Unix_to_UTC(unix_secs):
"""Unix to UTC timestamp, keeping UTC as timezone"""
round_secs = round(unix_secs) == unix_secs
format = "%Y-%m-%dT%H:%M:%S" if round_secs else "%Y-%m-%dT%H:%M:%S.%f"
s = datetime.fromtimestamp(unix_secs).astimezone(timezone.utc).strftime(format)
return s if round_secs else s[:-2]
def NTP_to_UTC(ntp_seconds):
"""NTP to UTC timestamp"""
return Unix_to_UTC(NTP_to_Unix(ntp_seconds))
def TAI_to_GPS(tai_secs):
"""TAI to GPS timestamp"""
return tai_secs + TAI_EPOCH - GPS_EPOCH - 19
def GPS_to_TAI(gps_secs):
"""GPS to TAI timestamp"""
return gps_secs + GPS_EPOCH - TAI_EPOCH + 19
# The next 12 conversions require leap-second adjustments. Since the leap second
# change timestamps are in NTC for TAI conversions, set up the conversions as
# between NTP and TAI first and let the other conversions depend on those two.
def NTP_to_TAI(ntp_secs):
"""Convert NTP to TAI, adding leap seconds"""
tai = ntp_secs + NTP_EPOCH - TAI_EPOCH
if ntp_secs < CHANGE_TIMES[0]:
return tai
if ntp_secs >= CHANGE_TIMES[-1]:
return tai + NTP_TO_LEAP_SECONDS[CHANGE_TIMES[-1]]
idx = next(i for i in range(len(CHANGE_TIMES)) if CHANGE_TIMES[i] > ntp_secs)
return tai + NTP_TO_LEAP_SECONDS[CHANGE_TIMES[idx - 1]]
def TAI_to_NTP(tai_secs):
"""
Convert TAI to NTP, subtracting leap seconds. Adjust by 1 if ntp time
subtraction causes time to cross a leap second update.
"""
ntp = tai_secs + TAI_EPOCH - NTP_EPOCH
if ntp < CHANGE_TIMES[0]:
return ntp
if ntp >= CHANGE_TIMES[-1]:
delta = NTP_TO_LEAP_SECONDS[CHANGE_TIMES[-1]]
else:
idx = next(i for i in range(len(CHANGE_TIMES)) if CHANGE_TIMES[i] > ntp) - 1
delta = NTP_TO_LEAP_SECONDS[CHANGE_TIMES[idx]]
if ntp - delta < CHANGE_TIMES[0]:
return CHANGE_TIMES[0] - 1 + ntp - trunc(ntp)
return ntp - delta + 1 if ntp - delta < CHANGE_TIMES[idx] else ntp - delta
def NTP_to_GPS(ntp_secs):
"""NTP to GPS timestamp"""
return TAI_to_GPS(NTP_to_TAI(ntp_secs))
def GPS_to_NTP(gps_secs):
"""GPS to NTP timestamp"""
return TAI_to_NTP(GPS_to_TAI(gps_secs))
def TAI_to_Unix(tai_secs):
"""TAI to Unix timestamp"""
return NTP_to_Unix(TAI_to_NTP(tai_secs))
def Unix_to_TAI(unix_secs):
"""Unix to GPS to TAI timestamp"""
return NTP_to_TAI(Unix_to_NTP(unix_secs))
def TAI_to_UTC(tai_secs):
"""TAI to UTC timestamp"""
return NTP_to_UTC(TAI_to_NTP(tai_secs))
def UTC_to_TAI(utc_string):
"""UTC to TAI timestamp"""
return NTP_to_TAI(UTC_to_NTP(utc_string))
def GPS_to_Unix(gps_secs):
"""GPS to Unix timestamp"""
return TAI_to_Unix(GPS_to_TAI(gps_secs))
def Unix_to_GPS(unix_secs):
"""Unix to GPS timestamp"""
return TAI_to_GPS(Unix_to_TAI(unix_secs))
def UTC_to_GPS(utc_string):
"""UTC to GPS timestamp"""
return TAI_to_GPS(UTC_to_TAI(utc_string))
def GPS_to_UTC(gps_secs):
"""GPS to UTC timestamp"""
return TAI_to_UTC(GPS_to_TAI(gps_secs))
if __name__ == "__main__":
print(
" UTC", " " * 20, "Unix", " " * 16, "NTP", " " * 16, "TAI", " " * 16, "GPS"
)
print("_" * 108)
for s in [
"0001-01-01T00:00:00",
"1900-01-01T00:00:00",
"1958-01-01T00:00:00",
"1970-01-01T00:00:00",
"1980-01-06T00:00:00",
"1989-12-31T00:00:00",
"1990-01-06T00:00:00",
"2025-06-01T00:00:00",
"2050-01-01T00:00:00",
]:
print(
f"{s:<28}{UTC_to_Unix(s):<21}{UTC_to_NTP(s):<21d}{UTC_to_TAI(s):<21d}{UTC_to_GPS(s):<21d}"
)
for un in [
1810753809.806,
154956295.688,
780673454.121,
]:
print(
f"{Unix_to_UTC(un):<28}{un:<21}{Unix_to_NTP(un):<21.14}{Unix_to_TAI(un):<21.14}{Unix_to_GPS(un):<21.14}"
)
for nt in [
2871676795,
2335219189,
3029443171,
3124137599,
3124137600,
]:
print(
f"{NTP_to_UTC(nt):28}{NTP_to_Unix(nt):<21d}{nt:<21d}{NTP_to_TAI(nt):<21d}{NTP_to_GPS(nt):<21d}"
)
for ta in [
996796823,
996796824,
996796825,
996796826,
1293840030,
1293840031,
1293840032,
1293840033,
]:
print(
f"{TAI_to_UTC(ta):28}{TAI_to_Unix(ta):<21}{TAI_to_NTP(ta):<21}{ta:<21}{TAI_to_GPS(ta):<21}"
)
for gp in [
996796804.250,
996796805.5,
996796806.750,
996796807.9999,
]:
print(
f"{GPS_to_UTC(gp):28}{GPS_to_Unix(gp):<21.14}{GPS_to_NTP(gp):<21.14}{GPS_to_TAI(gp):<21.14}{gp:<21.14}"
)
- Output:
UTC Unix NTP TAI GPS ____________________________________________________________________________________________________________ 0001-01-01T00:00:00 -62135596800 -59926608000 -61756905600 -62451561619 1900-01-01T00:00:00 -2208988800 0 -1830297600 -2524953619 1958-01-01T00:00:00 -378691200 1830297600 0 -694656019 1970-01-01T00:00:00 0 2208988800 378691200 -315964819 1980-01-06T00:00:00 315964800 2524953600 694656019 0 1989-12-31T00:00:00 631065600 2840054400 1009756824 315100805 1990-01-06T00:00:00 631584000 2840572800 1010275225 315619206 2025-06-01T00:00:00 1748736000 3957724800 2127427237 1432771218 2050-01-01T00:00:00 2524608000 4733596800 2903299237 2208643218 2027-05-19T19:10:09.8060 1810753809.806 4019742609.806 2189445046.806 1494789027.806 1974-11-29T11:24:55.6880 154956295.688 2363945095.688 533647508.688 -161008510.312 1994-09-27T13:44:14.1210 780673454.121 2989662254.121 1159364683.121 464708664.121 1990-12-31T23:59:55 662687995 2871676795 1041379220 346723201 1973-12-31T23:59:49 126230389 2335219189 504921601 -189734418 1995-12-31T23:59:31 820454371 3029443171 1199145600 504489581 1998-12-31T23:59:59 915148799 3124137599 1293840030 599184011 1999-01-01T00:00:00 915148800 3124137600 1293840032 599184013 1989-08-02T23:59:59 618105599 2827094399 996796823 302140804 1989-08-03T00:00:00 618105600 2827094400 996796824 302140805 1989-08-03T00:00:01 618105601 2827094401 996796825 302140806 1989-08-03T00:00:02 618105602 2827094402 996796826 302140807 1998-12-31T23:59:59 915148799 3124137599 1293840030 599184011 1999-01-01T00:00:00 915148800 3124137600 1293840031 599184012 1999-01-01T00:00:00 915148800 3124137600 1293840032 599184013 1999-01-01T00:00:01 915148801 3124137601 1293840033 599184014 2011-08-07T23:59:49.2500 1312761589.25 3521750389.25 1691452823.25 996796804.25 2011-08-07T23:59:50.5000 1312761590.5 3521750390.5 1691452824.5 996796805.5 2011-08-07T23:59:51.7500 1312761591.75 3521750391.75 1691452825.75 996796806.75 2011-08-07T23:59:52.9999 1312761592.9999 3521750392.9999 1691452826.9999 996796807.9999
Raku
When using the Instant class for leap second operations in Raku, the results may be version-dependent as it is preloaded with leap seconds adjustments at the then current point in time. Users should be aware of this and take it into consideration, as the knowledge of future leap seconds can vary between different Raku compiler versions.
# 20241023 Raku programming solution
my (\UNIX_EPOCH, \NTP_EPOCH, \TAI_EPOCH, \GPS_EPOCH) = map { DateTime.new: $_ },
< 1970-01-01 1900-01-01 1958-01-01 1980-01-06 >;
my %NTP_TO_LS = ( my @CHANGE_TIMES = <
2272060800 2287785600 2303683200 2335219200 2366755200 2398291200 2429913600
2461449600 2492985600 2524521600 2571782400 2603318400 2634854400 2698012800
2776982400 2840140800 2871676800 2918937600 2950473600 2982009600 3029443200
3076704000 3124137600 3345062400 3439756800 3550089600 3644697600 3692217600
> ) Z=> 10 .. * ;
# The conversions without leap second adjustments
my &utc2unix = -> $utcStr {
do given DateTime.new($utcStr).posix { * < 0 ?? $_ !! $_ - UNIX_EPOCH }
}
my &utc2ntp = -> $utcStr { utc2unix($utcStr) + UNIX_EPOCH - NTP_EPOCH }
my &ntp2unix = -> $ntpSecs { $ntpSecs + NTP_EPOCH - UNIX_EPOCH }
my &unix2ntp = -> $unixSecs { $unixSecs + UNIX_EPOCH - NTP_EPOCH }
my &unix2utc = -> $unixSecs { Instant.from-posix($unixSecs).DateTime }
my &ntp2utc = -> $ntpSecs { unix2utc(ntp2unix($ntpSecs)) }
my &tai2gps = -> $taiSecs { $taiSecs + TAI_EPOCH - GPS_EPOCH - 10 }
my &gps2tai = -> $gpsSecs { $gpsSecs + GPS_EPOCH - TAI_EPOCH + 10 }
# Conversions requiring leap-second adjustments
my &ntp2tai = sub ($ntpSecs) {
my $tai = $ntpSecs + NTP_EPOCH - TAI_EPOCH;
return $tai if $ntpSecs < @CHANGE_TIMES[0];
return $tai+%NTP_TO_LS{@CHANGE_TIMES[*-1]} if $ntpSecs >= @CHANGE_TIMES[*-1];
for 1 .. @CHANGE_TIMES.elems - 1 {
if $ntpSecs < @CHANGE_TIMES[$_] {
return $tai + %NTP_TO_LS{@CHANGE_TIMES[$_-1]}
}
}
}
my &tai2ntp = sub ($taiSecs) {
my $ntp = $taiSecs + TAI_EPOCH - NTP_EPOCH;
return $ntp if $ntp < @CHANGE_TIMES[0];
my ($idx, $delta) = (0, 0);
if $ntp >= @CHANGE_TIMES[*-1] {
$delta = %NTP_TO_LS{@CHANGE_TIMES[*-1]}
} else {
for 1 .. @CHANGE_TIMES.elems - 1 {
if $ntp < @CHANGE_TIMES[$_] {
$delta = %NTP_TO_LS{@CHANGE_TIMES[$_-1]};
$idx = $_;
last;
}
}
}
return do given $ntp - $delta {
when * < @CHANGE_TIMES[0] { @CHANGE_TIMES[0] - 1 + $ntp.fraction }
when * < @CHANGE_TIMES[$idx-1] { $_ + 1 }
default { $_ }
}
}
my &ntp2gps = -> $ntpSecs { tai2gps(ntp2tai($ntpSecs)) }
my &gps2ntp = -> $gpsSecs { tai2ntp(gps2tai($gpsSecs)) }
my &tai2unix = -> $taiSecs { ntp2unix(tai2ntp($taiSecs)) }
my &unix2tai = -> $unixSecs { ntp2tai(unix2ntp($unixSecs)) }
my &tai2utc = -> $taiSecs { ntp2utc(tai2ntp($taiSecs)) }
my &utc2tai = -> $utcStr { ntp2tai(utc2ntp($utcStr)) }
my &gps2unix = -> $gpsSecs { tai2unix(gps2tai($gpsSecs)) }
my &unix2gps = -> $unixSecs { tai2gps(unix2tai($unixSecs)) }
my &utc2gps = -> $utcStr { tai2gps(utc2tai($utcStr)) }
my &gps2utc = -> $gpsSecs { tai2utc(gps2tai($gpsSecs)) }
# Output formatting
say " UTC Unix NTP TAI GPS";
say '_' x 100;
my $fmt = "%-28s%18.4f%18.4f%18.4f%18.4f\n";
for < 0001-01-01T00:00:00 1900-01-01T00:00:00 1958-01-01T00:00:00
1970-01-01T00:00:00 1980-01-06T00:00:00 1989-12-31T00:00:00
1990-01-06T00:00:00 2025-06-01T00:00:00 2050-01-01T00:00:00 > {
printf $fmt, $_, .&utc2unix, .&utc2ntp, .&utc2tai, .&utc2gps
}
for < 1810753809.806 154956295.688 780673454.121 > {
printf $fmt, .&unix2utc, $_, .&unix2ntp, .&unix2tai, .&unix2gps
}
for < 2871676795 2335219189 3029443171 3124137599 3124137600 > {
printf $fmt, .&ntp2utc, .&ntp2unix, $_, .&ntp2tai, .&ntp2gps
}
for < 996796823 996796824 996796825 996796826
1293840030 1293840031 1293840032 1293840033 > {
printf $fmt, .&tai2utc, .&tai2unix, .&tai2ntp, $_, .&tai2gps
}
for < 996796804.250 996796805.5 996796806.750 996796807.9999 > {
printf $fmt, .&gps2utc, .&gps2unix, .&gps2ntp, .&gps2tai, $_
}
You may Attempt This Online!
- Output:
UTC Unix NTP TAI GPS ____________________________________________________________________________________________________ 0001-01-01T00:00:00 -62135596800.0000 -59926608000.0000 -61756905600.0000 -62451561619.0000 1900-01-01T00:00:00 -2208988800.0000 0.0000 -1830297600.0000 -2524953619.0000 1958-01-01T00:00:00 -378691200.0000 1830297600.0000 0.0000 -694656019.0000 1970-01-01T00:00:00 0.0000 2208988800.0000 378691200.0000 -315964819.0000 1980-01-06T00:00:00 315964800.0000 2524953600.0000 694656019.0000 0.0000 1989-12-31T00:00:00 631065600.0000 2840054400.0000 1009756824.0000 315100805.0000 1990-01-06T00:00:00 631584000.0000 2840572800.0000 1010275225.0000 315619206.0000 2025-06-01T00:00:00 1748736000.0000 3957724800.0000 2127427237.0000 1432771218.0000 2050-01-01T00:00:00 2524608000.0000 4733596800.0000 2903299237.0000 2208643218.0000 2027-05-19T19:10:09.806000Z 1810753809.8060 4019742609.8060 2189445046.8060 1494789027.8060 1974-11-29T11:24:55.688000Z 154956295.6880 2363945095.6880 533647508.6880 -161008510.3120 1994-09-27T13:44:14.121000Z 780673454.1210 2989662254.1210 1159364683.1210 464708664.1210 1990-12-31T23:59:55Z 662687995.0000 2871676795.0000 1041379220.0000 346723201.0000 1973-12-31T23:59:49Z 126230389.0000 2335219189.0000 504921601.0000 -189734418.0000 1995-12-31T23:59:31Z 820454371.0000 3029443171.0000 1199145600.0000 504489581.0000 1998-12-31T23:59:59Z 915148799.0000 3124137599.0000 1293840030.0000 599184011.0000 1999-01-01T00:00:00Z 915148800.0000 3124137600.0000 1293840032.0000 599184013.0000 1989-08-02T23:59:59Z 618105599.0000 2827094399.0000 996796823.0000 302140804.0000 1989-08-03T00:00:00Z 618105600.0000 2827094400.0000 996796824.0000 302140805.0000 1989-08-03T00:00:01Z 618105601.0000 2827094401.0000 996796825.0000 302140806.0000 1989-08-03T00:00:02Z 618105602.0000 2827094402.0000 996796826.0000 302140807.0000 1998-12-31T23:59:59Z 915148799.0000 3124137599.0000 1293840030.0000 599184011.0000 1999-01-01T00:00:00Z 915148800.0000 3124137600.0000 1293840031.0000 599184012.0000 1999-01-01T00:00:00Z 915148800.0000 3124137600.0000 1293840032.0000 599184013.0000 1999-01-01T00:00:01Z 915148801.0000 3124137601.0000 1293840033.0000 599184014.0000 2011-08-07T23:59:49.250000Z 1312761589.2500 3521750389.2500 1691452823.2500 996796804.2500 2011-08-07T23:59:50.500000Z 1312761590.5000 3521750390.5000 1691452824.5000 996796805.5000 2011-08-07T23:59:51.750000Z 1312761591.7500 3521750391.7500 1691452825.7500 996796806.7500 2011-08-07T23:59:52.999900Z 1312761592.9999 3521750392.9999 1691452826.9999 996796807.9999
Rust
using NanoDates
using Printf
# internal calculations are in nanoseconds for a NanoDate
const SECONDS_PER_DAY = 86400
const NANOSECONDS_PER_SECOND = Int128(1_000_000_000)
const NANOSECONDS_PER_MILLISECOND = Int128(1_000_000)
const UNIX_EPOCH_NANOSECONDS = 62135683200 * NANOSECONDS_PER_SECOND
const NTP_EPOCH_NANOSECONDS = 59926694400 * NANOSECONDS_PER_SECOND
const TAI_EPOCH_NANOSECONDS = 61756992000 * NANOSECONDS_PER_SECOND
const GPS_EPOCH_NANOSECONDS = 62451648000 * NANOSECONDS_PER_SECOND
const TAI_GPS_DIFFERENCE_NANOSECONDS = 19 * NANOSECONDS_PER_SECOND
const NTP_TO_LEAP_SECONDS = [
(2272060800, 10),
(2287785600, 11),
(2303683200, 12),
(2335219200, 13),
(2366755200, 14),
(2398291200, 15),
(2429913600, 16),
(2461449600, 17),
(2492985600, 18),
(2524521600, 19),
(2571782400, 20),
(2603318400, 21),
(2634854400, 22),
(2698012800, 23),
(2776982400, 24),
(2840140800, 25),
(2871676800, 26),
(2918937600, 27),
(2950473600, 28),
(2982009600, 29),
(3029443200, 30),
(3076704000, 31),
(3124137600, 32),
(3345062400, 33),
(3439756800, 34),
(3550089600, 35),
(3644697600, 36),
(3692217600, 37),
];
"""
Finds the index of the leap second that should be used based on the given NTP time in seconds.
"""
function leap_seconds_current_index(ntp_secs)
ntp_secs < NTP_TO_LEAP_SECONDS[begin][begin] && return nothing
for (i, pair) in enumerate(NTP_TO_LEAP_SECONDS)
ntp_secs < pair[begin] && return i - 1
end
return length(NTP_TO_LEAP_SECONDS) - 1
end
from_utc(utc_string::AbstractString) = NanoDate(utc_string)
from_unix(unix_secs::Integer) =
NanoDate(unix_secs * NANOSECONDS_PER_SECOND + UNIX_EPOCH_NANOSECONDS)
from_unix(unix_secs) =
NanoDate((Int128(round(unix_secs * NANOSECONDS_PER_SECOND)) + UNIX_EPOCH_NANOSECONDS))
from_ntp(ntp_secs::Integer) =
NanoDate(ntp_secs * NANOSECONDS_PER_SECOND + NTP_EPOCH_NANOSECONDS)
from_ntp(ntp_secs) =
NanoDate(round(ntp_secs * NANOSECONDS_PER_SECOND) + NTP_EPOCH_NANOSECONDS)
function from_tai_nsec(tai_nsec::Int128)
ntp_secs =
(tai_nsec + TAI_EPOCH_NANOSECONDS - NTP_EPOCH_NANOSECONDS) ÷ NANOSECONDS_PER_SECOND
nsecs = tai_nsec % NANOSECONDS_PER_SECOND
idx = leap_seconds_current_index(ntp_secs)
isnothing(idx) &&
return NanoDate(ntp_secs * NANOSECONDS_PER_SECOND + nsecs + NTP_EPOCH_NANOSECONDS)
delta = NTP_TO_LEAP_SECONDS[idx][end]
return NanoDate(
(
ntp_secs - delta < NTP_TO_LEAP_SECONDS[begin][begin] ?
NANOSECONDS_PER_SECOND * NTP_TO_LEAP_SECONDS[begin][begin] -
NANOSECONDS_PER_SECOND + (tai_nsec % NANOSECONDS_PER_SECOND) :
ntp_secs - delta < NTP_TO_LEAP_SECONDS[idx][begin] ?
(ntp_secs - delta) * NANOSECONDS_PER_SECOND + NANOSECONDS_PER_SECOND + nsecs :
(ntp_secs - delta) * NANOSECONDS_PER_SECOND + nsecs
) + NTP_EPOCH_NANOSECONDS,
)
end
from_tai(tai_secs::Integer) = from_tai_nsec(tai_secs * NANOSECONDS_PER_SECOND)
from_tai(tai_secs) = from_tai_nsec(Int128(round(tai_secs * NANOSECONDS_PER_SECOND)))
function from_gps_nsec(gps_nsecs::Int128)
return from_tai_nsec(
gps_nsecs + GPS_EPOCH_NANOSECONDS - TAI_EPOCH_NANOSECONDS +
TAI_GPS_DIFFERENCE_NANOSECONDS,
)
end
from_gps(gps_secs::Integer) = from_gps_nsec(gps_secs * NANOSECONDS_PER_SECOND)
from_gps(gps_secs) = from_gps_nsec(Int128(round(gps_secs * NANOSECONDS_PER_SECOND)))
function to_utc(nd::NanoDate; precision = 3)
fmt = Printf.Format("%07.$(precision)f")
s = string(nd)
return replace(
s,
r"(\d\d\.\d+)" => (ns) -> Printf.format(fmt, parse(Float64, ns))
)
end
function to_unix(nd::NanoDate)
return NanoDates.value(nd) % NANOSECONDS_PER_SECOND == 0 ?
(NanoDates.value(nd) - UNIX_EPOCH_NANOSECONDS) ÷ NANOSECONDS_PER_SECOND :
(NanoDates.value(nd) - UNIX_EPOCH_NANOSECONDS) / NANOSECONDS_PER_SECOND
end
function to_ntp(nd::NanoDate)
return NanoDates.value(nd) % NANOSECONDS_PER_SECOND == 0 ?
(NanoDates.value(nd) - NTP_EPOCH_NANOSECONDS) ÷ NANOSECONDS_PER_SECOND :
(NanoDates.value(nd) - NTP_EPOCH_NANOSECONDS) / NANOSECONDS_PER_SECOND
end
function to_tai_nsec(nd::NanoDate)
ntp_sec = (NanoDates.value(nd) - NTP_EPOCH_NANOSECONDS) / NANOSECONDS_PER_SECOND
idx = leap_seconds_current_index(ntp_sec)
return idx == nothing ? NanoDates.value(nd) - TAI_EPOCH_NANOSECONDS :
NanoDates.value(nd) - TAI_EPOCH_NANOSECONDS +
NANOSECONDS_PER_SECOND * NTP_TO_LEAP_SECONDS[idx][end]
end
function to_tai(nd::NanoDate)
tai_nsec = to_tai_nsec(nd)
return tai_nsec % NANOSECONDS_PER_SECOND == 0 ?
tai_nsec ÷ NANOSECONDS_PER_SECOND :
tai_nsec / NANOSECONDS_PER_SECOND
end
function to_gps(nd::NanoDate)
gps_nsec =
to_tai_nsec(nd) + TAI_EPOCH_NANOSECONDS - GPS_EPOCH_NANOSECONDS -
TAI_GPS_DIFFERENCE_NANOSECONDS
return gps_nsec % NANOSECONDS_PER_SECOND == 0 ?
gps_nsec ÷ NANOSECONDS_PER_SECOND :
gps_nsec / NANOSECONDS_PER_SECOND
end
"""
print_stamps(nd::NanoDate, s, str_idx; precision = "3", sfmt = Printf.Format("%.3f"))
Prints formatted timestamp information based on the given `NanoDate` object.
# Arguments
- `nd::NanoDate`: The date and time to be formatted and printed.
- `s`: A string to be printed when `str_idx` matches the index.
- `str_idx`: An integer indicating which string index to print.
- `precision`: A string specifying the desired precision for the UTC representation. Defaults to "3".
- `sfmt`: A `Printf.Format` object specifying the format for floating-point numbers. Defaults to `Printf.Format("%.3f")`.
# Description
The function prints out various time representations of the given `NanoDate` object `nd`. It uses `str_idx` to determine which string to print, and formats the output based on whether the time is an integer second or not. The representations include UTC, Unix, NTP, TAI, and GPS time formats.
"""
function print_stamps(
nd::NanoDate,
s,
str_idx;
precision = "3",
sfmt = Printf.Format("%.3f"),
)
int = NanoDates.value(nd) % NANOSECONDS_PER_SECOND == 0
print(rpad(str_idx == 1 ? s : to_utc(nd; precision), 26))
print(rpad(str_idx == 2 ? s : int ? to_unix(nd) : Printf.format(sfmt, to_unix(nd)), 17))
print(rpad(str_idx == 3 ? s : int ? to_ntp(nd) : Printf.format(sfmt, to_ntp(nd)), 17))
print(rpad(str_idx == 4 ? s : int ? to_tai(nd) : Printf.format(sfmt, to_tai(nd)), 17))
println(rpad(str_idx == 5 ? s : int ? to_gps(nd) : Printf.format(sfmt, to_gps(nd)), 17))
end
"""
timestamp_conversions()
Converts and prints timestamp information across various time standards including UTC, Unix, NTP, TAI, and GPS.
The function performs the following steps:
- Prints a header with the names of the time standards.
- Iterates over a list of predefined UTC timestamps, converting each to a normalized date and time, and prints the results.
- Converts and prints a specific floating-point UTC timestamp.
- Converts and prints a list of Unix timestamps.
- Converts and prints a list of NTP timestamps.
- Converts and prints a list of TAI timestamps.
- Converts and prints a list of GPS timestamps with specified precision.
Each conversion relies on helper functions (`from_utc`, `from_unix`, `from_ntp`, `from_tai`, `from_gps`) to parse and convert the timestamps, and `print_stamps` to format and display the results.
"""
function timestamp_conversions()
@printf("%17s%13s%22s%16s%17s\n", "UTC", "Unix", "NTP", "TAI", "GPS")
println("_"^96)
for s in [
"0001-01-01T00:00:00",
"1900-01-01T00:00:00",
"1958-01-01T00:00:00",
"1970-01-01T00:00:00",
"1980-01-06T00:00:00",
"1989-12-31T00:00:00",
"1990-01-01T00:00:00",
"2025-06-01T00:00:00",
"2050-01-01T00:00:00",
]
nd = from_utc(s)
isnothing(nd) && throw("Parse error parsing $s")
print_stamps(nd, s, 1)
end
floating = "1900-01-01T00:00:07.36"
nd = from_utc(floating)
isnothing(nd) && throw("Parse error parsing $floating")
print_stamps(nd, floating, 1)
for unix in [1810753809.806, 154956295.688, 780673454.121]
nd = from_unix(unix)
print_stamps(nd, @sprintf("%.3f", unix), 2)
end
for ntp in [2871676795, 2335219189, 3029443171]
nd = from_ntp(ntp)
print_stamps(nd, string(ntp), 3)
end
for tai in [996796823, 996796824, 996796825, 996796826, 1293840030, 1293840031, 1293840032, 1293840033,]
nd = from_tai(tai)
print_stamps(nd, string(tai), 4)
end
for gps in [996796804.250, 996796805.5, 996796806.750, 996796807.9999]
nd = from_gps(gps)
print_stamps(
nd,
@sprintf("%.4f", gps),
5,
precision = "4",
sfmt = Printf.Format("%.4f"),
)
end
end
timestamp_conversions()
- Output:
UTC Unix NTP TAI GPS ________________________________________________________________________________________________ 0001-01-01T00:00:00 -62135596800 -59926608000 -61756905600 -62451561619 1900-01-01T00:00:00 -2208988800 0 -1830297600 -2524953619 1958-01-01T00:00:00 -378691200 1830297600 0 -694656019 1970-01-01T00:00:00 0 2208988800 378691200 -315964819 1980-01-06T00:00:00 315964800 2524953600 694656019 0 1989-12-31T00:00:00 631065600 2840054400 1009756824 315100805 1990-01-01T00:00:00 631152000 2840140800 1009843225 315187206 2025-06-01T00:00:00 1748736000 3957724800 2127427236 1432771217 2050-01-01T00:00:00 2524608000 4733596800 2903299236 2208643217 1900-01-01T00:00:07.36 -2208988792.640 7.360 -1830297592.640 -2524953611.640 2027-05-19T19:10:009.806 1810753809.806 4019742609.806 2189445045.806 1494789026.806 1974-11-29T11:24:055.688 154956295.688 2363945095.688 533647508.688 -161008510.312 1994-09-27T13:44:014.121 780673454.121 2989662254.121 1159364683.121 464708664.121 1990-12-31T23:59:55 662687995 2871676795 1041379220 346723201 1973-12-31T23:59:49 126230389 2335219189 504921601 -189734418 1995-12-31T23:59:31 820454371 3029443171 1199145600 504489581 1989-08-02T23:59:59 618105599 2827094399 996796823 302140804 1989-08-03T00:00:00 618105600 2827094400 996796824 302140805 1989-08-03T00:00:01 618105601 2827094401 996796825 302140806 1989-08-03T00:00:02 618105602 2827094402 996796826 302140807 1998-12-31T23:59:59 915148799 3124137599 1293840030 599184011 1999-01-01T00:00:00 915148800 3124137600 1293840031 599184013 1999-01-01T00:00:00 915148800 3124137600 1293840032 599184013 1999-01-01T00:00:01 915148801 3124137601 1293840033 599184014 2011-08-07T23:59:49.2500 1312761589.2500 3521750389.2500 1691452823.2500 996796804.2500 2011-08-07T23:59:50.5000 1312761590.5000 3521750390.5000 1691452824.5000 996796805.5000 2011-08-07T23:59:51.7500 1312761591.7500 3521750391.7500 1691452825.7500 996796806.7500 2011-08-07T23:59:52.9999 1312761592.9999 3521750392.9999 1691452826.9999 996796807.9999
Wren
import "./date" for Date
import "./fmt" for Fmt
var UNIX_EPOCH = Date.unixEpoch.number / 1000
var NTP_EPOCH = Date.new(1900, 1, 1).number / 1000
var TAI_EPOCH = Date.new(1958, 1, 1).number / 1000
var GPS_EPOCH = Date.new(1980, 1, 6).number / 1000
var NTP_TO_LS = {
2272060800: 10,
2287785600: 11,
2303683200: 12,
2335219200: 13,
2366755200: 14,
2398291200: 15,
2429913600: 16,
2461449600: 17,
2492985600: 18,
2524521600: 19,
2571782400: 20,
2603318400: 21,
2634854400: 22,
2698012800: 23,
2776982400: 24,
2840140800: 25,
2871676800: 26,
2918937600: 27,
2950473600: 28,
2982009600: 29,
3029443200: 30,
3076704000: 31,
3124137600: 32,
3345062400: 33,
3439756800: 34,
3550089600: 35,
3644697600: 36,
3692217600: 37
}
var CHANGE_TIMES = NTP_TO_LS.keys.toList.sort()
// The 8 conversions below do not need leap second adjustments.
var utc2unix = Fn.new { |utcStr| Date.parse(utcStr).number/1000 - UNIX_EPOCH }
var utc2ntp = Fn.new { |utcStr| utc2unix.call(utcStr) + UNIX_EPOCH - NTP_EPOCH }
var ntp2unix = Fn.new { |ntpSecs| ntpSecs + NTP_EPOCH - UNIX_EPOCH }
var unix2ntp = Fn.new { |unixSecs| unixSecs + UNIX_EPOCH - NTP_EPOCH }
var unix2utc = Fn.new { |unixSecs| Date.fromNumber((unixSecs + UNIX_EPOCH) * 1000).toString }
var ntp2utc = Fn.new { |ntpSecs| unix2utc.call(ntp2unix.call(ntpSecs)) }
var tai2gps = Fn.new { |taiSecs| taiSecs + TAI_EPOCH - GPS_EPOCH - 19 }
var gps2tai = Fn.new { |gpsSecs| gpsSecs + GPS_EPOCH - TAI_EPOCH + 19 }
/*
The next 12 conversions require leap-second adjustments. Since the leap second
change timestamps are in NTC for TAI conversions, set up the conversions as
between NTP and TAI first and let the other conversions depend on those two.
*/
// Convert NTP to TAI, adding leap seconds.
var ntp2tai = Fn.new { |ntpSecs|
var tai = ntpSecs + NTP_EPOCH - TAI_EPOCH
if (ntpSecs < CHANGE_TIMES[0]) return tai
if (ntpSecs >= CHANGE_TIMES[-1]) return tai + NTP_TO_LS[CHANGE_TIMES[-1]]
for (i in 1...CHANGE_TIMES.count) {
if (ntpSecs < CHANGE_TIMES[i]) return tai + NTP_TO_LS[CHANGE_TIMES[i-1]]
}
}
// Convert TAI to NTP, subtracting leap seconds. Adjust by 1 if ntp time
// subtraction causes time to cross a leap second update.
var tai2ntp = Fn.new { |taiSecs|
var ntp = taiSecs + TAI_EPOCH - NTP_EPOCH
if (ntp < CHANGE_TIMES[0]) return ntp
var idx = 0
var delta = 0
if (ntp >= CHANGE_TIMES[-1]) {
delta = NTP_TO_LS[CHANGE_TIMES[-1]]
} else {
for (i in 1...CHANGE_TIMES.count) {
if (ntp < CHANGE_TIMES[i]) {
delta = NTP_TO_LS[CHANGE_TIMES[i-1]]
idx = i
break
}
}
}
if (ntp - delta < CHANGE_TIMES[0]) return CHANGE_TIMES[0] - 1 + ntp.fraction
if (ntp - delta < CHANGE_TIMES[idx-1]) return ntp - delta + 1
return ntp - delta
}
var ntp2gps = Fn.new { |ntpSecs| tai2gps.call(ntp2tai.call(ntpSecs)) }
var gps2ntp = Fn.new { |gpsSecs| tai2ntp.call(gps2tai.call(gpsSecs)) }
var tai2unix = Fn.new { |taiSecs| ntp2unix.call(tai2ntp.call(taiSecs)) }
var unix2tai = Fn.new { |unixSecs| ntp2tai.call(unix2ntp.call(unixSecs)) }
var tai2utc = Fn.new { |taiSecs| ntp2utc.call(tai2ntp.call(taiSecs)) }
var utc2tai = Fn.new { |utcStr| ntp2tai.call(utc2ntp.call(utcStr)) }
var gps2unix = Fn.new { |gpsSecs| tai2unix.call(gps2tai.call(gpsSecs)) }
var unix2gps = Fn.new { |unixSecs| tai2gps.call(unix2tai.call(unixSecs)) }
var utc2gps = Fn.new { |utcStr| tai2gps.call(utc2tai.call(utcStr)) }
var gps2utc = Fn.new { |gpsSecs| tai2utc.call(gps2tai.call(gpsSecs)) }
Fmt.print(" UTC Unix NTP TAI GPS")
Fmt.print("_" * 95)
var f = "$-23s $17.4h $17.4h $17.4h $17.4h"
for (s in ["0001-01-01T00:00:00", "1900-01-01T00:00:00", "1958-01-01T00:00:00",
"1970-01-01T00:00:00", "1980-01-06T00:00:00", "1989-12-31T00:00:00",
"1990-01-06T00:00:00", "2025-06-01T00:00:00", "2050-01-01T00:00:00"]) {
Fmt.print(f, s, utc2unix.call(s), utc2ntp.call(s), utc2tai.call(s), utc2gps.call(s))
}
for (un in [1810753809.806, 154956295.688, 780673454.121]) {
var dt = unix2utc.call(un).replace(" ", "T")
Fmt.print(f, dt, un, unix2ntp.call(un), unix2tai.call(un), unix2gps.call(un))
}
for (nt in [2871676795, 2335219189, 3029443171, 3124137599, 3124137600]) {
var dt = ntp2utc.call(nt).replace(" ", "T")
Fmt.print(f, dt, ntp2unix.call(nt), nt, ntp2tai.call(nt), ntp2gps.call(nt))
}
for (ta in [996796823, 996796824, 996796825, 996796826, 1293840030,
1293840031, 1293840032, 1293840033]) {
var dt = tai2utc.call(ta).replace(" ", "T")
Fmt.print(f, dt, tai2unix.call(ta), tai2ntp.call(ta), ta, tai2gps.call(ta))
}
for (gp in [996796804.250, 996796805.5, 996796806.750, 996796807.9999] ) {
var dt = gps2utc.call(gp).replace(" ", "T")
Fmt.print(f, dt, gps2unix.call(gp), gps2ntp.call(gp), gps2tai.call(gp), gp)
}
- Output:
UTC Unix NTP TAI GPS _______________________________________________________________________________________________ 0001-01-01T00:00:00 -62135596800 -59926608000 -61756905600 -62451561619 1900-01-01T00:00:00 -2208988800 0 -1830297600 -2524953619 1958-01-01T00:00:00 -378691200 1830297600 0 -694656019 1970-01-01T00:00:00 0 2208988800 378691200 -315964819 1980-01-06T00:00:00 315964800 2524953600 694656019 0 1989-12-31T00:00:00 631065600 2840054400 1009756824 315100805 1990-01-06T00:00:00 631584000 2840572800 1010275225 315619206 2025-06-01T00:00:00 1748736000 3957724800 2127427237 1432771218 2050-01-01T00:00:00 2524608000 4733596800 2903299237 2208643218 2027-05-19T19:10:09.806 1810753809.806 4019742609.806 2189445046.806 1494789027.806 1974-11-29T11:24:55.688 154956295.688 2363945095.688 533647508.688 -161008510.312 1994-09-27T13:44:14.121 780673454.121 2989662254.121 1159364683.121 464708664.121 1990-12-31T23:59:55.000 662687995 2871676795 1041379220 346723201 1973-12-31T23:59:49.000 126230389 2335219189 504921601 -189734418 1995-12-31T23:59:31.000 820454371 3029443171 1199145600 504489581 1998-12-31T23:59:59.000 915148799 3124137599 1293840030 599184011 1999-01-01T00:00:00.000 915148800 3124137600 1293840032 599184013 1989-08-02T23:59:59.000 618105599 2827094399 996796823 302140804 1989-08-03T00:00:00.000 618105600 2827094400 996796824 302140805 1989-08-03T00:00:01.000 618105601 2827094401 996796825 302140806 1989-08-03T00:00:02.000 618105602 2827094402 996796826 302140807 1998-12-31T23:59:59.000 915148799 3124137599 1293840030 599184011 1999-01-01T00:00:00.000 915148800 3124137600 1293840031 599184012 1999-01-01T00:00:00.000 915148800 3124137600 1293840032 599184013 1999-01-01T00:00:01.000 915148801 3124137601 1293840033 599184014 2011-08-07T23:59:49.250 1312761589.25 3521750389.25 1691452823.25 996796804.25 2011-08-07T23:59:50.500 1312761590.5 3521750390.5 1691452824.5 996796805.5 2011-08-07T23:59:51.750 1312761591.75 3521750391.75 1691452825.75 996796806.75 2011-08-07T23:59:52.999 1312761592.9999 3521750392.9999 1691452826.9999 996796807.9999