Skip to content

Commit 148bee1

Browse files
Josh-GPowerKiKi
authored andcommitted
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
1 parent 1adc3a6 commit 148bee1

File tree

6 files changed

+157
-5
lines changed

6 files changed

+157
-5
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
1111

1212
- HTML writer creates a generator meta tag - [#312](https://github.com/PHPOffice/PhpSpreadsheet/issues/312)
1313
- Support invalid zoom value in XLSX format - [#350](https://github.com/PHPOffice/PhpSpreadsheet/pull/350)
14+
- Support for `_xlfn.` prefixed functions and `ISFORMULA`, `MODE.SNGL`, `STDEV.S`, `STDEV.P` - [#390](https://github.com/PHPOffice/PhpSpreadsheet/pull/390)
1415

1516
### Fixed
1617

src/PhpSpreadsheet/Calculation/Calculation.php

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ class Calculation
2323
// Opening bracket
2424
const CALCULATION_REGEXP_OPENBRACE = '\(';
2525
// Function (allow for the old @ symbol that could be used to prefix a function, but we'll ignore it)
26-
const CALCULATION_REGEXP_FUNCTION = '@?([A-Z][A-Z0-9\.]*)[\s]*\(';
26+
const CALCULATION_REGEXP_FUNCTION = '@?(?:_xlfn\.)?([A-Z][A-Z0-9\.]*)[\s]*\(';
2727
// Cell reference (cell or range of cells, with or without a sheet reference)
2828
const CALCULATION_REGEXP_CELLREF = '((([^\s,!&%^\/\*\+<>=-]*)|(\'[^\']*\')|(\"[^\"]*\"))!)?\$?([a-z]{1,3})\$?(\d{1,7})';
2929
// Named Range of cells
@@ -1082,6 +1082,13 @@ class Calculation
10821082
'functionCall' => [Functions::class, 'isEven'],
10831083
'argumentCount' => '1',
10841084
],
1085+
'ISFORMULA' => [
1086+
'category' => Category::CATEGORY_INFORMATION,
1087+
'functionCall' => [Functions::class, 'isFormula'],
1088+
'argumentCount' => '1',
1089+
'passCellReference' => true,
1090+
'passByReference' => [true],
1091+
],
10851092
'ISLOGICAL' => [
10861093
'category' => Category::CATEGORY_INFORMATION,
10871094
'functionCall' => [Functions::class, 'isLogical'],
@@ -1302,6 +1309,11 @@ class Calculation
13021309
'functionCall' => [Statistical::class, 'MODE'],
13031310
'argumentCount' => '1+',
13041311
],
1312+
'MODE.SNGL' => [
1313+
'category' => Category::CATEGORY_STATISTICAL,
1314+
'functionCall' => [Statistical::class, 'MODE'],
1315+
'argumentCount' => '1+',
1316+
],
13051317
'MONTH' => [
13061318
'category' => Category::CATEGORY_DATE_AND_TIME,
13071319
'functionCall' => [DateTime::class, 'MONTHOFYEAR'],
@@ -1700,6 +1712,16 @@ class Calculation
17001712
'functionCall' => [Statistical::class, 'STDEV'],
17011713
'argumentCount' => '1+',
17021714
],
1715+
'STDEV.S' => [
1716+
'category' => Category::CATEGORY_STATISTICAL,
1717+
'functionCall' => [Statistical::class, 'STDEV'],
1718+
'argumentCount' => '1+',
1719+
],
1720+
'STDEV.P' => [
1721+
'category' => Category::CATEGORY_STATISTICAL,
1722+
'functionCall' => [Statistical::class, 'STDEVP'],
1723+
'argumentCount' => '1+',
1724+
],
17031725
'STDEVA' => [
17041726
'category' => Category::CATEGORY_STATISTICAL,
17051727
'functionCall' => [Statistical::class, 'STDEVA'],
@@ -3772,10 +3794,6 @@ private function processTokenStack($tokens, $cellID = null, Cell $pCell = null)
37723794
$namedRange = $matches[6];
37733795
$this->debugLog->writeDebugLog('Evaluating Named Range ', $namedRange);
37743796

3775-
if (substr($namedRange, 0, 6) === '_xlfn.') {
3776-
return $this->raiseFormulaError("undefined named range / function '$token'");
3777-
}
3778-
37793797
$cellValue = $this->extractNamedRange($namedRange, ((null !== $pCell) ? $pCellWorksheet : null), false);
37803798
$pCell->attach($pCellParent);
37813799
$this->debugLog->writeDebugLog('Evaluation Result for named range ', $namedRange, ' is ', $this->showTypeDetails($cellValue));

src/PhpSpreadsheet/Calculation/Functions.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
namespace PhpOffice\PhpSpreadsheet\Calculation;
44

5+
use PhpOffice\PhpSpreadsheet\Cell\Cell;
6+
57
class Functions
68
{
79
const PRECISION = 8.88E-016;
@@ -642,4 +644,21 @@ public static function flattenSingleValue($value = '')
642644

643645
return $value;
644646
}
647+
648+
/**
649+
* ISFORMULA.
650+
*
651+
* @param mixed $value The cell to check
652+
* @param Cell $pCell The current cell (containing this formula)
653+
*
654+
* @return bool|string
655+
*/
656+
public static function isFormula($value = '', Cell $pCell = null)
657+
{
658+
if ($pCell === null) {
659+
return self::REF();
660+
}
661+
662+
return substr($pCell->getWorksheet()->getCell($value)->getValue(), 0, 1) === '=';
663+
}
645664
}

tests/PhpSpreadsheetTests/Calculation/CalculationTest.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,4 +102,19 @@ public function providerCanLoadAllSupportedLocales()
102102
['tr'],
103103
];
104104
}
105+
106+
public function testDoesHandleXlfnFunctions()
107+
{
108+
$calculation = Calculation::getInstance();
109+
110+
$tree = $calculation->parseFormula('=_xlfn.ISFORMULA(A1)');
111+
self::assertCount(3, $tree);
112+
$function = $tree[2];
113+
self::assertEquals('Function', $function['type']);
114+
115+
$tree = $calculation->parseFormula('=_xlfn.STDEV.S(A1:B2)');
116+
self::assertCount(5, $tree);
117+
$function = $tree[4];
118+
self::assertEquals('Function', $function['type']);
119+
}
105120
}

tests/PhpSpreadsheetTests/Calculation/FunctionsTest.php

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
namespace PhpOffice\PhpSpreadsheetTests\Calculation;
44

55
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
6+
use PhpOffice\PhpSpreadsheet\Cell\Cell;
7+
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
68
use PHPUnit\Framework\TestCase;
79

810
class FunctionsTest extends TestCase
@@ -267,4 +269,42 @@ public function providerN()
267269
{
268270
return require 'data/Calculation/Functions/N.php';
269271
}
272+
273+
/**
274+
* @dataProvider providerIsFormula
275+
*
276+
* @param mixed $expectedResult
277+
* @param mixed $value
278+
*/
279+
public function testIsFormula($expectedResult, $value = 'undefined')
280+
{
281+
$ourCell = null;
282+
if ($value !== 'undefined') {
283+
$remoteCell = $this->getMockBuilder(Cell::class)
284+
->disableOriginalConstructor()
285+
->getMock();
286+
$remoteCell->method('getValue')
287+
->will($this->returnValue($value));
288+
289+
$sheet = $this->getMockBuilder(Worksheet::class)
290+
->disableOriginalConstructor()
291+
->getMock();
292+
$sheet->method('getCell')
293+
->will($this->returnValue($remoteCell));
294+
295+
$ourCell = $this->getMockBuilder(Cell::class)
296+
->disableOriginalConstructor()
297+
->getMock();
298+
$ourCell->method('getWorksheet')
299+
->will($this->returnValue($sheet));
300+
}
301+
302+
$result = Functions::isFormula($value, $ourCell);
303+
self::assertEquals($expectedResult, $result, null, 1E-8);
304+
}
305+
306+
public function providerIsFormula()
307+
{
308+
return require 'data/Calculation/Functions/ISFORMULA.php';
309+
}
270310
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php
2+
3+
return [
4+
[
5+
false,
6+
null,
7+
],
8+
[
9+
false,
10+
-1,
11+
],
12+
[
13+
false,
14+
0,
15+
],
16+
[
17+
false,
18+
1,
19+
],
20+
[
21+
false,
22+
'',
23+
],
24+
[
25+
false,
26+
'2',
27+
],
28+
[
29+
false,
30+
'#VALUE!',
31+
],
32+
[
33+
false,
34+
'#N/A',
35+
],
36+
[
37+
false,
38+
'TRUE',
39+
],
40+
[
41+
false,
42+
true,
43+
],
44+
[
45+
false,
46+
false,
47+
],
48+
[
49+
true,
50+
'="ABC"',
51+
],
52+
[
53+
true,
54+
'=A1',
55+
],
56+
[
57+
'#REF!',
58+
],
59+
];

0 commit comments

Comments
 (0)