RSS Feed

Lisp Project of the Day

group-by

You can support this project by donating at:

Donate using PatreonDonate using Liberapay

group-byutilsdata-structuresalgorithms

Documentation🤨
Docstrings😀
Tests 🥺
Examples😀
RepositoryActivity🥺
CI 🥺

This small utility library implements a really handy facility. It allows you to group items by one or many keys.

Here is how it works. In the next example we'll group names by their first letter:

POFTHEDAY> (group-by:group-by
            '("Alice"
              "Bob"
              "Ashley"
              "Katie"
              "Brittany"
              "Jessica"
              "Daniel"
              "Josh")
            :key (lambda (name)
                   (elt name 0))
            :value #'identity)

((#\A "Alice"
      "Ashley")
 (#\B "Bob"
      "Brittany")
 (#\K "Katie")
 (#\J "Jessica"
      "Josh")
 (#\D "Daniel"))

If we are going to group by the first letter and next by the second, we need to use group-by-repeated function:

POFTHEDAY> (flet ((first-letter (name)
                    (elt name 0))
                  (second-letter (name)
                    (elt name 1)))
             (group-by:group-by-repeated
              '("Alice"
                "Bob"
                "Ashley"
                "Katie"
                "Brittany"
                "Jessica"
                "Daniel"
                "Josh")
            :keys (list #'first-letter
                       #'second-letter)))

((#\D (#\a "Daniel"))
 (#\J (#\o "Josh")
      (#\e "Jessica"))
 (#\K (#\a "Katie"))
 (#\B (#\r "Brittany")
      (#\o "Bob"))
 (#\A (#\s "Ashley")
      (#\l "Alice")))

This library also provides a way to accumulate grouped items into a special object. This could be useful when you don't have all items right away, but receiving them one by one from some source.

Here is how it can be used in the simplest case. First, we'll request names from the user and will be collecting them into a special grouped list data structure:

POFTHEDAY> (flet ((first-letter (name)
                    (elt name 0))
                  (second-letter (name)
                    (elt name 1))
                  (request-name ()
                    (format t "Enter a name: ")
                    (read)))
             (loop with accumulator = (group-by:make-grouped-list
                                       nil
                                       :keys (list #'first-letter
                                                   #'second-letter))
                   for name = (request-name)
                     then (request-name)
                   while name
                   do (group-by:add-item-to-grouping
                       name
                       accumulator)
                   finally (return accumulator)))
Enter a name: "Markus"
Enter a name: "Bob"
Enter a name: "Betty"
Enter a name: "Mery"
Enter a name: "Oleg"
Enter a name: "Marianna"
Enter a name: nil
#<GROUP-BY:GROUPED-LIST {1006D3EC43}>

POFTHEDAY> (defparameter *grouping* *)

At any time we can access the data structure to work with already collected items. Let's write a recursive function to see what we've collected so far:

POFTHEDAY> (defun print-tree (grouping &optional (depth 0))
             (let ((prefix (make-string depth :initial-element #\Space))
                   (key (group-by:key-value grouping))
                   (items (group-by:items-in-group grouping))
                   (subgroups (group-by:child-groupings grouping)))
               (when key
                 (format t "~A~A~&" prefix key)
                 (incf depth))

               (if subgroups
                   (loop for child in subgroups
                         do (print-tree child depth))
                   (loop for item in items
                         do (format t "~A - ~A~%"
                                    prefix
                                    item)))))

POFTHEDAY> (print-tree *grouping*)
O
 l
  - Oleg
B
 e
  - Betty
 o
  - Bob
M
 e
  - Mery
 a
  - Marianna
  - Markus

This library is powerful enough to have it in your toolbox. Go and group something now!


Brought to you by 40Ants under Creative Commons License