UNIXタイムスタンプを日付に変換するには
UNIX タイムスタンプ(エポック秒)を日付と時刻に変換する基本形を各言語で示す。
秒とミリ秒の取り違え、UTC と地域の時刻の違い、範囲外の値がどう扱われるかまでを扱う。
公開:
各言語見出しの横のバッジは検証状態を表す。実行確認済みはコードを実際に実行して確認したもの、静的確認は構文と公式 API ドキュメントで確認したものである。
Python 実行確認済み
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
ts = 1700000000 # エポック秒(10 桁)
utc = datetime.fromtimestamp(ts, tz=timezone.utc)
print(utc) # 2023-11-14 22:13:20+00:00
# 表示したいタイムゾーンへは astimezone で変換する
jst = utc.astimezone(ZoneInfo("Asia/Tokyo"))
print(jst) # 2023-11-15 07:13:20+09:00
# tz を省くと、実行環境のローカル時刻(タイムゾーン情報なし)になる
print(datetime.fromtimestamp(ts)) # TZ=Asia/Tokyo の環境では 2023-11-15 07:13:20
# ミリ秒(13 桁)は 1000 で割ってから渡す
ms = 1700000000123
print(datetime.fromtimestamp(ms / 1000, tz=timezone.utc)) # 2023-11-14 22:13:20.123000+00:00
# ミリ秒のまま渡すと範囲外で ValueError になる
try:
datetime.fromtimestamp(ms, tz=timezone.utc)
except ValueError as e:
print(e) # year 55840 is out of rangefromtimestamp に tz=timezone.utc を渡すとタイムゾーン情報を持つ UTC の値が返り、astimezone で表示したい地域へ変換できる。
tz を省くと実行環境のローカル時刻で tzinfo の無い値になり、aware な値と大小を比べたり引き算したりすると TypeError になる(== は例外にならず常に False を返す)。
utcfromtimestamp は Python 3.12 で非推奨になったため、tz を渡す形に統一する。
うまくいかない時: Python で datetime の naive と aware を比較できず TypeError になる
JavaScript 実行確認済み
const sec = 1700000000; // エポック秒(10 桁)
const date = new Date(sec * 1000); // Date が受け取るのはミリ秒
console.log(date.toISOString()); // 2023-11-14T22:13:20.000Z
// 表示したいタイムゾーンを指定して整形する
console.log(date.toLocaleString("ja-JP", { timeZone: "Asia/Tokyo" })); // 2023/11/15 7:13:20
// 秒のまま渡すと 1970 年 1 月の日付になる
console.log(new Date(sec).toISOString()); // 1970-01-20T16:13:20.000Z
// 不正な値は Invalid Date になり、toISOString は RangeError を投げる
const bad = new Date(Number("abc"));
console.log(Number.isNaN(bad.getTime())); // true
try {
bad.toISOString();
} catch (e) {
console.log(e.message); // Invalid time value
}Date コンストラクタが受け取るのはミリ秒なので、エポック秒は 1000 倍して渡す。
toISOString は常に UTC の文字列を返し、地域の時刻で表示したいときは toLocaleString に timeZone を指定する。
TypeScript 実行確認済み
type EpochSeconds = number & { readonly __unit: "seconds" };
type EpochMillis = number & { readonly __unit: "millis" };
const fromSeconds = (s: EpochSeconds): Date => new Date(s * 1000);
const fromMillis = (ms: EpochMillis): Date => new Date(ms);
const sec = 1700000000 as EpochSeconds;
const ms = 1700000000123 as EpochMillis;
console.log(fromSeconds(sec).toISOString()); // 2023-11-14T22:13:20.000Z
console.log(fromMillis(ms).toISOString()); // 2023-11-14T22:13:20.123Z
// 単位を取り違えた呼び出しはコンパイルエラーになる
// fromSeconds(ms); // error TS2345: Argument of type 'EpochMillis' is not assignable to parameter of type 'EpochSeconds'.number 型は秒なのかミリ秒なのかを区別できないため、型に単位の印を付けておくと取り違えを実行前に防げる。
EpochSeconds を受け取る関数にミリ秒の値を渡すと TS2345 になる。
ただし as で型を付ける境界の箇所だけは、人が単位を確かめる必要がある。
うまくいかない時: TypeScript で「Argument of type ... is not assignable」が解消できない
Go 実行確認済み
package main
import (
"fmt"
"time"
)
func main() {
sec := int64(1700000000) // エポック秒(10 桁)
t := time.Unix(sec, 0) // ローカルタイムゾーンの time.Time になる
fmt.Println(t) // TZ=Asia/Tokyo の環境では 2023-11-15 07:13:20 +0900 JST
fmt.Println(t.UTC()) // 2023-11-14 22:13:20 +0000 UTC
fmt.Println(t.UTC().Format(time.RFC3339)) // 2023-11-14T22:13:20Z
jst, err := time.LoadLocation("Asia/Tokyo")
if err != nil {
panic(err)
}
fmt.Println(t.In(jst)) // 2023-11-15 07:13:20 +0900 JST
// ミリ秒は UnixMilli で渡す(Go 1.17 以降)
fmt.Println(time.UnixMilli(1700000000123).UTC()) // 2023-11-14 22:13:20.123 +0000 UTC
// ミリ秒を Unix に渡してもエラーにはならず、遠い未来の日付になる
fmt.Println(time.Unix(1700000000123, 0).UTC().Year()) // 55840
}time.Unix は秒とナノ秒を受け取り、ローカルタイムゾーンの time.Time を返す。
UTC で扱うなら UTC() を付け、地域の時刻にするなら LoadLocation で得た Location を In に渡す。
ミリ秒は UnixMilli で渡す。
Rust 実行確認済み
// Cargo.toml: chrono = "0.4"
use chrono::{DateTime, FixedOffset, Utc};
fn main() {
let sec: i64 = 1_700_000_000; // エポック秒(10 桁)
// 範囲外の値だと None になるので、Option で受け取る
let utc: DateTime<Utc> = DateTime::from_timestamp(sec, 0).unwrap();
println!("{utc}"); // 2023-11-14 22:13:20 UTC
println!("{}", utc.to_rfc3339()); // 2023-11-14T22:13:20+00:00
let jst = FixedOffset::east_opt(9 * 3600).unwrap();
println!("{}", utc.with_timezone(&jst)); // 2023-11-15 07:13:20 +09:00
// ミリ秒は from_timestamp_millis で渡す
let ms = DateTime::from_timestamp_millis(1_700_000_000_123).unwrap();
println!("{ms}"); // 2023-11-14 22:13:20.123 UTC
// ミリ秒を from_timestamp に渡してもエラーにならず、遠い未来の日付になる
let far = DateTime::from_timestamp(1_700_000_000_123, 0).unwrap();
println!("{}", far.to_rfc3339()); // +55840-11-08T22:15:23+00:00
// None になるのは chrono が表せる範囲(約 ±26 万年)を超えたとき
println!("{:?}", DateTime::from_timestamp(i64::MAX, 0)); // None
}標準ライブラリはカレンダー上の日付を扱えないため、chrono クレートを使う。
DateTime::from_timestamp は表せない範囲の値に対して None を返すので Option で受け取り、ミリ秒の値は from_timestamp_millis に渡す。
Java 実行確認済み
import java.time.Instant;
import java.time.ZoneId;
public class Main {
public static void main(String[] args) {
long sec = 1_700_000_000L; // エポック秒(10 桁)
Instant instant = Instant.ofEpochSecond(sec);
System.out.println(instant); // 2023-11-14T22:13:20Z
// 表示したいタイムゾーンは atZone で付ける
System.out.println(instant.atZone(ZoneId.of("Asia/Tokyo"))); // 2023-11-15T07:13:20+09:00[Asia/Tokyo]
// ミリ秒は ofEpochMilli で渡す
System.out.println(Instant.ofEpochMilli(1_700_000_000_123L)); // 2023-11-14T22:13:20.123Z
// ミリ秒を ofEpochSecond に渡してもエラーにならず、遠い未来の日付になる
System.out.println(Instant.ofEpochSecond(1_700_000_000_123L)); // +55840-11-08T22:15:23Z
}
}Instant.ofEpochSecond は秒から UTC 上の瞬間を作り、表示したい地域は atZone で付ける。
ミリ秒は ofEpochMilli で渡す。
秒の引数にミリ秒の値を渡しても例外にはならず、遠い未来の日付になる。
C# 実行確認済み
using System;
class Program {
static void Main() {
long sec = 1700000000; // エポック秒(10 桁)
DateTimeOffset utc = DateTimeOffset.FromUnixTimeSeconds(sec); // オフセットは +00:00
Console.WriteLine(utc.ToString("o")); // 2023-11-14T22:13:20.0000000+00:00
// 固定オフセットなら ToOffset、夏時間のある地域なら TimeZoneInfo で変換する
Console.WriteLine(utc.ToOffset(TimeSpan.FromHours(9)).ToString("o")); // 2023-11-15T07:13:20.0000000+09:00
var tokyo = TimeZoneInfo.FindSystemTimeZoneById("Asia/Tokyo");
Console.WriteLine(TimeZoneInfo.ConvertTime(utc, tokyo).ToString("o")); // 2023-11-15T07:13:20.0000000+09:00
// ミリ秒は FromUnixTimeMilliseconds で渡す
Console.WriteLine(DateTimeOffset.FromUnixTimeMilliseconds(1700000000123).ToString("o")); // 2023-11-14T22:13:20.1230000+00:00
// ミリ秒を秒として渡すと、9999 年を超えるため例外になる
try {
DateTimeOffset.FromUnixTimeSeconds(1700000000123);
} catch (ArgumentOutOfRangeException) {
Console.WriteLine("ArgumentOutOfRangeException");
}
}
}DateTimeOffset.FromUnixTimeSeconds は UTC(オフセット +00:00)の値を返し、ToOffset で固定のオフセットへ、TimeZoneInfo.ConvertTime で夏時間のある地域へ変換できる。
ミリ秒は FromUnixTimeMilliseconds で渡す。
秒の引数にミリ秒の値を渡すと、9999 年を超えるため ArgumentOutOfRangeException になる。
つまずき
エポックからの経過時間は、秒で持つ場合とミリ秒で持つ場合があり、現在時刻付近では秒が 10 桁、ミリ秒が 13 桁になる。
変換関数が受け取る単位は言語ごとに違う。
JavaScript の Date と Java の ofEpochMilli と C# の FromUnixTimeMilliseconds はミリ秒、Python の fromtimestamp と Go の Unix と Rust の from_timestamp は秒である。
取り違えても例外にならない言語が多く、秒を JavaScript の Date に渡すと 1970 年 1 月 20 日、ミリ秒を Go や Java や Rust に渡すと西暦 55840 年付近の日付になる。
Python と C# はミリ秒を秒として渡すと範囲外として例外を投げる。
結果の年が明らかにおかしいときは、まず桁数を疑う。
同じ瞬間でもタイムゾーンで日付が変わる
エポック秒はタイムゾーンを持たない「ある瞬間」を表す数値で、日付に直すときに初めてどの地域の暦で見るかが決まる。
1700000000 は UTC では 2023-11-14 22:13:20 だが、日本時間では 2023-11-15 07:13:20 になり、日付そのものが 1 日ずれる。
Python で tz を省いた場合や Go の time.Unix の戻り値は実行環境のタイムゾーンに従うため、サーバーと手元で結果が変わりうる。
保存や比較には UTC を使い、表示するときだけ地域の時刻へ変換すると、この種のずれを防げる。
32 ビット整数で持つと 2038 年で溢れる
エポック秒を符号付き 32 ビット整数で持つ処理系では、上限が 2147483647 であり、これは 2038-01-19 03:14:07 UTC にあたる。
1 秒進むと負の値に折り返し、日付は 1901 年になる。
Python の整数や、本記事の Go(int64)・Java(long)・C#(long)・Rust(i64)の例のように 64 ビットで持てば問題にならない。
外部のシステムや DB の列が 32 ビットのままになっていないかは、桁あふれを避けるうえで確認しておく価値がある。