惯用地分组字符串(计算连续重复的字符)

Idiomatically group String (count consecutively repeated characters)

用什么成语怎么达到预期的效果?

val input = "aaaabbbcca"

val result = input.(here do the transformations)

val output = listOf("a" to 4, "b" to 3, "c" to 2, "a" to 1)

assert(result == output)

这里有一个有趣的方法,可以使用 fold

fun main() {
    val result = "aaaabbbcca"
        .chunked(1)
        .fold(emptyList<Pair<String, Int>>()) { list, current ->
            val (prev, count) = list.lastOrNull() ?: Pair(current, 0)
            if (prev == current) list.dropLast(1) + Pair(current, count + 1)
            else list + Pair(current, 1)
        }

    val output = listOf("a" to 4, "b" to 3, "c" to 2, "a" to 1)
    check(result == output)
    println(result)
}

输出:

[(a, 4), (b, 3), (c, 2), (a, 1)]

这是一个棘手的小问题,我找不到特定的惯用解决方案。

然而,这里有一个非常简洁的:

val result = input.replace(Regex("(.)(?!\1)(.)"), "§")
                  .split("§")
                  .map{ Pair(it[0], it.length) }

它使用一个复杂的小正则表达式在每对不同的字符之间插入一个标记字符(此处为 §,当然它可以处理任何不能出现在输入中的字符)。 ((?…) 是零宽度先行断言,因此 (?!) 断言下一个字符与前一个字符不同。我们需要在匹配中包含下一个字符,否则它会追加最后一个字符后也有一个标记。)

在这种情况下得到 aaaa§bbb§cc§a

然后我们在标记处拆分字符串,给出字符组列表(在本例中为 "aaaa", "bbb", "cc", "a"),很容易将其转换为(字符,长度)对。

使用正则表达式并不总是一个好的解决方案,尤其是当它像这个一样复杂且不直观时。所以这在生产代码中可能不是一个好的选择。另一方面,使用 fold()reduce() 的解决方案可能也不会更容易阅读。事实上,最可维护的解决方案可能是循环字符的老式解决方案……

我相信没有好的(有效的、可读的)惯用方法来解决这个问题。我们可以使用一种很好的、​​陈旧的、无聊的循环方法。为了让它至少更有趣一点,我们可以使用协程进行惰性计算:

fun String.countConsecutive() = sequence {
    if (isEmpty()) return@sequence
    val it = iterator()
    var curr = it.next()
    var count = 1
    it.forEach {
        if (curr == it) {
            count++
        } else {
            yield(curr.toString() to count)
            curr = it
            count = 1
        }
    }
    yield(curr.toString() to count)
}

如果我们的字符串很长并且我们只需要遍历连续的组,这很好。如果我们不需要遍历所有这些就更好了。