问题
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 T
s 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 List
s).
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