Skip to content

Commit

Permalink
Support _xlfn. prefix and add ISFORMULA, MODE.SNGL, STDEV.S, …
Browse files Browse the repository at this point in the history
…`STDEV.P`

This change adds support for newer functions that are prefixed
by _xlfn. (PHPOffice#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 PHPOffice#356
Closes PHPOffice#390
  • Loading branch information
Josh-G authored and Frederic Delaunay committed Oct 29, 2018
1 parent f179996 commit 078a688
Show file tree
Hide file tree
Showing 6 changed files with 160 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
- 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

Expand Down
26 changes: 26 additions & 0 deletions src/PhpSpreadsheet/Calculation/Calculation.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,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)
<<<<<<< f179996bfc279e9964c56b6b321c42d23ae62624
const CALCULATION_REGEXP_FUNCTION = '@?(('.self::XLFN_PREFIX.')?[A-Z][A-Z0-9\.]*)[\s]*\(';
=======
const CALCULATION_REGEXP_FUNCTION = '@?(?:_xlfn\.)?([A-Z][A-Z0-9\.]*)[\s]*\(';
>>>>>>> Support `_xlfn.` prefix and add `ISFORMULA`, `MODE.SNGL`, `STDEV.S`, `STDEV.P`
// 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})';
// Named Range of cells
Expand Down Expand Up @@ -1085,6 +1089,13 @@ class Calculation
'functionCall' => [Functions::class, 'isEven'],
'argumentCount' => '1',
],
'ISFORMULA' => [
'category' => Category::CATEGORY_INFORMATION,
'functionCall' => [Functions::class, 'isFormula'],
'argumentCount' => '1',
'passCellReference' => true,
'passByReference' => [true],
],
'ISLOGICAL' => [
'category' => Category::CATEGORY_INFORMATION,
'functionCall' => [Functions::class, 'isLogical'],
Expand Down Expand Up @@ -1305,6 +1316,11 @@ class Calculation
'functionCall' => [Statistical::class, 'MODE'],
'argumentCount' => '1+',
],
'MODE.SNGL' => [
'category' => Category::CATEGORY_STATISTICAL,
'functionCall' => [Statistical::class, 'MODE'],
'argumentCount' => '1+',
],
'MONTH' => [
'category' => Category::CATEGORY_DATE_AND_TIME,
'functionCall' => [DateTime::class, 'MONTHOFYEAR'],
Expand Down Expand Up @@ -1703,6 +1719,16 @@ class Calculation
'functionCall' => [Statistical::class, 'STDEV'],
'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' => [
'category' => Category::CATEGORY_STATISTICAL,
'functionCall' => [Statistical::class, 'STDEVA'],
Expand Down
19 changes: 19 additions & 0 deletions src/PhpSpreadsheet/Calculation/Functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace PhpOffice\PhpSpreadsheet\Calculation;

use PhpOffice\PhpSpreadsheet\Cell\Cell;

class Functions
{
const PRECISION = 8.88E-016;
Expand Down Expand Up @@ -642,4 +644,21 @@ public static function flattenSingleValue($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) === '=';
}
}
15 changes: 15 additions & 0 deletions tests/PhpSpreadsheetTests/Calculation/CalculationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -102,4 +102,19 @@ public function providerCanLoadAllSupportedLocales()
['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']);
}
}
40 changes: 40 additions & 0 deletions tests/PhpSpreadsheetTests/Calculation/FunctionsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
namespace PhpOffice\PhpSpreadsheetTests\Calculation;

use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Cell\Cell;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use PHPUnit\Framework\TestCase;

class FunctionsTest extends TestCase
Expand Down Expand Up @@ -267,4 +269,42 @@ public function providerN()
{
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';
}
}
59 changes: 59 additions & 0 deletions tests/data/Calculation/Functions/ISFORMULA.php
Original file line number Diff line number Diff line change
@@ -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!',
],
];

0 comments on commit 078a688

Please sign in to comment.