Add unicodeScalars property to Character
01 何が問題だったのか
Swift の String は Unicode の extended grapheme cluster を単位とするコレクションで、その要素型が Character です。Character は人間が1文字として認識する単位を表しますが、内部的には複数の Unicode scalar(Unicode.Scalar)から構成されることがあります。たとえば "é" は単一のスカラーで表すこともできますし、"e" と結合用アクセント記号の2スカラーで表すこともできます。絵文字の一部や異体字セレクタを伴う文字も、複数スカラーの組み合わせです。
Swift 4.0 より前の Character は、比較・リテラル初期化・String.init の引数として使うくらいしか用途のない、ほぼ中身の見えない型でした。一方で String には unicodeScalars という Unicode scalar 列のビューがあり、個々のスカラーを調べられるようになっています。
このため、文字列を「文字ごとに」走査しつつ各文字を構成するスカラーを検査したい、という自然な処理が書きにくい状態でした。たとえば、任意の空白文字(ASCII の空白だけでなく、改行や Unicode の各種空白を含む)を区切りとして分割したいとき、String レベルでは次のように書けます。
let s = "one two three"
s.split(separator: " ")
しかし、CharacterSet.whitespacesAndNewlines のような Unicode scalar 単位の集合を使って「空白スカラーを含む文字」で分割しようとすると、Character からそのスカラー列に直接アクセスする手段がないため、次のような書き方はできませんでした。
let ws = CharacterSet.whitespacesAndNewlines
s.split { $0.unicodeScalars.contains(where: ws.contains) } // 書けない
ASCII 判定のように「grapheme を構成するスカラーを調べればわかること」の多くも、同じ理由で Character 単体では表現しづらくなっていました。
02 どのように解決されるのか
Character に、その文字を構成する Unicode scalar をたどるための読み取り専用ビュー unicodeScalars プロパティを追加します。String.unicodeScalars に相当するものの、Character 版です。
使い方
Character の unicodeScalars を使えば、Motivation で挙げられていた「空白文字で分割する」処理が次のように書けるようになります。
import Foundation
let s = "one two\tthree\nfour"
let ws = CharacterSet.whitespacesAndNewlines
let parts = s.split { $0.unicodeScalars.contains(where: ws.contains) }
// parts == ["one", "two", "three", "four"]
ASCII かどうかの判定のように、grapheme を構成するスカラーを見ればわかる性質も、Character 単体で扱えます。
extension Character {
var isASCII: Bool {
return unicodeScalars.count == 1 && unicodeScalars.first!.value < 128
}
}
Character("A").isASCII // true
Character("あ").isASCII // false
Character("\u{65}\u{301}").isASCII // false("é" を "e" + アクセントで構成)
ビューは BidirectionalCollection に適合するので、for ループや count、first / last、逆順走査などがそのまま使えます。
let c: Character = "\u{1F1EF}\u{1F1F5}" // 🇯🇵(国旗は2つの地域指示子スカラーで構成される)
for scalar in c.unicodeScalars {
print(String(format: "U+%04X", scalar.value))
}
// U+1F1EF
// U+1F1F5
ビューの性質
Character.UnicodeScalarView は Character にネストされた型として定義され、次のような形をしています。
extension Character {
public struct UnicodeScalarView: BidirectionalCollection {
public struct Index { /* ... */ }
public var startIndex: Index { get }
public var endIndex: Index { get }
public func index(after i: Index) -> Index
public func index(before i: Index) -> Index
public subscript(i: Index) -> UnicodeScalar
}
public var unicodeScalars: UnicodeScalarView { get }
}
String.unicodeScalars とは異なり、このビューは 読み取り専用 です。Character はちょうど1つの grapheme を保持するという不変条件があるため、スカラー単位で書き換えられるとこれが壊れかねません。非リテラルな Character を作成・加工したいときは、これまで通り String を経由します。UnicodeScalarView のイニシャライザも internal で、このビューは常に Character から取り出す形で利用します。
また CustomStringConvertible などの利便プロトコルにも適合します。
utf8 / utf16 ビューについて
Character に utf8 や utf16 のビューを追加することも検討されましたが、本提案のスコープには含まれていません。必要な場合は String の対応するビューを使う前提です。isASCII のような便利プロパティを Character に追加することも、unicodeScalars という土台ができた上で将来追加的に行える範囲とされています(あくまで見通しであり、約束ではありません)。