— treastrain / Tanaka.R (@treastrain) September 25, 2020
令和1998年……?
この記事は iOS Advent Calendar 2020 最終日、25日目の記事です。記事を書き始めてから、この内容は iOS 以外でも、Swift が使えるプラットフォームで有効なものであることに気が付きました。時すでに遅しなのでこのままいきます…。
Swift でたとえば Date
を 2020年11月28日 23:59
という String
に変換したいとき、DateFormatter
を使います。
しかし、このデータの String
へのフォーマットは、Date
だけでないさまざまな要素に対応した Formatter
があらかじめ用意されています。
- 日時
- 数、通貨
- データのサイズ
- リストを結合
- 人の名前
- 単位
-
MeasurementFormatter
-
LengthFormatter
、MassFormatter
、EnergyFormatter
は Deprecated となりMeasurementFormatter
に統合
-
-
なぜ Formatter
を使ったほうがいいのか
各データから String
へのフォーマットするための実装は簡単に思えるかもしれません。
では日付、Date
を "2020/11/28 21:31"
にしたければ、年月日時分を取って "\(year)/\(month)/\(day) \(hour):\(minute)"
とでもしておきましょうか。
でも待ってください。もし iPhone の設定で、「24時間表示」をオフにしている(=午後9:31)ユーザーがいたらどうしましょうか。暦を西暦ではなく和暦(=令和2年)としている場合は?そもそもアメリカ圏であれば "Nov 28, 2020 at 9:31 PM"
という表記のほうがよいでしょう。
また、1,234.56(せん にひゃく さんじゅう よん てん ごろく)という数、小数点を「.」ではなく「,」で表記する地域もありますが、それだと(いち てん にさんよん?ごろく)という意味になってしまいます。
さらにデータのサイズ、134217728バイトを「134.2 MB」のように表示したいとすれば?
1024で何回除算したかで K、M、G と接頭辞をつけるように自分でプログラムしますか?小数点以下はどのように処理しましょうか。今はこれで十分かもしれませんが、もし T、P、E と来た場合は?
ひとことに String
への変換と言っても、考慮すべきことは無限にあります。実装に漏れが出ると、「令和1998年生まれ」のひとが出現します。
ということで、すでに Foundation
に用意されている各 Formatter
をおとなしく使うことにしましょう。数行のサンプルも載せますので、頭の片隅に「こんな Formatter
あったな」と置いておくと、いつの日か役に立つかもしれません。
おしながき
クラス名をクリックするとジャンプできます。情報は記事執筆時点のものです。
Availability |
iOS | macOS | tvOS | watchOS | Linux etc. (Swift) |
---|---|---|---|---|---|
DateFormatter |
2.0+ | 10.0+ | 9.0+ | 2.0+ | ✅ Supported |
DateComponentsFormatter |
8.0+ | 10.10+ | 9.0+ | 2.0+ | ❌ Unimplemented |
RelativeDateTimeFormatter |
13.0+ | 10.15+ | 13.0+ | 6.0+ | ❌ Unimplemented |
DateIntervalFormatter |
8.0+ | 10.10+ | 9.0+ | 2.0+ | ✅ Supported |
ISO8601DateFormatter |
10.0+ | 10.12+ | 10.0+ | 3.0+ | ✅ Supported |
NumberFormatter |
2.0+ | 10.0+ | 9.0+ | 2.0+ | ✅ Supported |
ByteCountFormatter |
6.0+ | 10.8+ | 9.0+ | 2.0+ | ✅ Supported |
ListFormatter |
13.0+ | 10.15+ | 13.0+ | 6.0+ | ❌ Unimplemented |
PersonNameComponentsFormatter |
9.0+ | 10.11+ | 9.0+ | 2.0+ | ❌ Unimplemented |
MeasurementFormatter |
10.0+ | 10.12+ | 10.0+ | 3.0+ | Unimplemented |
MeasurementFormatter
に統合される前はこちら
Availability
iOS macOS tvOS watchOS Linux etc. (Swift) LengthFormatter
8.0+ 10.10+ 9.0+ 2.0+ ✅ Supported MassFormatter
8.0+ 10.10+ 9.0+ 2.0+ ✅ Supported EnergyFormatter
8.0+ 10.10+ 9.0+ 2.0+ ✅ Supported
DateFormatter
Google 検索でたくさんの記事がヒットするのはこの DateFormatter
の使い方でしょう。
Date
からフォーマットされた String
を得たり、その逆を行うことができます。
let date = Date(timeIntervalSince1970: 1608856860)
let formatter = DateFormatter()
formatter.dateStyle = .medium
formatter.timeStyle = .short
formatter.locale = Locale(identifier: "ja_JP")
print(formatter.string(from: date)) // 2020/12/25 9:41
formatter.locale = Locale(identifier: "en_US")
print(formatter.string(from: date)) // Dec 25, 2020 at 9:41 AM
formatter.locale = Locale(identifier: "zh_HK")
print(formatter.string(from: date)) // 2020年12月25日 上午9:41
dateStyle
や timeStyle
で String
にするときのスタイルを指定します。locale
で String
にするときのロケールを指定します(指定しない場合は Locale.current
が使用されます)。
お願いですから DateFormatter.dateFormat
に直接 String
を入れて Date
→ String
に変換するのはやめてください。 WWDC のセッションでも「やるな。ほぼ確実に間違った結果になる。(意訳)」と言っています。
let date = Date(timeIntervalSince1970: 1608856860)
let formatter = DateFormatter()
formatter.dateFormat = "dMMM" // ❌ 絶対にやめて ❌
formatter.locale = Locale(identifier: "en_US")
print(formatter.string(from: date)) // 25Dec ❌ 事故
formatter.locale = Locale(identifier: "ja_JP")
print(formatter.string(from: date)) // 2512月 ❌ 大事故
/* ---------- */
formatter.locale = Locale(identifier: "en_US")
// ✅ 例えば setLocalizedDateFormatFromTemplate(_:) を使う
formatter.setLocalizedDateFormatFromTemplate("dMMM")
print(formatter.string(from: date)) // Dec 25
formatter.locale = Locale(identifier: "ja_JP")
// ✅ 例えば setLocalizedDateFormatFromTemplate(_:) を使う
formatter.setLocalizedDateFormatFromTemplate("dMMM")
print(formatter.string(from: date)) // 12月25日
DateComponentsFormatter
時刻に関するものは DateFormatter
でしたが、
時間に関するものは DateComponentsFormatter
となります。
// ロケールが日本語環境の場合
// はやぶさ44号 新函館北斗~東京 所要時間
let components = DateComponents(hour: 3, minute: 58)
let formatter = DateComponentsFormatter()
formatter.unitsStyle = .abbreviated
print(formatter.string(from: components)) // 3時間58分
formatter.unitsStyle = .positional
print(formatter.string(from: components)) // 3:58
unitsStyle
で String
にするときのスタイルを指定します。ユーザーの実行環境におけるロケールに合わせた String
が得られます。
RelativeDateTimeFormatter
こちらはクラス名の通り、相対的な日時の関係を返してくれる Formatter
です。dateTimeStyle
を .named
にすることで「昨日」「今日」「明日」…のようにローカライズされた文言が返されます。
let formatter = RelativeDateTimeFormatter()
formatter.locale = Locale(identifier: "ja_JP")
formatter.dateTimeStyle = .named
print(formatter.localizedString(from: DateComponents(month: -1))) // 先月
print(formatter.localizedString(from: DateComponents(month: 0))) // 今月
print(formatter.localizedString(from: DateComponents(month: 1))) // 来月
print(formatter.localizedString(from: DateComponents(day: -3))) // 3 日前
print(formatter.localizedString(from: DateComponents(day: -2))) // 一昨日
print(formatter.localizedString(from: DateComponents(day: -1))) // 昨日
print(formatter.localizedString(from: DateComponents(day: 0))) // 今日
print(formatter.localizedString(from: DateComponents(day: 1))) // 明日
print(formatter.localizedString(from: DateComponents(day: 2))) // 明後日
print(formatter.localizedString(from: DateComponents(day: 3))) // 3 日後
DateIntervalFormatter
日時の範囲をフォーマットするには DateIntervalFormatter
を使います。地域によって「〜」「 – 」などと異なることがわかります。
dateTemplate
にフォーマット文字列を指定して、カスタムすることもできます。この文字列は Unicode Technical Standard #35 の Date Format Patterns で定義されたパターンを使用します。
let formatter = DateIntervalFormatter()
formatter.dateStyle = .short
formatter.timeStyle = .short
let startDate = Date(timeIntervalSince1970: 1608856860)
let endDate = Date(timeInterval: 86400, since: startDate)
formatter.locale = Locale(identifier: "en_US")
print(formatter.string(from: startDate, to: endDate)) // 12/25/20, 9:41 AM – 12/26/20, 9:41 AM
formatter.locale = Locale(identifier: "zh_HK")
print(formatter.string(from: startDate, to: endDate)) // 25/12/2020 上午9:41至26/12/2020 上午9:41
formatter.locale = Locale(identifier: "ja_JP")
print(formatter.string(from: startDate, to: endDate)) // 2020/12/25 9:41~2020/12/26 9:41
formatter.dateTemplate = "EEE, MMM d, ''yyyy"
print(formatter.string(from: startDate, to: endDate)) // 2020年12月25日(金)~26日(土)
ISO8601DateFormatter
ISO 8601 形式の時刻表記の文字列と Date
を相互に変換できる Formatter
です。formatOptions
でカスタムすることもできます。
let formatter = ISO8601DateFormatter()
let dateString = "2020-12-25T09:41:00Z"
let date = formatter.date(from: dateString)
print(date) // Optional(2020-12-25 09:41:00 +0000)
/* ---------- */
formatter.formatOptions = [.withYear, .withMonth, .withDay, .withTime]
let date = Date(timeIntervalSince1970: 1608889260)
let dateString = formatter.string(from: date)
print(dateString) // 20201225T094100
NumberFormatter
NumberFormatter
では数、通貨に関するフォーマットを行うことができます。あまりに機能が多すぎてこれだけで1記事以上のボリュームになってしまうので、簡単なサンプルだけ…。
let formatter = NumberFormatter()
// 3桁おきにカンマが入る
formatter.numberStyle = .decimal
formatter.locale = Locale(identifier: "ja_JP")
print(formatter.string(from: 1234.56)) // Optional("1,234.56")
// 小数点 (decimalSeparator) が「ドット」ではなく「カンマ」で表される地域もある
formatter.numberStyle = .decimal
formatter.locale = Locale(identifier: "fr_FR")
print(formatter.string(from: 1234.56)) // Optional("1 234,56")
/* ---------- */
// パーセント表示
formatter.numberStyle = .percent
formatter.locale = Locale(identifier: "ja_JP")
print(formatter.string(from: 0.95)) // Optional("95%")
// パーセントが数字の前にくる地域もある
formatter.numberStyle = .percent
formatter.locale = Locale(identifier: "tr_TR")
print(formatter.string(from: 0.95)) // Optional("%95")
// パーセント記号(percentSymbol)が「%」ではない地域もある
formatter.numberStyle = .percent
formatter.locale = Locale(identifier: "ar_EG")
print(formatter.string(from: 0.95)) // Optional("٩٥٪")
/* ---------- */
// 通貨 "ja_JP"
formatter.numberStyle = .currency
formatter.locale = Locale(identifier: "ja_JP")
// formatter.currencyCode = "JPY" // 指定しなければ locale による
print(formatter.string(from: 1234.56)) // Optional("¥1,235") // 四捨五入されている
// 通貨 "en_US"
formatter.numberStyle = .currency
formatter.locale = Locale(identifier: "en_US")
print(formatter.string(from: 1234.56)) // Optional("$1,234.56")
// 通貨 "zh_HK"
formatter.numberStyle = .currency
formatter.locale = Locale(identifier: "zh_HK")
print(formatter.string(from: 1234.56)) // Optional("HK$1,234.56")
/* ---------- */
// 頭に半角スペースを入れて文字列幅を合わせる
formatter.numberStyle = .decimal
formatter.formatWidth = 6
print(formatter.string(from: 100)) // Optional(" 100")
/* ---------- */
// その他の numberStyle("ja_JP")
formatter.locale = Locale(identifier: "ja_JP")
formatter.numberStyle = .scientific
print(formatter.string(from: 123456789)) // Optional("1.23456789E8")
formatter.numberStyle = .spellOut
print(formatter.string(from: 123456789)) // Optional("一億二千三百四十五万六千七百八十九")
formatter.numberStyle = .ordinal
print(formatter.string(from: 2)) // Optional("第2")
formatter.numberStyle = .currencyAccounting
print(formatter.string(from: -12345)) // Optional("(¥12,345)") // 負数が括弧になる
formatter.numberStyle = .currencyISOCode
print(formatter.string(from: 12345)) // Optional("JPY 12,345")
formatter.numberStyle = .currencyPlural
print(formatter.string(from: 12345)) // Optional("12,345 円")
/* ---------- */
// positivePrefix や negativePrefix を指定する
formatter.numberStyle = .currency
formatter.positivePrefix = "△"
formatter.negativePrefix = "▲"
print(formatter.string(from: 1234.56)) // Optional("△1,234.56")
print(formatter.string(from: -1234.56)) // Optional("▲1,234.56")
ByteCountFormatter
ByteCountFormatter | Apple Developer Documentation
バイトサイズを KB や MB のようにフォーマットされたものを取得できます。YB まで対応していますが、Int64.max
を入れても 9.22 EB なので、ZB や YB は小数表示のみになります。
let formatter = ByteCountFormatter()
formatter.countStyle = .file
print(formatter.string(fromByteCount: 134217728)) // 134.2 MB
formatter.countStyle = .memory
print(formatter.string(fromByteCount: 134217728)) // 128 MB
formatter.countStyle = .decimal
print(formatter.string(fromByteCount: 134217728)) // 134.2 MB
formatter.countStyle = .binary
print(formatter.string(fromByteCount: 134217728)) // 128 MB
ListFormatter
ListFormatter | Apple Developer Documentation
配列の要素をひとつづきの文字列に変換できます。ListFormatter.string(from:)
の引数の型は [Any]
なので、ListFormatter.itemFormatter
に別な Formatter
を入れることもできます。
var languages: [String] = []
let formatter = ListFormatter()
// "ja_JP"
formatter.locale = Locale(identifier: "ja_JP")
languages = ["英語", "日本語", "中国語"]
print(formatter.string(from: languages)) // Optional("英語、日本語、中国語")
languages = ["英語", "日本語"]
print(formatter.string(from: languages)) // Optional("英語、日本語")
// "en_US"
formatter.locale = Locale(identifier: "en_US")
languages = ["English", "Japanese", "Chinese"]
print(formatter.string(from: languages)) // Optional("English, Japanese, and Chinese")
languages = ["English", "Japanese"]
print(formatter.string(from: languages)) // Optional("English and Japanese")
/* ---------- */
// 他の Formatter との組み合わせ
let numberFormatter = NumberFormatter()
numberFormatter.numberStyle = .percent // パーセント表示にする
let listFormatter = ListFormatter()
listFormatter.locale = Locale(identifier: "en_US")
listFormatter.itemFormatter = numberFormatter
let items = [0.85, 0.12, 0.34]
print(listFormatter.string(from: items)) // Optional("85%, 12%, and 34%")
PersonNameComponentsFormatter
PersonNameComponentsFormatter | Apple Developer Documentation
人の名前を適切にフォーマットした文字列を返します。また逆に文字列から名前の各要素(姓と名…など)を取り出すこともできます。
let formatter = PersonNameComponentsFormatter()
var components = PersonNameComponents()
components.familyName = "蒲焼"
components.middleName = "うなぎ"
components.givenName = "太郎"
components.nickname = "KT"
print(formatter.string(from: components)) // 蒲焼太郎
formatter.style = .short
print(formatter.string(from: components)) // KT
formatter.style = .medium
print(formatter.string(from: components)) // 蒲焼太郎
formatter.style = .long
print(formatter.string(from: components)) // 蒲焼太郎うなぎ
formatter.style = .abbreviated
print(formatter.string(from: components)) // 蒲焼
/* ---------- */
var components = formatter.personNameComponents(from: "蒲焼さん 太郎")
print(components?.familyName) // Optional("蒲焼さん")
print(components?.middleName) // nil
print(components?.givenName) // Optional("太郎")
components = formatter.personNameComponents(from: "John Appleseed")
print(components?.familyName) // Optional("Appleseed")
print(components?.middleName) // nil
print(components?.givenName) // Optional("John")
PersonNameComponentsFormatter
ついては iOSDC Japan の以下のトーク内でも紹介されていて参考になります。
MeasurementFormatter
Measurement<UnitType>
で値を指定して、それを MeasurementFormatter.string(from:)
に入れることでフォーマットされた文字列を取得できます。UnitType
には22種類もの Unit が用意されています。
`UnitType` の一覧とドキュメントへのリンクを表示
-
Dimension
UnitAcceleration
UnitAngle
UnitArea
UnitConcentrationMass
UnitDispersion
UnitDuration
UnitElectricCharge
UnitElectricCurrent
UnitElectricPotentialDifference
UnitElectricResistance
UnitEnergy
UnitFrequency
UnitFuelEfficiency
UnitIlluminance
UnitInformationStorage
UnitLength
UnitMass
UnitPower
UnitPressure
UnitSpeed
UnitTemperature
UnitVolume
let formatter = MeasurementFormatter()
formatter.numberFormatter.locale = Locale(identifier: "ja_JP")
// 長さ
let length = Measurement<UnitLength>(value: 100, unit: .meters)
print(formatter.string(from: length)) // 0.1 km
// 温度
let temperature = Measurement<UnitTemperature>(value: 68.0, unit: .fahrenheit) // 華氏68度
print(formatter.string(from: temperature)) // 20°C
// 速度
let speed = Measurement<UnitSpeed>(value: 100, unit: .kilometersPerHour)
print(formatter.string(from: speed)) // 100 km/h
参考
これらの Formatter に関しては以下の WWDC 2020 のビデオが非常に参考になります。ぜひチェックしてみてください。
- Formatters: Make data human-friendly - WWDC 2020 - Videos - Apple Developer https://developer.apple.com/videos/play/wwdc2020/10160/
- Data Formatting | Apple Developer Documentation https://developer.apple.com/documentation/foundation/data_formatting