-
Notifications
You must be signed in to change notification settings - Fork 3.6k
Improved Support for INDIRECT, ROW, and COLUMN Functions #1995
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
Conversation
This should address issues #1913 and #1993. INDIRECT had heretofore not supported an optional parameter intended to support addresses in R1C1 format which was introduced with Excel 2010. It also had not supported the use of defined names as an argument. The unit tests for INDIRECT had used mocking, and were sorely lacking (tested only error conditions). They have been replaced with normal, and hopefully adequate, tests. This includes testing globally defined names, as well as locally defined names, both in and out of scope. The test case in 1913 was too complicated for me to add as a unit test. The main impediments to it are now removed, and its complex situation, will I hope, be corrected with this fix.
One doc block, one in deprecated function.
2 corrections.
They need it too, just like INDIRECT.
The ROW and COLUMN functions also should support defined names. I have added that, and test cases, with the latest push. ROWS and COLUMNS already supported it correctly, but there were no test cases. Because ROW and COLUMN can return arrays, and PhpSpreadsheet does not support dynamic arrays, I left the existing direct-call tests unchanged to demonstrate those capabilities. |
Still getting used to it.
Third times a charm?
@PowerKiKi, I am getting an error I do not understand from Phpstan.
If I'm reading it correctly, it says it's expecting to receive an error, and failing because it doesn't receive one. Is that true? Whether it is true or not, do you know how I would go about fixing it? |
I have a small test script that I use to debug the calculation engine, dumping the token stack, and that enables the calculation engine log to show the various steps taken to evaluate a formula: $spreadSheet = new Spreadsheet();
$workSheet = $spreadSheet->getActiveSheet();
$workSheet->setCellValue('A1', 'EUR');
$workSheet->setCellValue('A2', 'USD');
$workSheet->setCellValue('B1', 360);
$workSheet->setCellValue('B2', 300);
$spreadSheet->addNamedRange(new \PhpOffice\PhpSpreadsheet\NamedRange('EUR', $workSheet, '=$B$1'));
$spreadSheet->addNamedRange(new \PhpOffice\PhpSpreadsheet\NamedRange('USD', $workSheet, '=$B$2'));
$cell = $workSheet->getCell('E1');
$cell->setValue('=INDIRECT("USD")');
$spreadSheet->setActiveSheetIndex(0);
$formulaCells = [
'E1',
];
foreach ($formulaCells as $cellAddress) {
$result = evaluate($spreadSheet, $spreadSheet->getActiveSheet(), $cellAddress);
}
function evaluate(Spreadsheet $spreadSheet, Worksheet $workSheet, string $cell)
{
// Initialise the calculation engine for debug logging
$calculationEngine = Calculation::getInstance($spreadSheet);
$debugLog = $calculationEngine->getDebugLog();
$calculationEngine->flushInstance();
$debugLog->setWriteDebugLog(true);
$formulaValue = $workSheet->getCell($cell)->getValue();
echo PHP_EOL, 'Formula value for evaluation is ', $formulaValue, PHP_EOL;
$canExecuteCalculation = false;
try {
$tokens = $calculationEngine->parseFormula($formulaValue, $workSheet->getCell($cell));
echo 'Parser Stack :-', PHP_EOL;
print_r($tokens);
$canExecuteCalculation = true;
} catch (Exception $e) {
echo 'PARSER ERROR: ', $e->getMessage(), PHP_EOL;
echo 'Parser Stack :-';
print_r($tokens);
}
$callStartTime = microtime(true);
if ($canExecuteCalculation) {
// calculate
try {
$cellValue = $workSheet->getCell($cell)->getCalculatedValue();
echo 'Result is ', $cellValue, PHP_EOL;
echo 'Evaluation Log:', PHP_EOL;
print_r($debugLog->getLog());
} catch (Exception $e) {
echo 'CALCULATION ENGINE ERROR: ', $e->getMessage(), PHP_EOL;
echo 'Evaluation Log:', PHP_EOL;
print_r($debugLog->getLog());
}
}
$callEndTime = microtime(true);
$callTime = $callEndTime - $callStartTime;
echo PHP_EOL;
echo 'Call time to evaluate formula was ', sprintf('%.4f', $callTime), ' seconds', PHP_EOL;
return $cellValue;
}
// Echo memory usage
echo ' Current memory usage: ' , (memory_get_usage(true) / 1024) , ' KB' , PHP_EOL;
echo ' Peak memory usage: ' , (memory_get_peak_usage(true) / 1024) , ' KB' , PHP_EOL; In this case, I've set it up to evaluate
Running the same script against my early partial PR to resolve #1993 gives the following correct result:
So I'll need to take a closer look at how your INDIRECT() is handling the defined name |
The PHPStan Error is the same as I was getting over the weekend: an error that PHPStan had previously been told to ignore now no longer exists because it has been fixed by your changes, so PHPStan chooses to tell you that the lack of an error is now an error. |
My fork was created before phpstan, so I don't think I can make the phpstan-baseline.neon change you recommend in this PR. So, I am converting this PR to a draft, and will re-create my fork with phpstan (which I was planning to do soon anyhow), and then create a new PR. It could take a couple of days. I will make sure to add your test case. |
@MarkBaker, I added the following test, and got the expected result: public function testINDIRECTEurUsd(): void
{
$sheet = $this->sheet;
$sheet->getCell('A1')->setValue('EUR');
$sheet->getCell('A2')->setValue('USD');
$sheet->getCell('B1')->setValue(360);
$sheet->getCell('B2')->setValue(300);
$this->spreadsheet->addNamedRange(new NamedRange('EUR', $sheet, '$B$1'));
$this->spreadsheet->addNamedRange(new NamedRange('USD', $sheet, '$B$2'));
$this->setCell('E1', '=INDIRECT("USD")');
$result = $sheet->getCell('E1')->getCalculatedValue();
self::assertSame(300, $result);
} So, I'm not sure why your test script thinks the result is wrong. |
There was an impression that code would fail the test in this commit.
I agree that I duplicate your test script's results when my code is not part of the formal test suite. At the moment, I have no idea why. |
The defined name value property has a leading equals sign which needs to be deal with. New push shortly. I still don't understand the mismatching behavior between phpunit test and your test. |
Failure pointed out by @MarkBaker. Prior code mysteriously succeeded as part of unit test, but failed as standalone code.
Yes, I'd just spotted the |
Also hitting discrepancies if I load from a saved file containing that basic defined name and indirect in a formula, although this is an issue with the spreadsheet name; this time because the sheet name is being prepended on the named range value, which already contains the sheet name. |
Similarly with this slightly more quirky variant that uses a dropdown selector in cell D1 to decide which Currency should be selected I feel like I'm being really evil here; but if I'm not evil now, then we'll have a user raising an issue about it in months time when the code is no longer fresh in our minds |
No problem about "evil". I will make sure your new test cases are covered when I re-create this PR. |
Mark's answer was correct. For details, see #1990 (comment)
It sounds like you might not be familiar with |
Thank you for the suggestion about rebase. I did not see it until too late, but I will attempt it the next time I want to resync. I have gotten Phpstan to accept the changes in my new push. |
Closing, replaced by #2004. |
* Improved Support for INDIRECT, ROW, and COLUMN Functions This should address issues #1913 and #1993. INDIRECT had heretofore not supported an optional parameter intended to support addresses in R1C1 format which was introduced with Excel 2010. It also had not supported the use of defined names as an argument. This PR is a replacement for #1995, which is currently in draft status and which I will close in a day or two. The ROW and COLUMN functions also should support defined names. I have added that, and test cases, with the latest push. ROWS and COLUMNS already supported it correctly, but there had been no test cases. Because ROW and COLUMN can return arrays, and PhpSpreadsheet does not support dynamic arrays, I left the existing direct-call tests unchanged to demonstrate those capabilities. The unit tests for INDIRECT had used mocking, and were sorely lacking (tested only error conditions). They have been replaced with normal, and hopefully adequate, tests. This includes testing globally defined names, as well as locally defined names, both in and out of scope. The test case in 1913 was too complicated for me to add as a unit test. The main impediments to it are now removed, and its complex situation will, I hope, be corrected with this fix. INDIRECT can also support a reference of the form Sheetname!localName when localName on its own would be out of scope. That functionality is added. It is also added, in theory, for ROW and COLUMN, however such a construction is rejected by the Calculation engine before passing control to ROW or COLUMN. It might be possible to change the engine to allow this, and I may want to look into that later, but it seems much too risky, and not nearly useful enough, to attempt to address that as part of this change. Several unusual test cases (leading equals sign, not-quite-as-expected name definition in file, complex indirection involving concatenation and a dropdown list) were suggested by @MarkBaker and are included in this request.
This should address issues #1913 and #1993. INDIRECT had heretofore not supported an optional parameter intended to support addresses in R1C1 format which was introduced with Excel 2010. It also had not supported the use of defined names as an argument.
The unit tests for INDIRECT had used mocking, and were sorely lacking (tested only error conditions). They have been replaced with normal, and hopefully adequate, tests. This includes testing globally defined names, as well as locally defined names, both in and out of scope.
The test case in 1913 was too complicated for me to add as a unit test. The main impediments to it are now removed, and its complex situation, will I hope, be corrected with this fix.
This is:
Checklist:
Why this change is needed?