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日に固定されます。
そんな中、五十日(ごとおび)始まりのカレンダーを作る機会があったため、メモを残します。
目次
環境
- Python 3.9
- openpyxl 3.0.7
仕様
Wikipediaによると、五十日とは
五十日(ごとおび)とは、毎月5日・10日・15日・20日・25日と、30日または月末日のことである。
とのことです。
月により月末日が変わる点が厄介でしたが、今回は「月末の五十日は月初の五十日と1日しか異ならないし、月末始まりのカレンダーは使わない」ということだったので、月末の五十日は仕様対象外としました。
最終的には
- カレンダーの初日に当たる年月日をコマンドラインから入力
- カレンダーの罫線や週の数は不変のため、テンプレートとしてExcelファイルを用意し、そこに日付を埋めていく
- 29日以降の日付を入力したらエラーにする
- 月末の扱いが手間なので、一番日数が少ない2月に合わせた
という仕様としました。
なお、Excel製テンプレートはこんな感じです。
プログラムの構成
大きく分けて
- 年月日の入力
- カレンダー用データの作成
- 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 作成しました
できあがりです。