将 UTC 偏移量的日期时间字符串解析为 time_t

Parsing datetime string with UTC offset to time_t

提问人:InterLinked 提问时间:6/8/2023 更新时间:6/8/2023 访问量:66

问:

这个问题在这篇文章中有所暗示,但这个问题的答案根本没有回答这个问题,而且我到处散布着相互矛盾的建议和提示。

我的问题相对简单,但在深入研究时,我有点被绊倒了。

假设我有一个格式如下的字符串:2023-06-07 03:04:56 -0700

目标是将其规范化为纪元时间戳(在 C 中)。我以为这很简单,但似乎不是。这里的问题似乎是最后的。time_t-0700

似乎忽略了修改后的,可能,也许(同样,关于如何使用它,在不同的实现中等,我有相互矛盾的报告)。FWIW,我使用的是 Linux/glibc,所以我更关心它是否在那里工作,而不是它不在 C 标准中。strptime(3)%z

稍微玩了一下,在我看来,它确实忽略了时区偏移量。中的小时只是字符串中的小时。小时根本不会根据时区偏移量进行修改。据说这就是非标准成员的用途,但是在阅读时,我似乎得到了一个巨大的值,这绝对比任何UTC偏移量都要大得多,所以我也不确定该怎么做。strptimestruct tmtm_gmoff

举个例子:

#define _XOPEN_SOURCE

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

int main()
{
    struct tm tm;
    time_t epoch;
    char buf[40];

    strcpy(buf, "2023-06-07 03:04:56 -0700");
    memset(&tm, 0, sizeof(tm));

    strptime(buf, "%Y-%m-%d %H:%M:%S %z", &tm);
    printf("Parsed datetime %s (hour %d, offset %lu)\n", buf, tm.tm_hour, tm.__tm_gmtoff);
    tm.tm_isdst = -1;
    setenv("TZ", "US/Eastern");
    epoch = mktime(&tm);
    printf("Parsed datetime -> epoch %lu\n", epoch); // 7:04AM UTC
    epoch = timegm(&tm);
    printf("Parsed datetime -> epoch %lu\n", epoch); // 3:04AM UTC
    return 0;
}

https://www.onlinegdb.com/online_c_compiler 上运行时,提供:

Parsed datetime 2023-06-07 03:04:56 -0700 (hour 3, offset 18446744073709526416)
Parsed datetime -> epoch 1686121496
Parsed datetime -> epoch 1686107096

请注意,字符串中的偏移量是任意的,系统上的本地时区也是任意的。例如,是太平洋时间,但系统可能是东部时间,这实际上与问题完全无关(即,在转换中不应使用本地时区,因为它无关紧要 - 应使用偏移量的时区 - 重要的是,本地时区不应混淆答案)。-0700-0700

上面,正确答案是 UTC 时间上午 10:04(字符串显然应该转换为什么)。盲目使用给出错误的答案,甚至更不对劲。问题似乎是这里没有考虑偏移量。如果 为偏移量添加了 +7 小时,或者如果根据 中的某个内容(例如 tm_gmtoff)向答案添加了 +7 小时,则使用的第二个答案是正确的。但这些事情似乎都没有发生。mktimetimegmtimegmstruct tmtimegmstruct tm

除了编写手动函数来解析时间字符串中并手动将此偏移量添加到 ,有没有更好的“内置”方法来使用标准函数执行此操作?(可移植性在这里并不是特别重要,只要它在 .鉴于这似乎是一种非常常见的转换类型,我认为一定有一种方法可以正确地做到这一点,而无需手动进行计算,使用 .我以为这就是目的,但似乎不是这样——我在这里错过了什么吗?%ztime_tglibcgmtimetm_gmtoff

c 时区偏移 strptime mktime

评论

0赞 chux - Reinstate Monica 6/8/2023
第 1 步:返回的指向什么?一个空间,还是什么?char *strptime()'\0'

答:

0赞 Craig Estey 6/8/2023 #1

几个问题......

  1. 签名 [因此打印不正确]__tm_gmtoff%lu
  2. __tm_gmtoff 设置正确(例如)。-7 * 3600
  3. 做是行不通的。它使用系统设置的本地时区。(例如,-0700 是美国/太平洋(?)夏令时,但我得到了 -0400(美国/东部夏令时)。setenv("TZ",...)
  4. timegm忽略 __tm_gmtoff
  5. 在 linux/glibc 上,符号是 [AFAICT]。tm_gmtoff
  6. 最好手动使用和应用以获得正确的时区。timegmtm_gmtoff

这是一些更正的代码(分阶段)。它可能仍然被打破。请务必阅读评论:

//#define _XOPEN_SOURCE
#define _GNU_SOURCE

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

void
sepline(const char *tag)
{

    printf("\n");
    for (int col = 1;  col <= 80;  ++col)
        putchar('-');
    printf("\n");
    printf("%s:\n",tag);
    printf("\n");
}

void
tmshow(const struct tm *tm,const char *tag)
{

    printf("TMX: %4.4d/%2.2d/%2.2d-%2.2d:%2.2d:%2.2d (%ld/%ld) (from %s)\n",
        tm->tm_year + 1900,tm->tm_mon + 1,tm->tm_mday,
        tm->tm_hour,tm->tm_min,tm->tm_sec,tm->tm_gmtoff,tm->tm_gmtoff / 3600,
        tag);
}

void
todshow(time_t tod,int gmtflg,const char *tag)
{
    struct tm tm;

    if (gmtflg)
        gmtime_r(&tod,&tm);
    else
        localtime_r(&tod,&tm);

    printf("\n");
    printf("TOD: %ld (from %s)\n",tod,tag);
    tmshow(&tm,tag);
}

void
orig(const char *buf)
{
    struct tm tm;
    memset(&tm, 0, sizeof(tm));

    sepline("ORIG");

    printf("BUF: %s\n",buf);
    strptime(buf, "%Y-%m-%d %H:%M:%S %z", &tm);
    printf("Parsed datetime %s (hour %d, offset %lu)\n",
        buf, tm.tm_hour, tm.tm_gmtoff);

    tm.tm_isdst = -1;
    setenv("TZ", "US/Eastern", 1);
    tmshow(&tm,"strptime");

    time_t epoch_mktime = mktime(&tm);
    printf("Parsed mktime -> epoch %lu\n", epoch_mktime);   // 7:04AM UTC

    time_t epoch_timegm = timegm(&tm);
    printf("Parsed timegm -> epoch %lu\n", epoch_timegm);   // 3:04AM UTC

    time_t diff = epoch_mktime - epoch_timegm;
    printf("diff = %ld (%.3f)\n",diff,diff / 3600.0);
}

void
fix1(const char *buf)
{
    struct tm tm;
    memset(&tm, 0, sizeof(tm));

    sepline("FIX1");

    printf("BUF: %s\n",buf);
    strptime(buf, "%Y-%m-%d %H:%M:%S %z", &tm);
#if 0
    printf("Parsed datetime %s (hour %d, offset %ld/%ld)\n",
        buf, tm.tm_hour, tm.tm_gmtoff, tm.tm_gmtoff / 3600);
#endif

    tm.tm_isdst = -1;
    //setenv("TZ", "US/Eastern", 1);
    unsetenv("TZ");
    tmshow(&tm,"strptime");

    time_t epoch_mktime = mktime(&tm);
    //printf("Parsed mktime -> epoch %lu\n", epoch_mktime); // 7:04AM UTC
    todshow(epoch_mktime,0,"mktime");

    time_t epoch_timegm = timegm(&tm);
    //printf("Parsed timegm -> epoch %lu\n", epoch_timegm); // 3:04AM UTC
    todshow(epoch_timegm,1,"timegm");

    time_t diff = epoch_mktime - epoch_timegm;
    printf("diff = %ld (%.3f)\n",diff,diff / 3600.0);
}

void
fix2(const char *buf)
{
    struct tm tm;
    memset(&tm, 0, sizeof(tm));

    sepline("FIX2");

    printf("BUF: %s\n",buf);
    strptime(buf, "%Y-%m-%d %H:%M:%S %z", &tm);
    tmshow(&tm,"strptime");

    // NOTE: timegm ignores this -- so remember it
    time_t offset = tm.tm_gmtoff;

    //tm.tm_gmtoff = 0;
    time_t epoch_timegm = timegm(&tm);
    todshow(epoch_timegm,1,"timegm");

    // adjust for timezone -- this produces correct GMT
    todshow(epoch_timegm - offset,1,"timegm+offset");

// NOTE/BUG: setting TZ does _not_ work
#if 0
    time_t epoch_mktime = epoch_timegm;
    epoch_mktime -= offset;
    setenv("TZ", "US/Pacific", 1);
    localtime_r(&epoch_mktime,&tm);
#endif
#if 1
    time_t epoch_mktime = epoch_timegm;
    //epoch_mktime += offset;
    //epoch_mktime += offset;
    gmtime_r(&epoch_mktime,&tm);
    tm.tm_gmtoff += offset;
    //tm.tm_gmtoff += offset;
#endif

    //printf("Parsed mktime -> epoch %lu\n", epoch_mktime); // 7:04AM UTC
    tmshow(&tm,"localtime_r");

    time_t diff = epoch_mktime - epoch_timegm;
    printf("diff = %ld (%.3f)\n",diff,diff / 3600.0);
}

int
main()
{
    char buf[40];

    // Pacific time???
    strcpy(buf, "2023-06-07 03:04:56 -0700");

    orig(buf);
    fix1(buf);
    fix2(buf);

    return 0;
}

以下是程序输出:


--------------------------------------------------------------------------------
ORIG:

BUF: 2023-06-07 03:04:56 -0700
Parsed datetime 2023-06-07 03:04:56 -0700 (hour 3, offset 18446744073709526416)
TMX: 2023/06/07-03:04:56 (-25200/-7) (from strptime)
Parsed mktime -> epoch 1686121496
Parsed timegm -> epoch 1686107096
diff = 14400 (4.000)

--------------------------------------------------------------------------------
FIX1:

BUF: 2023-06-07 03:04:56 -0700
TMX: 2023/06/07-03:04:56 (-25200/-7) (from strptime)

TOD: 1686121496 (from mktime)
TMX: 2023/06/07-03:04:56 (-14400/-4) (from mktime)

TOD: 1686107096 (from timegm)
TMX: 2023/06/07-03:04:56 (0/0) (from timegm)
diff = 14400 (4.000)

--------------------------------------------------------------------------------
FIX2:

BUF: 2023-06-07 03:04:56 -0700
TMX: 2023/06/07-03:04:56 (-25200/-7) (from strptime)

TOD: 1686107096 (from timegm)
TMX: 2023/06/07-03:04:56 (0/0) (from timegm)

TOD: 1686132296 (from timegm+offset)
TMX: 2023/06/07-10:04:56 (0/0) (from timegm+offset)
TMX: 2023/06/07-03:04:56 (-25200/-7) (from localtime_r)
diff = 0 (0.000)

评论

0赞 chux - Reinstate Monica 6/8/2023
“It may still be broken” --> --> time_t格式说明符。或者选择宽符号类型time_t epoch_mktime ... printf("Parsed mktime -> epoch %lu\n", epoch_mktime);
1赞 Jonathan Leffler 6/8/2023
简单地在环境中设置是行不通的,但如果在设置环境后调用,则应使用时区变量的当前值。TZtzset()