Resolve utf-8 named ranges in calculation engine (#1522)

* Resolve use of UTF-8 in defined names in the calculation engine
This commit is contained in:
Mark Baker 2020-06-13 17:35:29 +02:00 committed by GitHub
parent 7ab920de5b
commit 12dd92bafe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 46 additions and 10 deletions

View File

@ -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) The format is based on [Keep a Changelog](https://keepachangelog.com)
and this project adheres to [Semantic Versioning](https://semver.org). 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 ## [1.13.0] - 2020-05-31
### Added ### Added

View File

@ -24,11 +24,11 @@ class Calculation
// Opening bracket // Opening bracket
const CALCULATION_REGEXP_OPENBRACE = '\('; const CALCULATION_REGEXP_OPENBRACE = '\(';
// Function (allow for the old @ symbol that could be used to prefix a function, but we'll ignore it) // 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) // 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.])'; const CALCULATION_REGEXP_CELLREF = '((([^\s,!&%^\/\*\+<>=-]*)|(\'[^\']*\')|(\"[^\"]*\"))!)?\$?\b([a-z]{1,3})\$?(\d{1,7})(?![\w.])';
// Named Range of cells // 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 // Error
const CALCULATION_REGEXP_ERROR = '\#[A-Z][A-Z0_\/]*[!\?]?'; const CALCULATION_REGEXP_ERROR = '\#[A-Z][A-Z0_\/]*[!\?]?';
@ -3395,7 +3395,7 @@ class Calculation
'|' . self::CALCULATION_REGEXP_OPENBRACE . '|' . self::CALCULATION_REGEXP_OPENBRACE .
'|' . self::CALCULATION_REGEXP_NAMEDRANGE . '|' . self::CALCULATION_REGEXP_NAMEDRANGE .
'|' . self::CALCULATION_REGEXP_ERROR . '|' . self::CALCULATION_REGEXP_ERROR .
')/si'; ')/sui';
// Start with initialisation // Start with initialisation
$index = 0; $index = 0;
@ -3499,7 +3499,7 @@ class Calculation
--$parenthesisDepthMap[$pendingStoreKey]; --$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) { if (!empty($pendingStoreKey) && $parenthesisDepthMap[$pendingStoreKey] == -1) {
// we are closing an IF( // we are closing an IF(
if ($d['value'] != 'IF(') { if ($d['value'] != 'IF(') {
@ -3602,7 +3602,7 @@ class Calculation
} }
// make sure there was a function // make sure there was a function
$d = $stack->last(2); $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 ,'); return $this->raiseFormulaError('Formula Error: Unexpected ,');
} }
$d = $stack->pop(); $d = $stack->pop();
@ -3625,7 +3625,7 @@ class Calculation
$expectingOperand = false; $expectingOperand = false;
$val = $match[1]; $val = $match[1];
$length = strlen($val); $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); $val = preg_replace('/\s/u', '', $val);
if (isset(self::$phpSpreadsheetFunctions[strtoupper($matches[1])]) || isset(self::$controlFunctions[strtoupper($matches[1])])) { // it's a function if (isset(self::$phpSpreadsheetFunctions[strtoupper($matches[1])]) || isset(self::$controlFunctions[strtoupper($matches[1])])) { // it's a function
$valToUpper = strtoupper($val); $valToUpper = strtoupper($val);
@ -3733,7 +3733,7 @@ class Calculation
} elseif (($localeConstant = array_search(trim(strtoupper($val)), self::$localeBoolean)) !== false) { } elseif (($localeConstant = array_search(trim(strtoupper($val)), self::$localeBoolean)) !== false) {
$stackItemType = 'Constant'; $stackItemType = 'Constant';
$val = self::$excelConstants[$localeConstant]; $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'; $stackItemType = 'Named Range';
$stackItemReference = $val; $stackItemReference = $val;
} }
@ -3782,7 +3782,7 @@ class Calculation
if (($expectingOperator) && if (($expectingOperator) &&
((preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '.*/Ui', substr($formula, $index), $match)) && ((preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '.*/Ui', substr($formula, $index), $match)) &&
($output[count($output) - 1]['type'] == 'Cell Reference') || ($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') ($output[count($output) - 1]['type'] == 'Named Range' || $output[count($output) - 1]['type'] == 'Value')
)) { )) {
while ($stack->count() > 0 && 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 // 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) { if ($pCellParent) {
$pCell->attach($pCellParent); $pCell->attach($pCellParent);
} }
@ -4306,7 +4306,7 @@ class Calculation
$branchStore[$storeKey] = $token; $branchStore[$storeKey] = $token;
} }
// if the token is a named range, push the named range name onto the stack // 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]; $namedRange = $matches[6];
$this->debugLog->writeDebugLog('Evaluating Named Range ', $namedRange); $this->debugLog->writeDebugLog('Evaluating Named Range ', $namedRange);

View File

@ -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 * @dataProvider providerCompositeNamedRangeEvaluation
* *