2025年1月09日·1分で読めます

サマータイムのバグ:タイムスタンプとレポートのルール

サマータイム(DST)によるバグを避ける実践的ルール:UTCで瞬間を保存し、IANAタイムゾーンで文脈を保持し、人が理解できるローカル表示と信頼できるレポートを作る方法。

サマータイムのバグ:タイムスタンプとレポートのルール

なぜ普通の製品でこれらのバグが起きるのか

時間のバグは人々がUTCで生活していないことから生じます。人々はローカル時間で生活しており、ローカル時間は前に進んだり戻ったり、年ごとにルールが変わることがあります。同じ瞬間を見ても、二人のユーザーが違う時計表示を見ることがあります。さらに悪いことに、同じローカルの時計表示が異なる実際の瞬間を指すこともあります。

サマータイム(DST)のバグは年に数回しか現れないため見落とされがちです。開発環境では問題が見えず、本番で顧客が切替の週末に予約やタイムシート、レポートを扱うと何かが違って見えることがあります。

チームは通常いくつかのパターンに気づきます:予定が消えるかズレる「失われた1時間」、ログやアラートが重複して見える「重複した1時間」、そして「1日」が23時間や25時間になるために日次合計がずれることです。

これは開発者だけの問題ではありません。サポートには「アプリが会議時間を変えた」というチケットが入ります。経理は日別収益が合わないと気づきます。運用は夜間ジョブが二度走ったり飛ばされたりした理由を探します。「今日作成」フィルタも地域によって結果が異なることがあります。

目標は地味で信頼できることです:意味を失わない方法で時間を保存し、人が期待するようにローカル時間を表示し、変な日でも正しいレポートを作ること。そうすればビジネスのすべての部分が数値を信頼できます。

カスタムコードで作るにせよAppMasterのようなプラットフォームで作るにせよ、ルールは同じです。元の瞬間を保持するタイムスタンプと、その瞬間がユーザーの時計上でどう見えたかを説明するための十分な文脈(ユーザーのタイムゾーンなど)を持ちたいのです。

時間の簡単で平易なモデル

ほとんどのDSTバグは「ある瞬間(moment in time)」と「時計が示す表示(what a clock shows)」を混同することから起きます。これらを分けて考えるとルールはずっと簡単になります。

いくつかの用語(平易な説明):

  • タイムスタンプ:タイムライン上の正確な瞬間(場所に依存しない)。
  • UTC:タイムスタンプを一貫して表すための世界基準の時計。
  • ローカル時間:ある場所で人が壁掛け時計に見る時間(例:ニューヨークの午前9時)。
  • オフセット:ある瞬間のUTCとの差(例:+02:00-05:00)。
  • タイムゾーン:日付ごとのオフセットを決めるルールセットの名前(例:America/New_York)。

オフセットはタイムゾーンと同じではありません。-05:00はある瞬間のUTCとの差しか教えてくれません。夏に**-04:00**に切り替わるか、来年法律が変わるかは分かりません。タイムゾーン名はそれらのルールと履歴を持っています。

DSTはオフセットを変えますが、基礎にあるタイムスタンプ(瞬間)は変わりません。出来事は同じ瞬間に起きており、ローカル時計のラベルだけが変わります。

混乱を生むのは主に二つの時間帯です:

  • 春のスキップ(spring skip):時計が前に進んで、あるローカル時間の範囲が存在しなくなる(例:2:30 AMが存在しない)。
  • 秋の繰り返し(fall repeat):時計が戻って同じローカル時間が二度起きる(例:1:30 AMが曖昧になる)。

秋の繰り返しの間に「1:30 AM」でサポートチケットが作られたら、イベントを正しく並べるためにタイムゾーンと正確な瞬間(UTCタイムスタンプ)が必要です。

ほとんどの問題を防ぐデータルール

多くのDSTバグはフォーマットの問題ではなくデータの問題として始まります。保存された値が曖昧だと、後の画面やレポートが推測を強いられ、その推測同士が食い違います。

ルール1:実際に起きたイベントは絶対的な瞬間(UTC)で保存する

支払い確定、チケットへの返信、シフト開始など、特定の瞬間に起きたことはUTCで保存してください。UTCは前後にジャンプしないので、DST切替の影響を受けません。

例:サポート担当が時計が切り替わる日にニューヨークの現地時間9:15に返信した場合、UTCで保存すれば後でロンドンの誰かがスレッドを見ても順序は正しく保たれます。

ルール2:タイムゾーンの文脈はIANAタイムゾーンIDで保存する

人に分かりやすく時間を表示するには、ユーザーや場所のタイムゾーンが必要です。America/New_YorkEurope/LondonのようなIANAタイムゾーンIDで保存し、「EST」のような曖昧なラベルは避けてください。略称は曖昧になりがちで、オフセットだけではDSTルールを表せません。

シンプルなパターンは:イベント時刻をUTCで保存し、ユーザーやオフィス、デバイスに関連する別のフィールドでtz_idを保持することです。

ルール3:日付のみの値は日付(date)として保存する

誕生日、「毎月5日に更新」や「請求書の期日」などは瞬間ではないことが多く、日付専用のフィールドで保存すべきです。タイムスタンプで保存すると、タイムゾーン変換により前日や翌日に移動してしまうことがあります。

ルール4:ローカル時間をゾーン情報なしで文字列だけで保存しない

2026-03-08 02:309:00 AM のようにタイムゾーンなしで保存するのは避けてください。DST切替時にその時間が曖昧だったり存在しなかったりします。

ローカル入力を受ける必要がある場合は、ローカルの値とタイムゾーンIDの両方を保存し、境界(APIやフォーム送信時)でUTCに変換してください。

レコード種類ごとの保存方針の決め方

多くのDSTバグはレコードタイプの扱いを混同することで起きます。監査ログ、カレンダーの予定、給与締め日などはどれも「日付と時刻」に見えますが、それぞれに必要なデータは異なります。

過去のイベント(既に起きたこと):正確な瞬間、通常はUTCタイムスタンプを保存します。ユーザーに見せた表示を再現する必要があるなら、イベント時点のユーザーのタイムゾーン(America/New_YorkのようなIANA ID)も保存してください。これにより、後でユーザーがプロフィールのタイムゾーンを変えても当時の表示を再現できます。

スケジューリング(ローカルの壁掛け時間で起きるべきこと):意図されたローカルの日付と時刻、そしてタイムゾーンIDを保存してください。UTCに変換して元の値を捨てないでください。例:「3月10日 09:00 in Europe/Berlin」が意図です。UTCは派生値に過ぎず、ルールが変わればその派生値も変わり得ます。

人は移動し、オフィスが移転し、会社の方針が変わるのは普通です。履歴記録ではユーザーがプロフィールのタイムゾーンを更新しても過去の時刻を書き換えないでください。将来の予定では、その予定がユーザーに追従するのか(旅行対応)拠点に固定されるのか(オフィス対応)を決めて、その場所のタイムゾーンを保存してください。

古いデータでローカルタイムしかない場合は厄介です。ソースタイムゾーンが分かるならそれを付けて、古い時刻をローカルとして扱ってください。分からない場合は「フローティング」として扱い、レポートで正直に表示する(例えば保存された値を変換せずにそのまま表示)と良いでしょう。画面やレポートが混同しないよう、こうした値を別フィールドでモデル化するのも有効です。

タイムスタンプを安全に保存する手順

時間処理を集中管理する
アプリ全体で使う共通のビジネスロジックに時間変換ルールをまとめる。
今すぐ構築

DSTバグを防ぐには、曖昧さのない一つの記録方式を選び、表示時だけ変換する運用を徹底します。

チームのルールを文書化してください:データベース内のタイムスタンプはすべてUTCにする、など。コメントやドキュメントに書いておかないと、あとでいつの間にか別の扱いが混入します。

実務的な保存パターンは次の通りです:

  • システムの記録単位としてUTCを選び、フィールド名で明確にする(例:created_at_utc)。
  • 実際に必要なフィールドを追加する:イベント時刻のUTC(例:occurred_at_utc)と、ローカル文脈が必要ならtz_id(IANAタイムゾーンID)を持つ。
  • 入力を受けるときはローカルの日付・時刻とtz_idを収集し、境界で一度だけUTCに変換する。層ごとに何度も変換しない。
  • 保存と検索はUTCで行い、ローカル時間への変換はエッジ(UI、メール、エクスポート)でのみ行う。
  • 支払い、コンプライアンス、スケジューリングなど重要な操作については受け取ったもの(元のローカル文字列、tz_id、計算したUTC)もログに残す。ユーザーが時間を争ったときの監査証跡になります。

例:ユーザーがAmerica/Los_Angelesで「11月5日 9:00」を予定したら、occurred_at_utc = 2026-11-05T17:00:00Ztz_id = America/Los_Angeles を保存します。DSTルールが後で変わっても、そのユーザーが何を意図したかを説明できます。

PostgreSQLでモデルする場合(視覚的なデータモデリングツールを含む)、カラム型を明確にし、一貫してUTCを書き込むことをアプリ側で保証してください。

ユーザーが理解できるローカル時間の表示

時間を読みやすくする
日付・時刻・ゾーンを表示するラベルを追加して、ユーザーが推測しなくて済むUIにする。
プロトタイプを作る

ほとんどのDSTバグはデータベースではなくUIで表面化します。人は画面に表示されたものを見てメッセージにコピペし、それを基に予定を立てます。画面が曖昧だとユーザーは誤解します。

時間が重要な場面(予約、チケット、予定、配送ウィンドウ)では、レシートのように「完全で特定され、ラベル付き」で表示してください。

表示を予測可能に保つためのルール:

  • 日付+時刻+タイムゾーンを表示する(例:「Mar 10, 2026, 9:30 AM America/New_York」)。
  • タイムゾーンラベルは時間の隣に置き、設定に隠さない。
  • 相対表示(「あと2時間」)を出す場合は、近くに正確なタイムスタンプを置く。
  • 共有アイテムでは、閲覧者のローカル時間とイベントのタイムゾーンの両方を表示することを検討する。

DSTのエッジケースには明確な振る舞いを決めておく必要があります。ユーザーが任意の時間を入力できると、やがて存在しない時間や二重の時間を受け入れることになります。

  • 春の前進(missing times):無効な選択を禁止し、次に有効な時間を提案する。
  • 秋の繰り返し(ambiguous times):オフセットを示すか、どちらのインスタンスかを明確に選ばせる(例:「1:30 AM UTC-4」または「1:30 AM UTC-5」)。
  • 既存レコードの編集:書式が変わっても元の瞬間は保持する。

例:ベルリンのサポート担当がニューヨークの顧客と「11月3日 1:30 AM」に通話を予定したとします。秋の巻き戻しでニューヨークのその時間が二度起きるなら、UIが「Nov 3, 1:30 AM (UTC-4)」のように表示すれば混乱は消えます。

嘘をつかないレポートの作り方

同じデータが閲覧者によって異なる合計を示すと信頼が失われます。DSTバグを避けるには、レポートで何を基準にグループ化しているかを決めて、それに従って実装してください。

まず各レポートで「日」が何を意味するかを決めます。サポートは顧客のローカル日を基準に考えることが多いでしょう。経理はアカウントの法的タイムゾーンが必要なことがあります。技術的なレポートはUTC日の方が安全な場合があります。

ローカル日でのグルーピングはDST付近で合計を変えます。春の前進日は1時間が失われ、秋の巻き戻し日は1時間が重複します。ローカル日でグループ化する場合は明確なルールが必要です。

実務的なルール:各レポートに1つの報告用タイムゾーンを決め、ヘッダーに表示する(例:「All dates shown in America/New_York」)。これにより計算が予測可能になり、サポートが参照できる明確な基準ができます。

多地域チームではユーザーにレポートのタイムゾーンを切り替えさせても構いませんが、それは同じデータへの別の見方と扱い、DSTや深夜付近では異なる日桶(バケット)が見えることが普通であると明示してください。

驚きを防ぐための選択肢:

  • レポート日境界を定義し(ユーザーゾーン、アカウントゾーン、またはUTC)、ドキュメント化する。
  • レポート実行時は1つのタイムゾーンを使い、日付範囲の横に表示する。
  • 日次合計は選択したゾーンのローカル日でグループ化する(UTC日ではない)。
  • 時間別チャートでは、秋の巻き戻し日に重複する時間をラベル表示する。
  • 期間は経過秒を保存・合算してから表示形式に変換する。

期間(durations)は注意が必要です。秋の夜に跨る「2時間シフト」は壁掛け時計では3時間に見えることがありますが、実際に働いたのが2時間なら経過時間は2時間です。ユーザーがどちらを期待するかを決め、一貫した丸めルールを適用してください(例:行ごとに丸めず合算後に丸める)。

よくある落とし穴と回避法

時間に強いアプリを出荷する
タイムスタンプを一貫して扱いながら、本番対応のバックエンド・Web・モバイルを生成する。
アプリを構築

DSTバグは「難しい数学」ではなく、時間の扱いに関する小さな前提が徐々に混入することから生まれます。

典型的な失敗はローカルタイムをUTCとしてラベル付けして保存してしまうことです。普段は問題が見えませんが、他のタイムゾーンの人が開くと勝手にシフトします。安全なルールは単純です:瞬間はUTCで保存し、必要ならレコードにローカル解釈に必要な文脈(ユーザーや場所のタイムゾーン)を一緒に保存すること。

もう一つの原因は固定オフセット(例:-05:00)を使うことです。オフセットはDSTや過去のルールを知らないので、実際の変換に誤差が出ます。America/New_YorkのようなIANAタイムゾーンIDを使って、その日付に正しいルールを適用しましょう。

いくつかの習慣で多くの「二重シフト」驚きを避けられます:

  • 境界でのみ変換する:一度だけ解析し、保存し、表示する。
  • 「瞬間(instant)」フィールド(UTC)と「壁掛け時計(wall clock)」フィールド(ローカル日時)を明確に分ける。
  • ローカル解釈が必要なレコードにはタイムゾーンIDを保存する。
  • サーバーのタイムゾーンを無意味にするため、常にUTCで読み書きする。
  • レポートでは報告タイムゾーンを定義し、UIで表示する。

また裏で隠れた変換に注意してください。よくあるパターンは、ユーザーのローカル時間をUTCに変換して保存したが、後でUIライブラリがその値を「ローカル」と仮定して再変換してしまうことです。結果として一時間のズレが発生し、一部のユーザーや特定の日付でのみ現れることになります。

最後に、請求やコンプライアンスの基準にクライアント端末のタイムゾーンを使わないでください。旅行者の端末は移動中にタイムゾーンが変わる可能性があります。代わりに、顧客アカウントのタイムゾーンや拠点のタイムゾーンなど明示的なビジネスルールに基づいてください。

テスト:ほとんどのバグを検出する少数ケース

時間のバグは年に数日しか表に出ないため見逃されます。解決策は正しい瞬間をテストし、それを再現可能にすることです。

DSTを観測するタイムゾーンを1つ選び(例:America/New_YorkEurope/Berlin)、その2つの切替日に対するテストを書いてください。次にDSTを使わないゾーン(例:Asia/SingaporeAfrica/Nairobi)で同じ日付について繰り返しテストし、違いを確認します。

永続的に残すべき5つのテスト

  • 春の前進日:存在しない時間をスケジュールできないこと、変換で存在しない時間が生み出されないことを検証する。
  • 秋の巻き戻し日:同じローカル時間に二つの異なるUTCが対応すること、ログやエクスポートで区別できることを検証する。
  • 日跨ぎ:ローカル時間で日跨ぎするイベントを作り、UTCで見たときのソートとグルーピングが正しいことを確認する。
  • 非DST対照:DSTを使わないゾーンでも同じ日付で変換を繰り返し、結果が安定していることを確認する。
  • レポートのスナップショット:月末やDST週の周辺で期待される合計を保存しておき、変更後に出力を比較する。

実例シナリオ

サポートチームが秋の巻き戻しの夜に「01:30」のフォローアップを予定したとします。UIが表示されたローカル時間だけを保存していると、どちらの「01:30」か分かりません。良いテストは、ローカル表示上は両方とも「01:30」となる二つのUTCタイムスタンプを作り、アプリがそれらを区別して保持することを確認します。

これらのテストは、システムが正しい事実(UTCインスタント、タイムゾーンID、場合によっては元のローカル時間)を保存しているか、またタイムゾーンが変わるときにレポートが正しく表示されるかを素早く示します。

出荷前の簡単チェックリスト

スキーマで時間を正す
視覚的なPostgreSQLデータモデルで分かりやすいタイムスタンプと日付フィールドを設計する。
データをモデリング

DSTバグはアプリがほとんどの日で正しいように見えるため見落とされます。時間を表示、日付でフィルタ、またはレポートをエクスポートする機能を出す前にこのチェックリストを使ってください。

  • 各レポートに対して単一の報告用タイムゾーンを決め(例:「本社の時間」または「ユーザーの時間」)、レポートヘッダーに表示し、テーブル・合計・チャートで一貫させる。
  • すべての「瞬間」はUTCで保存する(created_at, paid_at, message_sent_atなど)。文脈が必要ならIANAタイムゾーンIDを保存する。
  • DSTが適用される可能性がある場合、UTC-5のような固定オフセットで計算しない。日付ごとのタイムゾーンルールで変換する。
  • UI、メール、エクスポートですべての時間に日付・時刻・タイムゾーンのラベルを付け、スクリーンショットやCSVが誤読されないようにする。
  • 小さなDSTテストセットを持つ:春の直前のタイムスタンプ、春の直後、秋の重複時間周辺のタイムスタンプ。

現実的な確認:ニューヨークのサポートマネージャーが「日曜日に作成されたチケット」をエクスポートし、ロンドンの同僚がそのファイルを開いたとき、タイムスタンプがどのタイムゾーンを表しているか両者が推測せずに分かるべきです。

例:タイムゾーンをまたぐサポートワークフロー

DSTのエッジケースを抑える
入力時に明確な選択肢を示して、後で驚かないように欠落・重複時間を扱う。
試してみる

顧客がニューヨークでチケットを作成した週は米国がサマータイムに切り替わっているが、英国はまだ切り替わっていないとします。サポートチームはロンドンにいます。

3月12日に顧客がニューヨーク現地時間09:30にチケットを送信しました。その瞬間はニューヨークがUTC-4なので13:30 UTCです。ロンドンのエージェントはロンドン時間14:10に返信し、その時はロンドンがまだUTC+0なので返信は14:10 UTCです。返信はチケット作成から40分後に来ています。

これをローカル時間だけで保存するとどう壊れるか:

  • 「09:30」と「14:10」をプレーンなタイムスタンプとして保存します。
  • 後のレポート処理が「ニューヨークは常にUTC-5だ」と仮定する(あるいはサーバーのタイムゾーンを使う)。
  • 09:30を14:30 UTCとして扱い、本当の13:30 UTCではないと判断される。
  • SLAの計測が1時間ずれて、2時間以内のSLAを達成していたチケットが遅延扱いになることがあります。

安全なモデルはUIとレポートを一貫させます。イベント時刻をUTCタイムスタンプとして保存し、顧客にはAmerica/New_York、エージェントにはEurope/LondonのようなIANAタイムゾーンIDを保存します。UIでは保存された同じUTC瞬間を、閲覧者のタイムゾーンでその日付のルールを使って表示してください。

週次レポートでは「顧客ローカル日で集計する」といった明確なルールを選びます。America/New_Yorkで日境界を決め(0時から24時)、その境界をUTCに変換してからその範囲内のチケットをカウントします。こうすればDST週でも数字は安定します。

次の一歩:アプリ全体で時間処理を一貫させる

製品がDSTバグに悩まされているなら、まずいくつかのルールを書き出して全体に適用するのが最速です。「まあまあ一致している」状態が時間の問題を生みます。

ルールは短く具体的に保ってください:

  • 保存フォーマット: 何を保存するか(通常はUTCの瞬間)と絶対に保存しないもの(ゾーンなしの曖昧なローカル時間)。
  • レポートのタイムゾーン: デフォルトでどのゾーンを使うか、ユーザーがどう切り替えられるか。
  • UIラベリング: 時刻の横に何を表示するか(例:「Mar 10, 09:00 (America/New_York)」 vs 単に「09:00」)。
  • 丸めルール: どのゾーンで時間をバケット化するか(時間、日、週)。
  • 監査フィールド: どのタイムスタンプが「イベント発生」を示し、どれが「レコード作成/更新」を示すか。

リスクが低い方法で展開してください。まず新しいレコードの扱いを直して問題の拡大を止めます。次に履歴データをバッチで移行します。移行中は元の値と正規化後の値の両方をしばらく保持して、レポートの差分を確認できるようにします。

AppMaster(appmaster.io)を使っている場合の実務的な利点は、これらのルールをデータモデルと共通ビジネスロジックに集中させられることです:UTCタイムスタンプを一貫して保存し、ローカルの意味が必要なレコードにはIANAタイムゾーンIDを保持し、入力と表示の境界で変換を行うようにします。

次の実務的なステップとしては、1つのタイムゾーン安全なレポート(例:「日別に解決したチケット数」)を作り、上記のテストケースで検証することです。DST切替週に二つの異なるタイムゾーンで正しく動作すれば、概ね安心できます。

よくある質問

コードが正しく見えてもDSTバグはなぜ起きるのですか?

サマータイムはローカルのオフセットを変えるだけで、イベントが起きた実際の瞬間は変わりません。ローカルの時計表示をそのまま「瞬間」と扱うと、春には「存在しない時間」が発生し、秋には「重複する時間」が生じます。

データベースにはタイムスタンプをどう保存するのが最も安全ですか?

実際に起きたイベントはUTCという絶対的な瞬間(インスタント)として保存するのが安全です。オフセットが変わっても値は変動しません。表示時にだけ、閲覧者のローカル時間へ変換し、変換には実際のタイムゾーンIDを使ってください。

タイムゾーン名の代わりにUTCオフセットだけ保存してはいけないのはなぜですか?

-05:00のようなオフセットは、その瞬間のUTCとの差だけを示しますが、DSTのルールや過去の変更は含みません。America/New_YorkのようなIANAタイムゾーンは、その日付に応じたルールと履歴を持つため、正しい変換ができます。

いつ値を日付として保存し、タイムスタンプにしないべきですか?

誕生日、請求書の期日、毎月の更新日など、瞬間ではない値は日付だけ(date型)で保存してください。タイムスタンプとして保存すると、タイムゾーン変換により前日や翌日にずれることがあります。

DST切替でスキップされたり重複したりする時間をアプリはどう扱うべきですか?

「春の前進(spring-forward)」ではローカル時間が存在しないため、無効な選択をブロックして次の有効な時間を提案します。「秋の巻き戻し(fall-back)」では同じ時刻が二度発生するので、ユーザーにどちらのインスタンスかを選ばせる(またはオフセットを表示する)必要があります。

スケジュールを保存するときにUTCに変換して良いですか?

スケジューリングでは、ユーザーの意図はローカルの日時とタイムゾーンIDなので、それをそのまま保存してください。実行のために派生したUTCインスタントを同時に保存しても良いですが、元のローカル意図を捨てないでください。ルールが変わったときに意味を失います。

異なるユーザーで日次合計がズレるのを防ぐには?

レポートごとに「日」の定義を決め、ヘッダーなどで明示してください(例:「All dates shown in America/New_York」)。ローカル日で集計するとDSTの影響で23時間や25時間の日が出ますが、どのタイムゾーンで集計しているかが明確なら問題ありません。

「1時間ズレる」バグの最も多い原因は?

よくあるミスは、入力時に一度だけ変換するというルールを破り、ある層が値をローカルだと仮定して再変換してしまうことです。境界で一度だけ解析・保存し、表示時にフォーマットするという基本ルールを守ってください。

DSTの変化をまたぐ期間はどう計算すべきですか?

経過時間は秒などの絶対単位で保存して合算し、表示用に整形してください。勤怠やSLAで「壁掛け時計の時間(wall-clock)」を期待するのか、実際の経過時間を期待するのかを先に決め、一貫した丸め(例:合算後に丸め)を適用しましょう。DSTの夜は壁掛け時間が長く見えることがありますが、経過秒は変わりません。

顧客に見つかる前にDSTバグを見つけるテストは何ですか?

少なくとも1つのDST適用ゾーン(例:America/New_YorkEurope/Berlin)で春と秋の切替日をテストし、DST非適用ゾーン(例:Asia/SingaporeAfrica/Nairobi)で対照テストを行ってください。欠落時間、重複時間、日跨ぎ、レポートのバケット化を含めると、問題の大半を捕まえられます。

始めやすい
何かを作成する 素晴らしい

無料プランで AppMaster を試してみてください。
準備が整ったら、適切なサブスクリプションを選択できます。

始める