Skip to content

Commit a895721

Browse files
authored
Handling of #REF! Errors in Subtotal, and More (#2902)
* Handling of #REF! Errors in Subtotal, and More This PR derives from, and supersedes, PR #2870, submitted by @ndench. The problem reported in the original is that SUBTOTAL does not handle #REF! errors in its arguments properly; however, my investigation has enlarged the scope. The main problem is in Calculation, and it has a simple fix. When the calculation engine finds a reference to an uninitialized cell, it uses `null` as the value. This is appropriate when the cell belongs to a defined sheet; however, for an undefined sheet, #REF! is more appropriate. With that fix in place, SUBTOTAL still needs a small fix of its own. It tries to parse its cell reference arguments into an array, but, if the reference does not match the expected format (as #REF! will not), this results in referencing undefined array indexes, with attendant messages. That assignment is changed to be more flexible, eliminating the problem and the messages. Those 2 fixes are sufficient to ensure that the original problem is resolved. It also resolves a similar problem with some other functions (e.g. SUM). However, it does not resolve it for all functions. Or, to be more particular, many functions will return #VALUE! rather than #REF! if this arises, and the same is true for other errors in the function arguments, e.g. #DIV/0!. This PR does not attempt to address all functions; I need to think of a systematic way to pursue that. However, at least for most MathTrig functions, which validate their arguments using a common method, it is relatively easy to get the function to propagate the proper error result. * Arrange Array The Way call_user_func_array Wants Problem with Php8.0+ - array passed to call_user_func_array must have int keys before string keys, otherwise Php thinks we are passing positional parameters after keyword parameters. 7 other functions use flattenArrayIndexed, but Subtotal is the only one which uses that result to subsequently pass arguments to call_user_func_array. So the others should not require a change. A specific test is added for SUM to validate that conclusion. * Change Needed for Hidden Row Filter Same as change made to Formula Args filter.
1 parent 177a362 commit a895721

File tree

7 files changed

+122
-7
lines changed

7 files changed

+122
-7
lines changed

src/PhpSpreadsheet/Calculation/Calculation.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4834,7 +4834,7 @@ private function processTokenStack($tokens, $cellID = null, ?Cell $cell = null)
48344834
$cell->attach($pCellParent);
48354835
} else {
48364836
$cellRef = ($cellSheet !== null) ? "'{$matches[2]}'!{$cellRef}" : $cellRef;
4837-
$cellValue = null;
4837+
$cellValue = ($cellSheet !== null) ? null : Information\ExcelError::REF();
48384838
}
48394839
} else {
48404840
return $this->raiseFormulaError('Unable to access Cell Reference');

src/PhpSpreadsheet/Calculation/Information/ExcelError.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,14 @@ class ExcelError
2525
'spill' => '#SPILL!',
2626
];
2727

28+
/**
29+
* @param mixed $value
30+
*/
31+
public static function throwError($value): string
32+
{
33+
return in_array($value, self::$errorCodes, true) ? $value : self::$errorCodes['value'];
34+
}
35+
2836
/**
2937
* ERROR_TYPE.
3038
*

src/PhpSpreadsheet/Calculation/MathTrig/Helpers.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public static function validateNumericNullBool($number)
3838
return 0 + $number;
3939
}
4040

41-
throw new Exception(ExcelError::VALUE());
41+
throw new Exception(ExcelError::throwError($number));
4242
}
4343

4444
/**
@@ -59,7 +59,7 @@ public static function validateNumericNullSubstitution($number, $substitute)
5959
return 0 + $number;
6060
}
6161

62-
throw new Exception(ExcelError::VALUE());
62+
throw new Exception(ExcelError::throwError($number));
6363
}
6464

6565
/**

src/PhpSpreadsheet/Calculation/MathTrig/Operations.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ function ($value) {
118118
if (is_numeric($arg)) {
119119
$returnValue *= $arg;
120120
} else {
121-
return ExcelError::VALUE();
121+
return ExcelError::throwError($arg);
122122
}
123123
}
124124

src/PhpSpreadsheet/Calculation/MathTrig/Subtotal.php

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,11 @@ protected static function filterHiddenArgs($cellReference, $args): array
1818
return array_filter(
1919
$args,
2020
function ($index) use ($cellReference) {
21-
[, $row, ] = explode('.', $index);
21+
$explodeArray = explode('.', $index);
22+
$row = $explodeArray[1] ?? '';
23+
if (!is_numeric($row)) {
24+
return true;
25+
}
2226

2327
return $cellReference->getWorksheet()->getRowDimension($row)->getVisible();
2428
},
@@ -35,7 +39,9 @@ protected static function filterFormulaArgs($cellReference, $args): array
3539
return array_filter(
3640
$args,
3741
function ($index) use ($cellReference) {
38-
[, $row, $column] = explode('.', $index);
42+
$explodeArray = explode('.', $index);
43+
$row = $explodeArray[1] ?? '';
44+
$column = $explodeArray[2] ?? '';
3945
$retVal = true;
4046
if ($cellReference->getWorksheet()->cellExists($column . $row)) {
4147
//take this cell out if it contains the SUBTOTAL or AGGREGATE functions in a formula
@@ -87,7 +93,22 @@ function ($index) use ($cellReference) {
8793
public static function evaluate($functionType, ...$args)
8894
{
8995
$cellReference = array_pop($args);
90-
$aArgs = Functions::flattenArrayIndexed($args);
96+
$bArgs = Functions::flattenArrayIndexed($args);
97+
$aArgs = [];
98+
// int keys must come before string keys for PHP 8.0+
99+
// Otherwise, PHP thinks positional args follow keyword
100+
// in the subsequent call to call_user_func_array.
101+
// Fortunately, order of args is unimportant to Subtotal.
102+
foreach ($bArgs as $key => $value) {
103+
if (is_int($key)) {
104+
$aArgs[$key] = $value;
105+
}
106+
}
107+
foreach ($bArgs as $key => $value) {
108+
if (!is_int($key)) {
109+
$aArgs[$key] = $value;
110+
}
111+
}
91112

92113
try {
93114
$subtotal = (int) Helpers::validateNumericNullBool($functionType);

tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SubTotalTest.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,4 +127,32 @@ public function testSubtotalNested(): void
127127
$sheet->getCell('H1')->setValue("=SUBTOTAL(9, A1:$maxCol$maxRow)");
128128
self::assertEquals(362, $sheet->getCell('H1')->getCalculatedValue());
129129
}
130+
131+
public function testRefError(): void
132+
{
133+
$sheet = $this->getSheet();
134+
$sheet->getCell('A1')->setValue('=SUBTOTAL(9, #REF!)');
135+
self::assertEquals('#REF!', $sheet->getCell('A1')->getCalculatedValue());
136+
}
137+
138+
public function testSecondaryRefError(): void
139+
{
140+
$sheet = $this->getSheet();
141+
$sheet->getCell('A1')->setValue('=SUBTOTAL(9, B1:B9,#REF!,C1:C9)');
142+
self::assertEquals('#REF!', $sheet->getCell('A1')->getCalculatedValue());
143+
}
144+
145+
public function testNonStringSingleCellRefError(): void
146+
{
147+
$sheet = $this->getSheet();
148+
$sheet->getCell('A1')->setValue('=SUBTOTAL(9, 1, C1, Sheet99!A11)');
149+
self::assertEquals('#REF!', $sheet->getCell('A1')->getCalculatedValue());
150+
}
151+
152+
public function testNonStringCellRangeRefError(): void
153+
{
154+
$sheet = $this->getSheet();
155+
$sheet->getCell('A1')->setValue('=SUBTOTAL(9, Sheet99!A1)');
156+
self::assertEquals('#REF!', $sheet->getCell('A1')->getCalculatedValue());
157+
}
130158
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?php
2+
3+
namespace PhpOffice\PhpSpreadsheetTests\Calculation;
4+
5+
use PhpOffice\PhpSpreadsheet\Spreadsheet;
6+
use PHPUnit\Framework\TestCase;
7+
8+
class RefErrorTest extends TestCase
9+
{
10+
/**
11+
* @param mixed $expected
12+
*
13+
* @dataProvider providerRefError
14+
*/
15+
public function testRefError($expected, string $formula): void
16+
{
17+
$spreadsheet = new Spreadsheet();
18+
$sheet1 = $spreadsheet->getActiveSheet();
19+
$sheet1->setTitle('Sheet1');
20+
$sheet2 = $spreadsheet->createSheet();
21+
$sheet2->setTitle('Sheet2');
22+
$sheet2->getCell('A1')->setValue(5);
23+
$sheet1->getCell('A1')->setValue(9);
24+
$sheet1->getCell('A2')->setValue(2);
25+
$sheet1->getCell('A3')->setValue(4);
26+
$sheet1->getCell('A4')->setValue(6);
27+
$sheet1->getCell('A5')->setValue(7);
28+
$sheet1->getRowDimension(5)->setVisible(false);
29+
$sheet1->getCell('B1')->setValue('=1/0');
30+
$sheet1->getCell('C1')->setValue('=Sheet99!A1');
31+
$sheet1->getCell('C2')->setValue('=Sheet2!A1');
32+
$sheet1->getCell('C3')->setValue('=Sheet2!A2');
33+
$sheet1->getCell('H1')->setValue($formula);
34+
self::assertSame($expected, $sheet1->getCell('H1')->getCalculatedValue());
35+
$spreadsheet->disconnectWorksheets();
36+
}
37+
38+
public function providerRefError(): array
39+
{
40+
return [
41+
'Subtotal9 Ok' => [12, '=SUBTOTAL(A1,A2:A4)'],
42+
'Subtotal9 REF' => ['#REF!', '=SUBTOTAL(A1,A2:A4,C1)'],
43+
'Subtotal9 with literal and cells' => [111, '=SUBTOTAL(A1,A2:A4,99)'],
44+
'Subtotal9 with literal no rows hidden' => [111, '=SUBTOTAL(109,A2:A4,99)'],
45+
'Subtotal9 with literal ignoring hidden row' => [111, '=SUBTOTAL(109,A2:A5,99)'],
46+
'Subtotal9 with literal using hidden row' => [118, '=SUBTOTAL(9,A2:A5,99)'],
47+
'Subtotal9 with Null same sheet' => [12, '=SUBTOTAL(A1,A2:A4,A99)'],
48+
'Subtotal9 with Null Different sheet' => [12, '=SUBTOTAL(A1,A2:A4,C3)'],
49+
'Subtotal9 with NonNull Different sheet' => [17, '=SUBTOTAL(A1,A2:A4,C2)'],
50+
'Product DIV0' => ['#DIV/0!', '=PRODUCT(2, 3, B1)'],
51+
'Sqrt REF' => ['#REF!', '=SQRT(C1)'],
52+
'Sum NUM' => ['#NUM!', '=SUM(SQRT(-1), A2:A4)'],
53+
'Sum with literal and cells' => [111, '=SUM(A2:A4, 99)'],
54+
'Sum REF' => ['#REF!', '=SUM(A2:A4, C1)'],
55+
'Tan DIV0' => ['#DIV/0!', '=TAN(B1)'],
56+
];
57+
}
58+
}

0 commit comments

Comments
 (0)