Pythonで日付操作を完全マスター！！

## 概要

プログラミングの中で、日付の扱いは非常に重要です。
皆さんも一度は「UTC?JST?何がどう違うのー？」と悩まれたことがあるのではないでしょうか。
私も新入社員の時に大変混乱しました。
その時はJavaScriptでしたが、現在はPythonよく使用しますので、Pythonにおける日付の操作方法についてまとめます。
※Python3.6以上を前提としています

## JSTとUTCとは？

実際のプログラムの中身に入る前に、JSTとUTCについてザックリ説明します。
JST(日本標準時)とは、**日本の現地時間のこと**です。
UTC(協定世界時)とは、**イギリス(ロンドン)の現地時間のこと**です。

JSTやUTCは特にプログラミングとは関係なく、どの国の時間なのか、を表す表記になります。
例えば、「JSTでお昼の12時」というと日本の現地時間でお昼の12時という意味なので、UTC(イギリスの現地時間)では朝の3時となります。(時差9時間で計算)

世界の表記の一覧は下記で確認することができます。
[世界のタイムゾーンと主要都市の時差を計算](https://www.jisakeisan.com/#list)

## ISO8601フォーマットって？

ISO8601フォーマットは、日付の表記フォーマットの一つです。
こちらはプログラミングに関係してきます。

「`yyyy-MM-ddTHH:mm:ss`」のような形で日時を表現します。
JST(日本標準時)の場合：
2019-09-08T13:02:40+09:00

見方としては、まず「T」よりも左が日付で、右が時刻となります。
最後の「+09:00」はタイムゾーンを表しており、「UTC+9時間」という意味になります。
つまり、2019-09-08T13:02:40はUTC時間から+9時間された時間である、ということになります。
(2019-09-08T13:02:40に+9時間するのではなく、すでに+9時間された値であるということに注意)

UTC(協定世界時)の場合：
2019-09-08T04:02:40Z

UTCも基本的には同じですが、+0の部分を特別にZとして表記します。

## datetime

Pythonで日付を扱う場合、基本的にはdatetimeモジュールを使用します。
Pythonに標準で搭載されているので、別途インストールは必要ありません。

datetimeモジュールには下記の型が含まれます。

* date
* time
* datetime
* timedelta
* tzinfo
* timezone

ここで混乱を招く要素として、datetimeモジュールの中にdatetimeクラスが存在する、ということです。ですので、使用する場合は、下記のようにインポートする必要があります。

```python
from datetime import datetime
```

では、実際にそれぞれの使い方を見ていきましょう。

### datetime.date

時刻が必要なく、日付のみ必要な場合に利用します。
詳細は[date オブジェクト](https://docs.python.org/ja/3/library/datetime.html#date-objects)を参照ください。

現在の日付を取得したい場合は、下記のようにします。

```python
from datetime import date

print(date.today()) #2019-09-08
print(type(date.today())) #<class 'datetime.date'>
```

年、月、日を別々で取得したい場合は、下記のようにします。

```python

from datetime import date

today = date.today()
print('year: {}, month: {}, day: {}'.format(today.year, today.month, today.day))
```

曜日を取得したい場合は、下記二つのメソッドが利用できます。

* date.weekday()
* date.isoweekday()

注意点として、`date.weekday()`は月曜日を0、金曜日を6として扱うのに対して、`date.isoweekday()`は月曜日を1、金曜日を7として扱います。

```python
from datetime import date

WEEKDAY = ['月', '火', '水', '木', '金', '土', '日']

today = date.today()
print('本日は{}曜日です'.format(WEEKDAY[today.weekday()]))
print('本日は{}曜日です'.format(WEEKDAY[today.isoweekday()-1]))
```

datetime.dateのオブジェクトを作成したい場合は、下記のようにします。

```python
from datetime import date

# コンストラクタで作成する場合
past_date = date(2018, 1, 1)
print(past_date) #2018-01-01
print(type(past_date)) #<class 'datetime.date'>

# isoフォーマット文字列から作成する場合
past_date = date.fromisoformat('2018-02-01')
print(past_date) #2018-02-01
print(type(past_date)) #<class 'datetime.date'>
```

文字列として任意の形式で出力したい場合は、下記のようにします。
strftimeのプレースフォルダーについては、[strftime() と strptime() の振る舞い](https://docs.python.org/ja/3/library/datetime.html#strftime-strptime-behavior)を参照してください。
ちなみに、`%-d`のようにマイナスをつけるとゼロ埋めをせずに表示することが可能です。

```python
from datetime import date

WEEKDAY = ['月', '火', '水', '木', '金', '土', '日']

past_date = date(2018, 1, 1)
# isoフォーマットで出力する場合
print(past_date.isoformat()) #2018-01-01
# 任意の形式で出力する場合
# 2018年1月1日月曜日
print(past_date.strftime('%Y年%-m月%-d日{}曜日'.format(WEEKDAY[past_date.weekday()])))
```

datetime.dateオブジェクト同士の比較も可能です。
例えば、どちらの日付の方が新しいのか、などを比較できます。

```python
from datetime import date

date1 = date(2018, 1, 1)
date2 = date.fromisoformat('2018-02-01')

if date1 < date2:
    print('{}の方が{}よりも日付が古いです'.format(date1, date2))
elif date1 == date2:
    print('二つは同じ日付です')
else:
    print('{}の方が{}よりも日付が古いです'.format(date2, date1))
```

### datetime.time

日付が必要なく、時刻のみ必要な場合に利用します。
詳細は[timeオブジェクト](https://docs.python.org/ja/3/library/datetime.html#time-objects)を参照ください。

**現在の時刻を`datetime.time`を使用して取得することはできません。**
現在時刻を取得するには`time.time()`か`datetime.datetime.now()`（後述）のどちらかを利用する必要があります。
datetime.time単体では非常に使いにくいため、基本的にはdatetime.datetimeと一緒に使います。

```python
from datetime import datetime, time

# 現在時刻を取得する(datetime.datetimeについては後述)
now = datetime.now().time()
print(now) # 13:02:14.321305
print(type(now)) # <class 'datetime.time'>
```

時間、分、秒について取得するには下記のようにします。

```python
from datetime import datetime, time

# 現在時刻を取得する(datetime.datetimeについては後述)
now = datetime.now().time()
print('現在時刻は{}時{}分{}.{}秒です'.format(now.hour, now.minute, now.second, now.microsecond)) 
```

文字列として任意の形式で出力したい場合は、下記のようにします。

```python
from datetime import datetime, time

# 現在時刻を取得する
now = datetime.now().time()
# isoフォーマットで出力(マイクロ秒あり)
print(now.isoformat()) # 13:17:17.342040
# isoフォーマットで出力(マイクロ秒なし)
print(now.isoformat(timespec='seconds')) # 13:17:17

# 任意の形式で出力
# 13時17分17.342040秒
print(now.strftime('%-H時%-M分%-S.%f秒'))
```

個人的には、時間をマイクロ秒まで含めて扱うのは、よっぽどの精度が求められる場合を除き、避ける方がいいかなと思っています。
一番の理由は、マイクロ秒の桁数が場合によって異なることを避けるためにです。

datetime.timeもオブジェクト同士で比較することが可能です。

```python
from datetime import time

time1 = time(hour=14, minute=24, second=12)
time2 = time(hour=18, minute=4, second=6)

if time1 < time2:
    print('{}の方が{}よりも時間が早いです'.format(time1, time2))
elif time1 == time2:
    print('同じ時刻です')
else:
    print('{}の方が{}よりも時間が早いです'.format(time2, time1))
```

### datetime.datetime

一番よく使うクラスかと思います。
日付(datetime.date)と時刻(datetime.time)の両方を含んだクラスとなります。
詳細は[datetimeオブジェクト](https://docs.python.org/ja/3/library/datetime.html#datetime-objects)を参照ください。

現在の時間を取得するには下記のようにします。

```python
from datetime import datetime

# ローカルの現在時刻を取得する(日本現地時間)
now = datetime.today()
print(now)

# UTC現在時刻を取得する(協定世界時、イギリスの現地時間)
utc_now = datetime.utcnow()
print(utc_now)
```

特定の形式からdatetime.datetimeに変換する場合は、下記のようにします。

```python
from datetime import datetime

# Unixタイムスタンプから生成する
unix_time = datetime.fromtimestamp(1567919335)
print(unix_time) # 2019-09-08 14:08:55
print(type(unix_time)) # <class 'datetime.datetime'>

# isoフォーマット時間から生成
iso_time = datetime.fromisoformat('2019-09-08T14:32:01+09:00')
print(iso_time) # 2019-09-08 14:32:01+09:00
print(type(iso_time)) # <class 'datetime.datetime'>
print(iso_time.tzinfo) # UTC+09:00

# 任意の形式から生成
custome_time = datetime.strptime('2019年9月8日 14時38分1秒', '%Y年%m月%d日 %H時%M分%S秒')
print(custome_time) # 2019-09-08 14:38:01
print(type(custome_time)) # <class 'datetime.datetime'>
```

datetime.strptimeについて少し補足します。
第1引数に変換元の文字列を、第2引数にテンプレートを指定します。
第2引数で指定したテンプレートをもとに、第1引数の日時をパースするので、正常にパースできない場合はエラーとなります。
テンプレートで利用できるプレースホルダーは[strftime() と strptime() の振る舞い](https://docs.python.org/ja/3/library/datetime.html#strftime-strptime-behavior)を参照ください。


前述したdatetime.dateとdatetime.timeに変換する場合は、下記のようにします。

```python
from datetime import datetime

# 現在時間を取得する
now = datetime.today()

# datetime.dateに変換
now_date = now.date() 
print(now_date) # 2019-09-08
print(type(now_date)) # <class 'datetime.date'>

# datetime.timeに変換
now_time = now.time()
print(now_time) # 14:52:30.292221
print(type(now_time)) # <class 'datetime.time'>
```

文字列として任意の形式で出力したい場合は、下記のようにします。

```python
from datetime import datetime

# 現在時間を取得する
now = datetime.today()

# isoフォーマットで出力(マイクロ秒あり)
print(now.isoformat()) # 2019-09-08T14:57:36.896772
# isoフォーマットで出力(マイクロ秒なし)
print(now.isoformat(timespec='seconds')) # 2019-09-08T14:57:36

# 任意の形式で出力
# 2019年9月8日 14時57分36.896772秒
print(now.strftime('%Y年%-m月%-d日 %-H時%-M分%-S.%f秒'))
```

datetime.datetimeもオブジェクト同士で比較することが可能です。

```python
from datetime import datetime

date_time1 = datetime(2019, 9, 8, hour=14, minute=20, second=10)
date_time2 = datetime(2019, 9, 8, hour=17, minute=0, second=30)

if date_time1 < date_time2:
    print('{}の方が{}よりも日時が早いです'.format(date_time1, date_time2))
elif date_time1 == date_time2:
    print('同じ日時です')
else:
    print('{}の方が{}よりも日時が早いです'.format(date_time2, date_time1))
```


### datetime.timedelta

日時や時間をずらす際に利用します。
例えば、1時間遅くする、1日進めるなどの操作が可能です。
datetime.date、datetime.time、datetime.datetimeのそれぞれに適用可能です。
詳細は[timedeltaオブジェクト](https://docs.python.org/ja/3/library/datetime.html#timedelta-objects)を参照ください。

実際の使い方は下記となります。

```python
from datetime import datetime, timedelta

# 現在時間を取得する
now = datetime.today()
print(now) # 2019-09-08 17:18:38.256536

# 日にちを1日進める
print(now + timedelta(days=1)) # 2019-09-09 17:18:38.256536
# 日にちを1日遅らせる
print(now - timedelta(days=1)) # 2019-09-07 17:18:38.256536

# 1時間進める
print(now + timedelta(hours=1)) # 2019-09-08 18:18:38.256536
```

調整可能なパラメーターは以下です。

* days
* seconds
* microseconds
* milliseconds
* minutes
* hours
* weeks

月単位の計算、年単位の計算は後述するdatetutilを使用して計算が可能です。


### datetime.tzinfo

タイムゾーンについての抽象クラスであり、このクラス単体での利用はできません。
後述するdatetime.timezoneをメインで利用するため、詳細な説明は省力します。
詳細を知りたい場合は、[tzinfoオブジェクト](https://docs.python.org/ja/3/library/datetime.html#tzinfo-objects)を参照ください。

### datetime.timezone

datetimeはデフォルトではタイムゾーンの情報を持たないのですが、持たせることも可能です。
例えば、グローバルなサービスの場合、時刻だけの情報だと、その時刻がどの現地時間なのかが分からなくなるので、システムが正常に動作しなくなる可能性があるので、タイムゾーンを持たせる必要があります。
また、タイムゾーンを持たせることで、現地時間をUTCに変換する作業も、簡単に行うことができます。

```python
from datetime import datetime, timedelta, timezone

JST = timezone(timedelta(hours=9))

# 現在時刻を取得する(日本標準時JST)
now = datetime.now(tz=JST)

print(now) # 2019-09-08 19:21:24.538441+09:00
print(now.isoformat()) # 2019-09-08T19:21:24.538441+09:00
print(now.strftime('%Y年%-m月%-d日 %-H時%-M分%-S秒')) # 2019年9月8日 19時21分24秒

# UTCに変換する
# 2019年9月8日 10時21分24秒
print(now.astimezone(timezone.utc).strftime('%Y年%-m月%-d日 %-H時%-M分%-S秒'))
```

また、既存のdatetimeのタイムゾーンを置き換える、タイムゾーンを変更した新規datetimeを作成する場合は、replaceやastimezoneを使うことで実現できます。

```python
# 下記コードは修正途中です。。。
custome_time = datetime.strptime('2019年9月8日 14時38分1秒', '%Y年%m月%d日 %H時%M分%S秒')
print(custome_time.tzinfo)
```

## dateutil

datetimeだけでも十分な機能があるのですが、さらに便利に使うために、dateutilを紹介します。
dateutilは標準ライブラリではないので、別途インストールする必要があります。
(ちなみに、AWS Lambda Python環境にはデフォルトでインストールされています)

### 任意の文字列からdatetimeオブジェクトを簡単に生成したい

datetimeの場合は、下記のようにstrptime関数の第2引数にテンプレートを指定する必要があります。
このテンプレートにマッチしない文字列の場合は、エラーとなってしまいます。

```python
custome_time = datetime.strptime('2019年9月8日 14時38分1秒', '%Y年%m月%d日 %H時%M分%S秒')
```

もっと簡単に文字列からdatetimeオブジェクトを生成するには、下記のようにdateutil.parserを使用します。
ある程度の制限はありますが、基本的な日時フォーマットであれば、ある程度の表記揺れは自動的にカバーしてくれます。
リファレンスは[こちら](https://dateutil.readthedocs.io/en/stable/parser.html)

```python
from dateutil import parser

date1 = '2019-09-09 19:35:12'
date2 = '2019-9-9 8:2:6'
date3 = '2019年9月9日 18時20分13秒'
date4 = '2019/9/9 14:2:51'
date5 = 'Today is January 1, 2047 at 8:21:00AM'

date1_parsed = parser.parse(date1)
print(date1_parsed) # 2019-09-09 19:35:12
print(type(date1_parsed)) # <class 'datetime.datetime'>

date2_parsed = parser.parse(date2)
print(date2_parsed) # 2019-09-09 08:02:06
print(type(date2_parsed)) # <class 'datetime.datetime'>

# 日本語を含む場合はエラーになる
# date3_parsed = parser.parse(date3)

date4_parsed = parser.parse(date4)
print(date4_parsed) # 2019-09-09 14:02:51
print(type(date4_parsed)) # <class 'datetime.datetime'>

date5_parsed = parser.parse(date5, fuzzy_with_tokens=True)
print(date5_parsed) # (datetime.datetime(2047, 1, 1, 8, 21), ('Today is ', ' ', ' ', 'at '))
print(type(date5_parsed[0])) # <class 'datetime.datetime'>
```

最後の例のようにfuzzy_with_tokensのパラメータをTrueに設定すると、英語の場合はある程度のあいまいさを自動的に判断してくれます。
(プログラム的には少し危険ですが、、)

### 月、年単位で日時を操作したい

datetime.timedeltaでは、月、年単位での操作はできません。
自分で計算するのは大変なので、dateutil.relativedeltaを使います。
リファレンスは[こちら](https://dateutil.readthedocs.io/en/stable/relativedelta.html)
dateutil.relativedeltaで操作できるパラメータは、「years, months, weeks, days, hours, minutes, seconds, microseconds」の8つ。

```python
from dateutil import parser, relativedelta

date1 = '2019-09-09 19:35:12'

date1_parsed = parser.parse(date1)
# 1月加算
# 2019-10-09 19:35:12
print(date1_parsed + relativedelta.relativedelta(months=1))
# 1月減算
# 2019-08-09 19:35:12
print(date1_parsed - relativedelta.relativedelta(months=1))
# 1年加算
# 2020-09-09 19:35:12
print(date1_parsed + relativedelta.relativedelta(years=1))
# 1年減算
# 2018-09-09 19:35:12
print(date1_parsed - relativedelta.relativedelta(years=1))
```

## 時間を丸めたい

時間を5分や10分で丸めたいということはあるかと思います。
その場合は、下記のようにすれば分単位で丸めることが可能です。

```python
from datetime import datetime

INTERVAL_MINUTE = 10

time = custome_time = datetime.strptime('2019年9月8日 14時59分1秒', '%Y年%m月%d日 %H時%M分%S秒')
tmp = time.replace(minute=time.minute - time.minute % INTERVAL_MINUTE, second=0, microsecond=0)
print(time.isoformat()) # 2019-09-08T14:59:01
print(tmp.isoformat()) # 2019-09-08T14:50:00
```

## 最後に

タイムゾーン、日時操作はどのプログラミング言語でも重要な操作となります。
少しでも皆さんのお役に立てれば幸いです。