原生 JavaScript 和 HTML5 的時間功能

蟲探理查
12分鐘風格

最近和一間公司合作一個 prototype,需要把感測器的資料用圖表呈現,其中會用到一些時間轉換,但是只會在以下幾種常見的格式下轉換:

  1. 和中華電信 IoT 平台 API 溝通,要求的資料格式是 ISO8601 的 UTC 格式。
  2. 瀏覽器所在本地時間的格式。
  3. 瀏覽器內建的日期選擇器的字串格式。

因為不牽涉其它時區,所以我就不用套件了,直接用原生 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
timestamp

要注意的是這個 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 相同,都是格林威治天文台,不是我們手機所在地的時區。

轉換

有上面的型別和時區的觀念後,我們可以來練習一些常用的轉換,要注意的地方:

  1. 後面練習的幾個 method ,注意一定要是 Date 物件才能用,如果你發現不能用,很可能你是用數字字串去呼叫。
    • .setHours()
    • .toISOString()
    • .getDate().setDate()
    • .toLocaleDateString();
    • .getFullyear()
    • .getMonth()
  2. 這幾個 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'})}`
二月 13, 2022 更新