Group tables or lists of lists by index

131 Views Asked by At

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))
1

There are 1 best solutions below

1
ashawley On BEST ANSWER

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))