Skip to content

Commit 5839533

Browse files
committed
Handling the new _xlws. prefix used for SORT() and FILTER() functions in both the Calculation Engine, and in the Xlsx Writer
Create stubs for new uncategorised ANCHORARRAY(), LAMBDA(), and SINGLE(); and for the new Logical functions BYCOL() and BYROW()
1 parent c8cd9a8 commit 5839533

File tree

7 files changed

+122
-11
lines changed

7 files changed

+122
-11
lines changed

CHANGELOG.md

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

3232
### Fixed
3333

34+
- 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)
3435
- 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)
3536
- 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)
3637
- 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)

src/PhpSpreadsheet/Calculation/Calculation.php

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ class Calculation
3333
// Opening bracket
3434
const CALCULATION_REGEXP_OPENBRACE = '\(';
3535
// Function (allow for the old @ symbol that could be used to prefix a function, but we'll ignore it)
36-
const CALCULATION_REGEXP_FUNCTION = '@?(?:_xlfn\.)?([\p{L}][\p{L}\p{N}\.]*)[\s]*\(';
36+
const CALCULATION_REGEXP_FUNCTION = '@?(?:_xlfn\.)?(?:_xlws\.)?([\p{L}][\p{L}\p{N}\.]*)[\s]*\(';
3737
// Cell reference (cell or range of cells, with or without a sheet reference)
3838
const CALCULATION_REGEXP_CELLREF = '((([^\s,!&%^\/\*\+<>=:`-]*)|(\'(?:[^\']|\'[^!])+?\')|(\"(?:[^\"]|\"[^!])+?\"))!)?\$?\b([a-z]{1,3})\$?(\d{1,7})(?![\w.])';
3939
// Cell reference (with or without a sheet reference) ensuring absolute/relative
@@ -328,6 +328,11 @@ public static function getExcelConstants(string $key)
328328
'functionCall' => [MathTrig\Arabic::class, 'evaluate'],
329329
'argumentCount' => '1',
330330
],
331+
'ANCHORARRAY' => [
332+
'category' => Category::CATEGORY_UNCATEGORISED,
333+
'functionCall' => [Functions::class, 'DUMMY'],
334+
'argumentCount' => '*',
335+
],
331336
'AREAS' => [
332337
'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
333338
'functionCall' => [Functions::class, 'DUMMY'],
@@ -503,6 +508,16 @@ public static function getExcelConstants(string $key)
503508
'functionCall' => [Engineering\BitWise::class, 'BITRSHIFT'],
504509
'argumentCount' => '2',
505510
],
511+
'BYCOL' => [
512+
'category' => Category::CATEGORY_LOGICAL,
513+
'functionCall' => [Functions::class, 'DUMMY'],
514+
'argumentCount' => '*',
515+
],
516+
'BYROW' => [
517+
'category' => Category::CATEGORY_LOGICAL,
518+
'functionCall' => [Functions::class, 'DUMMY'],
519+
'argumentCount' => '*',
520+
],
506521
'CEILING' => [
507522
'category' => Category::CATEGORY_MATH_AND_TRIG,
508523
'functionCall' => [MathTrig\Ceiling::class, 'ceiling'],
@@ -1613,6 +1628,11 @@ public static function getExcelConstants(string $key)
16131628
'functionCall' => [Statistical\Deviations::class, 'kurtosis'],
16141629
'argumentCount' => '1+',
16151630
],
1631+
'LAMBDA' => [
1632+
'category' => Category::CATEGORY_UNCATEGORISED,
1633+
'functionCall' => [Functions::class, 'DUMMY'],
1634+
'argumentCount' => '*',
1635+
],
16161636
'LARGE' => [
16171637
'category' => Category::CATEGORY_STATISTICAL,
16181638
'functionCall' => [Statistical\Size::class, 'large'],
@@ -2307,6 +2327,11 @@ public static function getExcelConstants(string $key)
23072327
'functionCall' => [MathTrig\Trig\Sine::class, 'sin'],
23082328
'argumentCount' => '1',
23092329
],
2330+
'SINGLE' => [
2331+
'category' => Category::CATEGORY_UNCATEGORISED,
2332+
'functionCall' => [Functions::class, 'DUMMY'],
2333+
'argumentCount' => '*',
2334+
],
23102335
'SINH' => [
23112336
'category' => Category::CATEGORY_MATH_AND_TRIG,
23122337
'functionCall' => [MathTrig\Trig\Sine::class, 'sinh'],

src/PhpSpreadsheet/Calculation/Category.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,5 @@ abstract class Category
1717
const CATEGORY_STATISTICAL = 'Statistical';
1818
const CATEGORY_TEXT_AND_DATA = 'Text and Data';
1919
const CATEGORY_WEB = 'Web';
20+
const CATEGORY_UNCATEGORISED = 'Uncategorised';
2021
}

src/PhpSpreadsheet/Writer/Xlsx/Xlfn.php renamed to src/PhpSpreadsheet/Writer/Xlsx/FunctionPrefix.php

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22

33
namespace PhpOffice\PhpSpreadsheet\Writer\Xlsx;
44

5-
class Xlfn
5+
class FunctionPrefix
66
{
7-
const XLFNREGEXP = '/(?<!_xlfn[.])\\b('
7+
const XLFNREGEXP = '/(?:_xlfn\.)?((?:_xlws\.)?('
88
// functions added with Excel 2010
99
. 'beta[.]dist'
1010
. '|beta[.]inv'
@@ -134,6 +134,7 @@ class Xlfn
134134
// functions added with Excel 365
135135
. '|filter'
136136
. '|randarray'
137+
. '|anchorarray'
137138
. '|sequence'
138139
. '|sort'
139140
. '|sortby'
@@ -143,27 +144,51 @@ class Xlfn
143144
. '|arraytotext'
144145
. '|call'
145146
. '|let'
147+
. '|lambda'
148+
. '|single'
146149
. '|register[.]id'
147150
. '|textafter'
148151
. '|textbefore'
149152
. '|textsplit'
150153
. '|valuetotext'
151-
. ')(?=\\s*[(])/i';
154+
. '))\s*\(/Umui';
155+
156+
const XLWSREGEXP = '/(?<!_xlws\.)('
157+
// functions added with Excel 365
158+
. 'filter'
159+
. '|sort'
160+
. ')\s*\(/mui';
161+
162+
/**
163+
* Prefix function name in string with _xlfn. where required.
164+
*/
165+
protected static function addXlfnPrefix(string $functionString): string
166+
{
167+
return (string) preg_replace(self::XLFNREGEXP, '_xlfn.$1(', $functionString);
168+
}
169+
170+
/**
171+
* Prefix function name in string with _xlws. where required.
172+
*/
173+
protected static function addXlwsPrefix(string $functionString): string
174+
{
175+
return (string) preg_replace(self::XLWSREGEXP, '_xlws.$1(', $functionString);
176+
}
152177

153178
/**
154179
* Prefix function name in string with _xlfn. where required.
155180
*/
156-
public static function addXlfn(string $funcstring): string
181+
public static function addFunctionPrefix(string $functionString): string
157182
{
158-
return (string) preg_replace(self::XLFNREGEXP, '_xlfn.$1', $funcstring);
183+
return self::addXlwsPrefix(self::addXlfnPrefix($functionString));
159184
}
160185

161186
/**
162187
* Prefix function name in string with _xlfn. where required.
163188
* Leading character, expected to be equals sign, is stripped.
164189
*/
165-
public static function addXlfnStripEquals(string $funcstring): string
190+
public static function addFunctionPrefixStripEquals(string $functionString): string
166191
{
167-
return self::addXlfn(substr($funcstring, 1));
192+
return self::addFunctionPrefix(substr($functionString, 1));
168193
}
169194
}

src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -498,7 +498,7 @@ private static function writeOtherCondElements(XMLWriter $objWriter, Conditional
498498
if (is_bool($formula)) {
499499
$formula = $formula ? 'TRUE' : 'FALSE';
500500
}
501-
$objWriter->writeElement('formula', Xlfn::addXlfn("$formula"));
501+
$objWriter->writeElement('formula', FunctionPrefix::addFunctionPrefix("$formula"));
502502
}
503503
} else {
504504
if ($conditional->getConditionType() == Conditional::CONDITION_CONTAINSBLANKS) {
@@ -1204,7 +1204,7 @@ private function writeCellError(XMLWriter $objWriter, string $mappedType, string
12041204
{
12051205
$objWriter->writeAttribute('t', $mappedType);
12061206
$cellIsFormula = substr($cellValue, 0, 1) === '=';
1207-
self::writeElementIf($objWriter, $cellIsFormula, 'f', Xlfn::addXlfnStripEquals($cellValue));
1207+
self::writeElementIf($objWriter, $cellIsFormula, 'f', FunctionPrefix::addFunctionPrefixStripEquals($cellValue));
12081208
$objWriter->writeElement('v', $cellIsFormula ? $formulaerr : $cellValue);
12091209
}
12101210

@@ -1234,7 +1234,7 @@ private function writeCellFormula(XMLWriter $objWriter, string $cellValue, Cell
12341234
$objWriter->text(substr($cellValue, 1));
12351235
$objWriter->endElement();
12361236
} else {
1237-
$objWriter->writeElement('f', Xlfn::addXlfnStripEquals($cellValue));
1237+
$objWriter->writeElement('f', FunctionPrefix::addFunctionPrefixStripEquals($cellValue));
12381238
self::writeElementIf(
12391239
$objWriter,
12401240
$this->getParentWriter()->getOffice2003Compatibility() === false,

tests/PhpSpreadsheetTests/DocumentGeneratorTest.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,11 @@ public function providerGenerateFunctionListByCategory(): array
133133
Excel Function | PhpSpreadsheet Function
134134
-------------------------|--------------------------------------
135135
136+
## CATEGORY_UNCATEGORISED
137+
138+
Excel Function | PhpSpreadsheet Function
139+
-------------------------|--------------------------------------
140+
136141
EXPECTED
137142

138143
],
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
3+
namespace PhpOffice\PhpSpreadsheetTests\Writer\Xlsx;
4+
5+
use PhpOffice\PhpSpreadsheet\Writer\Xlsx\FunctionPrefix;
6+
use PHPUnit\Framework\TestCase;
7+
8+
class FunctionPrefixTest extends TestCase
9+
{
10+
/**
11+
* @dataProvider functionPrefixProvider
12+
*/
13+
public function testFunctionPrefix(string $expectedResult, string $functionString): void
14+
{
15+
$result = FunctionPrefix::addFunctionPrefix($functionString);
16+
self::assertSame($expectedResult, $result);
17+
}
18+
19+
public function functionPrefixProvider(): array
20+
{
21+
return [
22+
'Basic Legacy Function' => ['SUM()', 'SUM()'],
23+
'New Function without Prefix' => ['_xlfn.ARABIC()', 'ARABIC()'],
24+
'New Function already Prefixed' => ['_xlfn.ARABIC()', '_xlfn.ARABIC()'],
25+
'New Function requiring Double-Prefix' => ['_xlfn._xlws.FILTER()', 'FILTER()'],
26+
'New Function requiring Double-Prefix already partially Prefixed' => ['_xlfn._xlws.FILTER()', '_xlfn.FILTER()'],
27+
'New Function requiring Double-Prefix already partially Prefixed #2' => ['_xlfn._xlws.FILTER()', '_xlws.FILTER()'],
28+
'New Function requiring Double-Prefix already Fully Prefixed' => ['_xlfn._xlws.FILTER()', '_xlfn._xlws.FILTER()'],
29+
'Multiple Functions' => ['_xlfn._xlws.SORT(_xlfn._xlws.FILTER(A:A, A:A<>""))', 'SORT(FILTER(A:A, A:A<>""))'],
30+
];
31+
}
32+
33+
// /**
34+
// * @dataProvider functionPrefixWithEqualsProvider
35+
// */
36+
// public function testFunctionPrefixWithEquals(string $expectedResult, string $functionString): void
37+
// {
38+
// $result = FunctionPrefix::addFunctionPrefixStripEquals($functionString);
39+
// self::assertSame($expectedResult, $result);
40+
// }
41+
//
42+
// public function functionPrefixWithEqualsProvider(): array
43+
// {
44+
// return [
45+
// 'Basic Legacy Function' => ['SUM()', '=SUM()'],
46+
// 'New Function without Prefix' => ['_xlfn.ARABIC()', '=ARABIC()'],
47+
// 'New Function already Prefixed' => ['_xlfn.ARABIC()', '=_xlfn.ARABIC()'],
48+
// 'New Function requiring Double-Prefix' => ['_xlfn._xlws.FILTER()', '=FILTER()'],
49+
// 'New Function requiring Double-Prefix already partially Prefixed' => ['_xlfn._xlws.FILTER()', '=_xlfn.FILTER()'],
50+
// 'New Function requiring Double-Prefix already partially Prefixed #2' => ['_xlfn._xlws.FILTER()', '=_xlws.FILTER()'],
51+
// 'New Function requiring Double-Prefix already Fully Prefixed' => ['_xlfn._xlws.FILTER()', '=_xlfn._xlws.FILTER()'],
52+
// ];
53+
// }
54+
}

0 commit comments

Comments
 (0)