65.9K
CodeProject 正在变化。 阅读更多。
Home

时间格式转换轻松搞定

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.87/5 (60投票s)

2011年1月8日

CPOL

15分钟阅读

viewsIcon

169012

downloadIcon

2436

关于常见 Windows 时间格式的转换和思考

引言

Windows 使用不同的时间格式,并且它们之间的转换路径有限。本文提供了最常见的时间格式之间的全面转换,并讨论了日期和时间为什么如此复杂以及如何保持理智。本文不包含日期和时间的文本表示。

为了更新,我添加了章节添加或修改的日期说明。

 

时间是最简单的事情

这本应是我文章的标题。由于 Joseph M. Newcomer 已经有一篇同名文章(文章链接),我只能使用您看到的这个标题。

日期和时间的处理是容易出错的领域之一,因为该问题的复杂性常常被低估。从Zune 错误到无数未命名的奇怪现象,我们对日期和时间的度量包含许多不易进行简单计算的约定,并且暗示的假设并不总是成立。我将从这些奇怪的现象开始。

闰年

闰年用于使日历年与太阳年保持同步。如果年份能被 4 整除,则插入一天(2 月 29 日),除非该年份能被 100 整除但不能被 400 整除。

isLeapYear = (year % 400 == 0) || ( (year % 4 == 0) && !(year % 100 == 0) )

 1996 年和 2000 年是闰年,但 1900 年不是。日期/时间 API 通常会考虑闰年,例如在计算时间跨度时,但并不总是遵循世纪规则。请记住完整的规则。

闰秒

闰秒用于使日历日与地球日保持同步。在 6 月 30 日或 12 月 31 日会插入一秒。最后一次发生在 2008 年 12 月 31 日 23:59:60。这在全球同一时间发生,因此在日本就是 1 月 1 日 08:59:60。是的,这些是有效的时间。
由于底层效应不可预测,闰秒会提前 6 个月公布。

大多数时间 API 都**忽略**闰秒——即,像time_t这样的连续格式会回退一秒,或者有一个双倍长的秒。

时区

帮助大家一致同意 6:30 太早。
世界被划分为大致与经度相关的时区,并根据政治边界进行了调整。这很好,否则您的卧室可能已经是 6:30,而办公室已经 7:30 了。

“无时区”——标准时间是世界协调时间 (UTC)。时区通过与此时间偏移来描述。

本初子午线(零度经线)穿过伦敦附近的格林威治,所以伦敦与 UTC 没有时区偏移。柏林是 +1 小时——也就是说,柏林比伦敦晚一小时。有些地区有半小时偏移(例如印度 +5 ½)。中国在地理上横跨 4 个时区,但只有一个统一的时间。

与本初子午线相对的是日期变更线,越过它会让您进入未来一天或返回一天。不过,这不能阻止脱发。

夏令时 (DST)

别跟我提这个——总之,它是昆虫收集者提出的,这样您就能获得一小时的下班日光。出于多种原因的支持和反对,时钟在春天向前拨快一小时,在秋天调回。时间和具体日期因地点而异,并且经常变化。以色列通过议会辩论决定了日期和时间,以及今年是否实行夏令时。这意味着静态的夏令时规则定义很快就会过时。最好依靠您平台的更新机制来保证准确性。

UTC 偏移量

UTC 偏移量是本地时间和世界协调时间 (UTC) 之间的差值:本地时间 = UTC + 偏移量。UTC 偏移量通常包括时区偏移量以及任何额外的偏移量,如夏令时。

日历

以上所有内容仅适用于一个历法——格里高利历,幸运的是它是国际事实上的标准,尽管其他历法仍在使用中。格里高利历于 1582 年引入,用以纠正当时普遍使用的儒略历的误差,其中一项更改是将 10 月 4 日直接跳到 10 月 15 日,以纠正儒略历几个世纪以来积累的不准确性。如果您想处理此日期之前的时间,还有额外的麻烦在等着您。

一些仍在使用的历法包括希伯来历、波斯历和印度历,它们具有长度不一的天、闰月以及其他特性,会让上述问题显得简单。

这意味着什么?

常见的时段表达,如“两周”,其实际持续时间会因发生的时间而异。

在夏令时变化前后,相同的本地时间代表两个时间点,相隔一小时——而您无法确定它指的是哪一个。有一小时的本地时间戳不存在。

大多数环境在跟上这些变化方面存在问题,并且无法准确存储和考虑过去的更改,因此某些计算——尤其是两个时间点之间的时间跨度——会受到不准确性的影响。

清单

有些事情有助于保持理智

不要假设日期和时间处理是简单的。注意 UTC 与本地时间。使用经过测试的 API 而不是自己实现——除非您确切知道自己在做什么。测试。同行评审。测试您的测试。测试您的同行评审员。检查您的宠物是否有跳蚤。无论如何都要做。

本地时间与 UTC 的意识。本地时间戳不可比较,并且某些值是模棱两可的。UTC 时间戳用户难以理解。一个常见的错误是将本地时间戳值与 UTC 混淆。请为变量、API 和文件格式记录时间戳值是 UTC 还是本地时间。

比较 UTC 时间。如果您想“按日期排序”,或找出哪个事件先发生,请存储并使用 UTC。即使您的数据只发生在一个时区,夏令时也会弄乱顺序。

没有位置的本地时间毫无意义。我建议“了解”您随身携带的每个本地时间的 UTC 偏移量。在持久化或传输时间戳时,请包含生成时间戳时的 UTC 和本地偏移量。与此相关的三个时间表示可能非常有用:

  • 用于比较和排序的 UTC
  • 发送者的本地时间——例如,您的跨大西洋合作伙伴报告问题“总是在午夜左右”的日志
  • 接收者的本地时间——例如,查找发生在世界另一端“半小时前”的事件

不要依赖恒定的固定时间跨度。 不仅一个月的时间长度会变化,一天、一小时和一分钟的时间长度也会变化。更准确地说:时间跨度单位之间的转换取决于它们应用的实际时间戳。在允许输入口语化时间跨度(如“两个月”)时,需要考虑这一点。

时间转换库

支持的类型

类型 基本类型  分辨率 1 纪元 2 最早日期 4 最晚日期 4
time_t (32 位) 32 位有符号整数 1 秒 1.1.1970 1901 年 12 月 13 日  20:45:52 2038 年 1 月 19 日  03:14:07
time_t (64 位) 64 位有符号整数 1 秒 1.1.1970 时间不存在 太阳消失
struct tm 结构体 (36 字节) 1 秒 1.1.1900 1.1.1900 31.12.3000 23:59:59
SYSTEMTIME 结构体 (16 字节) 1 毫秒   1.1.1601 31.12.30827 23:59:59.999
OLE 日期/时间 double 0.5 秒 5 30.12.1899 1.1.100 5 31.12.9999
FILETIME 64 位无符号整数 3 100 纳秒 1.1.1601 1.1.1601 ~公元 60055 年 

1) 可以表示的最小单位。获取当前时间的方法可能依赖于以更大步长增加的底层计数器。例如,GetSystemTimeAsFileTime 可能以大约 15 毫秒的时间间隔返回当前时间。
2) 通常是日历开始的那一天。对于连续格式,这是 UTC 中表示为 0 的日期/时间。请注意,有些允许负值,表示在纪元之前。
3) 在一个由 2 个 32 位无符号整数组成的结构中
4) 某些 API 可能有更严格的限制,例如,某些转换函数不接受负的 time_t 值,64 位 time_t 通常限制在 3000 年 12 月 31 日。
5) OLE 日期/时间有点疯狂,详见下文。

OLE 日期特别棘手

OLE 日期 (VT_DATE) 本来可能很棒:它们指定了自纪元(1899 年 12 月 30 日)以来的天数,小数部分表示一天的小数部分(例如,0.5 表示中午)。浮点数允许在纪元附近进行计算以换取长期计算的精度。

与其他线性格式一样,夏令时和闰秒被忽略了。前者可以通过只存储 UTC 来避免,后者是所有格式的问题。但是,在实现中存在两个需要注意的奇怪之处:

负值需要拆分成整数和小数部分:例如,-2.5 表示自纪元以来倒数两天,再加上**向前**半天(即符号应用于整数部分,但不应用于小数部分)。这使得数值错误尤其麻烦,-2 表示“自纪元以来倒数两天”,最终是 1899 年 12 月 28 日。-1.9999999 表示倒回一天,然后几乎向前走一整天,最终停在 1899 年 12 月 30 日之前一点——相差近两天。

纪元的选择是为了使 Excel 与 Lotus 1-2-3 中的一个错误兼容,导致 1900 年 1 月和 2 月出现一些不准确的转换。有关详细信息,请参见下面的链接列表。

选择存储时间戳的格式

对于典型需求,FILETIME 似乎很有前途:高分辨率、足够的范围,并且使用 int64 进行转换和时间跨度计算非常简单。通过简单的 time_t 转换或到 time_t 的转换,可移植性足够好。

time_t (64 位)是默认选择,但您只有一秒的分辨率,这可能不足以满足某些应用程序的需求。它可能是最便携的。某些 API 将范围限制在 3000 年。不惜一切代价避免使用 32 位 time_t。

我建议避免 SYSTEMTIME,因为它是一个平台相关的结构。OLE DATE/TIME——尽管概念上很有趣——只提供半秒分辨率,对负值进行算术运算有误,而且您必须准备好应对数值精度问题。

另一个选择是将字符串存储为一种不区分文化格式。这需要可变长度,并且对于大量数据来说,额外的格式化和解析成本可能是禁止的,但您对实际支持的日期/时间 API 和格式的依赖性会更小。

我目前的赌注是 FILETIME。API 显示了一种将 FILETIME 和 UTC 偏移量打包到 int64 中而没有明显损失的方法,从而可以轻松处理。

API 参考

time_ref, time_const_ref

参数转换器,用于实现接受所有受支持类型的函数。
(您不必真正了解它们,但TimeConvert API 看起来很奇怪)
TimeConvert 在时间格式之间进行转换
FileTime, FileTime64 FILETIME结构和unsigned __int64之间进行转换
GetUTCOffset 返回本地时间和 UTC 之间的偏移量(以秒为单位) 
FmtUTCOffset  将 UTC 偏移量格式化为字符串(例如,-7 或 +5:30) 
PackFileTime64
UnpackFileTime
GetPackedFileTimeNow
将时间和 UTC 偏移量组合成一个 64 位整数。
获取当前时间和 UTC 偏移量作为 64 位整数

time_ref, time_const_ref 

此结构充当参数适配器,允许编写接受上述类型引用的函数。通常仅用于参数列表,您不应创建此类型的实例。请参阅TimeConvert中的一个使用它的方法。

struct time_ref
{
   ETimeFmt fmt;

   union
   {
      __time32_t * m_time32_t;
      __time64_t * m_time64_t;
      ...
   }; 

   time_const_ref(__time32_t & m)       : fmt(tftUnix32), m_time32_t(&m) {}
   time_const_ref(__time64_t & m)       : fmt(tftUnix64), m_time64_t(&m) {}
   ...
}

time_const_ref的实现类似,但接受const引用并保存const指针。

TimeConvert  

HRESULT TimeConvert(time_const_ref src, time_ref dst)

src中传递的时间转换为dst中传递的时间。如果转换失败,dst将保持不变。

Returns

  • S_OK 如果转换成功
  • E_INVALIDARG 如果自定义转换由于源值超出目标值范围而失败
  • 系统转换函数出错代码,如果该方法失败

FileTime, FileTime64

inline unsigned __int64 FileTime64(FILETIME ft)
inline FILETIME FileTime(unsigned __int64 ft64)

FILETIME结构和 64 位整数之间进行转换。

GetUTCOffset 

GetUTCOffset 计算当前本地时间和 UTC 之间的差值。这包括时区和夏令时(如果适用)。  

没有官方的标准方法可以检索这些数据,我使用的是以下算法:  

  • 获取 time_t 作为 UTC 
  • 使用 gmtime 将其转换为 struct tm  
  • 将此结构传递给 mktime,它假定它是本地时间,减去 UTC 偏移量并转换回 time_t 
  • 结果与 UTC 之间的差值就是 UTC 偏移量,只是符号相反。  

(我肯定希望这个算法没有任何问题。)

不过,这有一个烦人的小问题:在获取 UTC 和 UTC 偏移量之间,UTC 偏移量可能会发生变化。我不确定这在实践中有多重要,我可以想象在夏令时变化期间存在一个小的竞争条件,但我不会冒险。

因此,GetUTCOffset 可以选择性地返回用于此偏移量的 UTC 时间。为了保证一致性,计算至少重复两次,如果最后两次计算之间的 UTC 偏移量发生变化,则重复次数更多。当time_t * pNow_utc参数为 NULL 时,不进行重复。

如果您需要当前时间和 UTC 偏移量,但格式不同怎么办?  使用循环,如下例所示(针对 SYSTEMTIME): 

BOOL ok = FALSE;
SYSTEMTIME st = { 0 };
int utcOffset = 0, utcOffset2 = 0;

do
{
   utcOffset = GetUTCOffset();
   ok = GetSystemTime(&st);
   utcOffset2 = GetUTCOffset();
} while(ok && utcOffset != utcOffset2);

// !ok: GetSystemTime failed, otherwise, you can use st and utcOffset

FmtUTCOffset  

将 UTC 偏移量(以秒为单位,从 GetUTCOffset 返回)格式化为字符串。 当偏移量为 0 时,字符串为空,当分钟为 0 时,则省略分钟。秒始终被忽略。 

它打算与本地时间后的 UTC 前缀一起使用: 

string.Format(_T("%s UTC%s"),  myDateTimeString, FmtUTCOffset(utcOffset)); 

PackFileTime64, UnpackFileTime

典型的编码人员在处理文件格式几次后会发生什么?他会发明一种新的!这正是我在撰写本文时想要避免的。

PackFileTime64 将 FILETIME 值和 UTC 偏移量合并成一个单一的 64 位无符号整数

  • FILETIME 的分辨率从 100ns 降低到 12.8µs  (四舍五入到最接近的值)
  • UTC 偏移量四舍五入到最接近的 15 分钟倍数,并且必须在 -15..+16h 的范围内(目前,时区范围从 -12h 到 +14h)。

UTC 偏移量直接存储在低位,因为高分辨率很少需要甚至不可用。(在我的系统上,GetSystemTimeAsFileTime 以 15.6 毫秒的步长递增)。

GetPackedFileTimeNow

检索当前的 UTC 时间和 UTC 偏移量作为“打包的 int64”(如上所述)。

 

更多数据

典型增量

下表显示了连续类型在

时间增量(不包括闰秒、闰日)

 
类型  second  minute hour day 非闰年 2)  
time_t  60 3600 86400 604800 31536000  
OLE 日期/时间 1/86400
1,157407e-5
1/1440
6.94e-4
1/24
0.0416...
1 7 365
FILETIME  1e7  6e8 3.6e10  8.64e11  6.048e12  315360000000
3.1536e14 

1) 经验法则:一年大约有 PI 乘以 107 
2) 对于非常长的时间范围,您可以按每年 365.25 天计算,以考虑闰年

不同格式下的 UTC 与本地时间

类型 当前时间 (UTC) 当前时间 (本地) UTC 转本地 本地转 UTC
time_t time() - localtime mktime
OLE 日期/时间 - - - -
FILETIME - - FileTimeToLocalFileTime LocalFileTimeToFileTime
SYSTEMTIME GetSystemTime GetLocalTime SystemTimeToTzSpecificLocalTime TzSpecificLocalTimeToSystemTime

处理夏令时规则变化

为了处理不断变化的夏令时规则,Windows 提供了以下附加 API:

  • GetTimeZoneInformationForYear
  • GetDynamicTimeZoneInformation
  • SetDynamicTimeZoneInformation

我还没有评估它们在多大程度上涵盖了所有变化,不要指望有奇迹。

更多信息

测试和实现细节

最困难的不是实际的转换,而是编写测试、找出可靠的转换路径,以及用它们来检查不可靠的路径。这是我使用的转换矩阵:

转换矩阵

使用了以下转换矩阵。绿色表示安全的转换(简单的复制或由系统库提供),红色表示自定义计算。连续格式(time_tFILETIMEOLE DATE / TIME)之间的转换是线性的,即:
 timeB = timeA * factor + offset.
因此,对每次转换在两个点进行了检查。

(表格也包含在下载的 OpenCalc 文档中)。

Time Conversion overview 

历史

这段代码的触发是因为遇到了一个棘手的错误,该错误在一年中的一半时间里才会通过单元测试发现(设置了struct tmisdst成员)。

  • 2011 年 1 月 - 首次发布
  • 2011 年 11 月 - 修正了文本中的一个错误值,增加了对 UTC 偏移量的支持
  • 2011 年 12 月 - 稍微整理了一下文本流程
  • 2012 年 4 月 - 当你让一个程序员处理半打时间格式时会发生什么?他会发明一种新的!
    添加了 PackFileTime64、UnpackFileTime、GetPackedFileTimeNow

最后

这是一个非常枯燥的话题,所以我引用了电影中一句我喜欢的关于时间的台词(为了国际观众已适当修改)。

让我解释几件事。

时间是短暂的。这是第一件事。

对于黄鼠狼来说,时间是黄鼠狼。
对于英雄来说,时间是英雄式的。
如果你温柔,你的时间就是温柔的。
如果你匆忙,时间就飞逝。
如果你是它的主人,时间就是仆人。
如果你是它的狗,时间就是你的神。
我们是时间的创造者
时间的受害者
以及时间的杀手。

时间是永恒的。
这是第二件事。

你就是时钟,卡西尔。

出自:温·温德斯 1993 年电影《直到世界尽头》

© . All rights reserved.