├── org-sort-tasks.gif ├── .github └── FUNDING.yml ├── LICENSE ├── README.org └── org-sort-tasks.el /org-sort-tasks.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felipelalli/org-sort-tasks/HEAD/org-sort-tasks.gif -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # felipelalli 4 | patreon: DarrenHernandez 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Felipe Micaroni Lalli 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.org: -------------------------------------------------------------------------------- 1 | * Organize your TODO list 2 | You can sort an unsorted TODO list using mergesort with `org-sort-tasks`. The fn will try to find which task is more important taking into account deadline, scheduled time, priority and then, if everything is equal, finally ask to the user. 3 | 4 | When the list is already sorted, you can use fn `org-insert-sorted-todo-heading` to insert a task in a right position using binary search. 5 | 6 | ** org-insert-sorted-todo-heading 7 | An interactive fn that inserts a TODO heading in the right position in a pre-sorted list. Let the cursor above the root (parent) element. 8 | 9 | *WARNING:* If the list is unsorted, use `org-sort-tasks` first. 10 | ** org-sort-tasks 11 | An interactive fn that sorts a list of tasks in the selected region or under the headline on cursor. 12 | 13 | There are two main ways of use: 14 | 15 | 1) Let the cursor at any position of a root headline and press M-x org-sort-tasks. 16 | 2) Mark a region and use M-x org-sort-tasks. 17 | 18 | The user will be prompted to reply a simple question like \"Should 'xxx task' BE DONE BEFORE 'yyy task'?\". After reply some questions, the fn will open a new buffer and build a sorted list of tasks. It is very useful for who uses GTD method and work with huge unsorted lists of tasks. The number of questions will be in avg O(n log n). 19 | 20 | [[./org-sort-tasks.gif]] 21 | 22 | -------------------------------------------------------------------------------- /org-sort-tasks.el: -------------------------------------------------------------------------------- 1 | ;;; org-sort-tasks.el --- An easy way to sort your long TODO list. -*- lexical-binding: t -*- 2 | 3 | ;; Version: 3.0 4 | ;; Keywords: orgmode, sort, task, todo, ordered list 5 | 6 | ;; MIT License 7 | 8 | ;; Copyright (c) 2019 Felipe Micaroni Lalli 9 | 10 | ;; Permission is hereby granted, free of charge, to any person obtaining a copy 11 | ;; of this software and associated documentation files (the "Software"), to deal 12 | ;; in the Software without restriction, including without limitation the rights 13 | ;; to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | ;; copies of the Software, and to permit persons to whom the Software is 15 | ;; furnished to do so, subject to the following conditions: 16 | 17 | ;; The above copyright notice and this permission notice shall be included in all 18 | ;; copies or substantial portions of the Software. 19 | 20 | ;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | ;; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | ;; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | ;; AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | ;; LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | ;; OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | ;; SOFTWARE. 27 | 28 | ;; -*- lexical-binding:t -*- 29 | 30 | ;; 31 | ;; CUSTOMIZATION 32 | ;; 33 | 34 | (defcustom org-sort-tasks-algo 'org-sort-tasks/adapted-merge-sort 35 | "The sorting fn. Prefer functions that reduce the number of COMPARISONS and is good for short and pre-sorted lists." 36 | :type '(symbol) 37 | :group 'convenience) 38 | 39 | ;; 40 | ;; COMMON CODE 41 | ;; 42 | 43 | (setq max-lisp-eval-depth 50000) 44 | (setq max-specpdl-size 50000) 45 | 46 | (defun org-sort-tasks/create-counter () 47 | (let ((c 0)) 48 | (lambda () 49 | (setq c (+ c 1)) 50 | c))) 51 | 52 | (defun org-sort-tasks/nshuffle (sequence) 53 | (loop for i from (length sequence) downto 2 54 | do (rotatef (elt sequence (random i)) 55 | (elt sequence (1- i)))) 56 | sequence) 57 | 58 | (defun org-sort-tasks/slightly-nshuffle (factor sequence) 59 | (loop for i to (min 2 (/ (length sequence) factor)) 60 | do (rotatef (elt sequence (random (length sequence))) 61 | (elt sequence (random (length sequence))))) 62 | sequence) 63 | 64 | (defun org-sort-tasks/sort-counting (sort-algo lst) 65 | (let* ((counter (org-sort-tasks/create-counter)) 66 | (result (apply sort-algo (list lst (lambda (a b) 67 | (funcall counter) 68 | (< a b)))))) 69 | (/ (* (funcall counter) 1.0) 70 | (length result)))) 71 | 72 | (defun org-sort-tasks/sort-counting-avg (sort-algo lst-generator) 73 | (/ (seq-reduce #'+ 74 | (mapcar (lambda (c) 75 | (org-sort-tasks/sort-counting 76 | sort-algo 77 | (funcall lst-generator))) 78 | (cl-loop for x from 1 to 512 collect x)) 0) 79 | 512.0)) 80 | 81 | (defun org-sort-tasks/merge (lst1 lst2 comparison-fn) 82 | "If lst1 < lst2 then merge immediately." 83 | (if (or (null lst1) 84 | (null lst2)) 85 | (append lst1 lst2) 86 | (if (not (funcall comparison-fn 87 | (car lst2) 88 | (car (last lst1)))) 89 | (append lst1 lst2) 90 | (let ((i 0) 91 | (j 0) 92 | (result '())) 93 | (cl-labels ((go-next () 94 | (cond ((>= i (length lst1)) 95 | (setq result (append result (seq-drop lst2 j)))) 96 | ((>= j (length lst2)) 97 | (setq result (append result (seq-drop lst1 i)))) 98 | (t (progn 99 | (if (funcall comparison-fn 100 | (nth i lst1) 101 | (nth j lst2)) 102 | (progn 103 | (setq result (append result (list (nth i lst1)))) 104 | (setq i (+ i 1))) 105 | (progn 106 | (setq result (append result (list (nth j lst2)))) 107 | (setq j (+ j 1)))) 108 | (go-next)))))) 109 | (go-next) 110 | result))))) 111 | 112 | (defun org-sort-tasks/insert-in-the-middle (lst pos element) 113 | (append (seq-take lst pos) (list element) (seq-drop lst pos))) 114 | 115 | (defun org-sort-tasks/insert-sort (lst comparison-fn) 116 | (let ((result '())) 117 | (cl-labels ((go-next (i) 118 | (cond ((>= i (length lst)) 119 | result) 120 | ((null result) 121 | (setq result (list (nth i lst))) 122 | (go-next (+ i 1))) 123 | (t (cl-labels ((go-backward (j) 124 | (cond ((< j 0) 125 | (setq result (append (list (nth i lst)) 126 | result))) 127 | ((funcall comparison-fn 128 | (nth i lst) 129 | (nth j result)) 130 | (go-backward (- j 1))) 131 | (t (setq result 132 | (org-sort-tasks/insert-in-the-middle 133 | result (+ j 1) (nth i lst))))))) 134 | (go-backward (- (length result) 1)) 135 | (go-next (+ i 1))))))) 136 | (go-next 0)))) 137 | 138 | (defun org-sort-tasks/adapted-merge-sort (lst comparison-fn) 139 | (if (<= (length lst) 8) 140 | (org-sort-tasks/insert-sort lst comparison-fn) 141 | (org-sort-tasks/merge (org-sort-tasks/adapted-merge-sort (seq-take lst (/ (length lst) 2)) comparison-fn) 142 | (org-sort-tasks/adapted-merge-sort (seq-drop lst (/ (length lst) 2)) comparison-fn) 143 | comparison-fn))) 144 | 145 | (defun org-sort-tasks/test-sort-algo (sort-algo) 146 | (let ((a (org-sort-tasks/sort-counting sort-algo (cl-loop for x from 1 to 16 collect x))) 147 | (b (org-sort-tasks/sort-counting sort-algo (cl-loop for x from 1 to 64 collect x))) 148 | (c (org-sort-tasks/sort-counting sort-algo (cl-loop for x from 16 downto 1 collect x))) 149 | (d (org-sort-tasks/sort-counting sort-algo (cl-loop for x from 64 downto 1 collect x))) 150 | (e (org-sort-tasks/sort-counting-avg sort-algo (lambda () 151 | (org-sort-tasks/nshuffle 152 | (cl-loop for x from 1 to 16 collect x))))) 153 | (f (org-sort-tasks/sort-counting-avg sort-algo (lambda () 154 | (org-sort-tasks/nshuffle 155 | (cl-loop for x from 1 to 64 collect x))))) 156 | (g (org-sort-tasks/sort-counting-avg sort-algo (lambda () 157 | (org-sort-tasks/slightly-nshuffle 6 158 | (cl-loop for x from 1 to 16 collect x))))) 159 | (h (org-sort-tasks/sort-counting-avg sort-algo (lambda () 160 | (org-sort-tasks/slightly-nshuffle 6 161 | (cl-loop for x from 1 to 64 collect x))))) 162 | (i (org-sort-tasks/sort-counting-avg sort-algo (lambda () 163 | (org-sort-tasks/slightly-nshuffle 6 164 | (cl-loop for x from 1 to 512 collect x))))) 165 | (j (org-sort-tasks/sort-counting-avg sort-algo (lambda () 166 | (org-sort-tasks/slightly-nshuffle 12 167 | (cl-loop for x from 1 to 64 collect x))))) 168 | ) 169 | (message (format " 170 | Short sorted list:............... %s 171 | Long sorted list:................ %s 172 | Short reversed list:............. %s 173 | Long reversed list:.............. %s 174 | Short shuffled list:............. %s 175 | Long shuffled list:.............. %s 176 | Short slightly shuffled list: (*) %s 177 | Long slightly shuffled list:..... %s 178 | Very long slightly shuffled list: %s 179 | L v. slightly shuffled list (*):. %s 180 | AVG.............................: %s 181 | " a b c d e f g h i j (/ (+ a b c d e f g h i j) 10.0))))) 182 | 183 | 184 | ;(org-sort-tasks/test-sort-algo 'sort) 185 | ;(org-sort-tasks/test-sort-algo 'org-sort-tasks/insert-sort) 186 | ;(org-sort-tasks/test-sort-algo 'org-sort-tasks/adapted-merge-sort) 187 | 188 | (defun sort-tasks/timestamp-obj=? (ts1 ts2) 189 | "Compare two timestamp object and returns true if they are equal until day level." 190 | (and (= (org-element-property :year-start ts1) 191 | (org-element-property :year-start ts2)) 192 | (= (org-element-property :month-start ts1) 193 | (org-element-property :month-start ts2)) 194 | (= (org-element-property :day-start ts1) 195 | (org-element-property :day-start ts2)))) 196 | 197 | (defun sort-tasks/timestamp-obj p1 p2) nil) 232 | (t (not (with-local-quit 233 | (sort-tasks/sort/interactive 234 | (car (org-element-property :title task1)) 235 | (car (org-element-property :title task2))))))))) 236 | 237 | ;; 238 | ;; SORT A LIST OF TASKS 239 | ;; 240 | 241 | (defun sort-tasks/sort-list (task-list) 242 | (apply org-sort-tasks-algo (list task-list 'sort-tasks/sort))) 243 | 244 | (defun sort-tasks/sort-children (final-buffer element) 245 | "This fn receives a root element and sort all its children. 246 | 247 | Note: sort-tasks/sort-children is private and it is used by the main org-sort-tasks fn." 248 | (let* ((list-of-tasks 249 | (org-element-map element 'headline 250 | (lambda (task) 251 | (if (and (= (+ (org-element-property :level element) 1) 252 | (org-element-property :level task))) 253 | task 254 | nil)))) 255 | (aprox-steps (ceiling (* (length list-of-tasks) (log (max 1 (length list-of-tasks)) 5))))) 256 | (let ((sorted-list (sort-tasks/sort-list list-of-tasks))) 257 | (with-current-buffer final-buffer (insert (format "* %s\n" (car (org-element-property :title element))))) 258 | (mapcar (lambda (c) 259 | (let ((task-content (buffer-substring (org-element-property :begin c) 260 | (org-element-property :end c)))) 261 | (with-current-buffer final-buffer 262 | (insert (format "%s" task-content))))) 263 | sorted-list) 264 | t))) 265 | 266 | (defun org-sort-tasks/main () 267 | (let ((final-buffer (generate-new-buffer "*sorted-tasks*")) 268 | (no-selection (not (use-region-p))) 269 | (inhibit-quit t)) ; If C-g is pressed then try to build a partial sorted list. 270 | (with-current-buffer final-buffer (erase-buffer)) 271 | (when no-selection 272 | (beginning-of-line) 273 | (org-mark-subtree)) 274 | (deactivate-mark) 275 | (save-restriction 276 | (narrow-to-region (region-beginning) (region-end)) 277 | (beginning-of-buffer) 278 | (let ((first-element (org-element-at-point))) 279 | (if (not (eq (org-element-type first-element) 'headline)) 280 | (error "The first element must be a headline.") 281 | (let ((result-list 282 | (org-element-map (org-element-parse-buffer) 'headline 283 | (lambda (task) 284 | (when (= (org-element-property :level first-element) 285 | (org-element-property :level task)) 286 | (sort-tasks/sort-children final-buffer task)))))) 287 | (if (null result-list) 288 | (message "Aborted.") 289 | (progn 290 | (switch-to-buffer final-buffer) 291 | (beginning-of-buffer) 292 | (org-mode) 293 | (org-cycle) 294 | (message "Done! A sorted list was built and opened in a new disposable buffer."))))))))) 295 | 296 | (defun org-sort-tasks () 297 | "An interactive fn that sorts a list of tasks in the selected region or under the headline on cursor. 298 | 299 | There are two main ways of use: 300 | 301 | 1) Let the cursor at any position of a root headline and press M-x org-sort-tasks. 302 | 2) Mark a region and use M-x org-sort-tasks. 303 | 304 | The user will be prompted to reply a simple question like \"Should 'xxx task' BE DONE BEFORE 'yyy task'?\". After reply some questions, the fn will open a new buffer and build a sorted list of tasks. It is very useful for who uses GTD method and work with huge unsorted lists of tasks. The number of questions will be in avg O(n log n)." 305 | (interactive) 306 | (org-sort-tasks/main)) 307 | 308 | ;; 309 | ;; INSERT A NEW TASK 310 | ;; 311 | 312 | (defun org-insert-sorted-todo-heading/insert (position before) 313 | (goto-char position) 314 | (beginning-of-line) 315 | (if before 316 | (org-insert-todo-heading nil) 317 | (org-insert-todo-heading-respect-content)) 318 | (message "Done!")) 319 | 320 | (defun org-insert-sorted-todo-heading/insert-in-right-position-using-binary-search 321 | (task-short-description task-list) 322 | (when (< (length task-list) 1) 323 | (error "The list is empty.")) 324 | (let ((task-short-description (if (and task-short-description 325 | (not (string= "" task-short-description))) 326 | task-short-description 327 | "THE NEW TASK"))) 328 | (cond ((= (length task-list) 1) 329 | (org-insert-sorted-todo-heading/insert 330 | (org-element-property :begin (car task-list)) 331 | (sort-tasks/sort/interactive 332 | (car (org-element-property :title (car task-list))) 333 | task-short-description))) 334 | (t (let* ((pivot (/ (length task-list) 2)) 335 | (left-list (butlast task-list (- (length task-list) pivot))) 336 | (right-list (nthcdr (+ pivot 1) task-list))) 337 | (if (sort-tasks/sort/interactive 338 | (car (org-element-property :title (nth pivot task-list))) 339 | task-short-description) 340 | (org-insert-sorted-todo-heading/insert-in-right-position-using-binary-search 341 | task-short-description 342 | left-list) 343 | (if (null right-list) 344 | (org-insert-sorted-todo-heading/insert (org-element-property :begin (nth pivot task-list)) nil) 345 | (org-insert-sorted-todo-heading/insert-in-right-position-using-binary-search 346 | task-short-description 347 | right-list)))))))) 348 | 349 | (defun org-insert-sorted-todo-heading/main (task-short-description) 350 | (if (use-region-p) 351 | (error "Do not mark. Just let the cursor at some root heading.") 352 | (progn 353 | (beginning-of-line) 354 | (org-mark-subtree) 355 | (next-line) 356 | (deactivate-mark) 357 | (save-restriction 358 | (narrow-to-region (region-beginning) (region-end)) 359 | (beginning-of-buffer) 360 | (let ((first-element (org-element-at-point))) 361 | (if (not (eq (org-element-type first-element) 'headline)) 362 | (error "The first element must be a headline.") 363 | (org-insert-sorted-todo-heading/insert-in-right-position-using-binary-search 364 | task-short-description 365 | (org-element-contents (org-element-parse-buffer)))))) 366 | (recenter-top-bottom) 367 | (when task-short-description 368 | (insert task-short-description))))) 369 | 370 | (defun org-insert-sorted-todo-heading (task-short-description) 371 | "An interactive fn that inserts a TODO heading in the right position in a pre-sorted list. Let the cursor above the root (parent) element. 372 | 373 | *WARNING:* If the list is unsorted, use `org-sort-tasks` first." 374 | (interactive "sType the task short description: ") 375 | (org-insert-sorted-todo-heading/main task-short-description)) 376 | 377 | ;; Export 378 | 379 | (provide 'org-sort-tasks) 380 | (provide 'org-insert-sorted-todo-heading) 381 | 382 | 383 | --------------------------------------------------------------------------------