Skip to content

Commit 4f6d4af

Browse files
oleibmanPowerKiKi
authored andcommitted
Save Excel 2010+ Functions Properly
For functions introduced in Excel 2010 and beyond, Excel saves them in formulas with the xlfn_ prefix. PhpSpreadsheet does not do this; as a result, when a spreadsheet so created is opened, the cells which use the new functions display a #NAME? error. This the cause of bug report 1246: #1246 This change corrects that problem when the Xlsx writer encounters a 2010+ formula for a cell or a conditional style. A new class Writer/Xlsx/Xlfn, with 2 static methods, is introduced to facilitate this change. As part of the testing for this, I found some additional problems. When an unknown function name is used, Excel generates a #NAME? error. However, when an unknown function is used in PhpSpreadsheet: - if there are no parameters, it returns #VALUE!, which is wrong - if there are parameters, it throws an exception, which is horrible Both of these situations will now return #NAME? Tests have been added for these situations. The MODE (and MODE.SNGL) function is not quite in alignment with Excel. MODE(3, 3, 4, 4) returns 3 in both Excel and PhpSpreadsheet. However, MODE(4, 3, 3, 4) returns 4 in Excel, but 3 in PhpSpreadsheet. Both situations will now match Excel's result. Also, Excel allows its parameters for MODE to be an array, but PhpSpreadsheet did not; it now will. There had not been any tests for MODE. Now there are. The SHEET and SHEETS functions were introduced in Excel 2013, but were not introduced in PhpSpreadsheet. They are now introduced as DUMMY functions so that they can be parsed appropriately. Finally, in common with the "rate" changes for which I am creating a pull request at the same time as this one: samples/Basic/13_CalculationCyclicFormulae PhpUnit started reporting an error like "too much regression". The test deals with an infinite cyclic formula, and allowed the calculation engine to run for 100 cycles. The actual number of cycles seems irrelevant for the purpose of this test. I changed it to 15, and PhpUnit no longer complains.
1 parent 414e569 commit 4f6d4af

File tree

11 files changed

+665
-155
lines changed

11 files changed

+665
-155
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org).
1717
- Fix Chart samples by updating chart parameter from 0 to DataSeries::EMPTY_AS_GAP [#1448](https://github.com/PHPOffice/PhpSpreadsheet/pull/1448)
1818
- Fix return type in docblock for the Cells::get() [#1398](https://github.com/PHPOffice/PhpSpreadsheet/pull/1398)
1919
- Fix RATE, PRICE, XIRR, and XNPV Functions [#1456](https://github.com/PHPOffice/PhpSpreadsheet/pull/1456)
20+
- Save Excel 2010+ functions properly in XLSX [#1461](https://github.com/PHPOffice/PhpSpreadsheet/pull/1461)
2021

2122
### Changed
2223

src/PhpSpreadsheet/Calculation/Calculation.php

+38-24
Original file line numberDiff line numberDiff line change
@@ -1853,6 +1853,16 @@ class Calculation
18531853
'functionCall' => [MathTrig::class, 'SERIESSUM'],
18541854
'argumentCount' => '4',
18551855
],
1856+
'SHEET' => [
1857+
'category' => Category::CATEGORY_INFORMATION,
1858+
'functionCall' => [Functions::class, 'DUMMY'],
1859+
'argumentCount' => '0,1',
1860+
],
1861+
'SHEETS' => [
1862+
'category' => Category::CATEGORY_INFORMATION,
1863+
'functionCall' => [Functions::class, 'DUMMY'],
1864+
'argumentCount' => '0,1',
1865+
],
18561866
'SIGN' => [
18571867
'category' => Category::CATEGORY_MATH_AND_TRIG,
18581868
'functionCall' => [MathTrig::class, 'SIGN'],
@@ -2247,6 +2257,10 @@ class Calculation
22472257
'argumentCount' => '*',
22482258
'functionCall' => [__CLASS__, 'mkMatrix'],
22492259
],
2260+
'NAME.ERROR' => [
2261+
'argumentCount' => '*',
2262+
'functionCall' => [Functions::class, 'NAME'],
2263+
],
22502264
];
22512265

22522266
public function __construct(Spreadsheet $spreadsheet = null)
@@ -3615,33 +3629,33 @@ private function _parseFormula($formula, Cell $pCell = null)
36153629
$val = preg_replace('/\s/u', '', $val);
36163630
if (isset(self::$phpSpreadsheetFunctions[strtoupper($matches[1])]) || isset(self::$controlFunctions[strtoupper($matches[1])])) { // it's a function
36173631
$valToUpper = strtoupper($val);
3618-
// here $matches[1] will contain values like "IF"
3619-
// and $val "IF("
3620-
if ($this->branchPruningEnabled && ($valToUpper == 'IF(')) { // we handle a new if
3621-
$pendingStoreKey = $this->getUnusedBranchStoreKey();
3622-
$pendingStoreKeysStack[] = $pendingStoreKey;
3623-
$expectingConditionMap[$pendingStoreKey] = true;
3624-
$parenthesisDepthMap[$pendingStoreKey] = 0;
3625-
} else { // this is not a if but we good deeper
3626-
if (!empty($pendingStoreKey) && array_key_exists($pendingStoreKey, $parenthesisDepthMap)) {
3627-
$parenthesisDepthMap[$pendingStoreKey] += 1;
3628-
}
3632+
} else {
3633+
$valToUpper = 'NAME.ERROR(';
3634+
}
3635+
// here $matches[1] will contain values like "IF"
3636+
// and $val "IF("
3637+
if ($this->branchPruningEnabled && ($valToUpper == 'IF(')) { // we handle a new if
3638+
$pendingStoreKey = $this->getUnusedBranchStoreKey();
3639+
$pendingStoreKeysStack[] = $pendingStoreKey;
3640+
$expectingConditionMap[$pendingStoreKey] = true;
3641+
$parenthesisDepthMap[$pendingStoreKey] = 0;
3642+
} else { // this is not an if but we go deeper
3643+
if (!empty($pendingStoreKey) && array_key_exists($pendingStoreKey, $parenthesisDepthMap)) {
3644+
$parenthesisDepthMap[$pendingStoreKey] += 1;
36293645
}
3646+
}
36303647

3631-
$stack->push('Function', $valToUpper, null, $currentCondition, $currentOnlyIf, $currentOnlyIfNot);
3632-
// tests if the function is closed right after opening
3633-
$ax = preg_match('/^\s*(\s*\))/ui', substr($formula, $index + $length), $amatch);
3634-
if ($ax) {
3635-
$stack->push('Operand Count for Function ' . $valToUpper . ')', 0, null, $currentCondition, $currentOnlyIf, $currentOnlyIfNot);
3636-
$expectingOperator = true;
3637-
} else {
3638-
$stack->push('Operand Count for Function ' . $valToUpper . ')', 1, null, $currentCondition, $currentOnlyIf, $currentOnlyIfNot);
3639-
$expectingOperator = false;
3640-
}
3641-
$stack->push('Brace', '(');
3642-
} else { // it's a var w/ implicit multiplication
3643-
$output[] = ['type' => 'Value', 'value' => $matches[1], 'reference' => null];
3648+
$stack->push('Function', $valToUpper, null, $currentCondition, $currentOnlyIf, $currentOnlyIfNot);
3649+
// tests if the function is closed right after opening
3650+
$ax = preg_match('/^\s*\)/u', substr($formula, $index + $length));
3651+
if ($ax) {
3652+
$stack->push('Operand Count for Function ' . $valToUpper . ')', 0, null, $currentCondition, $currentOnlyIf, $currentOnlyIfNot);
3653+
$expectingOperator = true;
3654+
} else {
3655+
$stack->push('Operand Count for Function ' . $valToUpper . ')', 1, null, $currentCondition, $currentOnlyIf, $currentOnlyIfNot);
3656+
$expectingOperator = false;
36443657
}
3658+
$stack->push('Brace', '(');
36453659
} elseif (preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/i', $val, $matches)) {
36463660
// Watch for this case-change when modifying to allow cell references in different worksheets...
36473661
// Should only be applied to the actual cell column, not the worksheet name

src/PhpSpreadsheet/Calculation/Statistical.php

+19-8
Original file line numberDiff line numberDiff line change
@@ -2468,11 +2468,27 @@ public static function MINIFS(...$args)
24682468
private static function modeCalc($data)
24692469
{
24702470
$frequencyArray = [];
2471+
$index = 0;
2472+
$maxfreq = 0;
2473+
$maxfreqkey = '';
2474+
$maxfreqdatum = '';
24712475
foreach ($data as $datum) {
24722476
$found = false;
2477+
++$index;
24732478
foreach ($frequencyArray as $key => $value) {
24742479
if ((string) $value['value'] == (string) $datum) {
24752480
++$frequencyArray[$key]['frequency'];
2481+
$freq = $frequencyArray[$key]['frequency'];
2482+
if ($freq > $maxfreq) {
2483+
$maxfreq = $freq;
2484+
$maxfreqkey = $key;
2485+
$maxfreqdatum = $datum;
2486+
} elseif ($freq == $maxfreq) {
2487+
if ($frequencyArray[$key]['index'] < $frequencyArray[$maxfreqkey]['index']) {
2488+
$maxfreqkey = $key;
2489+
$maxfreqdatum = $datum;
2490+
}
2491+
}
24762492
$found = true;
24772493

24782494
break;
@@ -2482,21 +2498,16 @@ private static function modeCalc($data)
24822498
$frequencyArray[] = [
24832499
'value' => $datum,
24842500
'frequency' => 1,
2501+
'index' => $index,
24852502
];
24862503
}
24872504
}
24882505

2489-
foreach ($frequencyArray as $key => $value) {
2490-
$frequencyList[$key] = $value['frequency'];
2491-
$valueList[$key] = $value['value'];
2492-
}
2493-
array_multisort($frequencyList, SORT_DESC, $valueList, SORT_ASC, SORT_NUMERIC, $frequencyArray);
2494-
2495-
if ($frequencyArray[0]['frequency'] == 1) {
2506+
if ($maxfreq <= 1) {
24962507
return Functions::NA();
24972508
}
24982509

2499-
return $frequencyArray[0]['value'];
2510+
return $maxfreqdatum;
25002511
}
25012512

25022513
/**

src/PhpSpreadsheet/Calculation/functionlist.txt

+2
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,8 @@ SEC
316316
SECH
317317
SECOND
318318
SERIESSUM
319+
SHEET
320+
SHEETS
319321
SIGN
320322
SIN
321323
SINH

0 commit comments

Comments
 (0)