Function: icalendar-recur-find-absolute-interval
icalendar-recur-find-absolute-interval is a byte-compiled function
defined in icalendar-recur.el.gz.
Signature
(icalendar-recur-find-absolute-interval TARGET DTSTART INTERVALSIZE FREQS &optional VTIMEZONE)
Documentation
Find a recurrence interval based on a fixed number of seconds.
INTERVALSIZE should be the total size of the interval in seconds. FREQS
should be the number of seconds between the lower bound of the interval
and the upper bound for candidate recurrences; it is the number of
seconds in the unit of time in a recurrence rule's FREQ part. The
returned interval looks like (LOW LOW+FREQS LOW+INTERVALSIZE). See
icalendar-recur-find-interval for other arguments' meanings.
Source Code
;; Defined in /usr/src/emacs/lisp/calendar/icalendar-recur.el.gz
(aref interval (1- (length interval)))) ; = NEXT-LOW if present, HIGH otherwise
;; The following functions deal with constructing intervals, given a
;; target, a start date/time, an intervalsize, and optionally a time
;; zone. The main entry point is `icalendar-recur-find-interval'.
;; Look, dragons already:
(defun icr:find-absolute-interval (target dtstart intervalsize freqs
&optional vtimezone)
"Find a recurrence interval based on a fixed number of seconds.
INTERVALSIZE should be the total size of the interval in seconds. FREQS
should be the number of seconds between the lower bound of the interval
and the upper bound for candidate recurrences; it is the number of
seconds in the unit of time in a recurrence rule's FREQ part. The
returned interval looks like (LOW LOW+FREQS LOW+INTERVALSIZE). See
`icalendar-recur-find-interval' for other arguments' meanings."
;; We assume here that the interval needs to be calculated using
;; absolute times for SECONDLY, MINUTELY, and HOURLY rules.
;; There are two reasons for this:
;;
;; 1) Time zone shifts. If we don't use absolute times, and instead
;; find interval boundaries using local clock times with e.g.
;; `ical:date/time-add' (as we do with time units of a day or
;; greater below), we have to adjust for clock time changes. Using
;; absolute times is simpler.
;; 2) More problematically, using local clock times, at least in its
;; most straightforward implementation, has pathological results
;; when `intervalsize' is relatively prime with 60 (for a SECONDLY
;; rule, similarly for the others): intervals generated by
;; successive enumeration from one target value will not in general
;; align with intervals generated from a different, but nearby,
;; target value. (So going this route seems to mean giving up on
;; the idea that intervals can be calculated just from `target',
;; `dtstart' and `intervalsize', and instead always enumerating
;; them from the beginning.)
;;
;; In effect, we are deciding that a rule like "every 3 hours" always
;; means every 3 * 60 * 60 = 10800 seconds after `dtstart', and not
;; "every 10800 seconds, except when there's a time zone observance
;; change". People who want the latter have another option: use a
;; DAILY rule and specify the (local) times for the hours they want in
;; the BYHOUR clause, etc. (People who want it for a number of hours,
;; e.g. 7, which does not divide 24, unfortunately do *not* have this
;; option, but anyone who wants that but does not want to understand
;; "7 hours" as a fixed number of seconds has a pathology that I
;; cannot cure here.)
;;
;; RFC5545 does not seem to pronounce one way or the other on whether
;; this decision is correct: there are no examples of SECONDLY rules
;; to go on, and the few examples for MINUTELY and HOURLY rules only
;; use "nice" values in the INTERVAL clause (real-life examples
;; probably(?) will too). Our assumption has some possibly
;; unintuitive consequences for `intervalsize' values that are not
;; "nice" (basically, whenever intervalsize and either 60 or 24 are
;; relatively prime), and for how interval boundaries behave at the
;; shifts between time zone observances (since local clock times in
;; the interval bounds will shift from what they would have been
;; before the observance change -- arguably correct but possibly
;; surprising, depending on the case). But the alternative seems
;; worse, so until countervailing evidence emerges, this approach
;; seems reasonable.
(let* ((given-start-zone (decoded-time-zone dtstart))
(start-w/zone (cond (given-start-zone dtstart)
((ical:vtimezone-component-p vtimezone)
(ical:date-time-variant dtstart :tz vtimezone))
(t
;; "Floating" time should be interpreted in user's
;; current time zone; see RFC5545 Sec 3.3.5
(ical:date-time-variant
dtstart :zone (car (current-time-zone))))))
(start-abs (ignore-errors
(time-convert (encode-time start-w/zone) 'integer)))
(given-target-zone (decoded-time-zone target))
(target-w/zone (cond (given-target-zone target)
(vtimezone
(ical:date-time-variant target :tz vtimezone))
(t
(ical:date-time-variant
target :zone (car (current-time-zone))))))
(target-abs (ignore-errors
(time-convert (encode-time target-w/zone) 'integer)))
low-abs low high next-low)
(unless (zerop (mod intervalsize freqs))
;; Bad things will happen if intervalsize is not an integer
;; multiple of freqs
(error "FREQS=%d does not divide INTERVALSIZE=%d" freqs intervalsize))
(unless (and start-abs target-abs)
(when (not start-abs)
(error "Could not determine an offset for DTSTART=%s" dtstart))
(when (not target-abs)
(error "Could not determine an offset for TARGET=%s" target)))
;; Find the lower bound below target that is the closest integer
;; multiple of intervalsize seconds from dtstart
(setq low-abs (- target-abs
(mod (- target-abs start-abs) intervalsize)))
(if vtimezone
(setq low (icr:tz-decode-time low-abs vtimezone)
high (icr:tz-decode-time (+ low-abs freqs) vtimezone)
next-low (icr:tz-decode-time (+ low-abs intervalsize) vtimezone))
;; best we can do is decode into target's zone:
(let ((offset (decoded-time-zone target-w/zone)))
(setq low (icr:tz-decode-time low-abs offset)
high (icr:tz-decode-time (+ low-abs freqs) offset)
next-low (when (< 1 intervalsize)
(icr:tz-decode-time (+ low-abs intervalsize) offset)))))
(unless (and given-start-zone given-target-zone)
;; but if we started with floating times, we should return floating times:
(setf (decoded-time-zone low) nil)
(setf (decoded-time-dst low) -1)
(setf (decoded-time-zone high) nil)
(setf (decoded-time-dst high) -1)
(when next-low
(setf (decoded-time-zone next-low) nil)
(setf (decoded-time-dst next-low) -1)))
(icr:make-interval low high next-low)))