Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F15376961
D16634.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
15 KB
Referenced Files
None
Subscribers
None
D16634.diff
View Options
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
Details
Attached
Mime Type
text/plain
Expires
Fri, Mar 14, 6:50 AM (1 w, 1 d ago)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
7649961
Default Alt Text
D16634.diff (15 KB)
Attached To
Mode
D16634: Implement RFC5545 WEEKLY RRULE tests
Attached
Detach File
Event Timeline
Log In to Comment