画面にこう出れば正解
どの言語で書いても、同じ入力なら画面に出る文字は1文字も違わない、というのが正解の決め方です(例は Python で動かしたときのコマンドです)。
日本時間で書いた日時
$ python3 iso8601.py 2026-10-30T09:00:00+09:00
UTC 2026-10-30T00:00:00Z
日本時間 2026年10月30日 09:00:00UTC(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
iso8601.py3.14.7 で 15 / 15 件の出力が正解と一致
# ISO 8601 の日時を受け取って、UTC と日本時間で書き出す
import re
import sys
from datetime import datetime, timedelta, timezone
# datetime.fromisoformat は 3.11 から「20261030T090000」や「+0900」も受け付けるので、形は先に正規表現で決める。
# \d は全角の数字にも当たるので [0-9] と書く
ISO = re.compile(r"([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})?")
m = ISO.fullmatch(sys.argv[1])
if not m:
print("NG: 日時の形ではありません")
sys.exit()
y, mo, d, h, mi, se, _frac, tz = m.groups()
# datetime は 2月30日や25時を ValueError にする
try:
local = datetime(int(y), int(mo), int(d), int(h), int(mi), int(se))
except ValueError:
print("NG: 日時の形ではありません")
sys.exit()
# タイムゾーンの無い datetime を astimezone() に渡すと、動かす場所の時間帯で読まれてしまう
if tz is None:
print("NG: タイムゾーン(Z か +09:00 など)を付けてください")
sys.exit()
if tz == "Z":
offset = timedelta(0)
else:
oh, om = int(tz[1:3]), int(tz[4:6])
if oh > 23 or om > 59:
print("NG: 日時の形ではありません")
sys.exit()
offset = (1 if tz[0] == "+" else -1) * timedelta(hours=oh, minutes=om)
# timezone(timedelta) は時差だけのタイムゾーン。tzdata を見ないので、どこで動かしても同じになる
t = local.replace(tzinfo=timezone(offset))
utc = t.astimezone(timezone.utc)
jst = t.astimezone(timezone(timedelta(hours=9)))
print(f"UTC {utc:%Y-%m-%dT%H:%M:%S}Z")
print(f"日本時間 {jst:%Y年%m月%d日 %H:%M:%S}")
動かし方(ファイルを保存したフォルダで)
$ python3 iso8601.py 2026-10-30T09:00:00+09:00
iso8601.go1.27.2 で 15 / 15 件の出力が正解と一致
// ISO 8601 の日時を受け取って、UTC と日本時間で書き出す
package main
import (
"fmt"
"os"
"regexp"
"time"
)
// time.Parse は秒の小数が書式に無くても読み、タイムゾーンが無いのと形の違いを同じエラーにする。
// 返す言葉を分けるため、形は先に正規表現で決める
var iso = regexp.MustCompile(`^[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})?$`)
func main() {
s := os.Args[1]
m := iso.FindStringSubmatch(s)
if m == nil {
fmt.Println("NG: 日時の形ではありません")
return
}
if m[2] == "" {
// タイムゾーンが無くても、日時として正しいかは先に確かめる(2月30日は「形が違う」を返す)
if _, err := time.Parse("2006-01-02T15:04:05", s); err != nil {
fmt.Println("NG: 日時の形ではありません")
return
}
fmt.Println("NG: タイムゾーン(Z か +09:00 など)を付けてください")
return
}
// RFC3339 は Z と ±HH:MM を読み、2月30日・25時はエラーにする。ただし時差の範囲は見ず「+09:60」も通すので、自分で確かめる
t, err := time.Parse(time.RFC3339, s)
if tz := m[2]; err != nil || tz != "Z" && (tz[1:3] > "23" || tz[4:6] > "59") {
fmt.Println("NG: 日時の形ではありません")
return
}
// time.Local を使わず、UTC と「+9時間だけのタイムゾーン」を作る(tzdata が無くても動く)
jst := time.FixedZone("JST", 9*60*60)
// Format は秒の小数を書式に入れなければ切り捨てて書く
fmt.Println("UTC " + t.UTC().Format("2006-01-02T15:04:05Z"))
fmt.Println("日本時間 " + t.In(jst).Format("2006年01月02日 15:04:05"))
}
動かし方(ファイルを保存したフォルダで)
$ go run iso8601.go 2026-10-30T09:00:00+09:00
iso8601.js24.15.0 で 15 / 15 件の出力が正解と一致
// ISO 8601 の日時を受け取って、UTC と日本時間で書き出す
// new Date(s) は 2月30日を3月2日に、タイムゾーンの無い日時を動かす場所の時間帯で読む。
// 形は正規表現で決めて、数字を取り出して自分で組み立てる
const ISO = /^([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})?$/;
function convert(s) {
const m = ISO.exec(s);
if (!m) return "NG: 日時の形ではありません";
const [y, mo, d, h, mi, se] = m.slice(1, 7).map(Number);
// Date.UTC は 0〜99年を1900年代にするので、setUTCFullYear で年をそのまま入れる。
// どちらも 2月30日を3月2日に、25時を翌日1時に繰り上げるので、作った値が入力どおりかを確かめる
const t = new Date(0);
t.setUTCFullYear(y, mo - 1, d);
t.setUTCHours(h, mi, se);
const local = t.getTime();
if (t.getUTCFullYear() !== y || t.getUTCMonth() !== mo - 1 || t.getUTCDate() !== d || t.getUTCHours() !== h || t.getUTCMinutes() !== mi || t.getUTCSeconds() !== se) {
return "NG: 日時の形ではありません";
}
const tz = m[8];
if (tz === undefined) return "NG: タイムゾーン(Z か +09:00 など)を付けてください";
let offsetMin = 0;
if (tz !== "Z") {
const oh = Number(tz.slice(1, 3)), om = Number(tz.slice(4, 6));
if (oh > 23 || om > 59) return "NG: 日時の形ではありません";
offsetMin = (tz[0] === "+" ? 1 : -1) * (oh * 60 + om);
}
// 書いてある時刻から時差を引くと UTC。日本時間は UTC に9時間足して、getUTC* で読む(getHours などは動かす場所の時間帯)
const utc = new Date(local - offsetMin * 60000);
const jst = new Date(utc.getTime() + 9 * 3600000);
const p = (n, w = 2) => String(n).padStart(w, "0");
return `UTC ${utc.toISOString().slice(0, 19)}Z\n` +
`日本時間 ${p(jst.getUTCFullYear(), 4)}年${p(jst.getUTCMonth() + 1)}月${p(jst.getUTCDate())}日 ${p(jst.getUTCHours())}:${p(jst.getUTCMinutes())}:${p(jst.getUTCSeconds())}`;
}
console.log(convert(process.argv[2]));
動かし方(ファイルを保存したフォルダで)
$ node iso8601.js 2026-10-30T09:00:00+09:00
Iso8601.java21.0.12 で 15 / 15 件の出力が正解と一致
// ISO 8601 の日時を受け取って、UTC と日本時間で書き出す
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.regex.Pattern;
public class Iso8601 {
// 標準の ISO の書式は大文字小文字を区別せず(「t」「z」も通る)、「+09:00:00」も読む。形は先に正規表現で決める
static final Pattern ISO = Pattern.compile("[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})?");
public static void main(String[] args) {
var m = ISO.matcher(args[0]);
if (!m.matches()) {
System.out.println("NG: 日時の形ではありません");
return;
}
try {
if (m.group(2) == null) {
// タイムゾーンが無くても、2月30日なら「形が違う」を返したいので、日時としては読んでみる
LocalDateTime.parse(args[0]);
System.out.println("NG: タイムゾーン(Z か +09:00 など)を付けてください");
return;
}
// ISO の書式は STRICT で読むので、2月30日・25時はエラーになる(自分で作る ofPattern は既定が SMART で、2月30日を2月28日にする)
OffsetDateTime t = OffsetDateTime.parse(args[0]);
// ZonedDateTime や ZoneId.systemDefault() を使わず、時差だけ(ZoneOffset)で変える
var utc = t.withOffsetSameInstant(ZoneOffset.UTC);
var jst = t.withOffsetSameInstant(ZoneOffset.ofHours(9));
// 年は yyyy(紀元の年)ではなく uuuu で書く。秒の小数は書式に入れないので切り捨てになる
System.out.println("UTC " + utc.format(DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mm:ss'Z'")));
System.out.println("日本時間 " + jst.format(DateTimeFormatter.ofPattern("uuuu年MM月dd日 HH:mm:ss")));
} catch (DateTimeParseException e) {
System.out.println("NG: 日時の形ではありません");
}
}
}
動かし方(ファイルを保存したフォルダで)
$ java Iso8601.java 2026-10-30T09:00:00+09:00
iso8601.cs10.0.402 で 15 / 15 件の出力が正解と一致
// ISO 8601 の日時を受け取って、UTC と日本時間で書き出す
using System.Globalization;
using System.Text.RegularExpressions;
// DateTimeOffset.Parse は「2026/10/30」や空白区切りも読み、秒の小数は7桁(100ナノ秒)に四捨五入する(.999999999 は次の秒になる)。
// 形は正規表現で決めて、小数は切り捨てるので外してから読む。\d は全角やアラビア数字にも当たるので [0-9] と書く
var m = Regex.Match(args[0], @"\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");
Console.WriteLine(Convert(m));
static string Convert(Match m)
{
if (!m.Success) return "NG: 日時の形ではありません";
var inv = CultureInfo.InvariantCulture;
// ParseExact は 2月30日・25時を FormatException にする。カルチャを固定しないと、和暦などの設定で読み違える
if (!DateTime.TryParseExact(m.Groups[1].Value, "yyyy-MM-dd'T'HH:mm:ss", inv, DateTimeStyles.None, out _))
return "NG: 日時の形ではありません";
var tz = m.Groups[3].Value;
if (tz == "") return "NG: タイムゾーン(Z か +09:00 など)を付けてください";
// zzz は「+09:00」を読む。Z は zzz では読めないので +00:00 に置き換える
if (!DateTimeOffset.TryParseExact(m.Groups[1].Value + (tz == "Z" ? "+00:00" : tz), "yyyy-MM-dd'T'HH:mm:sszzz", inv, DateTimeStyles.None, out var t))
return "NG: 日時の形ではありません";
// ToLocalTime() や TimeZoneInfo.Local は動かす場所の時間帯。時差(TimeSpan)を直接渡して変える
var utc = t.ToOffset(TimeSpan.Zero);
var jst = t.ToOffset(TimeSpan.FromHours(9));
return "UTC " + utc.ToString("yyyy-MM-dd'T'HH:mm:ss'Z'", inv) + "\n" +
"日本時間 " + jst.ToString("yyyy年MM月dd日 HH:mm:ss", inv);
}
動かし方(ファイルを保存したフォルダで)
$ dotnet run iso8601.cs -- 2026-10-30T09:00:00+09:00
iso8601.rs1.99.0 で 15 / 15 件の出力が正解と一致
// ISO 8601 の日時を受け取って、UTC と日本時間で書き出す
// Rust の標準ライブラリには日時の型も正規表現も無い。形は1文字ずつ確かめ、日時は「1970-01-01 からの秒数」にして計算する
// (SystemTime は「いま」を測るための型で、年月日との行き来はできない)
// 4で割り切れる年はうるう年。ただし100で割り切れる年は違い、400で割り切れる年はうるう年
fn is_leap(y: i64) -> bool {
(y % 4 == 0 && y % 100 != 0) || y % 400 == 0
}
fn days_in_month(y: i64, m: i64) -> i64 {
match m {
2 => if is_leap(y) { 29 } else { 28 },
4 | 6 | 9 | 11 => 30,
_ => 31,
}
}
// 1970-01-01 から数えて何日目か。年ごと・月ごとの日数を足していく(1970年より前は引く)
fn days_from_epoch(y: i64, m: i64, d: i64) -> i64 {
let mut days = 0;
if y >= 1970 {
for yy in 1970..y { days += if is_leap(yy) { 366 } else { 365 }; }
} else {
for yy in y..1970 { days -= if is_leap(yy) { 366 } else { 365 }; }
}
for mm in 1..m { days += days_in_month(y, mm); }
days + d - 1
}
// 秒数から年月日と時分秒に戻す。日数を年・月の長さで順に引いて、月末・年末の繰り上がりを作る
fn from_epoch(secs: i64) -> (i64, i64, i64, i64, i64, i64) {
// 負の数の割り算は 0 に向かって丸めるので、日と秒への分け方は div_euclid / rem_euclid で書く
let (mut days, rest) = (secs.div_euclid(86400), secs.rem_euclid(86400));
let mut y = 1970;
while days < 0 { y -= 1; days += if is_leap(y) { 366 } else { 365 }; }
while days >= if is_leap(y) { 366 } else { 365 } { days -= if is_leap(y) { 366 } else { 365 }; y += 1; }
let mut m = 1;
while days >= days_in_month(y, m) { days -= days_in_month(y, m); m += 1; }
(y, m, days + 1, rest / 3600, rest / 60 % 60, rest % 60)
}
// 数字だけの並びを整数にする。&str の parse は「+9」のような符号も通すので、先に1文字ずつ数字か確かめる
fn num(b: &[u8]) -> Option<i64> {
if b.is_empty() || !b.iter().all(|c| c.is_ascii_digit()) { return None; }
Some(b.iter().fold(0, |n, c| n * 10 + (c - b'0') as i64))
}
fn convert(s: &str) -> Result<String, &'static str> {
const FORM: &str = "NG: 日時の形ではありません";
// 位置で切り出すので、バイト列として見る(日本語が混ざっても添字の位置で落ちない)
let b = s.as_bytes();
if b.len() < 19 || b[4] != b'-' || b[7] != b'-' || b[10] != b'T' || b[13] != b':' || b[16] != b':' { return Err(FORM); }
let f = |i: usize, j: usize| num(&b[i..j]).ok_or(FORM);
let (y, mo, d, h, mi, se) = (f(0, 4)?, f(5, 7)?, f(8, 10)?, f(11, 13)?, f(14, 16)?, f(17, 19)?);
let mut rest = &b[19..];
// 秒の小数は1〜9桁。切り捨てるので、数字か確かめて読み飛ばす
if rest.first() == Some(&b'.') {
let n = rest[1..].iter().take_while(|c| c.is_ascii_digit()).count();
if n < 1 || n > 9 { return Err(FORM); }
rest = &rest[1 + n..];
}
// タイムゾーンは無し・Z・±HH:MM のどれか。ほかの形はここで弾く
let offset = match rest {
[] => None,
[b'Z'] => Some(0),
[sign @ (b'+' | b'-'), _, _, b':', _, _] => {
let (oh, om) = (num(&rest[1..3]).ok_or(FORM)?, num(&rest[4..6]).ok_or(FORM)?);
if oh > 23 || om > 59 { return Err(FORM); }
Some(if *sign == b'+' { 1 } else { -1 } * (oh * 3600 + om * 60))
}
_ => return Err(FORM),
};
// 2月30日・25時は、月の日数と時刻の範囲と比べて見分ける
if mo < 1 || mo > 12 || d < 1 || d > days_in_month(y, mo) || h > 23 || mi > 59 || se > 59 { return Err(FORM); }
let offset = offset.ok_or("NG: タイムゾーン(Z か +09:00 など)を付けてください")?;
// 書いてある時刻から時差を引くと UTC。日本時間は UTC に9時間足す
let utc = days_from_epoch(y, mo, d) * 86400 + h * 3600 + mi * 60 + se - offset;
let (uy, um, ud, uh, umi, us) = from_epoch(utc);
let (jy, jm, jd, jh, jmi, js) = from_epoch(utc + 9 * 3600);
Ok(format!("UTC {:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z\n日本時間 {:04}年{:02}月{:02}日 {:02}:{:02}:{:02}",
uy, um, ud, uh, umi, us, jy, jm, jd, jh, jmi, js))
}
fn main() {
let s = std::env::args().nth(1).unwrap_or_default();
match convert(&s) {
Ok(out) => println!("{}", out),
Err(msg) => println!("{}", msg),
}
}
動かし方(ファイルを保存したフォルダで)
$ rustc iso8601.rs && ./iso8601 2026-10-30T09:00:00+09:00
iso8601.rb4.0.7 で 15 / 15 件の出力が正解と一致
# ISO 8601 の日時を受け取って、UTC と日本時間で書き出す
require "date"
# Time.iso8601 は小文字の t・z、「+0900」を受け付け、タイムゾーンが無ければ動かす場所の時間帯で読み、
# 2月30日を3月2日にする。形は正規表現で決める。^ $ は行の頭と終わりなので、文字列の頭と終わりは \A \z
ISO = /\A(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(\.\d{1,9})?(Z|[+-]\d{2}:\d{2})?\z/
def convert(s)
m = ISO.match(s)
return "NG: 日時の形ではありません" unless m
y, mo, d, h, mi, se = m.captures[0, 6].map(&:to_i)
# Time.utc(2026, 2, 30) は黙って3月2日になる。日付は Date.valid_date? で、時刻は範囲で確かめる
return "NG: 日時の形ではありません" unless Date.valid_date?(y, mo, d) && h <= 23 && mi <= 59 && se <= 59
tz = m[8]
return "NG: タイムゾーン(Z か +09:00 など)を付けてください" if tz.nil?
tz = "+00:00" if tz == "Z"
return "NG: 日時の形ではありません" if tz[1, 2].to_i > 23 || tz[4, 2].to_i > 59
# 時差を "+05:30" の形で渡すと、TZ や tzdata を見ずにその時差で作る。getlocal も時差を渡せば同じ
t = Time.new(y, mo, d, h, mi, se, tz)
"UTC #{t.getutc.strftime('%Y-%m-%dT%H:%M:%SZ')}\n日本時間 #{t.getlocal('+09:00').strftime('%Y年%m月%d日 %H:%M:%S')}"
end
puts convert(ARGV[0])
動かし方(ファイルを保存したフォルダで)
$ ruby iso8601.rb 2026-10-30T09:00:00+09:00
iso8601.kt2.4.21 で 15 / 15 件の出力が正解と一致
// ISO 8601 の日時を受け取って、UTC と日本時間で書き出す
import java.time.LocalDateTime
import java.time.OffsetDateTime
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
import java.time.format.DateTimeParseException
// 標準の ISO の書式は大文字小文字を区別せず(「t」「z」も通る)、「+09:00:00」も読む。形は先に正規表現で決める
val ISO = Regex("""[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})?""")
fun convert(s: String): String {
// matchEntire は文字列全体が合うときだけ当たる(find は途中に合えば当たる)
val m = ISO.matchEntire(s) ?: return "NG: 日時の形ではありません"
return try {
if (m.groupValues[2].isEmpty()) {
// タイムゾーンが無くても、2月30日なら「形が違う」を返したいので、日時としては読んでみる
LocalDateTime.parse(s)
return "NG: タイムゾーン(Z か +09:00 など)を付けてください"
}
// ISO の書式は STRICT で読むので、2月30日・25時はエラーになる
val t = OffsetDateTime.parse(s)
// ZoneId.systemDefault() を使わず、時差だけ(ZoneOffset)で変える
val utc = t.withOffsetSameInstant(ZoneOffset.UTC)
val jst = t.withOffsetSameInstant(ZoneOffset.ofHours(9))
// 年は yyyy(紀元の年)ではなく uuuu。秒の小数は書式に入れないので切り捨てになる
"UTC " + utc.format(DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mm:ss'Z'")) + "\n" +
"日本時間 " + jst.format(DateTimeFormatter.ofPattern("uuuu年MM月dd日 HH:mm:ss"))
} catch (e: DateTimeParseException) {
"NG: 日時の形ではありません"
}
}
fun main(args: Array<String>) {
println(convert(args[0]))
}
動かし方(ファイルを保存したフォルダで)
$ kotlinc iso8601.kt -include-runtime -d iso8601.jar && java -jar iso8601.jar 2026-10-30T09:00:00+09:00
iso8601.swift6.3.3 で 15 / 15 件の出力が正解と一致
// ISO 8601 の日時を受け取って、UTC と日本時間で書き出す
import Foundation
// ISO8601DateFormatter は秒の小数を「有り」か「無し」のどちらかに決めないと読めず、桁数の扱いも Mac と Linux で揃わない。
// 形は正規表現で決めて、数字を取り出して Calendar で組み立てる。
// /…/ と書く正規表現はコンパイラの設定(Swift 5 モード)で使えないことがあるので、#/…/# で書く
let iso = #/([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})?/#
func convert(_ s: String) -> String {
// wholeMatch は文字列全体が合うときだけ当たる
guard let m = s.wholeMatch(of: iso) else { return "NG: 日時の形ではありません" }
let (y, mo, d, h, mi, se) = (Int(m.1)!, Int(m.2)!, Int(m.3)!, Int(m.4)!, Int(m.5)!, Int(m.6)!)
// Calendar は動かす場所の設定(Calendar.current)を使わず、西暦の暦と UTC に固定する。
// TimeZone(secondsFromGMT:) は時差だけのタイムゾーンで、tzdata を見ない
var cal = Calendar(identifier: .gregorian)
cal.timeZone = TimeZone(secondsFromGMT: 0)!
// date(from:) は 2月30日を3月2日に繰り上げるので、作った日時を年月日に戻して入力どおりかを確かめる
let local = DateComponents(year: y, month: mo, day: d, hour: h, minute: mi, second: se)
guard let lt = cal.date(from: local),
cal.dateComponents([.year, .month, .day, .hour, .minute, .second], from: lt) == local
else { return "NG: 日時の形ではありません" }
guard let tz = m.8 else { return "NG: タイムゾーン(Z か +09:00 など)を付けてください" }
var offset = 0
if tz != "Z" {
let oh = Int(tz.dropFirst().prefix(2))!, om = Int(tz.suffix(2))!
if oh > 23 || om > 59 { return "NG: 日時の形ではありません" }
offset = (tz.first == "+" ? 1 : -1) * (oh * 3600 + om * 60)
}
// 書いてある時刻を UTC として作ったので、時差を引くと本当の UTC になる
let t = lt.addingTimeInterval(TimeInterval(-offset))
func show(_ format: String, _ secondsFromGMT: Int) -> String {
let f = DateFormatter()
f.calendar = cal
f.locale = Locale(identifier: "en_US_POSIX")
f.timeZone = TimeZone(secondsFromGMT: secondsFromGMT)!
f.dateFormat = format
return f.string(from: t)
}
return "UTC " + show("yyyy-MM-dd'T'HH:mm:ss'Z'", 0) + "\n" + "日本時間 " + show("yyyy年MM月dd日 HH:mm:ss", 9 * 3600)
}
print(convert(CommandLine.arguments[1]))
動かし方(ファイルを保存したフォルダで)
$ swift iso8601.swift 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 |
|
秒の小数が9桁2026-10-30T09:00:00.123456789Z |
|
インド(+05:30)2026-10-30T12:00:00+05:30 | ✓9言語すべて正解 |
うるう年の2月29日2028-02-29T12:00:00Z | ✓9言語すべて正解 |
タイムゾーンが無い2026-10-30T09:00:00 |
|
ありえない日付(2月30日)2026-02-30T09:00:00Z |
|
ありえない時刻(25時)2026-10-30T25:00:00Z | ✓9言語すべて正解 |
うるう年でない2月29日2026-02-29T12:00:00Z |
|
日付だけ2026-10-30 |
|
コロンの無いタイムゾーン2026-10-30T09:00:00+0900 |
|
T の代わりに空白"2026-10-30 09:00:00Z" |
|
この書き方の決まり
- 受け付ける形は
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 DateTimeImmutable | 3月2日にする | php.ini の時間帯(既定は UTC)で読む | 読む | 読む |
JavaScript new Date | 3月2日にする | 動かす場所の時間帯で読む | UTC として読む | 読む |
Ruby Time.parse | 3月2日にする | 動かす場所の時間帯で読む | 読む | 読む |
Swift ISO8601DateFormatter | 3月2日にする | NG | NG | +0900 を読む |
Python datetime.fromisoformat | NG | 動かす場所の時間帯の日時として扱われる | 読む | 読む |
C# DateTimeOffset.TryParse | NG | 動かす場所の時間帯で読む | 読む | 読む |
Go time.Parse(RFC3339)・Java ZonedDateTime.parse・Kotlin Instant.parse | NG | NG | NG | NG |
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)