user
## 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
assistant
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
```