Add: Convert bare strings to (regexp) predicates

This commit is contained in:
Adam Porter 2019-07-24 20:15:52 -05:00
parent 7c4297b667
commit 70176d9c5f
3 changed files with 60 additions and 3 deletions

View file

@ -103,6 +103,10 @@ Here's an example of using it to generate an agenda-like view for certain files
A query is a lisp form which may contain arbitrary lisp forms, as well as certain built-in predicates. It is byte-compiled into a predicate function which is tested with point on each heading in an Org buffer; when it returns non-nil, the heading matches the query. A query is a lisp form which may contain arbitrary lisp forms, as well as certain built-in predicates. It is byte-compiled into a predicate function which is tested with point on each heading in an Org buffer; when it returns non-nil, the heading matches the query.
*Notes:*
+ Bare strings like ~"string"~ are automatically converted to ~(regexp "string")~ predicates.
+ Standard numeric comparator function symbols (~<~, ~<=~, ~>~, ~>=~, ~=~ ) need not be quoted when passed as an argument to these predicates. The resemblance to infix notation is coincidental. See examples in documentation.
*** Predicates *** Predicates
:PROPERTIES: :PROPERTIES:
:TOC: ignore :TOC: ignore
@ -110,8 +114,6 @@ A query is a lisp form which may contain arbitrary lisp forms, as well as certai
Arguments are listed next to predicate names, where applicable. Arguments are listed next to predicate names, where applicable.
Note that, for convenience, standard numeric comparator function symbols (~<~, ~=~, etc.) do not need to be quoted when passed as an argument to these predicates. The resemblance to infix notation is coincidental. See examples in documentation.
+ ~category (&optional categories)~ :: Return non-nil if current heading is in one or more of ~CATEGORIES~ (a list of strings). + ~category (&optional categories)~ :: Return non-nil if current heading is in one or more of ~CATEGORIES~ (a list of strings).
+ ~clocked (&key from to on)~ :: Return non-nil if current entry was clocked in given period. If no arguments are specified, return non-nil if entry was clocked at any time. If ~FROM~, return non-nil if entry was clocked on or after ~FROM~. If ~TO~, return non-nil if entry was clocked on or before ~TO~. If ~ON~, return non-nil if entry was clocked on date ~ON~. ~FROM~, ~TO~, and ~ON~ should be strings parseable by ~parse-time-string~ but may omit the time value. Note: Clock entries are expected to be clocked out. Currently clocked entries (i.e. with unclosed timestamp ranges) are ignored. + ~clocked (&key from to on)~ :: Return non-nil if current entry was clocked in given period. If no arguments are specified, return non-nil if entry was clocked at any time. If ~FROM~, return non-nil if entry was clocked on or after ~FROM~. If ~TO~, return non-nil if entry was clocked on or before ~TO~. If ~ON~, return non-nil if entry was clocked on date ~ON~. ~FROM~, ~TO~, and ~ON~ should be strings parseable by ~parse-time-string~ but may omit the time value. Note: Clock entries are expected to be clocked out. Currently clocked entries (i.e. with unclosed timestamp ranges) are ignored.
+ ~closed (&optional comparator target-date)~ :: Return non-nil if entry's closed date compares with ~TARGET-DATE~ using ~COMPARATOR~. ~TARGET-DATE~ should be a string parseable by ~date-to-day~. ~COMPARATOR~ should be a function (like ~<=~). + ~closed (&optional comparator target-date)~ :: Return non-nil if entry's closed date compares with ~TARGET-DATE~ using ~COMPARATOR~. ~TARGET-DATE~ should be a string parseable by ~date-to-day~. ~COMPARATOR~ should be a function (like ~<=~).
@ -309,6 +311,7 @@ Expands into a call to ~org-ql-select~ with the same arguments. For convenience
*Added* *Added*
+ Function ~org-ql-query~, like ~org-ql-select~ but with arguments named more like a SQL query. + Function ~org-ql-query~, like ~org-ql-select~ but with arguments named more like a SQL query.
+ Bare strings like ~"string"~ can be used in queries, which are converted to ~(regexp "string")~ automatically.
*Changed* *Changed*
+ Function ~org-ql-query~ renamed to ~org-ql-select~. + Function ~org-ql-query~ renamed to ~org-ql-select~.

View file

@ -150,6 +150,7 @@ non-nil."
;; a buffer if the filename doesn't exist. ;; a buffer if the filename doesn't exist.
(find-file-noselect it)) (find-file-noselect it))
(user-error "Can't open file: %s" it))))))) (user-error "Can't open file: %s" it)))))))
(query (org-ql--pre-process-query query))
((query preamble-re) (org-ql--query-preamble query)) ((query preamble-re) (org-ql--query-preamble query))
(predicate (org-ql--query-predicate query)) (predicate (org-ql--query-predicate query))
(action (pcase action (action (pcase action
@ -221,6 +222,23 @@ should be an `org-ql' query sexp."
(org-ql-select from where (org-ql-select from where
:action select)) :action select))
(defun org-ql--pre-process-query (query)
"Return QUERY having been pre-processed.
Replaces bare strings with (regexp) selectors."
;; This is unsophisticated, but it works.
(cl-labels ((rec (element)
(pcase element
(`(or . ,clauses) `(or ,@(mapcar #'rec clauses)))
(`(and . ,clauses) `(and ,@(mapcar #'rec clauses)))
(`(when ,condition . ,clauses) `(when ,(rec condition)
,@(mapcar #'rec clauses)))
(`(unless ,condition . ,clauses) `(unless ,(rec condition)
,@(mapcar #'rec clauses)))
;; TODO: Combine (regexp) when appropriate (i.e. inside an OR, not an AND).
((pred stringp) `(regexp ,element))
(_ element))))
(rec query)))
(defun org-ql--query-predicate (query) (defun org-ql--query-predicate (query)
"Return predicate function for QUERY." "Return predicate function for QUERY."
(byte-compile `(lambda () (byte-compile `(lambda ()

View file

@ -38,10 +38,12 @@
(correct-sexp-p (pcase (car sexp) (correct-sexp-p (pcase (car sexp)
('org-ql 'org-ql) ('org-ql 'org-ql)
('org-ql--query-preamble 'query-preamble) ('org-ql--query-preamble 'query-preamble)
('org-ql--pre-process-query t)
(_ nil))) (_ nil)))
(result (pcase (car sexp) (result (pcase (car sexp)
('org-ql (org-ql-test--format-result--ql sexp)) ('org-ql (org-ql-test--format-result--ql sexp))
('org-ql--query-preamble (org-ql-test--format-result--query-preamble sexp)) ('org-ql--query-preamble (org-ql-test--format-result--query-preamble sexp))
('org-ql--pre-process-query (format "'%S" (eval sexp)))
(_ nil)))) (_ nil))))
(insert " :to-equal " result) (insert " :to-equal " result)
(user-error "Point must be after an `org-ql' form"))) (user-error "Point must be after an `org-ql' form")))
@ -107,6 +109,27 @@ 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)))))
(it "Query pre-processing"
(expect (org-ql--pre-process-query '(and "string1" "string2"))
:to-equal '(and (regexp "string1") (regexp "string2")))
(expect (org-ql--pre-process-query '(or "string1" "string2"))
:to-equal '(or (regexp "string1") (regexp "string2")))
(expect (org-ql--pre-process-query '(and (todo "TODO")
(or "string1" "string2")))
:to-equal '(and (todo "TODO") (or (regexp "string1") (regexp "string2"))))
(expect (org-ql--pre-process-query '(when (todo "TODO")
(or "string1" "string2")))
:to-equal '(when (todo "TODO") (or (regexp "string1") (regexp "string2"))))
(expect (org-ql--pre-process-query '(when "string-cond1"
(or "string1" "string2")))
:to-equal '(when (regexp "string-cond1") (or (regexp "string1") (regexp "string2"))))
(expect (org-ql--pre-process-query '(when (and "string-cond1" "string-cond2")
(or "string1" "string2")))
:to-equal '(when (and (regexp "string-cond1") (regexp "string-cond2")) (or (regexp "string1") (regexp "string2"))))
(expect (org-ql--pre-process-query '(unless (and "stringcondition1" "stringcond2")
(or "string1" "string2")))
:to-equal '(unless (and (regexp "stringcondition1") (regexp "stringcond2")) (or (regexp "string1") (regexp "string2")))))
(describe "Query compiling" (describe "Query compiling"
;; Okay, so it's not really "compiling," but it sounds fancy. :) ;; Okay, so it's not really "compiling," but it sounds fancy. :)
@ -337,7 +360,20 @@ Based on Buttercup macro `it'."
(expect (org-ql test-buffer (expect (org-ql test-buffer
(regexp "Take over" "pizza") (regexp "Take over" "pizza")
:sort todo :sort todo
:action (org-ql-test-org-get-heading)) :to-equal '("Take over the universe" "Take over the world" "Take over Mars" "Take over the moon" "Order a pizza" "Get haircut")))) :action (org-ql-test-org-get-heading))
:to-equal '("Take over the universe" "Take over the world" "Take over Mars" "Take over the moon" "Order a pizza" "Get haircut")))
(org-ql-it "with a plain string"
(expect (org-ql test-buffer
"Take over"
:sort todo
:action (org-ql-test-org-get-heading))
:to-equal '("Take over the universe" "Take over the world" "Take over Mars" "Take over the moon" "Get haircut")))
(org-ql-it "with two plain strings"
(expect (org-ql test-buffer
(or "Take over" "pizza")
:sort todo
:action (org-ql-test-org-get-heading))
:to-equal '("Take over the universe" "Take over the world" "Take over Mars" "Take over the moon" "Order a pizza" "Get haircut"))))
(describe "(todo)" (describe "(todo)"
(it "without arguments" (it "without arguments"