Files
PhpSpreadsheet/tests/PhpSpreadsheetTests/Writer/PreCalcTest.php
T
oleibman 406988e53d SetCalculatedValue Avoid Casting String to Numeric (#3685)
Fix #3658. Readers will call `setCalculatedValue` to save the original calculated value for a cell that contains a formula; this affects the result of `getOldCalculatedValue`. The set routine automatically casts numeric values to float, which is not appropriate when the calculated value is explicitly set to a string type. An optional parameter is added to the set routine to indicate whether a cast is appropriate (default is true so userland uses of this method will remain unaffected). The readers for Xlsx, Xls, Xml, Ods, and Slk are all changed to set the new parameter appropriately. Gnumeric saves the formula, but does not appear to save the result, so no change is needed there. Html and Csv save the result but not the formula (for preCalculateFormulas true - they save the formula but not the result for false), so no change is needed for them.

It is not clear what the use case is for oldCalculatedValue, but it does date back to PhpExcel. A possible use case is demonstrated in the new tests - Excel function INFO is not implemented, so getCalculatedValue will generally return null on such a cell, but will return oldCalculatedValue when it sees that the function is not implemented but oldCalculatedValue has been set (typically by loading an existing spreadsheet), so the result that is on the spreadsheet will be available to the program which loaded it.

Reading a spreadsheet which has been created with preCalculateFormulas false will, or at least should, result in a null oldCalculatedValue. This was true for Xls, Xml, and Slk. Xlsx was actually storing 0 in this situation; that wasn't precisely a problem, but it wasn't necessary and seems misleading. Xlsx Writer is changed to no longer set a value in this situation. This is technically a break, but the existing code was wrong; one unit test needed a very minor modification as a result of this change.
2023-08-26 07:17:55 -07:00

209 lines
7.8 KiB
PHP

<?php
namespace PhpOffice\PhpSpreadsheetTests\Writer;
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
use PhpOffice\PhpSpreadsheet\IOFactory;
use PhpOffice\PhpSpreadsheet\Shared\File;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Worksheet\ColumnDimension;
use PhpOffice\PhpSpreadsheetTests\Functional\AbstractFunctional;
class PreCalcTest extends AbstractFunctional
{
/** @var string */
private $outfile = '';
protected function tearDown(): void
{
if ($this->outfile !== '') {
unlink($this->outfile);
$this->outfile = '';
}
}
public static function providerPreCalc(): array
{
return [
[true, 'Xlsx'],
[false, 'Xlsx'],
[null, 'Xlsx'],
[true, 'Xls'],
[false, 'Xls'],
[null, 'Xls'],
[true, 'Ods'],
[false, 'Ods'],
[null, 'Ods'],
[true, 'Html'],
[false, 'Html'],
[null, 'Html'],
[true, 'Csv'],
[false, 'Csv'],
[null, 'Csv'],
];
}
private static function autoSize(?ColumnDimension $columnDimension): void
{
if ($columnDimension === null) {
self::fail('Unable to getColumnDimension');
} else {
$columnDimension->setAutoSize(true);
}
}
private static function verifyA2(Calculation $calculation, string $title, ?bool $preCalc): void
{
$cellValue = 0;
// A2 has no cached calculation value if preCalc is false
if ($preCalc === false) {
self::assertFalse($calculation->getValueFromCache("$title!A2", $cellValue));
} else {
self::assertTrue($calculation->getValueFromCache("$title!A2", $cellValue));
self::assertSame(3, $cellValue);
}
}
private const AUTOSIZE_TYPES = ['Xlsx', 'Xls', 'Html', 'Ods'];
private static function verifyA3B2(Calculation $calculation, string $title, ?bool $preCalc, string $type): void
{
$cellValue = 0;
if (in_array($type, self::AUTOSIZE_TYPES) || $preCalc !== false) {
// These 3 types support auto-sizing.
// A3 has cached calculation value because it is used in B2 calculation
self::assertTrue($calculation->getValueFromCache("$title!A3", $cellValue));
self::assertSame(11, $cellValue);
// B2 has cached calculation value because its column is auto-sized
self::assertTrue($calculation->getValueFromCache("$title!B2", $cellValue));
self::assertSame(14, $cellValue);
} else {
self::assertFalse($calculation->getValueFromCache("$title!A3", $cellValue));
self::assertFalse($calculation->getValueFromCache("$title!B2", $cellValue));
}
}
private static function readFile(string $file): string
{
$dataOut = '';
$data = file_get_contents($file);
// confirm that file contains B2 pre-calculated or not as appropriate
if ($data === false) {
self::fail("Unable to read $file");
} else {
$dataOut = $data;
}
return $dataOut;
}
private function verifyXlsx(?bool $preCalc, string $type): void
{
if ($type === 'Xlsx') {
$file = 'zip://';
$file .= $this->outfile;
$file .= '#xl/worksheets/sheet1.xml';
$data = self::readFile($file);
// confirm that file contains B2 pre-calculated or not as appropriate
if ($preCalc === false) {
self::assertStringContainsString('<c r="B2" t="str"><f>3+A3</f></c>', $data);
} else {
self::assertStringContainsString('<c r="B2"><f>3+A3</f><v>14</v></c>', $data);
}
$file = 'zip://';
$file .= $this->outfile;
$file .= '#xl/workbook.xml';
$data = self::readFile($file);
// confirm whether workbook is set to recalculate
if ($preCalc === false) {
self::assertStringContainsString('<calcPr calcId="999999" calcMode="auto" calcCompleted="0" fullCalcOnLoad="1" forceFullCalc="1"/>', $data);
} else {
self::assertStringContainsString('<calcPr calcId="999999" calcMode="auto" calcCompleted="1" fullCalcOnLoad="0" forceFullCalc="0"/>', $data);
}
}
}
private function verifyOds(?bool $preCalc, string $type): void
{
if ($type === 'Ods') {
$file = 'zip://';
$file .= $this->outfile;
$file .= '#content.xml';
$data = self::readFile($file);
// confirm that file contains B2 pre-calculated or not as appropriate
if ($preCalc === false) {
self::assertStringContainsString('table:formula="of:=3+[.A3]" office:value-type="string" office:value="=3+A3"', $data);
} else {
self::assertStringContainsString(' table:formula="of:=3+[.A3]" office:value-type="float" office:value="14"', $data);
}
}
}
private function verifyHtml(?bool $preCalc, string $type): void
{
if ($type === 'Html') {
$data = self::readFile($this->outfile);
// confirm that file contains B2 pre-calculated or not as appropriate
if ($preCalc === false) {
self::assertStringContainsString('>=1+2</td>', $data);
self::assertStringContainsString('>=3+A3</td>', $data);
self::assertStringContainsString('>=5+6</td>', $data);
} else {
self::assertStringContainsString('>3</td>', $data);
self::assertStringContainsString('>14</td>', $data);
self::assertStringContainsString('>11</td>', $data);
}
}
}
private function verifyCsv(?bool $preCalc, string $type): void
{
if ($type === 'Csv') {
$data = self::readFile($this->outfile);
// confirm that file contains B2 pre-calculated or not as appropriate
if ($preCalc === false) {
self::assertStringContainsString('"=1+2"', $data);
self::assertStringContainsString('"=3+A3"', $data);
self::assertStringContainsString('"=5+6"', $data);
} else {
self::assertStringContainsString('"3"', $data);
self::assertStringContainsString('"14"', $data);
self::assertStringContainsString('"11"', $data);
}
}
}
/**
* @dataProvider providerPreCalc
*/
public function testPreCalc(?bool $preCalc, string $type): void
{
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$sheet->getCell('A1')->setValue('Column not set to autoSize');
$sheet->getCell('B1')->setValue('Column set to autoSize');
$sheet->getCell('A2')->setValue('=1+2');
$sheet->getCell('A3')->setValue('=5+6');
$sheet->getCell('B2')->setValue('=3+A3');
$columnDimension = $sheet->getColumnDimension('B');
self::autoSize($columnDimension);
$writer = IOFactory::createWriter($spreadsheet, $type);
if ($preCalc !== null) {
$writer->setPreCalculateFormulas($preCalc);
}
$this->outfile = File::temporaryFilename();
$writer->save($this->outfile);
$title = $sheet->getTitle();
$calculation = Calculation::getInstance($spreadsheet);
// verify values in Calculation cache
self::verifyA2($calculation, $title, $preCalc);
self::verifyA3B2($calculation, $title, $preCalc, $type);
// verify values in output file
$this->verifyXlsx($preCalc, $type);
$this->verifyOds($preCalc, $type);
$this->verifyHtml($preCalc, $type);
$this->verifyCsv($preCalc, $type);
}
}