First of all, I'm writing this blog because, for the fourth or fifth time in my life, I've wrapped my head around it. Yes, that does mean that I fully knew this, and forgot it again at least three other times.
Second, the examples here are using glibc/Linux programming in straight C language. The concepts are portable, but it is always in C where I find myself returning, once again, to having to do something to get around a timezone issue.
IMPORTANT: Before tracking down my email to yell at me about how wrong I am, check the Side Notes section.
The next few sections are some background concepts for those who want to read this, but don't spend their spare time programming. For those that already know, please skip ahead.
On UNIX systems, the EPOCH ( time_t is the datatype ) zero (0)
exactly represents
1970-01-01T00:00:00Zwhere Z means UTC or Greenwich Mean Time. That is to say, when I ask glibc for the time(), what I actually get is the current time, represented in a count of seconds, since 1970 January 1, midnight GMT.
This returns a time_t representing "now" (whenever it is run).
This will split a time_t value into an easier to use structure called 'tm'.
struct tm { int tm_sec; /* Seconds (0-60) */ int tm_min; /* Minutes (0-59) */ int tm_hour; /* Hours (0-23) */ int tm_mday; /* Day of the month (1-31) */ int tm_mon; /* Month (0-11) */ int tm_year; /* Year - 1900 */ int tm_wday; /* Day of the week (0-6, Sunday = 0) */ int tm_yday; /* Day in the year (0-365, 1 Jan = 0) */ int tm_isdst; /* Daylight saving time */ };
The output will be translated to the local timezone.
One of the most useful tools in dealing with time in C, glibc's mktime() function. The way it works is, I pass in a structure that represents year, month, day, hour, minute, second; and I get back a time_t for that array. This is particularly fun, because I can do evil things like subtract 1 from the hour, and EVEN IF the hour ends up as -1, mktime will correct the hour to 23, and subtract a day (all the way up).
However, this ALWAYS assumes that the "representation" is represented in the user's local time zone. But that isn't true in the OTHER direction!
This will take a time_t value, and put it into a 'tm' structure without applying the local timezone to the values.
This is the function I use to print a time_t in a readable form (ISO-8601-1:1998 format):
void printtime (time_t input, short wantlocal) { struct tm t; /* Zero out the structure (no, C doesn't do this automatically) */ memset( &t, 0, sizeof( struct tm ) ); if (wantlocal) { /* Convert time_t into a LOCALIZED struct tm */ localtime_r( &input, &t ); printf( " my_time = %04d-%02d-%02dT%02d:%02d:%02d%+03d:%02d\n", t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, (int)( (-timezone) / 3600 ), abs((int)( ( timezone % 3600 ) / 60 ) )); } else { /* Convert time_t into a UTC struct tm */ gmtime_r( &input, &t ); printf( " my_time = %04d-%02d-%02dT%02d:%02d:%02dZ\n", t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec ); } }
Starting off, is is weird to wrap one's head around a time_t (always in UTC) and a struct tm representation which /may/ be in any timezone.
I'm going to start illustrating this by attempting to calculate the UNIX EPOCH. It is a good place to start, because we absolutely know that the result should be zero (0). However, since mktime() always expects struct tm to represent the user's /local/ timezone, that makes trying to calculate any UTC timestamp difficult.
I'll start with a code-snippet:
dev% cat timefun.c /* timefun.c */ #include#include #include int main() { struct tm example; /* First, create a struct tm that is all zero... */ memset( &example, 0, sizeof( struct tm ) ); /* ...except, set year to 1970... */ example.tm_year = 70; /* ...and set day of month to 1. */ example.tm_mday = 1; /* use mktime to give us a time_t */ time_t my_epoch = mktime( &example ); /* put the result on screen. */ printf( " my_epoch = %ld\n", my_epoch ); } /* EOF */ dev% gcc -o timefun timefun.c dev% ./timefun my_epoch = 18000 dev%
I am in the US Eastern Time Zone, and it is currently standard time, so 18000 is the UTC time_t for when it was
1970-01-01T00:00:00-05:00or, 5 hours worth of seconds PAST when midnight happened at GMT. Written in GMT, that looks like this:
1970-01-01T05:00:00Z
But, what I am /trying/ to do, is get mktime to calculate the EPOCH, which is supposed to be in UTC. But since mktime adjusts the input 'tm' as /localtime/ it will always give me back the localized version of what I ask for!
That said, it is useful to look for EPOCH, and get back the wrong answer (it allows us to find the right answer).
Here, I'm going to shift gears a little, since the example is silly (just type a zero). Instead, I'd like to seek something that is actually useful. Calculate today's midnight UTC (always in the past). Put another way, what time was it when UTC last switched day?
There are multiple ways to solve this issue.
This illustrates what is actually happening, but is definitely NOT the best solution.
dev% cat timefun2.c /* timefun2.c - Calculate today's midnight GMT (always in the past) */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <time.h> /* function printtime removed */ int main() { /* First part identical to timefun.c */ struct tm example; memset( &example, 0, sizeof( struct tm ) ); example.tm_year = 70; /* 1970 */ example.tm_mday = 1; time_t my_epoch = mktime( &example ); printf( " my_epoch = %ld\n", my_epoch ); /* New part */ /* Zero out the structure again */ memset( &example, 0, sizeof( struct tm ) ); /* Get the CURRENT time */ time_t my_today = time(NULL); /* expand current time into UTC tm structure */ gmtime_r( &my_today, &example ); /* set hour, minute and second back to zero (midnight) */ example.tm_hour = 0; example.tm_min = 0; example.tm_sec = 0; /* Once again, mktime is going struct tm as a local time. */ my_today = mktime( &example ); /* fix my_today, by undoing the assumption mktime had... */ my_today = ( my_today + ( -my_epoch ) ); /* Print out the results */ printf( " my_today = %ld\n", my_today ); printtime( my_today, 1 ); printtime( my_today, 0 ); exit(0); } /* EOF */ dev%
Note, that if I externally set my timezone to the other side of the world, it still works, giving the exact same UTC answer (with the localtime reflecting the requested timezone). The first my_time (printed in local) answers when was the last time the GMT day switched. The second one prints that exact second.
dev% gcc -o timefun2 timefun2.c dev% ./timefun2 my_epoch = 18000 my_today = 1630195200 my_time = 2021-08-28T20:00:00-05:00 my_time = 2021-08-29T00:00:00Z dev% TZ="Asia/Taipei" ./timefun2 my_epoch = -28800 my_today = 1630195200 my_time = 2021-08-29T08:00:00+08:00 my_time = 2021-08-29T00:00:00Z dev%
Introducing tzset().
This function sets up global variables (defined in time.h,
one of which is 'timezone').
The timezone is always the same as 'my_epoch' as calculated above.
As in, timezone is:
18000 in Summer, America/New_York
-28800 in Summer, Asia/Taipei
-34200 in Winter, Australia/Darwin
So, we can do the same thing, without calculating the timezone offset first...
dev% cat timefun3.c /* timefun3.c - Calculate today's midnight GMT (always in the past) */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <time.h> /* function printtime removed */ int main() { struct tm example; /* Introducing tzset()! */ tzset(); /* get 'timezone' */ /* No need to calculate my_epoch */ /* The part is the same as the "New part" of timefun2.c */ memset( &example, 0, sizeof( struct tm ) ); /* Get the CURRENT time */ time_t my_today = time(NULL); /* expand current time into UTC tm structure */ gmtime_r( &my_today, &example ); /* set hour, minute and second back to zero (midnight) */ example.tm_hour = 0; example.tm_min = 0; example.tm_sec = 0; /* Once again, mktime is going struct tm as a local time. */ my_today = mktime( &example ); /* fix my_today, by undoing the assumption, this time with -timezone */ my_today = ( my_today + ( -timezone ) ); /* Print out the results */ printf( " my_today = %ld\n", my_today ); printtime( my_today, 1 ); printtime( my_today, 0 ); exit(0); } /* EOF */ dev% gcc -o timefun3 timefun3.c dev% ./timefun3 my_today = 1630195200 my_time = 2021-08-28T20:00:00-05:00 my_time = 2021-08-29T00:00:00Z dev% TZ="Australia/Darwin" ./timefun3 my_today = 1630195200 my_time = 2021-08-29T09:30:00+09:30 my_time = 2021-08-29T00:00:00Z dev%
So this takes the same approach as above, except now it puts ( -timezone ) directly into the tm_sec field BEFORE running mktime().
dev% cat timefun4.c /* timefun4.c - Calculate today's midnight GMT (always in the past) */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <time.h> /* function printtime removed */ int main() { struct tm example; tzset(); /* get 'timezone' */ memset( &example, 0, sizeof( struct tm ) ); time_t my_today = time(NULL); gmtime_r( &my_today, &example ); example.tm_hour = 0; example.tm_min = 0; example.tm_sec = ( -timezone ); /* USE -timezone instead! */ my_today = mktime( &example ); /* my_today is already fixed, by manipulating tm_sec */ /* Print out the results */ printf( " my_today = %ld\n", my_today ); printtime( my_today, 1 ); printtime( my_today, 0 ); exit(0); } /* EOF */ dev% gcc -o timefun4 timefun4.c dev% ./timefun4 my_today = 1630195200 my_time = 2021-08-28T20:00:00-05:00 my_time = 2021-08-29T00:00:00Z dev% TZ="Asia/Kathmandu" ./timefun4 my_today = 1630195200 my_time = 2021-08-29T05:45:00+05:45 my_time = 2021-08-29T00:00:00Z dev%
I want to make it clear that I think this last example is harder to read, and because of that, I wouldn't recommend it as my best solution.
Computer stuff is pedantic, so here's some things that I expect certain people might get angry about.
All snippets I used to write this page are at: gitlab.home.vollink.com. These compile cleanly on Ubuntu 20.04, YRMV.
Yes, I'm saying Greenwich Mean Time as a synonym of UTC here. Even though "UTC" or "Universal Coordinated Time" was established in 1967, it doesn't seem like the UNIX systems programmers had heard (or cared about) the news, since the few time functions that specifically avoid applying the local timezone are named gmt*.
That is, I'm mentioning GMT, the solar position timezone, because I'm also mentioning gmtime_r(), and THAT name treats gmt as a synonym of UTC.
At the time, no computer could keep time within the difference between UTC and GMT for more than a few seconds, so maybe that is why they named functions gmt instead of utc.
While there are still systems that will not be able to track dates beyond 19 January 2038, this was solved by 2015 for most new things that actually care. Even on a 32-bit Linux, it is trivial to ask for a 64-bit time_t Even the oldest Raspberry Pi has a 64-bit time_t available.
Anyway, anybody who is programming something new, now, is not likely to need to worry about it. Of course, this problem WILL still hurt folks who are still running an old hardware/OS, but those same people are unlikely to be developing new things on the old hardware without being deeply aware of the time_t constraints.
EPOCH representing number of seconds since 1970 is not at all right! The time() actually completely ignores leap-seconds. It is like they don't exist. AND to be clear, UNIX does not completely ignore them, but instead slews its clock to absorb a leap second throughout the day. Facebook has a fascinating article about how they handle leap seconds in their nanosecond accuracy datacenter time service.
Messing with the environment will absolutely work! That said, it is always a better idea to stick to 'threadsafe' methods. That is, once TZ is set for a program, it affects EVERY part of that program. So if one part of your program temporarily changes TZ, another part of the program will get unexpected results from localtime. Note, too, that some libraries use threading internally, even if the main program doesn't explicitly ask for it.
For around two years, I've spent about a day a month working on cracking Apple Music's .musicdb format. This format, like iTunes' .itl format before it, uses an EPOCH of 1-Jan-1904, midnight UTC. This is stored in an unsigned 32-bit number, which is compatible with the native date format of MacOS 9 and earlier.
Ultimately, I need to decode that timestamp, convert it's EPOCH to a 64-bit, signed time_t before I have a representable copy on a UNIX system, which I will eventually need for my Playlister project.
Okay, my e-mail address can be found below.