
最近,我正在执行将时区添加到团队维护的JS日历库中的任务。 我很清楚JavaScript中对无用时区的支持,但是我希望抽象现有数据对象将使解决大多数困难变得容易。
但是,我的梦想破灭了。 当我深入研究任务时,我意识到用这种语言处理时区确实很困难。 与简单地使用复杂的操作(日历功能)格式化时间显示和计算日期相比,实现更复杂的功能极其困难。 我在解决这个问题上获得了宝贵的经验,这带来了新的困难。
在本文中,我想讨论一下我遇到的问题以及如何解决。 在编写文本时,我意识到所有困难的原因是我对时区主题的理解不足。 鉴于这种认识,我建议首先详细讨论定义和标准,然后再继续使用JavaScript。
什么是时区?
时区是使用政府设置的相同本地时间的地理区域。 许多国家/地区完全与任何特定时区相关,在俄罗斯和美国等大州的领土上,使用了多个时区。 奇怪的是,尽管中国也足够大,但它只接受一个时区。 有时,当该国西部地区的日出开始于上午10点左右时,这会导致一些奇怪的情况。
GMT,UTC和偏移
格林尼治标准时间
韩国当地时间指定为
GMT+09:00
。 GMT代表格林威治标准时间(GMT),即英国格林威治皇家天文台的时钟。 它位于本初子午线上。 GMT系统的无线电信号于1924年2月5日开始广播,并于1972年1月1日成为世界标准。
世界标准时间
许多人认为GMT和UTC是同一件事,经常将它们用作可互换的系统。 但这是一个错误。 UTC系统出现在1972年,是一种补偿地球自转影响的方法。 该系统基于国际原子时间,该时间由铯原子的电磁振动频率计算得出。 换句话说,UTC是GMT的更准确替代。 尽管两个系统之间的实时差异很小,但对于软件开发人员而言,最好依靠UTC。
一个有趣的事实:当仍在开发UTC时,在英语国家中建议将其称为CUT(世界协调时间),而在法语国家中则建议将其称为TUC(Temps Universal Coordonn)。 但是,所有营地都无法获胜,他们同意将这两个提议的选项(C,T和U)拼写为系统UTC。
偏移量
UTC+09:00
+09:00
UTC+09:00
表示本地时间比标准UTC时间早9小时。 也就是说,在韩国晚上9点时,是UTC地区的中午。 标准UTC时间与本地时间之间的时差称为“偏移”,表示为正值或负值:
+09:00
,
-03:00
等。
在许多国家/地区,习惯上给您的时区起唯一的名称。 例如,韩国的时区称为KST(韩国标准时间),其偏移量表示为
KST = UTC+09:00
。 但是,偏移量
+09:00
不仅在韩国使用,而且在日本,印度尼西亚和许多其他国家中也使用,因此,偏移量和皮带之间的关系不是表示为1:1,而是表示为1:N。
此处提供了
+09:00
偏移的国家/地区列表。
某些胶印不仅工作数小时。 例如,在朝鲜,标准时间是
+08:30
,而在澳大利亚,某些地区使用的是
+8:45
,
+8:45
+09:30
和
+10:30
。
这里是UTC偏移的完整列表。
时区==偏移量?
就像我说的,我们使用时区名称(KST,JST),它们的偏移量可以互换使用,而无需区分它们。 但是,考虑同时发生的时间和特定区域的移动将是错误的。 有以下几个原因:
夏令时(DST)
在某些国家/地区,这个名词未知,但是在许多州,主要是在欧洲,实行夏令时。 为此,采用国际标准时间DST-夏令时。 这意味着夏天将时钟比标准时间提前一小时。
例如,加利福尼亚在冬季使用PST(太平洋标准时间),在冬季使用PDT(太平洋夏令时间,
UTC-07:00
)。 在美国和加拿大,术语太平洋时间(PT,Pacific Time)用于使用两个时区的地区。
夏季时间什么时候开始和结束? 这一切都取决于国家。 例如,在2006年之前的美国和加拿大,从4月第一个星期日的凌晨2点到10月的最后一个星期日的凌晨12点使用DST。 从2007年开始,夏季时间开始从3月第二个星期日的2个晚上到11月第一个星期日的2个晚上。 在欧洲,不同国家/地区会根据每个时区逐步使用DST。
时区在变化吗?
每个国家/地区都会确定要使用的时区,因此当地时间可能会因政治和/或经济原因而改变。 例如,在美国,由于乔治·W·布什(George W. Bush)于2005年发起了能源政策,DST的边界在2007年发生了变化。 埃及和俄罗斯曾经改用夏令时,但自2011年起便放弃了。
在某些情况下,政府不仅可以更改夏令时,还可以更改标准时间。 例如,较早的萨摩亚使用偏移量
UTC-10:00
,但是后来由于与澳大利亚和新西兰的
UTC+14:00
,他们改用
UTC+14:00
来减少贸易损失。 据
世界各地的报纸报道,这一决定导致该国全天丧命-2011年12月30日。
在荷兰,自1909年以来一直使用
+0:19:32.13
的偏移量,自1937年以来该国家将其偏移量设置为
+00:20
,而从1940年更改为
+01:00
,此后主要时间并未更改。
时区1:N偏移
因此,时区可以具有一个或多个偏移量。 哪个时间可以作为标准时间,取决于特定国家/地区当前的政治和/或经济原因。
在日常生活中,除非您尝试根据某些规则将这些数据系统化,否则这不会造成任何麻烦。 想象一下,您想使用某种偏差在智能手机上设置标准时间。 如果您居住在实行夏令时的地区,那么您的智能手机应该确切地知道何时来回旅行。 也就是说,您需要在一个时区(例如,太平洋时间)中建立标准时间与夏令时之间的关系。
但这不能通过几个简单的规则来完成。 例如,在2007年美国更改DST的开始和结束时,应该在2006年5月31日使用PDT(
-07:00
)作为标准时间,在2007年3月31日使用PDT(
-08:00
)作为标准时间。 事实证明,要引用特定时区,您需要了解时区更改的全部历史记录或夏令时规则的更改日期。
您可以说:“纽约时区是PST(
-08:00
)。” 但是,需要澄清一下:“纽约的当前时区是PST。” 而且,为了准确地实现系统,您需要使用更准确的表达式。 忘记术语“时区”。 您必须说:“现在在纽约,标准时间是PST。”
那么,应该使用什么代替偏移来确定特定区域的时区? 该区域的名称。 更准确地说,您应该在一个时区中将它们均等转换为夏令时和标准时间的那些地区分组。 您可以使用诸如PT(太平洋时间)之类的名称,但是它们仅结合当前的标准时间和夏令时,而不必考虑所有历史变化。 此外,由于PT仅在美国和加拿大流通,因此您需要依赖信誉良好的组织的既定标准来确保软件的通用性。
IANA时区数据库
我必须承认,有关时区的信息是数据库,而不是一组规则,因为该信息应包含所有相关的历史变化。 有几个标准数据库设计用于解决与时区有关的任务。 最常用的是
IANA时区数据库 ,通常称为tz数据库(或tzdata)。 该数据库包含有关全球标准时间和夏令时变化的历史数据。 此外,它经过组织,以便可以检查所有历史数据并确保从Unix时间(
1970.01/01 00:00:00
)开始的时间是准确的。 尽管您甚至可以在1970年之前在数据库中找到信息,但不能保证其准确性。
命名约定使用区域/位置规则。 大陆或海洋的名称(亚洲,美洲,太平洋)通常用作区域,主要城市的名称(首尔,纽约)用作位置。 原因是城市的寿命通常比国家长。 例如,韩国的时区为
Asia/Seoul
,日本为
Asia/Tokyo
。 尽管两个国家/地区使用相同的
UTC+09:00
偏移量,但是它们的本地时间却有所不同,因此将它们划分为不同的时区。
IANA基地由许多开发人员和历史社区组成。 最新发现的历史数据会立即输入其中,并且会更新当前策略,因此,当今的数据库可以被视为最可靠的数据源。 而且,许多Unix系统(包括Linux和MacOS)以及许多流行的编程语言(包括Java和PHP)都在后台使用它。
请注意,Windows使用
Microsoft数据库 。 但是,它在历史数据方面是不准确的,并且仅由Microsoft本身支持。 因此,该基础不如IANA基础可靠。
JavaScript和IANA
与时区相关的功能是通过JavaScript突然实现的。 默认情况下,该语言使用当前区域传送带(更准确地说,是在OS安装期间选择的传送带),并且无法更改它。 此外,即使JavaScript中的数据库标准规范也含糊不清,如果您决定处理ES2015规范,您自己会理解这一点。 关于本地时区和夏令时的可用性,只有几句含糊的陈述。 例如,DST定义如下:
ECMAScript 2015-夏令时调整 。
这是与实现有关的算法,它使用最佳的可用时区信息来确定DaylightSavingTA(t)本地夏时制设置,以毫秒为单位进行计算。 ECMAScript实现应帮助您更好地确定本地夏令时设置。
看起来他们只是在说:“伙计们,请尝试使其正常工作。” 除其他外,您必须解决与不同浏览器的兼容性问题。 您说:“真是一团糟!”然后阅读以下行:
注意:我们建议您在实现中使用IANA时区数据库http://www.iana.org/time-zones/中的信息。
是的 ECMA规范为您提供了使用IANA数据库的毫不客气的建议,因为JavaScript没有特殊的标准数据库。 结果,不同的浏览器使用它们自己的时区操作来计算时间,这些时区通常彼此不兼容。 后来,在ECMA中,对于国际API,添加了使用ECMA-402
Intl.DateTimeFormat
格式的IANA数据的选项。 但是,此选项的可靠性远不如其他编程语言中的类似选项可靠。
服务器-客户端环境中的时区
考虑一个简单的场景:我们需要确定时区。 假设我们创建了一个处理时间信息的日历。 当客户端环境中的用户在注册窗口中输入日期和时间时,该日期将传输到服务器并存储在数据库中。 然后,客户端从服务器接收时间表中注册的日期,以显示在屏幕上。
在这里,您需要决定一些事情。 如果某些访问服务器的客户端位于不同的时区怎么办? 日程安排中的活动已于2017年3月10日在纽约的23.30处注册,应在2017年3月10日的9.30处显示。 为了使服务器能够为来自不同时区的客户端提供服务,存储在其上的日程表必须包含不依赖于时区的绝对值。 每个服务器都有自己的存储此类值的方法,这个问题超出了本文的范围,因为一切都取决于特定的服务器或数据库。 通常,应以基于单个偏移量的值形式(通常为UTC)或包含有关客户端环境时区信息的值形式来显示从客户端传输到服务器的日期和时间。
通常,此类数据以UTC格式或符合
ISO-8601标准的带偏移信息的
Unix时间传输。 如果在我们的示例中,我们将2017年3月10日的Seoul 21.30转换为Unix时间,那么我们将获得整数值
1489113000
。 并且以ISO-8601格式,字符串值是
2017–03–10T11:30:00+09:00
。
如果您在浏览器环境中使用JavaScript,则必须如上所述转换输入的值,然后将其转换回以匹配用户的时区。 有必要解决这两个问题。 从编程语言的角度来看,第一个操作称为“解析”,第二个操作称为“格式化”。 现在,让我们看看这是如何在JavaScript中完成的。
即使在使用Node.js的服务器环境中使用JS时,也可能需要解析从客户端接收的数据。 但是,由于服务器和数据库的时区通常是同步的,并且格式已分配给客户端,因此在浏览器环境中,您需要确定几个因素。 我将进一步解释浏览器环境。
JavaScript日期对象
使用
Date
对象可以解决涉及给定时间或指定时间工作的任务。 这是ECMAScript中定义的本机对象,例如
Array
或
Function
。 也就是说,在大多数情况下,它是使用C ++之类的本机代码实现的。
MDN文档中对API进行了详细
说明 。 Java的
java.util.Date类对该对象有很大的影响,因此它继承了一些不良的属性,例如可变数据的特征以及从零开始的月份。
在后台,JavaScript
Date
对象使用Unix-time格式的绝对值与时间一起使用。 但是诸如
parse()
,
getHour()
,
setHour()
类的构造函数和方法和其他方法会受客户端所在时区(更确切地说,是运行浏览器的OS中指定的区域)的影响。 因此,如果您直接使用用户输入的数据创建
Date
对象,则客户的本地时区将反映在该数据中。
正如我提到的,JavaScript没有提供任何方式来随意更改时区。 因此,我们假设我们可以直接使用浏览器中指定的时区值。
使用用户输入创建日期对象
让我们回到第一个例子。 假设用户于2017年3月11日在其设备上的首尔时间输入11.30。此数据被保存为五个数字:2017、2、11、11和30-分别为年,月,日,小时和分钟(因为月从0开始,因此该值应为3-1 = 2)。 使用构造函数,您可以轻松创建
Date
对象:
const d1 = new Date(2017, 2, 11, 11, 30); d1.toString();
如果查看
d1.toString()
返回的值,您会看到创建的对象的绝对值在2017年3月11日为11:00,它是使用
+09:00
(KST)混合计算的。
您可以在构造函数中使用字符串数据。 如果将它们应用于
Date
,则对象在内部调用
Date.parse()
并
Date.parse()
正确的值。 此功能支持
RFC2888和
ISO-8601规范。 但是
MDN文档Date.parse()表示,此方法返回的值取决于浏览器,并且字符串类型的格式可能会影响确切的最终值。 因此,最好不要使用此方法。 例如,在Safari和Internet Explorer中,像
2015–10–12 12:00:00
这样的字符串值将返回
NaN
,而在Chrome和Firefox中,它将返回本地时区。 在某些情况下,将返回基于UTC的值。
使用服务器数据创建日期对象
假设您要从服务器接收数据。 如果它们采用数字Unix时间形式,那么您可以简单地使用构造函数来创建
Date
对象。 我还没有提到,当
Date
构造函数将单个值作为单个参数接收时,它将以毫秒为单位计算Unix时间值(注意:JS以毫秒为单位计算Unix时间。这意味着第二个值必须乘以1000)。 当执行以下代码时,我们得到与上一个示例相同的值:
const d1 = new Date(1489199400000); d1.toString();
并且如果不是使用Unix-time而是使用字符串类型ISO-8601? 如前所述,
Date.parse()
方法变得不可靠,最好不要使用它。 但是,从ECMAScript 5开始,可以在Internet Explorer 9.0和更高版本的
Date
构造函数中使用ISO-8601格式的字符串构造。
如果您使用的不是最新的浏览器,请确保
Z
位于值的末尾。 没有它,您的旧浏览器可以根据本地时间而不是UTC来解释值。 这是Internet Explorer 10中的示例用法:
const d1 = new Date('2017-03-11T11:30:00'); const d2 = new Date('2017-03-11T11:30:00Z'); d1.toString();
根据规范,在两种情况下均应获得相同的值。 但是它们是不同的。 在更高版本的浏览器中,值将相同。 为避免此问题,如果没有时区信息,请始终在行末添加
Z
创建要传递给服务器的日期
现在,您可以使用先前创建的
Date
,根据本地时区自由检索或添加时间。 仅在处理结束时,别忘了在将数据返回服务器之前将其转换为以前的格式。
如果这是Unix时间,则可以使用
getTime()
方法(不要忘记毫秒)。
const d1 = new Date(2017, 2, 11, 11, 30); d1.getTime();
那么ISO-8601格式的字符串值呢? 正如我所说,Internet Explorer 9.0及更高版本支持ECMAScript 5,而更高版本则支持ISO-8601。 因此,使用
toISOString()
或
toJSON()
方法,可以在ISO-8601中创建一个字符串(
toJSON()
可用于
JSON.stringify()
等递归调用)。 除了处理无效数据时,这两种方法给出的结果相同:
const d1 = new Date(2017, 2, 11, 11, 30); d1.toISOString();
您可以使用
toGMTString()
或
toUTCString()
方法创建UTC字符串。 这将导致一个符合
RFC-1123的值 。
Date
对象包括
toString()
,
toLocaleString()
及其扩展方法。 但是它们没什么用,因为它们主要用于根据本地时区返回字符串,并且返回的值取决于浏览器和OS。
更改您的当地时区
如您所见,JS中提供了某种时区支持。 ? ? , JS . , . . , .
. . 11.30 11 2017 - . Unix- , -
-05:00
. , .
getTimeZoneOffset()
. API JavaScript, . :
const seoul = new Date(1489199400000); seoul.getTimeZoneOffset();
-540
, 540 . , (
+09:00
). , , . -,
60 * 5 = 300
.
840
Date
.
getXX
. :
function formatDate(date) { return date.getFullYear() + '/' + (date.getMonth() + 1) + '/' + date.getDate() + ' ' + date.getHours() + ':' + date.getMinutes(); } const seoul = new Date(1489199400000); const ny = new Date(1489199400000 - (840 * 60 * 1000)); formatDate(seoul);
formatDate()
-. , . , ? , . , — , . ( ).
, . , -, 11 15 .
setDate()
Date
, , .
ny.setDate(15); formatDate(ny);
, . , ? ,
getTime()
getISOString()
. , .
const time = ny.getTime() + (840 * 60 * 1000);
, , .
Date
? 不行
Date
11- 15-, 4 (
24 * 4 * 60 * 60 * 1000
). - 10- 15-, 5 (
24* 5 * 60 * 60 * 1000
). .
. , . - 12 , 15 2017
-04:00
,
-05:00
. , 780 , 60 , .
const time = ny.getTime() + (780 * 60 * 1000);
, - , .
, . , , , . , ,
IANA .
,
Date
, . , . , . . JS . .
Moment Timezone
Moment — JavaScript-, . API , .
Moment Timezone , . IANA API, .
. , .
.
, Moment Timezone:
const seoul = moment(1489199400000).tz('Asia/Seoul'); const ny = moment(1489199400000).tz('America/New_York'); seoul.format();
seoul
,
ny
-05:00
-04:00
.
format()
, ISO-8601, . .
结论
API , JavaScript, . , API, Internet Explorer 9 . , . ,
getTimezoneOffset()
. , . Moment Timezone.
, , . : . , , , . , JavaScript . , .