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