PHP's DateTime skipping addition - php

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

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

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";
}

PHP timezone offset incorrect

The following PHP Code:
function serverTimeZone_offset($userTimeZone)
{
$userDateTimeZone = new DateTimeZone($userTimeZone);
$userDateTime = new DateTime("now", $userDateTimeZone);
$serverTimeZone = date_default_timezone_get();
$serverDateTimeZone = new DateTimeZone($serverTimeZone);
$serverDateTime = new DateTime("now", $serverDateTimeZone);
return $serverDateTimeZone->getOffset($userDateTime);
}
function getDefineTimeZone($timezone)
{
$userDateTimeZone = new DateTimeZone($timezone);
return new DateTime("now", $userDateTimeZone);
}
function getServerTimeZone()
{
$serverTimeZone = date_default_timezone_get();
$serverDateTimeZone = new DateTimeZone($serverTimeZone);
return new DateTime("now", $serverDateTimeZone);
}
$userDateTime = getDefineTimeZone('America/Curacao');
$serverDateTime = getServerTimeZone();
$timeOffset = serverTimeZone_offset('America/Curacao');
var_dump($userDateTime);
var_dump($serverDateTime);
var_dump($timeOffset); // the seconds is incorrect ?!?!
// adding the timezone difference
$userDateTime->add(new DateInterval('PT'.$timeOffset.'S'));
var_dump($userDateTime);
Will output:
object(DateTime)[2]
public 'date' => string '2014-10-22 17:36:39' (length=19)
public 'timezone_type' => int 3
public 'timezone' => string 'America/Curacao' (length=15)
object(DateTime)[3]
public 'date' => string '2014-10-22 23:36:39' (length=19)
public 'timezone_type' => int 3
public 'timezone' => string 'Europe/Paris' (length=12)
int 7200
object(DateTime)[2]
public 'date' => string '2014-10-22 19:36:39' (length=19)
public 'timezone_type' => int 3
public 'timezone' => string 'America/Curacao' (length=15)
Which is obviously incorrect. The offset is returning back 7200 seconds (only 2 hours) rather than 21600 seconds (6 hours). Why?
I think uou are misinterpreting the behaviour of DateTimeZone::getOffset(). As said in DateTimeZone php docs:
This function returns the offset to GMT for the date/time specified in the datetime parameter. The GMT offset is calculated with the timezone information contained in the DateTimeZone object being used.
So if server timezone is Europe/Paris, then getOffset() will return the 7200 seconds, as Europe/Paris is GMT+01:00, and it is summer time right now, so it is GMT+02:00.
Try using this code instead:
function serverTimeZone_offset($userTimeZone)
{
$userDateTimeZone = new DateTimeZone($userTimeZone);
$userDateTime = new DateTime("now", $userDateTimeZone);
$serverTimeZone = date_default_timezone_get();
$serverDateTimeZone = new DateTimeZone($serverTimeZone);
$serverDateTime = new DateTime("now", $serverDateTimeZone);
return $serverDateTimeZone->getOffset($userDateTime) - $userDateTimeZone->getOffset($userDateTime);
}

PHP wrong DateTime::diff() returns wrong DateInterval

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.

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.

Categories