1. Introduction

Dates and times are critical to the stability of computer systems. Thus, producing and processing them properly can be vital for avoiding incorrect settings that may lead to unexpected behavior. To avoid such cases, there are widely-adopted standards for the formats of dates and times.

In this tutorial, we’ll discuss the RFC 3339 date specification and how we can produce a date-time of this format in Linux. First, we look at the standard and its basic definitions. After that, we go over ways to format timestamps according to them.

We tested the code in this tutorial on Debian 11 (Bullseye) with GNU Bash 5.1.4. It should work in most POSIX-compliant environments unless otherwise specified.

2. Internet Date and Time

As one of the main drivers for standards, the Internet is a primary target of RFC 3339 – Date and Time on the Internet: Timestamps.

In short, the document defines a date-time format based on ISO-8601. Further, the standard includes all necessary information without overcomplicating the output:

+------------------------------------------------------------------------------+
|   Component   |   Format    |                     Values                     |
+---------------+-------------+------------------------------------------------+
| date-fullyear | 4DIGIT      | NNNN                                           |
| date-month    | 2DIGIT      | 01-12                                          |
| date-mday     | 2DIGIT      | 01-28, 01-29, 01-30, 01-31 based on month/year |
| time-hour     | 2DIGIT      | 00-23                                          |
| time-minute   | 2DIGIT      | 00-59                                          |
| time-second   | 2DIGIT      | 00-58, 00-59, 00-60 based on leap second rules |
| time-secfrac  | "." 1*DIGIT | >0                                             |
+------------------------------------------------------------------------------+

Based on these components, RFC-3339 defines the full format options:

+----------------------------------------------------------------------------+
|    Section     |                          Content                          |
+----------------+-----------------------------------------------------------+
| time-numoffset |  ("+" / "-") time-hour ":" time-minute                    |
| time-offset    |  "Z" / time-numoffset                                     |
| partial-time   |  time-hour ":" time-minute ":" time-second [time-secfrac] |
| full-date      |  date-fullyear "-" date-month "-" date-mday               |
| full-time      |  partial-time time-offset                                 |
| date-time      |  full-date "T" full-time                                  |
+----------------------------------------------------------------------------+

Often pronounced Zulu from the International Civil Aviation Organization (ICAO) alphabet, Z stands for Coordinated Universal Time (UTC) 00:00, while T is simply a separator like . period, : colon, and dash. Further, T can simply be a space, depending on the implementation.

So, we can now construct a couple of examples:

  • 1966-03-03T00:06:56.52Z – year 1966, March 3rd, time 00:06:56, at 520 milliseconds in the UTC timezone
  • 1999-01-01T10:00:10+02:00 – year 1999, January 1st, time 10:00:10 in the UTC+02:00 timezone

Now, let’s turn to generating such dates in Linux.

3. Output RFC-3339 Dates

Due to its widespread adoption, many tools support the RFC-3339 date-time standard and can produce timestamps based on it.

Let’s see how.

3.1. Using date

The ubiquitous date command from the GNU coreutils package can output timestamps in custom and preset formats, including ISO-8601 and RFC-3339.

In fact, the –rfc-3339 option has three possible values:

  • date – output only full-date
  • seconds – output date-time with second precision
  • ns – output date-time with nanosecond precision

Let’s check the output of each:

$ date --rfc-3339=date
2023-05-25
$ date --rfc-3339=seconds
2023-05-25 00:10:01-02:00
$ date --rfc-3339=ns
2023-05-25 00:10:01.100666177-02:00

Moreover, we can get the T separator via the –iso-8601 or -I option with the same seconds value:

$ date --iso-8601=seconds
2023-05-25T00:10:01-02:00

Critically, using ns with –iso-8601 replaces the . period separator with a , comma.

Of course, we can always recreate the output via the format sequences, including the non-standard %#N for subsecond granularity and %:z to insert a colon:

$ date +%Y-%m-%dT%T.%9N%:z
2023-05-25T00:10:01-02:00

In fact, this can help us when using another tool for our needs.

3.2. Using Perl

As usual, the perl interpreter offers a solution to suit our needs:

$ perl -MPOSIX -e '$t = strftime "%Y-%m-%dT%T%z", localtime; $t =~ s/..$/:$&/; print $t;'
2023-05-25T00:10:01-02:00

Here, we [-e]xecute a Perl one-liner with the POSIX [-M]odule. The latter offers a way to execute the strftime() system call with our format string.

The only difference is that we use %z instead of the non-standard %:z, so we add the : colon via a regular expression before printing out the timestamp.

3.3. Using strftime()

In general, the strftime() system call is available in many tools and accepts standard format strings, so we can just create the format we need:

%Y-%m-%dT%T%z

However, since strftime() follows POSIX, we can’t achieve subsecond granularity. Still, that part of RFC-3339 is optional.

4. Summary

In this article, we discussed the RFC-3339 standard and ways to produce dates based on it.

In conclusion, although we looked at the date and perl commands, many tools and languages support the strftime() system call, which can produce date-time stamps according to any format as long as we don’t need subsecond granularity.