From 70176d9c5fff9917d392be4c19e3d32716d21055 Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Wed, 24 Jul 2019 20:15:52 -0500 Subject: [PATCH] Add: Convert bare strings to (regexp) predicates --- README.org | 7 +++++-- org-ql.el | 18 ++++++++++++++++++ tests/test-org-ql.el | 38 +++++++++++++++++++++++++++++++++++++- 3 files changed, 60 insertions(+), 3 deletions(-) diff --git a/README.org b/README.org index cc68153..64c473b 100644 --- a/README.org +++ b/README.org @@ -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. +*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 :PROPERTIES: :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. -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). + ~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 ~<=~). @@ -309,6 +311,7 @@ Expands into a call to ~org-ql-select~ with the same arguments. For convenience *Added* + 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* + Function ~org-ql-query~ renamed to ~org-ql-select~. diff --git a/org-ql.el b/org-ql.el index a680080..2abeaa3 100644 --- a/org-ql.el +++ b/org-ql.el @@ -150,6 +150,7 @@ non-nil." ;; a buffer if the filename doesn't exist. (find-file-noselect it)) (user-error "Can't open file: %s" it))))))) + (query (org-ql--pre-process-query query)) ((query preamble-re) (org-ql--query-preamble query)) (predicate (org-ql--query-predicate query)) (action (pcase action @@ -221,6 +222,23 @@ should be an `org-ql' query sexp." (org-ql-select from where :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) "Return predicate function for QUERY." (byte-compile `(lambda () diff --git a/tests/test-org-ql.el b/tests/test-org-ql.el index 0cb32e3..835b93b 100644 --- a/tests/test-org-ql.el +++ b/tests/test-org-ql.el @@ -38,10 +38,12 @@ (correct-sexp-p (pcase (car sexp) ('org-ql 'org-ql) ('org-ql--query-preamble 'query-preamble) + ('org-ql--pre-process-query t) (_ nil))) (result (pcase (car sexp) ('org-ql (org-ql-test--format-result--ql sexp)) ('org-ql--query-preamble (org-ql-test--format-result--query-preamble sexp)) + ('org-ql--pre-process-query (format "'%S" (eval sexp))) (_ nil)))) (insert " :to-equal " result) (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) 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" ;; 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 (regexp "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")))) + :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)" (it "without arguments"