【ITニュース解説】new Date('2026-03-01') is 28 February if your user is in California
2026年09月11日に「Dev.to」が公開したITニュース「new Date('2026-03-01') is 28 February if your user is in California」について初心者にもわかりやすく解説しています。
ITニュース概要
JavaScriptの`Date`オブジェクトは、日付文字列の解釈や日付計算でタイムゾーン・夏時間の影響を受け、意図しない日付になることがある。特に`YYYY-MM-DD`形式は危険。文字列でなく年・月・日の数値から`Date`を作り、日数計算も日単位で行うことで、日付のずれやバグを防げる。テストは別タイムゾーンでも実施しよう。
ITニュース解説
システム開発において、日付や時刻の扱いは、一見すると単純な機能に見えて、実は非常に複雑で多くの落とし穴が潜んでいる。ユーザーが入力した日付をそのまま表示するだけの処理も、思わぬバグを引き起こす可能性がある。JavaScriptのDateオブジェクトを使った日付処理で発生しやすい典型的な三つの問題と、その解決策について詳しく解説する。
一つ目の問題は、タイムゾーンによって日付がずれてしまう現象である。Webフォームの<input type="date">要素から取得した「2026-03-01」のような日付文字列をnew Date()に渡して日付オブジェクトを生成する際、ECMAScriptの仕様により、この形式の文字列はUTC(協定世界時、世界の標準となる時刻)の午前0時として解釈される。例えば、「2026-03-01」は「2026年3月1日 00:00:00 UTC」となる。この日付をtoLocaleDateStringメソッドでユーザーのいる地域のローカルタイムゾーンに合わせて表示しようとすると、問題が発生する。もしユーザーがロサンゼルスにいる場合、ロサンゼルスはUTCより8時間遅れている(夏時間ではない場合)。そのため、「2026年3月1日 00:00:00 UTC」は、ロサンゼルスの時間では「2026年2月28日 16:00:00」(午後4時)と解釈される。結果として、ユーザーが「3月1日」を選択したにもかかわらず、画面や出力される書類には「2月28日」と表示されてしまう。一方、UTCより進んでいるタイムゾーン(例えばヘルシンキはUTCより2時間進んでいる)では、この現象は発生せず、正しく「3月1日」と表示される。このため、開発者がUTCに近いタイムゾーンにいる場合、このバグに気づきにくいという隠れた危険性がある。
二つ目の問題は、存在しない日付が自動的に補正されてしまうことである。開発者がnew Date()に「2026-02-30」(2月30日)のような日付として存在しない文字列を渡した場合、JavaScriptのDateオブジェクトはエラーを発生させるのではなく、自動的に日付を繰り上げて「2026-03-02」(3月2日)として解釈してしまう。これは、Dateオブジェクトが、不正な日付を「無効な日付」として扱うのではなく、「次の有効な日付」に変換しようとする挙動によるものだ。その結果、本来無効な入力であるはずのデータが、Number.isNaN(d.getTime())のような一般的なチェックでは「有効な日付」として処理されてしまい、開発者が意図しない日付がシステム内で使われてしまう可能性がある。例えば、退職日が3月1日なのに、ユーザーが誤って2月30日と入力してしまった場合、システムがそれを3月2日と解釈し、最終的な退職日が間違った日付になってしまう、といった深刻な事態も考えられる。
三つ目の問題は、日付の加算をミリ秒単位で行うことの危険性である。例えば、「今日から2週間後」の日付を計算したい場合、start.getTime() + 14 * 86400000(14日×1日のミリ秒数)のように、現在時刻のミリ秒に日数をミリ秒に換算した値を足し算したくなるだろう。しかし、この方法は夏時間(DST)が導入されている地域では正確な結果を返さない。夏時間が切り替わる日には、1日が23時間になったり、逆に25時間になったりする。そのため、固定のミリ秒数を加算すると、カレンダー上の正確な日数とは異なる日付になってしまうのだ。記事の例では、10月25日から2週間後を計算する際に、夏時間の切り替わりを挟むことで、ミリ秒計算では11月7日となるが、カレンダー上の正確な計算では11月8日となる。これは、Dateオブジェクトが「時間軸上の一点(瞬間の時間)」と「カレンダー上の日」という異なる概念を混同していることに起因する。ミリ秒での加算は物理的な時間の加算であり、カレンダー上の日数の加算とは異なる。
これらの問題を解決するためには、日付の扱い方を根本的に見直す必要がある。 具体的な修正方法としては、以下の三つの変更点が挙げられる。
まず、日付文字列の解析を厳密に行うことである。new Date()に直接文字列を渡すことを避け、入力される日付の形式を厳密にチェックする。例えば、正規表現を使って「YYYY-MM-DD」形式であるかを確認し、この形式に合致しない文字列は日付として処理せず、元の入力値をそのまま返す。これにより、システムが意図しない形式の文字列を誤って日付として解釈することを防ぎ、ユーザーが入力した「3月末」のような自由記述のテキストも、そのまま受け入れることができる。
次に、日付オブジェクトを年、月、日の各コンポーネントからローカル時刻として生成することである。new Date(year, month - 1, day)の形式で日付オブジェクトを生成する。この形式で日付を作成すると、JavaScriptはUTCではなく、実行環境のローカルタイムゾーンで日付を構築する。月は0から始まるため、例えば3月は「2」として指定する必要がある。この方法ならば、前述のタイムゾーンによる日付のずれが発生しない。
そして、**生成した日付が元の入力と一致するか確認する(ラウンドトリップチェック)**ことである。年、月、日の各コンポーネントからDateオブジェクトを生成した後、そのオブジェクトから改めて年、月、日を取得し、元の入力値と比較する。もしnew Date(2026, 1, 30)(2026年2月30日)のように不正な日付が入力された場合、自動的に2026-03-02に繰り上がってしまうため、取得した日付が元の入力日と一致しないことで、不正な入力が補正されたことを検出できる。これにより、自動的な日付の繰り上がりを防ぎ、ユーザーに正確な入力を促すか、エラーとして処理できるようになる。
日付の加算についても同様に、ミリ秒ではなく年、月、日のコンポーネントを直接操作するべきである。const end = new Date(start.getFullYear(), start.getMonth(), start.getDate() + 14);のように記述することで、Dateオブジェクトが自動的に月の変わり目や年の変わり目、そして夏時間の調整を正確に行ってくれる。これは、日付の加算が「カレンダー上の操作」であり、物理的な時間の加算ではないという根本的な理解に基づいている。
最後に、これらのバグを未然に防ぐための一般的なルールとテストの習慣についても言及する。Dateオブジェクトが「時間軸上の一点(インスタント)」と「カレンダー上の日」という二つの異なる概念を混同していることが、多くの日付関連バグの根本原因である。誕生日や締め切り、最終勤務日などはカレンダー上の日であり、特定のタイムゾーンを紐付けない限り、時間軸上の一点として存在しない。
この種の問題を早期に発見するためには、テスト環境の工夫が不可欠である。一つは、CI(継続的インテグレーション)環境で、少なくとも一つはUTCから負のオフセットを持つタイムゾーン(例: TZ=America/Los_Angeles)を設定してテストを実行することである。開発者がUTCやヨーロッパのタイムゾーンでしかテストしていない場合、アメリカ大陸などで発生するタイムゾーン起因のバグは見過ごされてしまう可能性が高い。もう一つは、夏時間(DST)の境界日を意識的にテストに含めることである。ターゲットとするタイムゾーンで夏時間が切り替わる具体的な週末の日付をテストデータとして用意し、その日付を跨ぐ計算が正しく行われるかを確認する。これは、ミリ秒での日付加算のような、一見正しそうに見えるロジックの落とし穴を検出する唯一の方法である。
これらの注意点を理解し、適切な方法で日付を扱うことは、システムの信頼性を確保するために極めて重要である。たった数行のコードでも、日付処理には常に慎重な設計と徹底したテストが求められるのだ。