Kotlin: преобразовать большой список в подсписку заданного размера раздела

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

Ответ 1

Вот реализация функции lazy batching extension, которая возьмет коллекцию или что-нибудь, что может стать Sequence и вернуть Sequence из List каждого из этих размеров, причем последний из них имеет размер или меньше.

Пример использования для итерации списка в виде партий:

myList.asSequence().batch(5).forEach { group ->
   // receive a Sequence of size 5 (or less for final)
}

Пример для преобразования партий List в Set:

myList.asSequence().batch(5).map { it.toSet() }

См. первый тестовый пример ниже для отображения выходного данных с конкретным вводом.

Код для функции Sequence<T>.batch(groupSize):

public fun <T> Sequence<T>.batch(n: Int): Sequence<List<T>> {
    return BatchingSequence(this, n)
}

private class BatchingSequence<T>(val source: Sequence<T>, val batchSize: Int) : Sequence<List<T>> {
    override fun iterator(): Iterator<List<T>> = object : AbstractIterator<List<T>>() {
        val iterate = if (batchSize > 0) source.iterator() else emptyList<T>().iterator()
        override fun computeNext() {
            if (iterate.hasNext()) setNext(iterate.asSequence().take(batchSize).toList())
            else done() 
        }
    }
}

Единичные тесты, доказывающие, что они работают:

class TestGroupingStream {

    @Test fun testConvertToListOfGroupsWithoutConsumingGroup() {
        val listOfGroups = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10).asSequence().batch(2).toList()
        assertEquals(5, listOfGroups.size)
        assertEquals(listOf(1,2), listOfGroups[0].toList())
        assertEquals(listOf(3,4), listOfGroups[1].toList())
        assertEquals(listOf(5,6), listOfGroups[2].toList())
        assertEquals(listOf(7,8), listOfGroups[3].toList())
        assertEquals(listOf(9,10), listOfGroups[4].toList())
    }

    @Test fun testSpecificCase() {
        val originalStream = listOf(1,2,3,4,5,6,7,8,9,10)

        val results = originalStream.asSequence().batch(3).map { group ->
            group.toList()
        }.toList()

        assertEquals(listOf(1,2,3), results[0])
        assertEquals(listOf(4,5,6), results[1])
        assertEquals(listOf(7,8,9), results[2])
        assertEquals(listOf(10), results[3])
    }


    fun testStream(testList: List<Int>, batchSize: Int, expectedGroups: Int) {
        var groupSeenCount = 0
        var itemsSeen = ArrayList<Int>()

        testList.asSequence().batch(batchSize).forEach { groupStream ->
            groupSeenCount++
            groupStream.forEach { item ->
                itemsSeen.add(item)
            }
        }

        assertEquals(testList, itemsSeen)
        assertEquals(groupSeenCount, expectedGroups)
    }

    @Test fun groupsOfExactSize() {
        testStream(listOf(1,2,3,4,5,6,7,8,9,10,11,12,13,14,15), 5, 3)
    }

    @Test fun groupsOfOddSize() {
        testStream(listOf(1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18), 5, 4)
        testStream(listOf(1,2,3,4), 3, 2)
    }

    @Test fun groupsOfLessThanBatchSize() {
        testStream(listOf(1,2,3), 5, 1)
        testStream(listOf(1), 5, 1)
    }

    @Test fun groupsOfSize1() {
        testStream(listOf(1,2,3), 1, 3)
    }

    @Test fun groupsOfSize0() {
        val testList = listOf(1,2,3)

        val groupCountZero =   testList.asSequence().batch(0).toList().size
        assertEquals(0, groupCountZero)

        val groupCountNeg =  testList.asSequence().batch(-1).toList().size
        assertEquals(0, groupCountNeg)

    }

    @Test fun emptySource() {
        listOf<Int>().asSequence().batch(1).forEach { groupStream ->
            fail()
        }

    }
}

Ответ 2

С помощью Kotlin 1.2-M1, в соответствии с вашими потребностями, вы можете выбрать один из следующих способов решения своей проблемы.


# 1. Используя chunked(size: Int)

fun main(args: Array<String>) {
    val list = listOf(2, 4, 3, 10, 8, 7)
    val newList = list.chunked(2)
    //val newList = list.chunked(size = 2) // also works
    print(newList)
}

/*
prints:
[[2, 4], [3, 10], [8, 7], [9]]
*/

# 2. Используя windowed(size: Int, step: Int)

fun main(args: Array<String>) {
    val list = listOf(2, 4, 3, 10, 8, 7, 9)
    val newList = list.windowed(2, 2)
    //val newList = list.windowed(size = 2, step = 2) // also works
    println(newList)
}

/*
prints:
[[2, 4], [3, 10], [8, 7], [9]]
*/

Ответ 3

Я не вижу одного в kotlin-stdlib. Я рекомендую использовать Lists.partition(List, int) из google-guava (он использует java.util.List.subList(int, int)):

Если вы не знакомы с Guava, см. CollectionUtilitiesExplained · google/guava Wiki для более подробной информации.

Вы можете создать свою собственную функцию расширения , если хотите:

fun <T> List<T>.collate(size: Int): List<List<T>> = Lists.partition(this, size)

Если вы хотите использовать функцию расширения для изменяемых списков, то в отдельном файле Kotlin (чтобы избежать столкновений с объявлением платформы):

fun <T> MutableList<T>.collate(size: Int): List<MutableList<T>> = Lists.partition(this, size)

Если вы хотите что-то ленивое загрузиться, как в ответ Джейсон Минард, вы можете использовать Iterables.partition( Iterable, int). Вы также можете быть заинтересованы в Iterables.paddedPartition(Iterable, int), если вы хотите поместить последний подсписчик, если он меньше указанного size. Они возвращают Iterable<List<T>> (я не вижу особого смысла сделать это Iterable<Iterable<T>> как subList возвращает эффективный вид).

Если по какой-то причине вы не хотите зависеть от Guava, вы можете легко свернуть свою собственную, используя функцию subList, которую вы упомянули

fun <T> List<T>.collate(size: Int): List<List<T>> {
    require(size > 0)
    return if (isEmpty()) {
        emptyList()
    } else {
        (0..lastIndex / size).map {
            val fromIndex = it * size
            val toIndex = Math.min(fromIndex + size, this.size)
            subList(fromIndex, toIndex)
        }
    }
}

или

fun <T> List<T>.collate(size: Int): Sequence<List<T>> {
    require(size > 0)
    return if (isEmpty()) {
        emptySequence()
    } else {
        (0..lastIndex / size).asSequence().map {
            val fromIndex = it * size
            val toIndex = Math.min(fromIndex + size, this.size)
            subList(fromIndex, toIndex)
        }
    }
}

Ответ 4

Более упрощенное/функциональное решение будет

val items = (1..100).map { "foo_${it}" }

fun <T> Iterable<T>.batch(chunkSize: Int) =
   withIndex().                        // create index value pairs
   groupBy { it.index / chunkSize }.   // create grouping index
   map { it.value.map { it.value } }   // split into different partitions


items.batch(3)

Примечание 1: Лично я предпочел бы partition как имя метода здесь, но он уже присутствует в Kotlin stdlib для разделить списки на 2 части с учетом предиката.

Примечание 2: Решение итератора из Jayson может масштабироваться лучше, чем это решение для больших коллекций.

Ответ 5

К сожалению, для этого пока нет встроенной функции, и пока функциональные и Sequence -установленные реализации из других ответов выглядят хорошо, если вам просто нужно List of List s, я бы предложил написать немного уродливого, императивного, но исполнительного кода.

Это мой окончательный результат:

fun <T> List<T>.batch(chunkSize: Int): List<List<T>> {
    if (chunkSize <= 0) {
        throw IllegalArgumentException("chunkSize must be greater than 0")
    }
    val capacity = (this.size + chunkSize - 1) / chunkSize
    val list = ArrayList<ArrayList<T>>(capacity)
    for (i in 0 until this.size) {
        if (i % chunkSize == 0) {
            list.add(ArrayList(chunkSize))
        }
        list.last().add(this.get(i))
    }
    return list
}