Merge pull request #155 from KiNgMaR/Fix-Dates-Beyond-2037
XLSX Date Support / Fix for years beyond 2037
This commit is contained in:
commit
9a85d84a2e
@ -28,7 +28,6 @@ class CellValueFormatter
|
|||||||
const XML_ATTRIBUTE_STYLE_ID = 's';
|
const XML_ATTRIBUTE_STYLE_ID = 's';
|
||||||
|
|
||||||
/** Constants used for date formatting */
|
/** 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;
|
const NUM_SECONDS_IN_ONE_DAY = 86400;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -183,17 +182,26 @@ class CellValueFormatter
|
|||||||
*/
|
*/
|
||||||
protected function formatExcelTimestampValue($nodeValue)
|
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
|
// Fix for the erroneous leap year in Excel
|
||||||
if ($nodeValue < self::ERRONEOUS_EXCEL_LEAP_YEAR_DAY) {
|
if (ceil($nodeValue) > self::ERRONEOUS_EXCEL_LEAP_YEAR_DAY) {
|
||||||
$numDaysSince1970Jan1++;
|
--$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 {
|
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;
|
return $cellValue;
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
return null;
|
return null;
|
||||||
|
80
tests/Spout/Reader/XLSX/Helper/CellValueFormatterTest.php
Normal file
80
tests/Spout/Reader/XLSX/Helper/CellValueFormatterTest.php
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Box\Spout\Reader\XLSX\Helper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class CellValueFormatterTest
|
||||||
|
*
|
||||||
|
* @package Box\Spout\Reader\XLSX\Helper
|
||||||
|
*/
|
||||||
|
class CellValueFormatterTest extends \PHPUnit_Framework_TestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function dataProviderForExcelDateTest()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
[ CellValueFormatter::CELL_TYPE_NUMERIC, 42429, '2016-02-29 00:00:00' ],
|
||||||
|
[ CellValueFormatter::CELL_TYPE_NUMERIC, '146098', '2299-12-31 00:00:00' ],
|
||||||
|
[ CellValueFormatter::CELL_TYPE_NUMERIC, -700, null ],
|
||||||
|
[ CellValueFormatter::CELL_TYPE_NUMERIC, 0, null ],
|
||||||
|
[ CellValueFormatter::CELL_TYPE_NUMERIC, 0.5, null ],
|
||||||
|
[ CellValueFormatter::CELL_TYPE_NUMERIC, 1, '1900-01-01 00:00:00' ],
|
||||||
|
[ CellValueFormatter::CELL_TYPE_NUMERIC, 59.999988425926, '1900-02-28 23:59:59' ],
|
||||||
|
[ CellValueFormatter::CELL_TYPE_NUMERIC, 60.458333333333, '1900-02-28 11:00:00' ],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider dataProviderForExcelDateTest
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testExcelDate($cellType, $nodeValue, $expectedDateAsString)
|
||||||
|
{
|
||||||
|
$nodeListMock = $this->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'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user