From 5dd7e883c6bd409063882de2cde1a07a556f25be Mon Sep 17 00:00:00 2001 From: oleibman Date: Sun, 24 May 2020 11:02:39 -0700 Subject: [PATCH] Fix Issue 1441 (isDateTime and Formulas) (#1480) * Fix Issue 1441 (isDateTime and Formulas) When you have a date-field which is a formula, isDateTime returns false. https://github.com/PHPOffice/PhpSpreadsheet/issues/1441 Report makes sense; fixed as suggested. Also fixed a few minor related issues, and added tests so that Shared/Date and Shared/TimeZone are now completely covered. Date/setDefaultTimeZone and TimeZone/setTimeZone were not consistent about what to do in event of failure - return false or throw. They will now both return false, which is what Date's function said it would do in its doc block anyhow. Date/validateTimeZone will continue to throw; it was protected, but was never called outside Date, so I changed it to private. TimeZone/getTimeZoneAdjustment checked for 'UST' when it probably meant 'UTC', and, as it turns out, the check is not even needed. The most serious problem was that TimeZone/validateTimeZone does not check the backwards-compatible time zones. The timezone project aggressively, and very controversially, "demotes" timezones; such timezones eventually wind up in the PHP backwards-compatible list. We want to make sure to check that list so that our applications do not break when this happens. --- src/PhpSpreadsheet/Shared/Date.php | 25 ++++---- src/PhpSpreadsheet/Shared/TimeZone.php | 6 +- tests/PhpSpreadsheetTests/Shared/DateTest.php | 50 ++++++++++++++++ .../Shared/TimeZoneTest.php | 58 ++++++++++++++++++- tests/data/Shared/Date/FormatCodes.php | 4 ++ 5 files changed, 125 insertions(+), 18 deletions(-) diff --git a/src/PhpSpreadsheet/Shared/Date.php b/src/PhpSpreadsheet/Shared/Date.php index fd49c1ec..9dc99292 100644 --- a/src/PhpSpreadsheet/Shared/Date.php +++ b/src/PhpSpreadsheet/Shared/Date.php @@ -4,10 +4,10 @@ namespace PhpOffice\PhpSpreadsheet\Shared; use DateTimeInterface; use DateTimeZone; -use Exception; use PhpOffice\PhpSpreadsheet\Calculation\DateTime; use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Cell\Cell; +use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException; use PhpOffice\PhpSpreadsheet\Style\NumberFormat; class Date @@ -97,17 +97,18 @@ class Date * @param DateTimeZone|string $timeZone The timezone to set for all Excel datetimestamp to PHP DateTime Object conversions * * @return bool Success or failure - * @return bool Success or failure */ public static function setDefaultTimezone($timeZone) { - if ($timeZone = self::validateTimeZone($timeZone)) { + try { + $timeZone = self::validateTimeZone($timeZone); self::$defaultTimeZone = $timeZone; - - return true; + $retval = true; + } catch (PhpSpreadsheetException $e) { + $retval = false; } - return false; + return $retval; } /** @@ -130,17 +131,17 @@ class Date * @param DateTimeZone|string $timeZone The timezone to validate, either as a timezone string or object * * @return DateTimeZone The timezone as a timezone object - * @return DateTimeZone The timezone as a timezone object */ - protected static function validateTimeZone($timeZone) + private static function validateTimeZone($timeZone) { - if (is_object($timeZone) && $timeZone instanceof DateTimeZone) { + if ($timeZone instanceof DateTimeZone) { return $timeZone; - } elseif (is_string($timeZone)) { + } + if (in_array($timeZone, DateTimeZone::listIdentifiers(DateTimeZone::ALL_WITH_BC))) { return new DateTimeZone($timeZone); } - throw new Exception('Invalid timezone'); + throw new PhpSpreadsheetException('Invalid timezone'); } /** @@ -316,7 +317,7 @@ class Date */ public static function isDateTime(Cell $pCell) { - return is_numeric($pCell->getValue()) && + return is_numeric($pCell->getCalculatedValue()) && self::isDateTimeFormat( $pCell->getWorksheet()->getStyle( $pCell->getCoordinate() diff --git a/src/PhpSpreadsheet/Shared/TimeZone.php b/src/PhpSpreadsheet/Shared/TimeZone.php index a87987df..43fd3653 100644 --- a/src/PhpSpreadsheet/Shared/TimeZone.php +++ b/src/PhpSpreadsheet/Shared/TimeZone.php @@ -23,7 +23,7 @@ class TimeZone */ private static function validateTimeZone($timezone) { - return in_array($timezone, DateTimeZone::listIdentifiers()); + return in_array($timezone, DateTimeZone::listIdentifiers(DateTimeZone::ALL_WITH_BC)); } /** @@ -73,10 +73,6 @@ class TimeZone $timezone = self::$timezone; } - if ($timezone == 'UST') { - return 0; - } - $objTimezone = new DateTimeZone($timezone); $transitions = $objTimezone->getTransitions($timestamp, $timestamp); diff --git a/tests/PhpSpreadsheetTests/Shared/DateTest.php b/tests/PhpSpreadsheetTests/Shared/DateTest.php index 23b31076..7254635e 100644 --- a/tests/PhpSpreadsheetTests/Shared/DateTest.php +++ b/tests/PhpSpreadsheetTests/Shared/DateTest.php @@ -3,10 +3,23 @@ namespace PhpOffice\PhpSpreadsheetTests\Shared; use PhpOffice\PhpSpreadsheet\Shared\Date; +use PhpOffice\PhpSpreadsheet\Style\NumberFormat; use PHPUnit\Framework\TestCase; class DateTest extends TestCase { + private $dttimezone; + + protected function setUp(): void + { + $this->dttimezone = Date::getDefaultTimeZone(); + } + + protected function tearDown(): void + { + Date::setDefaultTimeZone($this->dttimezone); + } + public function testSetExcelCalendar(): void { $calendarValues = [ @@ -168,4 +181,41 @@ class DateTest extends TestCase { return require 'tests/data/Shared/Date/ExcelToTimestamp1900Timezone.php'; } + + public function testVarious(): void + { + Date::setDefaultTimeZone('UTC'); + self::assertFalse(Date::stringToExcel('2019-02-29')); + self::assertTrue((bool) Date::stringToExcel('2019-02-28')); + self::assertTrue((bool) Date::stringToExcel('2019-02-28 11:18')); + self::assertFalse(Date::stringToExcel('2019-02-28 11:71')); + $date = Date::PHPToExcel('2020-01-01'); + self::assertEquals(43831.0, $date); + $spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setCellValue('B1', 'x'); + $val = $sheet->getCell('B1')->getValue(); + self::assertFalse(Date::timestampToExcel($val)); + $cell = $sheet->getCell('A1'); + self::assertNotNull($cell); + $cell->setValue($date); + $sheet->getStyle('A1') + ->getNumberFormat() + ->setFormatCode(NumberFormat::FORMAT_DATE_DATETIME); + self::assertTrue(null !== $cell && Date::isDateTime($cell)); + $cella2 = $sheet->getCell('A2'); + self::assertNotNull($cella2); + $cella2->setValue('=A1+2'); + $sheet->getStyle('A2') + ->getNumberFormat() + ->setFormatCode(NumberFormat::FORMAT_DATE_DATETIME); + self::assertTrue(null !== $cella2 && Date::isDateTime($cella2)); + $cella3 = $sheet->getCell('A3'); + self::assertNotNull($cella3); + $cella3->setValue('=A1+4'); + $sheet->getStyle('A3') + ->getNumberFormat() + ->setFormatCode('0.00E+00'); + self::assertFalse(null !== $cella3 && Date::isDateTime($cella3)); + } } diff --git a/tests/PhpSpreadsheetTests/Shared/TimeZoneTest.php b/tests/PhpSpreadsheetTests/Shared/TimeZoneTest.php index f6e2f5d5..ff38badf 100644 --- a/tests/PhpSpreadsheetTests/Shared/TimeZoneTest.php +++ b/tests/PhpSpreadsheetTests/Shared/TimeZoneTest.php @@ -2,11 +2,29 @@ namespace PhpOffice\PhpSpreadsheetTests\Shared; +use DateTime; +use PhpOffice\PhpSpreadsheet\Shared\Date; use PhpOffice\PhpSpreadsheet\Shared\TimeZone; use PHPUnit\Framework\TestCase; class TimeZoneTest extends TestCase { + private $tztimezone; + + private $dttimezone; + + protected function setUp(): void + { + $this->tztimezone = TimeZone::getTimeZone(); + $this->dttimezone = Date::getDefaultTimeZone(); + } + + protected function tearDown(): void + { + TimeZone::setTimeZone($this->tztimezone); + Date::setDefaultTimeZone($this->dttimezone); + } + public function testSetTimezone(): void { $timezoneValues = [ @@ -20,13 +38,51 @@ class TimeZoneTest extends TestCase foreach ($timezoneValues as $timezoneValue) { $result = TimeZone::setTimezone($timezoneValue); self::assertTrue($result); + $result = Date::setDefaultTimezone($timezoneValue); + self::assertTrue($result); } } + public function testSetTimezoneBackwardCompatible(): void + { + $bcTimezone = 'Etc/GMT+10'; + $result = TimeZone::setTimezone($bcTimezone); + self::assertTrue($result); + $result = Date::setDefaultTimezone($bcTimezone); + self::assertTrue($result); + } + public function testSetTimezoneWithInvalidValue(): void { - $unsupportedTimezone = 'Etc/GMT+10'; + $unsupportedTimezone = 'XEtc/GMT+10'; $result = TimeZone::setTimezone($unsupportedTimezone); self::assertFalse($result); + $result = Date::setDefaultTimezone($unsupportedTimezone); + self::assertFalse($result); + } + + public function testTimeZoneAdjustmentsInvalidTz(): void + { + $this->expectException(\PhpOffice\PhpSpreadsheet\Exception::class); + $dtobj = DateTime::createFromFormat('Y-m-d H:i:s', '2008-09-22 00:00:00'); + $tstmp = $dtobj->getTimestamp(); + $unsupportedTimeZone = 'XEtc/GMT+10'; + TimeZone::getTimeZoneAdjustment($unsupportedTimeZone, $tstmp); + } + + public function testTimeZoneAdjustments(): void + { + $dtobj = DateTime::createFromFormat('Y-m-d H:i:s', '2008-01-01 00:00:00'); + $tstmp = $dtobj->getTimestamp(); + $supportedTimeZone = 'UTC'; + $adj = TimeZone::getTimeZoneAdjustment($supportedTimeZone, $tstmp); + self::assertEquals(0, $adj); + $supportedTimeZone = 'America/Toronto'; + $adj = TimeZone::getTimeZoneAdjustment($supportedTimeZone, $tstmp); + self::assertEquals(-18000, $adj); + $supportedTimeZone = 'America/Chicago'; + TimeZone::setTimeZone($supportedTimeZone); + $adj = TimeZone::getTimeZoneAdjustment(null, $tstmp); + self::assertEquals(-21600, $adj); } } diff --git a/tests/data/Shared/Date/FormatCodes.php b/tests/data/Shared/Date/FormatCodes.php index 7245000a..64810de3 100644 --- a/tests/data/Shared/Date/FormatCodes.php +++ b/tests/data/Shared/Date/FormatCodes.php @@ -146,4 +146,8 @@ return [ false, '#,##0.00 "dollars"', ], + [ + true, + '"date " y-m-d', + ], ];