来たる令和元年に備える、macOS / iOS の日付変換処理

# はじめに

macOS Mojave 10.14.5 Beta 2 および iOS 12.3 beta 2 は新元号「令和」に対応するそうです。気になったのでさっそく新しいOS(beta)での日付処理の挙動を調べてみました。
例によりOSそのもののスクショは貼りませんが、Playgroundの実行結果を記載しておきます。

https://developer.apple.com/documentation/macos_release_notes/macos_mojave_10_14_5_beta_2_release_notes
https://developer.apple.com/documentation/ios_release_notes/ios_12_3_beta_2_release_notes

> **New Features**
Support for the Reiwa (令和) era of the Japanese calendar, which begins on May 1, 2019, is now available. The first year of Japanese-calendar era is represented as  “元年” (“Gannen”) instead of “1年”, except in the shorter numeric-style formats which typically also use the narrow era name; for example: “R1/05/01”. (27323929)
**Known Issues**
You might experience unexpected behavior while changing the system time ahead to May 1 when using a device with the system language and calendar set to Japanese. (49371044)

# 「西暦 → 和暦 → 西暦」の連続変換で検証

Playgroundで次のような簡単な（汚い）コードにより検証を行いました。

```swift
import Cocoa

let cal_japanese = Calendar(identifier: .japanese)
let cal_gregorian = Calendar(identifier: .gregorian)
let locale_ja = Locale(identifier: "ja")

func date(fromGregorianDateString dateString: String, dateFormatter: DateFormatter) -> Date? {
	dateFormatter.calendar = cal_gregorian
	dateFormatter.dateFormat = "y-MM-dd"
	return dateFormatter.date(from: dateString)
}

func japaneseDateString(fromDate date: Date, dateFormatter: DateFormatter) -> String? {
	dateFormatter.calendar = cal_japanese
	return dateFormatter.string(from: date)
}

func gregorianDateString(fromJapaneseDateString dateString: String, dateFormatter: DateFormatter, outputDateStyle: DateFormatter.Style) -> String? {
	dateFormatter.calendar = cal_japanese
	//dateFormatter.dateStyle = .medium
	//dateFormatter.dateFormat = "Gy年M月d日"

	guard let date = dateFormatter.date(from: dateString) else  {
		return nil
	}

	dateFormatter.calendar = cal_gregorian
	dateFormatter.dateStyle = outputDateStyle
	return dateFormatter.string(from: date)
}

// --

let df = DateFormatter()
df.locale = locale_ja

// memo:
//	  Showa 64.01.07（昭和６４年）= A.D.1989.01.07
//	 Heisei 01.01.08（平成元年）　= A.D.1989.01.08
//	 Heisei 31.04.30（平成３１年）= A.D.2019.04.30
//	  Reiwa 01.05.01（令和元年）　= A.D.2019.05.01

let gregorianDate_input1 = "1989-01-07"
let gregorianDate_input2 = "1989-01-08"
let gregorianDate_input3 = "2019-04-30"
let gregorianDate_input4 = "2019-05-01"
let gregorianDate_input5 = "0645-07-17"

let gregorianDate_sample1 = date(fromGregorianDateString: gregorianDate_input1, dateFormatter: df)!
let gregorianDate_sample2 = date(fromGregorianDateString: gregorianDate_input2, dateFormatter: df)!
let gregorianDate_sample3 = date(fromGregorianDateString: gregorianDate_input3, dateFormatter: df)!
let gregorianDate_sample4 = date(fromGregorianDateString: gregorianDate_input4, dateFormatter: df)!
let gregorianDate_sample5 = date(fromGregorianDateString: gregorianDate_input5, dateFormatter: df)!

df.dateStyle = .long

let japaneseDateStr1 = japaneseDateString(fromDate: gregorianDate_sample1, dateFormatter: df)
let japaneseDateStr2 = japaneseDateString(fromDate: gregorianDate_sample2, dateFormatter: df)
let japaneseDateStr3 = japaneseDateString(fromDate: gregorianDate_sample3, dateFormatter: df)
let japaneseDateStr4 = japaneseDateString(fromDate: gregorianDate_sample4, dateFormatter: df)
let japaneseDateStr5 = japaneseDateString(fromDate: gregorianDate_sample5, dateFormatter: df)

if let japaneseDateStr = japaneseDateStr1, let gregorianDateStr = gregorianDateString(fromJapaneseDateString: japaneseDateStr, dateFormatter: df, outputDateStyle: .long) {
	print("\"\(gregorianDate_input1)\" → \(japaneseDateStr1 ?? "") → \(gregorianDateStr)")
}
if let japaneseDateStr = japaneseDateStr2, let gregorianDateStr = gregorianDateString(fromJapaneseDateString: japaneseDateStr, dateFormatter: df, outputDateStyle: .long) {
	print("\"\(gregorianDate_input2)\" → \(japaneseDateStr2 ?? "") → \(gregorianDateStr)")
}
if let japaneseDateStr = japaneseDateStr3, let gregorianDateStr = gregorianDateString(fromJapaneseDateString: japaneseDateStr, dateFormatter: df, outputDateStyle: .long) {
	print("\"\(gregorianDate_input3)\" → \(japaneseDateStr3 ?? "") → \(gregorianDateStr)")
}
if let japaneseDateStr = japaneseDateStr4, let gregorianDateStr = gregorianDateString(fromJapaneseDateString: japaneseDateStr, dateFormatter: df, outputDateStyle: .long) {
	print("\"\(gregorianDate_input4)\" → \(japaneseDateStr4 ?? "") → \(gregorianDateStr)")
}
if let japaneseDateStr = japaneseDateStr5, let gregorianDateStr = gregorianDateString(fromJapaneseDateString: japaneseDateStr, dateFormatter: df, outputDateStyle: .long) {
	print("\"\(gregorianDate_input5)\" → \(japaneseDateStr5 ?? "") → \(gregorianDateStr)")
}
```

---

<img width="798" src="https://qiita-image-store.s3.amazonaws.com/0/3603/001a7893-e776-6740-d145-32754232e9e3.png">

<img width="809" src="https://qiita-image-store.s3.amazonaws.com/0/3603/0324d097-8d7d-e0b3-4f2a-95747d93b1d0.png">


# 所感

OSが新元号に対応することで、日付処理はCocoaの仕組みに正しく乗っかっておけばとりあえず難しいことは考えずとも新元号元年を迎えられそうです。日付データを特定の暦に依存するのではなく、Date型と互換性のある情報で持っておけばユーザーインターフェイスはいつでも好きな暦に変換すれば良いだけになるなので、そこに大きな苦労はないように思います。

ただし、データモデルに和暦で直接日付データを持つとか、明治以前の大過去の情報を扱う可能性があるシステムでは要注意です。（後述）

## 書式

出力の際は変に独自の書式を指定せず、`DateFormatter.Style`を利用するのが無難に思えました。いろいろなユースケースがあると思うので一概には言えませんが、特に都合が悪くなければこれに頼っておくことで妙な不具合は起きにくくなります。

過去にも「0031年」などと西暦の日付設定しか考慮されていないシステムでの和暦表記の不具合を見かけたことがありました。

## 過去の日付

現在の新暦（太陽暦）で直接遡れる範囲であれば問題ないように思います。改暦が行われた1873年1月1日まではおそらく。それよりも前になると日本の場合は天保暦になるので、このコードでは直接変換ができないことになります。

試しに建元より前の日付（と言っても暦が違うのでそもそもずれているが）を指定してみたところ、「大化0年12月31日」「大化-2年12月31日」という具合に、大化0年や大化マイナス年となってしまいました。そういう扱いなのか。

# 解説

登場人物は以下のクラスです。基本的には `DateFormatter` で変換するという方針です。

- (NS)DateFormatter
- (NS)Calendar
- (NS)Locale
- (NS)Date
- (NS)String

## 西暦の日付文字列を Date に変換

グレゴリオ暦（西暦）のカレンダーオブジェクトを用意して、それを DateFormatter に渡します。日付書式は適当で良いですが、ここでは `y-MM-dd` を渡しています。`y` は一つだけで通常の4桁を受け入れられるようなのでこのようにしています。

```swift
let cal_gregorian = Calendar(identifier: .gregorian)
dateFormatter.locale = Locale(identifier: "ja")

let dateString = "1989-01-07"

dateFormatter.calendar = cal_gregorian
dateFormatter.dateFormat = "y-MM-dd"
let date = dateFormatter.date(from: dateString)
```

## Date を和暦の日付文字列に変換

和暦のカレンダーオブジェクトを DateFormatter に渡し、ロケールを日本にします。
日付の書式は標準スタイルの `.long` を採用しました。ここで `.medium` にすると例えば「平成」ならば「H31」という表記になります。

```swift
let cal_japanese = Calendar(identifier: .japanese)
dateFormatter.locale = Locale(identifier: "ja")

let date = Date()

dateFormatter.calendar = cal_japanese
dateFormatter.dateStyle = .long
let japaneseDateString = dateFormatter.string(from: date) // "平成31年4月30日"
```

標準のスタイルではなく独自の書式を当てる場合は、例えば次のようにします。

```swift
//dateFormatter.dateStyle = .long // この行を消す
dateFormatter.dateFormat = "Gy年M月d日"
```

`G`が元号、`y`が年数です。yはいくつ重ねても零埋め表記にはなりませんでした。（以前のOSではなっていたような？？ 確証なし）

## 元号だけ取り出す

書式に`G`を当てれば良いだけです。動的に元号の文字列が欲しい場合に使えます。ロケール次第では "Heisei" のような言語ごとの表記でも取得することができます。

```swift
let cal_japanese = Calendar(identifier: .japanese)
dateFormatter.locale = Locale(identifier: "ja")

let date = Date()

dateFormatter.calendar = cal_japanese
dateFormatter.dateFormat = "G"
let currentEra = dateFormatter.string(from: date) // "平成"
```


## 元年

`1`年目は自動的に`元`年に置き換えられます。

「平成1年」→「**平成元年**」
「令和1年」→「**令和元年**」


## 短縮表記

`DateFormatter.Style` に `.medium` や `.short` を指定すると短縮表記を得られます。この際、単位も省略され、区切り文字として`/`などが用いられるようになります。

「明治45年」→「**M45**」
「大正15年」→「**T15**」
「昭和64年」→「**S64**」
「平成31年」→「**H31**」
「令和1年」→「**R1**」

明治より前の元号には短縮表記が定められていないため、通常の元号表記で出力されます。


「慶応4年」→「**慶応4**」


## 古いOSだとどうなるのか

おそらく、平成を永久に続けることになります。表記としては正しくありませんが、免許証などによくみられる未来の日付として便宜上平成を書くあの感じになるかと思います。裏側のデータがDateオブジェクトになっていればさほど大きな問題はないかと。