Commit 6267b9ee by Carsten Brandt

Fixed issue with timezone conversion in formatter

related to #5128
parent 0763cb46
...@@ -88,6 +88,8 @@ See http://site.icu-project.org/ for the format. ...@@ -88,6 +88,8 @@ See http://site.icu-project.org/ for the format.
and now in human readable form. and now in human readable form.
The input value for date and time formatting is assumed to be in UTC unless a timezone is explicitly given.
Formatting Numbers Formatting Numbers
------------------ ------------------
......
...@@ -4,6 +4,7 @@ Yii Framework 2 Change Log ...@@ -4,6 +4,7 @@ Yii Framework 2 Change Log
2.0.0 under development 2.0.0 under development
----------------------- -----------------------
- Bug: Date and time formatting now assumes UTC as the timezone for input dates unless a timezone is explicitly given (cebe)
- Enh #4275: Added `removeChildren()` to `yii\rbac\ManagerInterface` and implementations (samdark) - Enh #4275: Added `removeChildren()` to `yii\rbac\ManagerInterface` and implementations (samdark)
...@@ -623,6 +624,7 @@ Yii Framework 2 Change Log ...@@ -623,6 +624,7 @@ Yii Framework 2 Change Log
- New: Added various authentication methods, including `HttpBasicAuth`, `HttpBearerAuth`, `QueryParamAuth`, and `CompositeAuth` (qiangxue) - New: Added various authentication methods, including `HttpBasicAuth`, `HttpBearerAuth`, `QueryParamAuth`, and `CompositeAuth` (qiangxue)
- New: Added `HtmlResponseFormatter` and `JsonResponseFormatter` (qiangxue) - New: Added `HtmlResponseFormatter` and `JsonResponseFormatter` (qiangxue)
2.0.0-alpha, December 1, 2013 2.0.0-alpha, December 1, 2013
----------------------------- -----------------------------
......
...@@ -13,6 +13,12 @@ Upgrade from Yii 2.0 RC ...@@ -13,6 +13,12 @@ Upgrade from Yii 2.0 RC
* If you've implemented `yii\rbac\ManagerInterface` you need to add implementation for new method `removeChildren()`. * If you've implemented `yii\rbac\ManagerInterface` you need to add implementation for new method `removeChildren()`.
* The input dates for datetime formatting are now assumed to be in UTC unless a timezone is explicitly given.
Before, the timezone assumed for input dates was the default timezone set by PHP which is the same as `Yii::$app->timeZone`.
This causes trouble because the formatter uses `Yii::$app->timeZone` as the default values for output so no timezone conversion
was possible. If your timestamps are stored in the database without a timezone identifier you have to ensure they are in UTC or
add a timezone identifier explicitly.
Upgrade from Yii 2.0 Beta Upgrade from Yii 2.0 Beta
------------------------- -------------------------
......
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
namespace yii\i18n; namespace yii\i18n;
use DateTime; use DateTime;
use DateTimeZone;
use IntlDateFormatter; use IntlDateFormatter;
use NumberFormatter; use NumberFormatter;
use Yii; use Yii;
...@@ -66,6 +67,9 @@ class Formatter extends Component ...@@ -66,6 +67,9 @@ class Formatter extends Component
* e.g. `UTC`, `Europe/Berlin` or `America/Chicago`. * e.g. `UTC`, `Europe/Berlin` or `America/Chicago`.
* Refer to the [php manual](http://www.php.net/manual/en/timezones.php) for available timezones. * Refer to the [php manual](http://www.php.net/manual/en/timezones.php) for available timezones.
* If this property is not set, [[\yii\base\Application::timeZone]] will be used. * If this property is not set, [[\yii\base\Application::timeZone]] will be used.
*
* Note that the input timezone is assumed to be UTC always if no timezone is included in the input date value.
* Make sure to store datetime values in UTC in your database.
*/ */
public $timeZone; public $timeZone;
/** /**
...@@ -387,8 +391,9 @@ class Formatter extends Component ...@@ -387,8 +391,9 @@ class Formatter extends Component
* types of value are supported: * types of value are supported:
* *
* - an integer representing a UNIX timestamp * - an integer representing a UNIX timestamp
* - a string that can be parsed into a UNIX timestamp via `strtotime()` * - a string that can be [parsed to create a DateTime object](http://php.net/manual/en/datetime.formats.php).
* - a PHP DateTime object * The timestamp is assumed to be in UTC unless a timezone is explicitly given.
* - a PHP [DateTime](http://php.net/manual/en/class.datetime.php) object
* *
* @param string $format the format used to convert the value into a date string. * @param string $format the format used to convert the value into a date string.
* If null, [[dateFormat]] will be used. * If null, [[dateFormat]] will be used.
...@@ -399,9 +404,9 @@ class Formatter extends Component ...@@ -399,9 +404,9 @@ class Formatter extends Component
* Alternatively this can be a string prefixed with `php:` representing a format that can be recognized by the * Alternatively this can be a string prefixed with `php:` representing a format that can be recognized by the
* PHP [date()](http://php.net/manual/de/function.date.php)-function. * PHP [date()](http://php.net/manual/de/function.date.php)-function.
* *
* @return string the formatted result.
* @throws InvalidParamException if the input value can not be evaluated as a date value. * @throws InvalidParamException if the input value can not be evaluated as a date value.
* @throws InvalidConfigException if the date format is invalid. * @throws InvalidConfigException if the date format is invalid.
* @return string the formatted result.
* @see dateFormat * @see dateFormat
*/ */
public function asDate($value, $format = null) public function asDate($value, $format = null)
...@@ -418,8 +423,9 @@ class Formatter extends Component ...@@ -418,8 +423,9 @@ class Formatter extends Component
* types of value are supported: * types of value are supported:
* *
* - an integer representing a UNIX timestamp * - an integer representing a UNIX timestamp
* - a string that can be parsed into a UNIX timestamp via `strtotime()` * - a string that can be [parsed to create a DateTime object](http://php.net/manual/en/datetime.formats.php).
* - a PHP DateTime object * The timestamp is assumed to be in UTC unless a timezone is explicitly given.
* - a PHP [DateTime](http://php.net/manual/en/class.datetime.php) object
* *
* @param string $format the format used to convert the value into a date string. * @param string $format the format used to convert the value into a date string.
* If null, [[timeFormat]] will be used. * If null, [[timeFormat]] will be used.
...@@ -430,9 +436,9 @@ class Formatter extends Component ...@@ -430,9 +436,9 @@ class Formatter extends Component
* Alternatively this can be a string prefixed with `php:` representing a format that can be recognized by the * Alternatively this can be a string prefixed with `php:` representing a format that can be recognized by the
* PHP [date()](http://php.net/manual/de/function.date.php)-function. * PHP [date()](http://php.net/manual/de/function.date.php)-function.
* *
* @return string the formatted result.
* @throws InvalidParamException if the input value can not be evaluated as a date value. * @throws InvalidParamException if the input value can not be evaluated as a date value.
* @throws InvalidConfigException if the date format is invalid. * @throws InvalidConfigException if the date format is invalid.
* @return string the formatted result.
* @see timeFormat * @see timeFormat
*/ */
public function asTime($value, $format = null) public function asTime($value, $format = null)
...@@ -449,8 +455,9 @@ class Formatter extends Component ...@@ -449,8 +455,9 @@ class Formatter extends Component
* types of value are supported: * types of value are supported:
* *
* - an integer representing a UNIX timestamp * - an integer representing a UNIX timestamp
* - a string that can be parsed into a UNIX timestamp via `strtotime()` * - a string that can be [parsed to create a DateTime object](http://php.net/manual/en/datetime.formats.php).
* - a PHP DateTime object * The timestamp is assumed to be in UTC unless a timezone is explicitly given.
* - a PHP [DateTime](http://php.net/manual/en/class.datetime.php) object
* *
* @param string $format the format used to convert the value into a date string. * @param string $format the format used to convert the value into a date string.
* If null, [[dateFormat]] will be used. * If null, [[dateFormat]] will be used.
...@@ -461,9 +468,9 @@ class Formatter extends Component ...@@ -461,9 +468,9 @@ class Formatter extends Component
* Alternatively this can be a string prefixed with `php:` representing a format that can be recognized by the * Alternatively this can be a string prefixed with `php:` representing a format that can be recognized by the
* PHP [date()](http://php.net/manual/de/function.date.php)-function. * PHP [date()](http://php.net/manual/de/function.date.php)-function.
* *
* @return string the formatted result.
* @throws InvalidParamException if the input value can not be evaluated as a date value. * @throws InvalidParamException if the input value can not be evaluated as a date value.
* @throws InvalidConfigException if the date format is invalid. * @throws InvalidConfigException if the date format is invalid.
* @return string the formatted result.
* @see datetimeFormat * @see datetimeFormat
*/ */
public function asDatetime($value, $format = null) public function asDatetime($value, $format = null)
...@@ -485,7 +492,14 @@ class Formatter extends Component ...@@ -485,7 +492,14 @@ class Formatter extends Component
]; ];
/** /**
* @param integer $value normalized datetime value * @param integer|string|DateTime $value the value to be formatted. The following
* types of value are supported:
*
* - an integer representing a UNIX timestamp
* - a string that can be [parsed to create a DateTime object](http://php.net/manual/en/datetime.formats.php).
* The timestamp is assumed to be in UTC unless a timezone is explicitly given.
* - a PHP [DateTime](http://php.net/manual/en/class.datetime.php) object
*
* @param string $format the format used to convert the value into a date string. * @param string $format the format used to convert the value into a date string.
* @param string $type 'date', 'time', or 'datetime'. * @param string $type 'date', 'time', or 'datetime'.
* @throws InvalidConfigException if the date format is invalid. * @throws InvalidConfigException if the date format is invalid.
...@@ -524,7 +538,7 @@ class Formatter extends Component ...@@ -524,7 +538,7 @@ class Formatter extends Component
$format = FormatConverter::convertDateIcuToPhp($format, $type, $this->locale); $format = FormatConverter::convertDateIcuToPhp($format, $type, $this->locale);
} }
if ($this->timeZone != null) { if ($this->timeZone != null) {
$timestamp->setTimezone(new \DateTimeZone($this->timeZone)); $timestamp->setTimezone(new DateTimeZone($this->timeZone));
} }
return $timestamp->format($format); return $timestamp->format($format);
} }
...@@ -533,7 +547,14 @@ class Formatter extends Component ...@@ -533,7 +547,14 @@ class Formatter extends Component
/** /**
* Normalizes the given datetime value as a DateTime object that can be taken by various date/time formatting methods. * Normalizes the given datetime value as a DateTime object that can be taken by various date/time formatting methods.
* *
* @param mixed $value the datetime value to be normalized. * @param integer|string|DateTime $value the datetime value to be normalized. The following
* types of value are supported:
*
* - an integer representing a UNIX timestamp
* - a string that can be [parsed to create a DateTime object](http://php.net/manual/en/datetime.formats.php).
* The timestamp is assumed to be in UTC unless a timezone is explicitly given.
* - a PHP [DateTime](http://php.net/manual/en/class.datetime.php) object
*
* @return DateTime the normalized datetime value * @return DateTime the normalized datetime value
* @throws InvalidParamException if the input value can not be evaluated as a date value. * @throws InvalidParamException if the input value can not be evaluated as a date value.
*/ */
...@@ -548,17 +569,17 @@ class Formatter extends Component ...@@ -548,17 +569,17 @@ class Formatter extends Component
} }
try { try {
if (is_numeric($value)) { // process as unix timestamp if (is_numeric($value)) { // process as unix timestamp
if (($timestamp = DateTime::createFromFormat('U', $value)) === false) { if (($timestamp = DateTime::createFromFormat('U', $value, new DateTimeZone('UTC'))) === false) {
throw new InvalidParamException("Failed to parse '$value' as a UNIX timestamp."); throw new InvalidParamException("Failed to parse '$value' as a UNIX timestamp.");
} }
return $timestamp; return $timestamp;
} elseif (($timestamp = DateTime::createFromFormat('Y-m-d', $value)) !== false) { // try Y-m-d format } elseif (($timestamp = DateTime::createFromFormat('Y-m-d', $value, new DateTimeZone('UTC'))) !== false) { // try Y-m-d format (support invalid dates like 2012-13-01)
return $timestamp; return $timestamp;
} elseif (($timestamp = DateTime::createFromFormat('Y-m-d H:i:s', $value)) !== false) { // try Y-m-d H:i:s format } elseif (($timestamp = DateTime::createFromFormat('Y-m-d H:i:s', $value, new DateTimeZone('UTC'))) !== false) { // try Y-m-d H:i:s format (support invalid dates like 2012-13-01 12:63:12)
return $timestamp; return $timestamp;
} }
// finally try to create a DateTime object with the value // finally try to create a DateTime object with the value
$timestamp = new DateTime($value); $timestamp = new DateTime($value, new DateTimeZone('UTC'));
return $timestamp; return $timestamp;
} catch(\Exception $e) { } catch(\Exception $e) {
throw new InvalidParamException("'$value' is not a valid date time value: " . $e->getMessage() throw new InvalidParamException("'$value' is not a valid date time value: " . $e->getMessage()
...@@ -623,7 +644,7 @@ class Formatter extends Component ...@@ -623,7 +644,7 @@ class Formatter extends Component
return $this->nullDisplay; return $this->nullDisplay;
} }
} else { } else {
$timezone = new \DateTimeZone($this->timeZone); $timezone = new DateTimeZone($this->timeZone);
if ($referenceTime === null) { if ($referenceTime === null) {
$dateNow = new DateTime('now', $timezone); $dateNow = new DateTime('now', $timezone);
......
...@@ -483,6 +483,90 @@ class FormatterTest extends TestCase ...@@ -483,6 +483,90 @@ class FormatterTest extends TestCase
} }
public function provideTimezones()
{
return [
['UTC'],
['Europe/Berlin'],
['America/Jamaica'],
];
}
/**
* provide default timezones times input date value
*/
public function provideTimesAndTz()
{
$result = [];
foreach($this->provideTimezones() as $tz) {
$result[] = [$tz[0], 1407674460, 1388580060];
$result[] = [$tz[0], '2014-08-10 12:41:00', '2014-01-01 12:41:00'];
$result[] = [$tz[0], '2014-08-10 12:41:00 UTC', '2014-01-01 12:41:00 UTC'];
$result[] = [$tz[0], '2014-08-10 14:41:00 Europe/Berlin', '2014-01-01 13:41:00 Europe/Berlin'];
$result[] = [$tz[0], '2014-08-10 14:41:00 CEST', '2014-01-01 13:41:00 CET'];
$result[] = [$tz[0], '2014-08-10 14:41:00+0200', '2014-01-01 13:41:00+0100'];
$result[] = [$tz[0], '2014-08-10 14:41:00+02:00', '2014-01-01 13:41:00+01:00'];
$result[] = [$tz[0], '2014-08-10 14:41:00 +0200', '2014-01-01 13:41:00 +0100'];
$result[] = [$tz[0], '2014-08-10 14:41:00 +02:00', '2014-01-01 13:41:00 +01:00'];
$result[] = [$tz[0], '2014-08-10T14:41:00+02:00', '2014-01-01T13:41:00+01:00']; // ISO 8601
}
return $result;
}
/**
* Test timezones with input date and time in other timezones
* @dataProvider provideTimesAndTz
*/
public function testIntlTimezoneInput($defaultTz, $inputTimeDst, $inputTimeNonDst)
{
$this->testTimezoneInput($defaultTz, $inputTimeDst, $inputTimeNonDst);
}
/**
* Test timezones with input date and time in other timezones
* @dataProvider provideTimesAndTz
*/
public function testTimezoneInput($defaultTz, $inputTimeDst, $inputTimeNonDst)
{
date_default_timezone_set($defaultTz); // formatting has to be independent of the default timezone set by PHP
$this->formatter->datetimeFormat = 'yyyy-MM-dd HH:mm:ss';
$this->formatter->dateFormat = 'yyyy-MM-dd';
$this->formatter->timeFormat = 'HH:mm:ss';
// daylight saving time
$this->formatter->timeZone = 'UTC';
$this->assertSame('2014-08-10 12:41:00', $this->formatter->asDatetime($inputTimeDst));
$this->assertSame('2014-08-10', $this->formatter->asDate($inputTimeDst));
$this->assertSame('12:41:00', $this->formatter->asTime($inputTimeDst));
$this->assertSame('1407674460', $this->formatter->asTimestamp($inputTimeDst));
$this->formatter->timeZone = 'Europe/Berlin';
$this->assertSame('2014-08-10 14:41:00', $this->formatter->asDatetime($inputTimeDst));
$this->assertSame('2014-08-10', $this->formatter->asDate($inputTimeDst));
$this->assertSame('14:41:00', $this->formatter->asTime($inputTimeDst));
$this->assertSame('1407674460', $this->formatter->asTimestamp($inputTimeDst));
// non daylight saving time
$this->formatter->timeZone = 'UTC';
$this->assertSame('2014-01-01 12:41:00', $this->formatter->asDatetime($inputTimeNonDst));
$this->assertSame('2014-01-01', $this->formatter->asDate($inputTimeNonDst));
$this->assertSame('12:41:00', $this->formatter->asTime($inputTimeNonDst));
$this->assertSame('1388580060', $this->formatter->asTimestamp($inputTimeNonDst));
$this->formatter->timeZone = 'Europe/Berlin';
$this->assertSame('2014-01-01 13:41:00', $this->formatter->asDatetime($inputTimeNonDst));
$this->assertSame('2014-01-01', $this->formatter->asDate($inputTimeNonDst));
$this->assertSame('13:41:00', $this->formatter->asTime($inputTimeNonDst));
$this->assertSame('1388580060', $this->formatter->asTimestamp($inputTimeNonDst));
// tests for relative time
if ($inputTimeDst !== 1407674460) {
$this->assertSame('3 hours ago', $this->formatter->asRelativeTime($inputTimeDst, $relativeTime = str_replace(['14:41', '12:41'], ['17:41', '15:41'], $inputTimeDst)));
$this->assertSame('in 3 hours', $this->formatter->asRelativeTime($relativeTime, $inputTimeDst));
$this->assertSame('3 hours ago', $this->formatter->asRelativeTime($inputTimeNonDst, $relativeTime = str_replace(['13:41', '12:41'], ['16:41', '15:41'], $inputTimeNonDst)));
$this->assertSame('in 3 hours', $this->formatter->asRelativeTime($relativeTime, $inputTimeNonDst));
}
}
// number format // number format
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment