Pythonうるう年を判定・カウント・列挙する方法【完全解説】

 

Pythonでうるう年を判定、カウント、列挙する方法を詳しく解説します。標準ライブラリのcalendarモジュールを使用した基本的な方法から、カスタム実装まで幅広くカバーします。

うるう年の基本概念

うるう年は4年に1度発生しますが、以下のルールに従います:

  • 4で割り切れる年はうるう年
  • ただし、100で割り切れる年は平年
  • ただし、400で割り切れる年はうるう年
import calendar

# 基本的なうるう年判定
print(f"2024年: {calendar.isleap(2024)}")  # True
print(f"2023年: {calendar.isleap(2023)}")  # False
print(f"2000年: {calendar.isleap(2000)}")  # True (400で割り切れる)
print(f"1900年: {calendar.isleap(1900)}")  # False (100で割り切れるが400では割り切れない)

うるう年の判定方法

calendarモジュールを使用

import calendar

def check_leap_year_calendar(year):
    """calendarモジュールを使用したうるう年判定"""
    return calendar.isleap(year)

# 使用例
years = [2020, 2021, 2022, 2023, 2024]
for year in years:
    is_leap = check_leap_year_calendar(year)
    print(f"{year}年: {'うるう年' if is_leap else '平年'}")

手動実装による判定

def is_leap_year_manual(year):
    """うるう年判定の手動実装"""
    if year % 400 == 0:
        return True
    elif year % 100 == 0:
        return False
    elif year % 4 == 0:
        return True
    else:
        return False

def is_leap_year_compact(year):
    """うるう年判定のコンパクト実装"""
    return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)

# 検証用テスト
test_years = [1600, 1700, 1800, 1900, 2000, 2020, 2021, 2024]
print("=== うるう年判定の検証 ===")
for year in test_years:
    calendar_result = calendar.isleap(year)
    manual_result = is_leap_year_manual(year)
    compact_result = is_leap_year_compact(year)
    
    print(f"{year}: calendar={calendar_result}, manual={manual_result}, compact={compact_result}")

詳細な判定結果を返す関数

def analyze_leap_year(year):
    """うるう年の詳細な分析結果を返す"""
    analysis = {
        'year': year,
        'is_leap': calendar.isleap(year),
        'divisible_by_4': year % 4 == 0,
        'divisible_by_100': year % 100 == 0,
        'divisible_by_400': year % 400 == 0,
        'reason': ''
    }
    
    if analysis['divisible_by_400']:
        analysis['reason'] = '400で割り切れるためうるう年'
    elif analysis['divisible_by_100']:
        analysis['reason'] = '100で割り切れるが400で割り切れないため平年'
    elif analysis['divisible_by_4']:
        analysis['reason'] = '4で割り切れるためうるう年'
    else:
        analysis['reason'] = '4で割り切れないため平年'
    
    return analysis

# 使用例
special_years = [1600, 1700, 1900, 2000, 2024]
for year in special_years:
    result = analyze_leap_year(year)
    print(f"{year}年: {result['reason']}")

うるう年のカウント

期間内のうるう年をカウント

import calendar

def count_leap_years(start_year, end_year):
    """指定期間内のうるう年をカウント"""
    count = 0
    leap_years = []
    
    for year in range(start_year, end_year + 1):
        if calendar.isleap(year):
            count += 1
            leap_years.append(year)
    
    return {
        'count': count,
        'leap_years': leap_years,
        'period': f"{start_year}-{end_year}",
        'total_years': end_year - start_year + 1
    }

# 使用例
result = count_leap_years(2000, 2030)
print(f"2000-2030年のうるう年: {result['count']}年")
print(f"うるう年: {result['leap_years']}")

効率的なうるう年カウント

def count_leap_years_efficient(start_year, end_year):
    """数学的計算による効率的なうるう年カウント"""
    def count_divisible(start, end, divisor):
        """指定した除数で割り切れる年の数をカウント"""
        return end // divisor - (start - 1) // divisor
    
    # 4で割り切れる年の数
    divisible_by_4 = count_divisible(start_year, end_year, 4)
    
    # 100で割り切れる年の数(平年になる)
    divisible_by_100 = count_divisible(start_year, end_year, 100)
    
    # 400で割り切れる年の数(うるう年になる)
    divisible_by_400 = count_divisible(start_year, end_year, 400)
    
    # うるう年の総数
    leap_count = divisible_by_4 - divisible_by_100 + divisible_by_400
    
    return {
        'count': leap_count,
        'divisible_by_4': divisible_by_4,
        'divisible_by_100': divisible_by_100,
        'divisible_by_400': divisible_by_400
    }

# 使用例と検証
start, end = 1900, 2100
efficient_result = count_leap_years_efficient(start, end)
normal_result = count_leap_years(start, end)

print(f"効率的計算: {efficient_result['count']}年")
print(f"通常計算: {normal_result['count']}年")
print(f"結果一致: {efficient_result['count'] == normal_result['count']}")

うるう年の列挙

基本的な列挙

import calendar

def list_leap_years(start_year, end_year):
    """指定範囲のうるう年を列挙"""
    leap_years = []
    for year in range(start_year, end_year + 1):
        if calendar.isleap(year):
            leap_years.append(year)
    return leap_years

def list_leap_years_generator(start_year, end_year):
    """ジェネレータを使用したうるう年列挙"""
    for year in range(start_year, end_year + 1):
        if calendar.isleap(year):
            yield year

# 使用例
leap_years_list = list_leap_years(2020, 2040)
print(f"2020-2040年のうるう年: {leap_years_list}")

# ジェネレータの使用
print("ジェネレータでの取得:")
for leap_year in list_leap_years_generator(2020, 2030):
    print(f"  {leap_year}年")

詳細情報付きの列挙

import calendar
from datetime import date

def enumerate_leap_years_detailed(start_year, end_year):
    """うるう年の詳細情報を含む列挙"""
    leap_years_info = []
    
    for year in range(start_year, end_year + 1):
        if calendar.isleap(year):
            # 2月29日の曜日を取得
            feb_29 = date(year, 2, 29)
            weekday_names = ['月', '火', '水', '木', '金', '土', '日']
            weekday = weekday_names[feb_29.weekday()]
            
            leap_info = {
                'year': year,
                'feb_29_weekday': weekday,
                'days_in_year': 366,
                'years_since_last': None
            }
            
            # 前回のうるう年からの年数を計算
            if leap_years_info:
                leap_info['years_since_last'] = year - leap_years_info[-1]['year']
            
            leap_years_info.append(leap_info)
    
    return leap_years_info

# 使用例
detailed_leap_years = enumerate_leap_years_detailed(2020, 2032)
print("=== うるう年詳細情報 ===")
for info in detailed_leap_years:
    years_info = f" ({info['years_since_last']}年後)" if info['years_since_last'] else ""
    print(f"{info['year']}年: 2月29日は{info['feb_29_weekday']}曜日{years_info}")

特定条件でのうるう年列挙

import calendar

def find_leap_years_by_condition(start_year, end_year, condition_func):
    """特定条件を満たすうるう年を検索"""
    matching_years = []
    
    for year in range(start_year, end_year + 1):
        if calendar.isleap(year) and condition_func(year):
            matching_years.append(year)
    
    return matching_years

# 条件関数の例
def is_century_year(year):
    """世紀年(100で割り切れる年)かどうか"""
    return year % 100 == 0

def ends_with_digit(year, digit):
    """指定した数字で終わる年かどうか"""
    return year % 10 == digit

# 使用例
# 世紀年のうるう年
century_leap_years = find_leap_years_by_condition(1600, 2400, is_century_year)
print(f"世紀年のうるう年: {century_leap_years}")

# 4で終わるうるう年
leap_years_ending_4 = find_leap_years_by_condition(2000, 2100, lambda y: ends_with_digit(y, 4))
print(f"4で終わるうるう年: {leap_years_ending_4}")

実用的な応用例

年齢計算におけるうるう年考慮

import calendar
from datetime import date, timedelta

def calculate_age_with_leap_days(birth_year, birth_month, birth_day, target_date=None):
    """うるう年を考慮した年齢計算"""
    if target_date is None:
        target_date = date.today()
    
    birth_date = date(birth_year, birth_month, birth_day)
    
    # 基本年齢計算
    age = target_date.year - birth_date.year
    if target_date.month < birth_date.month or \
       (target_date.month == birth_date.month and target_date.day < birth_date.day):
        age -= 1
    
    # 経験したうるう日の数を計算
    leap_days_experienced = 0
    for year in range(birth_year, target_date.year + 1):
        if calendar.isleap(year):
            leap_day = date(year, 2, 29)
            if birth_date <= leap_day <= target_date:
                leap_days_experienced += 1
    
    # 総日数計算
    total_days = (target_date - birth_date).days
    
    return {
        'age_years': age,
        'total_days': total_days,
        'leap_days_experienced': leap_days_experienced,
        'birth_date': birth_date,
        'target_date': target_date
    }

# 使用例
age_info = calculate_age_with_leap_days(2000, 2, 28)
print(f"年齢: {age_info['age_years']}歳")
print(f"総日数: {age_info['total_days']}日")
print(f"経験したうるう日: {age_info['leap_days_experienced']}日")

カレンダー期間の計算

import calendar
from datetime import date, timedelta

def calculate_period_with_leap_info(start_date, end_date):
    """期間内のうるう年情報を含む計算"""
    total_days = (end_date - start_date).days
    
    # 期間内のうるう年を取得
    leap_years = []
    leap_days_in_period = 0
    
    for year in range(start_date.year, end_date.year + 1):
        if calendar.isleap(year):
            leap_day = date(year, 2, 29)
            if start_date <= leap_day <= end_date:
                leap_years.append(year)
                leap_days_in_period += 1
    
    # 平均年長計算
    years_span = end_date.year - start_date.year + 1
    average_year_length = total_days / years_span if years_span > 0 else 0
    
    return {
        'start_date': start_date,
        'end_date': end_date,
        'total_days': total_days,
        'years_span': years_span,
        'leap_years_in_period': leap_years,
        'leap_days_in_period': leap_days_in_period,
        'average_year_length': round(average_year_length, 2)
    }

# 使用例
start = date(2020, 1, 1)
end = date(2030, 12, 31)
period_info = calculate_period_with_leap_info(start, end)

print(f"期間: {period_info['start_date']} ~ {period_info['end_date']}")
print(f"総日数: {period_info['total_days']}日")
print(f"期間内のうるう年: {period_info['leap_years_in_period']}")
print(f"うるう日の数: {period_info['leap_days_in_period']}日")
print(f"平均年長: {period_info['average_year_length']}日")

2月29日生まれの人の誕生日計算

import calendar
from datetime import date

def get_birthday_dates_for_leap_day_born(birth_year, target_years):
    """2月29日生まれの人の各年の誕生日を計算"""
    if not calendar.isleap(birth_year):
        raise ValueError("誕生年がうるう年ではありません")
    
    birthday_info = []
    
    for year in target_years:
        if calendar.isleap(year):
            # うるう年の場合は2月29日
            birthday = date(year, 2, 29)
            is_actual_birthday = True
        else:
            # 平年の場合は2月28日または3月1日
            # 一般的には2月28日が使用される
            birthday = date(year, 2, 28)
            is_actual_birthday = False
        
        age = year - birth_year
        
        birthday_info.append({
            'year': year,
            'birthday': birthday,
            'age': age,
            'is_actual_birthday': is_actual_birthday,
            'is_leap_year': calendar.isleap(year)
        })
    
    return birthday_info

# 使用例
birth_year = 2000  # うるう年
target_years = list(range(2020, 2030))

birthday_schedule = get_birthday_dates_for_leap_day_born(birth_year, target_years)
print("=== 2月29日生まれの誕生日スケジュール ===")
for info in birthday_schedule:
    actual_note = "実際の誕生日" if info['is_actual_birthday'] else "代替日"
    print(f"{info['year']}年 ({info['age']}歳): {info['birthday']} ({actual_note})")

うるう年の統計分析

import calendar

def analyze_leap_year_statistics(start_year, end_year):
    """うるう年の統計分析"""
    total_years = end_year - start_year + 1
    leap_years = []
    intervals = []
    
    # うるう年の収集
    for year in range(start_year, end_year + 1):
        if calendar.isleap(year):
            leap_years.append(year)
    
    # うるう年間の間隔計算
    for i in range(1, len(leap_years)):
        intervals.append(leap_years[i] - leap_years[i-1])
    
    # 統計計算
    leap_count = len(leap_years)
    leap_percentage = (leap_count / total_years) * 100
    
    statistics = {
        'period': f"{start_year}-{end_year}",
        'total_years': total_years,
        'leap_years_count': leap_count,
        'leap_percentage': round(leap_percentage, 2),
        'leap_years': leap_years,
        'intervals': intervals,
        'average_interval': round(sum(intervals) / len(intervals), 2) if intervals else 0,
        'min_interval': min(intervals) if intervals else 0,
        'max_interval': max(intervals) if intervals else 0
    }
    
    return statistics

# 使用例
stats = analyze_leap_year_statistics(1900, 2100)
print(f"=== {stats['period']} うるう年統計 ===")
print(f"総年数: {stats['total_years']}年")
print(f"うるう年数: {stats['leap_years_count']}年")
print(f"うるう年割合: {stats['leap_percentage']}%")
print(f"平均間隔: {stats['average_interval']}年")
print(f"間隔範囲: {stats['min_interval']}-{stats['max_interval']}年")

うるう年カレンダージェネレータ

import calendar

class LeapYearCalendar:
    def __init__(self):
        self.leap_years_cache = {}
    
    def is_leap_year(self, year):
        """キャッシュ付きうるう年判定"""
        if year not in self.leap_years_cache:
            self.leap_years_cache[year] = calendar.isleap(year)
        return self.leap_years_cache[year]
    
    def get_february_info(self, year):
        """2月の情報を取得"""
        is_leap = self.is_leap_year(year)
        days_in_feb = 29 if is_leap else 28
        
        return {
            'year': year,
            'is_leap_year': is_leap,
            'days_in_february': days_in_feb,
            'february_calendar': calendar.monthcalendar(year, 2)
        }
    
    def compare_februaries(self, years):
        """複数年の2月を比較"""
        comparison = []
        
        for year in years:
            feb_info = self.get_february_info(year)
            comparison.append(feb_info)
        
        return comparison
    
    def next_leap_year(self, from_year):
        """指定年以降の次のうるう年を取得"""
        year = from_year + 1
        while not self.is_leap_year(year):
            year += 1
        return year
    
    def previous_leap_year(self, from_year):
        """指定年以前の前のうるう年を取得"""
        year = from_year - 1
        while year > 0 and not self.is_leap_year(year):
            year -= 1
        return year if year > 0 else None

# 使用例
leap_cal = LeapYearCalendar()

# 2月の比較
years_to_compare = [2023, 2024, 2025]
feb_comparison = leap_cal.compare_februaries(years_to_compare)

print("=== 2月の比較 ===")
for feb_info in feb_comparison:
    leap_status = "うるう年" if feb_info['is_leap_year'] else "平年"
    print(f"{feb_info['year']}年: {feb_info['days_in_february']}日 ({leap_status})")

# 次のうるう年検索
current_year = 2023
next_leap = leap_cal.next_leap_year(current_year)
prev_leap = leap_cal.previous_leap_year(current_year)

print(f"\n{current_year}年の前後のうるう年:")
print(f"前: {prev_leap}年")
print(f"次: {next_leap}年")

パフォーマンス比較

import calendar
import time

def performance_test():
    """うるう年判定のパフォーマンステスト"""
    test_years = list(range(1, 10000))
    
    # calendar.isleap()のテスト
    start_time = time.time()
    for year in test_years:
        calendar.isleap(year)
    calendar_time = time.time() - start_time
    
    # 手動実装のテスト
    start_time = time.time()
    for year in test_years:
        year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
    manual_time = time.time() - start_time
    
    print(f"=== パフォーマンステスト({len(test_years)}年分)===")
    print(f"calendar.isleap(): {calendar_time:.4f}秒")
    print(f"手動実装: {manual_time:.4f}秒")
    print(f"速度比: {calendar_time / manual_time:.2f}倍")

# パフォーマンステストの実行
performance_test()

まとめ

  • 判定方法
    • calendar.isleap():標準的な方法
    • 手動実装:カスタマイズが必要な場合
  • カウント:効率的な数学的計算が可能
  • 列挙:ジェネレータを活用したメモリ効率化
  • 実用例:年齢計算、誕生日管理、期間計算
  • 応用:統計分析、カレンダー生成、パフォーマンス最適化

うるう年の理解と適切な処理により、正確な日付計算アプリケーションが開発できます。

■プロンプトだけでオリジナルアプリを開発・公開してみた!!

■AI時代の第一歩!「AI駆動開発コース」はじめました!

テックジム東京本校で先行開始。

■テックジム東京本校

「武田塾」のプログラミング版といえば「テックジム」。
講義動画なし、教科書なし。「進捗管理とコーチング」で効率学習。
より早く、より安く、しかも対面型のプログラミングスクールです。

<短期講習>5日で5万円の「Pythonミニキャンプ」開催中。

<月1開催>放送作家による映像ディレクター養成講座

<オンライン無料>ゼロから始めるPython爆速講座