無駄に柔軟性がある FizzBuzz を作ってみた

多くのプログラマが通る有名な問題 [FizzBuzz](https://en.wikipedia.org/wiki/Fizz_buzz) を無駄に極限まで柔軟性がある実装にしてみた
※ 最短部門ならちらほら見かけるが最柔軟部門はあんまり見かけないので

- 方針
    - Java11
    - 標準ライブラリを積極的に使用
    - 最終的に Builder Pattern っぽいものになる

# 一般的な解

色々工夫すればもっと短くなるが教科書に載るような解答ならこんな感じでしょうか？

```java
public class FizzBuzz {
    public static void main(String[] args) {
        for (int i = 1; i <= 100; i++) {
            if (i % 15 == 0) {
                System.out.println("FizzBuzz");
            } else if (i % 3 == 0) {
                System.out.println("Fizz");
            } else if (i % 5 == 0) {
                System.out.println("Buzz");
            } else {
                System.out.println(i);
            }
        }
    }
}
```

output （見やすくするように一行にまとめています）

```
1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz 16 17 Fizz ...
```

# 魔改造開始

## 「3: Fizz」「5: Buzz」といったペアを外部化

- Map 形式で保存
- Stream で各数字の判定＆文字列を結合を行う

```java
Map<Integer, String> map = new HashMap<>();
map.put(3, "Fizz");
map.put(5, "Buzz");

for (int i = 1; i <= 100; i++) {
    final int current = i; // このあと消えます
    String result = map.entrySet().stream()
            .filter(v -> current % v.getKey() == 0)
            .map(Entry::getValue)
            .reduce("", (left, right) -> left + right);
    System.out.println(result.isBlank() ? i : result);
}
```

## 別メソッドに分離

- 結果をリストとして返すメソッドに分離
- ついでに開始と終了値もカスタマイズ可能に

```java
public static List<String> fizzBuzz(int start, int end, Map<Integer, String> map) {
    return IntStream.rangeClosed(start, end).mapToObj(i -> {
        String result = map.entrySet().stream()
                .filter(v -> i % v.getKey() == 0)
                .map(Entry::getValue)
                .reduce("", (left, right) -> left + right);
        return result.isBlank() ? String.valueOf(i) : result;
    }).collect(Collectors.toList());
}

public static void main(String[] args) {
    Map<Integer, String> map = new HashMap<>();
    map.put(3, "Fizz");
    map.put(5, "Buzz");

    fizzBuzz(1, 100, map).forEach(System.out::println);
}
```

## Builder pattern っぽいものにする

- 一般的に Builder pattern における build メソッドはある独立したクラスを返すべきだが、ここでは List\<String\> を返すとする
    - ちゃんとやるなら多分「FizzBuzzBuilder」と「FizzBuzz」の 2 クラスに分けることになるかと
- javadoc は割愛

```java
import java.util.*;
import java.util.Map.Entry;
import java.util.stream.*;

public class FizzBuzz {
    private Map<Integer, String> map = new HashMap<>();
    private int start = 1;
    private int end = 100;

    public List<String> build() {
        return IntStream.rangeClosed(start, end).mapToObj(i -> {
            String result = map.entrySet().stream()
                    .filter(v -> i % v.getKey() == 0)
                    .map(Entry::getValue)
                    .reduce("", (left, right) -> left + right);
            return result.isBlank() ? String.valueOf(i) : result;
        }).collect(Collectors.toList());
    }

    public FizzBuzz addPair(int value, String text) {
        map.put(value, text);
        return this;
    }

    public FizzBuzz start(int start) {
        this.start = start;
        return this;
    }

    public FizzBuzz end(int end) {
        this.end = end;
        return this;
    }

    public static void main(String[] args) {
        new FizzBuzz()
                .start(1)
                .end(20)
                .addPair(3, "Fizz")
                .addPair(5, "Buzz")
                .build()
                .forEach(System.out::println);
    }
}
```

# 試運転

- せっかくなので互いに素の 3, 5, 11, 13 でテスト
- 最小公倍数は 2145 なので 2130 ～ 2150 あたりを出力する

```java
new FizzBuzz()
        .start(2130)
        .end(2150)
        .addPair(3, "Fizz")
        .addPair(5, "Buzz")
        .addPair(11, "Foo")
        .addPair(13, "Bar")
        .build()
        .forEach(System.out::println);
```

## 結果

```
FizzBuzz
2131
Bar
Fizz
Foo
Buzz
Fizz
2137
2138
Fizz
Buzz
2141
Fizz
2143
2144
FizzBuzzFooBar
2146
2147
Fizz
2149
Buzz
```

# おまけ：JavaScript version

JavaScript のパラダイムをよくわかっていない人が作るとこうなる
本職の方が作るものと乖離しそうなのであくまで参考

```javascript
(() => {
    class FizzBuzz {
        constructor() {
            this._from = 1;
            this._to = 100;
            this._pair = [];
        }
        build() {
            const convert = i => this._pair
                .filter(divisor => i % divisor.value == 0)
                .reduce((left, right) => left + right.text, "") || i;
            return [...Array(this._to - this._from + 1).keys()]
                .map(i => i + this._from)
                .map(convert);
        }
        addPair(value, text) {
            this._pair.push({value: value, text: text});
            return this;
        }
        from(from) {
            this._from = from;
            return this;
        }
        to(to) {
            this._to = to;
            return this;
        }
    };
    new FizzBuzz()
        .from(2130)
        .to(2150)
        .addPair(3, "Fizz")
        .addPair(5, "Buzz")
        .addPair(11, "Foo")
        .addPair(13, "Bar")
        .build()
        .forEach(result => console.log(result));
})();