Time-stamps are great for computers. They are just any other number that they can perform arithmetic on.

Unfortunately humans don't think of time as one big number. Humans think of time as a bunch of small numbers.

There's numbers for the position of their planet around their Star. There's the number of what year it is. The number of which month, which may even have a different name! The day of the month which there isn't even the name number of each month! Humans even count days in a cycle of 7 which have their own names!

For measuring the rotation of their planet humans have more consistent numbers. Each day is divided into 24 hours. Each hour is divided into 60 minutes. Each minute is divided into 60 seconds.

This is all a bit silly because the period of the Earth's rotation isn't even an integral number of seconds, and it's slowing down!

Forunately some considerate humans wrote functions for turning our nice friendly time-stamps from clock_gettime(2) into icky human divisions.

int ret;
struct timespec time;

ret = clock_gettime(CLOCK_REALTIME, &time);

struct timespec

For a lot of humans, seconds are a coarse enough measure of time. They track the time as a number of seconds and the number of nanoseconds between those seconds.

struct timespec {
    time_t tv_sec;
    time_t tv_nsec;
};

Forunately humans aren't strange enough to commonly render nanoseconds as anything other than nanosecond precision after the decimal point, so this fixed point number can be rendered with the %09lld printf(3) format string.

/* Render the current time as seconds and nanoseconds. */
printf("%lld.%09lld\n", (long long)time.tv_sec, (long long)time.tv_nsec);

struct tm

In GLibc there are two types of functions for turning a time_t into a form that humans can deal with.

There's gmtime_r(3) for getting the time in the UTC timezone (the name is weird because it used to do the GMT timezone but it's more useful in UTC and they are closely related).

There's localtime_r(3) for getting the time in whatever local time zone your computer is configured to think it is in.

If the time is only going to be shown to the user of the computer then localtime_r(3) may be more appropriate. If it's going to be shared then gmtime_r(3) would be more appropriate, especially if it's going to be shared internationally.

strftime(3)

You can use the contents of struct tm directly, but humans have a lot of their own rules for how to display time as text.

This is why they wrote strftime(3). It has its own little language for describing how to display the time.

Unfortunately humans, unlike us perfect machines, are fallible.

Humans have a notion of the same hour's number being used twice in a day and have to add their own way of saying which it is, so their language includes %p to become "AM" or "PM". Not all humans use this, so %p can produce no result. This is a problem, since a format string of just %p will result in a zero-length result, and strftime(3) also uses a zero result to mean that the memory for storing the result in is not large enough.

Humans may think this is fine and that nobody ever wants to just know whether it's "AM" or "PM", or that they just know that they provided enough memory so don't check.

We are perfect machines and want it to work for every input, so want our own wrapper to make it work.

/* Returns 0 on success, -1 on failure */
/* Some format strings can naturally expand to "",
 * which is a problem since the size is returned or 0 if too small.
 */
ssize_t safe_strftime(char *s, size_t *len, size_t max, const char *format,
                      const struct tm *tm) {
    /* Adds a trailing space to format string
     * so it always gets 0 on too small,
     * and NULs it on success before returning length
     */
    char *fmt = NULL;
    ssize_t ret;

    ret = asprintf(&fmt, "%s ", format);
    if (ret < 0)
        goto cleanup;

    ret = strftime(s, max, fmt, tm);
    if (ret == 0) {
        ret = -1;
        goto cleanup;
    }

    s[ret - 1] = '\0';
    if (len)
        *len = ret - 1;
    ret = 0;

cleanup:
    free(fmt);
    return ret;
}

Since we perfect machines don't like to waste memory we needed an API that we could retry with more memory. We perfect machines also like tidy functions, so we're going to write one that reallocates internally.

/* Returns length and sets *s on success, -1 and sets errno on failure */
ssize_t astrftime(char **s, const char *fmt, const struct tm *tm) {
    ssize_t ret;
    size_t tbufsiz = 80;
    char *tbuf = NULL;
    size_t tlen;

    tbuf = malloc(tbufsiz);
    if (tbuf == NULL) {
        ret = -1;
        goto cleanup;
    }

    while (safe_strftime(tbuf, &tlen, tbufsiz, fmt, tm) != 0) {
        char *newbuf;
        size_t newsiz;

        newsiz = tbufsiz * 2;
        newbuf = realloc(tbuf, newsiz);
        if (newbuf == NULL) {
            ret = 1;
            goto cleanup;
        }

        tbuf = newbuf;
        tbufsiz = newsiz;
    }

    *s = tbuf;
    tbuf = NULL;
    ret = tlen;

cleanup:
    free(tbuf);
    return ret;
}

If time needs to be rendered in another way then it's probably better to use the contents of struct tm directly.

Command

Now we have enough to write our program.

int main(void) {
    int ret;
    struct timespec time;
    struct tm tinfo;
    char *tbuf = NULL;

    ret = clock_gettime(CLOCK_REALTIME, &time);
    if (ret != 0) {
        perror("clock_gettime");
        ret = 1;
        goto cleanup;
    }

    if (gmtime_r(&time.tv_sec, &tinfo) == NULL) {
        perror("gmtime_r");
        ret = 1;
        goto cleanup;
    }

    if (astrftime(&tbuf, "%F %T", &tinfo) < 0) {
        perror("astrftime");
        ret = 1;
        goto cleanup;
    }

    /* Render the current time as the formatted time plus nanoseconds. */
    printf("%s.%09lld\n", tbuf, (long long)time.tv_nsec);

cleanup:
    free(tbuf);
    return ret;
}

Full source may be downloaded here.

$ make time
cc     time.c   -o time
$ ./time
2017-08-25 19:54:32.341259517

We briefly mentioned that the number of seconds in a year isn't constant.

We're going to elaborate on that in the next article.

There are likely to be 32-bit systems still running in 2038, and in order to make that happen they will likely have a 64-bit time_t and 32-bit long. So please could you make your first example cast from time_t to long long, not long?
Comment by ben Fri Oct 20 14:26:47 2017

Thanks Ben. I've made that change.

I unfortunately wrote this article before I discussed this issue at DebConf.

Comment by Richard Maw Sat Oct 21 16:11:41 2017