From 5a7c2c1262ed683067c4ee074da104af658eddc7 Mon Sep 17 00:00:00 2001 From: Adrien Loison Date: Thu, 19 May 2016 09:40:12 -0700 Subject: [PATCH 1/7] Handle General number format as non date (#221) If the number format is set to General (id = 0), do no try to format the value as a date --- src/Spout/Reader/XLSX/Helper/StyleHelper.php | 16 ++++++++++++++-- .../Spout/Reader/XLSX/Helper/StyleHelperTest.php | 10 ++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/Spout/Reader/XLSX/Helper/StyleHelper.php b/src/Spout/Reader/XLSX/Helper/StyleHelper.php index 403d647..19014b5 100644 --- a/src/Spout/Reader/XLSX/Helper/StyleHelper.php +++ b/src/Spout/Reader/XLSX/Helper/StyleHelper.php @@ -171,11 +171,23 @@ class StyleHelper protected function doesNumFmtIdIndicateDate($numFmtId) { return ( - $this->isNumFmtIdBuiltInDateFormat($numFmtId) || - $this->isNumFmtIdCustomDateFormat($numFmtId) + !$this->doesNumFmtIdIndicateGeneralFormat($numFmtId) && + ( + $this->isNumFmtIdBuiltInDateFormat($numFmtId) || + $this->isNumFmtIdCustomDateFormat($numFmtId) + ) ); } + /** + * @param int $numFmtId + * @return bool Whether the number format ID indicates the "General" format (0 by convention) + */ + protected function doesNumFmtIdIndicateGeneralFormat($numFmtId) + { + return ($numFmtId === 0); + } + /** * @param int $numFmtId * @return bool Whether the number format ID indicates that the number is a timestamp diff --git a/tests/Spout/Reader/XLSX/Helper/StyleHelperTest.php b/tests/Spout/Reader/XLSX/Helper/StyleHelperTest.php index 3b8edff..57e8acb 100644 --- a/tests/Spout/Reader/XLSX/Helper/StyleHelperTest.php +++ b/tests/Spout/Reader/XLSX/Helper/StyleHelperTest.php @@ -59,6 +59,16 @@ class StyleHelperTest extends \PHPUnit_Framework_TestCase $this->assertFalse($shouldFormatAsDate); } + /** + * @return void + */ + public function testShouldFormatNumericValueAsDateWithGeneralFormat() + { + $styleHelper = $this->getStyleHelperMock([[], ['applyNumberFormat' => true, 'numFmtId' => 0]]); + $shouldFormatAsDate = $styleHelper->shouldFormatNumericValueAsDate(1); + $this->assertFalse($shouldFormatAsDate); + } + /** * @return void */ From b8fd789ac016636c871df5560b4e7514e00e3cfe Mon Sep 17 00:00:00 2001 From: Adrien Loison Date: Thu, 19 May 2016 10:37:48 -0700 Subject: [PATCH 2/7] Retrieve XLSX sheets in order of appearance (#220) Instead of relying on the ID, sheets should be retrieved in the order they appear in the file. Workbook.xml describes the correct order. This allows the reader to read data in the correct order when sheets have been manually moved after creation. --- src/Spout/Reader/XLSX/Helper/SheetHelper.php | 73 +++++++----------- tests/Spout/Reader/XLSX/ReaderTest.php | 2 +- .../file_with_no_sheets_in_content_types.xlsx | Bin 3720 -> 0 bytes .../file_with_no_sheets_in_workbook_xml.xlsx | Bin 0 -> 3704 bytes 4 files changed, 28 insertions(+), 47 deletions(-) delete mode 100644 tests/resources/xlsx/file_with_no_sheets_in_content_types.xlsx create mode 100644 tests/resources/xlsx/file_with_no_sheets_in_workbook_xml.xlsx diff --git a/src/Spout/Reader/XLSX/Helper/SheetHelper.php b/src/Spout/Reader/XLSX/Helper/SheetHelper.php index 3400509..23a2b08 100644 --- a/src/Spout/Reader/XLSX/Helper/SheetHelper.php +++ b/src/Spout/Reader/XLSX/Helper/SheetHelper.php @@ -14,18 +14,13 @@ use Box\Spout\Reader\XLSX\Sheet; class SheetHelper { /** Paths of XML files relative to the XLSX file root */ - const CONTENT_TYPES_XML_FILE_PATH = '[Content_Types].xml'; const WORKBOOK_XML_RELS_FILE_PATH = 'xl/_rels/workbook.xml.rels'; const WORKBOOK_XML_FILE_PATH = 'xl/workbook.xml'; /** Namespaces for the XML files */ - const MAIN_NAMESPACE_FOR_CONTENT_TYPES_XML = 'http://schemas.openxmlformats.org/package/2006/content-types'; const MAIN_NAMESPACE_FOR_WORKBOOK_XML_RELS = 'http://schemas.openxmlformats.org/package/2006/relationships'; const MAIN_NAMESPACE_FOR_WORKBOOK_XML = 'http://schemas.openxmlformats.org/spreadsheetml/2006/main'; - /** Value of the Override attribute used in [Content_Types].xml to define sheets */ - const OVERRIDE_CONTENT_TYPES_ATTRIBUTE = 'application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml'; - /** @var string Path of the XLSX file being read */ protected $filePath; @@ -63,65 +58,51 @@ class SheetHelper { $sheets = []; - $contentTypesAsXMLElement = $this->getFileAsXMLElementWithNamespace( - self::CONTENT_TYPES_XML_FILE_PATH, - self::MAIN_NAMESPACE_FOR_CONTENT_TYPES_XML - ); + // Starting from "workbook.xml" as this file is the source of truth for the sheets order + $workbookXMLElement = $this->getWorkbookXMLAsXMLElement(); + $sheetNodes = $workbookXMLElement->xpath('//ns:sheet'); - // find all nodes defining a sheet - $sheetNodes = $contentTypesAsXMLElement->xpath('//ns:Override[@ContentType="' . self::OVERRIDE_CONTENT_TYPES_ATTRIBUTE . '"]'); - $numSheetNodes = count($sheetNodes); - - for ($i = 0; $i < $numSheetNodes; $i++) { - $sheetNode = $sheetNodes[$i]; - $sheetDataXMLFilePath = $sheetNode->getAttribute('PartName'); - - $sheets[] = $this->getSheetFromXML($sheetDataXMLFilePath); + foreach ($sheetNodes as $sheetIndex => $sheetNode) { + $sheets[] = $this->getSheetFromSheetXMLNode($sheetNode, $sheetIndex); } - // make sure the sheets are sorted by index - // (as the sheets are not necessarily in this order in the XML file) - usort($sheets, function ($sheet1, $sheet2) { - return ($sheet1->getIndex() - $sheet2->getIndex()); - }); - return $sheets; } /** - * Returns an instance of a sheet, given the path of its data XML file. - * We first look at "xl/_rels/workbook.xml.rels" to find the relationship ID of the sheet. - * Then we look at "xl/worbook.xml" to find the sheet entry associated to the found ID. - * The entry contains the ID and name of the sheet. + * Returns an instance of a sheet, given the XML node describing the sheet - from "workbook.xml". + * We can find the XML file path describing the sheet inside "workbook.xml.res", by mapping with the sheet ID + * ("r:id" in "workbook.xml", "Id" in "workbook.xml.res"). * - * @param string $sheetDataXMLFilePath Path of the sheet data XML file as in [Content_Types].xml + * @param \Box\Spout\Reader\Wrapper\SimpleXMLElement $sheetNode XML Node describing the sheet, as defined in "workbook.xml" + * @param int $sheetIndexZeroBased Index of the sheet, based on order of appearance in the workbook (zero-based) * @return \Box\Spout\Reader\XLSX\Sheet Sheet instance */ - protected function getSheetFromXML($sheetDataXMLFilePath) + protected function getSheetFromSheetXMLNode($sheetNode, $sheetIndexZeroBased) { - // In [Content_Types].xml, the path is "/xl/worksheets/sheet1.xml" - // In workbook.xml.rels, it is only "worksheets/sheet1.xml" - $sheetDataXMLFilePathInWorkbookXMLRels = ltrim($sheetDataXMLFilePath, '/xl/'); - - // find the node associated to the given file path - $workbookXMLResElement = $this->getWorkbookXMLRelsAsXMLElement(); - $relationshipNodes = $workbookXMLResElement->xpath('//ns:Relationship[@Target="' . $sheetDataXMLFilePathInWorkbookXMLRels . '"]'); - $relationshipNode = $relationshipNodes[0]; - - $relationshipSheetId = $relationshipNode->getAttribute('Id'); - - $workbookXMLElement = $this->getWorkbookXMLAsXMLElement(); - $sheetNodes = $workbookXMLElement->xpath('//ns:sheet[@r:id="' . $relationshipSheetId . '"]'); - $sheetNode = $sheetNodes[0]; + // To retrieve namespaced attributes, some versions of LibXML will accept prefixing the attribute + // with the namespace directly (tested on LibXML 2.9.3). For older versions (tested on LibXML 2.7.8), + // attributes need to be retrieved without the namespace hint. + $sheetId = $sheetNode->getAttribute('r:id'); + if ($sheetId === null) { + $sheetId = $sheetNode->getAttribute('id'); + } $escapedSheetName = $sheetNode->getAttribute('name'); - $sheetIdOneBased = $sheetNode->getAttribute('sheetId'); - $sheetIndexZeroBased = $sheetIdOneBased - 1; /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */ $escaper = new \Box\Spout\Common\Escaper\XLSX(); $sheetName = $escaper->unescape($escapedSheetName); + // find the file path of the sheet, by looking at the "workbook.xml.res" file + $workbookXMLResElement = $this->getWorkbookXMLRelsAsXMLElement(); + $relationshipNodes = $workbookXMLResElement->xpath('//ns:Relationship[@Id="' . $sheetId . '"]'); + $relationshipNode = $relationshipNodes[0]; + + // In workbook.xml.rels, it is only "worksheets/sheet1.xml" + // In [Content_Types].xml, the path is "/xl/worksheets/sheet1.xml" + $sheetDataXMLFilePath = '/xl/' . $relationshipNode->getAttribute('Target'); + return new Sheet($this->filePath, $sheetDataXMLFilePath, $this->sharedStringsHelper, $sheetIndexZeroBased, $sheetName); } diff --git a/tests/Spout/Reader/XLSX/ReaderTest.php b/tests/Spout/Reader/XLSX/ReaderTest.php index b1e6fdd..176f6d0 100644 --- a/tests/Spout/Reader/XLSX/ReaderTest.php +++ b/tests/Spout/Reader/XLSX/ReaderTest.php @@ -23,7 +23,7 @@ class ReaderTest extends \PHPUnit_Framework_TestCase { return [ ['/path/to/fake/file.xlsx'], - ['file_with_no_sheets_in_content_types.xlsx'], + ['file_with_no_sheets_in_workbook_xml.xlsx'], ['file_with_sheet_xml_not_matching_content_types.xlsx'], ['file_corrupted.xlsx'], ]; diff --git a/tests/resources/xlsx/file_with_no_sheets_in_content_types.xlsx b/tests/resources/xlsx/file_with_no_sheets_in_content_types.xlsx deleted file mode 100644 index 597b23078fd27a6e807c3eff3dbdfa9f4c2d34d9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3720 zcmai12{@E(_aE6N%UFx-JCQLmb|uD=T__PUG-V&g$k>vJDcQHknm4J}+MpGcrdJeR zdnt`AJNb%92KgQ-Bl`cpb6wAM%{9;Yo#l6*`<(Mwo6$4yfno=aD1YMC2uLLcT! z%XF#LSUfj(szuM$RNTEXam{%~TpIg*ZJEV{C`5(!fl?_IyqOMAZvS_J(^ED&m2+K! zM5o+>J=4A*Q{3BQ`?L8;<>^I&6MImHFODc>ymT=nyD47kEHBY>8C|;9O*ZsbPH?|G zct5&eIOT5-b}5CJ&w3PVmL+_9N=xH$so(-)iWAEo{d4!OmhD$>3wJNpCTHcIy+e$G z;*K;8;1CZGPr`go*P~SuR}?OY>Lg8;zVd>(Jf#RV%ZbS$*pGZXoke!2EYohn58tQP zx<|4L<$rn%Jhop#v^LFqCV0)4q^PhEC8d(278RAoWG?WK8|Ikhe&f*d@R-VKjpE4R zy_m8YIvkl|F5>jcl%Vg2l#|Z$%xgU&61azl@zJD2 z*nz517g@OE*P^6ps=@p@Ch+m*cao*$zEKapLcicdN5Ec-xfAM!#Od6^*B&A|F<&eO zGNY4I!sHcO(k_ISmNKw}n#@QXTs{5@dNf4dgLp%2my7gXFrV=f!m7A(RO6}4g}pjf zle(e_WBLuL&L>Fesv-UT z*K0K|x#8+AWln=R*6)ZWXO&NS1_veaI2Zrku%n@H*d)Y6fx3AA;hpi>Q$Yj# zitN#fwiiYLO!CddCJY?T)2r9jUF?poP3B^wYlw5CtLWc(y_0_}iH zwQugrf140t-mcnP_fJ7>b4Wayc%WleT;k;XQfk|n^W$3j-z!A<02s;#q&W*bTw7Ch zM`KXyx%#F!TamPO~VI`zEr4N7t-TuN-8T&k=G}L>mHKv ztSsT&7L6)Mg&OvTgYorm@neVMqGMZ=$eto@la4-*zI{|2%uUuV6ZdbXe|ZyjubN9Z z!=}Y_dGxV~7$X4!&l*&2mHUpm$HL6YhtADDhXvhUHN;J|d?c4%7a)iWUX6{Lw_W+Z zvY-{Adi-pE28dTZ{e`WFsOxh&4zaX5NZpY&ASx&-#&mSDSw8m%`xyjWB!aLhO_FQ$n>*_a3?!kgfqICVX67 zm@QSNsvEsW1SzM^Scw`@hQoxqWw{rX`R`oi=4Il`IgDi@zfns1_imQ1`INno&o%9t z30!iV!+2jfbh#2Aw^n%=FX=#GH2y+u?V zn?y_bv19Y89U=Lg(rr}NmSe_z;O=CW*ceAe-d=pB=vtq8xp(a{==bnXnwE@f0@rF3 zI864R;bTL5P#baESfEK_Z1q;}B9HXjSBVsRnmSUrJ;#GOE8xnzx%nCVdS>1v!6)sj zFkj|XE^v&5N7N96#SCWe4pzp+t2}$vBanlSM8D1qZj&%6yA=RtoD=&(nOfSP)V=SU z7u}Uxd-++(+)EtdZaw&nfFT`Mw{+GMHuyrW4(8;b86^kFYYty!%DS6ub+1`g2-%#; zcrPifG`Hm_s;+0IqGa9a)x|FTT3d@x-HrYV-{p5%#tt_J-^Qh> zZT7}d4RY(zgNX4#Hsk3%{PQehQVs`)1W?mF{1SQ?9;Bge7|2L)8_4P!m97&(%Sim<}~d>54WisE_Q>-p^32&;kwz= z1uoA#ck_~7L<@)i^*ogFtv%l%ajwPq9H40f`+n^7yFBCPL^QAuK1w+xc-e?B}hnbkE+?l2h&VbZsJu`d)*j&uyk6 z(6^%^LQqjt!!wn=Dna#|_CITGj*(V%)Lp-F-q~y*w44JyP$=y7Np^HuEjS3^rW_0c z@$b<60a0GRMj5>yEHA-$pM4JF8HifAF#Rn{*$lbQH-e5rC(z3-QBzcIIY3U_R~?rU z*eHDYKMD(xbUw?67Me59KdCO3n#34#`wG;~wGgG|&`NRVQor43aiR21q^@{a-R~Kp z*LGMOI#rI~W*jl=B*6USC>Ev6qnYQsWn45*s^8(X`jA5H@Vr;xClH!le!>cCohQzR z9DxxI2|YGqeIE{a7tK11t<(>{FmV2+?B}6W++9!MLOfN_9C6Od#7S1#o;0Z+C^Fai ztx7sJjMKF9`V$qZs)h}*&HjMhVT--ZP!!xQKHs?JQFAxAn6NL=Rig#WAn9r(!LRue zCZPV^=(0~Sf-sTXAk1mbt|HXnpTz|>dKPa0J6KzdINT*}jBOf%Y(rij=qt>cv=-Ci z^=D?VC9xU9gw8VHxnPo9@drN5+|8f7YlJ;X+HMe&F!5}Hl9|)7CYmtCXKh9Yw1)qC zO9swsi~M0{rWx2-8S5({P3(Xg_%(27PXDwb(lEG9%#Vrl|2bRVrD$>m$ZE@G^}E0A zyA=&qxfS+v)1o=uSZKGeD>QUAFn2ed{#ssW@SOy$&uN+rGW?v{zoOj<_&rHF#vgz` zle&G5)BMP7g>7wB+c@4xcl!jS*--<|_fPYXmhpy#?L&xWf%4PB=1AHO-QHViPzRu! z0M56DPXq5HZoNs-!~)>)CwQ}E{UUuM=Iu?3h7H+)rL{0?GsYh^4*@|ymcZdkt$*-< F{tH}$XDt8# diff --git a/tests/resources/xlsx/file_with_no_sheets_in_workbook_xml.xlsx b/tests/resources/xlsx/file_with_no_sheets_in_workbook_xml.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..74de5277f2c8b64188bc2dbc8d2b67a5365cfbfc GIT binary patch literal 3704 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 TebYPy1Ob@?hYMYOp8@>`O9OBp literal 0 HcmV?d00001 From bb20d2e6bb23307b39e245cebf0c54fe0947aff5 Mon Sep 17 00:00:00 2001 From: Adrien Loison Date: Thu, 19 May 2016 11:25:00 -0700 Subject: [PATCH 3/7] Update Scrutinizer dependency and code coverage (#223) --- .travis.yml | 5 +++-- composer.json | 3 +-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 52b729e..b23c7fd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,7 +16,8 @@ install: script: - mkdir -p build/logs - - php vendor/bin/phpunit --coverage-clover build/logs/clover.xml + - php vendor/bin/phpunit --coverage-clover=build/logs/coverage.clover after_script: - - if [[ $TRAVIS_PHP_VERSION != 'hhvm' && $TRAVIS_PHP_VERSION != '7.0' ]]; then php vendor/bin/ocular code-coverage:upload --format=php-clover build/logs/clover.xml; fi + - if [[ $TRAVIS_PHP_VERSION != 'hhvm' && $TRAVIS_PHP_VERSION != '7.0' ]]; then wget https://scrutinizer-ci.com/ocular.phar; fi + - if [[ $TRAVIS_PHP_VERSION != 'hhvm' && $TRAVIS_PHP_VERSION != '7.0' ]]; then php ocular.phar code-coverage:upload --format=php-clover build/logs/coverage.clover; fi diff --git a/composer.json b/composer.json index 46c412e..48f6eb8 100644 --- a/composer.json +++ b/composer.json @@ -18,8 +18,7 @@ "ext-simplexml": "*" }, "require-dev": { - "phpunit/phpunit": ">=3.7", - "scrutinizer/ocular": "~1.1" + "phpunit/phpunit": ">=3.7" }, "suggest": { "ext-iconv": "To handle non UTF-8 CSV files (if \"php-intl\" is not already installed or is too limited)", From b4724906c44991f2c65344870375a4f845591358 Mon Sep 17 00:00:00 2001 From: Adrien Loison Date: Thu, 19 May 2016 13:10:47 -0700 Subject: [PATCH 4/7] Add support for cells formatted as time (#224) Cells formatted as "time" have values between 0 and 1. These values used to be considered as invalid. Note: this uses what was started in #202 --- .../Reader/XLSX/Helper/CellValueFormatter.php | 52 ++++++++++++++++-- .../XLSX/Helper/CellValueFormatterTest.php | 7 ++- tests/Spout/Reader/XLSX/ReaderTest.php | 22 ++++++++ ...et_with_different_numeric_value_times.xlsx | Bin 0 -> 28226 bytes 4 files changed, 73 insertions(+), 8 deletions(-) create mode 100644 tests/resources/xlsx/sheet_with_different_numeric_value_times.xlsx diff --git a/src/Spout/Reader/XLSX/Helper/CellValueFormatter.php b/src/Spout/Reader/XLSX/Helper/CellValueFormatter.php index e63384d..046336a 100644 --- a/src/Spout/Reader/XLSX/Helper/CellValueFormatter.php +++ b/src/Spout/Reader/XLSX/Helper/CellValueFormatter.php @@ -29,6 +29,8 @@ class CellValueFormatter /** Constants used for date formatting */ const NUM_SECONDS_IN_ONE_DAY = 86400; + const NUM_SECONDS_IN_ONE_HOUR = 3600; + const NUM_SECONDS_IN_ONE_MINUTE = 60; /** * February 29th, 1900 is NOT a leap year but Excel thinks it is... @@ -176,6 +178,7 @@ 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 can also represent a time, if it is a value between 0 and 1. * * @param float $nodeValue * @return \DateTime|null The value associated with the cell or NULL if invalid date value @@ -187,22 +190,59 @@ class CellValueFormatter --$nodeValue; } - // The value 1.0 represents 1900-01-01. Numbers below 1.0 are not valid Excel dates. - if ($nodeValue < 1.0) { + if ($nodeValue >= 1) { + // Values greater than 1 represent "dates". The value 1.0 representing the "base" date: 1900-01-01. + return $this->formatExcelTimestampValueAsDateValue($nodeValue); + } else if ($nodeValue >= 0) { + // Values between 0 and 1 represent "times". + return $this->formatExcelTimestampValueAsTimeValue($nodeValue); + } else { + // invalid date return null; } + } + /** + * 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). + * + * @param float $nodeValue + * @return \DateTime The value associated with the cell + */ + protected function formatExcelTimestampValueAsTimeValue($nodeValue) + { + $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); + + // using the base Excel date (Jan 1st, 1900) - not relevant here + $dateObj = new \DateTime('1900-01-01'); + $dateObj->setTime($hours, $minutes, $seconds); + + return $dateObj; + } + + /** + * 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 + * @return \DateTime|null The value associated with the cell or NULL if invalid date value + */ + protected function formatExcelTimestampValueAsDateValue($nodeValue) + { // 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 = \DateTime::createFromFormat('|Y-m-d', '1899-12-31'); - $cellValue->modify('+' . intval($nodeValue) . 'days'); - $cellValue->modify('+' . $secondsRemainder . 'seconds'); + $dateObj = \DateTime::createFromFormat('|Y-m-d', '1899-12-31'); + $dateObj->modify('+' . intval($nodeValue) . 'days'); + $dateObj->modify('+' . $secondsRemainder . 'seconds'); - return $cellValue; + return $dateObj; } catch (\Exception $e) { return null; } diff --git a/tests/Spout/Reader/XLSX/Helper/CellValueFormatterTest.php b/tests/Spout/Reader/XLSX/Helper/CellValueFormatterTest.php index 73863ae..6ea5b92 100644 --- a/tests/Spout/Reader/XLSX/Helper/CellValueFormatterTest.php +++ b/tests/Spout/Reader/XLSX/Helper/CellValueFormatterTest.php @@ -18,8 +18,11 @@ class CellValueFormatterTest extends \PHPUnit_Framework_TestCase [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, 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'], diff --git a/tests/Spout/Reader/XLSX/ReaderTest.php b/tests/Spout/Reader/XLSX/ReaderTest.php index 176f6d0..ee36266 100644 --- a/tests/Spout/Reader/XLSX/ReaderTest.php +++ b/tests/Spout/Reader/XLSX/ReaderTest.php @@ -181,6 +181,28 @@ class ReaderTest extends \PHPUnit_Framework_TestCase $this->assertEquals($expectedRows, $allRows); } + /** + * @return void + */ + public function testReadShouldSupportDifferentTimesAsNumericTimestamp() + { + // make sure dates are always created with the same timezone + date_default_timezone_set('UTC'); + + $allRows = $this->getAllRowsForFile('sheet_with_different_numeric_value_times.xlsx'); + + $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'), + ] + ]; + $this->assertEquals($expectedRows, $allRows); + } + /** * @return void */ diff --git a/tests/resources/xlsx/sheet_with_different_numeric_value_times.xlsx b/tests/resources/xlsx/sheet_with_different_numeric_value_times.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..e3ab29ee877adb8fe0cb91d6b8b2e3decfe18257 GIT binary patch literal 28226 zcmeFZcU;p+_b>i|Ac&Mulny~bQE4hoF(fKV5h;R#fRII`Nr_Se1cK5*L`7jm1r-n# z5DP^Sh;$K=4gx|*P$_~W(mp~+eiOGm`|R$$`#k&I`}^nK_%cjp&gaaWGv}On&zVWA z%n&?lAzo-T1VQT|@ng&|2QCOg^FWX|w3^$|5bqc0<`;-N5_;M#z)6GP>!WywhkNra zh#S!Vzt{i88Yr}B_N(0{Qe`)Z-Cu2+csu^VgVk~sa*o1ti1&oc&j@KvR`9aLA!<~o zB2UIK2CVK%zV=4f$isW}y&LNN&!ip;`h3iNV&06A#D7z2*|tVCHdx|d*{YLIC#G9Z zKAw+~zO9~Kn9_kgbK$6f(GOJgu?O86Qlb-j+V7K3ZIKyS%;`WB-$?nvRbjWV?#;?Q z>bCms#j*p#yVmb)$qE_Mzkiv3ru4yn<($fbug=Gz6vWHYtHxixVm(%^7vEL(dUS}r zhN6(Ew92ib*ZS=7i1r}fjq9%`i%??M&k%FIr3$oYj-@x8_BIf9aw=GpTVNaKSF{JU z$Ha&0PR9WU0i`QL;R@}R@t5-5h;B+wq(nF-)kK;rt2)EcL|cVtyPwX;)fYpTgtv5s zw?@yJcp*<$FJaQ<^%wW^tlzqm=jqHPWRmvQF#&}1Z4GZ zE*bIWNR|+A$syoz0>CA4{%$@2ni`z<|L2PT7yI9DL@&H-Zc(>QBzDo{eNAS>Xo+Hs z)lEMU>yXv4h(ps}C&bvVg4b6W#2qlq6+3z^&tP56f&wS5PY}lc1hkZ&@)e7EizGdEwJ+0to#_LK&_aE_J{e7v26C8R-uhYe!OTV=}9%*Bw*^H zS=iWaC3fou@ogXDMb4}4mu-k4XWKfA14`yfpq;ia~Nh`oZ1Gv{WuefZ*D zel8)fOj0!L>Jn7vvw$4BDZR-nh%)@>M5)zWr`d?vs|*yb_j;3(WXeEr_VKXOLD_NYDP2i7##{I&dkU5T${_g_wZ%FSReUhv&b2)n@AcKD)z3f__F1E?*I5LT!i{W-2{um{OKE@8=A$aO|I}w~fsqO= zMv-{o*A$Vxs{6VEO|{c_EmwOlVx`I+Zj=mf*{y!)$2~W!)b=b_Un*+)csKnZ+Ty&3 z_h8#0LrQe5$##_RRlz%3PYXYsZoo-C^u`8S@4fQS`r!60IQ!wVPU=~w7>9=Z!L{O7gWd2%pD*(cY@ zcq-$nha%Q~ea}M(xo&MDIQ6aN-Rg^Hu{sR68&2k%SoQsIOOxMJo9T&D=mtgou1=wGpJIAwTk}P2NzWryjMCj3<&!JR^@Q z4>B)qn*DfgQ_GaetdFr*dRKPe^Uu`m8t>+t^JT(Xemd9Ogy-j&@;&QgrAF-kH)FfZXWD+>=8)hppmH�B`6fPWC13I!e@c)3H6xjD2IJi8j= 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 IK!5%AKM{|qp#T5? literal 0 HcmV?d00001 From 2d923c7e46820a5a764a57096c603b1997fc70cc Mon Sep 17 00:00:00 2001 From: madflow Date: Fri, 20 May 2016 18:32:47 +0200 Subject: [PATCH 5/7] Fix issue #218 (#222) --- .../Reader/ODS/Helper/CellValueFormatter.php | 3 +++ tests/Spout/Reader/ODS/ReaderTest.php | 17 +++++++++++++++++ tests/resources/ods/sheet_with_hyperlinks.ods | Bin 0 -> 9905 bytes 3 files changed, 20 insertions(+) create mode 100644 tests/resources/ods/sheet_with_hyperlinks.ods diff --git a/src/Spout/Reader/ODS/Helper/CellValueFormatter.php b/src/Spout/Reader/ODS/Helper/CellValueFormatter.php index bd21576..3eb1918 100644 --- a/src/Spout/Reader/ODS/Helper/CellValueFormatter.php +++ b/src/Spout/Reader/ODS/Helper/CellValueFormatter.php @@ -23,6 +23,7 @@ class CellValueFormatter /** Definition of XML nodes names used to parse data */ const XML_NODE_P = 'p'; const XML_NODE_S = 'text:s'; + const XML_NODE_A = 'text:a'; /** Definition of XML attribute used to parse data */ const XML_ATTRIBUTE_TYPE = 'office:value-type'; @@ -98,6 +99,8 @@ class CellValueFormatter $spaceAttribute = $childNode->getAttribute(self::XML_ATTRIBUTE_C); $numSpaces = (!empty($spaceAttribute)) ? intval($spaceAttribute) : 1; $currentPValue .= str_repeat(' ', $numSpaces); + } else if ($childNode->nodeName === self::XML_NODE_A) { + $currentPValue .= $childNode->nodeValue; } } diff --git a/tests/Spout/Reader/ODS/ReaderTest.php b/tests/Spout/Reader/ODS/ReaderTest.php index 8683459..4c95fd9 100644 --- a/tests/Spout/Reader/ODS/ReaderTest.php +++ b/tests/Spout/Reader/ODS/ReaderTest.php @@ -436,6 +436,23 @@ class ReaderTest extends \PHPUnit_Framework_TestCase $this->assertEquals($expectedRows, $allRows, 'Cell values should not be trimmed'); } + /** + * https://github.com/box/spout/issues/218 + * @return void + */ + public function testReaderShouldReadTextInHyperlinks() + { + $allRows = $this->getAllRowsForFile('sheet_with_hyperlinks.ods'); + + $expectedRows = [ + ['email', 'text'], + ['1@example.com', 'text'], + ['2@example.com', 'text and https://github.com/box/spout/issues/218 and text'], + ]; + + $this->assertEquals($expectedRows, $allRows, 'Text in hyperlinks should be read'); + } + /** * @param string $fileName * @return array All the read rows the given file diff --git a/tests/resources/ods/sheet_with_hyperlinks.ods b/tests/resources/ods/sheet_with_hyperlinks.ods new file mode 100644 index 0000000000000000000000000000000000000000..7246db5580e7ceb2f85e502efa60baa0d53a6ec9 GIT binary patch literal 9905 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 Date: Fri, 20 May 2016 16:00:22 -0700 Subject: [PATCH 6/7] Pin PHPUnit version to keep support for PHP5.4 (#227) --- composer.json | 2 +- composer.lock | 815 +++++--------------------------------------------- 2 files changed, 75 insertions(+), 742 deletions(-) diff --git a/composer.json b/composer.json index 48f6eb8..e88e2c8 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,7 @@ "ext-simplexml": "*" }, "require-dev": { - "phpunit/phpunit": ">=3.7" + "phpunit/phpunit": "^4.8.0" }, "suggest": { "ext-iconv": "To handle non UTF-8 CSV files (if \"php-intl\" is not already installed or is too limited)", diff --git a/composer.lock b/composer.lock index ae8bb33..df7690a 100644 --- a/composer.lock +++ b/composer.lock @@ -4,77 +4,10 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "0866be323931eaa6e9431b3bbf0a817a", + "hash": "8957b9da742e28d7250c02fca8f9a5a7", + "content-hash": "973b8a4a1d8c520dd99fcd32cb5e022f", "packages": [], "packages-dev": [ - { - "name": "doctrine/annotations", - "version": "v1.2.6", - "source": { - "type": "git", - "url": "https://github.com/doctrine/annotations.git", - "reference": "f4a91702ca3cd2e568c3736aa031ed00c3752af4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/annotations/zipball/f4a91702ca3cd2e568c3736aa031ed00c3752af4", - "reference": "f4a91702ca3cd2e568c3736aa031ed00c3752af4", - "shasum": "" - }, - "require": { - "doctrine/lexer": "1.*", - "php": ">=5.3.2" - }, - "require-dev": { - "doctrine/cache": "1.*", - "phpunit/phpunit": "4.*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.3.x-dev" - } - }, - "autoload": { - "psr-0": { - "Doctrine\\Common\\Annotations\\": "lib/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Roman Borschel", - "email": "roman@code-factory.org" - }, - { - "name": "Benjamin Eberlei", - "email": "kontakt@beberlei.de" - }, - { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com" - }, - { - "name": "Jonathan Wage", - "email": "jonwage@gmail.com" - }, - { - "name": "Johannes Schmitt", - "email": "schmittjoh@gmail.com" - } - ], - "description": "Docblock Annotations Parser", - "homepage": "http://www.doctrine-project.org", - "keywords": [ - "annotations", - "docblock", - "parser" - ], - "time": "2015-06-17 12:21:22" - }, { "name": "doctrine/instantiator", "version": "1.0.5", @@ -129,362 +62,6 @@ ], "time": "2015-06-14 21:17:01" }, - { - "name": "doctrine/lexer", - "version": "v1.0.1", - "source": { - "type": "git", - "url": "https://github.com/doctrine/lexer.git", - "reference": "83893c552fd2045dd78aef794c31e694c37c0b8c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/lexer/zipball/83893c552fd2045dd78aef794c31e694c37c0b8c", - "reference": "83893c552fd2045dd78aef794c31e694c37c0b8c", - "shasum": "" - }, - "require": { - "php": ">=5.3.2" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-0": { - "Doctrine\\Common\\Lexer\\": "lib/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Roman Borschel", - "email": "roman@code-factory.org" - }, - { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com" - }, - { - "name": "Johannes Schmitt", - "email": "schmittjoh@gmail.com" - } - ], - "description": "Base library for a lexer that can be used in Top-Down, Recursive Descent Parsers.", - "homepage": "http://www.doctrine-project.org", - "keywords": [ - "lexer", - "parser" - ], - "time": "2014-09-09 13:34:57" - }, - { - "name": "guzzle/guzzle", - "version": "v3.9.3", - "source": { - "type": "git", - "url": "https://github.com/guzzle/guzzle3.git", - "reference": "0645b70d953bc1c067bbc8d5bc53194706b628d9" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle3/zipball/0645b70d953bc1c067bbc8d5bc53194706b628d9", - "reference": "0645b70d953bc1c067bbc8d5bc53194706b628d9", - "shasum": "" - }, - "require": { - "ext-curl": "*", - "php": ">=5.3.3", - "symfony/event-dispatcher": "~2.1" - }, - "replace": { - "guzzle/batch": "self.version", - "guzzle/cache": "self.version", - "guzzle/common": "self.version", - "guzzle/http": "self.version", - "guzzle/inflection": "self.version", - "guzzle/iterator": "self.version", - "guzzle/log": "self.version", - "guzzle/parser": "self.version", - "guzzle/plugin": "self.version", - "guzzle/plugin-async": "self.version", - "guzzle/plugin-backoff": "self.version", - "guzzle/plugin-cache": "self.version", - "guzzle/plugin-cookie": "self.version", - "guzzle/plugin-curlauth": "self.version", - "guzzle/plugin-error-response": "self.version", - "guzzle/plugin-history": "self.version", - "guzzle/plugin-log": "self.version", - "guzzle/plugin-md5": "self.version", - "guzzle/plugin-mock": "self.version", - "guzzle/plugin-oauth": "self.version", - "guzzle/service": "self.version", - "guzzle/stream": "self.version" - }, - "require-dev": { - "doctrine/cache": "~1.3", - "monolog/monolog": "~1.0", - "phpunit/phpunit": "3.7.*", - "psr/log": "~1.0", - "symfony/class-loader": "~2.1", - "zendframework/zend-cache": "2.*,<2.3", - "zendframework/zend-log": "2.*,<2.3" - }, - "suggest": { - "guzzlehttp/guzzle": "Guzzle 5 has moved to a new package name. The package you have installed, Guzzle 3, is deprecated." - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.9-dev" - } - }, - "autoload": { - "psr-0": { - "Guzzle": "src/", - "Guzzle\\Tests": "tests/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Michael Dowling", - "email": "mtdowling@gmail.com", - "homepage": "https://github.com/mtdowling" - }, - { - "name": "Guzzle Community", - "homepage": "https://github.com/guzzle/guzzle/contributors" - } - ], - "description": "PHP HTTP client. This library is deprecated in favor of https://packagist.org/packages/guzzlehttp/guzzle", - "homepage": "http://guzzlephp.org/", - "keywords": [ - "client", - "curl", - "framework", - "http", - "http client", - "rest", - "web service" - ], - "time": "2015-03-18 18:23:50" - }, - { - "name": "jms/metadata", - "version": "1.5.1", - "source": { - "type": "git", - "url": "https://github.com/schmittjoh/metadata.git", - "reference": "22b72455559a25777cfd28c4ffda81ff7639f353" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/metadata/zipball/22b72455559a25777cfd28c4ffda81ff7639f353", - "reference": "22b72455559a25777cfd28c4ffda81ff7639f353", - "shasum": "" - }, - "require": { - "php": ">=5.3.0" - }, - "require-dev": { - "doctrine/cache": "~1.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.5.x-dev" - } - }, - "autoload": { - "psr-0": { - "Metadata\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "Apache" - ], - "authors": [ - { - "name": "Johannes Schmitt", - "email": "schmittjoh@gmail.com", - "homepage": "https://github.com/schmittjoh", - "role": "Developer of wrapped JMSSerializerBundle" - } - ], - "description": "Class/method/property metadata management in PHP", - "keywords": [ - "annotations", - "metadata", - "xml", - "yaml" - ], - "time": "2014-07-12 07:13:19" - }, - { - "name": "jms/parser-lib", - "version": "1.0.0", - "source": { - "type": "git", - "url": "https://github.com/schmittjoh/parser-lib.git", - "reference": "c509473bc1b4866415627af0e1c6cc8ac97fa51d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/parser-lib/zipball/c509473bc1b4866415627af0e1c6cc8ac97fa51d", - "reference": "c509473bc1b4866415627af0e1c6cc8ac97fa51d", - "shasum": "" - }, - "require": { - "phpoption/phpoption": ">=0.9,<2.0-dev" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, - "autoload": { - "psr-0": { - "JMS\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "Apache2" - ], - "description": "A library for easily creating recursive-descent parsers.", - "time": "2012-11-18 18:08:43" - }, - { - "name": "jms/serializer", - "version": "0.16.0", - "source": { - "type": "git", - "url": "https://github.com/schmittjoh/serializer.git", - "reference": "c8a171357ca92b6706e395c757f334902d430ea9" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/serializer/zipball/c8a171357ca92b6706e395c757f334902d430ea9", - "reference": "c8a171357ca92b6706e395c757f334902d430ea9", - "shasum": "" - }, - "require": { - "doctrine/annotations": "1.*", - "jms/metadata": "~1.1", - "jms/parser-lib": "1.*", - "php": ">=5.3.2", - "phpcollection/phpcollection": "~0.1" - }, - "require-dev": { - "doctrine/orm": "~2.1", - "doctrine/phpcr-odm": "~1.0.1", - "jackalope/jackalope-doctrine-dbal": "1.0.*", - "propel/propel1": "~1.7", - "symfony/filesystem": "2.*", - "symfony/form": "~2.1", - "symfony/translation": "~2.0", - "symfony/validator": "~2.0", - "symfony/yaml": "2.*", - "twig/twig": ">=1.8,<2.0-dev" - }, - "suggest": { - "symfony/yaml": "Required if you'd like to serialize data to YAML format." - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "0.15-dev" - } - }, - "autoload": { - "psr-0": { - "JMS\\Serializer": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "Apache2" - ], - "authors": [ - { - "name": "Johannes Schmitt", - "email": "schmittjoh@gmail.com", - "homepage": "https://github.com/schmittjoh", - "role": "Developer of wrapped JMSSerializerBundle" - } - ], - "description": "Library for (de-)serializing data of any complexity; supports XML, JSON, and YAML.", - "homepage": "http://jmsyst.com/libs/serializer", - "keywords": [ - "deserialization", - "jaxb", - "json", - "serialization", - "xml" - ], - "time": "2014-03-18 08:39:00" - }, - { - "name": "phpcollection/phpcollection", - "version": "0.4.0", - "source": { - "type": "git", - "url": "https://github.com/schmittjoh/php-collection.git", - "reference": "b8bf55a0a929ca43b01232b36719f176f86c7e83" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/php-collection/zipball/b8bf55a0a929ca43b01232b36719f176f86c7e83", - "reference": "b8bf55a0a929ca43b01232b36719f176f86c7e83", - "shasum": "" - }, - "require": { - "phpoption/phpoption": "1.*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "0.3-dev" - } - }, - "autoload": { - "psr-0": { - "PhpCollection": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "Apache2" - ], - "authors": [ - { - "name": "Johannes Schmitt", - "email": "schmittjoh@gmail.com", - "homepage": "https://github.com/schmittjoh", - "role": "Developer of wrapped JMSSerializerBundle" - } - ], - "description": "General-Purpose Collection Library for PHP", - "keywords": [ - "collection", - "list", - "map", - "sequence", - "set" - ], - "time": "2014-03-11 13:46:42" - }, { "name": "phpdocumentor/reflection-docblock", "version": "2.0.4", @@ -534,73 +111,26 @@ ], "time": "2015-02-03 12:10:50" }, - { - "name": "phpoption/phpoption", - "version": "1.4.0", - "source": { - "type": "git", - "url": "https://github.com/schmittjoh/php-option.git", - "reference": "5d099bcf0393908bf4ad69cc47dafb785d51f7f5" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/5d099bcf0393908bf4ad69cc47dafb785d51f7f5", - "reference": "5d099bcf0393908bf4ad69cc47dafb785d51f7f5", - "shasum": "" - }, - "require": { - "php": ">=5.3.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.3-dev" - } - }, - "autoload": { - "psr-0": { - "PhpOption\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "Apache2" - ], - "authors": [ - { - "name": "Johannes Schmitt", - "email": "schmittjoh@gmail.com", - "homepage": "https://github.com/schmittjoh", - "role": "Developer of wrapped JMSSerializerBundle" - } - ], - "description": "Option Type for PHP", - "keywords": [ - "language", - "option", - "php", - "type" - ], - "time": "2014-01-09 22:37:17" - }, { "name": "phpspec/prophecy", - "version": "v1.5.0", + "version": "v1.6.0", "source": { "type": "git", "url": "https://github.com/phpspec/prophecy.git", - "reference": "4745ded9307786b730d7a60df5cb5a6c43cf95f7" + "reference": "3c91bdf81797d725b14cb62906f9a4ce44235972" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/4745ded9307786b730d7a60df5cb5a6c43cf95f7", - "reference": "4745ded9307786b730d7a60df5cb5a6c43cf95f7", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/3c91bdf81797d725b14cb62906f9a4ce44235972", + "reference": "3c91bdf81797d725b14cb62906f9a4ce44235972", "shasum": "" }, "require": { "doctrine/instantiator": "^1.0.2", + "php": "^5.3|^7.0", "phpdocumentor/reflection-docblock": "~2.0", - "sebastian/comparator": "~1.1" + "sebastian/comparator": "~1.1", + "sebastian/recursion-context": "~1.0" }, "require-dev": { "phpspec/phpspec": "~2.0" @@ -608,7 +138,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.4.x-dev" + "dev-master": "1.5.x-dev" } }, "autoload": { @@ -641,20 +171,20 @@ "spy", "stub" ], - "time": "2015-08-13 10:07:40" + "time": "2016-02-15 07:46:21" }, { "name": "phpunit/php-code-coverage", - "version": "2.2.2", + "version": "2.2.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "2d7c03c0e4e080901b8f33b2897b0577be18a13c" + "reference": "eabf68b476ac7d0f73793aada060f1c1a9bf8979" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2d7c03c0e4e080901b8f33b2897b0577be18a13c", - "reference": "2d7c03c0e4e080901b8f33b2897b0577be18a13c", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/eabf68b476ac7d0f73793aada060f1c1a9bf8979", + "reference": "eabf68b476ac7d0f73793aada060f1c1a9bf8979", "shasum": "" }, "require": { @@ -703,7 +233,7 @@ "testing", "xunit" ], - "time": "2015-08-04 03:42:39" + "time": "2015-10-06 15:47:00" }, { "name": "phpunit/php-file-iterator", @@ -795,21 +325,24 @@ }, { "name": "phpunit/php-timer", - "version": "1.0.7", + "version": "1.0.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "3e82f4e9fc92665fafd9157568e4dcb01d014e5b" + "reference": "38e9124049cf1a164f1e4537caf19c99bf1eb260" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3e82f4e9fc92665fafd9157568e4dcb01d014e5b", - "reference": "3e82f4e9fc92665fafd9157568e4dcb01d014e5b", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/38e9124049cf1a164f1e4537caf19c99bf1eb260", + "reference": "38e9124049cf1a164f1e4537caf19c99bf1eb260", "shasum": "" }, "require": { "php": ">=5.3.3" }, + "require-dev": { + "phpunit/phpunit": "~4|~5" + }, "type": "library", "autoload": { "classmap": [ @@ -832,20 +365,20 @@ "keywords": [ "timer" ], - "time": "2015-06-21 08:01:12" + "time": "2016-05-12 18:03:57" }, { "name": "phpunit/php-token-stream", - "version": "1.4.6", + "version": "1.4.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-token-stream.git", - "reference": "3ab72c62e550370a6cd5dc873e1a04ab57562f5b" + "reference": "3144ae21711fb6cac0b1ab4cbe63b75ce3d4e8da" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/3ab72c62e550370a6cd5dc873e1a04ab57562f5b", - "reference": "3ab72c62e550370a6cd5dc873e1a04ab57562f5b", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/3144ae21711fb6cac0b1ab4cbe63b75ce3d4e8da", + "reference": "3144ae21711fb6cac0b1ab4cbe63b75ce3d4e8da", "shasum": "" }, "require": { @@ -881,20 +414,20 @@ "keywords": [ "tokenizer" ], - "time": "2015-08-16 08:51:00" + "time": "2015-09-15 10:49:45" }, { "name": "phpunit/phpunit", - "version": "4.8.5", + "version": "4.8.26", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "9b7417edaf28059ea63d86be941e6004dbfcc0cc" + "reference": "fc1d8cd5b5de11625979125c5639347896ac2c74" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/9b7417edaf28059ea63d86be941e6004dbfcc0cc", - "reference": "9b7417edaf28059ea63d86be941e6004dbfcc0cc", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/fc1d8cd5b5de11625979125c5639347896ac2c74", + "reference": "fc1d8cd5b5de11625979125c5639347896ac2c74", "shasum": "" }, "require": { @@ -908,7 +441,7 @@ "phpunit/php-code-coverage": "~2.1", "phpunit/php-file-iterator": "~1.4", "phpunit/php-text-template": "~1.2", - "phpunit/php-timer": ">=1.0.6", + "phpunit/php-timer": "^1.0.6", "phpunit/phpunit-mock-objects": "~2.3", "sebastian/comparator": "~1.1", "sebastian/diff": "~1.2", @@ -953,20 +486,20 @@ "testing", "xunit" ], - "time": "2015-08-19 09:20:57" + "time": "2016-05-17 03:09:28" }, { "name": "phpunit/phpunit-mock-objects", - "version": "2.3.7", + "version": "2.3.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", - "reference": "5e2645ad49d196e020b85598d7c97e482725786a" + "reference": "ac8e7a3db35738d56ee9a76e78a4e03d97628983" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/5e2645ad49d196e020b85598d7c97e482725786a", - "reference": "5e2645ad49d196e020b85598d7c97e482725786a", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/ac8e7a3db35738d56ee9a76e78a4e03d97628983", + "reference": "ac8e7a3db35738d56ee9a76e78a4e03d97628983", "shasum": "" }, "require": { @@ -1009,43 +542,7 @@ "mock", "xunit" ], - "time": "2015-08-19 09:14:08" - }, - { - "name": "scrutinizer/ocular", - "version": "1.1.1", - "source": { - "type": "git", - "url": "https://github.com/scrutinizer-ci/ocular.git", - "reference": "8e0a8c7f085bc4857bd52132833679dcfd504fc1" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/scrutinizer-ci/ocular/zipball/8e0a8c7f085bc4857bd52132833679dcfd504fc1", - "reference": "8e0a8c7f085bc4857bd52132833679dcfd504fc1", - "shasum": "" - }, - "require": { - "guzzle/guzzle": "~3.0", - "jms/serializer": "~0.13", - "phpoption/phpoption": "~1.0", - "symfony/console": "~2.0", - "symfony/process": "~2.3" - }, - "require-dev": { - "symfony/filesystem": "~2.0" - }, - "bin": [ - "bin/ocular" - ], - "type": "library", - "autoload": { - "psr-0": { - "Scrutinizer\\Ocular\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "time": "2014-04-12 20:46:35" + "time": "2015-10-02 06:51:40" }, { "name": "sebastian/comparator", @@ -1113,28 +610,28 @@ }, { "name": "sebastian/diff", - "version": "1.3.0", + "version": "1.4.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "863df9687835c62aa423a22412d26fa2ebde3fd3" + "reference": "13edfd8706462032c2f52b4b862974dd46b71c9e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/863df9687835c62aa423a22412d26fa2ebde3fd3", - "reference": "863df9687835c62aa423a22412d26fa2ebde3fd3", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/13edfd8706462032c2f52b4b862974dd46b71c9e", + "reference": "13edfd8706462032c2f52b4b862974dd46b71c9e", "shasum": "" }, "require": { "php": ">=5.3.3" }, "require-dev": { - "phpunit/phpunit": "~4.2" + "phpunit/phpunit": "~4.8" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.3-dev" + "dev-master": "1.4-dev" } }, "autoload": { @@ -1157,24 +654,24 @@ } ], "description": "Diff implementation", - "homepage": "http://www.github.com/sebastianbergmann/diff", + "homepage": "https://github.com/sebastianbergmann/diff", "keywords": [ "diff" ], - "time": "2015-02-22 15:13:53" + "time": "2015-12-08 07:14:41" }, { "name": "sebastian/environment", - "version": "1.3.2", + "version": "1.3.7", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "6324c907ce7a52478eeeaede764f48733ef5ae44" + "reference": "4e8f0da10ac5802913afc151413bc8c53b6c2716" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/6324c907ce7a52478eeeaede764f48733ef5ae44", - "reference": "6324c907ce7a52478eeeaede764f48733ef5ae44", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/4e8f0da10ac5802913afc151413bc8c53b6c2716", + "reference": "4e8f0da10ac5802913afc151413bc8c53b6c2716", "shasum": "" }, "require": { @@ -1211,7 +708,7 @@ "environment", "hhvm" ], - "time": "2015-08-03 06:14:51" + "time": "2016-05-17 03:18:57" }, { "name": "sebastian/exporter", @@ -1281,16 +778,16 @@ }, { "name": "sebastian/global-state", - "version": "1.0.0", + "version": "1.1.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "c7428acdb62ece0a45e6306f1ae85e1c05b09c01" + "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/c7428acdb62ece0a45e6306f1ae85e1c05b09c01", - "reference": "c7428acdb62ece0a45e6306f1ae85e1c05b09c01", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bc37d50fea7d017d3d340f230811c9f1d7280af4", + "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4", "shasum": "" }, "require": { @@ -1328,20 +825,20 @@ "keywords": [ "global state" ], - "time": "2014-10-06 09:23:50" + "time": "2015-10-12 03:26:01" }, { "name": "sebastian/recursion-context", - "version": "1.0.1", + "version": "1.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "994d4a811bafe801fb06dccbee797863ba2792ba" + "reference": "913401df809e99e4f47b27cdd781f4a258d58791" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/994d4a811bafe801fb06dccbee797863ba2792ba", - "reference": "994d4a811bafe801fb06dccbee797863ba2792ba", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/913401df809e99e4f47b27cdd781f4a258d58791", + "reference": "913401df809e99e4f47b27cdd781f4a258d58791", "shasum": "" }, "require": { @@ -1381,7 +878,7 @@ ], "description": "Provides functionality to recursively process PHP variables", "homepage": "http://www.github.com/sebastianbergmann/recursion-context", - "time": "2015-06-21 08:04:50" + "time": "2015-11-11 19:50:13" }, { "name": "sebastian/version", @@ -1418,200 +915,36 @@ "homepage": "https://github.com/sebastianbergmann/version", "time": "2015-06-21 13:59:46" }, - { - "name": "symfony/console", - "version": "v2.7.3", - "source": { - "type": "git", - "url": "https://github.com/symfony/Console.git", - "reference": "d6cf02fe73634c96677e428f840704bfbcaec29e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/Console/zipball/d6cf02fe73634c96677e428f840704bfbcaec29e", - "reference": "d6cf02fe73634c96677e428f840704bfbcaec29e", - "shasum": "" - }, - "require": { - "php": ">=5.3.9" - }, - "require-dev": { - "psr/log": "~1.0", - "symfony/event-dispatcher": "~2.1", - "symfony/phpunit-bridge": "~2.7", - "symfony/process": "~2.1" - }, - "suggest": { - "psr/log": "For using the console logger", - "symfony/event-dispatcher": "", - "symfony/process": "" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.7-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\Console\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony Console Component", - "homepage": "https://symfony.com", - "time": "2015-07-28 15:18:12" - }, - { - "name": "symfony/event-dispatcher", - "version": "v2.7.3", - "source": { - "type": "git", - "url": "https://github.com/symfony/EventDispatcher.git", - "reference": "9310b5f9a87ec2ea75d20fec0b0017c77c66dac3" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/EventDispatcher/zipball/9310b5f9a87ec2ea75d20fec0b0017c77c66dac3", - "reference": "9310b5f9a87ec2ea75d20fec0b0017c77c66dac3", - "shasum": "" - }, - "require": { - "php": ">=5.3.9" - }, - "require-dev": { - "psr/log": "~1.0", - "symfony/config": "~2.0,>=2.0.5", - "symfony/dependency-injection": "~2.6", - "symfony/expression-language": "~2.6", - "symfony/phpunit-bridge": "~2.7", - "symfony/stopwatch": "~2.3" - }, - "suggest": { - "symfony/dependency-injection": "", - "symfony/http-kernel": "" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.7-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\EventDispatcher\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony EventDispatcher Component", - "homepage": "https://symfony.com", - "time": "2015-06-18 19:21:56" - }, - { - "name": "symfony/process", - "version": "v2.7.3", - "source": { - "type": "git", - "url": "https://github.com/symfony/Process.git", - "reference": "48aeb0e48600321c272955132d7606ab0a49adb3" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/Process/zipball/48aeb0e48600321c272955132d7606ab0a49adb3", - "reference": "48aeb0e48600321c272955132d7606ab0a49adb3", - "shasum": "" - }, - "require": { - "php": ">=5.3.9" - }, - "require-dev": { - "symfony/phpunit-bridge": "~2.7" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.7-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\Process\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony Process Component", - "homepage": "https://symfony.com", - "time": "2015-07-01 11:25:50" - }, { "name": "symfony/yaml", - "version": "v2.7.3", + "version": "v2.8.6", "source": { "type": "git", - "url": "https://github.com/symfony/Yaml.git", - "reference": "71340e996171474a53f3d29111d046be4ad8a0ff" + "url": "https://github.com/symfony/yaml.git", + "reference": "e4fbcc65f90909c999ac3b4dfa699ee6563a9940" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/Yaml/zipball/71340e996171474a53f3d29111d046be4ad8a0ff", - "reference": "71340e996171474a53f3d29111d046be4ad8a0ff", + "url": "https://api.github.com/repos/symfony/yaml/zipball/e4fbcc65f90909c999ac3b4dfa699ee6563a9940", + "reference": "e4fbcc65f90909c999ac3b4dfa699ee6563a9940", "shasum": "" }, "require": { "php": ">=5.3.9" }, - "require-dev": { - "symfony/phpunit-bridge": "~2.7" - }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.7-dev" + "dev-master": "2.8-dev" } }, "autoload": { "psr-4": { "Symfony\\Component\\Yaml\\": "" - } + }, + "exclude-from-classmap": [ + "/Tests/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -1629,7 +962,7 @@ ], "description": "Symfony Yaml Component", "homepage": "https://symfony.com", - "time": "2015-07-28 14:07:07" + "time": "2016-03-29 19:00:15" } ], "aliases": [], From 104cd9b8114659751d29f34376851bc8c36f1bdc Mon Sep 17 00:00:00 2001 From: Adrien Loison Date: Fri, 20 May 2016 16:08:35 -0700 Subject: [PATCH 7/7] Option to return formatted dates instead of PHP objects (#226) When reading spreadsheets, Spout should be able to return formatted dates, as shown when opened with Excel for instance. It currently only returns DateTime/DateInterval objects, making it impossible to read + write, as the Writer does not accept objects. --- README.md | 17 ++- src/Spout/Reader/AbstractReader.php | 17 ++- .../Reader/ODS/Helper/CellValueFormatter.php | 58 +++++++-- src/Spout/Reader/ODS/Reader.php | 2 +- src/Spout/Reader/ODS/RowIterator.php | 7 +- src/Spout/Reader/ODS/Sheet.php | 5 +- src/Spout/Reader/ODS/SheetIterator.php | 9 +- .../Reader/XLSX/Helper/CellValueFormatter.php | 42 ++++-- .../Reader/XLSX/Helper/DateFormatHelper.php | 122 ++++++++++++++++++ src/Spout/Reader/XLSX/Helper/SheetHelper.php | 9 +- src/Spout/Reader/XLSX/Helper/StyleHelper.php | 44 ++++++- src/Spout/Reader/XLSX/Reader.php | 2 +- src/Spout/Reader/XLSX/RowIterator.php | 5 +- src/Spout/Reader/XLSX/Sheet.php | 5 +- src/Spout/Reader/XLSX/SheetIterator.php | 5 +- tests/Spout/Reader/ODS/ReaderTest.php | 19 ++- .../XLSX/Helper/CellValueFormatterTest.php | 6 +- .../XLSX/Helper/DateFormatHelperTest.php | 47 +++++++ tests/Spout/Reader/XLSX/ReaderTest.php | 19 ++- .../ods/sheet_with_dates_and_times.ods | Bin 0 -> 9988 bytes .../xlsx/sheet_with_dates_and_times.xlsx | Bin 0 -> 22453 bytes 21 files changed, 389 insertions(+), 51 deletions(-) create mode 100644 src/Spout/Reader/XLSX/Helper/DateFormatHelper.php create mode 100644 tests/Spout/Reader/XLSX/Helper/DateFormatHelperTest.php create mode 100644 tests/resources/ods/sheet_with_dates_and_times.ods create mode 100644 tests/resources/xlsx/sheet_with_dates_and_times.xlsx diff --git a/README.md b/README.md index 6bb73aa..a34c365 100644 --- a/README.md +++ b/README.md @@ -121,7 +121,7 @@ $reader->setEncoding('UTF-16LE'); The writer always generate CSV files encoded in UTF-8, with a BOM. -### Configuring the XLSX and ODS writers +### Configuring the XLSX and ODS readers and writers #### Row styling @@ -163,7 +163,6 @@ Font | Bold | `StyleBuilder::setFontBold()` | Font color | `StyleBuilder::setFontColor(Color::BLUE)`
`StyleBuilder::setFontColor(Color::rgb(0, 128, 255))` Alignment | Wrap text | `StyleBuilder::setShouldWrapText()` - #### New sheet creation It is also possible to change the behavior of the writer when the maximum number of rows (1,048,576) have been written in the current sheet: @@ -208,6 +207,20 @@ $writer->setShouldUseInlineStrings(false); // will use shared strings > Apple's products (Numbers and the iOS previewer) don't support inline strings and display empty cells instead. Therefore, if these platforms need to be supported, make sure to use shared strings! +#### Date/Time formatting + +When reading a spreadsheet containing dates or times, Spout returns the values by default as DateTime objects. +It is possible to change this behavior and have a formatted date returned instead (e.g. "2016-11-29 1:22 AM"). The format of the date corresponds to what is specified in the spreadsheet. + +```php +use Box\Spout\Reader\ReaderFactory; +use Box\Spout\Common\Type; + +$reader = ReaderFactory::create(Type::XLSX); +$reader->setShouldFormatDates(false); // default value +$reader->setShouldFormatDates(true); // will return formatted dates +``` + ### Playing with sheets When creating a XLSX or ODS file, it is possible to control which sheet the data will be written into. At any time, you can retrieve or set the current sheet: diff --git a/src/Spout/Reader/AbstractReader.php b/src/Spout/Reader/AbstractReader.php index d6d38e2..cb476ab 100644 --- a/src/Spout/Reader/AbstractReader.php +++ b/src/Spout/Reader/AbstractReader.php @@ -19,6 +19,9 @@ abstract class AbstractReader implements ReaderInterface /** @var \Box\Spout\Common\Helper\GlobalFunctionsHelper Helper to work with global functions */ protected $globalFunctionsHelper; + /** @var bool Whether date/time values should be returned as PHP objects or be formatted as strings */ + protected $shouldFormatDates = false; + /** * Returns whether stream wrappers are supported * @@ -49,7 +52,7 @@ abstract class AbstractReader implements ReaderInterface abstract protected function closeReader(); /** - * @param $globalFunctionsHelper + * @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper * @return AbstractReader */ public function setGlobalFunctionsHelper($globalFunctionsHelper) @@ -58,6 +61,18 @@ abstract class AbstractReader implements ReaderInterface return $this; } + /** + * Sets whether date/time values should be returned as PHP objects or be formatted as strings. + * + * @param bool $shouldFormatDates + * @return AbstractReader + */ + public function setShouldFormatDates($shouldFormatDates) + { + $this->shouldFormatDates = $shouldFormatDates; + return $this; + } + /** * Prepares the reader to read the given file. It also makes sure * that the file exists and is readable. diff --git a/src/Spout/Reader/ODS/Helper/CellValueFormatter.php b/src/Spout/Reader/ODS/Helper/CellValueFormatter.php index 3eb1918..b39af21 100644 --- a/src/Spout/Reader/ODS/Helper/CellValueFormatter.php +++ b/src/Spout/Reader/ODS/Helper/CellValueFormatter.php @@ -34,14 +34,19 @@ class CellValueFormatter const XML_ATTRIBUTE_CURRENCY = 'office:currency'; const XML_ATTRIBUTE_C = 'text:c'; + /** @var bool Whether date/time values should be returned as PHP objects or be formatted as strings */ + protected $shouldFormatDates; + /** @var \Box\Spout\Common\Escaper\ODS Used to unescape XML data */ protected $escaper; /** - * + * @param bool $shouldFormatDates Whether date/time values should be returned as PHP objects or be formatted as strings */ - public function __construct() + public function __construct($shouldFormatDates) { + $this->shouldFormatDates = $shouldFormatDates; + /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */ $this->escaper = new \Box\Spout\Common\Escaper\ODS(); } @@ -122,6 +127,7 @@ class CellValueFormatter { $nodeValue = $node->getAttribute(self::XML_ATTRIBUTE_VALUE); $nodeIntValue = intval($nodeValue); + // The "==" is intentionally not a "===" because only the value matters, not the type $cellValue = ($nodeIntValue == $nodeValue) ? $nodeIntValue : floatval($nodeValue); return $cellValue; } @@ -144,15 +150,27 @@ class CellValueFormatter * Returns the cell Date value from the given node. * * @param \DOMNode $node - * @return \DateTime|null The value associated with the cell or NULL if invalid date value + * @return \DateTime|string|null The value associated with the cell or NULL if invalid date value */ protected function formatDateCellValue($node) { - try { - $nodeValue = $node->getAttribute(self::XML_ATTRIBUTE_DATE_VALUE); - return new \DateTime($nodeValue); - } catch (\Exception $e) { - return null; + // The XML node looks like this: + // + // 05/19/16 04:39 PM + // + + if ($this->shouldFormatDates) { + // The date is already formatted in the "p" tag + $nodeWithValueAlreadyFormatted = $node->getElementsByTagName(self::XML_NODE_P)->item(0); + return $nodeWithValueAlreadyFormatted->nodeValue; + } else { + // otherwise, get it from the "date-value" attribute + try { + $nodeValue = $node->getAttribute(self::XML_ATTRIBUTE_DATE_VALUE); + return new \DateTime($nodeValue); + } catch (\Exception $e) { + return null; + } } } @@ -160,15 +178,27 @@ class CellValueFormatter * Returns the cell Time value from the given node. * * @param \DOMNode $node - * @return \DateInterval|null The value associated with the cell or NULL if invalid time value + * @return \DateInterval|string|null The value associated with the cell or NULL if invalid time value */ protected function formatTimeCellValue($node) { - try { - $nodeValue = $node->getAttribute(self::XML_ATTRIBUTE_TIME_VALUE); - return new \DateInterval($nodeValue); - } catch (\Exception $e) { - return null; + // The XML node looks like this: + // + // 01:24:00 PM + // + + if ($this->shouldFormatDates) { + // The date is already formatted in the "p" tag + $nodeWithValueAlreadyFormatted = $node->getElementsByTagName(self::XML_NODE_P)->item(0); + return $nodeWithValueAlreadyFormatted->nodeValue; + } else { + // otherwise, get it from the "time-value" attribute + try { + $nodeValue = $node->getAttribute(self::XML_ATTRIBUTE_TIME_VALUE); + return new \DateInterval($nodeValue); + } catch (\Exception $e) { + return null; + } } } diff --git a/src/Spout/Reader/ODS/Reader.php b/src/Spout/Reader/ODS/Reader.php index b4093ae..a52bafa 100644 --- a/src/Spout/Reader/ODS/Reader.php +++ b/src/Spout/Reader/ODS/Reader.php @@ -42,7 +42,7 @@ class Reader extends AbstractReader $this->zip = new \ZipArchive(); if ($this->zip->open($filePath) === true) { - $this->sheetIterator = new SheetIterator($filePath); + $this->sheetIterator = new SheetIterator($filePath, $this->shouldFormatDates); } else { throw new IOException("Could not open $filePath for reading."); } diff --git a/src/Spout/Reader/ODS/RowIterator.php b/src/Spout/Reader/ODS/RowIterator.php index aa7a496..e91ad90 100644 --- a/src/Spout/Reader/ODS/RowIterator.php +++ b/src/Spout/Reader/ODS/RowIterator.php @@ -45,11 +45,12 @@ class RowIterator implements IteratorInterface /** * @param XMLReader $xmlReader XML Reader, positioned on the "" element + * @param bool $shouldFormatDates Whether date/time values should be returned as PHP objects or be formatted as strings */ - public function __construct($xmlReader) + public function __construct($xmlReader, $shouldFormatDates) { $this->xmlReader = $xmlReader; - $this->cellValueFormatter = new CellValueFormatter(); + $this->cellValueFormatter = new CellValueFormatter($shouldFormatDates); } /** @@ -186,7 +187,7 @@ class RowIterator implements IteratorInterface /** * empty() replacement that honours 0 as a valid value * - * @param $value The cell value + * @param string|int|float|bool|\DateTime|\DateInterval|null $value The cell value * @return bool */ protected function isEmptyCellValue($value) diff --git a/src/Spout/Reader/ODS/Sheet.php b/src/Spout/Reader/ODS/Sheet.php index c78e4aa..98d00b1 100644 --- a/src/Spout/Reader/ODS/Sheet.php +++ b/src/Spout/Reader/ODS/Sheet.php @@ -27,12 +27,13 @@ class Sheet implements SheetInterface /** * @param XMLReader $xmlReader XML Reader, positioned on the "" element + * @param bool $shouldFormatDates Whether date/time values should be returned as PHP objects or be formatted as strings * @param int $sheetIndex Index of the sheet, based on order in the workbook (zero-based) * @param string $sheetName Name of the sheet */ - public function __construct($xmlReader, $sheetIndex, $sheetName) + public function __construct($xmlReader, $shouldFormatDates, $sheetIndex, $sheetName) { - $this->rowIterator = new RowIterator($xmlReader); + $this->rowIterator = new RowIterator($xmlReader, $shouldFormatDates); $this->index = $sheetIndex; $this->name = $sheetName; } diff --git a/src/Spout/Reader/ODS/SheetIterator.php b/src/Spout/Reader/ODS/SheetIterator.php index f8683f0..d0010bd 100644 --- a/src/Spout/Reader/ODS/SheetIterator.php +++ b/src/Spout/Reader/ODS/SheetIterator.php @@ -22,6 +22,9 @@ class SheetIterator implements IteratorInterface /** @var string $filePath Path of the file to be read */ protected $filePath; + /** @var bool Whether date/time values should be returned as PHP objects or be formatted as strings */ + protected $shouldFormatDates; + /** @var XMLReader The XMLReader object that will help read sheet's XML data */ protected $xmlReader; @@ -36,11 +39,13 @@ class SheetIterator implements IteratorInterface /** * @param string $filePath Path of the file to be read + * @param bool $shouldFormatDates Whether date/time values should be returned as PHP objects or be formatted as strings * @throws \Box\Spout\Reader\Exception\NoSheetsFoundException If there are no sheets in the file */ - public function __construct($filePath) + public function __construct($filePath, $shouldFormatDates) { $this->filePath = $filePath; + $this->shouldFormatDates = $shouldFormatDates; $this->xmlReader = new XMLReader(); /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */ @@ -109,7 +114,7 @@ class SheetIterator implements IteratorInterface $escapedSheetName = $this->xmlReader->getAttribute(self::XML_ATTRIBUTE_TABLE_NAME); $sheetName = $this->escaper->unescape($escapedSheetName); - return new Sheet($this->xmlReader, $sheetName, $this->currentSheetIndex); + return new Sheet($this->xmlReader, $this->shouldFormatDates, $sheetName, $this->currentSheetIndex); } /** diff --git a/src/Spout/Reader/XLSX/Helper/CellValueFormatter.php b/src/Spout/Reader/XLSX/Helper/CellValueFormatter.php index 046336a..286d348 100644 --- a/src/Spout/Reader/XLSX/Helper/CellValueFormatter.php +++ b/src/Spout/Reader/XLSX/Helper/CellValueFormatter.php @@ -44,17 +44,22 @@ class CellValueFormatter /** @var StyleHelper Helper to work with styles */ protected $styleHelper; + /** @var bool Whether date/time values should be returned as PHP objects or be formatted as strings */ + protected $shouldFormatDates; + /** @var \Box\Spout\Common\Escaper\XLSX Used to unescape XML data */ protected $escaper; /** * @param SharedStringsHelper $sharedStringsHelper Helper to work with shared strings * @param StyleHelper $styleHelper Helper to work with styles + * @param bool $shouldFormatDates Whether date/time values should be returned as PHP objects or be formatted as strings */ - public function __construct($sharedStringsHelper, $styleHelper) + public function __construct($sharedStringsHelper, $styleHelper, $shouldFormatDates) { $this->sharedStringsHelper = $sharedStringsHelper; $this->styleHelper = $styleHelper; + $this->shouldFormatDates = $shouldFormatDates; /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */ $this->escaper = new \Box\Spout\Common\Escaper\XLSX(); @@ -168,7 +173,7 @@ class CellValueFormatter $shouldFormatAsDate = $this->styleHelper->shouldFormatNumericValueAsDate($cellStyleId); if ($shouldFormatAsDate) { - return $this->formatExcelTimestampValue(floatval($nodeValue)); + return $this->formatExcelTimestampValue(floatval($nodeValue), $cellStyleId); } else { $nodeIntValue = intval($nodeValue); return ($nodeIntValue == $nodeValue) ? $nodeIntValue : floatval($nodeValue); @@ -181,9 +186,10 @@ class CellValueFormatter * NOTE: The timestamp can also represent a time, if it is a value between 0 and 1. * * @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) + protected function formatExcelTimestampValue($nodeValue, $cellStyleId) { // Fix for the erroneous leap year in Excel if (ceil($nodeValue) > self::ERRONEOUS_EXCEL_LEAP_YEAR_DAY) { @@ -192,10 +198,10 @@ class CellValueFormatter if ($nodeValue >= 1) { // Values greater than 1 represent "dates". The value 1.0 representing the "base" date: 1900-01-01. - return $this->formatExcelTimestampValueAsDateValue($nodeValue); + return $this->formatExcelTimestampValueAsDateValue($nodeValue, $cellStyleId); } else if ($nodeValue >= 0) { // Values between 0 and 1 represent "times". - return $this->formatExcelTimestampValueAsTimeValue($nodeValue); + return $this->formatExcelTimestampValueAsTimeValue($nodeValue, $cellStyleId); } else { // invalid date return null; @@ -207,9 +213,10 @@ class CellValueFormatter * Only the time value matters. The date part is set to Jan 1st, 1900 (base Excel date). * * @param float $nodeValue - * @return \DateTime The value associated with the cell + * @param int $cellStyleId 0 being the default style + * @return \DateTime|string The value associated with the cell */ - protected function formatExcelTimestampValueAsTimeValue($nodeValue) + protected function formatExcelTimestampValueAsTimeValue($nodeValue, $cellStyleId) { $time = round($nodeValue * self::NUM_SECONDS_IN_ONE_DAY); $hours = floor($time / self::NUM_SECONDS_IN_ONE_HOUR); @@ -220,7 +227,13 @@ class CellValueFormatter $dateObj = new \DateTime('1900-01-01'); $dateObj->setTime($hours, $minutes, $seconds); - return $dateObj; + if ($this->shouldFormatDates) { + $styleNumberFormat = $this->styleHelper->getNumberFormat($cellStyleId); + $phpDateFormat = DateFormatHelper::toPHPDateFormat($styleNumberFormat); + return $dateObj->format($phpDateFormat); + } else { + return $dateObj; + } } /** @@ -228,9 +241,10 @@ class CellValueFormatter * NOTE: The timestamp is a float representing the number of days since January 1st, 1900. * * @param float $nodeValue - * @return \DateTime|null The value associated with the cell or NULL if invalid date value + * @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) + protected function formatExcelTimestampValueAsDateValue($nodeValue, $cellStyleId) { // Do not use any unix timestamps for calculation to prevent // issues with numbers exceeding 2^31. @@ -242,7 +256,13 @@ class CellValueFormatter $dateObj->modify('+' . intval($nodeValue) . 'days'); $dateObj->modify('+' . $secondsRemainder . 'seconds'); - return $dateObj; + if ($this->shouldFormatDates) { + $styleNumberFormat = $this->styleHelper->getNumberFormat($cellStyleId); + $phpDateFormat = DateFormatHelper::toPHPDateFormat($styleNumberFormat); + return $dateObj->format($phpDateFormat); + } else { + return $dateObj; + } } catch (\Exception $e) { return null; } diff --git a/src/Spout/Reader/XLSX/Helper/DateFormatHelper.php b/src/Spout/Reader/XLSX/Helper/DateFormatHelper.php new file mode 100644 index 0000000..4acbef7 --- /dev/null +++ b/src/Spout/Reader/XLSX/Helper/DateFormatHelper.php @@ -0,0 +1,122 @@ + [ + // Time + 'am/pm' => 'A', // Uppercase Ante meridiem and Post meridiem + ':mm' => ':i', // Minutes with leading zeros - if preceded by a ":" (otherwise month) + 'mm:' => 'i:', // Minutes with leading zeros - if followed by a ":" (otherwise month) + 'ss' => 's', // Seconds, with leading zeros + '.s' => '', // Ignore (fractional seconds format does not exist in PHP) + + // Date + 'e' => 'Y', // Full numeric representation of a year, 4 digits + 'yyyy' => 'Y', // Full numeric representation of a year, 4 digits + 'yy' => 'y', // Two digit representation of a year + 'mmmmm' => 'M', // Short textual representation of a month, three letters ("mmmmm" should only contain the 1st letter...) + 'mmmm' => 'F', // Full textual representation of a month + 'mmm' => 'M', // Short textual representation of a month, three letters + 'mm' => 'm', // Numeric representation of a month, with leading zeros + 'm' => 'n', // Numeric representation of a month, without leading zeros + 'dddd' => 'l', // Full textual representation of the day of the week + 'ddd' => 'D', // Textual representation of a day, three letters + 'dd' => 'd', // Day of the month, 2 digits with leading zeros + 'd' => 'j', // Day of the month without leading zeros + ], + self::KEY_HOUR_12 => [ + 'hh' => 'h', // 12-hour format of an hour without leading zeros + 'h' => 'g', // 12-hour format of an hour without leading zeros + ], + self::KEY_HOUR_24 => [ + 'hh' => 'H', // 24-hour hours with leading zero + 'h' => 'G', // 24-hour format of an hour without leading zeros + ], + ]; + + /** + * Converts the given Excel date format to a format understandable by the PHP date function. + * + * @param string $excelDateFormat Excel date format + * @return string PHP date format (as defined here: http://php.net/manual/en/function.date.php) + */ + public static function toPHPDateFormat($excelDateFormat) + { + // Remove brackets potentially present at the beginning of the format string + $dateFormat = preg_replace('/^(\[\$[^\]]+?\])/i', '', $excelDateFormat); + + // Double quotes are used to escape characters that must not be interpreted. + // For instance, ["Day " dd] should result in "Day 13" and we should not try to interpret "D", "a", "y" + // By exploding the format string using double quote as a delimiter, we can get all parts + // that must be transformed (even indexes) and all parts that must not be (odd indexes). + $dateFormatParts = explode('"', $dateFormat); + + foreach ($dateFormatParts as $partIndex => $dateFormatPart) { + // do not look at odd indexes + if ($partIndex % 2 === 1) { + continue; + } + + // Make sure all characters are lowercase, as the mapping table is using lowercase characters + $transformedPart = strtolower($dateFormatPart); + + // Remove escapes related to non-format characters + $transformedPart = str_replace('\\', '', $transformedPart); + + // Apply general transformation first... + $transformedPart = strtr($transformedPart, self::$excelDateFormatToPHPDateFormatMapping[self::KEY_GENERAL]); + + // ... then apply hour transformation, for 12-hour or 24-hour format + if (self::has12HourFormatMarker($dateFormatPart)) { + $transformedPart = strtr($transformedPart, self::$excelDateFormatToPHPDateFormatMapping[self::KEY_HOUR_12]); + } else { + $transformedPart = strtr($transformedPart, self::$excelDateFormatToPHPDateFormatMapping[self::KEY_HOUR_24]); + } + + // overwrite the parts array with the new transformed part + $dateFormatParts[$partIndex] = $transformedPart; + } + + // Merge all transformed parts back together + $phpDateFormat = implode('"', $dateFormatParts); + + // Finally, to have the date format compatible with the DateTime::format() function, we need to escape + // all characters that are inside double quotes (and double quotes must be removed). + // For instance, ["Day " dd] should become [\D\a\y\ dd] + $phpDateFormat = preg_replace_callback('/"(.+?)"/', function($matches) { + $stringToEscape = $matches[1]; + $letters = preg_split('//u', $stringToEscape, -1, PREG_SPLIT_NO_EMPTY); + return '\\' . implode('\\', $letters); + }, $phpDateFormat); + + return $phpDateFormat; + } + + /** + * @param string $excelDateFormat Date format as defined by Excel + * @return bool Whether the given date format has the 12-hour format marker + */ + private static function has12HourFormatMarker($excelDateFormat) + { + return (stripos($excelDateFormat, 'am/pm') !== false); + } +} diff --git a/src/Spout/Reader/XLSX/Helper/SheetHelper.php b/src/Spout/Reader/XLSX/Helper/SheetHelper.php index 23a2b08..5f74f44 100644 --- a/src/Spout/Reader/XLSX/Helper/SheetHelper.php +++ b/src/Spout/Reader/XLSX/Helper/SheetHelper.php @@ -30,6 +30,9 @@ class SheetHelper /** @var \Box\Spout\Common\Helper\GlobalFunctionsHelper Helper to work with global functions */ protected $globalFunctionsHelper; + /** @var bool Whether date/time values should be returned as PHP objects or be formatted as strings */ + protected $shouldFormatDates; + /** @var \Box\Spout\Reader\Wrapper\SimpleXMLElement XML element representing the workbook.xml.rels file */ protected $workbookXMLRelsAsXMLElement; @@ -40,12 +43,14 @@ class SheetHelper * @param string $filePath Path of the XLSX file being read * @param \Box\Spout\Reader\XLSX\Helper\SharedStringsHelper Helper to work with shared strings * @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper + * @param bool $shouldFormatDates Whether date/time values should be returned as PHP objects or be formatted as strings */ - public function __construct($filePath, $sharedStringsHelper, $globalFunctionsHelper) + public function __construct($filePath, $sharedStringsHelper, $globalFunctionsHelper, $shouldFormatDates) { $this->filePath = $filePath; $this->sharedStringsHelper = $sharedStringsHelper; $this->globalFunctionsHelper = $globalFunctionsHelper; + $this->shouldFormatDates = $shouldFormatDates; } /** @@ -103,7 +108,7 @@ class SheetHelper // In [Content_Types].xml, the path is "/xl/worksheets/sheet1.xml" $sheetDataXMLFilePath = '/xl/' . $relationshipNode->getAttribute('Target'); - return new Sheet($this->filePath, $sheetDataXMLFilePath, $this->sharedStringsHelper, $sheetIndexZeroBased, $sheetName); + return new Sheet($this->filePath, $sheetDataXMLFilePath, $this->sharedStringsHelper, $this->shouldFormatDates, $sheetIndexZeroBased, $sheetName); } /** diff --git a/src/Spout/Reader/XLSX/Helper/StyleHelper.php b/src/Spout/Reader/XLSX/Helper/StyleHelper.php index 19014b5..462433c 100644 --- a/src/Spout/Reader/XLSX/Helper/StyleHelper.php +++ b/src/Spout/Reader/XLSX/Helper/StyleHelper.php @@ -30,6 +30,25 @@ class StyleHelper /** By convention, default style ID is 0 */ const DEFAULT_STYLE_ID = 0; + /** + * @see https://msdn.microsoft.com/en-us/library/ff529597(v=office.12).aspx + * @var array Mapping between built-in numFmtId and the associated format - for dates only + */ + protected static $builtinNumFmtIdToNumFormatMapping = [ + 14 => 'm/d/yyyy', // @NOTE: ECMA spec is 'mm-dd-yy' + 15 => 'd-mmm-yy', + 16 => 'd-mmm', + 17 => 'mmm-yy', + 18 => 'h:mm AM/PM', + 19 => 'h:mm:ss AM/PM', + 20 => 'h:mm', + 21 => 'h:mm:ss', + 22 => 'm/d/yyyy h:mm', // @NOTE: ECMA spec is 'm/d/yy h:mm', + 45 => 'mm:ss', + 46 => '[h]:mm:ss', + 47 => 'mm:ss.0', // @NOTE: ECMA spec is 'mmss.0', + ]; + /** @var string Path of the XLSX file being read */ protected $filePath; @@ -194,7 +213,7 @@ class StyleHelper */ protected function isNumFmtIdBuiltInDateFormat($numFmtId) { - $builtInDateFormatIds = [14, 15, 16, 17, 18, 19, 20, 21, 22, 45, 46, 47]; + $builtInDateFormatIds = array_keys(self::$builtinNumFmtIdToNumFormatMapping); return in_array($numFmtId, $builtInDateFormatIds); } @@ -235,4 +254,27 @@ class StyleHelper return $hasFoundDateFormatCharacter; } + + /** + * Returns the format as defined in "styles.xml" of the given style. + * NOTE: It is assumed that the style DOES have a number format associated to it. + * + * @param int $styleId Zero-based style ID + * @return string The number format associated with the given style + */ + public function getNumberFormat($styleId) + { + $stylesAttributes = $this->getStylesAttributes(); + $styleAttributes = $stylesAttributes[$styleId]; + $numFmtId = $styleAttributes[self::XML_ATTRIBUTE_NUM_FMT_ID]; + + if ($this->isNumFmtIdBuiltInDateFormat($numFmtId)) { + $numberFormat = self::$builtinNumFmtIdToNumFormatMapping[$numFmtId]; + } else { + $customNumberFormats = $this->getCustomNumberFormats(); + $numberFormat = $customNumberFormats[$numFmtId]; + } + + return $numberFormat; + } } diff --git a/src/Spout/Reader/XLSX/Reader.php b/src/Spout/Reader/XLSX/Reader.php index 42c6f02..bcf02cc 100644 --- a/src/Spout/Reader/XLSX/Reader.php +++ b/src/Spout/Reader/XLSX/Reader.php @@ -69,7 +69,7 @@ class Reader extends AbstractReader $this->sharedStringsHelper->extractSharedStrings(); } - $this->sheetIterator = new SheetIterator($filePath, $this->sharedStringsHelper, $this->globalFunctionsHelper); + $this->sheetIterator = new SheetIterator($filePath, $this->sharedStringsHelper, $this->globalFunctionsHelper, $this->shouldFormatDates); } else { throw new IOException("Could not open $filePath for reading."); } diff --git a/src/Spout/Reader/XLSX/RowIterator.php b/src/Spout/Reader/XLSX/RowIterator.php index d1913bd..c7491ac 100644 --- a/src/Spout/Reader/XLSX/RowIterator.php +++ b/src/Spout/Reader/XLSX/RowIterator.php @@ -59,8 +59,9 @@ class RowIterator implements IteratorInterface * @param string $filePath Path of the XLSX file being read * @param string $sheetDataXMLFilePath Path of the sheet data XML file as in [Content_Types].xml * @param Helper\SharedStringsHelper $sharedStringsHelper Helper to work with shared strings + * @param bool $shouldFormatDates Whether date/time values should be returned as PHP objects or be formatted as strings */ - public function __construct($filePath, $sheetDataXMLFilePath, $sharedStringsHelper) + public function __construct($filePath, $sheetDataXMLFilePath, $sharedStringsHelper, $shouldFormatDates) { $this->filePath = $filePath; $this->sheetDataXMLFilePath = $this->normalizeSheetDataXMLFilePath($sheetDataXMLFilePath); @@ -68,7 +69,7 @@ class RowIterator implements IteratorInterface $this->xmlReader = new XMLReader(); $this->styleHelper = new StyleHelper($filePath); - $this->cellValueFormatter = new CellValueFormatter($sharedStringsHelper, $this->styleHelper); + $this->cellValueFormatter = new CellValueFormatter($sharedStringsHelper, $this->styleHelper, $shouldFormatDates); } /** diff --git a/src/Spout/Reader/XLSX/Sheet.php b/src/Spout/Reader/XLSX/Sheet.php index 85a4dc9..a1c7d95 100644 --- a/src/Spout/Reader/XLSX/Sheet.php +++ b/src/Spout/Reader/XLSX/Sheet.php @@ -25,12 +25,13 @@ class Sheet implements SheetInterface * @param string $filePath Path of the XLSX file being read * @param string $sheetDataXMLFilePath Path of the sheet data XML file as in [Content_Types].xml * @param Helper\SharedStringsHelper Helper to work with shared strings + * @param bool $shouldFormatDates Whether date/time values should be returned as PHP objects or be formatted as strings * @param int $sheetIndex Index of the sheet, based on order in the workbook (zero-based) * @param string $sheetName Name of the sheet */ - public function __construct($filePath, $sheetDataXMLFilePath, $sharedStringsHelper, $sheetIndex, $sheetName) + public function __construct($filePath, $sheetDataXMLFilePath, $sharedStringsHelper, $shouldFormatDates, $sheetIndex, $sheetName) { - $this->rowIterator = new RowIterator($filePath, $sheetDataXMLFilePath, $sharedStringsHelper); + $this->rowIterator = new RowIterator($filePath, $sheetDataXMLFilePath, $sharedStringsHelper, $shouldFormatDates); $this->index = $sheetIndex; $this->name = $sheetName; } diff --git a/src/Spout/Reader/XLSX/SheetIterator.php b/src/Spout/Reader/XLSX/SheetIterator.php index 7b3d3dd..f7a3f59 100644 --- a/src/Spout/Reader/XLSX/SheetIterator.php +++ b/src/Spout/Reader/XLSX/SheetIterator.php @@ -24,12 +24,13 @@ class SheetIterator implements IteratorInterface * @param string $filePath Path of the file to be read * @param \Box\Spout\Reader\XLSX\Helper\SharedStringsHelper $sharedStringsHelper * @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper + * @param bool $shouldFormatDates Whether date/time values should be returned as PHP objects or be formatted as strings * @throws \Box\Spout\Reader\Exception\NoSheetsFoundException If there are no sheets in the file */ - public function __construct($filePath, $sharedStringsHelper, $globalFunctionsHelper) + public function __construct($filePath, $sharedStringsHelper, $globalFunctionsHelper, $shouldFormatDates) { // Fetch all available sheets - $sheetHelper = new SheetHelper($filePath, $sharedStringsHelper, $globalFunctionsHelper); + $sheetHelper = new SheetHelper($filePath, $sharedStringsHelper, $globalFunctionsHelper, $shouldFormatDates); $this->sheets = $sheetHelper->getSheets(); if (count($this->sheets) === 0) { diff --git a/tests/Spout/Reader/ODS/ReaderTest.php b/tests/Spout/Reader/ODS/ReaderTest.php index 4c95fd9..759d842 100644 --- a/tests/Spout/Reader/ODS/ReaderTest.php +++ b/tests/Spout/Reader/ODS/ReaderTest.php @@ -164,6 +164,21 @@ class ReaderTest extends \PHPUnit_Framework_TestCase $this->assertEquals($expectedRows, $allRows); } + /** + * @return void + */ + public function testReadShouldSupportFormatDatesAndTimesIfSpecified() + { + $shouldFormatDates = true; + $allRows = $this->getAllRowsForFile('sheet_with_dates_and_times.ods', $shouldFormatDates); + + $expectedRows = [ + ['05/19/2016', '5/19/16', '05/19/2016 16:39:00', '05/19/16 04:39 PM', '5/19/2016'], + ['11:29', '13:23:45', '01:23:45', '01:23:45 AM', '01:23:45 PM'], + ]; + $this->assertEquals($expectedRows, $allRows); + } + /** * @return void */ @@ -455,14 +470,16 @@ class ReaderTest extends \PHPUnit_Framework_TestCase /** * @param string $fileName + * @param bool|void $shouldFormatDates * @return array All the read rows the given file */ - private function getAllRowsForFile($fileName) + private function getAllRowsForFile($fileName, $shouldFormatDates = false) { $allRows = []; $resourcePath = $this->getResourcePath($fileName); $reader = ReaderFactory::create(Type::ODS); + $reader->setShouldFormatDates($shouldFormatDates); $reader->open($resourcePath); foreach ($reader->getSheetIterator() as $sheetIndex => $sheet) { diff --git a/tests/Spout/Reader/XLSX/Helper/CellValueFormatterTest.php b/tests/Spout/Reader/XLSX/Helper/CellValueFormatterTest.php index 6ea5b92..92831ab 100644 --- a/tests/Spout/Reader/XLSX/Helper/CellValueFormatterTest.php +++ b/tests/Spout/Reader/XLSX/Helper/CellValueFormatterTest.php @@ -71,7 +71,7 @@ class CellValueFormatterTest extends \PHPUnit_Framework_TestCase ->with(123) ->will($this->returnValue(true)); - $formatter = new CellValueFormatter(null, $styleHelperMock); + $formatter = new CellValueFormatter(null, $styleHelperMock, false); $result = $formatter->extractAndFormatNodeValue($nodeMock); if ($expectedDateAsString === null) { @@ -120,7 +120,7 @@ class CellValueFormatterTest extends \PHPUnit_Framework_TestCase ->method('shouldFormatNumericValueAsDate') ->will($this->returnValue(false)); - $formatter = new CellValueFormatter(null, $styleHelperMock); + $formatter = new CellValueFormatter(null, $styleHelperMock, false); $formattedValue = \ReflectionHelper::callMethodOnObject($formatter, 'formatNumericCellValue', $value, 0); $this->assertEquals($expectedFormattedValue, $formattedValue); @@ -163,7 +163,7 @@ class CellValueFormatterTest extends \PHPUnit_Framework_TestCase ->with(CellValueFormatter::XML_NODE_INLINE_STRING_VALUE) ->will($this->returnValue($nodeListMock)); - $formatter = new CellValueFormatter(null, null); + $formatter = new CellValueFormatter(null, null, false); $formattedValue = \ReflectionHelper::callMethodOnObject($formatter, 'formatInlineStringCellValue', $nodeMock); $this->assertEquals($expectedFormattedValue, $formattedValue); diff --git a/tests/Spout/Reader/XLSX/Helper/DateFormatHelperTest.php b/tests/Spout/Reader/XLSX/Helper/DateFormatHelperTest.php new file mode 100644 index 0000000..b6d852c --- /dev/null +++ b/tests/Spout/Reader/XLSX/Helper/DateFormatHelperTest.php @@ -0,0 +1,47 @@ +assertEquals($expectedPHPDateFormat, $phpDateFormat); + } +} diff --git a/tests/Spout/Reader/XLSX/ReaderTest.php b/tests/Spout/Reader/XLSX/ReaderTest.php index ee36266..8620ed5 100644 --- a/tests/Spout/Reader/XLSX/ReaderTest.php +++ b/tests/Spout/Reader/XLSX/ReaderTest.php @@ -203,6 +203,21 @@ class ReaderTest extends \PHPUnit_Framework_TestCase $this->assertEquals($expectedRows, $allRows); } + /** + * @return void + */ + public function testReadShouldSupportFormatDatesAndTimesIfSpecified() + { + $shouldFormatDates = true; + $allRows = $this->getAllRowsForFile('sheet_with_dates_and_times.xlsx', $shouldFormatDates); + + $expectedRows = [ + ['1/13/2016', '01/13/2016', '13-Jan-16', 'Wednesday January 13, 16', 'Today is 1/13/2016'], + ['4:43:25', '04:43', '4:43', '4:43:25 AM', '4:43:25 PM'], + ]; + $this->assertEquals($expectedRows, $allRows); + } + /** * @return void */ @@ -503,14 +518,16 @@ class ReaderTest extends \PHPUnit_Framework_TestCase /** * @param string $fileName + * @param bool|void $shouldFormatDates * @return array All the read rows the given file */ - private function getAllRowsForFile($fileName) + private function getAllRowsForFile($fileName, $shouldFormatDates = false) { $allRows = []; $resourcePath = $this->getResourcePath($fileName); $reader = ReaderFactory::create(Type::XLSX); + $reader->setShouldFormatDates($shouldFormatDates); $reader->open($resourcePath); foreach ($reader->getSheetIterator() as $sheetIndex => $sheet) { diff --git a/tests/resources/ods/sheet_with_dates_and_times.ods b/tests/resources/ods/sheet_with_dates_and_times.ods new file mode 100644 index 0000000000000000000000000000000000000000..0e0fb5f89e268891784ccde64becbecf218c4d44 GIT binary patch literal 9988 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 M_&)x4Q$2k8AH1^?od5s; literal 0 HcmV?d00001 diff --git a/tests/resources/xlsx/sheet_with_dates_and_times.xlsx b/tests/resources/xlsx/sheet_with_dates_and_times.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..769e03b81942f43b0bce8317d312783abeb55f24 GIT binary patch literal 22453 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