class DateObject in Date 7
Same name and namespace in other branches
- 7.3 date_api/date_api.module \DateObject
- 7.2 date_api/date_api.module \DateObject
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.
Hierarchy
- class \DateObject extends \DateTime
Expanded class hierarchy of DateObject
See also
FeedsDateTimeElement class
File
- date_api/
date_api.module, line 53 - 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.
View source
class DateObject extends DateTime {
public $granularity = array();
public $errors = array();
protected static $allgranularity = array(
'year',
'month',
'day',
'hour',
'minute',
'second',
'timezone',
);
private $_serialized_time;
private $_serialized_timezone;
/**
* Helper function to prepare the object during serialization.
*
* We are extending a core class and core classes cannot be serialized.
*
* Ref: http://bugs.php.net/41334, http://bugs.php.net/39821
*/
public function __sleep() {
$this->_serialized_time = $this
->format('c');
$this->_serialized_timezone = $this
->getTimezone()
->getName();
return array(
'_serialized_time',
'_serialized_timezone',
);
}
/**
* Upon unserializing, we must re-build ourselves using local variables.
*/
public function __wakeup() {
$this
->__construct($this->_serialized_time, new DateTimeZone($this->_serialized_timezone));
}
public function __toString() {
return $this
->format(DATE_FORMAT_DATETIME) . ' ' . $this
->getTimeZone()
->getName();
}
/**
* Overridden constructor.
*
* @param $time
* time string, flexible format including timestamp.
* @param $tz
* PHP DateTimeZone object, string or NULL allowed, defaults to site timezone.
* @param $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;
// Allow string timezones
if (!empty($tz) && !is_object($tz)) {
$tz = new DateTimeZone($tz);
}
elseif (empty($tz)) {
$tz = date_default_timezone_object();
}
// Special handling for Unix timestamps expressed in the local timezone.
// Create a date object in UTC and convert it to the local timezone.
// Don't try to turn things like '2010' with a format of 'Y' into a timestamp.
if (is_numeric($time) && (empty($format) || $format == 'U')) {
// Assume timestamp.
$time = "@" . $time;
if ($tz
->getName() != 'UTC') {
$date = new DateObject($time, 'UTC');
$date
->setTimezone($tz);
$time = $date
->format(DATE_FORMAT_DATETIME);
$format = DATE_FORMAT_DATETIME;
}
}
if (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 the step of parsing the input values.
$format = NULL;
}
// 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;
}
$this
->setGranularityFromTime($time, $tz);
}
// This tz was given as just an offset, which causes problems,
// or the timezone was invalid.
if (!$this
->getTimezone() || !preg_match('/[a-zA-Z]/', $this
->getTimezone()
->getName())) {
$this
->setTimezone(new DateTimeZone("UTC"));
}
}
/**
* This function will keep this object's values by default.
*/
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;
}
/**
* 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.
*/
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);
}
/**
* Overrides base format function, formats this date according to its available granularity,
* unless $force'ed not to limit to granularity.
*
* @TODO Incorporate translation into this so translated names will be provided.
*/
public function format($format, $force = FALSE) {
return parent::format($force ? $format : date_limit_format($format, $this->granularity));
}
/**
* Safely adds a granularity entry to the array.
*/
public function addGranularity($g) {
$this->granularity[] = $g;
$this->granularity = array_unique($this->granularity);
}
/**
* Removes a granularity entry from the array.
*/
public function removeGranularity($g) {
if ($key = array_search($g, $this->granularity)) {
unset($this->granularity[$key]);
}
}
/**
* Checks granularity array for a given entry.
* Accepts an array, in which case all items must be present (AND's the query)
*/
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);
}
// whether a date is valid for a given $granularity array, depending on if it's allowed to be flexible.
public function validGranularity($granularity = NULL, $flexible = FALSE) {
return $this
->hasGranularity() && (!$granularity || $flexible || $this
->hasGranularity($granularity));
}
/**
* Returns whether this object has time set. Used primarily for timezone
* conversion and formatting.
*/
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.
*/
public function completeDate() {
return $this->completeDate;
}
/**
* In common usage we should not unset timezone through this.
*/
public function limitGranularity($gran) {
foreach ($this->granularity as $key => $val) {
if ($val != 'timezone' && !in_array($val, $gran)) {
unset($this->granularity[$key]);
}
}
}
/**
* Protected function to find the granularity given by the arguments to the
* constructor.
*/
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');
}
}
protected function parse($date, $tz, $format) {
$array = date_format_patterns();
foreach ($array as $key => $value) {
$patterns[] = "`(^|[^\\\\\\\\])" . $key . "`";
// the letter with no preceding '\'
$repl1[] = '${1}(.)';
// a single character
$repl2[] = '${1}(' . $value . ')';
// the
}
$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 . '$`', $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)) {
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_month_long[$value];
$this
->addGranularity('month');
break;
case 'M':
$array_month = array_flip(date_month_names_abbr());
$final_date['month'] = $array_month[$value];
$this
->addGranularity('month');
break;
case 'Y':
$final_date['year'] = $value;
$this
->addGranularity('year');
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);
// 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 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)) {
$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;
}
/**
* Helper to return all standard date parts in an array.
* Will return '' for parts in which it lacks granularity.
*/
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),
);
}
/**
* Create an ISO date from an array of values.
*/
public function toISO($arr, $full = FALSE) {
// Add empty values to avoid errors
$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;
}
/**
* Force an incomplete date to be valid, for instance to add
* a valid year, month, and day if only the time has been defined.
*
* @param $date
* An array of date parts or a datetime string with values to be forced into date.
* @param $format
* The format of the date.
* @param $default
* 'current' - default to current day values.
* 'first' - default to the first possible valid value.
*/
public function setFuzzyDate($date, $format = NULL, $default = 'first') {
$comp = new DateObject($date, $this
->getTimeZone()
->getName(), $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']);
}
/**
* Convert a date part into something that will produce a valid date.
*/
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;
}
}
// Find 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.
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');
foreach ($arr as $part => $value) {
// Avoid false errors when a numeric value is input as a string by forcing it numeric.
$value = intval($value);
if (!empty($value) && $this
->forceValid($part, $value, 'now', $default_month, $default_year) != $value) {
// Use a switchcase 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;
}
}
}
return $errors;
}
/**
* Compute difference between two days using a given measure.
*
* @param mixed $date1
* the starting date
* @param mixed $date2
* the ending date
* @param string $measure
* 'years', 'months', 'weeks', 'days', 'hours', 'minutes', 'seconds'
* @param string $type
* the type of dates provided:
* DATE_OBJECT, DATE_DATETIME, DATE_ISO, DATE_UNIX, DATE_ARRAY
*/
public function difference($date2_in, $measure = 'seconds') {
// 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) {
// 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);
}
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);
}
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');
for ($i = 1; $i <= $year_diff; $i++) {
date_modify($date1, '+1 year');
$week_diff += date_iso_weeks_in_year($date1);
}
return $week_diff;
}
}
return NULL;
}
}
Members
Name | Modifiers | Type | Description | Overrides |
---|---|---|---|---|
DateObject:: |
protected static | property | ||
DateObject:: |
public | property | ||
DateObject:: |
public | property | ||
DateObject:: |
private | property | ||
DateObject:: |
private | property | ||
DateObject:: |
public | function | Safely adds a granularity entry to the array. | |
DateObject:: |
public | function | ||
DateObject:: |
public | function | Returns whether the input values included a year. Useful to use pseudo date objects when we only are interested in the time. | |
DateObject:: |
public | function | Compute difference between two days using a given measure. | |
DateObject:: |
protected | function | Convert a date part into something that will produce a valid date. | |
DateObject:: |
public | function | Overrides base format function, formats this date according to its available granularity, unless $force'ed not to limit to granularity. | |
DateObject:: |
public | function | Checks granularity array for a given entry. Accepts an array, in which case all items must be present (AND's the query) | |
DateObject:: |
public | function | Returns whether this object has time set. Used primarily for timezone conversion and formatting. | |
DateObject:: |
public | function | In common usage we should not unset timezone through this. | |
DateObject:: |
public | function | This function will keep this object's values by default. | |
DateObject:: |
protected | function | ||
DateObject:: |
public | function | Removes a granularity entry from the array. | |
DateObject:: |
public | function | Force an incomplete date to be valid, for instance to add a valid year, month, and day if only the time has been defined. | |
DateObject:: |
protected | function | Protected function to find the granularity given by the arguments to the constructor. | |
DateObject:: |
public | function | 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. | |
DateObject:: |
public | function | Helper to return all standard date parts in an array. Will return '' for parts in which it lacks granularity. | |
DateObject:: |
public | function | Create an ISO date from an array of values. | |
DateObject:: |
public | function | ||
DateObject:: |
public | function | Overridden constructor. | |
DateObject:: |
public | function | Helper function to prepare the object during serialization. | |
DateObject:: |
public | function | ||
DateObject:: |
public | function | Upon unserializing, we must re-build ourselves using local variables. |