Split a list into groups of consecutive elements based on a condition in Kotlin

北慕城南 提交于 2021-02-05 09:17:47

问题


I am trying to group a list based on some type and if they are in sequence

data class Item(val type: Int, val name: String)

private fun splitItems(items: List<Item>): List<List<Item>> {
    val groupedItems = mutableListOf<List<Item>>()
    var tempList = mutableListOf<Item>()
    items.mapIndexed { index, item ->
        if (index > 0) {
            val previousItem = items[index - 1]
            if (previousItem.type == item.type) {
                tempList.add(item)
            } else {
                if (tempList.isNotEmpty()) groupedItems.add(tempList)
                tempList = mutableListOf()
                tempList.add(item)
            }
        } else tempList.add(item)
    }
    if (tempList.isNotEmpty()) groupedItems.add(tempList)
    return groupedItems
}

Now this fun will take

val items = mutableListOf(
    Item(1, "Shirt"),
    Item(1, "Shirt"),
    Item(2, "Pant"),
    Item(2, "Pant"),
    Item(2, "Pant"),
    Item(1, "Shirt"),
    Item(1, "Shirt"),
    Item(3, "Tee"),
    Item(3, "Tee"),
    Item(2, "Pant"),
    Item(2, "Pant"),
    Item(1, "Shirt"),
    Item(1, "Shirt"),
    Item(1, "Shirt")
)

and return

[Item(type=1, name=Shirt), Item(type=1, name=Shirt)]
[Item(type=2, name=Pant), Item(type=2, name=Pant), Item(type=2, name=Pant)]
[Item(type=1, name=Shirt), Item(type=1, name=Shirt)]
[Item(type=3, name=Tee), Item(type=3, name=Tee)]
[Item(type=2, name=Pant), Item(type=2, name=Pant)]
[Item(type=1, name=Shirt), Item(type=1, name=Shirt), Item(type=1, name=Shirt)]

This is working as expected. Since I am trying to learn Kotlin and I know there is a beautiful way of doing this, I would like to know how I can simplify this logic in kotlin way.


回答1:


This is the original answer with the original solution. For a better (imo) implementation, scroll to the EDIT

Different approaches are chosen for the grouping functionality by different languages.

Some languages, like Kotlin, take the approach of implementing groupBy by taking a unary function (T) -> U and returning a dictionary that maps every U to a list of Ts that mapped to it. Other languages, like Haskell, work with (T, T) -> Boolean predicates that group consecutive elements that satisfy the predicate.

No functionality in Kotlin supports conveniently such operation that you desire to use. Because of that, you have to implement your own. A slightly shorter code than yours would be:

fun <T> Iterable<T>.groupConsecutiveBy(predicate: (T, T) -> Boolean): List<List<T>> {
    var leftToGroup = this.toList()
    val groups = mutableListOf<List<T>>()

    while (leftToGroup.isNotEmpty()) {
        val firstItem = leftToGroup[0]
        val newGroup = leftToGroup.takeWhile { predicate(it, firstItem) }
        groups.add(newGroup)
        leftToGroup = leftToGroup.subList(newGroup.size, leftToGroup.size)
    }

    return groups
}

and call it like:

fun main() {
    val items = mutableListOf(
        Item(1, "Shirt"),
        Item(1, "Shirt"),
        Item(2, "Pant"),
        Item(2, "Pant"),
        Item(2, "Pant"),
        Item(1, "Shirt"),
        Item(1, "Shirt"),
        Item(3, "Tee"),
        Item(3, "Tee"),
        Item(2, "Pant"),
        Item(2, "Pant"),
        Item(1, "Shirt"),
        Item(1, "Shirt"),
        Item(1, "Shirt")
    )
    
    items.groupConsecutiveBy{ left, right -> left.type == right.type }.also(::print)
}

which would yield:

[[Item(type=1, name=Shirt), Item(type=1, name=Shirt)], [Item(type=2, name=Pant), Item(type=2, name=Pant), Item(type=2, name=Pant)], [Item(type=1, name=Shirt), Item(type=1, name=Shirt)], [Item(type=3, name=Tee), Item(type=3, name=Tee)], [Item(type=2, name=Pant), Item(type=2, name=Pant)], [Item(type=1, name=Shirt), Item(type=1, name=Shirt), Item(type=1, name=Shirt)]]

We are using na extension method for an Iterable of any T here. This is the idiomatic way of introducing such functionality in Kotlin, because it's an operation that will be done by any Iterable. I also made it generic (T) and to accept any predicate that will be tested with consecutive elements.


EDIT: Actually, there is a functionality that accumulates every single element one by one to a certain structure. It's sometimes called accumulate, reduce or, in Kotlin, fold:

fun <T> Iterable<T>.groupConsecutiveBy(groupIdentifier: (T, T) -> Boolean) =
    if (!this.any())
        emptyList()
    else this
        .drop(1)
        .fold(mutableListOf(mutableListOf(this.first()))) { groups, t ->
            groups.last().apply {
                if (groupIdentifier.invoke(last(), t)) {
                    add(t)
                } else {
                    groups.add(mutableListOf(t))
                }
            }
            groups
        }

This is a single expression which is, arguably, even more idiomatic than the previous one. It uses no raw loops and holds no state in between code parts. It's also very simple - either the Iterable in question is empty and, in that case, we return an empty list, or, in case of elements present, we fold them into a list of groups (List of Lists).

Note the drop(1) - we do that because we fold all elements into the final list that already contains that element (by construction in the fold() call). By doing that we save ourselves from introducing additional checks for the emptiness of the list.




回答2:


You should use partition.

val array = intArrayOf(1, 2, 3, 4, 5)
val (even, odd) = array.partition { it % 2 == 0 }
println(even) // [2, 4]
println(odd) // [1, 3, 5]

https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/partition.html



来源:https://stackoverflow.com/questions/65355091/split-a-list-into-groups-of-consecutive-elements-based-on-a-condition-in-kotlin

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!