【python】毒島、閏秒はdatetime.datetimeで変換できないってよ！

#### 背景

先日、「閏秒（うるう秒：以下、すべて漢字表記します）」のレコードを持つデータを扱う機会があり、その際の対処について調べた結果をまとめておきます。

<font color=#CCCCCC>タイトルは、映画『桐島、部活やめるってよ』のパロディです　　Pythonなので大蛇⇒ニシキヘビ⇒毒⇒毒島...すみません...</font>

　
先人たちの知恵をお借りするなどして解決できたことを、この場をお借りして感謝するとともに、大変恐縮ですが自分のメモとしても、こちらへまとめておきます。

#### 環境
- Windows 10 Pro
- Python 3.9.0, 3.8.5
- Django 3.1.4
- PostgreSQL 13.1
- Nginx 1.95.1
- Gunicorn
- Putty 0.74


### 0.閏秒とは

[Wikipedia - 閏秒](https://ja.wikipedia.org/wiki/%E9%96%8F%E7%A7%92) より引用：
> 閏秒は、現行の協定世界時 において、世界時のUT1との差を調整するために追加もしくは削除される秒である 。この現行方式のUTCは1972年に始まった。2019年までに実施された計27回の閏秒（★注1）は、いずれも1秒追加による調整であった。 直近の閏秒の挿入は、2017年1月1日午前9時直前に行われた。

（★注1） 末尾に実施日の一覧も掲載しています。


### 1.現象 

```python
    nofrag, frag = t.split('.')
    nofrag_dt = datetime.datetime.strptime(nofrag, "%Y-%m-%dT%H:%M:%S")
    dt = nofrag_dt.replace(microsecond=int(frag))
```

```Terminal
ValueError: second must be in 0..59
```

『値エラー：秒は0～59であるべき』が発生。

↓

当然じゃん……ん！？で、フリーズ……恐る恐るデータを見てみると…
『なんということでしょう～　データを開けるとそこには、<b>`2016-12-31 23:59:60.202915`</b> というレコードが隠されていました～』<font color=#999999>~~（大改造!!劇的ビフォーアフター風に）~~</font>
という結果です。

その文字列を解析してdatetimeオブジェクトに変換するために、上記のコードを使用したところで値エラーに...


### 2.原因

[Python公式ドキュメント - time --- 時刻データへのアクセスと変換 -- time.strftime](https://docs.python.org/ja/3/library/time.html#time.strftime) では、うるう秒をサポートしていると記載あり
⇒　”値60は閏秒を表すタイムスタンプで有効で、値61は歴史的な理由からサポートされています”　との注釈入り

上述の通り、Python のドキュメントでは、`%S` は [0, 61] を受け入れるので、これは問題ではないと思いますね。
しかし、上記のタイムスタンプでこのエラーが発生します。

なんと、秒数が59を超える場合、ここでの問題は .strftime での扱いではなく、**datetime.datetime** へ変換できないことだったようです。

`%S` のドキュメントには、次のように記載あり：
 
> time モジュールとは異なり、datetime モジュールは閏秒をサポートしていません。

上述の時間文字列 <b>`"2016-12-31T23:59:60.202915"`</b> は、時刻が UTC（協定世界時）における（現時点では最後の（直近の）閏秒）であることを意味します。


### 3.対処

以下の変換論理を用いて、**datetime.datetime** で扱えるようにします。参考元コードは某所よりお借りしてきました。

【注意】：現地時間での変換では失敗する可能性があります。（例：DST（夏時間）への遷移中）

```python
import time
from calendar import timegm
from datetime import datetime, timedelta

time_string = '2016-12-31T23:59:60.202915'
time_string, dot, us = time_string.partition('.')
utc_time_tuple = time.strptime(time_string, "%Y-%m-%dT%H:%M:%S")
dt = datetime(1970, 1, 1) + timedelta(seconds=timegm(utc_time_tuple))
if dot:
    dt = dt.replace(microsecond=datetime.strptime(us, '%f').microsecond)
print(dt)
```


### 4.結果

```Terminal
2017-01-01 00:00:00.202915
```

閏秒の表記が、<b>`2017-01-01 00:00:00.202915`</b> へ変換されました。

⇒ 上記例 <b>`2016-12-31 23:59:60.202915`</b> は、JST（日本標準時）における <b>`2017-01-01 08:59:60.202915`</b> に相当します。※JST＝UTC＋9時間

---
#### 参考
- [「うるう秒」ってなに？](https://www.nao.ac.jp/faq/a0403.html)
- [1日の長さは変化しているの？](https://www.nao.ac.jp/faq/a0404.html)
- [周波数と時刻に関するＱ＆Ａ](https://jjy.nict.go.jp/QandA/FAQ/time_qa.html)
- [うるう秒について](https://www.miz.nao.ac.jp/vlbi/leapsec.html)

　

##### 閏秒の実施：
- [国立研究開発法人情報通信研究機構（NICT） - うるう秒実施日一覧](https://jjy.nict.go.jp/QandA/data/leapsec.html)
![jjy.nict.go.jp.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/902793/013819ed-9efc-b328-3075-1ae8de65cdeb.png)


---
#### (編集後記)

データ分析・解析の前処理では、例外レコードを扱う機会も多いかと思いますが、こういった歴史的にも特殊な事柄に関連する事象を、つい忘れがちです。
（00～59に当てはまらないため）例外だからエラーデータとして弾いてしまおう…とすると、正しい分析・解析に至らない可能性もあります。
何が原因なのか、このレコードが発生した背景・理由や、レコードの必要性や妥当性を、今一度考えるきっかけにしたいと思います。