原生 JavaScript 和 HTML5 的時間功能
最近和一間公司合作一個 prototype,需要把感測器的資料用圖表呈現,其中會用到一些時間轉換,但是只會在以下幾種常見的格式下轉換:
- 和中華電信 IoT 平台 API 溝通,要求的資料格式是 ISO8601 的 UTC 格式。
- 瀏覽器所在本地時間的格式。
- 瀏覽器內建的日期選擇器的字串格式。
因為不牽涉其它時區,所以我就不用套件了,直接用原生 JavaScript 處理,並且整理一下這次學到的觀念。
timestamp
處理時間首先要有 timestamp 的背景概念,在資訊的世界裡處理時間的方法,是將紀錄時間的起點設在 1970 年 1 月 1 日 0 點 0 分 0 秒 0 毫秒。
沒寫過 JavaScript 也可以玩看看,打開 Chrome 瀏覽器,鍵盤按下 Fn + F12( windows 電腦直接按 F12),點 console ,然後在出現的 console 裡面打上或貼上 Date.now()
,按 Enter 就可以得到從1970年到現在,共過了幾毫秒了 (1625798004061 毫秒),你就可以跟別人說你有寫過 JavaScript 而且不是寫 Hello World 喔 (誤)。
要注意的是這個 timestamp 是用 UTC 時間 (時區和格林威治天文台相同),所以不管你人位在哪個時區,你得到的 timestamp 會相同。你的手機會因所在時區不同,經過 GPS 或是網路定位,知道你現在人在台北,才會把 timestamp 用 +8 小時的時差,算出正確的台北時間,顯示在你的手機上。
時區和型別
有程式經驗的人會知道型別的重要性,我這次的專案會用到三種型別的轉換:數字(number)、物件(object)、字串(string)。
剛提到的 Date.now() 的型別是一個數字。如果要轉成 Date 的物件型別,可以用 new Date(1625798004061)
,顯示出來的會是瀏覽器的時區,得到 Fri Jul 09 2021 10:33:24 GMT+0800 (Taipei Standard Time)
。
new Date()
參數什麼都不放的話可以直接得到現在的 Date 物件,你可以試著放不同的格式的參數進去,但是都一樣會得到一個 Date 物件。例如:可以放這種格式的字串 new Date('2021-07-09')
,或這種格式的數字 new Date(2021,7,9)
。
如果你用 jsx (或 HTML5) 的 <input type="date" onChange={this.onChange} />
,也就是瀏覽器內建日期選擇器,取得的值或是要給它的 props ,都會是一個時間的字串,時區也是瀏覽器所在的時區,例如:'2021-07-09'
,我們後面會再談到。
我最近弄的這個專案,中華電信 API 要求的格式則是 ISO8601 的 UTC 格式,型別是字串,長這樣 2021-07-09T02:33:24Z
,要注意的是這個時區和 timestamp 相同,都是格林威治天文台,不是我們手機所在地的時區。
轉換
有上面的型別和時區的觀念後,我們可以來練習一些常用的轉換,要注意的地方:
- 後面練習的幾個 method ,注意一定要是 Date 物件才能用,如果你發現不能用,很可能你是用數字或字串去呼叫。
.setHours()
.toISOString()
.getDate()
和.setDate()
.toLocaleDateString();
.getFullyear()
.getMonth()
- 這幾個 method 回傳的值很多不是 Date 物件,要再透過
new Date()
轉成 Date 物件才能繼續用這幾個 method。
練習1:ISO8601 的 UTC 格式轉換
目的是要把一個 Date 物件轉成中華電信要求的格式,才能用來向中華電信發出 request。
const today = new Date();
const requestTime = new Date(today.setHours(0, 0, 0)).toISOString().slice(0, 19) + 'Z';
console.log('中華電信要求的格式:',requestTime)
//中華電信要求的格式: 2021-07-09T02:33:24Z
因為 today 是一個 Date 物件,所以可以用 .setHours(0,0,0)
把 Fri Jul 09 2021 10:33:24 GMT+0800 (Taipei Standard Time)
這個時間設成一天的開始 Fri Jul 09 2021 0:0:0 GMT+0800 (Taipei Standard Time)
。
這裡 .setHours()
回傳的不是 Date 物件,而是數字,所以要再度放入 new Date()
裡面才會變回 Date 物件,才能用接下來要用的 .toISOString()
。
.toISOString()
後就非常接近中華電信要的格式了,是 UTC 的時區的字串,用.slice()把小數點後去掉加上 Z
,就是正確的格式:2021-07-09T02:33:24Z
。
練習2:計算日期
時間要用 Date 物件還有一個重要原因是方便計算,例如,假如我寫程式想算出昨天的日期,今天剛好是 7 月 1 日或 7 月 2 日的狀況就不同,要分開考慮,很麻煩,但是用 Date 物件和它的 methods 來計算就很方便。
const firstDay = new Date('2021-07-01');
const oneDaysBeforeFirstDay = new Date(firstDay.setDate(firstDay.getDate()-1));
console.log(oneDaysBeforeFirstDay);
//Wed Jun 30 2021 08:00:00 GMT+0800 (Taipei Standard Time)
要算 7 月 1 日的前一天的時候,.getDate()
會取得 7 月 1 日的數字 1,我們減 1 之後會變 0,而 .setDate()
會很聰明的知道 0 就是前一個月的最後一天,所以可以 7 月 1 日直接減 1 後,算出來的日期就是 6 月 30 日。
這裡依然要記得 .setDate()
一樣是回傳 timestamp 數字的型別,要經過 new Date()
才能轉成 Date 物件。
練習3:Date 物件轉換成字串
.toLocaleDateString()
可以把日期轉成 2017/7/9
的字串格式,但是如果你是要給 HTLM5 日期選擇器用,將無法直接使用,因為他會需要 2017-07-09
的字串格式。這個轉換的方法我只想到自幹了,如果大家有其它方式,歡迎介紹給我一下。
做法是分別用 .getFullYear()
.getMonth()
.getDate()
分別取得年月日的數字,然後用 .toString()
把數字轉成字串。還要判斷原本取得的數字是一位數還是兩位數,如果是一位數,前面要加上 0
的字串。
另外,.getMonth()
的地方要特別注意一個細節,和 array 的 index 規則一樣,如果得到的數字是 6 ,那麼代表的是 7 月;如果得到的數字是 11,那麼代表的是 12 月,所以得到的數字要先加 1,才能正確轉換。
//把時間物件換成日期選取器相容的格式
formatDate = (dateOriginal) => {
//取得年
const year = dateOriginal.getFullYear().toString();
//取得月份,JS裡面月份數字從0開始,所以需要+1
//如果只有一位數,前面要加上0
const month =
(dateOriginal.getMonth() + 1).toString().length === 1 ?
'0' + (dateOriginal.getMonth() + 1).toString()
:
(dateOriginal.getMonth() + 1).toString();
//取得日
//如果只有一位數,前面要加上0
const day = dateOriginal.getDate().toString().length === 1 ?
'0' + dateOriginal.getDate().toString()
:
dateOriginal.getDate().toString()
//年月日加起來
const dateFormatted = year + '-' + month + '-' + day;
return dateFormatted
}
const today = new Date();
const dateForInput = formatDate(today);
console.log('今天是',dateForInput);
//今天是2021-07-09
日期選擇器
用上面自幹的範例 function 轉換格式後,我們可以用來設定 HTML5 支援的日期選擇器,例如,今天是 7 月 16 日,把這個選擇器能挑選的日期限制在 6 月 28 日到昨天 (max 和 min 的 props),截取日期選擇器的程式碼如下:
<input
className="mv2 ba"
type="date"
placeholder="yyyy-mm-dd"
value={this.state.selectedDate}
onChange={this.onChange}
max={this.formatDate(yesterday)}
min={'2021-06-28'}
/>
不過,這個日期選擇器的 UI 是瀏覽器提供的,雖然 iOS safari 有提供 UI,但是 safari 桌面版並不提供,所以在 mac 上出現的會是你要用鍵盤輸入的介面。如果你不是像我一樣,目前只是要先暫時做一個 prototype 出來的話,可以去找一些現有的套件來用,才容易掌控各版瀏覽器的介面。
其它時區的轉換
如果你的需求會碰到其它時區轉換的話,可以參考這篇文章:利用原生 JavaScript 計算各時區時間。另外也有一些功能強大好用的套件可以用,例如:著名的 Moment.js。如果嫌 Moment.js 已經沒在維護了的話,可以用比較新的 luxon 。
心得
HTML5 和原生 JavaScript 的時間處理,看起來有點繁鎖,但是觀念上並不會太複雜,或許還蠻適合讓 JavaScript 初學者練習,也可以藉此了解到寫程式的過程,常會需要在不同型別和被要求的格式間轉換。大家如果有不同的做法,也歡迎互相交流。
更新
關於前面提到的formDate()
後來在Front-End Developers Taiwan有其它人提供不錯的作法,大家可以去參考看看,我個人最喜歡的作法如下:
const formatDate = (dateOriginal) => `${dateOriginal.toLocaleString('en', {year: 'numeric'})}-${dateOriginal.toLocaleString('en', {month: '2-digit'})}-${dateOriginal.toLocaleString('en', {day: '2-digit'})}`