From a84684b70534f505c0dc197b9a7dc09354f6db86 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Tue, 21 Feb 2023 17:18:23 -0800 Subject: [PATCH] Xls Writer Parser Handle Boolean Literals as Function Arguments Fix #3369. The parser had failed to account for `TRUE` and `FALSE` when supplied as arguments to a function. I had hoped to be able to do something about its inability to handle defined names as well. I failed. I think another section might need to be added to the Writer output which specifies the defined names. I haven't yet located any suitable documentation. --- src/PhpSpreadsheet/Writer/Xls/Parser.php | 205 +++++++++++------- src/PhpSpreadsheet/Writer/Xls/Worksheet.php | 17 ++ .../Reader/Xls/FormulasTest.php | 39 ++++ .../Writer/Xls/BooleanLiteralTest.php | 25 +++ .../Writer/Xls/FormulaErrTest.php | 62 ++++-- 5 files changed, 261 insertions(+), 87 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Writer/Xls/BooleanLiteralTest.php diff --git a/src/PhpSpreadsheet/Writer/Xls/Parser.php b/src/PhpSpreadsheet/Writer/Xls/Parser.php index 3fe9e87285..6b98395f5a 100644 --- a/src/PhpSpreadsheet/Writer/Xls/Parser.php +++ b/src/PhpSpreadsheet/Writer/Xls/Parser.php @@ -499,43 +499,60 @@ private function convert($token) { if (preg_match('/"([^"]|""){0,255}"/', $token)) { return $this->convertString($token); - } elseif (is_numeric($token)) { + } + if (is_numeric($token)) { return $this->convertNumber($token); + } // match references like A1 or $A$1 - } elseif (preg_match('/^\$?([A-Ia-i]?[A-Za-z])\$?(\d+)$/', $token)) { + if (preg_match('/^\$?([A-Ia-i]?[A-Za-z])\$?(\d+)$/', $token)) { return $this->convertRef2d($token); + } // match external references like Sheet1!A1 or Sheet1:Sheet2!A1 or Sheet1!$A$1 or Sheet1:Sheet2!$A$1 - } elseif (preg_match('/^' . self::REGEX_SHEET_TITLE_UNQUOTED . '(\\:' . self::REGEX_SHEET_TITLE_UNQUOTED . ')?\\!\$?[A-Ia-i]?[A-Za-z]\$?(\\d+)$/u', $token)) { + if (preg_match('/^' . self::REGEX_SHEET_TITLE_UNQUOTED . '(\\:' . self::REGEX_SHEET_TITLE_UNQUOTED . ')?\\!\$?[A-Ia-i]?[A-Za-z]\$?(\\d+)$/u', $token)) { return $this->convertRef3d($token); + } // match external references like 'Sheet1'!A1 or 'Sheet1:Sheet2'!A1 or 'Sheet1'!$A$1 or 'Sheet1:Sheet2'!$A$1 - } elseif (preg_match("/^'" . self::REGEX_SHEET_TITLE_QUOTED . '(\\:' . self::REGEX_SHEET_TITLE_QUOTED . ")?'\\!\\$?[A-Ia-i]?[A-Za-z]\\$?(\\d+)$/u", $token)) { + if (preg_match("/^'" . self::REGEX_SHEET_TITLE_QUOTED . '(\\:' . self::REGEX_SHEET_TITLE_QUOTED . ")?'\\!\\$?[A-Ia-i]?[A-Za-z]\\$?(\\d+)$/u", $token)) { return $this->convertRef3d($token); + } // match ranges like A1:B2 or $A$1:$B$2 - } elseif (preg_match('/^(\$)?[A-Ia-i]?[A-Za-z](\$)?(\d+)\:(\$)?[A-Ia-i]?[A-Za-z](\$)?(\d+)$/', $token)) { + if (preg_match('/^(\$)?[A-Ia-i]?[A-Za-z](\$)?(\d+)\:(\$)?[A-Ia-i]?[A-Za-z](\$)?(\d+)$/', $token)) { return $this->convertRange2d($token); + } // match external ranges like Sheet1!A1:B2 or Sheet1:Sheet2!A1:B2 or Sheet1!$A$1:$B$2 or Sheet1:Sheet2!$A$1:$B$2 - } elseif (preg_match('/^' . self::REGEX_SHEET_TITLE_UNQUOTED . '(\\:' . self::REGEX_SHEET_TITLE_UNQUOTED . ')?\\!\$?([A-Ia-i]?[A-Za-z])?\$?(\\d+)\\:\$?([A-Ia-i]?[A-Za-z])?\$?(\\d+)$/u', $token)) { + if (preg_match('/^' . self::REGEX_SHEET_TITLE_UNQUOTED . '(\\:' . self::REGEX_SHEET_TITLE_UNQUOTED . ')?\\!\$?([A-Ia-i]?[A-Za-z])?\$?(\\d+)\\:\$?([A-Ia-i]?[A-Za-z])?\$?(\\d+)$/u', $token)) { return $this->convertRange3d($token); + } // match external ranges like 'Sheet1'!A1:B2 or 'Sheet1:Sheet2'!A1:B2 or 'Sheet1'!$A$1:$B$2 or 'Sheet1:Sheet2'!$A$1:$B$2 - } elseif (preg_match("/^'" . self::REGEX_SHEET_TITLE_QUOTED . '(\\:' . self::REGEX_SHEET_TITLE_QUOTED . ")?'\\!\\$?([A-Ia-i]?[A-Za-z])?\\$?(\\d+)\\:\\$?([A-Ia-i]?[A-Za-z])?\\$?(\\d+)$/u", $token)) { + if (preg_match("/^'" . self::REGEX_SHEET_TITLE_QUOTED . '(\\:' . self::REGEX_SHEET_TITLE_QUOTED . ")?'\\!\\$?([A-Ia-i]?[A-Za-z])?\\$?(\\d+)\\:\\$?([A-Ia-i]?[A-Za-z])?\\$?(\\d+)$/u", $token)) { return $this->convertRange3d($token); + } // operators (including parentheses) - } elseif (isset($this->ptg[$token])) { + if (isset($this->ptg[$token])) { return pack('C', $this->ptg[$token]); + } // match error codes - } elseif (preg_match('/^#[A-Z0\\/]{3,5}[!?]{1}$/', $token) || $token == '#N/A') { + if (preg_match('/^#[A-Z0\\/]{3,5}[!?]{1}$/', $token) || $token == '#N/A') { return $this->convertError($token); - } elseif (preg_match('/^' . Calculation::CALCULATION_REGEXP_DEFINEDNAME . '$/mui', $token) && $this->spreadsheet->getDefinedName($token) !== null) { + } + if (preg_match('/^' . Calculation::CALCULATION_REGEXP_DEFINEDNAME . '$/mui', $token) && $this->spreadsheet->getDefinedName($token) !== null) { return $this->convertDefinedName($token); + } // commented so argument number can be processed correctly. See toReversePolish(). - /*elseif (preg_match("/[A-Z0-9\xc0-\xdc\.]+/", $token)) - { - return($this->convertFunction($token, $this->_func_args)); - }*/ + /*if (preg_match("/[A-Z0-9\xc0-\xdc\.]+/", $token)) + { + return($this->convertFunction($token, $this->_func_args)); + }*/ // if it's an argument, ignore the token (the argument remains) - } elseif ($token == 'arg') { + if ($token == 'arg') { return ''; } + if (preg_match('/^true$/i', $token)) { + return $this->convertBool(1); + } + if (preg_match('/^false$/i', $token)) { + return $this->convertBool(0); + } // TODO: use real error codes throw new WriterException("Unknown token $token"); @@ -563,6 +580,11 @@ private function convertNumber($num) return pack('Cd', $this->ptg['ptgNum'], $num); } + private function convertBool(int $num): string + { + return pack('CC', $this->ptg['ptgBool'], $num); + } + /** * Convert a string token to ptgStr. * @@ -747,27 +769,32 @@ private function convertError($errorCode) return pack('C', 0xFF); } + /** @var bool */ + private $tryDefinedName = false; + private function convertDefinedName(string $name): string { if (strlen($name) > 255) { throw new WriterException('Defined Name is too long'); } - throw new WriterException('Cannot yet write formulae with defined names to Xls'); - /* - $nameReference = 1; - foreach ($this->spreadsheet->getDefinedNames() as $definedName) { - if ($name === $definedName->getName()) { - break; + if ($this->tryDefinedName) { + // @codeCoverageIgnoreStart + $nameReference = 1; + foreach ($this->spreadsheet->getDefinedNames() as $definedName) { + if ($name === $definedName->getName()) { + break; + } + ++$nameReference; } - ++$nameReference; - } - $ptgRef = pack('Cvxx', $this->ptg['ptgName'], $nameReference); + $ptgRef = pack('Cvxx', $this->ptg['ptgName'], $nameReference); + return $ptgRef; + // @codeCoverageIgnoreEnd + } - return $ptgRef; - */ + throw new WriterException('Cannot yet write formulae with defined names to Xls'); } /** @@ -1052,48 +1079,64 @@ private function match($token) } return $token; + } - default: - // if it's a reference A1 or $A$1 or $A1 or A$1 - if (preg_match('/^\$?[A-Ia-i]?[A-Za-z]\$?\d+$/', $token) && !preg_match('/\d/', $this->lookAhead) && ($this->lookAhead !== ':') && ($this->lookAhead !== '.') && ($this->lookAhead !== '!')) { - return $token; - } elseif (preg_match('/^' . self::REGEX_SHEET_TITLE_UNQUOTED . '(\\:' . self::REGEX_SHEET_TITLE_UNQUOTED . ')?\\!\$?[A-Ia-i]?[A-Za-z]\$?\\d+$/u', $token) && !preg_match('/\d/', $this->lookAhead) && ($this->lookAhead !== ':') && ($this->lookAhead !== '.')) { - // If it's an external reference (Sheet1!A1 or Sheet1:Sheet2!A1 or Sheet1!$A$1 or Sheet1:Sheet2!$A$1) - return $token; - } elseif (preg_match("/^'" . self::REGEX_SHEET_TITLE_QUOTED . '(\\:' . self::REGEX_SHEET_TITLE_QUOTED . ")?'\\!\\$?[A-Ia-i]?[A-Za-z]\\$?\\d+$/u", $token) && !preg_match('/\d/', $this->lookAhead) && ($this->lookAhead !== ':') && ($this->lookAhead !== '.')) { - // If it's an external reference ('Sheet1'!A1 or 'Sheet1:Sheet2'!A1 or 'Sheet1'!$A$1 or 'Sheet1:Sheet2'!$A$1) - return $token; - } elseif (preg_match('/^(\$)?[A-Ia-i]?[A-Za-z](\$)?\d+:(\$)?[A-Ia-i]?[A-Za-z](\$)?\d+$/', $token) && !preg_match('/\d/', $this->lookAhead)) { - // if it's a range A1:A2 or $A$1:$A$2 - return $token; - } elseif (preg_match('/^' . self::REGEX_SHEET_TITLE_UNQUOTED . '(\\:' . self::REGEX_SHEET_TITLE_UNQUOTED . ')?\\!\$?([A-Ia-i]?[A-Za-z])?\$?\\d+:\$?([A-Ia-i]?[A-Za-z])?\$?\\d+$/u', $token) && !preg_match('/\d/', $this->lookAhead)) { - // If it's an external range like Sheet1!A1:B2 or Sheet1:Sheet2!A1:B2 or Sheet1!$A$1:$B$2 or Sheet1:Sheet2!$A$1:$B$2 - return $token; - } elseif (preg_match("/^'" . self::REGEX_SHEET_TITLE_QUOTED . '(\\:' . self::REGEX_SHEET_TITLE_QUOTED . ")?'\\!\\$?([A-Ia-i]?[A-Za-z])?\\$?\\d+:\\$?([A-Ia-i]?[A-Za-z])?\\$?\\d+$/u", $token) && !preg_match('/\d/', $this->lookAhead)) { - // If it's an external range like 'Sheet1'!A1:B2 or 'Sheet1:Sheet2'!A1:B2 or 'Sheet1'!$A$1:$B$2 or 'Sheet1:Sheet2'!$A$1:$B$2 - return $token; - } elseif (is_numeric($token) && (!is_numeric($token . $this->lookAhead) || ($this->lookAhead == '')) && ($this->lookAhead !== '!') && ($this->lookAhead !== ':')) { - // If it's a number (check that it's not a sheet name or range) - return $token; - } elseif (preg_match('/"([^"]|""){0,255}"/', $token) && $this->lookAhead !== '"' && (substr_count($token, '"') % 2 == 0)) { - // If it's a string (of maximum 255 characters) - return $token; - } elseif (preg_match('/^#[A-Z0\\/]{3,5}[!?]{1}$/', $token) || $token === '#N/A') { - // If it's an error code - return $token; - } elseif (preg_match("/^[A-Z0-9\xc0-\xdc\\.]+$/i", $token) && ($this->lookAhead === '(')) { - // if it's a function call - return $token; - } elseif (preg_match('/^' . Calculation::CALCULATION_REGEXP_DEFINEDNAME . '$/miu', $token) && $this->spreadsheet->getDefinedName($token) !== null) { - return $token; - } elseif (substr($token, -1) === ')') { - // It's an argument of some description (e.g. a named range), - // precise nature yet to be determined - return $token; - } - - return ''; + // if it's a reference A1 or $A$1 or $A1 or A$1 + if (preg_match('/^\$?[A-Ia-i]?[A-Za-z]\$?\d+$/', $token) && !preg_match('/\d/', $this->lookAhead) && ($this->lookAhead !== ':') && ($this->lookAhead !== '.') && ($this->lookAhead !== '!')) { + return $token; + } + // If it's an external reference (Sheet1!A1 or Sheet1:Sheet2!A1 or Sheet1!$A$1 or Sheet1:Sheet2!$A$1) + if (preg_match('/^' . self::REGEX_SHEET_TITLE_UNQUOTED . '(\\:' . self::REGEX_SHEET_TITLE_UNQUOTED . ')?\\!\$?[A-Ia-i]?[A-Za-z]\$?\\d+$/u', $token) && !preg_match('/\d/', $this->lookAhead) && ($this->lookAhead !== ':') && ($this->lookAhead !== '.')) { + return $token; } + // If it's an external reference ('Sheet1'!A1 or 'Sheet1:Sheet2'!A1 or 'Sheet1'!$A$1 or 'Sheet1:Sheet2'!$A$1) + if (preg_match("/^'" . self::REGEX_SHEET_TITLE_QUOTED . '(\\:' . self::REGEX_SHEET_TITLE_QUOTED . ")?'\\!\\$?[A-Ia-i]?[A-Za-z]\\$?\\d+$/u", $token) && !preg_match('/\d/', $this->lookAhead) && ($this->lookAhead !== ':') && ($this->lookAhead !== '.')) { + return $token; + } + // if it's a range A1:A2 or $A$1:$A$2 + if (preg_match('/^(\$)?[A-Ia-i]?[A-Za-z](\$)?\d+:(\$)?[A-Ia-i]?[A-Za-z](\$)?\d+$/', $token) && !preg_match('/\d/', $this->lookAhead)) { + return $token; + } + // If it's an external range like Sheet1!A1:B2 or Sheet1:Sheet2!A1:B2 or Sheet1!$A$1:$B$2 or Sheet1:Sheet2!$A$1:$B$2 + if (preg_match('/^' . self::REGEX_SHEET_TITLE_UNQUOTED . '(\\:' . self::REGEX_SHEET_TITLE_UNQUOTED . ')?\\!\$?([A-Ia-i]?[A-Za-z])?\$?\\d+:\$?([A-Ia-i]?[A-Za-z])?\$?\\d+$/u', $token) && !preg_match('/\d/', $this->lookAhead)) { + return $token; + } + // If it's an external range like 'Sheet1'!A1:B2 or 'Sheet1:Sheet2'!A1:B2 or 'Sheet1'!$A$1:$B$2 or 'Sheet1:Sheet2'!$A$1:$B$2 + if (preg_match("/^'" . self::REGEX_SHEET_TITLE_QUOTED . '(\\:' . self::REGEX_SHEET_TITLE_QUOTED . ")?'\\!\\$?([A-Ia-i]?[A-Za-z])?\\$?\\d+:\\$?([A-Ia-i]?[A-Za-z])?\\$?\\d+$/u", $token) && !preg_match('/\d/', $this->lookAhead)) { + return $token; + } + // If it's a number (check that it's not a sheet name or range) + if (is_numeric($token) && (!is_numeric($token . $this->lookAhead) || ($this->lookAhead == '')) && ($this->lookAhead !== '!') && ($this->lookAhead !== ':')) { + return $token; + } + // If it's a string (of maximum 255 characters) + if (preg_match('/"([^"]|""){0,255}"/', $token) && $this->lookAhead !== '"' && (substr_count($token, '"') % 2 == 0)) { + return $token; + } + // If it's an error code + if (preg_match('/^#[A-Z0\\/]{3,5}[!?]{1}$/', $token) || $token === '#N/A') { + return $token; + } + // if it's a function call + if (preg_match("/^[A-Z0-9\xc0-\xdc\\.]+$/i", $token) && ($this->lookAhead === '(')) { + return $token; + } + if (preg_match('/^' . Calculation::CALCULATION_REGEXP_DEFINEDNAME . '$/miu', $token) && $this->spreadsheet->getDefinedName($token) !== null) { + return $token; + } + if (preg_match('/^true$/i', $token) && ($this->lookAhead === ')' || $this->lookAhead === ',')) { + return $token; + } + if (preg_match('/^false$/i', $token) && ($this->lookAhead === ')' || $this->lookAhead === ',')) { + return $token; + } + if (substr($token, -1) === ')') { + // It's an argument of some description (e.g. a named range), + // precise nature yet to be determined + return $token; + } + + return ''; } /** @@ -1295,19 +1338,22 @@ private function fact() $this->advance(); return $result; - } elseif (preg_match('/^' . self::REGEX_SHEET_TITLE_UNQUOTED . '(\\:' . self::REGEX_SHEET_TITLE_UNQUOTED . ')?\\!\$?[A-Ia-i]?[A-Za-z]\$?\\d+$/u', $this->currentToken)) { + } + if (preg_match('/^' . self::REGEX_SHEET_TITLE_UNQUOTED . '(\\:' . self::REGEX_SHEET_TITLE_UNQUOTED . ')?\\!\$?[A-Ia-i]?[A-Za-z]\$?\\d+$/u', $this->currentToken)) { // If it's an external reference (Sheet1!A1 or Sheet1:Sheet2!A1 or Sheet1!$A$1 or Sheet1:Sheet2!$A$1) $result = $this->createTree($this->currentToken, '', ''); $this->advance(); return $result; - } elseif (preg_match("/^'" . self::REGEX_SHEET_TITLE_QUOTED . '(\\:' . self::REGEX_SHEET_TITLE_QUOTED . ")?'\\!\\$?[A-Ia-i]?[A-Za-z]\\$?\\d+$/u", $this->currentToken)) { + } + if (preg_match("/^'" . self::REGEX_SHEET_TITLE_QUOTED . '(\\:' . self::REGEX_SHEET_TITLE_QUOTED . ")?'\\!\\$?[A-Ia-i]?[A-Za-z]\\$?\\d+$/u", $this->currentToken)) { // If it's an external reference ('Sheet1'!A1 or 'Sheet1:Sheet2'!A1 or 'Sheet1'!$A$1 or 'Sheet1:Sheet2'!$A$1) $result = $this->createTree($this->currentToken, '', ''); $this->advance(); return $result; - } elseif ( + } + if ( preg_match('/^(\$)?[A-Ia-i]?[A-Za-z](\$)?\d+:(\$)?[A-Ia-i]?[A-Za-z](\$)?\d+$/', $this->currentToken) || preg_match('/^(\$)?[A-Ia-i]?[A-Za-z](\$)?\d+\.\.(\$)?[A-Ia-i]?[A-Za-z](\$)?\d+$/', $this->currentToken) ) { @@ -1317,21 +1363,24 @@ private function fact() $this->advance(); return $result; - } elseif (preg_match('/^' . self::REGEX_SHEET_TITLE_UNQUOTED . '(\\:' . self::REGEX_SHEET_TITLE_UNQUOTED . ')?\\!\$?([A-Ia-i]?[A-Za-z])?\$?\\d+:\$?([A-Ia-i]?[A-Za-z])?\$?\\d+$/u', $this->currentToken)) { + } + if (preg_match('/^' . self::REGEX_SHEET_TITLE_UNQUOTED . '(\\:' . self::REGEX_SHEET_TITLE_UNQUOTED . ')?\\!\$?([A-Ia-i]?[A-Za-z])?\$?\\d+:\$?([A-Ia-i]?[A-Za-z])?\$?\\d+$/u', $this->currentToken)) { // If it's an external range (Sheet1!A1:B2 or Sheet1:Sheet2!A1:B2 or Sheet1!$A$1:$B$2 or Sheet1:Sheet2!$A$1:$B$2) // must be an error? $result = $this->createTree($this->currentToken, '', ''); $this->advance(); return $result; - } elseif (preg_match("/^'" . self::REGEX_SHEET_TITLE_QUOTED . '(\\:' . self::REGEX_SHEET_TITLE_QUOTED . ")?'\\!\\$?([A-Ia-i]?[A-Za-z])?\\$?\\d+:\\$?([A-Ia-i]?[A-Za-z])?\\$?\\d+$/u", $this->currentToken)) { + } + if (preg_match("/^'" . self::REGEX_SHEET_TITLE_QUOTED . '(\\:' . self::REGEX_SHEET_TITLE_QUOTED . ")?'\\!\\$?([A-Ia-i]?[A-Za-z])?\\$?\\d+:\\$?([A-Ia-i]?[A-Za-z])?\\$?\\d+$/u", $this->currentToken)) { // If it's an external range ('Sheet1'!A1:B2 or 'Sheet1'!A1:B2 or 'Sheet1'!$A$1:$B$2 or 'Sheet1'!$A$1:$B$2) // must be an error? $result = $this->createTree($this->currentToken, '', ''); $this->advance(); return $result; - } elseif (is_numeric($this->currentToken)) { + } + if (is_numeric($this->currentToken)) { // If it's a number or a percent if ($this->lookAhead === '%') { $result = $this->createTree('ptgPercent', $this->currentToken, ''); @@ -1342,15 +1391,23 @@ private function fact() $this->advance(); return $result; - } elseif (preg_match("/^[A-Z0-9\xc0-\xdc\\.]+$/i", $this->currentToken) && ($this->lookAhead === '(')) { + } + if (preg_match("/^[A-Z0-9\xc0-\xdc\\.]+$/i", $this->currentToken) && ($this->lookAhead === '(')) { // if it's a function call return $this->func(); - } elseif (preg_match('/^' . Calculation::CALCULATION_REGEXP_DEFINEDNAME . '$/miu', $this->currentToken) && $this->spreadsheet->getDefinedName($this->currentToken) !== null) { + } + if (preg_match('/^' . Calculation::CALCULATION_REGEXP_DEFINEDNAME . '$/miu', $this->currentToken) && $this->spreadsheet->getDefinedName($this->currentToken) !== null) { $result = $this->createTree('ptgName', $this->currentToken, ''); $this->advance(); return $result; } + if (preg_match('/^true|false$/i', $this->currentToken)) { + $result = $this->createTree($this->currentToken, '', ''); + $this->advance(); + + return $result; + } throw new WriterException('Syntax error: ' . $this->currentToken . ', lookahead: ' . $this->lookAhead . ', current char: ' . $this->currentCharacter); } diff --git a/src/PhpSpreadsheet/Writer/Xls/Worksheet.php b/src/PhpSpreadsheet/Writer/Xls/Worksheet.php index bcd79dda16..0983712bdf 100644 --- a/src/PhpSpreadsheet/Writer/Xls/Worksheet.php +++ b/src/PhpSpreadsheet/Writer/Xls/Worksheet.php @@ -802,6 +802,19 @@ private function writeBoolErr($row, $col, $value, $isError, $xfIndex) const WRITE_FORMULA_RANGE = -2; const WRITE_FORMULA_EXCEPTION = -3; + /** @var bool */ + private static $allowThrow = false; + + public static function setAllowThrow(bool $allowThrow): void + { + self::$allowThrow = $allowThrow; + } + + public static function getAllowThrow(): bool + { + return self::$allowThrow; + } + /** * Write a formula to the specified row and column (zero indexed). * The textual representation of the formula is passed to the parser in @@ -892,6 +905,10 @@ private function writeFormula($row, $col, $formula, $xfIndex, $calculatedValue) return self::WRITE_FORMULA_NORMAL; } catch (PhpSpreadsheetException $e) { + if (self::$allowThrow) { + throw $e; + } + return self::WRITE_FORMULA_EXCEPTION; } } diff --git a/tests/PhpSpreadsheetTests/Reader/Xls/FormulasTest.php b/tests/PhpSpreadsheetTests/Reader/Xls/FormulasTest.php index ff93d06528..1617f89d7d 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xls/FormulasTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Xls/FormulasTest.php @@ -3,6 +3,8 @@ namespace PhpOffice\PhpSpreadsheetTests\Reader\Xls; use PhpOffice\PhpSpreadsheet\Reader\Xls; +use PhpOffice\PhpSpreadsheet\Spreadsheet; +use PhpOffice\PhpSpreadsheet\Writer\Xls as WriterXls; use PhpOffice\PhpSpreadsheetTests\Functional\AbstractFunctional; class FormulasTest extends AbstractFunctional @@ -57,4 +59,41 @@ public function testOtherFormulas(): void self::assertSame($originalArray, $newArray); $newSpreadsheet->disconnectWorksheets(); } + + public static function customizeWriter(WriterXls $writer): void + { + $writer->setPreCalculateFormulas(false); + } + + public function testCaveatEmptor(): void + { + // This test confirms only that the 5 problematic functions + // in it are parsed correctly. + // When these are written to an Xls spreadsheet: + // Excel is buggy regarding their support; only BAHTTEXT + // will work as expected. + // LibreOffice handles them without problem. + // So does Gnumeric, except it doesn't implement BAHTTEXT. + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $originalArray = [ + [1], + [2], + ['=INDEX(TRANSPOSE(A1:A2),1,2)'], + ['=BAHTTEXT(2)'], + ['=CELL("ADDRESS",A3)'], + ['=OFFSET(A3,-2,0)'], + ['=GETPIVOTDATA("Sales",A3)'], + ]; + $sheet->fromArray($originalArray); + + /** @var callable */ + $writerCustomizer = [self::class, 'customizeWriter']; + $newSpreadsheet = $this->writeAndReload($spreadsheet, 'Xls', null, $writerCustomizer); + $spreadsheet->disconnectWorksheets(); + $newWorksheet = $newSpreadsheet->getActiveSheet(); + $newArray = $newWorksheet->toArray(null, false, false, false); + self::assertSame($originalArray, $newArray); + $newSpreadsheet->disconnectWorksheets(); + } } diff --git a/tests/PhpSpreadsheetTests/Writer/Xls/BooleanLiteralTest.php b/tests/PhpSpreadsheetTests/Writer/Xls/BooleanLiteralTest.php new file mode 100644 index 0000000000..3528308b14 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Xls/BooleanLiteralTest.php @@ -0,0 +1,25 @@ +setActiveSheetIndex(0); + $spreadsheet->getActiveSheet()->setCellValue('A1', $formula); + + $robj = $this->writeAndReload($spreadsheet, 'Xls'); + $spreadsheet->disconnectWorksheets(); + $sheet0 = $robj->setActiveSheetIndex(0); + self::assertSame(strtoupper($formula), $sheet0->getCell('A1')->getValue()); + $robj->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Writer/Xls/FormulaErrTest.php b/tests/PhpSpreadsheetTests/Writer/Xls/FormulaErrTest.php index 3965db625d..48cba12a64 100644 --- a/tests/PhpSpreadsheetTests/Writer/Xls/FormulaErrTest.php +++ b/tests/PhpSpreadsheetTests/Writer/Xls/FormulaErrTest.php @@ -2,28 +2,52 @@ namespace PhpOffice\PhpSpreadsheetTests\Writer\Xls; -use PhpOffice\PhpSpreadsheet\IOFactory; use PhpOffice\PhpSpreadsheet\NamedRange; -use PhpOffice\PhpSpreadsheet\Shared\File; -use PHPUnit\Framework\TestCase; +use PhpOffice\PhpSpreadsheet\Spreadsheet; +use PhpOffice\PhpSpreadsheet\Writer\Exception as WriterException; +use PhpOffice\PhpSpreadsheet\Writer\Xls\Worksheet; +use PhpOffice\PhpSpreadsheetTests\Functional\AbstractFunctional; -class FormulaErrTest extends TestCase +class FormulaErrTest extends AbstractFunctional { - public function testFormulaError(): void + /** @var ?Spreadsheet */ + private $spreadsheet; + + /** @var ?Spreadsheet */ + private $reloadedSpreadsheet; + + /** @var bool */ + private $allowThrow; + + protected function setUp(): void { - $obj = new \PhpOffice\PhpSpreadsheet\Spreadsheet(); + $this->allowThrow = Worksheet::getAllowThrow(); + } + + protected function tearDown(): void + { + Worksheet::setAllowThrow($this->allowThrow); + if ($this->spreadsheet !== null) { + $this->spreadsheet->disconnectWorksheets(); + $this->spreadsheet = null; + } + if ($this->reloadedSpreadsheet !== null) { + $this->reloadedSpreadsheet->disconnectWorksheets(); + $this->reloadedSpreadsheet = null; + } + } + + private function xtestFormulaError(bool $allowThrow): void + { + Worksheet::setAllowThrow($allowThrow); + $this->spreadsheet = $obj = new Spreadsheet(); $sheet0 = $obj->setActiveSheetIndex(0); $sheet0->setCellValue('A1', 2); $obj->addNamedRange(new NamedRange('DEFNAM', $sheet0, '$A$1')); $sheet0->setCellValue('B1', '=2*DEFNAM'); $sheet0->setCellValue('C1', '=DEFNAM=2'); - $sheet0->setCellValue('D1', '=CONCAT("X",DEFNAM)'); - $writer = IOFactory::createWriter($obj, 'Xls'); - $filename = File::temporaryFilename(); - $writer->save($filename); - $reader = IOFactory::createReader('Xls'); - $robj = $reader->load($filename); - unlink($filename); + $sheet0->setCellValue('D1', '=CONCATENATE("X",DEFNAM)'); + $this->reloadedSpreadsheet = $robj = $this->writeAndReload($obj, 'Xls'); $sheet0 = $robj->setActiveSheetIndex(0); $a1 = $sheet0->getCell('A1')->getCalculatedValue(); self::assertEquals(2, $a1); @@ -35,4 +59,16 @@ public function testFormulaError(): void $d1 = $sheet0->getCell('D1')->getCalculatedValue(); self::assertEquals('X2', $d1); } + + public function testFormulaErrorWithThrow(): void + { + $this->expectException(WriterException::class); + $this->expectExceptionMessage('Cannot yet write formulae with defined names to Xls'); + $this->xtestFormulaError(true); + } + + public function testFormulaError(): void + { + $this->xtestFormulaError(false); + } }