Sponsor Link
環境&対象
- macOS Monterey 12.5 Beta
- Xcode 14.0 beta
- iOS 16.0 beta
正規表現
正規表現は、文字列を評価して特定のフォーマットに従って情報を取得することに使用されます。
^[0-9]*$ みたいな謎の文字列が特徴的です。
以降では、Markdown をパースできる正規表現を Regex で書き直してみて、様子(?)を見てみます。
MarkdownHeader をパースする
Markdown の Header は、文頭から # が複数個(最大6個)連続し、その後に スペース が1つあり、その後 Header 文字列があるという構造になっています。
なお、パースした結果として、Header にマッチしたかどうか だけではなく、# がいくつかあったか、Header 文字列は何か という情報も取得できるようにする必要があります。
従来の方法
Swft5.7 以前は、正規表現を String で表現し、その String を使って、NSRegularExpression を生成して使用していました。
なお、Markdown の Header をパースするための表現は以下です。
^((?<Mark>#{1,6})\s(?<Text>.*))$
文頭からの # 1~6 個にマッチする文字列を “Mark” という名称でキャプチャしています。
その後、\s で1つのスペースにマッチします。(特に キャプチャしません)
その後で、1文字以上の文字列を “Text” という名称でキャプチャしています。
Markdown の Header は構造がシンプルなので、従来の正規表現を使った表現もシンプルですが、ぱっと見 わかりやすいかというと微妙です。
個人的には、キャプチャし始めると 階層構造が入り始め 謎の文字列集合になりがちな気がします。
以下の関数は、与えられた文字列の Range について Markdown の Header にマッチするかを返します。(Range としては、1行が与えられる想定です。複数箇所にマッチすることを想定していません)
func parseHeader(_ str: S, range: NSRange) -> (headerMark: S.SubSequence,
headerString: S.SubSequence)? {
let pattern = #"^((?<Mark>#{1,6})\s(?<Text>.*))$"#
let regEx = try! NSRegularExpression(pattern: pattern)
let matches = regEx.matches(in: String(str), options: [], range: range)
guard let firstMatch = matches.first else { return nil } // 1 line has one match at most
return ( str[Range(firstMatch.range(withName: "Mark"), in:str)!],
str[Range(firstMatch.range(withName: "Text"), in:str)!])
}
ちなみに、正規表現としておかしな記述がされていると、実行時に、 NSRegularExpression 生成時に例外が発生します。
Regex 型
Swift5.7 では、正規表現向けの型として Regex が導入されました。
先の正規表現を Regex にして定義してみます。
従来の正規表現は一旦 String 型で記述され、その後 NSRegularExpression を作成していました。
Regex 型は、String 型を使って直接定義できます。
見た目は似ていますが、#”…”# や “…” という形ではなく、/…/ という形で記述します。
let pattern = /^(?<Mark>#{1,6})\s(?<Text>.*)/
この時に定義された変数 pattern は、Regex 型となっています。
ポイントとしては、変数定義時にすでに 正規表現としての妥当性がチェックされている点です。
例えば、キャプチャするための ( と ) が同じ数記述されていないと エラーになります。
このことから、文字列としてではなく 正規表現としての妥当性をチェックしていることがわかります。
正規表現のフォーマットチェック例
例えば、( を1つ多くなるようにした正規表現で Regex を定義しようとすると以下のようなエラーになります。
このことは、Swift が普段実現しようとしている 強い型づけの1つです。 このように具体的な型に応じたチェックを可能とすることで、より早い段階でのエラーが検知できます。
なお、あくまで、正規表現としての定義のチェックであり、書いた人の意図通りかはチェックできません。
RegexBuilder
Regex という型が導入され、ある程度(?) 事前にチェックされるようになりましたが、それでも、おまじない的な文字列を書く必要があり すこし難しいです。
Swift 自身には、ResultBuilder という DSL (Domain Specific Language) をサポートしやすくするための仕組みを導入しています。その代表例は、SwiftUI ですが、正規表現に対しても、ResultBuilder を使って記述できるようになりました。
なお、RegexBuilder で 定義・生成する型は、Regex 型です。
RegexBuilder を使って、Markdown Header をパースする
Markdown の Header をパースするための Regex を RegexBuilder を使って記述すると以下のようになります。
let headerMarks = /#{1,6}/
let pattern = Regex {
headerMarks
CharacterClass.whitespace
ZeroOrMore(.any)
}
テキストを使った正規表現と比較すると、読みやすさが上がっていることがわかります。
RegexBuilder 表記で、#{1,6} を簡単に表現する方法を見つけることができませんでした・・・orz
上記のように従来の表記も簡単に組み入れることができるのは、RegexBuilder の利点の1つです。
#{1,6} は、Repeat を使って、以下のように書くことができます。
Repeat(1...6) {
"#"
}
Xcode 上で、既存の正規表現で記述した後に、コンテキストメニューの “Refactor”-“Convert to Regex Builder” メニューの機能を使うと Regex Builder の表記に変換されるので 便利です。
RegexBuilderでのキャプチャ
正規表現ではマッチした文字列を記憶しておくことを キャプチャと呼びますが、RegexBuilder でのキャプチャは、文字通り Capture で行います。
let headerMarks = /#{1,6}/
let pattern = Regex {
Capture {
headerMarks
}
CharacterClass.whitespace
Capture{
ZeroOrMore(.any)
}
}
上記では、headerMarks にマッチした箇所と、ホワイトスペースの後の文字列をそれぞれキャプチャしています。
キャプチャした文字列は、以下のように参照することができます。
if let match = String(str).wholeMatch(of: pattern) {
print(match.output.1) // headerMarks
print(match.output.2) // ZeroOrMore(.any)
}
なお、マッチした文字列全体は、match.output.0 で参照することができます。
この辺りは、従来の正規表現のマッチ結果である NSTextCheckingResult の range(at:0) でマッチした文字列全体が取得できるのと同じです。
なお、ResultBuilder のお約束(?) で、子要素として 10 以上の Capture を持つことはできません。
名前付きキャプチャ
正規表現でパースするときに、以前の正規表現での (?<name>…) のようにキャプチャに名前をつけたくなります。
もちろん、RegexBuilder の Capture でもサポートされています。
func parseHeaderWithRegexBuilder(_ str: S) -> (headerLevel: Substring,
headerString: Substring)? {
let headerMarks = /#{1,6}/
let refMark = Reference(Substring.self)
let refText = Reference(Substring.self)
let pattern = Regex {
Capture(headerMarks, as: refMark)
CharacterClass.whitespace
Capture(ZeroOrMore(.any), as: refText)
}
guard let match = String(str).wholeMatch(of: pattern) else { return nil }
return (match[refMark], match[refText])
}
match したキャプチャは、Reference を使用した subscript 経由でアクセスできるようになります。
この Reference は、初期化時に指定された型を持つため、アクセスする時にも 型をチェックしたアクセスが可能となります。
まとめ
Swift5.7 で導入された Regex と RegexBuilder を説明しました。
- Regex を使うと、正規表現をコンパイル時にチェックできる
- RegexBuilder を使うと、Declarative に正規表現を記述できる
説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。
Swift学習におすすめの本
詳解Swift
Swift の学習には、詳解 Swift という書籍が、おすすめです。
著者は、Swift の初期から書籍を出していますし、Swift の前に主力言語だった Objective-C という言語についても同様の書籍を出しています。
最新版を購入するのがおすすめです。
現時点では、上記の Swift 5 に対応した第5版が最新版です。
Swift ポケットリファレンス
Swift を学んでも、プログラミング言語の文法を全て記憶しておくことは無理なので、ちょっとした文法の確認をするために、リファレンス本を手元に置いておくと便利です。
Swift4 までしか対応していないので、相違点を理解して参照する必要があります。
そろそろ Swift5 に対応した版が欲しいですね・・・
Sponsor Link