多数の制約をかいくぐって JavaScript で FizzBuzz

某SNSで、こんな興味深い投稿があった。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">弊社のコーディング規約で FizzBuzz を書ける人はおりまするか？🤔<br><br>＜規約＞<br>(1) else if を含む nested if 禁止<br>(2) switch 文禁止<br>(3) 三項演算子を if 文の制御構造の代わりに使うの禁止<br>(4) 以下の逐次処理禁止<br>- for<br>- while<br>- do-while<br>- for-in<br>- for-of<br>- Array#forEach<a href="https://twitter.com/hashtag/JavaScript?src=hash&amp;ref_src=twsrc%5Etfw">#JavaScript</a></p>&mdash; 悉生 游漩 (@StewEucen) <a href="https://twitter.com/StewEucen/status/1900715327715242160?ref_src=twsrc%5Etfw">March 15, 2025</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

返信に書かれている内容も含めると、以下の構文を使わずに JavaScript で「Fizzbuzz」を書け、とのことである。

* else if を含む nested if
* switch 文
* 三項演算子 (if 文の制御構造の代わり)
* for
* while
* do-while
* for-in
* for-of
* Array#forEach
* if 文 (高階関数に渡す関数内)
* 三項演算子のネスト
* 短絡評価 (条件分岐として)
* let
* var
* 再帰処理 (ループ代わり)

さらに、「既存のコードを改変せずにパターンを追加できるようにする」という条件もある。
追加するパターンとして、

* 7 の倍数で Quzz、11 の倍数で Kass
* 「3 の倍数で Fizz」をなしにして、3 の倍数と 3 が含まれる数で Aho

が挙げられている。

<details><summary>これらの出典</summary>

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">(5) 高階関数に渡す関数内で if 文禁止<br>(6) 三項演算子のネスト禁止</p>&mdash; 悉生 游漩 (@StewEucen) <a href="https://twitter.com/StewEucen/status/1900720896270365190?ref_src=twsrc%5Etfw">March 15, 2025</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">(7) 短絡評価を条件分岐として使うの禁止<br><br>(・ω・)＜弊社の規約多過ぎ</p>&mdash; 悉生 游漩 (@StewEucen) <a href="https://twitter.com/StewEucen/status/1900727894986682663?ref_src=twsrc%5Etfw">March 15, 2025</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">＜コーディング規約＞<br>(8) let/var は禁止<br>(9) 再帰処理をループ代わりに使うのは禁止</p>&mdash; 悉生 游漩 (@StewEucen) <a href="https://twitter.com/StewEucen/status/1900848951097782709?ref_src=twsrc%5Etfw">March 15, 2025</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">＜追加仕様＞<br><br>変換する倍数のパターンを任意数追加可能にする。<br>其の際、既存のコードを改変してはならない。<br><br>7の倍数で Quzz<br>11の倍数で Kass<br>...</p>&mdash; 悉生 游漩 (@StewEucen) <a href="https://twitter.com/StewEucen/status/1900762301579620777?ref_src=twsrc%5Etfw">March 15, 2025</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">＜更に追加仕様＞<br><br>「3の倍数で Fizz」をなしにして、<br>「3の倍数と3が含まれる数で Aho」を追加。<br><br>其の際、既存のコードを改変してはならない。</p>&mdash; 悉生 游漩 (@StewEucen) <a href="https://twitter.com/StewEucen/status/1900819186395340910?ref_src=twsrc%5Etfw">March 15, 2025</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

</details>

# 今回実装する関数

「FizzBuzz」というのは曖昧である。
今回は、以下の仕様の関数を実装する。

```javascript
function FizzBuzz(n, patterns = null)
```

## 引数

* `n`
  * 生成する範囲の最大値を表す、$2^{32}$ 未満の非負整数
* `patterns` (省略可)
  * 以下のメンバを持つオブジェクトの配列
    * `pred`：1個の整数を引数として取り、その整数を変換するべきなら真、そうでないなら偽を返す関数
    * `str`：関数 `pred` が真を返したとき、整数を変換する文字列

## 返値

1以上 `n` 以下の整数を 1 個ずつ順に並べた配列について、以下の変換を行うことを考える。

1. 変換結果を空文字列に初期化する
2. 配列 `patterns` の各要素について、変換対象の整数を渡して関数 `pred` を実行する
   * それが真を返したら、変換結果の最後に `str` を結合し、変換結果を更新する
   * そうでなければ、何もしない
3. `pred` が真を返す要素があった場合は、変換結果に変換する。そうでない場合は、変換対象の整数を十進文字列に変換したものに変換する

変換を行った配列の各要素の間に改行 (`\n`) を挟んで結合した文字列を返す。
`n === 0` の場合は、空文字列を返す。

`patterns` を省略した場合、および配列でないものが渡された場合は、かわりに以下の配列を用いる。

```javascript
[
  { pred: (v) => v % 3 === 0, str: "Fizz" },
  { pred: (v) => v % 5 === 0, str: "Buzz" },
]
```

# 実装のポイント

## 変数のかわりに関数のプロパティを用いる

今回の条件では、`let` および `var` の使用が禁止されている。
どうせなら、`const` も使わないでおく。

JavaScript では、`const` で宣言された変数に代入されていても、オブジェクトや配列の内容を書き換えることができる。
これを応用し、「`const` で宣言された変数に代入されたオブジェクト」のかわりに「`function` で宣言した関数」を用いることで、[厳格モード](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Strict_mode)でも `var`・`let`・`const` を使わずに変数 (のようなもの) を使用できる。

```javascript
"use strict";
function status() {}
status.hoge = 41;
status.hoge++;
console.log(status.hoge); // 42 を出力する
```

## 分岐のかわりにオブジェクトを用いる

今回の条件では、`if` 文・三項演算子・短絡評価の使用に様々な制約がある。
うっかり制約を踏んでしまうのを避けるため、これらは一切使わないことにする。

かわりに、オブジェクトのプロパティを用いることで、値の真偽によって他の値を選択する。

[論理否定演算子 `!`](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Logical_NOT) は、オペランドが真であれば `false` を、偽であれば `true` を返す。
オブジェクトのプロパティに [`[]` を用いてアクセス](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Property_accessors) すると、`[]` の中の値を**文字列に変換した結果**のプロパティにアクセスすることになる。
よって、文字列 `false` および `true` を名前とするプロパティを用意することで、選択ができる。

```javascript
"use strict";
// 条件 cond が真なら whenTrue を、偽なら whenFalse を返す
function select(cond, whenTrue, whenFalse) {
  return {
    false: whenTrue,
    true: whenFalse,
  }[!cond];
}

console.log(select(1 === 1, "yes", "no")); // yes を出力する
console.log(select(1 === 2, "yes", "no")); // no を出力する
```

## ループのかわりに Array.prototype.every() を用いる

今回は `Array#forEach` 以外の標準関数は禁止されていないようである。
そこで、今回はループとして [`Array.prototype.every()`](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/every) を用いる。
これは、最大で対象の配列の要素数だけ引数に渡した関数を実行する関数である。
ただし、実行された関数が偽を返すと、そこで実行が終了する。

```javascript
"use strict";
let i = 0; // これは every() のデモなので、簡単のため let を用いる

Array.from(new Array(10)).every(() => {
  console.log(i); // 0～9 が順に出力される
  i++;
  return true;
});
```

[`Array` のコンストラクター](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/Array)に $2^{32}$ 未満の非負整数を 1 個渡すことで、その要素数の配列を生成することができる。
`new Array(10)` だけでは、値が入っていない[疎配列](https://developer.mozilla.org/ja/docs/Web/JavaScript/Guide/Indexed_collections#%E7%96%8E%E9%85%8D%E5%88%97)ができ、`every()` では全く関数を実行してくれない。
そこで、[`Array.from()`](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/from) を通すことで、値 (`undefined`) が入った配列に変換し、`every()` で関数が実行されるようにした。

# 実装

## 本体

値を選択する関数 `s` を、変数の格納先としても用いた。

:::note warn
`s.matched` を使わずに処理後の `s.cur` が空かどうかで数字を入れるべきかどうかを判定すると、`patterns` 内で `str` として空文字列が指定された場合に判定を間違える原因となる。
:::

```javascript
"use strict";

function FizzBuzz(n, patterns = null) {
  function s(cond, whenTrue, whenFalse) {
    return {
      false: whenTrue,
      true: whenFalse,
    }[!cond];
  }
  s.result = [];
  s.i = 1;
  s.patterns = s(Array.isArray(patterns), patterns, [
    { pred: (v) => v % 3 === 0, str: "Fizz" },
    { pred: (v) => v % 5 === 0, str: "Buzz" },
  ]);
  Array.from(new Array(n)).every(() => {
    s.cur = "";
    s.matched = false;
    s.j = 0;
    Array.from(new Array(s.patterns.length)).every(() => {
      s.judge = s.patterns[s.j].pred(s.i);
      s.cur = s(s.judge, s.cur + s.patterns[s.j].str, s.cur);
      s.matched = s(s.judge, true, s.matched);
      s.j++;
      return true;
    });
    s.result.push(s(s.matched, s.cur, s.i.toString()));
    s.i++;
    return true;
  });
  return s.result.join("\n");
}
```

## 呼び出し

### 基本

```javascript
console.log(FizzBuzz(100));
```

[[JavaScript] Node.js 20.17.0 - Wandbox](https://wandbox.org/permlink/Trg7cO8zowAZDkW5)

<details><summary>実行結果</summary>

```text
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
16
17
Fizz
19
Buzz
Fizz
22
23
Fizz
Buzz
26
Fizz
28
29
FizzBuzz
31
32
Fizz
34
Buzz
Fizz
37
38
Fizz
Buzz
41
Fizz
43
44
FizzBuzz
46
47
Fizz
49
Buzz
Fizz
52
53
Fizz
Buzz
56
Fizz
58
59
FizzBuzz
61
62
Fizz
64
Buzz
Fizz
67
68
Fizz
Buzz
71
Fizz
73
74
FizzBuzz
76
77
Fizz
79
Buzz
Fizz
82
83
Fizz
Buzz
86
Fizz
88
89
FizzBuzz
91
92
Fizz
94
Buzz
Fizz
97
98
Fizz
Buzz
```

</details>

### 追加仕様1

> 7 の倍数で Quzz、11 の倍数で Kass

という条件を追加したバージョン。
また、3個結合されるパターンを見られるよう、出力する範囲を拡大した。

```javascript
console.log(FizzBuzz(300, [
  { pred: (v) => v % 3 === 0, str: "Fizz" },
  { pred: (v) => v % 5 === 0, str: "Buzz" },
  { pred: (v) => v % 7 === 0, str: "Quzz" },
  { pred: (v) => v % 11 === 0, str: "Kass" },
]));
```

[[JavaScript] Node.js 20.17.0 - Wandbox](https://wandbox.org/permlink/kDJy8K6Hovgw079E)

<details><summary>実行結果</summary>

```text
1
2
Fizz
4
Buzz
Fizz
Quzz
8
Fizz
Buzz
Kass
Fizz
13
Quzz
FizzBuzz
16
17
Fizz
19
Buzz
FizzQuzz
Kass
23
Fizz
Buzz
26
Fizz
Quzz
29
FizzBuzz
31
32
FizzKass
34
BuzzQuzz
Fizz
37
38
Fizz
Buzz
41
FizzQuzz
43
Kass
FizzBuzz
46
47
Fizz
Quzz
Buzz
Fizz
52
53
Fizz
BuzzKass
Quzz
Fizz
58
59
FizzBuzz
61
62
FizzQuzz
64
Buzz
FizzKass
67
68
Fizz
BuzzQuzz
71
Fizz
73
74
FizzBuzz
76
QuzzKass
Fizz
79
Buzz
Fizz
82
83
FizzQuzz
Buzz
86
Fizz
Kass
89
FizzBuzz
Quzz
92
Fizz
94
Buzz
Fizz
97
Quzz
FizzKass
Buzz
101
Fizz
103
104
FizzBuzzQuzz
106
107
Fizz
109
BuzzKass
Fizz
Quzz
113
Fizz
Buzz
116
Fizz
118
Quzz
FizzBuzz
Kass
122
Fizz
124
Buzz
FizzQuzz
127
128
Fizz
Buzz
131
FizzKass
Quzz
134
FizzBuzz
136
137
Fizz
139
BuzzQuzz
Fizz
142
Kass
Fizz
Buzz
146
FizzQuzz
148
149
FizzBuzz
151
152
Fizz
QuzzKass
Buzz
Fizz
157
158
Fizz
Buzz
Quzz
Fizz
163
164
FizzBuzzKass
166
167
FizzQuzz
169
Buzz
Fizz
172
173
Fizz
BuzzQuzz
Kass
Fizz
178
179
FizzBuzz
181
Quzz
Fizz
184
Buzz
Fizz
Kass
188
FizzQuzz
Buzz
191
Fizz
193
194
FizzBuzz
Quzz
197
FizzKass
199
Buzz
Fizz
202
Quzz
Fizz
Buzz
206
Fizz
208
Kass
FizzBuzzQuzz
211
212
Fizz
214
Buzz
Fizz
Quzz
218
Fizz
BuzzKass
221
Fizz
223
Quzz
FizzBuzz
226
227
Fizz
229
Buzz
FizzQuzzKass
232
233
Fizz
Buzz
236
Fizz
Quzz
239
FizzBuzz
241
Kass
Fizz
244
BuzzQuzz
Fizz
247
248
Fizz
Buzz
251
FizzQuzz
Kass
254
FizzBuzz
256
257
Fizz
Quzz
Buzz
Fizz
262
263
FizzKass
Buzz
Quzz
Fizz
268
269
FizzBuzz
271
272
FizzQuzz
274
BuzzKass
Fizz
277
278
Fizz
BuzzQuzz
281
Fizz
283
284
FizzBuzz
Kass
Quzz
Fizz
289
Buzz
Fizz
292
293
FizzQuzz
Buzz
296
FizzKass
298
299
FizzBuzz
```

</details>

### 追加仕様2

> 「3 の倍数で Fizz」をなしにして、3 の倍数と 3 が含まれる数で Aho

という条件を追加したバージョン。
「3 が含まれる数」は、「十進数で表現したとき、1 個以上の桁が `3` である数」と解釈した。
3 で割った余りに 3 を足した数を結合することで、「3 が含まれる数」の判定で「3 の倍数」も判定できるようにした。

```javascript
console.log(FizzBuzz(100, [
  { pred: (v) => (v.toString() + (v % 3 + 3)).indexOf("3") >= 0, str: "Aho" },
  { pred: (v) => v % 5 === 0, str: "Buzz" },
]));
```

[[JavaScript] Node.js 20.17.0 - Wandbox](https://wandbox.org/permlink/OHtq0uA3PrvvZawi)

<details><summary>実行結果</summary>

```text
1
2
Aho
4
Buzz
Aho
7
8
Aho
Buzz
11
Aho
Aho
14
AhoBuzz
16
17
Aho
19
Buzz
Aho
22
Aho
Aho
Buzz
26
Aho
28
29
AhoBuzz
Aho
Aho
Aho
Aho
AhoBuzz
Aho
Aho
Aho
Aho
Buzz
41
Aho
Aho
44
AhoBuzz
46
47
Aho
49
Buzz
Aho
52
Aho
Aho
Buzz
56
Aho
58
59
AhoBuzz
61
62
Aho
64
Buzz
Aho
67
68
Aho
Buzz
71
Aho
Aho
74
AhoBuzz
76
77
Aho
79
Buzz
Aho
82
Aho
Aho
Buzz
86
Aho
88
89
AhoBuzz
91
92
Aho
94
Buzz
Aho
97
98
Aho
Buzz
```

</details>

### 追加仕様1+2

> * 7 の倍数で Quzz、11 の倍数で Kass
> * 「3 の倍数で Fizz」をなしにして、3 の倍数と 3 が含まれる数で Aho

という条件を両方追加したバージョン。

```javascript
console.log(FizzBuzz(300, [
  { pred: (v) => (v.toString() + (v % 3 + 3)).indexOf("3") >= 0, str: "Aho" },
  { pred: (v) => v % 5 === 0, str: "Buzz" },
  { pred: (v) => v % 7 === 0, str: "Quzz" },
  { pred: (v) => v % 11 === 0, str: "Kass" },
]));
```

[[JavaScript] Node.js 20.17.0 - Wandbox](https://wandbox.org/permlink/oBpa3Yi85lNArsJZ)

<details><summary>実行結果</summary>

```text
1
2
Aho
4
Buzz
Aho
Quzz
8
Aho
Buzz
Kass
Aho
Aho
Quzz
AhoBuzz
16
17
Aho
19
Buzz
AhoQuzz
Kass
Aho
Aho
Buzz
26
Aho
Quzz
29
AhoBuzz
Aho
Aho
AhoKass
Aho
AhoBuzzQuzz
Aho
Aho
Aho
Aho
Buzz
41
AhoQuzz
Aho
Kass
AhoBuzz
46
47
Aho
Quzz
Buzz
Aho
52
Aho
Aho
BuzzKass
Quzz
Aho
58
59
AhoBuzz
61
62
AhoQuzz
64
Buzz
AhoKass
67
68
Aho
BuzzQuzz
71
Aho
Aho
74
AhoBuzz
76
QuzzKass
Aho
79
Buzz
Aho
82
Aho
AhoQuzz
Buzz
86
Aho
Kass
89
AhoBuzz
Quzz
92
Aho
94
Buzz
Aho
97
Quzz
AhoKass
Buzz
101
Aho
Aho
104
AhoBuzzQuzz
106
107
Aho
109
BuzzKass
Aho
Quzz
Aho
Aho
Buzz
116
Aho
118
Quzz
AhoBuzz
Kass
122
Aho
124
Buzz
AhoQuzz
127
128
Aho
AhoBuzz
Aho
AhoKass
AhoQuzz
Aho
AhoBuzz
Aho
Aho
Aho
Aho
BuzzQuzz
Aho
142
AhoKass
Aho
Buzz
146
AhoQuzz
148
149
AhoBuzz
151
152
Aho
QuzzKass
Buzz
Aho
157
158
Aho
Buzz
Quzz
Aho
Aho
164
AhoBuzzKass
166
167
AhoQuzz
169
Buzz
Aho
172
Aho
Aho
BuzzQuzz
Kass
Aho
178
179
AhoBuzz
181
Quzz
Aho
184
Buzz
Aho
Kass
188
AhoQuzz
Buzz
191
Aho
Aho
194
AhoBuzz
Quzz
197
AhoKass
199
Buzz
Aho
202
AhoQuzz
Aho
Buzz
206
Aho
208
Kass
AhoBuzzQuzz
211
212
Aho
214
Buzz
Aho
Quzz
218
Aho
BuzzKass
221
Aho
Aho
Quzz
AhoBuzz
226
227
Aho
229
AhoBuzz
AhoQuzzKass
Aho
Aho
Aho
AhoBuzz
Aho
Aho
AhoQuzz
Aho
AhoBuzz
241
Kass
Aho
244
BuzzQuzz
Aho
247
248
Aho
Buzz
251
AhoQuzz
AhoKass
254
AhoBuzz
256
257
Aho
Quzz
Buzz
Aho
262
Aho
AhoKass
Buzz
Quzz
Aho
268
269
AhoBuzz
271
272
AhoQuzz
274
BuzzKass
Aho
277
278
Aho
BuzzQuzz
281
Aho
Aho
284
AhoBuzz
Kass
Quzz
Aho
289
Buzz
Aho
292
Aho
AhoQuzz
Buzz
296
AhoKass
298
299
AhoBuzz
```

</details>

# 結論

提示された制約を守りつつ、JavaScript で FizzBuzz を実装することができた。