From 18b57af5acab42f633ee90728c234b2d1765f69b Mon Sep 17 00:00:00 2001 From: Kartik Visweswaran Date: Fri, 12 Sep 2014 20:34:42 +0530 Subject: [PATCH] Better date parsing and formatting including 32 bit support Enhances `normalizeDateTimeValue` to return a DateTime object instead of a converted double value, that fails. The DateTime object input is supported by 32 bit, 64 bit, as well as the `IntlDateFormatter` to format all years. (including fixing of the Y2K38 bug). Fixes issue in #4989. close #5000 --- framework/i18n/Formatter.php | 65 ++++++----- tests/unit/framework/i18n/FormatterTest.php | 116 ++++++++++++++++---- 2 files changed, 132 insertions(+), 49 deletions(-) diff --git a/framework/i18n/Formatter.php b/framework/i18n/Formatter.php index 9519295593..eec947ff7a 100644 --- a/framework/i18n/Formatter.php +++ b/framework/i18n/Formatter.php @@ -494,8 +494,8 @@ class Formatter extends Component */ private function formatDateTimeValue($value, $format, $type) { - $value = $this->normalizeDatetimeValue($value); - if ($value === null) { + $timestamp = $this->normalizeDatetimeValue($value); + if ($timestamp === null) { return $this->nullDisplay; } @@ -517,42 +517,49 @@ class Formatter extends Component if ($formatter === null) { throw new InvalidConfigException(intl_get_error_message()); } - return $formatter->format($value); + return $formatter->format($timestamp); } else { if (strncmp($format, 'php:', 4) === 0) { $format = substr($format, 4); } else { $format = FormatConverter::convertDateIcuToPhp($format, $type, $this->locale); } - $date = new DateTime(null, new \DateTimeZone($this->timeZone)); - $date->setTimestamp($value); - return $date->format($format); + if ($this->timeZone != null) { + $timestamp->setTimezone(new \DateTimeZone($this->timeZone)); + } + return $timestamp->format($format); } } /** - * Normalizes the given datetime value as a UNIX timestamp 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. - * @return float the normalized datetime value (int64) + * @return DateTime the normalized datetime value + * @throws InvalidParamException if the input value can not be evaluated as a date value. */ protected function normalizeDatetimeValue($value) { - if ($value === null) { - return null; - } elseif (is_string($value)) { - if (is_numeric($value) || $value === '') { - $value = (double)$value; - } else { - $date = new DateTime($value); - $value = (double)$date->format('U'); - } + if ($value === null || $value instanceof DateTime) { + // skip any processing return $value; - - } elseif ($value instanceof DateTime || $value instanceof DateTimeInterface) { - return (double)$value->format('U'); - } else { - return (double)$value; + } + if (empty($value)) { + $value = 0; + } + try { + if (is_numeric($value)) { + // process as unix timestamp + if (($timestamp = DateTime::createFromFormat('U', $value)) === false) { + throw new InvalidParamException("Failed to parse '$value' as a UNIX timestamp."); + } + return $timestamp; + } + $timestamp = new DateTime($value); + return $timestamp; + } catch(\Exception $e) { + throw new InvalidParamException("'$value' is not a valid date time value: " . $e->getMessage() + . "\n" . print_r(DateTime::getLastErrors(), true), $e->getCode(), $e); } } @@ -573,7 +580,8 @@ class Formatter extends Component if ($value === null) { return $this->nullDisplay; } - return number_format($this->normalizeDatetimeValue($value), 0, '.', ''); + $timestamp = $this->normalizeDatetimeValue($value); + return number_format($timestamp->format('U'), 0, '.', ''); } /** @@ -617,13 +625,11 @@ class Formatter extends Component if ($referenceTime === null) { $dateNow = new DateTime('now', $timezone); } else { - $referenceTime = $this->normalizeDatetimeValue($referenceTime); - $dateNow = new DateTime(null, $timezone); - $dateNow->setTimestamp($referenceTime); + $dateNow = $this->normalizeDatetimeValue($referenceTime); + $dateNow->setTimezone($timezone); } - $dateThen = new DateTime(null, $timezone); - $dateThen->setTimestamp($timestamp); + $dateThen = $timestamp->setTimezone($timezone); $interval = $dateThen->diff($dateNow); } @@ -1020,6 +1026,9 @@ class Formatter extends Component */ protected function normalizeNumericValue($value) { + if (empty($value)) { + return 0; + } if (is_string($value) && is_numeric($value)) { $value = (float) $value; } diff --git a/tests/unit/framework/i18n/FormatterTest.php b/tests/unit/framework/i18n/FormatterTest.php index 415a9b16d8..0eed5af5e9 100644 --- a/tests/unit/framework/i18n/FormatterTest.php +++ b/tests/unit/framework/i18n/FormatterTest.php @@ -204,6 +204,7 @@ class FormatterTest extends TestCase $this->assertSame('Jan 1, 1970', $this->formatter->asDate('')); $this->assertSame('Jan 1, 1970', $this->formatter->asDate(0)); $this->assertSame('Jan 1, 1970', $this->formatter->asDate(false)); + // null display $this->assertSame($this->formatter->nullDisplay, $this->formatter->asDate(null)); } @@ -258,6 +259,11 @@ class FormatterTest extends TestCase $this->assertSame($this->formatter->nullDisplay, $this->formatter->asDatetime(null)); } + public function testIntlAsTimestamp() + { + $this->testAsTimestamp(); + } + public function testAsTimestamp() { $value = time(); @@ -275,6 +281,11 @@ class FormatterTest extends TestCase $this->assertSame($this->formatter->nullDisplay, $this->formatter->asTimestamp(null)); } + public function testIntlDateRangeLow() + { + $this->testDateRangeLow(); + } + /** * Test for dates before 1970 * https://github.com/yiisoft/yii2/issues/3126 @@ -282,18 +293,22 @@ class FormatterTest extends TestCase public function testDateRangeLow() { $this->assertSame('12-08-1922', $this->formatter->asDate('1922-08-12', 'dd-MM-yyyy')); + $this->assertSame('14-01-1732', $this->formatter->asDate('1732-01-14', 'dd-MM-yyyy')); } - /** + public function testIntlDateRangeHigh() + { + $this->testDateRangeHigh(); + } + + /** * Test for dates after 2038 * https://github.com/yiisoft/yii2/issues/3126 */ public function testDateRangeHigh() { - if (PHP_INT_SIZE < 8) { - $this->markTestSkipped('Dates > 2038 only work on PHP compiled with 64bit support.'); - } $this->assertSame('17-12-2048', $this->formatter->asDate('2048-12-17', 'dd-MM-yyyy')); + $this->assertSame('17-12-3048', $this->formatter->asDate('3048-12-17', 'dd-MM-yyyy')); } private function buildDateSubIntervals($referenceDate, $intervals) @@ -305,6 +320,11 @@ class FormatterTest extends TestCase return $date; } + public function testIntlAsRelativeTime() + { + $this->testAsRelativeTime(); + } + public function testAsRelativeTime() { $interval_1_second = new DateInterval("PT1S"); @@ -431,6 +451,36 @@ class FormatterTest extends TestCase $this->assertSame($this->formatter->nullDisplay, $this->formatter->asRelativeTime(null, time())); } + public function dateInputs() + { + return [ + [false, '2014-13-01', 'yii\base\InvalidParamException'], + [false, 'asdfg', 'yii\base\InvalidParamException'], +// [(string)strtotime('now'), 'now'], // fails randomly + ]; + } + + /** + * @dataProvider dateInputs + */ + public function testIntlDateInput($expected, $value, $expectedException = null) + { + $this->testDateInput($expected, $value, $expectedException); + } + + /** + * @dataProvider dateInputs + */ + public function testDateInput($expected, $value, $expectedException = null) + { + if ($expectedException !== null) { + $this->setExpectedException($expectedException); + } + $this->assertSame($expected, $this->formatter->asDate($value, 'php:U')); + $this->assertSame($expected, $this->formatter->asTime($value, 'php:U')); + $this->assertSame($expected, $this->formatter->asDatetime($value, 'php:U')); + } + // number format @@ -452,6 +502,10 @@ class FormatterTest extends TestCase $this->assertSame("123,456", $this->formatter->asInteger(123456)); $this->assertSame("123,456", $this->formatter->asInteger(123456.789)); + // empty input + $this->assertSame("0", $this->formatter->asInteger(false)); + $this->assertSame("0", $this->formatter->asInteger("")); + // null display $this->assertSame($this->formatter->nullDisplay, $this->formatter->asInteger(null)); } @@ -472,22 +526,6 @@ class FormatterTest extends TestCase $this->formatter->asInteger('-123abc'); } - /** - * @expectedException \yii\base\InvalidParamException - */ - public function testAsIntegerException3() - { - $this->formatter->asInteger(''); - } - - /** - * @expectedException \yii\base\InvalidParamException - */ - public function testAsIntegerException4() - { - $this->formatter->asInteger(false); - } - public function testIntlAsDecimal() { $value = 123.12; @@ -523,6 +561,10 @@ class FormatterTest extends TestCase $value = '-123456.123'; $this->assertSame("-123,456.123", $this->formatter->asDecimal($value)); + // empty input + $this->assertSame("0", $this->formatter->asInteger(false)); + $this->assertSame("0", $this->formatter->asInteger("")); + // null display $this->assertSame($this->formatter->nullDisplay, $this->formatter->asDecimal(null)); } @@ -560,6 +602,10 @@ class FormatterTest extends TestCase $value = '-123456.123'; $this->assertSame("-123,456.123", $this->formatter->asDecimal($value, 3)); + // empty input + $this->assertSame("0", $this->formatter->asInteger(false)); + $this->assertSame("0", $this->formatter->asInteger("")); + // null display $this->assertSame($this->formatter->nullDisplay, $this->formatter->asDecimal(null)); } @@ -578,6 +624,10 @@ class FormatterTest extends TestCase $this->assertSame("-1%", $this->formatter->asPercent(-0.009343)); $this->assertSame("-1%", $this->formatter->asPercent('-0.009343')); + // empty input + $this->assertSame("0", $this->formatter->asInteger(false)); + $this->assertSame("0", $this->formatter->asInteger("")); + // null display $this->assertSame($this->formatter->nullDisplay, $this->formatter->asPercent(null)); } @@ -607,6 +657,10 @@ class FormatterTest extends TestCase $this->formatter->currencyCode = 'EUR'; $this->assertSame('123,00 €', $this->formatter->asCurrency('123')); + // empty input + $this->assertSame("0", $this->formatter->asInteger(false)); + $this->assertSame("0", $this->formatter->asInteger("")); + // null display $this->assertSame($this->formatter->nullDisplay, $this->formatter->asCurrency(null)); } @@ -625,6 +679,10 @@ class FormatterTest extends TestCase $this->assertSame('EUR -123.45', $this->formatter->asCurrency('-123.45')); $this->assertSame('EUR -123.45', $this->formatter->asCurrency(-123.45)); + // empty input + $this->assertSame("0", $this->formatter->asInteger(false)); + $this->assertSame("0", $this->formatter->asInteger("")); + // null display $this->assertSame($this->formatter->nullDisplay, $this->formatter->asCurrency(null)); } @@ -638,6 +696,10 @@ class FormatterTest extends TestCase $value = '-123456.123'; $this->assertSame("-1.23456123E5", $this->formatter->asScientific($value)); + // empty input + $this->assertSame("0", $this->formatter->asInteger(false)); + $this->assertSame("0", $this->formatter->asInteger("")); + // null display $this->assertSame($this->formatter->nullDisplay, $this->formatter->asScientific(null)); } @@ -651,6 +713,10 @@ class FormatterTest extends TestCase $value = '-123456.123'; $this->assertSame("-1.234561E+5", $this->formatter->asScientific($value)); + // empty input + $this->assertSame("0", $this->formatter->asInteger(false)); + $this->assertSame("0", $this->formatter->asInteger("")); + // null display $this->assertSame($this->formatter->nullDisplay, $this->formatter->asScientific(null)); } @@ -808,12 +874,20 @@ class FormatterTest extends TestCase $this->assertSame($this->formatter->nullDisplay, $this->formatter->asSize(null)); } + public function testIntlAsSizeConfiguration() + { + $this->assertSame("1023 bytes", $this->formatter->asSize(1023)); + $this->formatter->thousandSeparator = '.'; + $this->assertSame("1023 bytes", $this->formatter->asSize(1023)); + } + /** * https://github.com/yiisoft/yii2/issues/4960 */ public function testAsSizeConfiguration() { -// $this->formatter->thousandSeparator = ''; + $this->assertSame("1023 bytes", $this->formatter->asSize(1023)); + $this->formatter->thousandSeparator = '.'; $this->assertSame("1023 bytes", $this->formatter->asSize(1023)); } }