これはiOS Advent Calendar 2016の12/2の記事です。 アドベントカレンダーはDate/Calendarネタを描こうと思っているので、今年はいろいろな現場で必ず目にする日付フォーマットのバグについて。 ある程度iOSをやっている方には当たり前の話なんですが、なぜかこのバグはいろんなところで見かけるんですよね……。 最近iOSをはじめた人は是非覚えておいてくださいね。
日時情報フォーマット
みなさん、APIに日付を含まれる場合、どんなフォーマットにしているでしょうか。
一般的に使われるのは、ISO8601かUnix timestamp。 ISO8601はWebの世界で標準的に使われている日時を表す文字列、Unix timestampは1970/1/1からの秒数を表す数値です。 ISO8601は文字列なので、ぱっと見て日時を識別できるのが利点であり、Unix timestampは変換時のバグが処理時間が少ないのが、利点です。
生のログを見るときにはISO8601は視認性がいいですが、その利点をうわまわるバグが発生していると思うので、私は自分でAPIの設計をするときには、必ず Unix timestamp を使うようにしています。
Webの世界でも、新しいAPIには、Unix timestampが多いようです。 (例えばTwitter APIはISO 8601形式、SlackのAPIなどはUnix timestampになっています。)
iOSでの正しい ISO 8601 Format
さて、iOSでISO8601文字列を作るときにはどうすればいいでしょうか。 ISO8601 iOS でググるといろいろ情報がでてきますが、だいたいこんな感じのものがでてくるかと思います。
let targetDate = Date() // 変換対象日時 let myISO8601Formatter = DateFormatter() myISO8601Formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ" let result = myISO8601Formatter.string(from: targetDate) // ISO8601形式の文字列(と思われるもの)
実は、これをそのまま実装してしまうとバグになる場合が多いです。
日付情報を文字列にする場合、対象となるカレンダーとタイムゾーンを指定する必要があるんですが、上記のようになにも設定しないとシステム設定のカレンダーとタイムゾーンが使われてしまいます。
このコードを実行すると、iOSのシステム設定のカレンダーがグレゴリオ暦になっている場合には "2016-12-02T12:34:56+09:00" が表示されますが、和暦になっている場合には "0028-12-02T12:34:56+09:00" が表示されます。
(もちろん、iOSにはグレゴリオ暦、和暦以外のカレンダーも入っているので、その場合にも同じように失敗します。)
正しいISO8601変換をする場合には、DateFormatterにカレンダーを指定しなくてはいけません。
let targetDate = Date() // 変換対象日時 let myISO8601Formatter = DateFormatter() let gregorianCalendar = Calendar(identifier: Calendar.Identifier.gregorian) myISO8601Formatter.calendar = gregorianCalendar myISO8601Formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ" let result = myISO8601Formatter.string(from: targetDate) // ISO8601形式の文字列
これで、システム設定のカレンダーがどれに設定されていても正しいISO8601文字列が取得できます。
なお、ISO8601のフォーマットには、タイムゾーンがGMT(グリニッジ標準時)の場合と、ローカルのタイムゾーンの場合とがありますが、上記ではローカルのタイムゾーンのISO8601となります。
GMTのISO8601を取得したい場合、下記のようにtimeZoneを設定しましょう。
myISO8601Formatter.timeZone = TimeZone(abbreviation: "GMT")
これで、 "2016-12-02T03:34:56Z" を取得することができます。
iOS10以上で使えるISO8601 Format
さて、iOS10になって、APIにISO8601 Formatterが追加されました。
let targetDate = Date() let formatter = ISO8601DateFormatter() let result = formatter.string(from: targetDate) // ISO8601形式の文字列
このFormatterのいいところは、CalendarやTimezoneを設定することができなくなっているため、どんな環境でも同じ動作が保証されることです。 たとえクライアントのカレンダーが和暦になっていても、正しいISO 8601 Formatを表示することができます。
動作環境をiOS10以上にすることができるのであれば、このFormatterを使うのが一番いいでしょう。
ISO8601でUnix timestampで対応できない日時とは
さて、日時フォーマットにはISO8601よりUnix Timestampの方が圧倒的におすすめなんですが、実はUnix timestampでは表示できない時があります。
それは、地球の自転から導き出された、UT(世界時)とUTC(協定世界時)の差が生じた時、いわゆるうるう秒です。
Unix timestampは原子由来のいわば計算上の時刻なので、どうしても実際の天体の動きとはずれてしまうんですね。 そのずれを補正するために、何年かに一度うるう秒というものが挿入されます。
前回のうるう秒は2015/7/1でした。 そのときには、ISO8601で表示すると、
"2015-06-30T23:59:59Z" "2015-06-30T23:59:60Z" "2015-07-01T00:00:00Z"
の順番に時計がかわっていました。
これをunix timestampでみてみましょう。
// 2015/6/30 23:59 var dateComp20150630235959 = DateComponents() dateComp20150630235959.calendar = calendar dateComp20150630235959.year = 2015 dateComp20150630235959.month = 6 dateComp20150630235959.day = 30 dateComp20150630235959.hour = 23 dateComp20150630235959.minute = 59 dateComp20150630235959.second = 59 dateComp20150630235959.date?.timeIntervalSince1970 // => 1435676399 // 2015/6/30 23:60 var dateComp20150630235960 = DateComponents() dateComp20150630235960.calendar = calendar dateComp20150630235960.year = 2015 dateComp20150630235960.month = 6 dateComp20150630235960.day = 30 dateComp20150630235960.hour = 23 dateComp20150630235960.minute = 59 dateComp20150630235960.second = 60 dateComp20150630235960.date?.timeIntervalSince1970 // => 1435676400 // 2015/7/1 00:00:00 var dateComp20150701000000 = DateComponents() dateComp20150701000000.calendar = calendar dateComp20150701000000.year = 2015 dateComp20150701000000.month = 7 dateComp20150701000000.day = 1 dateComp20150701000000.hour = 0 dateComp20150701000000.minute = 0 dateComp20150701000000.second = 0 dateComp20150701000000.date?.timeIntervalSince1970 // => 1435676400
というように、6/30 23:60と7/1 00:00が同じunix timestam値(1435676400)となってしまい、この二つの日時をわけて認識することができません。
うるう秒自体がUTC(計算された時刻)とUT(天体の時刻)の差を補完するものなので、UTCをもとに設定されているunix timestampで表現できないんですね。
つぎのうるう秒は、来年の1月、2017/1/1。 「うるう秒」挿入のお知らせ 2016/12/31 23:59:59のあとに、2016/12/31 23:59:60という、通常にはない1秒が挿入されます。
まとめ
うるう秒が発生するときには、unix timestampでは表示できませんが、個人的なおすすめは、やっぱりバグがでない unix timestampです。 どうしてもISO8601フォーマットを使う場合には、カレンダーの設定を忘れないようにしてくださいね。