はじめに
こんにちは、フロントエンド担当の曽我です。
システム開発において「日時の扱い」は、ほぼすべての領域で登場するテーマです。
ユーザーへの情報表示、バックエンド処理、データベースへの記録、サービス間の連携など、その活用は多岐にわたります。
どの場面でも「いつ」という情報を正しく扱う必要があります。
- 人事異動した人が4月1日だけ2人に分身する。
- 有効期間チェックのバグで期限切れ扱いになる。
- 複数の端末で処理の時系列がバラバラになる。
こうした問題の多くは、日時処理の仕様や実装のわずかなズレが原因です。
この記事では、「Webエンジニアなら知っておきたい」こととして、 フロントエンド・バックエンド・データベースといった層を横断し、最低限知っておくべき日時の扱い方を整理します。
コンピューターにおける日付の扱い方
UTC(協定世界時)
UTC(協定世界時) は、現在世界で共通に使われている基準時刻です。
原子時計をもとに定義されており、国や地域に関係なく一意に扱える「国際的な標準時間」です。
かつてはGMT(グリニッジ標準時)が用いられていましたが、現在はUTCが正式な基準となっています。
各地域の時刻は、このUTCを基準にしています。
例えば「UTCから+9時間」の日本標準時(JST)や、「UTCから-5時間」のアメリカ東部標準時(EST)のように表されます。
アメリカなど一部の国では夏時間(DST)が導入されているため、時期によって時差が変わることもあります。
UTCは非常に正確ですが、閏秒を導入して地球の自転に合わせるという特徴があります。
例えば、数年に一度「23時59分60秒」が挿入されることがあります。
地球の自転(天文時)は、極めて正確な原子の振動(原子時)と少しずつズレていきます。この差を補正するのが閏秒の役割です。
なお、将来的には閏秒の導入を廃止する方針も検討されています。2035年以降は、UTCから閏秒がなくなる可能性があります。
UNIX時間
多くのOSやプログラミング環境では、UTCを直接は扱いません。
「1970年1月1日0時0分0秒(UTC)」を基準に、経過秒数を単調に数える仕組みを使います。
この基準をUNIXエポック(UNIX Epoch) と呼びます。そこからの経過秒数(またはミリ秒)が UNIX時間(UNIX Time) です。
UNIX時間は定義上UTCを基準にしています。しかし、閏秒を無視する設計のため、厳密なUTC時刻とはわずかに異なります。
UTCでは「23:59:60」という瞬間が存在することがあります。対してUNIX時間では、その1秒を飛ばしたり同じ秒を繰り返したりして閏秒を無視します。
これにより、「23:59:60」のような例外的な時刻を考慮する必要がなく、1日を常に86400秒として扱えるなど、計算が単純になるため、実装上扱いやすい時間となっています。
この方式には大きな利点があります。 日付を「年・月・日・時・分・秒」といった複雑な構造体で持つ必要がなく、単なる数値として表せるため、以下のような実装上のメリットがあります。
- 2つの日時を大小比較できる
- 「◯秒後」「◯日後」といった計算が単純な加算・減算で行える
- データとしても軽量に扱える
// 現在時刻のUNIX時間(ミリ秒)を取得 const unixTime = Date.now() // 例: 1759536000000 // UNIX時間(ミリ秒)からDateオブジェクトを生成 const date = new Date(unixTime) console.log(date.toISOString()) // "2025-10-04T00:00:00.000Z"
JavaScriptの Date は「UTC時刻」を表示できますが、内部的には「UTCを基準にしたUNIX時間(ミリ秒)」を保持しています。
このため、閏秒の存在は考慮されず、常に連続した時間として扱われます。
各言語における日付型
多くのプログラミング言語やOSでは、このUNIX時間を基礎にしています(一部の実装では独自の内部表現を用いる場合もあります)。
- JavaScript:
Date。内部はUNIX時間(ミリ秒単位) - Python:
datetime。タイムゾーンなし(naive)とあり(aware)がある - Go:
time.Time。UNIX時間(ナノ秒単位)を保持し、タイムゾーン情報を付加できる
つまり、人間が見ると「UTC」や「日本時間」でも、 内部的な表現としてはUNIX時間で記録されていることがほとんどです。
基準の時計
システムはさまざまな場所で「時刻」を扱いますが、それぞれが独自に時計を持っています。 代表的なものは以下の3つです。
- フロントエンド: ユーザー端末が持っている時計(ブラウザやアプリはこれを参照する)
- バックエンド: アプリケーション(AP)サーバーのシステムクロック
- データベース: データベース(DB)サーバーのシステムクロック
一見どれを使っても良さそうに思えますが、これらの時計は必ずしも同じ時刻を指しているとは限りません。 例えば以下のようなケースが考えられます。
ユーザー端末の時計がずれている
手動で合わせていたり、NTPが無効な端末では数分以上のズレが普通に起きる。意図的な日時の改ざんも簡単にできてしまう。サーバーごとに時刻同期の精度が異なる
同じNTPに同期していても、個体差やネットワーク遅延で数msから数百msの差が出ることもある。NTPを利用できない環境で徐々にずれる. オンプレや閉域環境などで外部NTPと同期できないと、時計が少しずつずれていく。
このように、同じ「今」を指しているつもりでもズレが生じることがあります。
こうした問題を避けるために、基準にする時計をどこに置くか が重要です。 一般的には以下のような指針があります。
基準をバックエンドの UTC 時刻とする
バックエンドサーバーの時計を信頼できる基準として使い、可能であればNTPなどで外部の時刻と同期させておく。フロントエンドの時刻は信用しない
ユーザー端末の時刻は手動で変更可能であり、基準として使うのは危険。データベースのクロックは補助的に使う
NOW()などの関数は便利だが、APサーバーとDBサーバーが異なる場合は時刻がズレる可能性を考慮する。
このように「どの時計を信頼するか」を明確に決めておけば、システム全体で一貫した時刻を扱えます。
外部NTPと同期できなくても、システム全体で同じ基準を共有していれば一貫性は確保可能です。
ただし、現実世界の時刻とのズレをどこまで許容できるかは、システム要件に応じた判断が必要です。
日付の受け渡し
システムは単体で動くのではなく、複数のコンポーネント間で日付や時刻をやり取りします。
例えば、フロントエンド・バックエンド・データベースや、外部サービスなどが挙げられます。
このとき問題になるのが「どの形式で受け渡すか」です。
曖昧な表現の危険性
日時の表現方法は多様です。同じ内容でもシステムや言語、文化によって解釈が異なることがあります。例えば、同じ「2025年10月4日9時」を表すつもりでも、次のような書き方が存在します。
2025-10-04 09:002025-10-04 09:00:002025-10-4 09:002025-10-4 9:002025/10/04 09:0010/04/2025 09:00(国や地域によって日付の並びが異なる)2025/10/03 33:00(日跨ぎシフトなどで実務上使われることがある)
フォーマットが曖昧だと、チーム間で扱う形式にズレが生じることがあります。また、システム内で複数の形式が混在する可能性も考えられます。その結果、意図しない形式のため正しくパースできなかったり、誤った日時として扱われたりします。
RFC 3339
システム間で日付や時刻をやり取りする際は、こうした曖昧さを避ける必要があります。 そのため、RFC 3339(ISO 8601に準拠した日時表現) を使うのが一般的です。
RFC 3339形式は、「日付」「時刻」「タイムゾーン」を一意に表現できるため、どのシステム・言語・地域でも同じ瞬間を正しく解釈できます。
| 内容 | 表現例 | 意味 |
|---|---|---|
| UTC の時刻 | 2025-10-04T00:00:00Z |
2025 年 10 月 4 日 0 時(UTC) |
| 日本時間(JST) | 2025-10-04T09:00:00+09:00 |
UTC+9 の同時刻 |
この形式のポイントは以下の3つです。
Tで日付と時刻を区切るZまたは±hh:mmでタイムゾーンを明示する- 桁数・区切り文字が固定されている
多くの言語やAPIでは、RFC 3339形式のUTC表現(末尾が Z)がデフォルトとして採用されています。
const now = new Date() console.log(now.toISOString()) // 例: "2025-10-04T00:00:00.000Z"
時差・タイムゾーンの扱い方
UTCを基準に扱うのが原則ですが、最終的にユーザーに見せる時刻はローカル時刻です。 システム内のどの層で「UTCとローカル時刻の相互変換」を行うかを、明確にしておくことが重要です。
変換の責任を明確にする
一般的には次のように分担します。
- バックエンド:内部的な計算・保存はすべてUTC。APIではUTCのまま返す。
- フロントエンド:ユーザーのタイムゾーンを取得し、表示時にローカル時刻へ変換する。
- データベース:保存はUTC。
TIMESTAMP型などで自動変換する場合は挙動を明示しておく。
「どこで変換するか」を明確にすると、変換処理の抜け漏れや重複を防げます。これにより、時刻の扱いを簡潔に保てます。
夏時間(DST)への対応
夏時間を採用している地域では、同じ日付でも1時間ずれることがあります。 アプリケーション側でローカル時刻へ変換する際は注意が必要です。単に「UTC−5」などの固定オフセットを使うと、夏時間の切り替えに対応できません。
そのため、タイムゾーン名(例: America/New_York)を基準に変換 するのが安全です。
また、夏時間がない地域でも、Asia/Tokyo のようなタイムゾーン名で扱うのが一般的です。
日付のみの扱い
誕生日・営業日・有効期限など、「日付だけを扱いたい」ケースはよくあります。
こうした日付をタイムゾーン付きの日時型で扱うと、意図しない問題が起きることがあります。タイムゾーンの違いによって「日付がずれる」ことがあるためです。
例えば、日本時間での2025-10-04を考えます。これを日時として保存すると、UTCでは2025-10-03になる場合があります。これでは日付が「前日」にずれてしまいます。
2025-10-04T00:00:00+09:00 (日本時間) → 2025-10-03T15:00:00Z (UTC)
このようなズレを避けるには、日付のみを扱う形式で保存するのが安全です。時刻やタイムゾーンを持たない「YYYY-MM-DD」形式などを利用します。実装に関しても、タイムゾーンを持たない日付型、または文字列を利用します。
このように「日付そのもの」に意味がある場合は注意が必要です。システム全体で時刻情報を付与しない運用ルールを徹底しておくと安全です。
日時の範囲の扱い方
システムでは、単一の時刻だけでなく「いつからいつまで」という期間(範囲)を扱う場面も多くあります。
予約、契約、在庫、人事、アイテムの有効期間など、あらゆる業務で期間の表現は欠かせません。
しかし、この「範囲の扱い方」には意外と落とし穴が多く存在します。
閉区間と開区間
範囲を定義する際、始端と終端を含むか/含まないかを明確にする必要があります。
- 閉区間
[start, end]:始端と終端を含む - 開区間
(start, end):始端と終端を含まない - 半開区間(左開右閉)
(start, end]:始端を含まず、終端を含む - 半開区間(左閉右開)
[start, end):始端を含み、終端を含まない
基本的には要件に基づいて適切な形式を選ぶのが理想ですが、
多くのケースでは 半開区間(左閉右開) [start, end) に統一するのがシンプルで安全です。
例えば予約システムを考えてみましょう。
「9:00〜10:00」「10:00〜11:00」という時間帯を閉区間で扱うと「10:00」が重なってしまい、逆に開区間では「10:00」が空白になってしまいます。
その点、半開区間 [start, end) にしておけば、「開始時刻は含むが、終了時刻は含まない」という明確なルールで連続した区間を安全に表現できます。
また、実装上も [a, b) で統一しておくと、範囲判定を常に start <= x < end の形に揃えられるため、ロジックが簡潔になり、ミスを減らすことにもつながります。
const start = new Date('2025-10-04T09:00:00Z')
const end = new Date('2025-10-04T10:00:00Z')
const x = new Date('2025-10-04T09:30:00Z')
console.log(start <= x && x < end) // true
NULL や特殊値の扱い
終了時刻が未定な場合や、「無期限」を表現したい場合があります。その際、終了日時をNULLにしたり、9999-12-31のような「最大日付」で代用したりします。
どちらを使うかは要件によりますが、意味が異なる点に注意が必要です。
NULL:終了がまだ設定されていない(未定)9999-12-31:無期限・上限を表す疑似的な終端。NOT NULL制約を維持したい場合によく使われる。
NULL を使う場合
NULL は「値が存在しない」という意味を正しく表現できます。そのため、意味論的には最も自然な選択です。
しかし、実装上はいくつかの注意点があります。
- 比較演算では
NULLは常に「不明」と扱われるため、end > nowのような判定が期待どおりに動かない(end IS NULL OR end > nowのように明示的な条件を書く必要がある) - 集計・ソート・インデックスなどで扱いが特殊になる
- 型の
NULL警告やチェック処理が増える
このように、NULL は意味的に正しくても実装コストが上がりやすいです。
そのため、「クエリを単純にしたい」「NOT NULL で統一したい」といった理由から、9999-12-31 のような特殊値を採用する設計もよく見られます。
精度と区間の関係
9999-12-31を使う場合でも、区間の定義によって扱い方が異なります。
- 半開区間
[start, end)の場合: 終端を含まないため、9999-12-31は「上限」として安全に使える。 - 閉区間
[start, end]の場合: 終端を含むため、精度の違いによって比較結果が不安定になる。
たとえば以下はすべて9999-12-31ですが、異なる日時を表しています。
9999-12-31T00:00:009999-12-31T23:59:599999-12-31T23:59:59.999
この違いによって、「無期限のデータを取得しようとして漏れる」といったバグを引き起こすことがあります。
一方で、半開区間 [start, end) であれば終端を含まないため、こうした精度問題を気にせず「上限」として 9999-12-31 を安全に利用できます。
おわりに
日時の扱いは、一見単純なようで、システムのあらゆる層に関わりトラブルも多い、奥の深いテーマです。
- 内部ではUTCを使う
- 信頼できる基準の時計を決める
- 受け渡しはRFC 3339形式で行う
- 期間は半開区間で扱う
といった基本を押さえておくだけで、多くのトラブルを防ぐことができます。
特に日時は「なんとなく動いているうちは気づかない」ことが多く、後になってデータの不整合として現れるケースが少なくありません。 だからこそ、システム全体で共通の前提を持ち、「時間をどう扱うか」を明文化しておくことが重要です。 この記事が少しでも役に立てば幸いです。
参考文献
- The Open Group Base Specifications Issue 7 — POSIX.1-2017 - 4.16 Seconds Since the Epoch
- ISO 8601 — Date and time format
- RFC 3339 - Date and Time on the Internet: Timestamps
- ECMAScript 2025 Language Specification - Ecma International - 21.4 Date Objects
- MDN Web Docs - Date - JavaScript
- MDN Web Docs - Intl.DateTimeFormat - JavaScript
- システムにおける日時の扱いについて私の考えを書く