iOSと新しい元号

これはiOSアドベントカレンダーの2日目の記事です。

毎年アドベントカレンダーでは、カレンダーに関係あることを書くことにしています。 今回は来年5月の改元。 iOS始まって以来の改元ですね。

改元前にバグになりそうなポイントを確認しておきましょう。

元号

この業界にいると元号って年に数回くらいしか使わないですよね。 私は使うタイミングになるたびに毎回毎回「今年って平成何年だっけ?」って繰り返していて、結局平成が終わるまで、「今年って平成何年だっけ?」から卒業できませんでした。 きっと次の元号でもずっとそうなんだろうなと思います。

新しい元号になるのは2019年5月。 2019年4月30日までは「平成」、2019年5月1日から新しい元号が適用されます。

iOSエンジニアは改元でなにをすべきか

さて、この改元にともなってiOSエンジニアがすべきことはなんでしょうか。

iOSで和暦がUIに表示されるのは、ユーザーが設定の「一般」>「言語と地域」>「カレンダー」で和暦を選んでいる時だけです。

これを設定すると、カレンダーアプリなどの日付の表示が和暦になり、DatePickerで年を選ぶときにも西暦ではなく和暦の年が表示されるようになります。 レイアウト上の都合からユーザーが和暦を選択していても西暦で表示しているアプリも多いので、和暦を設定していても、表示上は関係ないアプリも多いと思います。

元号についての処理は、FoundationフレームワークのCalendarに含まれています。 元号が正式に決定されたあとの iOSのバージョンアップで、新元号が含まれたフレームワークになるので、基本的にはエンジニアがしなければいけないことはなにもありません。

ただし、日時の計算で正しくCalendarクラスを使っていないとバグが発生することがあります。

改元のタイミングで発生しがちなバグ

改元のタイミングで発生しがちなバグを、昭和から平成になったときの場合で確認してみましょう。

昭和から平成への移行、私は記憶があるんですけど、たぶんこの記事を読んでいる人は覚えていない人の方が(というかそもそも生まれていない人の方が)多いんでしょうね……。

1989年のお正月早々の1月7日に、昭和天皇が崩御しました。 年末から天皇陛下の具合がだいぶ悪いという話だったのでそのこと自体に驚く人はいませんでしたが、やはり時代の節目として社会に大きな衝撃をもたらしました。 すぐに新しい元号の「平成」が発表され、1989年の1月7日まで昭和、1月8日から平成になりました。

さて、このときの DateComponentの処理を見てみましょう。 例として、1/7の3日後の日付を取得する処理を書いてみます。

let currentCalendar = Calendar(identifier: .gregorian)

// 1989年1月7日のDateです。
let now_19890107 = Date(timeIntervalSinceReferenceDate: -378172800)

// 上のDateからDateComponentを作ります。
let nowComponent = currentCalendar.dateComponents([.year,.month,.day], from: now_19890107)

let year = (nowComponent.year)!
let month = (nowComponent.month)!
let day = (nowComponent.day)!

// 上のDateComponentの3日後のDateComponentを作ります。
let newDay = day + 3
var newComponent = DateComponents()
newComponent.year = year
newComponent.month = month
newComponent.day = newDay

// 上で作ったDateComponentから3日後のDateを作成
let newDate = currentCalendar.date(from: newComponent)
print("newDate:\(String(describing: newDate))")

DateからDateComponentsを作成し、それに3を加えて新しいDateComponentsを作り、それをDateにして 計算結果はこうなります。

newDate:Optional(1989-01-10 00:00:00 +0000)

1/7の3日後なので、1/10になってしますね。

さて、上記の計算を和暦でやってみましょう。 次のコードは、上のコードの1行目のCalendarを変えただけです。(あとはコピペです)

let currentCalendar = Calendar(identifier: .japanese)

// 1989年1月7日のDateです。
let now_19890107 = Date(timeIntervalSinceReferenceDate: -378172800)

// 上のDateからDateComponentを作ります。
let nowComponent = currentCalendar.dateComponents([.year,.month,.day], from: now_19890107)

let year = (nowComponent.year)!
let month = (nowComponent.month)!
let day = (nowComponent.day)!

// 上のDateComponentの3日後のDateComponentを作ります。
let newDay = day + 3
var newComponent = DateComponents()
newComponent.year = year
newComponent.month = month
newComponent.day = newDay

// 上で作ったDateComponentから3日後のDateを作成
let newDate = currentCalendar.date(from: newComponent)
print("newDate:\(String(describing: newDate))")

さて、上記の結果はこうなります。

newDate:Optional(2052-01-10 00:00:00 +0000)

上記の計算は、わかりやすくというと昭和64年1月7日の3日後を平成64年1月10日で計算しちゃった感じですね。

Calendarオブジェクトを作るときに、下記のようにcurrentの要素を使ってしまうと、端末で設定しているカレンダーとなります。 端末のカレンダーを西暦にしているユーザーのところでは予想通りに動作しますが、端末を和暦にしているユーザーのところでは正しい日付が取れません。

let currentCalendar = Calendar.current

たとえアプリの表示に和暦要素がなくても、日付系の計算をするときに、Calendar.currentを使っていたらバグが発生する可能性があります。 日付計算をするときには、Calendar.currentは絶対に使わないこと。 改元まで時間がある今の時期に、あらためて確認してみましょう。

でも実は上の計算、わざわざDateComponentsのプロパティを操作する必要はないんです。 下記のように、CalendarクラスにDateComponentsベースで加算減算を行えるメソッドがあるので、これを使えば currentCalendarが西暦でも和暦でも問題はでません。 (この方が短くて済みますし。)

currentCalendar.date(byAdding: .day, value: 3, to: now_19890107)

和暦の元号

和暦の元号はCalendarクラスのなかにどのように保存されているんでしょうか。

CalendarにerasとlongErasというプロパティがあり、このなかにStringの配列として保存されています。 (和暦の場合にはerasとlongErasの中身は同じです。)

let eras = japaneseCalendar.eraSymbols
let longEras = japaneseCalendar.longEraSymbols

このStringの配列は、Localeが日本語だとこちら。

["大化", "白雉", "白鳳", "朱鳥", "大宝", "慶雲", "和銅", "霊亀", "養老", "神亀", "天平", "天平感宝", "天平勝宝", "天平宝字", "天平神護", "神護景雲", "宝亀", "天応", "延暦", "大同", "弘仁", "天長", "承和", "嘉祥", "仁寿", "斉衡", "天安", "貞観", "元慶", "仁和", "寛平", "昌泰", "延喜", "延長", "承平", "天慶", "天暦", "天徳", "応和", "康保", "安和", "天禄", "天延", "貞元", "天元", "永観", "寛和", "永延", "永祚", "正暦", "長徳", "長保", "寛弘", "長和", "寛仁", "治安", "万寿", "長元", "長暦", "長久", "寛徳", "永承", "天喜", "康平", "治暦", "延久", "承保", "承暦", "永保", "応徳", "寛治", "嘉保", "永長", "承徳", "康和", "長治", "嘉承", "天仁", "天永", "永久", …, "宝暦", "明和", "安永", "天明", "寛政", "享和", "文化", "文政", "天保", "弘化", "嘉永", "安政", "万延", "文久", "元治", "慶応", "明治", "大正", "昭和", "平成"]

英語だとこちらの値が入っています。

["Taika (645–650)", "Hakuchi (650–671)", "Hakuhō (672–686)", "Shuchō (686–701)", "Taihō (701–704)", "Keiun (704–708)", "Wadō (708–715)", "Reiki (715–717)", "Yōrō (717–724)", "Jinki (724–729)", "Tenpyō (729–749)", "Tenpyō-kampō (749–749)", "Tenpyō-shōhō (749–757)", "Tenpyō-hōji (757–765)", "Tenpyō-jingo (765–767)", "Jingo-keiun (767–770)", "Hōki (770–780)", "Ten-ō (781–782)", "Enryaku (782–806)", "Daidō (806–810)", "Kōnin (810–824)", "Tenchō (824–834)", "Jōwa (834–848)", "Kajō (848–851)", "Ninju (851–854)", "Saikō (854–857)", "Ten-an (857–859)", "Jōgan (859–877)", "Gangyō (877–885)", "Ninna (885–889)", "Kanpyō (889–898)", "Shōtai (898–901)", "Engi (901–923)", "Enchō (923–931)", "Jōhei (931–938)", "Tengyō (938–947)", "Tenryaku (947–957)", "Tentoku (957–961)", "Ōwa (961–964)", "Kōhō (964–968)", "Anna (968–970)", "Tenroku (970–973)", "Ten’en (973–976)", "Jōgen (976–978)", "Tengen (978–983)", "Eikan (983–985)", "Kanna (985–987)", "Eien (987–989)", "Eiso (989–990)", "Shōryaku (990–995)", "Chōtoku (995–999)", "Chōhō (999–1004)", "Kankō (1004–1012)", "Chōwa (1012–1017)", "Kannin (1017–1021)", "Jian (1021–1024)", "Manju (1024–1028)", "Chōgen (1028–1037)", "Chōryaku (1037–1040)", "Chōkyū (1040–1044)", "Kantoku (1044–1046)", "Eishō (1046–1053)", "Tengi (1053–1058)", "Kōhei (1058–1065)", "Jiryaku (1065–1069)", "Enkyū (1069–1074)", "Shōho (1074–1077)", "Shōryaku (1077–1081)", "Eihō (1081–1084)", "Ōtoku (1084–1087)", "Kanji (1087–1094)", "Kahō (1094–1096)", "Eichō (1096–1097)", "Jōtoku (1097–1099)", "Kōwa (1099–1104)", "Chōji (1104–1106)", "Kashō (1106–1108)", "Tennin (1108–1110)", "Ten-ei (1110–1113)", "Eikyū (1113–1118)", …, "Hōreki (1751–1764)", "Meiwa (1764–1772)", "An’ei (1772–1781)", "Tenmei (1781–1789)", "Kansei (1789–1801)", "Kyōwa (1801–1804)", "Bunka (1804–1818)", "Bunsei (1818–1830)", "Tenpō (1830–1844)", "Kōka (1844–1848)", "Kaei (1848–1854)", "Ansei (1854–1860)", "Man’en (1860–1861)", "Bunkyū (1861–1864)", "Genji (1864–1865)", "Keiō (1865–1868)", "Meiji", "Taishō", "Shōwa", "Heisei"]

上記では途中省略されていますが、どちらも236個のStringが入っています。 来年の5月にはこの配列の要素数が一つ増えるわけですね。

上記の一番古い要素は「大化」です。

Wikipediaを見ると一番古い年号が大化なので、それに従っています。 元号一覧 (日本) - Wikipedia

大化の改新で天皇中心の政治になったと同時に元号制が始まったわけですね。

でも、ちょっとDatePickerで確認してみたところ、DatePickerに表示される一番古い年号って"白雉"になってるんですよね。 eraには"大化"も入っているのになんで表示されないんでしょう……?

2013年のアドベントカレンダーでもCalendarネタをとりあげていたんですが、その時にはDatePickerで"大化"が表示されているのは確認してるんですよね。

www.toyship.org

ちょっと謎……。