Alternatives to long complex format statements in emacs-lisp

| categories: emacs-lisp | tags: | View Comments

At one point I had a string I wanted to fill in with a bunch of variables.

(insert (format"
 :PROPERTIES:
  :Custom_ID: %s
  :AUTHOR: %s
  :JOURNAL: %s
  :YEAR: %s
  :VOLUME: %s
  :PAGES: %s
  :DOI: %s
  :URL: %s
 :END:
[[cite:%s]] [[file:%s/%s.pdf][pdf]]\n\n"
key author journal year volume pages doi url key jorg-bib-pdf-directory key ))

I find that very difficult to use, because it is tedious to make sure all the variables are in the right order, and it is difficult to change later. In Python, you would be able to put named expansions in, e.g. {author} and then used named arguments. That does not exist as far as I know in emacs-lisp.

Below is an alternatme approach that uses concat to construct this string.

(let ((key "kitchin-2014")
      (author "Kitchin, J. R.")
      (journal "HACS")
      (year "2014")
      (volume "1")
      (pages "1--10")
      (doi "10.1.1.109/hacs.1.10")
      (url "http://hacs.org/10.1.1.109/hacs.1.10")
      (jorg-bib-pdf-directory "/home/jkitchin/pdfs"))

(concat "
 :PROPERTIES:
  :Custom_ID: " key "
  :AUTHOR: " author "
  :JOURNAL: " journal "
  :YEAR: " year "
  :VOLUME: " volume "
  :PAGES: " pages "
  :DOI: " doi "
  :URL: " url "
 :END:
[[cite:" key "]] [[file:" jorg-bib-pdf-directory "/" key ".pdf][pdf]]\n\n"))
 :PROPERTIES:
  :Custom_ID: kitchin-2014
  :AUTHOR: Kitchin, J. R.
  :JOURNAL: HACS
  :YEAR: 2014
  :VOLUME: 1
  :PAGES: 1--10
  :DOI: 10.1.1.109/hacs.1.10
  :URL: http://hacs.org/10.1.1.109/hacs.1.10
 :END:
[[cite:kitchin-2014]] [[file:/home/jkitchin/pdfs/kitchin-2014.pdf][pdf]]

That is kind of interesting. It is a little tedious to use all the quotes. It seems like there should be soemthing like named expansions. Let us write one of our own. We will use a regular expression to find {:keyword} and a plist. There is a regexp to match this, and then we can take the characters from position 1 to the second to last character as the keyword. That is not beautiful to me, but it works here. Then we just get the keyword from the plist. The keywords in a plist are symbols, and we will have strings. We have to use the intern function to convert them to symbols.

(defun expand-template (s plist)
  "expand a template containing {:keyword} with the definitions in plist"
  (replace-regexp-in-string "{\\(:[^}]+\\)}" 
                            (lambda (arg) 
                              (let ((keyword (intern (substring arg 1 -1))))
                                (format "%s" (plist-get plist keyword)))) s))

(let ((template "
 :PROPERTIES:
  :Custom_ID: {:key}
  :AUTHOR: {:author}
  :JOURNAL: {:journal}
  :YEAR: {:year}
  :VOLUME: {:volume}
  :PAGES: {:pages}
  :DOI: {:doi}
  :URL: {:url}
 :END:
[[cite:{:key}]] [[file:{:pdf-dir}/{:key}.pdf][pdf]]\n\n"))

(expand-template template
                 '(:key "kitchin-2014"
                        :author "Kitchin, J. R."
                        :journal "HACS"
                        :year 2014
                        :volume 1
                        :pages "1--10"
                        :doi "10.1.1.109/hacs.1.10"
                        :url "http://hacs.org/10.1.1.109/hacs.1.10"
                        :pdf-dir "/home/jkitchin/pdfs")))
 :PROPERTIES:
  :Custom_ID: kitchin-2014
  :AUTHOR: Kitchin, J. R.
  :JOURNAL: HACS
  :YEAR: 2014
  :VOLUME: 1
  :PAGES: 1--10
  :DOI: 10.1.1.109/hacs.1.10
  :URL: http://hacs.org/10.1.1.109/hacs.1.10
 :END:
[[cite:kitchin-2014]] [[file:/home/jkitchin/pdfs/kitchin-2014.pdf][pdf]]

That is pretty close to what I am used to from python! I am surprised there aren't other solutions for this around. I looked, and couldn't find them.

Copyright (C) 2014 by John Kitchin. See the License for information about copying.

org-mode source

Org-mode version = 8.2.5f

Read and Post Comments

Send email to a list of users

| categories: emacs, emacs-lisp | tags: | View Comments

I have a need to send a lot of emails to users in my class. I have to send each student an email containing there userid and a password assigned to them. I have a list of these, so the strategy is to create a function that will email that information to one user, and then use mapcar to apply the function to each pair in a list. First, we work out a function that will send one email to one user.

(defun send-mail (userid password)
  "send email to userid@andrew.cmu.edu containing their password"
  (interactive)
  (mail)
  (mail-to)
  (insert (format "%s@andrew.cmu.edu" userid))
  (mail-subject)
  (insert "[06-640] account information")
  (mail-text)
  (insert (format "
An account has been created on gilgamesh.cheme.cmu.edu
userid: %s
password: %s" userid password))
  (mail-send-and-exit))

(send-mail "jkitchin" "trustme99")

That worked well. I ran the block and got the email.

Now, suppose I have this data:

userid password
user1 trustme99
user2 foolme99
user3 blameme99

We can pass that to a source block as a list of lists that will look like this:

 ((user1 trustme99) (user2 foolme99) (user3 blameme99))

Then, we can use a mapcar to process each element. Here I use a dummy function with two arguments. If I substitute the function above, each of these users would get an email.

(defun fun (a b)
  (princ (format "user: %s\npassword: %s\n" a b)))

(mapcar (lambda (x) (fun (car x) (cadr x))) data)
user: user1
password: trustme99
user: user2
password: foolme99
user: user3
password: blameme99

I am not sure that is the best way to get the first and second elements in the list element. It looks funny to me, but it works fine. the alternative is not much prettier:

(defun fun (a b)
  (princ (format "user: %s\npassword: %s\n" a b)))

(mapcar (lambda (x) (fun (nth 0 x) (nth 1 x))) data)
user: user1
password: trustme99
user: user2
password: foolme99
user: user3
password: blameme99

Copyright (C) 2014 by John Kitchin. See the License for information about copying.

org-mode source

Read and Post Comments

An improved index function for emacs-lisp

| categories: emacs-lisp | tags: | View Comments

I previously worked out an index function for a list of strings in emacs-lisp, but at the end I showed it would not work for arbitrary elements of a list. Here is an exercise to improve on that. The goal is a function that looks like this:

(index 1 '("a" 2 1 "b"))

that would return 2 in this case. Last time I used string=, which is why I could not find a number in the list. This time, we will use equal (see http://www.gnu.org/software/emacs/manual/html_node/elisp/Equality-Predicates.html ) which compares components of objects for equality. That should let us find arbitrary objects in a list.

Here is our improved function:

(defun index (object list)
  "return the index of object in list"
  (let ((counter 0)
        (found nil))
    (catch 'finished
      (dolist (listelement list counter)
        (if (equal object listelement)
            (progn
              (setq found t)
              (throw 'finished counter))
          ;; else increment counter
          (incf counter)))
    ;; if we found it return counter otherwise return nil
    (if found counter nil))))

Now, let us test some examples:

(index 1 '("a" 2 1 "b"))
2

No problem finding a number in a list.

(index "b" '("a" 2 1 "b"))
3

How about something more complicated, like a list in a list?

(index '(1 2) '("a" 2 1 (1 2) "b"))
3

That looks good.

(princ (index '(1 2) '("a" 2 1 (2 (1 2)) "b")))
nil

Note, we do not find the nested object. That is ok, the location of that object would require two indices, which this function is not designed for.

Here we consider an object of an a-list

(index '("nut" . "acorn") '(("nut" . "acorn") ("fruit" . "apple")))
0

I am not quite sure how you would use that, but it does illustrate the generality of the index function!

Copyright (C) 2013 by John Kitchin. See the License for information about copying.

org-mode source

Read and Post Comments

Summarizing org-files in a report

| categories: emacs-lisp, org-mode | tags: | View Comments

This is an example of using emacs-lisp to extract pieces of information about a bunch of org-files into a single report, as well as aggregating data from those files. The scenario where this would likely be useful is if you have a set of org-files that contain information, e.g. from a bunch of different calculations, or from documents turned in by different students, and you want to aggregate the results into a report.

In this example, I have a set of org-files in this directory that contain simulated homework assignments turned in. The files in this example all look something like this. Each heading corresponds to a problem, and there is a properties drawer for each heading that contains the grade.

We will create a navigation document that facilitates reviewing each of the files, as well as collecting the grades from the files. Here is what a typical file looks like:

#+PROPERTY: NAME Ellen Donnte
* 1a
  :PROPERTIES:
  :lettergrade: A
  :END:
* 1b
  :PROPERTIES:
  :lettergrade: R
  :END:
* 2
  :PROPERTIES:
  :lettergrade: A
  :END:
#+BEGIN_SRC emacs-lisp
(prin1 42)
#+END_SRC

#+RESULTS:
: 42

* 3
  :PROPERTIES:
  :lettergrade: C
  :END:

1 Creating a navigation document

In this section we write some code that creates text with a link to each file we need to review. This is something I imagine we would do after all the files have been turned in and collected. This buffer would facilitate navigating all the files, and checking them off. First we create checkboxes. All this does is create an easy to use navigation document that facilitates opening the files, grading them, and marking them as done.1

(require 'find-lisp)

(dolist (fname (find-lisp-find-files "." "\\HW1.org$") nil)
  (princ (format "- [ ] [[file:%s][%s]]\n" fname (file-name-nondirectory fname))))

In the results above I have marked one entry as completed.

It might be preferrable to have links to places in the file, e.g. to problem 2.

(require 'find-lisp)

(dolist (fname (find-lisp-find-files "." "\\HW1.org$") nil)
  (princ (format "- [ ] [[file:%s::*2][%s - problem 2]]\n" fname (file-name-nondirectory fname))))

2 Aggregating properties

Our goal here is to use emacs-lisp to aggregate the letter grades from all the assignments into a table. This would be done after all the files have been reviewed. First, we write a function that gets the data we want. The function should take a filename, and return the letter grade for a problem, e.g. (get-letter-grade filename problem) -> lettergrade. Then, we will map that function onto a list of files.

(require 'find-lisp)

(defun get-letter-grade (filename problem-name)
  "Open filename, get the grade associated with the heading of problem-name."
  (with-temp-buffer
    (insert-file-contents filename)
    (let ((studentname nil)
           (lettergrade nil))
      (org-ctrl-c-ctrl-c) ; this is needed to read the NAME property!
      (setq studentname (org-entry-get (point) "NAME" t))
      (goto-char (point-min))
      (search-forward problem-name)
      (setq lettergrade (org-entry-get (point) "lettergrade"))
      
      (princ (format "|%s|%s|%s|\n" studentname problem-name lettergrade)))))

(princ "#+ATTR_HTML: :border 2 :rules all :frame border\n")
(princ "#+tblname: GRADES\n")
(princ "| Name | Problem | Grade |\n|-\n")

(dolist (problem-name '("1a" "1b" "2" "3") nil)
  (mapcar (lambda (fname) (get-letter-grade fname problem-name)) 
      (find-lisp-find-files "." "\\HW1.org$")))
Name Problem Grade
Slim Shady 1a A
John Doe 1a B
Jim Vicious 1a B
Ellen Donnte 1a A
Slim Shady 1b B
John Doe 1b A
Jim Vicious 1b C
Ellen Donnte 1b R
Slim Shady 2 R
John Doe 2 B
Jim Vicious 2 R
Ellen Donnte 2 A
Slim Shady 3 R
John Doe 3 B
Jim Vicious 3 D
Ellen Donnte 3 C

You could imagine some other kind of aggregating or analysis here too. Now that we have that table, we can use it in other analysis. Let us count the number of A's.

(save-excursion
  (goto-char (point-min))
  (search-forward-regexp "^#\\+tblname: GRADES")
  (next-line)
  (let ((A-COUNT 0)
        (letter-grade nil)
        ;; cddr is used to remove the first two rows of the table
        (data (cddr (org-table-to-lisp))))
    (dolist (entry data nil)
      (setq letter-grade (nth 2 entry))
      (if (equal  letter-grade "A")
          (incf A-COUNT)))
    (princ (format "%s A's counted" A-COUNT))))
4 A's counted

Since we are in org-mode, we can use the table directly! Let us do that and count the number of R's.

(let ((COUNT 0)
      (letter-grade nil))
    (dolist (entry (cddr data) nil)
      (setq letter-grade (nth 2 entry))
      (if (equal  letter-grade "R")
          (incf COUNT)))
    (princ (format "%s R's counted" COUNT))))
4 R's counted

3 Aggregating sections of org-files into one file

Another scenario that may be interesting is to collect all of the responses in a single document. This might be useful to show examples in class, or to review all the problems to see if there are common errors. Here we collect Problem 2.

(require 'find-lisp)

(generate-new-buffer "Problem 2")
(set-buffer "Problem 2")
(insert "#+TITLE: Summary of problem 2\n")

(dolist (fname (find-lisp-find-files "." "\\HW1.org$") nil)
  (save-excursion
    (goto-char (point-max))
    (org-mode)
    (with-temp-buffer 
      (insert-file-contents fname)
      (org-mode)
      (goto-char (point-min))
      (setq studentname (org-entry-get nil "NAME" t))
      (search-forward "* 2")
      (org-narrow-to-subtree)
      (forward-line) ; skip heading
      (setq text (buffer-substring (point) (point-max))))
    (insert (format "* 2 - %s\n" studentname))
    (insert text "\n")
          
    (search-backward "* 2")
    (org-entry-put nil "NAME" studentname)
    (org-entry-put nil "source" (format "[[%s][link]]" fname))
))

(switch-to-buffer "Problem 2")
(org-mode) ; switch to org-mode in that buffer

;; print the lines to see what we got
(dolist (line (split-string (buffer-string) "\n") nil) (princ (format ": %s\n" line)))
: #+TITLE: Summary of problem 2
: * 2 - Slim Shady
:   :PROPERTIES:
:   :lettergrade: R
:   :NAME:     Slim Shady
:   :source:   [[c:/Users/jkitchin/Dropbox/blogofile-jkitchin.github.com/_blog/org-report/Slim-Shady-HW1.org][link]]
:   :END:
: #+BEGIN_SRC python
: print 3
: 
: #+END_SRC
: 
: #+RESULTS:
: : 3
: 
: * 2 - John Doe
:   :PROPERTIES:
:   :lettergrade: B
:   :NAME:     John Doe
:   :source:   [[c:/Users/jkitchin/Dropbox/blogofile-jkitchin.github.com/_blog/org-report/John-Doe-HW1.org][link]]
:   :END:
: Here is my solution
: #+BEGIN_SRC python
: print 4
: #+END_SRC
: 
: #+RESULTS:
: : 4
: 
: * 2 - Jim Vicious
:   :PROPERTIES:
:   :lettergrade: R
:   :NAME:     Jim Vicious
:   :source:   [[c:/Users/jkitchin/Dropbox/blogofile-jkitchin.github.com/_blog/org-report/Jim-Vicious-HW1.org][link]]
:   :END:
: I could not figure this out
: * 2 - Ellen Donnte
:   :PROPERTIES:
:   :lettergrade: A
:   :NAME:     Ellen Donnte
:   :source:   [[c:/Users/jkitchin/Dropbox/blogofile-jkitchin.github.com/_blog/org-report/Ellen-Donnte-HW1.org][link]]
:   :END:
: #+BEGIN_SRC emacs-lisp
: (prin1 42)
: #+END_SRC
: 
: #+RESULTS:
: : 42

I am not super thrilled with this approach. It feels too much like hand-crafting a result, but it does show some possibilities!

Copyright (C) 2013 by John Kitchin. See the License for information about copying.

org-mode source

Read and Post Comments

An index function for strings in emacs-lisp

| categories: emacs-lisp | tags: | View Comments

I could not find an index function for strings in emacs-lisp. The position function seems to work for numbers, but not strings. Here is a version that works on strings.

(defun index (item list)
  "return index of item in list or nil"
  (let ((counter 0)
        (found nil))
    (dolist (listelement list counter)
      (if (string= item listelement)
        (progn 
          (setq found t)
          (return counter)) ; exit the loop
        ;; else increment counter
        (incf counter)))
    ;; if we found it return counter otherwise return nil
    (if found counter nil)))
index

Here are some example uses:

(index "test" '("a" "test" "y"))
1
(index "z" '("a" "b" "z"))
2
(index "testy" '("a" "test" "y"))
nil

This raises an error because we use string=.

(index 1 '("a" "test" "y" 1))

Copyright (C) 2013 by John Kitchin. See the License for information about copying.

org-mode source

Read and Post Comments

« Previous Page