From 7fb7b51f1b36d49b576a359ee178d85e6d4b192a Mon Sep 17 00:00:00 2001 From: Geoff Lankow Date: Thu, 8 Sep 2022 15:44:51 +1200 Subject: [PATCH] Fix recurrence iteration where there is a negative BYMONTHDAY rule. (#530) --- lib/ical/recur_iterator.js | 16 ++++++----- test/recur_iterator_test.js | 55 +++++++++++++++++++++++++++---------- 2 files changed, 50 insertions(+), 21 deletions(-) diff --git a/lib/ical/recur_iterator.js b/lib/ical/recur_iterator.js index 3a4570b1..c9d99f5b 100644 --- a/lib/ical/recur_iterator.js +++ b/lib/ical/recur_iterator.js @@ -214,7 +214,7 @@ ICAL.RecurIterator = (function() { this.last.second = this.setup_defaults("BYSECOND", "SECONDLY", this.dtstart.second); this.last.minute = this.setup_defaults("BYMINUTE", "MINUTELY", this.dtstart.minute); this.last.hour = this.setup_defaults("BYHOUR", "HOURLY", this.dtstart.hour); - this.last.day = this.setup_defaults("BYMONTHDAY", "DAILY", this.dtstart.day); + var dayOffset = this.last.day = this.setup_defaults("BYMONTHDAY", "DAILY", this.dtstart.day); this.last.month = this.setup_defaults("BYMONTH", "MONTHLY", this.dtstart.month); if (this.rule.freq == "WEEKLY") { @@ -301,13 +301,15 @@ ICAL.RecurIterator = (function() { throw new Error("Malformed values in BYDAY part"); } - } else if (this.has_by_data("BYMONTHDAY")) { - if (this.last.day < 0) { - var daysInMonth = ICAL.Time.daysInMonth(this.last.month, this.last.year); - this.last.day = daysInMonth + this.last.day + 1; - } - } + } else if (this.has_by_data("BYMONTHDAY") && dayOffset < 0) { + // Attempting to access `this.last.day` will cause the date to be normalised and + // not return a negative value. We keep the value in a separate variable instead. + // Now change the day value so that normalisation won't change the month. + this.last.day = 1; + var daysInMonth = ICAL.Time.daysInMonth(this.last.month, this.last.year); + this.last.day = daysInMonth + dayOffset + 1; + } }, /** diff --git a/test/recur_iterator_test.js b/test/recur_iterator_test.js index 2eaf4737..99456fbf 100644 --- a/test/recur_iterator_test.js +++ b/test/recur_iterator_test.js @@ -516,12 +516,12 @@ suite('recur_iterator', function() { // monthly, the third instance of tu,we,th testRRULE('FREQ=MONTHLY;COUNT=3;BYDAY=TU,WE,TH;BYSETPOS=3', { - byCount: true, - dates: [ - '1997-09-04T09:00:00', - '1997-10-07T09:00:00', - '1997-11-06T09:00:00' - ] + byCount: true, + dates: [ + '1997-09-04T09:00:00', + '1997-10-07T09:00:00', + '1997-11-06T09:00:00' + ] }); //monthly, each month last day that is monday @@ -604,7 +604,8 @@ suite('recur_iterator', function() { '2015-01-19T08:00:00', '2015-01-21T08:00:00', '2015-01-23T08:00:00' - ]}); + ] + }); //Repeat Monthly, the fifth Saturday (BYDAY=5SA) testRRULE('FREQ=MONTHLY;BYDAY=5SA', { @@ -616,8 +617,8 @@ suite('recur_iterator', function() { '2016-01-30T08:00:00', '2016-04-30T08:00:00', '2016-07-30T08:00:00' - ] - }); + ] + }); // Repeat Monthly, the fifth Wednesday every two months (BYDAY=5WE) testRRULE('FREQ=MONTHLY;INTERVAL=2;BYDAY=5WE', { @@ -666,7 +667,7 @@ suite('recur_iterator', function() { ] }); - // monthly, bymonthday + // Last day of the month, monthly. testRRULE('FREQ=MONTHLY;BYMONTHDAY=-1', { dtStart: '2015-01-01T08:00:00', dates: [ @@ -676,6 +677,32 @@ suite('recur_iterator', function() { ] }); + // Last day of the month, every 3 months. + testRRULE('FREQ=MONTHLY;INTERVAL=3;BYMONTHDAY=-1', { + dtStart: '2022-06-01T08:00:00', + dates: [ + '2022-06-30T08:00:00', + '2022-09-30T08:00:00', + '2022-12-31T08:00:00', + '2023-03-31T08:00:00', + '2023-06-30T08:00:00', + '2023-09-30T08:00:00', + ] + }); + + // Second-to-last day of the month, every 3 months. + testRRULE('FREQ=MONTHLY;INTERVAL=3;BYMONTHDAY=-2', { + dtStart: '2022-06-01T08:00:00', + dates: [ + '2022-06-29T08:00:00', + '2022-09-29T08:00:00', + '2022-12-30T08:00:00', + '2023-03-30T08:00:00', + '2023-06-29T08:00:00', + '2023-09-29T08:00:00', + ] + }); + // monthly + by month testRRULE('FREQ=MONTHLY;BYMONTH=1,3,6,9,12', { dates: [ @@ -696,7 +723,7 @@ suite('recur_iterator', function() { '2015-03-23T08:00:00Z', '2015-03-27T08:00:00Z', ] - }) + }); testRRULE('FREQ=MONTHLY;BYDAY=MO,FR;BYMONTHDAY=1,3,5,7,9,11,13,15,17,19,21,23,25,27,29,31;COUNT=4', { dtStart: '2015-04-01T08:00:00Z', byCount: true, @@ -706,7 +733,7 @@ suite('recur_iterator', function() { '2015-04-17T08:00:00Z', '2015-04-27T08:00:00Z' ] - }) + }); testRRULE('FREQ=MONTHLY;BYDAY=MO,SA;BYMONTHDAY=1,3,5,7,9,11,13,15,17,19,21,23,25,27,29,31;COUNT=4', { dtStart: '2015-04-01T08:00:00Z', byCount: true, @@ -716,7 +743,7 @@ suite('recur_iterator', function() { '2015-04-25T08:00:00Z', '2015-04-27T08:00:00Z' ] - }) + }); testRRULE('FREQ=MONTHLY;BYDAY=SU,FR;BYMONTHDAY=1,3,5,7,9,11,13,15,17,19,21,23,25,27,29,31;COUNT=9', { dtStart: '2015-02-28T08:00:00Z', byCount: true, @@ -731,7 +758,7 @@ suite('recur_iterator', function() { "2015-04-17T08:00:00Z", "2015-04-19T08:00:00Z" ] - }) + }); }); suite('YEARLY', function() {