“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.
Just a couple quick terms in case you don’t have them down.
UTC: Coordinated Universal Time is a universally190 agreed upon, absolute time. Everyone on the planet thinks it’s the same time right now in UTC… even though they have different local times.
GMT: Greenwich Mean Time, effectively the same as UTC191. You probably want to say UTC, or “universal time”. If you’re talking specifically about the GMT time zone, say GMT. Confusingly, many of C’s UTC functions predate UTC and still refer to Greenwich Mean Time. When you see that, know that C means UTC.
Local time: what time it is where the computer running the program is located. This is described as an offset from UTC. Although there are many time zones in the world, most computers do work in either local time or UTC.
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.)
There are two192 main types in C when it comes to dates: time_t
and struct tm
.
The spec doesn’t actually say much about them:
time_t
: a real type capable of holding a time. So by the spec, this could be a floating type or integer type. In POSIX (Unix-likes), it’s an integer. This holds calendar time. Which you can think of as UTC time.
struct tm
: holds the components of a calendar time. This is a broken-down time, i.e. the components of the time, like hour, minute, second, day, month, year, etc.
On a lot of systems, time_t
represents the number of seconds since Epoch193. 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?”
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
= time(NULL); // You can get it like this...
now
(&now); // ...or this. Same as the previous line. time
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:
= time(NULL);
now ("%s", ctime(&now)); printf
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
.
time_t
to struct tm
There are two amazing ways to do this conversion:
localtime()
: this function converts a time_t
to a struct tm
in local time.gmtime()
: this function converts a time_t
to a struct tm
in UTC. (See ye olde GMT creeping into that function name?)Let’s see what time it is now by printing out a struct tm
with the asctime()
function:
("Local: %s", asctime(localtime(&now)));
printf(" UTC: %s", asctime(gmtime(&now))); printf
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!
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;
= mktime(&some_time);
some_time_epoch
("%s", ctime(&some_time_epoch));
printf("Is DST: %d\n", some_time.tm_isdst); printf
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()
.
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);
("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 printf
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 page194.
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;
(&ts, TIME_UTC);
timespec_get
("%ld s, %ld ns\n", ts.tv_sec, ts.tv_nsec);
printf
double float_time = ts.tv_sec + ts.tv_nsec/1000000000.0;
("%f seconds since epoch\n", float_time); printf
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.
One quick note about getting the difference between two time_t
s: since the spec doesn’t dictate how that type represents a time, you might not be able to simply subtract two time_t
s and get anything sensible195.
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.