できない.dev

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 range

fromtimestamp に 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 ビットのままになっていないかは、桁あふれを避けるうえで確認しておく価値がある。

この記事は役立ちましたか?