From 12dd92bafe75d6fa19a19604156a06ae9ebb346c Mon Sep 17 00:00:00 2001 From: Mark Baker Date: Sat, 13 Jun 2020 17:35:29 +0200 Subject: [PATCH] Resolve utf-8 named ranges in calculation engine (#1522) * Resolve use of UTF-8 in defined names in the calculation engine --- CHANGELOG.md | 6 ++++ .../Calculation/Calculation.php | 20 ++++++------- .../Calculation/Engine/RangeTest.php | 30 +++++++++++++++++++ 3 files changed, 46 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c139850..37f4f3dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ 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). +## [Unreleased] + +### Fixed + +- Resolve evaluation of utf-8 named ranges in calculation engine [#1522](https://github.com/PHPOffice/PhpSpreadsheet/pull/1522) + ## [1.13.0] - 2020-05-31 ### Added diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index 2d67a134..848f9068 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -24,11 +24,11 @@ class Calculation // Opening bracket const CALCULATION_REGEXP_OPENBRACE = '\('; // Function (allow for the old @ symbol that could be used to prefix a function, but we'll ignore it) - const CALCULATION_REGEXP_FUNCTION = '@?(?:_xlfn\.)?([A-Z][A-Z0-9\.]*)[\s]*\('; + const CALCULATION_REGEXP_FUNCTION = '@?(?:_xlfn\.)?([\p{L}][\p{L}\p{N}\.]*)[\s]*\('; // Cell reference (cell or range of cells, with or without a sheet reference) const CALCULATION_REGEXP_CELLREF = '((([^\s,!&%^\/\*\+<>=-]*)|(\'[^\']*\')|(\"[^\"]*\"))!)?\$?\b([a-z]{1,3})\$?(\d{1,7})(?![\w.])'; // Named Range of cells - const CALCULATION_REGEXP_NAMEDRANGE = '((([^\s,!&%^\/\*\+<>=-]*)|(\'[^\']*\')|(\"[^\"]*\"))!)?([_A-Z][_A-Z0-9\.]*)'; + const CALCULATION_REGEXP_NAMEDRANGE = '((([^\s,!&%^\/\*\+<>=-]*)|(\'[^\']*\')|(\"[^\"]*\"))!)?([_\p{L}][_\p{L}\p{N}\.]*)'; // Error const CALCULATION_REGEXP_ERROR = '\#[A-Z][A-Z0_\/]*[!\?]?'; @@ -3395,7 +3395,7 @@ class Calculation '|' . self::CALCULATION_REGEXP_OPENBRACE . '|' . self::CALCULATION_REGEXP_NAMEDRANGE . '|' . self::CALCULATION_REGEXP_ERROR . - ')/si'; + ')/sui'; // Start with initialisation $index = 0; @@ -3499,7 +3499,7 @@ class Calculation --$parenthesisDepthMap[$pendingStoreKey]; } - if (is_array($d) && preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/i', $d['value'], $matches)) { // Did this parenthesis just close a function? + if (is_array($d) && preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/miu', $d['value'], $matches)) { // Did this parenthesis just close a function? if (!empty($pendingStoreKey) && $parenthesisDepthMap[$pendingStoreKey] == -1) { // we are closing an IF( if ($d['value'] != 'IF(') { @@ -3602,7 +3602,7 @@ class Calculation } // make sure there was a function $d = $stack->last(2); - if (!preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/i', $d['value'], $matches)) { + if (!preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/miu', $d['value'], $matches)) { return $this->raiseFormulaError('Formula Error: Unexpected ,'); } $d = $stack->pop(); @@ -3625,7 +3625,7 @@ class Calculation $expectingOperand = false; $val = $match[1]; $length = strlen($val); - if (preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/i', $val, $matches)) { + if (preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/miu', $val, $matches)) { $val = preg_replace('/\s/u', '', $val); if (isset(self::$phpSpreadsheetFunctions[strtoupper($matches[1])]) || isset(self::$controlFunctions[strtoupper($matches[1])])) { // it's a function $valToUpper = strtoupper($val); @@ -3733,7 +3733,7 @@ class Calculation } elseif (($localeConstant = array_search(trim(strtoupper($val)), self::$localeBoolean)) !== false) { $stackItemType = 'Constant'; $val = self::$excelConstants[$localeConstant]; - } elseif (preg_match('/^' . self::CALCULATION_REGEXP_NAMEDRANGE . '.*/Ui', $val, $match)) { + } elseif (preg_match('/^' . self::CALCULATION_REGEXP_NAMEDRANGE . '.*/miu', $val, $match)) { $stackItemType = 'Named Range'; $stackItemReference = $val; } @@ -3782,7 +3782,7 @@ class Calculation if (($expectingOperator) && ((preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '.*/Ui', substr($formula, $index), $match)) && ($output[count($output) - 1]['type'] == 'Cell Reference') || - (preg_match('/^' . self::CALCULATION_REGEXP_NAMEDRANGE . '.*/Ui', substr($formula, $index), $match)) && + (preg_match('/^' . self::CALCULATION_REGEXP_NAMEDRANGE . '.*/miu', substr($formula, $index), $match)) && ($output[count($output) - 1]['type'] == 'Named Range' || $output[count($output) - 1]['type'] == 'Value') )) { while ($stack->count() > 0 && @@ -4208,7 +4208,7 @@ class Calculation } // if the token is a function, pop arguments off the stack, hand them to the function, and push the result back on - } elseif (preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/i', $token, $matches)) { + } elseif (preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/miu', $token, $matches)) { if ($pCellParent) { $pCell->attach($pCellParent); } @@ -4306,7 +4306,7 @@ class Calculation $branchStore[$storeKey] = $token; } // if the token is a named range, push the named range name onto the stack - } elseif (preg_match('/^' . self::CALCULATION_REGEXP_NAMEDRANGE . '$/i', $token, $matches)) { + } elseif (preg_match('/^' . self::CALCULATION_REGEXP_NAMEDRANGE . '$/miu', $token, $matches)) { $namedRange = $matches[6]; $this->debugLog->writeDebugLog('Evaluating Named Range ', $namedRange); diff --git a/tests/PhpSpreadsheetTests/Calculation/Engine/RangeTest.php b/tests/PhpSpreadsheetTests/Calculation/Engine/RangeTest.php index ee566db2..e2e6e11d 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Engine/RangeTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Engine/RangeTest.php @@ -97,6 +97,36 @@ class RangeTest extends TestCase ]; } + /** + * @dataProvider providerUTF8NamedRangeEvaluation + * + * @param string $names + * @param string $ranges + * @param string $formula + * @param int $expectedResult + */ + public function testUTF8NamedRangeEvaluation($names, $ranges, $formula, $expectedResult): void + { + $workSheet = $this->spreadSheet->getActiveSheet(); + foreach ($names as $index => $name) { + $range = $ranges[$index]; + $this->spreadSheet->addNamedRange(new NamedRange($name, $workSheet, $range)); + } + $workSheet->setCellValue('E1', $formula); + + $sumRresult = $workSheet->getCell('E1')->getCalculatedValue(); + self::assertSame($expectedResult, $sumRresult); + } + + public function providerUTF8NamedRangeEvaluation() + { + return[ + [['Γειά', 'σου', 'Κόσμε'], ['A1', 'B1:B2', 'C1:C3'], '=SUM(Γειά,σου,Κόσμε)', 26], + [['Γειά', 'σου', 'Κόσμε'], ['A1', 'B1:B2', 'C1:C3'], '=COUNT(Γειά,σου,Κόσμε)', 6], + [['Здравствуй', 'мир'], ['A1:A3', 'C1:C3'], '=SUM(Здравствуй,мир)', 30], + ]; + } + /** * @dataProvider providerCompositeNamedRangeEvaluation *