はじめに
Unixタイムスタンプは、コンピュータサイエンスにおける最も基本的な概念のひとつです。これは、Unixエポック(1970年1月1日 00:00:00 UTC)から経過した秒数を単純な整数で表したものです。このシンプルで汎用的なフォーマットは、データベースレコード、APIレスポンス、ログファイル、スケジュールタスクなど、あらゆる場面を支えています。
"2024年4月9日"のような人間が読める日付文字列と異なり、Unixタイムスタンプにはタイムゾーンも、ロケールも、あいまいさもありません。数値1712620800は地球上のどこでも、まったく同じ瞬間を指します。この特性こそが、Unixタイムスタンプをソフトウェアエンジニアリングにおける日時処理の共通言語にしています。
Unixエポックの歴史
Unixオペレーティングシステムは、1960年代後半から1970年代初頭にかけて、ベル研究所(Bell Labs)のKen Thompson、Dennis Ritchieらによって開発されました。時間の追跡はカーネルに最初から組み込まれていましたが、エポック日付の正確な選択は初期の開発過程で変遷しました。
最初期のUnixバージョンの一部では、1971年1月1日がエポックとして使用されていました。また1969年1月1日を試みたものもありました。最終的に、1970年1月1日 00:00:00 UTCが標準化されました。これはシステムの誕生に十分近い「きりのよい」日付であり、実際のイベントのタイムスタンプが小さな正の整数になるという利点がありました。
1970-01-01という選択は、本質的には任意のものです。重要なのは日付そのものではなく、慣習の一貫性です。すべてのシステムが同じエポックに合意しているため、タイムスタンプはプログラム、言語、プラットフォームをまたいで変換コストなしにやり取りできます。
この慣習は後にPOSIX(ポータブルオペレーティングシステムインタフェース)で正式化されました。POSIXはUnixタイムスタンプを「1970-01-01T00:00:00Zからの非うるう秒数」と定義しています。
秒とミリ秒
すべての「タイムスタンプ」が同じ単位ではありません。最も重要な違いは単位にあります:
- Unixタイムスタンプ(POSIX): エポックからの秒数 — 例:
1712620800 - JavaScriptの
Date: エポックからのミリ秒数 — 例:1712620800000 - Javaの
System.currentTimeMillis(): エポックからのミリ秒数 - Goの
time.Now().Unix(): 秒;time.Now().UnixNano()でナノ秒精度 - Pythonの
time.time(): 浮動小数点秒数
秒とミリ秒の1000倍の差は、よくあるバグの原因です。ミリ秒のタイムスタンプを秒を期待するコードに渡すと、遥か未来(約西暦56000年)の日付が生成されます。逆の場合は1970年1月の日付が生成されます。
変換は簡単です:
const unix_ms = unix_s * 1000;
const unix_s = Math.floor(unix_ms / 1000);
実用的な判断基準:タイムスタンプが10桁の数値であれば秒、13桁であればミリ秒の可能性が高いです。
ISO 8601フォーマットとその変形
ISO 8601は日付と時刻の表現に関する国際規格です。一連のあいまいさのない文字列フォーマットを定義しています:
2024-04-09 # 日付のみ
2024-04-09T12:00:00 # ローカル日時(タイムゾーン情報なし)
2024-04-09T12:00:00Z # UTC(Z = Zulu時間 = UTC+0)
2024-04-09T20:00:00+08:00 # UTCオフセット付き(例:Asia/Shanghai)
2024-04-09T12:00:00.123Z # ミリ秒付き
2024-04-09T12:00:00.123456789Z # ナノ秒付き
Tは日付部分と時刻部分を区切ります。Zサフィックス(NATOの音声アルファベット「Zulu」、つまりUTC+0)は時刻がUTCであることを示します。+08:00のようなオフセットは、現地時刻がUTCより8時間進んでいることを意味します。
ISO 8601は、REST API、ログファイル、境界をまたいで時間データを交換するあらゆるシステムに推奨されるフォーマットです。人間が読めて、文字列として辞書順に並べ替えられ、あいまいさがありません。
タイムゾーンとUTC
UTC(協定世界時) は世界の主要な時間規格です。タイムゾーンそのものではなく、すべてのタイムゾーンが定義される基準です。UTC+0は冬季のグリニッジ標準時(GMT)と同じです。
タイムゾーンはUTCからのオフセットとして表されます:UTC+5:30(インド)、UTC-8(アメリカ太平洋標準時)、UTC+9(日本)。ただし、生のオフセットだけではタイムゾーンを完全に記述するには不十分です。サマータイム(DST)によってオフセットが変わるためです。
IANAタイムゾーンデータベース(tzデータベースまたはzoneinfoとも呼ばれる)は、世界のすべてのタイムゾーンの権威あるリストです。America/New_York、Europe/Berlin、Asia/Tokyo、Asia/Kolkataなどの識別子を使用します。これらの識別子は、現在のUTCオフセットだけでなく、DSTルールや政治的変更(例:国がタイムゾーンを変更した場合)の完全な歴史的記録も封じ込めています。
主要なプログラミング言語とオペレーティングシステムはすべてIANAデータベースを含んでいます:
- JavaScript(Node.js):IANA識別子を使用する
Intl.DateTimeFormat - Python:
zoneinfoモジュール(Python 3.9以降)またはpytz - Java:
java.time.ZoneId(例:ZoneId.of("Asia/Tokyo")) - Go:
time.LoadLocation("Asia/Tokyo")
アプリケーションロジックでタイムゾーンを表すのに、+09:00のような生のUTCオフセットを使用しないでください。季節によってオフセットが変わるため、IANA識別子を使用してください。
サマータイム(DST)の複雑さ
サマータイムは、夕方の日照時間を延ばすために夏季に時計を1時間進める慣行です。北米・欧州のほとんどの地域と、南米・中東・オセアニアの一部で実施されています。日本、中国、インドなど多くの国はサマータイムを実施していません。
DSTは2つの古典的な異常をもたらします:
春の切り替え(Spring forward): 時計が午前2:00から直接午前3:00に進みます。午前2:00〜3:00の60分間は現地時間として存在しません。午前2:30にタスクをスケジュールすると、午前3:30に実行されるか、スケジューラの実装によっては完全にスキップされます。
秋の切り替え(Fall back): 時計が午前2:00から午前1:00に戻ります。午前1:00〜2:00の60分間が2回発生します。「現地時間の午前1:45」のログエントリを記録すると、それはあいまいです——その時間の最初の発生か2回目の発生かわかりません。
Unixタイムスタンプは常にUTCを基準としているため、DSTの影響をまったく受けません。数値1712620800はどこにいても、どの季節でも、常に同じ瞬間を指します。
黄金律: 常にUTCでタイムスタンプを保存・転送してください。現地時間への変換は、ユーザーに表示する直前の表示層でのみ行ってください。
2038年問題
2038年問題(Y2K38またはUnixミレニアムバグとも呼ばれる)は、2000年のY2K問題と性質が似たソフトウェアの脆弱性です。
根本原因:多くのレガシーシステムがUnixタイムスタンプを32ビット符号付き整数として保存しています。32ビット符号付き整数の最大値は2,147,483,647です。これは以下に対応します:
2038年1月19日 03:14:07 UTC
その瞬間の1秒後、32ビット符号付き整数はオーバーフローして最小の負の値-2,147,483,648に折り返します。これは1901年12月13日 20:45:52 UTCに対応します。オーバーフローするシステムは突然、日付が1901年だと認識します。
影響を受ける可能性があるシステム:
- 32ビットアーキテクチャ向けにコンパイルされたレガシー組み込みシステムやIoTデバイス
- MySQLの
TIMESTAMP型を使用した古いデータベーススキーマ(バージョン8.0以前は32ビットストレージを使用) - 32ビットLinuxカーネル(64ビットプラットフォームではカーネルレベルで解決済み)
- 変更時刻を32ビット整数で記録する一部の古いファイルシステム
解決策は簡単です:すべてのタイムスタンプ保存を64ビット符号付き整数に移行してください。64ビットタイムスタンプは約292,277,026,596年まで表現でき、実用上の懸念はありません。現代の64ビットシステムのほとんどはすでにこれを正しく処理しています。
異なる言語でのタイムスタンプ操作
JavaScript
// 現在時刻
const now = Date.now(); // エポックからのミリ秒
const unix = Math.floor(now / 1000); // 秒に変換
// Unixタイムスタンプの解析
const date = new Date(unix * 1000);
console.log(date.toISOString()); // "2024-04-09T12:00:00.000Z"
console.log(date.toLocaleString("ja-JP", { timeZone: "Asia/Tokyo" }));
Python
import time
import datetime
# 現在のUnixタイムスタンプ(秒)
unix = int(time.time())
# UTC日時に変換
dt = datetime.datetime.fromtimestamp(unix, tz=datetime.timezone.utc)
print(dt.isoformat()) # "2024-04-09T12:00:00+00:00"
# IANAタイムゾーンにzoneinfoを使用
from zoneinfo import ZoneInfo
dt_local = dt.astimezone(ZoneInfo("Asia/Tokyo"))
print(dt_local.isoformat())
Go
package main
import (
"fmt"
"time"
)
func main() {
unix := time.Now().Unix() // int64 秒
t := time.Unix(unix, 0).UTC()
fmt.Println(t.Format(time.RFC3339)) // "2024-04-09T12:00:00Z"
loc, _ := time.LoadLocation("Asia/Tokyo")
fmt.Println(t.In(loc).Format(time.RFC3339)) // "2024-04-09T21:00:00+09:00"
}
Java
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
long unix = Instant.now().getEpochSecond(); // 秒
Instant instant = Instant.ofEpochSecond(unix);
System.out.println(instant.toString()); // "2024-04-09T12:00:00Z"
ZonedDateTime zdt = instant.atZone(ZoneId.of("Asia/Tokyo"));
System.out.println(zdt.toString());
Rust
use std::time::{SystemTime, UNIX_EPOCH};
fn main() {
let unix = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("時間が逆行しました")
.as_secs(); // u64 秒
println!("{}", unix); // 例:1712620800
}
主要なタイムスタンプフォーマット一覧
| フォーマット | 例 | 使用場面 |
|---|---|---|
| Unix(秒) | 1712620800 |
Unix/Linux、POSIX API |
| Unix(ミリ秒) | 1712620800000 |
JavaScript、Java |
| Unix(マイクロ秒) | 1712620800000000 |
PostgreSQL、一部のAPI |
| Unix(ナノ秒) | 1712620800000000000 |
Go、Rust |
| ISO 8601 UTC | 2024-04-09T12:00:00Z |
REST API、データベース |
| ISO 8601(オフセット付き) | 2024-04-09T20:00:00+08:00 |
カレンダーアプリ |
| RFC 2822 | Tue, 09 Apr 2024 12:00:00 +0000 |
メールヘッダー |
| RFC 3339 | 2024-04-09T12:00:00Z |
インターネットプロトコル |
| HTTP日付 | Tue, 09 Apr 2024 12:00:00 GMT |
HTTPヘッダー |
タイムスタンプの算術演算
Unixタイムスタンプはただの数値なので、算術演算は非常に簡単です。
2つのタイムスタンプ間の期間を計算する:
start = 1712620800
end = 1712707200
duration_seconds = end - start # 86400秒 = ちょうど1日
未来または過去の日付を求める:
const now = Math.floor(Date.now() / 1000);
const oneWeekLater = now + 7 * 24 * 60 * 60; // +604800秒
const thirtyDaysAgo = now - 30 * 24 * 60 * 60; // -2592000秒
秒で表した一般的な期間:
| 期間 | 秒数 |
|---|---|
| 1分 | 60 |
| 1時間 | 3,600 |
| 1日 | 86,400 |
| 1週間 | 604,800 |
| 30日 | 2,592,000 |
| 1年(365日) | 31,536,000 |
注意:「1ヶ月追加」のようなカレンダーを意識した算術には、生の秒数ではなく日付ライブラリを使用してください。月の長さは異なり、DSTによって一部の日は23時間や25時間になることがあります。
ユースケース
アプリケーションログ: Unixタイムスタンプ付きのログエントリは、異なるタイムゾーンで動作する分散システム間で、あいまいさなくソート、フィルタリング、比較ができます。
REST API: Unixの整数としてタイムスタンプを返すことで、サーバー側でのタイムゾーン解釈を回避できます。クライアントは整数を読み取り、ユーザーのローカルタイムゾーンでフォーマットします。
データベース: タイムスタンプを整数(またはISO 8601文字列)として保存することは、プラットフォーム固有の日付型より移植性が高くなります。PostgreSQLのTIMESTAMPTZは内部でUTCを使用し、MySQLのDATETIME(TIMESTAMPより推奨)はY2038の制限を回避します。
スケジュールタスクとcronジョブ: UTCで「毎日午前3:00に実行」を計算することで、DSTによる予期せぬ動作を回避できます。多くのスケジューリングフレームワーク(Kubernetes CronJobs、GitHub Actionsスケジュール)は慣習としてUTCを使用します。
キャッシュの有効期限とTTL: HTTPのCache-Control: max-age=3600とExpiresヘッダーは絶対Unixタイムスタンプまたは相対秒数を使用します。CDNとブラウザはキャッシュを無効化するために正確なタイムスタンプの算術に依存しています。
イベントソーシングと監査証跡: 不変のイベントログには、イベントを明確に順序付けるタイムスタンプが必要です。Unixタイムスタンプ、特にナノ秒精度のものは、高スループットシステムでもこの保証を提供します。
ベストプラクティス
常にUTCでタイムスタンプを保存する。 データベースにローカル時刻を保存しないでください。ローカル時刻への変換は表示層でのみ行います。
64ビット整数を使用する。 新しいコードではタイムスタンプに
int32を使用しないでください。システムが近い将来の日付しか扱わない場合でも、64ビットが安全なデフォルトです。人間が読めるシリアル化にはISO 8601を使用する。 ログやAPIで文字列表現が必要な場合、ISO 8601はあいまいさがなく辞書順で並べ替えられます。
オフセットではなくIANAタイムゾーン識別子を使用する。
"Asia/Tokyo"は正しいです。"+09:00"は(DSTを実施する地域では)年2回変わるため脆弱です。タイムスタンプの単位を検証する。 外部タイムスタンプを使用する前に、秒、ミリ秒、その他の単位のいずれかを確認してください。簡単な確認方法:10桁の数字は秒の可能性が高く、13桁はミリ秒の可能性が高いです。
本番コードでは正規表現で日付を解析しない。 標準ライブラリまたは十分にテストされたサードパーティライブラリを使用してください。
「真夜中」に注意する。 DSTを実施するタイムゾーンでは、特定の日付に真夜中が存在しない場合があります(春の切り替え時)。日付のみの計算では、安全な「代表的」時刻として正午(UTC 12:00)を使用してください。
DST切り替え付近でテストする。 アプリケーションにスケジューリングや時間計算が含まれる場合、関連するタイムゾーンの春の切り替えと秋の切り替えの境界を具体的にテストするテストケースを作成してください。
よくある質問(FAQ)
Q:Unixタイムスタンプの0は何を表しますか?
A:1970年1月1日 00:00:00 UTC——Unixエポックです。負のタイムスタンプは1970年以前の日付を表します。
Q:タイムスタンプをソートに使用できますか?
A:はい。Unixタイムスタンプは単調増加する整数なので、タイムスタンプでソートすることは時系列でソートすることと同じです。
Q:Unix時刻はうるう秒の影響を受けますか?
A:POSIXはUnix時刻を「各日がちょうど86,400秒」として定義しており、うるう秒は計算されません。POSIXタイムスタンプは技術的には「Unix時刻」または「POSIX時刻」であり、「真の」国際原子時(TAI)ではありません。実際のところ、アプリケーションコードではこれがほとんど問題になることはありません。
Q:Unixタイムスタンプで表現できる最大の日付は何ですか?
A:64ビット符号付き整数では、最大値は約西暦292,277,026,596年に対応します。32ビット符号付き整数では、2038年1月19日 03:14:07 UTCが最大値です。
Q:ブラウザで現在のUnixタイムスタンプを取得するにはどうすればいいですか?
A:ブラウザのコンソールでMath.floor(Date.now() / 1000)を実行すると、現在の秒単位のUnixタイムスタンプが返されます。
Q:タイムスタンプを変換すると1970-01-01と表示されるのはなぜですか?
A:秒を期待する関数にミリ秒を渡している(またはその逆)可能性がほぼ確実です。遥か未来の日付が表示される場合は1000で割り、1970年1月1日が表示される場合は1000を掛けてください。