日々之迷歩

世の中わからんことだらけ

ITが複雑で難しくなっていく様に翻弄される日々です。微力ながら共著させていただいた「シェル・ワンライナー160本ノック」をよろしくお願い申し上げます。

拡張FizzBuzz問題をシェルプログラミングで

先日なぜかふと思いつく。FizzBuzz問題を解くのに、シェル芸で制御構造的なもの無しで解いた。

シェル芸でFizzBuzz問題

一見回りくどいように見えるが、この方法には一つ利点?があるように思う。

$ seq 20 |
> awk 'NR%3==0{print $0,"Fizz"}NR%5==0{print $0,"Buzz"}' |
> yarr num=1 |
> loopj num=1 <(seq 20) - |
> sed 's/ 0$//' |
> tr -d ' ' |
> sed '/zz/s/[0-9]*//'
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
16
17
Fizz
19
Buzz

このシェル芸の本質は前半まで。まずは倍数と対応する文字列部分を表示

$ seq 20 | awk 'NR%3==0{print $0,"Fizz"}NR%5==0{print $0,"Buzz"}'
3 Fizz
5 Buzz
6 Fizz
9 Fizz
10 Buzz
12 Fizz
15 Fizz
15 Buzz
18 Fizz
20 Buzz

次に同じキー(数字)の出力を、Tukubaiコマンドのyarrを使って横に並べる。

$ seq 20 | awk 'NR%3==0{print $0,"Fizz"}NR%5==0{print $0,"Buzz"}' |
> yarr num=1
3 Fizz
5 Buzz
6 Fizz
9 Fizz
10 Buzz
12 Fizz
15 Fizz Buzz
18 Fizz
20 Buzz

あとは1から20までの数字を使ってloopjで補完。

$ seq 20 |
> awk 'NR%3==0{print $0,"Fizz"}NR%5==0{print $0,"Buzz"}' |
> yarr num=1 | loopj num=1 <(seq 20) -
1 0
2 0
3 Fizz
4 0
5 Buzz
6 Fizz
7 0
8 0
9 Fizz
10 Buzz
11 0
12 Fizz
13 0
14 0
15 Fizz Buzz
16 0
17 0
18 Fizz
19 0
20 Buzz

あとは下記のように文字列処理する。

  • 倍数に対応しない数字の行( 0で終わる行)は、0を削除
  • 倍数に対応する部分は文字だけ表示

このシェル芸の利点?は、倍数と出力する文字列の指定は、最初のawkに指定するだけ。倍数と文字列を増やすのも簡単ということ。
例えば2の倍数の時にJazzと出すのを追加するなら、awk 'NR%2==0{print $0,"Jazz"}NR%3==0{print $0,"Fizz"}NR%5==0{print $0,"Buzz"}'という具合。

FizzBuzz拡張編を考える

その後、どうせならその利点を生かして、拡張版FizzBuzz問題をシェルプログラミングで解いてみることにした。

環境

下記のUNIX環境。

  • OS X El Capitan
  • sedawk等のコマンド
  • Open usp Tukubai

お題(拡張版FizzBuzz問題

倍数と表示する文字列を標準入力から指定する。出力する数字の数は第一引数で指定する。

入力
$ cat input
2 Fizz
3 Buzz
出力
$ cat input | ./fizzbuzz.sh 10
1
Fizz
Buzz
Fizz
5
FizzBuzz
7
Fizz
Buzz
Fizz

さて倍数と表示する文字列の処理をするコードをどうするか?下記のようにawkを使ってawkスクリプトを作ってしまおう。

$ cat input
3 Fizz
5 Buzz

$ cat input | awk '{printf "NR%%%s==0{print $0,\"%s\"}\n", $1,$2}'
NR%3==0{print $0,"Fizz"}
NR%5==0{print $0,"Buzz"}

これを一時ファイルに保存して、awkスクリプトとして渡してやる。
ではシェルスクリプトはこちら。

$ cat efizzbuzz.sh
#!/bin/sh

tmp=/tmp/$$
num=30
[ $# -eq 1 ] && num=$1

seq $num > $tmp-num

sed '/^$/d'            |   
awk '{printf "NR%%%s==0{print $0,\"%s\"}\n", $1,$2}' > $tmp-awkscript

cat $tmp-num           |   
awk -f $tmp-awkscript  |
yarr num=1             |   
loopj num=1 $tmp-num - | 
sed 's/ 0$//'          |   
awk 'NF==1{print}NF!=1{$1="";gsub(/ /,"",$0);print}'

rm -f $tmp*

実行例は下記の通り。

$ cat input
2 Bizz
3 Fizz
5 Buzz
7 Kuzz

$ cat input | ./efizzbuzz.sh 100
1
Bizz
Fizz
Bizz
Buzz
BizzFizz
Kuzz
Bizz
Fizz
BizzBuzz
..........
Kuzz
Bizz
Fizz
Bizz
Buzz
BizzFizz
97
BizzKuzz
Fizz
BizzBuzz

このシェルスクリプトのポイントは下記の通り。

  • 条件判定の部分は最小限
  • ループ処理は無し

理解出来れば案外スッキリしているのではないか?と思うがどうだろうか?