Swift6時代の文字列→日時変換
はじめに
SwiftのDate.ISO8601FormatStyleについて書きます。
これは、iOS15から追加された日時と文字列を変換するためのユーティリティです。String
とDate
を相互に変換できます。
しばらくApple系OSでしか使えませんでしたが、Swift6で晴れてLinuxなど他OSでも利用できるようになり、実質的にSwiftの新しい標準APIの1つとなりました。
同時に出たAPIに、Date.FormatStyleがあります。似たAPIですが、利用シーンは明確に分かれると思っています。
- 文字列→日時の変換をするケースは、多くは外部APIから受け取ったデータのパースだと思います。この際、フォーマットとしてISO8601は広く利用されています。
- 日時→文字列の変換をするケースは、多くがアプリケーションが画面上に日時を表示するためだと思います。この際のフォーマットは、アプリケーションの見た目に合わせて柔軟な形式が求められるでしょう。
このため、ISO8601FormatStyle
を文字列→日時変換に使って、FormatStyle
は日時→文字列変換で利用されることが多いと思います。
本記事では文字列から日時への変換のみにフォーカスし、ISO8601FormatStyle
のみ取り扱います。
使い方
import Foundation
// 通常
let _ = try Date(
"2021-12-01T10:00:00Z",
strategy: .iso8601
)
// 浮動小数点あり
let _ = try Date(
"2021-12-01T10:00:00.000Z",
strategy: .iso8601.year().month().day().time(includingFractionalSeconds: true).timeZone(separator: .omitted)
)
// タイムゾーン指定を忘れないように注意。忘れると以下のようにズレる
let _ = try Date(
"2021-12-01T10:00:00.000+09:00",
strategy: .iso8601.year().month().day().time(includingFractionalSeconds: true)
)
// >> "Dec 1, 2021 at 19:00" in JST
let _ = try Date(
"2021-12-01T10:00:00.000+09:00",
strategy: .iso8601.year().month().day().time(includingFractionalSeconds: true).timeZone(separator: .omitted)
)
// >> "Dec 1, 2021 at 10:00" in JST
// Date.ISO8601FormatStyleを直接initする方法でも可
let _ = try Date(
"2021-12-01T10:00:00.000Z",
strategy: Date.ISO8601FormatStyle(includingFractionalSeconds: true)
)
// 日付だけ読む場合
let _ = try Date("2021-12-01", strategy: .iso8601.year().month().day())
.iso8601
を呼び出した時点では、デフォルトで日時とタイムゾーンを読み取る設定になっています。
しかし、year()
month()
day()
らを1つでも呼び出した場合、内部的に状態が切り替わり、呼び出したものについてスキャンする方式に変化します。
デフォルトで含まれるコンポーネントは以下です。
ちょっとややこしいですよね。
ISO8601DateFormatter
との違い
以前からあるAPIにISO8601DateFormatter
があります。
こちらは古いAPIで、速度・並行安全性共に劣っています。使える状況であれば積極的に新しいものを利用しましょう。
Sendable | 速度 | 備考 | |
---|---|---|---|
ISO8601DateFormatter |
× | 低速 | 昔からある |
Date.ISO8601FormatStyle |
⚪︎ | 高速 | 古い環境では利用できない |
簡易ベンチマーク
ベンチマークコード
import Foundation
let iso8601String = "2023-10-09T16:34:23Z"
let iterations = 1000
// ISO8601DateFormatter
let iso8601DateFormatter = ISO8601DateFormatter()
var iso8601DateFormatterTime: TimeInterval = 0
for _ in 0..<iterations {
let start = Date()
_ = iso8601DateFormatter.date(from: iso8601String)
let end = Date()
iso8601DateFormatterTime += end.timeIntervalSince(start)
}
// Date.ISO8601FormatStyle
var iso8601FormatStyleTime: TimeInterval = 0
for _ in 0..<iterations {
let start = Date()
_ = try? Date(iso8601String, strategy: .iso8601)
let end = Date()
iso8601FormatStyleTime += end.timeIntervalSince(start)
}
print("ISO8601DateFormatter Time: \(iso8601DateFormatterTime) seconds")
print("Date.ISO8601FormatStyle Time: \(iso8601FormatStyleTime) seconds")
結果:
- Linuxの場合
$ swift --version
Swift version 6.0.3 (swift-6.0.3-RELEASE)
Target: x86_64-unknown-linux-gnu
$ swift -O test.swift
ISO8601DateFormatter Time: 0.6819443702697754 seconds
Date.ISO8601FormatStyle Time: 0.002825140953063965 seconds
- macOSの場合
$ swift --version
Apple Swift version 6.0.3 (swiftlang-6.0.3.1.10 clang-1600.0.30.1)
Target: arm64-apple-macosx15.0
$ swift -O test.swift
ISO8601DateFormatter Time: 0.03178668022155762 seconds
Date.ISO8601FormatStyle Time: 0.0003638267517089844 seconds
ケタ違いに速いことが一目瞭然ですね。
なんで速いの?
ISO8601DateFormatter
は裏でC言語のCoreFoundationに処理を委譲しています。
CoreFoundationの中ではおそらく汎用的なICUのカレンダー処理に委譲されているのだと思います。
Date.ISO8601FormatStyle
は中身がすべてSwiftで記述されており、パース処理が専用に書き下されています。またカレンダーもグレゴリオ暦のみを前提として特殊化されています。
用途を限定することで高速な処理が記述できているということですね。
よくある落とし穴
浮動小数点のありなしを意識せずにデコードしたい
できません。
ISO8601DateFormatter
のときと同様、浮動小数点の有無は厳密に区別されます。
よってありなし両方に対応した処理を記述したければ、異なる設定でパースを2回を走らせる必要があります。
浮動小数点をつける操作が長いよ〜
.iso8601.year().month().day().time(includingFractionalSeconds: true).timeZone(separator: .omitted)
も、Date.ISO8601FormatStyle(includingFractionalSeconds: true)
も、補完を考慮しても長いですよね。
実はISO8601FormatStyle
はRegexとしても機能する上に、Regexとして生成する用のextensionまであります。それを使うと.iso8601WithTimeZone(includingFractionalSeconds: true)
として記述できます。
let _: Date? = "2021-12-01T10:00:00.000Z".wholeMatch(
of: .iso8601WithTimeZone(includingFractionalSeconds: true)
)?.output
ただし、この方法はRegexの処理が挟まってしまうので遅くなってしまいます。私の手元では10倍の時間がかかりました。
素直に自分でエイリアスを生やしたほうが良さそうですね。
Discussion