fix: don't infer cell t attribute from formula source when pre-calc is off

writeCellFormula() falls back to using $cellValue (the formula source) as
$calculatedValue when getPreCalculateFormulas() is false. The source is
always a string, so the is_string($result) branch always fires, every
formula cell ends up with t="str" — even those whose formula evaluates
to a number or boolean.

Some readers (notably older LibreOffice and Gnumeric) treat t="str" as
a hint that the cell is a string, and either skip recomputation or
present a #NAME error for numeric formulas they would otherwise compute
correctly.

Fix: leave $calculatedValue null and $calculatedValueString empty when
pre-calc is off. The type-inference branches no longer fire, so no t
attribute is written. The surrounding writeElementIf already guards <v>
emission on getPreCalculateFormulas(), so no cached value is written
either — readers see "formula with no cached value, no type hint",
respect the workbook's fullCalcOnLoad="1", and recompute on open.

Updates the existing PreCalcTest assertion to match: <c r="B2"><f>3+A3</f></c>
instead of <c r="B2" t="str"><f>3+A3</f></c>.
This commit is contained in:
William Desportes
2026-04-29 19:27:57 +02:00
parent c0e328482a
commit 5eb433e08b
2 changed files with 15 additions and 3 deletions
+11 -2
View File
@@ -1576,7 +1576,13 @@ class Worksheet extends WriterPart
{
$attributes = $cell->getFormulaAttributes() ?? [];
$coordinate = $cell->getCoordinate();
$calculatedValue = $this->getParentWriter()->getPreCalculateFormulas() ? $cell->getCalculatedValue() : $cellValue;
$preCalc = $this->getParentWriter()->getPreCalculateFormulas();
// When pre-calc is off we have no calculated value to infer the cell type from. The
// previous fall-back of $cellValue (the formula source) made every formula cell write
// t="str" because the source is always a string — misleading for formulas that resolve
// to numbers/booleans. Leave $calculatedValue/$calculatedValueString null so the
// type-inference branches below are skipped and no t attribute is written.
$calculatedValue = $preCalc ? $cell->getCalculatedValue() : null;
if ($calculatedValue === ExcelError::SPILL()) {
$objWriter->writeAttribute('t', 'e');
//$objWriter->writeAttribute('cm', '1'); // already added
@@ -1592,7 +1598,10 @@ class Worksheet extends WriterPart
return;
}
$calculatedValueString = $this->getParentWriter()->getPreCalculateFormulas() ? $cell->getCalculatedValueString() : $cellValue;
// Empty string (not null) so str_starts_with($calculatedValueString, '#') below stays
// type-correct when pre-calc is off; the surrounding writeElementIf condition guards
// against actually emitting <v> when there is no calculated value.
$calculatedValueString = $preCalc ? $cell->getCalculatedValueString() : '';
$result = $calculatedValue;
while (is_array($result)) {
$result = array_shift($result);
@@ -108,7 +108,10 @@ class PreCalcTest extends AbstractFunctional
$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);
// No t="str" when pre-calc is off: with no calculated value to inspect, the
// writer can no longer infer the result type from the formula source string
// (which is always a string). Readers compute on open per fullCalcOnLoad.
self::assertStringContainsString('<c r="B2"><f>3+A3</f></c>', $data);
} else {
self::assertStringContainsString('<c r="B2"><f>3+A3</f><v>14</v></c>', $data);
}