PHP wrong DateTime::diff() returns wrong DateInterval - php

I have an issue with a difference of two Datetime. Here is the command line to display the DateInterval object :
php -r "\$a = new Datetime('first day of 4 months ago midnight'); \$b = new Datetime('first day of 1 month ago midnight'); var_dump(\$a->diff(\$b));"
And here the DateInterval output :
class DateInterval#3 (15) {
public $y => int(0)
public $m => int(3)
public $d => int(3)
public $h => int(0)
public $i => int(0)
public $s => int(0)
public $weekday => int(0)
public $weekday_behavior => int(0)
public $first_last_day_of => int(0)
public $invert => int(0)
public $days => int(92)
public $special_type => int(0)
public $special_amount => int(0)
public $have_weekday_relative => int(0)
public $have_special_relative => int(0)
}
Edit: The first and second Datetime:
class DateTime#1 (3) {
public $date =>
string(19) "2014-03-01 00:00:00"
public $timezone_type =>
int(3)
public $timezone =>
string(13) "Europe/Zurich"
}
class DateTime#2 (3) {
public $date =>
string(19) "2014-06-01 00:00:00"
public $timezone_type =>
int(3)
public $timezone =>
string(13) "Europe/Zurich"
}
Notice the 3 days! I'm on PHP 5.5.8 but I'm sure that this DateInterval had 0 months a few days ago. The DateInterval output 0 days in PHP 5.4.28 and the 5.5.14. I'm not sure that the PHP version has an effect.
In both cases, the days property is 92.

Providing insight into Paul T. Rawkeen's answer, the problem with DateTime::diff is that it first converts the timezone to UTC before computation.
<?php
$zurich = new DateTimeZone('Europe/Zurich');
$utc = new DateTimeZone('UTC');
$a = new DateTime('first day of 4 months ago midnight',$zurich);
$b = new DateTime('first day of 1 month ago midnight',$zurich);
var_dump($a,$b);
$a->setTimezone($utc);
$b->setTimezone($utc);
var_dump($a,$b);
?>
Gives the following:
object(DateTime)[3]
public 'date' => string '2014-03-01 00:00:00' (length=19)
public 'timezone_type' => int 3
public 'timezone' => string 'Europe/Zurich' (length=13)
object(DateTime)[4]
public 'date' => string '2014-06-01 00:00:00' (length=19)
public 'timezone_type' => int 3
public 'timezone' => string 'Europe/Zurich' (length=13)
object(DateTime)[3]
public 'date' => string '2014-02-28 23:00:00' (length=19)
public 'timezone_type' => int 3
public 'timezone' => string 'UTC' (length=3)
object(DateTime)[4]
public 'date' => string '2014-05-31 22:00:00' (length=19)
public 'timezone_type' => int 3
public 'timezone' => string 'UTC' (length=3)
The 3 day discrepancy is now very clear, after the timezone is converted from Europe/Zurich to UTC the dates are now 2014-02-28 23:00:00 and 2014-05-31 22:00:00 for $a and $b respectively.
The solution is to work entirely in UTC and convert before displaying the DateTime:
<?php
$zurich = new DateTimeZone('Europe/Zurich');
$utc = new DateTimeZone('UTC');
$a = new DateTime('first day of 4 months ago midnight',$utc);
$b = new DateTime('first day of 1 month ago midnight',$utc);
var_dump($a,$b);
$a->setTimezone($zurich);
$b->setTimezone($zurich);
var_dump($a,$b);
?>
Notice that all days are now 01, albeit the hours are now a little different (see the note at the end of this answer):
object(DateTime)[3]
public 'date' => string '2014-03-01 00:00:00' (length=19)
public 'timezone_type' => int 3
public 'timezone' => string 'UTC' (length=3)
object(DateTime)[4]
public 'date' => string '2014-06-01 00:00:00' (length=19)
public 'timezone_type' => int 3
public 'timezone' => string 'UTC' (length=3)
object(DateTime)[3]
public 'date' => string '2014-03-01 01:00:00' (length=19)
public 'timezone_type' => int 3
public 'timezone' => string 'Europe/Zurich' (length=13)
object(DateTime)[4]
public 'date' => string '2014-06-01 02:00:00' (length=19)
public 'timezone_type' => int 3
public 'timezone' => string 'Europe/Zurich' (length=13)
To offer a bit of insight into this phenomenon, note the following:
February has 28 days (in 2014)
May has 31 days
DST starts on March 30: +01:00
Europe/Zurich is UTC+02:00
When converting from Europe/Zurich to UTC one must consider more than just the year, month and day, but the hours, minutes, and seconds too. If this was any other day than the first of any month, this problem would not occur, however the hours would still be 23:00 and 22:00 (pre the examples above).

This thing depends on DateTimeZone you provide.
If you set Europe/Zurich or any EEST time you will get the described result.
If GMT/UTC for e.g., you will get $d = 0.
You can use global time zone definition along your project to avoid such problems (if it suits you)
date_default_timezone_set( "Europe/Zurich" );
or define required time zone for DateTime objects.
UPD: as was mentioned below in comment, by #mudasobwa, this problem is mentioned here about 3 years ago.

Related

PHP Bug day of year [duplicate]

This question already has answers here:
Convert day of the year to datetime in php
(2 answers)
Closed 2 years ago.
I'm trying to convert day of year into date.
So no problem i use DateTime::createFromFormat with z and Y.
DateTime::createFromFormat('z Y', '199 2020');
/*RESULT*/
object(DateTime)[3]
public 'date' => string '2020-07-19 12:45:24.000000' (length=26)
public 'timezone_type' => int 3
public 'timezone' => string 'Europe/Paris' (length=12)
But when i test the result i get the wrong day of year...
date('z Y', strtotime('2020-07-19'));
/*RESULT*/
'198 2020' (length=8)
So i trying this
DateTime::createFromFormat('z Y', date('z Y', strtotime('2020-07-19')));
/*RESULT*/
object(DateTime)[3]
public 'date' => string '2020-07-20 12:52:10.000000' (length=26)
public 'timezone_type' => int 3
public 'timezone' => string 'Europe/Paris' (length=12)
if i check day of year 199 of 2020 on this website: https://www.calendrier.best/numero-de-jour-2020.html
I get 2020-07-17
What i'm doing wrong ??
If you know the year, you can use something like this. Outputs:
string(10) "2020-07-17"
Code:
<?php
$dayOfYearNumber = 199;
$year = 2020;
// Add day of year number to the first day of the year, substracting 1 because Jan 1st will be the first day.
$date = (new DateTime("$year-1-1"))->add(new DateInterval('P' . $dayOfYearNumber - 1 . 'D'));
var_dump($date->format('Y-m-d')); // string(10) "2020-07-17"
PHP only detects some standards like you can see here https://www.php.net/manual/de/datetime.formats.date.php
In Your first example you defined your format but strtotime() has no such parameter
If you have these values as variable you could use mktime() instead https://www.php.net/manual/de/function.mktime
But I recommend to always give PHP a month e.x. January

Carbon createFromFormat unexpected result

::createFromFormat() results in being one month ahead:
var_dump($_GET['archive']);
var_dump(Carbon::createFromFormat('m/Y', $_GET['archive']));
Result:
string '11/2015' (length=7)
object(Carbon\Carbon)[160]
public 'date' => string '2015-12-01 10:38:41.000000' (length=26)
public 'timezone_type' => int 3
public 'timezone' => string 'Europe/London' (length=13)
Seems that you just got unlucky because you were testing on the 31st of the month.
However, this is (apparently) a documented feature. See: http://php.net/manual/en/datetime.createfromformat.php
If you set your format mask "correctly", it works as you'd expect.
$x = Carbon::createFromFormat ('m/Y', '04/2009');
$y = DateTime::createFromFormat ('m/Y|', '04/2009');
dd ($x, $y);
To avoid ambiguity, the Carbon function is just a wrapper for the underlying DateTime function of PHP; I'm just aiming to prove that this is not a Carbon feature that's causing our headache.
If you test on the 31st of the month, $x above will be read as 31 April 2009 which will be displayed as 1st May.
With the pipe symbol in the mask though, all the non-declared time variables (H, i, s) are set to zero, and date variables (d, m) set to 1, so $y will always respond as you expect regardless of the day of the week.
For me testing at 17:46 in the evening of the 4th day of the month, the above code produces the following:
Carbon #1233769590 {#219 ▼
date: 2009-02-04 17:46:30.0 Europe/London (+00:00)
}
DateTime #1233446400 {#217 ▼
date: 2009-02-01 00:00:00.0 Europe/London (+00:00)
}
This is a really weird implementation of date/time function in my opinion. The only real-world situation that you'd want to use this function in, is if you're reading data from an external source (such as an XML "date" field).
Which programmer on earth would want their program to assume that the missing time (or in your case, day) component of the incoming data, should be taken from the runtime execution time? It's absolutely crazy bonkers.
It seems that Carbon uses the current day if not provided. Hence
var_dump(Carbon::createFromFormat('m/Y', '10/2015'));
var_dump(Carbon::createFromFormat('m/Y', '11/2015'));
results e.g. on July 31st in
object(Carbon\Carbon)[156]
public 'date' => string '2015-10-31 11:03:10.000000' (length=26)
public 'timezone_type' => int 3
public 'timezone' => string 'Europe/London' (length=13)
object(Carbon\Carbon)[138]
public 'date' => string '2015-12-01 11:03:10.000000' (length=26)
public 'timezone_type' => int 3
public 'timezone' => string 'Europe/London' (length=13)
as October has 31st but "November 31st is actually December 1st".

PHP | Get total Months of two Overlapping Date Ranges

i am trying to figure out how to get total Months in between two overlapping date Ranges.
e-g
Date Range from Date-A to Date-B overlapping the Date range of Date-X and Date-Y.
_ Start|Jan - Feb - March - April ------ Nov - Dec|End (DateRange A)
Start|Jan - Feb ---- Dec - Jan - Feb|End _
In two date ranges the Jan and Feb Months are colliding. Means Total of 2 Months are there.
So i want these two months in my array so i can apply different functions on it.
e-g i have these two dates
$dateRange1Start = "2015-07-01";
$dateRange1End = "2014-06-30";
=-=-=-=====-=-==
$dateRange2Start = "2012-02-01";
$dateRange2End = "2014-12-31";
There are total 6 Months in colliding between these two date ranges. i want to get these 6 Months.
I tried to search for help in google, but mostly i get less than or greater than signs. but specifically like this. I tried to implement my own logic but its not getting me anywhere, sadly to only more problems :(
i am trying to get results like this
$collidingDates = array("2014-07","2014-08","2014-09","2014-10","2014-11","2014-12");
Any help would be appreciated. :)
=-=-=-=-=-=-=-==-=-=-=-=-=-=-=-=--=-=
UPDATE:
Here is what i have done so far, yes getting some code from here and there. and tweaking it a bit up to fulfill my requirements.
//Getting all Months from First Date Range
$start = (new DateTime('2014-06-01'))->modify('first day of this month');
$end = (new DateTime('2015-05-06'))->modify('first day of next month');
$interval = DateInterval::createFromDateString('1 month');
$firstPeriod = new DatePeriod($start, $interval, $end);
//Getting all Months from Second Date Range.
$start = (new DateTime('2012-02-01'))->modify('first day of this month');
$end = (new DateTime('2014-12-31'))->modify('first day of next month');
$interval = DateInterval::createFromDateString('1 month');
$secondPeriod = new DatePeriod($start, $interval, $end);
$collidingDates = array();
foreach ($firstPeriod as $f_dt) {
foreach($secondPeriod as $s_dt){
if($f_dt->format("Y-m") === $s_dt->format("Y-m")){
array_push($collidingDates,$f_dt->format("Y-m"));
}
}
}
echo "<pre>";
print_r($collidingDates);
i got this output.
Array
(
[0] => 2014-06
[1] => 2014-07
[2] => 2014-08
[3] => 2014-09
[4] => 2014-10
[5] => 2014-11
[6] => 2014-12
)
But i think i am getting 1 extra month
2014-06, not sure how O_o??
This is a two step process. First you need to establish the narrowest date range from the start and end dates. Then list the months between those dates.
// An array of start and end dates. There are just 2 in this example but you
// could have as many as you like in the same "start, then end" format.
$ranges = [
[new DateTime('2014-07-01'), new DateTime('2015-06-30')],
[new DateTime('2012-02-01'), new DateTime('2014-12-31')]
];
// Reduce the ranges to one set of two dates, the latest of the start dates
// and the earliest of the end dates.
$range = array_reduce($ranges, function($carry, $item){
return $carry
? [max($carry[0], $item[0]), min($carry[1], $item[1])]
: $item;
});
var_dump($range);
/*
array (size=2)
0 =>
object(DateTime)[1]
public 'date' => string '2014-07-01 00:00:00.000000' (length=26)
public 'timezone_type' => int 3
public 'timezone' => string 'Europe/London' (length=13)
1 =>
object(DateTime)[4]
public 'date' => string '2014-12-31 00:00:00.000000' (length=26)
public 'timezone_type' => int 3
public 'timezone' => string 'Europe/London' (length=13)
*/
// Shift both dates to the first of the month. Strictly speaking, we only
// need to do this with the start date.
$range = array_map(function($date){
return $date->modify("first day of this month");
}, $range);
var_dump($range);
/*
array (size=2)
0 =>
object(DateTime)[1]
public 'date' => string '2014-07-01 00:00:00.000000' (length=26)
public 'timezone_type' => int 3
public 'timezone' => string 'Europe/London' (length=13)
1 =>
object(DateTime)[4]
public 'date' => string '2014-12-01 00:00:00.000000' (length=26)
public 'timezone_type' => int 3
public 'timezone' => string 'Europe/London' (length=13)
*/
$months = [];
$interval = new DateInterval("P1M");
for ($month = $range[0]; $month <= $range[1]; $month->add($interval)) {
$months[] = $month->format("Y-m");
}
var_dump($months);
/*
array (size=6)
0 => string '2014-07' (length=7)
1 => string '2014-08' (length=7)
2 => string '2014-09' (length=7)
3 => string '2014-10' (length=7)
4 => string '2014-11' (length=7)
5 => string '2014-12' (length=7)
*/
I think you are looking for this:
$start = (
new DateTime('2015-12-02'))
->modify('first day of this month');
$end = (new DateTime('2016-05-06'))->modify('first day of next month');
$interval = DateInterval::createFromDateString('1 month');
$period = new DatePeriod($start, $interval, $end);
foreach ($period as $dt) {
echo $dt->format("Y-m") . "<br>\n";
}

DateTime "first day of last month" not returning the first day

I looked at this answer already, and it's quite close to what I have.
Here is my PHP code:
$start = new DateTime('0:00 first day of previous month', new DateTimeZone('UTC'));
/*
if (isset($_GET['year']) && isset($_GET['month']) && checkdate($_GET['month'], 1, $_GET['year'])) {
$start = DateTime::createFromFormat('Y-m-d', $_GET['year'] . '-' . $_GET['month'] . '-1');
}*/
$middle = DateTime::createFromFormat('U', strtotime('first day of last month', $start->format('U')));
$middle->setTimezone(new DateTimeZone('UTC'));
$end = DateTime::createFromFormat('U', strtotime('first day of 2 months ago', $start->format('U')));
$end->setTimezone(new DateTimeZone('UTC'));
var_dump($start);
var_dump($middle);
var_dump($end);
Today is August 27th, so I would expect July 1, June 1, and May 1. Here's what the actual output is:
object(DateTime)[1]
public 'date' => string '2013-07-01 00:00:00' (length=19)
public 'timezone_type' => int 3
public 'timezone' => string 'UTC' (length=3)
object(DateTime)[2]
public 'date' => string '2013-05-02 00:00:00' (length=19)
public 'timezone_type' => int 3
public 'timezone' => string 'UTC' (length=3)
object(DateTime)[3]
public 'date' => string '2013-04-02 00:00:00' (length=19)
public 'timezone_type' => int 3
public 'timezone' => string 'UTC' (length=3)
Why is it returning the second day of the months for me?
I've also tried it without the new DateTimeZone('GMT') as the second parameter of the constructor for the initial DateTime but it still gives me the same result, just with different times.
this part irrelevant - question was edited
Because of the timezone difference. $start is calculated in the 'Rainy River timezone', while $middle and $end are in UTC time.
The 'Rainy River timezone has a -06:00 hour offset from UTC (exactly the difference in hours between the first with the second and third results).
update 1 - solution
It seems the problem lies somewhere around strtotime. For some reason it yields a result with an offset of one day (further explanation needed). A simple solution, is to subtract one second from that date and it will produce the correct result.
$timezone = new DateTimeZone('UTC');
$start = new DateTime('0:00 first day of previous month', $timezone );
$middle = DateTime::createFromFormat('U', strtotime('first day of last month',($start ->format('U'))-1),$timezone);
echo $middle->format('Y-m-d')."\n";
Result:
2013-05-01
update 2 - reason for problem
Eventually I find out that the problem originates from the instantiation of the fisrt date object. Here is an illustration.
This will give a correct result:
$original = new DateTime('2013-05-01');
echo $original->format('Y-m-d')."\n";
$previous= DateTime::createFromFormat('U', strtotime('first day of last month',($original->format('U'))),new DateTimeZone('UTC'));
echo $previous->format('Y-m-d')."\n";
Result (OK):
2013-05-01
2013-04-01 <--- OK
However, this will not (only first line different, as in the original code):
$original = new DateTime('0:00 first day of previous month', new DateTimeZone('UTC'));
echo $original->format('Y-m-d')."\n";
$previous= DateTime::createFromFormat('U', strtotime('first day of last month',($original->format('U'))),new DateTimeZone('UTC'));
echo $previous->format('Y-m-d')."\n";
Result:
2013-07-01
2013-05-02 <--- BAD
After reading the answer here, I had a better idea:
$start = new DateTime('0:00 first day of previous month');
/*
if (isset($_GET['year']) && isset($_GET['month']) && checkdate($_GET['month'], 1, $_GET['year'])) {
$start = DateTime::createFromFormat('Y-m-d', $_GET['year'] . '-' . $_GET['month'] . '-1');
}*/
$middle = clone $start;
$middle->modify('first day of last month');
$end = clone $start;
$end->modify('first day of 2 months ago');
var_dump($start);
var_dump($middle);
var_dump($end);
Output:
object(DateTime)[1]
public 'date' => string '2013-07-01 00:00:00' (length=19)
public 'timezone_type' => int 3
public 'timezone' => string 'America/Rainy_River' (length=19)
object(DateTime)[2]
public 'date' => string '2013-06-01 00:00:00' (length=19)
public 'timezone_type' => int 3
public 'timezone' => string 'America/Rainy_River' (length=19)
object(DateTime)[3]
public 'date' => string '2013-05-01 00:00:00' (length=19)
public 'timezone_type' => int 3
public 'timezone' => string 'America/Rainy_River' (length=19)
Also, I realize that a DateTimeImmutable would be a better choice for the $start instance (so that I don't have to clone the other two), but I don't have access to PHP 5.5 yet.

PHP's DateTime skipping addition

I am seeing very strange behaviour, with DateTime choosing to add a day sometimes, but not others.
<?php
// If you're running this after Jan 2012, use: new DateTime(date('Y-m-d', strtotime('2012-01-09')));
$month_end_date = new DateTime();
$month_end_date->modify('last day of this month');
$event_end_date = new DateTime('2012-03-15');
if ($event_end_date > $month_end_date) {
// Using this line a day is never added on below and the date stays as 31 Jan 2012
$event_end_date = clone $month_end_date;
// This line allows the ->add() call to work, and gives 1 Feb 2012 as output:
#$event_end_date = new DateTime($month_end_date->format('Y-m-d'));
}
$event_end_date->add(new DateInterval('P1D'));
// Date should now be 1st Feb
echo "Should be 1 Feb: ". $event_end_date->format('Y-m-d');
?>
It appears to be the ->modify('last day of this month') line which breaks my code; it will print 1 Feb 2012 if I replace the first two lines with $month_end_date = new DateTime('2011-01-31'); or
$month_end_date = new DateTime('last day of this month');
$month_end_date = new DateTime($month_end_date->format(DateTime::W3C));
or use my alternative $event_end_date = new DateTime($month_end_date->format('Y-m-d'));.
Does it make sense that I need to call format before making a second modification?
It appears the usage of "first" or "last" directly in the constructor of the DateTime object causes it to become immutable. This does seem like a bug.
e.g.
date_default_timezone_set('Europe/London');
ini_set('display_errors', 1);
error_reporting(E_ALL);
$interval = new DateInterval('P1D');
$date1 = new DateTime('last day of this month');
var_dump($date1);
$date1->add($interval);
var_dump($date1);
echo "-------------------------------\n\n";
$lastDayOfMonth = date('Y-m-d H:i:s', strtotime('last day of this month'));
$date2 = new DateTime($lastDayOfMonth);
var_dump($date2);
$date2->add($interval);
var_dump($date2);
echo "-------------------------------\n\n";
$date3 = new DateTime('2012-01-31 '.date('H:i:s'));
var_dump($date3);
$date3->add($interval);
var_dump($date3);
Results in:
object(DateTime)[2]
public 'date' => string '2012-01-31 11:40:10' (length=19)
public 'timezone_type' => int 3
public 'timezone' => string 'Europe/London' (length=13)
object(DateTime)[2]
public 'date' => string '2012-01-31 11:40:10' (length=19)
public 'timezone_type' => int 3
public 'timezone' => string 'Europe/London' (length=13)
-------------------------------
object(DateTime)[3]
public 'date' => string '2012-01-31 11:40:10' (length=19)
public 'timezone_type' => int 3
public 'timezone' => string 'Europe/London' (length=13)
object(DateTime)[3]
public 'date' => string '2012-02-01 11:40:10' (length=19)
public 'timezone_type' => int 3
public 'timezone' => string 'Europe/London' (length=13)
-------------------------------
object(DateTime)[4]
public 'date' => string '2012-01-31 11:40:10' (length=19)
public 'timezone_type' => int 3
public 'timezone' => string 'Europe/London' (length=13)
object(DateTime)[4]
public 'date' => string '2012-02-01 11:40:10' (length=19)
public 'timezone_type' => int 3
public 'timezone' => string 'Europe/London' (length=13)
I've whittled it down to a simpler test case:
<?php
date_default_timezone_set('Europe/Berlin');
$date = new DateTime('last day of this month');
$original_date = clone($date);
$date->add(new DateInterval('P1D'));
if ($date == $original_date) {
echo 'Fail' . PHP_EOL;
}
echo 'Original: ' . $original_date->format(DateTime::W3C) . PHP_EOL;
echo 'Date: ' . $date->format(DateTime::W3C) . PHP_EOL;
echo 'Diff: ' . $original_date->diff($date)->format('%d %H:%i:%s') . PHP_EOL;
It gives:
[joshua#appliance][/tmp/php]$ php -v
PHP 5.3.6 with Suhosin-Patch (cli) (built: Sep 8 2011 19:34:00)
Copyright (c) 1997-2011 The PHP Group
Zend Engine v2.3.0, Copyright (c) 1998-2011 Zend Technologies
[joshua#appliance][/tmp/php]$ php test.php
Fail
Original: 2012-01-31T11:47:47+01:00
Date: 2012-01-31T11:47:47+01:00
Diff: 0 00:0:0
The 'Fail' line should never be output if the date is incremented. Also, note that the diff is 0. This looks like a bug :)

Categories