Add/Change: Use ts.el, add ts-a and ts-i

Improves performance, reduces code, and more test cases added.
This commit is contained in:
Adam Porter 2019-08-11 11:39:04 -05:00
parent 913292787b
commit 4954789ca5
4 changed files with 261 additions and 154 deletions

168
org-ql.el
View file

@ -3,7 +3,7 @@
;; Author: Adam Porter <adam@alphapapa.net>
;; Url: https://github.com/alphapapa/org-ql
;; Version: 0.2-pre
;; Package-Requires: ((emacs "26.1") (dash "2.13") (org "9.0") (s "1.12.0"))
;; Package-Requires: ((emacs "26.1") (dash "2.13") (org "9.0") (s "1.12.0") (ts "0.2"))
;; Keywords: hypermedia, outlines, Org, agenda
;;; Commentary:
@ -40,6 +40,7 @@
(require 'subr-x)
(require 'dash)
(require 'ts)
;;;; Compatibility
@ -53,6 +54,12 @@
;;;; Variables
(defconst org-ql-tsr-regexp-inactive
(concat org-ts-regexp-inactive "\\(--?-?"
org-ts-regexp-inactive "\\)?")
;; MAYBE: Propose this for org.el.
"Regular expression matching an inactive timestamp or timestamp range.")
(defvar org-ql--today nil)
(defvar org-ql-use-preamble t
@ -232,7 +239,8 @@ NARROW corresponds to the `org-ql-select' argument NARROW."
(defun org-ql--pre-process-query (query)
"Return QUERY having been pre-processed.
Replaces bare strings with (regexp) selectors."
Replaces bare strings with (regexp) selectors, and appropriate
`ts'-related selectors."
;; This is unsophisticated, but it works.
(cl-labels ((rec (element)
(pcase element
@ -244,6 +252,8 @@ Replaces bare strings with (regexp) selectors."
,@(mapcar #'rec clauses)))
;; TODO: Combine (regexp) when appropriate (i.e. inside an OR, not an AND).
((pred stringp) `(regexp ,element))
(`(,(or 'ts-active 'ts-a) . ,rest) `(ts :type active ,@rest))
(`(,(or 'ts-inactive 'ts-i) . ,rest) `(ts :type inactive ,@rest))
(_ element))))
(rec query)))
@ -267,39 +277,21 @@ Replaces bare strings with (regexp) selectors."
;; NOTE: The macro must expand to the actual `org-ql--predicate-clocked'
;; function, not another `clocked'.
`(org-ql--predicate-clocked :from ,from :to ,to))
(ts (&key from to on)
(ts (&key from to on (type 'both))
(when on
(setq from on
to on))
(when from
(setq from (org-ql--parse-time-string from)))
(setq from (ts-parse-fill 'begin from)))
(when to
(setq to (org-ql--parse-time-string to 'end)))
(setq to (ts-parse-fill 'end to)))
;; NOTE: The macro must expand to the actual `org-ql--predicate-ts'
;; function, not another `ts'.
`(org-ql--predicate-ts :from ,from :to ,to))
(ts-active (&key from to on)
(when on
(setq from on
to on))
(when from
(setq from (org-ql--parse-time-string from)))
(when to
(setq to (org-ql--parse-time-string to 'end)))
;; NOTE: The macro must expand to the actual `org-ql--predicate-ts'
;; function, not another `ts'.
`(org-ql--predicate-ts-active :from ,from :to ,to))
(ts-inactive (&key from to on)
(when on
(setq from on
to on))
(when from
(setq from (org-ql--parse-time-string from)))
(when to
(setq to (org-ql--parse-time-string to 'end)))
;; NOTE: The macro must expand to the actual `org-ql--predicate-ts'
;; function, not another `ts'.
`(org-ql--predicate-ts-inactive :from ,from :to ,to)))
`(org-ql--predicate-ts :from ,from :to ,to
:regexp ,(pcase type
('both org-tsr-regexp-both)
('active org-tsr-regexp)
('inactive org-ql-tsr-regexp-inactive)))))
(cl-symbol-macrolet ((today org-ql--today) ; Necessary because of byte-compiling the lambda
(= #'=)
(< #'<)
@ -710,9 +702,11 @@ comparator, PRIORITY should be a priority string."
;; and language-independent than using from/to. Alternatively, add :before/:after, but I
;; think the comparators are better. Also consider using a macro to DRY these out.
(org-ql--defpred ts (&key from to _on)
;; The underscore before `on' prevents "unused lexical variable" warnings, because we
;; pre-process that argument in a macro before this function is called.
(org-ql--defpred ts (&key from to _on regexp)
;; The underscore before `on' prevents "unused lexical variable" warnings,
;; because we pre-process that argument in a macro before this function is
;; called. The `regexp' argument is also provided by the macro and is not
;; to be given by the user, so it is omitted from the docstring.
"Return non-nil if current entry has a timestamp in given period.
If no arguments are specified, return non-nil if entry has any
timestamp.
@ -725,116 +719,28 @@ If TO, return non-nil if entry has a timestamp on or before TO.
If ON, return non-nil if entry has a timestamp on date ON.
FROM, TO, and ON should be strings parseable by
`parse-time-string' but may omit the time value."
`parse-time-string' but may omit the time value.
TYPE may be `active' to match active timestamps, `inactive' to
match inactive ones, or `both' / nil to match both types."
;; TODO: DRY this with the clocked predicate.
;; NOTE: FROM and TO are actually expected to be Unix timestamps. The docstring is written
;; for end users, for which the arguments are pre-processed by `org-ql-select'.
;; FIXME: This assumes every "clocked" entry is a range. Unclosed clock entries are not handled.
(cl-macrolet ((next-timestamp ()
`(when (re-search-forward org-element--timestamp-regexp end-pos t)
(save-excursion
(goto-char (match-beginning 0))
(org-element-timestamp-parser))))
`(when (re-search-forward regexp end-pos t)
(ts-parse-org (match-string 0))))
(test-timestamps (pred-form)
`(cl-loop for next-ts = (next-timestamp)
while next-ts
do (setf beg (float-time (org-timestamp-to-time next-ts))
end (float-time (org-timestamp-to-time next-ts 'end)))
thereis ,pred-form)))
(save-excursion
(let ((end-pos (org-entry-end-position))
beg end)
(cond ((not (or from to)) (next-timestamp))
((and from to) (test-timestamps (and (<= beg to)
(>= end from))))
(from (test-timestamps (<= from end)))
(to (test-timestamps (<= beg to))))))))
(org-ql--defpred ts-active (&key from to _on)
;; The underscore before `on' prevents "unused lexical variable" warnings, because we
;; pre-process that argument in a macro before this function is called.
"Return non-nil if current entry has an active timestamp in given period.
If no arguments are specified, return non-nil if entry has any
active timestamp.
If FROM, return non-nil if entry has an active timestamp on or
after FROM.
If TO, return non-nil if entry has an active timestamp on or
before TO.
If ON, return non-nil if entry has an active timestamp on date
ON.
FROM, TO, and ON should be strings parseable by
`parse-time-string' but may omit the time value."
;; TODO: DRY this with the clocked predicate.
;; NOTE: FROM and TO are actually expected to be Unix timestamps. The docstring is written
;; for end users, for which the arguments are pre-processed by `org-ql-select'.
;; FIXME: This assumes every "clocked" entry is a range. Unclosed clock entries are not handled.
(cl-macrolet ((next-timestamp ()
`(when (re-search-forward org-element--timestamp-regexp end-pos t)
(save-excursion
(goto-char (match-beginning 0))
(org-element-timestamp-parser))))
(test-timestamps (pred-form)
`(cl-loop for next-ts = (next-timestamp)
while next-ts
when (string-prefix-p "<" next-ts)
do (setf beg (float-time (org-timestamp-to-time next-ts))
end (float-time (org-timestamp-to-time next-ts 'end)))
thereis ,pred-form)))
(save-excursion
(let ((end-pos (org-entry-end-position))
beg end)
(cond ((not (or from to)) (next-timestamp))
((and from to) (test-timestamps (and (<= beg to)
(>= end from))))
(from (test-timestamps (<= from end)))
(to (test-timestamps (<= beg to))))))))
(org-ql--defpred ts-inactive (&key from to _on)
;; The underscore before `on' prevents "unused lexical variable" warnings, because we
;; pre-process that argument in a macro before this function is called.
"Return non-nil if current entry has an inactive timestamp in given period.
If no arguments are specified, return non-nil if entry has any
inactive timestamp.
If FROM, return non-nil if entry has an inactive timestamp on or
after FROM.
If TO, return non-nil if entry has an inactive timestamp on or
before TO.
If ON, return non-nil if entry has an inactive timestamp on date
ON.
FROM, TO, and ON should be strings parseable by
`parse-time-string' but may omit the time value."
;; TODO: DRY this with the clocked predicate.
;; NOTE: FROM and TO are actually expected to be Unix timestamps. The docstring is written
;; for end users, for which the arguments are pre-processed by `org-ql-select'.
;; FIXME: This assumes every "clocked" entry is a range. Unclosed clock entries are not handled.
(cl-macrolet ((next-timestamp ()
`(when (re-search-forward org-element--timestamp-regexp end-pos t)
(save-excursion
(goto-char (match-beginning 0))
(org-element-timestamp-parser))))
(test-timestamps (pred-form)
`(cl-loop for next-ts = (next-timestamp)
while next-ts
when (string-prefix-p "[" next-ts)
do (setf beg (float-time (org-timestamp-to-time next-ts))
end (float-time (org-timestamp-to-time next-ts 'end)))
thereis ,pred-form)))
(save-excursion
(let ((end-pos (org-entry-end-position))
beg end)
(cond ((not (or from to)) (next-timestamp))
((and from to) (test-timestamps (and (<= beg to)
(>= end from))))
(from (test-timestamps (<= from end)))
(to (test-timestamps (<= beg to))))))))
(let ((end-pos (org-entry-end-position)))
(cond ((not (or from to)) (re-search-forward regexp end-pos t))
((and from to) (test-timestamps (and (ts<= from next-ts)
(ts<= next-ts to))))
(from (test-timestamps (ts<= from next-ts)))
(to (test-timestamps (ts<= next-ts to))))))))
;;;;; Date comparison