職業プログラマの休日出勤

職業プログラマによる日曜自宅プログラミングや思考実験の成果たち。リアル休日出勤が発生すると更新が滞りがちになる。記事の内容は個人の意見であり、所属している(いた)組織の意見ではない。

JSの Date.parse の 挙動メモ

JavaScriptDate.parse の挙動が、期待していたものと違っていたことから、仕事で変なバグを作り込んでしまいました。
このブログ記事はその反省文です。
タイトルは575です。

何をやろうとしたの?

サーバから「yyyy年m月d日」の文字列が落ちてくるアプリケーションで、それをparseしてDateオブジェクトを得て、データの大小比較などに使う、生JavaScriptのコードでした。
環境的にとても特殊な業務であって、ビルドやデプロイが「1ファイルを置くだけ」であるべきという厳しい制約があったため、TypeScriptは利用できませんし、サードパーティのライブラリを利用することもできませんでした。そのため生のJavaScriptで書く必要がありました。

なお、日付の文字列は、月や日の数字の頭の0埋めが為されていないデータです。例えば 2022年3月4日 といった文字列が落ちてきます。

バグってるコードは、どんなの?

こんな感じのコードでした。

function parseDateWithBug(dateString) {
	return new Date(dateString.replaceAll(/[年月]/g, '-').replaceAll(/日/g, ''));
}

日本語で言うと、こうなりますね:与えられた日付の「年」と「月」をハイフン「-」に置き換えて、「日」を消し、それをparseしてDateオブジェクトに変換しています。

「ブログ記事のタイトルでは Date.parse って言ってるのに、呼んでないじゃないか〜〜」というお叱りはあるかもしれませんが、new Date() に文字列を与えた時の挙動は Date.parse() と同じですから許して😂

どういうバグなの…?

parseDateWithBug('2022年9月30日') の実行結果は、

  • Firefoxでは Fri Sep 30 2022 09:00:00 GMT+0900 (日本標準時) なDateオブジェクトで、
  • Chromeでは Fri Sep 30 2022 00:00:00 GMT+0900 (日本標準時) なDateオブジェクトでした。

よく見ると、時刻が異なっていますね😅
そしてMacSafariでは Invalid Date なDateオブジェクトでした😇😇😇😇😇😇

このように、主要の各ブラウザで挙動がこれだけ異なっているというのがバグの現象です。バグに気付いたキッカケは、Safariでのエラーでした。

各ブラウザの挙動の違い

他にもいくつかの引数について new Date(dateString) というコードで試してみました。

引数 Firefox Chrome Safari
2022-03-30 3月30日 午前9時 3月30日 午前9時 3月30日 午前9時
2022-3-30 3月30日 午前9時 3月30日 午前0時 Invalid Date
2022-10-30 10月30日 午前9時 10月30日 午前9時 10月30日 午前9時
2022/03/30 3月30日 午前0時 3月30日 午前0時 3月30日 午前0時
2022/3/30 3月30日 午前0時 3月30日 午前0時 3月30日 午前0時
2022/10/30 10月30日 午前0時 3月30日 午前0時 10月30日 午前0時

なんで???

この手のことで困ったら、初手としてはmdn先生の資料に当たるのが良いでしょう。
developer.mozilla.org
重要なポイントを抜き出すと

  • Only the ISO 8601 format (YYYY-MM-DDTHH:mm:ss.sssZ) is explicitly specified to be supported. Other formats are implementation-defined and may not work across all browsers.
    • ISO 8601 書式だけが、仕様として明記されており、それ以外の書式は実装依存である
  • When the time zone offset is absent, date-only forms are interpreted as a UTC time and date-time forms are interpreted as local time.
    • タイムゾーンが明記されていない場合、日付だけの文字列はUTCの時刻(訳注:時刻が書いてないのはUTCの0時となり、日本時間の午前9時になるのでしょう)として解釈され、日付時刻の文字列はローカル時刻として解釈される。

…ってな感じですね。ISO 8601 の日付のみの文字列 2022-03-302022-10-30 を与えたときの挙動は、どのブラウザの挙動も仕様通りと言えます。
直感的には 2022-3-30 を与えたときはFirefoxのように 3月30日 午前9時 を返して欲しいところですが、どういう訳か Chrome は午前0時(つまりローカル時刻の0時)として解釈され、Safariではparse失敗しました。正しい ISO 8601 の日付文字列ではないのでブラウザごとに挙動が異なるのは良いのですが、各ブラウザがどういう設計思想に基づいてこの実装に至ったのかを経緯を調べようとして私は力尽きました。ゴメンね😉

さて、区切り文字にハイフン「-」ではなくスラッシュ「/」を用いると、何故か各ブラウザでの挙動が揃いました。みんなローカル時刻として解釈したのか、時刻はローカル時刻の午前0時です。こうなった理由は………やはり私が力尽きて調べがついていません😇😇😇

yyyy年m年d日 の正しいparse

では、私はこの業務の中でどのように立ち回るべきだったのでしょうか?*1
パッと思いつくところでは、以下の案を挙げることができます。

  • 区切り文字にスラッシュ「/」を使う : 短期的には良さそうですが、主要ブラウザが偶然揃っているだけであって、マイナーなエンジンや、主要ブラウザの将来や過去のバージョンだと別の挙動をするかもしれない点には注意です。コードの修正量は明らかにこれが一番少ないので、マイナーなブラウザを無視して良くてシステム自体の稼働が短期的であるなら、非常に有力な案です。
  • 月の数字を、頭0埋めする : きちんと ISO 8601 準拠の日付表示にする作戦です。本来こうあるべきなのでしょう。
  • new Date(year, month, day) 形式で呼び出すようにする : これも良い対応と言えましょう。頭0埋めのコードよりは「読み易い」コードになることが期待できますから、この方策も有力株です。なお、この場合はmonthの数字が1つズレていることに注意です(0始まりでの値を与えるべし)
  • サードパーティのライブラリを採用する : 政治的根回し力を身に付けましょう😇(デプロイする「1ファイル」の中にライブラリを内包しても良いのですが、その手法には生理的嫌悪感ががが……)
  • yyyy年m月d日 の文字列をparseできるように ECMA Script の仕様を変える : みんな頑張って仕事をして、絶大な権力を手に入れて、世界の発展に貢献しましょう😃

環境情報

  • OS : macOS Monterey 12.6 (Venturaでも同様の挙動でしたが、きちんとしたメモを取っておりませんでした。記事執筆マシンがMontereyという状態です)
  • Firefox : 107.0
  • Chrome : 107.0.5304.110(Official Build)
  • Safari : 16.0 (17614.1.25.9.10, 17614) ※本記事執筆時点での最新stableは16.1のはずですが…😇

*1:ようやく、反省文っぽいフレーズが出てきましたね!