Take the 2-minute tour ×
Super User is a question and answer site for computer enthusiasts and power users. It's 100% free, no registration required.

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 the following thread (i.e., moving the undated tasks to a different heading in one fell swoop): How to automate org-refile for multiple todo


EDIT: The following code looks promising, but needs some TLC and a workaround for the undated todo bug:

;; 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-*$")
          (kill-whole-line)
)
          (goto-char start)
;;          (dotimes (x ) (org-cycle))
          )))))

EDIT: This function is also showing promise and I will report back when I've done more testing.

(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: For the adventurous souls interested in fixing the 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))))))
share|improve this question

1 Answer 1

up vote 0 down vote accepted

EDIT (March 7, 2014):  See also the following related thread:

http://stackoverflow.com/a/22232709/2112489

EDIT (January 13, 2014):  With respect to the function org-sort-entries, the subsections relating to time / scheduled / deadline / created use (org-float-time now) aka (org-float-time (current-time)) for todo tasks that do not contain dates matching the regexp in relation thereto. Therefore, tasks without dates will be weighted as being equal to today -- and this causes them to be sorted prior to tasks with upcoming dates. To weight the tasks without dates so that they are sorted after tasks with dates, it is possible to give them a date so far off into the future that they will be sorted after the dated taks -- e.g., setting the value to the year 2030, like this: (org-time-string-to-seconds "<2030-12-31 Tue>"). This, of course, will require creating an alternate function or a defalias. So what appeared to be a bug was actually meant by design, however, I prefer that undated tasks not be weighted as being equal to today.

For anyone battling with org-tags-view and org-scan-tags and org-agenda-list and org-agenda-get-deadlines and other related functions, if you will take a look at the section relating to org-add-props txt props, you will see that the org team has not provided enough text properties to sort the todo tasks with all the various options listed in defcustom org-agenda-sorting-strategy -- so before driving yourself crazy trying to figure out why all the sorting options don't work, first take a look to see if there are even text properties assigned to deal with those sorting options -- if not, then you will need spend time modifying things if you want those options to be available. See the following thread for an example of a comprehensive regexp that can be used to isolate parts of a todo task: http://stackoverflow.com/a/20960301/2112489

EDIT (July 11, 2013):  First working draft. NOTE: The undated tasks need to be refiled elsewhere (bug workaround) to avoid sorting by deadline a group containing both dated and undated tasks. See the following link as an example to accomplish the aforementioned deadline sort bug workaround: How to automate org-refile for multiple todo

(defun lawlist-sort ()
    (interactive)
    (goto-char (point-min))
    (while
    (re-search-forward "^\* " nil t) ;; sorts the whole kitten caboodle
        (goto-char (match-beginning 0))
        (condition-case err
            (progn
                (org-sort-entries t ?a)
                (org-sort-entries t ?o)
                (org-sort-entries t ?p)
                (org-sort-entries t ?t)
                (forward-line))
            (error nil)
        )
    )
    (goto-char (point-min))
    (while
        (re-search-forward "^\* ARCHIVES" nil t)
        (goto-char (match-beginning 0))
        (condition-case err
           (progn
                (org-sort-entries t ?a)
                (org-sort-entries t ?o)
                (org-sort-entries t ?p)
                (forward-line))
        (error nil)
        )
    ) )
share|improve this answer

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.