Function: icalendar-recur-tz-observance-on

icalendar-recur-tz-observance-on is a byte-compiled function defined in icalendar-recur.el.gz.

Signature

(icalendar-recur-tz-observance-on DT VTIMEZONE &optional UPDATE NONEXISTENT)

Documentation

Return the time zone observance in effect on DT in VTIMEZONE.

If there is such an observance, the returned value is a list (OBSERVANCE ONSET). OBSERVANCE is an icalendar-standard or icalendar-daylight component node. ONSET is the recurrence of OBSERVANCE (an icalendar-date-time) which occurs closest in time, but before, DT.

If there is no such observance in VTIMEZONE, the returned value is nil.

VTIMEZONE should be an icalendar-vtimezone component node.

DT may be an an icalendar-date-time or a Lisp timestamp. If it is a date-time, it represents a local time assumed to be in VTIMEZONE. Any existing offset in DT is ignored, and DT is compared with the local clock time at the start of each observance in VTIMEZONE to determine the correct observance and onset. (This is so that the correct observance can be found for clock times generated during recurrence rule calculations.)

If UPDATE is non-nil, the observance found will be used to update the offset value in DT (as a side effect) before returning the observance and onset.

If UPDATE is non-nil, NONEXISTENT specifies how to handle clock times that do not exist in the observance (see icalendar-recur-tz-nonexistent-date-time-p). The keyword :error means to signal an 'icalendar-tz-nonexistent-time error, without modifying any of the fields in DT. Otherwise, the default is to interpret DT using the offset from UTC before the onset of the found observance, and then reset the clock time in DT to the corresponding existing time after the onset of the observance. For example, the nonexistent time 2:30AM in Standard time on the day of the switch to Daylight time in the US Eastern time zone will be reset to 3:30AM Eastern Daylight time.

If DT is a Lisp timestamp, it represents an absolute time and comparisons with the onsets in VTIMEZONE are performed with absolute times. UPDATE and NONEXISTENT have no meaning in this case and are ignored.

Source Code

;; Defined in /usr/src/emacs/lisp/calendar/icalendar-recur.el.gz
;; DRAGONS DRAGONS DRAGONS
(defun icr:tz-observance-on (dt vtimezone &optional update nonexistent)
  "Return the time zone observance in effect on DT in VTIMEZONE.

If there is such an observance, the returned value is a list (OBSERVANCE
ONSET).  OBSERVANCE is an `icalendar-standard' or `icalendar-daylight'
component node.  ONSET is the recurrence of OBSERVANCE (an
`icalendar-date-time') which occurs closest in time, but before, DT.

If there is no such observance in VTIMEZONE, the returned value is nil.

VTIMEZONE should be an `icalendar-vtimezone' component node.

DT may be an an `icalendar-date-time' or a Lisp timestamp.  If it is a
date-time, it represents a local time assumed to be in VTIMEZONE.  Any
existing offset in DT is ignored, and DT is compared with the local
clock time at the start of each observance in VTIMEZONE to determine the
correct observance and onset.  (This is so that the correct observance
can be found for clock times generated during recurrence rule
calculations.)

If UPDATE is non-nil, the observance found will be used to update the
offset value in DT (as a side effect) before returning the observance
and onset.

If UPDATE is non-nil, NONEXISTENT specifies how to handle clock times
that do not exist in the observance (see
`icalendar-recur-tz-nonexistent-date-time-p').  The keyword `:error'
means to signal an \\='icalendar-tz-nonexistent-time error, without
modifying any of the fields in DT.  Otherwise, the default is to
interpret DT using the offset from UTC before the onset of the found
observance, and then reset the clock time in DT to the corresponding
existing time after the onset of the observance.  For example, the
nonexistent time 2:30AM in Standard time on the day of the switch to
Daylight time in the US Eastern time zone will be reset to 3:30AM
Eastern Daylight time.

If DT is a Lisp timestamp, it represents an absolute time and
comparisons with the onsets in VTIMEZONE are performed with absolute
times.  UPDATE and NONEXISTENT have no meaning in this case and are
ignored."
  (ical:with-component vtimezone
    ((ical:standard :all stds)
     (ical:daylight :all dls))
    (let (given-abs-time     ;; = `dt', if given a Lisp timestamp
          given-clock-time   ;; = `dt', if given a decoded time
          nearest-observance ;; the observance we're looking for
          nearest-onset      ;; latest onset of this observance before `dt'
          updated)           ;; stores how `dt's fields should be updated
                             ;; in line with this observance, if requested

      (if (cl-typep dt 'ical:date-time)
          ;; We were passed a date-time with local clock time, not an
          ;; absolute time; in this case, we must make local clock time
          ;; comparisons with the observance onset start and recurrences
          ;; (in order to determine the correct offset for it within the
          ;; zone)
          (setq given-clock-time dt
                given-abs-time nil)
        ;; We were passed an absolute time, not a date-time; in this
        ;; case, we can make comparisons in absolute time with
        ;; observance onset start and recurrences (in order to determine
        ;; the correct offset for decoding it)
        (setq given-abs-time dt
              given-clock-time nil))

      (dolist (obs (append stds dls))
        (ical:with-component obs
          ((ical:dtstart :value start)
           (ical:rrule :value rrule)
           (ical:rdate :all rdate-nodes)
           (ical:tzoffsetfrom :value offset-from))
          ;; DTSTART of the observance must be given as local time, and is
          ;; combined with TZOFFSETFROM to define the effective onset
          ;; for the observance in absolute time.
          (let* ((is-daylight (ical:daylight-component-p obs))
                 (effective-start
                  (ical:date-time-variant start :zone offset-from
                                          :dst (not is-daylight)))
                 (until (ical:rrule-until rrule))
                 (bound
                  ;; Optimization: compute a rough upper bound for when
                  ;; an observance might apply, thus allowing us to skip
                  ;; computing recurrences for irrelevant observances.
                  ;; The UNTIL date, if any, is the last *recurrence* of
                  ;; the observance.  The observance is therefore in
                  ;; effect for some time after this recurrence, so we
                  ;; can't just use UNTIL as an upper bound, but it's
                  ;; guaranteed to end within N years after UNTIL, where
                  ;; N is the interval size.  This is not the tightest
                  ;; possible bound but it is the cheapest to compute here.
                  (when until
                    (ical:date-time-variant until
                                            :year (+ (decoded-time-year until)
                                                     (ical:rrule-interval-size
                                                      rrule)))))
                 (observance-might-apply
                  (if given-clock-time
                      (icr:-w/in-locally-p given-clock-time effective-start bound)
                    (icr:-w/in-abs-p given-abs-time effective-start bound))))

            (when observance-might-apply
              ;; Initialize our return values on the first iteration
              ;; where an observance potentially applies:
              (unless nearest-onset
                (setq nearest-onset effective-start
                      nearest-observance obs)
                (when (and update given-clock-time)
                  (setq updated
                        (icr:tz--get-updated-in given-clock-time
                                                effective-start obs))))

              ;; We first check whether any RDATEs in the observance are
              ;; the relevant onset:
              (let ((rdates
                     (mapcar #'ical:ast-node-value
                             (apply #'append
                                    (mapcar #'ical:ast-node-value rdate-nodes)))))
                (dolist (rd rdates)
                  (let* ((effective-rd
                          ;; N.B.: we don't have to worry about rd being
                          ;; an ical:period or ical:date here because in
                          ;; time zone observances, RDATE values are
                          ;; *only* allowed to be local date-times; see
                          ;; https://www.rfc-editor.org/rfc/rfc5545#section-3.6.5
                          ;; and `ical:rrule-validator'
                          (ical:date-time-variant rd :zone offset-from
                                                  :dst (not is-daylight)))
                         (onset-applies
                          (if given-clock-time
                              (ical:date-time-locally<= effective-rd
                                                        given-clock-time)
                            (ical:time<= (encode-time effective-rd)
                                         given-abs-time))))

                    (when (and onset-applies nearest-onset
                               (ical:date-time< nearest-onset effective-rd))
                      (setq nearest-onset effective-rd
                            nearest-observance obs)

                      (when (and update given-clock-time)
                        (setq updated
                              (icr:tz--get-updated-in given-clock-time
                                                      effective-rd obs)))))))

              ;; If the observance has a recurrence value, it's the
              ;; relevant observance if it:
              ;; (1) has a recurrence which starts before dt
              ;; (2) that recurrence is the nearest in the zone
              ;;     which starts before dt
              ;; Note that we intentionally do *not* pass `vtimezone'
              ;; through here to find-interval, recurrences-in-interval,
              ;; etc. so as not to cause infinite recursion.  Instead we
              ;; directly pass `offset-from' (the offset from UTC at the
              ;; start of each observance onset), which
              ;; `icr:tz-set-zone' knows to handle specially without
              ;; calling this function.
              (when rrule
                (let* ((target (or given-clock-time
                                   (decode-time given-abs-time offset-from)))
                       (int (icr:find-interval
                             target effective-start rrule offset-from))
                       (<=given
                        (if given-clock-time
                            (lambda (rec)
                              (ical:date-time-locally<= rec given-clock-time))
                          (lambda (rec)
                            (ical:time<= (encode-time rec) given-abs-time))))
                       (int-recs (sort
                                  (seq-filter <=given ; (1)
                                              (icr:recurrences-in-interval
                                               int obs offset-from))
                                    :lessp #'ical:date-time<
                                    :in-place t :reverse t))
                       latest-rec)

                       (unless int-recs
                         ;; The closest observance onset before `dt' might
                         ;; actually be in the previous interval, e.g.
                         ;; if `dt' is in January after an annual change to
                         ;; Standard Time in November.  So check that as well.
                         (setq int (icr:previous-interval int rrule
                                                          effective-start
                                                          offset-from))
                         (setq int-recs
                               (when int
                                 (sort
                                  (seq-filter <=given ; (1)
                                              (icr:recurrences-in-interval
                                               int obs offset-from))
                                  :lessp #'ical:date-time<
                                  :in-place t :reverse t))))
                       (setq latest-rec (car int-recs))

                  (when (and latest-rec
                             (ical:date-time< nearest-onset latest-rec)) ; (2)
                    (setf (decoded-time-dst latest-rec)
                          ;; if obs is a DAYLIGHT observance, latest-rec
                          ;; represents the last moment of standard time, and
                          ;; vice versa
                          (not is-daylight))
                    (setq nearest-onset latest-rec
                          nearest-observance obs)
                    (when (and update given-clock-time)
                      (setq updated
                            (icr:tz--get-updated-in given-clock-time
                                                    latest-rec obs))))))))))

      ;; We've now found the nearest observance, if there was one.
      ;; Update `dt' as a side effect if requested.  This saves
      ;; repeating a lot of the above in a separate function.
      (when (and update given-clock-time nearest-observance updated)
        ;; signal an error when `dt' does not exist if requested, so the
        ;; nonexistence can be handled further up the stack:
        (when (and (eq :error nonexistent)
                   (not (ical:date-time-locally-simultaneous-p dt updated)))
          (signal 'ical:tz-nonexistent-time
                  (list
                   :message
                   (format "%d-%02d-%02d %02d:%02d:%02d does not exist in %s"
                           (decoded-time-year dt)
                           (decoded-time-month dt)
                           (decoded-time-day dt)
                           (decoded-time-hour dt)
                           (decoded-time-minute dt)
                           (decoded-time-second dt)
                           (or
                            (ical:with-property-of nearest-observance
                                                   'ical:tzname nil value)
                            "time zone observance"))
                   :date-time dt
                   :observance nearest-observance)))
        ;; otherwise we copy `updated' over to `dt', which resets the
        ;; clock time in `dt' if it did not exist:
        (setf (decoded-time-zone dt) (decoded-time-zone updated))
        (setf (decoded-time-dst dt) (decoded-time-dst updated))
        (setf (decoded-time-second dt) (decoded-time-second updated))
        (setf (decoded-time-minute dt) (decoded-time-minute updated))
        (setf (decoded-time-hour dt) (decoded-time-hour updated))
        (setf (decoded-time-day dt) (decoded-time-day updated))
        (setf (decoded-time-month dt) (decoded-time-month updated))
        (setf (decoded-time-year dt) (decoded-time-year updated))
        (setf (decoded-time-weekday dt)
              (calendar-day-of-week (ical:date-time-to-date updated))))

      ;; Return the observance and onset if found, nil if not:
      (when nearest-observance
        (list nearest-observance nearest-onset)))))