ENH optimize org-x-headline-has-children
This commit is contained in:
parent
8907ab75f6
commit
c7111e24b6
|
@ -98,8 +98,7 @@ entire subtrees to save time and ignore tasks")
|
|||
(defun org-x-get-date-property (timestamp-property)
|
||||
"Get TIMESTAMP-PROPERTY on current heading and convert to a number.
|
||||
If it does not have a date, it will return nil."
|
||||
(let ((ts (org-entry-get nil timestamp-property)))
|
||||
(when ts (org-2ft ts))))
|
||||
(-some-> (org-entry-get nil timestamp-property) (org-2ft)))
|
||||
|
||||
;; TODO I don't like this function...it perplexes me
|
||||
(defun org-x-heading-compare-timestamp (timestamp-fun &optional ref-time future)
|
||||
|
@ -270,19 +269,38 @@ compared to REF-TIME. Returns nil if no timestamp is found."
|
|||
|
||||
;; relational testing
|
||||
|
||||
(defun org-x-headline-has-children (heading-test)
|
||||
"Return t if heading has a child for whom HEADING-TEST is t."
|
||||
(let ((subtree-end (save-excursion (org-end-of-subtree t)))
|
||||
has-children previous-point)
|
||||
;; ;; TODO this function seems slow
|
||||
;; (defun org-x-headline-has-children (heading-test)
|
||||
;; "Return t if heading has a child for whom HEADING-TEST is t."
|
||||
;; (let ((subtree-end (save-excursion (org-end-of-subtree t)))
|
||||
;; has-children previous-point)
|
||||
;; (save-excursion
|
||||
;; (setq previous-point (point))
|
||||
;; (outline-next-heading)
|
||||
;; (while (and (not has-children)
|
||||
;; (< previous-point (point) subtree-end))
|
||||
;; (when (funcall heading-test)
|
||||
;; (setq has-children t))
|
||||
;; (setq previous-point (point))
|
||||
;; (org-forward-heading-same-level 1 t)))
|
||||
;; has-children))
|
||||
|
||||
(defun org-x-headline-has-children (test-fun)
|
||||
"Return t if heading has a child for whom TEST-FUN is t."
|
||||
;; assume that point is at the beginning of a headline
|
||||
(let* ((level (1+ (org-current-level)))
|
||||
(has-children nil)
|
||||
(cur-level level))
|
||||
;; skip over the current headline
|
||||
(re-search-forward org-outline-regexp-bol nil t)
|
||||
(save-excursion
|
||||
(setq previous-point (point))
|
||||
(outline-next-heading)
|
||||
(while (and (not has-children)
|
||||
(< previous-point (point) subtree-end))
|
||||
(when (funcall heading-test)
|
||||
(setq has-children t))
|
||||
(setq previous-point (point))
|
||||
(org-forward-heading-same-level 1 t)))
|
||||
(while (and (<= level cur-level)
|
||||
(re-search-forward org-outline-regexp-bol nil t))
|
||||
;; it's actually more efficient to scan every headline and check its
|
||||
;; level rather than using a regexp to match the target depth
|
||||
(setq cur-level (- (match-end 0) (match-beginning 0) 1))
|
||||
(when (and (= cur-level level) (funcall test-fun))
|
||||
(setq has-children t))))
|
||||
has-children))
|
||||
|
||||
(defun org-x-headline-has-parent (heading-test)
|
||||
|
@ -317,9 +335,7 @@ compared to REF-TIME. Returns nil if no timestamp is found."
|
|||
`(,op (cl-position ,sc1 ,sc-list) (cl-position ,sc2 ,sc-list)))
|
||||
|
||||
;; TODO there is likely a better way to handle this
|
||||
(defun org-x-descend-into-project (allowed-statuscodes trans-tbl
|
||||
get-task-status
|
||||
callback-fun)
|
||||
(defmacro org-x-descend-into-project (statuscode-tree get-task-status callback-fun)
|
||||
"Loop through (sub)project and return overall statuscode.
|
||||
|
||||
The returned statuscode is chosen from list ALLOWED-STATUSCODES where
|
||||
|
@ -341,105 +357,99 @@ should be this function again)."
|
|||
;; define "breaker-status" as the last of the allowed-statuscodes
|
||||
;; when this is encountered the loop is broken because we are done
|
||||
;; (the last entry trumps all others)
|
||||
(let ((project-status (cl-first allowed-statuscodes))
|
||||
(breaker-status (-last-item allowed-statuscodes))
|
||||
(previous-point))
|
||||
(save-excursion
|
||||
(setq previous-point (point))
|
||||
(outline-next-heading)
|
||||
;; loop through subproject tasks until breaker-status found
|
||||
(while (and (not (eq project-status breaker-status))
|
||||
(> (point) previous-point))
|
||||
(let ((keyword (org-x-is-todoitem-p)))
|
||||
(if keyword
|
||||
(let ((new-status
|
||||
;; if project then descend recursively
|
||||
(if (org-x-headline-has-children 'org-x-is-todoitem-p)
|
||||
(let ((n (funcall callback-fun)))
|
||||
;; if project returns an allowed status
|
||||
;; then use that
|
||||
(or (and (member n allowed-statuscodes) n)
|
||||
;; otherwise look up the value in the
|
||||
;; translation table and return error
|
||||
;; if not found
|
||||
(nth (or (alist-get n trans-tbl)
|
||||
(error (concat "status not found: " n)))
|
||||
allowed-statuscodes)))
|
||||
;; if tasks then use get-task-status to obtain status
|
||||
(nth (funcall get-task-status keyword)
|
||||
allowed-statuscodes))))
|
||||
(if (org-x-compare-statuscodes > new-status project-status allowed-statuscodes)
|
||||
(setq project-status new-status)))))
|
||||
(setq previous-point (point))
|
||||
(org-forward-heading-same-level 1 t)))
|
||||
project-status))
|
||||
(let* ((allowed-statuscodes (-map #'car statuscode-tree))
|
||||
(trans-tbl (->> statuscode-tree
|
||||
(--map (-let (((a . bs) it)) (--map (cons it a) bs)))
|
||||
(-flatten-n 1)))
|
||||
(breaker-status (-last-item allowed-statuscodes))
|
||||
(initial-status (car allowed-statuscodes)))
|
||||
`(let ((project-status ,initial-status)
|
||||
(previous-point nil)
|
||||
(new-status nil)
|
||||
(it-kw nil))
|
||||
(save-excursion
|
||||
(setq previous-point (point))
|
||||
(outline-next-heading)
|
||||
;; loop through subproject tasks until breaker-status found
|
||||
(while (and (not (eq project-status ,breaker-status))
|
||||
(> (point) previous-point))
|
||||
(setq it-kw (org-get-todo-state))
|
||||
(when it-kw
|
||||
(if (org-x-headline-has-children #'org-x-is-todoitem-p)
|
||||
(progn
|
||||
;; If project returns an allowed status then use that.
|
||||
;; Otherwise look up the value in the translation table and
|
||||
;; return error if not found.
|
||||
(setq new-status (,callback-fun))
|
||||
(unless (member new-status ',allowed-statuscodes)
|
||||
(setq new-status (alist-get new-status ',trans-tbl))))
|
||||
;; if tasks then use get-task-status to obtain status
|
||||
(setq new-status (nth ,get-task-status ',allowed-statuscodes)))
|
||||
(when (org-x-compare-statuscodes > new-status project-status ',allowed-statuscodes)
|
||||
(setq project-status new-status)))
|
||||
(setq previous-point (point))
|
||||
(org-forward-heading-same-level 1 t)))
|
||||
project-status)))
|
||||
|
||||
(defun org-x-get-project-status ()
|
||||
"Return project heading statuscode (assumes it is indeed a project)."
|
||||
(let ((keyword (org-x-is-todoitem-p)))
|
||||
;;
|
||||
;; these first three are easy because they only require
|
||||
;; testing the project headline and nothing underneath
|
||||
;;
|
||||
(cond
|
||||
;; it does not make sense for projects to be scheduled
|
||||
((org-x-is-scheduled-heading-p) :scheduled-project)
|
||||
;;
|
||||
;; these first three are easy because they only require
|
||||
;; testing the project headline and nothing underneath
|
||||
;;
|
||||
;; it does not make sense for projects to be scheduled
|
||||
(if (org-x-is-scheduled-heading-p) :scheduled-project
|
||||
(-when-let (keyword (org-get-todo-state))
|
||||
(cond
|
||||
;; held projects do not care what is underneath them
|
||||
;; only need to test if they are inert
|
||||
((equal keyword "HOLD") (if (org-x-is-inert-p) :inert :held))
|
||||
|
||||
;; held projects do not care what is underneath them
|
||||
;; only need to test if they are inert
|
||||
((equal keyword "HOLD") (if (org-x-is-inert-p) :inert :held))
|
||||
;; projects with invalid todostates are nonsense
|
||||
((member keyword org-x-project-invalid-todostates)
|
||||
:invalid-todostate)
|
||||
|
||||
;; projects with invalid todostates are nonsense
|
||||
((member keyword org-x-project-invalid-todostates)
|
||||
:invalid-todostate)
|
||||
;; canceled projects can either be archivable or complete
|
||||
;; any errors or undone tasks are irrelevant
|
||||
((equal keyword "CANC") (if (org-x-is-archivable-heading-p) :archivable
|
||||
:complete))
|
||||
|
||||
;;
|
||||
;; these require descending into the project subtasks
|
||||
;;
|
||||
|
||||
;; canceled projects can either be archivable or complete
|
||||
;; any errors or undone tasks are irrelevant
|
||||
((equal keyword "CANC") (if (org-x-is-archivable-heading-p) :archivable
|
||||
:complete))
|
||||
|
||||
;;
|
||||
;; these require descending into the project subtasks
|
||||
;;
|
||||
|
||||
;; done projects are like canceled projects but can also be incomplete
|
||||
((equal keyword "DONE")
|
||||
(org-x-descend-into-project
|
||||
'(:archivable :complete :done-incomplete)
|
||||
'((:stuck . 2)
|
||||
(:inert . 2)
|
||||
(:held . 2)
|
||||
(:wait . 2)
|
||||
(:active . 2)
|
||||
(:scheduled-project . 2)
|
||||
(:invalid-todostate . 2)
|
||||
(:undone-complete . 2))
|
||||
(lambda (k)
|
||||
(if (member k org-done-keywords)
|
||||
;; done projects are like canceled projects but can also be incomplete
|
||||
((equal keyword "DONE")
|
||||
(org-x-descend-into-project
|
||||
((:archivable)
|
||||
(:complete)
|
||||
(:done-incomplete :stuck :inert :held :wait :active
|
||||
:scheduled-project :invalid-todostate
|
||||
:undone-complete))
|
||||
(if (member it-kw org-done-keywords)
|
||||
(if (org-x-is-archivable-heading-p) 0 1)
|
||||
2))
|
||||
#'org-x-get-project-status))
|
||||
|
||||
;; project with TODO states could be basically any status
|
||||
((equal keyword "TODO")
|
||||
(org-x-descend-into-project
|
||||
'(:undone-complete :stuck :held :wait :inert :active)
|
||||
'((:complete . 0)
|
||||
(:archivable . 0)
|
||||
(:scheduled-project . 1)
|
||||
(:invalid-todostate . 1)
|
||||
(:done-incomplete . 1))
|
||||
(lambda (k)
|
||||
(cond ((and (not (member k org-done-keywords))
|
||||
(org-x-is-inert-p)) 4)
|
||||
((equal k "TODO") (if (org-x-is-scheduled-heading-p) 5 1))
|
||||
((equal k "HOLD") 2)
|
||||
((equal k "WAIT") 3)
|
||||
((equal k "NEXT") 5)
|
||||
(t 0)))
|
||||
#'org-x-get-project-status))
|
||||
|
||||
(t (error (concat "invalid keyword detected: " keyword))))))
|
||||
2)
|
||||
org-x-get-project-status))
|
||||
|
||||
;; project with TODO states could be basically any status
|
||||
((equal keyword "TODO")
|
||||
(org-x-descend-into-project
|
||||
((:undone-complete :complete :archivable)
|
||||
(:stuck :scheduled-project :invalid-todostate :done-incomplete)
|
||||
(:held)
|
||||
(:wait)
|
||||
(:inert)
|
||||
(:active))
|
||||
(cond
|
||||
((and (not (member it-kw org-done-keywords)) (org-x-is-inert-p)) 4)
|
||||
((equal it-kw "TODO") (if (org-x-is-scheduled-heading-p) 5 1))
|
||||
((equal it-kw "HOLD") 2)
|
||||
((equal it-kw "WAIT") 3)
|
||||
((equal it-kw "NEXT") 5)
|
||||
(t 0))
|
||||
org-x-get-project-status))
|
||||
|
||||
(t (error (concat "invalid keyword detected: " keyword)))))))
|
||||
|
||||
;; skip functions
|
||||
|
||||
|
|
Loading…
Reference in New Issue