Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b69e28050b | ||
|
|
d6e8fe4b54 | ||
|
|
e321f30c3b | ||
|
|
c31373fb1a | ||
|
|
6c57125c0c | ||
|
|
30837f869d | ||
|
|
e60054f3c4 | ||
|
|
3ee7099c95 | ||
|
|
2b1160bb33 | ||
|
|
049fd990b9 | ||
|
|
d2ac54c578 | ||
|
|
0c90d102ef | ||
|
|
e39dcb3847 | ||
|
|
10d1140a95 | ||
|
|
48debbcbca | ||
|
|
f4d6fb87ee | ||
|
|
771afcb5f1 | ||
|
|
86e26632f6 | ||
|
|
8614f79da3 | ||
|
|
4827e56cac | ||
|
|
03e85ffc21 | ||
|
|
e4cc8b4eaa | ||
|
|
73341d06a9 | ||
|
|
209372462b | ||
|
|
4a5da2ad74 | ||
|
|
c48c07db99 | ||
|
|
a804be4844 | ||
|
|
4bfbb41c95 | ||
|
|
61b71bf379 |
No files matched your search
@@ -0,0 +1,11 @@
|
||||
# Ignore all test and documentation for archive
|
||||
/tests export-ignore
|
||||
/.editorconfig export-ignore
|
||||
/.gitattributes export-ignore
|
||||
/.gitignore export-ignore
|
||||
/.scrutinizer.yml export-ignore
|
||||
/.travis.yml export-ignore
|
||||
/composer.lock export-ignore
|
||||
/CONTRIBUTING.md export-ignore
|
||||
/logo.png export-ignore
|
||||
/phpunit.xml export-ignore
|
||||
@@ -110,6 +110,7 @@ use Box\Spout\Common\Type;
|
||||
$reader = ReaderFactory::create(Type::CSV);
|
||||
$reader->setFieldDelimiter('|');
|
||||
$reader->setFieldEnclosure('@');
|
||||
$reader->setEndOfLineCharacter("\r");
|
||||
```
|
||||
|
||||
Additionally, if you need to read non UTF-8 files, you can specify the encoding of your file this way:
|
||||
@@ -159,7 +160,7 @@ Font | Bold | `StyleBuilder::setFontBold()`
|
||||
| Strikethrough | `StyleBuilder::setFontStrikethrough()`
|
||||
| Font name | `StyleBuilder::setFontName('Arial')`
|
||||
| Font size | `StyleBuilder::setFontSize(14)`
|
||||
| Font color | `StyleBuilder::setFontSize(Color::BLUE)`<br>`StyleBuilder::setFontSize(Color::rgb(0, 128, 255))`
|
||||
| Font color | `StyleBuilder::setFontColor(Color::BLUE)`<br>`StyleBuilder::setFontColor(Color::rgb(0, 128, 255))`
|
||||
Alignment | Wrap text | `StyleBuilder::setShouldWrapText()`
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@
|
||||
},
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "2.4.x-dev"
|
||||
"dev-master": "2.5.x-dev"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -281,32 +281,14 @@ class GlobalFunctionsHelper
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around global function stream_get_line()
|
||||
* @see stream_get_line()
|
||||
* Wrapper around global function stream_get_wrappers()
|
||||
* @see stream_get_wrappers()
|
||||
*
|
||||
* @param resource $handle
|
||||
* @param int $length
|
||||
* @param string|void $ending
|
||||
* @return string|bool
|
||||
*/
|
||||
public function stream_get_line($handle, $length, $ending = null)
|
||||
{
|
||||
return stream_get_line($handle, $length, $ending);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around global function str_getcsv()
|
||||
* @see str_getcsv()
|
||||
*
|
||||
* @param string $input
|
||||
* @param string|void $delimiter
|
||||
* @param string|void $enclosure
|
||||
* @param string|void $escape
|
||||
* @return array
|
||||
*/
|
||||
public function str_getcsv($input, $delimiter = null, $enclosure = null, $escape = null)
|
||||
public function stream_get_wrappers()
|
||||
{
|
||||
return str_getcsv($input, $delimiter, $enclosure, $escape);
|
||||
return stream_get_wrappers();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -19,6 +19,13 @@ abstract class AbstractReader implements ReaderInterface
|
||||
/** @var \Box\Spout\Common\Helper\GlobalFunctionsHelper Helper to work with global functions */
|
||||
protected $globalFunctionsHelper;
|
||||
|
||||
/**
|
||||
* Returns whether stream wrappers are supported
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
abstract protected function doesSupportStreamWrapper();
|
||||
|
||||
/**
|
||||
* Opens the file at the given file path to make it ready to be read
|
||||
*
|
||||
@@ -62,6 +69,10 @@ abstract class AbstractReader implements ReaderInterface
|
||||
*/
|
||||
public function open($filePath)
|
||||
{
|
||||
if ($this->isStreamWrapper($filePath) && (!$this->doesSupportStreamWrapper() || !$this->isSupportedStreamWrapper($filePath))) {
|
||||
throw new IOException("Could not open $filePath for reading! Stream wrapper used is not supported for this type of file.");
|
||||
}
|
||||
|
||||
if (!$this->isPhpStream($filePath)) {
|
||||
// we skip the checks if the provided file path points to a PHP stream
|
||||
if (!$this->globalFunctionsHelper->file_exists($filePath)) {
|
||||
@@ -72,8 +83,7 @@ abstract class AbstractReader implements ReaderInterface
|
||||
}
|
||||
|
||||
try {
|
||||
// Need to use realpath to fix "Can't open file" on some Windows setup
|
||||
$fileRealPath = realpath($filePath);
|
||||
$fileRealPath = $this->getFileRealPath($filePath);
|
||||
$this->openReader($fileRealPath);
|
||||
$this->isStreamOpened = true;
|
||||
} catch (\Exception $exception) {
|
||||
@@ -81,6 +91,67 @@ abstract class AbstractReader implements ReaderInterface
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the real path of the given path.
|
||||
* If the given path is a valid stream wrapper, returns the path unchanged.
|
||||
*
|
||||
* @param string $filePath
|
||||
* @return string
|
||||
*/
|
||||
protected function getFileRealPath($filePath)
|
||||
{
|
||||
if ($this->isSupportedStreamWrapper($filePath)) {
|
||||
return $filePath;
|
||||
}
|
||||
|
||||
// Need to use realpath to fix "Can't open file" on some Windows setup
|
||||
return realpath($filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the scheme of the custom stream wrapper, if the path indicates a stream wrapper is used.
|
||||
* For example, php://temp => php, s3://path/to/file => s3...
|
||||
*
|
||||
* @param string $filePath Path of the file to be read
|
||||
* @return string|null The stream wrapper scheme or NULL if not a stream wrapper
|
||||
*/
|
||||
protected function getStreamWrapperScheme($filePath)
|
||||
{
|
||||
$streamScheme = null;
|
||||
if (preg_match('/^(\w+):\/\//', $filePath, $matches)) {
|
||||
$streamScheme = $matches[1];
|
||||
}
|
||||
return $streamScheme;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given path is an unsupported stream wrapper
|
||||
* (like local path, php://temp, mystream://foo/bar...).
|
||||
*
|
||||
* @param string $filePath Path of the file to be read
|
||||
* @return bool Whether the given path is an unsupported stream wrapper
|
||||
*/
|
||||
protected function isStreamWrapper($filePath)
|
||||
{
|
||||
return ($this->getStreamWrapperScheme($filePath) !== null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given path is an supported stream wrapper
|
||||
* (like php://temp, mystream://foo/bar...).
|
||||
* If the given path is a local path, returns true.
|
||||
*
|
||||
* @param string $filePath Path of the file to be read
|
||||
* @return bool Whether the given path is an supported stream wrapper
|
||||
*/
|
||||
protected function isSupportedStreamWrapper($filePath)
|
||||
{
|
||||
$streamScheme = $this->getStreamWrapperScheme($filePath);
|
||||
return ($streamScheme !== null) ?
|
||||
in_array($streamScheme, $this->globalFunctionsHelper->stream_get_wrappers()) :
|
||||
true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a path is a PHP stream (like php://output, php://memory, ...)
|
||||
*
|
||||
@@ -89,7 +160,8 @@ abstract class AbstractReader implements ReaderInterface
|
||||
*/
|
||||
protected function isPhpStream($filePath)
|
||||
{
|
||||
return (strpos($filePath, 'php://') === 0);
|
||||
$streamScheme = $this->getStreamWrapperScheme($filePath);
|
||||
return ($streamScheme === 'php');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -29,6 +29,12 @@ class Reader extends AbstractReader
|
||||
/** @var string Encoding of the CSV file to be read */
|
||||
protected $encoding = EncodingHelper::ENCODING_UTF8;
|
||||
|
||||
/** @var string Defines the End of line */
|
||||
protected $endOfLineCharacter = "\n";
|
||||
|
||||
/** @var string */
|
||||
protected $autoDetectLineEndings;
|
||||
|
||||
/**
|
||||
* Sets the field delimiter for the CSV.
|
||||
* Needs to be called before opening the reader.
|
||||
@@ -68,6 +74,29 @@ class Reader extends AbstractReader
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the EOL for the CSV.
|
||||
* Needs to be called before opening the reader.
|
||||
*
|
||||
* @param string $endOfLineCharacter used to properly get lines from the CSV file.
|
||||
* @return Reader
|
||||
*/
|
||||
public function setEndOfLineCharacter($endOfLineCharacter)
|
||||
{
|
||||
$this->endOfLineCharacter = $endOfLineCharacter;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether stream wrappers are supported
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function doesSupportStreamWrapper()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the file at the given path to make it ready to be read.
|
||||
* If setEncoding() was not called, it assumes that the file is encoded in UTF-8.
|
||||
@@ -78,6 +107,9 @@ class Reader extends AbstractReader
|
||||
*/
|
||||
protected function openReader($filePath)
|
||||
{
|
||||
$this->autoDetectLineEndings = ini_get('auto_detect_line_endings');
|
||||
ini_set('auto_detect_line_endings', '1');
|
||||
|
||||
$this->filePointer = $this->globalFunctionsHelper->fopen($filePath, 'r');
|
||||
if (!$this->filePointer) {
|
||||
throw new IOException("Could not open file $filePath for reading.");
|
||||
@@ -88,6 +120,7 @@ class Reader extends AbstractReader
|
||||
$this->fieldDelimiter,
|
||||
$this->fieldEnclosure,
|
||||
$this->encoding,
|
||||
$this->endOfLineCharacter,
|
||||
$this->globalFunctionsHelper
|
||||
);
|
||||
}
|
||||
@@ -113,5 +146,7 @@ class Reader extends AbstractReader
|
||||
if ($this->filePointer) {
|
||||
$this->globalFunctionsHelper->fclose($this->filePointer);
|
||||
}
|
||||
|
||||
ini_set('auto_detect_line_endings', $this->autoDetectLineEndings);
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ use Box\Spout\Common\Helper\EncodingHelper;
|
||||
class RowIterator implements IteratorInterface
|
||||
{
|
||||
/**
|
||||
* If no value is given to stream_get_line(), it defaults to 8192 (which may be too low).
|
||||
* If no value is given to fgetcsv(), 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;
|
||||
@@ -49,6 +49,9 @@ class RowIterator implements IteratorInterface
|
||||
/** @var string End of line delimiter, encoded using the same encoding as the CSV */
|
||||
protected $encodedEOLDelimiter;
|
||||
|
||||
/** @var string End of line delimiter, given by the user as input. */
|
||||
protected $inputEOLDelimiter;
|
||||
|
||||
/**
|
||||
* @param resource $filePointer Pointer to the CSV file to read
|
||||
* @param string $fieldDelimiter Character that delimits fields
|
||||
@@ -56,12 +59,13 @@ class RowIterator implements IteratorInterface
|
||||
* @param string $encoding Encoding of the CSV file to be read
|
||||
* @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper
|
||||
*/
|
||||
public function __construct($filePointer, $fieldDelimiter, $fieldEnclosure, $encoding, $globalFunctionsHelper)
|
||||
public function __construct($filePointer, $fieldDelimiter, $fieldEnclosure, $encoding, $endOfLineDelimiter, $globalFunctionsHelper)
|
||||
{
|
||||
$this->filePointer = $filePointer;
|
||||
$this->fieldDelimiter = $fieldDelimiter;
|
||||
$this->fieldEnclosure = $fieldEnclosure;
|
||||
$this->encoding = $encoding;
|
||||
$this->inputEOLDelimiter = $endOfLineDelimiter;
|
||||
$this->globalFunctionsHelper = $globalFunctionsHelper;
|
||||
|
||||
$this->encodingHelper = new EncodingHelper($globalFunctionsHelper);
|
||||
@@ -124,16 +128,12 @@ class RowIterator implements IteratorInterface
|
||||
}
|
||||
|
||||
do {
|
||||
$lineData = false;
|
||||
$utf8EncodedLineData = $this->getNextUTF8EncodedLine();
|
||||
if ($utf8EncodedLineData !== false) {
|
||||
$lineData = $this->globalFunctionsHelper->str_getcsv($utf8EncodedLineData, $this->fieldDelimiter, $this->fieldEnclosure);
|
||||
}
|
||||
$rowData = $this->getNextUTF8EncodedRow();
|
||||
$hasNowReachedEndOfFile = $this->globalFunctionsHelper->feof($this->filePointer);
|
||||
} while (($lineData === false && !$hasNowReachedEndOfFile) || $this->isEmptyLine($lineData));
|
||||
} while (($rowData === false && !$hasNowReachedEndOfFile) || $this->isEmptyLine($rowData));
|
||||
|
||||
if ($lineData !== false) {
|
||||
$this->rowDataBuffer = $lineData;
|
||||
if ($rowData !== false) {
|
||||
$this->rowDataBuffer = $rowData;
|
||||
$this->numReadRows++;
|
||||
} else {
|
||||
// If we reach this point, it means end of file was reached.
|
||||
@@ -143,24 +143,39 @@ class RowIterator implements IteratorInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next line, converted if necessary to UTF-8.
|
||||
* Neither fgets nor fgetcsv don't work with non UTF-8 data... so we need to do some things manually.
|
||||
* Returns the next row, converted if necessary to UTF-8.
|
||||
* As fgetcsv() does not manage correctly encoding for non UTF-8 data,
|
||||
* we remove manually whitespace with ltrim or rtrim (depending on the order of the bytes)
|
||||
*
|
||||
* @return string|false The next line for the current file pointer, encoded in UTF-8 or FALSE if nothing to read
|
||||
* @return array|false The row for the current file pointer, encoded in UTF-8 or FALSE if nothing to read
|
||||
* @throws \Box\Spout\Common\Exception\EncodingConversionException If unable to convert data to UTF-8
|
||||
*/
|
||||
protected function getNextUTF8EncodedLine()
|
||||
protected function getNextUTF8EncodedRow()
|
||||
{
|
||||
// Read until the EOL delimiter or EOF is reached. The delimiter's encoding needs to match the CSV's encoding.
|
||||
$encodedEOLDelimiter = $this->getEncodedEOLDelimiter();
|
||||
$encodedLineData = $this->globalFunctionsHelper->stream_get_line($this->filePointer, self::MAX_READ_BYTES_PER_LINE, $encodedEOLDelimiter);
|
||||
$encodedRowData = fgetcsv($this->filePointer, self::MAX_READ_BYTES_PER_LINE, $this->fieldDelimiter, $this->fieldEnclosure);
|
||||
if (false === $encodedRowData) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the line could have been read, it can be converted to UTF-8
|
||||
$utf8EncodedLineData = ($encodedLineData !== false) ?
|
||||
$this->encodingHelper->attemptConversionToUTF8($encodedLineData, $this->encoding) :
|
||||
false;
|
||||
foreach ($encodedRowData as $cellIndex => $cellValue) {
|
||||
switch($this->encoding) {
|
||||
case EncodingHelper::ENCODING_UTF16_LE:
|
||||
case EncodingHelper::ENCODING_UTF32_LE:
|
||||
// remove whitespace from the beginning of a string as fgetcsv() add extra whitespace when it try to explode non UTF-8 data
|
||||
$cellValue = ltrim($cellValue);
|
||||
break;
|
||||
|
||||
return $utf8EncodedLineData;
|
||||
case EncodingHelper::ENCODING_UTF16_BE:
|
||||
case EncodingHelper::ENCODING_UTF32_BE:
|
||||
// remove whitespace from the end of a string as fgetcsv() add extra whitespace when it try to explode non UTF-8 data
|
||||
$cellValue = rtrim($cellValue);
|
||||
break;
|
||||
}
|
||||
|
||||
$encodedRowData[$cellIndex] = $this->encodingHelper->attemptConversionToUTF8($cellValue, $this->encoding);
|
||||
}
|
||||
|
||||
return $encodedRowData;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -172,7 +187,7 @@ class RowIterator implements IteratorInterface
|
||||
protected function getEncodedEOLDelimiter()
|
||||
{
|
||||
if (!isset($this->encodedEOLDelimiter)) {
|
||||
$this->encodedEOLDelimiter = $this->encodingHelper->attemptConversionFromUTF8("\n", $this->encoding);
|
||||
$this->encodedEOLDelimiter = $this->encodingHelper->attemptConversionFromUTF8($this->inputEOLDelimiter, $this->encoding);
|
||||
}
|
||||
|
||||
return $this->encodedEOLDelimiter;
|
||||
|
||||
@@ -21,9 +21,9 @@ class Sheet implements SheetInterface
|
||||
* @param string $encoding Encoding of the CSV file to be read
|
||||
* @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper
|
||||
*/
|
||||
public function __construct($filePointer, $fieldDelimiter, $fieldEnclosure, $encoding, $globalFunctionsHelper)
|
||||
public function __construct($filePointer, $fieldDelimiter, $fieldEnclosure, $encoding, $endOfLineCharacter, $globalFunctionsHelper)
|
||||
{
|
||||
$this->rowIterator = new RowIterator($filePointer, $fieldDelimiter, $fieldEnclosure, $encoding, $globalFunctionsHelper);
|
||||
$this->rowIterator = new RowIterator($filePointer, $fieldDelimiter, $fieldEnclosure, $encoding, $endOfLineCharacter, $globalFunctionsHelper);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -25,9 +25,9 @@ class SheetIterator implements IteratorInterface
|
||||
* @param string $encoding Encoding of the CSV file to be read
|
||||
* @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper
|
||||
*/
|
||||
public function __construct($filePointer, $fieldDelimiter, $fieldEnclosure, $encoding, $globalFunctionsHelper)
|
||||
public function __construct($filePointer, $fieldDelimiter, $fieldEnclosure, $encoding, $endOfLineCharacter, $globalFunctionsHelper)
|
||||
{
|
||||
$this->sheet = new Sheet($filePointer, $fieldDelimiter, $fieldEnclosure, $encoding, $globalFunctionsHelper);
|
||||
$this->sheet = new Sheet($filePointer, $fieldDelimiter, $fieldEnclosure, $encoding, $endOfLineCharacter, $globalFunctionsHelper);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -47,7 +47,6 @@ 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
|
||||
@@ -119,7 +118,8 @@ class CellValueFormatter
|
||||
protected function formatFloatCellValue($node)
|
||||
{
|
||||
$nodeValue = $node->getAttribute(self::XML_ATTRIBUTE_VALUE);
|
||||
$cellValue = is_int($nodeValue) ? intval($nodeValue) : floatval($nodeValue);
|
||||
$nodeIntValue = intval($nodeValue);
|
||||
$cellValue = ($nodeIntValue == $nodeValue) ? $nodeIntValue : floatval($nodeValue);
|
||||
return $cellValue;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,16 @@ class Reader extends AbstractReader
|
||||
/** @var SheetIterator To iterator over the ODS sheets */
|
||||
protected $sheetIterator;
|
||||
|
||||
/**
|
||||
* Returns whether stream wrappers are supported
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function doesSupportStreamWrapper()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the file at the given file path to make it ready to be read.
|
||||
*
|
||||
|
||||
@@ -20,6 +20,7 @@ class RowIterator implements IteratorInterface
|
||||
const XML_NODE_TABLE = 'table:table';
|
||||
const XML_NODE_ROW = 'table:table-row';
|
||||
const XML_NODE_CELL = 'table:table-cell';
|
||||
const MAX_COLUMNS_EXCEL = 16384;
|
||||
|
||||
/** Definition of XML attribute used to parse data */
|
||||
const XML_ATTRIBUTE_NUM_COLUMNS_REPEATED = 'table:number-columns-repeated';
|
||||
@@ -127,22 +128,23 @@ class RowIterator implements IteratorInterface
|
||||
|
||||
} else if ($this->xmlReader->isPositionedOnEndingNode(self::XML_NODE_ROW)) {
|
||||
// End of the row description
|
||||
$isEmptyRow = ($numCellsRead <= 1 && empty($cellValue));
|
||||
$isEmptyRow = ($numCellsRead <= 1 && $this->isEmptyCellValue($cellValue));
|
||||
if ($isEmptyRow) {
|
||||
// skip empty rows
|
||||
$this->next();
|
||||
return;
|
||||
}
|
||||
|
||||
// Only add value if the last read cell is not empty or does not need to repeat cell values.
|
||||
// Only add the value if the last read cell is not a trailing empty cell repeater in Excel.
|
||||
// The current count of read columns is determined by counting the values in $rowData.
|
||||
// This is to avoid creating a lot of empty cells, as Excel adds a last empty "<table:table-cell>"
|
||||
// with a number-columns-repeated value equals to the number of (supported columns - used columns).
|
||||
// In Excel, the number of supported columns is 16384, but we don't want to returns rows with always 16384 cells.
|
||||
if (!empty($cellValue) || $numColumnsRepeated === 1) {
|
||||
// In Excel, the number of supported columns is 16384, but we don't want to returns rows with
|
||||
// always 16384 cells.
|
||||
if ((count($rowData) + $numColumnsRepeated) !== self::MAX_COLUMNS_EXCEL) {
|
||||
for ($i = 0; $i < $numColumnsRepeated; $i++) {
|
||||
$rowData[] = $cellValue;
|
||||
}
|
||||
|
||||
$this->numReadRows++;
|
||||
}
|
||||
break;
|
||||
@@ -181,6 +183,17 @@ class RowIterator implements IteratorInterface
|
||||
return $this->cellValueFormatter->extractAndFormatNodeValue($node);
|
||||
}
|
||||
|
||||
/**
|
||||
* empty() replacement that honours 0 as a valid value
|
||||
*
|
||||
* @param $value The cell value
|
||||
* @return bool
|
||||
*/
|
||||
protected function isEmptyCellValue($value)
|
||||
{
|
||||
return (!isset($value) || trim($value) === '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current element, from the buffer.
|
||||
* @link http://php.net/manual/en/iterator.current.php
|
||||
|
||||
@@ -19,7 +19,7 @@ class Sheet implements SheetInterface
|
||||
/** @var int ID of the sheet */
|
||||
protected $id;
|
||||
|
||||
/** @var int Index of the sheet, based on order of creation (zero-based) */
|
||||
/** @var int Index of the sheet, based on order in the workbook (zero-based) */
|
||||
protected $index;
|
||||
|
||||
/** @var string Name of the sheet */
|
||||
@@ -27,7 +27,7 @@ class Sheet implements SheetInterface
|
||||
|
||||
/**
|
||||
* @param XMLReader $xmlReader XML Reader, positioned on the "<table:table>" element
|
||||
* @param int $sheetIndex Index of the sheet, based on order of creation (zero-based)
|
||||
* @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)
|
||||
@@ -48,7 +48,7 @@ class Sheet implements SheetInterface
|
||||
|
||||
/**
|
||||
* @api
|
||||
* @return int Index of the sheet, based on order of creation (zero-based)
|
||||
* @return int Index of the sheet, based on order in the workbook (zero-based)
|
||||
*/
|
||||
public function getIndex()
|
||||
{
|
||||
|
||||
@@ -169,7 +169,8 @@ class CellValueFormatter
|
||||
if ($shouldFormatAsDate) {
|
||||
return $this->formatExcelTimestampValue(floatval($nodeValue));
|
||||
} else {
|
||||
return is_int($nodeValue) ? intval($nodeValue) : floatval($nodeValue);
|
||||
$nodeIntValue = intval($nodeValue);
|
||||
return ($nodeIntValue == $nodeValue) ? $nodeIntValue : floatval($nodeValue);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,9 +13,6 @@ use Box\Spout\Reader\XLSX\Sheet;
|
||||
*/
|
||||
class SheetHelper
|
||||
{
|
||||
/** Extension for XML files */
|
||||
const XML_EXTENSION = '.xml';
|
||||
|
||||
/** 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';
|
||||
@@ -79,9 +76,15 @@ class SheetHelper
|
||||
$sheetNode = $sheetNodes[$i];
|
||||
$sheetDataXMLFilePath = $sheetNode->getAttribute('PartName');
|
||||
|
||||
$sheets[] = $this->getSheetFromXML($sheetDataXMLFilePath, $i);
|
||||
$sheets[] = $this->getSheetFromXML($sheetDataXMLFilePath);
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
@@ -91,60 +94,37 @@ class SheetHelper
|
||||
* 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.
|
||||
*
|
||||
* If this piece of data can't be found by parsing the different XML files, the ID will default
|
||||
* to the sheet index, based on order in [Content_Types].xml. Similarly, the sheet's name will
|
||||
* default to the data sheet XML file name ("xl/worksheets/sheet2.xml" => "sheet2").
|
||||
*
|
||||
* @param string $sheetDataXMLFilePath Path of the sheet data XML file as in [Content_Types].xml
|
||||
* @param int $sheetIndexZeroBased Index of the sheet, based on order in [Content_Types].xml (zero-based)
|
||||
* @return \Box\Spout\Reader\XLSX\Sheet Sheet instance
|
||||
*/
|
||||
protected function getSheetFromXML($sheetDataXMLFilePath, $sheetIndexZeroBased)
|
||||
protected function getSheetFromXML($sheetDataXMLFilePath)
|
||||
{
|
||||
$sheetName = $this->getDefaultSheetName($sheetDataXMLFilePath);
|
||||
|
||||
/*
|
||||
* In [Content_Types].xml, the path is "/xl/worksheets/sheet1.xml"
|
||||
* In workbook.xml.rels, it is only "worksheets/sheet1.xml"
|
||||
*/
|
||||
// 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];
|
||||
|
||||
if (count($relationshipNodes) === 1) {
|
||||
$relationshipNode = $relationshipNodes[0];
|
||||
$sheetId = $relationshipNode->getAttribute('Id');
|
||||
$relationshipSheetId = $relationshipNode->getAttribute('Id');
|
||||
|
||||
$workbookXMLElement = $this->getWorkbookXMLAsXMLElement();
|
||||
$sheetNodes = $workbookXMLElement->xpath('//ns:sheet[@r:id="' . $sheetId . '"]');
|
||||
$workbookXMLElement = $this->getWorkbookXMLAsXMLElement();
|
||||
$sheetNodes = $workbookXMLElement->xpath('//ns:sheet[@r:id="' . $relationshipSheetId . '"]');
|
||||
$sheetNode = $sheetNodes[0];
|
||||
|
||||
if (count($sheetNodes) === 1) {
|
||||
$sheetNode = $sheetNodes[0];
|
||||
$escapedSheetName = $sheetNode->getAttribute('name');
|
||||
$escapedSheetName = $sheetNode->getAttribute('name');
|
||||
$sheetIdOneBased = $sheetNode->getAttribute('sheetId');
|
||||
$sheetIndexZeroBased = $sheetIdOneBased - 1;
|
||||
|
||||
/** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */
|
||||
$escaper = new \Box\Spout\Common\Escaper\XLSX();
|
||||
$sheetName = $escaper->unescape($escapedSheetName);
|
||||
}
|
||||
}
|
||||
/** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */
|
||||
$escaper = new \Box\Spout\Common\Escaper\XLSX();
|
||||
$sheetName = $escaper->unescape($escapedSheetName);
|
||||
|
||||
return new Sheet($this->filePath, $sheetDataXMLFilePath, $this->sharedStringsHelper, $sheetIndexZeroBased, $sheetName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the default name of the sheet whose data is located
|
||||
* at the given path.
|
||||
*
|
||||
* @param string $sheetDataXMLFilePath Path of the sheet data XML file
|
||||
* @return string The default sheet name
|
||||
*/
|
||||
protected function getDefaultSheetName($sheetDataXMLFilePath)
|
||||
{
|
||||
return $this->globalFunctionsHelper->basename($sheetDataXMLFilePath, self::XML_EXTENSION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a representation of the workbook.xml.rels file, ready to be parsed.
|
||||
* The returned value is cached.
|
||||
|
||||
@@ -37,6 +37,16 @@ class Reader extends AbstractReader
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether stream wrappers are supported
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function doesSupportStreamWrapper()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the file at the given file path to make it ready to be read.
|
||||
* It also parses the sharedStrings.xml file to get all the shared strings available in memory
|
||||
|
||||
@@ -15,7 +15,7 @@ class Sheet implements SheetInterface
|
||||
/** @var \Box\Spout\Reader\XLSX\RowIterator To iterate over sheet's rows */
|
||||
protected $rowIterator;
|
||||
|
||||
/** @var int Index of the sheet, based on order of creation (zero-based) */
|
||||
/** @var int Index of the sheet, based on order in the workbook (zero-based) */
|
||||
protected $index;
|
||||
|
||||
/** @var string Name of the sheet */
|
||||
@@ -25,7 +25,7 @@ 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 int $sheetIndex Index of the sheet, based on order of creation (zero-based)
|
||||
* @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)
|
||||
@@ -46,7 +46,7 @@ class Sheet implements SheetInterface
|
||||
|
||||
/**
|
||||
* @api
|
||||
* @return int Index of the sheet, based on order of creation (zero-based)
|
||||
* @return int Index of the sheet, based on order in the workbook (zero-based)
|
||||
*/
|
||||
public function getIndex()
|
||||
{
|
||||
|
||||
@@ -319,6 +319,11 @@ abstract class AbstractWriter implements WriterInterface
|
||||
public function close()
|
||||
{
|
||||
$this->closeWriter();
|
||||
|
||||
if (is_resource($this->filePointer)) {
|
||||
$this->globalFunctionsHelper->fclose($this->filePointer);
|
||||
}
|
||||
|
||||
$this->isWriterOpened = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,10 +96,6 @@ class Writer extends AbstractWriter
|
||||
*/
|
||||
protected function closeWriter()
|
||||
{
|
||||
if ($this->filePointer) {
|
||||
$this->globalFunctionsHelper->fclose($this->filePointer);
|
||||
}
|
||||
|
||||
$this->lastWrittenRowIndex = 0;
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@ class Sheet
|
||||
/** @var array Associative array [SHEET_INDEX] => [SHEET_NAME] keeping track of sheets' name to enforce uniqueness */
|
||||
protected static $SHEETS_NAME_USED = [];
|
||||
|
||||
/** @var int Index of the sheet, based on order of creation (zero-based) */
|
||||
/** @var int Index of the sheet, based on order in the workbook (zero-based) */
|
||||
protected $index;
|
||||
|
||||
/** @var string Name of the sheet */
|
||||
@@ -34,7 +34,7 @@ class Sheet
|
||||
protected $stringHelper;
|
||||
|
||||
/**
|
||||
* @param int $sheetIndex Index of the sheet, based on order of creation (zero-based)
|
||||
* @param int $sheetIndex Index of the sheet, based on order in the workbook (zero-based)
|
||||
*/
|
||||
public function __construct($sheetIndex)
|
||||
{
|
||||
@@ -45,7 +45,7 @@ class Sheet
|
||||
|
||||
/**
|
||||
* @api
|
||||
* @return int Index of the sheet, based on order of creation (zero-based)
|
||||
* @return int Index of the sheet, based on order in the workbook (zero-based)
|
||||
*/
|
||||
public function getIndex()
|
||||
{
|
||||
|
||||
@@ -146,7 +146,7 @@ EOD;
|
||||
$cellXML .= ' t="s"><v>' . $sharedStringId . '</v></c>';
|
||||
}
|
||||
} else if (CellHelper::isBoolean($cellValue)) {
|
||||
$cellXML .= ' t="b"><v>' . $cellValue . '</v></c>';
|
||||
$cellXML .= ' t="b"><v>' . intval($cellValue) . '</v></c>';
|
||||
} else if (CellHelper::isNumeric($cellValue)) {
|
||||
$cellXML .= '><v>' . $cellValue . '</v></c>';
|
||||
} else if (empty($cellValue)) {
|
||||
|
||||
@@ -204,6 +204,15 @@ class ReaderTest extends \PHPUnit_Framework_TestCase
|
||||
$this->assertEquals('This is, a comma', $allRows[0][0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function testReadShouldNotTruncateLineBreak()
|
||||
{
|
||||
$allRows = $this->getAllRowsForFile('csv_with_line_breaks.csv', ',');
|
||||
$this->assertEquals("This is,\na comma", $allRows[0][0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
@@ -343,6 +352,38 @@ class ReaderTest extends \PHPUnit_Framework_TestCase
|
||||
$this->assertEquals($expectedRows, $allRows);
|
||||
}
|
||||
|
||||
/**
|
||||
* https://github.com/box/spout/issues/184
|
||||
* @return void
|
||||
*/
|
||||
public function testReadShouldInludeRowsWithZerosOnly()
|
||||
{
|
||||
$allRows = $this->getAllRowsForFile('sheet_with_zeros_in_row.csv');
|
||||
|
||||
$expectedRows = [
|
||||
['A', 'B', 'C'],
|
||||
['1', '2', '3'],
|
||||
['0', '0', '0']
|
||||
];
|
||||
$this->assertEquals($expectedRows, $allRows, 'There should be only 3 rows, because zeros (0) are valid values');
|
||||
}
|
||||
|
||||
/**
|
||||
* https://github.com/box/spout/issues/184
|
||||
* @return void
|
||||
*/
|
||||
public function testReadShouldCreateOutputEmptyCellPreserved()
|
||||
{
|
||||
$allRows = $this->getAllRowsForFile('sheet_with_empty_cells.csv');
|
||||
|
||||
$expectedRows = [
|
||||
['A', 'B', 'C'],
|
||||
['0', '', ''],
|
||||
['1', '1', '']
|
||||
];
|
||||
$this->assertEquals($expectedRows, $allRows, 'There should be 3 rows, with equal length');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $fileName
|
||||
* @param string|void $fieldDelimiter
|
||||
@@ -377,4 +418,96 @@ class ReaderTest extends \PHPUnit_Framework_TestCase
|
||||
|
||||
return $allRows;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function dataProviderForTestReadCustomEOL()
|
||||
{
|
||||
return [
|
||||
['csv_with_CR_EOL.csv', "\r"],
|
||||
['csv_standard.csv', "\n"],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider dataProviderForTestReadCustomEOL
|
||||
*
|
||||
* @param string $fileName
|
||||
* @param string $customEOL
|
||||
* @return void
|
||||
*/
|
||||
public function testReadCustomEOLs($fileName, $customEOL)
|
||||
{
|
||||
$allRows = [];
|
||||
$resourcePath = $this->getResourcePath($fileName);
|
||||
|
||||
/** @var \Box\Spout\Reader\CSV\Reader $reader */
|
||||
$reader = ReaderFactory::create(Type::CSV);
|
||||
$reader
|
||||
->setEndOfLineCharacter($customEOL)
|
||||
->open($resourcePath);
|
||||
|
||||
foreach ($reader->getSheetIterator() as $sheet) {
|
||||
foreach ($sheet->getRowIterator() as $row) {
|
||||
$allRows[] = $row;
|
||||
}
|
||||
}
|
||||
|
||||
$reader->close();
|
||||
|
||||
$expectedRows = [
|
||||
['csv--11', 'csv--12', 'csv--13'],
|
||||
['csv--21', 'csv--22', 'csv--23'],
|
||||
['csv--31', 'csv--32', 'csv--33'],
|
||||
];
|
||||
$this->assertEquals($expectedRows, $allRows);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function testReadCustomStreamWrapper()
|
||||
{
|
||||
$allRows = [];
|
||||
$resourcePath = 'spout://csv_standard';
|
||||
|
||||
// register stream wrapper
|
||||
stream_wrapper_register('spout', SpoutTestStream::CLASS_NAME);
|
||||
|
||||
/** @var \Box\Spout\Reader\CSV\Reader $reader */
|
||||
$reader = ReaderFactory::create(Type::CSV);
|
||||
$reader->open($resourcePath);
|
||||
|
||||
foreach ($reader->getSheetIterator() as $sheet) {
|
||||
foreach ($sheet->getRowIterator() as $row) {
|
||||
$allRows[] = $row;
|
||||
}
|
||||
}
|
||||
|
||||
$reader->close();
|
||||
|
||||
$expectedRows = [
|
||||
['csv--11', 'csv--12', 'csv--13'],
|
||||
['csv--21', 'csv--22', 'csv--23'],
|
||||
['csv--31', 'csv--32', 'csv--33'],
|
||||
];
|
||||
$this->assertEquals($expectedRows, $allRows);
|
||||
|
||||
// cleanup
|
||||
stream_wrapper_unregister('spout');
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Box\Spout\Common\Exception\IOException
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testReadWithUnsupportedCustomStreamWrapper()
|
||||
{
|
||||
/** @var \Box\Spout\Reader\CSV\Reader $reader */
|
||||
$reader = ReaderFactory::create(Type::CSV);
|
||||
$reader->open('unsupported://foobar');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Reader\CSV;
|
||||
|
||||
/**
|
||||
* Class SpoutTestStream
|
||||
* Custom stream that reads CSV files located in the tests/resources/csv folder.
|
||||
* For example: spout://foobar will point to tests/resources/csv/foobar.csv
|
||||
*
|
||||
* @package Box\Spout\Reader\CSV
|
||||
*/
|
||||
class SpoutTestStream
|
||||
{
|
||||
const CLASS_NAME = __CLASS__;
|
||||
|
||||
const PATH_TO_CSV_RESOURCES = 'tests/resources/csv/';
|
||||
const CSV_EXTENSION = '.csv';
|
||||
|
||||
/** @var int $position */
|
||||
private $position;
|
||||
|
||||
/** @var resource $fileHandle */
|
||||
private $fileHandle;
|
||||
|
||||
/**
|
||||
* @param string $path
|
||||
* @param int $flag
|
||||
* @return array
|
||||
*/
|
||||
public function url_stat($path, $flag)
|
||||
{
|
||||
$filePath = $this->getFilePathFromStreamPath($path);
|
||||
return stat($filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $streamPath
|
||||
* @return string
|
||||
*/
|
||||
private function getFilePathFromStreamPath($streamPath)
|
||||
{
|
||||
$fileName = parse_url($streamPath, PHP_URL_HOST);
|
||||
return self::PATH_TO_CSV_RESOURCES . $fileName . self::CSV_EXTENSION;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $path
|
||||
* @param string $mode
|
||||
* @param int $options
|
||||
* @param string $opened_path
|
||||
* @return bool
|
||||
*/
|
||||
public function stream_open($path, $mode, $options, &$opened_path)
|
||||
{
|
||||
$this->position = 0;
|
||||
|
||||
// the path is like "spout://csv_name" so the actual file name correspond the name of the host.
|
||||
$filePath = $this->getFilePathFromStreamPath($path);
|
||||
$this->fileHandle = fopen($filePath, $mode);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $numBytes
|
||||
* @return string
|
||||
*/
|
||||
public function stream_read($numBytes)
|
||||
{
|
||||
$this->position += $numBytes;
|
||||
return fread($this->fileHandle, $numBytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function stream_tell()
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $offset
|
||||
* @param int|void $whence
|
||||
* @return bool
|
||||
*/
|
||||
public function stream_seek($offset, $whence = SEEK_SET)
|
||||
{
|
||||
$result = fseek($this->fileHandle, $offset, $whence);
|
||||
if ($result === -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($whence === SEEK_SET) {
|
||||
$this->position = $offset;
|
||||
} else if ($whence === SEEK_CUR) {
|
||||
$this->position += $offset;
|
||||
} else {
|
||||
// not implemented
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function stream_close()
|
||||
{
|
||||
return fclose($this->fileHandle);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function stream_eof()
|
||||
{
|
||||
return feof($this->fileHandle);
|
||||
}
|
||||
}
|
||||
@@ -363,6 +363,62 @@ class ReaderTest extends \PHPUnit_Framework_TestCase
|
||||
$this->assertEquals($expectedRows, $allRows);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Box\Spout\Common\Exception\IOException
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testReadWithUnsupportedCustomStreamWrapper()
|
||||
{
|
||||
/** @var \Box\Spout\Reader\ODS\Reader $reader */
|
||||
$reader = ReaderFactory::create(Type::ODS);
|
||||
$reader->open('unsupported://foobar');
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Box\Spout\Common\Exception\IOException
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testReadWithSupportedCustomStreamWrapper()
|
||||
{
|
||||
/** @var \Box\Spout\Reader\ODS\Reader $reader */
|
||||
$reader = ReaderFactory::create(Type::ODS);
|
||||
$reader->open('php://memory');
|
||||
}
|
||||
|
||||
/**
|
||||
* https://github.com/box/spout/issues/184
|
||||
* @return void
|
||||
*/
|
||||
public function testReadShouldInludeRowsWithZerosOnly()
|
||||
{
|
||||
$allRows = $this->getAllRowsForFile('sheet_with_zeros_in_row.ods');
|
||||
|
||||
$expectedRows = [
|
||||
['A', 'B', 'C'],
|
||||
['1', '2', '3'],
|
||||
['0', '0', '0']
|
||||
];
|
||||
$this->assertEquals($expectedRows, $allRows, 'There should be only 3 rows, because zeros (0) are valid values');
|
||||
}
|
||||
|
||||
/**
|
||||
* https://github.com/box/spout/issues/184
|
||||
* @return void
|
||||
*/
|
||||
public function testReadShouldCreateOutputEmptyCellPreserved()
|
||||
{
|
||||
$allRows = $this->getAllRowsForFile('sheet_with_empty_cells.ods');
|
||||
|
||||
$expectedRows = [
|
||||
['A', 'B', 'C'],
|
||||
['0', '', ''],
|
||||
['1', '1', '']
|
||||
];
|
||||
$this->assertEquals($expectedRows, $allRows, 'There should be 3 rows, with equal length');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $fileName
|
||||
* @return array All the read rows the given file
|
||||
|
||||
@@ -12,23 +12,26 @@ class CellValueFormatterTest extends \PHPUnit_Framework_TestCase
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function dataProviderForExcelDateTest()
|
||||
public function dataProviderForTestExcelDate()
|
||||
{
|
||||
return [
|
||||
[ CellValueFormatter::CELL_TYPE_NUMERIC, 42429, '2016-02-29 00:00:00' ],
|
||||
[ CellValueFormatter::CELL_TYPE_NUMERIC, '146098', '2299-12-31 00:00:00' ],
|
||||
[ CellValueFormatter::CELL_TYPE_NUMERIC, -700, null ],
|
||||
[ CellValueFormatter::CELL_TYPE_NUMERIC, 0, null ],
|
||||
[ CellValueFormatter::CELL_TYPE_NUMERIC, 0.5, null ],
|
||||
[ 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' ],
|
||||
[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, 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'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider dataProviderForExcelDateTest
|
||||
* @dataProvider dataProviderForTestExcelDate
|
||||
*
|
||||
* @param string $cellType
|
||||
* @param int|float|string $nodeValue
|
||||
* @param string|null $expectedDateAsString
|
||||
* @return void
|
||||
*/
|
||||
public function testExcelDate($cellType, $nodeValue, $expectedDateAsString)
|
||||
@@ -39,7 +42,7 @@ class CellValueFormatterTest extends \PHPUnit_Framework_TestCase
|
||||
->expects($this->atLeastOnce())
|
||||
->method('item')
|
||||
->with(0)
|
||||
->will($this->returnValue((object)[ 'nodeValue' => $nodeValue ]));
|
||||
->will($this->returnValue((object)['nodeValue' => $nodeValue]));
|
||||
|
||||
$nodeMock = $this->getMockBuilder('DOMElement')->disableOriginalConstructor()->getMock();
|
||||
|
||||
@@ -47,8 +50,8 @@ class CellValueFormatterTest extends \PHPUnit_Framework_TestCase
|
||||
->expects($this->atLeastOnce())
|
||||
->method('getAttribute')
|
||||
->will($this->returnValueMap([
|
||||
[ CellValueFormatter::XML_ATTRIBUTE_TYPE, $cellType ],
|
||||
[ CellValueFormatter::XML_ATTRIBUTE_STYLE_ID, 123 ],
|
||||
[CellValueFormatter::XML_ATTRIBUTE_TYPE, $cellType],
|
||||
[CellValueFormatter::XML_ATTRIBUTE_STYLE_ID, 123],
|
||||
]));
|
||||
|
||||
$nodeMock
|
||||
@@ -57,7 +60,7 @@ class CellValueFormatterTest extends \PHPUnit_Framework_TestCase
|
||||
->with(CellValueFormatter::XML_NODE_VALUE)
|
||||
->will($this->returnValue($nodeListMock));
|
||||
|
||||
$styleHelperMock = $this->getMockBuilder(__NAMESPACE__ . '\StyleHelper')->disableOriginalConstructor()->getMock();
|
||||
$styleHelperMock = $this->getMockBuilder('Box\Spout\Reader\XLSX\Helper\StyleHelper')->disableOriginalConstructor()->getMock();
|
||||
|
||||
$styleHelperMock
|
||||
->expects($this->once())
|
||||
@@ -65,9 +68,8 @@ class CellValueFormatterTest extends \PHPUnit_Framework_TestCase
|
||||
->with(123)
|
||||
->will($this->returnValue(true));
|
||||
|
||||
$instance = new CellValueFormatter(null, $styleHelperMock);
|
||||
|
||||
$result = $instance->extractAndFormatNodeValue($nodeMock);
|
||||
$formatter = new CellValueFormatter(null, $styleHelperMock);
|
||||
$result = $formatter->extractAndFormatNodeValue($nodeMock);
|
||||
|
||||
if ($expectedDateAsString === null) {
|
||||
$this->assertNull($result);
|
||||
@@ -77,4 +79,48 @@ class CellValueFormatterTest extends \PHPUnit_Framework_TestCase
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function dataProviderForTestFormatNumericCellValueWithNumbers()
|
||||
{
|
||||
return [
|
||||
[42, 42, 'integer'],
|
||||
[42.5, 42.5, 'double'],
|
||||
[-42, -42, 'integer'],
|
||||
[-42.5, -42.5, 'double'],
|
||||
['42', 42, 'integer'],
|
||||
['42.5', 42.5, 'double'],
|
||||
[865640023012945, 865640023012945, 'integer'],
|
||||
['865640023012945', 865640023012945, 'integer'],
|
||||
[865640023012945.5, 865640023012945.5, 'double'],
|
||||
['865640023012945.5', 865640023012945.5, 'double'],
|
||||
[PHP_INT_MAX, PHP_INT_MAX, 'integer'],
|
||||
[~PHP_INT_MAX + 1, ~PHP_INT_MAX + 1, 'integer'], // ~PHP_INT_MAX === PHP_INT_MIN, PHP_INT_MIN being PHP7+
|
||||
[PHP_INT_MAX + 1, PHP_INT_MAX + 1, 'double'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider dataProviderForTestFormatNumericCellValueWithNumbers
|
||||
*
|
||||
* @param int|float|string $value
|
||||
* @param int|float $expectedFormattedValue
|
||||
* @param string $expectedType
|
||||
* @return void
|
||||
*/
|
||||
public function testFormatNumericCellValueWithNumbers($value, $expectedFormattedValue, $expectedType)
|
||||
{
|
||||
$styleHelperMock = $this->getMockBuilder('Box\Spout\Reader\XLSX\Helper\StyleHelper')->disableOriginalConstructor()->getMock();
|
||||
$styleHelperMock
|
||||
->expects($this->once())
|
||||
->method('shouldFormatNumericValueAsDate')
|
||||
->will($this->returnValue(false));
|
||||
|
||||
$formatter = new CellValueFormatter(null, $styleHelperMock);
|
||||
$formattedValue = \ReflectionHelper::callMethodOnObject($formatter, 'formatNumericCellValue', $value, 0);
|
||||
|
||||
$this->assertEquals($expectedFormattedValue, $formattedValue);
|
||||
$this->assertEquals($expectedType, gettype($formattedValue));
|
||||
}
|
||||
}
|
||||
@@ -73,6 +73,28 @@ class ReaderTest extends \PHPUnit_Framework_TestCase
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function testReadShouldSupportSheetsDefinitionInRandomOrder()
|
||||
{
|
||||
$allRows = $this->getAllRowsForFile('two_sheets_with_sheets_definition_in_reverse_order.xlsx');
|
||||
|
||||
$expectedRows = [
|
||||
['s1 - A1', 's1 - B1', 's1 - C1', 's1 - D1', 's1 - E1'],
|
||||
['s1 - A2', 's1 - B2', 's1 - C2', 's1 - D2', 's1 - E2'],
|
||||
['s1 - A3', 's1 - B3', 's1 - C3', 's1 - D3', 's1 - E3'],
|
||||
['s1 - A4', 's1 - B4', 's1 - C4', 's1 - D4', 's1 - E4'],
|
||||
['s1 - A5', 's1 - B5', 's1 - C5', 's1 - D5', 's1 - E5'],
|
||||
['s2 - A1', 's2 - B1', 's2 - C1', 's2 - D1', 's2 - E1'],
|
||||
['s2 - A2', 's2 - B2', 's2 - C2', 's2 - D2', 's2 - E2'],
|
||||
['s2 - A3', 's2 - B3', 's2 - C3', 's2 - D3', 's2 - E3'],
|
||||
['s2 - A4', 's2 - B4', 's2 - C4', 's2 - D4', 's2 - E4'],
|
||||
['s2 - A5', 's2 - B5', 's2 - C5', 's2 - D5', 's2 - E5'],
|
||||
];
|
||||
$this->assertEquals($expectedRows, $allRows);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
@@ -382,6 +404,63 @@ class ReaderTest extends \PHPUnit_Framework_TestCase
|
||||
$this->assertEquals($expectedRows, $allRows);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Box\Spout\Common\Exception\IOException
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testReadWithUnsupportedCustomStreamWrapper()
|
||||
{
|
||||
/** @var \Box\Spout\Reader\XLSX\Reader $reader */
|
||||
$reader = ReaderFactory::create(Type::XLSX);
|
||||
$reader->open('unsupported://foobar');
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Box\Spout\Common\Exception\IOException
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testReadWithSupportedCustomStreamWrapper()
|
||||
{
|
||||
/** @var \Box\Spout\Reader\XLSX\Reader $reader */
|
||||
$reader = ReaderFactory::create(Type::XLSX);
|
||||
$reader->open('php://memory');
|
||||
}
|
||||
|
||||
/**
|
||||
* https://github.com/box/spout/issues/184
|
||||
* @return void
|
||||
*/
|
||||
public function testReadShouldInludeRowsWithZerosOnly()
|
||||
{
|
||||
$allRows = $this->getAllRowsForFile('sheet_with_zeros_in_row.xlsx');
|
||||
|
||||
$expectedRows = [
|
||||
['A', 'B', 'C'],
|
||||
['1', '2', '3'],
|
||||
['0', '0', '0']
|
||||
];
|
||||
$this->assertEquals($expectedRows, $allRows, 'There should be only 3 rows, because zeros (0) are valid values');
|
||||
}
|
||||
|
||||
/**
|
||||
* https://github.com/box/spout/issues/184
|
||||
* @return void
|
||||
*/
|
||||
public function testReadShouldCreateOutputEmptyCellPreserved()
|
||||
{
|
||||
$allRows = $this->getAllRowsForFile('sheet_with_empty_cells.xlsx');
|
||||
|
||||
$expectedRows = [
|
||||
['A', 'B', 'C'],
|
||||
['0', '', ''],
|
||||
['1', '1', '']
|
||||
];
|
||||
$this->assertEquals($expectedRows, $allRows, 'There should be 3 rows, with equal length');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param string $fileName
|
||||
* @return array All the read rows the given file
|
||||
|
||||
@@ -3,6 +3,7 @@ require_once(dirname(__DIR__) . '/vendor/autoload.php');
|
||||
|
||||
require_once(dirname(__DIR__) . '/tests/Spout/TestUsingResource.php');
|
||||
require_once(dirname(__DIR__) . '/tests/Spout/ReflectionHelper.php');
|
||||
require_once(dirname(__DIR__) . '/tests/Spout/Reader/CSV/SpoutTestStream.php');
|
||||
|
||||
// Make sure a timezone is set to be able to work with dates
|
||||
date_default_timezone_set('UTC');
|
||||
@@ -0,0 +1 @@
|
||||
csv--11,csv--12,csv--13␍csv--21,csv--22,csv--23␍csv--31,csv--32,csv--33
|
||||
|
@@ -0,0 +1,2 @@
|
||||
"This is,
|
||||
a comma",csv--12
|
||||
|
@@ -0,0 +1,3 @@
|
||||
"A","B","C"
|
||||
0,,
|
||||
1,1,
|
||||
|
@@ -0,0 +1,3 @@
|
||||
"A","B","C"
|
||||
1,2,3
|
||||
0,0,0
|
||||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in new issue
Block a user