3

I am trying to create an agenda search that matches on the DATE property of certain entries.

This works fine for one-off tasks/events; however, when I have a repeating task whose DATE is of the form <some-ancient-date +1w>, running a search like

DATE>"<now>"+DATE<"<tomorrow>"

to get my upcoming events for today doesn't capture this task because its DATE value is the date at which I first defined this task. How can I modify my query to capture the fact that this event is supposed to recur today?

Minimal Working Example

Suppose my agenda file has the following entries:

* Receive Nobel prize
  :PROPERTIES:
  :DATE: <2019-06-04 10:00>
  :END:
* Floss
  :PROPERTIES:
  :DATE: <2019-01-01 7:30 +1d>
  :END:

And I have set up my init.el with

(setq org-agenda-custom-commands
      '(("d" "Day's events"
         ((tags "DATE>=\"<now>\"+DATE<\"<tomorrow>\""
                ((org-agenda-overriding-header "Today's events")))))))

Exepcted output

When I press C-c C-a d (assuming today is June 4, 2019), I expect to see

Today's events
agenda: Receive Nobel prize
agenda: Floss

Actual output

Today's events
agenda: Receive Nobel prize

I think this is because the matcher looks at the date at which the recurring task was defined and does not actually do the math to realize that it's supposed to recur today. My question is, can I force the matcher to somehow do this math? I think this should be easy considering that the day view of the agenda has that information, I just don't know how to go about it.

Aside Please let's not quibble over the fact that "Floss" is not an event but a habit and that I shouldn't be reporting it in my agenda this way: my actual use case has a recurring event where it makes sense for me to report it like this.

A (non-ideal) workaround

One way to overcome this problem is to convert this headline to a TODO item and use the SCHEDULED property instead of the date property. This way, every time you mark the task as DONE (or any other "finished" state), your SCHEDULED property will get updated with the next date and you can match on that quite easily. The problem is, my use case is for an optional recurring event that I may or may not want to attend. I don't want it to appear as a TODO item in my agenda and get automatically carried over to the next day if I don't mark it DONE.

  • Can you provide the code or command you're using, and possibly a minimal working example as well. – jagrg Jun 5 at 0:42
  • @jagrg Done. Please see the updated question. – Joseph R. Jun 5 at 3:19
2

One way of solving your problem is to add a skip function to the agenda command (see org-agenda-skip-function), skipping all entries not matching any of the following conditions:

  1. The timestamp matches today's date
  2. The timestamp contains a +1d repeater
  3. The timestamp contains a +Nd or +Nw repeater AND the number of days between DATE and NOW divided by the repeater is equal to a non-negative integer
  4. The timestamp contains a +Nm repeater AND the day of month matches today's day of month
(setq org-agenda-custom-commands
      '(("d" "Day's events"
         ((tags "DATE={.}"
                ((org-agenda-skip-function 'my-skip-fun)
                 (org-agenda-overriding-header "Today's events")))))))
(defun repeater-to-days (repeater)
  (when (string-match "\\+\\([[:digit:]]\\)+\\([a-z]\\)" repeater)
    (let ((value (match-string 1 repeater))
          (unit  (match-string 2 repeater)))
      (cond ((string= unit "d")
             (string-to-number value))
            ((string= unit "w")
             (* (string-to-number value) 7))))))

(defmacro strip-trailing-zero (num)
  `(if (= (floor ,num) (float ,num))
       (floor ,num) ,num))

(defun my-skip-fun ()
  (let ((metadata-end (save-excursion
                        (org-end-of-meta-data))))
    (if (and (re-search-forward (org-re-timestamp 'active) metadata-end t)
             (let* ((match (match-string 0))
                    (days (- (org-time-stamp-to-now match)))
                    (repeater (or (org-get-repeat match) ""))
                    (repeater-num (repeater-to-days repeater))
                    (time (org-time-string-to-time match))
                    (ts (format-time-string "%F" time))
                    (dd (format-time-string "%d" time)))
               (or (string= (format-time-string "%F" (current-time)) ts)
                   (string= "+1d" repeater)
                   (and repeater-num
                        (wholenump (strip-trailing-zero
                                    (/ days (float repeater-num)))))
                   (and (string-match "\\+[[:digit:]]+m" repeater)
                        (string= (format-time-string "%d" (current-time)) dd)))))
        nil
      (save-excursion
        (org-end-of-subtree)))))
  • Amazing! I'll test it later tonight and accept this if it works. Thank you. One question: why [[:digit:]]* and not [[:digit:]]+? Can you have something like +w? – Joseph R. Jun 6 at 2:07
  • Also, not to diminish the value of your solution but wouldn't this fail if I had a repeater like +2w so that looking at the weekday by itself is not sufficient? – Joseph R. Jun 6 at 2:15
  • This works great with the caveat I anticipated. Biweekly events are matched every week but barring more complicated calendar acrobatics in the function's logic, I don't think this is easily solved. Your code, as is, has a little bug that was tripping me up at first. When (org-get-repeat match) returns nil, the first regex match on repeater errors out. I fixed it by modifying the let* binding to have (repeater-maybe (org-get-repeat match)) followed by (repeater (concat "" repeater-maybe)). I have suggested an edit with that fix. – Joseph R. Jun 6 at 9:34
  • I think one way to solve this problem is to count the number of days between DATE and NOW and divide it by the repeater. If the result is not a non-negative integer then we skip it. I'll try this and report back. – jagrg Jun 6 at 18:41
  • Yeah, that's what I meant by "calendar acrobatics". I can probably add that stuff on my own to your implementation if the need arises. If you want to add it here for the sake of completeness and for other people's sake, feel free but this answer is complete as far as I'm concerned. Thank you :) – Joseph R. Jun 6 at 20:58

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.