1. Introduction

In this tutorial, we’re going to explore how to split a list into sub-lists of the same size. For example, given a list of characters List(‘b’, ‘a’, ‘e’, ‘l’, ‘d’, ‘u’, ‘n’, ‘g’), and a destination size of 3, we’d obtain the list List(List(‘b’, ‘a’, ‘e’), List(‘l’, ‘d’, ‘u’), List(‘n’, ‘g’)).

2. Description of the Solution

The input to this algorithm has two inputs:

  • The original list that we want to split into sub-lists
  • The size of the sub-lists

The original list will be split into lists, each of them being of the size specified in the second parameter. Attention must be paid to the following points:

  1. If the original list is empty, the resulting list will have only one element: an empty list
  2. The last element of the resulting list might have fewer elements than the desired slice size
  3. The minimum slice size is one, as it doesn’t make sense to have slices of size zero

3. Recursive Approach

We can easily implement a recursive method to do the split:

@throws[IllegalArgumentException]("if the slice size is less than one")
private def recursiveSplit[A](list: List[A], n: Int): List[List[A]] =
  if (n < 1)
    throw new IllegalArgumentException("The minimum slice size is one")
  else {
    if (list.isEmpty) List.empty[List[A]]
    else list.take(n) :: recursiveSplit(list.drop(n), n)
  }

Note that the method takes a type parameter, because we want it to work on lists with elements of any type. The method is a simple recursive function.

  1. The first thing to do is to validate the parameter that could make it fail: the size of the produced slices.
  2. The next thing is the exit condition (as is usual in recursive functions). For us, that happens when a empty list is passed as a parameter. What is there to split? Nothing!
  3. Otherwise, we have a non empty list and a correct slice size. All we have to do is take the first available slice from the list, and use it as the first element of a new list. The rest of the elements of that list come from the recursive application of the method.

Let’s also add a wrapper method to cater to the expected Try monad:

def split[A](list: List[A], n: Int): Try[List[List[A]]] =
  Try(recursiveSplit(list, n))

4. Using the Collections Library

Even more efficient is to use the native collections library that Scala provides out of the box. For example, in our case, we can traverse the list using the sliding() method, then transform the returned iterator to a list:

@throws[IllegalArgumentException]("if the slice size is less than one")
private def splitUsingSliding[A](list: List[A], n: Int): List[List[A]] =
  if (n < 1)
    throw new IllegalArgumentException("The minimum slice size is one")
  else list.sliding(n, n).toList

5. Testing the Splitting

Lets test the considerations expressed in section 2. If we split a list of, say, 105 elements, into lists of 10, we should get 11 lists:

"ListSplitter" should {
  "return a list of the right number of lists" in {
    val originalList = (0 until 105).toList
    val splitListTry = ListSplitter.split(originalList, 10)
    assert(splitListTry.isSuccess)
    val splitList = splitListTry.get
    assertResult(11)(splitList.size)
  }
...

If we split our list of 105 elements into lists of size 10, and then reassemble them, we should get a new list of 105 elements:

...
"return a list of lists with the right cumulative number of elements" in {
  val originalList = (0 until 105).toList
  val splitListTry = ListSplitter.split(originalList, 10)
  assert(splitListTry.isSuccess)
  val splitList = splitListTry.get
  val count = splitList.foldLeft(0)((n, bs) => n + bs.size)
  assertResult(105)(count)
}
...

Let’s try with a list of chars. This is the original example used in the introduction:

...
"return a list of lists with the right elements and the right order" in {
  val baeldung = List('b', 'a', 'e', 'l', 'd', 'u', 'n', 'g')
  val splitBaeldungTry = ListSplitter.split(baeldung, 3)
  assert(splitBaeldungTry.isSuccess)
  val splitBaeldung = splitBaeldungTry.get
  val expected =
  List(List('b', 'a', 'e'), List('l', 'd', 'u'), List('n', 'g'))
  assertResult(expected)(splitBaeldung)
}
...

6. Conclusion

In terms of efficiency and development time, using the available Scala libraries is recommended. However, sometimes exploring ways to implement solutions to specific small problems is a good way to hone our programming skills. As usual, the full source code for this article is available on GitHub.

Comments are open for 30 days after publishing a post. For any issues past this date, use the Contact form on the site.