ISO8601とUnix Timestamp


これは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フォーマットを使う場合には、カレンダーの設定を忘れないようにしてくださいね。