1. Introduction

Let’s say we have an array like [a, b, c, d, e, f] and we want to split the elements up into separate groups, like [[a, b], [c, d], [e, f]] or [[a, b, c], [d], [e,f]].

In this tutorial, we’ll achieve this while examining some differences between Kotlin’s groupBy, chunked, and windowed.

2. Splitting a List Into a List of Pairs

For our examples, we’ll use two lists – one with an even number of elements and one with an odd number of elements:

val evenList = listOf(0, "a", 1, "b", 2, "c");
val unevenList = listOf(0, "a", 1, "b", 2, "c", 3);

Clearly, we can divide our evenList into exactly three pairs. However, our unevenList will have one extra element.

In the remainder of this section, we’ll see various implementations for splitting our two lists, including how they deal with the extra element in unevenList.

2.1. Using groupBy

First, let’s implement a solution with groupBy. We’ll create a list with ascending numbers and use groupBy to split them:

val numberList = listOf(1, 2, 3, 4, 5, 6);
numberList.groupBy { (it + 1) / 2 }.values

This gives the desired result:

[[1, 2], [3, 4], [5, 6]]

How does it work? Well, groupBy executes the supplied function (it + 1) / 2 on every element:

  • (1 + 1) / 2 = 1
  • (2 + 1) / 2 = 1.5, which is rounded to 1
  • (3 + 1) / 2 = 2
  • (4 + 1) / 2 = 2.5, which is rounded to 2
  • (5 + 1) / 2 = 3
  • (6 + 1) / 2 = 3.5, which is rounded to 3

Then, groupBy groups the elements in the list that gave the same result.

Now, when we do the same with an uneven list:

val numberList = listOf(1, 2, 3, 4, 5, 6, 7);
numberList.groupBy { (it + 1) / 2 }.values

We get all the pairs and one extra element:

[[1, 2], [3, 4], [5, 6], [7]]

But, if we go a bit further with some random numbers:

val numberList = listOf(1, 3, 8, 20, 23, 30);
numberList.groupBy { (it + 1) / 2 }.values

We’ll get something that is completely undesired:

[[1], [3], [8], [20], [23], [30]]

The reason is simple; applying the (it + 1) / 2 function on every element gives: 1, 2, 4, 10, 12, 15. All the results differ, so no elements are grouped together.

When we use our evenList or unevenList, it’s even worse — the code doesn’t compile, as the function cannot be applied to Strings.

2.2. Using groupBy and withIndex

Really, if we want to group an arbitrary list into pairs, we don’t want to modify the value by our function, but the index:

evenList.withIndex()
    .groupBy { it.index / 2 }
    .map { it.value.map { it.value } }

This returns the list of pairs we want:

[[0, "a"], [1, "b"], [2, "c"]]

Furthermore, if we use the unevenList, we even get our separate element:

[[0, "a"], [1, "b"], [2, "c"], [3]]

2.3. Using groupBy With foldIndexed

We can go a step further than just using index and program a bit more with foldIndexed to save some allocations:

evenList.foldIndexed(ArrayList<ArrayList<Any>>(evenList.size / 2)) { index, acc, item ->
    if (index % 2 == 0) {
        acc.add(ArrayList(2))
    }
    acc.last().add(item)
    acc
}

While a bit more verbose, the foldIndexed solution simply performs the operation on each element, whereas the withIndex function first creates an iterator and wraps each element.

2.4. Using chunked

But, we can do this more elegantly with chunked. So, let’s apply the method to our evenList:

evenList.chunked(2)

The evenList provides us with the pairs we want:

[[0, "a"], [1, "b"], [2, "c"]]

While the unevenList gives us the pairs and the extra element:

[[0, "a"], [1, "b"], [2, "c"], [3]]

2.5. Using windowed

And chunked works really well, but sometimes we need a bit more control.

For instance, we may need to specify if we want only pairs, or if we want to include the extra element. The windowed method provides us with a partialWindows Boolean, which indicates if we want the partial result or not.

By default, partialWindows is false. So, the following statements produce the same result:

evenList.windowed(2, 2)
unevenList.windowed(2, 2, false)

Both return the list without the separate element:

[[0, "a"], [1, "b"], [2, "c"]]

Finally, when we set partialWindows to true to include the partial result:

unevenList.windowed(2, 2, true)

We’ll get the list of pairs plus the separate element:

[[0, "a"], [1, "b"], [2, "c"], [3]]

3. A Few Words About the partition() Function

We’ve solved our problem using groupBy(), chunked(), and windowed(). partition() is another function that can split a list into parts. As such, it can be pretty convenient for solving some similar problems.

Kotlin’s partition() function can split a list into a pair of two lists by a given predicate function.

An example can clarify this quickly. Let’s say we have a list of unsorted integers and we’d like to split it into two lists – one containing numbers less than 42, and the other holding numbers greater than or equal to 42.

If we solve this problem using partition(), it’s pretty easy:

val numbers = listOf(42, 1984, 1, 0, -4, 23, 100, 6, 8)
val aPairOfList = numbers.partition { it < 42 }
with(aPairOfList) {
    assertEquals(listOf(1, 0, -4, 23, 6, 8), first)
    assertEquals(listOf(42, 1984, 100), second)
}

As the example above shows, we provided it < 42 as the predicate function. In the resulting pair, the first list carries all elements for which the predicate function yielded true, and the second list holds elements for which the predicate function returns false.

Sometimes, we want to use the split lists as variables directly instead of using them like thePair.first and thePair.second. In this case, **we can use a deconstructing declaration to assign partition()‘s result to two list variables in one shot:

val (lessThan42, greaterThanOrEq42) = numbers.partition { it < 42 }

assertEquals(listOf(1, 0, -4, 23, 6, 8), lessThan42)
assertEquals(listOf(42, 1984, 100), greaterThanOrEq42)

4. Conclusion

Using groupBy is a nice programming exercise, but it can be quite error-prone. Some of the errors can be resolved simply by using an index.

To optimize the code, we can even use foldIndexed. However, this results in even more code. Luckily, the chunked method offers us the same functionality out of the box.

Moreover, the windowed method provides additional configuration options. If possible, it’s best to use the chunked method, and if we need additional configuration, we should use the windowed method.

Finally, we talked about the partition() function through examples. It allows us easily to split a list into two based on a given predicate function.

As usual, the full source code is available over on GitHub.