Kotlinで逆FizzBuzz

ネタ元

https://qiita.com/matsurih/items/e911e996bc0cd7783f97

> "Fizz","Buzz","FizzBuzz"の3つの文字列のいずれかのみからなるリストLが与えられた時
> FizzBuzz操作を行うとそのリストLと同じような出力を行う自然数列a<sub>n</sub>のうち、最も要素数が少ないものをA<sub>n</sub>とする。
> （ただしここではa<sub>i</sub>が3の倍数でも5の倍数でもない場合は何も出力しないこととする。）
>
> このようなA<sub>n</sub>を出力するプログラムを作成せよ。
> （ただし、A<sub>n</sub>の候補が複数存在する場合は、その総和が最も小さいものを$A_n$とする。）


## 実装

```Kotlin
/**
 * FizzBuzz 文字列（"Fizz", "Buzz", "FizzBuzz" のいずれか）のリストに合致する逆 FizzBuzz を返す。
 * 合致するものがなければ空のリストを返す。
 *
 * @receiver FizzBuzz 文字列（"Fizz", "Buzz", "FizzBuzz" のいずれか）のリスト。
 */
fun List<String>.toInverseFizzBuzz(): List<Int> {
    (0 until countOfFizzBuzzStringOfRound).forEach { index ->
        sequenceOfFizzBuzzStringAndNumber
            .drop(index)
            .take(this.size)
            .toList()
            .takeIf {
                it.map { (fizzBuzz, _) ->
                    fizzBuzz
                } == this
            }
            ?.also {
                return run { it.first().second..it.last().second }
                    .toList()
            }
    }
    
    return emptyList()
}

/**
 * 1から15までで FizzBuzz 文字列（"Fizz", "Buzz", "FizzBuzz" のいずれか）が出力される数値の、
 * FizzBuzz文字列と数値のペアのリスト。
 */
private val listOfFizzBuzzStringAndNumberOfFirstRound: List<Pair<String, Int>> =
    (1..15).mapNotNull { number ->
        number.toFizzBuzz()?.let { it to number }
    }

/** 1から15までで FizzBuzz 文字列（"Fizz", "Buzz", "FizzBuzz" のいずれか）が出力される回数。 */
private val countOfFizzBuzzStringOfRound: Int
    get() = listOfFizzBuzzStringAndNumberOfFirstRound.size

/**
 * FizzBuzz 文字列（"Fizz", "Buzz", "FizzBuzz" のいずれか）が出力される数値の、
 * FizzBuzz 文字列と数値のペアのシーケンス。
 * 数値は1以上の小さい順。
 */
private val sequenceOfFizzBuzzStringAndNumber: Sequence<Pair<String, Int>>
    get() {
        var index = 0
        return sequence {
            while (true) {
                listOfFizzBuzzStringAndNumberOfFirstRound[index % countOfFizzBuzzStringOfRound]
                    .let { (fizzBuzz, number) ->
                        Pair(
                            fizzBuzz,
                            number + 15 * (index / countOfFizzBuzzStringOfRound),
                        )
                    }.also {
                        yield(it)
                    }
                index += 1
            }
        }
    }

/**
 * 数値を FizzBuzz 文字列（"Fizz", "Buzz", "FizzBuzz" のいずれか）に変換する。
 * 数値が 3 の倍数でも 5 の倍数でもない場合は null を返す。
 */
private fun Int.toFizzBuzz(): String? {
    return when {
        this % 15 == 0 -> "FizzBuzz"
        this % 3 == 0 -> "Fizz"
        this % 5 == 0 -> "Buzz"
        else -> null
    }
}
```


## 解説

FizzBuzz での文字列の出力は、3 と 5 の最小公倍数である 15の倍数ごとにループする。

| 数値 | 出力文字列 |
|---|---|
| 3 | Fizz |
| 5 | Buzz |
| 6 | Fizz |
| 9 | Fizz |
| 10 | Buzz |
| 12 | Fizz |
| 15 | FizzBuzz |
| 18 | Fizz |
| 20 | Buzz |
| 21 | Fizz |
| 24 | Fizz |
| 25 | Buzz |
| 27 | Fizz |
| 30 | FizzBuzz |

従って逆 FizzBuzz においては、
入力が "Fizz", "Buzz", "Fizz", "Buzz", "Fizz", "FizzBuzz", ... で始まっていれば結果は 3 から始まり、
入力が "Buzz", "Fizz", "Buzz", "Fizz", "FizzBuzz", "Fizz", ... で始まっていれば結果は 5 から始まり、
入力が "FizzBuzz", "Fizz", "Buzz", "Fizz", "Buzz", "Fizz", ... で始まっていれば結果は 15 から始まる、
ということになる。
3, 5, 6, 10, 12, 15 のいずれかから始まるパターンのいずれにもマッチしなければ、それ以降のいずれにもマッチしない。

1 から 15 までの出力文字列と数値のペアのリストを変数 `listOfFizzBuzzStringAndNumberOfFirstRound` として定義する。
これを用いて、FizzBuzz の出力文字列と数値のペアを順に返すシーケンス `sequenceOfFizzBuzzStringAndNumber` を実装する。
`sequenceOfFizzBuzzStringAndNumber` のインデックス 0 の要素から入力の要素数と同じ要素数を取り出したリストが入力と一致すれば結果は 3 始まりである。
`sequenceOfFizzBuzzStringAndNumber` のインデックス 1 の要素から入力の要素数と同じ要素数を取り出したリストが入力と一致すれば結果は 5 始まりである。
`sequenceOfFizzBuzzStringAndNumber` のインデックス 5 の要素から入力の要素数と同じ要素数を取り出したリストが入力と一致すれば結果は 15 始まりである。
インデックス 6 以降はあり得ない（15 まででループしているので、あるならすでにマッチしている）。


## 動作確認

```Kotlin
fun main() {
    assertInverseFizzBuzz(
        listOf("Fizz"),
        listOf(3)
    ) // > OK
    assertInverseFizzBuzz(
        listOf("Fizz"),
        listOf(3)
    ) // > OK
    assertInverseFizzBuzz(
        listOf("Buzz"),
        listOf(5)
    ) // > OK
    assertInverseFizzBuzz(
        listOf("FizzBuzz"),
        listOf(15)
    ) // > OK
    assertInverseFizzBuzz(
        listOf("Fizz", "Fizz"),
        listOf(6, 7, 8, 9)
    ) // > OK
    assertInverseFizzBuzz(
        listOf("Fizz", "Buzz"),
        listOf(9, 10)
    ) // > OK
    assertInverseFizzBuzz(
        listOf("Buzz", "Fizz"),
        listOf(5, 6)
    ) // > OK
    assertInverseFizzBuzz(
        listOf("Fizz", "FizzBuzz"),
        listOf(12, 13, 14, 15)
    ) // > OK
    assertInverseFizzBuzz(
        listOf("FizzBuzz", "Fizz"),
        listOf(15, 16, 17, 18)
    ) // > OK
    assertInverseFizzBuzz(
        listOf("Fizz", "Fizz", "Buzz"),
        listOf(6, 7, 8, 9, 10)
    ) // > OK
    assertInverseFizzBuzz(
        listOf("Fizz", "Buzz", "Fizz"),
        listOf(3, 4, 5, 6)
    ) // > OK
    assertInverseFizzBuzz(
        listOf("Buzz", "Fizz", "Fizz"),
        listOf(5, 6, 7, 8, 9)
    ) // > OK
    assertInverseFizzBuzz(
        listOf("Buzz", "Fizz", "FizzBuzz"),
        listOf(10, 11, 12, 13, 14, 15)
    ) // > OK
    assertInverseFizzBuzz(
        listOf("Fizz", "Fizz", "Buzz", "Fizz"),
        listOf(6, 7, 8, 9, 10, 11, 12)
    ) // > OK
    assertInverseFizzBuzz(
        listOf("Fizz", "Buzz", "Fizz", "Fizz"),
        listOf(3, 4, 5, 6, 7, 8, 9)
    ) // > OK
    assertInverseFizzBuzz(
        listOf("Buzz", "Fizz", "Fizz", "Buzz"),
        listOf(5, 6, 7, 8, 9, 10)
    ) // > OK
    assertInverseFizzBuzz(
        listOf("Fizz", "Buzz", "Fizz", "FizzBuzz"),
        listOf(9, 10, 11, 12, 13, 14, 15)
    ) // > OK
    assertInverseFizzBuzz(
        listOf("Fizz", "Buzz", "Fizz", "Fizz", "Buzz"),
        listOf(3, 4, 5, 6, 7, 8, 9, 10)
    ) // > OK
    assertInverseFizzBuzz(
        listOf("Buzz", "Fizz", "Fizz", "Buzz", "Fizz"),
        listOf(5, 6, 7, 8, 9, 10, 11, 12)
    ) // > OK
    assertInverseFizzBuzz(
        listOf("Fizz", "Fizz", "Buzz", "Fizz", "FizzBuzz"),
        listOf(6, 7, 8, 9, 10, 11, 12, 13, 14, 15)
    ) // > OK
    assertInverseFizzBuzz(
        listOf("Fizz", "Buzz", "Fizz", "Fizz", "Buzz", "Fizz"),
        listOf(3, 4, 5, 6, 7, 8, 9, 10, 11, 12)
    ) // > OK
    assertInverseFizzBuzz(
        listOf("Buzz", "Fizz", "Fizz", "Buzz", "Fizz", "FizzBuzz"),
        listOf(5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15)
    ) // > OK
    assertInverseFizzBuzz(
        listOf("Fizz", "Buzz", "Fizz", "Fizz", "Buzz", "Fizz", "FizzBuzz"),
        listOf(3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15)
    ) // > OK
}

fun assertInverseFizzBuzz(input: List<String>, expectedOutput: List<Int>) {
    val actualOutput = input.toInverseFizzBuzz()
    if (actualOutput == expectedOutput) {
        println("OK")
    } else {
        println("NG")
    }
}
```

／以上