Разделите список на группы последовательных элементов на основе условия в Котлине

#list #kotlin

Вопрос:

Я пытаюсь сгруппировать список по какому — то типу и если они расположены последовательно

 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
}
 

Теперь это развлечение займет

 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")
)
 

и вернуться

 [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)]
 

Это работает так, как и ожидалось. Поскольку я пытаюсь изучить Котлин и знаю, что есть прекрасный способ сделать это, я хотел бы знать, как я могу упростить эту логику способом котлина.

Комментарии:

1. Используйте хэш-карту с типом в качестве ключа и ArrayList<Item> в качестве значения.

2. Могу я спросить, каков ваш вариант использования для этого? Обычно мы использовали groupBy бы или аналогичные утилиты, но это не сохранит информацию о «последовательности» в результате. Поэтому мне интересно, зачем вам нужна эта структура данных в качестве вывода.

3. На самом деле элементы сортируются по времени, и мне нужно сохранить последовательность

4. Можно было бы использовать другой подход fold() . (Извините, сейчас нет времени что-то писать; вероятно, в предыдущих вопросах есть примеры. Или это было бы поучительным упражнением 🙂

Ответ №1:

Это оригинальный ответ с оригинальным решением. Для лучшей реализации (imo) перейдите к РЕДАКТИРОВАНИЮ

Для функций группировки по разным языкам выбраны разные подходы.

Некоторые языки, такие как Kotlin, используют подход реализации groupBy , используя унарную функцию (T) -> U и возвращая словарь, который сопоставляет каждый U со списком T s, сопоставленных с ним. Другие языки, такие как Haskell, работают с (T, T) -> Boolean предикатами, которые группируют последовательные элементы, удовлетворяющие предикату.

Никакая функциональность в Kotlin не поддерживает удобно такую операцию, которую вы хотите использовать. Из-за этого вам придется реализовывать свои собственные. Немного более короткий код, чем у вас, был бы:

 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
}
 

и назови это так:

 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)
}
 

что дало бы:

 [[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)]]
 

Мы используем метод расширения na для Iterable любого T здесь. Это идиоматический способ внедрения такой функциональности в Kotlin, потому что это операция, которую выполнит любой Iterable . Я также сделал его универсальным ( T ) и принял все predicate , что будет проверено с помощью последовательных элементов.


ПРАВКА: На самом деле, существует функциональность, которая накапливает каждый отдельный элемент один за другим в определенной структуре. Это иногда называют накоплением, уменьшением или, в Котлине, 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
        }
 

Это единственное выражение, которое, возможно, даже более идиоматично, чем предыдущее. Он не использует необработанные циклы и не содержит состояния между частями кода. Это также очень просто — либо Iterable рассматриваемый объект пуст, и в этом случае мы возвращаем пустой список, либо, в случае наличия элементов, мы складываем их в список групп ( List групп List ).

Обратите drop(1) внимание — мы делаем это, потому что мы fold все элементы в конечном списке, который уже содержит этот элемент (по конструкции в fold() вызове). Поступая таким образом, мы избавляем себя от введения дополнительных проверок на пустоту списка.

Ответ №2:

Вот краткий и эффективный способ сделать это с помощью императивного кода. Я не могу придумать чисто функциональное решение из стандартной библиотеки, которое было бы столь же эффективным. На мой взгляд, использование fold , а затем внутреннее управление несколькими списками и обработка первого элемента как особого случая не так удобочитаемы и не так эффективны.

 fun <T, C> Iterable<T>.chunkedByConsecutive(comparisonSelector: (T) -> C): List<List<T>> {
    if (none()) return emptyList()
    val lists = mutableListOf<MutableList<T>>()
    var previous: C? = null
    for (item in this) {
        if (previous != comparisonSelector(item).also { previous = it } || lists.isEmpty()) {
            lists  = mutableListOf(item)
        } else {
            lists.last()  = item
        }
    }
    return lists
}
 

Ответ №3:

Вы должны использовать раздел.

 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

Комментарии:

1. Это просто для разделения на две части.