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,19 +4312,29 @@ 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
"[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))
(string-to-number (match-string 3 date-start)))))
(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))
(string-to-number (match-string 3 date-start)))))
(type (cond (diaryp 'diary)
((and activep (or date-end time-range)) 'active-range)
(activep 'active)
@ -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,137 +4434,144 @@ 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)
(let* ((repeat-string
(concat
(pcase (org-element-property :repeater-type timestamp)
(`cumulate "+") (`catch-up "++") (`restart ".+"))
(let ((val (org-element-property :repeater-value timestamp)))
(and val (number-to-string val)))
(pcase (org-element-property :repeater-unit timestamp)
(`hour "h") (`day "d") (`week "w") (`month "m") (`year "y"))
(when-let ((repeater-deadline-value
(org-element-property :repeater-deadline-value timestamp))
(repeater-deadline-unit
(org-element-property :repeater-deadline-unit timestamp)))
(concat
"/"
(number-to-string repeater-deadline-value)
(pcase repeater-deadline-unit
(`hour "h") (`day "d") (`week "w") (`month "m") (`year "y"))))))
(range-type (org-element-property :range-type timestamp))
(warning-string
(concat
(pcase (org-element-property :warning-type timestamp)
(`first "--") (`all "-"))
(let ((val (org-element-property :warning-value timestamp)))
(and val (number-to-string val)))
(pcase (org-element-property :warning-unit timestamp)
(`hour "h") (`day "d") (`week "w") (`month "m") (`year "y"))))
(hour-start (org-element-property :hour-start timestamp))
(minute-start (org-element-property :minute-start timestamp))
(brackets
(if (member
type
'(inactive inactive-range))
(cons "[" "]")
(cons "<" ">")))
(timestamp-end
(concat
(and (org-string-nw-p repeat-string) (concat " " repeat-string))
(and (org-string-nw-p warning-string) (concat " " warning-string))
(cdr brackets))))
(concat
;; Opening backet: [ or <
(car brackets)
;; Starting date/time: YYYY-MM-DD DAY[ HH:MM]
(format-time-string
;; `org-time-stamp-formats'.
(org-time-stamp-format
;; Ignore time unless both HH:MM are available.
;; Ignore means (car org-timestamp-formats).
(and minute-start hour-start)
'no-brackets)
(org-encode-time
0 (or minute-start 0) (or hour-start 0)
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)))
(pcase type
((or `active `inactive)
;; `org-element-timestamp-parser' uses this type
;; when no time/date range is provided. So,
;; should normally return nil in this clause.
(pcase range-type
(`nil
;; `org-element-timestamp-parser' assigns end
;; times for `active'/`inactive' TYPE if start
;; time is not nil. But manually built
;; timestamps may not contain end times, so
;; check for end times anyway.
(when (and hour-start hour-end minute-start minute-end
(or (/= hour-start hour-end)
(/= minute-start minute-end)))
;; Could also throw an error. Return range
;; timestamp nevertheless to preserve
;; historical behavior.
(format "-%02d:%02d" hour-end minute-end)))
((or `timerange `daterange)
(error "`:range-type' must be `nil' for `active'/`inactive' type"))))
;; Range must be present.
((or `active-range `inactive-range)
(pcase range-type
;; End time: -HH:MM.
;; Fall back to start time if end time is not defined (arbitrary historical choice).
;; Error will be thrown if both end and begin time is not defined.
(`timerange (format "-%02d:%02d" (or hour-end hour-start) (or minute-end minute-start)))
;; End date: TIMESTAMP-END--[YYYY-MM-DD DAY HH:MM
((or `daterange
;; Should never happen in the output of `org-element-timestamp-parser'.
;; Treat as an equivalent of `daterange' arbitrarily.
`nil)
(concat
;; repeater + warning + closing > or ]
;; This info is duplicated in date ranges.
timestamp-end
"--" (car brackets)
(format-time-string
;; `org-time-stamp-formats'.
(org-time-stamp-format
;; Ignore time unless both HH:MM are available.
;; Ignore means (car org-timestamp-formats).
(and minute-end hour-end)
'no-brackets)
(org-encode-time
;; Closing HH:MM missing is a valid scenario.
0 (or minute-end 0) (or hour-end 0)
;; YEAR/MONTH/DAY-END will always be present
;; for `daterange' range-type, as parsed by
;; `org-element-timestamp-parser'.
;; For manually constructed timestamp
;; object, arbitrarily fall back to starting
;; date.
(or (org-element-property :day-end timestamp) day-start)
(or (org-element-property :month-end timestamp) month-start)
(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))))
(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 (or (and day-start month-start year-start)
(eq type 'diary))
(let* ((repeat-string
(concat
(pcase (org-element-property :repeater-type timestamp)
(`cumulate "+") (`catch-up "++") (`restart ".+"))
(let ((val (org-element-property :repeater-value timestamp)))
(and val (number-to-string val)))
(pcase (org-element-property :repeater-unit timestamp)
(`hour "h") (`day "d") (`week "w") (`month "m") (`year "y"))
(when-let ((repeater-deadline-value
(org-element-property :repeater-deadline-value timestamp))
(repeater-deadline-unit
(org-element-property :repeater-deadline-unit timestamp)))
(concat
"/"
(number-to-string repeater-deadline-value)
(pcase repeater-deadline-unit
(`hour "h") (`day "d") (`week "w") (`month "m") (`year "y"))))))
(range-type (org-element-property :range-type timestamp))
(warning-string
(concat
(pcase (org-element-property :warning-type timestamp)
(`first "--") (`all "-"))
(let ((val (org-element-property :warning-value timestamp)))
(and val (number-to-string val)))
(pcase (org-element-property :warning-unit timestamp)
(`hour "h") (`day "d") (`week "w") (`month "m") (`year "y"))))
(hour-start (org-element-property :hour-start timestamp))
(minute-start (org-element-property :minute-start timestamp))
(brackets
(if (member
type
'(inactive inactive-range))
(cons "[" "]")
;; diary as well
(cons "<" ">")))
(timestamp-end
(concat
(and (org-string-nw-p repeat-string) (concat " " repeat-string))
(and (org-string-nw-p warning-string) (concat " " warning-string))
(cdr brackets))))
(concat
;; 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
;; Ignore time unless both HH:MM are available.
;; Ignore means (car org-timestamp-formats).
(and minute-start hour-start)
'no-brackets)
(org-encode-time
0 (or minute-start 0) (or hour-start 0)
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)))
(pcase type
((or `active `inactive)
;; `org-element-timestamp-parser' uses this type
;; when no time/date range is provided. So,
;; should normally return nil in this clause.
(pcase range-type
(`nil
;; `org-element-timestamp-parser' assigns end
;; times for `active'/`inactive' TYPE if start
;; time is not nil. But manually built
;; timestamps may not contain end times, so
;; check for end times anyway.
(when (and hour-start hour-end minute-start minute-end
(or (/= hour-start hour-end)
(/= minute-start minute-end)))
;; Could also throw an error. Return range
;; timestamp nevertheless to preserve
;; historical behavior.
(format "-%02d:%02d" hour-end minute-end)))
((or `timerange `daterange)
(error "`:range-type' must be `nil' for `active'/`inactive' type"))))
;; Range must be present.
((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).
;; Error will be thrown if both end and begin time is not defined.
(`timerange (format "-%02d:%02d" (or hour-end hour-start) (or minute-end minute-start)))
;; End date: TIMESTAMP-END--[YYYY-MM-DD DAY HH:MM
((or `daterange
;; Should never happen in the output of `org-element-timestamp-parser'.
;; Treat as an equivalent of `daterange' arbitrarily.
`nil)
(concat
;; repeater + warning + closing > or ]
;; This info is duplicated in date ranges.
timestamp-end
"--" (car brackets)
(format-time-string
;; `org-time-stamp-formats'.
(org-time-stamp-format
;; Ignore time unless both HH:MM are available.
;; Ignore means (car org-timestamp-formats).
(and minute-end hour-end)
'no-brackets)
(org-encode-time
;; Closing HH:MM missing is a valid scenario.
0 (or minute-end 0) (or hour-end 0)
;; YEAR/MONTH/DAY-END will always be present
;; for `daterange' range-type, as parsed by
;; `org-element-timestamp-parser'.
;; For manually constructed timestamp
;; object, arbitrarily fall back to starting
;; date.
(or (org-element-property :day-end timestamp) day-start)
(or (org-element-property :month-end timestamp) month-start)
(or (org-element-property :year-end timestamp) year-start)))))))))
;; repeater + warning + closing > or ]
;; This info is duplicated in date ranges.
timestamp-end))))))
;;;; Underline
(defun org-element-underline-parser ()

View File

@ -690,43 +690,65 @@ Sunday 7 January 2024
(ert-deftest test-org-agenda/skip-deadline-prewarning-if-scheduled ()
"Test `org-agenda-skip-deadline-prewarning-if-scheduled'."
(org-test-at-time
"2024-01-15"
(let ((org-agenda-skip-deadline-prewarning-if-scheduled t))
(org-test-agenda-with-agenda
"* TODO foo\nDEADLINE: <2024-01-20 Sat> SCHEDULED: <2024-01-19 Fri>"
"2024-01-15"
(let ((org-agenda-skip-deadline-prewarning-if-scheduled t))
(org-test-agenda-with-agenda
"* TODO foo\nDEADLINE: <2024-01-20 Sat> SCHEDULED: <2024-01-19 Fri>"
(org-agenda-list nil nil 1)
(should-not (search-forward "In " nil t))))
(let ((org-agenda-skip-deadline-prewarning-if-scheduled 10))
(org-test-agenda-with-agenda
"* TODO foo\nDEADLINE: <2024-01-20 Sat> SCHEDULED: <2024-01-19 Fri>"
(org-agenda-list nil nil 1)
(should (search-forward "In " nil t))))
;; Custom prewarning cookie "-3d", so there should be no warning anyway.
(let ((org-agenda-skip-deadline-prewarning-if-scheduled 10))
(org-test-agenda-with-agenda
"* TODO foo\nDEADLINE: <2024-01-20 Sat -3d> SCHEDULED: <2024-01-19 Fri>"
(org-agenda-list nil nil 1)
(should-not (search-forward "In " nil t))))
(let ((org-agenda-skip-deadline-prewarning-if-scheduled 3))
(org-test-agenda-with-agenda
"* TODO foo\nDEADLINE: <2024-01-20 Sat> SCHEDULED: <2024-01-19 Fri>"
(org-agenda-list nil nil 1)
(should-not (search-forward "In " nil t))))
(let ((org-agenda-skip-deadline-prewarning-if-scheduled nil))
(org-test-agenda-with-agenda
"* TODO foo\nDEADLINE: <2024-01-20 Sat> SCHEDULED: <2024-01-19 Fri>"
(org-agenda-list nil nil 1)
(should (search-forward "In " nil t))))
(let ((org-agenda-skip-deadline-prewarning-if-scheduled 'pre-scheduled))
(org-test-agenda-with-agenda
"* TODO foo\nDEADLINE: <2024-01-20 Sat> SCHEDULED: <2024-01-16 Tue>"
(org-agenda-list nil nil 1)
(should-not (search-forward "In " nil t))))
(let ((org-agenda-skip-deadline-prewarning-if-scheduled 'pre-scheduled))
(org-test-agenda-with-agenda
"* TODO foo\nDEADLINE: <2024-01-20 Sat> SCHEDULED: <2024-01-15 Mon>"
(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-not (search-forward "In " nil t))))
(let ((org-agenda-skip-deadline-prewarning-if-scheduled 10))
(org-test-agenda-with-agenda
"* TODO foo\nDEADLINE: <2024-01-20 Sat> SCHEDULED: <2024-01-19 Fri>"
(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 (search-forward "In " nil t))))
;; Custom prewarning cookie "-3d", so there should be no warning anyway.
(let ((org-agenda-skip-deadline-prewarning-if-scheduled 10))
(org-test-agenda-with-agenda
"* TODO foo\nDEADLINE: <2024-01-20 Sat -3d> SCHEDULED: <2024-01-19 Fri>"
(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-not (search-forward "In " nil t))))
(let ((org-agenda-skip-deadline-prewarning-if-scheduled 3))
(org-test-agenda-with-agenda
"* TODO foo\nDEADLINE: <2024-01-20 Sat> SCHEDULED: <2024-01-19 Fri>"
(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-not (search-forward "In " nil t))))
(let ((org-agenda-skip-deadline-prewarning-if-scheduled nil))
(org-test-agenda-with-agenda
"* TODO foo\nDEADLINE: <2024-01-20 Sat> SCHEDULED: <2024-01-19 Fri>"
(org-agenda-list nil nil 1)
(should (search-forward "In " nil t))))
(let ((org-agenda-skip-deadline-prewarning-if-scheduled 'pre-scheduled))
(org-test-agenda-with-agenda
"* TODO foo\nDEADLINE: <2024-01-20 Sat> SCHEDULED: <2024-01-16 Tue>"
(org-agenda-list nil nil 1)
(should-not (search-forward "In " nil t))))
(let ((org-agenda-skip-deadline-prewarning-if-scheduled 'pre-scheduled))
(org-test-agenda-with-agenda
"* TODO foo\nDEADLINE: <2024-01-20 Sat> SCHEDULED: <2024-01-15 Mon>"
(org-agenda-list nil nil 1)
(should (search-forward "In " nil t))))))
(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>"