ISO 8601のP1DT12HとPT36Hは夏時間の日に1時間ずれる
ISO 8601 の期間表記で、P1DT12H(1日と12時間)と PT36H(36時間)は、秒に直すとどちらも 129600 秒になる。ところが、アメリカの夏時間が始まる前日の正午に足すと、片方は翌々日の 0 時、もう片方は 1 時になる。
ハシトシステムの ISO 8601 期間(duration)変換 は、起点の日時を持たずに期間を秒に直すツールなので、この2つを同じ 129600 秒として出す。この記事では、同じ文字列を JavaScript の Temporal.Duration にも読ませて結果を比べ、「起点がないと決まらないもの」がどこにあるかを確かめる。
期間表記の基本と、規格による違い
P で始まり、日付の部分(Y=年、M=月、W=週、D=日)、T の後ろに時刻の部分(H=時、M=分、S=秒)を書く。T の前の M は月、後ろの M は分なので、P1M は1か月、PT1M は1分である。
細かい決まりは規格によって違う。
- RFC 3339 の Appendix A(情報提供のための ABNF)では、
duration = "P" (dur-date / dur-time / dur-week)で、週(W)は単独でしか書けない。小数も定義されていない - MDN の
Temporal.Durationの説明によると、ISO 8601-1 では週を他の単位と組み合わせられず、負の期間も書けない。Temporal は ISO 8601-2 の拡張に従い、先頭の符号と、P3W1Dのような週と他の単位の組み合わせを受け付ける - Temporal では、小数を書けるのは最後の成分だけで、しかもそれが時・分・秒のときに限る(
PT1.1Hは可、P1.5Dは不可)。小数点は.と,のどちらでもよい
同じ文字列を2つの実装に読ませる
公開中のツールのページから parseIsoDuration を取り出したものと、Node.js v24.11.1(V8 13.6)の Temporal.Duration.from に同じ文字列を渡した。Node.js 24 では Temporal は既定で有効になっておらず、--harmony-temporal フラグを付けたときだけ使える。
| 入力 | ツール(秒) | Temporal.Duration.from |
total({unit:'seconds'}) |
|---|---|---|---|
PT1H30M |
5400 | PT1H30M |
5400 |
PT36H |
129600 | PT36H |
129600 |
P1DT12H |
129600 | P1DT12H |
129600 |
P1M |
2592000(概算) | P1M |
RangeError |
P1Y |
31536000(概算) | P1Y |
RangeError |
P1W3D |
864000 | P1W3D |
RangeError |
PT1.5H |
5400 | PT1H30M |
5400 |
PT1.5H30M |
7200 | RangeError | - |
P1.5D |
129600 | RangeError | - |
PT0,5S |
0.5 | PT0.5S |
0.5 |
pt90s |
90 | PT90S |
90 |
P / PT |
エラー | RangeError | - |
読み取れることは次のとおり。
- 年・月・週を含む期間は、Temporal では起点なしに秒にできない。
total()が RangeError を投げる。ツールは1年365日・1か月30日・1週7日で計算し、年か月を含むときは「概算」と表示する。週は7日固定なので概算の表示は出ない。 - 日(D)は、起点がなければ Temporal も24時間として扱う。そのため
P1DT12HもPT36Hも 129600 秒になり、ここまでは両者が一致する。 - 小数の扱いが分かれた。ツールはどの成分の小数も受け付けて足し合わせるので、
PT1.5H30Mは2時間になる。Temporal は最後の成分以外に小数があると受け付けない。P1.5Dも、日は時刻の成分ではないので受け付けない。 P1W3Dは Temporal とツールの両方が読めるが、RFC 3339 の ABNF には合わない。他のシステムに渡すならP10Dと書くほうが通りやすい。
夏時間の日に足すと1時間ずれる
2026年のアメリカ(America/New_York)の夏時間は、3月8日の午前2時に始まる。この日は23時間しかない。その前日、3月7日の正午を起点にして、4つの期間を Temporal.ZonedDateTime に足した。
| 期間 | 結果 | 実際の経過時間 |
|---|---|---|
P1DT12H |
2026-03-09T00:00-04:00 | 35 時間 |
PT36H |
2026-03-09T01:00-04:00 | 36 時間 |
P1D |
2026-03-08T12:00-04:00 | 23 時間 |
PT24H |
2026-03-08T13:00-04:00 | 24 時間 |
Temporal は、日を「カレンダーの1日(翌日の同じ時刻)」、時間を「実際に経過する時間」として足す。P1D は翌日の正午になり、その間は23時間しかない。PT24H は24時間後なので、時計の上では13時になる。
つまり、秒に直すと同じ期間でも、日で書くか時間で書くかで意味が違う。「毎日同じ時刻に実行」は P1D、「ちょうど24時間後に期限切れ」は PT24H と書き分ける必要がある。起点を持たない変換ツールは、この違いを区別できない。
Python の timedelta は壁時計の足し算になる
Python 3.11.9 で、zoneinfo の America/New_York を持つ aware な datetime に timedelta を足すと、結果は次のようになった。
| 足したもの | 結果 | 実際の経過時間 |
|---|---|---|
timedelta(days=1, hours=12) |
2026-03-09T00:00-04:00 | 35 時間 |
timedelta(hours=36) |
2026-03-09T00:00-04:00 | 35 時間 |
UTC に直して hours=36 を足し、戻す |
2026-03-09T01:00-04:00 | 36 時間 |
Python のドキュメントは、datetime + timedelta について「入力が aware でもタイムゾーンの調整は行わない」と書いている。足し算は壁時計の上で行われ、hours=36 でも実際の経過は35時間になる。36時間後を求めたいときは、UTC に直してから足す。
実際に試す
Node.js 24(記事では v24.11.1、--harmony-temporal フラグが必要)と Python 3.9 以降(zoneinfo を使うため。記事では 3.11.9)で試せる。Windows の Python で zoneinfo がタイムゾーンを見つけられない場合は、pip install tzdata を入れる。
1. ツールのパーサーと Temporal を比べる
1 | |
1 | |
ページの中身が変われば結果も変わるので、2026-11-03 時点の結果として読んでほしい。
2. 夏時間の前日に足す・月末に1か月足す
1 | |
1 | |
月の足し算は、存在しない日(2月31日など)になると、既定ではその月の末日に丸める。overflow: 'reject' を渡すと RangeError になる。丸めが起きるので、1か月ずつ2回足すのと、2か月を1回足すのでは結果が違う(3月28日と3月31日)。毎月の請求日のように「起点から N か月後」を出すときは、前回の結果に足し続けず、起点に P{N}M を足す。
relativeTo に起点を渡すと、P1M の長さは2026年2月なら28日、3月なら31日、2028年2月なら29日と決まる。タイムゾーンつきの起点を渡すと、夏時間が始まる日の P1D は23時間になる。
3. Python で同じ足し算をする
1 | |
1 | |
実際の経過時間は、両方を UTC に直してから引き算して求めている。同じ tzinfo を持つ aware な datetime 同士の引き算も、ドキュメントのとおりタイムゾーンを無視した壁時計の差になるので、そのまま引くと 36 時間と出てしまう。
ハシトシステムの ISO 8601 期間変換では
ISO 8601 期間(duration)変換 は、起点を持たずに期間を秒・時:分:秒・読み下しに直すツールである。2026-11-03 時点のコードでは、次のように動く。
- 日は24時間、週は7日として計算する。年は365日、月は30日の概算で、年か月を含むときは結果の下に概算である旨を表示する。
P1DT12HとPT36Hは同じ 129600 秒になる。夏時間をまたぐ実際の足し算の結果は、起点とタイムゾーンを決めて Temporal などで計算する。- 小数はどの成分にも書ける。
PT1.5H30Mは2時間として読む。Temporal など、最後の成分以外の小数を受け付けない実装もあるので、他のシステムに渡す文字列は、ツールの「期間から ISO 8601 を作る」で整数の表記に直してから使うとよい。 - 先頭の
-と小文字(pt90s)も受け付ける。
動画の長さやレシピの調理時間のように、カレンダーと関係のない長さを書くなら PT1H30M のように時刻の部分だけで書くと、起点による違いが出ない。日付と時刻の変換の話は、new Date(‘2026-10-30’) が UTC になる理由の記事 と 東京の10時を他都市に変換する記事 にも書いた。Unix 時間との相互変換は Unix時間 変換 で試せる。
まとめ
P1Mは1か月、PT1Mは1分。Tの前のMは月、後ろは分。P1DT12HとPT36Hは秒にすると同じだが、夏時間をまたいで足すと1時間ずれる。日はカレンダーの1日、時間は実際の経過時間として足される。- 年・月・週を含む期間は、起点がないと秒にできない。Temporal では
relativeToを渡す。 - 月末に
P1Mを足すと末日に丸められる。1か月ずつ足し続けると、2か月を1回足した結果とずれる。 - Python の aware な
datetimeにtimedeltaを足すと、タイムゾーンの調整なしの壁時計の足し算になる。経過時間で足すなら UTC に直してから足す。 - 週と他の単位の組み合わせや小数の書き方は、規格と実装で受け付ける範囲が違う。他のシステムに渡すなら整数の時刻部分だけで書く。
参考
- RFC 3339 Date and Time on the Internet: Timestamps — Appendix A. ISO 8601 Collected ABNF(duration)
- MDN Web Docs: Temporal.Duration(ISO 8601 duration format、Calendar durations、relativeTo)
- Python 3 ドキュメント datetime — Basic date and time types: datetime のサポートされる演算(注1〜注3)