“中国要复兴、富强,必须在开源软件领域起到主导作用,为了国家安全和人类发展,责无旁贷,我们须为此而奋斗”——By:云客
时区概念:
时间和空间是人们进行协作的前提,由于地球是圆的,很容易想到用经纬度确定空间的唯一性,而对于时间而言,人们对她的感知来源于空间中的运动或变化,在一个静置的空间中不会有时间存在,因此对时间的度量也是依托于运动或变化,太阳是地球上人人都能感知到的运动参考,自然而然的成为了度量时间的工具,由于人们的作息更多依靠的是太阳而不是众多统一运转的钟表,因此时间的制定需要能表达这种作息感受,使得不管对位于世界哪儿的人而言,同一个时间数字应能表达相似的感受,比如12点代表着太阳当空的正午, 7点左右是起床的清晨,19点是日落的傍晚,这样在沟通协作中就有了相同的前提。
为了做到这一点,1884年10月13日在华盛顿召开的国际经度会议(又称国际子午线会议)上,确定把通过英国伦敦Greenwich(通常翻译为格林尼治或格林威治)天文台的经线确定为0度经线,也称为本初子午线(prime meridian),地球一圈有360度,从0度经线向东180度为正数,向西180度为负,纬度自然地从赤道开始,北纬90度为正数,南纬90度为负数,这就确定了全球的位置。
有了经度后,将地球自转一圈定为24个小时,据此把地球像橘子一样分为24个区,太阳每转过一个区需要一小时,以0度经线东西各7.5度范围做第0时区,然后每15度划分为一个时区,每个时区以其中央经线所在时间作为该时区的区时,也称为该时区的地方时(本地时间),比如0度经线的时间就是0时区的地方时,相邻区的地方时在表示上相差一个小时,同样,时区以0度经线向东为正,向西为负,这样当确定一个时区的地方时就能推导出其他时区的地方时,全球时间就统一了,如果一个人自西向东走,每跨一个时区,所戴手表需要增加一个小时,比如7:30需要调整为8:30,反之如果自东向西走则减少一小时,东经180度和西经180度重合,理论上讲该经线应该是换日线,换日线两边的地方时相差一天,但为了避免换日线所在区域的时间混乱,前文所述的国际经度会议将换日线确定为一条沿着180度经线延伸的曲线,其按自然条件从人烟稀少的地方走,避免将人口繁多的地区或国家分成两半,从而避免该地区人们使用的时间相差一天。
通过以上原理确定的时间称为格林尼治标准时间(Greenwich Mean Time),简称GMT,这是世界第一个统一时间,在此之前世界各地有许多地方标准时间,这些地方标准在当地使用没有什么问题,但在全球性活动中就会导致协作沟通困难,比如期货、股票、赛事、跨国贸易运输等等,因此GMT时间被全球接受。
GMT时间是以地球自转为基础的时标,由于地球自转有季节性的变化和突然的不规则变化,实际上GMT采用的是平均时间(参考平太阳时),但随着科技的发展,人们发现地球的自转有逐渐变慢的趋势,这就导致了GMT时间并不均匀,理想状态下,时间应该是均匀的,换句话说物理上每一秒应当是一样长的,因此在1979年末在日内瓦举行的世界无线电行政大会上通过决议,确定用“世界协调时Universal Time Coordinated”取代格林尼治时间作为无线电通讯领域内国际标准时间,简称UTC,它不以地球自转为基础,而是以原子时为基础,由原子钟提供时间,其原理是利用原子吸收或释放能量时发出的电磁波来计时的,这种计时非常均匀稳定,可达到37亿年误差不超过1秒的水平,UTC在时刻上尽量接近GMT,以加闰秒的方式修正GMT时间,UTC 更加科学更加精确,在不需要精确到秒的情况下,可将GMT 和UTC 视为相同。
要在全球传递时间那么就需要在地方时上加上时区信息,实际使用中,有些国家很大,比如俄罗斯跨了11个时区,在沟通中带时区信息不太方便,因此有些国家会在内部统一时间或重新划定时区,如美国重新划定时区为“东部、中部、山地、太平洋”四个,大多数国家都是基于UTC时区而调整的;在中国一共跨了5个时区,人口最多的时区位于东八区,为方便沟通,全国统一采用了东八区时间,冠以首都名字,就是我们常听的“北京时间”,由于新疆离东八区太远,因此也被允许使用东六区时间,俗称新疆时间。
时区缩写:
由于各个国家可能重新划定时区,实际世界上有很多时区,每一个时区都有一个名字,为了书写方便,往往采用时区缩写,有些时区的名字缩写是相同的,因此同一个缩写可能对应着很多时区,比如中国标准时间“China Standard Time”的缩写是CST,而美国的中部标准时间“Central Standard Time”的缩写也是CST,这些缩写和时区对应表可参见:
https://www.timeanddate.com/time/zones/
时区映射图可参考:https://www.timeanddate.com/time/map/
夏令时(daylight saving time):
由于时区的定立,各时区的当地时间就定下来了,比如上班时间、航空时间、铁路时刻表都定下来了,随之作息规律就定下来了,但我们知道夏天天亮的早,黑的也早,如果不随太阳时间作息就会浪费很多能源,早晨天亮了却在睡觉,天黑了还在用电工作,因此就有人提出在夏天把时间提前以充分利用太阳,比如把7点调整为8点,这样一来原本8点上班的人们7点就需要上班了,因此必须早起,当然下班也随之提前一小时,这样就达到了充分利用阳光的目的,在冬天的时候再把时间调慢一小时恢复到原状,这样调整之后的时间就称为“日光节约时间daylight saving time”,简称DST,翻译为夏令时。
可见夏令时是一种人为调整时间的制度,这种制度节约了能源,但调时会有成本,也会打乱人们及动物的生物钟等等弊端,因此利弊之下也有反对声音,所以根据情况有些国家实行夏令时,有些国家不实行,实行夏令时的国家会在规定时间点将所有计时工具统一调慢或调快,包括电脑,这也涉及到了程序获取到的时间。我国在1986年至1991年曾实行夏令时随后取消,美国有部分地区实行,欧盟国家和瑞士从3月最后一个星期日到10月最后一个星期日实行夏令时,新西兰由于处于南半球,所以夏季和北半球相反,它从9月最后一个星期日到4月第一个星期天实行夏时。
在计算机中,由于夏令时的存在,各国家又不尽相同,这让时区计算变复杂了一些。
javascript时区:
在JS中,以函数方式调用Date()会返回一个日期时间字符串,如:
console.log(Date());
将输出类似:
Mon Dec 02 2019 16:58:18 GMT+0800 (中国标准时间)
这表示GMT时间,时区为东八区,括号里是时区名的本地化表示
如果以对象方式调用:
var myDate=new Date();
myDate.getTimezoneOffset();
这将返回当前本地时间与格林尼治标准时间的分钟差,这个差是受到夏令时影响的。
HTTP协议时区:
在HTTP响应头中,Date标头通常使用GMT时间,如下:
Sat, 30 Nov 2019 03:40:40 GMT
或:Sat, 30 Nov 2019 03:40:40 GMT +0800
在GMT后没有加时区信息时,表示格林威治当地时间(0时区的地方时),加了则表示对应时区的当地时间,通常没有加
PHP时间表示:
在PHP中time()函数返回的是一个时间戳,表示自从 Unix 纪元(格林尼治的地方时间 1970 年 1 月 1 日 00:00:00)到当前时间的秒数,注意是“格林尼治的地方时间”,因此时间戳是一个绝对时间值,和时区无关,换句话说,同一个时间戳,在不同时区下“date('Y-m-d H:i:s',$t)”函数将显示不同时间字符串。
php为每个时区都定义了一个标识符,字符串值,称为时区标识符,见该链接:
https://www.php.net/manual/zh/timezones.php
可使用以下函数查询PHP支持的所有时区标识符:
timezone_identifiers_list()
在设置中可用这些时区标识符指定脚本使用的时区,见下文。
中国统一采用东八区时间,即北京时间,时区标识符可使用“Asia/Shanghai”、“Asia/Hong_Kong”或“Asia/Taipei”,在系统中效果完全一样,也可以设置为“中华人民共和国”的简写:“PRC”
PHP时区相关设置:
配置:date.timezone
位于配置文件php.ini中,用于配置系统默认时区,其值为时区标识符,默认为空字符串,此时将用UTC,即0时区
配置:date.default_latitude
一个浮点数,表示默认纬度,默认值为31.7667
配置:date.default_longitude
一个浮点数,表示默认经度,默认值为35.2333
配置:date.sunrise_zenith
一个浮点数,表示默认日出天顶,默认值为90.83
配置:date.sunset_zenith
一个浮点数,表示默认日落天顶,默认值为90.83
注:后四个选项目前仅用于函数:date_sunrise()和date_sunset(),以返回和日出日落相关的信息
PHP时区相关主要函数:
date_default_timezone_set ( string $timezone_identifier )
设定脚本随后使用的时区,参数为时区标识符,参数无效则返回 FALSE,否则返回 TRUE,作用于所有日期时间函数,仅限当前脚本,并不修改配置
date_default_timezone_get ( void )
返回当前使用的默认时区,优先级依次为:
date_default_timezone_set()
date.timezone 配置选项
UTC 的默认时区
timezone_identifiers_list()
以数组方式返回PHP所支持的所有时区标识符,约四百多个
timezone_abbreviations_list()
返回时区缩写,数组值,键名为时区缩写,键值为该缩写包含的各时区数组,在时区数组中三个键:
dst:布尔值,该时区是否使用夏令时
offset:与格林尼治时间的时差,一个整数,单位秒
timezone_id:时区标识符
由于是否采用夏令时在各国中是变动的,比如我国曾经采用,现在不用,因此该函数返回的数据包含了历史数据,导致有很多时区标识符重复出现,因此该函数仅供参考
timezone_version_get()
返回系统使用的时区数据库的版本,一个字符串值,如:“2017.3”
timezone_location_get ( DateTimeZone $object )
返回时区相关的定位信息,返回一个数组,类似如下:
Array
(
[country_code] => CN
[latitude] => 31.23333
[longitude] => 121.46666
[comments] => Beijing Time
)
timezone_name_from_abbr( string $abbr [, int $gmtOffset = -1 [, int $isdst = -1 ]] )
返回时区标识符,通过时区缩写、时差偏移、是否受夏令时影响进行综合判断,无结果时,返回false。
参数如下:
$abbr:时区的缩写,见前文,一个缩写可能对应很多时区标识符;可为空字符串,此时用后两参数判断
$gmtOffset:与格林尼治地方时相差的秒数,一个整数,默认为“-1”,为-1时,将返回包含在这个缩写中的第一个时区,否则继续在其中查找具有该偏移的时区,如未找到依然返回第一个
$isdst:是否采用夏令时,1为采用,表示偏移是受夏令时影响的,0为不采用,偏移不受夏令时影响,默认为“-1”,默认值意为搜索时不考虑夏令时
PHP日期时间对象:
php提供了以下几个时间相关的对象以进行OOP编程:
日期时间对象DateTime:
示例:
date_default_timezone_set('Asia/Shanghai');
$date = new DateTime();
echo $date->format('Y-m-d H:i:s');
注意:
$d=new \DateTime('2019-12-09 10:15:30',new \DateTimeZone('UTC'));
$d->setTimezone(new \DateTimeZone('Asia/Shanghai'));
echo $d->format('Y-m-d H:i:s');
将输出 2019-12-09 18:15:30,换句话说设置时区不会改变对象所表示的绝对时间
不可变日期时间对象DateTimeImmutable:
和日期时间对象完全一样,唯一的区别是不可修改,注意不可修改是指每次修改都将返回一个新的不可变日期时间对象,而不是修改时抛出异常,因此如果有修改操作,应该接受返回的值,示例:
$date = new \DateTimeImmutable();
$date=$date->setDate(2001, 2, 28);
echo $date->format('Y-m-d H:i:s');
日期时间接口DateTimeInterface:
以上两个对象都实现了此接口,常量定义位于该接口中
时区对象DateTimeZone:
用来表示一个时区,示例:
$tz = new DateTimeZone('Asia/Shanghai');
print_r($tz->getLocation());
时间间隔对象DateInterval:
用来表示两个时间点之间的间隔,也就是有多长时间,示例:
$interval = new DateInterval('P2Y3M4DT6H8M10S');
echo $interval->format('%y 年 %m 月 %d 天 %h 小时 %i 分钟 %s 秒 %f 微秒');
构造参数接收一个“时周标识符”用来表示时间间隔,无效标识符会抛出异常,格式规则如下:
以字母“P”开始,Y表示年,M月,D天,W周(不能和天同用),H小时,M分钟,S秒,全大写,不必给出全部标识符,但必须从大单位到小单位排列,如秒不能在月前,如果包含了表示时间的标识符(如时分秒,非日期标识符),则必须在这些标识符前面整体加T,仅加一个,换句话说:1分30秒记为:“PT1M30S”,而不是“PT1MT30S”, 3秒可写为:“PT3S”,而不必写为“PT0H0M3S”,数字没有上限,即月可以超过12,分也可以超过60等。
标识符也可以用日期格式串表示,如“P0001-00-04T00:00:00”,但这种表示方法数字有上限,如25时是无效的。
标识符中没有微秒,但间隔对象可能是计算返回的,如DateTime::diff(),因此输出格式有微秒占位符
时间迭代器对象DatePeriod:
用来在一个给定时间段内,用给定间隔进行迭代,换句话说,即用给定时间间隔去遍历一个时间范围,示例:
$start = new DateTime('2019-07-01');
$interval = new DateInterval('P7D');
$end = new DateTime('2019-07-31');
$recurrences = 4;
$iso = 'R4/2019-07-01T00:00:00Z/P7D';
$period = new DatePeriod($start, $interval, $recurrences);
$period = new DatePeriod($start, $interval, $end);
$period = new DatePeriod($iso);
//以上三种实例化方法得到的实例完全等同
foreach ($period as $date) {
echo $date->format('Y-m-d')."\n";
}
在示例的构造函数中还有最后一个参数没有列出,如果给定“DatePeriod::EXCLUDE_START_DATE”,则遍历会排除开始时间,默认不排除
Drupal时区侦查:
用户时区的侦查在drupal中由前后端共同完成,在前端使用如下库:
核心库core/drupal.timezone:
文件:core/misc/timezone.es6.js
该库依据客户端JS时间信息侦测用户所在时区。在文档就绪后执行,仅在文档中包含有具备类属性“timezone-detect”的表单元素时,该库才起作用,她将侦查到的时区标识符用作该表单的值。
在实现上,该库收集以下三个信息:
abbreviation:时区缩写,从JS函数Date()返回的字符串中提取
offsetNow:本地时间相对于格林尼治时间的时差(秒)
isDaylightSavingTime:是否受到夏令时影响,如果7月和1月的时差不一样,则受到了影响
收集到这三个信息后,该库将其送给后端去解析返回
后端定义了以下路由:
system.timezone:
path: '/system/timezone/{abbreviation}/{offset}/{is_daylight_saving_time}'
defaults:
_controller: '\Drupal\system\Controller\TimezoneController::getTimezone'
abbreviation: ''
offset: -1
is_daylight_saving_time: NULL
requirements:
_access: 'TRUE'
控制器逻辑很简单,全依靠php原生函数:
timezone_name_from_abbr($abbreviation, intval($offset), $is_daylight_saving_time);
关于该函数请见前文
关于该侦查方式请注意:
1、如果解析不到,那么后端将返回布尔值false;网络可能超时,因此侦查可能不可靠
2、前端库以同步方式请求后端,这意味着在取回时区标识符前浏览器会被暂时冻结
Drupal系统时区设置:
在以下三种事件派发时均会设置时区:
请求事件:\Symfony\Component\HttpKernel\KernelEvents::REQUEST
配置保存:\Drupal\Core\Config\ConfigEvents::SAVE
用户设置事件:\Drupal\Core\Session\AccountEvents::SET_USER
设置工作由以下订阅器完成:
服务名:system.timezone_resolver
订阅器类:\Drupal\system\TimeZoneResolver
执行方法:setDefaultTimeZone
优先级:299-0
该订阅器订阅了以上三种事件以设置事件发生后系统使用的时区,时区设置逻辑如下:
如果允许用户设置时区且用户设置了自己的时区,那么将以用户设置的时区作为系统时区,否则将以系统设置的默认时区作为系统时区
在用户认证订阅器中(服务名:authentication_subscriber,请求事件,优先级300),首先调用了该服务设置时区,账户认证订阅器在向用户代理对象注入用户账户时,将派发以下事件:
\Drupal\Core\Session\AccountEvents::SET_USER
该订阅器订阅了设置用户事件,紧随其后在请求事件中该服务再一次执行了时区初始化,看似重复但却兼顾了这三种事件的订阅
在该服务设置时区之前系统使用的是来自php的时区
补充:
1、时间为什么采用60进制呢,和圆有关系,也和60这个数的数学特性有关系,曾经在法国采用过一百进制,但在法国大革命时被证明是失败的
2、Drupal系统时区设置:/admin/config/regional/settings
反馈互动