2015-04-26 18:38:56 +02:00

2785 lines
87 KiB
Plaintext

<?php
/**
* @file
* This module will make the date API available to other modules.
* Designed to provide a light but flexible assortment of functions
* and constants, with more functionality in additional files that
* are not loaded unless other modules specifically include them.
*/
/**
* Set up some constants.
*
* Includes standard date types, format strings, strict regex strings for ISO
* and DATETIME formats (seconds are optional).
*
* The loose regex will find any variety of ISO date and time, with or
* without time, with or without dashes and colons separating the elements,
* and with either a 'T' or a space separating date and time.
*/
define('DATE_ISO', 'date');
define('DATE_UNIX', 'datestamp');
define('DATE_DATETIME', 'datetime');
define('DATE_ARRAY', 'array');
define('DATE_OBJECT', 'object');
define('DATE_ICAL', 'ical');
define('DATE_FORMAT_ISO', "Y-m-d\TH:i:s");
define('DATE_FORMAT_UNIX', "U");
define('DATE_FORMAT_DATETIME', "Y-m-d H:i:s");
define('DATE_FORMAT_ICAL', "Ymd\THis");
define('DATE_FORMAT_ICAL_DATE', "Ymd");
define('DATE_FORMAT_DATE', 'Y-m-d');
define('DATE_REGEX_ISO', '/(\d{4})?(-(\d{2}))?(-(\d{2}))?([T\s](\d{2}))?(:(\d{2}))?(:(\d{2}))?/');
define('DATE_REGEX_DATETIME', '/(\d{4})-(\d{2})-(\d{2})\s(\d{2}):(\d{2}):?(\d{2})?/');
define('DATE_REGEX_LOOSE', '/(\d{4})-?(\d{1,2})-?(\d{1,2})([T\s]?(\d{2}):?(\d{2}):?(\d{2})?(\.\d+)?(Z|[\+\-]\d{2}:?\d{2})?)?/');
define('DATE_REGEX_ICAL_DATE', '/(\d{4})(\d{2})(\d{2})/');
define('DATE_REGEX_ICAL_DATETIME', '/(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})(Z)?/');
/**
* Core DateTime extension module used for as many date operations as possible.
*/
/**
* Implements hook_help().
*/
function date_help($path, $arg) {
switch ($path) {
case 'admin/help#date':
$output = '';
$messages = date_api_status();
$output = '<h2>Date API Status</h2>';
if (!empty($messages['success'])) {
$output .= '<ul><li>' . implode('</li><li>', $messages['success']) . '</li></ul>';
}
if (!empty($messages['errors'])) {
$output .= '<h3>Errors</h3><ul class="error"><li>' . implode('</li><li>', $messages['errors']) . '</li></ul>';
}
if (module_exists('date_tools')) {
$output .= '<h3>Date Tools</h3>' . t('Dates and calendars can be complicated to set up. The !date_wizard makes it easy to create a simple date content type and with a date field. ', array('!date_wizard' => l(t('Date wizard'), 'admin/config/date/tools/date_wizard')));
}
else {
$output .= '<h3>Date Tools</h3>' . t('Dates and calendars can be complicated to set up. If you enable the Date Tools module, it provides a Date Wizard that makes it easy to create a simple date content type with a date field. ');
}
$output .= '<h2>More Information</h2><p>' . t('Complete documentation for the Date and Date API modules is available at <a href="@link">http://drupal.org/node/92460</a>.', array('@link' => 'http://drupal.org/node/262062')) . '</p>';
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 <a href="@regional_settings">@timezone</a>.', array('@regional_settings' => url('admin/config/regional/settings'), '@timezone' => $value));
}
else {
$error_messages[] = $t('The Date API requires that you set up the <a href="@regional_settings">site timezone</a> 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 <a href="@regional_settings">@day</a>.', array('@regional_settings' => url('admin/config/regional/settings'), '@day' => $days[$value]));
}
else {
$error_messages[] = $t('The Date API requires that you set up the <a href="@regional_settings">site first day of week settings</a> 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 <a href="@regional_date_time">Date and time</a> 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 <a href="@regional_date_time">system date formats</a> 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[] = '((?<!\\\\)[a|A])';
}
// Create regular expressions to remove selected values from string.
// Use (?<!\\\\) to keep escaped letters from being removed.
foreach (date_nongranularity($granularity) as $element) {
switch ($element) {
case 'year':
$regex[] = '([\-/\.,:]?\s?(?<!\\\\)[Yy])';
break;
case 'day':
$regex[] = '([\-/\.,:]?\s?(?<!\\\\)[l|D|d|dS|j|jS|N|w|W|z]{1,2})';
break;
case 'month':
$regex[] = '([\-/\.,:]?\s?(?<!\\\\)[FMmn])';
break;
case 'hour':
$regex[] = '([\-/\.,:]?\s?(?<!\\\\)[HhGg])';
break;
case 'minute':
$regex[] = '([\-/\.,:]?\s?(?<!\\\\)[i])';
break;
case 'second':
$regex[] = '([\-/\.,:]?\s?(?<!\\\\)[s])';
break;
case 'timezone':
$regex[] = '([\-/\.,:]?\s?(?<!\\\\)[TOZPe])';
break;
}
}
// Remove empty parentheses, brackets, pipes.
$regex[] = '(\(\))';
$regex[] = '(\[\])';
$regex[] = '(\|\|)';
// Remove selected values from string.
$format = trim(preg_replace($regex, array(), $format));
// Remove orphaned punctuation at the beginning of the string.
$format = preg_replace('`^([\-/\.,:\'])`', '', $format);
// Remove orphaned punctuation at the end of the string.
$format = preg_replace('([\-/,:\']$)', '', $format);
$format = preg_replace('(\\$)', '', $format);
// Trim any whitespace from the result.
$format = trim($format);
// After removing the non-desired parts of the format, test if the only things
// left are escaped, non-date, characters. If so, return nothing.
// Using S instead of w to pick up non-ASCII characters.
$test = trim(preg_replace('(\\\\\S{1,3})u', '', $format));
if (empty($test)) {
$format = '';
}
// Store the return value in the static array for performance.
$formats[$format_granularity_cid] = $format;
return $format;
}
/**
* Converts a format to an ordered array of granularity parts.
*
* Example:
* date_format_order('m/d/Y H:i')
* returns
* array(
* 0 => '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;
}