118
95

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

iOSAdvent Calendar 2020

Day 25

【Swift】日時、数、通貨、データサイズ、リスト、人の名前、単位付きの数から String へのフォーマットは自分で実装しないで

Last updated at Posted at 2020-12-25

令和1998年……?

この記事は iOS Advent Calendar 2020 最終日、25日目の記事です。記事を書き始めてから、この内容は iOS 以外でも、Swift が使えるプラットフォームで有効なものであることに気が付きました。時すでに遅しなのでこのままいきます…。


Swift でたとえば Date2020年11月28日 23:59 という String に変換したいとき、DateFormatter を使います。

しかし、このデータの String へのフォーマットは、Date だけでないさまざまな要素に対応した Formatter があらかじめ用意されています。

なぜ 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+ :warning: Unimplemented

:warning: 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

dateStyletimeStyleString にするときのスタイルを指定します。localeString にするときのロケールを指定します(指定しない場合は Locale.current が使用されます)。

お願いですから DateFormatter.dateFormat に直接 String を入れて DateString に変換するのはやめてください。 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

unitsStyleString にするときのスタイルを指定します。ユーザーの実行環境におけるロケールに合わせた 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` の一覧とドキュメントへのリンクを表示
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 のビデオが非常に参考になります。ぜひチェックしてみてください。

118
95
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
118
95

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?