Page Menu
Home
GnuPG
Search
Configure Global Search
Log In
Files
F34621300
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Size
32 KB
Subscribers
None
View Options
diff --git a/src/parser/calendar/data/PhutilCalendarRecurrenceRule.php b/src/parser/calendar/data/PhutilCalendarRecurrenceRule.php
index 200d4d7..9c16840 100644
--- a/src/parser/calendar/data/PhutilCalendarRecurrenceRule.php
+++ b/src/parser/calendar/data/PhutilCalendarRecurrenceRule.php
@@ -1,996 +1,1008 @@
<?php
final class PhutilCalendarRecurrenceRule
extends PhutilCalendarRecurrenceSource {
private $startDateTime;
private $frequency;
private $frequencyScale;
private $interval = 1;
private $bySecond = array();
private $byMinute = array();
private $byHour = array();
private $byDay = array();
private $byMonthDay = array();
private $byYearDay = array();
private $byWeekNumber = array();
private $byMonth = array();
private $bySetPosition = array();
private $weekStart = self::WEEKDAY_MONDAY;
private $cursorSecond;
private $cursorMinute;
private $cursorHour;
private $cursorWeek;
private $cursorDay;
private $cursorMonth;
private $cursorYear;
private $setSeconds;
private $setMinutes;
private $setHours;
private $setDays;
private $setMonths;
private $setYears;
private $stateSecond;
private $stateMinute;
private $stateHour;
private $stateDay;
private $stateMonth;
private $stateYear;
const FREQUENCY_SECONDLY = 'SECONDLY';
const FREQUENCY_MINUTELY = 'MINUTELY';
const FREQUENCY_HOURLY = 'HOURLY';
const FREQUENCY_DAILY = 'DAILY';
const FREQUENCY_WEEKLY = 'WEEKLY';
const FREQUENCY_MONTHLY = 'MONTHLY';
const FREQUENCY_YEARLY = 'YEARLY';
const SCALE_SECONDLY = 1;
const SCALE_MINUTELY = 2;
const SCALE_HOURLY = 3;
const SCALE_DAILY = 4;
const SCALE_WEEKLY = 5;
const SCALE_MONTHLY = 6;
const SCALE_YEARLY = 7;
const WEEKDAY_SUNDAY = 'SU';
const WEEKDAY_MONDAY = 'MO';
const WEEKDAY_TUESDAY = 'TU';
const WEEKDAY_WEDNESDAY = 'WE';
const WEEKDAY_THURSDAY = 'TH';
const WEEKDAY_FRIDAY = 'FR';
const WEEKDAY_SATURDAY = 'SA';
const WEEKINDEX_SUNDAY = 0;
const WEEKINDEX_MONDAY = 1;
const WEEKINDEX_TUESDAY = 2;
const WEEKINDEX_WEDNESDAY = 3;
const WEEKINDEX_THURSDAY = 4;
const WEEKINDEX_FRIDAY = 5;
const WEEKINDEX_SATURDAY = 6;
private static function getAllWeekdayConstants() {
return array_keys(self::getWeekdayIndexMap());
}
private static function getWeekdayIndexMap() {
static $map = array(
self::WEEKDAY_SUNDAY => self::WEEKINDEX_SUNDAY,
self::WEEKDAY_MONDAY => self::WEEKINDEX_MONDAY,
self::WEEKDAY_TUESDAY => self::WEEKINDEX_TUESDAY,
self::WEEKDAY_WEDNESDAY => self::WEEKINDEX_WEDNESDAY,
self::WEEKDAY_THURSDAY => self::WEEKINDEX_THURSDAY,
self::WEEKDAY_FRIDAY => self::WEEKINDEX_FRIDAY,
self::WEEKDAY_SATURDAY => self::WEEKINDEX_SATURDAY,
);
return $map;
}
private static function getWeekdayIndex($weekday) {
$map = self::getWeekdayIndexMap();
if (!isset($map[$weekday])) {
$constants = array_keys($map);
throw new Exception(
pht(
'Weekday "%s" is not a valid weekday constant. Valid constants '.
'are: %s.',
implode(', ', $constants)));
}
return $map[$weekday];
}
public function setStartDateTime(PhutilCalendarDateTime $start) {
$this->startDateTime = $start;
return $this;
}
public function getStartDateTime() {
return $this->startDateTime;
}
public function setFrequency($frequency) {
static $map = array(
self::FREQUENCY_SECONDLY => self::SCALE_SECONDLY,
self::FREQUENCY_MINUTELY => self::SCALE_MINUTELY,
self::FREQUENCY_HOURLY => self::SCALE_HOURLY,
self::FREQUENCY_DAILY => self::SCALE_DAILY,
self::FREQUENCY_WEEKLY => self::SCALE_WEEKLY,
self::FREQUENCY_MONTHLY => self::SCALE_MONTHLY,
self::FREQUENCY_YEARLY => self::SCALE_YEARLY,
);
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))));
}
$this->frequency = $frequency;
$this->frequencyScale = $map[$frequency];
return $this;
}
public function getFrequency() {
return $this->frequency;
}
public function getFrequencyScale() {
return $this->frequencyScale;
}
public function setInterval($interval) {
if (!is_int($interval)) {
throw new Exception(
pht(
'RRULE INTERVAL "%s" is invalid: interval must be an integer.',
$interval));
}
if ($interval < 1) {
throw new Exception(
pht(
'RRULE INTERVAL "%s" is invalid: interval must be 1 or more.',
$interval));
}
$this->interval = $interval;
return $this;
}
public function getInterval() {
return $this->interval;
}
public function setBySecond(array $by_second) {
$this->assertByRange('BYSECOND', $by_second, 0, 60);
$this->bySecond = array_fuse($by_second);
return $this;
}
public function getBySecond() {
return $this->bySecond;
}
public function setByMinute(array $by_minute) {
$this->assertByRange('BYMINUTE', $by_minute, 0, 59);
$this->byMinute = array_fuse($by_minute);
return $this;
}
public function getByMinute() {
return $this->byMinute;
}
public function setByHour(array $by_hour) {
$this->assertByRange('BYHOUR', $by_hour, 0, 23);
$this->byHour = array_fuse($by_hour);
return $this;
}
public function getByHour() {
return $this->byHour;
}
public function setByDay(array $by_day) {
$constants = self::getAllWeekdayConstants();
$constants = implode('|', $constants);
$pattern = '/^(?:[+-]?([1-9]\d?))?('.$constants.')\z/';
foreach ($by_day as $value) {
$matches = null;
if (!preg_match($pattern, $value, $matches)) {
throw new Exception(
pht(
'RRULE BYDAY value "%s" is invalid: rule part must be in the '.
'expected form (like "MO", "-3TH", or "+2SU").',
$value));
}
// The maximum allowed value is 53, which corresponds to "the 53rd
// Monday every year" or similar when evaluated against a YEARLY rule.
$maximum = 53;
$magnitude = (int)$matches[1];
if ($magnitude > $maximum) {
throw new Exception(
pht(
'RRULE BYDAY value "%s" has an offset with magnitude "%s", but '.
'the maximum permitted value is "%s".',
$value,
$magnitude,
$maximum));
}
}
$this->byDay = array_fuse($by_day);
return $this;
}
public function getByDay() {
return $this->byDay;
}
public function setByMonthDay(array $by_month_day) {
$this->assertByRange('BYMONTHDAY', $by_month_day, -31, 31, false);
$this->byMonthDay = array_fuse($by_month_day);
return $this;
}
public function getByMonthDay() {
return $this->byMonthDay;
}
public function setByYearDay($by_year_day) {
$this->assertByRange('BYYEARDAY', $by_year_day, -366, 366, false);
$this->byYearDay = array_fuse($by_year_day);
return $this;
}
public function getByYearDay() {
return $this->byYearDay;
}
public function setByMonth(array $by_month) {
$this->assertByRange('BYMONTH', $by_month, 1, 12);
$this->byMonth = array_fuse($by_month);
return $this;
}
public function getByMonth() {
return $this->byMonth;
}
public function setByWeekNumber(array $by_week_number) {
$this->assertByRange('BYWEEKNO', $by_week_number, -53, 53, false);
$this->byWeekNumber = $by_week_number;
return $this;
}
public function getByWeekNumber() {
return $this->byWeekNumber;
}
public function setBySetPosition(array $by_set_position) {
$this->assertByRange('BYSETPOS', $by_set_position, -366, 366, false);
$this->bySetPosition = $by_set_position;
return $this;
}
public function getBySetPosition() {
return $this->bySetPosition;
}
public function setWeekStart($week_start) {
// Make sure this is a valid weekday constant.
self::getWeekdayIndex($week_start);
$this->weekStart = $week_start;
return $this;
}
public function getWeekStart() {
return $this->weekStart;
}
public function resetSource() {
$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();
$this->setSeconds = array();
$this->setMinutes = array();
$this->setHours = array();
$this->setDays = array();
$this->setMonths = array();
$this->setYears = array();
$this->stateSecond = null;
$this->stateMinute = null;
$this->stateHour = null;
$this->stateDay = null;
$this->stateMonth = null;
$this->stateYear = null;
}
public function getNextEvent($cursor) {
$date = $this->getStartDateTime();
$all_day = $date->getIsAllDay();
if ($all_day) {
$this->nextDay();
} else {
$this->nextSecond();
}
$result = id(new PhutilCalendarAbsoluteDateTime())
->setViewerTimezone($this->getViewerTimezone())
->setYear($this->stateYear)
->setMonth($this->stateMonth)
->setDay($this->stateDay);
if ($all_day) {
$result->setIsAllDay(true);
} else {
$result
->setHour($this->stateHour)
->setMinute($this->stateMinute)
->setSecond($this->stateSecond);
}
return $result;
}
protected function nextSecond() {
if ($this->setSeconds) {
$this->stateSecond = array_pop($this->setSeconds);
return;
}
$frequency = $this->getFrequency();
$interval = $this->getInterval();
$is_secondly = ($frequency == self::FREQUENCY_SECONDLY);
$by_second = $this->getBySecond();
$by_setpos = $this->getBySetPosition();
while (!$this->setSeconds) {
$this->nextMinute();
if ($is_secondly || $by_second) {
$seconds = $this->newSecondsSet(
($is_secondly ? $interval : 1),
$by_second);
} else {
$seconds = array(
$this->cursorSecond,
);
}
if ($is_secondly && $by_setpos) {
$seconds = $this->applySetPos($seconds, $by_setpos);
}
$this->setSeconds = array_reverse($seconds);
}
$this->stateSecond = array_pop($this->setSeconds);
}
protected function nextMinute() {
if ($this->setMinutes) {
$this->stateMinute = array_pop($this->setMinutes);
return;
}
$frequency = $this->getFrequency();
$interval = $this->getInterval();
$scale = $this->getFrequencyScale();
$is_minutely = ($frequency === self::FREQUENCY_MINUTELY);
$by_minute = $this->getByMinute();
$by_setpos = $this->getBySetPosition();
while (!$this->setMinutes) {
$this->nextHour();
if ($is_minutely || $by_minute) {
$minutes = $this->newMinutesSet(
($is_minutely ? $interval : 1),
$by_minute);
} else if ($scale < self::SCALE_MINUTELY) {
$minutes = $this->newMinutesSet(
1,
array());
} else {
$minutes = array(
$this->cursorMinute,
);
}
if ($is_minutely && $by_setpos) {
$minutes = $this->applySetPos($minutes, $by_setpos);
}
$this->setMinutes = array_reverse($minutes);
}
$this->stateMinute = array_pop($this->setMinutes);
}
protected function nextHour() {
if ($this->setHours) {
$this->stateHour = array_pop($this->setHours);
return;
}
$frequency = $this->getFrequency();
$interval = $this->getInterval();
$scale = $this->getFrequencyScale();
$is_hourly = ($frequency === self::FREQUENCY_HOURLY);
$by_hour = $this->getByHour();
$by_setpos = $this->getBySetPosition();
while (!$this->setHours) {
$this->nextDay();
if ($is_hourly || $by_hour) {
$hours = $this->newHoursSet(
($is_hourly ? $interval : 1),
$by_hour);
} else if ($scale < self::SCALE_HOURLY) {
$hours = $this->newHoursSet(
1,
array());
} else {
$hours = array(
$this->cursorHour,
);
}
if ($is_hourly && $by_setpos) {
$hours = $this->applySetPos($hours, $by_setpos);
}
$this->setHours = array_reverse($hours);
}
$this->stateHour = array_pop($this->setHours);
}
protected function nextDay() {
if ($this->setDays) {
$this->stateDay = array_pop($this->setDays);
return;
}
$frequency = $this->getFrequency();
$interval = $this->getInterval();
$scale = $this->getFrequencyScale();
$is_daily = ($frequency === self::FREQUENCY_DAILY);
$is_weekly = ($frequency === self::FREQUENCY_WEEKLY);
$by_day = $this->getByDay();
$by_monthday = $this->getByMonthDay();
$by_yearday = $this->getByYearDay();
$by_weekno = $this->getByWeekNumber();
$by_setpos = $this->getBySetPosition();
+ $week_start = $this->getWeekStart();
while (!$this->setDays) {
$this->nextMonth();
if ($is_daily || $by_day || $by_monthday || $by_yearday || $by_weekno) {
$weeks = $this->newDaysSet(
($is_daily ? $interval : null),
($is_weekly ? $interval : null),
$by_day,
$by_monthday,
$by_yearday,
$by_weekno,
- $this->getWeekStart());
+ $week_start);
} else if ($scale < self::SCALE_DAILY) {
$weeks = $this->newDaysSet(
1,
null,
array(),
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.
if ($is_weekly && $by_setpos) {
$weeks = $this->applySetPos($weeks, $by_setpos);
}
// Unpack the weeks into days.
$days = array_mergev($weeks);
// Apply daily BYSETPOS, if one exists.
if ($is_daily && $by_setpos) {
$days = $this->applySetPos($days, $by_setpos);
}
$this->setDays = array_reverse($days);
}
$this->stateDay = array_pop($this->setDays);
}
protected function nextMonth() {
if ($this->setMonths) {
$this->stateMonth = array_pop($this->setMonths);
return;
}
$frequency = $this->getFrequency();
$interval = $this->getInterval();
$scale = $this->getFrequencyScale();
$is_monthly = ($frequency === self::FREQUENCY_MONTHLY);
$by_month = $this->getByMonth();
$by_setpos = $this->getBySetPosition();
while (!$this->setMonths) {
$this->nextYear();
if ($is_monthly || $by_month) {
$months = $this->newMonthsSet(
($is_monthly ? $interval : 1),
$by_month);
} else if ($scale < self::SCALE_MONTHLY) {
$months = $this->newMonthsSet(
1,
array());
} else {
$months = array(
$this->cursorMonth,
);
}
if ($is_monthly && $by_setpos) {
$months = $this->applySetPos($months, $by_setpos);
}
$this->setMonths = array_reverse($months);
}
$this->stateMonth = array_pop($this->setMonths);
}
protected function nextYear() {
+
$this->stateYear = $this->cursorYear;
$frequency = $this->getFrequency();
$is_yearly = ($frequency === self::FREQUENCY_YEARLY);
if ($is_yearly) {
$interval = $this->getInterval();
} else {
$interval = 1;
}
$this->cursorYear = $this->cursorYear + $interval;
}
private function newSecondsSet($interval, $set) {
// TODO: This doesn't account for leap seconds. In theory, it probably
// should, although this shouldn't impact any real events.
$seconds_in_minute = 60;
if ($this->cursorSecond >= $seconds_in_minute) {
$this->cursorSecond -= $seconds_in_minute;
return array();
}
list($cursor, $result) = $this->newIteratorSet(
$this->cursorSecond,
$interval,
$set,
$seconds_in_minute);
$this->cursorSecond = ($cursor - $seconds_in_minute);
return $result;
}
private function newMinutesSet($interval, $set) {
// NOTE: This value is legitimately a constant! Amazing!
$minutes_in_hour = 60;
if ($this->cursorMinute >= $minutes_in_hour) {
$this->cursorMinute -= $minutes_in_hour;
return array();
}
list($cursor, $result) = $this->newIteratorSet(
$this->cursorMinute,
$interval,
$set,
$minutes_in_hour);
$this->cursorMinute = ($cursor - $minutes_in_hour);
return $result;
}
private function newHoursSet($interval, $set) {
// TODO: This doesn't account for hours caused by daylight savings time.
// It probably should, although this seems unlikely to impact any real
// events.
$hours_in_day = 24;
if ($this->cursorHour >= $hours_in_day) {
$this->cursorHour -= $hours_in_day;
return array();
}
list($cursor, $result) = $this->newIteratorSet(
$this->cursorHour,
$interval,
$set,
$hours_in_day);
$this->cursorHour = ($cursor - $hours_in_day);
return $result;
}
private function newDaysSet(
$interval_day,
$interval_week,
$by_day,
$by_monthday,
$by_yearday,
$by_weekno,
$week_start) {
$year_map = $this->getYearMap($this->cursorYear, $week_start);
$selection = array();
if ($interval_week) {
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) {
$selection[] = $year_map['info'][$key];
}
$last = last($selection);
- if ($last['month'] > $this->cursorMonth) {
+ if ($last['month'] > $this->stateMonth) {
break;
}
$this->cursorWeek += $interval_week;
}
} else {
$calendar = $year_map['calendar'];
- $month_idx = $this->cursorMonth;
+ $month_idx = $this->stateMonth;
if (!$interval_day) {
$interval_day = 1;
}
while (true) {
$month_days = $year_map['monthDays'][$month_idx];
if ($this->cursorDay > $month_days) {
$this->cursorDay -= $month_days;
break;
}
$day_idx = $this->cursorDay;
$key = $calendar[$month_idx][$day_idx]['key'];
$selection[] = $year_map['info'][$key];
$this->cursorDay += $interval_day;
}
}
$weeks = array();
foreach ($selection as $key => $info) {
- if ($info['month'] != $this->cursorMonth) {
+ if ($info['month'] != $this->stateMonth) {
continue;
}
if ($by_day) {
// TODO: This only handles "BYDAY=MO,TU". It does not yet properly
// handle "BYDAY=+1FR" (e.g., the first Friday in the month).
if (empty($by_day[$info['weekday']])) {
continue;
}
}
if ($by_monthday) {
if (empty($by_monthday[$info['monthday']]) &&
empty($by_monthday[$info['-monthday']])) {
continue;
}
}
if ($by_yearday) {
if (empty($by_monthday[$info['yearday']]) &&
empty($by_monthday[$info['-yearday']])) {
continue;
}
}
if ($by_weekno) {
if (empty($by_weekno[$info['week']]) &&
empty($by_weekno[$info['-week']])) {
continue;
}
}
$weeks[$info['week']][] = $info['monthday'];
}
return array_values($weeks);
}
private function newMonthsSet($interval, $set) {
// NOTE: This value is also a real constant! Wow!
$months_in_year = 12;
if ($this->cursorMonth > $months_in_year) {
$this->cursorMonth - $months_in_year;
return array();
}
list($cursor, $result) = $this->newIteratorSet(
$this->cursorMonth,
$interval,
$set,
$months_in_year + 1);
$this->cursorMonth = ($cursor - $months_in_year);
return $result;
}
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);
$maps[$key] = $map;
return $maps[$key];
}
private static function newYearMap($year, $weekday_index) {
$is_leap = (($year % 4 === 0) && ($year % 100 !== 0)) ||
($year % 400 === 0);
// There may be some clever way to figure out which day of the week a given
// year starts on and avoid the cost of a DateTime construction, but I
// wasn't able to turn it up and we only need to do this once per year.
$datetime = new DateTime("{$year}-01-01", new DateTimeZone('UTC'));
$weekday = $datetime->format('w');
if ($is_leap) {
$max_day = 366;
} else {
$max_day = 365;
}
$month_days = array(
1 => 31,
2 => $is_leap ? 29 : 28,
3 => 31,
4 => 30,
5 => 31,
6 => 30,
7 => 31,
8 => 31,
9 => 30,
10 => 31,
11 => 30,
12 => 31,
);
// Per the spec, the first week of the year must contain at least four
// days. If the week starts on a Monday but the year starts on a Saturday,
// the first couple of days don't count as a week. In this case, the first
// week will begin on January 3.
$first_week_size = 0;
$first_weekday = $weekday;
for ($year_day = 1; $year_day <= $max_day; $year_day++) {
$first_weekday = ($first_weekday + 1) % 7;
if ($first_weekday === $weekday_index) {
break;
}
$first_week_size++;
}
if ($first_week_size >= 4) {
$week_number = 1;
} else {
$week_number = 0;
}
$info_map = array();
$calendar_map = array();
$week_map = array();
$yearday_map = array();
$weekday_map = self::getWeekdayIndexMap();
$weekday_map = array_flip($weekday_map);
$month_number = 1;
$month_day = 1;
for ($year_day = 1; $year_day <= $max_day; $year_day++) {
$key = "{$month_number}M{$month_day}D";
$info = array(
'key' => $key,
'month' => $month_number,
'monthday' => $month_day,
'-monthday' => -$month_days[$month_number] + $month_day - 1,
'yearday' => $year_day,
'-yearday' => -$max_day + $year_day - 1,
'week' => $week_number,
'weekday' => $weekday_map[$weekday],
);
$info_map[$key] = $info;
$calendar_map[$month_number][$month_day] = $info;
$week_map[$week_number][] = $info;
$yearday_map[$year_day] = $info;
$weekday = ($weekday + 1) % 7;
if ($weekday === $weekday_index) {
$week_number++;
}
$month_day = ($month_day + 1);
if ($month_day > $month_days[$month_number]) {
$month_day = 1;
$month_number++;
}
}
// 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;
} else {
$week_value = -$week_number + $week - 1;
}
$info['-week'] = $week_value;
}
return array(
'info' => $info_map,
'calendar' => $calendar_map,
'weeks' => $week_map,
'yeardays' => $yearday_map,
'weekCount' => $week_number,
'dayCount' => $max_day,
'monthDays' => $month_days,
);
}
private function newIteratorSet($cursor, $interval, $set, $limit) {
if ($interval < 1) {
throw new Exception(
pht(
'Invalid iteration interval ("%d"), must be at least 1.',
$interval));
}
$result = array();
$seen = array();
$ii = $cursor;
while (true) {
if (!$set || isset($set[$ii])) {
$result[] = $ii;
}
$ii = ($ii + $interval);
if ($ii >= $limit) {
break;
}
}
sort($result);
$result = array_values($result);
return array($ii, $result);
}
private function applySetPos(array $values, array $setpos) {
$select = array();
$count = count($values);
foreach ($setpos as $pos) {
if ($pos > 0 && $pos <= $count) {
$select[] = ($pos - 1);
} else if ($pos < 0 && $pos >= -$count) {
$select[] = ($count + $pos);
}
}
sort($select);
return array_select_keys($values, $select);
}
private function assertByRange(
$source,
array $values,
$min,
$max,
$allow_zero = true) {
foreach ($values as $value) {
if (!is_int($value)) {
throw new Exception(
pht(
'Value "%s" in RRULE "%s" parameter is invalid: values must be '.
'integers.',
$value,
$source));
}
if ($value < $min || $value > $max) {
throw new Exception(
pht(
'Value "%s" in RRULE "%s" parameter is invalid: it must be '.
'between %s and %s.',
$value,
$source,
$min,
$max));
}
if (!$value && !$allow_zero) {
throw new Exception(
pht(
'Value "%s" in RRULE "%s" parameter is invalid: it must not '.
'be zero.',
$value,
$source));
}
}
}
}
diff --git a/src/parser/calendar/data/__tests__/PhutilCalendarRecurrenceRuleTestCase.php b/src/parser/calendar/data/__tests__/PhutilCalendarRecurrenceRuleTestCase.php
index f6e234b..63444c4 100644
--- a/src/parser/calendar/data/__tests__/PhutilCalendarRecurrenceRuleTestCase.php
+++ b/src/parser/calendar/data/__tests__/PhutilCalendarRecurrenceRuleTestCase.php
@@ -1,105 +1,172 @@
<?php
final class PhutilCalendarRecurrenceRuleTestCase extends PhutilTestCase {
public function testSimpleRecurrenceRules() {
$start = PhutilCalendarAbsoluteDateTime::newFromISO8601('20160101T120000Z');
$rrule = id(new PhutilCalendarRecurrenceRule())
->setStartDateTime($start)
->setFrequency(PhutilCalendarRecurrenceRule::FREQUENCY_DAILY);
$set = id(new PhutilCalendarRecurrenceSet())
->addSource($rrule);
$result = $set->getEventsBetween(null, null, 3);
$expect = array(
PhutilCalendarAbsoluteDateTime::newFromISO8601('20160101T120000Z'),
PhutilCalendarAbsoluteDateTime::newFromISO8601('20160102T120000Z'),
PhutilCalendarAbsoluteDateTime::newFromISO8601('20160103T120000Z'),
);
$this->assertEqual(
mpull($expect, 'getISO8601'),
mpull($result, 'getISO8601'),
pht('Simple daily event.'));
$rrule = id(new PhutilCalendarRecurrenceRule())
->setStartDateTime($start)
->setFrequency(PhutilCalendarRecurrenceRule::FREQUENCY_HOURLY)
->setByHour(array(12, 13));
$set = id(new PhutilCalendarRecurrenceSet())
->addSource($rrule);
$result = $set->getEventsBetween(null, null, 5);
$expect = array(
PhutilCalendarAbsoluteDateTime::newFromISO8601('20160101T120000Z'),
PhutilCalendarAbsoluteDateTime::newFromISO8601('20160101T130000Z'),
PhutilCalendarAbsoluteDateTime::newFromISO8601('20160102T120000Z'),
PhutilCalendarAbsoluteDateTime::newFromISO8601('20160102T130000Z'),
PhutilCalendarAbsoluteDateTime::newFromISO8601('20160103T120000Z'),
);
$this->assertEqual(
mpull($expect, 'getISO8601'),
mpull($result, 'getISO8601'),
pht('Hourly event with BYHOUR.'));
$rrule = id(new PhutilCalendarRecurrenceRule())
->setStartDateTime($start)
->setFrequency(PhutilCalendarRecurrenceRule::FREQUENCY_YEARLY);
$set = id(new PhutilCalendarRecurrenceSet())
->addSource($rrule);
$result = $set->getEventsBetween(null, null, 2);
$expect = array(
PhutilCalendarAbsoluteDateTime::newFromISO8601('20160101T120000Z'),
PhutilCalendarAbsoluteDateTime::newFromISO8601('20170101T120000Z'),
);
$this->assertEqual(
mpull($expect, 'getISO8601'),
mpull($result, 'getISO8601'),
pht('Yearly event.'));
// This is an efficiency test for bizarre rules: it defines a secondly
// event which only occurs one a year, and generates 3 instances of it.
// This implementation should be fast enough that this test doesn't take
// a significant amount of time.
$rrule = id(new PhutilCalendarRecurrenceRule())
->setStartDateTime($start)
->setFrequency(PhutilCalendarRecurrenceRule::FREQUENCY_SECONDLY)
->setByMonth(array(1))
->setByMonthDay(array(1))
->setByHour(array(12))
->setByMinute(array(0))
->setBySecond(array(0));
$set = id(new PhutilCalendarRecurrenceSet())
->addSource($rrule);
$result = $set->getEventsBetween(null, null, 3);
$expect = array(
PhutilCalendarAbsoluteDateTime::newFromISO8601('20160101T120000Z'),
PhutilCalendarAbsoluteDateTime::newFromISO8601('20170101T120000Z'),
PhutilCalendarAbsoluteDateTime::newFromISO8601('20180101T120000Z'),
);
$this->assertEqual(
mpull($expect, 'getISO8601'),
mpull($result, 'getISO8601'),
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'));
+ }
+ }
+
+
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Tue, Jan 20, 1:09 AM (6 h, 13 m)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
3b/47/6d7cdc8415a7ffb19cb226f582b6
Attached To
rPHUTIL libphutil
Event Timeline
Log In to Comment