Change: Improve level predicate

Now accepts e.g. (level 2 4) to match headings from levels 2-4,
inclusive.

Also add tests for (level) preamble conversion.
This commit is contained in:
Adam Porter 2019-07-23 19:14:01 -05:00
parent 99b38d131e
commit bdf473356d
3 changed files with 57 additions and 21 deletions

View file

@ -118,7 +118,7 @@ Note that, for convenience, standard numeric comparator function symbols (~<~, ~
+ ~done~ :: Return non-nil if entry's ~TODO~ keyword is in ~org-done-keywords~. + ~done~ :: Return non-nil if entry's ~TODO~ keyword is in ~org-done-keywords~.
+ ~habit~ :: Return non-nil if entry is a habit. + ~habit~ :: Return non-nil if entry is a habit.
+ ~heading (regexp)~ :: Return non-nil if current entry's heading matches ~REGEXP~ (a regexp string). + ~heading (regexp)~ :: Return non-nil if current entry's heading matches ~REGEXP~ (a regexp string).
+ ~level (level-or-comparator &optional level)~ :: Return non-nil if current heading's outline level matches ~LEVEL~ with ~COMPARATOR~. If ~LEVEL~ is nil, ~LEVEL-OR-COMPARATOR~ should be an integer level, which will be tested for equality to the heading's outline level. If ~LEVEL~ is non-nil, ~LEVEL-OR-COMPARATOR~ should be a comparator function (like ~<=~). + ~level (level-or-comparator &optional level)~ :: Return non-nil if current heading's outline level matches arguments. The following forms are accepted: ~(level NUMBER)~: Matches if heading level is ~NUMBER~. ~(level NUMBER NUMBER)~: Matches if heading level is equal to or between NUMBERs. ~(level COMPARATOR NUMBER)~: Matches if heading level compares to ~NUMBER~ with ~COMPARATOR~. ~COMPARATOR~ may be ~<~, ~<=~, ~>~, or ~>=~.
+ ~planning (&optional comparator target-date)~ :: Return non-nil if entry's planning date (deadline or scheduled) compares with ~TARGET-DATE~ using ~COMPARATOR~. ~TARGET-DATE~ should be a string parseable by ~date-to-day~. ~COMPARATOR~ should be a function (like ~<=~). + ~planning (&optional comparator target-date)~ :: Return non-nil if entry's planning date (deadline or scheduled) compares with ~TARGET-DATE~ using ~COMPARATOR~. ~TARGET-DATE~ should be a string parseable by ~date-to-day~. ~COMPARATOR~ should be a function (like ~<=~).
+ ~priority (&optional comparator-or-priority priority)~ :: Return non-nil if current heading has a certain priority. ~COMPARATOR-OR-PRIORITY~ should be either a comparator function, like ~<=~, or a priority string, like "A" (in which case (~=~ will be the comparator). If ~COMPARATOR-OR-PRIORITY~ is a comparator, ~PRIORITY~ should be a priority string. + ~priority (&optional comparator-or-priority priority)~ :: Return non-nil if current heading has a certain priority. ~COMPARATOR-OR-PRIORITY~ should be either a comparator function, like ~<=~, or a priority string, like "A" (in which case (~=~ will be the comparator). If ~COMPARATOR-OR-PRIORITY~ is a comparator, ~PRIORITY~ should be a priority string.
+ ~property (property &optional value)~ :: Return non-nil if current entry has ~PROPERTY~ (a string), and optionally ~VALUE~ (a string). + ~property (property &optional value)~ :: Return non-nil if current entry has ~PROPERTY~ (a string), and optionally ~VALUE~ (a string).

View file

@ -281,7 +281,7 @@ replace the clause with a preamble."
(pcase element (pcase element
(`(or _) element) (`(or _) element)
(`(regexp . ,regexps) (`(regexp . ,regexps)
(let* ((regexp (rx-to-string `(or ,@regexps)))) (let* ((regexp (rx-to-string `(or ,@regexps) t)))
(setq org-ql-preamble regexp) (setq org-ql-preamble regexp)
;; Return nil ;; Return nil
nil)) nil))
@ -289,13 +289,21 @@ replace the clause with a preamble."
(let* ((regexps (--map (list 'regexp (let* ((regexps (--map (list 'regexp
(format org-heading-keyword-regexp-format it)) (format org-heading-keyword-regexp-format it))
todo-keywords)) todo-keywords))
(regexp (rx-to-string `(or ,@regexps)))) (regexp (rx-to-string `(or ,@regexps) t)))
(setq org-ql-preamble regexp) (setq org-ql-preamble regexp)
;; Return nil ;; Return nil
nil)) nil))
;; FIXME: Need to handle e.g. (level <= 2) (`(level ,comparator-or-num ,num)
(let ((repeat (pcase comparator-or-num
('< `(repeat 1 ,(1- num) "*"))
('<= `(repeat 1 ,num "*"))
('> `(>= ,(1+ num) "*"))
('>= `(>= ,num "*"))
((pred integerp) `(repeat ,comparator-or-num ,num "*")))))
(setq org-ql-preamble (rx-to-string `(seq bol ,repeat " ") t))
nil))
(`(level ,num) (`(level ,num)
(let* ((regexp (rx-to-string `(seq bol (repeat ,num "*") " ")))) (let* ((regexp (rx-to-string `(seq bol (repeat ,num "*") " ") t)))
(setq org-ql-preamble regexp) (setq org-ql-preamble regexp)
nil)) nil))
(`(and . ,rest) (`(and . ,rest)
@ -502,20 +510,25 @@ With KEYWORDS, return non-nil if its keyword is one of KEYWORDS (a list of strin
(otherwise (seq-intersection tags tags-at))))) (otherwise (seq-intersection tags tags-at)))))
(org-ql--defpred level (level-or-comparator &optional level) (org-ql--defpred level (level-or-comparator &optional level)
"Return non-nil if current heading's outline level matches LEVEL with COMPARATOR. "Return non-nil if current heading's outline level matches arguments.
The following forms are accepted:
If LEVEL is nil, LEVEL-OR-COMPARATOR should be an integer level, (level NUMBER): Matches if heading level is NUMBER.
which will be tested for equality to the heading's outline level. (level NUMBER NUMBER): Matches if heading level is equal to or between NUMBERs.
If LEVEL is non-nil, LEVEL-OR-COMPARATOR should be a comparator (level COMPARATOR NUMBER): Matches if heading level compares to NUMBER with COMPARATOR.
function (like `<=')."
COMPARATOR may be `<', `<=', `>', or `>='."
;; NOTE: It might be necessary to take into account `org-odd-levels'; see docstring for ;; NOTE: It might be necessary to take into account `org-odd-levels'; see docstring for
;; `org-outline-level'. ;; `org-outline-level'.
(when-let ((outline-level (org-outline-level))) (when-let ((outline-level (org-outline-level)))
(pcase level (pcase level-or-comparator
;; Check for equality ((pred numberp) (pcase level
((pred null) (= outline-level level-or-comparator)) ('nil ;; Equality
;; Check with comparator (= outline-level level-or-comparator))
(_ (funcall level-or-comparator outline-level level))))) ((pred numberp) ;; Between two levels
(>= level-or-comparator outline-level level))))
((pred symbolp) ;; Compare with function
(funcall level-or-comparator outline-level level)))))
(org-ql--defpred priority (&optional comparator-or-priority priority) (org-ql--defpred priority (&optional comparator-or-priority priority)
"Return non-nil if current heading has a certain priority. "Return non-nil if current heading has a certain priority.

View file

@ -107,7 +107,34 @@ Based on Buttercup macro `it'."
(cl-loop while (re-search-forward org-heading-regexp nil t) (cl-loop while (re-search-forward org-heading-regexp nil t)
sum 1))))) sum 1)))))
(describe "Predicates" (describe "Query compiling"
;; Okay, so it's not really "compiling," but it sounds fancy. :)
;; TODO: Other predicates.
(describe "(level)"
(it "with a number"
(expect (org-ql--query-preamble '(level 2))
:to-equal `(t ,(rx bol (repeat 2 "*") " "))))
(it "with two numbers"
(expect (org-ql--query-preamble '(level 2 4))
:to-equal `(t ,(rx bol (repeat 2 4 "*") " "))))
(it "<"
(expect (org-ql--query-preamble '(level < 3))
:to-equal `(t ,(rx bol (repeat 1 2 "*") " "))))
(it "<="
(expect (org-ql--query-preamble '(level <= 2))
:to-equal `(t ,(rx bol (repeat 1 2 "*") " "))))
(it ">"
(expect (org-ql--query-preamble '(level > 2))
:to-equal `(t ,(rx bol (>= 3 "*") " "))))
(it ">="
(expect (org-ql--query-preamble '(level >= 2))
:to-equal `(t ,(rx bol (>= 2 "*") " "))))))
(describe "Query results"
;; TODO: Other predicates.
(describe "(category)" (describe "(category)"
(org-ql-it "without arguments" (org-ql-it "without arguments"
@ -364,10 +391,6 @@ Based on Buttercup macro `it'."
(expect (org-ql test-buffer (expect (org-ql test-buffer
(ts :on "2019-06-09") (ts :on "2019-06-09")
:action (org-ql-test-org-get-heading)) :action (org-ql-test-org-get-heading))
:to-equal nil)) :to-equal nil)))))
)
;; TODO: Other predicates.
))
;;; org-ql.el ends here ;;; org-ql.el ends here