Re: Diary icalendar tests still broken

Richard Lawrence <[email protected]> Fri, 31 Jul 2026 08:19:17 +0200
Newsgroups gmane.emacs.devel
Message-ID <[email protected]>
Sean Whitton <[email protected]> writes:

> Richard Lawrence [30/Jul 12:12pm +02] wrote:
>> OK, here as promised is a patch for this issue.  I've tested it in all
>> of the above time zones and it works, here at least.  Please test on
>> your systems!
>
> Confirmed, thanks.

Great.

> Do you think the existing tests suite is thorough enough to catch any
> other issues this patch may cause?

"Why yes", I thought. "I *do* think it's thorough enough.  I'll just
double-check before I reply..." At which point I of course discovered a
lingering issue (namely, `decode-time' returns 0, not t, in the zone
slot when decoding to UTC, which needed fixing in a handful of places).
Revised patch attached.

If there is one thing that writing this library has taught me again and
again, it is to be humble about all code dealing with time.  The test
suite is large, because I rely on it.  I think I would have gone insane
without it.  (It's not 100% coverage, and it would be nice at some point
to automate running the tests across different time zones, but that's a
bigger change that requires some more thought and shouldn't hold up the
release, IMHO.)

The patch looks big, because it tweaks the representation of one of the
most widely-used data structures, but in fact it is a small change and I
am pleased with how easy it was to make.  I can't promise there are no
new bugs, but I *am* confident that this patch is strictly an
improvement over the code without it, and any new issues will be minor.

-- 
Best,
Richard
0001-Distinguish-UTC-vs.-UTC-0-00-times-in-iCalendar-libr.patch (text/x-diff, 22.1 KB)
From 812d91be7118c1deb258466da7c6b5ace8053e93 Mon Sep 17 00:00:00 2001
From: Richard Lawrence <[email protected]>
Date: Wed, 29 Jul 2026 09:29:25 +0200
Subject: [PATCH] Distinguish UTC vs. UTC+0:00 times in iCalendar library

This fixes a bug discussed on emacs-devel; see the thread at
https://https://lists.gnu.org/archive/html/emacs-devel/2026-07/msg00445.html

The issue was that the library did not distinguish between UTC time and
times that have an offset of 0 but are not in the UTC time zone,
e.g. standard times in Europe/London or Europe/Dublin.  This led to test
failures on systems in these time zones.  This fix represents true UTC
times by inserting the special value `t' in the zone slot of a decoded
time at parse time (which `encode-time' already handles correctly).
This distinguishes them from decoded times with an offset of 0 seconds.

* lisp/calendar/icalendar-ast.el (icalendar-make-component): Ensure
auto-generated DTSTAMP uses the new representation.
(icalendar-make-node-from-templates): Fix docstring.
* lisp/calendar/icalendar-parser.el (icalendar-read-time)
(icalendar-read-date-time): Read iCalendar UTC times to the new
representation.
(icalendar-print-time): Print the new representation.
(icalendar--decoded-time-p, icalendar--decoded-date-time-p)
(icalendar-date-time-is-utc-p): Check for the new representation.
(icalendar-date-time): Docstring improvement.
(icalendar-requires-utc-validator): New validator function to check that
property values are in UTC.
(icalendar-completed, icalendar-created, icalendar-dtstamp): Use it.
* lisp/calendar/icalendar-recur.el (icalendar-recur-tz-decode-time):
Decode to new representation; explicitly support this.
* lisp/calendar/icalendar-utils.el (icalendar-date-time<)
(icalendar-date-time-simultaneous-p): Work with the new representation.
* lisp/calendar/diary-icalendar.el
(diary-icalendar-format-time-as-local): Work with the new
representation.
(diary-icalendar-convert-time-via-strategy, diary-icalendar-parse-entry):
Ensure times decode to new representation.
* test/lisp/calendar/icalendar-parser-tests.el
(icalendar-parser-test-bad-hyphenated-dates):
* test/lisp/calendar/icalendar-recur-tests.el
(icalendar-test-recur-find-secondly-interval)
(icalendar-test-recur-tz-observance-on): Update tests to use new
representation.
* test/lisp/calendar/icalendar-parser-tests.el
(icalendar-parse-test-utc+0-is-not-utc): New regression test.
* test/lisp/calendar/diary-icalendar-tests.el: Update 'to-utc test
*
test/lisp/calendar/diary-icalendar-resources/import-bug-24199.diary-all:
Update diary representation of UTC times in a recurrence rule.
---
 lisp/calendar/diary-icalendar.el              | 11 ++++--
 lisp/calendar/icalendar-ast.el                | 10 +++--
 lisp/calendar/icalendar-parser.el             | 28 +++++++++-----
 lisp/calendar/icalendar-recur.el              | 10 +++--
 lisp/calendar/icalendar-utils.el              |  8 ++--
 .../import-bug-24199.diary-all                |  6 +--
 test/lisp/calendar/diary-icalendar-tests.el   |  2 +-
 test/lisp/calendar/icalendar-parser-tests.el  | 37 ++++++++++++++++++-
 test/lisp/calendar/icalendar-recur-tests.el   | 28 +++++++-------
 9 files changed, 97 insertions(+), 43 deletions(-)

diff --git a/lisp/calendar/diary-icalendar.el b/lisp/calendar/diary-icalendar.el
index 9604dce8e4e..99d85157a4a 100644
--- a/lisp/calendar/diary-icalendar.el
+++ b/lisp/calendar/diary-icalendar.el
@@ -1365,7 +1365,7 @@ di:format-time-as-local
             (local-dt (decode-time ts local-tz))
             (local-str (di:format-time local-dt)))
        (if (and original-tzname original-offset
-                (not (= original-offset local-offset)))
+                (not (eql original-offset local-offset)))
            (format "%s (%s)" local-str (di:format-time dt original-tzname))
          local-str)))))
 
@@ -2507,8 +2507,8 @@ di:convert-time-via-strategy
             (icr:tz-decode-time (encode-time dt) vtimezone)
           (icr:tz-set-zone dt vtimezone :error)))
        ((or (eq 'to-utc di:time-zone-export-strategy)
-            (di:-tz-is-utc-p)) ; we're already in UTC, so mark dt as such
-        (decode-time (encode-time dt) t))
+            (di:-tz-is-utc-p))
+        (icr:tz-decode-time (encode-time dt) t)) ; ensure dt is in UTC
        ((eq 'floating di:time-zone-export-strategy)
         (setf (decoded-time-zone dt) nil)
         dt)))))
@@ -3340,7 +3340,10 @@ di:parse-entry
           ;; Collect the remaining properties:
           (setq all-props (append (di:parse-summary-and-description) all-props))
           (setq all-props (append (di:parse-attendees-and-organizer) all-props))
-          (push (ical:make-property ical:dtstamp (decode-time nil t)) all-props)
+          (push
+           (ical:make-property ical:dtstamp
+               (icr:tz-decode-time (current-time) t)) ; ensure UTC
+           all-props)
           (let ((class (di:parse-class))
                 (location (di:parse-location))
                 (status (di:parse-status))
diff --git a/lisp/calendar/icalendar-ast.el b/lisp/calendar/icalendar-ast.el
index c4faf48173f..74193ee391e 100644
--- a/lisp/calendar/icalendar-ast.el
+++ b/lisp/calendar/icalendar-ast.el
@@ -568,8 +568,13 @@ ical:make-component
   ;; Add templates for required properties automatically if we can:
   (when (memq type '(ical:vevent ical:vtodo ical:vjournal ical:vfreebusy))
     (unless (assq 'ical:dtstamp templates)
-      (push '(ical:dtstamp (decode-time nil t))
-            templates))
+      (push
+       '(icalendar-make-property icalendar-dtstamp
+            (let ((stamp (decode-time nil t)))
+              ;; Ensure we return UTC, not just :zone 0:
+              (setf (decoded-time-zone stamp) t)
+              stamp))
+       templates))
     (unless (assq 'ical:uid templates)
       (push `(ical:uid ,(ical:make-uid templates))
             templates)))
@@ -651,7 +656,6 @@ ical:make-node-from-templates
 For example, an iCalendar VEVENT could be written like this:
 
   (icalendar-make-node-from-templates icalendar-vevent
-    (icalendar-dtstamp (decode-time (current-time) 0))
     (icalendar-uid \"some-unique-id\")
     (icalendar-summary \"Party\")
     (icalendar-location \"Robot House\")
diff --git a/lisp/calendar/icalendar-parser.el b/lisp/calendar/icalendar-parser.el
index c6100c828e4..0616897d388 100644
--- a/lisp/calendar/icalendar-parser.el
+++ b/lisp/calendar/icalendar-parser.el
@@ -895,7 +895,7 @@ ical:read-time
         (second (string-to-number (substring s 4 6)))
         (utcoffset (if (and (length= s 7)
                             (equal "Z" (substring s 6 7)))
-                       0
+                       t ; UTC
                      ;; unknown/'floating' time zone:
                      nil)))
     (ical:make-date-time :second second
@@ -909,7 +909,7 @@ ical:print-time
           (decoded-time-hour time)
           (decoded-time-minute time)
           (decoded-time-second time)
-          (if (eql 0 (decoded-time-zone time))
+          (if (eq t (decoded-time-zone time))
               "Z" "")))
 
 (defun ical:-decoded-time-p (val)
@@ -922,7 +922,7 @@ ical:-decoded-time-p
        (cl-typep (decoded-time-minute val) 'ical:numeric-minute)
        (cl-typep (decoded-time-hour val) 'ical:numeric-hour)
        (cl-typep (decoded-time-dst val) '(member t nil -1))
-       (cl-typep (decoded-time-zone val) '(or integer null))))
+       (cl-typep (decoded-time-zone val) '(or integer boolean))))
 
 (ical:define-type ical:time "TIME"
   "Type for Time values.
@@ -966,7 +966,7 @@ ical:-decoded-date-time-p
        ;; `make-decoded-time':
        ;; (cl-typep (decoded-time-weekday val) '(integer 0 6))
        (cl-typep (decoded-time-dst val) '(member t nil -1))
-       (cl-typep (decoded-time-zone val) '(or integer null))))
+       (cl-typep (decoded-time-zone val) '(or integer boolean))))
 
 (defun ical:read-date-time (s)
   "Read an `icalendar-date-time' from a string S.
@@ -981,7 +981,7 @@ ical:read-date-time
         (second (string-to-number (substring s 13 15)))
         (utcoffset (if (and (length= s 16)
                             (equal "Z" (substring s 15 16)))
-                       0
+                       t ; UTC
                      ;; unknown/'floating' time zone:
                      nil)))
     (ical:make-date-time :second second
@@ -1006,16 +1006,15 @@ ical:print-date-time
 
 (defun ical:date-time-is-utc-p (datetime)
   "Return non-nil if DATETIME is in UTC time."
-  (let ((offset (decoded-time-zone datetime)))
-    (and offset (= 0 offset))))
+  (eq t (decoded-time-zone datetime)))
 
 (ical:define-type ical:date-time "DATE-TIME"
    "Type for Date-Time values.
 
 When printed, a date-time is a string of digits like:
   YYYYMMDDTHHMMSS
-where the 'T' is literal, and separates the date string from the
-time string.
+where the 'T' is literal, and separates the date string from the time
+string.  If followed by a 'Z', the string represents a UTC date-time.
 
 When read, a date-time is a decoded time, i.e. a list in the format
 (SEC MINUTE HOUR DAY MONTH YEAR DOW DST UTCOFF).  See
@@ -2932,6 +2931,7 @@ ical:completed
 `icalendar-date-time' with a UTC time."
   ical:date-time
   :child-spec (:zero-or-more (ical:otherparam))
+  :other-validator ical:requires-utc-validator
   :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.2.1")
 
 (ical:define-property ical:dtend "DTEND"
@@ -3380,6 +3380,7 @@ ical:created
 in UTC time."
   ical:date-time
   :child-spec (:zero-or-more (ical:otherparam))
+  :other-validator ical:requires-utc-validator
   :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.7.1")
 
 (ical:define-property ical:dtstamp "DTSTAMP"
@@ -3403,8 +3404,17 @@ ical:dtstamp
 The value must be in UTC time."
   ical:date-time
   :child-spec (:zero-or-more (ical:otherparam))
+  :other-validator ical:requires-utc-validator
   :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.7.2")
 
+(defun ical:requires-utc-validator (node)
+  "Validate that a property NODE's value is a UTC date-time"
+  (ical:with-property node nil
+    (unless (ical:date-time-is-utc-p value)
+      (ical:signal-validation-error
+       (format "An `%s's value must be in UTC" (ical:ast-node-type node))
+       :node node))))
+
 (ical:define-property ical:last-modified "LAST-MODIFIED"
   "Last Modified timestamp.
 
diff --git a/lisp/calendar/icalendar-recur.el b/lisp/calendar/icalendar-recur.el
index fbbfb209ca7..49f7f248918 100644
--- a/lisp/calendar/icalendar-recur.el
+++ b/lisp/calendar/icalendar-recur.el
@@ -1970,19 +1970,21 @@ icr:tz-decode-time
 observance that applies to TS, it is decoded into UTC time.
 
 VTIMEZONE may also be an `icalendar-utc-offset'.  In this case TS is
-decoded directly into this UTC offset, and its dst slot is set to -1."
+decoded directly into this UTC offset, and its dst slot is set to -1.
+If VTIMEZONE is t, TS is decoded to UTC time."
   (let* ((observance (when (ical:vtimezone-component-p vtimezone)
                        (car (icr:tz-observance-on ts vtimezone))))
          (offset (cond (observance (icr:tz-offset-in observance))
                        ((cl-typep vtimezone 'ical:utc-offset)
                         vtimezone)
-                       (t 0))))
+                       (t t)))) ; decode to UTC
 
     (ical:date-time-variant ; ensures weekday gets set, too
      (decode-time ts offset)
      :zone offset
-     :dst (if observance (ical:daylight-component-p observance)
-            -1))))
+     :dst (cond (observance (ical:daylight-component-p observance))
+                ((eq t vtimezone) nil) ; UTC
+                (t -1)))))
 
 (defun icr:tz-set-zone (dt vtimezone &optional nonexistent)
   "Set the time zone offset and dst flag in DT based on VTIMEZONE.
diff --git a/lisp/calendar/icalendar-utils.el b/lisp/calendar/icalendar-utils.el
index 565901940f0..ab5af4f5532 100644
--- a/lisp/calendar/icalendar-utils.el
+++ b/lisp/calendar/icalendar-utils.el
@@ -288,7 +288,7 @@ ical:date-time<
 signaled."
   (let ((zone1 (decoded-time-zone dt1))
         (zone2 (decoded-time-zone dt2)))
-    (cond ((and (integerp zone1) (integerp zone2))
+    (cond ((and zone1 zone2)
            (time-less-p (encode-time dt1) (encode-time dt2)))
           ((and (null zone1) (null zone2))
            (ical:date-time-locally< dt1 dt2))
@@ -356,11 +356,11 @@ ical:date-time-simultaneous-p
 signaled."
   (let ((zone1 (decoded-time-zone dt1))
         (zone2 (decoded-time-zone dt2)))
-    (cond ((and (integerp zone1) (integerp zone2))
+    (cond ((and zone1 zone2)
            (time-equal-p (encode-time dt1) (encode-time dt2)))
           ((and (null zone1) (null zone2))
-           (time-equal-p (encode-time (ical:date-time-variant dt1 :zone 0))
-                         (encode-time (ical:date-time-variant dt2 :zone 0))))
+           (time-equal-p (encode-time (ical:date-time-variant dt1 :zone t))
+                         (encode-time (ical:date-time-variant dt2 :zone t))))
           (t
            ;; Best effort:
            ;; TODO: I'm not convinced this is the right thing to do yet.
diff --git a/test/lisp/calendar/diary-icalendar-resources/import-bug-24199.diary-all b/test/lisp/calendar/diary-icalendar-resources/import-bug-24199.diary-all
index cf3e5884710..9e60011535e 100644
--- a/test/lisp/calendar/diary-icalendar-resources/import-bug-24199.diary-all
+++ b/test/lisp/calendar/diary-icalendar-resources/import-bug-24199.diary-all
@@ -1,8 +1,8 @@
 &%%(diary-rrule :rule '((FREQ MONTHLY) (BYDAY ((3 . 1))) (INTERVAL 1))
 	     :exclude
-	     '((0 46 11 6 1 2016 3 -1 0) (0 46 11 3 2 2016 3 -1 0)
-	       (0 46 11 2 3 2016 3 -1 0) (0 46 10 4 5 2016 3 -1 0)
-	       (0 46 10 1 6 2016 3 -1 0))
+	     '((0 46 11 6 1 2016 3 -1 t) (0 46 11 3 2 2016 3 -1 t)
+	       (0 46 11 2 3 2016 3 -1 t) (0 46 10 4 5 2016 3 -1 t)
+	       (0 46 10 1 6 2016 3 -1 t))
 	     :start '(0 46 12 2 12 2015 3 -1 nil) :duration
 	     '(0 14 3 0 nil nil nil -1 nil)) Summary
  Location: Loc
diff --git a/test/lisp/calendar/diary-icalendar-tests.el b/test/lisp/calendar/diary-icalendar-tests.el
index 06272a39cf4..96c39114ef3 100644
--- a/test/lisp/calendar/diary-icalendar-tests.el
+++ b/test/lisp/calendar/diary-icalendar-tests.el
@@ -1153,7 +1153,7 @@ dit:entry-parser
  (unwind-protect
      (ical:with-component (car parsed)
        ((ical:dtstart :first start-node :value start))
-       (should (= 0 (decoded-time-zone start)))
+       (should (ical:date-time-is-utc-p start))
        (should (= (- 16 2) (decoded-time-hour start)))
        (should-not (ical:with-param-of start-node 'ical:tzidparam)))
    ;; restore time zone
diff --git a/test/lisp/calendar/icalendar-parser-tests.el b/test/lisp/calendar/icalendar-parser-tests.el
index 8215f977e26..4b56edb8780 100644
--- a/test/lisp/calendar/icalendar-parser-tests.el
+++ b/test/lisp/calendar/icalendar-parser-tests.el
@@ -1962,7 +1962,7 @@ ipt:bad-hyphenated-dates
               (expected-dtstamp
                (ical:make-date-time :year 2023 :month 7 :day 30
                                     :hour 19 :minute 47 :second 0
-                                    :zone 0)))
+                                    :zone t)))
           (should (not (ical:errors-p)))
           (should (ical:ast-node-valid-p vcal t))
           (ical:with-component vcal
@@ -2023,6 +2023,41 @@ ipt:bad-user-addresses
                   ((ical:sentbyparam :value sent-by))
                   (should (equal sent-by expected-sender))))))))))
 
+
+;; Tests for bugfixes:
+(ert-deftest ipt:utc+0-is-not-utc ()
+  "Are UTC times parsed distinctly from times in other zones with 0 offset?"
+  ;; An explicit UTC time should parse with `t' in its zone field; this
+  ;; distinguishes it from times that merely have a 0 second offset from
+  ;; UTC, but might have a defined non-UTC time zone, e.g. in Europe/London
+  ;; or Europe/Dublin.  Bug discussion:
+  ;; https://https://lists.gnu.org/archive/html/emacs-devel/2026-07/msg00445.html
+  (let* ((s-utc "20260101T111111Z")
+         (s-non "20260101T111111")
+         (parsed-utc (ical:ast-node-value
+                      (ical:parse-from-string 'ical:date-time s-utc)))
+         (expected-utc (ical:make-date-time :year 2026 :month 1 :day 1
+                                            :hour 11 :minute 11 :second 11
+                                            :zone t))
+         (parsed-non (ical:ast-node-value
+                      (ical:parse-from-string 'ical:date-time s-non)))
+         (parsed-non-zoned (ical:date-time-variant parsed-non :zone 0))
+         (expected-non (ical:make-date-time :year 2026 :month 1 :day 1
+                                            :hour 11 :minute 11 :second 11
+                                            :zone nil))
+         (expected-non-zoned (ical:date-time-variant expected-non :zone 0)))
+    ;; Test that the two times are parsed distinctly:
+    (should (equal parsed-utc expected-utc))
+    (should (equal parsed-non expected-non))
+    (should-not (equal (decoded-time-zone parsed-utc)
+                       (decoded-time-zone parsed-non)))
+    ;; Test that `icalendar-date-time-is-utc-p' distinguishes the two cases:
+    (should (ical:date-time-is-utc-p parsed-utc))
+    (should-not (ical:date-time-is-utc-p parsed-non))
+    (should-not (ical:date-time-is-utc-p parsed-non-zoned))
+    ;; but also that `icalendar-date-time-simultaneous-p' does not:
+    (should (ical:date-time-simultaneous-p parsed-utc expected-utc))
+    (should (ical:date-time-simultaneous-p parsed-utc parsed-non-zoned))))
 
 
 
diff --git a/test/lisp/calendar/icalendar-recur-tests.el b/test/lisp/calendar/icalendar-recur-tests.el
index 199d6c4aa25..20b6dc57376 100644
--- a/test/lisp/calendar/icalendar-recur-tests.el
+++ b/test/lisp/calendar/icalendar-recur-tests.el
@@ -271,7 +271,7 @@ ict:recur-find-secondly-interval
                                        ;; Use UTC for the tests with no
                                        ;; time zone, so that the results
                                        ;; don't depend on system's local time
-                                       :zone 0))
+                                       :zone t))
          (dtstart/tz (ical:date-time-variant dtstart :zone ict:est :dst nil)))
 
     ;; Year numbers are monotonically increasing in the following test cases,
@@ -279,24 +279,24 @@ ict:recur-find-secondly-interval
 
     ;; No timezone, just clock time, around a target that doesn't fall on
     ;; an interval boundary:
-    (let* ((target (ical:date-time-variant dtstart :year 2026 :second 5 :zone 0))
+    (let* ((target (ical:date-time-variant dtstart :year 2026 :second 5 :zone t))
            (expected-int
             (icr:make-interval
-             (ical:date-time-variant target :second 0 :tz 'preserve)
-             (ical:date-time-variant target :second 1 :tz 'preserve)
-             (ical:date-time-variant target :second 10 :tz 'preserve))))
+             (ical:date-time-variant target :second 0 :dst nil :tz 'preserve)
+             (ical:date-time-variant target :second 1 :dst nil :tz 'preserve)
+             (ical:date-time-variant target :second 10 :dst nil :tz 'preserve))))
       (should
        (equal expected-int
               (icr:find-secondly-interval target dtstart 10))))
 
     ;; No timezone, just clock time, around a target that does fall on
     ;; an interval boundary:
-    (let* ((target (ical:date-time-variant dtstart :year 2027 :second 10 :zone 0))
+    (let* ((target (ical:date-time-variant dtstart :year 2027 :second 10 :zone t))
            (expected-int
             (icr:make-interval
-             (ical:date-time-variant target :second 10 :tz 'preserve)
-             (ical:date-time-variant target :second 11 :tz 'preserve)
-             (ical:date-time-variant target :second 20 :tz 'preserve))))
+             (ical:date-time-variant target :second 10 :dst nil :tz 'preserve)
+             (ical:date-time-variant target :second 11 :dst nil :tz 'preserve)
+             (ical:date-time-variant target :second 20 :dst nil :tz 'preserve))))
       (should
        (equal expected-int
               (icr:find-secondly-interval target dtstart 10))))
@@ -1334,7 +1334,7 @@ ict:recur-tz-observance-on
   ;; A date matching the end of a STANDARD observance:
   (let* ((ut (ical:make-date-time :year 2006 :month 10 :day 29
                                   :hour 6 :minute 0 :second 0
-                                  :zone 0 :dst nil)) ; UNTIL is in UTC
+                                  :zone t :dst nil)) ; UNTIL is in UTC
          (dt (ical:make-date-time :year 2006 :month 10 :day 29
                                   :hour 2 :minute 0 :second 0
                                   :zone ict:edt :dst t))
@@ -1352,7 +1352,7 @@ ict:recur-tz-observance-on
   ;; A date matching the end of a DAYLIGHT observance:
   (let* ((ut (ical:make-date-time :year 2006 :month 4 :day 2
                                   :hour 7 :minute 0 :second 0
-                                  :zone 0 :dst nil)) ; UNTIL is in UTC
+                                  :zone t :dst nil)) ; UNTIL is in UTC
          (dt (ical:make-date-time :year 2006 :month 4 :day 2
                                   :hour 2 :minute 0 :second 0
                                   :zone ict:est :dst nil))
@@ -1374,7 +1374,7 @@ ict:recur-tz-observance-on
                                   :zone ict:est :dst nil))
          (end (ical:make-date-time :year 1986 :month 4 :day 27
                                    :hour 7 :minute 0 :second 0
-                                   :zone 0)) ; UNTIL is in UTC
+                                   :zone t)) ; UNTIL is in UTC
          (obs/onset (icr:tz-observance-on dt ict:tz-eastern))
          (obs (car obs/onset))
          (onset (cadr obs/onset))
@@ -2009,7 +2009,7 @@ ict:rrule-test
                                :hour 9 :minute 0 :second 0
                                :zone ict:edt :dst t)
  :high (ical:make-date-time :year 1997 :month 10 :day 8
-                            :hour 0 :minute 0 :second 0 :zone 0)
+                            :hour 0 :minute 0 :second 0 :zone t)
  :members
  (list
   ;; ==> (1997 9:00 AM EDT) September 2,4,9,11,16,18,23,25,30;
@@ -2034,7 +2034,7 @@ ict:rrule-test
                                :hour 9 :minute 0 :second 0
                                :zone ict:edt :dst t)
  :high (ical:make-date-time :year 1997 :month 10 :day 8
-                            :hour 0 :minute 0 :second 0 :zone 0)
+                            :hour 0 :minute 0 :second 0 :zone t)
  :members
  (list
   ;; ==> (1997 9:00 AM EDT) September 2,4,9,11,16,18,23,25,30;
-- 
2.39.5