From e40438916f5c29d13f8f98d175a0595b5b7b0422 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Tue, 25 Jun 2024 22:06:36 -0700 Subject: [PATCH 1/8] Change Style Without Affecting Current Cell/Sheet, and Invalid Formulas Fix #1310, which was closed as stale in 2020, but which I will now reopen. Supersedes PR #1311 (@jaiminmoslake7020), from which I will remove the stale label but leave closed. The issue and the PR were too limited - they detected that the use of two equal signs at the start of a string made for an invalid formula, but there are variations, trivial and otherwise, which might also be detected. Using `setValue` with a string which starts with an equal sign will now attempt to parse (not evaluate) the formula; for certain situations in which the parser throws an exception, the string will be treated as a string rather than a formula. An example where it will still be treated as a formula is a 3D range reference, where the problem is not that it can't be parsed, but rather that the formula isn't supported (see unit test Calculation/Engine/RangeTest::test3dRangeEvaluation). Allowing such a formula might cause problems later on, but that is already what happens. A string beginning with an equal sign but which isn't treated as a formula will automatically set the `quotePrefix` attribute to `true`; all other `setValue` attempts will set it to `false`. This avoids the problem of a lingering value causing problems later on. It has long been a matter of discontent that setting a style can change the selected cells. A new method is added to `Worksheet`: ```php applyStylesFromArray(string $coordinate, array $styleArray) ``` This will attempt to guarantee that the active sheet in the current spreadsheet, and the selected cells in the current worksheet, remain undisturbed after the call. The setting of `quotePrefix` above is the first use of the new method. --- .../Calculation/Calculation.php | 2 +- src/PhpSpreadsheet/Cell/Cell.php | 6 +++ .../Cell/DefaultValueBinder.php | 40 +++++++++++++++---- src/PhpSpreadsheet/Cell/StringValueBinder.php | 7 +--- src/PhpSpreadsheet/Worksheet/Worksheet.php | 15 +++++++ .../Calculation/Engine/RangeTest.php | 35 ++++++++++++++-- .../Cell/AdvancedValueBinderTest.php | 28 +++++++++++++ .../Cell/StringValueBinderTest.php | 8 +++- tests/data/Cell/DefaultValueBinder.php | 3 ++ 9 files changed, 126 insertions(+), 18 deletions(-) diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index 6028a9d72..f445717d0 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -4065,7 +4065,7 @@ class Calculation $opCharacter = $formula[$index]; // Get the first character of the value at the current index position // Check for two-character operators (e.g. >=, <=, <>) - if ((isset(self::$comparisonOperators[$opCharacter])) && (strlen($formula) > $index) && (isset(self::$comparisonOperators[$formula[$index + 1]]))) { + if ((isset(self::$comparisonOperators[$opCharacter])) && (strlen($formula) > $index) && isset($formula[$index + 1], self::$comparisonOperators[$formula[$index + 1]])) { $opCharacter .= $formula[++$index]; } // Find out if we're currently at the beginning of a number, variable, cell/row/column reference, diff --git a/src/PhpSpreadsheet/Cell/Cell.php b/src/PhpSpreadsheet/Cell/Cell.php index 3dc6c2eab..bf0b681ac 100644 --- a/src/PhpSpreadsheet/Cell/Cell.php +++ b/src/PhpSpreadsheet/Cell/Cell.php @@ -248,6 +248,7 @@ class Cell implements Stringable public function setValueExplicit(mixed $value, string $dataType = DataType::TYPE_STRING): self { $oldValue = $this->value; + $quotePrefix = false; // set the value according to data type switch ($dataType) { @@ -260,6 +261,10 @@ class Cell implements Stringable // no break case DataType::TYPE_STRING: // Synonym for string + if (is_string($value) && strlen($value) > 1 && $value[0] === '=') { + $quotePrefix = true; + } + // no break case DataType::TYPE_INLINE: // Rich text $this->value = DataType::checkString($value); @@ -299,6 +304,7 @@ class Cell implements Stringable $this->updateInCollection(); $cellCoordinate = $this->getCoordinate(); self::updateIfCellIsTableHeader($this->getParent()?->getParent(), $this, $oldValue, $value); + $this->getWorksheet()->applyStylesFromArray($cellCoordinate, ['quotePrefix' => $quotePrefix]); return $this->getParent()?->get($cellCoordinate) ?? $this; } diff --git a/src/PhpSpreadsheet/Cell/DefaultValueBinder.php b/src/PhpSpreadsheet/Cell/DefaultValueBinder.php index 2710ead43..da5138e7d 100644 --- a/src/PhpSpreadsheet/Cell/DefaultValueBinder.php +++ b/src/PhpSpreadsheet/Cell/DefaultValueBinder.php @@ -3,6 +3,8 @@ namespace PhpOffice\PhpSpreadsheet\Cell; use DateTimeInterface; +use PhpOffice\PhpSpreadsheet\Calculation\Calculation; +use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalculationException; use PhpOffice\PhpSpreadsheet\Exception as SpreadsheetException; use PhpOffice\PhpSpreadsheet\RichText\RichText; use PhpOffice\PhpSpreadsheet\Shared\StringHelper; @@ -46,17 +48,40 @@ class DefaultValueBinder implements IValueBinder // Match the value against a few data types if ($value === null) { return DataType::TYPE_NULL; - } elseif (is_float($value) || is_int($value)) { + } + if (is_float($value) || is_int($value)) { return DataType::TYPE_NUMERIC; - } elseif (is_bool($value)) { + } + if (is_bool($value)) { return DataType::TYPE_BOOL; - } elseif ($value === '') { + } + if ($value === '') { return DataType::TYPE_STRING; - } elseif ($value instanceof RichText) { + } + if ($value instanceof RichText) { return DataType::TYPE_INLINE; - } elseif (is_string($value) && strlen($value) > 1 && $value[0] === '=') { + } + if (is_string($value) && strlen($value) > 1 && $value[0] === '=') { + $calculation = new Calculation(); + $calculation->disableBranchPruning(); + + try { + if (empty($calculation->parseFormula($value))) { + return DataType::TYPE_STRING; + } + } catch (CalculationException $e) { + $message = $e->getMessage(); + if ( + $message === 'Formula Error: An unexpected error occurred' + || str_contains($message, 'has no operands') + ) { + return DataType::TYPE_STRING; + } + } + return DataType::TYPE_FORMULA; - } elseif (preg_match('/^[\+\-]?(\d+\\.?\d*|\d*\\.?\d+)([Ee][\-\+]?[0-2]?\d{1,3})?$/', $value)) { + } + if (preg_match('/^[\+\-]?(\d+\\.?\d*|\d*\\.?\d+)([Ee][\-\+]?[0-2]?\d{1,3})?$/', $value)) { $tValue = ltrim($value, '+-'); if (is_string($value) && strlen($tValue) > 1 && $tValue[0] === '0' && $tValue[1] !== '.') { return DataType::TYPE_STRING; @@ -67,7 +92,8 @@ class DefaultValueBinder implements IValueBinder } return DataType::TYPE_NUMERIC; - } elseif (is_string($value)) { + } + if (is_string($value)) { $errorCodes = DataType::getErrorCodes(); if (isset($errorCodes[$value])) { return DataType::TYPE_ERROR; diff --git a/src/PhpSpreadsheet/Cell/StringValueBinder.php b/src/PhpSpreadsheet/Cell/StringValueBinder.php index 6ff258d93..d86cdabd3 100644 --- a/src/PhpSpreadsheet/Cell/StringValueBinder.php +++ b/src/PhpSpreadsheet/Cell/StringValueBinder.php @@ -8,7 +8,7 @@ use PhpOffice\PhpSpreadsheet\RichText\RichText; use PhpOffice\PhpSpreadsheet\Shared\StringHelper; use Stringable; -class StringValueBinder implements IValueBinder +class StringValueBinder extends DefaultValueBinder implements IValueBinder { protected bool $convertNull = true; @@ -87,12 +87,9 @@ class StringValueBinder implements IValueBinder $cell->setValueExplicit($value, DataType::TYPE_BOOL); } elseif ((is_int($value) || is_float($value)) && $this->convertNumeric === false) { $cell->setValueExplicit($value, DataType::TYPE_NUMERIC); - } elseif (is_string($value) && strlen($value) > 1 && $value[0] === '=' && $this->convertFormula === false) { + } elseif (is_string($value) && strlen($value) > 1 && $value[0] === '=' && $this->convertFormula === false && parent::dataTypeForValue($value) === DataType::TYPE_FORMULA) { $cell->setValueExplicit($value, DataType::TYPE_FORMULA); } else { - if (is_string($value) && strlen($value) > 1 && $value[0] === '=') { - $cell->getStyle()->setQuotePrefix(true); - } $cell->setValueExplicit((string) $value, DataType::TYPE_STRING); } diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index 8adada4c1..0bb64ba59 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -3673,4 +3673,19 @@ class Worksheet implements IComparable } } } + + public function applyStylesFromArray(string $coordinate, array $styleArray): bool + { + $spreadsheet = $this->parent; + if ($spreadsheet === null) { + return false; + } + $activeSheetIndex = $spreadsheet->getActiveSheetIndex(); + $originalSelected = $this->selectedCells; + $this->getStyle($coordinate)->applyFromArray($styleArray); + $this->selectedCells = $originalSelected; + $spreadsheet->setActiveSheetIndex($activeSheetIndex); + + return true; + } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Engine/RangeTest.php b/tests/PhpSpreadsheetTests/Calculation/Engine/RangeTest.php index fbac0ff91..aa7bc529d 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Engine/RangeTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Engine/RangeTest.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace PhpOffice\PhpSpreadsheetTests\Calculation\Engine; +use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; use PhpOffice\PhpSpreadsheet\NamedRange; use PhpOffice\PhpSpreadsheet\Spreadsheet; @@ -13,13 +14,23 @@ class RangeTest extends TestCase { private string $incompleteMessage = 'Must be revisited'; - private Spreadsheet $spreadSheet; + private ?Spreadsheet $spreadSheet = null; - protected function setUp(): void + protected function getSpreadsheet(): Spreadsheet { - $this->spreadSheet = new Spreadsheet(); - $this->spreadSheet->getActiveSheet() + $spreadsheet = new Spreadsheet(); + $spreadsheet->getActiveSheet() ->fromArray(array_chunk(range(1, 240), 6), null, 'A1', true); + + return $spreadsheet; + } + + protected function tearDown(): void + { + if ($this->spreadSheet !== null) { + $this->spreadSheet->disconnectWorksheets(); + $this->spreadSheet = null; + } } /** @@ -27,6 +38,7 @@ class RangeTest extends TestCase */ public function testRangeEvaluation(string $formula, int|string $expectedResult): void { + $this->spreadSheet = $this->getSpreadsheet(); $workSheet = $this->spreadSheet->getActiveSheet(); $workSheet->setCellValue('H1', $formula); @@ -64,8 +76,20 @@ class RangeTest extends TestCase ]; } + public function test3dRangeParsing(): void + { + // This test shows that parsing throws exception. + // Next test shows that formula is still treated as a formula + // despite the parse failure. + $this->expectExceptionMessage('3D Range references are not yet supported'); + $calculation = new Calculation(); + $calculation->disableBranchPruning(); + $calculation->parseFormula('=SUM(Worksheet!A1:Worksheet2!B3'); + } + public function test3dRangeEvaluation(): void { + $this->spreadSheet = $this->getSpreadsheet(); $workSheet = $this->spreadSheet->getActiveSheet(); $workSheet->setCellValue('E1', '=SUM(Worksheet!A1:Worksheet2!B3)'); @@ -78,6 +102,7 @@ class RangeTest extends TestCase */ public function testNamedRangeEvaluation(array $ranges, string $formula, int $expectedResult): void { + $this->spreadSheet = $this->getSpreadsheet(); $workSheet = $this->spreadSheet->getActiveSheet(); foreach ($ranges as $id => $range) { $this->spreadSheet->addNamedRange(new NamedRange('GROUP' . ++$id, $workSheet, $range)); @@ -116,6 +141,7 @@ class RangeTest extends TestCase */ public function testUTF8NamedRangeEvaluation(array $names, array $ranges, string $formula, int $expectedResult): void { + $this->spreadSheet = $this->getSpreadsheet(); $workSheet = $this->spreadSheet->getActiveSheet(); foreach ($names as $index => $name) { $range = $ranges[$index]; @@ -144,6 +170,7 @@ class RangeTest extends TestCase if ($this->incompleteMessage !== '') { self::markTestIncomplete($this->incompleteMessage); } + $this->spreadSheet = $this->getSpreadsheet(); $workSheet = $this->spreadSheet->getActiveSheet(); $this->spreadSheet->addNamedRange(new NamedRange('COMPOSITE', $workSheet, $composite)); diff --git a/tests/PhpSpreadsheetTests/Cell/AdvancedValueBinderTest.php b/tests/PhpSpreadsheetTests/Cell/AdvancedValueBinderTest.php index d3f0ad664..d8de97a33 100644 --- a/tests/PhpSpreadsheetTests/Cell/AdvancedValueBinderTest.php +++ b/tests/PhpSpreadsheetTests/Cell/AdvancedValueBinderTest.php @@ -6,6 +6,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Cell; use PhpOffice\PhpSpreadsheet\Cell\AdvancedValueBinder; use PhpOffice\PhpSpreadsheet\Cell\Cell; +use PhpOffice\PhpSpreadsheet\Cell\DataType; use PhpOffice\PhpSpreadsheet\Cell\IValueBinder; use PhpOffice\PhpSpreadsheet\Settings; use PhpOffice\PhpSpreadsheet\Shared\StringHelper; @@ -232,4 +233,31 @@ class AdvancedValueBinderTest extends TestCase ["Hello\nWorld", true], ]; } + + /** + * @dataProvider formulaProvider + */ + public function testFormula(string $value, string $dataType): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + + $sheet->getCell('A1')->setValue($value); + self::assertSame($dataType, $sheet->getCell('A1')->getDataType()); + if ($dataType === DataType::TYPE_FORMULA) { + self::assertFalse($sheet->getStyle('A1')->getQuotePrefix()); + } else { + self::assertTrue($sheet->getStyle('A1')->getQuotePrefix()); + } + + $spreadsheet->disconnectWorksheets(); + } + + public static function formulaProvider(): array + { + return [ + 'normal formula' => ['=SUM(A1:C3)', DataType::TYPE_FORMULA], + 'issue 1310' => ['======', DataType::TYPE_STRING], + ]; + } } diff --git a/tests/PhpSpreadsheetTests/Cell/StringValueBinderTest.php b/tests/PhpSpreadsheetTests/Cell/StringValueBinderTest.php index 43ac2fbd1..71006fff8 100644 --- a/tests/PhpSpreadsheetTests/Cell/StringValueBinderTest.php +++ b/tests/PhpSpreadsheetTests/Cell/StringValueBinderTest.php @@ -211,13 +211,19 @@ class StringValueBinderTest extends TestCase $cell->setValue($value); self::assertSame($expectedValue, $cell->getValue()); self::assertSame($expectedDataType, $cell->getDataType()); + if ($expectedDataType === DataType::TYPE_FORMULA) { + self::assertFalse($sheet->getStyle('A1')->getQuotePrefix()); + } else { + self::assertTrue($sheet->getStyle('A1')->getQuotePrefix()); + } $spreadsheet->disconnectWorksheets(); } public static function providerDataValuesSuppressFormulaConversion(): array { return [ - ['=SUM(A1:C3)', '=SUM(A1:C3)', DataType::TYPE_FORMULA, false], + 'normal formula' => ['=SUM(A1:C3)', '=SUM(A1:C3)', DataType::TYPE_FORMULA], + 'issue 1310' => ['======', '======', DataType::TYPE_STRING], ]; } diff --git a/tests/data/Cell/DefaultValueBinder.php b/tests/data/Cell/DefaultValueBinder.php index b9f3d51e1..bd4295c0b 100644 --- a/tests/data/Cell/DefaultValueBinder.php +++ b/tests/data/Cell/DefaultValueBinder.php @@ -83,4 +83,7 @@ return [ 's', '1234567890123459012345689012345690', ], + 'Issue 1310 Multiple = at start' => ['s', '======'], + 'Issue 1310 Variant 1' => ['s', '= ====='], + 'Issue 1310 Variant 2' => ['s', '=2*3='], ]; From 352872048be062f5a0b78cb179df23928a0b4a9c Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Tue, 25 Jun 2024 23:16:10 -0700 Subject: [PATCH 2/8] Resolve Merge Conflicts --- .../Cell/DefaultValueBinder.php | 18 ++++++++++----- .../Cell/DefaultValueBinderTest.php | 23 +++++++++++++++++++ 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/src/PhpSpreadsheet/Cell/DefaultValueBinder.php b/src/PhpSpreadsheet/Cell/DefaultValueBinder.php index da5138e7d..e6f97317e 100644 --- a/src/PhpSpreadsheet/Cell/DefaultValueBinder.php +++ b/src/PhpSpreadsheet/Cell/DefaultValueBinder.php @@ -61,7 +61,15 @@ class DefaultValueBinder implements IValueBinder if ($value instanceof RichText) { return DataType::TYPE_INLINE; } - if (is_string($value) && strlen($value) > 1 && $value[0] === '=') { + if ($value instanceof Stringable) { + $value = (string) $value; + } + if (!is_string($value)) { + $gettype = is_object($value) ? get_class($value) : gettype($value); + + throw new SpreadsheetException("unusable type $gettype"); + } + if (strlen($value) > 1 && $value[0] === '=') { $calculation = new Calculation(); $calculation->disableBranchPruning(); @@ -93,11 +101,9 @@ class DefaultValueBinder implements IValueBinder return DataType::TYPE_NUMERIC; } - if (is_string($value)) { - $errorCodes = DataType::getErrorCodes(); - if (isset($errorCodes[$value])) { - return DataType::TYPE_ERROR; - } + $errorCodes = DataType::getErrorCodes(); + if (isset($errorCodes[$value])) { + return DataType::TYPE_ERROR; } return DataType::TYPE_STRING; diff --git a/tests/PhpSpreadsheetTests/Cell/DefaultValueBinderTest.php b/tests/PhpSpreadsheetTests/Cell/DefaultValueBinderTest.php index fb9b42dbe..8cb5d8876 100644 --- a/tests/PhpSpreadsheetTests/Cell/DefaultValueBinderTest.php +++ b/tests/PhpSpreadsheetTests/Cell/DefaultValueBinderTest.php @@ -108,4 +108,27 @@ class DefaultValueBinderTest extends TestCase self::assertTrue($binder::$called); $spreadsheet->disconnectWorksheets(); } + + public function testDataTypeForValueExceptions(): void + { + try { + self::assertSame('s', DefaultValueBinder::dataTypeForValue(new SpreadsheetException())); + } catch (SpreadsheetException $e) { + self::fail('Should not have failed for stringable'); + } + + try { + DefaultValueBinder::dataTypeForValue([]); + self::fail('Should have failed for array'); + } catch (SpreadsheetException $e) { + self::assertStringContainsString('unusable type array', $e->getMessage()); + } + + try { + DefaultValueBinder::dataTypeForValue(new DateTime()); + self::fail('Should have failed for DateTime'); + } catch (SpreadsheetException $e) { + self::assertStringContainsString('unusable type DateTime', $e->getMessage()); + } + } } From 4b04cc1c8d6c539967effa523a907bf2daaf5cfb Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sat, 29 Jun 2024 22:00:39 -0700 Subject: [PATCH 3/8] Propagate Errors in Text Functions Fix #2581 (not obvious - see next paragraph for explanation). This continues the work of PR #2902 (and also PR #3467) to have errors propagated through function calculations rather than treating them as strings. All text functions, and the concatenation operator, are addressed in this PR. In the original issue, the spreadsheet being loaded uses the result of an unimplemented function as an argument to another function. When `getCalculatedValue` is used on the cell in question, the result is returned as `#VALUE!`. If the cell had just contained a function call to the unimplemented function, getCalculatedValue would have recognized the situation and returned oldCalculatedValue as the result. Not perfect, but good enough most of the time. User would like oldCalculatedValue returned here as well, which seems like a reasonable request. PhpSpreadsheet always returns `#Not Yet Implemented` as the result for a function which it knows about but which is not yet implemented. That is the key to the `Cell` class being able to substitute oldCalculatedValue in the first place. However, in order to do that for the issue in question, that result has to be propagated to any functions for which the result is an argument. I don't want to add unimplemented to the list of known error codes, but I am willing to add a parameter to `ErrorValue::isError` to indicate whether that value should be considered an error (default is "no"). The first use of that new parameter would be by the text functions. They go through a common Helper routine, so it is pretty easily implemented. And, as it turns out, most of the text functions do not currently propagate errors, e.g. if A1 results in a value error, `=LEFT(A1,2)` will result in `#V` rather than `#VALUE!`. With this PR, they will now be handled correctly. --- .../Calculation/Calculation.php | 29 ++++-- src/PhpSpreadsheet/Calculation/Functions.php | 4 +- .../Calculation/Information/ErrorValue.php | 6 +- .../Calculation/TextData/CaseConvert.php | 19 +++- .../Calculation/TextData/CharacterConvert.php | 15 ++- .../Calculation/TextData/Concatenate.php | 6 +- .../Calculation/TextData/Extract.php | 22 +++- .../Calculation/TextData/Format.php | 15 ++- .../Calculation/TextData/Helpers.php | 14 ++- .../Calculation/TextData/Search.php | 8 +- .../Calculation/TextData/Text.php | 26 +++-- src/PhpSpreadsheet/Cell/Cell.php | 3 +- .../TextData/ErrorPropagationTest.php | 96 ++++++++++++++++++ .../Reader/Xlsx/Issue2581Test.php | 25 +++++ tests/data/Reader/XLSX/issue.2581.xlsx | Bin 0 -> 9189 bytes 15 files changed, 248 insertions(+), 40 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ErrorPropagationTest.php create mode 100644 tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2581Test.php create mode 100644 tests/data/Reader/XLSX/issue.2581.xlsx diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index 6028a9d72..e60c12ed7 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -4792,13 +4792,20 @@ class Calculation for ($row = 0; $row < $rows; ++$row) { for ($column = 0; $column < $columns; ++$column) { - $operand1[$row][$column] - = Shared\StringHelper::substring( - self::boolToString($operand1[$row][$column]) - . self::boolToString($operand2[$row][$column]), - 0, - DataType::MAX_STRING_LENGTH - ); + $op1x = self::boolToString($operand1[$row][$column]); + $op2x = self::boolToString($operand2[$row][$column]); + if (Information\ErrorValue::isError($op1x)) { + // no need to do anything + } elseif (Information\ErrorValue::isError($op2x)) { + $operand1[$row][$column] = $op2x; + } else { + $operand1[$row][$column] + = Shared\StringHelper::substring( + $op1x . $op2x, + 0, + DataType::MAX_STRING_LENGTH + ); + } } } $result = $operand1; @@ -4808,7 +4815,13 @@ class Calculation // using the concatenation operator // with literals that fits in 32K, // so I don't think we can overflow here. - $result = self::FORMULA_STRING_QUOTE . str_replace('""', self::FORMULA_STRING_QUOTE, self::unwrapResult($operand1) . self::unwrapResult($operand2)) . self::FORMULA_STRING_QUOTE; + if (Information\ErrorValue::isError($operand1)) { + $result = $operand1; + } elseif (Information\ErrorValue::isError($operand2)) { + $result = $operand2; + } else { + $result = self::FORMULA_STRING_QUOTE . str_replace('""', self::FORMULA_STRING_QUOTE, self::unwrapResult($operand1) . self::unwrapResult($operand2)) . self::FORMULA_STRING_QUOTE; + } } $this->debugLog->writeDebugLog('Evaluation Result is %s', $this->showTypeDetails($result)); $stack->push('Value', $result); diff --git a/src/PhpSpreadsheet/Calculation/Functions.php b/src/PhpSpreadsheet/Calculation/Functions.php index 6b7450069..77f8317ae 100644 --- a/src/PhpSpreadsheet/Calculation/Functions.php +++ b/src/PhpSpreadsheet/Calculation/Functions.php @@ -26,6 +26,8 @@ class Functions const RETURNDATE_PHP_DATETIME_OBJECT = 'O'; const RETURNDATE_EXCEL = 'E'; + public const NOT_YET_IMPLEMENTED = '#Not Yet Implemented'; + /** * Compatibility mode to use for error checking and responses. */ @@ -123,7 +125,7 @@ class Functions */ public static function DUMMY(): string { - return '#Not Yet Implemented'; + return self::NOT_YET_IMPLEMENTED; } public static function isMatrixValue(mixed $idx): bool diff --git a/src/PhpSpreadsheet/Calculation/Information/ErrorValue.php b/src/PhpSpreadsheet/Calculation/Information/ErrorValue.php index dcef43990..f3a746273 100644 --- a/src/PhpSpreadsheet/Calculation/Information/ErrorValue.php +++ b/src/PhpSpreadsheet/Calculation/Information/ErrorValue.php @@ -3,6 +3,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Information; use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; +use PhpOffice\PhpSpreadsheet\Calculation\Functions; class ErrorValue { @@ -35,7 +36,7 @@ class ErrorValue * @return array|bool If an array of numbers is passed as an argument, then the returned result will also be an array * with the same dimensions */ - public static function isError(mixed $value = ''): array|bool + public static function isError(mixed $value = '', bool $tryNotImplemented = false): array|bool { if (is_array($value)) { return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $value); @@ -44,6 +45,9 @@ class ErrorValue if (!is_string($value)) { return false; } + if ($tryNotImplemented && $value === Functions::NOT_YET_IMPLEMENTED) { + return true; + } return in_array($value, ExcelError::ERROR_CODES, true); } diff --git a/src/PhpSpreadsheet/Calculation/TextData/CaseConvert.php b/src/PhpSpreadsheet/Calculation/TextData/CaseConvert.php index 83cc4ee1f..6667bac54 100644 --- a/src/PhpSpreadsheet/Calculation/TextData/CaseConvert.php +++ b/src/PhpSpreadsheet/Calculation/TextData/CaseConvert.php @@ -3,6 +3,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\TextData; use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; +use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; use PhpOffice\PhpSpreadsheet\Shared\StringHelper; class CaseConvert @@ -26,7 +27,11 @@ class CaseConvert return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $mixedCaseValue); } - $mixedCaseValue = Helpers::extractString($mixedCaseValue); + try { + $mixedCaseValue = Helpers::extractString($mixedCaseValue, true); + } catch (CalcExp $e) { + return $e->getMessage(); + } return StringHelper::strToLower($mixedCaseValue); } @@ -48,7 +53,11 @@ class CaseConvert return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $mixedCaseValue); } - $mixedCaseValue = Helpers::extractString($mixedCaseValue); + try { + $mixedCaseValue = Helpers::extractString($mixedCaseValue, true); + } catch (CalcExp $e) { + return $e->getMessage(); + } return StringHelper::strToUpper($mixedCaseValue); } @@ -70,7 +79,11 @@ class CaseConvert return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $mixedCaseValue); } - $mixedCaseValue = Helpers::extractString($mixedCaseValue); + try { + $mixedCaseValue = Helpers::extractString($mixedCaseValue, true); + } catch (CalcExp $e) { + return $e->getMessage(); + } return StringHelper::strToTitle($mixedCaseValue); } diff --git a/src/PhpSpreadsheet/Calculation/TextData/CharacterConvert.php b/src/PhpSpreadsheet/Calculation/TextData/CharacterConvert.php index 8d90a17ba..06d0f9009 100644 --- a/src/PhpSpreadsheet/Calculation/TextData/CharacterConvert.php +++ b/src/PhpSpreadsheet/Calculation/TextData/CharacterConvert.php @@ -3,6 +3,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\TextData; use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; +use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; @@ -26,7 +27,12 @@ class CharacterConvert return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $character); } - $character = Helpers::validateInt($character); + try { + $character = Helpers::validateInt($character, true); + } catch (CalcExp $e) { + return $e->getMessage(); + } + $min = Functions::getCompatibilityMode() === Functions::COMPATIBILITY_OPENOFFICE ? 0 : 1; if ($character < $min || $character > 255) { return ExcelError::VALUE(); @@ -52,7 +58,12 @@ class CharacterConvert return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $characters); } - $characters = Helpers::extractString($characters); + try { + $characters = Helpers::extractString($characters, true); + } catch (CalcExp $e) { + return $e->getMessage(); + } + if ($characters === '') { return ExcelError::VALUE(); } diff --git a/src/PhpSpreadsheet/Calculation/TextData/Concatenate.php b/src/PhpSpreadsheet/Calculation/TextData/Concatenate.php index c2281d436..78940ed16 100644 --- a/src/PhpSpreadsheet/Calculation/TextData/Concatenate.php +++ b/src/PhpSpreadsheet/Calculation/TextData/Concatenate.php @@ -27,7 +27,7 @@ class Concatenate foreach ($aArgs as $arg) { $value = Helpers::extractString($arg); - if (ErrorValue::isError($value)) { + if (ErrorValue::isError($value, true)) { $returnValue = $value; break; @@ -85,7 +85,7 @@ class Concatenate { foreach ($aArgs as $key => &$arg) { $value = Helpers::extractString($arg); - if (ErrorValue::isError($value)) { + if (ErrorValue::isError($value, true)) { return $value; } @@ -123,7 +123,7 @@ class Concatenate if (!is_numeric($repeatCount) || $repeatCount < 0) { $returnValue = ExcelError::VALUE(); - } elseif (ErrorValue::isError($stringValue)) { + } elseif (ErrorValue::isError($stringValue, true)) { $returnValue = $stringValue; } else { $returnValue = str_repeat($stringValue, (int) $repeatCount); diff --git a/src/PhpSpreadsheet/Calculation/TextData/Extract.php b/src/PhpSpreadsheet/Calculation/TextData/Extract.php index 32e5b967a..1dfb724cd 100644 --- a/src/PhpSpreadsheet/Calculation/TextData/Extract.php +++ b/src/PhpSpreadsheet/Calculation/TextData/Extract.php @@ -31,7 +31,7 @@ class Extract } try { - $value = Helpers::extractString($value); + $value = Helpers::extractString($value, true); $chars = Helpers::extractInt($chars, 0, 1); } catch (CalcExp $e) { return $e->getMessage(); @@ -61,7 +61,7 @@ class Extract } try { - $value = Helpers::extractString($value); + $value = Helpers::extractString($value, true); $start = Helpers::extractInt($start, 1); $chars = Helpers::extractInt($chars, 0); } catch (CalcExp $e) { @@ -90,7 +90,7 @@ class Extract } try { - $value = Helpers::extractString($value); + $value = Helpers::extractString($value, true); $chars = Helpers::extractInt($chars, 0, 1); } catch (CalcExp $e) { return $e->getMessage(); @@ -132,7 +132,13 @@ class Extract return self::evaluateArrayArgumentsIgnore([self::class, __FUNCTION__], 1, $text, $delimiter, $instance, $matchMode, $matchEnd, $ifNotFound); } - $text = Helpers::extractString($text ?? ''); + try { + $text = Helpers::extractString($text ?? '', true); + Helpers::extractString(Functions::flattenSingleValue($delimiter ?? ''), true); + } catch (CalcExp $e) { + return $e->getMessage(); + } + $instance = (int) $instance; $matchMode = (int) $matchMode; $matchEnd = (int) $matchEnd; @@ -190,7 +196,13 @@ class Extract return self::evaluateArrayArgumentsIgnore([self::class, __FUNCTION__], 1, $text, $delimiter, $instance, $matchMode, $matchEnd, $ifNotFound); } - $text = Helpers::extractString($text ?? ''); + try { + $text = Helpers::extractString($text ?? '', true); + Helpers::extractString(Functions::flattenSingleValue($delimiter ?? ''), true); + } catch (CalcExp $e) { + return $e->getMessage(); + } + $instance = (int) $instance; $matchMode = (int) $matchMode; $matchEnd = (int) $matchEnd; diff --git a/src/PhpSpreadsheet/Calculation/TextData/Format.php b/src/PhpSpreadsheet/Calculation/TextData/Format.php index 40335ced7..0560b376b 100644 --- a/src/PhpSpreadsheet/Calculation/TextData/Format.php +++ b/src/PhpSpreadsheet/Calculation/TextData/Format.php @@ -8,6 +8,7 @@ use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ErrorValue; use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; use PhpOffice\PhpSpreadsheet\Calculation\MathTrig; use PhpOffice\PhpSpreadsheet\RichText\RichText; @@ -123,8 +124,13 @@ class Format return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $format); } - $value = Helpers::extractString($value); - $format = Helpers::extractString($format); + try { + $value = Helpers::extractString($value, true); + $format = Helpers::extractString($format, true); + } catch (CalcExp $e) { + return $e->getMessage(); + } + $format = (string) NumberFormat::convertSystemFormats($format); if (!is_numeric($value) && Date::isDateTimeFormatCode($format)) { @@ -152,6 +158,9 @@ class Format } if (is_string($value)) { $value = trim($value); + if (ErrorValue::isError($value, true)) { + throw new CalcExp($value); + } if ($spacesMeanZero && $value === '') { $value = 0; } @@ -220,7 +229,7 @@ class Format } /** - * TEXT. + * VALUETOTEXT. * * @param mixed $value The value to format * Or can be an array of values diff --git a/src/PhpSpreadsheet/Calculation/TextData/Helpers.php b/src/PhpSpreadsheet/Calculation/TextData/Helpers.php index 15b046704..719de04a8 100644 --- a/src/PhpSpreadsheet/Calculation/TextData/Helpers.php +++ b/src/PhpSpreadsheet/Calculation/TextData/Helpers.php @@ -27,7 +27,7 @@ class Helpers if (is_bool($value)) { return self::convertBooleanValue($value); } - if ($throwIfError && is_string($value) && ErrorValue::isError($value)) { + if ($throwIfError && is_string($value) && ErrorValue::isError($value, true)) { throw new CalcExp($value); } @@ -63,18 +63,28 @@ class Helpers $value = (float) $value; } if (!is_numeric($value)) { + if (is_string($value) && ErrorValue::isError($value, true)) { + throw new CalcExp($value); + } + throw new CalcExp(ExcelError::VALUE()); } return (float) $value; } - public static function validateInt(mixed $value): int + public static function validateInt(mixed $value, bool $throwIfError = false): int { if ($value === null) { $value = 0; } elseif (is_bool($value)) { $value = (int) $value; + } elseif ($throwIfError && is_string($value) && !is_numeric($value)) { + if (!ErrorValue::isError($value, true)) { + $value = ExcelError::VALUE(); + } + + throw new CalcExp($value); } return (int) $value; diff --git a/src/PhpSpreadsheet/Calculation/TextData/Search.php b/src/PhpSpreadsheet/Calculation/TextData/Search.php index ad83f1a37..663d49fc2 100644 --- a/src/PhpSpreadsheet/Calculation/TextData/Search.php +++ b/src/PhpSpreadsheet/Calculation/TextData/Search.php @@ -32,8 +32,8 @@ class Search } try { - $needle = Helpers::extractString($needle); - $haystack = Helpers::extractString($haystack); + $needle = Helpers::extractString($needle, true); + $haystack = Helpers::extractString($haystack, true); $offset = Helpers::extractInt($offset, 1, 0, true); } catch (CalcExp $e) { return $e->getMessage(); @@ -74,8 +74,8 @@ class Search } try { - $needle = Helpers::extractString($needle); - $haystack = Helpers::extractString($haystack); + $needle = Helpers::extractString($needle, true); + $haystack = Helpers::extractString($haystack, true); $offset = Helpers::extractInt($offset, 1, 0, true); } catch (CalcExp $e) { return $e->getMessage(); diff --git a/src/PhpSpreadsheet/Calculation/TextData/Text.php b/src/PhpSpreadsheet/Calculation/TextData/Text.php index 44e0cd402..81c0ca7a1 100644 --- a/src/PhpSpreadsheet/Calculation/TextData/Text.php +++ b/src/PhpSpreadsheet/Calculation/TextData/Text.php @@ -4,6 +4,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\TextData; use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; +use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Calculation\Information\ErrorValue; @@ -20,13 +21,17 @@ class Text * @return array|int If an array of values is passed for the argument, then the returned result * will also be an array with matching dimensions */ - public static function length(mixed $value = ''): array|int + public static function length(mixed $value = ''): array|int|string { if (is_array($value)) { return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $value); } - $value = Helpers::extractString($value); + try { + $value = Helpers::extractString($value, true); + } catch (CalcExp $e) { + return $e->getMessage(); + } return mb_strlen($value, 'UTF-8'); } @@ -44,14 +49,18 @@ class Text * @return array|bool If an array of values is passed for either of the arguments, then the returned result * will also be an array with matching dimensions */ - public static function exact(mixed $value1, mixed $value2): array|bool + public static function exact(mixed $value1, mixed $value2): array|bool|string { if (is_array($value1) || is_array($value2)) { return self::evaluateArrayArguments([self::class, __FUNCTION__], $value1, $value2); } - $value1 = Helpers::extractString($value1); - $value2 = Helpers::extractString($value2); + try { + $value1 = Helpers::extractString($value1, true); + $value2 = Helpers::extractString($value2, true); + } catch (CalcExp $e) { + return $e->getMessage(); + } return $value2 === $value1; } @@ -97,11 +106,14 @@ class Text * @param mixed $padding The value with which to pad the result. * The default is #N/A. * - * @return array the array built from the text, split by the row and column delimiters + * @return array|string the array built from the text, split by the row and column delimiters, or an error string */ - public static function split(mixed $text, $columnDelimiter = null, $rowDelimiter = null, bool $ignoreEmpty = false, bool $matchMode = true, mixed $padding = '#N/A'): array + public static function split(mixed $text, $columnDelimiter = null, $rowDelimiter = null, bool $ignoreEmpty = false, bool $matchMode = true, mixed $padding = '#N/A'): array|string { $text = Functions::flattenSingleValue($text); + if (ErrorValue::isError($text, true)) { + return $text; + } $flags = self::matchFlags($matchMode); diff --git a/src/PhpSpreadsheet/Cell/Cell.php b/src/PhpSpreadsheet/Cell/Cell.php index 3dc6c2eab..45b58d2a3 100644 --- a/src/PhpSpreadsheet/Cell/Cell.php +++ b/src/PhpSpreadsheet/Cell/Cell.php @@ -4,6 +4,7 @@ namespace PhpOffice\PhpSpreadsheet\Cell; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalculationException; +use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; use PhpOffice\PhpSpreadsheet\Collection\Cells; use PhpOffice\PhpSpreadsheet\Exception as SpreadsheetException; @@ -392,7 +393,7 @@ class Cell implements Stringable ); } - if ($result === '#Not Yet Implemented') { + if ($result === Functions::NOT_YET_IMPLEMENTED) { return $this->calculatedValue; // Fallback if calculation engine does not support the formula. } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ErrorPropagationTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ErrorPropagationTest.php new file mode 100644 index 000000000..9ab19d94c --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ErrorPropagationTest.php @@ -0,0 +1,96 @@ +getSheet(); + $sheet->getCell('A1')->setValue('=ABS("X")'); + self::assertSame('#VALUE!', $sheet->getCell('A1')->getCalculatedValue()); + $sheet->getCell('A2')->setValue('=SQRT(-1)'); + self::assertSame('#NUM!', $sheet->getCell('A2')->getCalculatedValue()); + $sheet->getCell('A3')->setValue('=3/0'); + self::assertSame('#DIV/0!', $sheet->getCell('A3')->getCalculatedValue()); + $sheet->getCell('A4')->setValue('=XXXX()'); + self::assertSame('#NAME?', $sheet->getCell('A4')->getCalculatedValue()); + $sheet->getCell('A5')->setValue('=ABS("X")'); + self::assertSame('#VALUE!', $sheet->getCell('A5')->getCalculatedValue()); + + $sheet->getCell('B1')->setValue('=UPPER(A1)'); + self::assertSame('#VALUE!', $sheet->getCell('A1')->getCalculatedValue()); + $sheet->getCell('B2')->setValue('=LOWER(A2)'); + self::assertSame('#NUM!', $sheet->getCell('A2')->getCalculatedValue()); + $sheet->getCell('B3')->setValue('=PROPER(A3)'); + self::assertSame('#DIV/0!', $sheet->getCell('A3')->getCalculatedValue()); + + $sheet->getCell('C2')->setValue('=CHAR(A2)'); + self::assertSame('#NUM!', $sheet->getCell('C2')->getCalculatedValue()); + $sheet->getCell('C3')->setValue('=CODE(A3)'); + self::assertSame('#DIV/0!', $sheet->getCell('C3')->getCalculatedValue()); + + $sheet->getCell('D1')->setValue('=CONCATENATE(A1,A1)'); + self::assertSame('#VALUE!', $sheet->getCell('D1')->getCalculatedValue()); + $sheet->getCell('D2')->setValue('=TEXTJOIN(",",TRUE,A2,A3)'); + self::assertSame('#NUM!', $sheet->getCell('D2')->getCalculatedValue()); + $sheet->getCell('D3')->setValue('=REPT(A3,3)'); + self::assertSame('#DIV/0!', $sheet->getCell('D3')->getCalculatedValue()); + $sheet->getCell('D4')->setValue('=CONCAT(A4,A4)'); + self::assertSame('#NAME?', $sheet->getCell('D4')->getCalculatedValue()); + $sheet->getCell('D5')->setValue('="X"&A4'); + self::assertSame('#NAME?', $sheet->getCell('D5')->getCalculatedValue()); + $sheet->getCell('D6')->setValue('=A2&"X"'); + self::assertSame('#NUM!', $sheet->getCell('D6')->getCalculatedValue()); + + $sheet->getCell('E1')->setValue('=LEFT(A1)'); + self::assertSame('#VALUE!', $sheet->getCell('E1')->getCalculatedValue()); + $sheet->getCell('E2')->setValue('=RIGHT(A2)'); + self::assertSame('#NUM!', $sheet->getCell('E2')->getCalculatedValue()); + $sheet->getCell('E3')->setValue('=MID(A3,2,2)'); + self::assertSame('#DIV/0!', $sheet->getCell('E3')->getCalculatedValue()); + $sheet->getCell('E4')->setValue('=TEXTBEFORE(A4,"M")'); + self::assertSame('#NAME?', $sheet->getCell('E4')->getCalculatedValue()); + $sheet->getCell('E5')->setValue('=TEXTAFTER(A5,"U")'); + self::assertSame('#VALUE!', $sheet->getCell('E5')->getCalculatedValue()); + + $sheet->getCell('F1')->setValue('=VALUETOTEXT(A1)'); + self::assertSame('#VALUE!', $sheet->getCell('F1')->getCalculatedValue()); + $sheet->getCell('F2')->setValue('=DOLLAR(A2)'); + self::assertSame('#NUM!', $sheet->getCell('F2')->getCalculatedValue()); + $sheet->getCell('F3')->setValue('=FIXED(A3)'); + self::assertSame('#DIV/0!', $sheet->getCell('E3')->getCalculatedValue()); + $sheet->getCell('F4')->setValue('=TEXT(A4,"M")'); + self::assertSame('#NAME?', $sheet->getCell('F4')->getCalculatedValue()); + $sheet->getCell('F5')->setValue('=VALUE(A2)'); + self::assertSame('#NUM!', $sheet->getCell('F5')->getCalculatedValue()); + $sheet->getCell('F6')->setValue('=NUMBERVALUE(A3)'); + self::assertSame('#DIV/0!', $sheet->getCell('F6')->getCalculatedValue()); + + $sheet->getCell('G1')->setValue('=REPLACE("oldtext",2,2,A1)'); + self::assertSame('#VALUE!', $sheet->getCell('G1')->getCalculatedValue()); + $sheet->getCell('G2')->setValue('=SUBSTITUTE(A2,"U","V")'); + self::assertSame('#NUM!', $sheet->getCell('G2')->getCalculatedValue()); + + $sheet->getCell('H1')->setValue('=FIND(A1, "U")'); + self::assertSame('#VALUE!', $sheet->getCell('H1')->getCalculatedValue()); + $sheet->getCell('H2')->setValue('=SEARCH(A2,"U")'); + self::assertSame('#NUM!', $sheet->getCell('H2')->getCalculatedValue()); + + $sheet->getCell('I1')->setValue('=LEN(A1)'); + self::assertSame('#VALUE!', $sheet->getCell('I1')->getCalculatedValue()); + $sheet->getCell('I2')->setValue('=EXACT(A2,A2)'); + self::assertSame('#NUM!', $sheet->getCell('I2')->getCalculatedValue()); + $sheet->getCell('I3')->setValue('=T(A3)'); + self::assertSame('#DIV/0!', $sheet->getCell('I3')->getCalculatedValue()); + $sheet->getCell('I4')->setValue('=TEXTSPLIT(A4,"M")'); + self::assertSame('#NAME?', $sheet->getCell('I4')->getCalculatedValue()); + + $sheet->getCell('J1')->setValue('=TRIM(A1)'); + self::assertSame('#VALUE!', $sheet->getCell('J1')->getCalculatedValue()); + $sheet->getCell('J2')->setValue('=CLEAN(A2)'); + self::assertSame('#NUM!', $sheet->getCell('J2')->getCalculatedValue()); + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2581Test.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2581Test.php new file mode 100644 index 000000000..6ba87bd53 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2581Test.php @@ -0,0 +1,25 @@ +load($filename); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame('=CONCATENATE("Prefix ",MID(CELL("filename"),FIND("]",CELL("filename"))+1,255), " Suffix")', $sheet->getCell('B1')->getValue()); + self::assertSame('Prefix SomeName Suffix', $sheet->getCell('B1')->getCalculatedValue()); + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/data/Reader/XLSX/issue.2581.xlsx b/tests/data/Reader/XLSX/issue.2581.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..82af1c627c90cf6ae4e78b3953624b3b483d6686 GIT binary patch literal 9189 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 literal 0 HcmV?d00001 From 4860e8e4b89866a6884ffd16a6fc9183f32ccac4 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sat, 29 Jun 2024 22:23:50 -0700 Subject: [PATCH 4/8] Correct 2 Doc-block statements --- src/PhpSpreadsheet/Calculation/TextData/Text.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PhpSpreadsheet/Calculation/TextData/Text.php b/src/PhpSpreadsheet/Calculation/TextData/Text.php index 81c0ca7a1..f988a6c19 100644 --- a/src/PhpSpreadsheet/Calculation/TextData/Text.php +++ b/src/PhpSpreadsheet/Calculation/TextData/Text.php @@ -18,7 +18,7 @@ class Text * @param mixed $value String Value * Or can be an array of values * - * @return array|int If an array of values is passed for the argument, then the returned result + * @return array|int|string If an array of values is passed for the argument, then the returned result * will also be an array with matching dimensions */ public static function length(mixed $value = ''): array|int|string @@ -46,7 +46,7 @@ class Text * @param mixed $value2 String Value * Or can be an array of values * - * @return array|bool If an array of values is passed for either of the arguments, then the returned result + * @return array|bool|string If an array of values is passed for either of the arguments, then the returned result * will also be an array with matching dimensions */ public static function exact(mixed $value1, mixed $value2): array|bool|string From 8557ccb72a482fefc08b2ee82948e5d5989887a6 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Mon, 1 Jul 2024 14:52:23 -0700 Subject: [PATCH 5/8] Ods Comments With Newlines Fix #4081. Ods Reader was not reading entire contents of comment. On further inspection, Ods Writer also was not handling comments completely correctly. Ods comments are recorded as `text:p` children of `office:annotation` elements. A newline is inserted between successive `text:p` elements. The `text:p` element itself can have as descendants (at least): - raw text - `text:span` elements - `text:line-break` elements, which also causes the insertion of a newline Ods Writer is changed to use a single `text:p` with multiple span/linebreak elements. Ods Reader is changed to process in their entirety either that form, or multiple `text:p` elements. Styling of the individual elements of the comment is permitted in Ods. That has not been supported till now by PhpSpreadsheet, and this PR will not address that situation - Ods Reader hast little style support, and this would hardly be the most urgent case where it is missing. --- src/PhpSpreadsheet/Reader/Ods.php | 27 +++++++++--- .../Writer/Ods/Cell/Comment.php | 17 +++++++- .../Reader/Ods/MultiLineCommentTest.php | 41 ++++++++++++++++++ tests/data/Reader/Ods/issue.4081.ods | Bin 0 -> 3146 bytes 4 files changed, 77 insertions(+), 8 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Reader/Ods/MultiLineCommentTest.php create mode 100644 tests/data/Reader/Ods/issue.4081.ods diff --git a/src/PhpSpreadsheet/Reader/Ods.php b/src/PhpSpreadsheet/Reader/Ods.php index ceb345dc3..346d97c54 100644 --- a/src/PhpSpreadsheet/Reader/Ods.php +++ b/src/PhpSpreadsheet/Reader/Ods.php @@ -436,14 +436,25 @@ class Ods extends BaseReader if ($annotation->length > 0 && $annotation->item(0) !== null) { $textNode = $annotation->item(0)->getElementsByTagNameNS($textNs, 'p'); + $textNodeLength = $textNode->length; + $newLineOwed = false; + for ($textNodeIndex = 0; $textNodeIndex < $textNodeLength; ++$textNodeIndex) { + $textNodeItem = $textNode->item($textNodeIndex); + if ($textNodeItem !== null) { + $text = $this->scanElementForText($textNodeItem); + if ($newLineOwed) { + $spreadsheet->getActiveSheet() + ->getComment($columnID . $rowID) + ->getText() + ->createText("\n"); + } + $newLineOwed = true; - if ($textNode->length > 0 && $textNode->item(0) !== null) { - $text = $this->scanElementForText($textNode->item(0)); - - $spreadsheet->getActiveSheet() - ->getComment($columnID . $rowID) - ->setText($this->parseRichText($text)); -// ->setAuthor( $author ) + $spreadsheet->getActiveSheet() + ->getComment($columnID . $rowID) + ->getText() + ->createText($this->parseRichText($text)); + } } } @@ -731,6 +742,8 @@ class Ods extends BaseReader /** @var DOMNode $child */ if ($child->nodeType == XML_TEXT_NODE) { $str .= $child->nodeValue; + } elseif ($child->nodeType == XML_ELEMENT_NODE && $child->nodeName == 'text:line-break') { + $str .= "\n"; } elseif ($child->nodeType == XML_ELEMENT_NODE && $child->nodeName == 'text:s') { // It's a space diff --git a/src/PhpSpreadsheet/Writer/Ods/Cell/Comment.php b/src/PhpSpreadsheet/Writer/Ods/Cell/Comment.php index b0829bf1d..f0b7d5704 100644 --- a/src/PhpSpreadsheet/Writer/Ods/Cell/Comment.php +++ b/src/PhpSpreadsheet/Writer/Ods/Cell/Comment.php @@ -24,7 +24,22 @@ class Comment $objWriter->writeAttribute('svg:x', $comment->getMarginLeft()); $objWriter->writeAttribute('svg:y', $comment->getMarginTop()); $objWriter->writeElement('dc:creator', $comment->getAuthor()); - $objWriter->writeElement('text:p', $comment->getText()->getPlainText()); + + $objWriter->startElement('text:p'); + $text = $comment->getText()->getPlainText(); + $textElements = explode("\n", $text); + $newLineOwed = false; + foreach ($textElements as $textSegment) { + if ($newLineOwed) { + $objWriter->writeElement('text:line-break'); + } + $newLineOwed = true; + if ($textSegment !== '') { + $objWriter->writeElement('text:span', $textSegment); + } + } + $objWriter->endElement(); // text:p + $objWriter->endElement(); } } diff --git a/tests/PhpSpreadsheetTests/Reader/Ods/MultiLineCommentTest.php b/tests/PhpSpreadsheetTests/Reader/Ods/MultiLineCommentTest.php new file mode 100644 index 000000000..f6fd5657a --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Ods/MultiLineCommentTest.php @@ -0,0 +1,41 @@ +load($filename); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame("First line.\n\nSecond line.", $sheet->getComment('A1')->getText()->getPlainText()); + $spreadsheet->disconnectWorksheets(); + } + + public function testOneParagraphMultipleSpans(): void + { + $spreadsheetOld = new Spreadsheet(); + $sheetOld = $spreadsheetOld->getActiveSheet(); + $sheetOld->getCell('A1')->setValue('Hello'); + $text = $sheetOld->getComment('A1')->getText(); + $text->createText('First'); + $text->createText(' line.'); + $text->createText("\n"); + $text->createText("\n"); + $text->createText("Second line.\nThird line."); + $spreadsheet = $this->writeAndReload($spreadsheetOld, 'Ods'); + $spreadsheetOld->disconnectWorksheets(); + + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame("First line.\n\nSecond line.\nThird line.", $sheet->getComment('A1')->getText()->getPlainText()); + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/data/Reader/Ods/issue.4081.ods b/tests/data/Reader/Ods/issue.4081.ods new file mode 100644 index 0000000000000000000000000000000000000000..6d690bd4e14c5429fea6048e336a8fdea8103a90 GIT binary patch literal 3146 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 Date: Fri, 5 Jul 2024 22:20:57 -0700 Subject: [PATCH 6/8] Update CHANGELOG.md --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 229a32022..bffebad01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Added - 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) +- Means to change style without affecting current cell/sheet. [PR #4073](https://github.com/PHPOffice/PhpSpreadsheet/pull/4073) ### Changed @@ -36,6 +37,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). - 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) - More RTL in Xlsx/Html Comments [Issue #4004](https://github.com/PHPOffice/PhpSpreadsheet/issues/4004) [PR #4065](https://github.com/PHPOffice/PhpSpreadsheet/pull/4065) - Empty String in sharedStrings. [Issue #4063](https://github.com/PHPOffice/PhpSpreadsheet/issues/4063) [PR #4064](https://github.com/PHPOffice/PhpSpreadsheet/pull/4064) +- Treat invalid formulas as strings. [Issue #1310](https://github.com/PHPOffice/PhpSpreadsheet/issues/1310) [PR #4073](https://github.com/PHPOffice/PhpSpreadsheet/pull/4073) ## 2024-05-11 - 2.1.0 From 61b8aff3599936df6bd58efddc59e702714c4473 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Fri, 5 Jul 2024 22:46:55 -0700 Subject: [PATCH 7/8] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ecca9678..d0f147133 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Xls Conditional Format Improvements. [PR #4030](https://github.com/PHPOffice/PhpSpreadsheet/pull/4030) [PR #4033](https://github.com/PHPOffice/PhpSpreadsheet/pull/4033) - Conditional Range Unions and Intersections [Issue #4039](https://github.com/PHPOffice/PhpSpreadsheet/issues/4039) [PR #4042](https://github.com/PHPOffice/PhpSpreadsheet/pull/4042) - 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) +- Propagate errors in Text functions. [Issue #2581](https://github.com/PHPOffice/PhpSpreadsheet/issues/2581) [PR #4080](https://github.com/PHPOffice/PhpSpreadsheet/pull/4080) ## 2024-05-11 - 2.1.0 From 81964f991ac9b73058ebc402a674d067540ff3ad Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sun, 7 Jul 2024 07:26:18 -0700 Subject: [PATCH 8/8] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ecca9678..96cc24630 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Xls Conditional Format Improvements. [PR #4030](https://github.com/PHPOffice/PhpSpreadsheet/pull/4030) [PR #4033](https://github.com/PHPOffice/PhpSpreadsheet/pull/4033) - Conditional Range Unions and Intersections [Issue #4039](https://github.com/PHPOffice/PhpSpreadsheet/issues/4039) [PR #4042](https://github.com/PHPOffice/PhpSpreadsheet/pull/4042) - 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) +- Ods comments with newlines. [Issue #4081](https://github.com/PHPOffice/PhpSpreadsheet/issues/4081) [PR #4086](https://github.com/PHPOffice/PhpSpreadsheet/pull/4086) ## 2024-05-11 - 2.1.0