Skip to content

Add forceFullCalc Option to Xlsx Writer #4271

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

Merged
merged 4 commits into from
Dec 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org).

### Fixed

- Add forceFullCalc option to Xlsx Writer. [Issue #4269](https://github.com/PHPOffice/PhpSpreadsheet/issues/4269) [PR #4271](https://github.com/PHPOffice/PhpSpreadsheet/pull/4271)
- More context options may be needed for http(s) image. [Php issue 17121](https://github.com/php/php-src/issues/17121) [PR #4276](https://github.com/PHPOffice/PhpSpreadsheet/pull/4276)
- Several fixed to ODS Writer. [Issue #4261](https://github.com/PHPOffice/PhpSpreadsheet/issues/4261) [PR #4263](https://github.com/PHPOffice/PhpSpreadsheet/pull/4263) [PR #4264](https://github.com/PHPOffice/PhpSpreadsheet/pull/4264) [PR #4266](https://github.com/PHPOffice/PhpSpreadsheet/pull/4266)

Expand Down
6 changes: 6 additions & 0 deletions docs/topics/reading-and-writing-to-file.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,12 @@ $writer->save("05featuredemo.xlsx");
**Note** Formulas will still be calculated in any column set to be autosized
even if pre-calculated is set to false

**Note** Prior to release 3.7.0, the use of this feature will cause Excel to be used in a mode where opening a sheet saved in this manner *might* not automatically recalculate a cell's formula when a cell used it the formula changes. Furthermore, that behavior might be applied to all spreadsheets open at the time. To avoid this behavior, add the following statement after `setPreCalculateFormulas` above:
```php
$writer->setForceFullCalc(false);
```
In a future release, the property's default may change to `false` and that statement may no longer be required.

#### Office 2003 compatibility pack

Because of a bug in the Office2003 compatibility pack, there can be some
Expand Down
20 changes: 19 additions & 1 deletion src/PhpSpreadsheet/Writer/Xlsx.php
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,8 @@ class Xlsx extends BaseWriter

private bool $useDynamicArray = false;

private ?bool $forceFullCalc = null;

/**
* Create a new Xlsx Writer.
*/
Expand Down Expand Up @@ -342,7 +344,7 @@ public function save($filename, int $flags = 0): void
$zipContent['xl/styles.xml'] = $this->getWriterPartStyle()->writeStyles($this->spreadSheet);

// Add workbook to ZIP file
$zipContent['xl/workbook.xml'] = $this->getWriterPartWorkbook()->writeWorkbook($this->spreadSheet, $this->preCalculateFormulas);
$zipContent['xl/workbook.xml'] = $this->getWriterPartWorkbook()->writeWorkbook($this->spreadSheet, $this->preCalculateFormulas, $this->forceFullCalc);

$chartCount = 0;
// Add worksheets
Expand Down Expand Up @@ -747,4 +749,20 @@ private function determineUseDynamicArrays(): void
{
$this->useDynamicArray = $this->preCalculateFormulas && Calculation::getInstance($this->spreadSheet)->getInstanceArrayReturnType() === Calculation::RETURN_ARRAY_AS_ARRAY && !$this->useCSEArrays;
}

/**
* If this is set when a spreadsheet is opened,
* values may not be automatically re-calculated,
* and a button will be available to force re-calculation.
* This may apply to all spreadsheets open at that time.
* If null, this will be set to the opposite of $preCalculateFormulas.
* It is likely that false is the desired setting, although
* cases have been reported where true is required (issue #456).
*/
public function setForceFullCalc(?bool $forceFullCalc): self
{
$this->forceFullCalc = $forceFullCalc;

return $this;
}
}
13 changes: 9 additions & 4 deletions src/PhpSpreadsheet/Writer/Xlsx/Workbook.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@ class Workbook extends WriterPart
* Write workbook to XML format.
*
* @param bool $preCalculateFormulas If true, formulas will be calculated before writing
* @param ?bool $forceFullCalc If null, !$preCalculateFormulas
*
* @return string XML Output
*/
public function writeWorkbook(Spreadsheet $spreadsheet, bool $preCalculateFormulas = false): string
public function writeWorkbook(Spreadsheet $spreadsheet, bool $preCalculateFormulas = false, ?bool $forceFullCalc = null): string
{
// Create XML writer
if ($this->getParentWriter()->getUseDiskCaching()) {
Expand Down Expand Up @@ -57,7 +58,7 @@ public function writeWorkbook(Spreadsheet $spreadsheet, bool $preCalculateFormul
(new DefinedNamesWriter($objWriter, $spreadsheet))->write();

// calcPr
$this->writeCalcPr($objWriter, $preCalculateFormulas);
$this->writeCalcPr($objWriter, $preCalculateFormulas, $forceFullCalc);

$objWriter->endElement();

Expand Down Expand Up @@ -148,7 +149,7 @@ private function writeWorkbookProtection(XMLWriter $objWriter, Spreadsheet $spre
*
* @param bool $preCalculateFormulas If true, formulas will be calculated before writing
*/
private function writeCalcPr(XMLWriter $objWriter, bool $preCalculateFormulas = true): void
private function writeCalcPr(XMLWriter $objWriter, bool $preCalculateFormulas, ?bool $forceFullCalc): void
{
$objWriter->startElement('calcPr');

Expand All @@ -160,7 +161,11 @@ private function writeCalcPr(XMLWriter $objWriter, bool $preCalculateFormulas =
// fullCalcOnLoad isn't needed if we will calculate before writing
$objWriter->writeAttribute('calcCompleted', ($preCalculateFormulas) ? '1' : '0');
$objWriter->writeAttribute('fullCalcOnLoad', ($preCalculateFormulas) ? '0' : '1');
$objWriter->writeAttribute('forceFullCalc', ($preCalculateFormulas) ? '0' : '1');
if ($forceFullCalc === null) {
$objWriter->writeAttribute('forceFullCalc', $preCalculateFormulas ? '0' : '1');
} else {
$objWriter->writeAttribute('forceFullCalc', $forceFullCalc ? '1' : '0');
}

$objWriter->endElement();
}
Expand Down
65 changes: 65 additions & 0 deletions tests/PhpSpreadsheetTests/Writer/Xlsx/Issue4269Test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php

declare(strict_types=1);

namespace PhpOffice\PhpSpreadsheetTests\Writer\Xlsx;

use PhpOffice\PhpSpreadsheet\Shared\File;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx as XlsxWriter;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;

class Issue4269Test extends TestCase
{
private string $outputFile = '';

protected function tearDown(): void
{
if ($this->outputFile !== '') {
unlink($this->outputFile);
$this->outputFile = '';
}
}

#[DataProvider('validationProvider')]
public function testWriteArrayFormulaTextJoin(
bool $preCalculateFormulas,
?bool $forceFullCalc,
string $expected
): void {
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$sheet->setCellValue('A1', '=A2*2');
$sheet->setCellValue('A2', 0);

$writer = new XlsxWriter($spreadsheet);
$writer->setPreCalculateFormulas($preCalculateFormulas);
if ($forceFullCalc !== null) {
$writer->setForceFullCalc($forceFullCalc);
}
$this->outputFile = File::temporaryFilename();
$writer->save($this->outputFile);
$spreadsheet->disconnectWorksheets();

$file = 'zip://';
$file .= $this->outputFile;
$file .= '#xl/workbook.xml';
$data = file_get_contents($file);
if ($data === false) {
self::fail('Unable to read file');
} else {
self::assertStringContainsString($expected, $data);
}
}

public static function validationProvider(): array
{
return [
'normal case' => [true, null, 'calcMode="auto" calcCompleted="1" fullCalcOnLoad="0" forceFullCalc="0"'],
'issue 456' => [false, null, 'calcMode="auto" calcCompleted="0" fullCalcOnLoad="1" forceFullCalc="1"'],
'better choice for no precalc' => [false, false, 'calcMode="auto" calcCompleted="0" fullCalcOnLoad="1" forceFullCalc="0"'],
'unlikely use case' => [true, true, 'calcMode="auto" calcCompleted="1" fullCalcOnLoad="0" forceFullCalc="1"'],
];
}
}
Loading