From 4407cffeffc3d5c6d4db3be3d7c0181f549f0fba Mon Sep 17 00:00:00 2001 From: Ingmar Runge Date: Tue, 15 Dec 2015 16:22:15 +0100 Subject: [PATCH] XLSX Date Support / Test + Fix for years beyond 2037 This also fixes years < 1902 on 32-bit PHP systems. --- .../Reader/XLSX/Helper/CellValueFormatter.php | 22 +++-- .../XLSX/Helper/CellValueFormatterTest.php | 80 +++++++++++++++++++ 2 files changed, 95 insertions(+), 7 deletions(-) create mode 100644 tests/Spout/Reader/XLSX/Helper/CellValueFormatterTest.php diff --git a/src/Spout/Reader/XLSX/Helper/CellValueFormatter.php b/src/Spout/Reader/XLSX/Helper/CellValueFormatter.php index 3c417ca..269168d 100644 --- a/src/Spout/Reader/XLSX/Helper/CellValueFormatter.php +++ b/src/Spout/Reader/XLSX/Helper/CellValueFormatter.php @@ -28,7 +28,6 @@ class CellValueFormatter const XML_ATTRIBUTE_STYLE_ID = 's'; /** Constants used for date formatting */ - const NUM_DAYS_FROM_JAN_1_1900_TO_JAN_1_1970 = 25569; // actually 25567 but accommodating for an Excel bug with some bisextile years const NUM_SECONDS_IN_ONE_DAY = 86400; /** @@ -183,17 +182,26 @@ class CellValueFormatter */ protected function formatExcelTimestampValue($nodeValue) { - $numDaysSince1970Jan1 = $nodeValue - self::NUM_DAYS_FROM_JAN_1_1900_TO_JAN_1_1970; - // Fix for the erroneous leap year in Excel - if ($nodeValue < self::ERRONEOUS_EXCEL_LEAP_YEAR_DAY) { - $numDaysSince1970Jan1++; + if (ceil($nodeValue) > self::ERRONEOUS_EXCEL_LEAP_YEAR_DAY) { + --$nodeValue; } - $unixTimestamp = round($numDaysSince1970Jan1 * self::NUM_SECONDS_IN_ONE_DAY); + // The value 1.0 represents 1900-01-01. Numbers below 1.0 are not valid Excel dates. + if ($nodeValue < 1.0) { + return null; + } + + // Do not use any unix timestamps for calculation to prevent + // issues with numbers exceeding 2^31. + $secondsRemainder = fmod($nodeValue, 1) * self::NUM_SECONDS_IN_ONE_DAY; + $secondsRemainder = round($secondsRemainder, 0); try { - $cellValue = (new \DateTime())->setTimestamp($unixTimestamp); + $cellValue = \DateTime::createFromFormat('|Y-m-d', '1899-12-31'); + $cellValue->modify('+' . intval($nodeValue) . 'days'); + $cellValue->modify('+' . $secondsRemainder . 'seconds'); + return $cellValue; } catch (\Exception $e) { return null; diff --git a/tests/Spout/Reader/XLSX/Helper/CellValueFormatterTest.php b/tests/Spout/Reader/XLSX/Helper/CellValueFormatterTest.php new file mode 100644 index 0000000..b7eca71 --- /dev/null +++ b/tests/Spout/Reader/XLSX/Helper/CellValueFormatterTest.php @@ -0,0 +1,80 @@ +getMockBuilder('DOMNodeList')->disableOriginalConstructor()->getMock(); + + $nodeListMock + ->expects($this->atLeastOnce()) + ->method('item') + ->with(0) + ->will($this->returnValue((object)[ 'nodeValue' => $nodeValue ])); + + $nodeMock = $this->getMockBuilder('DOMElement')->disableOriginalConstructor()->getMock(); + + $nodeMock + ->expects($this->atLeastOnce()) + ->method('getAttribute') + ->will($this->returnValueMap([ + [ CellValueFormatter::XML_ATTRIBUTE_TYPE, $cellType ], + [ CellValueFormatter::XML_ATTRIBUTE_STYLE_ID, 123 ], + ])); + + $nodeMock + ->expects($this->atLeastOnce()) + ->method('getElementsByTagName') + ->with(CellValueFormatter::XML_NODE_VALUE) + ->will($this->returnValue($nodeListMock)); + + $styleHelperMock = $this->getMockBuilder(__NAMESPACE__ . '\StyleHelper')->disableOriginalConstructor()->getMock(); + + $styleHelperMock + ->expects($this->once()) + ->method('shouldFormatNumericValueAsDate') + ->with(123) + ->will($this->returnValue(true)); + + $instance = new CellValueFormatter(null, $styleHelperMock); + + $result = $instance->extractAndFormatNodeValue($nodeMock); + + if ($expectedDateAsString === null) { + $this->assertNull($result); + } else { + $this->assertInstanceOf('DateTime', $result); + $this->assertSame($expectedDateAsString, $result->format('Y-m-d H:i:s')); + } + } + +}