diff --git a/src/Spout/Reader/Common/Entity/Options.php b/src/Spout/Reader/Common/Entity/Options.php index 615160d..293d4c0 100644 --- a/src/Spout/Reader/Common/Entity/Options.php +++ b/src/Spout/Reader/Common/Entity/Options.php @@ -19,4 +19,5 @@ abstract class Options // XLSX specific options const TEMP_FOLDER = 'tempFolder'; + const SHOULD_USE_1904_DATES = 'shouldUse1904Dates'; } diff --git a/src/Spout/Reader/XLSX/Creator/EntityFactory.php b/src/Spout/Reader/XLSX/Creator/EntityFactory.php index 18fe3ef..8699529 100644 --- a/src/Spout/Reader/XLSX/Creator/EntityFactory.php +++ b/src/Spout/Reader/XLSX/Creator/EntityFactory.php @@ -85,7 +85,14 @@ class EntityFactory implements EntityFactoryInterface $styleManager = $this->managerFactory->createStyleManager($filePath, $this); $shouldFormatDates = $optionsManager->getOption(Options::SHOULD_FORMAT_DATES); - $cellValueFormatter = $this->helperFactory->createCellValueFormatter($sharedStringsManager, $styleManager, $shouldFormatDates); + $shouldUse1904Dates = $optionsManager->getOption(Options::SHOULD_USE_1904_DATES); + + $cellValueFormatter = $this->helperFactory->createCellValueFormatter( + $sharedStringsManager, + $styleManager, + $shouldFormatDates, + $shouldUse1904Dates + ); $shouldPreserveEmptyRows = $optionsManager->getOption(Options::SHOULD_PRESERVE_EMPTY_ROWS); diff --git a/src/Spout/Reader/XLSX/Creator/HelperFactory.php b/src/Spout/Reader/XLSX/Creator/HelperFactory.php index 6546712..80ee692 100644 --- a/src/Spout/Reader/XLSX/Creator/HelperFactory.php +++ b/src/Spout/Reader/XLSX/Creator/HelperFactory.php @@ -17,13 +17,14 @@ class HelperFactory extends \Box\Spout\Common\Creator\HelperFactory * @param SharedStringsManager $sharedStringsManager Manages shared strings * @param StyleManager $styleManager Manages styles * @param bool $shouldFormatDates Whether date/time values should be returned as PHP objects or be formatted as strings + * @param bool $shouldUse1904Dates Whether date/time values should use a calendar starting in 1904 instead of 1900 * @return CellValueFormatter */ - public function createCellValueFormatter($sharedStringsManager, $styleManager, $shouldFormatDates) + public function createCellValueFormatter($sharedStringsManager, $styleManager, $shouldFormatDates, $shouldUse1904Dates) { $escaper = $this->createStringsEscaper(); - return new CellValueFormatter($sharedStringsManager, $styleManager, $shouldFormatDates, $escaper); + return new CellValueFormatter($sharedStringsManager, $styleManager, $shouldFormatDates, $shouldUse1904Dates, $escaper); } /** diff --git a/src/Spout/Reader/XLSX/Helper/CellValueFormatter.php b/src/Spout/Reader/XLSX/Helper/CellValueFormatter.php index 458765e..ccf7115 100644 --- a/src/Spout/Reader/XLSX/Helper/CellValueFormatter.php +++ b/src/Spout/Reader/XLSX/Helper/CellValueFormatter.php @@ -48,6 +48,9 @@ class CellValueFormatter /** @var bool Whether date/time values should be returned as PHP objects or be formatted as strings */ protected $shouldFormatDates; + /** @var bool Whether date/time values should use a calendar starting in 1904 instead of 1900 */ + protected $shouldUse1904Dates; + /** @var \Box\Spout\Common\Helper\Escaper\XLSX Used to unescape XML data */ protected $escaper; @@ -55,13 +58,15 @@ class CellValueFormatter * @param SharedStringsManager $sharedStringsManager Manages shared strings * @param StyleManager $styleManager Manages styles * @param bool $shouldFormatDates Whether date/time values should be returned as PHP objects or be formatted as strings + * @param bool $shouldUse1904Dates Whether date/time values should use a calendar starting in 1904 instead of 1900 * @param \Box\Spout\Common\Helper\Escaper\XLSX $escaper Used to unescape XML data */ - public function __construct($sharedStringsManager, $styleManager, $shouldFormatDates, $escaper) + public function __construct($sharedStringsManager, $styleManager, $shouldFormatDates, $shouldUse1904Dates, $escaper) { $this->sharedStringsManager = $sharedStringsManager; $this->styleManager = $styleManager; $this->shouldFormatDates = $shouldFormatDates; + $this->shouldUse1904Dates = $shouldUse1904Dates; $this->escaper = $escaper; } @@ -189,26 +194,20 @@ class CellValueFormatter /** * Returns a cell's PHP Date value, associated to the given timestamp. - * NOTE: The timestamp is a float representing the number of days since January 1st, 1900. + * NOTE: The timestamp is a float representing the number of days since the base Excel date: + * Dec 30th 1899, 1900 or Jan 1st, 1904, depending on the Workbook setting. * NOTE: The timestamp can also represent a time, if it is a value between 0 and 1. * + * @see ECMA-376 Part 1 - §18.17.4 + * * @param float $nodeValue * @param int $cellStyleId 0 being the default style * @return \DateTime|null The value associated with the cell or NULL if invalid date value */ protected function formatExcelTimestampValue($nodeValue, $cellStyleId) { - // Fix for the erroneous leap year in Excel - if (ceil($nodeValue) > self::ERRONEOUS_EXCEL_LEAP_YEAR_DAY) { - $nodeValue--; - } - - if ($nodeValue >= 1) { - // Values greater than 1 represent "dates". The value 1.0 representing the "base" date: 1900-01-01. - $cellValue = $this->formatExcelTimestampValueAsDateValue($nodeValue, $cellStyleId); - } elseif ($nodeValue >= 0) { - // Values between 0 and 1 represent "times". - $cellValue = $this->formatExcelTimestampValueAsTimeValue($nodeValue, $cellStyleId); + if ($this->isValidTimestampValue($nodeValue)) { + $cellValue = $this->formatExcelTimestampValueAsDateTimeValue($nodeValue, $cellStyleId); } else { // invalid date $cellValue = null; @@ -217,24 +216,42 @@ class CellValueFormatter return $cellValue; } + /** + * Returns whether the given timestamp is supported by SpreadsheetML + * @see ECMA-376 Part 1 - §18.17.4 - this specifies the timestamp boundaries. + * + * @param float $timestampValue + * @return bool + */ + protected function isValidTimestampValue($timestampValue) + { + // @NOTE: some versions of Excel don't support negative dates (e.g. Excel for Mac 2011) + return ( + $this->shouldUse1904Dates && $timestampValue >= -695055 && $timestampValue <= 2957003.9999884 || + !$this->shouldUse1904Dates && $timestampValue >= -693593 && $timestampValue <= 2958465.9999884 + ); + } + /** * Returns a cell's PHP DateTime value, associated to the given timestamp. - * Only the time value matters. The date part is set to Jan 1st, 1900 (base Excel date). + * Only the time value matters. The date part is set to the base Excel date: + * Dec 30th 1899, 1900 or Jan 1st, 1904, depending on the Workbook setting. * * @param float $nodeValue * @param int $cellStyleId 0 being the default style * @return \DateTime|string The value associated with the cell */ - protected function formatExcelTimestampValueAsTimeValue($nodeValue, $cellStyleId) + protected function formatExcelTimestampValueAsDateTimeValue($nodeValue, $cellStyleId) { - $time = round($nodeValue * self::NUM_SECONDS_IN_ONE_DAY); - $hours = floor($time / self::NUM_SECONDS_IN_ONE_HOUR); - $minutes = floor($time / self::NUM_SECONDS_IN_ONE_MINUTE) - ($hours * self::NUM_SECONDS_IN_ONE_MINUTE); - $seconds = $time - ($hours * self::NUM_SECONDS_IN_ONE_HOUR) - ($minutes * self::NUM_SECONDS_IN_ONE_MINUTE); + $baseDate = $this->shouldUse1904Dates ? '1904-01-01' : '1899-12-30'; - // using the base Excel date (Jan 1st, 1900) - not relevant here - $dateObj = new \DateTime('1900-01-01'); - $dateObj->setTime($hours, $minutes, $seconds); + $daysSinceBaseDate = (int) $nodeValue; + $timeRemainder = fmod($nodeValue, 1); + $secondsRemainder = round($timeRemainder * self::NUM_SECONDS_IN_ONE_DAY, 0); + + $dateObj = \DateTime::createFromFormat('|Y-m-d', $baseDate); + $dateObj->modify('+' . $daysSinceBaseDate . 'days'); + $dateObj->modify('+' . $secondsRemainder . 'seconds'); if ($this->shouldFormatDates) { $styleNumberFormatCode = $this->styleManager->getNumberFormatCode($cellStyleId); @@ -247,40 +264,6 @@ class CellValueFormatter return $cellValue; } - /** - * Returns a cell's PHP Date value, associated to the given timestamp. - * NOTE: The timestamp is a float representing the number of days since January 1st, 1900. - * - * @param float $nodeValue - * @param int $cellStyleId 0 being the default style - * @return \DateTime|string|null The value associated with the cell or NULL if invalid date value - */ - protected function formatExcelTimestampValueAsDateValue($nodeValue, $cellStyleId) - { - // 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 { - $dateObj = \DateTime::createFromFormat('|Y-m-d', '1899-12-31'); - $dateObj->modify('+' . (int) $nodeValue . 'days'); - $dateObj->modify('+' . $secondsRemainder . 'seconds'); - - if ($this->shouldFormatDates) { - $styleNumberFormatCode = $this->styleManager->getNumberFormatCode($cellStyleId); - $phpDateFormat = DateFormatHelper::toPHPDateFormat($styleNumberFormatCode); - $cellValue = $dateObj->format($phpDateFormat); - } else { - $cellValue = $dateObj; - } - } catch (\Exception $e) { - $cellValue = null; - } - - return $cellValue; - } - /** * Returns the cell Boolean value from a specific node's Value. * diff --git a/src/Spout/Reader/XLSX/Manager/OptionsManager.php b/src/Spout/Reader/XLSX/Manager/OptionsManager.php index 7677c22..253e009 100644 --- a/src/Spout/Reader/XLSX/Manager/OptionsManager.php +++ b/src/Spout/Reader/XLSX/Manager/OptionsManager.php @@ -20,6 +20,7 @@ class OptionsManager extends OptionsManagerAbstract Options::TEMP_FOLDER, Options::SHOULD_FORMAT_DATES, Options::SHOULD_PRESERVE_EMPTY_ROWS, + Options::SHOULD_USE_1904_DATES, ]; } @@ -31,5 +32,6 @@ class OptionsManager extends OptionsManagerAbstract $this->setOption(Options::TEMP_FOLDER, sys_get_temp_dir()); $this->setOption(Options::SHOULD_FORMAT_DATES, false); $this->setOption(Options::SHOULD_PRESERVE_EMPTY_ROWS, false); + $this->setOption(Options::SHOULD_USE_1904_DATES, false); } } diff --git a/src/Spout/Reader/XLSX/Manager/SheetManager.php b/src/Spout/Reader/XLSX/Manager/SheetManager.php index a848948..1dbcc95 100644 --- a/src/Spout/Reader/XLSX/Manager/SheetManager.php +++ b/src/Spout/Reader/XLSX/Manager/SheetManager.php @@ -2,6 +2,8 @@ namespace Box\Spout\Reader\XLSX\Manager; +use Box\Spout\Reader\Common\Entity\Options; +use Box\Spout\Reader\Common\XMLProcessor; use Box\Spout\Reader\XLSX\Creator\EntityFactory; use Box\Spout\Reader\XLSX\Sheet; @@ -16,12 +18,14 @@ class SheetManager const WORKBOOK_XML_FILE_PATH = 'xl/workbook.xml'; /** Definition of XML node names used to parse data */ + const XML_NODE_WORKBOOK_PROPERTIES = 'workbookPr'; const XML_NODE_WORKBOOK_VIEW = 'workbookView'; const XML_NODE_SHEET = 'sheet'; const XML_NODE_SHEETS = 'sheets'; const XML_NODE_RELATIONSHIP = 'Relationship'; /** Definition of XML attributes used to parse data */ + const XML_ATTRIBUTE_DATE_1904 = 'date1904'; const XML_ATTRIBUTE_ACTIVE_TAB = 'activeTab'; const XML_ATTRIBUTE_R_ID = 'r:id'; const XML_ATTRIBUTE_NAME = 'name'; @@ -46,6 +50,15 @@ class SheetManager /** @var \Box\Spout\Common\Helper\Escaper\XLSX Used to unescape XML data */ protected $escaper; + /** @var array List of sheets */ + protected $sheets; + + /** @var int Index of the sheet currently read */ + protected $currentSheetIndex; + + /** @var int Index of the active sheet (0 by default) */ + protected $activeSheetIndex; + /** * @param string $filePath Path of the XLSX file being read * @param \Box\Spout\Common\Manager\OptionsManagerInterface $optionsManager Reader's options manager @@ -71,32 +84,70 @@ class SheetManager */ public function getSheets() { - $sheets = []; - $sheetIndex = 0; - $activeSheetIndex = 0; // By default, the first sheet is active + $this->sheets = []; + $this->currentSheetIndex = 0; + $this->activeSheetIndex = 0; // By default, the first sheet is active $xmlReader = $this->entityFactory->createXMLReader(); + $xmlProcessor = $this->entityFactory->createXMLProcessor($xmlReader); + + $xmlProcessor->registerCallback(self::XML_NODE_WORKBOOK_PROPERTIES, XMLProcessor::NODE_TYPE_START, [$this, 'processWorkbookPropertiesStartingNode']); + $xmlProcessor->registerCallback(self::XML_NODE_WORKBOOK_VIEW, XMLProcessor::NODE_TYPE_START, [$this, 'processWorkbookViewStartingNode']); + $xmlProcessor->registerCallback(self::XML_NODE_SHEET, XMLProcessor::NODE_TYPE_START, [$this, 'processSheetStartingNode']); + $xmlProcessor->registerCallback(self::XML_NODE_SHEETS, XMLProcessor::NODE_TYPE_END, [$this, 'processSheetsEndingNode']); if ($xmlReader->openFileInZip($this->filePath, self::WORKBOOK_XML_FILE_PATH)) { - while ($xmlReader->read()) { - if ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_WORKBOOK_VIEW)) { - // The "workbookView" node is located before "sheet" nodes, ensuring that - // the active sheet is known before parsing sheets data. - $activeSheetIndex = (int) $xmlReader->getAttribute(self::XML_ATTRIBUTE_ACTIVE_TAB); - } elseif ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_SHEET)) { - $isSheetActive = ($sheetIndex === $activeSheetIndex); - $sheets[] = $this->getSheetFromSheetXMLNode($xmlReader, $sheetIndex, $isSheetActive); - $sheetIndex++; - } elseif ($xmlReader->isPositionedOnEndingNode(self::XML_NODE_SHEETS)) { - // stop reading once all sheets have been read - break; - } - } - + $xmlProcessor->readUntilStopped(); $xmlReader->close(); } - return $sheets; + return $this->sheets; + } + + /** + * @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "" starting node + * @return int A return code that indicates what action should the processor take next + */ + protected function processWorkbookPropertiesStartingNode($xmlReader) + { + $shouldUse1904Dates = (bool) $xmlReader->getAttribute(self::XML_ATTRIBUTE_DATE_1904); + $this->optionsManager->setOption(Options::SHOULD_USE_1904_DATES, $shouldUse1904Dates); + + return XMLProcessor::PROCESSING_CONTINUE; + } + + /** + * @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "" starting node + * @return int A return code that indicates what action should the processor take next + */ + protected function processWorkbookViewStartingNode($xmlReader) + { + // The "workbookView" node is located before "sheet" nodes, ensuring that + // the active sheet is known before parsing sheets data. + $this->activeSheetIndex = (int) $xmlReader->getAttribute(self::XML_ATTRIBUTE_ACTIVE_TAB); + + return XMLProcessor::PROCESSING_CONTINUE; + } + + /** + * @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "" starting node + * @return int A return code that indicates what action should the processor take next + */ + protected function processSheetStartingNode($xmlReader) + { + $isSheetActive = ($this->currentSheetIndex === $this->activeSheetIndex); + $this->sheets[] = $this->getSheetFromSheetXMLNode($xmlReader, $this->currentSheetIndex, $isSheetActive); + $this->currentSheetIndex++; + + return XMLProcessor::PROCESSING_CONTINUE; + } + + /** + * @return int A return code that indicates what action should the processor take next + */ + protected function processSheetsEndingNode() + { + return XMLProcessor::PROCESSING_STOP; } /** diff --git a/tests/Spout/Reader/XLSX/Helper/CellValueFormatterTest.php b/tests/Spout/Reader/XLSX/Helper/CellValueFormatterTest.php index ef58574..4167cf8 100644 --- a/tests/Spout/Reader/XLSX/Helper/CellValueFormatterTest.php +++ b/tests/Spout/Reader/XLSX/Helper/CellValueFormatterTest.php @@ -15,29 +15,51 @@ class CellValueFormatterTest extends \PHPUnit_Framework_TestCase public function dataProviderForTestExcelDate() { 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, '1900-01-01 00:00:00'], - [CellValueFormatter::CELL_TYPE_NUMERIC, 0.25, '1900-01-01 06:00:00'], - [CellValueFormatter::CELL_TYPE_NUMERIC, 0.5, '1900-01-01 12:00:00'], - [CellValueFormatter::CELL_TYPE_NUMERIC, 0.75, '1900-01-01 18:00:00'], - [CellValueFormatter::CELL_TYPE_NUMERIC, 0.99999, '1900-01-01 23:59:59'], - [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'], + // use 1904 dates, node value, expected date as string + + // 1900 calendar + [false, 3687.4207639, '1910-02-03 10:05:54'], + [false, 2.5000000, '1900-01-01 12:00:00'], + [false, 2958465.9999884, '9999-12-31 23:59:59'], + [false, 2958465.9999885, null], + [false, -2337.999989, '1893-08-05 00:00:01'], + [false, -693593, '0001-01-01 00:00:00'], + [false, -693593.0000001, null], + [false, 0, '1899-12-30 00:00:00'], + [false, 0.25, '1899-12-30 06:00:00'], + [false, 0.5, '1899-12-30 12:00:00'], + [false, 0.75, '1899-12-30 18:00:00'], + [false, 0.99999, '1899-12-30 23:59:59'], + [false, 1, '1899-12-31 00:00:00'], + [false, '3687.4207639', '1910-02-03 10:05:54'], + + // 1904 calendar + [true, 2225.4207639, '1910-02-03 10:05:54'], + [true, 2.5000000, '1904-01-03 12:00:00'], + [true, 2957003.9999884, '9999-12-31 23:59:59'], + [true, 2957003.9999885, null], + [true, -3799.999989, '1893-08-05 00:00:01'], + [true, -695055, '0001-01-01 00:00:00'], + [true, -695055.0000001, null], + [true, 0, '1904-01-01 00:00:00'], + [true, 0.25, '1904-01-01 06:00:00'], + [true, 0.5, '1904-01-01 12:00:00'], + [true, 0.75, '1904-01-01 18:00:00'], + [true, 0.99999, '1904-01-01 23:59:59'], + [true, 1, '1904-01-02 00:00:00'], + [true, '2225.4207639', '1910-02-03 10:05:54'], ]; } /** * @dataProvider dataProviderForTestExcelDate * - * @param string $cellType + * @param bool $shouldUse1904Dates * @param int|float|string $nodeValue * @param string|null $expectedDateAsString * @return void */ - public function testExcelDate($cellType, $nodeValue, $expectedDateAsString) + public function testExcelDate($shouldUse1904Dates, $nodeValue, $expectedDateAsString) { $nodeListMock = $this->getMockBuilder('DOMNodeList')->disableOriginalConstructor()->getMock(); @@ -53,7 +75,7 @@ class CellValueFormatterTest extends \PHPUnit_Framework_TestCase ->expects($this->atLeastOnce()) ->method('getAttribute') ->will($this->returnValueMap([ - [CellValueFormatter::XML_ATTRIBUTE_TYPE, $cellType], + [CellValueFormatter::XML_ATTRIBUTE_TYPE, CellValueFormatter::CELL_TYPE_NUMERIC], [CellValueFormatter::XML_ATTRIBUTE_STYLE_ID, 123], ])); @@ -72,7 +94,7 @@ class CellValueFormatterTest extends \PHPUnit_Framework_TestCase ->with(123) ->will($this->returnValue(true)); - $formatter = new CellValueFormatter(null, $styleManagerMock, false, new Escaper\XLSX()); + $formatter = new CellValueFormatter(null, $styleManagerMock, false, $shouldUse1904Dates, new Escaper\XLSX()); $result = $formatter->extractAndFormatNodeValue($nodeMock); if ($expectedDateAsString === null) { @@ -126,7 +148,7 @@ class CellValueFormatterTest extends \PHPUnit_Framework_TestCase ->method('shouldFormatNumericValueAsDate') ->will($this->returnValue(false)); - $formatter = new CellValueFormatter(null, $styleManagerMock, false, new Escaper\XLSX()); + $formatter = new CellValueFormatter(null, $styleManagerMock, false, false, new Escaper\XLSX()); $formattedValue = \ReflectionHelper::callMethodOnObject($formatter, 'formatNumericCellValue', $value, 0); $this->assertEquals($expectedFormattedValue, $formattedValue); @@ -169,7 +191,7 @@ class CellValueFormatterTest extends \PHPUnit_Framework_TestCase ->with(CellValueFormatter::XML_NODE_INLINE_STRING_VALUE) ->will($this->returnValue($nodeListMock)); - $formatter = new CellValueFormatter(null, null, false, new Escaper\XLSX()); + $formatter = new CellValueFormatter(null, null, false, false, new Escaper\XLSX()); $formattedValue = \ReflectionHelper::callMethodOnObject($formatter, 'formatInlineStringCellValue', $nodeMock); $this->assertEquals($expectedFormattedValue, $formattedValue); diff --git a/tests/Spout/Reader/XLSX/ReaderTest.php b/tests/Spout/Reader/XLSX/ReaderTest.php index e51f2df..00a0ede 100644 --- a/tests/Spout/Reader/XLSX/ReaderTest.php +++ b/tests/Spout/Reader/XLSX/ReaderTest.php @@ -231,9 +231,34 @@ class ReaderTest extends \PHPUnit_Framework_TestCase \DateTime::createFromFormat('Y-m-d H:i:s', '2015-09-01 22:23:00'), ], [ - \DateTime::createFromFormat('Y-m-d H:i:s', '1900-02-28 23:59:59'), + \DateTime::createFromFormat('Y-m-d H:i:s', '1900-02-27 23:59:59'), \DateTime::createFromFormat('Y-m-d H:i:s', '1900-03-01 00:00:00'), - \DateTime::createFromFormat('Y-m-d H:i:s', '1900-02-28 11:00:00'), // 1900-02-29 should be converted to 1900-02-28 + \DateTime::createFromFormat('Y-m-d H:i:s', '1900-02-28 11:00:00'), + ], + ]; + $this->assertEquals($expectedRows, $allRows); + } + + /** + * @return void + */ + public function testReadShouldSupportDifferentDatesAsNumericTimestampWith1904Calendar() + { + // make sure dates are always created with the same timezone + date_default_timezone_set('UTC'); + + $allRows = $this->getAllRowsForFile('sheet_with_different_numeric_value_dates_1904_calendar.xlsx'); + + $expectedRows = [ + [ + \DateTime::createFromFormat('Y-m-d H:i:s', '2019-09-02 00:00:00'), + \DateTime::createFromFormat('Y-m-d H:i:s', '2019-09-03 00:00:00'), + \DateTime::createFromFormat('Y-m-d H:i:s', '2019-09-02 22:23:00'), + ], + [ + \DateTime::createFromFormat('Y-m-d H:i:s', '1904-02-29 23:59:59'), + \DateTime::createFromFormat('Y-m-d H:i:s', '1904-03-02 00:00:00'), + \DateTime::createFromFormat('Y-m-d H:i:s', '1904-03-01 11:00:00'), ], ]; $this->assertEquals($expectedRows, $allRows); @@ -251,11 +276,11 @@ class ReaderTest extends \PHPUnit_Framework_TestCase $expectedRows = [ [ - \DateTime::createFromFormat('Y-m-d H:i:s', '1900-01-01 00:00:00'), - \DateTime::createFromFormat('Y-m-d H:i:s', '1900-01-01 11:29:00'), - \DateTime::createFromFormat('Y-m-d H:i:s', '1900-01-01 23:29:00'), - \DateTime::createFromFormat('Y-m-d H:i:s', '1900-01-01 01:42:25'), - \DateTime::createFromFormat('Y-m-d H:i:s', '1900-01-01 13:42:25'), + \DateTime::createFromFormat('Y-m-d H:i:s', '1899-12-30 00:00:00'), + \DateTime::createFromFormat('Y-m-d H:i:s', '1899-12-30 11:29:00'), + \DateTime::createFromFormat('Y-m-d H:i:s', '1899-12-30 23:29:00'), + \DateTime::createFromFormat('Y-m-d H:i:s', '1899-12-30 01:42:25'), + \DateTime::createFromFormat('Y-m-d H:i:s', '1899-12-30 13:42:25'), ], ]; $this->assertEquals($expectedRows, $allRows); diff --git a/tests/resources/xlsx/sheet_with_different_numeric_value_dates_1904_calendar.xlsx b/tests/resources/xlsx/sheet_with_different_numeric_value_dates_1904_calendar.xlsx new file mode 100644 index 0000000..e496ef0 Binary files /dev/null and b/tests/resources/xlsx/sheet_with_different_numeric_value_dates_1904_calendar.xlsx differ