Jump to content
NotionCommotion

Unexpected results when adding intervals to DateTime

Recommended Posts

If there are two DataTime's which are exactly X seconds apart and the difference between those two DateTime's is added to a third DateTime, then the third DataTime's timestamp would change by exactly X seconds, true?

Then why doesn't the second test below show those results, but is off by 1 day?

<?php

function test(string $start, string $end, string $newStart) {
    $startDt = new \DateTime($start);
    $endDt = new \DateTime($end);
    $intervalDt=$startDt->diff($endDt);
    $startSeconds=$startDt->getTimestamp();
    $endSeconds=$endDt->getTimestamp();
    $seconds = $endSeconds - $startSeconds;

    $dt = new \DateTime($newStart);
    $newStartSeconds=$dt->getTimestamp();
    $dt->add($intervalDt);
    $newEndSeconds=$dt->getTimestamp();
    $newSeconds = $newEndSeconds - $newStartSeconds;

    $newEnd = $dt->format(\DateTime::RFC3339);
    echo("original: $start  =>  $end   $endSeconds - $startSeconds = $seconds seconds<br>");
    echo("new: $newStart  =>  $newEnd  $newEndSeconds - $newStartSeconds = $newSeconds seconds<br>");
    echo("Error: seconds - newSeconds = ".($seconds - $newSeconds));
}

test("2017-06-26T00:00:00Z", "2017-07-09T00:00:00Z", "2018-03-05T00:00:00Z");
echo('<br><br>');
test("2017-12-08T00:00:00Z", "2018-02-15T00:00:00Z", "2018-03-05T00:00:00Z");
Quote

original: 2017-06-26T00:00:00Z => 2017-07-09T00:00:00Z 1499558400 - 1498435200 = 1123200 seconds
new: 2018-03-05T00:00:00Z => 2018-03-18T00:00:00+00:00 1521331200 - 1520208000 = 1123200 seconds
Error: seconds - newSeconds = 0

original: 2017-12-08T00:00:00Z => 2018-02-15T00:00:00Z 1518652800 - 1512691200 = 5961600 seconds
new: 2018-03-05T00:00:00Z => 2018-05-12T00:00:00+00:00 1526083200 - 1520208000 = 5875200 seconds
Error: seconds - newSeconds = 86400

 

Share this post


Link to post
Share on other sites

When I do this I get exactly the same differences???

function test2 ($start, $end, $newstart)
{
    $dt1 = new DateTime($start);
    $dt2 = new DateTime($end);
    $dif = $dt1->diff($dt2);
    
    $dt3 = new DateTime($newstart);
    $dt4 = clone $dt3;
    $dt4->add($dif);
    
    echo $dt1->format('Y-m-d H:i:s') . ' | ' ;
    echo $dt2->format('Y-m-d H:i:s') . ' | ' ;
    echo $dif->format('%y %m %d %h %i %s') . '<br>' ;
    
    echo $dt3->format('Y-m-d H:i:s') . ' | ' ;
    echo $dt4->format('Y-m-d H:i:s') . ' | ' ;
    echo $dt3->diff($dt4)->format('%y %m %d %h %i %s') . '<br>' ;
    
    
}    
    
test2('2019-07-15 12:30:00', '2019-07-17 14:15:30', '2019-08-15');    

 

Share this post


Link to post
Share on other sites

If I remember correctly, ->diff measures the apparent difference between dates. Like the difference between 00:00 and 05:00 is apparently five hours.

But timestamps are a measure of elapsed time. 00:00 to 05:00 could be five hours, or it could be four or six if it crosses a DST boundary.

Share this post


Link to post
Share on other sites

That could account for a one hour difference, but a whole day!?

Share this post


Link to post
Share on other sites

Immedialtely after posting my first reply I realised I hadn't posted my results. I therefore edited the reply and added them.

But they aren't there!

Anyway, they were

2019-07-15 12:30:00 | 2019-07-17 14:15:30 | 0 0 2 1 45 30
2019-08-15 00:00:00 | 2019-08-17 01:45:30 | 0 0 2 1 45 30

 

Share this post


Link to post
Share on other sites
$dt4->add($dif); 

I think that gives the result expected. Not sure why. 

2019-07-15 12:30:00 | 2019-07-17 14:15:30 | 0 0 2 1 45 30<br>2019-08-17 01:45:30 | 2019-08-19 03:31:00 | 0 0 2 1 45 30<br>

 

Share this post


Link to post
Share on other sites

It's (cough) possible I saw the number 86400, thought "oh I recognize that", and went straight into the DST explanation. Which is wrong for two reasons: that number is for a day not an hour, and because we're dealing with UTC here.

But what I was right about was the apparent vs. elapsed issue. Look at the dates - just the dates:

2017-06-26 => 2017-07-09 = 4 days of June + 9 days of July = 13 days
2018-03-05 => 2018-03-18 = 13 days
Good

2017-12-08 => 2018-02-15 = 24 days of December + 31 days of January + 15 days of February = 69 days
2018-03-05 => 2018-05-12 = 27 days of March + 30 days of April + 12 days of May = 69 days
Good

But that's elapsed time and diff deals with apparent time. And apparently,

2017-06-26 => 2017-07-09 = 0 months + 13 days (overflowing after 30) = 13 days
2018-03-05 => 2018-03-18 = 0 months + 13 days = 13 days
Same

2017-12-08 => 2018-02-15 = 2 months (December with 31 days, January with 31 days) + 7 days = 69 days
2018-03-05 => 2018-05-12 = 2 months (March with 31 days, April with 30 days) + 7 days = 68 days
Oh no

The results are even more off when you have to deal with February and its mere 28 days.

It's weird, but that's what you get when you try to do date math. And unfortunately there's no simple moral to this story: sometimes a diff-ed DateInterval is appropriate (tends to look nicer across DST boundaries), sometimes a diff-ed timestamp is appropriate (tends to work more like what you'd want), and using the wrong one will screw you up.

Share this post


Link to post
Share on other sites

Thanks for the explanation.

What also threw me for a loop was when I attempted to compare DateInterval's (i.e. if($di1>$di2)...) similar to comparing DateTime's.  It sometimes works but other times does not and one gets different results when stepping through with a debugger opposed to  fully running.  I think there is a takeaway moral for this one... Just don't do it.  Any disagreement?

Regarding whether diff-ed DataIntervals are appropriate or diff-ed timestamps, what do you think for getSubstitute()?  I have a bunch of time-series data, and when a day or many days is missing data, wish to find another range of data both with the same duration and starts and ends on the same day of the week (guess ends goes without saying).

    public function read(int $id):array {
        $results = [];
        //$data is grouped by day.  [['time'=>'2017-06-21','count'=>123]...]
        $data=$this->getData($id);
        //Groups is ['gaps'=>[['start'=>'2017-06-21','end'=>'2017-06-21'], ...], 'fills'=>[sameAsGaps]]
        $groups=$this->getGroups($data);
        foreach($groups['gaps'] as $gap) {
            if($substitute=$this->getSubstitute($gap, $groups['fills'])) {
                $results[$gap['time']]=true;
                $this->processGroup($gap, $substitute);
            }
            else {
                //No substitute found for this range
                $results[$gap['time']]=false;
            }
        }
        return $results;
    }

    private function getGroups(array $data):array {
        $gaps=[];
        $fills=[['start'=>$data[0]['time']]];
        $missing=false; //First group must have data and last group must be the end of data

        foreach($data as $day) {
            if($missing) {
                if($day['count']>$this->minimumSamples) {
                    //End of missing data
                    $missing=false;
                    $fills[] = ['start'=>$day['time']];
                    $gaps[count($gaps)-1]['end']=$day['time'];
                }
            }
            else {
                if($day['count']<=$this->minimumSamples) {
                    //Start of missing data
                    $missing=true;
                    $gaps[] = ['start'=>$day['time']];
                    $fills[count($fills)-1]['end']=$day['time'];
                }
            }
        }
        $fills[count($fills)-1]['end']=$data[count($data)-1]['time'];
        return ['gaps'=>$gaps, 'fills'=>$fills];
    }

    public function getSubstitute(array $gap, $fills):?array {
        $gapStart = new \DateTime($gap['start']);
        $gapEnd = new \DateTime($gap['end']);
        $gapInterval=$gapStart->diff($gapEnd);
        $gapStartTs = $gapStart->getTimestamp();
        $gapEndTs = $gapEnd->getTimestamp();
        $gapDiffSeconds = $gapEndTs - $gapStartTs;
        $startWeekDay = $gapStart->format('l');
        $endWeekDay = $gapEnd->format('l');
        $substitute=null;
        $substituteOffset=null;
        foreach($fills as $fill) {
            $fillStart=new \DateTime($fill['start']);
            $fillStart->modify($startWeekDay);
            $fillEnd=new \DateTime($fill['end']);
            $fillEnd->modify("previous $endWeekDay");
            if($fillEnd > $fillStart) {
                $fillStartTs=$fillStart->getTimestamp();
                if(($fillEnd->getTimestamp() - $fillStartTs) >= $gapDiffSeconds) {
                    if($fillStart > $gapStart) {
                        //Fill data is newer than gap data.  Takes precidence
                        $offset=$fillStartTs - $gapStartTs;
                        if(is_null($substituteOffset) || $offset <= $substituteOffset) {
                            $substituteOffset=$offset;
                            $substitute=$fill;
                            $substitute['start']=$fillStart;
                            $substitute['end']=(clone $fillStart)->add($gapInterval);;
                        }
                    }
                    else {
                        //Fill data is older than gap data.  Does not take precidence
                        $offset=$gapStartTs - $fillStartTs;
                        if(is_null($substituteOffset) || $offset < $substituteOffset) {
                            $substituteOffset=$offset;
                            $substitute=$fill;
                            $substitute['end']=$fillEnd;
                            $substitute['start']=(clone $fillEnd)->sub($gapInterval);;
                        }
                    }
                }
            }
            //else {syslog(LOG_INFO, 'Start time occurs later than end time.  Ignore');}
        }
        return $substitute;
    }


 

 

Share this post


Link to post
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • Create New...

Important Information

We have placed cookies on your device to help make this website better. You can adjust your cookie settings, otherwise we'll assume you're okay to continue.