【Swift】Date ⇔ String 変換

はじめに

何かと忘れやすい(主に自分が)DateとStringの相互変換についてまとめます。
– Swift編 –

コードの細かい解説は他のサイト様にまかせて、目的別にすぐ使えるように書きたいと思います。

動作確認の環境は以下の通りです。

  • macOS: Mojave (10.14.6)
  • Xcode: 11.3
  • Swift: 5

まずはここから、DateFormatterを用意

// Date ⇔ Stringの相互変換をしてくれるすごい人
let dateFormatter = DateFormatter()

日付や時刻の扱いで気を付けるべき重要要素

※以下、少々長くなるため、実際の変換コードだけ見たい人は上の目次から飛んでください。

カレンダー(暦法)

カレンダーは の扱いに影響します。

端末(iPhoneやiPadなど)では設定アプリの「一般 > 言語と地域 > 暦法」から設定可能で、手元のiPhoneでは 西暦(グレゴリオ暦)和暦タイ仏暦 の3つから選択できます。

例えば「2020」という文字列を年としてDate型に変換しようとした場合、カレンダーの設定が「西暦」であれば正しく「2020年」として変換することができます。

ですが、もし「和暦」になっていたとすると、「令和2020年」として解釈され、これを西暦になおすと「西暦4038年」ととんでもない値になってしまいます。

そのため、プログラムではこの端末のカレンダー設定により意図しない変換が行われないよう、設定を固定化して処理するのが一般的です。

具体的には以下のように設定します。

// 端末設定によらず西暦(グレゴリオ暦)で処理するようにする
dateFormatter.calendar = Calendar(identifier: .gregorian)

// 意図して和暦扱いしたい場合は .japanese を指定
dateFormatter.calendar = Calendar(identifier: .japanese)

ロケール(言語と地域)

ロケールは 表記に使用する言語日付や時刻の書き順 に影響します。

言語はそのままズバリ、曜日を「金曜日」と書くのか、「Friday」と書くのかや、時分秒の区切りを「11時22分33秒」のように書くか、単に「11:22:33」のようにコロン : で区切って書くのか、といったところの設定になります。

地域は、日本なら日付の並びは「年、月、日、曜日」ですが、アメリカでは「曜日、月、日、年」、イギリスでは「曜日、日、月、年」になります。
こうした地域固有の表記の仕方に影響するのがこの設定になります。

※ただし、プログラムで日付や時刻の並びを自分で指定する固定フォーマットで処理する場合は、この地域設定の影響は受けません。

端末では設定アプリの「一般 > 言語と地域 > iPhoneの使用言語」(iPhoneの場合)から言語を、「一般 > 言語と地域 > 地域」から地域を設定することができます。

カレンダーと同様、端末の設定に振り回されたくない場合はプログラムで固定化して処理します。
(日本国内向けで、主に日本語のわかる人をターゲットにしたアプリを開発するケースなど)

具体的には以下のように設定します。

// 日本語、日本(地域)に固定
dateFormatter.locale = Locale(identifier: "ja_JP")

// 英語、アメリカに固定
dateFormatter.locale = Locale(identifier: "en_US")

// 将来的に不変が約束された特別なロケール
dateFormatter.locale = Locale(identifier: "en_US_POSIX")

// 意図して端末設定に合わせたい場合
dateFormatter.locale = Locale.current

en_US_POSIX

これは、Appleが将来的に設定を変えないことを保証している唯一特別なロケールになります。

現在、英語・アメリカの書き順は上記のとおりですが、もしかすると将来変更される可能性があります。

その場合、en_US はその影響を受けますが、en_US_POSIX は変わらず今のままを保持してくれます。

また、このロケールを設定すると端末のカレンダー設定も無視してくれるため、端末で「和暦」に設定していたとしても、「2020」は西暦として解釈してくれます。

そのため、カレンダー設定によらず西暦で処理したい場合(特に String ⇒ Date の変換において)は、dateFormatter.calendar を変えるよりも、まずこのロケールの設定を行うことが推奨されます。

(詳しくはこちら

【 Locale.current 】

Xcodeで開発する上において注意したいのが、開発言語設定です。

Locale.current は現在の端末設定を表しますが、普通にXcodeを起動して新規プロジェクトを作った場合、言語と地域の設定でどちらも日本語、日本に設定していたとしても、これで得られるロケールは en_JP になります。

iOS10以前は ja_JP で取れていましたが、iOS11から仕様が変更されたようで、Xcodeの開発言語設定(Localizations)に該当する言語がないと、デフォルトの言語(Base。未設定状態だとEnglishとイコール)が選択されてしまいます。

Xcodeの開発言語設定

↑のようにJapaneseを追加することで、言語の設定に合わせて ja_JP が得られるようになります。

タイムゾーン(時間帯)

タイムゾーンは 時差 に影響します。

端末では設定アプリの「一般 > 日付と時刻 > 時間帯」から設定可能です。

※ただし、通常は時刻の自動合わせをオンにしているため、手動でこれを変更することはそうそうないかと思われます。

時差とは言わずもがな、世界標準時(協定世界時 [UTC]、もしくはグリニッジ平均時 [GMT])とその地域の標準時との時間差のことです。

iOS SDKでは、Date型のオブジェクトはカレンダーやタイムゾーンの設定に関係なく、内部では単に 2001/01/01 00:00:00 UTC を 0.0(基準)とした秒数差の数値でデータを保持しています。
(詳しくはこちら

では日本標準時(JST)で日付を画面表示させたい場合、この数値に9時間分の秒数(9hour * 60min * 60sec = 32,400)を足すのかと言えば、そうではありません。

あくまでDateオブジェクトの中身はそのままで、DateからStringに変換する際に、この時差を加味してくれる設定をDateFormatterに与えます。
(逆のStringからDateも然り)

具体的には以下のように設定します。

// 日本標準時(地域名で指定)
dateFormatter.timeZone = TimeZone(identifier: "Asia/Tokyo")

// 日本標準時(省略名で指定)
dateFormatter.timeZone = TimeZone(abbreviation: "JST")

// UTC+9(世界標準時からの秒数差で指定)
dateFormatter.timeZone = TimeZone(secondsFromGMT: 9 * 60 * 60)

↑こうしておくことで、0.0(2001/01/01 00:00:00 UTC相当)の数値を持ったDateオブジェクトでも、Stringに変換すると「2001/01/01 09:00:00」というような値を返してくれるようになります。

【 サマータイム 】

タイムゾーンを設定する際、気を付けたいのがサマータイムの存在です。
(サマータイムについてはWikiなどで)

以下の3つ
・TimeZone(identifier: “Asia/Tokyo”)
・TimeZone(abbreviation: “JST”)
・TimeZone(secondsFromGMT: 9 * 60 * 60)
はイコールのように思われますが、実際はそうではありません。

identifierabbreviation で設定すると、返されるTimeZoneオブジェクトはその地域のサマータイムが考慮されたものとなりますが、secondsFromGMT で設定すると、単にGMT(UTC)からの時間差だけを意識したものとなります。

そのため、上記の上2つでもって変換処理を行うと、サマータイムにかかる期間では時差がUTC+10として計算されたり、「1951/05/06」のString ⇒ Date変換が失敗したりします。

これは、1951/05/06 はサマータイム期間の開始日であり、実質1日が23時間になるため、この日の最初の1時間、つまり 00:00:00 〜 00:59:59 に相当する日本時間というのは存在しない扱いになるためのようです。
(こちらの記事を参考にさせていただきました)

よって、処理の要件により、サマータイムを考慮する必要があるかどうかで設定方法を選んで頂く必要があります。

Date ⇒ String 変換

APIのGETパラメータ、XMLやJSONなどのデータ用に

区切りなし

// フォーマット設定
dateFormatter.dateFormat = "yyyyMMddHHmmss"
//dateFormatter.dateFormat = "yyyyMMddHHmmssSSS" // ミリ秒込み

// ロケール設定(端末の暦設定に引きづられないようにする)
dateFormatter.locale = Locale(identifier: "en_US_POSIX")

// タイムゾーン設定(端末設定によらず固定にしたい場合)
dateFormatter.timeZone = TimeZone(identifier: "Asia/Tokyo")

// 変換
let str = dateFormatter.string(from: Date())

// 結果表示
print(str) // -> 20200108170511

ISO8601形式

// フォーマット設定
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssXXX" // 拡張形式
//dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX" // ミリ秒込み
//dateFormatter.dateFormat = "yyyyMMdd'T'HHmmssXX" // 基本形式

// ロケール設定(端末の暦設定に引きづられないようにする)
dateFormatter.locale = Locale(identifier: "en_US_POSIX")

// タイムゾーン設定(端末設定によらず固定にしたい場合)
dateFormatter.timeZone = TimeZone(identifier: "Asia/Tokyo")

// 変換
let str = dateFormatter.string(from: Date())

// 結果表示
print(str) // -> 2020-01-08T17:53:10+09:00

ISO8601形式(iOS10.0以上)

// iOS10.0以上ならズバリのフォーマッターが使える
let iso8601DateFormatter = ISO8601DateFormatter()
//iso8601DateFormatter.formatOptions.insert(.withFractionalSeconds) // ミリ秒を含める
//iso8601DateFormatter.formatOptions = [.withYear, .withMonth, .withDay, .withTime, .withTimeZone] // 区切りなし

// タイムゾーン設定(端末設定によらず固定にしたい場合)
iso8601DateFormatter.timeZone = TimeZone(identifier: "Asia/Tokyo")

// 変換
let str = iso8601DateFormatter.string(from: Date())

// 結果表示
print(str) // -> 2020-01-08T17:53:10+09:00

画面表示など、人が見る用に

スラッシュ、コロン区切り

// フォーマット設定
dateFormatter.dateFormat = "yyyy/MM/dd HH:mm:ss"

// ロケール設定(端末の暦設定に引きづられないようにする)
dateFormatter.locale = Locale(identifier: "en_US_POSIX")

// タイムゾーン設定(端末設定によらず固定にしたい場合)
dateFormatter.timeZone = TimeZone(identifier: "Asia/Tokyo")

// 変換
let str = dateFormatter.string(from: Date())

// 結果表示
print(str) // -> 2020/01/09 17:18:01

日本語 / 国内向け / 西暦表示

// フォーマット設定
dateFormatter.dateFormat = "yyyy'年'M'月'd'日('EEEEE') 'H'時'm'分's'秒'" // 曜日1文字
//dateFormatter.dateFormat = "M'月'd'日 ('EEEE')'" // 曜日3文字

// ロケール設定(日本語・日本国固定)
dateFormatter.locale = Locale(identifier: "ja_JP")

// タイムゾーン設定(日本標準時固定)
dateFormatter.timeZone = TimeZone(identifier: "Asia/Tokyo")

// 変換
let str = dateFormatter.string(from: Date())

// 結果表示
print(str) // -> 2020年1月9日(木) 18時29分19秒

日本語 / 国内向け / 和暦表示

// フォーマット設定
dateFormatter.dateFormat = "Gy'年'M'月'd'日"

// カレンダー設定(和暦固定)
dateFormatter.calendar = Calendar(identifier: .japanese)

// ロケール設定(日本語・日本国固定)
dateFormatter.locale = Locale(identifier: "ja_JP")

// タイムゾーン設定(日本標準時固定)
dateFormatter.timeZone = TimeZone(identifier: "Asia/Tokyo")

// 変換
let str = dateFormatter.string(from: Date())

// 結果表示
print(str) // -> 令和2年1月9日

国際対応 / 自動フォーマット その1

// フォーマット設定(テンプレートで指定)
// fromTemplateに欲しい要素を入れれば、指定したlocaleに応じて良いようにしてくれる
// ここでは端末設定に従う(Locale.current)。
dateFormatter.dateFormat = DateFormatter.dateFormat(fromTemplate: "yyyyMMMdEEEHHmmss", options: 0, locale: Locale.current)

// 自動フォーマットで日本語固定にしたい場合は、以下のようにdateFormatとlocaleの2箇所で日本語設定を入れる
//dateFormatter.dateFormat = DateFormatter.dateFormat(fromTemplate: "yyyyMMMdEEEHHmmss", options: 0, locale: Locale(identifier: "ja_JP"))
//dateFormatter.locale = Locale(identifier: "ja_JP")

// カレンダー設定(グレゴリオ暦固定)
dateFormatter.calendar = Calendar(identifier: .gregorian)

// 変換
let str = dateFormatter.string(from: Date())

// 結果表示
print(str) // -> Thu, Jan 9, 2020 AD, 23:27:18

国際対応 / 自動フォーマット その2

// フォーマット設定(日付と時刻をスタイルで指定。それぞれ none, short, medium, long, full の5パターンから指定できる)
dateFormatter.dateStyle = .medium
dateFormatter.timeStyle = .medium

// カレンダー設定(グレゴリオ暦固定)
dateFormatter.calendar = Calendar(identifier: .gregorian)

// 変換
let str = dateFormatter.string(from: Date())

// 結果表示
print(str) // -> Jan 9, 2020 at 11:31:41 PM

String ⇒ Date 変換

XMLやJSON、CSVなどから

区切りなし / タイムゾーンなし

// フォーマット設定
dateFormatter.dateFormat = "yyyyMMddHHmmss"
//dateFormatter.dateFormat = "yyyyMMddHHmmssSSS" // ミリ秒込み

// ロケール設定(端末の暦設定に引きづられないようにする)
dateFormatter.locale = Locale(identifier: "en_US_POSIX")

// タイムゾーン設定(端末設定によらず、どこの地域の時間帯なのかを指定する)
dateFormatter.timeZone = TimeZone(identifier: "Asia/Tokyo")
//dateFormatter.timeZone = TimeZone(identifier: "Etc/GMT") // 世界標準時

// 変換
let date = dateFormatter.date(from: "20201020112233")
//let date = dateFormatter.date(from: "20201020112233456") // ミリ秒込み

// 結果表示
print(date!) // -> 2020-10-20 02:22:33 +0000

区切りなし / タイムゾーンあり

// フォーマット設定
dateFormatter.dateFormat = "yyyyMMddHHmmssXX"

// ロケール設定(端末の暦設定に引きづられないようにする)
dateFormatter.locale = Locale(identifier: "en_US_POSIX")

// 変換
let date = dateFormatter.date(from: "20201020112233+0900")

// 結果表示
print(date!) // -> 2020-10-20 02:22:33 +0000

ISO8601形式

// フォーマット設定
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssXXX" // 拡張形式
//dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX" // ミリ秒込み
//dateFormatter.dateFormat = "yyyyMMdd'T'HHmmssXX" // 基本形式

// ロケール設定(端末の暦設定に引きづられないようにする)
dateFormatter.locale = Locale(identifier: "en_US_POSIX")

// 変換
let date = dateFormatter.date(from: "2020-10-20T11:22:33+09:00") // 拡張形式
//let date = dateFormatter.date(from: "2020-10-20T11:22:33.456+09:00") // ミリ秒込み
//let date = dateFormatter.date(from: "20201020T112233+0900") // 基本形式

// 結果表示
print(date!) // -> 2020-10-20 02:22:33 +0000

ISO8601形式(iOS10.0以上)

// iOS10.0以上ならズバリのフォーマッターが使える
let iso8601DateFormatter = ISO8601DateFormatter()
//iso8601DateFormatter.formatOptions.insert(.withFractionalSeconds) // ミリ秒を含める
//iso8601DateFormatter.formatOptions = [.withYear, .withMonth, .withDay, .withTime, .withTimeZone] // 区切りなし

// 変換
let date = iso8601DateFormatter.date(from: "2020-10-20T11:22:33+09:00") // 拡張形式
//let date = iso8601DateFormatter.date(from: "2020-10-20T11:22:33.456+09:00") // ミリ秒込み
//let date = iso8601DateFormatter.date(from: "20201020T112233+0900") // 基本形式

// 結果表示
print(date!) // -> 2020-10-20 02:22:33 +0000

画面表示用のフォーマットなどから

スラッシュ、コロン区切り

// フォーマット設定
dateFormatter.dateFormat = "yyyy/MM/dd HH:mm:ss"

// ロケール設定(端末の暦設定に引きづられないようにする)
dateFormatter.locale = Locale(identifier: "en_US_POSIX")

// タイムゾーン設定(端末設定によらず、どこの地域の時間帯なのかを指定する)
dateFormatter.timeZone = TimeZone(identifier: "Asia/Tokyo")

// 変換
let date = dateFormatter.date(from: "2020/10/20 11:22:33")

// 結果表示
print(date!) // -> 2020-10-20 02:22:33 +0000

日本語 / 西暦表示

// フォーマット設定
dateFormatter.dateFormat = "yyyy'年'M'月'd'日('EEEEE') 'H'時'm'分's'秒'" // 曜日1文字
//dateFormatter.dateFormat = "M'月'd'日 ('EEEE')'" // 曜日3文字

// ロケール設定(曜日が含まれる場合、その解釈のため日本語の指定が必要)
dateFormatter.locale = Locale(identifier: "ja_JP")

// タイムゾーン設定(端末設定によらず、どこの地域の時間帯なのかを指定する)
dateFormatter.timeZone = TimeZone(identifier: "Asia/Tokyo")

// 変換
let date = dateFormatter.date(from: "2020年10月20日(火) 11時22分33秒")
//let date = dateFormatter.date(from: "10月20日 (火曜日)")

// 結果表示
print(date!) // -> 2020-10-20 02:22:33 +0000

日本語 / 和暦表示

// フォーマット設定
dateFormatter.dateFormat = "Gy'年'M'月'd'日"

// カレンダー設定(和暦固定)
dateFormatter.calendar = Calendar(identifier: .japanese)

// ロケール設定(日本語・日本国固定)
dateFormatter.locale = Locale(identifier: "ja_JP")

// タイムゾーン設定(端末設定によらず、どこの地域の時間帯なのかを指定する)
dateFormatter.timeZone = TimeZone(identifier: "Asia/Tokyo")

// 変換
let date = dateFormatter.date(from: "令和2年10月20日")

// 結果表示
print(date!) // -> 2020-10-19 15:00:00 +0000

おわりに

このDate ⇔ String変換は、一度書くと完璧に理解した気持ちになるけど、時を置いて再び書く機会が訪れたりすると(特に別の会社・現場で新規に書かなきゃいけない時)意外と迷うものではないかと思います。

私はそのたびにあれこれググって正解をかき集めるのですが、この記事が同じような誰かの助けとなれば幸いです。

参考リンク

DateFormatterについて
https://developer.apple.com/documentation/foundation/dateformatter

日付・時刻の書式フィールドの詳細について
https://www.unicode.org/reports/tr35/tr35-57/tr35-dates.html#Date_Format_Patterns