近期一个上马的项目涉及到国际化,其中一个点就是时间显示的问题,比如现在时间是 北京时间 2022-05-29 23:21:30
,这个在国内显示没问题,如果在墨西哥显示呢,显然是不能用北京时间的,毕竟两者隔了十三个时区,这时对方才 2022-05-29 10:21:30
呢。
北京时间是东八区,墨西哥是西五区。
显然,时间国际化涉及到了时区概念,这里对时区做个简单的介绍。国际上规定,每隔 15° 划为一个时区,全球可分为 24 个时区。以本初子午线为基准,从西经 7.5° 到东经 7.5° 就是 GMT 0 时区。
东西经各 180°,那么 360/15= 24 个时区。
GMT,Greenwich Mean Time, 即格林尼(威)治标准时间。
一般来说,北京时间就是 GMT +8 时区的,即东八区,墨西哥时间是 GMT -5,即西五区。
其实,在国际上,GMT 是等同于 UTC 的。UTC(Coordinated Universal Time)又叫做协调世界时,也即是标准时间。UTC 是根据原子钟来计算时间,而 GMT 是根据地球的自转和公转来计算时间,也就是太阳每天经过位于英国伦敦郊区的皇家格林威治天文台的时间就是中午12点。由于科技的进步,目前最先进的原子钟 50 亿年才误差 1 秒,而地球自转在大部分情况下其速度是越来越慢的,因此 UTC 时间更加精确。
UTC 是 GMT 的换算单位是 UTC = GMT + 0
。因此 GMT +8
等同于 UTC +8
。
好了,铺垫就到此为止了,接下来就用 golang 代码演示下如何做到时间国际化。
其实,思路也很简单,就两步:
- 一、获取时间戳;
- 二、依据时区编码,从而计算 UTC 与 给定时区的时差。
时区编码文件放在 $GOROOT/lib/time/zoneinfo.zip 中,解压下,发现第一层都是目录,第二层和第一层相结合就是具体的时区编码,比如 America/Mexico_City。
还是以博文开头的时间为例,2022-05-29 23:21:30
对应的时间戳为 1653837690
。
package main
import (
"log"
"time"
)
// GetTimeOffset 国际化时间戳偏移
func GetTimeOffset(timezone string, ts int64) (offset int) {
var loc, _ = time.LoadLocation(timezone)
_, offset = time.Unix(ts, 0).In(loc).Zone()
return
}
func main() {
ts := int64(1653837690)
offset := GetTimeOffset("America/Mexico_City", ts)
log.Println(offset) // -18000
}
计算可知偏移量为 -18000 秒,ok,换算成时区就是 GMT -5 也就是 西五区了,此时,如果把偏移量和时间戳丢给前端渲染,由前端自己本地化时区转化为用户所在时区对应的时间即可。
var d = new Date("2022-05-29 23:21:30");
var utc = d.getTime() + (d.getTimezoneOffset() * 60000);
var nd = new Date(utc + (3600000 * -13));
// 2022 4 29 2 21 30
console.log(nd.getFullYear(), nd.getMonth(), nd.getDate(), nd.getHours(),nd.getMinutes(),nd.getSeconds())
如果都是由后端计算并传值,前端显示的话,也简单,代码如下
package main
import (
"log"
"time"
)
const DefaultDate = "2006-01-02 15:04:05"
// FormatWithLocation 国际化时间戳转换字符串
func FormatWithLocation(timezone string, ts int64) string {
lt, _ := time.LoadLocation(timezone)
str := time.Unix(ts, 0).In(lt).Format(DefaultDate)
return str
}
// ParseWithLocation 国际化时间字符串转换时间戳
func ParseWithLocation(timezone string, timeStr string) int64 {
l, _ := time.LoadLocation(timezone)
lt, _ := time.ParseInLocation(DefaultDate, timeStr, l)
return lt.Unix()
}
func main() {
// 北京时间
str := "2022-05-29 23:21:30"
// 注意,这里不要用 time.Parse 直接转时间戳,这个转的结果为 UTC 时间
// 结果会默认加上时差,也就是多八个小时
// 对应的时间戳
ts := ParseWithLocation("Asia/Shanghai", str)
// 获取最终的时间戳
log.Println(FormatWithLocation("America/Mexico_City", ts)) // 2022-05-29 10:21:30
}
最后,给个简单的 php 的实现方案。
<?php
function getGlobalDate($timezone, $timeStr) {
return new DateTime($timeStr, new DateTimeZone($timezone));
}
try {
$date = getGlobalDate('Asia/Shanghai', '2022-05-29 23:21:30');
$offsetBJ = $date->getOffset();
$ts = $date->getTimestamp();
$date = getGlobalDate('America/Mexico_City', '2022-05-29 23:21:30');
$offsetMX = $date->getOffset();
echo date('Y-m-d H:i:s', $ts - $offsetBJ + $offsetMX) . "\n"; // 2022-05-29 10:21:30
} catch (Exception $e) {
echo $e->getMessage();
}
【参考】
计算机时间到底是怎么来的?程序员必看的时间知识!
Golang Time包的本地化时区一次需求实现
UTC和GMT什么关系?
彻底弄懂GMT、UTC、时区和夏令时