JavaScript 有一個內建的物件 Date,用來儲存日期與時間,並提供管理它們的方法。
它是以 Java 的 java.util.Date 類別為基礎,該類別在 2010 年代初被取代,但為了向後相容,JavaScript 的 Date 至今仍保留著。
由於日期本身就很難處理,加上時區處理既糟糕又常常付之闕如,因此市面上存在許多函式庫,例如 moment.js、day.js、date-fns 和 luxon。
這些在 Exercism 上都無法使用。
在你自己的專案中,不要使用 moment.js 這類已棄用或無人維護的套件,請改用 luxon 等更現代的做法,或是尚未普及的 Temporal。
本練習的重點是 Date,它在 JavaScript 走到盡頭之前都會是重要的存在。
Date 物件是 Date 類別的實例。
它可以在不傳任何引數給建構函式的情況下建立。
這樣會得到一個代表目前日期與時間的 Date 物件:
const now = new Date();
// => Thu Apr 14 2022 11:46:08 GMT+0530 (India Standard Time)
// Shows current day, date and time (in your time zone).
如果傳入的是數字,它會被解讀為 timestamp。
時間戳記是一個整數,代表自 1970 年 1 月 1 日 UTC+0 以來所經過的毫秒數。
const epoch = new Date(0);
// Thu Jan 01 1970 01:00:00 GMT+0100 (Central European Standard Time)
const another = new Date(1749508766627);
// Tue Jun 10 2025 00:39:26 GMT+0200 (Central European Summer Time)
你可能會以為 new Date(0) 會產生「最早」的日期物件,但 JavaScript 會把日期轉換成你的本地時區,這表示只有身處 GMT / UTC+0 時區附近的人,才會真正得到 Unix 紀元 的值。
你可以傳入一個代表日期的字串值給 Date 建構函式。
在所有實作之間唯一一致的格式,是國際公認且標準化的所謂 ISO 8601 時間戳記字串 的簡化版本。
一個位於 UTC 的時刻看起來像這樣:
YYYY-MM-DDTHH:mm:ss.mssZ
YYYYMMDDTHHmmss.mssZ
其中各部分的替換如下:
| 鍵 | 說明 | 預設值 |
|---|---|---|
| YYYY | 西元年,以 4 位數表示 | |
| MM | 月份,以 2 位數表示 | 01 |
| DD | 日期,以 2 位數表示 | 01 |
| T | 字面上的字母 T,用來分隔日期與時間 | |
| HH | 24 小時制的小時,以 2 位數表示 | 00 |
| mm | 分鐘,以 2 位數表示 | 00 |
| ss | 秒,以 2 位數表示 | 00 |
| mss | 毫秒,以 3 位數表示 | 000 |
| Z | 字面上的字母 Z,或位移 +/-HH:mm
|
字面上的字母 Z 代表 UTC(沒有時區,也沒有日光節約時間)。
由於大多數組成部分都有預設值,因此省略結尾的部分是有效的:
YYYY-MM-DD
預設時間為 00:00:00.000。
如果時間戳記的結尾不是 Z,也不是以 +HH:mm 或 -HH:mm(代表時區位移)結尾,那麼基於歷史因素,下列規則適用:
當缺少時區位移時,只含日期的格式會解讀為 UTC 時間,而含日期與時間的格式則會解讀為本地時間。 解讀成 UTC 時間是源自一個歷史上的規格錯誤,它與 ISO 8601 並不一致,但因為網頁相容性的緣故已無法更動。 請參閱 損壞的剖析器:一個網頁現實問題。
其他 Date.parse 接受的格式可能可行,也可能不行。
在 JavaScript 中處理日期時,從 string 轉換成 Date 時_永遠_要使用 ISO 8601 時間戳記。
只含日期的格式是允許的,但並非所有 ISO 8601 格式都受到支援。 請參閱 MDN 上的簡化版本說明頁面。
Date 物件既有的日期物件也可以當作建構函式的引數傳入。
這會複製既有的 Date 物件,並帶有相同的日期與時間。
const t1 = new Date();
const t2 = new Date(t1);
// Values of t1 and t2 will be the same.
傳入三個數字可以建立一個代表日期的物件。 傳入 6 個數字則可以建立一個代表日期與時間的物件。
const date1 = new Date(95, 11, 17);
// Creates Date for Dec 17 1995 00:00 if your local timezone is equivalent to UTC.
const date2 = new Date(2013, 12, 5, 13, 24, 0);
// Creates Date for Jan 5 2014 13:24 if your local timezone is equivalent to UTC.
第二個值是 month,從代表一月的 0 開始,一直到代表十二月的 11。
Date.parse()你可能會看到有人提到或引用一個日期剖析函式 Date.parse。
由於這個函式只有少數不變的規則,其餘的實作並未被規定(因此也不標準),所以不應該使用它。
Date 的組成部分日期物件上有各種方法可以回傳日期的組成部分:
getFullYear(); // Get the year (4 digits)
getMonth(); // Get the month, from 0 to 11.
getDate(); // Get the day of month, from 1 to 31.
getHours(); // Get the hour in a 24 clock, from 0 to 23
getMinutes(); // Get the minutes, from 0 to 59
getSeconds(); // Get the seconds, from 0 to 59
getMilliseconds(); // Get the milliseconds, from 0 and 999
getDay(); // Get the day of week, from 0 (Sunday) to 6 (Saturday).
每一個都有對應的 set 版本(例如 setFullYear)可以更新值。
任何超出範圍的值都會進位到下一個組成部分:
const date = new Date('2025-02-28T12:42:00Z');
// => Fri Feb 28 2025 13:42:00 GMT+0100 (Central European Standard Time)
date.setDate(29);
// there was no February 29th in 2025.
date.getDate();
// => 1
date.toString();
// => Sat Mar 01 2025 13:42:00 GMT+0100 (Central European Standard Time)
所有方法都有忽略本地時區的 UTC 版本。
日期物件有一個方法 getTime(),會回傳以毫秒為單位的 UNIX 時間戳記,也就是自 1970 年 1 月 1 日 UTC 午夜起算的毫秒數。
此外,還有 toISOString() 方法可以將日期物件轉換成 ISO 8601 時間戳記字串。
大於(>)與大於等於(>=),以及小於(<)與小於等於(<=),都可以直接用在兩個日期之間,或日期與數字之間。
之所以可行,是因為 JavaScript 會嘗試把日期強制轉型成原始型別。
在比較兩個日期,或比較日期與數字時,JavaScript 會呼叫 [Symbol.toPrimitive]("number"),而它內部會呼叫 date.valueOf()。
後者等同於呼叫 date.getTime()。
如果你不想依賴這種行為,請先用 getTime() 轉換成數字。
日期不能使用相等運算子(== 和 ===)比較,但 .getTime() 的結果可以。
在這道練習中,你會動手實作幾個管理預約的函式。 系統會把所有資料都存成 ISO 8601 格式的字串,但人們並不是這樣使用行事曆的。 因此需要各種函式來在不同格式之間轉換。
建立一筆從現在起 n 天後、時間為當下時刻的預約。
這個函式接收 n 天,並回傳從現在起 n 天後的預約時間。
createAppointment(4, now);
// Given now is Sun Oct 05 2022 23:28:43 GMT+0600 (Bangladesh Standard Time)
// => Sun Oct 09 2022 23:28:43 GMT+0600 (Bangladesh Standard Time)
如果沒有用到第二個參數 now,就應該改用目前時間。
許多工具只支援國際標準化的 ISO 8601 格式。
請撰寫函式getAppointmentTimestamp,接收一個日期並回傳該格式的字串。
const appointment = new Date(Date.UTC(2010, 6, 16, 12, 42, 0, 0));
getAppointmentTimestamp(appointment);
// => '2010-07-16T12:42:00.000Z'
時間戳記不好閱讀,而一個能取得預約詳細資料的函式正好可以幫上忙。
函式getAppointmentDetails接收 ISO 8601 格式的時間戳記,並回傳年、月、日、時、分。
getAppointmentDetails('2022-04-24T08:15:00.000');
// => { year: 2022, month: 3, date: 24, hour: 8, minute: 15 }
這個函式的第一個引數是預約時間,第二個引數則是裝有一些選項的物件。 你必須依照物件中的選項更新預約,並回傳新的預約日期。 選項物件裡可能包含多個選項。
updateAppointment('2022-02-09T09:20:00.000', { month: 6 });
// => { year: 2022, month: 6, date: 9, hour: 10, minute: 20 }
這個函式會接收兩筆預約(時間戳記)作為引數。 你必須回傳這兩個時間相差的秒數。
由於半秒幾乎沒有意義,請在回傳前先將數字四捨五入。
timeBetween('2022-12-12T09:20:00.000', '2022-12-18T08:30:00.000');
// => 515400
最後,當預約建立後,系統需要檢查它是否有效。 換句話說,預約必須落在未來,而不是過去。
請撰寫函式isValid,它接收兩個引數:一個是預約的時間戳記(字串),另一個是以時間戳記表示的目前時間(字串);在給定目前時間的情況下,如果該預約位在未來,就回傳true。
isValid('2022-02-11T23:00:00.000', '2022-02-08T23:00:00.000');
// => true