You are here

class DateObject in Date 7.2

Same name and namespace in other branches
  1. 7.3 date_api/date_api.module \DateObject
  2. 7 date_api/date_api.module \DateObject

Extend PHP DateTime class.

Adds granularity handling, merge functionality and slightly more flexible initialization parameters.

This class is a Drupal independent extension of the >= PHP 5.2 DateTime class.

Hierarchy

Expanded class hierarchy of DateObject

See also

FeedsDateTimeElement class

File

date_api/date_api.module, line 158
This module will make the date API available to other modules.

View source
class DateObject extends DateTime {
  public $granularity = array();
  public $errors = array();
  protected static $allgranularity = array(
    'year',
    'month',
    'day',
    'hour',
    'minute',
    'second',
    'timezone',
  );
  private $serializedTime;
  private $serializedTimezone;

  /**
   * Prepares the object during serialization.
   *
   * We are extending a core class and core classes cannot be serialized.
   *
   * @return array
   *   Returns an array with the names of the variables that were serialized.
   *
   * @see http://bugs.php.net/41334
   * @see http://bugs.php.net/39821
   */
  public function __sleep() {
    $this->serializedTime = $this
      ->format('c');
    $this->serializedTimezone = $this
      ->getTimezone()
      ->getName();
    return array(
      'serializedTime',
      'serializedTimezone',
    );
  }

  /**
   * Re-builds the object using local variables.
   */
  public function __wakeup() {
    $this
      ->__construct($this->serializedTime, new DateTimeZone($this->serializedTimezone));
  }

  /**
   * Returns the date object as a string.
   *
   * @return string
   *   The date object formatted as a string.
   */
  public function __toString() {
    return $this
      ->format(DATE_FORMAT_DATETIME) . ' ' . $this
      ->getTimeZone()
      ->getName();
  }

  /**
   * Constructs a date object.
   *
   * @param string $time
   *   A date/time string or array. Defaults to 'now'.
   * @param object|string|null $tz
   *   PHP DateTimeZone object, string or NULL allowed. Defaults to NULL.
   * @param string $format
   *   PHP date() type format for parsing. Doesn't support timezones; if you
   *   have a timezone, send NULL and the default constructor method will
   *   hopefully parse it. $format is recommended in order to use negative or
   *   large years, which PHP's parser fails on.
   */
  public function __construct($time = 'now', $tz = NULL, $format = NULL) {
    $this->timeOnly = FALSE;
    $this->dateOnly = FALSE;

    // Store the raw time input so it is available for validation.
    $this->originalTime = $time;

    // Allow string timezones.
    if (!empty($tz) && !is_object($tz)) {
      $tz = new DateTimeZone($tz);
    }
    elseif (empty($tz)) {
      $tz = date_default_timezone_object();
    }

    // Special handling for Unix timestamps expressed in the local timezone.
    // Create a date object in UTC and convert it to the local timezone. Don't
    // try to turn things like '2010' with a format of 'Y' into a timestamp.
    if (!is_array($time) && preg_match('`^-?\\d+$`', $time) && (empty($format) || $format == 'U')) {

      // Assume timestamp.
      $time = "@" . $time;
      $date = new DateObject($time, 'UTC');
      if ($tz
        ->getName() != 'UTC') {
        $date
          ->setTimezone($tz);
      }
      $time = $date
        ->format(DATE_FORMAT_DATETIME);
      $format = DATE_FORMAT_DATETIME;
      $this
        ->addGranularity('timezone');
    }
    elseif (is_array($time)) {

      // Assume we were passed an indexed array.
      if (empty($time['year']) && empty($time['month']) && empty($time['day'])) {
        $this->timeOnly = TRUE;
      }
      if (empty($time['hour']) && empty($time['minute']) && empty($time['second'])) {
        $this->dateOnly = TRUE;
      }
      $this->errors = $this
        ->arrayErrors($time);

      // Make this into an ISO date, forcing a full ISO date even if some values
      // are missing.
      $time = $this
        ->toISO($time, TRUE);

      // We checked for errors already, skip parsing the input values.
      $format = NULL;
    }
    else {

      // Make sure dates like 2010-00-00T00:00:00 get converted to
      // 2010-01-01T00:00:00 before creating a date object
      // to avoid unintended changes in the month or day.
      $time = date_make_iso_valid($time);
    }

    // The parse function will also set errors on the date parts.
    if (!empty($format)) {
      $arg = self::$allgranularity;
      $element = array_pop($arg);
      while (!$this
        ->parse($time, $tz, $format) && $element != 'year') {
        $element = array_pop($arg);
        $format = date_limit_format($format, $arg);
      }
      if ($element == 'year') {
        return FALSE;
      }
    }
    elseif (is_string($time)) {

      // PHP < 5.3 doesn't like the GMT- notation for parsing timezones.
      $time = str_replace("GMT-", "-", $time);
      $time = str_replace("GMT+", "+", $time);

      // We are going to let the parent dateObject do a best effort attempt to
      // turn this string into a valid date. It might fail and we want to
      // control the error messages.
      try {
        @parent::__construct($time, $tz);
      } catch (Exception $e) {
        $this->errors['date'] = $e;
        return;
      }
      if (empty($this->granularity)) {
        $this
          ->setGranularityFromTime($time, $tz);
      }
    }

    // If we haven't got a valid timezone name yet, we need to set one or
    // we will get undefined index errors.
    // This can happen if $time had an offset or no timezone.
    if (!$this
      ->getTimezone() || !preg_match('/[a-zA-Z]/', $this
      ->getTimezone()
      ->getName())) {

      // If the original $tz has a name, use it.
      if (preg_match('/[a-zA-Z]/', $tz
        ->getName())) {
        $this
          ->setTimezone($tz);
      }
      else {
        $this
          ->setTimezone(new DateTimeZone("UTC"));
        $this->errors['timezone'] = t('No valid timezone name was provided.');
      }
    }
  }

  /**
   * Merges two date objects together using the current date values as defaults.
   *
   * @param DateObject $other
   *   Another date object to merge with.
   *
   * @return DateObject
   *   A merged date object.
   */
  public function merge(DateObject $other) {
    $other_tz = $other
      ->getTimezone();
    $this_tz = $this
      ->getTimezone();

    // Figure out which timezone to use for combination.
    $use_tz = $this
      ->hasGranularity('timezone') || !$other
      ->hasGranularity('timezone') ? $this_tz : $other_tz;
    $this2 = clone $this;
    $this2
      ->setTimezone($use_tz);
    $other
      ->setTimezone($use_tz);
    $val = $this2
      ->toArray(TRUE);
    $otherval = $other
      ->toArray();
    foreach (self::$allgranularity as $g) {
      if ($other
        ->hasGranularity($g) && !$this2
        ->hasGranularity($g)) {

        // The other class has a property we don't; steal it.
        $this2
          ->addGranularity($g);
        $val[$g] = $otherval[$g];
      }
    }
    $other
      ->setTimezone($other_tz);
    $this2
      ->setDate($val['year'], $val['month'], $val['day']);
    $this2
      ->setTime($val['hour'], $val['minute'], $val['second']);
    return $this2;
  }

  /**
   * Sets the time zone for the current date.
   *
   * Overrides default DateTime function. Only changes output values if
   * actually had time granularity. This should be used as a "converter" for
   * output, to switch tzs.
   *
   * In order to set a timezone for a datetime that doesn't have such
   * granularity, merge() it with one that does.
   *
   * @param object $tz
   *   A timezone object.
   * @param bool $force
   *   Whether or not to skip a date with no time. Defaults to FALSE.
   */
  public function setTimezone($tz, $force = FALSE) {

    // PHP 5.2.6 has a fatal error when setting a date's timezone to itself.
    // http://bugs.php.net/bug.php?id=45038
    if (version_compare(PHP_VERSION, '5.2.7', '<') && $tz == $this
      ->getTimezone()) {
      $tz = new DateTimeZone($tz
        ->getName());
    }
    if (!$this
      ->hasTime() || !$this
      ->hasGranularity('timezone') || $force) {

      // This has no time or timezone granularity, so timezone doesn't mean
      // much. We set the timezone using the method, which will change the
      // day/hour, but then we switch back.
      $arr = $this
        ->toArray(TRUE);
      parent::setTimezone($tz);
      $this
        ->setDate($arr['year'], $arr['month'], $arr['day']);
      $this
        ->setTime($arr['hour'], $arr['minute'], $arr['second']);
      $this
        ->addGranularity('timezone');
      return;
    }
    return parent::setTimezone($tz);
  }

  /**
   * Returns date formatted according to given format.
   *
   * Overrides base format function, formats this date according to its
   * available granularity, unless $force'ed not to limit to granularity.
   *
   * @todo Add translation into this so translated names will be provided.
   *
   * @param string $format
   *   A date format string.
   * @param bool $force
   *   Whether or not to limit the granularity. Defaults to FALSE.
   *
   * @return string|bool
   *   Returns the formatted date string on success or FALSE on failure.
   */
  public function format($format, $force = FALSE) {
    return parent::format($force ? $format : date_limit_format($format, $this->granularity));
  }

  /**
   * Adds a granularity entry to the array.
   *
   * @param string $g
   *   A single date part.
   */
  public function addGranularity($g) {
    $this->granularity[] = $g;
    $this->granularity = array_unique($this->granularity);
  }

  /**
   * Removes a granularity entry from the array.
   *
   * @param string $g
   *   A single date part.
   */
  public function removeGranularity($g) {
    if (($key = array_search($g, $this->granularity)) !== FALSE) {
      unset($this->granularity[$key]);
    }
  }

  /**
   * Checks granularity array for a given entry.
   *
   * @param array|null $g
   *   An array of date parts. Defaults to NULL.
   *
   * @returns bool
   *   TRUE if the date part is present in the date's granularity.
   */
  public function hasGranularity($g = NULL) {
    if ($g === NULL) {

      // Just want to know if it has something valid means no lower
      // granularities without higher ones.
      $last = TRUE;
      foreach (self::$allgranularity as $arg) {
        if ($arg == 'timezone') {
          continue;
        }
        if (in_array($arg, $this->granularity) && !$last) {
          return FALSE;
        }
        $last = in_array($arg, $this->granularity);
      }
      return in_array('year', $this->granularity);
    }
    if (is_array($g)) {
      foreach ($g as $gran) {
        if (!in_array($gran, $this->granularity)) {
          return FALSE;
        }
      }
      return TRUE;
    }
    return in_array($g, $this->granularity);
  }

  /**
   * Determines if a a date is valid for a given granularity.
   *
   * @param array|null $granularity
   *   An array of date parts. Defaults to NULL.
   * @param bool $flexible
   *   TRUE if the granuliarty is flexible, FALSE otherwise. Defaults to FALSE.
   *
   * @return bool
   *   Whether a date is valid for a given granularity.
   */
  public function validGranularity($granularity = NULL, $flexible = FALSE) {
    $true = $this
      ->hasGranularity() && (!$granularity || $flexible || $this
      ->hasGranularity($granularity));
    if (!$true && $granularity) {
      $allowed_values = array(
        'second',
        'minute',
        'hour',
        'day',
        'month',
        'year',
      );
      foreach ((array) $granularity as $part) {
        if (!$this
          ->hasGranularity($part) && in_array($part, $allowed_values)) {
          switch ($part) {
            case 'second':
              $this->errors[$part] = t('The second is missing.');
              break;
            case 'minute':
              $this->errors[$part] = t('The minute is missing.');
              break;
            case 'hour':
              $this->errors[$part] = t('The hour is missing.');
              break;
            case 'day':
              $this->errors[$part] = t('The day is missing.');
              break;
            case 'month':
              $this->errors[$part] = t('The month is missing.');
              break;
            case 'year':
              $this->errors[$part] = t('The year is missing.');
              break;
          }
        }
      }
    }
    return $true;
  }

  /**
   * Returns whether this object has time set.
   *
   * Used primarily for timezone conversion and formatting.
   *
   * @return bool
   *   TRUE if the date contains time parts, FALSE otherwise.
   */
  public function hasTime() {
    return $this
      ->hasGranularity('hour');
  }

  /**
   * Returns whether the input values included a year.
   *
   * Useful to use pseudo date objects when we only are interested in the time.
   *
   * @todo $this->completeDate does not actually exist?
   */
  public function completeDate() {
    return $this->completeDate;
  }

  /**
   * Removes unwanted date parts from a date.
   *
   * In common usage we should not unset timezone through this.
   *
   * @param array $granularity
   *   An array of date parts.
   */
  public function limitGranularity(array $granularity) {
    foreach ($this->granularity as $key => $val) {
      if ($val != 'timezone' && !in_array($val, $granularity)) {
        unset($this->granularity[$key]);
      }
    }
  }

  /**
   * Determines the granularity of a date based on the constructor's arguments.
   *
   * @param string $time
   *   A date string.
   * @param bool $tz
   *   TRUE if the date has a timezone, FALSE otherwise.
   */
  protected function setGranularityFromTime($time, $tz) {
    $this->granularity = array();
    $temp = date_parse($time);

    // Special case for 'now'.
    if ($time == 'now') {
      $this->granularity = array(
        'year',
        'month',
        'day',
        'hour',
        'minute',
        'second',
      );
    }
    else {

      // This PHP date_parse() method currently doesn't have resolution down to
      // seconds, so if there is some time, all will be set.
      foreach (self::$allgranularity as $g) {
        if (isset($temp[$g]) && is_numeric($temp[$g]) || $g == 'timezone' && (isset($temp['zone_type']) && $temp['zone_type'] > 0)) {
          $this->granularity[] = $g;
        }
      }
    }
    if ($tz) {
      $this
        ->addGranularity('timezone');
    }
  }

  /**
   * Converts a date string into a date object.
   *
   * @param string $date
   *   The date string to parse.
   * @param object $tz
   *   A timezone object.
   * @param string $format
   *   The date format string.
   *
   * @return object
   *   Returns the date object.
   */
  protected function parse($date, $tz, $format) {
    $array = date_format_patterns();
    foreach ($array as $key => $value) {

      // The letter with no preceding '\'.
      $patterns[] = "`(^|[^\\\\\\\\])" . $key . "`";

      // A single character.
      $repl1[] = '${1}(.)';

      // The.
      $repl2[] = '${1}(' . $value . ')';
    }
    $patterns[] = "`\\\\\\\\([" . implode(array_keys($array)) . "])`";
    $repl1[] = '${1}';
    $repl2[] = '${1}';
    $format_regexp = preg_quote($format);

    // Extract letters.
    $regex1 = preg_replace($patterns, $repl1, $format_regexp, 1);
    $regex1 = str_replace('A', '(.)', $regex1);
    $regex1 = str_replace('a', '(.)', $regex1);
    preg_match('`^' . $regex1 . '$`', stripslashes($format), $letters);
    array_shift($letters);
    $ampm_upper = date_ampm_options(FALSE, TRUE);
    $ampm_lower = date_ampm_options(FALSE, FALSE);
    $regex2 = strtr(preg_replace($patterns, $repl2, $format_regexp, 1), array(
      'A' => '(' . preg_quote($ampm_upper['am'], '`') . '|' . preg_quote($ampm_upper['pm'], '`') . ')',
      'a' => '(' . preg_quote($ampm_lower['am'], '`') . '|' . preg_quote($ampm_lower['pm'], '`') . ')',
    ));
    preg_match('`^' . $regex2 . '$`u', $date, $values);
    array_shift($values);

    // If we did not find all the values for the patterns in the format, abort.
    if (count($letters) != count($values)) {
      $this->errors['invalid'] = t('The value @date does not match the expected format.', array(
        '@date' => $date,
      ));
      return FALSE;
    }
    $this->granularity = array();
    $final_date = array(
      'hour' => 0,
      'minute' => 0,
      'second' => 0,
      'month' => 1,
      'day' => 1,
      'year' => 0,
    );
    foreach ($letters as $i => $letter) {
      $value = $values[$i];
      switch ($letter) {
        case 'd':
        case 'j':
          $final_date['day'] = intval($value);
          $this
            ->addGranularity('day');
          if (empty($value)) {
            $this->errors['day'] = t('The day is invalid.');
          }
          break;
        case 'n':
        case 'm':
          $final_date['month'] = intval($value);
          $this
            ->addGranularity('month');
          if (empty($value)) {
            $this->errors['month'] = t('The month is invalid.');
          }
          break;
        case 'F':
          $array_month_long = array_flip(date_month_names());
          $final_date['month'] = array_key_exists($value, $array_month_long) ? $array_month_long[$value] : -1;
          $this
            ->addGranularity('month');
          break;
        case 'M':
          $array_month = array_flip(date_month_names_abbr());
          $final_date['month'] = array_key_exists($value, $array_month) ? $array_month[$value] : -1;
          $this
            ->addGranularity('month');
          break;
        case 'Y':
          $final_date['year'] = $value;
          $this
            ->addGranularity('year');
          if (strlen($value) < 4) {
            $this->errors['year'] = t('The year is invalid. Please check that entry includes four digits.');
          }
          break;
        case 'y':
          $year = $value;

          // If no century, we add the current one ("06" => "2006").
          $final_date['year'] = str_pad($year, 4, substr(date("Y"), 0, 2), STR_PAD_LEFT);
          $this
            ->addGranularity('year');
          break;
        case 'a':
          $ampm = $value == $ampm_lower['am'] ? 'am' : 'pm';
          break;
        case 'A':
          $ampm = $value == $ampm_upper['am'] ? 'am' : 'pm';
          break;
        case 'g':
        case 'h':
        case 'G':
        case 'H':
          $final_date['hour'] = intval($value);
          $this
            ->addGranularity('hour');
          break;
        case 'i':
          $final_date['minute'] = intval($value);
          $this
            ->addGranularity('minute');
          break;
        case 's':
          $final_date['second'] = intval($value);
          $this
            ->addGranularity('second');
          break;
        case 'U':
          parent::__construct($value, $tz ? $tz : new DateTimeZone("UTC"));
          $this
            ->addGranularity('year');
          $this
            ->addGranularity('month');
          $this
            ->addGranularity('day');
          $this
            ->addGranularity('hour');
          $this
            ->addGranularity('minute');
          $this
            ->addGranularity('second');
          return $this;
      }
    }
    if (isset($ampm)) {
      if ($ampm == 'pm' && $final_date['hour'] < 12) {
        $final_date['hour'] += 12;
      }
      elseif ($ampm == 'am' && $final_date['hour'] == 12) {
        $final_date['hour'] -= 12;
      }
    }

    // Blank becomes current time, given TZ.
    parent::__construct('', $tz ? $tz : new DateTimeZone("UTC"));
    if ($tz) {
      $this
        ->addGranularity('timezone');
    }

    // SetDate expects an integer value for the year, results can be unexpected
    // if we feed it something like '0100' or '0000'.
    $final_date['year'] = intval($final_date['year']);
    $this->errors += $this
      ->arrayErrors($final_date);
    $granularity = drupal_map_assoc($this->granularity);

    // If the input year value is empty or '0000', PHP's date class will later
    // incorrectly convert it if we do setDate() here. If we don't do setDate()
    // here, it will default to the current date and we will lose any way to
    // tell that there was no date in the original input value(s). So set a
    // flag we can use later to tell that this date object was created using
    // only time values, and that the date values are artificial.
    if (empty($final_date['year'])) {
      $this->timeOnly = TRUE;
    }
    elseif (empty($this->errors)) {

      // setDate() expects a valid year, month, and day.
      // Set some defaults for dates that don't use this to
      // keep PHP from interpreting it as the last day of
      // the previous month or last month of the previous year.
      if (empty($granularity['month'])) {
        $final_date['month'] = 1;
      }
      if (empty($granularity['day'])) {
        $final_date['day'] = 1;
      }
      $this
        ->setDate($final_date['year'], $final_date['month'], $final_date['day']);
    }
    if (empty($final_date['hour']) && empty($final_date['minute']) && empty($final_date['second'])) {
      $this->dateOnly = TRUE;
    }
    if (empty($this->errors)) {
      $this
        ->setTime($final_date['hour'], $final_date['minute'], $final_date['second']);
    }
    return $this;
  }

  /**
   * Returns all standard date parts in an array.
   *
   * Will return '' for parts in which it lacks granularity.
   *
   * @param bool $force
   *   Whether or not to limit the granularity. Defaults to FALSE.
   *
   * @return array
   *   An array of formatted date part values, keyed by date parts.
   */
  public function toArray($force = FALSE) {
    return array(
      'year' => $this
        ->format('Y', $force),
      'month' => $this
        ->format('n', $force),
      'day' => $this
        ->format('j', $force),
      'hour' => intval($this
        ->format('H', $force)),
      'minute' => intval($this
        ->format('i', $force)),
      'second' => intval($this
        ->format('s', $force)),
      'timezone' => $this
        ->format('e', $force),
    );
  }

  /**
   * Creates an ISO date from an array of values.
   *
   * @param array $arr
   *   An array of date values keyed by date part.
   * @param bool $full
   *   (optional) Whether to force a full date by filling in missing values.
   *   Defaults to FALSE.
   */
  public function toISO(array $arr, $full = FALSE) {

    // Add empty values to avoid errors. The empty values must create a valid
    // date or we will get date slippage, i.e. a value of 2011-00-00 will get
    // interpreted as November of 2010 by PHP.
    if ($full) {
      $arr += array(
        'year' => 0,
        'month' => 1,
        'day' => 1,
        'hour' => 0,
        'minute' => 0,
        'second' => 0,
      );
    }
    else {
      $arr += array(
        'year' => '',
        'month' => '',
        'day' => '',
        'hour' => '',
        'minute' => '',
        'second' => '',
      );
    }
    $datetime = '';
    if ($arr['year'] !== '') {
      $datetime = date_pad(intval($arr['year']), 4);
      if ($full || $arr['month'] !== '') {
        $datetime .= '-' . date_pad(intval($arr['month']));
        if ($full || $arr['day'] !== '') {
          $datetime .= '-' . date_pad(intval($arr['day']));
        }
      }
    }
    if ($arr['hour'] !== '') {
      $datetime .= $datetime ? 'T' : '';
      $datetime .= date_pad(intval($arr['hour']));
      if ($full || $arr['minute'] !== '') {
        $datetime .= ':' . date_pad(intval($arr['minute']));
        if ($full || $arr['second'] !== '') {
          $datetime .= ':' . date_pad(intval($arr['second']));
        }
      }
    }
    return $datetime;
  }

  /**
   * Forces an incomplete date to be valid.
   *
   * E.g., add a valid year, month, and day if only the time has been defined.
   *
   * @param array|string $date
   *   An array of date parts or a datetime string with values to be massaged
   *   into a valid date object.
   * @param string $format
   *   (optional) The format of the date. Defaults to NULL.
   * @param string $default
   *   (optional) If the fallback should use the first value of the date part,
   *   or the current value of the date part. Defaults to 'first'.
   */
  public function setFuzzyDate($date, $format = NULL, $default = 'first') {
    $timezone = $this
      ->getTimeZone() ? $this
      ->getTimeZone()
      ->getName() : NULL;
    $comp = new DateObject($date, $timezone, $format);
    $arr = $comp
      ->toArray(TRUE);
    foreach ($arr as $key => $value) {

      // Set to intval here and then test that it is still an integer.
      // Needed because sometimes valid integers come through as strings.
      $arr[$key] = $this
        ->forceValid($key, intval($value), $default, $arr['month'], $arr['year']);
    }
    $this
      ->setDate($arr['year'], $arr['month'], $arr['day']);
    $this
      ->setTime($arr['hour'], $arr['minute'], $arr['second']);
  }

  /**
   * Converts a date part into something that will produce a valid date.
   *
   * @param string $part
   *   The date part.
   * @param int $value
   *   The date value for this part.
   * @param string $default
   *   (optional) If the fallback should use the first value of the date part,
   *   or the current value of the date part. Defaults to 'first'.
   * @param int $month
   *   (optional) Used when the date part is less than 'month' to specify the
   *   date. Defaults to NULL.
   * @param int $year
   *   (optional) Used when the date part is less than 'year' to specify the
   *   date. Defaults to NULL.
   *
   * @return int
   *   A valid date value.
   */
  protected function forceValid($part, $value, $default = 'first', $month = NULL, $year = NULL) {
    $now = date_now();
    switch ($part) {
      case 'year':
        $fallback = $now
          ->format('Y');
        return !is_int($value) || empty($value) || $value < variable_get('date_min_year', 1) || $value > variable_get('date_max_year', 4000) ? $fallback : $value;
      case 'month':
        $fallback = $default == 'first' ? 1 : $now
          ->format('n');
        return !is_int($value) || empty($value) || $value <= 0 || $value > 12 ? $fallback : $value;
      case 'day':
        $fallback = $default == 'first' ? 1 : $now
          ->format('j');
        $max_day = isset($year) && isset($month) ? date_days_in_month($year, $month) : 31;
        return !is_int($value) || empty($value) || $value <= 0 || $value > $max_day ? $fallback : $value;
      case 'hour':
        $fallback = $default == 'first' ? 0 : $now
          ->format('G');
        return !is_int($value) || $value < 0 || $value > 23 ? $fallback : $value;
      case 'minute':
        $fallback = $default == 'first' ? 0 : $now
          ->format('i');
        return !is_int($value) || $value < 0 || $value > 59 ? $fallback : $value;
      case 'second':
        $fallback = $default == 'first' ? 0 : $now
          ->format('s');
        return !is_int($value) || $value < 0 || $value > 59 ? $fallback : $value;
    }
  }

  /**
   * Finds possible errors in an array of date part values.
   *
   * The forceValid() function will change an invalid value to a valid one, so
   * we just need to see if the value got altered.
   *
   * @param array $arr
   *   An array of date values, keyed by date part.
   *
   * @return array
   *   An array of error messages, keyed by date part.
   */
  public function arrayErrors(array $arr) {
    $errors = array();
    $now = date_now();
    $default_month = !empty($arr['month']) ? $arr['month'] : $now
      ->format('n');
    $default_year = !empty($arr['year']) ? $arr['year'] : $now
      ->format('Y');
    $this->granularity = array();
    foreach ($arr as $part => $value) {

      // Explicitly set the granularity to the values in the input array.
      if (is_numeric($value)) {
        $this
          ->addGranularity($part);
      }

      // Avoid false errors when a numeric value is input as a string by casting
      // as an integer.
      $value = intval($value);
      if (!empty($value) && $this
        ->forceValid($part, $value, 'now', $default_month, $default_year) != $value) {

        // Use a switch/case to make translation easier by providing a different
        // message for each part.
        switch ($part) {
          case 'year':
            $errors['year'] = t('The year is invalid.');
            break;
          case 'month':
            $errors['month'] = t('The month is invalid.');
            break;
          case 'day':
            $errors['day'] = t('The day is invalid.');
            break;
          case 'hour':
            $errors['hour'] = t('The hour is invalid.');
            break;
          case 'minute':
            $errors['minute'] = t('The minute is invalid.');
            break;
          case 'second':
            $errors['second'] = t('The second is invalid.');
            break;
        }
      }
    }
    if ($this
      ->hasTime()) {
      $this
        ->addGranularity('timezone');
    }
    return $errors;
  }

  /**
   * Computes difference between two days using a given measure.
   *
   * @param object $date2_in
   *   The stop date.
   * @param string $measure
   *   (optional) A granularity date part. Defaults to 'seconds'.
   * @param bool $absolute
   *   (optional) Indicate whether the absolute value of the difference should
   *   be returned or if the sign should be retained. Defaults to TRUE.
   */
  public function difference($date2_in, $measure = 'seconds', $absolute = TRUE) {

    // Create cloned objects or original dates will be impacted by the
    // date_modify() operations done in this code.
    $date1 = clone $this;
    $date2 = clone $date2_in;
    if (is_object($date1) && is_object($date2)) {
      $diff = date_format($date2, 'U') - date_format($date1, 'U');
      if ($diff == 0) {
        return 0;
      }
      elseif ($diff < 0 && $absolute) {

        // Make sure $date1 is the smaller date.
        $temp = $date2;
        $date2 = $date1;
        $date1 = $temp;
        $diff = date_format($date2, 'U') - date_format($date1, 'U');
      }
      $year_diff = intval(date_format($date2, 'Y') - date_format($date1, 'Y'));
      switch ($measure) {

        // The easy cases first.
        case 'seconds':
          return $diff;
        case 'minutes':
          return $diff / 60;
        case 'hours':
          return $diff / 3600;
        case 'years':
          return $year_diff;
        case 'months':
          $format = 'n';
          $item1 = date_format($date1, $format);
          $item2 = date_format($date2, $format);
          if ($year_diff == 0) {
            return intval($item2 - $item1);
          }
          elseif ($year_diff < 0) {
            $item_diff = 0 - $item1;
            $item_diff -= intval((abs($year_diff) - 1) * 12);
            return $item_diff - (12 - $item2);
          }
          else {
            $item_diff = 12 - $item1;
            $item_diff += intval(($year_diff - 1) * 12);
            return $item_diff + $item2;
          }
          break;
        case 'days':
          $format = 'z';
          $item1 = date_format($date1, $format);
          $item2 = date_format($date2, $format);
          if ($year_diff == 0) {
            return intval($item2 - $item1);
          }
          elseif ($year_diff < 0) {
            $item_diff = 0 - $item1;
            for ($i = 1; $i < abs($year_diff); $i++) {
              date_modify($date1, '-1 year');
              $item_diff -= date_days_in_year($date1);
            }
            return $item_diff - (date_days_in_year($date2) - $item2);
          }
          else {
            $item_diff = date_days_in_year($date1) - $item1;
            for ($i = 1; $i < $year_diff; $i++) {
              date_modify($date1, '+1 year');
              $item_diff += date_days_in_year($date1);
            }
            return $item_diff + $item2;
          }
          break;
        case 'weeks':
          $week_diff = date_format($date2, 'W') - date_format($date1, 'W');
          $year_diff = date_format($date2, 'o') - date_format($date1, 'o');
          $sign = $year_diff < 0 ? -1 : 1;
          for ($i = 1; $i <= abs($year_diff); $i++) {
            date_modify($date1, ($sign > 0 ? '+' : '-') . '1 year');
            $week_diff += date_iso_weeks_in_year($date1) * $sign;
          }
          return $week_diff;
      }
    }
    return NULL;
  }

}

Members

Namesort descending Modifiers Type Description Overrides
DateObject::$allgranularity protected static property
DateObject::$errors public property
DateObject::$granularity public property
DateObject::$serializedTime private property
DateObject::$serializedTimezone private property
DateObject::addGranularity public function Adds a granularity entry to the array.
DateObject::arrayErrors public function Finds possible errors in an array of date part values.
DateObject::completeDate public function Returns whether the input values included a year.
DateObject::difference public function Computes difference between two days using a given measure.
DateObject::forceValid protected function Converts a date part into something that will produce a valid date.
DateObject::format public function Returns date formatted according to given format.
DateObject::hasGranularity public function Checks granularity array for a given entry.
DateObject::hasTime public function Returns whether this object has time set.
DateObject::limitGranularity public function Removes unwanted date parts from a date.
DateObject::merge public function Merges two date objects together using the current date values as defaults.
DateObject::parse protected function Converts a date string into a date object.
DateObject::removeGranularity public function Removes a granularity entry from the array.
DateObject::setFuzzyDate public function Forces an incomplete date to be valid.
DateObject::setGranularityFromTime protected function Determines the granularity of a date based on the constructor's arguments.
DateObject::setTimezone public function Sets the time zone for the current date.
DateObject::toArray public function Returns all standard date parts in an array.
DateObject::toISO public function Creates an ISO date from an array of values.
DateObject::validGranularity public function Determines if a a date is valid for a given granularity.
DateObject::__construct public function Constructs a date object.
DateObject::__sleep public function Prepares the object during serialization.
DateObject::__toString public function Returns the date object as a string.
DateObject::__wakeup public function Re-builds the object using local variables.