C#
C#小品集シリース

C#:FizzBuzz問題を一般化して考えてみた

More than 1 year has passed since last update.

FizzBuzz問題が話題になってから、すでに10年以上経過するわけですが、 C#でFizzBuzz問題について考えてみたいと思います。

FizzBuzz問題とは

まずは、FizzBizz問題とは、以下のような問題です。

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

どうしてプログラマに・・・プログラムが書けないのか?から引用

普通に解く(1)

using System;

namespace FizzBuzzApp {
    class Program {
        static void Main(string[] args) {
            for (int i = 1; i <= 100; i++) {
                Console.WriteLine(FizzBuzz(i));
            }
        }

        static string FizzBuzz(int n) {
            if (n % 3 == 0 && n % 5 == 0)
                return "FizzBuzz";
            if (n % 3 == 0)
                return "Fizz";
            if (n % 5 == 0)
                return "Buzz";
            return n.ToString();
        }
    }
}

ロジックの中に、出力のコード(Console.WriteLine)が入るのは嫌なので、FizzBuzzというメソッドを定義しました。

普通に解く(2)

上のFizzBuzzメソッドは、次のようにも書けますね。

static string FizzBuzz(int n) {
    string s = "";
    if (n % 3 == 0)
        s += "Fizz";
    if (n % 5 == 0)
        s += "Buzz";
    return s == "" ? n.ToString() : s;
}

このほうが、比較する回数が少なくなります。
ここまでは、ありふれたFizzBuzz問題のコード。

一般化を試みる

さて、このFizzBuzz問題を一般化してみようと思います。やりたいことは、ルールをFizzBuzzメソッドの外側から与えられるようにしたいということ。 そうすれば、

  • 1からnまでの数をプリントするプログラムを書け。ただし
  • 3の倍数のときは数の代わりに「Jazz」とプリント
  • 5の倍数のときは「Buzz」とプリント
  • 7の倍数のときは「Pizz」とプリント
  • 3と5両方の倍数の場合には「JazzBuzz」とプリント
  • 3と7両方の倍数の場合には「JazzPizz」とプリント
  • 5と7両方の倍数の場合には「BuzzPizz」とプリント
  • 3と5と7の倍数の場合には「JazzBuzzPizz」とプリントすること。

のような問題にも答えることができるようになります。

C#のコードは以下の通り。いままではメソッドでしたが、FizzBuzzクラスとしました。

using System;
using System.Collections.Generic;
using System.Linq;

namespace Sample {
    class Program {
        static void Main(string[] args) {
            var fb = new FizzBuzz();
            fb.AddRule(n => n % 3 == 0, _ => "Fizz");
            fb.AddRule(n => n % 5 == 0, _ => "Buzz");
            fb.Execute(1, 100, Console.WriteLine);
        }
    }

    public class FizzBuzz {
        class Rule {
            public Func<int, bool> Condition { get; set; }
            public Func<int, string> Value { get; set; }
        }

        private List<Rule> rules = new List<Rule>();

        public void Execute(int start, int stop, Action<string> action) {
            string GetText(int n) {
                var s = rules.Aggregate("", (a, rule) => rule.Condition(n) ? a + rule.Value(n) : a);
                return (s == "") ? n.ToString() : s;
            }       

            for (int i = start; i <= stop; i++) {
                action(GetText(i));
            }
        }

        public void AddRule(Func<int, bool> cond, Func<int, string> value) {
            rules.Add(new Rule { Condition = cond, Value = value });
        }
    }
}

AddRuleメソッドで、ルールを追加しています。 第1引数が条件のラムダ式、第2引数が出力する文字列を組み立てるラムダ式です。 第2引数は、単純な文字列でも良かったのですが、柔軟性を持たせるためにラムダ式を受け取るようにしました。

Executeでは、ローカル関数を使ってみました。あまり意味ないけど...

単純なバージョンと比べてかなり複雑になってしまいましたが、このFizzBuzzクラスを利用すれば、以下のようにルールを変更することができます。

    static void Main(string[] args) {
        var fb = new FizzBuzz();
        fb.AddRule(n => n % 3 == 0, _ => "Jazz");
        fb.AddRule(n => n % 5 == 0, _ => "Buzz");
        fb.AddRule(n => n % 7 == 0, _ => "Pizz");
        fb.Execute(1, 200, Console.WriteLine);
    }

この記事は、Gushwell's C# Programming Pageで公開したものを加筆・修正したものです。