休日つき datetime.date 風クラス


JavaScript で作ったものを Python で実装しました。

https://qiita.com/ikiuo/items/4ab99731ecac076f3608

# JHDate クラス

JHDate はオブジェクトの生成時に休日を取得しています。

datetime.date に以下を追加しています。

```text:
プロパティ:
    holiday
        休日名（振替休日も対象）

クラス メソッド:
    holidays(year, month=None)
        指定された年(year)と月(month)から祝日の一覧を返す。
        振替休日は含みません。
```

```python:jholiday.py
#!/usr/bin/env python3

import datetime


class JHoliday:
    MONTH = {n+1: {} for n in range(12)}

    def __init__(self, name, month, day, start=None, end=None):
        self._name = name
        self._month = month
        self._day = day
        self._start = start
        self._end = end
        mtab = JHoliday.MONTH[month]
        if day not in mtab:
            mtab[day] = []
        mtab[day].append(self)

    @property
    def name(self):
        return self._name

    @property
    def month(self):
        return self._month

    @property
    def day(self):
        return self._day

    @property
    def start(self):
        return self._start or 1948

    @property
    def end(self):
        return self._end or 10000

    def __repr__(self):
        return ('JHoliday('
                f'name={repr(self.name)},'
                f' month={self.month},'
                f' day={repr(self.day)},'
                f' start={self._start},'
                f' end={self._end})')

    def valid(self, year):
        return self.start <= year and year <= self.end

    @staticmethod
    def monthtable(month):
        return JHoliday.MONTH.get(month)

    @staticmethod
    def basic(date):
        S = JHoliday

        year = date.year
        month = date.month
        mday = date.day
        wday = date.isoweekday()

        mtab = S.MONTH.get(month)
        dtab = mtab.get(mday)
        if dtab:
            ytab = list(filter(lambda h: h.valid(year), dtab))
            if ytab:
                return ytab[0].name
        if wday == 1:
            mweek = (mday - 1) // 7
            monday = mtab.get(f'M{mweek+1}')
            if monday:
                t = list(filter(lambda h: h.valid(year), monday))
                if t:
                    return t[0].name
        t = mtab.get('VE')
        if t:
            ve = S.vernalequinox(year)
            if ve == date:
                return t[0].name
        t = mtab.get('AE')
        if t:
            ae = S.autumnequinox(year)
            if ae == date:
                return t[0].name
        return None

    @staticmethod
    def vernalequinox(year):
        sec = (315569255616 * (year - 1970) + 68619916800) // 10000
        return datetime.date.fromtimestamp(sec)

    @staticmethod
    def autumnequinox(year):
        sec = (315569255616 * (year - 1970) + 229674407040) // 10000
        return datetime.date.fromtimestamp(sec)

    WEEK = {}

    @staticmethod
    def week(date):
        S = JHoliday
        D = datetime.date
        T = datetime.timedelta

        year = date.year
        month = date.month
        mday = date.day

        if year < 1948:
            return [None] * 8

        wtop = D(year, month, 1).isoweekday()

        mweek = (wtop + mday - 1) // 7
        index = (year * 12 + month) * 6 + mweek
        cache = S.WEEK.get(index)
        if cache:
            return cache

        wday = date.isoweekday()
        line = [S.basic(date + T(i - wday)) for i in range(8)]
        if year >= 1973 and line[0]:
            for i in range(1, 7):
                if line[i] is None:
                    line[i] = "休日"
                    break
        if year >= 1986:
            for i in range(1, 7):
                if not line[i] and line[i - 1] and line[i + 1]:
                    line[i] = "休日"

        S.WEEK[index] = line
        return line

    @staticmethod
    def holiday(date):
        return JHoliday.week(date)[date.isoweekday()]

    @staticmethod
    def holidays(year, month=None):
        if month is None:
            month = list(range(1, 12+1))
        elif isinstance(month, int):
            month = [month]
        r = []
        for mon in sorted(month):
            mtab = JHoliday.MONTH.get(mon)
            if not mtab:
                continue
            for day in mtab:
                if not list(filter(lambda h: h.valid(year), mtab[day])):
                    continue
                if not isinstance(day, str):
                    r.append(datetime.date(year, mon, day))
                    continue
                if day == 'VE':
                    r.append(JHoliday.vernalequinox(year))
                elif day == 'AE':
                    r.append(JHoliday.autumnequinox(year))
                elif day[:1] == 'M':
                    m1 = datetime.date(year, mon, 1)
                    mday = 1 + (7 - m1.weekday()) % 7 + (int(day[1:]) - 1) * 7
                    r.append(datetime.date(year, mon, mday))
        return r


class JHDate:
    HOLIDAYS = list(JHoliday(*p) for p in (
        ('元日', 1, 1),
        ('成人の日', 1, 15, 0, 1999),
        ('成人の日', 1, 'M2', 2000),
        ('建国記念の日', 2, 11),
        ('春分の日', 3, 'VE'),
        ('憲法記念日', 5, 3),
        ('みどりの日', 5, 4, 2007),
        ('こどもの日', 5, 5),
        ('海の日', 7, 20, 1996, 2002),
        ('海の日', 7, 'M3', 2003),
        ('山の日', 8, 11, 2016),
        ('敬老の日', 9, 15, 1966, 2002),
        ('敬老の日', 9, 'M3', 2003),
        ('秋分の日', 9, 'AE'),
        ('体育の日', 10, 10, 0, 1999),
        ('体育の日', 10, 'M2', 2000, 2019),
        ('スポーツの日', 10, 'M2', 2020),
        ('文化の日', 11, 3),
        ('勤労感謝の日', 11, 23),
        # 昭和
        ('天皇誕生日', 4, 29, 0, 1988),
        ('みどりの日', 4, 29, 1989, 2006),
        ('昭和の日', 4, 29, 2007),
        # 平成
        ('天皇誕生日', 12, 23, 1989, 2018),
        # 令和
        ('天皇の即位の日', 5, 1, 2019, 2019),
        ('即位礼正殿の儀が行われる日', 10, 22, 2019, 2019),
        ('天皇誕生日', 2, 23, 2020),
    ))

    def __init__(self, *args):
        if len(args) == 1 and isinstance(args[0], datetime.date):
            date = args[0]
        elif len(args) == 3:
            date = datetime.date(*args)
        else:
            raise TypeError(*args)
        self._date = date
        self._holiday = JHoliday.holiday(date) or ''

    @property
    def min(self):
        return self._date.min

    @property
    def max(self):
        return self._date.max

    @property
    def resolution(self):
        return self._date.resolution

    @property
    def year(self):
        return self._date.year

    @property
    def month(self):
        return self._date.month

    @property
    def day(self):
        return self._date.day

    @property
    def holiday(self):
        return self._holiday

    def __repr__(self):
        return f'JHDate({self.year}, {self.month}, {self.day}, {repr(self.holiday)})'

    def __lt__(self, rhs): return self._date.__lt__(rhs._date if isinstance(rhs, JHDate) else rhs)
    def __le__(self, rhs): return self._date.__le__(rhs._date if isinstance(rhs, JHDate) else rhs)
    def __eq__(self, rhs): return self._date.__eq__(rhs._date if isinstance(rhs, JHDate) else rhs)
    def __ne__(self, rhs): return self._date.__ne__(rhs._date if isinstance(rhs, JHDate) else rhs)
    def __ge__(self, rhs): return self._date.__ge__(rhs._date if isinstance(rhs, JHDate) else rhs)
    def __gt__(self, rhs): return self._date.__gt__(rhs._date if isinstance(rhs, JHDate) else rhs)

    def __add__(self, rhs): return JHDate(self._date + rhs)
    def __sub__(self, rhs): return JHDate(self._date - rhs)

    def __format__(self, spec): return format(self._date, spec)
    def __str__(self): return str(self._date)

    def timetuple(self): return self._date.timetuple()
    def toordinal(self): return self._date.toordinal()
    def weekday(self): return self._date.weekday()
    def isoweekday(self): return self._date.isoweekday()
    def isocalendar(self): return self._date.isocalendar()
    def isoformat(self): return self._date.isoformat()
    def ctime(self): return self._date.ctime()
    def strftime(self, fmt): return self._date.strftime(fmt)

    def replace(self, year=None, month=None, day=None):
        return JHDate(self._date.replace(
            year if year is not None else self.year,
            month if month is not None else self.month,
            day if day is not None else self.day))

    @staticmethod
    def today():
        return JHDate(datetime.date.today())

    @staticmethod
    def fromtimestamp(timestamp):
        return JHDate(datetime.date.fromtimestamp(timestamp))

    @staticmethod
    def fromordinal(ordinal):
        return JHDate(datetime.date.fromordinal(ordinal))

    @staticmethod
    def fromisoformat(iso):
        return JHDate(datetime.date.fromisoformat(iso))

    @staticmethod
    def fromisocalendar(year, week, weekday):
        return JHDate(datetime.date.fromisocalendar(year, week, weekday))

    @staticmethod
    def holidays(year, month=None):
        return tuple(map(JHDate, JHoliday.holidays(year, month)))


if __name__ == '__main__':
    import argparse
    import sys

    parser = argparse.ArgumentParser()
    parser.add_argument('isodate', metavar='DATE', nargs='+', help='format: YYYY[-MM[-DD]]')

    args = parser.parse_args()

    holidays = []
    for iso in args.isodate:
        ymd = iso.split('-')
        if len(ymd) == 1:
            holidays += JHDate.holidays(int(ymd[0]))
        elif len(ymd) == 2:
            holidays += JHDate.holidays(int(ymd[0]), int(ymd[1]))
        elif len(ymd) == 3:
            holidays += [JHDate.fromisoformat(iso)]
        else:
            raise TypeError(iso)

    WEEKDAY = ('月火水木金土日')
    for date in sorted(holidays):
        holiday = date.holiday
        w = WEEKDAY[date.weekday()]
        s = f'{date.year:4}年{date.month:2}月{date.day:2}日({w})'
        print(f'{s}: {holiday}')
```

```console:実行例
$ python3 jholiday.py 2024
2024年 1月 1日(月): 元日
2024年 1月 8日(月): 成人の日
2024年 2月11日(日): 建国記念の日
2024年 2月23日(金): 天皇誕生日
2024年 3月20日(水): 春分の日
2024年 4月29日(月): 昭和の日
2024年 5月 3日(金): 憲法記念日
2024年 5月 4日(土): みどりの日
2024年 5月 5日(日): こどもの日
2024年 7月15日(月): 海の日
2024年 8月11日(日): 山の日
2024年 9月16日(月): 敬老の日
2024年 9月23日(月): 秋分の日
2024年10月14日(月): スポーツの日
2024年11月 3日(日): 文化の日
2024年11月23日(土): 勤労感謝の日
$ python3 jholiday.py 2024-04 2024-05
2024年 4月29日(月): 昭和の日
2024年 5月 3日(金): 憲法記念日
2024年 5月 4日(土): みどりの日
2024年 5月 5日(日): こどもの日
```