Jump to content

Time conventions and conversions

From Rosetta Code
Time conventions and conversions is a draft programming task. It is not yet considered ready to be promoted as a complete task, for reasons that should be found in its talk page.

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

Translation of: Python
#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

Translation of: Rust
""" 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.

Translation of: Wren
# 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

Translation of: Python
Library: Wren-date
Library: Wren-fmt
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
Cookies help us deliver our services. By using our services, you agree to our use of cookies.