Commit 18b57af5 by Kartik Visweswaran Committed by Carsten Brandt

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
parent a8ed099b
...@@ -494,8 +494,8 @@ class Formatter extends Component ...@@ -494,8 +494,8 @@ class Formatter extends Component
*/ */
private function formatDateTimeValue($value, $format, $type) private function formatDateTimeValue($value, $format, $type)
{ {
$value = $this->normalizeDatetimeValue($value); $timestamp = $this->normalizeDatetimeValue($value);
if ($value === null) { if ($timestamp === null) {
return $this->nullDisplay; return $this->nullDisplay;
} }
...@@ -517,42 +517,49 @@ class Formatter extends Component ...@@ -517,42 +517,49 @@ class Formatter extends Component
if ($formatter === null) { if ($formatter === null) {
throw new InvalidConfigException(intl_get_error_message()); throw new InvalidConfigException(intl_get_error_message());
} }
return $formatter->format($value); return $formatter->format($timestamp);
} else { } else {
if (strncmp($format, 'php:', 4) === 0) { if (strncmp($format, 'php:', 4) === 0) {
$format = substr($format, 4); $format = substr($format, 4);
} else { } else {
$format = FormatConverter::convertDateIcuToPhp($format, $type, $this->locale); $format = FormatConverter::convertDateIcuToPhp($format, $type, $this->locale);
} }
$date = new DateTime(null, new \DateTimeZone($this->timeZone)); if ($this->timeZone != null) {
$date->setTimestamp($value); $timestamp->setTimezone(new \DateTimeZone($this->timeZone));
return $date->format($format); }
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. * @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) protected function normalizeDatetimeValue($value)
{ {
if ($value === null) { if ($value === null || $value instanceof DateTime) {
return null; // skip any processing
} elseif (is_string($value)) {
if (is_numeric($value) || $value === '') {
$value = (double)$value;
} else {
$date = new DateTime($value);
$value = (double)$date->format('U');
}
return $value; return $value;
}
} elseif ($value instanceof DateTime || $value instanceof DateTimeInterface) { if (empty($value)) {
return (double)$value->format('U'); $value = 0;
} else { }
return (double)$value; 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 ...@@ -573,7 +580,8 @@ class Formatter extends Component
if ($value === null) { if ($value === null) {
return $this->nullDisplay; 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 ...@@ -617,13 +625,11 @@ class Formatter extends Component
if ($referenceTime === null) { if ($referenceTime === null) {
$dateNow = new DateTime('now', $timezone); $dateNow = new DateTime('now', $timezone);
} else { } else {
$referenceTime = $this->normalizeDatetimeValue($referenceTime); $dateNow = $this->normalizeDatetimeValue($referenceTime);
$dateNow = new DateTime(null, $timezone); $dateNow->setTimezone($timezone);
$dateNow->setTimestamp($referenceTime);
} }
$dateThen = new DateTime(null, $timezone); $dateThen = $timestamp->setTimezone($timezone);
$dateThen->setTimestamp($timestamp);
$interval = $dateThen->diff($dateNow); $interval = $dateThen->diff($dateNow);
} }
...@@ -1020,6 +1026,9 @@ class Formatter extends Component ...@@ -1020,6 +1026,9 @@ class Formatter extends Component
*/ */
protected function normalizeNumericValue($value) protected function normalizeNumericValue($value)
{ {
if (empty($value)) {
return 0;
}
if (is_string($value) && is_numeric($value)) { if (is_string($value) && is_numeric($value)) {
$value = (float) $value; $value = (float) $value;
} }
......
...@@ -204,6 +204,7 @@ class FormatterTest extends TestCase ...@@ -204,6 +204,7 @@ class FormatterTest extends TestCase
$this->assertSame('Jan 1, 1970', $this->formatter->asDate('')); $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(0));
$this->assertSame('Jan 1, 1970', $this->formatter->asDate(false)); $this->assertSame('Jan 1, 1970', $this->formatter->asDate(false));
// null display // null display
$this->assertSame($this->formatter->nullDisplay, $this->formatter->asDate(null)); $this->assertSame($this->formatter->nullDisplay, $this->formatter->asDate(null));
} }
...@@ -258,6 +259,11 @@ class FormatterTest extends TestCase ...@@ -258,6 +259,11 @@ class FormatterTest extends TestCase
$this->assertSame($this->formatter->nullDisplay, $this->formatter->asDatetime(null)); $this->assertSame($this->formatter->nullDisplay, $this->formatter->asDatetime(null));
} }
public function testIntlAsTimestamp()
{
$this->testAsTimestamp();
}
public function testAsTimestamp() public function testAsTimestamp()
{ {
$value = time(); $value = time();
...@@ -275,6 +281,11 @@ class FormatterTest extends TestCase ...@@ -275,6 +281,11 @@ class FormatterTest extends TestCase
$this->assertSame($this->formatter->nullDisplay, $this->formatter->asTimestamp(null)); $this->assertSame($this->formatter->nullDisplay, $this->formatter->asTimestamp(null));
} }
public function testIntlDateRangeLow()
{
$this->testDateRangeLow();
}
/** /**
* Test for dates before 1970 * Test for dates before 1970
* https://github.com/yiisoft/yii2/issues/3126 * https://github.com/yiisoft/yii2/issues/3126
...@@ -282,6 +293,12 @@ class FormatterTest extends TestCase ...@@ -282,6 +293,12 @@ class FormatterTest extends TestCase
public function testDateRangeLow() public function testDateRangeLow()
{ {
$this->assertSame('12-08-1922', $this->formatter->asDate('1922-08-12', 'dd-MM-yyyy')); $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();
} }
/** /**
...@@ -290,10 +307,8 @@ class FormatterTest extends TestCase ...@@ -290,10 +307,8 @@ class FormatterTest extends TestCase
*/ */
public function testDateRangeHigh() 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-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) private function buildDateSubIntervals($referenceDate, $intervals)
...@@ -305,6 +320,11 @@ class FormatterTest extends TestCase ...@@ -305,6 +320,11 @@ class FormatterTest extends TestCase
return $date; return $date;
} }
public function testIntlAsRelativeTime()
{
$this->testAsRelativeTime();
}
public function testAsRelativeTime() public function testAsRelativeTime()
{ {
$interval_1_second = new DateInterval("PT1S"); $interval_1_second = new DateInterval("PT1S");
...@@ -431,6 +451,36 @@ class FormatterTest extends TestCase ...@@ -431,6 +451,36 @@ class FormatterTest extends TestCase
$this->assertSame($this->formatter->nullDisplay, $this->formatter->asRelativeTime(null, time())); $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 // number format
...@@ -452,6 +502,10 @@ class FormatterTest extends TestCase ...@@ -452,6 +502,10 @@ class FormatterTest extends TestCase
$this->assertSame("123,456", $this->formatter->asInteger(123456)); $this->assertSame("123,456", $this->formatter->asInteger(123456));
$this->assertSame("123,456", $this->formatter->asInteger(123456.789)); $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 // null display
$this->assertSame($this->formatter->nullDisplay, $this->formatter->asInteger(null)); $this->assertSame($this->formatter->nullDisplay, $this->formatter->asInteger(null));
} }
...@@ -472,22 +526,6 @@ class FormatterTest extends TestCase ...@@ -472,22 +526,6 @@ class FormatterTest extends TestCase
$this->formatter->asInteger('-123abc'); $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() public function testIntlAsDecimal()
{ {
$value = 123.12; $value = 123.12;
...@@ -523,6 +561,10 @@ class FormatterTest extends TestCase ...@@ -523,6 +561,10 @@ class FormatterTest extends TestCase
$value = '-123456.123'; $value = '-123456.123';
$this->assertSame("-123,456.123", $this->formatter->asDecimal($value)); $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 // null display
$this->assertSame($this->formatter->nullDisplay, $this->formatter->asDecimal(null)); $this->assertSame($this->formatter->nullDisplay, $this->formatter->asDecimal(null));
} }
...@@ -560,6 +602,10 @@ class FormatterTest extends TestCase ...@@ -560,6 +602,10 @@ class FormatterTest extends TestCase
$value = '-123456.123'; $value = '-123456.123';
$this->assertSame("-123,456.123", $this->formatter->asDecimal($value, 3)); $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 // null display
$this->assertSame($this->formatter->nullDisplay, $this->formatter->asDecimal(null)); $this->assertSame($this->formatter->nullDisplay, $this->formatter->asDecimal(null));
} }
...@@ -578,6 +624,10 @@ class FormatterTest extends TestCase ...@@ -578,6 +624,10 @@ class FormatterTest extends TestCase
$this->assertSame("-1%", $this->formatter->asPercent(-0.009343)); $this->assertSame("-1%", $this->formatter->asPercent(-0.009343));
$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 // null display
$this->assertSame($this->formatter->nullDisplay, $this->formatter->asPercent(null)); $this->assertSame($this->formatter->nullDisplay, $this->formatter->asPercent(null));
} }
...@@ -607,6 +657,10 @@ class FormatterTest extends TestCase ...@@ -607,6 +657,10 @@ class FormatterTest extends TestCase
$this->formatter->currencyCode = 'EUR'; $this->formatter->currencyCode = 'EUR';
$this->assertSame('123,00 €', $this->formatter->asCurrency('123')); $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 // null display
$this->assertSame($this->formatter->nullDisplay, $this->formatter->asCurrency(null)); $this->assertSame($this->formatter->nullDisplay, $this->formatter->asCurrency(null));
} }
...@@ -625,6 +679,10 @@ class FormatterTest extends TestCase ...@@ -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'));
$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 // null display
$this->assertSame($this->formatter->nullDisplay, $this->formatter->asCurrency(null)); $this->assertSame($this->formatter->nullDisplay, $this->formatter->asCurrency(null));
} }
...@@ -638,6 +696,10 @@ class FormatterTest extends TestCase ...@@ -638,6 +696,10 @@ class FormatterTest extends TestCase
$value = '-123456.123'; $value = '-123456.123';
$this->assertSame("-1.23456123E5", $this->formatter->asScientific($value)); $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 // null display
$this->assertSame($this->formatter->nullDisplay, $this->formatter->asScientific(null)); $this->assertSame($this->formatter->nullDisplay, $this->formatter->asScientific(null));
} }
...@@ -651,6 +713,10 @@ class FormatterTest extends TestCase ...@@ -651,6 +713,10 @@ class FormatterTest extends TestCase
$value = '-123456.123'; $value = '-123456.123';
$this->assertSame("-1.234561E+5", $this->formatter->asScientific($value)); $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 // null display
$this->assertSame($this->formatter->nullDisplay, $this->formatter->asScientific(null)); $this->assertSame($this->formatter->nullDisplay, $this->formatter->asScientific(null));
} }
...@@ -808,12 +874,20 @@ class FormatterTest extends TestCase ...@@ -808,12 +874,20 @@ class FormatterTest extends TestCase
$this->assertSame($this->formatter->nullDisplay, $this->formatter->asSize(null)); $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 * https://github.com/yiisoft/yii2/issues/4960
*/ */
public function testAsSizeConfiguration() 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)); $this->assertSame("1023 bytes", $this->formatter->asSize(1023));
} }
} }
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