DST-switch-aware getter,用于当天当地时间午夜的UNIX时间戳

时间:2011-04-05 22:22:54

标签: c dst unix-timestamp

(语言/ API:标准C 89库和/或POSIX)

可能是一个微不足道的问题,但我有一种感觉,我错过了一些东西。

我需要实现这个功能:

time_t get_local_midnight_timestamp(time_t ts);

也就是说,我们得到任意时间戳(例如,从去年开始),并将其四舍五入到同一天的午夜。

问题是该功能必须知道DST开关和DST规则的变化(如DST取消和/或扩展)。

该功能还必须面向未来,并应对奇怪的TZ变化(如提前30分钟的时区转移等)。

(我需要实现这一切的原因需要查看一些较旧的统计数据。)

据我了解,将struct tm时间字段归零的天真方法不起作用 - 正是因为DST的东西(在DST更改日看起来有两个本地午夜time_t时间戳)。

请指出正确的方向......

我怀疑它可以用标准C 89完成,因此POSIX特定的解决方案是可以接受的。如果不是POSIX,那么Debian特有的东西会做......

更新:另外:有些东西告诉我,我也应该考虑闰秒。也许我应该考虑尝试直接使用Tz database ...(这是相当悲伤 - 如此多的/感知/开销这么小的任务。)......或者不 - 似乎libc应该使用它,所以也许我只是做错了......

更新2:这就是为什么我认为天真的解决方案不起作用的原因:

#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);
  }
}

输出(我运行此时的本地时间是MSD):

$ 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

如你所见,两个中午。

1 个答案:

答案 0 :(得分:5)

我将TZ环境变量设置为“Europe / Moscow”运行您的代码,并且能够重现您的输出。以下是我的想法:

在前两行,一切都很好。然后我们“向前迈进”,凌晨2点变成凌晨3点。让我们使用gdbmktime的入口处中断并查看每次的参数:

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

那发生了什么?你的代码每次将小时设置为0,但是在DST切换之后这是一个问题,因为不可能发生了:它现在在DST开关之前就时间而言,但isdst现在已设置并且gmtoff已经增加了一个小时。通过乱砍时间,你已经“创建”了午夜的时间但启用了DST,这基本上是无效的。

你现在可能想知道,我们怎么能摆脱这个烂摊子?不要灰心! 当您手动调整tm_hour字段时,只需将tm_isdst设置为-1即可承认您不再知道DST状态。此特殊值,即记录在man localtime中,表示DST状态为“不可用”。所以计算机会搞清楚,一切都应该正常。

这是我的代码补丁:

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

现在我得到了这个输出,我希望是你想要的:

$ 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