はじめに
以前、「CharacterSetはCharacterのsetではありませんよ?」という記事を書きました。簡単に言うと、「Foundation
フレームワークで提供されるCharacterSet
は、Character
のsetではなくUnicode.Scalar
のsetだ」ということです。
でもやっぱり、それで投げっぱなしは良くないと思ったのです。
というわけで、作りました。Character
のsetをね。
そして、SwiftBonaFideCharacterSetという名前でGitHubに上げました。
Character
のsetを作る過程
Character
のsetを作ろうと思っても一筋縄では行かなかったので、その壁をいくつかここに記載しておこうと思います。
Character
の種類は無限大!
Unicodeのスカラ値は所詮U+0000からU+10FFFFまでの範囲内に収まります。しかし、Character
はそうはいきません。結合文字を組み合わせることでいくらでも文字(Unicode的には「拡張書記素クラスタ」)を作れます。
たとえば、U+20DDは"COMBINING ENCLOSING CIRCLE"といって、前の文字に丸をつけるための結合文字です。そして、結合文字は基本的にいくつ繋げてもいいことになっています(それに意味があるかどうかは別として)。
Swiftで書くと:
print("A".count) // -> 1
print("A\u{20DD}".count) // -> 1
print("A\u{20DD}\u{20DD}".count) // -> 1
print("A\u{20DD}\u{20DD}\u{20DD}".count) // -> 1
print("A\u{20DD}\u{20DD}\u{20DD}\u{20DD}".count) // -> 1
となります。
たとえば、Character
の集合として、{ c | c は"A"から"Z"までの文字またはその文字に結合文字が結合した文字 }
を定義したいとして、この集合は(理論的には)無限集合となります1。
高々有限個の文字の集合であればSet<Character>
を使えますが、汎用的なCharacter
のsetを考えるなら、自分で実装するしかありません。
Rangeの種類が足りない!
というわけで、前回の記事のようにpredicateで集合を定義することを考えました。そうすれば、有限だろうが無限だろうが集合を定義することはできるはずです。
Character
はComparable
プロトコルに準拠しているので、Range
を使えばCharacter
の集合を表すことができそうです。たとえば、前述の{ c | c は"A"から"Z"までの文字またはその文字に結合文字が結合した文字 }
を{ c | cは"A"以上"["未満の文字 }
2というように考えれば、Swift的には"A"..<"["
というRange
に含まれる文字というpredicateを集合の定義に使えば良さそうです。
しかし、今考えているのは汎用的なCharacter
のsetなので、SetAlgebra
に準拠させることを考えなくてはいけません。そこで問題となるのが、たとえばsubtracting(_:)
といったメソッドです。
さて、突然ですが、《"A"..<"["
というRange
で定義される集合》から《"A"..."C"
というClosedRange
で定義される集合》を引いたらどうなるでしょう?
アルファベットでCの次はDだから…《"D"..<"["
というRange
で定義される集合》ですか?
いいえ、違います。
さっき紹介したU+20DDという結合文字を使った"C\u{20DD}"
というCharacter
について考えてみましょう。"C\u{20DD}"
は当然"A"..<"["
に含まれます。そして、"C\u{20DD}"
は"C"よりも大きいので"A"..."C"
には含まれません。即ち【《"A"..<"["
というRange
で定義される集合》から《"A"..."C"
というClosedRange
で定義される集合》を引いた集合】に"C\u{20DD}"
は含まれているはずです3。
したがって、この問題の答えは{ c | cは"C"より大きく"["未満の文字 }
で示される集合となります。しかし、残念ながらSwiftには下端(lower bound)を含まない範囲を表す構造体が標準では存在しません。
というわけで、そういった範囲を表す構造体を実装する必要があります。それが、以前2回に渡って記事にした4SwiftRangesです。
こうして、なんとかCharacter
のsetを作ることができそうなところまで来ました。
Character
の比較結果が安定しない
Character
がComparable
に準拠しているのは確かなのですが、Swiftのバージョンによってその比較結果が違う場合があるのです5。
たとえば、"Ä"という文字があります。Unicodeにおける名前は"Latin Capital Letter A with Diaeresis"で、Aというアルファベットにウムラウト(またはトレマ)が付いた文字です。スカラ値はU+00C4が割り当てられていますが、当然"A"(U+0041)に結合文字としてのウムラウト(U+0308)が続く2つのスカラ値で表される書記素クラスタも同じ文字を表します:
"\u{00C4}" == "\u{0041}\u{0308}" // -> true
問題はこの文字が"B"より小さいのかどうかということです:
"\u{00C4}" < "B" // -> 結果は?
結論から言うと、Swift 4.1.2 (Xcode 9.4.1)ではtrue
です。そして、Swift 4.2 (Xcode 10)ではfalse
です。さらにいえば、(意外なことに?)Swift 4.1.50 (Xcode 10のSwift 4.1互換モード)でもfalse
です。
このことを把握していればいいかもしれませんが、場合によってはSwiftのバージョンを意識せずにコードを書いてバグを生み出す可能性もあります。
というわけで、SwiftBonaFideCharacterSetでは、DecomposedCharacter
とPrecomposedCharacter
という構造体を用意。それぞれ、常にNFDとNFCでnormalizeされ、比較の際にはユニコードスカラ値の配列として大小を比較する設計となっています:
DecomposedCharacter("Ä") < DecomposedCharacter("B")
// 常にtrue
PrecomposedCharacter("Ä") < PrecomposedCharacter("B")
// 常にfalse
こうしてsetのほうもBonaFideCharacterSet
のほか、DecomposedCharacterSet
とPrecomposedCharacterSet
も用意することになりました。
そんなにたくさん構造体を実装したくない
でも、同じような構造体を3つも別々で実装するなんて面倒ですよね。DRYという言葉も耳にタコができるぐらい聞きますし。
そこでまずは、CharacterExpression
というプロトコルを用意して、Character
もDecomposedCharacter
もPrecomposedCharacter
も、そのプロトコルに準拠することにします6。
そうすれば、あとは前回の記事で紹介したTotallyOrderedSet<Element>
を使って次のようにするだけでよくなります:
public typealias CharacterExpressionSet<C> = TotallyOrderedSet<C> where C:CharacterExpression
public typealias BonaFideCharacterSet = CharacterExpressionSet<Character>
public typealias DecomposedCharacterSet = CharacterExpressionSet<DecomposedCharacter>
public typealias PrecomposedCharacterSet = CharacterExpressionSet<PrecomposedCharacter>
そして、たとえばStringProtocol
に対する拡張も次のように簡単にできます:
extension StringProtocol {
public func split<C>(separator:CharacterExpressionSet<C>,
maxSplits:Int = Int.max,
omittingEmptySubsequences:Bool = true) -> [SubSequence]
where C:CharacterExpression
{
return self.split(maxSplits:maxSplits,
omittingEmptySubsequences:omittingEmptySubsequences,
whereSeparator:{ separator.contains(C($0)) })
}
}
これでDRYは達成…ですかね?
おわりに
というわけで、“本当の”CharacterSetを作ってみたというお話でした。ただ、作ってみたのはいいけど、役に立つのかどうかは分からないというオチなのでした。
-
非可算無限集合かな? ↩
-
"Z"がU+005Aで、"["がU+005Bなので…とりあえず。 ↩
-
実際には
DecomposedCharacter
とPrecomposedCharacter
は共通点が多いので、CharacterExpression
を継承したNoramalizedCharacter
というプロトコルを挟んでいます。 ↩