Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Wrong first occurrence for Monthly rule with BYMONTHDAY in particular cases #418

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions lib/ical/recur.js
Original file line number Diff line number Diff line change
Expand Up @@ -361,13 +361,13 @@

if (min !== undefined && value < min) {
throw new Error(
type + ': invalid value "' + value + '" must be > ' + min
type + ': invalid value "' + value + '" must be >= ' + min
);
}

if (max !== undefined && value > max) {
throw new Error(
type + ': invalid value "' + value + '" must be < ' + min
type + ': invalid value "' + value + '" must be <= ' + max
);
}

Expand Down
32 changes: 26 additions & 6 deletions lib/ical/recur_iterator.js
Original file line number Diff line number Diff line change
Expand Up @@ -214,8 +214,25 @@ 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 day = this.setup_defaults("BYMONTHDAY", "DAILY", this.dtstart.day);
// In case of BYMONTHDAY, the day must be set on the correct month otherwise
// the automatic normalization might cause a wrong "last" date.
this.last.month = this.setup_defaults("BYMONTH", "MONTHLY", this.dtstart.month);
var daysInMonth = ICAL.Time.daysInMonth(this.last.month, this.last.year);
var monthCounter = 12;
while (Math.abs(day) > daysInMonth && monthCounter--) {
this.increment_month();
daysInMonth = ICAL.Time.daysInMonth(this.last.month, this.last.year);
}
if (monthCounter < 0) {
// A combination of numeric values in BYMONTHDAY and BYMONTH makes
// impossible to find a day that matches the rule.
throw new Error("Wrong combination of numeric values in BYMONTHDAY and BYMONTH");
}
if (day < 0) {
day += daysInMonth + 1;
}
this.last.day = day;

if (this.rule.freq == "WEEKLY") {
if ("BYDAY" in parts) {
Expand Down Expand Up @@ -248,7 +265,7 @@ ICAL.RecurIterator = (function() {
if (this.rule.freq == "MONTHLY" && this.has_by_data("BYDAY")) {
var tempLast = null;
var initLast = this.last.clone();
var daysInMonth = ICAL.Time.daysInMonth(this.last.month, this.last.year);
daysInMonth = ICAL.Time.daysInMonth(this.last.month, this.last.year);

// Check every weekday in BYDAY with relative dow and pos.
for (var i in this.by_data.BYDAY) {
Expand Down Expand Up @@ -295,6 +312,7 @@ ICAL.RecurIterator = (function() {
// would be missed.
if (this.has_by_data('BYMONTHDAY')) {
this._byDayAndMonthDay(true);
daysInMonth = ICAL.Time.daysInMonth(this.last.month, this.last.year);
}

if (this.last.day > daysInMonth || this.last.day == 0) {
Expand All @@ -303,8 +321,8 @@ ICAL.RecurIterator = (function() {

} 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;
daysInMonth = ICAL.Time.daysInMonth(this.last.month, this.last.year);
this.last.day += daysInMonth + 1;
}
}

Expand Down Expand Up @@ -726,14 +744,16 @@ ICAL.RecurIterator = (function() {
var day = this.by_data.BYMONTHDAY[this.by_indices.BYMONTHDAY];

if (day < 0) {
day = daysInMonth + day + 1;
day += daysInMonth + 1;
}

if (day > daysInMonth) {
this.last.day = 1;
data_valid = this.is_day_in_byday(this.last);
} else {
} else if (day > 0) {
this.last.day = day;
} else {
data_valid = 0;
}

} else {
Expand Down
188 changes: 151 additions & 37 deletions test/recur_iterator_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,7 @@ suite('recur_iterator', function() {
]
});

//buisness days for 31 occurances'
//business days for 31 occurrences'
testRRULE('FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR', {
dates: [
'2012-01-02T09:00:00', '2012-01-03T09:00:00', '2012-01-04T09:00:00', '2012-01-05T09:00:00', '2012-01-06T09:00:00',
Expand Down Expand Up @@ -513,7 +513,6 @@ 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,
Expand All @@ -526,7 +525,7 @@ suite('recur_iterator', function() {

//monthly, each month last day that is monday
testRRULE('FREQ=MONTHLY;BYMONTHDAY=-1;BYDAY=MO', {
dtStart: '2012-01-01T09:00:00',
dtStart: '2012-02-01T09:00:00',
dates: [
'2012-04-30T09:00:00',
'2012-12-31T09:00:00'
Expand Down Expand Up @@ -666,7 +665,7 @@ suite('recur_iterator', function() {
]
});

// monthly, bymonthday
// monthly, bymonthday, the last day of the month
testRRULE('FREQ=MONTHLY;BYMONTHDAY=-1', {
dtStart: '2015-01-01T08:00:00',
dates: [
Expand All @@ -676,6 +675,85 @@ suite('recur_iterator', function() {
]
});

// monthly, bymonthday the last day of the month with interval
testRRULE('FREQ=MONTHLY;BYMONTHDAY=-1;INTERVAL=3', {
dtStart: '2016-04-20T08:00:00Z',
dates: [
'2016-04-30T08:00:00Z',
'2016-07-31T08:00:00Z',
'2016-10-31T08:00:00Z',
'2017-01-31T08:00:00Z'
]
});

// monthly, bymonthday the second to last day of the month with interval
testRRULE('FREQ=MONTHLY;BYMONTHDAY=-2;INTERVAL=2', {
dtStart: '2016-04-20T08:00:00Z',
dates: [
'2016-04-29T08:00:00Z',
'2016-06-29T08:00:00Z',
'2016-08-30T08:00:00Z',
'2016-10-30T08:00:00Z',
'2016-12-30T08:00:00Z',
'2017-02-27T08:00:00Z'
]
});

// monthly, bymonthday the 31st to last day of the month
testRRULE('FREQ=MONTHLY;BYMONTHDAY=-31', {
dtStart: '2016-02-08T08:00:00Z',
dates: [
'2016-03-01T08:00:00Z',
'2016-05-01T08:00:00Z',
'2016-07-01T08:00:00Z',
'2016-08-01T08:00:00Z',
'2016-10-01T08:00:00Z'
]
});

// monthly, bymonthday the 31st to last day of the month with interval
testRRULE('FREQ=MONTHLY;BYMONTHDAY=-31;INTERVAL=4', {
dtStart: '2016-02-08T08:00:00Z',
dates: [
'2016-10-01T08:00:00Z',
'2017-10-01T08:00:00Z',
'2018-10-01T08:00:00Z',
'2019-10-01T08:00:00Z'
]
});

// monthly, bymonthday=31 starting from a month with less than 31 days
testRRULE('FREQ=MONTHLY;BYMONTHDAY=31', {
dtStart: '2016-02-08T08:00:00Z',
dates: [
'2016-03-31T08:00:00Z',
'2016-05-31T08:00:00Z',
'2016-07-31T08:00:00Z',
'2016-08-31T08:00:00Z'
]
});

// monthly, bymonthday=31 starting from a month with less than 31 days with interval
testRRULE('FREQ=MONTHLY;BYMONTHDAY=31;INTERVAL=2', {
dtStart: '2016-02-08T08:00:00Z',
dates: [
'2016-08-31T08:00:00Z',
'2016-10-31T08:00:00Z',
'2016-12-31T08:00:00Z',
'2017-08-31T08:00:00Z'
]
});

// monthly, bymonthday=-31 byday=MO with interval: first occurrence far in the future from start date
testRRULE('FREQ=MONTHLY;INTERVAL=9;BYMONTHDAY=-31;BYDAY=MO', {
dtStart: '2016-03-08T08:00:00Z',
dates: [
'2025-12-01T08:00:00Z',
'2031-12-01T08:00:00Z',
'2049-03-01T08:00:00Z'
]
});

// monthly + by month
testRRULE('FREQ=MONTHLY;BYMONTH=1,3,6,9,12', {
dates: [
Expand All @@ -694,9 +772,9 @@ suite('recur_iterator', function() {
'2015-03-09T08:00:00Z',
'2015-03-13T08:00:00Z',
'2015-03-23T08:00:00Z',
'2015-03-27T08: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,
Expand All @@ -706,7 +784,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,
Expand All @@ -716,22 +794,22 @@ 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,
dates: [
"2015-03-01T08:00:00Z",
"2015-03-13T08:00:00Z",
"2015-03-15T08:00:00Z",
"2015-03-27T08:00:00Z",
"2015-03-29T08:00:00Z",
"2015-04-03T08:00:00Z",
"2015-04-05T08:00:00Z",
"2015-04-17T08:00:00Z",
"2015-04-19T08:00:00Z"
]
})
'2015-03-01T08:00:00Z',
'2015-03-13T08:00:00Z',
'2015-03-15T08:00:00Z',
'2015-03-27T08:00:00Z',
'2015-03-29T08:00:00Z',
'2015-04-03T08:00:00Z',
'2015-04-05T08:00:00Z',
'2015-04-17T08:00:00Z',
'2015-04-19T08:00:00Z'
]
});
});

suite('YEARLY', function() {
Expand Down Expand Up @@ -842,26 +920,62 @@ suite('recur_iterator', function() {

//Weekly Sunday, Monday, Tuesday with count=3
testRRULE('FREQ=WEEKLY;COUNT=3;BYDAY=MO,SU,TU', {
dtStart: '2017-07-30',
byCount: true,
dates: [
'2017-07-30',
'2017-07-31',
'2017-08-01'
]
dtStart: '2017-07-30',
byCount: true,
dates: [
'2017-07-30',
'2017-07-31',
'2017-08-01'
]
});

//Weekly Sunday, Wednesday with count=5
testRRULE('FREQ=WEEKLY;COUNT=5;BYDAY=SU,WE', {
dtStart: '2017-04-23',
byCount: true,
dates: [
'2017-04-23',
'2017-04-26',
'2017-04-30',
'2017-05-03',
'2017-05-07'
]
dtStart: '2017-04-23',
byCount: true,
dates: [
'2017-04-23',
'2017-04-26',
'2017-04-30',
'2017-05-03',
'2017-05-07'
]
});

//yearly, byDay,byMonth. The 4th Thursday of November (Thanksgiving Day)
testRRULE('FREQ=YEARLY;BYDAY=4TH;BYMONTH=11', {
dates: [
'2016-11-24',
'2017-11-23',
'2018-11-22',
'2019-11-28',
'2020-11-26',
'2021-11-25'
]
});

//yearly, byDay,byMonth,bySetPos. The 4th Thursday of November (Thanksgiving Day)
testRRULE('FREQ=YEARLY;BYDAY=TH;BYSETPOS=4;BYMONTH=11', {
dates: [
'2016-11-24',
'2017-11-23',
'2018-11-22',
'2019-11-28',
'2020-11-26',
'2021-11-25'
]
});

//yearly, byDay,byMonthday,byMonth with interval. The first Tuesday after November 1 (USA presidential elections)
testRRULE('FREQ=YEARLY;BYDAY=TU;BYMONTHDAY=2,3,4,5,6,7,8;BYMONTH=11;INTERVAL=4', {
dates: [
'2004-11-02',
'2008-11-04',
'2012-11-06',
'2016-11-08',
'2020-11-03',
'2024-11-05'
]
});

//yearly, byMonth, byweekNo
Expand Down Expand Up @@ -995,7 +1109,7 @@ suite('recur_iterator', function() {
dates: [
'2012-02-28',
'2012-02-29',
'2012-03-01',
'2012-03-01'
]
});

Expand All @@ -1005,7 +1119,7 @@ suite('recur_iterator', function() {
dates: [
'2013-02-28',
'2013-03-01',
'2013-03-02',
'2013-03-02'
]
});

Expand Down
Loading