Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handling the new _xlws. prefix used for SORT() and FILTER() functions #3247

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ and this project adheres to [Semantic Versioning](https://semver.org).

### Fixed

- Fixed handling for `_xlws` prefixed functions from Office365 [Issue #3245](https://github.com/PHPOffice/PhpSpreadsheet/issues/3245) [PR #3247](https://github.com/PHPOffice/PhpSpreadsheet/pull/3247)
- Conditionals formatting rules applied to rows/columns are removed [Issue #3184](https://github.com/PHPOffice/PhpSpreadsheet/issues/3184) [PR #3213](https://github.com/PHPOffice/PhpSpreadsheet/pull/3213)
- Treat strings containing currency or accounting values as floats in Calculation Engine operations [Issue #3165](https://github.com/PHPOffice/PhpSpreadsheet/issues/3165) [PR #3189](https://github.com/PHPOffice/PhpSpreadsheet/pull/3189)
- Treat strings containing percentage values as floats in Calculation Engine operations [Issue #3155](https://github.com/PHPOffice/PhpSpreadsheet/issues/3155) [PR #3156](https://github.com/PHPOffice/PhpSpreadsheet/pull/3156) and [PR #3164](https://github.com/PHPOffice/PhpSpreadsheet/pull/3164)
Expand Down
27 changes: 26 additions & 1 deletion src/PhpSpreadsheet/Calculation/Calculation.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ 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)
const CALCULATION_REGEXP_FUNCTION = '@?(?:_xlfn\.)?([\p{L}][\p{L}\p{N}\.]*)[\s]*\(';
const CALCULATION_REGEXP_FUNCTION = '@?(?:_xlfn\.)?(?:_xlws\.)?([\p{L}][\p{L}\p{N}\.]*)[\s]*\(';
// 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.])';
// Cell reference (with or without a sheet reference) ensuring absolute/relative
Expand Down Expand Up @@ -328,6 +328,11 @@ public static function getExcelConstants(string $key)
'functionCall' => [MathTrig\Arabic::class, 'evaluate'],
'argumentCount' => '1',
],
'ANCHORARRAY' => [
'category' => Category::CATEGORY_UNCATEGORISED,
'functionCall' => [Functions::class, 'DUMMY'],
'argumentCount' => '*',
],
'AREAS' => [
'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
'functionCall' => [Functions::class, 'DUMMY'],
Expand Down Expand Up @@ -503,6 +508,16 @@ public static function getExcelConstants(string $key)
'functionCall' => [Engineering\BitWise::class, 'BITRSHIFT'],
'argumentCount' => '2',
],
'BYCOL' => [
'category' => Category::CATEGORY_LOGICAL,
'functionCall' => [Functions::class, 'DUMMY'],
'argumentCount' => '*',
],
'BYROW' => [
'category' => Category::CATEGORY_LOGICAL,
'functionCall' => [Functions::class, 'DUMMY'],
'argumentCount' => '*',
],
'CEILING' => [
'category' => Category::CATEGORY_MATH_AND_TRIG,
'functionCall' => [MathTrig\Ceiling::class, 'ceiling'],
Expand Down Expand Up @@ -1613,6 +1628,11 @@ public static function getExcelConstants(string $key)
'functionCall' => [Statistical\Deviations::class, 'kurtosis'],
'argumentCount' => '1+',
],
'LAMBDA' => [
'category' => Category::CATEGORY_UNCATEGORISED,
'functionCall' => [Functions::class, 'DUMMY'],
'argumentCount' => '*',
],
'LARGE' => [
'category' => Category::CATEGORY_STATISTICAL,
'functionCall' => [Statistical\Size::class, 'large'],
Expand Down Expand Up @@ -2307,6 +2327,11 @@ public static function getExcelConstants(string $key)
'functionCall' => [MathTrig\Trig\Sine::class, 'sin'],
'argumentCount' => '1',
],
'SINGLE' => [
'category' => Category::CATEGORY_UNCATEGORISED,
'functionCall' => [Functions::class, 'DUMMY'],
'argumentCount' => '*',
],
'SINH' => [
'category' => Category::CATEGORY_MATH_AND_TRIG,
'functionCall' => [MathTrig\Trig\Sine::class, 'sinh'],
Expand Down
1 change: 1 addition & 0 deletions src/PhpSpreadsheet/Calculation/Category.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ abstract class Category
const CATEGORY_STATISTICAL = 'Statistical';
const CATEGORY_TEXT_AND_DATA = 'Text and Data';
const CATEGORY_WEB = 'Web';
const CATEGORY_UNCATEGORISED = 'Uncategorised';
}
Binary file modified src/PhpSpreadsheet/Calculation/locale/Translations.xlsx
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

namespace PhpOffice\PhpSpreadsheet\Writer\Xlsx;

class Xlfn
class FunctionPrefix
{
const XLFNREGEXP = '/(?<!_xlfn[.])\\b('
const XLFNREGEXP = '/(?:_xlfn\.)?((?:_xlws\.)?('
// functions added with Excel 2010
. 'beta[.]dist'
. '|beta[.]inv'
Expand Down Expand Up @@ -134,6 +134,7 @@ class Xlfn
// functions added with Excel 365
. '|filter'
. '|randarray'
. '|anchorarray'
. '|sequence'
. '|sort'
. '|sortby'
Expand All @@ -143,27 +144,51 @@ class Xlfn
. '|arraytotext'
. '|call'
. '|let'
. '|lambda'
. '|single'
. '|register[.]id'
. '|textafter'
. '|textbefore'
. '|textsplit'
. '|valuetotext'
. ')(?=\\s*[(])/i';
. '))\s*\(/Umui';

const XLWSREGEXP = '/(?<!_xlws\.)('
// functions added with Excel 365
. 'filter'
. '|sort'
. ')\s*\(/mui';

/**
* Prefix function name in string with _xlfn. where required.
*/
protected static function addXlfnPrefix(string $functionString): string
{
return (string) preg_replace(self::XLFNREGEXP, '_xlfn.$1(', $functionString);
}

/**
* Prefix function name in string with _xlws. where required.
*/
protected static function addXlwsPrefix(string $functionString): string
{
return (string) preg_replace(self::XLWSREGEXP, '_xlws.$1(', $functionString);
}

/**
* Prefix function name in string with _xlfn. where required.
*/
public static function addXlfn(string $funcstring): string
public static function addFunctionPrefix(string $functionString): string
{
return (string) preg_replace(self::XLFNREGEXP, '_xlfn.$1', $funcstring);
return self::addXlwsPrefix(self::addXlfnPrefix($functionString));
}

/**
* Prefix function name in string with _xlfn. where required.
* Leading character, expected to be equals sign, is stripped.
*/
public static function addXlfnStripEquals(string $funcstring): string
public static function addFunctionPrefixStripEquals(string $functionString): string
{
return self::addXlfn(substr($funcstring, 1));
return self::addFunctionPrefix(substr($functionString, 1));
}
}
6 changes: 3 additions & 3 deletions src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php
Original file line number Diff line number Diff line change
Expand Up @@ -498,7 +498,7 @@ private static function writeOtherCondElements(XMLWriter $objWriter, Conditional
if (is_bool($formula)) {
$formula = $formula ? 'TRUE' : 'FALSE';
}
$objWriter->writeElement('formula', Xlfn::addXlfn("$formula"));
$objWriter->writeElement('formula', FunctionPrefix::addFunctionPrefix("$formula"));
}
} else {
if ($conditional->getConditionType() == Conditional::CONDITION_CONTAINSBLANKS) {
Expand Down Expand Up @@ -1204,7 +1204,7 @@ private function writeCellError(XMLWriter $objWriter, string $mappedType, string
{
$objWriter->writeAttribute('t', $mappedType);
$cellIsFormula = substr($cellValue, 0, 1) === '=';
self::writeElementIf($objWriter, $cellIsFormula, 'f', Xlfn::addXlfnStripEquals($cellValue));
self::writeElementIf($objWriter, $cellIsFormula, 'f', FunctionPrefix::addFunctionPrefixStripEquals($cellValue));
$objWriter->writeElement('v', $cellIsFormula ? $formulaerr : $cellValue);
}

Expand Down Expand Up @@ -1234,7 +1234,7 @@ private function writeCellFormula(XMLWriter $objWriter, string $cellValue, Cell
$objWriter->text(substr($cellValue, 1));
$objWriter->endElement();
} else {
$objWriter->writeElement('f', Xlfn::addXlfnStripEquals($cellValue));
$objWriter->writeElement('f', FunctionPrefix::addFunctionPrefixStripEquals($cellValue));
self::writeElementIf(
$objWriter,
$this->getParentWriter()->getOffice2003Compatibility() === false,
Expand Down
5 changes: 5 additions & 0 deletions tests/PhpSpreadsheetTests/DocumentGeneratorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,11 @@ public function providerGenerateFunctionListByCategory(): array
Excel Function | PhpSpreadsheet Function
-------------------------|--------------------------------------

## CATEGORY_UNCATEGORISED

Excel Function | PhpSpreadsheet Function
-------------------------|--------------------------------------

EXPECTED

],
Expand Down
54 changes: 54 additions & 0 deletions tests/PhpSpreadsheetTests/Writer/Xlsx/FunctionPrefixTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

namespace PhpOffice\PhpSpreadsheetTests\Writer\Xlsx;

use PhpOffice\PhpSpreadsheet\Writer\Xlsx\FunctionPrefix;
use PHPUnit\Framework\TestCase;

class FunctionPrefixTest extends TestCase
{
/**
* @dataProvider functionPrefixProvider
*/
public function testFunctionPrefix(string $expectedResult, string $functionString): void
{
$result = FunctionPrefix::addFunctionPrefix($functionString);
self::assertSame($expectedResult, $result);
}

public function functionPrefixProvider(): array
{
return [
'Basic Legacy Function' => ['SUM()', 'SUM()'],
'New Function without Prefix' => ['_xlfn.ARABIC()', 'ARABIC()'],
'New Function already Prefixed' => ['_xlfn.ARABIC()', '_xlfn.ARABIC()'],
'New Function requiring Double-Prefix' => ['_xlfn._xlws.FILTER()', 'FILTER()'],
'New Function requiring Double-Prefix already partially Prefixed' => ['_xlfn._xlws.FILTER()', '_xlfn.FILTER()'],
'New Function requiring Double-Prefix already partially Prefixed #2' => ['_xlfn._xlws.FILTER()', '_xlws.FILTER()'],
'New Function requiring Double-Prefix already Fully Prefixed' => ['_xlfn._xlws.FILTER()', '_xlfn._xlws.FILTER()'],
'Multiple Functions' => ['_xlfn._xlws.SORT(_xlfn._xlws.FILTER(A:A, A:A<>""))', 'SORT(FILTER(A:A, A:A<>""))'],
];
}

// /**
// * @dataProvider functionPrefixWithEqualsProvider
// */
// public function testFunctionPrefixWithEquals(string $expectedResult, string $functionString): void
// {
// $result = FunctionPrefix::addFunctionPrefixStripEquals($functionString);
// self::assertSame($expectedResult, $result);
// }
//
// public function functionPrefixWithEqualsProvider(): array
// {
// return [
// 'Basic Legacy Function' => ['SUM()', '=SUM()'],
// 'New Function without Prefix' => ['_xlfn.ARABIC()', '=ARABIC()'],
// 'New Function already Prefixed' => ['_xlfn.ARABIC()', '=_xlfn.ARABIC()'],
// 'New Function requiring Double-Prefix' => ['_xlfn._xlws.FILTER()', '=FILTER()'],
// 'New Function requiring Double-Prefix already partially Prefixed' => ['_xlfn._xlws.FILTER()', '=_xlfn.FILTER()'],
// 'New Function requiring Double-Prefix already partially Prefixed #2' => ['_xlfn._xlws.FILTER()', '=_xlws.FILTER()'],
// 'New Function requiring Double-Prefix already Fully Prefixed' => ['_xlfn._xlws.FILTER()', '=_xlfn._xlws.FILTER()'],
// ];
// }
}