From ac0ad56053c26329887a0eb9075bffaaa43ebd27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=B8=D0=BB=D1=8F=D0=BD=20=D0=9F=D0=B0=D0=BB=D0=B0?= =?UTF-8?q?=D1=83=D0=B7=D0=BE=D0=B2?= Date: Tue, 29 Nov 2022 09:34:59 +0200 Subject: [PATCH] recur_expansion: EXDATE can be DATE and DTSTART can be DATE-TIME When EXDATE is in DATE format and DTSTART is DATE-TIME, then to determine whether an occurrence shall be excluded, the occurrence shall be converted to DATE and then compared to EXDATE. This applies also for the very first EXDATE, when it coincides with DTSTART. --- lib/ical/recur_expansion.js | 24 +++++++++++++++++++++--- samples/rdate_exdate.ics | 8 ++++++++ test/recur_expansion_test.js | 13 +++++++++++++ 3 files changed, 42 insertions(+), 3 deletions(-) create mode 100644 samples/rdate_exdate.ics diff --git a/lib/ical/recur_expansion.js b/lib/ical/recur_expansion.js index 2694b3a1..1da38e94 100644 --- a/lib/ical/recur_expansion.js +++ b/lib/ical/recur_expansion.js @@ -198,6 +198,21 @@ class RecurExpansion { } } + /** + * Compare two ICAL.Time objects. When the second parameter is a timeless date + * and the first parameter is date-with-time, strip the time and compare only + * the days. + * + * @private + * @param {ICAL.Time} a The one object to compare + * @param {ICAL.Time} b The other object to compare + */ + _compare_special(a, b) { + if (!a.isDate && b.isDate) + return new Time({ year: a.year, month: a.month, day: a.day }).compare(b); + return a.compare(b); + } + /** * Retrieve the next occurrence in the series. * @return {ICAL.Time} @@ -248,9 +263,10 @@ class RecurExpansion { // check the negative rules if (this.exDate) { - compare = this.exDate.compare(this.last); + //EXDATE can be in DATE format, but DTSTART is in DATE-TIME format + compare = this._compare_special(this.last, this.exDate); - if (compare < 0) { + if (compare > 0) { this._nextExDay(); } @@ -397,10 +413,12 @@ class RecurExpansion { if (component.hasProperty('exdate')) { this.exDates = this._extractDates(component, 'exdate'); // if we have a .last day we increment the index to beyond it. + // When DTSTART is in DATE-TIME format, EXDATE is in DATE format and EXDATE is + // the date of DTSTART, _compare_special finds this out and compareTime fails. this.exDateInc = binsearchInsert( this.exDates, this.last, - (a, b) => a.compare(b) + this._compare_special ); this.exDate = this.exDates[this.exDateInc]; diff --git a/samples/rdate_exdate.ics b/samples/rdate_exdate.ics new file mode 100644 index 00000000..4ff511f6 --- /dev/null +++ b/samples/rdate_exdate.ics @@ -0,0 +1,8 @@ +BEGIN:VCALENDAR +BEGIN:VEVENT +UID:123 +DTSTART:20240609T030000Z +RRULE:FREQ=DAILY;INTERVAL=1;COUNT=4 +EXDATE;VALUE=DATE:20240611 +END:VEVENT +END:VCALENDAR diff --git a/test/recur_expansion_test.js b/test/recur_expansion_test.js index eb5d37d1..3cdfc7b9 100644 --- a/test/recur_expansion_test.js +++ b/test/recur_expansion_test.js @@ -329,4 +329,17 @@ suite('recur_expansion', function() { }); + suite('EXDATE and DTSTART have different value type', function() { + createSubject('rdate_exdate.ics'); + test('Compare EXDATE;VALUE=DATE and DTSTART;VALUE=DATE-TIME', function() { + let dates = [], next; + while ((next = subject.next())) + dates.push(next.toJSDate()); + assert.deepEqual(dates, [ + new Date('2024-06-09T03:00:00.000Z'), + new Date('2024-06-10T03:00:00.000Z'), + new Date('2024-06-12T03:00:00.000Z') + ]); + }); + }); });