Skip to content

Commit edf7f71

Browse files
authored
Merge branch 'master' into atsign
2 parents 1253f35 + 14c3df0 commit edf7f71

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+901
-91
lines changed

CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@ and this project adheres to [Semantic Versioning](https://semver.org).
1010
### Added
1111

1212
- Xlsx Reader Optionally Ignore Rows With No Cells. [Issue #3982](https://github.com/PHPOffice/PhpSpreadsheet/issues/3982) [PR #4035](https://github.com/PHPOffice/PhpSpreadsheet/pull/4035)
13+
- Option for CSV output file to have varying numbers of columns for each row. [Issue #1415](https://github.com/PHPOffice/PhpSpreadsheet/issues/1415) [PR #4076](https://github.com/PHPOffice/PhpSpreadsheet/pull/4076)
1314

1415
### Changed
1516

1617
- On read, Xlsx Reader had been breaking up union ranges into separate individual ranges. It will now try to preserve range as it was read in. [PR #4042](https://github.com/PHPOffice/PhpSpreadsheet/pull/4042)
18+
- Xlsx/Xls spreadsheet calculation and formatting of dates will use base date of spreadsheet even when spreadsheets with different base dates are simultaneously open. [Issue #1036](https://github.com/PHPOffice/PhpSpreadsheet/issues/1036) [Issue #1635](https://github.com/PHPOffice/PhpSpreadsheet/issues/1635) [PR #4071](https://github.com/PHPOffice/PhpSpreadsheet/pull/4071)
1719

1820
### Deprecated
1921

@@ -32,7 +34,10 @@ and this project adheres to [Semantic Versioning](https://semver.org).
3234
- Mpdf and Tcpdf Borders on Merged Cells. [Issue #3557](https://github.com/PHPOffice/PhpSpreadsheet/issues/3557) [PR #4047](https://github.com/PHPOffice/PhpSpreadsheet/pull/4047)
3335
- Xls Conditional Format Improvements. [PR #4030](https://github.com/PHPOffice/PhpSpreadsheet/pull/4030) [PR #4033](https://github.com/PHPOffice/PhpSpreadsheet/pull/4033)
3436
- Conditional Range Unions and Intersections [Issue #4039](https://github.com/PHPOffice/PhpSpreadsheet/issues/4039) [PR #4042](https://github.com/PHPOffice/PhpSpreadsheet/pull/4042)
35-
- Csv Reader allow use of html mimetype. [Issue #4036](https://github.com/PHPOffice/PhpSpreadsheet/issues/4036) [PR #4049](https://github.com/PHPOffice/PhpSpreadsheet/pull/4040)
37+
- Csv Reader allow use of html mimetype. [Issue #4036](https://github.com/PHPOffice/PhpSpreadsheet/issues/4036) [PR #4040](https://github.com/PHPOffice/PhpSpreadsheet/pull/4040)
38+
- Problem rendering line chart with missing plot label. [PR #4074](https://github.com/PHPOffice/PhpSpreadsheet/pull/4074)
39+
- More RTL in Xlsx/Html Comments [Issue #4004](https://github.com/PHPOffice/PhpSpreadsheet/issues/4004) [PR #4065](https://github.com/PHPOffice/PhpSpreadsheet/pull/4065)
40+
- Empty String in sharedStrings. [Issue #4063](https://github.com/PHPOffice/PhpSpreadsheet/issues/4063) [PR #4064](https://github.com/PHPOffice/PhpSpreadsheet/pull/4064)
3641

3742
## 2024-05-11 - 2.1.0
3843

docs/topics/Looping the Loop.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,28 @@ But a peak memory usage of 49,152KB compared with the 57,344KB used by `toArray(
308308
Like `toArray()`, `rangeToArray()` is easy to use, but it has the same limitations for flexibility. It provides the same limited control over how the data from each cell is returned in the array as `toArray()`.
309309
The same additional arguments that can be provided for the `toArray()` method can also be provided to `rangeToArray()`.
310310

311+
312+
## Using `rangeToArrayYieldRows()`
313+
314+
Since v2.1.0 the worksheet method `rangeToArrayYieldRows()` is available.
315+
It allows you to iterate over all sheet's rows with little memory consumption,
316+
while obtaining each row as an array:
317+
318+
```php
319+
$rowGenerator = $sheet->rangeToArrayYieldRows(
320+
'A1:' . $sheet->getHighestDataColumn() . $sheet->getHighestDataRow(),
321+
null,
322+
false,
323+
false
324+
);
325+
foreach ($rowGenerator as $row) {
326+
echo $row[0] . ' | ' . $row[1] . "\n";
327+
}
328+
```
329+
330+
See `samples/Reader2/23_iterateRowsYield.php`.
331+
332+
311333
## Using Iterators
312334

313335
You don't need to build an array from the worksheet to loop through the rows and columns and do whatever processing you need; you can loop through the rows and columns in the Worksheet directly and more efficiently using PhpSpreadsheet's built-in iterators.

docs/topics/reading-and-writing-to-file.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -679,6 +679,18 @@ $writer->setOutputEncoding('SJIS-WIN');
679679
$writer->save("05featuredemo.csv");
680680
```
681681

682+
#### Writing CSV files with varying numbers of columns
683+
684+
A CSV file can have a different number of columns in each row. This
685+
differs from the default behavior when saving as a .csv in Excel, but
686+
can be enabled in PhpSpreadsheet by using the following code:
687+
688+
``` php
689+
$writer = new \PhpOffice\PhpSpreadsheet\Writer\Csv($spreadsheet);
690+
$writer->setVariableColumns(true);
691+
$writer->save("05featuredemo.csv");
692+
```
693+
682694
#### Decimal and thousands separators
683695

684696
If the worksheet you are exporting contains numbers with decimal or
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
/**
4+
* Use rangeToArrayYieldRows() to efficiently iterate over all rows.
5+
*/
6+
7+
require __DIR__ . '/../Header.php';
8+
9+
$inputFileName = __DIR__ . '/../Reader/sampleData/example1.xls';
10+
11+
$spreadsheet = PhpOffice\PhpSpreadsheet\IOFactory::load(
12+
$inputFileName,
13+
PhpOffice\PhpSpreadsheet\Reader\IReader::READ_DATA_ONLY
14+
);
15+
$sheet = $spreadsheet->getSheet(0);
16+
17+
$rowGenerator = $sheet->rangeToArrayYieldRows(
18+
$spreadsheet->getActiveSheet()->calculateWorksheetDataDimension(),
19+
null,
20+
false,
21+
false
22+
);
23+
foreach ($rowGenerator as $row) {
24+
echo '| ' . $row[0] . ' | ' . $row[1] . "|\n";
25+
}

src/PhpSpreadsheet/Calculation/Calculation.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ class Calculation
122122
*/
123123
private Logger $debugLog;
124124

125-
private bool $suppressFormulaErrorsNew = false;
125+
private bool $suppressFormulaErrors = false;
126126

127127
/**
128128
* Error message for any error that was raised/thrown by the calculation engine.
@@ -5382,7 +5382,7 @@ protected function raiseFormulaError(string $errorMessage, int $code = 0, ?Throw
53825382
{
53835383
$this->formulaError = $errorMessage;
53845384
$this->cyclicReferenceStack->clear();
5385-
$suppress = $this->suppressFormulaErrors ?? $this->suppressFormulaErrorsNew;
5385+
$suppress = $this->suppressFormulaErrors;
53865386
if (!$suppress) {
53875387
throw new Exception($errorMessage, $code, $exception);
53885388
}
@@ -5666,12 +5666,12 @@ private function evaluateDefinedName(Cell $cell, DefinedName $namedRange, Worksh
56665666

56675667
public function setSuppressFormulaErrors(bool $suppressFormulaErrors): void
56685668
{
5669-
$this->suppressFormulaErrorsNew = $suppressFormulaErrors;
5669+
$this->suppressFormulaErrors = $suppressFormulaErrors;
56705670
}
56715671

56725672
public function getSuppressFormulaErrors(): bool
56735673
{
5674-
return $this->suppressFormulaErrorsNew;
5674+
return $this->suppressFormulaErrors;
56755675
}
56765676

56775677
public static function boolToString(mixed $operand1): mixed

src/PhpSpreadsheet/Cell/AdvancedValueBinder.php

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
66
use PhpOffice\PhpSpreadsheet\Calculation\Engine\FormattedNumber;
7-
use PhpOffice\PhpSpreadsheet\RichText\RichText;
87
use PhpOffice\PhpSpreadsheet\Shared\Date;
98
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
109
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
@@ -30,7 +29,7 @@ public function bindValue(Cell $cell, mixed $value = null): bool
3029
$dataType = parent::dataTypeForValue($value);
3130

3231
// Style logic - strings
33-
if ($dataType === DataType::TYPE_STRING && !$value instanceof RichText) {
32+
if ($dataType === DataType::TYPE_STRING && is_string($value)) {
3433
// Test for booleans using locale-setting
3534
if (StringHelper::strToUpper($value) === Calculation::getTRUE()) {
3635
$cell->setValueExplicit(true, DataType::TYPE_BOOL);
@@ -54,17 +53,17 @@ public function bindValue(Cell $cell, mixed $value = null): bool
5453
$thousandsSeparator = preg_quote(StringHelper::getThousandsSeparator(), '/');
5554

5655
// Check for percentage
57-
if (preg_match('/^\-?\d*' . $decimalSeparator . '?\d*\s?\%$/', preg_replace('/(\d)' . $thousandsSeparator . '(\d)/u', '$1$2', $value))) {
58-
return $this->setPercentage(preg_replace('/(\d)' . $thousandsSeparator . '(\d)/u', '$1$2', $value), $cell);
56+
if (preg_match('/^\-?\d*' . $decimalSeparator . '?\d*\s?\%$/', (string) preg_replace('/(\d)' . $thousandsSeparator . '(\d)/u', '$1$2', $value))) {
57+
return $this->setPercentage((string) preg_replace('/(\d)' . $thousandsSeparator . '(\d)/u', '$1$2', $value), $cell);
5958
}
6059

6160
// Check for currency
62-
if (preg_match(FormattedNumber::currencyMatcherRegexp(), preg_replace('/(\d)' . $thousandsSeparator . '(\d)/u', '$1$2', $value), $matches, PREG_UNMATCHED_AS_NULL)) {
61+
if (preg_match(FormattedNumber::currencyMatcherRegexp(), (string) preg_replace('/(\d)' . $thousandsSeparator . '(\d)/u', '$1$2', $value), $matches, PREG_UNMATCHED_AS_NULL)) {
6362
// Convert value to number
6463
$sign = ($matches['PrefixedSign'] ?? $matches['PrefixedSign2'] ?? $matches['PostfixedSign']) ?? null;
6564
$currencyCode = $matches['PrefixedCurrency'] ?? $matches['PostfixedCurrency'];
6665
/** @var string */
67-
$temp = str_replace([$decimalSeparatorNoPreg, $currencyCode, ' ', '-'], ['.', '', '', ''], preg_replace('/(\d)' . $thousandsSeparator . '(\d)/u', '$1$2', $value));
66+
$temp = str_replace([$decimalSeparatorNoPreg, $currencyCode, ' ', '-'], ['.', '', '', ''], (string) preg_replace('/(\d)' . $thousandsSeparator . '(\d)/u', '$1$2', $value));
6867
$value = (float) ($sign . trim($temp));
6968

7069
return $this->setCurrency($value, $cell, $currencyCode ?? '');

src/PhpSpreadsheet/Cell/Cell.php

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -189,14 +189,21 @@ public function getValueString(): string
189189
*/
190190
public function getFormattedValue(): string
191191
{
192-
return (string) NumberFormat::toFormattedString(
192+
$currentCalendar = SharedDate::getExcelCalendar();
193+
SharedDate::setExcelCalendar($this->getWorksheet()->getParent()?->getExcelCalendar());
194+
$formattedValue = (string) NumberFormat::toFormattedString(
193195
$this->getCalculatedValue(),
194196
(string) $this->getStyle()->getNumberFormat()->getFormatCode(true)
195197
);
198+
SharedDate::setExcelCalendar($currentCalendar);
199+
200+
return $formattedValue;
196201
}
197202

198203
protected static function updateIfCellIsTableHeader(?Worksheet $workSheet, self $cell, mixed $oldValue, mixed $newValue): void
199204
{
205+
$oldValue = (is_scalar($oldValue) || $oldValue instanceof Stringable) ? ((string) $oldValue) : null;
206+
$newValue = (is_scalar($newValue) || $newValue instanceof Stringable) ? ((string) $newValue) : null;
200207
if (StringHelper::strToLower($oldValue ?? '') === StringHelper::strToLower($newValue ?? '') || $workSheet === null) {
201208
return;
202209
}
@@ -264,7 +271,10 @@ public function setValueExplicit(mixed $value, string $dataType = DataType::TYPE
264271
// Synonym for string
265272
case DataType::TYPE_INLINE:
266273
// Rich text
267-
$this->value = DataType::checkString($value);
274+
if ($value !== null && !is_scalar($value) && !($value instanceof Stringable)) {
275+
throw new SpreadsheetException('Invalid unstringable value for datatype Inline/String/String2');
276+
}
277+
$this->value = DataType::checkString(($value instanceof RichText) ? $value : ((string) $value));
268278

269279
break;
270280
case DataType::TYPE_NUMERIC:
@@ -385,6 +395,8 @@ public function getCalculatedValue(bool $resetLog = true): mixed
385395

386396
if ($this->dataType === DataType::TYPE_FORMULA) {
387397
try {
398+
$currentCalendar = SharedDate::getExcelCalendar();
399+
SharedDate::setExcelCalendar($this->getWorksheet()->getParent()?->getExcelCalendar());
388400
$index = $this->getWorksheet()->getParentOrThrow()->getActiveSheetIndex();
389401
$selected = $this->getWorksheet()->getSelectedCells();
390402
$thisworksheet = $this->getWorksheet();
@@ -509,6 +521,7 @@ public function getCalculatedValue(bool $resetLog = true): mixed
509521
$this->dataType = $originalDataType;
510522
}
511523
} catch (SpreadsheetException $ex) {
524+
SharedDate::setExcelCalendar($currentCalendar);
512525
if (($ex->getMessage() === 'Unable to access External Workbook') && ($this->calculatedValue !== null)) {
513526
return $this->calculatedValue; // Fallback for calculations referencing external files.
514527
} elseif (preg_match('/[Uu]ndefined (name|offset: 2|array key 2)/', $ex->getMessage()) === 1) {
@@ -521,6 +534,7 @@ public function getCalculatedValue(bool $resetLog = true): mixed
521534
$ex
522535
);
523536
}
537+
SharedDate::setExcelCalendar($currentCalendar);
524538

525539
if ($result === '#Not Yet Implemented') {
526540
return $this->calculatedValue; // Fallback if calculation engine does not support the formula.
@@ -924,7 +938,9 @@ public function getFormulaAttributes(): mixed
924938
*/
925939
public function __toString(): string
926940
{
927-
return (string) $this->getValue();
941+
$retVal = $this->value;
942+
943+
return ($retVal === null || is_scalar($retVal) || $retVal instanceof Stringable) ? ((string) $retVal) : '';
928944
}
929945

930946
public function getIgnoredErrors(): IgnoredErrors

src/PhpSpreadsheet/Cell/DataType.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use PhpOffice\PhpSpreadsheet\RichText\RichText;
66
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
7+
use Stringable;
78

89
class DataType
910
{
@@ -78,7 +79,7 @@ public static function checkString(null|RichText|string $textValue): RichText|st
7879
*/
7980
public static function checkErrorCode(mixed $value): string
8081
{
81-
$value = (string) $value;
82+
$value = (is_scalar($value) || $value instanceof Stringable) ? ((string) $value) : '#NULL!';
8283

8384
if (!isset(self::$errorCodes[$value])) {
8485
$value = '#NULL!';

src/PhpSpreadsheet/Cell/DataValidator.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public function isValid(Cell $cell): bool
4646
$returnValue = $this->numericOperator($dataValidation, (float) $cellValue);
4747
}
4848
} elseif ($type === DataValidation::TYPE_TEXTLENGTH) {
49-
$returnValue = $this->numericOperator($dataValidation, mb_strlen((string) $cellValue));
49+
$returnValue = $this->numericOperator($dataValidation, mb_strlen($cell->getValueString()));
5050
}
5151

5252
return $returnValue;
@@ -86,14 +86,14 @@ private function numericOperator(DataValidation $dataValidation, int|float $cell
8686
*/
8787
private function isValueInList(Cell $cell): bool
8888
{
89-
$cellValue = $cell->getValue();
89+
$cellValueString = $cell->getValueString();
9090
$dataValidation = $cell->getDataValidation();
9191

9292
$formula1 = $dataValidation->getFormula1();
9393
if (!empty($formula1)) {
9494
// inline values list
9595
if ($formula1[0] === '"') {
96-
return in_array(strtolower($cellValue), explode(',', strtolower(trim($formula1, '"'))), true);
96+
return in_array(strtolower($cellValueString), explode(',', strtolower(trim($formula1, '"'))), true);
9797
} elseif (strpos($formula1, ':') > 0) {
9898
// values list cells
9999
$matchFormula = '=MATCH(' . $cell->getCoordinate() . ', ' . $formula1 . ', 0)';

src/PhpSpreadsheet/Cell/DefaultValueBinder.php

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -46,19 +46,33 @@ public static function dataTypeForValue(mixed $value): string
4646
// Match the value against a few data types
4747
if ($value === null) {
4848
return DataType::TYPE_NULL;
49-
} elseif (is_float($value) || is_int($value)) {
49+
}
50+
if (is_float($value) || is_int($value)) {
5051
return DataType::TYPE_NUMERIC;
51-
} elseif (is_bool($value)) {
52+
}
53+
if (is_bool($value)) {
5254
return DataType::TYPE_BOOL;
53-
} elseif ($value === '') {
55+
}
56+
if ($value === '') {
5457
return DataType::TYPE_STRING;
55-
} elseif ($value instanceof RichText) {
58+
}
59+
if ($value instanceof RichText) {
5660
return DataType::TYPE_INLINE;
57-
} elseif (is_string($value) && strlen($value) > 1 && $value[0] === '=') {
61+
}
62+
if ($value instanceof Stringable) {
63+
$value = (string) $value;
64+
}
65+
if (!is_string($value)) {
66+
$gettype = is_object($value) ? get_class($value) : gettype($value);
67+
68+
throw new SpreadsheetException("unusable type $gettype");
69+
}
70+
if (strlen($value) > 1 && $value[0] === '=') {
5871
return DataType::TYPE_FORMULA;
59-
} elseif (preg_match('/^[\+\-]?(\d+\\.?\d*|\d*\\.?\d+)([Ee][\-\+]?[0-2]?\d{1,3})?$/', $value)) {
72+
}
73+
if (preg_match('/^[\+\-]?(\d+\\.?\d*|\d*\\.?\d+)([Ee][\-\+]?[0-2]?\d{1,3})?$/', $value)) {
6074
$tValue = ltrim($value, '+-');
61-
if (is_string($value) && strlen($tValue) > 1 && $tValue[0] === '0' && $tValue[1] !== '.') {
75+
if (strlen($tValue) > 1 && $tValue[0] === '0' && $tValue[1] !== '.') {
6276
return DataType::TYPE_STRING;
6377
} elseif ((!str_contains($value, '.')) && ($value > PHP_INT_MAX)) {
6478
return DataType::TYPE_STRING;
@@ -67,11 +81,10 @@ public static function dataTypeForValue(mixed $value): string
6781
}
6882

6983
return DataType::TYPE_NUMERIC;
70-
} elseif (is_string($value)) {
71-
$errorCodes = DataType::getErrorCodes();
72-
if (isset($errorCodes[$value])) {
73-
return DataType::TYPE_ERROR;
74-
}
84+
}
85+
$errorCodes = DataType::getErrorCodes();
86+
if (isset($errorCodes[$value])) {
87+
return DataType::TYPE_ERROR;
7588
}
7689

7790
return DataType::TYPE_STRING;

0 commit comments

Comments
 (0)