Reputation: 67
I have several date ranges in form of DateTime $begin, DateTime $end
. Those ranges can overlap in every possible way:
|-------|
|=======|
|------|
|======|
|------------|
|=======|
|---|
etc.
What I am trying to do is to get length (in seconds or DateInterval
) of those ranges between start of the first one and the end of the latest one (fourth in the case above), excluding regions not covered by any range.
There is no problem for only two ranges, but I can't work out how to extend it to handle more than two.
EDIT:
class Range {
public DateTime $begin;
public DateTime $end;
}
$ranges = getRanges(); # function that returns array of Range objects
function getActiveHours($_ranges = array()) {
$sum = 0;
# this is the function I'd like to have
return $sum;
}
For two ranges only I have a function which returns DateInterval
object:
function addTimeRanges(DateTime $b1, DateTime $e1, DateTime $b2, DateTime $e2) {
$res = null;
if ($e1 < $b2 || $e2 < $b1) { # separate ranges
$r1 = $b1->diff($e1);
$r2 = $b2->diff($e2);
$res = addIntervals($r1, $r2);
} else if ($b1 <= $b2 && $e1 >= $e2) { # first range includes second
$res = $b1->diff($e1);
} else if ($b1 > $b2 && $e1 < $e2) { # second range includes first
$res = $b2->diff($e2);
} else if ($b1 < $b2 && $e1 <= $e2 && $b2 <= $e1) { # partial intersection
$res = $b1->diff($e2);
} else if ($b2 < $b1 && $e2 <= $e1 && $b1 <= $e2) { # partial intersection
$res = $b2->diff($e1);
}
return $res;
}
where addIntervals
is a function that returns sum of two DateInterval
objects as another DateInterval
object.
This is some basic version, in my production code I use a lot of other irrelevant stuff.
To simplify let's say we have only Time part of DateTime
: ('06:00:00' to '08:00:00'), ('07:00:00' to '09:00:00'), ('06:00:00', '08:00:00'), ('11:00:00' to '12:00:00') (there will be many such ranges). The result I'd like to have now is 4 hours (from 6:00 to 9:00 + from 11:00 to 12:00).
Upvotes: 2
Views: 874
Reputation: 2688
Following code can be used as part of solution after converting dates to timestamps: https://stackoverflow.com/a/3631016/1414555
Once $data is array with timestamps you can use it:
usort($data, function($a, $b) { return $a[0] - $b[0]; });
$n = 0; $len = count($data);
for ($i = 1; $i < $len; ++$i) {
if ($data[$i][0] > $data[$n][1] + 1)
$n = $i;
else {
if ($data[$n][1] < $data[$i][1])
$data[$n][1] = $data[$i][1];
unset($data[$i]);
}
}
$duration = 0; //Duration in seconds
foreach ($data as $range)
$duration += ($range[1] - $range[0]);
Upvotes: 0
Reputation: 99
The Example For given task
class DateRange
{
private $startDate;
private $endDate;
public function getStart(){
return clone $this->startDate;
}
public function getEnd(){
return clone $this->endDate;
}
public function __construct(\DateTime $startDate, \DateTime $endDate = null)
{
$this->startDate = $startDate;
if (is_null($endDate)) {
$this->endDate = new \DateTime();
} else {
$this->endDate = $endDate;
}
}
}
class DateRanges
{
private $ranges = array();
public function addRange(\DateRange $range)
{
$this->ranges[] = $range;
}
private function _RageToArray(\DateRange $_in)
{
$_r = array();
$start = $_in->getStart();
$end = $_in->getEnd();
while($start<$end){
$_r[$start->format('Y-m-d')] = null;
$start->modify('+1 days');
}
return $_r;
}
public function getDaysCount()
{
$_r = array();
foreach($this->ranges as $range){
$_r += $this->_RageToArray($range);
}
return count($_r);
}
}
$today = new DateTime();
$ranges = new DateRanges();
$x = new stdClass();
$x->start = (clone $today);
$x->start->modify('-3 years');
$x->end = (clone $x->start);
$x->end->modify('+1 month');
$ranges->addRange(new DateRange($x->start, $x->end));
$x = new stdClass();
$x->start = (clone $today);
$x->start->modify('-3 years');
$x->end = (clone $x->start);
$x->end->modify('+15 days');
$ranges->addRange(new DateRange($x->start, $x->end));
$x = new stdClass();
$x->start = (clone $today);
$x->start->modify('-4 years');
$x->end = (clone $x->start);
$x->end->modify('+15 days');
$ranges->addRange(new DateRange($x->start, $x->end));
echo $ranges->getDaysCount() . ' must be near ' . (31 + 15) . PHP_EOL;
Upvotes: 0
Reputation: 18430
I would recommend that you create a function that returns an instance of your Range class that has its properties set to the start and end of the whole period. Something like this:-
class Range
{
public $startDate;
public $endDate;
public function __construct(\DateTime $startDate, \DateTime $endDate)
{
$this->startDate = $startDate;
$this->endDate = $endDate;
}
public function getInterval()
{
return $this->startDate->diff($this->endDate);
}
public function getSeconds()
{
return $this->endDate->getTimestamp() - $this->startDate->getTimestamp();
}
}
I chose to create a minimal factory class that could, among other things, do this type of calculation for you:
class Ranges
{
private $ranges = array();
public function addRange(\Range $range)
{
$this->ranges[] = $range;
}
public function getFullRange()
{
$fullRange = new \Range($this->ranges[0]->startDate, $this->ranges[0]->endDate);
foreach($this->ranges as $range){
if($range->startDate < $fullRange->startDate){
$fullRange->startDate = $range->startDate;
}
if($range->endDate > $fullRange->endDate){
$fullRange->endDate = $range->endDate;
}
}
return $fullRange;
}
}
Some code to demonstrate that it works:-
$ranges = new \Ranges();
$ranges->addRange(new \Range(new \DateTime(), new \DateTime('+ 2 hours')));
$ranges->addRange(new \Range(new \DateTime('1st Jan 2012'), new \DateTime('3rd Jan 2012')));
$ranges->addRange(new \Range(new \DateTime('- 4 days'), new \DateTime('+ 30 days')));
$fullRange = $ranges->getFullRange();
var_dump($fullRange);
var_dump($fullRange->getInterval());
var_dump($fullRange->getSeconds());
At the time I ran it I got the following result:-
object(Range)[11]
public 'startDate' =>
object(DateTime)[6]
public 'date' => string '2012-01-01 00:00:00' (length=19)
public 'timezone_type' => int 3
public 'timezone' => string 'Europe/London' (length=13)
public 'endDate' =>
object(DateTime)[10]
public 'date' => string '2013-05-14 19:36:06' (length=19)
public 'timezone_type' => int 3
public 'timezone' => string 'Europe/London' (length=13)
object(DateInterval)[12]
public 'y' => int 1
public 'm' => int 4
public 'd' => int 13
public 'h' => int 19
public 'i' => int 36
public 's' => int 6
public 'invert' => int 0
public 'days' => int 499
int 43180566
This will cope with any number of Range objects in any order and always return a Range object that gives you the earliest and latest dates spanned by all the ranges supplied.
I also added methods to allow you to get the result as a DateInterval instance, or as a number of seconds.
Upvotes: 0
Reputation: 76
$ranges = array(
array(date_create_from_format('U', 1364654958), date_create_from_format('U', 1364655758)), //800s (intersect with 2 row, 700s) = 100s
array(date_create_from_format('U', 1364654658), date_create_from_format('U', 1364655658)), //1000s (intersect with 1 row)
array(date_create_from_format('U', 1364656858), date_create_from_format('U', 1364656958)), //100s
); //total 1200s = 20m
array_multisort($ranges, SORT_ASC, array_map(function($a){return $a[0];}, $ranges));
$count = count($ranges)-1;
for ($i=0; $i < $count; $i++) {
if ($ranges[$i+1][0] < $ranges[$i][1]) {
$ranges[$i][1] = max($ranges[$i][1], $ranges[$i+1][1]);
unset($ranges[$i+1]);
$i--;
$count--;
}
}
$sum = date_create();
foreach ($ranges as $value) {
date_add($sum, date_diff($value[0],$value[1]));
}
print_r(date_diff(date_create(), $sum));
Upvotes: 0