diff --git a/CHANGELOG.md b/CHANGELOG.md index e575e8a2a7..b266875df8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com) and this project adheres to [Semantic Versioning](https://semver.org). +# TBD - 2.3.8 + +### Fixed + +- Xls writer Parser Mishandling True/False Argument. Backport of [PR #4333](https://github.com/PHPOffice/PhpSpreadsheet/pull/4333) +- Xls writer Parser Parse By Character Not Byte. Backport of [PR #4344](https://github.com/PHPOffice/PhpSpreadsheet/pull/4344) + # 2025-01-26 - 2.3.7 ### Fixed diff --git a/src/PhpSpreadsheet/Writer/Xls/Parser.php b/src/PhpSpreadsheet/Writer/Xls/Parser.php index c0f60d66e1..dd49b619b4 100644 --- a/src/PhpSpreadsheet/Writer/Xls/Parser.php +++ b/src/PhpSpreadsheet/Writer/Xls/Parser.php @@ -67,6 +67,8 @@ class Parser . '[$]?[A-Ia-i]?[A-Za-z][$]?(\\d+)' . '$~u'; + private const UTF8 = 'UTF-8'; + /** * The index of the character we are currently looking at. */ @@ -991,29 +993,30 @@ private function advance(): void { $token = ''; $i = $this->currentCharacter; - $formula_length = strlen($this->formula); + $formula = mb_str_split($this->formula, 1, self::UTF8); + $formula_length = count($formula); // eat up white spaces if ($i < $formula_length) { - while ($this->formula[$i] == ' ') { + while ($formula[$i] === ' ') { ++$i; } if ($i < ($formula_length - 1)) { - $this->lookAhead = $this->formula[$i + 1]; + $this->lookAhead = $formula[$i + 1]; } $token = ''; } while ($i < $formula_length) { - $token .= $this->formula[$i]; + $token .= $formula[$i]; if ($i < ($formula_length - 1)) { - $this->lookAhead = $this->formula[$i + 1]; + $this->lookAhead = $formula[$i + 1]; } else { $this->lookAhead = ''; } - if ($this->match($token) != '') { + if ($this->match($token) !== '') { $this->currentCharacter = $i + 1; $this->currentToken = $token; @@ -1021,7 +1024,7 @@ private function advance(): void } if ($i < ($formula_length - 2)) { - $this->lookAhead = $this->formula[$i + 2]; + $this->lookAhead = $formula[$i + 2]; } else { // if we run out of characters lookAhead becomes empty $this->lookAhead = ''; } @@ -1198,8 +1201,8 @@ private function match(string $token): string public function parse(string $formula): bool { $this->currentCharacter = 0; - $this->formula = (string) $formula; - $this->lookAhead = $formula[1] ?? ''; + $this->formula = $formula; + $this->lookAhead = mb_substr($formula, 1, 1, self::UTF8); $this->advance(); $this->parseTree = $this->condition(); @@ -1624,7 +1627,9 @@ public function toReversePolish(array $tree = []): string } // add its left subtree and return. - return $left_tree . $this->convertFunction($tree['value'], $tree['right']); + if ($left_tree !== '' || $tree['right'] !== '') { + return $left_tree . $this->convertFunction($tree['value'], $tree['right'] ?: 0); + } } $converted_tree = $this->convert($tree['value']); diff --git a/tests/PhpSpreadsheetTests/Writer/Xls/Issue4331Test.php b/tests/PhpSpreadsheetTests/Writer/Xls/Issue4331Test.php new file mode 100644 index 0000000000..77841d8290 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Xls/Issue4331Test.php @@ -0,0 +1,56 @@ +getActiveSheet(); + $c3 = '=VLOOKUP(B3,$B$10:$C$13,2,FALSE)'; + $d3 = '=VLOOKUP("intermediate",$B$10:$C$13,2,TRUE)'; + $c4 = '=VLOOKUP(B3,$B$10:$C$13,2,FALSE())'; + $d4 = '=VLOOKUP("intermediate",$B$10:$C$13,2,TRUE())'; + $sheet->fromArray( + [ + ['level', 'result'], + ['medium', $c3, $d3], + [null, $c4, $d4], + ], + null, + 'B2', + true + ); + $sheet->fromArray( + [ + ['high', 6], + ['low', 2], + ['medium', 4], + ['none', 0], + ], + null, + 'B10', + true + ); + + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xls'); + $spreadsheet->disconnectWorksheets(); + + $worksheet = $reloadedSpreadsheet->getActiveSheet(); + self::assertSame($c3, $worksheet->getCell('C3')->getValue()); + self::assertSame(4, $worksheet->getCell('C3')->getCalculatedValue()); + self::assertSame($d3, $worksheet->getCell('D3')->getValue()); + self::assertSame(6, $worksheet->getCell('D3')->getCalculatedValue()); + self::assertSame($c4, $worksheet->getCell('C4')->getValue()); + self::assertSame(4, $worksheet->getCell('C4')->getCalculatedValue()); + self::assertSame($d4, $worksheet->getCell('D4')->getValue()); + self::assertSame(6, $worksheet->getCell('D4')->getCalculatedValue()); + $reloadedSpreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Writer/Xls/NonLatinFormulasTest.php b/tests/PhpSpreadsheetTests/Writer/Xls/NonLatinFormulasTest.php new file mode 100644 index 0000000000..d8ec963181 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Xls/NonLatinFormulasTest.php @@ -0,0 +1,65 @@ +getActiveSheet(); + + $validation = $worksheet->getCell('B1')->getDataValidation(); + $validation->setType(DataValidation::TYPE_LIST); + $validation->setErrorStyle(DataValidation::STYLE_STOP); + $validation->setAllowBlank(false); + $validation->setShowInputMessage(true); + $validation->setShowErrorMessage(true); + $validation->setShowDropDown(true); + $validation->setFormula1('"слово, сло"'); + + $dataValidator = new DataValidator(); + $worksheet->getCell('B1')->setValue('слово'); + self::assertTrue( + $dataValidator->isValid($worksheet->getCell('B1')) + ); + $worksheet->getCell('B1')->setValue('слов'); + self::assertFalse( + $dataValidator->isValid($worksheet->getCell('B1')) + ); + + $worksheet->setTitle('словслов'); + $worksheet->getCell('A1')->setValue('=словслов!B1'); + $worksheet->getCell('A2')->setValue("='словслов'!B1"); + $spreadsheet->addNamedRange(new NamedRange('слсл', $worksheet, '$B$1')); + $worksheet->getCell('A3')->setValue('=слсл'); + + $robj = $this->writeAndReload($spreadsheet, 'Xls'); + $spreadsheet->disconnectWorksheets(); + $sheet0 = $robj->getActiveSheet(); + self::assertSame('словслов', $sheet0->getTitle()); + self::assertSame('=словслов!B1', $sheet0->getCell('A1')->getValue()); + self::assertSame('слов', $sheet0->getCell('A1')->getCalculatedValue()); + // Quotes around sheet name are stripped off - harmless + //self::assertSame("='словслов'!B1", $sheet0->getCell('A2')->getValue()); + self::assertSame('слов', $sheet0->getCell('A2')->getCalculatedValue()); + // Formulas with defined names don't work in Xls Writer + //self::assertSame('=слсл', $sheet0->getCell('A3')->getValue()); + // But result should be accurate + self::assertSame('слов', $sheet0->getCell('A3')->getCalculatedValue()); + $names = $robj->getDefinedNames(); + self::assertCount(1, $names); + // name has been uppercased + $namedRange = $names['СЛСЛ'] ?? null; + self::assertInstanceOf(NamedRange::class, $namedRange); + self::assertSame('$B$1', $namedRange->getRange()); + + $robj->disconnectWorksheets(); + } +}