diff --git a/src/Spout/Reader/ODS/Helper/CellValueFormatter.php b/src/Spout/Reader/ODS/Helper/CellValueFormatter.php index 15a8cad..0a2f18d 100644 --- a/src/Spout/Reader/ODS/Helper/CellValueFormatter.php +++ b/src/Spout/Reader/ODS/Helper/CellValueFormatter.php @@ -12,8 +12,13 @@ class CellValueFormatter { /** Definition of all possible cell types */ const CELL_TYPE_STRING = 'string'; - const CELL_TYPE_BOOLEAN = 'boolean'; const CELL_TYPE_FLOAT = 'float'; + const CELL_TYPE_BOOLEAN = 'boolean'; + const CELL_TYPE_DATE = 'date'; + const CELL_TYPE_TIME = 'time'; + const CELL_TYPE_CURRENCY = 'currency'; + const CELL_TYPE_PERCENTAGE = 'percentage'; + const CELL_TYPE_VOID = 'void'; /** Definition of XML nodes names used to parse data */ const XML_NODE_P = 'p'; @@ -21,6 +26,11 @@ class CellValueFormatter /** Definition of XML attribute used to parse data */ const XML_ATTRIBUTE_TYPE = 'office:value-type'; + const XML_ATTRIBUTE_VALUE = 'office:value'; + const XML_ATTRIBUTE_BOOLEAN_VALUE = 'office:boolean-value'; + const XML_ATTRIBUTE_DATE_VALUE = 'office:date-value'; + const XML_ATTRIBUTE_TIME_VALUE = 'office:time-value'; + const XML_ATTRIBUTE_CURRENCY = 'office:currency'; const XML_ATTRIBUTE_C = 'text:c'; /** @var \Box\Spout\Common\Escaper\ODS Used to unescape XML data */ @@ -38,45 +48,36 @@ class CellValueFormatter /** * Returns the (unescaped) correctly marshalled, cell value associated to the given XML node. * @TODO Add other types !! + * @see http://docs.oasis-open.org/office/v1.2/os/OpenDocument-v1.2-os-part1.html#refTable13 * * @param \DOMNode $node - * @return string|int|float|bool The value associated with the cell (or empty string if cell's type is undefined) + * @return string|int|float|bool|\DateTime|\DateInterval|null The value associated with the cell, empty string if cell's type is void/undefined, null on error */ public function extractAndFormatNodeValue($node) { $cellType = $node->getAttribute(self::XML_ATTRIBUTE_TYPE); - $pNodeValue = $this->getFirstPNodeValue($node); switch ($cellType) { case self::CELL_TYPE_STRING: return $this->formatStringCellValue($node); case self::CELL_TYPE_FLOAT: - return $this->formatFloatCellValue($pNodeValue); + return $this->formatFloatCellValue($node); case self::CELL_TYPE_BOOLEAN: - return $this->formatBooleanCellValue($pNodeValue); + return $this->formatBooleanCellValue($node); + case self::CELL_TYPE_DATE: + return $this->formatDateCellValue($node); + case self::CELL_TYPE_TIME: + return $this->formatTimeCellValue($node); + case self::CELL_TYPE_CURRENCY: + return $this->formatCurrencyCellValue($node); + case self::CELL_TYPE_PERCENTAGE: + return $this->formatPercentageCellValue($node); + case self::CELL_TYPE_VOID: default: return ''; } } - /** - * Returns the value of the first "" node within the given node. - * - * @param \DOMNode $node - * @return string Value for the first "" node or empty string if no "" found - */ - protected function getFirstPNodeValue($node) - { - $nodeValue = ''; - $pNodes = $node->getElementsByTagName(self::XML_NODE_P); - - if ($pNodes->length > 0) { - $nodeValue = $pNodes->item(0)->nodeValue; - } - - return $nodeValue; - } - /** * Returns the cell String value. * @@ -110,27 +111,87 @@ class CellValueFormatter } /** - * Returns the cell Numeric value from string of nodeValue. + * Returns the cell Numeric value from the given node. * - * @param string $pNodeValue + * @param \DOMNode $node * @return int|float The value associated with the cell */ - protected function formatFloatCellValue($pNodeValue) + protected function formatFloatCellValue($node) { - $cellValue = is_int($pNodeValue) ? intval($pNodeValue) : floatval($pNodeValue); + $nodeValue = $node->getAttribute(self::XML_ATTRIBUTE_VALUE); + $cellValue = is_int($nodeValue) ? intval($nodeValue) : floatval($nodeValue); return $cellValue; } /** - * Returns the cell Boolean value from a specific node's Value. + * Returns the cell Boolean value from the given node. * - * @param string $pNodeValue + * @param \DOMNode $node * @return bool The value associated with the cell */ - protected function formatBooleanCellValue($pNodeValue) + protected function formatBooleanCellValue($node) { + $nodeValue = $node->getAttribute(self::XML_ATTRIBUTE_BOOLEAN_VALUE); // !! is similar to boolval() - $cellValue = !!$pNodeValue; + $cellValue = !!$nodeValue; return $cellValue; } + + /** + * 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 + */ + protected function formatDateCellValue($node) + { + try { + $nodeValue = $node->getAttribute(self::XML_ATTRIBUTE_DATE_VALUE); + return new \DateTime($nodeValue); + } catch (\Exception $e) { + return null; + } + } + + /** + * 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 + */ + protected function formatTimeCellValue($node) + { + try { + $nodeValue = $node->getAttribute(self::XML_ATTRIBUTE_TIME_VALUE); + return new \DateInterval($nodeValue); + } catch (\Exception $e) { + return null; + } + } + + /** + * Returns the cell Currency value from the given node. + * + * @param \DOMNode $node + * @return string The value associated with the cell (e.g. "100 USD" or "9.99 EUR") + */ + protected function formatCurrencyCellValue($node) + { + $value = $node->getAttribute(self::XML_ATTRIBUTE_VALUE); + $currency = $node->getAttribute(self::XML_ATTRIBUTE_CURRENCY); + + return "$value $currency"; + } + + /** + * Returns the cell Percentage value from the given node. + * + * @param \DOMNode $node + * @return int|float The value associated with the cell + */ + protected function formatPercentageCellValue($node) + { + // percentages are formatted like floats + return $this->formatFloatCellValue($node); + } } diff --git a/src/Spout/Reader/ODS/RowIterator.php b/src/Spout/Reader/ODS/RowIterator.php index 7a3745f..def7712 100644 --- a/src/Spout/Reader/ODS/RowIterator.php +++ b/src/Spout/Reader/ODS/RowIterator.php @@ -174,7 +174,7 @@ class RowIterator implements IteratorInterface * Returns the (unescaped) correctly marshalled, cell value associated to the given XML node. * * @param \DOMNode $node - * @return string|int|float|bool The value associated with the cell (or empty string if cell's type is undefined) + * @return string|int|float|bool|\DateTime|\DateInterval|null The value associated with the cell, empty string if cell's type is void/undefined, null on error */ protected function getCellValue($node) { diff --git a/src/Spout/Reader/XLSX/Helper/CellValueFormatter.php b/src/Spout/Reader/XLSX/Helper/CellValueFormatter.php index 99d4920..79f92e7 100644 --- a/src/Spout/Reader/XLSX/Helper/CellValueFormatter.php +++ b/src/Spout/Reader/XLSX/Helper/CellValueFormatter.php @@ -165,7 +165,7 @@ class CellValueFormatter * Returns a cell's PHP Date value, associated to the given stored nodeValue. * * @param string $nodeValue - * @return \DateTime|null The value associated with the cell (null when the cell has an error) + * @return \DateTime|null The value associated with the cell or NULL if invalid date value */ protected function formatDateCellValue($nodeValue) { diff --git a/src/Spout/Writer/ODS/Internal/Worksheet.php b/src/Spout/Writer/ODS/Internal/Worksheet.php index a3d5d76..19305f0 100644 --- a/src/Spout/Writer/ODS/Internal/Worksheet.php +++ b/src/Spout/Writer/ODS/Internal/Worksheet.php @@ -194,7 +194,7 @@ class Worksheet implements WorksheetInterface $data .= ''; } else if (CellHelper::isBoolean($cellValue)) { - $data .= ' office:value-type="boolean" calcext:value-type="boolean" office:value="' . $cellValue . '">'; + $data .= ' office:value-type="boolean" calcext:value-type="boolean" office:boolean-value="' . $cellValue . '">'; $data .= '' . $cellValue . ''; $data .= ''; } else if (CellHelper::isNumeric($cellValue)) { diff --git a/tests/Spout/Reader/ODS/ReaderTest.php b/tests/Spout/Reader/ODS/ReaderTest.php index 81d808e..df04ffc 100644 --- a/tests/Spout/Reader/ODS/ReaderTest.php +++ b/tests/Spout/Reader/ODS/ReaderTest.php @@ -143,6 +143,9 @@ class ReaderTest extends \PHPUnit_Framework_TestCase */ public function testReadShouldSupportAllCellTypes() { + $utcTz = new \DateTimeZone('UTC'); + $honoluluTz = new \DateTimeZone('Pacific/Honolulu'); // UTC-10 + $allRows = $this->getAllRowsForFile('sheet_with_all_cell_types.ods'); $expectedRows = [ @@ -150,6 +153,11 @@ class ReaderTest extends \PHPUnit_Framework_TestCase 'ods--11', 'ods--12', true, false, 0, 10.43, + new \DateTime('1987-11-29T00:00:00', $utcTz), new \DateTime('1987-11-29T13:37:00', $utcTz), + new \DateTime('1987-11-29T13:37:00', $utcTz), new \DateTime('1987-11-29T13:37:00', $honoluluTz), + new \DateInterval('PT13H37M00S'), + 0, 0.42, + '42 USD', '9.99 EUR', '', ], ]; @@ -165,6 +173,15 @@ class ReaderTest extends \PHPUnit_Framework_TestCase $this->assertEquals([['ods--11', '', 'ods--13']], $allRows); } + /** + * @return void + */ + public function testReadShouldReturnNullOnInvalidDateOrTime() + { + $allRows = $this->getAllRowsForFile('sheet_with_invalid_date_time.ods'); + $this->assertEquals([[null, null]], $allRows); + } + /** * @return void */ diff --git a/tests/resources/ods/sheet_with_all_cell_types.ods b/tests/resources/ods/sheet_with_all_cell_types.ods index 5843ac8..440f21c 100644 Binary files a/tests/resources/ods/sheet_with_all_cell_types.ods and b/tests/resources/ods/sheet_with_all_cell_types.ods differ diff --git a/tests/resources/ods/sheet_with_invalid_date_time.ods b/tests/resources/ods/sheet_with_invalid_date_time.ods new file mode 100644 index 0000000..b823ee5 Binary files /dev/null and b/tests/resources/ods/sheet_with_invalid_date_time.ods differ diff --git a/tests/resources/ods/sheet_with_number_columns_repeated.ods b/tests/resources/ods/sheet_with_number_columns_repeated.ods index 0f4a802..26c6420 100644 Binary files a/tests/resources/ods/sheet_with_number_columns_repeated.ods and b/tests/resources/ods/sheet_with_number_columns_repeated.ods differ