JavaScript Date:計算不同時區的時間
19 Oct 2022利用 JavaScript 的 Date object 計算不同時區的時間。
我的此刻
首先來定義「此刻」是什麼。
在瀏覽器的 console 輸入 new Date()
後,會得到「Tue Oct 18 2022 17:20:49 GMT+0800 (Taipei Standard Time)」這樣的結果,這個就是我所在的時區、目前的時間,就是「我的此刻」。
new Date()
Tue Oct 18 2022 12:00:00 GMT+0800 (Taipei Standard Time)
「我的此刻」是 2022/10/18 的 12PM,比格林威治標準時間快 8 小時。
也就是說,「此刻」和所在地區有關 (時區列表可參考這裡,台灣在 UTC+8 這一塊)。
別人的此刻
既然「此刻」和我所在的地區有關,那麼,我可以推算不同時區,現在的時間是什麼時候嗎?
當然可以,只要知道對方的時區就可以了。
像是,小貓在 UTC+0 (冰島),小豬在 UTC-12 (貝克島),小狗在 UTC+12 (紐西蘭),那他們此刻的時間就是
- 小貓 UTC+0 (冰島):相較我的此刻來說,小貓比我早 8 小時,也就是 2022/10/18 的 4AM。
- 小豬 UTC-12 (貝克島):相較我的此刻來說,小豬比我早 12 小時,也就是 2022/10/17 的 4PM。
- 小狗 UTC+12 (紐西蘭):相較我的此刻來說,小狗比我晚 12 小時,也就是 2022/10/18 的 4PM。
怎麼算別人的時間
先講結論「先從自己所在的時區,推到 UTC+0 然後再加上 timezone」即可。
像是剛剛我們先把我的時間推導出小貓的時間 (UTC+0),然後再去推測小豬 (UTC-12) 和 小狗 (UTC+12) 的時間。
實作為程式碼的推導過程是以下這樣的…
首先,從自己的時間,推算出 UTC+0 的時間。為了便於說明,這裡都是帶入指定的時間「Tue Oct 18 2022 12:00:00 GMT+0800 (Taipei Standard Time)」。
const event = new Date('Tue Oct 18 2022 12:00:00 GMT+0800 (Taipei Standard Time)');
const utcTime = event.toUTCString();
console.log(utcTime);
得到以下結果,目前 UTC+0 的時間是 2022/10/18 4AM。
'Tue, 18 Oct 2022 04:00:00 GMT';
再來,結合時區,像是計算 UTC-12 (貝克島) 的時間。
setUTCHours
帶入目前 UTC+0 的時間,並加上或減去時區,來推算指定時區的時間,得到人看不懂的 timestamp,因此要用 toUTCString 做轉換成人能看懂的字串。
const event = new Date('Tue Oct 18 2022 12:00:00 GMT+0800 (Taipei Standard Time)');
event.setUTCHours(event.getUTCHours() - 12);
const calculatedTime = event.toUTCString();
console.log(calculatedTime);
得到以下結果,目前 UTC-12 的時間是 2022/10/17 4PM。
'Mon, 17 Oct 2022 16:00:00 GMT';
以上會需要這樣計算…是因為無法直接將利用 date time string 帶入時區,來取得該時區的時間 😂
我的幾天前
我的前幾天,要怎麼算呢?
實作 addDays 方法,使用 local 時間加上指定的天數,可傳入要延後 (+) 或提前 (-) 的天數。
Date.prototype.addDays = function (offset) {
this.setDate(this.getDate() + offset);
return this;
};
當我輸入 -7
天時,就可以得到 7 天前的時間,也就是 2022/10/11 12PM。
var today = new Date('Tue Oct 18 2022 12:00:00 GMT+0800 (Taipei Standard Time)');
today; // Tue Oct 18 2022 12:00:00 GMT+0800 (Taipei Standard Time)
today.addDays(-7); // Tue Oct 11 2022 12:00:00 GMT+0800 (Taipei Standard Time)
別人的幾天前
如果我想知道,我的前幾天,是別人的什麼時候呢?
實作 getDateByTimezone 函式。
const getDateByTimezone = ({ givenDate = null, offset = 0, timezone = 0 }) => {
const event = givenDate ? new Date(givenDate) : new Date();
event.addDays(offset); // (1)
event.setUTCHours(event.getUTCHours() + timezone); // (2)
const year = event.getUTCFullYear(); // (3)
const month = event.getUTCMonth() + 1; // (4)
const date = event.getUTCDate();
return `${year}-${month}-${date}`; // (5)
};
說明如下:
- (1) 使用 local 時間加上指定的天數,例如:在台北的我,現在是 10/18,想要推算前一天,就帶入 offset 為
-1
。 - (2) 利用
getUTCHours
取得 UTC+0 時間後加上 timezone,例如:在台北的我,就是帶入 timezone 為+8
,再利用setUTCHours
重新設定為目前的時間。 - (3) 根據 (2) 的結果,利用
getUTCFullYear
、getUTCMonth
、getUTCDate
取得該時區現在時間的年、月、日,也可以得到時分秒等。 - (4) 由於月份計算是從 0 開始,也就是 0 ~ 11 的範圍,因此若想得到我們平日在日曆看到的數字,就必須加一,得到 1 ~ 12 的範圍。
- (5) 客製化要回傳的時間格式
yyyy-mm-dd
,或是利用toUTCString
或toLocaleString
也是可以的。
或是回傳不同的格式。
event.toUTCString().replace(/ GMT$/, ''); // 'Wed, 19 Oct 2022 16:00:00'
event.toLocaleString('en-CA'); // '2022-10-20, 12:00:00 a.m.'
利用前面實作的 getDateByTimezone 函式,來推算我的前一天,是其他人的什麼時刻?來計算小貓、小豬、小狗的前一天時間。
小貓 UTC+0 (冰島) 的前一天是 2022/10/17 04:00 UTC。
getDateByTimezone({
givenDate: 'Tue Oct 18 2022 12:00:00 GMT+0800',
offset: -1,
timezone: 0,
});
從 getDateByTimezone 得到 2022-10-17。
小豬 UTC-12 (貝克島) 的前一天是 2022/10/16 16:00 UTC-12。
getDateByTimezone({
givenDate: 'Tue Oct 18 2022 12:00:00 GMT+0800',
offset: -1,
timezone: -12,
});
從 getDateByTimezone 得到 2022-10-16。
小狗 UTC+12 (紐西蘭) 的前一天是 2022/10/17 16:00 UTC+12。
getDateByTimezone({
givenDate: 'Tue Oct 18 2022 12:00:00 GMT+0800',
offset: -1,
timezone: 12,
});
從 getDateByTimezone 得到 2022-10-17。
總結以下結果,若「此刻」的我是 2022/10/18 12:00 UTC+8,則我的前一天是 2022/10/17 12:00 UTC+8。
- 小貓 UTC+0 (冰島) 的前一天是 2022/10/17 04:00 UTC,從 getDateByTimezone 得到結果 2022-10-17。
- 小豬 UTC-12 (貝克島) 的前一天是 2022/10/16 16:00 UTC-12,從 getDateByTimezone 得到結果 2022-10-16。
- 小狗 UTC+12 (紐西蘭) 的前一天是 2022/10/17 16:00 UTC+12,從 getDateByTimezone 得到結果 2022-10-17。
一些疑難雜症
GTM vs UTC
基本上 GTM 和 UTC 是相同的。那到底差異點是什麼呢?
GTM 是「格林威治標準時間」,由格林威治天文台觀測太陽仰角來計算目前時間;而 UTC 是「世界協調時間」,由更精密的太陽日計算方式而得。相較 GTM,UTC 的準確度更高,只是說你的時間是一秒鐘幾十萬上下,那才會感覺到差異摟!
getYear vs getFullYear vs getUTCFullYear
- 使用 getYear 會拿到減去 1900 後的數字,而且是 local time,不過由於[千禧危機](https://zh.wikipedia.org/zh-tw/2000%E5%B9%B4%E9%97%AE%E9%A2%98{:target=”_blank”},因此已被廢棄。例如:
new Date('Tue Oct 18 2022 12:00:00 GMT+0800 (Taipei Standard Time)').getYear()
會得到 122,建議使用 getFullYear,例如:new Date('Tue Oct 18 2022 12:00:00 GMT+0800 (Taipei Standard Time)').getFullYear()
來得到 2022。 - getUTCFullYear 是指取得 UTC+0 的年份,範例如下
- 2022/12/31 23:00 UTC+11,在時區為 UTC+0 的年份需要減去 11 個小時,是同一天的 12:00,因此得到年份是 2022。
- 2022/12/31 23:00 UTC-11,在時區為 UTC+0 的年份需要加上 11 個小時,是後一天的 10:00,因此得到年份是 2023。
const date1 = new Date('December 31, 2022, 23:00:00 GMT+11:00');
const date2 = new Date('December 31, 2022, 23:00:00 GMT-11:00');
console.log(date1.getUTCFullYear()); // 2022
console.log(date2.getUTCFullYear()); // 2023
Timestamp
timestamp 是表示在 1970/01/01 00:00:00 UTC+0 後經過的毫秒數 (ms),也就是時間的絕對值,沒有時區轉換的問題,因此我們可以經由 timestamp 來換算不同時區的時間。
例如:Date.now()
得到 1667878276459。
由 timestamp 取得 local time (UTC+8)。
new Date(1667878276459)
'Tue Nov 08 2022 11:31:16 GMT+0800 (Taipei Standard Time)'
由 timestamp 取得 UTC+0 的時間。
new Date(1667878276459).toUTCString()
'Tue, 08 Nov 2022 03:31:16 GMT'
若已知日期、時間和時區,要怎麼取得 timestamp 呢?通常會想要轉成 timestamp,就是要利用它能溝通不同時區的時間的特性。
利用 Date.parse
將已知日期、時間和時區轉換成 timestamp。
Date.parse('2022-11-08 GMT+0800')
得到 timestamp 如下。
1667836800000
取得 UTC+0 的時間字串。
const date = new Date(1667836800000);
getDateByTimezone({
givenDate: date,
offset: 0,
timezone: 0,
});
offset 填 0 表示當天。
得到
"2022-11-7"
取得 UTC+8 的時間字串。
const date = new Date(1667836800000);
getDateByTimezone({
givenDate: date,
offset: 0,
timezone: 8,
});
得到
"2022-11-8"
以上證明 timestamp 真的可以溝通不同時區的時間。
Date.now()
vs Date().getTime
Date.now()
和 Date().getTime
同樣是得到回傳 1970/01/01 00:00:00 UTC+0 後經過的毫秒數 (ms)。差異在於,在使用方面,Date.now()
是 Date 物件的 static method,無法經由 instance 來叫用;而 Date().getTime
會先呼叫 Date constructor 的 method 來初始化實體。(instance),再取得 timestamp。也因為叫用方法的差異,在計算效能時,Date.now()
會比 Date().getTime
來得快 (點此測試)。
Date & Time 函式庫…或是?
常見的 Date & Time 函式庫有以下這些…
使用函式庫的優點是簡單便利,但缺點就是要考量的東西頗多,像是一但不維護了怎麼辦?文件是否友善?擴充性如何?和其他 library 的 dependency 如何?多國語系的支援程度?package 大小?生態系?等等一堆問題。
Temporal
我們或許可以考慮 Temporal,Temporal 不是 library 而是標準的提案,這個提案目前在 Stage 3,這樣就能有更好的原生 JavaScript API 以供使用,這裡有介紹文和 polyfills 可以裝來玩玩看。
如下範例所示,(1) 取得 UTC+0 的時間;(2) 我所在的時區是 Asia/Taipei UTC+8;(3) 目前台北的時間 (4) 台北時間 7 天後的日期。
const instant = Temporal.Now.instant(); // (1) current time in UTC+0
console.log('current time in UTC+0: ', instant);
const timezone = Temporal.Now.timeZone();
console.log('local timezone: ', timezone); // (2) local timezone
const localDateTime = Temporal.Now.zonedDateTimeISO();
console.log('local time: ', localDateTime.toString()); // (3) local time
const nextSevenDays = localDateTime.add({ days: 7 }).toString();
console.log('next 7 days in local time: ', nextSevenDays); // (4) next 7 days in UTC+8
總結
為了避免時間計算錯誤,有幾個小技巧:
- 一律由前端在瀏覽器取得 local time,這樣才能確切知道目前使用者所在時區的時間。
- 如果覺得人生太複雜,一律將 local time 轉到 UTC+0 再做後續處理。
- 關於跨時區計算的步驟:(1) 取得 local time;(2) 轉換至 UTC+0;(3) 依照目標的時區加/減相對 UTC+0 所需要的小時,例如 UTC+8 表示加 8 小時,UTC-11 表示減 11 小時。
- 細節 1:注意傳入時間的格式,格式不同會得到不同時區的時間或是不合法的格式,如下例所示:
- (O)
new Date('December 31, 2022, 23:00:00 GMT+11:00')
會得到指定 timezone 的時間,在這裡是 UTC+11。 - (O)
new Date(2022, 12, 21, 23)
會得到 local time。 - (X)
new Date('2022--12--21')
並非合法的格式,因此會得到錯誤訊息「Invalid Date」。
合法的格式可參考這裡,並且截圖如下。
- (O)
- 細節 2:只有 UTC 字樣的方法 (例如:getUTCFullYear、getUTCMonth 和 getUTCDate) 才能取得 UTC+0 的時間,其他都是取得 local time。
- 撰寫單元測試時,記得多測幾個時區,例如:UTC+12、UTC+0、UTC-12,這樣就能保證對所有時區的計算是正確的,不是剛好猜中 😂