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

<blockquote class="twitter-tweet" data-partner="tweetdeck"><p lang="ja" dir="ltr">お？ <a href="https://t.co/E2dOZYFXNc">pic.twitter.com/E2dOZYFXNc</a></p>&mdash; treastrain / Tanaka.R (@treastrain) <a href="https://twitter.com/treastrain/status/1309510636523589632?ref_src=twsrc%5Etfw">September 25, 2020</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

**令和1998年……？**

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

---

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

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

- 日時
  - [`DateFormatter`](#dateformatter)
  - [`DateComponentsFormatter`](#datecomponentsformatter)
  - [`RelativeDateTimeFormatter`](#relativedatetimeformatter)
  - [`DateIntervalFormatter`](#dateintervalformatter)
  - [`ISO8601DateFormatter`](#iso8601dateformatter)
- 数、通貨
  - [`NumberFormatter`](#numberformatter)
- データのサイズ
  - [`ByteCountFormatter`](#bytecountformatter)
- リストを結合
  - [`ListFormatter`](#listformatter)
- 人の名前
  - [`PersonNameComponentsFormatter`](#personnamecomponentsformatter)
- 単位
  - [`MeasurementFormatter`](#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`](#dateformatter)|2.0+|10.0+|9.0+|2.0+|✅ Supported|
|[`DateComponentsFormatter`](#datecomponentsformatter)|8.0+|10.10+|9.0+|2.0+|❌ Unimplemented|
|[`RelativeDateTimeFormatter`](#relativedatetimeformatter)|13.0+|10.15+|13.0+|6.0+|❌ Unimplemented|
|[`DateIntervalFormatter`](#dateintervalformatter)|8.0+|10.10+|9.0+|2.0+|✅ Supported|
|[`ISO8601DateFormatter`](#iso8601dateformatter)|10.0+|10.12+|10.0+|3.0+|✅ Supported|
|[`NumberFormatter`](#numberformatter)|2.0+|10.0+|9.0+|2.0+|✅ Supported|
|[`ByteCountFormatter`](#bytecountformatter)|6.0+|10.8+|9.0+|2.0+|✅ Supported|
|[`ListFormatter`](#listformatter)|13.0+|10.15+|13.0+|6.0+|❌ Unimplemented|
|[`PersonNameComponentsFormatter`](#personnamecomponentsformatter)|9.0+|10.11+|9.0+|2.0+|❌ Unimplemented|
|[`MeasurementFormatter`](#measurementformatter)|10.0+|10.12+|10.0+|3.0+|:warning: Unimplemented|

> :warning: [`MeasurementFormatter`](#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`
- [DateFormatter | Apple Developer Documentation](https://developer.apple.com/documentation/foundation/dateformatter)

Google 検索でたくさんの記事がヒットするのはこの `DateFormatter` の使い方でしょう。
`Date` からフォーマットされた `String` を得たり、その逆を行うことができます。

```swift
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 のセッションでも「やるな。ほぼ確実に間違った結果になる。(意訳)」と言っています。](https://developer.apple.com/videos/play/wwdc2020-10160/?time=337)_

```swift
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`
- [DateComponentsFormatter | Apple Developer Documentation](https://developer.apple.com/documentation/foundation/datecomponentsformatter)

時刻に関するものは `DateFormatter` でしたが、
時間に関するものは `DateComponentsFormatter` となります。

```swift
// ロケールが日本語環境の場合
// はやぶさ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`
- [RelativeDateTimeFormatter | Apple Developer Documentation](https://developer.apple.com/documentation/foundation/relativedatetimeformatter)

こちらはクラス名の通り、相対的な日時の関係を返してくれる `Formatter` です。`dateTimeStyle` を `.named` にすることで「昨日」「今日」「明日」…のようにローカライズされた文言が返されます。

```swift
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 | Apple Developer Documentation](https://developer.apple.com/documentation/foundation/DateIntervalFormatter)

日時の範囲をフォーマットするには `DateIntervalFormatter` を使います。地域によって「〜」「 – 」などと異なることがわかります。
`dateTemplate` にフォーマット文字列を指定して、カスタムすることもできます。この文字列は [Unicode Technical Standard #35 の Date Format Patterns で定義されたパターン](https://unicode.org/reports/tr35/tr35-dates.html#Date_Format_Patterns)を使用します。

```swift
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`
- [ISO8601DateFormatter | Apple Developer Documentation](https://developer.apple.com/documentation/foundation/ISO8601DateFormatter)

ISO 8601 形式の時刻表記の文字列と `Date` を相互に変換できる `Formatter` です。`formatOptions` でカスタムすることもできます。

```swift
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 | Apple Developer Documentation](https://developer.apple.com/documentation/foundation/numberformatter)

`NumberFormatter` では数、通貨に関するフォーマットを行うことができます。あまりに機能が多すぎてこれだけで1記事以上のボリュームになってしまうので、簡単なサンプルだけ…。

```swift
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](https://developer.apple.com/documentation/foundation/bytecountformatter)

バイトサイズを KB や MB のようにフォーマットされたものを取得できます。YB まで対応していますが、`Int64.max` を入れても 9.22 EB なので、ZB や YB は小数表示のみになります。

```swift
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](https://developer.apple.com/documentation/foundation/listformatter)

配列の要素をひとつづきの文字列に変換できます。`ListFormatter.string(from:)` の引数の型は `[Any]` なので、`ListFormatter.itemFormatter` に別な `Formatter` を入れることもできます。

```swift
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](https://developer.apple.com/documentation/foundation/personnamecomponentsformatter)

人の名前を適切にフォーマットした文字列を返します。また逆に文字列から名前の各要素（姓と名…など）を取り出すこともできます。

```swift
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 の以下のトーク内でも紹介されていて参考になります。

- [あなたの知らない連絡先の世界 by 日向強 | トーク | iOSDC Japan 2020 - fortee.jp](https://fortee.jp/iosdc-japan-2020/proposal/a33cfac2-ee95-4daa-99cc-bd8960bf514a)

# `MeasurementFormatter`
- [MeasurementFormatter | Apple Developer Documentation](https://developer.apple.com/documentation/foundation/MeasurementFormatter)
  - [LengthFormatter | Apple Developer Documentation](https://developer.apple.com/documentation/foundation/lengthformatter)
  - [MassFormatter | Apple Developer Documentation](https://developer.apple.com/documentation/foundation/massformatter)
  - [EnergyFormatter | Apple Developer Documentation](https://developer.apple.com/documentation/foundation/energyformatter)

`Measurement<UnitType>` で値を指定して、それを `MeasurementFormatter.string(from:)` に入れることでフォーマットされた文字列を取得できます。`UnitType` には22種類もの Unit が用意されています。

<details><summary>`UnitType` の一覧とドキュメントへのリンクを表示</summary><div>

- [`Dimension`](https://developer.apple.com/documentation/foundation/dimension)
  - [`UnitAcceleration`](https://developer.apple.com/documentation/foundation/unitacceleration)
  - [`UnitAngle`](https://developer.apple.com/documentation/foundation/unitangle)
  - [`UnitArea`](https://developer.apple.com/documentation/foundation/unitarea)
  - [`UnitConcentrationMass`](https://developer.apple.com/documentation/foundation/unitconcentrationmass)
  - [`UnitDispersion`](https://developer.apple.com/documentation/foundation/unitdispersion)
  - [`UnitDuration`](https://developer.apple.com/documentation/foundation/unitduration)
  - [`UnitElectricCharge`](https://developer.apple.com/documentation/foundation/unitelectriccharge)
  - [`UnitElectricCurrent`](https://developer.apple.com/documentation/foundation/unitelectriccurrent)
  - [`UnitElectricPotentialDifference`](https://developer.apple.com/documentation/foundation/unitelectricpotentialdifference)
  - [`UnitElectricResistance`](https://developer.apple.com/documentation/foundation/unitelectricresistance)
  - [`UnitEnergy`](https://developer.apple.com/documentation/foundation/unitenergy)
  - [`UnitFrequency`](https://developer.apple.com/documentation/foundation/unitfrequency)
  - [`UnitFuelEfficiency`](https://developer.apple.com/documentation/foundation/unitfuelefficiency)
  - [`UnitIlluminance`](https://developer.apple.com/documentation/foundation/unitilluminance)
  - [`UnitInformationStorage`](https://developer.apple.com/documentation/foundation/unitinformationstorage)
  - [`UnitLength`](https://developer.apple.com/documentation/foundation/unitlength)
  - [`UnitMass`](https://developer.apple.com/documentation/foundation/unitmass)
  - [`UnitPower`](https://developer.apple.com/documentation/foundation/unitpower)
  - [`UnitPressure`](https://developer.apple.com/documentation/foundation/unitpressure)
  - [`UnitSpeed`](https://developer.apple.com/documentation/foundation/unitspeed)
  - [`UnitTemperature`](https://developer.apple.com/documentation/foundation/unittemperature)
  - [`UnitVolume`](https://developer.apple.com/documentation/foundation/unitvolume)

</div></details>

```swift
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