org-sort multi: date/time (?d ?t) | priority (?p) | title (?a)
- by lawlist
Is anyone aware of an org-sort function / modification that can refile / organize a group of TODO so that it sorts them by three (3) criteria: first sort by due date, second sort by priority, and third sort by by title of the task?
EDIT: I believe that org-sort by deadline (?d) has a bug that cannot properly handle undated tasks. I am working on a workaround (i.e., moving the undated todo to a different heading before the deadline (?d) sort occurs), but perhaps the best thing to do would be to try and fix the original sorting function. Development of the workaround can be found in this thread (i.e., moving the undated tasks to a different heading in one fell swoop): How to automate org-refile for multiple todo
EDIT: Apparently, the following code (ancient history) that I found on the internet was eventually modified and included as a part of org-sort-entries. Unfortunately, undated todo are not properly sorted when sorting by deadline -- i.e., they are mixed in with the dated todo.
;; multiple sort
(defun org-sort-multi (&rest sort-types)
"Multiple sorts on a certain level of an outline tree, or plain list items.
SORT-TYPES is a list where each entry is either a character or a
cons pair (BOOL . CHAR), where BOOL is whether or not to sort
case-sensitively, and CHAR is one of the characters defined in
`org-sort-entries-or-items'. Entries are applied in back to
front order.
Example: To sort first by TODO status, then by priority, then by
date, then alphabetically (case-sensitive) use the following
call:
(org-sort-multi '(?d ?p ?t (t . ?a)))"
(interactive)
(dolist (x (nreverse sort-types))
(when (char-valid-p x)
(setq x (cons nil x)))
(condition-case nil
(org-sort-entries (car x) (cdr x))
(error nil))))
;; sort current level
(defun lawlist-sort (&rest sort-types)
"Sort the current org level.
SORT-TYPES is a list where each entry is either a character or a
cons pair (BOOL . CHAR), where BOOL is whether or not to sort
case-sensitively, and CHAR is one of the characters defined in
`org-sort-entries-or-items'. Entries are applied in back to
front order.
Defaults to \"?o ?p\" which is sorted by TODO status, then by
priority"
(interactive)
(when (equal mode-name "Org")
(let ((sort-types (or sort-types
(if (or (org-entry-get nil "TODO")
(org-entry-get nil "PRIORITY"))
'(?d ?t ?p) ;; date, time, priority
'((nil . ?a))))))
(save-excursion
(outline-up-heading 1)
(let ((start (point))
end)
(while (and (not (bobp)) (not (eobp)) (<= (point) start))
(condition-case nil
(outline-forward-same-level 1)
(error (outline-up-heading 1))))
(unless (> (point) start)
(goto-char (point-max)))
(setq end (point))
(goto-char start)
(apply 'org-sort-multi sort-types)
(goto-char end)
(when (eobp)
(forward-line -1))
(when (looking-at "^\\s-*$")
;; (delete-line)
)
(goto-char start)
;; (dotimes (x ) (org-cycle))
)))))
EDIT:
Here is a more modern version of multi-sort, which is likely based upon further development of the above-code:
(defun org-sort-all ()
(interactive)
(save-excursion
(goto-char (point-min))
(while (re-search-forward "^\* " nil t)
(goto-char (match-beginning 0))
(condition-case err
(progn
(org-sort-entries t ?a)
(org-sort-entries t ?p)
(org-sort-entries t ?o)
(forward-line))
(error nil)))
(goto-char (point-min))
(while (re-search-forward "\* PROJECT " nil t)
(goto-char (line-beginning-position))
(ignore-errors
(org-sort-entries t ?a)
(org-sort-entries t ?p)
(org-sort-entries t ?o))
(forward-line))))
EDIT: The best option will be to fix sorting of deadlines (?d) so that undated todo are moved to the bottom of the outline, instead of mixed in with the dated todo. Here is an excerpt from the current org.el included within Emacs Trunk (as of July 1, 2013):
(defun org-sort (with-case)
"Call `org-sort-entries', `org-table-sort-lines' or `org-sort-list'.
Optional argument WITH-CASE means sort case-sensitively."
(interactive "P")
(cond
((org-at-table-p) (org-call-with-arg 'org-table-sort-lines with-case))
((org-at-item-p) (org-call-with-arg 'org-sort-list with-case))
(t
(org-call-with-arg 'org-sort-entries with-case))))
(defun org-sort-remove-invisible (s)
(remove-text-properties 0 (length s) org-rm-props s)
(while (string-match org-bracket-link-regexp s)
(setq s (replace-match (if (match-end 2)
(match-string 3 s)
(match-string 1 s)) t t s)))
s)
(defvar org-priority-regexp) ; defined later in the file
(defvar org-after-sorting-entries-or-items-hook nil
"Hook that is run after a bunch of entries or items have been sorted.
When children are sorted, the cursor is in the parent line when this
hook gets called. When a region or a plain list is sorted, the cursor
will be in the first entry of the sorted region/list.")
(defun org-sort-entries
(&optional with-case sorting-type getkey-func compare-func property)
"Sort entries on a certain level of an outline tree.
If there is an active region, the entries in the region are sorted.
Else, if the cursor is before the first entry, sort the top-level items.
Else, the children of the entry at point are sorted.
Sorting can be alphabetically, numerically, by date/time as given by
a time stamp, by a property or by priority.
The command prompts for the sorting type unless it has been given to the
function through the SORTING-TYPE argument, which needs to be a character,
\(?n ?N ?a ?A ?t ?T ?s ?S ?d ?D ?p ?P ?o ?O ?r ?R ?f ?F). Here is the
precise meaning of each character:
n Numerically, by converting the beginning of the entry/item to a number.
a Alphabetically, ignoring the TODO keyword and the priority, if any.
o By order of TODO keywords.
t By date/time, either the first active time stamp in the entry, or, if
none exist, by the first inactive one.
s By the scheduled date/time.
d By deadline date/time.
c By creation time, which is assumed to be the first inactive time stamp
at the beginning of a line.
p By priority according to the cookie.
r By the value of a property.
Capital letters will reverse the sort order.
If the SORTING-TYPE is ?f or ?F, then GETKEY-FUNC specifies a function to be
called with point at the beginning of the record. It must return either
a string or a number that should serve as the sorting key for that record.
Comparing entries ignores case by default. However, with an optional argument
WITH-CASE, the sorting considers case as well."
(interactive "P")
(let ((case-func (if with-case 'identity 'downcase))
(cmstr
;; The clock marker is lost when using `sort-subr', let's
;; store the clocking string.
(when (equal (marker-buffer org-clock-marker) (current-buffer))
(save-excursion
(goto-char org-clock-marker)
(looking-back "^.*") (match-string-no-properties 0))))
start beg end stars re re2
txt what tmp)
;; Find beginning and end of region to sort
(cond
((org-region-active-p)
;; we will sort the region
(setq end (region-end)
what "region")
(goto-char (region-beginning))
(if (not (org-at-heading-p)) (outline-next-heading))
(setq start (point)))
((or (org-at-heading-p)
(condition-case nil (progn (org-back-to-heading) t) (error nil)))
;; we will sort the children of the current headline
(org-back-to-heading)
(setq start (point)
end (progn (org-end-of-subtree t t)
(or (bolp) (insert "\n"))
(org-back-over-empty-lines)
(point))
what "children")
(goto-char start)
(show-subtree)
(outline-next-heading))
(t
;; we will sort the top-level entries in this file
(goto-char (point-min))
(or (org-at-heading-p) (outline-next-heading))
(setq start (point))
(goto-char (point-max))
(beginning-of-line 1)
(when (looking-at ".*?\\S-")
;; File ends in a non-white line
(end-of-line 1)
(insert "\n"))
(setq end (point-max))
(setq what "top-level")
(goto-char start)
(show-all)))
(setq beg (point))
(if (>= beg end) (error "Nothing to sort"))
(looking-at "\\(\\*+\\)")
(setq stars (match-string 1)
re (concat "^" (regexp-quote stars) " +")
re2 (concat "^" (regexp-quote (substring stars 0 -1)) "[ \t\n]")
txt (buffer-substring beg end))
(if (not (equal (substring txt -1) "\n")) (setq txt (concat txt "\n")))
(if (and (not (equal stars "*")) (string-match re2 txt))
(error "Region to sort contains a level above the first entry"))
(unless sorting-type
(message
"Sort %s: [a]lpha [n]umeric [p]riority p[r]operty todo[o]rder [f]unc
[t]ime [s]cheduled [d]eadline [c]reated
A/N/P/R/O/F/T/S/D/C means reversed:"
what)
(setq sorting-type (read-char-exclusive))
(and (= (downcase sorting-type) ?f)
(setq getkey-func
(org-icompleting-read "Sort using function: "
obarray 'fboundp t nil nil))
(setq getkey-func (intern getkey-func)))
(and (= (downcase sorting-type) ?r)
(setq property
(org-icompleting-read "Property: "
(mapcar 'list (org-buffer-property-keys t))
nil t))))
(message "Sorting entries...")
(save-restriction
(narrow-to-region start end)
(let ((dcst (downcase sorting-type))
(case-fold-search nil)
(now (current-time)))
(sort-subr
(/= dcst sorting-type)
;; This function moves to the beginning character of the "record" to
;; be sorted.
(lambda nil
(if (re-search-forward re nil t)
(goto-char (match-beginning 0))
(goto-char (point-max))))
;; This function moves to the last character of the "record" being
;; sorted.
(lambda nil
(save-match-data
(condition-case nil
(outline-forward-same-level 1)
(error
(goto-char (point-max))))))
;; This function returns the value that gets sorted against.
(lambda nil
(cond
((= dcst ?n)
(if (looking-at org-complex-heading-regexp)
(string-to-number (match-string 4))
nil))
((= dcst ?a)
(if (looking-at org-complex-heading-regexp)
(funcall case-func (match-string 4))
nil))
((= dcst ?t)
(let ((end (save-excursion (outline-next-heading) (point))))
(if (or (re-search-forward org-ts-regexp end t)
(re-search-forward org-ts-regexp-both end t))
(org-time-string-to-seconds (match-string 0))
(org-float-time now))))
((= dcst ?c)
(let ((end (save-excursion (outline-next-heading) (point))))
(if (re-search-forward
(concat "^[ \t]*\\[" org-ts-regexp1 "\\]")
end t)
(org-time-string-to-seconds (match-string 0))
(org-float-time now))))
((= dcst ?s)
(let ((end (save-excursion (outline-next-heading) (point))))
(if (re-search-forward org-scheduled-time-regexp end t)
(org-time-string-to-seconds (match-string 1))
(org-float-time now))))
((= dcst ?d)
(let ((end (save-excursion (outline-next-heading) (point))))
(if (re-search-forward org-deadline-time-regexp end t)
(org-time-string-to-seconds (match-string 1))
(org-float-time now))))
((= dcst ?p)
(if (re-search-forward org-priority-regexp (point-at-eol) t)
(string-to-char (match-string 2))
org-default-priority))
((= dcst ?r)
(or (org-entry-get nil property) ""))
((= dcst ?o)
(if (looking-at org-complex-heading-regexp)
(- 9999 (length (member (match-string 2)
org-todo-keywords-1)))))
((= dcst ?f)
(if getkey-func
(progn
(setq tmp (funcall getkey-func))
(if (stringp tmp) (setq tmp (funcall case-func tmp)))
tmp)
(error "Invalid key function `%s'" getkey-func)))
(t (error "Invalid sorting type `%c'" sorting-type))))
nil
(cond
((= dcst ?a) 'string<)
((= dcst ?f) compare-func)
((member dcst '(?p ?t ?s ?d ?c)) '<)))))
(run-hooks 'org-after-sorting-entries-or-items-hook)
;; Reset the clock marker if needed
(when cmstr
(save-excursion
(goto-char start)
(search-forward cmstr nil t)
(move-marker org-clock-marker (point))))
(message "Sorting entries...done")))
(defun org-do-sort (table what &optional with-case sorting-type)
"Sort TABLE of WHAT according to SORTING-TYPE.
The user will be prompted for the SORTING-TYPE if the call to this
function does not specify it. WHAT is only for the prompt, to indicate
what is being sorted. The sorting key will be extracted from
the car of the elements of the table.
If WITH-CASE is non-nil, the sorting will be case-sensitive."
(unless sorting-type
(message
"Sort %s: [a]lphabetic, [n]umeric, [t]ime. A/N/T means reversed:"
what)
(setq sorting-type (read-char-exclusive)))
(let ((dcst (downcase sorting-type))
extractfun comparefun)
;; Define the appropriate functions
(cond
((= dcst ?n)
(setq extractfun 'string-to-number
comparefun (if (= dcst sorting-type) '< '>)))
((= dcst ?a)
(setq extractfun (if with-case (lambda(x) (org-sort-remove-invisible x))
(lambda(x) (downcase (org-sort-remove-invisible x))))
comparefun (if (= dcst sorting-type)
'string<
(lambda (a b) (and (not (string< a b))
(not (string= a b)))))))
((= dcst ?t)
(setq extractfun
(lambda (x)
(if (or (string-match org-ts-regexp x)
(string-match org-ts-regexp-both x))
(org-float-time
(org-time-string-to-time (match-string 0 x)))
0))
comparefun (if (= dcst sorting-type) '< '>)))
(t (error "Invalid sorting type `%c'" sorting-type)))
(sort (mapcar (lambda (x) (cons (funcall extractfun (car x)) (cdr x)))
table)
(lambda (a b) (funcall comparefun (car a) (car b))))))