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

发布于 2024-10-29 23:56:37 字数 1942 浏览 1 评论 0原文

(语言/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 数据库...(这相当悲伤 - 所以对于如此小的任务来说,有很多/感知/开销。)...或者不是 - 似乎 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

如您所见,两个午夜。

(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 tells 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.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

夏日浅笑〃 2024-11-05 23:56:37

我将 TZ 环境变量设置为“Europe/Moscow”来运行您的代码,并且能够重现您的输出。我认为发生的事情是这样的:

在前两行,一切都很好。然后我们“向前冲”,凌晨 2 点变成了凌晨 3 点。让我们使用 gdb 在进入 mktime 时中断,看看每次它的参数是什么:

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增加了一小时。通过修改时间,您“创建”了午夜时间,但启用了夏令时,这基本上是无效的。

您现在可能想知道,我们怎样才能摆脱困境?不要绝望! 当您手动调整 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

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
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文