Python で「営業日」を計算する(祝日・休業日対応)

業務システムでは「3 営業日後」「月末の最終営業日」といった計算が頻繁に必要になります。Python には営業日計算をサポートするライブラリがいくつかありますが、それぞれ特徴が異なります。

numpy の busday 関数

NumPy には営業日計算用の関数が組み込まれています。科学計算ライブラリですが、日付計算にも使えます。

import numpy as np
from datetime import date

# 5営業日後を計算
today = date(2024, 12, 20)  # 金曜日
result = np.busday_offset(today, 5)
print(result)
# 2024-12-27(土日を除いた5営業日後)

busday_offset は土日を自動的にスキップします。祝日を考慮するには holidays パラメータを指定します。

import numpy as np
from datetime import date

# 祝日を指定
holidays = [
    '2024-12-23',  # 天皇誕生日の振替
    '2024-12-31',  # 年末
    '2025-01-01',  # 元日
]

today = date(2024, 12, 20)
result = np.busday_offset(today, 5, holidays=holidays)
print(result)
# 2024-12-30(祝日も除外)

2 つの日付間の営業日数を数えることもできます。

import numpy as np

start = '2024-12-01'
end = '2024-12-31'
count = np.busday_count(start, end)
print(count)
# 22(12月の営業日数、祝日考慮なし)

pandas の営業日オフセット

pandas はより柔軟な営業日計算を提供します。

import pandas as pd
from datetime import date

# BusinessDay オフセット
today = pd.Timestamp(2024, 12, 20)
result = today + pd.offsets.BusinessDay(5)
print(result)
# 2024-12-27 00:00:00

pandas の強みはカスタムカレンダーを定義できる点です。

import pandas as pd
from pandas.tseries.holiday import AbstractHolidayCalendar, Holiday

# 日本の祝日カレンダーを定義
class JapaneseHolidayCalendar(AbstractHolidayCalendar):
    rules = [
        Holiday('元日', month=1, day=1),
        Holiday('成人の日', month=1, day=8),  # 第2月曜
        Holiday('建国記念の日', month=2, day=11),
        # ... 他の祝日を追加
    ]

# カスタムカレンダーを使った営業日
from pandas.tseries.offsets import CustomBusinessDay

jpn_bd = CustomBusinessDay(calendar=JapaneseHolidayCalendar())
today = pd.Timestamp(2024, 12, 20)
result = today + 5 * jpn_bd
print(result)

ただし、日本の祝日は「春分の日」や「秋分の日」のように天文計算で決まるもの、「ハッピーマンデー」で移動するものがあり、手動での定義は現実的ではありません。

jpholiday ライブラリ

日本の祝日に特化した jpholiday ライブラリを使うと、複雑な祝日ルールを自分で実装する必要がなくなります。

import jpholiday
from datetime import date

# 特定の日が祝日かどうか
print(jpholiday.is_holiday(date(2024, 11, 3)))
# True(文化の日)

# 祝日名を取得
print(jpholiday.is_holiday_name(date(2024, 11, 3)))
# 文化の日

# 期間内の祝日一覧
holidays = jpholiday.between(date(2024, 1, 1), date(2024, 12, 31))
for d, name in holidays:
    print(f"{d}: {name}")

jpholiday は振替休日や国民の休日も正しく計算してくれます。

numpy.busday

高速だが祝日リストを自分で用意する必要がある

jpholiday

日本の祝日を自動計算してくれるが、営業日計算機能自体はない

営業日計算の実装

jpholiday と組み合わせて営業日計算を実装してみます。

import jpholiday
from datetime import date, timedelta

def is_business_day(d: date) -> bool:
    """営業日かどうか判定"""
    # 土日チェック
    if d.weekday() >= 5:
        return False
    # 祝日チェック
    if jpholiday.is_holiday(d):
        return False
    return True

def add_business_days(start: date, days: int) -> date:
    """営業日を加算"""
    current = start
    remaining = days
    direction = 1 if days >= 0 else -1
    remaining = abs(remaining)
    
    while remaining > 0:
        current += timedelta(days=direction)
        if is_business_day(current):
            remaining -= 1
    
    return current

# 使用例
today = date(2024, 12, 20)
result = add_business_days(today, 5)
print(result)

この実装はシンプルですが、大量の日付を処理する場合はパフォーマンスに問題が出ます。

高速化のアプローチ

営業日を事前計算してキャッシュする方法が効果的です。

import jpholiday
from datetime import date, timedelta
from functools import lru_cache

@lru_cache(maxsize=None)
def get_business_days_list(year: int) -> list[date]:
    """年間の営業日リストを生成"""
    start = date(year, 1, 1)
    end = date(year, 12, 31)
    
    business_days = []
    current = start
    while current <= end:
        if current.weekday() < 5 and not jpholiday.is_holiday(current):
            business_days.append(current)
        current += timedelta(days=1)
    
    return business_days

def add_business_days_fast(start: date, days: int) -> date:
    """キャッシュを使った高速営業日計算"""
    bd_list = get_business_days_list(start.year)
    
    try:
        idx = bd_list.index(start)
    except ValueError:
        # startが営業日でない場合、次の営業日を探す
        for i, d in enumerate(bd_list):
            if d > start:
                idx = i
                break
    
    new_idx = idx + days
    
    # 年をまたぐ場合の処理
    while new_idx >= len(bd_list):
        new_idx -= len(bd_list)
        bd_list = get_business_days_list(bd_list[0].year + 1)
    
    return bd_list[new_idx]

月末営業日の計算

「月末の最終営業日」もよく使う計算パターンです。

import jpholiday
from datetime import date, timedelta
import calendar

def last_business_day_of_month(year: int, month: int) -> date:
    """月の最終営業日を取得"""
    # 月末日を取得
    last_day = calendar.monthrange(year, month)[1]
    d = date(year, month, last_day)
    
    # 営業日になるまで遡る
    while d.weekday() >= 5 or jpholiday.is_holiday(d):
        d -= timedelta(days=1)
    
    return d

# 使用例
for month in range(1, 13):
    last_bd = last_business_day_of_month(2024, month)
    print(f"2024年{month}月の最終営業日: {last_bd}")

会社独自の休業日

実務では会社独自の休業日(創立記念日、年末年始の特別休暇など)を考慮する必要があります。

import jpholiday
from datetime import date

class BusinessDayCalculator:
    def __init__(self, company_holidays: list[date] = None):
        self.company_holidays = set(company_holidays or [])
    
    def is_business_day(self, d: date) -> bool:
        if d.weekday() >= 5:
            return False
        if jpholiday.is_holiday(d):
            return False
        if d in self.company_holidays:
            return False
        return True

# 使用例
company_holidays = [
    date(2024, 12, 28),  # 仕事納め翌日
    date(2024, 12, 30),
    date(2024, 12, 31),
]

calc = BusinessDayCalculator(company_holidays)
print(calc.is_business_day(date(2024, 12, 30)))
# False(会社休業日)
NumPy を使う場合

高速だが祝日リストを手動管理。バッチ処理向き。

jpholiday を使う場合

日本の祝日を自動計算。会社独自の休業日と組み合わせて使う。

営業日計算は一見単純ですが、祝日の扱い、年またぎ、うるう年など考慮すべき点が多いです。信頼性のあるライブラリを活用し、テストケースを充実させることが重要です。