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
メソッドの再帰呼び出しで繰り返しを実現しているが、再帰の終了判定はゼロ除算例外の有無で判断している。
まさに、珍コードもここに極まれり、と言えそうなコードである。