Subject: bug#54939: 29.0.50; icalendar cannot infer the DTEND from DTSTART + DURATION

From: John Hamelink <[email-masked]>
Date: Fri, 15 Apr 2022 10:39:20 +0100

John Hamelink <[email-masked]> writes:
> Great, then I'll have a crack at writing such a patch. I'm new to
> Emacs lisp, but this seems to me like a manageable first task!

OK, so I've implemented something that's approaching spec - but not
ready for full review yet: particularly because I have a problem I
need some guidance on. I have introduced the following tests:

`gnus-icalendar-dtstart-only-date' (Failing)
- `DTSTART' is `DTSTART;TZID=Europe/Berlin:20200915'
- `DTEND' is undefined
- `DURATION' is undefined
- Fails because the `gnus-icalendar--datetimep' returns a truthy
  value.

`gnus-icalendar-dtstart-only-datetime' (Passing)
- `DTSTART' is `DTSTART;TZID=Europe/Berlin:20200915T140000'
- `DTEND' is undefined
- `DURATION' is undefined

`gnus-icalendar-dtstart-duration' (Passing)
- `DTSTART' is `DTSTART;TZID=Europe/Berlin:20200915T140000'
- `DTEND' is undefined
- `DURATION' is `PT3H'

The reason `gnus-icalendar-dtstart-only-date' currently fails is
because I haven't found a good way to differentiate between a date and
a datetime - as the spec requires.

In an attempt to unblock the rest of the implementation, I used the
following:

(defun gnus-icalendar--datep (date)
"return t if DATE matches a date list."
(and (length= date 9)
    (length=
        (seq-filter 'integerp (seq-take date 6))
        3)))

(defun gnus-icalendar--datetimep (datetime)
"Return t if DATETIME matches a date-time list."
(and (length= datetime 9)
(length=
    (seq-filter 'integerp (seq-take datetime 6))
    6)))

This strategy doesn't work. In `icalendar--decode-isodatetime', hours,
minutes and seconds are set to 0 by default, effectively normalising
date to datetime. I wonder if there's a function already implemented
for this that I don't know about yet?

I've included all my patches so far, but I do plan on refactoring more
and then checking my contribution against the contributing guidelines
before formally submitting a patch for review.

I've also sent an email to [email-masked] pre-emptively, in case that
is necessary.

Thanks!
JH
From c8e460cc0657cb9ddf9e0fbc9abbca32c9344414 Mon Sep 17 00:00:00 2001
From: John Hamelink <[email-masked]>
Date: Fri, 15 Apr 2022 03:35:18 +0100
Subject: [PATCH 5/5] lisp/gnus/gnus-icalendar: Add RFC5545 DTEND calculation
 function

`gnus-icalendar-event--calculate-end-time' returns:
 - DTEND if it is set
 - or DTSTART + DURATION if DURATION is set
 - or DTSTART + 1 Day if DTSTART is a DATE
 - or DTSTART if DTSTART is a DATETIME
---
 lisp/gnus/gnus-icalendar.el            |  27 ++-
 test/lisp/gnus/gnus-icalendar-tests.el | 238 +++++++++++++++++++++++++
 2 files changed, 263 insertions(+), 2 deletions(-)

diff --git a/lisp/gnus/gnus-icalendar.el b/lisp/gnus/gnus-icalendar.el
index 036832e1d5..64b21794cb 100644
--- a/lisp/gnus/gnus-icalendar.el
+++ b/lisp/gnus/gnus-icalendar.el
@@ -237,6 +237,29 @@ gnus-icalendar--datetimep
     (seq-filter 'integerp (seq-take datetime 6))
     6)))
 
+(defun gnus-icalendar-event--calculate-end-time (event zone-map)
+  "Return DTEND, or calculate it as per RFC5545 3.6.1."
+  (let* ((dtstart (decode-time (gnus-icalendar-event--decode-datefield
+                                event 'DTSTART zone-map)))
+         (dtend (gnus-icalendar-event--decode-datefield
+                 event 'DTEND zone-map))
+         (duration (gnus-icalendar-event--decode-duration
+                    event 'DURATION)))
+    (cond
+     ;; Either Return DTEND
+     ((not (null dtend)) dtend)
+     ;; or DTSTART + DURATION if DURATION exists
+     ((not (null duration))
+      (encode-time
+       (icalendar--add-decoded-times dtstart duration)))
+     ;; or DTSTART + 1DAY if DTSTART is a DATE
+     ((gnus-icalendar--datep dtstart)
+      (encode-time (icalendar--add-decoded-times
+                    dtstart (icalendar--decode-isoduration "1D"))))
+     ;; or DSTART if DTSTART is a DATE-TIME
+     ((gnus-icalendar--datetimep dtstart)
+      (encode-time dtstart)))))
+
 (defun gnus-icalendar-event-from-ical (ical &optional attendee-name-or-email)
   (let* ((event (car (icalendar--all-events ical)))
          (organizer (replace-regexp-in-string
@@ -266,8 +289,8 @@ gnus-icalendar-event-from-ical
                 :organizer organizer
                 :start-time (gnus-icalendar-event--decode-datefield
                              event 'DTSTART zone-map)
-                :end-time (gnus-icalendar-event--decode-datefield
-                           event 'DTEND zone-map)
+                :end-time (gnus-icalendar-event--calculate-end-time
+                           event zone-map)
                 :rsvp (string= (plist-get (cadr attendee) 'RSVP) "TRUE")
                 :participation-type participation-type
                 :req-participants (car attendee-names)
diff --git a/test/lisp/gnus/gnus-icalendar-tests.el b/test/lisp/gnus/gnus-icalendar-tests.el
index 5fecfd3773..c26ddaf44c 100644
--- a/test/lisp/gnus/gnus-icalendar-tests.el
+++ b/test/lisp/gnus/gnus-icalendar-tests.el
@@ -281,5 +281,243 @@ gnus-icalendary-weekly-byday
 <2020-09-21 14:00-14:30 +1w>")))
       (setenv "TZ" tz))))
 
+(ert-deftest gnus-icalendar-dtstart-duration ()
+  ""
+
+  (let ((tz (getenv "TZ"))
+        (event (gnus-icalendar-tests--get-ical-event "\
+BEGIN:VCALENDAR
+PRODID:-//Google Inc//Google Calendar 70.9054//EN
+VERSION:2.0
+CALSCALE:GREGORIAN
+METHOD:REQUEST
+BEGIN:VTIMEZONE
+TZID:Europe/Berlin
+X-LIC-LOCATION:Europe/Berlin
+BEGIN:DAYLIGHT
+TZOFFSETFROM:+0100
+TZOFFSETTO:+0200
+TZNAME:CEST
+DTSTART:19700329T020000
+RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
+END:DAYLIGHT
+BEGIN:STANDARD
+TZOFFSETFROM:+0200
+TZOFFSETTO:+0100
+TZNAME:CET
+DTSTART:19701025T030000
+RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+DTSTART;TZID=Europe/Berlin:20200915T140000
+DURATION:PT3H
+RRULE:FREQ=WEEKLY;BYDAY=FR,MO,TH,TU,WE
+DTSTAMP:20200915T120627Z
+ORGANIZER;CN=[email-masked]:mailto:[email-masked]
+UID:[email-masked]
+ATTENDEE;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED;RSVP=TRUE
+ ;CN=[email-masked];X-NUM-GUESTS=0:mailto:[email-masked]
+CREATED:20200325T095723Z
+DESCRIPTION:Coffee talk
+LAST-MODIFIED:20200915T120623Z
+LOCATION:
+SEQUENCE:0
+STATUS:CONFIRMED
+SUMMARY:Casual coffee talk
+TRANSP:OPAQUE
+END:VEVENT
+END:VCALENDAR" (list "[email-masked]"))))
+
+    (unwind-protect
+        (progn
+          ;; Use this form so as not to rely on system tz database.
+          ;; Eg hydra.nixos.org.
+          (setenv "TZ" "CET-1CEST,M3.5.0/2,M10.5.0/3")
+          (should (eq (eieio-object-class event) 'gnus-icalendar-event-request))
+          (should (gnus-icalendar-event:recurring-p event))
+          (should (string= (gnus-icalendar-event:recurring-interval event) "1"))
+          (should (string= (gnus-icalendar-event:start event) "2020-09-15 14:00"))
+          (should (string= (gnus-icalendar-event:end event) "2020-09-15 17:00"))
+          (with-slots (organizer summary description location end-time uid rsvp participation-type) event
+            (should (string= organizer "[email-masked]"))
+            (should (string= summary "Casual coffee talk"))
+            (should (string= description "Coffee talk"))
+            (should (string= location ""))
+            (should (string= (format-time-string "%Y-%m-%d %H:%M" end-time) "2020-09-15 17:00"))
+            (should (string= uid "[email-masked]"))
+            (should rsvp)
+            (should (eq participation-type 'required)))
+          (should (equal (sort (gnus-icalendar-event:recurring-days event) #'<) '(1 2 3 4 5)))
+          (should (string= (gnus-icalendar-event:org-timestamp event) "<2020-09-15 14:00-17:00 +1w>
+<2020-09-16 14:00-17:00 +1w>
+<2020-09-17 14:00-17:00 +1w>
+<2020-09-18 14:00-17:00 +1w>
+<2020-09-21 14:00-17:00 +1w>"))
+
+          )
+      (setenv "TZ" tz))))
+
+
+(ert-deftest gnus-icalendar-dtstart-only-datetime ()
+  ""
+
+  (let ((tz (getenv "TZ"))
+        (event (gnus-icalendar-tests--get-ical-event "\
+BEGIN:VCALENDAR
+PRODID:-//Google Inc//Google Calendar 70.9054//EN
+VERSION:2.0
+CALSCALE:GREGORIAN
+METHOD:REQUEST
+BEGIN:VTIMEZONE
+TZID:Europe/Berlin
+X-LIC-LOCATION:Europe/Berlin
+BEGIN:DAYLIGHT
+TZOFFSETFROM:+0100
+TZOFFSETTO:+0200
+TZNAME:CEST
+DTSTART:19700329T020000
+RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
+END:DAYLIGHT
+BEGIN:STANDARD
+TZOFFSETFROM:+0200
+TZOFFSETTO:+0100
+TZNAME:CET
+DTSTART:19701025T030000
+RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+DTSTART;TZID=Europe/Berlin:20200915T140000
+RRULE:FREQ=WEEKLY;BYDAY=FR,MO,TH,TU,WE
+DTSTAMP:20200915T120627Z
+ORGANIZER;CN=[email-masked]:mailto:[email-masked]
+UID:[email-masked]
+ATTENDEE;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED;RSVP=TRUE
+ ;CN=[email-masked];X-NUM-GUESTS=0:mailto:[email-masked]
+CREATED:20200325T095723Z
+DESCRIPTION:Coffee talk
+LAST-MODIFIED:20200915T120623Z
+LOCATION:
+SEQUENCE:0
+STATUS:CONFIRMED
+SUMMARY:Casual coffee talk
+TRANSP:OPAQUE
+END:VEVENT
+END:VCALENDAR" (list "[email-masked]"))))
+
+    (unwind-protect
+        (progn
+          ;; Use this form so as not to rely on system tz database.
+          ;; Eg hydra.nixos.org.
+          (setenv "TZ" "CET-1CEST,M3.5.0/2,M10.5.0/3")
+          (should (eq (eieio-object-class event) 'gnus-icalendar-event-request))
+          (should (gnus-icalendar-event:recurring-p event))
+          (should (string= (gnus-icalendar-event:recurring-interval event) "1"))
+          (should (string= (gnus-icalendar-event:start event) "2020-09-15 14:00"))
+          ;; For cases where a "VEVENT" calendar component specifies a
+          ;; "DTSTART" property with a DATE-TIME value type but no
+          ;; "DTEND" property, the event ends on the same calendar
+          ;; date and time of day specified by the "DTSTART" property.
+          (should (string= (gnus-icalendar-event:end event) "2020-09-15 14:00"))
+          (with-slots (organizer summary description location end-time uid rsvp participation-type) event
+            (should (string= organizer "[email-masked]"))
+            (should (string= summary "Casual coffee talk"))
+            (should (string= description "Coffee talk"))
+            (should (string= location ""))
+            (should (string= (format-time-string "%Y-%m-%d %H:%M" end-time) "2020-09-15 14:00"))
+            (should (string= uid "[email-masked]"))
+            (should rsvp)
+            (should (eq participation-type 'required)))
+          (should (equal (sort (gnus-icalendar-event:recurring-days event) #'<) '(1 2 3 4 5)))
+          (should (string= (gnus-icalendar-event:org-timestamp event) "<2020-09-15 14:00-14:00 +1w>
+<2020-09-16 14:00-14:00 +1w>
+<2020-09-17 14:00-14:00 +1w>
+<2020-09-18 14:00-14:00 +1w>
+<2020-09-21 14:00-14:00 +1w>"))
+
+          )
+      (setenv "TZ" tz))))
+
+(ert-deftest gnus-icalendar-dtstart-only-date ()
+  ""
+
+  (let ((tz (getenv "TZ"))
+        (event (gnus-icalendar-tests--get-ical-event "\
+BEGIN:VCALENDAR
+PRODID:-//Google Inc//Google Calendar 70.9054//EN
+VERSION:2.0
+CALSCALE:GREGORIAN
+METHOD:REQUEST
+BEGIN:VTIMEZONE
+TZID:Europe/Berlin
+X-LIC-LOCATION:Europe/Berlin
+BEGIN:DAYLIGHT
+TZOFFSETFROM:+0100
+TZOFFSETTO:+0200
+TZNAME:CEST
+DTSTART:19700329T020000
+RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
+END:DAYLIGHT
+BEGIN:STANDARD
+TZOFFSETFROM:+0200
+TZOFFSETTO:+0100
+TZNAME:CET
+DTSTART:19701025T030000
+RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+DTSTART;TZID=Europe/Berlin:20200915
+RRULE:FREQ=WEEKLY;BYDAY=FR,MO,TH,TU,WE
+DTSTAMP:20200915T120627Z
+ORGANIZER;CN=[email-masked]:mailto:[email-masked]
+UID:[email-masked]
+ATTENDEE;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED;RSVP=TRUE
+ ;CN=[email-masked];X-NUM-GUESTS=0:mailto:[email-masked]
+CREATED:20200325T095723Z
+DESCRIPTION:Coffee talk
+LAST-MODIFIED:20200915T120623Z
+LOCATION:
+SEQUENCE:0
+STATUS:CONFIRMED
+SUMMARY:Casual coffee talk
+TRANSP:OPAQUE
+END:VEVENT
+END:VCALENDAR" (list "[email-masked]"))))
+
+    (unwind-protect
+        (progn
+          ;; Use this form so as not to rely on system tz database.
+          ;; Eg hydra.nixos.org.
+          (setenv "TZ" "CET-1CEST,M3.5.0/2,M10.5.0/3")
+          (should (eq (eieio-object-class event) 'gnus-icalendar-event-request))
+          (should (gnus-icalendar-event:recurring-p event))
+          (should (string= (gnus-icalendar-event:recurring-interval event) "1"))
+          (should (string= (gnus-icalendar-event:start event) "2020-09-15 00:00"))
+          ;; For cases where a "VEVENT" calendar component
+          ;; specifies a "DTSTART" property with a DATE value type but no
+          ;; "DTEND" nor "DURATION" property, the event's duration is taken to
+          ;; be one day.
+          (should (string= (gnus-icalendar-event:end event) "2020-09-16 00:00"))
+          (with-slots (organizer summary description location end-time uid rsvp participation-type) event
+            (should (string= organizer "[email-masked]"))
+            (should (string= summary "Casual coffee talk"))
+            (should (string= description "Coffee talk"))
+            (should (string= location ""))
+            (should (string= (format-time-string "%Y-%m-%d %H:%M" end-time) "2020-09-16 00:00"))
+            (should (string= uid "[email-masked]"))
+            (should rsvp)
+            (should (eq participation-type 'required)))
+          (should (equal (sort (gnus-icalendar-event:recurring-days event) #'<) '(1 2 3 4 5)))
+          (should (string= (gnus-icalendar-event:org-timestamp event) "<2020-09-15 00:00-00:00 +1w>
+<2020-09-16 00:00-00:00 +1w>
+<2020-09-17 00:00-00:00 +1w>
+<2020-09-18 00:00-00:00 +1w>
+<2020-09-21 00:00-00:00 +1w>"))
+
+          )
+      (setenv "TZ" tz))))
+
 (provide 'gnus-icalendar-tests)
 ;;; gnus-icalendar-tests.el ends here
-- 
2.35.2

From 7134ca9ecadf60efbe703ffd2203d25aada7fdfa Mon Sep 17 00:00:00 2001
From: John Hamelink <[email-masked]>
Date: Fri, 15 Apr 2022 02:51:11 +0100
Subject: [PATCH 4/5] lisp/gnus/gnus-icalendar: Add gnus-icalendar-event:end

---
 lisp/gnus/gnus-icalendar.el | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/lisp/gnus/gnus-icalendar.el b/lisp/gnus/gnus-icalendar.el
index 221d98c63a..036832e1d5 100644
--- a/lisp/gnus/gnus-icalendar.el
+++ b/lisp/gnus/gnus-icalendar.el
@@ -159,6 +159,9 @@ gnus-icalendar-event:recurring-days
 (cl-defmethod gnus-icalendar-event:start ((event gnus-icalendar-event))
   (format-time-string "%Y-%m-%d %H:%M" (gnus-icalendar-event:start-time event)))
 
+(cl-defmethod gnus-icalendar-event:end ((event gnus-icalendar-event))
+  (format-time-string "%Y-%m-%d %H:%M" (gnus-icalendar-event:end-time event)))
+
 (defun gnus-icalendar-event--decode-duration (event field)
   (let ((duration (icalendar--get-event-property event field)))
     (unless (null duration)
-- 
2.35.2

From 6245118d273eb7672849fa31e3eb4f3992a6237b Mon Sep 17 00:00:00 2001
From: John Hamelink <[email-masked]>
Date: Fri, 15 Apr 2022 02:49:55 +0100
Subject: [PATCH 3/5] lisp/gnus/gnus-icalendar: Add
 gnus-icalendar-event--decode-duration

---
 lisp/gnus/gnus-icalendar.el | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/lisp/gnus/gnus-icalendar.el b/lisp/gnus/gnus-icalendar.el
index 8eb3172e01..221d98c63a 100644
--- a/lisp/gnus/gnus-icalendar.el
+++ b/lisp/gnus/gnus-icalendar.el
@@ -159,6 +159,11 @@ gnus-icalendar-event:recurring-days
 (cl-defmethod gnus-icalendar-event:start ((event gnus-icalendar-event))
   (format-time-string "%Y-%m-%d %H:%M" (gnus-icalendar-event:start-time event)))
 
+(defun gnus-icalendar-event--decode-duration (event field)
+  (let ((duration (icalendar--get-event-property event field)))
+    (unless (null duration)
+      (icalendar--decode-isoduration duration))))
+
 (defun gnus-icalendar-event--decode-datefield (event field zone-map)
   (let* ((dtdate (icalendar--get-event-property event field))
          (dtdate-zone (icalendar--find-time-zone
-- 
2.35.2

From 6245118d273eb7672849fa31e3eb4f3992a6237b Mon Sep 17 00:00:00 2001
From: John Hamelink <[email-masked]>
Date: Fri, 15 Apr 2022 02:49:55 +0100
Subject: [PATCH 3/5] lisp/gnus/gnus-icalendar: Add
 gnus-icalendar-event--decode-duration

---
 lisp/gnus/gnus-icalendar.el | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/lisp/gnus/gnus-icalendar.el b/lisp/gnus/gnus-icalendar.el
index 8eb3172e01..221d98c63a 100644
--- a/lisp/gnus/gnus-icalendar.el
+++ b/lisp/gnus/gnus-icalendar.el
@@ -159,6 +159,11 @@ gnus-icalendar-event:recurring-days
 (cl-defmethod gnus-icalendar-event:start ((event gnus-icalendar-event))
   (format-time-string "%Y-%m-%d %H:%M" (gnus-icalendar-event:start-time event)))
 
+(defun gnus-icalendar-event--decode-duration (event field)
+  (let ((duration (icalendar--get-event-property event field)))
+    (unless (null duration)
+      (icalendar--decode-isoduration duration))))
+
 (defun gnus-icalendar-event--decode-datefield (event field zone-map)
   (let* ((dtdate (icalendar--get-event-property event field))
          (dtdate-zone (icalendar--find-time-zone
-- 
2.35.2

From d46a3e9982eb771255aad802d401c540995c79ac Mon Sep 17 00:00:00 2001
From: John Hamelink <[email-masked]>
Date: Fri, 15 Apr 2022 02:40:45 +0100
Subject: [PATCH 2/5] lisp/gnus/gnus-icalendar: Return nil if datefield couldnt
 be decoded

---
 lisp/gnus/gnus-icalendar.el | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/lisp/gnus/gnus-icalendar.el b/lisp/gnus/gnus-icalendar.el
index 7d4a98e034..8eb3172e01 100644
--- a/lisp/gnus/gnus-icalendar.el
+++ b/lisp/gnus/gnus-icalendar.el
@@ -165,7 +165,8 @@ gnus-icalendar-event--decode-datefield
                        (icalendar--get-event-property-attributes
                         event field) zone-map))
          (dtdate-dec (icalendar--decode-isodatetime dtdate nil dtdate-zone)))
-    (encode-time dtdate-dec)))
+    (unless (null dtdate-dec)
+      (encode-time dtdate-dec))))
 
 (defun gnus-icalendar-event--find-attendee (ical name-or-email)
   (let* ((event (car (icalendar--all-events ical)))
-- 
2.35.2

From 8de7d6f33199d136d05226548db776fc7b64f59e Mon Sep 17 00:00:00 2001
From: John Hamelink <[email-masked]>
Date: Fri, 15 Apr 2022 02:35:05 +0100
Subject: [PATCH 1/5] lisp/gnus/gnus-icalendar: Added date and datetime
 predicates

RFC5545 states that the end-time should be calculated differently
depending on whether DTSTART is a date or a date-time.
---
 lisp/gnus/gnus-icalendar.el            | 14 ++++++++++++++
 test/lisp/gnus/gnus-icalendar-tests.el | 26 ++++++++++++++++++++++++++
 2 files changed, 40 insertions(+)

diff --git a/lisp/gnus/gnus-icalendar.el b/lisp/gnus/gnus-icalendar.el
index 1bffdf3513..7d4a98e034 100644
--- a/lisp/gnus/gnus-icalendar.el
+++ b/lisp/gnus/gnus-icalendar.el
@@ -214,6 +214,20 @@ gnus-icalendar-event--get-attendee-names
        (attendee-names-by-type "REQ-PARTICIPANT")
        (attendee-names-by-type "OPT-PARTICIPANT")))))
 
+(defun gnus-icalendar--datep (date)
+  "return t if DATE matches a date list."
+  (and (length= date 9)
+       (length=
+        (seq-filter 'integerp (seq-take date 6))
+        3)))
+
+(defun gnus-icalendar--datetimep (datetime)
+  "Return t if DATETIME matches a date-time list."
+  (and (length= datetime 9)
+   (length=
+    (seq-filter 'integerp (seq-take datetime 6))
+    6)))
+
 (defun gnus-icalendar-event-from-ical (ical &optional attendee-name-or-email)
   (let* ((event (car (icalendar--all-events ical)))
          (organizer (replace-regexp-in-string
diff --git a/test/lisp/gnus/gnus-icalendar-tests.el b/test/lisp/gnus/gnus-icalendar-tests.el
index 348ddf9f05..5fecfd3773 100644
--- a/test/lisp/gnus/gnus-icalendar-tests.el
+++ b/test/lisp/gnus/gnus-icalendar-tests.el
@@ -38,6 +38,32 @@ gnus-icalendar-tests--get-ical-event
       (setq event (gnus-icalendar-event-from-buffer (buffer-name) participant)))
     event))
 
+(ert-deftest gnus-icalendar-datetimep ()
+  "Can differentiate between dates and datetimes."
+  (should
+   (equal
+    (gnus-icalendar--datetimep
+     (parse-time-string "2020-09-15 14:00"))
+    t))
+  (should
+   (equal
+    (gnus-icalendar--datetimep
+     (iso8601-parse-date "2020-09-15"))
+    nil)))
+
+(ert-deftest gnus-icalendar-datep ()
+  "Can differentiate between dates and datetimes."
+  (should
+   (equal
+    (gnus-icalendar--datep
+     (iso8601-parse-date "2020-09-15"))
+    t))
+  (should
+   (equal
+    (gnus-icalendar--datep
+     (parse-time-string "2020-09-15 14:00"))
+    nil)))
+
 (ert-deftest gnus-icalendar-parse ()
   "test"
   (let ((tz (getenv "TZ"))
-- 
2.35.2