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 @@ -210,7 +210,7 @@ $constants = implode('|', $constants); $pattern = '/^(?:[+-]?([1-9]\d?))?('.$constants.')\z/'; - foreach ($by_day as $value) { + foreach ($by_day as $key => $value) { $matches = null; if (!preg_match($pattern, $value, $matches)) { throw new Exception( @@ -234,6 +234,9 @@ $magnitude, $maximum)); } + + // Normalize "+3FR" into "3FR". + $by_day[$key] = ltrim($value, '+'); } $this->byDay = array_fuse($by_day); @@ -589,9 +592,10 @@ $by_monthday = $this->getByMonthDay(); // Likewise, we need to generate all months if we have BYYEARDAY or - // BYWEEKNO. + // BYWEEKNO or BYDAY. $by_yearday = $this->getByYearDay(); $by_weekno = $this->getByWeekNumber(); + $by_day = $this->getByDay(); while (!$this->setMonths) { $this->nextYear(); @@ -601,6 +605,7 @@ || $by_monthday || $by_yearday || $by_weekno + || $by_day || ($scale < self::SCALE_MONTHLY); if ($is_dynamic) { @@ -776,6 +781,19 @@ } } + $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) { + if ($this->getByMonth()) { + $is_yearly = false; + $is_monthly = true; + } + } + $weeks = array(); foreach ($selection as $key => $info) { if ($info['month'] != $this->stateMonth) { @@ -783,11 +801,20 @@ } 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 ($is_yearly) { + if (empty($by_day[$info['weekday.yearly']]) && + empty($by_day[$info['-weekday.yearly']])) { + continue; + } + } else if ($is_monthly) { + if (empty($by_day[$info['weekday.monthly']]) && + empty($by_day[$info['-weekday.monthly']])) { + continue; + } + } else { + continue; + } } } @@ -910,11 +937,25 @@ $weekday_map = self::getWeekdayIndexMap(); $weekday_map = array_flip($weekday_map); + $yearly_counts = array(); + $monthly_counts = array(); + $month_number = 1; $month_day = 1; for ($year_day = 1; $year_day <= $max_day; $year_day++) { $key = "{$month_number}M{$month_day}D"; + $short_day = $weekday_map[$weekday]; + if (empty($yearly_counts[$short_day])) { + $yearly_counts[$short_day] = 0; + } + $yearly_counts[$short_day]++; + + if (empty($monthly_counts[$month_number][$short_day])) { + $monthly_counts[$month_number][$short_day] = 0; + } + $monthly_counts[$month_number][$short_day]++; + $info = array( 'year' => $year, 'key' => $key, @@ -924,7 +965,9 @@ 'yearday' => $year_day, '-yearday' => -$max_day + $year_day - 1, 'week' => $week_number, - 'weekday' => $weekday_map[$weekday], + 'weekday' => $short_day, + 'weekday.yearly' => $yearly_counts[$short_day], + 'weekday.monthly' => $monthly_counts[$month_number][$short_day], ); $info_map[$key] = $info; @@ -984,6 +1027,22 @@ $info['-week'] = -$week_number + $week - 1; } + // Do all the arithmetic to figure out if this is the -19th Thursday + // in the year and such. + $month_number = $info['month']; + $short_day = $info['weekday']; + $monthly_count = $monthly_counts[$month_number][$short_day]; + $monthly_index = $info['weekday.monthly']; + $info['-weekday.monthly'] = -$monthly_count + $monthly_index - 1; + $info['-weekday.monthly'] .= $short_day; + $info['weekday.monthly'] .= $short_day; + + $yearly_count = $yearly_counts[$short_day]; + $yearly_index = $info['weekday.yearly']; + $info['-weekday.yearly'] = -$yearly_count + $yearly_index - 1; + $info['-weekday.yearly'] .= $short_day; + $info['weekday.yearly'] .= $short_day; + $info_map[$key] = $info; } 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 @@ -209,6 +209,54 @@ ); $tests[] = array( + 'BYDAY' => array('1TU', '-1TH'), + ); + $expect[] = array( + '19971225', + '19980106', + '19981231', + ); + + // Same test as above, just making sure the optional "+" syntax works. + $tests[] = array( + 'BYDAY' => array('+1TU', '-1TH'), + ); + $expect[] = array( + '19971225', + '19980106', + '19981231', + ); + + $tests[] = array( + 'BYDAY' => array('3TU', '-3TH'), + ); + $expect[] = array( + '19971211', + '19980120', + '19981217', + ); + + $tests[] = array( + 'BYMONTH' => array(1, 3), + 'BYDAY' => array('1TU', '-1TH'), + ); + $expect[] = array( + '19980106', + '19980129', + '19980303', + ); + + $tests[] = array( + 'BYMONTH' => array(1, 3), + 'BYDAY' => array('3TU', '-3TH'), + ); + $expect[] = array( + '19980115', + '19980120', + '19980312', + ); + + $tests[] = array( 'BYYEARDAY' => array(1, 100, 200, 365), 'COUNT' => 4, );