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


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

## FizzBuzz問題とは

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

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

[どうしてプログラマに・・・プログラムが書けないのか?](http://www.aoky.net/articles/jeff_atwood/why_cant_programmers_program.htm)から引用


## 普通に解く(1)

```csharp
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メソッドは、次のようにも書けますね。

```csharp
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クラスとしました。


```csharp
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メソッドで、ルールを追加しています。 第１引数が条件のラムダ式、第２引数が出力する文字列を組み立てるラムダ式です。 第２引数は、単純な文字列でも良かったのですが、柔軟性を持たせるためにラムダ式を受け取るようにしました。

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

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

```csharp
    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で公開したものを加筆・修正したものです。