0

Working in an org-mode file, I have this table

#+tblname: site-charac-matrix
|   | glaze | orn | color | thin |
|---+-------+-----+-------+------|
| A |     0 |   1 |     0 |    0 |
| B |     1 |   0 |     0 |    0 |
| C |     1 |   0 |     1 |    0 |
| D |     1 |   1 |     1 |    1 |
| E |     0 |   1 |     1 |    1 |

which represents archaeological dig sites (one per row) for pottery, where four characteristics of the pottery are noted, 1 or 0 for yes or no for that characteristic of the pottery. Here is the "internal" list for the table

#+begin_src emacs-lisp :var site-matrix=site-charac-matrix :results raw
site-matrix
#+end_src

#+RESULTS:
((A 0 1 0 0) (B 1 0 0 0) (C 1 0 1 0) (D 1 1 1 1) (E 0 1 1 1))

I created this function to input a site and a characteristic argument and get back a yes or no statement

#+begin_src emacs-lisp :var site-matrix=site-charac-matrix
(defun site-charac-4 (site charac)
  (cond  ((= site 1) (format "At site %d the pottery %s have the %s characteristic." 
                 site (if (= 1 (nth (charac-to-index charac) (nth 0 site-matrix))) "does" "does not") charac)) 
     ((= site 2) (format "At site %d the pottery %s have the %s characteristic."
                 site (if (= 1 (nth (charac-to-index charac) (nth 1 site-matrix))) "does" "does not") charac))
     ((= site 3) (format "At site %d the pottery %s have the %s characteristic."
                 site (if (= 1 (nth (charac-to-index charac) (nth 2 site-matrix))) "does" "does not") charac))
     ((= site 4) (format "At site %d the pottery %s have the %s characteristic."
                 site (if (= 1 (nth (charac-to-index charac) (nth 3 site-matrix))) "does" "does not") charac))
     ((= site 5) (format "At site %d the pottery %s have the %s characteristic."
                 site (if (= 1 (nth (charac-to-index charac) (nth 4 site-matrix))) "does" "does not") charac))
     (t nil)))
#+end_src

As you see, I have a cond to sort for the incoming site number, but then I need another conditional, an if embedded in each cond to get the yes or no of the characteristic from the site's row list. In addition, to translate the characteristics, I have this function

#+begin_src emacs-lisp
(defun charac-to-index (charac)
  (pcase charac
    ('glaze 1)
    ('orn 2)
    ('color 3)
    ('thin 4)))
#+end_src

which gives nth an index for accessing the correct place in the site row lists. This all works

#+begin_src emacs-lisp :var site-matrix=site-charac-matrix
(site-charac-4 5 'color)
#+end_src

#+RESULTS:
At site 5 the pottery does have the color characteristic.

My Lisp beginner's question is, could some of this be consolidated, e.g., the charac-to-indexinto the main function? Also, how do you have nested conditionals? I can't help but think my use of if embedded in the format of each cond case is kludgy. Any insights appreciated.

2

I think indexing org tables by column and row names is such a general task that it deserves a general function org+-table-get.

You could put the following code into your initialization file.

(defsubst org+-table-get-row (table row-id &optional start noerror)
  "Get Org TABLE row by ROW-ID.
Search for ROW-ID from START.
START is zero-based. It defaults to 1.
That way the header row is skipped.
Don't panic in the case of out-of-range access if NOERROR is nil."
  (unless (numberp start)
    (setq start 1))
  (unless (or
       noerror
       (>= start 0)
       (< start (length table)))
    (user-error "Start row index %s out of range [0,%s)" start (length table)))
  (when (< start 0)
    (setq start 0))
  (when (symbolp row-id)
    (setq row-id (symbol-name row-id)))
  (let ((ret (assoc row-id (nthcdr start table))))
    (unless (or noerror ret)
      (user-error "Row %s not found" row-id))
    ret))

(defun org+-table-get (table row-id col-id &optional start-row start-col noerror value)
  "Get Org TABLE element content by ROW-ID and COL-ID.
Search rows from START-ROW and cols from START-COL.
START-ROW and START-COL are zero-based and default to 1.
That way the header row and the column with the row names are skipped.
Don't panic in the case of out-of-range access if NOERROR is nil.
If VALUE is non-nil then assign VALUE to the table cell."
  (when (symbolp col-id)
    (setq col-id (symbol-name col-id)))
  (unless (numberp start-col)
    (setq start-col 1))
  (let* ((col-nr (+ start-col (cl-position col-id (nthcdr start-col (car table)) :test #'equal)))
     (row (if col-nr
          (org+-table-get-row table row-id start-row noerror)
        (unless noerror
          (user-error "Column %s not found" col-id)))))
    (and row
     (if value
         (setf (nth col-nr row) value)
       (nth col-nr row)))))

(defun org+-table-setter (&rest args)
  ""
  (let ((val (car (last args)))
     (n (length args)))
     (setq args (butlast args))
     (when (< n 7)
       (setq args (append args
              (make-list (- 7 n) nil))))
     (unless val
       (setq val ""))
     (setq args (nconc args (list val)))
     (apply #'org+-table-get args)))

(gv-define-simple-setter
 org+-table-get
 org+-table-setter)

(defun org+-table-add-header-hline (table)
  "Add hline below header row by modifying TABLE.
Return the modified TABLE."
  (setcdr table (cons 'hline (cdr table)))
  table)

With those definitions the code in the following Org file would work:

#+tblname: site-charac-matrix
|   | glaze | orn | color | thin |
|---+-------+-----+-------+------|
| A |     0 |   1 |     0 |    0 |
| B |     1 |   0 |     0 |    0 |
| C |     1 |   0 |     1 |    0 |
| D |     1 |   1 |     1 |    1 |
| E |     0 |   1 |     1 |    1 |

#+BEGIN_SRC emacs-lisp :var matrix=site-charac-matrix site="E" char='color :colnames no
(format "At site %s the pottery does%s have the %s characteristic.\n"
    site
    (if (eq (org+-table-get matrix site char) 1)
        ""
      " not")
    char)
#+END_SRC

#+RESULTS:
: At site D the pottery does have the color characteristic.

Also setting table cells is possible:
#+BEGIN_SRC emacs-lisp :var matrix=site-charac-matrix site="E" char='color val=2 :colnames no
(setf (org+-table-get matrix site char) val)
(org+-table-add-header-hline matrix)
#+END_SRC

#+RESULTS:
|   | glaze | orn | color | thin |
|---+-------+-----+-------+------|
| A |     0 |   1 |     0 |    0 |
| B |     1 |   0 |     0 |    0 |
| C |     1 |   0 |     1 |    0 |
| D |     1 |   1 |     1 |    1 |
| E |     0 |   1 |     2 |    1 |
| improve this answer | |
1

There are lots of different ways to write code that's similar but a bit different. So the question might be closed as being a bit opinion-based.

Anyway, here's one rewrite, which factors some of the stuff. I don't claim it is in any particular way better than what you wrote.

(setq site-matrix  '((A 0 1 0 0) (B 1 0 0 0) (C 1 0 1 0) (D 1 1 1 1) (E 0 1 1 1)))

(defun site-charac-4 (site charac)
  (let* ((ii  (1- site))
         (jj  (nth (cl-case charac
                     (glaze 1)
                     (orn   2)
                     (color 3)
                     (thin  4))
                   (nth ii site-matrix)))
         (dn  (if (= 1 jj) "" " not")))
    (and (member site '(1 2 3 4 5))
         (format "At site %d the pottery does%s have the %s characteristic." site dn charac))))

;; (site-charac-4 5 'color)
;; => "At site 5 the pottery does have the color characteristic."
| improve this answer | |
  • What you and the others have offered is exactly what I, a beginner, need. (I'm teaching this stuff to a local YMCA STEM club, BTW.) So maybe examples shouldn't be condemned as "opinions?" That would be assigning motives and intent to honest suggestions. – 147pm Feb 22 at 17:42
1

I might be missing the point of the question, but I think the main reason this looks messy is that the cond statement has 5 branches. A nested conditional on its own doesn't look that bad to me, so noticing that the only thing changing in the main cond branch is the argument to nth, I would rewrite the function as

(defun site-charac-4 (site charac)
  (if (and (< 0 site) (> 6 site))
      (format "At site %d the pottery %s have the %s characteristic." 
          site
          (if (= 1 (nth (charac-to-index charac)
                (nth (- site 1) site-matrix)))
              "does"
            "does not")
          charac)))
| improve this answer | |
1

This is an X-Y-problem.

Just avoid the multi-branched conditionals if you actually do not need them. I show you one way to do that with the code below.

The most important details of the code:

  • If you use the :colnames no header argument you get the characteristics in the first line of the matrix.
  • You can get the column number of the given characteristic with the column names with cl-position.
  • You can select the site with its site name via assoc-string.
  • You can process multiple characteristics at once for one site with cl-loop.
#+tblname: site-charac-matrix
|   | glaze | orn | color | thin |
|---+-------+-----+-------+------|
| A |     0 |   1 |     0 |    0 |
| B |     1 |   0 |     0 |    0 |
| C |     1 |   0 |     1 |    0 |
| D |     1 |   1 |     1 |    1 |
| E |     0 |   1 |     1 |    1 |

#+BEGIN_SRC emacs-lisp :results none
(defun site-char (matrix site &rest characteristics)
  "Report whether SITE has CHARACTERISTICS in MATRIX.
MATRIX is a cons (COLSROW . DATA).
COLSROW is a a cons (\"\" . PROPS) where
the list PROPS contains the property names.
DATA contains the data rows.
Each data row is a cons (NAME . FLAGS).
NAME contains the name of the site as string or symbol.
FLAGS is a list of flags with the same length as PROPS.
SITE is one of the site names in MATRIX.
CHARACTERISTICS is a list with names from PROPS.
SITE's flags are those associated with SITE in DATA.
SITE can also be the site's row index."
  (when (numberp site) ;; unify treatment of sites
    (unless (and (> site 0)
         (<= site (length (car matrix))))
      (user-error "Index out of range"))
    (setq site (car (nth site matrix))))
  (let (col row)
    (setq row (assoc-string site matrix))
    (unless row
      (user-error "Site %s unknown." site))
    (cl-loop for char in characteristics
         if (setq col (cl-position char (car matrix) :test #'string-equal))
         collect (format "At site %s the pottery does%s have the %s characteristic.\n"
                 site
                 (if (eq (nth col row) 1)
                 ""
                   " not")
                 char)
         else
         do (user-error "Characteristic %s unknown" char))))
#+END_SRC

#+BEGIN_SRC emacs-lisp :var matrix=site-charac-matrix site="E" char='color :colnames no
(site-char matrix site char)
#+END_SRC

#+RESULTS:
| At site E the pottery does have the color characteristic. |

| improve this answer | |
  • Does it not seem counterintuitive that you have to say :colnames no to get the column names? – NickD Feb 21 at 15:01
  • 1
    I don't think this is an "X-Y Problem" per se. Yes, I have a working solution, but I'm merely asking for a better way to do it from all the more experienced people out there. – 147pm Feb 21 at 15:52
  • @NickD Yes that is one of those rare features where I have to consult the Org manual every time I use it. I think that is almost "counterintuitive by definition". – Tobias Feb 21 at 20:21
  • But do you think that the manual accurately describes the behavior? I don't really understand what it says, but it feels like a bug to me. I guess I'll ask on the ML. – NickD Feb 21 at 23:21
  • @NickD Yes the manual describes the right behavior. :colnames switches off the special treatment of column names and therefore you get them. That is just confusing. That's for sure! – Tobias Feb 22 at 0:15

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.