[Kotlin] Version のソートアルゴリズムを高階関数縛りで考えてみた

ネタ元

https://qiita.com/StewEucen/items/7fdef77a53e98ef676fb#comment-0a53ab98788c80783ba8

セマンティックバージョニングのような、ドットで区切られたバージョン番号の比較を、`for` 文を使わず高階関数縛りで行う。
（`for` 文を使わないことより、再代入可能な変数（`var`）やミュータブルな値を使わないというのが本質かな。）


全文は長いのでまず要点を抜粋。
```Kotlin
/**
 * バージョンを比較する。
 */
fun compareVersion(v1: String, v2: String): Int {
    return run {
        // それぞれのバージョンを "." で分割し、インデックスが同じ要素同士をペアにしたリストにする。
        // 要素の数が異なる場合、多い方の要素は無視される。
        v1.split(".") zip v2.split(".")
    }
        .dropWhile { (p1, p2) ->
            p1 == p2
        }
        // ^ 同じインデックスの要素が等価である部分を除く。
        .firstOrNull()
        // ^ 同じインデックスの要素が等価である最初の要素を取得する。
        ?.let { (p1, p2) ->
            // パート（バージョンを "." で分割したリストにしたときの要素）を比較する。
            comparePart(p1, p2)
        }
        ?: compareValues(v1.length, v2.length)
    // ^ 一方のバージョンの先頭が他方のバージョンと一致する場合、短い方が小さいとする。
}
```

コード全文
```Kotlin
fun main() {
    val comparator = Comparator<String> { v1, v2 ->
        compareVersion(v1, v2)
    }
    versions.sortedWith(comparator.reversed())
        .also { sortedVersions ->
            if (sortedVersions == output) {
                println("OK")
                return
            }

            println("NG")
            sortedVersions.forEach { println(it) }
        }
}

val versions = listOf(
    "5.0.0.",
    "1.3.0.9",
    "0.2.0",
    "3.1.2",
    "5",
    "0.1.6",
    "5.0.0",
    "3.3.3.3",
    "3.3.3.3.3",
    "3.10",
    "0.2.0",
    "0.2.0a",
    "0.2.0aa",
    "0.2.0AA",
    "0.2.0Aa",
    "0.2.0aA",
    "0.2.0c",
    "0.2.0b",
    "0.2.0A",
    "0.2.0C",
    "0.2.0B",
    "0.2.0.a",
    "0.2.0.c",
    "0.2.0.b",
    "0.2.0.A",
    "0.2.0.C",
    "0.2.0.B",
    "5.0.0.",
    "5.0.0.0",
)

val output = listOf(
    "5.0.0.0",
    "5.0.0.",
    "5.0.0.",
    "5.0.0",
    "5",
    "3.10",
    "3.3.3.3.3",
    "3.3.3.3",
    "3.1.2",
    "1.3.0.9",
    "0.2.0C",
    "0.2.0c",
    "0.2.0B",
    "0.2.0b",
    "0.2.0AA",
    "0.2.0Aa",
    "0.2.0aA",
    "0.2.0aa",
    "0.2.0A",
    "0.2.0a",
    "0.2.0.C",
    "0.2.0.c",
    "0.2.0.B",
    "0.2.0.b",
    "0.2.0.A",
    "0.2.0.a",
    "0.2.0",
    "0.2.0",
    "0.1.6",
)

/**
 * バージョンを比較する。
 */
fun compareVersion(v1: String, v2: String): Int {
    return run {
        // それぞれのバージョンを "." で分割し、インデックスが同じ要素同士をペアにしたリストにする。
        // 要素の数が異なる場合、多い方の要素は無視される。
        v1.split(".") zip v2.split(".")
    }
        .dropWhile { (p1, p2) ->
            p1 == p2
        }
        // ^ 同じインデックスの要素が等価である部分を除く。
        .firstOrNull()
        // ^ 同じインデックスの要素が等価であるない最初の要素を取得する。
        ?.let { (p1, p2) ->
            // パート（バージョンを "." で分割したリストにしたときの要素）を比較する。
            comparePart(p1, p2)
        }
        ?: compareValues(v1.length, v2.length)
    // ^ 一方のバージョンの先頭が他方のバージョンと一致する場合、短い方が小さいとする。
}

/**
 * パート（バージョンを "." で分割したリストにしたときの要素）を比較する。
 */
fun comparePart(p1: String, p2: String): Int {
    val (i1, a1) = p1.toIntAndAlphabetsFromPart()
    val (i2, a2) = p2.toIntAndAlphabetsFromPart()

    return compareValues(i1, i2)
        .takeIf { it != 0 }
        ?: compareAlphabets(a1, a2)
}

/**
 * アルファベット列を比較する。
 */
fun compareAlphabets(a1: String, a2: String): Int {
    return compareBy<String> { it.uppercase() }
        .thenByDescending { it }
        .compare(a1, a2)
}

/**
 * パート（バージョンを "." で分割したリストにしたときの要素）を整数部分とアルファベット列部分にパースして返す。
 */
fun String.toIntAndAlphabetsFromPart(): Pair<Int?, String> {
    return "^(0|[1-9][0-9]*|)([a-zA-Z]*)$"
        .toRegex()
        .matchEntire(this)
        .let { requireNotNull(it) }
        .groupValues
        .let { groupValues ->
            Pair(
                groupValues[1].takeIf { it.isNotEmpty() }?.toInt(),
                groupValues[2],
            )
        }
}
```

／以上