11

I didn't find a trivial way to get the time offset in minutes between the local time and the UTC time.

At first I intended to use tzset() but it doesn't provide the daylight saving time. According to the man page, it is simply an integer different of zero if day light saving is in effect. While it is usually an hour, it may be half an hour in some country.

I would prefer avoiding to compute the time difference between current UTC returned by gmtime() and localtime().

A more general solution would give me this information for a specified location and a positive time_t value, or at least locally.

Edit 1: the use case is to get the right local time offset for https://github.com/chmike/timez. BTW, If you thought libc functions to manipulate time were Ok, read this https://rachelbythebay.com/w/2013/03/17/time/.

Edit 2: the best and simplest solution I have so far to compute the time offset to UTC in minutes is

// Bogus: assumes DST is always one hour
tzset();
int offset = (int)(-timezone / 60 + (daylight ? 60 : 0));

The problem is to determine the real day light saving time.

Edit 3: Inspired by the answer of @trenki, I came up with the following solution. This is a hack in that it tricks mktime() to consider the output of gmtime() as the localtime. The result is inaccurate when the DST change is in the time span between UTC time and localtime.

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

int main()
{
    time_t rawtime = time(NULL);
    struct tm *ptm = gmtime(&rawtime);
    // Request that mktime() looksup dst in timezone database
    ptm->tm_isdst = -1;                
    time_t gmt = mktime(ptm);
    double offset = difftime(rawtime, gmt) / 60;
    printf("%f\n", offset);
    return 0;
}
Community
  • 1
  • 1
chmike
  • 20,922
  • 21
  • 83
  • 106
  • 1
    Why do you need this? Explain much more about the context; Why can't you simply use `gmtime` and `localtime` and make a difference of their `struct tm`? Why would your user care about that local time offset? And there are lot of weird cases (e.g. New Year's Eve, ...) – Basile Starynkevitch Sep 06 '15 at 13:34
  • 1
    Also, your question is very probably operating system specific ... AFAIK, [locale(7)](http://man7.org/linux/man-pages/man7/locale.7.html) are not in standard C99, but in POSIX – Basile Starynkevitch Sep 06 '15 at 13:38
  • @BasileStarynkevitch Locales are in C99, even C89 has `strftime`, alas only with `%Z` but not `%z`. – Jens Sep 06 '15 at 16:12
  • "general solution would give me this information for a specified location" ---> And how do you want to specify location? latitude/longitude? Now you need a timezone map. Since maps change over time, now one needs a history of maps. Recommend instead to simple design for a "general solution ... for a specified timezone". – chux - Reinstate Monica Sep 06 '15 at 16:56
  • @BasileStarynkevitch It is for this small library : https://github.com/chmike/timez. It is a work in progress. I still have to verify the validity of a few things like the minute time offset resolution. My question is not OS specific. I would like that the library could be used on Windows or Linux. On Unix and GNU the tm structure has the tm_gmtoff field which holds the requested information. – chmike Sep 06 '15 at 18:56
  • I looked into the `tzcode` library available here https://github.com/eggert/tz. The information is computed inside but apparently not made available through the standard libc API. – chmike Sep 06 '15 at 19:01
  • @chux the information is obtained from the IANA time zone database. Most OS include this information so that `localtime()` can compute the time offset. The location is expressed in the form "Europe\Paris". This is needed when you have a web forum and people from different region of the world post (stamped) messages and want to see date and times from their local time reference. In their account configuration users can pick their time zone from a proposed list. Longitude and latitude coordinates are very difficult to map to the named zone because limits are fuzzy. – chmike Sep 08 '15 at 09:48
  • @chmike Being very familiar with for decades, I was surprised to learn "Most OS include this information". What is the source of that statistic? – chux - Reinstate Monica Sep 08 '15 at 14:05
  • @chux This is common sense. Any OS providing the ability for the user to define its local time will use the time zone database of IANA to get the most accurate and up to date information. The IANA timezone database provides the information in this format. OS that don't provide such possibility will of course not provide by default the IANA timezone database. – chmike Sep 09 '15 at 08:49
  • I have provided an aswer on the similar question here: https://stackoverflow.com/a/69950393/1558980 – Victor Nov 13 '21 at 00:34

6 Answers6

4

Does your system's strftime() function support the %z and %Z specifiers? On FreeBSD,

 %Z    is replaced by the time zone name.

 %z    is replaced by the time zone offset from UTC; a leading plus sign
       stands for east of UTC, a minus sign for west of UTC, hours and
       minutes follow with two digits each and no delimiter between them
       (common form for RFC 822 date headers).

and I can use this to print this:

$ date +"%Z: %z"
CEST: +0200

ISO C99 has this in 7.23.3.5 The strftime function:

%z     is replaced by the offset from UTC in the ISO 8601 format
       ‘‘−0430’’ (meaning 4 hours 30 minutes behind UTC, west of Greenwich),
       or by no characters if no time zone is determinable. [tm_isdst]
%Z     is replaced by the locale’s time zone name or abbreviation, or by no
       characters if no time zone is determinable. [tm_isdst]
Jens
  • 69,818
  • 15
  • 125
  • 179
  • That would be a way to get the information. A bit convoluted, but it would work. But Visual studio, for instance, doesn't support C99. Not sure about VC 2015, but that is a know problem. I would prefer avoiding the dependency to C99. – chmike Sep 06 '15 at 19:05
  • 1
    Parsing the text `"-0030"` (and others) needs to be careful to apply the sign correctly. E.g. `sscanf("-0030", "%c%2d%2d", &sign, &hour, &min); delta = (sign == '-' ? -1: 1) * (hour*60 + min);` or `sscanf("-0030", "%d", &delta); delta = delta/100*60 + delta%100;`. `sscanf("-0030", "%3d%2d", ...)` will not work. – chux - Reinstate Monica Apr 16 '18 at 20:45
  • The %z does not work reliably on Windows: on my Windows 7 PC, it returns the timezone name: "W. Europe Standard Time", while on my colleague's Windows 10 PC it returns the expected timezone offset: "+0100" The %Z, however, returns the timezone name on both PCs... The Microsoft documentation of strftime() simply states that the output depends on registry settings (not giving any details as to *WHICH* registry settings... :-( ) – Lanzelot Dec 19 '18 at 15:48
  • @Lanzelot Windows (Visual Studio) does not support C99 at all, as I understand it. Some C99 features happen to work since they are the same in C++, others are not implemented. – Jens Jan 07 '19 at 23:06
4

This C code computes the local time offset in minutes relative to UTC. It assumes that DST is always one hour offset.

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

int main()
{
    time_t rawtime = time(NULL);
    struct tm *ptm = gmtime(&rawtime);
    time_t gmt = mktime(ptm);
    ptm = localtime(&rawtime);
    time_t offset = rawtime - gmt + (ptm->tm_isdst ? 3600 : 0);

    printf("%i\n", (int)offset);
}

It uses gmtime and localtime though. Why don't you want to use those functions?

trenki
  • 7,133
  • 7
  • 49
  • 61
  • 2
    In Lord Howe Island (australasia), the DST is 30 minutes. See http://www.timeanddate.com/time/zone/australia/lord-howe-island. I would prefer avoiding `gmtime()` and `localtime()` because 1) it involves much more computing than needed 2) computing the time offset from the fields is tricky and error prone. – chmike Sep 07 '15 at 15:23
  • 1
    In Ireland, the DST is negative: see https://en.wikipedia.org/wiki/Winter_time_(clock_lag) and https://en.wikipedia.org/wiki/List_of_tz_database_time_zones – Andrea Nov 04 '21 at 20:47
4

... to get local time offset ... relative to UTC?

@Serge Ballesta answer is good. So I though I would test it and clean-up a few details. I would have posted as a comment but obviously too big for that. I only exercised it for my timezone, but though others may want to try on their machine and zone.

I made to community wiki as not to garner rep. Imitation is the sincerest form of flattery

This answer is akin to @trenki except that it subtracts nearby struct tm values instead of assuming DST shift is 1 hour and time_t is in seconds.

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

// return difference in **seconds** of the tm_mday, tm_hour, tm_min, tm_sec members.
long tz_offset_second(time_t t) {
  struct tm local = *localtime(&t);
  struct tm utc = *gmtime(&t);
  long diff = ((local.tm_hour - utc.tm_hour) * 60 + (local.tm_min - utc.tm_min))
          * 60L + (local.tm_sec - utc.tm_sec);
  int delta_day = local.tm_mday - utc.tm_mday;
  // If |delta_day| > 1, then end-of-month wrap 
  if ((delta_day == 1) || (delta_day < -1)) {
    diff += 24L * 60 * 60;
  } else if ((delta_day == -1) || (delta_day > 1)) {
    diff -= 24L * 60 * 60;
  }
  return diff;
}

void testtz(void) {
  long off = -1;
  int delta = 600;
  for (time_t t = 0; t < LONG_MAX-delta; t+=delta) {
    long off2 = tz_offset_second(t);

    // Print time whenever offset changes.
    if (off != off2) {
      struct tm utc = *gmtime(&t);
      printf("%10jd %04d-%02d-%02dT%02d:%02d:%02dZ\n", (intmax_t) t,
              utc.tm_year + 1900, utc.tm_mon + 1, utc.tm_mday,
              utc.tm_hour, utc.tm_min, utc.tm_sec);
      struct tm local = *localtime(&t);
      off = off2;
      printf("%10s %04d-%02d-%02d %02d:%02d:%02d %2d %6ld\n\n", "",
              local.tm_year + 1900, local.tm_mon + 1, local.tm_mday,
              local.tm_hour, local.tm_min, local.tm_sec, local.tm_isdst ,off);
      fflush(stdout);
    }
  }
  puts("Done");
}

Output

                                  v----v  Difference in seconds
         0 1970-01-01T00:00:00Z
           1969-12-31 18:00:00  0 -21600

   5731200 1970-03-08T08:00:00Z
           1970-03-08 03:00:00  1 -18000

  26290800 1970-11-01T07:00:00Z
           1970-11-01 01:00:00  0 -21600

...

2109222000 2036-11-02T07:00:00Z
           2036-11-02 01:00:00  0 -21600

2120112000 2037-03-08T08:00:00Z
           2037-03-08 03:00:00  1 -18000

2140671600 2037-11-01T07:00:00Z
           2037-11-01 01:00:00  0 -21600

Done
chux - Reinstate Monica
  • 143,097
  • 13
  • 135
  • 256
3

IMHO the only foolproof and portable way is to use localtime and gmtime and manually compute the delta in minute because those 2 functions exist on all known systems. For example:

int deltam() {
    time_t t = time(NULL);
    struct tm *loc = localtime(&t);
    /* save values because they could be erased by the call to gmtime */
    int loc_min = loc->tm_min;
    int loc_hour = loc->tm_hour;
    int loc_day = loc->tm_mday;
    struct tm *utc = gmtime(&t);
    int delta = loc_min - utc->tm_min;
    int deltaj = loc_day - utc->tm_mday;
    delta += (loc_hour - utc->tm_hour) * 60;
    /* hack for the day because the difference actually is only 0, 1 or -1 */
    if ((deltaj == 1) || (deltaj < -1)) {
        delta += 1440;
    }
    else if ((deltaj == -1) || (deltaj > 1)) {
        delta -= 1440;
    }
    return delta;
}

Beware, I did not test all possible corner cases, but it could be a starting point for your requirement.

chmike
  • 20,922
  • 21
  • 83
  • 106
Serge Ballesta
  • 143,923
  • 11
  • 122
  • 252
  • The problem are the corner case. This is what makes this approach difficult. – chmike Sep 07 '15 at 15:42
  • What if the time offset crosses year boundary ? – chmike Sep 07 '15 at 16:06
  • @chmike: That's the reason for what I called *hack for the day*. I do not use `tm_mon` nor `tm_year` fields because I assume the following: if the difference between the `tm_mday` fields is 0, 1 or -1, then it is the real difference ; if it is near +30 (> 1 in my hack), there is a month change, and the actual value is -1; and if it is near -30 (< -1 in my code) there is a month change in the opposite side and the actual value is +1. – Serge Ballesta Sep 07 '15 at 17:19
  • @chmike: The corner cases I did not test was whether there could be problems near the date change line, of with unusual locale (I already process differences of x h + 15,30 or 45') because I did not control the full zinfo database. But for all locales that I know (mainly Europe and North America) it should work fine independently of month or year crossing or of DST change. – Serge Ballesta Sep 07 '15 at 17:19
  • @SergeBallesta The end-of-year cases look well covered here. Challenging corners cases that remain include a zone [shifting across the IDL](https://www.timeanddate.com/time/zone/kiribati/kiritimati?year=1995). – chux - Reinstate Monica Apr 16 '18 at 20:51
1

I would like to submit yet another answer to this question, one that AFAICS also deals with the IDL.

This solution depends on timegm and mktime. On Windows timegm is available as _mkgmtime from the CRT, in other words define a conditional macro.

#if _WIN32
#    define timegm _mkgmtime
#endif

int local_utc_offset_minutes ( ) {
    time_t t  = time ( NULL );
    struct tm * locg = localtime ( &t );
    struct tm locl;
    memcpy ( &locl, locg, sizeof ( struct tm ) );
    return (int)( timegm ( locg ) - mktime ( &locl ) ) / 60;
}
ceztko
  • 14,736
  • 5
  • 58
  • 73
degski
  • 642
  • 5
  • 13
  • Why the copy locg ? – chmike Aug 17 '19 at 10:55
  • @chmike The data from `localtime` needs to be created 2 times as the calls to both `timegm` and `mktime` are destructive on the result of `localtime` and we need clean data. Another option would be to call `localtime` twice, but that's at least as expensive as just copying the data on the stack. – degski Aug 18 '19 at 08:30
  • 1) `timegm()` is not part of the standard C library. 2) Code assume `time_t` is in seconds. (Reasonable, yet not as portable as `difftime()`. – chux - Reinstate Monica Aug 22 '19 at 05:42
  • `memcpy ( &locl, locg, sizeof ( tm ) );` is unnecessarily complex. Suggest `locl = *locg;` – chux - Reinstate Monica Aug 22 '19 at 05:43
  • @chux You don't have to like it, nor use it, feel free to write your own. It's a gcc extension, also on BSD, Windows has its own flavor as indicated. `memcpy` will get optimized to whatever is the most optimal on the platform, trust the compiler! – degski Aug 22 '19 at 09:43
  • @chux As to the `time_t` are seconds assumption, I'm sure there are platforms where this does not hold. I'm also sure those platforms are not relevant [any more]. I try to write code for use with real compilers and existing OSes, not pie in the sky. The thing is that those 'de-facto' standards will not be broken, simply because it would break shitloads of existing code [which is avoided at any (at the level of obsessive) cost]. – degski Aug 22 '19 at 12:05
  • Posted code not valid C. Recommend `tm` --> `struct tm` in 3 places. – chux - Reinstate Monica Aug 23 '19 at 01:06
  • @degski I posted a [solution](https://stackoverflow.com/a/32470889/2410359) 4 years ago. One that compiles in C, unlike this. One that does not rely on non-standard C functions - like this. One that does not assume `time_t` is in seconds, like this. There is no whining. Just pointing out this answer's limitations and areas for quick improvement. Nothing personal. – chux - Reinstate Monica Aug 24 '19 at 02:55
  • @chux Why do you say it doesn't compile in C (maybe I missed something, it compiles in C++)? The non-standard C function is a gcc-extension, this means clang (libc++) also implements it, and for windows there is a facsimile as well, for me that ,makes it pretty de-facto std, that it is not present in some ISO-document, I don't consider of much relevance. If you follow that document to the letter, half the linux-kernel is UB and many STL implementations could actually not be written. – degski Aug 24 '19 at 06:02
  • "Why do you say it doesn't compile in C" is best answered by trying to compile in C rather than another language. `tm * locg ...` errors with "error: unknown type name 'tm'; use 'struct' keyword to refer to the type" with gcc, see [C structure and C++ structure](https://stackoverflow.com/a/2242769/2410359) In 2019 most new processors are by far embedded ones (billions/per year). Many of those are OS-less. Non Windows/non linux compilers are quite relevant. – chux - Reinstate Monica Aug 24 '19 at 14:26
  • @chux Ah, yes, my C is getting rusty, thanks for pointing that out, I'm just C++-blinded. Updated. – degski Aug 25 '19 at 02:33
1

Here is my way:

time_t z = 0;
struct tm * pdt = gmtime(&z);
time_t tzlag = mktime(pdt);

Alternative with automatic, local storage of struct tm:

struct tm dt;
memset(&dt, 0, sizeof(struct tm));
dt.tm_mday=1; dt.tm_year=70;
time_t tzlag = mktime(&dt);

tzlag, in seconds, will be the negative of the UTC offset; lag of your timezone Standard Time compared to UTC:

LocalST + tzlag = UTC

If you want to also account for "Daylight savings", subtract tm_isdst from tzlag, where tm_isdst is the field for a particular local time struct tm, after applying mktime to it (or after obtaining it with localtime ).

Why it works:
The set struct tm is for "epoch" moment, Jan 1 1970, which corresponds to a time_t of 0. Calling mktime() on that date converts it to time_t as if it were UTC (thus getting 0), then subtracts the UTC offset from it in order to produce the output time_t. Thus it produces negative of UTC_offset.

Victor
  • 21
  • 4