Support `_xlfn.` prefix and add `ISFORMULA`, `MODE.SNGL`, `STDEV.S`, `STDEV.P`
This change adds support for newer functions that are prefixed by _xlfn. (#356). The calculation engine has been updated to recognise these as functions, and drop the _xlfn. part. It also add a couple of the new functions such as STDEV.S/P, MODE.SNGL, ISFORMULA. Fixes #356 Closes #390
This commit is contained in:
parent
1adc3a6688
commit
148bee1991
|
@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||||
|
|
||||||
- HTML writer creates a generator meta tag - [#312](https://github.com/PHPOffice/PhpSpreadsheet/issues/312)
|
- HTML writer creates a generator meta tag - [#312](https://github.com/PHPOffice/PhpSpreadsheet/issues/312)
|
||||||
- Support invalid zoom value in XLSX format - [#350](https://github.com/PHPOffice/PhpSpreadsheet/pull/350)
|
- Support invalid zoom value in XLSX format - [#350](https://github.com/PHPOffice/PhpSpreadsheet/pull/350)
|
||||||
|
- Support for `_xlfn.` prefixed functions and `ISFORMULA`, `MODE.SNGL`, `STDEV.S`, `STDEV.P` - [#390](https://github.com/PHPOffice/PhpSpreadsheet/pull/390)
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,7 @@ 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 = '@?([A-Z][A-Z0-9\.]*)[\s]*\(';
|
const CALCULATION_REGEXP_FUNCTION = '@?(?:_xlfn\.)?([A-Z][A-Z0-9\.]*)[\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,!&%^\/\*\+<>=-]*)|(\'[^\']*\')|(\"[^\"]*\"))!)?\$?([a-z]{1,3})\$?(\d{1,7})';
|
const CALCULATION_REGEXP_CELLREF = '((([^\s,!&%^\/\*\+<>=-]*)|(\'[^\']*\')|(\"[^\"]*\"))!)?\$?([a-z]{1,3})\$?(\d{1,7})';
|
||||||
// Named Range of cells
|
// Named Range of cells
|
||||||
|
@ -1082,6 +1082,13 @@ class Calculation
|
||||||
'functionCall' => [Functions::class, 'isEven'],
|
'functionCall' => [Functions::class, 'isEven'],
|
||||||
'argumentCount' => '1',
|
'argumentCount' => '1',
|
||||||
],
|
],
|
||||||
|
'ISFORMULA' => [
|
||||||
|
'category' => Category::CATEGORY_INFORMATION,
|
||||||
|
'functionCall' => [Functions::class, 'isFormula'],
|
||||||
|
'argumentCount' => '1',
|
||||||
|
'passCellReference' => true,
|
||||||
|
'passByReference' => [true],
|
||||||
|
],
|
||||||
'ISLOGICAL' => [
|
'ISLOGICAL' => [
|
||||||
'category' => Category::CATEGORY_INFORMATION,
|
'category' => Category::CATEGORY_INFORMATION,
|
||||||
'functionCall' => [Functions::class, 'isLogical'],
|
'functionCall' => [Functions::class, 'isLogical'],
|
||||||
|
@ -1302,6 +1309,11 @@ class Calculation
|
||||||
'functionCall' => [Statistical::class, 'MODE'],
|
'functionCall' => [Statistical::class, 'MODE'],
|
||||||
'argumentCount' => '1+',
|
'argumentCount' => '1+',
|
||||||
],
|
],
|
||||||
|
'MODE.SNGL' => [
|
||||||
|
'category' => Category::CATEGORY_STATISTICAL,
|
||||||
|
'functionCall' => [Statistical::class, 'MODE'],
|
||||||
|
'argumentCount' => '1+',
|
||||||
|
],
|
||||||
'MONTH' => [
|
'MONTH' => [
|
||||||
'category' => Category::CATEGORY_DATE_AND_TIME,
|
'category' => Category::CATEGORY_DATE_AND_TIME,
|
||||||
'functionCall' => [DateTime::class, 'MONTHOFYEAR'],
|
'functionCall' => [DateTime::class, 'MONTHOFYEAR'],
|
||||||
|
@ -1700,6 +1712,16 @@ class Calculation
|
||||||
'functionCall' => [Statistical::class, 'STDEV'],
|
'functionCall' => [Statistical::class, 'STDEV'],
|
||||||
'argumentCount' => '1+',
|
'argumentCount' => '1+',
|
||||||
],
|
],
|
||||||
|
'STDEV.S' => [
|
||||||
|
'category' => Category::CATEGORY_STATISTICAL,
|
||||||
|
'functionCall' => [Statistical::class, 'STDEV'],
|
||||||
|
'argumentCount' => '1+',
|
||||||
|
],
|
||||||
|
'STDEV.P' => [
|
||||||
|
'category' => Category::CATEGORY_STATISTICAL,
|
||||||
|
'functionCall' => [Statistical::class, 'STDEVP'],
|
||||||
|
'argumentCount' => '1+',
|
||||||
|
],
|
||||||
'STDEVA' => [
|
'STDEVA' => [
|
||||||
'category' => Category::CATEGORY_STATISTICAL,
|
'category' => Category::CATEGORY_STATISTICAL,
|
||||||
'functionCall' => [Statistical::class, 'STDEVA'],
|
'functionCall' => [Statistical::class, 'STDEVA'],
|
||||||
|
@ -3772,10 +3794,6 @@ class Calculation
|
||||||
$namedRange = $matches[6];
|
$namedRange = $matches[6];
|
||||||
$this->debugLog->writeDebugLog('Evaluating Named Range ', $namedRange);
|
$this->debugLog->writeDebugLog('Evaluating Named Range ', $namedRange);
|
||||||
|
|
||||||
if (substr($namedRange, 0, 6) === '_xlfn.') {
|
|
||||||
return $this->raiseFormulaError("undefined named range / function '$token'");
|
|
||||||
}
|
|
||||||
|
|
||||||
$cellValue = $this->extractNamedRange($namedRange, ((null !== $pCell) ? $pCellWorksheet : null), false);
|
$cellValue = $this->extractNamedRange($namedRange, ((null !== $pCell) ? $pCellWorksheet : null), false);
|
||||||
$pCell->attach($pCellParent);
|
$pCell->attach($pCellParent);
|
||||||
$this->debugLog->writeDebugLog('Evaluation Result for named range ', $namedRange, ' is ', $this->showTypeDetails($cellValue));
|
$this->debugLog->writeDebugLog('Evaluation Result for named range ', $namedRange, ' is ', $this->showTypeDetails($cellValue));
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
namespace PhpOffice\PhpSpreadsheet\Calculation;
|
namespace PhpOffice\PhpSpreadsheet\Calculation;
|
||||||
|
|
||||||
|
use PhpOffice\PhpSpreadsheet\Cell\Cell;
|
||||||
|
|
||||||
class Functions
|
class Functions
|
||||||
{
|
{
|
||||||
const PRECISION = 8.88E-016;
|
const PRECISION = 8.88E-016;
|
||||||
|
@ -642,4 +644,21 @@ class Functions
|
||||||
|
|
||||||
return $value;
|
return $value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ISFORMULA.
|
||||||
|
*
|
||||||
|
* @param mixed $value The cell to check
|
||||||
|
* @param Cell $pCell The current cell (containing this formula)
|
||||||
|
*
|
||||||
|
* @return bool|string
|
||||||
|
*/
|
||||||
|
public static function isFormula($value = '', Cell $pCell = null)
|
||||||
|
{
|
||||||
|
if ($pCell === null) {
|
||||||
|
return self::REF();
|
||||||
|
}
|
||||||
|
|
||||||
|
return substr($pCell->getWorksheet()->getCell($value)->getValue(), 0, 1) === '=';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -102,4 +102,19 @@ class CalculationTest extends TestCase
|
||||||
['tr'],
|
['tr'],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testDoesHandleXlfnFunctions()
|
||||||
|
{
|
||||||
|
$calculation = Calculation::getInstance();
|
||||||
|
|
||||||
|
$tree = $calculation->parseFormula('=_xlfn.ISFORMULA(A1)');
|
||||||
|
self::assertCount(3, $tree);
|
||||||
|
$function = $tree[2];
|
||||||
|
self::assertEquals('Function', $function['type']);
|
||||||
|
|
||||||
|
$tree = $calculation->parseFormula('=_xlfn.STDEV.S(A1:B2)');
|
||||||
|
self::assertCount(5, $tree);
|
||||||
|
$function = $tree[4];
|
||||||
|
self::assertEquals('Function', $function['type']);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
namespace PhpOffice\PhpSpreadsheetTests\Calculation;
|
namespace PhpOffice\PhpSpreadsheetTests\Calculation;
|
||||||
|
|
||||||
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
|
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Cell\Cell;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
class FunctionsTest extends TestCase
|
class FunctionsTest extends TestCase
|
||||||
|
@ -267,4 +269,42 @@ class FunctionsTest extends TestCase
|
||||||
{
|
{
|
||||||
return require 'data/Calculation/Functions/N.php';
|
return require 'data/Calculation/Functions/N.php';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider providerIsFormula
|
||||||
|
*
|
||||||
|
* @param mixed $expectedResult
|
||||||
|
* @param mixed $value
|
||||||
|
*/
|
||||||
|
public function testIsFormula($expectedResult, $value = 'undefined')
|
||||||
|
{
|
||||||
|
$ourCell = null;
|
||||||
|
if ($value !== 'undefined') {
|
||||||
|
$remoteCell = $this->getMockBuilder(Cell::class)
|
||||||
|
->disableOriginalConstructor()
|
||||||
|
->getMock();
|
||||||
|
$remoteCell->method('getValue')
|
||||||
|
->will($this->returnValue($value));
|
||||||
|
|
||||||
|
$sheet = $this->getMockBuilder(Worksheet::class)
|
||||||
|
->disableOriginalConstructor()
|
||||||
|
->getMock();
|
||||||
|
$sheet->method('getCell')
|
||||||
|
->will($this->returnValue($remoteCell));
|
||||||
|
|
||||||
|
$ourCell = $this->getMockBuilder(Cell::class)
|
||||||
|
->disableOriginalConstructor()
|
||||||
|
->getMock();
|
||||||
|
$ourCell->method('getWorksheet')
|
||||||
|
->will($this->returnValue($sheet));
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = Functions::isFormula($value, $ourCell);
|
||||||
|
self::assertEquals($expectedResult, $result, null, 1E-8);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function providerIsFormula()
|
||||||
|
{
|
||||||
|
return require 'data/Calculation/Functions/ISFORMULA.php';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
[
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
false,
|
||||||
|
-1,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
false,
|
||||||
|
0,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
false,
|
||||||
|
1,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
false,
|
||||||
|
'',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
false,
|
||||||
|
'2',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
false,
|
||||||
|
'#VALUE!',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
false,
|
||||||
|
'#N/A',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
false,
|
||||||
|
'TRUE',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
true,
|
||||||
|
'="ABC"',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
true,
|
||||||
|
'=A1',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'#REF!',
|
||||||
|
],
|
||||||
|
];
|
Loading…
Reference in New Issue