y_uti のブログ

統計、機械学習、自然言語処理などに興味を持つエンジニアの技術ブログです

ワンライナーで Fizz Buzz

Fizz Buzz をワンライナーで書いてみます。ただし、ワンライナーといっても awk などを使ってしまえば普通のプログラミング言語で書くのと変わりませんので、そのような言語を使わず、また、シェルの for や while, if といった制御構造も使わないという制限を課してみました。

Fizz Buzz 問題については Wikipedia などに説明があります。簡単なプログラミングの問題です。
Fizz Buzz - Wikipedia

まず、1 から 100 の連番を作成します。これは seq コマンドを使うのが簡単です。出力をリダイレクトして 1.txt に保存しておきます*1

$ seq 1 100 >1.txt

次のようなファイルが作成されます。1 から 100 まで全体で 100 行のテキストファイルになります。

$ head -n 5 1.txt
1
2
3
4
5

次に、3 の倍数を作成します。以下のように実行すると、3 から開始して 3 刻みで 100 までの数列が生成されます。これもリダイレクトして 3.txt に保存します。

$ seq 3 3 100 >3.txt

次のように、3 の倍数が列挙されたファイルが作成されます。引数に指定した 100 は 3 の倍数ではないので、最終行の値は 99 になります。

$ head -n 5 3.txt
3
6
9
12
15
$ tail -n 3 3.txt
93
96
99

同様に 5 の倍数も作成します。

$ seq 5 5 100 >5.txt

3 の倍数のときには Fizz と出力したいので、3.txt の各行に文字列 Fizz を追加します。

$ yes Fizz | head -n 100 | paste 3.txt - | grep ^[0-9] >3_fizz.txt

先頭の yes Fizz は、指定された文字列 Fizz を永久に出力し続けるコマンドです*2。これを head コマンドに渡すことで、各行に文字列 Fizz が書かれた 100 行の出力が得られます。この出力を paste コマンドで先ほどの 3.txt と貼り合わせて、最後の grep で余分な Fizz を取り除きます*3。結果は以下のようになります。

$ head -n 5 3_fizz.txt
3       Fizz
6       Fizz
9       Fizz
12      Fizz
15      Fizz

5.txt からも同様に 5_buzz.txt を作成します。

$ yes Buzz | head -n 100 | paste 5.txt - | grep ^[0-9] >5_buzz.txt

ここから join コマンドを使って Fizz Buzz の出力を作成します。join コマンドは、入力ファイルが文字列順にソートされていることを前提とするので、まず、各ファイルをソートします。

$ sort 1.txt >1_sorted.txt
$ sort 3_fizz.txt >3_fizz_sorted.txt
$ sort 5_buzz.txt >5_buzz_sorted.txt

文字列順にソートすると、たとえば 5_buzz_sorted.txt は次のようになります。

$ head 5_buzz_sorted.txt
10      Buzz
100     Buzz
15      Buzz
20      Buzz
25      Buzz
30      Buzz
35      Buzz
40      Buzz
45      Buzz
5       Buzz

join は、二つのファイルの中で共通のフィールドを持つ行を結合するコマンドです。たとえば、3_fizz_sorted.txt と 5_fizz_sorted.txt を join すると、両方のファイルに含まれる行が次のように出力されます。それぞれのファイルの何番目の列を結合に利用するかは、-1, -2 オプションで指定できます。指定されなければ先頭列が利用されます。

$ join 3_fizz_sorted.txt 5_buzz_sorted.txt
15 Fizz Buzz
30 Fizz Buzz
45 Fizz Buzz
60 Fizz Buzz
75 Fizz Buzz
90 Fizz Buzz

join コマンドに -a オプションを指定することで、結合されなかった行も出力に含めることができます*4。今回は、-a オプションを指定することで、1_sorted.txt に 3_fizz_sorted.txt と 5_buzz_sorted.txt を付け足す形で Fizz Buzz の出力を組み立てます。

$ join -a1 1_sorted.txt 3_fizz_sorted.txt | join -a1 - 5_buzz_sorted.txt >fizz_buzz.txt

出力結果はこのようになります。

$ head fizz_buzz.txt
1
10 Buzz
100 Buzz
11
12 Fizz
13
14
15 Fizz Buzz
16
17

これを sort コマンドで数値順に戻します。また、Fizz Buzz 問題では、数字の代わりに Fizz または Buzz を出力するということなので、不要な数字を除去します。

$ sort -n fizz_buzz.txt | sed 's/^[0-9]* //' >fizz_buzz_answer.txt

最終的に以下のような出力が正しく得られました*5

$ cat fizz_buzz_answer.txt
1
2
Fizz
4
Buzz
...
Fizz Buzz
91
92
Fizz
94
Buzz
Fizz
97
98
Fizz
Buzz

ここまでの説明では、一つ一つの処理結果をファイルに出力していましたが、bash のプロセス置換機能を使うと、全体の処理を以下のように書き下すことができます。

$ join -a1 <(seq 1 100 | sort) \
           <(yes Fizz | head -n 100 | paste <(seq 3 3 100) - | grep ^[0-9] | sort) |\
  join -a1 - \
           <(yes Buzz | head -n 100 | paste <(seq 5 5 100) - | grep ^[0-9] | sort) |\
  sort -n |\
  sed 's/[0-9]* //'

*1:手順の説明のため、一つずつファイルに書き出しています。この記事の末尾で全体をワンライナーにまとめます。

*2:引数が指定されなかった場合は y という文字列が出力されます。

*3:3.txt の行数は 33 行なので、head -n 33 としておけば最後の grep は不要です。

*4:SQL になぞらえると、オプションを指定しない場合が INNER JOIN、-a1 の指定は LEFT JOIN、-a2 の指定は RIGHT JOIN に相当します。

*5:先頭の 5 行と、末尾の 10 行を掲載しています。