]> git.bundesbrandschatzamt.de Git - emacsconfig.git/commitdiff
add org-listcruncher
authorAndreas Gerler <baronNOSPAMbundesbrandschatzamt.de>
Sat, 26 Feb 2022 10:47:23 +0000 (11:47 +0100)
committerAndreas Gerler <baronNOSPAMbundesbrandschatzamt.de>
Sat, 26 Feb 2022 10:50:08 +0000 (11:50 +0100)
Signed-off-by: Andreas Gerler <baronNOSPAMbundesbrandschatzamt.de>
lisp/org-listcruncher.el [new file with mode: 0644]

diff --git a/lisp/org-listcruncher.el b/lisp/org-listcruncher.el
new file mode 100644 (file)
index 0000000..de31dbe
--- /dev/null
@@ -0,0 +1,354 @@
+;;; org-listcruncher.el --- Planning tool - Parse Org mode lists into table  -*- lexical-binding: t; -*-
+
+;; Author: Derek Feichtinger <dfeichNOSPAMgmail.com>
+;; Keywords: convenience
+;; Package-Requires: ((seq "2.3") (emacs "26.1"))
+;; Homepage: https://github.com/dfeich/org-listcruncher
+;; Version: 1.4
+
+;; This program is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation; either version 3, or (at your option)
+;; any later version.
+;;
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+;;
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs.  If not, see <http://www.gnu.org/licenses/>.
+
+
+;;; Commentary:
+;; org-listcruncher is a planning tool that allows the conversion of
+;; an Org mode list to an Org table (a list of lists).  The table can
+;; be used by other Org tables or Org code blocks for further
+;; calculations.
+;;
+;; Example:
+;;
+;;   #+NAME: lstTest
+;;   - item: item X modified by replacing values (amount: 15, recurrence: 1, end-year: 2020)
+;;     - modification of item X (amount: 20)
+;;     - another modification of item X (other: 500)
+;;       - modification of the modification (other: 299)
+;;   - illustrating inheritance (recurrence: 2, end-year: 2024)
+;;     - item: item A. Some longer explanation that may run over
+;;       multiple lines (amount: 10)
+;;     - item: item B (amount: 20)
+;;     - item: item C (amount: 30)
+;;       - a modification to item C (amount: 25, recurrence: 3)
+;;   - item: item Y modified by operations (amount: 50, recurrence: 4, end-year: 2026)
+;;     - modification by an operation (amount: +50)
+;;     - modification by an operation (amount: *1.5)
+;;   - item: item Z entered in scientific format (amount: 1e3, recurrence: 3, end-year: 2025)
+;;     - modification by an operation (amount: -1e2)
+;; 
+;;   We can use org-listcruncher to convert this list into a table
+;; 
+;;   #+NAME: src-example1
+;;   #+BEGIN_SRC elisp :results value :var listname="lstTest" :exports both
+;;     (org-listcruncher-to-table listname)
+;;   #+END_SRC
+;; 
+;;   #+RESULTS: src-example1
+;;   | description                         | other | amount | recurrence | end-year |
+;;   |-------------------------------------+-------+--------+------------+----------|
+;;   | item X modified by replacing values |   299 |     20 |          1 |     2020 |
+;;   | item A                              |       |     10 |          2 |     2024 |
+;;   | item B                              |       |     20 |          2 |     2024 |
+;;   | item C                              |       |     25 |          3 |     2024 |
+;;   | item Y modified by operations       |       |  150.0 |          4 |     2026 |
+;;   | item Z entered in scientific format |       |  900.0 |          3 |     2025 |
+;;
+;;  The parsing and consolidation functions for producing the table can be modified by
+;;  the user.  Please refer to the README and to the documentation strings of the
+;;  functions.
+;;; Code:
+(require 'org)
+(require 'cl-lib)
+(require 'seq)
+
+(defgroup org-listcruncher nil
+  "Parses Org mode lists according to a parsing function and yields an org table structure."
+  :group 'org)
+
+(defcustom org-listcruncher-parse-fn #'org-listcruncher-parseitem-default
+  "Function used for parsing list items.
+
+The function receives a list item as its single argument.  It must
+return a list (OUTP, DESCR, VARLST), where OUTP is a boolean
+indicating whether this list item will become a table row, DESCR
+is its description string appearing in the table, VARLST is the
+list of key/value pairs corresponding to the column name /
+values.  Simple example functions for this purpose can be generated
+using the `org-listcruncher-mk-parseitem-default' generator
+function."
+  :group 'org-listcruncher
+  :type 'function)
+
+(defcustom org-listcruncher-consolidate-fn #'org-listcruncher-consolidate-default
+  "Function for consolidating a sequence of values for a certain key.
+
+The function must accept two arguments: KEY and LIST.  The KEY is
+the key selecting the (KEY VALUE) pairs from the given LIST.  The
+function must return a single value based on consolidating the
+VALUEs from the given key-value pairs.  Refer to the default
+function `org-listcruncher-consolidate-default'."
+  :group 'org-listcruncher
+  :type 'function )
+
+;; TODO: make parentheses definitions a parameter
+;;;###autoload
+(cl-defun org-listcruncher-mk-parseitem-default (&key (tag "\\*?item:\\*?")
+                                                     (endtag ".")
+                                                     (bra "(")
+                                                     (ket ")"))
+  "List item default parsing function generator for org-listcruncher.
+
+This generator can be used to produce a family of parsing
+functions with similar structure. It returns a parsing function
+that will take a list item line as its only argument.
+
+The generated parsing functions all share the following features.
+1. Whether a list item will become a table row is defined by a matching
+   TAG at the beginning of the list item. Default is \"item:\" and allowing
+   for org bold markers.
+2. The row's description is defined by the string following the TAG up to
+   a) a character contained in the ENDTAG argument or
+   b) the opening parenthesis BRA used for beginning the key/value pairs.
+   The default for ENDTAG is \".\".
+3. The key/value pairs are separated by commas, and a key is separated from
+   its value by a colon key1: val1, key2: val2. The default brackets are
+   \"(\" and \")\".
+
+The resulting function can be modified by the following keyword arguments:
+- TAG REGEXP defines the REGEXP used for identifying whether a line will become
+  a table row.
+- ENDTAG STRING: Each character contained in that string will act as a
+  terminator for the description of an item.
+- The BRA and KET keywords can be used to define strings defining the opening
+  and closing parentheses to be used for enclosing the key/value pairs
+  The given strings will get regexp quoted."
+  (lambda (line)
+    (let (outp descr varstr varlst)
+      ;; TODO: I should make the expression for the key:val list more restrictive
+      (if (string-match
+          (concat
+           "^ *\\(" tag "\\)?" ;; tag
+           " *\\([^" endtag bra "]*\\)" ;; description terminated by endtag or bra
+           "[^" bra "]*" ;; ignore everything until a bracket expression begins
+           ;; key/val pairs
+           "\\\(" (regexp-quote  bra) "\\\(\\\(\\\([^:,)]+\\\):\\\([^,)]+\\\),?\\\)+\\\)"
+           (regexp-quote  ket) "\\\)?")
+          line)
+         (progn
+           (setq outp (if (match-string 1 line) t nil)
+                 descr (replace-regexp-in-string " *$" "" (match-string 2 line))
+                 varstr (match-string 4 line))))
+      (when varstr
+       (setq varlst
+             (cl-loop for elem in (split-string varstr ", *")
+                      if (string-match "\\([^:]+\\): *\\(.*\\)" elem)
+                      collect (list (match-string 1 elem)
+                                     (match-string 2 elem)) into result
+                      finally return result)))
+      (list outp descr varlst))))
+
+
+(defun org-listcruncher-parseitem-default (line)
+  "List item default parsing function for org-listcruncher.
+
+Parses the given list item LINE."
+  (funcall (org-listcruncher-mk-parseitem-default) line))
+
+(defun org-listcruncher--calc-orgtable (tbl)
+  "Aligns and calculates table functions of the given org table.
+
+The table given in TBL will be processed using the standard
+org mode `org-table-align' and `org-table-recalculate' functions.
+The resulting table is returned in a string."
+  (with-temp-buffer
+    (erase-buffer)
+    (insert tbl)
+    (goto-char (point-min))
+    (org-mode)
+    (while
+        (search-forward-regexp org-table-any-line-regexp nil t)
+      (org-table-align)
+      (org-table-recalculate 'iterate)
+      (goto-char (org-table-end)))
+    (buffer-string)))
+
+(defun org-listcruncher--sparse-to-table (sparselst &optional order)
+  "Return list of all unique keys of the list of alists in SPARSELST.
+
+If a list is provided in the ORDER argument, the table columns
+will be ordered according to this list.  The list may contain only
+a subset of the items.  The remaining columns will be added in the
+original order."
+  (let* ((keylst
+         ;; list of all unique keys of the list of alists in SPARSELST
+         (cl-loop for alst in sparselst
+                  with reslst = nil
+                  collect (mapcar (lambda (kvpair) (car kvpair))  alst) into reslst
+                  finally return (seq-uniq (apply #'append  reslst))))
+        (orderedlst (append order
+                            (cl-loop for elm in order
+                                     do (setq keylst (delete elm keylst))
+                                     finally return keylst)))
+        ;; for each key, find var values in each given row in sparselist
+        (rows
+         (cl-loop for alst in sparselst
+                  with reslst = nil
+                  collect (mapcar (lambda (key)
+                                    (apply org-listcruncher-consolidate-fn
+                                            (list key alst)))
+                                  orderedlst) into reslst
+                  finally return reslst)))
+    (if rows
+       (append `(,orderedlst) '(hline) rows)
+      nil)))
+
+
+;;;###autoload
+(cl-defun org-listcruncher-to-table (listname
+                                    &key (parsefn org-listcruncher-parse-fn)
+                                    (order nil)
+                                     (formula nil))
+  "Return a table structure based on parsing the Org list with name LISTNAME.
+
+Optional keyword arguments: The user may use the PARSEFN
+FUNCTION argument to define another parsing function for the list
+items.  The ORDER keyword takes a list containing column names as
+its argument for defining the output table's desired columns
+order. The list may contain only a subset of the items.  The
+remaining columns will be added in the original order.
+
+If FORMULA is non-nil the given Calc formula will be calculated
+by org spreadsheet functions (what usually would follow the
+#+TBLFM: in an org spreadsheet table). The result is no longer a
+Lisp table structure but a string containing the fully formatted
+table."
+  (let* ((lst
+         (save-excursion
+           (goto-char (point-min))
+           (unless (search-forward-regexp (concat  "^[ \t]*#\\\+NAME: .*" listname) nil t)
+             (error "No list of this name found: %s" listname))
+           (forward-line 1)
+           (org-list-to-lisp)))
+         (tbl
+          (org-listcruncher--sparse-to-table
+           (cadr (org-listcruncher--parselist lst parsefn nil nil))
+           order)))
+    (if formula
+        (org-listcruncher--calc-orgtable
+         (orgtbl-to-orgtbl (append tbl '(hline ("")))
+                           `(:tend ,(concat "#+TBLFM: " formula))))
+      tbl)))
+
+(defun org-listcruncher--parselist (lst parsefn inheritvars resultlst)
+  "Parse an org list into a table structure.
+
+LST is a list as produced from `org-list-to-lisp'.  PARSEFN is the
+parsing function for the list items.  INHERITVARS is an
+association list of (varname value) pairs that constitute the
+inherited variable values from the parent.  RESULTLST contains the
+current result structure in form of a list of association
+lists.  Each contained association list corresponds to a later
+table row."
+  (let ((ltype (car lst))
+       (itemstructs (cdr lst))
+       retvarlst)
+    (setq retvarlst
+         (cl-loop for struct in itemstructs
+                  with joinedsubvarlst = nil
+                  do (let ((itemtext (car struct))
+                           (sublist (cadr struct))
+                           itemvarlst subtreevarlst outvarlst)
+                       ;; parse this item
+                       (let* ((prsitem (apply parsefn `(,itemtext)))
+                              (outp (car prsitem))
+                              (descr (nth 1 prsitem))
+                              (itemvarlst (nth 2 prsitem)))
+                         ;; (princ (format "DEBUG: item [%s] varlst: %s\n" descr itemvarlst))
+                         ;; if item has a sublist, recurse with this sublist and get varlst of this tree
+                         (when sublist
+                           (let ((parseresult (org-listcruncher--parselist sublist
+                                                                           parsefn
+                                                                           (append itemvarlst inheritvars)
+                                                                           resultlst)))
+                             (setq subtreevarlst (car parseresult))
+                             (setq resultlst (cadr parseresult)))
+                           ;;(princ (format "DEBUG: received subtreevarlst %s\n" subtreevarlst))
+                           )
+                         ;; only prepare an output line if this item is flagged as an output item
+                         (when outp
+                           ;; the current item's description always is placed first in the list
+                           (setq outvarlst (append `(("description" ,descr)) subtreevarlst itemvarlst inheritvars))
+                           (setq resultlst (append resultlst (list outvarlst))))
+                         ;; accumulate all item's varlists for returning to parent item
+                         (setq joinedsubvarlst (append subtreevarlst itemvarlst joinedsubvarlst))))
+                  ;; we return the consolidated varlst of this tree
+                  finally return joinedsubvarlst))
+    (list retvarlst resultlst)))
+
+(defun org-listcruncher-consolidate-default (key lst)
+  "Return consolidated value for KEY out of the list LST of key-value pairs.
+
+The list is given in reverse order (stack), i.e. the newest item
+is at the beginning.
+
+Example list:\n '((\"key\" \"+=10\") (\"key\" \"50\") (\"otherkey\"
+\"hello\"))
+
+When calling the function on this list with the KEY
+argument set to \"key\" it will return 60."
+  (let* ((values (cl-loop for kv in lst
+                         if (equal key (car kv))
+                         collect (cadr kv) into reslst
+                         finally return (nreverse reslst)))
+        (result (pop values)))
+    (cl-loop for v in values
+            if (string-match "^\\\([+/*]=?\\\|-=\\\)\\\([0-9.]+\\\(e[0-9+]\\\)?\\\)" v)
+            do (progn
+                 (when (eq (type-of result) 'string)
+                   (setq result (string-to-number result)))
+                 (setq result (apply (pcase (match-string 1 v)
+                                       ((or "+=" "+") '+)
+                                       ("-=" '-)
+                                       ((or "/=" "/") '/)
+                                       ((or "*=" "*") '*))
+                                     (list result
+                                            (string-to-number (match-string 2 v))))))
+            else if (string-match "^\\(+\\|-\\)=\\(.*\\)" v)
+             do (progn (when (or (integerp result) (floatp result))
+                         (setq result (number-to-string result)))
+                       (pcase (match-string 1 v)
+                         ("+" (setq result (concat result " " (match-string 2 v))))
+                         ("-" (setq result
+                                    (mapconcat
+                                     'identity
+                                     (remove (match-string 2 v) (split-string result " "))
+                                     " ")))))
+             else
+             do (setq result v))
+    (or result "")))
+
+
+;;;###autoload
+(cl-defun org-listcruncher-get-field (listname row col &key (parsefn org-listcruncher-parse-fn))
+  "Return field defined by ROW,COL from the table derived from LISTNAME.
+
+The given list with LISTNAME is parsed by listcruncher to obtain a table.
+The field is defined by the two strings for ROW and COL, where the ROW string
+corresponds to the contents of the item's \"description\" column and the COL
+string corresponds to the column's name."
+  (let* ((tbl (org-listcruncher-to-table listname :parsefn parsefn))
+        (colnames (car tbl))
+        (colidx (cl-position col colnames :test #'equal)))
+    (nth colidx (assoc row tbl))))
+
+(provide 'org-listcruncher)
+;;; org-listcruncher.el ends here