疑念は探究の動機であり、探究の唯一の目的は信念の確定である。

数学・論理学・哲学・語学のことを書きたいと思います。どんなことでも何かコメントいただけるとうれしいです。特に、勉学のことで間違いなどあったらご指摘いただけると幸いです。 よろしくお願いします。くりぃむのラジオを聴くこととパワポケ2と日向坂46が人生の唯一の楽しみです。

第6回: Pythonのdatetimeとjpholidayを使って給料日がいつなのか表示する関数を作った

概要: 今回Pythondatetimejpholidayを使って、いつ給料日か判定する仕組み(payday関数)をつくった。datetimeは日にちにかんするモジュールであり、これはPythonの標準ライブラリの1つである。たいしてjpholidayは日本の祝日を調べるときに使うモジュールであり、pipからインストールしなければならない。この2つをインポートして給料日がいつなのか調べることができる。
まずモジュールの説明、標準ライブラリの説明、インストールの仕方を勉強する。そのあとにpayday関数の論理(ロジック)を解説する。次にdatetimejpholidayを使って「祝日でなくかつ平日である日」という条件をつくる。最後にpayday関数のプログラム全体を解説する。

#ある年の給料日がいつなのか表示するpayday関数
import datetime as dt
import jpholiday as jp
def payday(year, initial_day):
    for month in range(1, 13):
        day = initial_day
        week = {0: '(月)', 1: '(火)', 2:'(水)', 3:'(木)', 4:'(金)', 5:'(土)', 6:'(日)'}
        while(True):
            date_month = dt.date(year, month, day)
            weekday_month = date_month.weekday()
            is_holiday_month = jp.is_holiday(date_month)
            if (is_holiday_month is False) and (weekday_month != 5 ) and (weekday_month != 6):
                break                                
            day-= 1
        print(str(year) + '年' +str(month) + '月' + str(day) + '日' + week[weekday_month])



#2019年の15日振込の場合
payday(2019, 15)

"""
2019年1月15日(火)
2019年2月15日(金)
2019年3月15日(金)
2019年4月15日(月)
2019年5月15日(水)
2019年6月14日(金)
2019年7月12日(金)
2019年8月15日(木)
2019年9月13日(金)
2019年10月15日(火)
2019年11月15日(金)
2019年12月13日(金)
"""


#2019年の25日振込の場合
payday(2019, 25)

"""
2019年1月25日(金)
2019年2月25日(月)
2019年3月25日(月)
2019年4月25日(木)
2019年5月24日(金)
2019年6月25日(火)
2019年7月25日(木)
2019年8月23日(金)
2019年9月25日(水)
2019年10月25日(金)
2019年11月25日(月)
2019年12月25日(水)
"""

ここで学ぶこと

  1. モジュール、パッケージ
  2. 標準ライブラリ
  3. モジュールのインストール
  4. モジュールのインポート
  5. datetime
  6. jpholiday
  7. for
  8. while, break
  9. if
  10. 辞書型

はじめに: 給料日がいつなのか知りたい!

どうも僕です。働いている私たちにとっていちばんの楽しみは給料日ですよね。給料日は会社によって別々ですが、基本的には15日か25日だそうです。また公務員の場合は16日か17日か18日だそうです。しかし、給料日が土日や祝日の場合は前日の平日に早まります。例えば、15日付の給料日の方は今月の給料日は12日(金)になります。というのも、15日は月曜日ですが海の日で祝日なので、前の日になります。さらに14日、13日はそれぞれ日曜日、土曜日です。ですから、給料日は12日となります。とってもうれしい!!!!

このように給料日はたまに早まることがあります。そこで「今月の給料日が知りたい!」と思いましたので、今回給料日を知る関数(それをpayday関数としましょう)をつくります。この給料日関数の引数(インプット)は

  1. 年 (e.g., 2019)
  2. 給料日 (e.g., 15)

の2つとします。そしてその関数が返してくれるもの(アウトプット)は一年間の給料日です。

例えば、payday(2019, 15)とpayday(2019, 25)それぞれ入力したら、次のように出力するような関数をつくります。

#2019年の15日づけの給料日
payday(2019, 15)

2019年1月15日(火)
2019年2月15日(金)
2019年3月15日(金)
2019年4月15日(月)
2019年5月15日(水)
2019年6月14日(金) #6月15日 (土)
2019年7月12日(金) #7月15日 (月, 祝)
2019年8月15日(木)
2019年9月13日(金) #9月15日 (日)
2019年10月15日(火)
2019年11月15日(金)
2019年12月13日(金) #12月15日 (土)


#2019年の25日づけの給料日
payday(2019, 25)

2019年1月25日(金)
2019年2月25日(月)
2019年3月25日(月)
2019年4月25日(木)
2019年5月24日(金) #5月25日 (土)
2019年6月25日(火)
2019年7月25日(木)
2019年8月23日(金) #8月25日(日)
2019年9月25日(水)
2019年10月25日(金)
2019年11月25日(月)
2019年12月25日(水)

このような関数をこれから作りたいと思います。ただし、ここで8月のお盆休みについてですが、もしも15日が平日ならばその日を給料日とします。どうやら8/15はお盆休みですが、銀行は普通に営業しているとのことなので、8/15が給料日になるそうです。給料日関数paydayを作るためには、datetimeモジュールとjpholidayモジュールというのを使わなければなりませんので、次にモジュールについて説明します。

モジュールについて

モジュールとは.pyで終わっているファイルのことです。Pythonのファイルがモジュールです。そのファイルにはクラスが書かれていたり関数が書かれています。
モジュールは「道具箱」と考えたほうがいいと思います。道具箱にはドライバーやペンチなどがあるように、クラスがあったり関数があったりします。さらにモジュールの集まり(つまり.pyのファイルの集まり)をパッケージと言います。

標準ライブラリ

自分たちでモジュールを作ることができますが、しばしば使われるモジュールはすでに整備されています。例えば今回使うdatetimeモジュールです。これは日にちに関する道具箱です。
このようにすでに整備されているモジュールを標準ライブラリと言います。datetimeモジュールは標準ライブラリです。他にも数学に関するmathモジュールがあります。

モジュールのインストール

標準ライブラリ以外のモジュールを使うためには、インストールしなければなりません。例えば今回使うjpholidayモジュールは日本の祝日に関するモジュールですが、それはインストールしなければなりません。インストールする方法はターミナル上で次のように行います。

$ pip3 install jpholiday

pip3でできなければ単にpipで試してみてください。

import jpholiday

と入力してエンターボタンを押してエラーが起こらなければインストールの成功です。

モジュールのインポート

さて、モジュールを使うためには次のようにインポート(導入)しなければなりません。

import datetime
import jpholiday

これでdatetimeモジュールとjpholidayモジュールを使うことができます。次のプログラムを参考にしてください。

>>> import datetime

#今日の日付
>>> datetime.date.today()
datetime.date(2019, 7, 10)

#今日の年
>>> datetime.date.today().year
2019

#今日の月
>>> datetime.date.today().month
7

#今日の日
>>> datetime.date.today().day
10

#今日の曜日
>>> datetime.date.today().weekday()
2

曜日は月曜日を0として、火曜日を1、水曜日を2として、以下土曜日を5として、日曜日を6として表されます。数字を漢字の曜日に変換するためには、辞書型を使います。それはのちに書きます。

>>> import jpholiday
>>> import datetime
>>> today = datetime.date.today()

#今日が祝日かどうか
>>> jpholiday.is_holiday(today)  
False

#2019年7月15日は祝日かどうか (この日は海の日であるから祝日である)
>>> date = datetime.date(2019, 7, 15)
>>> jpholiday.is_holiday(date)
True


他の使い方は適当に調べてみてください。
ちなみに、datetimeやjpholidayをインポートするときに文字が長くてめんどくさいなと思われるかもしれません。そんなときはasを使って名称を省略してインポートすることができます。

import datetime as dt 
import jpholiday as jp

today = dt.date.today()
jp.is_holiday(today)  
False

payday関数のロジックについて

私たちが作りたい給料日関数の基本的な論理は次のようになります。

給料日を知るための論理(ロジック)

  1. 特定の年と日を選択する(eg., 2019, 15)。
  2. ある月の曜日を調べる。(eg., 2019年7月15日は月曜日)
  3. その月がもし、条件「「祝日でない」かつ「土曜日でない」かつ「日曜日でない」」ならば、出力される(プリントされる)。
  4. そうでないならば、日にちを1日前にする。
  5. 条件が満たされているかどうか調べる。
  6. 満たされるまで(4), (5)を繰り返す。満たされていたらその日にちが出力される。(eg., 7/15は月曜日であるが、祝日であるため条件を満たしていない。したがって、日にちを1日前にする。つまり、7/14を調べる。しかしこれは日曜日であるため、条件を満たしていない。したがって、再び、日にちを1日前にする。つまり、7/13を調べる。しかしこれは土曜日であるため、条件を満たしていない。したがって、再び、日にちを1日前にする。つまり、7/12を調べる。これは金曜日であり祝日でないため、条件を満たしている。したがって、7/12を出力する。
  7. (2)-(6)の処理を12月まで繰り返す。


条件「「祝日でない」かつ「土曜日でない」かつ「日曜日でない」」は後で考えるので、ひとまず簡単な条件「日(day)が4で割り切れる」で上のロジックを考えてみましょう。するとプログラムは次のようになります。

#給料日の基本的なロジック
def function(year, initial_day):
    for month in range(1, 13):
        day = initial_day
        while(True):
            if day % 4 == 0:
                break
            day -= 1    
        print(year, month, day)

#結果
function(2019, 15)
2019 1 12
2019 2 12
2019 3 12
2019 4 12
2019 5 12
2019 6 12
2019 7 12
2019 8 12
2019 9 12
2019 10 12
2019 11 12
2019 12 12


ここで1つ注意があります。それは引数をinitial_dayとしたときに、for文の後にday = initial_dayがあることです。そしてプリントされるのはdayのほうです。このように初期化しないと問題が発生して期待したものが手に入れられません。次のプログラムを参考にしてください。

#初期化なし。失敗例
initial_day = 15
for month in range(1, 3):
    while(True):
        print(str(month) + ' month: while: ' + str(initial_day))
        if initial_day % 4 == 0:
            break
        initial_day-= 1
    print(str(month) + ' month: for: ' + str(initial_day))   

#結果
1 month: while: 15
1 month: while: 14
1 month: while: 13
1 month: while: 12
1 month: for: 12
2 month: while: 12
2 month: for: 12


#初期化あり。成功例
initial_day = 15
for month in range(1, 3):
    day = initial_day
    while(True):
        print(str(month) + ' month: while: ' + str(day))
        if day % 4 == 0:
            break
        day-= 1
    print(str(month) + ' month: for: ' + str(day))

#結果
1 month: while: 15
1 month: while: 14
1 month: while: 13
1 month: while: 12
1 month: for: 12
2 month: while: 15
2 month: while: 14
2 month: while: 13
2 month: while: 12
2 month: for: 12


month=1のとき、initial_day = 12となります。したがって何回かループします。しかし、month = 2のとき、15から始まって欲しいところをinitial_day = 12であるために一度も回らず終わってしまいます。これは誤りであり、初期化することによって解消されます。

例えば、給料関数payday(year, initial_day)で初期化がなされていないとします。つまり、day = initial_dayがないとします。すると、7月のとき12日(金)となりますが、次の8月の場合、15日からではなく、12日が平日かどうかを調べてしまいます。すると8月12日は日曜日ですので、結果として8月の給料日が8月9日(金)となってしまいます。

条件「祝日でなくかつ平日である」を書く

次に条件「「祝日でない」かつ「土曜日でない」かつ「日曜日でない」」を考えてみましょう。これは要は「(祝日でない)平日」のことを言っています。それは次のプログラムで表されます。

#もし平日ならば「平日」を表示して、そうでなかったら「祝日か週末」を表示する。
import datetime as dt
import jpholiday as jp
year = 2019
month = 8
day = 15
date_month = dt.date(year, month, day)  #日にち
weekday_month = date_month.weekday()  #日にちの曜日
is_holiday_month = jp.is_holiday(date_month)  #日にちが祝日かどうか
if (is_holiday_month is False) and (weekday_month != 5) and (weekday_month != 6):
    print('平日')
else:
    print('祝日か週末')


#結果(2019/08/15は木曜日で平日)
平日


曜日は数字で表されます。土曜日は5であり、日曜日は6です。したがって、if文の条件は「祝日でなく」かつ「土曜日でなく」かつ「日曜日でない」となっています。
ちなみに「is_holiday_month is False」とisが使われていますが、そのへんは私自身も適当に書いています。「==」と「is」の違いがまだよくわかりませんので。もしかしたら「is_holiday_month == False」でも問題ないかもしれません。


payday関数のプログラム

最後にpayday関数のプログラムを書きます。上の2つのプログラムをつなげればできます。

#payday関数
import datetime as dt
import jpholiday as jp
def payday(year, initial_day):
    for month in range(1, 13):
        day = initial_day
        week = {0: '(月)', 1: '(火)', 2:'(水)', 3:'(木)', 4:'(金)', 5:'(土)', 6:'(日)'}
        while(True):
            date_month = dt.date(year, month, day)
            weekday_month = date_month.weekday()
            is_holiday_month = jp.is_holiday(date_month)
            if (is_holiday_month is False) and (weekday_month != 5 ) and (weekday_month != 6):
                break                                
            day-= 1
        print(str(year) + '年' +str(month) + '月' + str(day) + '日' + week[weekday_month])


ここでは数字を曜日に変換するために使用されている辞書型について簡単に説明します。
0と(月)、1と(火)と数字と曜日を一対一対応にするために、辞書型を次のように作ります。

#曜日の辞書型
week = {0: '(月)', 1: '(火)', 2:'(水)', 3:'(木)', 4:'(金)', 5:'(土)', 6:'(日)'}

#数字を取ると対応する曜日が表示される。
>>> week[0]
'(月)'

>>> week[5]
'(土)'

>>> week[6]
'(日)'


上にあるweekday_monthは曜日の数字で表示されるので、week[weekday_month]で曜日が表示されます。

これでpayday関数ができました。


最後に

このpayday関数はいくつかの修正と発展が考えられます。

改善点

いつ給料日なのかがこれでわかりました。基本的にはこのプログラムで問題ありませんがいくつか改善点があります。

  1. try exceptを書く。

payday関数の引数に数字以外のものが入るとエラーが生じます。例えば二日などです。それだけでなく例えば、02と入力してもエラーが生じます。

>>> payday(2019, 02)
  File "<stdin>", line 1
    payday(2019, 02)
                  ^
SyntaxError: invalid token


このようなエラー(例外)が生じても、大丈夫なようにtry, exceptを記入して修正したほうがいいのかもしれません。

  1. 1日や2日をちゃんと扱えるようにする。

もし1日や2日など月のはじめに入力するとエラーが生じることがあります。

>>> payday(2019, 2)
201912日(水)
201921日(金)
201931日(金)
201942日(火)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 6, in payday
ValueError: day is out of range for month


5月2日はGW期間中ですので、5月2日から日にちが下がって、1日からさらに月またぎをして4月30日....と下がらなければなりません。ですが、そのような月またぎができるようには設定していないのでこのようなエラーが生じました。現実的には給料日は基本的に15日か25日ですので問題ありません。ですが、このような任意の日に対してもできるように修正してもいいと思います。

応用編

今回は給料日の判定でしたが、これは他にも家賃の引き落とし日や水道代や光熱費などの生活費の引き落とし日にも応用できます。引き落とし日は給料日のときとは逆で、翌営業日になります。つまり例えば、もし7月15日(月)にガス料金を支払わなければならないとすれば、翌日の16日(火)となります。さらに水道代の料金は2ヶ月に一度(隔月)なので、毎月なのか隔月なのか指定できるように関数を修正してもいいかもしれません。
今回は年と日をインプットしたら、その年の給料日を表示しますが、年と月と日を入力したら、その一年間分の給料日を表示するように修正してもいいかもしれません。例えば、payday2(year, initial_month, initial_day)として、payday2(2019, 5, 15)と入力すれば、2019/5/15 (Wed), 2019/6/14 (Fri), ...., 2020/3/13 (Fri), 2020/4/15 (Wed)と出力するように書き換えたほうがいいのかもしれません。

他にも「次の給料日まであと〇〇日!!」みたいなものも、payday関数を使えばできると思います。
いろいろ試してみてください!!!


追記 2019/07/29 月末の支払い日・ある月から一年間の支払日の表示

水道代やガス代など支払日のなかには、「月末支払い」があります。そのような場合にも対応しようとするといくつかの問題点が生じます。

  • 月によって月末日が変わる。例えば、7月の場合は31日までであるが、9月の場合は30日までしかない。
  • 月末が土曜日や日曜日ならば、支払いが翌月になる。例えば、2019年8月31日は土曜日であるので、月末支払日は翌営業日の9月2日の月曜日となる。このように月またぎの問題を解決しなければならない。

また、これまで作ったpayday関数は指定された年の1月から12月までの給料日を表示するものでした。例えば、payday(2019, 15)とすると、2019年1月15日(火)、2019年2月15日(金)、......2019年11月15日(金)、2019年12月13日(金)と表示されます。そうではなく、ある月から一年間の給料日を表示するものを考えます。例えば、payday2(2019, 5, 15)と入力すれば、2019/5/15 (Wed), 2019/6/14 (Fri), ...., 2020/3/13 (Fri), 2020/4/15 (Wed)と表示するものです。
このようなpayday関数は年またぎの問題があります。


次のようなpayday関数を考えます。ある月から一年間分の給料日(または支払日)を表示するものを作ります。ただし、特に年を指定しない場合はその年のものとなります。
payday(initial_month, initial_day, year=None, interval=1, backward=True)
例えば、次の場合を考えます。

  • 給料日が毎月の25日づけであり、(今年つまり2019年の)7月25日からの一年間分の給料日を表示する。

payday(7, 25)
2019年7月25日(木)
2019年8月23日(金)
2019年9月25日(水)
2019年10月25日(金)
2019年11月25日(月)
2019年12月25日(水)
2020年1月24日(金)
2020年2月25日(火)
2020年3月25日(水)
2020年4月24日(金)
2020年5月25日(月)
2020年6月25日(木)

  • 給料日が毎月の15日づけであり、来年つまり2020年の9月15日からの一年間分の給料日を表示する。

payday(9, 15, year=2020)
2020年9月15日(火)
2020年10月15日(木)
2020年11月13日(金)
2020年12月15日(火)
2021年1月15日(金)
2021年2月15日(月)
2021年3月15日(月)
2021年4月15日(木)
2021年5月14日(金)
2021年6月15日(火)
2021年7月15日(木)
2021年8月13日(金)

  • 支払日が隔月(2ヶ月に一度)の8日で、今年(2019年)の8月8日からの一年間分の支払日を表示する。

payday(8, 8, interval=2, backward=False)
2019年8月8日(木)
2019年10月8日(火)
2019年12月9日(月)
2020年2月10日(月)
2020年4月8日(水)
2020年6月8日(月)

  • 支払日が毎月の月末であり、去年(2018年)の11月からの一年間分の支払日を表示する。

payday(11, 30, year=2018, backward=False)
2018年11月30日(金)
2018年1月2日(火)
2019年1月30日(水)
2019年2月28日(木)
2019年4月1日(月)
2019年5月7日(火)
2019年5月30日(木)
2019年7月1日(月)
2019年7月30日(火)
2019年8月30日(金)
2019年9月30日(月)
2019年10月30日(水)


このようなことを処理する関数をつくります。ただし、月末表示の場合はinitial_dayに正確な日を入力しなければなりません。そこに「月末」やその月よりも大きい数を入れるとエラーが生じます。例えば、payday(11, '月末', year=2018, backward=False)と入力すると、「TypeError: an integer is required (got type str)」というエラーが生じます。また、payday(11, 31, year=2018, backward=False)と入力すると、「ValueError: day is out of range for month」というエラーが生じます(このあたりはtry-exceptを追加すればさらにいいと思いますが、今回は省略します)。


上のような機能を持つpayday関数は以下のようにして作ります。解説は今のところ省略します。勝手に使って構いません。多分もう少し綺麗に書けると思います。

import datetime as dt
import dateutil.relativedelta as dr
import jpholiday as jp
#月末取得関数。初期設定はその年
def the_end_of_month(month, year=None):
    if not year:
        year = dt.datetime.today().year
    if month != 12:
        next_month = month + 1
    else:
        next_month = 1
    date = dt.datetime(year, next_month, 1)
    previous_day = date - dt.timedelta(days=1)
    the_last_day = previous_day.day
    return the_last_day

#payday関数
backward_and_forward = {True:-1, False:+1}
week = {0: '(月)', 1: '(火)', 2:'(水)', 3:'(木)', 4:'(金)', 5:'(土)', 6:'(日)'}
def payday(initial_month, initial_day, year=None, interval=1, backward=True):
    if not year:
        year = dt.datetime.today().year
    initial_date = dt.date(year, initial_month, initial_day)
    for i in range(0, 12, interval):
        date = initial_date + dr.relativedelta(months=i)
        year = date.year
        month = date.month
        day = initial_day
        if day < the_end_of_month(month, year=year):
            while(True):
                date_month = dt.date(year, month, day)
                weekday_month = date_month.weekday()
                is_holiday_month = jp.is_holiday(date_month)
                if (is_holiday_month is False) and (weekday_month != 5 ) and (weekday_month != 6):
                    break  
                day = (date_month + dt.timedelta(days=backward_and_forward[backward])).day
                if (day == the_end_of_month(month, year=year)) and (not backward):
                    month = (date_month + dr.relativedelta(months=1)).month
                    day = 1
            print(str(year) + '年' +str(month) + '月' + str(day) + '日' + week[weekday_month])
        else:
            day = the_end_of_month(month, year=year)
            while(True):
                date_month = dt.date(year, month, day)
                weekday_month = date_month.weekday()
                is_holiday_month = jp.is_holiday(date_month)
                if (is_holiday_month is False) and (weekday_month != 5 ) and (weekday_month != 6):
                    break
                if (day == the_end_of_month(month, year=year)) and (not backward):
                    month = (date_month + dr.relativedelta(months=backward_and_forward[backward])).month
                day = (date_month + dt.timedelta(days=backward_and_forward[backward])).day    
            print(str(year) + '年' +str(month) + '月' + str(day) + '日' + week[weekday_month])




僕から以上