エンタープライズギークス (Enterprise Geeks)

企業システムの企画・開発に携わる技術者集団のブログです。開発言語やフレームワークなどアプリケーション開発に関する各種情報を発信しています。ウルシステムズのエンジニア有志が運営しています。

FizzBuzz珍コード集(前編)

柔らかめのプログラミングネタを一つ。
社内のあるチームで、ちょっとしたプログラミングコンテストのような活動をしているが、その中のお題の1つがなかなか面白かった。

お題はおなじみのFizzBuzzである。

1から指定した整数まで数を出力する。
ただし、3で割り切れるときは"Fizz"を、5で割り切れるときは"Buzz"を、3でも5でも割り切れるときは"FizzBuzz"を出力する。

ここまでは普通だが、さらに次の制約を追加した。

繰り返し命令(for文, while文)とStream APIは使用禁止とする。
可能なら、条件分岐(if, switch, 三項演算子)と%演算子も使用しないでプログラミングすること。

■作品No.1 論理積で条件分岐を表現する

最初に紹介するコードは中堅メンバーのH氏が書いたコードである。

import java.math.BigDecimal;

public class FizzBuzz01 {

  public static void main(String[] args) {
    // 入力チェックは省略
    int num = Integer.parseInt(args[0]);
    printFizzBuzz(1, num);
  }

  /**
   * @param curNum 現在の数値
   * @param lastNum 出力対象の最大値
   * @return 便宜上の真偽値
   */
  private static boolean printFizzBuzz(int curNum, int lastNum) {
    StringBuilder sb = new StringBuilder();

    boolean tmp; // 使い捨ての変数
    tmp = (new BigDecimal(curNum / 3.0).scale() == 0) 
           && sb.append("Fizz").equals("");
    tmp = (new BigDecimal(curNum / 5.0).scale() == 0)
           && sb.append("Buzz").equals("");
    tmp = (sb.length() == 0) && sb.append(curNum).equals("");

    sb.append(',');
    System.out.print(sb.toString());

    return (curNum < lastNum) && printFizzBuzz(++curNum, lastNum);
  }
}

このコードでは、数値が3または5で割り切れるかどうかをBigDecimal#scale()を利用して判定している。

その上で、条件分岐のロジックは論理演算子&&を使って表現している。

左辺 && 右辺

上記の式で、右辺を評価するのは、左辺が成り立つ場合のみである。この性質を使って、左辺に条件を記述し、右辺に副作用として処理を記述した上で、equals==を使って強引に右辺を条件式にしている。

繰り返しは再帰呼び出しで実現しており、最後のreturn文で再帰の終了判定をする際にも論理積による条件分岐を使っている。

作成者いわく「残念な実装」とのことだが、構造がシンプルでコード行数も短いため、問題文の制約を前提にすれば、かなりエレガントなコードといえるだろう。

■作品No.2 ジャンプテーブルで条件分岐を表現する

次に紹介するのは、別のH氏が書いたコードである。

ここでは、数値に応じた出力文字を求める仕組みとして、数値を15で割った余りを求め、余りの値に対応する出力処理をジャンプテーブルに格納する方法をとっている。

public class FizzBuzz02 {
  public static void main(String... args) {
    int count = Integer.parseInt(args[0]);
    fizzBuzzRecursive(count);
  }

  // for/whileを使えないので再帰でループを実現する
  private static void fizzBuzzRecursive(int number) {
    // パターンマッチがあれば...
    if (number <= 0) return;

    fizzBuzzRecursive(number - 1);
    printNumber(number);
    System.out.print(',');
  }

  // 条件分岐を使わずに出力内容を切り分けるためのジャンプテーブル
  // 余りの値(0-14)に応じたPrinterオブジェクトを格納しておく
  private static final int TABLE_SIZE = 15;
  private static final Printer[] JUMP_TABLE = new Printer[TABLE_SIZE];
  static {
    JUMP_TABLE[0]  = new TextPrinter("FizzBuzz");
    JUMP_TABLE[1]  = new NumberPrinter();
    JUMP_TABLE[2]  = new NumberPrinter();
    JUMP_TABLE[3]  = new TextPrinter("Fizz");
    JUMP_TABLE[4]  = new NumberPrinter();
    JUMP_TABLE[5]  = new TextPrinter("Buzz");
    JUMP_TABLE[6]  = new TextPrinter("Fizz");
    JUMP_TABLE[7]  = new NumberPrinter();
    JUMP_TABLE[8]  = new NumberPrinter();
    JUMP_TABLE[9]  = new TextPrinter("Fizz");
    JUMP_TABLE[10] = new TextPrinter("Buzz");
    JUMP_TABLE[11] = new NumberPrinter();
    JUMP_TABLE[12] = new TextPrinter("Fizz");
    JUMP_TABLE[13] = new NumberPrinter();
    JUMP_TABLE[14] = new NumberPrinter();
  }

  private static void printNumber(int number) {
    JUMP_TABLE[getRemainder(number, TABLE_SIZE)].print(number);
  }

  //「%」記号を使わずに余りを求めるメソッド
  private static int getRemainder(int number, int divisor) {
    return number - (number / divisor * divisor);
  }
}

// 文字列を出力するクラス(ポリモーフィズムで使い分ける)
abstract class Printer {
  abstract void print(int number);
}
// 固定文字列を出力するクラス(Fizz/Buzz/FizzBuzz用)
class TextPrinter extends Printer {
  private final String fixedText;
  TextPrinter(String ft) {
    this.fixedText = ft;
  }
  @Override
  protected void print(int number) {
    // 引数の数字は無視
    System.out.print(this.fixedText);
  }
}
// 数字をそのまま出力するクラス
class NumberPrinter extends Printer {
  @Override
  protected void print(int number) {
    System.out.print(number);
  }
}

このコードを書いた人はかなりのベテランで、若い頃はアセンブラでプログラミングをしていたそうである。このため、%演算子を使わずに余りを求めるロジックや、条件分岐にジャンプテーブルを使う仕組みはすぐに思いついたそうである。

繰り返しは再帰で実現しているが、if文を使わずに再帰の終了条件を記述する方法は思いつかなかったらしい。

それにしても、ジャンプテーブルとポリモーフィズムを組み合わせたコードは、実に珍妙である。

後編に続く)