Webエンジニアなら知っておきたい日時の扱い

はじめに

こんにちは、フロントエンド担当の曽我です。

システム開発において「日時の扱い」は、ほぼすべての領域で登場するテーマです。
ユーザーへの情報表示、バックエンド処理、データベースへの記録、サービス間の連携など、その活用は多岐にわたります。
どの場面でも「いつ」という情報を正しく扱う必要があります。

  • 人事異動した人が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:00
  • 2025-10-04 09:00:00
  • 2025-10-4 09:00
  • 2025-10-4 9:00
  • 2025/10/04 09:00
  • 10/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:00
  • 9999-12-31T23:59:59
  • 9999-12-31T23:59:59.999

この違いによって、「無期限のデータを取得しようとして漏れる」といったバグを引き起こすことがあります。

一方で、半開区間 [start, end) であれば終端を含まないため、こうした精度問題を気にせず「上限」として 9999-12-31 を安全に利用できます。

おわりに

日時の扱いは、一見単純なようで、システムのあらゆる層に関わりトラブルも多い、奥の深いテーマです。

  • 内部ではUTCを使う
  • 信頼できる基準の時計を決める
  • 受け渡しはRFC 3339形式で行う
  • 期間は半開区間で扱う

といった基本を押さえておくだけで、多くのトラブルを防ぐことができます。

特に日時は「なんとなく動いているうちは気づかない」ことが多く、後になってデータの不整合として現れるケースが少なくありません。 だからこそ、システム全体で共通の前提を持ち、「時間をどう扱うか」を明文化しておくことが重要です。 この記事が少しでも役に立てば幸いです。

参考文献