一、時間標準(Time Standard) vs 時區(Time Zone)
A time standard is a specification for measuring time: either the rate at which time passes; or points in time; or both.
https://en.wikipedia.org/wiki/Time_standard從維基百科上摘取的一段定義,大意是時間標準是度量時間的一種規範,既可指時間流逝的速率亦或指時間點。我們常用的UTC(Coordinated Universal Time)就是現在國際通用的時間標準。UTC的定義由兩部分決定:原子時(TAI - International Atomic Time)和世界時(UT1 - Universal TIme)。原子時可以非常穩定和精確地定義1秒時間(銫-133原子震動9,192,631,770次),由於它很穩定,我們用它來衡量時間流逝的速率,也就是時鐘上秒針轉動的速率;世界時是基於地球自轉的一種時間計量方式,通過它我們可以衡量地球一天有多長。但由於地球自轉因潮汐力等客觀因素,導致自轉速度在不斷變慢,所以人為引入閏秒(leap second)來彌補世界時和地球真實自轉速度之間的誤差。
追溯下歷史,UT最早在1884年被創立,當時選用的Greenwich Mean Time(GMT)作為世界時間標準,把經過經度為0的倫敦格林威治這個地方作為我們現在熟知的本初子午線的原始起點。
之後,UTC最早在1960被引入,最終在多次完善後,從1972年開始替代GMT,自此GMT不再是一個時間標準,而僅僅只是代表一個時區(零時區)。
我們接著說時區:
A time zone is a region of the globe that observes a uniform standard time for legal, commercial and social purposes.
https://en.wikipedia.org/wiki/Time_zone簡而言之,為了滿足法律、商業和社交需求,我們用時區將全球劃分成不同地區並遵循統一的時間表達方法。剛才說到的GMT是零時區的一個縮寫(也叫Z - Zulu Time Zone),還有CST(China Standard Time)也是一個時區的縮寫,全球各個國家、地區或城市根據時區的劃分使用特定的縮寫。舉個例子,比如我們知道在倫敦的這個時間點 2020-04-27 07:00:00,我們想知道中國是幾點,由於中國在CST時區,倫敦在Z時區,CST時區和Z時區相差8個小時,此時在CST時區,當地時間表示為2020-04-27 15:00:00 CST,UTC時間表示為2020-04-27 15:00:00 UTC+08:00,或者,2020-04-27 02:00:00 UTC-05:00代表了EST(Eastern Standard Time)時區的當地時間。
我們可以發現,UTC時間裡帶上了和零時區的正負時間偏移量。值得注意的是,這個偏移量的最小單位並不是小時,而是分鐘,比如IST(India Standard Time),它的偏移量是UTC+05:30,也就是5個半小時,再如NPT(Nepal Time),它的偏移量是UTC+05:45,5個小時45分鐘。通過這個連結:https://www.timeanddate.com/time/zones/,可以找到所有時區的縮寫和對應的偏移量(註:不同地區可能共用一個時區縮寫,比如BST,在亞洲代表Bangladesh Standard Time, UTC+06:00;在大洋洲代表Bougainville Standard Time, UTC+11:00;在歐洲代表British Summer Time, UTC+01:00)。
綜上,我們現在可以理解清楚UTC和GMT之間的區別了。但由於歷史原因,GMT有一段時間的作用和UTC一樣,只是現在UTC替代了GMT,但很多站點上可能還會保留使用2020-04-27 15:00:00 GMT+08:00的形式來表達國際標準時間,雖然不是什麼大是大非的問題,但結合GMT現存的特定意義,僅有部分歐洲和非洲地區在使用,所以還是更推薦使用當地時區縮寫的形式來表達用戶當地時間。
二、DST (Daylight Saving Time)絕大多數高級開發語言都對時區展示有專門的封裝,下面以Java為例,看下如何來處理時區問題。
TimeZone utc = SimpleTimeZone.getTimeZone("UTC");String pattern = "yyyy-MM-dd HH:mm:ss z (zzzz)";DateFormat dfUTC = new SimpleDateFormat(pattern, Locale.ENGLISH);dfUTC.setTimeZone(utc);
String zoneId = "Europe/Vienna";TimeZone tz = SimpleTimeZone.getTimeZone(zoneId);
pattern = "yyyy-MM-dd HH:mm:ss z (zzzz) 'UTC'XXX";DateFormat dfLocal = new SimpleDateFormat(pattern, Locale.ENGLISH);dfLocal.setTimeZone(tz);
Calendar cd = Calendar.getInstance(tz);cd.set(2010, 8, 1, 10, 0, 0);Date dt = new Date(cd.getTimeInMillis());System.out.print("local time:\t");System.out.println(dfLocal.format(dt));System.out.print("UTC time:\t");System.out.println(dfUTC.format(dt));
String abbr = tz.getDisplayName(tz.inDaylightTime(dt), 0, Locale.ENGLISH);System.out.print("abbr.(DST):\t");System.out.println(abbr);
cd.set(2010, 10, 26, 10, 0, 0);dt = new Date(cd.getTimeInMillis());System.out.print("local time:\t");System.out.println(dfLocal.format(dt));System.out.print("UTC time:\t");System.out.println(dfUTC.format(dt));
abbr = tz.getDisplayName(tz.inDaylightTime(dt), 0, Locale.ENGLISH);System.out.print("abbr:\t");System.out.println(abbr);執行上面的代碼,可以得到下面的輸出:
從JDK1.8開始新增了ZonedDateTime類,可以更簡單明了地從一個已知時區的時間轉換成某個目標時區的時間。// 注意下,這裡的month是從1開始的,而Calendar裡是從0開始的。ZonedDateTime zdt = ZonedDateTime.of(2010, 9, 1, 10, 0, 0, 0, ZoneId.of("Europe/Vienna")) .withZoneSameInstant(ZoneId.of("Asia/Shanghai"));System.out.println(zdt.format(DateTimeFormatter.ofPattern(pattern, Locale.ENGLISH));2010-09-01 16:00:00 CST (China Standard Time) UTC+08:00另外,上面ZoneId構造的時候,類似"Asia/Shanghai"可以替換成"+08:00",但這樣輸出的結果裡是沒有CST和China Standard Time,且都被+08:00替換。
2010-09-01 16:00:00 +08:00 (+08:00) UTC+08:00從上一部分的描述中,我們可以發現,各個地區的UTC的偏離量,還有DST的規則等等是沒有規律的,是需要日常維護的,但上面獲取到的Vienna的DST時間區間是從哪裡來的呢?
翻看JDK源碼,我們可以發現,SimpleTimeZone.getTimeZone()方法的實現最終可以追溯到ZoneInfoFile這個類。
static { String oldmapping = AccessController.doPrivileged( new GetPropertyAction("sun.timezone.ids.oldmapping", "false")).toLowerCase(Locale.ROOT); USE_OLDMAPPING = (oldmapping.equals("yes") || oldmapping.equals("true")); AccessController.doPrivileged(new PrivilegedAction<Object>() { public Object run() { try { String libDir = System.getProperty("java.home") + File.separator + "lib"; try (DataInputStream dis = new DataInputStream( new BufferedInputStream(new FileInputStream( new File(libDir, "tzdb.dat"))))) { load(dis); } } catch (Exception x) { throw new Error(x); } return null; } });}可以看到,ZoneInfoFile在靜態構造塊中,會去JRE的根目錄加載一個叫tzdb.dat的文件。追根溯源,這個Tzdb來自於IANA提供的TimeZone資料庫,https://iana.org/time-zones,目前維護著最新最全的全球時區相關基礎數據。我們可以通過getVersion方法來查看下系統加載的這個資料庫的版本。
String version = ZoneInfoFile.getVersion();System.out.println(String.format("TZDB[%s]", version));下面這個截圖是從我本機輸出的結果,可以看出是2015年的版本。
為了能夠應用最新的數據,我們可以替換tzdb.bat這個文件。但從IANA上下載的並不是這種格式的,我們需要用到這個工具:https://github.com/akashche/tzdbgen,它可以幫助我們生成最新的tzdb.bat文件。(根據使用開發語言的不同,下載下來的數據需要再編譯成對應語言的文件,可以在IANA上找到相對應的編譯工具。)除此之外,我們也可以自己實現一個ZoneRulesProvider來定製自己的時區使用規則。
一個確切時間的完整表述都應該有對應的時區,如果沒有明確設置,那麼要麼默認是設備當地時區,要麼是Z時區。
在本地化展示用戶端當地時間的時候,儘量讓用戶端來處理本地化輸出格式,因為客戶端對時間處理的方法一般默認用的是系統設置的當地時區。
如果需要準確輸出某個時間所在的時區名字,在後端的數據存儲和交換過程中,也需要保存時區ID(比如Asia/Shanghai)。
僅通過offset推算的新時間,是無法準確獲取新時間所在的時區名的。比如通過UTC-5,既可以對應EST(Eastern Standard Time),也可以對應ECT(Ecuador Time)。所以如果需要準確輸出時區縮略名或者完整的名字,還需要時區ID。