hogashi.*

日記から何から

正規表現でmasawada

 masawada Advent Calendar 2021 - Adventar 10日目の記事です。昨日の id:Sixeight さんに見事に釘を刺されてしまったので、ありがたく受け取り、whywaita Advent Calendar 2021 - Adventarはこちらに書きました → 秘蔵UserCSS大放出祭 - hogashi.*。大目に見てください!


 id:masawada / id:whywaita Advent Calendar 七五三おめでとうございます。 7, 5, 3 と素数ですね*1ゴルゴ13が好きな id:hogashi です。
 七五三は 11月 15日にやるそうです。 11 は素数だし、 15 は 3+5+7 ですね。 3×5 でも 15 になります。
 能では、 7歳ごろから稽古を始めるので、このあたりを初心というそうで*2世阿弥曰く、このころに自然に出る風情を大事にするのが良いようです。一方僕は 24歳ですが、このあたりは青年期といい「初心を忘れず稽古すべし」な年頃ということでした。
 3, 5, 15, 初心を忘れず……というわけで FizzBuzz について書きます。正規表現が好きなので正規表現でやってみました*3。せっかくなので masa と wada でいきます。

正規表現FizzBuzz とは

 今回は、入力に対して正規表現でパターンマッチして置換して出力することで、 FizzBuzz もとい masawada を実現することにしました。数字が勝手に入ってくるので、順番にパターンマッチして、 3の倍数かつ 5の倍数なら masawada と置き換える、みたいな感じ。趣味なので速度とかは何も考えてません。パターンが書きたかった。

できあがったものがこちらです

 大きい正規表現を見やすく書きつつ入出力をシュッとやるのに Perl は便利ですね。 8桁くらいまでの数字で試してみたところ、多分あってそう。 GitHub にも公開してあります GitHub - hogashi/fizzbuzz-regexp: FizzBuzz in RegExp

ここを開くと正規表現(が書かれた Perl の長いコード)がバーンと出ます
my $from = 1;
my $to = 30;
for my $i ($from .. $to) {
    # どの順で適用しても大丈夫なようにそれぞれ独立に書いている

    # 3の倍数 かつ 5の倍数ではない
    # - 最後が0と5以外で, 合わせて3の倍数
    $i =~ s/^
        ( # 合わせて3の倍数となるセットのパターン
            # 3の倍数
            [0369]
            |
            # 1,4,7: 3で割った余りが1
            # 2,5,8: 3で割った余りが1
            # 1,4,7のどれかひとつと2,5,8のどれかひとつがあると, 合わせて3の倍数(間に3の倍数があってもよい)
            [147][0369]*[258]
            |
            # 同上, 並びが違うもの
            [258][0369]*[147]
            |
            # 1,4,7のどれかひとつずつが3つあると, 合わせて3の倍数(間に3の倍数があってもよい)
            ([147][0369]*){2}[147]
            |
            # 2,5,8のどれかひとつずつが3つあると, 合わせて3の倍数(間に3の倍数があってもよい)
            ([258][0369]*){2}[258]
            |
            # 22121... のように, 3で割った余りが 2→1→2→1→... と続くパターン
            # これは3で割った余りが2の数の次に, 3で割った余りが2の数→3で割った余りが1の数, というセットが繰り返されると起きる
            # 3の倍数となる場合は, 2→1→...→2→1→1 か 2→1→...→2→1→2→2 のどちらか
            ( # まず3で割った余りが2の数から始まる
                # 1,4,7のどれかひとつずつが2つあると, 合わせて3で割った余りが2(間に3の倍数があってもよい)
                [147][0369]*[147]
                |
                # 2,5,8は3で割った余りが2
                [258]
            )
            ( # 上で拾った数(3で割った余りが2の数)に続いて, 3で割った余りが →1→2→1→2→... と続くパターン (間に3の倍数があってもよい)
                # 2,5,8が続くと3で割った余りが1になり,
                # 1,4,7が続くと3で割った余りが2になる
                # この並び以外のときは, 合わせて3の倍数になるセットのほうでマッチできるはず
                [0369]*[258][0369]*[147]
            )*
            # この間に3の倍数があってもよい
            [0369]*
            ( # 最後に →1 か →2→2 のどちらかで終わると, 合わせて3の倍数となる
                # →1 のパターン
                [147]
                |
                # →2→2 のパターン(間に3の倍数があってもよい)
                [258][0369]*[258]
            )
        )* # 下で1桁はマッチするのでここは2桁目以降になるので, 0回以上の繰り返し
        ( # 合わせて3の倍数となるセットのパターンのうち, 最後の桁が0,5以外
            # 最後の桁で0が出る可能性があったのはここだったので0を削る
            [369]
            |
            [147][0369]*[28]
            |
            [258][0369]*[147]
            |
            ([147][0369]*){2}[147]
            |
            ([258][0369]*){2}[28]
            |
            (
                [147][0369]*[147]
                |
                [258]
            )
            (
                [0369]*[258][0369]*[147]
            )*
            [0369]*
            (
                [147]
                |
                # 最後の桁で5が出る可能性があったのはここ(右の[258])だったので5を削る
                [258][0369]*[28]
            )
        )
        $/masa/x;
    # 3の倍数ではない かつ 5の倍数
    # - 最後が5のとき, 最終的に3の倍数にならないためには, それより前の桁までが, 合わせて3で割った余りが1にならなければよい
    #   - まず「合わせて3の倍数」な桁セットがあるか, 何もない
    #   - その次に「合わせて3で割った余りが2」な桁セットがあるか, 何もない
    #     - つまり「合わせて3で割った余りが1」な桁セットがない
    #   - その次に「合わせて3の倍数」な桁セットがあるか, 何もない
    #   - 最後は5
    # - 最後が0のとき, 最終的に3の倍数にならないためには, それより前の桁までが, 合わせて3の倍数にならなければよい
    #   - まず「合わせて3の倍数」な桁セットがあるか, 何もない
    #   - その次に「合わせて3で割った余りが1」か「合わせて3で割った余りが2」な桁セットがある
    #   - その次に「合わせて3の倍数」な桁セットがあるか, 何もない
    #   - 最後は0
    $i =~ s/^
        (
            # 最後が5のパターン
            ( # 合わせて3の倍数となるセットのパターン
                [0369]
                |
                [147][0369]*[258]
                |
                [258][0369]*[147]
                |
                ([147][0369]*){2}[147]
                |
                ([258][0369]*){2}[258]
                |
                (
                    [147][0369]*[147]
                    |
                    [258]
                )
                (
                    [0369]*[258][0369]*[147]
                )*
                [0369]*
                (
                    [147]
                    |
                    [258][0369]*[258]
                )
            )* # 3の倍数はいくらでもあってよいし, なくてもよい
            ( # 3で割った余りが2なセットのパターン(これがあると最後の桁が5なので最終的に3で割った余りは1になる)
                [258]
                |
                [147][0369]*[147]
            )? # なくてもよい(ないと最後の桁が5なので最終的に3で割った余りは2になる)
            ( # 合わせて3の倍数となるセットのパターン
                [0369]
                |
                [147][0369]*[258]
                |
                [258][0369]*[147]
                |
                ([147][0369]*){2}[147]
                |
                ([258][0369]*){2}[258]
                |
                (
                    [147][0369]*[147]
                    |
                    [258]
                )
                (
                    [0369]*[258][0369]*[147]
                )*
                [0369]*
                (
                    [147]
                    |
                    [258][0369]*[258]
                )
            )* # 3の倍数はいくらでもあってよいし, なくてもよい
            # 最後は5
            5
            |
            # 最後が0のパターン
            ( # 合わせて3の倍数となるセットのパターン
                [0369]
                |
                [147][0369]*[258]
                |
                [258][0369]*[147]
                |
                ([147][0369]*){2}[147]
                |
                ([258][0369]*){2}[258]
                |
                (
                    [147][0369]*[147]
                    |
                    [258]
                )
                (
                    [0369]*[258][0369]*[147]
                )*
                [0369]*
                (
                    [147]
                    |
                    [258][0369]*[258]
                )
            )* # 3の倍数はいくらでもあってよいし, なくてもよい
            ( # 3で割った余りが1 or 2なセットのパターン(最後の桁が0なので最終的に3で割った余りは1 or 2になる)
                # 3で割った余りが1
                [147]
                |
                # 3で割った余りが2
                [258]
                |
                # 3で割った余りが2
                [147][0369]*[147]
                |
                # 3で割った余りが1
                [258][0369]*[258]
            )
            ( # 合わせて3の倍数となるセットのパターン
                [0369]
                |
                [147][0369]*[258]
                |
                [258][0369]*[147]
                |
                ([147][0369]*){2}[147]
                |
                ([258][0369]*){2}[258]
                |
                (
                    [147][0369]*[147]
                    |
                    [258]
                )
                (
                    [0369]*[258][0369]*[147]
                )*
                [0369]*
                (
                    [147]
                    |
                    [258][0369]*[258]
                )
            )* # 3の倍数はいくらでもあってよいし, なくてもよい
            # 最後は0
            0
        )
        $/wada/x;
    # 3の倍数 かつ 5の倍数
    # - 最後が0のとき
    #   - それまでの桁が合わせて3の倍数
    # - 最後が5のとき
    #   - まず「合わせて3の倍数」な桁セットがある
    #   - その次に「合わせて3で割った余りが1」な桁セットがある
    #   - その次に「合わせて3の倍数」な桁セットがある
    #   - 最後は5
    $i =~ s/^
        (
            # 最後が0のパターン
            ( # 合わせて3の倍数となるセットのパターン
                [0369]
                |
                [147][0369]*[258]
                |
                [258][0369]*[147]
                |
                ([147][0369]*){2}[147]
                |
                ([258][0369]*){2}[258]
                |
                (
                    [147][0369]*[147]
                    |
                    [258]
                )
                (
                    [0369]*[258][0369]*[147]
                )*
                [0369]*
                (
                    [147]
                    |
                    [258][0369]*[258]
                )
            )* # 3の倍数はいくらでもあってよいし, なくてもよい
            # 最後は0
            0
            |
            # 最後が5のパターン
            ( # 合わせて3の倍数となるセットのパターン
                [0369]
                |
                [147][0369]*[258]
                |
                [258][0369]*[147]
                |
                ([147][0369]*){2}[147]
                |
                ([258][0369]*){2}[258]
                |
                (
                    [147][0369]*[147]
                    |
                    [258]
                )
                (
                    [0369]*[258][0369]*[147]
                )*
                [0369]*
                (
                    [147]
                    |
                    [258][0369]*[258]
                )
            )* # 3の倍数はいくらでもあってよいし, なくてもよい
            ( # 合わせて3で割った余りが1なセットのパターン(最後の5と合わせて3の倍数となる)
                [147]
                |
                [258][0369]*[258]
            )
            ( # 合わせて3の倍数となるセットのパターン
                [0369]
                |
                [147][0369]*[258]
                |
                [258][0369]*[147]
                |
                ([147][0369]*){2}[147]
                |
                ([258][0369]*){2}[258]
                |
                (
                    [147][0369]*[147]
                    |
                    [258]
                )
                (
                    [0369]*[258][0369]*[147]
                )*
                [0369]*
                (
                    [147]
                    |
                    [258][0369]*[258]
                )
            )* # 3の倍数はいくらでもあってよいし, なくてもよい
            # 最後は5
            5
        )
        $/masawada/x;
    print $i . qq(\n);
}
実行するとこうなります
1
2
masa
4
wada
masa
7
8
masa
wada
11
masa
13
14
masawada
16
17
masa
19
wada
masa
22
23
masa
wada
26
masa
28
29
masawada

どう作ったのか

 ここからが長いので、適宜斜めに読んでください :pray:

最初思いついたやつ

 1 から並んでいると仮定できるなら、数字がなんであれ、 3個おきに masa 、 5個おきに wada 、 15個おきに masawada にしたらよいはずなので、 15個ずつ数字を置き換えまくっていくと完成するはず。

$ seq 1 30 | tr '\n' ',' | perl -pe 's|((?:\d+,){2})\d+,(\d+,)\d+,\d+,((?:\d+,){2})\d+,\d+,(\d+,)\d+,((?:\d+,){2})\d+,|${1}Fizz,${2}Buzz,Fizz,${3}Fizz,Buzz,${4}Fizz,${5}FizzBuzz,|g' | tr ',' '\n'

 ただこれだと、 15個まとめてマッチするので、 1 〜 40 とかで実行したとき 31 〜 40 は余ってしまってマッチせず、 33 とかがそのままになってしまうのでした。ちょっと無念。

Regexp::Assemble

 じゃあどうやるといいのかなと思って、とりあえず Regexp::Assemble *4 に入れてみたところ、いいヒントがもらえて、 3で割った余りが同じ数字の並びのパターンを頑張ってやっていくとできそうなことがわかりました。 111 とか 12 とかになってたら、ひとまとめにして 3の倍数とできるな、みたいな。

$ rassemble 1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz 16 17 Fizz 19 Buzz Fizz 22 23 Fizz Buzz 26 Fizz 28 29 FizzBuzz 31 32 Fizz 34 Buzz Fizz 37 38 Fizz Buzz 41 Fizz 43 44 FizzBuzz 46 47 Fizz 49 Buzz Fizz 52 53 Fizz Buzz 56 Fizz 58 59 FizzBuzz 61 62 Fizz 64 Buzz Fizz 67 68 Fizz Buzz 71 Fizz 73 74 FizzBuzz 76 77 Fizz 79 Buzz Fizz 82 83 Fizz Buzz 86 Fizz 88 89 FizzBuzz
[147][134679]?|[28][23689]?|Fizz(?:Buzz)?|Buzz|[36][12478]|5[23689]
作戦

 ひとつの置換作業でいっぺんにまとめてやろうと思うと、元の数が 3の倍数 (でない) かつ 5の倍数 (でない) という情報を置換するときに伝える必要があるけど、それは無理そうかな〜*5と思ったので、 masa, wada, masawada のそれぞれで計 3回置換することにしました。その代わりの頑張りポイントとして、どの順番でやっても成功するように、それぞれに依存しない独立したパターンを書きました *6*7

masa

 3の倍数かつ 5の倍数でない、というのは「全部の桁を足して 3の倍数、かつ、最後の桁が 0 でも 5 でもない」ということです。頑張って全部の桁を足して 3の倍数であることを確認しつつ、最後の桁が 0, 5 以外、というパターンを書きます。
 まず、数の桁のうちのある箇所が 3の倍数である、というパターンで思いつくのは、

  • (A) 0, 3, 6, 9 が何個でも
    • ...3930...
  • (B) 1, 4, 7 (3で割った余りが 1) が 1つと 2, 5, 8 (3で割った余りが 2) が 1つ
    • ...18...
    • (C) 前後逆でもよい
  • (D) 1, 4, 7 (3で割った余りが 1) が 3つ
    • ...174...
  • (E) 2, 5, 8 (3で割った余りが 2) が 3つ
    • ...255...

という感じになります (あとそれぞれの桁の間に 0, 3, 6, 9 が何個挟まっていてもよい)。これを正規表現に起こすとこう。

qr/
[0369]                 # (A)
|
[147][0369]*[258]      # (B)
|
[258][0369]*[147]      # (C)
|
([147][0369]*){2}[147] # (D)
|
([258][0369]*){2}[258] # (E)
/x

 これに加えて、 ...22 で始まり、 ...22121 と続き、 ...22121211... のようになる場合があります(それぞれの桁で 3で割った余りが同じなら同じなので ...25184271... でも同じ)。 3で割った余りを順番に見ていくと 2→1→2→1→2→1→2→0 で*8、言うなれば「C のパターンが、先頭の 2 と末尾の 1 の間に(1個以上)挟まれている」ような形になっています。先頭の 2 を遠くの末尾の 1 と結びつけるようなパターンは、上で出した A〜E にはないので、これのために別途パターンを書く必要があります。

f:id:hogashi:20211206015926j:plain

 とはいえ、このパターンは割と単純で、 221 か 1121 で始まり、 21 を繰り返し、 1 または 22 で終わります (12, 21, 111, 222 なら B〜E でマッチできる)。 21 を繰り返すのが面白いんですが、これは 3で割った余りが 2 の状態から 0 にならないように 2 で飛び越える感じで、個人的にはラダートレーニングのようなイメージがあります(下の図の②みたいな感じで、またぐために左足を大きく前に出す必要がある)。

https://item-shopping.c.yimg.jp/i/l/ishi0424_training-ladder_6

https://store.shopping.yahoo.co.jp/ishi0424/k-training-ladder.html

 というわけで正規表現に起こすとこうなります。もちろんこの場合も、それぞれの桁の間に 0, 3, 6, 9 が何個挟まっていてもよいです。

qr/
( # 先頭は 11 か 2
    [147][0369]*[147]
    |
    [258]
)
( # 21 の繰り返し
    [0369]*[258][0369]*[147]
)*
[0369]*
( # 最後に →1 か →2→2 のどちらかで終わる
    [147]
    |
    [258][0369]*[258]
)
/x

 ということで、 A 〜 E のパターンと、このラダーのパターンを合わせることで、 3の倍数を表すパターンが完成します。ちなみにこれは今後 wada と masawada でも使います。

 さて、ここまでで 3の倍数を表すことはできました。ここに 5の倍数ではないという条件を追加でくっつけることで、 masa のパターンをつくることができます。
 0, 5 以外で終わる、ということなので、さっきの 3の倍数パターンから、最後の桁が 0, 5 になるものを削ったパターンが、(さっきのパターンの後で)最後に登場していればよいことになります。具体的には、 [0369][369] になったり、 [147][0369]*[258][147][0369]*[28] になったりするわけですね。

qr/
(
    [369]                  # A'
    |
    [147][0369]*[28]       # B'
    |
    [258][0369]*[147]      # C
    |
    ([147][0369]*){2}[147] # D
    |
    ([258][0369]*){2}[28]  # E'
    |
                           # ↓ラダーのパターン'
    (
        [147][0369]*[147]
        |
        [258]
    )
    (
        [0369]*[258][0369]*[147]
    )*
    [0369]*
    (
        [147]
        |
        # 最後の桁で5が出る可能性があったのはここ(右の[258])だったので5を削る
        [258][0369]*[28]
    )
)
$
/x

 組み合わせるとこうなって完成です(最初にまとめて details に入れていた 1つ目を再掲)。

masa(3の倍数かつ5の倍数でない)のパターン
s/^
( # 合わせて3の倍数となるセットのパターン
    # 3の倍数
    [0369]
    |
    # 1,4,7: 3で割った余りが1
    # 2,5,8: 3で割った余りが1
    # 1,4,7のどれかひとつと2,5,8のどれかひとつがあると, 合わせて3の倍数(間に3の倍数があってもよい)
    [147][0369]*[258]
    |
    # 同上, 並びが違うもの
    [258][0369]*[147]
    |
    # 1,4,7のどれかひとつずつが3つあると, 合わせて3の倍数(間に3の倍数があってもよい)
    ([147][0369]*){2}[147]
    |
    # 2,5,8のどれかひとつずつが3つあると, 合わせて3の倍数(間に3の倍数があってもよい)
    ([258][0369]*){2}[258]
    |
    # 22121... のように, 3で割った余りが 2→1→2→1→... と続くパターン
    # これは3で割った余りが2の数の次に, 3で割った余りが2の数→3で割った余りが1の数, というセットが繰り返されると起きる
    # 3の倍数となる場合は, 2→1→...→2→1→1 か 2→1→...→2→1→2→2 のどちらか
    ( # まず3で割った余りが2の数から始まる
        # 1,4,7のどれかひとつずつが2つあると, 合わせて3で割った余りが2(間に3の倍数があってもよい)
        [147][0369]*[147]
        |
        # 2,5,8は3で割った余りが2
        [258]
    )
    ( # 上で拾った数(3で割った余りが2の数)に続いて, 3で割った余りが →1→2→1→2→... と続くパターン (間に3の倍数があってもよい)
        # 2,5,8が続くと3で割った余りが1になり,
        # 1,4,7が続くと3で割った余りが2になる
        # この並び以外のときは, 合わせて3の倍数になるセットのほうでマッチできるはず
        [0369]*[258][0369]*[147]
    )*
    # この間に3の倍数があってもよい
    [0369]*
    ( # 最後に →1 か →2→2 のどちらかで終わると, 合わせて3の倍数となる
        # →1 のパターン
        [147]
        |
        # →2→2 のパターン(間に3の倍数があってもよい)
        [258][0369]*[258]
    )
)* # 下で1桁はマッチするのでここは2桁目以降になるので, 0回以上の繰り返し
( # 合わせて3の倍数となるセットのパターンのうち, 最後の桁が0,5以外
    # 最後の桁で0が出る可能性があったのはここだったので0を削る
    [369]
    |
    [147][0369]*[28]
    |
    [258][0369]*[147]
    |
    ([147][0369]*){2}[147]
    |
    ([258][0369]*){2}[28]
    |
    (
        [147][0369]*[147]
        |
        [258]
    )
    (
        [0369]*[258][0369]*[147]
    )*
    [0369]*
    (
        [147]
        |
        # 最後の桁で5が出る可能性があったのはここ(右の[258])だったので5を削る
        [258][0369]*[28]
    )
)
$/masa/x

 水曜どうでしょうの「これはね……初日がヤマなんでしょう」を思い出す大変さでしたが、この後は、水どうよりは楽になっていきます。

wada

 今度は、 3の倍数でないかつ 5の倍数です。 3の倍数でない、というのを確かめるのは 5の倍数でないことよりも分かりづらいんですが、「3の倍数な部分の桁を全部取り去ったら、残った桁が 3の倍数でない」という作戦でいきます。ということはやはり、さっき作った 3の倍数のパターンは使います。

最後が 0

 まず最後の桁が 0 の場合。全部の桁で見て、 3の倍数になっていないこと (3で割った余りが 1 か 2) が確認できたら ok です。

xxxxxxxxxx0
|--------|
 ここが 3で割った余りが 1 or 2

 3で割った余りが 1 か 2 になるパターンはこれだけ。

qr/
[147]             # 7 とか → 余りは 1
|
[258]             # 8 とか → 余りは 2
|
[147][0369]*[147] # 794 とか → 余りは 2
|
[258][0369]*[258] # 808 とか → 余りは 1
/x

 これの前後に 3の倍数な桁のまとまりがあってもよく、そして最後が 0 、というパターンになります。単純。

最後が 5

 最後の桁が 5 の場合も似たような感じです。が、全体で見たときに、 3で割った余りが(最後の 5 によって)ずれるので、そこに注意します。 5 は 3で割ると余りが 2 なので、最後の桁を除いた桁は、 3の倍数か、 3で割った余りが 2 であれば、 5 と合わせて 3の倍数にならずに済みます (3 で割った余りが 1 のときは 5 と合わせると 3の倍数になってしまう)。

xxxxxxxxxx5
|--------|
 ここが 3の倍数 or 3で割った余りが 2

 正規表現に起こすと、 3で割った余りが 2 なパターンがあるか、何もない、というおもしろパターンになります。これも前後に 3の倍数があってもよく、そして最後は 5。

qr/
(
    [258]             # 8 とか → 余りは 2
    |
    [147][0369]*[147] # 794 とか → 余りは 2
)?                    # これがあるか、何もない
/x

 てことで、最後が 5 のパターンと最後が 0 のパターンのどちらか、という感じでつなげれば、 wada のパターンも完成です。ここまでくれば慣れたものです。

wada (3の倍数でないかつ5の倍数) のパターン
s/^
(
    # 最後が5のパターン
    ( # 合わせて3の倍数となるセットのパターン
        [0369]
        |
        [147][0369]*[258]
        |
        [258][0369]*[147]
        |
        ([147][0369]*){2}[147]
        |
        ([258][0369]*){2}[258]
        |
        (
            [147][0369]*[147]
            |
            [258]
        )
        (
            [0369]*[258][0369]*[147]
        )*
        [0369]*
        (
            [147]
            |
            [258][0369]*[258]
        )
    )* # 3の倍数はいくらでもあってよいし, なくてもよい
    ( # 3で割った余りが2なセットのパターン(これがあると最後の桁が5なので最終的に3で割った余りは1になる)
        [258]
        |
        [147][0369]*[147]
    )? # なくてもよい(ないと最後の桁が5なので最終的に3で割った余りは2になる)
    ( # 合わせて3の倍数となるセットのパターン
        [0369]
        |
        [147][0369]*[258]
        |
        [258][0369]*[147]
        |
        ([147][0369]*){2}[147]
        |
        ([258][0369]*){2}[258]
        |
        (
            [147][0369]*[147]
            |
            [258]
        )
        (
            [0369]*[258][0369]*[147]
        )*
        [0369]*
        (
            [147]
            |
            [258][0369]*[258]
        )
    )* # 3の倍数はいくらでもあってよいし, なくてもよい
    # 最後は5
    5
    |
    # 最後が0のパターン
    ( # 合わせて3の倍数となるセットのパターン
        [0369]
        |
        [147][0369]*[258]
        |
        [258][0369]*[147]
        |
        ([147][0369]*){2}[147]
        |
        ([258][0369]*){2}[258]
        |
        (
            [147][0369]*[147]
            |
            [258]
        )
        (
            [0369]*[258][0369]*[147]
        )*
        [0369]*
        (
            [147]
            |
            [258][0369]*[258]
        )
    )* # 3の倍数はいくらでもあってよいし, なくてもよい
    ( # 3で割った余りが1 or 2なセットのパターン(最後の桁が0なので最終的に3で割った余りは1 or 2になる)
        # 3で割った余りが1
        [147]
        |
        # 3で割った余りが2
        [258]
        |
        # 3で割った余りが2
        [147][0369]*[147]
        |
        # 3で割った余りが1
        [258][0369]*[258]
    )
    ( # 合わせて3の倍数となるセットのパターン
        [0369]
        |
        [147][0369]*[258]
        |
        [258][0369]*[147]
        |
        ([147][0369]*){2}[147]
        |
        ([258][0369]*){2}[258]
        |
        (
            [147][0369]*[147]
            |
            [258]
        )
        (
            [0369]*[258][0369]*[147]
        )*
        [0369]*
        (
            [147]
            |
            [258][0369]*[258]
        )
    )* # 3の倍数はいくらでもあってよいし, なくてもよい
    # 最後は0
    0
)
$/wada/x;
masawada

 ついに masawada です。 3の倍数かつ 5の倍数ですが、 wada の考え方のまま、全体で 3の倍数になるように変えてあげると完成します。つまり、最後が 0 のときはそれ以前は 3の倍数、最後が 5 のときはそれ以前は 3で割った余りが 1 になっていればよいですね。

xxxxxxxxxx0
|--------|
 ここが 3の倍数

xxxxxxxxxx5
|--------|
 ここが 3で割った余りが 1

 最後が 0 の場合は、素朴に 3の倍数のパターン + 0。最後が 5 の場合は、↓に書くような 3で割った余りが 1 のパターンがある (前後に 3の倍数があってもよく、最後が 5) 、でよいです。もう見覚えしかありません。

qr/
[147]             # 7 とか → 余りは 1
|
[258][0369]*[258] # 808 とか → 余りは 1
/x

 完成。めでたく 15 が masawada になります。

masawada (3の倍数かつ 5の倍数) のパターン
s/^
(
    # 最後が0のパターン
    ( # 合わせて3の倍数となるセットのパターン
        [0369]
        |
        [147][0369]*[258]
        |
        [258][0369]*[147]
        |
        ([147][0369]*){2}[147]
        |
        ([258][0369]*){2}[258]
        |
        (
            [147][0369]*[147]
            |
            [258]
        )
        (
            [0369]*[258][0369]*[147]
        )*
        [0369]*
        (
            [147]
            |
            [258][0369]*[258]
        )
    )* # 3の倍数はいくらでもあってよいし, なくてもよい
    # 最後は0
    0
    |
    # 最後が5のパターン
    ( # 合わせて3の倍数となるセットのパターン
        [0369]
        |
        [147][0369]*[258]
        |
        [258][0369]*[147]
        |
        ([147][0369]*){2}[147]
        |
        ([258][0369]*){2}[258]
        |
        (
            [147][0369]*[147]
            |
            [258]
        )
        (
            [0369]*[258][0369]*[147]
        )*
        [0369]*
        (
            [147]
            |
            [258][0369]*[258]
        )
    )* # 3の倍数はいくらでもあってよいし, なくてもよい
    ( # 合わせて3で割った余りが1なセットのパターン(最後の5と合わせて3の倍数となる)
        [147]
        |
        [258][0369]*[258]
    )
    ( # 合わせて3の倍数となるセットのパターン
        [0369]
        |
        [147][0369]*[258]
        |
        [258][0369]*[147]
        |
        ([147][0369]*){2}[147]
        |
        ([258][0369]*){2}[258]
        |
        (
            [147][0369]*[147]
            |
            [258]
        )
        (
            [0369]*[258][0369]*[147]
        )*
        [0369]*
        (
            [147]
            |
            [258][0369]*[258]
        )
    )* # 3の倍数はいくらでもあってよいし, なくてもよい
    # 最後は5
    5
)
$/masawada/x;

 数に対してこれらをどの順番でも適用してやれば、 1, 2, masa, 4, wada, ... という具合に masawada ができるようになりました。作ったときもかなり達成感があったけど、ここまで書いた今も同じくらい達成感があります。読んだ皆さんにも感じてもらえていたら幸いです。

むすび

 正規表現FizzBuzz もとい masawada をしました。足し算とかはできないもんな〜と思っていたけど、意外となんとかなるものなんだな、という感慨があってめちゃくちゃ面白かったです。こういうひたすらこねてなんとかするような趣味にはぴったりなので、皆さんも正規表現と戯れてみてください。 String::Random - Perl module to generate random strings based on a pattern - metacpan.org はおすすめで、なんとブラウザでも遊べるサイトがあります Demo of String_random.js

 masawada Advent Calendar 2021 - Adventar、明日は id:mazco さんです。

追伸

 正規表現可視化サイト (https://regexper.com/) があったので突っ込んでみました。わかりやすい。

masa wada masawada
f:id:hogashi:20211222114042p:plain f:id:hogashi:20211222114034p:plain f:id:hogashi:20211222114047p:plain

*1:masawada さんの m はアルファベットで頭から 13番目、whywaita さんの w は 23番目なので、何かと素数に縁がありそうな雰囲気を感じますね

*2:世阿弥のことば:7段階の人生論

*3:実はちょっと前にやってて、文章を書いていたら年末になっていた

*4:使ったのは GitHub - itchyny/rassemble-go: Go implementation of Regexp::Assemble

*5:もしかしたら方法があるかもしれないけどわからなかった

*6:依存させて良い場合、先に 3の倍数かつ 5の倍数なものを masawada に置換してしまえば、あとは 3の倍数なら masa にしてしまっていい (5の倍数でないことを確認しなくても、 5の倍数でないものしか残っていない)

*7:多分あってると思うけど間違ってたら大目に見てください

*8:最初は 2 なので余りは 2 、次は 2 なので足して 4 なので余りは 1 、次は 1 なので足して 5 なので余りは 2 、という具合