[Swift][Swift5.7] Regex/RegexBuilder の使い方

     

TAGS:

Swift5.7 で追加された Regex/RegexBuilder の使い方を説明していきます。

環境&対象

以下の環境で動作確認を行なっています。

  • 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 を定義しようとすると以下のようなエラーになります。

ErrorInRegex
MEMO
このことは、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)
    }

テキストを使った正規表現と比較すると、読みやすさが上がっていることがわかります。

MEMO
RegexBuilder 表記で、#{1,6} を簡単に表現する方法を見つけることができませんでした・・・orz
上記のように従来の表記も簡単に組み入れることができるのは、RegexBuilder の利点の1つです。

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
  • Regex を使うと、正規表現をコンパイル時にチェックできる
  • RegexBuilder を使うと、Declarative に正規表現を記述できる

説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。

Swift学習におすすめの本

詳解Swift

Swift の学習には、詳解 Swift という書籍が、おすすめです。

著者は、Swift の初期から書籍を出していますし、Swift の前に主力言語だった Objective-C という言語についても同様の書籍を出しています。

最新版を購入するのがおすすめです。

現時点では、上記の Swift 5 に対応した第5版が最新版です。

Swift ポケットリファレンス

Swift を学んでも、プログラミング言語の文法を全て記憶しておくことは無理なので、ちょっとした文法の確認をするために、リファレンス本を手元に置いておくと便利です。

注意
Swift4 までしか対応していないので、相違点を理解して参照する必要があります。

そろそろ Swift5 に対応した版が欲しいですね・・・

コメントを残す

メールアドレスが公開されることはありません。