I am wondering why I have been getting totally invalid computation by PHP all the time.
I have code like this:
echo floor((strtotime("2017-03-27") - strtotime("2017-03-24"))/86400);
which on the one of the server returns: 3 (like 3 days)
and on the another server returns: 2! (2.9583333333333 day?) Why is there a variance?
Or, just use better methods:
EDIT I updated the code to use the same dates as OP. It returns 3, as it should, and I'm fairly confident it will do that on whatever server you put it on to test.
<?php
$date1 = date_create('2017-03-27');
$date2 = date_create('2017-03-24');
$interval = date_diff($date1,$date2);
echo $interval->format('%a');
?>
Ref: http://php.net/manual/en/function.date-diff.php
Your servers maybe sitting in two different time zones. Convert the days to hours and you will get a difference of one hour.
3 days * 24 hours/1 day = 72 hours
2.958333 days * 24 hours/1 day = 71 hours
Since some people are having difficulty accepting that this is due to a TZ-specific DST change, have some proof:
function poc($tz_str) {
$tz = new DateTimeZone($tz_str);
$start = (new DateTime('2017-03-24', $tz))->format('U');
$end = (new DateTime('2017-03-27', $tz))->format('U');
return [$tz_str, $end - $start, ( $end - $start ) / 86400];
}
var_dump(
poc('UTC'),
poc('Europe/London'),
poc('America/New_York')
);
Output:
array(3) {
[0]=> string(3) "UTC"
[1]=> int(259200)
[2]=> int(3)
}
array(3) {
[0]=> string(13) "Europe/London"
[1]=> int(255600)
[2]=> float(2.9583333333333)
}
array(3) {
[0]=> string(16) "America/New_York"
[1]=> int(259200)
[2]=> int(3)
}
UTC has no DST, not affected.
Most of the eastern hemisphere has their DST change on March 26, affected.
Most of the western hemisphere has their DST change on March 12, not affected.
That said, don't do manual date math on a Unix timestamp.
Related
is there a good way to calculate months diff between dates without days? I mean I have two not full dates for example:
2017-09 and 2018-11. I need to calculate how many months is between this two dates. I read something about this and I know I can for example use:
$firstDate = "2017-09";
$secondDate = "2018-11";
$firstDate = new DateTime($firstDate . "-01");
$secondDate = new DateTime($secondDate . "-01");
$interval = date_diff($firstDate, $secondDate);
var_dump($interval->format('%m months'));exit();
This show me 2 months.
How can I reach this? And is there a way to calculate this without adding days "-01" to end of my dates?
I want to calculate difference of months for dates without write "-01" in this dates. Only year and month.
You need the years also.
Also you had a parse error on the date 2017-0901 is not valid either 2017-09 or 2017-09-01.
$firstDate = "2017-09";
$secondDate = "2018-11";
$firstDate = new DateTime($firstDate);
$secondDate = new DateTime($secondDate);
$interval = date_diff($firstDate, $secondDate);
echo $interval->format('%y')*12+$interval->format('%m') . " months";
// 14 months
https://3v4l.org/XGdXg
Just use plain arithmetic. We're clearly not concerned about timezones, daylight saving, calendar changes, etc. so we're also not concerned about parsing the date "in some timezone, based on some calendar". What we're left with is just plain arithemtics, using months. A year is 12 months. And now we're almost done already.
function ym_as_months($v) {
$v = array_map("intval", explode("-", $v));
return $v[0]*12 + $v[1];
}
$firstDate = "2017-09";
$firstMonths = ym_as_months($firstDate);
$secondDate = "2018-11";
$secondMonths = ym_as_months($secondDate);
$diff = $secondMonths - $firstMonths;
echo "There are $diff months between $firstDate and $secondDate.";
And we get:
There are 14 months between 2017-09 and 2018-11.
Perfect.
Of course, depending on how you get those date stamps in your application, it might be far easier to not even pass them in as string, but simply as two numbers from the get go, in which case this becomes even less work.
Alternatively, do your conversion as the very last step, as per another answer here.
I will suggest using Carbon for dates related calculations in PHP as it really makes everything easy.
To calculate months between two dates with Carbon, you simply need to do this
//year & month
$startDate = Carbon::create('2017', '9');
$endDate = Carbon::create('2018','11');
$diff = $startDate->diffInMonths($endDate);
https://secure.php.net/manual/en/datetime.createfromformat.php
$a = DateTime::createFromFormat('Y-m', '2017-09');
$b = DateTime::createFromFormat('Y-m', '2018-11');
$diff = $a->diff($b);
var_dump(
$a, $b,
$diff->format("%y years, %m months"),
sprintf("%d months", $diff->y * 12 + $diff->m)
);
Output:
object(DateTime)#1 (3) {
["date"]=>
string(26) "2017-09-06 18:33:58.000000"
["timezone_type"]=>
int(3)
["timezone"]=>
string(16) "Europe/Amsterdam"
}
object(DateTime)#2 (3) {
["date"]=>
string(26) "2018-11-06 18:33:58.000000"
["timezone_type"]=>
int(3)
["timezone"]=>
string(16) "Europe/Amsterdam"
}
string(17) "1 years, 2 months"
string(9) "14 months"
This morning I ran unit tests and they failed.
Tests took offers from last 48 hours by calculating time with:
date('U', time() - 48 * 3600)
When I used:
(new DateTime('-48 hours'))->format('U')
it shown one hour difference.
May the reason be that on sunday in Poland time was moved one hour backwards
I presume your server runs with Europe/Warsaw as default time zone. PHP date/time calculations are often incorrect if they cross DST boundaries, as it's the case here. I suggest you do all maths in UTC and convert from/to local time as needed.
Please compare:
$warsaw = new DateTimeZone('Europe/Warsaw');
$utc = new DateTimeZone('UTC');
$start = new DateTime('2018-10-28 04:30:00', $warsaw);
$start->modify('-4 hours');
var_dump($start);
$start = new DateTime('2018-10-28 04:30:00', $warsaw);
$start->setTimezone($utc);
$start->modify('-4 hours');
$start->setTimezone($warsaw);
var_dump($start);
object(DateTime)#3 (3) {
["date"]=>
string(26) "2018-10-28 00:30:00.000000"
["timezone_type"]=>
int(3)
["timezone"]=>
string(13) "Europe/Warsaw"
}
object(DateTime)#4 (3) {
["date"]=>
string(26) "2018-10-28 01:30:00.000000"
["timezone_type"]=>
int(3)
["timezone"]=>
string(13) "Europe/Warsaw"
}
The Unix timestamp version of your code should be unaffected because a Unix timestamp is a fixed moment in time (thus doesn't switch with time zones).
When I run this the first one is correctly created into a date. The second one fails, returning a boolean and so I cannot format. Is the time out of range?
//works correctly
$startDate = "2015-05-06 10:49:20.637133";
$start = DateTime::createFromFormat('Y-m-d h:m:s.u',$startDate);
echo $start->format('m/d/y');
//doesn't work correctly
$startDate = "2015-05-12 15:49:06.821289";
$start = DateTime::createFromFormat('Y-m-d h:m:s.u',$startDate);
echo $start->format('m/d/y');
Code to reproduce the error
Check DateTime::getLastErrors():
php > var_dump(DateTime::createFromFormat('Y-m-d h:m:s',"2015-05-12 15:49:06"));
bool(false)
php > var_dump(DateTime::getLastErrors());
array(4) {
["warning_count"]=>
int(1)
["warnings"]=>
array(1) {
[19]=>
string(27) "The parsed date was invalid"
}
["error_count"]=>
int(1)
["errors"]=>
array(1) {
[11]=>
string(30) "Hour can not be higher than 12"
Change the h to a big H, since the small one is 12-hours format and the big one is 24-hours format.
You can see all formats in the manual. And a quote from there:
h 12-hour format of an hour with leading zeros 01 through 12
H 24-hour format of an hour with leading zeros 00 through 23
Means right now your code fails, because there is no 15 in the 12 hour format.
In addition to the other answers, for standard formats understood by DateTime you don't need to create from a format:
$startDate = "2015-05-12 15:49:06.821289";
$start = new DateTime($startDate);
echo $start->format('m/d/y');
I'm trying to compare to DateTime objects in PHP.
$Time1 = DateTime::createFromFormat('UP', '1409900072+0200');
$Time2 = new DateTime('2014-09-05 07:54:32');
The Time2 use the defoult which is Europe/Copenhagen, comparing yields the following
if ($Time2 > $Time1){
echo "true \n";
} else {
echo "false \n";
}
true
object(DateTime)#1 (3) {
["date"]=>
string(19) "2014-09-05 06:54:32"
["timezone_type"]=>
int(1)
["timezone"]=>
string(6) "+02:00"
}
object(DateTime)#2 (3) {
["date"]=>
string(19) "2014-09-05 07:54:32"
["timezone_type"]=>
int(3)
["timezone"]=>
string(17) "Europe/Copenhagen"
}
The way I understand it is the actual local time for Time1 is 08:54:32, so how can I get the comparison at the same timezone?
Thanks in advance
The really weird part is your initial value of 1409900072+0200. If 1409900072 is a UNIX timestamp, passing a particular timezone with it makes little sense. And it seems to cause PHP to create the instance incorrectly; it creates the instance with the time set to the UTC value (6:54), but the timezone offset of +0200 (where the time should actually be 8:54).
Arguably this should be filed as a bug report; but arguably the input data is nonsensical to begin with.
If you're feeding in a UNIX timestamp, then ignore any timezone information it may contain and explicitly set the timezone to UTC, then it all works as expected:
$t1 = DateTime::createFromFormat('U+', '1409900072+0200', new DateTimeZone('UTC'));
$t2 = new DateTime('2014-09-05 07:54:32', new DateTimeZone('Europe/Copenhagen'));
var_dump($t1 > $t2); // true
Note that PHP before 5.3.9 seems to have problems with the createFromFormat call; you'll probably have to filter out the trailing timezone by hand if you need to support those versions.
Convert both DateTimes to UTC (setTimeZone('UTC')) and then compare them.
<?php
$Time1 = DateTime::createFromFormat('UP', '1409900072+0200');
$Time2 = new DateTime('2014-09-05 07:54:32');
// convert
$utc = new DateTimeZone('UTC');
$time1_utc = clone $Time1;
$time1_utc->setTimeZone($utc);
$time2_utc = clone $Time2;
$time2_utc->setTimeZone($utc);
var_dump($Time1,$Time2);
var_dump($time1_utc,$time2_utc);
I have a string in this format: 2013-07-31T19:20:30.45-07:00 and I want to parse it so that I can, for example, say what day of the week it is. But I'm struggling to cope with the timezone offset. If I do date_parse("2013-07-31T19:20:30.45-07:00") then I end up with something like this:
array(15) {
["year"]=> int(2013)
["month"]=> int(7)
["day"]=> int(31)
["hour"]=> int(19)
["minute"]=> int(20)
["second"]=> int(30)
["fraction"]=> float(0.45)
["warning_count"]=> int(0)
["warnings"]=> array(0) { }
["error_count"]=> int(0)
["errors"]=> array(0) { }
["is_localtime"]=> bool(true)
["zone_type"]=> int(1)
["zone"]=> int(420)
["is_dst"]=> bool(false)
}
It's done something with the timezone, but what do I do with 420 if I want to, for example, show information about the timezone?
In case it matters, I have previously set my default timezone using date_default_timezone_set('UTC').
UPDATE: If the string has a positive timezone, eg 2013-07-31T19:20:30.45+07:00 then the last part of the date_parse() output is:
["is_localtime"]=> bool(true)
["zone_type"]=> int(1)
["zone"]=> int(-420)
["is_dst"]=> bool(false)
}
420 is zone in minutes.
420/60 = 7
I want to parse it so that I can, for example, say what day of the
week it is.
If you want to know what day of the week it is, you have many options. For example you can use date and mktime-functions:
$parsed = date_parse("2013-07-31T19:20:30.45-07:00");
$unix_timestamp = mktime($parsed['hour'], 0, 0, $parsed['month'], $parsed['day'], $parsed['year'], (int)$parsed['is_dst']);
echo date('l', $unix_timestamp);
So you want to show the information about timezone? You can get the time zone name by using timezone_name_from_abbr function:
$name = timezone_name_from_abbr("", -1*$parsed['zone']*60, false); // NB: Offset in seconds!
var_dump($name);
$timezone = new DateTimezone($name);
var_dump($timezone);
2013-07-31T19:20:30.45-07:00
^ y-m-d ^ time ^ timezone offset
I'm guessing the timezone is -7 hours from UTC.
Keep in mind that some countries have half-hour timezones, or even minute timezones. This is probably why you get the timezone in minutes.