これは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 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 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を設定しましょう。
これで、
“2016-12-02T03:34:56Z”
を取得することができます。
iOS10以上で使えるISO8601 Format
さて、iOS10になって、APIにISO8601 Formatterが追加されました。
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:60Z"
"2015-07-01T00:00:00Z"
の順番に時計がかわっていました。
これをunix timestampでみてみましょう。
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フォーマットを使う場合には、カレンダーの設定を忘れないようにしてくださいね。