Date API Status'; if (!empty($messages['success'])) { $output .= '
' . t('Complete documentation for the Date and Date API modules is available at http://drupal.org/node/92460.', array('@link' => 'http://drupal.org/node/262062')) . '
'; return $output; break; } } /** * Helper function to retun the status of required date variables. */ function date_api_status() { $t = get_t(); $error_messages = array(); $success_messages = array(); $value = variable_get('date_default_timezone'); if (isset($value)) { $success_messages[] = $t('The timezone has been set to @timezone.', array('@regional_settings' => url('admin/config/regional/settings'), '@timezone' => $value)); } else { $error_messages[] = $t('The Date API requires that you set up the site timezone to function correctly.', array('@regional_settings' => url('admin/config/regional/settings'))); } $value = variable_get('date_first_day'); if (isset($value)) { $days = date_week_days(); $success_messages[] = $t('The first day of the week has been set to @day.', array('@regional_settings' => url('admin/config/regional/settings'), '@day' => $days[$value])); } else { $error_messages[] = $t('The Date API requires that you set up the site first day of week settings to function correctly.', array('@regional_settings' => url('admin/config/regional/settings'))); } $value = variable_get('date_format_medium'); if (isset($value)) { $now = date_now(); $success_messages[] = $t('The medium date format type has been set to to @value. You may find it helpful to add new format types like Date, Time, Month, or Year, with appropriate formats, at Date and time settings.', array('@value' => $now->format($value), '@regional_date_time' => url('admin/config/regional/date-time'))); } else { $error_messages[] = $t('The Date API requires that you set up the system date formats to function correctly.', array('@regional_date_time' => url('admin/config/regional/date-time'))); } return array('errors', $error_messages, 'success' => $success_messages); } /** * Implements hook_menu(). * * Creates a 'Date API' section on the administration page for Date * modules to use for their configuration and settings. */ function date_api_menu() { $items['admin/config/date'] = array( 'title' => 'Date API', 'description' => 'Settings for modules the use the Date API.', 'position' => 'left', 'weight' => -10, 'page callback' => 'system_admin_menu_block_page', 'access arguments' => array('administer site configuration'), 'file' => 'system.admin.inc', 'file path' => drupal_get_path('module', 'system'), ); return $items; } /** * Extend PHP DateTime class with granularity handling, merge functionality and * slightly more flexible initialization parameters. * * This class is a Drupal independent extension of the >= PHP 5.2 DateTime * class. * * @see FeedsDateTimeElement class */ 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); } // Default to the site timezone when not explicitly provided. 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_numeric($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); } // We have no information about the timezone so must fallback to a default. 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 object $other * Another date object to merge with. * * @return object * A merged date object. */ public function merge(FeedsDateTime $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|false * 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)) { 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) { foreach ((array) $granularity as $part) { if (!$this->hasGranularity($part) && in_array($part, array('second', 'minute', 'hour', 'day', 'month', 'year'))) { 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($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); // Extract values. $regex2 = preg_replace($patterns, $repl2, $format_regexp, 1); $regex2 = str_replace('A', '(AM|PM)', $regex2); $regex2 = str_replace('a', '(am|pm)', $regex2); 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'); break; case 'n': case 'm': $final_date['month'] = intval($value); $this->addGranularity('month'); 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': case 'A': $ampm = strtolower($value); 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; break; } } if (isset($ampm) && $ampm == 'pm' && $final_date['hour'] < 12) { $final_date['hour'] += 12; } elseif (isset($ampm) && $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 value is '0000-00-00', PHP's date class will later // incorrectly convert it to something like '-0001-11-30' 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 orignal // input values. 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 // artifical. if (empty($final_date['year']) && empty($final_date['month']) && empty($final_date['day'])) { $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 (!isset($final_date['hour']) && !isset($final_date['minute']) && !isset($final_date['second'])) { $this->dateOnly = TRUE; } elseif (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($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; break; case 'month': $fallback = $default == 'first' ? 1 : $now->format('n'); return !is_int($value) || empty($value) || $value <= 0 || $value > 12 ? $fallback : $value; break; 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; break; case 'hour': $fallback = $default == 'first' ? 0 : $now->format('G'); return !is_int($value) || $value < 0 || $value > 23 ? $fallback : $value; break; case 'minute': $fallback = $default == 'first' ? 0 : $now->format('i'); return !is_int($value) || $value < 0 || $value > 59 ? $fallback : $value; break; case 'second': $fallback = $default == 'first' ? 0 : $now->format('s'); return !is_int($value) || $value < 0 || $value > 59 ? $fallback : $value; break; } } /** * 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($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 boolean $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; } } /** * Determines if the date element needs to be processed. * * Helper function to see if date element has been hidden by FAPI to see if it * needs to be processed or just pass the value through. This is needed since * normal date processing explands the date element into parts and then * reconstructs it, which is not needed or desirable if the field is hidden. * * @param array $element * The date element to check. * * @return bool * TRUE if the element is effectively hidden, FALSE otherwise. */ function date_hidden_element($element) { // @TODO What else needs to be tested to see if dates are hidden or disabled? if ((isset($element['#access']) && empty($element['#access'])) || !empty($element['#programmed']) || in_array($element['#type'], array('hidden', 'value'))) { return TRUE; } return FALSE; } /** * Helper function for getting the format string for a date type. * * @param string $type * A date type format name. * * @return string * A date type format, like 'Y-m-d H:i:s'. */ function date_type_format($type) { switch ($type) { case DATE_ISO: return DATE_FORMAT_ISO; case DATE_UNIX: return DATE_FORMAT_UNIX; case DATE_DATETIME: return DATE_FORMAT_DATETIME; case DATE_ICAL: return DATE_FORMAT_ICAL; } } /** * Constructs an untranslated array of month names. * * Needed for CSS, translation functions, strtotime(), and other places * that use the English versions of these words. * * @return array * An array of month names. */ function date_month_names_untranslated() { static $month_names; if (empty($month_names)) { $month_names = array( 1 => 'January', 2 => 'February', 3 => 'March', 4 => 'April', 5 => 'May', 6 => 'June', 7 => 'July', 8 => 'August', 9 => 'September', 10 => 'October', 11 => 'November', 12 => 'December', ); } return $month_names; } /** * Returns a translated array of month names. * * @param bool $required * (optional) If FALSE, the returned array will include a blank value. * Defaults to FALSE. * * @return array * An array of month names. */ function date_month_names($required = FALSE) { $month_names = array(); foreach (date_month_names_untranslated() as $key => $month) { $month_names[$key] = t($month, array(), array('context' => 'Long month name')); } $none = array('' => ''); return !$required ? $none + $month_names : $month_names; } /** * Constructs a translated array of month name abbreviations * * @param bool $required * (optional) If FALSE, the returned array will include a blank value. * Defaults to FALSE. * @param int $length * (optional) The length of the abbreviation. Defaults to 3. * * @return array * An array of month abbreviations. */ function date_month_names_abbr($required = FALSE, $length = 3) { $month_names = array(); foreach (date_month_names_untranslated() as $key => $month) { if ($length == 3) { $month_names[$key] = t(substr($month, 0, $length), array()); } else { $month_names[$key] = t(substr($month, 0, $length), array(), array('context' => 'month_abbr')); } } $none = array('' => ''); return !$required ? $none + $month_names : $month_names; } /** * Constructs an untranslated array of week days. * * Needed for CSS, translation functions, strtotime(), and other places * that use the English versions of these words. * * @param bool $refresh * (optional) Whether to refresh the list. Defaults to TRUE. * * @return array * An array of week day names */ function date_week_days_untranslated($refresh = TRUE) { static $weekdays; if ($refresh || empty($weekdays)) { $weekdays = array( 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', ); } return $weekdays; } /** * Returns a translated array of week names. * * @param bool $required * (optional) If FALSE, the returned array will include a blank value. * Defaults to FALSE. * * @return array * An array of week day names */ function date_week_days($required = FALSE, $refresh = TRUE) { $weekdays = array(); foreach (date_week_days_untranslated() as $key => $day) { $weekdays[$key] = t($day, array(), array('context' => '')); } $none = array('' => ''); return !$required ? $none + $weekdays : $weekdays; } /** * Constructs a translated array of week day abbreviations. * * @param bool $required * (optional) If FALSE, the returned array will include a blank value. * Defaults to FALSE. * @param bool $refresh * (optional) Whether to refresh the list. Defaults to TRUE. * @param int $length * (optional) The length of the abbreviation. Defaults to 3. * * @return array * An array of week day abbreviations */ function date_week_days_abbr($required = FALSE, $refresh = TRUE, $length = 3) { $weekdays = array(); switch ($length) { case 1: $context = 'day_abbr1'; break; case 2: $context = 'day_abbr2'; break; default: $context = ''; break; } foreach (date_week_days_untranslated() as $key => $day) { $weekdays[$key] = t(substr($day, 0, $length), array(), array('context' => $context)); } $none = array('' => ''); return !$required ? $none + $weekdays : $weekdays; } /** * Reorders weekdays to match the first day of the week. * * @param array $weekdays * An array of weekdays. * * @return array * An array of weekdays reordered to match the first day of the week. */ function date_week_days_ordered($weekdays) { $first_day = variable_get('date_first_day', 0); if ($first_day > 0) { for ($i = 1; $i <= $first_day; $i++) { $last = array_shift($weekdays); array_push($weekdays, $last); } } return $weekdays; } /** * Constructs an array of years. * * @param int $min * The minimum year in the array. * @param int $max * The maximum year in the array. * @param bool $required * (optional) If FALSE, the returned array will include a blank value. * Defaults to FALSE. * * @return array * An array of years in the selected range. */ function date_years($min = 0, $max = 0, $required = FALSE) { // Ensure $min and $max are valid values. if (empty($min)) { $min = intval(date('Y', REQUEST_TIME) - 3); } if (empty($max)) { $max = intval(date('Y', REQUEST_TIME) + 3); } $none = array(0 => ''); return !$required ? $none + drupal_map_assoc(range($min, $max)) : drupal_map_assoc(range($min, $max)); } /** * Constructs an array of days in a month. * * @param bool $required * (optional) If FALSE, the returned array will include a blank value. * Defaults to FALSE. * @param int $month * (optional) The month in which to find the number of days. * @param int $year * (optional) The year in which to find the number of days. * * @return array * An array of days for the selected month. */ function date_days($required = FALSE, $month = NULL, $year = NULL) { // If we have a month and year, find the right last day of the month. if (!empty($month) && !empty($year)) { $date = new DateObject($year . '-' . $month . '-01 00:00:00', 'UTC'); $max = $date->format('t'); } // If there is no month and year given, default to 31. if (empty($max)) { $max = 31; } $none = array(0 => ''); return !$required ? $none + drupal_map_assoc(range(1, $max)) : drupal_map_assoc(range(1, $max)); } /** * Constructs an array of hours. * * @param string $format * A date format string. * @param bool $required * (optional) If FALSE, the returned array will include a blank value. * Defaults to FALSE. * * @return array * An array of hours in the selected format. */ function date_hours($format = 'H', $required = FALSE) { $hours = array(); if ($format == 'h' || $format == 'g') { $min = 1; $max = 12; } else { $min = 0; $max = 23; } for ($i = $min; $i <= $max; $i++) { $hours[$i] = $i < 10 && ($format == 'H' || $format == 'h') ? "0$i" : $i; } $none = array('' => ''); return !$required ? $none + $hours : $hours; } /** * Constructs an array of minutes. * * @param string $format * A date format string. * @param bool $required * (optional) If FALSE, the returned array will include a blank value. * Defaults to FALSE. * * @return array * An array of minutes in the selected format. */ function date_minutes($format = 'i', $required = FALSE, $increment = 1) { $minutes = array(); // Ensure $increment has a value so we don't loop endlessly. if (empty($increment)) { $increment = 1; } for ($i = 0; $i < 60; $i += $increment) { $minutes[$i] = $i < 10 && $format == 'i' ? "0$i" : $i; } $none = array('' => ''); return !$required ? $none + $minutes : $minutes; } /** * Constructs an array of seconds. * * @param string $format * A date format string. * @param bool $required * (optional) If FALSE, the returned array will include a blank value. * Defaults to FALSE. * * @return array * An array of seconds in the selected format. */ function date_seconds($format = 's', $required = FALSE, $increment = 1) { $seconds = array(); // Ensure $increment has a value so we don't loop endlessly. if (empty($increment)) { $increment = 1; } for ($i = 0; $i < 60; $i += $increment) { $seconds[$i] = $i < 10 && $format == 's' ? "0$i" : $i; } $none = array('' => ''); return !$required ? $none + $seconds : $seconds; } /** * Constructs an array of AM and PM options. * * @param bool $required * (optional) If FALSE, the returned array will include a blank value. * Defaults to FALSE. * * @return array * An array of AM and PM options. */ function date_ampm($required = FALSE) { $none = array('' => ''); $ampm = array( 'am' => t('am', array(), array('context' => 'ampm')), 'pm' => t('pm', array(), array('context' => 'ampm')), ); return !$required ? $none + $ampm : $ampm; } /** * Constructs an array of regex replacement strings for date format elements. * * @param bool $strict * Whether or not to force 2 digits for elements that sometimes allow either * 1 or 2 digits. * * @return array * An array of date() format letters and their regex equivalents. */ function date_format_patterns($strict = FALSE) { return array( 'd' => '\d{' . ($strict ? '2' : '1,2') . '}', 'm' => '\d{' . ($strict ? '2' : '1,2') . '}', 'h' => '\d{' . ($strict ? '2' : '1,2') . '}', 'H' => '\d{' . ($strict ? '2' : '1,2') . '}', 'i' => '\d{' . ($strict ? '2' : '1,2') . '}', 's' => '\d{' . ($strict ? '2' : '1,2') . '}', 'j' => '\d{1,2}', 'N' => '\d', 'S' => '\w{2}', 'w' => '\d', 'z' => '\d{1,3}', 'W' => '\d{1,2}', 'n' => '\d{1,2}', 't' => '\d{2}', 'L' => '\d', 'o' => '\d{4}', 'Y' => '-?\d{1,6}', 'y' => '\d{2}', 'B' => '\d{3}', 'g' => '\d{1,2}', 'G' => '\d{1,2}', 'e' => '\w*', 'I' => '\d', 'T' => '\w*', 'U' => '\d*', 'z' => '[+-]?\d*', 'O' => '[+-]?\d{4}', // Using S instead of w and 3 as well as 4 to pick up non-ASCII chars like // German umlaut. Per http://drupal.org/node/1101284, we may need as little // as 2 and as many as 5 characters in some languages. 'D' => '\S{2,5}', 'l' => '\S*', 'M' => '\S{2,5}', 'F' => '\S*', 'P' => '[+-]?\d{2}\:\d{2}', 'O' => '[+-]\d{4}', 'c' => '(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})([+-]?\d{2}\:\d{2})', 'r' => '(\w{3}), (\d{2})\s(\w{3})\s(\d{2,4})\s(\d{2}):(\d{2}):(\d{2})([+-]?\d{4})?', ); } /** * Constructs an array of granularity options and their labels. * * @return array * An array of translated date parts, keyed by their machine name. */ function date_granularity_names() { return array( 'year' => t('Year', array(), array('context' => 'datetime')), 'month' => t('Month', array(), array('context' => 'datetime')), 'day' => t('Day', array(), array('context' => 'datetime')), 'hour' => t('Hour', array(), array('context' => 'datetime')), 'minute' => t('Minute', array(), array('context' => 'datetime')), 'second' => t('Second', array(), array('context' => 'datetime')), ); } /** * Sorts a granularity array. * * @param array $granularity * An array of date parts. */ function date_granularity_sorted($granularity) { return array_intersect(array('year', 'month', 'day', 'hour', 'minute', 'second'), $granularity); } /** * Constructs an array of granularity based on a given precision. * * @param string $precision * A granularity item. * * @return array * A granularity array containing the given precision and all those above it. * For example, passing in 'month' will return array('year', 'month'). */ function date_granularity_array_from_precision($precision) { $granularity_array = array('year', 'month', 'day', 'hour', 'minute', 'second'); switch ($precision) { case 'year': return array_slice($granularity_array, -6, 1); case 'month': return array_slice($granularity_array, -6, 2); case 'day': return array_slice($granularity_array, -6, 3); case 'hour': return array_slice($granularity_array, -6, 4); case 'minute': return array_slice($granularity_array, -6, 5); default: return $granularity_array; } } /** * Give a granularity array, return the highest precision. * * @param array $granularity_array * An array of date parts. * * @return string * The most precise element in a granularity array. */ function date_granularity_precision($granularity_array) { $input = date_granularity_sorted($granularity_array); return array_pop($input); } /** * Constructs a valid DATETIME format string for the granularity of an item. * * @todo This function is no longer used as of * http://drupalcode.org/project/date.git/commit/07efbb5. */ function date_granularity_format($granularity) { if (is_array($granularity)) { $granularity = date_granularity_precision($granularity); } $format = 'Y-m-d H:i:s'; switch ($granularity) { case 'year': return substr($format, 0, 1); case 'month': return substr($format, 0, 3); case 'day': return substr($format, 0, 5); case 'hour'; return substr($format, 0, 7); case 'minute': return substr($format, 0, 9); default: return $format; } } /** * Returns a translated array of timezone names. * * Cache the untranslated array, make the translated array a static variable. * * @param bool $required * (optional) If FALSE, the returned array will include a blank value. * Defaults to FALSE. * @param bool $refresh * (optional) Whether to refresh the list. Defaults to TRUE. * * @return array * An array of timezone names. */ function date_timezone_names($required = FALSE, $refresh = FALSE) { static $zonenames; if (empty($zonenames) || $refresh) { $cached = cache_get('date_timezone_identifiers_list'); $zonenames = !empty($cached) ? $cached->data : array(); if ($refresh || empty($cached) || empty($zonenames)) { $data = timezone_identifiers_list(); asort($data); foreach ($data as $delta => $zone) { // Because many timezones exist in PHP only for backward compatibility // reasons and should not be used, the list is filtered by a regular // expression. if (preg_match('!^((Africa|America|Antarctica|Arctic|Asia|Atlantic|Australia|Europe|Indian|Pacific)/|UTC$)!', $zone)) { $zonenames[$zone] = $zone; } } if (!empty($zonenames)) { cache_set('date_timezone_identifiers_list', $zonenames); } } foreach ($zonenames as $zone) { $zonenames[$zone] = t('!timezone', array('!timezone' => t($zone))); } } $none = array('' => ''); return !$required ? $none + $zonenames : $zonenames; } /** * Returns an array of system-allowed timezone abbreviations. * * Cache an array of just the abbreviation names because the whole * timezone_abbreviations_list() is huge, so we don't want to retrieve it more * than necessary. * * @param bool $refresh * (optional) Whether to refresh the list. Defaults to TRUE. * * @return array * An array of allowed timezone abbreviations. */ function date_timezone_abbr($refresh = FALSE) { $cached = cache_get('date_timezone_abbreviations'); $data = isset($cached->data) ? $cached->data : array(); if (empty($data) || $refresh) { $data = array_keys(timezone_abbreviations_list()); cache_set('date_timezone_abbreviations', $data); } return $data; } /** * Formats a date, using a date type or a custom date format string. * * Reworked from Drupal's format_date function to handle pre-1970 and * post-2038 dates and accept a date object instead of a timestamp as input. * Translates formatted date results, unlike PHP function date_format(). * Should only be used for display, not input, because it can't be parsed. * * @param object $date * A date object. * @param string $type * (optional) The date format to use. Can be 'small', 'medium' or 'large' for * the preconfigured date formats. If 'custom' is specified, then $format is * required as well. Defaults to 'medium'. * @param string $format * (optional) A PHP date format string as required by date(). A backslash * should be used before a character to avoid interpreting the character as * part of a date format. Defaults to an empty string. * @param string $langcode * (optional) Language code to translate to. Defaults to NULL. * * @return string * A translated date string in the requested format. * * @see format_date() */ function date_format_date($date, $type = 'medium', $format = '', $langcode = NULL) { if (empty($date)) { return ''; } if ($type != 'custom') { $format = variable_get('date_format_' . $type); } if ($type != 'custom' && empty($format)) { $format = variable_get('date_format_medium', 'D, m/d/Y - H:i'); } $format = date_limit_format($format, $date->granularity); $max = strlen($format); $datestring = ''; for ($i = 0; $i < $max; $i++) { $c = $format[$i]; switch ($c) { case 'l': $datestring .= t($date->format('l'), array(), array('context' => '', 'langcode' => $langcode)); break; case 'D': $datestring .= t($date->format('D'), array(), array('context' => '', 'langcode' => $langcode)); break; case 'F': $datestring .= t($date->format('F'), array(), array('context' => 'Long month name', 'langcode' => $langcode)); break; case 'M': $datestring .= t($date->format('M'), array(), array('langcode' => $langcode)); break; case 'A': case 'a': $datestring .= t($date->format($c), array(), array('context' => 'ampm', 'langcode' => $langcode)); break; // The timezone name translations can use t(). case 'e': case 'T': $datestring .= t($date->format($c)); break; // Remaining date parts need no translation. case 'O': $datestring .= sprintf('%s%02d%02d', (date_offset_get($date) < 0 ? '-' : '+'), abs(date_offset_get($date) / 3600), abs(date_offset_get($date) % 3600) / 60); break; case 'P': $datestring .= sprintf('%s%02d:%02d', (date_offset_get($date) < 0 ? '-' : '+'), abs(date_offset_get($date) / 3600), abs(date_offset_get($date) % 3600) / 60); break; case 'Z': $datestring .= date_offset_get($date); break; case '\\': $datestring .= $format[++$i]; break; case 'r': $datestring .= date_format_date($date, 'custom', 'D, d M Y H:i:s O', $langcode); break; default: if (strpos('BdcgGhHiIjLmnNosStTuUwWYyz', $c) !== FALSE) { $datestring .= $date->format($c); } else { $datestring .= $c; } } } return $datestring; } /** * Formats a time interval with granularity, including past and future context. * * @param object $date * The current date object. * @param int $granularity * (optional) Number of units to display in the string. Defaults to 2. * * @return string * A translated string representation of the interval. * * @see format_interval() */ function date_format_interval($date, $granularity = 2, $display_ago = TRUE) { // If no date is sent, then return nothing. if (empty($date)) { return NULL; } $interval = REQUEST_TIME - $date->format('U'); if ($interval > 0) { return $display_ago ? t('!time ago', array('!time' => format_interval($interval, $granularity))) : t('!time', array('!time' => format_interval($interval, $granularity))); } else { return format_interval(abs($interval), $granularity); } } /** * A date object for the current time. * * @param object $timezone * (optional) Optionally force time to a specific timezone, defaults to user * timezone, if set, otherwise site timezone. Defaults to NULL. * * @param boolean $reset [optional] * Static cache reset * * @return object * The current time as a date object. */ function date_now($timezone = NULL, $reset = FALSE) { if ($reset) { drupal_static_reset(__FUNCTION__ . $timezone); } $now = &drupal_static(__FUNCTION__ . $timezone); if (!isset($now)) { $now = new DateObject('now', $timezone); } // Avoid unexpected manipulation of cached $now object // by subsequent code execution // @see https://drupal.org/node/2261395 $clone = clone $now; return $clone; } /** * Determines if a timezone string is valid. * * @param string $timezone * A potentially invalid timezone string. * * @return bool * TRUE if the timezone is valid, FALSE otherwise. */ function date_timezone_is_valid($timezone) { static $timezone_names; if (empty($timezone_names)) { $timezone_names = array_keys(date_timezone_names(TRUE)); } return in_array($timezone, $timezone_names); } /** * Returns a timezone name to use as a default. * * @param bool $check_user * (optional) Whether or not to check for a user-configured timezone. * Defaults to TRUE. * * @return string * The default timezone for a user, if available, otherwise the site. */ function date_default_timezone($check_user = TRUE) { global $user; if ($check_user && variable_get('configurable_timezones', 1) && !empty($user->timezone)) { return $user->timezone; } else { $default = variable_get('date_default_timezone', ''); return empty($default) ? 'UTC' : $default; } } /** * Returns a timezone object for the default timezone. * * @param bool $check_user * (optional) Whether or not to check for a user-configured timezone. * Defaults to TRUE. * * @return object * The default timezone for a user, if available, otherwise the site. */ function date_default_timezone_object($check_user = TRUE) { return timezone_open(date_default_timezone($check_user)); } /** * Identifies the number of days in a month for a date. */ function date_days_in_month($year, $month) { // Pick a day in the middle of the month to avoid timezone shifts. $datetime = date_pad($year, 4) . '-' . date_pad($month) . '-15 00:00:00'; $date = new DateObject($datetime); return $date->format('t'); } /** * Identifies the number of days in a year for a date. * * @param mixed $date * (optional) The current date object, or a date string. Defaults to NULL. * * @return integer * The number of days in the year. */ function date_days_in_year($date = NULL) { if (empty($date)) { $date = date_now(); } elseif (!is_object($date)) { $date = new DateObject($date); } if (is_object($date)) { if ($date->format('L')) { return 366; } else { return 365; } } return NULL; } /** * Identifies the number of ISO weeks in a year for a date. * * December 28 is always in the last ISO week of the year. * * @param mixed $date * (optional) The current date object, or a date string. Defaults to NULL. * * @return integer * The number of ISO weeks in a year. */ function date_iso_weeks_in_year($date = NULL) { if (empty($date)) { $date = date_now(); } elseif (!is_object($date)) { $date = new DateObject($date); } if (is_object($date)) { date_date_set($date, $date->format('Y'), 12, 28); return $date->format('W'); } return NULL; } /** * Returns day of week for a given date (0 = Sunday). * * @param mixed $date * (optional) A date, default is current local day. Defaults to NULL. * * @return int * The number of the day in the week. */ function date_day_of_week($date = NULL) { if (empty($date)) { $date = date_now(); } elseif (!is_object($date)) { $date = new DateObject($date); } if (is_object($date)) { return $date->format('w'); } return NULL; } /** * Returns translated name of the day of week for a given date. * * @param mixed $date * (optional) A date, default is current local day. Defaults to NULL. * @param string $abbr * (optional) Whether to return the abbreviated name for that day. * Defaults to TRUE. * * @return string * The name of the day in the week for that date. */ function date_day_of_week_name($date = NULL, $abbr = TRUE) { if (!is_object($date)) { $date = new DateObject($date); } $dow = date_day_of_week($date); $days = $abbr ? date_week_days_abbr() : date_week_days(); return $days[$dow]; } /** * Calculates the start and end dates for a calendar week. * * The dates are adjusted to use the chosen first day of week for this site. * * @param int $week * The week value. * @param int $year * The year value. * * @return array * A numeric array containing the start and end dates of a week. */ function date_week_range($week, $year) { if (variable_get('date_api_use_iso8601', FALSE)) { return date_iso_week_range($week, $year); } $min_date = new DateObject($year . '-01-01 00:00:00'); $min_date->setTimezone(date_default_timezone_object()); // Move to the right week. date_modify($min_date, '+' . strval(7 * ($week - 1)) . ' days'); // Move backwards to the first day of the week. $first_day = variable_get('date_first_day', 0); $day_wday = date_format($min_date, 'w'); date_modify($min_date, '-' . strval((7 + $day_wday - $first_day) % 7) . ' days'); // Move forwards to the last day of the week. $max_date = clone($min_date); date_modify($max_date, '+7 days'); if (date_format($min_date, 'Y') != $year) { $min_date = new DateObject($year . '-01-01 00:00:00'); } return array($min_date, $max_date); } /** * Calculates the start and end dates for an ISO week. * * @param int $week * The week value. * @param int $year * The year value. * * @return array * A numeric array containing the start and end dates of an ISO week. */ function date_iso_week_range($week, $year) { // Get to the last ISO week of the previous year. $min_date = new DateObject(($year - 1) . '-12-28 00:00:00'); date_timezone_set($min_date, date_default_timezone_object()); // Find the first day of the first ISO week in the year. date_modify($min_date, '+1 Monday'); // Jump ahead to the desired week for the beginning of the week range. if ($week > 1) { date_modify($min_date, '+ ' . ($week - 1) . ' weeks'); } // Move forwards to the last day of the week. $max_date = clone($min_date); date_modify($max_date, '+7 days'); return array($min_date, $max_date); } /** * The number of calendar weeks in a year. * * PHP week functions return the ISO week, not the calendar week. * * @param int $year * A year value. * * @return int * Number of calendar weeks in selected year. */ function date_weeks_in_year($year) { $date = new DateObject(($year + 1) . '-01-01 12:00:00', 'UTC'); date_modify($date, '-1 day'); return date_week($date->format('Y-m-d')); } /** * The calendar week number for a date. * * PHP week functions return the ISO week, not the calendar week. * * @param string $date * A date string in the format Y-m-d. * * @return int * The calendar week number. */ function date_week($date) { $date = substr($date, 0, 10); $parts = explode('-', $date); $date = new DateObject($date . ' 12:00:00', 'UTC'); // If we are using ISO weeks, this is easy. if (variable_get('date_api_use_iso8601', FALSE)) { return intval($date->format('W')); } $year_date = new DateObject($parts[0] . '-01-01 12:00:00', 'UTC'); $week = intval($date->format('W')); $year_week = intval(date_format($year_date, 'W')); $date_year = intval($date->format('o')); // Remove the leap week if it's present. if ($date_year > intval($parts[0])) { $last_date = clone($date); date_modify($last_date, '-7 days'); $week = date_format($last_date, 'W') + 1; } elseif ($date_year < intval($parts[0])) { $week = 0; } if ($year_week != 1) { $week++; } // Convert to ISO-8601 day number, to match weeks calculated above. $iso_first_day = 1 + (variable_get('date_first_day', 0) + 6) % 7; // If it's before the starting day, it's the previous week. if (intval($date->format('N')) < $iso_first_day) { $week--; } // If the year starts before, it's an extra week at the beginning. if (intval(date_format($year_date, 'N')) < $iso_first_day) { $week++; } return $week; } /** * Helper function to left pad date parts with zeros. * * Provided because this is needed so often with dates. * * @param int $value * The value to pad. * @param int $size * (optional) Total size expected, usually 2 or 4. Defaults to 2. * * @return string * The padded value. */ function date_pad($value, $size = 2) { return sprintf("%0" . $size . "d", $value); } /** * Determines if the granularity contains a time portion. * * @param array $granularity * An array of allowed date parts, all others will be removed. * * @return bool * TRUE if the granularity contains a time portion, FALSE otherwise. */ function date_has_time($granularity) { if (!is_array($granularity)) { $granularity = array(); } return (bool) count(array_intersect($granularity, array('hour', 'minute', 'second'))); } /** * Determines if the granularity contains a date portion. * * @param array $granularity * An array of allowed date parts, all others will be removed. * * @return bool * TRUE if the granularity contains a date portion, FALSE otherwise. */ function date_has_date($granularity) { if (!is_array($granularity)) { $granularity = array(); } return (bool) count(array_intersect($granularity, array('year', 'month', 'day'))); } /** * Helper function to get a format for a specific part of a date field. * * @param string $part * The date field part, either 'time' or 'date'. * @param string $format * A date format string. * * @return string * The date format for the given part. */ function date_part_format($part, $format) { switch ($part) { case 'date': return date_limit_format($format, array('year', 'month', 'day')); case 'time': return date_limit_format($format, array('hour', 'minute', 'second')); default: return date_limit_format($format, array($part)); } } /** * Limits a date format to include only elements from a given granularity array. * * Example: * date_limit_format('F j, Y - H:i', array('year', 'month', 'day')); * returns 'F j, Y' * * @param string $format * A date format string. * @param array $granularity * An array of allowed date parts, all others will be removed. * * @return string * The format string with all other elements removed. */ function date_limit_format($format, $granularity) { // Use the advanced drupal_static() pattern to improve performance. static $drupal_static_fast; if (!isset($drupal_static_fast)) { $drupal_static_fast['formats'] = &drupal_static(__FUNCTION__); } $formats = &$drupal_static_fast['formats']; $format_granularity_cid = $format .'|'. implode(',', $granularity); if (isset($formats[$format_granularity_cid])) { return $formats[$format_granularity_cid]; } // If punctuation has been escaped, remove the escaping. Done using strtr() // because it is easier than getting the escape character extracted using // preg_replace(). $replace = array( '\-' => '-', '\:' => ':', "\'" => "'", '\. ' => ' . ', '\,' => ',', ); $format = strtr($format, $replace); // Get the 'T' out of ISO date formats that don't have both date and time. if (!date_has_time($granularity) || !date_has_date($granularity)) { $format = str_replace('\T', ' ', $format); $format = str_replace('T', ' ', $format); } $regex = array(); if (!date_has_time($granularity)) { $regex[] = '((? 'month', * 1 => 'day', * 2 => 'year', * 3 => 'hour', * 4 => 'minute', * ); * * @param string $format * A date format string. * * @return array * An array of ordered granularity elements from the given format string. */ function date_format_order($format) { $order = array(); if (empty($format)) { return $order; } $max = strlen($format); for ($i = 0; $i <= $max; $i++) { if (!isset($format[$i])) { break; } switch ($format[$i]) { case 'd': case 'j': $order[] = 'day'; break; case 'F': case 'M': case 'm': case 'n': $order[] = 'month'; break; case 'Y': case 'y': $order[] = 'year'; break; case 'g': case 'G': case 'h': case 'H': $order[] = 'hour'; break; case 'i': $order[] = 'minute'; break; case 's': $order[] = 'second'; break; } } return $order; } /** * Strips out unwanted granularity elements. * * @param array $granularity * An array like ('year', 'month', 'day', 'hour', 'minute', 'second'); * * @return array * A reduced set of granularitiy elements. */ function date_nongranularity($granularity) { return array_diff(array('year', 'month', 'day', 'hour', 'minute', 'second', 'timezone'), (array) $granularity); } /** * Implements hook_element_info(). */ function date_api_element_info() { module_load_include('inc', 'date_api', 'date_api_elements'); return _date_api_element_info(); } /** * Implements hook_theme(). */ function date_api_theme($existing, $type, $theme, $path) { $base = array( 'file' => 'theme.inc', 'path' => "$path/theme", ); return array( 'date_nav_title' => $base + array('variables' => array('granularity' => NULL, 'view' => NULL, 'link' => NULL, 'format' => NULL)), 'date_timezone' => $base + array('render element' => 'element'), 'date_select' => $base + array('render element' => 'element'), 'date_text' => $base + array('render element' => 'element'), 'date_select_element' => $base + array('render element' => 'element'), 'date_textfield_element' => $base + array('render element' => 'element'), 'date_part_hour_prefix' => $base + array('render element' => 'element'), 'date_part_minsec_prefix' => $base + array('render element' => 'element'), 'date_part_label_year' => $base + array('variables' => array('date_part' => NULL, 'element' => NULL)), 'date_part_label_month' => $base + array('variables' => array('date_part' => NULL, 'element' => NULL)), 'date_part_label_day' => $base + array('variables' => array('date_part' => NULL, 'element' => NULL)), 'date_part_label_hour' => $base + array('variables' => array('date_part' => NULL, 'element' => NULL)), 'date_part_label_minute' => $base + array('variables' => array('date_part' => NULL, 'element' => NULL)), 'date_part_label_second' => $base + array('variables' => array('date_part' => NULL, 'element' => NULL)), 'date_part_label_ampm' => $base + array('variables' => array('date_part' => NULL, 'element' => NULL)), 'date_part_label_timezone' => $base + array('variables' => array('date_part' => NULL, 'element' => NULL)), 'date_part_label_date' => $base + array('variables' => array('date_part' => NULL, 'element' => NULL)), 'date_part_label_time' => $base + array('variables' => array('date_part' => NULL, 'element' => NULL)), 'date_views_filter_form' => $base + array('template' => 'date-views-filter-form', 'render element' => 'form'), 'date_calendar_day' => $base + array('variables' => array('date' => NULL)), 'date_time_ago' => $base + array('variables' => array('start_date' => NULL, 'end_date' => NULL, 'interval' => NULL)), ); } /** * Function to figure out which local timezone applies to a date and select it. * * @param string $handling * The timezone handling. * @param string $timezone * (optional) A timezone string. Defaults to an empty string. * * @return string * The timezone string. */ function date_get_timezone($handling, $timezone = '') { switch ($handling) { case 'date': $timezone = !empty($timezone) ? $timezone : date_default_timezone(); break; case 'utc': $timezone = 'UTC'; break; default: $timezone = date_default_timezone(); } return $timezone > '' ? $timezone : date_default_timezone(); } /** * Function to figure out which db timezone applies to a date. * * @param string $handling * The timezone handling. * @param string $timezone * (optional) When $handling is 'date', date_get_timezone_db() returns this * value. * * @return string * The timezone string. */ function date_get_timezone_db($handling, $timezone = NULL) { switch ($handling) { case ('utc'): case ('site'): case ('user'): // These handling modes all convert to UTC before storing in the DB. $timezone = 'UTC'; break; case ('date'): if ($timezone == NULL) { // This shouldn't happen, since it's meaning is undefined. But we need // to fall back to *something* that's a legal timezone. $timezone = date_default_timezone(); } break; case ('none'): default: $timezone = date_default_timezone(); break; } return $timezone; } /** * Helper function for converting back and forth from '+1' to 'First'. */ function date_order_translated() { return array( '+1' => t('First', array(), array('context' => 'date_order')), '+2' => t('Second', array(), array('context' => 'date_order')), '+3' => t('Third', array(), array('context' => 'date_order')), '+4' => t('Fourth', array(), array('context' => 'date_order')), '+5' => t('Fifth', array(), array('context' => 'date_order')), '-1' => t('Last', array(), array('context' => 'date_order_reverse')), '-2' => t('Next to last', array(), array('context' => 'date_order_reverse')), '-3' => t('Third from last', array(), array('context' => 'date_order_reverse')), '-4' => t('Fourth from last', array(), array('context' => 'date_order_reverse')), '-5' => t('Fifth from last', array(), array('context' => 'date_order_reverse')), ); } /** * Creates an array of ordered strings, using English text when possible. */ function date_order() { return array( '+1' => 'First', '+2' => 'Second', '+3' => 'Third', '+4' => 'Fourth', '+5' => 'Fifth', '-1' => 'Last', '-2' => '-2', '-3' => '-3', '-4' => '-4', '-5' => '-5', ); } /** * Tests validity of a date range string. * * @param string $string * A min and max year string like '-3:+1'a. * * @return bool * TRUE if the date range is valid, FALSE otherwise. */ function date_range_valid($string) { $matches = preg_match('@^(\-[0-9]+|[0-9]{4}):([\+|\-][0-9]+|[0-9]{4})$@', $string); return $matches < 1 ? FALSE : TRUE; } /** * Splits a string like -3:+3 or 2001:2010 into an array of min and max years. * * Center the range around the current year, if any, but expand it far * enough so it will pick up the year value in the field in case * the value in the field is outside the initial range. * * @param string $string * A min and max year string like '-3:+1'. * @param object $date * (optional) A date object. Defaults to NULL. * * @return array * A numerically indexed array, containing a minimum and maximum year. */ function date_range_years($string, $date = NULL) { $this_year = date_format(date_now(), 'Y'); list($min_year, $max_year) = explode(':', $string); // Valid patterns would be -5:+5, 0:+1, 2008:2010. $plus_pattern = '@[\+|\-][0-9]{1,4}@'; $year_pattern = '@^[0-9]{4}@'; if (!preg_match($year_pattern, $min_year, $matches)) { if (preg_match($plus_pattern, $min_year, $matches)) { $min_year = $this_year + $matches[0]; } else { $min_year = $this_year; } } if (!preg_match($year_pattern, $max_year, $matches)) { if (preg_match($plus_pattern, $max_year, $matches)) { $max_year = $this_year + $matches[0]; } else { $max_year = $this_year; } } // We expect the $min year to be less than the $max year. // Some custom values for -99:+99 might not obey that. if ($min_year > $max_year) { $temp = $max_year; $max_year = $min_year; $min_year = $temp; } // If there is a current value, stretch the range to include it. $value_year = is_object($date) ? $date->format('Y') : ''; if (!empty($value_year)) { $min_year = min($value_year, $min_year); $max_year = max($value_year, $max_year); } return array($min_year, $max_year); } /** * Converts a min and max year into a string like '-3:+1'. * * @param array $years * A numerically indexed array, containing a minimum and maximum year. * * @return string * A min and max year string like '-3:+1'. */ function date_range_string($years) { $this_year = date_format(date_now(), 'Y'); if ($years[0] < $this_year) { $min = '-' . ($this_year - $years[0]); } else { $min = '+' . ($years[0] - $this_year); } if ($years[1] < $this_year) { $max = '-' . ($this_year - $years[1]); } else { $max = '+' . ($years[1] - $this_year); } return $min . ':' . $max; } /** * Temporary helper to re-create equivalent of content_database_info(). */ function date_api_database_info($field, $revision = FIELD_LOAD_CURRENT) { return array( 'columns' => $field['storage']['details']['sql'][$revision], 'table' => _field_sql_storage_tablename($field), ); } /** * Implements hook_form_FORM_ID_alter() for system_regional_settings(). * * Add a form element to configure whether or not week numbers are ISO-8601, the * default is FALSE (US/UK/AUS norm). */ function date_api_form_system_regional_settings_alter(&$form, &$form_state, $form_id) { $form['locale']['date_api_use_iso8601'] = array( '#type' => 'checkbox', '#title' => t('Use ISO-8601 week numbers'), '#default_value' => variable_get('date_api_use_iso8601', FALSE), '#description' => t('IMPORTANT! If checked, First day of week MUST be set to Monday'), ); $form['#validate'][] = 'date_api_form_system_settings_validate'; } /** * Validate that the option to use ISO weeks matches first day of week choice. */ function date_api_form_system_settings_validate(&$form, &$form_state) { $form_values = $form_state['values']; if ($form_values['date_api_use_iso8601'] && $form_values['date_first_day'] != 1) { form_set_error('date_first_day', t('When using ISO-8601 week numbers, the first day of the week must be set to Monday.')); } } /** * Creates an array of date format types for use as an options list. */ function date_format_type_options() { $options = array(); $format_types = system_get_date_types(); if (!empty($format_types)) { foreach ($format_types as $type => $type_info) { $options[$type] = $type_info['title'] . ' (' . date_format_date(date_example_date(), $type) . ')'; } } return $options; } /** * Creates an example date. * * This ensures a clear difference between month and day, and 12 and 24 hours. */ function date_example_date() { $now = date_now(); if (date_format($now, 'M') == date_format($now, 'F')) { date_modify($now, '+1 month'); } if (date_format($now, 'm') == date_format($now, 'd')) { date_modify($now, '+1 day'); } if (date_format($now, 'H') == date_format($now, 'h')) { date_modify($now, '+12 hours'); } return $now; } /** * Determine if a start/end date combination qualify as 'All day'. * * @param string $string1 * A string date in datetime format for the 'start' date. * @param string $string2 * A string date in datetime format for the 'end' date. * @param string $granularity * (optional) The granularity of the date. Defaults to 'second'. * @param int $increment * (optional) The increment of the date. Defaults to 1. * * @return bool * TRUE if the date is all day, FALSE otherwise. */ function date_is_all_day($string1, $string2, $granularity = 'second', $increment = 1) { if (empty($string1) || empty($string2)) { return FALSE; } elseif (!in_array($granularity, array('hour', 'minute', 'second'))) { return FALSE; } preg_match('/([0-9]{4}-[0-9]{2}-[0-9]{2}) (([0-9]{2}):([0-9]{2}):([0-9]{2}))/', $string1, $matches); $count = count($matches); $date1 = $count > 1 ? $matches[1] : ''; $time1 = $count > 2 ? $matches[2] : ''; $hour1 = $count > 3 ? intval($matches[3]) : 0; $min1 = $count > 4 ? intval($matches[4]) : 0; $sec1 = $count > 5 ? intval($matches[5]) : 0; preg_match('/([0-9]{4}-[0-9]{2}-[0-9]{2}) (([0-9]{2}):([0-9]{2}):([0-9]{2}))/', $string2, $matches); $count = count($matches); $date2 = $count > 1 ? $matches[1] : ''; $time2 = $count > 2 ? $matches[2] : ''; $hour2 = $count > 3 ? intval($matches[3]) : 0; $min2 = $count > 4 ? intval($matches[4]) : 0; $sec2 = $count > 5 ? intval($matches[5]) : 0; if (empty($date1) || empty($date2)) { return FALSE; } if (empty($time1) || empty($time2)) { return FALSE; } $tmp = date_seconds('s', TRUE, $increment); $max_seconds = intval(array_pop($tmp)); $tmp = date_minutes('i', TRUE, $increment); $max_minutes = intval(array_pop($tmp)); // See if minutes and seconds are the maximum allowed for an increment or the // maximum possible (59), or 0. switch ($granularity) { case 'second': $min_match = $time1 == '00:00:00' || ($hour1 == 0 && $min1 == 0 && $sec1 == 0); $max_match = $time2 == '00:00:00' || ($hour2 == 23 && in_array($min2, array($max_minutes, 59)) && in_array($sec2, array($max_seconds, 59))) || ($hour1 == 0 && $hour2 == 0 && $min1 == 0 && $min2 == 0 && $sec1 == 0 && $sec2 == 0); break; case 'minute': $min_match = $time1 == '00:00:00' || ($hour1 == 0 && $min1 == 0); $max_match = $time2 == '00:00:00' || ($hour2 == 23 && in_array($min2, array($max_minutes, 59))) || ($hour1 == 0 && $hour2 == 0 && $min1 == 0 && $min2 == 0); break; case 'hour': $min_match = $time1 == '00:00:00' || ($hour1 == 0); $max_match = $time2 == '00:00:00' || ($hour2 == 23) || ($hour1 == 0 && $hour2 == 0); break; default: $min_match = TRUE; $max_match = FALSE; } if ($min_match && $max_match) { return TRUE; } return FALSE; } /** * Helper function to round minutes and seconds to requested value. */ function date_increment_round(&$date, $increment) { // Round minutes and seconds, if necessary. if (is_object($date) && $increment > 1) { $day = intval(date_format($date, 'j')); $hour = intval(date_format($date, 'H')); $second = intval(round(intval(date_format($date, 's')) / $increment) * $increment); $minute = intval(date_format($date, 'i')); if ($second == 60) { $minute += 1; $second = 0; } $minute = intval(round($minute / $increment) * $increment); if ($minute == 60) { $hour += 1; $minute = 0; } date_time_set($date, $hour, $minute, $second); if ($hour == 24) { $day += 1; $hour = 0; $year = date_format($date, 'Y'); $month = date_format($date, 'n'); date_date_set($date, $year, $month, $day); } } return $date; } /** * Determines if a date object is valid. * * @param object $date * The date object to check. * * @return bool * TRUE if the date is a valid date object, FALSE otherwise. */ function date_is_date($date) { if (empty($date) || !is_object($date) || !empty($date->errors)) { return FALSE; } return TRUE; } /** * This function will replace ISO values that have the pattern 9999-00-00T00:00:00 * with a pattern like 9999-01-01T00:00:00, to match the behavior of non-ISO * dates and ensure that date objects created from this value contain a valid month * and day. Without this fix, the ISO date '2020-00-00T00:00:00' would be created as * November 30, 2019 (the previous day in the previous month). * * @param string $iso_string * An ISO string that needs to be made into a complete, valid date. * * @TODO Expand on this to work with all sorts of partial ISO dates. */ function date_make_iso_valid($iso_string) { // If this isn't a value that uses an ISO pattern, there is nothing to do. if (is_numeric($iso_string) || !preg_match(DATE_REGEX_ISO, $iso_string)) { return $iso_string; } // First see if month and day parts are '-00-00'. if (substr($iso_string, 4, 6) == '-00-00') { return preg_replace('/([\d]{4}-)(00-00)(T[\d]{2}:[\d]{2}:[\d]{2})/', '${1}01-01${3}', $iso_string); } // Then see if the day part is '-00'. elseif (substr($iso_string, 7, 3) == '-00') { return preg_replace('/([\d]{4}-[\d]{2}-)(00)(T[\d]{2}:[\d]{2}:[\d]{2})/', '${1}01${3}', $iso_string); } // Fall through, no changes required. return $iso_string; }