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(存在する最も近い日に調整)
日数単位の加減算。「365 日後」と「1 年後」は別の意味になる。
年・月単位の加減算。存在しない日付は自動調整される。
月末日の計算とうるう年
「月末日」を扱う処理では、うるう年が影響します。
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 年までに廃止される予定(国際度量衡総会の決議)。
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
うるう年は正しく扱えるが、閏秒は扱えない。多くのアプリケーションではこれで十分。
astropy や専用ライブラリを検討。経過時間は time.perf_counter() を使う。
実務では、閏秒の厳密な扱いが本当に必要か要件を確認することが大切です。ほとんどの場合、datetime の「閏秒を無視する」動作で問題ありません。逆に、金融取引の時刻記録や科学計測など、1 秒未満の精度が求められる場面では、専用のライブラリや TAI 時刻の使用を検討してください。