Python で和暦変換を行う

# はじめに

Python の和暦計算で困っていませんか？
+ 和暦表示の基準日に対して1週間後の日付を計算して出力したい、でも和暦表示に対する計算方法が分からない。
+ 和暦表示の文字列を出力したい。そのためには日付に応じた元号を計算しないといけない。

この問題で難しいのは、入力日付も出力日付も表記が状況によって異なることだと思います。
例えば、利用する場面によって次のような様々な表記が考えられます。
- 令和1年5月1日
- S1.5.1
- S010501 

他にも、年だけ表記する場合もあれば、時刻までの情報が必要な場合もあるかもしれません。
このような様々な入出力日付の書式に対し、日付処理を毎回実装するのは大変です。
ということで、私がこの問題に取り組んだ際の結果を共有したいと思います。

本記事の要点は次の2点です。
+ [汎用的な和暦変換モジュール](#汎用的な和暦変換モジュール) では和暦変換ライブラリを紹介します。
+ [実装方法](#実装方法) では和暦変換の実装方法を紹介します。

# Python の日付処理

まずは和暦を含まない日付文字列の操作を考えます。
西暦の場合は Python の標準モジュール datetime に含まれる datetime クラスが便利です。

datetime クラスを使うと次のような操作が可能です。
+ 日付文字列を解析して datetime オブジェクト化する。 
+ 様々な日付計算を行う。
+ datetime オブジェクトを日付文字列に変換する。

それぞれの操作の具体的な方法として次のようなものがあります。
+ `strptime()` 関数を用いると、解析したい日付文字とその書式から解析してオブジェクトとして取得できます。
+ timedelta オブジェクトを加算することで、１日後や１週間後といった計算を行うことができます。また、`replace()` 関数を用いて、年や月など値を指定して変更することができます。
+ `strftime()` 関数を用いると、datetime オブジェクトを指定した書式の文字列に変更することができます。

例えば、次のように日付文字列を操作することができます。

``` python
# datetime パッケージをインポート
import datetime

# 日付文字列を解析して datetime オブジェクト化する
date_obj = datetime.datetime.strptime('2019年5月1日', '%Y年%m月%d日')

# datetime オブジェクトの１週間後の日付を計算する
date_obj = date_obj + datetime.timedelta(weeks=1)

# datetime オブジェクトの月を１月に変更する
date_obj = date_obj.replace(month=1)

# datetime オブジェクトを日付文字列に変換する
result = date_obj.strftime('%Y%m%d')

# 結果を確認する
print(result)   # -> '20190108'
```

この例で指定している日付文字の書式 `%Y年%m月%d日` や `%Y%m%d` を変えることで、様々な書式の入出力に対応することができます。


書式の指定方法は公式ドキュメント [strftime() と strptime() の書式コード](https://docs.python.org/ja/3/library/datetime.html#strftime-and-strptime-format-codes) に記載されています。
次の表は、上の例で使っている部分を抜粋したものです。
| ディレクティブ | 意味 | 使用例 |
| --- | ----------- | ------- |
| `%d` | 0埋めした10進数で表記した月中の日にち。 | 01, 02, ..., 31 |
| `%m` | 0埋めした10進数で表記した月。 | 01, 02, ..., 12 |
| `%Y` | 西暦 ( 4桁) の 10 進表記を表します。 | 0001, 0002, ..., 2013, 2014, ..., 9998, 9999 |


# 和暦処理の課題

西暦の場合は標準モジュール datetime が便利であることを見てきましたが、残念ながら datetime モジュールは和暦に対応していません。
和暦を扱うには、外部モジュールを探すか自分でモジュールを作成する必要がありそうです。
ここでは、自分でモジュールを作成する場合の課題について見ていきます。

## 和暦変換のシンプルな実装例

自分で和暦モジュールを作成する場合には、次のような処理を書くことになると思います。

``` python
import datetime

# 和暦情報
ERA_DICT = {
    '令和': datetime.datetime(2019, 5, 1),
    '平成': datetime.datetime(1989, 1, 8),
    '昭和': datetime.datetime(1926, 12, 25)
}

# 西暦→和暦変換関数
def to_wareki(date_obj):
    if ERA_DICT['令和'] <= date_obj:
        era_start = ERA_DICT['令和']
        era = '令和'
    elif ERA_DICT['平成'] <= date_obj:
        era_start = ERA_DICT['平成']
        era = '平成'
    elif ERA_DICT['昭和'] <= date_obj:
        era_start = ERA_DICT['昭和']
        era = '昭和'
    jp_year = date_obj.year - era_start.year + 1
    return f'{era}{jp_year}年{date_obj.month}月{date_obj.day}日'

# 和暦→西暦変換関数
def to_seireki(era, year):
    era_start = ERA_DICT[era]
    jp_year = era_start.year + year - 1
    return str(jp_year) + '年'

# テスト実行
if __name__ == '__main__':
    print(to_wareki(datetime.datetime(2019, 4, 30))) # -> 平成31年4月30日
    print(to_wareki(datetime.datetime(2019, 5, 1))) # -> 令和1年5月1日
    print(to_seireki('平成', 31)) # -> 2019年
    print(to_seireki('令和', 1)) # -> 2019年
```

この例では、`to_wareki()` 関数は datetime オブジェクトを受取って '元号Y年M年D日' という書式の文字列を返します。
また、`to_seireki()` 関数は元号の文字列と年数を受取って西暦の年数を 'Y年' という書式の文字列で返します。

このようなシンプルな関数でも、必要に応じて調整すれば問題なく使えるケースが多いと思います。

## シンプルな実装での課題
先ほどのシンプルな実装で問題となるのは、出力の書式が固定となっている点です。
つまり、`to_wareki()` 関数では '元号Y年M年D日' という書式、`to_seireki()` 関数では 'Y年' という書式で固定となっています。
しかし実際に利用する書式は、カンマ区切りやスラッシュ区切りなどの書式、もしくは年月までの書式など状況によって様々だと思います。
さらに '令和1年' は '令和元年' と表記したいというように、元年への対応が必要な状況も考えられます。

また、和暦から西暦への変換関数の入力には元号の文字列と年数を渡す必要があり、こちらも状況によっては扱いにくいのではないかと思います。

これらの様々な書式に対してシンプルな実装で済ませる場合、似たような関数を複製することになってしまうことが多いです。
しかし、似たような関数を作成するのは [DRY 原則](https://ja.wikipedia.org/wiki/Don%27t_repeat_yourself) の観点から嬉しくありません。
そこで入出力の書式に工夫を行い、汎用的な和暦変換モジュールが欲しいと考えました。



# 設計

ここでは和暦対応の入出力書式を工夫していきます。
まずは上記 [Python の日付処理](#python-の日付処理) で確認した標準モジュール datetime の datetime クラスを考えます。

繰り返しになりますが、datetime クラスを使うと次のような操作が可能でした。
+ 日付文字列を解析して datetime オブジェクト化する。 
+ datetime オブジェクトを日付文字列に変換する。

日付文字列の解析を行うのが `strptime()` 関数、 datetime オブジェクトから日付文字列を出力するのが `strftime()` 関数です。
これを図にすると次のようになります。

![datetime.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3452983/e5eff731-a6d3-7fc5-06ee-5f3388f3e71d.jpeg)


この構造に倣って、和暦文字列に対しても次のような操作ができると便利ではないでしょうか。
+ 和暦文字列を解析してオブジェクトに変換する。
+ オブジェクトを和暦文字列に変換する。

![datetime_function.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3452983/1a1ec62c-b0ea-b1a9-c632-d5a23f59f517.jpeg)

図中の「作る」と記載の操作を行う関数が欲しいというイメージです。


# 汎用的な和暦変換モジュール


今回作成した汎用的な和暦変換モジュール datetimejp の利用方法を紹介します。
実行するには pip コマンドでインストールしておく必要があります。

```
pip install datetimejp
```

実装方法は次章で説明しますので、自身で実装する場合はインストールは不要です。
サンプルコードとして眺めてください。

## 概要

今回は datetime クラスを継承することで、和暦変換クラス JDatetime を作成しました。
`strptime()` 関数と `strftime()` 関数を和暦文字列にも対応するようにオーバーライドしています。


![datetimejp.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3452983/5ed81158-b6ca-beae-9b76-1545bc23ef6a.jpeg)



拡張された主な書式化指定子は次のとおりです。

|書式化指定子|意味|使用例|
|:---|:---|:---|
| `%g` | 元号を日本語表記で表示します。 | 明治, 大正, 昭和, 平成, 令和 |
| `%e` | 0埋めした10進数で表記した年度を表します。 | 01, 02, ..., 99 |



## 和暦→西暦の変換サンプル


以下の例では '令和1年5月1日' という文字情報を解析して日付オブジェクトを生成します。
その後、'20190501' という文字列に変換して出力します。


``` python
from datetimejp import JDatetime
jd = JDatetime.strptime('令和1年5月1日', '%g%e年%m月%d日')
result = jd.strftime('%Y%m%d')
print(result) # -> '20190501'
```


## 西暦→和暦の変換サンプル


以下の例では '2020/2/2' という文字情報を解析して日付オブジェクトを生成します。
その後、'令和02年02月02日' という文字情報に変換して出力します。

``` python
from datetimejp import JDatetime
jd = JDatetime.strptime('2020/2/2', '%Y/%m/%d')
result = jd.strftime('%g%e年%m月%d日')
print(result) # -> '令和02年02月02日'
```

## 和暦文字列の変換サンプル


以下の例では実行日基準の日付オブジェクトを生成します。
その後、翌日の日付を計算し、和暦表示の文字情報に変換して出力します。

``` python
from datetime import timedelta
from datetimejp import JDatetime
jd = JDatetime.now()
jd = jd + timedelta(days=1)
result = jd.strftime('%g%e年%m月%d日')
print(result) # -> 実行日翌日の和暦表示
```

# 実装方法

ここでは和暦変換の実装部分について説明します。
外部モジュールを使わない状況や機能を拡張したい場面で参考になるかもしれません。
逆に、説明を簡単にするために datetimejp の書き方とは異なるサンプルを記載していますので、datetimejp をインストールしている場合はご注意ください。

## 和暦文字列の出力

和暦の情報さえあれば、datetime オブジェクトを和暦文字列に変換して出力することは難しくありません。
上記 [和暦変換のシンプルな実装例](#和暦変換のシンプルな実装例) では固定の書式で出力していましたが、ここでは引数で指定した書式 `format_string` の形で出力するように修正します。
書式の指定方法は元号を `%g`、和暦の年度を `%e` で表す仕様としています。

``` python
import datetime

# 和暦情報
ERA_DICT = {
    '令和': datetime.datetime(2019, 5, 1),
    '平成': datetime.datetime(1989, 1, 8),
    '昭和': datetime.datetime(1926, 12, 25)
}

# 和暦文字列出力関数
def format_date(date_obj, format_string):
    if ERA_DICT['令和'] <= date_obj:
        era_start = ERA_DICT['令和']
        era = '令和'
    elif ERA_DICT['平成'] <= date_obj:
        era_start = ERA_DICT['平成']
        era = '平成'
    elif ERA_DICT['昭和'] <= date_obj:
        era_start = ERA_DICT['昭和']
        era = '昭和'
    jp_year = str(date_obj.year - era_start.year + 1)
    jp_format = format_string.replace('%g', era).replace('%e', jp_year)
    return date_obj.strftime(jp_format)
```

次のようなコードで関数の動作確認を行うことができます。

``` python
date_obj = datetime.datetime(2019, 5, 1)
print(format_date(date_obj, '%g%e年%m月%d日'))
# -> 令和1年05月01日

date_obj = datetime.datetime(2019, 4, 30)
print(format_date(date_obj, '%g%e年%m月%d日'))
# -> 平成31年04月30日
```

この関数は昭和から令和までしか対応していませんが、和暦の情報を追加することで対応範囲を拡張することができます。
このとき、 if 文を用いた判定処理だと変更範囲が広いので、if 文を使わないように書き換えておくと便利です。
具体的には、和暦情報から次の基準により元号を抽出します。

:::note
「入力日付が元号の開始日付以降である元号の内、元号の開始日付が一番大きいもの」が入力日付と対応した元号である。
:::

例えば '2020年' は '令和2年' にも '平成32年' にも解釈できますが、'令和2年' として抽出しましょう、というプログラミング的な考え方です。
このように考えることで、和暦情報 `ERA_DICT` をメンテナンスするだけで元号の抽出ができます。

これを実際に実装したコードがこちらです。

``` python
import datetime

# 和暦情報
ERA_DICT = {
    '令和': datetime.datetime(2019, 5, 1),
    '平成': datetime.datetime(1989, 1, 8),
    '昭和': datetime.datetime(1926, 12, 25),
    '大正': datetime.datetime(1912, 7, 30),
    '明治': datetime.datetime(1868, 1, 25),
}

# 和暦文字列出力関数
def format_date(date_obj, format_string):
    eras_filter = filter(lambda x: ERA_DICT[x] <= date_obj, ERA_DICT)
    era = max(eras_filter, key=lambda x: ERA_DICT[x])
    jp_year = str(date_obj.year - ERA_DICT[era].year + 1)
    jp_format = format_string.replace('%g', era).replace('%e', jp_year)
    return date_obj.strftime(jp_format)
```


次のコードで追加した和暦情報についても動作確認を行います。

``` python
date_obj = datetime.datetime(2019, 5, 1)
print(format_date(date_obj, '%g%e年%m月%d日'))
# -> 令和1年05月01日

date_obj = datetime.datetime(2019, 4, 30)
print(format_date(date_obj, '%g%e年%m月%d日'))
# -> 平成31年04月30日

date_obj = datetime.datetime(1912, 7, 30)
print(format_date(date_obj, '%g%e年%m月%d日'))
# -> 大正1年07月30日

date_obj = datetime.datetime(1912, 7, 29)
print(format_date(date_obj, '%g%e年%m月%d日'))
# -> 明治45年07月29日
```

補足として、以下のエラー処理はサンプルコードを簡単にするために省略しています。

:::note alert
次のような場合にはエラーが発生する。
・ 日付が古く、対応する和暦情報がない場合。
・ 引数 `date_obj` に datetime オブジェクトではなく date オブジェクトが指定された場合。
:::

つまり、このままでは次のような挙動になっている点に注意してください。

``` python
try:
    date_obj = datetime.datetime(1868, 1, 24)
    print(format_date(date_obj, '%g%e年%m月%d日'))
    # -> エラーが発生する
except Exception as e:
    print(e)

try:
    date_obj = datetime.date(2020, 1, 1)
    print(format_date(date_obj, '%g%e年%m月%d日'))
    # -> エラーが発生する
except Exception as e:
    print(e)
```


## 和暦文字列の解析

和暦文字列の解析は和暦文字列の出力と比較して難しく、工夫が必要です。
そしてこの解析手法に関する工夫こそが、 **この記事の本題** といっても過言ではありません。

さて、 [Python の日付処理](#python-の日付処理) では、標準モジュール datetime で西暦文字列を解析できることを確認しました。
そして[シンプルな実装での課題](#シンプルな実装での課題) で見てきたように、解析したい書式が様々であることを想定しています。

ここまでの内容を踏まえて、datetime の解析処理を次のように拡張することで和暦変換の実現方法を考えていきます。

|書式化指定子|意味|使用例|
|:---|:---|:---|
| `%g` | 元号を日本語表記で表します。 | 明治, 大正, 昭和, 平成, 令和 |
| `%e` | 0埋めした10進数で表記した和暦の年度を表します。 | 01, 02, ..., 99 |

### 和暦文字列の解析方針

まずは公式ドキュメント [strftime() と strptime() の書式コード](https://docs.python.org/ja/3/library/datetime.html#strftime-and-strptime-format-codes) を参照します。
今回注目するのは次の記載です。

| ディレクティブ | 意味 | 使用例 |
| --- | ----------- | ------- |
| ... | ... | ... |
| `%y` | 0埋めした10進数で表記した世紀無しの年。 | 00, 01, ..., 99 |
| ... | ... | ... |

和暦の年度は（今のところ）2桁しかないので、`%y` を和暦の年度に代用することができそうです。
例えば、次のサンプルコードは和暦文字列から日付を抽出します。


``` python
import datetime
dtime = datetime.datetime.strptime('令和01年05月01日', '令和%y年%m月%d日')
print(dtime.strftime('%y/%m/%d')) # -> 01/05/01
```

ここでは解析書式 `令和%y年%m月%d日` の中で元号を `令和` として指定しており、厳密には和暦文字列の解析を行っているわけではありません。
実際には21世紀の01年、すなわち2001年として解析されています。
これは出力書式に `%y` ではなく `%Y` を指定することで確認することができます。

``` python
import datetime
dtime = datetime.datetime.strptime('令和01年05月01日', '令和%y年%m月%d日')
print(dtime.strftime('%Y/%m/%d')) # -> 2001/05/01
```


しかし注目すべきは、和暦文字列から日付を抽出することに成功している点です。
これを上述の [和暦変換のシンプルな実装例](#和暦変換のシンプルな実装例) の処理と組み合わせると、抽出した日付を期待する日付に変換することができます。

``` python
import datetime

# 和暦情報
ERA_DICT = {
    '令和': datetime.datetime(2019, 5, 1),
    '平成': datetime.datetime(1989, 1, 8),
    '昭和': datetime.datetime(1926, 12, 25)
}

# 和暦→西暦変換関数(2)
def to_seireki_2(era, year):
    era_start = ERA_DICT[era]
    jp_year = era_start.year + int(year) - 1
    return jp_year

# 和暦文字列の解析テスト
dtime = datetime.datetime.strptime('令和01年05月01日', '令和%y年%m月%d日')
jp_year = to_seireki_2('令和', dtime.strftime('%y'))
dtime = dtime.replace(year=jp_year)
print(dtime.strftime('%Y/%m/%d')) # -> 2019/05/01
```


このコードでは期待する西暦の情報を取得できており、和暦文字列の解析としてこの方針が採用できそうだと考えます。
その際に解決すべき問題として残っているのは次の2点です。

:::note warn
・ 元号の解析は別に実装する必要がある。
・ 書式 `%y` は0埋めされていない1桁の年度に対応していない。
:::

### 和暦文字列の解析方法

これらの問題は強引に解決することができます。
つまり、合致するまで全てのパターンを順番に試していくという解決方法です。
上記の例では人間の判断で解析書式に `令和` を指定していましたが、Python プログラムには `令和` や `平成`、`昭和` といった全ての元号を試してもらえば良いのです。

例えば、`%g%e年%m月%d日` という書式で和暦文字列を解析する際には、`%g` と `%e` の部分を置換した以下の解析書式を想定します。
+ `令和%y年%m月%d日`
+ `平成%y年%m月%d日`
+ `昭和%y年%m月%d日`

もし入力の和暦文字列が '平成30年01月01日' だった場合、書式 `令和%y年%m月%d日` では解析できずにエラーになります。
その場合は、次に書式 `平成%y年%m月%d日` を試して解析に成功するという作戦です。

解決すべき問題はもう一つ残っています。

:::note warn
・ 書式 `%y` は0埋めされていない1桁の年度に対応していない。
:::

つまり、`%y年` で '01年' を解析することはできますが、'1年' を解析することはできないのです。
とはいえ、この問題も元号と同様に '1年' から '9年' までの9パターンを想定することで対応可能です。

以上をまとめると、次のように強引な方法で和暦文字列の解析を実装することができます。

:::note
解析書式内の和暦指定書式 `%g` と `%e` を下記候補に読み替えて西暦文字列として解析を行い、解析に成功するまで候補の読み替えを繰り返す。
+ 書式 `%g` の候補は '令和', '平成', '昭和' である。
+ 書式 `%e` の候補は '%y', '1', '2', '3', ...,'9' である。

解析に成功した候補の情報に基づき、西暦の解析結果を変換して和暦情報を取得する。
:::


例えば書式 `%g%e年%m月%d日` の場合、次のような書式を想定することになります。

+ `令和%y年%m月%d日`
+ `令和1年%m月%d日`
+ `令和2年%m月%d日`
...
+ `令和9年%m月%d日`
+ `平成%y年%m月%d日`
+ `平成1年%m月%d日`
+ `平成2年%m月%d日`
...
+ `平成9年%m月%d日`
+ `昭和%y年%m月%d日`
+ `昭和1年%m月%d日`
+ `昭和2年%m月%d日`
...
+ `昭和9年%m月%d日`

### 和暦文字列の解析実装

すべてのパターンを試すのは無駄が多いと思いましたか？
**ちょっと待ってください！これから無駄を減らす工夫を行います。**

ここまでは `%g%e年%m月%d日` のように解析書式だけしか見ていませんでした。
この条件だけでは、`%g` の部分が '令和' や '平成' といった文字列の可能性があるのは事実です。
しかし、実際には解析したい文字列が '平成30年01月01日' のように入力されているわけです。

この解析対象文字列 '平成30年01月01日' を解析する際には、`%g` の候補から '令和' や '昭和' といった元号が除外できそうです。

これをプログラムとして実装する際の考え方は次のようになります。

:::note
元号書式 `%g` に対する候補文字列 '令和', '平成', '昭和' について、解析対象文字列に含まれていない候補文字列は候補から除外できる。
:::

これは和暦年度書式 `%e` についても同様です。

:::note
年度書式 `%e` に対する候補文字列 '1', '2', '3', ...,'9' について、解析対象文字列に含まれていない候補文字列は候補から除外できる。
:::

例えば、`%g%e年%m月%d日` という書式で和暦文字列 '令和1年12月31日' を解析する際には、元号は '令和' だけに絞れます。
一方で1桁の数字は '1', '2', '3' が含まれており、和暦年度の候補は '1', '2', '3' が残ることになります。
結果として、次の４通りのパターンを順番に試せば良いことが分かります。
+ 令和%y年%m月%d日
+ 令和1年%m月%d日
+ 令和2年%m月%d日
+ 令和3年%m月%d日



これを実装したものが次のコードになります。

``` python
import datetime

# 和暦情報
ERA_DICT = {
    '令和': datetime.datetime(2019, 5, 1),
    '平成': datetime.datetime(1989, 1, 8),
    '昭和': datetime.datetime(1926, 12, 25)
}

# 和暦文字列解析関数
def parse_date(date_string, format_string):
    # 候補リスト作成
    eras = [e for e in ERA_DICT.keys() if e in date_string]
    jp_years = [str(n) for n in range(1, 10) if str(n) in date_string]

    # 元号に対して解析処理を繰り返す
    for era in eras:
        try:
            temp_format = format_string.replace('%g', era).replace('%e', '%y')
            date_obj = datetime.datetime.strptime(date_string, temp_format)
            jp_year = ERA_DICT[era].year + int(date_obj.strftime('%y')) - 1
            date_obj = date_obj.replace(year=jp_year)
            return date_obj
        except ValueError:
            pass

    # 1桁の年度と元号に対して解析処理を繰り返す
    for y in jp_years:
        for era in eras:
            try:
                temp_format = format_string.replace('%g', era).replace('%e', y)
                date_obj = datetime.datetime.strptime(date_string, temp_format)
                jp_year = ERA_DICT[era].year + int(y) - 1
                date_obj = date_obj.replace(year=jp_year)
                return date_obj
            except ValueError:
                pass

# テスト実行
if __name__ == '__main__':
    print(parse_date('平成31年4月30日', '%g%e年%m月%d日')) # -> 2019-04-30 00:00:00
    print(parse_date('令和1年5月1日', '%g%e年%m月%d日')) # -> 2019-05-01 00:00:00
```


# 「うるう年」対応
※ 2024年6月30日追記。ご指摘ありがとうございます。

上述の実装方法で基本的な和暦変換ができるはずです。
しかし、日付を扱う際には「うるう年」に注意する必要があります。

と言っても、今回の方針では標準ライブラリ datetime を使用して日付を扱っているので、「うるう年」であろうと datetime ライブラリが扱えるので問題ない・・・

**というわけにはいきません。**
先ほど作成した関数を使って、期待通りに動作するか確認してみましょう。

## 「うるう年」に対する動作確認

例えば西暦2020年（令和2年）は「うるう年」のため、2月29日が存在します。この日の和暦変換も正しく動作する必要があります。
次のような動作確認コードを作成します。

``` python
# うるう年のテスト(和暦から西暦)
date_obj = parse_date('令和2年2月29日', '%g%e年%m月%d日')
print(date_obj) # -> None

# うるう年のテスト(西暦から和暦)
date_obj = datetime.datetime.strptime('2020年02月29日', '%Y年%m月%d日')
print(format_date(date_obj, '%g%e年%m月%d日')) # -> 令和2年02月29日
```

先ほど作成した関数に動作確認コードを追加して実行してみると、和暦から西暦の結果が None になっていることが分かります。
どうやら `令和2年2月29日` の解析に失敗したようです。
特殊な日付ですが、**正しく解析できない日付がある**のは問題です。

一方、西暦から和暦は期待通り変換できています。
和暦文字列の解析の実装に何か問題があったのだと考えられます。

## 解析失敗の原因
そもそも [和暦文字列の解析方針](#和暦文字列の解析方針) は、`%y` を用いて `令和01年` を `21世紀の '01年'` とみなして日付を取得するというものでした。
つまりこの方針では、`令和2年2月29日` を一旦 `2002年2月29日` として取得し、その後に年度の調整を行うことになります。
しかし2002年は「うるう年」ではないため、`2002年2月29日` の取得に失敗してしまうのです。


## 「うるう年」対応の実装
以上の内容を踏まえ、「うるう年」に対応したプログラムの実装方法を考えます。
ここでの目標は、**解析対象の和暦文字列が「うるう年」の2月29日の場合も正しく解析可能**とすることです。

さて、現時点で「うるう年」の2月29日以外の和暦文字列は解析できています。
これまでの処理を「うるう年」対応のためだけに変えたくはありません。
そこで、和暦文字列が「うるう年」の2月29日の場合専用の処理を追加する方針を考えます。

そして追加する「うるう年」専用の処理は、これまでの実装内容と同じような考え方で作成できます。
つまり、先に日付を無視した解析を行い、後から解析結果の日付を調整するのです。

:::note
「うるう年」の2月29日の和暦文字列は、次の手順で解析する。
1. 解析書式内の日付指定書式 `%d` を `29` に読み替えて解析を行う。
1. 解析結果の日付を29日に変換して和暦情報を取得する。
:::

「うるう年」対応が必要な日付は2月29日だけなので、日付指定書式 %d を 29 に読み替えることができます。
そして解析書式に日付が含まれない場合、解析日付は1日として扱われます。
結果として、「うるう年」の2月29日は、2月1日の日付として解析を行うことができます。
ここで重要なのは、**和暦文字列を解析して年と月を取得できる**という点です。
最後に解析結果の日付を29日に調整すれば、正しく和暦文字列の解析を行うことができます。



具体的に `令和2年2月29日` という和暦文字列を解析する手順を考えてみましょう。
解析書式は `%g%e年%m月%d日` を指定します。
1. 和暦文字列 `令和2年2月29日` を書式 `%g%e年%m月%d日` で解析し、解析に失敗します。
1. 解析書式 `%g%e年%m月%d日` を `%g%e年%m月29日` に読み替えます。
1. 和暦文字列 `令和2年2月29日` を書式 `%g%e年%m月29日` で解析し、2020年2月1日のオブジェクトを取得します。
1. 取得した日付オブジェクトの日付を29日に変換し、2020年2月29日のオブジェクトを取得します。



## 和暦対応コードまとめ

以上の実装コードをまとめると次のようになります。
直接実行した際にテストできるように、メイン処理として動作確認用のコードを記載しています。


``` python
import datetime

# 和暦情報
ERA_DICT = {
    '令和': datetime.datetime(2019, 5, 1),
    '平成': datetime.datetime(1989, 1, 8),
    '昭和': datetime.datetime(1926, 12, 25),
    '大正': datetime.datetime(1912, 7, 30),
    '明治': datetime.datetime(1868, 1, 25),
}

# 和暦文字列出力関数
def format_date(date_obj, format_string):
    eras_filter = filter(lambda x: ERA_DICT[x] <= date_obj, ERA_DICT)
    era = max(eras_filter, key=lambda x: ERA_DICT[x])
    jp_year = str(date_obj.year - ERA_DICT[era].year + 1)
    jp_format = format_string.replace('%g', era).replace('%e', jp_year)
    return date_obj.strftime(jp_format)

# 和暦文字列解析関数
def parse_date(date_string, format_string):
    # 候補リスト作成
    eras = [e for e in ERA_DICT.keys() if e in date_string]
    jp_years = [str(n) for n in range(1, 10) if str(n) in date_string]

    # 元号に対して解析処理を繰り返す
    for era in eras:
        try:
            temp_format = format_string.replace('%g', era).replace('%e', '%y')
            date_obj = datetime.datetime.strptime(date_string, temp_format)
            jp_year = ERA_DICT[era].year + int(date_obj.strftime('%y')) - 1
            date_obj = date_obj.replace(year=jp_year)
            return date_obj
        except ValueError:
            pass

    # 1桁の年度と元号に対して解析処理を繰り返す
    for y in jp_years:
        for era in eras:
            try:
                temp_format = format_string.replace('%g', era).replace('%e', y)
                date_obj = datetime.datetime.strptime(date_string, temp_format)
                jp_year = ERA_DICT[era].year + int(y) - 1
                date_obj = date_obj.replace(year=jp_year)
                return date_obj
            except ValueError:
                pass
                
    # うるう年対応
    if '29' in date_string and '%d' in format_string:
         new_format_string = format_string.replace('%d', '29')
         date_obj = parse_date(date_string, new_format_string)
         date_obj = date_obj.replace(day=29)
         return date_obj

# テスト実行
if __name__ == '__main__':
    # 和暦から西暦への変換 (1)
    date_obj = parse_date('平成31年4月30日', '%g%e年%m月%d日')
    print(date_obj.strftime('%Y年%m月%d日')) # -> 2019年04月30日

    # 和暦から西暦への変換 (2)
    date_obj = parse_date('令和1年5月1日', '%g%e年%m月%d日')
    print(date_obj.strftime('%Y年%m月%d日')) # -> 2019年05月01日

    # 西暦から和暦への変換 (1)
    date_obj = datetime.datetime.strptime('2019年4月30日', '%Y年%m月%d日')
    print(format_date(date_obj, '%g%e年%m月%d日')) # -> 平成31年04月30日

    # 西暦から和暦への変換 (2)
    date_obj = datetime.datetime.strptime('2019年5月1日', '%Y年%m月%d日')
    print(format_date(date_obj, '%g%e年%m月%d日')) # -> 令和1年05月01日
    
    # うるう年のテスト(和暦から西暦)
    date_obj = parse_date('令和2年2月29日', '%g%e年%m月%d日')
    print(date_obj.strftime('%Y年%m月%d日')) # -> 2020年02月29日

    # うるう年のテスト(西暦から和暦)
    date_obj = datetime.datetime.strptime('2020年02月29日', '%Y年%m月%d日')
    print(format_date(date_obj, '%g%e年%m月%d日')) # -> 令和2年02月29日

```



# さいごに
本記事では和暦変換処理の実装方法を焦点を当てて紹介しています。
 [和暦対応コードまとめ](#和暦対応コードまとめ) に記載のコードはコピペしてそのまま使えますし、カスタマイズして使う際の参考になれば嬉しく思います。

一方で、和暦変換処理をより便利に扱いやすくしたパッケージ [datetimejp](https://pypi.org/project/datetimejp/) も用意しています。
この記事では紹介できなかった機能やエラー処理も実装していますので、是非こちらもご利用ください。