夏令时和时区最佳实践

我希望使这个问题及其答案成为处理夏时制(尤其是处理实际变更)的权威指南。

如果您要添加任何内容,请执行

许多系统都依赖于保持准确的时间,问题在于夏时制导致的时间变化 - 向前或向后移动时钟。

例如,一个在接订单系统中有取决于订单时间的业务规则 - 如果时钟改变,则规则可能不太清楚。订购时间应如何保留?当然,有无数种情况 - 这只是一种说明。

  • 您如何处理夏令时问题?
  • 解决方案中包含哪些假设? (在此处查找上下文)

同样重要,如果不是这样的话:

  • 您尝试了什么却不起作用?
  • 为什么不起作用?

我会对编程,操作系统,数据持久性以及该问题的其他相关方面感兴趣。

一般的答案很好,但是我也想看看细节,特别是如果它们仅在一个平台上可用。

答案

我不确定我可以在上面的答案中添加什么,但是我有几点建议:

时间类型

您应该考虑四个不同的时间:

  1. 活动时间:例如,国际体育赛事发生的时间或加冕 / 死亡 / 等时间。这取决于事件的时区,而不取决于查看器。
  2. 电视时间:例如,某个特定的电视节目在世界各地的当地时间晚上 9 点播出。考虑在您的网站上发布结果(例如 “美国偶像”)时很重要
  3. 相对时间:例如:此问题在 21 小时内有悬空赏金。这很容易显示
  4. 循环播放时间:例如:即使 DST 更改,电视节目也每周星期一晚上 9 点播放。

也有历史 / 备用时间。这些很烦人,因为它们可能无法映射回标准时间。例如:朱利安(Julian)日期,根据土星(Klingon)日历上的阴历确定日期。

在 UTC 中存储开始 / 结束时间戳记效果很好。对于 1,您需要一个事件时区名称 + 与事件一起存储的偏移量。对于 2,您需要在每个区域存储一个本地时间标识符,并为每个查看者存储一个本地时区名称 + 偏移量(如果您处于紧要关头,可以从 IP 导出该时间)。对于 3,以 UTC 秒存储,不需要时区。 4 是 1 或 2 的特殊情况,具体取决于它是全局事件还是本地事件,但是您还需要存储一个时间戳记创建的事件,以便您可以知道在创建此事件之前或之后是否更改了时区定义。如果需要显示历史数据,这是必需的。

储存时间

  • 始终将时间存储在 UTC 中
  • 转换为显示的本地时间(本地由用户查看数据定义)
  • 存储时区时,您需要名称,时间戳和偏移量。这是必需的,因为政府有时会更改其时区的含义(例如:美国政府更改了夏令时的日期),并且您的应用程序需要妥善处理…… 例如:当 DOST 规则前后都出现 LOST 事件时的确切时间戳改变了。

偏移量和名称

以上是一个示例:

足球世界杯决赛比赛于 2010 年 7 月 11 日世界标准时间 19:00 在南非(UTC + 2--SAST)举行。

利用此信息,即使南非时区定义发生了变化,我们也可以从历史上确定 2010 年 WCS 决赛的确切时间, 能够在观众查询数据库时向其本地时区的查看者显示该时间。

系统时间

您还需要使您的 OS,数据库和应用程序 tzdata 文件彼此之间以及与世界其他地方保持同步,并在升级时进行广泛的测试。您依赖的第三方应用未正确处理 TZ 更改并非闻所未闻。

确保硬件时钟设置为 UTC,并且如果您在世界各地运行服务器,请确保其操作系统也配置为使用 UTC。当您需要从多个时区的服务器复制每小时轮换的 apache 日志文件时,这变得显而易见。仅当所有文件都使用相同的时区命名时,才按文件名对它们进行排序。这也意味着,当您从一个框切换到另一个框并需要比较时间戳时,您不必费心地进行日期数学运算。

另外,在所有机器上运行 ntpd。

客户群

永远不要相信从客户端计算机获得的时间戳是有效的。例如,Date:HTTP 标头或 javascript Date.getTime()调用。当用作不透明标识符时,或者在同一客户端上的单个会话中进行日期数学运算时,这些方法都很好,但是请勿尝试将这些值与服务器上的内容交叉引用。您的客户端不运行 NTP,并且不一定为他们的 BIOS 时钟配备可用的电池。

琐事

最后,政府有时会做非常奇怪的事情:

从 1909-05-01 到 1937-06-30,根据法律,荷兰的标准时间恰好比 UTC 提前 19 分 32.13 秒。无法使用 HH:MM 格式准确表示该时区。

好吧,我想我完成了。

这是一个重要且令人惊讶的棘手问题。事实是,持久性没有完全令人满意的标准。例如,SQL 标准和 ISO 格式(ISO 8601)显然不够。

从概念的角度来看,通常处理两种类型的时间日期数据,并且很容易区分它们(上述标准没有):“ 物理时间 ” 和 “ 民事时间 ”。

时间的 “物理” 时刻是物理处理的连续通用时间线中的一个点(当然,忽略相对论)。例如,此概念可以在 UTC 中进行适当的编码持久化(如果您可以忽略 leap 秒)。

“民事” 时间是遵循民事规范的日期时间规范:此处的时间点由一组日期时间字段(Y,M,D,H,MM,S,FS)加上 TZ(时区规范)完全指定(实际上,这也是一个 “日历”;但假设我们将讨论范围限制为公历)。时区和日历共同(原则上)允许从一种表示映射到另一种表示。但是民用和物理时刻在根本上是不同的量值类型,应在概念上将它们保持分开并以不同的方式对待(类比:字节和字符串数组)。

这个问题令人困惑,因为我们可以互换地谈论这些类型的事件,并且因为平民时代可能会发生政治变化。对于将来的事件,问题(以及区分这些概念的需求)变得更加明显。示例(摘自我在这里的讨论。

John 在他的日历中记录了日期为2019-Jul-27, 10:30:00 TZ 的Chile/Santiago某些事件的提醒(已抵消 GMT-4,因此对应于 UTC 2019-Jul-27 14:30:00 )。但是在将来的某一天,该国决定将 TZ 偏移量更改为 GMT-5。

现在,当一天到来时... 该提醒是否应在

A) 2019-Jul-27 10:30:00 Chile/Santiago = UTC time 2019-Jul-27 15:30:00

要么

B) 2019-Jul-27 9:30:00 Chile/Santiago = UTC time 2019-Jul-27 14:30:00

没有正确的答案,除非有人知道约翰在告诉日历 “请在2019-Jul-27, 10:30:00 TZ=Chile/Santiago给我打电话” 时的概念含义。

他的意思是 “民用日期时间”(“我城市的时钟说 10:30 时”)吗?在这种情况下,A)是正确的答案。

或者他的意思是 “时间的物理瞬间”,即我们宇宙连续时间线中的一个点,例如,“下一次日食发生时”。在这种情况下,答案 B)是正确的答案。

一些 Date / Time API 可以很好地实现这种区分:其中有Jodatime ,它是下一个(第三个!)Java DateTime API(JSR 310)的基础。

明确关注点的架构分离 - 要确切知道与用户交互的层,并且必须更改规范表示(UTC)的日期 / 时间。 非 UTC 日期时间是表示形式 (遵循用户本地时区), UTC 时间是模型 (后端和中间层保持唯一)。

另外,请确定您的实际受众是什么,您不必为什么服务以及在哪里划界 。除非您在那里确实有重要的客户,然后再考虑仅用于该地区的面向用户的服务器,否则请勿触摸异国日历。

如果您可以获取并维护用户的位置,请使用位置进行系统的日期时间转换(例如. NET 文化或 SQL 表),但如果日期时间对您的用户而言至关重要,则可为最终用户提供替代方法。

如果涉及历史审计义务 (例如准确说出 AZ 的 Jo 在 2 月前何时支付账单), 则保留 UTC 和本地时间作为记录(您的转换表将随时间变化)。

为大量数据定义时间参考时区 (例如文件,Web 服务等)。说东海岸公司在 CA 设有数据中心 - 您需要询问并知道它们用作标准是什么,而不是假设一个或另一个。

不要相信嵌入在日期时间文本表示形式中的时区偏移量,也不接受解析和跟踪它们。 而是始终要求必须明确定义时区和 / 或参考区 。您可以轻松接收带有 PST 偏移量的时间,但是该时间实际上是 EST,因为这是客户端的参考时间,并且记录只是在 PST 中的服务器上导出的。

您需要了解 Olson tz 数据库 ,该数据库可从ftp://elsie.nci.nih.gov/pub http://iana.org/time-zones / 获得 。每年对它进行多次更新,以应对世界上不同国家冬季和夏季(标准时间和夏时制)时间之间(以及是否)切换时间的最后一刻变化。在 2009 年,最新版本是 2009 年代;在 2010 年是 2010 年;在 2011 年是 2011 年;在 2012 年 5 月底,版本是 2012c。请注意,在两个单独的存档(tzcode20xxy.tar.gz 和 tzdata20xxy.tar.gz)中,有一组代码可以管理数据和实际时区数据本身。代码和数据均属于公共领域。

这是时区名称的来源,例如 America / Los_Angeles(以及诸如 US / Pacific 的同义词)。

如果需要跟踪不同的区域,则需要 Olson 数据库。正如其他人所建议的那样,您还希望以固定格式存储数据(通常选择 UTC)以及记录生成数据的时区的记录。您可能要区分时间与 UTC 的偏移量和时区名称;以后可以有所作为。另外,知道当前是 2010-03-28T23:47:00-07:00(美国 / 太平洋)可能会或可能不会帮助您解释值 2010-11-15T12:30 - 大概是在 PST 中指定的(太平洋标准时间)而不是 PDT(太平洋夏令时)。

标准的 C 库接口对于此类工作不是很有帮助。


奥尔森(Olson)的数据有所变化,部分原因是公元奥尔森(AD Olson)即将退休,部分原因是针对维护者的版权侵权提起了诉讼(现已撤销)。现在,时区数据库由IANA (互联网号码分配机构)负责管理,并且首页上有一个指向“时区数据库” 的链接 。现在,讨论邮件列表为tz@iana.org ;公告列表是tz-announce@iana.org

通常,在存储的时间戳中包括本地时间偏移(包括 DST 偏移) :如果您以后要在其原始时区(和 DST 设置)中显示时间戳,仅使用 UTC 是不够的。

请记住,偏移量并非始终是整数小时(例如,印度标准时间为 UTC + 05:30)。

例如,合适的格式是元组(unix 时间,以分钟为单位的偏移量)或ISO 8601

跨越 “计算机时间” 和 “人员时间” 的界限是一场噩梦。主要的问题是,关于时区和夏令时的规则没有任何标准。各国可以随时自由更改其时区和夏令时规则。

一些国家(例如以色列,巴西)每年决定何时设置夏令时,因此无法预先知道夏令时的生效时间。其他人对 DST 生效时间有固定的(ish)规则。其他国家并没有全部使用 DST。

时区不必与格林尼治标准时间整整一小时。尼泊尔为 + 5.45。甚至有 + 13 时区。这意味着:

SUN 23:00 in Howland Island (-12)
MON 11:00 GMT 
TUE 00:00 in Tonga (+13)

都是同一时间,但 3 天不同!

时区的缩写以及在 DST 中时的变化也没有明确的标准,因此您最终会遇到如下情况:

AST Arab Standard Time     UTC+03
AST Arabian Standard Time  UTC+04
AST Arabic Standard Time   UTC+03

最好的建议是尽可能远离当地时间,并坚持使用 UTC。仅在最后可能的时间转换为当地时间。

在进行测试时,请确保您测试的是西半球和东半球的国家(既有正在进行的 DST,也没有进行 DST 的国家)以及未使用 DST 的国家(共计 6 个)。

对于 PHP:

PHP> 5.2 中的 DateTimeZone 类已经基于其他人提到的 Olson 数据库,因此,如果您在 PHP 中而不是在 DB 中进行时区转换,则无需使用(难以理解的)Olson 文件。

但是,PHP 的更新频率不如 Olson DB 那样频繁,因此仅使用 PHP 的时区转换可能会给您带来过时的 DST 信息,并影响数据的正确性。尽管这种情况不会经常发生,但可能会发生,并且如果您在全球拥有大量用户,则可能发生。

要解决上述问题,请使用timezonedb pecl 包 。它的功能是更新 PHP 的时区数据。请在更新后频繁安装此软件包。 (我不确定此软件包的更新是否完全跟在 Olson 更新之后,但是似乎更新的频率至少与 Olson 更新的频率非常接近。)

如果您的设计可以容纳它,请避免一起进行本地时间转换!

我知道有些听上去很疯狂,但考虑到 UX:用户一眼就能看到,相对日期(今天,昨天,下周一)的处理时间比绝对日期(2010.09.17,9 月 17 日,星期五)要快。而且,当您考虑得更多时,时区(和 DST)的准确性越接近日期,就越重要now()因此now() ,因此,如果您可以用 +/- 1 或 2 周的相对格式来表示日期 / 日期时间,其余日期可以是 UTC,对于 95%的用户而言,它并不重要。

这样,您可以将所有日期存储在 UTC 中,并在 UTC 中进行相对比较,并且只需在相对日期阈值之外显示用户 UTC 日期即可。

这也可以应用于用户输入(但通常以更有限的方式)。从一个只有 {昨天,今天,明天,下一个星期一,下一个星期四} 的下拉列表中进行选择,对用户而言,它比日期选择器要简单得多。日期选择器是表单填充中最容易引起疼痛的部分。当然,这并非在所有情况下都适用,但是您可以看到,仅需一点点巧妙的设计就可以使其非常强大。

虽然我还没有尝试过,但我发现令人信服的时区调整方法如下:

  1. 将所有内容存储在 UTC 中。

  2. 用三列创建表TZOffsets :RegionClassId,StartDateTime 和 OffsetMinutes(整数,以分钟为单位)。

在表中,存储当地时间更改的日期和时间列表以及更改的时间。表中的区域数和日期数将取决于您需要支持的日期范围和世界范围。即使日期应该包括将来的某个实际限制,也可以将其视为 “历史” 日期。

当您需要计算任何 UTC 时间的本地时间时,只需执行以下操作:

SELECT DATEADD('m', SUM(OffsetMinutes), @inputdatetime) AS LocalDateTime
FROM   TZOffsets
WHERE  StartDateTime <= @inputdatetime
       AND RegionClassId = @RegionClassId;

您可能希望将此表缓存在您的应用程序中,并使用 LINQ 或其他类似方法进行查询,而不是访问数据库。

可以从公共领域的 tz 数据库中提取这些数据

此方法的优点和脚注:

  1. 没有规则融入代码中,您可以轻松调整新区域或日期范围的偏移量。
  2. 您不必支持所有日期或区域范围,可以根据需要添加它们。
  3. 区域不必直接对应于地缘政治边界,并且为了避免重复行(例如,美国的大多数州以相同的方式处理 DST),您可以具有广泛的 RegionClass 条目,这些条目在另一个表中链接到更传统的列表州,国家等
  4. 对于美国这样的情况,在过去几年中,夏令时的开始和结束日期已发生变化,这很容易处理。
  5. 由于 StartDateTime 字段也可以存储时间,因此可以轻松处理 2:00 AM 标准转换时间。
  6. 世界上并非每个地方都使用 1 小时 DST。这样可以轻松处理这些情况。
  7. 数据表是跨平台的,可以是一个单独的开源项目,几乎使用任何数据库平台或编程语言的开发人员都可以使用。
  8. 这可用于与时区无关的偏移量。例如,不时进行 1 秒调整以适应地球的自转,对公历及其内部的历史调整等。
  9. 由于这是在数据库表中,因此标准报表查询等可以利用数据,而无需花费业务逻辑代码。
  10. 如果需要,它也可以处理时区偏移,甚至可以考虑将区域分配给另一个时区的特殊历史情况。您所需要的只是一个初始日期,该日期以最小的开始日期为每个区域分配时区偏移量。这将需要为每个时区创建至少一个区域,但是您可以提出一些有趣的问题,例如:“1989 年 2 月 2 日凌晨 5:00,亚利桑那州尤马市和华盛顿西雅图市之间的当地时间有何不同?” (只需从另一个中减去一个 SUM())。

现在,这种方法或其他方法的唯一缺点是, 本地时间到 GMT 的转换并不完美,因为任何对时钟有负偏移的 DST 更改都会重复给定的本地时间。恐怕没有简单的方法可以解决这个问题,这是首先存储本地时间是坏消息的原因。