开发者

DST-switch-aware getter for UNIX timestamp of current day's local time midnight

(Language/API: Standard C 89 library and / or POSIX)

Probably a trivial question, but I've got a feeling that I'm missing something.

I need to implement this function:

time_t get_local_midnight_timestamp(time_t ts);

That is, we get arbitrary timestamp (from the last year, for example), and return it rounded up to the midnight of the same day.

The problem is that the function must be aware of DST switches and DST rules changes (like DST cancellation and/or extension).

The function must also be future-proof, and cope with weird TZ changes (like shift of time zone 30 minutes ahead etc.).

(The reason I need all this that I need to implement look up into some older statistics data.)

As far as I understand, naïve approach with zeroing out struct tm time fields would not work — precisely because of DST stuff (looks like in DST-change day there are two local midnight time_t timestamps).

Please point me in the right direction...

I doubt that it can be done with standard C 89, so POSIX-specific solutions are acceptable. If not POSIX, then something Debian-specific would do...

Update: Also: Something tel开发者_运维知识库ls me that I should also take leap seconds in account. Maybe I should look into trying to directly use Tz database... (Which is rather sad — so much /perceived/ overhead for so small task.) ...Or not — seems that libc should use it, so maybe I'm just doing it wrong...

Update 2: Here is why I think that naïve solution does not work:

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

int main()
{
  struct tm date_tm;
  time_t date_start = 1301173200; /* Sunday 27 March 2011 0:00:00 AM MSK */
  time_t midnight = 0;
  char buf1[256];
  char buf2[256];
  int i = 0;

  for (i = 0; i < 4 * 60 * 60; i += 60 * 60)
  {
    time_t date = date_start + i;
    localtime_r(&date, &date_tm);

    strftime(buf1, 256, "%c %Z", &date_tm);

    date_tm.tm_sec = 0;
    date_tm.tm_min = 0;
    date_tm.tm_hour = 0;

    midnight = mktime(&date_tm);
    strftime(buf2, 256, "%c %Z", &date_tm);

    printf("%d : %s -> %d : %s\n", (int)date, buf1, (int)midnight, buf2);
  }
}

Output (local time was MSD at the moment when I run this):

$ gcc time.c && ./a.out
1301173200 : Sun Mar 27 00:00:00 2011 MSK -> 1301173200 : Sun Mar 27 00:00:00 2011 MSK
1301176800 : Sun Mar 27 01:00:00 2011 MSK -> 1301173200 : Sun Mar 27 00:00:00 2011 MSK
1301180400 : Sun Mar 27 03:00:00 2011 MSD -> 1301169600 : Sat Mar 26 23:00:00 2011 MSK
1301184000 : Sun Mar 27 04:00:00 2011 MSD -> 1301169600 : Sat Mar 26 23:00:00 2011 MSK

As you can see, two midnights.


I ran your code with the TZ environment variable set to "Europe/Moscow" and was able to reproduce your output. Here's what I think is going on:

On the first two lines, everything is fine. Then we "spring ahead" and 2 AM becomes 3 AM. Let's use gdb to break on entry to mktime and see what its argument is each time:

hour mday mon year wday yday isdst gmtoff tm_zone
   0   27   2  111    0   85     0  10800     MSK
   0   27   2  111    0   85     0  10800     MSK
   0   27   2  111    0   85     1  14400     MSD
   0   27   2  111    0   85     1  14400     MSD

So what has happened? Your code sets the hour to 0 each time, but this is a problem after the DST switch, because the impossible has happened: it is now "before" the DST switch in terms of the time of day, yet isdst is now set and gmtoff has been increased by one hour. By hacking up the time, you have "created" a time of midnight but with DST enabled, which is basically invalid.

You may now wonder, how can we get out of this mess? Do not despair! When you are adjusting the tm_hour field by hand, simply admit that you no longer know what the DST status is by setting tm_isdst to -1. This special value, which is documented in man localtime, means the DST status is "not available." So the computer will figure it out, and everything should work fine.

Here's my patch for your code:

date_tm.tm_hour = 0;
+ date_tm.tm_isdst = -1; /* we no longer know if it's DST or not */

Now I get this output, I hope is what you want:

$ TZ='Europe/Moscow' ./a.out
1301173200 : Sun Mar 27 00:00:00 2011 MSK -> 1301173200 : Sun Mar 27 00:00:00 2011 MSK
1301176800 : Sun Mar 27 01:00:00 2011 MSK -> 1301173200 : Sun Mar 27 00:00:00 2011 MSK
1301180400 : Sun Mar 27 03:00:00 2011 MSD -> 1301173200 : Sun Mar 27 00:00:00 2011 MSK
1301184000 : Sun Mar 27 04:00:00 2011 MSD -> 1301173200 : Sun Mar 27 00:00:00 2011 MSK
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜