Pythonである日付から別の日付の間にある月の初めと終わりの日付リストを作成する方法

time-image
Image from pixabay

今回の記事では、ある年月から別の年月までの間の全ての月に対して、月の初めと終わりの日付のリストを方法を整理します。

使う場面は限られますが、私の場合は数年分のデータから月ごとのグラフを作成する際に使用しました。

コードの途中で日付の間の月の数を数える方法なども使うので、合わせてご確認いただければと思います。

概要

全体としての目標は、月の初めと終わりの日付情報のリストを作成することです。

要素となる関数は2つで、1つは2つの日付の間の月の数を数える関数、もう1つはある日付から数ヶ月後の月の初日と最終日を返す関数です。最終的には、これらの関数を使って、二つの日付の間の全ての月の初めと終わりの日付情報のリストを作成する関数を作ります。

なお、このコードはPython3でJupyter Notebook上で動作確認をしています。

コード

二つの日付の間の月の数を数える

まずは単純に二つの日付の間の月の数を数える関数を準備します。
この方法は、下記のリンクの中で言及されています。
https://stackoverflow.com/questions/4039879/best-way-to-find-the-months-between-two-dates

今回は、自分がわかりやすいように引数をアレンジしています。引数の型は、datetime.datetime型とdatetime.date型のどちらかを想定し、単純に差を返します。

import datetime
import calendar

def diff_month(start_date, end_date):
    # 引数はdatetime.datetime型かdatetime.date型を想定
    return (end_date.year - start_date.year) * 12 + end_date.month - start_date.month

動作を確認してみます。

start_date = datetime.date(2015, 12, 1)
end_date = datetime.date(2016, 3, 1)
print(diff_month(start_date, end_date))

出力結果はこちらです。

3

この例では引数にdate型を指定していますが、datetime型でも動作します。注意点としては、「間の月の数」を返すところでしょうか。後で日付情報の取得にループで使う際には1を足して利用します。

ある日付の数ヶ月後の月の初日と最終日の情報の取得

次に、ある日付と、そこから指定した月後の月における初日と最終日の情報を取得する方法です。

第一引数の日付情報はdatetime.datetime型とdatetime.date型のどちらでも良いですが、返り値はdatetime.date型のリストになっています。

後ほどdatetime.datetime型を返すことができるように拡張します。

参考にしたのはこちらのページです。
https://stackoverflow.com/questions/7015587/python-difference-of-2-datetimes-in-months

ポイントとしては、calendar.monthrange()を使っている部分でしょうか。この関数はある月の1日の曜日と日数をリストとして返すので、ここから得られる日数の情報を利用します。
(参考: https://docs.python.jp/3/library/calendar.html )

では、コードを確認しましょう。

def create_date_pair(base_date, add_months):
    c_month = base_date.month -1 + add_months
    c_year = base_date.year + c_month // 12
    c_month = c_month % 12 + 1
    last_day = calendar.monthrange(c_year, c_month)[1]
    start_date = datetime.date(c_year, c_month, 1)
    end_date = datetime.date(c_year, c_month, last_day)
    return [start_date, end_date]

次に、2つのパターンで出力の様子を確認します。

start_date = datetime.date(2015, 8, 1)
print(create_date_pair(start_date, 3))
print(create_date_pair(start_date, 6))

出力結果はこちらです。

[datetime.date(2015, 11, 1), datetime.date(2015, 11, 30)]
[datetime.date(2016, 2, 1), datetime.date(2016, 2, 29)]

想定した動作をしていました。

ある月から別の月までの各月の初日・最終日のリストの作成

ここまでで使用した2つの関数を使って、目的としていた各月の初日と最終日のリストを作成します。

def create_start_and_end_date_pairs(s_year, s_month, e_year, e_month):
    ret_list = []
    start_date = datetime.date(s_year, s_month, 1)
    end_date = datetime.date(e_year, e_month, 1)
    # 指定した最後の日時の情報も作成するため、diff_month()の結果に1を足す
    add_months = diff_month(start_date, end_date) + 1
    for i in range(add_months):
        c_pair = create_date_pair(start_date, i)
        ret_list.append(c_pair)
    return ret_list

使用例はこちら。

date_list = create_start_and_end_date_pairs(2016, 12, 2017, 3)

date_list中身は下記のようになっています。

[[datetime.date(2016, 12, 1), datetime.date(2016, 12, 31)],
[datetime.date(2017, 1, 1), datetime.date(2017, 1, 31)],
[datetime.date(2017, 2, 1), datetime.date(2017, 2, 28)],
[datetime.date(2017, 3, 1), datetime.date(2017, 3, 31)]]

ポイントはコメントに書いているように、ループのために1を追加する箇所ぐらいでしょうか。

ただ、今のままでは出力がdate型だけになっていたり、開始日と終了日が逆転した場合に対応できないので、最後にそれらの問題を解決しておきます。

最終的なコード

最終版では、出力の型をdate型、datetime型のどちらかから選べるようにし、開始日と終了日の逆転にも対応させます。

月の初日と最終日を返す関数にも変更が入るのでご注意ください。月の数を数える関数には変更はありません。

def create_date_pair_with_choice(base_date, add_months, ret_type="date"):
    c_month = base_date.month -1 + add_months
    c_year = base_date.year + c_month // 12
    c_month = c_month % 12 + 1
    last_day = calendar.monthrange(c_year, c_month)[1]
    if ret_type == "date":
        start_date = datetime.date(c_year, c_month, 1)
        end_date = datetime.date(c_year, c_month, last_day)
    elif ret_type == "datetime":
        start_date = datetime.datetime(c_year, c_month, 1)
        end_date = datetime.datetime(c_year, c_month, last_day)
    else:
        print("Choose argment 'ret_type' from 'date' or 'datetime' as string.")
        return None
    return [start_date, end_date]

def create_start_and_end_date_pairs_with_choice(s_year, s_month, e_year, e_month, ret_type="date"):
    ret_list = []
    start_date = datetime.date(s_year, s_month, 1)
    end_date = datetime.date(e_year, e_month, 1)
    add_months = diff_month(start_date, end_date)

    # 正負両方への対応
    if add_months >= 0:
        direction = 1
    else:
        direction = -1
    add_months += direction

    for i in range(0, add_months, direction):
        c_pair = create_date_pair_with_choice(start_date, i, ret_type)
        if c_pair is None:
            return None
        ret_list.append(c_pair)
    return ret_list

動作確認を2パターン行っておきます。

(1)まずは、一番シンプルなパターンで、返り値にdatetime型を指定します。

date_list = create_start_and_end_date_pairs_with_choice(2016, 12, 2017, 3, ret_type="datetime")
print(date_list)

date_listの中身は下記の通りです。

[[datetime.datetime(2016, 12, 1, 0, 0), datetime.datetime(2016, 12, 31, 0, 0)],
[datetime.datetime(2017, 1, 1, 0, 0), datetime.datetime(2017, 1, 31, 0, 0)],
[datetime.datetime(2017, 2, 1, 0, 0), datetime.datetime(2017, 2, 28, 0, 0)],
[datetime.datetime(2017, 3, 1, 0, 0), datetime.datetime(2017, 3, 31, 0, 0)]]

(2)次に、開始日と終了日を逆転させます。

date_list = create_start_and_end_date_pairs_with_choice(2017, 3, 2016, 12, ret_type="datetime")

date_listの中身は下記の通りです。日付が逆転してもうまく対応できています。必要であればdate_list.sort()を入れて、時系列的に正しくしても良いでしょう。

[[datetime.datetime(2017, 3, 1, 0, 0), datetime.datetime(2017, 3, 31, 0, 0)],
[datetime.datetime(2017, 2, 1, 0, 0), datetime.datetime(2017, 2, 28, 0, 0)],
[datetime.datetime(2017, 1, 1, 0, 0), datetime.datetime(2017, 1, 31, 0, 0)],
[datetime.datetime(2016, 12, 1, 0, 0), datetime.datetime(2016, 12, 31, 0, 0)]]

というわけで、目標とする結果が得られました。

まとめ

ある年月から別の年月までの間の全ての月に対して、月の初めと終わりの日付リストを作成する紹介しました。

使う場面はそこまで多くないかもしれませんが、月ごとのグラフを作成するなど、データフレームを月ごとに区切る際などに役に立つでしょうか。

参考資料