How can I group a list of lists by a given index in elisp? The lists represent tables, like org-tables, so each sublist represents a row, eg.
| a | 1 | 0 |
| b | 1 | 1 |
| c | 0 | 0 |
would be '((a 1 0) (b 1 1) (c 0 0))
.
I want to be able to then group a given column by another column. So, for example, grouping the first column by the third, I would expect '((0 a c) (1 b))
since the third column of the first and third rows is 0.
I have tried the following code, but it make so many loops. Is there a grouping function in elisp or a better way?
;; group column1 by column2 in table
(defun group-by (col1 col2 table)
(let ((vals (cl-remove-duplicates ;find unique values to group by
(cl-loop for row in table
collect (nth col2 row)))))
(cl-loop for val in vals ;for each unique value
collect (cons val (cl-loop for row in table ;check each row for match
when (eq val (nth col2 row))
collect (nth col1 row))))))
(defvar tst-data '((a 1 0) (b 1 1) (c 0 0)))
(group-by 0 2 tst-data)
;; ((1 b)
;; (0 a c))
There happens to be a function on the ElispCookbook that can group elements of a list, called group-by-eq
. This function in-turn takes a function, f
, that will be applied to each element ("row") of the list, and then group the list ("rows") by that value.
Using that function, we can write a group by column function by passing it a function calling nth
:
(defun group-by-col (n table)
(group-by-eq (lambda (row) (nth n row)) table))
And your question asks for a double-grouping, but let's just group once:
(let ((test-data '((a 1 0)
(b 1 1)
(c 0 0))))
(mapcar 'cdr (group-by-col 2 test-data)))
;; (((c 0 0) (a 1 0)) ((b 1 1)))
Well, I thought there was a second grouping, but then it looks like you want to just select the nth element of each group, in this case the first element:
(let ((test-data '((a 1 0)
(b 1 1)
(c 0 0))))
(mapcar
(lambda (grp)
(cons (car grp) (mapcar (lambda (lst) (nth 0 lst)) (cdr grp))))
(group-by-col 2 test-data)))
;; ((0 c a) (1 b))