我希望将这个问题及其答案作为处理夏令时的最终指南,特别是处理实际转换时。

如果你有什么要补充的,请做

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

例如,一个订单接受系统中的业务规则取决于订单的时间——如果时钟发生变化,规则可能就不那么清晰了。应如何保持订单的时间?当然,有无数的场景——这只是一个例证。

你是如何处理日光节约问题的?您的解决方案包含哪些假设?(在此处查找上下文)

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

你尝试了什么但没有奏效?为什么不起作用?

我会对编程、操作系统、数据持久性和其他相关方面感兴趣。

一般的答案很好,但我也希望看到细节,特别是如果它们只在一个平台上提供的话。


当前回答

对于PHP:

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

然而,PHP的更新频率不如Olson DB,因此仅使用PHPs时区转换可能会给您留下过时的DST信息,并影响数据的正确性。虽然这种情况预计不会经常发生,但如果您在全球拥有大量用户,这种情况可能会发生。

要解决上述问题,请使用timezonedb pecl包。它的功能是更新PHP的时区数据。根据更新的频率安装此软件包。(我不确定这个软件包的更新是否完全遵循奥尔森的更新,但它的更新频率似乎至少非常接近奥尔森更新的频率。)

其他回答

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

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

时间的“物理”瞬间是物理学处理的连续宇宙时间线中的一个点(当然忽略了相对论)。例如,如果可以忽略闰秒,则可以在UTC中对这个概念进行适当的编码。

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

这个问题令人困惑,因为我们可以互换地谈论这些类型的事件,并且因为公民时代会受到政治变化的影响。这个问题(以及区分这些概念的必要性)在未来的事件中变得更加明显。示例(摘自我在这里的讨论。

约翰在日历上记录了某个日期的事件提醒2019-Jul-27,10:30:00,TZ=智利/圣地亚哥,(已偏移GMT-4,因此它对应于UTC 2019-Jul-27 14:30:00)。但总有一天未来,该国决定将TZ偏移量改为GMT-5。

现在,当这一天到来。。。该提醒是否应在

A) 2019-Jul-27 10:30:00智利/圣地亚哥=UTC时间2019-Jur-27 15:30:00?

or

B) 2019-Jul-27 9:3:00智利/圣地亚哥=UTC时间2019-Jul-27 14:30:00?

没有正确的答案,除非你知道约翰在概念上的意思当他告诉日历“请在2019年7月27日10:30:00给我打电话TZ=智利/圣地亚哥”。

他是说“公民约会时间”吗10:30“)?在这种情况下,A)是正确答案。

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

一些Date/Time API正确地实现了这一区别:其中的Jodatime是下一个(第三个!)Java DateTime API(JSR 310)的基础。

仅举一个例子来证明,处理时间是描述的巨大混乱,你永远不会自满。在本页的几个地方,闰秒被忽略了。

几年前,Android操作系统使用GPS卫星获取UTC时间参考,但忽略了GPS卫星不使用闰秒的事实。没有人注意到,直到除夕夜出现了混乱,苹果手机用户和安卓手机用户相隔约15秒进行倒计时。

我想它已经被修复了,但你永远不知道这些“小细节”什么时候会回来困扰你。

答案和其他数据摘要:(请添加您的答案)

Do:

无论何时,只要你指的是一个确切的时刻,都要根据不受夏令时影响的统一标准坚持时间。(GMT和UTC在这方面相当,但最好使用UTC一词。请注意,UTC也被称为祖鲁时间或Z时间。)相反,如果您选择使用本地时间值保存一个(过去的)时间,请包括该特定时间与UTC的本地时间偏移量(该偏移量可能会在一年中发生变化),以便以后可以明确地解释时间戳。在某些情况下,您可能需要同时存储UTC时间和等效的本地时间。通常这是通过两个单独的字段来完成的,但有些平台支持datetimeoffset类型,可以将这两个字段存储在一个字段中。当将时间戳存储为数值时,请使用Unix时间-这是自1970-01-01T00:00:00Z以来的整秒数(不包括闰秒)。如果需要更高的精度,请改用毫秒。该值应始终基于UTC,不进行任何时区调整。如果以后可能需要修改时间戳,请包括原始时区ID,以便确定偏移量是否与记录的原始值发生了变化。在安排未来事件时,通常首选当地时间而不是UTC,因为偏移量通常会发生变化。请参阅答案和博客文章。存储整个日期(如生日和周年纪念日)时,不要转换为UTC或任何其他时区。如果可能,请存储不包含时间的仅日期数据类型。如果这种类型不可用,请确保在解释值时始终忽略一天中的时间。如果您不能保证一天中的时间会被忽略,请选择中午12:00,而不是午夜00:00作为当天更安全的代表时间。请记住,时区偏移并不总是整数小时(例如,印度标准时间为UTC+05:30,尼泊尔使用UTC+05:45)。如果使用Java,请将Java.time用于Java 8和更高版本。java.time的大部分功能在ThreeTen Backport库中被向后移植到java 6和7。进一步适用于ThreeTenABP库中的早期Android(<26)。这些项目正式取代了现在处于维护模式的久负盛名的Joda Time。Joda Time、ThreeTen Backport、ThreeTen-Extra、java.Time类和JSR 310由同一个人Stephen Colebourne领导。如果使用.NET,请考虑使用Noda Time。如果使用没有Noda Time的.NET,请考虑DateTimeOffset通常比DateTime更好。如果使用Perl,请使用DateTime。如果使用Python 3.9或更高版本,请使用内置的zoneinfo处理时区。否则,请使用dateutil或arrow。通常可以避免使用较旧的pytz库。如果使用JavaScript,请避免使用旧的moment.js或moment时区库,因为它们不再被主动维护。有关详细信息,请参阅Moment.js项目状态。相反,考虑Luxon、date fns、day.js或js joda。如果使用PHP>5.2,请使用DateTime和DateTimeZone类提供的本地时区转换。使用DateTimeZone::listAb缩略语()时要小心-请参阅答案。要使PHP保持最新的Olson数据,请定期安装timezonedb PECL包;见答案。如果使用C++,请确保使用正确实现IANA时区数据库的库。其中包括cctz、ICU和霍华德·希南特的“tz”图书馆。在C++20中,后者被采用到标准<chrono>库中。不要将Boost用于时区转换。尽管其API声称支持标准IANA(又称“zoneinfo”)标识符,但它将它们粗略地映射到POSIX风格的数据,而没有考虑每个区域可能发生的丰富变化历史。(此外,该文件已无法维护。)如果使用Rust,请使用chrono。大多数业务规则使用民用时间,而不是UTC或GMT。因此,在应用应用程序逻辑之前,计划将UTC时间戳转换为本地时区。请记住,时区和偏移不是固定的,可能会改变。例如,历史上,美国和英国使用相同的日期来“向前冲”和“向后冲”。然而,在2007年,美国改变了时钟的日期。这意味着一年中的48周,伦敦时间和纽约时间相差5小时,4周(春季3小时,秋季1小时)相差4小时。在涉及多个区域的任何计算中,请注意类似的项目。考虑时间类型(实际事件时间、广播时间、相对时间、历史时间、重现时间),您需要存储哪些元素(时间戳、时区偏移和时区名称)以进行正确检索-请参阅本答案中的“时间类型”。让您的操作系统、数据库和应用程序tzdata文件在它们与世界其他地方之间保持同步。在服务器上,将硬件时钟和操作系统时钟设置为UTC,而不是本地时区。无论前面的要点是什么,服务器端代码(包括网站)都不应该期望服务器的本地时区是任何特定的。见答案。更喜欢在应用程序代码中逐个处理时区,而不是通过配置文件设置或默认值进行全局处理。在所有服务器上使用NTP服务。如果使用FAT32,请记住时间戳存储在本地时间,而不是UTC。当处理重复发生的事件(例如每周电视节目)时,请记住,时间会随着夏令时的变化而变化,并且不同时区的时间也会不同。始终将日期时间值查询为下限包含,上限不包含(>=,<)。

不要:

不要将“时区”(如美国/纽约)与“时区偏移”(如-05:00)混淆。它们是两种不同的东西。请参见时区标签wiki。不要使用JavaScript的Date对象在较旧的web浏览器中执行日期和时间计算,因为ECMAScript 5.1及更低版本有一个设计缺陷,可能会错误地使用夏时制。(这在ECMAScript 6/2015中已修复)。永远不要相信客户的时钟。这很可能是不正确的。不要告诉人们“在任何地方都要使用UTC”。这一广泛的建议是对本文档前面描述的几种有效场景的短视。相反,对正在处理的数据使用适当的时间参考。(时间戳可以使用UTC,但未来时间调度和仅日期值不应使用。)

测试:

测试时,请确保您测试了西半球、东半球、北半球和南半球的国家(事实上,在全球每四分之一的地区,共有4个地区),无论是否正在进行夏令时测试(给出8个),以及一个不使用夏令时的国家(另外4个国家覆盖所有地区,共12个)。测试DST转换,即当您当前处于夏季时,选择冬季的时间值。测试边界情况,例如UTC+12时区,夏令时,使当地时间UTC+13在夏季,甚至UTC+13的地方在冬季测试所有第三方库和应用程序,确保它们正确处理时区数据。至少测试半小时时区。

参考:

堆栈溢出上的详细时区标记wiki页面Olson数据库,又名Tz_databaseIETF奥尔森数据库维护程序草案时区和夏令时的来源ISO格式(ISO 8601)UnicodeConsortium提供的Olson数据库与Windows时区ID之间的映射维基百科上的时区页面StackOverflow问题标记为dstStackOverflow问题标记了时区处理DST-Microsoft DateTime最佳实践维基百科上的网络时间协议

其他:

游说你的代表结束DST这一令人厌恶的行为。我们总是希望。。。地球标准时间大厅

虽然我没有尝试过,但我认为时区调整的方法很有说服力,如下所示:

以UTC格式存储所有内容。创建具有三列的表TZOffsets:RegionClassId、StartDateTime和OffsetMinutes(int,以分钟为单位)。

在表中,存储本地时间更改的日期和时间列表,以及更改的程度。表中区域的数量和日期的数量取决于您需要支持的日期范围和世界范围。将其视为“历史”日期,尽管日期应包括未来,但实际的限制。

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

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

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

这些数据可以从公共域tz数据库中提取出来。

这种方法的优点和脚注:

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

现在,这种方法或任何其他方法的唯一缺点是,从本地时间到GMT的转换并不完美,因为任何与时钟有负偏移的DST变化都会重复给定的本地时间。恐怕没有简单的方法来处理这个问题,这也是存储当地时间首先是坏消息的原因之一。

商业规则应始终适用于民事诉讼时间(除非法律另有规定)。请注意,公民时间是一团糟,但这是人们使用的时间,所以这才是最重要的。

在内部,将时间戳保持在从大纪元开始的民用时间秒中。纪元并不特别重要(我喜欢Unix纪元),但它确实比其他纪元更容易。假装闰秒不存在,除非你正在做一些真正需要它们的事情(例如,卫星跟踪)。时间戳和显示时间之间的映射是应用DST规则的唯一点;规则经常变化(在全球范围内,一年几次;指责政客),所以你应该确保你没有硬编码地图。奥尔森的TZ数据库是无价的。