2
4

休日つき datetime.date 風クラス

Last updated at Posted at 2024-02-03

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

JHDate クラス

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

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

プロパティ:
    holiday
        休日名(振替休日も対象)

クラス メソッド:
    holidays(year, month=None)
        指定された年(year)と月(month)から祝日の一覧を返す。
        振替休日は含みません。
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}')
実行例
$ 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日(日): こどもの日
2
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
4