Python で閏秒やうるう年を正しく扱う

日付・時刻を扱うプログラムでは、閏秒やうるう年といった例外的なケースを正しく処理する必要があります。Python の datetime がこれらをどう扱うか、そして実務でどう対処すべきかを解説します。

うるう年の判定

うるう年のルールは「4 で割り切れる年はうるう年、ただし 100 で割り切れる年は平年、ただし 400 で割り切れる年はうるう年」です。Python の calendar モジュールにはこの判定関数があります。

import calendar

# うるう年の判定
print(calendar.isleap(2024))  # True(4で割り切れる)
print(calendar.isleap(2100))  # False(100で割り切れるが400で割り切れない)
print(calendar.isleap(2000))  # True(400で割り切れる)

datetime でも 2 月 29 日を指定すれば、うるう年かどうかを間接的に確認できます。

from datetime import date

try:
    d = date(2024, 2, 29)
    print(f"{d.year}年はうるう年")
except ValueError:
    print(f"うるう年ではない")

うるう年をまたぐ計算

うるう年で問題になるのは「1 年後」の計算です。2024 年 2 月 29 日の 1 年後は何日でしょうか。

from datetime import date, timedelta

# 2月29日に365日を足す
feb29 = date(2024, 2, 29)
one_year_later = feb29 + timedelta(days=365)
print(one_year_later)
# 2025-02-28(365日後)

timedelta(days=365) は単純に 365 日を足すので、2 月 28 日になります。では timedelta(days=366) なら?

one_year_later = feb29 + timedelta(days=366)
print(one_year_later)
# 2025-03-01

「1 年後」の定義によって結果が変わります。業務ロジックで「1 年後」が必要な場合、dateutil ライブラリの relativedelta を使うと直感的です。

from datetime import date
from dateutil.relativedelta import relativedelta

feb29 = date(2024, 2, 29)
one_year_later = feb29 + relativedelta(years=1)
print(one_year_later)
# 2025-02-28(存在する最も近い日に調整)
timedelta

日数単位の加減算。「365 日後」と「1 年後」は別の意味になる。

relativedelta

年・月単位の加減算。存在しない日付は自動調整される。

月末日の計算とうるう年

「月末日」を扱う処理では、うるう年が影響します。

import calendar
from datetime import date

def last_day_of_month(year: int, month: int) -> date:
    """月の最終日を取得"""
    last_day = calendar.monthrange(year, month)[1]
    return date(year, month, last_day)

# うるう年の2月
print(last_day_of_month(2024, 2))  # 2024-02-29
print(last_day_of_month(2025, 2))  # 2025-02-28

月末起算で「翌月の同日」を計算する場合、3 月 31 日の翌月は 4 月 30 日になるべきか、エラーにすべきか、業務要件次第です。

from datetime import date
from dateutil.relativedelta import relativedelta

# 3月31日の1ヶ月後
mar31 = date(2024, 3, 31)
next_month = mar31 + relativedelta(months=1)
print(next_month)
# 2024-04-30(4月31日は存在しないので調整)

閏秒とは

閏秒は地球の自転速度のゆらぎを補正するため、UTC に 1 秒を挿入(または削除)する仕組みです。最後に閏秒が挿入されたのは 2016 年 12 月 31 日で、23:59:60 という通常存在しない時刻が発生しました。

閏秒の歴史

1972 年から 2016 年までに 27 回挿入された。2035 年までに廃止される予定(国際度量衡総会の決議)。

UTC と TAI

TAI(国際原子時)は閏秒を含まない。UTC = TAI - 37 秒(2024 年現在)。

Python と閏秒

結論から言うと、Python の datetime は閏秒を扱えません。

from datetime import datetime

# 閏秒の時刻を作ろうとする
try:
    dt = datetime(2016, 12, 31, 23, 59, 60)
except ValueError as e:
    print(e)
# second must be in 0..59

これは datetime が POSIX 時間に基づいているためです。POSIX 時間では 1 日は常に 86400 秒と定義されており、閏秒は存在しません。

from datetime import datetime, timezone

# Unix タイムスタンプから datetime
ts = 1483228800  # 2017-01-01 00:00:00 UTC
dt = datetime.fromtimestamp(ts, tz=timezone.utc)
print(dt)
# 2017-01-01 00:00:00+00:00

Unix タイムスタンプ 1483228800 は、閏秒を含めれば 2016-12-31 23:59:60 を指すはずですが、POSIX の定義では 2017-01-01 00:00:00 になります。この 1 秒のズレは POSIX 時間の仕様上避けられません。

閏秒が問題になるケース

多くのアプリケーションでは閏秒を無視しても問題ありません。しかし、以下のケースでは注意が必要です。

高精度な時刻同期が必要なシステム

長時間の経過秒数を正確に計算する処理

異なる時刻基準のシステム間連携

実務での対処法

閏秒を正確に扱う必要がある場合、以下のアプローチがあります。

# TAI(国際原子時)を扱うライブラリの例
# pip install astropy

from astropy.time import Time

# UTC 時刻を TAI に変換
t = Time('2016-12-31 23:59:59', scale='utc')
print(t.tai)
# 2017-01-01 00:00:35.000

# TAI は閏秒を含まないので一貫した時刻計算が可能

astropy は天文学用のライブラリですが、高精度な時刻計算が必要な場合に使えます。

# 経過時間の計測には time.perf_counter()
import time

start = time.perf_counter()
# ... 処理 ...
elapsed = time.perf_counter() - start
print(f"経過時間: {elapsed}")

time.perf_counter() はシステムの高精度タイマーを使い、システム時刻の調整(NTP 同期や閏秒)の影響を受けません。経過時間の計測にはこちらを使うべきです。

2038 年問題

Unix タイムスタンプは多くのシステムで 32 ビット符号付き整数として格納されています。この場合、2038 年 1 月 19 日 3:14:07 UTC を超えるとオーバーフローします。

from datetime import datetime, timezone

# 32ビット符号付き整数の最大値
max_32bit = 2**31 - 1
dt = datetime.fromtimestamp(max_32bit, tz=timezone.utc)
print(dt)
# 2038-01-19 03:14:07+00:00

# その1秒後は32ビットでは表現できない
# (オーバーフローして1901年になる環境もある)

Python 自体は任意精度整数を使うので問題ありませんが、OS やデータベースとやり取りする際に注意が必要です。

from datetime import datetime, timezone

# Python は問題なく2038年以降を扱える
future = datetime(2050, 1, 1, tzinfo=timezone.utc)
print(future.timestamp())
# 2524608000.0

# 64ビットシステムでは問題ない
print(future)
# 2050-01-01 00:00:00+00:00
datetime モジュール

うるう年は正しく扱えるが、閏秒は扱えない。多くのアプリケーションではこれで十分。

高精度が必要な場合

astropy や専用ライブラリを検討。経過時間は time.perf_counter() を使う。

実務では、閏秒の厳密な扱いが本当に必要か要件を確認することが大切です。ほとんどの場合、datetime の「閏秒を無視する」動作で問題ありません。逆に、金融取引の時刻記録や科学計測など、1 秒未満の精度が求められる場面では、専用のライブラリや TAI 時刻の使用を検討してください。