Merge branch 'master' into custom-column-widths
This commit is contained in:
commit
77189d72e6
@ -8,9 +8,9 @@
|
||||
[](https://packagist.org/packages/box/spout)
|
||||
|
||||
Spout is a PHP library to read and write spreadsheet files (CSV, XLSX and ODS), in a fast and scalable way.
|
||||
Contrary to other file readers or writers, it is capable of processing very large files while keeping the memory usage really low (less than 3MB).
|
||||
Unlike other file readers or writers, it is capable of processing very large files, while keeping the memory usage really low (less than 3MB).
|
||||
|
||||
Join the community and come discuss about Spout: [](https://gitter.im/box/spout?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
|
||||
Join the community and come discuss Spout: [](https://gitter.im/box/spout?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
|
||||
|
||||
|
||||
## Documentation
|
||||
@ -43,7 +43,7 @@ For information, the performance tests take about 10 minutes to run (processing
|
||||
|
||||
## Support
|
||||
|
||||
You can ask questions, submit new features ideas or discuss about Spout in the chat room:<br>
|
||||
You can ask questions, submit new features ideas or discuss Spout in the chat room:<br>
|
||||
[](https://gitter.im/box/spout?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
|
||||
|
||||
## Copyright and License
|
||||
|
@ -14,7 +14,8 @@
|
||||
"require": {
|
||||
"php": ">=7.2.0",
|
||||
"ext-zip": "*",
|
||||
"ext-xmlreader" : "*"
|
||||
"ext-xmlreader": "*",
|
||||
"ext-dom": "*"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^8",
|
||||
|
@ -116,18 +116,19 @@ $reader->setShouldPreserveEmptyRows(true);
|
||||
|
||||
For fonts and alignments, {{ site.spout_html }} does not support all the possible formatting options yet. But you can find the most important ones:
|
||||
|
||||
| Category | Property | API
|
||||
|:----------|:---------------|:--------------------------------------
|
||||
| Font | Bold | `StyleBuilder::setFontBold()`
|
||||
| | Italic | `StyleBuilder::setFontItalic()`
|
||||
| | Underline | `StyleBuilder::setFontUnderline()`
|
||||
| | Strikethrough | `StyleBuilder::setFontStrikethrough()`
|
||||
| | Font name | `StyleBuilder::setFontName('Arial')`
|
||||
| | Font size | `StyleBuilder::setFontSize(14)`
|
||||
| | Font color | `StyleBuilder::setFontColor(Color::BLUE)`<br>`StyleBuilder::setFontColor(Color::rgb(0, 128, 255))`
|
||||
| Alignment | Cell alignment | `StyleBuilder::setCellAlignment(CellAlignment::CENTER)`
|
||||
| | Wrap text | `StyleBuilder::setShouldWrapText(true)`
|
||||
|
||||
| Category | Property | API
|
||||
|:---------------------|:---------------|:--------------------------------------
|
||||
| Font | Bold | `StyleBuilder::setFontBold()`
|
||||
| | Italic | `StyleBuilder::setFontItalic()`
|
||||
| | Underline | `StyleBuilder::setFontUnderline()`
|
||||
| | Strikethrough | `StyleBuilder::setFontStrikethrough()`
|
||||
| | Font name | `StyleBuilder::setFontName('Arial')`
|
||||
| | Font size | `StyleBuilder::setFontSize(14)`
|
||||
| | Font color | `StyleBuilder::setFontColor(Color::BLUE)`<br>`StyleBuilder::setFontColor(Color::rgb(0, 128, 255))`
|
||||
| Alignment | Cell alignment | `StyleBuilder::setCellAlignment(CellAlignment::CENTER)`
|
||||
| | Wrap text | `StyleBuilder::setShouldWrapText(true)`
|
||||
| Format _(XLSX only)_ | Number format | `StyleBuilder::setFormat('0.000')`
|
||||
| | Date format | `StyleBuilder::setFormat('m/d/yy h:mm')`
|
||||
|
||||
### Styling rows
|
||||
|
||||
|
@ -65,7 +65,7 @@ class Cell
|
||||
protected $style;
|
||||
|
||||
/**
|
||||
* @param $value mixed
|
||||
* @param mixed|null $value
|
||||
* @param Style|null $style
|
||||
*/
|
||||
public function __construct($value, Style $style = null)
|
||||
|
@ -13,12 +13,20 @@ class StringHelper
|
||||
/** @var bool Whether the mbstring extension is loaded */
|
||||
protected $hasMbstringSupport;
|
||||
|
||||
/** @var bool Whether the code is running with PHP7 or older versions */
|
||||
private $isRunningPhp7OrOlder;
|
||||
|
||||
/** @var array Locale info, used for number formatting */
|
||||
private $localeInfo;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->hasMbstringSupport = \extension_loaded('mbstring');
|
||||
$this->isRunningPhp7OrOlder = \version_compare(PHP_VERSION, '8.0.0') < 0;
|
||||
$this->localeInfo = \localeconv();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -68,4 +76,30 @@ class StringHelper
|
||||
|
||||
return ($position !== false) ? $position : -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a numeric value (int or float) in a way that's compatible with the expected spreadsheet format.
|
||||
*
|
||||
* Formatting of float values is locale dependent in PHP < 8.
|
||||
* Thousands separators and decimal points vary from locale to locale (en_US: 12.34 vs pl_PL: 12,34).
|
||||
* However, float values must be formatted with no thousands separator and a "." as decimal point
|
||||
* to work properly. This method can be used to convert the value to the correct format before storing it.
|
||||
*
|
||||
* @see https://wiki.php.net/rfc/locale_independent_float_to_string for the changed behavior in PHP8.
|
||||
*
|
||||
* @param int|float $numericValue
|
||||
* @return string
|
||||
*/
|
||||
public function formatNumericValue($numericValue)
|
||||
{
|
||||
if ($this->isRunningPhp7OrOlder && is_float($numericValue)) {
|
||||
return str_replace(
|
||||
[$this->localeInfo['thousands_sep'], $this->localeInfo['decimal_point']],
|
||||
['', '.'],
|
||||
$numericValue
|
||||
);
|
||||
}
|
||||
|
||||
return $numericValue;
|
||||
}
|
||||
}
|
||||
|
@ -56,12 +56,25 @@ class RowManager
|
||||
$rowCells = $row->getCells();
|
||||
$maxCellIndex = $numCells;
|
||||
|
||||
// If the row has empty cells, calling "setCellAtIndex" will add the cell
|
||||
// but in the wrong place (the new cell is added at the end of the array).
|
||||
// Therefore, we need to sort the array using keys to have proper order.
|
||||
// @see https://github.com/box/spout/issues/740
|
||||
$needsSorting = false;
|
||||
|
||||
for ($cellIndex = 0; $cellIndex < $maxCellIndex; $cellIndex++) {
|
||||
if (!isset($rowCells[$cellIndex])) {
|
||||
$row->setCellAtIndex($this->entityFactory->createCell(''), $cellIndex);
|
||||
$needsSorting = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($needsSorting) {
|
||||
$rowCells = $row->getCells();
|
||||
ksort($rowCells);
|
||||
$row->setCells($rowCells);
|
||||
}
|
||||
|
||||
return $row;
|
||||
}
|
||||
}
|
||||
|
@ -31,14 +31,6 @@ 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...
|
||||
* @see https://en.wikipedia.org/wiki/Year_1900_problem#Microsoft_Excel
|
||||
*/
|
||||
const ERRONEOUS_EXCEL_LEAP_YEAR_DAY = 60;
|
||||
|
||||
/** @var SharedStringsManager Manages shared strings */
|
||||
protected $sharedStringsManager;
|
||||
@ -130,10 +122,15 @@ class CellValueFormatter
|
||||
*/
|
||||
protected function formatInlineStringCellValue($node)
|
||||
{
|
||||
// inline strings are formatted this way:
|
||||
// <c r="A1" t="inlineStr"><is><t>[INLINE_STRING]</t></is></c>
|
||||
$tNode = $node->getElementsByTagName(self::XML_NODE_INLINE_STRING_VALUE)->item(0);
|
||||
$cellValue = $this->escaper->unescape($tNode->nodeValue);
|
||||
// inline strings are formatted this way (they can contain any number of <t> nodes):
|
||||
// <c r="A1" t="inlineStr"><is><t>[INLINE_STRING]</t><t>[INLINE_STRING_2]</t></is></c>
|
||||
$tNodes = $node->getElementsByTagName(self::XML_NODE_INLINE_STRING_VALUE);
|
||||
|
||||
$cellValue = '';
|
||||
for ($i = 0; $i < $tNodes->count(); $i++) {
|
||||
$tNode = $tNodes->item($i);
|
||||
$cellValue .= $this->escaper->unescape($tNode->nodeValue);
|
||||
}
|
||||
|
||||
return $cellValue;
|
||||
}
|
||||
|
@ -16,9 +16,6 @@ use Box\Spout\Reader\XLSX\Manager\SharedStringsCaching\CachingStrategyInterface;
|
||||
*/
|
||||
class SharedStringsManager
|
||||
{
|
||||
/** Main namespace for the sharedStrings.xml file */
|
||||
const MAIN_NAMESPACE_FOR_SHARED_STRINGS_XML = 'http://schemas.openxmlformats.org/spreadsheetml/2006/main';
|
||||
|
||||
/** Definition of XML nodes names used to parse data */
|
||||
const XML_NODE_SST = 'sst';
|
||||
const XML_NODE_SI = 'si';
|
||||
|
@ -17,10 +17,11 @@ class WorkbookRelationshipsManager
|
||||
/** Path of workbook relationships XML file inside the XLSX file */
|
||||
const WORKBOOK_RELS_XML_FILE_PATH = 'xl/_rels/workbook.xml.rels';
|
||||
|
||||
/** Relationships types */
|
||||
/** Relationships types - For Transitional and Strict OOXML */
|
||||
const RELATIONSHIP_TYPE_SHARED_STRINGS = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings';
|
||||
const RELATIONSHIP_TYPE_STYLES = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles';
|
||||
const RELATIONSHIP_TYPE_WORKSHEET = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet';
|
||||
const RELATIONSHIP_TYPE_SHARED_STRINGS_STRICT = 'http://purl.oclc.org/ooxml/officeDocument/relationships/sharedStrings';
|
||||
const RELATIONSHIP_TYPE_STYLES_STRICT = 'http://purl.oclc.org/ooxml/officeDocument/relationships/styles';
|
||||
|
||||
/** Nodes and attributes used to find relevant information in the workbook relationships XML file */
|
||||
const XML_NODE_RELATIONSHIP = 'Relationship';
|
||||
@ -52,7 +53,8 @@ class WorkbookRelationshipsManager
|
||||
public function getSharedStringsXMLFilePath()
|
||||
{
|
||||
$workbookRelationships = $this->getWorkbookRelationships();
|
||||
$sharedStringsXMLFilePath = $workbookRelationships[self::RELATIONSHIP_TYPE_SHARED_STRINGS];
|
||||
$sharedStringsXMLFilePath = $workbookRelationships[self::RELATIONSHIP_TYPE_SHARED_STRINGS]
|
||||
?? $workbookRelationships[self::RELATIONSHIP_TYPE_SHARED_STRINGS_STRICT];
|
||||
|
||||
// the file path can be relative (e.g. "styles.xml") or absolute (e.g. "/xl/styles.xml")
|
||||
$doesContainBasePath = (\strpos($sharedStringsXMLFilePath, self::BASE_PATH) !== false);
|
||||
@ -71,7 +73,8 @@ class WorkbookRelationshipsManager
|
||||
{
|
||||
$workbookRelationships = $this->getWorkbookRelationships();
|
||||
|
||||
return isset($workbookRelationships[self::RELATIONSHIP_TYPE_SHARED_STRINGS]);
|
||||
return isset($workbookRelationships[self::RELATIONSHIP_TYPE_SHARED_STRINGS])
|
||||
|| isset($workbookRelationships[self::RELATIONSHIP_TYPE_SHARED_STRINGS_STRICT]);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -81,7 +84,8 @@ class WorkbookRelationshipsManager
|
||||
{
|
||||
$workbookRelationships = $this->getWorkbookRelationships();
|
||||
|
||||
return isset($workbookRelationships[self::RELATIONSHIP_TYPE_STYLES]);
|
||||
return isset($workbookRelationships[self::RELATIONSHIP_TYPE_STYLES])
|
||||
|| isset($workbookRelationships[self::RELATIONSHIP_TYPE_STYLES_STRICT]);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -90,7 +94,8 @@ class WorkbookRelationshipsManager
|
||||
public function getStylesXMLFilePath()
|
||||
{
|
||||
$workbookRelationships = $this->getWorkbookRelationships();
|
||||
$stylesXMLFilePath = $workbookRelationships[self::RELATIONSHIP_TYPE_STYLES];
|
||||
$stylesXMLFilePath = $workbookRelationships[self::RELATIONSHIP_TYPE_STYLES]
|
||||
?? $workbookRelationships[self::RELATIONSHIP_TYPE_STYLES_STRICT];
|
||||
|
||||
// the file path can be relative (e.g. "styles.xml") or absolute (e.g. "/xl/styles.xml")
|
||||
$doesContainBasePath = (\strpos($stylesXMLFilePath, self::BASE_PATH) !== false);
|
||||
|
@ -231,8 +231,9 @@ class WorksheetManager implements WorksheetManagerInterface
|
||||
$data .= '<text:p>' . $cell->getValue() . '</text:p>';
|
||||
$data .= '</table:table-cell>';
|
||||
} elseif ($cell->isNumeric()) {
|
||||
$data .= ' office:value-type="float" calcext:value-type="float" office:value="' . $cell->getValue() . '">';
|
||||
$data .= '<text:p>' . $cell->getValue() . '</text:p>';
|
||||
$cellValue = $this->stringHelper->formatNumericValue($cell->getValue());
|
||||
$data .= ' office:value-type="float" calcext:value-type="float" office:value="' . $cellValue . '">';
|
||||
$data .= '<text:p>' . $cellValue . '</text:p>';
|
||||
$data .= '</table:table-cell>';
|
||||
} elseif ($cell->isError() && is_string($cell->getValueEvenIfError())) {
|
||||
// only writes the error value if it's a string
|
||||
|
@ -265,7 +265,7 @@ EOD;
|
||||
} elseif ($cell->isBoolean()) {
|
||||
$cellXML .= ' t="b"><v>' . (int) ($cell->getValue()) . '</v></c>';
|
||||
} elseif ($cell->isNumeric()) {
|
||||
$cellXML .= '><v>' . $cell->getValue() . '</v></c>';
|
||||
$cellXML .= '><v>' . $this->stringHelper->formatNumericValue($cell->getValue()) . '</v></c>';
|
||||
} elseif ($cell->isError() && is_string($cell->getValueEvenIfError())) {
|
||||
// only writes the error value if it's a string
|
||||
$cellXML .= ' t="e"><v>' . $cell->getValueEvenIfError() . '</v></c>';
|
||||
|
@ -186,18 +186,22 @@ class CellValueFormatterTest extends TestCase
|
||||
public function testFormatInlineStringCellValue($value, $expectedFormattedValue)
|
||||
{
|
||||
$nodeListMock = $this->createMock(\DOMNodeList::class);
|
||||
$nodeListMock
|
||||
->expects($this->atLeastOnce())
|
||||
->method('count')
|
||||
->willReturn(1);
|
||||
$nodeListMock
|
||||
->expects($this->atLeastOnce())
|
||||
->method('item')
|
||||
->with(0)
|
||||
->will($this->returnValue((object) ['nodeValue' => $value]));
|
||||
->willReturn((object) ['nodeValue' => $value]);
|
||||
|
||||
$nodeMock = $this->createMock(\DOMElement::class);
|
||||
$nodeMock
|
||||
->expects($this->atLeastOnce())
|
||||
->method('getElementsByTagName')
|
||||
->with(CellValueFormatter::XML_NODE_INLINE_STRING_VALUE)
|
||||
->will($this->returnValue($nodeListMock));
|
||||
->willReturn($nodeListMock);
|
||||
|
||||
$formatter = new CellValueFormatter(null, null, false, false, new Escaper\XLSX());
|
||||
$formattedValue = \ReflectionHelper::callMethodOnObject($formatter, 'formatInlineStringCellValue', $nodeMock);
|
||||
|
@ -72,6 +72,19 @@ class ReaderTest extends TestCase
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function testReadShouldSupportInlineStringsWithMultipleValueNodes()
|
||||
{
|
||||
$allRows = $this->getAllRowsForFile('sheet_with_multiple_value_nodes_in_inline_strings.xlsx');
|
||||
|
||||
$expectedRows = [
|
||||
['VALUE 1 VALUE 2 VALUE 3 VALUE 4', 's1 - B1'],
|
||||
];
|
||||
$this->assertEquals($expectedRows, $allRows);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
@ -679,13 +692,29 @@ class ReaderTest extends TestCase
|
||||
$allRows = $this->getAllRowsForFile('sheet_with_empty_cells.xlsx');
|
||||
|
||||
$expectedRows = [
|
||||
['A', 'B', 'C'],
|
||||
['A', '', 'C'],
|
||||
['0', '', ''],
|
||||
['1', '1', ''],
|
||||
];
|
||||
$this->assertEquals($expectedRows, $allRows, 'There should be 3 rows, with equal length');
|
||||
}
|
||||
|
||||
/**
|
||||
* https://github.com/box/spout/issues/184
|
||||
* @return void
|
||||
*/
|
||||
public function testReadShouldCreateOutputEmptyCellPreservedWhenNoDimensionsSpecified()
|
||||
{
|
||||
$allRows = $this->getAllRowsForFile('sheet_with_empty_cells_without_dimensions.xlsx');
|
||||
|
||||
$expectedRows = [
|
||||
['A', '', 'C'],
|
||||
['0'],
|
||||
['1', '1'],
|
||||
];
|
||||
$this->assertEquals($expectedRows, $allRows);
|
||||
}
|
||||
|
||||
/**
|
||||
* https://github.com/box/spout/issues/195
|
||||
* @return void
|
||||
@ -703,6 +732,18 @@ class ReaderTest extends TestCase
|
||||
$this->assertEquals($expectedRows, $allRows, 'Cell values should not be trimmed');
|
||||
}
|
||||
|
||||
/**
|
||||
* https://github.com/box/spout/issues/726
|
||||
* @return void
|
||||
*/
|
||||
public function testReaderShouldSupportStrictOOXML()
|
||||
{
|
||||
$allRows = $this->getAllRowsForFile('sheet_with_strict_ooxml.xlsx');
|
||||
|
||||
$this->assertEquals('UNIQUE_ACCOUNT_IDENTIFIER', $allRows[0][0]);
|
||||
$this->assertEquals('A2Z34NJA7N2ESJ', $allRows[1][0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $fileName
|
||||
* @param bool $shouldFormatDates
|
||||
|
@ -279,6 +279,41 @@ class WriterTest extends TestCase
|
||||
$this->assertValueWasWrittenToSheet($fileName, 1, 10.2);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function testAddRowShouldSupportFloatValuesInDifferentLocale()
|
||||
{
|
||||
$previousLocale = \setlocale(LC_ALL, 0);
|
||||
|
||||
try {
|
||||
// Pick a supported locale whose decimal point is a comma.
|
||||
// Installed locales differ from one system to another, so we can't pick
|
||||
// a given locale.
|
||||
$supportedLocales = explode("\n", shell_exec('locale -a'));
|
||||
foreach ($supportedLocales as $supportedLocale) {
|
||||
\setlocale(LC_ALL, $supportedLocale);
|
||||
if (\localeconv()['decimal_point'] === ',') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
$this->assertEquals(',', \localeconv()['decimal_point']);
|
||||
|
||||
$fileName = 'test_add_row_should_support_float_values_in_different_locale.xlsx';
|
||||
$dataRows = $this->createRowsFromValues([
|
||||
[1234.5],
|
||||
]);
|
||||
|
||||
$this->writeToODSFile($dataRows, $fileName);
|
||||
|
||||
$this->assertValueWasNotWrittenToSheet($fileName, 1, "1234,5");
|
||||
$this->assertValueWasWrittenToSheet($fileName, 1, "1234.5");
|
||||
} finally {
|
||||
// reset locale
|
||||
\setlocale(LC_ALL, $previousLocale);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
|
@ -393,6 +393,42 @@ class WriterTest extends TestCase
|
||||
$this->assertInlineDataWasWrittenToSheet($fileName, 1, 't="e"><v>#DIV/0</v>');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function testAddRowShouldSupportFloatValuesInDifferentLocale()
|
||||
{
|
||||
$previousLocale = \setlocale(LC_ALL, 0);
|
||||
$valueToWrite = 1234.5; // needs to be defined before changing the locale as PHP8 would expect 1234,5
|
||||
|
||||
try {
|
||||
// Pick a supported locale whose decimal point is a comma.
|
||||
// Installed locales differ from one system to another, so we can't pick
|
||||
// a given locale.
|
||||
$supportedLocales = explode("\n", shell_exec('locale -a'));
|
||||
foreach ($supportedLocales as $supportedLocale) {
|
||||
\setlocale(LC_ALL, $supportedLocale);
|
||||
if (\localeconv()['decimal_point'] === ',') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
$this->assertEquals(',', \localeconv()['decimal_point']);
|
||||
|
||||
$fileName = 'test_add_row_should_support_float_values_in_different_locale.xlsx';
|
||||
$dataRows = $this->createRowsFromValues([
|
||||
[$valueToWrite],
|
||||
]);
|
||||
|
||||
$this->writeToXLSXFile($dataRows, $fileName, $shouldUseInlineStrings = false);
|
||||
|
||||
$this->assertInlineDataWasNotWrittenToSheet($fileName, 1, "1234,5");
|
||||
$this->assertInlineDataWasWrittenToSheet($fileName, 1, "1234.5");
|
||||
} finally {
|
||||
// reset locale
|
||||
\setlocale(LC_ALL, $previousLocale);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
tests/resources/xlsx/sheet_with_strict_ooxml.xlsx
Normal file
BIN
tests/resources/xlsx/sheet_with_strict_ooxml.xlsx
Normal file
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user