diff --git a/calendar/base/modules/ical.js b/calendar/base/modules/ical.js index 1d07bda463..5ce949d358 100644 --- a/calendar/base/modules/ical.js +++ b/calendar/base/modules/ical.js @@ -4647,33 +4647,43 @@ ICAL.RecurIterator = (function() { } if (this.rule.freq == "MONTHLY" && this.has_by_data("BYDAY")) { - - var coded_day = this.by_data.BYDAY[this.by_indices.BYDAY]; - var parts = this.ruleDayOfWeek(coded_day); - var pos = parts[0]; - var dow = parts[1]; - + var tempLast = null; + var initLast = this.last.clone(); var daysInMonth = ICAL.Time.daysInMonth(this.last.month, this.last.year); - var poscount = 0; - if (pos >= 0) { - for (this.last.day = 1; this.last.day <= daysInMonth; this.last.day++) { - if (this.last.dayOfWeek() == dow) { - if (++poscount == pos || pos == 0) { - break; - } + // Check every weekday in BYDAY with relative dow and pos. + for (var i in this.by_data.BYDAY) { + this.last = initLast.clone(); + var parts = this.ruleDayOfWeek(this.by_data.BYDAY[i]); + var pos = parts[0]; + var dow = parts[1]; + var dayOfMonth = this.last.nthWeekDay(dow, pos); + + // If |pos| >= 6, the byday is invalid for a monthly rule. + if (pos >= 6 || pos <= -6) { + throw new Error("Malformed values in BYDAY part"); + } + + // If a Byday with pos=+/-5 is not in the current month it + // must be searched in the next months. + if (dayOfMonth > daysInMonth || dayOfMonth <= 0) { + // Skip if we have already found a "last" in this month. + if (tempLast && tempLast.month == initLast.month) { + continue; + } + while (dayOfMonth > daysInMonth || dayOfMonth <= 0) { + this.increment_month(); + daysInMonth = ICAL.Time.daysInMonth(this.last.month, this.last.year); + dayOfMonth = this.last.nthWeekDay(dow, pos); } } - } else { - pos = -pos; - for (this.last.day = daysInMonth; this.last.day != 0; this.last.day--) { - if (this.last.dayOfWeek() == dow) { - if (++poscount == pos) { - break; - } - } + + this.last.day = dayOfMonth; + if (!tempLast || this.last.compare(tempLast) < 0) { + tempLast = this.last.clone(); } } + this.last = tempLast.clone(); //XXX: This feels like a hack, but we need to initialize // the BYMONTHDAY case correctly and byDayAndMonthDay handles diff --git a/calendar/libical/src/libical/icalrecur.c b/calendar/libical/src/libical/icalrecur.c index 58587305e4..1b4b5ae840 100644 --- a/calendar/libical/src/libical/icalrecur.c +++ b/calendar/libical/src/libical/icalrecur.c @@ -812,6 +812,8 @@ static int has_by_data(icalrecur_iterator* impl, enum byrule byrule){ static int expand_year_days(icalrecur_iterator* impl, int year); +static int nth_weekday(int dow, int pos, struct icaltimetype t); +static void increment_month(icalrecur_iterator* impl); icalrecur_iterator* icalrecur_iterator_new(struct icalrecurrencetype rule, @@ -1025,52 +1027,58 @@ icalrecur_iterator* icalrecur_iterator_new(struct icalrecurrencetype rule, if(impl->rule.freq == ICAL_MONTHLY_RECURRENCE && has_by_data(impl,BY_DAY)) { - int dow = icalrecurrencetype_day_day_of_week( - impl->by_ptrs[BY_DAY][impl->by_indices[BY_DAY]]); - int pos = icalrecurrencetype_day_position( - impl->by_ptrs[BY_DAY][impl->by_indices[BY_DAY]]); - - int poscount = 0; - int days_in_month = - icaltime_days_in_month(impl->last.month, impl->last.year); - - if(pos >= 0){ - /* Count up from the first day pf the month to find the - pos'th weekday of dow ( like the second monday. ) */ + struct icaltimetype tmp_last = icaltime_null_time(); + struct icaltimetype init_last = impl->last; + int days_in_month = + icaltime_days_in_month(impl->last.month, impl->last.year); + int i, dow, pos, day_of_month; - for(impl->last.day = 1; - impl->last.day <= days_in_month; - impl->last.day++){ - - if(icaltime_day_of_week(impl->last) == dow){ - if(++poscount == pos || pos == 0){ - break; - } + /* Check every weekday in BYDAY with relative dow and pos. */ + for (i = 0; impl->by_ptrs[BY_DAY][i] != ICAL_RECURRENCE_ARRAY_MAX; i++) { + impl->last = init_last; + dow = icalrecurrencetype_day_day_of_week(impl->by_ptrs[BY_DAY][i]); + pos = icalrecurrencetype_day_position(impl->by_ptrs[BY_DAY][i]); + day_of_month = nth_weekday(dow, pos, impl->last); + + /* If |pos| >= 6, the byday is invalid for a monthly rule */ + if (pos >= 6 || pos <= -6) { + icalerror_set_errno(ICAL_MALFORMEDDATA_ERROR); + free(impl); + return 0; + } + + /* If a Byday with pos=+/-5 is not in the current month it + must be searched in the next months. */ + if (day_of_month > days_in_month || day_of_month <= 0) { + /* Skip if we have already found a "last" in this month. */ + if (!icaltime_is_null_time(tmp_last) && tmp_last.month == init_last.month) { + continue; + } + while (day_of_month > days_in_month || day_of_month <= 0) { + impl->last.day = 1; + increment_month(impl); + days_in_month = + icaltime_days_in_month(impl->last.month, impl->last.year); + day_of_month = nth_weekday(dow, pos, impl->last); } } - } else { - /* Count down from the last day pf the month to find the - pos'th weekday of dow ( like the second to last monday. ) */ - pos = -pos; - for(impl->last.day = days_in_month; - impl->last.day != 0; - impl->last.day--){ - - if(icaltime_day_of_week(impl->last) == dow){ - if(++poscount == pos ){ - break; - } - } + + impl->last.day = day_of_month; + if (icaltime_is_null_time(tmp_last) || + icaltime_compare(impl->last, tmp_last) < 0) { + tmp_last = impl->last; } } + impl->last = tmp_last; - if(impl->last.day > days_in_month || impl->last.day == 0){ - icalerror_set_errno(ICAL_MALFORMEDDATA_ERROR); + + if (impl->last.day > days_in_month || impl->last.day == 0) { + icalerror_set_errno(ICAL_MALFORMEDDATA_ERROR); free(impl); return 0; - } - + } + } else if (has_by_data(impl,BY_MONTH_DAY)) { // setup_defaults sets the day to -1 for negative BYMONTHDAY values, // so make sure to re-calculate with days_in_month diff --git a/calendar/test/unit/test_recur.js b/calendar/test/unit/test_recur.js index a83d4138f9..80e8a3287d 100644 --- a/calendar/test/unit/test_recur.js +++ b/calendar/test/unit/test_recur.js @@ -268,6 +268,61 @@ function test_rules() { expectedDates, false); + // Bug 958974 - Monthly recurrence every WE, FR and the third MO (monthly with more bydays). + // Check the occurrences in the first month until the week with the first monday of the rule. + check_recur(createEventFromIcalString("BEGIN:VCALENDAR\nBEGIN:VEVENT\n" + + "DESCRIPTION:Repeat Monthly every Wednesday, Friday and the third Monday\n" + + "RRULE:FREQ=MONTHLY;COUNT=8;BYDAY=3MO,WE,FR\n" + + "DTSTART:20150102T080000Z\n" + + "DTEND:20150102T090000Z\n" + + "END:VEVENT\nEND:VCALENDAR\n"), + ["20150102T080000Z", "20150107T080000Z", "20150109T080000Z", + "20150114T080000Z", "20150116T080000Z", "20150119T080000Z", + "20150121T080000Z", "20150123T080000Z"], + false); + + // Bug 419490 - Monthly recurrence, the fifth Saturday starting from February. + // Check a monthly rule that specifies a day that is not part of the month + // the events starts in. + check_recur(createEventFromIcalString("BEGIN:VCALENDAR\nBEGIN:VEVENT\n" + + "DESCRIPTION:Repeat Monthly the fifth Saturday\n" + + "RRULE:FREQ=MONTHLY;COUNT=6;BYDAY=5SA\n" + + "DTSTART:20150202T080000Z\n" + + "DTEND:20150202T090000Z\n" + + "END:VEVENT\nEND:VCALENDAR\n"), + ["20150202T080000Z", + "20150530T080000Z", "20150829T080000Z", "20151031T080000Z", + "20160130T080000Z", "20160430T080000Z", "20160730T080000Z"], + false); + + // Bug 419490 - Monthly recurrence, the fifth Wednesday every two months starting from February. + // Check a monthly rule that specifies a day that is not part of the month + // the events starts in. + check_recur(createEventFromIcalString("BEGIN:VCALENDAR\nBEGIN:VEVENT\n" + + "DESCRIPTION:Repeat Monthly the fifth Friday every two months\n" + + "RRULE:FREQ=MONTHLY;INTERVAL=2;COUNT=6;BYDAY=5FR\n" + + "DTSTART:20150202T080000Z\n" + + "DTEND:20150202T090000Z\n" + + "END:VEVENT\nEND:VCALENDAR\n"), + ["20150202T080000Z", + "20151030T080000Z", "20160429T080000Z", "20161230T080000Z", + "20170630T080000Z", "20171229T080000Z", "20180629T080000Z"], + false); + + // Bugs 419490, 958974 - Monthly recurrence, the 2nd Monday, 5th Wednesday and the 5th to last Saturday every month starting from February. + // Check a monthly rule that specifies a day that is not part of the month + // the events starts in with positive and negative position along with other byday. + check_recur(createEventFromIcalString("BEGIN:VCALENDAR\nBEGIN:VEVENT\n" + + "DESCRIPTION:Repeat Monthly the 2nd Monday, 5th Wednesday and the 5th to last Saturday every month\n" + + "RRULE:FREQ=MONTHLY;COUNT=7;BYDAY=2MO,-5WE,5SA\n" + + "DTSTART:20150401T080000Z\n" + + "DTEND:20150401T090000Z\n" + + "END:VEVENT\nEND:VCALENDAR\n"), + ["20150401T080000Z", + "20150413T080000Z", "20150511T080000Z", "20150530T080000Z", + "20150608T080000Z", "20150701T080000Z", "20150713T080000Z"], + false); + let item, occ1; item = makeEvent("DESCRIPTION:occurrence on day 1 moved between the occurrences " + "on days 2 and 3\n" +