読者です 読者をやめる 読者になる 読者になる

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

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

FizzBuzz珍コード集(後編)

先月アップした珍妙なFizzBuzzコードの続編である。

問題文は次の通り。

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

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

■作品No.3 リフレクションとBoolean演算を駆使したFizzBuzz

今回まず紹介するのは、ベテランのS氏が書いたコードである。

import java.lang.reflect.Method;

public class FizzBuzz03 {
  public static void main(String[] args) throws Exception {
    int count = Integer.parseInt(args[0]);
    System.out.println(fizzbuzzRecursive(count));
  }

  /**
   * FizzBuzz文字列を組み上げるメソッド
   * @param n 数値
   * @return 出力文字列
   * @throws Exception
   */
  private static String fizzbuzzRecursive(int n) throws Exception {
    Class[] parameterTypes = {Integer.class, String.class};
    Method m = FizzBuzz03.class.getDeclaredMethod(getMethodName(n),
                                                  parameterTypes);
    Object[] methodArgs = {n, getFizzBuzzString(n)};
    return (String)m.invoke(null, methodArgs);
  }

  /**
   * リフレクションで呼び出すメソッド名を取得する
   * @param n 数値
   * @return 数値が1の場合は最後のメソッド、2以上の場合は再帰メソッド
   */
  private static String getMethodName(int n) {
      int end = Boolean.TRUE.compareTo(n <= 1);
      String[] methodNames = {"returnOne", "returnFizzBuzz"};
      return methodNames[end];
  }

  /**
   * 値が1の時に呼び出される再帰終了時のメソッド
   * @param n 数値
   * @param str 編集済みの文字列
   * @return 出力文字列
   */
  @SuppressWarnings("unused")
  private static String returnOne(Integer n, String str) {
      return "1";
  }

  /**
   * 値が2以上の時に呼び出される再帰処理メソッド
   * @param n 数値
   * @param str 編集済みの文字列
   * @return 出力文字列
   */
  @SuppressWarnings("unused")
  private static String returnFizzBuzz(Integer n, String str)
                                               throws Exception {
      return fizzbuzzRecursive(n-1) + "," + str;
  }

  /**
   * 数値に対応するFizzBuzz文字列を取得する
   * @param n 数値
   * @return FizzBuzz文字列
   */
  private static String getFizzBuzzString(int n) {
      int mod3 = Boolean.TRUE.compareTo(n - (n/3*3) == 0);
      int mod5 = Boolean.TRUE.compareTo(n - (n/5*5) == 0) * 2;

      String[] strings
          = {"FizzBuzz", "Buzz", "Fizz", Integer.toString(n)};
      return strings[mod3 + mod5];
  }
}

このコードでは、条件分岐をかなりトリッキーな方法で実現している。

すなわち、compareTo()メソッドの結果が-1, 0, 1になる性質を利用して、条件式として書きたいロジックをBoolean.TRUE.compareTo()の引数に記述しておく。
そして、条件に応じた処理を示す値を配列に格納し、TRUE.compareTo()の結果に応じた値を取り出して実際の処理を行う。

最後のgetFizzBuzzStringメソッドでは、この仕組みを利用して、数値に対応する出力文字列(Fizz/Buzz/FizzBuzz/数字)を求めている。

ループ処理は再帰で実現しているが、再帰終了の条件判定でもTRUE.compareTo()を使って、リフレクションで呼び出すメソッド名を判断している。
リフレクションを使ってreturnFizzBuzzメソッドを呼び出し、その中で再帰のためにfizzbuzzRecursiveメソッドを呼び出すあたりは、かなりトリッキーな仕組みである。

■作品No.4 正規表現とゼロ除算例外を使ったFizzBuzz

最後に紹介するのは若手のM氏が書いたコードである。

import java.util.regex.Pattern;

public class FizzBuzz04 {

  /**
   * 3の倍数にマッチする正規表現
   */
  public static String multiplesOfThree = ""
          + "^(?:[0369]|[258][0369]*[147]|"
          + "(?:[147]|[258][0369]*[258])"
          + "(?:[0369]|[147][0369]*[258])*"
          + "(?:[258]|[147][0369]*[147]))";

  /**
   * 5の倍数にマッチする正規表現(の一部)
   */
  public static String multiplesOfFive = ".*[05]";

  /**
   * 数字+"FizzBuzz"(例:15FizzBuzz)の形の文字列を、
   * 以下の正規表現で置換するとFizzBuzzの解に置換されます。
   * 以下が処理の流れです。
   * [1] 3の倍数と5の倍数にマッチした場合に"FizzBuzz"を\1にキャプチャします。
   * [2] 3の倍数にマッチした場合にFizzを\2にキャプチャします。
   * [3] 5の倍数にマッチした場合にBuzzを\3にキャプチャします。
   * [4] 上記にマッチしなかった場合、数字を\4にキャプチャします。
   * 置換前:(?=.*[05]FizzBuzz$)^(?:[0369]|[258][0369]*[147]|(?:[147]|[258][0369]*[258])(?:[0369]|[147][0369]*[258])*(?:[258]|[147][0369]*[147]))*(FizzBuzz)$|^(?:[0369]|[258][0369]*[147]|(?:[147]|[258][0369]*[258])(?:[0369]|[147][0369]*[258])*(?:[258]|[147][0369]*[147]))*(Fizz)Buzz$|(?:.*[05]Fizz(Buzz)$)|(.*)FizzBuzz$") 
   * 置換後:\1\2\3\4
   */
  private static final Pattern FizzBuzzRegEx = Pattern.compile(""
    /* [1] */ + "(?=" + multiplesOfFive + "FizzBuzz$)"
              + multiplesOfThree+ "*(FizzBuzz)$"//
    /* [2] */ + "|" + multiplesOfThree + "*(Fizz)Buzz$"//
    /* [3] */ + "|(?:" + multiplesOfFive + "Fizz(Buzz)$)"//
    /* [4] */ + "|(.*)FizzBuzz$");//

  /**
   * 置換後のキャプチャ取得文字列
   */
  private static final String regExOut = "$1$2$3$4";

  /**
   * 先頭と末尾の1文字を排除した文字列を取得するための正規表現
   */
  private static final Pattern deleteFirstAndLast
                                 = Pattern.compile("^.|.$");

  /**
   * 数字を与えられるとカンマ区切りのFizzBuzzの解の文字列に変換して返却します。
   * @param i 対象数字
   * @return FizzBuzzの解の文字列
   */
  public static String createFizzBuzzString(int i) {
    return deleteFirstAndLast
               .split(convertFizzBuzz(i, new StringBuilder())
               .toString())[1];
  }

  /**
   * 対象数字に対して正規表現でFizzBuzzの解に置換し、sbに挿入していきます。
   * 対象数字を-1しながら再帰的に呼び出されます。
   * 対象数字が0になった場合、ArithmeticExceptionを発生させて再帰を抜けます。
   * @param i 対象数字
   * @param sb FizzBuzzの解を保持するバッファ
   * @return FizzBuzzの解を保持したStringBuilder
   */
  public static StringBuilder convertFizzBuzz(Integer i, 
                                              StringBuilder sb) {
    try {
      convertFizzBuzz(
        i - 1,
        sb.insert(0, ",")
          .insert(0, i / i)
          .deleteCharAt(0)
          .insert(0, FizzBuzzRegEx.matcher(i + "FizzBuzz")
                                  .replaceAll(regExOut)));
    } catch (ArithmeticException e) {
      // 再帰処理終了
    }
    return sb;
  }

  public static void main(String[] args) {
    System.out.println(createFizzBuzzString(Integer.parseInt(args[0])));
  }
}

このコードでは、数値に対応する出力文字列(Fizz/Buzz/FizzBuzz/数字)を正規表現を使って求めており、かなり複雑怪奇なロジックになっている。

正規表現に加えてユニークなのは、再帰の終了判定だ。
他のコードと同様に、convertFizzBuzzメソッドの再帰呼び出しで繰り返しを実現しているが、再帰の終了判定はゼロ除算例外の有無で判断している。
まさに、珍コードもここに極まれり、と言えそうなコードである。