I use org-mode for many things, especially tracking all the time I work on projects. To keep tracking as easy as possible, I use a table for each project. Each table consists of the fields: day, duration and comments. For me, there is no need to track certain tasks on a project, only the duration matters. Most of the projects have a lifetime of one year. I have to sync my tracking with our time-tracking-system each month, so I need an overview over all entries of a month ordered by the projects. At the moment I work on seven projects in parallel. You can imagine that the more all the tables grow, the more the org-file becomes unhandy. I don’t want to split up the file into several others to keep things simple. So I decided to write a lisp function that creates a new buffer and lists the entries of a given month of all tables. So let’s dive into some lisp code 😉

The tables in my org file almost look like this

#+NAME: <PROJECT>
|   | Date       |       Duration | Comment |
|---+------------+----------------+---------|
|   | 2017-02-28 |          08:00 |         |
|---+------------+----------------+---------|
| _ |            | tot<IDOFTABLE> |         |
| # | Sum        |          08:00 |         |

There is a report-table in the same file. tot<IDOFTBALE> is the reference to the sum, and I use it to build the report-table. In further iterations, I will refactor the report so that this tag won’t be necessary for the future.

I want to exclude some tables from processing, so we store the names of those in a global variable.

(defvar psp-tables-to-ignore '(template report-summary-year))

Furthermore, we want to exclude some rows from processing, especially hlines, headers and summary lines. The following helper function tests if a given row should be excluded and returns true or false.

(defun psp-ignore-row-p (row)
  "Should the given row be ignored?"
  (or (equalp 'hline row) ; ignore hlines 
      (equalp "Date" (cadr row)) ; ignore header row  
      (equalp "_" (car row)) ; ignore reference row
      (equalp "#" (car row)) ; ignore summary row 
      (equalp (cadr row) ""))) ; ignore empty rows

Next, we create our function body.

(defun psp-calculate-month (month)
  "PSP calculator - shows all tracked times of a given month."
  (interactive "MMonth")
)

The function is declared as interactive so that we can set the month as a parameter in the minibuffer. The “M” in front of “Month” means, take an arbitrary text from the minibuffers input and return it as a string.

We can use the function org-table-map-tables to iterate over all tables in an org file. org-table-map-tables takes a function and invokes that on each table of a buffer.

(org-table-map-tables <FUNCTION>)

org-table-map-tables operates on org-table structures. In my opinion, it is easier to work with Lisp lists. The function org-table-to-lisp converts the current org-table into a Lisp list structure. Keep in mind that it converts all rows including headers, hlines etc.

The function mapcar iterates over each line of a table and invokes a given function on each row. In Lisp it looks like that.

(mapcar #'<FUNCTION> (org-table-to-lisp))

Let’s build the things together, that we have so far.

(defun <FUNCTION_NAME> (<OPTIONS>)
  "<DESCRIPTION>"
  (interactive "")
  (org-table-map-tables (lambda () 
   (mapcar #'<FUNCTION> (org-table-to-lisp)))))

The structure above is all you need to iterate over all the tables and their rows. We will extend this structure to a full working example in the rest of this article, so you have something more realistic to play with.

First, we create our result buffer. Emacs Lisp provides the function get-buffer-create for this. get-buffer-create takes a string with the name of a buffer and returns that buffer, or it creates a new buffer with that name if it does not exist yet. We save the buffer handle a function local variable for future usage.

(let ((RES_BUFFER (get-buffer-create "RESULT")))
    <BODY>)

Each table in the result buffer should have the same name like in the origin org file. We can use org-element-property to get the name of the table.

(let ((table_name (org-element-property :name (org-element-at-point))))
  <BODY>)

org-element-at-point returns the org-element at cursors current position. We need not move the cursor ourselves, org-table-map-tables already does this job for us. In the next step we test if the table_name is in our ignore list, for that we have to convert table_name into a symbol first, the lisp function intern does the job.

(if (not (member (intern table_name) psp-tables-to-ignore))
    <BODY>)

To save the current tables name in our result buffer, we use the emacs lisp functions save-current-buffer and insert.

(save-current-buffer
  (set-buffer PSPLIST) ; open destination buffer 
  (insert (format "/n*%s" table_name))) ; leave context and return to old buffer is handled by save-current-buffer

We use the same technique to copy relevant rows from our origin table into the destination buffer.

(if (not(psp-ignore-row-p row))
    (if (equalp (subseq (cadr row) 5 7) month)
        (save-current-buffer
          (set-buffer PSPLIST)
          (insert (format "|%s|%s|\n" (cadr row) (nth 2 row))))))

At the end, we open the new buffer in org-mode.

(switch-to-buffer PSPLIST)
(with-current-buffer PSPLIST
  (funcall 'org-mode)
  (outline-show-all))

Here you find the complete code. Hopefully, I didn’t forget to copy something 😉

(require 'org-table)
(defvar psp-tables-to-ignore '(template report-summary-year))

(defun psp-ignore-row-p (row)
  "Should the given row be ignored?"
  (or (equalp 'hline row) ; ignore hlines 
      (equalp "Date" (cadr row)) ; ignore header row  
      (equalp "_" (car row)) ; ignore reference row
      (equalp "#" (car row)) ; ignore summary row 
      (equalp (cadr row) ""))) ; ignore empty rows

(defun psp-calculate-month (month)
  "PSP calculator - shows all tracked times of a given month"
  (interactive "MMonth")
  (let ((cur_buf (current-buffer))
        (PSPLIST (get-buffer-create "PSP-LISTE")))
    (org-table-map-tables 
     (lambda ()
       (let ((table_name (org-element-property :name (org-element-at-point)))) 
         (if (not (member (intern table_name) psp-tables-to-ignore))
             (progn
               (save-current-buffer
                 (set-buffer PSPLIST) 
                 (insert (format "/n*%s" table_name)))             
               (mapcar #'(lambda (row)
                           (if (not(psp-ignore-row-p row))
                               (if (equalp (subseq (cadr row) 5 7) month)
                                   (save-current-buffer
                                     (set-buffer PSPLIST)
                                     (insert (format "|%s|%s|\n" (cadr row) (nth 2 row))))))) 
                       (org-table-to-lisp)))))))
    (switch-to-buffer PSPLIST)
    (with-current-buffer PSPLIST
      (funcall 'org-mode)
      (outline-show-all))))

(provide 'psp-calc)

Let me know if this article helped you, or what things can be improved.

Leave a comment

Your email address will not be published. Required fields are marked *