Hung-Yi's LogoHung-Yi’s Journal

Split a List Into Batches Using Emacs Lisp

An exercise to write a simple Emacs Lisp function that can be used to chop a list of things into batches of a given size, including examples of unit testing and boundary value analysis.

[2021-10-07 Thu] I made a mistake! 😅 There’s (probably) nothing wrong with the code I wrote in my original post, but there’s already a function in Emacs that does exactly what I was trying to achieve: namely seq-partition. It even takes the exact same parameters as my old function.

It turns out “partition” is a word commonly used in computer science to refer to splitting lists in various ways, like in the ubiquitous quicksort algorithm.

Here’s a quick demonstration of it in action:

(require 'seq)
(seq-partition '(1 2 3 4 5 6 7 8 9) 4)
((1 2 3 4)
 (5 6 7 8)
 (9))

Please use the built-in seq-partition and not my code 🙏

If you’d like to read through my adventures in naivety anyway, the original post follows below. It’s still a good exercise in using cl-loop and boundary value analysis.


Sometimes a list of things is too large and it needs to be split into batches before it can be processed effectively. A quick google didn’t reveal any quick copy-pastable elisp (a.k.a. Emacs Lisp) functions that did this kind of batching, so I cobbled together what I found and came up with this cl-loop implementation, which is hopefully performant.

Feel free to copy paste it as is, since it should be general enough to use anywhere .

(require 'cl-lib)

(defun my/batch-list (input size)
  "Split INPUT list into a batches (i.e. sublists) of maximum SIZE."
  (when (< size 1)
    (error "SIZE of the batches must be at least 1"))
  (unless (seqp input)
    (error "INPUT must be a sequence or list"))
  (cl-loop with tail = input
           while tail
           collect (cl-loop for ptr on tail
                            for i upfrom 0
                            while (< i size)
                            collect (car ptr)
                            finally (setf tail ptr))))

Example Usage & Unit Tests

To use it, just call it with the input list and the maximum size of the batches that it should split into.

For example, splitting a list of (1 2 3 4 5 6 7 8 9) into batches of max size 3

(my/batch-list '(1 2 3 4 5 6 7 8 9) 3)
((1 2 3)
 (4 5 6)
 (7 8 9))

And a batch size of 4 to see what happens when the size doesn’t evenly divide the length of the input list.

(my/batch-list '(1 2 3 4 5 6 7 8 9) 4)

Spoiler: the last batch is chopped short compared to the others 🪓

((1 2 3 4)
 (5 6 7 8)
 (9))

Let’s try some other tricky inputs to check for sane behavior 😈 1

Like a size that is equal to the length of the input list

(my/batch-list '(1 2 3 4 5 6 7 8 9) 9)
((1 2 3 4 5 6 7 8 9))

Or a size that exceeds the length of input

(my/batch-list '(1 2 3 4 5 6 7 8 9) 10)
((1 2 3 4 5 6 7 8 9))

How about a batch size of … just 1?

(my/batch-list '(1 2 3 4 5 6 7 8 9) 1)
((1)
 (2)
 (3)
 (4)
 (5)
 (6)
 (7)
 (8)
 (9))

Footnotes:

1

This kind of messing around is actually called Boundary Testing or Boundary-Value Analysis and is a good way to ensure your code is robust. You’ll also notice in the function that I also checked that the size is above 0, otherwise cl-loop would run forever.