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.
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
------------------
......
......@@ -4,6 +4,7 @@ Yii Framework 2 Change Log
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)
......@@ -623,6 +624,7 @@ Yii Framework 2 Change Log
- New: Added various authentication methods, including `HttpBasicAuth`, `HttpBearerAuth`, `QueryParamAuth`, and `CompositeAuth` (qiangxue)
- New: Added `HtmlResponseFormatter` and `JsonResponseFormatter` (qiangxue)
2.0.0-alpha, December 1, 2013
-----------------------------
......
......@@ -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()`.
* 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
-------------------------
......
......@@ -8,6 +8,7 @@
namespace yii\i18n;
use DateTime;
use DateTimeZone;
use IntlDateFormatter;
use NumberFormatter;
use Yii;
......@@ -66,6 +67,9 @@ class Formatter extends Component
* e.g. `UTC`, `Europe/Berlin` or `America/Chicago`.
* 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.
*
* 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;
/**
......@@ -387,8 +391,9 @@ class Formatter extends Component
* types of value are supported:
*
* - an integer representing a UNIX timestamp
* - a string that can be parsed into a UNIX timestamp via `strtotime()`
* - a PHP DateTime object
* - 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.
* If null, [[dateFormat]] will be used.
......@@ -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
* 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 InvalidConfigException if the date format is invalid.
* @return string the formatted result.
* @see dateFormat
*/
public function asDate($value, $format = null)
......@@ -418,8 +423,9 @@ class Formatter extends Component
* types of value are supported:
*
* - an integer representing a UNIX timestamp
* - a string that can be parsed into a UNIX timestamp via `strtotime()`
* - a PHP DateTime object
* - 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.
* If null, [[timeFormat]] will be used.
......@@ -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
* 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 InvalidConfigException if the date format is invalid.
* @return string the formatted result.
* @see timeFormat
*/
public function asTime($value, $format = null)
......@@ -449,8 +455,9 @@ class Formatter extends Component
* types of value are supported:
*
* - an integer representing a UNIX timestamp
* - a string that can be parsed into a UNIX timestamp via `strtotime()`
* - a PHP DateTime object
* - 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.
* If null, [[dateFormat]] will be used.
......@@ -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
* 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 InvalidConfigException if the date format is invalid.
* @return string the formatted result.
* @see datetimeFormat
*/
public function asDatetime($value, $format = null)
......@@ -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 $type 'date', 'time', or 'datetime'.
* @throws InvalidConfigException if the date format is invalid.
......@@ -524,7 +538,7 @@ class Formatter extends Component
$format = FormatConverter::convertDateIcuToPhp($format, $type, $this->locale);
}
if ($this->timeZone != null) {
$timestamp->setTimezone(new \DateTimeZone($this->timeZone));
$timestamp->setTimezone(new DateTimeZone($this->timeZone));
}
return $timestamp->format($format);
}
......@@ -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.
*
* @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
* @throws InvalidParamException if the input value can not be evaluated as a date value.
*/
......@@ -548,17 +569,17 @@ class Formatter extends Component
}
try {
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.");
}
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;
} 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;
}
// finally try to create a DateTime object with the value
$timestamp = new DateTime($value);
$timestamp = new DateTime($value, new DateTimeZone('UTC'));
return $timestamp;
} catch(\Exception $e) {
throw new InvalidParamException("'$value' is not a valid date time value: " . $e->getMessage()
......@@ -623,7 +644,7 @@ class Formatter extends Component
return $this->nullDisplay;
}
} else {
$timezone = new \DateTimeZone($this->timeZone);
$timezone = new DateTimeZone($this->timeZone);
if ($referenceTime === null) {
$dateNow = new DateTime('now', $timezone);
......
......@@ -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
......
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