Home » Linux » Time zone conversion C API on Linux, anyone?

Time zone conversion C API on Linux, anyone?

Posted by: admin November 30, 2017 Leave a comment

Questions:

I’m looking for something that I presumed would be very simple – given local Unix time in a specific time zone (specified as a string, e.g., “America/New_York” – note that’s not my local time), get the corresponding time value in GMT. I.e., something along the lines of

time_t get_gmt_time(time_t local_time,
                    const char* time_zone);

As deceptively simple as it sounds, the closest I could find was the following code snippet from timegm’s man page:

       #include <time.h>
       #include <stdlib.h>

       time_t
       my_timegm(struct tm *tm)
       {
           time_t ret;
           char *tz;

           tz = getenv("TZ");
           setenv("TZ", "", 1);
           tzset();
           ret = mktime(tm);
           if (tz)
               setenv("TZ", tz, 1);
           else
               unsetenv("TZ");
           tzset();
           return ret;
       } 

There gotta be a better way than this belligerently not thread-safe abomination, right? Right??

Answers:

Wanted to add a bit more detail here.

If you try the following:

#include <stdio.h>
#include <time.h>    /* defines 'extern long timezone' */

int main(int argc, char **argv)
{
    time_t t, lt, gt;
    struct tm tm;

    t = time(NULL);
    lt = mktime(localtime(&t));
    gt = mktime(gmtime(&t));

    printf( "(t = time(NULL)) == %x,\n"
        "mktime(localtime(&t)) == %x,\n"
        "mktime(gmtime(&t)) == %x\n"
        "difftime(...) == %f\n"
        "timezone == %d\n", t, lt, gt,
        difftime(gt, lt), timezone);
    return 0;
}

you’ll notice that timezone conversions make sure that:

  • mktime(localtime(t)) == t, and
  • mktime(gmtime(t)) == t + timezone,
    therefore:
  • difftime(mktime(gmtime(t)), mktime(localtime(t))) == timezone
    (the latter is a global variable initialized by either tzset() or the invocation of any timezone conversion function).

Example output of the above:

$ TZ=GMT ./xx
(t = time(NULL)) == 4dd13bac,
mktime(localtime(&t)) == 4dd13bac,
mktime(gmtime(&t)) == 4dd13bac
difftime(...) == 0.000000
timezone == 0

$ TZ=EST ./xx
(t = time(NULL)) == 4dd13baf,
mktime(localtime(&t)) == 4dd13baf,
mktime(gmtime(&t)) == 4dd181ff
difftime(...) == 18000.000000
timezone == 18000

$ TZ=CET ./xx
(t = time(NULL)) == 4dd13bb2,
mktime(localtime(&t)) == 4dd13bb2,
mktime(gmtime(&t)) == 4dd12da2
difftime(...) == -3600.000000
timezone == -3600

In that sense, you’re attempting to “do it backwards” – time_t is treated as absolute in UN*X, i.e. always relative to the “EPOCH” (0:00 UTC on 01/01/1970).

The difference between UTC and the current timezone (last tzset() call) is always in the external long timezone global.

That doesn’t get rid of the environment manipulation uglyness, but you can save yourself the effort of going through mktime().

Questions:
Answers:

From tzfile(5), which documents the files in /usr/share/zoneinfo (on my system) in gruesome detail:

It seems that timezone uses tzfile
internally, but glibc refuses to
expose it to userspace. This is most
likely because the standardised
functions are more useful and
portable, and actually documented by
glibc.

Again, this is probably not what you’re looking for (ie. an API), but the information is there and you can parse it without too much pain.

Questions:
Answers:

I really thought there was something in glib, but seem to have misremembered. I know you’re probably looking for straight-up C code, but here’s the best I’ve got:

I know that Python has some notion of timezones through a tzinfo class – you can read about it in the datetime documentation. You can have a look at the source code for the module (in the tarball, it’s in Modules/datetime.c) – it appears to have some documentation, so maybe you can get something out of it.

Questions:
Answers:

Similar to the Python answer, I can show you what R does:

R> now <- Sys.time()       # get current time
R> format(now)             # format under local TZ
[1] "2009-08-03 18:55:57"
R> format(now,tz="Europe/London")   # format under explicit TZ
[1] "2009-08-04 00:55:57"
R> format(now,tz="America/Chicago") # format under explicit TZ
[1] "2009-08-03 18:55:57"
R> 

but R uses an internal representation that extends the usual struct tm — see R-2.9.1/src/main/datetime.c.

Still, this is a hairy topic and it would be nice if it were the standard library. As it isn’t maybe your best bet is to use Boost Date_Time (example)

Questions:
Answers:

The problem with gmtime, localtime and their variants is the reliance on the TZ environment variable. The time functions first call tzset(void), which reads TZ to determine offsets DST, etc. If TZ is not set in the user’s environment, (g)libc uses the system timezone. So if you have a local struct tm in, say, ‘Europe/Paris’ and your machine or environment is set to ‘America/Denver’, the wrong offset will be applied when you convert to GMT. All the time functions call tzset(void) which reads TZ to set char *tzname[2], long timezone (diff, in seconds, from GMT) and int daylight (boolean for DST). Setting these directly has no affect, because tzset() will overwrite them the next time you call localtime, etc.

I was faced with the same issue as ‘igor’ in the original question, while setenv works it seems problematic (re-entran?). I decided to look further to see if I could modify tzset (void) to tzset(char*) to explicitly set the above mentioned variables. Well, of course, that’s just a bad idea… but in probing the glibc source and the IANA TZ database source, I came to the conclusion that the setenv approach ain’t so bad.

First, setenv only modifies the process global ‘char **environ’ (not the calling shell, so the ‘real’ TZ is not affected). And, second, glibc actually puts a lock in setenv. The drawback is that setenv/tzset calls are not atomic, so another thread could conceivably write to TZ before the original thread call tzset. But a well-implemented application that uses threads should watch for that anyway.

It would be cool if POSIX defined tzset to take a char* for look up in the extensive IANA TZ database (and take NULL to mean, ‘use the user or system TZ/), but failing that, setenv seems to be ok.

Questions:
Answers:

Why can’t you use the gmtime_r()? Following worked fine for me:

int main()
{
    time_t t_gmt, t_local=time(NULL);
    struct tm tm_gmt;

    gmtime_r(&t_local, &tm_gmt);

    t_gmt = mktime(&tm_gmt);

    printf("Time now is:    %s", ctime(&t_local));
    printf("Time in GMT is: %s", ctime(&t_gmt));

    return 0;
}

Leave a Reply

Your email address will not be published. Required fields are marked *