JavaScriptのDate()に日付文字列を渡すとどうなるか

皆さん初めまして、バックエンドエンジニアのitohisです。
アイスタイルAdvent Calender2021(2枠目)12/10の担当をさせて頂きます。よろしくお願いします。

今回は社内で私が参画していたプロジェクトで話題になった、 
「JavaScriptのDate()コンストラクターに日付文字列を渡したときにどのような挙動をするか」
といった内容についてご紹介したいと思います。
少しややこしく、誰もが失敗する可能性のある挙動ですので、軽い気持ちでご覧になっていただき、頭の片隅に印象が残ってくれたら嬉しいなと思います。是非ご覧ください。

日付オブジェクトを生成する

さて、JavaScriptではnew Date()と書くと現在時刻からDateインスタンスを生成できます。
ブラウザがChromeの方は、F12を押すと使えるデベロッパーツールで簡単に試すことができます。
今回は、このDate()コンストラクターがどんな動きをするか試してみます。

引数なしで生成する

itohis「まずは引数なしで試してみよう」

const now = new Date();
console.log(now);
// Wed Dec 08 2021 17:49:02 GMT+0900 (日本標準時)
console.log(now.toLocaleString());
// 2021/12/8 17:49:02

一般的なブラウザ実行環境においては、タイムゾーンや、ロケール(言語と地域の情報)が日本のものに設定されていると思われます。
そのままコンソール出力すると見づらいので、toLocalString()でロケールに準拠した文字列を返して見やすくしています。
確認してみると、この記事を執筆しているのが12/8なので、2021/12/8 17:49:02と、今まさにnew Date() した時間を用いてDateオブジェクトが生成されていることがわかりますね。

日付文字列を渡す

itohis「ほう…(腕組み)」

itohis「じゃあ日付を具体的に渡したらその日付になりそうじゃない?」

const now = new Date('2021-12-25 00:00:00');
console.log(now);
// Sat Dec 25 2021 00:00:00 GMT+0900 (日本標準時)
console.log(now.toLocaleString());
// 2021/12/25 0:00:00

itohis「なったわ(笑顔)」

特に理由も悪意もありませんが、今回は「2021-12-25 00:00:00」を指定してみました。
日付の区切り文字はハイフン(-)を使用しました。
指定された通りの 2021/12/25 0:00:00でDateオブジェクトが生成されています。

※この時、itohisは意図の通りにDateオブジェクトが生成されたと思い込んでいますし、日付の値も正しいですが、これはDate()コンストラクターが渡された文字列を解釈して、実行環境からたまたま日本時に合わせてDateオブジェクトを生成したにすぎないという点に留意してください。
つまり、「2021-12-25 00:00:00」は推奨された形でなく、どちらかと言うと身勝手なフォーマットになるということを伝えたいです。

日付文字列を渡すが、時刻を省略する

itohis「面倒だし、時刻省略しちゃおう」

const now = new Date('2021-12-25');
console.log(now);
// Sat Dec 25 2021 09:00:00 GMT+0900 (日本標準時)
console.log(now.toLocaleString());
// 2021/12/25 9:00:00

ドン

itohis「な なにーーーっ!!(画像省略)」

はい、9時間分ずれてますね。
そう、実は「YYYY-MM-DD」の形式で指定すると、協定世界時 (UTC) を基準としてオブジェクトが生成されてしまいます。
設定されているタイムゾーン(Asia/Tokyo)に対する考慮が外れるので、9時間分時刻がずれて生成されてしまうわけですね。

日付文字列から時刻を切り落とし、00:00:00のDateオブジェクトを作ろうとして、この方法を用いるとバグの温床となることがわかりました。
実業務の中でも、ユニットテストで上記方法を使っていたことがあり、テストに手を加えた際に想定通りにテストが通らなくなったことがありました。
あまり気にかけない部分ですが、やはり注意が必要です。

協定世界時 (UTC) とは

そもそも協定世界時(UTC) ってなんやねんって話ですが、簡単に言うと世界各地の標準時であって、日本標準時(JST)から9時間戻した時間になるそうです。よく出てくるもので、UTCと等しいものとされているGMT(グリニッジ標準時)というものもありますが、本題から逸れるので、そちらは調べてみてください。

また、日付と時刻の表記に関する国際規格であるISO8061では、日付と時刻をT記号で区切り、末尾にZ記号をつけるとUTCを表したりするので、
2021-12-25 00:00:00」のようなT記号なしの書き方は異質で、
2021-12-25T00:00:00+09:00」の方がより一般的なのかもしれません。
今回の件も、JavaScriptのDateコンストラクターがそれぞれよしなに処理してくれた結果というわけです。

オレオレでやってしまいがちな日付表記、今回はハイフン(-)で区切りましたが、これを(/)スラッシュに変えてみると、今度は日本時が基準になってイメージ通りに生成されるのでややこしかったりします…

const now = new Date('2021/12/25');
console.log(now);
// Sat Dec 25 2021 00:00:00 GMT+0900 (日本標準時)
console.log(now.toLocaleString());
// 2021/12/25 0:00:00

まとめ

じゃあ結局どうすればいいんですか… 何がベストになるんですか… となりますが、
みんなお世話になってるMDN Web DocsのDateコンストラクターのページを参照してみると、

*注: 日付文字列を Date コンストラクター (または同等の Date.parse) で解釈する方法は、ブラウザーごとに動作が異なり一貫性がないため、避けることを強くおすすめします。*

という注釈が書かれています。
避けることを強くおすすめしますとのことなので、今回の解釈の差異が発生する点や、ブラウザーごとの動作も含め、やはりJavaScriptのDate周りは大人しくライブラリを導入し、誰が触っても直感的に使えるようにした方が良いかもしれません。
つまり、Date()コンストラクターに日付文字列は渡さない方が良いかもしれない、ということです。
実際に当時のプロジェクトでは、日付を扱う場合は日付ライブラリを用いるように舵を取りました。
その結果、ある程度安定して時刻の取り扱いができるようになったと思います。

最後に

長くなってしまいましたが、ここまで読んでくれた方がいらっしゃいましたら、ありがとうございました。
サクッと使ってしまいがちな「new Date()」ですが、今回のご紹介で少しでもお役に立てたらと思います。
ぜひ年末は家族でJavaScriptを触りましょう、ありがとうございました!

煌めくバックエンドエンジニア