Prev | Contents | Next

38 Date and Time Functionality

“Time is an illusion. Lunchtime doubly so.”
—Ford Prefect, The Hitchhikers Guide to the Galaxy

This isn’t too complex, but it can be a little intimidating at first, both with the different types available and the way we can convert between them.

Mix in GMT (UTC) and local time and we have all the Usual Fun™ one gets with times and dates.

And of course never forget the golden rule of dates and times: Never attempt to write your own date and time functionality. Only use what the library gives you.

Time is too complex for mere mortal programmers to handle correctly. Seriously, we all owe a point to everyone who worked on any date and time library, so put that in your budget.

38.1 Quick Terminology and Information

Just a couple quick terms in case you don’t have them down.

As a general rule, if you are describing an event that happens one time, like a log entry, or a rocket launch, or when pointers finally clicked for you, use UTC.

On the other hand, if it’s something that happens the same time in every time zone, like New Year’s Eve or dinner time, use local time.

Since a lot of languages are only good at converting between UTC and local time, you can cause yourself a lot of pain by choosing to store your dates in the wrong form. (Ask me how I know.)

38.2 Date Types

There are two191 main types in C when it comes to dates: time_t and struct tm.

The spec doesn’t actually say much about them:

On a lot of systems, time_t represents the number of seconds since Epoch192. Epoch is in some ways the start of time from the computer’s perspective, which is commonly January 1, 1970 UTC. time_t can go negative to represent times before Epoch. Windows behaves the same way as Unix from what I can tell.

And what’s in a struct tm? The following fields:

struct tm {
    int tm_sec;    // seconds after the minute -- [0, 60]
    int tm_min;    // minutes after the hour -- [0, 59]
    int tm_hour;   // hours since midnight -- [0, 23]
    int tm_mday;   // day of the month -- [1, 31]
    int tm_mon;    // months since January -- [0, 11]
    int tm_year;   // years since 1900
    int tm_wday;   // days since Sunday -- [0, 6]
    int tm_yday;   // days since January 1 -- [0, 365]
    int tm_isdst;  // Daylight Saving Time flag
};

Note that everything is zero-based except the day of the month.

It’s important to know that you can put any values in these types you want. There are functions to help get the time now, but the types hold a time, not the time.

So the question becomes: “How do you initialize data of these types, and how do you convert between them?”

38.3 Initialization and Conversion Between Types

First, you can get the current time and store it in a time_t with the time() function.

time_t now;  // Variable to hold the time now

now = time(NULL);  // You can get it like this...

time(&now);        // ...or this. Same as the previous line.

Great! You have a variable that gets you the time now.

Amusingly, there’s only one portable way to print out what’s in a time_t, and that’s the rarely-used ctime() function that prints the value in local time:

now = time(NULL);
printf("%s", ctime(&now));

This returns a string with a very specific form that includes a newline at the end:

Sun Feb 28 18:47:25 2021

So that’s kind of inflexible. If you want more control, you should convert that time_t into a struct tm.

38.3.1 Converting time_t to struct tm

There are two amazing ways to do this conversion:

Let’s see what time it is now by printing out a struct tm with the asctime() function:

printf("Local: %s", asctime(localtime(&now)));
printf("  UTC: %s", asctime(gmtime(&now)));

Output (I’m in the Pacific Standard Time zone):

Local: Sun Feb 28 20:15:27 2021
  UTC: Mon Mar  1 04:15:27 2021

Once you have your time_t in a struct tm, it opens all kinds of doors. You can print out the time in a variety of ways, figure out which day of the week a date is, and so on. Or convert it back into a time_t.

More on that soon!

38.3.2 Converting struct tm to time_t

If you want to go the other way, you can use mktime() to get that information.

mktime() sets the values of tm_wday and tm_yday for you, so don’t bother filling them out because they’ll just be overwritten.

Also, you can set tm_isdst to -1 to have it make the determination for you. Or you can manually set it to true or false.

// Don't be tempted to put leading zeros on these numbers (unless you
// mean for them to be in octal)!

struct tm some_time = {
    .tm_year=82,   // years since 1900
    .tm_mon=3,     // months since January -- [0, 11]
    .tm_mday=12,   // day of the month -- [1, 31]
    .tm_hour=12,   // hours since midnight -- [0, 23]
    .tm_min=0,     // minutes after the hour -- [0, 59]
    .tm_sec=4,     // seconds after the minute -- [0, 60]
    .tm_isdst=-1,  // Daylight Saving Time flag
};

time_t some_time_epoch;

some_time_epoch = mktime(&some_time);

printf("%s", ctime(&some_time_epoch));
printf("Is DST: %d\n", some_time.tm_isdst);

Output:

Mon Apr 12 12:00:04 1982
Is DST: 0

When you manually load a struct tm like that, it should be in local time. mktime() will convert that local time into a time_t calendar time.

Weirdly, however, the standard doesn’t give us a way to load up a struct tm with a UTC time and convert that to a time_t. If you want to do that with Unix-likes, try the non-standard timegm(). On Windows, _mkgmtime().

38.4 Formatted Date Output

We’ve already seen a couple ways to print formatted date output to the screen. With time_t we can use ctime(), and with struct tm we can use asctime().

time_t now = time(NULL);
struct tm *local = localtime(&now);
struct tm *utc = gmtime(&now);

printf("Local time: %s", ctime(&now));     // Local time with time_t
printf("Local time: %s", asctime(local));  // Local time with struct tm
printf("UTC       : %s", asctime(utc));    // UTC with a struct tm

But what if I told you, dear reader, that there’s a way to have much more control over how the date was printed?

Sure, we could fish individual fields out of the struct tm, but there’s a great function called strftime() that will do a lot of the hard work for you. It’s like printf(), except for dates!

Let’s see some examples. In each of these, we pass in a destination buffer, a maximum number of characters to write, and then a format string (in the style of—but not the same as—printf()) which tells strftime() which components of a struct tm to print and how.

You can add other constant characters to include in the output in the format string, as well, just like with printf().

We get a struct tm in this case from localtime(), but any source works fine.

#include <stdio.h>
#include <time.h>

int main(void)
{
    char s[128];
    time_t now = time(NULL);

    // %c: print date as per current locale
    strftime(s, sizeof s, "%c", localtime(&now));
    puts(s);   // Sun Feb 28 22:29:00 2021

    // %A: full weekday name
    // %B: full month name
    // %d: day of the month
    strftime(s, sizeof s, "%A, %B %d", localtime(&now));
    puts(s);   // Sunday, February 28

    // %I: hour (12 hour clock)
    // %M: minute
    // %S: second
    // %p: AM or PM
    strftime(s, sizeof s, "It's %I:%M:%S %p", localtime(&now));
    puts(s);   // It's 10:29:00 PM

    // %F: ISO 8601 yyyy-mm-dd
    // %T: ISO 8601 hh:mm:ss
    // %z: ISO 8601 time zone offset
    strftime(s, sizeof s, "ISO 8601: %FT%T%z", localtime(&now));
    puts(s);   // ISO 8601: 2021-02-28T22:29:00-0800
}

There are a ton of date printing format specifiers for strftime(), so be sure to check them out in the strftime() reference page193.

38.5 More Resolution with timespec_get()

You can get the number of seconds and nanoseconds since Epoch with timespec_get().

Maybe.

Implementations might not have nanosecond resolution (that’s one billionth of a second) so who knows how many significant places you’ll get, but give it a shot and see.

timespec_get() takes two arguments. One is a pointer to a struct timespec to hold the time information. And the other is the base, which the spec lets you set to TIME_UTC indicating that you’re interested in seconds since Epoch. (Other implementations might give you more options for the base.)

And the structure itself has two fields:

struct timespec {
    time_t tv_sec;   // Seconds
    long   tv_nsec;  // Nanoseconds (billionths of a second)
};

Here’s an example where we get the time and print it out both as integer values and also a floating value:

struct timespec ts;

timespec_get(&ts, TIME_UTC);

printf("%ld s, %ld ns\n", ts.tv_sec, ts.tv_nsec);

double float_time = ts.tv_sec + ts.tv_nsec/1000000000.0;
printf("%f seconds since epoch\n", float_time);

Example output:

1614581530 s, 806325800 ns
1614581530.806326 seconds since epoch

struct timespec also makes an appearance in a number of the threading functions that need to be able to specify time with that resolution.

38.6 Differences Between Times

One quick note about getting the difference between two time_ts: since the spec doesn’t dictate how that type represents a time, you might not be able to simply subtract two time_ts and get anything sensible194.

Luckily you can use difftime() to compute the difference in seconds between two dates.

In the following example, we have two events that occur some time apart, and we use difftime() to compute the difference.

#include <stdio.h>
#include <time.h>

int main(void)
{
    struct tm time_a = {
        .tm_year=82,   // years since 1900
        .tm_mon=3,     // months since January -- [0, 11]
        .tm_mday=12,   // day of the month -- [1, 31]
        .tm_hour=4,    // hours since midnight -- [0, 23]
        .tm_min=00,    // minutes after the hour -- [0, 59]
        .tm_sec=04,    // seconds after the minute -- [0, 60]
        .tm_isdst=-1,  // Daylight Saving Time flag
    };

    struct tm time_b = {
        .tm_year=120,  // years since 1900
        .tm_mon=10,    // months since January -- [0, 11]
        .tm_mday=15,   // day of the month -- [1, 31]
        .tm_hour=16,   // hours since midnight -- [0, 23]
        .tm_min=27,    // minutes after the hour -- [0, 59]
        .tm_sec=00,    // seconds after the minute -- [0, 60]
        .tm_isdst=-1,  // Daylight Saving Time flag
    };

    time_t cal_a = mktime(&time_a);
    time_t cal_b = mktime(&time_b);

    double diff = difftime(cal_b, cal_a);

    double years = diff / 60 / 60 / 24 / 365.2425;  // close enough

    printf("%f seconds (%f years) between events\n", diff, years);
}

Output:

1217996816.000000 seconds (38.596783 years) between events

And there you have it! Remember to use difftime() to take the time difference. Even though you can just subtract on a POSIX system, might as well stay portable.


Prev | Contents | Next