Why does iterating over weeks go wrong with PHP date? - php

I'm writing a php script that iterates over the Monday of each week.
However the script seemed to get out of sync after 22nd of October.
<?php
$october_8th = strtotime("2012-10-08");
$one_week = 7 * 24 * 60 * 60;
$october_15th = $october_8th + $one_week;
$october_22nd = $october_15th + $one_week;
$october_29th = $october_22nd + $one_week;
$november_5th = $october_29th + $one_week;
echo date("Y-m-d -> l", $october_8th) . '<br />';
echo date("Y-m-d -> l", $october_15th) . '<br />';
echo date("Y-m-d -> l", $october_22nd) . '<br />';
echo date("Y-m-d -> l", $october_29th) . '<br />';
echo date("Y-m-d -> l", $november_5th) . '<br />';
This would output:
2012-10-08 -> Monday
2012-10-15 -> Monday
2012-10-22 -> Monday
2012-10-28 -> Sunday
2012-11-04 -> Sunday
I would expect it to say the 29th of October but it gets stuck at the 28th.
How should I get around this problem?

A preferred choice would be to use PHP's date-related classes to get the dates.
These classes importantly handle the daylight-savings boundaries for you, in a way that manually adding a given number of seconds to a Unix timestamp (the number from strtotime() that you used) cannot.
The following example takes your start dates and loops four times, each time adding a week to the date.
$start_date = new DateTime('2012-10-08');
$interval = new DateInterval('P1W');
$recurrences = 4;
foreach (new DatePeriod($start_date, $interval, $recurrences) as $date) {
echo $date->format('Y-m-d -> l') . '<br/>';
}
PHP Manual links:
The DatePeriod class
The DateInterval class
The DateTime class

While writing this question I discovered that day light saving time ends at the 28th of October.
Because the date at initialization doesn't contain a specific time automatically midnight is assigned. This however yields a problem when summertime ends. Suddenly the time isn't midnight anymore but one hour before that AND thus a day earlier then you would expect.
An easy fix would be to initialize the time to be midday instead of midnight:
$october_8th = strtotime("2012-10-08 12:00");
Perhaps there might be more elegant solution (you're welcome to leave one), but this will do for this purpose.

Related

Date Difference in Days doesn't calculate correctly

I am not sure if I am going crazy or have missed something simple.
I am simply trying to get the difference between 2 dates in days but it doesn't show what I think it should.
I am currently using the following code
$firstDate = new DateTime("06/09/2021");
$secondDate = new DateTime("05/01/2022");
$intvl = $firstDate->diff($secondDate);
// Total amount of days
echo $intvl->days . " days ";
This returns: 326 days but it should be 121...
Am I doing something wrong here, any help is appreciated!
Your input dates are ambiguous: is "06/09/2021" the 6th September (as a UK reader would assume) or 9th June (as a US reader would assume)? The US interpretation leads to the 326 days PHP is returning; the UK interpretation leads to the 121 days you were expecting.
It's better to either specify the format you're expecting, with DateTime::createFromFormat(...), or to use an unambiguous format, such as "2021-09-06".
You might also want to get into the habit of using the DateTimeImmutable class, which is generally less confusing to work with.
So:
$firstDate = DateTimeImmutable::createFromFormat("d/m/Y", "06/09/2021");
$secondDate = DateTimeImmutable::createFromFormat("d/m/Y", "05/01/2022");
$intvl = $firstDate->diff($secondDate);
echo $intvl->days . " days ";
// 121 days
$firstDate = DateTimeImmutable::createFromFormat("m/d/Y", "06/09/2021");
$secondDate = DateTimeImmutable::createFromFormat("m/d/Y", "05/01/2022");
$intvl = $firstDate->diff($secondDate);
echo $intvl->days . " days ";
// 326 days
$firstDate = new DateTimeImmutable("2021-09-06");
$secondDate = new DateTimeImmutable("2022-01-05");
$intvl = $firstDate->diff($secondDate);
echo $intvl->days . " days ";
// 121 days
9th June 2021 to 1st May 2022 is definitely 326 days.
By any chance are you expecting that PHP will interpret these as 6th September and 5th January? If we read the documentation for the DateTime constructor, the linked article shows us the formats which it can parse by default. Based on what you've provided, it will assume it's m/d/Y rather than d/m/Y.
If you need to parse other formats which aren't supported by the default parser, then you need to use the createFromFormat function to generate the DateTime object, rather than the constructor.
e.g.
$firstDate = DateTime::createFromFormat("d/m/Y", "06/09/2021");
$secondDate = DateTime::createFromFormat("d/m/Y", "05/01/2022");
$intvl = $firstDate->diff($secondDate);
// Total amount of days
echo $intvl->days . " days ";
Demo: http://sandbox.onlinephpfunctions.com/code/2f1689615f5813dbf699d92c70ad1679a9a96e4f
Alternatively, to avoid any ambiguity, it's generally a good idea wherever possible to generate and pass your date strings around in an ISO8601-compatible format which has no other recognised (mis-)interpretations, e.g. Y-m-d:
$firstDate = new DateTime("2021-09-06");
$secondDate = new DateTime("2022-01-05");
$intvl = $firstDate->diff($secondDate);
// Total amount of days
echo $intvl->days . " days ";
Demo: http://sandbox.onlinephpfunctions.com/code/506cbe95dee91736bcc1e33ae57d02cdaf258114

How does daylight saving work in php (difference between php and hhvm)?

I need to detect when a timestamp is in daylight saving or not.
I'm using this code to test the functionality:
<?php
date_default_timezone_set('Europe/Berlin');
$timestamp = $baseTimestamp = 1509234900; // Sunday, 29 October of 2017 1:55:00 GMT+02:00 DST
$date = (new DateTime)->setTimestamp($baseTimestamp);
echo "DateTime\t\t| Is in summer \t| Minutes passed\n";
for($i = 0; $i < 70; $i++) {
$date = (new DateTime)->setTimestamp($timestamp);
echo $date->format("Y-m-d H:i:s \t|I") . "\t\t| " . ($timestamp - $baseTimestamp)/60 . "\n";
$timestamp = $timestamp + 60;
}
https://3v4l.org/dqNlK
Working with Europe/Berlin, I've seen that in March, when at 2.00 we pass from winter to summer, php solves it right, but then in October, when it is supposed to come back from summer to winter at 3.00, it doesn't work as expected.
In this case, we have two timestamps corresponding to the same hour (at 3 is 2 again), but the timestamp is unique, so for 2.00 there must be one timestamp in the summer time and another one in the winter time.
Using hhvm, it shows the right value, but normal php interpreters show that is not in summer for both 2.00 (the first one, which is 2am and the second one which is when at 3am is 2am again. This is the one that should say is not in summer anymore)
Yes, there are several long-lived, unresolved, DST-related bugs in PHP.
You appear to have hit on this one:
https://bugs.php.net/bug.php?id=68549
function isSummer($timestamp) {
$date = (new DateTime)->setTimestamp($timestamp);
if ($date->format('I') == 1) {
return true;
}
return ($date->getTimestamp() > $timestamp);
}
I think this is going to do the trick

PHP date incrementing error

I am trying to create a calendar where the date increments and each date is clickable, which links to a search. The strange part is that the date stops at 25th October, and stops incrementing. i.e 24th, 25th, 25th, 25th...
Its doesn't matter which day the calendar started with (been staring at it for a few days), but at 25th it stops incrementing.
Grateful for any advice.(The 2nd part after the gap is probably irrelevant, but including it in case there could be any link)
for ($i = 1; $i <= 30; $i++){
$date = date("d-m-Y", strtotime($date) + 86400);
array_push($array_date, $date);
$separatedate = explode('-', $date);
$getday = date("l", strtotime($date));
print "<button class='submitsearch btn' value=$array_date[$i]>" . ltrim($separatedate[0], '0') . "<br>" . $getday . "<br></button>";
if (!checkdate($separatedate[1] , $separatedate[0]+1 , $separatedate[2])) {
$nextmonth = date("F", strtotime($date) + 86400);
print "<strong>". $nextmonth . "</strong><hr/>";
}
}
This is the problem:
$date = date("d-m-Y", strtotime($date) + 86400);
You appear to be relying on that to increment the date. Usually, that will be fine... but it isn't when we have a 25 hour day, due to daylight saving time changes.
I suggest you use date/time arithmetic functions (e.g. date_add) designed to add a day, rather than adding 24 hours. Or make sure all arithmetic is done in UTC, which won't have any time zone changes. In general, I would try to avoid performing any more string conversions than you really need to: keep a variable representing the date/time in an idiomatic way, and perform arithmetic on that - then just format that variable when you need to. I don't see any need to call strtotime anywhere, if you do this right.

Choose the preferred offset during a DateTime DST overlap

In time-zones observing Daylight Saving Time, the clock typically:
moves forward during transition from winter to summer
is set back during transition from summer to winter
For example, in the Europe/Paris time-zone, the UTC offset changes from +02:00 to +01:00 during the transition from summer to winter, at 3:00 AM on the last Sunday of the month of October.
In other words:
At 3:00 AM (+02:00) on 2014-10-26, clocks are set back to 2:00 AM (+01:00).
Which means that creating a DateTime for 2014-10-26 at 02:30 AM in the Europe/Paris time-zone is ambiguous, as it can represent either:
2014-10-26T02:30+01:00 (timestamp 1414287000)
2014-10-26T02:30+02:00 (timestamp 1414283400)
Java's ZonedDateTime documentation explains this problem very well, and their API offers a way to choose the preferred offset if needed.
In PHP however, it seems that this ambiguity is resolved by choosing arbitrarily the winter time:
$dt = new DateTime('2014-10-26T02:30', new DateTimeZone('Europe/Paris'));
echo $dt->format(DateTime::ISO8601); // 2014-10-26T02:30:00+0100
echo $dt->getTimestamp(); // 1414287000
echo $dt->getOffset(); // 3600
echo $dt->getTimeZone->getName(); // Europe/Paris
(By arbitrarily, I mean that I could not find any documentation about it).
Is there a way to choose the preferred offset when creating a DateTime from a date and time that fall within a DST overlap for the given time-zone?
Or in other words:
How can I create a DateTime object that would exhibit the following characteristics:
echo $dt->format(DateTime::ISO8601); // 2014-10-26T02:30:00+0200
echo $dt->getTimestamp(); // 1414283400
echo $dt->getOffset(); // 7200
echo $dt->getTimeZone->getName(); // Europe/Paris
That is, an object representing this date/time in the Europe/Paris time-zone in summer time?
First, consider that there is a known bug in PHP that will affect you here. Consider:
$dt = new DateTime('2014-10-26T02:30', new DateTimeZone('Europe/Paris'));
echo $dt->format(DateTime::ISO8601) . " (" . $dt->getTimeStamp() . ")\n";
$dt->setTimeStamp($dt->getTimeStamp() - 3600);
echo $dt->format(DateTime::ISO8601) . " (" . $dt->getTimeStamp() . ")\n";
Output:
2014-10-26T02:30:00+0100 (1414287000)
2014-10-26T02:30:00+0100 (1414287000)
Even though you adjusted the timestamp back an hour to reflect summer time, PHP erroneously advanced it to the winter time position.
You can work around this for display purposes by using UTC as an intermediary.
$tz = new DateTimeZone('Europe/Paris');
$dt = new DateTime('2014-10-26T02:30', $tz);
echo $dt->format(DateTime::ISO8601) . " (" . $dt->getTimeStamp() . ")\n";
$ts = $dt->getTimeStamp() - 3600;
$dt = new DateTime("#$ts", new DateTimeZone('UTC'));
$dt->setTimeZone($tz);
echo $dt->format(DateTime::ISO8601) . " (" . $dt->getTimeStamp() . ")\n";
Output:
2014-10-26T02:30:00+0100 (1414287000)
2014-10-26T02:30:00+0200 (1414287000)
Note that even though the wrong timestamp is returned (it should be 1414283400), it does retain the desired summer-time offset of +0200.
Now, lets tackle the problem of knowing when to apply this. We'll examine the transitions and use that to decide whether or not to subtract an hour.
// set up the original input values
$tz = new DateTimeZone('Europe/Paris');
$dt = new DateTime('2014-10-26T02:30', $tz);
echo $dt->format(DateTime::ISO8601) . "\n";
// check for a transition +/- an hour from the current time stamp
$ts = $dt->getTimestamp();
$transitions = $tz->getTransitions($ts - 3600, $ts + 3600);
if (count($transitions) > 1) {
// see if we are moving backwards, creating the ambiguity
$shift = $transitions[1]['offset'] - $transitions[0]['offset'];
if ($shift < 0)
{
// apply the difference in offsets to move back to summer time
$ts = $ts + $shift;
$dt = new DateTime("#$ts", new DateTimeZone('UTC'));
$dt->setTimeZone($tz);
}
}
echo $dt->format(DateTime::ISO8601) . "\n";
Output:
2014-10-26T02:30:00+0100
2014-10-26T02:30:00+0200
You may also wish to read this related question and answer.

PHP adding exact weekdays to a timestamp

I want to add an x number of week days (e.g. 48 weekday hours) to the current timestamp. I am trying to do this using the following
echo (strtotime('2 weekdays');
However, this doesn't seem to take me an exact 48 hours ahead in time. For example, inputting the current server time of Tuesday 18/03/2014 10:47 returns Thursday 20/03/2014 00:00. using the following function:
echo (strtotime('2 weekdays')-mktime())/86400;
It can tell that it's returning only 1.3 weekdays from now.
Why is it doing this? Are there any existing functions which allow an exact amount of weekday hours?
Given you want to preserve the weekdays functionality and not loose the hours, minutes and seconds, you could do this:
$now = new DateTime();
$hms = new DateInterval(
'PT'.$now->format('H').'H'.
$now->format('i').'M'.
$now->format('s').'S'.
);
$date = new DateTime('2 weekdays');
$date->add($hms);//add hours here again
The reason why weekday doesn't add the hours is because, if you add 1 weekday at any point in time on a monday, the next weekday has to be tuesday.
The hour simply does not matter. Say your date is 2014-01-02 12:12:12, and you want the next weekday, that day starts at 2014-01-03 00:00:00, so that's what you get.
My last solution works though, and here's how: I use the $now instance of DateTime, and its format method to construct a DateInterval format string, to be passed to the constructor. An interval format is quite easy: it starts with P, for period, then a digit and a char to indicate what that digit represents: 1Y for 1 Year, and 2D for 2 Days.
However, we're only interested in hours, minutes and seconds. Actual time, which is indicated using a T in the interval format string, hence we start the string with PT (Period Time).
Using the format specifiers H, i and s, we construct an interval format that in the case of 12:12:12 looks like this:
$hms = new DateInterval(
'PT12H12M12S'
);
Then, it's a simple matter of calling the DateTime::add method to add the hours, minutes and seconds to our date + weekdays:
$weekdays = new DateTime('6 weekdays');
$weekdays->add($hms);
echo $weekdays->format('Y-m-d H:i:s'), PHP_EOL;
And you're there.
Alternatively, you could just use the basic same trick to compute the actual day-difference between your initial date, and that date + x weekdays, and then add that diff to your initial date. It's the same basic principle, but instead of having to create a format like PTXHXMXS, a simple PXD will do.
Working example here
I'd urge you to use the DateInterface classes, as it is more flexible, allows for type-hinting to be used and makes dealing with dates just a whole lot easier for all of us. Besides, it's not too different from your current code:
$today = new DateTime;
$tomorrow = new DateTime('tomorrow');
$dayAfter = new DateTime('2 days');
In fact, it's a lot easier if you want to do frequent date manipulations on a single date:
$date = new DateTime();//or DateTime::createFromFormat('Y-m-d H:i:s', $dateString);
$diff = new DateInterval('P2D');//2 days
$date->add($diff);
echo $date->format('Y-m-d H:i:s'), PHP_EOL, 'is the date + 2 days', PHP_EOL;
$date->sub($diff);
echo $date->format('Y-m-d H:i:s'), PHP_EOL, 'was the original date, now restored';
Easy, once you've spent some time browsing through the docs
I think I have found a solution. It's primitive but after some quick testing it seems to work.
The function calculates the time passed since midnight of the current day, and adds it onto the date returned by strtotime. Since this could fall into a weekend day, I've checked and added an extra day or two accordingly.
function weekDays($days) {
$tstamp = (strtotime($days.' weekdays') + (time() - strtotime("today")));
if(date('D',$tstamp) == 'Sat') {
$tstamp = $tstamp + 86400*2;
}
elseif(date('D',$tstamp) == 'Sun') {
$tstamp = $tstamp + 86400;
}
return $tstamp;
}
Function strtotime('2 weekdays') seems to add 2 weekdays to the current date without the time.
If you want to add 48 hours why not adding 2*24*60*60 to mktime()?
echo(date('Y-m-d', mktime()+2*24*60*60));
The currently accepted solution works, but it will fail when you want to add weekdays to a timestamp that is not now. Here's a simpler snippet that will work for any given point in time:
$start = new DateTime('2021-09-29 15:12:10');
$start->add(date_interval_create_from_date_string('+ 3 weekdays'));
echo $start->format('Y-m-d H:i:s'); // 2021-10-04 15:12:10
Note that this will also work for a negative amount of weekdays:
$start = new DateTime('2021-09-29 15:12:10');
$start->add(date_interval_create_from_date_string('- 3 weekdays'));
echo $start->format('Y-m-d H:i:s'); // 2021-09-24 15:12:10

Categories