Page MenuHomePhabricator

D16634.diff
No OneTemporary

D16634.diff

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
@@ -23,6 +23,8 @@
private $cursorHour;
private $cursorHourState;
private $cursorWeek;
+ private $cursorWeekday;
+ private $cursorWeekState;
private $cursorDay;
private $cursorDayState;
private $cursorMonth;
@@ -33,12 +35,14 @@
private $setHours;
private $setDays;
private $setMonths;
+ private $setWeeks;
private $setYears;
private $stateSecond;
private $stateMinute;
private $stateHour;
private $stateDay;
+ private $stateWeek;
private $stateMonth;
private $stateYear;
@@ -313,18 +317,63 @@
}
public function resetSource() {
+ $frequency = $this->getFrequency();
+
+ if ($this->getByMonthDay()) {
+ switch ($frequency) {
+ case self::FREQUENCY_WEEKLY:
+ // RFC5545: "The BYMONTHDAY rule part MUST NOT be specified when the
+ // FREQ rule part is set to WEEKLY."
+ throw new Exception(
+ pht(
+ 'RRULE specifies BYMONTHDAY with FREQ set to WEEKLY, which '.
+ 'violates RFC5545.'));
+ break;
+ default:
+ break;
+ }
+
+ }
+
+ if ($this->getByYearDay()) {
+ switch ($frequency) {
+ case self::FREQUENCY_DAILY:
+ case self::FREQUENCY_WEEKLY:
+ case self::FREQUENCY_MONTHLY:
+ // RFC5545: "The BYYEARDAY rule part MUST NOT be specified when the
+ // FREQ rule part is set to DAILY, WEEKLY, or MONTHLY."
+ throw new Exception(
+ pht(
+ 'RRULE specifies BYYEARDAY with FREQ of DAILY, WEEKLY or '.
+ 'MONTHLY, which violates RFC5545.'));
+ default:
+ break;
+ }
+ }
+
+ // TODO
+ // RFC5545: "The BYDAY rule part MUST NOT be specified with a numeric
+ // value when the FREQ rule part is not set to MONTHLY or YEARLY."
+ // RFC5545: "Furthermore, the BYDAY rule part MUST NOT be specified with a
+ // numeric value with the FREQ rule part set to YEARLY when the BYWEEKNO
+ // rule part is specified."
+
+
$date = $this->getStartDateTime();
$this->cursorSecond = $date->getSecond();
$this->cursorMinute = $date->getMinute();
$this->cursorHour = $date->getHour();
- // TODO: Figure this out.
- $this->cursorWeek = null;
$this->cursorDay = $date->getDay();
$this->cursorMonth = $date->getMonth();
$this->cursorYear = $date->getYear();
+ $year_map = $this->getYearMap($this->cursorYear, $this->getWeekStart());
+ $key = $this->cursorMonth.'M'.$this->cursorDay.'D';
+ $this->cursorWeek = $year_map['info'][$key]['week'];
+ $this->cursorWeekday = $year_map['info'][$key]['weekday'];
+
$this->setSeconds = array();
$this->setMinutes = array();
$this->setHours = array();
@@ -336,6 +385,7 @@
$this->stateMinute = null;
$this->stateHour = null;
$this->stateDay = null;
+ $this->stateWeek = null;
$this->stateMonth = null;
$this->stateYear = null;
@@ -344,7 +394,6 @@
// the start date, but we need to go back one interval to generate
// BYSETPOS events so we can make sure the entire set is generated.
if ($this->getBySetPosition()) {
- $frequency = $this->getFrequency();
$interval = $this->getInterval();
switch ($frequency) {
case self::FREQUENCY_YEARLY:
@@ -373,20 +422,22 @@
'RRULE specifies BYSETPOS with FREQ "%s", but this is invalid.',
$frequency));
}
-
- $this->minimumEpoch = $this->getStartDateTime()->getEpoch();
- } else {
- $this->minimumEpoch = null;
}
+ // We can generate events from before the cursor when evaluating rules
+ // with BYSETPOS or FREQ=WEEKLY.
+ $this->minimumEpoch = $this->getStartDateTime()->getEpoch();
+
$cursor_state = array(
'year' => $this->cursorYear,
'month' => $this->cursorMonth,
+ 'week' => $this->cursorWeek,
'day' => $this->cursorDay,
'hour' => $this->cursorHour,
);
$this->cursorDayState = $cursor_state;
+ $this->cursorWeekState = $cursor_state;
$this->cursorHourState = $cursor_state;
$by_hour = $this->getByHour();
@@ -596,7 +647,8 @@
protected function nextDay() {
if ($this->setDays) {
- $this->stateDay = array_pop($this->setDays);
+ $info = array_pop($this->setDays);
+ $this->setDayState($info);
return;
}
@@ -614,9 +666,14 @@
$week_start = $this->getWeekStart();
while (!$this->setDays) {
- $this->nextMonth();
+ if ($is_weekly) {
+ $this->nextWeek();
+ } else {
+ $this->nextMonth();
+ }
$is_dynamic = $is_daily
+ || $is_weekly
|| $by_day
|| $by_monthday
|| $by_yearday
@@ -626,7 +683,7 @@
if ($is_dynamic) {
$weeks = $this->newDaysSet(
($is_daily ? $interval : 1),
- ($is_weekly ? $interval : null),
+ ($is_weekly ? $interval : 1),
$by_day,
$by_monthday,
$by_yearday,
@@ -642,8 +699,9 @@
array(),
);
} else {
+ $key = $this->stateMonth.'M'.$this->cursorDay.'D';
$weeks = array(
- array($this->cursorDay),
+ array($year_map['info'][$key]),
);
}
}
@@ -654,7 +712,14 @@
$this->setDays = array_reverse($days);
}
- $this->stateDay = array_pop($this->setDays);
+ $info = array_pop($this->setDays);
+ $this->setDayState($info);
+ }
+
+ private function setDayState(array $info) {
+ $this->stateDay = $info['monthday'];
+ $this->stateWeek = $info['week'];
+ $this->stateMonth = $info['month'];
}
protected function nextMonth() {
@@ -708,6 +773,30 @@
$this->stateMonth = array_pop($this->setMonths);
}
+ protected function nextWeek() {
+ if ($this->setWeeks) {
+ $this->stateWeek = array_pop($this->setWeeks);
+ return;
+ }
+
+ $frequency = $this->getFrequency();
+ $interval = $this->getInterval();
+ $scale = $this->getFrequencyScale();
+ $by_weekno = $this->getByWeekNumber();
+
+ while (!$this->setWeeks) {
+ $this->nextYear();
+
+ $weeks = $this->newWeeksSet(
+ $interval,
+ $by_weekno);
+
+ $this->setWeeks = array_reverse($weeks);
+ }
+
+ $this->stateWeek = array_pop($this->setWeeks);
+ }
+
protected function nextYear() {
$this->stateYear = $this->cursorYear;
@@ -801,6 +890,35 @@
return $result;
}
+ private function newWeeksSet($interval, $set) {
+ $week_start = $this->getWeekStart();
+
+ list($skip, $this->cursorWeekState) = $this->advanceCursorState(
+ $this->cursorWeekState,
+ self::SCALE_WEEKLY,
+ $interval,
+ $week_start);
+
+ if ($skip) {
+ return array();
+ }
+
+ $year_map = $this->getYearMap($this->stateYear, $week_start);
+
+ $result = array();
+ while (true) {
+ if (!isset($year_map['weekMap'][$this->cursorWeek])) {
+ break;
+ }
+ $result[] = $this->cursorWeek;
+ $this->cursorWeek += $interval;
+ }
+
+ $this->cursorWeek -= $year_map['weekCount'];
+
+ return $result;
+ }
+
private function newDaysSet(
$interval_day,
$interval_week,
@@ -810,33 +928,21 @@
$by_weekno,
$week_start) {
+ $frequency = $this->getFrequency();
+ $is_yearly = ($frequency == self::FREQUENCY_YEARLY);
+ $is_monthly = ($frequency == self::FREQUENCY_MONTHLY);
+ $is_weekly = ($frequency == self::FREQUENCY_WEEKLY);
+
$selection = array();
- if ($interval_week) {
+ if ($is_weekly) {
$year_map = $this->getYearMap($this->stateYear, $week_start);
- while (true) {
- // TODO: This is all garbage?
- if ($this->cursorWeek > $year_map['weekCount']) {
- $this->cursorWeek -= $year_map['weekCount'];
- break;
- }
-
- foreach ($year_map['weeks'][$this->cursorWeek] as $key) {
+ if (isset($year_map['weekMap'][$this->stateWeek])) {
+ foreach ($year_map['weekMap'][$this->stateWeek] as $key) {
$selection[] = $year_map['info'][$key];
}
-
- $last = last($selection);
- if ($last['month'] > $this->stateMonth) {
- break;
- }
-
- $this->cursorWeek += $interval_week;
}
} else {
- if (!$interval_day) {
- $interval_day = 1;
- }
-
// If the day cursor is behind the current year and month, we need to
// forward it in INTERVAL increments so we end up with the right offset
// in the current month.
@@ -867,10 +973,6 @@
}
}
- $frequency = $this->getFrequency();
- $is_yearly = ($frequency == self::FREQUENCY_YEARLY);
- $is_monthly = ($frequency == self::FREQUENCY_MONTHLY);
-
// As a special case, BYDAY applies to relative month offsets if BYMONTH
// is present in a YEARLY rule.
if ($is_yearly) {
@@ -880,10 +982,27 @@
}
}
+ // As a special case, BYDAY makes us examine all week days. This doesn't
+ // check BYMONTHDAY or BYYEARDAY because they are not valid with WEEKLY.
+ $filter_weekday = true;
+ if ($is_weekly) {
+ if ($by_day) {
+ $filter_weekday = false;
+ }
+ }
+
$weeks = array();
foreach ($selection as $key => $info) {
- if ($info['month'] != $this->stateMonth) {
- continue;
+ if ($is_weekly) {
+ if ($filter_weekday) {
+ if ($info['weekday'] != $this->cursorWeekday) {
+ continue;
+ }
+ }
+ } else {
+ if ($info['month'] != $this->stateMonth) {
+ continue;
+ }
}
if ($by_day) {
@@ -925,7 +1044,7 @@
}
}
- $weeks[$info['week']][] = $info['monthday'];
+ $weeks[$info['week']][] = $info;
}
return array_values($weeks);
@@ -1132,11 +1251,17 @@
$info_map[$key] = $info;
}
+ $week_map = array();
+ foreach ($info_map as $key => $info) {
+ $week_map[$info['week']][] = $key;
+ }
+
return array(
'info' => $info_map,
'weekCount' => $week_number,
'dayCount' => $max_day,
'monthDays' => $month_days,
+ 'weekMap' => $week_map,
);
}
@@ -1232,17 +1357,22 @@
$parts = array();
$parts[] = $this->stateYear;
- if ($scale < self::SCALE_YEARLY) {
- $parts[] = $this->stateMonth;
- }
- if ($scale < self::SCALE_MONTHLY) {
- $parts[] = $this->stateDay;
- }
- if ($scale < self::SCALE_DAILY) {
- $parts[] = $this->stateHour;
- }
- if ($scale < self::SCALE_HOURLY) {
- $parts[] = $this->stateMinute;
+
+ if ($scale == self::SCALE_WEEKLY) {
+ $parts[] = $this->stateWeek;
+ } else {
+ if ($scale < self::SCALE_YEARLY) {
+ $parts[] = $this->stateMonth;
+ }
+ if ($scale < self::SCALE_MONTHLY) {
+ $parts[] = $this->stateDay;
+ }
+ if ($scale < self::SCALE_DAILY) {
+ $parts[] = $this->stateHour;
+ }
+ if ($scale < self::SCALE_HOURLY) {
+ $parts[] = $this->stateMinute;
+ }
}
return implode('/', $parts);
@@ -1290,6 +1420,7 @@
$state = array(
'year' => $this->stateYear,
'month' => $this->stateMonth,
+ 'week' => $this->stateWeek,
'day' => $this->stateDay,
'hour' => $this->stateHour,
);
@@ -1306,8 +1437,12 @@
case self::SCALE_HOURLY:
$this->cursorHour = 0;
break;
+ case self::SCALE_WEEKLY:
+ $this->cursorWeek = 1;
+ break;
}
}
+
return array(false, $state);
}
@@ -1320,6 +1455,9 @@
case self::SCALE_HOURLY:
$cursor['hour'] += $interval;
break;
+ case self::SCALE_WEEKLY:
+ $cursor['week'] += $interval;
+ break;
}
if ($scale <= self::SCALE_HOURLY) {
@@ -1329,6 +1467,14 @@
}
}
+ if ($scale == self::SCALE_WEEKLY) {
+ while ($cursor['week'] > $year_map['weekCount']) {
+ $cursor['week'] -= $year_map['weekCount'];
+ $cursor['year']++;
+ $year_map = $this->getYearMap($cursor['year'], $week_start);
+ }
+ }
+
if ($scale <= self::SCALE_DAILY) {
while ($cursor['day'] > $year_map['monthDays'][$cursor['month']]) {
$cursor['day'] -= $year_map['monthDays'][$cursor['month']];
@@ -1349,6 +1495,9 @@
case self::SCALE_HOURLY:
$this->cursorHour = $cursor['hour'];
break;
+ case self::SCALE_WEEKLY:
+ $this->cursorWeek = $cursor['week'];
+ break;
}
$skip = $this->isCursorBehind($state, $cursor, $scale);
@@ -1363,6 +1512,10 @@
return false;
}
+ if ($scale == self::SCALE_WEEKLY) {
+ return false;
+ }
+
if ($cursor['month'] < $state['month']) {
return true;
} else if ($cursor['month'] > $state['month']) {
@@ -1393,5 +1546,4 @@
}
-
}
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
@@ -1474,6 +1474,108 @@
'19970902T133000Z',
);
+ $tests[] = array(
+ 'FREQ' => 'WEEKLY',
+ 'COUNT' => 10,
+ 'DTSTART' => '19970902T090000Z',
+ );
+ $expect[] = array(
+ '19970902T090000Z',
+ '19970909T090000Z',
+ '19970916T090000Z',
+ '19970923T090000Z',
+ '19970930T090000Z',
+ '19971007T090000Z',
+ '19971014T090000Z',
+ '19971021T090000Z',
+ '19971028T090000Z',
+ '19971104T090000Z',
+ );
+
+ $tests[] = array(
+ 'FREQ' => 'WEEKLY',
+ 'INTERVAL' => 2,
+ 'COUNT' => 6,
+ 'DTSTART' => '19970902T090000Z',
+ );
+ $expect[] = array(
+ '19970902T090000Z',
+ '19970916T090000Z',
+ '19970930T090000Z',
+ '19971014T090000Z',
+ '19971028T090000Z',
+ '19971111T090000Z',
+ );
+
+ $tests[] = array(
+ 'FREQ' => 'WEEKLY',
+ 'COUNT' => 10,
+ 'WKST' => 'SU',
+ 'BYDAY' => array('TU', 'TH'),
+ 'DTSTART' => '19970902T090000Z',
+ );
+ $expect[] = array(
+ '19970902T090000Z',
+ '19970904T090000Z',
+ '19970909T090000Z',
+ '19970911T090000Z',
+ '19970916T090000Z',
+ '19970918T090000Z',
+ '19970923T090000Z',
+ '19970925T090000Z',
+ '19970930T090000Z',
+ '19971002T090000Z',
+ );
+
+ $tests[] = array(
+ 'FREQ' => 'WEEKLY',
+ 'INTERVAL' => 2,
+ 'COUNT' => 8,
+ 'WKST' => 'SU',
+ 'BYDAY' => array('TU', 'TH'),
+ 'DTSTART' => '19970902T090000Z',
+ );
+ $expect[] = array(
+ '19970902T090000Z',
+ '19970904T090000Z',
+ '19970916T090000Z',
+ '19970918T090000Z',
+ '19970930T090000Z',
+ '19971002T090000Z',
+ '19971014T090000Z',
+ '19971016T090000Z',
+ );
+
+ $tests[] = array(
+ 'FREQ' => 'WEEKLY',
+ 'INTERVAL' => 2,
+ 'COUNT' => 4,
+ 'BYDAY' => array('TU', 'SU'),
+ 'WKST' => 'MO',
+ 'DTSTART' => '19970805T090000Z',
+ );
+ $expect[] = array(
+ '19970805T090000Z',
+ '19970810T090000Z',
+ '19970819T090000Z',
+ '19970824T090000Z',
+ );
+
+ $tests[] = array(
+ 'FREQ' => 'WEEKLY',
+ 'INTERVAL' => 2,
+ 'COUNT' => 4,
+ 'BYDAY' => array('TU', 'SU'),
+ 'WKST' => 'SU',
+ 'DTSTART' => '19970805T090000Z',
+ );
+ $expect[] = array(
+ '19970805T090000Z',
+ '19970817T090000Z',
+ '19970819T090000Z',
+ '19970831T090000Z',
+ );
+
$this->assertRules(array(), $tests, $expect);
}
@@ -1540,6 +1642,11 @@
$rrule->setBySetPosition($by_setpos);
}
+ $week_start = idx($options, 'WKST');
+ if ($week_start) {
+ $rrule->setWeekStart($week_start);
+ }
+
$set = id(new PhutilCalendarRecurrenceSet())
->addSource($rrule);

File Metadata

Mime Type
text/plain
Expires
Wed, Jun 26, 6:16 PM (4 d, 41 m ago)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
6288532
Default Alt Text
D16634.diff (15 KB)

Event Timeline