[Swift] 文字列補完 使ってますか？


# もちろん、使ってますよね。

Swiftの文字列補完とは、以下のように、文字列リテラルに変数や式を埋め込んで文字列を生成できる便利な記法です。_（[公式ドキュメント](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/stringsandcharacters/#String-Interpolation)から例を引用）_

```swift:String Interpolation（公式ドキュメントから引用）
let multiplier = 3
let message = "\(multiplier) times 2.5 is \(Double(multiplier) * 2.5)"
// message is "3 times 2.5 is 7.5"
```

でも、Swiftの文字列補完は、Pythonの[フォーマット済み文字列リテラルのように](https://docs.python.org/ja/3.8/reference/lexical_analysis.html#formatted-string-literals)、書式を自由に指定できないです[^1]。

[^1]: SwiftUIのTextなどを除く

Swiftで書式化しようとすると、`String(format:_:)`を使うのが一般的かと思います。

先に引用した文字列補完を`String(format:_:)`で書くと、以下のようになります。

```swift:String(format:_:)
let multiplier = 3
let message = String(format: "%d times 2.5 is %.1f", multiplier, Double(multiplier) * 2.5)
// message is "3 times 2.5 is 7.5"
```

でもやっぱり、Swiftの文字列補完で書式を指定したいと思い、使用頻度が高いと思われるいくつかのケースに限って、`String.StringInterpolation`を拡張して、書式指定を作ってみました。


# 書式指定を どう使うの？

以下の使用例をお見せすれば、簡単にイメージをつかんでいただけると思います。

```swift:使用例1（整数型）
print("\(10)") //書式無し
//10

print("\(10, width: 20)") //幅20桁
//                  10

print("\(11, width: 20, leadingZeros: true)") //幅20桁、前ゼロあり
//00000000000000000011

print("\(11, width: -20)") //マイナス幅で前ゼロありを指定可
//00000000000000000011

//16進数文字
print("\(toHex: 127)") //最小桁
//0x7f

print("\(toHex: 127, width: 8)") //前ゼロあり8桁
//0x0000007f

print("\(toHex: 127, width: 10, prefix: "", leadingZeros: false)")
//        7f
```

```swift:使用例2（浮動小数点型）
print("\(12.123)") //書式無し
//12.123

print("\(12.123, width: 20)") //幅20桁
//            12.12300

print("\(13.123, width: 20, precision: 3)") //幅20桁、少数以下3桁
//              13.123

print("\(14.123, width: 20, leadingZeros: true)") //幅20桁、前ゼロあり
//00000000000014.12300

print("\(14.123, width: -20)") //マイナス幅で前ゼロありを指定可
//00000000000014.12300
```

```swift:使用例3（3桁カンマ付き）
//整数型
print("\(withComma: 10_001)") //3桁カンマ、幅指定無し
//10,001

print("\(withComma: 10_002, width: 20)") //3桁カンマ、幅20桁
//              10,002

print("\(withComma: 10_002, width: 20, leadingZeros: true)") //3桁カンマ、幅20桁、前ゼロあり
//00,000,000,000,000,010,002
//前ゼロを指定すると、幅指定に`,`の文字数は含まないようだ

//浮動小数点型
print("\(withComma: 10_003.2345)") //浮動小数点3桁カンマ、幅指定無し
//10,003.23450

print("\(withComma: 10_004.2345, precision: 3)") //少数以下3桁
//10,004.234

print("\(withComma: 10_005.2345, width: 20, precision: 3)") //幅20桁、少数以下3桁
//          10,005.234

print("\(withComma: 10_006.2345, width: 20, leadingZeros: true)") //幅20桁、少数以下3桁、前ゼロあり
//        10,006.23450
//浮動小数点型の3桁カンマ付きは、前ゼロありを指定しても付きませんね
```

```swift:使用例4（Bool型）
print("\(true)") //false: 0, true: 1
//1

print("\(false, label: (" No", "Yes"))") //任意のラベル （false: " No", true: "Yes"）
// No
```

```swift:使用例5（Date型）
print("\(Date.now, dateFormat: "yyyy/MM/dd(E)HH:mm:ss.SSS")")
//2023/12/17(日)00:28:51.015

print("\(Date.now, dateFormat: "MM/dd/yyyy(E)HH:mm:ss.SSS", locale: "en_US", timeZone: "America/New_York")")
//12/16/2023(Sat)10:28:51.015
```

```swift:使用例6（format直接指定）
//上のどれにも当てはまらない場合は、直接formatを指定できる
//整数型、浮動小数点型、String型のみ指定可

print("\(10.123456, format: "%-20.10f+")")
//10.1234560000       +

print("\(11.123456, format: "%20.10f+")")
//       11.1234560000+

```

上の`format`だけあれば、用は足りると云えば足りますね。

# 実装コード

以下に、StringInterpolation拡張の実装コードを示す。引数とformat指定子を見ていただければ、内容は一目瞭然と思います。

```swift
extension String.StringInterpolation {
    mutating func appendInterpolation(_ value: any FixedWidthInteger, width: Int, leadingZeros: Bool = false) {
        let leadingZeros = leadingZeros || width < 0, width = abs(width)
        appendLiteral(String(format: "%\(leadingZeros ? "0" : "")\(width)d", value as! CVarArg))
    }
    mutating func appendInterpolation(_ value: any BinaryFloatingPoint, width: Int = 0, precision: Int = 5, leadingZeros: Bool = false) {
        let leadingZeros = leadingZeros || width < 0, width = abs(width)
        appendLiteral(String(format: "%\(leadingZeros ? "0" : "")" + (width > 0 ? "\(width).\(precision)f" : ".\(precision)f"), value as! CVarArg))
    }
    mutating func appendInterpolation(toHex value: any FixedWidthInteger, width: Int = 0, prefix: String = "0x", leadingZeros: Bool = true) {
        let leadingZeros = leadingZeros || width < 0, width = abs(width)
        appendLiteral(String(format: prefix + "%\(leadingZeros ? "0" : "")\(width)x", value as! CVarArg))
    }
    mutating func appendInterpolation(withComma value: any FixedWidthInteger, width: Int = 0, leadingZeros: Bool = false) {
        let leadingZeros = leadingZeros || width < 0, width = abs(width)
        appendLiteral(String.localizedStringWithFormat("%\(leadingZeros ? "0" : "")" + (width > 0 ? "\(width)d" : "d"), value as! CVarArg))
    }
    mutating func appendInterpolation(withComma value: any BinaryFloatingPoint, width: Int = 0, precision: Int = 5, leadingZeros: Bool = false) {
        let leadingZeros = leadingZeros || width < 0, width = abs(width)
        appendLiteral(String.localizedStringWithFormat("%\(leadingZeros ? "0" : "")" + (width > 0 ? "\(width).\(precision)f" : ".\(precision)f"), value as! CVarArg))
    }
    mutating func appendInterpolation(_ value: Bool, label: (false: String, true: String) = ("0", "1")) {
        appendLiteral(value ? label.true : label.false)
    }
    mutating func appendInterpolation(_ date: Date, dateFormat: String, locale: String = "ja_JP", timeZone: String = "Asia/Tokyo") {
        let formatter = DateFormatter()
        formatter.locale = Locale(identifier: locale)
        formatter.timeZone = TimeZone(identifier: timeZone)
        formatter.dateFormat = dateFormat
        appendLiteral(formatter.string(from: date))
    }
    mutating func appendInterpolation(_ value: Any, format: String) {
        guard (value is any FixedWidthInteger) || (value is any BinaryFloatingPoint) || (value is any StringProtocol) else { return }
        appendLiteral(String(format: format, value as! CVarArg))
    }
}
```



### 以上

普段使いそうなパターンは網羅したつもりです。


# おまけ

文字列補完とは直接関係しませんが、`DateFormatter`で指定できる`timeZone`と`locale`を確認する方法は、以下のとおりです。何百もあるんですね。

```swift
import Foundation
print(TimeZone.knownTimeZoneIdentifiers)
```

```console:TimeZone.knownTimeZoneIdentifiers
[
 "Africa/Abidjan",
 "Africa/Accra",
 "Africa/Addis_Ababa",
 "Africa/Algiers",

 : : : 

 "Pacific/Tongatapu",
 "Pacific/Truk",
 "Pacific/Wake",
 "Pacific/Wallis"
]
```

```swift
import Foundation
print(Locale.availableIdentifiers.sorted())
```

```console:Locale.availableIdentifiers
[
 "af",
 "af_NA",
 "af_ZA",
 "agq",
 "agq_CM",

 : : : 

 "zh_Hant_CN",
 "zh_Hant_HK",
 "zh_Hant_MO",
 "zh_Hant_TW",
 "zu",
 "zu_ZA"
]
```