Python + openpyxlを使って、月末日を除く五十日始まりのカレンダーを作成してみた

Pythonの標準モジュール calendar では、カレンダーを作るための便利な関数が用意されています。
calendar --- 一般的なカレンダーに関する関数群 — Python 3.9.4 ドキュメント

例えば、 monthcalendar で年月を指定すると週ごとに日付リストが得られるため、これを元にしたカレンダーが作りやすいです。

>>> import calendar
>>> import pprint

>>> pprint.pprint(calendar.monthcalendar(2021, 4))
[[0, 0, 0, 1, 2, 3, 4],
 [5, 6, 7, 8, 9, 10, 11],
 [12, 13, 14, 15, 16, 17, 18],
 [19, 20, 21, 22, 23, 24, 25],
 [26, 27, 28, 29, 30, 0, 0]]

 
ただ、この calendar モジュールではカレンダーの初日が1日に固定されます。

そんな中、五十日(ごとおび)始まりのカレンダーを作る機会があったため、メモを残します。

 

目次

 

環境

 

仕様

Wikipediaによると、五十日とは

五十日(ごとおび)とは、毎月5日・10日・15日・20日・25日と、30日または月末日のことである。

五十日 - Wikipedia

とのことです。

月により月末日が変わる点が厄介でしたが、今回は「月末の五十日は月初の五十日と1日しか異ならないし、月末始まりのカレンダーは使わない」ということだったので、月末の五十日は仕様対象外としました。

最終的には

  • カレンダーの初日に当たる年月日をコマンドラインから入力
  • カレンダーの罫線や週の数は不変のため、テンプレートとしてExcelファイルを用意し、そこに日付を埋めていく
  • 29日以降の日付を入力したらエラーにする
    • 月末の扱いが手間なので、一番日数が少ない2月に合わせた

という仕様としました。

なお、Excel製テンプレートはこんな感じです。

f:id:thinkAmi:20210429130754p:plain

 

プログラムの構成

大きく分けて

  • 年月日の入力
  • カレンダー用データの作成
  • Excelへの埋め込み

の3つの構成としました。

 

年月の入力

ここは普通に入力を受け取るだけです。

def input_values():
    print('開始する年を入力してください')
    yyyy = input()

    print('開始する月を入力してください')
    mm = input()

    print('開始する日を入力してください')
    dd = input()

    try:
        if int(dd) > 28:
            print('29日以降はサポート対象外です')
            return None
        return datetime.datetime(int(yyyy), int(mm), int(dd))
    except:
        print('日付ではありません')
        return None

 

カレンダー用データの作成

扱いやすそうだった、標準モジュール calendar.monthcalendar の戻り値の形式に合わせてデータを作成します。

まずは「開始日」〜「翌月の開始日-1日」を作成します。

# カレンダーの初日から最終日までの日付を作る
dates = []
while True:
    dates.append(current_date)
    current_date += datetime.timedelta(days=1)

    # ここまでで翌月の当日になっていたら、処理を終了する
    if start_at == current_date.day:
        break

# ここまでの結果 (2021/7/10始まりの場合)
# [
#   datetime.datetime(2021, 7, 10, 0, 0), ... , datetime.datetime(2021, 8, 9, 0, 0)
# ]

 
続いて、テンプレートよりカレンダーは日曜日始まりとする調整を行います。

カレンダー初日が日曜日でない場合は、標準モジュールのようにダミーの値を先頭に挿入します。今回は None を挿入しました。

# 先頭は日曜日始まり
weekday_of_first_day = dates[0].weekday()
if weekday_of_first_day != 6:  # 日曜日以外
    # 日曜日始まりでない場合、開始日の曜日以前はダミー(None)を入れておく
    for _ in range(weekday_of_first_day + 1):
        dates.insert(0, None)

# ここまでの結果
# [
#  None, None, None, None, None, None,
#  datetime.datetime(2021, 7, 10, 0, 0), ... , datetime.datetime(2021, 8, 9, 0, 0)
# ]

 
ここまででひと月分のカレンダーデータができました。

テンプレートに埋めやすくするため、カレンダーデータを週ごと(7要素ごと)に分割します。

なお、月によっては最後の週だけが7つの要素にならないことがあるため、以下の記事を参考に itertools.zip_longest を使って足りない分はNoneを埋め込みます。
リストをn個ずつのサブリストに分割 (Python) - おぎろぐはてブロ

dates_by_calendar = [item for item in itertools.zip_longest(*[iter(dates)] * 7)]

# ここまでの結果
# [
#  (None, None, None, None, None, None, datetime.datetime(2021, 7, 10, 0, 0)),
#  (datetime.datetime(2021, 7, 11, 0, 0), ... , datetime.datetime(2021, 7, 17, 0, 0)),
#  ...
#  (datetime.datetime(2021, 8, 8, 0, 0), datetime.datetime(2021, 8, 9, 0, 0), None, None, None, None, None)
# ]

 
後はこれを12ヶ月分繰り返します。

全体像はこんな感じです。

def create_calendar(current_date):
    start_at = current_date.day
    calendar = []

    for _ in range(12):
        # カレンダーの初日から最終日までの日付を作る
        dates = []
        while True:
            dates.append(current_date)
            current_date += datetime.timedelta(days=1)

            # ここまでで翌月の当日になっていたら、処理を終了する
            if start_at == current_date.day:
                break

        # 先頭を埋める
        # 先頭は日曜日始まり
        weekday_of_first_day = dates[0].weekday()
        if weekday_of_first_day != 6:  # 日曜日以外
            # 日曜日始まりでない場合、開始日の曜日以前はダミー(None)を入れておく
            for _ in range(weekday_of_first_day + 1):
                dates.insert(0, None)

        # リストを一週間ごと(7要素ごと)のリストへ分割し、最後の要素が足りない場合はNoneを入れる
        dates_by_calendar = [item for item in itertools.zip_longest(*[iter(dates)] * 7)]
        calendar.append(dates_by_calendar)

    return calendar

 

Excelへの埋め込み

openpyxlを使って埋め込みます。

Excleのセル数に合わせて細かいことをしていますが、コメント通りです。

def to_excel(calendar):
    wb = openpyxl.load_workbook('template_cal.xlsx')

    sheet = wb.copy_worksheet(wb['テンプレート'])
    sheet.title = '結果'

    plot(sheet, calendar)

    wb.save(f'cal_{datetime.datetime.now().strftime("%Y%m%d%H%M%S")}.xlsx')


def plot(sheet, calendar):
    for i, weeks_of_month in enumerate(calendar, 1):
        is_first_time = True

        mod_col = i % 3
        if mod_col == 1:  # 左端のカレンダーに入力
            pos_col = 2
        elif mod_col == 2:  # 中央のカレンダーに入力
            pos_col = 11
        else:
            pos_col = 20  # 右のカレンダーに入力

        if 1 <= i <= 3:  # 1行目のカレンダーに入力
            pos_row = 4
        elif 4 <= i <= 6:  # 2行目のカレンダーに入力
            pos_row = 13
        elif 7 <= i <= 9:  # 3行目のカレンダーに入力
            pos_row = 22
        else:
            pos_row = 31  # 4行目のカレンダーに入力

        # 一ヶ月のうちの一週間分の日付を取得する
        for row_index, current_week in enumerate(weeks_of_month):
            # 日付をセルに設定する
            for col_index, current_date in enumerate(current_week):
                if current_date:  # ダミーは印字しない
                    # そのカレンダーに初めて日付を設定する場合、タイトルも設定する
                    if is_first_time:
                        sheet.cell(row=pos_row-2, column=pos_col, value=f'{current_date.month}月')
                        is_first_time = False

                    sheet.cell(
                        row=pos_row+row_index, column=pos_col+col_index, value=current_date.day
                    )

 
これで必要な関数はできたため、 main 関数でそれぞれを呼び出せば完成です。

def main():
    input_date = input_values()
    if not input_date:
        print('終了します')
        return

    calendar = create_calendar(input_date)
    to_excel(calendar)
    print('作成しました')

 

動作確認

実際に動かしてみます。

% python run.py     
開始する年を入力してください
2021
開始する月を入力してください
7
開始する日を入力してください
10
作成しました

 
できあがりです。

f:id:thinkAmi:20210429132235p:plain

 

ソースコード

Githubに上げました。
https://github.com/thinkAmi/gotobi_calendar