From 1765e91d2f7875b321703afe34e32754a022bef4 Mon Sep 17 00:00:00 2001 From: Jens Schmidt Date: Thu, 3 Aug 2023 22:34:56 +0200 Subject: [PATCH] org-make-tags-matcher: Add starred property operators, more operator synonyms * lisp/org.el (org-make-tags-matcher): Add starred property operators. Recognize additional operators "==", "!=", "/=". Clean up and document match term parsing. (org-op-to-function): Recognize additional inequality operator "/=". * doc/org-manual.org (Matching tags and properties): * etc/ORG-NEWS: (~org-tags-view~ supports more property operators): * testing/lisp/test-org.el (test-org/map-entries): Add documentation, announcement, and tests on starred and additional operators. --- doc/org-manual.org | 20 ++++++++- etc/ORG-NEWS | 10 ++++- lisp/org.el | 96 +++++++++++++++++++++++++++++----------- testing/lisp/test-org.el | 33 ++++++++++++++ 4 files changed, 130 insertions(+), 29 deletions(-) diff --git a/doc/org-manual.org b/doc/org-manual.org index 16fbb268f..27051fbfc 100644 --- a/doc/org-manual.org +++ b/doc/org-manual.org @@ -9246,16 +9246,25 @@ When matching properties, a number of different operators can be used to test the value of a property. Here is a complex example: #+begin_example -+work-boss+PRIORITY="A"+Coffee="unlimited"+Effort<2 ++work-boss+PRIORITY="A"+Coffee="unlimited"+Effort<*2 +With={Sarah\|Denny}+SCHEDULED>="<2008-10-11>" #+end_example +#+cindex: operator, for property search #+texinfo: @noindent The type of comparison depends on how the comparison value is written: - If the comparison value is a plain number, a numerical comparison is done, and the allowed operators are =<=, ===, =>=, =<==, =>==, and - =<>=. + =<>=. As synonym for the equality operator there is also ====, as + synonyms for the inequality operator there are =!== and =/==. + +- All operators may be optionally followed by an asterisk =*=, like in + =<*=, =!=*=, etc. Such /starred operators/ work like their regular, + unstarred counterparts except that they match only headlines where + the tested property is actually present. This is most useful for + search terms that logically exclude results, like the inequality + operator. - If the comparison value is enclosed in double-quotes, a string comparison is done, and the same operators are allowed. @@ -9280,6 +9289,13 @@ smaller than 2, a =With= property that is matched by the regular expression =Sarah\|Denny=, and that are scheduled on or after October 11, 2008. +Note that the test on the =EFFORT= property uses operator =<*=, so +that the search result will include only entries that actually have an +=EFFORT= property defined and with numerical value smaller than 2. +With the regular =<= operator, the search would handle entries without +an =EFFORT= property as having a zero effort and would include them in +the result as well. + You can configure Org mode to use property inheritance during a search, but beware that this can slow down searches considerably. See [[*Property Inheritance]], for details. diff --git a/etc/ORG-NEWS b/etc/ORG-NEWS index 4f16eda24..10c51e354 100644 --- a/etc/ORG-NEWS +++ b/etc/ORG-NEWS @@ -125,7 +125,7 @@ New functions to retrieve and set (via ~setf~) commonly used element properties: - =:contents-post-affiliated= :: ~org-element-post-affiliated~ - =:contents-post-blank= :: ~org-element-post-blank~ - =:parent= :: ~org-element-parent~ - + ***** New macro ~org-element-with-enabled-cache~ The macro arranges the element cache to be active during =BODY= execution. @@ -558,6 +558,14 @@ special repeaters ~++~ and ~.+~ are skipped. A capture template can target ~(here)~ which is the equivalent of invoking a capture template with a zero prefix. +*** ~org-tags-view~ supports more property operators + +It supports inequality operators ~!=~ and ~/=~ in addition to the less +common (BASIC? Pascal? SQL?) ~<>~. And it supports starred versions +of all relational operators (~<*~, ~=*~, ~!=*~, etc.) that work like +the regular, unstarred operators but match a headline only if the +tested property is actually present. + ** New functions and changes in function arguments *** =TYPES= argument in ~org-element-lineage~ can now be a symbol diff --git a/lisp/org.el b/lisp/org.el index 1ac912e61..a1f4c1c53 100644 --- a/lisp/org.el +++ b/lisp/org.el @@ -11306,11 +11306,33 @@ See also `org-scan-tags'." (let ((match0 match) (re (concat - "^&?\\([-+:]\\)?\\({[^}]+}\\|LEVEL\\([<=>]\\{1,2\\}\\)" - "\\([0-9]+\\)\\|\\(\\(?:[[:alnum:]_]+\\(?:\\\\-\\)*\\)+\\)" - "\\([<>=]\\{1,2\\}\\)" - "\\({[^}]+}\\|\"[^\"]*\"\\|-?[.0-9]+\\(?:[eE][-+]?[0-9]+\\)?\\)" - "\\|" org-tag-re "\\)")) + "^" + ;; implicit AND operator (OR is done by global splitting) + "&?" + ;; exclusion and inclusion (the latter being implicit) + "\\(?1:[-+:]\\)?" + ;; query term + "\\(?2:" + ;; tag regexp match + "{[^}]+}\\|" + ;; LEVEL property match + "LEVEL\\(?3:[<=>]=?\\|[!/]=\\|<>\\)\\(?4:[0-9]+\\)\\|" + ;; regular property match + "\\(?:" + ;; property name + "\\(?5:\\(?:[[:alnum:]_]+\\(?:\\\\-\\)*\\)+\\)" + ;; operator, optionally starred + "\\(?6:[<=>]=?\\|[!/]=\\|<>\\)\\(?7:\\*\\)?" + ;; operand + "\\(?8:" + "{[^}]+}\\|" + "\"[^\"]*\"\\|" + "-?[.0-9]+\\(?:[eE][-+]?[0-9]+\\)?" + "\\)" + "\\)\\|" + ;; exact tag match + org-tag-re + "\\)")) (start 0) tagsmatch todomatch tagsmatcher todomatcher) @@ -11352,6 +11374,11 @@ See also `org-scan-tags'." (let* ((rest (substring term (match-end 0))) (minus (and (match-end 1) (equal (match-string 1 term) "-"))) + ;; Bind the whole term to `tag' and use that + ;; variable for a tag regexp match in (1) or as an + ;; exact tag match in (2). Unquote quoted minus + ;; characters, which would be actually required + ;; only for the former case. (tag (save-match-data (replace-regexp-in-string "\\\\-" "-" (match-string 2 term)))) @@ -11360,7 +11387,7 @@ See also `org-scan-tags'." (propp (match-end 5)) (mm (cond - (regexp + (regexp ; (1) `(with-syntax-table org-mode-tags-syntax-table (org-match-any-p ,(substring tag 1 -1) tags-list))) (levelp @@ -11368,28 +11395,45 @@ See also `org-scan-tags'." level ,(string-to-number (match-string 4 term)))) (propp - (let* ((gv (pcase (upcase (match-string 5 term)) + (let* (;; Convert property name to an Elisp + ;; accessor for that property. + (gv (pcase (upcase (match-string 5 term)) ("CATEGORY" '(org-get-category (point))) ("TODO" 'todo) (p `(org-cached-entry-get nil ,p)))) - (pv (match-string 7 term)) + ;; Determine operand (aka. property + ;; value) + (pv (match-string 8 term)) + ;; Determine type of operand. Note that + ;; these are not exclusive: Any TIMEP is + ;; also STRP. (regexp (eq (string-to-char pv) ?{)) (strp (eq (string-to-char pv) ?\")) (timep (string-match-p "^\"[[<]\\(?:[0-9]+\\|now\\|today\\|tomorrow\\|[+-][0-9]+[dmwy]\\).*[]>]\"$" pv)) + ;; Massage operand. TIMEP must come + ;; before STRP. + (pv (cond (regexp (substring pv 1 -1)) + (timep (org-matcher-time + (substring pv 1 -1))) + (strp (substring pv 1 -1)) + (t pv))) + ;; Convert operator to Elisp. (po (org-op-to-function (match-string 6 term) - (if timep 'time strp)))) - (setq pv (if (or regexp strp) (substring pv 1 -1) pv)) - (when timep (setq pv (org-matcher-time pv))) - (cond ((and regexp (eq po '/=)) - `(not (string-match ,pv (or ,gv "")))) - (regexp `(string-match ,pv (or ,gv ""))) - (strp `(,po (or ,gv "") ,pv)) - (t - `(,po - (string-to-number (or ,gv "")) - ,(string-to-number pv)))))) - (t `(member ,tag tags-list))))) + (if timep 'time strp))) + ;; Convert whole term to Elisp. + (pt (cond ((and regexp (eq po '/=)) + `(not (string-match ,pv (or ,gv "")))) + (regexp `(string-match ,pv (or ,gv ""))) + (strp `(,po (or ,gv "") ,pv)) + (t + `(,po + (string-to-number (or ,gv "")) + ,(string-to-number pv))))) + ;; Respect the star after the operand. + (pt (if (match-end 7) `(and ,gv ,pt) pt))) + pt)) + (t `(member ,tag tags-list))))) ; (2) (push (if minus `(not ,mm) mm) tagsmatcher) (setq term rest))) (push `(and ,@tagsmatcher) orlist) @@ -11520,12 +11564,12 @@ the list of tags in this group." "Turn an operator into the appropriate function." (setq op (cond - ((equal op "<" ) '(< org-string< org-time<)) - ((equal op ">" ) '(> org-string> org-time>)) - ((member op '("<=" "=<")) '(<= org-string<= org-time<=)) - ((member op '(">=" "=>")) '(>= org-string>= org-time>=)) - ((member op '("=" "==")) '(= string= org-time=)) - ((member op '("<>" "!=")) '(/= org-string<> org-time<>)))) + ((equal op "<" ) '(< org-string< org-time<)) + ((equal op ">" ) '(> org-string> org-time>)) + ((member op '("<=" "=<" )) '(<= org-string<= org-time<=)) + ((member op '(">=" "=>" )) '(>= org-string>= org-time>=)) + ((member op '("=" "==" )) '(= string= org-time=)) + ((member op '("<>" "!=" "/=")) '(/= org-string<> org-time<>)))) (nth (if (eq stringp 'time) 2 (if stringp 1 0)) op)) (defvar org-add-colon-after-tag-completion nil) ;; dynamically scoped param diff --git a/testing/lisp/test-org.el b/testing/lisp/test-org.el index 890ea6a8c..ef52b95c7 100644 --- a/testing/lisp/test-org.el +++ b/testing/lisp/test-org.el @@ -2851,6 +2851,11 @@ test (equal '(1) (org-test-with-temp-text "* [#A] H1\n* [#B] H2" (org-map-entries #'point "PRIORITY=\"A\"")))) + ;; Negative priority match. + (should + (equal '(11) + (org-test-with-temp-text "* [#A] H1\n* [#B] H2" + (org-map-entries #'point "PRIORITY/=\"A\"")))) ;; Date match. (should (equal '(36) @@ -2881,6 +2886,34 @@ SCHEDULED: <2014-03-04 tue.>" :TEST: 2 :END:" (org-map-entries #'point "TEST=1")))) + ;; Negative regular property match. + (should + (equal '(35 68) + (org-test-with-temp-text " +* H1 +:PROPERTIES: +:TEST: 1 +:END: +* H2 +:PROPERTIES: +:TEST: 2 +:END: +* H3" + (org-map-entries #'point "TEST!=1")))) + ;; Starred negative regular property match. + (should + (equal '(35) + (org-test-with-temp-text " +* H1 +:PROPERTIES: +:TEST: 1 +:END: +* H2 +:PROPERTIES: +:TEST: 2 +:END: +* H3" + (org-map-entries #'point "TEST!=*1")))) ;; Multiple criteria. (should (equal '(23) -- 2.30.2