Function: diary-rrule

diary-rrule is a byte-compiled function defined in diary-icalendar.el.gz.

Signature

(diary-rrule &key RULE START DURATION INCLUDE EXCLUDE)

Documentation

Diary S-expression for iCalendar recurrence rules.

Entry applies if the queried date matches the recurrence rule.

The keyword arguments RULE, START, INCLUDE and EXCLUDE should contain the recurrence data from an iCalendar component. RULE should be an icalendar-recur value, START an icalendar-date or icalendar-date-time, DURATION an icalendar-dur-value, and INCLUDE and EXCLUDE should be lists of icalendar-date or icalendar-date-time values (of the same type as START).

Source Code

;; Defined in /usr/src/emacs/lisp/calendar/diary-icalendar.el.gz
;; To be called from diary-sexp-entry, where DATE, ENTRY are bound.
(cl-defun diary-rrule (&key rule start duration include exclude)
  "Diary S-expression for iCalendar recurrence rules.

Entry applies if the queried date matches the recurrence rule.

The keyword arguments RULE, START, INCLUDE and EXCLUDE should contain
the recurrence data from an iCalendar component.  RULE should be an
`icalendar-recur' value, START an `icalendar-date' or
`icalendar-date-time', DURATION an `icalendar-dur-value', and INCLUDE
and EXCLUDE should be lists of `icalendar-date' or `icalendar-date-time'
values (of the same type as START)."
  ;; TODO: also support a format that is nicer to read and type by hand.
  ;; e.g. just letting a rule be specified in a recur-value string like
  ;;   :rule "FREQ=MONTHLY;BYDAY=1SU"
  ;; is perhaps already better than the raw Lisp format.  We could at least
  ;; support specifying the clauses with keywords, e.g.
  ;;   :freq :monthly :byday '("Sunday" . 1)
  ;; would be better than the current
  ;;   :rule '((FREQ MONTHLY) (BYDAY ((0 . 1))))
  (with-no-warnings (defvar date) (defvar entry))
  (when (ical:date<= start date)
    (let* ((vevent (ical:make-vevent
                    (ical:rrule rule)
                    (ical:dtstart start)
                    (ical:rdate include)
                    (ical:exdate exclude)))
           (interval (icr:find-interval date start rule)))
      (cl-typecase start
        (ical:date
          (if (ical:rrule-count rule)
              (when (member date (icr:recurrences-to-count vevent))
                entry)
            (when (member date (icr:recurrences-in-interval interval vevent))
              entry)))
        (ical:date-time
         ;; TODO.  If start is a date-time, it was probably imported from
         ;; an iCalendar file, but in order to calculate recurrences, we
         ;; really need all the time zone information from that file,
         ;; not just the rule, start, include and exclude.  But encoding
         ;; all that tz info in a diary s-exp is cumbersome and ugly and
         ;; probably not worth the trouble.  Since this is the diary, we
         ;; assume that all we really care about here is whether there
         ;; are recurrences on a particular day.  Thus we convert
         ;; HOURLY/MINUTELY/SECONDLY rules to a DAILY rule, and all
         ;; values to plain dates.  This keeps things simple (and
         ;; hopefully quicker) but means that information gets lost.  I
         ;; hope this can be changed to do things right at some point,
         ;; but that will require first adding more robust time zone
         ;; support to the diary somehow -- perhaps via #included
         ;; iCalendar files?
         (let* ((date-rule (copy-sequence rule))
                (start-date (ical:date-time-to-date start))
                (include-dates (mapcar #'ical:date-time-to-date include))
                (exclude-dates (mapcar #'ical:date-time-to-date exclude))
                ;; Preserve the clock times in the entry:
                (entry-time
                 (if duration
                     (di:format-time-range
                      start
                      (ical:date/time-add-duration start duration))
                   (di:format-time-as-local start)))
                (date-entry (concat entry-time " " entry)))
           (when (memq (ical:rrule-freq date-rule) '(HOURLY MINUTELY SECONDLY))
             (setf (alist-get 'FREQ date-rule) 'DAILY)
             (setf (alist-get 'INTERVAL date-rule) 1)
             (setf (alist-get 'BYHOUR date-rule nil t) nil)
             (setf (alist-get 'BYMINUTE date-rule nil t) nil)
             (setf (alist-get 'BYSECOND date-rule nil t) nil))
           ;; Recurse with the plain date values:
           (calendar-dlet
               ((date date)
                (entry date-entry))
             (diary-rrule :rule date-rule :start start-date
                          :include include-dates :exclude exclude-dates))))))))