唯物是真 @Scaled_Wurm

プログラミング(主にPython2.7)とか機械学習とか

正規表現でFizzBuzz

FizzBuzzを正規表現で解きたくなったので解いた
知ってる人が多いと思いますが念のため問題の内容を説明しておくと、入力された整数が3の倍数のときはFizz、5の倍数のときはBuzz、15の倍数のときはFizzBuzz、その他のときは数値をそのまま出力します

正規表現

というわけで書いた正規表現のコマンドを以下に書いておきます(seq 1 100は入力の整数列)
seq 1 100 | perl -pe 's/^(\d*[05])$/$1Buzz/;s/^((?:[0369]|[147](?:[0369]|[147][0369]*[258])*(?:[258]|[147][0369]*[147])|[258](?:[0369]|[258][0369]*[147])*(?:[147]|[258][0369]*[258]))+)(?=\D)/$1Fizz/;s/^\d+(?=B|F)//'

簡単に説明

5の倍数

^(\d*[05])$
末尾が0か5ならよいので判定は簡単です

3の倍数

(?:[0369]|[147](?:[0369]|[147][0369]*[258])*(?:[258]|[147][0369]*[147])|[258](?:[0369]|[258][0369]*[147])*(?:[147]|[258][0369]*[258]))+
以下の記事の正規表現を参考にしてほぼそのまま使わせていただきました
d.hatena.ne.jp

上の記事を読んだほうがわかりやすいと思いますが、雑に説明しておきます
ある数の各桁の和が3の倍数であれば、その数は3の倍数であることが知られています
それぞれの数字は3で割ったあまりが0になる数[0369]、1になる数[147]、2になる数[258]に分けることができます
3の倍数は、あまりが0の状態から変化して0の状態に戻るループとなる数字列の繰り返しとして考えることができます
以下の正規表現は。あまりが0から変化しない[0369]、あまりが1増えてから0に戻る[147](?:[0369]|[147][0369]*[258])*(?:[258]|[147][0369]*[147])、あまりが2増えてから0に戻る[258](?:[0369]|[258][0369]*[147])*(?:[147]|[258][0369]*[258])のそれぞれの繰り返しを表しています
わかりやすくするために改行を入れると、以下のそれぞれの3つの行の部位が上で説明した3つの遷移に対応しています

(?:
  [0369]
  | [147](?:[0369]|[147][0369]*[258])*(?:[258]|[147][0369]*[147])
  | [258](?:[0369]|[258][0369]*[147])*(?:[147]|[258][0369]*[258])
)+

可視化したらこんな感じになりました
f:id:sucrose:20180324234552p:plain

文字列の置換

というわけで3の倍数と5の倍数が求められるようになったので15の倍数も求めることができるようになっています
後は簡単にコマンドで置換している部分を説明しておきます

一つ目の置換までを適用すると、5の倍数の最後にBuzzが付けられます
s/^(\d*[05])$/$1Buzz/;

1
2
3
4
5Buzz
6
7
8
9
10Buzz
11
12
13
14
15Buzz
16
17
18
19
20Buzz

2つ目の正規表現では、3の倍数の後にFizzを付けます
s/^((?:[0369]|[147](?:[0369]|[147][0369]*[258])*(?:[258]|[147][0369]*[147])|[258](?:[0369]|[258][0369]*[147])*(?:[147]|[258][0369]*[258]))+)(?=\D)/$1Fizz/;

1
2
3Fizz
4
5Buzz
6Fizz
7
8
9Fizz
10Buzz
11
12Fizz
13
14
15FizzBuzz
16
17
18Fizz
19
20Buzz

最後にFizzBuzzが付いている行の数字を消して終わりです
s/^\d+(?=B|F)//

1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
16
17
Fizz
19
Buzz

追記

以下の記事で少し正規表現を改良しました
sucrose.hatenablog.com
改良後の正規表現
seq 1 10000 | perl -pe 's/^(\d*[05])$/$1Buzz/;s/^(?>([0369]|([147](?1)*)(?:([258](?1)*)|(?2){2})|(?3)(?:(?2)|(?3){2})))+(?=\D)/$1Fizz/;s/^\d+(?=B|F)//