関数だけがExcelではないというのは言うまでもありませんが、Excelが得意な人は関数もマクロもよく知っているはずです。
ところで、プログラミング界には「FizzBuzz問題」という超有名な問題がありますが、皆さんはご存知でしょうか? Excelの関数やVBAで解くことも可能なのでチャレンジした人も多いのではないかと思います。ネット上にもExcelで解く方法がたくさん載っています。
そこで、ExcelでFizzBuzz問題を解いた場合の標準的な回答例を難易度順にまとめました。あなたの「Excelの習熟度」を診断いたします!!
問題
Excelの関数またはマクロを用いて、1から100までの整数を表示する方法をできるだけ多く考えなさい。ただし、3の倍数は「Fizz」、5の倍数は「Buzz」とし、15の倍数の場合は「FizzBuzz」としなければならない。
※なお、出力する場所は問わない。
目次
- 0.FizzBuzz問題の基礎知識
- 1.初級レベルその1:IF関数とMOD関数
- 2.初級レベルその2:ROW関数と一括入力
- 3.中級レベルその1:MID関数
- 4.中級レベルその2:CHOOSE関数
- 5.中級レベルその3:フェルマーの小定理
- 6.中級レベルその4:GCD関数
- 7.中級レベルその5:VBAを使う
- 8.中級レベルその6:行数を減らす
- 9.上級レベル:ワンライナー
- 10.さいごに
0.FizzBuzz問題の基礎知識
(1)FizzBuzz問題とは何か
もともとFizzBuzzとはイギリスの子供たちの言葉遊びです。日本では桂三度(世界のナベアツ)氏が、3の倍数と3のつく数字でアホになりましたが、英語圏では、3、5、15の倍数のときにそれぞれ「Fizz」「Buzz」「FizzBuzz」と言わなければならないというルールだそうです。
2007年にアメリカのソフトウェア開発者がブログで、プログラミングが苦手なプログラマを特定する方法(identify the weak ones)として「FizzBuzz問題」を挙げました。このブログ記事が日本にも紹介され、今では、プログラミング能力を試す典型的な問題の一つとなっています。
最初に出題した人:Imran Ghory氏
Using FizzBuzz to Find Developers who Grok Coding
https://imranontech.com/2007/01/24/using-fizzbuzz-to-find-developers-who-grok-coding/問題提起した人:Jeff Atwood氏
Why Can't Programmers.. Program?
https://blog.codinghorror.com/why-cant-programmers-program/
(2)解くときに忘れてはならないこと
FizzBuzz問題を解くときには、上記のブログ記事を熟読した上で「なぜこの問題が出題されたのか」を理解しなければなりません。
また、非常に有名な問題なので、最近は何らかの制限を加えて解くのが一般的です。単に解けたらよいという問題ではありません。特に、次のようなルールがあることを意識しながら解かなければなりません。
- ワンライナー(one liner、1行でプログラムや計算式を記述する)
- コードゴルフ(code golf、できるだけ少ない文字数で記述する)
- タイムリミット(time limit、*分以内に解く)
要するに、できるだけ少ない手順で連番を出力することが求められているので、事前に別の表を作って検索する、ユーザー設定リストに登録する、マクロを記録するなどの方法は問題の趣旨に反します。
(3)教養の問題です
算数が苦手だからFizzBuzzが分からないなどと意味不明なことを言うシステムエンジニアもいるそうですが、こういう有名な問題は基礎教養であり、Hello WorldもFizzBuzzも知らない人間が情報技術者を名乗ってはいけません。石川遼を知らずにゴルフを語るようなものです。
1.初級レベルその1:IF関数とMOD関数
IF関数とMOD関数は、Excel2~3級レベルの関数です。A列に1~100の連番を並べておいて、B1に次の数式を入力するのが最も初歩的なやり方です。基本は大事です。
- =IF(MOD(A1,15)=0,"FizzBuzz",IF(MOD(A1,3)=0,"Fizz",IF(MOD(A1,5)=0,"Buzz",A1)))
ここで真偽を逆にします。Excelでは、0をFalse、0以外をTrueとするため、真偽を逆にすることで「=0」の部分を無くすことができます。
- =IF(MOD(A1,15),IF(MOD(A1,3),IF(MOD(A1,5),A1,"Buzz"),"Fizz"),"FizzBuzz")
理論的には同じなのですが、前述のコードゴルフの場合は、計算式がより短いほうが優れているということになります。数式が分かりやすいかどうかではなく、「IF関数の条件式が等式不等式の形でなくてもよい」ということが分かっていることが大事です。
15の倍数を使わない分岐の仕方だと、さらに1文字減ります。
- =IF(MOD(A1,5),IF(MOD(A1,3),A1,"Fizz"),IF(MOD(A1,3),"Buzz","FizzBuzz"))
3の倍数はFizz、5の倍数はBuzzで、15の倍数の時にこれらを文字列連結演算子「&」で連結するという方法があります。しかし1文字増えてしまいます。
- =IF(MOD(A1,3)*MOD(A1,5),A1,IF(MOD(A1,3),,"Fizz")&IF(MOD(A1,5),,"Buzz"))
なお、Office365の契約をしているExcel2016と、Excel2019以降で新たに追加された関数「IFS」「SWITCH」を使う方法もありますが、365の契約の無い2016とそれ以前のバージョンではエラーになるので説明を割愛します。
2.初級レベルその2:ROW関数と一括入力
さきほどのやり方だと、いったんA列に1~100の連番を入力しておく必要があり、解き方として美しくありません。数式の文字数は多くなりますが、ROW関数で行番号(連番)を取得するのが良いと思います。
- =IF(MOD(ROW(),5),IF(MOD(ROW(),3),ROW(),"Fizz"),IF(MOD(ROW(),3),"Buzz","FizzBuzz"))
ROW関数を使うことにより、Ctrlキーを用いてセル範囲A1:A100に一括入力をすることができます(ROWがループの代わりになる)。
3.中級レベルその1:MID関数
「FizzBuzz」の前半の4文字と後半の4文字をMID関数で取得するという方法があります。さらに「"FizzBuzz"&ROW()」と行番号を連結すれば、MID関数1回で計算することができます。
開始番号は、3の倍数と15の倍数の場合が1、5の倍数の場合が5、それ以外が9です。
取得文字数は、15の倍数のときだけ8文字で、それ以外は4です。なお、MID関数の開始位置と取得文字数の和が文字列の文字数を超えるときは最後までの文字が返されます。
MID関数の中にIF関数を入れて、開始番号と取得文字数を求めます。この場合もMOD=0とせず、MODだけにして文字数を減らします。
- =MID("FizzBuzz"&ROW(),IF(MOD(ROW(),3),IF(MOD(ROW(),5),9,5),1),IF(MOD(ROW(),15),4,8))
4.中級レベルその2:CHOOSE関数
FizzBuzz問題は、余りの数を文字列に変換する問題でもあります。このように数値を別のデータに変換するときに使われる関数としては、LOOKUP系の関数またはCHOOSE関数が考えられます。しかし、整数問題の場合はCHOOSEを使ったほうが分かりやすいです。
3、5で割った時の余りをそれぞれA、Bとすると、A=0、B=0はTRUEまたはFALSEになります。2進数のようなイメージで(A=0)+(B=0)*2+1を計算すると、1~4のコードになります。
これをインデックスとしてCHOOSE関数で変換します。
- =CHOOSE(1~4のコード,元の数,"Fizz","Buzz","FizzBuzz")
MODとROWを使うと次のようになります。IF関数やMID関数を使った場合よりも数式の文字数が少なく、分かりやすい数式になります。
- =CHOOSE((MOD(ROW(),3)=0)+(MOD(ROW(),5)=0)*2+1,ROW(),"Fizz","Buzz","FizzBuzz")
5.中級レベルその3:フェルマーの小定理
FizzBuzz問題と言えば「フェルマーの小定理」を使って求める方法があります。下記の記事によると、FizzBuzz問題が話題となった2007年にフェルマーの小定理で解いた人がいたらしいです。
フェルマーの小定理を使って条件分岐を全く使っていない例があってその発想に驚く。
http://www.guru.gr.jp/~awa/blog/archives/2007/06/entry_1022.html
フェルマーの小定理(Fermat's little theorem)とは、「pを素数とし、aをpの倍数でない整数とするとき、aのp−1乗をpで割った余りは1である」というものです。
Excelでは「MOD(a^(p-1),p)=1」となります。もし、aがpの倍数だったら割り切れますから余りは0です。したがって、p−1乗をpで割った余りは0か1のどちらかになり、pの倍数かを判定することができます。
3も5も素数なのでこの定理が使えます。実際に、2乗を3で割った余りと、4乗を5で割った余りをそれぞれ求めてみると0と1だけになります。そして、よく見ると先ほどのTRUE/FALSEの逆になっています。
この定理を利用するとCHOOSE関数の数式は次のようになります(先ほどの式のA、Bを、1-A、1-Bに置き換える)。数式が4文字減ります。
- =CHOOSE(4-MOD(ROW()^2,3)-MOD(ROW()^4,5)*2,ROW(),"Fizz","Buzz","FizzBuzz")
ただし、この数式は「4乗」を使っていることに注意しなければなりません。例えば、6000を4乗すると16桁となり、Excelの有効桁数である15桁を超えてしまいます。
FizzBuzz問題は100までなので問題ありませんが、大きな数を出力するときにはこの方法は使えません。
6.中級レベルその4:GCD関数
GCDは最大公約数のことです。15の約数は1、3、5、15の4つしかなく、それぞれ、数値、Fizz、Buzz、FizzBuzzに対応しています。このことを利用して行番号との最大公約数GCD(ROW(),15)をCHOOSE関数のインデックスにします。
- =CHOOSE(GCD(ROW(),15),ROW(),,"Fizz",,"Buzz",,,,,,,,,,"FizzBuzz")
1、3、5、15なのでカンマが多いのが気になります。「15」をいかに小さくするかがポイントですが、9で割った余りにするとカンマが減って、数式が2文字減ります。
- =CHOOSE(MOD(GCD(ROW(),15),9),ROW(),,"Fizz",,"Buzz","FizzBuzz")
CHOOSE関数のインデックスが小数の場合は小数部分が切り捨てられます。1、3、5、15を5倍して自然対数をとると、小数点以下切り捨てでそれぞれ1、2、3、4となるため次の数式が考えられます。
- =CHOOSE(LN(GCD(ROW(),15)*5),ROW(),"Fizz","Buzz","FizzBuzz")
また、1、3、5、15を0.7乗して小数点以下を切り捨てると、それぞれ1、2、3、6となるため次の数式が考えられます。これが、Excelを使った場合の最短の数式です(自信はありません・・・)。
- =CHOOSE(GCD(ROW(),15)^0.7,ROW(),"Fizz","Buzz",,,"FizzBuzz")
対数関数の式と累乗の式はいずれも先頭のイコールを含めて59文字です。しかし、累乗の場合は入力するときに「0.7」を「.7」と入力すればいいので実際には58打となります。日本ゴルフツアーの最小スコア記録は石川遼選手の58打なので、ゴルフで言えば日本記録タイということになります(?)。
7.中級レベルその5:VBAを使う
マクロを使うのであれば、CHOOSE関数の数式を100回繰り返せば済むのですが、「2分以内」などの時間制限がある場合、CHOOSEやGCDを考えている暇はないので、ForとIfで記述したほうが速いです(Select Caseでも可)。FizzとBuzzを連結する方法と、連結しない方法の2通りがあります。
ちなみに、「Cells(i, 1) = s」の部分を「Selection.TypeText s & " "」にするとWordVBAになります。
8.中級レベルその6:行数を減らす
上記のような平凡なマクロを書いて満足しているようではダメです。コードの行数を減らすまたは文字数を減らすことを考えなければなりません。例えば、三項演算子の代わりであるIIf関数を使えば、For文の中が3行になります。
また、VBAのChoose関数を使えば1行になります。なお、GCDを使う場合は「WorksheetFunction.Gcd」としなければなりません。
複数の命令を1行にまとめたいときはコロン(:)を使えばよいので、For文自体を1行にすることができます。
9.上級レベル:ワンライナー
Excelでワンライナーのコードを実行するには「イミディエイトウィンドウ」を使います。
コロン(:)を使うことによって、For文やIf文を1行で実行することができます。しかし、残念ながらイミディエイトウィンドウでは、For文の中にIf文を入れて実行することができません。仕方がないのでIIf関数を使うか、Choose関数を使います。
for i=1 to 100:a=i mod 5:b=i mod 3:?iif(a*b,i,iif(a,"","Fizz")&iif(b,"","Buzz")):next
for i=1 to 100:?choose(4-i^2 mod 3-(i^4 mod 5)*2,i,"Fizz","Buzz","FizzBuzz"):next
イミディエイトウィンドウでは、予約語の左側が数値の場合その間にあるスペースを削除してもよいので、その分だけ文字数を減らすことができます。
for i=1to 100:a=i mod 5:b=i mod 3:?iif(a*b,i,iif(a,"","Fizz")&iif(b,"","Buzz")):next
for i=1to 100:?choose(4-i^2mod 3-(i^4mod 5)*2,i,"Fizz","Buzz","FizzBuzz"):next
もちろん直接、ワークシートのセルに出力することも可能です。
for i=1to 100:a=i mod 5:b=i mod 3:cells(i,1)=iif(a*b,i,iif(a,"","Fizz")&iif(b,"","Buzz")):next
for i=1to 100:cells(i,1)=choose(4-i^2mod 3-(i^4mod 5)*2,i,"Fizz","Buzz","FizzBuzz"):next
10.さいごに
標準モジュールにユーザー定義関数を作って呼び出す方法も考えられます。
以上、難易度順に解法をご紹介しましたが、結局のところ、マクロでやるよりも関数一括入力のほうが打数(文字数)が少ないです。
その他にもいろいろな解き方が考えられますが、とにかく短い時間、短い計算式でシンプルに解きましょう。そして、時間があったらFizzBuzz問題よりもオモローな桂三度問題もチャレンジしてみてください。