Lambdaカクテル

京都在住Webエンジニアの日記です

Invite link for Scalaわいわいランド

Scalaの正規表現で名前付きキャプチャグループを使う

こんな記事を読んだ。

blog.s2n.tech

正規表現はメンテ不可能になりがちなので、キャプチャグループに名前を付けたり、キャプチャさせなかったりしてメモリを節約するのは良いこと。

Scalaでも同じことができそうだったけれどはっきりとは覚えていなかったので復習した。

名前付きキャプチャグループ

Scalaにはregex literalがないので.rメソッドでStringをRegexに変換して使う。

正規表現では、改行やバックスラッシュを含むような表現が頻出するため、"""string"""raw"string"といった記法(前者は改行が可能)を使って文字列リテラルを定義すると便利だから覚えておこう。

val Re = """v(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)""".r

Re.findFirstMatchIn("v1.2.3") match {
  case Some(mtch) => mtch.group("major")
  case None       => ???
}

正規表現を文字列にマッチさせ、マッチオブジェクトを取り出すにはRegex.findFirstMatchInを使う。

そして、文字列に該当するマッチグループの文字列を取り出すにはMatch.group()を使う。

www.scala-lang.org

ちなみに、名前付きキャプチャグループを使わない場合、Scalaでは次のように書いたほうが便利だ:

val Re = """v(\d+)\.(\d+)\.(\d+)""".r

"v1.2.3" match {
  case Re(major, minor, patch) => ???
}

このように書けるのは、Regexはunapplyを実装しているので、文字列をRegexにmatchさせるといった表現ができるためだ。残念ながらこの方法ではMatchオブジェクト自体は取り出せない。

後方参照

後方参照もScalaで可能だ。

val SameRe = """v(?<version>\d+)\.\k<version>\.\k<version>""".r

"v7.7.7" match {
  case SameRe(_:_*) => "matched"
}

case SameRe(_:_*)と書いているのは、いくつマッチしてもその結果を捨てるという意味。数が合わなくて実行時エラーになるのを防いでいる。

非キャプチャグループ

非キャプチャグループもScalaで動作する。

val HelloWorldRe = """(?:Hello|Goodbye), world!""".r

"Hello, world!" match {
  case HelloWorldRe(_:_*) => "matched"
}

元記事ではregexに^$を付けていたが、Scalaのregexはデフォルトでanchorされているので^$は使わなくてよい(必要に応じて.unanchoredする)。

まとめ

Scalaでも名前付きキャプチャグループや非キャプチャグループを使うことができることがわかった。また、一定の場合にはmatch構文と組み合わせると便利なこともわかった。

matchと名前付きキャプチャグループの相性が悪いので、なんか良い方法がないだろうかと思う。また、リテラルでそのまま正規表現を書いている場合はコンパイル時にキャプチャグループの名前は判明するはずなので、コンパイラが支援して自動的に名前付きキャプチャグループを安全に利用できるといいのにと思う。

ちなみにESLintのprefer-named-capture-groupにあたるlinterは見当らなかった。wartremoverとかで作ってみるのも面白いと思う。

★記事をRTしてもらえると喜びます
Webアプリケーション開発関連の記事を投稿しています.読者になってみませんか?