[Swift] 悠久の日本史に触れる! DateとDateFormaterを使った和暦取得で遡る日本の元号1500年の旅

Developers.IOで歴史の話を書くと思わなかったよ
2020.05.04

はじめに

CX事業本部の中安です。ご無沙汰しております。今年初ブログです。おおきに。

さて、Swiftの標準コンポーネントである Date 構造体とその周辺クラスは、日付を扱うにあたって非常に強力な機能を兼ね備えています。

今回はその中でも日本特有の「元号」にまつわる話です。

DateDateFormater を組み合わせることで、実は和暦を簡単に文字列として取得することが可能です。 では、いったいそれはどこまで取得できるのか。 ちょっと実験してみたいと思います。

準備

コンポーネントを整える

今回の実験をするために Date の extension に便利なメソッドやプロパティを定義していきます。

まずはカレンダーやロケールなどの生成を何度も書くことがないように共通化をしておきます。

import Foundation

extension Date {
    
    // カレンダー取得
    
    private static func createCalendar(identifier: Calendar.Identifier) -> Calendar {
        var calendar = Calendar(identifier: identifier)
        calendar.timeZone = Date.tokyoTimeZone
        calendar.locale = Date.japanLocale
        return calendar
    }
    
    private static var gregorianCalendar: Calendar {
        return createCalendar(identifier: .gregorian)
    }
    
    private static var japanCalendar: Calendar {
        return createCalendar(identifier: .japanese)
    }
    
    // タイムゾーン取得
    
    private static var tokyoTimeZone: TimeZone {
        return TimeZone(identifier: "Asia/Tokyo")!
    }
    
    // ロケール取得
    
    private static var japanLocale: Locale {
        return Locale(identifier: "ja_JP")
    }
    
    // 日付コンポーネント取得
    
    private static var dateComponents: DateComponents {
        var comp = DateComponents()
        comp.calendar = Date.gregorianCalendar
        comp.timeZone = Date.tokyoTimeZone
        return comp
    }
}

こんな感じ。

ここの詳細は以前にブログに書いていますので、ご参考ください。(今回は実験のためなので、以前と書き方を若干変えています)

【Swift】Tips: あると便利だったextension達(Date編) | Developers.IO

【Swift】Tips: あると便利だったextension達(Date編 その2) | Developers.IO

和暦を取得してみよう

それでは Date から和暦を文字列として取得する方法を書き記します。

DateFormaterを介して任意のフォーマットで日付文字列を取得できるようにしておきます。 後々のことを考えてカレンダーとロケールを引数で指定できるようにしておきます。

private func convertDateString(format: String, calendar: Calendar, locale: Locale) -> String {
    let dateFormatter = DateFormatter()
    dateFormatter.calendar = calendar
    dateFormatter.locale = locale
    dateFormatter.dateFormat = format
    return dateFormatter.string(from: self)
}

そして、和暦を返却するパブリックなプロパティを定義します。

var wareki: String {
    return convertDateString(
    	format: "Gy年M月d日",
        calendar: Date.japanCalendar,
        locale: Date.japanLocale
    )
}

フォーマットに Gを指定していますが、これが「元号」になります。

  • カレンダーは前項で定義した日本用のものを使用します。グレゴリオ暦で指定すると元号は返されず「西暦」という文字が返されます。
  • ロケールも前項で定義した日本用のものを使用します。これを違うロケールにすると「令和」は「Reiwa」で返ってきてしまいます。

さぁ、これでまずはちゃんと取得できるかを試してみます。

print(Date().wareki)
// 令和2年5月4日

このように実行した日付の和暦が文字列としてログ出力されます。

任意の日付を指定できるようにしよう

前項では現在日付を使って和暦を表示するところまでを試してみました。

しかし、今回は壮大な実験です。

プログラマブルに日付を指定して和暦文字列を得られるように、日付オブジェクト生成用のメソッドを作ってみます。

static func factory(_ year: Int, _ month: Int, _ day: Int) -> Date {
    var comp = Date.dateComponents
    
    comp.year = year
    comp.month = month
    comp.day = day
    comp.hour = 0
    comp.minute = 0
    comp.second = 0
    
    return Date.gregorianCalendar.date(from: comp)!
}

この静的メソッドにより年月日を指定することで任意の日付オブジェクトを生成できるようにしました。 今回は時刻は関係ないので、とりあえず0固定にしています。

使用する時はこんな感じ。

print(Date.factory(1945, 8, 15).wareki)
// 昭和20年8月15日

1945年8月15日は終戦の日ですが、「昭和20年8月15日」という和暦で取得できていることがわかります。 シンプルに整数だけで指定するので見た目にもわかりやすいのではないでしょうか。 自分の誕生日などを西暦で渡して試してみてください。

月の最後の日を取得できるようにする

後の実験のために、指定した月の最後の日を取得できるようにしておきます。

static func lastDayOfMonth(_ year: Int, _ month: Int) -> Int {
    var comp = Date.dateComponents
    
    comp.year = year
    comp.month = month + 1
    comp.day = 0
    comp.hour = 0
    comp.minute = 0
    comp.second = 0
    
    let date = Date.gregorianCalendar.date(from: comp)!
    return Date.gregorianCalendar.component(.day, from: date)
}

この静的メソッドにより、年と月を指定することで月の最終日を取得できます。 先ほどの factory(_:_:_:) メソッドに実装は似ていますが、月に1を足して、日を0にしているところがポイントです。

詳しくは、下記のブログに書いています。

【Swift】Tips: あると便利だったextension達(Date編 その2) | Developers.IO

月の最初と最後の日付

print(Date.lastDayOfMonth(2019, 2))
// 28
print(Date.lastDayOfMonth(2020, 2))
// 29

2月を指定すると2019年は平年なので28。2020年は閏年なので29が返ってきます。

悠久の旅へレッツゴー

さて、これで準備が整いました。

ここまでで準備したものを使って、現在から2020年間の旅をしたいと思います。

まずは1年ごとに出力

以下のようにシンプルに1年ごとに年を減らしていって、どのように和暦が返ってくるかを見てみます。

for year in (0...2020).reversed() {
    // とりあえず1月1日で出力
    print(Date.factory(year, 1, 1).wareki)
}

結果としては

令和2年1月1日
平成31年1月1日
平成30年1月1日
:
:
平成3年1月1日
平成2年1月1日
昭和64年1月1日
昭和63年1月1日
:
:
明治4年1月1日
明治3年1月1日
明治2年1月1日
慶応4年1月1日
慶応3年1月1日
慶応2年1月1日
元治2年1月1日
文久4年1月1日
文久3年1月1日
:
:
大化3年1月1日
大化2年1月1日
大化元年1月1日
大化0年1月1日
大化元年1月1日
大化-2年1月1日
大化-3年1月1日
:
:
大化-604年1月1日

2020行も出力されてしまうので省略しますが、なんと日本最初の元号である「大化」まで遡ることができるのです。

しかも DateFormatter「大化1年」とは出力せずに「大化元年」と出力までしてくれます。 Appleがここまでちゃんと日本式の年の数え方を考慮しているとは思いませんでした。 しかし、この特徴を把握していないと「1」として出力される前提でプログラムを組んでしまうと思わぬバグを埋め込みそうなので、覚えておきましょう。

改元が行われた日付

さて、この DateFormatter は日本の元号の詳細についてどこまで知ってるのでしょうか。

「平成」が終わって、現在の元号「令和」が始まったのは2019年の5月1日でした。 これをちゃんと把握しているでしょうか。

先程作った「月の最終日を取得するメソッド」を用いて、日ごとに和暦を出力してみたいと思います。

for year in (2018...2020).reversed() {
    for month in (1...12).reversed() {
        let lastDay = Date.lastDayOfMonth(year, month)
        for day in (1...lastDay).reversed() {
            print(Date.factory(year, month, day).wareki)
        }
    }
}

とりあえず2020年から2018年まで3年間をループで回してみました。

令和2年12月31日
令和2年12月30日
:
:
令和元年5月2日
令和元年5月1日
平成31年4月30日
平成31年4月29日
平成31年4月28日
:
:
平成30年1月2日
平成30年1月1日

ちゃんと2019年の5月1日に平成から令和に切り替わったことが示されています。

DateFormatterは、ちゃんと改元日まで把握しているということですね。

でも、これって、、1000年以上も前の改元まで把握しているのでしょうか?

延暦

実験として「延暦」という元号で閾値を見てみたいと思います。

延暦は平安京に都が遷された時の元号です。そう「鳴くよ(794)ウグイス平安京」と言われますから、奈良時代から平安時代に変わるタイミングである約1200年前に使用されていた元号ということになります。 ちなみに、かの有名な比叡山延暦寺はこの時代に建立されたので延暦寺という名前となっています。

Wikipediaを参考にすると、延暦は「天応」の次の元号で、西暦782年8月19日(ユリウス暦782年9月30日)から西暦806年5月18日(ユリウス暦806年6月8日)まで続いたとされています。 この当時の元号にしては異様の長さとも言えます。

延暦

1418年( 応永25年)までの600年以上にわたって、最も長い日本の元号であった。 一世一元の制導入以前の元号の中では応永に次ぎ2番目に長い。一世一元の制導入後の 1893年( 明治26年)に明治が、 1951年( 昭和26年)に昭和が、 2014年( 平成 26年)に平成がそれぞれ追い抜き、現在では歴代で5番目に長い元号となっている。

では、その改元の時期から前後1日を出力してみます。

print(Date.factory(782, 8, 18).wareki)
print(Date.factory(782, 8, 19).wareki) // この日が延暦の始まり
print(Date.factory(782, 8, 20).wareki)

print(Date.factory(806, 5, 17).wareki) // この日が延暦の終わり
print(Date.factory(806, 5, 18).wareki)
print(Date.factory(806, 5, 19).wareki)
天応2年8月18日
延暦元年8月19日
延暦元年8月20日

延暦25年5月17日
大同元年5月18日
大同元年5月19日

Wikipediaに書いてあるとおりの結果が返ってきたことがわかります。

他の元号で試してみてもおおよそ日付は史実通りに改元日が設定されていることがわかりました。

Appleはすごいですね。改元日を全て調べて、その全てをDateFormatterに搭載しているということですね。

いくつかの疑問

ここまでで「DateFormatterは凄いな」となっているのですが、長い歴史を持つ元号とはそれほど簡単なものではありません。 疑問に思ったことを検証プログラムに書いていきながら仕様や動作を見ていこうと思います。

大化以前

先ほども書いたとおり、日本における最初の元号は「大化」です。

日本史の授業でも聞いたことがあるであろう「大化の改新」はこの元号のときに行われました。

ちなみに日本に元号ができたわけは、その当時に強かった中国に日本が隷属しないよう独立国であるということを示すために採用されたといい、 唯一大化だけが元号の始まりを「改元」ではなく「建元」と呼びます。

大化

『 日本書紀』によれば「天豊財重日足姫天皇の四年を改めて大化元年とす」と記され、 皇極天皇4年 6月19日( ユリウス暦 645年 7月17日)の 孝徳天皇 即位 のときから実施された。この建元により、日本で元号が使われ始めた。

Wikipediaを見てみると、西暦645年6月19日(ユリウス暦645年7月17日)に大化が建元されたと書いてあります。 先ほどのソースコードの時を戻そう ((C)ぺこぱ)

print(Date.factory(645, 6, 18).wareki)
print(Date.factory(645, 6, 19).wareki) // この日が大化の始まり
print(Date.factory(645, 6, 20).wareki)
大化元年6月18日
大化元年6月19日
大化元年6月20日

おや、645年6月18日も「大化」ということになってしまっています。これはどういうことなのでしょうか。

もう少し遡って、645年になる前後を見てみます。

print(Date.factory(644, 12, 31).wareki)
print(Date.factory(645, 1, 1).wareki)
print(Date.factory(645, 1, 2).wareki)
大化0年12月31日
大化元年1月1日
大化元年1月2日

おや、「大化0年」というありえない年が現れてしまいました。

一番最初の実験の際にも「大化-604年1月1日」というふうに出力されていたので、 どうやら「大化の建元」と「大化以前」については考慮が含まれていないようです。

また、「大化-1年」は「大化元年」とも返ってきてしまいます

ここまで時間を遡るようなアプリは、歴史アプリを開発するときくらいしか考えられませんが、 何も考えずに実装してしまうと思わぬバグが出てしまいそうなので、ここは気をつけましょう。

元号の空白

脈略と連なってきたかのように見える日本の元号ですが、実は何度か元号は途絶えています。 先ほどの大化の次の元号「白雉」が天皇の崩御で終わると、それ以降しばらく元号は制定されませんでした。

では、DateFormatterはどのように値を返すのでしょうか。

調べたところ、西暦654年10月10日(ユリウス暦654年11月24日)に「白雉」は終わってしまします。

白雉

白雉(はくち)は、日本の元号のひとつで大化の後。朱鳥の前。西暦で650年から654年までの期間を指す(九州年号では、652年から661年までの期間を指す)。この時代の天皇は孝徳天皇。

print(Date.factory(654, 10, 9).wareki) // この日で白雉は終わり
print(Date.factory(654, 10, 10).wareki)
print(Date.factory(654, 10, 11).wareki)
白雉5年10月9日
白雉5年10月10日
白雉5年10月11日

おっと、終わったはずの白雉がまだ続いています。

この白雉は一体どこまで続いているのでしょうか。

それを調べるために白雉の次の元号「朱鳥」の始まりまで時を進めてみましょう。

調べたところ、「白雉」のあと30年近く断絶していた元号は、西暦686年7月20日(ユリウス暦686年8月14日)に天武天皇によって再び「朱鳥」として制定されます。

朱鳥

元号制度は 孝徳天皇の代から始められ、 大化・ 白雉の2つの元号が定められたが、孝徳天皇が崩御して次の 斉明天皇の代より断絶していた。 天武天皇15年 7月20日( ユリウス暦 686年 8月14日)に朱鳥と定められ、32年ぶりに再開された。

これに従ってまた出力してみましょう。

print(Date.factory(686, 7, 19).wareki) 
print(Date.factory(686, 7, 20).wareki) // この日に朱鳥が始まる
print(Date.factory(686, 7, 21).wareki)
白鳳15年7月19日
朱鳥元年7月20日
朱鳥元年7月21日

おっと!!

「白鳳15年」という結果が返ってきました。

「白鳳」という元号、実は元号一覧で調べても出てこない日本書紀に書かれていない元号らしいです。 つまり、日本の歴史上アンオフィシャルな元号であり、元号があったほうが便利なのに無くなってしまったので、民衆が便宜上勝手に元号を作って使っていたのでは? というものだそうです。 (これを「私元号」と呼ぶとのこと)

Appleはまさかそんなアンオフィシャルなものまでカバーしていたのでしょうか。

この白鳳はいったい、いつ白雉から改元されたことになっているのか調べてみます。

// 白雉から朱鳥までをログ出力してみる
for year in (654...686).reversed() {
    for month in (1...12).reversed() {
        let lastDay = Date.lastDayOfMonth(year, month)
        for day in (1...lastDay).reversed() {
            print(Date.factory(year, month, day).wareki)
        }
    }
}
:
:
白鳳元年1月3日
白鳳元年1月2日
白鳳元年1月1日
白雉22年12月31日
白雉22年12月30日
白雉22年12月29日
:
:

このように、白雉は22年間の後に西暦672年に白鳳に改元されたことになっています。

Wikipediaによると「中世以降の寺社縁起等では672年〜685年の期間を指すものもある」と書いてあるので、 改元の日付はわからないけども史実的には672年に切り替わったときに改元したことにしておこうという仕様なのでしょうか。

白鳳

出典: フリー百科事典『ウィキペディア(Wikipedia)』 なお、『続日本紀』 神亀元年冬十月条( 724年 )に「白鳳より以来、朱雀以前、年代玄遠にして、尋問明め難し。」といった記事がみられる。 梁の 劉勰『 文心雕龍 』「第四十八節知音篇」の以下の部分にもとづき、「白雉」を「白鳳」と言い換えたのではないかと考えられている。 原文 ...

ちなみに「白鳳文化」という言葉を学校で習ったことがあると思いますが、このあたりの年代の文化に当たります。

元号の空白(その2)

元号が途絶えたことはもう一度あります。先ほど出てきた「朱鳥」の後です。

「朱鳥」という元号自体はものすごく短く、およそ2ヶ月で終わってしまいます。 そして、そのあと「大宝」という元号になるまでまたもしばらく断絶してしまいます。

さて、このあたりはどのように扱われているのか見てみましょう。

朱鳥の終わりは西暦686年9月9日(ユリウス暦686年10月1日)となっています。

print(Date.factory(686, 9, 8).wareki) // この日に朱鳥が終わる
print(Date.factory(686, 9, 9).wareki)
print(Date.factory(686, 9, 10).wareki)
朱鳥元年9月8日
朱鳥元年9月9日
朱鳥元年9月10日

これまた白雉と同じく、元号が終わった後も朱鳥として扱われます。

次の「大宝」は西暦701年3月21日(ユリウス暦701年5月3日)に始まるとされていますので、その付近も調べてみます。

大宝 (日本)

出典: フリー百科事典『ウィキペディア(Wikipedia)』 大宝年間には完成した 大宝律令が施行され、都城としての 藤原京や 遣唐使派遣ならび、元号制定も 律令国家成立の一環として行われた。『 日本書紀』に拠れば、大宝以前にも 大化( 645年 - 650年)、 白雉( 650年 - 654年)、1年だけ存在した朱鳥( 686年 )などの年号があったとされるが、日本における元号制度は断絶状態にあり、「大宝」の改元により元号使用は再開される。以降、元号制度は途切れることなく現在に至るまで続いている。 。

print(Date.factory(701, 3, 20).wareki) 
print(Date.factory(701, 3, 21).wareki) // この日に大宝が始まる
print(Date.factory(701, 3, 22).wareki)
朱鳥16年3月20日
大宝元年3月21日
大宝元年3月22日

「朱鳥16年」というふうに出力されました。

史実としては2ヶ月で終わった元号が、16年も続いているということになります。 これは不具合なのか、それとも白鳳のように「私元号」として民衆に使われ続けていたのか。。

そこまで調べるのは大変なので歴史に詳しい方にお任せします。

4文字の元号

日本の元号といえば2文字というイメージですが、奈良時代では4文字の元号が使用されていました。

  • 天平感宝 749
  • 天平勝宝 749-757
  • 天平宝字 757-765
  • 天平神護 765-767
  • 神護景雲 767-770

(※数字は西暦)

2文字の前提で文字レイアウトを決めてしまうと、入りきらなくなってしまうので注意しましょう。

南北朝時代

日本は一度朝廷が真っ二つに分かれてしまった時代があります。 室町時代初期に始まった「南北朝時代」です。

南北朝時代 (日本)

南北朝時代(なんぼくちょう じだい)は、 日本の歴史区分の一つ。 1336年から 1392年までの57年間を指す。ただし、始期の日付から終期の日付までの期間は、56年弱である(後述)。 鎌倉時代と(狭義の) 室町時代に挟まれる時代で、広義の室町時代に含まれる。

南北朝時代は、後嵯峨天皇の後継者争いに端を発した室町幕府初代将軍・足利尊氏による光明天皇擁立と、そこに正統性を主張して対抗する後醍醐天皇の構図から始まります。 詳細に言及すると物凄い文章量になってしまうので割愛しますが、ここから朝廷は京都側の「北朝」と奈良吉野側の「南朝」に分かれてしまいます。

天皇が「北朝」と「南朝」に一人ずつ存在するという状態は元号にも影響を与えます。 それぞれの朝廷で別々の元号を使うことになるのです。

これを DateFormatterはどのように表現しているのでしょうか。

南北朝時代の元号

南北朝時代は「建武の新政」が起きた1336年から「明徳の和約」が行われた1392年までの57年間を指します。 史実上は以下のような元号の移り変わりがあります。

北朝 元号 期間 南朝 元号 期間
暦応 1338-1342 延元 1336-1340
康永 1342-1345 興国 1340-1347
貞和 1345-1350 正平 1347-1370
観応 1350-1352 建徳 1370-1372
文和 1352-1356 文中 1372-1375
延文 1356-1361 天授 1375-1381
康安 1361-1362 弘和 1381-1384
貞治 1362-1368 元中 1384-1392
応安 1368-1375
永和 1375-1379
康暦 1379-1381
永徳 1381-1384
至徳 1384-1387
嘉慶 1387-1389
康応 1389-1390
明徳 1390-1394

改元が行われるタイミングを取得

まずDateの extension に新たにプロパティ定義を追加します。

extension Date {
    
    // 西暦を取得する
    var year: Int {
        return Date.gregorianCalendar.component(.year, from: self)
    }

    // 元号のみを取得する
    var gengou: String {
        return convertDateString(
            format: "G",
            calendar: Date.japanCalendar,
            locale: Date.japanLocale
        )
    }
}

そして以下のように実装をしてみます。

1336年から1392年まで一日ずつループし、元号が変わったタイミングでのみ、その日付の西暦と和暦を出力するという仕組みです。

var currentGengou = ""
for year in (1336...1392) {
    for month in (1...12) {
        let lastDay = Date.lastDayOfMonth(year, month)
        for day in (1...lastDay) {
            let date = Date.factory(year, month, day)
            if currentGengou != date.gengou {
                print("\(date.year) \(date.wareki)")
                currentGengou = date.gengou
            }
        }
    }
}

そうすると以下のような結果になります。

1336 建武3年1月1日
1336 延元元年2月29日
1340 興国元年4月28日
1346 正平元年12月8日
1370 建徳元年7月24日
1372 文中元年4月1日
1375 天授元年5月27日
1379 康暦元年3月22日
1381 弘和元年2月10日
1384 元中元年4月28日
1387 至徳元年8月22日
1387 嘉慶元年8月23日
1389 康応元年2月9日
1390 明徳元年3月26日

(※ 改元日が出力されるが、1336年1月1日からfor文が始まるため、最初は「建武3年1月1日」が出力される)

「建武」の後に「延元」「興国」と続いているので南朝の元号が続いていますが、 南朝最後の「元中」が8年続く間に北朝の元号「至徳」などが入り込んでしまっています。

こうなってくると、どっちがどっちの元号なのか混乱が生じてしまいそうです。

このあたりを正確にやろうとすると、 両朝の元号をマッピングした上で、この期間だけは南北どちらなのかを指定して、適切な元号を返すようにしなくてはなりません。

その実装方法はさすがに需要もなさそうですし、面倒くさそうなので今回はやめておきます。

ここで言えることはこの時代の元号は混ざった状態で取得されるので、正確とは言い難いということでしょうか。

まとめ

DateDateFormatter を使った和暦取得で、日本の元号の悠久の旅をしてきました。

まとめると

  • Appleはすごく日本の元号を調べていて、おおよそ全ての改元日を正確にカバーしている
  • 最初の元号である「大化」だけは建元以前も含め、始まりの部分が曖昧になっている
  • 元号の断絶については正確ではなさそう。しかしアンオフィシャルな元号である「白凰」までカバーしていた
  • 元号には4文字のものが存在するので注意
  • 南北朝時代は南朝ベース。しかし北朝元号が混ざるなどの揺れがある

こんな感じでしょうか。

テックブログにしては日本史の話が長かったですね(笑)

ではでは