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

Fix: SUBTOTAL to handle #REF! errors #2870

Closed
wants to merge 4 commits into from
Closed

Fix: SUBTOTAL to handle #REF! errors #2870

wants to merge 4 commits into from

Conversation

ndench
Copy link
Contributor

@ndench ndench commented Jun 3, 2022

This is:

- [x] a bugfix
- [ ] a new feature
- [ ] refactoring
- [ ] additional unit tests

Checklist:

Why this change is needed?

When Excel calculates a SUBTOTAL that contains #REF!, such as =SUBTOTAL(9, #REF!) it returns #REF!. PhpSpreadsheet causes a PHP warning to be raised due to an array unpacking in Subtotal.

The root cause is the call to Functions::flattenArrayIndexed which returns something that looks like [0 => '#REF!'] as opposed to [0.W.29 => '#REF!']. The Subtotal.php function then does an explode on the key to get the column and row values to check if they are hidden or contain other aggregate functions that the SUBTOTAL should ignore.

I'm unsure of the best way to handle this.

  1. Should the FlattenArrayIndexed be updated?
    • I've steered clear of this because it's used in many places and I was unsure of flow on effects
  2. Are there other #REF! calculation errors that should also be tracked down?
    • A quick search shows that not many places use explode and then unpack the result immediately into more than property. So I think it's unlikely.
  3. Can Subtotal.php handle other error type values?
    • There are already tests for the #VALUE! error. Excel won't let me write other errors into a column to see how it handles them with the SUBTOTAL function.

Submitting this PR to provide some failing test cases and my suggested solution (which has the fewest flow on effects possible).
Let me know if there's another way this should be handled.

@oleibman
Copy link
Collaborator

oleibman commented Jun 7, 2022

I am not duplicating your result in PhpSpreadsheet 1.23; in particular, I do not see a warning message. Please tell Php and PhpSpreadsheet release where you see a warning message (and the contents of the message).

There is at least one problem with your test case, so let's start with that. We currently cannot always distinguish the error situation #REF! from the string with the same content. You are testing the string case, and that's okay, but you should also be testing the error case. You might get that by using a cell from a non-existent sheet in your formula, e.g. =Sheet99!A1. I am a little surprised that the error condition and the string don't lead to the same result in 1.23, but the string leads to the same result as Excel #REF!, while the error condition leads to a different result (0) - your test should demonstrate that you have fixed that problem.

@ndench
Copy link
Contributor Author

ndench commented Jun 14, 2022

More details

Thanks for the comment @oleibman. I can confirm that I'm using PhpSpreadsheet 1.23.0 and PHP 8.1.6.
I see two different warning messages, both originating at MathTrig\Subtotal#38.

Warning: Undefined array key 1
Warning: Undefined array key 2

New tests

I've added in 2 more tests as you requested, both of which fail with the Undefined array key, error (Excel returns #REF! for both of these). However I'm not sure of the best way to make Subtotal.php handle these cases, since the result from Functions::flattenArrayIndexed($args) is actually an array of 0 and null instead of #REF!. This leads me to believe that something needs to be fixed in the flattenArrayIndexed method. The only thing we could do in Subtotal is to something like this:

$foo = explode('.', $index);
if (\count($foo) !== 3) {
    return false;
}

[, $row, $column] = $foo;

But that leads to a 0 result instead of #REF!. I'd really appreciate some feedback on how you think this should be handled.

RE: Unable to reproduce the issue:

I'm not sure why you can't reproduce it, but commenting out my changes to Subtotal.php gives be the following results with phpunit running on PHP 8.1.6:

root@b013d8eb1009:/var/www/html/vendor/phpoffice/phpspreadsheet# ./vendor/bin/phpunit --filter SubTotalTest
PHPUnit 9.5.20 #StandWithUkraine

..........................................EEEE                    46 / 46 (100%)

Time: 00:00.143, Memory: 76.00 MB

There were 4 errors:

1) PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig\SubTotalTest::testRefError
PhpOffice\PhpSpreadsheet\Calculation\Exception: Worksheet!A1 -> Undefined array key 1

/var/www/html/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/Cell.php:293
/var/www/html/vendor/phpoffice/phpspreadsheet/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SubTotalTest.php:135

2) PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig\SubTotalTest::testSecondaryRefError
PhpOffice\PhpSpreadsheet\Calculation\Exception: Worksheet!A1 -> Undefined array key 1

/var/www/html/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/Cell.php:293
/var/www/html/vendor/phpoffice/phpspreadsheet/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SubTotalTest.php:142

3) PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig\SubTotalTest::testNonStringSingleCellRefError
PhpOffice\PhpSpreadsheet\Calculation\Exception: Worksheet!A1 -> Undefined array key 1

/var/www/html/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/Cell.php:293
/var/www/html/vendor/phpoffice/phpspreadsheet/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SubTotalTest.php:149

4) PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig\SubTotalTest::testNonStringCellRangeRefError
PhpOffice\PhpSpreadsheet\Calculation\Exception: Worksheet!A1 -> Undefined array key 1

/var/www/html/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/Cell.php:293
/var/www/html/vendor/phpoffice/phpspreadsheet/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SubTotalTest.php:156

ERRORS!
Tests: 46, Assertions: 42, Errors: 4.

And you can see the PHP 7.4 build is now failing with Undefined offset because of the new test cases.

@oleibman oleibman marked this pull request as draft June 14, 2022 22:56
@oleibman
Copy link
Collaborator

Small changes are required in Calculation and SubTotal to get everything to work properly. Ironically, once these changes are made, your change to Subtotal is no longer necessary, as your tests will prove. For now, I am placing your change in draft status. If you are willing to expand your scope to include the change in Calculation (plus a few extra test cases that such a change will require), I can discuss that with you. Otherwise, I will open a PR to make the change. Let me know how you wish to proceed.

@ndench
Copy link
Contributor Author

ndench commented Jun 19, 2022

Ok no problem. I'm not entirely sure which changes are required. If you're able to do up a PR im happy to test it 🙂

oleibman added a commit to oleibman/PhpSpreadsheet that referenced this pull request Jun 20, 2022
This PR derives from, and supersedes, PR PHPOffice#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.
@oleibman
Copy link
Collaborator

Closing, superseded by #2902. @ndench please test. I could certainly use some more test cases in RefErrorTest.

@oleibman oleibman closed this Jun 20, 2022
oleibman added a commit that referenced this pull request Jun 26, 2022
* 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

2 participants