diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 3064b93abb..1347813cd9 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -18,6 +18,7 @@ Yii Framework 2 Change Log - Bug #7890: Allow `migrate/mark` to mark history at the point of the base migration (cebe) - Bug #11825: User can login by cookie only once when `autoRenewCookie` is set to false (shirase, silverfire) - Bug #12860: Fixed possible race conditions in `yii\mutex\FileMutex` (kidol) +- Bug #13720: Improve `yii\helpers\FormatConverter::convertDatePhpToIcu()` to handle escaped chars correctly (rob006) - Bug #13757: Fixed ambiguous column error in `BaseActiveRecord::refresh()` when the query adds a JOIN by default (cebe, ivankff) - Bug #13779: Fixed `yii\db\ActiveRecord::joinWith()` unable to use relation defined via attached behavior (ElisDN, klimov-paul) - Bug #13859: Fixed ambiguous column error in `Query::column()` when `$indexBy` is used with a JOIN (cebe) diff --git a/framework/helpers/BaseFormatConverter.php b/framework/helpers/BaseFormatConverter.php index 29e538fe9b..c54a8e8587 100644 --- a/framework/helpers/BaseFormatConverter.php +++ b/framework/helpers/BaseFormatConverter.php @@ -130,7 +130,7 @@ class BaseFormatConverter } return strtr($pattern, array_merge($escaped, [ - '\'\'' => '\\\'', // two single quotes produce one + "''" => "\\'", // two single quotes produce one 'G' => '', // era designator like (Anno Domini) 'Y' => 'o', // 4digit year of "Week of Year" 'y' => 'Y', // 4digit year e.g. 2014 @@ -236,66 +236,110 @@ class BaseFormatConverter /** * Converts a date format pattern from [php date() function format][] to [ICU format][]. * - * The conversion is limited to date patterns that do not use escaped characters. - * Patterns like `jS \o\f F Y` which will result in a date like `1st of December 2014` may not be converted correctly - * because of the use of escaped characters. - * * Pattern constructs that are not supported by the ICU format will be removed. * * [php date() function format]: http://php.net/manual/en/function.date.php * [ICU format]: http://userguide.icu-project.org/formatparse/datetime#TOC-Date-Time-Format-Syntax * + * Since 2.0.13 it handles escaped characters correctly. + * * @param string $pattern date format pattern in php date()-function format. * @return string The converted date format pattern. */ public static function convertDatePhpToIcu($pattern) { // http://php.net/manual/en/function.date.php - return strtr($pattern, [ + $result = strtr($pattern, [ + "'" => "''''", // single `'` should be encoded as `''`, which internally should be encoded as `''''` // Day + '\d' => "'d'", 'd' => 'dd', // Day of the month, 2 digits with leading zeros 01 to 31 + '\D' => "'D'", 'D' => 'eee', // A textual representation of a day, three letters Mon through Sun + '\j' => "'j'", 'j' => 'd', // Day of the month without leading zeros 1 to 31 + '\l' => "'l'", 'l' => 'eeee', // A full textual representation of the day of the week Sunday through Saturday + '\N' => "'N'", 'N' => 'e', // ISO-8601 numeric representation of the day of the week, 1 (for Monday) through 7 (for Sunday) + '\S' => "'S'", 'S' => '', // English ordinal suffix for the day of the month, 2 characters st, nd, rd or th. Works well with j + '\w' => "'w'", 'w' => '', // Numeric representation of the day of the week 0 (for Sunday) through 6 (for Saturday) + '\z' => "'z'", 'z' => 'D', // The day of the year (starting from 0) 0 through 365 // Week + '\W' => "'W'", 'W' => 'w', // ISO-8601 week number of year, weeks starting on Monday (added in PHP 4.1.0) Example: 42 (the 42nd week in the year) // Month + '\F' => "'F'", 'F' => 'MMMM', // A full textual representation of a month, January through December + '\m' => "'m'", 'm' => 'MM', // Numeric representation of a month, with leading zeros 01 through 12 + '\M' => "'M'", 'M' => 'MMM', // A short textual representation of a month, three letters Jan through Dec + '\n' => "'n'", 'n' => 'M', // Numeric representation of a month, without leading zeros 1 through 12, not supported by ICU but we fallback to "with leading zero" + '\t' => "'t'", 't' => '', // Number of days in the given month 28 through 31 // Year + '\L' => "'L'", 'L' => '', // Whether it's a leap year, 1 if it is a leap year, 0 otherwise. + '\o' => "'o'", 'o' => 'Y', // ISO-8601 year number. This has the same value as Y, except that if the ISO week number (W) belongs to the previous or next year, that year is used instead. + '\Y' => "'Y'", 'Y' => 'yyyy', // A full numeric representation of a year, 4 digits Examples: 1999 or 2003 + '\y' => "'y'", 'y' => 'yy', // A two digit representation of a year Examples: 99 or 03 // Time + '\a' => "'a'", 'a' => 'a', // Lowercase Ante meridiem and Post meridiem, am or pm + '\A' => "'A'", 'A' => 'a', // Uppercase Ante meridiem and Post meridiem, AM or PM, not supported by ICU but we fallback to lowercase + '\B' => "'B'", 'B' => '', // Swatch Internet time 000 through 999 + '\g' => "'g'", 'g' => 'h', // 12-hour format of an hour without leading zeros 1 through 12 + '\G' => "'G'", 'G' => 'H', // 24-hour format of an hour without leading zeros 0 to 23h + '\h' => "'h'", 'h' => 'hh', // 12-hour format of an hour with leading zeros, 01 to 12 h + '\H' => "'H'", 'H' => 'HH', // 24-hour format of an hour with leading zeros, 00 to 23 h + '\i' => "'i'", 'i' => 'mm', // Minutes with leading zeros 00 to 59 + '\s' => "'s'", 's' => 'ss', // Seconds, with leading zeros 00 through 59 + '\u' => "'u'", 'u' => '', // Microseconds. Example: 654321 // Timezone + '\e' => "'e'", 'e' => 'VV', // Timezone identifier. Examples: UTC, GMT, Atlantic/Azores + '\I' => "'I'", 'I' => '', // Whether or not the date is in daylight saving time, 1 if Daylight Saving Time, 0 otherwise. + '\O' => "'O'", 'O' => 'xx', // Difference to Greenwich time (GMT) in hours, Example: +0200 + '\P' => "'P'", 'P' => 'xxx', // Difference to Greenwich time (GMT) with colon between hours and minutes, Example: +02:00 + '\T' => "'T'", 'T' => 'zzz', // Timezone abbreviation, Examples: EST, MDT ... + '\Z' => "'Z'", 'Z' => '', // Timezone offset in seconds. The offset for timezones west of UTC is always negative, and for those east of UTC is always positive. -43200 through 50400 // Full Date/Time - 'c' => 'yyyy-MM-dd\'T\'HH:mm:ssxxx', // ISO 8601 date, e.g. 2004-02-12T15:19:21+00:00 + '\c' => "'c'", + 'c' => "yyyy-MM-dd'T'HH:mm:ssxxx", // ISO 8601 date, e.g. 2004-02-12T15:19:21+00:00 + '\r' => "'r'", 'r' => 'eee, dd MMM yyyy HH:mm:ss xx', // RFC 2822 formatted date, Example: Thu, 21 Dec 2000 16:01:07 +0200 + '\U' => "'U'", 'U' => '', // Seconds since the Unix Epoch (January 1 1970 00:00:00 GMT) + '\\\\' => '\\', + ]); + + // remove `''` - the're result of consecutive escaped chars (`\A\B` will be `'A''B'`, but should be `'AB'`) + // real `'` are encoded as `''''` + return strtr($result, [ + "''''" => "''", + "''" => '', ]); } diff --git a/tests/framework/helpers/FormatConverterTest.php b/tests/framework/helpers/FormatConverterTest.php index fe193ecc2f..e928b37286 100644 --- a/tests/framework/helpers/FormatConverterTest.php +++ b/tests/framework/helpers/FormatConverterTest.php @@ -73,12 +73,35 @@ class FormatConverterTest extends TestCase public function testIntlUtf8Ru() { - $this->assertEquals('d M Y \г.', FormatConverter::convertDateIcuToPhp('dd MMM y \'г\'.', 'date', 'ru-RU')); - $this->assertEquals('dd M yy \'г\'.', FormatConverter::convertDateIcuToJui('dd MMM y \'г\'.', 'date', 'ru-RU')); + $this->assertEquals('d M Y \г.', FormatConverter::convertDateIcuToPhp("dd MMM y 'г'.", 'date', 'ru-RU')); + $this->assertEquals("dd M yy 'г'.", FormatConverter::convertDateIcuToJui("dd MMM y 'г'.", 'date', 'ru-RU')); $formatter = new Formatter(['locale' => 'ru-RU']); // There is a dot after month name in updated ICU data and no dot in old data. Both are acceptable. // See https://github.com/yiisoft/yii2/issues/9906 - $this->assertRegExp('/24 авг\.? 2014 г\./', $formatter->asDate('2014-8-24', 'dd MMM y \'г\'.')); + $this->assertRegExp('/24 авг\.? 2014 г\./', $formatter->asDate('2014-8-24', "dd MMM y 'г'.")); + } + + public function testPhpToICU() + { + $expected = "yyyy-MM-dd'T'HH:mm:ssxxx"; + $actual = FormatConverter::convertDatePhpToIcu('Y-m-d\TH:i:sP'); + $this->assertEquals($expected, $actual); + + $expected = "yyyy-MM-dd'Yii'HH:mm:ssxxx"; + $actual = FormatConverter::convertDatePhpToIcu('Y-m-d\Y\i\iH:i:sP'); + $this->assertEquals($expected, $actual); + + $expected = "yyyy-MM-dd'Yii'HH:mm:ssxxx''''"; + $actual = FormatConverter::convertDatePhpToIcu("Y-m-d\Y\i\iH:i:sP''"); + $this->assertEquals($expected, $actual); + + $expected = "yyyy-MM-dd'Yii'\HH:mm:ssxxx''''"; + $actual = FormatConverter::convertDatePhpToIcu("Y-m-d\Y\i\i\\\\H:i:sP''"); + $this->assertEquals($expected, $actual); + + $expected = "'dDjlNSwZWFmMntLoYyaBghHisueIOPTZcru'"; + $actual = FormatConverter::convertDatePhpToIcu('\d\D\j\l\N\S\w\Z\W\F\m\M\n\t\L\o\Y\y\a\B\g\h\H\i\s\u\e\I\O\P\T\Z\c\r\u'); + $this->assertEquals($expected, $actual); } } diff --git a/tests/framework/i18n/FormatterDateTest.php b/tests/framework/i18n/FormatterDateTest.php index bb6f53c817..2213311b31 100644 --- a/tests/framework/i18n/FormatterDateTest.php +++ b/tests/framework/i18n/FormatterDateTest.php @@ -204,6 +204,10 @@ class FormatterDateTest extends TestCase $this->assertRegExp('~Jan 1, 1970,? 12:00:00 AM~', $this->formatter->asDatetime(false)); // null display $this->assertSame($this->formatter->nullDisplay, $this->formatter->asDatetime(null)); + + // DATE_ATOM + $value = time(); + $this->assertEquals(date(DATE_ATOM, $value), $this->formatter->asDatetime($value, 'php:' . DATE_ATOM)); } public function testIntlAsTimestamp()