「正規表現が読みやすくてもいいじゃない」 Composed Regex パターン

# 「正規表現が読みやすくてもいいじゃない」 Composed Regex パターン

## 概要
Martin Fowler氏のblikiで2009年に紹介されていた
パターンである ***Composed Regex パターン*** について。

[Composed Method パターン](http://qiita.com/tbpgr/items/06266a4161019b445756)の 正規表現版ですね。

正規表現は非常に便利ですが、正規表現の達人が書いたコードは
読み解くのが非常に難しいです。
正規表現に慣れた開発者であっても、「濃い正規表現」については
一度書いて、しばらく経ってから読んだ時に理解に多少なりとも時間がかかるのではないでしょうか。

## Composed Regex パターン
### サンプル仕様
パイプ区切りでストリートファイター2の技データが記録されているファイルから
正規表現でデータを抽出して、技モデルに設定します。

* フォーマット

~~~
コマンド|技名|カナ|発生フレーム
~~~

使用言語はRubyです。

※厳密なゲームの仕様と異なりますがあくまでサンプルということで。

### ファイルサンプル

~~~
236P|波動拳|はどうけん|11
214K|竜巻旋風脚|たつまきせんぷうきゃく|11
623P|昇竜拳|しょうりゅうけん|4
~~~

### 適用前

~~~ruby
Skill = Struct.new(:command, :name, :kana, :frame)
class Skill
  define_method :to_s do
    <<-EOS
--------------------------
コマンド     = #{command}
名前         = #{name}
カナ         = #{kana}
発生フレーム = #{frame}
--------------------------
    EOS
  end
end

src = DATA.read
skills = src.split("\n").each_with_object([]) do |line, skills|
  _, command, name, kana, frame = */([0-9]+[PK]?)\|(.+)\|(.+)\|(\d+)/.match(line)
  skills << Skill.new(command, name, kana, frame)
end

puts skills

__END__
236P|波動拳|はどうけん|11
214K|竜巻旋風脚|たつまきせんぷうきゃく|11
623P|昇竜拳|しょうりゅうけん|4
~~~

出力

~~~
--------------------------
コマンド     = 236P
名前         = 波動拳
カナ         = はどうけん
発生フレーム = 11
--------------------------
--------------------------
コマンド     = 214K
名前         = 竜巻旋風脚
カナ         = たつまきせんぷうきゃく
発生フレーム = 11
--------------------------
--------------------------
コマンド     = 623P
名前         = 昇竜拳
カナ         = しょうりゅうけん
発生フレーム = 4
--------------------------
~~~

### Rubyの正規表現の xオプション でコメントを付与してみる

コメントの付与でも大分可読性はあがります。
Martinも可読性の低い正規表現に対して、コメントがないよりはあったほうが良いとしています。
しかし、コード自体で可読性を上げることができるならそちらのほうがより良い選択肢である、
と述べています。

~~~ruby
Skill = Struct.new(:command, :name, :kana, :frame)
class Skill
  define_method :to_s do
    <<-EOS
--------------------------
コマンド     = #{command}
名前         = #{name}
カナ         = #{kana}
発生フレーム = #{frame}
--------------------------
    EOS
  end
end

src = DATA.read
skills = src.split("\n").each_with_object([]) do |line, skills|
  _, command, name, kana, frame = */
      ([0-9]+[PK]?) # コマンド
      \|
      (.+)          # 名前
      \|
      (.+)          # カナ
      \|
      (\d+)         # 発生フレーム
    /x.match(line)
  skills << Skill.new(command, name, kana, frame)
end

puts skills

__END__
236P|波動拳|はどうけん|11
214K|竜巻旋風脚|たつまきせんぷうきゃく|11
623P|昇竜拳|しょうりゅうけん|4
~~~

### Composed Regex パターン適用1段階目

各抽出パターンを定数化することで、コメントが不要になりました

~~~ruby
Skill = Struct.new(:command, :name, :kana, :frame)
class Skill
  define_method :to_s do
    <<-EOS
--------------------------
コマンド     = #{command}
名前         = #{name}
カナ         = #{kana}
発生フレーム = #{frame}
--------------------------
    EOS
  end
end

module SkillRegexps
  COMMAND = '([0-9]+[PK]?)'
  NAME = '(.+)'
  KANA = '(.+)'
  FRAME = '(\d+)'
end

src = DATA.read
skills = src.split("\n").each_with_object([]) do |line, skills|
  _, command, name, kana, frame = */
      #{SkillRegexps::COMMAND}
      \|
      #{SkillRegexps::NAME}
      \|
      #{SkillRegexps::KANA}
      \|
      #{SkillRegexps::FRAME}
    /x.match(line)
  skills << Skill.new(command, name, kana, frame)
end

puts skills

__END__
236P|波動拳|はどうけん|11
214K|竜巻旋風脚|たつまきせんぷうきゃく|11
623P|昇竜拳|しょうりゅうけん|4
~~~

### Composed Regex パターン適用2段階目

パイプ区切りの構造がハードコーディングなので、
メソッドとして抽出します。 (Extract Method)

~~~ruby
Skill = Struct.new(:command, :name, :kana, :frame)
class Skill
  define_method :to_s do
    <<-EOS
--------------------------
コマンド     = #{command}
名前         = #{name}
カナ         = #{kana}
発生フレーム = #{frame}
--------------------------
    EOS
  end
end

module SkillRegexps
  COMMAND = '([0-9]+[PK]?)'
  NAME = '(.+)'
  KANA = '(.+)'
  FRAME = '(\d+)'
end

def join_skill_regexp_patterns(*skill_attrributes)
  /#{skill_attrributes.join('\|')}/
end

src = DATA.read
skill_regexp = join_skill_regexp_patterns(SkillRegexps::COMMAND, SkillRegexps::NAME, SkillRegexps::KANA, SkillRegexps::FRAME)
skills = src.split("\n").each_with_object([]) do |line, skills|
  _, command, name, kana, frame = *skill_regexp.match(line)
  skills << Skill.new(command, name, kana, frame)
end

puts skills

__END__
236P|波動拳|はどうけん|11
214K|竜巻旋風脚|たつまきせんぷうきゃく|11
623P|昇竜拳|しょうりゅうけん|4
~~~

## 参照
http://martinfowler.com/bliki/ComposedRegex.html