Compare commits

...
12 Commits
Author SHA1 Message Date
Adrien Loison 582da8403d Merge pull request #139 from box/fix_phpdoc
Fix PHPDoc to work with Augmented Types
2015-11-05 15:52:56 -08:00
Adrien Loison 8b666fc6cd Fix PHPDoc to work with Augmented Types 2015-11-05 15:48:26 -08:00
Adrien Loison f8aab6eefd Merge pull request #138 from box/increase_entropy_uniqid
Increase entropy of uniqid
2015-11-05 10:44:47 -08:00
Adrien Loison c4c6dddb20 Increase entropy of uniqid
This is to avoid conflicts if two folders are being created at the exact same time.
2015-11-05 10:40:19 -08:00
Adrien Loison 6c97141679 Merge pull request #133 from philipbrown/patch-1
Update README.md
2015-10-27 11:44:01 -07:00
Philip Brown 9c8cc05364 Update README.md 2015-10-27 18:34:58 +00:00
Adrien Loison 2a9400dfca Merge pull request #131 from box/better_date_support_xlsx
Better date support
2015-10-23 16:26:00 -07:00
Adrien Loison 8ef6bdac62 Better date support
Although Excel has a Date type, older Excel versions use numeric values to store dates.
The value represents the number of days since Jan 1st, 1900.
The only way to tell if the value is a number or a date is to look at the styles.xml and check if the cell has date formatting.
2015-10-23 16:04:38 -07:00
Adrien Loison 46c246e6a4 Merge pull request #130 from box/highlight_xlsx_over_csv
Highlight XLSX over CSV in documentation
2015-10-23 11:00:04 -07:00
Adrien Loison 8fd606ae4f Highlight XLSX over CSV in documentation 2015-10-23 10:52:12 -07:00
Adrien Loison 6767386daf Merge pull request #128 from box/increase_max_read_bytes_for_csv
Increase max read bytes per line for CSV
2015-10-22 13:39:18 -07:00
Adrien Loison 3395d3abb3 Increase max read bytes per line for CSV
Specify a bigger value than the default one to support long lines.
2015-10-22 10:54:12 -07:00
17 changed files with 509 additions and 32 deletions

No files matched your search

+6 -13
View File
@@ -18,16 +18,9 @@ Join the community and come discuss about Spout: [![Gitter](https://badges.gitte
Spout can be installed directly from [Composer](https://getcomposer.org/). Spout can be installed directly from [Composer](https://getcomposer.org/).
Add "box/spout" as a dependency in your project's composer.json file: Run the following command:
```json
"require": {
"box/spout": "~2.0"
}
``` ```
$ composer require box/spout
Then run the install command from Composer:
```
php composer.phar install
``` ```
### Manual installation ### Manual installation
@@ -62,8 +55,8 @@ Regardless of the file type, the interface to read a file is always the same:
use Box\Spout\Reader\ReaderFactory; use Box\Spout\Reader\ReaderFactory;
use Box\Spout\Common\Type; use Box\Spout\Common\Type;
$reader = ReaderFactory::create(Type::CSV); // for CSV files $reader = ReaderFactory::create(Type::XLSX); // for XLSX files
//$reader = ReaderFactory::create(Type::XLSX); // for XLSX files //$reader = ReaderFactory::create(Type::CSV); // for CSV files
//$reader = ReaderFactory::create(Type::ODS); // for ODS files //$reader = ReaderFactory::create(Type::ODS); // for ODS files
$reader->open($filePath); $reader->open($filePath);
@@ -87,8 +80,8 @@ As with the reader, there is one common interface to write data to a file:
use Box\Spout\Writer\WriterFactory; use Box\Spout\Writer\WriterFactory;
use Box\Spout\Common\Type; use Box\Spout\Common\Type;
$writer = WriterFactory::create(Type::CSV); // for CSV files $writer = WriterFactory::create(Type::XLSX); // for XLSX files
//$writer = WriterFactory::create(Type::XLSX); // for XLSX files //$writer = WriterFactory::create(Type::CSV); // for CSV files
//$writer = WriterFactory::create(Type::ODS); // for ODS files //$writer = WriterFactory::create(Type::ODS); // for ODS files
$writer->openToFile($filePath); // write data to a file or to a PHP stream $writer->openToFile($filePath); // write data to a file or to a PHP stream
+7 -1
View File
@@ -13,6 +13,12 @@ use Box\Spout\Common\Helper\EncodingHelper;
*/ */
class RowIterator implements IteratorInterface class RowIterator implements IteratorInterface
{ {
/**
* If no value is given to stream_get_line(), it defaults to 8192 (which may be too low).
* Alignement with other functions like fgets() is discussed here: https://bugs.php.net/bug.php?id=48421
*/
const MAX_READ_BYTES_PER_LINE = 32768;
/** @var resource Pointer to the CSV file to read */ /** @var resource Pointer to the CSV file to read */
protected $filePointer; protected $filePointer;
@@ -147,7 +153,7 @@ class RowIterator implements IteratorInterface
{ {
// Read until the EOL delimiter or EOF is reached. The delimiter's encoding needs to match the CSV's encoding. // Read until the EOL delimiter or EOF is reached. The delimiter's encoding needs to match the CSV's encoding.
$encodedEOLDelimiter = $this->getEncodedEOLDelimiter(); $encodedEOLDelimiter = $this->getEncodedEOLDelimiter();
$encodedLineData = $this->globalFunctionsHelper->stream_get_line($this->filePointer, 0, $encodedEOLDelimiter); $encodedLineData = $this->globalFunctionsHelper->stream_get_line($this->filePointer, self::MAX_READ_BYTES_PER_LINE, $encodedEOLDelimiter);
// If the line could have been read, it can be converted to UTF-8 // If the line could have been read, it can be converted to UTF-8
$utf8EncodedLineData = ($encodedLineData !== false) ? $utf8EncodedLineData = ($encodedLineData !== false) ?
+2 -2
View File
@@ -11,7 +11,7 @@ use Box\Spout\Reader\SheetInterface;
*/ */
class Sheet implements SheetInterface class Sheet implements SheetInterface
{ {
/** @var RowIterator To iterate over the CSV's rows */ /** @var \Box\Spout\Reader\CSV\RowIterator To iterate over the CSV's rows */
protected $rowIterator; protected $rowIterator;
/** /**
@@ -28,7 +28,7 @@ class Sheet implements SheetInterface
/** /**
* @api * @api
* @return RowIterator * @return \Box\Spout\Reader\CSV\RowIterator
*/ */
public function getRowIterator() public function getRowIterator()
{ {
+2 -2
View File
@@ -12,7 +12,7 @@ use Box\Spout\Reader\IteratorInterface;
*/ */
class SheetIterator implements IteratorInterface class SheetIterator implements IteratorInterface
{ {
/** @var Sheet The CSV unique "sheet" */ /** @var \Box\Spout\Reader\CSV\Sheet The CSV unique "sheet" */
protected $sheet; protected $sheet;
/** @var bool Whether the unique "sheet" has already been read */ /** @var bool Whether the unique "sheet" has already been read */
@@ -67,7 +67,7 @@ class SheetIterator implements IteratorInterface
* Return the current element * Return the current element
* @link http://php.net/manual/en/iterator.current.php * @link http://php.net/manual/en/iterator.current.php
* *
* @return Sheet * @return \Box\Spout\Reader\CSV\Sheet
*/ */
public function current() public function current()
{ {
+2 -2
View File
@@ -13,7 +13,7 @@ use Box\Spout\Reader\Wrapper\XMLReader;
*/ */
class Sheet implements SheetInterface class Sheet implements SheetInterface
{ {
/** @var RowIterator To iterate over sheet's rows */ /** @var \Box\Spout\Reader\ODS\RowIterator To iterate over sheet's rows */
protected $rowIterator; protected $rowIterator;
/** @var int ID of the sheet */ /** @var int ID of the sheet */
@@ -39,7 +39,7 @@ class Sheet implements SheetInterface
/** /**
* @api * @api
* @return RowIterator * @return \Box\Spout\Reader\ODS\RowIterator
*/ */
public function getRowIterator() public function getRowIterator()
{ {
+1 -1
View File
@@ -102,7 +102,7 @@ class SheetIterator implements IteratorInterface
* Return the current element * Return the current element
* @link http://php.net/manual/en/iterator.current.php * @link http://php.net/manual/en/iterator.current.php
* *
* @return Sheet * @return \Box\Spout\Reader\ODS\Sheet
*/ */
public function current() public function current()
{ {
@@ -151,6 +151,22 @@ class SimpleXMLElement
return $doesElementExist ? $this->wrapSimpleXMLElement($realElement) : null; return $doesElementExist ? $this->wrapSimpleXMLElement($realElement) : null;
} }
/**
* Returns the immediate children.
*
* @return array The children
*/
public function children()
{
$children = [];
foreach ($this->simpleXMLElement->children() as $child) {
$children[] = $this->wrapSimpleXMLElement($child);
}
return $children;
}
/** /**
* @return string * @return string
*/ */
@@ -25,19 +25,35 @@ class CellValueFormatter
/** Definition of XML attributes used to parse data */ /** Definition of XML attributes used to parse data */
const XML_ATTRIBUTE_TYPE = 't'; const XML_ATTRIBUTE_TYPE = 't';
const XML_ATTRIBUTE_STYLE_ID = 's';
/** Constants used for date formatting */
const NUM_DAYS_FROM_JAN_1_1900_TO_JAN_1_1970 = 25569; // actually 25567 but accommodating for an Excel bug with some bisextile years
const NUM_SECONDS_IN_ONE_DAY = 86400;
/**
* February 29th, 1900 is NOT a leap year but Excel thinks it is...
* @see https://en.wikipedia.org/wiki/Year_1900_problem#Microsoft_Excel
*/
const ERRONEOUS_EXCEL_LEAP_YEAR_DAY = 60;
/** @var SharedStringsHelper Helper to work with shared strings */ /** @var SharedStringsHelper Helper to work with shared strings */
protected $sharedStringsHelper; protected $sharedStringsHelper;
/** @var StyleHelper Helper to work with styles */
protected $styleHelper;
/** @var \Box\Spout\Common\Escaper\XLSX Used to unescape XML data */ /** @var \Box\Spout\Common\Escaper\XLSX Used to unescape XML data */
protected $escaper; protected $escaper;
/** /**
* @param SharedStringsHelper $sharedStringsHelper Helper to work with shared strings * @param SharedStringsHelper $sharedStringsHelper Helper to work with shared strings
* @param StyleHelper $styleHelper Helper to work with styles
*/ */
public function __construct($sharedStringsHelper) public function __construct($sharedStringsHelper, $styleHelper)
{ {
$this->sharedStringsHelper = $sharedStringsHelper; $this->sharedStringsHelper = $sharedStringsHelper;
$this->styleHelper = $styleHelper;
/** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */ /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */
$this->escaper = new \Box\Spout\Common\Escaper\XLSX(); $this->escaper = new \Box\Spout\Common\Escaper\XLSX();
@@ -53,6 +69,7 @@ class CellValueFormatter
{ {
// Default cell type is "n" // Default cell type is "n"
$cellType = $node->getAttribute(self::XML_ATTRIBUTE_TYPE) ?: self::CELL_TYPE_NUMERIC; $cellType = $node->getAttribute(self::XML_ATTRIBUTE_TYPE) ?: self::CELL_TYPE_NUMERIC;
$cellStyleId = intval($node->getAttribute(self::XML_ATTRIBUTE_STYLE_ID));
$vNodeValue = $this->getVNodeValue($node); $vNodeValue = $this->getVNodeValue($node);
if (($vNodeValue === '') && ($cellType !== self::CELL_TYPE_INLINE_STRING)) { if (($vNodeValue === '') && ($cellType !== self::CELL_TYPE_INLINE_STRING)) {
@@ -69,7 +86,7 @@ class CellValueFormatter
case self::CELL_TYPE_BOOLEAN: case self::CELL_TYPE_BOOLEAN:
return $this->formatBooleanCellValue($vNodeValue); return $this->formatBooleanCellValue($vNodeValue);
case self::CELL_TYPE_NUMERIC: case self::CELL_TYPE_NUMERIC:
return $this->formatNumericCellValue($vNodeValue); return $this->formatNumericCellValue($vNodeValue, $cellStyleId);
case self::CELL_TYPE_DATE: case self::CELL_TYPE_DATE:
return $this->formatDateCellValue($vNodeValue); return $this->formatDateCellValue($vNodeValue);
default: default:
@@ -138,14 +155,49 @@ class CellValueFormatter
/** /**
* Returns the cell Numeric value from string of nodeValue. * Returns the cell Numeric value from string of nodeValue.
* The value can also represent a timestamp and a DateTime will be returned.
* *
* @param string $nodeValue * @param string $nodeValue
* @return int|float The value associated with the cell * @param int $cellStyleId 0 being the default style
* @return int|float|\DateTime|null The value associated with the cell
*/ */
protected function formatNumericCellValue($nodeValue) protected function formatNumericCellValue($nodeValue, $cellStyleId)
{ {
$cellValue = is_int($nodeValue) ? intval($nodeValue) : floatval($nodeValue); // Numeric values can represent numbers as well as timestamps.
// We need to look at the style of the cell to determine whether it is one or the other.
$shouldFormatAsDate = $this->styleHelper->shouldFormatNumericValueAsDate($cellStyleId);
if ($shouldFormatAsDate) {
return $this->formatExcelTimestampValue(floatval($nodeValue));
} else {
return is_int($nodeValue) ? intval($nodeValue) : floatval($nodeValue);
}
}
/**
* 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 formatExcelTimestampValue($nodeValue)
{
$numDaysSince1970Jan1 = $nodeValue - self::NUM_DAYS_FROM_JAN_1_1900_TO_JAN_1_1970;
// Fix for the erroneous leap year in Excel
if ($nodeValue < self::ERRONEOUS_EXCEL_LEAP_YEAR_DAY) {
$numDaysSince1970Jan1++;
}
$unixTimestamp = round($numDaysSince1970Jan1 * self::NUM_SECONDS_IN_ONE_DAY);
try {
$cellValue = (new \DateTime())->setTimestamp($unixTimestamp);
return $cellValue; return $cellValue;
} catch (\Exception $e) {
return null;
}
} }
/** /**
@@ -0,0 +1,224 @@
<?php
namespace Box\Spout\Reader\XLSX\Helper;
use Box\Spout\Reader\Wrapper\SimpleXMLElement;
use Box\Spout\Reader\Wrapper\XMLReader;
/**
* Class StyleHelper
* This class provides helper functions related to XLSX styles
*
* @package Box\Spout\Reader\XLSX\Helper
*/
class StyleHelper
{
/** Paths of XML files relative to the XLSX file root */
const STYLES_XML_FILE_PATH = 'xl/styles.xml';
/** Nodes used to find relevant information in the styles XML file */
const XML_NODE_NUM_FMTS = 'numFmts';
const XML_NODE_NUM_FMT = 'numFmt';
const XML_NODE_CELL_XFS = 'cellXfs';
const XML_NODE_XF = 'xf';
/** Attributes used to find relevant information in the styles XML file */
const XML_ATTRIBUTE_NUM_FMT_ID = 'numFmtId';
const XML_ATTRIBUTE_FORMAT_CODE = 'formatCode';
const XML_ATTRIBUTE_APPLY_NUMBER_FORMAT = 'applyNumberFormat';
/** By convention, default style ID is 0 */
const DEFAULT_STYLE_ID = 0;
/** @var string Path of the XLSX file being read */
protected $filePath;
/** @var array Array containing a mapping NUM_FMT_ID => FORMAT_CODE */
protected $customNumberFormats;
/** @var array Array containing a mapping STYLE_ID => [STYLE_ATTRIBUTES] */
protected $stylesAttributes;
/**
* @param string $filePath Path of the XLSX file being read
*/
public function __construct($filePath)
{
$this->filePath = $filePath;
}
/**
* Reads the styles.xml file and extract the relevant information from the file.
*
* @return void
*/
protected function extractRelevantInfo()
{
$this->customNumberFormats = [];
$this->stylesAttributes = [];
$stylesXmlFilePath = $this->filePath .'#' . self::STYLES_XML_FILE_PATH;
$xmlReader = new XMLReader();
if ($xmlReader->open('zip://' . $stylesXmlFilePath)) {
while ($xmlReader->read()) {
if ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_NUM_FMTS)) {
$numFmtsNode = new SimpleXMLElement($xmlReader->readOuterXml());
$this->extractNumberFormats($numFmtsNode);
} else if ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_CELL_XFS)) {
$cellXfsNode = new SimpleXMLElement($xmlReader->readOuterXml());
$this->extractStyleAttributes($cellXfsNode);
}
}
$xmlReader->close();
}
}
/**
* Extracts number formats from the "numFmt" nodes.
* For simplicity, the styles attributes are kept in memory. This is possible thanks
* to the reuse of formats. So 1 million cells should not use 1 million formats.
*
* @param SimpleXMLElement $numFmtsNode The "numFmts" node
* @return void
*/
protected function extractNumberFormats($numFmtsNode)
{
foreach ($numFmtsNode->children() as $numFmtNode) {
$numFmtId = intval($numFmtNode->getAttribute(self::XML_ATTRIBUTE_NUM_FMT_ID));
$formatCode = $numFmtNode->getAttribute(self::XML_ATTRIBUTE_FORMAT_CODE);
$this->customNumberFormats[$numFmtId] = $formatCode;
}
}
/**
* Extracts style attributes from the "xf" nodes, inside the "cellXfs" section.
* For simplicity, the styles attributes are kept in memory. This is possible thanks
* to the reuse of styles. So 1 million cells should not use 1 million styles.
*
* @param SimpleXMLElement $cellXfsNode The "cellXfs" node
* @return void
*/
protected function extractStyleAttributes($cellXfsNode)
{
foreach ($cellXfsNode->children() as $xfNode) {
$this->stylesAttributes[] = [
self::XML_ATTRIBUTE_NUM_FMT_ID => intval($xfNode->getAttribute(self::XML_ATTRIBUTE_NUM_FMT_ID)),
self::XML_ATTRIBUTE_APPLY_NUMBER_FORMAT => !!($xfNode->getAttribute(self::XML_ATTRIBUTE_APPLY_NUMBER_FORMAT)),
];
}
}
/**
* @return array The custom number formats
*/
protected function getCustomNumberFormats()
{
if (!isset($this->customNumberFormats)) {
$this->extractRelevantInfo();
}
return $this->customNumberFormats;
}
/**
* @return array The styles attributes
*/
protected function getStylesAttributes()
{
if (!isset($this->stylesAttributes)) {
$this->extractRelevantInfo();
}
return $this->stylesAttributes;
}
/**
* Returns whether the style with the given ID should consider
* numeric values as timestamps and format the cell as a date.
*
* @param int $styleId Zero-based style ID
* @return bool Whether the cell with the given cell should display a date instead of a numeric value
*/
public function shouldFormatNumericValueAsDate($styleId)
{
$stylesAttributes = $this->getStylesAttributes();
// Default style (0) does not format numeric values as timestamps. Only custom styles do.
// Also if the style ID does not exist in the styles.xml file, format as numeric value.
if ($styleId === self::DEFAULT_STYLE_ID || !array_key_exists($styleId, $stylesAttributes)) {
return false;
}
$styleAttributes = $stylesAttributes[$styleId];
$applyNumberFormat = $styleAttributes[self::XML_ATTRIBUTE_APPLY_NUMBER_FORMAT];
if (!$applyNumberFormat) {
return false;
}
$numFmtId = $styleAttributes[self::XML_ATTRIBUTE_NUM_FMT_ID];
return $this->doesNumFmtIdIndicateDate($numFmtId);
}
/**
* @param int $numFmtId
* @return bool Whether the number format ID indicates that the number is a timestamp
*/
protected function doesNumFmtIdIndicateDate($numFmtId)
{
return (
$this->isNumFmtIdBuiltInDateFormat($numFmtId) ||
$this->isNumFmtIdCustomDateFormat($numFmtId)
);
}
/**
* @param int $numFmtId
* @return bool Whether the number format ID indicates that the number is a timestamp
*/
protected function isNumFmtIdBuiltInDateFormat($numFmtId)
{
$builtInDateFormatIds = [14, 15, 16, 17, 18, 19, 20, 21, 22, 45, 46, 47];
return in_array($numFmtId, $builtInDateFormatIds);
}
/**
* @param int $numFmtId
* @return bool Whether the number format ID indicates that the number is a timestamp
*/
protected function isNumFmtIdCustomDateFormat($numFmtId)
{
$customNumberFormats = $this->getCustomNumberFormats();
if (!array_key_exists($numFmtId, $customNumberFormats)) {
return false;
}
$customNumberFormat = $customNumberFormats[$numFmtId];
// Remove extra formatting (what's between [ ], the brackets should not be preceded by a "\")
$pattern = '((?<!\\\)\[.+?(?<!\\\)\])';
$customNumberFormat = preg_replace($pattern, '', $customNumberFormat);
// custom date formats contain specific characters to represent the date:
// e - yy - m - d - h - s
// and all of their variants (yyyy - mm - dd...)
$dateFormatCharacters = ['e', 'yy', 'm', 'd', 'h', 's'];
$hasFoundDateFormatCharacter = false;
foreach ($dateFormatCharacters as $dateFormatCharacter) {
// character not preceded by "\"
$pattern = '/(?<!\\\)' . $dateFormatCharacter . '/';
if (preg_match($pattern, $customNumberFormat)) {
$hasFoundDateFormatCharacter = true;
break;
}
}
return $hasFoundDateFormatCharacter;
}
}
+7 -1
View File
@@ -8,6 +8,7 @@ use Box\Spout\Reader\IteratorInterface;
use Box\Spout\Reader\Wrapper\XMLReader; use Box\Spout\Reader\Wrapper\XMLReader;
use Box\Spout\Reader\XLSX\Helper\CellHelper; use Box\Spout\Reader\XLSX\Helper\CellHelper;
use Box\Spout\Reader\XLSX\Helper\CellValueFormatter; use Box\Spout\Reader\XLSX\Helper\CellValueFormatter;
use Box\Spout\Reader\XLSX\Helper\StyleHelper;
/** /**
* Class RowIterator * Class RowIterator
@@ -39,6 +40,9 @@ class RowIterator implements IteratorInterface
/** @var Helper\CellValueFormatter Helper to format cell values */ /** @var Helper\CellValueFormatter Helper to format cell values */
protected $cellValueFormatter; protected $cellValueFormatter;
/** @var Helper\StyleHelper $styleHelper Helper to work with styles */
protected $styleHelper;
/** @var int Number of read rows */ /** @var int Number of read rows */
protected $numReadRows = 0; protected $numReadRows = 0;
@@ -62,7 +66,9 @@ class RowIterator implements IteratorInterface
$this->sheetDataXMLFilePath = $this->normalizeSheetDataXMLFilePath($sheetDataXMLFilePath); $this->sheetDataXMLFilePath = $this->normalizeSheetDataXMLFilePath($sheetDataXMLFilePath);
$this->xmlReader = new XMLReader(); $this->xmlReader = new XMLReader();
$this->cellValueFormatter = new CellValueFormatter($sharedStringsHelper);
$this->styleHelper = new StyleHelper($filePath);
$this->cellValueFormatter = new CellValueFormatter($sharedStringsHelper, $this->styleHelper);
} }
/** /**
+2 -2
View File
@@ -12,7 +12,7 @@ use Box\Spout\Reader\SheetInterface;
*/ */
class Sheet implements SheetInterface class Sheet implements SheetInterface
{ {
/** @var RowIterator To iterate over sheet's rows */ /** @var \Box\Spout\Reader\XLSX\RowIterator To iterate over sheet's rows */
protected $rowIterator; protected $rowIterator;
/** @var int Index of the sheet, based on order of creation (zero-based) */ /** @var int Index of the sheet, based on order of creation (zero-based) */
@@ -37,7 +37,7 @@ class Sheet implements SheetInterface
/** /**
* @api * @api
* @return RowIterator * @return \Box\Spout\Reader\XLSX\RowIterator
*/ */
public function getRowIterator() public function getRowIterator()
{ {
+2 -2
View File
@@ -14,7 +14,7 @@ use Box\Spout\Reader\Exception\NoSheetsFoundException;
*/ */
class SheetIterator implements IteratorInterface class SheetIterator implements IteratorInterface
{ {
/** @var Sheet[] The list of sheet present in the file */ /** @var \Box\Spout\Reader\XLSX\Sheet[] The list of sheet present in the file */
protected $sheets; protected $sheets;
/** @var int The index of the sheet being read (zero-based) */ /** @var int The index of the sheet being read (zero-based) */
@@ -79,7 +79,7 @@ class SheetIterator implements IteratorInterface
* Return the current element * Return the current element
* @link http://php.net/manual/en/iterator.current.php * @link http://php.net/manual/en/iterator.current.php
* *
* @return Sheet * @return \Box\Spout\Reader\XLSX\Sheet
*/ */
public function current() public function current()
{ {
@@ -94,7 +94,7 @@ class FileSystemHelper extends \Box\Spout\Common\Helper\FileSystemHelper
*/ */
protected function createRootFolder() protected function createRootFolder()
{ {
$this->rootFolder = $this->createFolder($this->baseFolderPath, uniqid('xlsx')); $this->rootFolder = $this->createFolder($this->baseFolderPath, uniqid('xlsx', true));
return $this; return $this;
} }
@@ -0,0 +1,134 @@
<?php
namespace Box\Spout\Reader\XLSX\Helper;
/**
* Class StyleHelperTest
*
* @package Box\Spout\Reader\XLSX\Helper
*/
class StyleHelperTest extends \PHPUnit_Framework_TestCase
{
/**
* @param array $styleAttributes
* @param array|void $customNumberFormats
* @return StyleHelper
*/
private function getStyleHelperMock($styleAttributes, $customNumberFormats = [])
{
/** @var StyleHelper $styleHelper */
$styleHelper = $this->getMockBuilder('\Box\Spout\Reader\XLSX\Helper\StyleHelper')
->setMethods(['getCustomNumberFormats', 'getStylesAttributes'])
->disableOriginalConstructor()
->getMock();
$styleHelper->method('getStylesAttributes')->willReturn($styleAttributes);
$styleHelper->method('getCustomNumberFormats')->willReturn($customNumberFormats);
return $styleHelper;
}
/**
* @return void
*/
public function testShouldFormatNumericValueAsDateWithDefaultStyle()
{
$styleHelper = $this->getStyleHelperMock([]);
$shouldFormatAsDate = $styleHelper->shouldFormatNumericValueAsDate(0);
$this->assertFalse($shouldFormatAsDate);
}
/**
* @return void
*/
public function testShouldFormatNumericValueAsDateWhenStyleIdNotListed()
{
$styleHelper = $this->getStyleHelperMock([['applyNumberFormat' => true]]);
$shouldFormatAsDate = $styleHelper->shouldFormatNumericValueAsDate(1);
$this->assertFalse($shouldFormatAsDate);
}
/**
* @return void
*/
public function testShouldFormatNumericValueAsDateWhenShouldNotApplyNumberFormat()
{
$styleHelper = $this->getStyleHelperMock([[], ['applyNumberFormat' => false]]);
$shouldFormatAsDate = $styleHelper->shouldFormatNumericValueAsDate(1);
$this->assertFalse($shouldFormatAsDate);
}
/**
* @return void
*/
public function testShouldFormatNumericValueAsDateWithBuiltinDateFormats()
{
$builtinNumFmtIdsForDate = [14, 15, 16, 17, 18, 19, 20, 21, 22, 45, 46, 47];
foreach ($builtinNumFmtIdsForDate as $builtinNumFmtIdForDate) {
$styleHelper = $this->getStyleHelperMock([[], ['applyNumberFormat' => true, 'numFmtId' => $builtinNumFmtIdForDate]]);
$shouldFormatAsDate = $styleHelper->shouldFormatNumericValueAsDate(1);
$this->assertTrue($shouldFormatAsDate);
}
}
/**
* @return void
*/
public function testShouldFormatNumericValueAsDateWhenCustomNumberFormatNotFound()
{
$styleHelper = $this->getStyleHelperMock([[], ['applyNumberFormat' => true, 'numFmtId' => 165]], [166 => []]);
$shouldFormatAsDate = $styleHelper->shouldFormatNumericValueAsDate(1);
$this->assertFalse($shouldFormatAsDate);
}
/**
* @return array
*/
public function dataProviderForCustomDateFormats()
{
return [
// number format, expectedResult
['[$-409]dddd\,\ mmmm\ d\,\ yy', true],
['[$-409]d\-mmm\-yy;@', true],
['[$-409]d\-mmm\-yyyy;@', true],
['mm/dd/yy;@', true],
['[$-F800]dddd\,\ mmmm\ dd\,\ yyyy', true],
['m/d;@', true],
['m/d/yy;@', true],
['[$-409]d\-mmm;@', true],
['[$-409]dd\-mmm\-yy;@', true],
['[$-409]mmm\-yy;@', true],
['[$-409]mmmm\-yy;@', true],
['[$-409]mmmm\ d\,\ yyyy;@', true],
['[$-409]m/d/yy\ h:mm\ AM/PM;@', true],
['m/d/yy\ h:mm;@', true],
['[$-409]mmmmm;@', true],
['[$-409]mmmmm\-yy;@', true],
['m/d/yyyy;@', true],
['[$-409]m/d/yy\--h:mm;@', true],
['GENERAL', false],
['\ma\yb\e', false],
['[Red]foo;', false],
];
}
/**
* @dataProvider dataProviderForCustomDateFormats
*
* @param string $numberFormat
* @param bool $expectedResult
* @return void
*/
public function testShouldFormatNumericValueAsDateWithCustomDateFormats($numberFormat, $expectedResult)
{
$numFmtId = 165;
$styleHelper = $this->getStyleHelperMock([[], ['applyNumberFormat' => true, 'numFmtId' => $numFmtId]], [$numFmtId => $numberFormat]);
$shouldFormatAsDate = $styleHelper->shouldFormatNumericValueAsDate(1);
$this->assertEquals($expectedResult, $shouldFormatAsDate);
}
}
+46
View File
@@ -113,6 +113,52 @@ class ReaderTest extends \PHPUnit_Framework_TestCase
$this->assertEquals($expectedRows, $allRows); $this->assertEquals($expectedRows, $allRows);
} }
/**
* @return void
*/
public function testReadShouldSupportNumericTimestampFormattedDifferentlyAsDate()
{
// make sure dates are always created with the same timezone
date_default_timezone_set('UTC');
$allRows = $this->getAllRowsForFile('sheet_with_same_numeric_value_date_formatted_differently.xlsx');
$expectedDate = \DateTime::createFromFormat('Y-m-d H:i:s', '2015-01-01 00:00:00');
$expectedRows = [
array_fill(0, 10, $expectedDate),
array_fill(0, 10, $expectedDate),
array_fill(0, 10, $expectedDate),
array_merge(array_fill(0, 7, $expectedDate), ['', '', '']),
];
$this->assertEquals($expectedRows, $allRows);
}
/**
* @return void
*/
public function testReadShouldSupportDifferentDatesAsNumericTimestamp()
{
// make sure dates are always created with the same timezone
date_default_timezone_set('UTC');
$allRows = $this->getAllRowsForFile('sheet_with_different_numeric_value_dates.xlsx');
$expectedRows = [
[
\DateTime::createFromFormat('Y-m-d H:i:s', '2015-09-01 00:00:00'),
\DateTime::createFromFormat('Y-m-d H:i:s', '2015-09-02 00:00:00'),
\DateTime::createFromFormat('Y-m-d H:i:s', '2015-09-01 22:23:00'),
],
[
\DateTime::createFromFormat('Y-m-d H:i:s', '1900-02-28 23:59:59'),
\DateTime::createFromFormat('Y-m-d H:i:s', '1900-03-01 00:00:00'),
\DateTime::createFromFormat('Y-m-d H:i:s', '1900-02-28 11:00:00'), // 1900-02-29 should be converted to 1900-02-28
]
];
$this->assertEquals($expectedRows, $allRows);
}
/** /**
* @return void * @return void
*/ */