Generic Top

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE

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. 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.

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

Generic bottom

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE

Leave a Reply

avatar
  Subscribe  
Notify of