【Kotlin】更に仕様変更に強い FizzBuzz


記事「【Kotlin】仕様変更に強い FizzBuzz」の発展。
さらに仕様変更に強くする。

/**
 * FizzBuzz を行うためのクラス。
 *
 * [invoke] 関数に数値を与えると対応する文字列を返す。
 */
class FizzBuzz private constructor(
    private val fizzBuzzMap: List<(Int, String) -> String>
) {
    /**
     * [block] 内で [FizzBuzzMapBuilder.to] もしくは
     *  [FizzBuzzMapBuilder.unaryPlus] を呼び出すことで
     *  変換規則を追加できる。
     */
    constructor(
        block: FizzBuzzMapBuilder.() -> Unit
    ) : this(
        FizzBuzzMapBuilder().also {
            it.block()
        }.build()
    )

    /** 与えられた数値に対応する文字列を返す。 */
    operator fun invoke(num: Int): String =
        fizzBuzzMap.asSequence()
            .fold("") { str, entry ->
                entry(num, str)
            }
            .takeUnless { it.isEmpty() }
            ?: num.toString()

    class FizzBuzzMapBuilder {
        private val fizzBuzzMap: MutableList<(Int, String) -> String> = mutableListOf()

        /**
         * 割る数と、それで割り切れたときに出力する文字列を
         * 変換規則に追加する。
         *
         * @receiver 割る数
         * @param str 割り切れたときに出力する文字列
         */
        infix fun Int.to(str: String) {
            fizzBuzzMap += { num: Int, currentStr: String ->
                if (num % this == 0) currentStr + str
                else currentStr
            }
        }

        /**
         * 変換規則を追加する。
         * 
         * @receiver 変換規則。
         *  引数 num: [FizzBuzz.invoke] に与えられた数値。
         *  引数 str: この変換規則より前に処理された変換規則により構築された文字列。
         *  返値: この変換規則を適用した結果の文字列。
         *      この変換規則が適用されない場合は引数 str を返すこと。
         *      空文字列を返すと、この変換規則より前に処理された変換規則が
         *      適用されなかったのと同じになる。
         */
        operator fun ((num: Int, str: String) -> String).unaryPlus() {
            fizzBuzzMap += this
        }

        fun build(): List<(Int, String) -> String> =
            fizzBuzzMap.toList()
    }
}

普通の FizzBuzz

/** 変換規則の定義。 */
val fizzBuzz = FizzBuzz {
    3 to "Fizz"
    5 to "Buzz"
}

fun main() {
    (1..15).asSequence()
        .map {
            fizzBuzz(it)
        }.forEach {
            println(it)
        }
}

出力

1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz

複雑な変換規則

  • 2 で割り切れる場合は Foo を出力する。
  • 3 で割り切れる場合は Bar を出力する。
  • 2 でも 3 でも割り切れる場合は FooBar を出力する。
  • ただし 4 で割り切れる場合は Baz のみを出力する。
  • 上記のいずれでもない場合は数値を < > で囲って出力する。
/** 変換規則の定義。 */
val fizzBuzz = FizzBuzz {
    2 to "Foo"
    3 to "Bar"
    +{ num: Int, str: String ->
        if (num % 4 == 0) "Baz"
        else str
    }
    +{ num: Int, str: String ->
        str.takeUnless { str.isEmpty() }
            ?: "<$num>"
    }
}

// main 関数は同じなので省略

出力

<1>
Foo
Bar
Baz
<5>
FooBar
<7>
Baz
Bar
Foo
<11>
Baz
<13>
Foo
Bar

雑感

特殊な変換規則のラムダ式の引数に型を明示しなければいけないのが面倒。

/以上