'Bout time I posted something here again.
When you have a linear space on which you can identify specific points (any collection of things which have a specific order - numbers, dates, etc.) one of the first extensions of the concept is that you can have intervals - by picking out two points in the space you can specify the entire chunk of space that lies between them.
So here's a class or three for PHP5. interval is the base class, and also implements the space of real numbers. Subclassed from this is a date_interval class; and there is a class that implements sets of intervals.
I think I've pretty much covered the basic operations on general-purpose intervals: you can determine if two intervals overlap, or if one interval is contained within another; you can join two overlapping intervals into one, or excise one interval from another, and so on.
When subclassing, there are a few methods to override:
lt: the "less than" operator which defines the ordering of points in the space (it takes two points as its arguments, not intervals)
eq: tests the equality of two points
length: the length of this interval
widen: this is the basic function for moving the endpoints of an interval left or right. You may also wish to override widen_left and widen_right as well
midpoint: returns the point that lies at the midpoint of the interval of course
__toString() is used to return a human-readable string representation of the interval.
(gt, ge, le, and ne are also available: these are all defined in terms of lt and eq, however, so explicit overriding is not necessary for them to work as expected).
Well, there's some ad-hoc commentary in the file. If I didn't welcome criticism I wouldn't be posting here. Someone will probably yell at me for the accessor methods. For an intro, part of the file is excerpted below, followed by some weedy examples of its use.
/*
An example extension of the interval class: intervals of days
(i.e., date ranges). Dates are passed in as associative arrays
('y'=>year, 'm'=>month, 'd'=>day) - all three use calendrical indexing (i.e,
1-indexed). So for example January 26, 1986 would be passed in as
array('y'=>1986, 'm'=>1, 'd'=>26).
Since intervals are closed, the range (2005,5,1) to (2005,5,7) is seven
days long - not the six as you might expect from simply subtracting the
two. Similarly, the range (1883,9,6) to (1883,9,6) is one day long.
For exemplary reasons, all date arithmetic is performed in-house,
rather than using native functions and storing dates as timestamps.
(It thus works outside the range of ordinary timestamps.)
Exercise for the reader: implement a date_interval class using that approach.
*/
class date_interval extends interval
{
public function __construct($left=null, $right=null)
{
if($left===null)
{
// The only place where a native date function is used.
// Warning: this means that if 32-bit signed arithmetic is still
// being used to store timestamps, then this code will break after
// 2038 and before 1900. Time travellers, you have been warned.
$now = getdate();
$left = array('y'=>$now['year'], 'm'=>$now['mon'], 'd'=>$now['mday']);
}
if($right===null) $right=$left;
self::fixdate($left);
self::fixdate($right);
if(self::lt($right,$left))
{
$t = $left;
$left = $right;
$right = $t;
}
$this->left = $left;
$this->right = $right;
}
public function __toString()
{
$left = $this->left['y']. '-'.$this->left['m']. '-'.$this->left['d'];
$right = $this->right['y'].'-'.$this->right['m'].'-'.$this->right['d'];
return '['.$left.', '.$right.']';
}
// Overriding the space-specific methods of the interval class.
public static function lt($a,$b)
{
if($a['y']<$b['y']) return true;
if($a['y']>$b['y']) return false;
if($a['m']<$b['m']) return true;
if($a['m']>$b['m']) return false;
if($a['d']<$b['d']) return true;
if($a['d']>$b['d']) return false;
return false;
}
public static function eq($a,$b)
{
return $a['y']==$b['y'] && $a['m']==$b['m'] && $a['d']==$b['d'];
}
// Number of days between two dates - measured in days
// Yes, I suppose I could have converted both to Julian
// Days and then subtracted - but naaah.
// Note that the number of days between a date and itself is 1
// (i.e. the interval from 1990-12-14 to 1990-12-14 is one day long).
public function length()
{
static $days = array(null,0,31,59,90,120,151,181,212,243,273,304,334);
$length = 1+$this->right['d']-$this->left['d'];
$length += $days[$this->right['m']]-$days[$this->left['m']];
if($this->left['m']>2 && self::leap($this->left['y']))
$length--;
if($this->right['m']>2 && self::leap($this->right['y']))
$length++;
for($year = $this->left['y']; $year<$this->right['y']; ++$year)
$length += self::leap($year) ? 366 : 365;
return $length;
}
public function midpoint()
{
$midpoint = $this->left;
$midpoint['d'] += $this->length()>>1;
self::fixdate($midpoint);
return $midpoint;
}
public function widen($v,$w=null)
{
if($w===null) $w = $v;
$this->left['d'] -= $v;
self::fixdate($this->left);
$this->right['d'] += $w;
self::fixdate($this->right);
// Since we can "widen" by negative amounts:
if(self::lt($this->right,$this->left))
{
$t = $this->left;
$this->left = $this->right;
$this->right = $t;
}
}
// Some private utility functions
protected static final function leap($year)
{
return !(($year&3 || !($year%100)) && $year%400);
}
protected static final function dinm($year, $month)
{
static $months = array(null,31,28,31,30,31,30,31,31,30,31,30,31);
if($month==2)
{
return self::leap($year) ? 29 : 28;
}
return $months[$month];
}
// 146th January is really 26th May
// (except in a leap year, when it's only the 25th).
// TODO:
// I'd like to speed this up by rolling back and forth
// by full years when possible.
// Anyone care to take a punt on writing this?
protected static final function fixdate(&$date)
{
while($date['d']<1)
{
--$date['m'];
if($date['m']==0)
{
$date['m'] = 12;
--$date['y'];
}
$date['d'] += self::dinm($date['y'], $date['m']);
}
while($date['d']>($dinm = self::dinm($date['y'], $date['m'])))
{
$date['d'] -= $dinm;
++$date['m'];
if($date['m']==13)
{
$date['m'] = 1;
++$date['y'];
}
}
}
}
// Examples:
$start_date = array('y'=>1993, 'm'=>9, 'd'=>1);
$September_that_never_ended = new date_interval(null, $start_date);
echo "September ".$September_that_never_ended->length().", 1993";
// Note that the resulting interval is 1000001 days long;
// today plus the million days following.
$one_million_days_from_now = new date_interval();
$one_million_days_from_now->widen_right(1000000);
echo join('-', $one_million_days_from_now->right());
$date1 = array('y'=>1973, 'm'=>5, 'd'=>14);
$date2 = array('y'=>1986, 'm'=>1, 'd'=>26);
$date3 = array('y'=>1980, 'm'=>8, 'd'=>31);
$date4 = array('y'=>2005, 'm'=>1, 'd'=>1);
$interval1 = new date_interval($date1, $date2);
$interval2 = new date_interval($date3, $date4);
echo date_interval::common_interval($interval1, $interval2);