— 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