CODE Polyglot / よく使う書き方 / 04

日付と時刻を ISO 8601 で読み書きする

API やログでよく使う 2026-10-30T09:00:00+09:00 の形を読み、UTC と日本時間で書き直します。標準のパーサーにそのまま渡すと、2月30日を3月2日として読んだり、タイムゾーンの無い日時を動かす場所の時間帯で読んだりします。

画面にこう出れば正解

どの言語で書いても、同じ入力なら画面に出る文字は1文字も違わない、というのが正解の決め方です(例は Python で動かしたときのコマンドです)。

日本時間で書いた日時

$ python3 iso8601.py 2026-10-30T09:00:00+09:00
UTC 2026-10-30T00:00:00Z
日本時間 2026年10月30日 09:00:00

UTC(Z)

$ python3 iso8601.py 2026-10-30T00:00:00Z
UTC 2026-10-30T00:00:00Z
日本時間 2026年10月30日 09:00:00

日付をまたぐ(ニューヨーク)

$ python3 iso8601.py 2026-10-30T20:30:00-05:00
UTC 2026-10-31T01:30:00Z
日本時間 2026年10月31日 10:30:00

年をまたぐ

$ python3 iso8601.py 2026-12-31T23:30:00Z
UTC 2026-12-31T23:30:00Z
日本時間 2027年01月01日 08:30:00

秒の小数(切り捨て)

$ python3 iso8601.py 2026-10-30T09:00:00.999+09:00
UTC 2026-10-30T00:00:00Z
日本時間 2026年10月30日 09:00:00

秒の小数が9桁

$ python3 iso8601.py 2026-10-30T09:00:00.123456789Z
UTC 2026-10-30T09:00:00Z
日本時間 2026年10月30日 18:00:00

インド(+05:30)

$ python3 iso8601.py 2026-10-30T12:00:00+05:30
UTC 2026-10-30T06:30:00Z
日本時間 2026年10月30日 15:30:00

うるう年の2月29日

$ python3 iso8601.py 2028-02-29T12:00:00Z
UTC 2028-02-29T12:00:00Z
日本時間 2028年02月29日 21:00:00

タイムゾーンが無い

$ python3 iso8601.py 2026-10-30T09:00:00
NG: タイムゾーン(Z か +09:00 など)を付けてください

ありえない日付(2月30日)

$ python3 iso8601.py 2026-02-30T09:00:00Z
NG: 日時の形ではありません

ありえない時刻(25時)

$ python3 iso8601.py 2026-10-30T25:00:00Z
NG: 日時の形ではありません

うるう年でない2月29日

$ python3 iso8601.py 2026-02-29T12:00:00Z
NG: 日時の形ではありません

日付だけ

$ python3 iso8601.py 2026-10-30
NG: 日時の形ではありません

コロンの無いタイムゾーン

$ python3 iso8601.py 2026-10-30T09:00:00+0900
NG: 日時の形ではありません

T の代わりに空白

$ python3 iso8601.py "2026-10-30 09:00:00Z"
NG: 日時の形ではありません

10言語のプログラム

どれもそのまま保存して動かせます。毎朝、上の正解の例をすべて実際に動かして確かめています。

iso8601.php8.4.26 で 15 / 15 件の出力が正解と一致

<?php
// ISO 8601 の日時を受け取って、UTC と日本時間で書き出す

// new DateTime() は「2026-02-30」を3月2日に、「+0900」も「t」も受け付けるので、形は先に正規表現で決める。
// $ は末尾の改行も通すので、文字列の終わりは \z で書く
$re = '/\A([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})(\.[0-9]{1,9})?(Z|[+-][0-9]{2}:[0-9]{2})?\z/';
if (!preg_match($re, $argv[1], $m)) {
    echo "NG: 日時の形ではありません\n";
    exit;
}
// 2月30日・25時は DateTime が黙って繰り上げるので、checkdate と範囲で確かめる
if (!checkdate((int)$m[2], (int)$m[3], (int)$m[1]) || $m[4] > 23 || $m[5] > 59 || $m[6] > 59) {
    echo "NG: 日時の形ではありません\n";
    exit;
}
// タイムゾーンが無いと、php.ini の date.timezone(サーバーの設定)で読まれてしまう
if (($m[8] ?? "") === "") {
    echo "NG: タイムゾーン(Z か +09:00 など)を付けてください\n";
    exit;
}
if ($m[8] !== "Z" && (substr($m[8], 1, 2) > 23 || substr($m[8], 4, 2) > 59)) {
    echo "NG: 日時の形ではありません\n";
    exit;
}

// "+09:00" のような時差だけのタイムゾーンは tzdata を見ないので、どこで動かしても同じになる。
// 秒の小数は切り捨てるので、読むときから渡さない
$tz = new DateTimeZone($m[8] === "Z" ? "+00:00" : $m[8]);
$t = new DateTimeImmutable("$m[1]-$m[2]-$m[3] $m[4]:$m[5]:$m[6]", $tz);
$utc = $t->setTimezone(new DateTimeZone("+00:00"));
$jst = $t->setTimezone(new DateTimeZone("+09:00"));

echo "UTC " . $utc->format("Y-m-d\TH:i:s\Z") . "\n";
echo "日本時間 " . $jst->format("Y年m月d日 H:i:s") . "\n";

動かし方(ファイルを保存したフォルダで)

$ php iso8601.php 2026-10-30T09:00:00+09:00

よく見かける書き方で試すと

仕事でよく見かける書き方(標準の関数をそのまま使う形や、よく紹介される正規表現など)を、同じ正解表で動かした結果です。言語を出した答えごとにまとめ、赤い字はその言語が実際に出した間違った行です。

入力よく見かける書き方の答え(9言語)
日本時間で書いた日時2026-10-30T09:00:00+09:00

✓9言語すべて正解

UTC(Z)2026-10-30T00:00:00Z

✓9言語すべて正解

日付をまたぐ(ニューヨーク)2026-10-30T20:30:00-05:00

✓9言語すべて正解

年をまたぐ2026-12-31T23:30:00Z

✓9言語すべて正解

秒の小数(切り捨て)2026-10-30T09:00:00.999+09:00
  • SwiftNG: 日時の形ではありません
  • PHP・Python・Go・JavaScript・Java・C#・Ruby・Kotlin✓ 正解
秒の小数が9桁2026-10-30T09:00:00.123456789Z
  • SwiftNG: 日時の形ではありません
  • PHP・Python・Go・JavaScript・Java・C#・Ruby・Kotlin✓ 正解
インド(+05:30)2026-10-30T12:00:00+05:30

✓9言語すべて正解

うるう年の2月29日2028-02-29T12:00:00Z

✓9言語すべて正解

タイムゾーンが無い2026-10-30T09:00:00
  • PHPUTC 2026-10-30T09:00:00Z
    日本時間 2026年10月30日 18:00:00
  • Python・JavaScript・C#・RubyUTC 2026-10-30T00:00:00Z
    日本時間 2026年10月30日 09:00:00
  • Go・Java・Kotlin・SwiftNG: 日時の形ではありません
ありえない日付(2月30日)2026-02-30T09:00:00Z
  • PHP・JavaScript・Ruby・SwiftUTC 2026-03-02T09:00:00Z
    日本時間 2026年03月02日 18:00:00
  • Python・Go・Java・C#・Kotlin✓ 正解
ありえない時刻(25時)2026-10-30T25:00:00Z

✓9言語すべて正解

うるう年でない2月29日2026-02-29T12:00:00Z
  • PHP・JavaScript・Ruby・SwiftUTC 2026-03-01T12:00:00Z
    日本時間 2026年03月01日 21:00:00
  • Python・Go・Java・C#・Kotlin✓ 正解
日付だけ2026-10-30
  • PHP・JavaScriptUTC 2026-10-30T00:00:00Z
    日本時間 2026年10月30日 09:00:00
  • Python・C#・RubyUTC 2026-10-29T15:00:00Z
    日本時間 2026年10月30日 00:00:00
  • Go・Java・Kotlin・Swift✓ 正解
コロンの無いタイムゾーン2026-10-30T09:00:00+0900
  • PHP・Python・JavaScript・C#・Ruby・SwiftUTC 2026-10-30T00:00:00Z
    日本時間 2026年10月30日 09:00:00
  • Go・Java・Kotlin✓ 正解
T の代わりに空白"2026-10-30 09:00:00Z"
  • PHP・Python・JavaScript・C#・RubyUTC 2026-10-30T09:00:00Z
    日本時間 2026年10月30日 18:00:00
  • Go・Java・Kotlin・Swift✓ 正解

この書き方の決まり

  • 受け付ける形は YYYY-MM-DDTHH:MM:SS に、秒の小数(任意。1〜9桁)と、タイムゾーン(Z か ±HH:MM)を付けたもの
  • 出力は「UTC」と「日本時間」の2行。秒の小数は切り捨てる
  • タイムゾーンが無ければ NG(動かす場所の時間帯で読まない)
  • ありえない日付(2月30日)・時刻(25時)、T の代わりの空白、コロンの無い +0900 は NG
  • 日本時間は、UTC に9時間を足して作る(動かす場所のタイムゾーンの設定を使わない)

解説

標準のパーサーは、思ったより何でも読む

各言語の標準のパーサーに、そのまま渡した結果です。

よく見かける書き方2月30日タイムゾーンなし日付だけ+0900・空白区切り
PHP new DateTimeImmutable3月2日にするphp.ini の時間帯(既定は UTC)で読む読む読む
JavaScript new Date3月2日にする動かす場所の時間帯で読むUTC として読む読む
Ruby Time.parse3月2日にする動かす場所の時間帯で読む読む読む
Swift ISO8601DateFormatter3月2日にするNGNG+0900 を読む
Python datetime.fromisoformatNG動かす場所の時間帯の日時として扱われる読む読む
C# DateTimeOffset.TryParseNG動かす場所の時間帯で読む読む読む
Go time.Parse(RFC3339)・Java ZonedDateTime.parse・Kotlin Instant.parseNGNGNGNG

Go・Java・Kotlin のパーサーは厳しく、落ちたのは「タイムゾーンが無いときに、それを理由として言えない」1件だけでした。逆に、ゆるいパーサーは入力の間違いを黙って直してしまいます。受け付ける形が決まっているなら、先に正規表現で形を確かめてから読みます。

動かす場所の時間帯に、答えがよらないようにする

JavaScript の getHours() などは、動かす場所の時間帯の値を返します。日本時間の Mac では正しく見えても、UTC で動くサーバーでは、日本時間の行がすべて UTC のまま出ました(15件中1件しか合わない)。毎朝の検証のコンテナも、TZ を UTC に変えて同じことを確かめています。

この書き方では、どの言語も「時差だけを持つタイムゾーン」(Python timezone(timedelta(hours=9))、Java ZoneOffset.ofHours(9)、Go time.FixedZone など)を使います。サーバーに tzdata(世界のタイムゾーンの表)が入っているかにも左右されません。

言語ごとの細かい違い

  • C# の DateTimeOffset.Parse は、秒の小数を7桁に四捨五入します。09:00:59.999999999Z が 09:01:00 になりました。この書き方では、小数を外してから読んでいます
  • Go の time.Parse は、+24:00 や +09:60 のようなありえない時差も通します
  • Java・Kotlin・C# は、時差の上限があります(Java は ±18時間、C# は ±14時間)
  • JavaScript の Date.UTC は、0〜99年を1900年代として扱います
  • Rust には標準に日時の型がありません。この書き方では、うるう年と月の日数の計算を自分で書いています(よく使われるのは外部の chrono)