From 03183c8b1f42509432fa39b53540918a51ab3f24 Mon Sep 17 00:00:00 2001 From: Adrien Loison Date: Thu, 19 May 2016 11:51:49 -0700 Subject: [PATCH] Add support for cells formatted as time 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