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:
Josh Grant 2018-02-24 12:14:48 +00:00 committed by Adrien Crivelli
parent 1adc3a6688
commit 148bee1991
No known key found for this signature in database
GPG Key ID: B182FD79DC6DE92E
6 changed files with 157 additions and 5 deletions

View File

@ -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

View File

@ -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));

View File

@ -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) === '=';
}
} }

View File

@ -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']);
}
} }

View File

@ -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';
}
} }

View File

@ -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!',
],
];