diff --git a/src/parser/calendar/data/PhutilCalendarRecurrenceRule.php b/src/parser/calendar/data/PhutilCalendarRecurrenceRule.php --- a/src/parser/calendar/data/PhutilCalendarRecurrenceRule.php +++ b/src/parser/calendar/data/PhutilCalendarRecurrenceRule.php @@ -42,6 +42,7 @@ private $initialMonth; private $initialYear; + private $baseYear; const FREQUENCY_SECONDLY = 'SECONDLY'; const FREQUENCY_MINUTELY = 'MINUTELY'; @@ -101,6 +102,7 @@ pht( 'Weekday "%s" is not a valid weekday constant. Valid constants '. 'are: %s.', + $weekday, implode(', ', $constants))); } @@ -337,6 +339,8 @@ public function getNextEvent($cursor) { $date = $this->getStartDateTime(); + $this->baseYear = $this->cursorYear; + $all_day = $date->getIsAllDay(); if ($all_day) { $this->nextDay(); @@ -608,7 +612,6 @@ } protected function nextYear() { - $this->stateYear = $this->cursorYear; $frequency = $this->getFrequency(); @@ -621,6 +624,14 @@ } $this->cursorYear = $this->cursorYear + $interval; + + if ($this->cursorYear > ($this->baseYear + 100)) { + throw new Exception( + pht( + 'RRULE evaluation failed to generate more events in the next 100 '. + 'years. This RRULE is likely invalid or degenerate.')); + } + } private function newSecondsSet($interval, $set) { @@ -818,20 +829,20 @@ public static function getYearMap($year, $week_start) { static $maps = array(); - $weekday_index = self::getWeekdayIndex($week_start); - $key = "{$year}/{$week_start}"; if (isset($maps[$key])) { return $maps[$key]; } - $map = self::newYearMap($year, $weekday_index); + $map = self::newYearMap($year, $week_start); $maps[$key] = $map; return $maps[$key]; } - private static function newYearMap($year, $weekday_index) { + private static function newYearMap($year, $weekday_start) { + $weekday_index = self::getWeekdayIndex($weekday_start); + $is_leap = (($year % 4 === 0) && ($year % 100 !== 0)) || ($year % 400 === 0); @@ -918,21 +929,50 @@ } } + // Check how long the final week is. If it doesn't have four days, this + // is really the first week of the next year. + $final_week = array(); + foreach ($info_map as $key => $info) { + if ($info['week'] == $week_number) { + $final_week[] = $key; + } + } + + if (count($final_week) < 4) { + $week_number = $week_number - 1; + $next_year = self::getYearMap($year + 1, $weekday_start); + $next_year_weeks = $next_year['weekCount']; + } else { + $next_year_weeks = null; + } + + if ($first_week_size < 4) { + $last_year = self::getYearMap($year - 1, $weekday_start); + $last_year_weeks = $last_year['weekCount']; + } else { + $last_year_weeks = null; + } + // Now that we know how many weeks the year has, we can compute the // negative offsets. foreach ($info_map as $key => $info) { $week = $info['week']; - if (!$week) { - // If this day is part of the "zeroth" week of the year, it does not - // get a reverse index. In particular, it is not week "-53" (ethe - // 53rd week from the end of the year) in a 52-week year. - $week_value = 0; + if ($week === 0) { + // If this day is part of the first partial week of the year, give + // it the week number of the last week of the prior year instead. + $info['week'] = $last_year_weeks; + $info['-week'] = -1; + } else if ($week > $week_number) { + // If this day is part of the last partial week of the year, give + // it week numbers from the next year. + $info['week'] = 1; + $info['-week'] = -$next_year_weeks; } else { - $week_value = -$week_number + $week - 1; + $info['-week'] = -$week_number + $week - 1; } - $info_map[$key]['-week'] = $week_value; + $info_map[$key] = $info; } return array( diff --git a/src/parser/calendar/data/__tests__/PhutilCalendarRecurrenceRuleTestCase.php b/src/parser/calendar/data/__tests__/PhutilCalendarRecurrenceRuleTestCase.php --- a/src/parser/calendar/data/__tests__/PhutilCalendarRecurrenceRuleTestCase.php +++ b/src/parser/calendar/data/__tests__/PhutilCalendarRecurrenceRuleTestCase.php @@ -263,6 +263,45 @@ '19980513', ); + $tests[] = array( + 'BYWEEKNO' => array(1), + 'BYDAY' => array('MO'), + ); + $expect[] = array( + '19971229', + '19990104', + '20000103', + ); + + $tests[] = array( + 'BYWEEKNO' => array(52), + 'BYDAY' => array('SU'), + ); + $expect[] = array( + '19971228', + '19981227', + '20000102', + ); + + $tests[] = array( + 'BYWEEKNO' => array(-1), + 'BYDAY' => array('SU'), + ); + $expect[] = array( + '19971228', + '19990103', + '20000102', + ); + + $tests[] = array( + 'BYWEEKNO' => array(53), + 'BYDAY' => array('MO'), + ); + $expect[] = array( + '19981228', + '20041227', + '20091228', + ); $this->assertRules( array(