* [PATCH] lisp/org.el: Add ability to sort tags by hierarchy
@ 2024-06-15 12:35 Morgan Smith
2024-06-15 14:25 ` Ihor Radchenko
0 siblings, 1 reply; 7+ messages in thread
From: Morgan Smith @ 2024-06-15 12:35 UTC (permalink / raw)
To: emacs-orgmode; +Cc: Morgan Smith
* lisp/org.el (org-tags-sort-hierarchy): New function.
(org-tags-sort-function): Add new function to type.
* testing/lisp/test-org.el (test-org/tags-sort-hierarchy): New test
---
This is one of those things that I thought would be easy but then ended up
hard.
I wrote this so that items in my agenda would sort nicely. Items tagged in the
same hierarchy would end up next to each other.
lisp/org.el | 38 +++++++++++++++++++++++++++++++++++++-
testing/lisp/test-org.el | 19 +++++++++++++++++++
2 files changed, 56 insertions(+), 1 deletion(-)
diff --git a/lisp/org.el b/lisp/org.el
index 750b060f3..b828f4127 100644
--- a/lisp/org.el
+++ b/lisp/org.el
@@ -2955,7 +2955,8 @@ is better to limit inheritance to certain tags using the variables
(const :tag "No sorting" nil)
(const :tag "Alphabetical" org-string<)
(const :tag "Reverse alphabetical" org-string>)
- (function :tag "Custom function" nil)))
+ (const :tag "Hierarchy" org-tags-sort-hierarchy)
+ (function :tag "Custom function" nil)))
(defvar org-tags-history nil
"History of minibuffer reads for tags.")
@@ -4262,6 +4263,41 @@ See `org-tag-alist' for their structure."
;; Preserve order of ALIST1.
(append (nreverse to-add) alist2)))))
+(defun org-tags-sort-hierarchy (tag1 tag2)
+ "Sort tags TAG1 and TAG2 by the tag hierarchy.
+Sorting is done alphabetically. This function is intended to be a value
+of `org-tags-sort-function'."
+ (let ((sort-func #'org-string<)
+ (group-alist (or org-tag-groups-alist-for-agenda
+ org-tag-groups-alist)))
+ (if (not (and org-group-tags
+ group-alist))
+ (funcall sort-func tag1 tag2)
+ (let* ((tag-path-function
+ ;; Returns a list of tags describing the tag path
+ ;; ex: '("top level tag" "second level" "tag")
+ (lambda (tag)
+ (let ((result (list tag)))
+ (while (setq tag
+ (map-some
+ (lambda (key tags)
+ (when (and (member tag tags)
+ ;; infinite loop (only catches the trivial case)
+ (not (string-equal tag key)))
+ key))
+ group-alist))
+ (push tag result))
+ result)))
+ (tag1-path (funcall tag-path-function tag1))
+ (tag2-path (funcall tag-path-function tag2)))
+ ;; value< was added in Emacs 30
+ ;; (value< tag1-path tag2-path)
+ (catch :result
+ (dotimes (n (min (length tag1-path) (length tag2-path)))
+ (unless (string-equal (nth n tag1-path) (nth n tag2-path))
+ (throw :result (funcall sort-func (nth n tag1-path) (nth n tag2-path)))))
+ (< (length tag1-path) (length tag2-path)))))))
+
(defun org-priority-to-value (s)
"Convert priority string S to its numeric value."
(or (save-match-data
diff --git a/testing/lisp/test-org.el b/testing/lisp/test-org.el
index f21e52bfd..59b16a62a 100644
--- a/testing/lisp/test-org.el
+++ b/testing/lisp/test-org.el
@@ -8508,6 +8508,25 @@ Paragraph<point>"
(org-mode-restart)
(let ((org-tag-alist-for-agenda nil)) (org-tags-expand "{A+}"))))))
+(ert-deftest test-org/tags-sort-hierarchy ()
+ "Test `org-tags-sort-hierarchy' specifications."
+ (let ((org-tag-groups-alist-for-agenda
+ '(("A" "B" "D" "z" "zz")
+ ("B" "y")
+ ("C" "x")
+ ("D" "w")
+ ("E" "C" "v")))
+ (test-list '("v" "w" "x" "y" "zz" "z" "E" "D" "C" "B" "A")))
+ (should (equal
+ '("A" "B" "y" "D" "w" "z" "zz" "E" "C" "x" "v")
+ (sort test-list #'org-tags-sort-hierarchy))))
+ ;; infinite loop (tag "A" should not be in the "A" group)
+ (let ((org-tag-groups-alist-for-agenda
+ '(("A" "A" "B")))
+ (test-list '("B" "A")))
+ (should (equal
+ '("A" "B")
+ (sort test-list #'org-tags-sort-hierarchy)))))
\f
;;; TODO keywords
--
2.45.1
^ permalink raw reply related [flat|nested] 7+ messages in thread
* Re: [PATCH] lisp/org.el: Add ability to sort tags by hierarchy
2024-06-15 12:35 [PATCH] lisp/org.el: Add ability to sort tags by hierarchy Morgan Smith
@ 2024-06-15 14:25 ` Ihor Radchenko
2024-08-28 15:39 ` Ihor Radchenko
2024-12-24 21:48 ` Morgan Smith
0 siblings, 2 replies; 7+ messages in thread
From: Ihor Radchenko @ 2024-06-15 14:25 UTC (permalink / raw)
To: Morgan Smith; +Cc: emacs-orgmode
Morgan Smith <Morgan.J.Smith@outlook.com> writes:
> * lisp/org.el (org-tags-sort-hierarchy): New function.
> (org-tags-sort-function): Add new function to type.
> * testing/lisp/test-org.el (test-org/tags-sort-hierarchy): New test
> ---
>
> This is one of those things that I thought would be easy but then ended up
> hard.
>
> I wrote this so that items in my agenda would sort nicely. Items tagged in the
> same hierarchy would end up next to each other.
> + "Sort tags TAG1 and TAG2 by the tag hierarchy.
> +Sorting is done alphabetically. This function is intended to be a value
> +of `org-tags-sort-function'."
Thanks for the patch, but may you please elaborate what kind of sorting
order does your function imply? In particular, I am wondering about the
ordering of tags from different groups? Also, what happens when there
are no tag groups defined? (These questions should be answered in the
docstring and the etc/ORG-NEWS entry you need to add, announcing the new
feature)
Also, sorting may not be alphabetical, depending on the value of
`org-sort-function'.
--
Ihor Radchenko // yantar92,
Org mode contributor,
Learn more about Org mode at <https://orgmode.org/>.
Support Org development at <https://liberapay.com/org-mode>,
or support my work at <https://liberapay.com/yantar92>
^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [PATCH] lisp/org.el: Add ability to sort tags by hierarchy
2024-06-15 14:25 ` Ihor Radchenko
@ 2024-08-28 15:39 ` Ihor Radchenko
2024-08-28 16:10 ` Morgan Smith
2024-12-24 21:48 ` Morgan Smith
1 sibling, 1 reply; 7+ messages in thread
From: Ihor Radchenko @ 2024-08-28 15:39 UTC (permalink / raw)
To: Morgan Smith; +Cc: emacs-orgmode
Ihor Radchenko <yantar92@posteo.net> writes:
> ... may you please elaborate what kind of sorting
> order does your function imply? ...
It has been over a month. Have you had a chance to look into my
questions?
--
Ihor Radchenko // yantar92,
Org mode contributor,
Learn more about Org mode at <https://orgmode.org/>.
Support Org development at <https://liberapay.com/org-mode>,
or support my work at <https://liberapay.com/yantar92>
^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [PATCH] lisp/org.el: Add ability to sort tags by hierarchy
2024-08-28 15:39 ` Ihor Radchenko
@ 2024-08-28 16:10 ` Morgan Smith
2024-09-01 16:23 ` Ihor Radchenko
0 siblings, 1 reply; 7+ messages in thread
From: Morgan Smith @ 2024-08-28 16:10 UTC (permalink / raw)
To: Ihor Radchenko; +Cc: emacs-orgmode
Ihor Radchenko <yantar92@posteo.net> writes:
> Ihor Radchenko <yantar92@posteo.net> writes:
>
>> ... may you please elaborate what kind of sorting
>> order does your function imply? ...
>
> It has been over a month. Have you had a chance to look into my
> questions?
After you asked that question, I decided to demonstrate the changes I
wanted to make by also submitting a test. However, I thought it prudent
to first test what we already have. So I was waiting for a response to
my other patch (linked below) adding that test before continuing with
this thread.
I didn't make my intentions clear. Sorry about that. Thanks for
following up! I appreciate that.
https://list.orgmode.org/CH3PR84MB342464F6458F91EE5800545AC5C32@CH3PR84MB3424.NAMPRD84.PROD.OUTLOOK.COM/
^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [PATCH] lisp/org.el: Add ability to sort tags by hierarchy
2024-08-28 16:10 ` Morgan Smith
@ 2024-09-01 16:23 ` Ihor Radchenko
0 siblings, 0 replies; 7+ messages in thread
From: Ihor Radchenko @ 2024-09-01 16:23 UTC (permalink / raw)
To: Morgan Smith; +Cc: emacs-orgmode
Morgan Smith <morgan.j.smith@outlook.com> writes:
>> It has been over a month. Have you had a chance to look into my
>> questions?
>
> After you asked that question, I decided to demonstrate the changes I
> wanted to make by also submitting a test. However, I thought it prudent
> to first test what we already have. So I was waiting for a response to
> my other patch (linked below) adding that test before continuing with
> this thread.
I replied in that thread.
We can come back to the patch herein after finalizing the other patch.
--
Ihor Radchenko // yantar92,
Org mode contributor,
Learn more about Org mode at <https://orgmode.org/>.
Support Org development at <https://liberapay.com/org-mode>,
or support my work at <https://liberapay.com/yantar92>
^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [PATCH] lisp/org.el: Add ability to sort tags by hierarchy
2024-06-15 14:25 ` Ihor Radchenko
2024-08-28 15:39 ` Ihor Radchenko
@ 2024-12-24 21:48 ` Morgan Smith
2024-12-25 8:17 ` Ihor Radchenko
1 sibling, 1 reply; 7+ messages in thread
From: Morgan Smith @ 2024-12-24 21:48 UTC (permalink / raw)
To: Ihor Radchenko; +Cc: emacs-orgmode
[-- Attachment #1: Type: text/plain, Size: 1701 bytes --]
Hello!
I have updated the patch with many improvements and tests.
Ihor Radchenko <yantar92@posteo.net> writes:
> Morgan Smith <Morgan.J.Smith@outlook.com> writes:
>
>> * lisp/org.el (org-tags-sort-hierarchy): New function.
>> (org-tags-sort-function): Add new function to type.
>> * testing/lisp/test-org.el (test-org/tags-sort-hierarchy): New test
>> ---
>>
>> This is one of those things that I thought would be easy but then ended up
>> hard.
>>
>> I wrote this so that items in my agenda would sort nicely. Items tagged in the
>> same hierarchy would end up next to each other.
>
>> + "Sort tags TAG1 and TAG2 by the tag hierarchy.
>> +Sorting is done alphabetically. This function is intended to be a value
>> +of `org-tags-sort-function'."
>
> Thanks for the patch, but may you please elaborate what kind of sorting
> order does your function imply? In particular, I am wondering about the
> ordering of tags from different groups?
Hopefully the test case I added shows how the sorting work. The tags
end up sorted as this:
("group_a" "tag_a_1" "tag_a_2" "groupless" "lonely" "tag_b_1" "tag_b_2")
Things end up with other things of their group. Things higher in the
heirarchy end up earlier. When things are on the same level (like
tag_a_1 and tag_a_2 or group_a and groupless) they are sorted using
`org-sort-function'.
> Also, what happens when there
> are no tag groups defined? (These questions should be answered in the
> docstring and the etc/ORG-NEWS entry you need to add, announcing the new
> feature)
Fallback to `org-sort-function'.
> Also, sorting may not be alphabetical, depending on the value of
> `org-sort-function'.
I have corrected this in the documentation.
[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: 0001-lisp-org.el-Add-ability-to-sort-tags-by-hierarchy.patch --]
[-- Type: text/x-patch, Size: 6912 bytes --]
From 3f25374bbfd4134cea6ce0708633d500e8b41a89 Mon Sep 17 00:00:00 2001
From: Morgan Smith <Morgan.J.Smith@outlook.com>
Date: Fri, 14 Jun 2024 17:38:41 -0400
Subject: [PATCH] lisp/org.el: Add ability to sort tags by hierarchy
* lisp/org.el (org-tags-sort-hierarchy): New function.
(org-tags-sort-function): Add new function to type.
* testing/lisp/test-org-agenda.el (test-org-agenda/tags-sorting): Test
sorting with a value of 'org-tags-sort-hierarchy.
* etc/ORG-NEWS: Announce the new feature.
---
etc/ORG-NEWS | 7 ++++++
lisp/org.el | 41 ++++++++++++++++++++++++++++++++-
testing/lisp/test-org-agenda.el | 32 ++++++++++++++++++++-----
3 files changed, 73 insertions(+), 7 deletions(-)
diff --git a/etc/ORG-NEWS b/etc/ORG-NEWS
index 4c41f981c..62e8bb4ca 100644
--- a/etc/ORG-NEWS
+++ b/etc/ORG-NEWS
@@ -196,6 +196,13 @@ English. The default value is ~t~ as the CSL standard assumes that
English titles are specified in sentence-case but the bibtex
bibliography format requires them to be written in title-case.
+*** New tags sorting function ~org-tags-sort-hierarchy~
+
+By setting ~org-tags-sort-function~ to ~org-tags-sort-hierarchy~, tags
+are sorted taking their hierarchy into account. See ~org-tag-alist~
+for how to set up a tag hierarchy. Secondary sorting is done using
+~org-sort-function~.
+
** New functions and changes in function arguments
# This also includes changes in function behavior from Elisp perspective.
diff --git a/lisp/org.el b/lisp/org.el
index 748f258a2..6f5bf066d 100644
--- a/lisp/org.el
+++ b/lisp/org.el
@@ -231,6 +231,7 @@ Stars are put in group 1 and the trimmed body in group 2.")
(defvar org-element--timestamp-regexp)
(defvar org-indent-indentation-per-level)
(defvar org-radio-target-regexp)
+(defvar org-sort-function)
(defvar org-target-link-regexp)
(defvar org-target-regexp)
(defvar org-id-overriding-file-name)
@@ -2966,7 +2967,8 @@ default."
(const :tag "Default sorting" nil)
(const :tag "Alphabetical" org-string<)
(const :tag "Reverse alphabetical" org-string>)
- (function :tag "Custom function" nil)))
+ (const :tag "Sort by hierarchy" org-tags-sort-hierarchy)
+ (function :tag "Custom function" nil)))
(defvar org-tags-history nil
"History of minibuffer reads for tags.")
@@ -4275,6 +4277,43 @@ See `org-tag-alist' for their structure."
;; Preserve order of ALIST1.
(append (nreverse to-add) alist2)))))
+(defun org-tags-sort-hierarchy (tag1 tag2)
+ "Sort tags TAG1 and TAG2 by the tag hierarchy.
+See `org-tag-alist' for how to set up a tag hierarchy. Secondary
+sorting is done using `org-sort-function'. This function is intended to
+be a value of `org-tags-sort-function'."
+ (let ((group-alist (or org-tag-groups-alist-for-agenda
+ org-tag-groups-alist)))
+ (if (not (and org-group-tags
+ group-alist))
+ (funcall org-sort-function tag1 tag2)
+ (let* ((tag-path-function
+ ;; Returns a list of tags describing the tag path
+ ;; ex: '("top level tag" "second level" "tag")
+ (lambda (tag)
+ (let ((result (list tag)))
+ (while (setq tag
+ (map-some
+ (lambda (key tags)
+ (when (and (member tag tags)
+ ;; Prevent infinite loop
+ (not (member tag (cdr result))))
+ key))
+ group-alist))
+ (push tag result))
+ result)))
+ (tag1-path (funcall tag-path-function tag1))
+ (tag2-path (funcall tag-path-function tag2)))
+ ;; value< was added in Emacs 30 and does not allow us to use
+ ;; `org-sort-function'.
+ ;; (value< tag1-path tag2-path)
+ (catch :result
+ (dotimes (n (min (length tag1-path) (length tag2-path)))
+ ;; find the first difference and sort on that
+ (unless (string-equal (nth n tag1-path) (nth n tag2-path))
+ (throw :result (funcall org-sort-function (nth n tag1-path) (nth n tag2-path)))))
+ (< (length tag1-path) (length tag2-path)))))))
+
(defun org-priority-to-value (s)
"Convert priority string S to its numeric value."
(or (save-match-data
diff --git a/testing/lisp/test-org-agenda.el b/testing/lisp/test-org-agenda.el
index 06d5abc43..d623389d4 100644
--- a/testing/lisp/test-org-agenda.el
+++ b/testing/lisp/test-org-agenda.el
@@ -663,18 +663,34 @@ Sunday 7 January 2024
(org-agenda-overriding-header "")
(org-agenda-prefix-format "")
(org-agenda-remove-tags t)
- (org-agenda-sorting-strategy '(tag-up)))))))
+ (org-agenda-sorting-strategy '(tag-up))))))
+ (org-tag-alist
+ '((:startgrouptag)
+ ("group_a")
+ (:grouptags)
+ ("tag_a_1")
+ ("tag_a_2")
+ ("group_a") ;; try to create infinite loop
+ (:endgrouptag)
+ (:startgroup)
+ ("tag_b_1")
+ ("tag_b_1") ;; duplicated
+ ("tag_b_2")
+ (:endgroup)
+ ("groupless")
+ ("lonely"))))
(org-test-agenda-with-agenda
(string-join
'("* TODO group_a :group_a:"
- "* TODO tag_a_1 :tag_a_1:"
+ "* TODO groupless :groupless:"
"* TODO tag_a_2 :tag_a_2:"
- "* TODO tag_b_1 :tag_b_1:"
"* TODO tag_b_2 :tag_b_2:"
- "* TODO groupless :groupless:"
+ "* TODO tag_a_1 :tag_a_1:"
+ "* TODO tag_b_1 :tag_b_1:"
"* TODO lonely :lonely:")
"\n")
- (dolist (org-tags-sort-function '(nil org-string< org-string> ignore))
+ (dolist (org-tags-sort-function '(nil org-string< org-string>
+ ignore org-tags-sort-hierarchy))
(should
(string-equal
(string-trim
@@ -685,7 +701,7 @@ Sunday 7 January 2024
;; Not sorted
('ignore
(string-join
- '("group_a" "tag_a_1" "tag_a_2" "tag_b_1" "tag_b_2" "groupless" "lonely")
+ '("group_a" "groupless" "tag_a_2" "tag_b_2" "tag_a_1" "tag_b_1" "lonely")
"\n"))
((or 'nil 'org-string<)
(string-join
@@ -694,6 +710,10 @@ Sunday 7 January 2024
('org-string>
(string-join
'("tag_b_2" "tag_b_1" "tag_a_2" "tag_a_1" "lonely" "groupless" "group_a")
+ "\n"))
+ ('org-tags-sort-hierarchy
+ (string-join
+ '("group_a" "tag_a_1" "tag_a_2" "groupless" "lonely" "tag_b_1" "tag_b_2")
"\n")))))))))
(ert-deftest test-org-agenda/goto-date ()
--
2.47.1
^ permalink raw reply related [flat|nested] 7+ messages in thread
* Re: [PATCH] lisp/org.el: Add ability to sort tags by hierarchy
2024-12-24 21:48 ` Morgan Smith
@ 2024-12-25 8:17 ` Ihor Radchenko
0 siblings, 0 replies; 7+ messages in thread
From: Ihor Radchenko @ 2024-12-25 8:17 UTC (permalink / raw)
To: Morgan Smith; +Cc: emacs-orgmode
Morgan Smith <morgan.j.smith@outlook.com> writes:
>> Also, what happens when there
>> are no tag groups defined? (These questions should be answered in the
>> docstring and the etc/ORG-NEWS entry you need to add, announcing the new
>> feature)
>
> Fallback to `org-sort-function'.
I am not sure if it is the best idea to hard-code using
`org-sort-function' here.
What if a user wants specific `org-sort-function' for, say, table
sorting, but something else for tags?
Secondary sorting would better be customizeable.
Maybe you can have a function generator that will work like
(org-tags-sort-hierarchy-by org-sort-function)
or
(org-tags-sort-hierarchy-by #'string<)
--
Ihor Radchenko // yantar92,
Org mode maintainer,
Learn more about Org mode at <https://orgmode.org/>.
Support Org development at <https://liberapay.com/org-mode>,
or support my work at <https://liberapay.com/yantar92>
^ permalink raw reply [flat|nested] 7+ messages in thread
end of thread, other threads:[~2024-12-25 8:17 UTC | newest]
Thread overview: 7+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2024-06-15 12:35 [PATCH] lisp/org.el: Add ability to sort tags by hierarchy Morgan Smith
2024-06-15 14:25 ` Ihor Radchenko
2024-08-28 15:39 ` Ihor Radchenko
2024-08-28 16:10 ` Morgan Smith
2024-09-01 16:23 ` Ihor Radchenko
2024-12-24 21:48 ` Morgan Smith
2024-12-25 8:17 ` Ihor Radchenko
Code repositories for project(s) associated with this public inbox
https://git.savannah.gnu.org/cgit/emacs/org-mode.git
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).