You are here

class DateRRuleCalc in Date 8

Hierarchy

Expanded class hierarchy of DateRRuleCalc

5 files declare their use of DateRRuleCalc
DateRRuleCalcTest.php in date_repeat/lib/Drupal/date_repeat/Tests/DateRRuleCalcTest.php
Test Date Repeat calculations.
date_repeat.module in date_repeat/date_repeat.module
This module creates a form element that allows users to select repeat rules for a date, and reworks the result into an iCal RRULE string that can be stored in the database.
date_repeat_field.devel_generate.inc in date_repeat_field/date_repeat_field.devel_generate.inc
date_repeat_field.module in date_repeat_field/date_repeat_field.module
Creates the option of Repeating date fields and manages Date fields that use the Date Repeat API.
date_repeat_form.inc in date_repeat/date_repeat_form.inc
Code to add a date repeat selection form to a date field and create an iCal RRULE from the chosen selections.

File

date_repeat/lib/Drupal/date_repeat/DateRRuleCalc.php, line 28
Code to compute the dates that match an iCal RRULE.

Namespace

Drupal\date_repeat
View source
class DateRRuleCalc {

  /**
   * Map the abbreviation used in iCal day names
   * to the day name usable by DateTime and DateInterval.
   */
  public static $day_names = array(
    'SU' => 'Sunday',
    'MO' => 'Monday',
    'TU' => 'Tuesday',
    'WE' => 'Wednesday',
    'TH' => 'Thursday',
    'FR' => 'Friday',
    'SA' => 'Saturday',
  );

  /**
   * Map some common iCal direction values to the text that
   * works more reliably in DateTime::modify().
   */
  public static $date_order = array(
    '+1' => 'First',
    '+2' => 'Second',
    '+3' => 'Third',
    '+4' => 'Fourth',
    '+5' => 'Fifth',
    '-1' => 'Last',
    '-2' => '-2',
    '-3' => '-3',
    '-4' => '-4',
    '-5' => '-5',
  );

  /**
   * The format to use when creating and comparing dates.
   */
  public $default_format = 'Y-m-d H:i:s';

  /**
   * The name of the timezone to use in these computations.
   */
  public $timezone_name = 'UTC';

  /**
   * The time that will be used for all the created dates.
   */
  public $time_string = '00:00:00';

  /**
   * An array of RRULE parts, as parsed by the DateiCalParse.
   */
  public $rrule = array();

  /**
   * A date object for the start of the series.
   */
  public $start_date = NULL;

  /**
   * A date object for the start of the series.
   */
  public $end_date = NULL;

  /**
   * An optional limit on the number of results, as set in the RRULE.
   */
  public $max_count = NULL;

  /**
   * The array of days that match the criteria.
   */
  public $result = array();

  /**
   * The current day, as we iterate through the RRULE.
   */
  public $current_day = NULL;

  /**
   * An array of dates that should not be selected.
   */
  public $exceptions = array();

  /**
   * An array of dates that should be added.
   */
  public $additions = array();

  /**
   * The start day of the week.
   */
  public $week_start_day = 'MO';

  /**
   * A DateInterval representing the amount of time to jump
   * after each iteration of the calculation.
   */
  public $jump = NULL;

  /**
   * The maximum number of times to cycle through this code.
   * Needed to avoid endless loops that check for a COUNT
   * without finding any results. This checks the number of
   * times that $this->is_finished() gets called.
   */
  public $max_cycles = 10000000;

  /**
   * Compute dates that match the requested rule, within a specified
   * date range.
   *
   * @param string $rrule
   *   A string RRULE, in the standard iCal format.
   * @param object $start
   *   A date object to start the series.
   * @param object $end
   *   A date object to end the series, if not ended earlier by UNTIL
   *   or COUNT. Requred unless a COUNT is provided.
   * @param array $this->exceptions
   *   Optional array of exception dates, each in the standard ISO format
   *   of YYYY-MM-DD.
   * @param array $additions
   *   Optional array of additional dates, each in the standard ISO format
   *   of YYYY-MM-DD.
   */
  function __construct($rrule, $start, $end = NULL, $exceptions = array(), $additions = array()) {

    // Get the parsed array of rule values.
    $this->rrule = DateiCalParse::parse_rrule($rrule);

    // Create a date object for the start and end dates, if valid.
    $this->start_date = $start;
    $this->end_date = $end;
    $this->timezone_name = $this->start_date
      ->getTimezone()
      ->getName();

    // Make sure we have something we can work with.
    if (!$this
      ->isValid()) {
      return FALSE;
    }

    // If the rule has an UNTIL, see if that is earlier than the end date.
    if (!empty($this->rrule['UNTIL'])) {
      $until_date = DateiCalParse::ical_date($this->rrule['UNTIL'], $this->timezone_name);
      if (empty($this->end_date) || $until_date < $this->end_date) {
        $this->end_date = $until_date;
      }
    }

    // Versions of PHP greater than PHP 5.3.5 require that we set an
    // explicit time when using date_modify() or the time may not match
    // the original value. Adding this modifier gives us the same
    // results in both older and newer versions of PHP.
    $this->time_string = ' ' . $this->start_date
      ->format('g:ia');
    $this->max_count = isset($this->rrule['COUNT']) ? $this->rrule['COUNT'] : NULL;
    $this->exceptions = $exceptions;
    $this->additions = $additions;
  }

  /**
   * Basic validation for an RRULE we can do something with.
   */
  protected function isValid() {

    // We alwqys need a start date.
    if (!$this->start_date instanceof \DateTime) {
      return FALSE;
    }

    // The only valid option for an empty end date is when we have a count.
    if (!$this->end_date instanceof \DateTime && empty($this->rrule['COUNT'])) {
      return FALSE;
    }
    return TRUE;
  }
  public function compute() {

    // Make sure we have something we can work with.
    if (!$this
      ->isValid()) {
      return FALSE;
    }
    if (empty($this->rrule['FREQ'])) {
      $this->rrule['FREQ'] = 'DAILY';
    }

    // These default values indicate there is no RRULE here.
    if ($this->rrule['FREQ'] == 'NONE' || isset($this->rrule['INTERVAL']) && $this->rrule['INTERVAL'] == 0) {
      return array();
    }

    // Get an integer value for the interval, if none given, '1'
    // is implied.
    if (empty($this->rrule['INTERVAL'])) {
      $this->rrule['INTERVAL'] = 1;
    }
    $interval = max(1, $this->rrule['INTERVAL']);

    // Make sure DAILY frequency isn't used in places it won't work;
    if (!empty($this->rrule['BYMONTHDAY']) && !in_array($this->rrule['FREQ'], array(
      'MONTHLY',
      'YEARLY',
    ))) {
      $this->rrule['FREQ'] = 'MONTHLY';
    }
    elseif (!empty($this->rrule['BYDAY']) && !in_array($this->rrule['FREQ'], array(
      'MONTHLY',
      'WEEKLY',
      'YEARLY',
    ))) {
      $this->rrule['FREQ'] = 'WEEKLY';
    }

    // Find the time period to jump forward between dates.
    switch ($this->rrule['FREQ']) {
      case 'DAILY':
        $jump_interval = 'P' . $interval . 'D';
        break;
      case 'WEEKLY':
        $jump_interval = 'P' . $interval . 'W';
        break;
      case 'MONTHLY':
        $jump_interval = 'P' . $interval . 'M';
        break;
      case 'YEARLY':
        $jump_interval = 'P' . $interval . 'Y';
        break;
    }
    $this->jump = new \DateInterval($jump_interval);

    // Make sure the rrule array has all the values we expect.
    $this
      ->complete_rrule();

    // The start date always goes into the results, whether or not
    // it meets the rules. RFC 2445 includes examples where the start
    // date DOES NOT meet the rules, but the expected results always
    // include the start date.
    $this->result[] = date_format($this->start_date, $this->default_format);

    // BYMONTHDAY will look for specific days of the month in one or
    // more months. This process is only valid when frequency is
    // monthly or yearly.
    if (!empty($this->rrule['BYMONTHDAY'])) {
      $this
        ->get_bymonthday_results();
    }
    elseif (empty($this->rrule['BYDAY'])) {
      $this
        ->get_other_results();
    }
    else {
      $this
        ->get_byday_results();
    }

    // Add additional dates, if any.
    foreach ($this->additions as $addition) {
      $date = new DrupalDateTime($addition . ' ' . $this->time_string, $this->timezone_name);
      $this->result[] = date_format($date, $this->default_format);
    }

    // Sort and return the result.
    sort($this->result);
    return $this->result;
  }

  /**
   * Processing other than BYDAY or BYMONTHDAY.
   *
   * This is the simple fallback case, not looking for any specific day,
   * just repeating the start date. The complete_rrule() code ensures this
   * will only test TRUE for a DAILY or less frequency (like HOURLY).
   */
  protected function get_other_results() {
    $this->current_day = clone $this->start_date;
    $finished = FALSE;
    $months = !empty($this->rrule['BYMONTH']) ? $this->rrule['BYMONTH'] : array();
    while (!$finished) {
      $this
        ->add_current_day();
      $finished = $this
        ->is_finished();
      $this->current_day
        ->add($this->jump);
    }
  }

  /**
   * Processing for BYMONTHDAY values.
   *
   * BYMONTHDAY will look for specific days of the month in one or more
   * months. This process is only valid when frequency is monthly or yearly.
   * BYMONTHDAY values will look like '11' (the 11th day) or '-1'
   * (the last day) or '10,11,12' (the 10th, 11th, and 12th days).
   */
  protected function get_bymonthday_results() {
    $finished = FALSE;
    $time = $this->time_string;
    $this->current_day = clone $this->start_date;
    $month_days = array();

    // Deconstruct the day in case it has a negative modifier.
    foreach ($this->rrule['BYMONTHDAY'] as $day) {
      preg_match("@(-)?([0-9]{1,2})@", $day, $regs);
      if (!empty($regs[2])) {

        // Convert parameters into count, and direction.
        $month_days[$day] = array(
          'direction' => !empty($regs[1]) ? $regs[1] : '+',
          'count' => $regs[2],
        );
      }
    }
    while (!$finished) {
      $year_finished = FALSE;
      while (!$year_finished) {

        // Check each requested day in the month.
        foreach ($this->rrule['BYMONTHDAY'] as $monthday) {
          $day = $month_days[$monthday];
          if ($this
            ->set_month_day(NULL, $day['count'], $day['direction'])) {
            $this
              ->add_current_day();
          }
          if ($finished = $this
            ->is_finished()) {
            $year_finished = TRUE;
          }
        }
        switch ($this->rrule['FREQ']) {
          case 'MONTHLY':

            // If it's monthly, keep looping through months.
            if ($finished = $this
              ->is_finished()) {
              $year_finished = TRUE;
            }

            // Back up to first of month and jump.
            $this->current_day
              ->modify("first day of this month {$time}");
            $this->current_day
              ->add($this->jump);
            break;
          case 'YEARLY':

            // If it's yearly, break out of the loop at the
            // end of every year.
            if ($this->current_day
              ->format('n') == 12) {
              $year_finished = TRUE;
            }
            else {

              // Jump to first day of next month.
              $this->current_day
                ->modify("first day of next month {$time}");
            }
            break;
        }
      }
      if ($this->rrule['FREQ'] == 'YEARLY') {

        // Back up to first of year and jump to next year.
        $this->current_day
          ->modify("this year January 1");
        $this->current_day
          ->add($this->jump);
      }
      $finished = $this
        ->is_finished();
    }
  }

  /**
   * Processing for BYDAY values.
   *
   * More complex searches for day names and criteria like '-1SU'
   * or '2TU,2TH', require that we interate through the whole time
   * period checking each day selected in BYDAY.
   */
  protected function get_byday_results() {

    // Create helper array to pull day names out of iCal day strings.
    $day_names = self::$day_names;
    $this->days_of_week = array_keys($day_names);

    // Parse out information about the BYDAYs and separate them
    // depending on whether they have directional parameters
    // like -1SU or 2TH.
    $week_days = array();

    // Find the right first day of the week to use, iCal rules say
    // Monday should be used if none is specified.
    $week_start_rule = !empty($this->rrule['WKST']) ? trim($this->rrule['WKST']) : 'MO';
    $this->week_start_day = $day_names[$week_start_rule];

    // Make sure the week days array is sorted into week order,
    // we use the $ordered_keys to get the right values into the key
    // and force the array to that order. Needed later when we
    // iterate through each week looking for days so we don't
    // jump to the next week when we hit a day out of order.
    $ordered = date_repeat_days_ordered($week_start_rule);
    $ordered_keys = array_flip($ordered);
    foreach ($this->rrule['BYDAY'] as $day) {
      preg_match("@(-)?([0-9]+)?([SU|MO|TU|WE|TH|FR|SA]{2})@", trim($day), $regs);
      if (!empty($regs[2])) {

        // Convert parameters into full day name, count, and direction.
        $relative_days[] = array(
          'day' => $day_names[$regs[3]],
          'direction' => !empty($regs[1]) ? $regs[1] : '+',
          'direction_count' => $regs[2],
        );
      }
      else {
        $week_days[$ordered_keys[$regs[3]]] = $day_names[$regs[3]];
      }
    }
    ksort($week_days);

    // Get BYDAYs with parameters like -1SU (last Sun) or
    // 2TH (second Thur).
    if (!empty($relative_days) && in_array($this->rrule['FREQ'], array(
      'MONTHLY',
      'YEARLY',
    ))) {
      $this
        ->get_relative_bydays($relative_days);
    }

    // Get BYDAYs without parameters,like TU,TH (every
    // Tues and Thur).
    if (!empty($week_days) && in_array($this->rrule['FREQ'], array(
      'MONTHLY',
      'WEEKLY',
      'YEARLY',
    ))) {
      $this
        ->get_absolute_bydays($week_days);
    }
  }

  /**
   * Get results for relative BYDAY values.
   *
   * BYDAYs with parameters like -1SU (last Sun) or
   * 2TH (second Thur) need to be processed one month or
   * year at a time.
   */
  protected function get_relative_bydays($relative_days) {
    $finished = FALSE;
    $this->current_day = clone $this->start_date;
    while (!$finished) {
      foreach ($relative_days as $day) {

        // Find the BYDAY date in the current period.
        switch ($this->rrule['FREQ']) {
          case 'MONTHLY':
            if ($this
              ->set_month_day($day['day'], $day['direction_count'], $day['direction'])) {
              $this
                ->add_current_day();
            }
            break;
          case 'YEARLY':
            if ($this
              ->set_year_day($day['day'], $day['direction_count'], $day['direction'])) {
              $this
                ->add_current_day();
            }
            break;
        }
      }
      $finished = $this
        ->is_finished();

      // Reset to beginning of period before jumping to next period.
      // Needed especially when working with values like 'last
      // Saturday' to be sure we don't skip months like February.
      $year = $this->current_day
        ->format('Y');
      $month = $this->current_day
        ->format('n');
      switch ($this->rrule['FREQ']) {
        case 'MONTHLY':
          date_date_set($this->current_day, $year, $month, 1);
          break;
        case 'YEARLY':
          date_date_set($this->current_day, $year, 1, 1);
          break;
      }

      // Jump to the next period.
      $this->current_day
        ->add($this->jump);
    }
  }

  /**
   * Get values for absolute BYDAYs.
   *
   * For BYDAYs without parameters,like TU,TH (every Tues and Thur),
   * we look for every one of those days during the frequency period.
   * Iterate through periods of a WEEK, MONTH, or YEAR, checking for
   * the days of the week that match our criteria for each week in the
   * period, then jumping ahead to the next week, month, or year,
   * an INTERVAL at a time.
   */
  protected function get_absolute_bydays($week_days) {
    $finished = FALSE;
    $this->current_day = clone $this->start_date;
    $format = $this->rrule['FREQ'] == 'YEARLY' ? 'Y' : 'n';
    $current_period = $this->current_day
      ->format($format);

    // Back up to the beginning of the week in case we are somewhere
    // in the middle of the possible week days, needed so we don't
    // prematurely jump to the next week. The add_dates() function
    // will keep dates outside the range from getting added.
    if ($this->current_day
      ->format('l') != $this->week_start_day) {
      date_modify($this->current_day, 'last ' . $this->week_start_day . $this->time_string);
    }
    while (!$finished) {
      $period_finished = FALSE;
      while (!$period_finished) {
        $moved = FALSE;
        foreach ($week_days as $delta => $day) {

          // Find the next occurence of each day in this week, only
          // add it if we are still in the current month or year. The
          // add_current_date() function is insufficient to test whether
          // to include this date if we are using a rule like 'every
          // other month', so we must explicitly test it here.
          // If we're already on the right day, don't jump or we
          // will prematurely move into the next week.
          if ($this->current_day
            ->format('l') != $day) {
            date_modify($this->current_day, '+1 ' . $day . $this->time_string);
            $moved = TRUE;
          }
          if ($this->rrule['FREQ'] == 'WEEKLY' || $this->current_day
            ->format($format) == $current_period) {
            $this
              ->add_current_day();
          }
        }
        $finished = $this
          ->is_finished();

        // Make sure we don't get stuck in endless loop if the current
        // day never got changed above.
        if (!$moved) {
          date_modify($this->current_day, '+1 day' . $this->time_string);
        }

        // If this is a WEEKLY frequency, stop after each week,
        // otherwise, stop when we've moved outside the current period.
        // Jump to the end of the week, then test the period.
        if ($finished || $this->rrule['FREQ'] == 'WEEKLY') {
          $period_finished = TRUE;
        }
        elseif ($this->rrule['FREQ'] != 'WEEKLY' && $this->current_day
          ->format($format) != $current_period) {
          $period_finished = TRUE;
        }
      }
      if ($finished) {
        continue;
      }

      // We'll be at the end of a week, month, or year when
      // we get to this point in the code.
      // Go back to the beginning of this period before we jump, to
      // ensure we jump to the first day of the next period.
      switch ($this->rrule['FREQ']) {
        case 'WEEKLY':
          date_modify($this->current_day, '+1 ' . $this->week_start_day . $this->time_string);
          date_modify($this->current_day, '-1 week' . $this->time_string);
          break;
        case 'MONTHLY':
          date_modify($this->current_day, '-' . ($this->current_day
            ->format('j') - 1) . ' days' . $this->time_string);
          date_modify($this->current_day, '-1 month' . $this->time_string);
          break;
        case 'YEARLY':
          date_modify($this->current_day, '-' . $this->current_day
            ->format('z') . ' days' . $this->time_string);
          date_modify($this->current_day, '-1 year' . $this->time_string);
          break;
      }

      // Jump ahead to the next period to be evaluated.
      $this->current_day
        ->add($this->jump);
      $current_period = $this->current_day
        ->format($format);
      $finished = $this
        ->is_finished();
    }
  }

  /**
   * See if the RRULE needs some imputed values added to it.
   */
  protected function complete_rrule() {

    // If this is not a valid value, do nothing;
    if (empty($this->rrule) || empty($this->rrule['FREQ'])) {
      return FALSE;
    }

    // RFC 2445 says if no day or monthday is specified when creating repeats
    // for weeks, months, or years, impute the value from the start date.
    if (empty($this->rrule['BYDAY']) && $this->rrule['FREQ'] == 'WEEKLY') {
      $this->rrule['BYDAY'] = array(
        date_repeat_dow2day($this->start_date
          ->format('w')),
      );
    }
    elseif (empty($this->rrule['BYDAY']) && empty($this->rrule['BYMONTHDAY']) && $this->rrule['FREQ'] == 'MONTHLY') {
      $this->rrule['BYMONTHDAY'] = array(
        $this->start_date
          ->format('j'),
      );
    }
    elseif (empty($this->rrule['BYDAY']) && empty($this->rrule['BYMONTHDAY']) && empty($this->rrule['BYYEARDAY']) && $this->rrule['FREQ'] == 'YEARLY') {
      $this->rrule['BYMONTHDAY'] = array(
        $this->start_date
          ->format('j'),
      );
      if (empty($this->rrule['BYMONTH'])) {
        $this->rrule['BYMONTH'] = array(
          $this->start_date
            ->format('n'),
        );
      }
    }
    elseif (!empty($this->rrule['BYDAY']) && !in_array($this->rrule['FREQ'], array(
      'MONTHLY',
      'YEARLY',
    ))) {
      foreach ($this->rrule['BYDAY'] as $delta => $BYDAY) {
        $this->rrule['BYDAY'][$delta] = substr($BYDAY, -2);
      }
    }
  }

  /**
   * Helper function to add current date to the $dates array.
   *
   * Check that the date to be added is between the start and end date
   * and that it is not in the $this->exceptions, nor already in the
   * $this->result array, and that it meets other criteria in the RRULE.
   */
  protected function add_current_day() {
    if (!empty($this->max_count) && sizeof($this->result) >= $this->max_count) {
      return FALSE;
    }
    if (!empty($this->end_date) && $this->current_day > $this->end_date) {
      return FALSE;
    }
    if ($this->current_day < $this->start_date) {
      return FALSE;
    }
    if (in_array($this->current_day
      ->format('Y-m-d'), $this->exceptions)) {
      return FALSE;
    }
    if (!empty($this->rrule['BYDAY'])) {
      $BYDAYS = $this->rrule['BYDAY'];
      foreach ($BYDAYS as $delta => $BYDAY) {
        $BYDAYS[$delta] = substr($BYDAY, -2);
      }
      if (!in_array(date_repeat_dow2day($this->current_day
        ->format('w')), $BYDAYS)) {
        return FALSE;
      }
    }
    if (!empty($this->rrule['BYYEAR']) && !in_array($this->current_day
      ->format('Y'), $this->rrule['BYYEAR'])) {
      return FALSE;
    }
    if (!empty($this->rrule['BYMONTH']) && !in_array($this->current_day
      ->format('n'), $this->rrule['BYMONTH'])) {
      return FALSE;
    }
    if (!empty($this->rrule['BYMONTHDAY'])) {

      // Test month days, but only if there are no negative numbers.
      $test = TRUE;
      $BYMONTHDAYS = array();
      foreach ($this->rrule['BYMONTHDAY'] as $day) {
        if ($day > 0) {
          $BYMONTHDAYS[] = $day;
        }
        else {
          $test = FALSE;
          break;
        }
      }
      if ($test && !empty($BYMONTHDAYS) && !in_array($this->current_day
        ->format('j'), $BYMONTHDAYS)) {
        return FALSE;
      }
    }

    // Don't add a day if it is already saved so we don't throw the count off.
    $formatted = $this->current_day
      ->format($this->default_format);
    if (in_array($formatted, $this->result)) {
      return TRUE;
    }
    else {
      $this->result[] = $formatted;
    }
  }

  /**
   * Stop when $this->current_day is greater than $this->end_date
   * or $this->max_count is reached.
   */
  protected function is_finished() {
    static $cycles;
    $cycles++;
    if (!empty($this->max_count) && sizeof($this->result) >= $this->max_count) {
      return TRUE;
    }
    elseif (!empty($this->end_date) && $this->current_day > $this->end_date) {
      return TRUE;
    }
    elseif ($cycles >= $this->max_cycles) {
      return TRUE;
    }

    // Nothing tells us we are finished.
    return FALSE;
  }

  /**
   * Set a date object to a specific day of the month.
   *
   * Example,
   *   set_month_day('Sunday', 2, '-')
   *   will reset $date to the second to last Sunday in the month.
   *   If $day is empty, will set to the number of days from the
   *   beginning or end of the month.
   */
  protected function set_month_day($day, $count = 1, $direction = '+') {
    $time = $this->time_string;
    $current_month = $this->current_day
      ->format('n');

    // Create a clone and reset.
    $date = clone $this->current_day;
    if ($direction == '-') {

      // For negative search, start from just outside the end
      // of the month, so we can catch the last day of the month.
      $date
        ->modify("first day of next month {$time}");
    }
    else {

      // For positive search, back up one day to get outside the
      // current month, so we can catch the first of the month.
      $date
        ->modify("last day of last month {$time}");
    }
    if (empty($day)) {
      $date
        ->modify("{$direction} {$count} days {$time}");
    }
    else {

      // Use the English text for order, like First Sunday
      // instead of +1 Sunday to overcome PHP5 bug, (see #369020).
      $order = self::$date_order;
      $step = $count <= 5 ? $order[$direction . $count] : $direction . $count;
      $date
        ->modify("{$step} {$day} {$time}");
    }

    // If that takes us outside the current month, don't go there,
    // only reset the date if it's in the current month.
    if ($date
      ->format('n') == $current_month) {
      $this->current_day = $date;
      return TRUE;
    }
    else {
      return FALSE;
    }
  }

  /**
   * Set a date object to a specific day of the year.
   *
   * Example,
   *   date_set_year_day($date, 'Sunday', 2, '-')
   *   will reset $date to the second to last Sunday in the year.
   *   If $day is empty, will set to the number of days from the
   *   beginning or end of the year.
   */
  protected function set_year_day($day, $count = 1, $direction = '+') {
    $time = $this->time_string;
    $current_year = $this->current_day
      ->format('Y');

    // Create a clone and reset.
    $date = clone $this->current_day;
    if ($direction == '-') {

      // For negative search, start from the end of the year.
      // It is important to set year before month for some reason.
      $date
        ->modify("next year January 1 {$time}");
    }
    else {

      // For positive search, back up one day to get outside the
      // current year, so we can catch the first of the year.
      // It is important to set year before month for some reason.
      $date
        ->modify("last year December 31 {$time}");
    }
    if (empty($day)) {
      $date
        ->modify("{$direction} {$count} days {$time}");
    }
    else {

      // Use the English text for order, like First Sunday
      // instead of +1 Sunday to overcome PHP5 bug, (see #369020).
      $order = self::$date_order;
      $step = $count <= 5 ? $order[$direction . $count] : $direction . $count;
      $date
        ->modify("{$step} {$day} {$time}");
    }

    // If that takes us outside the current year, don't go there.
    if ($date
      ->format('Y') == $current_year) {
      $this->current_day = $date;
      return TRUE;
    }
    else {
      return FALSE;
    }
  }

}

Members

Namesort descending Modifiers Type Description Overrides
DateRRuleCalc::$additions public property An array of dates that should be added.
DateRRuleCalc::$current_day public property The current day, as we iterate through the RRULE.
DateRRuleCalc::$date_order public static property Map some common iCal direction values to the text that works more reliably in DateTime::modify().
DateRRuleCalc::$day_names public static property Map the abbreviation used in iCal day names to the day name usable by DateTime and DateInterval.
DateRRuleCalc::$default_format public property The format to use when creating and comparing dates.
DateRRuleCalc::$end_date public property A date object for the start of the series.
DateRRuleCalc::$exceptions public property An array of dates that should not be selected.
DateRRuleCalc::$jump public property A DateInterval representing the amount of time to jump after each iteration of the calculation.
DateRRuleCalc::$max_count public property An optional limit on the number of results, as set in the RRULE.
DateRRuleCalc::$max_cycles public property The maximum number of times to cycle through this code. Needed to avoid endless loops that check for a COUNT without finding any results. This checks the number of times that $this->is_finished() gets called.
DateRRuleCalc::$result public property The array of days that match the criteria.
DateRRuleCalc::$rrule public property An array of RRULE parts, as parsed by the DateiCalParse.
DateRRuleCalc::$start_date public property A date object for the start of the series.
DateRRuleCalc::$timezone_name public property The name of the timezone to use in these computations.
DateRRuleCalc::$time_string public property The time that will be used for all the created dates.
DateRRuleCalc::$week_start_day public property The start day of the week.
DateRRuleCalc::add_current_day protected function Helper function to add current date to the $dates array.
DateRRuleCalc::complete_rrule protected function See if the RRULE needs some imputed values added to it.
DateRRuleCalc::compute public function
DateRRuleCalc::get_absolute_bydays protected function Get values for absolute BYDAYs.
DateRRuleCalc::get_byday_results protected function Processing for BYDAY values.
DateRRuleCalc::get_bymonthday_results protected function Processing for BYMONTHDAY values.
DateRRuleCalc::get_other_results protected function Processing other than BYDAY or BYMONTHDAY.
DateRRuleCalc::get_relative_bydays protected function Get results for relative BYDAY values.
DateRRuleCalc::isValid protected function Basic validation for an RRULE we can do something with.
DateRRuleCalc::is_finished protected function Stop when $this->current_day is greater than $this->end_date or $this->max_count is reached.
DateRRuleCalc::set_month_day protected function Set a date object to a specific day of the month.
DateRRuleCalc::set_year_day protected function Set a date object to a specific day of the year.
DateRRuleCalc::__construct function Compute dates that match the requested rule, within a specified date range.