JavaScriptの Date.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オブジェクトでした。
よく見ると、時刻が異なっていますね😅
そしてMacのSafariでは 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.
…ってな感じですね。ISO 8601 の日付のみの文字列 2022-03-30
と 2022-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 の仕様を変える : みんな頑張って仕事をして、絶大な権力を手に入れて、世界の発展に貢献しましょう😃
環境情報
*1:ようやく、反省文っぽいフレーズが出てきましたね!