Python 日付、時刻の処理

# 概要
12月もそろそろ終り、年を越します。来年は元号も変る事から、このあたりでPython の日付処理を纏めておきたいと思います。

なぜここで日付なのかと言えば、日付、時刻を扱うのは想像以上に難しく、プログラムにおいて結構バグが多いからです。

13月とか、32日とか、25時とかが出現してシステムダウンしたり処理がされないシステムは実在します。

また、曜日関連のバグもよくあり、月末が日曜日な時だけ月末処理が起動しないバグを持ったシステムなども存在しています。

年末なんて非常に危険ですね。正月にシステム停止で呼び出しを受けたくないでしょう。


その他にも日付関連で難しいのは、時刻や日付の異常表記に関してはたまに仕様の場合もあってややこしい事がある事です。25時はバグではなく、業務都合で仕様であったという事はありました。

このような場合でも、内部の時刻は正確に処理しておき、表示上だけ工夫するようするのが常道です。

この文書では、Pythonでスタンダードな日付、時刻の処理に関してまとめておきたいと思います。

# 利用するPythonのバージョンに関して

Python 3.6 以上を利用します。

それ以前のバージョンに関しては記載していませんので、ご了承ください。

# Python 標準時刻関連ライブラリ
標準ライブラリで日付、時刻を扱うライブラリは以下になります

|ライブラリ  |概要                      |
|----------|--------------------------|
|time      |Unix time を扱うモジュール  |
|datetime  |時間を扱うモジュール         |
|calendar  |日付(年/月/週)を扱うモジュール|

「time」と「datetime」の違いは、現在時刻に該当する物を取得してみるとわかります。


```python
import time
print(time.time())

import datetime
print(datetime.datetime.now())
```

```
1541898912.2347882
2018-12-03 10:15:12.236847
```


# 外部モジュール「dateutil」「pytz」

外部のモジュールとしては、以下の物をいれるのを推奨します。

 - dateutil - powerful extensions to datetime: 
https://dateutil.readthedocs.io/en/stable/
   - 標準モジュールは互換性の問題でやや扱いずらい場面があったり、不足する機能もあるため機能を補完する
 - pytz:
https://pypi.org/project/pytz/
   - タイムゾーンを扱う
   
pip でインストールしてください。

```
pip install pytz dateutil
```

# 使い方

基本的には「datetime」と「dateutil」を中心に利用していきます。

どんな時に何を利用すれば良いか記載します。

基本的には dateutil の examples ( https://dateutil.readthedocs.io/en/stable/examples.html ) に基づいています。


```python
import datetime

import dateutil
from dateutil.relativedelta import relativedelta
```

# 現在時刻の取得
現在時刻の取得は以下で可能です。

この場合「実行した環境の」現在時刻を取得します。

```python
# 年月日 時分秒 ナノtime
print(datetime.datetime.now())

# 年月日
print(datetime.date.today())
```

```
2018-12-03 12:38:45.467878
2018-12-03
```


# 日付の加算、減算

日付の加算、減算は「dateutil.relativedelta」を利用するのが簡単です。


```python
from dateutil.relativedelta import relativedelta
```


```python
today = datetime.date.today()
print(today)

print(today + relativedelta(months=+1))
```

```
2018-12-03
2019-01-03
```

加算、減算に利用できる引数は以下になります

years, months, weeks, days, hours, minutes, seconds, microseconds

years や months に「+」「-」する事で年や月の加算・減算ができます。


```python
print(today + relativedelta(years=-3, months=+1, days=+9))
```

```
2016-01-12
```


「weeks」は週の加算・減算ができます。


```python
print(today + relativedelta(weeks=+3))
```

```
2018-12-24
```

「years」や「months」の「s」が付いていないと別の意味で、その値で入れかえになります。


```python
print(today + relativedelta(year=3, day=9))
```

```
0003-12-09
```


# 月初、月末の算出

「月初」は「1」に決っていますので、「day」を「1」にすれば問題ありません。


```python
print(today + relativedelta(day=1))
```

```
2018-12-01
```

月末の日付は確定していません。

2月は特に問題で、閏年には注意が必要です。

dateutil で月末を算出する方法は複数ありますが、日付を32以上に設定すると自動で月末(その月の最大日付)に補正してくれます。


```python
print(today + relativedelta(day=99))
```

```
2018-12-31
```

もし、上記の「99」のようなマジックナンバーを利用するのが趣味に合わない場合は、翌月の月初から、1を引くという処理をするのが良いでしょう。


```python
print(today + relativedelta(months=+1, day=1, days=-1) )
```

```
2018-12-31
```

# 日付らしき文字列をdatetime型に変換

文字列のままだと日付の加算・減算はできません。

加算・減算するのは文字列をdatetime型に変換する があります。

ただし、文字列の場合はかなりパターンが多いので気をつける必要があります。

変換には「dateutil.parser」を利用します。


```python
import dateutil.parser
```

標準的な日付であれば特に気にせずに変換できます


```python
print(dateutil.parser.parse('2018/12/20'))
```

```
2018-12-20 00:00:00
```

年が2桁の場合も処理可能です


```python
print(dateutil.parser.parse('18/12/20'))
```

```
2020-12-18 00:00:00
```

日付の表記はこれ以外にも「アメリカ式」「イギリス式」等の形式があります

- 年/月/日
- 日/月/年
- 月/日/年

スラッシュ等も利用しない場合も多いです。

以下の例であれば、ほとんどの場合目的通りにパースできます。


```python
print(dateutil.parser.parse('12/20/2018'))
print(dateutil.parser.parse('20/12/2018'))
```

```
2018-12-20 00:00:00
2018-12-20 00:00:00
```

以下のような場合、「年/月/日」「月/日/年」「日/月/年」のいずれなのか確定しません。

dateutil ではデフォルトは「月/日/年」が優先変換になります。


```python
print(dateutil.parser.parse('05/01/18'))
```

```
2018-05-01 00:00:00
```

これに対処するために「yearfirst」や「dayfirst」があります。

これは日付形式の変換「優先度」を指定できます。

組合せは以下のようになります。

|yearfirst |dayfirst|形式優先度|
|----------|--------|--------|
|False     |False   |月/日/年 -> 日/月/年 -> 年/月/日
|True      |False   |年/月/日 -> 月/日/年 -> 日/月/年 
|False     |True    |日/月/年 -> 月/日/年 -> 年/月/日
|True      |True    |年/月/日 -> 日/月/年 -> 月/日/年


「2018」だったら「年」に確定ですし、「18」であれば「年」か「日」です。それらを自動認識して変換されます。


```python
print(dateutil.parser.parse('05/01/11', yearfirst=True))
print(dateutil.parser.parse('05/01/11', dayfirst=True))
print(dateutil.parser.parse('05/01/11', yearfirst=True, dayfirst=True))
```

```
2005-01-11 00:00:00
2011-01-05 00:00:00
2005-11-01 00:00:00
```

だいたいの日付は変換する事が可能です。

参考：https://dateutil.readthedocs.io/en/stable/examples.html


```python
date_lst = [
    '2018.05.29',
    '2018-05-29T12:17:27Z',
    '2018-05-29 12:17:27.133860',
    '2018-05-29 12:17:27.133860+00:00',
    '2018-05-29 12:17:27.133860+05:00',
    'May 29 2018 12:17PM',
    'May 29 2018 at 12:17PM',
    'May 29, 2018, 12:17:27',
    'Tue, 05/29/2018, 12:17PM',
    'Tue, 29 May, 2018',
    'Tuesday, 29th May, 2018 at 12:17pm'
]

for val in date_lst:
    print(dateutil.parser.parse(val))
```

```
2018-05-29 00:00:00
2018-05-29 12:17:27+00:00
2018-05-29 12:17:27.133860
2018-05-29 12:17:27.133860+00:00
2018-05-29 12:17:27.133860+05:00
2018-05-29 12:17:00
2018-05-29 12:17:00
2018-05-29 12:17:27
2018-05-29 12:17:00
2018-05-29 00:00:00
2018-05-29 12:17:00
```

# datetime型を文字列に変換

datetime型を目的の文字列に変換する必要も頻繁にあります。

以下のように変換します。


```python
print(f'{datetime.datetime.now(): %Y-%m-%d}')

today = datetime.datetime.now()

print(f'{today: %Y-%m-%d}')
```

```
2018-12-03
2018-12-03
```

「f」は「f-strings」を示し、文字列をフォーマットします。Python 3.6以上で利用できる機能です。

「{}」で囲まれた範囲をフォーマット対象にします。

「:」の前に処理したい対象、「:」の後ろにディレクティブを指定します。

処理対象は関数や変数が利用できます。

「%Y」や「%m」が日付フォーマット用のディレクティブになります。

利用できるディレクティブで主な物を以下に示します。特に記載無い限り数字は0埋めになります。

|ディレクティブ|説明|
|-----------|----|
|%%|%を表示|
|%Y|4桁の西暦年|
|%y|2桁の西暦年|
|%m|2桁の月|
|%d|2桁の日|
|%H|24時間表示の時間|
|%I|12時間表示の時間|
|%M|分|
|%S|秒|
|%f|マイクロ秒|


全てを確認する場合は、Python datetimeドキュメントをご確認ください。

https://docs.python.jp/3/library/datetime.html#strftime-and-strptime-behavior

# timezone の処理

中国、アメリカ等は当然のように日本とは時間が異なります。

WEBサービス等は世界全てに提供されますので、timezoneの処理は非常に重要です。

「朝9時」と記載して「夜中2時」になってしまい、クレームになったり、裁判になるとかは避けたい所です。

Python で timezone を正しく利用するには「pytz」ライブラリをインストールしてください。

```
pip install pytz
```


```python
u = dateutil.tz.tzutc()
print(datetime.datetime.now(u))

u = dateutil.tz.gettz('US/Eastern')
print(datetime.datetime.now(u))
```

```
2018-12-04 02:05:22.300167+00:00
2018-12-03 21:05:22.300508-05:00
```

利用できるtimezoneは英語版wikipediaを参照するのが手軽です。

https://en.wikipedia.org/wiki/List_of_tz_database_time_zones

任意の日付で生成したい場合は以下のようにします。

ここではUS/Eastern での時間を指定してみます。


```python
t_a = datetime.datetime(2018, 8, 1, 13, 14, 43, tzinfo=u)
print(t_a)

t_b = datetime.datetime(2018, 12, 24, 13, 14, 43, tzinfo=u)
print(t_b)
```

```
2018-08-01 13:14:43-04:00
2018-12-24 13:14:43-05:00
```

ここで日付を二つ生成しているのは、アメリカには「夏時間」が存在していて、注意が必要なのを確認するためです。

日本時間で表示する場合、夏時間時は13時間、通常は14時間のずれになり、差が異なります。

ただしく変換されているか注意してください。


```python
print(t_a.astimezone())
print(f'{t_a.astimezone():%Y-%m-%d %H:%M:%S}')

print(t_b.astimezone())
print(f'{t_b.astimezone():%Y-%m-%d %H:%M:%S}')
```

```
2018-08-02 02:14:43+09:00
2018-08-02 02:14:43
2018-12-25 03:14:43+09:00
2018-12-25 03:14:43
```

2018年のアメリカの夏時間は「3/11 2:00」から「11/4 2:00」です。

日曜日に開始して、日曜日に終了ですので、年によって開始終了日付が異なりますので、注意が必要です。

以下の例をみると、夏時間を取り扱うのがどれほど難しいかが理解できるかと思います。

```python
t = datetime.datetime(2018, 3, 11, 1, 59, 59, tzinfo=u)
print(t)
print(f'{t.astimezone():%Y-%m-%d %H:%M:%S}')
t = datetime.datetime(2018, 3, 11, 2, 0, 0, tzinfo=u)
print(t)
print(f'{t.astimezone():%Y-%m-%d %H:%M:%S}')
```

```
2018-03-11 01:59:59-05:00
2018-03-11 15:59:59
2018-03-11 02:00:00-04:00
2018-03-11 15:00:00
```

# UTC、JSTについて

timezone を利用していると出てくる「UTC」や「JST」に関して知る必要があります。

UTC は「Coordinated Universal Time」の略で、「協定世界時」の意味で、協定で定められた基準時刻の事です。

日本の時刻は「UTC」から「+9」時間となりますので

「UTC+0900」と表記し、これを「JST」(日本標準時)と表記します。

timezone を実際に日本(Asia/Tokyo)にして実行してみると、この表現を見る事ができます。

```python
print(datetime.datetime.utcnow())

print(datetime.datetime.now())

u = dateutil.tz.gettz('Asia/Tokyo')
print(datetime.datetime.now(u))
```

```
2018-12-03 04:37:40.450556
2018-12-03 13:37:40.450824
2018-12-03 13:37:40.451025+09:00
```

この事から理解していただきたいのは、「現在時刻」と安易に表記してはいけない、という事です。

「now()」で取得できる時刻は「実行環境」の時刻になります。

実行環境のtimezoneは以下のようにすると取得できる場合が多いです。

(注：Python3.6時点ではバグがあるため、環境によっては取れない場合があります)

```python
import time
time.tzname
```

```
('JST', 'JST')
```

# TODO 時間がなく記載できなかった事項
以下の事項は後で更新するかもしれません。

- データベースでの時間の扱い
- OS毎のtimezone設定補足
- UTC から時刻の変換
- 24時,25時等の表記に関して

# まとめ

日付はかなり神経質に取り扱うべき事項です。

WEBサービスなどを作成する時は、世界に公開している事を十分に考慮した上で作成する必要があります。

この記事が少しでも皆様のお役に立てると幸いです。

# 参考サイト
 * https://docs.python.jp/3/library/datetime.html
 * https://dateutil.readthedocs.io/en/stable/index.html

# 書いた人

[Tech Fun株式会社](http://www.techfun.co.jp/)スペシャリスト、xza です。

社内で開催した初学者向け勉強会で利用した資料等を公開しています。