Skip to content

Instantly share code, notes, and snippets.

@pavlepredic
Created August 13, 2013 10:53
Show Gist options
  • Save pavlepredic/6220041 to your computer and use it in GitHub Desktop.
Save pavlepredic/6220041 to your computer and use it in GitHub Desktop.
TimeInterval class
<?php
/**
* Representation of a time interval, such as 1 year,
* or a more complex interval such as 1 month, 4 days and 6 hours.
* Provides methods for adding and substracting this interval
* to/from a given DateTime object or to/from a UNIX timestamp.
* Sample usage:
* $int = new TimeInterval(1, TimeInterval::YEAR); //1 year
* $int->addInterval(1, TimeInterval::MONTH); //allows creating more complex intervals (optional)
* $future = $int->addToDate(); //$future will be a DateTime object set to 1 year and 1 month from now
* $future = $int->addToDate(date_create('1955-11-05 20:00:00')); //$future will be a DateTime object set to 1956-12-05 20:00:00
* $intM = $int->toInterval(TimeInterval::MONTH);//converts this interval to months ($intM = 13)
*/
class TimeInterval
{
private $_intervals = array();
const YEAR = 0;
const MONTH = 1;
const WEEK = 2;
const DAY = 3;
const HOUR = 4;
const MINUTE = 5;
const SECOND = 6;
private static $conversionTable = array(
self::YEAR => array(
self::MONTH => 12,
),
self::MONTH => array(
self::WEEK => 4.34812,
),
self::WEEK => array(
self::DAY => 7,
),
self::DAY => array(
self::HOUR => 24,
),
self::HOUR => array(
self::MINUTE => 60,
),
self::MINUTE => array(
self::SECOND => 60,
),
);
/**
* Constructor
* Creates a TimeInterval instance using the provided time interval.
* @see addInterval()
* @param int $interval number of units
* @param int $unit time unit (must be one of TimeInterval class constants)
*/
public function __construct($interval, $unit = self::SECOND)
{
$this->addInterval($interval, $unit);
}
/**
* Adds the specified time interval to this instance.
* Use this method to specify a complex interval.
* @param int $interval number of units
* @param int $unit time unit (must be one of TimeInterval class constants)
*/
public function addInterval($interval, $unit = self::SECOND)
{
if (!array_key_exists($unit, $this->_getUnits()))
$unit = self::SECOND;
if (!isset($this->_intervals[$unit]))
$this->_intervals[$unit] = 0;
$this->_intervals[$unit] += $interval;
ksort($this->_intervals);
}
/**
* Adds this time interval to the provided DateTime object.
* If no DateTime object is supplied, it creates one from current time.
* When adding months, there is a risk of the result "overflowing".
* Eg: when adding 1 month to 2013-01-31, result is 2013-03-03.
* To prevent this overflow, set $preventMonthOverflow to true.
* In that case, return date would be 2013-02-28
* @param DateTime $date
* @param bool $preventMonthOverflow
* @return DateTime
*/
public function addToDate(DateTime $date = null, $preventMonthOverflow = false)
{
if ($date === null)
$date = date_create();
else
$date = clone $date;
foreach ($this->_intervals as $unit => $interval)
{
$newDate = clone $date;
$newDate->add(new DateInterval($this->_getIntervalSpecification($interval, $unit)));
if ($preventMonthOverflow and $unit == self::MONTH)
while ($this->_monthDiff($newDate, $date) > $interval)
$newDate->modify("-1 day");
$date = $newDate;
}
return $date;
}
/**
* Adds this time interval to the provided UNIX timestamp.
* If no timestamp is supplied, it creates one from current time.
* @param int $time
* @return int
*/
public function addToTime($time = null)
{
if ($time === null)
$time = time();
return $time + $this->toSeconds();
}
/**
* Substracts this time interval from the provided DateTime object.
* If no DateTime object is supplied, it creates one from current time.
* When substracting months, there is a risk of the result "overflowing".
* Eg: when substracting 1 month from 2013-03-31, result is 2013-03-03.
* To prevent this overflow, set $preventMonthOverflow to true.
* In that case, return date would be 2013-02-28
* @param DateTime $date
* @param bool $preventMonthOverflow
* @return DateTime
*/
public function substractFromDate(DateTime $date = null, $preventMonthOverflow = false)
{
if ($date === null)
$date = date_create();
else
$date = clone $date;
foreach ($this->_intervals as $unit => $interval)
{
$newDate = clone $date;
$newDate->sub(new DateInterval($this->_getIntervalSpecification($interval, $unit)));
if ($preventMonthOverflow and $unit == self::MONTH)
while ($this->_monthDiff($newDate, $date) < $interval)
$newDate->modify("-1 day");
$date = $newDate;
}
return $date;
}
/**
* Substracts this time interval from the provided UNIX timestamp.
* If no timestamp is supplied, it creates one from current time.
* @param int $time
* @return int
*/
public function substractFromTime($time = null)
{
if ($time === null)
$time = time();
return $time - $this->toSeconds();
}
/**
* Converts this time interval to seconds.
* @return int
*/
public function toSeconds()
{
return $this->toInterval(self::SECOND);
}
/**
* Converts one time interval to another
* @param int $i1 (one of TimeInterval constants)
* @param int $i2 (one of TimeInterval constants)
* @return number
*/
public static function convert($i1, $i2)
{
if ($i1 < self::YEAR or $i2 < self::YEAR or $i1 > self::SECOND or $i2 > self::SECOND)
return 0;
if ($i1 == $i2)
return 1;
if ($i1 < $i2) //eg day to hour
{
$to = key(self::$conversionTable[$i1]);
$factor = current(self::$conversionTable[$i1]);
if ($to == $i2)
return $factor;
else
return $factor * self::convert($to, $i2);
}
else //eg hour to day
{
foreach (self::$conversionTable as $to => $conversions)
{
if (!array_key_exists($i1, $conversions))
continue;
$factor = 1 / current($conversions);
if ($to == $i2)
return $factor;
else
return $factor * self::convert($to, $i2);
}
}
}
/**
* Converts this interval to another.
* @param int $toInterval interval to convert to (one of TimeInterval constants)
* @return number
*/
public function toInterval($toInterval)
{
$v = 0;
foreach ($this->_intervals as $fromInterval => $unit)
$v += $unit * self::convert($fromInterval, $toInterval);
return $v;
}
/**
* Magic method for converting to string
* @return string
*/
public function __toString()
{
return $this->asString();
}
/**
* Provides a string representation of this time interval
* (eg "1 year, 1 month")
* @param $singularMode - if true, returns in the form "3-month",
* otherwise uses form "3 months"
* @return string
*/
public function asString($singularMode = false)
{
$s = array();
$units = $this->_getUnits();
foreach ($this->_intervals as $unit => $interval)
{
if ($singularMode)
$s[] = $interval . '-' . ucfirst(strtolower($units[$unit]));
else
$s[] = $interval . ' ' . ucfirst(strtolower($units[$unit])) . ($interval > 1 ? 's' : '');
}
return join(', ', $s);
}
/**
* Returns this interval as a string suitable for use in SQL queries.
* Doesn't work with complex intervals.
* @throws TimeIntervalException if this is a complex interval
* @return string
*/
public function toSqlInterval()
{
if (count($this->_intervals) > 1)
throw new TimeIntervalException("Cannot convert complex intervals to SQL");
$units = $this->_getUnits();
$unit = key($this->_intervals);
$interval = current($this->_intervals);
return $interval . ' ' . $units[$unit];
}
private function _getUnits()
{
$r = new ReflectionClass(__CLASS__);
return array_flip($r->getConstants());
}
private function _monthDiff(DateTime $date1, DateTime $date2)
{
$ts1 = $date1->getTimestamp();
$ts2 = $date2->getTimestamp();
if ($ts1 == $ts2)
return 0;
elseif ($ts1 > $ts2)
{
$dateHi = $date1;
$dateLo = $date2;
}
else
{
$dateHi = $date2;
$dateLo = $date1;
}
return $dateHi->format('n') + 12 * ($dateHi->format('Y') - $dateLo->format('Y')) - $dateLo->format('n');
}
private function _getIntervalSpecification($interval, $unit)
{
$ps = $unit >= self::HOUR ? 'PT' : 'P';
$units = $this->_getUnits();
return sprintf("%s%d%s", $ps, $interval, substr($units[$unit], 0, 1));
}
/**
* Helper method. Does the same as constructor.
* @param int $interval
* @param int $unit
* @return TimeInterval
*/
public static function create($interval, $unit)
{
return new TimeInterval($interval, $unit);
}
/**
* Creates an instance of TimeInterval from a date range
* @param DateTime $date1
* @param DateTime $date2
* @return TimeInterval
*/
public static function fromDateRange(DateTime $date1, DateTime $date2)
{
$seconds = abs($date1->getTimestamp() - $date2->getTimestamp());
return new TimeInterval($seconds, self::SECOND);
}
/**
* Multiplies this interval by the provided factor.
* Eg interval of 1 month and 3 days multiplied by
* 3 will become 3 months and 9 days.
* @param int $factor
* @return TimeInterval
*/
public function multiply($factor)
{
foreach ($this->_intervals as $interval => &$unit)
$unit *= $factor;
return $this;
}
/**
* Merges this TimeInterval with the one provided
* in argument. Merging simply means that two intervals
* are added together (eg 2 months + 1 month and 1 day = 3 months and 1 day)
* @param TimeInterval $timeInterval
* @return TimeInterval
*/
public function mergeWith(TimeInterval $timeInterval)
{
foreach ($timeInterval->_intervals as $unit => $interval)
$this->addInterval($interval, $unit);
return $this;
}
}
class TimeIntervalException extends Exception {}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment