PHP IntlDateFormatter wrong date/time conversion - php

I recently stumbled on a problem with PHP v7.0.4. when trying to format some dates in the past.
I work on a project, in which there is a thing called "empty date", basically a "1800-01-01" (used instead of the NULL value). I'm using GMT+1, "Europe/Berlin".
In process of handling it, and with the Date localization involved, the IntlDateFormatter started making some issues and I chased them down to having exceptions if the dates are <= 1893-04-01 (an early April fool thing?).
You can see some interesting examples below. Could someone please confirm that they get the same issue on their system? It should be reproducible with:
$formatter = new \IntlDateFormatter('en_US', \IntlDateFormatter::MEDIUM, \IntlDateFormatter::LONG);
echo $formatter->format(new \DateTime("1893-04-01 00:00:00")) . '<br />';
echo $formatter->format(new \DateTime("1893-04-02 00:00:00")) . '<br />';
Which should return:
"Mar 31, 1893, 11:53:28 PM GMT+0:53:28" and
"Apr 2, 1893, 12:00:00 AM GMT+1"
Or for even more visible behavior "change":
echo $formatter->format(new \DateTime("1893-04-01 00:06:31")) . '<br />';
echo $formatter->format(new \DateTime("1893-04-01 00:06:32")) . '<br />';
which should return:
"Mar 31, 1893, 11:59:59 PM GMT+0:53:28" and
"Apr 1, 1893, 12:06:32 AM GMT+1"
I presume it has something to do with historical changes of the timezones (something like this: https://github.com/eggert/tz/blob/2017b/asia#L891, although that is about Asia, and I'm using the GMT+1).
If we assume that I would actually need to use this date, 01.01.1800 - would anyone see any "normal" way around this "problem"?

You're correct, it has to do with a historical change of the timezone. The GMT offset for Europe/Berlin is +0:53:28 until April of 1893 when it jumps to a full +1.
Thus its very first GMT+1 was:
Apr 1, 1893, 12:06:32 AM GMT+1
as the second before that was:
Mar 31, 1893, 11:59:59 PM GMT+00:53:28.
Basically this means that Apr 1 1893 00:00:00 through 00:06:31 didn't exist.
The normal way of avoiding a lot of confusion around this sorta thing is to work only in UTC and deal with timezone conversions just for display. For example:
date_default_timezone_set('UTC');
$formatter = new \IntlDateFormatter(
'en_US',
\IntlDateFormatter::MEDIUM,
\IntlDateFormatter::LONG,
'Europe/Berlin'
);
echo $formatter->format(new \DateTime("1800-01-01 00:00:00")) , "\n";
echo $formatter->format(new \DateTime("1893-03-31 23:06:31")) , "\n";
echo $formatter->format(new \DateTime("1893-03-31 23:06:32")) , "\n";
echo $formatter->format(new \DateTime("1893-04-01 00:00:00")) , "\n";
Outputs:
Jan 1, 1800, 12:53:28 AM GMT+0:53:28
Mar 31, 1893, 11:59:59 PM GMT+0:53:28
Apr 1, 1893, 12:06:32 AM GMT+1
Apr 1, 1893, 1:00:00 AM GMT+1
If you'd rather you can also force IntlDateFormatter to use GMT+1 instead of your timezone:
$formatter = new \IntlDateFormatter(
'en_US',
\IntlDateFormatter::MEDIUM,
\IntlDateFormatter::LONG,
'GMT+01:00'
);

Related

Using strtotime with TimeZone

I have a problem. I want to convert this DateTime: 2018-10-28 02:00:00 to a TimeStamp. Now the TimeStamp I am looking for is: 1540684800, but with my code I get this TimeStamp: 1540688400. I know it has something to do with my TimeZone, but I don't know how I can fix this.
I live in the Netherlands in Amsterdam.
Here is my code:
$LoopDateTime = "2018-10-28 02:00:00";
$search_key = (strtotime($LoopDateTime)*1000);
Can someone help me?
The time zone identifier for Amsterdam is Europe/Amsterdam and 1540688400 is the correct timestamp. There's surely an online tool to check but you can also verify it from PHP itself:
$date = new DateTime("#1540688400");
$date->setDateTimeZone(new DateTimeZone('Europe/Amsterdam'));
echo $date->format('r'); // Sun, 28 Oct 2018 02:00:00 +0100
However your code is not robust because depends on the configured timezone. You can just set it explicitly in a number of ways, e.g.:
$LoopDateTime = "2018-10-28 02:00:00";
$search_key = strtotime($LoopDateTime . ' Europe/Amsterdam') * 1000;
var_dump($search_key); // int(1540688400000)
Or:
date_default_timezone_set('Europe/Amsterdam');
$LoopDateTime = "2018-10-28 02:00:00";
$search_key = strtotime($LoopDateTime) * 1000;
var_dump($search_key); // int(1540688400000)
P.S. If I'm not wrong Sunday 28 Oct 2018 02:00:00 +0100 is the exact moment when most Europe has just switched from CEST (+0200) to CET (+0100).

Event Relative Time

So here's the scenario:
My application has a transaction section in it. It logs actions and stores the time as a UNIX timestamp (UTC).
So yesterday I got paid £100.00 and this was logged into the database with a unix timestamp of 1477762205 which equates to Sat, 29 Oct 2016 17:30:05 UTC/GMT.
(This is 19:30 local time for me)
My application (written in PHP) outputs using a datetime object and passing in the output timezone (Europe/London).
Yesterday it outputted correctly local time as 19:30 and due to the clock time change this morning at 2am it now reads 18:30.
This is technically correct but if I remember back relative to the event when logging the transaction in the first place this will now seem incorrect as I remember it being 19:30 local time.
My question is what is the best way to output the time from UTC, relative to the event? Do I need to store the timezone with the UTC log to determine the timezone at the point the log took place?
Code:
When storing the log:
(This is part of a logger class I'm creating)
if($dateTime == null) {
$dateTime = time();
}
$query = "INSERT INTO `".$this->databaseName."`.`log`(`datetime`,`action`) VALUES(?, ?);";
if(!$stmt = $this->database->prepare($query)) {
return $this->database->error;
}
$stmt->bind_param('ss',$dateTime,$action);
$stmt->execute();
When outputting this:
$dateTime = new DateTime(null, new DateTimeZone('UTC'));
$dateTime->setTimestamp($log->timeStamp);
$dateTime->setTimezone(new DateTimeZone('Europe/London'));
echo $dateTime->format('d/m/Y H:i');
Techincally correct output: 29/10/2016 18:30
But I wish for it to display being DST aware relative to the actual timestamp, not the DST relative to now.
So I wish for it to output: 29/10/2016 19:30
First of all, let's summarise the expected local values for 1477762205 Unix timestamp:
UTC: 17:30:05 (UTC, +0000)
London: 18:30:05 (BST, +0100 aka 3600, DST=1)
Madrid: 19:30:05 (CEST, +0200 aka 7200, DST=1)
Stuff seems to work as expected as long as you use city-based time zone identifiers:
foreach (['UTC', 'Europe/London', 'Europe/Madrid'] as $time_zone_id) {
$dt = new DateTime('#1477762205');
$tz = new DateTimeZone($time_zone_id);
$dt->setTimezone($tz);
echo $time_zone_id . ': ' . $dt->format('H:i:s [e=T, O]') . PHP_EOL;
}
UTC: 17:30:05 [UTC=UTC, +0000]
Europe/London: 18:30:05 [Europe/London=BST, +0100]
Europe/Madrid: 19:30:05 [Europe/Madrid=CEST, +0200]
As soon as we use named zones acronyms, strange things happen:
foreach (['UTC', 'BST', 'GMT', 'CEST', 'CET'] as $time_zone_id) {
$dt = new DateTime('#1477762205');
$tz = new DateTimeZone($time_zone_id);
$dt->setTimezone($tz);
echo $time_zone_id . ': ' . $dt->format('H:i:s [e=T, O]') . PHP_EOL;
}
UTC: 17:30:05 [UTC=UTC, +0000]
BST: 17:30:05 [BST=BST, +0000]
GMT: 17:30:05 [GMT=GMT, +0000]
CEST: 18:30:05 [CEST=CEST, +0100]
CET: 18:30:05 [CET=CET, +0100]
There's probably some relation with the information (or lack of it) about time zone transitions available in the underlying database:
$dt = new DateTime('#1477762205');
foreach (['UTC', 'BST', 'Europe/London', 'CEST', 'Europe/Madrid'] as $time_zone_id) {
$tz = new DateTimeZone($time_zone_id);
$dt->setTimezone($tz);
echo $time_zone_id . PHP_EOL;
echo '- Time zone offset: ' . $tz->getOffset($dt) . ' seconds' . PHP_EOL;
$transitions = $tz->getTransitions(mktime(0, 0, 0, 1, 1, 2016), mktime(0, 0, 0, 12, 31, 2016));
if ($transitions===false) {
echo '- Error fetching transitions' . PHP_EOL;
} else {
echo '- ' . count($transitions) . ' transitions found' . PHP_EOL;
}
}
UTC
- Time zone offset: 0 seconds
- 1 transitions found
BST
- Time zone offset: 0 seconds
- Error fetching transitions
Europe/London
- Time zone offset: 3600 seconds
- 3 transitions found
CEST
- Time zone offset: 3600 seconds
- Error fetching transitions
Europe/Madrid
- Time zone offset: 7200 seconds
- 3 transitions found
It's really hard to say how much of this is a plain bug and how much is counter-intuitive but documented; the PHP bug database is crowded with not a bug entries which are indeed a misunderstanding but I've personally found weird but actual bugs in date calculations involving DST boundaries.

Zend_Date, ISO_8601, date parsing and local system clock

I have a strange problem with Zend_Date object.
It seems that setters perform different operations with different system clock dates.
Let's assume that system date is 28 January 2013, following code:
$now=new Zend_Date(Zend_Date::ISO_8601);
$now->now();
echo '<br/>now: ' . $now->toString();
echo '<br/>now->day: ' . $now->get(Zend_Date::DAY);
echo '<br/>now->month: ' . $now->get(Zend_Date::MONTH);
echo '<br/>now->year: ' . $now->get(Zend_Date::YEAR);
$end=new Zend_Date('2013-02-25 14:23:34', Zend_Date::ISO_8601);
echo '<br/>end: ' . $end->toString();
$end->setHour('23')->setMinute('59')->setSecond('59')->setDay($now->get(Zend_Date::DAY))->setMonth($now->get(Zend_Date::MONTH))->setYear($now->get(Zend_Date::YEAR));
echo '<br/>endAfterSetters: ' . $end->toString();
will produce following output:
now: 28-01-2013 14:04:28
now->day: 28
now->month: 01
now->year: 2013
end: 25-02-2013 14:23:34
endAfterSetters: 28-01-2013 23:59:59
But if you change system clock to 29 January 2013, output is different from expectations:
now: 29-01-2013 14:07:22
now->day: 29
now->month: 01
now->year: 2013
end: 25-02-2013 14:23:34
endAfterSetters: 01-01-2013 23:59:59
Last output is 01-01-2013 23:59:59, but should be 29-01-2013 23:59:59 !
It happens on PHP 5.3.2 and 5.3.16, Zend_Framework 10.7, latest Zend_Date 24880 version.
Everyting worked fine in the past.
Any ideas why it happens?
P.S.: I have also found jquery datatime plugin malfunciton while using it at 29,30,31 January... But i will describe it in other question.
Remember that your setters are called in sequence. So when you call setDay(29) you're telling it to change the date to 29th February 2013, which isn't a valid date, so it's rolling that over to make it 1st March 2013. Then you call setMonth(1), which changes the month to January, giving you 1st January 2013.
You can control this behaviour by passing the extend_month option to the Zend_Date constructor, see: http://framework.zend.com/manual/1.12/en/zend.date.overview.html#zend.date.options.extendmonth

PHP Timestamp into DateTime

Do you know how I can convert this to a strtotime, or a similar type of value to pass into the DateTime object?
The date I have:
Mon, 12 Dec 2011 21:17:52 +0000
What I've tried:
$time = substr($item->pubDate, -14);
$date = substr($item->pubDate, 0, strlen($time));
$dtm = new DateTime(strtotime($time));
$dtm->setTimezone(new DateTimeZone(ADMIN_TIMEZONE));
$date = $dtm->format('D, M dS');
$time = $dtm->format('g:i a');
The above is not correct. If I loop through a lot of different dates its all the same date.
You don't need to turn the string into a timestamp in order to create the DateTime object (in fact, its constructor doesn't even allow you to do this, as you can tell). You can simply feed your date string into the DateTime constructor as-is:
// Assuming $item->pubDate is "Mon, 12 Dec 2011 21:17:52 +0000"
$dt = new DateTime($item->pubDate);
That being said, if you do have a timestamp that you wish to use instead of a string, you can do so using DateTime::setTimestamp():
$timestamp = strtotime('Mon, 12 Dec 2011 21:17:52 +0000');
$dt = new DateTime();
$dt->setTimestamp($timestamp);
Edit (2014-05-07):
I actually wasn't aware of this at the time, but the DateTime constructor does support creating instances directly from timestamps. According to this documentation, all you need to do is prepend the timestamp with an # character:
$timestamp = strtotime('Mon, 12 Dec 2011 21:17:52 +0000');
$dt = new DateTime('#' . $timestamp);
While #drrcknlsn is correct to assert there are multiple ways to convert a time string to a datatime, it's important to realize that these different ways don't deal with timezones in the same way.
Option 1 : DateTime('#' . $timestamp)
Consider the following code :
date_format(date_create('#'. strtotime('Mon, 12 Dec 2011 21:17:52 +0800')), 'c');
The strtotime bit eliminates the time zone information, and the date_create function assumes GMT.
As such, the output will be the following, no matter which server I run it on :
2011-12-12T13:17:52+00:00
Option 2 : date_create()->setTimestamp($timestamp)
Consider the following code :
date_format(date_create()->setTimestamp(strtotime('Mon, 12 Dec 2011 21:17:52 +0800')), 'c');
You might expect this to produce the same output. However, if I execute this code from a Belgian server, I get the following output :
2011-12-12T14:17:52+01:00
Unlike the date_create function, the setTimestamp method assumes the time zone of the server (CET in my case) rather than GMT.
Explicitly setting your time zone
If you want to make sure your output matches the time zone of your input, it's best to set it explicitly.
Consider the following code :
date_format(date_create('#'. strtotime('Mon, 12 Dec 2011 21:17:52 +0800'))->setTimezone(new DateTimeZone('Asia/Hong_Kong')), 'c')
Now, also consider the following code :
date_format(date_create()->setTimestamp(strtotime('Mon, 12 Dec 2011 21:17:52 +0800'))->setTimezone(new DateTimeZone('Asia/Hong_Kong')), 'c')
Because we explicitly set the time zone of the output to match that of the input, both will create the same (correct) output :
2011-12-12T21:17:52+08:00
Probably the simplest solution is just:
DateTime::createFromFormat('U', $timeStamp);
Where 'U' means Unix epoch. See docs: http://php.net/manual/en/datetime.createfromformat.php
This is my solution:
function changeDateTimezone($date, $from='UTC', $to='Asia/Tehran', $targetFormat="Y-m-d H:i:s") {
$date = new DateTime($date, new DateTimeZone($from));
$date->setTimeZone(new DateTimeZone($to));
return $date->format($targetFormat);
}

strtotime time zone offset with UTC?

The $bet_closing is set to this in the WordPress admin: November 9, 2011 6:59AM GMT
Which is 12:01 am PST (California Time) on November 9th. But I want to be able to add another option in the admin so they can select a timezone based on UTC. In my own state it's UTC -7. I tried this "November 9, 2011 6:59AM GMT -0700" but no go.
// Auto Close Bet
$now = time();
$bet_closing = strtotime(get_option('cp_gamble1_endtime'));
if ($bet_closing > $now) { // Betting Open!
$output .= 'On';
$cp_gamble1_onoff = true;
update_option('cp_gamble1_onoff', $cp_gamble1_onoff);
} else {
$cp_gamble1_onoff = false;
update_option('cp_gamble1_onoff', $cp_gamble1_onoff);
$output .= 'Over';
}
I'm sure there is a better way to do this. If there is I'm all ears :)
Just tested this (I put the command and the next line gives the result):
var_dump(strtotime("November 9, 2011 6:59AM GMT"));
int(1320821940)
the same as:
var_dump(strtotime("November 9, 2011 6:59AM GMT -0700"));
int(1320821940)
But without GMT works well:
var_dump(strtotime("November 9, 2011 6:59AM -0700"));
int(1320847140)
You can use date_default_timezone_set() function to set timezone before calling strtotime() function

Categories