2020年5月3日 星期日

[六角學院] [攻略心得] JavaScript 新手 JS 地下城4F - 時區




[攻略前言]


這關的要求很簡單明確,
其實只要知道怎麼取得各時區時間就收工了XD



[攻略過程]


這邊先劃重點,主要需要解決的 5 個問題:
  1. 怎麼取得各時區時間
  2. 我要怎麼知道各時區的 timeZone 字串長怎樣?
  3. 針對髒資料(不符合格式)要先濾除
  4. 承上,符合格式但會造成執行錯誤,則要做特別處理,讓後面程式可以繼續執行下去
  5. 月份是要輸出成英文字的。


雖然前面提到只要知道怎麼取得各時區時間就解決了,
其實對於取得各時區時間我是沒想法的,
所以還是偷偷看了樓主的部落格教學文。

取得指定時區的時間語法如下:

let currentTime = new Date();
currentTime.toLocaleString("en-US", {timeZone: "America/New_York", hour12: false});

這樣就可以取得紐約的當地時間。
(hour12 :false 表示要用24小時制)

接下來我的問題就來了,
我要怎麼知道各時區的 timeZone 字串長怎樣?

上網搜尋了一下,
也朝有沒有開放 timezone 的 API 的方向尋找,
有是有不過都要註冊取得 API key 或 token 才能用,
所以就放棄了orz

然後就找到有一個大大很厲(ㄏㄠˇ)害(ㄒㄧㄣ)的把全部時區的字串列出來!
Stack Overflow | How to get list of all timezones in javascript

---

【2020.05.03 更新】

皇天不負苦心人,我找到提供 timezone 的 json 了!!!
jsDelivr timezones.json

初步看了一下 json 資料長這樣:

[
  {
    "value": "Dateline Standard Time",
    "abbr": "DST",
    "offset": -12,
    "isdst": false,
    "text": "(UTC-12:00) International Date Line West",
    "utc": [
      "Etc/GMT+12"
    ]
  },
  {
    "value": "UTC-11",
    "abbr": "U",
    "offset": -11,
    "isdst": false,
    "text": "(UTC-11:00) Coordinated Universal Time-11",
    "utc": [
      "Etc/GMT+11",
      "Pacific/Midway",
      "Pacific/Niue",
      "Pacific/Pago_Pago"
    ]
  },

看起來可以用!
所以我就加了一段去問 timezone 資料的處理:

let requestURL = "https://cdn.jsdelivr.net/npm/timezones.json@1.5.2/timezones.json";
let xhr = new XMLHttpRequest();
xhr.open("GET",requestURL,true);
xhr.setRequestHeader("Content-type","application/x-www-form-urlencoded"); //post最常用的,告訴對方要傳怎樣的格式過去
xhr.responseType = 'json';
xhr.send();
// onload 是等資料拿到的時候才執行
xhr.onload = function(){
    timeZonesData = xhr.response;
    // *****等拿到資料再初始化頁面
    processTimezone(timeZonesData); // 將拿到的資料做初步處理
}

一開始有看到會有像 "Etc/GMT+12"、"PST8PDT" 這樣的資料,

所以我有寫正規表示式將這樣的資料濾除。

 // 比對資料中有包含 "Etc/GMT+12" 就不存在最後的時區資料中
 // 資料中還有包含 "PST8PDT" 這種資料,所以要含有 / 才存在最後時區資料
 let matchPattern =  new RegExp('[^etc].*/.*', 'i'); // [^]: 不在括號內的任何字元
 let matchResult = matchPattern.test(timeZonesData[i].utc[j]); // test 是回傳字串內"是否"含有正規表達式指定的字元。
 if ( matchResult == true ){
  timeZones.push(timeZonesData[i].utc[j]);
 }

正規表示式參考網站:



本來以為這樣就沒事了,
沒想到我遇到髒資料的問題orz
資料中有不合法的時區資料 (Antarctica/McMurdo),
導致 toLocaleString 回傳 error ,後面就不會繼續處理了。








所以針對導致執行錯誤的不合法資料要做特別處理,
讓後面程式可以繼續執行下去~~~


這邊就會用到 try catch 的用法,
將執行可能會出現不預期錯誤的語法包在 try 裡面,
遇到 error 的處理就寫在 catch 裡面。

try{
    let currentTime = new Date();
    let localTime = currentTime.toLocaleString("en-US", { timeZone: "Asia/Vladivost"} );
    console.log(localTime);

} catch (e) {
    console.log(e);
}

try catch 很常看到,
今天終於看懂而且會用了!

---

再來就是各種 split 將想要的內容切出來就收工了XD

不過我還有遇到一個問題,
因為用上面語法印出來結果長這樣:




上面依 en-US 的格式,年、月、日全部都是以數字呈現,
但題目原本的要求,月份是要輸出成英文字的。







但我總不能笨笨的用 switch case,
1 就轉換成 JAN,2就轉換成 FEB ....... 囧
所以再來找一下有沒有什麼時間格式的轉換方法。

找到這篇有提到一些有關 Date 的 option 用法,
Intl.DateTimeFormat - JavaScript | MDN

其中這段:
var date = new Date(Date.UTC(2012, 11, 20, 3, 0, 0, 200));
// request a weekday along with a long date
var options = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' };
// an application may want to use UTC and make that visible
console.log(new Intl.DateTimeFormat('en-US', options).format(date));
// → "Thursday, December 20, 2012, GMT"

太好了,有直接將月份變成英文的方法! → month: 'long'
再來就是將月份切出來之後,還要記得切前3碼,再轉成大寫。
dateStr.split(" ")[0].substr(0,3).toUpperCase()

substr(開始切的位置,切長度多少出來)
toUpperCase() 將英文字轉成大寫

(PS. 我原本想說時間跟年月日的 option 可以寫在一起,
但寫在一起後好像會後蓋前,所以我後來分開了,
也就是 toLocaleString 2次)
// 取得該地區日期
// month: "long"->會印出 April
let options = { timeZone: timeZones[i], year: "numeric", month: "long", day: "numeric" };
let dateStr = currentTime.toLocaleString("en-US", options);

// 取得該地區時間
// 如果把 year: "numeric" 等設定寫在下面那行,則無法print出時間,只會印出日期
// hour12: false -> 24小時制
let timeStr = currentTime.toLocaleString("en-US", {timeZone: timeZones[i], hour12: false});


再來都是一些細微處理了,
例如將城市的字串切出來後,發現城市中間帶_,
顯示的時候要將_取代為空白鍵。

// 如果城市有_符號,replace成空白鍵
// /_/g 是正規表示式,比對/ /裡面的字串,/g表示要比對多個,沒寫/g就會只比對1個
currentTimeLocalList.City = currentTimeLocalList.City.replace(/_/g," ");


再來想到世界時區這麼多,我想一邊看世界各地的時間時,
可以同時比較跟台灣的時差有多少,
所以就順便把 2F 時鐘的內容放進來(顯示台灣的時間)。

還有 timezone 字串的前面會帶洲別,
所以我也做了一個各洲列表,
點選的時候可以只看到該洲地區的時間。

哦對了,順道一提,
跟 2F - 時鐘 一樣,
為了要讓時間會一直更新要記得用 setInterval(updateTime, 1000); 語法,
但為了怕時間差問題,我並不是採用每分鐘去呼叫一次函數,
而是每秒,
但如何避免每次都去重新渲染一次網頁呢?
我有將做的時間記錄下來,
與現在時間比較,
如果一樣就不做,
不一樣才做。
例如 23:16 有執行,
我就記錄下來,
雖然每秒都去呼叫,
但 23:16 都不會走到重新渲染的邏輯,
直到 23:17 才會重新渲染。
// 避免每次執行函數都要重新渲染網頁,因此會先比較現在時間跟上次做動時間一不一樣
// 不一樣才重新渲染網頁
    if ( timeRecord != currentTimeStr ){

 }

還有一段我有寫小時>=24,要-24
if ( parseFloat(hour) >= 24 ) {
 hour -= 24;
}


是因為我發現有的國家的時間會顯示超過24小時~
之前只知道日本的電視台有時在說節目播出時間為了避免混淆會這樣講,
沒想到還真的有國家不用24小時制!
三十小時制- 維基百科,自由的百科全書 - Wikipedia

太平洋地區 (Pacific) 好像滿多國家都會使用超過24小時的顯示~
像是 PAGO PAGO (美屬薩摩亞的首都)






[這樓要交的功課]

【書寫能力】請寫一篇 BLOG 來介紹你的挑戰過程,並介紹 JavaScript 如何提供 GMT、UTC 時區語法,以及何謂 TimeStamp。

什麼是 GMT?什麼是UTC?

推薦看這篇→ 到底是 GMT+8 還是 UTC+8 ? | PanSci 泛科學

因為詳細的這篇都寫得很清楚了,
就不多說XD

這邊用我的話簡單詮釋一下:
GMT 就是格林威治標準時間,
在 UTC 出來前已經行之有年,
因此就算後來有測量時間技術比較精準的 UTC 問世,
但大家都用 GMT 了,
而且 GMT 跟 UTC 其實時間相差不多,至少體感無感XD (不到一秒)
因此 GMT+8 跟 UTC+8 可以說是相等的,
所以大部份還是繼續採用 GMT。
而地球自轉速度越來越慢,
所以當 UTC 運轉比 GMT 快一秒的時候,
會有「閏秒」的機制。(等 GMT 一秒)

JavaScript 提供 GMT、UTC 時區的語法

JavaScript 提供時間也分成 GMT 跟 UTC 格式,
詳細可往這邊走 → Date - JavaScript | MDN - Mozilla

let currentTime = new Date();
currentTime
Sat May 02 2020 17:42:58 GMT+0800 (台北標準時間)

GMT 系列:回傳【當地時間】 (所以我現在下以下語法,回傳的是台北時間 GMT+8 )
currentTime.getHours();
17
currentTime.getMinutes();
42
currentTime.getSeconds();
58

UTC 系列:回傳【標準時間】(所以我現在下以下語法,回傳的是 GMT+0 的時間)
currentTime.getUTCHours();
9 (註:17-8=9)
currentTime.getUTCMinutes();
42
currentTime.getUTCSeconds();
58

※同場加映:時區語法的雷

其實看文件就會發現,有一個語法有雷。








let currentTime = new Date();
undefined
currentTime
Sat May 02 2020 17:48:16 GMT+0800 (台北標準時間)
currentTime.getMonth();
4
currentTime.getUTCMonth();
4

現在明明5月為什麼回傳的會是4 XD
原因就是它從 0 開始計算XD (跟陣列一樣)
如果有用到 getMonth 的語法要特別小心。
(還好這關用的 toLocaleString 不會有這個問題XD)

何謂 TimeStamp?

TimeStamp (時間戳記),回傳由 1970-01-01 00:00:00 UTC 開始,到代表時間經過的毫秒數(以負值表示 1970 年之前的時間)。 (一樣這邊有說明→ Date - JavaScript | MDN - Mozilla)

// 取得 TimeStamp
currentTime.getTime()
1588412896570

// 將 TimeStamp 轉成時間格式
let specialTime = new Date(1588412896570);
undefined
specialTime
Sat May 02 2020 17:48:16 GMT+0800 (台北標準時間)

可以用在只能使用數字的地方,(例如2個時間相減)
又是唯一值,
儲存這樣的資料比時間的字串更好處理。



[程式碼]


See the Pen 新手 JS 地下城 4F - 時區 by Ado Lin (@misado) on CodePen.



沒有留言:

張貼留言