简体   繁体   中英

How to get total time from several date ranges in php

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).

$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));

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.

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;

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]);

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM