【ES6】FizzBuzz問題のJavaScript最短コードを解説します


FizzBuzz問題とは

問題
1から100までの数をプリントするプログラムを書け。
ただし3の倍数のときは数の代わりに「Fizz」、5の倍数のときは「Buzz」とプリントし、
3と5の両方の倍数のときには「FizzBuzz」とプリントすること。

上記問題をFizzBuzz問題と言い、エンジニアの中では有名な問題らしいです。(恥ずかしながらつい最近まで知りませんでした..)
問題自体は、if文を使って一つずつ条件を書き、for文を使って1から100までループさせることで比較的簡単に解くことができます。

FizzBuzzコードゴルフ

しかし、これを可能な限り少ないコードで書こうとすると、とたんに難しくなります。
(問題の条件を満たすプログラムをできるだけ短い文字数で実現する競技をCode Golfと呼びます。)

長いこと考えた末、、

JS
for(i=1;i<101;i++){console.log(i%15?i%3?i%5?i:'Buzz':'Fizz':'FizzBuzz')}

条件演算子を駆使して、なんとか72文字の解までたどり着きました。

が、

ネットの海に潜ってJavaScriptの最短コードを探したところ、なんと62文字の最強の解が見つかりました。
それがこれ↓

JS
for(i=0;++i<101;console.log(i%5?x||i:x+'Buzz'))x=i%3?'':'Fizz'

「なんだこれ?、意味わからん..」と思い、コードの中身を調べていたところ
そのあまりに綺麗なロジックに感動してしまいました。。

以下その感動を伝えたい解説記事です。

※最短解引用元
ES2015時代のFizzBuzzに挑戦する - Qiita
FizzBuzz JavaScript solution · GitHub

コード解説

ポイントは3点。
(1)for文の{ }を省略
(2)基礎を踏まえた繰り返しの定義
(3)神の条件分岐ロジック

(1)for文の{ }を省略

通常for文を書くとき、

for () {
//処理の文
}

のように書きますが、「処理の文」が1つのときは文を囲む{ }を省略することができます。
つまり、

for ()//処理の文

のように書くことができ、
FizzBuzz最短解ではこのショートハンドを使用しています。

// 最短解
for(i=0;++i<101;console.log(i%5?x||i:x+'Buzz'))x=i%3?'':'Fizz'


// for文の{}を省略せずに書いた場合
for(i=0;++i<101;console.log(i%5?x||i:x+'Buzz')) {
  x = i%3 ? '' : 'Fizz';
}

でも正直まだ全然わかんない..

(2)基礎を踏まえた繰り返しの定義

最短解をよく見てみると、for文の( )の中にconsole.log()が記述されていることが分かります。

FizzBuzz解のfor文条件式
for(i=0; ++i<101; console.log(i%5?x||i:x+'Buzz'))

ここでJavaScriptのfor文の定義を振り返ってみましょう。

for文の定義
for ([initialization]; [condition]; [final-expression]) statement

第1引数のinitialization変数宣言であり、たいていはカウンタ変数を初期化するために使われます。
FizzBuzz解でもi = 0;とループの初期値を定義しています。

第2引数のconditionループの各反復の前に評価される式です。
この式がtrueに評価された場合のみ、その後のstatementが実行されます。
FizzBuzz解では++i<101;という定義になっていますが、
これはi=i+1が101より小さいときに、式がtrueとなりループが実行されるということです。

第3引数のfinal-expressionループの各反復の終わりに評価される式です。
FizzBuzz解ではそこにconsole.log()を記述しているため、for文の各反復における処理の文が実行された後(反復の終わり)に、テキストが表示されるようになっています。

ここまでの内容を要約すると、

  • 各反復の初めにi=i+1が実行され(iの初期値は0)、その数が101より小さい時にfor文の処理が実行される。
  • 各反復でfor文の処理が実行された後に、console.log()が実行される。

となり、
さらにコード形式でまとめると、

// for文の定義
for ('①変数定義';
     '②ループの各反復の前に評価される式';
     '③ループの各反復の終わりに評価される式')
    {'②がtrueの時に実行される処理'}

// FizzBuzz最短解にあてはめる
for (i=0;
     ++i<101; // i=i+1が101より小さい時、つまりi+1が1〜100の時にループが実行される
     console.log(i%5?x||i:x+'Buzz')) // よってconsole.log()が100回実行される
    { x = i%3 ? '' : 'Fizz'; }

ということになります。
だいぶ見えてきたけど、まだ表示されるテキスト部分が分からんぞ?

(3)神の条件分岐ロジック

(3)-1 反復処理の内容

次にFizzBuzz最短解において、for文で反復実行される処理の内容を見ていきます。

FizzBuzz解の反復処理の内容
x = i%3 ? '' : 'Fizz';

この文はif文のショートハンドである条件演算子を使用しており、

条件演算子を使った条件分岐
'条件式'  'trueの場合の処理' : 'falseの場合の処理';

i%3trueの時はx=''falseの時はx='Fizz'となるよう変数xを定義しています。

補足1:%演算子
割り算の余りを求めることができる。i%3はiを3で割った余り。
ある整数を3で割って余りが0ならその数は3の倍数であると言える。

補足2:JavaScriptのtrue/false
0はboolean型でfalseの初期値を持つ。他にもnullundefined''(空文字)などもfalseとなる。
逆にその値がundefinednullでないオブジェクトは、条件文に通されると全てtrueに評価される。

今回の条件分岐をif文で書くと、

if文による条件分岐の書き換え
if ( i%3 ) {
  x = '';     // iが3の倍数ではないとき
} else {
  x = 'Fizz'  // iが3の倍数のとき
}

こうなり、
また表にまとめると、

こうなります。

(3)-2 console.log()で表示するテキスト

いよいよ最後!
実際にconsole.log()で表示されるテキストを見てみます。

FizzBuzz解において各反復後に実行される処理
console.log(i%5?x||i:x+'Buzz')

これを半角スペースを入れて少し分かりやすく書くと、

console.log( i%5 ? x||i : x+'Buzz')

条件演算子を使った(3)-1と同様の条件分岐であることが分かり、
i%5trueの時はx||iを、falseの時はx+'Buzz'を表示するよう定義されていることが理解できます。

これを表でまとめると、

こうなり、
さらに(3)-1で解説したxの値と合わせてまとめたものが以下の表になります。

注目すべきは、x||iの部分。
論理演算子である||は、xtrueの場合はxの値をそのまま返し、falseの場合はiを返します。
これによって、iが3の倍数の時は'Fizz'を表示し、そうでない時は1から100の数字であるiをそのまま表示できるようになっています。

4つの場合分けパターンに合わせた、神がかった条件定義はまさに感動モノ..

まとめ

解説は以上です!
ここまでの内容を理解できていれば、FizzBuzz最短解のコードロジックも読み取れるはず!

FizzBuzz最短解
for(i=0;++i<101;console.log(i%5?x||i:x+'Buzz'))x=i%3?'':'Fizz'

改めて見ても.. これ考えた人天才だ....

参考にしたサイト

for - JavaScript | MDN
条件 (三項) 演算子 - JavaScript | MDN
代入演算子 - JavaScript | MDN
論理演算子 - JavaScript | MDN
Boolean - JavaScript | MDN
JavaScript ショートコードテクニック集(ES6含む) - Qiita