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 @@ -127,7 +127,7 @@ if (empty($map[$frequency])) { throw new Exception( pht( - 'RRULE FREQUENCY "%s" is invalid. Valid frequencies are: %s.', + 'RRULE FREQ "%s" is invalid. Valid frequencies are: %s.', $frequency, implode(', ', array_keys($map)))); } @@ -489,6 +489,7 @@ $by_yearday = $this->getByYearDay(); $by_weekno = $this->getByWeekNumber(); $by_setpos = $this->getBySetPosition(); + $week_start = $this->getWeekStart(); while (!$this->setDays) { $this->nextMonth(); @@ -501,7 +502,7 @@ $by_monthday, $by_yearday, $by_weekno, - $this->getWeekStart()); + $week_start); } else if ($scale < self::SCALE_DAILY) { $weeks = $this->newDaysSet( 1, @@ -510,11 +511,21 @@ array(), array(), array(), - $this->getWeekStart()); + $week_start); } else { - $weeks = array( - array($this->cursorDay), - ); + // The cursor day may not actually exist in the current month, so + // make sure the day is valid before we generate a set which contains + // it. + $year_map = $this->getYearMap($this->stateYear, $week_start); + if ($this->cursorDay > $year_map['monthDays'][$this->stateMonth]) { + $weeks = array( + array(), + ); + } else { + $weeks = array( + array($this->cursorDay), + ); + } } // Apply weekly BYSETPOS, if one exists. @@ -578,6 +589,7 @@ } protected function nextYear() { + $this->stateYear = $this->cursorYear; $frequency = $this->getFrequency(); @@ -680,7 +692,7 @@ } $last = last($selection); - if ($last['month'] > $this->cursorMonth) { + if ($last['month'] > $this->stateMonth) { break; } @@ -688,7 +700,7 @@ } } else { $calendar = $year_map['calendar']; - $month_idx = $this->cursorMonth; + $month_idx = $this->stateMonth; if (!$interval_day) { $interval_day = 1; @@ -712,7 +724,7 @@ $weeks = array(); foreach ($selection as $key => $info) { - if ($info['month'] != $this->cursorMonth) { + if ($info['month'] != $this->stateMonth) { continue; } 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 @@ -102,4 +102,71 @@ pht('Secondly event with many constraints.')); } + public function testYearlyRecurrenceRules() { + $tests = array(); + $expect = array(); + + $tests[] = array(); + $expect[] = array( + '19970902', + '19980902', + '19990902', + ); + + $tests[] = array( + 'INTERVAL' => 2, + ); + $expect[] = array( + '19970902', + '19990902', + '20010902', + ); + + $tests[] = array( + 'DTSTART' => '20000229', + ); + $expect[] = array( + '20000229', + '20040229', + '20080229', + ); + + $this->assertRules( + array( + 'FREQ' => 'YEARLY', + 'COUNT' => 3, + 'DTSTART' => '19970902', + ), + $tests, + $expect); + } + + private function assertRules(array $defaults, array $tests, array $expect) { + foreach ($tests as $key => $test) { + $options = $test + $defaults; + + $start = PhutilCalendarAbsoluteDateTime::newFromISO8601( + $options['DTSTART']); + + $rrule = id(new PhutilCalendarRecurrenceRule()) + ->setStartDateTime($start) + ->setFrequency($options['FREQ']); + + $interval = idx($options, 'INTERVAL'); + if ($interval) { + $rrule->setInterval($interval); + } + + $set = id(new PhutilCalendarRecurrenceSet()) + ->addSource($rrule); + + $result = $set->getEventsBetween(null, null, $options['COUNT']); + + $this->assertEqual( + $expect[$key], + mpull($result, 'getISO8601')); + } + } + + }