程序员的困扰:C/C++中将UTC时间转换为UNIX时间戳竟如此复杂?

来源:网络时间:2025-01-22 18:06:48

程序员的噩梦:用C/C++把UTC时间转成UNIX时间戳竟然这么难?

时间处理的挑战与解决方案

编程中,时间处理看似平常,却隐藏着无数的坑点。本文以 C 或 C++ 中将 UTC 时间字符串转换为 UNIX 时间戳为例,探讨其中的难点及最优解决方案。

要将类似“Fri, 17 Jan 2025 06:07:07”这样的 UTC 时间字符串转换为 1737094027(即从 1970-01-01 00:00:00 UTC 开始的秒数),看起来似乎不难。然而,当你真正尝试时,会发现 POSIX 时间处理函数在各种 C 库及其衍生语言中存在许多让人意想不到的行为。尽管 C 和 UNIX 世界有许多优秀的设计,但时间处理显然不是其中之一。不过,仍有一些可行的方法可以解决问题。

快速解读(TL;DR)

1. 避免调用 setlocale():如果你从未调用过 setlocale(),可以直接使用 strpTIMe() 来解析 UTC 时间字符串。

2. 避免 %z 或 %Z 格式符:解析时请勿使用这两个格式符。

3. 转换为 UNIX 时间戳:将 strptime() 解析后生成的 struct tm 结构体传递给 timegm()(在 windows 上使用 mkgmtime()),即可得到对应的 UNIX 时间戳。

4. 如果使用了 setlocale(),需要更复杂的处理,具体解决方案将在下文中解释。

5. C++ 提供了更好的时间处理支持,这也可以从 C 中借用。

时间点的复杂性

即便忽略闰秒和广义相对论的影响,时间本身就足够复杂了。当我们将人类行为和政治因素引入时间处理时,事情会变得异常棘手。

例如,在阿姆斯特丹,“2025 年 3 月 30 日 02:20”这个时间点在当地根本不存在:

```shell

$ TZ=Europe/Amsterdam date -d '20250330 01:59:59'

Sun Mar 30 01:59:59 AM CET 2025

$ TZ=Europe/Amsterdam date -d '20250330 02:30:00'

date: invalid date ‘20250330 02:30:00’

```

由于夏令时的切换,时间会直接从 01:59:59 跳到 03:00:00。因此,工具无法解析“02:30:00”,因为在那一天的阿姆斯特丹,这个时间点根本不存在。

而对于“2024年10月27日 02:30”这个时间点,情况变得更加复杂。因为夏令时的结束,在 02:59:59 的下一秒,时间会重新变为 02:00:00。这意味着当地会出现两个都被称为“02:00”的时间点。工具在处理这种情况时开始做出一些看似任意的选择:

```shell

$ TZ=Europe/Amsterdam date -d '20241027 01:59:59' +"%Y-%m-%d %H:%M:%S %s %z"

2024-10-27 01:59:59 1729987199 +0200

$ TZ=Europe/Amsterdam date -d '20241027 02:00:00' +"%Y-%m-%d %H:%M:%S %s %z"

2024-10-27 02:00:00 1729990800 +0100

```

你看,当解析 02:00:00 时,GNU date 工具选择了第二个出现的时间点。据我观察,这可能是因为我在一月份运行了这个命令。如果在四月份运行,它可能会选择第一个 02:00:00 实例。是不是让人有些搞不懂?

POSIX 的时间概念

最有用的时间表示方式是用某个已知“纪元”之后或之前的秒数来指定时间点。例如:

- POSIX/Unix 的纪元是 1970-01-01 00:00:00 UTC

- gps 的纪元是 1980-01-06 00:00:00 UTC

- Galileo(欧盟版 GPS)的纪元是 1999-08-21 23:59:47 UTC

- 北斗系统的起始历元是 2006-01-01 00:00:00 UTC

GPS、Galileo 和北斗系统明智地忽略了闰秒,将这些问题留给人类去处理。我们偏爱 POSIX/Unix 的 time_t 是有充分理由的。它几乎不会有任何歧义,除了在闰秒期间——而闰秒可能再也不会出现了。

然而,人类难以理解诸如 1737214750 这样的数字。因此,我们需要将时间戳与包含月份等复杂概念的“人类友好”时间表示相互转换。为此,UNIX 提供了 struct tm,用于存储“细分时间”:

```cpp

struct tm {

int tm_sec; / Seconds [0, 60] /

int tm_min; / Minutes [0, 59] /

int tm_hour; / Hour [0, 23] /

int tm_mday; / Day of the month [1, 31] /

int tm_mon; / Month [0, 11] (January = 0) /

int tm_year; / Year minus 1900 /

int tm_wday; / Day of the week [0, 6] (Sunday = 0) /

int tm_yday; / Day of the year [0, 365] (Jan/01 = 0) /

int tm_isdst; / Daylight savings flag /

long tm_gmtoff; / Seconds East of UTC /

const char tm_zone; / Timezone abbreviation /

};

```

标准规定 struct tm 至少要包含这些字段,但实现中可能还会有其他字段。然而,现在这个结构体显然是“过度定义”的。例如,星期几(tm_wday)和每年的第几天(tm_yday)完全可以从其他字段推导出来。而 tm_gmtoff、tm_zone 和 tm_isdst 的意义定义不明确,使用时往往会造成困惑。

有趣的是,苏联的 GLONASS 卫星导航系统并没有采用纪元时间戳的方法,而是基于“莫斯科标准时间”的 struct tm,包括闰秒。这种设计据说引发了许多问题,也算是“自作自受”。

mktime() 的作用

struct tm 的一个重要用途是作为 mktime() 的输入。mktime() 的部分功能是将“根据你当地时区的细分时间”转换为 UNIX 时间戳(epoch 时间戳)。然而,mktime() 的作用远不止于此!

根据 Linux 的 glibc 手册页,mktime() 的描述相当模糊。而 IEEE Std 1003.1-2024 规范则用了更多(令人泄气的)文字来解释它。mktime() 不会处理 tm_gmtoff 或 tm_zone。其输入仅限于:tm_year、tm_mon、tm_mday、tm_hour、tm_min、tm_sec 和 tm_isdst。tm_isdst 也有特殊处理的情况,譬如 tm_isdst 可以设置为负值,表示让 mktime() 自动判断指定时间是否处于夏令时。

如上所述,时间问题其实在现实中很复杂。例如,如果想将日期调整一周,你可以简单地向 time_t 时间戳添加 604800 秒。但如果这种调整跨越了夏令时边界,你的下午两点约会可能会变成下一周的下午一点或三点。这显然不是人类期望的结果。

mktime() 不仅返回一个 time_t 值,还会规范化传入的 struct tm。截至 2024 年,关于如何规范化的规则已经明确。例如,要计算“下一周的同一时间”,可以将当前时间加上 7 天(tm.tm_mday += 7),然后再次调用 mktime()。即使你构造出了一个像“3月35日”这样的日期,mktime() 也会将其修正为有效日期。

然而,当我们实际这样做时,却发现它不起作用。

文章内容来源于网络,不代表本站立场,若侵犯到您的权益,可联系我们删除。(本站为非盈利性质网站) 联系邮箱:rjfawu@163.com