org-element-timestamp-parser: Allow time in diary sexp timestamps

* lisp/org-agenda.el (org-agenda-get-timestamps):
* lisp/org-element.el (org-element--timestamp-regexp): Adjust
timestamp regexp.
(org-element-timestamp-parser): Support the new syntax for diary sexp
timestamps.  The diary sexp is now stored in :diary-sexp property and
the time/time range is stored as usual.
(org-element-timestamp-interpreter): Interpret diary timestamp
according to its building blocks rather than raw value.
* testing/lisp/test-org-agenda.el (test-org-agenda/diary-timestamp):
New test checking for agenda support of times in diary timestamps.
*
testing/lisp/test-org-element.el (test-org-element/timestamp-interpreter):
Add parser tests.
* doc/org-manual.org (Timestamps): Add an example of the new syntax to
the manual.
* etc/ORG-NEWS (Diary type timestamps now support optional
time/timerange): Document the Org syntax addition.

This syntax modification is fixing an omission in org-element.el.  In
the past, org-agenda had explicit support for diary timestamps with
time/timerange, but that support was ad-hoc.  Now, after org-agenda
switched to use parser, we must modify Org syntax to fix the feature
regression.
This commit is contained in:
Ihor Radchenko 2024-01-08 13:33:59 +01:00
parent 1cafe3e8e4
commit 5da0eb6ea7
No known key found for this signature in database
GPG Key ID: 6470762A7DA11D8B
6 changed files with 283 additions and 174 deletions

View File

@ -6156,7 +6156,7 @@ the agenda (see [[*Weekly/daily agenda]]). We distinguish:
#+begin_example
,* 22:00-23:00 The nerd meeting on every 2nd Thursday of the month
<%%(diary-float t 4 2)>
<%%(diary-float t 4 2) 22:00-23:00>
#+end_example
- Time range ::

View File

@ -339,7 +339,43 @@ Now, ~org-store-link~ moves the stored link to front of the list of
stored links. This way, the link will show up first in the completion
and when inserting all the stored links with ~org-insert-all-links~.
*** Major changes and additions to Org API
*** Major changes and additions to Org element API
**** Diary type timestamps now support optional time/timerange
Previously, diary type timestamps could not specify time.
Now, it is allowed to add a time or time range:
: <%%(diary-float t 4 2) 22:00-23:00>
: <%%(diary-float t 4 2) 10:30>
The parsed representation of such timestamps will have ~:hour-start~,
~:minute-start~, ~:hour-end~, ~:minute-end~, and ~:range-type~
properties set appropriately. In addition, a new ~:diary-sexp~
property will store the diary sexp value.
For example,
: <%%(diary-float t 4 2) 22:00-23:00>
will have the following properties
#+begin_src emacs-lisp
:type: diary
:range-type: timerange
:raw-value: "<%%(diary-float t 4 2) 22:00-23:00>"
:year-start: nil
:month-start: nil
:day-start: nil
:hour-start: 22
:minute-start: 0
:year-end: nil
:month-end: nil
:day-end: nil
:hour-end: 23
:minute-end: 0
:diary-sexp: "(diary-float t 4 2)"
#+end_src
**** New term: "syntax node"
To reduce confusion with "element" referring to both "syntax element"

View File

@ -5831,7 +5831,7 @@ displayed in agenda view."
(org-encode-time ; DATE bound by calendar
0 0 0 (nth 1 date) (car date) (nth 2 date))))
"\\|\\(<[0-9]+-[0-9]+-[0-9]+[^>\n]+?\\+[0-9]+[hdwmy]>\\)"
"\\|\\(<%%\\(([^>\n]+)\\)>\\)"))
"\\|\\(<%%\\(([^>\n]+)\\)\\([^\n>]*\\)>\\)"))
timestamp-items)
(goto-char (point-min))
(while (re-search-forward regexp nil t)

View File

@ -4282,7 +4282,7 @@ Assume point is at the target."
"\\|"
"\\(?:<[0-9]+-[0-9]+-[0-9]+[^>\n]+?\\+[0-9]+[dwmy]>\\)"
"\\|"
"\\(?:<%%\\(?:([^>\n]+)\\)>\\)")
"\\(?:<%%\\(?:([^>\n]+)\\)\\([^\n>]*\\)>\\)")
"Regexp matching any timestamp type object.")
(defconst org-element--timestamp-raw-value-regexp
@ -4300,8 +4300,8 @@ containing `:type', `:range-type', `:raw-value', `:year-start',
`:year-end', `:month-end', `:day-end', `:hour-end', `:minute-end',
`:repeater-type', `:repeater-value', `:repeater-unit',
`:repeater-deadline-value', `:repeater-deadline-unit', `:warning-type',
`:warning-value', `:warning-unit', `:begin', `:end' and `:post-blank'
properties. Otherwise, return nil.
`:warning-value', `:warning-unit', `:diary-sexp', `:begin', `:end' and
`:post-blank' properties. Otherwise, return nil.
Assume point is at the beginning of the timestamp."
(when (looking-at-p org-element--timestamp-regexp)
@ -4312,15 +4312,25 @@ Assume point is at the beginning of the timestamp."
(progn
(looking-at org-element--timestamp-raw-value-regexp)
(match-string-no-properties 0)))
(date-start (match-string-no-properties 1))
(date-end (match-string-no-properties 3))
(diaryp (match-beginning 2))
diary-sexp
(date-start (if diaryp
;; Only consider part after sexp for
;; diary timestamps.
(save-match-data
(looking-at org-element--timestamp-regexp)
(setq diary-sexp
(buffer-substring-no-properties
(+ 3 (match-beginning 0))
(match-beginning 2)))
(match-string 2))
(match-string-no-properties 1)))
(date-end (match-string-no-properties 3))
(post-blank (progn (goto-char (match-end 0))
(skip-chars-forward " \t")))
(end (point))
(time-range
(and (not diaryp)
(string-match
(when (string-match
"[012]?[0-9]:[0-5][0-9]\\(-\\([012]?[0-9]\\):\\([0-5][0-9]\\)\\)"
date-start)
(cons (string-to-number (match-string 2 date-start))
@ -4395,6 +4405,17 @@ Assume point is at the beginning of the timestamp."
day-end (or (nth 3 date) day-start)
hour-end (or (nth 2 date) (car time-range) hour-start)
minute-end (or (nth 1 date) (cdr time-range) minute-start))))
;; Diary timestamp with time.
(when (and diaryp
(string-match "\\([012]?[0-9]\\):\\([0-5][0-9]\\)\\(-\\([012]?[0-9]\\):\\([0-5][0-9]\\)\\)?" date-start))
(setq hour-start (match-string 1 date-start)
minute-start (match-string 2 date-start)
hour-end (match-string 4 date-start)
minute-end (match-string 5 date-start))
(when hour-start (setq hour-start (string-to-number hour-start)))
(when minute-start (setq minute-start (string-to-number minute-start)))
(when hour-end (setq hour-end (string-to-number hour-end)))
(when minute-end (setq minute-end (string-to-number minute-end))))
(org-element-create
'timestamp
(nconc (list :type type
@ -4413,19 +4434,20 @@ Assume point is at the beginning of the timestamp."
:begin begin
:end end
:post-blank post-blank)
(and diary-sexp (list :diary-sexp diary-sexp))
repeater-props
warning-props))))))
(defun org-element-timestamp-interpreter (timestamp _)
"Interpret TIMESTAMP object as Org syntax."
(let((type (org-element-property :type timestamp)))
(if (member type '(active inactive inactive-range active-range))
(let ((day-start (org-element-property :day-start timestamp))
(month-start (org-element-property :month-start timestamp))
(year-start (org-element-property :year-start timestamp)))
;; Return nil when start date is not available. Could also
;; throw an error, but the current behavior is historical.
(when (and day-start month-start year-start)
(when (or (and day-start month-start year-start)
(eq type 'diary))
(let* ((repeat-string
(concat
(pcase (org-element-property :repeater-type timestamp)
@ -4459,6 +4481,7 @@ Assume point is at the beginning of the timestamp."
type
'(inactive inactive-range))
(cons "[" "]")
;; diary as well
(cons "<" ">")))
(timestamp-end
(concat
@ -4469,6 +4492,12 @@ Assume point is at the beginning of the timestamp."
;; Opening backet: [ or <
(car brackets)
;; Starting date/time: YYYY-MM-DD DAY[ HH:MM]
(if (eq type 'diary)
(concat
"%%"
(org-element-property :diary-sexp timestamp)
(when (and minute-start hour-start)
(format " %02d:%02d" hour-start minute-start)))
(format-time-string
;; `org-time-stamp-formats'.
(org-time-stamp-format
@ -4478,7 +4507,7 @@ Assume point is at the beginning of the timestamp."
'no-brackets)
(org-encode-time
0 (or minute-start 0) (or hour-start 0)
day-start month-start year-start))
day-start month-start year-start)))
;; Range: -HH:MM or TIMESTAMP-END--[YYYY-MM-DD DAY HH:MM]
(let ((hour-end (org-element-property :hour-end timestamp))
(minute-end (org-element-property :minute-end timestamp)))
@ -4504,7 +4533,8 @@ Assume point is at the beginning of the timestamp."
((or `timerange `daterange)
(error "`:range-type' must be `nil' for `active'/`inactive' type"))))
;; Range must be present.
((or `active-range `inactive-range)
((or `active-range `inactive-range
(and `diary (guard (eq 'timerange range-type))))
(pcase range-type
;; End time: -HH:MM.
;; Fall back to start time if end time is not defined (arbitrary historical choice).
@ -4541,9 +4571,7 @@ Assume point is at the beginning of the timestamp."
(or (org-element-property :year-end timestamp) year-start)))))))))
;; repeater + warning + closing > or ]
;; This info is duplicated in date ranges.
timestamp-end))))
;; diary type.
(org-element-property :raw-value timestamp))))
timestamp-end))))))
;;;; Underline
(defun org-element-underline-parser ()

View File

@ -728,6 +728,28 @@ Sunday 7 January 2024
(org-agenda-list nil nil 1)
(should (search-forward "In " nil t))))))
(ert-deftest test-org-agenda/diary-timestamp ()
"Test diary timestamp handling."
(org-test-at-time
"2024-01-15"
(org-test-agenda-with-agenda
"* TODO foo\n<%%(diary-date 01 15 2024)>"
(org-agenda-list nil nil 1)
(should (search-forward "foo" nil t)))
(org-test-agenda-with-agenda
"* TODO foo\n<%%(diary-date 02 15 2024)>"
(org-agenda-list nil nil 1)
(should-not (search-forward "foo" nil t)))
;; Test time and time ranges in diary timestamps.
(org-test-agenda-with-agenda
"* TODO foo\n<%%(diary-date 01 15 2024) 12:00>"
(org-agenda-list nil nil 1)
(should (search-forward "12:00" nil t)))
(org-test-agenda-with-agenda
"* TODO foo\n<%%(diary-date 01 15 2024) 12:00-14:00>"
(org-agenda-list nil nil 1)
(should (search-forward "12:00-14:00" nil t)))))
;; agenda redo

View File

@ -3989,8 +3989,31 @@ DEADLINE: <2012-03-29 thu.> SCHEDULED: <2012-03-29 thu.> CLOSED: [2012-03-29 thu
(org-test-parse-and-interpret
"<2012-03-29 thu. 16:40-16:41>")))
;; Diary.
(should (equal (org-test-parse-and-interpret "<%%diary-float t 4 2>")
"<%%diary-float t 4 2>\n"))
(should (equal (org-test-parse-and-interpret "<%%(diary-float t 4 2)>")
"<%%(diary-float t 4 2)>\n"))
;; Diary with time.
(should (equal (org-test-parse-and-interpret "<%%(diary-float t 4 2) 12:00>")
"<%%(diary-float t 4 2) 12:00>\n"))
(should (equal (org-test-parse-and-interpret "<%%(diary-cyclic 1 1 1 2020) 12:00-14:00>")
"<%%(diary-cyclic 1 1 1 2020) 12:00-14:00>\n"))
(org-test-with-temp-text "<%%(diary-float t 4 2) 12:00>"
(let ((ts (org-element-context)))
(should (org-element-type-p ts 'timestamp))
(should (eq 'diary (org-element-property :type ts)))
(should (eq nil (org-element-property :range-type ts)))
(should (equal 12 (org-element-property :hour-start ts)))
(should (equal 0 (org-element-property :minute-start ts)))
(should-not (org-element-property :hour-end ts))
(should-not (org-element-property :minute-end ts))))
(org-test-with-temp-text "<%%(diary-float t 4 2) 12:00-14:01>"
(let ((ts (org-element-context)))
(should (org-element-type-p ts 'timestamp))
(should (eq 'diary (org-element-property :type ts)))
(should (eq 'timerange (org-element-property :range-type ts)))
(should (equal 12 (org-element-property :hour-start ts)))
(should (equal 0 (org-element-property :minute-start ts)))
(should (equal 14 (org-element-property :hour-end ts)))
(should (equal 1 (org-element-property :minute-end ts)))))
;; Timestamp with repeater interval, repeater deadline, with delay, with combinations.
(should
(string-match "<2012-03-29 .* \\+1y>"